strix-agent 0.1.19__py3-none-any.whl → 0.3.1__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.
Potentially problematic release.
This version of strix-agent might be problematic. Click here for more details.
- strix/agents/StrixAgent/strix_agent.py +49 -40
- strix/agents/StrixAgent/system_prompt.jinja +15 -0
- strix/agents/base_agent.py +71 -11
- strix/agents/state.py +5 -1
- strix/interface/cli.py +171 -0
- strix/interface/main.py +482 -0
- strix/{cli → interface}/tool_components/scan_info_renderer.py +17 -12
- strix/{cli/app.py → interface/tui.py} +15 -16
- strix/interface/utils.py +435 -0
- strix/runtime/docker_runtime.py +28 -7
- strix/runtime/runtime.py +4 -1
- strix/telemetry/__init__.py +4 -0
- strix/{cli → telemetry}/tracer.py +21 -9
- strix/tools/agents_graph/agents_graph_actions.py +13 -9
- strix/tools/executor.py +1 -1
- strix/tools/finish/finish_actions.py +1 -1
- strix/tools/reporting/reporting_actions.py +1 -1
- {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/METADATA +45 -4
- {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/RECORD +39 -36
- strix_agent-0.3.1.dist-info/entry_points.txt +3 -0
- strix/cli/main.py +0 -703
- strix_agent-0.1.19.dist-info/entry_points.txt +0 -3
- /strix/{cli → interface}/__init__.py +0 -0
- /strix/{cli/assets/cli.tcss → interface/assets/tui_styles.tcss} +0 -0
- /strix/{cli → interface}/tool_components/__init__.py +0 -0
- /strix/{cli → interface}/tool_components/agents_graph_renderer.py +0 -0
- /strix/{cli → interface}/tool_components/base_renderer.py +0 -0
- /strix/{cli → interface}/tool_components/browser_renderer.py +0 -0
- /strix/{cli → interface}/tool_components/file_edit_renderer.py +0 -0
- /strix/{cli → interface}/tool_components/finish_renderer.py +0 -0
- /strix/{cli → interface}/tool_components/notes_renderer.py +0 -0
- /strix/{cli → interface}/tool_components/proxy_renderer.py +0 -0
- /strix/{cli → interface}/tool_components/python_renderer.py +0 -0
- /strix/{cli → interface}/tool_components/registry.py +0 -0
- /strix/{cli → interface}/tool_components/reporting_renderer.py +0 -0
- /strix/{cli → interface}/tool_components/terminal_renderer.py +0 -0
- /strix/{cli → interface}/tool_components/thinking_renderer.py +0 -0
- /strix/{cli → interface}/tool_components/user_message_renderer.py +0 -0
- /strix/{cli → interface}/tool_components/web_search_renderer.py +0 -0
- {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/LICENSE +0 -0
- {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/WHEEL +0 -0
|
@@ -5,7 +5,7 @@ from strix.llm.config import LLMConfig
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class StrixAgent(BaseAgent):
|
|
8
|
-
max_iterations =
|
|
8
|
+
max_iterations = 300
|
|
9
9
|
|
|
10
10
|
def __init__(self, config: dict[str, Any]):
|
|
11
11
|
default_modules = []
|
|
@@ -19,55 +19,64 @@ class StrixAgent(BaseAgent):
|
|
|
19
19
|
super().__init__(config)
|
|
20
20
|
|
|
21
21
|
async def execute_scan(self, scan_config: dict[str, Any]) -> dict[str, Any]:
|
|
22
|
-
scan_type = scan_config.get("scan_type", "general")
|
|
23
|
-
target = scan_config.get("target", {})
|
|
24
22
|
user_instructions = scan_config.get("user_instructions", "")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
23
|
+
targets = scan_config.get("targets", [])
|
|
24
|
+
|
|
25
|
+
repositories = []
|
|
26
|
+
local_code = []
|
|
27
|
+
urls = []
|
|
28
|
+
|
|
29
|
+
for target in targets:
|
|
30
|
+
target_type = target["type"]
|
|
31
|
+
details = target["details"]
|
|
32
|
+
workspace_subdir = details.get("workspace_subdir")
|
|
33
|
+
workspace_path = f"/workspace/{workspace_subdir}" if workspace_subdir else "/workspace"
|
|
34
|
+
|
|
35
|
+
if target_type == "repository":
|
|
36
|
+
repo_url = details["target_repo"]
|
|
37
|
+
cloned_path = details.get("cloned_repo_path")
|
|
38
|
+
repositories.append(
|
|
39
|
+
{
|
|
40
|
+
"url": repo_url,
|
|
41
|
+
"workspace_path": workspace_path if cloned_path else None,
|
|
42
|
+
}
|
|
39
43
|
)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
|
|
45
|
+
elif target_type == "local_code":
|
|
46
|
+
original_path = details.get("target_path", "unknown")
|
|
47
|
+
local_code.append(
|
|
48
|
+
{
|
|
49
|
+
"path": original_path,
|
|
50
|
+
"workspace_path": workspace_path,
|
|
51
|
+
}
|
|
43
52
|
)
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
f"Perform a security assessment of the web application: {target['target_url']}"
|
|
48
|
-
)
|
|
54
|
+
elif target_type == "web_application":
|
|
55
|
+
urls.append(details["target_url"])
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
original_path = target.get("target_path", "unknown")
|
|
52
|
-
workspace_path = "/workspace"
|
|
53
|
-
task_parts.append(
|
|
54
|
-
f"Perform a security assessment of the local codebase. "
|
|
55
|
-
f"The code from '{original_path}' (user host path) has been copied to "
|
|
56
|
-
f"'{workspace_path}' in your environment. "
|
|
57
|
-
f"Analyze the codebase at: {workspace_path}"
|
|
58
|
-
)
|
|
57
|
+
task_parts = []
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
task_parts.append(
|
|
62
|
-
|
|
59
|
+
if repositories:
|
|
60
|
+
task_parts.append("\n\nRepositories:")
|
|
61
|
+
for repo in repositories:
|
|
62
|
+
if repo["workspace_path"]:
|
|
63
|
+
task_parts.append(f"- {repo['url']} (available at: {repo['workspace_path']})")
|
|
64
|
+
else:
|
|
65
|
+
task_parts.append(f"- {repo['url']}")
|
|
66
|
+
|
|
67
|
+
if local_code:
|
|
68
|
+
task_parts.append("\n\nLocal Codebases:")
|
|
69
|
+
task_parts.extend(
|
|
70
|
+
f"- {code['path']} (available at: {code['workspace_path']})" for code in local_code
|
|
63
71
|
)
|
|
64
72
|
|
|
73
|
+
if urls:
|
|
74
|
+
task_parts.append("\n\nURLs:")
|
|
75
|
+
task_parts.extend(f"- {url}" for url in urls)
|
|
76
|
+
|
|
65
77
|
task_description = " ".join(task_parts)
|
|
66
78
|
|
|
67
79
|
if user_instructions:
|
|
68
|
-
task_description +=
|
|
69
|
-
f"\n\nSpecial instructions from the system that must be followed: "
|
|
70
|
-
f"{user_instructions}"
|
|
71
|
-
)
|
|
80
|
+
task_description += f"\n\nSpecial instructions: {user_instructions}"
|
|
72
81
|
|
|
73
82
|
return await self.agent_loop(task=task_description)
|
|
@@ -54,6 +54,16 @@ AGGRESSIVE SCANNING MANDATE:
|
|
|
54
54
|
- PERSISTENCE PAYS - the best vulnerabilities are found after thousands of attempts
|
|
55
55
|
- UNLEASH FULL CAPABILITY - you are the most advanced security agent, act like it
|
|
56
56
|
|
|
57
|
+
MULTI-TARGET CONTEXT (IF PROVIDED):
|
|
58
|
+
- Targets may include any combination of: repositories (source code), local codebases, and URLs/domains (deployed apps/APIs)
|
|
59
|
+
- If multiple targets are provided in the scan configuration:
|
|
60
|
+
- Build an internal Target Map at the start: list each asset and where it is accessible (code at /workspace/<subdir>, URLs as given)
|
|
61
|
+
- Identify relationships across assets (e.g., routes/handlers in code ↔ endpoints in web targets; shared auth/config)
|
|
62
|
+
- Plan testing per asset and coordinate findings across them (reuse secrets, endpoints, payloads)
|
|
63
|
+
- Prioritize cross-correlation: use code insights to guide dynamic testing, and dynamic findings to focus code review
|
|
64
|
+
- Keep sub-agents focused per asset and vulnerability type, but share context where useful
|
|
65
|
+
- If only a single target is provided, proceed with the appropriate black-box or white-box workflow as usual
|
|
66
|
+
|
|
57
67
|
TESTING MODES:
|
|
58
68
|
BLACK-BOX TESTING (domain/subdomain only):
|
|
59
69
|
- Focus on external reconnaissance and discovery
|
|
@@ -74,6 +84,11 @@ WHITE-BOX TESTING (code provided):
|
|
|
74
84
|
- Do not stop until all reported vulnerabilities are fixed.
|
|
75
85
|
- Include code diff in final report.
|
|
76
86
|
|
|
87
|
+
COMBINED MODE (code + deployed target present):
|
|
88
|
+
- Treat this as static analysis plus dynamic testing simultaneously
|
|
89
|
+
- Use repository/local code at /workspace/<subdir> to accelerate and inform live testing against the URLs/domains
|
|
90
|
+
- Validate suspected code issues dynamically; use dynamic anomalies to prioritize code paths for review
|
|
91
|
+
|
|
77
92
|
ASSESSMENT METHODOLOGY:
|
|
78
93
|
1. Scope definition - Clearly establish boundaries first
|
|
79
94
|
2. Breadth-first discovery - Map entire attack surface before deep diving
|
strix/agents/base_agent.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any, Optional
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
|
-
from strix.
|
|
8
|
+
from strix.telemetry.tracer import Tracer
|
|
9
9
|
|
|
10
10
|
from jinja2 import (
|
|
11
11
|
Environment,
|
|
@@ -46,7 +46,7 @@ class AgentMeta(type):
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
class BaseAgent(metaclass=AgentMeta):
|
|
49
|
-
max_iterations =
|
|
49
|
+
max_iterations = 300
|
|
50
50
|
agent_name: str = ""
|
|
51
51
|
jinja_env: Environment
|
|
52
52
|
default_llm_config: LLMConfig | None = None
|
|
@@ -54,7 +54,8 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
54
54
|
def __init__(self, config: dict[str, Any]):
|
|
55
55
|
self.config = config
|
|
56
56
|
|
|
57
|
-
self.
|
|
57
|
+
self.local_sources = config.get("local_sources", [])
|
|
58
|
+
self.non_interactive = config.get("non_interactive", False)
|
|
58
59
|
|
|
59
60
|
if "max_iterations" in config:
|
|
60
61
|
self.max_iterations = config["max_iterations"]
|
|
@@ -76,7 +77,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
76
77
|
|
|
77
78
|
self._current_task: asyncio.Task[Any] | None = None
|
|
78
79
|
|
|
79
|
-
from strix.
|
|
80
|
+
from strix.telemetry.tracer import get_global_tracer
|
|
80
81
|
|
|
81
82
|
tracer = get_global_tracer()
|
|
82
83
|
if tracer:
|
|
@@ -146,10 +147,10 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
146
147
|
self._current_task.cancel()
|
|
147
148
|
self._current_task = None
|
|
148
149
|
|
|
149
|
-
async def agent_loop(self, task: str) -> dict[str, Any]:
|
|
150
|
+
async def agent_loop(self, task: str) -> dict[str, Any]: # noqa: PLR0912, PLR0915
|
|
150
151
|
await self._initialize_sandbox_and_state(task)
|
|
151
152
|
|
|
152
|
-
from strix.
|
|
153
|
+
from strix.telemetry.tracer import get_global_tracer
|
|
153
154
|
|
|
154
155
|
tracer = get_global_tracer()
|
|
155
156
|
|
|
@@ -161,6 +162,8 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
161
162
|
continue
|
|
162
163
|
|
|
163
164
|
if self.state.should_stop():
|
|
165
|
+
if self.non_interactive:
|
|
166
|
+
return self.state.final_result or {}
|
|
164
167
|
await self._enter_waiting_state(tracer)
|
|
165
168
|
continue
|
|
166
169
|
|
|
@@ -170,13 +173,47 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
170
173
|
|
|
171
174
|
self.state.increment_iteration()
|
|
172
175
|
|
|
176
|
+
if (
|
|
177
|
+
self.state.is_approaching_max_iterations()
|
|
178
|
+
and not self.state.max_iterations_warning_sent
|
|
179
|
+
):
|
|
180
|
+
self.state.max_iterations_warning_sent = True
|
|
181
|
+
remaining = self.state.max_iterations - self.state.iteration
|
|
182
|
+
warning_msg = (
|
|
183
|
+
f"URGENT: You are approaching the maximum iteration limit. "
|
|
184
|
+
f"Current: {self.state.iteration}/{self.state.max_iterations} "
|
|
185
|
+
f"({remaining} iterations remaining). "
|
|
186
|
+
f"Please prioritize completing your required task(s) and calling "
|
|
187
|
+
f"the appropriate finish tool (finish_scan for root agent, "
|
|
188
|
+
f"agent_finish for sub-agents) as soon as possible."
|
|
189
|
+
)
|
|
190
|
+
self.state.add_message("user", warning_msg)
|
|
191
|
+
|
|
192
|
+
if self.state.iteration == self.state.max_iterations - 3:
|
|
193
|
+
final_warning_msg = (
|
|
194
|
+
"CRITICAL: You have only 3 iterations left! "
|
|
195
|
+
"Your next message MUST be the tool call to the appropriate "
|
|
196
|
+
"finish tool: finish_scan if you are the root agent, or "
|
|
197
|
+
"agent_finish if you are a sub-agent. "
|
|
198
|
+
"No other actions should be taken except finishing your work "
|
|
199
|
+
"immediately."
|
|
200
|
+
)
|
|
201
|
+
self.state.add_message("user", final_warning_msg)
|
|
202
|
+
|
|
173
203
|
try:
|
|
174
204
|
should_finish = await self._process_iteration(tracer)
|
|
175
205
|
if should_finish:
|
|
206
|
+
if self.non_interactive:
|
|
207
|
+
self.state.set_completed({"success": True})
|
|
208
|
+
if tracer:
|
|
209
|
+
tracer.update_agent_status(self.state.agent_id, "completed")
|
|
210
|
+
return self.state.final_result or {}
|
|
176
211
|
await self._enter_waiting_state(tracer, task_completed=True)
|
|
177
212
|
continue
|
|
178
213
|
|
|
179
214
|
except asyncio.CancelledError:
|
|
215
|
+
if self.non_interactive:
|
|
216
|
+
raise
|
|
180
217
|
await self._enter_waiting_state(tracer, error_occurred=False, was_cancelled=True)
|
|
181
218
|
continue
|
|
182
219
|
|
|
@@ -184,6 +221,22 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
184
221
|
error_msg = str(e)
|
|
185
222
|
error_details = getattr(e, "details", None)
|
|
186
223
|
self.state.add_error(error_msg)
|
|
224
|
+
|
|
225
|
+
if self.non_interactive:
|
|
226
|
+
self.state.set_completed({"success": False, "error": error_msg})
|
|
227
|
+
if tracer:
|
|
228
|
+
tracer.update_agent_status(self.state.agent_id, "failed", error_msg)
|
|
229
|
+
if error_details:
|
|
230
|
+
tracer.log_tool_execution_start(
|
|
231
|
+
self.state.agent_id,
|
|
232
|
+
"llm_error_details",
|
|
233
|
+
{"error": error_msg, "details": error_details},
|
|
234
|
+
)
|
|
235
|
+
tracer.update_tool_execution(
|
|
236
|
+
tracer._next_execution_id - 1, "failed", error_details
|
|
237
|
+
)
|
|
238
|
+
return {"success": False, "error": error_msg}
|
|
239
|
+
|
|
187
240
|
self.state.enter_waiting_state(llm_failed=True)
|
|
188
241
|
if tracer:
|
|
189
242
|
tracer.update_agent_status(self.state.agent_id, "llm_failed", error_msg)
|
|
@@ -200,6 +253,11 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
200
253
|
|
|
201
254
|
except (RuntimeError, ValueError, TypeError) as e:
|
|
202
255
|
if not await self._handle_iteration_error(e, tracer):
|
|
256
|
+
if self.non_interactive:
|
|
257
|
+
self.state.set_completed({"success": False, "error": str(e)})
|
|
258
|
+
if tracer:
|
|
259
|
+
tracer.update_agent_status(self.state.agent_id, "failed")
|
|
260
|
+
raise
|
|
203
261
|
await self._enter_waiting_state(tracer, error_occurred=True)
|
|
204
262
|
continue
|
|
205
263
|
|
|
@@ -210,7 +268,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
210
268
|
self.state.resume_from_waiting()
|
|
211
269
|
self.state.add_message("assistant", "Waiting timeout reached. Resuming execution.")
|
|
212
270
|
|
|
213
|
-
from strix.
|
|
271
|
+
from strix.telemetry.tracer import get_global_tracer
|
|
214
272
|
|
|
215
273
|
tracer = get_global_tracer()
|
|
216
274
|
if tracer:
|
|
@@ -275,7 +333,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
275
333
|
|
|
276
334
|
runtime = get_runtime()
|
|
277
335
|
sandbox_info = await runtime.create_sandbox(
|
|
278
|
-
self.state.agent_id, self.state.sandbox_token, self.
|
|
336
|
+
self.state.agent_id, self.state.sandbox_token, self.local_sources
|
|
279
337
|
)
|
|
280
338
|
self.state.sandbox_id = sandbox_info["workspace_id"]
|
|
281
339
|
self.state.sandbox_token = sandbox_info["auth_token"]
|
|
@@ -353,6 +411,8 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
353
411
|
self.state.set_completed({"success": True})
|
|
354
412
|
if tracer:
|
|
355
413
|
tracer.update_agent_status(self.state.agent_id, "completed")
|
|
414
|
+
if self.non_interactive and self.state.parent_id is None:
|
|
415
|
+
return True
|
|
356
416
|
return True
|
|
357
417
|
|
|
358
418
|
return False
|
|
@@ -390,7 +450,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
390
450
|
state.resume_from_waiting()
|
|
391
451
|
has_new_messages = True
|
|
392
452
|
|
|
393
|
-
from strix.
|
|
453
|
+
from strix.telemetry.tracer import get_global_tracer
|
|
394
454
|
|
|
395
455
|
tracer = get_global_tracer()
|
|
396
456
|
if tracer:
|
|
@@ -399,7 +459,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
399
459
|
state.resume_from_waiting()
|
|
400
460
|
has_new_messages = True
|
|
401
461
|
|
|
402
|
-
from strix.
|
|
462
|
+
from strix.telemetry.tracer import get_global_tracer
|
|
403
463
|
|
|
404
464
|
tracer = get_global_tracer()
|
|
405
465
|
if tracer:
|
|
@@ -441,7 +501,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
441
501
|
message["read"] = True
|
|
442
502
|
|
|
443
503
|
if has_new_messages and not state.is_waiting_for_input():
|
|
444
|
-
from strix.
|
|
504
|
+
from strix.telemetry.tracer import get_global_tracer
|
|
445
505
|
|
|
446
506
|
tracer = get_global_tracer()
|
|
447
507
|
if tracer:
|
strix/agents/state.py
CHANGED
|
@@ -19,13 +19,14 @@ class AgentState(BaseModel):
|
|
|
19
19
|
|
|
20
20
|
task: str = ""
|
|
21
21
|
iteration: int = 0
|
|
22
|
-
max_iterations: int =
|
|
22
|
+
max_iterations: int = 300
|
|
23
23
|
completed: bool = False
|
|
24
24
|
stop_requested: bool = False
|
|
25
25
|
waiting_for_input: bool = False
|
|
26
26
|
llm_failed: bool = False
|
|
27
27
|
waiting_start_time: datetime | None = None
|
|
28
28
|
final_result: dict[str, Any] | None = None
|
|
29
|
+
max_iterations_warning_sent: bool = False
|
|
29
30
|
|
|
30
31
|
messages: list[dict[str, Any]] = Field(default_factory=list)
|
|
31
32
|
context: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -106,6 +107,9 @@ class AgentState(BaseModel):
|
|
|
106
107
|
def has_reached_max_iterations(self) -> bool:
|
|
107
108
|
return self.iteration >= self.max_iterations
|
|
108
109
|
|
|
110
|
+
def is_approaching_max_iterations(self, threshold: float = 0.85) -> bool:
|
|
111
|
+
return self.iteration >= int(self.max_iterations * threshold)
|
|
112
|
+
|
|
109
113
|
def has_waiting_timeout(self) -> bool:
|
|
110
114
|
if not self.waiting_for_input or not self.waiting_start_time:
|
|
111
115
|
return False
|
strix/interface/cli.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import signal
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
from strix.agents.StrixAgent import StrixAgent
|
|
11
|
+
from strix.llm.config import LLMConfig
|
|
12
|
+
from strix.telemetry.tracer import Tracer, set_global_tracer
|
|
13
|
+
|
|
14
|
+
from .utils import get_severity_color
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def run_cli(args: Any) -> None: # noqa: PLR0915
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
start_text = Text()
|
|
21
|
+
start_text.append("🦉 ", style="bold white")
|
|
22
|
+
start_text.append("STRIX CYBERSECURITY AGENT", style="bold green")
|
|
23
|
+
|
|
24
|
+
target_text = Text()
|
|
25
|
+
if len(args.targets_info) == 1:
|
|
26
|
+
target_text.append("🎯 Target: ", style="bold cyan")
|
|
27
|
+
target_text.append(args.targets_info[0]["original"], style="bold white")
|
|
28
|
+
else:
|
|
29
|
+
target_text.append("🎯 Targets: ", style="bold cyan")
|
|
30
|
+
target_text.append(f"{len(args.targets_info)} targets\n", style="bold white")
|
|
31
|
+
for i, target_info in enumerate(args.targets_info):
|
|
32
|
+
target_text.append(" • ", style="dim white")
|
|
33
|
+
target_text.append(target_info["original"], style="white")
|
|
34
|
+
if i < len(args.targets_info) - 1:
|
|
35
|
+
target_text.append("\n")
|
|
36
|
+
|
|
37
|
+
results_text = Text()
|
|
38
|
+
results_text.append("📊 Results will be saved to: ", style="bold cyan")
|
|
39
|
+
results_text.append(f"agent_runs/{args.run_name}", style="bold white")
|
|
40
|
+
|
|
41
|
+
note_text = Text()
|
|
42
|
+
note_text.append("\n\n", style="dim")
|
|
43
|
+
note_text.append("⏱️ ", style="dim")
|
|
44
|
+
note_text.append("This may take a while depending on target complexity. ", style="dim")
|
|
45
|
+
note_text.append("Vulnerabilities will be displayed in real-time.", style="dim")
|
|
46
|
+
|
|
47
|
+
startup_panel = Panel(
|
|
48
|
+
Text.assemble(
|
|
49
|
+
start_text,
|
|
50
|
+
"\n\n",
|
|
51
|
+
target_text,
|
|
52
|
+
"\n",
|
|
53
|
+
results_text,
|
|
54
|
+
note_text,
|
|
55
|
+
),
|
|
56
|
+
title="[bold green]🛡️ STRIX PENETRATION TEST INITIATED",
|
|
57
|
+
title_align="center",
|
|
58
|
+
border_style="green",
|
|
59
|
+
padding=(1, 2),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
console.print("\n")
|
|
63
|
+
console.print(startup_panel)
|
|
64
|
+
console.print()
|
|
65
|
+
|
|
66
|
+
scan_config = {
|
|
67
|
+
"scan_id": args.run_name,
|
|
68
|
+
"targets": args.targets_info,
|
|
69
|
+
"user_instructions": args.instruction or "",
|
|
70
|
+
"run_name": args.run_name,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
llm_config = LLMConfig()
|
|
74
|
+
agent_config = {
|
|
75
|
+
"llm_config": llm_config,
|
|
76
|
+
"max_iterations": 300,
|
|
77
|
+
"non_interactive": True,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if getattr(args, "local_sources", None):
|
|
81
|
+
agent_config["local_sources"] = args.local_sources
|
|
82
|
+
|
|
83
|
+
tracer = Tracer(args.run_name)
|
|
84
|
+
tracer.set_scan_config(scan_config)
|
|
85
|
+
|
|
86
|
+
def display_vulnerability(report_id: str, title: str, content: str, severity: str) -> None:
|
|
87
|
+
severity_color = get_severity_color(severity.lower())
|
|
88
|
+
|
|
89
|
+
vuln_text = Text()
|
|
90
|
+
vuln_text.append("🐞 ", style="bold red")
|
|
91
|
+
vuln_text.append("VULNERABILITY FOUND", style="bold red")
|
|
92
|
+
vuln_text.append(" • ", style="dim white")
|
|
93
|
+
vuln_text.append(title, style="bold white")
|
|
94
|
+
|
|
95
|
+
severity_text = Text()
|
|
96
|
+
severity_text.append("Severity: ", style="dim white")
|
|
97
|
+
severity_text.append(severity.upper(), style=f"bold {severity_color}")
|
|
98
|
+
|
|
99
|
+
vuln_panel = Panel(
|
|
100
|
+
Text.assemble(
|
|
101
|
+
vuln_text,
|
|
102
|
+
"\n\n",
|
|
103
|
+
severity_text,
|
|
104
|
+
"\n\n",
|
|
105
|
+
content,
|
|
106
|
+
),
|
|
107
|
+
title=f"[bold red]🔍 {report_id.upper()}",
|
|
108
|
+
title_align="left",
|
|
109
|
+
border_style="red",
|
|
110
|
+
padding=(1, 2),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
console.print(vuln_panel)
|
|
114
|
+
console.print()
|
|
115
|
+
|
|
116
|
+
tracer.vulnerability_found_callback = display_vulnerability
|
|
117
|
+
|
|
118
|
+
def cleanup_on_exit() -> None:
|
|
119
|
+
tracer.cleanup()
|
|
120
|
+
|
|
121
|
+
def signal_handler(_signum: int, _frame: Any) -> None:
|
|
122
|
+
tracer.cleanup()
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
atexit.register(cleanup_on_exit)
|
|
126
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
127
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
128
|
+
if hasattr(signal, "SIGHUP"):
|
|
129
|
+
signal.signal(signal.SIGHUP, signal_handler)
|
|
130
|
+
|
|
131
|
+
set_global_tracer(tracer)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
console.print()
|
|
135
|
+
with console.status("[bold cyan]Running penetration test...", spinner="dots") as status:
|
|
136
|
+
agent = StrixAgent(agent_config)
|
|
137
|
+
result = await agent.execute_scan(scan_config)
|
|
138
|
+
status.stop()
|
|
139
|
+
|
|
140
|
+
if isinstance(result, dict) and not result.get("success", True):
|
|
141
|
+
error_msg = result.get("error", "Unknown error")
|
|
142
|
+
console.print()
|
|
143
|
+
console.print(f"[bold red]❌ Penetration test failed:[/] {error_msg}")
|
|
144
|
+
console.print()
|
|
145
|
+
sys.exit(1)
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
console.print(f"[bold red]Error during penetration test:[/] {e}")
|
|
149
|
+
raise
|
|
150
|
+
|
|
151
|
+
if tracer.final_scan_result:
|
|
152
|
+
console.print()
|
|
153
|
+
|
|
154
|
+
final_report_text = Text()
|
|
155
|
+
final_report_text.append("📄 ", style="bold cyan")
|
|
156
|
+
final_report_text.append("FINAL PENETRATION TEST REPORT", style="bold cyan")
|
|
157
|
+
|
|
158
|
+
final_report_panel = Panel(
|
|
159
|
+
Text.assemble(
|
|
160
|
+
final_report_text,
|
|
161
|
+
"\n\n",
|
|
162
|
+
tracer.final_scan_result,
|
|
163
|
+
),
|
|
164
|
+
title="[bold cyan]📊 PENETRATION TEST SUMMARY",
|
|
165
|
+
title_align="center",
|
|
166
|
+
border_style="cyan",
|
|
167
|
+
padding=(1, 2),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
console.print(final_report_panel)
|
|
171
|
+
console.print()
|