strix-agent 0.1.19__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 -40
- strix/agents/StrixAgent/system_prompt.jinja +15 -0
- strix/agents/base_agent.py +71 -11
- strix/agents/state.py +5 -1
- strix/interface/cli.py +171 -0
- strix/interface/main.py +482 -0
- strix/{cli → interface}/tool_components/scan_info_renderer.py +17 -12
- strix/{cli/app.py → interface/tui.py} +15 -16
- strix/interface/utils.py +435 -0
- 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 +13 -9
- strix/tools/executor.py +1 -1
- strix/tools/finish/finish_actions.py +1 -1
- strix/tools/reporting/reporting_actions.py +1 -1
- {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/METADATA +45 -4
- {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/RECORD +39 -36
- strix_agent-0.3.1.dist-info/entry_points.txt +3 -0
- strix/cli/main.py +0 -703
- strix_agent-0.1.19.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/base_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/reporting_renderer.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.19.dist-info → strix_agent-0.3.1.dist-info}/LICENSE +0 -0
- {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/WHEEL +0 -0
strix/cli/main.py
DELETED
|
@@ -1,703 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Strix Agent Command Line Interface
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import argparse
|
|
7
|
-
import asyncio
|
|
8
|
-
import logging
|
|
9
|
-
import os
|
|
10
|
-
import secrets
|
|
11
|
-
import shutil
|
|
12
|
-
import subprocess
|
|
13
|
-
import sys
|
|
14
|
-
import tempfile
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import Any
|
|
17
|
-
from urllib.parse import urlparse
|
|
18
|
-
|
|
19
|
-
import docker
|
|
20
|
-
import litellm
|
|
21
|
-
from docker.errors import DockerException
|
|
22
|
-
from rich.console import Console
|
|
23
|
-
from rich.panel import Panel
|
|
24
|
-
from rich.text import Text
|
|
25
|
-
|
|
26
|
-
from strix.cli.app import run_strix_cli
|
|
27
|
-
from strix.cli.tracer import get_global_tracer
|
|
28
|
-
from strix.runtime.docker_runtime import STRIX_IMAGE
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
logging.getLogger().setLevel(logging.ERROR)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def format_token_count(count: float) -> str:
|
|
35
|
-
count = int(count)
|
|
36
|
-
if count >= 1_000_000:
|
|
37
|
-
return f"{count / 1_000_000:.1f}M"
|
|
38
|
-
if count >= 1_000:
|
|
39
|
-
return f"{count / 1_000:.1f}K"
|
|
40
|
-
return str(count)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|
44
|
-
console = Console()
|
|
45
|
-
missing_required_vars = []
|
|
46
|
-
missing_optional_vars = []
|
|
47
|
-
|
|
48
|
-
if not os.getenv("STRIX_LLM"):
|
|
49
|
-
missing_required_vars.append("STRIX_LLM")
|
|
50
|
-
|
|
51
|
-
has_base_url = any(
|
|
52
|
-
[
|
|
53
|
-
os.getenv("LLM_API_BASE"),
|
|
54
|
-
os.getenv("OPENAI_API_BASE"),
|
|
55
|
-
os.getenv("LITELLM_BASE_URL"),
|
|
56
|
-
os.getenv("OLLAMA_API_BASE"),
|
|
57
|
-
]
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
if not os.getenv("LLM_API_KEY"):
|
|
61
|
-
if not has_base_url:
|
|
62
|
-
missing_required_vars.append("LLM_API_KEY")
|
|
63
|
-
else:
|
|
64
|
-
missing_optional_vars.append("LLM_API_KEY")
|
|
65
|
-
|
|
66
|
-
if not has_base_url:
|
|
67
|
-
missing_optional_vars.append("LLM_API_BASE")
|
|
68
|
-
|
|
69
|
-
if not os.getenv("PERPLEXITY_API_KEY"):
|
|
70
|
-
missing_optional_vars.append("PERPLEXITY_API_KEY")
|
|
71
|
-
|
|
72
|
-
if missing_required_vars:
|
|
73
|
-
error_text = Text()
|
|
74
|
-
error_text.append("❌ ", style="bold red")
|
|
75
|
-
error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red")
|
|
76
|
-
error_text.append("\n\n", style="white")
|
|
77
|
-
|
|
78
|
-
for var in missing_required_vars:
|
|
79
|
-
error_text.append(f"• {var}", style="bold yellow")
|
|
80
|
-
error_text.append(" is not set\n", style="white")
|
|
81
|
-
|
|
82
|
-
if missing_optional_vars:
|
|
83
|
-
error_text.append("\nOptional environment variables:\n", style="dim white")
|
|
84
|
-
for var in missing_optional_vars:
|
|
85
|
-
error_text.append(f"• {var}", style="dim yellow")
|
|
86
|
-
error_text.append(" is not set\n", style="dim white")
|
|
87
|
-
|
|
88
|
-
error_text.append("\nRequired environment variables:\n", style="white")
|
|
89
|
-
for var in missing_required_vars:
|
|
90
|
-
if var == "STRIX_LLM":
|
|
91
|
-
error_text.append("• ", style="white")
|
|
92
|
-
error_text.append("STRIX_LLM", style="bold cyan")
|
|
93
|
-
error_text.append(
|
|
94
|
-
" - Model name to use with litellm (e.g., 'openai/gpt-5')\n",
|
|
95
|
-
style="white",
|
|
96
|
-
)
|
|
97
|
-
elif var == "LLM_API_KEY":
|
|
98
|
-
error_text.append("• ", style="white")
|
|
99
|
-
error_text.append("LLM_API_KEY", style="bold cyan")
|
|
100
|
-
error_text.append(
|
|
101
|
-
" - API key for the LLM provider (required for cloud providers)\n",
|
|
102
|
-
style="white",
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
if missing_optional_vars:
|
|
106
|
-
error_text.append("\nOptional environment variables:\n", style="white")
|
|
107
|
-
for var in missing_optional_vars:
|
|
108
|
-
if var == "LLM_API_KEY":
|
|
109
|
-
error_text.append("• ", style="white")
|
|
110
|
-
error_text.append("LLM_API_KEY", style="bold cyan")
|
|
111
|
-
error_text.append(" - API key for the LLM provider\n", style="white")
|
|
112
|
-
elif var == "LLM_API_BASE":
|
|
113
|
-
error_text.append("• ", style="white")
|
|
114
|
-
error_text.append("LLM_API_BASE", style="bold cyan")
|
|
115
|
-
error_text.append(
|
|
116
|
-
" - Custom API base URL if using local models (e.g., Ollama, LMStudio)\n",
|
|
117
|
-
style="white",
|
|
118
|
-
)
|
|
119
|
-
elif var == "PERPLEXITY_API_KEY":
|
|
120
|
-
error_text.append("• ", style="white")
|
|
121
|
-
error_text.append("PERPLEXITY_API_KEY", style="bold cyan")
|
|
122
|
-
error_text.append(
|
|
123
|
-
" - API key for Perplexity AI web search (enables real-time research)\n",
|
|
124
|
-
style="white",
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
error_text.append("\nExample setup:\n", style="white")
|
|
128
|
-
error_text.append("export STRIX_LLM='openai/gpt-5'\n", style="dim white")
|
|
129
|
-
|
|
130
|
-
if "LLM_API_KEY" in missing_required_vars:
|
|
131
|
-
error_text.append("export LLM_API_KEY='your-api-key-here'\n", style="dim white")
|
|
132
|
-
|
|
133
|
-
if missing_optional_vars:
|
|
134
|
-
for var in missing_optional_vars:
|
|
135
|
-
if var == "LLM_API_KEY":
|
|
136
|
-
error_text.append(
|
|
137
|
-
"export LLM_API_KEY='your-api-key-here' # optional with local models\n",
|
|
138
|
-
style="dim white",
|
|
139
|
-
)
|
|
140
|
-
elif var == "LLM_API_BASE":
|
|
141
|
-
error_text.append(
|
|
142
|
-
"export LLM_API_BASE='http://localhost:11434' "
|
|
143
|
-
"# needed for local models only\n",
|
|
144
|
-
style="dim white",
|
|
145
|
-
)
|
|
146
|
-
elif var == "PERPLEXITY_API_KEY":
|
|
147
|
-
error_text.append(
|
|
148
|
-
"export PERPLEXITY_API_KEY='your-perplexity-key-here'\n", style="dim white"
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
panel = Panel(
|
|
152
|
-
error_text,
|
|
153
|
-
title="[bold red]🛡️ STRIX CONFIGURATION ERROR",
|
|
154
|
-
title_align="center",
|
|
155
|
-
border_style="red",
|
|
156
|
-
padding=(1, 2),
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
console.print("\n")
|
|
160
|
-
console.print(panel)
|
|
161
|
-
console.print()
|
|
162
|
-
sys.exit(1)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def _validate_llm_response(response: Any) -> None:
|
|
166
|
-
if not response or not response.choices or not response.choices[0].message.content:
|
|
167
|
-
raise RuntimeError("Invalid response from LLM")
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def check_docker_installed() -> None:
|
|
171
|
-
if shutil.which("docker") is None:
|
|
172
|
-
console = Console()
|
|
173
|
-
error_text = Text()
|
|
174
|
-
error_text.append("❌ ", style="bold red")
|
|
175
|
-
error_text.append("DOCKER NOT INSTALLED", style="bold red")
|
|
176
|
-
error_text.append("\n\n", style="white")
|
|
177
|
-
error_text.append("The 'docker' CLI was not found in your PATH.\n", style="white")
|
|
178
|
-
error_text.append(
|
|
179
|
-
"Please install Docker and ensure the 'docker' command is available.\n\n", style="white"
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
panel = Panel(
|
|
183
|
-
error_text,
|
|
184
|
-
title="[bold red]🛡️ STRIX STARTUP ERROR",
|
|
185
|
-
title_align="center",
|
|
186
|
-
border_style="red",
|
|
187
|
-
padding=(1, 2),
|
|
188
|
-
)
|
|
189
|
-
console.print("\n", panel, "\n")
|
|
190
|
-
sys.exit(1)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
async def warm_up_llm() -> None:
|
|
194
|
-
console = Console()
|
|
195
|
-
|
|
196
|
-
try:
|
|
197
|
-
model_name = os.getenv("STRIX_LLM", "openai/gpt-5")
|
|
198
|
-
api_key = os.getenv("LLM_API_KEY")
|
|
199
|
-
|
|
200
|
-
if api_key:
|
|
201
|
-
litellm.api_key = api_key
|
|
202
|
-
|
|
203
|
-
api_base = (
|
|
204
|
-
os.getenv("LLM_API_BASE")
|
|
205
|
-
or os.getenv("OPENAI_API_BASE")
|
|
206
|
-
or os.getenv("LITELLM_BASE_URL")
|
|
207
|
-
or os.getenv("OLLAMA_API_BASE")
|
|
208
|
-
)
|
|
209
|
-
if api_base:
|
|
210
|
-
litellm.api_base = api_base
|
|
211
|
-
|
|
212
|
-
test_messages = [
|
|
213
|
-
{"role": "system", "content": "You are a helpful assistant."},
|
|
214
|
-
{"role": "user", "content": "Reply with just 'OK'."},
|
|
215
|
-
]
|
|
216
|
-
|
|
217
|
-
response = litellm.completion(
|
|
218
|
-
model=model_name,
|
|
219
|
-
messages=test_messages,
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
_validate_llm_response(response)
|
|
223
|
-
|
|
224
|
-
except Exception as e: # noqa: BLE001
|
|
225
|
-
error_text = Text()
|
|
226
|
-
error_text.append("❌ ", style="bold red")
|
|
227
|
-
error_text.append("LLM CONNECTION FAILED", style="bold red")
|
|
228
|
-
error_text.append("\n\n", style="white")
|
|
229
|
-
error_text.append("Could not establish connection to the language model.\n", style="white")
|
|
230
|
-
error_text.append("Please check your configuration and try again.\n", style="white")
|
|
231
|
-
error_text.append(f"\nError: {e}", style="dim white")
|
|
232
|
-
|
|
233
|
-
panel = Panel(
|
|
234
|
-
error_text,
|
|
235
|
-
title="[bold red]🛡️ STRIX STARTUP ERROR",
|
|
236
|
-
title_align="center",
|
|
237
|
-
border_style="red",
|
|
238
|
-
padding=(1, 2),
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
console.print("\n")
|
|
242
|
-
console.print(panel)
|
|
243
|
-
console.print()
|
|
244
|
-
sys.exit(1)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
def generate_run_name() -> str:
|
|
248
|
-
# fmt: off
|
|
249
|
-
adjectives = [
|
|
250
|
-
"stealthy", "sneaky", "crafty", "elite", "phantom", "shadow", "silent",
|
|
251
|
-
"rogue", "covert", "ninja", "ghost", "cyber", "digital", "binary",
|
|
252
|
-
"encrypted", "obfuscated", "masked", "cloaked", "invisible", "anonymous"
|
|
253
|
-
]
|
|
254
|
-
nouns = [
|
|
255
|
-
"exploit", "payload", "backdoor", "rootkit", "keylogger", "botnet", "trojan",
|
|
256
|
-
"worm", "virus", "packet", "buffer", "shell", "daemon", "spider", "crawler",
|
|
257
|
-
"scanner", "sniffer", "honeypot", "firewall", "breach"
|
|
258
|
-
]
|
|
259
|
-
# fmt: on
|
|
260
|
-
adj = secrets.choice(adjectives)
|
|
261
|
-
noun = secrets.choice(nouns)
|
|
262
|
-
number = secrets.randbelow(900) + 100
|
|
263
|
-
return f"{adj}-{noun}-{number}"
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def clone_repository(repo_url: str, run_name: str) -> str:
|
|
267
|
-
console = Console()
|
|
268
|
-
|
|
269
|
-
git_executable = shutil.which("git")
|
|
270
|
-
if git_executable is None:
|
|
271
|
-
raise FileNotFoundError("Git executable not found in PATH")
|
|
272
|
-
|
|
273
|
-
temp_dir = Path(tempfile.gettempdir()) / "strix_repos" / run_name
|
|
274
|
-
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
275
|
-
|
|
276
|
-
repo_name = Path(repo_url).stem if repo_url.endswith(".git") else Path(repo_url).name
|
|
277
|
-
|
|
278
|
-
clone_path = temp_dir / repo_name
|
|
279
|
-
|
|
280
|
-
if clone_path.exists():
|
|
281
|
-
shutil.rmtree(clone_path)
|
|
282
|
-
|
|
283
|
-
try:
|
|
284
|
-
with console.status(f"[bold cyan]Cloning repository {repo_name}...", spinner="dots"):
|
|
285
|
-
subprocess.run( # noqa: S603
|
|
286
|
-
[
|
|
287
|
-
git_executable,
|
|
288
|
-
"clone",
|
|
289
|
-
repo_url,
|
|
290
|
-
str(clone_path),
|
|
291
|
-
],
|
|
292
|
-
capture_output=True,
|
|
293
|
-
text=True,
|
|
294
|
-
check=True,
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
return str(clone_path.absolute())
|
|
298
|
-
|
|
299
|
-
except subprocess.CalledProcessError as e:
|
|
300
|
-
error_text = Text()
|
|
301
|
-
error_text.append("❌ ", style="bold red")
|
|
302
|
-
error_text.append("REPOSITORY CLONE FAILED", style="bold red")
|
|
303
|
-
error_text.append("\n\n", style="white")
|
|
304
|
-
error_text.append(f"Could not clone repository: {repo_url}\n", style="white")
|
|
305
|
-
error_text.append(
|
|
306
|
-
f"Error: {e.stderr if hasattr(e, 'stderr') and e.stderr else str(e)}", style="dim red"
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
panel = Panel(
|
|
310
|
-
error_text,
|
|
311
|
-
title="[bold red]🛡️ STRIX CLONE ERROR",
|
|
312
|
-
title_align="center",
|
|
313
|
-
border_style="red",
|
|
314
|
-
padding=(1, 2),
|
|
315
|
-
)
|
|
316
|
-
console.print("\n")
|
|
317
|
-
console.print(panel)
|
|
318
|
-
console.print()
|
|
319
|
-
sys.exit(1)
|
|
320
|
-
except FileNotFoundError:
|
|
321
|
-
error_text = Text()
|
|
322
|
-
error_text.append("❌ ", style="bold red")
|
|
323
|
-
error_text.append("GIT NOT FOUND", style="bold red")
|
|
324
|
-
error_text.append("\n\n", style="white")
|
|
325
|
-
error_text.append("Git is not installed or not available in PATH.\n", style="white")
|
|
326
|
-
error_text.append("Please install Git to clone repositories.\n", style="white")
|
|
327
|
-
|
|
328
|
-
panel = Panel(
|
|
329
|
-
error_text,
|
|
330
|
-
title="[bold red]🛡️ STRIX CLONE ERROR",
|
|
331
|
-
title_align="center",
|
|
332
|
-
border_style="red",
|
|
333
|
-
padding=(1, 2),
|
|
334
|
-
)
|
|
335
|
-
console.print("\n")
|
|
336
|
-
console.print(panel)
|
|
337
|
-
console.print()
|
|
338
|
-
sys.exit(1)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def infer_target_type(target: str) -> tuple[str, dict[str, str]]:
|
|
342
|
-
if not target or not isinstance(target, str):
|
|
343
|
-
raise ValueError("Target must be a non-empty string")
|
|
344
|
-
|
|
345
|
-
target = target.strip()
|
|
346
|
-
|
|
347
|
-
parsed = urlparse(target)
|
|
348
|
-
if parsed.scheme in ("http", "https"):
|
|
349
|
-
if any(
|
|
350
|
-
host in parsed.netloc.lower() for host in ["github.com", "gitlab.com", "bitbucket.org"]
|
|
351
|
-
):
|
|
352
|
-
return "repository", {"target_repo": target}
|
|
353
|
-
return "web_application", {"target_url": target}
|
|
354
|
-
|
|
355
|
-
path = Path(target)
|
|
356
|
-
try:
|
|
357
|
-
if path.exists():
|
|
358
|
-
if path.is_dir():
|
|
359
|
-
return "local_code", {"target_path": str(path.absolute())}
|
|
360
|
-
raise ValueError(f"Path exists but is not a directory: {target}")
|
|
361
|
-
except (OSError, RuntimeError) as e:
|
|
362
|
-
raise ValueError(f"Invalid path: {target} - {e!s}") from e
|
|
363
|
-
|
|
364
|
-
if target.startswith("git@") or target.endswith(".git"):
|
|
365
|
-
return "repository", {"target_repo": target}
|
|
366
|
-
|
|
367
|
-
if "." in target and "/" not in target and not target.startswith("."):
|
|
368
|
-
parts = target.split(".")
|
|
369
|
-
if len(parts) >= 2 and all(p and p.strip() for p in parts):
|
|
370
|
-
return "web_application", {"target_url": f"https://{target}"}
|
|
371
|
-
|
|
372
|
-
raise ValueError(
|
|
373
|
-
f"Invalid target: {target}\n"
|
|
374
|
-
"Target must be one of:\n"
|
|
375
|
-
"- A valid URL (http:// or https://)\n"
|
|
376
|
-
"- A Git repository URL (https://github.com/... or git@github.com:...)\n"
|
|
377
|
-
"- A local directory path\n"
|
|
378
|
-
"- A domain name (e.g., example.com)"
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def parse_arguments() -> argparse.Namespace:
|
|
383
|
-
parser = argparse.ArgumentParser(
|
|
384
|
-
description="Strix Multi-Agent Cybersecurity Scanner",
|
|
385
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
386
|
-
epilog="""
|
|
387
|
-
Examples:
|
|
388
|
-
# Web application scan
|
|
389
|
-
strix --target https://example.com
|
|
390
|
-
|
|
391
|
-
# GitHub repository analysis
|
|
392
|
-
strix --target https://github.com/user/repo
|
|
393
|
-
strix --target git@github.com:user/repo.git
|
|
394
|
-
|
|
395
|
-
# Local code analysis
|
|
396
|
-
strix --target ./my-project
|
|
397
|
-
|
|
398
|
-
# Domain scan
|
|
399
|
-
strix --target example.com
|
|
400
|
-
|
|
401
|
-
# Custom instructions
|
|
402
|
-
strix --target example.com --instruction "Focus on authentication vulnerabilities"
|
|
403
|
-
""",
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
parser.add_argument(
|
|
407
|
-
"--target",
|
|
408
|
-
type=str,
|
|
409
|
-
required=True,
|
|
410
|
-
help="Target to scan (URL, repository, local directory path, or domain name)",
|
|
411
|
-
)
|
|
412
|
-
parser.add_argument(
|
|
413
|
-
"--instruction",
|
|
414
|
-
type=str,
|
|
415
|
-
help="Custom instructions for the scan. This can be "
|
|
416
|
-
"specific vulnerability types to focus on (e.g., 'Focus on IDOR and XSS'), "
|
|
417
|
-
"testing approaches (e.g., 'Perform thorough authentication testing'), "
|
|
418
|
-
"test credentials (e.g., 'Use the following credentials to access the app: "
|
|
419
|
-
"admin:password123'), "
|
|
420
|
-
"or areas of interest (e.g., 'Check login API endpoint for security issues')",
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
parser.add_argument(
|
|
424
|
-
"--run-name",
|
|
425
|
-
type=str,
|
|
426
|
-
help="Custom name for this scan run",
|
|
427
|
-
)
|
|
428
|
-
|
|
429
|
-
args = parser.parse_args()
|
|
430
|
-
|
|
431
|
-
try:
|
|
432
|
-
args.target_type, args.target_dict = infer_target_type(args.target)
|
|
433
|
-
except ValueError as e:
|
|
434
|
-
parser.error(str(e))
|
|
435
|
-
|
|
436
|
-
return args
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
def _build_stats_text(tracer: Any) -> Text:
|
|
440
|
-
stats_text = Text()
|
|
441
|
-
if not tracer:
|
|
442
|
-
return stats_text
|
|
443
|
-
|
|
444
|
-
vuln_count = len(tracer.vulnerability_reports)
|
|
445
|
-
tool_count = tracer.get_real_tool_count()
|
|
446
|
-
agent_count = len(tracer.agents)
|
|
447
|
-
|
|
448
|
-
if vuln_count > 0:
|
|
449
|
-
stats_text.append("🔍 Vulnerabilities Found: ", style="bold red")
|
|
450
|
-
stats_text.append(str(vuln_count), style="bold yellow")
|
|
451
|
-
stats_text.append(" • ", style="dim white")
|
|
452
|
-
|
|
453
|
-
stats_text.append("🤖 Agents Used: ", style="bold cyan")
|
|
454
|
-
stats_text.append(str(agent_count), style="bold white")
|
|
455
|
-
stats_text.append(" • ", style="dim white")
|
|
456
|
-
stats_text.append("🛠️ Tools Called: ", style="bold cyan")
|
|
457
|
-
stats_text.append(str(tool_count), style="bold white")
|
|
458
|
-
|
|
459
|
-
return stats_text
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
def _build_llm_stats_text(tracer: Any) -> Text:
|
|
463
|
-
llm_stats_text = Text()
|
|
464
|
-
if not tracer:
|
|
465
|
-
return llm_stats_text
|
|
466
|
-
|
|
467
|
-
llm_stats = tracer.get_total_llm_stats()
|
|
468
|
-
total_stats = llm_stats["total"]
|
|
469
|
-
|
|
470
|
-
if total_stats["requests"] > 0:
|
|
471
|
-
llm_stats_text.append("📥 Input Tokens: ", style="bold cyan")
|
|
472
|
-
llm_stats_text.append(format_token_count(total_stats["input_tokens"]), style="bold white")
|
|
473
|
-
|
|
474
|
-
if total_stats["cached_tokens"] > 0:
|
|
475
|
-
llm_stats_text.append(" • ", style="dim white")
|
|
476
|
-
llm_stats_text.append("⚡ Cached: ", style="bold green")
|
|
477
|
-
llm_stats_text.append(
|
|
478
|
-
format_token_count(total_stats["cached_tokens"]), style="bold green"
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
llm_stats_text.append(" • ", style="dim white")
|
|
482
|
-
llm_stats_text.append("📤 Output Tokens: ", style="bold cyan")
|
|
483
|
-
llm_stats_text.append(format_token_count(total_stats["output_tokens"]), style="bold white")
|
|
484
|
-
|
|
485
|
-
if total_stats["cost"] > 0:
|
|
486
|
-
llm_stats_text.append(" • ", style="dim white")
|
|
487
|
-
llm_stats_text.append("💰 Total Cost: $", style="bold cyan")
|
|
488
|
-
llm_stats_text.append(f"{total_stats['cost']:.4f}", style="bold yellow")
|
|
489
|
-
|
|
490
|
-
return llm_stats_text
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
def display_completion_message(args: argparse.Namespace, results_path: Path) -> None:
|
|
494
|
-
console = Console()
|
|
495
|
-
tracer = get_global_tracer()
|
|
496
|
-
|
|
497
|
-
target_value = next(iter(args.target_dict.values())) if args.target_dict else args.target
|
|
498
|
-
|
|
499
|
-
completion_text = Text()
|
|
500
|
-
completion_text.append("🦉 ", style="bold white")
|
|
501
|
-
completion_text.append("AGENT FINISHED", style="bold green")
|
|
502
|
-
completion_text.append(" • ", style="dim white")
|
|
503
|
-
completion_text.append("Security assessment completed", style="white")
|
|
504
|
-
|
|
505
|
-
stats_text = _build_stats_text(tracer)
|
|
506
|
-
|
|
507
|
-
llm_stats_text = _build_llm_stats_text(tracer)
|
|
508
|
-
|
|
509
|
-
target_text = Text()
|
|
510
|
-
target_text.append("🎯 Target: ", style="bold cyan")
|
|
511
|
-
target_text.append(str(target_value), style="bold white")
|
|
512
|
-
|
|
513
|
-
results_text = Text()
|
|
514
|
-
results_text.append("📊 Results Saved To: ", style="bold cyan")
|
|
515
|
-
results_text.append(str(results_path), style="bold yellow")
|
|
516
|
-
|
|
517
|
-
if stats_text.plain:
|
|
518
|
-
if llm_stats_text.plain:
|
|
519
|
-
panel_content = Text.assemble(
|
|
520
|
-
completion_text,
|
|
521
|
-
"\n\n",
|
|
522
|
-
target_text,
|
|
523
|
-
"\n",
|
|
524
|
-
stats_text,
|
|
525
|
-
"\n",
|
|
526
|
-
llm_stats_text,
|
|
527
|
-
"\n",
|
|
528
|
-
results_text,
|
|
529
|
-
)
|
|
530
|
-
else:
|
|
531
|
-
panel_content = Text.assemble(
|
|
532
|
-
completion_text, "\n\n", target_text, "\n", stats_text, "\n", results_text
|
|
533
|
-
)
|
|
534
|
-
elif llm_stats_text.plain:
|
|
535
|
-
panel_content = Text.assemble(
|
|
536
|
-
completion_text, "\n\n", target_text, "\n", llm_stats_text, "\n", results_text
|
|
537
|
-
)
|
|
538
|
-
else:
|
|
539
|
-
panel_content = Text.assemble(completion_text, "\n\n", target_text, "\n", results_text)
|
|
540
|
-
|
|
541
|
-
panel = Panel(
|
|
542
|
-
panel_content,
|
|
543
|
-
title="[bold green]🛡️ STRIX CYBERSECURITY AGENT",
|
|
544
|
-
title_align="center",
|
|
545
|
-
border_style="green",
|
|
546
|
-
padding=(1, 2),
|
|
547
|
-
)
|
|
548
|
-
|
|
549
|
-
console.print("\n")
|
|
550
|
-
console.print(panel)
|
|
551
|
-
console.print()
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
def _check_docker_connection() -> Any:
|
|
555
|
-
try:
|
|
556
|
-
return docker.from_env()
|
|
557
|
-
except DockerException:
|
|
558
|
-
console = Console()
|
|
559
|
-
error_text = Text()
|
|
560
|
-
error_text.append("❌ ", style="bold red")
|
|
561
|
-
error_text.append("DOCKER NOT AVAILABLE", style="bold red")
|
|
562
|
-
error_text.append("\n\n", style="white")
|
|
563
|
-
error_text.append("Cannot connect to Docker daemon.\n", style="white")
|
|
564
|
-
error_text.append("Please ensure Docker is installed and running.\n\n", style="white")
|
|
565
|
-
error_text.append("Try running: ", style="dim white")
|
|
566
|
-
error_text.append("sudo systemctl start docker", style="dim cyan")
|
|
567
|
-
|
|
568
|
-
panel = Panel(
|
|
569
|
-
error_text,
|
|
570
|
-
title="[bold red]🛡️ STRIX STARTUP ERROR",
|
|
571
|
-
title_align="center",
|
|
572
|
-
border_style="red",
|
|
573
|
-
padding=(1, 2),
|
|
574
|
-
)
|
|
575
|
-
console.print("\n", panel, "\n")
|
|
576
|
-
sys.exit(1)
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
def _image_exists(client: Any) -> bool:
|
|
580
|
-
try:
|
|
581
|
-
client.images.get(STRIX_IMAGE)
|
|
582
|
-
except docker.errors.ImageNotFound:
|
|
583
|
-
return False
|
|
584
|
-
else:
|
|
585
|
-
return True
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
def _update_layer_status(layers_info: dict[str, str], layer_id: str, layer_status: str) -> None:
|
|
589
|
-
if "Pull complete" in layer_status or "Already exists" in layer_status:
|
|
590
|
-
layers_info[layer_id] = "✓"
|
|
591
|
-
elif "Downloading" in layer_status:
|
|
592
|
-
layers_info[layer_id] = "↓"
|
|
593
|
-
elif "Extracting" in layer_status:
|
|
594
|
-
layers_info[layer_id] = "📦"
|
|
595
|
-
elif "Waiting" in layer_status:
|
|
596
|
-
layers_info[layer_id] = "⏳"
|
|
597
|
-
else:
|
|
598
|
-
layers_info[layer_id] = "•"
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
def _process_pull_line(
|
|
602
|
-
line: dict[str, Any], layers_info: dict[str, str], status: Any, last_update: str
|
|
603
|
-
) -> str:
|
|
604
|
-
if "id" in line and "status" in line:
|
|
605
|
-
layer_id = line["id"]
|
|
606
|
-
_update_layer_status(layers_info, layer_id, line["status"])
|
|
607
|
-
|
|
608
|
-
completed = sum(1 for v in layers_info.values() if v == "✓")
|
|
609
|
-
total = len(layers_info)
|
|
610
|
-
|
|
611
|
-
if total > 0:
|
|
612
|
-
update_msg = f"[bold cyan]Progress: {completed}/{total} layers complete"
|
|
613
|
-
if update_msg != last_update:
|
|
614
|
-
status.update(update_msg)
|
|
615
|
-
return update_msg
|
|
616
|
-
|
|
617
|
-
elif "status" in line and "id" not in line:
|
|
618
|
-
global_status = line["status"]
|
|
619
|
-
if "Pulling from" in global_status:
|
|
620
|
-
status.update("[bold cyan]Fetching image manifest...")
|
|
621
|
-
elif "Digest:" in global_status:
|
|
622
|
-
status.update("[bold cyan]Verifying image...")
|
|
623
|
-
elif "Status:" in global_status:
|
|
624
|
-
status.update("[bold cyan]Finalizing...")
|
|
625
|
-
|
|
626
|
-
return last_update
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
def pull_docker_image() -> None:
|
|
630
|
-
console = Console()
|
|
631
|
-
client = _check_docker_connection()
|
|
632
|
-
|
|
633
|
-
if _image_exists(client):
|
|
634
|
-
return
|
|
635
|
-
|
|
636
|
-
console.print()
|
|
637
|
-
console.print(f"[bold cyan]🐳 Pulling Docker image:[/] {STRIX_IMAGE}")
|
|
638
|
-
console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]")
|
|
639
|
-
console.print()
|
|
640
|
-
|
|
641
|
-
with console.status("[bold cyan]Downloading image layers...", spinner="dots") as status:
|
|
642
|
-
try:
|
|
643
|
-
layers_info: dict[str, str] = {}
|
|
644
|
-
last_update = ""
|
|
645
|
-
|
|
646
|
-
for line in client.api.pull(STRIX_IMAGE, stream=True, decode=True):
|
|
647
|
-
last_update = _process_pull_line(line, layers_info, status, last_update)
|
|
648
|
-
|
|
649
|
-
except DockerException as e:
|
|
650
|
-
console.print()
|
|
651
|
-
error_text = Text()
|
|
652
|
-
error_text.append("❌ ", style="bold red")
|
|
653
|
-
error_text.append("FAILED TO PULL IMAGE", style="bold red")
|
|
654
|
-
error_text.append("\n\n", style="white")
|
|
655
|
-
error_text.append(f"Could not download: {STRIX_IMAGE}\n", style="white")
|
|
656
|
-
error_text.append(str(e), style="dim red")
|
|
657
|
-
|
|
658
|
-
panel = Panel(
|
|
659
|
-
error_text,
|
|
660
|
-
title="[bold red]🛡️ DOCKER PULL ERROR",
|
|
661
|
-
title_align="center",
|
|
662
|
-
border_style="red",
|
|
663
|
-
padding=(1, 2),
|
|
664
|
-
)
|
|
665
|
-
console.print(panel, "\n")
|
|
666
|
-
sys.exit(1)
|
|
667
|
-
|
|
668
|
-
success_text = Text()
|
|
669
|
-
success_text.append("✅ ", style="bold green")
|
|
670
|
-
success_text.append("Successfully pulled Docker image", style="green")
|
|
671
|
-
console.print(success_text)
|
|
672
|
-
console.print()
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
def main() -> None:
|
|
676
|
-
if sys.platform == "win32":
|
|
677
|
-
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
678
|
-
|
|
679
|
-
args = parse_arguments()
|
|
680
|
-
|
|
681
|
-
check_docker_installed()
|
|
682
|
-
pull_docker_image()
|
|
683
|
-
|
|
684
|
-
validate_environment()
|
|
685
|
-
asyncio.run(warm_up_llm())
|
|
686
|
-
|
|
687
|
-
if not args.run_name:
|
|
688
|
-
args.run_name = generate_run_name()
|
|
689
|
-
|
|
690
|
-
if args.target_type == "repository":
|
|
691
|
-
repo_url = args.target_dict["target_repo"]
|
|
692
|
-
cloned_path = clone_repository(repo_url, args.run_name)
|
|
693
|
-
|
|
694
|
-
args.target_dict["cloned_repo_path"] = cloned_path
|
|
695
|
-
|
|
696
|
-
asyncio.run(run_strix_cli(args))
|
|
697
|
-
|
|
698
|
-
results_path = Path("agent_runs") / args.run_name
|
|
699
|
-
display_completion_message(args, results_path)
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if __name__ == "__main__":
|
|
703
|
-
main()
|