@vibecheckai/cli 3.2.2 → 3.2.4
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/bin/.generated +25 -25
- package/bin/dev/run-v2-torture.js +30 -30
- package/bin/runners/ENHANCEMENT_GUIDE.md +121 -121
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +117 -28
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +23 -14
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +72 -1
- package/bin/runners/lib/agent-firewall/interceptor/base.js +2 -2
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +6 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +34 -3
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +29 -4
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +12 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +21 -0
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
- package/bin/runners/lib/analyzers.js +606 -325
- package/bin/runners/lib/auth-truth.js +193 -193
- package/bin/runners/lib/backup.js +62 -62
- package/bin/runners/lib/billing.js +107 -107
- package/bin/runners/lib/claims.js +118 -118
- package/bin/runners/lib/cli-ui.js +540 -540
- package/bin/runners/lib/contracts/auth-contract.js +202 -202
- package/bin/runners/lib/contracts/env-contract.js +181 -181
- package/bin/runners/lib/contracts/external-contract.js +206 -206
- package/bin/runners/lib/contracts/guard.js +168 -168
- package/bin/runners/lib/contracts/index.js +89 -89
- package/bin/runners/lib/contracts/plan-validator.js +311 -311
- package/bin/runners/lib/contracts/route-contract.js +199 -199
- package/bin/runners/lib/contracts.js +804 -804
- package/bin/runners/lib/detect.js +89 -89
- package/bin/runners/lib/doctor/autofix.js +254 -254
- package/bin/runners/lib/doctor/index.js +37 -37
- package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
- package/bin/runners/lib/doctor/modules/index.js +46 -46
- package/bin/runners/lib/doctor/modules/network.js +250 -250
- package/bin/runners/lib/doctor/modules/project.js +312 -312
- package/bin/runners/lib/doctor/modules/runtime.js +224 -224
- package/bin/runners/lib/doctor/modules/security.js +348 -348
- package/bin/runners/lib/doctor/modules/system.js +213 -213
- package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
- package/bin/runners/lib/doctor/reporter.js +262 -262
- package/bin/runners/lib/doctor/service.js +262 -262
- package/bin/runners/lib/doctor/types.js +113 -113
- package/bin/runners/lib/doctor/ui.js +263 -263
- package/bin/runners/lib/doctor-v2.js +608 -608
- package/bin/runners/lib/drift.js +425 -425
- package/bin/runners/lib/enforcement.js +72 -72
- package/bin/runners/lib/engines/accessibility-engine.js +190 -0
- package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
- package/bin/runners/lib/engines/ast-cache.js +99 -0
- package/bin/runners/lib/engines/code-quality-engine.js +255 -0
- package/bin/runners/lib/engines/console-logs-engine.js +115 -0
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
- package/bin/runners/lib/engines/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
- package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
- package/bin/runners/lib/engines/file-filter.js +131 -0
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
- package/bin/runners/lib/engines/mock-data-engine.js +272 -0
- package/bin/runners/lib/engines/parallel-processor.js +71 -0
- package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
- package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
- package/bin/runners/lib/engines/type-aware-engine.js +152 -0
- package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
- package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
- package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
- package/bin/runners/lib/enterprise-detect.js +603 -603
- package/bin/runners/lib/enterprise-init.js +942 -942
- package/bin/runners/lib/env-resolver.js +417 -417
- package/bin/runners/lib/env-template.js +66 -66
- package/bin/runners/lib/env.js +189 -189
- package/bin/runners/lib/extractors/client-calls.js +990 -990
- package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
- package/bin/runners/lib/extractors/fastify-routes.js +426 -426
- package/bin/runners/lib/extractors/index.js +363 -363
- package/bin/runners/lib/extractors/next-routes.js +524 -524
- package/bin/runners/lib/extractors/proof-graph.js +431 -431
- package/bin/runners/lib/extractors/route-matcher.js +451 -451
- package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
- package/bin/runners/lib/extractors/ui-bindings.js +547 -547
- package/bin/runners/lib/findings-schema.js +281 -281
- package/bin/runners/lib/firewall-prompt.js +50 -50
- package/bin/runners/lib/global-flags.js +213 -213
- package/bin/runners/lib/graph/graph-builder.js +265 -265
- package/bin/runners/lib/graph/html-renderer.js +413 -413
- package/bin/runners/lib/graph/index.js +32 -32
- package/bin/runners/lib/graph/runtime-collector.js +215 -215
- package/bin/runners/lib/graph/static-extractor.js +518 -518
- package/bin/runners/lib/html-report.js +650 -650
- package/bin/runners/lib/interactive-menu.js +1496 -1496
- package/bin/runners/lib/llm.js +75 -75
- package/bin/runners/lib/meter.js +61 -61
- package/bin/runners/lib/missions/evidence.js +126 -126
- package/bin/runners/lib/patch.js +40 -40
- package/bin/runners/lib/permissions/auth-model.js +213 -213
- package/bin/runners/lib/permissions/idor-prover.js +205 -205
- package/bin/runners/lib/permissions/index.js +45 -45
- package/bin/runners/lib/permissions/matrix-builder.js +198 -198
- package/bin/runners/lib/pkgjson.js +28 -28
- package/bin/runners/lib/policy.js +295 -295
- package/bin/runners/lib/preflight.js +142 -142
- package/bin/runners/lib/reality/correlation-detectors.js +359 -359
- package/bin/runners/lib/reality/index.js +318 -318
- package/bin/runners/lib/reality/request-hashing.js +416 -416
- package/bin/runners/lib/reality/request-mapper.js +453 -453
- package/bin/runners/lib/reality/safety-rails.js +463 -463
- package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
- package/bin/runners/lib/reality/toast-detector.js +393 -393
- package/bin/runners/lib/reality-findings.js +84 -84
- package/bin/runners/lib/receipts.js +179 -179
- package/bin/runners/lib/redact.js +29 -29
- package/bin/runners/lib/replay/capsule-manager.js +154 -154
- package/bin/runners/lib/replay/index.js +263 -263
- package/bin/runners/lib/replay/player.js +348 -348
- package/bin/runners/lib/replay/recorder.js +331 -331
- package/bin/runners/lib/report-output.js +187 -187
- package/bin/runners/lib/report.js +135 -135
- package/bin/runners/lib/route-detection.js +1140 -1140
- package/bin/runners/lib/sandbox/index.js +59 -59
- package/bin/runners/lib/sandbox/proof-chain.js +399 -399
- package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
- package/bin/runners/lib/sandbox/worktree.js +174 -174
- package/bin/runners/lib/scan-output.js +525 -190
- package/bin/runners/lib/schema-validator.js +350 -350
- package/bin/runners/lib/schemas/contracts.schema.json +160 -160
- package/bin/runners/lib/schemas/finding.schema.json +100 -100
- package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
- package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
- package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
- package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
- package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
- package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
- package/bin/runners/lib/schemas/validator.js +438 -438
- package/bin/runners/lib/score-history.js +282 -282
- package/bin/runners/lib/share-pack.js +239 -239
- package/bin/runners/lib/snippets.js +67 -67
- package/bin/runners/lib/status-output.js +253 -253
- package/bin/runners/lib/terminal-ui.js +351 -271
- package/bin/runners/lib/upsell.js +510 -510
- package/bin/runners/lib/usage.js +153 -153
- package/bin/runners/lib/validate-patch.js +156 -156
- package/bin/runners/lib/verdict-engine.js +628 -628
- package/bin/runners/reality/engine.js +917 -917
- package/bin/runners/reality/flows.js +122 -122
- package/bin/runners/reality/report.js +378 -378
- package/bin/runners/reality/session.js +193 -193
- package/bin/runners/runGuard.js +168 -168
- package/bin/runners/runProof.zip +0 -0
- package/bin/runners/runProve.js +8 -0
- package/bin/runners/runReality.js +14 -0
- package/bin/runners/runScan.js +17 -1
- package/bin/runners/runTruth.js +15 -3
- package/mcp-server/tier-auth.js +4 -4
- package/mcp-server/tools/index.js +72 -72
- package/package.json +1 -1
|
@@ -1,1140 +1,1140 @@
|
|
|
1
|
-
// bin/runners/lib/route-detection.js
|
|
2
|
-
// Multi-framework route detection: Express, Flask, FastAPI, Django, Hono, Koa, etc.
|
|
3
|
-
|
|
4
|
-
const fg = require("fast-glob");
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const path = require("path");
|
|
7
|
-
const crypto = require("crypto");
|
|
8
|
-
const parser = require("@babel/parser");
|
|
9
|
-
const traverse = require("@babel/traverse").default;
|
|
10
|
-
const t = require("@babel/types");
|
|
11
|
-
|
|
12
|
-
// ============================================================================
|
|
13
|
-
// HELPERS
|
|
14
|
-
// ============================================================================
|
|
15
|
-
|
|
16
|
-
function sha256(text) {
|
|
17
|
-
return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function canonicalizeMethod(m) {
|
|
21
|
-
const u = String(m || "").toUpperCase();
|
|
22
|
-
if (u === "ALL" || u === "ANY" || u === "*") return "*";
|
|
23
|
-
return u;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function canonicalizePath(p) {
|
|
27
|
-
let s = String(p || "").trim();
|
|
28
|
-
if (!s.startsWith("/")) s = "/" + s;
|
|
29
|
-
s = s.replace(/\/+/g, "/");
|
|
30
|
-
// Next dynamic segments
|
|
31
|
-
s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, "*$1?");
|
|
32
|
-
s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1");
|
|
33
|
-
s = s.replace(/\[([^\]]+)\]/g, ":$1");
|
|
34
|
-
// Python path params: <id> or <int:id>
|
|
35
|
-
s = s.replace(/<(?:\w+:)?(\w+)>/g, ":$1");
|
|
36
|
-
// FastAPI path params: {id}
|
|
37
|
-
s = s.replace(/\{(\w+)\}/g, ":$1");
|
|
38
|
-
if (s.length > 1) s = s.replace(/\/$/, "");
|
|
39
|
-
return s;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function joinPaths(prefix, p) {
|
|
43
|
-
const a = canonicalizePath(prefix || "/");
|
|
44
|
-
const b = canonicalizePath(p || "/");
|
|
45
|
-
if (a === "/") return b;
|
|
46
|
-
if (b === "/") return a;
|
|
47
|
-
return canonicalizePath(a + "/" + b);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function exists(p) {
|
|
51
|
-
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function safeRead(fileAbs) {
|
|
55
|
-
try { return fs.readFileSync(fileAbs, "utf8"); } catch { return ""; }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function parseJS(code) {
|
|
59
|
-
return parser.parse(code, { sourceType: "unambiguous", plugins: ["typescript", "jsx"] });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function evidenceFromLoc({ fileAbs, fileRel, loc, reason }) {
|
|
63
|
-
if (!loc) return null;
|
|
64
|
-
try {
|
|
65
|
-
const lines = fs.readFileSync(fileAbs, "utf8").split(/\r?\n/);
|
|
66
|
-
const start = Math.max(1, loc.start?.line || 1);
|
|
67
|
-
const end = Math.max(start, loc.end?.line || start);
|
|
68
|
-
const snippet = lines.slice(start - 1, end).join("\n");
|
|
69
|
-
return {
|
|
70
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
71
|
-
file: fileRel,
|
|
72
|
-
lines: `${start}-${end}`,
|
|
73
|
-
snippetHash: sha256(snippet),
|
|
74
|
-
reason
|
|
75
|
-
};
|
|
76
|
-
} catch { return null; }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ============================================================================
|
|
80
|
-
// FRAMEWORK DETECTION
|
|
81
|
-
// ============================================================================
|
|
82
|
-
|
|
83
|
-
const FRAMEWORK_SIGNATURES = {
|
|
84
|
-
// JavaScript/TypeScript
|
|
85
|
-
express: ["express", "app.get", "app.post", "app.use", "router.get", "router.post"],
|
|
86
|
-
fastify: ["fastify", "Fastify", "fastify.get", "fastify.post", "fastify.register"],
|
|
87
|
-
koa: ["koa", "new Koa", "ctx.body", "router.get"],
|
|
88
|
-
hono: ["hono", "Hono", "app.get", "app.post"],
|
|
89
|
-
nestjs: ["@nestjs", "@Controller", "@Get", "@Post"],
|
|
90
|
-
nextjs: ["next", "next/server", "NextResponse"],
|
|
91
|
-
|
|
92
|
-
// Python
|
|
93
|
-
flask: ["flask", "Flask", "@app.route", "@blueprint.route"],
|
|
94
|
-
fastapi: ["fastapi", "FastAPI", "@app.get", "@app.post", "@router.get"],
|
|
95
|
-
django: ["django", "urlpatterns", "path(", "re_path("],
|
|
96
|
-
starlette: ["starlette", "Route(", "Mount("],
|
|
97
|
-
|
|
98
|
-
// Go
|
|
99
|
-
gin: ["gin-gonic", "gin.Default", "r.GET", "r.POST"],
|
|
100
|
-
echo: ["labstack/echo", "echo.New", "e.GET", "e.POST"],
|
|
101
|
-
fiber: ["gofiber/fiber", "fiber.New", "app.Get", "app.Post"],
|
|
102
|
-
|
|
103
|
-
// Ruby
|
|
104
|
-
rails: ["rails", "resources :", "get '", "post '"],
|
|
105
|
-
sinatra: ["sinatra", "get '", "post '"],
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
async function detectFrameworks(repoRoot) {
|
|
109
|
-
const detected = new Set();
|
|
110
|
-
|
|
111
|
-
// Check package.json for JS frameworks
|
|
112
|
-
const pkgPath = path.join(repoRoot, "package.json");
|
|
113
|
-
if (exists(pkgPath)) {
|
|
114
|
-
try {
|
|
115
|
-
const pkg = JSON.parse(safeRead(pkgPath));
|
|
116
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
117
|
-
|
|
118
|
-
if (allDeps.express) detected.add("express");
|
|
119
|
-
if (allDeps.fastify) detected.add("fastify");
|
|
120
|
-
if (allDeps.koa) detected.add("koa");
|
|
121
|
-
if (allDeps.hono) detected.add("hono");
|
|
122
|
-
if (allDeps["@nestjs/core"]) detected.add("nestjs");
|
|
123
|
-
if (allDeps.next) detected.add("nextjs");
|
|
124
|
-
} catch {}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Check requirements.txt / pyproject.toml for Python
|
|
128
|
-
const reqPath = path.join(repoRoot, "requirements.txt");
|
|
129
|
-
if (exists(reqPath)) {
|
|
130
|
-
const req = safeRead(reqPath).toLowerCase();
|
|
131
|
-
if (req.includes("flask")) detected.add("flask");
|
|
132
|
-
if (req.includes("fastapi")) detected.add("fastapi");
|
|
133
|
-
if (req.includes("django")) detected.add("django");
|
|
134
|
-
if (req.includes("starlette")) detected.add("starlette");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const pyprojectPath = path.join(repoRoot, "pyproject.toml");
|
|
138
|
-
if (exists(pyprojectPath)) {
|
|
139
|
-
const pyp = safeRead(pyprojectPath).toLowerCase();
|
|
140
|
-
if (pyp.includes("flask")) detected.add("flask");
|
|
141
|
-
if (pyp.includes("fastapi")) detected.add("fastapi");
|
|
142
|
-
if (pyp.includes("django")) detected.add("django");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Check Gemfile for Ruby
|
|
146
|
-
const gemfilePath = path.join(repoRoot, "Gemfile");
|
|
147
|
-
if (exists(gemfilePath)) {
|
|
148
|
-
const gem = safeRead(gemfilePath).toLowerCase();
|
|
149
|
-
if (gem.includes("rails")) detected.add("rails");
|
|
150
|
-
if (gem.includes("sinatra")) detected.add("sinatra");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Check go.mod for Go
|
|
154
|
-
const gomodPath = path.join(repoRoot, "go.mod");
|
|
155
|
-
if (exists(gomodPath)) {
|
|
156
|
-
const gomod = safeRead(gomodPath).toLowerCase();
|
|
157
|
-
if (gomod.includes("gin-gonic")) detected.add("gin");
|
|
158
|
-
if (gomod.includes("labstack/echo")) detected.add("echo");
|
|
159
|
-
if (gomod.includes("gofiber/fiber")) detected.add("fiber");
|
|
160
|
-
if (gomod.includes("go-chi/chi")) detected.add("chi");
|
|
161
|
-
if (gomod.includes("gorilla/mux")) detected.add("gorilla");
|
|
162
|
-
detected.add("go");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Check for OpenAPI/Swagger specs
|
|
166
|
-
const openapiFiles = ["openapi.json", "openapi.yaml", "openapi.yml", "swagger.json", "swagger.yaml"];
|
|
167
|
-
for (const f of openapiFiles) {
|
|
168
|
-
if (exists(path.join(repoRoot, f))) {
|
|
169
|
-
detected.add("openapi");
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Check for GraphQL
|
|
175
|
-
if (exists(path.join(repoRoot, "schema.graphql")) || exists(path.join(repoRoot, "schema.gql"))) {
|
|
176
|
-
detected.add("graphql");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return Array.from(detected);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ============================================================================
|
|
183
|
-
// EXPRESS ROUTE DETECTION
|
|
184
|
-
// ============================================================================
|
|
185
|
-
|
|
186
|
-
const EXPRESS_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "all", "use"]);
|
|
187
|
-
|
|
188
|
-
async function resolveExpressRoutes(repoRoot) {
|
|
189
|
-
// Find all potential Express/server files (comprehensive for monorepos)
|
|
190
|
-
const files = await fg([
|
|
191
|
-
"**/server.{ts,js}",
|
|
192
|
-
"**/app.{ts,js}",
|
|
193
|
-
"**/index.{ts,js}",
|
|
194
|
-
"**/routes/**/*.{ts,js}",
|
|
195
|
-
"**/api/**/*.{ts,js}",
|
|
196
|
-
"**/src/**/*.{ts,js}",
|
|
197
|
-
"**/apps/**/src/**/*.{ts,js}",
|
|
198
|
-
"**/packages/**/src/**/*.{ts,js}",
|
|
199
|
-
"**/services/**/*.{ts,js}"
|
|
200
|
-
], {
|
|
201
|
-
cwd: repoRoot,
|
|
202
|
-
absolute: true,
|
|
203
|
-
ignore: [
|
|
204
|
-
"**/node_modules/**",
|
|
205
|
-
"**/.next/**",
|
|
206
|
-
"**/dist/**",
|
|
207
|
-
"**/build/**",
|
|
208
|
-
"**/test/**",
|
|
209
|
-
"**/tests/**",
|
|
210
|
-
"**/__tests__/**",
|
|
211
|
-
"**/*.test.*",
|
|
212
|
-
"**/*.spec.*",
|
|
213
|
-
"**/jest.*",
|
|
214
|
-
"**/*.d.ts"
|
|
215
|
-
]
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
const routes = [];
|
|
219
|
-
const appNames = new Set(["app", "router", "express", "server"]);
|
|
220
|
-
|
|
221
|
-
for (const fileAbs of files) {
|
|
222
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
223
|
-
const code = safeRead(fileAbs);
|
|
224
|
-
|
|
225
|
-
// Quick check if file uses Express
|
|
226
|
-
if (!code.includes("express") && !code.includes("app.") && !code.includes("router.")) continue;
|
|
227
|
-
|
|
228
|
-
let ast;
|
|
229
|
-
try { ast = parseJS(code); } catch { continue; }
|
|
230
|
-
|
|
231
|
-
// Detect Express app/router instances
|
|
232
|
-
traverse(ast, {
|
|
233
|
-
VariableDeclarator(p) {
|
|
234
|
-
if (!t.isIdentifier(p.node.id)) return;
|
|
235
|
-
const id = p.node.id.name;
|
|
236
|
-
const init = p.node.init;
|
|
237
|
-
if (!init) return;
|
|
238
|
-
|
|
239
|
-
// const app = express()
|
|
240
|
-
if (t.isCallExpression(init) && t.isIdentifier(init.callee) && init.callee.name === "express") {
|
|
241
|
-
appNames.add(id);
|
|
242
|
-
}
|
|
243
|
-
// const router = express.Router()
|
|
244
|
-
if (t.isCallExpression(init) && t.isMemberExpression(init.callee)) {
|
|
245
|
-
const obj = init.callee.object;
|
|
246
|
-
const prop = init.callee.property;
|
|
247
|
-
if (t.isIdentifier(obj) && obj.name === "express" && t.isIdentifier(prop) && prop.name === "Router") {
|
|
248
|
-
appNames.add(id);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
// Extract routes
|
|
255
|
-
traverse(ast, {
|
|
256
|
-
CallExpression(p) {
|
|
257
|
-
const callee = p.node.callee;
|
|
258
|
-
if (!t.isMemberExpression(callee)) return;
|
|
259
|
-
if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
|
|
260
|
-
|
|
261
|
-
const obj = callee.object.name;
|
|
262
|
-
const method = callee.property.name;
|
|
263
|
-
|
|
264
|
-
if (!appNames.has(obj)) return;
|
|
265
|
-
if (!EXPRESS_METHODS.has(method)) return;
|
|
266
|
-
|
|
267
|
-
const arg0 = p.node.arguments[0];
|
|
268
|
-
if (!t.isStringLiteral(arg0)) return;
|
|
269
|
-
|
|
270
|
-
const routePath = canonicalizePath(arg0.value);
|
|
271
|
-
|
|
272
|
-
// Skip middleware mounts without paths
|
|
273
|
-
if (method === "use" && !arg0.value.startsWith("/")) return;
|
|
274
|
-
|
|
275
|
-
const ev = evidenceFromLoc({
|
|
276
|
-
fileAbs, fileRel, loc: p.node.loc,
|
|
277
|
-
reason: `Express ${method.toUpperCase()}("${arg0.value}")`
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
routes.push({
|
|
281
|
-
method: canonicalizeMethod(method === "use" ? "*" : method),
|
|
282
|
-
path: routePath,
|
|
283
|
-
handler: fileRel,
|
|
284
|
-
framework: "express",
|
|
285
|
-
confidence: "high",
|
|
286
|
-
evidence: ev ? [ev] : []
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return routes;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ============================================================================
|
|
296
|
-
// PYTHON: FLASK ROUTE DETECTION
|
|
297
|
-
// ============================================================================
|
|
298
|
-
|
|
299
|
-
async function resolveFlaskRoutes(repoRoot) {
|
|
300
|
-
const files = await fg(["**/*.py"], {
|
|
301
|
-
cwd: repoRoot,
|
|
302
|
-
absolute: true,
|
|
303
|
-
ignore: ["**/venv/**", "**/.venv/**", "**/env/**", "**/__pycache__/**", "**/site-packages/**", "**/test*/**"]
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
const routes = [];
|
|
307
|
-
|
|
308
|
-
// Flask route decorator pattern: @app.route('/path', methods=['GET', 'POST'])
|
|
309
|
-
const routePattern = /@(\w+)\.(route|get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
310
|
-
const methodsPattern = /methods\s*=\s*\[([^\]]+)\]/;
|
|
311
|
-
|
|
312
|
-
for (const fileAbs of files) {
|
|
313
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
314
|
-
const code = safeRead(fileAbs);
|
|
315
|
-
|
|
316
|
-
if (!code.includes("@") || (!code.includes("route") && !code.includes("flask"))) continue;
|
|
317
|
-
|
|
318
|
-
const lines = code.split(/\r?\n/);
|
|
319
|
-
|
|
320
|
-
for (let i = 0; i < lines.length; i++) {
|
|
321
|
-
const line = lines[i];
|
|
322
|
-
|
|
323
|
-
// Match @app.route('/path') or @blueprint.route('/path')
|
|
324
|
-
const routeMatch = line.match(/@(\w+)\.(route|get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/);
|
|
325
|
-
if (!routeMatch) continue;
|
|
326
|
-
|
|
327
|
-
const [, appName, decorator, routePath] = routeMatch;
|
|
328
|
-
let methods = ["GET"];
|
|
329
|
-
|
|
330
|
-
if (decorator !== "route") {
|
|
331
|
-
methods = [decorator.toUpperCase()];
|
|
332
|
-
} else {
|
|
333
|
-
// Check for methods parameter
|
|
334
|
-
const methodMatch = line.match(methodsPattern);
|
|
335
|
-
if (methodMatch) {
|
|
336
|
-
methods = methodMatch[1].replace(/['"]/g, "").split(",").map(m => m.trim().toUpperCase());
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
for (const method of methods) {
|
|
341
|
-
routes.push({
|
|
342
|
-
method: canonicalizeMethod(method),
|
|
343
|
-
path: canonicalizePath(routePath),
|
|
344
|
-
handler: fileRel,
|
|
345
|
-
framework: "flask",
|
|
346
|
-
confidence: "high",
|
|
347
|
-
evidence: [{
|
|
348
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
349
|
-
file: fileRel,
|
|
350
|
-
lines: `${i + 1}`,
|
|
351
|
-
reason: `Flask @${appName}.${decorator}("${routePath}")`
|
|
352
|
-
}]
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return routes;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// ============================================================================
|
|
362
|
-
// PYTHON: FASTAPI ROUTE DETECTION
|
|
363
|
-
// ============================================================================
|
|
364
|
-
|
|
365
|
-
async function resolveFastAPIRoutes(repoRoot) {
|
|
366
|
-
const files = await fg(["**/*.py"], {
|
|
367
|
-
cwd: repoRoot,
|
|
368
|
-
absolute: true,
|
|
369
|
-
ignore: ["**/venv/**", "**/.venv/**", "**/env/**", "**/__pycache__/**", "**/site-packages/**", "**/test*/**"]
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
const routes = [];
|
|
373
|
-
|
|
374
|
-
// FastAPI route decorator pattern: @app.get('/path') or @router.post('/path')
|
|
375
|
-
const routePattern = /@(\w+)\.(get|post|put|patch|delete|options|head)\s*\(\s*['"]([^'"]+)['"]/;
|
|
376
|
-
|
|
377
|
-
for (const fileAbs of files) {
|
|
378
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
379
|
-
const code = safeRead(fileAbs);
|
|
380
|
-
|
|
381
|
-
if (!code.includes("@") || !code.includes("fastapi")) continue;
|
|
382
|
-
|
|
383
|
-
const lines = code.split(/\r?\n/);
|
|
384
|
-
|
|
385
|
-
for (let i = 0; i < lines.length; i++) {
|
|
386
|
-
const line = lines[i];
|
|
387
|
-
|
|
388
|
-
const routeMatch = line.match(routePattern);
|
|
389
|
-
if (!routeMatch) continue;
|
|
390
|
-
|
|
391
|
-
const [, appName, method, routePath] = routeMatch;
|
|
392
|
-
|
|
393
|
-
routes.push({
|
|
394
|
-
method: canonicalizeMethod(method),
|
|
395
|
-
path: canonicalizePath(routePath),
|
|
396
|
-
handler: fileRel,
|
|
397
|
-
framework: "fastapi",
|
|
398
|
-
confidence: "high",
|
|
399
|
-
evidence: [{
|
|
400
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
401
|
-
file: fileRel,
|
|
402
|
-
lines: `${i + 1}`,
|
|
403
|
-
reason: `FastAPI @${appName}.${method}("${routePath}")`
|
|
404
|
-
}]
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return routes;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// ============================================================================
|
|
413
|
-
// PYTHON: DJANGO ROUTE DETECTION
|
|
414
|
-
// ============================================================================
|
|
415
|
-
|
|
416
|
-
async function resolveDjangoRoutes(repoRoot) {
|
|
417
|
-
const files = await fg(["**/urls.py", "**/urls/*.py"], {
|
|
418
|
-
cwd: repoRoot,
|
|
419
|
-
absolute: true,
|
|
420
|
-
ignore: ["**/venv/**", "**/.venv/**", "**/env/**", "**/__pycache__/**", "**/site-packages/**"]
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
const routes = [];
|
|
424
|
-
|
|
425
|
-
// Django path pattern: path('route/', view, name='name')
|
|
426
|
-
const pathPattern = /path\s*\(\s*['"]([^'"]*)['"]/g;
|
|
427
|
-
const rePathPattern = /re_path\s*\(\s*r?['"]([^'"]+)['"]/g;
|
|
428
|
-
|
|
429
|
-
for (const fileAbs of files) {
|
|
430
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
431
|
-
const code = safeRead(fileAbs);
|
|
432
|
-
|
|
433
|
-
if (!code.includes("path(") && !code.includes("re_path(")) continue;
|
|
434
|
-
|
|
435
|
-
const lines = code.split(/\r?\n/);
|
|
436
|
-
|
|
437
|
-
for (let i = 0; i < lines.length; i++) {
|
|
438
|
-
const line = lines[i];
|
|
439
|
-
|
|
440
|
-
// Match path('route/', ...)
|
|
441
|
-
const pathMatch = line.match(/path\s*\(\s*['"]([^'"]*)['"]/);
|
|
442
|
-
if (pathMatch) {
|
|
443
|
-
const routePath = pathMatch[1];
|
|
444
|
-
routes.push({
|
|
445
|
-
method: "*",
|
|
446
|
-
path: canonicalizePath("/" + routePath),
|
|
447
|
-
handler: fileRel,
|
|
448
|
-
framework: "django",
|
|
449
|
-
confidence: "med",
|
|
450
|
-
evidence: [{
|
|
451
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
452
|
-
file: fileRel,
|
|
453
|
-
lines: `${i + 1}`,
|
|
454
|
-
reason: `Django path("${routePath}")`
|
|
455
|
-
}]
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Match re_path(r'^route/', ...)
|
|
460
|
-
const rePathMatch = line.match(/re_path\s*\(\s*r?['"]([^'"]+)['"]/);
|
|
461
|
-
if (rePathMatch) {
|
|
462
|
-
let routePath = rePathMatch[1].replace(/^\^/, "").replace(/\$$/, "");
|
|
463
|
-
routes.push({
|
|
464
|
-
method: "*",
|
|
465
|
-
path: canonicalizePath("/" + routePath),
|
|
466
|
-
handler: fileRel,
|
|
467
|
-
framework: "django",
|
|
468
|
-
confidence: "med",
|
|
469
|
-
evidence: [{
|
|
470
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
471
|
-
file: fileRel,
|
|
472
|
-
lines: `${i + 1}`,
|
|
473
|
-
reason: `Django re_path("${routePath}")`
|
|
474
|
-
}]
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return routes;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// ============================================================================
|
|
484
|
-
// HONO ROUTE DETECTION
|
|
485
|
-
// ============================================================================
|
|
486
|
-
|
|
487
|
-
async function resolveHonoRoutes(repoRoot) {
|
|
488
|
-
const files = await fg(["**/*.{ts,js}"], {
|
|
489
|
-
cwd: repoRoot,
|
|
490
|
-
absolute: true,
|
|
491
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/test/**"]
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
const routes = [];
|
|
495
|
-
const HONO_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
|
|
496
|
-
|
|
497
|
-
for (const fileAbs of files) {
|
|
498
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
499
|
-
const code = safeRead(fileAbs);
|
|
500
|
-
|
|
501
|
-
if (!code.includes("Hono") && !code.includes("hono")) continue;
|
|
502
|
-
|
|
503
|
-
let ast;
|
|
504
|
-
try { ast = parseJS(code); } catch { continue; }
|
|
505
|
-
|
|
506
|
-
const honoNames = new Set(["app"]);
|
|
507
|
-
|
|
508
|
-
traverse(ast, {
|
|
509
|
-
VariableDeclarator(p) {
|
|
510
|
-
if (!t.isIdentifier(p.node.id)) return;
|
|
511
|
-
const init = p.node.init;
|
|
512
|
-
if (!init) return;
|
|
513
|
-
if (t.isNewExpression(init) && t.isIdentifier(init.callee) && init.callee.name === "Hono") {
|
|
514
|
-
honoNames.add(p.node.id.name);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
traverse(ast, {
|
|
520
|
-
CallExpression(p) {
|
|
521
|
-
const callee = p.node.callee;
|
|
522
|
-
if (!t.isMemberExpression(callee)) return;
|
|
523
|
-
if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
|
|
524
|
-
|
|
525
|
-
const obj = callee.object.name;
|
|
526
|
-
const method = callee.property.name;
|
|
527
|
-
|
|
528
|
-
if (!honoNames.has(obj)) return;
|
|
529
|
-
if (!HONO_METHODS.has(method)) return;
|
|
530
|
-
|
|
531
|
-
const arg0 = p.node.arguments[0];
|
|
532
|
-
if (!t.isStringLiteral(arg0)) return;
|
|
533
|
-
|
|
534
|
-
const ev = evidenceFromLoc({
|
|
535
|
-
fileAbs, fileRel, loc: p.node.loc,
|
|
536
|
-
reason: `Hono ${method.toUpperCase()}("${arg0.value}")`
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
routes.push({
|
|
540
|
-
method: canonicalizeMethod(method),
|
|
541
|
-
path: canonicalizePath(arg0.value),
|
|
542
|
-
handler: fileRel,
|
|
543
|
-
framework: "hono",
|
|
544
|
-
confidence: "high",
|
|
545
|
-
evidence: ev ? [ev] : []
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
return routes;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// ============================================================================
|
|
555
|
-
// KOA ROUTE DETECTION
|
|
556
|
-
// ============================================================================
|
|
557
|
-
|
|
558
|
-
async function resolveKoaRoutes(repoRoot) {
|
|
559
|
-
const files = await fg(["**/*.{ts,js}"], {
|
|
560
|
-
cwd: repoRoot,
|
|
561
|
-
absolute: true,
|
|
562
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/test/**"]
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
const routes = [];
|
|
566
|
-
const KOA_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
|
|
567
|
-
|
|
568
|
-
for (const fileAbs of files) {
|
|
569
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
570
|
-
const code = safeRead(fileAbs);
|
|
571
|
-
|
|
572
|
-
if (!code.includes("koa") && !code.includes("Router")) continue;
|
|
573
|
-
|
|
574
|
-
let ast;
|
|
575
|
-
try { ast = parseJS(code); } catch { continue; }
|
|
576
|
-
|
|
577
|
-
traverse(ast, {
|
|
578
|
-
CallExpression(p) {
|
|
579
|
-
const callee = p.node.callee;
|
|
580
|
-
if (!t.isMemberExpression(callee)) return;
|
|
581
|
-
if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
|
|
582
|
-
|
|
583
|
-
const obj = callee.object.name;
|
|
584
|
-
const method = callee.property.name;
|
|
585
|
-
|
|
586
|
-
if (!obj.toLowerCase().includes("router")) return;
|
|
587
|
-
if (!KOA_METHODS.has(method)) return;
|
|
588
|
-
|
|
589
|
-
const arg0 = p.node.arguments[0];
|
|
590
|
-
if (!t.isStringLiteral(arg0)) return;
|
|
591
|
-
|
|
592
|
-
const ev = evidenceFromLoc({
|
|
593
|
-
fileAbs, fileRel, loc: p.node.loc,
|
|
594
|
-
reason: `Koa router.${method}("${arg0.value}")`
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
routes.push({
|
|
598
|
-
method: canonicalizeMethod(method),
|
|
599
|
-
path: canonicalizePath(arg0.value),
|
|
600
|
-
handler: fileRel,
|
|
601
|
-
framework: "koa",
|
|
602
|
-
confidence: "high",
|
|
603
|
-
evidence: ev ? [ev] : []
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
return routes;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// ============================================================================
|
|
613
|
-
// OPENAPI/SWAGGER SPEC PARSING
|
|
614
|
-
// ============================================================================
|
|
615
|
-
|
|
616
|
-
async function resolveOpenAPIRoutes(repoRoot) {
|
|
617
|
-
const files = await fg([
|
|
618
|
-
"**/openapi.json",
|
|
619
|
-
"**/openapi.yaml",
|
|
620
|
-
"**/openapi.yml",
|
|
621
|
-
"**/swagger.json",
|
|
622
|
-
"**/swagger.yaml",
|
|
623
|
-
"**/swagger.yml",
|
|
624
|
-
"**/api-spec.json",
|
|
625
|
-
"**/api-spec.yaml"
|
|
626
|
-
], {
|
|
627
|
-
cwd: repoRoot,
|
|
628
|
-
absolute: true,
|
|
629
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
const routes = [];
|
|
633
|
-
|
|
634
|
-
for (const fileAbs of files) {
|
|
635
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
636
|
-
const content = safeRead(fileAbs);
|
|
637
|
-
|
|
638
|
-
let spec;
|
|
639
|
-
try {
|
|
640
|
-
if (fileAbs.endsWith(".json")) {
|
|
641
|
-
spec = JSON.parse(content);
|
|
642
|
-
} else {
|
|
643
|
-
// Simple YAML parsing for common patterns
|
|
644
|
-
const lines = content.split(/\r?\n/);
|
|
645
|
-
const pathMatches = [];
|
|
646
|
-
let currentPath = null;
|
|
647
|
-
|
|
648
|
-
for (const line of lines) {
|
|
649
|
-
// Match path definitions like " /api/users:"
|
|
650
|
-
const pathMatch = line.match(/^ ['"]?(\/[^'":\s]+)['"]?:\s*$/);
|
|
651
|
-
if (pathMatch) {
|
|
652
|
-
currentPath = pathMatch[1];
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
// Match HTTP methods like " get:" or " post:"
|
|
656
|
-
if (currentPath) {
|
|
657
|
-
const methodMatch = line.match(/^\s{4}(get|post|put|patch|delete|options|head):\s*$/);
|
|
658
|
-
if (methodMatch) {
|
|
659
|
-
pathMatches.push({ path: currentPath, method: methodMatch[1] });
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
for (const { path: p, method } of pathMatches) {
|
|
665
|
-
routes.push({
|
|
666
|
-
method: canonicalizeMethod(method),
|
|
667
|
-
path: canonicalizePath(p),
|
|
668
|
-
handler: fileRel,
|
|
669
|
-
framework: "openapi",
|
|
670
|
-
confidence: "high",
|
|
671
|
-
evidence: [{
|
|
672
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
673
|
-
file: fileRel,
|
|
674
|
-
lines: "1",
|
|
675
|
-
reason: `OpenAPI spec ${method.toUpperCase()} ${p}`
|
|
676
|
-
}]
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
} catch { continue; }
|
|
682
|
-
|
|
683
|
-
// Parse JSON OpenAPI spec
|
|
684
|
-
if (spec && spec.paths) {
|
|
685
|
-
for (const [pathStr, pathItem] of Object.entries(spec.paths)) {
|
|
686
|
-
const methods = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
687
|
-
for (const method of methods) {
|
|
688
|
-
if (pathItem[method]) {
|
|
689
|
-
routes.push({
|
|
690
|
-
method: canonicalizeMethod(method),
|
|
691
|
-
path: canonicalizePath(pathStr),
|
|
692
|
-
handler: fileRel,
|
|
693
|
-
framework: "openapi",
|
|
694
|
-
confidence: "high",
|
|
695
|
-
evidence: [{
|
|
696
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
697
|
-
file: fileRel,
|
|
698
|
-
lines: "1",
|
|
699
|
-
reason: `OpenAPI spec ${method.toUpperCase()} ${pathStr}`
|
|
700
|
-
}]
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
return routes;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// ============================================================================
|
|
712
|
-
// GRAPHQL ENDPOINT DETECTION
|
|
713
|
-
// ============================================================================
|
|
714
|
-
|
|
715
|
-
async function resolveGraphQLRoutes(repoRoot) {
|
|
716
|
-
const routes = [];
|
|
717
|
-
|
|
718
|
-
// Find GraphQL schema files
|
|
719
|
-
const schemaFiles = await fg([
|
|
720
|
-
"**/*.graphql",
|
|
721
|
-
"**/*.gql",
|
|
722
|
-
"**/schema.graphql",
|
|
723
|
-
"**/schema.gql"
|
|
724
|
-
], {
|
|
725
|
-
cwd: repoRoot,
|
|
726
|
-
absolute: true,
|
|
727
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
// Find GraphQL server setup files
|
|
731
|
-
const serverFiles = await fg([
|
|
732
|
-
"**/*.{ts,js,py}"
|
|
733
|
-
], {
|
|
734
|
-
cwd: repoRoot,
|
|
735
|
-
absolute: true,
|
|
736
|
-
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/test/**", "**/*.d.ts"]
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
// Check for GraphQL endpoints in server files
|
|
740
|
-
for (const fileAbs of serverFiles) {
|
|
741
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
742
|
-
const code = safeRead(fileAbs);
|
|
743
|
-
|
|
744
|
-
// Skip if no GraphQL indicators
|
|
745
|
-
if (!code.includes("graphql") && !code.includes("GraphQL") && !code.includes("gql")) continue;
|
|
746
|
-
|
|
747
|
-
// Detect common GraphQL endpoint patterns
|
|
748
|
-
const patterns = [
|
|
749
|
-
// Apollo Server: app.use('/graphql', ...)
|
|
750
|
-
/(?:app|server|router)\.(use|post|get)\s*\(\s*['"]([^'"]*graphql[^'"]*)['"]/gi,
|
|
751
|
-
// Express GraphQL: graphqlHTTP({ ... })
|
|
752
|
-
/graphqlHTTP\s*\(/gi,
|
|
753
|
-
// Apollo Server: new ApolloServer
|
|
754
|
-
/new\s+ApolloServer/gi,
|
|
755
|
-
// Yoga: createYoga
|
|
756
|
-
/createYoga\s*\(/gi,
|
|
757
|
-
// Python Strawberry/Ariadne
|
|
758
|
-
/GraphQLRouter|graphql_app|make_executable_schema/gi
|
|
759
|
-
];
|
|
760
|
-
|
|
761
|
-
let hasGraphQL = false;
|
|
762
|
-
let endpointPath = "/graphql"; // Default
|
|
763
|
-
|
|
764
|
-
for (const pattern of patterns) {
|
|
765
|
-
const match = code.match(pattern);
|
|
766
|
-
if (match) {
|
|
767
|
-
hasGraphQL = true;
|
|
768
|
-
// Try to extract custom endpoint path
|
|
769
|
-
const pathMatch = code.match(/['"]([^'"]*graphql[^'"]*)['"]/i);
|
|
770
|
-
if (pathMatch) {
|
|
771
|
-
endpointPath = pathMatch[1];
|
|
772
|
-
}
|
|
773
|
-
break;
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
if (hasGraphQL) {
|
|
778
|
-
routes.push({
|
|
779
|
-
method: "POST",
|
|
780
|
-
path: canonicalizePath(endpointPath),
|
|
781
|
-
handler: fileRel,
|
|
782
|
-
framework: "graphql",
|
|
783
|
-
confidence: "high",
|
|
784
|
-
evidence: [{
|
|
785
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
786
|
-
file: fileRel,
|
|
787
|
-
lines: "1",
|
|
788
|
-
reason: `GraphQL endpoint detected`
|
|
789
|
-
}]
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
// GraphQL also supports GET for queries
|
|
793
|
-
routes.push({
|
|
794
|
-
method: "GET",
|
|
795
|
-
path: canonicalizePath(endpointPath),
|
|
796
|
-
handler: fileRel,
|
|
797
|
-
framework: "graphql",
|
|
798
|
-
confidence: "med",
|
|
799
|
-
evidence: [{
|
|
800
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
801
|
-
file: fileRel,
|
|
802
|
-
lines: "1",
|
|
803
|
-
reason: `GraphQL endpoint (GET for queries)`
|
|
804
|
-
}]
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// If schema files exist, assume /graphql endpoint
|
|
810
|
-
if (schemaFiles.length > 0 && routes.length === 0) {
|
|
811
|
-
const fileRel = path.relative(repoRoot, schemaFiles[0]).replace(/\\/g, "/");
|
|
812
|
-
routes.push({
|
|
813
|
-
method: "*",
|
|
814
|
-
path: "/graphql",
|
|
815
|
-
handler: fileRel,
|
|
816
|
-
framework: "graphql",
|
|
817
|
-
confidence: "med",
|
|
818
|
-
evidence: [{
|
|
819
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
820
|
-
file: fileRel,
|
|
821
|
-
lines: "1",
|
|
822
|
-
reason: `GraphQL schema file found`
|
|
823
|
-
}]
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
return routes;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// ============================================================================
|
|
831
|
-
// GO ROUTE DETECTION (Gin, Echo, Fiber)
|
|
832
|
-
// ============================================================================
|
|
833
|
-
|
|
834
|
-
async function resolveGoRoutes(repoRoot) {
|
|
835
|
-
const files = await fg(["**/*.go"], {
|
|
836
|
-
cwd: repoRoot,
|
|
837
|
-
absolute: true,
|
|
838
|
-
ignore: ["**/vendor/**", "**/*_test.go", "**/testdata/**"]
|
|
839
|
-
});
|
|
840
|
-
|
|
841
|
-
const routes = [];
|
|
842
|
-
const GO_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "Any", "Handle"];
|
|
843
|
-
|
|
844
|
-
for (const fileAbs of files) {
|
|
845
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
846
|
-
const code = safeRead(fileAbs);
|
|
847
|
-
|
|
848
|
-
// Skip if no router indicators
|
|
849
|
-
if (!code.includes("gin") && !code.includes("echo") && !code.includes("fiber") &&
|
|
850
|
-
!code.includes("mux") && !code.includes("chi") && !code.includes("http.Handle")) continue;
|
|
851
|
-
|
|
852
|
-
const lines = code.split(/\r?\n/);
|
|
853
|
-
|
|
854
|
-
for (let i = 0; i < lines.length; i++) {
|
|
855
|
-
const line = lines[i];
|
|
856
|
-
|
|
857
|
-
// Gin: r.GET("/path", handler)
|
|
858
|
-
// Echo: e.GET("/path", handler)
|
|
859
|
-
// Fiber: app.Get("/path", handler)
|
|
860
|
-
const ginEchoMatch = line.match(/(\w+)\.(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD|Any|Handle)\s*\(\s*["'`]([^"'`]+)["'`]/i);
|
|
861
|
-
if (ginEchoMatch) {
|
|
862
|
-
const [, , method, routePath] = ginEchoMatch;
|
|
863
|
-
routes.push({
|
|
864
|
-
method: canonicalizeMethod(method === "Any" || method === "Handle" ? "*" : method),
|
|
865
|
-
path: canonicalizePath(routePath),
|
|
866
|
-
handler: fileRel,
|
|
867
|
-
framework: "go",
|
|
868
|
-
confidence: "high",
|
|
869
|
-
evidence: [{
|
|
870
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
871
|
-
file: fileRel,
|
|
872
|
-
lines: `${i + 1}`,
|
|
873
|
-
reason: `Go ${method}("${routePath}")`
|
|
874
|
-
}]
|
|
875
|
-
});
|
|
876
|
-
continue;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// Fiber lowercase: app.Get("/path", handler)
|
|
880
|
-
const fiberMatch = line.match(/(\w+)\.(Get|Post|Put|Patch|Delete|Options|Head|All)\s*\(\s*["'`]([^"'`]+)["'`]/);
|
|
881
|
-
if (fiberMatch) {
|
|
882
|
-
const [, , method, routePath] = fiberMatch;
|
|
883
|
-
routes.push({
|
|
884
|
-
method: canonicalizeMethod(method === "All" ? "*" : method),
|
|
885
|
-
path: canonicalizePath(routePath),
|
|
886
|
-
handler: fileRel,
|
|
887
|
-
framework: "fiber",
|
|
888
|
-
confidence: "high",
|
|
889
|
-
evidence: [{
|
|
890
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
891
|
-
file: fileRel,
|
|
892
|
-
lines: `${i + 1}`,
|
|
893
|
-
reason: `Fiber ${method}("${routePath}")`
|
|
894
|
-
}]
|
|
895
|
-
});
|
|
896
|
-
continue;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// Chi router: r.Get("/path", handler)
|
|
900
|
-
const chiMatch = line.match(/(\w+)\.(Get|Post|Put|Patch|Delete|Options|Head|Connect|Trace|MethodFunc)\s*\(\s*["'`]([^"'`]+)["'`]/);
|
|
901
|
-
if (chiMatch) {
|
|
902
|
-
const [, , method, routePath] = chiMatch;
|
|
903
|
-
routes.push({
|
|
904
|
-
method: canonicalizeMethod(method),
|
|
905
|
-
path: canonicalizePath(routePath),
|
|
906
|
-
handler: fileRel,
|
|
907
|
-
framework: "chi",
|
|
908
|
-
confidence: "high",
|
|
909
|
-
evidence: [{
|
|
910
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
911
|
-
file: fileRel,
|
|
912
|
-
lines: `${i + 1}`,
|
|
913
|
-
reason: `Chi ${method}("${routePath}")`
|
|
914
|
-
}]
|
|
915
|
-
});
|
|
916
|
-
continue;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// Gorilla mux: r.HandleFunc("/path", handler).Methods("GET")
|
|
920
|
-
const muxMatch = line.match(/HandleFunc\s*\(\s*["'`]([^"'`]+)["'`]/);
|
|
921
|
-
if (muxMatch) {
|
|
922
|
-
const routePath = muxMatch[1];
|
|
923
|
-
// Try to find .Methods() on same or next line
|
|
924
|
-
const methodMatch = (line + (lines[i + 1] || "")).match(/\.Methods\s*\(\s*["'`]([^"'`]+)["'`]/);
|
|
925
|
-
const method = methodMatch ? methodMatch[1] : "*";
|
|
926
|
-
|
|
927
|
-
routes.push({
|
|
928
|
-
method: canonicalizeMethod(method),
|
|
929
|
-
path: canonicalizePath(routePath),
|
|
930
|
-
handler: fileRel,
|
|
931
|
-
framework: "gorilla",
|
|
932
|
-
confidence: "high",
|
|
933
|
-
evidence: [{
|
|
934
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
935
|
-
file: fileRel,
|
|
936
|
-
lines: `${i + 1}`,
|
|
937
|
-
reason: `Gorilla mux HandleFunc("${routePath}")`
|
|
938
|
-
}]
|
|
939
|
-
});
|
|
940
|
-
continue;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// Standard library: http.HandleFunc("/path", handler)
|
|
944
|
-
const stdMatch = line.match(/http\.HandleFunc\s*\(\s*["'`]([^"'`]+)["'`]/);
|
|
945
|
-
if (stdMatch) {
|
|
946
|
-
routes.push({
|
|
947
|
-
method: "*",
|
|
948
|
-
path: canonicalizePath(stdMatch[1]),
|
|
949
|
-
handler: fileRel,
|
|
950
|
-
framework: "go-stdlib",
|
|
951
|
-
confidence: "high",
|
|
952
|
-
evidence: [{
|
|
953
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
954
|
-
file: fileRel,
|
|
955
|
-
lines: `${i + 1}`,
|
|
956
|
-
reason: `Go http.HandleFunc("${stdMatch[1]}")`
|
|
957
|
-
}]
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
return routes;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
// ============================================================================
|
|
967
|
-
// PYTHON CLIENT REFS (requests, httpx, aiohttp)
|
|
968
|
-
// ============================================================================
|
|
969
|
-
|
|
970
|
-
async function resolvePythonClientRefs(repoRoot) {
|
|
971
|
-
const files = await fg(["**/*.py"], {
|
|
972
|
-
cwd: repoRoot,
|
|
973
|
-
absolute: true,
|
|
974
|
-
ignore: ["**/venv/**", "**/.venv/**", "**/env/**", "**/__pycache__/**", "**/site-packages/**", "**/test*/**"]
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
const refs = [];
|
|
978
|
-
|
|
979
|
-
// Match requests.get('/api/...'), httpx.post('/api/...'), etc.
|
|
980
|
-
const httpPattern = /(requests|httpx|aiohttp|session)\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
981
|
-
const fetchPattern = /fetch\s*\(\s*['"]([^'"]+)['"]/g;
|
|
982
|
-
|
|
983
|
-
for (const fileAbs of files) {
|
|
984
|
-
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
985
|
-
const code = safeRead(fileAbs);
|
|
986
|
-
|
|
987
|
-
const lines = code.split(/\r?\n/);
|
|
988
|
-
|
|
989
|
-
for (let i = 0; i < lines.length; i++) {
|
|
990
|
-
const line = lines[i];
|
|
991
|
-
|
|
992
|
-
// Match requests.get('/api/...')
|
|
993
|
-
const httpMatch = line.match(/(requests|httpx|aiohttp|session)\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/);
|
|
994
|
-
if (httpMatch) {
|
|
995
|
-
const [, lib, method, url] = httpMatch;
|
|
996
|
-
if (url.startsWith("/") || url.startsWith("http")) {
|
|
997
|
-
refs.push({
|
|
998
|
-
method: canonicalizeMethod(method),
|
|
999
|
-
path: canonicalizePath(url.replace(/^https?:\/\/[^\/]+/, "")),
|
|
1000
|
-
source: fileRel,
|
|
1001
|
-
confidence: "high",
|
|
1002
|
-
evidence: [{
|
|
1003
|
-
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
1004
|
-
file: fileRel,
|
|
1005
|
-
lines: `${i + 1}`,
|
|
1006
|
-
reason: `Python ${lib}.${method}("${url}")`
|
|
1007
|
-
}]
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
return refs;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
// ============================================================================
|
|
1018
|
-
// MAIN: RESOLVE ALL ROUTES
|
|
1019
|
-
// ============================================================================
|
|
1020
|
-
|
|
1021
|
-
async function resolveAllRoutes(repoRoot) {
|
|
1022
|
-
const frameworks = await detectFrameworks(repoRoot);
|
|
1023
|
-
|
|
1024
|
-
const allRoutes = [];
|
|
1025
|
-
const allClientRefs = [];
|
|
1026
|
-
const gaps = [];
|
|
1027
|
-
|
|
1028
|
-
console.log(` 📦 Detected frameworks: ${frameworks.length > 0 ? frameworks.join(", ") : "none detected, scanning all"}`);
|
|
1029
|
-
|
|
1030
|
-
// Always scan for routes regardless of detected frameworks
|
|
1031
|
-
// JavaScript/TypeScript frameworks
|
|
1032
|
-
try {
|
|
1033
|
-
const expressRoutes = await resolveExpressRoutes(repoRoot);
|
|
1034
|
-
if (expressRoutes.length > 0) {
|
|
1035
|
-
console.log(` ✓ Express: ${expressRoutes.length} routes`);
|
|
1036
|
-
allRoutes.push(...expressRoutes);
|
|
1037
|
-
}
|
|
1038
|
-
} catch (e) { gaps.push({ kind: "express_scan_error", error: e.message }); }
|
|
1039
|
-
|
|
1040
|
-
try {
|
|
1041
|
-
const honoRoutes = await resolveHonoRoutes(repoRoot);
|
|
1042
|
-
if (honoRoutes.length > 0) {
|
|
1043
|
-
console.log(` ✓ Hono: ${honoRoutes.length} routes`);
|
|
1044
|
-
allRoutes.push(...honoRoutes);
|
|
1045
|
-
}
|
|
1046
|
-
} catch (e) { gaps.push({ kind: "hono_scan_error", error: e.message }); }
|
|
1047
|
-
|
|
1048
|
-
try {
|
|
1049
|
-
const koaRoutes = await resolveKoaRoutes(repoRoot);
|
|
1050
|
-
if (koaRoutes.length > 0) {
|
|
1051
|
-
console.log(` ✓ Koa: ${koaRoutes.length} routes`);
|
|
1052
|
-
allRoutes.push(...koaRoutes);
|
|
1053
|
-
}
|
|
1054
|
-
} catch (e) { gaps.push({ kind: "koa_scan_error", error: e.message }); }
|
|
1055
|
-
|
|
1056
|
-
// Python frameworks
|
|
1057
|
-
try {
|
|
1058
|
-
const flaskRoutes = await resolveFlaskRoutes(repoRoot);
|
|
1059
|
-
if (flaskRoutes.length > 0) {
|
|
1060
|
-
console.log(` ✓ Flask: ${flaskRoutes.length} routes`);
|
|
1061
|
-
allRoutes.push(...flaskRoutes);
|
|
1062
|
-
}
|
|
1063
|
-
} catch (e) { gaps.push({ kind: "flask_scan_error", error: e.message }); }
|
|
1064
|
-
|
|
1065
|
-
try {
|
|
1066
|
-
const fastapiRoutes = await resolveFastAPIRoutes(repoRoot);
|
|
1067
|
-
if (fastapiRoutes.length > 0) {
|
|
1068
|
-
console.log(` ✓ FastAPI: ${fastapiRoutes.length} routes`);
|
|
1069
|
-
allRoutes.push(...fastapiRoutes);
|
|
1070
|
-
}
|
|
1071
|
-
} catch (e) { gaps.push({ kind: "fastapi_scan_error", error: e.message }); }
|
|
1072
|
-
|
|
1073
|
-
try {
|
|
1074
|
-
const djangoRoutes = await resolveDjangoRoutes(repoRoot);
|
|
1075
|
-
if (djangoRoutes.length > 0) {
|
|
1076
|
-
console.log(` ✓ Django: ${djangoRoutes.length} routes`);
|
|
1077
|
-
allRoutes.push(...djangoRoutes);
|
|
1078
|
-
}
|
|
1079
|
-
} catch (e) { gaps.push({ kind: "django_scan_error", error: e.message }); }
|
|
1080
|
-
|
|
1081
|
-
// Python client refs
|
|
1082
|
-
try {
|
|
1083
|
-
const pythonRefs = await resolvePythonClientRefs(repoRoot);
|
|
1084
|
-
if (pythonRefs.length > 0) {
|
|
1085
|
-
console.log(` ✓ Python client refs: ${pythonRefs.length}`);
|
|
1086
|
-
allClientRefs.push(...pythonRefs);
|
|
1087
|
-
}
|
|
1088
|
-
} catch (e) { gaps.push({ kind: "python_client_scan_error", error: e.message }); }
|
|
1089
|
-
|
|
1090
|
-
// OpenAPI/Swagger specs (high accuracy)
|
|
1091
|
-
try {
|
|
1092
|
-
const openapiRoutes = await resolveOpenAPIRoutes(repoRoot);
|
|
1093
|
-
if (openapiRoutes.length > 0) {
|
|
1094
|
-
console.log(` ✓ OpenAPI spec: ${openapiRoutes.length} routes`);
|
|
1095
|
-
allRoutes.push(...openapiRoutes);
|
|
1096
|
-
}
|
|
1097
|
-
} catch (e) { gaps.push({ kind: "openapi_scan_error", error: e.message }); }
|
|
1098
|
-
|
|
1099
|
-
// GraphQL endpoints
|
|
1100
|
-
try {
|
|
1101
|
-
const graphqlRoutes = await resolveGraphQLRoutes(repoRoot);
|
|
1102
|
-
if (graphqlRoutes.length > 0) {
|
|
1103
|
-
console.log(` ✓ GraphQL: ${graphqlRoutes.length} endpoints`);
|
|
1104
|
-
allRoutes.push(...graphqlRoutes);
|
|
1105
|
-
}
|
|
1106
|
-
} catch (e) { gaps.push({ kind: "graphql_scan_error", error: e.message }); }
|
|
1107
|
-
|
|
1108
|
-
// Go frameworks (Gin, Echo, Fiber, Chi, Gorilla, stdlib)
|
|
1109
|
-
try {
|
|
1110
|
-
const goRoutes = await resolveGoRoutes(repoRoot);
|
|
1111
|
-
if (goRoutes.length > 0) {
|
|
1112
|
-
console.log(` ✓ Go: ${goRoutes.length} routes`);
|
|
1113
|
-
allRoutes.push(...goRoutes);
|
|
1114
|
-
}
|
|
1115
|
-
} catch (e) { gaps.push({ kind: "go_scan_error", error: e.message }); }
|
|
1116
|
-
|
|
1117
|
-
return {
|
|
1118
|
-
routes: allRoutes,
|
|
1119
|
-
clientRefs: allClientRefs,
|
|
1120
|
-
gaps,
|
|
1121
|
-
frameworks
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
module.exports = {
|
|
1126
|
-
detectFrameworks,
|
|
1127
|
-
resolveExpressRoutes,
|
|
1128
|
-
resolveFlaskRoutes,
|
|
1129
|
-
resolveFastAPIRoutes,
|
|
1130
|
-
resolveDjangoRoutes,
|
|
1131
|
-
resolveHonoRoutes,
|
|
1132
|
-
resolveKoaRoutes,
|
|
1133
|
-
resolveOpenAPIRoutes,
|
|
1134
|
-
resolveGraphQLRoutes,
|
|
1135
|
-
resolveGoRoutes,
|
|
1136
|
-
resolvePythonClientRefs,
|
|
1137
|
-
resolveAllRoutes,
|
|
1138
|
-
canonicalizeMethod,
|
|
1139
|
-
canonicalizePath
|
|
1140
|
-
};
|
|
1
|
+
// bin/runners/lib/route-detection.js
|
|
2
|
+
// Multi-framework route detection: Express, Flask, FastAPI, Django, Hono, Koa, etc.
|
|
3
|
+
|
|
4
|
+
const fg = require("fast-glob");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const crypto = require("crypto");
|
|
8
|
+
const parser = require("@babel/parser");
|
|
9
|
+
const traverse = require("@babel/traverse").default;
|
|
10
|
+
const t = require("@babel/types");
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// HELPERS
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
function sha256(text) {
|
|
17
|
+
return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function canonicalizeMethod(m) {
|
|
21
|
+
const u = String(m || "").toUpperCase();
|
|
22
|
+
if (u === "ALL" || u === "ANY" || u === "*") return "*";
|
|
23
|
+
return u;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canonicalizePath(p) {
|
|
27
|
+
let s = String(p || "").trim();
|
|
28
|
+
if (!s.startsWith("/")) s = "/" + s;
|
|
29
|
+
s = s.replace(/\/+/g, "/");
|
|
30
|
+
// Next dynamic segments
|
|
31
|
+
s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, "*$1?");
|
|
32
|
+
s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1");
|
|
33
|
+
s = s.replace(/\[([^\]]+)\]/g, ":$1");
|
|
34
|
+
// Python path params: <id> or <int:id>
|
|
35
|
+
s = s.replace(/<(?:\w+:)?(\w+)>/g, ":$1");
|
|
36
|
+
// FastAPI path params: {id}
|
|
37
|
+
s = s.replace(/\{(\w+)\}/g, ":$1");
|
|
38
|
+
if (s.length > 1) s = s.replace(/\/$/, "");
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function joinPaths(prefix, p) {
|
|
43
|
+
const a = canonicalizePath(prefix || "/");
|
|
44
|
+
const b = canonicalizePath(p || "/");
|
|
45
|
+
if (a === "/") return b;
|
|
46
|
+
if (b === "/") return a;
|
|
47
|
+
return canonicalizePath(a + "/" + b);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function exists(p) {
|
|
51
|
+
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function safeRead(fileAbs) {
|
|
55
|
+
try { return fs.readFileSync(fileAbs, "utf8"); } catch { return ""; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseJS(code) {
|
|
59
|
+
return parser.parse(code, { sourceType: "unambiguous", plugins: ["typescript", "jsx"] });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function evidenceFromLoc({ fileAbs, fileRel, loc, reason }) {
|
|
63
|
+
if (!loc) return null;
|
|
64
|
+
try {
|
|
65
|
+
const lines = fs.readFileSync(fileAbs, "utf8").split(/\r?\n/);
|
|
66
|
+
const start = Math.max(1, loc.start?.line || 1);
|
|
67
|
+
const end = Math.max(start, loc.end?.line || start);
|
|
68
|
+
const snippet = lines.slice(start - 1, end).join("\n");
|
|
69
|
+
return {
|
|
70
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
71
|
+
file: fileRel,
|
|
72
|
+
lines: `${start}-${end}`,
|
|
73
|
+
snippetHash: sha256(snippet),
|
|
74
|
+
reason
|
|
75
|
+
};
|
|
76
|
+
} catch { return null; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// FRAMEWORK DETECTION
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
const FRAMEWORK_SIGNATURES = {
|
|
84
|
+
// JavaScript/TypeScript
|
|
85
|
+
express: ["express", "app.get", "app.post", "app.use", "router.get", "router.post"],
|
|
86
|
+
fastify: ["fastify", "Fastify", "fastify.get", "fastify.post", "fastify.register"],
|
|
87
|
+
koa: ["koa", "new Koa", "ctx.body", "router.get"],
|
|
88
|
+
hono: ["hono", "Hono", "app.get", "app.post"],
|
|
89
|
+
nestjs: ["@nestjs", "@Controller", "@Get", "@Post"],
|
|
90
|
+
nextjs: ["next", "next/server", "NextResponse"],
|
|
91
|
+
|
|
92
|
+
// Python
|
|
93
|
+
flask: ["flask", "Flask", "@app.route", "@blueprint.route"],
|
|
94
|
+
fastapi: ["fastapi", "FastAPI", "@app.get", "@app.post", "@router.get"],
|
|
95
|
+
django: ["django", "urlpatterns", "path(", "re_path("],
|
|
96
|
+
starlette: ["starlette", "Route(", "Mount("],
|
|
97
|
+
|
|
98
|
+
// Go
|
|
99
|
+
gin: ["gin-gonic", "gin.Default", "r.GET", "r.POST"],
|
|
100
|
+
echo: ["labstack/echo", "echo.New", "e.GET", "e.POST"],
|
|
101
|
+
fiber: ["gofiber/fiber", "fiber.New", "app.Get", "app.Post"],
|
|
102
|
+
|
|
103
|
+
// Ruby
|
|
104
|
+
rails: ["rails", "resources :", "get '", "post '"],
|
|
105
|
+
sinatra: ["sinatra", "get '", "post '"],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
async function detectFrameworks(repoRoot) {
|
|
109
|
+
const detected = new Set();
|
|
110
|
+
|
|
111
|
+
// Check package.json for JS frameworks
|
|
112
|
+
const pkgPath = path.join(repoRoot, "package.json");
|
|
113
|
+
if (exists(pkgPath)) {
|
|
114
|
+
try {
|
|
115
|
+
const pkg = JSON.parse(safeRead(pkgPath));
|
|
116
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
117
|
+
|
|
118
|
+
if (allDeps.express) detected.add("express");
|
|
119
|
+
if (allDeps.fastify) detected.add("fastify");
|
|
120
|
+
if (allDeps.koa) detected.add("koa");
|
|
121
|
+
if (allDeps.hono) detected.add("hono");
|
|
122
|
+
if (allDeps["@nestjs/core"]) detected.add("nestjs");
|
|
123
|
+
if (allDeps.next) detected.add("nextjs");
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check requirements.txt / pyproject.toml for Python
|
|
128
|
+
const reqPath = path.join(repoRoot, "requirements.txt");
|
|
129
|
+
if (exists(reqPath)) {
|
|
130
|
+
const req = safeRead(reqPath).toLowerCase();
|
|
131
|
+
if (req.includes("flask")) detected.add("flask");
|
|
132
|
+
if (req.includes("fastapi")) detected.add("fastapi");
|
|
133
|
+
if (req.includes("django")) detected.add("django");
|
|
134
|
+
if (req.includes("starlette")) detected.add("starlette");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const pyprojectPath = path.join(repoRoot, "pyproject.toml");
|
|
138
|
+
if (exists(pyprojectPath)) {
|
|
139
|
+
const pyp = safeRead(pyprojectPath).toLowerCase();
|
|
140
|
+
if (pyp.includes("flask")) detected.add("flask");
|
|
141
|
+
if (pyp.includes("fastapi")) detected.add("fastapi");
|
|
142
|
+
if (pyp.includes("django")) detected.add("django");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check Gemfile for Ruby
|
|
146
|
+
const gemfilePath = path.join(repoRoot, "Gemfile");
|
|
147
|
+
if (exists(gemfilePath)) {
|
|
148
|
+
const gem = safeRead(gemfilePath).toLowerCase();
|
|
149
|
+
if (gem.includes("rails")) detected.add("rails");
|
|
150
|
+
if (gem.includes("sinatra")) detected.add("sinatra");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check go.mod for Go
|
|
154
|
+
const gomodPath = path.join(repoRoot, "go.mod");
|
|
155
|
+
if (exists(gomodPath)) {
|
|
156
|
+
const gomod = safeRead(gomodPath).toLowerCase();
|
|
157
|
+
if (gomod.includes("gin-gonic")) detected.add("gin");
|
|
158
|
+
if (gomod.includes("labstack/echo")) detected.add("echo");
|
|
159
|
+
if (gomod.includes("gofiber/fiber")) detected.add("fiber");
|
|
160
|
+
if (gomod.includes("go-chi/chi")) detected.add("chi");
|
|
161
|
+
if (gomod.includes("gorilla/mux")) detected.add("gorilla");
|
|
162
|
+
detected.add("go");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check for OpenAPI/Swagger specs
|
|
166
|
+
const openapiFiles = ["openapi.json", "openapi.yaml", "openapi.yml", "swagger.json", "swagger.yaml"];
|
|
167
|
+
for (const f of openapiFiles) {
|
|
168
|
+
if (exists(path.join(repoRoot, f))) {
|
|
169
|
+
detected.add("openapi");
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check for GraphQL
|
|
175
|
+
if (exists(path.join(repoRoot, "schema.graphql")) || exists(path.join(repoRoot, "schema.gql"))) {
|
|
176
|
+
detected.add("graphql");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return Array.from(detected);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// EXPRESS ROUTE DETECTION
|
|
184
|
+
// ============================================================================
|
|
185
|
+
|
|
186
|
+
const EXPRESS_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "all", "use"]);
|
|
187
|
+
|
|
188
|
+
async function resolveExpressRoutes(repoRoot) {
|
|
189
|
+
// Find all potential Express/server files (comprehensive for monorepos)
|
|
190
|
+
const files = await fg([
|
|
191
|
+
"**/server.{ts,js}",
|
|
192
|
+
"**/app.{ts,js}",
|
|
193
|
+
"**/index.{ts,js}",
|
|
194
|
+
"**/routes/**/*.{ts,js}",
|
|
195
|
+
"**/api/**/*.{ts,js}",
|
|
196
|
+
"**/src/**/*.{ts,js}",
|
|
197
|
+
"**/apps/**/src/**/*.{ts,js}",
|
|
198
|
+
"**/packages/**/src/**/*.{ts,js}",
|
|
199
|
+
"**/services/**/*.{ts,js}"
|
|
200
|
+
], {
|
|
201
|
+
cwd: repoRoot,
|
|
202
|
+
absolute: true,
|
|
203
|
+
ignore: [
|
|
204
|
+
"**/node_modules/**",
|
|
205
|
+
"**/.next/**",
|
|
206
|
+
"**/dist/**",
|
|
207
|
+
"**/build/**",
|
|
208
|
+
"**/test/**",
|
|
209
|
+
"**/tests/**",
|
|
210
|
+
"**/__tests__/**",
|
|
211
|
+
"**/*.test.*",
|
|
212
|
+
"**/*.spec.*",
|
|
213
|
+
"**/jest.*",
|
|
214
|
+
"**/*.d.ts"
|
|
215
|
+
]
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const routes = [];
|
|
219
|
+
const appNames = new Set(["app", "router", "express", "server"]);
|
|
220
|
+
|
|
221
|
+
for (const fileAbs of files) {
|
|
222
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
223
|
+
const code = safeRead(fileAbs);
|
|
224
|
+
|
|
225
|
+
// Quick check if file uses Express
|
|
226
|
+
if (!code.includes("express") && !code.includes("app.") && !code.includes("router.")) continue;
|
|
227
|
+
|
|
228
|
+
let ast;
|
|
229
|
+
try { ast = parseJS(code); } catch { continue; }
|
|
230
|
+
|
|
231
|
+
// Detect Express app/router instances
|
|
232
|
+
traverse(ast, {
|
|
233
|
+
VariableDeclarator(p) {
|
|
234
|
+
if (!t.isIdentifier(p.node.id)) return;
|
|
235
|
+
const id = p.node.id.name;
|
|
236
|
+
const init = p.node.init;
|
|
237
|
+
if (!init) return;
|
|
238
|
+
|
|
239
|
+
// const app = express()
|
|
240
|
+
if (t.isCallExpression(init) && t.isIdentifier(init.callee) && init.callee.name === "express") {
|
|
241
|
+
appNames.add(id);
|
|
242
|
+
}
|
|
243
|
+
// const router = express.Router()
|
|
244
|
+
if (t.isCallExpression(init) && t.isMemberExpression(init.callee)) {
|
|
245
|
+
const obj = init.callee.object;
|
|
246
|
+
const prop = init.callee.property;
|
|
247
|
+
if (t.isIdentifier(obj) && obj.name === "express" && t.isIdentifier(prop) && prop.name === "Router") {
|
|
248
|
+
appNames.add(id);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Extract routes
|
|
255
|
+
traverse(ast, {
|
|
256
|
+
CallExpression(p) {
|
|
257
|
+
const callee = p.node.callee;
|
|
258
|
+
if (!t.isMemberExpression(callee)) return;
|
|
259
|
+
if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
|
|
260
|
+
|
|
261
|
+
const obj = callee.object.name;
|
|
262
|
+
const method = callee.property.name;
|
|
263
|
+
|
|
264
|
+
if (!appNames.has(obj)) return;
|
|
265
|
+
if (!EXPRESS_METHODS.has(method)) return;
|
|
266
|
+
|
|
267
|
+
const arg0 = p.node.arguments[0];
|
|
268
|
+
if (!t.isStringLiteral(arg0)) return;
|
|
269
|
+
|
|
270
|
+
const routePath = canonicalizePath(arg0.value);
|
|
271
|
+
|
|
272
|
+
// Skip middleware mounts without paths
|
|
273
|
+
if (method === "use" && !arg0.value.startsWith("/")) return;
|
|
274
|
+
|
|
275
|
+
const ev = evidenceFromLoc({
|
|
276
|
+
fileAbs, fileRel, loc: p.node.loc,
|
|
277
|
+
reason: `Express ${method.toUpperCase()}("${arg0.value}")`
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
routes.push({
|
|
281
|
+
method: canonicalizeMethod(method === "use" ? "*" : method),
|
|
282
|
+
path: routePath,
|
|
283
|
+
handler: fileRel,
|
|
284
|
+
framework: "express",
|
|
285
|
+
confidence: "high",
|
|
286
|
+
evidence: ev ? [ev] : []
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return routes;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// PYTHON: FLASK ROUTE DETECTION
|
|
297
|
+
// ============================================================================
|
|
298
|
+
|
|
299
|
+
async function resolveFlaskRoutes(repoRoot) {
|
|
300
|
+
const files = await fg(["**/*.py"], {
|
|
301
|
+
cwd: repoRoot,
|
|
302
|
+
absolute: true,
|
|
303
|
+
ignore: ["**/venv/**", "**/.venv/**", "**/env/**", "**/__pycache__/**", "**/site-packages/**", "**/test*/**"]
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const routes = [];
|
|
307
|
+
|
|
308
|
+
// Flask route decorator pattern: @app.route('/path', methods=['GET', 'POST'])
|
|
309
|
+
const routePattern = /@(\w+)\.(route|get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
310
|
+
const methodsPattern = /methods\s*=\s*\[([^\]]+)\]/;
|
|
311
|
+
|
|
312
|
+
for (const fileAbs of files) {
|
|
313
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
314
|
+
const code = safeRead(fileAbs);
|
|
315
|
+
|
|
316
|
+
if (!code.includes("@") || (!code.includes("route") && !code.includes("flask"))) continue;
|
|
317
|
+
|
|
318
|
+
const lines = code.split(/\r?\n/);
|
|
319
|
+
|
|
320
|
+
for (let i = 0; i < lines.length; i++) {
|
|
321
|
+
const line = lines[i];
|
|
322
|
+
|
|
323
|
+
// Match @app.route('/path') or @blueprint.route('/path')
|
|
324
|
+
const routeMatch = line.match(/@(\w+)\.(route|get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/);
|
|
325
|
+
if (!routeMatch) continue;
|
|
326
|
+
|
|
327
|
+
const [, appName, decorator, routePath] = routeMatch;
|
|
328
|
+
let methods = ["GET"];
|
|
329
|
+
|
|
330
|
+
if (decorator !== "route") {
|
|
331
|
+
methods = [decorator.toUpperCase()];
|
|
332
|
+
} else {
|
|
333
|
+
// Check for methods parameter
|
|
334
|
+
const methodMatch = line.match(methodsPattern);
|
|
335
|
+
if (methodMatch) {
|
|
336
|
+
methods = methodMatch[1].replace(/['"]/g, "").split(",").map(m => m.trim().toUpperCase());
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
for (const method of methods) {
|
|
341
|
+
routes.push({
|
|
342
|
+
method: canonicalizeMethod(method),
|
|
343
|
+
path: canonicalizePath(routePath),
|
|
344
|
+
handler: fileRel,
|
|
345
|
+
framework: "flask",
|
|
346
|
+
confidence: "high",
|
|
347
|
+
evidence: [{
|
|
348
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
349
|
+
file: fileRel,
|
|
350
|
+
lines: `${i + 1}`,
|
|
351
|
+
reason: `Flask @${appName}.${decorator}("${routePath}")`
|
|
352
|
+
}]
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return routes;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ============================================================================
|
|
362
|
+
// PYTHON: FASTAPI ROUTE DETECTION
|
|
363
|
+
// ============================================================================
|
|
364
|
+
|
|
365
|
+
async function resolveFastAPIRoutes(repoRoot) {
|
|
366
|
+
const files = await fg(["**/*.py"], {
|
|
367
|
+
cwd: repoRoot,
|
|
368
|
+
absolute: true,
|
|
369
|
+
ignore: ["**/venv/**", "**/.venv/**", "**/env/**", "**/__pycache__/**", "**/site-packages/**", "**/test*/**"]
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const routes = [];
|
|
373
|
+
|
|
374
|
+
// FastAPI route decorator pattern: @app.get('/path') or @router.post('/path')
|
|
375
|
+
const routePattern = /@(\w+)\.(get|post|put|patch|delete|options|head)\s*\(\s*['"]([^'"]+)['"]/;
|
|
376
|
+
|
|
377
|
+
for (const fileAbs of files) {
|
|
378
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
379
|
+
const code = safeRead(fileAbs);
|
|
380
|
+
|
|
381
|
+
if (!code.includes("@") || !code.includes("fastapi")) continue;
|
|
382
|
+
|
|
383
|
+
const lines = code.split(/\r?\n/);
|
|
384
|
+
|
|
385
|
+
for (let i = 0; i < lines.length; i++) {
|
|
386
|
+
const line = lines[i];
|
|
387
|
+
|
|
388
|
+
const routeMatch = line.match(routePattern);
|
|
389
|
+
if (!routeMatch) continue;
|
|
390
|
+
|
|
391
|
+
const [, appName, method, routePath] = routeMatch;
|
|
392
|
+
|
|
393
|
+
routes.push({
|
|
394
|
+
method: canonicalizeMethod(method),
|
|
395
|
+
path: canonicalizePath(routePath),
|
|
396
|
+
handler: fileRel,
|
|
397
|
+
framework: "fastapi",
|
|
398
|
+
confidence: "high",
|
|
399
|
+
evidence: [{
|
|
400
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
401
|
+
file: fileRel,
|
|
402
|
+
lines: `${i + 1}`,
|
|
403
|
+
reason: `FastAPI @${appName}.${method}("${routePath}")`
|
|
404
|
+
}]
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return routes;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ============================================================================
|
|
413
|
+
// PYTHON: DJANGO ROUTE DETECTION
|
|
414
|
+
// ============================================================================
|
|
415
|
+
|
|
416
|
+
async function resolveDjangoRoutes(repoRoot) {
|
|
417
|
+
const files = await fg(["**/urls.py", "**/urls/*.py"], {
|
|
418
|
+
cwd: repoRoot,
|
|
419
|
+
absolute: true,
|
|
420
|
+
ignore: ["**/venv/**", "**/.venv/**", "**/env/**", "**/__pycache__/**", "**/site-packages/**"]
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const routes = [];
|
|
424
|
+
|
|
425
|
+
// Django path pattern: path('route/', view, name='name')
|
|
426
|
+
const pathPattern = /path\s*\(\s*['"]([^'"]*)['"]/g;
|
|
427
|
+
const rePathPattern = /re_path\s*\(\s*r?['"]([^'"]+)['"]/g;
|
|
428
|
+
|
|
429
|
+
for (const fileAbs of files) {
|
|
430
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
431
|
+
const code = safeRead(fileAbs);
|
|
432
|
+
|
|
433
|
+
if (!code.includes("path(") && !code.includes("re_path(")) continue;
|
|
434
|
+
|
|
435
|
+
const lines = code.split(/\r?\n/);
|
|
436
|
+
|
|
437
|
+
for (let i = 0; i < lines.length; i++) {
|
|
438
|
+
const line = lines[i];
|
|
439
|
+
|
|
440
|
+
// Match path('route/', ...)
|
|
441
|
+
const pathMatch = line.match(/path\s*\(\s*['"]([^'"]*)['"]/);
|
|
442
|
+
if (pathMatch) {
|
|
443
|
+
const routePath = pathMatch[1];
|
|
444
|
+
routes.push({
|
|
445
|
+
method: "*",
|
|
446
|
+
path: canonicalizePath("/" + routePath),
|
|
447
|
+
handler: fileRel,
|
|
448
|
+
framework: "django",
|
|
449
|
+
confidence: "med",
|
|
450
|
+
evidence: [{
|
|
451
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
452
|
+
file: fileRel,
|
|
453
|
+
lines: `${i + 1}`,
|
|
454
|
+
reason: `Django path("${routePath}")`
|
|
455
|
+
}]
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Match re_path(r'^route/', ...)
|
|
460
|
+
const rePathMatch = line.match(/re_path\s*\(\s*r?['"]([^'"]+)['"]/);
|
|
461
|
+
if (rePathMatch) {
|
|
462
|
+
let routePath = rePathMatch[1].replace(/^\^/, "").replace(/\$$/, "");
|
|
463
|
+
routes.push({
|
|
464
|
+
method: "*",
|
|
465
|
+
path: canonicalizePath("/" + routePath),
|
|
466
|
+
handler: fileRel,
|
|
467
|
+
framework: "django",
|
|
468
|
+
confidence: "med",
|
|
469
|
+
evidence: [{
|
|
470
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
471
|
+
file: fileRel,
|
|
472
|
+
lines: `${i + 1}`,
|
|
473
|
+
reason: `Django re_path("${routePath}")`
|
|
474
|
+
}]
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return routes;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ============================================================================
|
|
484
|
+
// HONO ROUTE DETECTION
|
|
485
|
+
// ============================================================================
|
|
486
|
+
|
|
487
|
+
async function resolveHonoRoutes(repoRoot) {
|
|
488
|
+
const files = await fg(["**/*.{ts,js}"], {
|
|
489
|
+
cwd: repoRoot,
|
|
490
|
+
absolute: true,
|
|
491
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/test/**"]
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const routes = [];
|
|
495
|
+
const HONO_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
|
|
496
|
+
|
|
497
|
+
for (const fileAbs of files) {
|
|
498
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
499
|
+
const code = safeRead(fileAbs);
|
|
500
|
+
|
|
501
|
+
if (!code.includes("Hono") && !code.includes("hono")) continue;
|
|
502
|
+
|
|
503
|
+
let ast;
|
|
504
|
+
try { ast = parseJS(code); } catch { continue; }
|
|
505
|
+
|
|
506
|
+
const honoNames = new Set(["app"]);
|
|
507
|
+
|
|
508
|
+
traverse(ast, {
|
|
509
|
+
VariableDeclarator(p) {
|
|
510
|
+
if (!t.isIdentifier(p.node.id)) return;
|
|
511
|
+
const init = p.node.init;
|
|
512
|
+
if (!init) return;
|
|
513
|
+
if (t.isNewExpression(init) && t.isIdentifier(init.callee) && init.callee.name === "Hono") {
|
|
514
|
+
honoNames.add(p.node.id.name);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
traverse(ast, {
|
|
520
|
+
CallExpression(p) {
|
|
521
|
+
const callee = p.node.callee;
|
|
522
|
+
if (!t.isMemberExpression(callee)) return;
|
|
523
|
+
if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
|
|
524
|
+
|
|
525
|
+
const obj = callee.object.name;
|
|
526
|
+
const method = callee.property.name;
|
|
527
|
+
|
|
528
|
+
if (!honoNames.has(obj)) return;
|
|
529
|
+
if (!HONO_METHODS.has(method)) return;
|
|
530
|
+
|
|
531
|
+
const arg0 = p.node.arguments[0];
|
|
532
|
+
if (!t.isStringLiteral(arg0)) return;
|
|
533
|
+
|
|
534
|
+
const ev = evidenceFromLoc({
|
|
535
|
+
fileAbs, fileRel, loc: p.node.loc,
|
|
536
|
+
reason: `Hono ${method.toUpperCase()}("${arg0.value}")`
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
routes.push({
|
|
540
|
+
method: canonicalizeMethod(method),
|
|
541
|
+
path: canonicalizePath(arg0.value),
|
|
542
|
+
handler: fileRel,
|
|
543
|
+
framework: "hono",
|
|
544
|
+
confidence: "high",
|
|
545
|
+
evidence: ev ? [ev] : []
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return routes;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ============================================================================
|
|
555
|
+
// KOA ROUTE DETECTION
|
|
556
|
+
// ============================================================================
|
|
557
|
+
|
|
558
|
+
async function resolveKoaRoutes(repoRoot) {
|
|
559
|
+
const files = await fg(["**/*.{ts,js}"], {
|
|
560
|
+
cwd: repoRoot,
|
|
561
|
+
absolute: true,
|
|
562
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/test/**"]
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
const routes = [];
|
|
566
|
+
const KOA_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
|
|
567
|
+
|
|
568
|
+
for (const fileAbs of files) {
|
|
569
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
570
|
+
const code = safeRead(fileAbs);
|
|
571
|
+
|
|
572
|
+
if (!code.includes("koa") && !code.includes("Router")) continue;
|
|
573
|
+
|
|
574
|
+
let ast;
|
|
575
|
+
try { ast = parseJS(code); } catch { continue; }
|
|
576
|
+
|
|
577
|
+
traverse(ast, {
|
|
578
|
+
CallExpression(p) {
|
|
579
|
+
const callee = p.node.callee;
|
|
580
|
+
if (!t.isMemberExpression(callee)) return;
|
|
581
|
+
if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
|
|
582
|
+
|
|
583
|
+
const obj = callee.object.name;
|
|
584
|
+
const method = callee.property.name;
|
|
585
|
+
|
|
586
|
+
if (!obj.toLowerCase().includes("router")) return;
|
|
587
|
+
if (!KOA_METHODS.has(method)) return;
|
|
588
|
+
|
|
589
|
+
const arg0 = p.node.arguments[0];
|
|
590
|
+
if (!t.isStringLiteral(arg0)) return;
|
|
591
|
+
|
|
592
|
+
const ev = evidenceFromLoc({
|
|
593
|
+
fileAbs, fileRel, loc: p.node.loc,
|
|
594
|
+
reason: `Koa router.${method}("${arg0.value}")`
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
routes.push({
|
|
598
|
+
method: canonicalizeMethod(method),
|
|
599
|
+
path: canonicalizePath(arg0.value),
|
|
600
|
+
handler: fileRel,
|
|
601
|
+
framework: "koa",
|
|
602
|
+
confidence: "high",
|
|
603
|
+
evidence: ev ? [ev] : []
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return routes;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ============================================================================
|
|
613
|
+
// OPENAPI/SWAGGER SPEC PARSING
|
|
614
|
+
// ============================================================================
|
|
615
|
+
|
|
616
|
+
async function resolveOpenAPIRoutes(repoRoot) {
|
|
617
|
+
const files = await fg([
|
|
618
|
+
"**/openapi.json",
|
|
619
|
+
"**/openapi.yaml",
|
|
620
|
+
"**/openapi.yml",
|
|
621
|
+
"**/swagger.json",
|
|
622
|
+
"**/swagger.yaml",
|
|
623
|
+
"**/swagger.yml",
|
|
624
|
+
"**/api-spec.json",
|
|
625
|
+
"**/api-spec.yaml"
|
|
626
|
+
], {
|
|
627
|
+
cwd: repoRoot,
|
|
628
|
+
absolute: true,
|
|
629
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const routes = [];
|
|
633
|
+
|
|
634
|
+
for (const fileAbs of files) {
|
|
635
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
636
|
+
const content = safeRead(fileAbs);
|
|
637
|
+
|
|
638
|
+
let spec;
|
|
639
|
+
try {
|
|
640
|
+
if (fileAbs.endsWith(".json")) {
|
|
641
|
+
spec = JSON.parse(content);
|
|
642
|
+
} else {
|
|
643
|
+
// Simple YAML parsing for common patterns
|
|
644
|
+
const lines = content.split(/\r?\n/);
|
|
645
|
+
const pathMatches = [];
|
|
646
|
+
let currentPath = null;
|
|
647
|
+
|
|
648
|
+
for (const line of lines) {
|
|
649
|
+
// Match path definitions like " /api/users:"
|
|
650
|
+
const pathMatch = line.match(/^ ['"]?(\/[^'":\s]+)['"]?:\s*$/);
|
|
651
|
+
if (pathMatch) {
|
|
652
|
+
currentPath = pathMatch[1];
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
// Match HTTP methods like " get:" or " post:"
|
|
656
|
+
if (currentPath) {
|
|
657
|
+
const methodMatch = line.match(/^\s{4}(get|post|put|patch|delete|options|head):\s*$/);
|
|
658
|
+
if (methodMatch) {
|
|
659
|
+
pathMatches.push({ path: currentPath, method: methodMatch[1] });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
for (const { path: p, method } of pathMatches) {
|
|
665
|
+
routes.push({
|
|
666
|
+
method: canonicalizeMethod(method),
|
|
667
|
+
path: canonicalizePath(p),
|
|
668
|
+
handler: fileRel,
|
|
669
|
+
framework: "openapi",
|
|
670
|
+
confidence: "high",
|
|
671
|
+
evidence: [{
|
|
672
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
673
|
+
file: fileRel,
|
|
674
|
+
lines: "1",
|
|
675
|
+
reason: `OpenAPI spec ${method.toUpperCase()} ${p}`
|
|
676
|
+
}]
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
} catch { continue; }
|
|
682
|
+
|
|
683
|
+
// Parse JSON OpenAPI spec
|
|
684
|
+
if (spec && spec.paths) {
|
|
685
|
+
for (const [pathStr, pathItem] of Object.entries(spec.paths)) {
|
|
686
|
+
const methods = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
687
|
+
for (const method of methods) {
|
|
688
|
+
if (pathItem[method]) {
|
|
689
|
+
routes.push({
|
|
690
|
+
method: canonicalizeMethod(method),
|
|
691
|
+
path: canonicalizePath(pathStr),
|
|
692
|
+
handler: fileRel,
|
|
693
|
+
framework: "openapi",
|
|
694
|
+
confidence: "high",
|
|
695
|
+
evidence: [{
|
|
696
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
697
|
+
file: fileRel,
|
|
698
|
+
lines: "1",
|
|
699
|
+
reason: `OpenAPI spec ${method.toUpperCase()} ${pathStr}`
|
|
700
|
+
}]
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return routes;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ============================================================================
|
|
712
|
+
// GRAPHQL ENDPOINT DETECTION
|
|
713
|
+
// ============================================================================
|
|
714
|
+
|
|
715
|
+
async function resolveGraphQLRoutes(repoRoot) {
|
|
716
|
+
const routes = [];
|
|
717
|
+
|
|
718
|
+
// Find GraphQL schema files
|
|
719
|
+
const schemaFiles = await fg([
|
|
720
|
+
"**/*.graphql",
|
|
721
|
+
"**/*.gql",
|
|
722
|
+
"**/schema.graphql",
|
|
723
|
+
"**/schema.gql"
|
|
724
|
+
], {
|
|
725
|
+
cwd: repoRoot,
|
|
726
|
+
absolute: true,
|
|
727
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Find GraphQL server setup files
|
|
731
|
+
const serverFiles = await fg([
|
|
732
|
+
"**/*.{ts,js,py}"
|
|
733
|
+
], {
|
|
734
|
+
cwd: repoRoot,
|
|
735
|
+
absolute: true,
|
|
736
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/test/**", "**/*.d.ts"]
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// Check for GraphQL endpoints in server files
|
|
740
|
+
for (const fileAbs of serverFiles) {
|
|
741
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
742
|
+
const code = safeRead(fileAbs);
|
|
743
|
+
|
|
744
|
+
// Skip if no GraphQL indicators
|
|
745
|
+
if (!code.includes("graphql") && !code.includes("GraphQL") && !code.includes("gql")) continue;
|
|
746
|
+
|
|
747
|
+
// Detect common GraphQL endpoint patterns
|
|
748
|
+
const patterns = [
|
|
749
|
+
// Apollo Server: app.use('/graphql', ...)
|
|
750
|
+
/(?:app|server|router)\.(use|post|get)\s*\(\s*['"]([^'"]*graphql[^'"]*)['"]/gi,
|
|
751
|
+
// Express GraphQL: graphqlHTTP({ ... })
|
|
752
|
+
/graphqlHTTP\s*\(/gi,
|
|
753
|
+
// Apollo Server: new ApolloServer
|
|
754
|
+
/new\s+ApolloServer/gi,
|
|
755
|
+
// Yoga: createYoga
|
|
756
|
+
/createYoga\s*\(/gi,
|
|
757
|
+
// Python Strawberry/Ariadne
|
|
758
|
+
/GraphQLRouter|graphql_app|make_executable_schema/gi
|
|
759
|
+
];
|
|
760
|
+
|
|
761
|
+
let hasGraphQL = false;
|
|
762
|
+
let endpointPath = "/graphql"; // Default
|
|
763
|
+
|
|
764
|
+
for (const pattern of patterns) {
|
|
765
|
+
const match = code.match(pattern);
|
|
766
|
+
if (match) {
|
|
767
|
+
hasGraphQL = true;
|
|
768
|
+
// Try to extract custom endpoint path
|
|
769
|
+
const pathMatch = code.match(/['"]([^'"]*graphql[^'"]*)['"]/i);
|
|
770
|
+
if (pathMatch) {
|
|
771
|
+
endpointPath = pathMatch[1];
|
|
772
|
+
}
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (hasGraphQL) {
|
|
778
|
+
routes.push({
|
|
779
|
+
method: "POST",
|
|
780
|
+
path: canonicalizePath(endpointPath),
|
|
781
|
+
handler: fileRel,
|
|
782
|
+
framework: "graphql",
|
|
783
|
+
confidence: "high",
|
|
784
|
+
evidence: [{
|
|
785
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
786
|
+
file: fileRel,
|
|
787
|
+
lines: "1",
|
|
788
|
+
reason: `GraphQL endpoint detected`
|
|
789
|
+
}]
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// GraphQL also supports GET for queries
|
|
793
|
+
routes.push({
|
|
794
|
+
method: "GET",
|
|
795
|
+
path: canonicalizePath(endpointPath),
|
|
796
|
+
handler: fileRel,
|
|
797
|
+
framework: "graphql",
|
|
798
|
+
confidence: "med",
|
|
799
|
+
evidence: [{
|
|
800
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
801
|
+
file: fileRel,
|
|
802
|
+
lines: "1",
|
|
803
|
+
reason: `GraphQL endpoint (GET for queries)`
|
|
804
|
+
}]
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// If schema files exist, assume /graphql endpoint
|
|
810
|
+
if (schemaFiles.length > 0 && routes.length === 0) {
|
|
811
|
+
const fileRel = path.relative(repoRoot, schemaFiles[0]).replace(/\\/g, "/");
|
|
812
|
+
routes.push({
|
|
813
|
+
method: "*",
|
|
814
|
+
path: "/graphql",
|
|
815
|
+
handler: fileRel,
|
|
816
|
+
framework: "graphql",
|
|
817
|
+
confidence: "med",
|
|
818
|
+
evidence: [{
|
|
819
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
820
|
+
file: fileRel,
|
|
821
|
+
lines: "1",
|
|
822
|
+
reason: `GraphQL schema file found`
|
|
823
|
+
}]
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return routes;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ============================================================================
|
|
831
|
+
// GO ROUTE DETECTION (Gin, Echo, Fiber)
|
|
832
|
+
// ============================================================================
|
|
833
|
+
|
|
834
|
+
async function resolveGoRoutes(repoRoot) {
|
|
835
|
+
const files = await fg(["**/*.go"], {
|
|
836
|
+
cwd: repoRoot,
|
|
837
|
+
absolute: true,
|
|
838
|
+
ignore: ["**/vendor/**", "**/*_test.go", "**/testdata/**"]
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
const routes = [];
|
|
842
|
+
const GO_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "Any", "Handle"];
|
|
843
|
+
|
|
844
|
+
for (const fileAbs of files) {
|
|
845
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
846
|
+
const code = safeRead(fileAbs);
|
|
847
|
+
|
|
848
|
+
// Skip if no router indicators
|
|
849
|
+
if (!code.includes("gin") && !code.includes("echo") && !code.includes("fiber") &&
|
|
850
|
+
!code.includes("mux") && !code.includes("chi") && !code.includes("http.Handle")) continue;
|
|
851
|
+
|
|
852
|
+
const lines = code.split(/\r?\n/);
|
|
853
|
+
|
|
854
|
+
for (let i = 0; i < lines.length; i++) {
|
|
855
|
+
const line = lines[i];
|
|
856
|
+
|
|
857
|
+
// Gin: r.GET("/path", handler)
|
|
858
|
+
// Echo: e.GET("/path", handler)
|
|
859
|
+
// Fiber: app.Get("/path", handler)
|
|
860
|
+
const ginEchoMatch = line.match(/(\w+)\.(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD|Any|Handle)\s*\(\s*["'`]([^"'`]+)["'`]/i);
|
|
861
|
+
if (ginEchoMatch) {
|
|
862
|
+
const [, , method, routePath] = ginEchoMatch;
|
|
863
|
+
routes.push({
|
|
864
|
+
method: canonicalizeMethod(method === "Any" || method === "Handle" ? "*" : method),
|
|
865
|
+
path: canonicalizePath(routePath),
|
|
866
|
+
handler: fileRel,
|
|
867
|
+
framework: "go",
|
|
868
|
+
confidence: "high",
|
|
869
|
+
evidence: [{
|
|
870
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
871
|
+
file: fileRel,
|
|
872
|
+
lines: `${i + 1}`,
|
|
873
|
+
reason: `Go ${method}("${routePath}")`
|
|
874
|
+
}]
|
|
875
|
+
});
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Fiber lowercase: app.Get("/path", handler)
|
|
880
|
+
const fiberMatch = line.match(/(\w+)\.(Get|Post|Put|Patch|Delete|Options|Head|All)\s*\(\s*["'`]([^"'`]+)["'`]/);
|
|
881
|
+
if (fiberMatch) {
|
|
882
|
+
const [, , method, routePath] = fiberMatch;
|
|
883
|
+
routes.push({
|
|
884
|
+
method: canonicalizeMethod(method === "All" ? "*" : method),
|
|
885
|
+
path: canonicalizePath(routePath),
|
|
886
|
+
handler: fileRel,
|
|
887
|
+
framework: "fiber",
|
|
888
|
+
confidence: "high",
|
|
889
|
+
evidence: [{
|
|
890
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
891
|
+
file: fileRel,
|
|
892
|
+
lines: `${i + 1}`,
|
|
893
|
+
reason: `Fiber ${method}("${routePath}")`
|
|
894
|
+
}]
|
|
895
|
+
});
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Chi router: r.Get("/path", handler)
|
|
900
|
+
const chiMatch = line.match(/(\w+)\.(Get|Post|Put|Patch|Delete|Options|Head|Connect|Trace|MethodFunc)\s*\(\s*["'`]([^"'`]+)["'`]/);
|
|
901
|
+
if (chiMatch) {
|
|
902
|
+
const [, , method, routePath] = chiMatch;
|
|
903
|
+
routes.push({
|
|
904
|
+
method: canonicalizeMethod(method),
|
|
905
|
+
path: canonicalizePath(routePath),
|
|
906
|
+
handler: fileRel,
|
|
907
|
+
framework: "chi",
|
|
908
|
+
confidence: "high",
|
|
909
|
+
evidence: [{
|
|
910
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
911
|
+
file: fileRel,
|
|
912
|
+
lines: `${i + 1}`,
|
|
913
|
+
reason: `Chi ${method}("${routePath}")`
|
|
914
|
+
}]
|
|
915
|
+
});
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Gorilla mux: r.HandleFunc("/path", handler).Methods("GET")
|
|
920
|
+
const muxMatch = line.match(/HandleFunc\s*\(\s*["'`]([^"'`]+)["'`]/);
|
|
921
|
+
if (muxMatch) {
|
|
922
|
+
const routePath = muxMatch[1];
|
|
923
|
+
// Try to find .Methods() on same or next line
|
|
924
|
+
const methodMatch = (line + (lines[i + 1] || "")).match(/\.Methods\s*\(\s*["'`]([^"'`]+)["'`]/);
|
|
925
|
+
const method = methodMatch ? methodMatch[1] : "*";
|
|
926
|
+
|
|
927
|
+
routes.push({
|
|
928
|
+
method: canonicalizeMethod(method),
|
|
929
|
+
path: canonicalizePath(routePath),
|
|
930
|
+
handler: fileRel,
|
|
931
|
+
framework: "gorilla",
|
|
932
|
+
confidence: "high",
|
|
933
|
+
evidence: [{
|
|
934
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
935
|
+
file: fileRel,
|
|
936
|
+
lines: `${i + 1}`,
|
|
937
|
+
reason: `Gorilla mux HandleFunc("${routePath}")`
|
|
938
|
+
}]
|
|
939
|
+
});
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Standard library: http.HandleFunc("/path", handler)
|
|
944
|
+
const stdMatch = line.match(/http\.HandleFunc\s*\(\s*["'`]([^"'`]+)["'`]/);
|
|
945
|
+
if (stdMatch) {
|
|
946
|
+
routes.push({
|
|
947
|
+
method: "*",
|
|
948
|
+
path: canonicalizePath(stdMatch[1]),
|
|
949
|
+
handler: fileRel,
|
|
950
|
+
framework: "go-stdlib",
|
|
951
|
+
confidence: "high",
|
|
952
|
+
evidence: [{
|
|
953
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
954
|
+
file: fileRel,
|
|
955
|
+
lines: `${i + 1}`,
|
|
956
|
+
reason: `Go http.HandleFunc("${stdMatch[1]}")`
|
|
957
|
+
}]
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
return routes;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// ============================================================================
|
|
967
|
+
// PYTHON CLIENT REFS (requests, httpx, aiohttp)
|
|
968
|
+
// ============================================================================
|
|
969
|
+
|
|
970
|
+
async function resolvePythonClientRefs(repoRoot) {
|
|
971
|
+
const files = await fg(["**/*.py"], {
|
|
972
|
+
cwd: repoRoot,
|
|
973
|
+
absolute: true,
|
|
974
|
+
ignore: ["**/venv/**", "**/.venv/**", "**/env/**", "**/__pycache__/**", "**/site-packages/**", "**/test*/**"]
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
const refs = [];
|
|
978
|
+
|
|
979
|
+
// Match requests.get('/api/...'), httpx.post('/api/...'), etc.
|
|
980
|
+
const httpPattern = /(requests|httpx|aiohttp|session)\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
981
|
+
const fetchPattern = /fetch\s*\(\s*['"]([^'"]+)['"]/g;
|
|
982
|
+
|
|
983
|
+
for (const fileAbs of files) {
|
|
984
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
985
|
+
const code = safeRead(fileAbs);
|
|
986
|
+
|
|
987
|
+
const lines = code.split(/\r?\n/);
|
|
988
|
+
|
|
989
|
+
for (let i = 0; i < lines.length; i++) {
|
|
990
|
+
const line = lines[i];
|
|
991
|
+
|
|
992
|
+
// Match requests.get('/api/...')
|
|
993
|
+
const httpMatch = line.match(/(requests|httpx|aiohttp|session)\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/);
|
|
994
|
+
if (httpMatch) {
|
|
995
|
+
const [, lib, method, url] = httpMatch;
|
|
996
|
+
if (url.startsWith("/") || url.startsWith("http")) {
|
|
997
|
+
refs.push({
|
|
998
|
+
method: canonicalizeMethod(method),
|
|
999
|
+
path: canonicalizePath(url.replace(/^https?:\/\/[^\/]+/, "")),
|
|
1000
|
+
source: fileRel,
|
|
1001
|
+
confidence: "high",
|
|
1002
|
+
evidence: [{
|
|
1003
|
+
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
1004
|
+
file: fileRel,
|
|
1005
|
+
lines: `${i + 1}`,
|
|
1006
|
+
reason: `Python ${lib}.${method}("${url}")`
|
|
1007
|
+
}]
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return refs;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// ============================================================================
|
|
1018
|
+
// MAIN: RESOLVE ALL ROUTES
|
|
1019
|
+
// ============================================================================
|
|
1020
|
+
|
|
1021
|
+
async function resolveAllRoutes(repoRoot) {
|
|
1022
|
+
const frameworks = await detectFrameworks(repoRoot);
|
|
1023
|
+
|
|
1024
|
+
const allRoutes = [];
|
|
1025
|
+
const allClientRefs = [];
|
|
1026
|
+
const gaps = [];
|
|
1027
|
+
|
|
1028
|
+
console.log(` 📦 Detected frameworks: ${frameworks.length > 0 ? frameworks.join(", ") : "none detected, scanning all"}`);
|
|
1029
|
+
|
|
1030
|
+
// Always scan for routes regardless of detected frameworks
|
|
1031
|
+
// JavaScript/TypeScript frameworks
|
|
1032
|
+
try {
|
|
1033
|
+
const expressRoutes = await resolveExpressRoutes(repoRoot);
|
|
1034
|
+
if (expressRoutes.length > 0) {
|
|
1035
|
+
console.log(` ✓ Express: ${expressRoutes.length} routes`);
|
|
1036
|
+
allRoutes.push(...expressRoutes);
|
|
1037
|
+
}
|
|
1038
|
+
} catch (e) { gaps.push({ kind: "express_scan_error", error: e.message }); }
|
|
1039
|
+
|
|
1040
|
+
try {
|
|
1041
|
+
const honoRoutes = await resolveHonoRoutes(repoRoot);
|
|
1042
|
+
if (honoRoutes.length > 0) {
|
|
1043
|
+
console.log(` ✓ Hono: ${honoRoutes.length} routes`);
|
|
1044
|
+
allRoutes.push(...honoRoutes);
|
|
1045
|
+
}
|
|
1046
|
+
} catch (e) { gaps.push({ kind: "hono_scan_error", error: e.message }); }
|
|
1047
|
+
|
|
1048
|
+
try {
|
|
1049
|
+
const koaRoutes = await resolveKoaRoutes(repoRoot);
|
|
1050
|
+
if (koaRoutes.length > 0) {
|
|
1051
|
+
console.log(` ✓ Koa: ${koaRoutes.length} routes`);
|
|
1052
|
+
allRoutes.push(...koaRoutes);
|
|
1053
|
+
}
|
|
1054
|
+
} catch (e) { gaps.push({ kind: "koa_scan_error", error: e.message }); }
|
|
1055
|
+
|
|
1056
|
+
// Python frameworks
|
|
1057
|
+
try {
|
|
1058
|
+
const flaskRoutes = await resolveFlaskRoutes(repoRoot);
|
|
1059
|
+
if (flaskRoutes.length > 0) {
|
|
1060
|
+
console.log(` ✓ Flask: ${flaskRoutes.length} routes`);
|
|
1061
|
+
allRoutes.push(...flaskRoutes);
|
|
1062
|
+
}
|
|
1063
|
+
} catch (e) { gaps.push({ kind: "flask_scan_error", error: e.message }); }
|
|
1064
|
+
|
|
1065
|
+
try {
|
|
1066
|
+
const fastapiRoutes = await resolveFastAPIRoutes(repoRoot);
|
|
1067
|
+
if (fastapiRoutes.length > 0) {
|
|
1068
|
+
console.log(` ✓ FastAPI: ${fastapiRoutes.length} routes`);
|
|
1069
|
+
allRoutes.push(...fastapiRoutes);
|
|
1070
|
+
}
|
|
1071
|
+
} catch (e) { gaps.push({ kind: "fastapi_scan_error", error: e.message }); }
|
|
1072
|
+
|
|
1073
|
+
try {
|
|
1074
|
+
const djangoRoutes = await resolveDjangoRoutes(repoRoot);
|
|
1075
|
+
if (djangoRoutes.length > 0) {
|
|
1076
|
+
console.log(` ✓ Django: ${djangoRoutes.length} routes`);
|
|
1077
|
+
allRoutes.push(...djangoRoutes);
|
|
1078
|
+
}
|
|
1079
|
+
} catch (e) { gaps.push({ kind: "django_scan_error", error: e.message }); }
|
|
1080
|
+
|
|
1081
|
+
// Python client refs
|
|
1082
|
+
try {
|
|
1083
|
+
const pythonRefs = await resolvePythonClientRefs(repoRoot);
|
|
1084
|
+
if (pythonRefs.length > 0) {
|
|
1085
|
+
console.log(` ✓ Python client refs: ${pythonRefs.length}`);
|
|
1086
|
+
allClientRefs.push(...pythonRefs);
|
|
1087
|
+
}
|
|
1088
|
+
} catch (e) { gaps.push({ kind: "python_client_scan_error", error: e.message }); }
|
|
1089
|
+
|
|
1090
|
+
// OpenAPI/Swagger specs (high accuracy)
|
|
1091
|
+
try {
|
|
1092
|
+
const openapiRoutes = await resolveOpenAPIRoutes(repoRoot);
|
|
1093
|
+
if (openapiRoutes.length > 0) {
|
|
1094
|
+
console.log(` ✓ OpenAPI spec: ${openapiRoutes.length} routes`);
|
|
1095
|
+
allRoutes.push(...openapiRoutes);
|
|
1096
|
+
}
|
|
1097
|
+
} catch (e) { gaps.push({ kind: "openapi_scan_error", error: e.message }); }
|
|
1098
|
+
|
|
1099
|
+
// GraphQL endpoints
|
|
1100
|
+
try {
|
|
1101
|
+
const graphqlRoutes = await resolveGraphQLRoutes(repoRoot);
|
|
1102
|
+
if (graphqlRoutes.length > 0) {
|
|
1103
|
+
console.log(` ✓ GraphQL: ${graphqlRoutes.length} endpoints`);
|
|
1104
|
+
allRoutes.push(...graphqlRoutes);
|
|
1105
|
+
}
|
|
1106
|
+
} catch (e) { gaps.push({ kind: "graphql_scan_error", error: e.message }); }
|
|
1107
|
+
|
|
1108
|
+
// Go frameworks (Gin, Echo, Fiber, Chi, Gorilla, stdlib)
|
|
1109
|
+
try {
|
|
1110
|
+
const goRoutes = await resolveGoRoutes(repoRoot);
|
|
1111
|
+
if (goRoutes.length > 0) {
|
|
1112
|
+
console.log(` ✓ Go: ${goRoutes.length} routes`);
|
|
1113
|
+
allRoutes.push(...goRoutes);
|
|
1114
|
+
}
|
|
1115
|
+
} catch (e) { gaps.push({ kind: "go_scan_error", error: e.message }); }
|
|
1116
|
+
|
|
1117
|
+
return {
|
|
1118
|
+
routes: allRoutes,
|
|
1119
|
+
clientRefs: allClientRefs,
|
|
1120
|
+
gaps,
|
|
1121
|
+
frameworks
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
module.exports = {
|
|
1126
|
+
detectFrameworks,
|
|
1127
|
+
resolveExpressRoutes,
|
|
1128
|
+
resolveFlaskRoutes,
|
|
1129
|
+
resolveFastAPIRoutes,
|
|
1130
|
+
resolveDjangoRoutes,
|
|
1131
|
+
resolveHonoRoutes,
|
|
1132
|
+
resolveKoaRoutes,
|
|
1133
|
+
resolveOpenAPIRoutes,
|
|
1134
|
+
resolveGraphQLRoutes,
|
|
1135
|
+
resolveGoRoutes,
|
|
1136
|
+
resolvePythonClientRefs,
|
|
1137
|
+
resolveAllRoutes,
|
|
1138
|
+
canonicalizeMethod,
|
|
1139
|
+
canonicalizePath
|
|
1140
|
+
};
|