DEV Community

Jason Miller
Jason Miller

Posted on Originally published at axeploit.com

Stop panic-grepping your lockfile after an npm compromise alert

A line in package-lock.json means your dependency tree can resolve the bad version. It says nothing about whether a machine downloaded it, and even less about whether it ran. Yet the first hour of every npm supply-chain alert gets spent on that one grep, and both outcomes are premature: found it, panic; absent, relief.

This is how I run that hour instead. Four questions, fixed order.

Q1. Is the package in any lockfile, in any repo?
    NO          -> Record the check, stand down.
    YES         -> Q2

Q2. Is the resolved version inside the advisory's affected range?
    NO          -> Verify the integrity hash, pin, stand down.
    YES         -> Q3

Q3. Did any CI job or dev machine install while that version
    was in the lockfile?
    NO          -> Fix the lockfile, confirm CI is green, no rotation.
    YES/UNKNOWN -> Q4

Q4. Could it have executed? (install scripts enabled, or the
    package is required by your build, tests, or bundler)
    NO          -> Fix the lockfile, archive logs, monitor.
    YES/UNKNOWN -> Rotate everything CI could see. Open an incident.
Enter fullscreen mode Exit fullscreen mode

Notice how often UNKNOWN is the honest answer. It routes to yes, every time. An unnecessary rotation costs an afternoon. A missed one costs an attacker sitting in your cloud account for a quarter while you feel good about a grep result.

Where each node goes wrong

Q1: monorepos and branches. Grep every lockfile: the second and third ones in the monorepo, the yarn and pnpm files, release branches that still get builds. CI builds branches, not just main. Then npm ls <package> shows who drags it in, usually transitively, via a build tool nobody remembers adding.

Q2: compare the pinned version against the advisory's affected range, then confirm the lockfile's integrity hash still matches what the registry serves (npm view <pkg>@<version> dist.integrity). A mismatch is its own incident.

Q3: reconstruct the window from git, not memory.

git log -S '"<package-name>"' --oneline -- package-lock.json
Enter fullscreen mode Exit fullscreen mode

The commit where the bad version appears and the one where it disappears bracket the window. Any job that ran npm ci inside it fetched the package. Archive the build logs immediately; CI retention is short and you cannot rebuild this timeline next week. If you cannot prove no install happened in the window, the answer is yes. Missing logs are not evidence.

Q4: two execution paths. Install-time payloads need lifecycle scripts, so jobs running with --ignore-scripts never fired them. Check the actual CI config for every job, not your memory of it. Runtime payloads are sneakier: if npm ls puts the package under your test runner or bundler, assume it executed in those jobs.

When Q4 says yes: rotation order

The attacker's head start is measured in hours, so order matters. Mine, by blast radius (no vendor publishes an official one that I know of):

  1. Push and publish credentials: npm tokens, write-scoped source-control tokens, deploy keys. These buy persistence. Revoke first.
  2. Cloud and infra keys held by CI. This is where a build compromise becomes a production compromise.
  3. Runtime secrets: DB passwords, API keys, webhook secrets.
  4. Developer machines, if the window was days long. Laptops installed it too.

Two operational rules: revoke, then reissue (on some platforms the old secret stays valid until explicitly revoked), and deliver new secrets through a clean path. Pushing fresh credentials into the same CI with the tokens that just leaked is how incidents get sequels.

Use today:

  • Treat the lockfile grep as question one of four, not the verdict.
  • Default UNKNOWN to yes at every node.
  • Archive CI logs the same hour; retention will not wait for your postmortem.
  • Practice the rotation from a known-good machine before you need it.

What I will argue in the comments: if your CI has no egress logging and keeps build logs for days, you cannot answer Q4 honestly, so every alert becomes a full rotation. Is that an argument for fixing the observability gap, or is blanket rotation genuinely the cheaper policy at your scale?

Longer writeup with the full argument and commands: https://axeploit.com/blog/lockfile-presence-is-not-execution-the-first-60-minutes-of-an-npm-supply

Top comments (0)