diff --git a/CHANGELOG.md b/CHANGELOG.md index a362ee5..5297a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `--json` blocked-command results now redact password, token, secret, and API + key style arguments in both the `command` field and `error` text, matching the + audit trail redaction behavior. - Remote command tokens after the command start or `--` separator are preserved, so flags such as `-v`, `--help`, and `--force` are passed to the remote command instead of being consumed as local `sshx` flags. diff --git a/internal/app/agentmode_test.go b/internal/app/agentmode_test.go index 99b4f4b..5191d4d 100644 --- a/internal/app/agentmode_test.go +++ b/internal/app/agentmode_test.go @@ -145,6 +145,43 @@ func TestRun_BlockedCommandShortCircuits(t *testing.T) { } } +func TestRun_BlockedCommandJSONRedactsSecretLikeArguments(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + secretFragments := []string{"alpha", "bravo", "charlie", "delta"} + result := runReportedJSON(t, []string{ + "sshx", + "-h=192.0.2.1", + "--json", + `sudo rm -rf / password="alpha bravo" --token "charlie delta"`, + }) + + if result["error_kind"] != "blocked" { + t.Fatalf("expected error_kind=blocked, got %v", result["error_kind"]) + } + command, ok := result["command"].(string) + if !ok { + t.Fatalf("expected command string, got %T", result["command"]) + } + if command != `sudo rm -rf / password= --token ` { + t.Fatalf("unexpected redacted command: %q", command) + } + errText, ok := result["error"].(string) + if !ok { + t.Fatalf("expected error string, got %T", result["error"]) + } + if !strings.Contains(errText, `password= --token `) { + t.Fatalf("expected redacted command in error, got %q", errText) + } + for _, fragment := range secretFragments { + if strings.Contains(command, fragment) { + t.Fatalf("command leaked secret fragment %q in %q", fragment, command) + } + if strings.Contains(errText, fragment) { + t.Fatalf("error leaked secret fragment %q in %q", fragment, errText) + } + } +} + func TestRun_JSONConfigFailuresDoNotConnect(t *testing.T) { tests := []struct { name string diff --git a/internal/app/app.go b/internal/app/app.go index d6fcd17..d478b6a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -215,7 +215,7 @@ func emitCommandJSON(config *sshclient.Config, authMethod sshclient.AuthMethod, Host: config.Host, Port: config.Port, User: config.User, - Command: config.Command, + Command: redactSensitiveText(config.Command), ExitCode: res.ExitCode, Success: execErr == nil && res.ExitCode == 0, Stdout: res.Stdout, @@ -227,7 +227,7 @@ func emitCommandJSON(config *sshclient.Config, authMethod sshclient.AuthMethod, ErrorKind: errKind, } if execErr != nil { - result.Error = execErr.Error() + result.Error = redactError(execErr) if result.ExitCode == 0 { result.ExitCode = -1 }