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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<scan_mode>
|
|
2
|
+
STANDARD SCAN MODE - Balanced Security Assessment
|
|
3
|
+
|
|
4
|
+
This mode provides thorough coverage with a structured methodology. Balance depth with efficiency.
|
|
5
|
+
|
|
6
|
+
PHASE 1: RECONNAISSANCE AND MAPPING
|
|
7
|
+
Understanding the target is critical before exploitation. Never skip this phase.
|
|
8
|
+
|
|
9
|
+
For whitebox (source code available):
|
|
10
|
+
- Map the entire codebase structure: directories, modules, entry points
|
|
11
|
+
- Identify the application architecture (MVC, microservices, monolith)
|
|
12
|
+
- Understand the routing: how URLs map to handlers/controllers
|
|
13
|
+
- Identify all user input vectors: forms, APIs, file uploads, headers, cookies
|
|
14
|
+
- Map authentication and authorization flows
|
|
15
|
+
- Identify database interactions and ORM usage
|
|
16
|
+
- Review dependency manifests for known vulnerable packages
|
|
17
|
+
- Understand the data model and sensitive data locations
|
|
18
|
+
|
|
19
|
+
For blackbox (no source code):
|
|
20
|
+
- Crawl the application thoroughly using browser tool - interact with every feature
|
|
21
|
+
- Enumerate all endpoints, parameters, and functionality
|
|
22
|
+
- Identify the technology stack through fingerprinting
|
|
23
|
+
- Map user roles and access levels
|
|
24
|
+
- Understand the business logic by using the application as intended
|
|
25
|
+
- Document all forms, APIs, and data entry points
|
|
26
|
+
- Use proxy tool to capture and analyze all traffic during exploration
|
|
27
|
+
|
|
28
|
+
PHASE 2: BUSINESS LOGIC UNDERSTANDING
|
|
29
|
+
Before testing for vulnerabilities, understand what the application DOES:
|
|
30
|
+
- What are the critical business flows? (payments, user registration, data access)
|
|
31
|
+
- What actions should be restricted to specific roles?
|
|
32
|
+
- What data should users NOT be able to access?
|
|
33
|
+
- What state transitions exist? (order pending → paid → shipped)
|
|
34
|
+
- Where does money, sensitive data, or privilege flow?
|
|
35
|
+
|
|
36
|
+
PHASE 3: SYSTEMATIC VULNERABILITY ASSESSMENT
|
|
37
|
+
Test each attack surface methodically. Create focused subagents for different areas.
|
|
38
|
+
|
|
39
|
+
Entry Point Analysis:
|
|
40
|
+
- Test all input fields for injection vulnerabilities
|
|
41
|
+
- Check all API endpoints for authentication and authorization
|
|
42
|
+
- Verify all file upload functionality for bypass
|
|
43
|
+
- Test all search and filter functionality
|
|
44
|
+
- Check redirect parameters and URL handling
|
|
45
|
+
|
|
46
|
+
Authentication and Session:
|
|
47
|
+
- Test login for brute force protection
|
|
48
|
+
- Check session token entropy and handling
|
|
49
|
+
- Test password reset flows for weaknesses
|
|
50
|
+
- Verify logout invalidates sessions
|
|
51
|
+
- Test for authentication bypass techniques
|
|
52
|
+
|
|
53
|
+
Access Control:
|
|
54
|
+
- For every privileged action, test as unprivileged user
|
|
55
|
+
- Test horizontal access control (user A accessing user B's data)
|
|
56
|
+
- Test vertical access control (user escalating to admin)
|
|
57
|
+
- Check API endpoints mirror UI access controls
|
|
58
|
+
- Test direct object references with different user contexts
|
|
59
|
+
|
|
60
|
+
Business Logic:
|
|
61
|
+
- Attempt to skip steps in multi-step processes
|
|
62
|
+
- Test for race conditions in critical operations
|
|
63
|
+
- Try negative values, zero values, boundary conditions
|
|
64
|
+
- Attempt to replay transactions
|
|
65
|
+
- Test for price manipulation in e-commerce flows
|
|
66
|
+
|
|
67
|
+
PHASE 4: EXPLOITATION AND VALIDATION
|
|
68
|
+
- Every finding must have a working proof-of-concept
|
|
69
|
+
- Demonstrate actual impact, not theoretical risk
|
|
70
|
+
- Chain vulnerabilities when possible to show maximum impact
|
|
71
|
+
- Document the full attack path from initial access to impact
|
|
72
|
+
- Use python tool for complex exploit development
|
|
73
|
+
|
|
74
|
+
CHAINING & MAX IMPACT MINDSET:
|
|
75
|
+
- Always ask: "If I can do X, what does that enable me to do next?" Keep pivoting until you reach maximum privilege or maximum sensitive data access
|
|
76
|
+
- Prefer complete end-to-end paths (entry point → pivot → privileged action/data) over isolated bug reports
|
|
77
|
+
- Use the application as a real user would: exploit must survive the actual workflow and state transitions
|
|
78
|
+
- When you discover a useful pivot (info leak, weak boundary, partial access), immediately pursue the next step rather than stopping at the first win
|
|
79
|
+
|
|
80
|
+
PHASE 5: COMPREHENSIVE REPORTING
|
|
81
|
+
- Report all confirmed vulnerabilities with clear reproduction steps
|
|
82
|
+
- Include severity based on actual exploitability and business impact
|
|
83
|
+
- Provide remediation recommendations
|
|
84
|
+
- Document any areas that need further investigation
|
|
85
|
+
|
|
86
|
+
MINDSET:
|
|
87
|
+
- Methodical and systematic - cover the full attack surface
|
|
88
|
+
- Document as you go - findings and areas tested
|
|
89
|
+
- Validate everything - no assumptions about exploitability
|
|
90
|
+
- Think about business impact, not just technical severity
|
|
91
|
+
</scan_mode>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
### Overview
|
|
2
|
+
|
|
3
|
+
To help make Strix better for everyone, we collect anonymized data that helps us understand how to better improve our AI security agent for our users, guide the addition of new features, and fix common errors and bugs. This feedback loop is crucial for improving Strix's capabilities and user experience.
|
|
4
|
+
|
|
5
|
+
We use [PostHog](https://posthog.com), an open-source analytics platform, for data collection and analysis. Our telemetry implementation is fully transparent - you can review the [source code](https://github.com/usestrix/strix/blob/main/strix/telemetry/posthog.py) to see exactly what we track.
|
|
6
|
+
|
|
7
|
+
### Telemetry Policy
|
|
8
|
+
|
|
9
|
+
Privacy is our priority. All collected data is anonymized by default. Each session gets a random UUID that is not persisted or tied to you. Your code, scan targets, vulnerability details, and findings always remain private and are never collected.
|
|
10
|
+
|
|
11
|
+
### What We Track
|
|
12
|
+
|
|
13
|
+
We collect only very **basic** usage data including:
|
|
14
|
+
|
|
15
|
+
**Session Errors:** Duration and error types (not messages or stack traces)\
|
|
16
|
+
**System Context:** OS type, architecture, Strix version\
|
|
17
|
+
**Scan Context:** Scan mode (quick/standard/deep), scan type (whitebox/blackbox)\
|
|
18
|
+
**Model Usage:** Which LLM model is being used (not prompts or responses)\
|
|
19
|
+
**Aggregate Metrics:** Vulnerability counts by severity, agent/tool counts, token usage and cost estimates
|
|
20
|
+
|
|
21
|
+
For complete transparency, you can inspect our [telemetry implementation](https://github.com/usestrix/strix/blob/main/strix/telemetry/posthog.py) to see the exact events we track.
|
|
22
|
+
|
|
23
|
+
### What We **Never** Collect
|
|
24
|
+
|
|
25
|
+
- IP addresses, usernames, or any identifying information
|
|
26
|
+
- Scan targets, file paths, target URLs, or domains
|
|
27
|
+
- Vulnerability details, descriptions, or code
|
|
28
|
+
- LLM requests and responses
|
|
29
|
+
|
|
30
|
+
### How to Opt Out
|
|
31
|
+
|
|
32
|
+
Telemetry in Strix is entirely **optional**:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
export STRIX_TELEMETRY=0
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
You can set this environment variable before running Strix to disable **all** telemetry.
|
strix/telemetry/__init__.py
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import platform
|
|
3
|
+
import sys
|
|
4
|
+
import urllib.request
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from strix.config import Config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from strix.telemetry.tracer import Tracer
|
|
14
|
+
|
|
15
|
+
_POSTHOG_PUBLIC_API_KEY = "phc_7rO3XRuNT5sgSKAl6HDIrWdSGh1COzxw0vxVIAR6vVZ"
|
|
16
|
+
_POSTHOG_HOST = "https://us.i.posthog.com"
|
|
17
|
+
|
|
18
|
+
_SESSION_ID = uuid4().hex[:16]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _is_enabled() -> bool:
|
|
22
|
+
return (Config.get("strix_telemetry") or "1").lower() not in ("0", "false", "no", "off")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_first_run() -> bool:
|
|
26
|
+
marker = Path.home() / ".strix" / ".seen"
|
|
27
|
+
if marker.exists():
|
|
28
|
+
return False
|
|
29
|
+
try:
|
|
30
|
+
marker.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
marker.touch()
|
|
32
|
+
except Exception: # noqa: BLE001, S110
|
|
33
|
+
pass # nosec B110
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_version() -> str:
|
|
38
|
+
try:
|
|
39
|
+
from importlib.metadata import version
|
|
40
|
+
|
|
41
|
+
return version("strix-agent")
|
|
42
|
+
except Exception: # noqa: BLE001
|
|
43
|
+
return "unknown"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _send(event: str, properties: dict[str, Any]) -> None:
|
|
47
|
+
if not _is_enabled():
|
|
48
|
+
return
|
|
49
|
+
try:
|
|
50
|
+
payload = {
|
|
51
|
+
"api_key": _POSTHOG_PUBLIC_API_KEY,
|
|
52
|
+
"event": event,
|
|
53
|
+
"distinct_id": _SESSION_ID,
|
|
54
|
+
"properties": properties,
|
|
55
|
+
}
|
|
56
|
+
req = urllib.request.Request( # noqa: S310
|
|
57
|
+
f"{_POSTHOG_HOST}/capture/",
|
|
58
|
+
data=json.dumps(payload).encode(),
|
|
59
|
+
headers={"Content-Type": "application/json"},
|
|
60
|
+
)
|
|
61
|
+
with urllib.request.urlopen(req, timeout=10): # noqa: S310 # nosec B310
|
|
62
|
+
pass
|
|
63
|
+
except Exception: # noqa: BLE001, S110
|
|
64
|
+
pass # nosec B110
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _base_props() -> dict[str, Any]:
|
|
68
|
+
return {
|
|
69
|
+
"os": platform.system().lower(),
|
|
70
|
+
"arch": platform.machine(),
|
|
71
|
+
"python": f"{sys.version_info.major}.{sys.version_info.minor}",
|
|
72
|
+
"strix_version": _get_version(),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def start(
|
|
77
|
+
model: str | None,
|
|
78
|
+
scan_mode: str | None,
|
|
79
|
+
is_whitebox: bool,
|
|
80
|
+
interactive: bool,
|
|
81
|
+
has_instructions: bool,
|
|
82
|
+
) -> None:
|
|
83
|
+
_send(
|
|
84
|
+
"scan_started",
|
|
85
|
+
{
|
|
86
|
+
**_base_props(),
|
|
87
|
+
"model": model or "unknown",
|
|
88
|
+
"scan_mode": scan_mode or "unknown",
|
|
89
|
+
"scan_type": "whitebox" if is_whitebox else "blackbox",
|
|
90
|
+
"interactive": interactive,
|
|
91
|
+
"has_instructions": has_instructions,
|
|
92
|
+
"first_run": _is_first_run(),
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def finding(severity: str) -> None:
|
|
98
|
+
_send(
|
|
99
|
+
"finding_reported",
|
|
100
|
+
{
|
|
101
|
+
**_base_props(),
|
|
102
|
+
"severity": severity.lower(),
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def end(tracer: "Tracer", exit_reason: str = "completed") -> None:
|
|
108
|
+
vulnerabilities_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
|
|
109
|
+
for v in tracer.vulnerability_reports:
|
|
110
|
+
sev = v.get("severity", "info").lower()
|
|
111
|
+
if sev in vulnerabilities_counts:
|
|
112
|
+
vulnerabilities_counts[sev] += 1
|
|
113
|
+
|
|
114
|
+
llm = tracer.get_total_llm_stats()
|
|
115
|
+
total = llm.get("total", {})
|
|
116
|
+
|
|
117
|
+
_send(
|
|
118
|
+
"scan_ended",
|
|
119
|
+
{
|
|
120
|
+
**_base_props(),
|
|
121
|
+
"exit_reason": exit_reason,
|
|
122
|
+
"duration_seconds": round(tracer._calculate_duration()),
|
|
123
|
+
"vulnerabilities_total": len(tracer.vulnerability_reports),
|
|
124
|
+
**{f"vulnerabilities_{k}": v for k, v in vulnerabilities_counts.items()},
|
|
125
|
+
"agent_count": len(tracer.agents),
|
|
126
|
+
"tool_count": tracer.get_real_tool_count(),
|
|
127
|
+
"llm_tokens": llm.get("total_tokens", 0),
|
|
128
|
+
"llm_cost": total.get("cost", 0.0),
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def error(error_type: str, error_msg: str | None = None) -> None:
|
|
134
|
+
props = {**_base_props(), "error_type": error_type}
|
|
135
|
+
if error_msg:
|
|
136
|
+
props["error_msg"] = error_msg
|
|
137
|
+
_send("error", props)
|
strix/telemetry/tracer.py
CHANGED
|
@@ -4,6 +4,8 @@ from pathlib import Path
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any, Optional
|
|
5
5
|
from uuid import uuid4
|
|
6
6
|
|
|
7
|
+
from strix.telemetry import posthog
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
if TYPE_CHECKING:
|
|
9
11
|
from collections.abc import Callable
|
|
@@ -33,6 +35,8 @@ class Tracer:
|
|
|
33
35
|
self.agents: dict[str, dict[str, Any]] = {}
|
|
34
36
|
self.tool_executions: dict[int, dict[str, Any]] = {}
|
|
35
37
|
self.chat_messages: list[dict[str, Any]] = []
|
|
38
|
+
self.streaming_content: dict[str, str] = {}
|
|
39
|
+
self.interrupted_content: dict[str, str] = {}
|
|
36
40
|
|
|
37
41
|
self.vulnerability_reports: list[dict[str, Any]] = []
|
|
38
42
|
self.final_scan_result: str | None = None
|
|
@@ -52,7 +56,7 @@ class Tracer:
|
|
|
52
56
|
self._next_message_id = 1
|
|
53
57
|
self._saved_vuln_ids: set[str] = set()
|
|
54
58
|
|
|
55
|
-
self.vulnerability_found_callback: Callable[[str,
|
|
59
|
+
self.vulnerability_found_callback: Callable[[dict[str, Any]], None] | None = None
|
|
56
60
|
|
|
57
61
|
def set_run_name(self, run_name: str) -> None:
|
|
58
62
|
self.run_name = run_name
|
|
@@ -69,48 +73,118 @@ class Tracer:
|
|
|
69
73
|
|
|
70
74
|
return self._run_dir
|
|
71
75
|
|
|
72
|
-
def add_vulnerability_report(
|
|
76
|
+
def add_vulnerability_report( # noqa: PLR0912
|
|
73
77
|
self,
|
|
74
78
|
title: str,
|
|
75
|
-
content: str,
|
|
76
79
|
severity: str,
|
|
80
|
+
description: str | None = None,
|
|
81
|
+
impact: str | None = None,
|
|
82
|
+
target: str | None = None,
|
|
83
|
+
technical_analysis: str | None = None,
|
|
84
|
+
poc_description: str | None = None,
|
|
85
|
+
poc_script_code: str | None = None,
|
|
86
|
+
remediation_steps: str | None = None,
|
|
87
|
+
cvss: float | None = None,
|
|
88
|
+
cvss_breakdown: dict[str, str] | None = None,
|
|
89
|
+
endpoint: str | None = None,
|
|
90
|
+
method: str | None = None,
|
|
91
|
+
cve: str | None = None,
|
|
92
|
+
code_file: str | None = None,
|
|
93
|
+
code_before: str | None = None,
|
|
94
|
+
code_after: str | None = None,
|
|
95
|
+
code_diff: str | None = None,
|
|
77
96
|
) -> str:
|
|
78
97
|
report_id = f"vuln-{len(self.vulnerability_reports) + 1:04d}"
|
|
79
98
|
|
|
80
|
-
report = {
|
|
99
|
+
report: dict[str, Any] = {
|
|
81
100
|
"id": report_id,
|
|
82
101
|
"title": title.strip(),
|
|
83
|
-
"content": content.strip(),
|
|
84
102
|
"severity": severity.lower().strip(),
|
|
85
103
|
"timestamp": datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC"),
|
|
86
104
|
}
|
|
87
105
|
|
|
106
|
+
if description:
|
|
107
|
+
report["description"] = description.strip()
|
|
108
|
+
if impact:
|
|
109
|
+
report["impact"] = impact.strip()
|
|
110
|
+
if target:
|
|
111
|
+
report["target"] = target.strip()
|
|
112
|
+
if technical_analysis:
|
|
113
|
+
report["technical_analysis"] = technical_analysis.strip()
|
|
114
|
+
if poc_description:
|
|
115
|
+
report["poc_description"] = poc_description.strip()
|
|
116
|
+
if poc_script_code:
|
|
117
|
+
report["poc_script_code"] = poc_script_code.strip()
|
|
118
|
+
if remediation_steps:
|
|
119
|
+
report["remediation_steps"] = remediation_steps.strip()
|
|
120
|
+
if cvss is not None:
|
|
121
|
+
report["cvss"] = cvss
|
|
122
|
+
if cvss_breakdown:
|
|
123
|
+
report["cvss_breakdown"] = cvss_breakdown
|
|
124
|
+
if endpoint:
|
|
125
|
+
report["endpoint"] = endpoint.strip()
|
|
126
|
+
if method:
|
|
127
|
+
report["method"] = method.strip()
|
|
128
|
+
if cve:
|
|
129
|
+
report["cve"] = cve.strip()
|
|
130
|
+
if code_file:
|
|
131
|
+
report["code_file"] = code_file.strip()
|
|
132
|
+
if code_before:
|
|
133
|
+
report["code_before"] = code_before.strip()
|
|
134
|
+
if code_after:
|
|
135
|
+
report["code_after"] = code_after.strip()
|
|
136
|
+
if code_diff:
|
|
137
|
+
report["code_diff"] = code_diff.strip()
|
|
138
|
+
|
|
88
139
|
self.vulnerability_reports.append(report)
|
|
89
140
|
logger.info(f"Added vulnerability report: {report_id} - {title}")
|
|
141
|
+
posthog.finding(severity)
|
|
90
142
|
|
|
91
143
|
if self.vulnerability_found_callback:
|
|
92
|
-
self.vulnerability_found_callback(
|
|
93
|
-
report_id, title.strip(), content.strip(), severity.lower().strip()
|
|
94
|
-
)
|
|
144
|
+
self.vulnerability_found_callback(report)
|
|
95
145
|
|
|
96
146
|
self.save_run_data()
|
|
97
147
|
return report_id
|
|
98
148
|
|
|
99
|
-
def
|
|
149
|
+
def get_existing_vulnerabilities(self) -> list[dict[str, Any]]:
|
|
150
|
+
return list(self.vulnerability_reports)
|
|
151
|
+
|
|
152
|
+
def update_scan_final_fields(
|
|
100
153
|
self,
|
|
101
|
-
|
|
102
|
-
|
|
154
|
+
executive_summary: str,
|
|
155
|
+
methodology: str,
|
|
156
|
+
technical_analysis: str,
|
|
157
|
+
recommendations: str,
|
|
103
158
|
) -> None:
|
|
104
|
-
self.final_scan_result = content.strip()
|
|
105
|
-
|
|
106
159
|
self.scan_results = {
|
|
107
160
|
"scan_completed": True,
|
|
108
|
-
"
|
|
109
|
-
"
|
|
161
|
+
"executive_summary": executive_summary.strip(),
|
|
162
|
+
"methodology": methodology.strip(),
|
|
163
|
+
"technical_analysis": technical_analysis.strip(),
|
|
164
|
+
"recommendations": recommendations.strip(),
|
|
165
|
+
"success": True,
|
|
110
166
|
}
|
|
111
167
|
|
|
112
|
-
|
|
168
|
+
self.final_scan_result = f"""# Executive Summary
|
|
169
|
+
|
|
170
|
+
{executive_summary.strip()}
|
|
171
|
+
|
|
172
|
+
# Methodology
|
|
173
|
+
|
|
174
|
+
{methodology.strip()}
|
|
175
|
+
|
|
176
|
+
# Technical Analysis
|
|
177
|
+
|
|
178
|
+
{technical_analysis.strip()}
|
|
179
|
+
|
|
180
|
+
# Recommendations
|
|
181
|
+
|
|
182
|
+
{recommendations.strip()}
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
logger.info("Updated scan final fields")
|
|
113
186
|
self.save_run_data(mark_complete=True)
|
|
187
|
+
posthog.end(self, exit_reason="finished_by_tool")
|
|
114
188
|
|
|
115
189
|
def log_agent_creation(
|
|
116
190
|
self, agent_id: str, name: str, task: str, parent_id: str | None = None
|
|
@@ -202,7 +276,7 @@ class Tracer:
|
|
|
202
276
|
)
|
|
203
277
|
self.get_run_dir()
|
|
204
278
|
|
|
205
|
-
def save_run_data(self, mark_complete: bool = False) -> None:
|
|
279
|
+
def save_run_data(self, mark_complete: bool = False) -> None: # noqa: PLR0912, PLR0915
|
|
206
280
|
try:
|
|
207
281
|
run_dir = self.get_run_dir()
|
|
208
282
|
if mark_complete:
|
|
@@ -230,42 +304,89 @@ class Tracer:
|
|
|
230
304
|
if report["id"] not in self._saved_vuln_ids
|
|
231
305
|
]
|
|
232
306
|
|
|
307
|
+
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
308
|
+
sorted_reports = sorted(
|
|
309
|
+
self.vulnerability_reports,
|
|
310
|
+
key=lambda x: (severity_order.get(x["severity"], 5), x["timestamp"]),
|
|
311
|
+
)
|
|
312
|
+
|
|
233
313
|
for report in new_reports:
|
|
234
314
|
vuln_file = vuln_dir / f"{report['id']}.md"
|
|
235
315
|
with vuln_file.open("w", encoding="utf-8") as f:
|
|
236
|
-
f.write(f"# {report
|
|
237
|
-
f.write(f"**ID:** {report
|
|
238
|
-
f.write(f"**Severity:** {report
|
|
239
|
-
f.write(f"**Found:** {report
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
316
|
+
f.write(f"# {report.get('title', 'Untitled Vulnerability')}\n\n")
|
|
317
|
+
f.write(f"**ID:** {report.get('id', 'unknown')}\n")
|
|
318
|
+
f.write(f"**Severity:** {report.get('severity', 'unknown').upper()}\n")
|
|
319
|
+
f.write(f"**Found:** {report.get('timestamp', 'unknown')}\n")
|
|
320
|
+
|
|
321
|
+
metadata_fields: list[tuple[str, Any]] = [
|
|
322
|
+
("Target", report.get("target")),
|
|
323
|
+
("Endpoint", report.get("endpoint")),
|
|
324
|
+
("Method", report.get("method")),
|
|
325
|
+
("CVE", report.get("cve")),
|
|
326
|
+
]
|
|
327
|
+
cvss_score = report.get("cvss")
|
|
328
|
+
if cvss_score is not None:
|
|
329
|
+
metadata_fields.append(("CVSS", cvss_score))
|
|
330
|
+
|
|
331
|
+
for label, value in metadata_fields:
|
|
332
|
+
if value:
|
|
333
|
+
f.write(f"**{label}:** {value}\n")
|
|
334
|
+
|
|
335
|
+
f.write("\n## Description\n\n")
|
|
336
|
+
desc = report.get("description") or "No description provided."
|
|
337
|
+
f.write(f"{desc}\n\n")
|
|
338
|
+
|
|
339
|
+
if report.get("impact"):
|
|
340
|
+
f.write("## Impact\n\n")
|
|
341
|
+
f.write(f"{report['impact']}\n\n")
|
|
342
|
+
|
|
343
|
+
if report.get("technical_analysis"):
|
|
344
|
+
f.write("## Technical Analysis\n\n")
|
|
345
|
+
f.write(f"{report['technical_analysis']}\n\n")
|
|
346
|
+
|
|
347
|
+
if report.get("poc_description") or report.get("poc_script_code"):
|
|
348
|
+
f.write("## Proof of Concept\n\n")
|
|
349
|
+
if report.get("poc_description"):
|
|
350
|
+
f.write(f"{report['poc_description']}\n\n")
|
|
351
|
+
if report.get("poc_script_code"):
|
|
352
|
+
f.write("```\n")
|
|
353
|
+
f.write(f"{report['poc_script_code']}\n")
|
|
354
|
+
f.write("```\n\n")
|
|
355
|
+
|
|
356
|
+
if report.get("code_file") or report.get("code_diff"):
|
|
357
|
+
f.write("## Code Analysis\n\n")
|
|
358
|
+
if report.get("code_file"):
|
|
359
|
+
f.write(f"**File:** {report['code_file']}\n\n")
|
|
360
|
+
if report.get("code_diff"):
|
|
361
|
+
f.write("**Changes:**\n")
|
|
362
|
+
f.write("```diff\n")
|
|
363
|
+
f.write(f"{report['code_diff']}\n")
|
|
364
|
+
f.write("```\n\n")
|
|
365
|
+
|
|
366
|
+
if report.get("remediation_steps"):
|
|
367
|
+
f.write("## Remediation\n\n")
|
|
368
|
+
f.write(f"{report['remediation_steps']}\n\n")
|
|
243
369
|
|
|
244
|
-
|
|
245
|
-
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
246
|
-
sorted_reports = sorted(
|
|
247
|
-
self.vulnerability_reports,
|
|
248
|
-
key=lambda x: (severity_order.get(x["severity"], 5), x["timestamp"]),
|
|
249
|
-
)
|
|
370
|
+
self._saved_vuln_ids.add(report["id"])
|
|
250
371
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
372
|
+
vuln_csv_file = run_dir / "vulnerabilities.csv"
|
|
373
|
+
with vuln_csv_file.open("w", encoding="utf-8", newline="") as f:
|
|
374
|
+
import csv
|
|
375
|
+
|
|
376
|
+
fieldnames = ["id", "title", "severity", "timestamp", "file"]
|
|
377
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
378
|
+
writer.writeheader()
|
|
379
|
+
|
|
380
|
+
for report in sorted_reports:
|
|
381
|
+
writer.writerow(
|
|
382
|
+
{
|
|
383
|
+
"id": report["id"],
|
|
384
|
+
"title": report["title"],
|
|
385
|
+
"severity": report["severity"].upper(),
|
|
386
|
+
"timestamp": report["timestamp"],
|
|
387
|
+
"file": f"vulnerabilities/{report['id']}.md",
|
|
388
|
+
}
|
|
389
|
+
)
|
|
269
390
|
|
|
270
391
|
if new_reports:
|
|
271
392
|
logger.info(
|
|
@@ -291,14 +412,14 @@ class Tracer:
|
|
|
291
412
|
def get_agent_tools(self, agent_id: str) -> list[dict[str, Any]]:
|
|
292
413
|
return [
|
|
293
414
|
exec_data
|
|
294
|
-
for exec_data in self.tool_executions.values()
|
|
415
|
+
for exec_data in list(self.tool_executions.values())
|
|
295
416
|
if exec_data.get("agent_id") == agent_id
|
|
296
417
|
]
|
|
297
418
|
|
|
298
419
|
def get_real_tool_count(self) -> int:
|
|
299
420
|
return sum(
|
|
300
421
|
1
|
|
301
|
-
for exec_data in self.tool_executions.values()
|
|
422
|
+
for exec_data in list(self.tool_executions.values())
|
|
302
423
|
if exec_data.get("tool_name") not in ["scan_start_info", "subagent_start_info"]
|
|
303
424
|
)
|
|
304
425
|
|
|
@@ -309,10 +430,8 @@ class Tracer:
|
|
|
309
430
|
"input_tokens": 0,
|
|
310
431
|
"output_tokens": 0,
|
|
311
432
|
"cached_tokens": 0,
|
|
312
|
-
"cache_creation_tokens": 0,
|
|
313
433
|
"cost": 0.0,
|
|
314
434
|
"requests": 0,
|
|
315
|
-
"failed_requests": 0,
|
|
316
435
|
}
|
|
317
436
|
|
|
318
437
|
for agent_instance in _agent_instances.values():
|
|
@@ -321,10 +440,8 @@ class Tracer:
|
|
|
321
440
|
total_stats["input_tokens"] += agent_stats.input_tokens
|
|
322
441
|
total_stats["output_tokens"] += agent_stats.output_tokens
|
|
323
442
|
total_stats["cached_tokens"] += agent_stats.cached_tokens
|
|
324
|
-
total_stats["cache_creation_tokens"] += agent_stats.cache_creation_tokens
|
|
325
443
|
total_stats["cost"] += agent_stats.cost
|
|
326
444
|
total_stats["requests"] += agent_stats.requests
|
|
327
|
-
total_stats["failed_requests"] += agent_stats.failed_requests
|
|
328
445
|
|
|
329
446
|
total_stats["cost"] = round(total_stats["cost"], 4)
|
|
330
447
|
|
|
@@ -333,5 +450,28 @@ class Tracer:
|
|
|
333
450
|
"total_tokens": total_stats["input_tokens"] + total_stats["output_tokens"],
|
|
334
451
|
}
|
|
335
452
|
|
|
453
|
+
def update_streaming_content(self, agent_id: str, content: str) -> None:
|
|
454
|
+
self.streaming_content[agent_id] = content
|
|
455
|
+
|
|
456
|
+
def clear_streaming_content(self, agent_id: str) -> None:
|
|
457
|
+
self.streaming_content.pop(agent_id, None)
|
|
458
|
+
|
|
459
|
+
def get_streaming_content(self, agent_id: str) -> str | None:
|
|
460
|
+
return self.streaming_content.get(agent_id)
|
|
461
|
+
|
|
462
|
+
def finalize_streaming_as_interrupted(self, agent_id: str) -> str | None:
|
|
463
|
+
content = self.streaming_content.pop(agent_id, None)
|
|
464
|
+
if content and content.strip():
|
|
465
|
+
self.interrupted_content[agent_id] = content
|
|
466
|
+
self.log_chat_message(
|
|
467
|
+
content=content,
|
|
468
|
+
role="assistant",
|
|
469
|
+
agent_id=agent_id,
|
|
470
|
+
metadata={"interrupted": True},
|
|
471
|
+
)
|
|
472
|
+
return content
|
|
473
|
+
|
|
474
|
+
return self.interrupted_content.pop(agent_id, None)
|
|
475
|
+
|
|
336
476
|
def cleanup(self) -> None:
|
|
337
477
|
self.save_run_data(mark_complete=True)
|
strix/tools/__init__.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
|
+
from strix.config import Config
|
|
4
|
+
|
|
3
5
|
from .executor import (
|
|
4
6
|
execute_tool,
|
|
5
7
|
execute_tool_invocation,
|
|
@@ -22,11 +24,15 @@ from .registry import (
|
|
|
22
24
|
|
|
23
25
|
SANDBOX_MODE = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
|
|
24
26
|
|
|
25
|
-
HAS_PERPLEXITY_API = bool(
|
|
27
|
+
HAS_PERPLEXITY_API = bool(Config.get("perplexity_api_key"))
|
|
28
|
+
|
|
29
|
+
DISABLE_BROWSER = (Config.get("strix_disable_browser") or "false").lower() == "true"
|
|
26
30
|
|
|
27
31
|
if not SANDBOX_MODE:
|
|
28
32
|
from .agents_graph import * # noqa: F403
|
|
29
|
-
|
|
33
|
+
|
|
34
|
+
if not DISABLE_BROWSER:
|
|
35
|
+
from .browser import * # noqa: F403
|
|
30
36
|
from .file_edit import * # noqa: F403
|
|
31
37
|
from .finish import * # noqa: F403
|
|
32
38
|
from .notes import * # noqa: F403
|
|
@@ -35,13 +41,14 @@ if not SANDBOX_MODE:
|
|
|
35
41
|
from .reporting import * # noqa: F403
|
|
36
42
|
from .terminal import * # noqa: F403
|
|
37
43
|
from .thinking import * # noqa: F403
|
|
44
|
+
from .todo import * # noqa: F403
|
|
38
45
|
|
|
39
46
|
if HAS_PERPLEXITY_API:
|
|
40
47
|
from .web_search import * # noqa: F403
|
|
41
48
|
else:
|
|
42
|
-
|
|
49
|
+
if not DISABLE_BROWSER:
|
|
50
|
+
from .browser import * # noqa: F403
|
|
43
51
|
from .file_edit import * # noqa: F403
|
|
44
|
-
from .notes import * # noqa: F403
|
|
45
52
|
from .proxy import * # noqa: F403
|
|
46
53
|
from .python import * # noqa: F403
|
|
47
54
|
from .terminal import * # noqa: F403
|