strix-agent 0.4.0__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/__init__.py +0 -0
- strix/agents/StrixAgent/__init__.py +4 -0
- strix/agents/StrixAgent/strix_agent.py +89 -0
- strix/agents/StrixAgent/system_prompt.jinja +404 -0
- strix/agents/__init__.py +10 -0
- strix/agents/base_agent.py +518 -0
- strix/agents/state.py +163 -0
- strix/interface/__init__.py +4 -0
- strix/interface/assets/tui_styles.tcss +694 -0
- strix/interface/cli.py +230 -0
- strix/interface/main.py +500 -0
- strix/interface/tool_components/__init__.py +39 -0
- strix/interface/tool_components/agents_graph_renderer.py +123 -0
- strix/interface/tool_components/base_renderer.py +62 -0
- strix/interface/tool_components/browser_renderer.py +120 -0
- strix/interface/tool_components/file_edit_renderer.py +99 -0
- strix/interface/tool_components/finish_renderer.py +31 -0
- strix/interface/tool_components/notes_renderer.py +108 -0
- strix/interface/tool_components/proxy_renderer.py +255 -0
- strix/interface/tool_components/python_renderer.py +34 -0
- strix/interface/tool_components/registry.py +72 -0
- strix/interface/tool_components/reporting_renderer.py +53 -0
- strix/interface/tool_components/scan_info_renderer.py +64 -0
- strix/interface/tool_components/terminal_renderer.py +131 -0
- strix/interface/tool_components/thinking_renderer.py +29 -0
- strix/interface/tool_components/user_message_renderer.py +43 -0
- strix/interface/tool_components/web_search_renderer.py +28 -0
- strix/interface/tui.py +1274 -0
- strix/interface/utils.py +559 -0
- strix/llm/__init__.py +15 -0
- strix/llm/config.py +20 -0
- strix/llm/llm.py +465 -0
- strix/llm/memory_compressor.py +212 -0
- strix/llm/request_queue.py +87 -0
- strix/llm/utils.py +87 -0
- strix/prompts/README.md +64 -0
- strix/prompts/__init__.py +109 -0
- strix/prompts/cloud/.gitkeep +0 -0
- strix/prompts/coordination/root_agent.jinja +41 -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 +147 -0
- strix/prompts/vulnerabilities/broken_function_level_authorization.jinja +146 -0
- strix/prompts/vulnerabilities/business_logic.jinja +171 -0
- strix/prompts/vulnerabilities/csrf.jinja +174 -0
- strix/prompts/vulnerabilities/idor.jinja +195 -0
- strix/prompts/vulnerabilities/information_disclosure.jinja +222 -0
- strix/prompts/vulnerabilities/insecure_file_uploads.jinja +188 -0
- strix/prompts/vulnerabilities/mass_assignment.jinja +141 -0
- strix/prompts/vulnerabilities/open_redirect.jinja +177 -0
- strix/prompts/vulnerabilities/path_traversal_lfi_rfi.jinja +142 -0
- strix/prompts/vulnerabilities/race_conditions.jinja +164 -0
- strix/prompts/vulnerabilities/rce.jinja +154 -0
- strix/prompts/vulnerabilities/sql_injection.jinja +151 -0
- strix/prompts/vulnerabilities/ssrf.jinja +135 -0
- strix/prompts/vulnerabilities/subdomain_takeover.jinja +155 -0
- strix/prompts/vulnerabilities/xss.jinja +169 -0
- strix/prompts/vulnerabilities/xxe.jinja +184 -0
- strix/runtime/__init__.py +19 -0
- strix/runtime/docker_runtime.py +399 -0
- strix/runtime/runtime.py +29 -0
- strix/runtime/tool_server.py +205 -0
- strix/telemetry/__init__.py +4 -0
- strix/telemetry/tracer.py +337 -0
- strix/tools/__init__.py +64 -0
- strix/tools/agents_graph/__init__.py +16 -0
- strix/tools/agents_graph/agents_graph_actions.py +621 -0
- strix/tools/agents_graph/agents_graph_actions_schema.xml +226 -0
- strix/tools/argument_parser.py +121 -0
- strix/tools/browser/__init__.py +4 -0
- strix/tools/browser/browser_actions.py +236 -0
- strix/tools/browser/browser_actions_schema.xml +183 -0
- strix/tools/browser/browser_instance.py +533 -0
- strix/tools/browser/tab_manager.py +342 -0
- strix/tools/executor.py +305 -0
- strix/tools/file_edit/__init__.py +4 -0
- strix/tools/file_edit/file_edit_actions.py +141 -0
- strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
- strix/tools/finish/__init__.py +4 -0
- strix/tools/finish/finish_actions.py +174 -0
- strix/tools/finish/finish_actions_schema.xml +45 -0
- strix/tools/notes/__init__.py +14 -0
- strix/tools/notes/notes_actions.py +191 -0
- strix/tools/notes/notes_actions_schema.xml +150 -0
- strix/tools/proxy/__init__.py +20 -0
- strix/tools/proxy/proxy_actions.py +101 -0
- strix/tools/proxy/proxy_actions_schema.xml +267 -0
- strix/tools/proxy/proxy_manager.py +785 -0
- strix/tools/python/__init__.py +4 -0
- strix/tools/python/python_actions.py +47 -0
- strix/tools/python/python_actions_schema.xml +131 -0
- strix/tools/python/python_instance.py +172 -0
- strix/tools/python/python_manager.py +131 -0
- strix/tools/registry.py +196 -0
- strix/tools/reporting/__init__.py +6 -0
- strix/tools/reporting/reporting_actions.py +63 -0
- strix/tools/reporting/reporting_actions_schema.xml +30 -0
- strix/tools/terminal/__init__.py +4 -0
- strix/tools/terminal/terminal_actions.py +35 -0
- strix/tools/terminal/terminal_actions_schema.xml +146 -0
- strix/tools/terminal/terminal_manager.py +151 -0
- strix/tools/terminal/terminal_session.py +447 -0
- strix/tools/thinking/__init__.py +4 -0
- strix/tools/thinking/thinking_actions.py +18 -0
- strix/tools/thinking/thinking_actions_schema.xml +52 -0
- strix/tools/web_search/__init__.py +4 -0
- strix/tools/web_search/web_search_actions.py +80 -0
- strix/tools/web_search/web_search_actions_schema.xml +83 -0
- strix_agent-0.4.0.dist-info/LICENSE +201 -0
- strix_agent-0.4.0.dist-info/METADATA +282 -0
- strix_agent-0.4.0.dist-info/RECORD +118 -0
- strix_agent-0.4.0.dist-info/WHEEL +4 -0
- strix_agent-0.4.0.dist-info/entry_points.txt +3 -0
strix/interface/main.py
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Strix Agent Interface
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import litellm
|
|
15
|
+
from docker.errors import DockerException
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
|
|
20
|
+
from strix.interface.cli import run_cli
|
|
21
|
+
from strix.interface.tui import run_tui
|
|
22
|
+
from strix.interface.utils import (
|
|
23
|
+
assign_workspace_subdirs,
|
|
24
|
+
build_final_stats_text,
|
|
25
|
+
check_docker_connection,
|
|
26
|
+
clone_repository,
|
|
27
|
+
collect_local_sources,
|
|
28
|
+
generate_run_name,
|
|
29
|
+
image_exists,
|
|
30
|
+
infer_target_type,
|
|
31
|
+
process_pull_line,
|
|
32
|
+
validate_llm_response,
|
|
33
|
+
)
|
|
34
|
+
from strix.runtime.docker_runtime import STRIX_IMAGE
|
|
35
|
+
from strix.telemetry.tracer import get_global_tracer
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
logging.getLogger().setLevel(logging.ERROR)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|
42
|
+
console = Console()
|
|
43
|
+
missing_required_vars = []
|
|
44
|
+
missing_optional_vars = []
|
|
45
|
+
|
|
46
|
+
if not os.getenv("STRIX_LLM"):
|
|
47
|
+
missing_required_vars.append("STRIX_LLM")
|
|
48
|
+
|
|
49
|
+
has_base_url = any(
|
|
50
|
+
[
|
|
51
|
+
os.getenv("LLM_API_BASE"),
|
|
52
|
+
os.getenv("OPENAI_API_BASE"),
|
|
53
|
+
os.getenv("LITELLM_BASE_URL"),
|
|
54
|
+
os.getenv("OLLAMA_API_BASE"),
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if not os.getenv("LLM_API_KEY"):
|
|
59
|
+
if not has_base_url:
|
|
60
|
+
missing_required_vars.append("LLM_API_KEY")
|
|
61
|
+
else:
|
|
62
|
+
missing_optional_vars.append("LLM_API_KEY")
|
|
63
|
+
|
|
64
|
+
if not has_base_url:
|
|
65
|
+
missing_optional_vars.append("LLM_API_BASE")
|
|
66
|
+
|
|
67
|
+
if not os.getenv("PERPLEXITY_API_KEY"):
|
|
68
|
+
missing_optional_vars.append("PERPLEXITY_API_KEY")
|
|
69
|
+
|
|
70
|
+
if missing_required_vars:
|
|
71
|
+
error_text = Text()
|
|
72
|
+
error_text.append("β ", style="bold red")
|
|
73
|
+
error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red")
|
|
74
|
+
error_text.append("\n\n", style="white")
|
|
75
|
+
|
|
76
|
+
for var in missing_required_vars:
|
|
77
|
+
error_text.append(f"β’ {var}", style="bold yellow")
|
|
78
|
+
error_text.append(" is not set\n", style="white")
|
|
79
|
+
|
|
80
|
+
if missing_optional_vars:
|
|
81
|
+
error_text.append("\nOptional environment variables:\n", style="dim white")
|
|
82
|
+
for var in missing_optional_vars:
|
|
83
|
+
error_text.append(f"β’ {var}", style="dim yellow")
|
|
84
|
+
error_text.append(" is not set\n", style="dim white")
|
|
85
|
+
|
|
86
|
+
error_text.append("\nRequired environment variables:\n", style="white")
|
|
87
|
+
for var in missing_required_vars:
|
|
88
|
+
if var == "STRIX_LLM":
|
|
89
|
+
error_text.append("β’ ", style="white")
|
|
90
|
+
error_text.append("STRIX_LLM", style="bold cyan")
|
|
91
|
+
error_text.append(
|
|
92
|
+
" - Model name to use with litellm (e.g., 'openai/gpt-5')\n",
|
|
93
|
+
style="white",
|
|
94
|
+
)
|
|
95
|
+
elif var == "LLM_API_KEY":
|
|
96
|
+
error_text.append("β’ ", style="white")
|
|
97
|
+
error_text.append("LLM_API_KEY", style="bold cyan")
|
|
98
|
+
error_text.append(
|
|
99
|
+
" - API key for the LLM provider (required for cloud providers)\n",
|
|
100
|
+
style="white",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if missing_optional_vars:
|
|
104
|
+
error_text.append("\nOptional environment variables:\n", style="white")
|
|
105
|
+
for var in missing_optional_vars:
|
|
106
|
+
if var == "LLM_API_KEY":
|
|
107
|
+
error_text.append("β’ ", style="white")
|
|
108
|
+
error_text.append("LLM_API_KEY", style="bold cyan")
|
|
109
|
+
error_text.append(" - API key for the LLM provider\n", style="white")
|
|
110
|
+
elif var == "LLM_API_BASE":
|
|
111
|
+
error_text.append("β’ ", style="white")
|
|
112
|
+
error_text.append("LLM_API_BASE", style="bold cyan")
|
|
113
|
+
error_text.append(
|
|
114
|
+
" - Custom API base URL if using local models (e.g., Ollama, LMStudio)\n",
|
|
115
|
+
style="white",
|
|
116
|
+
)
|
|
117
|
+
elif var == "PERPLEXITY_API_KEY":
|
|
118
|
+
error_text.append("β’ ", style="white")
|
|
119
|
+
error_text.append("PERPLEXITY_API_KEY", style="bold cyan")
|
|
120
|
+
error_text.append(
|
|
121
|
+
" - API key for Perplexity AI web search (enables real-time research)\n",
|
|
122
|
+
style="white",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
error_text.append("\nExample setup:\n", style="white")
|
|
126
|
+
error_text.append("export STRIX_LLM='openai/gpt-5'\n", style="dim white")
|
|
127
|
+
|
|
128
|
+
if "LLM_API_KEY" in missing_required_vars:
|
|
129
|
+
error_text.append("export LLM_API_KEY='your-api-key-here'\n", style="dim white")
|
|
130
|
+
|
|
131
|
+
if missing_optional_vars:
|
|
132
|
+
for var in missing_optional_vars:
|
|
133
|
+
if var == "LLM_API_KEY":
|
|
134
|
+
error_text.append(
|
|
135
|
+
"export LLM_API_KEY='your-api-key-here' # optional with local models\n",
|
|
136
|
+
style="dim white",
|
|
137
|
+
)
|
|
138
|
+
elif var == "LLM_API_BASE":
|
|
139
|
+
error_text.append(
|
|
140
|
+
"export LLM_API_BASE='http://localhost:11434' "
|
|
141
|
+
"# needed for local models only\n",
|
|
142
|
+
style="dim white",
|
|
143
|
+
)
|
|
144
|
+
elif var == "PERPLEXITY_API_KEY":
|
|
145
|
+
error_text.append(
|
|
146
|
+
"export PERPLEXITY_API_KEY='your-perplexity-key-here'\n", style="dim white"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
panel = Panel(
|
|
150
|
+
error_text,
|
|
151
|
+
title="[bold red]π‘οΈ STRIX CONFIGURATION ERROR",
|
|
152
|
+
title_align="center",
|
|
153
|
+
border_style="red",
|
|
154
|
+
padding=(1, 2),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
console.print("\n")
|
|
158
|
+
console.print(panel)
|
|
159
|
+
console.print()
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def check_docker_installed() -> None:
|
|
164
|
+
if shutil.which("docker") is None:
|
|
165
|
+
console = Console()
|
|
166
|
+
error_text = Text()
|
|
167
|
+
error_text.append("β ", style="bold red")
|
|
168
|
+
error_text.append("DOCKER NOT INSTALLED", style="bold red")
|
|
169
|
+
error_text.append("\n\n", style="white")
|
|
170
|
+
error_text.append("The 'docker' CLI was not found in your PATH.\n", style="white")
|
|
171
|
+
error_text.append(
|
|
172
|
+
"Please install Docker and ensure the 'docker' command is available.\n\n", style="white"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
panel = Panel(
|
|
176
|
+
error_text,
|
|
177
|
+
title="[bold red]π‘οΈ STRIX STARTUP ERROR",
|
|
178
|
+
title_align="center",
|
|
179
|
+
border_style="red",
|
|
180
|
+
padding=(1, 2),
|
|
181
|
+
)
|
|
182
|
+
console.print("\n", panel, "\n")
|
|
183
|
+
sys.exit(1)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
async def warm_up_llm() -> None:
|
|
187
|
+
console = Console()
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
model_name = os.getenv("STRIX_LLM", "openai/gpt-5")
|
|
191
|
+
api_key = os.getenv("LLM_API_KEY")
|
|
192
|
+
|
|
193
|
+
if api_key:
|
|
194
|
+
litellm.api_key = api_key
|
|
195
|
+
|
|
196
|
+
api_base = (
|
|
197
|
+
os.getenv("LLM_API_BASE")
|
|
198
|
+
or os.getenv("OPENAI_API_BASE")
|
|
199
|
+
or os.getenv("LITELLM_BASE_URL")
|
|
200
|
+
or os.getenv("OLLAMA_API_BASE")
|
|
201
|
+
)
|
|
202
|
+
if api_base:
|
|
203
|
+
litellm.api_base = api_base
|
|
204
|
+
|
|
205
|
+
test_messages = [
|
|
206
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
|
207
|
+
{"role": "user", "content": "Reply with just 'OK'."},
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
llm_timeout = int(os.getenv("LLM_TIMEOUT", "600"))
|
|
211
|
+
|
|
212
|
+
response = litellm.completion(
|
|
213
|
+
model=model_name,
|
|
214
|
+
messages=test_messages,
|
|
215
|
+
timeout=llm_timeout,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
validate_llm_response(response)
|
|
219
|
+
|
|
220
|
+
except Exception as e: # noqa: BLE001
|
|
221
|
+
error_text = Text()
|
|
222
|
+
error_text.append("β ", style="bold red")
|
|
223
|
+
error_text.append("LLM CONNECTION FAILED", style="bold red")
|
|
224
|
+
error_text.append("\n\n", style="white")
|
|
225
|
+
error_text.append("Could not establish connection to the language model.\n", style="white")
|
|
226
|
+
error_text.append("Please check your configuration and try again.\n", style="white")
|
|
227
|
+
error_text.append(f"\nError: {e}", style="dim white")
|
|
228
|
+
|
|
229
|
+
panel = Panel(
|
|
230
|
+
error_text,
|
|
231
|
+
title="[bold red]π‘οΈ STRIX STARTUP ERROR",
|
|
232
|
+
title_align="center",
|
|
233
|
+
border_style="red",
|
|
234
|
+
padding=(1, 2),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
console.print("\n")
|
|
238
|
+
console.print(panel)
|
|
239
|
+
console.print()
|
|
240
|
+
sys.exit(1)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def parse_arguments() -> argparse.Namespace:
|
|
244
|
+
parser = argparse.ArgumentParser(
|
|
245
|
+
description="Strix Multi-Agent Cybersecurity Penetration Testing Tool",
|
|
246
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
247
|
+
epilog="""
|
|
248
|
+
Examples:
|
|
249
|
+
# Web application penetration test
|
|
250
|
+
strix --target https://example.com
|
|
251
|
+
|
|
252
|
+
# GitHub repository analysis
|
|
253
|
+
strix --target https://github.com/user/repo
|
|
254
|
+
strix --target git@github.com:user/repo.git
|
|
255
|
+
|
|
256
|
+
# Local code analysis
|
|
257
|
+
strix --target ./my-project
|
|
258
|
+
|
|
259
|
+
# Domain penetration test
|
|
260
|
+
strix --target example.com
|
|
261
|
+
|
|
262
|
+
# IP address penetration test
|
|
263
|
+
strix --target 192.168.1.42
|
|
264
|
+
|
|
265
|
+
# Multiple targets (e.g., white-box testing with source and deployed app)
|
|
266
|
+
strix --target https://github.com/user/repo --target https://example.com
|
|
267
|
+
strix --target ./my-project --target https://staging.example.com --target https://prod.example.com
|
|
268
|
+
|
|
269
|
+
# Custom instructions (inline)
|
|
270
|
+
strix --target example.com --instruction "Focus on authentication vulnerabilities"
|
|
271
|
+
|
|
272
|
+
# Custom instructions (from file)
|
|
273
|
+
strix --target example.com --instruction ./instructions.txt
|
|
274
|
+
strix --target https://app.com --instruction /path/to/detailed_instructions.md
|
|
275
|
+
""",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
parser.add_argument(
|
|
279
|
+
"-t",
|
|
280
|
+
"--target",
|
|
281
|
+
type=str,
|
|
282
|
+
required=True,
|
|
283
|
+
action="append",
|
|
284
|
+
help="Target to test (URL, repository, local directory path, domain name, or IP address). "
|
|
285
|
+
"Can be specified multiple times for multi-target scans.",
|
|
286
|
+
)
|
|
287
|
+
parser.add_argument(
|
|
288
|
+
"--instruction",
|
|
289
|
+
type=str,
|
|
290
|
+
help="Custom instructions for the penetration test. This can be "
|
|
291
|
+
"specific vulnerability types to focus on (e.g., 'Focus on IDOR and XSS'), "
|
|
292
|
+
"testing approaches (e.g., 'Perform thorough authentication testing'), "
|
|
293
|
+
"test credentials (e.g., 'Use the following credentials to access the app: "
|
|
294
|
+
"admin:password123'), "
|
|
295
|
+
"or areas of interest (e.g., 'Check login API endpoint for security issues'). "
|
|
296
|
+
"You can also provide a path to a file containing detailed instructions "
|
|
297
|
+
"(e.g., '--instruction ./instructions.txt').",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
parser.add_argument(
|
|
301
|
+
"--run-name",
|
|
302
|
+
type=str,
|
|
303
|
+
help="Custom name for this penetration test run",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
parser.add_argument(
|
|
307
|
+
"-n",
|
|
308
|
+
"--non-interactive",
|
|
309
|
+
action="store_true",
|
|
310
|
+
help=(
|
|
311
|
+
"Run in non-interactive mode (no TUI, exits on completion). "
|
|
312
|
+
"Default is interactive mode with TUI."
|
|
313
|
+
),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
args = parser.parse_args()
|
|
317
|
+
|
|
318
|
+
if args.instruction:
|
|
319
|
+
instruction_path = Path(args.instruction)
|
|
320
|
+
if instruction_path.exists() and instruction_path.is_file():
|
|
321
|
+
try:
|
|
322
|
+
with instruction_path.open(encoding="utf-8") as f:
|
|
323
|
+
args.instruction = f.read().strip()
|
|
324
|
+
if not args.instruction:
|
|
325
|
+
parser.error(f"Instruction file '{instruction_path}' is empty")
|
|
326
|
+
except Exception as e: # noqa: BLE001
|
|
327
|
+
parser.error(f"Failed to read instruction file '{instruction_path}': {e}")
|
|
328
|
+
|
|
329
|
+
args.targets_info = []
|
|
330
|
+
for target in args.target:
|
|
331
|
+
try:
|
|
332
|
+
target_type, target_dict = infer_target_type(target)
|
|
333
|
+
|
|
334
|
+
if target_type == "local_code":
|
|
335
|
+
display_target = target_dict.get("target_path", target)
|
|
336
|
+
else:
|
|
337
|
+
display_target = target
|
|
338
|
+
|
|
339
|
+
args.targets_info.append(
|
|
340
|
+
{"type": target_type, "details": target_dict, "original": display_target}
|
|
341
|
+
)
|
|
342
|
+
except ValueError:
|
|
343
|
+
parser.error(f"Invalid target '{target}'")
|
|
344
|
+
|
|
345
|
+
assign_workspace_subdirs(args.targets_info)
|
|
346
|
+
|
|
347
|
+
return args
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def display_completion_message(args: argparse.Namespace, results_path: Path) -> None:
|
|
351
|
+
console = Console()
|
|
352
|
+
tracer = get_global_tracer()
|
|
353
|
+
|
|
354
|
+
scan_completed = False
|
|
355
|
+
if tracer and tracer.scan_results:
|
|
356
|
+
scan_completed = tracer.scan_results.get("scan_completed", False)
|
|
357
|
+
|
|
358
|
+
has_vulnerabilities = tracer and len(tracer.vulnerability_reports) > 0
|
|
359
|
+
|
|
360
|
+
completion_text = Text()
|
|
361
|
+
if scan_completed:
|
|
362
|
+
completion_text.append("π¦ ", style="bold white")
|
|
363
|
+
completion_text.append("AGENT FINISHED", style="bold green")
|
|
364
|
+
completion_text.append(" β’ ", style="dim white")
|
|
365
|
+
completion_text.append("Penetration test completed", style="white")
|
|
366
|
+
else:
|
|
367
|
+
completion_text.append("π¦ ", style="bold white")
|
|
368
|
+
completion_text.append("SESSION ENDED", style="bold yellow")
|
|
369
|
+
completion_text.append(" β’ ", style="dim white")
|
|
370
|
+
completion_text.append("Penetration test interrupted by user", style="white")
|
|
371
|
+
|
|
372
|
+
stats_text = build_final_stats_text(tracer)
|
|
373
|
+
|
|
374
|
+
target_text = Text()
|
|
375
|
+
if len(args.targets_info) == 1:
|
|
376
|
+
target_text.append("π― Target: ", style="bold cyan")
|
|
377
|
+
target_text.append(args.targets_info[0]["original"], style="bold white")
|
|
378
|
+
else:
|
|
379
|
+
target_text.append("π― Targets: ", style="bold cyan")
|
|
380
|
+
target_text.append(f"{len(args.targets_info)} targets\n", style="bold white")
|
|
381
|
+
for i, target_info in enumerate(args.targets_info):
|
|
382
|
+
target_text.append(" β’ ", style="dim white")
|
|
383
|
+
target_text.append(target_info["original"], style="white")
|
|
384
|
+
if i < len(args.targets_info) - 1:
|
|
385
|
+
target_text.append("\n")
|
|
386
|
+
|
|
387
|
+
panel_parts = [completion_text, "\n\n", target_text]
|
|
388
|
+
|
|
389
|
+
if stats_text.plain:
|
|
390
|
+
panel_parts.extend(["\n", stats_text])
|
|
391
|
+
|
|
392
|
+
if scan_completed or has_vulnerabilities:
|
|
393
|
+
results_text = Text()
|
|
394
|
+
results_text.append("π Results Saved To: ", style="bold cyan")
|
|
395
|
+
results_text.append(str(results_path), style="bold yellow")
|
|
396
|
+
panel_parts.extend(["\n\n", results_text])
|
|
397
|
+
|
|
398
|
+
panel_content = Text.assemble(*panel_parts)
|
|
399
|
+
|
|
400
|
+
border_style = "green" if scan_completed else "yellow"
|
|
401
|
+
|
|
402
|
+
panel = Panel(
|
|
403
|
+
panel_content,
|
|
404
|
+
title="[bold green]π‘οΈ STRIX CYBERSECURITY AGENT",
|
|
405
|
+
title_align="center",
|
|
406
|
+
border_style=border_style,
|
|
407
|
+
padding=(1, 2),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
console.print("\n")
|
|
411
|
+
console.print(panel)
|
|
412
|
+
console.print()
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def pull_docker_image() -> None:
|
|
416
|
+
console = Console()
|
|
417
|
+
client = check_docker_connection()
|
|
418
|
+
|
|
419
|
+
if image_exists(client, STRIX_IMAGE):
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
console.print()
|
|
423
|
+
console.print(f"[bold cyan]π³ Pulling Docker image:[/] {STRIX_IMAGE}")
|
|
424
|
+
console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]")
|
|
425
|
+
console.print()
|
|
426
|
+
|
|
427
|
+
with console.status("[bold cyan]Downloading image layers...", spinner="dots") as status:
|
|
428
|
+
try:
|
|
429
|
+
layers_info: dict[str, str] = {}
|
|
430
|
+
last_update = ""
|
|
431
|
+
|
|
432
|
+
for line in client.api.pull(STRIX_IMAGE, stream=True, decode=True):
|
|
433
|
+
last_update = process_pull_line(line, layers_info, status, last_update)
|
|
434
|
+
|
|
435
|
+
except DockerException as e:
|
|
436
|
+
console.print()
|
|
437
|
+
error_text = Text()
|
|
438
|
+
error_text.append("β ", style="bold red")
|
|
439
|
+
error_text.append("FAILED TO PULL IMAGE", style="bold red")
|
|
440
|
+
error_text.append("\n\n", style="white")
|
|
441
|
+
error_text.append(f"Could not download: {STRIX_IMAGE}\n", style="white")
|
|
442
|
+
error_text.append(str(e), style="dim red")
|
|
443
|
+
|
|
444
|
+
panel = Panel(
|
|
445
|
+
error_text,
|
|
446
|
+
title="[bold red]π‘οΈ DOCKER PULL ERROR",
|
|
447
|
+
title_align="center",
|
|
448
|
+
border_style="red",
|
|
449
|
+
padding=(1, 2),
|
|
450
|
+
)
|
|
451
|
+
console.print(panel, "\n")
|
|
452
|
+
sys.exit(1)
|
|
453
|
+
|
|
454
|
+
success_text = Text()
|
|
455
|
+
success_text.append("β
", style="bold green")
|
|
456
|
+
success_text.append("Successfully pulled Docker image", style="green")
|
|
457
|
+
console.print(success_text)
|
|
458
|
+
console.print()
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def main() -> None:
|
|
462
|
+
if sys.platform == "win32":
|
|
463
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
464
|
+
|
|
465
|
+
args = parse_arguments()
|
|
466
|
+
|
|
467
|
+
check_docker_installed()
|
|
468
|
+
pull_docker_image()
|
|
469
|
+
|
|
470
|
+
validate_environment()
|
|
471
|
+
asyncio.run(warm_up_llm())
|
|
472
|
+
|
|
473
|
+
if not args.run_name:
|
|
474
|
+
args.run_name = generate_run_name(args.targets_info)
|
|
475
|
+
|
|
476
|
+
for target_info in args.targets_info:
|
|
477
|
+
if target_info["type"] == "repository":
|
|
478
|
+
repo_url = target_info["details"]["target_repo"]
|
|
479
|
+
dest_name = target_info["details"].get("workspace_subdir")
|
|
480
|
+
cloned_path = clone_repository(repo_url, args.run_name, dest_name)
|
|
481
|
+
target_info["details"]["cloned_repo_path"] = cloned_path
|
|
482
|
+
|
|
483
|
+
args.local_sources = collect_local_sources(args.targets_info)
|
|
484
|
+
|
|
485
|
+
if args.non_interactive:
|
|
486
|
+
asyncio.run(run_cli(args))
|
|
487
|
+
else:
|
|
488
|
+
asyncio.run(run_tui(args))
|
|
489
|
+
|
|
490
|
+
results_path = Path("strix_runs") / args.run_name
|
|
491
|
+
display_completion_message(args, results_path)
|
|
492
|
+
|
|
493
|
+
if args.non_interactive:
|
|
494
|
+
tracer = get_global_tracer()
|
|
495
|
+
if tracer and tracer.vulnerability_reports:
|
|
496
|
+
sys.exit(2)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
if __name__ == "__main__":
|
|
500
|
+
main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from . import (
|
|
2
|
+
agents_graph_renderer,
|
|
3
|
+
browser_renderer,
|
|
4
|
+
file_edit_renderer,
|
|
5
|
+
finish_renderer,
|
|
6
|
+
notes_renderer,
|
|
7
|
+
proxy_renderer,
|
|
8
|
+
python_renderer,
|
|
9
|
+
reporting_renderer,
|
|
10
|
+
scan_info_renderer,
|
|
11
|
+
terminal_renderer,
|
|
12
|
+
thinking_renderer,
|
|
13
|
+
user_message_renderer,
|
|
14
|
+
web_search_renderer,
|
|
15
|
+
)
|
|
16
|
+
from .base_renderer import BaseToolRenderer
|
|
17
|
+
from .registry import ToolTUIRegistry, get_tool_renderer, register_tool_renderer, render_tool_widget
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"BaseToolRenderer",
|
|
22
|
+
"ToolTUIRegistry",
|
|
23
|
+
"agents_graph_renderer",
|
|
24
|
+
"browser_renderer",
|
|
25
|
+
"file_edit_renderer",
|
|
26
|
+
"finish_renderer",
|
|
27
|
+
"get_tool_renderer",
|
|
28
|
+
"notes_renderer",
|
|
29
|
+
"proxy_renderer",
|
|
30
|
+
"python_renderer",
|
|
31
|
+
"register_tool_renderer",
|
|
32
|
+
"render_tool_widget",
|
|
33
|
+
"reporting_renderer",
|
|
34
|
+
"scan_info_renderer",
|
|
35
|
+
"terminal_renderer",
|
|
36
|
+
"thinking_renderer",
|
|
37
|
+
"user_message_renderer",
|
|
38
|
+
"web_search_renderer",
|
|
39
|
+
]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from typing import Any, ClassVar
|
|
2
|
+
|
|
3
|
+
from textual.widgets import Static
|
|
4
|
+
|
|
5
|
+
from .base_renderer import BaseToolRenderer
|
|
6
|
+
from .registry import register_tool_renderer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register_tool_renderer
|
|
10
|
+
class ViewAgentGraphRenderer(BaseToolRenderer):
|
|
11
|
+
tool_name: ClassVar[str] = "view_agent_graph"
|
|
12
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
|
|
16
|
+
content_text = "πΈοΈ [bold #fbbf24]Viewing agents graph[/]"
|
|
17
|
+
|
|
18
|
+
css_classes = cls.get_css_classes("completed")
|
|
19
|
+
return Static(content_text, classes=css_classes)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@register_tool_renderer
|
|
23
|
+
class CreateAgentRenderer(BaseToolRenderer):
|
|
24
|
+
tool_name: ClassVar[str] = "create_agent"
|
|
25
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
29
|
+
args = tool_data.get("args", {})
|
|
30
|
+
|
|
31
|
+
task = args.get("task", "")
|
|
32
|
+
name = args.get("name", "Agent")
|
|
33
|
+
|
|
34
|
+
header = f"π€ [bold #fbbf24]Creating {cls.escape_markup(name)}[/]"
|
|
35
|
+
|
|
36
|
+
if task:
|
|
37
|
+
task_display = task[:400] + "..." if len(task) > 400 else task
|
|
38
|
+
content_text = f"{header}\n [dim]{cls.escape_markup(task_display)}[/]"
|
|
39
|
+
else:
|
|
40
|
+
content_text = f"{header}\n [dim]Spawning agent...[/]"
|
|
41
|
+
|
|
42
|
+
css_classes = cls.get_css_classes("completed")
|
|
43
|
+
return Static(content_text, classes=css_classes)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@register_tool_renderer
|
|
47
|
+
class SendMessageToAgentRenderer(BaseToolRenderer):
|
|
48
|
+
tool_name: ClassVar[str] = "send_message_to_agent"
|
|
49
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
53
|
+
args = tool_data.get("args", {})
|
|
54
|
+
|
|
55
|
+
message = args.get("message", "")
|
|
56
|
+
|
|
57
|
+
header = "π¬ [bold #fbbf24]Sending message[/]"
|
|
58
|
+
|
|
59
|
+
if message:
|
|
60
|
+
message_display = message[:400] + "..." if len(message) > 400 else message
|
|
61
|
+
content_text = f"{header}\n [dim]{cls.escape_markup(message_display)}[/]"
|
|
62
|
+
else:
|
|
63
|
+
content_text = f"{header}\n [dim]Sending...[/]"
|
|
64
|
+
|
|
65
|
+
css_classes = cls.get_css_classes("completed")
|
|
66
|
+
return Static(content_text, classes=css_classes)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@register_tool_renderer
|
|
70
|
+
class AgentFinishRenderer(BaseToolRenderer):
|
|
71
|
+
tool_name: ClassVar[str] = "agent_finish"
|
|
72
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
76
|
+
args = tool_data.get("args", {})
|
|
77
|
+
|
|
78
|
+
result_summary = args.get("result_summary", "")
|
|
79
|
+
findings = args.get("findings", [])
|
|
80
|
+
success = args.get("success", True)
|
|
81
|
+
|
|
82
|
+
header = (
|
|
83
|
+
"π [bold #fbbf24]Agent completed[/]" if success else "π [bold #fbbf24]Agent failed[/]"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if result_summary:
|
|
87
|
+
content_parts = [f"{header}\n [bold]{cls.escape_markup(result_summary)}[/]"]
|
|
88
|
+
|
|
89
|
+
if findings and isinstance(findings, list):
|
|
90
|
+
finding_lines = [f"β’ {finding}" for finding in findings]
|
|
91
|
+
content_parts.append(
|
|
92
|
+
f" [dim]{chr(10).join([cls.escape_markup(line) for line in finding_lines])}[/]"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
content_text = "\n".join(content_parts)
|
|
96
|
+
else:
|
|
97
|
+
content_text = f"{header}\n [dim]Completing task...[/]"
|
|
98
|
+
|
|
99
|
+
css_classes = cls.get_css_classes("completed")
|
|
100
|
+
return Static(content_text, classes=css_classes)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@register_tool_renderer
|
|
104
|
+
class WaitForMessageRenderer(BaseToolRenderer):
|
|
105
|
+
tool_name: ClassVar[str] = "wait_for_message"
|
|
106
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
110
|
+
args = tool_data.get("args", {})
|
|
111
|
+
|
|
112
|
+
reason = args.get("reason", "Waiting for messages from other agents or user input")
|
|
113
|
+
|
|
114
|
+
header = "βΈοΈ [bold #fbbf24]Waiting for messages[/]"
|
|
115
|
+
|
|
116
|
+
if reason:
|
|
117
|
+
reason_display = reason[:400] + "..." if len(reason) > 400 else reason
|
|
118
|
+
content_text = f"{header}\n [dim]{cls.escape_markup(reason_display)}[/]"
|
|
119
|
+
else:
|
|
120
|
+
content_text = f"{header}\n [dim]Agent paused until message received...[/]"
|
|
121
|
+
|
|
122
|
+
css_classes = cls.get_css_classes("completed")
|
|
123
|
+
return Static(content_text, classes=css_classes)
|