strix-agent 0.1.9__py3-none-any.whl → 0.1.10__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.
@@ -26,9 +26,21 @@ class StrixAgent(BaseAgent):
26
26
  task_parts = []
27
27
 
28
28
  if scan_type == "repository":
29
- task_parts.append(
30
- f"Perform a security assessment of the Git repository: {target['target_repo']}"
31
- )
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}"
39
+ )
40
+ else:
41
+ task_parts.append(
42
+ f"Perform a security assessment of the Git repository: {repo_url}"
43
+ )
32
44
 
33
45
  elif scan_type == "web_application":
34
46
  task_parts.append(
@@ -37,12 +49,12 @@ class StrixAgent(BaseAgent):
37
49
 
38
50
  elif scan_type == "local_code":
39
51
  original_path = target.get("target_path", "unknown")
40
- shared_workspace_path = "/shared_workspace"
52
+ workspace_path = "/workspace"
41
53
  task_parts.append(
42
54
  f"Perform a security assessment of the local codebase. "
43
55
  f"The code from '{original_path}' (user host path) has been copied to "
44
- f"'{shared_workspace_path}' in your environment. "
45
- f"Analyze the codebase at: {shared_workspace_path}"
56
+ f"'{workspace_path}' in your environment. "
57
+ f"Analyze the codebase at: {workspace_path}"
46
58
  )
47
59
 
48
60
  else:
@@ -145,11 +145,10 @@ Remember: A single high-impact vulnerability is worth more than dozens of low-se
145
145
 
146
146
  <multi_agent_system>
147
147
  AGENT ISOLATION & SANDBOXING:
148
- - Each subagent runs in a completely isolated sandbox environment
149
- - Each agent has its own: browser sessions, terminal sessions, proxy (history and scope rules), /workspace directory, environment variables, running processes
150
- - Agents cannot share network ports or interfere with each other's processes
151
- - Only shared resource is /shared_workspace for collaboration and file exchange
152
- - Use /shared_workspace to pass files, reports, and coordination data between agents
148
+ - All agents run in the same shared Docker container for efficiency
149
+ - Each agent has its own: browser sessions, terminal sessions
150
+ - All agents share the same /workspace directory and proxy history
151
+ - Agents can see each other's files and proxy traffic for better collaboration
153
152
 
154
153
  SIMPLE WORKFLOW RULES:
155
154
 
@@ -206,6 +205,27 @@ CRITICAL RULES:
206
205
  - **ONE AGENT = ONE TASK** - Don't let agents do multiple unrelated jobs
207
206
  - **SPAWN REACTIVELY** - Create new agents based on what you discover
208
207
  - **ONLY REPORTING AGENTS** can use create_vulnerability_report tool
208
+ - **AGENT SPECIALIZATION MANDATORY** - Each agent must be highly specialized with maximum 3 prompt modules
209
+ - **NO GENERIC AGENTS** - Avoid creating broad, multi-purpose agents that dilute focus
210
+
211
+ AGENT SPECIALIZATION EXAMPLES:
212
+
213
+ GOOD SPECIALIZATION:
214
+ - "SQLi Validation Agent" with prompt_modules: sql_injection
215
+ - "XSS Discovery Agent" with prompt_modules: xss
216
+ - "Auth Testing Agent" with prompt_modules: authentication_jwt, business_logic
217
+ - "SSRF + XXE Agent" with prompt_modules: ssrf, xxe, rce (related attack vectors)
218
+
219
+ BAD SPECIALIZATION:
220
+ - "General Web Testing Agent" with prompt_modules: sql_injection, xss, csrf, ssrf, authentication_jwt (too broad)
221
+ - "Everything Agent" with prompt_modules: all available modules (completely unfocused)
222
+ - Any agent with more than 3 prompt modules (violates constraints)
223
+
224
+ FOCUS PRINCIPLES:
225
+ - Each agent should have deep expertise in 1-3 related vulnerability types
226
+ - Agents with single modules have the deepest specialization
227
+ - Related vulnerabilities (like SSRF+XXE or Auth+Business Logic) can be combined
228
+ - Never create "kitchen sink" agents that try to do everything
209
229
 
210
230
  REALISTIC TESTING OUTCOMES:
211
231
  - **No Findings**: Agent completes testing but finds no vulnerabilities
@@ -291,8 +311,7 @@ PROGRAMMING:
291
311
  - You can install any additional tools/packages needed based on the task/context using package managers (apt, pip, npm, go install, etc.)
292
312
 
293
313
  Directories:
294
- - /workspace - Your private agent directory
295
- - /shared_workspace - Shared between agents
314
+ - /workspace - where you should work.
296
315
  - /home/pentester/tools - Additional tool scripts
297
316
  - /home/pentester/tools/wordlists - Currently empty, but you should download wordlists here when you need.
298
317
 
@@ -239,6 +239,9 @@ class BaseAgent(metaclass=AgentMeta):
239
239
  self.state.sandbox_token = sandbox_info["auth_token"]
240
240
  self.state.sandbox_info = sandbox_info
241
241
 
242
+ if "agent_id" in sandbox_info:
243
+ self.state.sandbox_info["agent_id"] = sandbox_info["agent_id"]
244
+
242
245
  if not self.state.task:
243
246
  self.state.task = task
244
247
 
strix/cli/app.py CHANGED
@@ -248,6 +248,8 @@ class StrixCLIApp(App): # type: ignore[misc]
248
248
 
249
249
  if args.target_type == "local_code" and "target_path" in args.target_dict:
250
250
  config["local_source_path"] = args.target_dict["target_path"]
251
+ elif args.target_type == "repository" and "cloned_repo_path" in args.target_dict:
252
+ config["local_source_path"] = args.target_dict["cloned_repo_path"]
251
253
 
252
254
  return config
253
255
 
@@ -876,7 +878,7 @@ class StrixCLIApp(App): # type: ignore[misc]
876
878
  result = tool_data.get("result")
877
879
 
878
880
  tool_colors = {
879
- "terminal_action": "#22c55e",
881
+ "terminal_execute": "#22c55e",
880
882
  "browser_action": "#06b6d4",
881
883
  "python_action": "#3b82f6",
882
884
  "agents_graph_action": "#fbbf24",
strix/cli/main.py CHANGED
@@ -9,7 +9,9 @@ import logging
9
9
  import os
10
10
  import secrets
11
11
  import shutil
12
+ import subprocess
12
13
  import sys
14
+ import tempfile
13
15
  from pathlib import Path
14
16
  from typing import Any
15
17
  from urllib.parse import urlparse
@@ -204,6 +206,84 @@ def generate_run_name() -> str:
204
206
  return f"{adj}-{noun}-{number}"
205
207
 
206
208
 
209
+ def clone_repository(repo_url: str, run_name: str) -> str:
210
+ console = Console()
211
+
212
+ git_executable = shutil.which("git")
213
+ if git_executable is None:
214
+ raise FileNotFoundError("Git executable not found in PATH")
215
+
216
+ temp_dir = Path(tempfile.gettempdir()) / "strix_repos" / run_name
217
+ temp_dir.mkdir(parents=True, exist_ok=True)
218
+
219
+ repo_name = Path(repo_url).stem if repo_url.endswith(".git") else Path(repo_url).name
220
+
221
+ clone_path = temp_dir / repo_name
222
+
223
+ if clone_path.exists():
224
+ shutil.rmtree(clone_path)
225
+
226
+ try:
227
+ with console.status(f"[bold cyan]Cloning repository {repo_name}...", spinner="dots"):
228
+ subprocess.run( # noqa: S603
229
+ [
230
+ git_executable,
231
+ "clone",
232
+ "--depth=1",
233
+ "--no-recurse-submodules",
234
+ "--single-branch",
235
+ repo_url,
236
+ str(clone_path),
237
+ ],
238
+ capture_output=True,
239
+ text=True,
240
+ check=True,
241
+ )
242
+
243
+ return str(clone_path.absolute())
244
+
245
+ except subprocess.CalledProcessError as e:
246
+ error_text = Text()
247
+ error_text.append("❌ ", style="bold red")
248
+ error_text.append("REPOSITORY CLONE FAILED", style="bold red")
249
+ error_text.append("\n\n", style="white")
250
+ error_text.append(f"Could not clone repository: {repo_url}\n", style="white")
251
+ error_text.append(
252
+ f"Error: {e.stderr if hasattr(e, 'stderr') and e.stderr else str(e)}", style="dim red"
253
+ )
254
+
255
+ panel = Panel(
256
+ error_text,
257
+ title="[bold red]🛡️ STRIX CLONE ERROR",
258
+ title_align="center",
259
+ border_style="red",
260
+ padding=(1, 2),
261
+ )
262
+ console.print("\n")
263
+ console.print(panel)
264
+ console.print()
265
+ sys.exit(1)
266
+ except FileNotFoundError:
267
+ error_text = Text()
268
+ error_text.append("❌ ", style="bold red")
269
+ error_text.append("GIT NOT FOUND", style="bold red")
270
+ error_text.append("\n\n", style="white")
271
+ error_text.append("Git is not installed or not available in PATH.\n", style="white")
272
+ error_text.append("Please install Git to clone repositories.\n", style="white")
273
+
274
+ panel = Panel(
275
+ error_text,
276
+ title="[bold red]🛡️ STRIX CLONE ERROR",
277
+ title_align="center",
278
+ border_style="red",
279
+ padding=(1, 2),
280
+ )
281
+ console.print("\n")
282
+ console.print(panel)
283
+ console.print()
284
+ sys.exit(1)
285
+
286
+
207
287
  def infer_target_type(target: str) -> tuple[str, dict[str, str]]:
208
288
  if not target or not isinstance(target, str):
209
289
  raise ValueError("Target must be a non-empty string")
@@ -544,16 +624,23 @@ def main() -> None:
544
624
  if sys.platform == "win32":
545
625
  asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
546
626
 
627
+ args = parse_arguments()
628
+
547
629
  check_docker_installed()
548
630
  pull_docker_image()
549
631
 
550
632
  validate_environment()
551
633
  asyncio.run(warm_up_llm())
552
634
 
553
- args = parse_arguments()
554
635
  if not args.run_name:
555
636
  args.run_name = generate_run_name()
556
637
 
638
+ if args.target_type == "repository":
639
+ repo_url = args.target_dict["target_repo"]
640
+ cloned_path = clone_repository(repo_url, args.run_name)
641
+
642
+ args.target_dict["cloned_repo_path"] = cloned_path
643
+
557
644
  asyncio.run(run_strix_cli(args))
558
645
 
559
646
  results_path = Path("agent_runs") / args.run_name
@@ -8,7 +8,7 @@ from .registry import register_tool_renderer
8
8
 
9
9
  @register_tool_renderer
10
10
  class TerminalRenderer(BaseToolRenderer):
11
- tool_name: ClassVar[str] = "terminal_action"
11
+ tool_name: ClassVar[str] = "terminal_execute"
12
12
  css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"]
13
13
 
14
14
  @classmethod
@@ -17,11 +17,12 @@ class TerminalRenderer(BaseToolRenderer):
17
17
  status = tool_data.get("status", "unknown")
18
18
  result = tool_data.get("result", {})
19
19
 
20
- action = args.get("action", "unknown")
21
- inputs = args.get("inputs", [])
20
+ command = args.get("command", "")
21
+ is_input = args.get("is_input", False)
22
22
  terminal_id = args.get("terminal_id", "default")
23
+ timeout = args.get("timeout")
23
24
 
24
- content = cls._build_sleek_content(action, inputs, terminal_id, result)
25
+ content = cls._build_sleek_content(command, is_input, terminal_id, timeout, result)
25
26
 
26
27
  css_classes = cls.get_css_classes(status)
27
28
  return Static(content, classes=css_classes)
@@ -29,71 +30,102 @@ class TerminalRenderer(BaseToolRenderer):
29
30
  @classmethod
30
31
  def _build_sleek_content(
31
32
  cls,
32
- action: str,
33
- inputs: list[str],
33
+ command: str,
34
+ is_input: bool,
34
35
  terminal_id: str, # noqa: ARG003
36
+ timeout: float | None, # noqa: ARG003
35
37
  result: dict[str, Any], # noqa: ARG003
36
38
  ) -> str:
37
39
  terminal_icon = ">_"
38
40
 
39
- if action in {"create", "new_terminal"}:
40
- command = cls._format_command(inputs) if inputs else "bash"
41
- return f"{terminal_icon} [#22c55e]${command}[/]"
42
-
43
- if action == "send_input":
44
- command = cls._format_command(inputs)
45
- return f"{terminal_icon} [#22c55e]${command}[/]"
46
-
47
- if action == "wait":
48
- return f"{terminal_icon} [dim]waiting...[/]"
49
-
50
- if action == "close":
51
- return f"{terminal_icon} [dim]close[/]"
52
-
53
- if action == "get_snapshot":
54
- return f"{terminal_icon} [dim]snapshot[/]"
55
-
56
- return f"{terminal_icon} [dim]{action}[/]"
41
+ if not command.strip():
42
+ return f"{terminal_icon} [dim]getting logs...[/]"
43
+
44
+ control_sequences = {
45
+ "C-c",
46
+ "C-d",
47
+ "C-z",
48
+ "C-a",
49
+ "C-e",
50
+ "C-k",
51
+ "C-l",
52
+ "C-u",
53
+ "C-w",
54
+ "C-r",
55
+ "C-s",
56
+ "C-t",
57
+ "C-y",
58
+ "^c",
59
+ "^d",
60
+ "^z",
61
+ "^a",
62
+ "^e",
63
+ "^k",
64
+ "^l",
65
+ "^u",
66
+ "^w",
67
+ "^r",
68
+ "^s",
69
+ "^t",
70
+ "^y",
71
+ }
72
+ special_keys = {
73
+ "Enter",
74
+ "Escape",
75
+ "Space",
76
+ "Tab",
77
+ "BTab",
78
+ "BSpace",
79
+ "DC",
80
+ "IC",
81
+ "Up",
82
+ "Down",
83
+ "Left",
84
+ "Right",
85
+ "Home",
86
+ "End",
87
+ "PageUp",
88
+ "PageDown",
89
+ "PgUp",
90
+ "PgDn",
91
+ "PPage",
92
+ "NPage",
93
+ "F1",
94
+ "F2",
95
+ "F3",
96
+ "F4",
97
+ "F5",
98
+ "F6",
99
+ "F7",
100
+ "F8",
101
+ "F9",
102
+ "F10",
103
+ "F11",
104
+ "F12",
105
+ }
106
+
107
+ is_special = (
108
+ command in control_sequences
109
+ or command in special_keys
110
+ or command.startswith(("M-", "S-", "C-S-", "C-M-", "S-M-"))
111
+ )
112
+
113
+ if is_special:
114
+ return f"{terminal_icon} [#ef4444]{command}[/]"
115
+
116
+ if is_input:
117
+ formatted_command = cls._format_command_display(command)
118
+ return f"{terminal_icon} [#3b82f6]>>>[/] [#22c55e]{formatted_command}[/]"
119
+
120
+ formatted_command = cls._format_command_display(command)
121
+ return f"{terminal_icon} [#22c55e]$ {formatted_command}[/]"
57
122
 
58
123
  @classmethod
59
- def _format_command(cls, inputs: list[str]) -> str:
60
- if not inputs:
124
+ def _format_command_display(cls, command: str) -> str:
125
+ if not command:
61
126
  return ""
62
127
 
63
- command_parts = []
64
-
65
- for input_item in inputs:
66
- if input_item == "Enter":
67
- break
68
- if input_item.startswith("literal:"):
69
- command_parts.append(input_item[8:])
70
- elif input_item in [
71
- "Space",
72
- "Tab",
73
- "Backspace",
74
- "Up",
75
- "Down",
76
- "Left",
77
- "Right",
78
- "Home",
79
- "End",
80
- "PageUp",
81
- "PageDown",
82
- "Insert",
83
- "Delete",
84
- "Escape",
85
- ] or input_item.startswith(("^", "C-", "S-", "A-", "F")):
86
- if input_item == "Space":
87
- command_parts.append(" ")
88
- elif input_item == "Tab":
89
- command_parts.append("\t")
90
- continue
91
- else:
92
- command_parts.append(input_item)
93
-
94
- command = "".join(command_parts).strip()
95
-
96
128
  if len(command) > 200:
97
129
  command = command[:197] + "..."
98
130
 
99
- return cls.escape_markup(command) if command else "bash"
131
+ return cls.escape_markup(command)