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,385 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hunter swarm agent that spawns focused sub-hunters concurrently.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .hunter import HunterAgent
|
|
10
|
+
from .llm import LLMClient
|
|
11
|
+
from .session import Session
|
|
12
|
+
from openhack.tools.registry import ToolRegistry
|
|
13
|
+
from openhack.categories import normalize_category
|
|
14
|
+
from openhack.config import settings
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
_FRAMEWORK_GROUP_TEMPLATES: dict[str, dict[str, dict]] = {
|
|
19
|
+
"nextjs": {
|
|
20
|
+
"input_validation": {
|
|
21
|
+
"categories": ["xss", "injection", "ssrf", "open_redirect"],
|
|
22
|
+
"task": (
|
|
23
|
+
"Hunt for input validation vulnerabilities in the Next.js application"
|
|
24
|
+
"{root_hint}: XSS, injection flaws, SSRF, and open redirects. "
|
|
25
|
+
"Trace user input from entry points to dangerous sinks. "
|
|
26
|
+
"Focus on reading route handlers, API endpoints, and components "
|
|
27
|
+
"that render user-controlled data.\n\n"
|
|
28
|
+
"IMPORTANT -- Open Redirect Hunting Strategy:\n"
|
|
29
|
+
"Open redirects are a high-value, commonly exploitable vulnerability class. "
|
|
30
|
+
"You MUST specifically search for them:\n"
|
|
31
|
+
"1. Use grep to find ALL callback handlers: search for files matching '**/callback*', "
|
|
32
|
+
"'**/api/auth/**', '**/api/integrations/**'\n"
|
|
33
|
+
"2. Search for redirect sink patterns: grep for 'redirect(', 'NextResponse.redirect', "
|
|
34
|
+
"'res.redirect', 'window.location'\n"
|
|
35
|
+
"3. Search for common redirect parameter names: grep for 'returnTo', 'redirectTo', "
|
|
36
|
+
"'redirect_url', 'onErrorReturnTo', 'callbackUrl', 'successUrl', 'cancelUrl'\n"
|
|
37
|
+
"4. For each redirect found, trace whether the URL is validated.\n"
|
|
38
|
+
"5. Pay special attention to OAuth/payment callback handlers."
|
|
39
|
+
),
|
|
40
|
+
},
|
|
41
|
+
"access_control": {
|
|
42
|
+
"categories": ["idor", "auth_bypass", "middleware_bypass"],
|
|
43
|
+
"task": (
|
|
44
|
+
"Hunt for access control vulnerabilities in the Next.js application"
|
|
45
|
+
"{root_hint}: IDOR, authentication bypass, "
|
|
46
|
+
"and middleware bypass. Focus on authorization checks, object ownership "
|
|
47
|
+
"validation, and middleware ordering."
|
|
48
|
+
),
|
|
49
|
+
},
|
|
50
|
+
"data_handling": {
|
|
51
|
+
"categories": ["data_exposure", "csrf", "server_actions", "misconfiguration"],
|
|
52
|
+
"task": (
|
|
53
|
+
"Hunt for data handling and configuration vulnerabilities in the Next.js application"
|
|
54
|
+
"{root_hint}: data exposure, CSRF, server action flaws, and security "
|
|
55
|
+
"misconfigurations."
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
"django": {
|
|
60
|
+
"input_validation": {
|
|
61
|
+
"categories": ["injection", "ssrf", "idor"],
|
|
62
|
+
"task": "Hunt for input validation vulnerabilities in the Django application{root_hint}: SQL injection via ORM escape hatches, SSRF, and IDOR.",
|
|
63
|
+
},
|
|
64
|
+
"access_control": {
|
|
65
|
+
"categories": ["auth_bypass", "csrf"],
|
|
66
|
+
"task": "Hunt for access control vulnerabilities in the Django application{root_hint}: missing @login_required, broken permissions, @csrf_exempt.",
|
|
67
|
+
},
|
|
68
|
+
"data_handling": {
|
|
69
|
+
"categories": ["data_exposure", "misconfiguration"],
|
|
70
|
+
"task": "Hunt for data handling and configuration vulnerabilities in the Django application{root_hint}: serializer exposure, DEBUG=True, hardcoded secrets.",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
"express": {
|
|
74
|
+
"input_validation": {
|
|
75
|
+
"categories": ["injection", "ssrf", "idor"],
|
|
76
|
+
"task": "Hunt for input validation vulnerabilities in the Express.js application{root_hint}: SQL/NoSQL injection, command injection, SSRF, IDOR.",
|
|
77
|
+
},
|
|
78
|
+
"access_control": {
|
|
79
|
+
"categories": ["auth_bypass"],
|
|
80
|
+
"task": "Hunt for access control vulnerabilities in the Express.js application{root_hint}: missing auth middleware, JWT issues.",
|
|
81
|
+
},
|
|
82
|
+
"data_handling": {
|
|
83
|
+
"categories": ["data_exposure", "misconfiguration"],
|
|
84
|
+
"task": "Hunt for data handling vulnerabilities in the Express.js application{root_hint}: data leaks, CORS issues, missing helmet.",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
"flask": {
|
|
88
|
+
"input_validation": {
|
|
89
|
+
"categories": ["injection", "ssrf", "idor"],
|
|
90
|
+
"task": "Hunt for input validation vulnerabilities in the Flask application{root_hint}: SQL injection, SSTI, command injection, SSRF, IDOR.",
|
|
91
|
+
},
|
|
92
|
+
"access_control": {
|
|
93
|
+
"categories": ["auth_bypass"],
|
|
94
|
+
"task": "Hunt for access control vulnerabilities in the Flask application{root_hint}: missing @login_required, unprotected blueprints.",
|
|
95
|
+
},
|
|
96
|
+
"data_handling": {
|
|
97
|
+
"categories": ["data_exposure", "misconfiguration"],
|
|
98
|
+
"task": "Hunt for data handling vulnerabilities in the Flask application{root_hint}: data leaks, DEBUG=True, hardcoded secrets.",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_hunter_groups(detected_frameworks: list[dict], has_supabase: bool = False) -> dict[str, dict]:
|
|
105
|
+
groups: dict[str, dict] = {}
|
|
106
|
+
|
|
107
|
+
for i, fw_info in enumerate(detected_frameworks):
|
|
108
|
+
fw_name = fw_info["framework"]
|
|
109
|
+
fw_root = fw_info["root"]
|
|
110
|
+
|
|
111
|
+
templates = _FRAMEWORK_GROUP_TEMPLATES.get(fw_name)
|
|
112
|
+
if templates is None:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
root_hint = (
|
|
116
|
+
f" located under `{fw_root}/`. Focus your file reads and grep "
|
|
117
|
+
f"searches within this directory"
|
|
118
|
+
if fw_root != "."
|
|
119
|
+
else ""
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
suffix = f"_{i}" if sum(1 for f in detected_frameworks if f["framework"] == fw_name) > 1 else ""
|
|
123
|
+
|
|
124
|
+
for group_key, template in templates.items():
|
|
125
|
+
group_name = f"{fw_name}_{group_key}{suffix}"
|
|
126
|
+
groups[group_name] = {
|
|
127
|
+
"categories": template["categories"],
|
|
128
|
+
"framework": fw_name,
|
|
129
|
+
"requires": "source_code",
|
|
130
|
+
"task": template["task"].format(root_hint=root_hint),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
source_frameworks = [f for f in detected_frameworks]
|
|
134
|
+
if len(source_frameworks) >= 2:
|
|
135
|
+
fw_summary = ", ".join(f"{f['framework']} at `{f['root']}/`" for f in source_frameworks)
|
|
136
|
+
groups["cross_framework"] = {
|
|
137
|
+
"categories": ["injection", "ssrf", "auth_bypass", "idor", "data_exposure"],
|
|
138
|
+
"framework": None,
|
|
139
|
+
"requires": "source_code",
|
|
140
|
+
"task": (
|
|
141
|
+
f"This is a monorepo with multiple frameworks: {fw_summary}. "
|
|
142
|
+
"Hunt specifically for CROSS-SERVICE vulnerability chains."
|
|
143
|
+
),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return groups
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
_CATEGORY_TO_DANGER_KEYWORDS: dict[str, list[str]] = {
|
|
150
|
+
"input_validation": ["SQLi", "XSS", "SSRF", "SSTI", "RCE", "Open Redirect", "Path Traversal",
|
|
151
|
+
"Command Injection", "Prototype Pollution"],
|
|
152
|
+
"access_control": ["IDOR", "Hardcoded Secret"],
|
|
153
|
+
"data_handling": ["Hardcoded Secret", "Race Condition"],
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _build_file_hints(attack_surface: dict, group_categories: list[str]) -> str:
|
|
158
|
+
"""Build a file hint section from the attack surface for a hunter group.
|
|
159
|
+
|
|
160
|
+
Maps hunter group categories to relevant attack surface files (danger pattern
|
|
161
|
+
files and import dependencies) so the hunter knows exactly which files to read
|
|
162
|
+
instead of guessing.
|
|
163
|
+
"""
|
|
164
|
+
# Determine which danger keywords are relevant for this group
|
|
165
|
+
relevant_keywords: set[str] = set()
|
|
166
|
+
for cat in group_categories:
|
|
167
|
+
for group_key, keywords in _CATEGORY_TO_DANGER_KEYWORDS.items():
|
|
168
|
+
if cat in _FRAMEWORK_GROUP_TEMPLATES.get("express", {}).get(group_key, {}).get("categories", []):
|
|
169
|
+
relevant_keywords.update(keywords)
|
|
170
|
+
if cat in _FRAMEWORK_GROUP_TEMPLATES.get("nextjs", {}).get(group_key, {}).get("categories", []):
|
|
171
|
+
relevant_keywords.update(keywords)
|
|
172
|
+
if cat in _FRAMEWORK_GROUP_TEMPLATES.get("django", {}).get(group_key, {}).get("categories", []):
|
|
173
|
+
relevant_keywords.update(keywords)
|
|
174
|
+
if cat in _FRAMEWORK_GROUP_TEMPLATES.get("flask", {}).get(group_key, {}).get("categories", []):
|
|
175
|
+
relevant_keywords.update(keywords)
|
|
176
|
+
# Fallback: if no keywords matched, include everything
|
|
177
|
+
if not relevant_keywords:
|
|
178
|
+
relevant_keywords = {"SQLi", "XSS", "SSRF", "SSTI", "RCE", "Path Traversal",
|
|
179
|
+
"Command Injection", "IDOR", "Open Redirect", "Prototype Pollution",
|
|
180
|
+
"Hardcoded Secret", "Race Condition"}
|
|
181
|
+
|
|
182
|
+
hint_files: list[str] = []
|
|
183
|
+
max_hints = 20
|
|
184
|
+
|
|
185
|
+
# Collect from danger_files and imported_dependencies
|
|
186
|
+
for source_key in ("danger_files", "imported_dependencies"):
|
|
187
|
+
for ep in attack_surface.get(source_key, []):
|
|
188
|
+
label = ep.get("label", "")
|
|
189
|
+
trigger = ep.get("trigger", "")
|
|
190
|
+
# Check if any relevant keyword appears in the label or trigger
|
|
191
|
+
if any(kw.lower() in label.lower() or kw.lower() in trigger.lower()
|
|
192
|
+
for kw in relevant_keywords):
|
|
193
|
+
signals = ep.get("danger_signals", [])
|
|
194
|
+
if signals:
|
|
195
|
+
signal_desc = "; ".join(
|
|
196
|
+
f"L{s['line']}: {s['description']}" for s in signals[:3]
|
|
197
|
+
)
|
|
198
|
+
hint_files.append(f" - `{ep['file']}` — {signal_desc}")
|
|
199
|
+
else:
|
|
200
|
+
hint_files.append(f" - `{ep['file']}` — {ep.get('trigger', label)}")
|
|
201
|
+
|
|
202
|
+
if len(hint_files) >= max_hints:
|
|
203
|
+
break
|
|
204
|
+
if len(hint_files) >= max_hints:
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
# Also include route handlers / framework entry points
|
|
208
|
+
route_files: list[str] = []
|
|
209
|
+
for source_key in ("route_handlers", "flask_routes", "django_views", "api_routes"):
|
|
210
|
+
for ep in attack_surface.get(source_key, []):
|
|
211
|
+
route_files.append(f" - `{ep['file']}`")
|
|
212
|
+
if len(route_files) >= 10:
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
if not hint_files and not route_files:
|
|
216
|
+
return ""
|
|
217
|
+
|
|
218
|
+
parts: list[str] = []
|
|
219
|
+
if route_files:
|
|
220
|
+
parts.append(
|
|
221
|
+
"**Entry point files** (route handlers — start your analysis here):\n"
|
|
222
|
+
+ "\n".join(route_files[:10])
|
|
223
|
+
)
|
|
224
|
+
if hint_files:
|
|
225
|
+
parts.append(
|
|
226
|
+
"**High-signal files** (contain dangerous sinks or are imported by entry points — "
|
|
227
|
+
"you MUST read these):\n"
|
|
228
|
+
+ "\n".join(hint_files)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
"\n\n## Discovered Attack Surface Files\n"
|
|
233
|
+
"The following files were identified by static analysis as security-relevant. "
|
|
234
|
+
"READ EACH of these files during your hunt — do not skip them.\n\n"
|
|
235
|
+
+ "\n\n".join(parts)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class HunterSwarmAgent:
|
|
240
|
+
name = "hunter_swarm"
|
|
241
|
+
description = "Hunter swarm coordinator"
|
|
242
|
+
|
|
243
|
+
def __init__(self, llm: LLMClient, tools: ToolRegistry, session: Session):
|
|
244
|
+
self.llm = llm
|
|
245
|
+
self.tools = tools
|
|
246
|
+
self.session = session
|
|
247
|
+
self.total_cost: float = 0.0
|
|
248
|
+
self.total_tokens: int = 0
|
|
249
|
+
self.total_input_tokens: int = 0
|
|
250
|
+
self.total_output_tokens: int = 0
|
|
251
|
+
|
|
252
|
+
def _get_model_for_hunter(self) -> str:
|
|
253
|
+
return settings.hunter_model_id or self.llm.model
|
|
254
|
+
|
|
255
|
+
def _create_llm_for_sub_hunter(self) -> LLMClient:
|
|
256
|
+
model = self._get_model_for_hunter()
|
|
257
|
+
return LLMClient(model=model, temperature=0.0, max_tokens=8192, provider=self.llm.provider, prompt_cache_key=self.llm.prompt_cache_key)
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def _deduplicate_findings(findings: list[dict]) -> list[dict]:
|
|
261
|
+
if not findings:
|
|
262
|
+
return findings
|
|
263
|
+
|
|
264
|
+
SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
265
|
+
CONFIDENCE_ORDER = {"high": 0, "medium": 1, "low": 2}
|
|
266
|
+
|
|
267
|
+
seen: dict[str, dict] = {}
|
|
268
|
+
for finding in findings:
|
|
269
|
+
file_path = (finding.get("file_path") or "").strip().lower()
|
|
270
|
+
raw_category = finding.get("vulnerability_type") or finding.get("category") or ""
|
|
271
|
+
vuln_type = normalize_category(raw_category).lower()
|
|
272
|
+
dedup_key = f"{file_path}::{vuln_type}"
|
|
273
|
+
|
|
274
|
+
if dedup_key not in seen:
|
|
275
|
+
seen[dedup_key] = finding
|
|
276
|
+
else:
|
|
277
|
+
existing = seen[dedup_key]
|
|
278
|
+
existing_sev = SEVERITY_ORDER.get((existing.get("severity") or "info").lower(), 4)
|
|
279
|
+
new_sev = SEVERITY_ORDER.get((finding.get("severity") or "info").lower(), 4)
|
|
280
|
+
existing_conf = CONFIDENCE_ORDER.get((existing.get("confidence") or "low").lower(), 2)
|
|
281
|
+
new_conf = CONFIDENCE_ORDER.get((finding.get("confidence") or "low").lower(), 2)
|
|
282
|
+
existing_detail = len(existing.get("description") or "")
|
|
283
|
+
new_detail = len(finding.get("description") or "")
|
|
284
|
+
|
|
285
|
+
if (new_sev, new_conf, -new_detail) < (existing_sev, existing_conf, -existing_detail):
|
|
286
|
+
seen[dedup_key] = finding
|
|
287
|
+
|
|
288
|
+
return list(seen.values())
|
|
289
|
+
|
|
290
|
+
def _determine_groups(self, context: dict) -> dict[str, dict]:
|
|
291
|
+
detected_frameworks = context.get("detected_frameworks", [])
|
|
292
|
+
if not detected_frameworks:
|
|
293
|
+
detected_frameworks = [{"framework": "nextjs", "root": "."}]
|
|
294
|
+
|
|
295
|
+
all_groups = build_hunter_groups(detected_frameworks)
|
|
296
|
+
|
|
297
|
+
return {name: config for name, config in all_groups.items()
|
|
298
|
+
if config["requires"] == "source_code"}
|
|
299
|
+
|
|
300
|
+
async def run(self, task: str, context: Optional[dict] = None) -> dict:
|
|
301
|
+
context = context or {}
|
|
302
|
+
|
|
303
|
+
active_groups = self._determine_groups(context)
|
|
304
|
+
|
|
305
|
+
if not active_groups:
|
|
306
|
+
return {"raw_output": "No hunter groups applicable", "findings": [], "type": "hunt_complete"}
|
|
307
|
+
|
|
308
|
+
self.session.add_trace(
|
|
309
|
+
agent=self.name, event_type="swarm_start",
|
|
310
|
+
content={"groups": list(active_groups.keys()), "group_count": len(active_groups)},
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Build file hints from attack surface for each hunter group
|
|
314
|
+
attack_surface = context.get("attack_surface", {})
|
|
315
|
+
|
|
316
|
+
sub_hunters: list[tuple[str, HunterAgent, str]] = []
|
|
317
|
+
for group_name, group_config in active_groups.items():
|
|
318
|
+
llm = self._create_llm_for_sub_hunter()
|
|
319
|
+
hunter = HunterAgent(
|
|
320
|
+
llm, self.tools, self.session,
|
|
321
|
+
vuln_categories=group_config["categories"],
|
|
322
|
+
group_name=group_name,
|
|
323
|
+
framework=group_config.get("framework"),
|
|
324
|
+
)
|
|
325
|
+
task_text = group_config["task"]
|
|
326
|
+
# Inject relevant file hints so the hunter knows WHERE to look
|
|
327
|
+
if attack_surface:
|
|
328
|
+
file_hints = _build_file_hints(attack_surface, group_config["categories"])
|
|
329
|
+
if file_hints:
|
|
330
|
+
task_text += file_hints
|
|
331
|
+
sub_hunters.append((group_name, hunter, task_text))
|
|
332
|
+
|
|
333
|
+
semaphore = asyncio.Semaphore(settings.max_concurrent_hunters)
|
|
334
|
+
|
|
335
|
+
async def run_sub_hunter(group_name, hunter, hunter_task):
|
|
336
|
+
async with semaphore:
|
|
337
|
+
try:
|
|
338
|
+
result = await hunter.run(hunter_task, context)
|
|
339
|
+
return group_name, result
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logger.error(f"Sub-hunter {group_name} failed: {e}")
|
|
342
|
+
return group_name, {"raw_output": f"Failed: {e}", "findings": [], "type": "hunt_failed"}
|
|
343
|
+
|
|
344
|
+
tasks = [
|
|
345
|
+
asyncio.create_task(run_sub_hunter(name, hunter, task))
|
|
346
|
+
for name, hunter, task in sub_hunters
|
|
347
|
+
]
|
|
348
|
+
try:
|
|
349
|
+
results = await asyncio.gather(*tasks)
|
|
350
|
+
except asyncio.CancelledError:
|
|
351
|
+
for t in tasks:
|
|
352
|
+
t.cancel()
|
|
353
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
354
|
+
raise
|
|
355
|
+
|
|
356
|
+
all_findings: list[dict] = []
|
|
357
|
+
all_files_analyzed: set[str] = set()
|
|
358
|
+
summaries: list[str] = []
|
|
359
|
+
|
|
360
|
+
for group_name, result in results:
|
|
361
|
+
all_findings.extend(result.get("findings", []))
|
|
362
|
+
all_files_analyzed.update(result.get("files_analyzed", []))
|
|
363
|
+
if result.get("raw_output"):
|
|
364
|
+
summaries.append(f"[{group_name}] {result['raw_output']}")
|
|
365
|
+
|
|
366
|
+
all_findings = self._deduplicate_findings(all_findings)
|
|
367
|
+
|
|
368
|
+
for _, hunter, _ in sub_hunters:
|
|
369
|
+
self.total_cost += hunter.llm.total_cost
|
|
370
|
+
self.total_tokens += hunter.llm.total_tokens
|
|
371
|
+
self.total_input_tokens += hunter.llm.total_input_tokens
|
|
372
|
+
self.total_output_tokens += hunter.llm.total_output_tokens
|
|
373
|
+
|
|
374
|
+
self.session.add_trace(
|
|
375
|
+
agent=self.name, event_type="swarm_complete",
|
|
376
|
+
content={"total_findings": len(all_findings), "groups_completed": len(results),
|
|
377
|
+
"total_cost": self.total_cost, "total_tokens": self.total_tokens},
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
"raw_output": "\n\n".join(summaries),
|
|
382
|
+
"findings": all_findings,
|
|
383
|
+
"type": "hunt_complete",
|
|
384
|
+
"files_analyzed": sorted(all_files_analyzed),
|
|
385
|
+
}
|