On April 29, 2026, four npm packages maintained by SAP — mbt 1.2.48, @cap-js/db-service 2.10.1, @cap-js/postgres 2.2.2, and @cap-js/sqlite 2.2.2 — were published with a malicious preinstall script that attackers had slipped into SAP's own release pipeline. Rather than dropping a typical Node.js payload, the script downloaded the Bun runtime directly from GitHub Releases and used it to execute an approximately 11.6MB obfuscated file, execution.js, that harvested npm and GitHub tokens, GitHub Actions secrets, and AWS, Azure, GCP, and Kubernetes credentials from every machine that ran npm install against a poisoned version. Security researchers who tracked the campaign — including teams at Snyk and StepSecurity — named it "Mini Shai-Hulud," after a wave of related npm worms that hit the ecosystem in 2025 and again that December. A follow-on wave in May 2026 spread the same Bun-loader technique to more than 170 additional packages across npm and PyPI, including scopes tied to TanStack, Mistral AI, Guardrails AI, and UiPath — a separate, larger wave days later hit the @antv ecosystem via a compromised maintainer account rather than a CI pipeline hijack. This postmortem walks through how the compromise happened, why the Bun-runtime trick evaded the log-based monitoring most teams already run, and what a defensible install-time detection posture actually looks like for CI pipelines that publish to public registries.
How did attackers get into SAP's release pipeline in the first place?
The entry point wasn't a stolen npm token used directly — it was the CI/CD release workflow that already held one. Reporting from Snyk and StepSecurity traced the compromise to unauthorized commits that hijacked a GitHub Actions release workflow configured to publish to npm automatically on merge, with no manual approval gate between a merge and a live publish. Once attackers could land a commit that the workflow treated as trusted, the workflow's own npm publish credentials did the rest of the work — no separate credential theft was needed to get the malicious versions live. This is the same root-cause pattern behind the original 2025 Shai-Hulud worm and December 2025's "Shai-Hulud 2.0": CI workflows accumulate long-lived publish permissions over years, and very few of them require a second human signoff before a version reaches a production registry. An unreviewed merge becomes an unreviewed publish.
What made the Bun-runtime delivery mechanism different from a typical npm stealer?
Most npm-borne credential stealers run inside the Node.js process that's already executing npm install, which means the malicious code shows up in the same process tree, file writes, and network calls that Node-focused security tooling is tuned to watch. The Mini Shai-Hulud payload sidestepped that assumption: its preinstall script (a loader named setup.mjs, byte-for-byte identical across all four compromised packages) fetched the Bun runtime binary straight from GitHub Releases — a domain most egress monitoring treats as benign developer traffic — and then handed off execution of the ~11.6MB obfuscated execution.js payload to that separate runtime entirely. Detection logic written to flag "Node process spawning unusual child processes" or "unexpected node network calls" simply never fired, because the credential-harvesting code never ran inside Node at all. StepSecurity and Upwind researchers who analyzed the campaign flagged this as the core evasion technique, not the obfuscation itself.
What did the payload actually steal, and how did it get the data out?
Once running under Bun, the payload scanned the host filesystem for local credential stores, then specifically targeted GitHub and npm authentication tokens plus cloud credentials for AWS, Azure, GCP, and Kubernetes — a footprint aimed squarely at CI runners and developer laptops that hold publish rights and cloud deploy access simultaneously. Stolen data was encrypted with AES-256-GCM, with the AES key itself wrapped using RSA-OAEP before exfiltration, and pushed to a "dead-drop" repository created on the victim's own GitHub account. Researchers noted the repos were literally titled "A Mini Shai-Hulud has Appeared" — public, attacker-branded, and discoverable via GitHub search in real time by anyone who knew to look, including the victims themselves if they happened to check their own account activity. On CI runners specifically, the malware went further, injecting a malicious GitHub Actions workflow that called toJSON(secrets) and uploaded the result as a downloadable build artifact, converting masked pipeline secrets into a plaintext file sitting in the Actions UI.
Why did stolen npm tokens make this self-propagating?
Any npm publish token the payload harvested wasn't just exfiltrated — it was built to be reused. Researchers who dissected the SAP payload found hardcoded propagation targets and self-propagation code designed to publish malicious versions of a compromised maintainer's other packages under their real identity, with no further supply-chain foothold required — though in the SAP incident itself, publicly documented spread stayed confined to the four original packages. That same worm-like design is what let the separate May 2026 wave cascade across 170-plus packages: each newly compromised maintainer's token became the entry point for the next set of packages. This propagation pattern is exactly why the "Shai-Hulud" naming stuck across multiple 2025–2026 waves — the malware doesn't need a new initial-access technique per victim, only one stolen token, because npm's per-package trust model has no separate check on whether a publish under a known-good maintainer's account matches that maintainer's normal release cadence or tooling.
What detection gap let this reach production dependency trees?
The gap wasn't a missing SBOM or an un-pinned version range — most enterprise consumers of @cap-js and mbt had those. It was that the entire malicious behavior happened inside a single npm install, as an ephemeral, mid-build process that completed and exited before any post-hoc log review would ever look at it. SIEM rules built around persistent processes, and SBOM diffing that only compares declared dependency trees against known-CVE databases, both miss a package that is malicious the moment it's installed rather than vulnerable in some function you might call later. StepSecurity's writeup on the campaign was explicit that the fix isn't more static diffing — it's install-time behavioral monitoring that can see a preinstall script fetch an unexpected runtime and a short-lived process reach for ~/.aws or ~/.npmrc, categories of behavior that exist regardless of which runtime executes them.
How Safeguard Helps
This is exactly the class of problem Safeguard's platform is built around: a continuously updated malware and compromised-package intelligence feed, surfaced through policy gates that can flag or block a known-malicious package version rather than waiting for it to surface as a dependency-vulnerability finding weeks later. That intelligence is behavior-pattern based — install-script activity and credential-path access are well-understood risk signals regardless of which runtime ultimately executes the code — so the detection logic isn't tied to Node-specific assumptions that a Bun handoff can slip past. And because that intelligence updates retroactively, teams that already had @cap-js/db-service 2.10.1 or mbt 1.2.48 in a dependency tree before the compromise was publicly disclosed can be matched against the existing SBOM the moment the package is confirmed malicious, instead of relying on someone to re-run a scan and catch it manually.