TRENDING
Rows of identical brass-colored apartment mailboxes with small locks and name labels along an orange corridor wall
October 9, 2026
How to Prevent Broken Object Level Authorization (IDOR) in a FastAPI App
Street-level upward view of the Monetary Authority of Singapore building and neighbouring office towers under a pale sky
October 9, 2026
Singapore’s AI Guidelines Turn Independent Review Into a Question of Who Sets the Risk Rating
Cast-iron late Qing dynasty coin minting press with a large flywheel, displayed in a museum case
October 9, 2026
Attackers Hijacked the .gh, .sl and .as Country Domains and Minted HTTPS Certificates for Google
Rows of closed oak library card catalog drawers, each with a brass pull and a blank label holder
October 9, 2026
How to Encrypt PII in Python and Keep It Searchable With Blind Indexes
Close-up of a vintage Western Electric manual telephone switchboard with orange lamps, red patch cords plugged into jacks, a rotary dial and a black handset
October 9, 2026
Microsoft’s Agent Lightning v1.0 Turns Agent Training Into a Sample-Accounting Problem
09 Oct 2026
SXZ.io SXZ.io
  • Home
Search the Site
Popular Searches:
Technology Amazon AI
Recent Posts
Two orange safety relief valves on grey pressure vessels in an industrial plant
How to Add Backpressure and Load Shedding to a Python Service Before Overload Takes It Down
October 8, 2026
Yellow diamond-shaped merging traffic warning sign showing a side road joining a main road
GitHub’s Git Rebuild Turns Repository Durability and Read Scale Into Two Separate Problems
October 8, 2026
A lugworm lying on wet sand and mud at low tide
A Compromised Admin Account Put the Shai-Hulud Worm Into AI Sandbox Maker Tensorlake’s npm SDK
October 8, 2026
SXZ.io SXZ.io
  • Home

Categories

Articles 232 Posts
News 234 Posts
Learning Hub 204 Posts
Home/Learning Hub/How to Detect Malicious npm preinstall Scripts and Verify Package Integrity
Learning Hub

How to Detect Malicious npm preinstall Scripts and Verify Package Integrity

On the morning of August 4, 2026, attackers compromised the publishing path for keyv, a widely used npm caching library, along with 10 related packages from the same maintainer. Security researchers...

August 5, 2026 16 Min Read
63

On the morning of August 4, 2026, attackers compromised the publishing path for keyv, a widely used npm caching library, along with 10 related packages from the same maintainer. Security researchers at Snyk independently confirmed 11 malicious releases that added a hidden preinstall script, one that ran automatically the moment anyone typed npm install, before the application’s own code ever started. The three most-downloaded affected packages, keyv, flat-cache, and file-entry-cache, each logged more than 500 million downloads between July 5 and August 3 alone, though Snyk cautions those figures overlap heavily (the packages depend on each other) and are not additive counts of distinct exposure.

Table Of Content

  • What You Will Accomplish
  • What Is a Software Supply Chain Compromise?
  • Prerequisites
  • Step 1: See What a Lifecycle Script Actually Does
  • preinstall, install, and postinstall
  • Blocking Lifecycle Scripts With –ignore-scripts
  • Step 2: Audit Your Installed Packages for Suspicious Scripts
  • A Real Gotcha: Byte Order Marks Silently Break the Scan
  • Alternatives on Linux and macOS
  • Step 3: Verify Package Integrity With Hashes
  • How the Lockfile’s integrity Field Works
  • Cross-Referencing a Real Advisory’s Published Hashes
  • Step 4: Pin Known-Safe Versions With npm Overrides
  • Why You Cannot Always Just Upgrade
  • A Gotcha: Overrides Need a Full Reinstall to Take Effect
  • Applying This to a Real Incident
  • Step 5: Understand What “Verified” Does and Does Not Mean
  • Running npm audit signatures
  • Why Valid Provenance Did Not Stop the keyv Attack
  • Common Mistakes to Avoid
  • Verifying Everything Works End to End
  • Next Steps

This tutorial is not about that one incident. It is about the durable skill the incident demonstrates: how to check whether your own Node.js projects are exposed to a compromised package, right now and in the future. By the end, you will understand what npm lifecycle scripts actually do, how to audit your installed dependencies for suspicious ones, how package integrity hashes catch tampering, how to pin a dependency to a known-safe version even when you do not control it directly, and why a “verified” package is not automatically a safe one. We will use the keyv incident as a real, current worked example throughout, with hashes and commands verified directly against npm’s own registry.

What You Will Accomplish

By working through this tutorial, you will build and run a small toolkit that:

  • Demonstrates exactly what an npm lifecycle script can do the instant you run npm install
  • Scans your node_modules folder for packages carrying preinstall, install, or postinstall hooks
  • Shows how npm’s lockfile integrity checks catch a tampered package before it ever runs
  • Uses overrides in package.json to force a safe version of a dependency, even one buried several levels deep in your dependency tree
  • Explains what npm audit signatures actually verifies, and why that was not enough to stop the keyv attack

What Is a Software Supply Chain Compromise?

A software supply chain compromise happens when an attacker inserts malicious code into a legitimate package that other developers already trust and install, rather than attacking those developers directly. Because your project’s dependencies (and their dependencies, and so on) can number in the hundreds, a single compromised package deep in that tree can run with the same file system access, network access, and credentials as the developer or CI system installing it. npm’s lifecycle scripts make this especially dangerous: certain scripts in a package’s package.json run automatically during installation, with no confirmation prompt, unless you have explicitly disabled them.

Prerequisites

  • Node.js 18 or later and a matching npm version. This tutorial was built and tested with Node.js 24.9.0 and npm 11.6.0; any reasonably current version behaves the same way for everything shown here.
  • A terminal. The commands below work in PowerShell, Git Bash, or a Linux/macOS shell; a couple of Linux-specific alternatives are noted where relevant.
  • Basic familiarity with running npm install and reading a package.json file. No prior security experience is required.

Step 1: See What a Lifecycle Script Actually Does

preinstall, install, and postinstall

npm supports several “lifecycle hooks”, scripts defined under the scripts key in package.json that npm runs automatically at specific points. Three of them matter most for security: preinstall runs before a package’s files are placed into node_modules, install runs after, and postinstall runs after that. All three execute with the same operating system privileges as whoever (or whatever CI runner) is running npm install, which typically includes read access to environment variables, SSH keys, and any credentials the process can reach.

This is not a bug. Plenty of legitimate packages need it: tools that compile native code (like better-sqlite3) or fetch a platform-specific binary (like esbuild) use these hooks for real work. The problem is that npm cannot tell the difference between a legitimate build step and an attacker’s payload; both are just a command the package author chose to run.

To see this in action safely, create a small local package with its own preinstall hook. Start a new project:

mkdir npm-audit-demo
cd npm-audit-demo
npm init -y

Now create a local package that stands in for a dependency with an install hook. Create local-packages/build-hook-demo/package.json:

{
  "name": "build-hook-demo",
  "version": "1.0.0",
  "description": "Local demo package used to safely illustrate an npm preinstall lifecycle hook.",
  "main": "index.js",
  "scripts": {
    "preinstall": "node setup.js"
  },
  "license": "MIT"
}

And local-packages/build-hook-demo/setup.js:

const fs = require("node:fs");
const path = require("node:path");

const markerPath = path.join(process.cwd(), "PREINSTALL_RAN.txt");
fs.writeFileSync(
  markerPath,
  `preinstall executed at ${new Date().toISOString()} with the privileges of the user running npm install\n`
);
console.log("[build-hook-demo] preinstall script executed:", markerPath);

This script only writes a text file, but notice what it proves: it runs with process.cwd() resolving to wherever npm placed the package, it can read the current time and write files, and nothing about running it required your permission beyond running npm install in the first place. Install it alongside a real, well-known package for contrast:

npm install ./local-packages/build-hook-demo picocolors --save

Check whether the script ran:

ls node_modules/build-hook-demo/PREINSTALL_RAN.txt

The file is there. One useful detail to know when you are hunting for evidence later: npm runs a dependency’s lifecycle scripts with the working directory set to that package’s own folder inside node_modules, not your project root. That is exactly where the marker file appeared, and it is exactly where a real attacker’s script would look first for things like SSH keys or environment files relative to your project.

In the real keyv incident, the equivalent line in the compromised package.json was:

"preinstall": "node setup.mjs"

Snyk’s security research team found that this 29,918-byte loader script fetched a much larger, 727,680-byte second-stage payload named Math_Symbol.js, which searched infected machines for GitHub and npm tokens, cloud provider credentials, private keys, database connection strings, and even GitHub Actions runner memory, then tried to install a persistence mechanism to survive after the initial compromise. It is the same mechanism as the harmless demo above, just aimed at something far more damaging.

Blocking Lifecycle Scripts With –ignore-scripts

npm gives you a direct way to refuse all of this: the --ignore-scripts flag. Remove the installed packages and reinstall with it:

rm -rf node_modules package-lock.json
npm install --ignore-scripts

Check for the marker file again:

ls node_modules/build-hook-demo/PREINSTALL_RAN.txt
# No such file or directory

The package files are still installed normally; only the lifecycle script was skipped. This is a real, immediately usable defense, but it is a blunt one: if any of your real dependencies genuinely need their install script to function (a native module that needs compiling is the most common case), that package will silently be broken until you either allow scripts for it specifically or build it another way. Treat --ignore-scripts as a default worth adopting for CI and first installs, then verify your build still works before you rely on it.

Step 2: Audit Your Installed Packages for Suspicious Scripts

Blocking scripts going forward is good, but it does not tell you whether something already installed is carrying a lifecycle hook you have not reviewed. For that, you need to scan what is already in node_modules.

Snyk’s own incident writeup for the keyv compromise suggests a Linux one-liner using find, xargs, and a small inline Node.js script. That works well on Linux and macOS, but find and xargs are not available by default on Windows, and plenty of developers run their day-to-day tooling there. The fix is to write the whole scan in Node.js itself, which runs identically everywhere npm does. Create audit-lifecycle-scripts.mjs:

#!/usr/bin/env node
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";

const ROOT = process.argv[2] || "node_modules";
const RISKY_HOOKS = ["preinstall", "install", "postinstall"];

function walk(dir, found) {
  let entries;
  try {
    entries = readdirSync(dir);
  } catch {
    return;
  }

  const pkgJsonPath = join(dir, "package.json");
  try {
    if (statSync(pkgJsonPath).isFile()) {
      // Strip a leading UTF-8 BOM before parsing; see the note below.
      const raw = readFileSync(pkgJsonPath, "utf8").replace(/^/, "");
      const pkg = JSON.parse(raw);
      const scripts = pkg.scripts || {};
      const hits = RISKY_HOOKS.filter((hook) => scripts[hook]);
      if (hits.length > 0) {
        found.push({
          name: pkg.name || "(unknown)",
          version: pkg.version || "(unknown)",
          path: dir,
          hooks: hits.map((hook) => `${hook}: ${scripts[hook]}`),
        });
      }
    }
  } catch {
    // Not a package directory, or an unreadable package.json. Skip it.
  }

  for (const entry of entries) {
    if (entry === ".bin") continue;
    const full = join(dir, entry);
    let isDir = false;
    try {
      isDir = statSync(full).isDirectory();
    } catch {
      continue;
    }
    if (!isDir) continue;

    if (entry.startsWith("@")) {
      walk(full, found);
    } else if (entry !== "node_modules") {
      walk(full, found);
      // Recurse into nested node_modules too: a transitive dependency
      // can carry its own hook even when the top-level package is clean.
      walk(join(full, "node_modules"), found);
    }
  }
}

const found = [];
walk(ROOT, found);

if (found.length === 0) {
  console.log(`No preinstall, install, or postinstall scripts found under ${ROOT}.`);
  process.exit(0);
}

console.log(`Found ${found.length} package(s) with install-time lifecycle scripts under ${ROOT}:\n`);
for (const pkg of found) {
  console.log(`  ${pkg.name}@${pkg.version}`);
  console.log(`    path: ${pkg.path}`);
  for (const hook of pkg.hooks) {
    console.log(`    ${hook}`);
  }
  console.log("");
}
process.exit(1);

Reinstall normally (scripts allowed this time, so the scan has something realistic to find) and run it:

npm install
node audit-lifecycle-scripts.mjs

Output:

Found 1 package(s) with install-time lifecycle scripts under node_modules:

  [email protected]
    path: node_modules\build-hook-demo
    preinstall: node setup.js

picocolors is not flagged, because it genuinely has no lifecycle scripts. The tool exits with status code 1 when it finds something, and 0 when it does not, which makes it easy to wire into a pre-commit hook or a CI job as a hard gate later.

A Real Gotcha: Byte Order Marks Silently Break the Scan

While building this script, the first version of the demo package’s package.json was created with a text editor that added a UTF-8 byte order mark (BOM), an invisible three-byte sequence (EF BB BF) at the very start of the file. JSON.parse() throws on a leading BOM, and because that error was inside a broad try/catch, the scanner silently skipped the package instead of reporting an error, reporting zero results even though the malicious-shaped package was sitting right there. The fix is the one already in the script above: strip a leading BOM before parsing. It is a one-line fix, but it is the kind of thing that turns an audit tool from “found nothing” (reassuring, but wrong) into “actually found nothing” (true). If you are writing your own JSON-scanning tooling, treat this defensively: some editors and Windows-native tools add a BOM to text files, and a scanner that trusts every package.json to be BOM-free will quietly miss anything saved by one of them.

Alternatives on Linux and macOS

If you are on a Unix-like system with ripgrep installed, Snyk’s incident post recommends checking your lockfiles directly by name, which is faster than walking the filesystem when you already know what you are looking for:

rg -n 'keyv|flat-cache|file-entry-cache|cacheable-request|cacheable|cache-manager|@cacheable/|ecto' \
  package-lock.json npm-shrinkwrap.json pnpm-lock.yaml yarn.lock

That is a good complement to the script above: the lockfile search is fast and specific when you already have a list of package names from a security advisory, while the Node.js scanner is useful for a general sweep when you do not know what you are looking for yet.

Step 3: Verify Package Integrity With Hashes

How the Lockfile’s integrity Field Works

Every package your project installs gets an entry in package-lock.json with an integrity field: a Subresource Integrity (SRI) hash, almost always SHA-512, encoded in base64. Here is the real entry for picocolors from the project above:

{
  "version": "1.1.1",
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
  "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
  "license": "ISC"
}

When you run npm ci (the command designed for reproducible installs from an existing lockfile, commonly used in CI), npm downloads each tarball and recomputes its hash before extracting it. If the computed hash does not match what is recorded in the lockfile, npm refuses to install it. To see this for real, deliberately corrupt the recorded hash:

node -e "
const fs = require('fs');
const lock = require('./package-lock.json');
lock.packages['node_modules/picocolors'].integrity =
  'sha512-AAAAtamperedHashThatWillNeverMatchAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==';
fs.writeFileSync('./package-lock.json', JSON.stringify(lock, null, 2));
"
rm -rf node_modules
npm ci

The real output:

npm warn tarball tarball data for picocolors@https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz (sha512-AAAA...) seems to be corrupted. Trying again.
npm error code EINTEGRITY
npm error sha512-AAAA... integrity checksum failed when using sha512: wanted sha512-AAAA... but got sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==. (2625 bytes)

npm shows you exactly what it wanted versus what it actually downloaded, and it refuses to proceed. This is the mechanism that protects you if a tarball were ever swapped out from under an existing, trusted lockfile entry. Restore a correct lockfile before continuing:

rm -f package-lock.json
npm install

Cross-Referencing a Real Advisory’s Published Hashes

Security advisories for a supply chain compromise typically publish the hashes of the malicious files themselves, so defenders can check their own systems without needing to trust a version number alone (version numbers and dist-tags can be manipulated or rolled back; a cryptographic hash of the actual bytes cannot). For the keyv incident, Snyk independently downloaded and hashed the compromised files:

keyv-6.0.0.tgz
  sha256 d584f9b6af48b7ed1f93713944f033783bf149e1c25e1643eb8c0e9df5dc7782

setup.mjs (29,918 bytes)
  sha256 54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668

Math_Symbol.js (727,680 bytes)
  sha256 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc

If you had these packages installed and wanted to check for the exact malicious files, you would compute the SHA-256 of anything suspicious in node_modules and compare it against values like these from the advisory:

# Linux/macOS
sha256sum node_modules/keyv/setup.mjs

# Windows PowerShell
Get-FileHash node_modules\keyv\setup.mjs -Algorithm SHA256

A match is definitive proof of compromise; it means the exact bytes on your disk are the exact bytes the security researchers analyzed.

While writing this tutorial, the npm registry’s own metadata for keyv was queried directly (a safe, read-only request; this only fetches JSON package metadata over HTTPS, it does not install or execute anything) to independently verify the incident’s current state. As of this writing, [email protected] has already been fully removed from the registry: it still appears in the package’s publish-time history (timestamped 2026-08-04T09:35:00.763Z, matching Snyk’s reported publish time of 09:35 UTC), but it no longer appears among the installable versions at all, which is a stronger response than simply moving the latest tag. The registry’s latest tag for keyv now points to 5.6.0, and every other affected package (flat-cache, cacheable-request, cache-manager, file-entry-cache) has likewise rolled back to the exact clean versions Snyk documented in its advisory.

Step 4: Pin Known-Safe Versions With npm Overrides

Why You Cannot Always Just Upgrade

Sometimes the compromised (or vulnerable) package is not one you depend on directly; it is a dependency of a dependency, several levels down, and you have no package.json entry for it to edit. npm’s overrides field solves exactly this: it lets you force a specific version of a package everywhere it appears in your dependency tree, no matter how deep, without needing to fork or directly depend on it yourself.

To see this with a real, deep dependency chain rather than a contrived one, add yargs to the demo project. It is a genuinely popular command-line argument parser, and it pulls in ansi-styles four levels down through its own dependencies:

npm install yargs
npm ls --all
[email protected]
├── [email protected] -> .\local-packages\build-hook-demo
├── [email protected]
└─┬ [email protected]
  └─┬ [email protected]
    └─┬ [email protected]
      └── [email protected]

Suppose a security advisory told you ansi-styles needed to be at 5.2.0 or later. You do not depend on it directly, so a plain npm install [email protected] would only add a new, separate top-level dependency; it would not touch the copy nested under yargs. Add an override instead, in package.json:

{
  "overrides": {
    "ansi-styles": "5.2.0"
  }
}

A Gotcha: Overrides Need a Full Reinstall to Take Effect

Adding that block and running a plain npm install on top of an existing lockfile is not enough; npm may report “up to date” and leave the old, nested version exactly where it was, because it does not always re-resolve the whole tree against a change to overrides alone. Force it by removing the lockfile and node_modules first:

rm -rf node_modules package-lock.json
npm install

Now the override is applied, and npm ls tells you so explicitly:

npm ls ansi-styles --all
[email protected]
└─┬ [email protected]
  └─┬ [email protected]
    └─┬ [email protected]
      └── [email protected] overridden

The word “overridden” at the end of that line is npm confirming the override actually took effect, not just that the version happens to match.

One more real edge case worth knowing before you rely on this in production: if the package you want to override is also a direct dependency of your own project (listed in your own dependencies), npm requires the override to match that direct dependency’s declared version range, or it fails outright with an error like this (captured from a real attempt to override a direct dependency to a version outside its declared range):

npm error code EOVERRIDE
npm error Override for picocolors@^1.1.1 conflicts with direct dependency

If you hit this, either update the direct dependency’s declared version in package.json to match, or use npm’s $ reference syntax (documented in npm’s own package.json reference) to point the override at whatever version you declare directly, so the two stay in sync automatically.

Applying This to a Real Incident

For the actual keyv compromise, Snyk published the exact override block needed to pin every affected package to its last known-clean version in one step:

{
  "overrides": {
    "keyv": "5.6.0",
    "flat-cache": "6.1.23",
    "file-entry-cache": "11.1.5",
    "cacheable-request": "13.0.19",
    "cacheable": "2.5.0",
    "@cacheable/utils": "2.5.0",
    "cache-manager": "7.2.9",
    "@cacheable/net": "2.1.0",
    "@cacheable/node-cache": "3.1.1",
    "@cacheable/memory": "2.2.0",
    "ecto": "5.0.0"
  }
}

Add a block like that, then rebuild cleanly exactly as above: delete node_modules and the lockfile, reinstall (ideally with --ignore-scripts the first time, so nothing has a chance to run before you confirm the versions), and confirm with npm ls <package> --all that every instance in your tree now resolves to the pinned, clean version.

Step 5: Understand What “Verified” Does and Does Not Mean

Running npm audit signatures

npm can check that every package you have installed carries a valid cryptographic signature from the registry, confirming the bytes you downloaded match what the registry actually holds. Run it in the demo project:

npm audit signatures
audited 15 packages in 0s

15 packages have verified registry signatures

That is a genuinely useful check: it protects against tampering in transit or a compromised mirror serving you something the real registry never had. It is not, however, a check on whether the code itself is safe to run.

Why Valid Provenance Did Not Stop the keyv Attack

This distinction is not theoretical; it is exactly what happened in the keyv incident. According to Snyk’s research, the npm registry’s manifest for the malicious [email protected] release identified GitHub Actions as a verified, trusted publisher, with a valid npm provenance attestation attached. Provenance attestation is meant to prove that a package was built by a specific, auditable CI workflow from a specific commit, rather than uploaded by hand from someone’s laptop; it is a real, valuable supply chain security control.

The problem was upstream of that control: the malicious setup.mjs and Math_Symbol.js files were already committed into the repository before the GitHub Actions workflow ran. The workflow did exactly what it was designed to do: it built the package from the repository’s actual contents and truthfully attested that it had done so. A now-removed test in the same commit even executed setup.mjs directly during the CI run itself, before a later commit quietly deleted that test. Valid provenance tells you who built a package and that the published artifact matches what that specific build produced; it does not, and cannot, tell you that what was committed to the repository was safe in the first place.

The practical takeaway: treat “verified publisher” and “valid provenance” badges as proof of a clean chain of custody from source to registry, not as proof that the source itself is trustworthy. They narrow down who to hold accountable and make tampering after the fact detectable; they do not replace the kind of hash verification, lifecycle script auditing, and dependency pinning covered in the steps above.

Common Mistakes to Avoid

  • Assuming node_modules root is where a script’s evidence lands. A dependency’s lifecycle script runs with its working directory set to that package’s own folder inside node_modules, not your project root. Look inside each package’s directory, not just the top level, when you are hunting for something a script may have written.
  • Trusting a naive JSON parser on arbitrary package.json files. A leading byte order mark will make a plain JSON.parse() throw, and a broad try/catch around it can turn a real finding into a silent false negative. Strip a BOM defensively in any tooling that walks third-party package.json files.
  • Adding an override and assuming a plain npm install picked it up. If a lockfile already exists, npm may not re-resolve the tree against a new overrides entry. Delete node_modules and package-lock.json and reinstall clean, then confirm with npm ls <package> --all.
  • Overriding a package that is also a direct dependency without reconciling the version range. npm will refuse with EOVERRIDE unless the override matches the direct dependency’s declared range, or you use the $ reference syntax to keep them in sync.
  • Expecting npm audit to flag a brand-new compromise immediately. There is a real lag between a package being compromised and a CVE or GitHub Security Advisory being assigned. During Snyk’s own research into the keyv incident, no CVE or GHSA record had been assigned yet, even as active exploitation was underway. Do not treat a clean npm audit as proof of safety for a package involved in a fast-moving, ongoing incident; check the vendor’s own advisory and the package’s real registry state directly.
  • Checking only top-level dependencies. Run npm ls <package> --all, not the default depth-limited form, and make sure any custom audit tooling recurses into nested node_modules folders, since a compromised package can arrive as someone else’s transitive dependency just as easily as your own.

Verifying Everything Works End to End

Before you trust this pipeline on a real project, confirm each piece independently, in order:

rm -rf node_modules package-lock.json
npm install
node audit-lifecycle-scripts.mjs   # exits 1 only for packages you have reviewed and expect
npm ci                              # exits 0; lockfile integrity matches every tarball
npm audit signatures                # confirms registry signatures, not source safety
npm ls --all                        # confirms every override actually resolved

If the audit script reports a package you do not recognize, if npm ci ever reports EINTEGRITY on a real install (not a deliberate test like the one above), or if npm ls shows a version your overrides should have pinned but did not, stop and investigate before shipping or deploying.

Next Steps

Once this works locally, the natural next step is making it automatic rather than something you remember to run by hand:

  • Run node audit-lifecycle-scripts.mjs as a CI step (it exits non-zero on any finding) or a Git pre-commit hook, so a new lifecycle script anywhere in your tree fails the build until someone reviews it.
  • Pair this with a dependency cooldown policy, giving the community time to catch a fresh compromise before you ever install it. sxz.io covers that separately in How to Set Up Dependency Cooldowns in npm and pip to Block Fresh Malicious Packages; the two techniques are complementary, cooldowns reduce your exposure to brand-new compromises, while the auditing in this tutorial catches what is already installed or slips through anyway.
  • If you publish your own packages, look at signing and attesting your own build artifacts so downstream users get the chain-of-custody benefits described in Step 5. sxz.io’s How to Generate and Sign an SBOM for a Container Image With Syft and Cosign covers the same underlying ideas applied to containers.
  • Consider a continuous monitoring tool (Snyk, Socket.dev, or similar) that watches your dependency tree for newly disclosed compromises after you have already deployed, since no amount of auditing at install time catches a package that becomes malicious after you already trusted it.

Tags:

DevSecOpsJavaScriptNode.jsnpmSupply Chain Security

Share

The White House North Portico and North Lawn fountain in Washington, D.C.
Previous Post

The White House Turns Frontier AI Cybersecurity Oversight Into a Secrecy Fight

Marines at computer workstations in a red-lit cyber operations room
Next Post

IBM’s Langflow Faces a Critical RCE Under Active Attack as CISA Sets a Three-Day Deadline

No Comment! Be the first one.

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Latest
08 Oct
How to Add Backpressure and Load Shedding to a Python Service Before Overload Takes It Down
08 Oct
GitHub’s Git Rebuild Turns Repository Durability and Read Scale Into Two Separate Problems
Trending
October 8, 2026
How to Add Backpressure and Load Shedding to a Python Service Before Overload Takes It Down
October 8, 2026
GitHub’s Git Rebuild Turns Repository Durability and Read Scale Into Two Separate Problems
October 8, 2026
A Compromised Admin Account Put the Shai-Hulud Worm Into AI Sandbox Maker Tensorlake’s npm SDK
October 8, 2026
How to Prevent Broken Object Level Authorization (IDOR) in a FastAPI App
October 8, 2026
Singapore’s AI Guidelines Turn Independent Review Into a Question of Who Sets the Risk Rating
October 8, 2026
Attackers Hijacked the .gh, .sl and .as Country Domains and Minted HTTPS Certificates for Google

Related Posts

A laptop wrapped in a chain and padlock, illustrating least-privilege controls for AI agents.
Learning Hub

How to Secure Tool-Using AI Agents Before They Touch Production

June 8, 2026
Colorful sticky notes arranged on an office wall, symbolizing governance checklists and planning.
Learning Hub

AI Governance for Agentic Apps: A Practical Checklist for Builders

June 8, 2026
A technician connects green fiber optic cables at a data center, representing a private production inference endpoint.
Learning Hub

How to Deploy a Fine-Tuned LLM Behind a Private Production Inference Endpoint

June 8, 2026
Narrow aisle behind black supercomputer racks in a data center
Learning Hub

Kubernetes SELinux Volume Labeling: What Cluster Operators Should Audit Before v1.37

June 8, 2026
SXZ.io SXZ.io
  • [email protected]

Categories

Articles
Learning Hub
News

All Rights Reserved by SXZ.io ©2026