import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; export type DependencyScope = "production" | "development"; export type LockfilePackage = Readonly<{ name: string; version: string; integrity: string; }>; export type DependencyInventoryRow = Readonly<{ name: string; version: string; direct: boolean; scope: DependencyScope; optional: boolean; license: string; dependencies: readonly string[]; }>; export type DependencyUpgrade = Readonly<{ name: string; from: string; to: string; }>; export type DependencyInventoryDiff = Readonly<{ added: readonly string[]; removed: readonly string[]; changed: readonly string[]; upgrades: readonly DependencyUpgrade[]; }>; type MutableDependencyRecord = { name: string; version: string; direct: boolean; scope: DependencyScope; optional: boolean; packagePath: string; dependencies: Set; }; type Document = Readonly>; function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function recordValue(value: unknown): Record { return isRecord(value) ? value : {}; } function recordRows(value: unknown): Record[] { return Array.isArray(value) ? value.filter(isRecord) : []; } function stringRows(value: unknown): string[] { return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : []; } function dependencyIdentity(row: Readonly>): string { return `${String(row.name ?? "")}@${String(row.version ?? "")}`; } export function canonicalizeSupplyChainValue(value: unknown): unknown { if (Array.isArray(value)) { return value .map(canonicalizeSupplyChainValue) .sort((left, right) => String(JSON.stringify(left)).localeCompare(String(JSON.stringify(right))), ); } if (isRecord(value)) { return Object.fromEntries( Object.entries(value) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, item]) => [key, canonicalizeSupplyChainValue(item)]), ); } return value; } export function supplyChainDigest(value: unknown): string { return createHash("sha256") .update(JSON.stringify(canonicalizeSupplyChainValue(value))) .digest("hex"); } export function parsePnpmLockfilePackages( lockfile: string, ): LockfilePackage[] { const entries: LockfilePackage[] = []; let inPackages = false; let current: { name: string; version: string; integrity: string } | null = null; for (const line of lockfile.split(/\r?\n/)) { if (line === "packages:") { inPackages = true; continue; } if (line === "snapshots:") { if (current) entries.push(current); break; } if (!inPackages) continue; const packageMatch = line.match(/^ {2}(\S.*):$/); if (packageMatch?.[1]) { if (current) entries.push(current); const key = packageMatch[1].replace(/^['"]|['"]$/g, ""); const separator = key.lastIndexOf("@"); current = { name: key.slice(0, separator), version: key.slice(separator + 1), integrity: "", }; continue; } const integrityMatch = line.match(/\bintegrity:\s*([^,}\s]+)/); if (current && integrityMatch?.[1]) { current.integrity = integrityMatch[1]; } } return entries.sort((left, right) => `${left.name}@${left.version}`.localeCompare( `${right.name}@${right.version}`, ), ); } export function isValidSha512Integrity(integrity: string): boolean { if (!integrity.startsWith("sha512-")) return false; try { return Buffer.from(integrity.slice("sha512-".length), "base64").length === 64; } catch { return false; } } export function normalizeLicense(raw: unknown): string { if (typeof raw === "string" && raw.trim()) return raw.trim(); if (isRecord(raw) && typeof raw.type === "string") return raw.type; if (Array.isArray(raw)) { const licenses = raw .map(normalizeLicense) .filter((license) => license !== "NOASSERTION"); return licenses.length > 0 ? licenses.join(" OR ") : "NOASSERTION"; } return "NOASSERTION"; } export async function flattenPnpmDependencyTree( root: Record, directProduction: Readonly>, directDevelopment: Readonly>, ): Promise { const records = new Map(); const directIds = new Set(); for (const [name, rawDependency] of Object.entries( recordValue(root.dependencies), )) { if (Object.hasOwn(directProduction, name) && isRecord(rawDependency)) { directIds.add(`${name}@${String(rawDependency.version ?? "")}`); } } for (const [name, rawDependency] of Object.entries( recordValue(root.devDependencies), )) { if (Object.hasOwn(directDevelopment, name) && isRecord(rawDependency)) { directIds.add(`${name}@${String(rawDependency.version ?? "")}`); } } function visit( node: Record, scope: DependencyScope, optionalPath: boolean, ): void { const groups = { dependencies: node.dependencies, devDependencies: node.devDependencies, optionalDependencies: node.optionalDependencies, }; for (const [groupName, group] of Object.entries(groups)) { for (const [name, rawDependency] of Object.entries(recordValue(group))) { if (!isRecord(rawDependency)) continue; const version = String(rawDependency.version ?? ""); const packagePath = String(rawDependency.path ?? ""); const identity = `${name}@${version}`; const childScope: DependencyScope = scope === "production" && groupName !== "devDependencies" ? "production" : "development"; const childOptional = optionalPath || groupName === "optionalDependencies"; const previous = records.get(identity); const dependencies = previous?.dependencies ?? new Set(); for (const childGroup of [ rawDependency.dependencies, rawDependency.optionalDependencies, ]) { for (const [childName, rawChild] of Object.entries( recordValue(childGroup), )) { if (isRecord(rawChild)) { dependencies.add( `${childName}@${String(rawChild.version ?? "")}`, ); } } } records.set(identity, { name, version, direct: directIds.has(identity), scope: previous?.scope === "production" || childScope === "production" ? "production" : "development", optional: previous ? previous.optional && childOptional : childOptional, packagePath: previous?.packagePath || packagePath, dependencies, }); visit(rawDependency, childScope, childOptional); } } } const productionRoot: Record = { dependencies: Object.fromEntries( Object.entries(recordValue(root.dependencies)).filter(([name]) => Object.hasOwn(directProduction, name), ), ), }; const developmentRoot: Record = { devDependencies: Object.fromEntries( Object.entries(recordValue(root.devDependencies)).filter(([name]) => Object.hasOwn(directDevelopment, name), ), ), }; visit(productionRoot, "production", false); visit(developmentRoot, "development", false); const result: DependencyInventoryRow[] = []; for (const record of records.values()) { let license = "NOASSERTION"; let optional = record.optional; if (record.packagePath) { try { const parsed: unknown = JSON.parse( await readFile(`${record.packagePath}/package.json`, "utf8"), ); const manifest = recordValue(parsed); license = normalizeLicense(manifest.license ?? manifest.licenses); } catch { // Platform-specific optional packages may not be materialized locally. optional = true; } } result.push({ name: record.name, version: record.version, direct: record.direct, scope: record.scope, optional, license, dependencies: [...record.dependencies].sort(), }); } return result.sort((left, right) => `${left.name}@${left.version}`.localeCompare( `${right.name}@${right.version}`, ), ); } export function diffDependencyInventories( before: Document, after: Document, ): DependencyInventoryDiff { const beforeRows = recordRows(before.dependencies); const afterRows = recordRows(after.dependencies); const beforeMap = new Map( beforeRows.map((row) => [dependencyIdentity(row), row] as const), ); const afterMap = new Map( afterRows.map((row) => [dependencyIdentity(row), row] as const), ); const added = [...afterMap.keys()].filter((key) => !beforeMap.has(key)); const removed = [...beforeMap.keys()].filter((key) => !afterMap.has(key)); const changed: string[] = []; for (const key of [...beforeMap.keys()].filter((item) => afterMap.has(item))) { if (supplyChainDigest(beforeMap.get(key)) !== supplyChainDigest(afterMap.get(key))) { changed.push(key); } } const upgrades: DependencyUpgrade[] = []; for (const removedKey of removed) { const previous = beforeMap.get(removedKey); if (!previous) continue; const replacement = added.find( (addedKey) => afterMap.get(addedKey)?.name === previous.name, ); const next = replacement ? afterMap.get(replacement) : undefined; if (next) { upgrades.push({ name: String(previous.name ?? ""), from: String(previous.version ?? ""), to: String(next.version ?? ""), }); } } return Object.freeze({ added: Object.freeze(added.sort()), removed: Object.freeze(removed.sort()), changed: Object.freeze(changed.sort()), upgrades: Object.freeze( upgrades.sort((left, right) => left.name.localeCompare(right.name)), ), }); } export function validateLicensePolicy( inventory: Document, policy: Document, ) { const allowed = new Set(stringRows(policy.allowedLicenses)); const denied = stringRows(policy.deniedLicensePatterns); const failures: string[] = []; const results: Array> = []; for (const dependency of recordRows(inventory.dependencies)) { const license = String(dependency.license ?? "NOASSERTION"); const explicitlyDenied = denied.some((pattern) => new RegExp(pattern, "i").test(license), ); const unknownAccepted = license === "NOASSERTION" && dependency.optional === true; const passed = !explicitlyDenied && (allowed.has(license) || unknownAccepted); results.push({ package: dependencyIdentity(dependency), license, passed, reason: unknownAccepted ? "platform-optional-not-materialized" : null, }); if (!passed) { failures.push( `${dependencyIdentity(dependency)} has disallowed license ${license}`, ); } } return Object.freeze({ passed: failures.length === 0, failures: Object.freeze(failures), results: Object.freeze(results), }); } export function validateDependencyReview( diff: DependencyInventoryDiff, inventory: Document, evidenceFile: Document, ) { const byIdentity = new Map( recordRows(inventory.dependencies).map( (row) => [dependencyIdentity(row), row] as const, ), ); const evidence = new Map>(); for (const entry of recordRows(evidenceFile.changes)) { if (typeof entry.changeId === "string") evidence.set(entry.changeId, entry); } const highRisk = diff.added.filter((identity) => { const row = byIdentity.get(identity); return row?.direct === true && row.scope === "production"; }); const failures: string[] = []; for (const identity of highRisk) { const changeId = `add:${identity}`; const entry = evidence.get(changeId); if (!entry) { failures.push(`high-risk dependency missing review: ${changeId}`); continue; } for (const field of ["owner", "reviewer", "reason", "rollback"] as const) { const value = entry[field]; if (typeof value !== "string" || !value.trim()) { failures.push(`${changeId} missing ${field}`); } } if (entry.owner === entry.reviewer) { failures.push(`${changeId} may not be self-approved`); } } return Object.freeze({ passed: failures.length === 0, highRisk: Object.freeze(highRisk), failures: Object.freeze(failures), }); } const severityRank: ReadonlyMap = new Map([ ["unknown", 0], ["low", 1], ["moderate", 2], ["high", 3], ["critical", 4], ]); export function validateVulnerabilityReport( report: Document, policy: Document, exceptionFile: Document, lockfileSha256: string, now: Date = new Date(), ) { const failures: string[] = []; if (report.scannedLockfileSha256 !== lockfileSha256) { failures.push("vulnerability report lockfile digest mismatch"); } if (typeof report.provider !== "string" || !report.provider.trim()) { failures.push("vulnerability report provider missing"); } const threshold = severityRank.get(String(policy.blockAtSeverity)) ?? 3; const exceptions = recordRows(exceptionFile.exceptions); const blocking: string[] = []; for (const finding of recordRows(report.findings)) { const severity = String(finding.severity ?? "unknown").toLowerCase(); if ((severityRank.get(severity) ?? 0) < threshold) continue; const exception = exceptions.find( (entry) => entry.vulnerabilityId === finding.id && entry.packageName === finding.packageName, ); const expiry = typeof exception?.expiresAt === "string" ? Date.parse(exception.expiresAt) : Number.NaN; const validException = Boolean( exception && typeof exception.owner === "string" && exception.owner.trim() && typeof exception.reviewer === "string" && exception.reviewer.trim() && exception.owner !== exception.reviewer && typeof exception.reason === "string" && exception.reason.trim() && Number.isFinite(expiry) && expiry > now.getTime(), ); if (!validException) { blocking.push( `${String(finding.id)}:${String(finding.packageName)}@${String(finding.version)}:${severity}`, ); } } return Object.freeze({ passed: failures.length === 0 && blocking.length === 0, failures: Object.freeze(failures), blocking: Object.freeze(blocking), }); } export function verifySupplyChainCoherence( sbom: Document, inventory: Document, provenance: Document, distDigest: string, ) { const failures: string[] = []; const componentCount = Array.isArray(sbom.components) ? sbom.components.length : -1; const dependencyCount = Array.isArray(inventory.dependencies) ? inventory.dependencies.length : -2; if (componentCount !== dependencyCount) { failures.push("SBOM component count does not match inventory"); } const metadata = recordValue(sbom.metadata); const properties = recordRows(metadata.properties); if ( properties.find( (property) => property.name === "ca:lockfileSha256" && property.value === inventory.lockfileSha256, ) === undefined ) { failures.push("SBOM lockfile digest does not match inventory"); } const subject = recordRows(provenance.subject)[0]; const subjectDigest = recordValue(subject?.digest); if (subjectDigest.sha256 !== distDigest) { failures.push("provenance subject does not match built dist digest"); } const predicate = recordValue(provenance.predicate); const materials = recordValue(predicate.materials); if (materials.lockfileSha256 !== inventory.lockfileSha256) { failures.push("provenance lockfile material does not match inventory"); } return Object.freeze({ passed: failures.length === 0, failures: Object.freeze(failures), }); }