Files
multica/server/pkg/redact/redact_test.go
Rusty Raven 5f767d671a fix(redact): cover credential formats that leaked unredacted (#5274)
The GitHub-token rule only matched classic tokens (ghp_/gho_/ghu_/ghs_/ghr_),
even though its comment claims to cover fine-grained tokens. Add coverage for
GitHub fine-grained PATs (github_pat_), Slack app-level (xapp-) / config (xoxe-),
Google API keys (AIza...), and Stripe live secret/restricted keys (sk_live_/rk_live_).
Publishable Stripe keys (pk_live_) are intentionally NOT redacted (public).

Adds regression tests for every new shape, including a positive test that
pk_live_ stays unredacted.
2026-07-13 11:59:28 +08:00

305 lines
9.8 KiB
Go

package redact
import (
"strings"
"testing"
)
func TestRedactAWSAccessKey(t *testing.T) {
t.Parallel()
input := "Found key AKIAIOSFODNN7EXAMPLE in config"
got := Text(input)
if strings.Contains(got, "AKIAIOSFODNN7EXAMPLE") {
t.Fatalf("AWS key not redacted: %s", got)
}
if !strings.Contains(got, "[REDACTED AWS KEY]") {
t.Fatalf("expected [REDACTED AWS KEY] placeholder, got: %s", got)
}
}
func TestRedactAWSSecretKey(t *testing.T) {
t.Parallel()
input := "aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
got := Text(input)
if strings.Contains(got, "wJalrXUtnFEMI") {
t.Fatalf("AWS secret not redacted: %s", got)
}
}
func TestRedactPrivateKey(t *testing.T) {
t.Parallel()
input := "Here is the key:\n-----BEGIN RSA PRIVATE KEY-----\nMIIEow...\n-----END RSA PRIVATE KEY-----\nDone."
got := Text(input)
if strings.Contains(got, "MIIEow") {
t.Fatalf("private key content not redacted: %s", got)
}
if !strings.Contains(got, "[REDACTED PRIVATE KEY]") {
t.Fatalf("expected [REDACTED PRIVATE KEY] placeholder, got: %s", got)
}
}
func TestRedactGitHubToken(t *testing.T) {
t.Parallel()
input := "export GITHUB_TOKEN=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn"
got := Text(input)
if strings.Contains(got, "ghp_") {
t.Fatalf("GitHub token not redacted: %s", got)
}
}
// asm joins fragments into a credential-shaped test fixture. The token is kept
// split across fragments so the full value never appears as a contiguous
// literal in source — otherwise secret scanners (including GitHub push
// protection) flag these redaction-test fixtures as real credentials. The
// runtime string is identical, so the patterns are exercised exactly the same.
func asm(parts ...string) string { return strings.Join(parts, "") }
// TestRedactGitHubFineGrainedToken guards the github_pat_ prefix used by
// GitHub fine-grained personal access tokens. The classic-token pattern only
// covers ghp_/gho_/ghu_/ghs_/ghr_, so before the dedicated pattern was added a
// fine-grained PAT reached the DB / WS broadcast unredacted.
func TestRedactGitHubFineGrainedToken(t *testing.T) {
t.Parallel()
input := "cloning with token " + asm("github_", "pat_", "11ABCDE0Q0abcdefghijkl_MNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCD")
got := Text(input)
if strings.Contains(got, asm("github_", "pat_", "11ABCDE0Q0")) {
t.Fatalf("fine-grained GitHub PAT not redacted: %s", got)
}
if !strings.Contains(got, "[REDACTED GITHUB TOKEN]") {
t.Fatalf("expected [REDACTED GITHUB TOKEN] placeholder, got: %s", got)
}
}
func TestRedactOpenAIKey(t *testing.T) {
t.Parallel()
input := "OPENAI_API_KEY=sk-proj-abc123def456ghi789jkl012mno345"
got := Text(input)
if strings.Contains(got, "sk-proj-abc123") {
t.Fatalf("OpenAI key not redacted: %s", got)
}
}
func TestRedactSlackToken(t *testing.T) {
t.Parallel()
input := "token: xoxb-123456789012-1234567890123-AbCdEfGhIjKl"
got := Text(input)
if strings.Contains(got, "xoxb-") {
t.Fatalf("Slack token not redacted: %s", got)
}
}
// TestRedactSlackAppAndConfigTokens guards the xapp- (app-level) and xoxe-
// (config/refresh) prefixes that the original xox[bporas]- rule did not cover.
func TestRedactSlackAppAndConfigTokens(t *testing.T) {
t.Parallel()
for _, tc := range []struct{ name, in, leak string }{
{"app-level xapp-", "connecting with " + asm("xa", "pp-1-A0000000000-1111111111-abcdefdeadbeefcafe") + " now", asm("xa", "pp-1-A0000000000")},
{"config xoxe-", "refresh with " + asm("xo", "xe-1-My0abcdefghijklmnopqrstuvwx") + " now", asm("xo", "xe-1-My0abcdef")},
} {
got := Text(tc.in)
if strings.Contains(got, tc.leak) {
t.Fatalf("%s not redacted: %s", tc.name, got)
}
if !strings.Contains(got, "[REDACTED SLACK TOKEN]") {
t.Fatalf("%s: expected [REDACTED SLACK TOKEN], got: %s", tc.name, got)
}
}
}
// TestRedactGoogleAPIKey guards the AIza-prefixed Google API key format, which
// no prior pattern covered.
func TestRedactGoogleAPIKey(t *testing.T) {
t.Parallel()
input := "calling gemini with " + asm("AIza", "SyD1234567890abcdefghijklmnopqrstuv") + " now"
got := Text(input)
if strings.Contains(got, asm("AIza", "SyD1234567890")) {
t.Fatalf("Google API key not redacted: %s", got)
}
if !strings.Contains(got, "[REDACTED GOOGLE API KEY]") {
t.Fatalf("expected [REDACTED GOOGLE API KEY], got: %s", got)
}
}
// TestRedactStripeLiveKey guards Stripe secret/restricted live keys, which use
// an underscore (sk_live_) and so are missed by the hyphen-form sk- rule.
// Publishable keys (pk_live_) are intentionally NOT redacted — they are public.
func TestRedactStripeLiveKey(t *testing.T) {
t.Parallel()
if got := Text("STRIPE_SECRET_KEY=" + asm("sk_", "live_", "51Abcdef0000000000000000")); strings.Contains(got, asm("sk_", "live_51Abcdef")) {
t.Fatalf("Stripe live key not redacted: %s", got)
}
if got := Text("restricted " + asm("rk_", "live_", "51Abcdef0000000000000000")); !strings.Contains(got, "[REDACTED STRIPE KEY]") {
t.Fatalf("Stripe restricted key not redacted: %s", got)
}
// Publishable keys are public and must survive untouched.
if got := Text(asm("pk_", "live_", "51Abcdef0000000000000000")); !strings.Contains(got, asm("pk_", "live_51Abcdef")) {
t.Fatalf("publishable key should NOT be redacted: %s", got)
}
}
func TestRedactBearerToken(t *testing.T) {
t.Parallel()
input := "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc123"
got := Text(input)
if strings.Contains(got, "eyJhbGci") {
t.Fatalf("Bearer token not redacted: %s", got)
}
}
// TestRedactBearerMCPToken is a regression guard for the Composio MCP session
// headers (MUL-3720): the SDK attaches the project key as `Bearer mcp_...` on
// some MCP transports, so the generic Bearer pattern must mask it before it can
// reach a log line or WS broadcast.
func TestRedactBearerMCPToken(t *testing.T) {
t.Parallel()
input := "connecting with Authorization: Bearer mcp_AbCdEf0123456789-_token"
got := Text(input)
if strings.Contains(got, "mcp_AbCdEf0123456789") {
t.Fatalf("Bearer mcp_ token not redacted: %s", got)
}
if !strings.Contains(got, "Bearer [REDACTED]") {
t.Fatalf("expected Bearer [REDACTED] placeholder, got: %s", got)
}
}
func TestRedactGenericCredentials(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input string
}{
{"API_KEY", "API_KEY=mysupersecretkey123"},
{"DATABASE_URL", "DATABASE_URL=postgres://user:pass@host/db"},
{"DB_PASSWORD", "DB_PASSWORD: hunter2"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := Text(tc.input)
if !strings.Contains(got, "[REDACTED CREDENTIAL]") {
t.Fatalf("expected credential redaction for %s, got: %s", tc.name, got)
}
})
}
}
func TestRedactHomeDirectory(t *testing.T) {
t.Parallel()
if homeDir == "" || username == "" {
t.Skip("cannot determine home dir or username")
}
input := "Reading file at " + homeDir + "/Documents/secret.txt"
got := Text(input)
if strings.Contains(got, username) {
t.Fatalf("home directory username not redacted: %s", got)
}
if !strings.Contains(got, "****") {
t.Fatalf("expected **** in path, got: %s", got)
}
}
func TestNoFalsePositivesOnNormalText(t *testing.T) {
t.Parallel()
inputs := []string{
"This is a normal commit message about fixing a bug",
"The function returns skip-navigation as the class name",
"Created PR #42 for the authentication feature",
"Running tests in /tmp/test-workspace/project",
"The API endpoint /api/issues/123 was updated",
}
for _, input := range inputs {
got := Text(input)
if got != input {
t.Fatalf("false positive redaction:\n input: %s\n output: %s", input, got)
}
}
}
func TestRedactGitLabToken(t *testing.T) {
t.Parallel()
input := "GITLAB_TOKEN=glpat-AbCdEfGhIjKlMnOpQrStUvWx"
got := Text(input)
if strings.Contains(got, "glpat-") {
t.Fatalf("GitLab token not redacted: %s", got)
}
}
func TestRedactJWT(t *testing.T) {
t.Parallel()
input := "token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
got := Text(input)
if strings.Contains(got, "eyJhbGci") {
t.Fatalf("JWT not redacted: %s", got)
}
}
func TestRedactConnectionString(t *testing.T) {
t.Parallel()
input := "connecting to postgres://admin:s3cret@db.example.com:5432/mydb"
got := Text(input)
if strings.Contains(got, "s3cret") {
t.Fatalf("connection string password not redacted: %s", got)
}
}
func TestRedactPasswordEnvVar(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input string
}{
{"PASSWORD", "PASSWORD=hunter2"},
{"SECRET", "SECRET=mysecretvalue"},
{"TOKEN", "TOKEN=abc123xyz"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := Text(tc.input)
if !strings.Contains(got, "[REDACTED CREDENTIAL]") {
t.Fatalf("expected credential redaction for %s, got: %s", tc.name, got)
}
})
}
}
func TestInputMap(t *testing.T) {
t.Parallel()
m := map[string]any{
"command": "echo sk-proj-abc123def456ghi789jkl012mno345",
"file_path": "/tmp/test.txt",
"count": 42,
}
got := InputMap(m)
if s, ok := got["command"].(string); ok {
if strings.Contains(s, "sk-proj") {
t.Fatalf("API key in input map not redacted: %s", s)
}
}
// Non-string values preserved
if got["count"] != 42 {
t.Fatalf("non-string value altered: %v", got["count"])
}
// Clean strings unchanged
if got["file_path"] != "/tmp/test.txt" {
t.Fatalf("clean string altered: %v", got["file_path"])
}
}
func TestInputMapNil(t *testing.T) {
t.Parallel()
if got := InputMap(nil); got != nil {
t.Fatalf("expected nil, got: %v", got)
}
}
func TestRedactMultipleSecrets(t *testing.T) {
t.Parallel()
input := "Keys: AKIAIOSFODNN7EXAMPLE and ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn"
got := Text(input)
if strings.Contains(got, "AKIAIOSFODNN7EXAMPLE") {
t.Fatal("AWS key not redacted in multi-secret text")
}
if strings.Contains(got, "ghp_") {
t.Fatal("GitHub token not redacted in multi-secret text")
}
}