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.
Files changed (47) hide show
  1. package/README.md +735 -641
  2. package/cli/agents/api-fuzzer.js +345 -345
  3. package/cli/agents/auth-bypass-agent.js +348 -348
  4. package/cli/agents/base-agent.js +272 -272
  5. package/cli/agents/cicd-scanner.js +236 -201
  6. package/cli/agents/config-auditor.js +521 -521
  7. package/cli/agents/deep-analyzer.js +6 -2
  8. package/cli/agents/git-history-scanner.js +170 -170
  9. package/cli/agents/html-reporter.js +568 -568
  10. package/cli/agents/index.js +84 -84
  11. package/cli/agents/injection-tester.js +500 -500
  12. package/cli/agents/llm-redteam.js +251 -251
  13. package/cli/agents/mobile-scanner.js +231 -231
  14. package/cli/agents/orchestrator.js +322 -322
  15. package/cli/agents/pii-compliance-agent.js +301 -301
  16. package/cli/agents/scoring-engine.js +248 -248
  17. package/cli/agents/supabase-rls-agent.js +154 -154
  18. package/cli/agents/supply-chain-agent.js +650 -507
  19. package/cli/bin/ship-safe.js +452 -426
  20. package/cli/commands/agent.js +608 -608
  21. package/cli/commands/audit.js +986 -980
  22. package/cli/commands/baseline.js +193 -193
  23. package/cli/commands/ci.js +342 -342
  24. package/cli/commands/deps.js +516 -516
  25. package/cli/commands/doctor.js +159 -159
  26. package/cli/commands/fix.js +218 -218
  27. package/cli/commands/hooks.js +268 -0
  28. package/cli/commands/init.js +407 -407
  29. package/cli/commands/mcp.js +304 -304
  30. package/cli/commands/red-team.js +7 -1
  31. package/cli/commands/remediate.js +798 -798
  32. package/cli/commands/rotate.js +571 -571
  33. package/cli/commands/scan.js +569 -569
  34. package/cli/commands/score.js +449 -449
  35. package/cli/commands/watch.js +281 -281
  36. package/cli/hooks/patterns.js +313 -0
  37. package/cli/hooks/post-tool-use.js +140 -0
  38. package/cli/hooks/pre-tool-use.js +186 -0
  39. package/cli/index.js +73 -69
  40. package/cli/providers/llm-provider.js +397 -287
  41. package/cli/utils/autofix-rules.js +74 -74
  42. package/cli/utils/cache-manager.js +311 -311
  43. package/cli/utils/output.js +230 -230
  44. package/cli/utils/patterns.js +1121 -1121
  45. package/cli/utils/pdf-generator.js +94 -94
  46. package/package.json +69 -69
  47. package/configs/supabase/rls-templates.sql +0 -242
@@ -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
+ }