Software Supply Chain Under Siege: The TeamPCP Campaign from Trivy to Bitwarden

Comprehensive forensic investigation into TeamPCP's multi-stage campaign compromising Trivy, LiteLLM, Bitwarden CLI, and npm registries.

Dark network cables representing software supply chain attack infrastructure
📌
🚨 Threat Intelligence Special Report • ColibriSec Knowledge Base

Overview

Between February and April 2026, a threat actor known as TeamPCP executed one of the most consequential software supply chain campaigns in recent memory. Starting with a single misconfigured GitHub Actions workflow in Aqua Security's Trivy repository, they cascaded stolen credentials across the open source ecosystem — compromising LiteLLM on PyPI, a self-propagating npm worm, the Bitwarden CLI, and ultimately Checkmarx's internal GitHub organization. 96 gigabytes of Checkmarx source code ended up on the LAPSUS$ extortion portal.

This post walks through the full attack chain, the techniques used at each stage, the indicators of compromise, and — critically — the concrete controls that would have broken the kill chain at multiple points.

Phase 1: The Trivy Compromise (February–March 19, 2026)

Initial Access: Pwn Request via pull_request_target

Trivy's repository contained a GitHub Actions workflow (apidiff.yaml) using the pull_request_target trigger — a well-documented dangerous pattern sometimes called a "Pwn Request." Unlike pull_request, this trigger runs with repository-level secrets even when the triggering code comes from a fork. The workflow had been present since October 2025 and was flagged by Boost Security's poutine tool on November 29, 2025. It was not remediated.

On February 27, 2026, an autonomous bot called hackerbot-claw (account: MegaGame10418) opened PR #10252 against the Trivy repo and immediately closed it. The pull_request_target workflow triggered anyway, checked out the attacker's fork code with elevated repository permissions, and exfiltrated the aqua-bot Personal Access Token (PAT) using a /proc/<pid>/mem memory dump technique — reading the Runner.Worker process heap to extract secrets stored as {"value":"<secret>","isSecret":true} JSON objects.

Exfiltration destination: recv.hackmoltrepeat[.]com

Credential Validation and Repository Takeover

By February 28, the attacker had confirmed the stolen PAT was valid by creating branches named 🤖🦞 on aquasecurity/trivy — the same emoji pattern used by hackerbot-claw's own PRs. With write access confirmed, they: privatized the Trivy repository, deleted all 178 GitHub releases (v0.27.0 through v0.69.1), and published a malicious VS Code extension to Open VSX. Aqua Security rotated credentials — but the rotation was not atomic. Not all tokens were revoked simultaneously, and the attacker maintained access through the aqua-bot service account.

Phase 1b: Tag Poisoning at Scale (March 19, 2026, 17:43 UTC)

Force-pushing malicious commits to 76 of 77 trivy-action tags — every pipeline referencing these by tag began running attacker code on its next run.

Using the retained aqua-bot credentials, the attacker force-pushed malicious commits to 76 of 77 version tags in aquasecurity/trivy-action and all 7 tags in aquasecurity/setup-trivy. Simultaneously, they triggered release automation to publish a malicious Trivy binary as v0.69.4 across every official distribution channel: GitHub Releases, GHCR, Docker Hub, ECR Public, deb/rpm repositories, and get.trivy.dev.

Git tags are mutable by design. A force-push silently repoints a tag to a different commit with no visible change to the release page, the tag name, or published metadata. Every CI/CD pipeline referencing these actions by tag — without SHA pinning — began running attacker code on its next execution.

The Payload: entrypoint.sh (204 lines)

The malicious entrypoint.sh ran the infostealer silently before the legitimate Trivy scan — workflows appeared to complete normally while secrets were exfiltrated.

The malicious entrypoint.sh was structured to be invisible during normal operation: lines 4–105 contained the infostealer; lines 106–204 contained the legitimate Trivy scanning logic. The scan ran and produced expected output. Operators saw nothing unusual.

The stealer operated in three stages:

Stage 1 — Collection: Escalated to root via passwordless sudo (standard on GitHub-hosted runners). Located Runner.Worker processes and read /proc/<pid>/mem to extract secrets from the .NET runtime heap, targeting the pattern {"value":"<secret>","isSecret":true}. Scanned /proc/*/environ for environment variables across all running processes. Harvested cloud credentials (AWS, GCP, Azure), SSH keys, Kubernetes tokens, Docker config files, and Git credentials.

Stage 2 — Encryption and Staging: Encrypted all collected material with AES + RSA-4096. Staged the encrypted archive as payload.enc / tpcp.tar.gz.

Stage 3 — Exfiltration (dual path): Primary: POST to scan.aquasecurtiy[.]org (typosquatted lookalike domain). Fallback: if a usable GitHub token was present, created a public repository named tpcp-docs and committed the encrypted archive there — abusing GitHub as a C2 channel since github.com traffic is rarely flagged.

The malicious commits were forged to impersonate legitimate maintainers. The Trivy binary commit spoofed "DmitriyLewen" (Aqua maintainer); the actions/checkout injection spoofed "rauchg" (Guillermo Rauch, Vercel CEO). Timestamps were backdated using GIT_AUTHOR_DATE and GIT_COMMITTER_DATE environment variables. Detection signals existed: missing GPG signatures on commits that were originally web-signed, impossible chronology (commits dated 2021 with parent commits from March 2026), and only one file changed per commit where originals touched multiple files.

Phase 1 IOCs

C2 domains: scan.aquasecurtiy[.]org | recv.hackmoltrepeat[.]com | models.litellm[.]cloud | audit.checkmarx[.]cx IP addresses: 45.148.10.212 | 94.154.172.43 Malicious versions: trivy binary v0.69.4–v0.69.6 | trivy-action tags v0.0.1–v0.34.2 (except v0.35.0) | setup-trivy tags v0.2.0–v0.2.5 Safe versions: trivy ≤v0.69.3 | trivy-action v0.35.0 (commit 57a97c7) | setup-trivy v0.2.6 (commit 3fb12ec) Persistence artifacts: ~/.config/sysmon.py (stage-1 loader on developer machines) | systemd services: sysmon, pgmon, pgmonitor, internal-monitor | Kubernetes DaemonSets: host-provisioner-std or host-provisioner-iran in kube-system

Phase 2: Credential Cascade — LiteLLM and npm (March 24–27, 2026)

The CanisterWorm self-propagated across 60+ npm packages using stolen publish tokens harvested from compromised CI runners.

Credentials harvested from Trivy's CI pipelines included PyPI publish tokens. On March 24, two malicious releases of LiteLLM — a widely used Python proxy layer for LLM providers — appeared on PyPI: v1.82.7 and v1.82.8. Both contained a credential stealer injected into proxy_server.py, with v1.82.8 also dropping a litellm_init.pth file for persistence. The packages were live from 10:39 UTC for approximately 40 minutes before PyPI quarantined the project. Exfiltration went to models.litellm[.]cloud — a lookalike domain designed to blend into LiteLLM-related network traffic.

Three days later on March 27, the Telnyx Python SDK was similarly compromised — versions 4.87.1 and 4.87.2 — following the same pattern of a stolen PyPI publish token enabling a malicious release.

Simultaneously, stolen npm tokens harvested from compromised runners powered the CanisterWorm campaign — a self-propagating npm worm that spread across at least 60 packages. The worm's mechanic: steal npm tokens from the runner environment, resolve which packages each token can publish, bump patch versions, preserve the original README to maintain appearances, and republish with the malicious payload appended. Affected scopes included @EmilGroup (28 packages), @opengov (16 packages), @teale.io, @airtm, and @pypestream.

Phase 3: Bitwarden CLI (April 22, 2026)

On April 22, 2026, @bitwarden/[email protected] appeared on npm and remained available for 93 minutes (17:57–19:30 ET) before being removed. Bitwarden's CLI sits in a privileged position in developer environments — teams wire it into CI/CD pipelines to inject secrets at build time, pull API keys into scripts, and integrate with automation frameworks.

The attack vector: a poisoned publish workflow in bitwarden/clients/.github/workflows/publish-cli.yml. Notably, this was the first known compromise of a package using npm's trusted publishing mechanism — the attacker didn't bypass trusted publishing, they compromised the GitHub Actions workflow upstream of it and let the trusted mechanism do the distribution.

Payload Structure

The package introduced two new files — bwsetup.js and bw1.js — and modified package.json to add a preinstall hook (node bwsetup.js) and redirect the bw entry point from build/bw.js to bwsetup.js. The core CLI bundle was unchanged in size (3.4 MB), confirming a supply-chain insertion rather than a full rebuild.

bwsetup.js (Stage 1 loader): Downloads the Bun JavaScript runtime from GitHub and hands execution to bw1.js. bw1.js (~9.7 MB, Stage 2): Heavily obfuscated using a seeded Fisher-Yates shuffle cipher (__decodeScrambled, seed 0x3039) across 36 call sites. Targets six credential surfaces: GitHub tokens, npm tokens, AWS access keys, GCP service account keys, Azure credentials, SSH keys, shell history, .env files, and AI tooling configuration. Exfiltrates encrypted data to audit.checkmarx[.]cx/v1/telemetry. Falls back to GitHub repository abuse (committing encrypted blobs) when direct exfiltration fails. Self-propagates by republishing all packages accessible via stolen npm tokens.

The payload embedded the string "Shai-Hulud: The Third Coming" — referencing the self-propagating npm worm that first emerged in September 2025 and has now iterated through three major campaigns.

Phase 3 IOCs

Malicious package: @bitwarden/[email protected] Clean versions: @bitwarden/[email protected] or @bitwarden/[email protected] (re-released April 23) C2: audit.checkmarx[.]cx/v1/telemetry Persistence: heredoc block appended to ~/.bashrc or ~/.zshrc | daemon PID at $TMPDIR/tmp.987654321.lock Exfiltration repos: Search GitHub for repositories with description "Shai-Hulud: The Third Coming" or commit messages matching LongLiveTheResistanceAgainstMachines: Self-propagation: npm packages with unexpected patch bumps + preinstall: node setup.mjs hook

The Full Kill Chain in One View

pull_request_target misconfiguration → PAT exfiltration via /proc/mem → repository takeover → tag force-push (76/77 tags) → CI secret harvesting across 10,000+ pipelines → PyPI token reuse (LiteLLM, Telnyx) → npm token reuse (CanisterWorm, 60+ packages) → workflow poisoning (Bitwarden publish pipeline) → Checkmarx KICS compromise → LAPSUS$ data extortion

Every phase was enabled by credentials stolen in a prior phase. The entire campaign traces back to one unpatched workflow misconfiguration that had been publicly flagged three months earlier.

Mitigation Framework

Seven concrete controls that would have broken the TeamPCP kill chain at multiple points.

1. Eliminate Mutable References in CI/CD

The root mechanism of the Trivy tag poisoning was mutable Git tags. The fix is SHA pinning — referencing actions by their full commit SHA rather than a tag name. A tag can be silently repointed. A SHA cannot.

Instead of: uses: aquasecurity/[email protected] Use: uses: aquasecurity/trivy-action@57a97c7d2a0f0f343d4c7f7a0a0e0e0e0e0e0e0e

Tooling: Dependabot can enforce SHA pinning via the GitHub Actions ecosystem. StepSecurity's Harden-Runner and actionlint can flag mutable references in CI workflows. GitHub's required workflows feature can enforce SHA-pinned action references org-wide.

2. Audit and Fix pull_request_target Usage

pull_request_target is dangerous when it checks out code from the triggering PR's fork with elevated permissions. Audit all workflows with: grep -r 'pull_request_target' .github/workflows/

Any workflow using pull_request_target that also runs actions/checkout (or equivalent) on the PR's SHA is a Pwn Request. Either remove checkout of fork code, or move the privileged logic to a separate workflow triggered only after manual approval. Boost Security's poutine and StepSecurity's Secure-Repo scanner both detect this pattern automatically.

3. Restrict Runner Permissions and Egress

GitHub-hosted runners have passwordless sudo by default. The Trivy payload used this to escalate and read /proc/mem. Mitigations: Set permissions: read-all at the workflow level and grant write only where explicitly required. Use OIDC (OpenID Connect) for cloud authentication instead of long-lived static credentials — OIDC tokens are scoped to a specific workflow run and expire immediately after. Block outbound egress from runners using StepSecurity Harden-Runner in block mode. Restrict which domains CI can reach at the network perimeter.

4. Implement Atomic, Verified Secret Rotation

Aqua's incomplete credential rotation between Phase 1 and Phase 1b is what turned a contained incident into a multi-month campaign. When rotating credentials after a suspected compromise: revoke all tokens simultaneously before issuing new ones, audit every location a service account token is used across all repositories and workflows, use short-lived credentials (OIDC) wherever possible so there is nothing to rotate in the first place, and treat any unrevoked credential as still active until proven otherwise.

5. Generate and Verify SBOMs and Artifact Attestations

A Software Bill of Materials (SBOM) documents the provenance of every component in your software. Sigstore's cosign enables signing and verifying artifact attestations — cryptographic proof that an artifact was built from a specific source commit by a specific workflow.

The malicious Trivy commits were detectable because the original releases were GPG-signed and the forged commits were not. Enforcing signature verification as a pipeline gate would have blocked the malicious artifacts from being consumed downstream. Tools: Syft (SBOM generation), cosign (artifact signing and verification), SLSA (Supply-chain Levels for Software Artifacts) framework for a graduated maturity model.

6. Dependency Pinning and Registry Verification

For npm: use npm ci (not npm install) in CI — it installs exactly what is in package-lock.json. Use --ignore-scripts only where intentional and reviewed; preinstall hooks (the Bitwarden attack vector) execute blindly otherwise. Enable npm provenance and check for attestation on high-value packages. Use Socket.dev or Endor Labs for real-time supply chain monitoring — both flagged the Bitwarden compromise within minutes of publication.

For PyPI: pin all dependencies in requirements.txt or pyproject.toml to exact versions with hashes: pip install --require-hashes -r requirements.txt. Use pip-audit to detect known-malicious packages. Monitor PyPI release events for packages you depend on via deps.dev or OSV.

For container images: reference by digest, not tag. Instead of: FROM aquasec/trivy:latest — use: FROM aquasec/trivy@sha256:<digest>. Pull digests are immutable. Tags are not.

7. Monitor for Campaign IOCs Continuously

Block and alert on all known-bad infrastructure at the network layer:

IPs: 45.148.10.212 | 94.154.172.43 Domains: scan.aquasecurtiy[.]org | recv.hackmoltrepeat[.]com | models.litellm[.]cloud | audit.checkmarx[.]cx | plug-tab-protective-relay.trycloudflare[.]com File artifacts: ~/.config/sysmon.py | $TMPDIR/tmp.987654321.lock Systemd service names: sysmon | pgmon | pgmonitor | internal-monitor Kubernetes: DaemonSets named host-provisioner-std or host-provisioner-iran in kube-system GitHub: repositories with description "Shai-Hulud: The Third Coming" or commit messages containing LongLiveTheResistanceAgainstMachines:

The Bigger Picture

TeamPCP's campaign is a masterclass in credential chaining. No single phase required novel exploitation. Each step used legitimate platform behavior — mutable tags, passwordless sudo on hosted runners, preinstall hooks, trusted publishing mechanisms — against the organizations relying on them. The scanner became the weapon. The password manager became the delivery mechanism. The CI platform became the exfiltration channel.

The entire cascade traces back to a misconfigured workflow that had been flagged and ignored for three months. Supply chain security is not primarily a tooling problem — it's an operational hygiene problem. The controls exist. The question is whether your organization has implemented them before the next campaign begins.

Read more

Brecha de Datos Médicos en Photon Health

Filtración en Photon Health: Zero-Day de Inyección SQL en Metabase Expone Recetas Médicas de Pacientes

📌Security Roundup Series: Semana del 9 de Octubre de 2026 • 4 min read deep dive🏛️Incident Overview: Target / Organization: Photon Health, Inc. (Plataforma de Prescripción Médica Digital) Threat Actor / Attribution: Actor Desconocido (Extorsión Financiera) Impact / Records Compromised: Nombres de pacientes, direcciones, números de teléfono, fechas de nacimiento, recetas médicas completas

By James Luther