From 013d0d66bb7a90d70aebe6367d6abf404736f571 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Thu, 31 Aug 2023 18:31:27 +0200 Subject: [PATCH] chore: disable sentry and amplitude for fedramp instance (#446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: disable amplitude and sentry for fedramp [HEAD-676] * chore: tests * chore: moa tests * fix: merge errors * chore: change check for fedramp to rely on the snykgov.io domain only * docs: update CHANGELOG.md * chore: make linter happy * chore: another linty fix * chore: indent how detekt wants it 🤷🏼 --- CHANGELOG.md | 6 +++ .../snyk/plugin/cli/ConsoleCommandRunner.kt | 6 ++- .../plugin/services/SnykAnalyticsService.kt | 27 +++++++------ .../amplitude/AmplitudeExperimentService.kt | 9 ++++- .../kotlin/snyk/common/CustomEndpoints.kt | 40 +++++++++++++++---- .../snyk/errorHandler/SentryErrorReporter.kt | 3 +- .../plugin/cli/ConsoleCommandRunnerTest.kt | 15 +++++++ .../AmplitudeExperimentServiceTest.kt | 15 ++++++- .../kotlin/snyk/common/CustomEndpointsTest.kt | 13 +++++- .../errorHandler/SentryErrorReporterTest.kt | 12 ++++++ 10 files changed, 119 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6703dd556..6adac01c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Snyk Changelog +## [2.5.1] + +### Changed + +- Disable analytics and error reporting for snykgov.io domain + ## [2.5.0] ### Changed diff --git a/src/main/kotlin/io/snyk/plugin/cli/ConsoleCommandRunner.kt b/src/main/kotlin/io/snyk/plugin/cli/ConsoleCommandRunner.kt index 5f785ef79..e75d1d5cc 100644 --- a/src/main/kotlin/io/snyk/plugin/cli/ConsoleCommandRunner.kt +++ b/src/main/kotlin/io/snyk/plugin/cli/ConsoleCommandRunner.kt @@ -16,6 +16,7 @@ import io.snyk.plugin.getWaitForResultsTimeout import io.snyk.plugin.pluginSettings import io.snyk.plugin.ui.SnykBalloonNotificationHelper import snyk.common.getEndpointUrl +import snyk.common.isFedramp import snyk.common.isOauth import snyk.errorHandler.SentryErrorReporter import snyk.pluginInfo @@ -95,7 +96,8 @@ open class ConsoleCommandRunner { val oauthEnvVar = "INTERNAL_OAUTH_TOKEN_STORAGE" val snykTokenEnvVar = "SNYK_TOKEN" - val oauthEnabled = URI(endpoint).isOauth() + val endpointURI = URI(endpoint) + val oauthEnabled = endpointURI.isOauth() if (oauthEnabled) { commandLine.environment[oauthEnabledEnvVar] = "1" commandLine.environment.remove(snykTokenEnvVar) @@ -114,7 +116,7 @@ open class ConsoleCommandRunner { commandLine.environment["SNYK_API"] = endpoint - if (!pluginSettings().usageAnalyticsEnabled) { + if (!pluginSettings().usageAnalyticsEnabled || endpointURI.isFedramp()) { commandLine.environment["SNYK_CFG_DISABLE_ANALYTICS"] = "1" } diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykAnalyticsService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykAnalyticsService.kt index f51a1482a..ae3f633f8 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykAnalyticsService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykAnalyticsService.kt @@ -21,6 +21,7 @@ import snyk.analytics.QuickFixIsDisplayed import snyk.analytics.QuickFixIsTriggered import snyk.analytics.WelcomeIsViewed import snyk.common.SnykError +import snyk.common.isFedramp import snyk.container.ContainerResult import snyk.iac.IacResult import snyk.oss.OssResult @@ -145,9 +146,11 @@ class SnykAnalyticsService : Disposable { } fun obtainUserId(token: String?): String { - if (!settings.usageAnalyticsEnabled) { + if (!settings.usageAnalyticsEnabled || isFedramp()) { + log.warn("Token is null or empty, or analytics disabled. User public id will not be obtained.") return "" } + if (token.isNullOrBlank()) { log.warn("Token is null or empty, user public id will not be obtained.") return "" @@ -161,7 +164,7 @@ class SnykAnalyticsService : Disposable { } fun identify() { - if (!settings.usageAnalyticsEnabled || userId.isBlank()) { + if (!settings.usageAnalyticsEnabled || isFedramp() || userId.isBlank()) { return } @@ -171,7 +174,7 @@ class SnykAnalyticsService : Disposable { } fun logWelcomeIsViewed(event: WelcomeIsViewed) { - if (!settings.usageAnalyticsEnabled) return + if (!settings.usageAnalyticsEnabled || isFedramp()) return catchAll(log, "welcomeIsViewed") { itly.logWelcomeIsViewed(userId, event) @@ -179,7 +182,7 @@ class SnykAnalyticsService : Disposable { } fun logAnalysisIsTriggered(event: AnalysisIsTriggered) { - if (!settings.usageAnalyticsEnabled || userId.isBlank()) { + if (!settings.usageAnalyticsEnabled || isFedramp() || userId.isBlank()) { return } @@ -189,7 +192,7 @@ class SnykAnalyticsService : Disposable { } private fun logAnalysisIsReady(event: AnalysisIsReady) { - if (!settings.usageAnalyticsEnabled || userId.isBlank()) { + if (!settings.usageAnalyticsEnabled || isFedramp() || userId.isBlank()) { return } @@ -199,7 +202,7 @@ class SnykAnalyticsService : Disposable { } fun logIssueInTreeIsClicked(event: IssueInTreeIsClicked) { - if (!settings.usageAnalyticsEnabled || userId.isBlank()) { + if (!settings.usageAnalyticsEnabled || isFedramp() || userId.isBlank()) { return } @@ -209,7 +212,7 @@ class SnykAnalyticsService : Disposable { } fun logHealthScoreIsClicked(event: HealthScoreIsClicked) { - if (!settings.usageAnalyticsEnabled || userId.isBlank()) { + if (!settings.usageAnalyticsEnabled || isFedramp() || userId.isBlank()) { return } @@ -219,7 +222,7 @@ class SnykAnalyticsService : Disposable { } fun logPluginIsInstalled(event: PluginIsInstalled) { - if (!settings.usageAnalyticsEnabled) return + if (!settings.usageAnalyticsEnabled || isFedramp()) return catchAll(log, "pluginIsInstalled") { itly.logPluginIsInstalled(userId, event) @@ -227,7 +230,7 @@ class SnykAnalyticsService : Disposable { } fun logPluginIsUninstalled(event: PluginIsUninstalled) { - if (!settings.usageAnalyticsEnabled) return + if (!settings.usageAnalyticsEnabled || isFedramp()) return catchAll(log, "pluginIsUninstalled") { itly.logPluginIsUninstalled(userId, event) @@ -235,7 +238,7 @@ class SnykAnalyticsService : Disposable { } fun logAuthenticateButtonIsClicked(event: AuthenticateButtonIsClicked) { - if (!settings.usageAnalyticsEnabled) return + if (!settings.usageAnalyticsEnabled || isFedramp()) return catchAll(log, "authenticateButtonIsClicked") { itly.logAuthenticateButtonIsClicked(userId, event) @@ -243,7 +246,7 @@ class SnykAnalyticsService : Disposable { } fun logQuickFixIsDisplayed(event: QuickFixIsDisplayed) { - if (!settings.usageAnalyticsEnabled) return + if (!settings.usageAnalyticsEnabled || isFedramp()) return catchAll(log, "quickFixIsDisplayed") { itly.logQuickFixIsDisplayed(userId, event) @@ -251,7 +254,7 @@ class SnykAnalyticsService : Disposable { } fun logQuickFixIsTriggered(event: QuickFixIsTriggered) { - if (!settings.usageAnalyticsEnabled) return + if (!settings.usageAnalyticsEnabled || isFedramp()) return catchAll(log, "quickFixIsTriggered") { itly.logQuickFixIsTriggered(userId, event) diff --git a/src/main/kotlin/snyk/amplitude/AmplitudeExperimentService.kt b/src/main/kotlin/snyk/amplitude/AmplitudeExperimentService.kt index 4c20d4fe7..8be818699 100644 --- a/src/main/kotlin/snyk/amplitude/AmplitudeExperimentService.kt +++ b/src/main/kotlin/snyk/amplitude/AmplitudeExperimentService.kt @@ -3,11 +3,13 @@ package snyk.amplitude import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.logger +import io.snyk.plugin.pluginSettings import org.jetbrains.annotations.TestOnly import snyk.amplitude.api.AmplitudeExperimentApiClient import snyk.amplitude.api.AmplitudeExperimentApiClient.Defaults.FALLBACK_VARIANT import snyk.amplitude.api.ExperimentUser import snyk.amplitude.api.Variant +import snyk.common.isFedramp import java.io.IOException import java.util.Properties import java.util.concurrent.ConcurrentHashMap @@ -28,7 +30,12 @@ class AmplitudeExperimentService : Disposable { val prop = Properties() prop.load(javaClass.classLoader.getResourceAsStream("application.properties")) val apiKey = prop.getProperty("amplitude.experiment.api-key") ?: "" - apiClient = AmplitudeExperimentApiClient.create(apiKey = apiKey) + val settings = pluginSettings() + if (settings.usageAnalyticsEnabled && !isFedramp()) { + apiClient = AmplitudeExperimentApiClient.create(apiKey = apiKey) + } else { + LOG.debug("Amplitude experiment disabled.") + } } catch (e: IllegalArgumentException) { LOG.warn("Property file contains a malformed Unicode escape sequence", e) } catch (e: IOException) { diff --git a/src/main/kotlin/snyk/common/CustomEndpoints.kt b/src/main/kotlin/snyk/common/CustomEndpoints.kt index 778144990..558b3742f 100644 --- a/src/main/kotlin/snyk/common/CustomEndpoints.kt +++ b/src/main/kotlin/snyk/common/CustomEndpoints.kt @@ -84,26 +84,50 @@ internal fun resolveCustomEndpoint(endpointUrl: String?): String { } fun URI.isSnykTenant() = - isSnykDomain() && (host.startsWith("app.") || host == "snyk.io" || isDev()) && path.endsWith("/api") + isSnykDomain() && + path.lowercase().endsWith("/api") && + ( + host.lowercase().startsWith("app.") || + host.lowercase() == "snyk.io" || + isDev() + ) -fun URI.isSnykApi() = isSnykDomain() && (host.startsWith("api.") || path.endsWith("/api")) +fun URI.isSnykApi() = isSnykDomain() && (host.lowercase().startsWith("api.") || path.lowercase().endsWith("/api")) fun URI.toSnykAPIv1(): URI { - val host = host + val host = host.lowercase() .replaceFirst("app.", "api.") .replaceFirst("deeproxy.", "api.") - .prefixIfNot("api.") + .prefixIfNot( + "api." + ) return URI(scheme, host, "/v1/", null) } -fun URI.isSnykDomain() = host != null && (host.endsWith("snyk.io") || host.endsWith("snykgov.io")) +fun URI.isSnykDomain() = host != null && + ( + host.lowercase().endsWith(".snyk.io") || + host.lowercase() == "snyk.io" || + host.lowercase().endsWith(".snykgov.io") + ) -fun URI.isDeeproxy() = isSnykDomain() && host.startsWith("deeproxy.") +fun URI.isDeeproxy() = isSnykDomain() && host.lowercase().startsWith("deeproxy.") -fun URI.isOauth() = host != null && host.endsWith(".snykgov.io") +fun URI.isSnykGov() = host != null && host.lowercase().endsWith(".snykgov.io") -fun URI.isDev() = isSnykDomain() && host.startsWith("dev.") +fun URI.isOauth() = isSnykGov() + +fun URI.isDev() = isSnykDomain() && host.lowercase().startsWith("dev.") + +fun URI.isFedramp() = isSnykGov() + +fun isFedramp(): Boolean { + val settings = pluginSettings() + return settings.customEndpointUrl + ?.let { URI(it) } + ?.isFedramp() ?: false +} internal fun String.removeTrailingSlashesIfPresent(): String { val candidate = this.replace(Regex("/+$"), "") diff --git a/src/main/kotlin/snyk/errorHandler/SentryErrorReporter.kt b/src/main/kotlin/snyk/errorHandler/SentryErrorReporter.kt index 40ea0dbac..2109e8a7d 100644 --- a/src/main/kotlin/snyk/errorHandler/SentryErrorReporter.kt +++ b/src/main/kotlin/snyk/errorHandler/SentryErrorReporter.kt @@ -18,6 +18,7 @@ import io.sentry.protocol.SentryId import io.sentry.protocol.SentryRuntime import io.snyk.plugin.pluginSettings import snyk.PropertyLoader +import snyk.common.isFedramp import snyk.pluginInfo object SentryErrorReporter { @@ -108,7 +109,7 @@ object SentryErrorReporter { if (ApplicationManager.getApplication().isUnitTestMode) return SentryId.EMPTY_ID val settings = pluginSettings() - return if (settings.crashReportingEnabled) { + return if (settings.crashReportingEnabled && !isFedramp()) { val sentryId = Sentry.captureException(throwable) LOG.info("Sentry event reported: $sentryId") sentryId diff --git a/src/test/kotlin/io/snyk/plugin/cli/ConsoleCommandRunnerTest.kt b/src/test/kotlin/io/snyk/plugin/cli/ConsoleCommandRunnerTest.kt index 2c17a426c..6c97ee997 100644 --- a/src/test/kotlin/io/snyk/plugin/cli/ConsoleCommandRunnerTest.kt +++ b/src/test/kotlin/io/snyk/plugin/cli/ConsoleCommandRunnerTest.kt @@ -75,6 +75,21 @@ class ConsoleCommandRunnerTest : LightPlatformTestCase() { } } + fun testSetupCliEnvironmentVariablesWithFedrampCustomEndpoint() { + val oldEndpoint = pluginSettings().customEndpointUrl + try { + val generalCommandLine = GeneralCommandLine("") + val expectedEndpoint = "https://api.fedramp.snykgov.io/" + pluginSettings().customEndpointUrl = expectedEndpoint + + ConsoleCommandRunner().setupCliEnvironmentVariables(generalCommandLine, "") + + assertEquals("1", generalCommandLine.environment["SNYK_CFG_DISABLE_ANALYTICS"]) + } finally { + pluginSettings().customEndpointUrl = oldEndpoint + } + } + fun testSetupCliEnvironmentVariablesWithOAuthEndpoint() { val oldEndpoint = pluginSettings().customEndpointUrl try { diff --git a/src/test/kotlin/snyk/amplitude/AmplitudeExperimentServiceTest.kt b/src/test/kotlin/snyk/amplitude/AmplitudeExperimentServiceTest.kt index a7094ceee..646c6a509 100644 --- a/src/test/kotlin/snyk/amplitude/AmplitudeExperimentServiceTest.kt +++ b/src/test/kotlin/snyk/amplitude/AmplitudeExperimentServiceTest.kt @@ -23,8 +23,6 @@ class AmplitudeExperimentServiceTest { unmockkAll() mockkStatic("io.snyk.plugin.UtilsKt") every { pluginSettings() } returns SnykApplicationSettingsStateService() - cut = AmplitudeExperimentService() - cut.setApiClient(amplitudeApiClientMock) user = ExperimentUser("testUser") } @@ -37,8 +35,21 @@ class AmplitudeExperimentServiceTest { fun `fetch should call amplitude for data with the userId`() { every { amplitudeApiClientMock.allVariants(any()) } returns emptyMap() + cut = AmplitudeExperimentService() + cut.setApiClient(amplitudeApiClientMock) cut.fetch(user) verify(exactly = 1) { amplitudeApiClientMock.allVariants(user) } } + + @Test + fun `fetch should not call amplitude for data with the userId when fedramp`() { + every { amplitudeApiClientMock.allVariants(any()) } returns emptyMap() + pluginSettings().customEndpointUrl = "https://api.fedramp.snykgov.io" + cut = AmplitudeExperimentService() + + cut.fetch(user) + + verify(exactly = 0) { amplitudeApiClientMock.allVariants(user) } + } } diff --git a/src/test/kotlin/snyk/common/CustomEndpointsTest.kt b/src/test/kotlin/snyk/common/CustomEndpointsTest.kt index e5ea49c6b..09e5a4d80 100644 --- a/src/test/kotlin/snyk/common/CustomEndpointsTest.kt +++ b/src/test/kotlin/snyk/common/CustomEndpointsTest.kt @@ -119,7 +119,7 @@ class CustomEndpointsTest { @Test fun `isSnykAPI false for api subdomain and not snyk domain`() { - val uri = URI("https://api.NOTSNYK.io") + val uri = URI("https://api.notsnyk.io") assertFalse(uri.isSnykApi()) } @@ -163,4 +163,15 @@ class CustomEndpointsTest { assertTrue(uri.isOauth()) } + @Test + fun `isFedramp true for the right URI`() { + val uri = URI("https://app.fedramp.snykgov.io") + assertTrue(uri.isFedramp()) + } + + @Test + fun `isFedramp false for the right URI`() { + val uri = URI("https://app.fedddramp.snykagov.io") + assertFalse(uri.isFedramp()) + } } diff --git a/src/test/kotlin/snyk/errorHandler/SentryErrorReporterTest.kt b/src/test/kotlin/snyk/errorHandler/SentryErrorReporterTest.kt index 1e10eca2c..a76bfb292 100644 --- a/src/test/kotlin/snyk/errorHandler/SentryErrorReporterTest.kt +++ b/src/test/kotlin/snyk/errorHandler/SentryErrorReporterTest.kt @@ -42,6 +42,18 @@ class SentryErrorReporterTest { verify(exactly = 1) { Sentry.captureException(any()) } } + @Test + fun `captureException should not send exceptions to Sentry when crashReportingEnabled is true and fedramp`() { + val settings = mockPluginInformation() + setUnitTesting(false) + settings.crashReportingEnabled = true + settings.customEndpointUrl = "https://api.fedramp.snykgov.io" + + SentryErrorReporter.captureException(RuntimeException("test")) + + verify(exactly = 0) { Sentry.captureException(any()) } + } + @Test fun `captureException should not send exceptions to Sentry when crashReportingEnabled is false`() { val settings = mockPluginInformation()