Trivy Supply Chain Compromise: What Happened, What Was Stolen, and How to Respond

SafeDep Team
10 min read

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:true in 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:

  1. Collection: On GitHub-hosted runners, the malware escalated to root via passwordless sudo, located Runner.Worker processes, 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.

  2. 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.

  3. Exfiltration: Primary channel was HTTPS POST to scan.aquasecurtiy[.]org (typosquat of aquasecurity.org, resolving to 45.148.10.212). Fallback: if INPUT_GITHUB_PAT was available, the malware created a public tpcp-docs repository 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)VersionCapability
~11:45v1Monolithic K8s DaemonSet with host escape, systemd persistence, Iran-targeted wiper
12:45v2Modular loader fetching kube.py, C2 rotated to Cloudflare tunnel
13:00v3Pure SSH/Docker worm (scans local /24 for ports 22, 2375)
13:25v3.1Module split: kube.py (broken K8s) + prop.py (SSH/Docker worm)
14:56v3.2Working ICP backdoor, kubectl re-enabled, new Cloudflare tunnel
16:15v3.3WAV 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

IndicatorRole
scan.aquasecurtiy[.]org / 45.148.10.212Primary C2 (TECHOFF SRV LIMITED, Amsterdam)
tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0[.]ioICP canister dead drop (denylisted 03/22)
plug-tab-protective-relay.trycloudflare[.]comCloudflare tunnel (credential exfil)
investigation-launches-hearings-copying.trycloudflare[.]comCloudflare tunnel (kamikaze v2)
championships-peoples-point-cassette.trycloudflare[.]comCloudflare tunnel (v3/v3.1)
create-sensitivity-grad-sequence.trycloudflare[.]comCloudflare tunnel (v3.2/v3.3)
recv.hackmoltrepeat[.]comPhase 1 PAT exfiltration

Affected Artifacts

ComponentAffected VersionsExposure
Trivy binaryv0.69.4 (all registries), Docker Hub v0.69.5/v0.69.6~3 hours
trivy-actionTags 0.0.1-0.34.2 (75 tags)~12 hours
setup-trivyTags v0.2.0-v0.2.5~4 hours
npm (@emilgroup, @opengov, others)135+ artifacts, 64+ packagesVaries

Malicious Container Digests (v0.69.4)

Per Aqua’s advisory:

ArchitectureDigest
Genericsha256:27f446230c60bbf0b70e008db798bd4f33b7826f9f76f756606f5417100beef3
amd64sha256:12c702212dee1cbec9471e9261501a3335963321fe76e60e5a715b5acd3c40a2
arm64sha256:2d7cee41048988eec27615412e7c6e2e21046f2b5faa888c24e11ca6764058ed

Filesystem Indicators

PathComponent
~/.config/systemd/user/sysmon.pyTrivy developer machine backdoor
~/.config/systemd/user/pgmon.serviceCanisterWorm persistence
~/.local/share/pgmon/service.pyCanisterWorm Python backdoor
/tmp/pglogPayload staging
/tmp/.pg_stateState tracking

Hunt Queries

  • GitHub: Search your organizations for repositories named tpcp-docs or matching tpcp-docs-*. Their existence indicates successful credential exfiltration via the dead drop fallback.
  • GitHub Actions: Review workflow runs from March 19-20 referencing aquasecurity/trivy-action or aquasecurity/setup-trivy. Check the “Run Trivy” and “Setup environment” steps for anomalous output.
  • Kubernetes: Search for DaemonSets named host-provisioner-std or host-provisioner-iran in kube-system namespace.
  • systemd: Check for user services matching sysmon, pgmon, pgmonitor, or internal-monitor.
  • npm: Audit package versions in @emilgroup, @opengov, @teale.io, @airtm, @pypestream scopes against expected versions.

What to Do Now

If you used Trivy between March 19-22, 2026:

  1. 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.
  2. Rotate every secret accessible to affected workflows: GitHub tokens, cloud provider credentials, Docker registry tokens, SSH keys.
  3. Block the C2 domains and IP at your network perimeter.
  4. 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_target workflows 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:

Terminal window
$ gh api repos/aquasecurity/trivy/releases --paginate \
--jq '.[].tag_name' | sort -V | tail -20
# ...
v0.25.4
v0.26.0
v0.69.2 # ← gap: 43 minor versions missing
v0.69.3

The malicious v0.69.4 tag has been deleted:

Terminal window
$ 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.

Terminal window
$ 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",
"email": "[email protected]",
"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):

Terminal window
$ 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:

Terminal window
$ gh api repos/aquasecurity/setup-trivy/tags --jq '.[].name'
v0.2.6

The setup-trivy events API captures the incident response in real-time:

Terminal window
$ 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 release

trivy-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:

Terminal window
$ gh api repos/aquasecurity/trivy-action/tags --paginate \
--jq '.[].name' | wc -l
74
$ gh api repos/aquasecurity/trivy-action/tags --paginate \
--jq '.[].name' | sort -V | tail -5
v0.33.0
v0.33.1
v0.34.0
v0.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:

Terminal window
$ 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:

Terminal window
$ 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:

Terminal window
$ 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 Logo

SafeDep Team

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

Background
SafeDep Logo

Ship Code

Not Malware

Install the SafeDep GitHub App to keep malicious packages out of your repos.

GitHub Install GitHub App