From 86c05faa0b67401b6c7b2cc5bc85e8b52bead1fc Mon Sep 17 00:00:00 2001 From: Nick Yasnohorodskyi Date: Fri, 14 Aug 2026 14:16:03 +0200 Subject: [PATCH 1/7] feat: wire live Configuration into analytics instrumentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sendInstrumentation now passes analytics.WithConfiguration(eng.GetConfiguration()) alongside the existing WithLogger option, so GetV2InstrumentationObject's new shape-based secret scrubbing (go-application-framework#704) runs on real CLI invocations, not just library callers who opt in manually. Blocked from building on this branch alone until go-application-framework#704 merges and is tagged, then go.mod is bumped for real — verified locally against a temporary `replace github.com/snyk/go-application-framework => ../../go-application-framework` pointing at that PR's branch, not included in this commit. --- cliv2/pkg/core/instrumentation.go | 2 +- cliv2/pkg/core/instrumentation_test.go | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/cliv2/pkg/core/instrumentation.go b/cliv2/pkg/core/instrumentation.go index 0560d9dc77..f721a7283e 100644 --- a/cliv2/pkg/core/instrumentation.go +++ b/cliv2/pkg/core/instrumentation.go @@ -133,7 +133,7 @@ func sendInstrumentation(ctx context.Context, eng workflow.Engine, instrumentor } logger.Print("Sending Instrumentation") - data, err := analytics.GetV2InstrumentationObject(instrumentor, analytics.WithLogger(logger)) + data, err := analytics.GetV2InstrumentationObject(instrumentor, analytics.WithLogger(logger), analytics.WithConfiguration(eng.GetConfiguration())) if err != nil { logger.Err(err).Msg("Failed to derive data object") } diff --git a/cliv2/pkg/core/instrumentation_test.go b/cliv2/pkg/core/instrumentation_test.go index e3657fabb9..e722fe7851 100644 --- a/cliv2/pkg/core/instrumentation_test.go +++ b/cliv2/pkg/core/instrumentation_test.go @@ -1,10 +1,15 @@ package core import ( + "context" "testing" + "github.com/golang/mock/gomock" + "github.com/rs/zerolog" "github.com/snyk/go-application-framework/pkg/analytics" "github.com/snyk/go-application-framework/pkg/configuration" + localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows" + "github.com/snyk/go-application-framework/pkg/mocks" "github.com/stretchr/testify/assert" ) @@ -27,6 +32,24 @@ func Test_shallSendInstrumentation(t *testing.T) { assert.False(t, actual) } +func Test_sendInstrumentation_passesEngineConfigurationToInstrumentationObject(t *testing.T) { + globalConfiguration = configuration.NewWithOpts(configuration.WithAutomaticEnv()) + + mockController := gomock.NewController(t) + mockEngine := mocks.NewMockEngine(mockController) + + // One call from shallSendInstrumentation, one to derive analytics.WithConfiguration. + // If the call site regresses to only passing WithLogger, this expectation goes unmet. + engineConfig := configuration.NewWithOpts(configuration.WithAutomaticEnv()) + mockEngine.EXPECT().GetConfiguration().Return(engineConfig).Times(2) + mockEngine.EXPECT().Invoke(localworkflows.WORKFLOWID_REPORT_ANALYTICS, gomock.Any(), gomock.Any()).Return(nil, nil) + + instrumentor := analytics.NewInstrumentationCollector() + logger := zerolog.Nop() + + sendInstrumentation(context.Background(), mockEngine, instrumentor, &logger) +} + func Test_addClientMachineId(t *testing.T) { t.Run("emits studio::client_machine_id when INTERNAL_SNYK_CLIENT_MACHINE_ID env var is set", func(t *testing.T) { // Mirrors how Studio sets the env var before exec'ing the snyk binary From 9961926bfa9ac1c5feddb3abaadc137cd6d21739 Mon Sep 17 00:00:00 2001 From: Nick Yasnohorodskyi Date: Fri, 14 Aug 2026 16:17:46 +0200 Subject: [PATCH 2/7] feat: populate REDACTION_TERMS unconditionally at teardown Extracts the debug-gated unknown-arg/env-value computation into populateRedactionTerms and calls it regardless of debugEnabled, setting configuration.REDACTION_TERMS so the analytics scrub chokepoint can see these terms even on non-debug runs. writeLogHeader/AddTermsToReplace keep their exact existing debugEnabled gate. --- cliv2/pkg/core/main.go | 19 ++++++++++++++++--- cliv2/pkg/core/main_test.go | 17 +++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/cliv2/pkg/core/main.go b/cliv2/pkg/core/main.go index bc832ba635..cbbd37fea7 100644 --- a/cliv2/pkg/core/main.go +++ b/cliv2/pkg/core/main.go @@ -637,12 +637,13 @@ func mainWithErrorCode(additionalExts []workflow.ExtensionInit) int { // init engine err = globalEngine.Init() + // Unconditional so the analytics scrub chokepoint (which reads configuration.REDACTION_TERMS + // off of config) sees these terms even on non-debug runs, not just when the debug log itself is scrubbed. + termsToRedact := populateRedactionTerms(globalConfiguration, globalEngine) + // We want to scrub the debug log of sensitive information. Since we have a list of commands we know can occur, we can intersect that with arguments we don't recognize, and automatically scrub all those from the logs. if debugEnabled { writeLogHeader(globalConfiguration, networkAccess) - knownTerms, _ := instrumentation.GetKnownCommandsAndFlags(globalEngine) - knownTerms = append(knownTerms, globalConfiguration.GetString(configuration.API_URL), globalConfiguration.GetString(configuration.ORGANIZATION), globalConfiguration.GetString(configuration.ORGANIZATION_SLUG)) - termsToRedact := cliv2utils.GetUnknownParameters(os.Args[1:], os.Environ(), knownTerms) scrubbedLogger.AddTermsToReplace(termsToRedact) } @@ -714,6 +715,18 @@ func mainWithErrorCode(additionalExts []workflow.ExtensionInit) int { return finalExitCode } +// populateRedactionTerms computes likely-secret literal values (unrecognized CLI +// arguments and environment variables) and records them on config under +// configuration.REDACTION_TERMS, so the analytics scrub chokepoint can redact +// them regardless of whether debug logging is enabled. +func populateRedactionTerms(config configuration.Configuration, engine workflow.Engine) []string { + knownTerms, _ := instrumentation.GetKnownCommandsAndFlags(engine) + knownTerms = append(knownTerms, config.GetString(configuration.API_URL), config.GetString(configuration.ORGANIZATION), config.GetString(configuration.ORGANIZATION_SLUG)) + termsToRedact := cliv2utils.GetUnknownParameters(os.Args[1:], os.Environ(), knownTerms) + config.Set(configuration.REDACTION_TERMS, termsToRedact) + return termsToRedact +} + func processError(err error, errorList []error) ([]error, error) { // ensure to use generic fallback error catalog error if no other is available resultError := decorateError(err) diff --git a/cliv2/pkg/core/main_test.go b/cliv2/pkg/core/main_test.go index 8d10a60ae4..6575d12012 100644 --- a/cliv2/pkg/core/main_test.go +++ b/cliv2/pkg/core/main_test.go @@ -68,6 +68,23 @@ func Test_mainWithErrorCode(t *testing.T) { }) } +func Test_populateRedactionTerms(t *testing.T) { + mockController := gomock.NewController(t) + mockEngine := mocks.NewMockEngine(mockController) + mockEngine.EXPECT().GetWorkflows().Return(nil) + + config := configuration.NewWithOpts(configuration.WithAutomaticEnv()) + t.Setenv("SNYK_TEST_REDACTION_MARKER", "unmistakably-secret-value") + + // No debugEnabled anywhere in this call: populateRedactionTerms runs + // unconditionally at its call site, so proving it sets config here proves + // the behavior holds regardless of debugEnabled. + terms := populateRedactionTerms(config, mockEngine) + + assert.Contains(t, terms, "unmistakably-secret-value") + assert.Equal(t, terms, config.GetStringSlice(configuration.REDACTION_TERMS)) +} + func Test_initApplicationConfiguration_DisablesAnalytics(t *testing.T) { t.Run("via SNYK_DISABLE_ANALYTICS (true)", func(t *testing.T) { c := configuration.NewWithOpts(configuration.WithAutomaticEnv()) From 4af3c256daadf9f95ed13719ffd48b2ebc10a5e9 Mon Sep 17 00:00:00 2001 From: Nick Yasnohorodskyi Date: Mon, 17 Aug 2026 10:55:07 +0200 Subject: [PATCH 3/7] fix: reference logging.REDACTION_TERMS after GAF review move GAF moved REDACTION_TERMS from pkg/configuration to pkg/logging (its only reader) per PR #704 review feedback. Follows that rename here. --- cliv2/pkg/core/main.go | 6 +++--- cliv2/pkg/core/main_test.go | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cliv2/pkg/core/main.go b/cliv2/pkg/core/main.go index cbbd37fea7..0779a0438e 100644 --- a/cliv2/pkg/core/main.go +++ b/cliv2/pkg/core/main.go @@ -637,7 +637,7 @@ func mainWithErrorCode(additionalExts []workflow.ExtensionInit) int { // init engine err = globalEngine.Init() - // Unconditional so the analytics scrub chokepoint (which reads configuration.REDACTION_TERMS + // Unconditional so the analytics scrub chokepoint (which reads logging.REDACTION_TERMS // off of config) sees these terms even on non-debug runs, not just when the debug log itself is scrubbed. termsToRedact := populateRedactionTerms(globalConfiguration, globalEngine) @@ -717,13 +717,13 @@ func mainWithErrorCode(additionalExts []workflow.ExtensionInit) int { // populateRedactionTerms computes likely-secret literal values (unrecognized CLI // arguments and environment variables) and records them on config under -// configuration.REDACTION_TERMS, so the analytics scrub chokepoint can redact +// logging.REDACTION_TERMS, so the analytics scrub chokepoint can redact // them regardless of whether debug logging is enabled. func populateRedactionTerms(config configuration.Configuration, engine workflow.Engine) []string { knownTerms, _ := instrumentation.GetKnownCommandsAndFlags(engine) knownTerms = append(knownTerms, config.GetString(configuration.API_URL), config.GetString(configuration.ORGANIZATION), config.GetString(configuration.ORGANIZATION_SLUG)) termsToRedact := cliv2utils.GetUnknownParameters(os.Args[1:], os.Environ(), knownTerms) - config.Set(configuration.REDACTION_TERMS, termsToRedact) + config.Set(logging.REDACTION_TERMS, termsToRedact) return termsToRedact } diff --git a/cliv2/pkg/core/main_test.go b/cliv2/pkg/core/main_test.go index 6575d12012..54588638fd 100644 --- a/cliv2/pkg/core/main_test.go +++ b/cliv2/pkg/core/main_test.go @@ -23,6 +23,7 @@ import ( "github.com/snyk/go-application-framework/pkg/local_workflows/content_type" "github.com/snyk/go-application-framework/pkg/local_workflows/json_schemas" "github.com/snyk/go-application-framework/pkg/local_workflows/local_models" + "github.com/snyk/go-application-framework/pkg/logging" "github.com/snyk/go-application-framework/pkg/mocks" "github.com/snyk/go-application-framework/pkg/utils/ufm" "github.com/snyk/go-application-framework/pkg/workflow" @@ -82,7 +83,7 @@ func Test_populateRedactionTerms(t *testing.T) { terms := populateRedactionTerms(config, mockEngine) assert.Contains(t, terms, "unmistakably-secret-value") - assert.Equal(t, terms, config.GetStringSlice(configuration.REDACTION_TERMS)) + assert.Equal(t, terms, config.GetStringSlice(logging.REDACTION_TERMS)) } func Test_initApplicationConfiguration_DisablesAnalytics(t *testing.T) { From fafa657f2a354c554aa831c4293f26f16ea5f583 Mon Sep 17 00:00:00 2001 From: Nick Yasnohorodskyi Date: Mon, 17 Aug 2026 12:12:15 +0200 Subject: [PATCH 4/7] chore: bump go-application-framework to v0.15.0 Pulls in the analytics extension chokepoint fix (#704): scrub secret-shaped extension values before marshaling, not the marshaled JSON bytes, with cycle-guard for self-referential extension maps. --- cliv2-private/go.mod | 2 +- cliv2-private/go.sum | 2 ++ cliv2/go.mod | 2 +- cliv2/go.sum | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cliv2-private/go.mod b/cliv2-private/go.mod index 717e170210..789c0f859a 100644 --- a/cliv2-private/go.mod +++ b/cliv2-private/go.mod @@ -222,7 +222,7 @@ require ( github.com/snyk/container-cli v0.0.0-20260213211631-cd2b2cf8f3ea // indirect github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62 // indirect github.com/snyk/error-catalog-golang-public v0.0.0-20260806122555-28dc45bbbde6 // indirect - github.com/snyk/go-application-framework v0.14.3 // indirect + github.com/snyk/go-application-framework v0.15.0 // indirect github.com/snyk/go-httpauth v0.0.0-20260810142636-0f6182aaccbc // indirect github.com/snyk/policy-engine v1.1.4 // indirect github.com/snyk/snyk-iac-capture v0.6.5 // indirect diff --git a/cliv2-private/go.sum b/cliv2-private/go.sum index e52faf9efa..c66c863049 100644 --- a/cliv2-private/go.sum +++ b/cliv2-private/go.sum @@ -594,6 +594,8 @@ github.com/snyk/error-catalog-golang-public v0.0.0-20260806122555-28dc45bbbde6 h github.com/snyk/error-catalog-golang-public v0.0.0-20260806122555-28dc45bbbde6/go.mod h1:0dz+HUR/r7VLlQpLfF0a/F1tdHH84NLTZzCxjZ+Q1nk= github.com/snyk/go-application-framework v0.14.3 h1:uZA73qFLmBBL4Y3p4VG5pkr2ys8ojiBIPq0AXJhx7yk= github.com/snyk/go-application-framework v0.14.3/go.mod h1:qJBU+FIY8s/lIg0IaKBj7WGeGERiWqyJvhajzxiA3Ls= +github.com/snyk/go-application-framework v0.15.0 h1:7OM9Lt5aB43iGMo6vAd6E+WUMogDUrO5WNgmzfxNfgA= +github.com/snyk/go-application-framework v0.15.0/go.mod h1:qJBU+FIY8s/lIg0IaKBj7WGeGERiWqyJvhajzxiA3Ls= github.com/snyk/go-httpauth v0.0.0-20260810142636-0f6182aaccbc h1:tuZVhmJFxS4qJlwYIIIw8xgw3VaVqIR3IAV0WaaFVnI= github.com/snyk/go-httpauth v0.0.0-20260810142636-0f6182aaccbc/go.mod h1:f42qLL7WXOS0od7dXJV/hK3myjms/r6HsXgLrg1HRRY= github.com/snyk/policy-engine v1.1.4 h1:0XpaMpl7ixSk4+dlpHYg2iKEBuv+5Ci+QIcbsmhktao= diff --git a/cliv2/go.mod b/cliv2/go.mod index 5f5e6ffc9c..6b32b9b360 100644 --- a/cliv2/go.mod +++ b/cliv2/go.mod @@ -22,7 +22,7 @@ require ( github.com/snyk/code-client-go v1.31.3 github.com/snyk/container-cli v0.0.0-20260213211631-cd2b2cf8f3ea github.com/snyk/error-catalog-golang-public v0.0.0-20260806122555-28dc45bbbde6 - github.com/snyk/go-application-framework v0.14.3 + github.com/snyk/go-application-framework v0.15.0 github.com/snyk/go-httpauth v0.0.0-20260810142636-0f6182aaccbc github.com/snyk/snyk-iac-capture v0.6.5 github.com/snyk/snyk-ls v0.0.0-20260814112015-c5325e836868 diff --git a/cliv2/go.sum b/cliv2/go.sum index 0a5c6e4880..dd377a5714 100644 --- a/cliv2/go.sum +++ b/cliv2/go.sum @@ -544,6 +544,8 @@ github.com/snyk/error-catalog-golang-public v0.0.0-20260806122555-28dc45bbbde6 h github.com/snyk/error-catalog-golang-public v0.0.0-20260806122555-28dc45bbbde6/go.mod h1:0dz+HUR/r7VLlQpLfF0a/F1tdHH84NLTZzCxjZ+Q1nk= github.com/snyk/go-application-framework v0.14.3 h1:uZA73qFLmBBL4Y3p4VG5pkr2ys8ojiBIPq0AXJhx7yk= github.com/snyk/go-application-framework v0.14.3/go.mod h1:qJBU+FIY8s/lIg0IaKBj7WGeGERiWqyJvhajzxiA3Ls= +github.com/snyk/go-application-framework v0.15.0 h1:7OM9Lt5aB43iGMo6vAd6E+WUMogDUrO5WNgmzfxNfgA= +github.com/snyk/go-application-framework v0.15.0/go.mod h1:qJBU+FIY8s/lIg0IaKBj7WGeGERiWqyJvhajzxiA3Ls= github.com/snyk/go-httpauth v0.0.0-20260810142636-0f6182aaccbc h1:tuZVhmJFxS4qJlwYIIIw8xgw3VaVqIR3IAV0WaaFVnI= github.com/snyk/go-httpauth v0.0.0-20260810142636-0f6182aaccbc/go.mod h1:f42qLL7WXOS0od7dXJV/hK3myjms/r6HsXgLrg1HRRY= github.com/snyk/policy-engine v1.1.4 h1:0XpaMpl7ixSk4+dlpHYg2iKEBuv+5Ci+QIcbsmhktao= From ea7fe19714bfeb215b866a5ceab00548aaeb5719 Mon Sep 17 00:00:00 2001 From: Nick Yasnohorodskyi Date: Mon, 17 Aug 2026 12:35:34 +0200 Subject: [PATCH 5/7] chore: tidy go.sum after go-application-framework bump go get left stale v0.14.3 hash entries behind; go mod tidy prunes them. --- cliv2-private/go.sum | 2 -- cliv2/go.sum | 2 -- 2 files changed, 4 deletions(-) diff --git a/cliv2-private/go.sum b/cliv2-private/go.sum index c66c863049..8552536cfd 100644 --- a/cliv2-private/go.sum +++ b/cliv2-private/go.sum @@ -592,8 +592,6 @@ github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62 h1:kgZNQ5ztI4+n3 github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62/go.mod h1:hTr91da/4ze2nk9q6ZW1BmfM2Z8rLUZSEZ3kK+6WGpc= github.com/snyk/error-catalog-golang-public v0.0.0-20260806122555-28dc45bbbde6 h1:XUPFP85nBh+zDCTvxxBuouZP9yG7H1qXZiMGFVMWVKM= github.com/snyk/error-catalog-golang-public v0.0.0-20260806122555-28dc45bbbde6/go.mod h1:0dz+HUR/r7VLlQpLfF0a/F1tdHH84NLTZzCxjZ+Q1nk= -github.com/snyk/go-application-framework v0.14.3 h1:uZA73qFLmBBL4Y3p4VG5pkr2ys8ojiBIPq0AXJhx7yk= -github.com/snyk/go-application-framework v0.14.3/go.mod h1:qJBU+FIY8s/lIg0IaKBj7WGeGERiWqyJvhajzxiA3Ls= github.com/snyk/go-application-framework v0.15.0 h1:7OM9Lt5aB43iGMo6vAd6E+WUMogDUrO5WNgmzfxNfgA= github.com/snyk/go-application-framework v0.15.0/go.mod h1:qJBU+FIY8s/lIg0IaKBj7WGeGERiWqyJvhajzxiA3Ls= github.com/snyk/go-httpauth v0.0.0-20260810142636-0f6182aaccbc h1:tuZVhmJFxS4qJlwYIIIw8xgw3VaVqIR3IAV0WaaFVnI= diff --git a/cliv2/go.sum b/cliv2/go.sum index dd377a5714..eb1735b50e 100644 --- a/cliv2/go.sum +++ b/cliv2/go.sum @@ -542,8 +542,6 @@ github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62 h1:kgZNQ5ztI4+n3 github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62/go.mod h1:hTr91da/4ze2nk9q6ZW1BmfM2Z8rLUZSEZ3kK+6WGpc= github.com/snyk/error-catalog-golang-public v0.0.0-20260806122555-28dc45bbbde6 h1:XUPFP85nBh+zDCTvxxBuouZP9yG7H1qXZiMGFVMWVKM= github.com/snyk/error-catalog-golang-public v0.0.0-20260806122555-28dc45bbbde6/go.mod h1:0dz+HUR/r7VLlQpLfF0a/F1tdHH84NLTZzCxjZ+Q1nk= -github.com/snyk/go-application-framework v0.14.3 h1:uZA73qFLmBBL4Y3p4VG5pkr2ys8ojiBIPq0AXJhx7yk= -github.com/snyk/go-application-framework v0.14.3/go.mod h1:qJBU+FIY8s/lIg0IaKBj7WGeGERiWqyJvhajzxiA3Ls= github.com/snyk/go-application-framework v0.15.0 h1:7OM9Lt5aB43iGMo6vAd6E+WUMogDUrO5WNgmzfxNfgA= github.com/snyk/go-application-framework v0.15.0/go.mod h1:qJBU+FIY8s/lIg0IaKBj7WGeGERiWqyJvhajzxiA3Ls= github.com/snyk/go-httpauth v0.0.0-20260810142636-0f6182aaccbc h1:tuZVhmJFxS4qJlwYIIIw8xgw3VaVqIR3IAV0WaaFVnI= From 8c9038551726c30ce68274b627e2ac76d8f0ebec Mon Sep 17 00:00:00 2001 From: Nick Yasnohorodskyi Date: Mon, 17 Aug 2026 13:22:15 +0200 Subject: [PATCH 6/7] fix: exclude client machine id and detected agent from redaction sweep populateRedactionTerms sweeps os.Environ() for unrecognized values and writes them to logging.REDACTION_TERMS, which the analytics scrub chokepoint (GAF #704) then redacts wherever it finds them verbatim. studio::client_machine_id and persona.agent both echo a raw env var value (INTERNAL_SNYK_CLIENT_MACHINE_ID, AI_AGENT) straight into an extension, so without this exclusion the chokepoint strips its own legitimate data back out as "***". Addresses review comment on #7133. --- cliv2/pkg/core/instrumentation.go | 8 ++++++- cliv2/pkg/core/instrumentation_test.go | 20 +++++++++++++++++- cliv2/pkg/core/main.go | 9 +++++++- cliv2/pkg/core/main_test.go | 29 ++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/cliv2/pkg/core/instrumentation.go b/cliv2/pkg/core/instrumentation.go index f721a7283e..a1d225495f 100644 --- a/cliv2/pkg/core/instrumentation.go +++ b/cliv2/pkg/core/instrumentation.go @@ -57,8 +57,14 @@ func addNetworkingDetails(instrumentor analytics.InstrumentationCollector, confi instrumentor.AddExtension("network-request-attempts", config.GetInt(middleware.ConfigurationKeyRequestAttempts)) } +// clientMachineIdConfigKey is the config key Studio sets before exec'ing the snyk +// binary (see Test_addClientMachineId). populateRedactionTerms in main.go must +// exclude this value from its sweep, or the scrub chokepoint redacts it right back +// out of the studio::client_machine_id extension it's meant to carry. +const clientMachineIdConfigKey = "internal_snyk_client_machine_id" + func addClientMachineId(instrumentor analytics.InstrumentationCollector, config configuration.Configuration) { - if id := config.GetString("internal_snyk_client_machine_id"); id != "" { + if id := config.GetString(clientMachineIdConfigKey); id != "" { instrumentor.AddExtension("studio::client_machine_id", id) } } diff --git a/cliv2/pkg/core/instrumentation_test.go b/cliv2/pkg/core/instrumentation_test.go index e722fe7851..8691afdd21 100644 --- a/cliv2/pkg/core/instrumentation_test.go +++ b/cliv2/pkg/core/instrumentation_test.go @@ -10,6 +10,7 @@ import ( "github.com/snyk/go-application-framework/pkg/configuration" localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows" "github.com/snyk/go-application-framework/pkg/mocks" + "github.com/snyk/go-application-framework/pkg/workflow" "github.com/stretchr/testify/assert" ) @@ -38,16 +39,33 @@ func Test_sendInstrumentation_passesEngineConfigurationToInstrumentationObject(t mockController := gomock.NewController(t) mockEngine := mocks.NewMockEngine(mockController) + // Mirrors production: populateRedactionTerms runs at startup and sweeps up any + // os.Environ() value it doesn't recognize. The client machine id is real Studio + // data, not a secret, so its own env var value must not end up in the terms + // this test's later scrub pass redacts against. + machineId := "studio-device-id-abc12345" + t.Setenv("INTERNAL_SNYK_CLIENT_MACHINE_ID", machineId) + engineConfig := configuration.NewWithOpts(configuration.WithAutomaticEnv()) + mockEngine.EXPECT().GetWorkflows().Return([]workflow.Identifier{}) + populateRedactionTerms(engineConfig, mockEngine) + // One call from shallSendInstrumentation, one to derive analytics.WithConfiguration. // If the call site regresses to only passing WithLogger, this expectation goes unmet. - engineConfig := configuration.NewWithOpts(configuration.WithAutomaticEnv()) mockEngine.EXPECT().GetConfiguration().Return(engineConfig).Times(2) mockEngine.EXPECT().Invoke(localworkflows.WORKFLOWID_REPORT_ANALYTICS, gomock.Any(), gomock.Any()).Return(nil, nil) instrumentor := analytics.NewInstrumentationCollector() + addClientMachineId(instrumentor, engineConfig) logger := zerolog.Nop() sendInstrumentation(context.Background(), mockEngine, instrumentor, &logger) + + // sendInstrumentation just ran the extension through the same scrub chokepoint; + // re-deriving the object (a pure read, doesn't mutate the collector) proves the + // machine id survived it rather than coming back "***". + obj, err := analytics.GetV2InstrumentationObject(instrumentor, analytics.WithConfiguration(engineConfig)) + assert.NoError(t, err) + assert.Equal(t, machineId, (*obj.Data.Attributes.Interaction.Extension)["studio::client_machine_id"]) } func Test_addClientMachineId(t *testing.T) { diff --git a/cliv2/pkg/core/main.go b/cliv2/pkg/core/main.go index 0779a0438e..e3c681e5b7 100644 --- a/cliv2/pkg/core/main.go +++ b/cliv2/pkg/core/main.go @@ -38,6 +38,7 @@ import ( "github.com/snyk/cli/cliv2/internal/constants" persona "github.com/snyk/cli/cliv2/internal/persona" + "github.com/snyk/cli/cliv2/internal/persona/agent" cliv2utils "github.com/snyk/cli/cliv2/internal/utils" localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows" @@ -721,7 +722,13 @@ func mainWithErrorCode(additionalExts []workflow.ExtensionInit) int { // them regardless of whether debug logging is enabled. func populateRedactionTerms(config configuration.Configuration, engine workflow.Engine) []string { knownTerms, _ := instrumentation.GetKnownCommandsAndFlags(engine) - knownTerms = append(knownTerms, config.GetString(configuration.API_URL), config.GetString(configuration.ORGANIZATION), config.GetString(configuration.ORGANIZATION_SLUG)) + knownTerms = append(knownTerms, config.GetString(configuration.API_URL), config.GetString(configuration.ORGANIZATION), config.GetString(configuration.ORGANIZATION_SLUG), config.GetString(clientMachineIdConfigKey)) + // AI_AGENT is trusted verbatim into the persona.agent extension (see + // agent.canonicalAgent) for any harness not on its short canonical list, so its + // raw value needs the same exclusion as the client machine id above. + if detectedAgent, ok := agent.DetectAgent(); ok { + knownTerms = append(knownTerms, string(detectedAgent)) + } termsToRedact := cliv2utils.GetUnknownParameters(os.Args[1:], os.Environ(), knownTerms) config.Set(logging.REDACTION_TERMS, termsToRedact) return termsToRedact diff --git a/cliv2/pkg/core/main_test.go b/cliv2/pkg/core/main_test.go index 54588638fd..cbd6c48c07 100644 --- a/cliv2/pkg/core/main_test.go +++ b/cliv2/pkg/core/main_test.go @@ -86,6 +86,35 @@ func Test_populateRedactionTerms(t *testing.T) { assert.Equal(t, terms, config.GetStringSlice(logging.REDACTION_TERMS)) } +func Test_populateRedactionTerms_excludesClientMachineId(t *testing.T) { + mockController := gomock.NewController(t) + mockEngine := mocks.NewMockEngine(mockController) + mockEngine.EXPECT().GetWorkflows().Return(nil) + + config := configuration.NewWithOpts(configuration.WithAutomaticEnv()) + machineId := "studio-device-id-abc12345" + t.Setenv("INTERNAL_SNYK_CLIENT_MACHINE_ID", machineId) + + terms := populateRedactionTerms(config, mockEngine) + + assert.NotContains(t, terms, machineId, "client machine id must never be swept into REDACTION_TERMS, or the analytics scrub chokepoint strips it right back out of its own extension") +} + +func Test_populateRedactionTerms_excludesDetectedAgent(t *testing.T) { + mockController := gomock.NewController(t) + mockEngine := mocks.NewMockEngine(mockController) + mockEngine.EXPECT().GetWorkflows().Return(nil) + + config := configuration.NewWithOpts(configuration.WithAutomaticEnv()) + // Not on agent.canonicalAgent's short-circuit list, so AI_AGENT is trusted + // verbatim into the persona.agent extension. + t.Setenv("AI_AGENT", "some-unlisted-harness") + + terms := populateRedactionTerms(config, mockEngine) + + assert.NotContains(t, terms, "some-unlisted-harness", "a caller-declared AI_AGENT value must never be swept into REDACTION_TERMS, or the analytics scrub chokepoint strips it right back out of the persona.agent extension") +} + func Test_initApplicationConfiguration_DisablesAnalytics(t *testing.T) { t.Run("via SNYK_DISABLE_ANALYTICS (true)", func(t *testing.T) { c := configuration.NewWithOpts(configuration.WithAutomaticEnv()) From e13fb8a3e8060147868d3b8fb0ffcd01bef02bfc Mon Sep 17 00:00:00 2001 From: Nick Yasnohorodskyi Date: Mon, 17 Aug 2026 15:25:07 +0200 Subject: [PATCH 7/7] chore: retrigger CI