Security

npm Supply-Chain Rapid Response

Last synced: Jul 21, 2026

npm Supply-Chain Rapid Response

Fire this runbook when ANY new npm supply-chain advisory drops (TanStack-style worm, malicious package, registry compromise). Designed to take ≤5 minutes per project for the cache-and-reinstall pass.

Standing defense (already in place)

  • minimumReleaseAge = 86400 in ~/.bunfig.toml and ~/.claude/bunfig.toml rejects any package published in the last 24 hours. The May 11 Mini Shai-Hulud worm lived 3 hours from publish to npm-pull; this filter neutralizes that attack window without requiring vendor trust.
  • Bun’s trustedDependencies allowlist — transitive packages don’t get to run lifecycle scripts unless their name is in your package.json’s trustedDependencies array. Default-deny on transitive scripts.
  • All active CI Actions SHA-pinned in each project’s .github/workflows/ directory with Dependabot auto-PR’ing bumps weekly.

Incident response steps

1. Read the advisory (1 min)

Pull the named compromised packages and their version ranges into a working note. Sources in order of priority: Wiz Blog, Snyk advisories, the upstream maintainer’s postmortem, npm’s security advisory feed.

2. Direct-IOC scan (2 min per scan)

# Pattern: every compromised package name, alternation-separated
IOC='@evil/pkg-one|@evil/pkg-two|"some-package":'

# Scan all lockfiles
find ~/Projects ~/LocalProjects ~/.claude -maxdepth 6 \
  \( -name "bun.lock" -o -name "package-lock.json" -o -name "yarn.lock" -o -name "pnpm-lock.yaml" \) \
  -not -path "*/node_modules/*" -not -path "*/LIFEOS_RELEASES/*" -not -path "*/.next/*" \
  2>/dev/null | xargs rg -l "$IOC"

# Scan all package.json (for declared deps that may have lockfile drift)
find ~/Projects ~/LocalProjects ~/.claude -maxdepth 6 -name "package.json" \
  -not -path "*/node_modules/*" -not -path "*/LIFEOS_RELEASES/*" -not -path "*/.next/*" \
  2>/dev/null | xargs rg -l "$IOC"

If both return empty → declared-dependency exposure is zero. Proceed to step 3 for transient-install coverage.

If either returns hits → record the project path + version, then per-project remediation: bump to a known-clean version, run cache-nuke (step 4), verify.

3. Mtime check for the worm window (1 min)

# Replace dates with the advisory's worm window
for lf in $(find ~/Projects ~/LocalProjects ~/.claude -maxdepth 6 -name "bun.lock" -not -path "*/node_modules/*" 2>/dev/null); do
  mt=$(stat -f "%Sm" -t "%Y-%m-%d" "$lf" 2>/dev/null)
  case "$mt" in
    YYYY-MM-DD|YYYY-MM-DD)  # <-- replace with worm window dates
      echo "  $mt  $lf"
      ;;
  esac
done

Any lockfile modified during the worm window is a candidate for cache-nuke even if direct IOC is clean (defense against transient/cached installs).

4. Cache-nuke and reinstall (per project, ≤2 min)

cd <project>
bun pm cache rm
rm -rf node_modules
bun install --frozen-lockfile

--frozen-lockfile ensures the lockfile is treated as authoritative; this catches any lockfile/cache drift that may have allowed a transient install.

5. Rotate suspect credentials (only if step 2 hit OR step 3 flagged transient-install)

  • npm token (if any) — npm token list, revoke, regenerate
  • GitHub PAT (any token used in CI on the host) — gh auth refresh
  • AWS / GCP / Azure on dev hosts where install ran during worm window
  • See the incident-response skill for the full rotation protocol

Hardening levers (one per attack surface)

SurfaceLever
Just-published malicious packagesminimumReleaseAge in bunfig (in place)
Lifecycle-script execution in CIbun install --ignore-scripts in workflow steps
Lifecycle-script execution locallyAdd malicious package’s name to trustedDependencies allowlist only when verified safe
GitHub Actions tag-move attacksSHA-pin all uses: lines + Dependabot weekly bumps (in place for Website)
Pull-request-target workflow exfilBan pull_request_target in any workflow that checks out PR code (currently zero use across {{PRINCIPAL_NAME}}‘s repos)
Credentials in CIDefault-deny permissions: block at workflow level; per-job grants
Stale npm cachebun pm cache rm before any reinstall during incident response
Dev-host credential blast radius~/.bun/install/cache is per-user; install lifecycle scripts run as the user → all reachable credentials are in scope

Anti-patterns

  • Don’t bun install against a fresh advisory before completing step 2.
  • Don’t rotate every credential reflexively — match rotation scope to actual exposure.
  • Don’t trust npm audit / bun audit as a primary signal during an active worm; the registry’s signal lags by hours.
  • Don’t silence the minimumReleaseAge warning globally by emptying the excludes list. Add the specific package name to minimumReleaseAgeExcludes and document why.

Examples

An advisory drops at 9pm — the fast path

A new advisory names two malicious packages, @evil/pkg-one and @evil/pkg-two, published earlier that day. Run the book, not your nerves:

  1. Read it, list the names, set the IOCIOC='@evil/pkg-one|@evil/pkg-two'. Two minutes.
  2. Direct-IOC scan across every lockfile and package.json — both come back empty. No project declares or locks either package.
  3. Mtime check for the worm window — the scan flags one lockfile touched during the advisory’s window. Direct IOC was clean, but a transient install could have pulled the package into the cache, so this project is a cache-nuke candidate.
  4. Cache-nuke that one projectbun pm cache rm, rm -rf node_modules, bun install --frozen-lockfile. The frozen lockfile refuses any drift.
  5. Rotate nothing — direct IOC never hit and no bad package ever ran; matching rotation scope to actual exposure means the credentials stay put. Done in under ten minutes.

The standing defenses did most of the work before you woke up: minimumReleaseAge = 86400 in bunfig means a package published that same day couldn’t have been installed at all — the 24-hour quarantine neutralized the attack window without trusting the registry.

When to rotate, and when not to

The tempting mistake under pressure is to rotate every credential reflexively. Don’t. Rotation scope follows evidence:

  • Direct IOC hit, or a lifecycle script ran during the window → rotate the credentials reachable from that dev host: npm token, CI PAT, cloud creds. The install ran as your user, so its blast radius is everything your user can reach.
  • Clean IOC, no install during the window → rotate nothing. A reinstall is precaution, not compromise.

The incident decision flow

flowchart TD
    A[New npm advisory drops] --> B[Read advisory, list bad packages]
    B --> C{Direct IOC scan hits?}
    C -->|yes| D[Bump to clean version, cache-nuke, reinstall]
    C -->|no| E{Lockfile touched in worm window?}
    E -->|yes| F[Cache-nuke and reinstall as precaution]
    E -->|no| G[Exposure zero — done]
    D --> H{Bad package ran, or transient install flagged?}
    F --> H
    H -->|yes| I[Rotate only creds in the host's blast radius]
    H -->|no| G

The shape of the runbook is a funnel: cheap scans first, escalation only where evidence demands it, and credential rotation gated behind proof of actual install — never fired on the mere existence of an advisory.


Reference