ship-safe 4.3.0 → 5.0.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.
@@ -283,7 +283,158 @@ export class SupplyChainAudit extends BaseAgent {
283
283
  }
284
284
  }
285
285
 
286
- // ── 5. Check Python requirements ──────────────────────────────────────────
286
+ // ── 5. Package behavioral signals (Socket-style) ─────────────────────────
287
+ if (fs.existsSync(pkgPath)) {
288
+ try {
289
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
290
+ const allDeps = {
291
+ ...(pkg.dependencies || {}),
292
+ ...(pkg.devDependencies || {}),
293
+ };
294
+
295
+ // Scan node_modules for behavioral red flags
296
+ const nodeModulesPath = path.join(rootPath, 'node_modules');
297
+ if (fs.existsSync(nodeModulesPath)) {
298
+ for (const depName of Object.keys(allDeps).slice(0, 50)) {
299
+ const depDir = path.join(nodeModulesPath, depName);
300
+ const depPkgPath = path.join(depDir, 'package.json');
301
+ if (!fs.existsSync(depPkgPath)) continue;
302
+ try {
303
+ const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
304
+
305
+ // Check for postinstall scripts with network/eval calls
306
+ const scripts = depPkg.scripts || {};
307
+ for (const hook of ['preinstall', 'install', 'postinstall']) {
308
+ const cmd = scripts[hook];
309
+ if (!cmd) continue;
310
+ if (/node\s+-e|node\s+--eval/.test(cmd)) {
311
+ findings.push(createFinding({
312
+ file: depPkgPath,
313
+ line: 0,
314
+ severity: 'high',
315
+ category: 'supply-chain',
316
+ rule: 'BEHAVIORAL_INLINE_EVAL',
317
+ title: `Inline Code Execution in ${hook}: ${depName}`,
318
+ description: `Dependency "${depName}" runs inline Node.js code during ${hook}. This is a common pattern in malicious packages.`,
319
+ matched: cmd.slice(0, 200),
320
+ fix: 'Review the inline code. Consider using --ignore-scripts or removing the dependency.',
321
+ }));
322
+ }
323
+ }
324
+ } catch { /* skip */ }
325
+ }
326
+ }
327
+
328
+ // Detect obfuscated code patterns in dependencies
329
+ const codeFiles = (context.files || []).filter(f =>
330
+ f.includes('node_modules') &&
331
+ !f.includes('node_modules/.cache') &&
332
+ path.extname(f).toLowerCase() === '.js' &&
333
+ !path.basename(f).endsWith('.min.js')
334
+ ).slice(0, 30); // Sample up to 30 files
335
+
336
+ for (const file of codeFiles) {
337
+ const content = this.readFile(file);
338
+ if (!content || content.length < 100) continue;
339
+
340
+ // Excessive hex encoding
341
+ const hexMatches = (content.match(/\\x[0-9a-fA-F]{2}/g) || []).length;
342
+ if (hexMatches > 20) {
343
+ findings.push(createFinding({
344
+ file,
345
+ line: 1,
346
+ severity: 'high',
347
+ category: 'supply-chain',
348
+ rule: 'BEHAVIORAL_HEX_OBFUSCATION',
349
+ title: 'Obfuscated Code: Excessive Hex Encoding',
350
+ description: `File contains ${hexMatches} hex-encoded sequences. Common in malicious packages trying to hide payload.`,
351
+ matched: `${hexMatches} hex sequences detected`,
352
+ fix: 'Inspect the deobfuscated code. Consider removing this dependency.',
353
+ }));
354
+ }
355
+
356
+ // Excessive String.fromCharCode
357
+ const charCodeMatches = (content.match(/String\.fromCharCode/g) || []).length;
358
+ if (charCodeMatches > 5) {
359
+ findings.push(createFinding({
360
+ file,
361
+ line: 1,
362
+ severity: 'high',
363
+ category: 'supply-chain',
364
+ rule: 'BEHAVIORAL_CHARCODE_OBFUSCATION',
365
+ title: 'Obfuscated Code: Excessive String.fromCharCode',
366
+ description: `File contains ${charCodeMatches} String.fromCharCode calls. Common obfuscation technique in malicious packages.`,
367
+ matched: `${charCodeMatches} String.fromCharCode calls`,
368
+ fix: 'Inspect the deobfuscated code. Consider removing this dependency.',
369
+ }));
370
+ }
371
+
372
+ // Base64 decode chains
373
+ const base64Matches = (content.match(/Buffer\.from\s*\([^,]+,\s*['"]base64['"]\)/g) || []).length;
374
+ if (base64Matches > 3) {
375
+ findings.push(createFinding({
376
+ file,
377
+ line: 1,
378
+ severity: 'medium',
379
+ category: 'supply-chain',
380
+ rule: 'BEHAVIORAL_BASE64_DECODE',
381
+ title: 'Suspicious: Multiple Base64 Decode Operations',
382
+ description: `File contains ${base64Matches} base64 decode operations. May indicate hidden payload.`,
383
+ matched: `${base64Matches} base64 decode operations`,
384
+ confidence: 'medium',
385
+ fix: 'Review what data is being decoded. Legitimate use is possible but warrants inspection.',
386
+ }));
387
+ }
388
+ }
389
+
390
+ // Detect unused dependencies (in package.json but never imported)
391
+ const projectFiles = (context.files || []).filter(f =>
392
+ !f.includes('node_modules') &&
393
+ ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(path.extname(f).toLowerCase())
394
+ );
395
+
396
+ if (projectFiles.length > 0 && projectFiles.length < 500) {
397
+ const allImports = new Set();
398
+ for (const file of projectFiles) {
399
+ const content = this.readFile(file);
400
+ if (!content) continue;
401
+ // Capture import/require module names
402
+ const importMatches = content.matchAll(/(?:from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g);
403
+ for (const m of importMatches) {
404
+ const mod = (m[1] || m[2] || '').split('/')[0]; // Get package name (not subpath)
405
+ if (mod && !mod.startsWith('.')) allImports.add(mod);
406
+ // Handle scoped packages
407
+ const fullMod = m[1] || m[2] || '';
408
+ if (fullMod.startsWith('@')) {
409
+ const scopedPkg = fullMod.split('/').slice(0, 2).join('/');
410
+ allImports.add(scopedPkg);
411
+ }
412
+ }
413
+ }
414
+
415
+ const prodDeps = Object.keys(pkg.dependencies || {});
416
+ for (const dep of prodDeps) {
417
+ if (!allImports.has(dep) && !dep.startsWith('@types/')) {
418
+ findings.push(createFinding({
419
+ file: pkgPath,
420
+ line: 0,
421
+ severity: 'low',
422
+ category: 'supply-chain',
423
+ rule: 'UNUSED_DEPENDENCY',
424
+ title: `Unused Dependency: ${dep}`,
425
+ description: `"${dep}" is in dependencies but never imported in project code. Unused dependencies increase attack surface.`,
426
+ matched: dep,
427
+ confidence: 'low',
428
+ fix: `Remove if unused: npm uninstall ${dep}`,
429
+ }));
430
+ }
431
+ }
432
+ }
433
+
434
+ } catch { /* skip */ }
435
+ }
436
+
437
+ // ── 6. Check Python requirements ──────────────────────────────────────────
287
438
  const reqPath = path.join(rootPath, 'requirements.txt');
288
439
  if (fs.existsSync(reqPath)) {
289
440
  const content = this.readFile(reqPath) || '';
@@ -0,0 +1,292 @@
1
+ /**
2
+ * VerifierAgent — Second-Pass Finding Confirmation
3
+ * ==================================================
4
+ *
5
+ * Runs after all agents complete. Takes high-confidence findings
6
+ * and attempts to confirm or downgrade them by analyzing surrounding
7
+ * code context.
8
+ *
9
+ * Checks:
10
+ * - Is the flagged value static/hardcoded or dynamic (from user input)?
11
+ * - Is there upstream sanitization or validation?
12
+ * - Is the code inside error handling that neutralizes it?
13
+ * - Is the finding in dead/unreachable code?
14
+ *
15
+ * Impact: Unverified findings get downgraded one confidence level.
16
+ */
17
+
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+
21
+ // =============================================================================
22
+ // HEURISTIC PATTERNS
23
+ // =============================================================================
24
+
25
+ /** Sources of user input — if a finding's matched code references these, it's more likely real */
26
+ const USER_INPUT_SOURCES = [
27
+ /req\.body/,
28
+ /req\.query/,
29
+ /req\.params/,
30
+ /req\.headers/,
31
+ /request\.body/,
32
+ /request\.query/,
33
+ /request\.params/,
34
+ /request\.form/,
35
+ /request\.args/,
36
+ /request\.json/,
37
+ /ctx\.request/,
38
+ /ctx\.query/,
39
+ /ctx\.params/,
40
+ /event\.body/,
41
+ /event\.queryStringParameters/,
42
+ /searchParams/,
43
+ /formData/,
44
+ /userinput/i,
45
+ /user_input/i,
46
+ /input\s*\(/,
47
+ /argv/,
48
+ /process\.env/,
49
+ /getenv/,
50
+ ];
51
+
52
+ /** Sanitization/validation indicators — presence near a finding suggests it's protected */
53
+ const SANITIZATION_PATTERNS = [
54
+ /sanitize/i,
55
+ /validate/i,
56
+ /escape/i,
57
+ /purify/i,
58
+ /DOMPurify/,
59
+ /xss\s*\(/i,
60
+ /htmlencode/i,
61
+ /encodeURI/,
62
+ /encodeURIComponent/,
63
+ /parameterized/i,
64
+ /prepared\s*statement/i,
65
+ /placeholder/i,
66
+ /\?\s*,/,
67
+ /\$\d+/,
68
+ /bindParam/i,
69
+ /bindValue/i,
70
+ /zod/i,
71
+ /yup/i,
72
+ /joi\./i,
73
+ /ajv/i,
74
+ /schema\.parse/i,
75
+ /safeParse/i,
76
+ /validator\./i,
77
+ /parseInt\s*\(/,
78
+ /parseFloat\s*\(/,
79
+ /Number\s*\(/,
80
+ /\.trim\s*\(/,
81
+ /\.replace\s*\(/,
82
+ /allowlist/i,
83
+ /whitelist/i,
84
+ /blocklist/i,
85
+ /blacklist/i,
86
+ ];
87
+
88
+ /** Error handling wrappers — findings inside these are less exploitable */
89
+ const ERROR_HANDLING_PATTERNS = [
90
+ /}\s*catch\s*\(/,
91
+ /\.catch\s*\(/,
92
+ /try\s*\{/,
93
+ /if\s*\(\s*err/,
94
+ /on\s*\(\s*['"]error['"]/,
95
+ /\.on\s*\(\s*['"]error['"]/,
96
+ ];
97
+
98
+ /** Static/hardcoded value indicators — finding uses a constant, not user input */
99
+ const STATIC_VALUE_PATTERNS = [
100
+ /['"][^'"]{0,200}['"]/,
101
+ /const\s+\w+\s*=\s*['"][^'"]*['"]/,
102
+ /^\s*\/\//,
103
+ /^\s*\*/,
104
+ /^\s*#/,
105
+ /TODO|FIXME|HACK|NOTE/,
106
+ ];
107
+
108
+ /** Dead code indicators */
109
+ const DEAD_CODE_PATTERNS = [
110
+ /return\s+/,
111
+ /throw\s+/,
112
+ /process\.exit/,
113
+ /^\s*\/\//,
114
+ ];
115
+
116
+ // =============================================================================
117
+ // VERIFIER AGENT
118
+ // =============================================================================
119
+
120
+ export class VerifierAgent {
121
+ constructor() {
122
+ this.name = 'VerifierAgent';
123
+ this.description = 'Second-pass verification of findings';
124
+ }
125
+
126
+ /**
127
+ * Verify an array of findings by analyzing surrounding code context.
128
+ * Returns findings with added `verified` and `verifierNote` fields.
129
+ *
130
+ * @param {object[]} findings — Findings from all agents (post-dedup)
131
+ * @param {object} options — { verbose }
132
+ * @returns {object[]} — Findings with verification metadata
133
+ */
134
+ verify(findings, options = {}) {
135
+ const fileCache = new Map();
136
+
137
+ for (const finding of findings) {
138
+ // Only verify critical and high severity findings
139
+ if (finding.severity !== 'critical' && finding.severity !== 'high') {
140
+ finding.verified = null; // not checked
141
+ continue;
142
+ }
143
+
144
+ const result = this._verifyFinding(finding, fileCache);
145
+ finding.verified = result.verified;
146
+ finding.verifierNote = result.note;
147
+
148
+ // Downgrade unverified findings one confidence level
149
+ if (!result.verified) {
150
+ if (finding.confidence === 'high') finding.confidence = 'medium';
151
+ else if (finding.confidence === 'medium') finding.confidence = 'low';
152
+ }
153
+ }
154
+
155
+ return findings;
156
+ }
157
+
158
+ /**
159
+ * Verify a single finding by reading surrounding code.
160
+ */
161
+ _verifyFinding(finding, fileCache) {
162
+ const { file, line, matched } = finding;
163
+ if (!file || !line) {
164
+ return { verified: null, note: 'Missing file or line info' };
165
+ }
166
+
167
+ // Read the file (cached)
168
+ let lines;
169
+ if (fileCache.has(file)) {
170
+ lines = fileCache.get(file);
171
+ } else {
172
+ try {
173
+ const content = fs.readFileSync(file, 'utf-8');
174
+ lines = content.split('\n');
175
+ fileCache.set(file, lines);
176
+ } catch {
177
+ return { verified: null, note: 'Could not read file' };
178
+ }
179
+ }
180
+
181
+ // Get a 30-line window around the finding (15 before, 15 after)
182
+ const windowStart = Math.max(0, line - 16);
183
+ const windowEnd = Math.min(lines.length, line + 15);
184
+ const window = lines.slice(windowStart, windowEnd);
185
+ const windowText = window.join('\n');
186
+
187
+ // Get lines BEFORE the finding (for upstream checks)
188
+ const beforeStart = Math.max(0, line - 16);
189
+ const beforeEnd = Math.max(0, line - 1);
190
+ const beforeText = lines.slice(beforeStart, beforeEnd).join('\n');
191
+
192
+ // Get the finding line itself
193
+ const findingLine = lines[line - 1] || '';
194
+
195
+ // ── Check 1: Is user input involved? ──────────────────────────
196
+ const hasUserInput = USER_INPUT_SOURCES.some(p => p.test(windowText));
197
+
198
+ // ── Check 2: Is there sanitization/validation upstream? ───────
199
+ const hasSanitization = SANITIZATION_PATTERNS.some(p => p.test(beforeText));
200
+
201
+ // ── Check 3: Is the value static/hardcoded? ───────────────────
202
+ const isStatic = this._isStaticValue(findingLine, matched);
203
+
204
+ // ── Check 4: Is it inside error handling? ─────────────────────
205
+ const inErrorHandler = ERROR_HANDLING_PATTERNS.some(p => p.test(beforeText));
206
+
207
+ // ── Check 5: Is it in dead/unreachable code? ──────────────────
208
+ const isDeadCode = this._isDeadCode(lines, line);
209
+
210
+ // ── Decision logic ────────────────────────────────────────────
211
+ if (isDeadCode) {
212
+ return {
213
+ verified: false,
214
+ note: 'Finding appears to be in unreachable code (after return/throw)',
215
+ };
216
+ }
217
+
218
+ if (isStatic && !hasUserInput) {
219
+ return {
220
+ verified: false,
221
+ note: 'Value appears to be static/hardcoded, not user-controlled',
222
+ };
223
+ }
224
+
225
+ if (hasSanitization) {
226
+ return {
227
+ verified: false,
228
+ note: 'Sanitization or validation detected upstream of finding',
229
+ };
230
+ }
231
+
232
+ if (hasUserInput && !hasSanitization) {
233
+ return {
234
+ verified: true,
235
+ note: 'User input flows to this sink without visible sanitization',
236
+ };
237
+ }
238
+
239
+ if (inErrorHandler) {
240
+ return {
241
+ verified: false,
242
+ note: 'Finding is inside error handling context, reducing exploitability',
243
+ };
244
+ }
245
+
246
+ // Default: cannot determine, keep as-is
247
+ return {
248
+ verified: null,
249
+ note: 'Could not determine verification status from code context',
250
+ };
251
+ }
252
+
253
+ /**
254
+ * Check if the matched code is using a static/hardcoded value.
255
+ */
256
+ _isStaticValue(line, matched) {
257
+ // If the finding line is a comment, it's static
258
+ if (/^\s*(?:\/\/|#|\*|\/\*)/.test(line)) return true;
259
+
260
+ // If the matched text is just a string literal with no interpolation
261
+ if (/^['"][^'"]*['"]$/.test(matched)) return true;
262
+
263
+ // If the line is a const assignment to a string literal
264
+ if (/const\s+\w+\s*=\s*['"][^'"]*['"]/.test(line)) return true;
265
+
266
+ // If it looks like a TODO/placeholder comment
267
+ if (/TODO|FIXME|EXAMPLE|PLACEHOLDER|SAMPLE/i.test(line)) return true;
268
+
269
+ return false;
270
+ }
271
+
272
+ /**
273
+ * Check if a line is after a return/throw (dead code).
274
+ */
275
+ _isDeadCode(lines, lineNum) {
276
+ // Check the 5 lines before the finding for return/throw
277
+ for (let i = Math.max(0, lineNum - 6); i < lineNum - 1; i++) {
278
+ const l = lines[i]?.trim() || '';
279
+ // If a return/throw is found and there's no conditional/block opener after
280
+ if (/^(?:return\s|throw\s|process\.exit)/.test(l)) {
281
+ // Check if there's a } or else between the return and our line
282
+ const between = lines.slice(i + 1, lineNum - 1).join('\n');
283
+ if (!/[{}]|else|case/.test(between)) {
284
+ return true;
285
+ }
286
+ }
287
+ }
288
+ return false;
289
+ }
290
+ }
291
+
292
+ export default VerifierAgent;
@@ -36,6 +36,7 @@ import { watchCommand } from '../commands/watch.js';
36
36
  import { auditCommand } from '../commands/audit.js';
37
37
  import { doctorCommand } from '../commands/doctor.js';
38
38
  import { baselineCommand } from '../commands/baseline.js';
39
+ import { ciCommand } from '../commands/ci.js';
39
40
  import { PolicyEngine } from '../agents/policy-engine.js';
40
41
  import { SBOMGenerator } from '../agents/sbom-generator.js';
41
42
 
@@ -188,7 +189,7 @@ program
188
189
  // -----------------------------------------------------------------------------
189
190
  program
190
191
  .command('audit [path]')
191
- .description('Full security audit: secrets + 12 agents + deps + score + remediation plan')
192
+ .description('Full security audit: secrets + 16 agents + deps + score + deep analysis + remediation plan')
192
193
  .option('--json', 'Output results as JSON')
193
194
  .option('--sarif', 'Output results in SARIF format')
194
195
  .option('--csv', 'Output results as CSV')
@@ -201,6 +202,11 @@ program
201
202
  .option('--no-cache', 'Force full rescan (ignore cached results)')
202
203
  .option('--baseline', 'Only show findings not in the baseline')
203
204
  .option('--pdf [file]', 'Generate PDF report (requires Chrome/Chromium)')
205
+ .option('--deep', 'LLM-powered taint analysis for critical/high findings')
206
+ .option('--local', 'Use local Ollama model for deep analysis (default: llama3.2)')
207
+ .option('--model <model>', 'LLM model to use for deep/AI analysis')
208
+ .option('--budget <cents>', 'Max spend in cents for deep analysis (default: 50)', parseInt)
209
+ .option('--verify', 'Check if leaked secrets are still active (probes provider APIs)')
204
210
  .option('-v, --verbose', 'Verbose output')
205
211
  .action(auditCommand);
206
212
 
@@ -209,7 +215,7 @@ program
209
215
  // -----------------------------------------------------------------------------
210
216
  program
211
217
  .command('red-team [path]')
212
- .description('Multi-agent security audit: 12 agents scan for 50+ attack classes')
218
+ .description('Multi-agent security audit: 16 agents scan for 80+ attack classes')
213
219
  .option('--agents <list>', 'Comma-separated list of agents to run')
214
220
  .option('--json', 'Output results as JSON')
215
221
  .option('--sarif', 'Output results in SARIF format')
@@ -217,6 +223,10 @@ program
217
223
  .option('--sbom [file]', 'Generate CycloneDX SBOM')
218
224
  .option('--no-deps', 'Skip dependency audit')
219
225
  .option('--no-ai', 'Skip AI classification')
226
+ .option('--deep', 'LLM-powered taint analysis for critical/high findings')
227
+ .option('--local', 'Use local Ollama model for deep analysis (default: llama3.2)')
228
+ .option('--model <model>', 'LLM model for deep analysis')
229
+ .option('--budget <cents>', 'Max spend in cents for deep analysis (default: 50)', parseInt)
220
230
  .option('-v, --verbose', 'Verbose output')
221
231
  .action(redTeamCommand);
222
232
 
@@ -269,6 +279,20 @@ program
269
279
  .option('--clear', 'Remove the baseline')
270
280
  .action(baselineCommand);
271
281
 
282
+ // -----------------------------------------------------------------------------
283
+ // CI COMMAND (v5.0 — CI/CD Pipeline Integration)
284
+ // -----------------------------------------------------------------------------
285
+ program
286
+ .command('ci [path]')
287
+ .description('CI/CD pipeline mode: scan, score, exit 1 on failure — optimized for automation')
288
+ .option('--threshold <score>', 'Minimum passing score (default: 75)', parseInt)
289
+ .option('--fail-on <severity>', 'Fail on findings at this severity or above (critical, high, medium)')
290
+ .option('--sarif <file>', 'Write SARIF output for GitHub Code Scanning')
291
+ .option('--json', 'JSON output')
292
+ .option('--no-deps', 'Skip dependency audit')
293
+ .option('--baseline', 'Only check new findings (not in baseline)')
294
+ .action(ciCommand);
295
+
272
296
  // -----------------------------------------------------------------------------
273
297
  // DOCTOR COMMAND
274
298
  // -----------------------------------------------------------------------------
@@ -285,11 +309,13 @@ program
285
309
  if (process.argv.length === 2) {
286
310
  console.log(banner);
287
311
  console.log(chalk.yellow('\nQuick start:\n'));
288
- console.log(chalk.cyan.bold(' v4.3 — Full Security Audit'));
289
- console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets + agents + deps + remediation plan'));
290
- console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('# 12-agent red team scan (50+ attack classes)'));
312
+ console.log(chalk.cyan.bold(' v5.0 — Full Security Audit'));
313
+ console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets + 16 agents + deps + remediation'));
314
+ console.log(chalk.white(' npx ship-safe audit . --deep') + chalk.gray('# LLM-powered taint analysis (Anthropic/Ollama)'));
315
+ console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('# 16-agent red team scan (80+ attack classes)'));
316
+ console.log(chalk.white(' npx ship-safe ci . ') + chalk.gray('# CI/CD mode: scan, score, exit code'));
291
317
  console.log(chalk.white(' npx ship-safe watch . ') + chalk.gray('# Continuous monitoring mode'));
292
- console.log(chalk.white(' npx ship-safe sbom . ') + chalk.gray('# Generate CycloneDX SBOM'));
318
+ console.log(chalk.white(' npx ship-safe sbom . ') + chalk.gray('# Generate CycloneDX SBOM (CRA-ready)'));
293
319
  console.log(chalk.white(' npx ship-safe policy init ') + chalk.gray('# Create security policy template'));
294
320
  console.log(chalk.white(' npx ship-safe doctor ') + chalk.gray('# Check environment and configuration'));
295
321
  console.log();
@@ -36,6 +36,7 @@ import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
36
36
  import { CacheManager } from '../utils/cache-manager.js';
37
37
  import { filterBaseline } from './baseline.js';
38
38
  import { generatePDF, generatePrintHTML, isChromeAvailable } from '../utils/pdf-generator.js';
39
+ import { SecretsVerifier } from '../utils/secrets-verifier.js';
39
40
 
40
41
  // =============================================================================
41
42
  // CONSTANTS
@@ -163,6 +164,11 @@ export async function auditCommand(targetPath = '.', options = {}) {
163
164
  // Suppress individual agent spinners by using quiet mode
164
165
  // Pass changedFiles for incremental scanning if cache is valid
165
166
  const orchestratorOpts = { quiet: true };
167
+ if (options.deep) orchestratorOpts.deep = true;
168
+ if (options.local) orchestratorOpts.local = true;
169
+ if (options.model) orchestratorOpts.model = options.model;
170
+ if (options.budget) orchestratorOpts.budget = options.budget;
171
+ if (options.verbose) orchestratorOpts.verbose = true;
166
172
  if (cacheDiff && cacheDiff.changedFiles.length < allFiles.length) {
167
173
  orchestratorOpts.changedFiles = cacheDiff.changedFiles;
168
174
  }
@@ -287,6 +293,32 @@ export async function auditCommand(targetPath = '.', options = {}) {
287
293
  }
288
294
  }
289
295
 
296
+ // ── Secrets Verification (optional, --verify flag) ─────────────────────
297
+ if (options.verify) {
298
+ const verifySpinner = machineOutput ? null : ora({ text: 'Verifying leaked secrets against provider APIs...', color: 'cyan' }).start();
299
+ try {
300
+ const verifier = new SecretsVerifier();
301
+ const verifyResults = await verifier.verify(filteredFindings);
302
+ const activeCount = verifyResults.filter(r => r.result.active === true).length;
303
+ const inactiveCount = verifyResults.filter(r => r.result.active === false).length;
304
+ if (verifySpinner) {
305
+ verifySpinner.succeed(chalk.green(
306
+ `Secrets verified: ${activeCount} active, ${inactiveCount} inactive, ${verifyResults.length - activeCount - inactiveCount} unknown`
307
+ ));
308
+ }
309
+ // Show active secrets warning
310
+ if (activeCount > 0 && !machineOutput) {
311
+ console.log(chalk.red.bold(' ⚠ ACTIVE SECRETS DETECTED — rotate immediately:'));
312
+ for (const r of verifyResults.filter(r => r.result.active === true)) {
313
+ const rel = path.relative(absolutePath, r.finding.file).replace(/\\/g, '/');
314
+ console.log(chalk.red(` ${r.result.provider}: ${rel}:${r.finding.line} — ${r.result.info}`));
315
+ }
316
+ }
317
+ } catch (err) {
318
+ if (verifySpinner) verifySpinner.fail(chalk.yellow(`Secrets verification failed: ${err.message}`));
319
+ }
320
+ }
321
+
290
322
  // ── Save Cache ──────────────────────────────────────────────────────────
291
323
  if (useCache) {
292
324
  try {