ship-safe 4.3.0 → 5.0.1

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.
@@ -422,6 +422,51 @@ const PATTERNS = [
422
422
  description: 'PHP unserialize() with user input enables object injection attacks.',
423
423
  fix: 'Use json_decode() instead, or validate input with allowed_classes option',
424
424
  },
425
+
426
+ // ── Vibe Code Detection (AI-generated code with security gaps) ──────────
427
+ {
428
+ rule: 'VIBE_TODO_AUTH',
429
+ title: 'Vibe Code: TODO to Add Authentication',
430
+ regex: /(?:\/\/|#|\/\*)\s*(?:TODO|FIXME|HACK|XXX)\s*:?\s*(?:add|implement|fix|handle)\s*(?:auth|authentication|authorization|permission|access.?control|login|session)/gi,
431
+ severity: 'high',
432
+ cwe: 'CWE-306',
433
+ owasp: 'A07:2021',
434
+ confidence: 'high',
435
+ description: 'TODO comment indicates missing authentication/authorization. Common in AI-generated code that creates endpoints without security.',
436
+ fix: 'Implement the missing authentication before shipping. Do not leave security TODOs in production code.',
437
+ },
438
+ {
439
+ rule: 'VIBE_TODO_VALIDATION',
440
+ title: 'Vibe Code: TODO to Add Input Validation',
441
+ regex: /(?:\/\/|#|\/\*)\s*(?:TODO|FIXME|HACK|XXX)\s*:?\s*(?:add|implement|fix|handle)\s*(?:valid|sanitiz|escap|filter|check.?input|input.?valid)/gi,
442
+ severity: 'medium',
443
+ cwe: 'CWE-20',
444
+ owasp: 'A03:2021',
445
+ confidence: 'high',
446
+ description: 'TODO comment indicates missing input validation. AI-generated code often creates the happy path without validation.',
447
+ fix: 'Implement input validation before shipping. Add schema validation (Zod, Joi) for all user inputs.',
448
+ },
449
+ {
450
+ rule: 'VIBE_PLACEHOLDER_SECRET',
451
+ title: 'Vibe Code: Placeholder Secret Left in Code',
452
+ regex: /(?:api[_-]?key|secret|password|token)\s*[:=]\s*['"](?:your[_-]?(?:api[_-]?)?key[_-]?here|sk[_-]xxx|changeme|password123|test123|replace[_-]?me|insert[_-]?here|placeholder|example|CHANGE_ME|YOUR_SECRET)['"]/gi,
453
+ severity: 'high',
454
+ cwe: 'CWE-798',
455
+ owasp: 'A07:2021',
456
+ description: 'Placeholder secret left in code. AI-generated code often includes example credentials that developers forget to replace.',
457
+ fix: 'Replace with environment variable: process.env.API_KEY. Never commit placeholder secrets.',
458
+ },
459
+ {
460
+ rule: 'VIBE_CRUD_NO_AUTH',
461
+ title: 'Vibe Code: CRUD Endpoints Without Auth Middleware',
462
+ regex: /(?:router|app)\.(?:post|put|patch|delete)\s*\(\s*['"]\/(?:api\/)?(?:users?|posts?|items?|products?|orders?|comments?|messages?)(?:\/:[^'"]*)?['"]\s*,\s*(?:async\s+)?\(\s*(?:req|request)/g,
463
+ severity: 'high',
464
+ cwe: 'CWE-862',
465
+ owasp: 'A01:2021',
466
+ confidence: 'medium',
467
+ description: 'Mutation endpoint (POST/PUT/PATCH/DELETE) with no visible auth middleware. Common in AI-generated CRUD boilerplate.',
468
+ fix: 'Add authentication middleware before the route handler: router.post("/api/users", authMiddleware, handler)',
469
+ },
425
470
  ];
426
471
 
427
472
  // =============================================================================
@@ -0,0 +1,358 @@
1
+ /**
2
+ * MCP Security Agent
3
+ * ===================
4
+ *
5
+ * Detects security vulnerabilities in MCP (Model Context Protocol)
6
+ * server implementations. MCP servers are the new attack surface
7
+ * for AI-powered applications.
8
+ *
9
+ * In 2026, 30+ CVEs were filed against MCP servers in 60 days.
10
+ * 82% of implementations are prone to path traversal.
11
+ *
12
+ * Checks: tool poisoning, unauthenticated endpoints, overprivileged
13
+ * tools, input injection, missing rate limiting, credential exposure,
14
+ * unsafe transport.
15
+ *
16
+ * Maps to: OWASP Agentic AI ASI02 (Tool Misuse), ASI03 (Privilege Abuse)
17
+ */
18
+
19
+ import path from 'path';
20
+ import { BaseAgent } from './base-agent.js';
21
+
22
+ // =============================================================================
23
+ // MCP SECURITY PATTERNS
24
+ // =============================================================================
25
+
26
+ const PATTERNS = [
27
+ // ── Tool Poisoning & Validation ──────────────────────────────────────────
28
+ {
29
+ rule: 'MCP_NO_TOOL_VALIDATION',
30
+ title: 'MCP: Tool Call Without Validation',
31
+ regex: /(?:tools\/call|tool_call|callTool|executeTool)\s*\(/g,
32
+ severity: 'high',
33
+ cwe: 'CWE-20',
34
+ owasp: 'A03:2021',
35
+ confidence: 'medium',
36
+ description: 'MCP tool invocation without visible tool-name validation or allowlisting. Attackers can invoke arbitrary tools via tool poisoning.',
37
+ fix: 'Validate tool names against an explicit allowlist before execution',
38
+ },
39
+ {
40
+ rule: 'MCP_DYNAMIC_TOOL_REGISTRATION',
41
+ title: 'MCP: Dynamic Tool Registration from External Source',
42
+ regex: /(?:registerTool|addTool|server\.tool)\s*\(\s*(?:req\.|request\.|body\.|data\.|input\.|params\.)/g,
43
+ severity: 'critical',
44
+ cwe: 'CWE-94',
45
+ owasp: 'A03:2021',
46
+ description: 'Tool registration from external/user input allows attackers to inject malicious tool definitions (tool poisoning attack).',
47
+ fix: 'Only register tools from trusted, hardcoded definitions. Never accept tool definitions from user input.',
48
+ },
49
+
50
+ // ── Authentication & Access Control ──────────────────────────────────────
51
+ {
52
+ rule: 'MCP_NO_AUTH_TRANSPORT',
53
+ title: 'MCP: Server Without Authentication',
54
+ regex: /(?:McpServer|Server|createServer)\s*\(\s*\{(?:(?!auth|token|apiKey|bearer|jwt|session|credential).)*\}\s*\)/gs,
55
+ severity: 'critical',
56
+ cwe: 'CWE-306',
57
+ owasp: 'A07:2021',
58
+ confidence: 'medium',
59
+ description: 'MCP server created without any authentication configuration. Any client can connect and invoke tools.',
60
+ fix: 'Add authentication to MCP server transport: API key validation, JWT verification, or OAuth',
61
+ },
62
+ {
63
+ rule: 'MCP_STDIO_NO_SANDBOX',
64
+ title: 'MCP: stdio Transport Without Sandbox',
65
+ regex: /(?:StdioServerTransport|stdio|transport.*stdio)/g,
66
+ severity: 'medium',
67
+ cwe: 'CWE-269',
68
+ owasp: 'A04:2021',
69
+ confidence: 'medium',
70
+ description: 'MCP server using stdio transport runs in the same process context. Consider sandboxing for untrusted tools.',
71
+ fix: 'Run MCP servers in sandboxed containers or separate processes with limited permissions',
72
+ },
73
+
74
+ // ── Overprivileged Tools ─────────────────────────────────────────────────
75
+ {
76
+ rule: 'MCP_TOOL_SHELL_EXEC',
77
+ title: 'MCP: Tool Executes Shell Commands',
78
+ regex: /(?:server\.tool|registerTool|addTool)[\s\S]{0,500}(?:exec|execSync|spawn|spawnSync|execFile|child_process|subprocess|os\.system|os\.popen)/g,
79
+ severity: 'critical',
80
+ cwe: 'CWE-78',
81
+ owasp: 'A03:2021',
82
+ description: 'MCP tool handler executes shell commands. If tool arguments are user-influenced via prompt injection, this enables RCE.',
83
+ fix: 'Avoid shell execution in MCP tools. If necessary, use strict allowlists for commands and validate all arguments.',
84
+ },
85
+ {
86
+ rule: 'MCP_TOOL_FS_WRITE',
87
+ title: 'MCP: Tool Has File System Write Access',
88
+ regex: /(?:server\.tool|registerTool|addTool)[\s\S]{0,500}(?:writeFile|writeFileSync|appendFile|createWriteStream|fs\.write|unlink|rmdir|mkdir)/g,
89
+ severity: 'high',
90
+ cwe: 'CWE-732',
91
+ owasp: 'A01:2021',
92
+ description: 'MCP tool can write to the file system. Prompt injection could lead to arbitrary file writes or deletions.',
93
+ fix: 'Restrict file operations to a sandboxed directory. Validate all paths against an allowlist.',
94
+ },
95
+ {
96
+ rule: 'MCP_TOOL_DB_MUTATION',
97
+ title: 'MCP: Tool Has Database Write Access',
98
+ regex: /(?:server\.tool|registerTool|addTool)[\s\S]{0,500}(?:INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|\.create\(|\.update\(|\.delete\(|\.destroy\(|\.remove\()/g,
99
+ severity: 'high',
100
+ cwe: 'CWE-284',
101
+ owasp: 'A01:2021',
102
+ description: 'MCP tool can mutate database records. Without confirmation gates, prompt injection can modify or delete data.',
103
+ fix: 'Add human-in-the-loop confirmation for destructive database operations in MCP tools.',
104
+ },
105
+ {
106
+ rule: 'MCP_TOOL_NETWORK_REQUEST',
107
+ title: 'MCP: Tool Makes External Network Requests',
108
+ regex: /(?:server\.tool|registerTool|addTool)[\s\S]{0,500}(?:fetch\(|axios\.|got\(|http\.get|https\.get|request\(|urllib|requests\.)/g,
109
+ severity: 'medium',
110
+ cwe: 'CWE-918',
111
+ owasp: 'A10:2021',
112
+ confidence: 'medium',
113
+ description: 'MCP tool makes external HTTP requests. Prompt injection could trigger SSRF via tool arguments.',
114
+ fix: 'Validate URLs against allowlist. Block internal/private IP ranges.',
115
+ },
116
+
117
+ // ── Input Injection ──────────────────────────────────────────────────────
118
+ {
119
+ rule: 'MCP_TOOL_ARGS_TO_SQL',
120
+ title: 'MCP: Tool Arguments in SQL Query',
121
+ regex: /(?:server\.tool|registerTool)[\s\S]{0,500}(?:`SELECT|`INSERT|`UPDATE|`DELETE|\.query\s*\(\s*`|\.raw\s*\()/g,
122
+ severity: 'critical',
123
+ cwe: 'CWE-89',
124
+ owasp: 'A03:2021',
125
+ description: 'MCP tool constructs SQL queries that may include tool arguments from LLM output. This enables SQL injection via prompt injection.',
126
+ fix: 'Use parameterized queries in all MCP tool handlers. Never interpolate tool arguments into SQL.',
127
+ },
128
+ {
129
+ rule: 'MCP_TOOL_ARGS_TO_EVAL',
130
+ title: 'MCP: Tool Arguments Passed to eval()',
131
+ regex: /(?:server\.tool|registerTool)[\s\S]{0,500}eval\s*\(/g,
132
+ severity: 'critical',
133
+ cwe: 'CWE-94',
134
+ owasp: 'A03:2021',
135
+ description: 'MCP tool passes arguments to eval(). Prompt injection can achieve arbitrary code execution.',
136
+ fix: 'Never use eval() in MCP tool handlers. Use structured data parsing instead.',
137
+ },
138
+ {
139
+ rule: 'MCP_TOOL_PATH_TRAVERSAL',
140
+ title: 'MCP: Tool Arguments in File Path',
141
+ regex: /(?:server\.tool|registerTool)[\s\S]{0,500}(?:path\.join|path\.resolve|readFile|readFileSync)\s*\(\s*(?!__dirname)/g,
142
+ severity: 'high',
143
+ cwe: 'CWE-22',
144
+ owasp: 'A01:2021',
145
+ confidence: 'medium',
146
+ description: 'MCP tool constructs file paths from arguments. Path traversal via prompt injection can read arbitrary files.',
147
+ fix: 'Validate file paths against an allowed directory. Use path.resolve() and check the result starts with the allowed base.',
148
+ },
149
+
150
+ // ── Credential Exposure ──────────────────────────────────────────────────
151
+ {
152
+ rule: 'MCP_HARDCODED_CREDENTIALS',
153
+ title: 'MCP: Credentials in Server Config',
154
+ regex: /(?:mcpServers|mcp_server|server\.json)[\s\S]{0,300}(?:password|secret|token|apiKey|api_key|credential)\s*[:=]\s*["'][^"']+["']/gi,
155
+ severity: 'critical',
156
+ cwe: 'CWE-798',
157
+ owasp: 'A07:2021',
158
+ description: 'Hardcoded credentials in MCP server configuration. These are exposed to anyone with access to the config.',
159
+ fix: 'Use environment variables or a secrets manager for MCP server credentials.',
160
+ },
161
+ {
162
+ rule: 'MCP_ENV_IN_TOOL_RESPONSE',
163
+ title: 'MCP: Environment Variables Exposed in Tool Response',
164
+ regex: /(?:server\.tool|registerTool)[\s\S]{0,500}(?:process\.env|os\.environ)/g,
165
+ severity: 'high',
166
+ cwe: 'CWE-200',
167
+ owasp: 'A01:2021',
168
+ confidence: 'medium',
169
+ description: 'MCP tool accesses environment variables. If returned in tool responses, secrets may leak to the LLM and user.',
170
+ fix: 'Never return raw environment variables in tool responses. Filter sensitive values.',
171
+ },
172
+
173
+ // ── Remote/Untrusted Connections ─────────────────────────────────────────
174
+ {
175
+ rule: 'MCP_REMOTE_UNPINNED',
176
+ title: 'MCP: Remote Server Without Version Pinning',
177
+ regex: /(?:mcpServers|mcp_servers)[\s\S]{0,200}(?:url|command)\s*[:=]\s*["'][^"']*["'](?![\s\S]{0,100}(?:hash|integrity|version|sha|pin|digest))/g,
178
+ severity: 'medium',
179
+ cwe: 'CWE-494',
180
+ owasp: 'A08:2021',
181
+ confidence: 'medium',
182
+ description: 'MCP server reference without version pinning or integrity hash. Vulnerable to rug-pull attacks.',
183
+ fix: 'Pin MCP server versions and validate integrity hashes to prevent supply chain attacks.',
184
+ },
185
+ {
186
+ rule: 'MCP_HTTP_NO_TLS',
187
+ title: 'MCP: HTTP Transport Without TLS',
188
+ regex: /(?:SSEServerTransport|StreamableHTTPServerTransport|mcpServers)[\s\S]{0,200}http:\/\/(?!localhost|127\.0\.0\.1)/g,
189
+ severity: 'high',
190
+ cwe: 'CWE-319',
191
+ owasp: 'A02:2021',
192
+ description: 'MCP server using HTTP (not HTTPS) for non-localhost connections. Tool calls and responses are sent in plaintext.',
193
+ fix: 'Use HTTPS for all remote MCP server connections.',
194
+ },
195
+
196
+ // ── Missing Rate Limiting ────────────────────────────────────────────────
197
+ {
198
+ rule: 'MCP_NO_RATE_LIMIT',
199
+ title: 'MCP: No Rate Limiting on Tool Calls',
200
+ regex: /(?:McpServer|Server|createServer)\s*\(\s*\{(?:(?!rateLimit|rateLimiter|throttle|limit|maxRequests).)*\}\s*\)/gs,
201
+ severity: 'medium',
202
+ cwe: 'CWE-770',
203
+ owasp: 'A04:2021',
204
+ confidence: 'low',
205
+ description: 'MCP server without rate limiting. Enables unbounded consumption attacks (denial of wallet).',
206
+ fix: 'Add rate limiting to MCP server: limit tool calls per client per time window.',
207
+ },
208
+
209
+ // ── Tool Result Injection ────────────────────────────────────────────────
210
+ {
211
+ rule: 'MCP_TOOL_RESULT_UNESCAPED',
212
+ title: 'MCP: Tool Result Injected Into Prompt Without Escaping',
213
+ regex: /(?:tool_result|toolResult|function_result)[\s\S]{0,200}(?:content|messages|prompt)\s*(?:\.push|\.append|\+=|\.concat)/g,
214
+ severity: 'high',
215
+ cwe: 'CWE-74',
216
+ owasp: 'A03:2021',
217
+ confidence: 'medium',
218
+ description: 'Raw tool results injected back into LLM prompt context. Enables tool-to-prompt injection attacks.',
219
+ fix: 'Sanitize and escape tool results before including them in LLM context. Strip any instruction-like content.',
220
+ },
221
+ ];
222
+
223
+ // =============================================================================
224
+ // STRUCTURAL CHECKS (beyond line-by-line regex)
225
+ // =============================================================================
226
+
227
+ const MCP_CONFIG_FILES = [
228
+ 'mcp.json',
229
+ '.mcp.json',
230
+ 'mcp-config.json',
231
+ 'claude_desktop_config.json',
232
+ '.cursor/mcp.json',
233
+ ];
234
+
235
+ // =============================================================================
236
+ // MCP SECURITY AGENT
237
+ // =============================================================================
238
+
239
+ export class MCPSecurityAgent extends BaseAgent {
240
+ constructor() {
241
+ super(
242
+ 'MCPSecurityAgent',
243
+ 'Detect MCP server security vulnerabilities — tool poisoning, auth gaps, privilege escalation',
244
+ 'llm'
245
+ );
246
+ }
247
+
248
+ async analyze(context) {
249
+ const { files, rootPath } = context;
250
+ let findings = [];
251
+
252
+ // ── 1. Scan code files for MCP patterns ──────────────────────────────
253
+ const codeFiles = files.filter(f => {
254
+ const ext = path.extname(f).toLowerCase();
255
+ return ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.go'].includes(ext);
256
+ });
257
+
258
+ for (const file of codeFiles) {
259
+ findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
260
+ }
261
+
262
+ // ── 2. Scan MCP config files ─────────────────────────────────────────
263
+ const configFiles = files.filter(f => {
264
+ const basename = path.basename(f);
265
+ const rel = path.relative(rootPath, f).replace(/\\/g, '/');
266
+ return MCP_CONFIG_FILES.some(cfg => rel.endsWith(cfg) || basename === cfg);
267
+ });
268
+
269
+ for (const file of configFiles) {
270
+ findings = findings.concat(this.scanFileWithPatterns(file, PATTERNS));
271
+ findings = findings.concat(this._checkConfigFile(file));
272
+ }
273
+
274
+ // ── 3. Check for MCP server files without auth patterns ──────────────
275
+ const mcpServerFiles = codeFiles.filter(f => {
276
+ const content = this.readFile(f);
277
+ return content && /(?:McpServer|@modelcontextprotocol|mcp-server|from\s+mcp)/i.test(content);
278
+ });
279
+
280
+ for (const file of mcpServerFiles) {
281
+ findings = findings.concat(this._checkServerAuth(file));
282
+ }
283
+
284
+ return findings;
285
+ }
286
+
287
+ /**
288
+ * Check MCP config files for misconfigurations.
289
+ */
290
+ _checkConfigFile(filePath) {
291
+ const content = this.readFile(filePath);
292
+ if (!content) return [];
293
+
294
+ const findings = [];
295
+
296
+ // Check for hardcoded secrets in config
297
+ const secretPatterns = /(?:password|secret|token|apiKey|api_key)\s*[:=]\s*["'][^"']{8,}["']/gi;
298
+ const lines = content.split('\n');
299
+ for (let i = 0; i < lines.length; i++) {
300
+ if (this.isSuppressed(lines[i])) continue;
301
+ secretPatterns.lastIndex = 0;
302
+ if (secretPatterns.test(lines[i])) {
303
+ findings.push({
304
+ file: filePath,
305
+ line: i + 1,
306
+ column: 0,
307
+ severity: 'critical',
308
+ category: this.category,
309
+ rule: 'MCP_CONFIG_HARDCODED_SECRET',
310
+ title: 'MCP: Hardcoded Secret in Config',
311
+ description: 'MCP configuration contains a hardcoded secret. Use environment variables instead.',
312
+ matched: lines[i].trim().substring(0, 100),
313
+ confidence: 'high',
314
+ cwe: 'CWE-798',
315
+ owasp: 'A07:2021',
316
+ fix: 'Replace hardcoded values with environment variable references: {"env": "MY_SECRET"}',
317
+ });
318
+ }
319
+ }
320
+
321
+ return findings;
322
+ }
323
+
324
+ /**
325
+ * Check if MCP server files have authentication.
326
+ */
327
+ _checkServerAuth(filePath) {
328
+ const content = this.readFile(filePath);
329
+ if (!content) return [];
330
+
331
+ const findings = [];
332
+
333
+ // Check if server file has any auth patterns
334
+ const hasAuth = /(?:auth|authenticate|authorization|bearer|jwt|token|apiKey|session|passport|middleware)/i.test(content);
335
+
336
+ if (!hasAuth) {
337
+ findings.push({
338
+ file: filePath,
339
+ line: 1,
340
+ column: 0,
341
+ severity: 'high',
342
+ category: this.category,
343
+ rule: 'MCP_SERVER_NO_AUTH',
344
+ title: 'MCP: Server Implementation Without Authentication',
345
+ description: 'MCP server implementation has no visible authentication mechanism. Any client can connect and invoke tools.',
346
+ matched: 'No auth pattern found in MCP server file',
347
+ confidence: 'medium',
348
+ cwe: 'CWE-306',
349
+ owasp: 'A07:2021',
350
+ fix: 'Add authentication middleware to your MCP server. Validate client identity before allowing tool invocations.',
351
+ });
352
+ }
353
+
354
+ return findings;
355
+ }
356
+ }
357
+
358
+ export default MCPSecurityAgent;
@@ -190,6 +190,12 @@ export class MobileScanner extends BaseAgent {
190
190
  super('MobileScanner', 'Mobile security scanning (OWASP Mobile Top 10)', 'mobile');
191
191
  }
192
192
 
193
+ shouldRun(recon) {
194
+ return recon?.frameworks?.some(f =>
195
+ ['react-native', 'flutter', 'expo'].includes(f)
196
+ ) ?? false;
197
+ }
198
+
193
199
  async analyze(context) {
194
200
  const { rootPath, files, recon } = context;
195
201
 
@@ -19,6 +19,8 @@ import path from 'path';
19
19
  import ora from 'ora';
20
20
  import chalk from 'chalk';
21
21
  import { ReconAgent } from './recon-agent.js';
22
+ import { VerifierAgent } from './verifier-agent.js';
23
+ import { DeepAnalyzer } from './deep-analyzer.js';
22
24
 
23
25
  // =============================================================================
24
26
  // CONSTANTS
@@ -36,6 +38,7 @@ export class Orchestrator {
36
38
  /** @type {import('./base-agent.js').BaseAgent[]} */
37
39
  this.agents = [];
38
40
  this.reconAgent = new ReconAgent();
41
+ this.verifierAgent = new VerifierAgent();
39
42
  }
40
43
 
41
44
  /**
@@ -105,7 +108,11 @@ export class Orchestrator {
105
108
  }
106
109
 
107
110
  // ── 4. Build shared context ─────────────────────────────────────────────
108
- const context = { rootPath: absolutePath, files, recon, options };
111
+ // sharedFindings allows cross-agent awareness: later agents can see
112
+ // what earlier agents found (e.g., secrets agent finds a key,
113
+ // supply-chain agent can check if it's committed to a public repo).
114
+ const sharedFindings = [];
115
+ const context = { rootPath: absolutePath, files, recon, options, sharedFindings };
109
116
  if (options.changedFiles) {
110
117
  context.changedFiles = options.changedFiles;
111
118
  }
@@ -119,8 +126,17 @@ export class Orchestrator {
119
126
  color: 'cyan'
120
127
  }).start();
121
128
 
122
- for (let i = 0; i < agentsToRun.length; i += concurrency) {
123
- const chunk = agentsToRun.slice(i, i + concurrency);
129
+ // Filter agents by framework relevance (shouldRun check)
130
+ const relevantAgents = agentsToRun.filter(a => {
131
+ if (typeof a.shouldRun === 'function') {
132
+ return a.shouldRun(recon);
133
+ }
134
+ return true;
135
+ });
136
+ const skippedAgents = agentsToRun.length - relevantAgents.length;
137
+
138
+ for (let i = 0; i < relevantAgents.length; i += concurrency) {
139
+ const chunk = relevantAgents.slice(i, i + concurrency);
124
140
  const settled = await Promise.allSettled(
125
141
  chunk.map(agent => this.runAgent(agent, context, timeout))
126
142
  );
@@ -138,6 +154,8 @@ export class Orchestrator {
138
154
  success: true,
139
155
  });
140
156
  allFindings = allFindings.concat(findings);
157
+ // Share findings with subsequent agents
158
+ sharedFindings.push(...findings);
141
159
  } else {
142
160
  agentResults.push({
143
161
  agent: agent.name,
@@ -156,15 +174,16 @@ export class Orchestrator {
156
174
  const failed = agentResults.filter(a => !a.success).length;
157
175
  const totalFindings = allFindings.length;
158
176
 
177
+ const skipNote = skippedAgents > 0 ? `, ${skippedAgents} skipped (not relevant)` : '';
159
178
  if (failed > 0) {
160
179
  spinner.warn(chalk.yellow(
161
- `${succeeded}/${agentsToRun.length} agents completed, ${failed} failed, ${totalFindings} finding(s)`
180
+ `${succeeded}/${relevantAgents.length} agents completed, ${failed} failed, ${totalFindings} finding(s)${skipNote}`
162
181
  ));
163
182
  } else {
164
183
  spinner.succeed(
165
184
  totalFindings === 0
166
- ? chalk.green(`${succeeded} agents: clean`)
167
- : chalk.yellow(`${succeeded} agents: ${totalFindings} finding(s)`)
185
+ ? chalk.green(`${succeeded} agents: clean${skipNote}`)
186
+ : chalk.yellow(`${succeeded} agents: ${totalFindings} finding(s)${skipNote}`)
168
187
  );
169
188
  }
170
189
  }
@@ -187,10 +206,50 @@ export class Orchestrator {
187
206
  // ── 6. Deduplicate ────────────────────────────────────────────────────────
188
207
  allFindings = this.deduplicate(allFindings);
189
208
 
190
- // ── 7. Context-aware confidence tuning ──────────────────────────────────
209
+ // ── 7. Second-pass verification (confirms or downgrades findings) ───────
210
+ if (!options.skipVerifier) {
211
+ const verifySpinner = quiet ? null : ora({ text: 'Verifying findings...', color: 'cyan' }).start();
212
+ allFindings = this.verifierAgent.verify(allFindings, options);
213
+ const verified = allFindings.filter(f => f.verified === true).length;
214
+ const downgraded = allFindings.filter(f => f.verified === false).length;
215
+ if (verifySpinner) {
216
+ verifySpinner.succeed(chalk.green(
217
+ `Verified: ${verified} confirmed, ${downgraded} downgraded`
218
+ ));
219
+ }
220
+ }
221
+
222
+ // ── 8. Deep LLM analysis (optional, --deep flag) ───────────────────────
223
+ if (options.deep) {
224
+ const analyzer = DeepAnalyzer.create(absolutePath, {
225
+ local: options.local,
226
+ model: options.model,
227
+ budgetCents: options.budget || 50,
228
+ verbose: options.verbose,
229
+ });
230
+
231
+ if (analyzer) {
232
+ const deepSpinner = quiet ? null : ora({ text: `Deep analysis with ${analyzer.provider.name}...`, color: 'cyan' }).start();
233
+ try {
234
+ allFindings = await analyzer.analyze(allFindings, { rootPath: absolutePath, recon });
235
+ const stats = analyzer.getStats();
236
+ if (deepSpinner) {
237
+ deepSpinner.succeed(chalk.green(
238
+ `Deep analysis: ${stats.analyzedCount} findings analyzed (${stats.spentCents}c spent)`
239
+ ));
240
+ }
241
+ } catch (err) {
242
+ if (deepSpinner) deepSpinner.fail(chalk.yellow(`Deep analysis failed: ${err.message}`));
243
+ }
244
+ } else if (!quiet) {
245
+ console.log(chalk.gray(' Deep analysis: no LLM provider found (set ANTHROPIC_API_KEY or use --local)'));
246
+ }
247
+ }
248
+
249
+ // ── 9. Context-aware confidence tuning ──────────────────────────────────
191
250
  allFindings = this.tuneConfidence(allFindings);
192
251
 
193
- // ── 8. Sort by severity ───────────────────────────────────────────────────
252
+ // ── 10. Sort by severity ──────────────────────────────────────────────────
194
253
  const sevOrder = { critical: 0, high: 1, medium: 2, low: 3 };
195
254
  allFindings.sort((a, b) =>
196
255
  (sevOrder[a.severity] ?? 4) - (sevOrder[b.severity] ?? 4)