220 lines
7.7 KiB
Go
220 lines
7.7 KiB
Go
package secrets
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"reasonix/internal/provider"
|
|
)
|
|
|
|
func TestRedactMasksCommonSecretShapes(t *testing.T) {
|
|
in := strings.Join([]string{
|
|
"DEEPSEEK_API_KEY=sk-real-secret-value-123456",
|
|
"Authorization: Bearer ghp_abcdefghijklmnopqrstuvwxyz",
|
|
"token xoxb-123456789012-abcdefabcdef",
|
|
"jwt eyJabc.def.ghi",
|
|
}, "\n")
|
|
|
|
got := Redact(in)
|
|
for _, leaked := range []string{
|
|
"sk-real-secret-value-123456",
|
|
"ghp_abcdefghijklmnopqrstuvwxyz",
|
|
"xoxb-123456789012-abcdefabcdef",
|
|
"eyJabc.def.ghi",
|
|
} {
|
|
if strings.Contains(got, leaked) {
|
|
t.Fatalf("secret leaked %q in:\n%s", leaked, got)
|
|
}
|
|
}
|
|
for _, want := range []string{"DEEPSEEK_API_KEY=sk-rea", "Authorization: Bearer [redacted]"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("redacted output missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRedactMasksJSONQuotedKeys(t *testing.T) {
|
|
in := `http 401: {"access_token":"sk-live-secret","x-api-key":"header-secret","password":"pw-secret"}`
|
|
got := Redact(in)
|
|
for _, leaked := range []string{"sk-live-secret", "header-secret", "pw-secret"} {
|
|
if strings.Contains(got, leaked) {
|
|
t.Fatalf("JSON credential leaked %q in:\n%s", leaked, got)
|
|
}
|
|
}
|
|
if !strings.Contains(got, `"access_token":"`) || !strings.Contains(got, "http 401") {
|
|
t.Fatalf("non-secret structure mangled:\n%s", got)
|
|
}
|
|
if again := Redact(got); again != got {
|
|
t.Fatalf("JSON redaction not idempotent:\nonce: %q\ntwice: %q", got, again)
|
|
}
|
|
}
|
|
|
|
func TestRedactMasksCookieHeaderValues(t *testing.T) {
|
|
in := "Cookie: session=cookie-secret\nSet-Cookie: sid=abc123def456; Path=/; HttpOnly"
|
|
got := Redact(in)
|
|
for _, leaked := range []string{"cookie-secret", "abc123def456"} {
|
|
if strings.Contains(got, leaked) {
|
|
t.Fatalf("cookie value leaked %q in:\n%s", leaked, got)
|
|
}
|
|
}
|
|
for _, want := range []string{"Cookie: session=[redacted]", "Set-Cookie: sid=[redacted]", "HttpOnly"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("redacted output missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
if again := Redact(got); again != got {
|
|
t.Fatalf("cookie redaction not idempotent:\nonce: %q\ntwice: %q", got, again)
|
|
}
|
|
}
|
|
|
|
func TestRedactMasksNonBearerAuthorizationSchemes(t *testing.T) {
|
|
in := strings.Join([]string{
|
|
"Authorization: Basic dXNlcjpwYXNzd29yZA==",
|
|
"Proxy-Authorization: Digest username-hash-abcdef0123456789",
|
|
"Authorization: dXNlcjpwYXNzd29yZC1yYXc=",
|
|
}, "\n")
|
|
got := Redact(in)
|
|
for _, leaked := range []string{"dXNlcjpwYXNzd29yZA==", "username-hash-abcdef0123456789", "dXNlcjpwYXNzd29yZC1yYXc="} {
|
|
if strings.Contains(got, leaked) {
|
|
t.Fatalf("authorization credential leaked %q:\n%s", leaked, got)
|
|
}
|
|
}
|
|
for _, want := range []string{"Authorization: Basic [redacted]", "Digest [redacted]", "Authorization: [redacted]"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("redacted output missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
if again := Redact(got); again != got {
|
|
t.Fatalf("authorization redaction not idempotent:\nonce: %q\ntwice: %q", got, again)
|
|
}
|
|
}
|
|
|
|
func TestRedactIsIdempotent(t *testing.T) {
|
|
// The session save path re-redacts loaded (already-redacted) transcripts;
|
|
// digest stability across load/save cycles requires a byte-for-byte no-op.
|
|
in := strings.Join([]string{
|
|
"DEEPSEEK_API_KEY=sk-real-secret-value-123456",
|
|
"Authorization: Bearer ghp_abcdefghijklmnopqrstuvwxyz",
|
|
"DB_PWD='hunter2-swordfish'",
|
|
"plain text with PWD=/home/user/project untouched",
|
|
}, "\n")
|
|
once := Redact(in)
|
|
twice := Redact(once)
|
|
if once != twice {
|
|
t.Fatalf("Redact not idempotent:\nonce: %q\ntwice: %q", once, twice)
|
|
}
|
|
}
|
|
|
|
func TestRedactLeavesWorkingDirectoryPWDAlone(t *testing.T) {
|
|
in := "PWD=/home/user/project\nOLDPWD=/home/user\nDB_PWD=hunter2-swordfish-123"
|
|
got := Redact(in)
|
|
if !strings.Contains(got, "PWD=/home/user/project") {
|
|
t.Fatalf("POSIX PWD variable was mangled:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "OLDPWD=/home/user") {
|
|
t.Fatalf("OLDPWD was mangled:\n%s", got)
|
|
}
|
|
if strings.Contains(got, "hunter2-swordfish-123") {
|
|
t.Fatalf("DB_PWD value leaked:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestEnvKeySensitive(t *testing.T) {
|
|
sensitive := []string{"DEEPSEEK_API_KEY", "GH_TOKEN", "AWS_SECRET_ACCESS_KEY", "DB_PASSWORD", "MYSQL_PWD", "NPM_TOKEN"}
|
|
for _, key := range sensitive {
|
|
if !EnvKeySensitive(key) {
|
|
t.Errorf("EnvKeySensitive(%q) = false, want true", key)
|
|
}
|
|
}
|
|
benign := []string{"PWD", "OLDPWD", "PATH", "HOME", "LANG", "GOPATH", "TERM"}
|
|
for _, key := range benign {
|
|
if EnvKeySensitive(key) {
|
|
t.Errorf("EnvKeySensitive(%q) = true, want false", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFilterEnvDropsSensitiveKeys(t *testing.T) {
|
|
got := FilterEnv([]string{
|
|
"PATH=/usr/bin",
|
|
"DEEPSEEK_API_KEY=sk-real-secret-value-123456",
|
|
"GH_TOKEN=ghp_abcdefghijklmnopqrstuvwxyz",
|
|
"PWD=/home/user/project",
|
|
"HOME=/tmp/home",
|
|
})
|
|
joined := strings.Join(got, "\n")
|
|
if strings.Contains(joined, "DEEPSEEK_API_KEY") || strings.Contains(joined, "GH_TOKEN") {
|
|
t.Fatalf("sensitive env survived:\n%s", joined)
|
|
}
|
|
for _, want := range []string{"PATH=/usr/bin", "HOME=/tmp/home", "PWD=/home/user/project"} {
|
|
if !strings.Contains(joined, want) {
|
|
t.Fatalf("non-sensitive env %q dropped:\n%s", want, joined)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProcessEnvUnfilteredByDefault(t *testing.T) {
|
|
t.Setenv("REASONIX_TEST_SECRET_TOKEN", "ghp_abcdefghijklmnopqrstuvwxyz")
|
|
joined := strings.Join(ProcessEnv(), "\n")
|
|
if !strings.Contains(joined, "REASONIX_TEST_SECRET_TOKEN=ghp_abcdefghijklmnopqrstuvwxyz") {
|
|
t.Fatalf("ProcessEnv filtered by default; filter_subprocess_env must be opt-in:\n%s", joined)
|
|
}
|
|
|
|
SetFilterSubprocessEnv(true)
|
|
t.Cleanup(func() { SetFilterSubprocessEnv(false) })
|
|
joined = strings.Join(ProcessEnv(), "\n")
|
|
if strings.Contains(joined, "REASONIX_TEST_SECRET_TOKEN") {
|
|
t.Fatalf("ProcessEnv leaked sensitive key with filtering enabled:\n%s", joined)
|
|
}
|
|
}
|
|
|
|
func TestRedactToolOutputHonorsToggle(t *testing.T) {
|
|
const in = "DEEPSEEK_API_KEY=sk-real-secret-value-123456"
|
|
if got := RedactToolOutput(in); strings.Contains(got, "sk-real-secret-value-123456") {
|
|
t.Fatalf("tool output not redacted by default:\n%s", got)
|
|
}
|
|
SetRedactToolOutput(false)
|
|
t.Cleanup(func() { SetRedactToolOutput(true) })
|
|
if got := RedactToolOutput(in); got != in {
|
|
t.Fatalf("RedactToolOutput altered output with the toggle off:\n%s", got)
|
|
}
|
|
// The durable-surface entry point ignores the toggle.
|
|
if got := Redact(in); strings.Contains(got, "sk-real-secret-value-123456") {
|
|
t.Fatalf("Redact must stay active regardless of the toggle:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestRedactMessagesDoesNotMutateInput(t *testing.T) {
|
|
const secret = "sk-real-secret-value-123456"
|
|
msgs := []provider.Message{
|
|
{
|
|
Role: provider.RoleAssistant,
|
|
Content: "checking",
|
|
ToolCalls: []provider.ToolCall{
|
|
{ID: "call_1", Name: "bash", Arguments: `{"command":"echo DEEPSEEK_API_KEY=` + secret + `"}`},
|
|
},
|
|
MemoryCitations: []provider.MemoryCitation{{Note: "token " + secret}},
|
|
},
|
|
{Role: provider.RoleTool, ToolCallID: "call_1", Content: "DEEPSEEK_API_KEY=" + secret},
|
|
}
|
|
|
|
out := RedactMessages(msgs)
|
|
|
|
// The redacted copy must not carry the raw secret...
|
|
if strings.Contains(out[0].ToolCalls[0].Arguments, secret) || strings.Contains(out[1].Content, secret) {
|
|
t.Fatalf("redacted copy leaked secret: %+v", out)
|
|
}
|
|
// ...and the input — live session history the model still replays — must
|
|
// be untouched, including through the shared ToolCalls/MemoryCitations
|
|
// backing arrays.
|
|
if !strings.Contains(msgs[0].ToolCalls[0].Arguments, secret) {
|
|
t.Fatalf("RedactMessages mutated the caller's ToolCalls: %q", msgs[0].ToolCalls[0].Arguments)
|
|
}
|
|
if !strings.Contains(msgs[0].MemoryCitations[0].Note, secret) {
|
|
t.Fatalf("RedactMessages mutated the caller's MemoryCitations: %q", msgs[0].MemoryCitations[0].Note)
|
|
}
|
|
if !strings.Contains(msgs[1].Content, secret) {
|
|
t.Fatalf("RedactMessages mutated the caller's Content: %q", msgs[1].Content)
|
|
}
|
|
}
|