qwen-code/scripts/audit-runtime-critical.js
Shaojin Wen 0f56e35c0a
ci: keep the critical-audit gate honest when npm cannot answer (#7743)
npm retired the `security/audits/quick` endpoint, which now 400s for this
package tree. `npm audit` exits 1 for that exactly as it does for a real
critical finding, so every PR in the repo went red on 2026-07-26 with
"Invalid package tree" — a failure no branch can fix.

The exit code alone cannot gate a merge: treating every non-zero as
vulnerable blocks the repo on an npm outage, and ignoring it retires the
gate. The payload separates them — a real audit always carries
metadata.vulnerabilities, a transport failure carries the request error.

Classify on that: a finding still fails, an unreachable endpoint warns and
passes, and anything unrecognised fails closed so a payload-shape change
gets a human rather than a silently disabled gate.

Co-authored-by: wenshao <wenshao@example.com>
2026-07-26 06:59:13 +00:00

88 lines
2.9 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { spawnSync } from 'node:child_process';
import { pathToFileURL } from 'node:url';
const AUDIT_ARGS = ['audit', '--omit=dev', '--audit-level=critical', '--json'];
/**
* Decide what `npm audit --json` actually told us.
*
* npm exits 1 both when it finds a critical vulnerability and when it never
* reached the registry, so the exit code alone cannot gate a merge: treating
* every non-zero as "vulnerable" blocks the whole repository on an npm outage,
* and treating it as "fine" silently disables the gate. The payload separates
* them — a real audit always carries `metadata.vulnerabilities`, while a
* transport failure carries the request error instead.
*
* @param {string} stdout raw stdout from `npm audit --json`
* @returns {'audited' | 'unreachable' | 'unreadable'}
*/
export function classifyAuditOutput(stdout) {
let parsed;
try {
parsed = JSON.parse(stdout);
} catch {
return 'unreadable';
}
if (parsed?.metadata?.vulnerabilities) {
return 'audited';
}
if (parsed?.error || typeof parsed?.statusCode === 'number') {
return 'unreachable';
}
return 'unreadable';
}
function main() {
const result = spawnSync('npm', AUDIT_ARGS, {
encoding: 'utf8',
shell: process.platform === 'win32',
});
const stdout = result.stdout ?? '';
const verdict = classifyAuditOutput(stdout);
if (verdict === 'audited') {
const counts = JSON.parse(stdout).metadata.vulnerabilities;
if (result.status === 0) {
console.log(
`No critical runtime vulnerabilities (${counts.critical} critical).`,
);
process.exitCode = 0;
return;
}
console.error('npm audit reported vulnerabilities at or above `critical`:');
console.error(JSON.stringify(counts, null, 2));
process.exitCode = result.status ?? 1;
return;
}
if (verdict === 'unreachable') {
// The registry, not this repository. Warn loudly and let the build pass:
// an npm-side outage must not gate every PR in the repo on a service we
// do not run. A real finding still fails above.
console.warn(
'WARNING: npm audit could not reach the advisory endpoint, so critical runtime dependencies were NOT audited this run.',
);
console.warn((result.stderr ?? '').trim() || stdout.trim());
process.exitCode = 0;
return;
}
// Unrecognised output: fail closed. If npm changes its payload shape, a red
// build gets a human to look rather than quietly retiring the gate.
console.error(
'npm audit produced output this checker does not recognise; failing closed.',
);
console.error(stdout.trim() || (result.stderr ?? '').trim());
process.exitCode = result.status === 0 ? 1 : (result.status ?? 1);
}
// Importable for tests, executable as the npm script.
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
main();
}