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.

npm audit triage funnel All advisories pass through an audit-level severity filter and an omit-dev reachability filter, leaving a small blocking set that is remediated, while filtered advisories are tracked separately. all advisories (--json) severity filter --audit-level reachability --omit=dev blocking set → remediate filtered out → track, don't block

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.

Anatomy of an npm audit JSON entry One entry of the vulnerabilities map broken into its severity, isDirect, via, range and fixAvailable fields, each annotated with the triage decision it drives. report.vulnerabilities["lodash"] = { severity "high" compared with --audit-level isDirect false yours to bump, or a parent's via [ advisory, … ] advisory text and dep path range <4.17.21 versions the flaw affects fixAvailable true | object picks the remediation route

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.

Remediation route for a surviving advisory A decision tree branching on the fixAvailable field into npm audit fix, an overrides pin, or mitigation and acceptance, with all three branches converging on a re-audit that must report zero high and critical findings. surviving advisory: read fixAvailable true object false npm audit fix upgrade within range add an overrides pin then npm install no patch published mitigate or accept re-audit: npm audit --omit=dev expect 0 high, 0 critical

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 fix will not upgrade a nested dependency past the range its parent allows, so it silently leaves the advisory open. The report’s fixAvailable field will be an object (naming the breaking parent) rather than true — that is your signal to use an overrides pin 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-level on the full tree block a release; gate on --omit=dev and track dev-only advisories on a slower cadence.

  • --force can rewrite your app’s major versions. npm audit fix --force will 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 audit only 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-level threshold, but the human summary can still list lower-severity items. In scripts, always drive decisions off the --json payload, 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.

Permalink to "Related"

Related Articles

Mapping CVEs to PCI DSS 6.4.3
Vulnerability Tracking & Triage Supply Chain Auditing & Depend…