name: Bun Runtime Spawned During npm Package Installation Analytic
category: 'Execution'
threatname: 'Command and Scripting Interpreter: JavaScript'
functionality: 'Endpoint Management Systems'
description: |
Detects the Bun JavaScript runtime being silently installed and executed during an npm
package installation lifecycle hook, consistent with the Mini Shai-Hulud worm (TeamPCP).
The attack embeds a prepare script in a malicious optional dependency (@tanstack/setup)
that installs Bun 1.3.13 silently, then executes a 2.3 MB obfuscated payload via bun run.
Bun is used specifically because it lacks the --require hook interception used by most
Node.js security and monitoring tools, and because its Bun.gunzipSync function is required
to decompress the AES-256-GCM encrypted secondary payloads. Bun being spawned as a child
of a node/npm install process with known worm payload filenames in the CommandLine is not
a pattern that occurs in legitimate dependency installation.
DEPLOYMENT SCOPE: Developer workstations and self-hosted CI runners only.
GitHub managed runners are not org-owned infrastructure — no Sysmon For Linux can be
deployed there. On self-hosted runners this rule fires at the exact moment the worm
payload begins executing, before any credential theft or exfiltration has occurred,
making it the earliest actionable alert in the execution chain. On developer workstations
it catches the same execution point and should be correlated with EDR-NIX16-RUN on the
same host to confirm active credential scraping.
Sysmon For Linux EVID 1 is the required telemetry source.
reference:
- https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem
- https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack
- https://github.com/fluffybunnies-h4x/FT-Linux-Sysmon-Config
labels:
- attack.execution
- attack.t1059.007
- attack.t1195.002
- attack.t1027
- Mini_Shai_Hulud
- TeamPCP
- Supply_Chain
- Bun
- TanStack
logsource:
category: process_creation
product: linux
detection:
selection_bun_image:
Image|endswith:
- '/bun'
- '/bun.exe'
selection_npm_parent_chain:
ParentImage|endswith:
- '/sh'
- '/bash'
ParentCommandLine|contains:
- 'bun run'
- 'bun install'
selection_grandparent_node:
# Bun is spawned through: npm -> node -> sh -> bun
# ParentCommandLine of sh will contain the bun run invocation
ParentCommandLine|contains:
- 'npm-cli.js'
- 'node install.js'
- 'node_modules'
selection_payload_filenames:
CommandLine|contains:
- 'tanstack_runner.js'
- 'opensearch_init.js'
- 'router_runtime.js'
- 'router_init.js'
filter_legitimate_bun:
# Filter environments where Bun is an approved primary runtime
ParentCommandLine|contains:
- 'bunx'
- 'bun.lockb'
condition: selection_bun_image and (selection_npm_parent_chain or selection_grandparent_node) and selection_payload_filenames and not filter_legitimate_bun
criticality: High
saveasthreat: false
violation_summary:
grouping_attribute: 'accountname'
level2_attribute: 'devicehostname'
level2_metadata_attributes:TECHNICAL DETAILS
DEPLOYMENT SCOPE
----------------
Target systems: Developer workstations, self-hosted CI runners
Do NOT deploy: GitHub managed runners (no endpoint visibility — not org-owned)
Priority target: Both are equal priority. Self-hosted runners carry higher
propagation risk (publish tokens present); developer workstations
carry higher persistence risk (IDE hooks, reboot-persistent services).
Correlation: When this rule fires, immediately check the same devicehostname
for EDR-NIX16-RUN within a 5-minute window. Co-occurrence is
near-certain confirmation of active Mini Shai-Hulud execution.
DETECTION MECHANICS
-------------------
The @tanstack/setup malicious optional dependency carries this prepare hook:
"scripts": { "prepare": "bun run tanstack_runner.js && exit 1" }
The && exit 1 causes the optional dependency to appear to fail gracefully so
npm install completes normally while the payload has already executed.
Observed Bun install and execution chain (StepSecurity process tree):
sh -c "node install.js" (PID 2355)
└─ node install.js (PID 2356)
└─ bun --version (PID 2363) # silent install verification
sh -c "bun run opensearch_init.js" (PID 2364)
└─ bun.exe opensearch_init.js (PID 2365) # worm payload execution begins here
Payload filenames in this campaign wave (may rotate in future campaigns):
tanstack_runner.js — from attacker fork (SHA-256: 2ec78d...)
router_init.js — embedded in compromised tarballs (SHA-256: ab4fca...)
opensearch_init.js — opensearch-project package variant
router_runtime.js — persistence copy dropped to .claude/ after first run
Filename selectors are campaign-specific. If TeamPCP rotates filenames the
selection_npm_parent_chain and selection_grandparent_node conditions remain
the durable detection even without filename matches. Consider tuning to remove
the filename requirement in environments where Bun is not an approved runtime,
accepting a slightly higher FP rate for broader coverage.
False Positives:
- Environments that explicitly use Bun as a primary runtime (bun.lockb present)
- Packages that legitimately declare bun in their prepare scripts (rare)
- Narrow via filter_legitimate_bun if FP rate is unacceptable
Policy building walkthrough can be found in this previous post:
Sysmon For Linux is CRITICAL for this detection to work
Here is the github repo I co-maintain to help security professionals who haven’t installed or worked with Sysmon For Linux get it up and running:
