openhack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. openhack/__init__.py +2 -0
  2. openhack/__main__.py +225 -0
  3. openhack/agents/__init__.py +30 -0
  4. openhack/agents/base.py +230 -0
  5. openhack/agents/browser_verifier.py +679 -0
  6. openhack/agents/browser_verifier_swarm.py +256 -0
  7. openhack/agents/checkpoint.py +89 -0
  8. openhack/agents/context_manager.py +356 -0
  9. openhack/agents/coordinator.py +1105 -0
  10. openhack/agents/endpoint_analyst.py +307 -0
  11. openhack/agents/feature_hunter.py +93 -0
  12. openhack/agents/hunter.py +481 -0
  13. openhack/agents/hunter_swarm.py +385 -0
  14. openhack/agents/llm.py +334 -0
  15. openhack/agents/recon.py +19 -0
  16. openhack/agents/sandbox_verifier.py +396 -0
  17. openhack/agents/sandbox_verifier_swarm.py +250 -0
  18. openhack/agents/session.py +286 -0
  19. openhack/agents/validator.py +217 -0
  20. openhack/agents/validator_swarm.py +106 -0
  21. openhack/auth.py +175 -0
  22. openhack/browser/__init__.py +12 -0
  23. openhack/browser/runner.py +385 -0
  24. openhack/categories.py +130 -0
  25. openhack/config.py +201 -0
  26. openhack/deterministic_recon.py +464 -0
  27. openhack/entry_points.py +745 -0
  28. openhack/framework_classifier.py +515 -0
  29. openhack/framework_detection.py +269 -0
  30. openhack/headless_scan.py +179 -0
  31. openhack/prompts/__init__.py +108 -0
  32. openhack/prompts/browser_verifier.py +171 -0
  33. openhack/prompts/coordinator.py +31 -0
  34. openhack/prompts/django/__init__.py +32 -0
  35. openhack/prompts/django/auth_bypass.py +76 -0
  36. openhack/prompts/django/csrf.py +62 -0
  37. openhack/prompts/django/data_exposure.py +67 -0
  38. openhack/prompts/django/idor.py +74 -0
  39. openhack/prompts/django/injection.py +67 -0
  40. openhack/prompts/django/misconfiguration.py +70 -0
  41. openhack/prompts/django/ssrf.py +64 -0
  42. openhack/prompts/endpoint_analyst.py +122 -0
  43. openhack/prompts/express/__init__.py +29 -0
  44. openhack/prompts/express/auth_bypass.py +71 -0
  45. openhack/prompts/express/data_exposure.py +77 -0
  46. openhack/prompts/express/idor.py +69 -0
  47. openhack/prompts/express/injection.py +75 -0
  48. openhack/prompts/express/misconfiguration.py +72 -0
  49. openhack/prompts/express/ssrf.py +63 -0
  50. openhack/prompts/feature_hunter.py +140 -0
  51. openhack/prompts/flask/__init__.py +29 -0
  52. openhack/prompts/flask/auth_bypass.py +86 -0
  53. openhack/prompts/flask/data_exposure.py +78 -0
  54. openhack/prompts/flask/idor.py +83 -0
  55. openhack/prompts/flask/injection.py +77 -0
  56. openhack/prompts/flask/misconfiguration.py +73 -0
  57. openhack/prompts/flask/ssrf.py +65 -0
  58. openhack/prompts/hunter.py +362 -0
  59. openhack/prompts/hunter_continuation_loop.py +12 -0
  60. openhack/prompts/hunter_continuation_no_findings.py +19 -0
  61. openhack/prompts/hunter_continuation_no_progress.py +22 -0
  62. openhack/prompts/hunter_tool_instructions.py +55 -0
  63. openhack/prompts/nextjs/__init__.py +42 -0
  64. openhack/prompts/nextjs/auth_bypass.py +80 -0
  65. openhack/prompts/nextjs/csrf.py +71 -0
  66. openhack/prompts/nextjs/data_exposure.py +88 -0
  67. openhack/prompts/nextjs/idor.py +64 -0
  68. openhack/prompts/nextjs/injection.py +65 -0
  69. openhack/prompts/nextjs/middleware_bypass.py +75 -0
  70. openhack/prompts/nextjs/misconfiguration.py +92 -0
  71. openhack/prompts/nextjs/server_actions.py +97 -0
  72. openhack/prompts/nextjs/ssrf.py +66 -0
  73. openhack/prompts/nextjs/xss.py +69 -0
  74. openhack/prompts/pr_analysis_system.py +80 -0
  75. openhack/prompts/pr_analysis_user.py +11 -0
  76. openhack/prompts/project_context.py +89 -0
  77. openhack/prompts/recon.py +199 -0
  78. openhack/prompts/reporter.py +88 -0
  79. openhack/prompts/researchers.py +434 -0
  80. openhack/prompts/sandbox_verifier.py +128 -0
  81. openhack/prompts/supabase/__init__.py +39 -0
  82. openhack/prompts/supabase/auth_tokens.py +131 -0
  83. openhack/prompts/supabase/edge_functions.py +150 -0
  84. openhack/prompts/supabase/graphql.py +102 -0
  85. openhack/prompts/supabase/postgrest.py +99 -0
  86. openhack/prompts/supabase/realtime.py +93 -0
  87. openhack/prompts/supabase/rls.py +110 -0
  88. openhack/prompts/supabase/rpc_functions.py +127 -0
  89. openhack/prompts/supabase/storage.py +110 -0
  90. openhack/prompts/supabase/tenant_isolation.py +118 -0
  91. openhack/prompts/validator.py +319 -0
  92. openhack/prompts/validator_continuation_incomplete.py +12 -0
  93. openhack/prompts/validator_tool_instructions.py +29 -0
  94. openhack/quality.py +231 -0
  95. openhack/sandbox/__init__.py +12 -0
  96. openhack/sandbox/orchestrator.py +517 -0
  97. openhack/sandbox/runner.py +177 -0
  98. openhack/scan_session.py +245 -0
  99. openhack/setup.py +452 -0
  100. openhack/static_validator.py +612 -0
  101. openhack/tools/__init__.py +1 -0
  102. openhack/tools/ast_tools.py +307 -0
  103. openhack/tools/coverage.py +1078 -0
  104. openhack/tools/filesystem.py +404 -0
  105. openhack/tools/nextjs.py +258 -0
  106. openhack/tools/registry.py +52 -0
  107. openhack/tui.py +3450 -0
  108. openhack/updates.py +170 -0
  109. openhack-0.1.0.dist-info/METADATA +189 -0
  110. openhack-0.1.0.dist-info/RECORD +113 -0
  111. openhack-0.1.0.dist-info/WHEEL +4 -0
  112. openhack-0.1.0.dist-info/entry_points.txt +2 -0
  113. openhack-0.1.0.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,745 @@
1
+ """
2
+ Entry point detector — deterministic extraction of all attack surface entry points.
3
+
4
+ For each detected framework, runs the appropriate extractor to find all
5
+ routes/endpoints/handlers. Returns a structured list that can be displayed
6
+ in the TUI and used for scan planning.
7
+ """
8
+
9
+ import json
10
+ import logging
11
+ import re
12
+ from typing import Optional
13
+
14
+ from .tools.filesystem import FileSystemTools
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def detect_entry_points(fs: FileSystemTools, classifications: list[dict]) -> list[dict]:
20
+ """Detect all entry points based on framework classifications.
21
+
22
+ Returns a list of entry point dicts:
23
+ - path: HTTP path or function signature (e.g., "/api/users/:id")
24
+ - method: HTTP method or "FUNC" for libraries (e.g., "GET", "POST", "FUNC")
25
+ - file: source file containing the handler
26
+ - line: line number (if detected)
27
+ - framework: which framework this belongs to
28
+ - auth: detected auth middleware (if any)
29
+ - status: "unscanned" (default)
30
+ """
31
+ all_entries = []
32
+
33
+ for classification in classifications:
34
+ root = classification["root"]
35
+ frameworks = classification["frameworks"]
36
+ language = classification["language"]
37
+
38
+ for framework in frameworks:
39
+ extractor = _EXTRACTORS.get(framework)
40
+ if extractor:
41
+ try:
42
+ entries = extractor(fs, root)
43
+ for entry in entries:
44
+ entry["framework"] = framework
45
+ entry.setdefault("status", "unscanned")
46
+ all_entries.extend(entries)
47
+ except Exception as e:
48
+ logger.warning(f"Entry point extraction failed for {framework} at {root}: {e}")
49
+
50
+ # Deduplicate by file + path + method
51
+ seen = set()
52
+ deduped = []
53
+ for entry in all_entries:
54
+ key = f"{entry.get('file', '')}::{entry.get('path', '')}::{entry.get('method', '')}"
55
+ if key not in seen:
56
+ seen.add(key)
57
+ deduped.append(entry)
58
+
59
+ logger.info(f"Detected {len(deduped)} entry points across {len(classifications)} framework(s)")
60
+ return deduped
61
+
62
+
63
+ # ============================================================
64
+ # Framework-specific extractors
65
+ # ============================================================
66
+
67
+ def _extract_nextjs(fs: FileSystemTools, root: str) -> list[dict]:
68
+ """Extract Next.js API routes from app/api/ directory."""
69
+ entries = []
70
+
71
+ # App Router: app/**/route.ts
72
+ for pattern in ["app/**/route.ts", "app/**/route.js", "src/app/**/route.ts", "src/app/**/route.js"]:
73
+ result = fs.glob(pattern, root)
74
+ for filepath in result.get("matches", []):
75
+ if "node_modules" in filepath:
76
+ continue
77
+ # Convert file path to route: app/api/users/[id]/route.ts -> /api/users/:id
78
+ route = _filepath_to_nextjs_route(filepath)
79
+ # Read file to detect methods
80
+ methods = _detect_nextjs_methods(fs, filepath)
81
+ for method in methods:
82
+ entries.append({
83
+ "path": route,
84
+ "method": method,
85
+ "file": filepath,
86
+ "line": None,
87
+ "auth": None,
88
+ })
89
+
90
+ # Server actions
91
+ for pattern in ["app/**/actions.ts", "app/**/actions.js", "src/app/**/actions.ts"]:
92
+ result = fs.glob(pattern, root)
93
+ for filepath in result.get("matches", []):
94
+ if "node_modules" in filepath:
95
+ continue
96
+ entries.append({
97
+ "path": f"[server-action] {filepath}",
98
+ "method": "POST",
99
+ "file": filepath,
100
+ "line": None,
101
+ "auth": None,
102
+ })
103
+
104
+ return entries
105
+
106
+
107
+ def _filepath_to_nextjs_route(filepath: str) -> str:
108
+ """Convert Next.js file path to HTTP route."""
109
+ # Remove app/ prefix and /route.ts suffix
110
+ route = filepath
111
+ for prefix in ["src/app/", "app/"]:
112
+ if route.startswith(prefix):
113
+ route = route[len(prefix):]
114
+ route = re.sub(r"/route\.(ts|js)$", "", route)
115
+ # Convert [param] to :param
116
+ route = re.sub(r"\[\.\.\.(\w+)\]", r"*\1", route)
117
+ route = re.sub(r"\[(\w+)\]", r":\1", route)
118
+ return f"/{route}" if route else "/"
119
+
120
+
121
+ def _detect_nextjs_methods(fs: FileSystemTools, filepath: str) -> list[str]:
122
+ """Detect which HTTP methods a Next.js route handler exports."""
123
+ result = fs.read_file(filepath)
124
+ if "error" in result:
125
+ return ["GET"]
126
+ content = result.get("content", "")
127
+ methods = []
128
+ for method in ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]:
129
+ if re.search(rf"export\s+(async\s+)?function\s+{method}\b", content):
130
+ methods.append(method)
131
+ return methods or ["GET"]
132
+
133
+
134
+ def _extract_express(fs: FileSystemTools, root: str) -> list[dict]:
135
+ """Extract Express.js route definitions."""
136
+ entries = []
137
+ # Find all JS/TS files and grep for route definitions
138
+ for pattern in ["**/*.js", "**/*.ts"]:
139
+ result = fs.glob(pattern, root)
140
+ for filepath in result.get("matches", []):
141
+ if any(skip in filepath for skip in ["node_modules/", "test/", "dist/", "build/", ".next/"]):
142
+ continue
143
+ read_result = fs.read_file(filepath)
144
+ if "error" in read_result:
145
+ continue
146
+ content = read_result.get("content", "")
147
+ lines = content.split("\n")
148
+ for i, line in enumerate(lines):
149
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
150
+ # Match: router.get('/path', ...), app.post('/path', ...), etc.
151
+ match = re.search(
152
+ r"(?:router|app|server)\.(get|post|put|patch|delete|all|use)\s*\(\s*['\"]([^'\"]+)['\"]",
153
+ raw_line, re.IGNORECASE
154
+ )
155
+ if match:
156
+ method = match.group(1).upper()
157
+ path = match.group(2)
158
+ if method == "USE":
159
+ method = "MIDDLEWARE"
160
+ entries.append({
161
+ "path": path,
162
+ "method": method,
163
+ "file": filepath,
164
+ "line": i + 1,
165
+ "auth": None,
166
+ })
167
+ return entries
168
+
169
+
170
+ def _extract_django(fs: FileSystemTools, root: str) -> list[dict]:
171
+ """Extract Django URL patterns."""
172
+ entries = []
173
+ # Find urls.py files
174
+ result = fs.glob("**/urls.py", root)
175
+ for filepath in result.get("matches", []):
176
+ if any(skip in filepath for skip in ["test/", ".venv/", "migrations/"]):
177
+ continue
178
+ read_result = fs.read_file(filepath)
179
+ if "error" in read_result:
180
+ continue
181
+ content = read_result.get("content", "")
182
+ lines = content.split("\n")
183
+ for i, line in enumerate(lines):
184
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
185
+ # Match: path('api/users/', views.UserView), re_path(r'^api/', ...)
186
+ match = re.search(r"(?:path|re_path)\s*\(\s*['\"]([^'\"]*)['\"]", raw_line)
187
+ if match:
188
+ path = match.group(1)
189
+ entries.append({
190
+ "path": f"/{path}" if not path.startswith("/") else path,
191
+ "method": "ALL",
192
+ "file": filepath,
193
+ "line": i + 1,
194
+ "auth": None,
195
+ })
196
+ return entries
197
+
198
+
199
+ def _extract_flask(fs: FileSystemTools, root: str) -> list[dict]:
200
+ """Extract Flask route decorators."""
201
+ entries = []
202
+ result = fs.glob("**/*.py", root)
203
+ for filepath in result.get("matches", []):
204
+ if any(skip in filepath for skip in ["test/", ".venv/", "migrations/"]):
205
+ continue
206
+ read_result = fs.read_file(filepath)
207
+ if "error" in read_result:
208
+ continue
209
+ content = read_result.get("content", "")
210
+ lines = content.split("\n")
211
+ for i, line in enumerate(lines):
212
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
213
+ match = re.search(r"@\w+\.route\s*\(\s*['\"]([^'\"]+)['\"]", raw_line)
214
+ if match:
215
+ path = match.group(1)
216
+ # Try to extract methods
217
+ methods_match = re.search(r"methods\s*=\s*\[([^\]]+)\]", raw_line)
218
+ methods = "ALL"
219
+ if methods_match:
220
+ methods = methods_match.group(1).replace("'", "").replace('"', '').strip()
221
+ entries.append({
222
+ "path": path,
223
+ "method": methods,
224
+ "file": filepath,
225
+ "line": i + 1,
226
+ "auth": None,
227
+ })
228
+ return entries
229
+
230
+
231
+ def _extract_fastapi(fs: FileSystemTools, root: str) -> list[dict]:
232
+ """Extract FastAPI route decorators."""
233
+ entries = []
234
+ result = fs.glob("**/*.py", root)
235
+ for filepath in result.get("matches", []):
236
+ if any(skip in filepath for skip in ["test/", ".venv/", "migrations/"]):
237
+ continue
238
+ read_result = fs.read_file(filepath)
239
+ if "error" in read_result:
240
+ continue
241
+ content = read_result.get("content", "")
242
+ lines = content.split("\n")
243
+ for i, line in enumerate(lines):
244
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
245
+ match = re.search(
246
+ r"@(?:app|router)\.(get|post|put|patch|delete)\s*\(\s*['\"]([^'\"]+)['\"]",
247
+ raw_line, re.IGNORECASE
248
+ )
249
+ if match:
250
+ method = match.group(1).upper()
251
+ path = match.group(2)
252
+ entries.append({
253
+ "path": path,
254
+ "method": method,
255
+ "file": filepath,
256
+ "line": i + 1,
257
+ "auth": None,
258
+ })
259
+ return entries
260
+
261
+
262
+ def _extract_laravel(fs: FileSystemTools, root: str) -> list[dict]:
263
+ """Extract Laravel route definitions."""
264
+ entries = []
265
+ for route_file in ["routes/web.php", "routes/api.php"]:
266
+ filepath = f"{root}/{route_file}" if root != "." else route_file
267
+ read_result = fs.read_file(filepath)
268
+ if "error" in read_result:
269
+ continue
270
+ content = read_result.get("content", "")
271
+ lines = content.split("\n")
272
+ for i, line in enumerate(lines):
273
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
274
+ match = re.search(
275
+ r"Route::(get|post|put|patch|delete|any)\s*\(\s*['\"]([^'\"]+)['\"]",
276
+ raw_line, re.IGNORECASE
277
+ )
278
+ if match:
279
+ method = match.group(1).upper()
280
+ path = match.group(2)
281
+ entries.append({
282
+ "path": f"/{path}" if not path.startswith("/") else path,
283
+ "method": method,
284
+ "file": filepath,
285
+ "line": i + 1,
286
+ "auth": None,
287
+ })
288
+ return entries
289
+
290
+
291
+ def _extract_rails(fs: FileSystemTools, root: str) -> list[dict]:
292
+ """Extract Rails entry points from controllers and routes."""
293
+ entries = []
294
+
295
+ # Scan controllers directly — more reliable than parsing complex routes.rb
296
+ controller_patterns = [
297
+ "app/controllers/**/*_controller.rb",
298
+ "app/controllers/**/*_controller.rb",
299
+ ]
300
+ seen_files = set()
301
+ for pattern in controller_patterns:
302
+ result = fs.glob(pattern, root)
303
+ for filepath in result.get("matches", []):
304
+ if filepath in seen_files:
305
+ continue
306
+ if any(skip in filepath for skip in ["test/", "spec/", "concerns/application"]):
307
+ continue
308
+ seen_files.add(filepath)
309
+ read_result = fs.read_file(filepath)
310
+ if "error" in read_result:
311
+ entries.append({
312
+ "path": f"/{filepath}",
313
+ "method": "ALL",
314
+ "file": filepath,
315
+ "line": 1,
316
+ "auth": None,
317
+ })
318
+ continue
319
+ content = read_result.get("content", "")
320
+ lines = content.split("\n")
321
+ found = False
322
+ for i, line in enumerate(lines):
323
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
324
+ match = re.search(r"def\s+(\w+)", raw_line)
325
+ if match:
326
+ action = match.group(1)
327
+ if action.startswith("_"):
328
+ continue
329
+ ctrl = filepath.replace("app/controllers/", "").replace("_controller.rb", "")
330
+ entries.append({
331
+ "path": f"/{ctrl}#{action}",
332
+ "method": "ALL",
333
+ "file": filepath,
334
+ "line": i + 1,
335
+ "auth": None,
336
+ })
337
+ found = True
338
+ if not found:
339
+ entries.append({
340
+ "path": f"/{filepath}",
341
+ "method": "ALL",
342
+ "file": filepath,
343
+ "line": 1,
344
+ "auth": None,
345
+ })
346
+
347
+ # Also scan services, middleware, and GraphQL for deeper attack surface
348
+ extra_patterns = [
349
+ ("app/services/**/*.rb", "service"),
350
+ ("app/middleware/**/*.rb", "middleware"),
351
+ ("app/graphql/**/*.rb", "graphql"),
352
+ ]
353
+ for pattern, kind in extra_patterns:
354
+ result = fs.glob(pattern, root)
355
+ for filepath in result.get("matches", []):
356
+ if filepath in seen_files:
357
+ continue
358
+ if any(skip in filepath for skip in ["test/", "spec/"]):
359
+ continue
360
+ seen_files.add(filepath)
361
+ entries.append({
362
+ "path": f"[{kind}] {filepath}",
363
+ "method": "ALL",
364
+ "file": filepath,
365
+ "line": 1,
366
+ "auth": None,
367
+ })
368
+
369
+ return entries
370
+
371
+
372
+ def _extract_spring(fs: FileSystemTools, root: str) -> list[dict]:
373
+ """Extract Spring Boot controller mappings."""
374
+ entries = []
375
+ result = fs.glob("**/*.java", root)
376
+ for filepath in result.get("matches", []):
377
+ if any(skip in filepath for skip in ["test/", "Test.java"]):
378
+ continue
379
+ read_result = fs.read_file(filepath)
380
+ if "error" in read_result:
381
+ continue
382
+ content = read_result.get("content", "")
383
+ lines = content.split("\n")
384
+ class_path = ""
385
+ for i, line in enumerate(lines):
386
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
387
+ # Class-level @RequestMapping
388
+ class_match = re.search(r'@RequestMapping\s*\(\s*["\']([^"\']+)', raw_line)
389
+ if class_match:
390
+ class_path = class_match.group(1)
391
+ # Method-level mappings
392
+ method_match = re.search(
393
+ r'@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*\(\s*(?:value\s*=\s*)?["\']([^"\']*)',
394
+ raw_line
395
+ )
396
+ if method_match:
397
+ annotation = method_match.group(1)
398
+ path = method_match.group(2)
399
+ method_map = {
400
+ "GetMapping": "GET", "PostMapping": "POST", "PutMapping": "PUT",
401
+ "PatchMapping": "PATCH", "DeleteMapping": "DELETE", "RequestMapping": "ALL",
402
+ }
403
+ method = method_map.get(annotation, "ALL")
404
+ full_path = f"{class_path}{path}" if class_path else path
405
+ entries.append({
406
+ "path": f"/{full_path}" if not full_path.startswith("/") else full_path,
407
+ "method": method,
408
+ "file": filepath,
409
+ "line": i + 1,
410
+ "auth": None,
411
+ })
412
+ return entries
413
+
414
+
415
+ def _extract_gin(fs: FileSystemTools, root: str) -> list[dict]:
416
+ """Extract Go Gin routes."""
417
+ entries = []
418
+ result = fs.glob("**/*.go", root)
419
+ for filepath in result.get("matches", []):
420
+ if any(skip in filepath for skip in ["test/", "_test.go", "vendor/"]):
421
+ continue
422
+ read_result = fs.read_file(filepath)
423
+ if "error" in read_result:
424
+ continue
425
+ content = read_result.get("content", "")
426
+ lines = content.split("\n")
427
+ for i, line in enumerate(lines):
428
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
429
+ match = re.search(
430
+ r"\.(GET|POST|PUT|PATCH|DELETE|Any|Handle)\s*\(\s*\"([^\"]+)\"",
431
+ raw_line
432
+ )
433
+ if match:
434
+ method = match.group(1).upper()
435
+ path = match.group(2)
436
+ entries.append({
437
+ "path": path,
438
+ "method": method,
439
+ "file": filepath,
440
+ "line": i + 1,
441
+ "auth": None,
442
+ })
443
+ return entries
444
+
445
+
446
+ def _extract_go_http(fs: FileSystemTools, root: str) -> list[dict]:
447
+ """Extract Go net/http and common router routes."""
448
+ entries = []
449
+ result = fs.glob("**/*.go", root)
450
+ for filepath in result.get("matches", []):
451
+ if any(skip in filepath for skip in ["test/", "_test.go", "vendor/"]):
452
+ continue
453
+ read_result = fs.read_file(filepath)
454
+ if "error" in read_result:
455
+ continue
456
+ content = read_result.get("content", "")
457
+ lines = content.split("\n")
458
+ for i, line in enumerate(lines):
459
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
460
+ # http.HandleFunc, mux.HandleFunc, r.HandleFunc
461
+ match = re.search(r'(?:HandleFunc|Handle)\s*\(\s*"([^"]+)"', raw_line)
462
+ if match:
463
+ path = match.group(1)
464
+ entries.append({
465
+ "path": path,
466
+ "method": "ALL",
467
+ "file": filepath,
468
+ "line": i + 1,
469
+ "auth": None,
470
+ })
471
+ return entries
472
+
473
+
474
+ def _extract_graphql(fs: FileSystemTools, root: str) -> list[dict]:
475
+ """Extract GraphQL queries and mutations."""
476
+ entries = []
477
+ for pattern in ["**/*.graphql", "**/*.gql"]:
478
+ result = fs.glob(pattern, root)
479
+ for filepath in result.get("matches", []):
480
+ if "node_modules" in filepath:
481
+ continue
482
+ read_result = fs.read_file(filepath)
483
+ if "error" in read_result:
484
+ continue
485
+ content = read_result.get("content", "")
486
+ lines = content.split("\n")
487
+ current_type = None
488
+ for i, line in enumerate(lines):
489
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
490
+ # Detect type Query { or type Mutation {
491
+ type_match = re.search(r"(?:extend\s+)?type\s+(Query|Mutation|Subscription)\s*\{", raw_line)
492
+ if type_match:
493
+ current_type = type_match.group(1)
494
+ continue
495
+ if current_type and raw_line.strip() == "}":
496
+ current_type = None
497
+ continue
498
+ if current_type:
499
+ # Extract field name
500
+ field_match = re.search(r"(\w+)\s*(?:\(|:)", raw_line.strip())
501
+ if field_match and not raw_line.strip().startswith("#"):
502
+ field_name = field_match.group(1)
503
+ method = "QUERY" if current_type == "Query" else "MUTATION" if current_type == "Mutation" else "SUBSCRIPTION"
504
+ entries.append({
505
+ "path": f"[GraphQL] {current_type}.{field_name}",
506
+ "method": method,
507
+ "file": filepath,
508
+ "line": i + 1,
509
+ "auth": None,
510
+ })
511
+ return entries
512
+
513
+
514
+ def _extract_c_library(fs: FileSystemTools, root: str) -> list[dict]:
515
+ """Extract public C/C++ API functions from header files."""
516
+ entries = []
517
+ for pattern in ["**/*.h", "include/**/*.h"]:
518
+ result = fs.glob(pattern, root)
519
+ for filepath in result.get("matches", []):
520
+ if any(skip in filepath for skip in ["test/", "internal/", "private/"]):
521
+ continue
522
+ read_result = fs.read_file(filepath)
523
+ if "error" in read_result:
524
+ continue
525
+ content = read_result.get("content", "")
526
+ lines = content.split("\n")
527
+ for i, line in enumerate(lines):
528
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
529
+ # Match function declarations (simplified)
530
+ match = re.search(
531
+ r"(?:extern\s+)?(?:const\s+)?(?:unsigned\s+)?(?:int|void|char|size_t|ssize_t|bool|\w+_t|\w+\s*\*)\s+(\w+)\s*\(",
532
+ raw_line
533
+ )
534
+ if match and not raw_line.strip().startswith("//") and not raw_line.strip().startswith("*"):
535
+ func_name = match.group(1)
536
+ # Skip common non-API patterns
537
+ if func_name.startswith("_") or func_name in ("main", "static", "inline"):
538
+ continue
539
+ entries.append({
540
+ "path": f"[C] {func_name}()",
541
+ "method": "FUNC",
542
+ "file": filepath,
543
+ "line": i + 1,
544
+ "auth": None,
545
+ })
546
+ return entries
547
+
548
+
549
+ def _extract_sails(fs: FileSystemTools, root: str) -> list[dict]:
550
+ """Extract Sails.js routes from config/routes.js."""
551
+ entries = []
552
+ filepath = f"{root}/config/routes.js" if root != "." else "config/routes.js"
553
+ # Also check server/config/routes.js
554
+ for fp in [filepath, f"{root}/server/config/routes.js" if root != "." else "server/config/routes.js"]:
555
+ read_result = fs.read_file(fp)
556
+ if "error" in read_result:
557
+ continue
558
+ content = read_result.get("content", "")
559
+ lines = content.split("\n")
560
+ for i, line in enumerate(lines):
561
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
562
+ # Match: 'GET /api/users': 'users/list'
563
+ match = re.search(r"['\"](?:(GET|POST|PUT|PATCH|DELETE)\s+)?(/[^'\"]+)['\"]", raw_line)
564
+ if match:
565
+ method = match.group(1) or "ALL"
566
+ path = match.group(2)
567
+ entries.append({
568
+ "path": path,
569
+ "method": method,
570
+ "file": fp,
571
+ "line": i + 1,
572
+ "auth": None,
573
+ })
574
+ break # Found routes file, stop looking
575
+ return entries
576
+
577
+
578
+ def _extract_nestjs(fs: FileSystemTools, root: str) -> list[dict]:
579
+ """Extract NestJS controller routes from decorators."""
580
+ entries = []
581
+ result = fs.glob("**/*.ts", root)
582
+ for filepath in result.get("matches", []):
583
+ if any(skip in filepath for skip in ["node_modules/", "test/", "dist/", ".spec.", ".test."]):
584
+ continue
585
+ # Only look at controller and gateway files
586
+ if not any(kw in filepath.lower() for kw in ["controller", "gateway", "resolver"]):
587
+ continue
588
+ read_result = fs.read_file(filepath)
589
+ if "error" in read_result:
590
+ continue
591
+ content = read_result.get("content", "")
592
+ lines = content.split("\n")
593
+ class_path = ""
594
+ for i, line in enumerate(lines):
595
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
596
+ # Class-level @Controller('/path')
597
+ controller_match = re.search(r"@Controller\s*\(\s*['\"]([^'\"]*)['\"]", raw_line)
598
+ if controller_match:
599
+ class_path = controller_match.group(1)
600
+ continue
601
+ # Method-level @Get(), @Post(), etc.
602
+ method_match = re.search(
603
+ r"@(Get|Post|Put|Patch|Delete|All)\s*\(\s*(?:['\"]([^'\"]*)['\"])?\s*\)",
604
+ raw_line
605
+ )
606
+ if method_match:
607
+ method = method_match.group(1).upper()
608
+ path = method_match.group(2) or ""
609
+ full_path = f"/{class_path}/{path}".replace("//", "/").rstrip("/") or "/"
610
+ entries.append({
611
+ "path": full_path,
612
+ "method": method,
613
+ "file": filepath,
614
+ "line": i + 1,
615
+ "auth": None,
616
+ })
617
+ # WebSocket @SubscribeMessage
618
+ ws_match = re.search(r"@SubscribeMessage\s*\(\s*['\"]([^'\"]+)['\"]", raw_line)
619
+ if ws_match:
620
+ entries.append({
621
+ "path": f"[WebSocket] {ws_match.group(1)}",
622
+ "method": "WS",
623
+ "file": filepath,
624
+ "line": i + 1,
625
+ "auth": None,
626
+ })
627
+ # GraphQL @Query, @Mutation
628
+ gql_match = re.search(r"@(Query|Mutation|Subscription)\s*\(", raw_line)
629
+ if gql_match:
630
+ gql_type = gql_match.group(1).upper()
631
+ # Try to get the function name from next line
632
+ func_match = re.search(r"(?:async\s+)?(\w+)\s*\(", lines[min(i+1, len(lines)-1)] if i+1 < len(lines) else "")
633
+ func_name = func_match.group(1) if func_match else "unknown"
634
+ entries.append({
635
+ "path": f"[GraphQL] {gql_type}.{func_name}",
636
+ "method": gql_type,
637
+ "file": filepath,
638
+ "line": i + 1,
639
+ "auth": None,
640
+ })
641
+ return entries
642
+
643
+
644
+ def _extract_php_raw(fs: FileSystemTools, root: str) -> list[dict]:
645
+ """Extract entry points from PHP — focus on controllers and route handlers, not all files."""
646
+ entries = []
647
+ # Look for files in controller-like directories
648
+ controller_patterns = [
649
+ "**/controllers/**/*.php",
650
+ "**/Controller/**/*.php",
651
+ "**/Controllers/**/*.php",
652
+ "**/api/**/*.php",
653
+ "**/routes/**/*.php",
654
+ "**/handlers/**/*.php",
655
+ "**/actions/**/*.php",
656
+ "**/endpoints/**/*.php",
657
+ ]
658
+ seen_files = set()
659
+ for pattern in controller_patterns:
660
+ result = fs.glob(pattern, root)
661
+ for filepath in result.get("matches", []):
662
+ if any(skip in filepath for skip in ["vendor/", "test/", "migrations/", "node_modules/"]):
663
+ continue
664
+ if filepath in seen_files:
665
+ continue
666
+ seen_files.add(filepath)
667
+
668
+ # Read the file and try to extract routes/actions
669
+ read_result = fs.read_file(filepath)
670
+ if "error" in read_result:
671
+ entries.append({
672
+ "path": f"/{filepath}",
673
+ "method": "ALL",
674
+ "file": filepath,
675
+ "line": 1,
676
+ "auth": None,
677
+ })
678
+ continue
679
+
680
+ content = read_result.get("content", "")
681
+ lines = content.split("\n")
682
+ found_methods = False
683
+ for i, line in enumerate(lines):
684
+ raw_line = line.split("\t", 1)[1] if "\t" in line else line
685
+ # Match public function names in controllers
686
+ match = re.search(r"public\s+function\s+(\w+)\s*\(", raw_line)
687
+ if match:
688
+ func_name = match.group(1)
689
+ if func_name.startswith("__"): # Skip magic methods
690
+ continue
691
+ entries.append({
692
+ "path": f"/{filepath}::{func_name}()",
693
+ "method": "ALL",
694
+ "file": filepath,
695
+ "line": i + 1,
696
+ "auth": None,
697
+ })
698
+ found_methods = True
699
+
700
+ if not found_methods:
701
+ entries.append({
702
+ "path": f"/{filepath}",
703
+ "method": "ALL",
704
+ "file": filepath,
705
+ "line": 1,
706
+ "auth": None,
707
+ })
708
+
709
+ return entries
710
+
711
+
712
+ # Map framework names to their extractors
713
+ _EXTRACTORS = {
714
+ "nextjs": _extract_nextjs,
715
+ "express": _extract_express,
716
+ "fastify": _extract_express, # Similar enough pattern
717
+ "nestjs": _extract_nestjs,
718
+ "hono": _extract_express,
719
+ "koa": _extract_express,
720
+ "sails": _extract_sails,
721
+ "nuxt": _extract_nextjs, # Similar file-based routing
722
+ "sveltekit": _extract_nextjs,
723
+ "django": _extract_django,
724
+ "django_rest": _extract_django,
725
+ "flask": _extract_flask,
726
+ "fastapi": _extract_fastapi,
727
+ "starlette": _extract_fastapi,
728
+ "laravel": _extract_laravel,
729
+ "symfony": _extract_laravel, # Similar routing patterns
730
+ "codeigniter": _extract_php_raw,
731
+ "php_raw": _extract_php_raw,
732
+ "rails": _extract_rails,
733
+ "sinatra": _extract_flask, # Similar decorator pattern
734
+ "spring": _extract_spring,
735
+ "gin": _extract_gin,
736
+ "echo": _extract_gin, # Similar pattern
737
+ "fiber": _extract_gin,
738
+ "chi": _extract_go_http,
739
+ "net_http": _extract_go_http,
740
+ "gorilla": _extract_go_http,
741
+ "graphql": _extract_graphql,
742
+ "c": _extract_c_library,
743
+ "cpp": _extract_c_library,
744
+ # Frameworks without specific extractors fall through to manager agent
745
+ }