Somebody on your team wanted fancy buttons. Nice ones, with the ripple effect, and a bit of reactivity so the form does not feel like 2004.
That is a perfectly reasonable thing to want, and nobody should sneer at it. I did exactly that this evening, on a clean machine, and counted what turned up.
The Count
Five packages requested: React, React DOM, a component library, and the two styling packages it insists on. Eighty-four installed. Forty-five megabytes, 5,925 files.
Then the same thing again, with the modest addition that somebody would like to build it: a bundler, TypeScript, a linter. Nine requested. Two hundred and eleven installed. One hundred and eighteen megabytes, 8,036 files, a ratio of one to twenty-three. Both counts come straight out of package-lock.json, which records the exact resolved version of everything that landed.
Not one of those two hundred and eleven was chosen. Two hundred and two of them arrived because something else wanted them, and that something else was itself chosen by something else. There is no meeting at which anyone said yes to them, no review, no name against the decision. They simply came with the buttons.
And the ecosystem they came from has an odd shape. An academic analysis of npm published last year found that 17.92 per cent of packages are trivial, meaning modules with minimal or no executable logic, and that their vulnerability levels are comparable to those of the substantial ones. Roughly one package in five does almost nothing and carries the same risk as one that does everything.
What the Scanner Says
So I ran the thing everybody runs. npm audit reported zero vulnerabilities.
That is a true statement and it is worth understanding exactly what it means. It means: among the versions currently installed, none matches an entry in a database of publicly known and published advisories, as of tonight. npm audit is a lookup. A good one, quick and free and worth having.
What it does not mean is that anybody has looked at the code. It does not mean the combination has been tested, because nobody tests combinations. The maintainer of package A has never seen package B and has no idea their work will be shipped in the same bundle, executed in the same process, with the same permissions. Each piece is examined alone. The mixture is what gets deployed.
Nor does it mean the answer holds tomorrow. Which brings us to the interesting part.
Five Times the Lookup Was Right and Useless
A handover, 2018. Dominic Tarr had maintained event-stream since 2012 and had long since stopped needing it. In September a volunteer offered to take it over, and he agreed, which is what a tired maintainer does after six unpaid years. The new owner added a dependency one level down, flatmap-stream, carrying obfuscated code that appeared only in the minified build. It did nothing at all unless it found a particular Bitcoin wallet library in the same tree, at which point it went for the keys.
The payload was not in the package anyone installed. It was underneath, and it only woke up in company.
A two-year courtship, 2024. The persona behind the xz backdoor began contributing in October 2021 and had its first commit merged in January 2022. Then came months of genuine, useful work: reviews, translations, build maintenance. Sockpuppet accounts pressed the exhausted original maintainer to appoint a co-maintainer. The backdoor went in during 2023, shipped in February 2024, and was found in March by an engineer who noticed half a second of latency where there should not have been one. Severity ten out of ten.
At every moment before discovery, a scanner would have reported a clean, well-maintained, actively developed dependency, because that is precisely what it was.
Two hours, September 2025. Eighteen packages were taken over, among them chalk and debug, together over 2.6 billion downloads a week. The route in was a phishing message imitating a two-factor reset, from a domain that looked close enough at a glance. The payload rewrote cryptocurrency transactions in the browser. The malicious versions were live for about two hours before the community spotted them and they were pulled.
A scanner running nightly would never have seen them at all. The report the next morning would have been green, and it would have been a lie.
A worm, September 2025. Shai-Hulud is, as far as anyone can tell, the first self-replicating supply chain attack. It spread through more than five hundred packages, and the mechanism deserves a moment: it propagates through the build pipeline. Landing in a repository, it looks for a registry token in the environment and publishes poisoned versions of whatever else that credential can reach. It scans for secrets with an off-the-shelf tool and makes private repositories public.
The automation that was supposed to be the control turned out to be the transport. CISA issued its own advisory.
A forged certificate of authenticity, spring 2026. The successor, catalogued as CVE-2026-45321, arrived in two waves, on 29 April and 11 May, taking 172 packages across 404 malicious versions on npm and PyPI together, among them the ecosystems of Mistral AI, TanStack and UiPath, with something over half a billion cumulative downloads behind them.
Two details deserve their own sentence. It abused a pull-request trigger to lift OIDC tokens out of the memory of the build runner and minted valid publishing credentials with them. And with those credentials it produced packages carrying valid SLSA Build Level 3 provenance attestations, which is the current gold standard for proving where a build came from.
The mechanism designed to certify origin certified the attack.
It also settles a fair objection, because it hit both registries: this is not an npm defect. Anything shipped as source and assembled at install time has the same shape.
Why It Holds Anyway
Every one of those was reported, discussed, post-mortemed. Nothing changed. That is the part worth explaining, and the explanation is not technical.
The green tick survives because two quite different groups need it.
The first has never been told what a scanner does. They see a check pass in the pipeline and read it as this was examined, which is a reasonable inference from the word “audit” and from the tick being green. Nobody sat them down and explained that it compares version strings against a list. They are not careless. They were handed a control and told it was a control.
The second group knows exactly what it is and takes it anyway, because it goes green in eighty seconds and the feature ships today. This is where the honest name for it is a discount rate. Whoever selected the dependencies is frequently not whoever will patch them in three years, and eighteen months is a long time in this industry. The cost is real, it is simply payable by somebody else, which is the oldest trick in capital allocation and works just as well on a sprint board.
And underneath both sits the quiet thing nobody puts in the retrospective. The people best placed to raise the objection are usually the people least able to afford it. If you are new, or on a contract, or in a team where questions are heard as delay, you take the library from the tutorial and you keep your reservations to yourself. That is not a character flaw. It is what happens when the person who can see the problem is also the person whose position depends on not being difficult.
So the argument here is not with them. It is with whoever ordered the pace and never asked what came with it.
The Limit
Several things push back, and they are stronger than this piece finds convenient.
Nobody should write their own date picker. The ones people write are worse, they fail on time zones and screen readers, and the ecosystem has saved a genuinely enormous amount of duplicated effort. That is not a concession, it is the reason the model won.
The maintainers are not the failure here. Tarr had given six years of unpaid work to something he no longer used. The xz maintainer was worn down and then deliberately worked on. The chalk maintainer was phished with a message that would have caught most of us before coffee. These are not lapses of character; they are the predictable stress points of a model that rests millions of installations on individuals who are paid nothing and thanked rarely.
The tools find real problems. When a published advisory affects a version you actually run, the scanner tells you, and that is worth having in the pipeline. The reachability analysis is sound as well, even if there is a certain wry symmetry in the fact that the firms now selling you a filter for the noise are largely the firms who sold you the noise.
And my count is a two-minute experiment, not a production system. Two hundred and eleven packages on a bare install proves the shape of the problem, not its severity in your codebase. Zero findings tonight is an argument against panic. It is not an argument against knowing what you run.
The scanner reports zero vulnerabilities, which is the least useful true sentence you own.
The Point
The scanner answers the question it can answer: is anything here on the list. That is a fine question. It is not the one that decides whether you are exposed, and the last few years have made the difference embarrassingly clear. A handover, a patient stranger, a phishing mail, a worm riding your own automation, and finally a worm that got your build system to vouch for it. None of those is a version string, and none of them was on any list at the moment it mattered.
The uncomfortable part is not that the check is imperfect. It is that a failing check makes people cautious and a passing one makes them certain, and certainty is the more expensive of the two.
So the modest proposal is not a better tool. It is a sentence in the definition of done that says who chose this, and a willingness to hear the answer “nobody” as a finding rather than as an inconvenience.
Two hundred and eleven packages, because somebody wanted buttons. That was never a security decision, which is exactly why it became one.