@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.
- package/README.md +135 -63
- package/bin/_deprecations.js +447 -19
- package/bin/_router.js +1 -1
- package/bin/registry.js +347 -280
- package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
- package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
- package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
- package/bin/runners/lib/agent-firewall/index.js +200 -0
- package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
- package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +622 -0
- package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
- package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
- package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
- package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
- package/bin/runners/lib/agent-firewall/session/index.js +26 -0
- package/bin/runners/lib/artifact-envelope.js +540 -0
- package/bin/runners/lib/auth-shared.js +977 -0
- package/bin/runners/lib/checkpoint.js +941 -0
- package/bin/runners/lib/cleanup/engine.js +571 -0
- package/bin/runners/lib/cleanup/index.js +53 -0
- package/bin/runners/lib/cleanup/output.js +375 -0
- package/bin/runners/lib/cleanup/rules.js +1060 -0
- package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
- package/bin/runners/lib/doctor/failure-signatures.js +526 -0
- package/bin/runners/lib/doctor/fix-script.js +336 -0
- package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
- package/bin/runners/lib/doctor/modules/index.js +62 -3
- package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
- package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
- package/bin/runners/lib/doctor/safe-repair.js +384 -0
- package/bin/runners/lib/engines/attack-detector.js +1192 -0
- package/bin/runners/lib/entitlements-v2.js +2 -2
- package/bin/runners/lib/missions/briefing.js +427 -0
- package/bin/runners/lib/missions/checkpoint.js +753 -0
- package/bin/runners/lib/missions/hardening.js +851 -0
- package/bin/runners/lib/missions/plan.js +421 -32
- package/bin/runners/lib/missions/safety-gates.js +645 -0
- package/bin/runners/lib/missions/schema.js +478 -0
- package/bin/runners/lib/packs/bundle.js +675 -0
- package/bin/runners/lib/packs/evidence-pack.js +671 -0
- package/bin/runners/lib/packs/pack-factory.js +837 -0
- package/bin/runners/lib/packs/permissions-pack.js +686 -0
- package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
- package/bin/runners/lib/safelist/index.js +96 -0
- package/bin/runners/lib/safelist/integration.js +334 -0
- package/bin/runners/lib/safelist/matcher.js +696 -0
- package/bin/runners/lib/safelist/schema.js +948 -0
- package/bin/runners/lib/safelist/store.js +438 -0
- package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
- package/bin/runners/lib/ship-gate.js +832 -0
- package/bin/runners/lib/ship-manifest.js +1153 -0
- package/bin/runners/lib/ship-output.js +1 -1
- package/bin/runners/lib/unified-cli-output.js +710 -383
- package/bin/runners/lib/upsell.js +3 -3
- package/bin/runners/lib/why-tree.js +650 -0
- package/bin/runners/runAllowlist.js +33 -4
- package/bin/runners/runApprove.js +240 -1122
- package/bin/runners/runAudit.js +692 -0
- package/bin/runners/runAuth.js +325 -29
- package/bin/runners/runCheckpoint.js +442 -494
- package/bin/runners/runCleanup.js +343 -0
- package/bin/runners/runDoctor.js +269 -19
- package/bin/runners/runFix.js +411 -32
- package/bin/runners/runForge.js +411 -0
- package/bin/runners/runIntent.js +906 -0
- package/bin/runners/runKickoff.js +878 -0
- package/bin/runners/runLaunch.js +2000 -0
- package/bin/runners/runLink.js +785 -0
- package/bin/runners/runMcp.js +1741 -837
- package/bin/runners/runPacks.js +2089 -0
- package/bin/runners/runPolish.js +41 -0
- package/bin/runners/runSafelist.js +1190 -0
- package/bin/runners/runScan.js +21 -9
- package/bin/runners/runShield.js +1282 -0
- package/bin/runners/runShip.js +395 -16
- package/bin/vibecheck.js +34 -6
- package/mcp-server/README.md +117 -158
- package/mcp-server/handlers/tool-handler.ts +3 -3
- package/mcp-server/index.js +16 -0
- package/mcp-server/intent-firewall-interceptor.js +529 -0
- package/mcp-server/manifest.json +473 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/registry/tool-registry.js +315 -523
- package/mcp-server/registry/tools.json +442 -428
- package/mcp-server/tier-auth.js +68 -11
- package/mcp-server/tools-v3.js +70 -16
- package/package.json +1 -1
- 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
|
+
};
|