Skip to main content

There is a command that makes a Node project feel audited. You run npm audit signatures, and it answers with a clean, confident line: every package has a verified registry signature. No warnings. Exit code zero. The supply-chain box is ticked and the sprint carries on.

On 6 October 2026 we installed twelve ordinary dependency trees and ran that command against all of them. All 880 installed packages had a verified registry signature. 293 had a verified build attestation. The command reported both numbers and exited zero either way.

TL;DR

  • We resolved production dependency trees for twelve common Node stacks with npm 10.9.4 on Node 22.22.0, giving 771 unique name@version pairs. 299 of them (38.8%) carry an npm provenance attestation. 472 do not.
  • npm audit signatures across the installed trees reported 880 of 880 packages with verified registry signatures and 293 with a verified attestation. The 587-package gap is the whole point: a registry signature says npm served the tarball it has on file, not that anyone knows where the tarball came from.
  • An Express API with cors, helmet and dotenv resolves to 72 packages. Exactly one has provenance. A NestJS API resolves to 98 packages with three. The command exits zero on both.
  • 18 of the 35 packages we deliberately chose and typed into package.json publish without provenance, including express, typescript, eslint, prettier, fastify, jsonwebtoken and @nestjs/core.
  • The 299 attested packages come from only 129 source repositories, and the top three account for 76 of them. The headline percentage is inflated by projects that publish dozens of per-architecture binaries from one pipeline.
  • npm supports two CI providers for provenance. All 298 attestations carrying a SLSA predicate came from GitHub-hosted runners. We found none from GitLab, and self-hosted runners cannot produce provenance at all.

What we measured, and how

We defined twelve dependency sets that look like real projects rather than benchmarks: front ends, APIs, a data layer, an auth stack, toolchains and an AI SDK stack. The full list is in the table below.

Each one was resolved with npm install --package-lock-only --omit=dev --ignore-scripts, so the versions are whatever npm’s own resolver picked on the day, not versions we chose. That produced 771 unique package versions across the twelve lockfiles. For each one we fetched https://registry.npmjs.org/<name>/<version> and checked for a dist.attestations object. Then we installed each tree properly and ran npm audit signatures so that npm, not us, produced the headline counts.

The two totals differ because the lockfile union counts unique versions while the audit counts installs, and the same version appears in more than one tree. The ratio is the same either way: roughly a third.

The split, stack by stack

Stack Packages audited Verified signature Verified attestation
Next.js app 26 26 21
AI SDKs (OpenAI, Anthropic) 19 19 14
Tailwind toolchain 15 15 10
Vite and React 20 20 13
Vitest and Testing Library 55 55 27
Auth (next-auth, JWT, bcrypt) 56 56 27
Prisma 360 360 153
TypeScript, ESLint, Prettier 80 80 16
Fastify API 50 50 6
axios, zod, date-fns 29 29 2
NestJS API 98 98 3
Express API 72 72 1
Total 880 880 293

The spread is the finding. A greenfield Next.js front end is at 81% attested, because the modern JavaScript build tooling it depends on was mostly set up during the years provenance existed. A conventional Express or NestJS API, the shape of most of the back ends we are asked to review, sits at 1% and 3%. Both pass npm audit signatures identically.

A signature and an attestation are not the same promise

The registry signature is npm attesting that the tarball you downloaded matches the one in its database. It defends against a tampering mirror, a malicious proxy, a corrupted CDN edge. It says nothing whatsoever about who built the tarball or from what source.

A provenance attestation is a different claim, cryptographically tied to a build. Pulling the bundle for next@16.3.8 and decoding the SLSA predicate gives you three facts the signature cannot: the source repository is github.com/vercel/next.js, the workflow that built it was .github/workflows/build_and_deploy.yml, and the git ref was refs/tags/v16.3.8. You can go and read that workflow. You can check that tag.

That difference has a name in the incident record. In September 2025 the debug package shipped a malicious 4.4.2 after an npm account takeover, recorded as GHSA-4x49-vf9v-38px. A stolen publish token is sufficient to push a package. It is not sufficient to mint an attestation, because the attestation is signed by the CI provider’s identity on a cloud-hosted runner, not by the token. debug is now at 4.4.3, it appears in five of our twelve trees, and it still publishes with no provenance.

The packages you chose yourself

It is tempting to treat this as a problem of obscure transitive dependencies. It is not. Of the 35 packages we deliberately named in package.json, 18 publish without provenance: express, typescript, eslint, prettier, fastify, @fastify/cors, jsonwebtoken, bcryptjs, date-fns, next-auth, rxjs, reflect-metadata, cors, helmet, dotenv, and the three @nestjs packages.

The most widespread unattested packages across all twelve trees are the small ones nobody has an opinion about: ms, picocolors, source-map-js, debug, tslib, cookie. These are exactly the packages that make good attack targets, because they are everywhere and nobody reads their release notes.

38.8% is softer than it looks

Counting packages flatters the ecosystem. The 299 attested packages in our sample were published from only 129 distinct source repositories. The top repository, rolldown/rolldown, accounts for 32 of them. evanw/esbuild accounts for 27 and lovell/sharp for 17. Those three repositories alone produce a quarter of every attested package we found, because each publishes a separate package per platform and architecture from one pipeline.

Ninety-eight of the 129 repositories published exactly one attested package. So the honest version of the headline is not that 39% of the ecosystem has adopted provenance. It is that a modest number of projects have, and a handful of them happen to ship a lot of packages.

GitHub Actions, or nothing

npm’s documentation lists two supported providers for provenance, GitHub Actions and GitLab CI/CD, and is explicit that both require a cloud-hosted runner. 298 of the 299 attestations carry a SLSA predicate, and in every one of them the builder identity was https://github.com/actions/runner/github-hosted. We found no GitLab-generated provenance in the sample at all.

For anyone publishing packages, that is the practical constraint. If your release pipeline runs on Buildkite, CircleCI, Jenkins, or a self-hosted GitHub runner because your build needs a machine you control, you cannot produce npm provenance today. That is a real architectural trade-off and it deserves to be made deliberately rather than discovered during a client security review.

One reassuring result: of the 299 attested packages, not one named a source repository that differed from the repository declared in its own package metadata. Where provenance exists, it is consistent.

npm gives you nothing to gate on

Here is the part that turns an observation into an action item. We ran npm audit signatures on the Express tree, where 71 of 72 packages have no attestation, and checked the exit code. It is zero. It is zero on every tree we tested. No CI pipeline anywhere has ever failed a build because of this number.

The machine-readable output is no better. npm audit signatures --json on a clean tree returns {"invalid":[],"missing":[]}, where missing refers to missing signatures, not missing attestations. There is no field to assert on. Checking npm config ls -l shows only two provenance-related settings, provenance and provenance-file, and both are publish-side flags you set when releasing your own package. There is no install-side equivalent.

So if you want a provenance threshold in CI, you have to build it. The version we used for this audit is short enough to paste into a pipeline:

npm ls --all --omit=dev --json | python3 -c '
import json,sys
def walk(n,out):
    for k,v in (n.get("dependencies") or {}).items():
        if v.get("version"): out.add((k,v["version"]))
        walk(v,out)
out=set(); walk(json.load(sys.stdin),out)
print("\n".join(f"{n}@{v}" for n,v in sorted(out)))' > pkgs.txt

while read -r spec; do
  name="${spec%@*}"; ver="${spec##*@}"
  curl -fsS "https://registry.npmjs.org/${name}/${ver}" | grep -q '"attestations"' \
    && echo "ok   $spec" || echo "none $spec"
done < pkgs.txt

Wrap that in a percentage check and fail below a threshold you choose. Pointed at our Next.js tree it reports 22 of 27 and passes. Pointed at the axios and zod tree it reports 2 of 29 and fails. That is a gate, and it took an afternoon.

What we would actually tell a client

Do not set the threshold at 100%. You will fail every build on day one and the gate will be disabled by Friday. Measure what you have, set the bar just under it, and ratchet.

Treat the number as a direction of travel rather than a verdict. A tree at 3% is not compromised, it is unverifiable, and those are different findings with different responses. The useful question for a dependency review is not “is this package safe” but “if this package were hijacked tomorrow, would anything in my pipeline notice”. For 587 of the 880 packages we audited, the answer is no.

If you publish packages yourself, this is the cheap half. Adding --provenance to a publish step on a GitHub-hosted runner takes about ten minutes and moves your consumers from trusting your npm token to trusting your repository. Of everything in this post, it is the only change that improves someone else’s security posture as well as your own.

And keep the audit dated. Our figures describe the registry on 6 October 2026. The whole value of a measurement like this is that re-running it in six months tells you whether the ecosystem moved, and nobody will re-run a script they cannot find.

REPTILEHAUS builds and audits this layer for clients: dependency and supply-chain review, CI/CD pipeline hardening, release provenance for published packages, and the monitoring that tells you when a thresholded gate has quietly been turned off. If you have run npm audit signatures, seen it pass, and are not sure what it actually proved, that is a short engagement with a definite answer. Get in touch.

Method note: all figures are from first-hand measurement on 6 October 2026, using npm 10.9.4 on Node 22.22.0 against the public npm registry, with provenance support quoted from npm’s current documentation and the debug advisory from OSV.

📷 Photo by Ali Mkumbwa on Unsplash