Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
npm-incident-response | Kitploit
Tools/GitHubGitHub/securest8/npm-incident-response
Vulnerability ScannersPersistence MechanismsMalware AnalysisDigital ForensicsSupply Chain SecurityIncident Response
GitHubsecurest8/npm-incident-response

npm-incident-response

View Repository
2114 days agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

npm-incident-response

English | Português

Standalone scanner for the keyv/cacheable supply-chain incident ("Shai-Hulud: Here We Go Again", Aug 4 2026) — 440+ npm packages compromised by a self-propagating worm that steals cloud/CI credentials and plants persistence with a dead-man's switch.

Detects, in minutes and without installing anything:

  • Compromised packages in package-lock.json, npm-shrinkwrap.json, yarn.lock (v1 and Berry), pnpm-lock.yaml and bun.lock — including transitive dependencies, with the full chain (e.g. eslint → file-entry-cache → flat-cache → [email protected]);
  • Installed payloads in node_modules (name + SHA-256 hash of the known artifacts);
  • Variants not yet on the IOC lists (heuristic: suspicious lifecycle scripts, files with the worm's names) — always marked SUSPECT, never confirmed without a hash;
  • Host persistence implants: LaunchAgent (macOS), systemd user service + linger (Linux), hooks in .claude/settings.json and .vscode/tasks.json, temporary artifacts (bun-dl-*);
  • The dead-man's switch: the implant monitors a GitHub token and runs a remote command when revocation returns 4xx. Rotating credentials before cleaning the host triggers the trap — the report warns you, and the response order below avoids the mistake.

Understand the attack

  1. The maintainer account for the keyv/cacheable families was compromised; the attacker published new versions with a "preinstall": "node setup.mjs" hook — code that runs before the package is installed, with the privileges of whoever ran npm install.
  2. setup.mjs downloads the Bun runtime from GitHub and runs the payload in it — evasion against tools that only monitor node processes.
  3. Math_Symbol.js (~728 KB, obfuscated) steals credentials: AWS instance metadata, AWS/GCP/Azure keys, Vault tokens, Kubernetes service accounts, GitHub Actions secrets, npm tokens, plus a generic regex sweep for private keys and bearer tokens on disk.
  4. It is a worm: with the stolen npm token, it injects the same hook into other packages that identity can publish, recomputes the integrity hashes and republishes. That is how it went from ~10 to hundreds of packages.
  5. It exfiltrates without a fixed C2 (GitHub repos created on the fly, DNS) and leaves a trap behind — see below.

The two vectors (the second is subtler)

  • Vector A — install: anyone who ran npm install/npm ci with lifecycle scripts enabled since 2026-08-04 09:35 UTC. With --ignore-scripts, the hook did not run.
  • Vector B — clone: the source repository received autostart hooks in .claude/settings.json (SessionStart) and .vscode/tasks.json (folderOpen) that run the loader when the cloned folder is opened — no npm install, nothing installed. This includes people who cloned the repo to investigate the incident and AI coding agents that opened the directory — one of the first public cases of AI-agent hooks (.claude/) used as a supply-chain vector.

The trap (dead-man's switch)

The implant installs a "watcher" (gh-token-monitor) kept alive by a LaunchAgent (macOS) or a systemd user service + loginctl enable-linger (Linux). Every 60 seconds it validates the stolen GitHub token against the API. While the token works, nothing happens. When the response turns 4xx — i.e. the moment you revoke the token — it runs, via eval, the contents of ~/.config/gh-token-monitor/handler: an arbitrary command defined remotely by the attacker. Public analysis does not know what it contains — it could be data destruction, re-implant, ransomware, or nothing. The risk is not assessable; that is why the response order is absolute.

Three properties that change the response:

  • Isolating the network is safe: with no connectivity there is no HTTP response, so no 4xx — the trap does not fire, and exfiltration stops. Isolate first, do not power off (volatile memory is evidence).
  • It is single-shot and self-clearing after firing — the behavior becomes unexplained, with no artifact left to investigate.
  • ~24h TTL: the watcher self-destructs after a day. Absence of artifacts does not prove the machine was clean — the scanner warns about this in host mode.

Why the usual defenses generally miss it

  • "The signature was valid" — [email protected] shipped with a passing SLSA attestation. Provenance attests to build integrity, not source: the legitimate workflow compiled already-trojanized code.
  • "The code diff didn't change" — correct: the library itself was not modified. The malice is in package.json (the preinstall hook) and two new files added to the package (setup.mjs, Math_Symbol.js).
  • "We don't use keyv" — you do, indirectly: the most common chain is eslint → file-entry-cache → flat-cache → keyv. That is why the scanner shows the chain in every finding.
  • "Nobody ran npm install" — insufficient: see Vector B.

What the script in this repository is

scan.mjs has the following properties — important for anyone responding to a supply-chain incident:

  • A single file, ~880 readable lines, zero dependencies. No npm install. Audit the whole of scan.mjs in 15 minutes before running it.
  • Zero egress. No data leaves your machine. No telemetry, no "send the result for analysis". The only network operation is --update (download a fresh IOC manifest), explicit and optional.
  • Read-only. The scanner does not modify, remove, or execute anything it finds.
  • Works offline. docker run --network=none or an isolated machine: just copy scan.mjs + iocs.json.

How to use it in your company

Requirement: Node.js ≥ 18 (any machine with npm already has it). Download the two files — scan.mjs + iocs.json — and that is it: no installation.

Heads-up: if you cloned this whole repository, the fixtures/ folder contains inert IOCs used in the tests (real names and versions, dummy content — no malware). The scanner skips it automatically and warns in the output; findings from it only appear if you scan it on purpose.

There are two run modes that answer different questions, and that is what decides where to run:

  • The repo mode reads lockfiles and node_modules — and lockfiles live in git, so it can be centralized: one person scans every repository in the company.
  • The host mode looks for the implant (watcher, LaunchAgent/systemd, IDE hooks), which lives on the machine where the code executed — that is not in git and cannot be centralized.

Step 1 — AppSec scans all repositories (one person, one machine)

root@kitploit:~
node scan.mjs repo /folder/with/all/the/repos --json=result.json --html=report.html

Answers "which projects are exposed" in minutes, without involving anyone. Accepts multiple paths; walks subdirectories (monorepos and workspaces included).

Step 2 — whoever worked on the affected projects scans their own machine

For each project with a finding, identify who touched it since 2026-08-04 09:35 UTC (git log, CI logs). Those people run, on their machine:

root@kitploit:~
node scan.mjs        # current directory + host, in ~30 seconds

In scope: anyone who (a) ran npm install/npm ci in the window; or (b) merely cloned and opened the folder in VS Code or an AI agent — Vector B needs no install.

Since the cost is ~30 seconds and the funnel can leak (a stray clone, a personal project), the safest internal message is: every developer runs node scan.mjs once and sends the --json/--html to AppSec. Sending is manual by design — the scanner has no telemetry (zero egress).

Step 3 — CI runners and build servers

Top priority: this is where the most valuable credentials live. Here the scanner has two different roles — one for the past, one for the future:

Triaging the past — do NOT scan the runner to decide. The question "was this runner hit?" is not answered by a scan: if any job installed an affected version without --ignore-scripts since 08-04, the credentials were already stolen at that moment, and the runner host rarely keeps evidence (ephemeral runners destroy the container at the end of the job; the watcher self-clears in ~24h). What answers it are the Step 1 lockfiles and the CI logs. If the answer is "yes, it installed": rebuild the runner and rotate its secrets — runners are ephemeral, there is no reason to clean them.

Prevention going forward — YES, run it in the pipeline. Add the scanner as a build step, in repo mode, after checkout and before npm install. It does not examine the runner host — it examines the code about to be installed, and the exit code fails the build before the malicious preinstall gets a chance to run:

root@kitploit:~
# example (GitHub Actions / GitLab CI — adapt):
- run: node scan.mjs repo . --json    # exit 0 clean · 1 findings · 2 COMPROMISED
- run: npm ci --ignore-scripts         # only runs if the previous step passed

Quick reference

root@kitploit:~
node scan.mjs                     # scan the current directory + the host
node scan.mjs repo /path/a /path/b
node scan.mjs host                # persistence/implants on the machine only
node scan.mjs repo . --json=result.json --html=report.html
node scan.mjs --update            # update iocs.json (the only network operation)

Triage

Report as evidence

--html produces a self-contained report with a timestamp, hostname, IOC manifest version and the scanner's own SHA-256 — usable as an attachment to an incident notification and an audit trail.

If the scanner reported COMPROMISED: the response order

Do not revoke or rotate any credential yet — that is the trap's trigger. The sequence:

1. ISOLATE — cut the machine off the network. It is safe: with no HTTP response there is no 4xx, the trap does not fire, and exfiltration stops. Do not power off (volatile memory is evidence).

2. PRESERVE — before deleting anything (the watcher self-destructs in ~24h):

root@kitploit:~
mkdir -p /tmp/evidence && cp -r ~/.config/gh-token-monitor /tmp/evidence/ 2>/dev/null
cp /tmp/gh-token-monitor.*.log /tmp/evidence/ 2>/dev/null
shasum -a 256 /tmp/evidence/* 2>/dev/null

The handler file is the attacker's command that would be executed — do not run it, do not paste it into a shell; treat it as inert text. The started_at file bounds the exposure window (the auditor and the regulator will ask for it).

3. ERADICATE — kill the watcher process first, then:

root@kitploit:~
# macOS
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
rm -f ~/Library/LaunchAgents/com.user.gh-token-monitor.plist

# Linux
systemctl --user disable --now gh-token-monitor.service
loginctl disable-linger "$USER"
rm -f ~/.config/systemd/user/gh-token-monitor.service

# both
rm -rf ~/.config/gh-token-monitor ~/.local/bin/gh-token-monitor.sh /tmp/bun-dl-*

Also remove the malicious hooks from .claude/settings.json and .vscode/tasks.json, the setup.mjs/Math_Symbol.js/math_init.js files, and clear the caches (~/.npm/_cacache, pnpm, yarn). Run node scan.mjs host again until it comes back clean.

4. ROTATE — only with every affected host cleaned and verified (a single live watcher is enough to fire the trigger). Revoke the npm token first (stops the worm from propagating); then GitHub (PATs, deploy keys), AWS/GCP/Azure, Vault, Kubernetes, CI secrets — and any secret that was on disk, because there was a regex sweep.

5. AUDIT — the worm acts in your name: look for repositories described Shai-Hulud: Here We Go Again in your organizations, npm versions published unexpectedly since 08-04 (deprecate them and notify consumers), and use of the credentials in CloudTrail/Audit Logs within the started_at window.

Afterwards: delete node_modules, reinstall from a clean lockfile with --ignore-scripts. CI runners and hosts with confirmed execution: rebuild from scratch, always — arbitrary code executed, and the list of known artifacts is no guarantee of completeness.

Regulated institutions (BR): a confirmed compromise with credential access can trigger reporting obligations (Res. CMN 4.893/2021, Res. BCB 85/2021; LGPD art. 48 if personal data is involved). Document the timeline in UTC — started_at, detection, containment, eradication, rotation — and confirm deadlines with legal/compliance.

Updating the IOCs (users)

The incident is active and the list grows. To pull the latest manifest:

root@kitploit:~
node scan.mjs --update          # the only operation that touches the network

--update fetches iocs.json from this repository (securest8/npm-incident-response), never from a third party — Securest8 is the curation gate. You get whatever was last published here.

Maintaining the IOCs (maintainers)

The package list comes from the public Wiz feed; the hashes, C2 domains, persistence IOCs and safe versions are static and curated in tools/gen-iocs.mjs. A snapshot of the Wiz CSV lives at tools/keyv-packages.csv for reproducibility and offline runs.

root@kitploit:~
node tools/gen-iocs.mjs               # fetch the latest Wiz CSV, regenerate iocs.json + refresh the snapshot
node tools/gen-iocs.mjs --offline     # regenerate from the committed snapshot, no network
node tools/gen-iocs.mjs --allow-shrink # allow a package count lower than the snapshot (guarded by default)

The generator is idempotent: it keeps the existing manifest_version and does not rewrite the file when nothing material changed, and it refuses to write an empty or shrunken manifest (guarding against a truncated/changed upstream feed).

Automation: .github/workflows/update-iocs.yml runs the generator daily (and on demand) and commits to main only when the IOCs actually change — so users' --update tracks the Wiz feed within about a day, with the full history auditable in commits. Flip it to a pull-request step (noted in the workflow) once the incident cools and you want a manual merge instead.

Tests

root@kitploit:~
node test/run-tests.mjs         # 15 assertions against fixtures/demo-repo

fixtures/demo-repo is a test repository with inert IOCs (real names and versions, dummy content — no malware). When scanning the scanner's own repository, the fixtures/ folder is skipped automatically (with a warning in the output); the tests scan it by passing the path explicitly.

Scope and credit

A single-incident tool, built for fast triage during the active window of the attack — not a replacement for Socket, Snyk or similar. Research and IOCs: Socket.dev, Wiz Research (public CSV), Kodem Security.

Maintained by Securest8. MIT License.

Download Tool
LevelMeaningAction
COMPROMISEDMalicious version installed in node_modules, payload confirmed by hash, or a persistence implant foundTreat the host as compromised; follow the response order — clean the implant before rotating credentials
EXPOSEDMalicious version pinned in a lockfile, no evidence of executionPin a safe version, delete node_modules, reinstall with --ignore-scripts
AT_RISKRange (^/~) in package.json that admits a malicious versionPin an exact version or block it at the registry proxy
SUSPECTHeuristic (worm filename with a mismatched hash, suspicious lifecycle script)Inspect manually — could be a new variant or a false positive
INFOVector present but no IOC (e.g. a generic folderOpen task)Review