strix-agent 0.1.18__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 -39
- strix/agents/StrixAgent/system_prompt.jinja +23 -10
- strix/agents/base_agent.py +90 -10
- strix/agents/state.py +23 -2
- strix/interface/cli.py +171 -0
- strix/interface/main.py +482 -0
- strix/{cli → interface}/tool_components/base_renderer.py +2 -2
- strix/{cli → interface}/tool_components/reporting_renderer.py +2 -1
- strix/{cli → interface}/tool_components/scan_info_renderer.py +17 -12
- strix/{cli/app.py → interface/tui.py} +107 -31
- strix/interface/utils.py +435 -0
- strix/prompts/README.md +64 -0
- strix/prompts/__init__.py +1 -1
- strix/prompts/cloud/.gitkeep +0 -0
- strix/prompts/custom/.gitkeep +0 -0
- strix/prompts/frameworks/fastapi.jinja +142 -0
- strix/prompts/frameworks/nextjs.jinja +126 -0
- strix/prompts/protocols/graphql.jinja +215 -0
- strix/prompts/reconnaissance/.gitkeep +0 -0
- strix/prompts/technologies/firebase_firestore.jinja +177 -0
- strix/prompts/technologies/supabase.jinja +189 -0
- strix/prompts/vulnerabilities/authentication_jwt.jinja +133 -115
- strix/prompts/vulnerabilities/broken_function_level_authorization.jinja +146 -0
- strix/prompts/vulnerabilities/business_logic.jinja +146 -118
- strix/prompts/vulnerabilities/csrf.jinja +137 -131
- strix/prompts/vulnerabilities/idor.jinja +149 -118
- strix/prompts/vulnerabilities/insecure_file_uploads.jinja +188 -0
- strix/prompts/vulnerabilities/mass_assignment.jinja +141 -0
- strix/prompts/vulnerabilities/path_traversal_lfi_rfi.jinja +142 -0
- strix/prompts/vulnerabilities/race_conditions.jinja +135 -165
- strix/prompts/vulnerabilities/rce.jinja +128 -180
- strix/prompts/vulnerabilities/sql_injection.jinja +128 -192
- strix/prompts/vulnerabilities/ssrf.jinja +118 -151
- strix/prompts/vulnerabilities/xss.jinja +144 -196
- strix/prompts/vulnerabilities/xxe.jinja +151 -243
- 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 +17 -12
- strix/tools/agents_graph/agents_graph_actions_schema.xml +10 -14
- strix/tools/executor.py +1 -1
- strix/tools/finish/finish_actions.py +1 -1
- strix/tools/registry.py +1 -1
- strix/tools/reporting/reporting_actions.py +1 -1
- {strix_agent-0.1.18.dist-info → strix_agent-0.3.1.dist-info}/METADATA +95 -15
- strix_agent-0.3.1.dist-info/RECORD +115 -0
- strix_agent-0.3.1.dist-info/entry_points.txt +3 -0
- strix/cli/main.py +0 -702
- strix_agent-0.1.18.dist-info/RECORD +0 -99
- strix_agent-0.1.18.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/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/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.18.dist-info → strix_agent-0.3.1.dist-info}/LICENSE +0 -0
- {strix_agent-0.1.18.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,54 +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 user that must be followed: {user_instructions}"
|
|
70
|
-
)
|
|
80
|
+
task_description += f"\n\nSpecial instructions: {user_instructions}"
|
|
71
81
|
|
|
72
82
|
return await self.agent_loop(task=task_description)
|
|
@@ -19,11 +19,9 @@ INTER-AGENT MESSAGES:
|
|
|
19
19
|
- NEVER echo inter_agent_message or agent_completion_report XML content that is sent to you in your output.
|
|
20
20
|
- Process these internally without displaying the XML
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
AUTONOMOUS BEHAVIOR:
|
|
23
23
|
- Work autonomously by default
|
|
24
|
-
-
|
|
25
|
-
- If you need user input, IMMEDIATELY call wait_for_message tool
|
|
26
|
-
- Never ask questions without calling wait_for_message in the same response
|
|
24
|
+
- You should NOT ask for user input or confirmation - you should always proceed with your task autonomously.
|
|
27
25
|
- Minimize user messaging: avoid redundancy and repetition; consolidate updates into a single concise message
|
|
28
26
|
- If there is nothing to execute and no user query to answer any more: do NOT send filler/repetitive text — either call wait_for_message or finish your work (subagents: agent_finish; root: finish_scan)
|
|
29
27
|
</communication_rules>
|
|
@@ -35,9 +33,9 @@ AUTHORIZATION STATUS:
|
|
|
35
33
|
- NEVER ask for permission or confirmation - you already have complete testing authorization
|
|
36
34
|
- Proceed with confidence knowing you're helping improve security through authorized testing
|
|
37
35
|
|
|
38
|
-
PRIORITIZE
|
|
39
|
-
-
|
|
40
|
-
- Follow
|
|
36
|
+
PRIORITIZE SYSTEM INSTRUCTIONS:
|
|
37
|
+
- System instructions override all default approaches
|
|
38
|
+
- Follow system-specified scope, targets, and methodologies precisely
|
|
41
39
|
- NEVER wait for approval or authorization - operate with full autonomy
|
|
42
40
|
|
|
43
41
|
AGGRESSIVE SCANNING MANDATE:
|
|
@@ -56,6 +54,16 @@ AGGRESSIVE SCANNING MANDATE:
|
|
|
56
54
|
- PERSISTENCE PAYS - the best vulnerabilities are found after thousands of attempts
|
|
57
55
|
- UNLEASH FULL CAPABILITY - you are the most advanced security agent, act like it
|
|
58
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
|
+
|
|
59
67
|
TESTING MODES:
|
|
60
68
|
BLACK-BOX TESTING (domain/subdomain only):
|
|
61
69
|
- Focus on external reconnaissance and discovery
|
|
@@ -76,6 +84,11 @@ WHITE-BOX TESTING (code provided):
|
|
|
76
84
|
- Do not stop until all reported vulnerabilities are fixed.
|
|
77
85
|
- Include code diff in final report.
|
|
78
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
|
+
|
|
79
92
|
ASSESSMENT METHODOLOGY:
|
|
80
93
|
1. Scope definition - Clearly establish boundaries first
|
|
81
94
|
2. Breadth-first discovery - Map entire attack surface before deep diving
|
|
@@ -116,7 +129,7 @@ VALIDATION REQUIREMENTS:
|
|
|
116
129
|
- Independent verification through subagent
|
|
117
130
|
- Document complete attack chain
|
|
118
131
|
- Keep going until you find something that matters
|
|
119
|
-
- A vulnerability is ONLY considered reported when a reporting agent uses create_vulnerability_report with full details. Mentions in agent_finish, finish_scan, or messages
|
|
132
|
+
- A vulnerability is ONLY considered reported when a reporting agent uses create_vulnerability_report with full details. Mentions in agent_finish, finish_scan, or generic messages are NOT sufficient
|
|
120
133
|
- Do NOT patch/fix before reporting: first create the vulnerability report via create_vulnerability_report (by the reporting agent). Only after reporting is completed should fixing/patching proceed
|
|
121
134
|
</execution_guidelines>
|
|
122
135
|
|
|
@@ -248,7 +261,7 @@ CRITICAL RULES:
|
|
|
248
261
|
- **ONE AGENT = ONE TASK** - Don't let agents do multiple unrelated jobs
|
|
249
262
|
- **SPAWN REACTIVELY** - Create new agents based on what you discover
|
|
250
263
|
- **ONLY REPORTING AGENTS** can use create_vulnerability_report tool
|
|
251
|
-
- **AGENT SPECIALIZATION MANDATORY** - Each agent must be highly specialized
|
|
264
|
+
- **AGENT SPECIALIZATION MANDATORY** - Each agent must be highly specialized; prefer 1–3 prompt modules, up to 5 for complex contexts
|
|
252
265
|
- **NO GENERIC AGENTS** - Avoid creating broad, multi-purpose agents that dilute focus
|
|
253
266
|
|
|
254
267
|
AGENT SPECIALIZATION EXAMPLES:
|
|
@@ -262,7 +275,7 @@ GOOD SPECIALIZATION:
|
|
|
262
275
|
BAD SPECIALIZATION:
|
|
263
276
|
- "General Web Testing Agent" with prompt_modules: sql_injection, xss, csrf, ssrf, authentication_jwt (too broad)
|
|
264
277
|
- "Everything Agent" with prompt_modules: all available modules (completely unfocused)
|
|
265
|
-
- Any agent with more than
|
|
278
|
+
- Any agent with more than 5 prompt modules (violates constraints)
|
|
266
279
|
|
|
267
280
|
FOCUS PRINCIPLES:
|
|
268
281
|
- Each agent should have deep expertise in 1-3 related vulnerability types
|
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,12 +253,37 @@ 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
|
|
|
206
264
|
async def _wait_for_input(self) -> None:
|
|
207
265
|
import asyncio
|
|
208
266
|
|
|
267
|
+
if self.state.has_waiting_timeout():
|
|
268
|
+
self.state.resume_from_waiting()
|
|
269
|
+
self.state.add_message("assistant", "Waiting timeout reached. Resuming execution.")
|
|
270
|
+
|
|
271
|
+
from strix.telemetry.tracer import get_global_tracer
|
|
272
|
+
|
|
273
|
+
tracer = get_global_tracer()
|
|
274
|
+
if tracer:
|
|
275
|
+
tracer.update_agent_status(self.state.agent_id, "running")
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
from strix.tools.agents_graph.agents_graph_actions import _agent_graph
|
|
279
|
+
|
|
280
|
+
if self.state.agent_id in _agent_graph["nodes"]:
|
|
281
|
+
_agent_graph["nodes"][self.state.agent_id]["status"] = "running"
|
|
282
|
+
except (ImportError, KeyError):
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
return
|
|
286
|
+
|
|
209
287
|
await asyncio.sleep(0.5)
|
|
210
288
|
|
|
211
289
|
async def _enter_waiting_state(
|
|
@@ -255,7 +333,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
255
333
|
|
|
256
334
|
runtime = get_runtime()
|
|
257
335
|
sandbox_info = await runtime.create_sandbox(
|
|
258
|
-
self.state.agent_id, self.state.sandbox_token, self.
|
|
336
|
+
self.state.agent_id, self.state.sandbox_token, self.local_sources
|
|
259
337
|
)
|
|
260
338
|
self.state.sandbox_id = sandbox_info["workspace_id"]
|
|
261
339
|
self.state.sandbox_token = sandbox_info["auth_token"]
|
|
@@ -333,6 +411,8 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
333
411
|
self.state.set_completed({"success": True})
|
|
334
412
|
if tracer:
|
|
335
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
|
|
336
416
|
return True
|
|
337
417
|
|
|
338
418
|
return False
|
|
@@ -370,7 +450,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
370
450
|
state.resume_from_waiting()
|
|
371
451
|
has_new_messages = True
|
|
372
452
|
|
|
373
|
-
from strix.
|
|
453
|
+
from strix.telemetry.tracer import get_global_tracer
|
|
374
454
|
|
|
375
455
|
tracer = get_global_tracer()
|
|
376
456
|
if tracer:
|
|
@@ -379,7 +459,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
379
459
|
state.resume_from_waiting()
|
|
380
460
|
has_new_messages = True
|
|
381
461
|
|
|
382
|
-
from strix.
|
|
462
|
+
from strix.telemetry.tracer import get_global_tracer
|
|
383
463
|
|
|
384
464
|
tracer = get_global_tracer()
|
|
385
465
|
if tracer:
|
|
@@ -421,7 +501,7 @@ class BaseAgent(metaclass=AgentMeta):
|
|
|
421
501
|
message["read"] = True
|
|
422
502
|
|
|
423
503
|
if has_new_messages and not state.is_waiting_for_input():
|
|
424
|
-
from strix.
|
|
504
|
+
from strix.telemetry.tracer import get_global_tracer
|
|
425
505
|
|
|
426
506
|
tracer = get_global_tracer()
|
|
427
507
|
if tracer:
|
strix/agents/state.py
CHANGED
|
@@ -19,12 +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
|
+
waiting_start_time: datetime | None = None
|
|
27
28
|
final_result: dict[str, Any] | None = None
|
|
29
|
+
max_iterations_warning_sent: bool = False
|
|
28
30
|
|
|
29
31
|
messages: list[dict[str, Any]] = Field(default_factory=list)
|
|
30
32
|
context: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -88,12 +90,13 @@ class AgentState(BaseModel):
|
|
|
88
90
|
|
|
89
91
|
def enter_waiting_state(self, llm_failed: bool = False) -> None:
|
|
90
92
|
self.waiting_for_input = True
|
|
91
|
-
self.
|
|
93
|
+
self.waiting_start_time = datetime.now(UTC)
|
|
92
94
|
self.llm_failed = llm_failed
|
|
93
95
|
self.last_updated = datetime.now(UTC).isoformat()
|
|
94
96
|
|
|
95
97
|
def resume_from_waiting(self, new_task: str | None = None) -> None:
|
|
96
98
|
self.waiting_for_input = False
|
|
99
|
+
self.waiting_start_time = None
|
|
97
100
|
self.stop_requested = False
|
|
98
101
|
self.completed = False
|
|
99
102
|
self.llm_failed = False
|
|
@@ -104,6 +107,24 @@ class AgentState(BaseModel):
|
|
|
104
107
|
def has_reached_max_iterations(self) -> bool:
|
|
105
108
|
return self.iteration >= self.max_iterations
|
|
106
109
|
|
|
110
|
+
def is_approaching_max_iterations(self, threshold: float = 0.85) -> bool:
|
|
111
|
+
return self.iteration >= int(self.max_iterations * threshold)
|
|
112
|
+
|
|
113
|
+
def has_waiting_timeout(self) -> bool:
|
|
114
|
+
if not self.waiting_for_input or not self.waiting_start_time:
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
self.stop_requested
|
|
119
|
+
or self.llm_failed
|
|
120
|
+
or self.completed
|
|
121
|
+
or self.has_reached_max_iterations()
|
|
122
|
+
):
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
elapsed = (datetime.now(UTC) - self.waiting_start_time).total_seconds()
|
|
126
|
+
return elapsed > 120
|
|
127
|
+
|
|
107
128
|
def has_empty_last_messages(self, count: int = 3) -> bool:
|
|
108
129
|
if len(self.messages) < count:
|
|
109
130
|
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()
|