All articles

npm supply chain attacks: How to audit your dependency controls

The Chainguard Team
Software Supply ChainDevSecOps
Key Takeaways
  • Configuration doesn't prove control: trace a dependency's real path through selection, endpoint, artifact evidence, and execution.

  • The Keyv/ChainDrop attack (1,300+ versions) carried valid SLSA provenance yet ran a malicious preinstall, so provenance isn't safety.

  • Chainguard Libraries for JavaScript delivers Chainguard-built packages with signed provenance and SPDX SBOMs, verified via chainctl.

An npm supply chain attack compromises software through an npm package, or the systems used to publish, select, distribute, and install dependencies. The attack may begin with a compromised publisher, an unexpected dependency selection, or a request that bypasses the intended registry. It may also involve a substituted artifact or code that runs during installation. Each step requires a different control, so a list of security tools can’t prove that every control applied to a particular install.

A dependency control must govern the package’s actual path into the build. A configured repository doesn’t prove that a request followed the intended route or that the expected checks applied to the artifact the build received.

How to audit the dependency path: Ask these four questions

Publisher controls govern how your team releases its own packages, and they sit outside this audit. If you publish, harden that path separately — npm's December 2025 authentication changes revoked classic tokens and made multi-factor authentication (MFA) the default for new packages and enforced it in interactive publishing sessions. However, a maintainer can still create granular tokens of up to 90 days that bypass MFA for publishing, and a phished session is still a publishing session. What follows traces the packages you consume, where a compromised publisher is someone else’s account and your problem.

Evaluating what your build consumes requires different evidence. It’s gathered by tracing one dependency from version selection through retrieval, artifact verification, and execution.

Direct public-registry access, registry overrides, alternate upstream sources, fallback, mirrors, and caches can create gaps between the repository npm was configured to use, the source that fulfilled the request, and the artifact the build received. Configuration alone cannot prove that the intended controls applied.

Use four questions to trace those gaps through a real install:

  1. What selected this package and version?

  2. Which endpoint fulfilled the request?

  3. What evidence belongs to this exact artifact?

  4. What can execute, and what can it reach?

To audit your dependency path, answer the following four questions in order. Compare the evidence at each step with the control you expected to apply: The first step that is uncontrolled, bypassed, or unsupported by evidence is the first gap to fix.

1. What selected this package and version?

Find the exact package and version your build installed. Then, figure out why it entered the dependency tree. It may be listed directly in package.json, pulled in by another dependency, selected from a version range, or changed by an override or workspace rule.

Use the committed package-lock.json to trace that decision. It records the dependency tree and the integrity information npm uses to check retrieved packages.

To follow a transitive path, npm ls <package> lists what is installed along with the dependencies that pulled it in, and npm explain <package> shows the chain of dependency relationships that put a specific version in the tree.

Record this information for the package you're auditing:

  • The package’s exact name and version

  • Whether it is a direct or transitive dependency

  • The manifest entry, dependency, range, override, or workspace rule that selected it

  • The npm version and install command settings used in CI. npm install can rewrite the dependency tree during the install, so if CI did not use npm ci, the committed lockfile might not describe the resolution that produced the artifact you are auditing

This baseline alone doesn’t necessarily give you insight into whether or not your version is compromised or malicious, but it’s important information to have as you move forward.

2. Which endpoint fulfilled the request?

Your registry configuration shows what registry your npm instance is configured to use, but it doesn’t prove which endpoint fulfilled a particular package request. Check the package’s resolved entry in the committed lockfile and npm’s replace-registry-host setting, as both affect where npm retrieves the tarball.

A repository manager may also retrieve the package from another upstream registry or return a cached copy. The configured URL alone can’t prove that the checks on your intended repository applied.

For the package you are auditing, record:

  • The effective registry setting used in CI, including scoped registry mappings, command-line or environment overrides, and any relevant replace-registry-host settings

  • The package’s resolved value in the committed lockfile

  • The metadata and tarball endpoints npm actually contacted, based on available npm HTTP, proxy, or egress logs

  • Whether the request was satisfied by npm’s local cache, a repository-manager cache, or an upstream registry

  • The results of controlled installs that isolate each cache layer, such as an install with an empty local npm cache and a repeat install with a fresh client cache after the repository cache has been populated

This evidence shows if the request passed through your intended repository and, in most cases, will help you determine which source behind it supplied the package. If npm retrieved the package directly from the public registry or from an unexpected upstream source, controls attached to the intended route did not run.

3. What evidence belongs to this exact artifact?

The registry that served a package does not establish who built it. Does the evidence you rely on belong to the exact package bytes your build used? Does it identify a signer, builder, source, or publishing process that your organization can trust?

Examine the local tarball or package-cache entry consumed by the build rather than on a release page, package name, or repository URL. For the package you are evaluating, ask:

  • What is the artifact’s digest, and does it match the integrity value in the committed lockfile or another approved source of metadata?

  • If the artifact is signed, does the signature verify successfully, and does the verified signer identity match one your organization trusts for this package?

  • Is provenance or a publication attestation available, and does its subject digest match the artifact?

  • Is a Software Bill of Materials (SBOM) available, and how is it linked to this exact artifact (for example, through a matching digest or signed attestation)?

A valid digest, signature, or attestation supports only the authorship or integrity claim it was created to verify. It doesn't showsafe behavior.

The August 2026 compromise of Keyv shows the limit in practice. Singapore’s Cyber Security Agency reported that the wider ChainDrop campaign compromised more than 1,300 npm package versions, including keyv@6.0.0. Datadog Security Labs found that this version carried genuine SLSA provenance connecting it to the project’s repository and GitHub Actions workflow. Yet the package included a malicious preinstall script that ran a credential-stealing loader and could use stolen access to compromise additional packages and repositories. The provenance accurately described how the artifact was produced. It did not establish that the artifact’s behavior was safe.

How Chainguard provides build evidence

After verifying an artifact’s origin, you still need to decide whether its builder and build process meet your requirements. Teams that want packages from a known third-party builder can use Chainguard Libraries for JavaScript, which distributes Chainguard-built packages through the Chainguard Repository npm endpoint.

A Chainguard-built JavaScript package carries Chainguard-signed provenance, signatures, and an SPDX SBOM. Before bundling it, run chainctl libraries verify against the local artifact to confirm that Chainguard built the package your build actually received.

With optional fallback enabled, the same repository can also serve eligible upstream packages that Chainguard did not build. Those versions remain upstream-built and carry no Chainguard-signed provenance. Verify any npm publisher attestation on them separately, because the repository governs how a package is delivered; the evidence attached to the artifact identifies who built it.

4. What can execute, and what can it reach?

The first three questions establish how a package reached the build and what evidence belongs to it. They don’t prevent an admitted package from executing during installation, build and test processes, or application runtime. And while npm v12 blocks dependency lifecycle scripts by default, that doesn’t address older clients, approved scripts, and code that executes at runtime or when a package is imported.

For the package you’re auditing, ask:

  • Which install scripts, build tools, package commands, or runtime paths can execute its code?

  • For scripts permitted by the project’s allowScripts policy, why was the script approved for auto-run, why is the capability necessary, and what review or safety measures apply? Approval allows execution; it doesn’t prove benign behavior.

  • Which credentials, files, internal services, network destinations, and child-process capabilities can that code reach?

  • Which least-privilege, egress, isolation, and monitoring controls limit that reach?

Scanners and policy engines that run on installed code belong at this step too, and at a different one from routing: They inspect admitted or installed code, so they detect what registry admission missed rather than substituting for it.

If malicious package code does run, determine what it accessed or could have accessed — CI credentials, source code, deployment systems, internal services. That will tell you which credentials to replace, which systems to rebuild, and which logs to review for unauthorized activity. Test the plan with an incident drill where a package gains access to a CI credential. Confirm that someone owns the response, knows which logs to inspect, can replace exposed credentials, and can rebuild affected systems.

What to fix first

Run the four audit questions in order against one real dependency. At each step, compare the evidence you collected with the control you expected to apply, then start with the earliest missing or bypassed control. A control later in the path might limit damage from a malicious package, but it can’t make up for an earlier bypassed control.

After fixing the gap, repeat the audit from a clean build. Registry policies, cache contents, overrides, and script approvals can change the package path and the controls that apply.If the audit shows that a build downloads packages directly from the public npm registry, find out why the request bypasses your intended repository.

Teams that want a managed repository to govern that route can evaluate Chainguard Libraries through Chainguard Repository. Chainguard Repository can apply its controls only when the request reaches its npm endpoint.

Share this article
Execute commandCG System prompt

$ chainguard learn --more

Contact us