In my previous post on the Trivy affair, I wrote: "As long as Aqua Security has not published a complete and transparent analysis of the incident, I cannot recommend this tool in good conscience." Aqua has finally published that analysis. It was updated five times between 22 March and 1 April 2026, with the support of the forensic investigation firm Sygnia and contributions from Wiz Research, Socket Security, Aikido Security and CrowdStrike. The result is a dense, technically rich document, but one that deserves a critical reading. Here is what it reveals, what it confirms, and what it does not say.
How slow was Aqua's communication really?
The useful level of detail arrived thirteen days after the 19 March attack, even though the first advisories came out quickly. Let us first put the dates in perspective. The affair does not start on 19 March. In late February 2026, the attacker exploited a misconfiguration in Trivy's GitHub Actions environment and exfiltrated a privileged access token. On 1 March, the Trivy team publicly disclosed this initial incident and launched a credential rotation. That is the subject of my first post. But the rotation was incomplete, as we would learn later.
On 19 March, the attacker reused the residual access. He published the
poisoned release v0.69.4 and rewrote almost all the tags of trivy-action.
The same evening, the Trivy team contained the attack by removing the
malicious artifacts.
On 20 March, the safe versions and the first IOCs were published. On 21 March, the GHSA GHSA-69fq-xp46-6x23 was published. On 23 March, CVE-2026-33634 arrived on the NVD side. Up to that point, the reaction was reasonably fast.
Then the pace slowed. On 22 March, two things happened: Aqua published
the first blog post, and the attacker struck again, Docker Hub images
0.69.5 and 0.69.6, internal repositories made public. The initial
containment did not hold.
The updates then followed one another: 23 March (Sygnia engaged, Docker Hub),
24 March (enterprise isolation, kics-github-action compromised), 25 March
(remediation and documentation phase), then 1 April with the complete
technical analysis.
| Date | Event |
|---|---|
| Late February | Exfiltration of the privileged access token |
| 1 March | Public disclosure of the initial incident, credential rotation |
| 19 March ~17:43 UTC | Release v0.69.4 + poisoned trivy-action tags |
| 19 March ~20:38 UTC | Containment, malicious artifacts removed |
| 20 March | Safe versions + IOCs published |
| 21 March | Publication of GHSA-69fq-xp46-6x23 |
| 22 March ~16:00 UTC | Docker Hub images 0.69.5/0.69.6 compromised |
| 22 March ~21:40 UTC | Internal repositories made public |
| 22 March | First Aqua blog post |
| 23 March | CVE-2026-33634 received by the NVD, Sygnia engaged |
| 24 March | Enterprise isolation, kics-github-action compromise |
| 25 March | Remediation/documentation phase |
| 1 April | Complete technical analysis, binary IOCs, attribution |
Three observations. First, the GHSA came on 21 March, then the CVE on 23 March, so this was not complete silence. But for a tool executed in thousands of pipelines, the actionable level of detail arrived much later: the substantial technical details only appear in the Aqua post enriched up to 1 April, thirteen days after the 19 March attack. Second, the post was enriched in successive waves, each update adding content that external researchers had already published. You can recognise the contributions of Wiz and Socket, flagged by asterisks in Aqua's text.
The transparency is real. The pace, however, is debatable.
What does Aqua's technical analysis reveal that is new?
Once complete, Aqua's post provides details that my first three articles did not cover, or only partially. Let us go through the content.
The tag poisoning was not a crude rewrite
What BoostSecurity had identified as a tag rewrite, Aqua (with Socket's
data) now details as a methodical forensic falsification. The GHSA speaks of
76 tags out of 77 rewritten on trivy-action (the Socket catalogue
counts 75, the numbering varies between sources). For each of these tags, the
attacker:
- Took the tree of the
HEADofmaster(57a97c7e). - Replaced only
entrypoint.shwith the infostealer payload. - Retrieved the original commit the tag pointed to.
- Cloned that commit's metadata: author, email, committer, both timestamps, the full message with the PR number and the "Fixes" references.
- Set the parent to
57a97c7einstead of the original parent. - Force-pushed the tag to this forged commit.
The result: a git log that displays exactly the expected metadata for every
version. Only a few forensic clues betray the falsification:
- The original commits were GPG-signed by GitHub during the merge through the web interface. The attacker's commits are not.
- Each forged commit claims to date from 2021 or 2022, but has a parent from March 2026, an impossible lineage.
- Each malicious commit modifies only
entrypoint.sh, whereas the originals touched several files.
This level of sophistication explains why detection was so hard. A pipeline auditing its Actions by looking at commit messages and dates would have seen nothing abnormal. You had to check the GPG signatures or the SHAs.
A three-stage payload
The malicious entrypoint.sh, injected into trivy-action and
setup-trivy, is 204 lines long, and the real scan still runs normally after
the exfiltration. The first 102 lines contain the infostealer. The next
102 contain the legitimate Trivy code. This makes detection by the user
nearly impossible: the expected result shows up in the logs.
The payload operates in three stages:
Stage 1, Collection. On GitHub-hosted runners, the malware locates the
Runner.Worker processes and reads /proc/<pid>/mem to extract the secrets
from the .NET heap. It looks for the JSON pattern
{"value":"<secret>", "isSecret": true}, the internal format GitHub Actions
uses to store secrets in memory. On self-hosted runners, a Python script
introduces itself as the "TeamPCP Cloud stealer" and targets more than
50 sensitive paths: SSH keys, AWS/GCP/Azure credentials,
Kubernetes tokens, Docker configs, TLS keys, WireGuard VPN configs, shell
history, and even cryptocurrency wallets (Bitcoin, Ethereum, Solana,
Monero, Ledger...).
Stage 2, Encryption. The collected data is encrypted with
AES-256-CBC using PBKDF2 derivation. The session key is then wrapped with
the attacker's RSA-4096 public key via OAEP. Everything is packaged into
tpcp.tar.gz.
Stage 3, Exfiltration. Primary channel: HTTPS POST to
scan.aquasecurtiy[.]org (a typosquat). Fallback channel: if the C2
fails and a GITHUB_PAT is available, the malware creates a public
tpcp-docs repository on the victim's GitHub account and uploads the
encrypted bundle as a release asset. The GITHUB_TOKEN is not enough, a user
PAT is required.
The v0.69.4 binary: persistence on developer machines
The malicious binary goes further than the Action: it targets developer
workstations. It checks that GITHUB_ACTIONS is not "true" to
distinguish a CI/CD runner from a developer machine. On a local machine,
it:
- Writes a Python dropper to
~/.config/systemd/user/sysmon.py. - Creates a user systemd service to run the script persistently.
sysmon.pysleeps for 5 minutes, then queries a C2 hosted on the ICP blockchain (tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io).- If the C2 returns a link that is not a YouTube URL (yes, the RickRoll
served as a placeholder), the dropper downloads the payload to
/tmp/pglogand executes it.
On 22 March, the ICP canister was actively serving a payload named
kamikaze.sh. It was reported and marked "Unavailable Due to Policy
Violation" the same day at 21:31 UTC.
The attacker also went after GPG keys and Docker Hub access
A detail my previous articles did not mention: the attacker used the
aqua-bot service account to push malicious workflows into the tfsec,
traceeshark and trivy-action repositories. These workflows were
designed to exfiltrate GPG keys, Docker Hub, Twitter and
Slack credentials (Aqua uses Teams, not Slack, so the targeted
credentials are probably inherited). The exfiltration was supposed to go
through a Cloudflare Tunnel endpoint
(plug-tab-protective-relay.trycloudflare.com).
The attacker also impersonated contributors by forging commit metadata:
rauchg (pushed towards actions/checkout) and DmitriyLewen (pushed
towards aquasecurity/trivy).
TeamPCP: the attribution
Aqua formally attributes the attack to the TeamPCP group, also tracked as DeadCatx3, PCPcat and ShellForce. The self-identification in the stealer's Python code ("TeamPCP Cloud stealer") could be a false flag, but the technical cross-checking is solid: cloud-native targeting, exploitation of misconfigured Docker and Kubernetes APIs, cryptomining and worm-based ransomware campaigns, use of ICP infrastructure. Wiz formally tracks this actor on threats.wiz.io.
What does Aqua's post not say, or only say between the lines?
"Incomplete early containment"
The most important sentence of the post is buried in the timeline: "Subsequent investigation revealed the rotation was not fully comprehensive, allowing the threat actor to retain residual access via still-valid credentials."
Translated: the early-March credential rotation did not work. The attacker kept a residual access that allowed him to strike again on 19 March, then on 22 March. This is exactly what BoostSecurity had assumed in its 20 March analysis. Aqua now confirms it, but the wording remains clinical. We would like to know how many credentials had not been rotated, why the rotation was incomplete, and which processes have been put in place to prevent it from happening again.
Enterprise isolation: defence or sales pitch?
Aqua insists, three times in the post, that its commercial environment is architecturally isolated: no shared repositories, no common CI/CD infrastructure, no shared secrets, SSO, IP allowlisting, ZTNA, a controlled fork with security review before integration.
That is reassuring. But it is also exactly what a security vendor has to say after an incident affecting its free open source version. The question is not whether the commercial architecture was isolated, it is whether this isolation has been verified by a third party and whether the results of that verification will be made public. The post mentions no external audit of the commercial environment.
CanisterWorm: the propagation continues
The "What's Next" section of the post confirms that the TeamPCP group pivoted to the npm ecosystem using the publishing tokens stolen from the compromised pipelines. Aikido Security documented this self-propagating worm named CanisterWorm. StepSecurity had already raised the alarm. Aqua now acknowledges it in its post, which confirms that the Trivy incident is not a closed event, it is the initial vector of an ongoing supply chain propagation campaign.
What are the lasting lessons?
After four posts and six weeks of follow-up, here is what this affair teaches in a durable way.
1. A botched secret rotation is worse than no rotation
A partial rotation gave a false assurance of containment for 18 days, during which the attacker prepared the second act. The community accepted Aqua's 1 March communication as a contained incident. It was not. Every secret rotation must be exhaustive, verified, and accompanied by an audit of all the usages of the rotated secret.
2. GitHub's Immutable badge is not enough
GitHub's immutable releases feature genuinely protects the tag after
publication, trivy-action@0.35.0 is the proof. But it does not protect
against a scenario where the attacker force-pushes a tag then publishes an
immutable release that freezes the malicious state. The "Immutable" badge
displayed by GitHub then becomes a false trust signal. Pinning on a
full SHA remains the only truly immutable protection for a pipeline.
3. A compromised security tool is not a classic CVE
The Trivy attack is not a software vulnerability, it is a machine identity compromise followed by a distribution chain poisoning. The CVE/GHSA model, designed for bugs in code, is ill-suited to this type of incident. The GHSA and the CVE arrived within days, but the actionable content for teams, payload details, binary IOCs, attribution, was only published by Aqua thirteen days later.
4. Reading /proc/pid/mem is an underestimated attack angle
The fact that the malware reads the memory of the runner processes to extract GitHub Actions secrets in cleartext is a brutal reminder: a pipeline's secrets are not encrypted in memory during execution. It is a known attack vector, but most teams do not model it in their CI/CD threat model.
5. Blockchain as a C2 complicates the response
Hosting the C2 on an ICP canister is a deliberate choice: there is no server to seize, no domain to blackhole, no hosting provider to contact. The removal eventually came ("Unavailable Due to Policy Violation"), but the delay is structurally longer than for a classic C2. Incident response teams must include blockchain endpoints in their network monitoring.
6. A vendor's transparency is measured by pace, not volume
Aqua eventually published a rich technical document, but most of the added value arrived thirteen days after the incident, and part of the content was contributed by external researchers (Wiz, Socket). Transparency is not a detailed blog post published two weeks later, it is clear communication in the first 48 hours, followed by enrichments.
Does this change my recommendations?
The answer to the question of my previous post is partially yes: the technical analysis is serious, but several points still hold me back. The question was: "Has Aqua published the complete and transparent analysis that would allow recommending Trivy again?"
The collaboration with Sygnia is a good signal. The admission of the incomplete rotation is an important act of transparency, even if the wording remains cautious.
But several points still hold me back:
- No external audit published on the isolation of the commercial environment.
- No detail on the number and nature of the credentials not rotated in March.
- The incident is not closed: CanisterWorm keeps propagating.
- The communication was slow. A tool executed in thousands of pipelines deserves a clear advisory within 24 hours, not a post enriched in waves over two weeks.
My recommendation therefore stays the same: for production pipelines, favour Grype as the main scanner and Syft for SBOM generation.
This is not a definitive judgement. If Aqua publishes a post-mortem with the complete Sygnia analysis, an independent audit of the commercial environment, and verifiable guarantees on the new release chain protections, I will re-evaluate.
Sources
- Aqua Security, Trivy Supply Chain Attack: What You Need to Know, official post, 5 updates (22 March → 1 April 2026)
- GitHub Discussion #10425, maintainers and community discussion
- GHSA-69fq-xp46-6x23, GitHub Security Advisory
- Wiz Research, TeamPCP Supply Chain Attack, attribution, binary IOCs, SITF model
- Socket Security, Trivy GitHub Actions Compromise, full catalogue of the 75+7 compromised tags
- Aikido Security, CanisterWorm, self-propagating npm worm
- Wiz Research, KICS GitHub Action Compromise, parallel compromise