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');
The payload that script reads is a map, not a list: every key under vulnerabilities is a package name and its value carries the handful of fields triage actually turns on. severity is the CVSS-derived rank the --audit-level gate compares against. isDirect says whether your own manifest names the package or a parent dragged it in, which decides whether a plain version bump is even available to you. via holds either the advisory objects themselves or the names of the intermediate packages that pull the vulnerable code in, so it doubles as the dependency path. range is the affected version range, and fixAvailable is the field that determines the remediation route. A parallel report.metadata.vulnerabilities object holds the per-severity counts printed in the human summary — useful for reporting, useless for deciding.
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. Read that diff rather than merging it blind: an audit-driven upgrade can quietly change the resolved version, registry URL or integrity hash of packages you never asked about, and the same review discipline described in Detecting Lockfile Tampering in Pull Requests applies to a remediation commit exactly as it does to a dependency bump from a contributor.
Variants
Permalink to "Variants"Which of these you reach for is not a matter of taste — one field decides it. An advisory whose fixAvailable is true is already handled by npm audit fix; one whose fixAvailable is an object needs a manual pin because npm would otherwise have to break a declared range; one that is false has no patched release at all, and the only honest responses are a compensating control or a recorded, time-boxed acceptance. Every route ends at the same place: a re-audit that proves the finding is gone rather than merely rearranged.
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 and triage workflow: OSV aggregates ecosystem sources that the GitHub Advisory Database sometimes lags, so an advisory that osv-scanner reports and npm audit does not is a real finding, not a false positive, and belongs in the same queue.
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, and a typosquat published an hour ago has no advisory to match against at all. Pair audit triage with provenance checks such as Verifying Sigstore Provenance for npm Packages, and remove the install-time execution path entirely by Disabling npm Install Scripts — the most common way a package with no advisory against it still runs code on your build agent. -
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.
Frequently Asked Questions
Permalink to "Frequently Asked Questions"What severity levels does npm audit report?
npm audit classifies advisories as info, low, moderate, high, and critical, mapped from the advisory’s CVSS score. --audit-level sets the lowest severity that causes a non-zero exit, so --audit-level=high fails only on high and critical findings.
Should a vulnerability in a devDependency fail my build?
Usually not. A flaw in a build-time or test-only package rarely reaches production. Run npm audit --omit=dev to see the advisories that affect shipped code, and gate CI on that result while tracking dev-only findings separately.
Why does npm audit fix not resolve a transitive advisory?
The vulnerable package is pinned by a parent that requires the old version. npm audit fix will not force an incompatible upgrade. Use an overrides block to pin the transitive dependency to a patched version, or wait for the parent to release a compatible update.
What does the fixAvailable field actually tell me?
fixAvailable is either a boolean or an object. true means a release that satisfies your declared ranges clears the advisory, so npm audit fix resolves it unattended. An object names the package npm would have to change and the version it would install, which almost always implies a semver-major bump — that is the signal to add an overrides pin instead. false means no patched release exists yet, so the only options are mitigation or documented acceptance.
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
- Registry & Package Manager Hardening — the install-time controls that keep vulnerable and unvetted versions out of the tree before audit ever sees them