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
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AST and code analysis tools for vulnerability scanning.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .filesystem import FileSystemTools
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ASTTools:
|
|
13
|
+
"""Tools for code analysis and pattern detection."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, fs_tools: FileSystemTools):
|
|
16
|
+
self.fs = fs_tools
|
|
17
|
+
|
|
18
|
+
def _get_raw_content(self, file_result: dict) -> str:
|
|
19
|
+
"""Extract raw content from file read result (remove line numbers)."""
|
|
20
|
+
if "error" in file_result:
|
|
21
|
+
return ""
|
|
22
|
+
lines = file_result["content"].split("\n")
|
|
23
|
+
return "\n".join(line.split("\t", 1)[1] if "\t" in line else line for line in lines)
|
|
24
|
+
|
|
25
|
+
def extract_functions(self, path: str) -> dict:
|
|
26
|
+
"""Extract all function definitions from a file."""
|
|
27
|
+
result = self.fs.read_file(path)
|
|
28
|
+
if "error" in result:
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
content = self._get_raw_content(result)
|
|
32
|
+
functions = []
|
|
33
|
+
|
|
34
|
+
patterns = [
|
|
35
|
+
(r"(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)", "function"),
|
|
36
|
+
(r"(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>", "arrow"),
|
|
37
|
+
(r"(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(([^)]*)\)", "function_expr"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
for pattern, func_type in patterns:
|
|
41
|
+
for match in re.finditer(pattern, content):
|
|
42
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
43
|
+
functions.append({
|
|
44
|
+
"name": match.group(1),
|
|
45
|
+
"params": match.group(2).strip(),
|
|
46
|
+
"type": func_type,
|
|
47
|
+
"line": line_num,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return {"file": path, "functions": functions}
|
|
51
|
+
|
|
52
|
+
def extract_exports(self, path: str) -> dict:
|
|
53
|
+
"""Extract all exports from a file."""
|
|
54
|
+
result = self.fs.read_file(path)
|
|
55
|
+
if "error" in result:
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
content = self._get_raw_content(result)
|
|
59
|
+
exports = {"named": [], "default": None}
|
|
60
|
+
|
|
61
|
+
named_pattern = r"export\s+(?:const|let|var|function|class|async\s+function)\s+(\w+)"
|
|
62
|
+
for match in re.finditer(named_pattern, content):
|
|
63
|
+
exports["named"].append(match.group(1))
|
|
64
|
+
|
|
65
|
+
export_list_pattern = r"export\s*\{([^}]+)\}"
|
|
66
|
+
for match in re.finditer(export_list_pattern, content):
|
|
67
|
+
items = [item.strip().split(" as ")[0].strip() for item in match.group(1).split(",")]
|
|
68
|
+
exports["named"].extend(items)
|
|
69
|
+
|
|
70
|
+
default_patterns = [
|
|
71
|
+
r"export\s+default\s+(?:function|class)\s+(\w+)",
|
|
72
|
+
r"export\s+default\s+(\w+)",
|
|
73
|
+
]
|
|
74
|
+
for pattern in default_patterns:
|
|
75
|
+
match = re.search(pattern, content)
|
|
76
|
+
if match:
|
|
77
|
+
exports["default"] = match.group(1)
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
return {"file": path, "exports": exports}
|
|
81
|
+
|
|
82
|
+
def extract_imports(self, path: str) -> dict:
|
|
83
|
+
"""Extract all imports from a file."""
|
|
84
|
+
result = self.fs.read_file(path)
|
|
85
|
+
if "error" in result:
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
content = self._get_raw_content(result)
|
|
89
|
+
imports = []
|
|
90
|
+
|
|
91
|
+
patterns = [
|
|
92
|
+
r"import\s+(\w+)\s+from\s+['\"]([^'\"]+)['\"]",
|
|
93
|
+
r"import\s*\{([^}]+)\}\s*from\s*['\"]([^'\"]+)['\"]",
|
|
94
|
+
r"import\s*\*\s*as\s+(\w+)\s+from\s+['\"]([^'\"]+)['\"]",
|
|
95
|
+
r"import\s+['\"]([^'\"]+)['\"]",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
for match in re.finditer(patterns[0], content):
|
|
99
|
+
imports.append({"type": "default", "name": match.group(1), "source": match.group(2)})
|
|
100
|
+
|
|
101
|
+
for match in re.finditer(patterns[1], content):
|
|
102
|
+
names = [n.strip().split(" as ")[0].strip() for n in match.group(1).split(",")]
|
|
103
|
+
imports.append({"type": "named", "names": names, "source": match.group(2)})
|
|
104
|
+
|
|
105
|
+
for match in re.finditer(patterns[2], content):
|
|
106
|
+
imports.append({"type": "namespace", "name": match.group(1), "source": match.group(2)})
|
|
107
|
+
|
|
108
|
+
for match in re.finditer(patterns[3], content):
|
|
109
|
+
if not any(match.group(1) in str(i.get("source", "")) for i in imports):
|
|
110
|
+
imports.append({"type": "side_effect", "source": match.group(1)})
|
|
111
|
+
|
|
112
|
+
return {"file": path, "imports": imports}
|
|
113
|
+
|
|
114
|
+
def find_api_handlers(self, path: str) -> dict:
|
|
115
|
+
"""Find HTTP method handlers (GET, POST, etc.) in a route file."""
|
|
116
|
+
result = self.fs.read_file(path)
|
|
117
|
+
if "error" in result:
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
content = self._get_raw_content(result)
|
|
121
|
+
handlers = []
|
|
122
|
+
|
|
123
|
+
http_methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]
|
|
124
|
+
|
|
125
|
+
for method in http_methods:
|
|
126
|
+
patterns = [
|
|
127
|
+
rf"export\s+(?:async\s+)?function\s+{method}\s*\(",
|
|
128
|
+
rf"export\s+const\s+{method}\s*=",
|
|
129
|
+
]
|
|
130
|
+
for pattern in patterns:
|
|
131
|
+
match = re.search(pattern, content)
|
|
132
|
+
if match:
|
|
133
|
+
line_num = content[: match.start()].count("\n") + 1
|
|
134
|
+
handlers.append({"method": method, "line": line_num})
|
|
135
|
+
|
|
136
|
+
if "export default" in content:
|
|
137
|
+
handler_match = re.search(
|
|
138
|
+
r"export\s+default\s+(?:async\s+)?function\s*(?:\w*)?\s*\(\s*req",
|
|
139
|
+
content,
|
|
140
|
+
)
|
|
141
|
+
if handler_match:
|
|
142
|
+
handlers.append({"method": "DEFAULT_HANDLER", "line": content[: handler_match.start()].count("\n") + 1})
|
|
143
|
+
|
|
144
|
+
return {"file": path, "handlers": handlers}
|
|
145
|
+
|
|
146
|
+
def trace_variable(self, path: str, variable_name: str) -> dict:
|
|
147
|
+
"""Trace all usages of a variable through a file to understand data flow."""
|
|
148
|
+
result = self.fs.read_file(path)
|
|
149
|
+
if "error" in result:
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
content = self._get_raw_content(result)
|
|
153
|
+
usages = []
|
|
154
|
+
|
|
155
|
+
lines = content.split("\n")
|
|
156
|
+
for i, line in enumerate(lines, 1):
|
|
157
|
+
if re.search(rf"\b{re.escape(variable_name)}\b", line):
|
|
158
|
+
context = "unknown"
|
|
159
|
+
if re.search(rf"(?:const|let|var)\s+.*{re.escape(variable_name)}", line):
|
|
160
|
+
context = "declaration"
|
|
161
|
+
elif re.search(rf"{re.escape(variable_name)}\s*=", line):
|
|
162
|
+
context = "assignment"
|
|
163
|
+
elif re.search(rf"(?:params|query|body|searchParams).*{re.escape(variable_name)}", line):
|
|
164
|
+
context = "input_source"
|
|
165
|
+
elif re.search(rf"(?:sql|query|exec|eval|innerHTML|dangerouslySetInnerHTML).*{re.escape(variable_name)}", line):
|
|
166
|
+
context = "dangerous_sink"
|
|
167
|
+
elif re.search(rf"return.*{re.escape(variable_name)}", line):
|
|
168
|
+
context = "return"
|
|
169
|
+
else:
|
|
170
|
+
context = "usage"
|
|
171
|
+
|
|
172
|
+
usages.append({"line": i, "content": line.strip(), "context": context})
|
|
173
|
+
|
|
174
|
+
return {"file": path, "variable": variable_name, "usages": usages}
|
|
175
|
+
|
|
176
|
+
def find_dangerous_patterns(self, path: str) -> dict:
|
|
177
|
+
"""Find potentially dangerous code patterns (eval, innerHTML, SQL injection, etc.)."""
|
|
178
|
+
result = self.fs.read_file(path)
|
|
179
|
+
if "error" in result:
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
content = self._get_raw_content(result)
|
|
183
|
+
findings = []
|
|
184
|
+
|
|
185
|
+
dangerous_patterns = [
|
|
186
|
+
(r"dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:", "XSS", "dangerouslySetInnerHTML usage"),
|
|
187
|
+
(r"eval\s*\(", "RCE", "eval() usage"),
|
|
188
|
+
(r"new\s+Function\s*\(", "RCE", "Function constructor"),
|
|
189
|
+
(r"innerHTML\s*=", "XSS", "innerHTML assignment"),
|
|
190
|
+
(r"document\.write\s*\(", "XSS", "document.write usage"),
|
|
191
|
+
(r"\$\{.*\}\s*(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)", "SQLi", "String interpolation in SQL"),
|
|
192
|
+
(r"exec\s*\(\s*[`'\"].*\$\{", "RCE", "Command injection risk"),
|
|
193
|
+
(r"child_process.*exec", "RCE", "child_process exec usage"),
|
|
194
|
+
(r"redirect\s*\(\s*(?:req|request|params|query|searchParams)", "Open Redirect", "User-controlled redirect"),
|
|
195
|
+
(r"(?:fetch|axios|http\.request)\s*\(\s*(?:req|request|params|query|url)", "SSRF", "User-controlled URL in request"),
|
|
196
|
+
(r"\.env\b", "Info Leak", "Potential env file access"),
|
|
197
|
+
(r"(?:password|secret|key|token)\s*=\s*['\"][^'\"]+['\"]", "Hardcoded Secret", "Hardcoded credential"),
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
lines = content.split("\n")
|
|
201
|
+
for pattern, category, description in dangerous_patterns:
|
|
202
|
+
for i, line in enumerate(lines, 1):
|
|
203
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
204
|
+
findings.append({
|
|
205
|
+
"line": i,
|
|
206
|
+
"category": category,
|
|
207
|
+
"description": description,
|
|
208
|
+
"content": line.strip()[:200],
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
return {"file": path, "findings": findings}
|
|
212
|
+
|
|
213
|
+
def get_tool_definitions(self) -> list[dict]:
|
|
214
|
+
"""Return OpenAI-compatible tool definitions."""
|
|
215
|
+
return [
|
|
216
|
+
{
|
|
217
|
+
"name": "extract_functions",
|
|
218
|
+
"description": "Extract all function definitions from a file.",
|
|
219
|
+
"parameters": {
|
|
220
|
+
"type": "object",
|
|
221
|
+
"properties": {
|
|
222
|
+
"path": {"type": "string", "description": "File path to analyze"},
|
|
223
|
+
},
|
|
224
|
+
"required": ["path"],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"name": "extract_exports",
|
|
229
|
+
"description": "Extract all exports from a file.",
|
|
230
|
+
"parameters": {
|
|
231
|
+
"type": "object",
|
|
232
|
+
"properties": {
|
|
233
|
+
"path": {"type": "string", "description": "File path to analyze"},
|
|
234
|
+
},
|
|
235
|
+
"required": ["path"],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"name": "extract_imports",
|
|
240
|
+
"description": "Extract all imports from a file.",
|
|
241
|
+
"parameters": {
|
|
242
|
+
"type": "object",
|
|
243
|
+
"properties": {
|
|
244
|
+
"path": {"type": "string", "description": "File path to analyze"},
|
|
245
|
+
},
|
|
246
|
+
"required": ["path"],
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"name": "find_api_handlers",
|
|
251
|
+
"description": "Find HTTP method handlers (GET, POST, etc.) in a route file.",
|
|
252
|
+
"parameters": {
|
|
253
|
+
"type": "object",
|
|
254
|
+
"properties": {
|
|
255
|
+
"path": {"type": "string", "description": "File path to analyze"},
|
|
256
|
+
},
|
|
257
|
+
"required": ["path"],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"name": "trace_variable",
|
|
262
|
+
"description": "Trace all usages of a variable through a file to understand data flow.",
|
|
263
|
+
"parameters": {
|
|
264
|
+
"type": "object",
|
|
265
|
+
"properties": {
|
|
266
|
+
"path": {"type": "string", "description": "File path to analyze"},
|
|
267
|
+
"variable_name": {"type": "string", "description": "Variable name to trace"},
|
|
268
|
+
},
|
|
269
|
+
"required": ["path", "variable_name"],
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"name": "find_dangerous_patterns",
|
|
274
|
+
"description": "Find potentially dangerous code patterns (eval, innerHTML, SQL injection, etc.).",
|
|
275
|
+
"parameters": {
|
|
276
|
+
"type": "object",
|
|
277
|
+
"properties": {
|
|
278
|
+
"path": {"type": "string", "description": "File path to analyze"},
|
|
279
|
+
},
|
|
280
|
+
"required": ["path"],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
def execute_tool(self, name: str, arguments: dict) -> dict:
|
|
286
|
+
"""Execute a tool by name with the given arguments.
|
|
287
|
+
|
|
288
|
+
Filters out unexpected keyword arguments that the LLM may hallucinate.
|
|
289
|
+
"""
|
|
290
|
+
import inspect
|
|
291
|
+
|
|
292
|
+
tools = {
|
|
293
|
+
"extract_functions": self.extract_functions,
|
|
294
|
+
"extract_exports": self.extract_exports,
|
|
295
|
+
"extract_imports": self.extract_imports,
|
|
296
|
+
"find_api_handlers": self.find_api_handlers,
|
|
297
|
+
"trace_variable": self.trace_variable,
|
|
298
|
+
"find_dangerous_patterns": self.find_dangerous_patterns,
|
|
299
|
+
}
|
|
300
|
+
if name not in tools:
|
|
301
|
+
return {"error": f"Unknown tool: {name}"}
|
|
302
|
+
|
|
303
|
+
func = tools[name]
|
|
304
|
+
sig = inspect.signature(func)
|
|
305
|
+
valid_params = set(sig.parameters.keys())
|
|
306
|
+
filtered_args = {k: v for k, v in arguments.items() if k in valid_params}
|
|
307
|
+
return func(**filtered_args)
|