Something went wrong. Try again.
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374/** * Finds things that look like secrets in text, so they are caught before they are committed. * The report never contains the secret: only the file, line, rule and the first four characters. * A line carrying `aite:allow-secret` is skipped (for deliberate test fixtures). */
export interface Finding { file: string; line: number; column: number; rule: string; redacted: string;}
interface Rule { name: string; pattern: RegExp; /** Extra condition on the whole line (for rules that are only suspicious in context). */ lineMustMatch?: RegExp;}
const RULES: Rule[] = [ { name: "gitlab-token", pattern: /glpat-[A-Za-z0-9_-]{10,}/g }, { name: "private-key", pattern: /-----BEGIN (?:[A-Z]+ )*PRIVATE KEY-----/g }, { name: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g }, { name: "app-password", pattern: /\b[a-z0-9]{4}(?:-[a-z0-9]{4}){3}\b/g, lineMustMatch: /app[ _-]?pass(?:word)?|password|passwd|pwd/i, }, { name: "hex-private-key", pattern: /(?:priv(?:ate)?[_-]?key|secret|mnemonic)\w*\s*[:=]\s*["']?(?:0x)?[0-9a-fA-F]{64}\b/gi, },];
const ALLOW_MARKER = "aite:allow-secret";
export function redact(secret: string): string { return secret.length <= 4 ? "…" : `${secret.slice(0, 4)}…`;}
export function scanText(text: string, file: string): Finding[] { if (text.includes("\u0000")) return []; const findings: Finding[] = []; text.split(/\r?\n/).forEach((lineText, index) => { if (lineText.includes(ALLOW_MARKER)) return; for (const rule of RULES) { if (rule.lineMustMatch && !rule.lineMustMatch.test(lineText)) continue; for (const match of lineText.matchAll(rule.pattern)) { findings.push({ file, line: index + 1, column: (match.index ?? 0) + 1, rule: rule.name, redacted: redact(match[0]) }); } } }); return findings.sort((a, b) => a.line - b.line || a.column - b.column);}
const SKIP_DIRS = new Set(["node_modules", "dist", "coverage", "reference", "results", ".git"]);const SKIP_EXT = /\.(zip|png|jpe?g|gif|ico|woff2?|pdf)$/i;
/** Which tracked files are worth reading as text. */export function shouldScanPath(path: string): boolean { const parts = path.split("/"); const name = parts[parts.length - 1] ?? ""; if (parts.slice(0, -1).some((p) => SKIP_DIRS.has(p))) return false; if (name === "package-lock.json") return false; return !SKIP_EXT.test(name);}
export function formatFindings(findings: Finding[]): string { if (findings.length === 0) return "no secrets found"; return findings.map((f) => `${f.file}:${f.line}:${f.column}: ${f.rule} (${f.redacted})`).join("\n");}