Support reviewed npm audit baselines

This commit is contained in:
2026-09-01 20:06:16 +01:00
parent 8098495abf
commit 903e907225
5 changed files with 85 additions and 3 deletions
+44 -3
View File
@@ -67,11 +67,18 @@ export async function checkProject(options, injected = {}) {
for (const args of [
["ci", "--ignore-scripts"],
["audit", "signatures"],
["audit", "--omit=dev"],
["audit", "--omit=dev", "--json"],
]) {
const command = await execute(run, "npm", args, projectPath, options.commandTimeoutMs);
const isAdvisoryAudit = args.includes("--json");
const command = await execute(run, "npm", args, projectPath, options.commandTimeoutMs, isAdvisoryAudit ? 5_000_000 : 20_000);
report.commands.push(command.evidence);
if (command.result.code !== 0) report.failures.push(`npm ${args.join(" ")} failed${command.result.timedOut ? " (timed out)" : ""}`);
if (command.result.code !== 0 && isAdvisoryAudit && !command.result.timedOut) {
const assessment = assessAudit(command.result.stdout, options.policy, dependencies.now());
report.failures.push(...assessment.failures);
report.warnings.push(...assessment.warnings);
} else if (command.result.code !== 0) {
report.failures.push(`npm ${args.join(" ")} failed${command.result.timedOut ? " (timed out)" : ""}`);
}
if (command.result.code !== 0 && args[0] === "ci") break;
}
}
@@ -95,6 +102,40 @@ export async function checkProject(options, injected = {}) {
return report;
}
function assessAudit(stdout, policy, now) {
let audit;
try {
audit = JSON.parse(stdout);
} catch {
return { failures: ["npm audit returned invalid JSON"], warnings: [] };
}
if (!audit.vulnerabilities || typeof audit.vulnerabilities !== "object") {
return { failures: ["npm audit failed without vulnerability details"], warnings: [] };
}
const advisories = new Set();
let malformed = false;
for (const vulnerability of Object.values(audit.vulnerabilities)) {
for (const via of vulnerability.via ?? []) {
if (!via || typeof via !== "object") continue;
const id = String(via.url ?? "").match(/GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}/i)?.[0]?.toUpperCase();
if (id) advisories.add(id);
else malformed = true;
}
}
const failures = [];
if (malformed || advisories.size === 0) failures.push("npm audit returned vulnerability data without recognizable GHSA identifiers");
const accepted = [];
for (const id of advisories) {
const exception = policy.auditExceptions[id];
if (exception && Date.parse(exception.expiresAt) > now) accepted.push(id);
else failures.push(`npm audit advisory ${id} is not covered by an unexpired policy exception`);
}
return {
failures,
warnings: accepted.length ? [`npm audit reports ${accepted.length} reviewed advisory exception(s): ${accepted.sort().join(", ")}`] : [],
};
}
async function execute(run, command, args, cwd, timeoutMs = COMMAND_TIMEOUT_MS, maxOutputBytes = 20_000) {
const result = await run(command, args, { cwd, timeoutMs, maxOutputBytes });
return {