Triaging npm audit Findings
Permalink to "Triaging npm audit Findings"Part of Vulnerability Tracking & Triage, this page turns a wall of npm audit output into a short list of advisories that actually matter and shows how to remediate them without breaking the build.
Quick Reference
Permalink to "Quick Reference"| Control | Value | Effect |
|---|---|---|
| Severity levels | info, low, moderate, high, critical |
CVSS-derived rank shown per advisory |
--audit-level=<level> |
e.g. high |
Lowest severity that causes a non-zero exit |
--omit=dev |
flag | Excludes devDependencies from the scan |
--json |
flag | Emits structured advisory objects for tooling |
--production |
flag (legacy alias) | Older name for --omit=dev |
npm audit fix |
subcommand | Upgrades within the manifest’s allowed ranges |
npm audit fix --force |
subcommand | Allows semver-major upgrades — may break your app |
Default triage order: scan with --json, gate on --audit-level=high --omit=dev, then remediate the surviving advisories one at a time.
The mental model
Permalink to "The mental model"npm audit cross-references your installed dependency tree against the GitHub Advisory Database and reports every package whose resolved version falls in a vulnerable range. The raw report is noisy on purpose — it surfaces everything, including informational advisories and flaws buried in test-only tooling. Triage is the act of reducing that firehose to the advisories that are both exploitable in your context and reachable from shipped code, then deciding for each whether to upgrade, override, or accept the risk. Two dimensions drive nearly every decision: severity (how bad the flaw is) and reachability (whether the vulnerable code runs in production versus only at build time). The tooling exposes both — --audit-level filters on the first, --omit=dev filters on the second — and disciplined use of the two keeps a pipeline from either drowning in noise or waving through a critical.
Canonical example: parse --json and gate
Permalink to "Canonical example: parse --json and gate" Run the audit in machine-readable mode and let a small script decide the exit code. This example counts high-and-above advisories that affect production dependencies and fails the job only on those.
# Produce structured output for production deps only
npm audit --json --omit=dev > audit.json || true
// scripts/triage-audit.js — run: node scripts/triage-audit.js
import { readFileSync } from 'node:fs';
const report = JSON.parse(readFileSync('audit.json', 'utf8'));
const blocking = ['high', 'critical'];
const hits = Object.values(report.vulnerabilities ?? {})
.filter((v) => blocking.includes(v.severity));
for (const v of hits) {
console.log(`${v.severity.toUpperCase()} ${v.name} (fix available: ${Boolean(v.fixAvailable)})`);
}
if (hits.length > 0) {
console.error(`\n${hits.length} blocking advisory(ies) in production dependencies`);
process.exit(1);
}
console.log('No blocking production advisories');
Remediate the advisories the script prints. For most, the compatible upgrade path is:
# Applies upgrades that stay within package.json's declared ranges
npm audit fix
npm audit fix never crosses a semver-major boundary on its own, so it is safe to run in a branch and review the resulting lockfile diff.
Variants
Permalink to "Variants"Pin a patched transitive version with overrides
Permalink to "Pin a patched transitive version with overrides" When a vulnerable package is dragged in by a parent that has not shipped a fix, force the patched version directly in package.json:
{
"overrides": {
"vulnerable-lib": "1.4.2"
}
}
Run npm install to rewrite the lockfile, then re-audit. Scope the override under the parent ("parent-pkg": { "vulnerable-lib": "1.4.2" }) if you only want it applied in that subtree.
Gate CI at a chosen severity
Permalink to "Gate CI at a chosen severity"Skip the custom script when you only need a threshold:
# .github/workflows/audit.yml
- name: Audit production deps
run: npm audit --audit-level=high --omit=dev
The job exits non-zero only on high or critical production advisories; lower severities are reported but do not block.
Cross-check with osv-scanner
Permalink to "Cross-check with osv-scanner"npm audit reads the GitHub Advisory Database; a second source catches gaps. osv-scanner reads the lockfile against the OSV database:
osv-scanner --lockfile=package-lock.json
Reconciling both feeds is the job of the broader Vulnerability Tracking & Triage workflow.
Gotchas and Edge Cases
Permalink to "Gotchas and Edge Cases"-
Transitive fixes may be blocked by a parent.
npm audit fixwill not upgrade a nested dependency past the range its parent allows, so it silently leaves the advisory open. The report’sfixAvailablefield will be an object (naming the breaking parent) rather thantrue— that is your signal to use anoverridespin instead. -
DevDependency findings are usually false positives for production risk. A prototype-pollution advisory in a test runner does not ship to users. Do not let
--audit-levelon the full tree block a release; gate on--omit=devand track dev-only advisories on a slower cadence. -
--forcecan rewrite your app’s major versions.npm audit fix --forcewill happily bump a direct dependency across a semver-major boundary to clear an advisory, potentially breaking your build with no warning beyond a log line. Never run it unattended in CI — run it in a branch and test. -
A cleared audit is not a clean bill of health.
npm auditonly knows about published advisories. A zero-day or an unreported malicious package passes silently. Pair audit triage with provenance checks such as Verifying Sigstore Provenance for npm Packages. -
Exit codes differ from advisory counts. The command exits non-zero if any advisory meets the
--audit-levelthreshold, but the human summary can still list lower-severity items. In scripts, always drive decisions off the--jsonpayload, not the printed summary text.
Verification Steps
Permalink to "Verification Steps"1. Confirm the severity gate behaves
Permalink to "1. Confirm the severity gate behaves"npm audit --audit-level=critical --omit=dev; echo "exit: $?"
If only high-or-lower advisories exist, the exit code is 0; introduce or lower the threshold to high and confirm the exit flips to 1 when a matching advisory is present.
2. Confirm a remediation actually cleared the advisory
Permalink to "2. Confirm a remediation actually cleared the advisory"After applying npm audit fix or an override, re-run and check the summary counts:
npm audit --omit=dev --json | jq '.metadata.vulnerabilities'
Expected output for a clean production tree:
{
"info": 0, "low": 0, "moderate": 0, "high": 0, "critical": 0, "total": 0
}
3. CI gate — block the merge on production highs
Permalink to "3. CI gate — block the merge on production highs"- name: Triage npm audit
run: |
npm audit --json --omit=dev > audit.json || true
node scripts/triage-audit.js
A non-zero exit from the triage script blocks promotion; the printed advisory list gives the reviewer the exact packages to remediate.
Related
Permalink to "Related"- Vulnerability Tracking & Triage — the parent workflow for reconciling advisory feeds and tracking remediation SLAs
- Mapping CVEs to PCI DSS 6.4.3 — turning triaged CVEs into the payment-page script evidence auditors require
- Verifying Sigstore Provenance for npm Packages — catching malicious packages that no published advisory covers