npm-trust-downgrade: checking npm lockfiles for provenance downgrades

A small tool that reads a package-lock.json, transitive dependencies included, and reports versions published with weaker trust evidence than earlier releases of the same package.

The problem

An attacker who holds an npm token or a maintainer's machine, but not the project's CI pipeline, publishes by hand. The malicious version then has no provenance, while earlier versions do. This is what happened with axios 1.14.1 and 0.30.4 in March 2026.

pnpm can reject such a version at install time with trustPolicy: no-downgrade. npm cannot:

  • npm/cli#9242 asks for the same setting. It has 68 reactions and has been open since April 2026.
  • PR #9902 implements it, has had no activity since September, and its approach is disputed.
  • npq detects a provenance regression, but according to its README it covers only the direct dependencies in package.json and does not read lockfiles.

How the tool works

npm-trust-downgrade reads package-lock.json, fetches the registry metadata of each package and compares each locked version with the history of that package. There are three evidence levels: trusted publisher, provenance attestation and none. It has no runtime dependencies, which matters for a tool in this position.

A version is reported when a release that is both published earlier and lower in semver order has stronger evidence. pnpm compares by date alone. The semver condition follows an objection from the discussion of PR #9902: a maintenance release of an old line should not be measured against a newer line that adopted provenance later.

pino@9.13.1 has a transitive dependency that shows the output:

$ npx --yes github:Jwrede/npm-trust-downgrade#v0.1.0
DOWNGRADE  slow-redact@0.3.2
  has: none; 0.3.1 had: trusted publisher
  0.3.2 published 2025-10-11 by matteo.collina
  0.3.1 published 2025-10-03 by GitHub Actions

13 checked, 1 finding(s), 0 not checked

This is a request for review. The two publisher lines show that a maintainer published by hand, and nothing here says the release is malicious. With --since origin/main the check covers only what a pull request changes, and --exclude records a finding that was reviewed. The tool also reports versions that a lockfile still references but that were removed from the registry.

Testing

The tests run the CLI against a local mock registry with trimmed copies of real metadata. They include the axios history reconstructed as it looked while the malicious version was live. A second suite generates lockfiles with real npm and compares the verdicts with real pnpm. CI runs both on Node 20, 22 and 24 (the live suite on 22).

On a lockfile of 43 widely used packages with 2815 distinct versions, the tool reported 2 versions and the date rule reported 5. The other three, @octokit/endpoint@9.0.6, ua-parser-js@1.0.41 and css-declaration-sorter@6.4.1, are releases on an older line, and pnpm 10.34.9 rejected each of them. The run took 73 seconds. That sample contains no known compromised version, so it measures how often the check asks for a review and says nothing about detection rates.

Limits

  • Provenance is a signal, and there is a serious argument that it should not be used as a security check at all. A maintainer may stop using it for legitimate reasons, and that is reported too.
  • An attacker with access to the CI pipeline publishes with full evidence and is not detected. A package that never had evidence cannot be downgraded.
  • The semver condition has a cost. A new release below the first attested version passes, where pnpm would reject it.
  • Attestations are not verified cryptographically. npm audit signatures does that.
  • It is a separate step, so it does not stop npm install unless the pipeline runs it first. Only npm lockfiles are read, and requests carry no credentials for private registries.

The code is at github.com/Jwrede/npm-trust-downgrade.