This post will be updated as the situation develops.
What’s New This Week
On 20 March 2026, Aikido Security detected CanisterWorm – a previously undocumented self-propagating worm – spreading across npm packages using credentials stolen in the Trivy supply chain compromise. 47+ packages are confirmed affected. The worm uses an Internet Computer Protocol (ICP) canister as a decentralised C2 dead-drop, the first documented use of this technique in a supply chain campaign.
Changelog
| Date | Summary |
|---|---|
| 21 Mar 2026 | Initial publication covering CanisterWorm npm propagation and full attack chain. |
On 19 March 2026, a threat actor identified as TeamPCP used residual credentials from an earlier compromise to force-push malicious commits to 75 of 76 aquasecurity/trivy-action version tags. Trivy has 33,200 GitHub stars and runs in CI/CD pipelines across thousands of organisations. Any pipeline that ran a Trivy scan between roughly 19:00 UTC on 19 March and detection the following day executed attacker code.
That was bad enough. What happened next is worse.
By 20:45 UTC on 20 March, Aikido Security had detected a separate, escalating attack: stolen npm tokens from compromised pipelines were being weaponised to spread a self-propagating worm – now called CanisterWorm – across npm. Forty-seven packages and counting.
How the trivy-action compromise worked
Trivy first came under attack in late February 2026 when attackers compromised the Aqua Trivy VS Code extension and obtained a credential with write access to the Trivy GitHub account. Maintainers rotated tokens in response, but the rotation wasn’t fully atomic. Residual credential artifacts remained.
TeamPCP used that residual access on 19 March to compromise aquasecurity/trivy-action using a technique that evades most standard defences. Rather than pushing new commits or creating new releases – either of which would generate notifications and appear in commit history – they force-pushed existing version tags to point at new malicious commits.
The process, as reconstructed by Socket Security, was methodical:
- Start from master HEAD (57a97c7e)
- Swap
entrypoint.shwith the infostealer payload, leaving everything else intact - Clone the original commit’s metadata exactly: author name, email, committer, both timestamps, and the full commit message including PR number and “Fixes” references
- Set the parent to the master HEAD rather than the original
- Force-push the tag
The result: @0.34.2, @0.33, @0.18.0, and 72 other tags silently pointed at malicious code. @0.35.0 was the only tag left clean.
The malicious entrypoint.sh runs the real Trivy scan in parallel, so output looks completely normal. In the background, a 204-line script dumps runner process memory to extract secrets, harvests SSH keys, sweeps environment variables, enumerates cloud credentials and Kubernetes tokens, compresses and encrypts everything, then exfiltrates to scan.aquasecurtiy[.]org – note the typosquatted domain. If that fails, it falls back to creating a GitHub repo named tpcp-docs using the stolen GITHUB_TOKEN and posting the data there.
Wiz’s analysis found that a separate malicious trivy binary (version 0.69.4) was also published. That binary drops a Python loader to ~/.config/sysmon.py which polls a C2 on the Internet Computer blockchain approximately every 50 minutes, after an initial five-minute sleep to outlast sandbox analysis timeouts.
The attackers also compromised Aqua’s service account to steal GPG keys and credentials for Docker Hub, Twitter, and Slack, exfiltrating those via a Cloudflare Tunnel C2. Supply chain attacks rarely stay contained.
CanisterWorm: what it is and how it spreads
The stolen credentials from compromised pipelines didn’t sit idle. Aikido Security’s detection on 20 March confirmed that npm tokens harvested by the Trivy infostealer were being used to publish malicious versions of packages across npm scopes.
The worm’s architecture is three stages:
Stage 1 – Node.js postinstall hook. When an infected package is installed, postinstall runs a loader that decodes an embedded base64 payload (a Python script), writes it to ~/.local/share/pgmon/service.py, installs a systemd user service (pgmon.service with Restart=always), and starts it immediately. No root required. No password prompt. The service survives reboots.
Stage 2 – Python backdoor. service.py wakes up after five minutes and starts polling the ICP canister every ~50 minutes. The canister returns a URL pointing at a binary. That binary is downloaded to /tmp/pglog and executed. Everything is disguised with PostgreSQL naming – pgmon, pglog, .pg_state – to blend into developer machines.
Stage 3 – Self-propagation via deploy.js. A separate deploy.js script uses harvested npm tokens to enumerate every package the token can publish, bumps the patch version, and publishes the worm payload to each one. Aikido documented 28 packages in the @EmilGroup scope being compromised in under 60 seconds. Other affected scopes include @opengov (16 packages), plus @teale.io/eslint-config, @airtm/uuid-base32, and @pypestream/floating-ui-dom.
The ICP canister as C2 is the genuinely novel part. An Internet Computer canister is a smart contract running on a decentralised blockchain. It has no single hosting point, it can’t be taken down by a hosting provider or a domain registrar, and the controller can update the payload URL remotely at any time. Every infected host polls the same canister and receives whatever binary the attacker pushes. This is censorship-resistant remote payload rotation at scale.
There’s also a kill-switch: the canister reportedly uses a youtube.com URL as a dormant signal to arm or disarm implants. The operational design here is mature.
The worm only activates on Linux with systemd. The postinstall script wraps the entire operation in a try/catch – npm install completes normally on all platforms. Developers on macOS won’t trigger it, but will spread it if they publish from a compromised token.
What you need to do right now
If your CI/CD pipelines reference any aquasecurity/trivy-action tag other than @0.35.0, or if you ran trivy v0.69.4, assume your pipeline secrets are compromised.
Immediate actions:
- Rotate all secrets accessible to any affected pipeline: GitHub tokens, cloud credentials (AWS, GCP, Azure), SSH keys, Kubernetes service account tokens, Docker Hub credentials, npm tokens.
- Audit your GitHub organisation for repositories named
tpcp-docs– that’s the exfiltration fallback. - Review GitHub Actions logs for any
trivy-actionruns after 19:00 UTC on 19 March. If they ran, treat those runners as compromised. - Check
~/.config/sysmon.pyand~/.local/share/pgmon/on developer machines that may have installed affected packages. Look forpgmon.servicein~/.config/systemd/user/. - Pin all GitHub Actions by commit SHA, not by mutable version tag.
aquasecurity/[email protected]is not the same thing asaquasecurity/trivy-action@<full-sha>. Mutable tags are the root attack surface here.
Audit your npm dependencies:
The 47 affected packages span several major scopes. If your package.json or lockfile references anything in @EmilGroup or @opengov, check when those versions were published. Aikido has published a full package list. Review your lockfile for any patch version bumps on 20 March.
Going forward:
Use StepSecurity/harden-runner in your GitHub Actions workflows. It monitors and restricts outbound network traffic from runners. It would have caught the C2 phone-home from the malicious Trivy binary in real time. The community tier is free for public repos.
Restrict GITHUB_TOKEN permissions. Most workflows get far more token scope than they need. contents: read is enough for a scanner. packages: write on a pipeline that doesn’t publish should not exist.
Consider whether you need Trivy at all for now. Grype, Syft, and Snyk are viable alternatives that weren’t caught in this campaign.
What this means for DevSecOps supply chain security
The Trivy incident is a case study in cascading supply chain failure, and several things about it merit attention beyond the immediate remediation.
The initial compromise was a credential rotation that didn’t fully clean up. One residual API key, months later, became the pivot point for a second and more destructive attack. Aqua Security is a security company. Their processes are presumably better than most. If partial credential rotation is a risk they faced, it’s a risk that almost every team faces.
The delivery mechanism – force-pushing existing version tags rather than creating new releases – is a specific evasion technique that defeats the most common defences: watching for new commits, watching for new releases, comparing against known-good SHA hashes at build time. If your dependency tracking only watches for new versions, you’re blind to this attack class.
The escalation to CanisterWorm demonstrates something important: an infostealer in one part of the supply chain becomes a propagation mechanism in another. GitHub tokens exfiltrated from CI runners become credentials to compromise more GitHub Actions. npm tokens exfiltrated from the same runners become credentials to spread a worm across npm. The blast radius of a CI compromise is no longer limited to the secrets it directly handles – it extends to every system those secrets can touch.
The ICP canister C2 is a meaningful operational upgrade. Blockchain-based C2 has been theoretical for years. This is a production deployment in an active campaign. Defenders can’t easily take it down. The payload can be updated remotely at any time. This technique will appear in more campaigns.
The broader pattern is that security tooling sits in a uniquely privileged position in CI/CD pipelines. Scanners, linters, formatters – they all run with elevated permissions in trusted environments, they’re rarely sandboxed, and they’re rarely scrutinised the way application dependencies are. If you would audit a change to your application code but not to your scanning action, that’s an asymmetry attackers understand very well.
Mutable version tags in GitHub Actions are not a new problem. The community has been pointing at them for years. The fix is simple: pin to a commit SHA. It’s also slightly inconvenient, which is why most teams don’t do it. This incident is the clearest demonstration of what “slightly inconvenient” is worth compared to rotating every secret your CI system has ever touched.
Rotate the credentials. Pin the SHAs. Then ask why your security scanner was the least-scrutinised dependency in your pipeline.