Hunting Shai-Hulud: Detecting the npm Supply-Chain Worm with Sentinel
Shai-Hulud is a self-replicating npm supply-chain worm that slipped malicious post-install scripts into popular packages to steal secrets like npm/GitHub tokens, cloud keys, and create/modify GitHub repos & workflows for exfiltration, and then auto-publish poisoned updates wherever it found additional npm tokens—letting it spread across the ecosystem. Initial tallies cited 100+ affected packages; subsequent advisories and vendor tracking raised the scope to hundreds (500+ reported in some counts), so teams should assume impact is still evolving.
Behaviour
The Shai-Hulud worm behaves as a malicious npm package payload that executes a post-install script to harvest secrets (npm tokens, GitHub tokens, API keys, cloud credentials), then exfiltrates them to attacker-controlled GitHub repos and webhooks. It goes further by creating malicious GitHub Actions workflows to continuously leak secrets, and it forcibly migrates private repositories into public attacker-owned repos with a “Shai-Hulud Migration” tag. Critically, if additional npm tokens are present, it will auto-publish infected versions of any accessible packages, giving it worm-like, self-propagating capabilities across the npm ecosystem.
This is a massive supply chain attack and organisations if not already should treat this with high priority. This article is provided as a quick threat intelligence report you can read, although many sources and research is availible. This article goes into some KQL ( Kusto Query Language) scripts you can use to hunt for this activity. This article may also be benficial for new analyst who want to understand how to break down threat intelligence or research articles into actionable or useful bits of information for security operations.
Indicators of Compromise
Files & paths
/tmp/processor.sh— createsshai-huludbranch and uploads workflow payload./tmp/migrate-repos.sh— forces private repos → public “migration.”- Temp working dir observed:
/tmp/github-migration(used during repo cloning).
GitHub artifacts / repo signals
- Public repos named
Shai-Huludcontainingdata.json(double-base64 secrets dump). - Repos with description “Shai-Hulud Migration” and
-migrationsuffix (private → public flip). Look for a CreateEvent followed quickly by a PublicEvent. - Branches named
shai-huludcreated across impacted repos; hidden workflow commits under.github/workflows/*.yml.
Network / exfil endpoints
https://webhook[.]site/bb8ca5f6-4175-45d2-b042-fc9ebb8170b7(noting free-tier deactivated after excessive activity; secrets may still appear in GitHub workflow logs).
Hashes
46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09— malicious JS SHA256.
Tactics & behaviours to hunt
- Post-install scripts invoking TruffleHog; harvesting env vars/IMDS cloud creds.
- Automated
npm publishfrom unexpected actors/machines (worm propagation). - Creation/modification of
.github/workflows/*followed by unusual GitHub API POST/PUT events.
KQL Queries
Let’s jump into crafting a query we can use to hunt. This query can be adpated to whatever SIEM ( security incident event management) tool you are using or are learning with. KQL is the language I use so I am going to work with that.
let lookback = 7d;
let procs = DeviceProcessEvents
| where TimeGenerated >= ago(lookback)
| where FileName in ("node.exe","npm.exe","yarn.exe","git.exe","gh.exe")
| where ProcessCommandLine has_any ("postinstall","preinstall","install","bundle.js","eval(","trufflehog","gh repo create","push","remote add","api.github.com")
| project TimeGenerated, Source = "Process", DeviceName, AccountName, FileName, ProcessCommandLine;
let files = DeviceFileEvents
| where TimeGenerated >= ago(lookback)
| where FilePath has ".github\\workflows" or FileName in ("processor.sh","migrate-repos.sh")
| project TimeGenerated, Source = "File", DeviceName, AccountName, FilePath, FileName, ActionType, SHA256;
procs
| union files
| sort by TimeGenerated desc
| take 200
- Set the variable for time and the table you want to call in. This is a good habit to get into because it will make creating bigger queries further down the track easier.
- We are interested in
Filenametable andProcesscommandlineto detect the workflows we are looking for or any the /tmp/*.sh files the worm drops. - We are creating a second vairable to call to
DeviceFileEvents. - In that KQL, the
unionoperator is simply combining the results of two separate queries into one table.- The first part (
procs) collects suspicious process events (e.g.,npm installwithpostinstall,trufflehog, etc.). - The second part (
files) collects suspicious file events (e.g.,.github/workflowschanges or/tmp/processor.sh). unionmerges them together so you can review all suspicious signals in one results set instead of running two separate queries. If your just starting playing around running the queries separately and exploring the logs is good to, for getting a feel of the tables.
- The first part (
This query might be a bit noisy, however when hunting for activity we want to start wide and then narrow it down. It is good to get a sense for what is benign and what might be worth looking further into. The -migration will likely pull in legitimate migration activity, but again double checking might not hurt. You can start to filter out certain fields that you don’t want to see to narrow the information down.
Takeaways and Recommendations
The Shai-Hulud incident shows how fast a supply-chain compromise can spread and why proactive hunting matters. This mindset builds resilience for organisations and sharpens investigative skills for analysts.
- Always review new threat reports for relevance to your environment.
- If risk is high, run a targeted hunt rather than waiting for alerts.
- Maintain strong monitoring and tuned detections around Git and npm activity.
- Enforce clear policies for developer workflows and package use.
- Rotate and protect credentials aggressively after any suspected compromise.