← back to Secrets Manager
transcript-secret-lib.mjs
84 lines
// transcript-secret-lib.mjs — shared high-confidence secret detection for the
// Claude Code transcript scanner + redactor (TK-11683).
//
// WHY A SHARED LIB: the redactor MUST mask exactly what the scanner FLAGS. If the
// two carried independent copies of the pattern list they would drift, and the
// gated sweep memo's count would no longer match what redaction actually touches
// (the producer/consumer false-green class). One source of truth prevents that.
//
// HARD SECURITY RULE: nothing here ever emits a raw secret value. Findings carry
// {file, line, pattern, hc, last4, sha256_16} only.
import fs from 'node:fs';
import crypto from 'node:crypto';
// ─── high-confidence patterns ────────────────────────────────────────────────
// Reuse the `secrets` skill's leak_patterns (routes.json) verbatim, then add the
// provider prefixes the ticket calls out that aren't already there. Each pattern
// carries capture group 1 = the token where the regex has a var-name prefix, else
// the whole match. `hc: true` = high-confidence (drives the FAIL verdict / is what
// the redactor masks); the heuristic env-leak/bearer patterns are hc:false
// (reported, but do NOT alone FAIL and are NOT masked by the redactor).
export function loadPatterns(routesPath) {
let base = [];
try {
const routes = JSON.parse(fs.readFileSync(routesPath, 'utf8'));
base = (routes.leak_patterns || []).map(p => ({ ...p, hc: true }));
} catch { /* routes.json missing → still run with the additions below */ }
const additions = [
// provider prefixes explicitly named in TK-11683, not in routes.json
{ name: 'aws-access-key-id', regex: 'AKIA[0-9A-Z]{16}', hc: true },
{ name: 'aws-secret-access-key', regex: 'aws_secret_access_key\\s*[=:]\\s*["\']?([A-Za-z0-9/+]{40})', hc: true },
{ name: 'gitlab-pat', regex: 'glpat-[A-Za-z0-9_-]{20,}', hc: true },
{ name: 'sendgrid-key', regex: 'SG\\.[A-Za-z0-9_-]{22}\\.[A-Za-z0-9_-]{43}', hc: true },
{ name: 'twilio-sid', regex: 'AC[0-9a-fA-F]{32}', hc: true },
{ name: 'private-key-block', regex: '-----BEGIN (?:RSA |EC |OPENSSH |PGP |DSA )?PRIVATE KEY-----', hc: true },
// heuristic (reported, redacted in the report, but not a sole FAIL trigger) — noisier classes
{ name: 'bearer-token', regex: 'Bearer\\s+([A-Za-z0-9._~+/-]{24,}=*)', hc: false },
{ name: 'env-key-assignment', regex: '(?:[A-Z0-9_]*(?:API_?KEY|SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_?KEY|ACCESS_?KEY))\\s*[=:]\\s*["\']?([A-Za-z0-9_\\-./+]{20,})', hc: false },
];
// de-dupe by name (routes.json wins if a name collides)
const seen = new Set(base.map(p => p.name));
for (const a of additions) if (!seen.has(a.name)) base.push(a);
return base;
}
// Values that are obviously placeholders / examples — never a real live secret.
// Filtering these BEFORE hashing keeps the count honest; we still never emit a value.
export function isPlaceholder(v) {
const s = String(v);
if (/^x+$/i.test(s)) return true;
if (/[<>${}]/.test(s)) return true; // <TOKEN>, ${VAR}, {{ }}
if (/^(your|my|the|example|sample|placeholder|changeme|redacted|xxx|test123|dummy|fake|none|null|undefined)/i.test(s)) return true;
if (/(example|placeholder|redacted|xxxxxxxx|your_?key|your_?token|dummy|abcdef123456)/i.test(s)) return true;
if (/^0+$/.test(s)) return true;
return false;
}
export function digest(value) {
return {
last4: String(value).slice(-4),
sha256_16: crypto.createHash('sha256').update(String(value)).digest('hex').slice(0, 16),
};
}
// Scan one file's text; push redacted findings. Mutates `findings` and `hcSeen`.
export function scanText(file, text, patterns, findings, hcSeen) {
for (const p of patterns) {
let re;
try { re = new RegExp(p.regex, 'g'); } catch { continue; }
let m;
while ((m = re.exec(text)) !== null) {
const raw = (m[1] !== undefined ? m[1] : m[0]);
if (m[0].length === 0) { re.lastIndex++; continue; } // guard zero-width
if (isPlaceholder(raw)) continue;
const line = text.slice(0, m.index).split('\n').length;
const d = digest(raw);
findings.push({ file, line, pattern: p.name, hc: !!p.hc, last4: d.last4, sha256_16: d.sha256_16 });
if (p.hc) hcSeen.add(d.sha256_16);
}
}
}