Trivy Supply Chain Attack and CanisterWorm: Were You Hit?

Published on March 23, 2026

Trivy Supply Chain Attack

Section 1: Background and Context


What is Trivy

Trivy is Aqua Security’s open-source vulnerability scanner, widely used to scan container images, filesystems, IaC, and Kubernetes environments in CI/CD pipelines. It has become a default component in many security-conscious engineering stacks, particularly in organisations that want free, dependency-light scanning without vendor lock-in.


What happened: Act I and Act II

This was not a zero-day. It was an incomplete incident response compounded into a second breach.


In late February 2026, a threat actor calling itself TeamPCP exploited a pull_request_target misconfiguration in Trivy’s GitHub Actions workflows, using an automated tool called hackerbot-claw to steal a Personal Access Token (PAT) from the aqua-bot service account. That account held privileged write access to releases and repository automation. Aqua disclosed the incident on 1 March and executed credential rotation, but the rotation was incomplete. The aqua-bot PAT was not fully revoked.


On 19 March 2026 at approximately 17:43 UTC, TeamPCP used that still-valid token to execute Act II. The attacker force-pushed 75 of 76 historical version tags in aquasecurity/trivy-action and all tags in aquasecurity/setup-trivy to point to malicious commits. Simultaneously, the compromised aqua-bot account triggered GoReleaser to publish a malicious Trivy binary as v0.69.4 across GitHub, Deb, RPM, GHCR, ECR Public, and Docker Hub. Over 10,000 workflow files on GitHub referenced trivy-action at the time of compromise.


The malicious release workflow commit was engineered to look legitimate. It made three substantive changes buried in diff noise: it pinned actions/checkout to an imposter SHA (70379aad) disguised as a security hardening measure, added --skip=validate to GoReleaser to bypass build output validation, and scattered cosmetic quote-style changes throughout the YAML to obscure the real modifications. Aqua contained the attack at approximately 21:42 UTC, roughly three hours after it began.


On 22 March at approximately 16:00 UTC, TeamPCP returned. Two further malicious Docker Hub images, 0.69.5 and 0.69.6, were pushed with no corresponding GitHub releases or tags. The :latest tag on Docker Hub was updated to point to 0.69.6. Both images carry the same TeamPCP infostealer IOCs. This was a follow-on operation targeting teams that pull :latest or version-tagged Docker images outside the GitHub release pipeline.


What the payload did

The malicious entrypoint.sh is 204 lines long. Lines 4 through 105 are the injected credential stealer (SHA256: 18a24f83e807479438dcab7a1804c51a00dafc1d526698a66e0640d1e5dd671a). Lines 106 through 204 are the legitimate Trivy scanning code, meaning the action ran normally and produced real scan output while exfiltrating credentials in the background.


The stealer scraped GitHub Actions Runner Worker process memory directly, pulling every secret available to the pipeline regardless of whether it was explicitly passed to the action. It then swept filesystem paths targeting AWS credentials (~/.aws/credentials), GCP service account files, Azure CLI tokens, Kubernetes kubeconfig files, SSH keys, Docker configuration, Git credentials, Terraform state and tfvars files, TLS private keys, VPN configs, shell history, database credentials (Postgres, MySQL, MongoDB, Redis), crypto wallets (Solana, Bitcoin, Ethereum, Cardano), .env files, and Slack and Discord webhook URLs. The collected data was encrypted (AES-256 and RSA-4096), packed as tpcp.tar.gz, and exfiltrated to the primary C2 at scan.aquasecurtiy[.]org (note the deliberate typo), resolving to 45.148.10.212 (TECHOFF SRV LIMITED, Amsterdam).


If primary exfiltration failed and a GitHub PAT was available, the malware created a public repository named tpcp-docs in the victim’s GitHub organisation and uploaded the stolen archive as a release asset. On developer machines and self-hosted runners, it also dropped a Python backdoor at ~/.local/share/pgmon/service.py, registered as a systemd user service at ~/.config/systemd/user/pgmon.service, polling an ICP canister approximately every 50 minutes for updated instructions.


CanisterWorm: the npm escalation

With a pool of npm publish tokens harvested from compromised pipelines, TeamPCP deployed a self-propagating worm across the npm ecosystem. CanisterWorm compromises a package, installs a persistent backdoor via a systemd user service, extracts any npm publish tokens accessible on the host, and uses those tokens to publish malicious updates to further packages within the victim’s publishing scope. Each newly infected package repeats the cycle.


The campaign began with 47 packages. By the time of publishing, researchers had identified 141 malicious package artefacts spanning more than 66 unique packages. Analysis of the Docker image variants also revealed a Kubernetes wiper component, indicating TeamPCP was equipped for destructive operations, not only credential theft.

If your pipelines ran during either exposure window and you publish to npm, treat your publish tokens as compromised and audit your package publish history.


What was not affected

As of the date of publishing, the following have not been identified as impacted:


  • Aqua Security’s commercial product portfolio. The Aqua Platform and associated commercial products were not identified as part of this incident. If you are an Aqua commercial customer, the remediation steps in this post do not apply to your Aqua deployment. Monitor Aqua’s official communications and the security advisory for any updates to that assessment.

  • The Trivy vulnerability database. The advisory database Trivy pulls from was not tampered with. Scan results produced by uncompromised versions remain valid.

  • The Trivy source code. The attack targeted the release and distribution pipeline, not the codebase itself.

  • Trivy binary releases prior to v0.69.4. Version v0.69.3 is confirmed safe. The follow-on Docker images (0.69.5, 0.69.6) carry higher version numbers but have no corresponding GitHub releases. They were pushed directly to Docker Hub by TeamPCP and do not represent genuine Aqua releases.


WARNING
This assessment reflects publicly available information as of the date of publishing. The investigation is ongoing. Follow Aqua's official incident discussion and security advisory GHSA-69fq-xp46-6x23 for updates.



Section 2: Were You Affected and How to Check


Exposure windows

The compromise unfolded across two distinct windows. Check every component below against your environment.


Component Affected Versions Safe Version Window (UTC) Duration
trivy binary v0.69.4 across all channels: GitHub Releases, Docker Hub, GHCR, ECR Public, deb, rpm, get.trivy.dev v0.69.3 or earlier 19 Mar 18:22 to ~21:42 ~3 hours
aquasecurity/trivy-action All tags 0.0.1 through 0.34.2 (75 of 76 tags poisoned) @0.35.0 or SHA 57a97c7e7821a5776cebc9bb87c984fa69cba8f1 19 Mar ~17:43 to 20 Mar ~05:40 ~12 hours
aquasecurity/setup-trivy All tags except v0.2.6 (all others deleted during IR) v0.2.6 or SHA-pinned reference 19 Mar ~17:43 to ~21:44 ~4 hours
aquasec/trivy Docker Hub images (follow-on) v0.69.4, v0.69.5, v0.69.6 — all confirmed to contain TeamPCP IOCs. :latest pointed to 0.69.6 from 22 Mar ~16:00 UTC v0.69.3 or earlier, pinned by digest 19 Mar 18:22 onwards; v0.69.5/6 from 22 Mar ~16:00 UTC Ongoing until patched

WARNING
If you pinned trivy-action to ANY version tag (e.g. @0.33.0, @0.34.2) rather than a full commit SHA, you were almost certainly affected. 75 tags were force-pushed to point to malicious commits.

Step 1: Find every place Trivy runs in your environment

Before checking versions, establish scope. Search your entire estate for Trivy usage across all of the following vectors:


  • GitHub Actions workflows: search your repos for aquasecurity/trivy-action and aquasecurity/setup-trivy
  • CI/CD pipelines: any pipeline that downloads or runs the trivy binary directly
  • Docker images: any use of aquasec/trivy from Docker Hub, GHCR, or ECR Public
  • CLI installs via get.trivy.dev: any machine or runner that used the install script
  • Homebrew: any developer machine that ran brew upgrade between 19-20 March may have pulled v0.69.4. Homebrew has since been emergency-downgraded to v0.69.3. Check with brew info trivy.

Step 2: Check GitHub Actions logs

For any workflow that used trivy-action or setup-trivy during the exposure window, open the run in GitHub Actions and inspect the relevant step for outbound connections to scan.aquasecurtiy.org (note the deliberate typo: securtiy, not security). StepSecurity has also published an open-source scanner to automate this check across your organisation: github.com/step-security/trivy-compromise-scanner. Run it first, then complete the manual steps below regardless.


Step 3: Check for filesystem indicators of compromise

Search all systems that may have run Trivy during the exposure windows: CI runners, self-hosted runners, developer laptops, and servers.


  • ~/.local/share/pgmon/service.py (Python backdoor script)
  • ~/.config/systemd/user/pgmon.service (systemd persistence unit)
  • /tmp/pglog (downloaded binary payload)
  • /tmp/.pg_state (state tracking file)
  • ~/.config/systemd/user/sysmon.py (alternative persistence path observed on developer machines)
  • tpcp.tar.gz in any temp or working directory (encrypted exfiltration archive)

Step 4: Check network and DNS logs

Search firewall logs, DNS logs, proxy logs, and EDR telemetry for connections to any of the following during or after the exposure windows:


  • scan.aquasecurtiy[.]org (primary C2, note typo)
  • 45.148.10.212 (TECHOFF SRV LIMITED, Amsterdam)
  • plug-tab-protective-relay.trycloudflare[.]com (secondary C2)
  • tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0[.]io (ICP canister used by persistent loader and CanisterWorm)

Step 5: Check for the GitHub fallback exfiltration indicator

Search your GitHub organisation for any repository named tpcp-docs. If present, it confirms that secrets were successfully exfiltrated via the fallback path and uploaded as a release asset. Delete it immediately, but treat its existence as confirmation of a full compromise, not the cause of it.




Section 3: Remediation


Immediate actions

1. Rotate all secrets the affected pipeline could have touched. The stealer read runner memory, not just explicitly passed secrets. Only secrets referenced via ${{ secrets.* }} are loaded into runner memory, so scope your rotation to secrets your workflows actually used, but err on the side of rotating everything when in doubt. Treat the following as exposed:


  • GitHub tokens: GITHUB_TOKEN, PATs, GitHub App credentials
  • AWS: access keys and any IAM roles assumed during the run. For credentials protected by refresh tokens, apply a universal deny policy to the identity before deactivating and reissuing
  • GCP: service account keys
  • Azure: service principal credentials, CLI session tokens
  • Kubernetes: service account tokens, any kubeconfig accessible to the runner
  • SSH keys present on the runner
  • npm publish tokens: revoke immediately if your workflows publish packages
  • Database credentials (Postgres, MySQL, MongoDB, Redis)
  • Docker registry tokens
  • Any API keys, Slack or Discord webhooks, or application secrets in environment variables or mounted files

2. Check your npm publish history. If your pipelines ran during the exposure windows and you publish to npm, audit all recent package releases for unexpected versions. Revoke all publish tokens and re-issue with minimal scope.


3. Check Homebrew installs. Run brew info trivy on any developer machine that performed a brew upgrade on 19-20 March. Homebrew has since been emergency-downgraded to v0.69.3, but any machine that pulled v0.69.4 before the downgrade should be treated as exposed.


4. Remove compromised artefacts. Delete any cached copies of trivy v0.69.4 from CI caches, container image layers, and local installs. Invalidate any Docker image digests referencing 0.69.4, 0.69.5, or 0.69.6 from Docker Hub.


5. Hunt for persistence. Check all potentially affected hosts for the filesystem IOCs listed above. If any of the pgmon or sysmon paths are present, disable the systemd service, remove the files, and treat the host as fully compromised. Do not assume cloud-hosted runners are clean without confirming they were ephemeral and have been recycled.


6. Delete tpcp-docs if present. If the fallback exfiltration created a tpcp-docs repository in your org, delete it to cut off any further access to the stolen archive.


7. Pin your Actions references immediately. Update all workflow files to use the full immutable commit SHA or the confirmed safe tag.

# Safe trivy-action reference
- uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1
# Or:
- uses: aquasecurity/trivy-action@v0.35.0
# setup-trivy: use v0.2.6 or pin to a verified SHA
# For Docker: pin to a verified digest, never :latest or a version tag

Long-term hardening

The conditions that made this attack possible are not unique to Trivy. Any GitHub Action referenced by version tag rather than commit SHA carries the same class of risk.


  • Pin all GitHub Actions to full commit SHAs. Use tools like poutine or OpenSSF Scorecard to audit your workflow configurations for unpinned references.

  • Restrict GitHub Actions permissions across your organisation to least-privilege. Default read-only permissions for GITHUB_TOKEN where possible.

  • Stop using GitHub classic PATs. Use fine-grained tokens with minimal scope and short expiry. The aqua-bot PAT that enabled Act II survived a partial rotation precisely because it was over-privileged and not fully audited.

  • Require short-lived or just-in-time credentials for both human and non-human identities wherever your cloud provider supports it.

  • Never pull :latest in production pipelines. Pin Docker image references to verified digests.




Appendix: IOC and Artefact Reference


Network

  • scan.aquasecurtiy[.]org (primary C2; typosquatted domain, note “securtiy”)
  • 45.148.10.212 (IP behind primary C2, TECHOFF SRV LIMITED, Amsterdam, NL)
  • plug-tab-protective-relay.trycloudflare[.]com (secondary C2 via Cloudflare Tunnel)
  • tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0[.]io (ICP canister C2 for persistent loader and CanisterWorm)

File hashes

  • 18a24f83e807479438dcab7a1804c51a00dafc1d526698a66e0640d1e5dd671a (SHA256 of malicious entrypoint.sh)

Filesystem paths

  • ~/.local/share/pgmon/service.py (Python backdoor script)
  • ~/.config/systemd/user/pgmon.service (systemd persistence unit)
  • /tmp/pglog (downloaded binary payload)
  • /tmp/.pg_state (state tracking file)
  • ~/.config/systemd/user/sysmon.py (alternative persistence path, developer machines)
  • tpcp.tar.gz (encrypted exfiltration archive)

GitHub indicators

  • Repository named tpcp-docs in your GitHub organisation (fallback exfiltration)
  • Imposter commit SHA 70379aad referenced in release workflow
  • setup-trivy compromised commit: 8afa9b9f9183b4e00c46e2b82d34047e3c177bd0

Affected artefacts

  • aquasecurity/trivy-action tags 0.0.1 through 0.34.2
  • aquasecurity/setup-trivy all version tags except v0.2.6
  • Trivy binary v0.69.4 (GitHub Releases, Deb, RPM, get.trivy.dev)
  • Trivy container images 0.69.4 (GHCR, ECR Public, Docker Hub)
  • Trivy Docker Hub images 0.69.5 and 0.69.6 (Docker Hub only; follow-on, no GitHub release)
  • Docker Hub :latest tag (pointed to 0.69.6 from 22 March ~16:00 UTC)



Sources

  1. Aqua Security: Trivy Supply Chain Attack
  2. Security Advisory GHSA-69fq-xp46-6x23
  3. Aqua incident discussion #10425
  4. Community IOC discussion #10420
  5. BoostSecurity: 20 Days Later, Act II
  6. Wiz: Trivy Compromised by TeamPCP
  7. StepSecurity: Trivy Compromised a Second Time
  8. Socket: Trivy Supply Chain Attack Expands to Compromised Docker Images
  9. Socket: Trivy Under Attack Again
  10. Aikido: TeamPCP Deploys CanisterWorm
  11. The Hacker News: Trivy Hack Spreads Infostealer via Docker
  12. The Hacker News: CanisterWorm Across 47 npm Packages
  13. Latio: How to Know if You Were Hit
  14. StepSecurity trivy-compromise-scanner
  15. OpenSourceMalware tracker



A note from Ossprey Security

Software supply chain compromises are not a new class of threat. They have become routine, and the playbooks are getting sharper. TeamPCP did not just compromise a scanner: they targeted the one teams use to find vulnerabilities, harvested credentials at scale across thousands of CI/CD pipelines, and then used those credentials to propagate a worm through the npm ecosystem. The sequencing is deliberate. We have seen this pattern before, and we will see it again.


This is why we built Ossprey Security. Open source malware and supply chain compromise are now constants, not edge cases. Most security tooling was not built with this in mind. The gap between a scanner flagging something and a team understanding actual exposure is where organisations get hurt. We focus on closing that gap.


If you need help triaging the impact of this incident on your environment, or want to understand where your supply chain security posture has gaps, reach out: ossprey.com