import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { buildRepositoryFileInventory, parseRepositoryFileInventoryPolicy, } from "./lib/repository-file-inventory.ts"; import { findSecretMatches, secretScanRules, type SecretFinding, } from "./lib/secret-scan.ts"; 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[]; optionalRoots: readonly string[]; includedPaths: 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 inventoryPolicy = parseRepositoryFileInventoryPolicy(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: inventoryPolicy.trackedRoots, generatedRoots: inventoryPolicy.generatedRoots, optionalRoots: inventoryPolicy.optionalRoots, includedPaths: Object.freeze(strings(document.includedPaths)), 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 = secretScanRules(); const excluded = new Set( policy.excludedPaths.map((entry) => entry.replaceAll("\\", "/")), ); const included = policy.includedPaths.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 inventory = await buildRepositoryFileInventory({ trackedRoots: policy.trackedRoots, generatedRoots: policy.generatedRoots, optionalRoots: policy.optionalRoots, }); const scanFiles = inventory.files; for (const scanFile of [...new Set(scanFiles)].sort()) { const normalized = scanFile.replaceAll("\\", "/"); if ( (included.length > 0 && !included.some( (entry) => normalized === entry || normalized.startsWith(`${entry}/`), )) || [...excluded].some( (entry) => normalized === entry || normalized.startsWith(`${entry}/`), ) || /\.(?:png|jpe?g|gif|webp|woff2?|zip|gz|sarif)$/i.test(normalized) ) { continue; } const content = await readFile(scanFile, "utf8"); findings.push( ...findSecretMatches(normalized, content, { allowlist }), ); } 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`, );