Skip to content
Open
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
49 changes: 49 additions & 0 deletions internal/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,55 @@ func TestUserDefinedScannerUsesIsolatedCwd(t *testing.T) {
}
}

func TestSanitizedDeclaredEnvNamesStripsValues(t *testing.T) {
env := []string{"SECRET_KEY=value", "TOKEN", "EMPTY="}
names := sanitizedDeclaredEnvNames(env)
if len(names) != 3 || names[0] != "SECRET_KEY" || names[1] != "TOKEN" || names[2] != "EMPTY" {
t.Fatalf("names = %#v", names)
}
}

func TestRedactDeclaredEnvValuesRemovesSecrets(t *testing.T) {
env := map[string]string{"SECRET_KEY": "my-secret-value"}
names := []string{"SECRET_KEY"}
message := "Error: my-secret-value failed"
result := redactDeclaredEnvValues(message, env, names)
if result != "Error: [redacted] failed" {
t.Fatalf("result = %q", result)
}
}

func TestUserDefinedScannerRedactsDeclaredEnvInError(t *testing.T) {
adapter := NewUserDefinedScanner(UserDefinedScannerConfig{
ID: "test", Command: "test {{target}}", Targets: []string{"skill"},
Env: []string{"DECLARED_SECRET"},
})
registry, err := NewScannerRegistry(adapter)
if err != nil {
t.Fatal(err)
}
commandRunner := &recordingCommandRunner{
stderr: "Error: my-declared-secret-value failed",
err: errCommandFailed,
}
result, err := (ExternalScannerRunner{
Registry: registry, CommandRunner: commandRunner, Env: map[string]string{"DECLARED_SECRET": "my-declared-secret-value"},
SandboxMode: SandboxModeOff,
}).RunScanner("test", t.TempDir()+"/file.txt", "2026-07-21T00:00:00Z")
if err != nil {
t.Fatal(err)
}
if result.Status != "failed" {
t.Fatalf("status = %q", result.Status)
}
if !strings.Contains(result.Error, "[redacted]") {
t.Fatalf("error = %q, should contain [redacted]", result.Error)
}
if strings.Contains(result.Error, "my-declared-secret-value") {
t.Fatalf("error = %q, should not contain secret value", result.Error)
}
}

func TestUnsafeWindowsShellTargetDetectsInjectionCharacters(t *testing.T) {
unsafe := []string{
`%PATH%`,
Expand Down
31 changes: 31 additions & 0 deletions internal/runner/user_defined_scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,36 @@ func unsafeWindowsShellTarget(target string) bool {
return strings.ContainsAny(target, "%\"!")
}

// sanitizedDeclaredEnvNames returns bare variable names from declared env
// entries, dropping any accidental =value suffix so an inline value is never
// mistaken for a name.
func sanitizedDeclaredEnvNames(env []string) []string {
names := make([]string, 0, len(env))
for _, entry := range env {
name := entry
if i := strings.IndexByte(entry, '='); i >= 0 {
name = entry[:i]
}
if name = strings.TrimSpace(name); name != "" {
names = append(names, name)
}
}
return names
}

// redactDeclaredEnvValues scrubs the values of a user-defined scanner's declared
// env vars from free-text output. A scanner's env: entries exist to hand it
// secrets, so whatever their spelling their values must never be persisted, even
// when the name evades the secret-name heuristic.
func redactDeclaredEnvValues(message string, env map[string]string, names []string) string {
for _, name := range names {
if value := env[name]; value != "" {
message = strings.ReplaceAll(message, value, "[redacted]")
}
}
return message
}

func (adapter userDefinedScannerAdapter) Run(runner ExternalScannerRunner, target string, startedAt string) (ScannerResult, error) {
shell := userDefinedScannerShell(runtime.GOOS, runner.SandboxMode)
targetReplacement := shell.quote(target)
Expand Down Expand Up @@ -109,6 +139,7 @@ func (adapter userDefinedScannerAdapter) Run(runner ExternalScannerRunner, targe
raw := strings.TrimSpace(output.Stdout)
if runErr != nil {
message := commandError(runErr, output.Stderr, runner.Env)
message = redactDeclaredEnvValues(message, runner.Env, sanitizedDeclaredEnvNames(adapter.config.Env))
if json.Valid([]byte(raw)) {
return ScannerResult{
Status: "completed", StartedAt: startedAt, CompletedAt: completedAt, Command: fullCommand,
Expand Down
Loading