
Detection & remediation toolkit for the Miasma / Shai-Hulud worm and CVE-2026-35603 (AI-agent/IDE config injection)
Incident analysis + detection/remediation tooling for the Miasma worm (a Mini Shai-Hulud variant) that injects auto-run payloads into AI-agent / IDE configs and npm/GitHub repos.
⚠️ Disclaimer — no warranty. This is a community defensive toolkit provided "as is", without warranty of any kind (see
LICENSE). The scanner and YARA rules are read-only, butpurge-history.shrewrites git history and the.claude/hooks move files to quarantine — both can alter a repository. Back up first, review what each tool does, and run it at your own risk. A clean scan is not a guarantee of safety: indicators evolve and new variants appear. Use on systems and accounts you are authorized to inspect. Found a gap or a bug? SeeSECURITY.mdandCONTRIBUTING.md.
Install YARA (Windows): winget install VirusTotal.YARA — or choco install yara / scoop install yara.
On Linux/macOS: apt install yara / brew install yara. Verify with yara --version.
Note:
-Mode Localdefaults to scanning$env:USERPROFILEonly. Pass-CodeRootsto cover repos elsewhere, e.g.-CodeRoots E:\Sources,D:\work.
# Scan local machine + local repos (+ CVE-2026-35603 ProgramData)
pwsh -File Scan-Miasma.ps1 -Mode Local
pwsh -File Scan-Miasma.ps1 -Mode Local -CodeRoots E:\Sources,D:\work
# Scan remote GitHub repos (authenticate on the target account first for private repos/secrets)
gh auth login
pwsh -File Scan-Miasma.ps1 -Mode Remote -Owners your-org,your-user -OutReport report.md
# Everything + JSON + Markdown report
pwsh -File Scan-Miasma.ps1 -OutJson findings.json -OutReport report.md
Exit code 1 if any INFECTED finding (CI-friendly).
# Purge history of an infected repo (review PATHS in the script first)
./purge-history.sh /path/to/repo
# then, after reviewing the verification:
git push origin --force --all && git push origin --force --tags
# YARA scan
yara -r setup-js.yar /path/to/scan
# Linux/macOS: local scan (bash port — defaults to $HOME; pass roots to widen)
./scan-miasma.sh
./scan-miasma.sh ~/src /opt/work
# Statically deobfuscate a captured dropper (read-only — never runs it)
pwsh -File Expand-MiasmaPayload.ps1 -Path .github/setup.js # writes layers + iocs.txt to <Path>.deob/
pwsh -File Expand-MiasmaPayload.ps1 -SelfTest # verify the decode/decrypt engine
The deobfuscator unpacks the p,a,c,k,e,d packer wave, decodes the char-code wave, auto-detects
and reverses the Caesar shift (override with -Shift), then decrypts every embedded AES-128-GCM
blob (_b bootstrapper, _p stealer) and scans the recovered code for URLs, IPs, and dead-drop
accounts.
# After eradication: generate a prioritized secret-rotation checklist (revokes nothing)
pwsh -File Invoke-MiasmaRotation.ps1 # detects creds reachable here
pwsh -File Invoke-MiasmaRotation.ps1 -OutReport rotation.md
Use the bundled composite action as the first step of any workflow:
# In another repo (pin to a tag/SHA for supply-chain safety):
- uses: actions/checkout@v4
- uses: jchable/miasma-toolkit/.github/actions/miasma-guard@main
# with:
# full-scan: 'true' # also run Scan-Miasma.ps1 -Mode Local (needs pwsh)
This toolkit runs the same guard on itself via .github/workflows/miasma-ci.yml
(referencing the action by relative path). The guard is wave-agnostic and scoped to the worm's
launcher config files, so it never false-positives on docs that merely mention the IOC string.
.claude/)Real-time protection for machines that run Claude Code in this repo. The hooks reuse
iocs.psd1 (no duplicated indicators) and are wired in .claude/settings.json:
.miasma-quarantine/<timestamp>/…
(git-ignored) and logged to .miasma-quarantine/quarantine.log — restore with Move-Item.
Nothing is hard-deleted.Test-MiasmaExcluded in Miasma.Common.ps1): the toolkit's own files
legitimately contain IOC strings (iocs.psd1, Scan-Miasma.ps1, scan-miasma.sh,
Expand-MiasmaPayload.ps1, Invoke-MiasmaRotation.ps1, purge-history.sh, setup-js.yar,
content/, , , )
and are never blocked/quarantined.DROPPER, INJECT, FORGED, BADDEP, WORKFLOW, RUNNER, PAYLOAD, BUN-DROP, PROGRAMDATANPM-AUDIT, SECRETS (rotation inventory), SIGNATURE, PERSIST, PROGRAMDATA-CFGgh api <404> --jq dumps the error body to stdout → never trust output truthiness;
check $LASTEXITCODE + validate the format (numeric size, exact branch name).master→main redirect after rename → branches/master returns 200;
only accept a branch if the returned name == requested name.--package-lock-only (no npm install, no scripts executed).The original backlog (bash port, severity badges, packer-wave deobfuscation, rotation helper) is done. Future ideas:
scan-miasma.sh: add the remote/GitHub mode (currently local-only).--revoke mode (gated, with confirmation) for gh/npm/aws.| File | What it is |
|---|
content/incident-report.fr.md / content/incident-report.en.md | Full write-up: how it works, payload deobfuscation, IOCs, eradication |
Scan-Miasma.ps1 | Unified scanner (local + remote), structured JSON + per-repo Markdown report — use this |
iocs.psd1 | Shared indicators (hashes, signatures, bad packages, configs) loaded by the scanner — edit IOCs here |
Expand-MiasmaPayload.ps1 | Static deobfuscator for setup.js — peels packer/char-codes → Caesar → AES-128-GCM and extracts the C2/IOCs (never executes the payload) |
Invoke-MiasmaRotation.ps1 | Secret-rotation checklist — detects which credentials are reachable here and prints the revoke commands (read-only; revokes nothing) |
scan-miasma.sh | Bash port of the local scan for Linux/macOS (cross-platform subset) |
purge-history.sh | Purge malicious files from all git history (filter-repo → filter-branch) + backup |
setup-js.yar | YARA rules for the dropper + launcher configs |
.github/actions/miasma-guard/ | Reusable GitHub Actions composite action — refuses to build if the dropper/launcher is present |
.claude/ | Claude Code hooks — real-time guard/quarantine + /miasma-scan command (see below) |
| Tool | Needed for | Required? |
|---|
PowerShell 7+ (pwsh) | Scan-Miasma.ps1, Expand-MiasmaPayload.ps1, Invoke-MiasmaRotation.ps1 | required (those tools) |
bash + coreutils (sha256sum/shasum, awk, find) | scan-miasma.sh (Linux/macOS) | the bash port only |
| git | local-repo history checks, purge-history.sh | required |
GitHub CLI (gh, authenticated) | Scan-Miasma.ps1 -Mode Remote | remote scan only |
| npm | remote npm audit (lockfile-only) | optional (auto-skipped if absent) |
| git-filter-repo | preferred history rewrite (falls back to built-in git filter-branch) | optional |
YARA (yara) | setup-js.yar scan | optional |
| Hook | Event | Action |
|---|
Guard-Write.ps1 | PreToolUse (Write/Edit/MultiEdit) | Blocks a write that would inject a worm IOC (cancels the tool call) |
Guard-Bash.ps1 | PreToolUse (Bash) | Blocks worm execution — node .github/setup.js, bun/bunx, a piped bun.sh installer |
Scan-Write.ps1 | PostToolUse (Write/Edit/MultiEdit) | Alerts + quarantines an infected file after it lands |
Session-Sweep.ps1 | SessionStart | Fast repo sweep; reports any pre-existing infection into context |
/miasma-scan | command | On-demand wrapper around Scan-Miasma.ps1 |
.claude/hooks|commands.github/actions/miasma-guard/.github/workflows/Guard-Bash targets execution, not mention: its patterns (iocs.psd1 → CmdSigs) are
anchored to a command position, so triage commands that merely name an IOC
(grep "node .github/setup.js", yara -r setup-js.yar, cat setup.js) are not blocked —
only actually running node …/setup.js / bun / a bun.sh install pipe is. Note: bun/bunx
invocation is blocked outright (the worm's runtime); allowlist via CmdSigs if you genuinely need Bun.Scan-Miasma.ps1 (read-only), these hooks modify the tree (quarantine). They are
opt-in: Claude Code prompts to approve hooks the first time it loads .claude/settings.json.