From c6c82fb04801ba0ea22eb7a787bfa41e79dfa04d Mon Sep 17 00:00:00 2001 From: Christof <106981178+chdorner-snyk@users.noreply.github.com> Date: Wed, 7 Feb 2024 14:40:43 +0100 Subject: [PATCH] feat: only send Amplitude analytics events when connected to an MT US environment [IDE-24] * fix: only send analytics when connected to US-based, non-FedRAMP MT environments * fix: app hostname when custom endpoint is fedramp This was driving the link in the settings page to enable Snyk Code, if the endpoint was set to a FedRAMP environment, this still linked to app.snyk.io. * fix: SnykCodeSettingsUrl edge cases and add tests * chore: use parametrized tests for `IsAnalyticsPermitted` --- CHANGELOG.md | 3 ++ Snyk.Common/Settings/ISnykOptions.cs | 2 ++ .../CLI/SnykConsoleRunner.cs | 2 +- .../Service/SnykService.cs | 2 +- .../Settings/SnykGeneralOptionsDialogPage.cs | 27 +++++++++++---- .../Toolwindow/SnykToolWindowControl.xaml.cs | 2 +- .../GeneralOptionsDialogPage.cs | 34 +++++++++++++++++++ .../SnykCliTest.cs | 4 +++ .../SnykConsoleRunnerTest.cs | 28 +++++++-------- 9 files changed, 80 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4838c0f36..81fa5fd34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Snyk Changelog +## [1.1.46] +- Only send Amplitude analytics events when connected to an MT US environment + ## [1.1.45] ### Changed diff --git a/Snyk.Common/Settings/ISnykOptions.cs b/Snyk.Common/Settings/ISnykOptions.cs index 506052232..ca11e4c5a 100644 --- a/Snyk.Common/Settings/ISnykOptions.cs +++ b/Snyk.Common/Settings/ISnykOptions.cs @@ -24,6 +24,8 @@ public interface ISnykOptions bool IsFedramp(); + bool IsAnalyticsPermitted(); + /// /// Gets or sets a value indicating whether CLI custom endpoint parameter. /// diff --git a/Snyk.VisualStudio.Extension.Shared/CLI/SnykConsoleRunner.cs b/Snyk.VisualStudio.Extension.Shared/CLI/SnykConsoleRunner.cs index 093684b5e..223ed3e88 100644 --- a/Snyk.VisualStudio.Extension.Shared/CLI/SnykConsoleRunner.cs +++ b/Snyk.VisualStudio.Extension.Shared/CLI/SnykConsoleRunner.cs @@ -78,7 +78,7 @@ public virtual Process CreateProcess(string fileName, string arguments, StringDi } } - if (!this.options.UsageAnalyticsEnabled || this.options.IsFedramp()) + if (!this.options.UsageAnalyticsEnabled || !this.options.IsAnalyticsPermitted()) { processStartInfo.EnvironmentVariables["SNYK_CFG_DISABLE_ANALYTICS"] = "1"; } diff --git a/Snyk.VisualStudio.Extension.Shared/Service/SnykService.cs b/Snyk.VisualStudio.Extension.Shared/Service/SnykService.cs index 2b57e103d..a423eee78 100644 --- a/Snyk.VisualStudio.Extension.Shared/Service/SnykService.cs +++ b/Snyk.VisualStudio.Extension.Shared/Service/SnykService.cs @@ -319,7 +319,7 @@ private void InitializeAnalyticsService() string anonymousId = this.Options.AnonymousId; - var enabled = this.Options.UsageAnalyticsEnabled && !this.Options.IsFedramp(); + var enabled = this.Options.UsageAnalyticsEnabled && this.Options.IsAnalyticsPermitted(); var endpoint = this.ApiEndpointResolver.UserMeEndpoint; Logger.Information("analytics enabled = {Enabled}, endpoint = {Endpoint}", enabled, endpoint); diff --git a/Snyk.VisualStudio.Extension.Shared/Settings/SnykGeneralOptionsDialogPage.cs b/Snyk.VisualStudio.Extension.Shared/Settings/SnykGeneralOptionsDialogPage.cs index c8ebc483f..43d447a7d 100644 --- a/Snyk.VisualStudio.Extension.Shared/Settings/SnykGeneralOptionsDialogPage.cs +++ b/Snyk.VisualStudio.Extension.Shared/Settings/SnykGeneralOptionsDialogPage.cs @@ -2,6 +2,7 @@ { using System; using System.IO; + using System.Linq; using System.Runtime.InteropServices; using System.Security.Authentication; using System.Threading.Tasks; @@ -111,6 +112,18 @@ public bool IsFedramp() return endpointUri.Host.ToLower().EndsWith("snykgov.io"); } + /// + /// Checks if the current endpoint permits sending external analytics events + /// + /// + public bool IsAnalyticsPermitted() + { + var endpointUri = new Uri(this.GetBaseAppURL()); + + string[] permittedHosts = ["app.snyk.io", "app.us.snyk.io"]; + return permittedHosts.Contains(endpointUri.Host.ToLower()); + } + /// /// Gets or sets a value indicating whether Custom endpoint. /// @@ -141,7 +154,7 @@ public string CustomEndpoint } /// - public string SnykCodeSettingsUrl => $"{this.GetAppCustomEndpoint()}/manage/snyk-code"; + public string SnykCodeSettingsUrl => $"{this.GetBaseAppURL()}/manage/snyk-code"; public SastSettings SastSettings { @@ -383,22 +396,22 @@ public bool Authenticate() private void FireSettingsChangedEvent() => this.SettingsChanged?.Invoke(this, new SnykSettingsChangedEventArgs()); - private string GetAppCustomEndpoint() + private string GetBaseAppURL() { var endpoint = this.customEndpoint.IsNullOrEmpty() ? "https://app.snyk.io" : this.customEndpoint.RemoveTrailingSlashes(); Uri uri = new Uri(endpoint); - if (!uri.Host.StartsWith("app") && uri.Host.EndsWith("snyk.io")) + if (!uri.Host.StartsWith("app") && (uri.Host.EndsWith("snyk.io") || uri.Host.EndsWith("snykgov.io"))) { - return endpoint.Replace("https://", "https://app.").RemoveFromEnd("api"); + return endpoint.Replace("https://", "https://app.").RemoveFromEnd("/api"); } - else if (uri.Host.StartsWith("app") && uri.Host.EndsWith("snyk.io")) + else if (uri.Host.StartsWith("app") && (uri.Host.EndsWith("snyk.io") || uri.Host.EndsWith("snykgov.io"))) { - return endpoint.RemoveFromEnd("api"); + return endpoint.RemoveFromEnd("/api"); } else { - return "https://app.snyk.io/"; + return "https://app.snyk.io"; } } diff --git a/Snyk.VisualStudio.Extension.Shared/UI/Toolwindow/SnykToolWindowControl.xaml.cs b/Snyk.VisualStudio.Extension.Shared/UI/Toolwindow/SnykToolWindowControl.xaml.cs index ec432889f..1dae6cca5 100644 --- a/Snyk.VisualStudio.Extension.Shared/UI/Toolwindow/SnykToolWindowControl.xaml.cs +++ b/Snyk.VisualStudio.Extension.Shared/UI/Toolwindow/SnykToolWindowControl.xaml.cs @@ -676,7 +676,7 @@ private void ShowWelcomeOrRunScanScreen() { var options = this.serviceProvider.Options; this.serviceProvider.AnalyticsService.AnalyticsEnabledOption = - options.UsageAnalyticsEnabled && !options.IsFedramp(); + options.UsageAnalyticsEnabled && options.IsAnalyticsPermitted(); if (options.ApiToken.IsValid()) { diff --git a/Snyk.VisualStudio.Extension.Tests/GeneralOptionsDialogPage.cs b/Snyk.VisualStudio.Extension.Tests/GeneralOptionsDialogPage.cs index 4ec3ff0ad..437b9d958 100644 --- a/Snyk.VisualStudio.Extension.Tests/GeneralOptionsDialogPage.cs +++ b/Snyk.VisualStudio.Extension.Tests/GeneralOptionsDialogPage.cs @@ -1,5 +1,7 @@ namespace Snyk.VisualStudio.Extension.Tests { + using System.Collections.Generic; + using System.Net; using Moq; using Snyk.VisualStudio.Extension.Shared.CLI; using Snyk.VisualStudio.Extension.Shared.Service; @@ -26,5 +28,37 @@ public void ApiEndpointChanged_InvalidatesCliToken() // Assert cliMock.Verify(mock => mock.UnsetApiToken()); } + + [Theory] + [InlineData("https://app.snyk.io/api", true)] + [InlineData("https://snyk.io/api", true)] + [InlineData("https://app.us.snyk.io/api", true)] + [InlineData("https://app.eu.snyk.io/api", false)] + [InlineData("https://app.au.snyk.io/api", false)] + [InlineData("https://app.snykgov.io/api", false)] + public void IsAnalyticsPermitted(string endpoint, bool expected) + { + var optionsDialogPage = new SnykGeneralOptionsDialogPage(); + optionsDialogPage.CustomEndpoint = endpoint; + Assert.Equal(expected, optionsDialogPage.IsAnalyticsPermitted()); + } + + [Theory] + [InlineData(null, "https://app.snyk.io/manage/snyk-code")] + [InlineData("", "https://app.snyk.io/manage/snyk-code")] + [InlineData("https://snyk.io", "https://app.snyk.io/manage/snyk-code")] + [InlineData("https://snyk.io/api", "https://app.snyk.io/manage/snyk-code")] + [InlineData("https://snyk.io/api/", "https://app.snyk.io/manage/snyk-code")] + [InlineData("https://snykgov.io/api", "https://app.snykgov.io/manage/snyk-code")] + [InlineData("https://app.snyk.io/api", "https://app.snyk.io/manage/snyk-code")] + [InlineData("https://app.eu.snyk.io/api", "https://app.eu.snyk.io/manage/snyk-code")] + [InlineData("https://app.snykgov.io/api", "https://app.snykgov.io/manage/snyk-code")] + [InlineData("https://example.org", "https://app.snyk.io/manage/snyk-code")] + public void SnykCodeSettingsUrl(string endpoint, string expected) + { + var optionsDialogPage = new SnykGeneralOptionsDialogPage(); + optionsDialogPage.CustomEndpoint = endpoint; + Assert.Equal(expected, optionsDialogPage.SnykCodeSettingsUrl); + } } } \ No newline at end of file diff --git a/Snyk.VisualStudio.Extension.Tests/SnykCliTest.cs b/Snyk.VisualStudio.Extension.Tests/SnykCliTest.cs index f5ad819ce..d33dff6b8 100644 --- a/Snyk.VisualStudio.Extension.Tests/SnykCliTest.cs +++ b/Snyk.VisualStudio.Extension.Tests/SnykCliTest.cs @@ -27,6 +27,10 @@ public SnykCliTest() .Setup(options => options.IsFedramp()) .Returns(false); + this.optionsMock + .Setup(options => options.IsAnalyticsPermitted()) + .Returns(true); + this.optionsMock .Setup(options => options.ApiToken) .Returns(AuthenticationToken.EmptyToken); diff --git a/Snyk.VisualStudio.Extension.Tests/SnykConsoleRunnerTest.cs b/Snyk.VisualStudio.Extension.Tests/SnykConsoleRunnerTest.cs index 7f9084bf9..c61ae2998 100644 --- a/Snyk.VisualStudio.Extension.Tests/SnykConsoleRunnerTest.cs +++ b/Snyk.VisualStudio.Extension.Tests/SnykConsoleRunnerTest.cs @@ -12,7 +12,7 @@ public class SnykConsoleRunnerTest { [Fact] - public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_Off_IsFedramp() + public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_Off_IsAnalyticsNotPermitted() { var optionsMock = new Mock(); optionsMock @@ -20,8 +20,8 @@ public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_Off_IsFedramp .Returns(false); optionsMock - .Setup(options => options.IsFedramp()) - .Returns(true); + .Setup(options => options.IsAnalyticsPermitted()) + .Returns(false); var cut = new SnykConsoleRunner(optionsMock.Object); @@ -31,7 +31,7 @@ public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_Off_IsFedramp } [Fact] - public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_Off_IsNotFedramp() + public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_Off_IsAnalyticsPermitted() { var optionsMock = new Mock(); optionsMock @@ -39,8 +39,8 @@ public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_Off_IsNotFedr .Returns(false); optionsMock - .Setup(options => options.IsFedramp()) - .Returns(false); + .Setup(options => options.IsAnalyticsPermitted()) + .Returns(true); var cut = new SnykConsoleRunner(optionsMock.Object); @@ -48,9 +48,9 @@ public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_Off_IsNotFedr Assert.Equal("1", process.StartInfo.EnvironmentVariables["SNYK_CFG_DISABLE_ANALYTICS"]); } - + [Fact] - public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_On_IsFedramp() + public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_On_IsAnalyticsPermitted() { var optionsMock = new Mock(); optionsMock @@ -58,18 +58,18 @@ public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_On_IsFedramp( .Returns(true); optionsMock - .Setup(options => options.IsFedramp()) + .Setup(options => options.IsAnalyticsPermitted()) .Returns(true); var cut = new SnykConsoleRunner(optionsMock.Object); var process = cut.CreateProcess("snyk", "test"); - Assert.Equal("1", process.StartInfo.EnvironmentVariables["SNYK_CFG_DISABLE_ANALYTICS"]); + Assert.Null(process.StartInfo.EnvironmentVariables["SNYK_CFG_DISABLE_ANALYTICS"]); } - + [Fact] - public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_On_IsNotFedramp() + public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_On_IsAnalyticsNotPermitted() { var optionsMock = new Mock(); optionsMock @@ -77,14 +77,14 @@ public void SnykConsoleRunnerTest_CreateProcess_Respects_Analytics_On_IsNotFedra .Returns(true); optionsMock - .Setup(options => options.IsFedramp()) + .Setup(options => options.IsAnalyticsPermitted()) .Returns(false); var cut = new SnykConsoleRunner(optionsMock.Object); var process = cut.CreateProcess("snyk", "test"); - Assert.Null(process.StartInfo.EnvironmentVariables["SNYK_CFG_DISABLE_ANALYTICS"]); + Assert.Equal("1", process.StartInfo.EnvironmentVariables["SNYK_CFG_DISABLE_ANALYTICS"]); } } } \ No newline at end of file