ship-safe 6.1.1 → 6.2.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 +735 -641
- package/cli/agents/api-fuzzer.js +345 -345
- package/cli/agents/auth-bypass-agent.js +348 -348
- package/cli/agents/base-agent.js +272 -272
- package/cli/agents/cicd-scanner.js +236 -201
- package/cli/agents/config-auditor.js +521 -521
- package/cli/agents/deep-analyzer.js +6 -2
- package/cli/agents/git-history-scanner.js +170 -170
- package/cli/agents/html-reporter.js +568 -568
- package/cli/agents/index.js +84 -84
- package/cli/agents/injection-tester.js +500 -500
- package/cli/agents/llm-redteam.js +251 -251
- package/cli/agents/mobile-scanner.js +231 -231
- package/cli/agents/orchestrator.js +322 -322
- package/cli/agents/pii-compliance-agent.js +301 -301
- package/cli/agents/scoring-engine.js +248 -248
- package/cli/agents/supabase-rls-agent.js +154 -154
- package/cli/agents/supply-chain-agent.js +650 -507
- package/cli/bin/ship-safe.js +452 -426
- package/cli/commands/agent.js +608 -608
- package/cli/commands/audit.js +986 -980
- package/cli/commands/baseline.js +193 -193
- package/cli/commands/ci.js +342 -342
- package/cli/commands/deps.js +516 -516
- package/cli/commands/doctor.js +159 -159
- package/cli/commands/fix.js +218 -218
- package/cli/commands/hooks.js +268 -0
- package/cli/commands/init.js +407 -407
- package/cli/commands/mcp.js +304 -304
- package/cli/commands/red-team.js +7 -1
- package/cli/commands/remediate.js +798 -798
- package/cli/commands/rotate.js +571 -571
- package/cli/commands/scan.js +569 -569
- package/cli/commands/score.js +449 -449
- package/cli/commands/watch.js +281 -281
- package/cli/hooks/patterns.js +313 -0
- package/cli/hooks/post-tool-use.js +140 -0
- package/cli/hooks/pre-tool-use.js +186 -0
- package/cli/index.js +73 -69
- package/cli/providers/llm-provider.js +397 -287
- package/cli/utils/autofix-rules.js +74 -74
- package/cli/utils/cache-manager.js +311 -311
- package/cli/utils/output.js +230 -230
- package/cli/utils/patterns.js +1121 -1121
- package/cli/utils/pdf-generator.js +94 -94
- package/package.json +69 -69
- package/configs/supabase/rls-templates.sql +0 -242
package/cli/commands/deps.js
CHANGED
|
@@ -1,516 +1,516 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deps Command
|
|
3
|
-
* ============
|
|
4
|
-
*
|
|
5
|
-
* Audit project dependencies for known CVEs using the package manager's
|
|
6
|
-
* built-in audit tool (npm, yarn, pnpm, pip-audit, bundler-audit).
|
|
7
|
-
*
|
|
8
|
-
* USAGE:
|
|
9
|
-
* npx ship-safe deps [path] Audit dependencies in the project
|
|
10
|
-
* npx ship-safe deps . --fix Also run the package manager fix command
|
|
11
|
-
*
|
|
12
|
-
* SUPPORTED PACKAGE MANAGERS:
|
|
13
|
-
* npm → npm audit --json
|
|
14
|
-
* yarn → yarn audit --json (NDJSON format)
|
|
15
|
-
* pnpm → pnpm audit --json
|
|
16
|
-
* pip → pip-audit --format json (requires: pip install pip-audit)
|
|
17
|
-
* bundler → bundle-audit check (requires: gem install bundler-audit)
|
|
18
|
-
*
|
|
19
|
-
* EXIT CODES:
|
|
20
|
-
* 0 - No vulnerabilities found (or tool not available)
|
|
21
|
-
* 1 - Vulnerabilities found
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import fs from 'fs';
|
|
25
|
-
import path from 'path';
|
|
26
|
-
import { execSync } from 'child_process';
|
|
27
|
-
import chalk from 'chalk';
|
|
28
|
-
import ora from 'ora';
|
|
29
|
-
import * as output from '../utils/output.js';
|
|
30
|
-
|
|
31
|
-
// =============================================================================
|
|
32
|
-
// MAIN COMMAND
|
|
33
|
-
// =============================================================================
|
|
34
|
-
|
|
35
|
-
export async function depsCommand(targetPath = '.', options = {}) {
|
|
36
|
-
const absolutePath = path.resolve(targetPath);
|
|
37
|
-
|
|
38
|
-
if (!fs.existsSync(absolutePath)) {
|
|
39
|
-
output.error(`Path does not exist: ${absolutePath}`);
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
console.log();
|
|
44
|
-
output.header('Dependency Audit');
|
|
45
|
-
console.log();
|
|
46
|
-
|
|
47
|
-
// ── 1. Detect package manager ─────────────────────────────────────────────
|
|
48
|
-
const pm = detectPackageManager(absolutePath);
|
|
49
|
-
|
|
50
|
-
if (!pm) {
|
|
51
|
-
console.log(chalk.gray(' No supported package manifest found.'));
|
|
52
|
-
console.log(chalk.gray(' Supported: package.json (npm/yarn/pnpm), requirements.txt (pip), Gemfile (bundler)'));
|
|
53
|
-
console.log();
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
console.log(chalk.gray(` Package manager: ${chalk.white(pm.name)} Manifest: ${chalk.white(pm.manifest)}`));
|
|
58
|
-
console.log();
|
|
59
|
-
|
|
60
|
-
// ── 2. Run audit ──────────────────────────────────────────────────────────
|
|
61
|
-
const spinner = ora({ text: `Running ${pm.name} audit...`, color: 'cyan' }).start();
|
|
62
|
-
let vulns = [];
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
vulns = runAudit(pm, absolutePath);
|
|
66
|
-
spinner.stop();
|
|
67
|
-
} catch (err) {
|
|
68
|
-
spinner.stop();
|
|
69
|
-
if (err.code === 'ENOENT' || /not found|not recognized|command not found/i.test(err.message)) {
|
|
70
|
-
output.warning(`${pm.name} is not installed or not in PATH.`);
|
|
71
|
-
if (pm.installHint) {
|
|
72
|
-
console.log(chalk.gray(` Install it with: `) + chalk.cyan(pm.installHint));
|
|
73
|
-
}
|
|
74
|
-
console.log();
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
output.error(`Audit failed: ${err.message}`);
|
|
78
|
-
console.log(chalk.gray(` Try running manually: `) + chalk.cyan(pm.auditCommand));
|
|
79
|
-
console.log();
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ── 3. No vulnerabilities ─────────────────────────────────────────────────
|
|
84
|
-
if (vulns.length === 0) {
|
|
85
|
-
output.success(`No known vulnerabilities in your ${pm.name} dependencies!`);
|
|
86
|
-
console.log();
|
|
87
|
-
process.exit(0);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ── 4. Enrich with EPSS scores ──────────────────────────────────────────
|
|
91
|
-
const cves = vulns.map(v => v.cve).filter(Boolean);
|
|
92
|
-
if (cves.length > 0) {
|
|
93
|
-
const epssSpinner = ora({ text: 'Fetching EPSS exploit probability scores...', color: 'cyan' }).start();
|
|
94
|
-
try {
|
|
95
|
-
const epssData = await fetchEPSS(cves);
|
|
96
|
-
for (const v of vulns) {
|
|
97
|
-
if (v.cve && epssData[v.cve]) {
|
|
98
|
-
v.epss = epssData[v.cve].epss;
|
|
99
|
-
v.percentile = epssData[v.cve].percentile;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
epssSpinner.succeed(chalk.gray(`EPSS scores fetched for ${Object.keys(epssData).length} CVE(s)`));
|
|
103
|
-
} catch {
|
|
104
|
-
epssSpinner.stop();
|
|
105
|
-
// EPSS is optional — continue without it
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── 5. Display findings ───────────────────────────────────────────────────
|
|
110
|
-
printDepFindings(vulns, pm);
|
|
111
|
-
|
|
112
|
-
// ── 6. Optionally fix ─────────────────────────────────────────────────────
|
|
113
|
-
if (options.fix) {
|
|
114
|
-
console.log();
|
|
115
|
-
console.log(chalk.cyan(` Running: ${pm.fixCommand}`));
|
|
116
|
-
try {
|
|
117
|
-
execSync(pm.fixCommand, { cwd: absolutePath, stdio: 'inherit' }); // ship-safe-ignore — command is a hardcoded package manager command, not user input
|
|
118
|
-
} catch {
|
|
119
|
-
output.warning('Fix command exited with errors — some vulnerabilities may require manual updates.');
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
process.exit(1);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// =============================================================================
|
|
127
|
-
// PACKAGE MANAGER DETECTION
|
|
128
|
-
// =============================================================================
|
|
129
|
-
|
|
130
|
-
function detectPackageManager(rootPath) {
|
|
131
|
-
// Node.js — detect specific lock file first for accuracy
|
|
132
|
-
if (fs.existsSync(path.join(rootPath, 'pnpm-lock.yaml'))) {
|
|
133
|
-
return {
|
|
134
|
-
name: 'pnpm',
|
|
135
|
-
manifest: 'package.json',
|
|
136
|
-
auditCommand: 'pnpm audit --json',
|
|
137
|
-
fixCommand: 'pnpm audit --fix',
|
|
138
|
-
type: 'npm-v2'
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (fs.existsSync(path.join(rootPath, 'yarn.lock'))) {
|
|
143
|
-
return {
|
|
144
|
-
name: 'yarn',
|
|
145
|
-
manifest: 'package.json',
|
|
146
|
-
auditCommand: 'yarn audit --json',
|
|
147
|
-
fixCommand: 'yarn upgrade',
|
|
148
|
-
type: 'yarn'
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (fs.existsSync(path.join(rootPath, 'package.json'))) {
|
|
153
|
-
return {
|
|
154
|
-
name: 'npm',
|
|
155
|
-
manifest: 'package.json',
|
|
156
|
-
auditCommand: 'npm audit --json',
|
|
157
|
-
fixCommand: 'npm audit fix',
|
|
158
|
-
type: 'npm-v2'
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Python
|
|
163
|
-
if (fs.existsSync(path.join(rootPath, 'requirements.txt'))) {
|
|
164
|
-
return {
|
|
165
|
-
name: 'pip-audit',
|
|
166
|
-
manifest: 'requirements.txt',
|
|
167
|
-
auditCommand: 'pip-audit --format json -r requirements.txt',
|
|
168
|
-
fixCommand: 'pip-audit --fix -r requirements.txt',
|
|
169
|
-
type: 'pip',
|
|
170
|
-
installHint: 'pip install pip-audit'
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Ruby
|
|
175
|
-
if (fs.existsSync(path.join(rootPath, 'Gemfile.lock'))) {
|
|
176
|
-
return {
|
|
177
|
-
name: 'bundler-audit',
|
|
178
|
-
manifest: 'Gemfile.lock',
|
|
179
|
-
auditCommand: 'bundle-audit check',
|
|
180
|
-
fixCommand: 'bundle update',
|
|
181
|
-
type: 'bundler',
|
|
182
|
-
installHint: 'gem install bundler-audit'
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// =============================================================================
|
|
190
|
-
// RUNNING THE AUDIT
|
|
191
|
-
// =============================================================================
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Run the package manager audit and return normalized vulnerability list.
|
|
195
|
-
* Catches exit code 1 (vulnerabilities found) which is NOT a real error.
|
|
196
|
-
*/
|
|
197
|
-
function runAudit(pm, cwd) {
|
|
198
|
-
let stdout;
|
|
199
|
-
try {
|
|
200
|
-
stdout = execSync(pm.auditCommand, { cwd, stdio: ['pipe', 'pipe', 'pipe'] }).toString(); // ship-safe-ignore — command is a hardcoded package manager audit command, not user input
|
|
201
|
-
} catch (err) {
|
|
202
|
-
// npm/yarn/pnpm exit with code 1 when vulns found — that's expected
|
|
203
|
-
if (err.stdout) {
|
|
204
|
-
stdout = err.stdout.toString();
|
|
205
|
-
} else {
|
|
206
|
-
throw err;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
switch (pm.type) {
|
|
211
|
-
case 'npm-v2': return parseNpmAudit(stdout);
|
|
212
|
-
case 'yarn': return parseYarnAudit(stdout);
|
|
213
|
-
case 'pip': return parsePipAudit(stdout);
|
|
214
|
-
case 'bundler': return parseBundlerAudit(stdout);
|
|
215
|
-
default: return [];
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// =============================================================================
|
|
220
|
-
// AUDIT PARSERS
|
|
221
|
-
// =============================================================================
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Parse npm audit v7+ JSON (auditReportVersion: 2).
|
|
225
|
-
* Also works for pnpm which uses the same format.
|
|
226
|
-
*/
|
|
227
|
-
function parseNpmAudit(jsonStr) {
|
|
228
|
-
let data;
|
|
229
|
-
try {
|
|
230
|
-
data = JSON.parse(jsonStr);
|
|
231
|
-
} catch {
|
|
232
|
-
return [];
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const vulns = [];
|
|
236
|
-
|
|
237
|
-
if (data.auditReportVersion === 2) {
|
|
238
|
-
// npm v7+ format
|
|
239
|
-
for (const [name, vuln] of Object.entries(data.vulnerabilities || {})) {
|
|
240
|
-
// Find the advisory details (via array may contain objects or strings)
|
|
241
|
-
const advisory = Array.isArray(vuln.via)
|
|
242
|
-
? vuln.via.find(v => typeof v === 'object')
|
|
243
|
-
: null;
|
|
244
|
-
|
|
245
|
-
vulns.push({
|
|
246
|
-
name,
|
|
247
|
-
range: vuln.range || 'unknown',
|
|
248
|
-
severity: vuln.severity || 'unknown',
|
|
249
|
-
title: advisory?.title || name,
|
|
250
|
-
cve: advisory?.cves?.[0] || null,
|
|
251
|
-
url: advisory?.url || null,
|
|
252
|
-
fix: vuln.fixAvailable
|
|
253
|
-
? (typeof vuln.fixAvailable === 'object'
|
|
254
|
-
? `npm install ${vuln.fixAvailable.name}@${vuln.fixAvailable.version}`
|
|
255
|
-
: 'npm audit fix')
|
|
256
|
-
: null,
|
|
257
|
-
isDirect: vuln.isDirect ?? false
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
} else {
|
|
261
|
-
// npm v6 format (legacy)
|
|
262
|
-
for (const [, adv] of Object.entries(data.advisories || {})) {
|
|
263
|
-
vulns.push({
|
|
264
|
-
name: adv.module_name,
|
|
265
|
-
range: adv.vulnerable_versions,
|
|
266
|
-
severity: adv.severity,
|
|
267
|
-
title: adv.title,
|
|
268
|
-
cve: adv.cves?.[0] || null,
|
|
269
|
-
url: adv.url || null,
|
|
270
|
-
fix: adv.patched_versions !== '<0.0.0'
|
|
271
|
-
? `npm install ${adv.module_name}@"${adv.patched_versions}"`
|
|
272
|
-
: null,
|
|
273
|
-
isDirect: true
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Deduplicate by package name (transitive deps appear multiple times)
|
|
279
|
-
const seen = new Set();
|
|
280
|
-
return vulns.filter(v => {
|
|
281
|
-
const key = `${v.name}:${v.severity}`;
|
|
282
|
-
if (seen.has(key)) return false;
|
|
283
|
-
seen.add(key);
|
|
284
|
-
return true;
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Parse yarn audit output (NDJSON — one JSON object per line).
|
|
290
|
-
*/
|
|
291
|
-
function parseYarnAudit(ndjsonStr) {
|
|
292
|
-
const vulns = [];
|
|
293
|
-
const seen = new Set();
|
|
294
|
-
|
|
295
|
-
for (const line of ndjsonStr.split('\n')) {
|
|
296
|
-
if (!line.trim()) continue;
|
|
297
|
-
let obj;
|
|
298
|
-
try { obj = JSON.parse(line); } catch { continue; }
|
|
299
|
-
|
|
300
|
-
if (obj.type !== 'auditAdvisory') continue;
|
|
301
|
-
const adv = obj.data?.advisory;
|
|
302
|
-
if (!adv) continue;
|
|
303
|
-
|
|
304
|
-
const key = `${adv.module_name}:${adv.severity}`;
|
|
305
|
-
if (seen.has(key)) continue;
|
|
306
|
-
seen.add(key);
|
|
307
|
-
|
|
308
|
-
vulns.push({
|
|
309
|
-
name: adv.module_name,
|
|
310
|
-
range: adv.vulnerable_versions,
|
|
311
|
-
severity: adv.severity,
|
|
312
|
-
title: adv.title,
|
|
313
|
-
cve: adv.cves?.[0] || null,
|
|
314
|
-
url: adv.url || null,
|
|
315
|
-
fix: adv.patched_versions !== '<0.0.0'
|
|
316
|
-
? `yarn upgrade ${adv.module_name}`
|
|
317
|
-
: null,
|
|
318
|
-
isDirect: true
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return vulns;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Parse pip-audit JSON output.
|
|
327
|
-
* Format: [{ name, version, vulns: [{ id, fix_versions, description }] }]
|
|
328
|
-
*/
|
|
329
|
-
function parsePipAudit(jsonStr) {
|
|
330
|
-
let data;
|
|
331
|
-
try {
|
|
332
|
-
data = JSON.parse(jsonStr);
|
|
333
|
-
} catch {
|
|
334
|
-
return [];
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const vulns = [];
|
|
338
|
-
for (const pkg of (Array.isArray(data) ? data : [])) {
|
|
339
|
-
for (const vuln of (pkg.vulns || [])) {
|
|
340
|
-
const fixVersion = vuln.fix_versions?.[0];
|
|
341
|
-
vulns.push({
|
|
342
|
-
name: pkg.name,
|
|
343
|
-
range: `==${pkg.version}`,
|
|
344
|
-
severity: 'high', // pip-audit doesn't reliably report CVSS severity
|
|
345
|
-
title: (vuln.description || vuln.id).slice(0, 100),
|
|
346
|
-
cve: vuln.id.startsWith('CVE-') ? vuln.id : null,
|
|
347
|
-
url: `https://osv.dev/vulnerability/${vuln.id}`,
|
|
348
|
-
fix: fixVersion ? `pip install "${pkg.name}>=${fixVersion}"` : null,
|
|
349
|
-
isDirect: true
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return vulns;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Parse bundler-audit text output (plain text format).
|
|
359
|
-
*/
|
|
360
|
-
function parseBundlerAudit(text) {
|
|
361
|
-
const vulns = [];
|
|
362
|
-
const blocks = text.split(/\n(?=Name:)/);
|
|
363
|
-
|
|
364
|
-
for (const block of blocks) {
|
|
365
|
-
const name = block.match(/^Name:\s*(.+)/m)?.[1]?.trim();
|
|
366
|
-
const version = block.match(/Version:\s*(.+)/m)?.[1]?.trim();
|
|
367
|
-
const cve = block.match(/CVE:\s*(.+)/m)?.[1]?.trim();
|
|
368
|
-
const title = block.match(/Title:\s*(.+)/m)?.[1]?.trim();
|
|
369
|
-
const solution = block.match(/Solution:\s*(.+)/m)?.[1]?.trim();
|
|
370
|
-
|
|
371
|
-
if (name) {
|
|
372
|
-
vulns.push({
|
|
373
|
-
name,
|
|
374
|
-
range: version ? `==${version}` : 'unknown',
|
|
375
|
-
severity: 'high',
|
|
376
|
-
title: title || cve || name,
|
|
377
|
-
cve: cve || null,
|
|
378
|
-
url: cve ? `https://www.cve.org/CVERecord?id=${cve}` : null,
|
|
379
|
-
fix: solution ? `bundle update ${name}` : null,
|
|
380
|
-
isDirect: true
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
return vulns;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// =============================================================================
|
|
389
|
-
// EPSS (Exploit Prediction Scoring System)
|
|
390
|
-
// =============================================================================
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Fetch EPSS scores from FIRST.org API for a list of CVEs.
|
|
394
|
-
* Returns { 'CVE-2023-1234': { epss: 0.942, percentile: 0.99 }, ... }
|
|
395
|
-
* API docs: https://www.first.org/epss/api
|
|
396
|
-
*/
|
|
397
|
-
async function fetchEPSS(cves) {
|
|
398
|
-
if (cves.length === 0) return {};
|
|
399
|
-
|
|
400
|
-
// API accepts up to 100 CVEs per request — batch if needed
|
|
401
|
-
const results = {};
|
|
402
|
-
const batches = [];
|
|
403
|
-
for (let i = 0; i < cves.length; i += 100) {
|
|
404
|
-
batches.push(cves.slice(i, i + 100));
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
for (const batch of batches) {
|
|
408
|
-
const url = `https://api.first.org/data/v1/epss?cve=${batch.join(',')}`; // ship-safe-ignore — hardcoded FIRST.org API endpoint, CVE IDs are from audit results not user input
|
|
409
|
-
const response = await fetch(url, { // ship-safe-ignore — EPSS API fetch with fixed base URL
|
|
410
|
-
headers: { 'Accept': 'application/json' },
|
|
411
|
-
signal: AbortSignal.timeout(10000),
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
if (!response.ok) continue;
|
|
415
|
-
|
|
416
|
-
const json = await response.json();
|
|
417
|
-
for (const entry of (json.data || [])) {
|
|
418
|
-
results[entry.cve] = {
|
|
419
|
-
epss: parseFloat(entry.epss),
|
|
420
|
-
percentile: parseFloat(entry.percentile),
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return results;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// =============================================================================
|
|
429
|
-
// OUTPUT
|
|
430
|
-
// =============================================================================
|
|
431
|
-
|
|
432
|
-
const SEVERITY_ORDER = { critical: 0, high: 1, moderate: 2, medium: 2, low: 3, unknown: 4 };
|
|
433
|
-
const SEVERITY_COLOR = {
|
|
434
|
-
critical: chalk.red.bold,
|
|
435
|
-
high: chalk.red,
|
|
436
|
-
moderate: chalk.yellow,
|
|
437
|
-
medium: chalk.yellow,
|
|
438
|
-
low: chalk.gray,
|
|
439
|
-
unknown: chalk.gray
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
function printDepFindings(vulns, pm) {
|
|
443
|
-
// Sort by severity
|
|
444
|
-
const sorted = [...vulns].sort((a, b) =>
|
|
445
|
-
(SEVERITY_ORDER[a.severity] ?? 4) - (SEVERITY_ORDER[b.severity] ?? 4)
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
const counts = {};
|
|
449
|
-
for (const v of vulns) {
|
|
450
|
-
counts[v.severity] = (counts[v.severity] || 0) + 1;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
console.log(chalk.red.bold(` Dependency Vulnerabilities (${vulns.length})`));
|
|
454
|
-
console.log(chalk.red(' ' + '─'.repeat(58)));
|
|
455
|
-
console.log();
|
|
456
|
-
|
|
457
|
-
for (const v of sorted) {
|
|
458
|
-
const sevColor = SEVERITY_COLOR[v.severity] || chalk.gray;
|
|
459
|
-
const sevLabel = `[${v.severity.toUpperCase()}]`;
|
|
460
|
-
|
|
461
|
-
console.log(
|
|
462
|
-
` ${sevColor(sevLabel.padEnd(12))}` +
|
|
463
|
-
chalk.white(`${v.name}`) +
|
|
464
|
-
chalk.gray(`@${v.range}`)
|
|
465
|
-
);
|
|
466
|
-
console.log(chalk.gray(` ${v.title}`));
|
|
467
|
-
if (v.cve) {
|
|
468
|
-
console.log(chalk.gray(` ${v.cve}`) + (v.url ? chalk.gray(` ${v.url}`) : ''));
|
|
469
|
-
}
|
|
470
|
-
if (v.epss != null) {
|
|
471
|
-
const pct = (v.epss * 100).toFixed(1);
|
|
472
|
-
const epssColor = v.epss >= 0.5 ? chalk.red.bold
|
|
473
|
-
: v.epss >= 0.1 ? chalk.yellow
|
|
474
|
-
: chalk.gray;
|
|
475
|
-
const label = v.epss >= 0.5 ? ' — actively exploited in the wild'
|
|
476
|
-
: v.epss >= 0.1 ? ' — elevated exploit activity'
|
|
477
|
-
: '';
|
|
478
|
-
console.log(epssColor(` EPSS: ${pct}% exploit probability`) + chalk.gray(label));
|
|
479
|
-
}
|
|
480
|
-
if (v.fix) {
|
|
481
|
-
console.log(chalk.gray(' Fix: ') + chalk.cyan(v.fix));
|
|
482
|
-
}
|
|
483
|
-
console.log();
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
console.log(chalk.cyan('='.repeat(60)));
|
|
487
|
-
console.log(chalk.red.bold(` ⚠ Found ${vulns.length} vulnerable package(s)`));
|
|
488
|
-
for (const [sev, count] of Object.entries(counts).sort(([a], [b]) => (SEVERITY_ORDER[a] ?? 4) - (SEVERITY_ORDER[b] ?? 4))) {
|
|
489
|
-
const color = SEVERITY_COLOR[sev] || chalk.gray;
|
|
490
|
-
console.log(color(` • ${sev}: ${count}`));
|
|
491
|
-
}
|
|
492
|
-
console.log(chalk.gray(` Run: `) + chalk.cyan(pm.fixCommand) + chalk.gray(' to apply automatic fixes'));
|
|
493
|
-
console.log(chalk.cyan('='.repeat(60)));
|
|
494
|
-
console.log();
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// =============================================================================
|
|
498
|
-
// INTERNAL: run audit and return normalized vulns (used by score command)
|
|
499
|
-
// =============================================================================
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Run the dependency audit for a given path and return normalized vulnerabilities.
|
|
503
|
-
* Returns { pm, vulns } or { pm: null, vulns: [] } if no manifest found.
|
|
504
|
-
* Does not print anything — used programmatically by other commands.
|
|
505
|
-
*/
|
|
506
|
-
export async function runDepsAudit(rootPath) {
|
|
507
|
-
const pm = detectPackageManager(rootPath);
|
|
508
|
-
if (!pm) return { pm: null, vulns: [] };
|
|
509
|
-
|
|
510
|
-
try {
|
|
511
|
-
const vulns = runAudit(pm, rootPath);
|
|
512
|
-
return { pm, vulns };
|
|
513
|
-
} catch {
|
|
514
|
-
return { pm, vulns: [] };
|
|
515
|
-
}
|
|
516
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Deps Command
|
|
3
|
+
* ============
|
|
4
|
+
*
|
|
5
|
+
* Audit project dependencies for known CVEs using the package manager's
|
|
6
|
+
* built-in audit tool (npm, yarn, pnpm, pip-audit, bundler-audit).
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* npx ship-safe deps [path] Audit dependencies in the project
|
|
10
|
+
* npx ship-safe deps . --fix Also run the package manager fix command
|
|
11
|
+
*
|
|
12
|
+
* SUPPORTED PACKAGE MANAGERS:
|
|
13
|
+
* npm → npm audit --json
|
|
14
|
+
* yarn → yarn audit --json (NDJSON format)
|
|
15
|
+
* pnpm → pnpm audit --json
|
|
16
|
+
* pip → pip-audit --format json (requires: pip install pip-audit)
|
|
17
|
+
* bundler → bundle-audit check (requires: gem install bundler-audit)
|
|
18
|
+
*
|
|
19
|
+
* EXIT CODES:
|
|
20
|
+
* 0 - No vulnerabilities found (or tool not available)
|
|
21
|
+
* 1 - Vulnerabilities found
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import { execSync } from 'child_process';
|
|
27
|
+
import chalk from 'chalk';
|
|
28
|
+
import ora from 'ora';
|
|
29
|
+
import * as output from '../utils/output.js';
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// MAIN COMMAND
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
export async function depsCommand(targetPath = '.', options = {}) {
|
|
36
|
+
const absolutePath = path.resolve(targetPath);
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(absolutePath)) {
|
|
39
|
+
output.error(`Path does not exist: ${absolutePath}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log();
|
|
44
|
+
output.header('Dependency Audit');
|
|
45
|
+
console.log();
|
|
46
|
+
|
|
47
|
+
// ── 1. Detect package manager ─────────────────────────────────────────────
|
|
48
|
+
const pm = detectPackageManager(absolutePath);
|
|
49
|
+
|
|
50
|
+
if (!pm) {
|
|
51
|
+
console.log(chalk.gray(' No supported package manifest found.'));
|
|
52
|
+
console.log(chalk.gray(' Supported: package.json (npm/yarn/pnpm), requirements.txt (pip), Gemfile (bundler)'));
|
|
53
|
+
console.log();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(chalk.gray(` Package manager: ${chalk.white(pm.name)} Manifest: ${chalk.white(pm.manifest)}`));
|
|
58
|
+
console.log();
|
|
59
|
+
|
|
60
|
+
// ── 2. Run audit ──────────────────────────────────────────────────────────
|
|
61
|
+
const spinner = ora({ text: `Running ${pm.name} audit...`, color: 'cyan' }).start();
|
|
62
|
+
let vulns = [];
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
vulns = runAudit(pm, absolutePath);
|
|
66
|
+
spinner.stop();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
spinner.stop();
|
|
69
|
+
if (err.code === 'ENOENT' || /not found|not recognized|command not found/i.test(err.message)) {
|
|
70
|
+
output.warning(`${pm.name} is not installed or not in PATH.`);
|
|
71
|
+
if (pm.installHint) {
|
|
72
|
+
console.log(chalk.gray(` Install it with: `) + chalk.cyan(pm.installHint));
|
|
73
|
+
}
|
|
74
|
+
console.log();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
output.error(`Audit failed: ${err.message}`);
|
|
78
|
+
console.log(chalk.gray(` Try running manually: `) + chalk.cyan(pm.auditCommand));
|
|
79
|
+
console.log();
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── 3. No vulnerabilities ─────────────────────────────────────────────────
|
|
84
|
+
if (vulns.length === 0) {
|
|
85
|
+
output.success(`No known vulnerabilities in your ${pm.name} dependencies!`);
|
|
86
|
+
console.log();
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── 4. Enrich with EPSS scores ──────────────────────────────────────────
|
|
91
|
+
const cves = vulns.map(v => v.cve).filter(Boolean);
|
|
92
|
+
if (cves.length > 0) {
|
|
93
|
+
const epssSpinner = ora({ text: 'Fetching EPSS exploit probability scores...', color: 'cyan' }).start();
|
|
94
|
+
try {
|
|
95
|
+
const epssData = await fetchEPSS(cves);
|
|
96
|
+
for (const v of vulns) {
|
|
97
|
+
if (v.cve && epssData[v.cve]) {
|
|
98
|
+
v.epss = epssData[v.cve].epss;
|
|
99
|
+
v.percentile = epssData[v.cve].percentile;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
epssSpinner.succeed(chalk.gray(`EPSS scores fetched for ${Object.keys(epssData).length} CVE(s)`));
|
|
103
|
+
} catch {
|
|
104
|
+
epssSpinner.stop();
|
|
105
|
+
// EPSS is optional — continue without it
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 5. Display findings ───────────────────────────────────────────────────
|
|
110
|
+
printDepFindings(vulns, pm);
|
|
111
|
+
|
|
112
|
+
// ── 6. Optionally fix ─────────────────────────────────────────────────────
|
|
113
|
+
if (options.fix) {
|
|
114
|
+
console.log();
|
|
115
|
+
console.log(chalk.cyan(` Running: ${pm.fixCommand}`));
|
|
116
|
+
try {
|
|
117
|
+
execSync(pm.fixCommand, { cwd: absolutePath, stdio: 'inherit' }); // ship-safe-ignore — command is a hardcoded package manager command, not user input
|
|
118
|
+
} catch {
|
|
119
|
+
output.warning('Fix command exited with errors — some vulnerabilities may require manual updates.');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// PACKAGE MANAGER DETECTION
|
|
128
|
+
// =============================================================================
|
|
129
|
+
|
|
130
|
+
function detectPackageManager(rootPath) {
|
|
131
|
+
// Node.js — detect specific lock file first for accuracy
|
|
132
|
+
if (fs.existsSync(path.join(rootPath, 'pnpm-lock.yaml'))) {
|
|
133
|
+
return {
|
|
134
|
+
name: 'pnpm',
|
|
135
|
+
manifest: 'package.json',
|
|
136
|
+
auditCommand: 'pnpm audit --json',
|
|
137
|
+
fixCommand: 'pnpm audit --fix',
|
|
138
|
+
type: 'npm-v2'
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (fs.existsSync(path.join(rootPath, 'yarn.lock'))) {
|
|
143
|
+
return {
|
|
144
|
+
name: 'yarn',
|
|
145
|
+
manifest: 'package.json',
|
|
146
|
+
auditCommand: 'yarn audit --json',
|
|
147
|
+
fixCommand: 'yarn upgrade',
|
|
148
|
+
type: 'yarn'
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (fs.existsSync(path.join(rootPath, 'package.json'))) {
|
|
153
|
+
return {
|
|
154
|
+
name: 'npm',
|
|
155
|
+
manifest: 'package.json',
|
|
156
|
+
auditCommand: 'npm audit --json',
|
|
157
|
+
fixCommand: 'npm audit fix',
|
|
158
|
+
type: 'npm-v2'
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Python
|
|
163
|
+
if (fs.existsSync(path.join(rootPath, 'requirements.txt'))) {
|
|
164
|
+
return {
|
|
165
|
+
name: 'pip-audit',
|
|
166
|
+
manifest: 'requirements.txt',
|
|
167
|
+
auditCommand: 'pip-audit --format json -r requirements.txt',
|
|
168
|
+
fixCommand: 'pip-audit --fix -r requirements.txt',
|
|
169
|
+
type: 'pip',
|
|
170
|
+
installHint: 'pip install pip-audit'
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Ruby
|
|
175
|
+
if (fs.existsSync(path.join(rootPath, 'Gemfile.lock'))) {
|
|
176
|
+
return {
|
|
177
|
+
name: 'bundler-audit',
|
|
178
|
+
manifest: 'Gemfile.lock',
|
|
179
|
+
auditCommand: 'bundle-audit check',
|
|
180
|
+
fixCommand: 'bundle update',
|
|
181
|
+
type: 'bundler',
|
|
182
|
+
installHint: 'gem install bundler-audit'
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// =============================================================================
|
|
190
|
+
// RUNNING THE AUDIT
|
|
191
|
+
// =============================================================================
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Run the package manager audit and return normalized vulnerability list.
|
|
195
|
+
* Catches exit code 1 (vulnerabilities found) which is NOT a real error.
|
|
196
|
+
*/
|
|
197
|
+
function runAudit(pm, cwd) {
|
|
198
|
+
let stdout;
|
|
199
|
+
try {
|
|
200
|
+
stdout = execSync(pm.auditCommand, { cwd, stdio: ['pipe', 'pipe', 'pipe'] }).toString(); // ship-safe-ignore — command is a hardcoded package manager audit command, not user input
|
|
201
|
+
} catch (err) {
|
|
202
|
+
// npm/yarn/pnpm exit with code 1 when vulns found — that's expected
|
|
203
|
+
if (err.stdout) {
|
|
204
|
+
stdout = err.stdout.toString();
|
|
205
|
+
} else {
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
switch (pm.type) {
|
|
211
|
+
case 'npm-v2': return parseNpmAudit(stdout);
|
|
212
|
+
case 'yarn': return parseYarnAudit(stdout);
|
|
213
|
+
case 'pip': return parsePipAudit(stdout);
|
|
214
|
+
case 'bundler': return parseBundlerAudit(stdout);
|
|
215
|
+
default: return [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// =============================================================================
|
|
220
|
+
// AUDIT PARSERS
|
|
221
|
+
// =============================================================================
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Parse npm audit v7+ JSON (auditReportVersion: 2).
|
|
225
|
+
* Also works for pnpm which uses the same format.
|
|
226
|
+
*/
|
|
227
|
+
function parseNpmAudit(jsonStr) {
|
|
228
|
+
let data;
|
|
229
|
+
try {
|
|
230
|
+
data = JSON.parse(jsonStr);
|
|
231
|
+
} catch {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const vulns = [];
|
|
236
|
+
|
|
237
|
+
if (data.auditReportVersion === 2) {
|
|
238
|
+
// npm v7+ format
|
|
239
|
+
for (const [name, vuln] of Object.entries(data.vulnerabilities || {})) {
|
|
240
|
+
// Find the advisory details (via array may contain objects or strings)
|
|
241
|
+
const advisory = Array.isArray(vuln.via)
|
|
242
|
+
? vuln.via.find(v => typeof v === 'object')
|
|
243
|
+
: null;
|
|
244
|
+
|
|
245
|
+
vulns.push({
|
|
246
|
+
name,
|
|
247
|
+
range: vuln.range || 'unknown',
|
|
248
|
+
severity: vuln.severity || 'unknown',
|
|
249
|
+
title: advisory?.title || name,
|
|
250
|
+
cve: advisory?.cves?.[0] || null,
|
|
251
|
+
url: advisory?.url || null,
|
|
252
|
+
fix: vuln.fixAvailable
|
|
253
|
+
? (typeof vuln.fixAvailable === 'object'
|
|
254
|
+
? `npm install ${vuln.fixAvailable.name}@${vuln.fixAvailable.version}`
|
|
255
|
+
: 'npm audit fix')
|
|
256
|
+
: null,
|
|
257
|
+
isDirect: vuln.isDirect ?? false
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
// npm v6 format (legacy)
|
|
262
|
+
for (const [, adv] of Object.entries(data.advisories || {})) {
|
|
263
|
+
vulns.push({
|
|
264
|
+
name: adv.module_name,
|
|
265
|
+
range: adv.vulnerable_versions,
|
|
266
|
+
severity: adv.severity,
|
|
267
|
+
title: adv.title,
|
|
268
|
+
cve: adv.cves?.[0] || null,
|
|
269
|
+
url: adv.url || null,
|
|
270
|
+
fix: adv.patched_versions !== '<0.0.0'
|
|
271
|
+
? `npm install ${adv.module_name}@"${adv.patched_versions}"`
|
|
272
|
+
: null,
|
|
273
|
+
isDirect: true
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Deduplicate by package name (transitive deps appear multiple times)
|
|
279
|
+
const seen = new Set();
|
|
280
|
+
return vulns.filter(v => {
|
|
281
|
+
const key = `${v.name}:${v.severity}`;
|
|
282
|
+
if (seen.has(key)) return false;
|
|
283
|
+
seen.add(key);
|
|
284
|
+
return true;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Parse yarn audit output (NDJSON — one JSON object per line).
|
|
290
|
+
*/
|
|
291
|
+
function parseYarnAudit(ndjsonStr) {
|
|
292
|
+
const vulns = [];
|
|
293
|
+
const seen = new Set();
|
|
294
|
+
|
|
295
|
+
for (const line of ndjsonStr.split('\n')) {
|
|
296
|
+
if (!line.trim()) continue;
|
|
297
|
+
let obj;
|
|
298
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
299
|
+
|
|
300
|
+
if (obj.type !== 'auditAdvisory') continue;
|
|
301
|
+
const adv = obj.data?.advisory;
|
|
302
|
+
if (!adv) continue;
|
|
303
|
+
|
|
304
|
+
const key = `${adv.module_name}:${adv.severity}`;
|
|
305
|
+
if (seen.has(key)) continue;
|
|
306
|
+
seen.add(key);
|
|
307
|
+
|
|
308
|
+
vulns.push({
|
|
309
|
+
name: adv.module_name,
|
|
310
|
+
range: adv.vulnerable_versions,
|
|
311
|
+
severity: adv.severity,
|
|
312
|
+
title: adv.title,
|
|
313
|
+
cve: adv.cves?.[0] || null,
|
|
314
|
+
url: adv.url || null,
|
|
315
|
+
fix: adv.patched_versions !== '<0.0.0'
|
|
316
|
+
? `yarn upgrade ${adv.module_name}`
|
|
317
|
+
: null,
|
|
318
|
+
isDirect: true
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return vulns;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Parse pip-audit JSON output.
|
|
327
|
+
* Format: [{ name, version, vulns: [{ id, fix_versions, description }] }]
|
|
328
|
+
*/
|
|
329
|
+
function parsePipAudit(jsonStr) {
|
|
330
|
+
let data;
|
|
331
|
+
try {
|
|
332
|
+
data = JSON.parse(jsonStr);
|
|
333
|
+
} catch {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const vulns = [];
|
|
338
|
+
for (const pkg of (Array.isArray(data) ? data : [])) {
|
|
339
|
+
for (const vuln of (pkg.vulns || [])) {
|
|
340
|
+
const fixVersion = vuln.fix_versions?.[0];
|
|
341
|
+
vulns.push({
|
|
342
|
+
name: pkg.name,
|
|
343
|
+
range: `==${pkg.version}`,
|
|
344
|
+
severity: 'high', // pip-audit doesn't reliably report CVSS severity
|
|
345
|
+
title: (vuln.description || vuln.id).slice(0, 100),
|
|
346
|
+
cve: vuln.id.startsWith('CVE-') ? vuln.id : null,
|
|
347
|
+
url: `https://osv.dev/vulnerability/${vuln.id}`,
|
|
348
|
+
fix: fixVersion ? `pip install "${pkg.name}>=${fixVersion}"` : null,
|
|
349
|
+
isDirect: true
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return vulns;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Parse bundler-audit text output (plain text format).
|
|
359
|
+
*/
|
|
360
|
+
function parseBundlerAudit(text) {
|
|
361
|
+
const vulns = [];
|
|
362
|
+
const blocks = text.split(/\n(?=Name:)/);
|
|
363
|
+
|
|
364
|
+
for (const block of blocks) {
|
|
365
|
+
const name = block.match(/^Name:\s*(.+)/m)?.[1]?.trim();
|
|
366
|
+
const version = block.match(/Version:\s*(.+)/m)?.[1]?.trim();
|
|
367
|
+
const cve = block.match(/CVE:\s*(.+)/m)?.[1]?.trim();
|
|
368
|
+
const title = block.match(/Title:\s*(.+)/m)?.[1]?.trim();
|
|
369
|
+
const solution = block.match(/Solution:\s*(.+)/m)?.[1]?.trim();
|
|
370
|
+
|
|
371
|
+
if (name) {
|
|
372
|
+
vulns.push({
|
|
373
|
+
name,
|
|
374
|
+
range: version ? `==${version}` : 'unknown',
|
|
375
|
+
severity: 'high',
|
|
376
|
+
title: title || cve || name,
|
|
377
|
+
cve: cve || null,
|
|
378
|
+
url: cve ? `https://www.cve.org/CVERecord?id=${cve}` : null,
|
|
379
|
+
fix: solution ? `bundle update ${name}` : null,
|
|
380
|
+
isDirect: true
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return vulns;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// =============================================================================
|
|
389
|
+
// EPSS (Exploit Prediction Scoring System)
|
|
390
|
+
// =============================================================================
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Fetch EPSS scores from FIRST.org API for a list of CVEs.
|
|
394
|
+
* Returns { 'CVE-2023-1234': { epss: 0.942, percentile: 0.99 }, ... }
|
|
395
|
+
* API docs: https://www.first.org/epss/api
|
|
396
|
+
*/
|
|
397
|
+
async function fetchEPSS(cves) {
|
|
398
|
+
if (cves.length === 0) return {};
|
|
399
|
+
|
|
400
|
+
// API accepts up to 100 CVEs per request — batch if needed
|
|
401
|
+
const results = {};
|
|
402
|
+
const batches = [];
|
|
403
|
+
for (let i = 0; i < cves.length; i += 100) {
|
|
404
|
+
batches.push(cves.slice(i, i + 100));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
for (const batch of batches) {
|
|
408
|
+
const url = `https://api.first.org/data/v1/epss?cve=${batch.join(',')}`; // ship-safe-ignore — hardcoded FIRST.org API endpoint, CVE IDs are from audit results not user input
|
|
409
|
+
const response = await fetch(url, { // ship-safe-ignore — EPSS API fetch with fixed base URL
|
|
410
|
+
headers: { 'Accept': 'application/json' },
|
|
411
|
+
signal: AbortSignal.timeout(10000),
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
if (!response.ok) continue;
|
|
415
|
+
|
|
416
|
+
const json = await response.json();
|
|
417
|
+
for (const entry of (json.data || [])) {
|
|
418
|
+
results[entry.cve] = {
|
|
419
|
+
epss: parseFloat(entry.epss),
|
|
420
|
+
percentile: parseFloat(entry.percentile),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return results;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// =============================================================================
|
|
429
|
+
// OUTPUT
|
|
430
|
+
// =============================================================================
|
|
431
|
+
|
|
432
|
+
const SEVERITY_ORDER = { critical: 0, high: 1, moderate: 2, medium: 2, low: 3, unknown: 4 };
|
|
433
|
+
const SEVERITY_COLOR = {
|
|
434
|
+
critical: chalk.red.bold,
|
|
435
|
+
high: chalk.red,
|
|
436
|
+
moderate: chalk.yellow,
|
|
437
|
+
medium: chalk.yellow,
|
|
438
|
+
low: chalk.gray,
|
|
439
|
+
unknown: chalk.gray
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
function printDepFindings(vulns, pm) {
|
|
443
|
+
// Sort by severity
|
|
444
|
+
const sorted = [...vulns].sort((a, b) =>
|
|
445
|
+
(SEVERITY_ORDER[a.severity] ?? 4) - (SEVERITY_ORDER[b.severity] ?? 4)
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
const counts = {};
|
|
449
|
+
for (const v of vulns) {
|
|
450
|
+
counts[v.severity] = (counts[v.severity] || 0) + 1;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
console.log(chalk.red.bold(` Dependency Vulnerabilities (${vulns.length})`));
|
|
454
|
+
console.log(chalk.red(' ' + '─'.repeat(58)));
|
|
455
|
+
console.log();
|
|
456
|
+
|
|
457
|
+
for (const v of sorted) {
|
|
458
|
+
const sevColor = SEVERITY_COLOR[v.severity] || chalk.gray;
|
|
459
|
+
const sevLabel = `[${v.severity.toUpperCase()}]`;
|
|
460
|
+
|
|
461
|
+
console.log(
|
|
462
|
+
` ${sevColor(sevLabel.padEnd(12))}` +
|
|
463
|
+
chalk.white(`${v.name}`) +
|
|
464
|
+
chalk.gray(`@${v.range}`)
|
|
465
|
+
);
|
|
466
|
+
console.log(chalk.gray(` ${v.title}`));
|
|
467
|
+
if (v.cve) {
|
|
468
|
+
console.log(chalk.gray(` ${v.cve}`) + (v.url ? chalk.gray(` ${v.url}`) : ''));
|
|
469
|
+
}
|
|
470
|
+
if (v.epss != null) {
|
|
471
|
+
const pct = (v.epss * 100).toFixed(1);
|
|
472
|
+
const epssColor = v.epss >= 0.5 ? chalk.red.bold
|
|
473
|
+
: v.epss >= 0.1 ? chalk.yellow
|
|
474
|
+
: chalk.gray;
|
|
475
|
+
const label = v.epss >= 0.5 ? ' — actively exploited in the wild'
|
|
476
|
+
: v.epss >= 0.1 ? ' — elevated exploit activity'
|
|
477
|
+
: '';
|
|
478
|
+
console.log(epssColor(` EPSS: ${pct}% exploit probability`) + chalk.gray(label));
|
|
479
|
+
}
|
|
480
|
+
if (v.fix) {
|
|
481
|
+
console.log(chalk.gray(' Fix: ') + chalk.cyan(v.fix));
|
|
482
|
+
}
|
|
483
|
+
console.log();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
487
|
+
console.log(chalk.red.bold(` ⚠ Found ${vulns.length} vulnerable package(s)`));
|
|
488
|
+
for (const [sev, count] of Object.entries(counts).sort(([a], [b]) => (SEVERITY_ORDER[a] ?? 4) - (SEVERITY_ORDER[b] ?? 4))) {
|
|
489
|
+
const color = SEVERITY_COLOR[sev] || chalk.gray;
|
|
490
|
+
console.log(color(` • ${sev}: ${count}`));
|
|
491
|
+
}
|
|
492
|
+
console.log(chalk.gray(` Run: `) + chalk.cyan(pm.fixCommand) + chalk.gray(' to apply automatic fixes'));
|
|
493
|
+
console.log(chalk.cyan('='.repeat(60)));
|
|
494
|
+
console.log();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// =============================================================================
|
|
498
|
+
// INTERNAL: run audit and return normalized vulns (used by score command)
|
|
499
|
+
// =============================================================================
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Run the dependency audit for a given path and return normalized vulnerabilities.
|
|
503
|
+
* Returns { pm, vulns } or { pm: null, vulns: [] } if no manifest found.
|
|
504
|
+
* Does not print anything — used programmatically by other commands.
|
|
505
|
+
*/
|
|
506
|
+
export async function runDepsAudit(rootPath) {
|
|
507
|
+
const pm = detectPackageManager(rootPath);
|
|
508
|
+
if (!pm) return { pm: null, vulns: [] };
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
const vulns = runAudit(pm, rootPath);
|
|
512
|
+
return { pm, vulns };
|
|
513
|
+
} catch {
|
|
514
|
+
return { pm, vulns: [] };
|
|
515
|
+
}
|
|
516
|
+
}
|