@vibecheckai/cli 3.7.0 → 3.8.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 (99) hide show
  1. package/README.md +135 -63
  2. package/bin/_deprecations.js +447 -19
  3. package/bin/_router.js +1 -1
  4. package/bin/registry.js +347 -280
  5. package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
  6. package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
  7. package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
  8. package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
  9. package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
  10. package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
  11. package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
  12. package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
  13. package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
  14. package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
  15. package/bin/runners/lib/agent-firewall/index.js +200 -0
  16. package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
  17. package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
  18. package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +622 -0
  19. package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
  20. package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
  21. package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
  22. package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
  23. package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
  24. package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
  25. package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
  26. package/bin/runners/lib/agent-firewall/session/index.js +26 -0
  27. package/bin/runners/lib/artifact-envelope.js +540 -0
  28. package/bin/runners/lib/auth-shared.js +977 -0
  29. package/bin/runners/lib/checkpoint.js +941 -0
  30. package/bin/runners/lib/cleanup/engine.js +571 -0
  31. package/bin/runners/lib/cleanup/index.js +53 -0
  32. package/bin/runners/lib/cleanup/output.js +375 -0
  33. package/bin/runners/lib/cleanup/rules.js +1060 -0
  34. package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
  35. package/bin/runners/lib/doctor/failure-signatures.js +526 -0
  36. package/bin/runners/lib/doctor/fix-script.js +336 -0
  37. package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
  38. package/bin/runners/lib/doctor/modules/index.js +62 -3
  39. package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
  40. package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
  41. package/bin/runners/lib/doctor/safe-repair.js +384 -0
  42. package/bin/runners/lib/engines/attack-detector.js +1192 -0
  43. package/bin/runners/lib/entitlements-v2.js +2 -2
  44. package/bin/runners/lib/missions/briefing.js +427 -0
  45. package/bin/runners/lib/missions/checkpoint.js +753 -0
  46. package/bin/runners/lib/missions/hardening.js +851 -0
  47. package/bin/runners/lib/missions/plan.js +421 -32
  48. package/bin/runners/lib/missions/safety-gates.js +645 -0
  49. package/bin/runners/lib/missions/schema.js +478 -0
  50. package/bin/runners/lib/packs/bundle.js +675 -0
  51. package/bin/runners/lib/packs/evidence-pack.js +671 -0
  52. package/bin/runners/lib/packs/pack-factory.js +837 -0
  53. package/bin/runners/lib/packs/permissions-pack.js +686 -0
  54. package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
  55. package/bin/runners/lib/safelist/index.js +96 -0
  56. package/bin/runners/lib/safelist/integration.js +334 -0
  57. package/bin/runners/lib/safelist/matcher.js +696 -0
  58. package/bin/runners/lib/safelist/schema.js +948 -0
  59. package/bin/runners/lib/safelist/store.js +438 -0
  60. package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
  61. package/bin/runners/lib/ship-gate.js +832 -0
  62. package/bin/runners/lib/ship-manifest.js +1153 -0
  63. package/bin/runners/lib/ship-output.js +1 -1
  64. package/bin/runners/lib/unified-cli-output.js +710 -383
  65. package/bin/runners/lib/upsell.js +3 -3
  66. package/bin/runners/lib/why-tree.js +650 -0
  67. package/bin/runners/runAllowlist.js +33 -4
  68. package/bin/runners/runApprove.js +240 -1122
  69. package/bin/runners/runAudit.js +692 -0
  70. package/bin/runners/runAuth.js +325 -29
  71. package/bin/runners/runCheckpoint.js +442 -494
  72. package/bin/runners/runCleanup.js +343 -0
  73. package/bin/runners/runDoctor.js +269 -19
  74. package/bin/runners/runFix.js +411 -32
  75. package/bin/runners/runForge.js +411 -0
  76. package/bin/runners/runIntent.js +906 -0
  77. package/bin/runners/runKickoff.js +878 -0
  78. package/bin/runners/runLaunch.js +2000 -0
  79. package/bin/runners/runLink.js +785 -0
  80. package/bin/runners/runMcp.js +1741 -837
  81. package/bin/runners/runPacks.js +2089 -0
  82. package/bin/runners/runPolish.js +41 -0
  83. package/bin/runners/runSafelist.js +1190 -0
  84. package/bin/runners/runScan.js +21 -9
  85. package/bin/runners/runShield.js +1282 -0
  86. package/bin/runners/runShip.js +395 -16
  87. package/bin/vibecheck.js +34 -6
  88. package/mcp-server/README.md +117 -158
  89. package/mcp-server/handlers/tool-handler.ts +3 -3
  90. package/mcp-server/index.js +16 -0
  91. package/mcp-server/intent-firewall-interceptor.js +529 -0
  92. package/mcp-server/manifest.json +473 -0
  93. package/mcp-server/package.json +1 -1
  94. package/mcp-server/registry/tool-registry.js +315 -523
  95. package/mcp-server/registry/tools.json +442 -428
  96. package/mcp-server/tier-auth.js +68 -11
  97. package/mcp-server/tools-v3.js +70 -16
  98. package/package.json +1 -1
  99. package/bin/runners/runProof.zip +0 -0
@@ -0,0 +1,1192 @@
1
+ /**
2
+ * Attack Detector Engine - "Convincing Wrongness" Scanner
3
+ *
4
+ * ═══════════════════════════════════════════════════════════════════════════════
5
+ * Finds code that LOOKS done but DOESN'T WORK.
6
+ * The most dangerous kind of bug: the invisible one.
7
+ * ═══════════════════════════════════════════════════════════════════════════════
8
+ *
9
+ * Attack Categories:
10
+ * 1. DEAD_ROUTE - Routes that exist but don't work (client/server mismatch)
11
+ * 2. GHOST_ENV - Env vars used but never declared
12
+ * 3. FAKE_SUCCESS - UI shows success without actual operation
13
+ * 4. AUTH_DRIFT - Auth patterns that look secure but aren't
14
+ * 5. MOCK_LANDMINE - Mock/TODO code hidden in production paths
15
+ * 6. SILENT_FAIL - Errors swallowed without user feedback
16
+ * 7. OPTIMISTIC_BOMB - Optimistic UI without rollback
17
+ * 8. PAID_THEATER - Paid features that aren't enforced
18
+ *
19
+ * @version 2.0.0
20
+ */
21
+
22
+ "use strict";
23
+
24
+ const fs = require("fs");
25
+ const path = require("path");
26
+ const fg = require("fast-glob");
27
+ const crypto = require("crypto");
28
+
29
+ // ═══════════════════════════════════════════════════════════════════════════════
30
+ // CONSTANTS
31
+ // ═══════════════════════════════════════════════════════════════════════════════
32
+
33
+ const STANDARD_IGNORE = [
34
+ "**/node_modules/**",
35
+ "**/.git/**",
36
+ "**/dist/**",
37
+ "**/build/**",
38
+ "**/.next/**",
39
+ "**/coverage/**",
40
+ "**/_archive/**",
41
+ "**/*.min.js",
42
+ "**/*.bundle.js",
43
+ ];
44
+
45
+ const TEST_FILE_PATTERNS = [
46
+ /\.(test|spec|stories|mock)\.(tsx?|jsx?)$/i,
47
+ /__tests__/,
48
+ /__mocks__/,
49
+ /\/test\//,
50
+ /\/tests\//,
51
+ /\.test\./,
52
+ /\.spec\./,
53
+ ];
54
+
55
+ // Critical paths where findings are more severe
56
+ const CRITICAL_PATHS = [
57
+ "**/api/**",
58
+ "**/auth/**",
59
+ "**/payment/**",
60
+ "**/billing/**",
61
+ "**/checkout/**",
62
+ "**/middleware/**",
63
+ "**/login/**",
64
+ "**/signup/**",
65
+ "**/register/**",
66
+ "**/admin/**",
67
+ "**/dashboard/**",
68
+ ];
69
+
70
+ // ═══════════════════════════════════════════════════════════════════════════════
71
+ // FINDING SCHEMA
72
+ // ═══════════════════════════════════════════════════════════════════════════════
73
+
74
+ /**
75
+ * @typedef {Object} AttackFinding
76
+ * @property {string} id - Stable unique ID (e.g., "ATK-001-abc123")
77
+ * @property {string} type - Attack category
78
+ * @property {string} severity - "critical" | "high" | "medium" | "low"
79
+ * @property {number} confidence - 0-100 confidence score
80
+ * @property {number} blastRadius - 1-10 impact scope
81
+ * @property {string} title - Human-readable title
82
+ * @property {string} description - What's wrong
83
+ * @property {string} file - File path (relative)
84
+ * @property {number} line - Line number
85
+ * @property {string} snippet - Code snippet (max 120 chars)
86
+ * @property {string} whyConvincing - Why it LOOKS correct
87
+ * @property {string} whyWrong - Why it DOESN'T work
88
+ * @property {string} howToProve - Steps to verify the bug exists
89
+ * @property {string} howToFix - Actionable fix instructions
90
+ * @property {Array<{file: string, line?: number, reason: string}>} evidence - Supporting evidence
91
+ * @property {boolean} inCriticalPath - Is this in auth/payment/etc
92
+ * @property {string} attackVector - How this could be exploited
93
+ */
94
+
95
+ /**
96
+ * @typedef {Object} AttackReport
97
+ * @property {string} version - Report format version
98
+ * @property {string} timestamp - ISO timestamp
99
+ * @property {string} projectPath - Scanned project path
100
+ * @property {string} mode - "fast" | "deep"
101
+ * @property {AttackFinding[]} findings - All findings
102
+ * @property {Object} summary - Summary statistics
103
+ * @property {number} attackScore - 0-100 vulnerability score
104
+ * @property {Object} manifest - Deterministic manifest for CI
105
+ */
106
+
107
+ // ═══════════════════════════════════════════════════════════════════════════════
108
+ // SEVERITY MODEL
109
+ // ═══════════════════════════════════════════════════════════════════════════════
110
+
111
+ const SEVERITY_WEIGHTS = {
112
+ critical: 100,
113
+ high: 40,
114
+ medium: 15,
115
+ low: 5,
116
+ };
117
+
118
+ const BLAST_RADIUS = {
119
+ // Scope of impact
120
+ SINGLE_FILE: 1,
121
+ SINGLE_FEATURE: 3,
122
+ MODULE: 5,
123
+ CROSS_MODULE: 7,
124
+ ENTIRE_APP: 10,
125
+ };
126
+
127
+ // ═══════════════════════════════════════════════════════════════════════════════
128
+ // UTILITY FUNCTIONS
129
+ // ═══════════════════════════════════════════════════════════════════════════════
130
+
131
+ function stableId(type, context) {
132
+ const hash = crypto.createHash("sha256")
133
+ .update(`${type}:${context}`)
134
+ .digest("hex")
135
+ .slice(0, 8);
136
+ return `ATK-${type.slice(0, 3).toUpperCase()}-${hash}`;
137
+ }
138
+
139
+ function isTestFile(filePath) {
140
+ return TEST_FILE_PATTERNS.some(pattern =>
141
+ pattern instanceof RegExp ? pattern.test(filePath) : filePath.includes(pattern)
142
+ );
143
+ }
144
+
145
+ function isInCriticalPath(filePath) {
146
+ const normalized = filePath.replace(/\\/g, "/").toLowerCase();
147
+ for (const pattern of CRITICAL_PATHS) {
148
+ const regex = new RegExp(pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*"));
149
+ if (regex.test(normalized)) return true;
150
+ }
151
+ return false;
152
+ }
153
+
154
+ function readFileContent(filePath) {
155
+ try {
156
+ return fs.readFileSync(filePath, "utf8");
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ function getSnippet(content, line, maxLength = 120) {
163
+ const lines = content.split("\n");
164
+ const targetLine = lines[line - 1] || "";
165
+ return targetLine.trim().slice(0, maxLength);
166
+ }
167
+
168
+ function getCodeContext(content, line, before = 2, after = 2) {
169
+ const lines = content.split("\n");
170
+ const start = Math.max(0, line - 1 - before);
171
+ const end = Math.min(lines.length, line + after);
172
+ return lines.slice(start, end).join("\n");
173
+ }
174
+
175
+ // ═══════════════════════════════════════════════════════════════════════════════
176
+ // ATTACK DETECTORS
177
+ // ═══════════════════════════════════════════════════════════════════════════════
178
+
179
+ /**
180
+ * ATTACK 1: Dead Routes - Routes that exist but don't work
181
+ * Client references a route that doesn't exist on server, or vice versa
182
+ */
183
+ function detectDeadRoutes(repoRoot, mode = "fast") {
184
+ const findings = [];
185
+
186
+ // Find API routes (server-side)
187
+ const apiRoutes = new Map();
188
+ const apiFiles = fg.sync([
189
+ "**/app/api/**/route.{ts,tsx,js,jsx}",
190
+ "**/pages/api/**/*.{ts,tsx,js,jsx}",
191
+ "**/src/api/**/*.{ts,tsx,js,jsx}",
192
+ "**/server/routes/**/*.{ts,tsx,js,jsx}",
193
+ ], { cwd: repoRoot, absolute: true, ignore: STANDARD_IGNORE });
194
+
195
+ for (const file of apiFiles) {
196
+ const content = readFileContent(file);
197
+ if (!content) continue;
198
+
199
+ const relPath = path.relative(repoRoot, file).replace(/\\/g, "/");
200
+
201
+ // Extract route path from file path
202
+ let routePath = "";
203
+ if (relPath.includes("app/api/")) {
204
+ routePath = "/api/" + relPath.split("app/api/")[1].replace(/\/route\.(ts|tsx|js|jsx)$/, "");
205
+ } else if (relPath.includes("pages/api/")) {
206
+ routePath = "/api/" + relPath.split("pages/api/")[1].replace(/\.(ts|tsx|js|jsx)$/, "");
207
+ }
208
+
209
+ // Check what HTTP methods are exported
210
+ const methods = [];
211
+ if (/export\s+(async\s+)?function\s+GET/m.test(content)) methods.push("GET");
212
+ if (/export\s+(async\s+)?function\s+POST/m.test(content)) methods.push("POST");
213
+ if (/export\s+(async\s+)?function\s+PUT/m.test(content)) methods.push("PUT");
214
+ if (/export\s+(async\s+)?function\s+DELETE/m.test(content)) methods.push("DELETE");
215
+ if (/export\s+(async\s+)?function\s+PATCH/m.test(content)) methods.push("PATCH");
216
+
217
+ // Check for stub implementations
218
+ const isStub = /throw\s+new\s+Error\s*\(\s*["'`]Not implemented/i.test(content) ||
219
+ /\/\/\s*TODO/i.test(content) ||
220
+ /return\s+(NextResponse\.)?json\s*\(\s*\{\s*\}\s*\)/.test(content);
221
+
222
+ apiRoutes.set(routePath, { file: relPath, methods, isStub, content });
223
+ }
224
+
225
+ // Find client-side fetch calls
226
+ const clientFiles = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
227
+ cwd: repoRoot,
228
+ absolute: true,
229
+ ignore: [...STANDARD_IGNORE, "**/api/**"],
230
+ });
231
+
232
+ for (const file of clientFiles) {
233
+ if (isTestFile(file)) continue;
234
+
235
+ const content = readFileContent(file);
236
+ if (!content) continue;
237
+
238
+ const relPath = path.relative(repoRoot, file).replace(/\\/g, "/");
239
+
240
+ // Find fetch calls to /api routes
241
+ const fetchPattern = /fetch\s*\(\s*["'`]([^"'`]+api[^"'`]*)["'`]/g;
242
+ let match;
243
+
244
+ while ((match = fetchPattern.exec(content)) !== null) {
245
+ const fetchUrl = match[1];
246
+ const line = content.slice(0, match.index).split("\n").length;
247
+
248
+ // Normalize URL
249
+ let normalizedUrl = fetchUrl.split("?")[0]; // Remove query params
250
+ normalizedUrl = normalizedUrl.replace(/\$\{[^}]+\}/g, "[param]"); // Replace template literals
251
+
252
+ // Check if route exists
253
+ if (normalizedUrl.startsWith("/api/") && !apiRoutes.has(normalizedUrl)) {
254
+ // Check for dynamic routes
255
+ const dynamicMatches = [...apiRoutes.keys()].filter(r => {
256
+ const pattern = r.replace(/\[.*?\]/g, "[^/]+");
257
+ return new RegExp(`^${pattern}$`).test(normalizedUrl);
258
+ });
259
+
260
+ if (dynamicMatches.length === 0) {
261
+ findings.push({
262
+ id: stableId("DEAD_ROUTE", `${relPath}:${line}:${normalizedUrl}`),
263
+ type: "DEAD_ROUTE",
264
+ severity: isInCriticalPath(relPath) ? "critical" : "high",
265
+ confidence: 85,
266
+ blastRadius: BLAST_RADIUS.SINGLE_FEATURE,
267
+ title: `Dead API route: ${normalizedUrl}`,
268
+ description: `Client fetches ${normalizedUrl} but no matching API route exists`,
269
+ file: relPath,
270
+ line,
271
+ snippet: getSnippet(content, line),
272
+ whyConvincing: "The fetch call looks correct with proper URL formatting",
273
+ whyWrong: "The server has no handler for this route - requests will 404",
274
+ howToProve: `curl -X GET http://localhost:3000${normalizedUrl} - expect 404`,
275
+ howToFix: `Create API route at app/api${normalizedUrl.replace("/api", "")}/route.ts`,
276
+ evidence: [{ file: relPath, line, reason: "Fetch call to missing route" }],
277
+ inCriticalPath: isInCriticalPath(relPath),
278
+ attackVector: "Feature appears to work in UI but silently fails",
279
+ });
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ // Check for stub routes
286
+ for (const [routePath, { file, methods, isStub, content }] of apiRoutes) {
287
+ if (isStub) {
288
+ const line = content.split("\n").findIndex(l =>
289
+ /throw\s+new\s+Error\s*\(\s*["'`]Not implemented/i.test(l) ||
290
+ /\/\/\s*TODO/i.test(l)
291
+ ) + 1 || 1;
292
+
293
+ findings.push({
294
+ id: stableId("DEAD_ROUTE", `${file}:stub:${routePath}`),
295
+ type: "DEAD_ROUTE",
296
+ severity: isInCriticalPath(file) ? "critical" : "high",
297
+ confidence: 95,
298
+ blastRadius: BLAST_RADIUS.SINGLE_FEATURE,
299
+ title: `Stub API route: ${routePath}`,
300
+ description: `API route exists but throws "Not implemented" or has TODO`,
301
+ file,
302
+ line,
303
+ snippet: getSnippet(content, line),
304
+ whyConvincing: "Route file exists with proper Next.js structure",
305
+ whyWrong: "Handler throws error or is incomplete - will fail at runtime",
306
+ howToProve: `curl -X ${methods[0] || "GET"} http://localhost:3000${routePath}`,
307
+ howToFix: "Implement the route handler with actual logic",
308
+ evidence: [{ file, line, reason: "Stub implementation detected" }],
309
+ inCriticalPath: isInCriticalPath(file),
310
+ attackVector: "Feature endpoint returns 500 errors to users",
311
+ });
312
+ }
313
+ }
314
+
315
+ return findings;
316
+ }
317
+
318
+ /**
319
+ * ATTACK 2: Ghost Env - Environment variables used but never declared
320
+ */
321
+ function detectGhostEnv(repoRoot, mode = "fast") {
322
+ const findings = [];
323
+
324
+ // Standard Node.js/runtime env vars that don't need to be declared
325
+ const STANDARD_ENV_VARS = new Set([
326
+ "NODE_ENV",
327
+ "CI",
328
+ "DEBUG",
329
+ "HOME",
330
+ "PATH",
331
+ "USER",
332
+ "PWD",
333
+ "SHELL",
334
+ "TERM",
335
+ "LANG",
336
+ "TZ",
337
+ "TMPDIR",
338
+ "TEMP",
339
+ "TMP",
340
+ "npm_package_version",
341
+ "npm_package_name",
342
+ ]);
343
+
344
+ // Find declared env vars
345
+ const declaredEnvVars = new Set(STANDARD_ENV_VARS);
346
+ const envFiles = [".env", ".env.local", ".env.example", ".env.template", ".env.development"];
347
+
348
+ for (const envFile of envFiles) {
349
+ const envPath = path.join(repoRoot, envFile);
350
+ const content = readFileContent(envPath);
351
+ if (!content) continue;
352
+
353
+ const varPattern = /^([A-Z][A-Z0-9_]*)=/gm;
354
+ let match;
355
+ while ((match = varPattern.exec(content)) !== null) {
356
+ declaredEnvVars.add(match[1]);
357
+ }
358
+ }
359
+
360
+ // Find used env vars in code
361
+ const codeFiles = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
362
+ cwd: repoRoot,
363
+ absolute: true,
364
+ ignore: STANDARD_IGNORE,
365
+ });
366
+
367
+ for (const file of codeFiles) {
368
+ if (isTestFile(file)) continue;
369
+
370
+ const content = readFileContent(file);
371
+ if (!content) continue;
372
+
373
+ const relPath = path.relative(repoRoot, file).replace(/\\/g, "/");
374
+
375
+ // Find process.env.VAR_NAME usage
376
+ const envUsagePattern = /process\.env\.([A-Z][A-Z0-9_]*)/g;
377
+ let match;
378
+
379
+ while ((match = envUsagePattern.exec(content)) !== null) {
380
+ const varName = match[1];
381
+ const line = content.slice(0, match.index).split("\n").length;
382
+
383
+ // Skip if declared
384
+ if (declaredEnvVars.has(varName)) continue;
385
+
386
+ // Check if there's a fallback
387
+ const snippet = getSnippet(content, line);
388
+ const hasFallback = /\|\||\?\?/.test(snippet) ||
389
+ new RegExp(`${varName}.*\\|\\||${varName}.*\\?\\?`).test(snippet);
390
+
391
+ // Check if it's conditionally checked
392
+ const context = getCodeContext(content, line, 3, 0);
393
+ const isConditional = /if\s*\([^)]*process\.env/.test(context);
394
+
395
+ if (!hasFallback && !isConditional) {
396
+ findings.push({
397
+ id: stableId("GHOST_ENV", `${relPath}:${line}:${varName}`),
398
+ type: "GHOST_ENV",
399
+ severity: isInCriticalPath(relPath) ? "critical" : "high",
400
+ confidence: hasFallback ? 60 : 90,
401
+ blastRadius: isInCriticalPath(relPath) ? BLAST_RADIUS.MODULE : BLAST_RADIUS.SINGLE_FEATURE,
402
+ title: `Missing env var: ${varName}`,
403
+ description: `Code uses ${varName} but it's not declared in any .env file`,
404
+ file: relPath,
405
+ line,
406
+ snippet,
407
+ whyConvincing: "Environment variable access looks standard",
408
+ whyWrong: `${varName} is undefined - will be 'undefined' or cause runtime crash`,
409
+ howToProve: `Add console.log(process.env.${varName}) - will be undefined`,
410
+ howToFix: `Add ${varName}=your_value to .env.local and .env.example`,
411
+ evidence: [{ file: relPath, line, reason: `Uses undeclared ${varName}` }],
412
+ inCriticalPath: isInCriticalPath(relPath),
413
+ attackVector: "Feature silently fails or crashes in production",
414
+ });
415
+ }
416
+ }
417
+
418
+ // Check for import.meta.env (Vite)
419
+ const viteEnvPattern = /import\.meta\.env\.([A-Z][A-Z0-9_]*)/g;
420
+ while ((match = viteEnvPattern.exec(content)) !== null) {
421
+ const varName = match[1];
422
+ const line = content.slice(0, match.index).split("\n").length;
423
+
424
+ if (!declaredEnvVars.has(varName) && !varName.startsWith("VITE_")) {
425
+ findings.push({
426
+ id: stableId("GHOST_ENV", `${relPath}:${line}:${varName}:vite`),
427
+ type: "GHOST_ENV",
428
+ severity: "high",
429
+ confidence: 85,
430
+ blastRadius: BLAST_RADIUS.SINGLE_FEATURE,
431
+ title: `Missing Vite env var: ${varName}`,
432
+ description: `Vite env var ${varName} used but not declared`,
433
+ file: relPath,
434
+ line,
435
+ snippet: getSnippet(content, line),
436
+ whyConvincing: "Vite env pattern looks correct",
437
+ whyWrong: "Variable will be undefined at build time",
438
+ howToProve: "Check browser console for undefined value",
439
+ howToFix: `Add VITE_${varName}=value to .env`,
440
+ evidence: [{ file: relPath, line, reason: "Undeclared Vite env" }],
441
+ inCriticalPath: isInCriticalPath(relPath),
442
+ attackVector: "Client-side feature breaks silently",
443
+ });
444
+ }
445
+ }
446
+ }
447
+
448
+ return findings;
449
+ }
450
+
451
+ /**
452
+ * ATTACK 3: Fake Success - UI shows success without actual operation
453
+ */
454
+ function detectFakeSuccess(repoRoot, mode = "fast") {
455
+ const findings = [];
456
+
457
+ const codeFiles = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
458
+ cwd: repoRoot,
459
+ absolute: true,
460
+ ignore: STANDARD_IGNORE,
461
+ });
462
+
463
+ // Patterns that indicate fake success
464
+ const fakeSuccessPatterns = [
465
+ // Toast/notification without API call
466
+ {
467
+ pattern: /toast\.(success|info)\s*\([^)]+\)\s*;?\s*$/m,
468
+ name: "Toast success without API",
469
+ check: (content, match) => {
470
+ // Check if there's an await/fetch nearby
471
+ const context = getCodeContext(content, content.slice(0, match.index).split("\n").length, 10, 0);
472
+ return !/await\s|fetch\(|\.mutate\(|\.post\(|\.put\(/.test(context);
473
+ }
474
+ },
475
+ // Direct success state without API
476
+ {
477
+ pattern: /set(Is)?Success\s*\(\s*true\s*\)/,
478
+ name: "Success state set directly",
479
+ check: (content, match) => {
480
+ const context = getCodeContext(content, content.slice(0, match.index).split("\n").length, 10, 0);
481
+ return !/await\s|fetch\(|\.mutate\(|try\s*\{/.test(context);
482
+ }
483
+ },
484
+ // Alert success without operation
485
+ {
486
+ pattern: /alert\s*\(\s*["'`][^"'`]*success[^"'`]*["'`]\s*\)/i,
487
+ name: "Alert success message",
488
+ check: (content, match) => {
489
+ const context = getCodeContext(content, content.slice(0, match.index).split("\n").length, 10, 0);
490
+ return !/await\s|fetch\(|\.mutate\(/.test(context);
491
+ }
492
+ },
493
+ // onClick handlers that just show success
494
+ {
495
+ pattern: /onClick\s*=\s*\{\s*\(\s*\)\s*=>\s*\{?\s*(toast|alert|setSuccess|setIs)/,
496
+ name: "Click handler shows success immediately",
497
+ check: () => true, // Always suspicious
498
+ },
499
+ ];
500
+
501
+ for (const file of codeFiles) {
502
+ if (isTestFile(file)) continue;
503
+
504
+ const content = readFileContent(file);
505
+ if (!content) continue;
506
+
507
+ const relPath = path.relative(repoRoot, file).replace(/\\/g, "/");
508
+
509
+ for (const { pattern, name, check } of fakeSuccessPatterns) {
510
+ const regex = new RegExp(pattern, "gm");
511
+ let match;
512
+
513
+ while ((match = regex.exec(content)) !== null) {
514
+ if (check && !check(content, match)) continue;
515
+
516
+ const line = content.slice(0, match.index).split("\n").length;
517
+
518
+ findings.push({
519
+ id: stableId("FAKE_SUCCESS", `${relPath}:${line}:${name}`),
520
+ type: "FAKE_SUCCESS",
521
+ severity: isInCriticalPath(relPath) ? "critical" : "medium",
522
+ confidence: 75,
523
+ blastRadius: BLAST_RADIUS.SINGLE_FEATURE,
524
+ title: `Fake success: ${name}`,
525
+ description: "UI shows success message without performing actual operation",
526
+ file: relPath,
527
+ line,
528
+ snippet: getSnippet(content, line),
529
+ whyConvincing: "Success feedback looks like normal UX pattern",
530
+ whyWrong: "No API call, mutation, or database operation - data not saved",
531
+ howToProve: "Click the button, refresh page - changes are lost",
532
+ howToFix: "Add actual API call/mutation, show success only after 200 response",
533
+ evidence: [{ file: relPath, line, reason: name }],
534
+ inCriticalPath: isInCriticalPath(relPath),
535
+ attackVector: "User thinks action succeeded but nothing was saved",
536
+ });
537
+ }
538
+ }
539
+ }
540
+
541
+ return findings;
542
+ }
543
+
544
+ /**
545
+ * ATTACK 4: Auth Drift - Auth patterns that look secure but aren't
546
+ */
547
+ function detectAuthDrift(repoRoot, mode = "fast") {
548
+ const findings = [];
549
+
550
+ // Find middleware and auth files
551
+ const authFiles = fg.sync([
552
+ "**/middleware.{ts,tsx,js,jsx}",
553
+ "**/auth/**/*.{ts,tsx,js,jsx}",
554
+ "**/api/**/*.{ts,tsx,js,jsx}",
555
+ ], { cwd: repoRoot, absolute: true, ignore: STANDARD_IGNORE });
556
+
557
+ // Check for auth patterns
558
+ for (const file of authFiles) {
559
+ if (isTestFile(file)) continue;
560
+
561
+ const content = readFileContent(file);
562
+ if (!content) continue;
563
+
564
+ const relPath = path.relative(repoRoot, file).replace(/\\/g, "/");
565
+
566
+ // Pattern 1: Auth check with early return bypass
567
+ const earlyReturnPattern = /if\s*\(\s*!session\s*\)\s*\{[^}]*return\s+.*\}/g;
568
+ let match;
569
+ while ((match = earlyReturnPattern.exec(content)) !== null) {
570
+ // Check if there's a bypass condition before
571
+ const before = content.slice(Math.max(0, match.index - 200), match.index);
572
+ if (/if\s*\([^)]*dev|if\s*\([^)]*test|if\s*\([^)]*demo/i.test(before)) {
573
+ const line = content.slice(0, match.index).split("\n").length;
574
+ findings.push({
575
+ id: stableId("AUTH_DRIFT", `${relPath}:${line}:bypass`),
576
+ type: "AUTH_DRIFT",
577
+ severity: "critical",
578
+ confidence: 90,
579
+ blastRadius: BLAST_RADIUS.ENTIRE_APP,
580
+ title: "Auth bypass in dev/test mode",
581
+ description: "Authentication can be bypassed with dev/test flag",
582
+ file: relPath,
583
+ line,
584
+ snippet: getSnippet(content, line),
585
+ whyConvincing: "Auth check exists and looks proper",
586
+ whyWrong: "Dev/test bypass may be active in production",
587
+ howToProve: "Set NODE_ENV=development and access protected route",
588
+ howToFix: "Remove dev bypasses or ensure env check is strict",
589
+ evidence: [{ file: relPath, line, reason: "Dev bypass before auth check" }],
590
+ inCriticalPath: true,
591
+ attackVector: "Attacker sets dev flag to bypass authentication",
592
+ });
593
+ }
594
+ }
595
+
596
+ // Pattern 2: API routes without auth
597
+ if (relPath.includes("/api/") && !relPath.includes("/auth/") && !relPath.includes("/public/")) {
598
+ const hasAuthCheck = /getServerSession|auth\(\)|getSession|withAuth|requireAuth|verifyToken|jwtVerify/.test(content);
599
+ const hasPublicComment = /\/\/\s*public|\/\*\s*public|\*\s*@public/i.test(content);
600
+
601
+ if (!hasAuthCheck && !hasPublicComment) {
602
+ // Check if it's handling sensitive operations
603
+ const hasSensitiveOp = /create|update|delete|post|put|patch|prisma\.|db\./i.test(content);
604
+
605
+ if (hasSensitiveOp) {
606
+ findings.push({
607
+ id: stableId("AUTH_DRIFT", `${relPath}:noauth`),
608
+ type: "AUTH_DRIFT",
609
+ severity: "critical",
610
+ confidence: 80,
611
+ blastRadius: BLAST_RADIUS.MODULE,
612
+ title: "API route without authentication",
613
+ description: "API route performs sensitive operations without auth check",
614
+ file: relPath,
615
+ line: 1,
616
+ snippet: getSnippet(content, 1),
617
+ whyConvincing: "Route exists with proper API structure",
618
+ whyWrong: "Anyone can call this endpoint without being logged in",
619
+ howToProve: "Call endpoint with curl (no auth header) - should work",
620
+ howToFix: "Add getServerSession() or auth middleware",
621
+ evidence: [{ file: relPath, reason: "No auth check found in API route" }],
622
+ inCriticalPath: true,
623
+ attackVector: "Unauthorized users can access/modify data",
624
+ });
625
+ }
626
+ }
627
+ }
628
+
629
+ // Pattern 3: Role check that can be spoofed
630
+ const roleCheckPattern = /user\.role\s*===?\s*["'`]admin["'`]|isAdmin\s*:\s*true/g;
631
+ while ((match = roleCheckPattern.exec(content)) !== null) {
632
+ // Check if role comes from client input
633
+ const context = getCodeContext(content, content.slice(0, match.index).split("\n").length, 20, 5);
634
+ if (/req\.body\..*role|req\.query\..*role|params\..*role/.test(context)) {
635
+ const line = content.slice(0, match.index).split("\n").length;
636
+ findings.push({
637
+ id: stableId("AUTH_DRIFT", `${relPath}:${line}:role-spoof`),
638
+ type: "AUTH_DRIFT",
639
+ severity: "critical",
640
+ confidence: 85,
641
+ blastRadius: BLAST_RADIUS.ENTIRE_APP,
642
+ title: "Role check uses client-provided value",
643
+ description: "Admin/role check may use value from request body",
644
+ file: relPath,
645
+ line,
646
+ snippet: getSnippet(content, line),
647
+ whyConvincing: "Role check looks like proper authorization",
648
+ whyWrong: "Role value comes from client - can be forged",
649
+ howToProve: "Send request with role=admin in body",
650
+ howToFix: "Get role from database/session, never from client input",
651
+ evidence: [{ file: relPath, line, reason: "Role from client input" }],
652
+ inCriticalPath: true,
653
+ attackVector: "Any user can elevate to admin by setting role in request",
654
+ });
655
+ }
656
+ }
657
+ }
658
+
659
+ return findings;
660
+ }
661
+
662
+ /**
663
+ * ATTACK 5: Mock Landmine - Mock/TODO code in production paths
664
+ */
665
+ function detectMockLandmines(repoRoot, mode = "fast") {
666
+ const findings = [];
667
+
668
+ const codeFiles = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
669
+ cwd: repoRoot,
670
+ absolute: true,
671
+ ignore: STANDARD_IGNORE,
672
+ });
673
+
674
+ const landminePatterns = [
675
+ // Mock data patterns
676
+ { pattern: /return\s*(NextResponse\.)?json\s*\(\s*\{[^}]*mock/i, name: "Mock data in API response", severity: "high" },
677
+ { pattern: /return\s*(NextResponse\.)?json\s*\(\s*\{[^}]*fake/i, name: "Fake data in API response", severity: "high" },
678
+ { pattern: /return\s*(NextResponse\.)?json\s*\(\s*\[\s*\]?\s*\)/, name: "Empty array/object response", severity: "medium" },
679
+ { pattern: /Promise\.resolve\s*\(\s*\{[^}]*\}?\s*\)/, name: "Fake async - immediately resolved", severity: "medium" },
680
+
681
+ // Demo credentials
682
+ { pattern: /["'`]user@example\.com["'`]/i, name: "Demo email hardcoded", severity: "high" },
683
+ { pattern: /["'`]password123["'`]/i, name: "Demo password hardcoded", severity: "critical" },
684
+ { pattern: /["'`]sk[-_]test[-_]/i, name: "Test API key in code", severity: "critical" },
685
+ { pattern: /DEMO_API_KEY|YOUR_API_KEY/i, name: "Placeholder API key", severity: "high" },
686
+
687
+ // TODO landmines
688
+ { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|finish|complete)/i, name: "TODO - not implemented", severity: "medium" },
689
+ { pattern: /\/\/\s*FIXME/i, name: "FIXME - known bug", severity: "high" },
690
+ { pattern: /\/\/\s*HACK/i, name: "HACK - temporary workaround", severity: "medium" },
691
+ { pattern: /throw\s+new\s+Error\s*\(\s*["'`]Not implemented/i, name: "Throws 'Not implemented'", severity: "high" },
692
+
693
+ // Bypass patterns
694
+ { pattern: /bypassAuth|skipAuth|noAuth/i, name: "Auth bypass flag", severity: "critical" },
695
+ { pattern: /demoMode|isDemo|testMode/i, name: "Demo/test mode flag", severity: "high" },
696
+ { pattern: /if\s*\(\s*true\s*\)\s*\{/, name: "Always-true condition", severity: "medium" },
697
+ { pattern: /if\s*\(\s*false\s*\)\s*\{/, name: "Dead code (always false)", severity: "low" },
698
+ ];
699
+
700
+ for (const file of codeFiles) {
701
+ if (isTestFile(file)) continue;
702
+
703
+ const content = readFileContent(file);
704
+ if (!content) continue;
705
+
706
+ const relPath = path.relative(repoRoot, file).replace(/\\/g, "/");
707
+
708
+ for (const { pattern, name, severity } of landminePatterns) {
709
+ const regex = new RegExp(pattern, "gm");
710
+ let match;
711
+
712
+ while ((match = regex.exec(content)) !== null) {
713
+ const line = content.slice(0, match.index).split("\n").length;
714
+ const actualSeverity = isInCriticalPath(relPath) && severity !== "critical"
715
+ ? (severity === "low" ? "medium" : "high")
716
+ : severity;
717
+
718
+ findings.push({
719
+ id: stableId("MOCK_LANDMINE", `${relPath}:${line}:${name}`),
720
+ type: "MOCK_LANDMINE",
721
+ severity: actualSeverity,
722
+ confidence: 90,
723
+ blastRadius: isInCriticalPath(relPath) ? BLAST_RADIUS.MODULE : BLAST_RADIUS.SINGLE_FEATURE,
724
+ title: `Mock landmine: ${name}`,
725
+ description: `Production code contains ${name.toLowerCase()}`,
726
+ file: relPath,
727
+ line,
728
+ snippet: getSnippet(content, line),
729
+ whyConvincing: "Code exists and may appear to work in dev",
730
+ whyWrong: "Will fail, return wrong data, or bypass security in production",
731
+ howToProve: name.includes("TODO")
732
+ ? "Use the feature - it won't work"
733
+ : "Check if mock/fake data appears in production",
734
+ howToFix: name.includes("TODO")
735
+ ? "Implement the feature or remove the code"
736
+ : "Replace mock data with real implementation",
737
+ evidence: [{ file: relPath, line, reason: name }],
738
+ inCriticalPath: isInCriticalPath(relPath),
739
+ attackVector: name.includes("bypass") || name.includes("password")
740
+ ? "Security bypass in production"
741
+ : "Feature doesn't work as expected",
742
+ });
743
+ }
744
+ }
745
+ }
746
+
747
+ return findings;
748
+ }
749
+
750
+ /**
751
+ * ATTACK 6: Silent Fail - Errors swallowed without feedback
752
+ */
753
+ function detectSilentFails(repoRoot, mode = "fast") {
754
+ const findings = [];
755
+
756
+ const codeFiles = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
757
+ cwd: repoRoot,
758
+ absolute: true,
759
+ ignore: STANDARD_IGNORE,
760
+ });
761
+
762
+ for (const file of codeFiles) {
763
+ if (isTestFile(file)) continue;
764
+
765
+ const content = readFileContent(file);
766
+ if (!content) continue;
767
+
768
+ // Fast path: skip files without try-catch
769
+ if (!/\bcatch\s*\(/.test(content)) continue;
770
+
771
+ const relPath = path.relative(repoRoot, file).replace(/\\/g, "/");
772
+
773
+ // Find catch blocks
774
+ const catchPattern = /catch\s*\(\s*(\w+)?\s*\)\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g;
775
+ let match;
776
+
777
+ while ((match = catchPattern.exec(content)) !== null) {
778
+ const errorVar = match[1] || "error";
779
+ const catchBody = match[2];
780
+ const line = content.slice(0, match.index).split("\n").length;
781
+
782
+ // Check if catch body is effectively empty
783
+ const hasLogging = /console\.(error|warn|log)|logger\.(error|warn)|log\.(error|warn)/i.test(catchBody);
784
+ const hasRethrow = /throw\s/.test(catchBody);
785
+ const hasUserFeedback = /toast|notification|alert|setError|showError/i.test(catchBody);
786
+ const hasReturn = /return\s/.test(catchBody);
787
+ const hasComment = /\/\/\s*intentional|\/\/\s*ignore|\/\/\s*swallow/i.test(catchBody);
788
+
789
+ // Empty catch (only whitespace/comments)
790
+ if (catchBody.replace(/\/\/.*|\/\*[\s\S]*?\*\/|\s/g, "").length < 3) {
791
+ findings.push({
792
+ id: stableId("SILENT_FAIL", `${relPath}:${line}:empty`),
793
+ type: "SILENT_FAIL",
794
+ severity: isInCriticalPath(relPath) ? "critical" : "high",
795
+ confidence: 95,
796
+ blastRadius: BLAST_RADIUS.SINGLE_FEATURE,
797
+ title: "Empty catch block",
798
+ description: "Catch block is empty - errors are completely swallowed",
799
+ file: relPath,
800
+ line,
801
+ snippet: getSnippet(content, line),
802
+ whyConvincing: "Error handling structure exists",
803
+ whyWrong: "Errors disappear - no logging, no feedback, no debugging possible",
804
+ howToProve: "Trigger an error condition - nothing happens",
805
+ howToFix: `Add console.error(${errorVar}) or show user-friendly error`,
806
+ evidence: [{ file: relPath, line, reason: "Empty catch block" }],
807
+ inCriticalPath: isInCriticalPath(relPath),
808
+ attackVector: "Failures are invisible - users see broken state with no explanation",
809
+ });
810
+ }
811
+ // Silent catch (no user feedback)
812
+ else if (!hasLogging && !hasRethrow && !hasUserFeedback && !hasComment) {
813
+ findings.push({
814
+ id: stableId("SILENT_FAIL", `${relPath}:${line}:silent`),
815
+ type: "SILENT_FAIL",
816
+ severity: isInCriticalPath(relPath) ? "high" : "medium",
817
+ confidence: 75,
818
+ blastRadius: BLAST_RADIUS.SINGLE_FEATURE,
819
+ title: "Silent catch - no error feedback",
820
+ description: "Catch handles error but doesn't log or show user feedback",
821
+ file: relPath,
822
+ line,
823
+ snippet: getSnippet(content, line),
824
+ whyConvincing: "Catch block exists with some code",
825
+ whyWrong: "Users get no feedback when something fails",
826
+ howToProve: "Trigger error - no message appears",
827
+ howToFix: "Add toast.error() or console.error() for debugging",
828
+ evidence: [{ file: relPath, line, reason: "No user feedback in catch" }],
829
+ inCriticalPath: isInCriticalPath(relPath),
830
+ attackVector: "User confusion - action failed but no error shown",
831
+ });
832
+ }
833
+ }
834
+ }
835
+
836
+ return findings;
837
+ }
838
+
839
+ /**
840
+ * ATTACK 7: Optimistic Bomb - Optimistic UI without rollback
841
+ */
842
+ function detectOptimisticBombs(repoRoot, mode = "fast") {
843
+ const findings = [];
844
+
845
+ const codeFiles = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
846
+ cwd: repoRoot,
847
+ absolute: true,
848
+ ignore: STANDARD_IGNORE,
849
+ });
850
+
851
+ for (const file of codeFiles) {
852
+ if (isTestFile(file)) continue;
853
+
854
+ const content = readFileContent(file);
855
+ if (!content) continue;
856
+
857
+ // Fast path: skip files without optimistic patterns
858
+ if (!/setState|set[A-Z]\w*|useOptimistic|optimistic/i.test(content)) continue;
859
+ if (!/fetch|axios|mutate|useMutation/i.test(content)) continue;
860
+
861
+ const relPath = path.relative(repoRoot, file).replace(/\\/g, "/");
862
+
863
+ // Pattern: state update followed by fetch without try-catch rollback
864
+ const stateUpdatePattern = /(set[A-Z]\w*|setState)\s*\([^)]+\)/g;
865
+ let match;
866
+
867
+ while ((match = stateUpdatePattern.exec(content)) !== null) {
868
+ const line = content.slice(0, match.index).split("\n").length;
869
+ const afterUpdate = content.slice(match.index, match.index + 500);
870
+
871
+ // Check if there's a fetch/mutation call after
872
+ if (!/fetch\(|axios\.|\.mutate\(|useMutation/.test(afterUpdate)) continue;
873
+
874
+ // Check for rollback in catch
875
+ const hasCatch = /catch\s*\([^)]*\)\s*\{/.test(afterUpdate);
876
+ let hasRollback = false;
877
+
878
+ if (hasCatch) {
879
+ const catchMatch = afterUpdate.match(/catch\s*\([^)]*\)\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/);
880
+ if (catchMatch) {
881
+ hasRollback = /set[A-Z]\w*|setState/.test(catchMatch[1]);
882
+ }
883
+ }
884
+
885
+ if (!hasRollback) {
886
+ findings.push({
887
+ id: stableId("OPTIMISTIC_BOMB", `${relPath}:${line}`),
888
+ type: "OPTIMISTIC_BOMB",
889
+ severity: isInCriticalPath(relPath) ? "high" : "medium",
890
+ confidence: 70,
891
+ blastRadius: BLAST_RADIUS.SINGLE_FEATURE,
892
+ title: "Optimistic update without rollback",
893
+ description: "State updated before API call, but no rollback on failure",
894
+ file: relPath,
895
+ line,
896
+ snippet: getSnippet(content, line),
897
+ whyConvincing: "Optimistic UI pattern looks correct",
898
+ whyWrong: "If API fails, UI shows success but data wasn't saved",
899
+ howToProve: "Update item, kill network, refresh - changes gone",
900
+ howToFix: "Store previous state, restore in catch block on error",
901
+ evidence: [{ file: relPath, line, reason: "No rollback in catch" }],
902
+ inCriticalPath: isInCriticalPath(relPath),
903
+ attackVector: "Data loss - users think changes saved but they weren't",
904
+ });
905
+ }
906
+ }
907
+ }
908
+
909
+ return findings;
910
+ }
911
+
912
+ /**
913
+ * ATTACK 8: Paid Theater - Paid features that aren't enforced
914
+ */
915
+ function detectPaidTheater(repoRoot, mode = "fast") {
916
+ const findings = [];
917
+
918
+ // Find pricing/tier related files
919
+ const pricingFiles = fg.sync([
920
+ "**/pricing/**/*.{ts,tsx,js,jsx}",
921
+ "**/billing/**/*.{ts,tsx,js,jsx}",
922
+ "**/subscription/**/*.{ts,tsx,js,jsx}",
923
+ "**/plans/**/*.{ts,tsx,js,jsx}",
924
+ "**/*tier*.{ts,tsx,js,jsx}",
925
+ "**/*plan*.{ts,tsx,js,jsx}",
926
+ ], { cwd: repoRoot, absolute: true, ignore: STANDARD_IGNORE });
927
+
928
+ // Track premium features mentioned
929
+ const premiumFeatures = new Set();
930
+ const premiumPatterns = [
931
+ /["'`](pro|premium|enterprise|paid|unlimited)["'`]/gi,
932
+ /tier\s*[=!]==?\s*["'`](pro|premium|enterprise|paid)/gi,
933
+ /isPro|isPremium|isEnterprise|isPaid/gi,
934
+ ];
935
+
936
+ for (const file of pricingFiles) {
937
+ const content = readFileContent(file);
938
+ if (!content) continue;
939
+
940
+ for (const pattern of premiumPatterns) {
941
+ let match;
942
+ while ((match = pattern.exec(content)) !== null) {
943
+ premiumFeatures.add(match[0].toLowerCase());
944
+ }
945
+ }
946
+ }
947
+
948
+ if (premiumFeatures.size === 0) return findings;
949
+
950
+ // Check API routes for enforcement
951
+ const apiFiles = fg.sync([
952
+ "**/app/api/**/route.{ts,tsx,js,jsx}",
953
+ "**/pages/api/**/*.{ts,tsx,js,jsx}",
954
+ ], { cwd: repoRoot, absolute: true, ignore: STANDARD_IGNORE });
955
+
956
+ for (const file of apiFiles) {
957
+ if (isTestFile(file)) continue;
958
+
959
+ const content = readFileContent(file);
960
+ if (!content) continue;
961
+
962
+ const relPath = path.relative(repoRoot, file).replace(/\\/g, "/");
963
+
964
+ // Check if route mentions premium but doesn't enforce
965
+ const mentionsPremium = /pro|premium|enterprise|paid|tier/i.test(content);
966
+ const hasEnforcement = /checkSubscription|requireTier|isPro|isPremium|subscription\.status|tier\s*[=!]==?/.test(content);
967
+ const returnsUnauthorized = /401|403|Unauthorized|Forbidden|upgrade/i.test(content);
968
+
969
+ if (mentionsPremium && !hasEnforcement) {
970
+ findings.push({
971
+ id: stableId("PAID_THEATER", `${relPath}:no-check`),
972
+ type: "PAID_THEATER",
973
+ severity: "high",
974
+ confidence: 70,
975
+ blastRadius: BLAST_RADIUS.MODULE,
976
+ title: "Premium feature without tier check",
977
+ description: "API route mentions premium/pro but doesn't verify subscription",
978
+ file: relPath,
979
+ line: 1,
980
+ snippet: getSnippet(content, 1),
981
+ whyConvincing: "Pricing UI shows feature as premium-only",
982
+ whyWrong: "Anyone can access the API endpoint without paying",
983
+ howToProve: "Call endpoint with free account - it works",
984
+ howToFix: "Add tier check: if (!user.isPro) return 403",
985
+ evidence: [{ file: relPath, reason: "Mentions premium but no enforcement" }],
986
+ inCriticalPath: true,
987
+ attackVector: "Revenue loss - free users access paid features",
988
+ });
989
+ }
990
+
991
+ if (hasEnforcement && !returnsUnauthorized) {
992
+ // Has check but might not block
993
+ const hasBypass = /return\s+true|return\s+next|continue/.test(content.split(/if.*tier|if.*isPro/i)[1] || "");
994
+ if (hasBypass) {
995
+ findings.push({
996
+ id: stableId("PAID_THEATER", `${relPath}:weak-check`),
997
+ type: "PAID_THEATER",
998
+ severity: "medium",
999
+ confidence: 60,
1000
+ blastRadius: BLAST_RADIUS.SINGLE_FEATURE,
1001
+ title: "Weak tier enforcement",
1002
+ description: "Tier check exists but may not properly block access",
1003
+ file: relPath,
1004
+ line: 1,
1005
+ snippet: getSnippet(content, 1),
1006
+ whyConvincing: "Tier check code exists",
1007
+ whyWrong: "Check may be bypassed or not return 403",
1008
+ howToProve: "Test with wrong tier - may still work",
1009
+ howToFix: "Ensure check returns 403 for unauthorized tiers",
1010
+ evidence: [{ file: relPath, reason: "Tier check may not block" }],
1011
+ inCriticalPath: true,
1012
+ attackVector: "Free users might still access paid features",
1013
+ });
1014
+ }
1015
+ }
1016
+ }
1017
+
1018
+ return findings;
1019
+ }
1020
+
1021
+ // ═══════════════════════════════════════════════════════════════════════════════
1022
+ // DEEP MODE DETECTORS (Cross-file analysis)
1023
+ // ═══════════════════════════════════════════════════════════════════════════════
1024
+
1025
+ /**
1026
+ * Deep analysis: Cross-file route integrity check
1027
+ */
1028
+ function deepRouteAnalysis(repoRoot) {
1029
+ // More expensive: actually trace imports and exports
1030
+ // TODO: Implement full cross-file analysis
1031
+ return [];
1032
+ }
1033
+
1034
+ /**
1035
+ * Deep analysis: Auth flow tracing
1036
+ */
1037
+ function deepAuthAnalysis(repoRoot) {
1038
+ // Trace auth from middleware through to API routes
1039
+ // TODO: Implement full auth flow analysis
1040
+ return [];
1041
+ }
1042
+
1043
+ // ═══════════════════════════════════════════════════════════════════════════════
1044
+ // MAIN DETECTOR CLASS
1045
+ // ═══════════════════════════════════════════════════════════════════════════════
1046
+
1047
+ class AttackDetector {
1048
+ constructor(repoRoot, options = {}) {
1049
+ this.repoRoot = repoRoot;
1050
+ this.mode = options.mode || "fast";
1051
+ this.exclude = options.exclude || [];
1052
+ this.findings = [];
1053
+ }
1054
+
1055
+ async scan() {
1056
+ const startTime = Date.now();
1057
+ this.findings = [];
1058
+
1059
+ // Fast mode detectors (cheap, single-file analysis)
1060
+ const fastDetectors = [
1061
+ detectDeadRoutes,
1062
+ detectGhostEnv,
1063
+ detectFakeSuccess,
1064
+ detectAuthDrift,
1065
+ detectMockLandmines,
1066
+ detectSilentFails,
1067
+ detectOptimisticBombs,
1068
+ detectPaidTheater,
1069
+ ];
1070
+
1071
+ for (const detector of fastDetectors) {
1072
+ try {
1073
+ const results = detector(this.repoRoot, this.mode);
1074
+ this.findings.push(...results);
1075
+ } catch (err) {
1076
+ // Log but continue
1077
+ if (process.env.DEBUG) {
1078
+ console.error(`Detector error: ${err.message}`);
1079
+ }
1080
+ }
1081
+ }
1082
+
1083
+ // Deep mode detectors (expensive, cross-file analysis)
1084
+ if (this.mode === "deep") {
1085
+ const deepDetectors = [
1086
+ deepRouteAnalysis,
1087
+ deepAuthAnalysis,
1088
+ ];
1089
+
1090
+ for (const detector of deepDetectors) {
1091
+ try {
1092
+ const results = detector(this.repoRoot);
1093
+ this.findings.push(...results);
1094
+ } catch (err) {
1095
+ if (process.env.DEBUG) {
1096
+ console.error(`Deep detector error: ${err.message}`);
1097
+ }
1098
+ }
1099
+ }
1100
+ }
1101
+
1102
+ // Sort by severity and confidence
1103
+ this.findings.sort((a, b) => {
1104
+ const sevOrder = { critical: 0, high: 1, medium: 2, low: 3 };
1105
+ if (sevOrder[a.severity] !== sevOrder[b.severity]) {
1106
+ return sevOrder[a.severity] - sevOrder[b.severity];
1107
+ }
1108
+ return b.confidence - a.confidence;
1109
+ });
1110
+
1111
+ // Deduplicate by ID
1112
+ const seen = new Set();
1113
+ this.findings = this.findings.filter(f => {
1114
+ if (seen.has(f.id)) return false;
1115
+ seen.add(f.id);
1116
+ return true;
1117
+ });
1118
+
1119
+ return this.generateReport(Date.now() - startTime);
1120
+ }
1121
+
1122
+ generateReport(elapsedMs) {
1123
+ const summary = {
1124
+ total: this.findings.length,
1125
+ critical: this.findings.filter(f => f.severity === "critical").length,
1126
+ high: this.findings.filter(f => f.severity === "high").length,
1127
+ medium: this.findings.filter(f => f.severity === "medium").length,
1128
+ low: this.findings.filter(f => f.severity === "low").length,
1129
+ byType: {},
1130
+ inCriticalPaths: this.findings.filter(f => f.inCriticalPath).length,
1131
+ };
1132
+
1133
+ for (const f of this.findings) {
1134
+ summary.byType[f.type] = (summary.byType[f.type] || 0) + 1;
1135
+ }
1136
+
1137
+ // Calculate attack score (0-100)
1138
+ let attackScore = 0;
1139
+ for (const f of this.findings) {
1140
+ const weight = SEVERITY_WEIGHTS[f.severity] || 5;
1141
+ const confidenceMultiplier = f.confidence / 100;
1142
+ const criticalPathMultiplier = f.inCriticalPath ? 1.5 : 1;
1143
+ attackScore += weight * confidenceMultiplier * criticalPathMultiplier;
1144
+ }
1145
+ attackScore = Math.min(100, Math.round(attackScore));
1146
+
1147
+ // Generate deterministic manifest
1148
+ const manifest = {
1149
+ hash: crypto.createHash("sha256")
1150
+ .update(this.findings.map(f => f.id).sort().join(":"))
1151
+ .digest("hex"),
1152
+ findingCount: this.findings.length,
1153
+ criticalCount: summary.critical,
1154
+ highCount: summary.high,
1155
+ attackScore,
1156
+ findingIds: this.findings.map(f => f.id).sort(),
1157
+ };
1158
+
1159
+ return {
1160
+ version: "2.0.0",
1161
+ timestamp: new Date().toISOString(),
1162
+ projectPath: this.repoRoot,
1163
+ mode: this.mode,
1164
+ elapsedMs,
1165
+ findings: this.findings,
1166
+ summary,
1167
+ attackScore,
1168
+ manifest,
1169
+ };
1170
+ }
1171
+ }
1172
+
1173
+ // ═══════════════════════════════════════════════════════════════════════════════
1174
+ // EXPORTS
1175
+ // ═══════════════════════════════════════════════════════════════════════════════
1176
+
1177
+ module.exports = {
1178
+ AttackDetector,
1179
+ // Individual detectors for testing
1180
+ detectDeadRoutes,
1181
+ detectGhostEnv,
1182
+ detectFakeSuccess,
1183
+ detectAuthDrift,
1184
+ detectMockLandmines,
1185
+ detectSilentFails,
1186
+ detectOptimisticBombs,
1187
+ detectPaidTheater,
1188
+ // Constants
1189
+ SEVERITY_WEIGHTS,
1190
+ BLAST_RADIUS,
1191
+ CRITICAL_PATHS,
1192
+ };