This is an active incident. Facts are drawn from reporting by Wiz, Aikido, BleepingComputer, Socket, and Aqua Security’s own disclosure. The situation was still evolving at time of writing.
What’s New This Week
On March 19, TeamPCP compromised Aqua Security’s Trivy scanner and related GitHub Actions, then used credentials stolen in that breach to deploy CanisterWorm across 47 npm packages on March 20. By March 22, attackers had also pushed malicious Trivy images to Docker Hub (versions 0.69.5 and 0.69.6) and were serving a wiper payload (kamikaze.sh) from a decentralised ICP canister C2. The canister was taken offline by Internet Computer on the evening of March 22.
Changelog
| Date | Summary |
|---|---|
| 23 Mar 2026 | Initial writeup covering the full attack chain from GHA compromise through CanisterWorm npm spread and Docker Hub. |
The incident
On March 19, 2026, a threat actor known as TeamPCP compromised Aqua Security’s Trivy vulnerability scanner. Within hours, a trojanized release (v0.69.4) was live on GitHub Releases, Docker Hub, GHCR, and ECR. Seventy-five out of seventy-six tags in the aquasecurity/trivy-action repository had been force-pushed to point at malicious commits. Seven aquasecurity/setup-trivy tags were also replaced.
The malicious release was live for approximately three hours. The compromised GitHub Actions tags stayed active for up to 12 hours. During that window, every CI/CD pipeline that ran aquasecurity/trivy-action or aquasecurity/setup-trivy executed attacker-controlled code with full access to the runner’s secrets.
The next day, on March 20 at 20:45 UTC, Aikido detected a new self-propagating worm – CanisterWorm – spreading across npm. Forty-seven packages were compromised using credentials harvested from the Trivy breach. By March 22, attackers had published two further malicious Trivy images to Docker Hub (0.69.5 and 0.69.6) and were demonstrating continued access to Aqua’s systems by publishing internal repositories publicly on GitHub.
The ICP canister serving the worm’s second-stage payload was made unavailable at approximately 21:31 UTC on March 22. Whether that represents full containment remains unclear.
What Trivy is, and why this is worse than a generic compromise
Trivy is Aqua Security’s open-source vulnerability scanner. It identifies CVEs, misconfigurations, and exposed secrets across container images, Kubernetes clusters, code repositories, and cloud infrastructure. It is one of the most widely deployed security tools in DevSecOps pipelines, used precisely by teams that take security seriously.
That is the whole problem. Compromising Trivy doesn’t give you access to some random project’s CI/CD pipeline. It gives you access to the pipelines of security-conscious organisations – the ones that added a vulnerability scanner to their workflow. The teams most likely to be running aquasecurity/trivy-action are the same teams most likely to have robust CI/CD infrastructure, production credentials in their secrets store, and cloud provider keys with meaningful permissions.
The attacker found the security tool and pointed it at its own users.
How the attack chain worked
The March 19 compromise was the second time Trivy was hit within a month. The first incident dated to around March 1, when an autonomous bot called hackerbot-claw exploited a pull_request_target misconfiguration in Trivy’s GitHub workflows. That gave the bot access to a Personal Access Token with push rights. The credentials from that initial breach were never fully rotated.
Itay Shakury, VP of Open Source at Aqua Security, said in the project’s public disclosure: “This was a follow up from the recent incident (2026-03-01) which exfiltrated credentials. Our containment of the first incident was incomplete. We rotated secrets and tokens, but the process wasn’t atomic and attackers may have been privy to refreshed tokens.”
TeamPCP used those retained credentials on March 19. At 17:43:37 UTC, the Trivy repository’s v0.69.4 tag was pushed, triggering a release. The attackers had also compromised the aqua-bot service account, which they used to push malicious workflows to tfsec, traceeshark, and trivy-action. GPG keys, Docker Hub credentials, Twitter and Slack tokens were exfiltrated to a Cloudflare Tunnel C2 at plug-tab-protective-relay.trycloudflare.com.
The malicious commits were crafted to look legitimate. CrowdStrike noted that the author name, commit message, and timestamps all mirrored a real prior commit (the message read “Upgrade trivy to v0.53.0 (#369)”, dated 2024-07-09). Git allows arbitrary author and committer dates via environment variables, so commit metadata cannot be trusted. Wiz identified an additional forensic tell: each malicious commit claimed a timestamp from its original release year (2021, 2022, etc.) but had a parent commit dated March 2026 – physically impossible, and the fingerprint of a force-pushed rewrite.
Socket security researcher Philipp Burckhardt described the scope: “We identified that an attacker force-pushed 75 out of 76 version tags in the aquasecurity/trivy-action repository, the official GitHub Action for running Trivy vulnerability scans in CI/CD pipelines. These tags were modified to serve a malicious payload, effectively turning trusted version references into a distribution mechanism for an infostealer.”
On how the attackers got in: “They had valid credentials with sufficient privileges to push code and rewrite tags, which is what enabled the tag poisoning we observed.”
What the malicious payload did
The trojanized GitHub Actions injected a three-stage payload into both trivy-action (via entrypoint.sh) and setup-trivy (via action.yaml).
Collection. The malware scraped memory from the Runner.Worker process by reading /proc/*/mem and searching for the pattern {"value":"","isSecret":true} – the structure GitHub Actions uses to store secrets in memory. It also swept 50+ filesystem paths for SSH keys; cloud credentials (AWS, GCP, Azure); Kubernetes tokens; Docker configuration; database credentials (PostgreSQL, MySQL, MongoDB, Redis); Terraform, Jenkins, and GitLab CI files; TLS private keys; VPN configurations; Slack and Discord webhook tokens; shell history; /etc/passwd, /etc/shadow; and cryptocurrency wallets.
Encryption and exfiltration. Harvested data was encrypted with AES-256-CBC and RSA-4096, bundled into tpcp.tar.gz, and sent to the typosquatted domain scan.aquasecurtiy[.]org (note the transposed letter). If that failed, the malware created a public GitHub repository named tpcp-docs in the victim’s account and uploaded the stolen data as a release asset.
Persistence on developer machines. If the malware detected it was running outside GitHub Actions (i.e., on a developer’s workstation), it dropped a Python script at ~/.config/systemd/user/sysmon.py and registered it as a persistent systemd service. That script polled an ICP canister for additional payloads and executed whatever it was pointed at.
CanisterWorm: npm spread and Kubernetes wiper
On March 20, less than 24 hours after the Trivy breach, Aikido detected a wave of npm package compromises. Forty-seven packages were hit: 28 in the @EmilGroup scope, 16 in the @opengov scope, plus @teale.io/eslint-config, @airtm/uuid-base32, and @pypestream/floating-ui-dom. The credentials used came directly from what TeamPCP harvested in the Trivy breach.
Aikido named the worm CanisterWorm for its use of an Internet Computer Protocol (ICP) canister as a command-and-control dead-drop – the first time this technique has been observed in a campaign of this type.
The worm’s architecture has three layers.
The postinstall hook is a Node.js postinstall script that installs a persistent Python backdoor via a systemd user service named pgmon, masquerading as PostgreSQL tooling. The whole thing is wrapped in a try/catch so npm install completes normally. The backdoor only activates on Linux with systemd.
The self-propagating deploy script (deploy.js) takes the npm tokens available in the environment, resolves the associated usernames, enumerates all packages in the scope that the token can publish to, bumps patch versions, and republishes the malicious payload to each one. According to Aikido, it compromised 28 packages in under 60 seconds.
The persistent Python backdoor sleeps for five minutes (sandbox evasion), then polls the ICP canister tdtqy-oyaaa-aaaae-af2dq-cai every 50 minutes. The canister returns a URL pointing to a binary payload. The backdoor downloads it, marks it executable, and runs it in a detached process. The canister controller can swap the URL at any time, pushing new binaries to every infected host without touching the implant.
From March 22, Wiz reported the canister was actively serving kamikaze.sh – a wiper payload. The use of ICP makes takedown difficult: only the canister’s controller can remove it, and normal infrastructure channels don’t apply to a decentralised blockchain. Internet Computer ultimately took the canister offline via policy enforcement at 21:31 UTC on March 22.
Why CI/CD pipelines are the right target
CI/CD runners are credential warehouses. A typical production pipeline holds cloud provider access keys, container registry credentials, deployment SSH keys, database passwords, Terraform state credentials, Kubernetes service account tokens, and often more. These credentials frequently have broad permissions – they need them to deploy.
An attacker who runs code inside a GitHub Actions runner gets all of that. Not because of a flaw in GitHub, but because that is precisely what CI/CD secrets are for. And because the pipeline runs automatically on every commit, the attack executes repeatedly, harvesting fresh credentials from every project that uses the compromised action.
CanisterWorm compounds this. Using harvested npm publish tokens, it spreads to every package in the victim’s npm scope, infecting the downstream dependencies of whoever installs those packages next. Credentials harvested from one pipeline become the seed for infections across an entirely different ecosystem.
The pull_request_target footgun
The root of the initial March 1 breach was a pull_request_target misconfiguration – a well-documented but still widely misunderstood GitHub Actions vulnerability.
pull_request_target runs workflows in the context of the base repository, not the fork, which means it has access to base-repo secrets. This is intentional: it lets maintainers label PRs, post comments, and do other privileged operations in response to contributions from forks. The footgun is that if the workflow also checks out code from the fork and executes it, that forked code runs with access to those secrets.
An automated bot exploited this. It submitted a crafted pull request that triggered a pull_request_target workflow, which checked out the fork’s code and ran it. The bot stole a PAT with push rights. That PAT, or refreshed versions acquired before full rotation, is what enabled everything that followed on March 19.
The fix is straightforward: never combine pull_request_target with a checkout of the PR’s code without extremely careful scoping. GitHub’s own documentation flags this explicitly. For any workflow that handles untrusted input, pin to SHA and use a dedicated token with minimum permissions required.
What to do now
If your pipelines use Trivy, work through this.
Immediate. Check whether any pipeline ran aquasecurity/trivy-action or aquasecurity/setup-trivy between approximately 17:00 UTC on March 19 and 05:00 UTC on March 20. If so, treat those runner environments as fully compromised. Check whether you pulled or executed Trivy v0.69.4 or Docker images tagged 0.69.4, 0.69.5, or 0.69.6. Remove any affected artifacts. Look for repositories named tpcp-docs in your GitHub organisation – this is the fallback exfiltration mechanism. Rotate everything that could have been exposed: cloud credentials, SSH keys, API tokens, database passwords, npm publish tokens.
npm. Check your dependency lockfiles against the compromised packages: @EmilGroup/* (28 packages), @opengov/* (16 packages), @teale.io/eslint-config, @airtm/uuid-base32, @pypestream/floating-ui-dom. On any Linux system that installed a compromised package, check for a pgmon systemd user service and the files ~/.local/share/pgmon/service.py, /tmp/pglog, /tmp/.pg_state.
Long-term. Pin all GitHub Actions to full commit SHA, not version tags. Tags can be moved. A SHA cannot. This single change would have prevented the March 19 attack from executing in any downstream pipeline. Audit pull_request_target workflows across your repositories. If any check out fork code without strict scoping, fix them. Treat npm publish tokens as high-value credentials – scope them to the minimum set of packages required and rotate them on the same schedule as cloud access keys.
The pattern
SolarWinds. XZ Utils. Now Trivy.
The pattern is consistent: compromise a trusted tool in the software supply chain, use that trust to reach targets who would otherwise have robust defences, and extract credentials that open further doors. SolarWinds gave attackers access to the monitoring infrastructure of thousands of organisations. XZ Utils targeted a compression library present in almost every Linux distribution. Trivy targets the security tool that was supposed to catch exactly this kind of thing.
The particular cruelty of the Trivy attack is that it exploited the trust relationship between a security tool and the security-aware teams running it. The organisations hit are not the ones who ignored security. They are the ones who built automated vulnerability scanning into their pipelines. They added Trivy precisely because they wanted to catch this.
What the attack makes clear is that supply chain trust cannot be assumed, even for security tooling. Especially for security tooling. The tools you run in privileged pipeline positions are the highest-value targets.
Pin your actions to SHA. Treat every CI/CD secret as potentially compromised. Assume that any tool with write access to your pipeline is a target.
The scanner getting scanned is not a one-time irony. It is a template.