Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/core/redact-secrets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
});
14 changes: 12 additions & 2 deletions src/core/redact-secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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]')
Expand Down
Loading