import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; /** @param {unknown} value @returns {unknown} */ export function canonicalizeSupplyChainValue(value) { if (Array.isArray(value)) { return value .map(canonicalizeSupplyChainValue) .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)), ); } if (value && typeof value === "object") { return Object.fromEntries( Object.entries(value) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, item]) => [key, canonicalizeSupplyChainValue(item)]), ); } return value; } /** @param {unknown} value */ export function supplyChainDigest(value) { return createHash("sha256") .update(JSON.stringify(canonicalizeSupplyChainValue(value))) .digest("hex"); } /** @param {string} lockfile */ export function parsePnpmLockfilePackages(lockfile) { const entries = /** @type {Array<{name: string, version: string, integrity: string}>} */ ( [] ); let inPackages = false; /** @type {{name: string, version: string, integrity: string} | null} */ let current = 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) { 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) { current.integrity = integrityMatch[1]; } } return entries.sort((left, right) => `${left.name}@${left.version}`.localeCompare( `${right.name}@${right.version}`, ), ); } /** @param {string} integrity */ export function isValidSha512Integrity(integrity) { if (!integrity.startsWith("sha512-")) return false; try { return Buffer.from(integrity.slice("sha512-".length), "base64").length === 64; } catch { return false; } } /** * @param {unknown} raw * @returns {string} */ export function normalizeLicense(raw) { if (typeof raw === "string" && raw.trim()) return raw.trim(); if ( raw && typeof raw === "object" && "type" in 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"; } /** * @param {Record} root * @param {Readonly>} directProduction * @param {Readonly>} directDevelopment */ export async function flattenPnpmDependencyTree( root, directProduction, directDevelopment, ) { const records = /** @type {Map * }>} */ (new Map()); const directIds = new Set(); for (const [name, rawDependency] of Object.entries( /** @type {Record} */ (root.dependencies ?? {}), )) { if ( Object.hasOwn(directProduction, name) && rawDependency && typeof rawDependency === "object" && !Array.isArray(rawDependency) ) { directIds.add( `${name}@${String( /** @type {Record} */ (rawDependency).version ?? "", )}`, ); } } for (const [name, rawDependency] of Object.entries( /** @type {Record} */ (root.devDependencies ?? {}), )) { if ( Object.hasOwn(directDevelopment, name) && rawDependency && typeof rawDependency === "object" && !Array.isArray(rawDependency) ) { directIds.add( `${name}@${String( /** @type {Record} */ (rawDependency).version ?? "", )}`, ); } } /** * @param {Record} node * @param {"production" | "development"} scope * @param {boolean} optionalPath */ function visit(node, scope, optionalPath) { for (const [groupName, group] of Object.entries({ dependencies: node.dependencies, devDependencies: node.devDependencies, optionalDependencies: node.optionalDependencies, })) { if (!group || typeof group !== "object" || Array.isArray(group)) continue; for (const [name, rawDependency] of Object.entries(group)) { if ( !rawDependency || typeof rawDependency !== "object" || Array.isArray(rawDependency) ) { continue; } const dependency = /** @type {Record} */ (rawDependency); const version = String(dependency.version ?? ""); const packagePath = String(dependency.path ?? ""); const identity = `${name}@${version}`; const childScope = 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 [ dependency.dependencies, dependency.optionalDependencies, ]) { if ( !childGroup || typeof childGroup !== "object" || Array.isArray(childGroup) ) { continue; } for (const [childName, rawChild] of Object.entries(childGroup)) { if ( rawChild && typeof rawChild === "object" && !Array.isArray(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(dependency, childScope, childOptional); } } } const productionRoot = { dependencies: Object.fromEntries( Object.entries( /** @type {Record} */ (root.dependencies ?? {}), ).filter(([name]) => Object.hasOwn(directProduction, name)), ), }; const developmentRoot = { devDependencies: Object.fromEntries( Object.entries( /** @type {Record} */ (root.devDependencies ?? {}), ).filter(([name]) => Object.hasOwn(directDevelopment, name)), ), }; visit(productionRoot, "production", false); visit(developmentRoot, "development", false); const result = []; for (const record of records.values()) { let license = "NOASSERTION"; let optional = record.optional; if (record.packagePath) { try { const manifest = JSON.parse( await readFile(`${record.packagePath}/package.json`, "utf8"), ); 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}`, ), ); } /** * @param {Readonly>} before * @param {Readonly>} after */ export function diffDependencyInventories(before, after) { const beforeRows = /** @type {Array>} */ (before.dependencies ?? []); const afterRows = /** @type {Array>} */ (after.dependencies ?? []); const beforeMap = new Map( beforeRows.map((row) => [`${row.name}@${row.version}`, row]), ); const afterMap = new Map( afterRows.map((row) => [`${row.name}@${row.version}`, row]), ); const added = [...afterMap.keys()].filter((key) => !beforeMap.has(key)); const removed = [...beforeMap.keys()].filter((key) => !afterMap.has(key)); const changed = []; 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 = []; for (const removedKey of removed) { const previous = beforeMap.get(removedKey); const replacement = added.find( (addedKey) => afterMap.get(addedKey)?.name === previous?.name, ); if (replacement) { upgrades.push({ name: previous?.name, from: previous?.version, to: afterMap.get(replacement)?.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) => String(left.name).localeCompare(String(right.name)), ), ), }); } /** * @param {Readonly>} inventory * @param {Readonly>} policy */ export function validateLicensePolicy(inventory, policy) { const allowed = new Set( /** @type {string[]} */ (policy.allowedLicenses ?? []), ); const denied = /** @type {string[]} */ (policy.deniedLicensePatterns ?? []); const failures = []; const results = []; for (const dependency of /** @type {Array>} */ ( 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: `${dependency.name}@${dependency.version}`, license, passed, reason: unknownAccepted ? "platform-optional-not-materialized" : null, }); if (!passed) { failures.push( `${dependency.name}@${dependency.version} has disallowed license ${license}`, ); } } return Object.freeze({ passed: failures.length === 0, failures: Object.freeze(failures), results: Object.freeze(results), }); } /** * @param {ReturnType} diff * @param {Readonly>} inventory * @param {Readonly>} evidenceFile */ export function validateDependencyReview(diff, inventory, evidenceFile) { const rows = /** @type {Array>} */ (inventory.dependencies ?? []); const byIdentity = new Map( rows.map((row) => [`${row.name}@${row.version}`, row]), ); const evidence = new Map( /** @type {Array>} */ ( evidenceFile.changes ?? [] ).map((entry) => [entry.changeId, entry]), ); const highRisk = diff.added.filter((identity) => { const row = byIdentity.get(identity); return row?.direct === true && row.scope === "production"; }); const failures = []; 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"]) { if (typeof entry[field] !== "string" || !entry[field].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 = new Map([ ["unknown", 0], ["low", 1], ["moderate", 2], ["high", 3], ["critical", 4], ]); /** * @param {Readonly>} report * @param {Readonly>} policy * @param {Readonly>} exceptionFile * @param {string} lockfileSha256 * @param {Date} [now] */ export function validateVulnerabilityReport( report, policy, exceptionFile, lockfileSha256, now = new Date(), ) { const failures = []; 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 = /** @type {Array>} */ ( exceptionFile.exceptions ?? [] ); const blocking = []; for (const finding of /** @type {Array>} */ ( 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 = 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( `${finding.id}:${finding.packageName}@${finding.version}:${severity}`, ); } } return Object.freeze({ passed: failures.length === 0 && blocking.length === 0, failures: Object.freeze(failures), blocking: Object.freeze(blocking), }); } /** * @param {Readonly>} sbom * @param {Readonly>} inventory * @param {Readonly>} provenance * @param {string} distDigest */ export function verifySupplyChainCoherence( sbom, inventory, provenance, distDigest, ) { const failures = []; 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 = /** @type {Record} */ (sbom.metadata ?? {}); const properties = /** @type {Array<{name?: string, value?: string}>} */ ( metadata.properties ?? [] ); if (properties.find( /** @param {{name?: string, value?: string}} property */ (property) => property.name === "ca:lockfileSha256" && property.value === inventory.lockfileSha256, ) === undefined) { failures.push("SBOM lockfile digest does not match inventory"); } const subject = /** @type {Array>} */ (provenance.subject ?? [])[0]; const subjectDigest = /** @type {Record} */ (subject?.digest ?? {}); if (subjectDigest.sha256 !== distDigest) { failures.push("provenance subject does not match built dist digest"); } const predicate = /** @type {Record} */ (provenance.predicate ?? {}); const materials = /** @type {Record} */ (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), }); }