Skip to content

Commit

Permalink
chore: disable sentry and amplitude for fedramp instance (#446)
Browse files Browse the repository at this point in the history
* 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 🤷🏼
  • Loading branch information
bastiandoetsch committed Aug 31, 2023
1 parent 0de998b commit 013d0d6
Show file tree
Hide file tree
Showing 10 changed files with 119 additions and 27 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Snyk Changelog

## [2.5.1]

### Changed

- Disable analytics and error reporting for snykgov.io domain

## [2.5.0]

### Changed
Expand Down
6 changes: 4 additions & 2 deletions src/main/kotlin/io/snyk/plugin/cli/ConsoleCommandRunner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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"
}

Expand Down
27 changes: 15 additions & 12 deletions src/main/kotlin/io/snyk/plugin/services/SnykAnalyticsService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ""
Expand All @@ -161,7 +164,7 @@ class SnykAnalyticsService : Disposable {
}

fun identify() {
if (!settings.usageAnalyticsEnabled || userId.isBlank()) {
if (!settings.usageAnalyticsEnabled || isFedramp() || userId.isBlank()) {
return
}

Expand All @@ -171,15 +174,15 @@ class SnykAnalyticsService : Disposable {
}

fun logWelcomeIsViewed(event: WelcomeIsViewed) {
if (!settings.usageAnalyticsEnabled) return
if (!settings.usageAnalyticsEnabled || isFedramp()) return

catchAll(log, "welcomeIsViewed") {
itly.logWelcomeIsViewed(userId, event)
}
}

fun logAnalysisIsTriggered(event: AnalysisIsTriggered) {
if (!settings.usageAnalyticsEnabled || userId.isBlank()) {
if (!settings.usageAnalyticsEnabled || isFedramp() || userId.isBlank()) {
return
}

Expand All @@ -189,7 +192,7 @@ class SnykAnalyticsService : Disposable {
}

private fun logAnalysisIsReady(event: AnalysisIsReady) {
if (!settings.usageAnalyticsEnabled || userId.isBlank()) {
if (!settings.usageAnalyticsEnabled || isFedramp() || userId.isBlank()) {
return
}

Expand All @@ -199,7 +202,7 @@ class SnykAnalyticsService : Disposable {
}

fun logIssueInTreeIsClicked(event: IssueInTreeIsClicked) {
if (!settings.usageAnalyticsEnabled || userId.isBlank()) {
if (!settings.usageAnalyticsEnabled || isFedramp() || userId.isBlank()) {
return
}

Expand All @@ -209,7 +212,7 @@ class SnykAnalyticsService : Disposable {
}

fun logHealthScoreIsClicked(event: HealthScoreIsClicked) {
if (!settings.usageAnalyticsEnabled || userId.isBlank()) {
if (!settings.usageAnalyticsEnabled || isFedramp() || userId.isBlank()) {
return
}

Expand All @@ -219,39 +222,39 @@ class SnykAnalyticsService : Disposable {
}

fun logPluginIsInstalled(event: PluginIsInstalled) {
if (!settings.usageAnalyticsEnabled) return
if (!settings.usageAnalyticsEnabled || isFedramp()) return

catchAll(log, "pluginIsInstalled") {
itly.logPluginIsInstalled(userId, event)
}
}

fun logPluginIsUninstalled(event: PluginIsUninstalled) {
if (!settings.usageAnalyticsEnabled) return
if (!settings.usageAnalyticsEnabled || isFedramp()) return

catchAll(log, "pluginIsUninstalled") {
itly.logPluginIsUninstalled(userId, event)
}
}

fun logAuthenticateButtonIsClicked(event: AuthenticateButtonIsClicked) {
if (!settings.usageAnalyticsEnabled) return
if (!settings.usageAnalyticsEnabled || isFedramp()) return

catchAll(log, "authenticateButtonIsClicked") {
itly.logAuthenticateButtonIsClicked(userId, event)
}
}

fun logQuickFixIsDisplayed(event: QuickFixIsDisplayed) {
if (!settings.usageAnalyticsEnabled) return
if (!settings.usageAnalyticsEnabled || isFedramp()) return

catchAll(log, "quickFixIsDisplayed") {
itly.logQuickFixIsDisplayed(userId, event)
}
}

fun logQuickFixIsTriggered(event: QuickFixIsTriggered) {
if (!settings.usageAnalyticsEnabled) return
if (!settings.usageAnalyticsEnabled || isFedramp()) return

catchAll(log, "quickFixIsTriggered") {
itly.logQuickFixIsTriggered(userId, event)
Expand Down
9 changes: 8 additions & 1 deletion src/main/kotlin/snyk/amplitude/AmplitudeExperimentService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
40 changes: 32 additions & 8 deletions src/main/kotlin/snyk/common/CustomEndpoints.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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("/+$"), "")
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/snyk/errorHandler/SentryErrorReporter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/test/kotlin/io/snyk/plugin/cli/ConsoleCommandRunnerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 13 additions & 2 deletions src/test/kotlin/snyk/amplitude/AmplitudeExperimentServiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ class AmplitudeExperimentServiceTest {
unmockkAll()
mockkStatic("io.snyk.plugin.UtilsKt")
every { pluginSettings() } returns SnykApplicationSettingsStateService()
cut = AmplitudeExperimentService()
cut.setApiClient(amplitudeApiClientMock)
user = ExperimentUser("testUser")
}

Expand All @@ -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) }
}
}
13 changes: 12 additions & 1 deletion src/test/kotlin/snyk/common/CustomEndpointsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand Down Expand Up @@ -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())
}
}
12 changes: 12 additions & 0 deletions src/test/kotlin/snyk/errorHandler/SentryErrorReporterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 013d0d6

Please sign in to comment.