Back to Blog

What 10.7 million packages taught us about the noise floor

We run continuous analysis on new package publishes across npm, PyPI, crates.io, and Go — every new release gets pulled through a set of heuristics looking for the fingerprints of supply-chain attacks: install-time scripts, obfuscated code, typosquatting, dependency confusion, suspicious version-to-version diffs, and coordinated publishing bursts. Over about seven months that added up to 10.7 million analyzed packages.

Here's the first thing the data says, and it's worth stating plainly before anything else: the overwhelming majority of packages don't trip anything. Of 10.7 million analyzed, 80.4% raised zero signals. Roughly 14% raised one, 4.8% raised two, and fewer than 0.4% raised three or more.


1.00


So about one in five new packages trips at least one automated risk heuristic. And this is the single most important thing to understand about supply-chain scanning, so I'll be blunt about it: a signal is not a verdict. "Raised a heuristic" is not "is malicious." The vast majority of those one-in-five are benign — legitimate packages doing legitimate things that happen to resemble, at the level a fast heuristic can see, the things attackers also do. The entire difficulty of this problem lives in that gap. Anyone can build a scanner that flags 20% of packages. The hard part is everything that comes after.

The flag rate isn't the same everywhere

Break the same numbers down by ecosystem and they spread out by almost 3x:

Registry Packages analyzed Raised ≥1 signal
npm 3.9M 29.6%
crates.io 576K 27.0%
PyPI 654K 22.7%
Go 5.6M 11.6%

npm's new packages trip a signal nearly three times as often as Go's.

One caveat before anyone over-reads the counts: those "analyzed" numbers reflect what our pipeline covered in the window, not the true relative sizes of these ecosystems — our discovery leans heavier on some registries than others, which is why PyPI's analyzed count looks small next to Go's. The rates are rates, so they're comparable; the raw counts are not a census of ecosystem size.

But the rate difference is real, and the interesting question is why. It would be easy — and wrong — to conclude that npm is three times as dangerous as Go. The actual explanation is more useful.

npm's noise has a specific shape

When you break each ecosystem's signals down by type, npm doesn't look like the others:

1.00


  • npm: install-hook signals (\~710K) and obfuscation signals (\~697K) make up the overwhelming majority. Everything else is a rounding error next to those two.
  • Go: led by suspicious diffs and obfuscation; install hooks are minor.
  • PyPI and crates.io: led by suspicious diffs, with a much flatter spread across types.

npm's elevated flag rate is almost entirely an install-hooks-and-obfuscation story. And once you know that, the "npm is three times riskier" reading falls apart, because both of those signal types fire constantly on completely normal npm packages:

Install hooks are idiomatic in npm. Lifecycle scripts (postinstall and friends) are a standard, blessed part of how huge numbers of legitimate packages work — native modules that compile on install, tools that set themselves up. The same mechanism is also exactly how an attacker gets code to run the moment you npm install, which is why we flag it. The signal is doing its job; it just fires on a haystack that's mostly hay.

Obfuscation signals have the same problem in the other direction. npm's ecosystem norm is to ship minified, bundled distribution code. Minified JavaScript is, to an entropy-based heuristic, nearly indistinguishable from deliberately obfuscated malicious code — high entropy, long unreadable lines, no whitespace. A detector that flags "this looks obfuscated" will light up on a large fraction of perfectly ordinary npm packages, because a large fraction of ordinary npm packages ship code that looks exactly like that.

So npm doesn't necessarily have three times the malice of Go. It has ecosystem conventions — lifecycle scripts and shipped-minified code — that produce far more heuristic noise. Go, whose culture leans toward vendored, readable source and no install-time execution, gives a heuristic much less to trip over. The 3x is at least as much about idiom as about threat.

Which is the whole point

This is why the raw signal count is not a product, and why "we flag 20% of packages" is a starting line, not a finish. If a firewall quarantined every package that tripped a heuristic, it would block one in five installs — most of them legitimate — and get uninstalled by lunch. The signal is where the work begins.

What actually matters is the funnel after the signal. Of the 10.7 million packages we analyzed, about 2.7 million signals were raised. A small, prioritized fraction of those get escalated to deeper LLM-assisted review, and a smaller fraction still are confirmed as genuine threats and turned into advisories. Each stage is a filter whose job is to spend expensive scrutiny only where cheap scrutiny found something worth a closer look. The reason that funnel exists — the reason it has to exist — is everything above: most packages are clean, most signals are noise, and the ecosystems differ in ways that have more to do with their conventions than their threat level.

A package firewall isn't valuable because it can raise a signal. Raising signals is easy; we raised 2.7 million of them. It's valuable because of what it does with the 80% that are clean, the ecosystem-specific noise that makes npm look scarier than it is, and the rare real threat sitting in the middle of all of it. Telling the hay from the needle is the entire job — and the data is a good reminder of just how much hay there is.

Comments

Loading comments…

Protect Your Supply Chain

Add a security firewall to your package manager and CI/CD pipelines.

Get Started Free