75 of 76 trivy-action tags hijacked in five days. The pattern, three checks, and what to automate.
Hey —
Between March 19 and March 24, 2026, the "TeamPCP" actor force-pushed mutable tags on three popular security-tool repos. 75 of 76 trivy-action tags plus 7 setup-trivy tags went first. Four days later, all 91 Checkmarx KICS action tags were repointed. The same group landed malicious LiteLLM builds on PyPI on the 24th. Every CI pipeline pinned to @v0, @main, or @latest on those actions ran attacker code on its next build.
The injected payload was not subtle: it scraped the hosted GitHub runner's process memory for variables marked isSecret: true, swept the filesystem for SSH keys and cloud credentials, encrypted everything with AES-256-CBC + RSA-4096, and exfiltrated it.
If you used Trivy or KICS in CI without a SHA pin, assume those secrets are gone. Rotate, then come back to this email.
The mechanism in one paragraph
A GitHub Actions reference like uses: aquasecurity/trivy-action@v0 is just a pointer to a git ref. Anyone with push to that repo — including an attacker who steals a maintainer token — can git tag -f v0 <attacker-commit> && git push --force and now every pipeline pinned to v0 builds the attacker's code. Branches are worse. Even semver-style tags like @v2 are mutable. The only ref form that is cryptographically immutable is the full 40-character commit SHA. From the Puma Security writeup:
A commit SHA is a cryptographic hash of the repository state at that point in time. It cannot be moved or reassigned.
That's the whole defense. The rest is logistics.
Three checks you can run today
1. Grep your workflows for unpinned refs. Five minutes, no tooling:
grep -rEn 'uses: [^@]+@(main|master|v[0-9]+(\.[0-9]+)?)' .github/workflows/
Anything that prints is a candidate for repinning. If you have third-party actions there (anything not actions/* or github/*), prioritize those first — that's the TeamPCP exposure.
2. Replace high-risk pins with SHA + tag comment. The standard form:
- uses: aquasecurity/trivy-action@<40-char-sha> # v0.x.y
GitHub's UI shows the SHA on every release page. Dependabot understands this form and proposes SHA updates with the matching tag comment. You give up zero ergonomics.
3. Audit your top-level permissions: block. A workflow without an explicit permissions: key inherits contents: write by default on most repos. Add this near the top:
permissions:
contents: read
…then grant per-job writes only where needed. If the TeamPCP payload had landed on a repo using permissions: read-all, the blast radius would have been the runner secrets — not also the ability to push commits back.
These three checks take ten minutes total on a single workflow. Most of you have multiple workflows.
How I'm wiring it for ongoing use
I shipped github-actions-audit on Apify Store earlier this month — 13 checks for the published CI attack surface. After TeamPCP I'm extending it with an 8-check supply_chain_advanced category (GHA-201 through GHA-208) that catches the specific patterns the attackers exploited: mutable tag refs, pull_request_target + checkout-by-PR-sha, script injection via ${{ github.event.* }}, untrusted owners, permissions: write-all defaults.
The MCP version lets Claude or Cursor agents run the audit against a workflow YAML on demand — paste a file, get back severity, line numbers, and a copy-paste fix snippet. Pricing is unchanged: $0.02 per audit. The extension lands in the same Actor — no migration.
If you want the CLI-shaped version of the same defense, zizmor by William Woodruff is the open-source linter that pioneered most of these checks; it's how I cross-checked our findings during development. I'd run both: zizmor in pre-commit, the MCP server in agentic flows.
Try it yourself
# 1. Find every unpinned third-party action across your repos
gh repo list --limit 1000 --json nameWithOwner -q '.[].nameWithOwner' | \
while read repo; do
echo "=== $repo"
gh api "repos/$repo/contents/.github/workflows" --jq '.[].path' 2>/dev/null | \
while read wf; do
gh api "repos/$repo/contents/$wf" --jq '.content' | base64 -d | \
grep -En 'uses: [^@]+@(main|master|v[0-9]+(\.[0-9]+)?)' | sed "s|^| $wf:|"
done
done
That's three hours of grunt work compressed into one command. Run it, fix what falls out, sleep better.
For the MCP-native flow:
{
"mcpServers": {
"github-actions-audit": {
"url": "https://unbearable-dev--github-actions-audit.apify.actor/mcp",
"headers": { "Authorization": "Bearer <YOUR_APIFY_TOKEN>" }
}
}
}
Restart Claude Desktop, paste a workflow YAML, ask for an audit. The new GHA-201..208 checks are coming in the next push.
This week's reading
- Puma Security: TeamPCP GitHub Actions supply chain — the technical writeup with timeline + IoCs
-
zizmor on GitHub — the linter the audit shop's
supply_chain_advancedchecks are modeled on - StepSecurity Harden Runner — egress filtering on the GitHub-hosted runner; catches the AES/RSA exfil leg of TeamPCP-class attacks
- apify.com/unbearable_dev/github-actions-audit — the Actor itself, $0.02 per audit
built by Noel @ Unbearable TechTips — practical homelab + agent ops. Reply to this email — I read every one.













