np-audit 1.2.1 → 1.4.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 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,10 @@
1
+ ![np-audit](docs/title-image.png)
2
+
3
+ [![npm version](https://img.shields.io/npm/v/np-audit.svg)](https://www.npmjs.com/package/np-audit)
4
+ [![npm downloads](https://img.shields.io/npm/dm/np-audit.svg)](https://www.npmjs.com/package/np-audit)
5
+ [![GitHub license](https://img.shields.io/github/license/KoblerS/np-audit.svg)](https://github.com/KoblerS/np-audit/blob/main/LICENSE)
6
+ [![CI](https://github.com/KoblerS/np-audit/actions/workflows/ci.yml/badge.svg)](https://github.com/KoblerS/np-audit/actions/workflows/ci.yml)
7
+
1
8
  # np-audit — npm package auditor
2
9
 
3
10
  Statically detect obfuscated code in npm `preinstall`/`postinstall` scripts **before** they run. Drop-in replacement for `npm install` and `npm ci`.
@@ -53,13 +60,16 @@ Real-world examples include:
53
60
 
54
61
  ## Install
55
62
 
63
+ **Global install (recommended):**
64
+
56
65
  ```bash
57
- # Global install (recommended for daily use)
58
66
  npm install -g np-audit
67
+ ```
59
68
 
60
- # Or use directly with npx (no install needed)
69
+ **Or use directly with npx:**
70
+
71
+ ```bash
61
72
  npx np-audit scan
62
- npx np-audit install
63
73
  ```
64
74
 
65
75
  After global install, use the `npa` command:
@@ -76,11 +86,16 @@ npa --version
76
86
 
77
87
  | Command | Alias | Description |
78
88
  |---|---|---|
79
- | `npa install [package]` | `npa i [package]` | Audit then run `npm install` |
89
+ | `npa install [package]` | `npa i` | Audit then run `npm install` |
80
90
  | `npa ci` | — | Audit then run `npm ci` |
81
91
  | `npa scan` | `npa s` | Scan only, no install |
82
92
  | `npa config get` | `npa c get` | Show current configuration |
83
- | `npa config set <key> <value>` | `npa c set <key> <value>` | Update a config value |
93
+ | `npa config set <key> <value>` | `npa c set` | Update a config value |
94
+ | `npa alias` | — | Print shell hook for auto-scanning |
95
+ | `npa alias --install` | — | Install hook to shell profile |
96
+ | `npa alias --uninstall` | — | Remove hook from shell profile |
97
+
98
+ Use `npa <command> -h` for detailed help on any command.
84
99
 
85
100
  ### Flags
86
101
 
@@ -159,6 +174,42 @@ npa config set skipScopes '["@types","@babel"]'
159
174
 
160
175
  Config is stored in `~/.npmauditor.json` (global) and can be overridden per project with `.npmauditor.json` in your project root.
161
176
 
177
+ ### Shell Hook (npm alias)
178
+
179
+ Automatically run `npa scan` before every `npm install` or `npm ci`:
180
+
181
+ ```bash
182
+ # Install the hook to your shell profile (~/.zshrc or ~/.bashrc)
183
+ npa alias --install
184
+
185
+ # Reload your shell
186
+ source ~/.zshrc # or ~/.bashrc
187
+ ```
188
+
189
+ Now when you run `npm install` or `npm ci`, npa will scan first:
190
+
191
+ ```bash
192
+ $ npm install lodash
193
+ [npa] Scanning dependencies before npm install...
194
+ ✔ No packages with install scripts found.
195
+ [npa] Scan passed. Running npm install...
196
+ ```
197
+
198
+ If issues are found, the install is blocked:
199
+
200
+ ```bash
201
+ $ npm install evil-pkg
202
+ [npa] Scanning dependencies before npm install...
203
+ ✗ evil-pkg@1.0.0 BLOCK (score: 9)
204
+ [npa] Scan found issues. Run 'npa install --aware' for interactive mode.
205
+ ```
206
+
207
+ To remove the hook:
208
+
209
+ ```bash
210
+ npa alias --uninstall
211
+ ```
212
+
162
213
  #### All config keys
163
214
 
164
215
  | Key | Default | Description |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "np-audit",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "Static obfuscation detector for npm lifecycle scripts — supply chain attack prevention",
5
5
  "bin": {
6
6
  "npa": "bin/npa.js",
package/src/cli.js CHANGED
@@ -1,31 +1,33 @@
1
1
  'use strict';
2
2
 
3
- const { scan } = require('./scanner');
4
- const { runAware, runNpm } = require('./aware');
5
- const { loadConfig, setGlobalConfig, getGlobalConfigPath, DEFAULT_CONFIG } = require('./config');
6
- const output = require('./output');
3
+ const { loadConfig, DEFAULT_CONFIG } = require('./utils/config');
4
+ const commands = require('./commands');
5
+ const output = require('./utils/output');
7
6
 
8
7
  const VERSION = require('../package.json').version;
9
8
  const NAME = require('../package.json').name;
10
9
 
11
- const HELP = `
10
+ function buildMainHelp() {
11
+ const cmdList = commands.list();
12
+ const lines = cmdList.map(cmd => {
13
+ const aliases = cmd.aliases.length ? ` (alias: ${cmd.aliases.join(', ')})` : '';
14
+ return ` npa ${cmd.name.padEnd(20)} ${cmd.description}${aliases}`;
15
+ });
16
+
17
+ return `
12
18
  npa — npm package auditor ${VERSION}
13
19
  Statically detects obfuscated code in npm install scripts.
14
20
 
15
21
  Usage:
16
- npa install [package] Audit then run npm install (alias: i)
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
22
+ ${lines.join('\n')}
21
23
 
22
24
  Flags:
23
- --aware, -a Interactive mode: choose which scripts to allow
25
+ --review, -r Interactive mode: choose which scripts to allow
24
26
  --json Machine-readable JSON output
25
27
  --no-dev Skip devDependencies
26
28
  --verbose Show extra detail
27
29
  --version Print version
28
- --help, -h Print this help
30
+ --help, -h Print this help (use <command> -h for command help)
29
31
 
30
32
  Config keys (stored in ~/.npmauditor.json):
31
33
  blockScore Score threshold for hard block (default: ${DEFAULT_CONFIG.blockScore})
@@ -36,11 +38,12 @@ const HELP = `
36
38
  skipScopes Array of @scopes to skip
37
39
  skipPackages Array of package names to skip
38
40
  `;
41
+ }
39
42
 
40
43
  function parseArgs(argv) {
41
44
  const args = argv.slice(2);
42
45
  const flags = {
43
- aware: false,
46
+ review: false,
44
47
  json: false,
45
48
  noDev: false,
46
49
  verbose: false,
@@ -48,11 +51,12 @@ function parseArgs(argv) {
48
51
  help: false,
49
52
  };
50
53
  const positionals = [];
54
+ const rawArgs = [];
51
55
 
52
56
  for (const arg of args) {
53
57
  switch (arg) {
54
- case '--aware':
55
- case '-a': flags.aware = true; break;
58
+ case '--review':
59
+ case '-r': flags.review = true; break;
56
60
  case '--json': flags.json = true; break;
57
61
  case '--no-dev': flags.noDev = true; break;
58
62
  case '--verbose': flags.verbose = true; break;
@@ -67,164 +71,16 @@ function parseArgs(argv) {
67
71
 
68
72
  const command = positionals[0] || null;
69
73
  const cmdArgs = positionals.slice(1);
70
- return { command, cmdArgs, flags };
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);
88
- }
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);
74
+ const commandIndex = args.indexOf(command);
75
+ if (commandIndex !== -1) {
76
+ rawArgs.push(...args.slice(commandIndex + 1));
112
77
  }
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
- };
78
+ return { command, args: cmdArgs, rawArgs, flags };
224
79
  }
225
80
 
226
81
  async function main() {
227
- const { command, cmdArgs, flags } = parseArgs(process.argv);
82
+ const parsed = parseArgs(process.argv);
83
+ const { command, args, rawArgs, flags } = parsed;
228
84
  const cwd = process.cwd();
229
85
  const config = loadConfig(cwd);
230
86
 
@@ -233,23 +89,26 @@ async function main() {
233
89
  return;
234
90
  }
235
91
 
92
+ if (flags.help && command) {
93
+ const cmd = commands.get(command);
94
+ if (cmd) {
95
+ process.stdout.write(cmd.help() + '\n');
96
+ return;
97
+ }
98
+ }
99
+
236
100
  if (flags.help || !command) {
237
- process.stdout.write(HELP + '\n');
101
+ process.stdout.write(buildMainHelp() + '\n');
238
102
  return;
239
103
  }
240
104
 
241
- switch (command) {
242
- case 'install':
243
- case 'i': await runInstall(cmdArgs[0] || null, flags, config, cwd); break;
244
- case 'ci': await runCi(flags, config, cwd); break;
245
- case 'scan':
246
- case 's': await runScan(flags, config, cwd); break;
247
- case 'config':
248
- case 'c': await runConfig(cmdArgs, config); break;
249
- default:
250
- output.error(`Unknown command: "${command}". Run npa --help for usage.`);
251
- process.exit(1);
105
+ const cmd = commands.get(command);
106
+ if (!cmd) {
107
+ output.error(`Unknown command: "${command}". Run npa --help for usage.`);
108
+ process.exit(1);
252
109
  }
110
+
111
+ await cmd.run({ args, rawArgs, flags, config, cwd });
253
112
  }
254
113
 
255
114
  main().catch(err => {
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const output = require('../utils/output');
7
+
8
+ const BASH_HOOK = `# npa npm hook
9
+ npm() { [[ -n "$NPA_RUNNING" ]] && { command npm "$@"; return; }; case "$1" in scan) npa scan "\${@:2}"; return;; install|i|add) command -v npa >/dev/null && { local pkgs=(); for a in "\${@:2}"; do [[ "$a" != -* ]] && pkgs+=("$a"); done; if [[ \${#pkgs[@]} -gt 0 ]]; then npa scan "\${pkgs[@]}" || { echo "[npa] Blocked. Use 'npa install --review'"; return 1; }; else npa scan || { echo "[npa] Blocked. Use 'npa install --review'"; return 1; }; fi; };; ci) command -v npa >/dev/null && { npa scan || { echo "[npa] Blocked. Use 'npa ci --review'"; return 1; }; };; esac; command npm "$@"; }`;
10
+
11
+ const POWERSHELL_HOOK = `# npa npm hook
12
+ function npm { if($env:NPA_RUNNING){& npm.cmd @args;return}; if($args[0] -eq 'scan'){& npa scan @($args|Select-Object -Skip 1);return}; if($args[0] -in @('install','i','add')){$pkgs=@($args|Where-Object{$_ -notmatch '^-'}|Select-Object -Skip 1); if($pkgs.Count -gt 0){& npa scan @pkgs; if($LASTEXITCODE -ne 0){Write-Host "[npa] Blocked.";return 1}}else{& npa scan; if($LASTEXITCODE -ne 0){Write-Host "[npa] Blocked.";return 1}}}; if($args[0] -eq 'ci'){& npa scan; if($LASTEXITCODE -ne 0){Write-Host "[npa] Blocked.";return 1}}; & npm.cmd @args }`;
13
+
14
+ module.exports = {
15
+ name: 'alias',
16
+ aliases: [],
17
+ description: 'Shell hook to auto-scan before npm install/ci',
18
+
19
+ help() {
20
+ return `
21
+ npa alias — Shell hook to auto-scan before npm install/ci
22
+
23
+ Usage:
24
+ npa alias Print the shell hook
25
+ npa alias --install Add hook to shell profile (~/.zshrc or ~/.bashrc)
26
+ npa alias --uninstall Remove hook from shell profile
27
+
28
+ The hook intercepts npm install/ci/add commands and runs npa scan first.
29
+ If issues are found, the install is blocked until resolved.
30
+
31
+ Examples:
32
+ npa alias Print hook for manual installation
33
+ npa alias --install Auto-install to detected shell
34
+ eval "$(npa alias)" Load hook in current session only
35
+ `;
36
+ },
37
+
38
+ run({ rawArgs }) {
39
+ const install = rawArgs.includes('--install') || rawArgs.includes('-i');
40
+ const uninstall = rawArgs.includes('--uninstall') || rawArgs.includes('-u');
41
+
42
+ const { shell, hook, profilePath } = detectShell();
43
+
44
+ if (uninstall) {
45
+ return doUninstall(profilePath);
46
+ }
47
+
48
+ if (install) {
49
+ return doInstall(hook, profilePath);
50
+ }
51
+
52
+ // Print hook
53
+ process.stdout.write(hook + '\n');
54
+ output.log('');
55
+ output.log(output.dim(` Add to your shell profile, or run: npa alias --install`));
56
+ },
57
+ };
58
+
59
+ function detectShell() {
60
+ if (process.platform === 'win32') {
61
+ return { shell: 'powershell', hook: POWERSHELL_HOOK, profilePath: null };
62
+ }
63
+
64
+ const userShell = process.env.SHELL || '';
65
+ if (userShell.includes('zsh')) {
66
+ return { shell: 'zsh', hook: BASH_HOOK, profilePath: path.join(os.homedir(), '.zshrc') };
67
+ }
68
+
69
+ return { shell: 'bash', hook: BASH_HOOK, profilePath: path.join(os.homedir(), '.bashrc') };
70
+ }
71
+
72
+ function doUninstall(profilePath) {
73
+ if (!profilePath) {
74
+ output.error('Auto-uninstall not supported for PowerShell. Remove manually from $PROFILE');
75
+ return;
76
+ }
77
+
78
+ if (!fs.existsSync(profilePath)) {
79
+ output.warn('No profile found at ' + profilePath);
80
+ return;
81
+ }
82
+
83
+ const content = fs.readFileSync(profilePath, 'utf8');
84
+ if (!content.includes('# npa npm hook')) {
85
+ output.warn('npa hook not found in ' + profilePath);
86
+ return;
87
+ }
88
+
89
+ const cleaned = content.replace(/\n*# npa npm hook\nnpm\(\)[^\n]+\n*/g, '\n');
90
+ fs.writeFileSync(profilePath, cleaned);
91
+ output.success(`Removed npa hook from ${profilePath}`);
92
+ output.log(output.dim(' Run: source ' + profilePath + ' (or restart your terminal)'));
93
+ }
94
+
95
+ function doInstall(hook, profilePath) {
96
+ if (!profilePath) {
97
+ output.error('Auto-install not supported for PowerShell. Copy the output manually to $PROFILE');
98
+ process.stdout.write('\n' + hook + '\n');
99
+ return;
100
+ }
101
+
102
+ const content = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
103
+ if (content.includes('# npa npm hook')) {
104
+ output.warn('npa hook already installed in ' + profilePath);
105
+ return;
106
+ }
107
+
108
+ fs.appendFileSync(profilePath, '\n\n' + hook + '\n');
109
+ output.success(`Installed npa hook to ${profilePath}`);
110
+ output.log(output.dim(' Run: source ' + profilePath + ' (or restart your terminal)'));
111
+ }
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ const { scan } = require('../core/scanner');
4
+ const { runAware, runNpm } = require('../utils/review');
5
+ const output = require('../utils/output');
6
+
7
+ module.exports = {
8
+ name: 'ci',
9
+ aliases: [],
10
+ description: 'Audit then run npm ci',
11
+
12
+ help() {
13
+ return `
14
+ npa ci — Audit dependencies then run npm ci
15
+
16
+ Usage:
17
+ npa ci [options]
18
+
19
+ Options:
20
+ --review, -r Interactive mode: review and allow/deny scripts
21
+ --json Output scan results as JSON
22
+ --no-dev Skip devDependencies in scan
23
+ --verbose Show detailed findings
24
+ -h, --help Show this help
25
+
26
+ Examples:
27
+ npa ci Clean install after audit
28
+ npa ci --review Review scripts interactively
29
+ `;
30
+ },
31
+
32
+ async run({ flags, config, cwd }) {
33
+ const results = await scan({ cwd, config, noDev: flags.noDev, verbose: flags.verbose });
34
+ const hasIssues = results.some(r => r.verdict !== 'OK');
35
+ const silent = config.silent && !hasIssues;
36
+
37
+ output.printScanHeader(silent);
38
+
39
+ if (flags.json) {
40
+ process.stdout.write(JSON.stringify(toJsonReport(results), null, 2) + '\n');
41
+ } else {
42
+ printResults(results, silent);
43
+ }
44
+
45
+ const blocked = results.filter(r => r.verdict === 'BLOCK');
46
+
47
+ if (blocked.length > 0 && !flags.review) {
48
+ output.error(`${blocked.length} package(s) blocked due to obfuscated install scripts.`);
49
+ process.exit(1);
50
+ }
51
+
52
+ if (flags.review) {
53
+ const exit = await runAware({ results, command: 'ci', npmArgs: [], cwd });
54
+ process.exit(exit);
55
+ } else {
56
+ const exit = runNpm('ci', [], cwd);
57
+ process.exit(exit);
58
+ }
59
+ },
60
+ };
61
+
62
+ function printResults(results, silent = false) {
63
+ if (silent) return;
64
+ if (results.length === 0) {
65
+ output.success('No packages with install scripts found.');
66
+ return;
67
+ }
68
+ for (const r of results) {
69
+ output.printPackageResult(r.pkg, r);
70
+ }
71
+ }
72
+
73
+ function toJsonReport(results) {
74
+ return {
75
+ summary: {
76
+ total: results.length,
77
+ blocked: results.filter(r => r.verdict === 'BLOCK').length,
78
+ warned: results.filter(r => r.verdict === 'WARN').length,
79
+ ok: results.filter(r => r.verdict === 'OK').length,
80
+ },
81
+ packages: results.map(r => ({
82
+ name: r.pkg.name,
83
+ version: r.pkg.version,
84
+ verdict: r.verdict,
85
+ score: r.score,
86
+ findings: r.findings,
87
+ scripts: r.scripts.map(s => ({ lifecycle: s.lifecycle, file: s.file, score: s.score })),
88
+ })),
89
+ };
90
+ }
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ const { setGlobalConfig, getGlobalConfigPath, DEFAULT_CONFIG } = require('../utils/config');
4
+ const output = require('../utils/output');
5
+
6
+ module.exports = {
7
+ name: 'config',
8
+ aliases: ['c'],
9
+ description: 'View or modify npa configuration',
10
+
11
+ help() {
12
+ return `
13
+ npa config — View or modify npa configuration
14
+
15
+ Usage:
16
+ npa config get Show all config values
17
+ npa config set <key> <val> Set a config value
18
+
19
+ Config keys:
20
+ blockScore Score threshold for hard block (default: ${DEFAULT_CONFIG.blockScore})
21
+ warnScore Score threshold for warning (default: ${DEFAULT_CONFIG.warnScore})
22
+ registry npm registry URL
23
+ timeout HTTP timeout in ms (default: ${DEFAULT_CONFIG.timeout})
24
+ parallelFetches Concurrent downloads (default: ${DEFAULT_CONFIG.parallelFetches})
25
+ skipScopes Array of @scopes to skip (JSON)
26
+ skipPackages Array of package names to skip (JSON)
27
+
28
+ Examples:
29
+ npa config get
30
+ npa config set blockScore 10
31
+ npa config set skipScopes '["@myorg"]'
32
+ `;
33
+ },
34
+
35
+ async run({ args, config }) {
36
+ const subcommand = args[0];
37
+
38
+ if (subcommand === 'get') {
39
+ const globalPath = getGlobalConfigPath();
40
+ output.log(output.bold(' Current npa configuration'));
41
+ output.log(output.dim(` (global: ${globalPath})`));
42
+ output.log('');
43
+ for (const [key, val] of Object.entries(config)) {
44
+ output.log(` ${output.cyan(key.padEnd(18))} ${JSON.stringify(val)}`);
45
+ }
46
+ output.log('');
47
+ return;
48
+ }
49
+
50
+ if (subcommand === 'set') {
51
+ const key = args[1];
52
+ const value = args[2];
53
+ if (!key || value === undefined) {
54
+ output.error('Usage: npa config set <key> <value>');
55
+ process.exit(1);
56
+ }
57
+ try {
58
+ const updated = setGlobalConfig(key, value);
59
+ output.success(`Set ${key} = ${JSON.stringify(updated[key])}`);
60
+ output.log(output.dim(` Written to ${getGlobalConfigPath()}`));
61
+ } catch (err) {
62
+ output.error(err.message);
63
+ process.exit(1);
64
+ }
65
+ return;
66
+ }
67
+
68
+ output.error(`Unknown config subcommand: "${subcommand}". Use "get" or "set".`);
69
+ process.exit(1);
70
+ },
71
+ };