Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cliv2-private/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions cliv2-private/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -592,8 +592,8 @@ 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=
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=
Expand Down
2 changes: 1 addition & 1 deletion cliv2/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions cliv2/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -542,8 +542,8 @@ 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=
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=
Expand Down
10 changes: 8 additions & 2 deletions cliv2/pkg/core/instrumentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -133,7 +139,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")
}
Expand Down
41 changes: 41 additions & 0 deletions cliv2/pkg/core/instrumentation_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
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/snyk/go-application-framework/pkg/workflow"
"github.com/stretchr/testify/assert"
)

Expand All @@ -27,6 +33,41 @@ 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)

// 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.
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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you extend this test to verify the machine ID is not redacted by the new logic? More precisely: this test does not use the new scrubbing logic. It may miss the machine ID being replaced

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
Expand Down
26 changes: 23 additions & 3 deletions cliv2/pkg/core/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -637,12 +638,13 @@ func mainWithErrorCode(additionalExts []workflow.ExtensionInit) int {
// init engine
err = globalEngine.Init()

// 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)

// 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)
}

Expand Down Expand Up @@ -714,6 +716,24 @@ 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
// 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), 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
}

func processError(err error, errorList []error) ([]error, error) {
// ensure to use generic fallback error catalog error if no other is available
resultError := decorateError(err)
Expand Down
47 changes: 47 additions & 0 deletions cliv2/pkg/core/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -68,6 +69,52 @@ 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(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())
Expand Down