diff --git a/src/core/redact-secrets.test.ts b/src/core/redact-secrets.test.ts index fa463c3a..75a23853 100644 --- a/src/core/redact-secrets.test.ts +++ b/src/core/redact-secrets.test.ts @@ -125,3 +125,35 @@ describe('redactSecrets', () => { expect(reparsed.sessions[0].note).toBe('ok'); }); }); + +describe('compound secret key names', () => { + it('redacts underscore-prefixed key names that a word boundary would skip', () => { + for (const name of ['client_secret', 'refresh_token', 'app_token', 'db_password']) { + const out = redactSecrets(`"${name}":"abcdefgh12345678"`); + expect(out).toContain('[REDACTED]'); + expect(out).not.toContain('abcdefgh12345678'); + } + }); + + it('redacts JSON escaped by doubling its quotes inside a shell command', () => { + const rule = 'Bash(curl -d \'{""client_secret"":""8bavjPwL9uoXDp""}\')'; + const out = redactSecrets(rule); + expect(out).not.toContain('8bavjPwL9uoXDp'); + expect(out).toContain('[REDACTED]'); + }); + + it('redacts a Context7 api key', () => { + const out = redactSecrets('"CONTEXT7_API_KEY": "ctx7sk-1234abcd-5678-efgh-9012-ijklmnop"'); + expect(out).not.toContain('ctx7sk-1234abcd'); + }); + + it('leaves ordinary prose mentioning those words intact', () => { + for (const text of [ + 'The token bucket algorithm smooths bursty traffic.', + 'Set a secret in your CI provider, then reference it.', + 'const tokenizer = new Tokenizer();', + ]) { + expect(redactSecrets(text)).toBe(text); + } + }); +}); diff --git a/src/core/redact-secrets.ts b/src/core/redact-secrets.ts index 4ed9d945..1cf68d8e 100644 --- a/src/core/redact-secrets.ts +++ b/src/core/redact-secrets.ts @@ -11,6 +11,11 @@ * keys, tokens, private keys). Matching is intentionally conservative -- * well-known token prefixes and explicit key/value assignments only -- so * ordinary prose and code remain readable. + * + * Key names are matched with an optional compound prefix, so `client_secret` + * and `refresh_token` are caught as well as bare `secret` and `token`. Quote + * runs of one or two characters are accepted, because JSON embedded in a + * shell command is commonly escaped by doubling its quotes. */ const PRIVATE_KEY_BLOCK = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY( BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY( BLOCK)?-----|$)/g; @@ -21,16 +26,20 @@ const SLACK_APP_TOKEN = /\bxapp-[0-9]-[A-Za-z0-9-]{10,250}\b/g; const GOOGLE_API_KEY = /\bAIza[0-9A-Za-z_-]{35}\b/g; const NPM_TOKEN = /\bnpm_[A-Za-z0-9]{36}\b/g; const GITLAB_PAT = /\bglpat-[A-Za-z0-9_-]{20,80}\b/g; +const CONTEXT7_KEY = /\bctx7sk-[A-Za-z0-9_-]{10,250}\b/g; const SK_API_KEY = /\bsk-[A-Za-z0-9_-]{20,250}\b/g; // Stripe / underscore-prefixed keys (sk_live_, sk_test_, rk_live_, pk_live_, …). const STRIPE_KEY = /\b[srp]k_(?:live|test)_[0-9A-Za-z]{10,255}\b/g; // Credentials embedded in a connection-string URI (scheme://user:pass@host). const CONNECTION_STRING_CREDS = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s:@/]+:[^\s@/]+@/gi; // Quoted assignment whose value may contain spaces: "password": "two words here". -const QUOTED_SECRET_ASSIGNMENT = /\b(api[_-]?key|access[_-]?key|secret|token|password|passwd|credentials?)(["']?\s*[:=]\s*)(["'])((?:(?!\3).){4,512})\3/gi; +const SECRET_KEY_NAME = String.raw`(?:[A-Za-z0-9]+[_-])*(?:api[_-]?key|access[_-]?key|secret|token|password|passwd|credentials?)`; +const QUOTED_SECRET_ASSIGNMENT = new RegExp( + String.raw`\b(${SECRET_KEY_NAME})(["']{0,2}\s*[:=]\s*)(["']{1,2})((?:(?!\3).){4,512})\3`, 'gi'); const JWT = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g; const AUTH_HEADER = /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{16,512}/gi; -const KEY_VALUE_ASSIGNMENT = /\b(api[_-]?key|access[_-]?key|secret|token|password|passwd|credentials?)(["']?\s*[:=]\s*)(["']?)[^\s"'`;,]{8,512}\3/gi; +const KEY_VALUE_ASSIGNMENT = new RegExp( + String.raw`\b(${SECRET_KEY_NAME})(["']{0,2}\s*[:=]\s*)(["']{0,2})[^\s"'\`;,]{8,512}\3`, 'gi'); /** Mask credential-shaped substrings, keeping surrounding text intact. */ export function redactSecrets(text: string): string { @@ -44,6 +53,7 @@ export function redactSecrets(text: string): string { .replaceAll(GOOGLE_API_KEY, '[REDACTED:google-api-key]') .replaceAll(NPM_TOKEN, '[REDACTED:npm-token]') .replaceAll(GITLAB_PAT, '[REDACTED:gitlab-token]') + .replaceAll(CONTEXT7_KEY, '[REDACTED:api-key]') .replaceAll(STRIPE_KEY, '[REDACTED:stripe-key]') .replaceAll(SK_API_KEY, '[REDACTED:api-key]') .replaceAll(JWT, '[REDACTED:jwt]')