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');

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 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. 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-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.

Permalink to "Related"

Related Articles

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