strix-agent 0.1.9__py3-none-any.whl → 0.1.11__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.
- strix/agents/StrixAgent/strix_agent.py +18 -6
- strix/agents/StrixAgent/system_prompt.jinja +26 -7
- strix/agents/base_agent.py +3 -0
- strix/cli/app.py +3 -1
- strix/cli/main.py +85 -1
- strix/cli/tool_components/terminal_renderer.py +92 -60
- strix/llm/llm.py +3 -3
- strix/runtime/docker_runtime.py +204 -160
- strix/runtime/runtime.py +3 -2
- strix/runtime/tool_server.py +136 -28
- strix/tools/agents_graph/agents_graph_actions.py +4 -4
- strix/tools/agents_graph/agents_graph_actions_schema.xml +17 -1
- strix/tools/argument_parser.py +2 -1
- strix/tools/executor.py +3 -0
- strix/tools/terminal/__init__.py +2 -2
- strix/tools/terminal/terminal_actions.py +22 -40
- strix/tools/terminal/terminal_actions_schema.xml +113 -88
- strix/tools/terminal/terminal_manager.py +83 -123
- strix/tools/terminal/terminal_session.py +447 -0
- {strix_agent-0.1.9.dist-info → strix_agent-0.1.11.dist-info}/METADATA +4 -15
- {strix_agent-0.1.9.dist-info → strix_agent-0.1.11.dist-info}/RECORD +24 -24
- strix/tools/terminal/terminal_instance.py +0 -231
- {strix_agent-0.1.9.dist-info → strix_agent-0.1.11.dist-info}/LICENSE +0 -0
- {strix_agent-0.1.9.dist-info → strix_agent-0.1.11.dist-info}/WHEEL +0 -0
- {strix_agent-0.1.9.dist-info → strix_agent-0.1.11.dist-info}/entry_points.txt +0 -0
@@ -26,9 +26,21 @@ class StrixAgent(BaseAgent):
|
|
26
26
|
task_parts = []
|
27
27
|
|
28
28
|
if scan_type == "repository":
|
29
|
-
|
30
|
-
|
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
|
-
|
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"'{
|
45
|
-
f"Analyze the codebase at: {
|
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
|
-
-
|
149
|
-
- Each agent has its own: browser sessions, terminal sessions
|
150
|
-
-
|
151
|
-
-
|
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 -
|
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
|
|
strix/agents/base_agent.py
CHANGED
@@ -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
|
-
"
|
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,81 @@ 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
|
+
repo_url,
|
233
|
+
str(clone_path),
|
234
|
+
],
|
235
|
+
capture_output=True,
|
236
|
+
text=True,
|
237
|
+
check=True,
|
238
|
+
)
|
239
|
+
|
240
|
+
return str(clone_path.absolute())
|
241
|
+
|
242
|
+
except subprocess.CalledProcessError as e:
|
243
|
+
error_text = Text()
|
244
|
+
error_text.append("❌ ", style="bold red")
|
245
|
+
error_text.append("REPOSITORY CLONE FAILED", style="bold red")
|
246
|
+
error_text.append("\n\n", style="white")
|
247
|
+
error_text.append(f"Could not clone repository: {repo_url}\n", style="white")
|
248
|
+
error_text.append(
|
249
|
+
f"Error: {e.stderr if hasattr(e, 'stderr') and e.stderr else str(e)}", style="dim red"
|
250
|
+
)
|
251
|
+
|
252
|
+
panel = Panel(
|
253
|
+
error_text,
|
254
|
+
title="[bold red]🛡️ STRIX CLONE ERROR",
|
255
|
+
title_align="center",
|
256
|
+
border_style="red",
|
257
|
+
padding=(1, 2),
|
258
|
+
)
|
259
|
+
console.print("\n")
|
260
|
+
console.print(panel)
|
261
|
+
console.print()
|
262
|
+
sys.exit(1)
|
263
|
+
except FileNotFoundError:
|
264
|
+
error_text = Text()
|
265
|
+
error_text.append("❌ ", style="bold red")
|
266
|
+
error_text.append("GIT NOT FOUND", style="bold red")
|
267
|
+
error_text.append("\n\n", style="white")
|
268
|
+
error_text.append("Git is not installed or not available in PATH.\n", style="white")
|
269
|
+
error_text.append("Please install Git to clone repositories.\n", style="white")
|
270
|
+
|
271
|
+
panel = Panel(
|
272
|
+
error_text,
|
273
|
+
title="[bold red]🛡️ STRIX CLONE ERROR",
|
274
|
+
title_align="center",
|
275
|
+
border_style="red",
|
276
|
+
padding=(1, 2),
|
277
|
+
)
|
278
|
+
console.print("\n")
|
279
|
+
console.print(panel)
|
280
|
+
console.print()
|
281
|
+
sys.exit(1)
|
282
|
+
|
283
|
+
|
207
284
|
def infer_target_type(target: str) -> tuple[str, dict[str, str]]:
|
208
285
|
if not target or not isinstance(target, str):
|
209
286
|
raise ValueError("Target must be a non-empty string")
|
@@ -544,16 +621,23 @@ def main() -> None:
|
|
544
621
|
if sys.platform == "win32":
|
545
622
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
546
623
|
|
624
|
+
args = parse_arguments()
|
625
|
+
|
547
626
|
check_docker_installed()
|
548
627
|
pull_docker_image()
|
549
628
|
|
550
629
|
validate_environment()
|
551
630
|
asyncio.run(warm_up_llm())
|
552
631
|
|
553
|
-
args = parse_arguments()
|
554
632
|
if not args.run_name:
|
555
633
|
args.run_name = generate_run_name()
|
556
634
|
|
635
|
+
if args.target_type == "repository":
|
636
|
+
repo_url = args.target_dict["target_repo"]
|
637
|
+
cloned_path = clone_repository(repo_url, args.run_name)
|
638
|
+
|
639
|
+
args.target_dict["cloned_repo_path"] = cloned_path
|
640
|
+
|
557
641
|
asyncio.run(run_strix_cli(args))
|
558
642
|
|
559
643
|
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] = "
|
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
|
-
|
21
|
-
|
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(
|
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
|
-
|
33
|
-
|
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
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
60
|
-
if not
|
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)
|
131
|
+
return cls.escape_markup(command)
|
strix/llm/llm.py
CHANGED
@@ -313,7 +313,7 @@ class LLM:
|
|
313
313
|
completion_args["stop"] = ["</function>"]
|
314
314
|
|
315
315
|
if self._should_include_reasoning_effort():
|
316
|
-
completion_args["reasoning_effort"] = "
|
316
|
+
completion_args["reasoning_effort"] = "high"
|
317
317
|
|
318
318
|
queue = get_global_queue()
|
319
319
|
response = await queue.make_request(completion_args)
|
@@ -348,7 +348,7 @@ class LLM:
|
|
348
348
|
|
349
349
|
try:
|
350
350
|
cost = completion_cost(response) or 0.0
|
351
|
-
except
|
351
|
+
except Exception as e: # noqa: BLE001
|
352
352
|
logger.warning(f"Failed to calculate cost: {e}")
|
353
353
|
cost = 0.0
|
354
354
|
|
@@ -370,5 +370,5 @@ class LLM:
|
|
370
370
|
logger.info(f"Cache creation: {cache_creation_tokens} tokens written to cache")
|
371
371
|
|
372
372
|
logger.info(f"Usage stats: {self.usage_stats}")
|
373
|
-
except
|
373
|
+
except Exception as e: # noqa: BLE001
|
374
374
|
logger.warning(f"Failed to update usage stats: {e}")
|