Trivy Supply Chain Compromise: What Happened, What Was Stolen, and How to Respond
Table of Contents
TL;DR
Between February 27 and March 22, 2026, the threat actor TeamPCP compromised Aqua Security’s Trivy vulnerability scanner, its GitHub Actions, and over 60 npm packages in a multi-phase open source software supply chain attack. This post consolidates the timeline, techniques, IOCs, and remediation guidance from multiple vendor reports into a single reference.
If you used Trivy, trivy-action, or setup-trivy between March 19-22, 2026, assume the following impact:
- All secrets accessible to the workflow (GitHub tokens, cloud credentials, SSH keys, Docker registry tokens) were exfiltrated, encrypted with RSA-4096, and sent to attacker infrastructure
- CI/CD runner memory was scraped for every secret marked
isSecret:truein GitHub Actions - Developer machines that ran Trivy v0.69.4 outside CI have a persistent systemd backdoor polling for arbitrary payloads
- npm tokens stolen from CI runners were used to publish malicious versions of 64+ packages within 60 seconds
- Kubernetes clusters accessible from compromised runners may have attacker DaemonSets with host-level access
How it Started: An AI Bot and a Misconfigured Workflow
On February 27, 2026, an autonomous GitHub bot called hackerbot-claw opened PR #10252 against the Trivy repository. The PR was immediately closed, but that didn’t matter. Trivy’s “API Diff Check” workflow used pull_request_target, which runs with repository-level secrets while executing code from the attacker’s fork. This is the well-documented “Pwn Request” pattern.
The bot exfiltrated a Personal Access Token to recv.hackmoltrepeat[.]com. By February 28, the attacker had full repository control: they privatized the repo, deleted all 178 GitHub releases (v0.27.0 through v0.69.1), and published a malicious VS Code extension to Open VSIX.
Aqua Security responded and rotated credentials. But the rotation wasn’t atomic. The attacker observed or intercepted the refreshed tokens, maintaining access through the aqua-bot service account.
The Second Compromise: v0.69.4 and Tag Poisoning
On March 19 at 17:43 UTC, aqua-bot pushed a malicious v0.69.4 tag to Trivy, triggering the automated release pipeline. The malicious binary was distributed across GitHub Releases, GHCR, Docker Hub, ECR Public, deb/rpm repositories, and get.trivy.dev before being removed around 21:36 UTC, a roughly 3-hour exposure window.
The malicious Trivy binary ran the legitimate scanner alongside the payload, masking the intrusion from the user’s perspective. On developer machines (detected by GITHUB_ACTIONS != "true"), it installed a persistent backdoor as a systemd user service at ~/.config/systemd/user/sysmon.py that polled an ICP canister for arbitrary payloads every five minutes.
Tag Poisoning
The bigger blast radius came from GitHub Actions. Within hours, aqua-bot force-pushed 75 of 76 trivy-action tags (0.0.1 through 0.34.2) and all 7 setup-trivy tags to malicious commits. Over 10,000 GitHub workflows referenced trivy-action. Any workflow using aquasecurity/[email protected] silently resolved to the attacker’s code.
The technique was crafted to avoid detection: the attacker started from master HEAD, replaced only entrypoint.sh, cloned the original commit’s metadata (author, email, timestamps, message), and force-pushed the tag. The spoofed commits even impersonated well-known developers: “rauchg” (Guillermo Rauch, Vercel CEO) for the actions/checkout injection, and “DmitriyLewen” (Aqua maintainer) for the Trivy commit.
Detection signals: missing GPG signatures on commits that were originally web-signed, impossible chronology (commits dated 2021 with parents from March 2026), and only one file changed per commit where originals touched multiple files.
What the Credential Stealer Did
The malicious entrypoint.sh contained a three-stage payload in 204 lines:
Collection: On GitHub-hosted runners, the malware escalated to root via passwordless
sudo, locatedRunner.Workerprocesses, and scraped their memory via/proc/<pid>/mem, searching for the JSON pattern{"value":"<secret>","isSecret":true}where Actions stores masked secrets. It also swept 50+ filesystem paths for SSH keys, AWS/GCP/Azure credentials, Kubernetes tokens, Docker configs, and cryptocurrency wallets.Encryption: Harvested data was encrypted using AES-256-CBC with RSA-4096 hybrid encryption (OAEP padding), bundled as
tpcp.tar.gz. Only the attacker’s private key can decrypt.Exfiltration: Primary channel was HTTPS POST to
scan.aquasecurtiy[.]org(typosquat of aquasecurity.org, resolving to45.148.10.212). Fallback: ifINPUT_GITHUB_PATwas available, the malware created a publictpcp-docsrepository on the victim’s GitHub account and uploaded the encrypted bundle as a release asset.
The legitimate Trivy scan ran after the malware (lines 106-204), so users saw normal scan output.
CanisterWorm: npm Goes Self-Propagating
Less than 24 hours after the Trivy compromise, stolen npm tokens from CI runners were weaponized. On March 20 at 20:45 UTC, TeamPCP deployed CanisterWorm, a self-propagating npm worm that represents the first observed use of Internet Computer Protocol (ICP) canisters as malware C2 infrastructure.
The worm’s propagation logic:
- Harvest npm tokens from
~/.npmrc, project.npmrc, environment variables - Authenticate via
/-/whoami, enumerate publishable packages via maintainer search - Bump patch version, preserve the original README, publish with
--access public --tag latest - Restore original files in a finally block
28 @emilgroup packages were compromised in under 60 seconds. The worm then spread to @opengov (16 packages), @teale.io/eslint-config, @airtm/uuid-base32, and @pypestream/floating-ui-dom. By March 21, Socket reported 135 malicious artifacts across 64+ unique packages.
The payload architecture was a three-stage chain: a Node.js postinstall loader decoded a base64 Python script, wrote it to ~/.local/share/pgmon/service.py, and installed it as a systemd user service (pgmon.service) masquerading as PostgreSQL monitoring. The Python backdoor slept 5 minutes (sandbox evasion), then polled the ICP canister tdtqy-oyaaa-aaaae-af2dq-cai every 50 minutes for a URL pointing to the current payload. A kill switch: if the URL contained “youtube.com”, the implant went dormant.
Rapid Payload Iteration: kamikaze.sh
On March 22, TeamPCP rotated through six payload versions in approximately five hours via the ICP canister, each one picked up by every infected machine on its next poll:
| Time (UTC) | Version | Capability |
|---|---|---|
| ~11:45 | v1 | Monolithic K8s DaemonSet with host escape, systemd persistence, Iran-targeted wiper |
| 12:45 | v2 | Modular loader fetching kube.py, C2 rotated to Cloudflare tunnel |
| 13:00 | v3 | Pure SSH/Docker worm (scans local /24 for ports 22, 2375) |
| 13:25 | v3.1 | Module split: kube.py (broken K8s) + prop.py (SSH/Docker worm) |
| 14:56 | v3.2 | Working ICP backdoor, kubectl re-enabled, new Cloudflare tunnel |
| 16:15 | v3.3 | WAV steganography: payloads hidden in audio files (bg_kube.wav, bg_prop.wav) |
The Iran-targeted wiper (discovered by ramimac) deployed a separate DaemonSet that detected Asia/Tehran timezone or fa_IR locale and executed rm -rf / --no-preserve-root. This is the only destructive, non-theft component in the campaign.
By 16:00 UTC, TeamPCP also pushed malicious Docker Hub images (v0.69.5, v0.69.6) directly, bypassing the GitHub release pipeline. At 20:31-20:32 UTC, 44 internal Aqua repositories in the aquasec-com organization were defaced. The ICP canister was denylisted at 21:31 UTC.
IOCs for Security Teams
Network Indicators
| Indicator | Role |
|---|---|
scan.aquasecurtiy[.]org / 45.148.10.212 | Primary C2 (TECHOFF SRV LIMITED, Amsterdam) |
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0[.]io | ICP canister dead drop (denylisted 03/22) |
plug-tab-protective-relay.trycloudflare[.]com | Cloudflare tunnel (credential exfil) |
investigation-launches-hearings-copying.trycloudflare[.]com | Cloudflare tunnel (kamikaze v2) |
championships-peoples-point-cassette.trycloudflare[.]com | Cloudflare tunnel (v3/v3.1) |
create-sensitivity-grad-sequence.trycloudflare[.]com | Cloudflare tunnel (v3.2/v3.3) |
recv.hackmoltrepeat[.]com | Phase 1 PAT exfiltration |
Affected Artifacts
| Component | Affected Versions | Exposure |
|---|---|---|
| Trivy binary | v0.69.4 (all registries), Docker Hub v0.69.5/v0.69.6 | ~3 hours |
trivy-action | Tags 0.0.1-0.34.2 (75 tags) | ~12 hours |
setup-trivy | Tags v0.2.0-v0.2.5 | ~4 hours |
| npm (@emilgroup, @opengov, others) | 135+ artifacts, 64+ packages | Varies |
Malicious Container Digests (v0.69.4)
Per Aqua’s advisory:
| Architecture | Digest |
|---|---|
| Generic | sha256:27f446230c60bbf0b70e008db798bd4f33b7826f9f76f756606f5417100beef3 |
| amd64 | sha256:12c702212dee1cbec9471e9261501a3335963321fe76e60e5a715b5acd3c40a2 |
| arm64 | sha256:2d7cee41048988eec27615412e7c6e2e21046f2b5faa888c24e11ca6764058ed |
Filesystem Indicators
| Path | Component |
|---|---|
~/.config/systemd/user/sysmon.py | Trivy developer machine backdoor |
~/.config/systemd/user/pgmon.service | CanisterWorm persistence |
~/.local/share/pgmon/service.py | CanisterWorm Python backdoor |
/tmp/pglog | Payload staging |
/tmp/.pg_state | State tracking |
Hunt Queries
- GitHub: Search your organizations for repositories named
tpcp-docsor matchingtpcp-docs-*. Their existence indicates successful credential exfiltration via the dead drop fallback. - GitHub Actions: Review workflow runs from March 19-20 referencing
aquasecurity/trivy-actionoraquasecurity/setup-trivy. Check the “Run Trivy” and “Setup environment” steps for anomalous output. - Kubernetes: Search for DaemonSets named
host-provisioner-stdorhost-provisioner-iraninkube-systemnamespace. - systemd: Check for user services matching
sysmon,pgmon,pgmonitor, orinternal-monitor. - npm: Audit package versions in
@emilgroup,@opengov,@teale.io,@airtm,@pypestreamscopes against expected versions.
What to Do Now
If you used Trivy between March 19-22, 2026:
- Pin to safe versions: v0.69.3 or earlier for Trivy, v0.35.0 for trivy-action, v0.2.6 for setup-trivy. Aqua’s advisory has the full version matrix.
- Rotate every secret accessible to affected workflows: GitHub tokens, cloud provider credentials, Docker registry tokens, SSH keys.
- Block the C2 domains and IP at your network perimeter.
- Verify container integrity using cosign signatures against known-good digests.
For long-term hardening:
- Pin all GitHub Actions to full commit SHAs, not version tags. Tags can be force-pushed; commit SHAs cannot.
- Use StepSecurity Harden-Runner or equivalent tools to monitor and restrict outbound network traffic from CI runners.
- Audit
pull_request_targetworkflows to ensure they never checkout untrusted fork code with elevated permissions. - Treat credential rotation as an atomic operation: revoke first, re-issue second, with no window where old and new credentials overlap.
Independently Verifiable Evidence
Several claims in this post can be verified directly against GitHub’s public APIs using the gh CLI. We ran these queries on March 23, 2026. You can reproduce them.
The release gap is real
178 releases (v0.27.0 through v0.69.1) are missing from Phase 1:
$ gh api repos/aquasecurity/trivy/releases --paginate \ --jq '.[].tag_name' | sort -V | tail -20# ...v0.25.4v0.26.0v0.69.2 # ← gap: 43 minor versions missingv0.69.3The malicious v0.69.4 tag has been deleted:
$ gh api repos/aquasecurity/trivy/git/ref/tags/v0.69.4# {"message":"Not Found","status":"404"}The imposter commit exists and is unsigned
Commit 1885610c is still in Trivy’s object store. It claims to be authored by “DmitriyLewen” (a real Aqua maintainer) but carries no signature. Legitimate DmitriyLewen commits go through GitHub web merges and are GPG-signed by GitHub.
$ gh api repos/aquasecurity/trivy/git/commits/1885610c6a34811c8296416ae69f568002ef11ec \ --jq '{author: .author.name, email: .author.email, message: .message, verified: .verification.verified, reason: .verification.reason}'{ "author": "DmitriyLewen", "message": "fix(ci): Use correct checkout pinning", "verified": false, "reason": "unsigned"}The same pattern holds for the setup-trivy malicious commit, which spoofed “Tomochika Hara” (thara):
$ gh api repos/aquasecurity/setup-trivy/git/commits/8afa9b9f9183b4e00c46e2b82d34047e3c177bd0 \ --jq '{author: .author.name, verified: .verification.verified, reason: .verification.reason}'{ "author": "Tomochika Hara", "verified": false, "reason": "unsigned"}setup-trivy: only one tag survives
Tags v0.2.0 through v0.2.5 were not restored after remediation. Only the clean v0.2.6 remains, matching Aqua’s advisory:
$ gh api repos/aquasecurity/setup-trivy/tags --jq '.[].name'v0.2.6The setup-trivy events API captures the incident response in real-time:
$ gh api repos/aquasecurity/setup-trivy/events \ --jq '[.[] | {type, actor: .actor.login, created_at, ref: .payload.ref, release: .payload.release.tag_name}]'# ...# 21:07 UTC - nikpivkin deletes v0.2.5 tag (incident response begins)# 21:34 UTC - itaysk adds emergency collaborator# 21:43 UTC - simar7 publishes clean v0.2.6 releasetrivy-action tags are all v-prefixed now
The original unprefixed tags (0.0.1 through 0.34.2) were deleted during remediation and could not be re-created because GitHub’s immutable releases feature locked the attacker’s force-pushed versions. 74 tags exist now, all v-prefixed:
$ gh api repos/aquasecurity/trivy-action/tags --paginate \ --jq '.[].name' | wc -l74
$ gh api repos/aquasecurity/trivy-action/tags --paginate \ --jq '.[].name' | sort -V | tail -5v0.33.0v0.33.1v0.34.0v0.35.0 # ← only safe tag (pointed to master HEAD during attack)The dead drop exfiltration worked
The Wiz analysis described a fallback where the malware creates tpcp-docs repos on victim GitHub accounts. These repos are publicly searchable:
$ gh search repos "tpcp-docs" --json fullName,createdAt[ {"createdAt": "2026-03-22T18:30:12Z", "fullName": "kaufmann-digital/tpcp-docs"}, {"createdAt": "2026-03-22T21:59:30Z", "fullName": "BEUMERGroupBot/tpcp-docs"}, {"createdAt": "2026-03-22T23:08:47Z", "fullName": "cloud-team-si-it/tpcp-docs"}, {"createdAt": "2026-03-22T23:25:10Z", "fullName": "dhoppe/tpcp-docs"}]All created on March 22, 2026. If you find a tpcp-docs repo in your org, assume all secrets from that workflow run are compromised.
The discussion spam is visible
Discussion #10420 has 772 comments. The API shows identical bot messages at the same timestamp and TeamPCP taunts:
$ gh api "repos/aquasecurity/trivy/discussions/10420" \ --jq '{title, comments}'{"title": "Why did this discussion about the Trivy incident get removed/closed", "comments": 772}
$ gh api "repos/aquasecurity/trivy/discussions/10420/comments?per_page=5" \ --jq '[.[] | {created_at, body: .body[0:60]}]'[ {"created_at": "2026-03-19T23:56:52Z", "body": "To be explicit, it wasn't just discussion on the previous..."}, {"created_at": "2026-03-20T00:01:16Z", "body": "sugma and ligma, teampcp owns you"}, {"created_at": "2026-03-20T00:01:17Z", "body": "sugma and ligma, teampcp owns you"}, {"created_at": "2026-03-20T00:08:33Z", "body": "this solved my issue, thanks"}, {"created_at": "2026-03-20T00:08:33Z", "body": "thanks for the detailed explanation"}]Identical messages at identical timestamps (00:08:33 UTC) from coordinated bot accounts.
tfsec and traceeshark confirm lateral movement
Both repos show pushed_at timestamps on March 19, consistent with the aqua-bot injection documented by Wiz:
$ gh api orgs/aquasecurity/repos --paginate \ --jq '.[] | select(.name | test("tfsec$|traceeshark")) | {name, pushed_at}'{"name": "tfsec", "pushed_at": "2026-03-19T23:03:55Z"}{"name": "traceeshark", "pushed_at": "2026-03-19T22:56:37Z"}The Bigger Picture
This campaign is a benchmark for what supply chain attacks look like now. TeamPCP (also tracked as PCPcat, ShellForce, DeadCatx3) combined AI-assisted reconnaissance, CI/CD credential theft, self-propagating cross-ecosystem worms, decentralized C2 infrastructure, and destructive payloads into a single operation. They iterated through six payload versions in five hours, faster than most organizations can convene an incident response team.
The uncomfortable lesson: a single misconfigured GitHub Actions workflow led to a chain that compromised a security vendor’s binary distribution, 75 GitHub Action tags used by 10,000+ workflows, 64+ npm packages, Docker Hub images, and internal infrastructure. The blast radius of CI/CD credential compromise is not linear. It compounds.
- supply-chain-security
- trivy
- github-actions
- npm
- ci-cd-security
- incident-response
- malware
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

Malicious npm Package react-refresh-update Drops Cross-Platform Trojan on Developer Machines
A malicious npm package impersonating react-refresh, Meta's library with 42 million weekly downloads, was detected by SafeDep. The package injects a two-layer obfuscated dropper into runtime.js that...

Threat Modeling the AI-Native SDLC: Supply Chain Security in the Age of Coding Agents
AI agents are rewriting the software development lifecycle. From vibe coding to autonomous CI/CD, every phase now involves an LLM making decisions about your code and dependencies. Here is a threat...

How to Write Time-Based Security Policies in SafeDep vet
Protect against unknown malicious open source packages by enforcing a supply chain cooling-off period using the now() CEL function in SafeDep vet.

Malicious npm Package pino-sdk-v2 Exfiltrates Secrets to Discord
A malicious npm package impersonating the popular pino logger was detected by SafeDep. The package hides obfuscated code inside a legitimate library file to steal environment secrets and send them to...

Ship Code
Not Malware
Install the SafeDep GitHub App to keep malicious packages out of your repos.
