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.
@@ -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
+ };
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const commands = new Map();
7
+
8
+ // Load all command modules from this directory
9
+ const files = fs.readdirSync(__dirname).filter(f => f !== 'index.js' && f.endsWith('.js'));
10
+
11
+ for (const file of files) {
12
+ const cmd = require(path.join(__dirname, file));
13
+ commands.set(cmd.name, cmd);
14
+ for (const alias of cmd.aliases || []) {
15
+ commands.set(alias, cmd);
16
+ }
17
+ }
18
+
19
+ module.exports = {
20
+ commands,
21
+
22
+ get(name) {
23
+ return commands.get(name);
24
+ },
25
+
26
+ list() {
27
+ const seen = new Set();
28
+ const result = [];
29
+ for (const cmd of commands.values()) {
30
+ if (!seen.has(cmd.name)) {
31
+ seen.add(cmd.name);
32
+ result.push(cmd);
33
+ }
34
+ }
35
+ return result;
36
+ },
37
+ };
@@ -0,0 +1,109 @@
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: 'install',
9
+ aliases: ['i'],
10
+ description: 'Audit then run npm install',
11
+
12
+ help() {
13
+ return `
14
+ npa install — Audit dependencies then run npm install
15
+
16
+ Usage:
17
+ npa install [package] [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 install Install all deps after audit
28
+ npa install lodash Add lodash after auditing it
29
+ npa install --review Review scripts interactively
30
+ `;
31
+ },
32
+
33
+ async run({ args, flags, config, cwd }) {
34
+ const packages = args.filter(a => !a.startsWith('-'));
35
+
36
+ const results = await scan({
37
+ cwd,
38
+ config,
39
+ noDev: flags.noDev,
40
+ verbose: flags.verbose,
41
+ packages: packages.length > 0 ? packages : null,
42
+ });
43
+
44
+ const hasIssues = results.some(r => r.verdict !== 'OK');
45
+ const silent = config.silent && !hasIssues;
46
+
47
+ output.printScanHeader(silent);
48
+
49
+ if (flags.json) {
50
+ process.stdout.write(JSON.stringify(toJsonReport(results), null, 2) + '\n');
51
+ } else {
52
+ printResults(results, silent);
53
+ }
54
+
55
+ const blocked = results.filter(r => r.verdict === 'BLOCK');
56
+
57
+ if (blocked.length > 0 && !flags.review) {
58
+ output.error(`${blocked.length} package(s) blocked due to obfuscated install scripts.`);
59
+ output.log(output.dim(' Run with --review to interactively decide which scripts to allow.'));
60
+ process.exit(1);
61
+ }
62
+
63
+ const npmArgs = packages.length > 0 ? packages : [];
64
+
65
+ if (flags.review) {
66
+ const packagesWithScripts = results.filter(r => r.verdict !== 'OK' || r.scripts.length > 0);
67
+ const exit = await runAware({
68
+ results: packagesWithScripts.length > 0 ? packagesWithScripts : results,
69
+ command: 'install',
70
+ npmArgs,
71
+ cwd,
72
+ });
73
+ process.exit(exit);
74
+ } else {
75
+ const exit = runNpm('install', npmArgs, cwd);
76
+ process.exit(exit);
77
+ }
78
+ },
79
+ };
80
+
81
+ function printResults(results, silent = false) {
82
+ if (silent) return;
83
+ if (results.length === 0) {
84
+ output.success('No packages with install scripts found.');
85
+ return;
86
+ }
87
+ for (const r of results) {
88
+ output.printPackageResult(r.pkg, r);
89
+ }
90
+ }
91
+
92
+ function toJsonReport(results) {
93
+ return {
94
+ summary: {
95
+ total: results.length,
96
+ blocked: results.filter(r => r.verdict === 'BLOCK').length,
97
+ warned: results.filter(r => r.verdict === 'WARN').length,
98
+ ok: results.filter(r => r.verdict === 'OK').length,
99
+ },
100
+ packages: results.map(r => ({
101
+ name: r.pkg.name,
102
+ version: r.pkg.version,
103
+ verdict: r.verdict,
104
+ score: r.score,
105
+ findings: r.findings,
106
+ scripts: r.scripts.map(s => ({ lifecycle: s.lifecycle, file: s.file, score: s.score })),
107
+ })),
108
+ };
109
+ }
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const { scan } = require('../core/scanner');
4
+ const output = require('../utils/output');
5
+
6
+ module.exports = {
7
+ name: 'scan',
8
+ aliases: ['s'],
9
+ description: 'Scan only, no npm invocation',
10
+
11
+ help() {
12
+ return `
13
+ npa scan — Scan dependencies for obfuscated install scripts
14
+
15
+ Usage:
16
+ npa scan [package] [options]
17
+
18
+ Options:
19
+ --json Output results as JSON
20
+ --no-dev Skip devDependencies
21
+ --verbose Show detailed findings
22
+ -h, --help Show this help
23
+
24
+ Examples:
25
+ npa scan Scan all dependencies
26
+ npa scan lodash Scan a specific package before installing
27
+ npa scan --no-dev Scan production dependencies only
28
+ npa scan --json Output machine-readable JSON
29
+ `;
30
+ },
31
+
32
+ async run({ args, flags, config, cwd }) {
33
+ const packages = args.filter(a => !a.startsWith('-'));
34
+ const results = await scan({ cwd, config, noDev: flags.noDev, verbose: flags.verbose, packages: packages.length > 0 ? packages : null });
35
+ const hasIssues = results.some(r => r.verdict !== 'OK');
36
+ const silent = config.silent && !hasIssues;
37
+
38
+ output.printScanHeader(silent);
39
+
40
+ if (flags.json) {
41
+ process.stdout.write(JSON.stringify(toJsonReport(results), null, 2) + '\n');
42
+ const hasBlock = results.some(r => r.verdict === 'BLOCK');
43
+ process.exit(hasBlock ? 1 : 0);
44
+ }
45
+
46
+ printResults(results, silent);
47
+ if (!silent) output.printSummary(results);
48
+
49
+ const hasBlock = results.some(r => r.verdict === 'BLOCK');
50
+ process.exit(hasBlock ? 1 : 0);
51
+ },
52
+ };
53
+
54
+ function printResults(results, silent = false) {
55
+ if (silent) return;
56
+ if (results.length === 0) {
57
+ output.success('No packages with install scripts found.');
58
+ return;
59
+ }
60
+ for (const r of results) {
61
+ output.printPackageResult(r.pkg, r);
62
+ }
63
+ }
64
+
65
+ function toJsonReport(results) {
66
+ return {
67
+ summary: {
68
+ total: results.length,
69
+ blocked: results.filter(r => r.verdict === 'BLOCK').length,
70
+ warned: results.filter(r => r.verdict === 'WARN').length,
71
+ ok: results.filter(r => r.verdict === 'OK').length,
72
+ },
73
+ packages: results.map(r => ({
74
+ name: r.pkg.name,
75
+ version: r.pkg.version,
76
+ verdict: r.verdict,
77
+ score: r.score,
78
+ findings: r.findings,
79
+ scripts: r.scripts.map(s => ({ lifecycle: s.lifecycle, file: s.file, score: s.score })),
80
+ })),
81
+ };
82
+ }