In mid-May 2025, an attacker compromised a maintainer account in the antv organization on npm and published malicious versions of several widely depended-on packages, including @antv/g2. Because @antv/g2 is a transitive dependency of echarts-for-react and a long tail of downstream charting libraries, the malicious version reached an audience much larger than the maintainer’s direct user base — every CI runner that resolved a fresh dependency tree during the publish window, every developer machine that ran npm install against a project that pinned a caret range, every container build that did not cache.
This is not a new shape. We have seen it with event-stream in 2018, with ua-parser-js in 2021, with node-ipc in 2022, with the polyfill.io CDN takeover in 2024, with xz-utils in 2024. The interesting question is never “would we have caught this one?” The interesting question is what would have been cheap to catch — and what we should expect the next one to look like.
The shape of the attack
The malicious payload in @antv/g2 and its sibling packages was delivered through a preinstall lifecycle hook. The hook ran a Bun-based loader that decoded an obfuscated script, executed it in-process, and exited cleanly so that npm install reported success. The script targeted high-value secrets accessible from the install-time environment:
- GitHub personal access tokens and OIDC tokens from the runner environment
- npm authentication tokens from
~/.npmrc - AWS credentials, STS session tokens, and SSO caches
- SSH private keys and
known_hosts - Kubeconfigs
- Any
.envfile in the current working tree - SaaS bearer tokens identifiable by file-path heuristic (Slack, Stripe, Vault)
Exfiltration was an HTTPS POST to t.m-kosche[.]com:443 with TLS certificate validation disabled — the standard tell of a payload that does not want to depend on a clean trust store on the victim’s box.
Two further details elevate this above a standard credential-stealer. First, the byte payload was identical across every compromised package, which means the attacker built it once and used the maintainer account to fan it out. Second, the payload made use of stolen OIDC tokens to forge SLSA provenance attestations on follow-on releases of other packages the maintainer had permission to publish — a worm dressed in the language of supply chain integrity. The audit trail did not catch it because the audit trail said the right things.
Microsoft Defender, deployed on a meaningful fraction of the affected developer endpoints, fired on the post-install behavior. Its signatures landed as Trojan:AIGen/NPMStealer and the more generic Suspicious Node.js process behavior and Credential access attempt. Defender did what an EDR is supposed to do: it noticed a Node process reading credential-bearing files and reaching out to an unfamiliar host, and it stopped a portion of the exfiltration. That is a real save.
It is also not the question we want to be asking.
What would have been cheap to catch
Three observations about this payload have nothing to do with malware signatures, nothing to do with t.m-kosche[.]com being on a blocklist, and nothing to do with anyone having read the source of @antv/g2. Each of them is a property of the install event itself.
One. An npm preinstall hook ran outbound network. The install of a charting library — a piece of code whose entire purpose is to render SVG elements in a browser — established an HTTPS session to a third-party domain during its build phase. There is no version of “install a charting library” that has any business reaching out to the internet. The behavior is anomalous on its face, before any malware signature exists.
Two. The same hook read ~/.aws/credentials, ~/.ssh/id_*, ~/.npmrc, and every .env file it could find. These are not files an install script should ever touch. A categorical filesystem policy — install steps may touch the package cache, node_modules, and a scoped temp directory; nothing else — flags this without knowing what the script does.
Three. The malicious version was a behavioral diff away from the prior known-good version of @antv/g2. Same package name, same maintainer, same registry. The version that shipped in May 2025 added a preinstall hook that had not been there. The pre-install script did file reads that the prior version did not do. It opened a TLS session the prior version did not open. None of these required knowing the payload was malicious — they only required noticing that the package had grown a new mouth.
None of these three observations is novel. Defender’s behavior-based detection caught the post-install version of all three. But by the time post-install behavior is the question, the artifact is already on the endpoint, the credentials are already exposed, and the worm has had its publish window.
The cheap thing to catch is the publish event, not the install event.
The wedge
The structural difference between scan-after-install and prove-before-install is the wedge the whole product points at. The antv/g2 incident is a concise case for it.
Every defense that fired against this attack fired after the bad artifact reached someone’s machine. EDR signatures fire post-install. Outbound network blocks fire when the egress is attempted. GitHub secret-scanner alerts fire when an exposed PAT is observed in a public repo. These controls are real, they save real keys, and they are the wrong layer. The right layer is the one between the registry and the consumer — the layer that decides whether an artifact is admitted to a fleet’s catalog in the first place.
At that layer, the three observations above are not “interesting heuristics” — they are policy. Network egress from a preinstall hook is a policy violation. Credential-file reads during install is a policy violation. A package version that has grown new lifecycle hooks since the last admitted version is a state that demands human review before promotion. None of these requires knowledge of the specific attacker, the specific C2 domain, or the specific obfuscation routine. They are properties of the artifact, observable in a controlled environment, before the artifact is allowed near a real endpoint.
This is the boundary Attestree is built around. For winget today, our policy gate runs at ingest and the artifact never reaches a fleet node without a signed receipt that says it complied. For npm, the same substrate is on the pipeline roadmap — the gate moves from one package manager to the next, the policy language does not change.
What we would not claim
Three honest limits, because the alternative is the kind of marketing that loses an audit.
Time-bombed payloads defeat single-shot detonation. If the antv/g2 payload had been written to wait for a calendar-day check or for a specific weekday before activating, a one-time pass through an ingest sandbox would have admitted it as quiet. The mitigation is re-detonate on promote (every ring transition reruns the gate, with the clock advanced and jittered), but the failure mode is real and deserves naming.
Environment-aware payloads need realistic sandboxes. If the script had checked process.env.CI or for the existence of ~/.aws/credentials before exfiltrating, an empty CI-shaped sandbox would have looked clean. The mitigation is honey-credentials and realistic environment shape, and it is work — not a checkbox.
A catalog gate cannot stop npm-side worm propagation. Once the malicious version was published, every consumer who installed it before the gate observed it was at risk. A control-plane operator can refuse to admit the bad version into a managed catalog, but the public registry is not under their control. The post-compromise account rotation — the part where you assume every credential exposed during the publish window is burned — is not something attestation alone solves.
These limits are why the question is what would have been cheap to catch, not what would have been impossible to miss. The cheap things are real, they would have caught most of the blast radius of antv/g2, and they will catch most of the next one. The expensive things — perfect detection, zero-credential CI environments, single-package isolation — are a different conversation.
The next one
Two patterns to watch for. The first is maintainer-account compromise of a high-fanout package with a non-obvious post-install side effect — antv/g2 fits this; so does every similar attack since event-stream. The lifecycle hook may be preinstall (as in this case), postinstall, prepare, install, or any of the half-dozen others. The hook may not appear in the package’s own scripts but may be inserted as a transitive — a tweak to a build tool that everyone in the dependency tree calls without thinking.
The second is provenance forgery via stolen OIDC. The antv/g2 attacker did not just steal credentials and stop. They used them to publish further releases under valid SLSA attestations, against repos whose CI environments the attacker had reached. The attestation said the right things; the build environment was hostile. A catalog operator who accepts attestations without verifying the substance of what was attested — who treats “this build was signed” as equivalent to “this build did the things its manifest claims” — has not raised the bar. They have moved it.
The wedge holds against both patterns: the boundary one hop earlier, with policy that is about artifact behavior and not artifact provenance. Provenance is a necessary input. It is not the output that should gate admission. The output should be a controlled detonation, a behavioral diff against the last admitted version, and a signed receipt that an operator can show an auditor without flinching.
That receipt is what was missing on May 18, 2025. That is what we are building.
This essay reflects publicly reported indicators of the May 2025 antv supply-chain compromise. Attestree is in design-partner mode and pre-GA. Our winget ingest gate ships today in the free Community Edition, with the behavioral-divergence flag it feeds live for winget and Chocolatey in observe-only form; the npm gate is on the published roadmap. We do not claim to have caught this incident. We claim that a catalog-layer defense built on ingest-time behavioral policy would have raised the bar against it, and we are building exactly that.