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.
Files changed (117) hide show
  1. strix/agents/StrixAgent/strix_agent.py +3 -3
  2. strix/agents/StrixAgent/system_prompt.jinja +30 -26
  3. strix/agents/base_agent.py +159 -75
  4. strix/agents/state.py +5 -2
  5. strix/config/__init__.py +12 -0
  6. strix/config/config.py +172 -0
  7. strix/interface/assets/tui_styles.tcss +195 -230
  8. strix/interface/cli.py +16 -41
  9. strix/interface/main.py +151 -74
  10. strix/interface/streaming_parser.py +119 -0
  11. strix/interface/tool_components/__init__.py +4 -0
  12. strix/interface/tool_components/agent_message_renderer.py +190 -0
  13. strix/interface/tool_components/agents_graph_renderer.py +54 -38
  14. strix/interface/tool_components/base_renderer.py +68 -36
  15. strix/interface/tool_components/browser_renderer.py +106 -91
  16. strix/interface/tool_components/file_edit_renderer.py +117 -36
  17. strix/interface/tool_components/finish_renderer.py +43 -10
  18. strix/interface/tool_components/notes_renderer.py +63 -38
  19. strix/interface/tool_components/proxy_renderer.py +133 -92
  20. strix/interface/tool_components/python_renderer.py +121 -8
  21. strix/interface/tool_components/registry.py +19 -12
  22. strix/interface/tool_components/reporting_renderer.py +196 -28
  23. strix/interface/tool_components/scan_info_renderer.py +22 -19
  24. strix/interface/tool_components/terminal_renderer.py +270 -90
  25. strix/interface/tool_components/thinking_renderer.py +8 -6
  26. strix/interface/tool_components/todo_renderer.py +225 -0
  27. strix/interface/tool_components/user_message_renderer.py +26 -19
  28. strix/interface/tool_components/web_search_renderer.py +7 -6
  29. strix/interface/tui.py +907 -262
  30. strix/interface/utils.py +236 -4
  31. strix/llm/__init__.py +6 -2
  32. strix/llm/config.py +8 -5
  33. strix/llm/dedupe.py +217 -0
  34. strix/llm/llm.py +209 -356
  35. strix/llm/memory_compressor.py +6 -5
  36. strix/llm/utils.py +17 -8
  37. strix/runtime/__init__.py +12 -3
  38. strix/runtime/docker_runtime.py +121 -202
  39. strix/runtime/tool_server.py +55 -95
  40. strix/skills/README.md +64 -0
  41. strix/skills/__init__.py +110 -0
  42. strix/{prompts → skills}/frameworks/nextjs.jinja +26 -0
  43. strix/skills/scan_modes/deep.jinja +145 -0
  44. strix/skills/scan_modes/quick.jinja +63 -0
  45. strix/skills/scan_modes/standard.jinja +91 -0
  46. strix/telemetry/README.md +38 -0
  47. strix/telemetry/__init__.py +7 -1
  48. strix/telemetry/posthog.py +137 -0
  49. strix/telemetry/tracer.py +194 -54
  50. strix/tools/__init__.py +11 -4
  51. strix/tools/agents_graph/agents_graph_actions.py +20 -21
  52. strix/tools/agents_graph/agents_graph_actions_schema.xml +8 -8
  53. strix/tools/browser/browser_actions.py +10 -6
  54. strix/tools/browser/browser_actions_schema.xml +6 -1
  55. strix/tools/browser/browser_instance.py +96 -48
  56. strix/tools/browser/tab_manager.py +121 -102
  57. strix/tools/context.py +12 -0
  58. strix/tools/executor.py +63 -4
  59. strix/tools/file_edit/file_edit_actions.py +6 -3
  60. strix/tools/file_edit/file_edit_actions_schema.xml +45 -3
  61. strix/tools/finish/finish_actions.py +80 -105
  62. strix/tools/finish/finish_actions_schema.xml +121 -14
  63. strix/tools/notes/notes_actions.py +6 -33
  64. strix/tools/notes/notes_actions_schema.xml +50 -46
  65. strix/tools/proxy/proxy_actions.py +14 -2
  66. strix/tools/proxy/proxy_actions_schema.xml +0 -1
  67. strix/tools/proxy/proxy_manager.py +28 -16
  68. strix/tools/python/python_actions.py +2 -2
  69. strix/tools/python/python_actions_schema.xml +9 -1
  70. strix/tools/python/python_instance.py +39 -37
  71. strix/tools/python/python_manager.py +43 -31
  72. strix/tools/registry.py +73 -12
  73. strix/tools/reporting/reporting_actions.py +218 -31
  74. strix/tools/reporting/reporting_actions_schema.xml +256 -8
  75. strix/tools/terminal/terminal_actions.py +2 -2
  76. strix/tools/terminal/terminal_actions_schema.xml +6 -0
  77. strix/tools/terminal/terminal_manager.py +41 -30
  78. strix/tools/thinking/thinking_actions_schema.xml +27 -25
  79. strix/tools/todo/__init__.py +18 -0
  80. strix/tools/todo/todo_actions.py +568 -0
  81. strix/tools/todo/todo_actions_schema.xml +225 -0
  82. strix/utils/__init__.py +0 -0
  83. strix/utils/resource_paths.py +13 -0
  84. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/METADATA +90 -65
  85. strix_agent-0.6.2.dist-info/RECORD +134 -0
  86. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/WHEEL +1 -1
  87. strix/llm/request_queue.py +0 -87
  88. strix/prompts/README.md +0 -64
  89. strix/prompts/__init__.py +0 -109
  90. strix_agent-0.4.0.dist-info/RECORD +0 -118
  91. /strix/{prompts → skills}/cloud/.gitkeep +0 -0
  92. /strix/{prompts → skills}/coordination/root_agent.jinja +0 -0
  93. /strix/{prompts → skills}/custom/.gitkeep +0 -0
  94. /strix/{prompts → skills}/frameworks/fastapi.jinja +0 -0
  95. /strix/{prompts → skills}/protocols/graphql.jinja +0 -0
  96. /strix/{prompts → skills}/reconnaissance/.gitkeep +0 -0
  97. /strix/{prompts → skills}/technologies/firebase_firestore.jinja +0 -0
  98. /strix/{prompts → skills}/technologies/supabase.jinja +0 -0
  99. /strix/{prompts → skills}/vulnerabilities/authentication_jwt.jinja +0 -0
  100. /strix/{prompts → skills}/vulnerabilities/broken_function_level_authorization.jinja +0 -0
  101. /strix/{prompts → skills}/vulnerabilities/business_logic.jinja +0 -0
  102. /strix/{prompts → skills}/vulnerabilities/csrf.jinja +0 -0
  103. /strix/{prompts → skills}/vulnerabilities/idor.jinja +0 -0
  104. /strix/{prompts → skills}/vulnerabilities/information_disclosure.jinja +0 -0
  105. /strix/{prompts → skills}/vulnerabilities/insecure_file_uploads.jinja +0 -0
  106. /strix/{prompts → skills}/vulnerabilities/mass_assignment.jinja +0 -0
  107. /strix/{prompts → skills}/vulnerabilities/open_redirect.jinja +0 -0
  108. /strix/{prompts → skills}/vulnerabilities/path_traversal_lfi_rfi.jinja +0 -0
  109. /strix/{prompts → skills}/vulnerabilities/race_conditions.jinja +0 -0
  110. /strix/{prompts → skills}/vulnerabilities/rce.jinja +0 -0
  111. /strix/{prompts → skills}/vulnerabilities/sql_injection.jinja +0 -0
  112. /strix/{prompts → skills}/vulnerabilities/ssrf.jinja +0 -0
  113. /strix/{prompts → skills}/vulnerabilities/subdomain_takeover.jinja +0 -0
  114. /strix/{prompts → skills}/vulnerabilities/xss.jinja +0 -0
  115. /strix/{prompts → skills}/vulnerabilities/xxe.jinja +0 -0
  116. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/entry_points.txt +0 -0
  117. {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.
@@ -1,4 +1,10 @@
1
+ from . import posthog
1
2
  from .tracer import Tracer, get_global_tracer, set_global_tracer
2
3
 
3
4
 
4
- __all__ = ["Tracer", "get_global_tracer", "set_global_tracer"]
5
+ __all__ = [
6
+ "Tracer",
7
+ "get_global_tracer",
8
+ "posthog",
9
+ "set_global_tracer",
10
+ ]
@@ -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, str, str, str], None] | None = None
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 set_final_scan_result(
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
- content: str,
102
- success: bool = True,
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
- "content": content,
109
- "success": success,
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
- logger.info(f"Set final scan result: success={success}")
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['title']}\n\n")
237
- f.write(f"**ID:** {report['id']}\n")
238
- f.write(f"**Severity:** {report['severity'].upper()}\n")
239
- f.write(f"**Found:** {report['timestamp']}\n\n")
240
- f.write("## Description\n\n")
241
- f.write(f"{report['content']}\n")
242
- self._saved_vuln_ids.add(report["id"])
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
- if self.vulnerability_reports:
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
- vuln_csv_file = run_dir / "vulnerabilities.csv"
252
- with vuln_csv_file.open("w", encoding="utf-8", newline="") as f:
253
- import csv
254
-
255
- fieldnames = ["id", "title", "severity", "timestamp", "file"]
256
- writer = csv.DictWriter(f, fieldnames=fieldnames)
257
- writer.writeheader()
258
-
259
- for report in sorted_reports:
260
- writer.writerow(
261
- {
262
- "id": report["id"],
263
- "title": report["title"],
264
- "severity": report["severity"].upper(),
265
- "timestamp": report["timestamp"],
266
- "file": f"vulnerabilities/{report['id']}.md",
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(os.getenv("PERPLEXITY_API_KEY"))
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
- from .browser import * # noqa: F403
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
- from .browser import * # noqa: F403
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