Mini Shai-Hulud — TeamPCP npm/PyPI Supply-Chain Worm (CVE-2026-45321)

TeamPCP's self-propagating credential-stealing worm. It hijacks GitHub OIDC tokens to re-publish across 170+ npm/PyPI packages (@tanstack/*, @mistralai/*, @uipath/*, @opensearch-project/*), forges SLSA provenance, scrapes OIDC tokens from CI-runner memory, hooks AI coding agents/IDEs, and arms a dead-man's-switch that wipes $HOME if tokens are revoked before it's removed. Sibling of the later "Hades" wave. Defensive hunt & harden pack.
Threat
Mini Shai-Hulud (TeamPCP)
Severity
CVE-2026-45321 · CVSS 9.6 · WORM
Type
OSS supply chain · OIDC theft · self-propagating
Scope
170+ npm/PyPI packages · 11 May 2026
Related
Shai-Hulud "Hades" wave (sibling pack)
Version
v0.3 · 2026-06-12
Author
HuntPack
01

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.

Defender priority & critical sequencing: This is a dev/CI-CD threat — hunt SCA inventory + GitHub/OIDC audit, not endpoints alone. Highest-fidelity endpoint signals: Q1 (an npm lifecycle hook spawning 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.

02

Source Review & Web Hunter Notes

TierSourceKey FindingCarry
1 · Vendor researchPhoenix Security — Mini Shai-Hulud (TeamPCP)OIDC-token hijack propagation; 170 packages; TanStack/OpenSearch/Mistral; chain detailyes
1 · Vendor (vuln)Tenable — CVE-2026-45321 FAQCVSS 9.6; npm+PyPI campaign; affected scopes; remediationyes
1 · News of recordThe Hacker News — TanStack/Mistral/Guardrails compromisePackage list, timing, worm behavioryes
2 · Detection toolingshai-hulud-scan (GitHub) · Security Boulevard FAQIOC artifacts; SLSA L3 forgery; OIDC mem-extraction; AI-agent/IDE hooks; dead-man's-switchyes

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).

03

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.

StepBehaviorTelemetryHunt Angle
1 · Installpip/npm installs a trojanized package; lifecycle hook firespkg-manager execN1; Q1
2 · PayloadHook runs node/script; fetches stage from GitHubProcessRollup2; netQ1 / Q2
3 · OIDC theftExtract OIDC token from GitHub Actions runner env/memoryenv/mem accessQ3; N4
4 · PersistenceHooks into AI coding agents / IDE configs; dead-man's-switch armedfile writeQ4 / Q5; N3
5 · Worm + forgeRe-publish via stolen tokens; forge SLSA L3 provenanceGitHub/npm auditQ6; N5
6 · Anti-remediationWipes $HOME if tokens revoked before removalmass file-deleteQ5 (watcher)

Affected surface & telemetry

SurfaceRequired TelemetryPriorityGap Risk
Developer workstations (Linux/macOS/Windows)Process + file telemetry (Falcon sensor)CriticalMedium
CI/CD runners (esp. GitHub Actions)Runner process/env; Actions auditCriticalHigh — ephemeral runners often unmonitored
npm/PyPI registriesDependency inventory / SCAHighLow — lockfile/SBOM scan
GitHub org / OIDCActions + OIDC token auditHighMedium — needs audit-log ingestion
04

Consolidated IOC Table

Package versions rotate as the worm spreads — treat the list as a seed and keep SCA + GitHub/OIDC monitoring live. Copy-ready blocks in §9. Mind the dead-man's-switch when responding.
TypeValueConfActionContext
Package scope@tanstack/* · @mistralai/* · @uipath/* · @opensearch-project/*highdetectTrojanized package families — pin away
Behaviornpm/pip lifecycle hook spawns node/curl → GitHubhighdetectWorm payload execution
BehaviorOIDC token extraction from Actions runner memory/envhighhuntACTIONS_ID_TOKEN_REQUEST_* theft
Behaviordead-man's-switch: wipes $HOME on token revocationhighdetectAnti-remediation — remove worm first
TTPSLSA Build L3 provenance forgerymediumhuntMakes malicious builds look trusted
05

ATT&CK Mapping

TacticTechniqueObserved BehaviorQuery / Control
Initial AccessT1195.002 — Compromise Software Supply ChainTrojanized npm/PyPI packagesN1; SCA (§10)
ExecutionT1059.007 / T1059.006 — JavaScript / Python (lifecycle hook)postinstall hook runs the wormQ1, Q2
Credential AccessT1552.007 / T1528 — CI container/cloud creds / OIDC token theftOIDC scrape from runner memoryQ3; N4
PersistenceT1554 / T1546 — Compromise host software (IDE / AI-agent hooks)IDE/AI-agent config persistenceQ4; N3
ImpactT1485 — Data Destruction (dead-man's-switch)Wipes $HOME on token revocationQ5
Defense EvasionT1036 / T1195 — SLSA provenance forgeryForged build attestationN5
Lateral / ImpactT1195.002 (worm) — re-publish via stolen tokensSelf-propagationQ6; N5
06

Native Audit-Log Hunts (dev / CI-CD — primary surface)

HuntSourceLogicResponse
N1 · Affected-package inventorySCA / SBOM / lockfilesAny project resolving an affected package@version (@tanstack/@mistralai/@uipath/@opensearch-project + §9)Pin away; rebuild; remove worm before rotating
N2 · Worm artifact scanEndpoint file scan (shai-hulud-scan IOCs)Worm files, lock files, IDE/AI-agent hook configs on dev hostsRemove; treat host as compromised
N3 · IDE / AI-agent integrityConfig-file auditUnexpected modifications to IDE settings / AI-coding-agent config (persistence hooks)Restore clean config; rebuild
N4 · GitHub Actions / OIDC auditGitHub Actions + OIDC token auditOIDC token requests / workflow runs that don't match expected pipelines; new self-hosted runnersInvestigate; revoke (after worm removed)
N5 · Registry re-publishnpm/PyPI publish auditUnexpected package publishes from your org's tokens; forged provenance attestationsUnpublish; revoke publish tokens; report
07

CrowdStrike LogScale CQL Hunt Queries

Pick your tenant's cloud first — every "Open in Falcon" button below uses this selection.
These target Falcon-visible endpoint behaviors on dev workstations & CI runners (Linux/macOS/Windows sensors). Pair with SCA + GitHub/OIDC audit (§6). Field names validated against the Falcon event reference.
Q1 · npm/pip lifecycle hook spawning node/curl to GitHub
CONF HIGHFP LOW-MEDCOST LOW

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)
Q2 · Worm script execution from a package directory
CONF MED-HIGHFP LOW-MEDCOST LOW

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)
Q3 · OIDC token extraction from a CI runner (env/memory)
CONF HIGHFP LOW-MEDCOST MED

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)
Q4 · IDE / AI-coding-agent config persistence
CONF MEDFP MEDCOST LOW

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)
Q5 · Dead-man's-switch — mass home-directory deletion
CONF HIGHFP LOWCOST LOW

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)
Q6 · Runtime contacting npm registry / GitHub API to publish
CONF MEDFP MEDCOST MED

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)
08

Detection Recommendations (SCA / GitHub / CI)

The most reliable detections are SCA/SBOM scanning for the affected packages and GitHub Actions/OIDC audit-log monitoring, not endpoint IOAs. Promote these as platform rules alongside the endpoint queries — and bake the response sequencing (remove worm → then rotate) into the runbook.
DetectionLogicBenign ExclusionsDeployment Path
Affected-package gateBlock/alert on builds resolving affected package@versions (§9)None — hard-block known-bad versionsSCA / dependency firewall / CI gate
OIDC / Actions anomalyOIDC token requests / publishes that don't match expected workflows; new self-hosted runnersSanctioned pipelinesGitHub Advanced Security / SIEM correlation
Dead-man's-switch (Q5)Mass home-directory deletion on a dev hostNone expectedFalcon Custom IOA (process) / scheduled search
09

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.

Affected Package Scopespin away / SCA gate
# 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.
Behavioral Signaturesdetection logic
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
Response — ORDER MATTERSdead-man's-switch
# 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
Hardening / BlockCI & provenance
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
10

Hardening — Tiered & Deployable

Supply-chain prevention is dependency governance + OIDC/token hygiene + provenance you actually verify.

Immediate — Contain & gate (mind the wipe trap)
  • Contain affected hosts/runners BEFORE rotatingM1053. The dead-man's-switch wipes $HOME if you revoke tokens first; remove the worm, then rotate.
  • SCA / dependency-firewall: hard-block the affected scopesM1016. @tanstack/@mistralai/@uipath/@opensearch-project known-bad versions.
  • Disable npm/pip lifecycle scriptsM1042. npm ci --ignore-scripts; pip wheels + --no-build-isolation controls.
  • Lock down GitHub Actions OIDC: least privilege, scoped id-tokenM1018 / M1026.
Near term — Reduce token blast radius (pilot first)
  • Short-lived / least-privilege CI credentials; scope Actions permissionsM1026 / M1015.
  • Egress control on runnersM1037. Allow-list npm mirror + GitHub; alert on unexpected runtime downloads.
  • Hash-pinned lockfiles + dependency review gateM1016.
Strategic — Verify provenance, isolate builds
  • Independently verify SLSA provenance / signed artifactsM1016. The worm forges its own attestation — verify out-of-band (Sigstore).
  • Internal package mirror / allow-list with reviewM1016.
  • Secretless builds + workload identity; protect AI-agent/IDE config integrityM1015 / M1045.
11

Containment Runbook

Sequence is mandatory: network-contain and remove the worm before revoking any tokens, or the dead-man's-switch wipes $HOME.
PhaseActionsOwnerEvidence
Isolate (FIRST)Network-contain affected dev hosts/runners; freeze affected pipelines — do NOT revoke tokens yetSOC / PlatformContainment 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 goneIRRemoval log; watcher confirmed dead
Rotate (AFTER removal)Rotate GitHub OIDC/Actions + npm/PyPI publish tokens, cloud keys, SSH; rebuild runners from imagesIR / PlatformRotation log; rebuild tickets
RecoverRestore clean pinned dependencies + IDE/AI-agent configs; verify no further spreadIR / ITClean lockfiles; config integrity
HardenSCA gate + provenance verification + OIDC scoping + egress control; promote Q1/Q3/Q5 detectionsPlatform / Detection EngCI gate live; detections enabled
12

Detection Coverage Map & Validation

TechniqueBehaviorCQLNativeCoverage
T1195.002Trojanized package installQ1, Q2N1, N2Good via SCA
T1552.007 / T1528OIDC token theft (runner)Q3N4Partial needs runner telemetry
T1554 / T1546IDE / AI-agent persistenceQ4N3Partial
T1485Dead-man's-switch wipeQ5Good
T1195.002 (worm)Re-publish via stolen tokensQ6N5Partial registry/GitHub audit
T1036 / T1195SLSA provenance forgeryN5GAP 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.

13

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
14

Changelog

v0.52026-07-24CQL correctness pass. All 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.
v0.42026-06-29CQL syntax review (crowdstrike-logscale-v3): removed invalid FileCreateInfo event (Q4).
v0.32026-06-12Re-hunt + reformatted to the review-v2 gold layout (fixed left-sidebar scrollspy TOC, collapsible, cloud selector inside the CQL section, per-card Copy/Open-in-Falcon, Grouped IOC Quick-Copy grid with sequenced-response block, coverage+validation, .ticket). Refreshed from Phoenix/Tenable/THN: TeamPCP attribution, CVE-2026-45321 (CVSS 9.6), 170+ packages (@tanstack/@mistralai/@uipath/@opensearch-project), OIDC-from-runner-memory theft, SLSA L3 forgery, AI-agent/IDE persistence hooks, dead-man's-switch + the critical remove-before-rotate sequencing. Cross-linked the Shai-Hulud "Hades" sibling pack. 6 CQL + 5 native hunts.
v0.12026-05-26Initial pack: Mini Shai-Hulud npm/PyPI worm overview + hunts. Pre-gold top-bar layout.
15

References

TierSourceUsed For
1Phoenix Security — Mini Shai-Hulud (TeamPCP)OIDC propagation, 170 packages, chain detail
1Tenable — Mini Shai-Hulud FAQ (CVE-2026-45321)CVSS 9.6, scope, remediation
1The Hacker News — TanStack/Mistral/Guardrails compromisePackage list, timing, worm behavior
2shai-hulud-scan (IOC scanner) · Security Boulevard — TeamPCP FAQIOC 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.