import { createHash } from "node:crypto"; export type SecretFinding = Readonly<{ ruleId: string; file: string; line: number; fingerprint: string; }>; export type SecretAllowlistEntry = Readonly<{ path: string; ruleId: string; expiresAt: string; }>; const secretPatterns: readonly Readonly<{ id: string; expression: RegExp; }>[] = [ { id: "private-key", expression: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, }, { id: "aws-access-key", expression: /\bAKIA[0-9A-Z]{16}\b/g }, { id: "github-token", expression: /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g }, { id: "assigned-secret", expression: /(?[] { return secretPatterns; } export function findSecretMatches( file: string, content: string, options: Readonly<{ allowlist?: readonly SecretAllowlistEntry[]; now?: number; }> = {}, ): SecretFinding[] { const allowlist = options.allowlist ?? []; const now = options.now ?? Date.now(); const findings: SecretFinding[] = []; for (const pattern of secretPatterns) { pattern.expression.lastIndex = 0; for (const match of content.matchAll(pattern.expression)) { const isAllowed = allowlist.some( (entry) => entry.path === file && entry.ruleId === pattern.id && Date.parse(entry.expiresAt) > now, ); if (isAllowed) continue; const matchIndex = match.index ?? 0; findings.push({ ruleId: pattern.id, file, line: content.slice(0, matchIndex).split(/\r?\n/u).length, fingerprint: createHash("sha256") .update(`${pattern.id}:${file}:${String(matchIndex)}`) .digest("hex"), }); } } return findings; }