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
|
@@ -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
|
+
}
|