DEV Community

Noel Himer
Noel Himer

Posted on • Originally published at unbearabletechtips.beehiiv.com

If you use Trivy or KICS in CI, read this

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

…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
Enter fullscreen mode Exit fullscreen mode

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>" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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


built by Noel @ Unbearable TechTips — practical homelab + agent ops. Reply to this email — I read every one.

Sponsor this newsletter · GitHub · YouTube

Top comments (0)