package mcp import ( "strings" "testing" "github.com/sophotechlabs/spinoza/internal/api" ) func TestWhichNamesReadAsSecrets(t *testing.T) { cases := []struct { name string field string want bool }{ {name: "a password", field: "PASSWORD", want: true}, {name: "an api key", field: "api_key", want: true}, {name: "a bearer token", field: "AUTH_TOKEN", want: true}, {name: "a connection string", field: "DB_CONNECTION_STRING", want: true}, {name: "a private key", field: "tls.private_key", want: true}, {name: "a plain setting", field: "LOG_LEVEL", want: false}, {name: "a port", field: "HTTP_PORT", want: false}, {name: "nothing", field: "", want: false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := looksSecret(tc.field); got != tc.want { t.Fatalf("looksSecret(%q) = %v, want %v", tc.field, got, tc.want) } }) } } func TestScrubbingALine(t *testing.T) { awsAccessKey := "AKIA" + strings.Repeat("1", 16) stripeToken := "sk_" + "live_" + "1234567890abcdef" cases := []struct { name string line string gone string kept string }{ { name: "a bearer token", line: "GET /v1 Authorization: Bearer abcdef0123456789abcdef", gone: "abcdef0123456789abcdef", kept: "GET /v1", }, { name: "a jwt", line: "session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 opened", gone: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", kept: "opened", }, { name: "a password assignment", line: "connecting with password=hunter2000 to db", gone: "hunter2000", kept: "connecting with", }, { name: "a token in json", line: `{"api_token": "s3cr3t-value-here", "level": "info"}`, gone: "s3cr3t-value-here", kept: "info", }, { name: "a private key block", line: pemFixture(), gone: "MIIabc", kept: "", }, { name: "a long base64 blob", line: "payload " + strings.Repeat("QUJDRA", 9), gone: strings.Repeat("QUJDRA", 9), kept: "payload", }, { name: "a standalone github token", line: "clone failed for ghp_1234567890abcdefghijklmnopqrstuvwxyz", gone: "ghp_1234567890abcdefghijklmnopqrstuvwxyz", kept: "clone failed", }, { name: "a standalone slack token", line: "slack rejected xoxb-1234567890-abcdefghijklmnop", gone: "xoxb-1234567890-abcdefghijklmnop", kept: "slack rejected", }, { name: "a standalone stripe token", line: "stripe rejected " + stripeToken, gone: stripeToken, kept: "stripe rejected", }, { name: "a standalone gitlab token", line: "gitlab rejected glpat-1234567890abcdef", gone: "glpat-1234567890abcdef", kept: "gitlab rejected", }, { name: "a standalone aws access key", line: "aws rejected " + awsAccessKey, gone: awsAccessKey, kept: "aws rejected", }, { name: "a standalone google api key", line: "google rejected AIza1234567890abcdefghijklmn", gone: "AIza1234567890abcdefghijklmn", kept: "google rejected", }, { name: "a standalone sendgrid token", line: "sendgrid rejected SG.1234567890ab.abcdefghijklmnop", gone: "SG.1234567890ab.abcdefghijklmnop", kept: "sendgrid rejected", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { result := scrub(tc.line) if strings.Contains(result, tc.gone) { t.Fatalf("scrub kept %q in %q", tc.gone, result) } if tc.kept != "" && !strings.Contains(result, tc.kept) { t.Fatalf("scrub lost %q from %q", tc.kept, result) } if !strings.Contains(result, hidden) { t.Fatalf("scrub removed the value without saying so: %q", result) } }) } } func pemFixture() string { head := "-----BEGIN RSA " + "PRIVATE KEY-----" return head + "\nMIIabc\n" + strings.Replace(head, "BEGIN", "END", 1) } func TestScrubbingLeavesOrdinaryTextAlone(t *testing.T) { line := "listening on 127.0.0.1:34115, 3 workers, level=info" if got := scrub(line); got != line { t.Fatalf("scrub changed a line with no secret in it:\n got %q\nwant %q", got, line) } if got := scrub(""); got != "" { t.Fatalf("scrub of nothing = %q", got) } } func TestEveryLineIsScrubbed(t *testing.T) { result := scrubLines([]string{"password=letmein1", "all good"}) if strings.Contains(result[0], "letmein1") { t.Fatalf("the first line kept its secret: %q", result[0]) } if result[1] != "all good" { t.Fatalf("the second line changed: %q", result[1]) } } func TestASecretGivesUpKeysAndSizesOnly(t *testing.T) { entries := []api.DataEntry{ {Key: "password", Value: "hunter2", Bytes: 7}, {Key: "keystore", Value: "AAEC", Bytes: 3, Binary: true}, } result := keysOnly(entries) expected := []dataKey{ {Key: "password", Bytes: 7}, {Key: "keystore", Bytes: 3, Binary: true}, } if len(result) != len(expected) { t.Fatalf("keys = %v, want %v", result, expected) } for i, want := range expected { if result[i] != want { t.Fatalf("key %d = %+v, want %+v", i, result[i], want) } } } func TestTheRawDocumentIsWithheldForASecretAndScrubbedOtherwise(t *testing.T) { document := "apiVersion: v1\ndata:\n password: aHVudGVyMg==\n" if got := safeYAML("Secret", document); got != "" { t.Fatalf("a Secret gave up its document: %q", got) } scrubbed := safeYAML("ConfigMap", "env:\n - name: TOKEN\n value: not-a-real-value\n") if strings.Contains(scrubbed, "not-a-real-value") { t.Fatalf("a ConfigMap document kept a token: %q", scrubbed) } plain := safeYAML("Deployment", "spec:\n replicas: 3\n") if plain != "spec:\n replicas: 3\n" { t.Fatalf("an ordinary document was altered: %q", plain) } } func TestOnlySecretsCarrySecrets(t *testing.T) { if !carriesSecrets("Secret") { t.Fatal("a Secret must be treated as carrying secrets") } if carriesSecrets("ConfigMap") { t.Fatal("a ConfigMap is not withheld; its values are not secret by kind") } } func TestScrubbingNestedYAML(t *testing.T) { cases := []struct { name string body string gone string }{ { name: "a nested token", body: "auth:\n token: not-a-real-value\n", gone: "not-a-real-value", }, { name: "an env pair across two lines", body: "env:\n - name: DB_PASSWORD\n value: hunter2000\n", gone: "hunter2000", }, { name: "an env pair with a quoted name", body: `env:` + "\n" + ` - name: "API_TOKEN"` + "\n" + ` value: not-a-real-value` + "\n", gone: "not-a-real-value", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if got := scrub(tc.body); strings.Contains(got, tc.gone) { t.Fatalf("scrub kept %q in %q", tc.gone, got) } }) } } func TestStructuredUnknownCredentialFieldsAreRedacted(t *testing.T) { body := `ordinary: visible nested: license: license-value key: key-value url: https://user:password@example.com/path cookie: cookie-value code: authorization-code ` got := scrubStructuredYAML(body) for _, secret := range []string{ "license-value", "key-value", "https://user:password@example.com/path", "cookie-value", "authorization-code", } { if strings.Contains(got, secret) { t.Fatalf("structured output kept %q in %q", secret, got) } } if !strings.Contains(got, "ordinary: visible") { t.Fatalf("ordinary field was lost: %q", got) } if strings.Count(got, hidden) != 5 { t.Fatalf("redactions = %d, want 5 in %q", strings.Count(got, hidden), got) } } func TestAnEnvPairWithAnInnocentNameSurvives(t *testing.T) { body := "env:\n - name: LOG_LEVEL\n value: debug\n" if got := scrub(body); got != body { t.Fatalf("scrub changed an ordinary env pair:\n got %q\nwant %q", got, body) } } func TestAParentKeyIsNotMistakenForAValue(t *testing.T) { body := "auth:\n mode: oidc\n" if got := scrub(body); !strings.Contains(got, "mode: oidc") { t.Fatalf("scrub ate a value that is not a secret: %q", got) } } func TestLabelsAreScrubbedLikeEverythingElse(t *testing.T) { result := scrubMap(map[string]string{ "app": "web", "auth-token": "not-a-real-value", "release-note": "deployed with password=letmein1", }) if result["app"] != "web" { t.Fatalf("an ordinary label changed: %q", result["app"]) } if result["auth-token"] != hidden { t.Fatalf("a secret-shaped label kept its value: %q", result["auth-token"]) } if strings.Contains(result["release-note"], "letmein1") { t.Fatalf("a label value kept a secret: %q", result["release-note"]) } if scrubMap(nil) != nil { t.Fatal("no labels became an empty map rather than nothing") } }