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.

Files changed (41) hide show
  1. strix/agents/StrixAgent/strix_agent.py +49 -40
  2. strix/agents/StrixAgent/system_prompt.jinja +15 -0
  3. strix/agents/base_agent.py +71 -11
  4. strix/agents/state.py +5 -1
  5. strix/interface/cli.py +171 -0
  6. strix/interface/main.py +482 -0
  7. strix/{cli → interface}/tool_components/scan_info_renderer.py +17 -12
  8. strix/{cli/app.py → interface/tui.py} +15 -16
  9. strix/interface/utils.py +435 -0
  10. strix/runtime/docker_runtime.py +28 -7
  11. strix/runtime/runtime.py +4 -1
  12. strix/telemetry/__init__.py +4 -0
  13. strix/{cli → telemetry}/tracer.py +21 -9
  14. strix/tools/agents_graph/agents_graph_actions.py +13 -9
  15. strix/tools/executor.py +1 -1
  16. strix/tools/finish/finish_actions.py +1 -1
  17. strix/tools/reporting/reporting_actions.py +1 -1
  18. {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/METADATA +45 -4
  19. {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/RECORD +39 -36
  20. strix_agent-0.3.1.dist-info/entry_points.txt +3 -0
  21. strix/cli/main.py +0 -703
  22. strix_agent-0.1.19.dist-info/entry_points.txt +0 -3
  23. /strix/{cli → interface}/__init__.py +0 -0
  24. /strix/{cli/assets/cli.tcss → interface/assets/tui_styles.tcss} +0 -0
  25. /strix/{cli → interface}/tool_components/__init__.py +0 -0
  26. /strix/{cli → interface}/tool_components/agents_graph_renderer.py +0 -0
  27. /strix/{cli → interface}/tool_components/base_renderer.py +0 -0
  28. /strix/{cli → interface}/tool_components/browser_renderer.py +0 -0
  29. /strix/{cli → interface}/tool_components/file_edit_renderer.py +0 -0
  30. /strix/{cli → interface}/tool_components/finish_renderer.py +0 -0
  31. /strix/{cli → interface}/tool_components/notes_renderer.py +0 -0
  32. /strix/{cli → interface}/tool_components/proxy_renderer.py +0 -0
  33. /strix/{cli → interface}/tool_components/python_renderer.py +0 -0
  34. /strix/{cli → interface}/tool_components/registry.py +0 -0
  35. /strix/{cli → interface}/tool_components/reporting_renderer.py +0 -0
  36. /strix/{cli → interface}/tool_components/terminal_renderer.py +0 -0
  37. /strix/{cli → interface}/tool_components/thinking_renderer.py +0 -0
  38. /strix/{cli → interface}/tool_components/user_message_renderer.py +0 -0
  39. /strix/{cli → interface}/tool_components/web_search_renderer.py +0 -0
  40. {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/LICENSE +0 -0
  41. {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 = 200
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
- task_parts = []
27
-
28
- if scan_type == "repository":
29
- repo_url = target["target_repo"]
30
- cloned_path = target.get("cloned_repo_path")
31
-
32
- if cloned_path:
33
- workspace_path = "/workspace"
34
- task_parts.append(
35
- f"Perform a security assessment of the Git repository: {repo_url}. "
36
- f"The repository has been cloned from '{repo_url}' to '{cloned_path}' "
37
- f"(host path) and then copied to '{workspace_path}' in your environment."
38
- f"Analyze the codebase at: {workspace_path}"
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
- else:
41
- task_parts.append(
42
- f"Perform a security assessment of the Git repository: {repo_url}"
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
- elif scan_type == "web_application":
46
- task_parts.append(
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
- elif scan_type == "local_code":
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
- else:
61
- task_parts.append(
62
- f"Perform a general security assessment of: {next(iter(target.values()))}"
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
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any, Optional
5
5
 
6
6
 
7
7
  if TYPE_CHECKING:
8
- from strix.cli.tracer import Tracer
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 = 200
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.local_source_path = config.get("local_source_path")
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.cli.tracer import get_global_tracer
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.cli.tracer import get_global_tracer
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.cli.tracer import get_global_tracer
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.local_source_path
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.cli.tracer import get_global_tracer
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.cli.tracer import get_global_tracer
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.cli.tracer import get_global_tracer
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 = 200
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()