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,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deterministic framework detection for vulnerability scanning.
|
|
3
|
+
|
|
4
|
+
Detects the tech stack of a target repository by reading indicator files
|
|
5
|
+
(package.json, manage.py, requirements.txt, etc.) and returns a list of
|
|
6
|
+
detected frameworks with their root directories. Supports monorepos where
|
|
7
|
+
multiple frameworks live in different subdirectories.
|
|
8
|
+
|
|
9
|
+
No LLM calls -- pure file existence + content checks.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from openhack.tools.filesystem import FileSystemTools
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
SKIP_DIRS = {
|
|
24
|
+
"node_modules", ".git", "__pycache__", "venv", ".venv",
|
|
25
|
+
"dist", "build", ".next", "coverage", ".nyc_output",
|
|
26
|
+
"vendor", ".tox", "egg-info", ".eggs",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _read_raw(fs: FileSystemTools, path: str) -> str | None:
|
|
31
|
+
"""Read a file via FileSystemTools and return raw content (no line numbers)."""
|
|
32
|
+
result = fs.read_file(path)
|
|
33
|
+
if "error" in result:
|
|
34
|
+
return None
|
|
35
|
+
lines = result["content"].split("\n")
|
|
36
|
+
return "\n".join(
|
|
37
|
+
line.split("\t", 1)[1] if "\t" in line else line for line in lines
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_json(fs: FileSystemTools, path: str) -> dict | None:
|
|
42
|
+
raw = _read_raw(fs, path)
|
|
43
|
+
if raw is None:
|
|
44
|
+
return None
|
|
45
|
+
try:
|
|
46
|
+
return json.loads(raw)
|
|
47
|
+
except (json.JSONDecodeError, ValueError):
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _dir_of(path: str) -> str:
|
|
52
|
+
"""Return the parent directory of a file path, or '.' for root-level files."""
|
|
53
|
+
parent = str(Path(path).parent)
|
|
54
|
+
return "." if parent == "." or parent == "" else parent
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _glob_no_skip(fs: FileSystemTools, pattern: str) -> list[str]:
|
|
58
|
+
"""Glob for files, filtering out matches inside SKIP_DIRS."""
|
|
59
|
+
result = fs.glob(pattern)
|
|
60
|
+
if "error" in result:
|
|
61
|
+
return []
|
|
62
|
+
matches = []
|
|
63
|
+
for m in result.get("matches", []):
|
|
64
|
+
parts = Path(m).parts
|
|
65
|
+
if not SKIP_DIRS.intersection(parts):
|
|
66
|
+
matches.append(m)
|
|
67
|
+
return matches
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _detect_nextjs(fs: FileSystemTools) -> list[dict]:
|
|
71
|
+
"""Detect Next.js projects by next.config.* or 'next' in package.json deps."""
|
|
72
|
+
found: list[dict] = []
|
|
73
|
+
seen_roots: set[str] = set()
|
|
74
|
+
|
|
75
|
+
for config_name in ("next.config.js", "next.config.ts", "next.config.mjs"):
|
|
76
|
+
for match in _glob_no_skip(fs, f"**/{config_name}"):
|
|
77
|
+
root = _dir_of(match)
|
|
78
|
+
if root not in seen_roots:
|
|
79
|
+
seen_roots.add(root)
|
|
80
|
+
found.append({"framework": "nextjs", "root": root})
|
|
81
|
+
|
|
82
|
+
for pkg_path in _glob_no_skip(fs, "**/package.json"):
|
|
83
|
+
root = _dir_of(pkg_path)
|
|
84
|
+
if root in seen_roots:
|
|
85
|
+
continue
|
|
86
|
+
pkg = _parse_json(fs, pkg_path)
|
|
87
|
+
if pkg is None:
|
|
88
|
+
continue
|
|
89
|
+
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
|
90
|
+
if "next" in deps:
|
|
91
|
+
seen_roots.add(root)
|
|
92
|
+
found.append({"framework": "nextjs", "root": root})
|
|
93
|
+
|
|
94
|
+
return found
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _detect_express(fs: FileSystemTools) -> list[dict]:
|
|
98
|
+
"""Detect Express.js projects by 'express' in package.json deps."""
|
|
99
|
+
found: list[dict] = []
|
|
100
|
+
seen_roots: set[str] = set()
|
|
101
|
+
|
|
102
|
+
for pkg_path in _glob_no_skip(fs, "**/package.json"):
|
|
103
|
+
root = _dir_of(pkg_path)
|
|
104
|
+
if root in seen_roots:
|
|
105
|
+
continue
|
|
106
|
+
pkg = _parse_json(fs, pkg_path)
|
|
107
|
+
if pkg is None:
|
|
108
|
+
continue
|
|
109
|
+
deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
|
|
110
|
+
if "express" in deps:
|
|
111
|
+
seen_roots.add(root)
|
|
112
|
+
found.append({"framework": "express", "root": root})
|
|
113
|
+
|
|
114
|
+
return found
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _detect_django(fs: FileSystemTools) -> list[dict]:
|
|
118
|
+
"""Detect Django by manage.py + django in Python dependency files."""
|
|
119
|
+
found: list[dict] = []
|
|
120
|
+
seen_roots: set[str] = set()
|
|
121
|
+
|
|
122
|
+
for manage_path in _glob_no_skip(fs, "**/manage.py"):
|
|
123
|
+
root = _dir_of(manage_path)
|
|
124
|
+
if root in seen_roots:
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
content = _read_raw(fs, manage_path)
|
|
128
|
+
if content and "django" not in content.lower():
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
has_django_dep = False
|
|
132
|
+
for dep_file in ("requirements.txt", "Pipfile", "pyproject.toml", "setup.cfg"):
|
|
133
|
+
dep_path = f"{root}/{dep_file}" if root != "." else dep_file
|
|
134
|
+
dep_content = _read_raw(fs, dep_path)
|
|
135
|
+
if dep_content and re.search(r"django", dep_content, re.IGNORECASE):
|
|
136
|
+
has_django_dep = True
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
if not has_django_dep:
|
|
140
|
+
settings_matches = _glob_no_skip(fs, f"{'**/' if root == '.' else root + '/'}**/settings.py")
|
|
141
|
+
if settings_matches:
|
|
142
|
+
has_django_dep = True
|
|
143
|
+
|
|
144
|
+
if has_django_dep:
|
|
145
|
+
seen_roots.add(root)
|
|
146
|
+
found.append({"framework": "django", "root": root})
|
|
147
|
+
|
|
148
|
+
return found
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _detect_flask(fs: FileSystemTools) -> list[dict]:
|
|
152
|
+
"""Detect Flask by dependency files or app = Flask( pattern."""
|
|
153
|
+
found: list[dict] = []
|
|
154
|
+
seen_roots: set[str] = set()
|
|
155
|
+
|
|
156
|
+
for dep_file in ("requirements.txt", "Pipfile", "pyproject.toml", "setup.cfg"):
|
|
157
|
+
for dep_path in _glob_no_skip(fs, f"**/{dep_file}"):
|
|
158
|
+
root = _dir_of(dep_path)
|
|
159
|
+
if root in seen_roots:
|
|
160
|
+
continue
|
|
161
|
+
content = _read_raw(fs, dep_path)
|
|
162
|
+
if content and re.search(r"(?:^|\s|['\"])flask(?:\s|['\"><=,\[]|$)", content, re.IGNORECASE | re.MULTILINE):
|
|
163
|
+
seen_roots.add(root)
|
|
164
|
+
found.append({"framework": "flask", "root": root})
|
|
165
|
+
|
|
166
|
+
return found
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _detect_rails(fs: FileSystemTools) -> list[dict]:
|
|
170
|
+
"""Detect Rails by Gemfile with 'rails' or config/routes.rb."""
|
|
171
|
+
found: list[dict] = []
|
|
172
|
+
seen_roots: set[str] = set()
|
|
173
|
+
|
|
174
|
+
for gemfile_path in _glob_no_skip(fs, "**/Gemfile"):
|
|
175
|
+
root = _dir_of(gemfile_path)
|
|
176
|
+
if root in seen_roots:
|
|
177
|
+
continue
|
|
178
|
+
content = _read_raw(fs, gemfile_path)
|
|
179
|
+
if content and re.search(r"['\"]rails['\"]", content):
|
|
180
|
+
seen_roots.add(root)
|
|
181
|
+
found.append({"framework": "rails", "root": root})
|
|
182
|
+
|
|
183
|
+
for routes_path in _glob_no_skip(fs, "**/config/routes.rb"):
|
|
184
|
+
root = str(Path(_dir_of(routes_path)).parent)
|
|
185
|
+
if root == "":
|
|
186
|
+
root = "."
|
|
187
|
+
if root not in seen_roots:
|
|
188
|
+
seen_roots.add(root)
|
|
189
|
+
found.append({"framework": "rails", "root": root})
|
|
190
|
+
|
|
191
|
+
return found
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _detect_spring(fs: FileSystemTools) -> list[dict]:
|
|
195
|
+
"""Detect Spring Boot by pom.xml or build.gradle with spring-boot."""
|
|
196
|
+
found: list[dict] = []
|
|
197
|
+
seen_roots: set[str] = set()
|
|
198
|
+
|
|
199
|
+
for pom_path in _glob_no_skip(fs, "**/pom.xml"):
|
|
200
|
+
root = _dir_of(pom_path)
|
|
201
|
+
if root in seen_roots:
|
|
202
|
+
continue
|
|
203
|
+
content = _read_raw(fs, pom_path)
|
|
204
|
+
if content and "spring-boot" in content:
|
|
205
|
+
seen_roots.add(root)
|
|
206
|
+
found.append({"framework": "spring", "root": root})
|
|
207
|
+
|
|
208
|
+
for gradle_path in _glob_no_skip(fs, "**/build.gradle"):
|
|
209
|
+
root = _dir_of(gradle_path)
|
|
210
|
+
if root in seen_roots:
|
|
211
|
+
continue
|
|
212
|
+
content = _read_raw(fs, gradle_path)
|
|
213
|
+
if content and "spring" in content.lower():
|
|
214
|
+
seen_roots.add(root)
|
|
215
|
+
found.append({"framework": "spring", "root": root})
|
|
216
|
+
|
|
217
|
+
return found
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _detect_laravel(fs: FileSystemTools) -> list[dict]:
|
|
221
|
+
"""Detect Laravel by artisan + composer.json with laravel."""
|
|
222
|
+
found: list[dict] = []
|
|
223
|
+
seen_roots: set[str] = set()
|
|
224
|
+
|
|
225
|
+
for artisan_path in _glob_no_skip(fs, "**/artisan"):
|
|
226
|
+
root = _dir_of(artisan_path)
|
|
227
|
+
if root in seen_roots:
|
|
228
|
+
continue
|
|
229
|
+
composer_path = f"{root}/composer.json" if root != "." else "composer.json"
|
|
230
|
+
composer = _parse_json(fs, composer_path)
|
|
231
|
+
if composer is None:
|
|
232
|
+
continue
|
|
233
|
+
require = {**composer.get("require", {}), **composer.get("require-dev", {})}
|
|
234
|
+
if any("laravel" in k for k in require):
|
|
235
|
+
seen_roots.add(root)
|
|
236
|
+
found.append({"framework": "laravel", "root": root})
|
|
237
|
+
|
|
238
|
+
return found
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def detect_frameworks(fs: FileSystemTools) -> list[dict]:
|
|
242
|
+
"""Detect all frameworks in the target repository.
|
|
243
|
+
|
|
244
|
+
Returns a list of dicts, each with:
|
|
245
|
+
- framework: str (e.g. "nextjs", "django", "express", "flask")
|
|
246
|
+
- root: str (directory path relative to repo root, or "." for root)
|
|
247
|
+
|
|
248
|
+
Supports monorepos: a single repo can yield multiple entries.
|
|
249
|
+
"""
|
|
250
|
+
results: list[dict] = []
|
|
251
|
+
|
|
252
|
+
results.extend(_detect_nextjs(fs))
|
|
253
|
+
results.extend(_detect_django(fs))
|
|
254
|
+
results.extend(_detect_express(fs))
|
|
255
|
+
results.extend(_detect_flask(fs))
|
|
256
|
+
results.extend(_detect_rails(fs))
|
|
257
|
+
results.extend(_detect_spring(fs))
|
|
258
|
+
results.extend(_detect_laravel(fs))
|
|
259
|
+
|
|
260
|
+
# Deduplicate: if both nextjs and express are detected at the same root
|
|
261
|
+
# (common for Next.js apps that also list express as a dep), keep nextjs
|
|
262
|
+
# since it's more specific.
|
|
263
|
+
nextjs_roots = {r["root"] for r in results if r["framework"] == "nextjs"}
|
|
264
|
+
results = [
|
|
265
|
+
r for r in results
|
|
266
|
+
if not (r["framework"] == "express" and r["root"] in nextjs_roots)
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
return results
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Headless scan — runs the full pipeline without the TUI.
|
|
3
|
+
|
|
4
|
+
Uses the same coordinator pipeline as the TUI (recon → hunters →
|
|
5
|
+
feature deep dive → validation) with checkpoint support for resume.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from .agents.coordinator import CoordinatorAgent
|
|
17
|
+
from .agents.checkpoint import CheckpointManager
|
|
18
|
+
from .agents.llm import LLMClient
|
|
19
|
+
from .agents.session import Session, Finding, TraceEntry
|
|
20
|
+
from .tools.registry import ToolRegistry
|
|
21
|
+
from .config import reload_settings, settings
|
|
22
|
+
from .prompts.project_context import build_project_context
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
SCANS_DIR = Path.home() / ".openhack" / "scans"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _on_trace(entry: TraceEntry):
|
|
30
|
+
agent = entry.agent or "?"
|
|
31
|
+
event = entry.event_type or ""
|
|
32
|
+
if event == "tool_call":
|
|
33
|
+
pass
|
|
34
|
+
elif "finding" in event.lower():
|
|
35
|
+
print(f" [{agent}] FINDING: {entry.content}")
|
|
36
|
+
elif event in ("status", "step_start", "step_complete", "resume"):
|
|
37
|
+
snippet = str(entry.content or "")[:120]
|
|
38
|
+
print(f" [{agent}] {event}: {snippet}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _write_report(
|
|
42
|
+
session: Session,
|
|
43
|
+
target_dir: str,
|
|
44
|
+
status: str,
|
|
45
|
+
start_time: float,
|
|
46
|
+
) -> Path:
|
|
47
|
+
SCANS_DIR.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
report_path = SCANS_DIR / f"{session.id}.json"
|
|
49
|
+
elapsed = time.time() - start_time
|
|
50
|
+
|
|
51
|
+
report = {
|
|
52
|
+
"version": 2,
|
|
53
|
+
"scan_id": session.id,
|
|
54
|
+
"target_dir": target_dir,
|
|
55
|
+
"provider": settings.llm_provider,
|
|
56
|
+
"status": status,
|
|
57
|
+
"pid": os.getpid(),
|
|
58
|
+
"started_at": datetime.fromtimestamp(start_time).isoformat(),
|
|
59
|
+
"duration_seconds": round(elapsed, 2),
|
|
60
|
+
"cost": session.get_cost_breakdown(),
|
|
61
|
+
"findings": [f.to_dict() for f in session.findings],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
tmp_path = report_path.with_suffix(".json.tmp")
|
|
65
|
+
with open(tmp_path, "w") as fp:
|
|
66
|
+
json.dump(report, fp, indent=2, default=str, ensure_ascii=False)
|
|
67
|
+
tmp_path.rename(report_path)
|
|
68
|
+
|
|
69
|
+
return report_path
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def run_headless_scan(
|
|
73
|
+
target_dir: str,
|
|
74
|
+
resume_from_checkpoint: Optional[str] = None,
|
|
75
|
+
):
|
|
76
|
+
"""Run a headless scan using the same coordinator pipeline as the TUI.
|
|
77
|
+
|
|
78
|
+
If resume_from_checkpoint is a checkpoint step name (e.g. "recon",
|
|
79
|
+
"hunter", "feature_hunt"), the coordinator skips completed steps.
|
|
80
|
+
If it's a session ID, we look up the latest checkpoint for that session.
|
|
81
|
+
"""
|
|
82
|
+
reload_settings()
|
|
83
|
+
provider = settings.llm_provider
|
|
84
|
+
start_time = time.time()
|
|
85
|
+
|
|
86
|
+
print(f"\n{'='*60}")
|
|
87
|
+
if resume_from_checkpoint:
|
|
88
|
+
print(f" RESUMING SCAN — {target_dir}")
|
|
89
|
+
else:
|
|
90
|
+
print(f" SCANNING — {target_dir}")
|
|
91
|
+
print(f" Provider: {provider}")
|
|
92
|
+
print(f"{'='*60}\n")
|
|
93
|
+
|
|
94
|
+
# Resolve resume: if given a session ID, find its latest checkpoint
|
|
95
|
+
resume_step = None
|
|
96
|
+
reuse_id = None
|
|
97
|
+
if resume_from_checkpoint:
|
|
98
|
+
mgr = CheckpointManager(resume_from_checkpoint)
|
|
99
|
+
latest = mgr.get_latest_step()
|
|
100
|
+
if latest:
|
|
101
|
+
reuse_id = resume_from_checkpoint
|
|
102
|
+
resume_step = latest
|
|
103
|
+
print(f" Resuming from checkpoint: {latest}")
|
|
104
|
+
elif resume_from_checkpoint in ("recon", "hunter", "feature_hunt"):
|
|
105
|
+
resume_step = resume_from_checkpoint
|
|
106
|
+
else:
|
|
107
|
+
reuse_id = resume_from_checkpoint
|
|
108
|
+
print(f" No checkpoint found — starting fresh on same session")
|
|
109
|
+
|
|
110
|
+
project_context = build_project_context(target_dir)
|
|
111
|
+
session = Session(
|
|
112
|
+
target_dir=target_dir,
|
|
113
|
+
on_trace=_on_trace,
|
|
114
|
+
project_context=project_context,
|
|
115
|
+
scan_id=reuse_id,
|
|
116
|
+
)
|
|
117
|
+
tools = ToolRegistry(target_dir=Path(target_dir))
|
|
118
|
+
|
|
119
|
+
if project_context and project_context.get("openhack_md"):
|
|
120
|
+
print(f" Loaded .openhack.md project context ({len(project_context['openhack_md'])} chars)")
|
|
121
|
+
|
|
122
|
+
# Write initial report so --list-sessions shows a running scan
|
|
123
|
+
_write_report(session, target_dir, "running", start_time)
|
|
124
|
+
|
|
125
|
+
llm = LLMClient(
|
|
126
|
+
provider=provider,
|
|
127
|
+
temperature=0.0,
|
|
128
|
+
max_tokens=8192,
|
|
129
|
+
prompt_cache_key=session.id,
|
|
130
|
+
)
|
|
131
|
+
coordinator = CoordinatorAgent(
|
|
132
|
+
llm, tools, session,
|
|
133
|
+
resume_from=resume_step,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
result = await coordinator.run_full_scan()
|
|
138
|
+
|
|
139
|
+
# Print results
|
|
140
|
+
findings = session.findings
|
|
141
|
+
print(f"\n{'='*60}")
|
|
142
|
+
print(f" RESULTS")
|
|
143
|
+
print(f"{'='*60}")
|
|
144
|
+
|
|
145
|
+
if not findings:
|
|
146
|
+
print(" No vulnerabilities confirmed.")
|
|
147
|
+
else:
|
|
148
|
+
print(f" {len(findings)} vulnerability(ies) confirmed:\n")
|
|
149
|
+
for i, f in enumerate(findings, 1):
|
|
150
|
+
sev = f.severity.upper()
|
|
151
|
+
print(f" {i}. [{sev}] {f.category} — {f.file_path}")
|
|
152
|
+
desc = f.description or ""
|
|
153
|
+
if desc:
|
|
154
|
+
print(f" {desc}")
|
|
155
|
+
print()
|
|
156
|
+
|
|
157
|
+
cost = session.get_cost_breakdown()
|
|
158
|
+
elapsed = time.time() - start_time
|
|
159
|
+
m, s = divmod(int(elapsed), 60)
|
|
160
|
+
print(f" Cost: ${session.total_cost:.4f}")
|
|
161
|
+
print(f" Duration: {m}m {s:02d}s")
|
|
162
|
+
|
|
163
|
+
report_path = _write_report(session, target_dir, "completed", start_time)
|
|
164
|
+
print(f" Report: {report_path}")
|
|
165
|
+
print(f" Session: {session.id}\n")
|
|
166
|
+
|
|
167
|
+
except KeyboardInterrupt:
|
|
168
|
+
print("\n Scan interrupted.")
|
|
169
|
+
_write_report(session, target_dir, "cancelled", start_time)
|
|
170
|
+
mgr = CheckpointManager(session.id)
|
|
171
|
+
if mgr.get_latest_step():
|
|
172
|
+
print(f" Resume from checkpoint: openhack --resume {session.id}")
|
|
173
|
+
else:
|
|
174
|
+
print(f" Retry: openhack --resume {session.id}")
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
logger.debug(f"Scan failed: {exc}", exc_info=True)
|
|
177
|
+
_write_report(session, target_dir, "failed", start_time)
|
|
178
|
+
print(f"\n Scan failed: {exc}")
|
|
179
|
+
print(f" Retry: openhack --resume {session.id}")
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt templates for vulnerability scanning agents.
|
|
3
|
+
|
|
4
|
+
All prompts are organized as one prompt per file for easy maintenance.
|
|
5
|
+
Framework-specific prompts live under prompts/<framework>/<attack_type>.py.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .project_context import format_project_context, load_openhack_md, build_project_context
|
|
9
|
+
from .coordinator import COORDINATOR_PROMPT
|
|
10
|
+
from .recon import RECON_PROMPT
|
|
11
|
+
from .hunter import HUNTER_PROMPT
|
|
12
|
+
from .validator import VALIDATOR_PROMPT
|
|
13
|
+
from .reporter import REPORTER_PROMPT
|
|
14
|
+
from .pr_analysis_system import PR_ANALYSIS_SYSTEM_PROMPT
|
|
15
|
+
from .pr_analysis_user import PR_ANALYSIS_USER_TEMPLATE
|
|
16
|
+
from .hunter_tool_instructions import HUNTER_TOOL_INSTRUCTIONS
|
|
17
|
+
from .hunter_continuation_no_findings import HUNTER_CONTINUATION_NO_FINDINGS
|
|
18
|
+
from .hunter_continuation_loop import HUNTER_CONTINUATION_LOOP
|
|
19
|
+
from .hunter_continuation_no_progress import HUNTER_CONTINUATION_NO_PROGRESS
|
|
20
|
+
from .validator_tool_instructions import VALIDATOR_TOOL_INSTRUCTIONS
|
|
21
|
+
from .validator_continuation_incomplete import VALIDATOR_CONTINUATION_INCOMPLETE
|
|
22
|
+
from .sandbox_verifier import SANDBOX_VERIFIER_PROMPT, SANDBOX_VERIFIER_TOOL_INSTRUCTIONS
|
|
23
|
+
from .browser_verifier import BROWSER_VERIFIER_PROMPT, BROWSER_VERIFIER_TOOL_INSTRUCTIONS
|
|
24
|
+
from .feature_hunter import FEATURE_HUNTER_PROMPT, FEATURE_EXTRACTION_PROMPT
|
|
25
|
+
from .nextjs import (
|
|
26
|
+
NEXTJS_PROMPTS,
|
|
27
|
+
NEXTJS_IDOR_PROMPT,
|
|
28
|
+
NEXTJS_XSS_PROMPT,
|
|
29
|
+
NEXTJS_CSRF_PROMPT,
|
|
30
|
+
NEXTJS_SSRF_PROMPT,
|
|
31
|
+
NEXTJS_INJECTION_PROMPT,
|
|
32
|
+
NEXTJS_AUTH_BYPASS_PROMPT,
|
|
33
|
+
NEXTJS_DATA_EXPOSURE_PROMPT,
|
|
34
|
+
NEXTJS_MIDDLEWARE_BYPASS_PROMPT,
|
|
35
|
+
NEXTJS_SERVER_ACTIONS_PROMPT,
|
|
36
|
+
NEXTJS_MISCONFIGURATION_PROMPT,
|
|
37
|
+
)
|
|
38
|
+
from .supabase import (
|
|
39
|
+
SUPABASE_PROMPTS,
|
|
40
|
+
SUPABASE_RLS_PROMPT,
|
|
41
|
+
SUPABASE_POSTGREST_PROMPT,
|
|
42
|
+
SUPABASE_RPC_PROMPT,
|
|
43
|
+
SUPABASE_STORAGE_PROMPT,
|
|
44
|
+
SUPABASE_REALTIME_PROMPT,
|
|
45
|
+
SUPABASE_GRAPHQL_PROMPT,
|
|
46
|
+
SUPABASE_AUTH_PROMPT,
|
|
47
|
+
SUPABASE_EDGE_FUNCTIONS_PROMPT,
|
|
48
|
+
SUPABASE_TENANT_ISOLATION_PROMPT,
|
|
49
|
+
)
|
|
50
|
+
from .django import DJANGO_PROMPTS
|
|
51
|
+
from .express import EXPRESS_PROMPTS
|
|
52
|
+
from .flask import FLASK_PROMPTS
|
|
53
|
+
|
|
54
|
+
# Unified registry: framework name -> prompt dict.
|
|
55
|
+
# Used by HunterAgent to look up the correct prompts for its assigned framework.
|
|
56
|
+
ALL_FRAMEWORK_PROMPTS: dict[str, dict[str, str]] = {
|
|
57
|
+
"nextjs": NEXTJS_PROMPTS,
|
|
58
|
+
"django": DJANGO_PROMPTS,
|
|
59
|
+
"express": EXPRESS_PROMPTS,
|
|
60
|
+
"flask": FLASK_PROMPTS,
|
|
61
|
+
"supabase": SUPABASE_PROMPTS,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"format_project_context",
|
|
66
|
+
"COORDINATOR_PROMPT",
|
|
67
|
+
"RECON_PROMPT",
|
|
68
|
+
"HUNTER_PROMPT",
|
|
69
|
+
"VALIDATOR_PROMPT",
|
|
70
|
+
"REPORTER_PROMPT",
|
|
71
|
+
"PR_ANALYSIS_SYSTEM_PROMPT",
|
|
72
|
+
"PR_ANALYSIS_USER_TEMPLATE",
|
|
73
|
+
"HUNTER_TOOL_INSTRUCTIONS",
|
|
74
|
+
"HUNTER_CONTINUATION_NO_FINDINGS",
|
|
75
|
+
"HUNTER_CONTINUATION_LOOP",
|
|
76
|
+
"HUNTER_CONTINUATION_NO_PROGRESS",
|
|
77
|
+
"VALIDATOR_TOOL_INSTRUCTIONS",
|
|
78
|
+
"VALIDATOR_CONTINUATION_INCOMPLETE",
|
|
79
|
+
"SANDBOX_VERIFIER_PROMPT",
|
|
80
|
+
"SANDBOX_VERIFIER_TOOL_INSTRUCTIONS",
|
|
81
|
+
"BROWSER_VERIFIER_PROMPT",
|
|
82
|
+
"BROWSER_VERIFIER_TOOL_INSTRUCTIONS",
|
|
83
|
+
"ALL_FRAMEWORK_PROMPTS",
|
|
84
|
+
"NEXTJS_PROMPTS",
|
|
85
|
+
"NEXTJS_IDOR_PROMPT",
|
|
86
|
+
"NEXTJS_XSS_PROMPT",
|
|
87
|
+
"NEXTJS_CSRF_PROMPT",
|
|
88
|
+
"NEXTJS_SSRF_PROMPT",
|
|
89
|
+
"NEXTJS_INJECTION_PROMPT",
|
|
90
|
+
"NEXTJS_AUTH_BYPASS_PROMPT",
|
|
91
|
+
"NEXTJS_DATA_EXPOSURE_PROMPT",
|
|
92
|
+
"NEXTJS_MIDDLEWARE_BYPASS_PROMPT",
|
|
93
|
+
"NEXTJS_SERVER_ACTIONS_PROMPT",
|
|
94
|
+
"NEXTJS_MISCONFIGURATION_PROMPT",
|
|
95
|
+
"SUPABASE_PROMPTS",
|
|
96
|
+
"SUPABASE_RLS_PROMPT",
|
|
97
|
+
"SUPABASE_POSTGREST_PROMPT",
|
|
98
|
+
"SUPABASE_RPC_PROMPT",
|
|
99
|
+
"SUPABASE_STORAGE_PROMPT",
|
|
100
|
+
"SUPABASE_REALTIME_PROMPT",
|
|
101
|
+
"SUPABASE_GRAPHQL_PROMPT",
|
|
102
|
+
"SUPABASE_AUTH_PROMPT",
|
|
103
|
+
"SUPABASE_EDGE_FUNCTIONS_PROMPT",
|
|
104
|
+
"SUPABASE_TENANT_ISOLATION_PROMPT",
|
|
105
|
+
"DJANGO_PROMPTS",
|
|
106
|
+
"EXPRESS_PROMPTS",
|
|
107
|
+
"FLASK_PROMPTS",
|
|
108
|
+
]
|