import { createHash } from "node:crypto"; import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises"; import path from "node:path"; type SecretFinding = Readonly<{ ruleId: string; file: string; line: number; fingerprint: string; }>; type AllowlistEntry = Readonly<{ path: string; ruleId: string; owner: string; reason: string; expiresAt: string; }>; type SecretPolicy = Readonly<{ excludedPaths: readonly string[]; trackedRoots: readonly string[]; generatedRoots: readonly string[]; allowlist: readonly AllowlistEntry[]; }>; function argumentValue(name: string, fallback: string): string { const index = process.argv.indexOf(name); return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback; } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function strings(value: unknown): string[] { return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : []; } function parsePolicy(value: unknown): SecretPolicy { const document = isRecord(value) ? value : {}; const allowlist = Array.isArray(document.allowlist) ? document.allowlist.map((rawEntry) => { const entry = isRecord(rawEntry) ? rawEntry : {}; return { path: typeof entry.path === "string" ? entry.path : "", ruleId: typeof entry.ruleId === "string" ? entry.ruleId : "", owner: typeof entry.owner === "string" ? entry.owner : "", reason: typeof entry.reason === "string" ? entry.reason : "", expiresAt: typeof entry.expiresAt === "string" ? entry.expiresAt : "", }; }) : []; return Object.freeze({ excludedPaths: Object.freeze(strings(document.excludedPaths)), trackedRoots: Object.freeze(strings(document.trackedRoots)), generatedRoots: Object.freeze(strings(document.generatedRoots)), allowlist: Object.freeze(allowlist), }); } const policyPath = argumentValue( "--policy", "config/security/secret-scan-policy.json", ); const artifactPath = argumentValue( "--artifact", "artifacts/security/scan.sarif", ); const rawPolicy: unknown = JSON.parse(await readFile(policyPath, "utf8")); const policy = parsePolicy(rawPolicy); const findings: SecretFinding[] = []; const policyFailures: string[] = []; const patterns: 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: /\b(?:client_secret|password|private_key)\s*[:=]\s*["'][^"'${}]{12,}["']/gi, }, ]; async function filesWithin(target: string): Promise { try { const metadata = await stat(target); if (metadata.isFile()) return [target]; const entries = await readdir(target, { withFileTypes: true }); const nested: string[][] = await Promise.all( entries.map((entry) => { const child = path.join(target, entry.name); return entry.isDirectory() ? filesWithin(child) : [child]; }), ); return nested.flat(); } catch { return []; } } const excluded = new Set( policy.excludedPaths.map((entry) => entry.replaceAll("\\", "/")), ); const allowlist = policy.allowlist; for (const entry of allowlist) { const expiry = Date.parse(entry.expiresAt); if ( !entry.path.startsWith("tests/") || !entry.owner.trim() || !entry.reason.trim() || !Number.isFinite(expiry) || expiry <= Date.now() ) { policyFailures.push( `invalid or expired secret allowlist entry: ${entry.path}:${entry.ruleId}`, ); } } const roots = [...policy.trackedRoots, ...policy.generatedRoots]; const scanFiles = ( await Promise.all(roots.map((root) => filesWithin(root))) ).flat(); for (const scanFile of [...new Set(scanFiles)].sort()) { const normalized = scanFile.replaceAll("\\", "/"); if ( [...excluded].some( (entry) => normalized === entry || normalized.startsWith(`${entry}/`), ) || /\.(?:png|jpe?g|gif|webp|woff2?|zip|gz|sarif)$/i.test(normalized) ) { continue; } let content: string; try { content = await readFile(scanFile, "utf8"); } catch { continue; } for (const pattern of patterns) { pattern.expression.lastIndex = 0; for (const match of content.matchAll(pattern.expression)) { const isAllowed = allowlist.some( (entry) => entry.path === normalized && entry.ruleId === pattern.id && Date.parse(entry.expiresAt) > Date.now(), ); if (isAllowed) continue; const matchIndex = match.index ?? 0; const prefix = content.slice(0, matchIndex); findings.push({ ruleId: pattern.id, file: normalized, line: prefix.split(/\r?\n/).length, fingerprint: createHash("sha256") .update(`${pattern.id}:${normalized}:${String(matchIndex)}`) .digest("hex"), }); } } } const sarif = { version: "2.1.0", $schema: "https://json.schemastore.org/sarif-2.1.0.json", runs: [ { tool: { driver: { name: "ca-frontend-secret-scan", rules: patterns.map((pattern) => ({ id: pattern.id, shortDescription: { text: "Potential credential material" }, })), }, }, results: [ ...findings.map((finding) => ({ ruleId: finding.ruleId, message: { text: "Potential secret material must be removed." }, partialFingerprints: { primaryLocationLineHash: finding.fingerprint, }, locations: [ { physicalLocation: { artifactLocation: { uri: finding.file }, region: { startLine: finding.line }, }, }, ], })), ...policyFailures.map((failure) => ({ ruleId: "invalid-allowlist", message: { text: failure }, })), ], }, ], }; await mkdir(path.dirname(artifactPath), { recursive: true }); await writeFile(artifactPath, `${JSON.stringify(sarif, null, 2)}\n`); if (findings.length > 0 || policyFailures.length > 0) { process.stderr.write( `Security scan found ${findings.length + policyFailures.length} blocking result(s).\n`, ); process.exit(1); } process.stdout.write( `Tracked source, config, built asset and artifact secret scan: PASS (${scanFiles.length} files)\n`, );