A self-replicating worm turned a one-off typosquat into a campaign - and ~11% of orgs run known-malicious packages in production.
Supply chain attacks roughly doubled in 2025, to around 26 incidents a month, but the number that should change how you defend is not the volume - it is the automation. The Shai-Hulud worm did not just typosquat a single package and wait. It compromised a maintainer, harvested their credentials, and used those credentials to publish malicious versions of every other package that maintainer controlled. Each new victim became a new publisher. A one-off foothold turned into a self-propagating campaign that moved at the speed of the registry, not the speed of a human attacker.
That changes the defender's problem. When a compromise replicates on its own, the window between "a package broke" and "the package is in your build" collapses to hours. Detecting the CVE later is not enough. You have to know which dependency you actually pull, which one your code actually runs, and you have to know it the same day the worm starts moving - even if your build runs air-gapped.
The mechanism is mundane, which is what makes it dangerous. A package's install lifecycle script runs with the developer's or CI runner's privileges. If that script is malicious, it reads whatever tokens are in the environment and uses the legitimate publish API to ship itself into the maintainer's other packages. Nothing here is exotic code execution; it is the registry working exactly as designed, pointed the wrong way.
The defensive lesson is that you cannot enumerate the blast radius in advance. You do not know tomorrow's malicious version of a package you trusted yesterday. So the question stops being "is this a known-bad version?" and becomes "of everything I depend on, which ones do I actually import and reach - and are any of them flagged right now?"
apPosture's SCA engine parses the repo's manifests and lockfiles into a dependency inventory, then asks the cheap question first: is this package even imported, and if so, is the vulnerable symbol actually used? Import-level analysis answers "is the package pulled in?". A call-graph-lite pass goes one layer deeper - for Python it walks the AST to resolve import bindings and record which symbols of a package are referenced, so it distinguishes "imported but the vulnerable function is never called" from "actually reached." That distinction is the single biggest source of SCA false positives, and it is exactly the signal a VEX statement needs to justify code_not_reachable.
Every refined finding carries an explicit reach state instead of a blanket severity. A dependency that is present in the lockfile but unimported is not the same risk as one whose vulnerable symbol sits on a live code path, and the queue should not pretend it is:
| reach_level | What it means | How we treat it |
|---|---|---|
| symbol | The vulnerable function or identifier is actually referenced in source. | Top of the queue - reached on a real code path. |
| import | The package is imported, but the vulnerable symbol is not seen. | Lowered - present and loadable, not proven reached. |
| unimported | Listed in the lockfile, no import found in the scanned source. | Suppressed toward VEX code_not_reachable. |
| unknown | Ecosystem we do not yet resolve at symbol level. | Kept at the import-level verdict, never over-claimed. |
Reachability is deterministic, offline, and bounded by file and read caps. It does not guess; where it cannot prove a symbol is reached, it says so rather than inflating the finding. The same proof-over-heuristics rule that governs the DAST engine governs the dependency graph: present is not reached, and reached is not the same claim as exploited.
Worms create duplicate noise. The same compromised package will show up in your own SCA scan, in an imported third-party report, and in a container image inventory - three rows for one root cause. apPosture collapses them with a source-aware fingerprint. For dependency findings the identity key is the CVE plus the component, so the same Log4Shell on the same package merges into one Vulnerability no matter how many scanners reported it.
Merging is not cosmetic. Source count feeds risk scoring: a weakness confirmed by two independent tools is weighted up, while three copies of one finding no longer flood the queue. And when an advisory has no CVE - a GHSA on a package, for instance - the advisory id keys the merge instead, so distinct advisories stay distinct while the same one still collapses.
A worm that broke yesterday is useless intelligence if your build cannot see it. The bundled advisory DB is a curated, version-aware set authored in-house from public CVE facts, so an air-gapped pipeline matches dependencies with zero egress. When an operator enables egress, the engine additionally queries OSV - Google's open aggregator of GitHub Advisory, PyPA, Go, RustSec and npm data - and merges those results into the same findings. The typosquat heuristic runs offline too, flagging a direct dependency whose name is an edit-distance neighbour of a well-known package, alongside the curated known-malicious package feed.
There is one more discipline a supply chain tool owes you: it must not become the soft entry it is meant to catch. The agent that runs inside your pipeline is checksum-pinned. There is no curl-pipe-shell bootstrap that a compromised registry could swap underneath you mid-build - which is precisely the move a self-replicating worm is built to make.
Self-replication raised the floor on supply chain risk: the attack now scales without an attacker in the loop. The answer is not a louder feed of every CVE in your lockfile. It is a shorter, reachability-ranked, de-duplicated list that works offline, names the dependency you actually run, and refuses to trust its own delivery path.