Aller au contenu
Français
medium

The Trivy affair, Act IV, Aqua finally spoke and here is what it changes

Lire cet article en français

17 min read
Trivy, act IV

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.

DateEvent
Late FebruaryExfiltration of the privileged access token
1 MarchPublic disclosure of the initial incident, credential rotation
19 March ~17:43 UTCRelease v0.69.4 + poisoned trivy-action tags
19 March ~20:38 UTCContainment, malicious artifacts removed
20 MarchSafe versions + IOCs published
21 MarchPublication of GHSA-69fq-xp46-6x23
22 March ~16:00 UTCDocker Hub images 0.69.5/0.69.6 compromised
22 March ~21:40 UTCInternal repositories made public
22 MarchFirst Aqua blog post
23 MarchCVE-2026-33634 received by the NVD, Sygnia engaged
24 MarchEnterprise isolation, kics-github-action compromise
25 MarchRemediation/documentation phase
1 AprilComplete 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:

  1. Took the tree of the HEAD of master (57a97c7e).
  2. Replaced only entrypoint.sh with the infostealer payload.
  3. Retrieved the original commit the tag pointed to.
  4. Cloned that commit's metadata: author, email, committer, both timestamps, the full message with the PR number and the "Fixes" references.
  5. Set the parent to 57a97c7e instead of the original parent.
  6. 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:

  1. Writes a Python dropper to ~/.config/systemd/user/sysmon.py.
  2. Creates a user systemd service to run the script persistently.
  3. sysmon.py sleeps for 5 minutes, then queries a C2 hosted on the ICP blockchain (tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io).
  4. 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/pglog and 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