strix-agent 0.4.0__py3-none-any.whl → 0.6.2__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 +3 -3
- strix/agents/StrixAgent/system_prompt.jinja +30 -26
- strix/agents/base_agent.py +159 -75
- strix/agents/state.py +5 -2
- strix/config/__init__.py +12 -0
- strix/config/config.py +172 -0
- strix/interface/assets/tui_styles.tcss +195 -230
- strix/interface/cli.py +16 -41
- strix/interface/main.py +151 -74
- strix/interface/streaming_parser.py +119 -0
- strix/interface/tool_components/__init__.py +4 -0
- strix/interface/tool_components/agent_message_renderer.py +190 -0
- strix/interface/tool_components/agents_graph_renderer.py +54 -38
- strix/interface/tool_components/base_renderer.py +68 -36
- strix/interface/tool_components/browser_renderer.py +106 -91
- strix/interface/tool_components/file_edit_renderer.py +117 -36
- strix/interface/tool_components/finish_renderer.py +43 -10
- strix/interface/tool_components/notes_renderer.py +63 -38
- strix/interface/tool_components/proxy_renderer.py +133 -92
- strix/interface/tool_components/python_renderer.py +121 -8
- strix/interface/tool_components/registry.py +19 -12
- strix/interface/tool_components/reporting_renderer.py +196 -28
- strix/interface/tool_components/scan_info_renderer.py +22 -19
- strix/interface/tool_components/terminal_renderer.py +270 -90
- strix/interface/tool_components/thinking_renderer.py +8 -6
- strix/interface/tool_components/todo_renderer.py +225 -0
- strix/interface/tool_components/user_message_renderer.py +26 -19
- strix/interface/tool_components/web_search_renderer.py +7 -6
- strix/interface/tui.py +907 -262
- strix/interface/utils.py +236 -4
- strix/llm/__init__.py +6 -2
- strix/llm/config.py +8 -5
- strix/llm/dedupe.py +217 -0
- strix/llm/llm.py +209 -356
- strix/llm/memory_compressor.py +6 -5
- strix/llm/utils.py +17 -8
- strix/runtime/__init__.py +12 -3
- strix/runtime/docker_runtime.py +121 -202
- strix/runtime/tool_server.py +55 -95
- strix/skills/README.md +64 -0
- strix/skills/__init__.py +110 -0
- strix/{prompts → skills}/frameworks/nextjs.jinja +26 -0
- strix/skills/scan_modes/deep.jinja +145 -0
- strix/skills/scan_modes/quick.jinja +63 -0
- strix/skills/scan_modes/standard.jinja +91 -0
- strix/telemetry/README.md +38 -0
- strix/telemetry/__init__.py +7 -1
- strix/telemetry/posthog.py +137 -0
- strix/telemetry/tracer.py +194 -54
- strix/tools/__init__.py +11 -4
- strix/tools/agents_graph/agents_graph_actions.py +20 -21
- strix/tools/agents_graph/agents_graph_actions_schema.xml +8 -8
- strix/tools/browser/browser_actions.py +10 -6
- strix/tools/browser/browser_actions_schema.xml +6 -1
- strix/tools/browser/browser_instance.py +96 -48
- strix/tools/browser/tab_manager.py +121 -102
- strix/tools/context.py +12 -0
- strix/tools/executor.py +63 -4
- strix/tools/file_edit/file_edit_actions.py +6 -3
- strix/tools/file_edit/file_edit_actions_schema.xml +45 -3
- strix/tools/finish/finish_actions.py +80 -105
- strix/tools/finish/finish_actions_schema.xml +121 -14
- strix/tools/notes/notes_actions.py +6 -33
- strix/tools/notes/notes_actions_schema.xml +50 -46
- strix/tools/proxy/proxy_actions.py +14 -2
- strix/tools/proxy/proxy_actions_schema.xml +0 -1
- strix/tools/proxy/proxy_manager.py +28 -16
- strix/tools/python/python_actions.py +2 -2
- strix/tools/python/python_actions_schema.xml +9 -1
- strix/tools/python/python_instance.py +39 -37
- strix/tools/python/python_manager.py +43 -31
- strix/tools/registry.py +73 -12
- strix/tools/reporting/reporting_actions.py +218 -31
- strix/tools/reporting/reporting_actions_schema.xml +256 -8
- strix/tools/terminal/terminal_actions.py +2 -2
- strix/tools/terminal/terminal_actions_schema.xml +6 -0
- strix/tools/terminal/terminal_manager.py +41 -30
- strix/tools/thinking/thinking_actions_schema.xml +27 -25
- strix/tools/todo/__init__.py +18 -0
- strix/tools/todo/todo_actions.py +568 -0
- strix/tools/todo/todo_actions_schema.xml +225 -0
- strix/utils/__init__.py +0 -0
- strix/utils/resource_paths.py +13 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/METADATA +90 -65
- strix_agent-0.6.2.dist-info/RECORD +134 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/WHEEL +1 -1
- strix/llm/request_queue.py +0 -87
- strix/prompts/README.md +0 -64
- strix/prompts/__init__.py +0 -109
- strix_agent-0.4.0.dist-info/RECORD +0 -118
- /strix/{prompts → skills}/cloud/.gitkeep +0 -0
- /strix/{prompts → skills}/coordination/root_agent.jinja +0 -0
- /strix/{prompts → skills}/custom/.gitkeep +0 -0
- /strix/{prompts → skills}/frameworks/fastapi.jinja +0 -0
- /strix/{prompts → skills}/protocols/graphql.jinja +0 -0
- /strix/{prompts → skills}/reconnaissance/.gitkeep +0 -0
- /strix/{prompts → skills}/technologies/firebase_firestore.jinja +0 -0
- /strix/{prompts → skills}/technologies/supabase.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/authentication_jwt.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/broken_function_level_authorization.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/business_logic.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/csrf.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/idor.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/information_disclosure.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/insecure_file_uploads.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/mass_assignment.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/open_redirect.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/path_traversal_lfi_rfi.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/race_conditions.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/rce.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/sql_injection.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/ssrf.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/subdomain_takeover.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/xss.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/xxe.jinja +0 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/entry_points.txt +0 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
<tools>
|
|
2
2
|
<tool name="create_note">
|
|
3
|
-
<description>Create a personal note for
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
for keeping track of tasks, ideas, and things to remember or follow up on.</details>
|
|
3
|
+
<description>Create a personal note for observations, findings, and research during the scan.</description>
|
|
4
|
+
<details>Use this tool for documenting discoveries, observations, methodology notes, and questions.
|
|
5
|
+
This is your personal notepad for recording information you want to remember or reference later.
|
|
6
|
+
For tracking actionable tasks, use the todo tool instead.</details>
|
|
8
7
|
<parameters>
|
|
9
8
|
<parameter name="title" type="string" required="true">
|
|
10
9
|
<description>Title of the note</description>
|
|
@@ -13,49 +12,66 @@
|
|
|
13
12
|
<description>Content of the note</description>
|
|
14
13
|
</parameter>
|
|
15
14
|
<parameter name="category" type="string" required="false">
|
|
16
|
-
<description>Category to organize the note (default: "general", "findings", "methodology", "
|
|
15
|
+
<description>Category to organize the note (default: "general", "findings", "methodology", "questions", "plan")</description>
|
|
17
16
|
</parameter>
|
|
18
17
|
<parameter name="tags" type="string" required="false">
|
|
19
18
|
<description>Tags for categorization</description>
|
|
20
19
|
</parameter>
|
|
21
|
-
<parameter name="priority" type="string" required="false">
|
|
22
|
-
<description>Priority level of the note ("low", "normal", "high", "urgent")</description>
|
|
23
|
-
</parameter>
|
|
24
20
|
</parameters>
|
|
25
21
|
<returns type="Dict[str, Any]">
|
|
26
22
|
<description>Response containing: - note_id: ID of the created note - success: Whether the note was created successfully</description>
|
|
27
23
|
</returns>
|
|
28
24
|
<examples>
|
|
29
|
-
#
|
|
25
|
+
# Document an interesting finding
|
|
30
26
|
<function=create_note>
|
|
31
|
-
<parameter=title>
|
|
32
|
-
<parameter=content>
|
|
33
|
-
on the HTTPS service discovered on port 443. Also check for certificate
|
|
34
|
-
transparency logs.</parameter>
|
|
35
|
-
<parameter=category>todo</parameter>
|
|
36
|
-
<parameter=tags>["ssl", "certificate", "followup"]</parameter>
|
|
37
|
-
<parameter=priority>normal</parameter>
|
|
38
|
-
</function>
|
|
27
|
+
<parameter=title>Authentication Bypass Findings</parameter>
|
|
28
|
+
<parameter=content>Discovered multiple authentication bypass vectors in the login system:
|
|
39
29
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
30
|
+
1. SQL Injection in username field
|
|
31
|
+
- Payload: admin'--
|
|
32
|
+
- Result: Full authentication bypass
|
|
33
|
+
- Endpoint: POST /api/v1/auth/login
|
|
34
|
+
|
|
35
|
+
2. JWT Token Weakness
|
|
36
|
+
- Algorithm confusion attack possible (RS256 -> HS256)
|
|
37
|
+
- Token expiration is 24 hours but no refresh rotation
|
|
38
|
+
- Token stored in localStorage (XSS risk)
|
|
39
|
+
|
|
40
|
+
3. Password Reset Flow
|
|
41
|
+
- Reset tokens are only 6 digits (brute-forceable)
|
|
42
|
+
- No rate limiting on reset attempts
|
|
43
|
+
- Token valid for 48 hours
|
|
44
|
+
|
|
45
|
+
Next Steps:
|
|
46
|
+
- Extract full database via SQL injection
|
|
47
|
+
- Test JWT manipulation attacks
|
|
48
|
+
- Attempt password reset brute force</parameter>
|
|
49
|
+
<parameter=category>findings</parameter>
|
|
50
|
+
<parameter=tags>["auth", "sqli", "jwt", "critical"]</parameter>
|
|
48
51
|
</function>
|
|
49
52
|
|
|
50
|
-
#
|
|
53
|
+
# Methodology note
|
|
51
54
|
<function=create_note>
|
|
52
|
-
<parameter=title>
|
|
53
|
-
<parameter=content>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
<parameter=title>API Endpoint Mapping Complete</parameter>
|
|
56
|
+
<parameter=content>Completed comprehensive API enumeration using multiple techniques:
|
|
57
|
+
|
|
58
|
+
Discovered Endpoints:
|
|
59
|
+
- /api/v1/auth/* - Authentication endpoints (login, register, reset)
|
|
60
|
+
- /api/v1/users/* - User management (profile, settings, admin)
|
|
61
|
+
- /api/v1/orders/* - Order management (IDOR vulnerability confirmed)
|
|
62
|
+
- /api/v1/admin/* - Admin panel (403 but may be bypassable)
|
|
63
|
+
- /api/internal/* - Internal APIs (should not be exposed)
|
|
64
|
+
|
|
65
|
+
Methods Used:
|
|
66
|
+
- Analyzed JavaScript bundles for API calls
|
|
67
|
+
- Bruteforced common paths with ffuf
|
|
68
|
+
- Reviewed OpenAPI/Swagger documentation at /api/docs
|
|
69
|
+
- Monitored traffic during normal application usage
|
|
70
|
+
|
|
71
|
+
Priority Targets:
|
|
72
|
+
The /api/internal/* endpoints are high priority as they appear to lack authentication checks based on error message differences.</parameter>
|
|
73
|
+
<parameter=category>methodology</parameter>
|
|
74
|
+
<parameter=tags>["api", "enumeration", "recon"]</parameter>
|
|
59
75
|
</function>
|
|
60
76
|
</examples>
|
|
61
77
|
</tool>
|
|
@@ -84,9 +100,6 @@
|
|
|
84
100
|
<parameter name="tags" type="string" required="false">
|
|
85
101
|
<description>Filter by tags (returns notes with any of these tags)</description>
|
|
86
102
|
</parameter>
|
|
87
|
-
<parameter name="priority" type="string" required="false">
|
|
88
|
-
<description>Filter by priority level</description>
|
|
89
|
-
</parameter>
|
|
90
103
|
<parameter name="search" type="string" required="false">
|
|
91
104
|
<description>Search query to find in note titles and content</description>
|
|
92
105
|
</parameter>
|
|
@@ -100,11 +113,6 @@
|
|
|
100
113
|
<parameter=category>findings</parameter>
|
|
101
114
|
</function>
|
|
102
115
|
|
|
103
|
-
# List high priority items
|
|
104
|
-
<function=list_notes>
|
|
105
|
-
<parameter=priority>high</parameter>
|
|
106
|
-
</function>
|
|
107
|
-
|
|
108
116
|
# Search for SQL injection related notes
|
|
109
117
|
<function=list_notes>
|
|
110
118
|
<parameter=search>SQL injection</parameter>
|
|
@@ -132,9 +140,6 @@
|
|
|
132
140
|
<parameter name="tags" type="string" required="false">
|
|
133
141
|
<description>New tags for the note</description>
|
|
134
142
|
</parameter>
|
|
135
|
-
<parameter name="priority" type="string" required="false">
|
|
136
|
-
<description>New priority level</description>
|
|
137
|
-
</parameter>
|
|
138
143
|
</parameters>
|
|
139
144
|
<returns type="Dict[str, Any]">
|
|
140
145
|
<description>Response containing: - success: Whether the note was updated successfully</description>
|
|
@@ -143,7 +148,6 @@
|
|
|
143
148
|
<function=update_note>
|
|
144
149
|
<parameter=note_id>note_123</parameter>
|
|
145
150
|
<parameter=content>Updated content with new findings...</parameter>
|
|
146
|
-
<parameter=priority>urgent</parameter>
|
|
147
151
|
</function>
|
|
148
152
|
</examples>
|
|
149
153
|
</tool>
|
|
@@ -2,8 +2,6 @@ from typing import Any, Literal
|
|
|
2
2
|
|
|
3
3
|
from strix.tools.registry import register_tool
|
|
4
4
|
|
|
5
|
-
from .proxy_manager import get_proxy_manager
|
|
6
|
-
|
|
7
5
|
|
|
8
6
|
RequestPart = Literal["request", "response"]
|
|
9
7
|
|
|
@@ -27,6 +25,8 @@ def list_requests(
|
|
|
27
25
|
sort_order: Literal["asc", "desc"] = "desc",
|
|
28
26
|
scope_id: str | None = None,
|
|
29
27
|
) -> dict[str, Any]:
|
|
28
|
+
from .proxy_manager import get_proxy_manager
|
|
29
|
+
|
|
30
30
|
manager = get_proxy_manager()
|
|
31
31
|
return manager.list_requests(
|
|
32
32
|
httpql_filter, start_page, end_page, page_size, sort_by, sort_order, scope_id
|
|
@@ -41,6 +41,8 @@ def view_request(
|
|
|
41
41
|
page: int = 1,
|
|
42
42
|
page_size: int = 50,
|
|
43
43
|
) -> dict[str, Any]:
|
|
44
|
+
from .proxy_manager import get_proxy_manager
|
|
45
|
+
|
|
44
46
|
manager = get_proxy_manager()
|
|
45
47
|
return manager.view_request(request_id, part, search_pattern, page, page_size)
|
|
46
48
|
|
|
@@ -53,6 +55,8 @@ def send_request(
|
|
|
53
55
|
body: str = "",
|
|
54
56
|
timeout: int = 30,
|
|
55
57
|
) -> dict[str, Any]:
|
|
58
|
+
from .proxy_manager import get_proxy_manager
|
|
59
|
+
|
|
56
60
|
if headers is None:
|
|
57
61
|
headers = {}
|
|
58
62
|
manager = get_proxy_manager()
|
|
@@ -64,6 +68,8 @@ def repeat_request(
|
|
|
64
68
|
request_id: str,
|
|
65
69
|
modifications: dict[str, Any] | None = None,
|
|
66
70
|
) -> dict[str, Any]:
|
|
71
|
+
from .proxy_manager import get_proxy_manager
|
|
72
|
+
|
|
67
73
|
if modifications is None:
|
|
68
74
|
modifications = {}
|
|
69
75
|
manager = get_proxy_manager()
|
|
@@ -78,6 +84,8 @@ def scope_rules(
|
|
|
78
84
|
scope_id: str | None = None,
|
|
79
85
|
scope_name: str | None = None,
|
|
80
86
|
) -> dict[str, Any]:
|
|
87
|
+
from .proxy_manager import get_proxy_manager
|
|
88
|
+
|
|
81
89
|
manager = get_proxy_manager()
|
|
82
90
|
return manager.scope_rules(action, allowlist, denylist, scope_id, scope_name)
|
|
83
91
|
|
|
@@ -89,6 +97,8 @@ def list_sitemap(
|
|
|
89
97
|
depth: Literal["DIRECT", "ALL"] = "DIRECT",
|
|
90
98
|
page: int = 1,
|
|
91
99
|
) -> dict[str, Any]:
|
|
100
|
+
from .proxy_manager import get_proxy_manager
|
|
101
|
+
|
|
92
102
|
manager = get_proxy_manager()
|
|
93
103
|
return manager.list_sitemap(scope_id, parent_id, depth, page)
|
|
94
104
|
|
|
@@ -97,5 +107,7 @@ def list_sitemap(
|
|
|
97
107
|
def view_sitemap_entry(
|
|
98
108
|
entry_id: str,
|
|
99
109
|
) -> dict[str, Any]:
|
|
110
|
+
from .proxy_manager import get_proxy_manager
|
|
111
|
+
|
|
100
112
|
manager = get_proxy_manager()
|
|
101
113
|
return manager.view_sitemap_entry(entry_id)
|
|
@@ -16,17 +16,24 @@ if TYPE_CHECKING:
|
|
|
16
16
|
from collections.abc import Callable
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
CAIDO_PORT = 48080 # Fixed port inside container
|
|
20
|
+
|
|
21
|
+
|
|
19
22
|
class ProxyManager:
|
|
20
23
|
def __init__(self, auth_token: str | None = None):
|
|
21
24
|
host = "127.0.0.1"
|
|
22
|
-
|
|
23
|
-
self.
|
|
24
|
-
|
|
25
|
+
self.base_url = f"http://{host}:{CAIDO_PORT}/graphql"
|
|
26
|
+
self.proxies = {
|
|
27
|
+
"http": f"http://{host}:{CAIDO_PORT}",
|
|
28
|
+
"https": f"http://{host}:{CAIDO_PORT}",
|
|
29
|
+
}
|
|
25
30
|
self.auth_token = auth_token or os.getenv("CAIDO_API_TOKEN")
|
|
26
|
-
|
|
31
|
+
|
|
32
|
+
def _get_client(self) -> Client:
|
|
33
|
+
transport = RequestsHTTPTransport(
|
|
27
34
|
url=self.base_url, headers={"Authorization": f"Bearer {self.auth_token}"}
|
|
28
35
|
)
|
|
29
|
-
|
|
36
|
+
return Client(transport=transport, fetch_schema_from_transport=False)
|
|
30
37
|
|
|
31
38
|
def list_requests(
|
|
32
39
|
self,
|
|
@@ -85,7 +92,7 @@ class ProxyManager:
|
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
try:
|
|
88
|
-
result = self.
|
|
95
|
+
result = self._get_client().execute(query, variable_values=variables)
|
|
89
96
|
data = result.get("requestsByOffset", {})
|
|
90
97
|
nodes = [edge["node"] for edge in data.get("edges", [])]
|
|
91
98
|
|
|
@@ -132,7 +139,9 @@ class ProxyManager:
|
|
|
132
139
|
return {"error": f"Invalid part '{part}'. Use 'request' or 'response'"}
|
|
133
140
|
|
|
134
141
|
try:
|
|
135
|
-
result = self.
|
|
142
|
+
result = self._get_client().execute(
|
|
143
|
+
gql(queries[part]), variable_values={"id": request_id}
|
|
144
|
+
)
|
|
136
145
|
request_data = result.get("request", {})
|
|
137
146
|
|
|
138
147
|
if not request_data:
|
|
@@ -430,7 +439,9 @@ class ProxyManager:
|
|
|
430
439
|
}
|
|
431
440
|
|
|
432
441
|
def _handle_scope_list(self) -> dict[str, Any]:
|
|
433
|
-
result = self.
|
|
442
|
+
result = self._get_client().execute(
|
|
443
|
+
gql("query { scopes { id name allowlist denylist indexed } }")
|
|
444
|
+
)
|
|
434
445
|
scopes = result.get("scopes", [])
|
|
435
446
|
return {"scopes": scopes, "count": len(scopes)}
|
|
436
447
|
|
|
@@ -438,7 +449,7 @@ class ProxyManager:
|
|
|
438
449
|
if not scope_id:
|
|
439
450
|
return self._handle_scope_list()
|
|
440
451
|
|
|
441
|
-
result = self.
|
|
452
|
+
result = self._get_client().execute(
|
|
442
453
|
gql(
|
|
443
454
|
"query GetScope($id: ID!) { scope(id: $id) { id name allowlist denylist indexed } }"
|
|
444
455
|
),
|
|
@@ -467,7 +478,7 @@ class ProxyManager:
|
|
|
467
478
|
}
|
|
468
479
|
""")
|
|
469
480
|
|
|
470
|
-
result = self.
|
|
481
|
+
result = self._get_client().execute(
|
|
471
482
|
mutation,
|
|
472
483
|
variable_values={
|
|
473
484
|
"input": {
|
|
@@ -507,7 +518,7 @@ class ProxyManager:
|
|
|
507
518
|
}
|
|
508
519
|
""")
|
|
509
520
|
|
|
510
|
-
result = self.
|
|
521
|
+
result = self._get_client().execute(
|
|
511
522
|
mutation,
|
|
512
523
|
variable_values={
|
|
513
524
|
"id": scope_id,
|
|
@@ -530,7 +541,7 @@ class ProxyManager:
|
|
|
530
541
|
if not scope_id:
|
|
531
542
|
return {"error": "scope_id required for delete"}
|
|
532
543
|
|
|
533
|
-
result = self.
|
|
544
|
+
result = self._get_client().execute(
|
|
534
545
|
gql("mutation DeleteScope($id: ID!) { deleteScope(id: $id) { deletedId } }"),
|
|
535
546
|
variable_values={"id": scope_id},
|
|
536
547
|
)
|
|
@@ -607,7 +618,7 @@ class ProxyManager:
|
|
|
607
618
|
}
|
|
608
619
|
}
|
|
609
620
|
""")
|
|
610
|
-
result = self.
|
|
621
|
+
result = self._get_client().execute(
|
|
611
622
|
query, variable_values={"parentId": parent_id, "depth": depth}
|
|
612
623
|
)
|
|
613
624
|
data = result.get("sitemapDescendantEntries", {})
|
|
@@ -624,7 +635,7 @@ class ProxyManager:
|
|
|
624
635
|
}
|
|
625
636
|
}
|
|
626
637
|
""")
|
|
627
|
-
result = self.
|
|
638
|
+
result = self._get_client().execute(query, variable_values={"scopeId": scope_id})
|
|
628
639
|
data = result.get("sitemapRootEntries", {})
|
|
629
640
|
|
|
630
641
|
all_nodes = [edge["node"] for edge in data.get("edges", [])]
|
|
@@ -731,7 +742,7 @@ class ProxyManager:
|
|
|
731
742
|
}
|
|
732
743
|
""")
|
|
733
744
|
|
|
734
|
-
result = self.
|
|
745
|
+
result = self._get_client().execute(query, variable_values={"id": entry_id})
|
|
735
746
|
entry = result.get("sitemapEntry")
|
|
736
747
|
|
|
737
748
|
if not entry:
|
|
@@ -780,6 +791,7 @@ _PROXY_MANAGER: ProxyManager | None = None
|
|
|
780
791
|
|
|
781
792
|
|
|
782
793
|
def get_proxy_manager() -> ProxyManager:
|
|
794
|
+
global _PROXY_MANAGER # noqa: PLW0603
|
|
783
795
|
if _PROXY_MANAGER is None:
|
|
784
|
-
|
|
796
|
+
_PROXY_MANAGER = ProxyManager()
|
|
785
797
|
return _PROXY_MANAGER
|
|
@@ -2,8 +2,6 @@ from typing import Any, Literal
|
|
|
2
2
|
|
|
3
3
|
from strix.tools.registry import register_tool
|
|
4
4
|
|
|
5
|
-
from .python_manager import get_python_session_manager
|
|
6
|
-
|
|
7
5
|
|
|
8
6
|
PythonAction = Literal["new_session", "execute", "close", "list_sessions"]
|
|
9
7
|
|
|
@@ -15,6 +13,8 @@ def python_action(
|
|
|
15
13
|
timeout: int = 30,
|
|
16
14
|
session_id: str | None = None,
|
|
17
15
|
) -> dict[str, Any]:
|
|
16
|
+
from .python_manager import get_python_session_manager
|
|
17
|
+
|
|
18
18
|
def _validate_code(action_name: str, code: str | None) -> None:
|
|
19
19
|
if not code:
|
|
20
20
|
raise ValueError(f"code parameter is required for {action_name} action")
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
1
|
<tools>
|
|
3
2
|
<tool name="python_action">
|
|
4
3
|
<description>Perform Python actions using persistent interpreter sessions for cybersecurity tasks.</description>
|
|
@@ -55,6 +54,7 @@
|
|
|
55
54
|
- Print statements and stdout are captured
|
|
56
55
|
- Variables persist between executions in the same session
|
|
57
56
|
- Imports, function definitions, etc. persist in the session
|
|
57
|
+
- IMPORTANT (multiline): Put real line breaks in your code. Do NOT emit literal "\n" sequences — use actual newlines.
|
|
58
58
|
- IPython magic commands are fully supported (%pip, %time, %whos, %%writefile, etc.)
|
|
59
59
|
- Line magics (%) and cell magics (%%) work as expected
|
|
60
60
|
6. CLOSE: Terminates the session completely and frees memory
|
|
@@ -73,6 +73,14 @@
|
|
|
73
73
|
print("Security analysis session started")</parameter>
|
|
74
74
|
</function>
|
|
75
75
|
|
|
76
|
+
<function=python_action>
|
|
77
|
+
<parameter=action>execute</parameter>
|
|
78
|
+
<parameter=code>import requests
|
|
79
|
+
url = "https://example.com"
|
|
80
|
+
resp = requests.get(url, timeout=10)
|
|
81
|
+
print(resp.status_code)</parameter>
|
|
82
|
+
</function>
|
|
83
|
+
|
|
76
84
|
# Analyze security data in the default session
|
|
77
85
|
<function=python_action>
|
|
78
86
|
<parameter=action>execute</parameter>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import io
|
|
2
|
-
import signal
|
|
3
2
|
import sys
|
|
4
3
|
import threading
|
|
5
4
|
from typing import Any
|
|
@@ -57,28 +56,6 @@ class PythonInstance:
|
|
|
57
56
|
}
|
|
58
57
|
return None
|
|
59
58
|
|
|
60
|
-
def _setup_execution_environment(self, timeout: int) -> tuple[Any, io.StringIO, io.StringIO]:
|
|
61
|
-
stdout_capture = io.StringIO()
|
|
62
|
-
stderr_capture = io.StringIO()
|
|
63
|
-
|
|
64
|
-
def timeout_handler(signum: int, frame: Any) -> None:
|
|
65
|
-
raise TimeoutError(f"Code execution timed out after {timeout} seconds")
|
|
66
|
-
|
|
67
|
-
old_handler = signal.signal(signal.SIGALRM, timeout_handler)
|
|
68
|
-
signal.alarm(timeout)
|
|
69
|
-
|
|
70
|
-
sys.stdout = stdout_capture
|
|
71
|
-
sys.stderr = stderr_capture
|
|
72
|
-
|
|
73
|
-
return old_handler, stdout_capture, stderr_capture
|
|
74
|
-
|
|
75
|
-
def _cleanup_execution_environment(
|
|
76
|
-
self, old_handler: Any, old_stdout: Any, old_stderr: Any
|
|
77
|
-
) -> None:
|
|
78
|
-
signal.signal(signal.SIGALRM, old_handler)
|
|
79
|
-
sys.stdout = old_stdout
|
|
80
|
-
sys.stderr = old_stderr
|
|
81
|
-
|
|
82
59
|
def _truncate_output(self, content: str, max_length: int, suffix: str) -> str:
|
|
83
60
|
if len(content) > max_length:
|
|
84
61
|
return content[:max_length] + suffix
|
|
@@ -142,27 +119,52 @@ class PythonInstance:
|
|
|
142
119
|
return session_error
|
|
143
120
|
|
|
144
121
|
with self._execution_lock:
|
|
145
|
-
|
|
122
|
+
result_container: dict[str, Any] = {}
|
|
123
|
+
stdout_capture = io.StringIO()
|
|
124
|
+
stderr_capture = io.StringIO()
|
|
125
|
+
cancelled = threading.Event()
|
|
146
126
|
|
|
147
|
-
|
|
148
|
-
old_handler, stdout_capture, stderr_capture = self._setup_execution_environment(
|
|
149
|
-
timeout
|
|
150
|
-
)
|
|
127
|
+
old_stdout, old_stderr = sys.stdout, sys.stderr
|
|
151
128
|
|
|
129
|
+
def _run_code() -> None:
|
|
152
130
|
try:
|
|
131
|
+
sys.stdout = stdout_capture
|
|
132
|
+
sys.stderr = stderr_capture
|
|
153
133
|
execution_result = self.shell.run_cell(code, silent=False, store_history=True)
|
|
154
|
-
|
|
134
|
+
result_container["execution_result"] = execution_result
|
|
135
|
+
result_container["stdout"] = stdout_capture.getvalue()
|
|
136
|
+
result_container["stderr"] = stderr_capture.getvalue()
|
|
137
|
+
except (KeyboardInterrupt, SystemExit) as e:
|
|
138
|
+
result_container["error"] = e
|
|
139
|
+
except Exception as e: # noqa: BLE001
|
|
140
|
+
result_container["error"] = e
|
|
141
|
+
finally:
|
|
142
|
+
if not cancelled.is_set():
|
|
143
|
+
sys.stdout = old_stdout
|
|
144
|
+
sys.stderr = old_stderr
|
|
145
|
+
|
|
146
|
+
exec_thread = threading.Thread(target=_run_code, daemon=True)
|
|
147
|
+
exec_thread.start()
|
|
148
|
+
exec_thread.join(timeout=timeout)
|
|
149
|
+
|
|
150
|
+
if exec_thread.is_alive():
|
|
151
|
+
cancelled.set()
|
|
152
|
+
sys.stdout, sys.stderr = old_stdout, old_stderr
|
|
153
|
+
return self._handle_execution_error(
|
|
154
|
+
TimeoutError(f"Code execution timed out after {timeout} seconds")
|
|
155
|
+
)
|
|
155
156
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
157
|
+
if "error" in result_container:
|
|
158
|
+
return self._handle_execution_error(result_container["error"])
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
if "execution_result" in result_container:
|
|
161
|
+
return self._format_execution_result(
|
|
162
|
+
result_container["execution_result"],
|
|
163
|
+
result_container.get("stdout", ""),
|
|
164
|
+
result_container.get("stderr", ""),
|
|
165
|
+
)
|
|
163
166
|
|
|
164
|
-
|
|
165
|
-
self._cleanup_execution_environment(old_handler, old_stdout, old_stderr)
|
|
167
|
+
return self._handle_execution_error(RuntimeError("Unknown execution error"))
|
|
166
168
|
|
|
167
169
|
def close(self) -> None:
|
|
168
170
|
self.is_running = False
|
|
@@ -1,33 +1,41 @@
|
|
|
1
1
|
import atexit
|
|
2
2
|
import contextlib
|
|
3
|
-
import signal
|
|
4
|
-
import sys
|
|
5
3
|
import threading
|
|
6
4
|
from typing import Any
|
|
7
5
|
|
|
6
|
+
from strix.tools.context import get_current_agent_id
|
|
7
|
+
|
|
8
8
|
from .python_instance import PythonInstance
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class PythonSessionManager:
|
|
12
12
|
def __init__(self) -> None:
|
|
13
|
-
self.
|
|
13
|
+
self._sessions_by_agent: dict[str, dict[str, PythonInstance]] = {}
|
|
14
14
|
self._lock = threading.Lock()
|
|
15
15
|
self.default_session_id = "default"
|
|
16
16
|
|
|
17
17
|
self._register_cleanup_handlers()
|
|
18
18
|
|
|
19
|
+
def _get_agent_sessions(self) -> dict[str, PythonInstance]:
|
|
20
|
+
agent_id = get_current_agent_id()
|
|
21
|
+
with self._lock:
|
|
22
|
+
if agent_id not in self._sessions_by_agent:
|
|
23
|
+
self._sessions_by_agent[agent_id] = {}
|
|
24
|
+
return self._sessions_by_agent[agent_id]
|
|
25
|
+
|
|
19
26
|
def create_session(
|
|
20
27
|
self, session_id: str | None = None, initial_code: str | None = None, timeout: int = 30
|
|
21
28
|
) -> dict[str, Any]:
|
|
22
29
|
if session_id is None:
|
|
23
30
|
session_id = self.default_session_id
|
|
24
31
|
|
|
32
|
+
sessions = self._get_agent_sessions()
|
|
25
33
|
with self._lock:
|
|
26
|
-
if session_id in
|
|
34
|
+
if session_id in sessions:
|
|
27
35
|
raise ValueError(f"Python session '{session_id}' already exists")
|
|
28
36
|
|
|
29
37
|
session = PythonInstance(session_id)
|
|
30
|
-
|
|
38
|
+
sessions[session_id] = session
|
|
31
39
|
|
|
32
40
|
if initial_code:
|
|
33
41
|
result = session.execute_code(initial_code, timeout)
|
|
@@ -51,11 +59,12 @@ class PythonSessionManager:
|
|
|
51
59
|
if not code:
|
|
52
60
|
raise ValueError("No code provided for execution")
|
|
53
61
|
|
|
62
|
+
sessions = self._get_agent_sessions()
|
|
54
63
|
with self._lock:
|
|
55
|
-
if session_id not in
|
|
64
|
+
if session_id not in sessions:
|
|
56
65
|
raise ValueError(f"Python session '{session_id}' not found")
|
|
57
66
|
|
|
58
|
-
session =
|
|
67
|
+
session = sessions[session_id]
|
|
59
68
|
|
|
60
69
|
result = session.execute_code(code, timeout)
|
|
61
70
|
result["message"] = f"Code executed in session '{session_id}'"
|
|
@@ -65,11 +74,12 @@ class PythonSessionManager:
|
|
|
65
74
|
if session_id is None:
|
|
66
75
|
session_id = self.default_session_id
|
|
67
76
|
|
|
77
|
+
sessions = self._get_agent_sessions()
|
|
68
78
|
with self._lock:
|
|
69
|
-
if session_id not in
|
|
79
|
+
if session_id not in sessions:
|
|
70
80
|
raise ValueError(f"Python session '{session_id}' not found")
|
|
71
81
|
|
|
72
|
-
session =
|
|
82
|
+
session = sessions.pop(session_id)
|
|
73
83
|
|
|
74
84
|
session.close()
|
|
75
85
|
return {
|
|
@@ -79,9 +89,10 @@ class PythonSessionManager:
|
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
def list_sessions(self) -> dict[str, Any]:
|
|
92
|
+
sessions = self._get_agent_sessions()
|
|
82
93
|
with self._lock:
|
|
83
94
|
session_info = {}
|
|
84
|
-
for sid, session in
|
|
95
|
+
for sid, session in sessions.items():
|
|
85
96
|
session_info[sid] = {
|
|
86
97
|
"is_running": session.is_running,
|
|
87
98
|
"is_alive": session.is_alive(),
|
|
@@ -89,40 +100,41 @@ class PythonSessionManager:
|
|
|
89
100
|
|
|
90
101
|
return {"sessions": session_info, "total_count": len(session_info)}
|
|
91
102
|
|
|
103
|
+
def cleanup_agent(self, agent_id: str) -> None:
|
|
104
|
+
with self._lock:
|
|
105
|
+
sessions = self._sessions_by_agent.pop(agent_id, {})
|
|
106
|
+
|
|
107
|
+
for session in sessions.values():
|
|
108
|
+
with contextlib.suppress(Exception):
|
|
109
|
+
session.close()
|
|
110
|
+
|
|
92
111
|
def cleanup_dead_sessions(self) -> None:
|
|
93
112
|
with self._lock:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
113
|
+
for sessions in self._sessions_by_agent.values():
|
|
114
|
+
dead_sessions = []
|
|
115
|
+
for sid, session in sessions.items():
|
|
116
|
+
if not session.is_alive():
|
|
117
|
+
dead_sessions.append(sid)
|
|
98
118
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
119
|
+
for sid in dead_sessions:
|
|
120
|
+
session = sessions.pop(sid)
|
|
121
|
+
with contextlib.suppress(Exception):
|
|
122
|
+
session.close()
|
|
103
123
|
|
|
104
124
|
def close_all_sessions(self) -> None:
|
|
105
125
|
with self._lock:
|
|
106
|
-
|
|
107
|
-
self.
|
|
126
|
+
all_sessions: list[PythonInstance] = []
|
|
127
|
+
for sessions in self._sessions_by_agent.values():
|
|
128
|
+
all_sessions.extend(sessions.values())
|
|
129
|
+
self._sessions_by_agent.clear()
|
|
108
130
|
|
|
109
|
-
for session in
|
|
131
|
+
for session in all_sessions:
|
|
110
132
|
with contextlib.suppress(Exception):
|
|
111
133
|
session.close()
|
|
112
134
|
|
|
113
135
|
def _register_cleanup_handlers(self) -> None:
|
|
114
136
|
atexit.register(self.close_all_sessions)
|
|
115
137
|
|
|
116
|
-
signal.signal(signal.SIGTERM, self._signal_handler)
|
|
117
|
-
signal.signal(signal.SIGINT, self._signal_handler)
|
|
118
|
-
|
|
119
|
-
if hasattr(signal, "SIGHUP"):
|
|
120
|
-
signal.signal(signal.SIGHUP, self._signal_handler)
|
|
121
|
-
|
|
122
|
-
def _signal_handler(self, _signum: int, _frame: Any) -> None:
|
|
123
|
-
self.close_all_sessions()
|
|
124
|
-
sys.exit(0)
|
|
125
|
-
|
|
126
138
|
|
|
127
139
|
_python_session_manager = PythonSessionManager()
|
|
128
140
|
|