Check npm installs after a supply-chain poisoning advisory.
Scanned 9/10/2026
Install to Claude Code
npx -y skills add theheavenlyd3mon/hermes-profiles --skill npm-supply-chain-sweep --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Npm Supply Chain Sweep?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/theheavenlyd3mon-npm-supply-chain-sweep)More formats (shields.io, HTML) on the badges page.
---
name: npm-supply-chain-sweep
description: Check npm installs after a supply-chain poisoning advisory.
domain: cybersecurity
subdomain: supply-chain-security
version: '1.0'
---
# npm Supply-Chain Sweep
Advisory-driven verification: a maintainer account got compromised, poisoned
versions of package(s) were published, and the user wants to know "did I get
hit?". Answer with evidence, not vibes. Read-only, no installs required except
the packages you are checking against.
## When to Use
- "New npm supply-chain advisory just dropped — sweep my machine"
- "Did I install the compromised <pkg>@<version>?"
- Any poisoned-version list from an advisory (Mini Shai-Hulud, esbuild,
ua-parser-js, event-stream, colors/faker, etc.). The workflow is the same.
## Core Principle
A package DIRECTORY with a matching name is NOT a verdict. `keyv`,
`file-entry-cache`, `cacheable-request` and friends are ubiquitous transitive
deps of eslint/stylelint — their dirs appear in almost every node_modules.
**The exact installed VERSION is the verdict.** Always compare installed
version against the advisory's poisoned version list.
## Workflow
1. **Parse the advisory** — build the exact `pkg@poisoned-version` list.
Versions matter: `file-entry-cache@11.1.6` is poisoned while `11.1.2` is
fine. Extract payload/dropper filenames (e.g. `setup.mjs`, `Math_*.js`)
and any C2/exfil domains for later.
2. **Inventory tooling** — `node -v`, `npm -v`, `bun -v`, `npm ls -g --depth=0`,
`npm config get registry` (a non-registry.npmjs.org registry is itself a
finding).
3. **Find lockfiles** (exclude `*/Library/*`, `*/.hermes/*`, `*/node_modules/*`):
`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `bun.lock`, `bun.lockb`,
`npm-shrinkwrap.json`. For each poisoned package, grep with **package-name
context**, never a bare version string.
4. **Installed copies** — `find` `*/node_modules/<pkg>` (and nested copies
under `node_modules/<other>/node_modules/<pkg>` — nested deps are a common
hiding spot). Read each `package.json`: `version` + `scripts`. A
`preinstall`/`prepare` script in a version that shouldn't have one is a red
flag.
5. **Dropper artifacts** — search node_modules for the advisory's filenames
(`setup.mjs`, `Math_*.js`, `preinstall*`). Absence = strong clean signal.
6. **Caches** — `npm cache ls <pkg>` (poisoned tarball URL = exposure, even if
node_modules was reinstalled), plus the bun cache
`~/.bun/install/cache/<pkg>/`. `npm cache ls` per-package can be truncated
with `head` — run full when the poisoned version is the question.
7. **DECISIVE: publish-time proof** — query
`curl -s https://registry.npmjs.org/<pkg>` → `d['time'][version]` for the
poisoned version. Compare against install timestamps
(`ls -laT <pkg>/package.json`). If every install predates the poisoned
publish, infection is *physically impossible* — state that.
8. **Live-process check** — `ps aux | grep -E '/(bun|node|deno)'`: a payload
executes via bun/node; no such process = nothing running.
9. **Token surface** — list `.npmrc` files (existence only; NEVER print token
contents). No `.npmrc` in projects = nothing for the stealer to harvest.
10. **Network** — `lsof -i -P -n | grep ESTABLISHED` and grep for the
advisory's exfil domains. Check `/etc/hosts`: users often add their own
block entries for C2/exfil domains — that is intentional defense, not
hijack; note it, don't flag it.
## Report
Deliver findings IN CHAT, bottom line first ("no poisoned version installed,
and here is the timing proof"), then severity-ordered. HIGH = poisoned version
found OR patch debt; MEDIUM/LOW = posture items; INFO = intentional
defensive edits. State evidence chains (installed version, lockfile resolved
URL + integrity, publish timestamp) for every verdict.
## Support files
- `scripts/npm_poison_sweep.sh` — re-runnable sweep: takes `pkg@version`
pairs, checks lockfiles, all installed copies (incl. nested), npm cache,
dropper artifacts, and fetches the poisoned publish timestamp.
- `references/mini-shai-hulud-iocs.md` — worked example: the Mini Shai-Hulud
advisory (poisoned versions + publish dates, payload names, exfil domains,
Defender name) and the session's clean-verdict evidence chain.
## Pitfalls
- Grepping lockfiles for `"version": "6.0.0"` matches ANY package at 6.0.0
(e.g. `locate-path@6.0.0`) — massive false-positive noise. Anchor on the
package entry (`"node_modules/keyv"` block) or the `resolved` tarball URL.
- Nested copies: `node_modules/stylelint/node_modules/file-entry-cache` can be
a different (benign) major version than the top-level one — check every copy.
- `npm cache ls` may only show a few lines; the poisoned tarball could be
further down the list.
- Scoped packages (`@servicetitan/*`, `@cacheable/*`) need the `@scope/` in
the find path and grep patterns.
- Don't string-grep whole node_modules trees; target `find` by package dir.
- `freshclam`/ClamAV is a separate, slower pass — the npm sweep is
deterministic and does not need an AV scan to reach a verdict.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!