@vibecheckai/cli 3.0.2 → 3.0.3
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/package.json +9 -1
- package/bin/cli-hygiene.js +0 -241
- package/bin/guardrail.js +0 -834
- package/bin/runners/cli-utils.js +0 -1070
- package/bin/runners/context/ai-task-decomposer.js +0 -337
- package/bin/runners/context/analyzer.js +0 -462
- package/bin/runners/context/api-contracts.js +0 -427
- package/bin/runners/context/context-diff.js +0 -342
- package/bin/runners/context/context-pruner.js +0 -291
- package/bin/runners/context/dependency-graph.js +0 -414
- package/bin/runners/context/generators/claude.js +0 -107
- package/bin/runners/context/generators/codex.js +0 -108
- package/bin/runners/context/generators/copilot.js +0 -119
- package/bin/runners/context/generators/cursor.js +0 -514
- package/bin/runners/context/generators/mcp.js +0 -151
- package/bin/runners/context/generators/windsurf.js +0 -180
- package/bin/runners/context/git-context.js +0 -302
- package/bin/runners/context/index.js +0 -1042
- package/bin/runners/context/insights.js +0 -173
- package/bin/runners/context/mcp-server/generate-rules.js +0 -337
- package/bin/runners/context/mcp-server/index.js +0 -1176
- package/bin/runners/context/mcp-server/package.json +0 -24
- package/bin/runners/context/memory.js +0 -200
- package/bin/runners/context/monorepo.js +0 -215
- package/bin/runners/context/multi-repo-federation.js +0 -404
- package/bin/runners/context/patterns.js +0 -253
- package/bin/runners/context/proof-context.js +0 -972
- package/bin/runners/context/security-scanner.js +0 -303
- package/bin/runners/context/semantic-search.js +0 -350
- package/bin/runners/context/shared.js +0 -264
- package/bin/runners/context/team-conventions.js +0 -310
- package/bin/runners/lib/ai-bridge.js +0 -416
- package/bin/runners/lib/analysis-core.js +0 -271
- package/bin/runners/lib/analyzers.js +0 -541
- package/bin/runners/lib/audit-bridge.js +0 -391
- package/bin/runners/lib/auth-truth.js +0 -193
- package/bin/runners/lib/auth.js +0 -215
- package/bin/runners/lib/backup.js +0 -62
- package/bin/runners/lib/billing.js +0 -107
- package/bin/runners/lib/claims.js +0 -118
- package/bin/runners/lib/cli-ui.js +0 -540
- package/bin/runners/lib/compliance-bridge-new.js +0 -0
- package/bin/runners/lib/compliance-bridge.js +0 -165
- package/bin/runners/lib/contracts/auth-contract.js +0 -194
- package/bin/runners/lib/contracts/env-contract.js +0 -178
- package/bin/runners/lib/contracts/external-contract.js +0 -198
- package/bin/runners/lib/contracts/guard.js +0 -168
- package/bin/runners/lib/contracts/index.js +0 -89
- package/bin/runners/lib/contracts/plan-validator.js +0 -311
- package/bin/runners/lib/contracts/route-contract.js +0 -192
- package/bin/runners/lib/detect.js +0 -89
- package/bin/runners/lib/doctor/autofix.js +0 -254
- package/bin/runners/lib/doctor/index.js +0 -37
- package/bin/runners/lib/doctor/modules/dependencies.js +0 -325
- package/bin/runners/lib/doctor/modules/index.js +0 -46
- package/bin/runners/lib/doctor/modules/network.js +0 -250
- package/bin/runners/lib/doctor/modules/project.js +0 -312
- package/bin/runners/lib/doctor/modules/runtime.js +0 -224
- package/bin/runners/lib/doctor/modules/security.js +0 -348
- package/bin/runners/lib/doctor/modules/system.js +0 -213
- package/bin/runners/lib/doctor/modules/vibecheck.js +0 -394
- package/bin/runners/lib/doctor/reporter.js +0 -262
- package/bin/runners/lib/doctor/service.js +0 -262
- package/bin/runners/lib/doctor/types.js +0 -113
- package/bin/runners/lib/doctor/ui.js +0 -263
- package/bin/runners/lib/doctor-enhanced.js +0 -233
- package/bin/runners/lib/doctor-v2.js +0 -608
- package/bin/runners/lib/enforcement.js +0 -72
|
@@ -1,541 +0,0 @@
|
|
|
1
|
-
// bin/runners/lib/analyzers.js
|
|
2
|
-
const fs = require("fs");
|
|
3
|
-
const path = require("path");
|
|
4
|
-
const fg = require("fast-glob");
|
|
5
|
-
const crypto = require("crypto");
|
|
6
|
-
const parser = require("@babel/parser");
|
|
7
|
-
const traverse = require("@babel/traverse").default;
|
|
8
|
-
const t = require("@babel/types");
|
|
9
|
-
const { routeMatches } = require("./claims");
|
|
10
|
-
const { matcherCoversPath } = require("./auth-truth");
|
|
11
|
-
|
|
12
|
-
function sha256(text) {
|
|
13
|
-
return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function parseFile(code) {
|
|
17
|
-
return parser.parse(code, { sourceType: "unambiguous", plugins: ["typescript", "jsx"] });
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function evidenceFromLoc(fileAbs, repoRoot, loc, reason) {
|
|
21
|
-
if (!loc) return null;
|
|
22
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
23
|
-
const lines = fs.readFileSync(fileAbs, "utf8").split(/\r?\n/);
|
|
24
|
-
const start = Math.max(1, loc.start?.line || 1);
|
|
25
|
-
const end = Math.max(start, loc.end?.line || start);
|
|
26
|
-
const snippet = lines.slice(start - 1, end).join("\n");
|
|
27
|
-
return { id: `ev_${crypto.randomBytes(4).toString("hex")}`, file: fileRel, lines: `${start}-${end}`, snippetHash: sha256(snippet), reason };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function findMissingRoutes(truthpack) {
|
|
31
|
-
const findings = [];
|
|
32
|
-
const server = truthpack.routes.server || [];
|
|
33
|
-
const refs = truthpack.routes.clientRefs || [];
|
|
34
|
-
const gaps = truthpack.routes.gaps || [];
|
|
35
|
-
|
|
36
|
-
// If we have route detection gaps, be less aggressive with BLOCKs
|
|
37
|
-
const hasGaps = gaps.length > 0;
|
|
38
|
-
|
|
39
|
-
// Build a set of known route path prefixes for smarter matching
|
|
40
|
-
const knownPrefixes = new Set();
|
|
41
|
-
for (const r of server) {
|
|
42
|
-
const parts = r.path.split('/').filter(Boolean);
|
|
43
|
-
if (parts.length >= 2) {
|
|
44
|
-
knownPrefixes.add('/' + parts[0] + '/' + parts[1]);
|
|
45
|
-
}
|
|
46
|
-
if (parts.length >= 1) {
|
|
47
|
-
knownPrefixes.add('/' + parts[0]);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
for (const ref of refs) {
|
|
52
|
-
const method = ref.method || "*";
|
|
53
|
-
const p = ref.path;
|
|
54
|
-
|
|
55
|
-
const ok = server.some(r => routeMatches(r, method, p) || routeMatches(r, "*", p));
|
|
56
|
-
if (ok) continue;
|
|
57
|
-
|
|
58
|
-
// Check if route shares a prefix with known routes (might be undetected sibling)
|
|
59
|
-
const refParts = p.split('/').filter(Boolean);
|
|
60
|
-
const refPrefix1 = refParts.length >= 1 ? '/' + refParts[0] : '/';
|
|
61
|
-
const refPrefix2 = refParts.length >= 2 ? '/' + refParts[0] + '/' + refParts[1] : refPrefix1;
|
|
62
|
-
const sharesPrefix = knownPrefixes.has(refPrefix1) || knownPrefixes.has(refPrefix2);
|
|
63
|
-
|
|
64
|
-
// Determine severity based on confidence and context
|
|
65
|
-
// In monorepos with complex routing (plugins, dynamic registration), static analysis has limits
|
|
66
|
-
// Default to WARN unless we're very confident the route is truly invented
|
|
67
|
-
let severity = "WARN";
|
|
68
|
-
|
|
69
|
-
// Only BLOCK if:
|
|
70
|
-
// 1. High confidence client ref
|
|
71
|
-
// 2. No detection gaps
|
|
72
|
-
// 3. Doesn't share prefix with any known route
|
|
73
|
-
// 4. Looks like an invented/hallucinated route (unusual patterns)
|
|
74
|
-
const looksInvented = /\/(fake|test|mock|dummy|example|foo|bar|baz|xxx|yyy|placeholder)/i.test(p);
|
|
75
|
-
if (ref.confidence === "high" && !hasGaps && !sharesPrefix && looksInvented) {
|
|
76
|
-
severity = "BLOCK";
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Always WARN for common internal/utility routes
|
|
80
|
-
const isInternalRoute = /^\/(health|metrics|ready|live|version|debug|internal|suggestions|security|analyze|websocket|dashboard)/.test(p);
|
|
81
|
-
if (isInternalRoute) {
|
|
82
|
-
severity = "WARN";
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
findings.push({
|
|
86
|
-
id: `F_MISSING_ROUTE_${String(findings.length + 1).padStart(3, "0")}`,
|
|
87
|
-
severity,
|
|
88
|
-
category: "MissingRoute",
|
|
89
|
-
title: `Client references route that does not exist: ${method} ${p}`,
|
|
90
|
-
why: severity === "BLOCK"
|
|
91
|
-
? "AI frequently invents endpoints. Shipping this = broken flows (404 / silent failure)."
|
|
92
|
-
: "Route reference found but server route not detected. May be a false positive if route is defined dynamically.",
|
|
93
|
-
confidence: ref.confidence || "low",
|
|
94
|
-
evidence: ref.evidence || [],
|
|
95
|
-
fixHints: [
|
|
96
|
-
"Update the client call to a real server route (see route map).",
|
|
97
|
-
"If the route exists but wasn't detected, it may use dynamic registration.",
|
|
98
|
-
"If truly missing, implement it in your API and re-run ship."
|
|
99
|
-
]
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// If route scan had gaps, add a WARN so users know why some routes may be unknown
|
|
104
|
-
if (hasGaps) {
|
|
105
|
-
findings.push({
|
|
106
|
-
id: `F_ROUTE_MAP_GAPS_001`,
|
|
107
|
-
severity: "WARN",
|
|
108
|
-
category: "RouteMapGaps",
|
|
109
|
-
title: `Route map incomplete (${gaps.length} unresolved sources)`,
|
|
110
|
-
why: "Some routes may not be detected due to dynamic registration or unresolved plugins.",
|
|
111
|
-
confidence: "low",
|
|
112
|
-
evidence: [],
|
|
113
|
-
fixHints: [
|
|
114
|
-
"Routes registered dynamically or via unresolved imports may not be detected.",
|
|
115
|
-
"Consider using explicit route registration for better static analysis.",
|
|
116
|
-
"Run with --verbose to see detection gaps."
|
|
117
|
-
]
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return findings;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ============================================================================
|
|
125
|
-
// ENV GAPS ANALYZER
|
|
126
|
-
// ============================================================================
|
|
127
|
-
|
|
128
|
-
function findEnvGaps(truthpack) {
|
|
129
|
-
const findings = [];
|
|
130
|
-
const used = truthpack?.env?.vars || [];
|
|
131
|
-
const declared = new Set(truthpack?.env?.declared || []);
|
|
132
|
-
const declaredSources = truthpack?.env?.declaredSources || [];
|
|
133
|
-
|
|
134
|
-
// 1) USED but not declared in templates/examples => WARN (or BLOCK if required)
|
|
135
|
-
for (const v of used) {
|
|
136
|
-
if (declared.has(v.name)) continue;
|
|
137
|
-
|
|
138
|
-
const sev = v.required ? "BLOCK" : "WARN";
|
|
139
|
-
findings.push({
|
|
140
|
-
id: `F_ENV_UNDECLARED_${v.name}`,
|
|
141
|
-
severity: sev,
|
|
142
|
-
category: "EnvContract",
|
|
143
|
-
title: `Env var used but not declared in env templates: ${v.name}`,
|
|
144
|
-
why: v.required
|
|
145
|
-
? "Required env var is used with no fallback. Vibecoders will ship a broken app if it's not documented."
|
|
146
|
-
: "Env var appears optional but should still be documented to prevent guesswork.",
|
|
147
|
-
confidence: "high",
|
|
148
|
-
evidence: v.references || [],
|
|
149
|
-
fixHints: [
|
|
150
|
-
`Add ${v.name}= to .env.example (or .env.template).`,
|
|
151
|
-
"If it's truly optional, ensure the code has an explicit fallback or guard."
|
|
152
|
-
]
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// 2) Declared but never used => WARN (hygiene)
|
|
157
|
-
const usedSet = new Set(used.map(v => v.name));
|
|
158
|
-
for (const name of declared) {
|
|
159
|
-
if (usedSet.has(name)) continue;
|
|
160
|
-
|
|
161
|
-
findings.push({
|
|
162
|
-
id: `F_ENV_UNUSED_${name}`,
|
|
163
|
-
severity: "WARN",
|
|
164
|
-
category: "EnvContract",
|
|
165
|
-
title: `Env var declared but never used: ${name}`,
|
|
166
|
-
why: "Dead config creates confusion and encourages hallucinated wiring.",
|
|
167
|
-
confidence: "med",
|
|
168
|
-
evidence: [],
|
|
169
|
-
fixHints: [
|
|
170
|
-
"Remove it from templates if it's obsolete, or wire it into code intentionally.",
|
|
171
|
-
"If used at runtime only (in infra), document that explicitly."
|
|
172
|
-
]
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// 3) If no template sources exist, warn loudly
|
|
177
|
-
if (!declaredSources.length && used.length) {
|
|
178
|
-
findings.push({
|
|
179
|
-
id: "F_ENV_NO_TEMPLATE",
|
|
180
|
-
severity: "WARN",
|
|
181
|
-
category: "EnvContract",
|
|
182
|
-
title: "No .env.example/.env.template found",
|
|
183
|
-
why: "Without an env contract file, AI and humans will guess env vars and ship broken setups.",
|
|
184
|
-
confidence: "high",
|
|
185
|
-
evidence: [],
|
|
186
|
-
fixHints: ["Add a .env.example that lists required/optional vars with comments."]
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return findings;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ============================================================================
|
|
194
|
-
// FAKE SUCCESS ANALYZER (INV_NO_FAKE_SUCCESS)
|
|
195
|
-
// ============================================================================
|
|
196
|
-
|
|
197
|
-
function isToastSuccessCall(node) {
|
|
198
|
-
return t.isCallExpression(node) &&
|
|
199
|
-
t.isMemberExpression(node.callee) &&
|
|
200
|
-
t.isIdentifier(node.callee.object, { name: "toast" }) &&
|
|
201
|
-
t.isIdentifier(node.callee.property, { name: "success" });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function isRouterPushCall(node) {
|
|
205
|
-
return t.isCallExpression(node) && (
|
|
206
|
-
(t.isMemberExpression(node.callee) &&
|
|
207
|
-
t.isIdentifier(node.callee.property, { name: "push" })) ||
|
|
208
|
-
(t.isIdentifier(node.callee) && (node.callee.name === "navigate"))
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function isFetchCall(node) {
|
|
213
|
-
return t.isCallExpression(node) && t.isIdentifier(node.callee, { name: "fetch" });
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function isAxiosCall(node) {
|
|
217
|
-
return t.isCallExpression(node) &&
|
|
218
|
-
t.isMemberExpression(node.callee) &&
|
|
219
|
-
t.isIdentifier(node.callee.object, { name: "axios" }) &&
|
|
220
|
-
t.isIdentifier(node.callee.property) &&
|
|
221
|
-
["get","post","put","patch","delete"].includes(node.callee.property.name);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function findFakeSuccess(repoRoot) {
|
|
225
|
-
const findings = [];
|
|
226
|
-
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
227
|
-
cwd: repoRoot,
|
|
228
|
-
absolute: true,
|
|
229
|
-
ignore: ["**/node_modules/**","**/.next/**","**/dist/**","**/build/**"]
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
for (const fileAbs of files) {
|
|
233
|
-
const code = fs.readFileSync(fileAbs, "utf8");
|
|
234
|
-
let ast;
|
|
235
|
-
try { ast = parseFile(code); } catch { continue; }
|
|
236
|
-
|
|
237
|
-
traverse(ast, {
|
|
238
|
-
Function(pathFn) {
|
|
239
|
-
let hasSuccess = false;
|
|
240
|
-
let successLoc = null;
|
|
241
|
-
let hasNetwork = false;
|
|
242
|
-
let hasAwaitNetwork = false;
|
|
243
|
-
let hasOkCheck = false;
|
|
244
|
-
|
|
245
|
-
pathFn.traverse({
|
|
246
|
-
CallExpression(p) {
|
|
247
|
-
const n = p.node;
|
|
248
|
-
|
|
249
|
-
if (isToastSuccessCall(n) || isRouterPushCall(n)) {
|
|
250
|
-
hasSuccess = true;
|
|
251
|
-
successLoc = successLoc || n.loc;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (isFetchCall(n) || isAxiosCall(n)) {
|
|
255
|
-
hasNetwork = true;
|
|
256
|
-
if (p.parentPath && p.parentPath.isAwaitExpression()) hasAwaitNetwork = true;
|
|
257
|
-
}
|
|
258
|
-
},
|
|
259
|
-
IfStatement(p) {
|
|
260
|
-
const test = p.node.test;
|
|
261
|
-
const text = code.slice(test.start || 0, test.end || 0);
|
|
262
|
-
if (/\b(ok|status)\b/.test(text) && /(res|response)/.test(text)) {
|
|
263
|
-
hasOkCheck = true;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
if (!hasSuccess || !hasNetwork) return;
|
|
269
|
-
|
|
270
|
-
const severity = hasAwaitNetwork ? (hasOkCheck ? null : "WARN") : "BLOCK";
|
|
271
|
-
if (!severity) return;
|
|
272
|
-
|
|
273
|
-
const ev = evidenceFromLoc(fileAbs, repoRoot, successLoc, "Success UI call in networked flow");
|
|
274
|
-
findings.push({
|
|
275
|
-
id: `F_FAKE_SUCCESS_${String(findings.length + 1).padStart(3, "0")}`,
|
|
276
|
-
severity,
|
|
277
|
-
category: "FakeSuccess",
|
|
278
|
-
title: severity === "BLOCK"
|
|
279
|
-
? "Success UI triggered without awaiting network call"
|
|
280
|
-
: "Success UI triggered without verifying network result (res.ok/status)",
|
|
281
|
-
why: severity === "BLOCK"
|
|
282
|
-
? "This ships lies. Users see success even when the request never completed."
|
|
283
|
-
: "This often ships lies. You're not gating success on a real response.",
|
|
284
|
-
confidence: "med",
|
|
285
|
-
evidence: ev ? [ev] : [],
|
|
286
|
-
fixHints: [
|
|
287
|
-
"Await the network call (await fetch/await axios...).",
|
|
288
|
-
"Gate success UI behind res.ok / status checks; surface errors otherwise."
|
|
289
|
-
]
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
return findings;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// ============================================================================
|
|
299
|
-
// GHOST AUTH ANALYZER (INV_NO_GHOST_AUTH)
|
|
300
|
-
// ============================================================================
|
|
301
|
-
|
|
302
|
-
function looksSensitive(pathStr) {
|
|
303
|
-
const p = String(pathStr || "");
|
|
304
|
-
return (
|
|
305
|
-
p.startsWith("/api/admin") ||
|
|
306
|
-
p.startsWith("/api/billing") ||
|
|
307
|
-
p.startsWith("/api/stripe") ||
|
|
308
|
-
p.startsWith("/api/org") ||
|
|
309
|
-
p.startsWith("/api/team") ||
|
|
310
|
-
p.startsWith("/api/account") ||
|
|
311
|
-
p.startsWith("/api/settings") ||
|
|
312
|
-
p.startsWith("/api/users") ||
|
|
313
|
-
p.startsWith("/api/user")
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function hasRouteLevelProtection(routeDef) {
|
|
318
|
-
const hooks = routeDef.hooks || [];
|
|
319
|
-
if (hooks.includes("preHandler") || hooks.includes("onRequest") || hooks.includes("preValidation")) return true;
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function handlerHasAuthSignal(repoRoot, handlerRel) {
|
|
324
|
-
const abs = path.join(repoRoot, handlerRel);
|
|
325
|
-
if (!fs.existsSync(abs)) return false;
|
|
326
|
-
const code = fs.readFileSync(abs, "utf8");
|
|
327
|
-
|
|
328
|
-
return (
|
|
329
|
-
/\bgetServerSession\b|\bauth\(\)\b|\bclerk\b|@clerk\/nextjs|\bcreateRouteHandlerClient\b|@supabase/i.test(code) ||
|
|
330
|
-
/\b(jwtVerify|authorization|bearer|verifyToken|verifyJWT)\b/i.test(code) ||
|
|
331
|
-
/\b(isAdmin|adminOnly|permissions|rbac)\b/i.test(code)
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function isProtectedByNextMiddleware(truthpack, routePath) {
|
|
336
|
-
const patterns = truthpack?.auth?.nextMatcherPatterns || [];
|
|
337
|
-
return matcherCoversPath(patterns, routePath);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function findGhostAuth(truthpack, repoRoot) {
|
|
341
|
-
const findings = [];
|
|
342
|
-
const server = truthpack?.routes?.server || [];
|
|
343
|
-
|
|
344
|
-
for (const r of server) {
|
|
345
|
-
if (!looksSensitive(r.path)) continue;
|
|
346
|
-
|
|
347
|
-
const middlewareProtected = isProtectedByNextMiddleware(truthpack, r.path);
|
|
348
|
-
const routeHooksProtected = hasRouteLevelProtection(r);
|
|
349
|
-
const handlerProtected = r.handler ? handlerHasAuthSignal(repoRoot, r.handler) : false;
|
|
350
|
-
|
|
351
|
-
const protectedSomehow = middlewareProtected || routeHooksProtected || handlerProtected;
|
|
352
|
-
|
|
353
|
-
if (!protectedSomehow) {
|
|
354
|
-
findings.push({
|
|
355
|
-
id: `F_GHOST_AUTH_${r.method}_${r.path}`.replace(/[^A-Z0-9_\/:*-]/gi, "_"),
|
|
356
|
-
severity: "BLOCK",
|
|
357
|
-
category: "GhostAuth",
|
|
358
|
-
title: `Sensitive endpoint appears unprotected: ${r.method} ${r.path}`,
|
|
359
|
-
why: "This is how apps get owned. UI gating doesn't matter. If the server doesn't enforce auth, it's public.",
|
|
360
|
-
confidence: "med",
|
|
361
|
-
evidence: (r.evidence || []).slice(0, 2),
|
|
362
|
-
fixHints: [
|
|
363
|
-
"Add server-side auth verification in the handler (session/jwt).",
|
|
364
|
-
"Or protect the path via Next middleware matcher (and verify it actually applies).",
|
|
365
|
-
"If Fastify: add preHandler/onRequest auth hook and ensure it's registered for this route."
|
|
366
|
-
]
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// If there IS middleware but it doesn't cover obvious sensitive prefixes, warn
|
|
372
|
-
const patterns = truthpack?.auth?.nextMatcherPatterns || [];
|
|
373
|
-
if (patterns.length) {
|
|
374
|
-
const coversApi = patterns.some(p => String(p).includes("/api"));
|
|
375
|
-
if (!coversApi) {
|
|
376
|
-
findings.push({
|
|
377
|
-
id: "F_MIDDLEWARE_NOT_COVERING_API",
|
|
378
|
-
severity: "WARN",
|
|
379
|
-
category: "GhostAuth",
|
|
380
|
-
title: "Next middleware exists but does not appear to cover /api routes",
|
|
381
|
-
why: "People assume middleware protects APIs. Often it doesn't. Verify matcher patterns.",
|
|
382
|
-
confidence: "high",
|
|
383
|
-
evidence: (truthpack?.auth?.nextMiddleware?.[0]?.evidence || []).slice(0, 3),
|
|
384
|
-
fixHints: ["Add /api/:path* to middleware matcher if your design expects API auth protection."]
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return findings;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// ============================================================================
|
|
393
|
-
// STRIPE WEBHOOK VIOLATIONS (INV_WEBHOOK_VERIFIED + INV_WEBHOOK_IDEMPOTENT)
|
|
394
|
-
// ============================================================================
|
|
395
|
-
|
|
396
|
-
function findStripeWebhookViolations(truthpack) {
|
|
397
|
-
const findings = [];
|
|
398
|
-
const billing = truthpack?.billing;
|
|
399
|
-
|
|
400
|
-
if (!billing?.hasStripe) return findings;
|
|
401
|
-
|
|
402
|
-
const candidates = billing.webhookCandidates || [];
|
|
403
|
-
|
|
404
|
-
if (!candidates.length) {
|
|
405
|
-
findings.push({
|
|
406
|
-
id: "F_STRIPE_NO_WEBHOOK_HANDLER",
|
|
407
|
-
severity: "WARN",
|
|
408
|
-
category: "Billing",
|
|
409
|
-
title: "Stripe appears used but no webhook handler candidate detected",
|
|
410
|
-
why: "If you bill with Stripe, webhooks are usually required. Missing webhooks often means subscription state desync.",
|
|
411
|
-
confidence: "med",
|
|
412
|
-
evidence: [],
|
|
413
|
-
fixHints: ["Add a Stripe webhook handler with signature verification and idempotency."]
|
|
414
|
-
});
|
|
415
|
-
return findings;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
for (const w of candidates) {
|
|
419
|
-
const verified = w.signals.webhookConstructEvent && w.signals.rawBodySignal && w.signals.readsStripeSignatureHeader;
|
|
420
|
-
const idempotent = !!w.signals.idempotencySignal;
|
|
421
|
-
|
|
422
|
-
if (!verified) {
|
|
423
|
-
findings.push({
|
|
424
|
-
id: `F_STRIPE_WEBHOOK_NOT_VERIFIED_${w.file.replace(/[^a-z0-9]/gi, "_")}`,
|
|
425
|
-
severity: "BLOCK",
|
|
426
|
-
category: "Billing",
|
|
427
|
-
title: `Stripe webhook handler not clearly signature-verified: ${w.file}`,
|
|
428
|
-
why: "Unverified webhooks = spoofable billing state. That's catastrophic.",
|
|
429
|
-
confidence: "high",
|
|
430
|
-
evidence: (w.evidence || []).slice(0, 4),
|
|
431
|
-
fixHints: [
|
|
432
|
-
"Use stripe.webhooks.constructEvent(rawBody, sigHeader, STRIPE_WEBHOOK_SECRET).",
|
|
433
|
-
"Ensure raw body is used (disable bodyParser in pages router; in app router read req.text()/arrayBuffer).",
|
|
434
|
-
"Reject if signature missing/invalid."
|
|
435
|
-
]
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (!idempotent) {
|
|
440
|
-
findings.push({
|
|
441
|
-
id: `F_STRIPE_WEBHOOK_NOT_IDEMPOTENT_${w.file.replace(/[^a-z0-9]/gi, "_")}`,
|
|
442
|
-
severity: "BLOCK",
|
|
443
|
-
category: "Billing",
|
|
444
|
-
title: `Stripe webhook handler not clearly idempotent: ${w.file}`,
|
|
445
|
-
why: "Stripe retries webhooks. Without dedupe, you'll double-grant access, double-send emails, or double-write state.",
|
|
446
|
-
confidence: "med",
|
|
447
|
-
evidence: (w.evidence || []).slice(0, 4),
|
|
448
|
-
fixHints: [
|
|
449
|
-
"Persist event.id as processed (DB/Redis). If seen, return 200 immediately.",
|
|
450
|
-
"Wrap state mutation in a transaction keyed by event.id."
|
|
451
|
-
]
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
return findings;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// ============================================================================
|
|
460
|
-
// PAID SURFACE NOT ENFORCED (INV_PAID_FEATURE_ENFORCED_SERVER_SIDE)
|
|
461
|
-
// ============================================================================
|
|
462
|
-
|
|
463
|
-
function findPaidSurfaceNotEnforced(truthpack) {
|
|
464
|
-
const findings = [];
|
|
465
|
-
const enforcement = truthpack?.enforcement;
|
|
466
|
-
|
|
467
|
-
const checks = enforcement?.checks || [];
|
|
468
|
-
for (const c of checks) {
|
|
469
|
-
if (c.enforced) continue;
|
|
470
|
-
|
|
471
|
-
findings.push({
|
|
472
|
-
id: `F_PAID_SURFACE_NOT_ENFORCED_${c.method}_${c.path}`.replace(/[^a-z0-9]/gi, "_"),
|
|
473
|
-
severity: "BLOCK",
|
|
474
|
-
category: "Entitlements",
|
|
475
|
-
title: `Paid surface appears un-enforced server-side: ${c.method} ${c.path}`,
|
|
476
|
-
why: "If enforcement is only in the CLI/UI, users can call the endpoint directly. That's a free enterprise bypass.",
|
|
477
|
-
confidence: "med",
|
|
478
|
-
evidence: [],
|
|
479
|
-
fixHints: [
|
|
480
|
-
"Add enforceFeature/enforceLimit in the server handler BEFORE doing work.",
|
|
481
|
-
"Return 402/403 with a structured error code.",
|
|
482
|
-
"Make the CLI treat that code as an upgrade prompt."
|
|
483
|
-
]
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
return findings;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// ============================================================================
|
|
490
|
-
// OWNER MODE BYPASS (INV_NO_OWNER_MODE_BYPASS)
|
|
491
|
-
// ============================================================================
|
|
492
|
-
|
|
493
|
-
function findOwnerModeBypass(repoRoot) {
|
|
494
|
-
const findings = [];
|
|
495
|
-
const files = fg.sync(["**/*.{ts,tsx,js,jsx}"], {
|
|
496
|
-
cwd: repoRoot,
|
|
497
|
-
absolute: true,
|
|
498
|
-
ignore: ["**/node_modules/**","**/.next/**","**/dist/**","**/build/**"]
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
const patterns = [
|
|
502
|
-
/OWNER_MODE/i,
|
|
503
|
-
/GUARDRAIL_OWNER_MODE/i,
|
|
504
|
-
/VIBECHECK_OWNER_MODE/i,
|
|
505
|
-
/process\.env\.[A-Z0-9_]*OWNER[A-Z0-9_]*/i
|
|
506
|
-
];
|
|
507
|
-
|
|
508
|
-
for (const fileAbs of files) {
|
|
509
|
-
const code = fs.readFileSync(fileAbs, "utf8");
|
|
510
|
-
const hit = patterns.some(rx => rx.test(code));
|
|
511
|
-
if (!hit) continue;
|
|
512
|
-
|
|
513
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
514
|
-
|
|
515
|
-
findings.push({
|
|
516
|
-
id: `F_OWNER_MODE_BYPASS_${fileRel.replace(/[^a-z0-9]/gi, "_")}`,
|
|
517
|
-
severity: "BLOCK",
|
|
518
|
-
category: "Security",
|
|
519
|
-
title: `Owner mode / env bypass signal detected: ${fileRel}`,
|
|
520
|
-
why: "This is a production backdoor unless it's cryptographically gated. It cannot ship.",
|
|
521
|
-
confidence: "high",
|
|
522
|
-
evidence: [],
|
|
523
|
-
fixHints: [
|
|
524
|
-
"Delete owner mode bypass. If you need dev override, require a signed admin token + non-prod environment.",
|
|
525
|
-
"Add a test that asserts no OWNER_MODE env var grants entitlements."
|
|
526
|
-
]
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return findings;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
module.exports = {
|
|
534
|
-
findMissingRoutes,
|
|
535
|
-
findEnvGaps,
|
|
536
|
-
findFakeSuccess,
|
|
537
|
-
findGhostAuth,
|
|
538
|
-
findStripeWebhookViolations,
|
|
539
|
-
findPaidSurfaceNotEnforced,
|
|
540
|
-
findOwnerModeBypass
|
|
541
|
-
};
|