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.
- openhack/__init__.py +2 -0
- openhack/__main__.py +225 -0
- openhack/agents/__init__.py +30 -0
- openhack/agents/base.py +230 -0
- openhack/agents/browser_verifier.py +679 -0
- openhack/agents/browser_verifier_swarm.py +256 -0
- openhack/agents/checkpoint.py +89 -0
- openhack/agents/context_manager.py +356 -0
- openhack/agents/coordinator.py +1105 -0
- openhack/agents/endpoint_analyst.py +307 -0
- openhack/agents/feature_hunter.py +93 -0
- openhack/agents/hunter.py +481 -0
- openhack/agents/hunter_swarm.py +385 -0
- openhack/agents/llm.py +334 -0
- openhack/agents/recon.py +19 -0
- openhack/agents/sandbox_verifier.py +396 -0
- openhack/agents/sandbox_verifier_swarm.py +250 -0
- openhack/agents/session.py +286 -0
- openhack/agents/validator.py +217 -0
- openhack/agents/validator_swarm.py +106 -0
- openhack/auth.py +175 -0
- openhack/browser/__init__.py +12 -0
- openhack/browser/runner.py +385 -0
- openhack/categories.py +130 -0
- openhack/config.py +201 -0
- openhack/deterministic_recon.py +464 -0
- openhack/entry_points.py +745 -0
- openhack/framework_classifier.py +515 -0
- openhack/framework_detection.py +269 -0
- openhack/headless_scan.py +179 -0
- openhack/prompts/__init__.py +108 -0
- openhack/prompts/browser_verifier.py +171 -0
- openhack/prompts/coordinator.py +31 -0
- openhack/prompts/django/__init__.py +32 -0
- openhack/prompts/django/auth_bypass.py +76 -0
- openhack/prompts/django/csrf.py +62 -0
- openhack/prompts/django/data_exposure.py +67 -0
- openhack/prompts/django/idor.py +74 -0
- openhack/prompts/django/injection.py +67 -0
- openhack/prompts/django/misconfiguration.py +70 -0
- openhack/prompts/django/ssrf.py +64 -0
- openhack/prompts/endpoint_analyst.py +122 -0
- openhack/prompts/express/__init__.py +29 -0
- openhack/prompts/express/auth_bypass.py +71 -0
- openhack/prompts/express/data_exposure.py +77 -0
- openhack/prompts/express/idor.py +69 -0
- openhack/prompts/express/injection.py +75 -0
- openhack/prompts/express/misconfiguration.py +72 -0
- openhack/prompts/express/ssrf.py +63 -0
- openhack/prompts/feature_hunter.py +140 -0
- openhack/prompts/flask/__init__.py +29 -0
- openhack/prompts/flask/auth_bypass.py +86 -0
- openhack/prompts/flask/data_exposure.py +78 -0
- openhack/prompts/flask/idor.py +83 -0
- openhack/prompts/flask/injection.py +77 -0
- openhack/prompts/flask/misconfiguration.py +73 -0
- openhack/prompts/flask/ssrf.py +65 -0
- openhack/prompts/hunter.py +362 -0
- openhack/prompts/hunter_continuation_loop.py +12 -0
- openhack/prompts/hunter_continuation_no_findings.py +19 -0
- openhack/prompts/hunter_continuation_no_progress.py +22 -0
- openhack/prompts/hunter_tool_instructions.py +55 -0
- openhack/prompts/nextjs/__init__.py +42 -0
- openhack/prompts/nextjs/auth_bypass.py +80 -0
- openhack/prompts/nextjs/csrf.py +71 -0
- openhack/prompts/nextjs/data_exposure.py +88 -0
- openhack/prompts/nextjs/idor.py +64 -0
- openhack/prompts/nextjs/injection.py +65 -0
- openhack/prompts/nextjs/middleware_bypass.py +75 -0
- openhack/prompts/nextjs/misconfiguration.py +92 -0
- openhack/prompts/nextjs/server_actions.py +97 -0
- openhack/prompts/nextjs/ssrf.py +66 -0
- openhack/prompts/nextjs/xss.py +69 -0
- openhack/prompts/pr_analysis_system.py +80 -0
- openhack/prompts/pr_analysis_user.py +11 -0
- openhack/prompts/project_context.py +89 -0
- openhack/prompts/recon.py +199 -0
- openhack/prompts/reporter.py +88 -0
- openhack/prompts/researchers.py +434 -0
- openhack/prompts/sandbox_verifier.py +128 -0
- openhack/prompts/supabase/__init__.py +39 -0
- openhack/prompts/supabase/auth_tokens.py +131 -0
- openhack/prompts/supabase/edge_functions.py +150 -0
- openhack/prompts/supabase/graphql.py +102 -0
- openhack/prompts/supabase/postgrest.py +99 -0
- openhack/prompts/supabase/realtime.py +93 -0
- openhack/prompts/supabase/rls.py +110 -0
- openhack/prompts/supabase/rpc_functions.py +127 -0
- openhack/prompts/supabase/storage.py +110 -0
- openhack/prompts/supabase/tenant_isolation.py +118 -0
- openhack/prompts/validator.py +319 -0
- openhack/prompts/validator_continuation_incomplete.py +12 -0
- openhack/prompts/validator_tool_instructions.py +29 -0
- openhack/quality.py +231 -0
- openhack/sandbox/__init__.py +12 -0
- openhack/sandbox/orchestrator.py +517 -0
- openhack/sandbox/runner.py +177 -0
- openhack/scan_session.py +245 -0
- openhack/setup.py +452 -0
- openhack/static_validator.py +612 -0
- openhack/tools/__init__.py +1 -0
- openhack/tools/ast_tools.py +307 -0
- openhack/tools/coverage.py +1078 -0
- openhack/tools/filesystem.py +404 -0
- openhack/tools/nextjs.py +258 -0
- openhack/tools/registry.py +52 -0
- openhack/tui.py +3450 -0
- openhack/updates.py +170 -0
- openhack-0.1.0.dist-info/METADATA +189 -0
- openhack-0.1.0.dist-info/RECORD +113 -0
- openhack-0.1.0.dist-info/WHEEL +4 -0
- openhack-0.1.0.dist-info/entry_points.txt +2 -0
- openhack-0.1.0.dist-info/licenses/LICENSE +661 -0
openhack/entry_points.py
ADDED
|
@@ -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
|
+
}
|