Safeguard
Security Guides

npm audit: The Complete Guide to Auditing Node.js Dependencies

How npm audit really works, the exact commands to run in CI, where it silently falls short, and how to close the gaps with reachability-aware SCA and autonomous fixes.

Priya Mehta
Security Researcher
6 min read

A typical Node.js service declares a few dozen dependencies in package.json and then quietly installs several hundred more. Those extras — the transitive graph — are packages your team never chose, never reviewed, and in most cases has never heard of. When one of them ships a vulnerable version, it lands in your build the next time someone runs npm install, and nobody notices until a scanner shouts. That is the problem npm audit was built to surface, and it is worth understanding both what it does well and where it leaves you exposed.

What npm audit actually does

npm audit reads your package-lock.json (or npm-shrinkwrap.json), builds the full resolved dependency tree, and submits the list of package names and versions to the npm registry's audit endpoint. That endpoint compares your versions against the GitHub Advisory Database and returns any known vulnerabilities, including ones buried deep in transitive dependencies. Crucially, it audits the resolved tree, not just your top-level declarations — so a flaw in a dependency-of-a-dependency-of-a-dependency will still show up.

The baseline command is simply:

npm audit

You get a human-readable report grouped by advisory, with severity, the vulnerable path, and whether a fix is available. For automation, request structured output:

npm audit --json > audit-report.json

To gate a pipeline, set a severity floor so low-severity noise does not fail the build while high and critical issues do:

npm audit --audit-level=high

This exits non-zero only when a vulnerability at or above high is present, which makes it a clean CI check. If you only care about what ships to production, drop dev dependencies from the analysis:

npm audit --omit=dev

Fixing what it finds

npm audit fix will try to upgrade vulnerable packages to a patched version that still satisfies your declared semver ranges:

npm audit fix

Because it respects your ranges, it is safe to run routinely — it will not silently jump a major version. When the only patched version is behind a breaking major bump, npm refuses to apply it automatically and tells you so. You can force it, but you are opting into potential breakage:

npm audit fix --force

Before running --force in anger, preview the plan without touching anything:

npm audit fix --dry-run

For a transitive dependency that has no upstream fix but a safe version exists, you can pin it directly using the overrides field in package.json, which forces npm to resolve that package to a version you choose regardless of what intermediate packages request.

Where npm audit stops being enough

npm audit is a solid first line, but it has structural limits that matter once you are past a hobby project.

It reports presence, not reachability. npm audit tells you a vulnerable version is installed. It says nothing about whether your code ever calls the vulnerable function. Studies of real codebases consistently find that the large majority of flagged dependency vulnerabilities are never actually reachable from application entry points. Every one of those still shows up as a red line in your report, and your engineers still spend time triaging them.

It is noisy in dev dependencies. Build tooling drags in enormous transitive trees. A vulnerability in a package that only runs on a developer's laptop during a build is not the same risk as one in your request-handling path, but npm audit presents them with equal weight unless you manually filter with --omit=dev.

It only knows what the advisory database knows. Zero-days, malicious install scripts, typosquats, and freshly published compromised versions are invisible until an advisory is filed. npm audit is a lagging indicator by design.

It has no memory and no policy. Every run is a fresh snapshot. There is no built-in way to accept a risk with an expiry, track an SLA, suppress a confirmed false positive across the team, or prove to an auditor what you decided and when.

Going further: continuous, reachability-aware auditing

The fix is not to abandon npm audit — keep it as a fast local check — but to layer a continuous system on top that answers the questions npm audit cannot. Safeguard's software composition analysis engine scans the same lockfile but adds call-graph reachability, so a critical CVE in a code path you never invoke is ranked below a medium one that sits directly in your request handler. That reordering is usually the difference between a triage queue of hundreds and a list of a handful that genuinely matter.

You can run the same analysis locally and in CI with the Safeguard command-line scanner, which keeps the sub-second feedback loop developers expect from npm audit while adding policy gates, risk-acceptance records with expiry dates, and an audit trail. When a fix does exist, the autonomous auto-fix workflow opens a tested pull request that bumps the offending package — including transitive pins via overrides — so remediation is a review-and-merge rather than a manual investigation.

If you are weighing this against other commercial scanners, the Safeguard vs Snyk comparison breaks down how reachability and autonomous remediation change the day-to-day workflow.

Quick reference

TaskCommand
Basic auditnpm audit
Machine-readable outputnpm audit --json
Fail CI only on high+npm audit --audit-level=high
Ignore dev dependenciesnpm audit --omit=dev
Auto-fix within semvernpm audit fix
Preview a fixnpm audit fix --dry-run
Force through major bumpsnpm audit fix --force

A workable audit routine:

  • Run npm audit --audit-level=high --omit=dev as a required CI check.
  • Commit your package-lock.json so audits are reproducible.
  • Use overrides to pin transitive packages that lack an upstream fix.
  • Layer continuous, reachability-ranked scanning so you triage what is exploitable, not just what is present.
  • Keep a documented record of every accepted risk with an expiry date.

npm audit answers "what known-vulnerable packages are in my tree?" — a genuinely useful question. It does not answer "which of these can actually hurt me, and how do I fix them without breaking the build?" That is where a continuous platform earns its place.

Start auditing your Node.js dependencies with reachability context today — create a free account or read the documentation to wire it into your pipeline.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.