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,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)