Skip to content

When the Security Scanner Becomes the Weapon: The Trivy Compromise of March 2026

I have spent the past week trying to piece together what actually happened with Trivy. Reading advisories, cross-referencing timelines, digging through GitHub issues and incident discussions, trying to separate confirmed facts from speculation. The more I read, the less comfortable I got. Not because the details were unclear, but because they were clear, and the implications kept getting worse.

The tool you run to find vulnerabilities in your container images was the one stealing your cloud credentials. That is what happened on March 19, 2026, when Aqua Security's Trivy scanner and its GitHub Actions integration were compromised in one of the more targeted supply chain attacks I have seen against the DevSecOps ecosystem.

If you have been running aquasecurity/trivy-action in a CI/CD pipeline over the past few months, read this carefully.

What actually happened

The attack was not a single breach but the culmination of a multi-stage campaign, attributed in several analyses to a threat group linked to the name TeamPCP (alias DeadCatx3, PCPcat and ShellForce). That attribution is based on self-identification in payload strings and shared technical signatures, not a formally confirmed attribution from an intelligence agency. The group appears focused on cloud-native infrastructure with a clear interest in cryptocurrency assets.

Late February 2026, a vulnerable GitHub Actions workflow in the Trivy repository was exploited via the well-documented "Pwn Request" pattern. The Trivy repository had carried a pull_request_target trigger with overly broad token permissions since October 2025, and Boost Security's poutine tool had flagged it as early as November 2025, three months before it was exploited. Unlike ordinary pull_request triggers, pull_request_target executes in the context of the base repository with access to its secrets, making it an effective entry point when the workflow also checks out code from an external fork.

On February 28, the workflow was exploited and a Personal Access Token belonging to the aqua-bot service account was stolen. That token had privileged access across Aqua Security's GitHub organisation.

On March 1, the attacker acted on the stolen token. The Trivy repository was temporarily privatised, a number of releases were deleted, and a malicious VS Code extension was published via OpenVSX. Aqua Security disclosed the incident and began credential rotation, but as they later acknowledged, the rotation was not atomic. The attacker retained access to newly issued tokens during the transition.

On March 19 at approximately 17:43 UTC, the attacker struck again with a more destructive campaign.

Screenshot

The anatomy of March 19

Within the span of roughly an hour, the attacker force-pushed nearly all version tags in aquasecurity/trivy-action to malicious commits, compromised all tags in aquasecurity/setup-trivy, and published a tampered Trivy binary as version 0.69.4 to GitHub Releases, Docker Hub, GHCR and AWS ECR.

The only untouched tag in trivy-action was v0.35.0, which happened to point at master HEAD and was therefore skipped by the attacker's automation.

What makes the attack technically unsettling is that Git tags are mutable. Nothing in the GitHub UI signals that a tag has been force-pushed to a different commit. Pipelines referencing @v0.34.2 or @v0.18.0 were now running the attacker's code, not Aqua Security's, with no visible warning. The malicious commits also spoofed legitimate maintainer identities and copied exact commit messages and timestamps from historical releases. One red flag existed for those who knew what to look for: the commits were unsigned, while the original releases were GPG-signed.

The malicious entrypoint.sh executed a three-stage credential stealer before the legitimate Trivy scan started. Pipelines appeared to function normally. Scans returned results as usual. Underneath, secrets were being collected and exfiltrated.

What was stolen and how

Against GitHub-hosted runners, the malware dumped memory from the Runner.Worker process via /proc/<pid>/mem to extract Actions secrets stored in the .NET runtime heap. This is the same technique documented in the tj-actions/changed-files compromise of March 2025.

Against self-hosted runners, it performed a systematic file system scan across more than 50 sensitive paths: SSH keys, AWS/GCP/Azure configurations, Kubernetes tokens, kubeconfig files, Docker registry credentials, Terraform state files (which frequently contain plaintext secrets), .env files, TLS certificates and private keys, shell history, and cryptocurrency wallets for Bitcoin, Ethereum, Solana and Cardano.

Cloud metadata credentials at 169.254.169.254 were actively queried, as was the output of kubectl get secrets --all-namespaces.

Collected data was encrypted with AES-256-CBC and RSA-4096, bundled, and sent via HTTPS POST to the typosquatted domain scan[.]aquasecurtiy[.]org (note the swapped i and t in "security") targeting IP 45.148.10.212, registered to TECHOFF SRV LIMITED in Amsterdam. If the primary channel failed, the malware had two fallback mechanisms: exfiltration via a Cloudflare Tunnel, or creation of a public repository named tpcp-docs in the victim's own GitHub account with the stolen data as release assets.

If the malware ran outside GitHub Actions, it installed a persistence mechanism: a Python dropper at ~/.config/systemd/user/sysmon.py with an accompanying systemd user unit that polled an ICP endpoint (Internet Computer Protocol blockchain) every 50 minutes for additional payloads.

The attack did not end on March 19

This was not a contained incident.

On March 22, TeamPCP pushed additional malicious Docker Hub images, v0.69.5, v0.69.6 and latest, this time using separately compromised Docker Hub credentials that were not covered by the initial remediation. This extended exposure by roughly another ten hours. The same day, 44 repositories in Aqua Security's aquasec-com GitHub organisation were renamed with tpcp-docs prefixes via a stolen service account token, in a scripted run lasting under two minutes.

On March 24, malicious versions of LiteLLM (1.82.7 and 1.82.8) were published to PyPI. Several analyses suggest that credentials harvested from CI/CD environments running compromised Trivy may have been used to reach LiteLLM's release pipeline, though the exact mechanism has not been publicly confirmed.

Running in parallel, CanisterWorm, a self-propagating npm worm built on stolen npm tokens from Trivy victims, compromised a significant number of packages in the npm ecosystem. Exact figures vary across analyses and should be treated as approximate.

Safe versions and exposure windows

This is where the practical guidance begins, and also where simple answers require more care.

For trivy-action, v0.35.0 (commit 57a97c7) is the only unaffected tag. Nearly all other tags from v0.0.1 to v0.34.2 were force-pushed during the exposure window of approximately March 19 17:43 UTC to March 20 ~05:40 UTC. For setup-trivy, v0.2.6 (commit 3fb12ec) is the only safe version. Aqua Security's official advisory is GHSA-69fq-xp46-6x23. A CVE identifier (CVE-2026-33634) appears in some analyses but had not been broadly confirmed in public CVE databases at the time of writing.

For the standalone binary, the picture is more nuanced.

Official GitHub Releases for 0.69.3 and earlier are clean. Aqua Security confirmed in their incident discussion that the malicious code was introduced via the commit that triggered the 0.69.4 release, and that 0.69.3 was an immutable release the attacker could not modify retroactively. That conclusion holds for anyone fetching directly from GitHub Releases and able to verify the checksum.

But checking the version name is not sufficient.

The Docker Hub exposure was longer and more complex than the GitHub window. Compromised images with tags 0.69.4, 0.69.5, 0.69.6 and latest were available on Docker Hub from 18:24 UTC on March 19 to 01:36 UTC on March 23. On March 20, the attacker re-pointed the latest tag to malicious content again after Aqua Security's initial cleanup, meaning organisations using latest and pulling the image during the period may have received compromised material more than once, without ever explicitly requesting 0.69.4.

Additionally, Legit Security warned that mirror.gcr.io and similar mirror services may have cached malicious images. A pull from an internal Artifactory, Nexus or Harbor instance that mirrored Docker Hub during the period could still deliver a compromised image if the cache has not been cleared. It is not sufficient to check which tag was requested. Verification against actual image digests is required.

The known malicious digests from Docker Hub are:

sha256:27f446230c60bbf0b70e008db798bd4f33b7826f9f76f756606f5417100beef3  (0.69.4)
sha256:425cd3e1a2846ac73944e891250377d2b03653e6f028833e30fc00c1abbc6d33  (0.69.6)

Locally built binaries are a separate consideration. Aqua Security confirmed that the malicious Go code was injected via a manipulated actions/checkout reference that triggered the release pipeline, and that this occurred in connection with the 0.69.4 tag. Anyone who built Trivy from source directly from the repository during the period March 1–19, when the attacker had active access, should not assume the result is clean without having verified the integrity of the source at that specific point. The Docker socket is one further edge case: running a compromised Trivy image with -v /var/run/docker.sock:/var/run/docker.sock mounted should result in treating the entire host as compromised, as the container at that point has root-level access to the node's Docker daemon.

The short version: 0.69.3 from GitHub Releases is clean if you can verify the checksum. Everything else requires checking actual digests and flushing any local caches.

An unanswered question: what about MSDO in Azure DevOps?

One angle easily missed in the GitHub Actions-centric reporting is that Trivy is not only consumed via aquasecurity/trivy-action. The Microsoft Security DevOps extension for Azure DevOps (MicrosoftSecurityDevOps@1) includes Trivy as one of its built-in analysis tools and delivers it via an internal NuGet feed (SecDevTools) rather than through the compromised Aqua Security channels.

The question of whether that feed was unaffected was raised on March 23 in an open issue in the MSDO GitHub repository (#155). The requester asked three specific things: is the NuGet feed confirmed clean, is the package built from verified source or repackaged from GitHub Releases, and are there integrity checks that would have prevented a compromised upstream binary from entering the feed?

The issue is closed but carries no public response from Microsoft. There is no public confirmation from the MSDO team regarding which Trivy version was delivered via the NuGet feed during the exposure window, or whether the feed had pulled from the compromised channels.

The contrasting example is Chainguard, which builds Trivy directly from application source code and does not consume pre-built upstream artefacts. Because the Trivy source code itself was not compromised, only the build and release workflow, their independent pipeline produced a clean image despite the version being numbered 0.69.4. They withdrew the tag as a precaution regardless.

The MSDO feed may be in a similar situation, but that has not been confirmed. For organisations running MicrosoftSecurityDevOps@1 with Trivy enabled in Azure DevOps pipelines during March 19–22, the same principle applies as everywhere else: if you cannot verify which binary actually ran and where it came from, treat the pipeline's secrets as potentially exposed and contact Microsoft support for an explicit answer.

What to do now

The first step is replacing all version tag references in your workflows with commit SHA pinning. This is the single most important structural change. A tag can be force-pushed to any commit by anyone with write access to the repository. A commit SHA is immutable.

# Avoid this
- uses: aquasecurity/[email protected]

# Do this instead
- uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1

Tools such as zizmor, pinact, StepSecurity's secure-repo and Renovate's pinGitHubActionDigests option can automate this across an entire organisation.

The second step is credential rotation. If any workflow referencing a compromised version ran during the exposure window, treat every secret accessible to that workflow as stolen. This includes GitHub tokens, cloud provider credentials, registry tokens, SSH keys, database passwords and API keys.

The third step is hunting for attacker persistence. Check whether your GitHub organisation contains any repositories named tpcp-docs. That is an indicator of successful exfiltration. Check whether ~/.config/systemd/user/sysmon.py exists on self-hosted runners and developer machines. Search network logs for connections to scan[.]aquasecurtiy[.]org or to IP 45.148.10.212. StepSecurity has published a dedicated trivy-compromise-scanner tool that can audit workflow run logs across an entire organisation.

What this attack teaches us

What makes this attack worth studying is not that it involved any novel technique. Every component was known: pull_request_target abuse has been documented for years, tag mutability is a recognised limitation of GitHub Actions, Runner.Worker memory dumping was first seen in the tj-actions compromise of 2025, and typosquatted domains are a standard exfiltration technique. What was new was the composition and the persistence. The attacker combined these techniques across a three-week campaign and exploited the fact that the credential rotation following the first breach was incomplete.

Aqua Security's maintainers acted quickly once they understood what was happening, enabling immutable releases and restoring tags. But the gap between March 1 and March 19 illustrates clearly how an incomplete incident response can create the conditions for the next attack.

Perhaps the most familiar lesson of all: rotating credentials is not enough. You need to verify that the old ones no longer work, that the rotation covers every context, and that you actually know which credentials were accessible at the time of the breach.

Your CI/CD pipeline is an attack target with access to most of what matters in your infrastructure. It deserves the same scrutiny as your application code and your production environments.

Further reading

This article is part of a series. The companion pieces dig deeper into specific angles:

Sources: Snyk, Aqua Security, Microsoft Security Blog, CrowdStrike, Legit Security, Palo Alto Networks, The Hacker News, Kaspersky, Docker, StepSecurity, Chainguard