np-audit 1.3.0 → 1.5.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/LICENSE +21 -0
- package/README.md +131 -52
- package/package.json +1 -1
- package/src/cli.js +55 -181
- package/src/commands/alias.js +111 -0
- package/src/commands/ci.js +90 -0
- package/src/commands/config.js +71 -0
- package/src/commands/index.js +37 -0
- package/src/commands/install.js +109 -0
- package/src/commands/scan.js +82 -0
- package/src/core/detector.js +444 -0
- package/src/core/requireWalker.js +192 -0
- package/src/core/scanner.js +700 -0
- package/src/utils/command.js +256 -0
- package/src/{config.js → utils/config.js} +34 -2
- package/src/{output.js → utils/output.js} +22 -7
- package/src/{aware.js → utils/review.js} +56 -16
- package/src/{tarball.js → utils/tarball.js} +7 -1
- package/src/utils/updateChecker.js +72 -0
- package/src/detector.js +0 -300
- package/src/scanner.js +0 -407
- /package/src/{fetcher.js → utils/fetcher.js} +0 -0
- /package/src/{lockfile.js → utils/lockfile.js} +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Simon Kobler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/np-audit)
|
|
4
|
+
[](https://www.npmjs.com/package/np-audit)
|
|
5
|
+
[](https://www.npmjs.com/package/np-audit)
|
|
6
|
+
[](https://github.com/KoblerS/np-audit/blob/main/LICENSE)
|
|
7
|
+
[](https://github.com/KoblerS/np-audit/actions/workflows/ci.yml)
|
|
8
|
+
|
|
1
9
|
# np-audit — npm package auditor
|
|
2
10
|
|
|
3
11
|
Statically detect obfuscated code in npm `preinstall`/`postinstall` scripts **before** they run. Drop-in replacement for `npm install` and `npm ci`.
|
|
@@ -32,34 +40,59 @@ Real-world examples include:
|
|
|
32
40
|
- **[ua-parser-js (2021)](https://github.com/advisories/GHSA-pjwm-rvh2-c87w)** — cryptocurrency miner + info-stealer injected via hijacked maintainer account
|
|
33
41
|
- **[node-ipc (2022)](https://snyk.io/blog/peacenotwar-malicious-npm-node-ipc-package-vulnerability/)** — wiper malware targeting systems by geo-IP
|
|
34
42
|
- **[colors / faker (2022)](https://snyk.io/blog/open-source-npm-packages-colors-faker/)** — deliberate sabotage by the maintainer via postinstall
|
|
35
|
-
- **
|
|
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
|
|
36
44
|
|
|
37
45
|
**`npa` never executes the scripts.** It downloads and statically analyzes them, detecting:
|
|
38
46
|
|
|
39
|
-
| Signal
|
|
40
|
-
|
|
41
|
-
| `eval()` / `new Function()`
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
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…` |
|
|
45
57
|
| `String.fromCharCode()` chains | `String.fromCharCode(104,101,108,108,111)` |
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
| `
|
|
50
|
-
|
|
|
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`.
|
|
51
81
|
|
|
52
82
|
---
|
|
53
83
|
|
|
54
84
|
## Install
|
|
55
85
|
|
|
86
|
+
**Global install (recommended):**
|
|
87
|
+
|
|
56
88
|
```bash
|
|
57
|
-
# Global install (recommended for daily use)
|
|
58
89
|
npm install -g np-audit
|
|
90
|
+
```
|
|
59
91
|
|
|
60
|
-
|
|
92
|
+
**Or use directly with npx:**
|
|
93
|
+
|
|
94
|
+
```bash
|
|
61
95
|
npx np-audit scan
|
|
62
|
-
npx np-audit install
|
|
63
96
|
```
|
|
64
97
|
|
|
65
98
|
After global install, use the `npa` command:
|
|
@@ -74,24 +107,29 @@ npa --version
|
|
|
74
107
|
|
|
75
108
|
### Commands
|
|
76
109
|
|
|
77
|
-
| Command
|
|
78
|
-
|
|
79
|
-
| `npa install [package]`
|
|
80
|
-
| `npa ci`
|
|
81
|
-
| `npa scan`
|
|
82
|
-
| `npa config get`
|
|
83
|
-
| `npa config set <key> <value>` | `npa c set
|
|
110
|
+
| Command | Alias | Description |
|
|
111
|
+
| ------------------------------ | ----------- | ---------------------------------- |
|
|
112
|
+
| `npa install [package]` | `npa i` | Audit then run `npm install` |
|
|
113
|
+
| `npa ci` | — | Audit then run `npm ci` |
|
|
114
|
+
| `npa scan` | `npa s` | Scan only, no install |
|
|
115
|
+
| `npa config get` | `npa c get` | Show current configuration |
|
|
116
|
+
| `npa config set <key> <value>` | `npa c set` | Update a config value |
|
|
117
|
+
| `npa alias` | — | Print shell hook for auto-scanning |
|
|
118
|
+
| `npa alias --install` | — | Install hook to shell profile |
|
|
119
|
+
| `npa alias --uninstall` | — | Remove hook from shell profile |
|
|
120
|
+
|
|
121
|
+
Use `npa <command> -h` for detailed help on any command.
|
|
84
122
|
|
|
85
123
|
### Flags
|
|
86
124
|
|
|
87
|
-
| Flag
|
|
88
|
-
|
|
89
|
-
| `--
|
|
90
|
-
| `--json`
|
|
91
|
-
| `--no-dev`
|
|
92
|
-
| `--verbose` | —
|
|
93
|
-
| `--version` | —
|
|
94
|
-
| `--help`
|
|
125
|
+
| Flag | Alias | Works with | Description |
|
|
126
|
+
| ----------- | ----- | ----------------------- | ------------------------------------------------ |
|
|
127
|
+
| `--review` | `-r` | `install`, `ci` | Interactive mode — choose which scripts to allow |
|
|
128
|
+
| `--json` | — | `install`, `ci`, `scan` | Machine-readable JSON output |
|
|
129
|
+
| `--no-dev` | — | `install`, `ci`, `scan` | Skip devDependencies |
|
|
130
|
+
| `--verbose` | — | all | Show fetch progress and extra detail |
|
|
131
|
+
| `--version` | — | — | Print version and exit |
|
|
132
|
+
| `--help` | `-h` | — | Print help and exit |
|
|
95
133
|
|
|
96
134
|
### Drop-in replacement for `npm install`
|
|
97
135
|
|
|
@@ -118,23 +156,23 @@ npa s --no-dev # skip devDependencies
|
|
|
118
156
|
npa s --verbose # show fetch progress
|
|
119
157
|
```
|
|
120
158
|
|
|
121
|
-
### Interactive `--
|
|
159
|
+
### Interactive `--review` mode
|
|
122
160
|
|
|
123
161
|
Review each install script yourself and decide which to allow:
|
|
124
162
|
|
|
125
163
|
```bash
|
|
126
|
-
npa i --
|
|
127
|
-
npa ci --
|
|
164
|
+
npa i --review # or: npa i -r
|
|
165
|
+
npa ci --review
|
|
128
166
|
```
|
|
129
167
|
|
|
130
168
|
```
|
|
131
|
-
npa --
|
|
169
|
+
npa --review mode
|
|
132
170
|
Use ↑/↓ to navigate, SPACE to toggle, ENTER to confirm, q to quit
|
|
133
171
|
|
|
134
172
|
Found 3 package(s) with install scripts:
|
|
135
173
|
|
|
136
174
|
[✓ allow] esbuild@0.24.2 postinstall: post-install.js OK
|
|
137
|
-
▶ [✗ deny ] evil-sdk@1.0.0 postinstall: install.js
|
|
175
|
+
▶ [✗ deny ] evil-sdk@1.0.0 postinstall: install.js DANGER (score: 9)
|
|
138
176
|
[✓ allow] @scope/pkg@2.1.0 postinstall: install.js WARN (score: 5)
|
|
139
177
|
|
|
140
178
|
2 allowed 1 denied
|
|
@@ -159,26 +197,64 @@ npa config set skipScopes '["@types","@babel"]'
|
|
|
159
197
|
|
|
160
198
|
Config is stored in `~/.npmauditor.json` (global) and can be overridden per project with `.npmauditor.json` in your project root.
|
|
161
199
|
|
|
200
|
+
### Shell Hook (npm alias)
|
|
201
|
+
|
|
202
|
+
Automatically run `npa scan` before every `npm install` or `npm ci`:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# Install the hook to your shell profile (~/.zshrc or ~/.bashrc)
|
|
206
|
+
npa alias --install
|
|
207
|
+
|
|
208
|
+
# Reload your shell
|
|
209
|
+
source ~/.zshrc # or ~/.bashrc
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Now when you run `npm install` or `npm ci`, npa will scan first:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
$ npm install lodash
|
|
216
|
+
[npa] Scanning dependencies before npm install...
|
|
217
|
+
✔ No packages with install scripts found.
|
|
218
|
+
[npa] Scan passed. Running npm install...
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
If issues are found, the install is blocked:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
$ npm install evil-pkg
|
|
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.
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
To remove the hook:
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
npa alias --uninstall
|
|
234
|
+
```
|
|
235
|
+
|
|
162
236
|
#### All config keys
|
|
163
237
|
|
|
164
|
-
| Key
|
|
165
|
-
|
|
166
|
-
| `blockScore`
|
|
167
|
-
| `warnScore`
|
|
168
|
-
| `registry`
|
|
169
|
-
| `timeout`
|
|
170
|
-
| `parallelFetches` | `5`
|
|
171
|
-
| `skipScopes`
|
|
172
|
-
| `skipPackages`
|
|
238
|
+
| Key | Default | Description |
|
|
239
|
+
| ----------------- | ---------------------------- | --------------------------------------- |
|
|
240
|
+
| `blockScore` | `7` | Score threshold for hard block (exit 1) |
|
|
241
|
+
| `warnScore` | `4` | Score threshold for warning (exit 0) |
|
|
242
|
+
| `registry` | `https://registry.npmjs.org` | npm registry URL |
|
|
243
|
+
| `timeout` | `30000` | HTTP request timeout (ms) |
|
|
244
|
+
| `parallelFetches` | `5` | Concurrent tarball downloads |
|
|
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 |
|
|
173
249
|
|
|
174
250
|
---
|
|
175
251
|
|
|
176
252
|
## Exit codes
|
|
177
253
|
|
|
178
|
-
| Code | Meaning
|
|
179
|
-
|
|
180
|
-
| `0`
|
|
181
|
-
| `1`
|
|
254
|
+
| Code | Meaning |
|
|
255
|
+
| ---- | ----------------------------------- |
|
|
256
|
+
| `0` | All packages clean or only warnings |
|
|
257
|
+
| `1` | One or more packages blocked |
|
|
182
258
|
|
|
183
259
|
---
|
|
184
260
|
|
|
@@ -187,10 +263,13 @@ Config is stored in `~/.npmauditor.json` (global) and can be overridden per proj
|
|
|
187
263
|
1. **Parse** `package-lock.json` (supports v1, v2, v3 formats)
|
|
188
264
|
2. **Filter** packages: skip dev deps (`--no-dev`), skipped scopes/packages, packages without install scripts
|
|
189
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)
|
|
190
|
-
4. **
|
|
191
|
-
5. **
|
|
192
|
-
6. **
|
|
193
|
-
7. **
|
|
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
|
|
194
273
|
|
|
195
274
|
---
|
|
196
275
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
const {
|
|
5
|
-
const
|
|
6
|
-
const output
|
|
3
|
+
const { loadConfig, DEFAULT_CONFIG } = require('./utils/config');
|
|
4
|
+
const { checkForUpdate } = require('./utils/updateChecker');
|
|
5
|
+
const commands = require('./commands');
|
|
6
|
+
const output = require('./utils/output');
|
|
7
7
|
|
|
8
8
|
const VERSION = require('../package.json').version;
|
|
9
9
|
const NAME = require('../package.json').name;
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
function buildMainHelp() {
|
|
12
|
+
const cmdList = commands.list();
|
|
13
|
+
const lines = cmdList.map(cmd => {
|
|
14
|
+
const aliases = cmd.aliases.length ? ` (alias: ${cmd.aliases.join(', ')})` : '';
|
|
15
|
+
return ` npa ${cmd.name.padEnd(20)} ${cmd.description}${aliases}`;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return `
|
|
12
19
|
npa — npm package auditor ${VERSION}
|
|
13
20
|
Statically detects obfuscated code in npm install scripts.
|
|
14
21
|
|
|
15
22
|
Usage:
|
|
16
|
-
|
|
17
|
-
npa ci Audit then run npm ci
|
|
18
|
-
npa scan Scan only, no npm invocation (alias: s)
|
|
19
|
-
npa config get Show current configuration (alias: c)
|
|
20
|
-
npa config set <k> <v> Set a config value
|
|
23
|
+
${lines.join('\n')}
|
|
21
24
|
|
|
22
25
|
Flags:
|
|
23
|
-
--
|
|
26
|
+
--review, -r Interactive mode: choose which scripts to allow
|
|
24
27
|
--json Machine-readable JSON output
|
|
25
28
|
--no-dev Skip devDependencies
|
|
26
29
|
--verbose Show extra detail
|
|
27
30
|
--version Print version
|
|
28
|
-
--help, -h Print this help
|
|
31
|
+
--help, -h Print this help (use <command> -h for command help)
|
|
29
32
|
|
|
30
33
|
Config keys (stored in ~/.npmauditor.json):
|
|
31
34
|
blockScore Score threshold for hard block (default: ${DEFAULT_CONFIG.blockScore})
|
|
@@ -35,12 +38,14 @@ const HELP = `
|
|
|
35
38
|
parallelFetches Concurrent downloads (default: ${DEFAULT_CONFIG.parallelFetches})
|
|
36
39
|
skipScopes Array of @scopes to skip
|
|
37
40
|
skipPackages Array of package names to skip
|
|
41
|
+
maxTarballSize Max unpacked tarball size (default: ${DEFAULT_CONFIG.maxTarballSize})
|
|
38
42
|
`;
|
|
43
|
+
}
|
|
39
44
|
|
|
40
45
|
function parseArgs(argv) {
|
|
41
46
|
const args = argv.slice(2);
|
|
42
47
|
const flags = {
|
|
43
|
-
|
|
48
|
+
review: false,
|
|
44
49
|
json: false,
|
|
45
50
|
noDev: false,
|
|
46
51
|
verbose: false,
|
|
@@ -48,11 +53,12 @@ function parseArgs(argv) {
|
|
|
48
53
|
help: false,
|
|
49
54
|
};
|
|
50
55
|
const positionals = [];
|
|
56
|
+
const rawArgs = [];
|
|
51
57
|
|
|
52
58
|
for (const arg of args) {
|
|
53
59
|
switch (arg) {
|
|
54
|
-
case '--
|
|
55
|
-
case '-
|
|
60
|
+
case '--review':
|
|
61
|
+
case '-r': flags.review = true; break;
|
|
56
62
|
case '--json': flags.json = true; break;
|
|
57
63
|
case '--no-dev': flags.noDev = true; break;
|
|
58
64
|
case '--verbose': flags.verbose = true; break;
|
|
@@ -67,164 +73,16 @@ function parseArgs(argv) {
|
|
|
67
73
|
|
|
68
74
|
const command = positionals[0] || null;
|
|
69
75
|
const cmdArgs = positionals.slice(1);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
async function runInstall(pkgName, flags, config, cwd) {
|
|
74
|
-
output.printScanHeader();
|
|
75
|
-
|
|
76
|
-
const results = await scan({
|
|
77
|
-
cwd,
|
|
78
|
-
config,
|
|
79
|
-
noDev: flags.noDev,
|
|
80
|
-
verbose: flags.verbose,
|
|
81
|
-
singlePackage: pkgName || null,
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
if (flags.json) {
|
|
85
|
-
process.stdout.write(JSON.stringify(toJsonReport(results), null, 2) + '\n');
|
|
86
|
-
} else {
|
|
87
|
-
printResults(results);
|
|
76
|
+
const commandIndex = args.indexOf(command);
|
|
77
|
+
if (commandIndex !== -1) {
|
|
78
|
+
rawArgs.push(...args.slice(commandIndex + 1));
|
|
88
79
|
}
|
|
89
|
-
|
|
90
|
-
const blocked = results.filter(r => r.verdict === 'BLOCK');
|
|
91
|
-
|
|
92
|
-
if (blocked.length > 0 && !flags.aware) {
|
|
93
|
-
output.error(`${blocked.length} package(s) blocked due to obfuscated install scripts.`);
|
|
94
|
-
output.log(output.dim(' Run with --aware to interactively decide which scripts to allow.'));
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const npmArgs = pkgName ? [pkgName] : [];
|
|
99
|
-
|
|
100
|
-
if (flags.aware) {
|
|
101
|
-
const packagesWithScripts = results.filter(r => r.verdict !== 'OK' || r.scripts.length > 0);
|
|
102
|
-
const exit = await runAware({
|
|
103
|
-
results: packagesWithScripts.length > 0 ? packagesWithScripts : results,
|
|
104
|
-
command: 'install',
|
|
105
|
-
npmArgs,
|
|
106
|
-
cwd,
|
|
107
|
-
});
|
|
108
|
-
process.exit(exit);
|
|
109
|
-
} else {
|
|
110
|
-
const exit = runNpm('install', npmArgs, cwd);
|
|
111
|
-
process.exit(exit);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function runCi(flags, config, cwd) {
|
|
116
|
-
output.printScanHeader();
|
|
117
|
-
|
|
118
|
-
const results = await scan({ cwd, config, noDev: flags.noDev, verbose: flags.verbose });
|
|
119
|
-
|
|
120
|
-
if (flags.json) {
|
|
121
|
-
process.stdout.write(JSON.stringify(toJsonReport(results), null, 2) + '\n');
|
|
122
|
-
} else {
|
|
123
|
-
printResults(results);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const blocked = results.filter(r => r.verdict === 'BLOCK');
|
|
127
|
-
|
|
128
|
-
if (blocked.length > 0 && !flags.aware) {
|
|
129
|
-
output.error(`${blocked.length} package(s) blocked due to obfuscated install scripts.`);
|
|
130
|
-
process.exit(1);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (flags.aware) {
|
|
134
|
-
const exit = await runAware({ results, command: 'ci', npmArgs: [], cwd });
|
|
135
|
-
process.exit(exit);
|
|
136
|
-
} else {
|
|
137
|
-
const exit = runNpm('ci', [], cwd);
|
|
138
|
-
process.exit(exit);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function runScan(flags, config, cwd) {
|
|
143
|
-
output.printScanHeader();
|
|
144
|
-
|
|
145
|
-
const results = await scan({ cwd, config, noDev: flags.noDev, verbose: flags.verbose });
|
|
146
|
-
|
|
147
|
-
if (flags.json) {
|
|
148
|
-
process.stdout.write(JSON.stringify(toJsonReport(results), null, 2) + '\n');
|
|
149
|
-
const hasBlock = results.some(r => r.verdict === 'BLOCK');
|
|
150
|
-
process.exit(hasBlock ? 1 : 0);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
printResults(results);
|
|
154
|
-
output.printSummary(results.map(r => ({ verdict: r.verdict })));
|
|
155
|
-
|
|
156
|
-
const hasBlock = results.some(r => r.verdict === 'BLOCK');
|
|
157
|
-
process.exit(hasBlock ? 1 : 0);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function runConfig(cmdArgs, config) {
|
|
161
|
-
const subcommand = cmdArgs[0];
|
|
162
|
-
|
|
163
|
-
if (subcommand === 'get') {
|
|
164
|
-
const globalPath = getGlobalConfigPath();
|
|
165
|
-
output.log(output.bold(' Current npa configuration'));
|
|
166
|
-
output.log(output.dim(` (global: ${globalPath})`));
|
|
167
|
-
output.log('');
|
|
168
|
-
for (const [key, val] of Object.entries(config)) {
|
|
169
|
-
output.log(` ${output.cyan(key.padEnd(18))} ${JSON.stringify(val)}`);
|
|
170
|
-
}
|
|
171
|
-
output.log('');
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (subcommand === 'set') {
|
|
176
|
-
const key = cmdArgs[1];
|
|
177
|
-
const value = cmdArgs[2];
|
|
178
|
-
if (!key || value === undefined) {
|
|
179
|
-
output.error('Usage: npa config set <key> <value>');
|
|
180
|
-
process.exit(1);
|
|
181
|
-
}
|
|
182
|
-
try {
|
|
183
|
-
const updated = setGlobalConfig(key, value);
|
|
184
|
-
output.success(`Set ${key} = ${JSON.stringify(updated[key])}`);
|
|
185
|
-
output.log(output.dim(` Written to ${getGlobalConfigPath()}`));
|
|
186
|
-
} catch (err) {
|
|
187
|
-
output.error(err.message);
|
|
188
|
-
process.exit(1);
|
|
189
|
-
}
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
output.error(`Unknown config subcommand: "${subcommand}". Use "get" or "set".`);
|
|
194
|
-
process.exit(1);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function printResults(results) {
|
|
198
|
-
if (results.length === 0) {
|
|
199
|
-
output.success('No packages with install scripts found.');
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
for (const r of results) {
|
|
203
|
-
output.printPackageResult(r.pkg, r);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function toJsonReport(results) {
|
|
208
|
-
return {
|
|
209
|
-
summary: {
|
|
210
|
-
total: results.length,
|
|
211
|
-
blocked: results.filter(r => r.verdict === 'BLOCK').length,
|
|
212
|
-
warned: results.filter(r => r.verdict === 'WARN').length,
|
|
213
|
-
ok: results.filter(r => r.verdict === 'OK').length,
|
|
214
|
-
},
|
|
215
|
-
packages: results.map(r => ({
|
|
216
|
-
name: r.pkg.name,
|
|
217
|
-
version: r.pkg.version,
|
|
218
|
-
verdict: r.verdict,
|
|
219
|
-
score: r.score,
|
|
220
|
-
findings: r.findings,
|
|
221
|
-
scripts: r.scripts.map(s => ({ lifecycle: s.lifecycle, file: s.file, score: s.score })),
|
|
222
|
-
})),
|
|
223
|
-
};
|
|
80
|
+
return { command, args: cmdArgs, rawArgs, flags };
|
|
224
81
|
}
|
|
225
82
|
|
|
226
83
|
async function main() {
|
|
227
|
-
const
|
|
84
|
+
const parsed = parseArgs(process.argv);
|
|
85
|
+
const { command, args, rawArgs, flags } = parsed;
|
|
228
86
|
const cwd = process.cwd();
|
|
229
87
|
const config = loadConfig(cwd);
|
|
230
88
|
|
|
@@ -233,22 +91,38 @@ async function main() {
|
|
|
233
91
|
return;
|
|
234
92
|
}
|
|
235
93
|
|
|
94
|
+
if (flags.help && command) {
|
|
95
|
+
const cmd = commands.get(command);
|
|
96
|
+
if (cmd) {
|
|
97
|
+
process.stdout.write(cmd.help() + '\n');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
236
102
|
if (flags.help || !command) {
|
|
237
|
-
process.stdout.write(
|
|
103
|
+
process.stdout.write(buildMainHelp() + '\n');
|
|
238
104
|
return;
|
|
239
105
|
}
|
|
240
106
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
107
|
+
const cmd = commands.get(command);
|
|
108
|
+
if (!cmd) {
|
|
109
|
+
output.error(`Unknown command: "${command}". Run npa --help for usage.`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Fire update check only for scan/install/ci commands (non-blocking)
|
|
114
|
+
const UPDATE_COMMANDS = ['scan', 's', 'install', 'i', 'ci'];
|
|
115
|
+
const updatePromise = (UPDATE_COMMANDS.includes(command) && !flags.json && !config.silent)
|
|
116
|
+
? checkForUpdate(config, VERSION)
|
|
117
|
+
: Promise.resolve(null);
|
|
118
|
+
|
|
119
|
+
await cmd.run({ args, rawArgs, flags, config, cwd });
|
|
120
|
+
|
|
121
|
+
// Print update notice after command output
|
|
122
|
+
const latestVersion = await updatePromise;
|
|
123
|
+
if (latestVersion) {
|
|
124
|
+
output.log('');
|
|
125
|
+
output.log(output.dim(` Update available: ${VERSION} → ${latestVersion} — run "npm i -g np-audit" to update`));
|
|
252
126
|
}
|
|
253
127
|
}
|
|
254
128
|
|