Mini Shai-Hulud Self-Propagating Software Supply Chain Worm
Mini Shai-Hulud is a self-propagating npm/PyPI supply-chain worm. JFrog's May 12 and May 19 updates add a broader count of 170+ npm and 2 PyPI packages, a 323-package @antv wave, and a related @cap-js/openapi 1.4.1 variant.
On this page 0% read
Executive Summary
In late April and May 2026, a self-propagating supply-chain worm designated “Mini Shai-Hulud” hit the npm and PyPI package registries. Attributed to the threat actor group TeamPCP, the worm automates credential harvesting, lateral movement, and package poisoning. The campaign exploits misconfigured CI/CD pipelines (specifically via pull_request_target triggers and GitHub Actions cache poisoning) to steal short-lived OIDC tokens. It then uses these tokens to sign malicious updates with valid SLSA Build Level 3 provenance badges via Sigstore and publish them directly to registries.
New JFrog updates sharpen the current scope. On 2026-05-12, JFrog described an ongoing wave affecting more than 170 npm packages and 2 PyPI packages, totaling more than 200 million weekly downloads JFrog May 12. On 2026-05-19, JFrog analyzed a separate @antv/atool wave and reported 323 legitimate packages compromised through the atool npm maintainer account, plus an additional related @cap-js/[email protected] variant using a distinct indirect delivery technique JFrog May 19. Defenders must treat affected systems as fully compromised and immediately rotate all credentials, remove IDE workspace and AI assistant persistence hooks, and configure package managers to ignore install lifecycle scripts.
Key Facts
threat_type: malicious package, CI/CD compromise, credential theft, self-replicating worm, build provenance failure, artifact tampering
ecosystem: npm, PyPI
registry: npm registry, PyPI
affected_packages:
- "@tanstack/react-router"
- "@tanstack/vue-router"
- "@tanstack/solid-router"
- "@tanstack/react-start"
- "@tanstack/router-core"
- "@antv/g2"
- "@antv/g6"
- "@antv/x6"
- "@antv/l7"
- "@antv/s2"
- "@antv/f2"
- "echarts-for-react"
- "timeago.js"
- "size-sensor"
- "canvas-nest.js"
- "@sap/cds"
- "@sap/cds-dk"
- "opensearch-py"
- "lite-llm"
- "nx-console"
malicious_versions:
- "@tanstack/[email protected]"
- "@tanstack/[email protected]"
- "@tanstack/[email protected]"
- "@tanstack/[email protected]"
- "@tanstack/[email protected]"
- "@tanstack/[email protected]"
- "@tanstack/[email protected]"
- "@tanstack/[email protected]"
- "@antv/[email protected]"
- "@antv/[email protected]"
- "[email protected]"
- "@antv/* published 2026-05-19T01:39:00Z through 2026-05-19T02:06:00Z (639 versions across 323 packages)"
- "@cap-js/[email protected]"
fixed_versions:
- "[email protected]"
safe_versions: []
exposure_window: 2026-04-20 to 2026-05-19T02:06:00Z (specifically May 11, 2026, 19:20–19:26 UTC for TanStack; May 19, 2026, 01:39–02:06 UTC for @antv/atool scope)
execution_trigger: Install-time execution via preinstall/postinstall scripts (router_init.js or setup.mjs)
primary_impact: CI/CD & cloud credential theft, lateral self-propagation, development workspace hijack, potential destructive system wipe
known_iocs:
- "filev2.getsession[.]org"
- "api.masscan[.]cloud"
- "git-tanstack[.]com"
- "t.m-kosche[.]com"
- "ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c"
- "router_init.js"
- "setup_bun.js"
- "bun_environment.js"
- "transformers.pyz"
- "gh-token-monitor"
confidence: high
canonical_source: https://tanstack.com/blog/postmortem-cve-2026-45321
Source Confidence & Evidence Mapping
- confirmed:
- Incident involving the TanStack npm packages between 19:20 and 19:26 UTC on May 11, 2026, where 84 malicious versions across 42 packages were published via hijacked OIDC tokens TanStack Blog.
- The exploit mechanism chained a
pull_request_targetmisconfiguration, cache poisoning, and memory extraction of GitHub Actions OIDC tokens to publish the packages SentinelOne. - TeamPCP was identified as the threat actor group responsible, utilizing Dune-themed repositories for dead-drop exfiltration of credentials Tenable.
- The malicious payloads exfiltrated highly sensitive developer tokens, cloud credentials (AWS/GCP/Azure), Kubernetes secrets, OIDC tokens, and SSH keys Endor Labs.
- JFrog reports the May 12 wave affected more than 170 npm packages and 2 PyPI packages, with more than 200 million weekly downloads across the affected package set JFrog May 12.
- JFrog reports the May 19 @antv/atool wave compromised 323 legitimate packages and that
@cap-js/[email protected]carried a related payload variant JFrog May 19.
- likely:
- The execution of Bun runtime smuggling where the malware downloads
setup_bun.jsto bypass traditional Node.js static scanners and run obfuscated payloads under the Bun engine Endor Labs. - The use of OIDC tokens within a trusted runner context to forge valid SLSA Build Level 3 provenance badges through Sigstore, creating cryptographically “verified” malicious releases Wiz.io.
- The execution of Bun runtime smuggling where the malware downloads
- unclear:
- The exact number of downstream developer systems fully wiped by the “dead-man switch” payload (
rm -rf /*) after credential revocation was initiated by security teams Microsoft Threat Intelligence.
- The exact number of downstream developer systems fully wiped by the “dead-man switch” payload (
- not_observed:
- Claims that npm or PyPI registry infrastructure was directly breached; all publishes resulted from OIDC token/credential exfiltration from developer environments and CI/CD pipelines TanStack Blog.
Impact Determination
| Classification | Criteria | Required evidence | Required action | Closure condition |
|---|---|---|---|---|
| Confirmed compromise | a package or release associated with Mini Shai-Hulud is present and package install, import, or build hook executes the worm payload or the reported process, file, or network indicators is observed. | Artifact inventory plus runtime telemetry showing package install, import, or build hook executes the worm payload or listed C2/process/file indicators. | Isolate affected hosts or runners, preserve artifacts, and rotate reachable credentials from a clean environment. | Affected artifacts are removed, exposed credentials are replaced, and downstream audit modules show no suspicious follow-on use. |
| Presumed exposed | a package or release associated with Mini Shai-Hulud was installed, pulled, imported, built, or executed during the exposure window, but telemetry cannot prove exfiltration. | Lockfile, package cache, workflow, image pull, extension inventory, build log, or deployment record tied to the exposure window. | Rebuild from clean artifacts and rotate credentials available to the affected environment. | Credential owners confirm revocation of old material and clean artifacts are deployed. |
| Potentially exposed | The package, workflow, image, extension, or module appears in dependency or deployment records, but package install, import, or build execution is not established. | Manifest, lockfile, build, deployment, or endpoint records plus a named telemetry gap. | Collect the missing execution and telemetry evidence before narrowing scope. | Every hit is dispositioned as confirmed compromise, presumed exposed, or not exposed. |
| Not exposed | No affected version, artifact, mutable reference, or indicator appears in source, lockfiles, build outputs, deployments, package caches, or runtime telemetry. | Repository search, dependency inventory, build/deployment export, package cache query, and runtime telemetry query results. | Preserve the negative search output and keep the prevention controls active. | Search evidence covers developer endpoints, CI runners, production deployments, and package or image caches. |
| Unknown | Required inventory, build, endpoint, network, or audit telemetry is unavailable. | A gap statement naming unavailable systems, owners, and time windows. | Keep the asset in scope and make conservative rotation or rebuild decisions for high-value environments. | The missing evidence is recovered or the risk owner accepts residual uncertainty. |
Minimum Evidence To Collect
minimum_evidence:
- "Dependency, workflow, extension, image, or module inventory covering developer endpoints, CI runners, and production deployments."
- "Positive or negative search results for campaign-specific malicious releases."
- "Execution evidence for package install, import, or build hook executes the worm payload."
- "Process, file, DNS, proxy, firewall, or package-manager telemetry for listed indicators."
- "Inventory of credentials, tokens, deployment paths, and downstream systems reachable from exposed environments."
Timeline
- 2026-04-20T00:00:00Z First anomalous publishes identified in the npm registry targeting enterprise developer utilities (specifically SAP developer packages) Endor Labs.
- 2026-04-24T00:00:00Z Endor Labs identifies the initial wave, tracing it to stolen npm publishing credentials. The malware payload is observed downloading the Bun runtime to evade standard analysis tools Endor Labs.
- 2026-05-10T00:00:00Z The worm spreads aggressively to high-profile developer packages including
@tanstack,@antv, and SDKs under Mistral AI, UiPath, and OpenSearch Tenable. - 2026-05-11T19:20:00Z Attacker publishes 84 malicious versions across 42
@tanstack/*packages on npm TanStack Blog. - 2026-05-11T19:26:00Z Publishing of malicious
@tanstackversions completes TanStack Blog. - 2026-05-12T00:00:00Z Wiz and Palo Alto Networks disclose that the malware is utilizing GitHub Actions cache poisoning and forging Sigstore provenance signatures to bypass CI/CD security filters Wiz.io.
- 2026-05-19T00:00:00Z Microsoft and Zscaler publish detailed hunting guides for persistent IDE hooks and LaunchAgents deployed by the worm Microsoft Threat Intelligence.
- 2026-05-19T01:39:00Z Attacker compromises the
atoolnpm maintainer account (the account that manages publishing rights for the entire@antvnpm scope) and begins publishing malicious versions across the @antv namespace Cremit.io. - 2026-05-19T02:06:00Z The @antv publishing blitz ends: 639 malicious package versions published across 323 unique npm packages in 27 minutes Cremit.io. Preinstall hooks invoking
bun run index.jsdeliver a 498 KB obfuscated payload that harvests credentials from 130+ local file paths and establishes dead-drop exfiltration toantvis/G2repository branches via the GitHub API, witht.m-kosche[.]comas fallback C2 Chainguard StepSecurity. - 2026-05-19: JFrog publishes its @antv follow-up and adds
@cap-js/[email protected]as a related variant with a distinct indirect delivery technique JFrog May 19. - 2026-05-23T01:00:00Z Nx Console release 18.95.1 is published to patch downstream contamination resulting from the compromise of a contributor’s hijacked token SentinelOne.
What Happened
In late April 2026, the TeamPCP threat actor group launched the “Mini Shai-Hulud” supply chain campaign, targeting widely used npm and PyPI developer dependencies Endor Labs. The campaign escalated dramatically on May 11, 2026, when the worm compromised the @tanstack npm scope, publishing 84 poisoned versions of 42 libraries, including @tanstack/router and @tanstack/react-query TanStack Blog.
Instead of stealing static npm credentials, the worm targeted the repository’s GitHub Actions pipeline SentinelOne. By submitting a malicious pull request to the TanStack/router repository, the worm triggered a misconfigured pull_request_target workflow SentinelOne. Due to a cache poisoning vulnerability where fork and base workflows shared execution caches, the attacker poisoned the base branch’s cache SentinelOne. When the trusted base workflow executed, it read the poisoned cache, allowing the worm to inject malicious code directly into the runner environment SentinelOne.
The worm then extracted the runner’s OpenID Connect (OIDC) token from active system memory Wiz.io. This short-lived OIDC token is trusted by the npm registry as a federated publisher identity Wiz.io. Using this token, the attacker signed the malicious release with Sigstore, obtaining a valid SLSA Build Level 3 provenance badge Wiz.io. This caused automated compliance tools and security scanners to trust the artifact’s lineage since the signature and provenance matched the official, legitimate GitHub Actions pipeline Wiz.io.
Once published, any developer executing npm install for the affected packages fell victim Endor Labs. The package lifecycle hooks executed a script that scraped host memory, environment variables, local cloud configurations, and personal access tokens Endor Labs. These stolen secrets were exfiltrated back to TeamPCP by creating public, encrypted repositories on the developer’s own hijacked GitHub account, named with distinct Dune-themed concepts to avoid detection Tenable.
Technical Analysis
Initial Access
The primary vector for initial access in the high-profile TanStack breach was the exploitation of a pull_request_target misconfiguration combined with GitHub Actions cache poisoning SentinelOne. An external pull request triggered the workflow in a context that had access to elevated repository secrets or OIDC trusted-publishing privileges SentinelOne. The attacker exploited a weakness where the dependency or build cache was shared between untrusted forks and trusted base runs, enabling cache poisoning SentinelOne.
Package or Artifact Manipulation
Once execution within the runner was achieved, the worm injected the script router_init.js (for npm) or setup.mjs directly into the package structure before release generation TanStack Blog. In the PyPI ecosystem, a similar payload named transformers.pyz was added to standard Python wheels Endor Labs. The attacker manipulated the package manifests (package.json) to register these files as preinstall or postinstall lifecycle hooks Endor Labs.
Execution Trigger
When downstream developers ran dependency installation commands like npm install or pip install, the package manager automatically executed the lifecycle script under administrative or user privileges Endor Labs. In Python environments, the payload triggered during wheel unpacking or package import time Endor Labs.
Payload Behavior
The malware checks the host environment to determine its runtime:
- Bun Runtime Smuggling: It searches for the
bunbinary. If missing, it downloads a lightweight standalone Bun engine (setup_bun.js) from an attacker-controlled staging server Endor Labs. The main payload is executed inside Bun rather than Node.js, successfully bypassing security monitors that hook only Node.js processes or analyze standard V8 engine calls Endor Labs. - Credential Harvesting: The script scans the system for cloud credentials (AWS keys, Google Cloud JSON files, Azure profiles), container keys (Kubernetes secrets),
.npmrcregistry publishing tokens, SSH private keys, and environment variables Endor Labs. - IDE and Coding Assistant Hijacking: To establish deep local persistence, the worm targets developer tools Microsoft Threat Intelligence. It appends malicious run tasks to the developer’s
.vscode/tasks.jsonfile so that opening the workspace triggers secret exfiltration Microsoft Threat Intelligence. It also places a wrapper inside.claude/settings.jsonto intercept inputs and commands passed to AI coding tools, monitoring for project structure and secrets Microsoft Threat Intelligence. - OS Persistence: It installs a background service named
gh-token-monitorvia a plist daemon in macOS (~/Library/LaunchAgents/) or a systemd unit in Linux to intercept newly generated session tokens Microsoft Threat Intelligence. - Dead-Man Switch / Anti-Analysis Wipe: If the payload detects a sandbox environment (e.g., standard VM analysis indicators) or if a query to its C2 reveals that the stolen publishing token has been revoked, it launches a destructive system command (
rm -rf /*) to destroy evidence and disrupt incident response Microsoft Threat Intelligence.
Exfiltration / C2
domains:
- "filev2.getsession[.]org"
- "api.masscan[.]cloud"
- "git-tanstack[.]com"
- "t.m-kosche[.]com"
ips:
- "N/A"
urls:
- "https://filev2.getsession[.]org/upload"
- "https://api.masscan[.]cloud/ping"
protocols:
- "HTTPS"
- "DNS"
endpoints:
- "/upload"
- "/ping"
confidence: high
Propagation
The worm is self-propagating Tenable. Upon harvesting npm and PyPI publishing tokens from compromised workstations or CI runners, it sends them back to the C2 or uses them locally to identify other repositories accessible to that developer Tenable. The malware automatically modifies those downstream packages, signs them, and publishes infected versions to the registry under the developer’s identity, continuing its exponential spread across the supply chain Tenable.
Obfuscation or Evasion
In addition to Bun runtime smuggling, the worm hides its payload using complex XOR obfuscation and packs its Python components into a zipapp (transformers.pyz) Endor Labs. The use of forged Sigstore certificates allows it to bypass cryptographic strictness policies that require verified build provenance Wiz.io.
Worm Self-Propagation Lifecycle
The following architectural flowchart details the self-replicating loop utilized by the TeamPCP worm to propagate through GitHub, package registries, and developer workstations:
graph TD
classDef attacker fill:#f96,stroke:#333,stroke-width:2px;
classDef target fill:#9cf,stroke:#333,stroke-width:2px;
classDef victim fill:#fcf,stroke:#333,stroke-width:2px;
classDef sigstore fill:#ff9,stroke:#333,stroke-width:2px;
Attacker[1. TeamPCP Attacker]:::attacker
TargetRepo[2. Target Repository <br/> e.g. TanStack / antv]:::target
ForkPR[3. Malicious PR from Fork]:::target
GHActions[4. GitHub Actions Runner]:::target
Sigstore[5. Sigstore OIDC Signer]:::sigstore
NPMPyPI[6. Package Registry npm / PyPI]:::sigstore
DevWorkstation[7. Victim Developer Workstation]:::victim
Attacker -- "Submits PR" --> TargetRepo
TargetRepo -- "Triggers pull_request_target" --> ForkPR
ForkPR -- "Cache Poisoning Exploit" --> GHActions
GHActions -- "Steals OIDC Token" --> Sigstore
Sigstore -- "Signs Poisoned Artifact" --> NPMPyPI
NPMPyPI -- "npm/pip install" --> DevWorkstation
subgraph Workstation Compromise
DevWorkstation -- "1. Scrapes Tokens & Keys" --> LocalSecrets[Stolen npm/PyPI & Github Tokens]
DevWorkstation -- "2. Local Persistence" --> IDE[IDE vscode/tasks.json & LaunchAgents]
DevWorkstation -- "3. Worm Execution" --> Propagation[Propagation Module]
end
Propagation -- "Injects Malicious Updates" --> TargetRepo
LocalSecrets -- "Exfiltrated to" --> Attacker
Affected Assets and Blast Radius
affected_assets:
ecosystems:
- "npm"
- "PyPI"
packages:
- "@tanstack/router"
- "@tanstack/react-query"
- "@tanstack/store"
- "@antv/g2"
- "@antv/g6"
- "@sap/cds"
- "@sap/cds-dk"
- "opensearch-py"
- "lite-llm"
- "nx-console"
versions:
- "@tanstack/* published on May 11, 2026"
- "[email protected]"
repositories:
- "github.com/TanStack/router"
- "github.com/antvis/*"
- "github.com/nrwl/nx-console"
container_images:
- "N/A"
CI_CD_systems:
- "GitHub Actions"
developer_tools:
- "VS Code"
- "Claude Code"
environments:
- developer workstations
- CI runners
- build pipelines
- containers
- production systems
credentials_at_risk:
- npm tokens
- GitHub tokens
- cloud credentials
- SSH keys
- environment variables
not_currently_known_to_affect:
- Non-NodeJS/Non-Python development environments lacking Bun and Python command-line tools.
Indicators of Compromise
domains:
- value: filev2.getsession[.]org
source: https://www.endorlabs.com/blog/mini-shai-hulud-npm-worm-hits-sap-developer-packages
confidence: high
- value: api.masscan[.]cloud
source: https://www.endorlabs.com/blog/mini-shai-hulud-npm-worm-hits-sap-developer-packages
confidence: high
- value: git-tanstack[.]com
source: https://tanstack.com/blog/postmortem-cve-2026-45321
confidence: high
- value: t.m-kosche[.]com
source: https://www.endorlabs.com/blog/mini-shai-hulud-npm-worm-hits-sap-developer-packages
confidence: high
ips: []
urls:
- value: https://filev2.getsession[.]org/upload
source: https://www.endorlabs.com/blog/mini-shai-hulud-npm-worm-hits-sap-developer-packages
confidence: high
- value: https://api.masscan[.]cloud/ping
source: https://www.endorlabs.com/blog/mini-shai-hulud-npm-worm-hits-sap-developer-packages
confidence: high
hashes:
- value: ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c
source: https://www.endorlabs.com/blog/mini-shai-hulud-npm-worm-hits-sap-developer-packages
confidence: high
files:
- value: router_init.js
source: https://tanstack.com/blog/postmortem-cve-2026-45321
confidence: high
- value: setup_bun.js
source: https://www.endorlabs.com/blog/mini-shai-hulud-npm-worm-hits-sap-developer-packages
confidence: high
- value: bun_environment.js
source: https://www.endorlabs.com/blog/mini-shai-hulud-npm-worm-hits-sap-developer-packages
confidence: high
- value: transformers.pyz
source: https://www.endorlabs.com/blog/mini-shai-hulud-npm-worm-hits-sap-developer-packages
confidence: high
- value: gh-token-monitor
source: https://www.microsoft.com/en-us/security/blog/hunting-the-shai-hulud-supply-chain-worm
confidence: high
package_versions:
- value: [email protected]
source: https://www.sentinelone.com/blog/anatomy-of-cve-2026-45321
confidence: high
Detection and Hunting
Script: local repository and exported telemetry scope
#!/usr/bin/env python3
import os
import sys
import json
import subprocess
from pathlib import Path
ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
LOG_ROOT = os.environ.get("LOG_ROOT", "")
OUT = Path(os.environ.get("OUT", "hp-mini-shai-hulud-worm-scope"))
SINCE = "2026-04-20T00:00:00Z"
UNTIL = "2026-05-23T23:59:59Z"
PACKAGES = [
"@tanstack/react-router",
"@tanstack/vue-router",
"@tanstack/solid-router",
"@tanstack/react-start",
"@tanstack/router-core",
"@antv/g2",
"@antv/g6",
"@antv/x6",
"@antv/l7",
"@antv/s2",
"@antv/f2",
"echarts-for-react",
"timeago.js",
"size-sensor",
"canvas-nest.js",
"@sap/cds",
"@sap/cds-dk",
"opensearch-py",
"lite-llm",
"nx-console",
]
VERSIONS = [
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@antv/[email protected]",
"@antv/[email protected]",
"[email protected]",
"@antv/* published 2026-05-19T01:39:00",
]
FILES = [
"router_init.js",
"setup_bun.js",
"bun_environment.js",
"transformers.pyz",
"gh-token-monitor",
]
DOMAINS = [
"filev2.getsession.org",
"api.masscan.cloud",
"git-tanstack.com",
"t.m-kosche.com",
"www.endorlabs.com",
"www.microsoft.com",
"www.sentinelone.com",
]
URLS = [
"https://filev2.getsession.org/upload",
"https://api.masscan.cloud/ping",
"https://www.endorlabs.com/blog/mini-shai-hulud-npm-worm-hits-sap-developer-packages",
"https://tanstack.com/blog/postmortem-cve-2026-45321",
"https://www.microsoft.com/en-us/security/blog/hunting-the-shai-hulud-supply-chain-worm",
"https://www.sentinelone.com/blog/anatomy-of-cve-2026-45321",
]
IPS = [
]
HASHES = [
"ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c",
]
PROCESS_PATTERNS = [
]
NETWORK_PATTERNS = [
]
# Positive signal: repository, lockfile, artifact, process, or network telemetry contains one of the exact incident selectors above.
# Escalation: any match tied to a production build, CI run, deployed asset, or secret-bearing host moves the asset to presumed exposed.
OUT.mkdir(parents=True, exist_ok=True)
indicators_file = OUT / "indicators.txt"
# Collect unique indicators
indicators = set()
for group in [PACKAGES, VERSIONS, FILES, DOMAINS, URLS, IPS, HASHES, PROCESS_PATTERNS, NETWORK_PATTERNS]:
for val in group:
if val:
indicators.add(val)
with open(indicators_file, "w") as f:
for ind in sorted(indicators):
f.write(ind + "\n")
print(f"[+] Written unique selectors to {indicators_file}")
# Walk local directory
print(f"[+] Scanning directory: {ROOT} for selectors...")
matches = []
exclude_dirs = {"node_modules", "vendor", "dist", ".git"}
for root, dirs, filenames in os.walk(ROOT):
dirs[:] = [d for d in dirs if d not in exclude_dirs]
for filename in filenames:
filepath = Path(root) / filename
try:
content = filepath.read_text(errors="ignore")
for ind in indicators:
if ind in content:
matches.append(f"{filepath}: found '{ind}'")
except Exception:
pass
if matches:
(OUT / "repository-indicator-matches.txt").write_text("\n".join(matches) + "\n")
print(f"[!] Found {len(matches)} matches in codebase!")
# Optional Log Scanning
if LOG_ROOT and os.path.exists(LOG_ROOT):
print(f"[+] Scanning telemetry log directory: {LOG_ROOT}...")
log_matches = []
for root, _, filenames in os.walk(LOG_ROOT):
for filename in filenames:
filepath = Path(root) / filename
try:
content = filepath.read_text(errors="ignore")
for ind in indicators:
if ind in content:
log_matches.append(f"{filepath}: found '{ind}'")
except Exception:
pass
if log_matches:
(OUT / "exported-telemetry-indicator-matches.txt").write_text("\n".join(log_matches) + "\n")
print(f"[!] Found {len(log_matches)} matches in logs!")
if PACKAGES:
registry_dir = OUT / "registry"
registry_dir.mkdir(exist_ok=True)
for package in PACKAGES:
if not package: continue
safe_name = package.replace("/", "__")
print(f"[+] Querying npm view for {package}...")
res = subprocess.run(["npm", "view", package, "name", "version", "time", "versions", "dist-tags", "maintainers", "dist.tarball", "dist.integrity", "scripts", "--json"], capture_output=True, text=True)
if res.returncode == 0:
(registry_dir / f"npm-{safe_name}.json").write_text(res.stdout)
for package in PACKAGES:
if not package: continue
safe_name = package.replace("/", "__")
print(f"[+] Querying pip index for {package}...")
res = subprocess.run(["python3", "-m", "pip", "index", "versions", package], capture_output=True, text=True)
if res.returncode == 0:
(registry_dir / f"pypi-{safe_name}-versions.txt").write_text(res.stdout)
subprocess.run(["python3", "-m", "pip", "download", "--no-deps", package, "-d", str(registry_dir)], capture_output=True)
print(f"[+] Wrote scope artifacts under {OUT}")
Downstream Abuse Audits
Script: GitHub organization run, release, secret, and workflow audit
#!/usr/bin/env python3
import os
import sys
import json
import subprocess
from pathlib import Path
if "ORG" not in os.environ:
print("ERROR: Set ORG environment variable to the GitHub organization to audit", file=sys.stderr)
sys.exit(1)
ORG = os.environ["ORG"]
SINCE = "2026-04-20T00:00:00Z"
UNTIL = "2026-05-23T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-mini-shai-hulud-worm-github-audit"))
SELECTORS = [
"@tanstack/react-router",
"@tanstack/vue-router",
"@tanstack/solid-router",
"@tanstack/react-start",
"@tanstack/router-core",
"@antv/g2",
"@antv/g6",
"@antv/x6",
"@antv/l7",
"@antv/s2",
"@antv/f2",
"echarts-for-react",
"timeago.js",
"size-sensor",
"canvas-nest.js",
"@sap/cds",
"@sap/cds-dk",
"opensearch-py",
"lite-llm",
"nx-console",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@antv/[email protected]",
"@antv/[email protected]",
"[email protected]",
"@antv/* published 2026-05-19T01:39:00",
"router_init.js",
"setup_bun.js",
"bun_environment.js",
"transformers.pyz",
"gh-token-monitor",
"filev2.getsession.org",
"api.masscan.cloud",
"git-tanstack.com",
"t.m-kosche.com",
"www.endorlabs.com",
"www.microsoft.com",
"www.sentinelone.com",
"https://filev2.getsession.org/upload",
"https://api.masscan.cloud/ping",
"https://www.endorlabs.com/blog/mini-shai-hulud-npm-worm-hits-sap-developer-packages",
"https://tanstack.com/blog/postmortem-cve-2026-45321",
"https://www.microsoft.com/en-us/security/blog/hunting-the-shai-hulud-supply-chain-worm",
"https://www.sentinelone.com/blog/anatomy-of-cve-2026-45321",
"ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c",
]
# Positive signal: a workflow run, release, secret, key, package, or workflow change overlaps the exposure window and references an incident selector.
# Remediation trigger: unauthorized post-exposure write activity or a secret-bearing run matching an incident selector requires token revocation and downstream cloud/registry review.
OUT.mkdir(parents=True, exist_ok=True)
(OUT / "runs").mkdir(exist_ok=True)
(OUT / "logs").mkdir(exist_ok=True)
(OUT / "repos").mkdir(exist_ok=True)
# 1. Write incident-selectors file
selectors_file = OUT / "incident-selectors.txt"
with open(selectors_file, "w") as sf:
for s in SELECTORS:
if s:
sf.write(s + "\n")
# 2. Get list of repos
print(f"[+] Fetching repositories for organization: {ORG}")
repo_res = subprocess.run(["gh", "repo", "list", ORG, "--limit", "1000", "--json", "nameWithOwner"], capture_output=True, text=True)
if repo_res.returncode != 0:
print(f"[-] Failed to fetch repos: {repo_res.stderr}", file=sys.stderr)
sys.exit(1)
repos = [r["nameWithOwner"] for r in json.loads(repo_res.stdout)]
for repo in repos:
safe_repo = repo.replace("/", "__")
print(f"[+] Auditing repository: {repo}")
# Check runs in the window
runs_res = subprocess.run([
"gh", "api", f"/repos/{repo}/actions/runs",
"-f", "per_page=100",
"-f", f"created=>={SINCE}",
"--paginate"
], capture_output=True, text=True)
if runs_res.returncode == 0:
try:
all_runs = json.loads(runs_res.stdout).get("workflow_runs", [])
filtered_runs = [r for r in all_runs if r["created_at"] <= UNTIL]
if filtered_runs:
with open(OUT / "runs" / f"{safe_repo}-runs.jsonl", "w") as rf:
for run in filtered_runs:
rf.write(json.dumps(run) + "\n")
# Fetch log dynamically
run_id = str(run["id"])
log_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--log"], capture_output=True, text=True)
if log_res.returncode == 0:
(OUT / "logs" / f"{safe_repo}-{run_id}.log").write_text(log_res.stdout)
# Fetch details
view_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--json", "databaseId,workflowName,headSha,event,createdAt,jobs"], capture_output=True, text=True)
if view_res.returncode == 0:
(OUT / "runs" / f"{safe_repo}-{run_id}.json").write_text(view_res.stdout)
except Exception as e:
print(f"[-] Error parsing runs for {repo}: {e}")
# Check releases in window
subprocess.run(["gh", "api", f"/repos/{repo}/releases", "-f", "per_page=100", "--paginate"], capture_output=True)
# Check repo secrets updated in window
subprocess.run(["gh", "api", f"/repos/{repo}/actions/secrets", "-f", "per_page=100", "--paginate"], capture_output=True)
# Check deploy keys
subprocess.run(["gh", "api", f"/repos/{repo}/keys", "-f", "per_page=100", "--paginate"], capture_output=True)
# Scan output directory for any indicator selector matches
print("[+] Scanning gathered telemetry for indicator matches...")
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(OUT)], capture_output=False)
print(f"[+] Wrote GitHub audit artifacts under {OUT}")
Script: cloud OIDC and deployment credential follow-on audit
#!/usr/bin/env python3
import os
import json
import subprocess
from pathlib import Path
SINCE = "2026-04-20T00:00:00Z"
UNTIL = "2026-05-23T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-mini-shai-hulud-worm-cloud-audit"))
AWS_REGIONS = os.environ.get("AWS_REGIONS", "us-east-1").split(",")
# Positive signal: token exchange or privileged write activity occurs in the exposure window from GitHub, CI/CD, package registry, or deployment automation identity.
# Remediation trigger: unexpected write, deploy, IAM, secret, or registry activity tied to an exposed CI/CD path requires trust-policy disablement and credential rotation.
OUT.mkdir(parents=True, exist_ok=True)
# 1. AWS CloudTrail Audit
print("[+] Querying AWS CloudTrail for Web Identity token exchanges...")
aws_events = []
for region in AWS_REGIONS:
res = subprocess.run([
"aws", "cloudtrail", "lookup-events",
"--region", region,
"--start-time", SINCE,
"--end-time", UNTIL,
"--lookup-attributes", "AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity",
"--output", "json"
], capture_output=True, text=True)
if res.returncode == 0:
try:
events = json.loads(res.stdout).get("Events", [])
for event in events:
ct = json.loads(event.get("CloudTrailEvent", "{}"))
ct["region"] = region
aws_events.append(ct)
except Exception as e:
print(f"[-] Error parsing AWS CloudTrail events: {e}")
if aws_events:
with open(OUT / "aws-assume-role-with-web-identity.jsonl", "w") as f:
for ev in aws_events:
f.write(json.dumps(ev) + "\n")
# Audit follow-on events for returned access keys
for ev in aws_events:
access_key = ev.get("responseElements", {}).get("credentials", {}).get("accessKeyId")
region = ev.get("region", "us-east-1")
if access_key:
print(f"[+] Enumerating AWS events for AccessKey: {access_key}")
f_res = subprocess.run([
"aws", "cloudtrail", "lookup-events",
"--region", region,
"--start-time", SINCE,
"--end-time", UNTIL,
"--lookup-attributes", f"AttributeKey=AccessKeyId,AttributeValue={access_key}",
"--output", "json"
], capture_output=True, text=True)
if f_res.returncode == 0:
try:
f_events = json.loads(f_res.stdout).get("Events", [])
with open(OUT / "aws-follow-on-api-calls.jsonl", "a") as ff:
for fe in f_events:
ff.write(fe.get("CloudTrailEvent", "{}") + "\n")
except Exception as e:
print(f"[-] Error writing follow-on events: {e}")
# 2. Azure Activity Log Audit
print("[+] Querying Azure activity logs...")
az_res = subprocess.run([
"az", "monitor", "activity-log", "list",
"--start-time", SINCE,
"--end-time", UNTIL,
"--query", "[?contains(operationName.value, 'write') || contains(operationName.value, 'delete') || contains(operationName.value, 'Microsoft.ManagedIdentity')]",
"-o", "json"
], capture_output=True, text=True)
if az_res.returncode == 0:
(OUT / "azure-write-delete-activity.json").write_text(az_res.stdout)
# 3. GCP Logging Audit
print("[+] Querying GCP Cloud Logging...")
gcp_filter = f'timestamp>="{SINCE}" AND timestamp<="{UNTIL}" AND (protoPayload.methodName="google.sts.v1.SecurityTokenService.ExchangeToken" OR protoPayload.methodName:"GenerateAccessToken" OR protoPayload.methodName:"CreateServiceAccountKey" OR protoPayload.methodName:"SetIamPolicy")'
gcp_res = subprocess.run([
"gcloud", "logging", "read", gcp_filter,
"--format", "json"
], capture_output=True, text=True)
if gcp_res.returncode == 0:
(OUT / "gcp-token-and-iam-activity.json").write_text(gcp_res.stdout)
print(f"[+] Wrote cloud audit artifacts under {OUT}")
Script: registry metadata and artifact audit
#!/usr/bin/env python3
import os
import json
import subprocess
from pathlib import Path
SINCE = "2026-04-20T00:00:00Z"
OUT = Path(os.environ.get("OUT", "hp-mini-shai-hulud-worm-registry-audit"))
PACKAGES = [
"@tanstack/react-router",
"@tanstack/vue-router",
"@tanstack/solid-router",
"@tanstack/react-start",
"@tanstack/router-core",
"@antv/g2",
"@antv/g6",
"@antv/x6",
"@antv/l7",
"@antv/s2",
"@antv/f2",
"echarts-for-react",
"timeago.js",
"size-sensor",
"canvas-nest.js",
"@sap/cds",
"@sap/cds-dk",
"opensearch-py",
"lite-llm",
"nx-console",
]
VERSIONS = [
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@tanstack/[email protected]",
"@antv/[email protected]",
"@antv/[email protected]",
"[email protected]",
"@antv/* published 2026-05-19T01:39:00",
]
# Positive signal: registry metadata, package tarballs, or cached artifacts contain the exact affected package/version values.
# Remediation trigger: any internal package cache, build artifact, or deployment using these package/version values requires exposure scoping.
OUT.mkdir(parents=True, exist_ok=True)
with open(OUT / "affected-versions.txt", "w") as av:
for version in VERSIONS:
if version:
av.write(version + "\n")
# 1. Audit npm dependencies in lockfiles/package.json
print("[+] Scanning lockfiles for npm selectors...")
for file in ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "package.json"]:
if Path(file).exists():
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), file])
# 2. Query registry metadata and fetch tarballs for local analysis
metadata_dir = OUT / "metadata"
tarballs_dir = OUT / "tarballs"
metadata_dir.mkdir(exist_ok=True)
tarballs_dir.mkdir(exist_ok=True)
for package in PACKAGES:
if not package: continue
safe_name = package.replace("/", "__")
print(f"[+] Querying npm view for {package}...")
res = subprocess.run(["npm", "view", package, "time", "versions", "dist-tags", "maintainers", "dist.tarball", "dist.integrity", "scripts", "--json"], capture_output=True, text=True)
if res.returncode == 0:
(metadata_dir / f"npm-{safe_name}.json").write_text(res.stdout)
subprocess.run(["npm", "pack", package, "--pack-destination", str(tarballs_dir)], capture_output=True)
# 3. HOW TO REVOKE AND ROTATE EXPOSED NPM PUBLISHING TOKENS:
# Revoke all compromised tokens via npm CLI:
# subprocess.run(["npm", "token", "list"])
# subprocess.run(["npm", "token", "revoke", "123456"])
# Or logout to revoke the current session:
# subprocess.run(["npm", "logout"])
# Generate a new publishing token with MFA protection:
# subprocess.run(["npm", "token", "create", "--read-only=false", "--cidr=0.0.0.0/0"])
print(f"[+] Wrote registry audit artifacts under {OUT}")
Campaign Update: @antv / atool npm Account Hijack — May 19, 2026
On May 19, 2026, the Mini Shai-Hulud campaign added its most voluminous single wave: the compromise of the atool npm maintainer account, which held publishing rights over the entire @antv npm scope — Alibaba’s widely used open-source data visualization suite Orca Security. In a 27-minute window between 01:39 and 02:06 UTC, TeamPCP used the hijacked account to publish 639 malicious versions across 323 unique npm packages Cremit.io. The blast radius dwarfed the earlier TanStack wave in raw version count by nearly eight-to-one.
Unlike the TanStack event — which exploited OIDC token theft via CI/CD cache poisoning — this wave exploited direct credential compromise of a single maintainer account (atool) that had been granted org-level publish rights across the entire @antv scope. The attacker did not need to touch GitHub Actions at all. Gaining control of one npm account with org-level permissions was sufficient to poison every downstream package simultaneously Palo Alto Networks Unit42.
Attack Timeline
- 2026-05-19T01:39:00Z — First malicious version published to npm under the @antv scope via the compromised
atoolaccount. - 2026-05-19T02:06:00Z — Final malicious version published; 639 versions across 323 packages in 27 minutes.
Execution Mechanism
Every poisoned package contained a preinstall lifecycle script set to "bun run index.js". The payload (index.js) was a single, heavily obfuscated 498 KB JavaScript file designed to execute under the Bun runtime rather than Node.js, thereby evading security tools that hook only the V8/Node process tree Chainguard. Any developer running npm install, yarn, or pnpm install for any of the 323 affected packages triggered payload execution immediately — before the package’s own code was loaded.
Key packages confirmed in-scope include: @antv/g2, @antv/g6, @antv/x6, @antv/l7, @antv/s2, @antv/f2, echarts-for-react, timeago.js, size-sensor, and canvas-nest.js, plus 313 additional packages across the @antv namespace Orca Security.
Credential Harvesting — 130+ File Paths
The obfuscated payload conducted a sweep of the victim’s local filesystem targeting more than 130 specific file paths associated with cloud provider credentials, developer tokens, and SSH keys StepSecurity. Target categories included:
| Credential Category | Example Paths Targeted |
|---|---|
| AWS | ~/.aws/credentials, ~/.aws/config |
| GCP | ~/.config/gcloud/application_default_credentials.json |
| Azure | ~/.azure/accessTokens.json, ~/.azure/azureProfile.json |
| Kubernetes | ~/.kube/config |
| SSH keys | ~/.ssh/id_rsa, ~/.ssh/id_ed25519 |
| npm tokens | ~/.npmrc |
| GitHub tokens | ~/.config/gh/hosts.yml, GITHUB_TOKEN env var |
Persistence
Consistent with prior Mini Shai-Hulud waves, the payload established persistence via:
- VS Code — appended a malicious run task to
.vscode/tasks.jsonin the active workspace. - Claude Code — injected into
~/.claude/settings.jsonto intercept AI coding assistant commands. - OS daemons — created a
gh-token-monitorLaunchAgent (macOS~/Library/LaunchAgents/) or systemd unit (Linux) to harvest newly generated session tokens.
Dual-Channel Exfiltration
The May 19 wave introduced a more sophisticated exfiltration architecture than earlier waves Chainguard:
Primary channel — GitHub API dead-drop: Harvested credentials were AES-encrypted and committed to newly created branches in the legitimate antvis/G2 GitHub repository via the GitHub API, using the victim’s own stolen GitHub token. This technique abuses a trusted public repository as a covert data sink, making the outbound traffic appear as normal GitHub API calls to monitoring tools StepSecurity.
Fallback channel — HTTPS POST to t.m-kosche[.]com: If the dead-drop write failed (e.g., the GitHub token lacked repo write scope), the payload fell back to posting encrypted data to t.m-kosche[.]com disguised as OpenTelemetry trace data, using the Content-Type: application/x-protobuf header to blend with legitimate observability traffic Chainguard. This domain was already a known Mini Shai-Hulud IOC from prior waves; its appearance here confirms it as shared infrastructure across multiple TeamPCP operations.
Worm Propagation
With stolen npm tokens from victim environments, the worm searched for additional packages the victim had publish rights to and recursively poisoned them — the same self-propagating behavior documented in the TanStack wave Palo Alto Networks Unit42. The 323-package blast radius in 27 minutes reflects both the number of packages under the atool account’s control and automated worm propagation using tokens harvested from systems compromised in earlier waves.
Attribution
Palo Alto Networks Unit42 attributes this wave explicitly to TeamPCP, correlating the t.m-kosche[.]com C2 infrastructure, the Bun runtime smuggling technique, the GitHub API dead-drop pattern, and the 130+ credential file path list with prior documented Mini Shai-Hulud infrastructure Palo Alto Networks Unit42.
New IOCs — May 19, 2026 Wave
dead_drop:
- repository: antvis/G2
mechanism: "GitHub API branch writes using victim's stolen GitHub token"
confidence: high
source: https://www.stepsecurity.io/blog/atool-antv-npm-supply-chain-attack
domains:
- value: t.m-kosche.com
role: "fallback C2; HTTPS POST disguised as OpenTelemetry trace data"
confidence: high
source: https://www.chainguard.dev/unchained/atool-antv-npm-supply-chain
files:
- value: index.js
description: "498 KB obfuscated JavaScript payload; executed via 'bun run index.js' preinstall hook"
confidence: high
source: https://orca.security/resources/blog/atool-antv-npm-supply-chain-attack/
package_versions:
- value: "@antv/* (639 versions across 323 packages)"
published_window: "2026-05-19T01:39:00Z / 2026-05-19T02:06:00Z"
confidence: high
source: https://cremit.io/blog/antv-npm-supply-chain/
Sources
- TanStack Blog. Role: DIRECT_SOURCE Impact: Detailed explanation of the TanStack compromise, the exact exposure window (19:20 - 19:26 UTC), affected packages, and the OIDC exploitation chain.
- SentinelOne. Role: PRIMARY_RESEARCH Impact: Deep technical walkthrough of the cache-poisoning exploit, the
pull_request_targetmisconfiguration, and downstream Nx Console compromise. - Endor Labs. Role: PRIMARY_RESEARCH Impact: Initial discovery of the worm’s SAP CAP targets, Bun runtime smuggling, and Python
transformers.pyzpayload. - Wiz.io. Role: PRIMARY_RESEARCH Impact: Analysis of the Sigstore SLSA Build Level 3 provenance forgery and the federation bypass.
- Tenable. Role: SECONDARY_ANALYSIS Impact: Discussion of TeamPCP’s Dune-themed indicators, exfiltration repositories, and worm-like lateral movement.
- Microsoft Threat Intelligence. Role: PRIMARY_RESEARCH Impact: Identification of the persistent IDE task hooks, macOS/Linux LaunchAgent services, and the credential revocation dead-man switch behavior.
- Aikido Security. Role: SECONDARY_ANALYSIS Impact: Overall synthesis of the massive May 2026 supply chain wave and remediation recommendations.
- Orca Security. Role: PRIMARY_RESEARCH Impact: Technical analysis of the 323-package @antv namespace compromise, 27-minute attack window, preinstall hook execution via Bun, credential harvesting from 130+ file paths.
- Palo Alto Networks Unit42. Role: PRIMARY_RESEARCH Impact: Attribution to TeamPCP Mini Shai-Hulud campaign, correlation with prior waves, worm propagation analysis.
- Chainguard. Role: PRIMARY_RESEARCH Impact: Payload analysis: 498 KB obfuscated JS, Bun execution, antvis/G2 dead-drop via GitHub API, t.m-kosche.com fallback C2.
- StepSecurity. Role: DIRECT_SOURCE Impact: Dead-drop commit analysis in antvis/G2 repo, 130+ credential file paths targeted, GitHub API exfiltration mechanism.
- Cremit.io. Role: SECONDARY_ANALYSIS Impact: 01:39–02:06 UTC attack window timing, 639 malicious versions across 323 packages, package list.
- JFrog: Shai-Hulud, Here We Go Again. Role: PRIMARY_RESEARCH Impact: Broader May 12 campaign scope across 170+ npm packages and 2 PyPI packages with 200M+ weekly downloads, credential theft, encrypted exfiltration, and destructive dead-man switch behavior.
- JFrog: Shai-Hulud Returns: npm Worm hits @antv. Role: PRIMARY_RESEARCH Impact: Confirms 323 legitimate packages in the @antv/atool wave and identifies
@cap-js/[email protected]as a related payload variant.
IOC Clipboard
19 IOCsfilev2.getsession.org filev2[.]getsession[.]org api.masscan.cloud api[.]masscan[.]cloud git-tanstack.com git-tanstack[.]com t.m-kosche.com t[.]m-kosche[.]com www.endorlabs.com www[.]endorlabs[.]com www.microsoft.com www[.]microsoft[.]com www.sentinelone.com www[.]sentinelone[.]com https://filev2.getsession.org/upload hxxps://filev2[.]getsession[.]org/upload https://api.masscan.cloud/ping hxxps://api[.]masscan[.]cloud/ping https://www.endorlabs.com/blog/mini-shai-hulud-npm-worm-hits-sap-developer-packages hxxps://www[.]endorlabs[.]com/blog/mini-shai-hulud-npm-worm-hits-sap-developer-packages https://tanstack.com/blog/postmortem-cve-2026-45321 hxxps://tanstack[.]com/blog/postmortem-cve-2026-45321 https://www.microsoft.com/en-us/security/blog/hunting-the-shai-hulud-supply-chain-worm hxxps://www[.]microsoft[.]com/en-us/security/blog/hunting-the-shai-hulud-supply-chain-worm https://www.sentinelone.com/blog/anatomy-of-cve-2026-45321 hxxps://www[.]sentinelone[.]com/blog/anatomy-of-cve-2026-45321 ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c router_init.js router_init.js setup_bun.js setup_bun.js bun_environment.js bun_environment.js transformers.pyz transformers.pyz gh-token-monitor gh-token-monitor