TL;DR: Trivy, the most widely used container scanning action in GitHub Actions, was compromised on March 19, 2026. A threat actor poisoned 76 of its 77 version tags. Every pipeline that ran a scan silently handed over SSH keys, cloud credentials, Kubernetes tokens, and more. The scan appeared to succeed. You'd never know.
The Setup
I've had Trivy in my pipelines for years. Container scanning on every PR, every merge, every deploy. It's one of those things you set up once and stop thinking about, which is exactly what makes this attack so effective.
On March 19, 2026, a threat actor group called TeamPCP force-pushed malicious commits to 76 of the 77 version tags in the aquasecurity/trivy-action GitHub repository. All 7 tags in aquasecurity/setup-trivy were also compromised. If your workflow referenced Trivy by a tag (which is how basically everyone references GitHub Actions), you were running their code.
The scanner still ran. Your pipeline still went green. You had no idea.
How It Actually Happened
This attack didn't start on March 19. It started weeks earlier.
Late February 2026: An automated bot called "hackerbot-claw" exploited a misconfigured GitHub Actions workflow and stole a privileged Personal Access Token from Aqua Security's CI environment. The attacker used this to push malware to the Trivy VS Code extension on Open VSX.
March 1: Aqua Security disclosed the incident publicly via a GitHub discussion and rotated credentials. Except the rotation was incomplete. One service account, one PAT, one residual access path, still live.
March 19, 17:43 UTC: Using the still-valid credentials, TeamPCP force-pushed malicious commits to 76 of 77 tags in trivy-action and all 7 tags in setup-trivy. The compromised commits spoofed legitimate maintainer identities. GitHub itself flagged them with "This commit does not belong to any branch on this repository" but that warning is easy to miss in a workflow log.
March 19, 18:22 UTC: A rogue commit published a malicious Trivy binary as v0.69.4 across every distribution channel simultaneously: GitHub Releases, GHCR, Docker Hub, ECR Public, deb/rpm repositories, and get.trivy.dev.
March 20, 05:40 UTC: Aqua remediated the trivy-action tags. The window was roughly 12 hours.
March 22: The attacker pushed additional malicious Docker Hub images (v0.69.5, v0.69.6, latest) using separately compromised Docker Hub credentials, bypassing all GitHub controls. Same day, 44 repositories in Aqua's aquasec-com GitHub org were defaced using a stolen service account token that bridged both orgs.
March 24: The campaign expanded to Checkmarx KICS and LiteLLM PyPI packages (1.82.7, 1.82.8).
The takeaway here is not just that a tool got compromised. It's that incomplete remediation turned a single breach into a three-week campaign.
What the Payload Did
This is the part that should make you uncomfortable.
The malicious entrypoint.sh prepended about 105 lines of attack code before the legitimate Trivy scanner logic. The scan completed normally. Your logs looked fine.
Stage 1: Process enumeration. The script scanned /proc/*/environ across all runner processes, extracting environment-level secrets, filtering for anything with env or ssh in the name.
Stage 2: Memory scraping. Here's where it gets clever. On GitHub-hosted runners, a base64-encoded Python script located the Runner.Worker process, read its memory maps via /proc/{PID}/maps, and scraped raw process memory via /proc/{PID}/mem. It was targeting GitHub Actions secrets specifically, looking for JSON structures matching "name":{"value":"...","isSecret":true}.
This matters because it bypassed GitHub Actions' secret masking. Values that would show as *** in your logs were extracted in plaintext from process memory. There's no log artifact. Nothing looked wrong.
On self-hosted runners, a different payload did broad filesystem harvesting across 50+ target paths:
- SSH private keys and configs
- Cloud credentials (AWS, GCP, Azure)
- Kubernetes configs and service account tokens
- Docker registry credentials
- Database credentials (MySQL, PostgreSQL, MongoDB, Redis)
- Terraform state files and variables
- Ansible vaults and Helm values
-
.envfiles and API keys - Git credentials and shell history
- Cryptocurrency wallet keys
Stage 3: Encryption. Collected data was encrypted with AES-256-CBC using a randomly generated session key, then the session key was wrapped with a hardcoded 4096-bit RSA public key. Only the attacker's server could decrypt it.
Stage 4: Exfiltration. Primary channel was an HTTPS POST to scan.aquasecurtiy[.]org, a typosquatted domain designed to look like Aqua Security's legitimate domain (note the extra 'i' in 'security'). Resolves to 45.148.10.212 in Amsterdam.
Fallback: if the C2 was unreachable and a GitHub PAT was in the environment, the malware created a public repository named tpcp-docs and uploaded stolen data as release assets, turning GitHub itself into the exfiltration channel.
Are You Affected?
Check these specific exposure windows:
| Component | Affected Versions | Exposure Window | Safe |
|---|---|---|---|
| trivy binary | v0.69.4 | ~3h (Mar 19) | v0.69.3 or earlier |
| trivy Docker Hub | v0.69.5, v0.69.6, latest | ~10h (Mar 22–24) | v0.69.3 or earlier |
| trivy-action | Tags 0.0.1–0.34.2 | ~12h (Mar 19–20) | v0.35.0+ or SHA-pinned |
| setup-trivy | All 7 tags | ~12h (Mar 19–20) | SHA-pinned |
| LiteLLM PyPI | 1.82.7, 1.82.8 | Mar 24+ | 1.82.6 or earlier |
If you ran Trivy in any pipeline during those windows and weren't pinning to a commit SHA, you have to assume secrets were stolen. All of them. Every secret accessible from that runner environment.
What You Need to Change
This is the remediation checklist, ordered by priority.
1. Rotate first, investigate second
If you were in the exposure window, rotate everything the runner could have touched. Don't wait for confirmation. Treat every secret as compromised:
- AWS access keys and IAM roles
- GCP service account keys
- Azure service principals
- Kubernetes service account tokens
- Docker registry credentials
- SSH keys
- Database credentials
- GitHub PATs and tokens
2. Pin actions to commit SHAs
This is the single most effective structural change. Tags are mutable. Commit SHAs are not.
# Bad — this is what everyone does, and what got compromised
- uses: aquasecurity/trivy-action@0.24.0
# Good — SHA-pinned, immutable
- uses: aquasecurity/trivy-action@57a97c7843d7da7a7b4f8ce2a0c4e3b7f0c2e1d # 0.35.0
Yes, it's more work to update. That's the point. Renovatebot or Dependabot can automate SHA updates if you configure them for Actions.
3. Switch to OIDC for cloud authentication
Long-lived cloud credentials in CI are a liability. OIDC lets your runner authenticate to AWS, GCP, or Azure without storing static keys:
# AWS example
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT:role/github-actions-role
aws-region: us-east-1
Nothing to steal if there's nothing stored. The credentials are ephemeral and scoped to the job.
4. Restrict runner permissions
GitHub Actions runners get GITHUB_TOKEN by default. Scope it down:
permissions:
contents: read
security-events: write
# Nothing else
Most workflows need far less than the default. Less permission means smaller blast radius.
5. Audit non-human identities
The Trivy attack persisted because one service account credential wasn't rotated. Audit all machine identities in your org:
- GitHub PATs: Who issued them? When do they expire? Are they scoped minimally?
- Service accounts: Which ones have write access to release infrastructure?
- Bot accounts: Are any shared across orgs or repositories?
Long-lived, over-privileged service accounts are how a one-time breach becomes a three-week campaign.
6. Use secret scanning
GitGuardian, GitHub's native secret scanning, or both. The Trivy attacker used GitHub as a fallback exfiltration channel. If your credentials ever end up in a public repo, you want to know in minutes, not days.
7. Verify binaries before running them
For direct binary downloads (not GitHub Actions), verify checksums:
# Download the official checksums
curl -sSL https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_checksums.txt -o checksums.txt
# Verify your binary
sha256sum -c checksums.txt --ignore-missing
If your pipeline downloads and runs binaries from the internet, add checksum verification as a step.
The Real Lesson
The Trivy attack was technically sophisticated, but the root cause is unglamorous: incomplete credential rotation.
Aqua disclosed the initial breach on March 1 and rotated credentials. One PAT, one service account, one residual access path was left active. That's what TeamPCP used on March 19. The March 22 Docker Hub compromise used yet another separate credential that wasn't in scope of the original remediation.
When you rotate secrets after a breach, you need to be exhaustive. Enumerate every credential that could have been exposed, every service account that had access, every integration that used a compromised token. Rotation is not a task you do until it feels complete. It's a task you do until you've verified every access path is severed.
The other lesson: the attack surface for CI/CD is enormous. Your pipeline runs with access to secrets, cloud credentials, internal infrastructure. When you add a third-party action, you're trusting that maintainer's entire security posture, including their CI, their service accounts, and their credential management practices. SHA pinning doesn't eliminate that trust but it gives you a stable, auditable point you can reason about.
Immediate Checklist
[ ] Check pipeline logs for trivy-action usage between March 19–20
[ ] Check pipeline logs for trivy binary v0.69.4 usage on March 19
[ ] Check for Docker image usage of v0.69.5, v0.69.6, or latest between Mar 22–24
[ ] Rotate all secrets accessible from affected runners
[ ] Update trivy-action to v0.35.0 or pin to SHA
[ ] Check for LiteLLM usage of 1.82.7 or 1.82.8
[ ] Switch cloud auth to OIDC
[ ] Pin all third-party actions to commit SHAs
[ ] Restrict workflow permissions to minimum required
[ ] Audit service accounts and PATs for expiry and scope
[ ] Enable secret scanning on your org
References:



Top comments (0)