When the LiteLLM supply chain attack hit in March 2026, npm audit ran clean. There was nothing to flag. The CVE got filed afterward, after the code shipped, after the bill landed.
That's the structural problem with vulnerability scanning. It tells you what's already exploded.
What the LiteLLM, axios, and ua-parser-js incidents had in common wasn't a known CVE. It was the publisher profile: one account, millions of weekly downloads, often stale for over a year. That population is identifiable before anything happens.
The follow-up I keep getting asked: how do you actually measure it?
The metric
Two values, summed across the transitive dependency tree:
-
critical_concentration= sum of weekly downloads for every transitive dependency where one npm account holds publish access and weekly downloads exceed 10M. -
critical_paths= the dependency chains from your direct deps to each critical package.
The first maps directly to a credential-compromise attack. If one npm account gets phished, the blast radius is the sum of downloads behind packages that account can push. The second tells you which of your direct deps own that risk. You probably can't drop express. You can swap axios for fetch if its footprint is too concentrated for you.
There's nothing speculative here. npm publish access is queryable. Download stats are queryable. Release cadence is queryable. The only work is summing it across the tree.
The scan
curl -X POST https://poc-backend.amdal-dev.workers.dev/api/graph/npm \
-H "Content-Type: application/json" \
-d '{"package": "express", "depth": 2}'
Flag rules: CRITICAL when one publisher controls publish access AND weekly downloads exceed 10M. WARN when no release has shipped in over 12 months. HEALTHY otherwise. Same data is at getcommit.dev/npm/{pkg} for the human view.
Five packages, depth 2
I ran the scan against five widely-used npm packages. None of them look bad at depth 1. All of them carry critical concentration at depth 2.
express (102M weekly)
| Critical dep | Publisher | Weekly | Last release |
|---|---|---|---|
depd |
dougwilson |
114M | 2018 |
escape-html |
dougwilson |
76M | 2015 |
once |
isaacs |
111M | 2016 |
wrappy (via once) |
isaacs |
116M | 2016 |
Critical concentration: 417M downloads/week. The interesting part isn't the count, it's the publisher overlap. Four packages, two npm identities. Compromising isaacs (npm co-founder Isaac Schlueter) hits both once and wrappy. Compromising dougwilson hits both depd and escape-html. Two phished accounts, four critical paths.
axios (109M weekly)
| Critical dep | Publisher | Weekly |
|---|---|---|
https-proxy-agent |
tootallnate |
177M |
agent-base (via https-proxy-agent) |
tootallnate |
187M |
proxy-from-env |
rob-w |
105M |
Critical concentration: 469M downloads/week. Two of three under the same npm identity. One caveat: https-proxy-agent now publishes via npm Trusted Publishing (OIDC, not a personal token), which materially reduces the credential-phishing risk. The metric is structural. Mitigations exist when teams adopt them.
vite
Eight critical transitive deps. Critical concentration: 1.16B downloads/week. postcss and nanoid ship from the same npm account (ai, Andrey Sitnik), so another 399M sits behind one identity. The rest: lightningcss, tinyglobby, picocolors, source-map-js, fdir, detect-libc. All sole-publisher.
next
Seven critical transitive deps. Critical concentration: 1.05B downloads/week. Shares postcss, nanoid, picocolors, and source-map-js with vite. Same publishers, same blast radius. Adds @swc/helpers, caniuse-lite, and baseline-browser-mapping.
webpack
Seventeen critical transitive deps. Critical concentration: 1.49B downloads/week. Includes the entire @webassemblyjs/* family (one publisher, all stale), the @types/* packages that ship in every TypeScript build, plus graceful-fs, neo-async, browserslist, and friends.
What 2 billion downloads of surface area looks like
A normal modern stack pulling webpack, vite, and next carries roughly 2B weekly downloads of transitive surface behind single-person tokens, after deduping shared deps. Most of those packages will never get a CVE filed against them. They aren't broken. They're load-bearing infrastructure that's one credential reset away from being weaponizable.
That's the gap npm audit doesn't cover. CVEs are reactive by construction. Concentration is structural and visible right now.
Why JavaScript carries this
Architectural problem, not a culture problem.
npm separates publish access from source-code access. A package's GitHub repo can have 30 contributors and still ship from one npm account, because publish credentials are personal tokens tied to one user. Compromising that user is enough. No PR review, no merge, no second pair of eyes on what got published. The dependency tree is built from hundreds of microscopic utility packages, mostly one-maintainer, because that's the pattern the ecosystem rewards.
Go doesn't have this. Modules ship straight from VCS, anchored by go.sum checksums. Compromising one developer's account compromises a GitHub repo, which usually carries multiple maintainers and PR review. There is no separate publish credential to phish. That's why our scan of the top 20 Go modules came back with zero CRITICAL flags. The architecture forecloses on the attack class.
PyPI and Cargo share npm's pattern. Sole-owner accounts are the rule. The download volumes are smaller, but the structural exposure is the same.
Run this on your own tree
npx proof-of-commitment --file package-lock.json
Output lists every CRITICAL transitive package with its publisher, weekly downloads, and the import path from your direct dependencies. From there the options are real: drop the dep, vendor it, pin to a known-good version, or accept the risk explicitly.
The point isn't to scare you off express. It's to make the concentration visible so the decision is informed instead of inherited.
Originally published at getcommit.dev/blog/transitive-risk-methodology. getcommit.dev is behavioral supply chain scoring for npm, PyPI, Cargo, and Go. GitHub.
Top comments (0)