In March 2026, an actor known as TeamPCP carried out a chained compromise of several open source projects widely used in security and CI/CD pipelines. It started with Trivy, Aqua Security’s vulnerability scanner, and ended five days later with litellm, a popular gateway for calling LLM APIs. The public report from Arctic Wolf estimates at least 1,000 enterprise SaaS environments potentially affected. The most uncomfortable part of the case: many victims had “pinned versions.” They had just pinned them wrong.

This post focuses on one specific defense against supply chain attacks: strict version pinning. There are many other vectors worth considering, but we’ll dig into one that was central to how this attack escalated.

What is a supply chain attack?

A software supply chain attack doesn’t target you directly. It compromises something your code depends on — a PyPI library, a GitHub Action, a base Docker image — and lets you run it yourself inside your pipeline. When that dependency runs in your CI, it has the same permissions as your pipeline: your secrets, your cloud tokens, your deployment credentials. That’s why a single compromised package can hit thousands of organizations simultaneously.

The TeamPCP attack: from Trivy to litellm

March 19, 17:43 UTC — Trivy. TeamPCP had retained residual access to the aquasecurity organization from an earlier breach in February that wasn’t fully rotated. With that access, it force-pushed 76 of 77 version tags in aquasecurity/trivy-action, and all 7 tags of aquasecurity/setup-trivy. Every tag — including versions that had been stable for months — was repointed to a malicious commit. Any workflow in the world using uses: aquasecurity/trivy-action@v0.28.0 began executing a payload called “TeamPCP Cloud stealer”: it dumped memory from the Runner.Worker process, collected SSH, AWS, GCP, Azure, and Kubernetes secrets, encrypted them with AES-256 + RSA-4096, and exfiltrated them to a remote server.

March 24, 10:39 UTC — litellm. Here the vector changes. Instead of compromising GitHub Actions, TeamPCP published directly to PyPI two malicious versions (1.82.7 and 1.82.8), using credentials it had pulled from the Trivy compromise (litellm used trivy-action in its CI). Version 1.82.8 included a litellm_init.pth file that runs on any Python startup on the system — not only when you import litellm.proxy. The versions were live on PyPI for nearly three hours before being quarantined.

GitHub Actions and almost every package manager treat the notion of “version” as an alias. When you write uses: aquasecurity/trivy-action@v0.28.0, you’re not asking for a specific commit; you’re asking for “whatever v0.28.0 points to right now.” A tag is a pointer, and pointers are rewritable by anyone with push permissions and --force.

The only truly immutable reference in Git is the commit SHA. If your workflow references something by tag or branch, it isn’t pinned — and GitHub itself recommends it this way in its official documentation on the secure use of actions.

The defense, part 1: GitHub Actions by SHA

Replace this:

- uses: aquasecurity/trivy-action@master
- uses: aquasecurity/trivy-action@v0.28.0

With this:

- uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # v0.29.0

The # v0.29.0 comment is there so humans know which version it is. What GitHub Actions resolves is the SHA, and the SHA is immutable.

Apply this principle to all actions, not just the “security” ones. An action that looks harmless — actions/setup-node, actions/cache, a Slack notification — runs with the same permissions as the workflow and can read your secrets.

In August 2025, GitHub added a policy that refuses to run workflows that don’t use actions pinned to a SHA. It’s available at the enterprise, organization, or repository level — turn it on wherever you can. It doesn’t depend on team discipline; it’s a guardrail.

The defense, part 2: pip with --require-hashes

For Python, pinning the version (litellm==1.82.6) isn’t enough. The real defense is hash-checking mode.

Generate a lock with hashes for the entire tree:

pip-compile --generate-hashes --output-file=requirements.txt requirements.in

This produces:

litellm==1.82.6 \
    --hash=sha256:a8f3c4b8e7d6...
httpx==0.27.2 \
    --hash=sha256:5a7b3c8f9e1d...

And in your workflow:

- run: pip install --require-hashes --no-deps -r requirements.txt

--require-hashes makes pip fail loudly if any package has no declared hash or doesn’t match the contents of the wheel. Would this have protected you against the litellm hack? Yes. With 1.82.6 pinned along with its hash, any different version pip tried to install would have failed — the hash wouldn’t match.

Conclusion

What TeamPCP demonstrated isn’t a novel technical vulnerability. It’s that the industry trusts tags and version numbers as if they were immutable when they aren’t. The people who use Trivy use it precisely because they care about security — and even so, the vast majority of compromised pipelines were compromised because they pinned to @v0.28.0 instead of a 40-character SHA.

A good next step, today: open your .github/workflows/ and count how many uses: point to a tag or branch. Migrate each one to a commit SHA — that’s what will protect you when the next attack of this kind shows up.