Files
multica/server/pkg/redact/redact_test.go
YOMXXX c599f47ba4 fix(redact): cover GitHub fine-grained PATs and Google API keys (#4678)
The primary server redactor now covers github_pat_ and standard AIza keys, but the client transcript safety net does not. Add matching client patterns and regression coverage so previously stored or otherwise unredacted transcript values are masked at display time.

Also make the fixed-length Google key rule redact a key ending in '-' while preserving its trailing delimiter. Keep the 39-character format exact to avoid broadening false positives.

Co-authored-by: multica-agent <github@multica.ai>
2026-07-15 16:06:59 +08:00

317 lines
10 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 TestRedactGoogleAPIKeyEndingWithDash(t *testing.T) {
t.Parallel()
input := `the config file still had "` + asm("AIza", "SyB1cD3fGhIjKlMnOpQrStUvWxYz012345-") + `" in it`
got := Text(input)
if strings.Contains(got, asm("AIza", "SyB1cD3f")) {
t.Fatalf("Google API key ending with dash not redacted: %s", got)
}
if !strings.Contains(got, `"[REDACTED GOOGLE API KEY]" in it`) {
t.Fatalf("expected delimiter to be preserved, 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")
}
}