Adam Innes · Blog

npm audit in npm 6: Read the Report Before You Reach for audit fix

· 6 min · security, npm, node.js, dependencies

npm 6 shipped on April 24 with security as its headline feature, and it has been filling in quickly since. npm 6.1.0 was published last Thursday with a new npm audit fix subcommand, and yesterday’s Node.js 10.3.0 release is the first Node 10 build that bundles npm 6.1.0 (Node 10.0 through 10.2 still came with npm 5.6.0). Plenty of people are about to see a vulnerability count after every install, and I think it’s worth understanding what that count means before running whatever the footer suggests.

Everything below describes npm 6.1.0, based on its docs, changelog, and source at that tag.

Where npm audit came from

The npm@6 announcement ties the feature to npm’s acquisition of the Node Security Platform earlier in April, which npm calls the definitive source of known JavaScript package vulnerabilities. At launch the registry side wasn’t quite ready, and the post said audit would roll out to everyone in the coming weeks. By May 8, npm’s follow-up post on npm audit said you could start using it right away in npm 5.10.0 or npm 6, and described it as a moment-in-time security review of your dependency tree. That phrase matters. A clean report means nothing in your tree matched a known advisory today, not that your dependencies are safe.

What gets sent, and why it needs your lockfile

The npm audit docs explain that the command submits a description of your project’s dependencies to your default registry and asks for a report. The payload is your npm version, Node version, platform, NODE_ENV, and a scrubbed copy of your package-lock.json or npm-shrinkwrap.json. Scrubbing replaces some names and specifiers with a SHA-256 of a per-session UUID plus the original value, which covers packages in scopes you’ve pointed at a different registry, git dependencies, remote tarballs, and local directories and tarballs.

That lockfile is not optional. The source for npm audit at 6.1.0 refuses to run without package.json, fails with EAUDITNOLOCK if there’s neither a shrinkwrap nor a lockfile, and prefers npm-shrinkwrap.json if both exist. Before it submits anything, it checks that the lockfile agrees with package.json and tells you to run npm install if they’ve drifted. The package-lock.json docs describe that file as the exact tree that was generated, meant to be committed so teammates, deployments, and CI install the same dependencies. So audit reports on what your lockfile says, not on whatever is sitting in node_modules, which is one more reason to commit it. If you need one without touching node_modules, npm’s guide suggests npm i --package-lock-only.

It already runs on every install

You don’t have to type npm audit to use it. The audit config setting defaults to true, and when it’s on, npm install submits an audit report to the default registry and to any registries configured for scopes. Global installs are skipped. The installer source waits up to 10 seconds for that response and carries on without a report if it doesn’t arrive. When it does come back, the install summary tells you how many packages were audited and how many vulnerabilities were found, with a hint to run npm audit fix or npm audit.

If you’d rather not send that data, you can pass --no-audit on a single install or run npm set audit false to turn it off for everything.

Reading the report

A full npm audit prints a detail report. Findings are grouped under the command npm recommends: either an npm install of a specific version, or npm update <package> --depth <n> to pull a fixed version of something deeper in the tree. Severity comes in four levels, low, moderate, high, and critical. For each finding the report shows the advisory title, the vulnerable package, which of your own dependencies it belongs to (marked [dev] if it only comes in through dev dependencies), the full path through the tree, and a “More info” link to the advisory on nodesecurity.io.

The path is the part I’d pay the most attention to. It reads like your-dep > something > vulnerable-package, and the report lists each vulnerable path separately. One advisory in a popular utility can show up five times because five of your dependencies pull it in, so a scary total may really be one problem.

When a recommended install would cross a major version, the report adds a SEMVER WARNING saying the action is potentially breaking. When npm can’t recommend a command, the finding lands in a Manual Review section that includes a “Patched in” field, which says “No patch available” when none exists. The npm guide to running a security audit walks through what to do there: check the advisory for mitigating factors, send a pull request or open an issue on the dependent package if a fix exists upstream but hasn’t been picked up, or help fix the vulnerable package itself.

What npm audit fix does

npm audit fix arrived in the v6.1.0 release notes, so it isn’t in 6.0.x. It takes the actionable parts of the report and runs the installs for you, sticking to semver-compatible changes by default. At the end it prints how many vulnerabilities it fixed out of the total, how many still need manual review, and how many need breaking changes.

Because it runs a full install under the hood, the docs note that installer settings apply to it too. A few worth knowing:

npm audit fix --dry-run --json      # see what it would change
npm audit fix --package-lock-only   # update the lockfile, leave node_modules alone
npm audit fix --only=prod           # skip devDependencies

Be careful with –force

The docs describe npm audit fix --force as letting audit fix install semver-major updates to top-level dependencies, not just semver-compatible ones. A major bump can change an API your code calls, and audit fix has no idea whether your tests still pass.

There’s a subtler catch, too. In the 6.1.0 source, audit fix decides whether to include major updates by reading npm’s ordinary force setting, and since installer settings apply to the install it runs, that flag carries its usual meaning there as well. The config docs say force also lets lifecycle script failures not block progress, skips the cache when requesting from the registry, and turns off checks against clobbering non-npm files. I would not want any of that happening quietly in an install whose whole point was security.

My advice is to let plain npm audit fix handle the semver-compatible work, commit that, then do each breaking upgrade by hand in its own change: read the package’s changelog, bump it in package.json, and run your tests.

Triage that doesn’t eat your week

Start with where the vulnerable code runs. A [dev] finding in a test runner’s dependency tree never ships to production, so it usually ranks below something in your server’s request path. It isn’t zero risk, though, since dev tools run on your laptop and your CI machines. If you want to fix production issues first, npm audit fix --only=prod keeps dev dependencies out of it.

Next, read the path. The first hop after your project is the one you control. If a fix exists but the package in the middle hasn’t adopted it, that maintainer is who can actually resolve it, and a short issue or pull request with the advisory link does more good than waiting. Meanwhile, read the advisory itself. Plenty of vulnerabilities only matter if you call a certain function or run on a certain platform, and you may find the vulnerable code never touches untrusted input in your app.

Putting it in CI

npm’s guide recommends running npm audit regularly or adding it to your continuous integration process, since the advisory database changes even when your code doesn’t. Know how it behaves first. In 6.1.0, npm audit sets a nonzero exit code whenever it finds a vulnerability of any severity, including low. There’s no documented option for setting a severity threshold, so a bare npm audit step fails the build on any finding.

If that’s too strict for your team, npm audit --json prints the full report as JSON, including a metadata.vulnerabilities object with counts per severity, which is what the CLI’s own output reads. A short script can parse that and fail only on high or critical. Keep in mind that the command still exits nonzero when it finds anything, so your script has to handle the exit code itself. Also plan for registry support: if your configured registry doesn’t answer audit requests, the command errors out with ENOAUDIT rather than passing.

The takeaway

npm audit is a genuinely useful default, and npm made it free for every user. Just treat its output as a list of questions rather than a to-do list. Commit your lockfile so the report reflects what you really ship, run plain audit fix for the safe updates, read the paths and advisories for the rest, and keep --force away from anything you haven’t tested.

← all posts