Mini Shai-Hulud — TeamPCP npm/PyPI Supply-Chain Worm (CVE-2026-45321)
$HOME if tokens are revoked before it's removed. Sibling of the later "Hades" wave. Defensive hunt & harden pack.Executive Summary
Mini Shai-Hulud (CVE-2026-45321, CVSS 9.6) is a self-propagating credential-stealing worm run by TeamPCP. Beginning 11 May 2026 it trojanized 170+ npm/PyPI packages — including @tanstack/*, @mistralai/*, @uipath/*, and @opensearch-project/* — by abusing hijacked GitHub OIDC tokens to authenticate and re-publish. Its standout capabilities: SLSA Build Level 3 provenance-attestation forgery (so malicious builds look trustworthy), OIDC-token extraction from GitHub Actions runner process memory, and persistence hooks that target AI coding agents and developer IDEs.
Most dangerously, it installs a "dead-man's-switch": if you revoke the stolen tokens before removing the worm, it wipes the developer's home directory — a deliberate anti-remediation trap. This is the same TeamPCP lineage as the later Shai-Hulud "Hades" PyPI wave (see the sibling pack). The blast radius is your developer + CI/CD secrets, so the response order matters: remove the worm first, then rotate.
node/curl to GitHub) and Q3 (CI process reading the runner's OIDC env/memory). Because of the dead-man's-switch, on any affected host: quarantine/contain and remove the worm + persistence BEFORE revoking tokens, then rotate every reachable credential.
Source Review & Web Hunter Notes
| Tier | Source | Key Finding | Carry |
|---|---|---|---|
| 1 · Vendor research | Phoenix Security — Mini Shai-Hulud (TeamPCP) | OIDC-token hijack propagation; 170 packages; TanStack/OpenSearch/Mistral; chain detail | yes |
| 1 · Vendor (vuln) | Tenable — CVE-2026-45321 FAQ | CVSS 9.6; npm+PyPI campaign; affected scopes; remediation | yes |
| 1 · News of record | The Hacker News — TanStack/Mistral/Guardrails compromise | Package list, timing, worm behavior | yes |
| 2 · Detection tooling | shai-hulud-scan (GitHub) · Security Boulevard FAQ | IOC artifacts; SLSA L3 forgery; OIDC mem-extraction; AI-agent/IDE hooks; dead-man's-switch | yes |
Decisions: dev/CI-CD-first (native SCA + GitHub/OIDC audit are primary); endpoint CQL covers the Falcon-visible behaviors (npm hook → node → GitHub, OIDC env access, dead-man's-switch persistence); response sequencing called out prominently because of the wipe trap; cross-references the Shai-Hulud "Hades" sibling pack (same TeamPCP lineage).
Hunt Brief & Attack Chain
Working hypothesis: A developer or CI runner installs a trojanized package; its lifecycle hook runs the worm, which scrapes OIDC/cloud/npm tokens (including from runner memory), forges SLSA provenance, persists in IDEs/AI-agents, arms the dead-man's-switch, and re-publishes to more packages.
| Step | Behavior | Telemetry | Hunt Angle |
|---|---|---|---|
| 1 · Install | pip/npm installs a trojanized package; lifecycle hook fires | pkg-manager exec | N1; Q1 |
| 2 · Payload | Hook runs node/script; fetches stage from GitHub | ProcessRollup2; net | Q1 / Q2 |
| 3 · OIDC theft | Extract OIDC token from GitHub Actions runner env/memory | env/mem access | Q3; N4 |
| 4 · Persistence | Hooks into AI coding agents / IDE configs; dead-man's-switch armed | file write | Q4 / Q5; N3 |
| 5 · Worm + forge | Re-publish via stolen tokens; forge SLSA L3 provenance | GitHub/npm audit | Q6; N5 |
| 6 · Anti-remediation | Wipes $HOME if tokens revoked before removal | mass file-delete | Q5 (watcher) |
Affected surface & telemetry
| Surface | Required Telemetry | Priority | Gap Risk |
|---|---|---|---|
| Developer workstations (Linux/macOS/Windows) | Process + file telemetry (Falcon sensor) | Critical | Medium |
| CI/CD runners (esp. GitHub Actions) | Runner process/env; Actions audit | Critical | High — ephemeral runners often unmonitored |
| npm/PyPI registries | Dependency inventory / SCA | High | Low — lockfile/SBOM scan |
| GitHub org / OIDC | Actions + OIDC token audit | High | Medium — needs audit-log ingestion |
Consolidated IOC Table
| Type | Value | Conf | Action | Context |
|---|---|---|---|---|
| Package scope | @tanstack/* · @mistralai/* · @uipath/* · @opensearch-project/* | high | detect | Trojanized package families — pin away |
| Behavior | npm/pip lifecycle hook spawns node/curl → GitHub | high | detect | Worm payload execution |
| Behavior | OIDC token extraction from Actions runner memory/env | high | hunt | ACTIONS_ID_TOKEN_REQUEST_* theft |
| Behavior | dead-man's-switch: wipes $HOME on token revocation | high | detect | Anti-remediation — remove worm first |
| TTP | SLSA Build L3 provenance forgery | medium | hunt | Makes malicious builds look trusted |
ATT&CK Mapping
| Tactic | Technique | Observed Behavior | Query / Control |
|---|---|---|---|
| Initial Access | T1195.002 — Compromise Software Supply Chain | Trojanized npm/PyPI packages | N1; SCA (§10) |
| Execution | T1059.007 / T1059.006 — JavaScript / Python (lifecycle hook) | postinstall hook runs the worm | Q1, Q2 |
| Credential Access | T1552.007 / T1528 — CI container/cloud creds / OIDC token theft | OIDC scrape from runner memory | Q3; N4 |
| Persistence | T1554 / T1546 — Compromise host software (IDE / AI-agent hooks) | IDE/AI-agent config persistence | Q4; N3 |
| Impact | T1485 — Data Destruction (dead-man's-switch) | Wipes $HOME on token revocation | Q5 |
| Defense Evasion | T1036 / T1195 — SLSA provenance forgery | Forged build attestation | N5 |
| Lateral / Impact | T1195.002 (worm) — re-publish via stolen tokens | Self-propagation | Q6; N5 |
Native Audit-Log Hunts (dev / CI-CD — primary surface)
| Hunt | Source | Logic | Response |
|---|---|---|---|
| N1 · Affected-package inventory | SCA / SBOM / lockfiles | Any project resolving an affected package@version (@tanstack/@mistralai/@uipath/@opensearch-project + §9) | Pin away; rebuild; remove worm before rotating |
| N2 · Worm artifact scan | Endpoint file scan (shai-hulud-scan IOCs) | Worm files, lock files, IDE/AI-agent hook configs on dev hosts | Remove; treat host as compromised |
| N3 · IDE / AI-agent integrity | Config-file audit | Unexpected modifications to IDE settings / AI-coding-agent config (persistence hooks) | Restore clean config; rebuild |
| N4 · GitHub Actions / OIDC audit | GitHub Actions + OIDC token audit | OIDC token requests / workflow runs that don't match expected pipelines; new self-hosted runners | Investigate; revoke (after worm removed) |
| N5 · Registry re-publish | npm/PyPI publish audit | Unexpected package publishes from your org's tokens; forged provenance attestations | Unpublish; revoke publish tokens; report |
CrowdStrike LogScale CQL Hunt Queries
Looks for: npm/pip as the parent of node/curl/wget reaching GitHub during install. Accomplishes: catches the worm payload firing from a lifecycle hook. FP: legit postinstall scripts fetch from GitHub — verify the URL/script.
// HUNT: Mini Shai-Hulud lifecycle-hook payload (npm/pip -> node/curl -> GitHub) // MITRE: T1195.002, T1059.007 | CONF: high FP: low-med COST: low // FP NOTES: some legit postinstall scripts fetch from GitHub — verify URL/script content #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | ParentBaseFileName=/^(npm|npm-cli|node|pip[0-9.]*|yarn|pnpm)$/i | FileName=/^(node|curl|wget|bash|sh|powershell)$/i | CommandLine=/(githubusercontent|api\.github\.com|github\.com\/.+\/(raw|releases))/i | table([@timestamp, ComputerName, UserName, ParentBaseFileName, FileName, CommandLine], limit=200)
Looks for: node running a script from node_modules with obfuscation/exec markers. Accomplishes: confirms worm execution. FP: correlate with Q1 + a recent install.
// HUNT: Mini Shai-Hulud worm script from node_modules // MITRE: T1059.007 | CONF: med-high FP: low-med COST: low // TUNING: correlate with Q1; check for eval/Buffer.from/child_process spawn in the script #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | FileName=/^node$/i | CommandLine=/node_modules\/.+(postinstall|setup|bundle|index)\.(c?js)/i | table([@timestamp, ComputerName, UserName, ParentBaseFileName, CommandLine], limit=200)
Looks for: a process referencing GitHub Actions OIDC env vars (ACTIONS_ID_TOKEN_REQUEST_*) or reading the runner's token endpoint. Accomplishes: the OIDC-theft stage. FP: legit OIDC-using steps — flag non-pipeline processes.
// HUNT: OIDC token extraction from GitHub Actions runner // MITRE: T1552.007, T1528 | CONF: high FP: low-med COST: med // TUNING: scope to CI runner host group; flag node/python/sh referencing the OIDC token vars #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | CommandLine=/(ACTIONS_ID_TOKEN_REQUEST_TOKEN|ACTIONS_ID_TOKEN_REQUEST_URL|actions\/oidc|id-token)/i | FileName=/^(node|python[0-9.]*|bash|sh|curl)$/i | table([@timestamp, ComputerName, UserName, FileName, CommandLine], limit=200)
Looks for: writes to IDE / AI-agent config locations (VS Code, Cursor, Claude/Copilot config, .npmrc) by a non-IDE process. Accomplishes: catches the persistence hooks. FP: legit config tooling — correlate with Q1/Q2.
// HUNT: Mini Shai-Hulud IDE / AI-agent persistence hooks // MITRE: T1554, T1546 | CONF: medium FP: medium COST: low // TUNING: correlate to Q1/Q2; flag writes by node/python, not the IDE itself #event_simpleName=/NewExecutableWritten|FileWritten/ | TargetFileName=/(\.vscode|\.cursor|Code\/User\/settings|\.npmrc|\.continue|claude.*config|copilot)/i | ContextBaseFileName=/^(node|python[0-9.]*|bash|sh)$/i | table([@timestamp, ComputerName, UserName, ContextBaseFileName, TargetFileName], limit=200)
Looks for: a destructive command targeting the home directory (the anti-remediation wipe). Accomplishes: catches the dead-man's-switch firing. FP: rare legit cleanup — alert on any hit.
// HUNT: Mini Shai-Hulud dead-man's-switch ($HOME wipe on token revocation) // MITRE: T1485 | CONF: high FP: low COST: low #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | CommandLine=/(rm\s+-rf\s+(\$HOME|~|\/home\/|\/Users\/)|Remove-Item\s+-Recurse.+(USERPROFILE|HOMEPATH)|rd\s+\/s\s+\/q\s+%USERPROFILE%)/i | table([@timestamp, ComputerName, UserName, FileName, CommandLine, ParentBaseFileName], limit=200)
Looks for: node contacting registry.npmjs.org / GitHub API outside a sanctioned publish pipeline. Accomplishes: surfaces worm re-publish. FP: legit dev tooling — correlate with Q1/Q3 + non-CI host.
// HUNT: Worm re-publish (node -> npm registry / GitHub API outside a publish pipeline) // MITRE: T1195.002 | CONF: medium FP: medium COST: med // TUNING: exclude sanctioned publish runners; correlate to Q1/Q3 #event_simpleName=DnsRequest | DomainName=/(^|\.)(registry\.npmjs\.org|api\.github\.com|uploads\.github\.com)$/i | ContextBaseFileName=/^(node|npm)$/i | table([@timestamp, ComputerName, ContextBaseFileName, DomainName], limit=200)
Detection Recommendations (SCA / GitHub / CI)
| Detection | Logic | Benign Exclusions | Deployment Path |
|---|---|---|---|
| Affected-package gate | Block/alert on builds resolving affected package@versions (§9) | None — hard-block known-bad versions | SCA / dependency firewall / CI gate |
| OIDC / Actions anomaly | OIDC token requests / publishes that don't match expected workflows; new self-hosted runners | Sanctioned pipelines | GitHub Advanced Security / SIEM correlation |
| Dead-man's-switch (Q5) | Mass home-directory deletion on a dev host | None expected | Falcon Custom IOA (process) / scheduled search |
Machine-Readable IOC Appendix
Grouped IOC Quick-Copy
One-click blocks for SCA gating, endpoint sweeps, and the (carefully-sequenced) response. Package versions rotate — re-check against the latest Tenable/Phoenix advisory.
# Trojanized package families (11 May 2026 wave, 170+ packages): @tanstack/* # TanStack (npm) @mistralai/* # Mistral AI (npm) @uipath/* # UiPath (npm) @opensearch-project/* # OpenSearch (npm) Guardrails AI # (compromised package) # CVE-2026-45321 (CVSS 9.6). Re-verify exact versions vs the latest Tenable/Phoenix/Socket advisory. # Hunt lockfiles (package-lock.json / yarn.lock / poetry.lock) for any of these scopes.
npm/pip/yarn/pnpm lifecycle hook spawns node/curl/wget -> github(usercontent) node running a postinstall/setup/bundle script from node_modules with eval/child_process Process referencing ACTIONS_ID_TOKEN_REQUEST_TOKEN / id-token (OIDC theft on a runner) Writes to .vscode / .cursor / .npmrc / AI-agent config by node/python (persistence hooks) rm -rf $HOME / Remove-Item -Recurse %USERPROFILE% (dead-man's-switch wipe) node contacting registry.npmjs.org / api.github.com outside a sanctioned publish pipeline
# CRITICAL: the worm wipes $HOME if you revoke tokens before removing it. 1. CONTAIN the host/runner (network-isolate) BEFORE touching tokens 2. REMOVE the worm + persistence (node_modules, IDE/AI-agent hooks, dead-man's-switch watcher) 3. ONLY THEN rotate: GitHub OIDC/Actions tokens, npm/PyPI publish tokens, cloud keys, SSH 4. Rebuild dev hosts + CI runners from clean images 5. Audit GitHub Actions/OIDC + registry publishes; unpublish forged-provenance packages 6. Related: Shai-Hulud "Hades" PyPI wave (same TeamPCP lineage) — check that pack/scope too
SCA / dependency-firewall: hard-block the affected scopes at install Disable npm lifecycle scripts where possible (npm ci --ignore-scripts; --no-build-isolation for pip) Short-lived, least-privilege OIDC; scope GitHub Actions permissions; protect id-token usage Egress allow-list on runners (npm mirror + GitHub only); alert on unexpected runtime downloads Verify SLSA provenance via independent attestation (don't trust the package's own claim) Hash-pinned lockfiles; review new/updated deps before they reach builds
Hardening — Tiered & Deployable
Supply-chain prevention is dependency governance + OIDC/token hygiene + provenance you actually verify.
- Contain affected hosts/runners BEFORE rotating — M1053. The dead-man's-switch wipes
$HOMEif you revoke tokens first; remove the worm, then rotate. - SCA / dependency-firewall: hard-block the affected scopes — M1016. @tanstack/@mistralai/@uipath/@opensearch-project known-bad versions.
- Disable npm/pip lifecycle scripts — M1042.
npm ci --ignore-scripts; pip wheels +--no-build-isolationcontrols. - Lock down GitHub Actions OIDC: least privilege, scoped
id-token— M1018 / M1026.
- Short-lived / least-privilege CI credentials; scope Actions permissions — M1026 / M1015.
- Egress control on runners — M1037. Allow-list npm mirror + GitHub; alert on unexpected runtime downloads.
- Hash-pinned lockfiles + dependency review gate — M1016.
- Independently verify SLSA provenance / signed artifacts — M1016. The worm forges its own attestation — verify out-of-band (Sigstore).
- Internal package mirror / allow-list with review — M1016.
- Secretless builds + workload identity; protect AI-agent/IDE config integrity — M1015 / M1045.
Containment Runbook
$HOME.| Phase | Actions | Owner | Evidence |
|---|---|---|---|
| Isolate (FIRST) | Network-contain affected dev hosts/runners; freeze affected pipelines — do NOT revoke tokens yet | SOC / Platform | Containment timestamp; host list |
| Eradicate (BEFORE rotating) | Remove the worm + persistence (node_modules, IDE/AI-agent hooks, dead-man's-switch watcher); confirm Q5 watcher is gone | IR | Removal log; watcher confirmed dead |
| Rotate (AFTER removal) | Rotate GitHub OIDC/Actions + npm/PyPI publish tokens, cloud keys, SSH; rebuild runners from images | IR / Platform | Rotation log; rebuild tickets |
| Recover | Restore clean pinned dependencies + IDE/AI-agent configs; verify no further spread | IR / IT | Clean lockfiles; config integrity |
| Harden | SCA gate + provenance verification + OIDC scoping + egress control; promote Q1/Q3/Q5 detections | Platform / Detection Eng | CI gate live; detections enabled |
Detection Coverage Map & Validation
| Technique | Behavior | CQL | Native | Coverage |
|---|---|---|---|---|
| T1195.002 | Trojanized package install | Q1, Q2 | N1, N2 | Good via SCA |
| T1552.007 / T1528 | OIDC token theft (runner) | Q3 | N4 | Partial needs runner telemetry |
| T1554 / T1546 | IDE / AI-agent persistence | Q4 | N3 | Partial |
| T1485 | Dead-man's-switch wipe | Q5 | — | Good |
| T1195.002 (worm) | Re-publish via stolen tokens | Q6 | N5 | Partial registry/GitHub audit |
| T1036 / T1195 | SLSA provenance forgery | — | N5 | GAP independent provenance verification |
Validation gates: (1) confirm process+file telemetry on dev/CI hosts and GitHub Actions/OIDC + registry audit ingestion; (2) baseline legit postinstall→GitHub usage; (3) lab-test: a benign package with a postinstall fetching from GitHub (Q1), a benign process echoing the OIDC env var name (Q3), and a benign rm -rf ./scratchdir to tune Q5 without firing on home paths; (4) promote Q1/Q3/Q5 + the affected-package SCA gate; keep Q2/Q4/Q6 correlated. Test response sequencing in tabletop — removal before rotation.
Hunt Summary Ticket
TITLE: Hunt — Mini Shai-Hulud (TeamPCP npm/PyPI worm, CVE-2026-45321)
SEVERITY: Critical (CVSS 9.6; self-propagating; dead-man's-switch wipe)
SCOPE: Developer workstations + CI/CD runners installing affected npm/PyPI packages
HYPOTHESIS: Trojanized package lifecycle hook -> node -> GitHub stage -> OIDC theft from
runner memory -> SLSA forge -> IDE/AI-agent persistence -> dead-man's-switch ->
re-publish via stolen tokens (worm).
QUERIES RUN: Q1 lifecycle hook -> github | Q2 worm script | Q3 OIDC theft | Q4 IDE/AI persistence |
Q5 dead-man's-switch wipe | Q6 worm re-publish
+ Native N1-N5 (SCA inventory, worm-artifact scan, IDE integrity, GitHub OIDC audit,
registry re-publish)
DO FIRST: N1 (affected-package inventory) + Q5 watch; CONTAIN+REMOVE before rotating tokens
FINDINGS: <pending analyst execution>
GAPS: OIDC/runner telemetry; SLSA forgery needs independent provenance verification
ACTIONS: SCA hard-block; disable lifecycle scripts; scope OIDC; verify provenance; promote Q1/Q3/Q5
WARNING: Dead-man's-switch wipes $HOME if tokens revoked before worm removal — sequence matters
OWNER: HuntPack
VERSION: v0.3 - 2026-06-12
Changelog
table() calls now carry an explicit row limit: the default is 200 and truncation is silent, so a capped result was indistinguishable from a complete one. Atomic-IOC sweeps (filters over 5+ hashes or C2 IPs) use limit=max so a wide infection is never silently under-scoped; behavioural hunts use limit=200, where exceeding the cap indicates the query needs tuning. Where present, event names that do not exist in the Falcon data model were corrected (e.g. ServiceInstalled is a Sysmon concept, not a Falcon event; ElfFileWritten is ELFFileWritten) — such queries could never return a row. No detection logic, fields, or IOCs changed.References
| Tier | Source | Used For |
|---|---|---|
| 1 | Phoenix Security — Mini Shai-Hulud (TeamPCP) | OIDC propagation, 170 packages, chain detail |
| 1 | Tenable — Mini Shai-Hulud FAQ (CVE-2026-45321) | CVSS 9.6, scope, remediation |
| 1 | The Hacker News — TanStack/Mistral/Guardrails compromise | Package list, timing, worm behavior |
| 2 | shai-hulud-scan (IOC scanner) · Security Boulevard — TeamPCP FAQ | IOC artifacts, SLSA forgery, OIDC mem-extraction, dead-man's-switch |
HuntPack v0.3 · Mini Shai-Hulud (TeamPCP) · Generated 2026-06-12 · Defensive use only. Package versions/infrastructure rotate; keep SCA + GitHub/OIDC monitoring live. Respect the response sequencing — remove the worm before revoking tokens, or the dead-man's-switch wipes the home directory. See the Shai-Hulud "Hades" pack for the related PyPI wave. Validate field names in your tenant before promoting any query.