Security guides
Security guide

A Dependency Was Compromised: How to Respond to an npm Supply Chain Attack

By Sazabi Team

Last verified on Nov 8, 2026.

When a package in your dependency tree is reported as compromised, the work is the same each time: find out whether you installed the bad version and when, find out what ran on those machines, rotate whatever those machines could reach, and rebuild from a clean state. This guide is the response. For finding malicious packages before an incident, see supply chain attack detection.

Step 1: Pin Down the Bad Versions and the Window

Get three facts from the advisory before you search: the package name, the affected versions, and when the malicious versions were published and removed. That gives you an exposure window.

  • GitHub Advisory Database. Malware advisories are published from information provided by the npm security team, are exclusive to npm, and are found by searching type:malware. GitHub notes that Dependabot does not alert on malware, so you will not be notified automatically.
  • Via the API. The global advisories API excludes malware by default. Add the type parameter:
gh api '/advisories?type=malware&ecosystem=npm&affects=example-package' --jq '.[] | [.ghsa_id, .summary, .published_at] | @tsv'
  • OpenSSF Malicious Packages. The repository collects reports of malicious packages in OSV format, including account takeover and typosquatting cases.
  • The registry. npm view prints publish times for each version:
npm view example-package time --json | jq 'to_entries[] | select(.key | test("^[0-9]")) | "\(.value)  \(.key)"' -r | tail -10

For vulnerabilities rather than malware, cve.now matches your lockfile against known advisories. Upload package-lock.json, pnpm-lock.yaml, yarn.lock, or bun.lock, or post it with curl --data-binary @package-lock.json -H 'X-Lockfile-Name: package-lock.json' https://cve.now/api/stack. The check your stack docs note that a result with no matches does not prove your packages are safe, since it covers advisories that list affected versions.

Step 2: Check Whether You Installed It

Lockfiles in the current tree. npm ls prints the versions installed and, with --all, every path through the tree that includes the package:

npm ls example-package --all

Or read the lockfile directly. This lists every copy and whether it has an install script, using the hasInstallScript flag that package-lock.json records for packages with preinstall, install, or postinstall scripts:

jq -r '.packages | to_entries[]
  | select(.key | test("(^|/)node_modules/example-package$"))
  | [.key, .value.version, (.value.hasInstallScript // false)] | @tsv' package-lock.json

For other package managers, search the lockfile text (grep -n 'example-package@' yarn.lock pnpm-lock.yaml).

Lockfiles in history. The version you run now may not be the version you ran during the window. Find when the package entered or changed in the lockfile:

git log --format='%h %ad %an %s' --date=iso -S'node_modules/example-package' -- package-lock.json
git log -p -- package-lock.json | grep -n -B2 -A3 'node_modules/example-package"'

Run the search across every repository your organization builds, not only the one you were told about. A script that clones each repo and runs the jq above takes minutes.

Resolved ranges. npm ci installs exactly what the lockfile pins and errors if the lockfile and package.json disagree. npm install can resolve a newer version inside a semver range. A build that used npm install, or had no lockfile committed, can have pulled the bad version even though no lockfile ever listed it. Treat those builds as exposed if they ran in the window.

Machines and images. Developer laptops and build hosts keep their own node_modules:

find / -type d -path '*/node_modules/example-package' -prune \
  -exec sh -c 'printf "%s " "$1"; jq -r .version "$1/package.json"' _ {} \; 2>/dev/null

Check container images built during the window the same way, because a layer keeps the bad version after you fix the source.

Step 3: Check What Ran

Installing a package can run code. npm runs preinstall, install, and postinstall lifecycle scripts as part of an install, so a malicious version can execute on every machine and CI job that installed it, before any of your code runs. Read the package's scripts for the affected version:

npm view example-package@1.2.3 scripts --json

Then check CI. With GitHub Actions, list runs in the window and search the install step output:

gh run list --created '2026-11-01..2026-11-08' --limit 200 --json databaseId,name,createdAt \
  --jq '.[] | [.databaseId, .name, .createdAt] | @tsv'
gh run view 1234567890 --log | grep -nE 'npm (ci|install)|example-package@|added [0-9]+ packages'

In those logs, look for:

  • install steps that ran during the window and which command was used;
  • output printed by the package during install, such as a postinstall line you do not recognize;
  • unexpected steps, new network destinations, or processes launched after the install. CI logs show commands and output but not network connections, so use egress or proxy logs for the runners if you keep them, and compare destinations contacted during install steps against a normal run.

On developer machines, npm writes debug logs to a _logs directory in the npm cache, but removes the oldest when the count exceeds logs-max, so they may not reach back to the window. Shell history, endpoint tooling, and DNS or proxy logs are better sources.

If you cannot rule out that the code ran, assume it ran. That decision drives the next step.

Step 4: Rotate What the Install Could Reach

Install-time code runs with the permissions of whoever ran the install. Treat every secret visible to that process as exposed:

  • CI: repository and organization secrets exposed to the job, environment variables, deploy keys, and tokens the job could mint, such as cloud credentials from federated identity.
  • Developer machines: .env files, ~/.npmrc tokens, SSH keys, cloud CLI credentials, GitHub tokens, and anything in shell environment.
  • Publishing credentials: if your organization publishes packages, rotate npm tokens and review your own recent releases. List and delete tokens by ID:
npm token list
npm token delete 123456

npm warns revocation can take up to an hour. Then follow the order in leaked API key: revoke, find where the value went, check the provider's audit log for use during the window. For AWS credentials, see leaked AWS access key. If you are unsure what a CI job could read, secrets in logs covers finding credentials that were printed.

Check provider logs for use from addresses outside your CI and office ranges, starting at the first moment a bad install could have happened.

Step 5: Remove, Rebuild, and Tighten

  1. Pin to a known-good version the advisory names, or remove the dependency. Update the lockfile and commit it.
  2. Delete and rebuild. Remove node_modules and rebuild from the lockfile with npm ci. Rebuild container images and redeploy anything built from the bad version. Treat any host that ran the package as untrusted if the payload could have persisted, and rebuild it from a clean image.
  3. Verify what you installed. npm audit signatures verifies registry signatures and provenance attestations of downloaded packages. It is not a malware scan; a package can be signed and still malicious.
  4. Audit, with limits. npm audit submits your dependencies to the registry for known vulnerabilities. Do not rely on it to flag a malicious release; use the malware advisories above.
  5. Reduce install-time exposure. Setting ignore-scripts=true stops npm running install scripts from package.json files, which closes the most common path for an install-time payload. Install in CI with npm ci, minimal secrets in the install step, and no long-lived credentials on the runner.

Watching for the Next One

Subscribe to your stack's feed on cve.now to hear about vulnerabilities that affect the packages in your lockfile, and keep CI and audit logs searchable for the next window. Sazabi, an AI-native observability and security platform, can stream a GitHub Enterprise Cloud audit log, which records personal access token and deploy key activity and Actions secret changes, next to your application logs.

Frequently Asked Questions

How do I know if I installed a compromised npm package version?

Search every lockfile, in every repository and in git history, for the package at the affected versions. Also check CI logs from the exposure window and any machine with a node_modules folder, because a build that resolved a version range can pick up a version your lockfile never listed.

Is npm audit enough to find a malicious package?

No. npm audit reports known vulnerabilities from the registry. GitHub publishes malware advisories separately and does not send Dependabot alerts for them, so check the GitHub Advisory Database with type:malware, the OpenSSF malicious packages reports, and the announcement from the maintainers.

Which secrets should I rotate after a compromised package?

Every secret that was available to a process that installed or ran the package: CI environment variables and repository secrets, npm tokens in .npmrc, cloud and GitHub credentials on developer machines, and .env files. Rotate before you finish investigating.

Does ignore-scripts protect me?

It stops npm running install scripts, which closes the most common install-time attack path. It does not help if the malicious code runs when the package is imported, so it reduces risk without replacing rotation and rebuilding.

Sources

  1. GitHub malware advisories come from the npm security team, are exclusive to npm, are found with type:malware, and do not produce Dependabot alerts (accessed Nov 8, 2026)
  2. The global advisories API excludes malware by default and lists it when the type parameter is malware (accessed Nov 8, 2026)
  3. The OpenSSF Malicious Packages repository collects reports of malicious packages in OSV format, including account takeover and typosquatting (accessed Nov 8, 2026)
  4. npm ls prints installed package versions and, with --all, the full dependency tree, limited to a named package by a positional argument (accessed Nov 8, 2026)
  5. package-lock.json records each installed package version and integrity, with a hasInstallScript flag for packages that have preinstall, install, or postinstall scripts (accessed Nov 8, 2026)
  6. npm ci requires a lockfile, errors if it does not match package.json and never writes to the lockfile (accessed Nov 8, 2026)
  7. npm runs preinstall, install, and postinstall lifecycle scripts when a package is installed (accessed Nov 8, 2026)
  8. The ignore-scripts setting stops npm running scripts from package.json files during install (accessed Nov 8, 2026)
  9. npm view can print package fields as JSON, including a version publish time (accessed Nov 8, 2026)
  10. npm audit submits dependencies to the registry for a report of known vulnerabilities; npm audit signatures verifies registry signatures and provenance attestations (accessed Nov 8, 2026)
  11. npm access tokens are listed with npm token list and revoked with npm token delete by ID, and revocation can take up to an hour (accessed Nov 8, 2026)
  12. gh run view --log prints the full log of a GitHub Actions run (accessed Nov 8, 2026)
  13. cve.now matches the package versions in a lockfile against known advisories with affected version ranges and offers a feed of vulnerabilities affecting your packages; no match is not proof of safety (accessed Nov 8, 2026)