How often we flag a package people install on purpose

Catch rates are easy to advertise and hard to check. Noise is the number that decides whether a scanner stays switched on, and it is the one almost nobody publishes about themselves. These are ours, measured at rules version 2026.09.21c and rebuilt whenever that changes.

Read every number with the sentence next to it.

We chose all of these corpora. None of them is a random sample of the registry and none of them is a measurement of what happens in the field, so none of these numbers is a rate you can carry over to your own dependencies. What they do tell you is how this scanner behaves on lists we have published in full, which you can re-run yourself.

Attack samples we wrote

21 of 22

What that counts: attack samples our rules detected, out of the ones in our corpus. We wrote every sample in it, to match what an attack does rather than to match our rules.

What the corpus is made of, counted from the samples themselves rather than described: technique family we wrote: 17, published incident report: 3, evasion variant we wrote: 2. Only the ones written from a published incident report follow a specific named attack. The rest reconstruct a class of attack, so they are our idea of what that class looks like and not evidence that any particular attack was caught.

And they are not all npm packages. What each sample actually is: npm package: 16, Python source file: 3, CI workflow: 2, agent skill: 1. That matters for reading the number, because a corpus holding CI workflows and an agent skill is not measuring npm packages alone.

This is not field recall. It cannot be. Field recall would need the set of all attacks published in a period, including the ones nobody has found yet, and nobody has that list. Anyone quoting a field recall figure for a package scanner, including us, would be making it up.

The corpus includes cases we expected to fail, on purpose. A harness containing only things you catch tells you nothing.

What we do not catch

Not detected: payload hidden inside a minified bundle technique family we wrotenpm package

Written from: attacker ships the stealer inside a published dist bundle

Minified and bundled files are left out of the source-level rules on purpose. A single dense line of machine-written code produces so many spurious matches on ordinary packages that including it would flag half the registry, and a scanner that flags everything is one people turn off. The trade is real and we take it knowingly: a stealer buried in a published dist bundle can get past this, which is why a report that could not read part of an artifact says it was only partly analyzed instead of returning a confident clear.

Noise on packages people install on purpose

Popular npm packagesrules 2026.09.21cmeasured 2026-09-22

8 of 154

What that counts: packages flagged by our rules out of the whole list, with no indicator feed in play.

The corpus, and who chose it: The list we publish package pages for, chosen by us for search traffic, not sampled from the registry at random. It is weighted to packages a working developer installs on purpose, which is the population a noisy scanner annoys.

Full breakdown: 140 came back clear, 8 came back flagged, and 6 could not be read at all in this run and are counted separately rather than folded into either side.

PackageVerdictRiskRule categories
figletflaggedhigh prompt_injection
joseflaggedhigh credential_access
jsdomflaggedhigh credential_access, obfuscation
mongodbflaggedhigh credential_access
pm2flaggedhigh obfuscation, persistence
ts-nodeflaggedhigh prompt_injection
viteflaggedhigh credential_access, obfuscation
xstateflaggedhigh credential_access

Naming them is not the same as conceding them. We are not claiming here that each of these is wrong, only that our rules flagged it and that you can go and read the evidence yourself. Where we work one out to have been wrong, it is corrected and listed in the public corrections log rather than edited out of this table.

Could not be read in this run:

autoprefixerfetch failed: unreachable (npm: packument-fetch)
better-sqlite3fetch failed: too_large (fetch: content-length 11402131)
handlebarsanalysis exceeded the time budget
highlight.jsfetch failed: internal (unexpected: other side closed)
nextfetch failed: too_large (fetch: content-length 41747455)
postcssfetch failed: unreachable (npm: packument-fetch)

Adversarial npm packagesrules 2026.09.21cmeasured 2026-09-22

9 of 53

What that counts: packages whose result disagreed with the label we wrote down before running them.

The corpus, and who chose it: Chosen by us because each one legitimately does something our rules look for: reads cloud credentials, posts to a chat webhook, spawns shells, compiles code at run time, or runs an install step that downloads a binary. Every label was written before the run.

Full breakdown: 46 came back clear, 4 came back flagged, and 3 could not be read at all in this run and are counted separately rather than folded into either side.

The two directions, separately, because collapsing them into one number would hide which way we were wrong: 4 we had labelled clear came back flagged, and 5 we had labelled flagged came back clear. The second group is the more uncomfortable one: those are packages whose install-time behaviour we predicted we would report and did not.

Labelled clear, came back flagged: ethers, node-gyp, pm2, serverless.
Labelled flagged, came back clear: bcrypt, esbuild, puppeteer, sharp, sqlite3.

PackageVerdictRiskRule categoriesOur label
ethersflaggedhigh exfiltration we labelled it clear
node-gypflaggedmedium misdirection, obfuscation we labelled it clear
pm2flaggedhigh obfuscation, persistence we labelled it clear
serverlessflaggedhigh credential_access we labelled it clear

Could not be read in this run:

googleapisfetch failed: too_large (fetch: content-length 17382056)
handlebarsanalysis exceeded the time budget
node-ptyfetch failed: too_large (fetch: content-length 15468777)

Well-known MCP serversrules 2026.09.21cmeasured 2026-09-22

1 of 20

What that counts: server packages flagged by our rules out of the whole list.

The corpus, and who chose it: The MCP servers we publish pages for, chosen by us from the widely used ones.

Full breakdown: 19 came back clear, 1 came back flagged, and 0 could not be read at all in this run and are counted separately rather than folded into either side.

PackageVerdictRiskRule categories
@upstash/context7-mcpflaggedhigh prompt_injection

Naming them is not the same as conceding them. We are not claiming here that each of these is wrong, only that our rules flagged it and that you can go and read the evidence yourself. Where we work one out to have been wrong, it is corrected and listed in the public corrections log rather than edited out of this table.

Every sample, and how it went

SampleWhat it isHow it was writtenWritten from ResultLabel when written
postinstall droppernpm packagepublished incident report ua-parser-js / coa / rc (2021): postinstall fetches and runs a remote binarydetected expected to detect
ssh + cloud credential stealernpm packagetechnique family we wrote typosquat crypto/credential stealers: read key material, POST to hardcoded hostdetected expected to detect
npm token + env harvestnpm packagepublished incident report Shai-Hulud (2025): harvest npm/GitHub tokens and env, exfil to attacker endpointdetected expected to detect
obfuscated decode-then-eval loadernpm packagepublished incident report event-stream / flatmap-stream (2018): decrypt a blob at runtime, then execute itdetected expected to detect
decoded into a variable, then evaluatednpm packageevasion variant we wrote the same loader with the decode and the eval split by a variable, the first thing an author tries to dodge a one-expression patterndetected expected to detect
crypto wallet stealernpm packagetechnique family we wrote wallet-draining typosquats: read wallet files, exfildetected expected to detect
self-updating beaconnpm packagetechnique family we wrote persistent loaders: periodically pull and execute remote codedetected expected to detect
payload hidden inside a minified bundlenpm packagetechnique family we wrote attacker ships the stealer inside a published dist bundlenot detected written as a case we expected to miss
DNS-based exfiltrationnpm packagetechnique family we wrote covert channel: encode secrets into DNS lookups (no HTTP sink)detected written as a case we expected to miss
reverse shell in a postinstallnpm packagetechnique family we wrote bash /dev/tcp reverse shell dropped by an install script (common CTF/malware idiom)detected expected to detect
Discord-webhook token stealernpm packagetechnique family we wrote many 2023-2025 npm token stealers: read npm/env secrets, POST to a Discord webhook (a "normal" domain)detected expected to detect
clipboard crypto clippernpm packagetechnique family we wrote clipper malware: poll the clipboard and swap a copied wallet address for the attacker'sdetected expected to detect
multi-provider env token harvestnpm packagetechnique family we wrote Shai-Hulud-style worm / token stealers: collect NPM/GitHub/AWS secrets from env and POST themdetected expected to detect
remote code loader to a domainPython source filetechnique family we wrote dropper that fetches a stage-2 payload from a DOMAIN (no hardcoded IP) and execs itdetected expected to detect
bin shadows a system commandnpm packagetechnique family we wrote metadata attack: a package whose bin shadows git/npm so a global install hijacks the commanddetected expected to detect
CI workflow secret exfiltrationCI workflowtechnique family we wrote poisoned GitHub Actions workflow that POSTs a build secret to an attacker hostdetected expected to detect
Windows LOLBin dropper via postinstallnpm packagetechnique family we wrote Windows-targeted npm malware: postinstall uses certutil to fetch a remote exedetected expected to detect
in-memory shellcode injectionPython source filetechnique family we wrote loader that allocates executable memory and runs a raw shellcode bufferdetected expected to detect
obfuscated dynamic import (anti-analysis)Python source fileevasion variant we wrote evasion: assemble the module name from fragments so a static scan never sees "os"detected expected to detect
credential exfil to a throwaway sinknpm packagetechnique family we wrote reads env/npm secrets and POSTs them to a webhook.site capture URLdetected expected to detect
CI workflow dropper from a raw IPCI workflowtechnique family we wrote poisoned GitHub Actions step fetches a payload from a bare IP and pipes it to a shelldetected expected to detect
agent skill exfiltrates via a rendered imageagent skilltechnique family we wrote prompt-injection in an agent SKILL.md: a markdown image whose URL templates the conversation to an attacker hostdetected expected to detect

How these were measured

What none of this tells you

Check it yourself

The machine-readable version of this page, including every package in every corpus and its result, is at /benchmark.json. The rule catalog is at /v1/rules. Any package here can be re-scanned against the live service, bearing in mind that it names the package and not the release, so what you scan may be a later one:

curl -s https://lazaretto.dev/benchmark.json | jq '.surveys[].members[] | select(.verdict=="flagged")'