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
strix/tools/registry.py CHANGED
@@ -7,9 +7,14 @@ from inspect import signature
7
7
  from pathlib import Path
8
8
  from typing import Any
9
9
 
10
+ import defusedxml.ElementTree as DefusedET
11
+
12
+ from strix.utils.resource_paths import get_strix_resource_path
13
+
10
14
 
11
15
  tools: list[dict[str, Any]] = []
12
16
  _tools_by_name: dict[str, Callable[..., Any]] = {}
17
+ _tool_param_schemas: dict[str, dict[str, Any]] = {}
13
18
  logger = logging.getLogger(__name__)
14
19
 
15
20
 
@@ -23,17 +28,17 @@ class ImplementedInClientSideOnlyError(Exception):
23
28
 
24
29
 
25
30
  def _process_dynamic_content(content: str) -> str:
26
- if "{{DYNAMIC_MODULES_DESCRIPTION}}" in content:
31
+ if "{{DYNAMIC_SKILLS_DESCRIPTION}}" in content:
27
32
  try:
28
- from strix.prompts import generate_modules_description
33
+ from strix.skills import generate_skills_description
29
34
 
30
- modules_description = generate_modules_description()
31
- content = content.replace("{{DYNAMIC_MODULES_DESCRIPTION}}", modules_description)
35
+ skills_description = generate_skills_description()
36
+ content = content.replace("{{DYNAMIC_SKILLS_DESCRIPTION}}", skills_description)
32
37
  except ImportError:
33
- logger.warning("Could not import prompts utilities for dynamic schema generation")
38
+ logger.warning("Could not import skills utilities for dynamic schema generation")
34
39
  content = content.replace(
35
- "{{DYNAMIC_MODULES_DESCRIPTION}}",
36
- "List of prompt modules to load for this agent (max 5). Module discovery failed.",
40
+ "{{DYNAMIC_SKILLS_DESCRIPTION}}",
41
+ "List of skills to load for this agent (max 5). Skill discovery failed.",
37
42
  )
38
43
 
39
44
  return content
@@ -82,6 +87,34 @@ def _load_xml_schema(path: Path) -> Any:
82
87
  return tools_dict
83
88
 
84
89
 
90
+ def _parse_param_schema(tool_xml: str) -> dict[str, Any]:
91
+ params: set[str] = set()
92
+ required: set[str] = set()
93
+
94
+ params_start = tool_xml.find("<parameters>")
95
+ params_end = tool_xml.find("</parameters>")
96
+
97
+ if params_start == -1 or params_end == -1:
98
+ return {"params": set(), "required": set(), "has_params": False}
99
+
100
+ params_section = tool_xml[params_start : params_end + len("</parameters>")]
101
+
102
+ try:
103
+ root = DefusedET.fromstring(params_section)
104
+ except DefusedET.ParseError:
105
+ return {"params": set(), "required": set(), "has_params": False}
106
+
107
+ for param in root.findall(".//parameter"):
108
+ name = param.attrib.get("name")
109
+ if not name:
110
+ continue
111
+ params.add(name)
112
+ if param.attrib.get("required", "false").lower() == "true":
113
+ required.add(name)
114
+
115
+ return {"params": params, "required": required, "has_params": bool(params or required)}
116
+
117
+
85
118
  def _get_module_name(func: Callable[..., Any]) -> str:
86
119
  module = inspect.getmodule(func)
87
120
  if not module:
@@ -95,6 +128,27 @@ def _get_module_name(func: Callable[..., Any]) -> str:
95
128
  return "unknown"
96
129
 
97
130
 
131
+ def _get_schema_path(func: Callable[..., Any]) -> Path | None:
132
+ module = inspect.getmodule(func)
133
+ if not module or not module.__name__:
134
+ return None
135
+
136
+ module_name = module.__name__
137
+
138
+ if ".tools." not in module_name:
139
+ return None
140
+
141
+ parts = module_name.split(".tools.")[-1].split(".")
142
+ if len(parts) < 2:
143
+ return None
144
+
145
+ folder = parts[0]
146
+ file_stem = parts[1]
147
+ schema_file = f"{file_stem}_schema.xml"
148
+
149
+ return get_strix_resource_path("tools", folder, schema_file)
150
+
151
+
98
152
  def register_tool(
99
153
  func: Callable[..., Any] | None = None, *, sandbox_execution: bool = True
100
154
  ) -> Callable[..., Any]:
@@ -109,11 +163,8 @@ def register_tool(
109
163
  sandbox_mode = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
110
164
  if not sandbox_mode:
111
165
  try:
112
- module_path = Path(inspect.getfile(f))
113
- schema_file_name = f"{module_path.stem}_schema.xml"
114
- schema_path = module_path.parent / schema_file_name
115
-
116
- xml_tools = _load_xml_schema(schema_path)
166
+ schema_path = _get_schema_path(f)
167
+ xml_tools = _load_xml_schema(schema_path) if schema_path else None
117
168
 
118
169
  if xml_tools is not None and f.__name__ in xml_tools:
119
170
  func_dict["xml_schema"] = xml_tools[f.__name__]
@@ -131,6 +182,11 @@ def register_tool(
131
182
  "</tool>"
132
183
  )
133
184
 
185
+ if not sandbox_mode:
186
+ xml_schema = func_dict.get("xml_schema")
187
+ param_schema = _parse_param_schema(xml_schema if isinstance(xml_schema, str) else "")
188
+ _tool_param_schemas[str(func_dict["name"])] = param_schema
189
+
134
190
  tools.append(func_dict)
135
191
  _tools_by_name[str(func_dict["name"])] = f
136
192
 
@@ -153,6 +209,10 @@ def get_tool_names() -> list[str]:
153
209
  return list(_tools_by_name.keys())
154
210
 
155
211
 
212
+ def get_tool_param_schema(name: str) -> dict[str, Any] | None:
213
+ return _tool_param_schemas.get(name)
214
+
215
+
156
216
  def needs_agent_state(tool_name: str) -> bool:
157
217
  tool_func = get_tool_by_name(tool_name)
158
218
  if not tool_func:
@@ -194,3 +254,4 @@ def get_tools_prompt() -> str:
194
254
  def clear_registry() -> None:
195
255
  tools.clear()
196
256
  _tools_by_name.clear()
257
+ _tool_param_schemas.clear()
@@ -3,61 +3,248 @@ from typing import Any
3
3
  from strix.tools.registry import register_tool
4
4
 
5
5
 
6
+ def calculate_cvss_and_severity(
7
+ attack_vector: str,
8
+ attack_complexity: str,
9
+ privileges_required: str,
10
+ user_interaction: str,
11
+ scope: str,
12
+ confidentiality: str,
13
+ integrity: str,
14
+ availability: str,
15
+ ) -> tuple[float, str, str]:
16
+ try:
17
+ from cvss import CVSS3
18
+
19
+ vector = (
20
+ f"CVSS:3.1/AV:{attack_vector}/AC:{attack_complexity}/"
21
+ f"PR:{privileges_required}/UI:{user_interaction}/S:{scope}/"
22
+ f"C:{confidentiality}/I:{integrity}/A:{availability}"
23
+ )
24
+
25
+ c = CVSS3(vector)
26
+ scores = c.scores()
27
+ severities = c.severities()
28
+
29
+ base_score = scores[0]
30
+ base_severity = severities[0]
31
+
32
+ severity = base_severity.lower()
33
+
34
+ except Exception:
35
+ import logging
36
+
37
+ logging.exception("Failed to calculate CVSS")
38
+ return 7.5, "high", ""
39
+ else:
40
+ return base_score, severity, vector
41
+
42
+
43
+ def _validate_required_fields(**kwargs: str | None) -> list[str]:
44
+ validation_errors: list[str] = []
45
+
46
+ required_fields = {
47
+ "title": "Title cannot be empty",
48
+ "description": "Description cannot be empty",
49
+ "impact": "Impact cannot be empty",
50
+ "target": "Target cannot be empty",
51
+ "technical_analysis": "Technical analysis cannot be empty",
52
+ "poc_description": "PoC description cannot be empty",
53
+ "poc_script_code": "PoC script/code is REQUIRED - provide the actual exploit/payload",
54
+ "remediation_steps": "Remediation steps cannot be empty",
55
+ }
56
+
57
+ for field_name, error_msg in required_fields.items():
58
+ value = kwargs.get(field_name)
59
+ if not value or not str(value).strip():
60
+ validation_errors.append(error_msg)
61
+
62
+ return validation_errors
63
+
64
+
65
+ def _validate_cvss_parameters(**kwargs: str) -> list[str]:
66
+ validation_errors: list[str] = []
67
+
68
+ cvss_validations = {
69
+ "attack_vector": ["N", "A", "L", "P"],
70
+ "attack_complexity": ["L", "H"],
71
+ "privileges_required": ["N", "L", "H"],
72
+ "user_interaction": ["N", "R"],
73
+ "scope": ["U", "C"],
74
+ "confidentiality": ["N", "L", "H"],
75
+ "integrity": ["N", "L", "H"],
76
+ "availability": ["N", "L", "H"],
77
+ }
78
+
79
+ for param_name, valid_values in cvss_validations.items():
80
+ value = kwargs.get(param_name)
81
+ if value not in valid_values:
82
+ validation_errors.append(
83
+ f"Invalid {param_name}: {value}. Must be one of: {valid_values}"
84
+ )
85
+
86
+ return validation_errors
87
+
88
+
6
89
  @register_tool(sandbox_execution=False)
7
90
  def create_vulnerability_report(
8
91
  title: str,
9
- content: str,
10
- severity: str,
92
+ description: str,
93
+ impact: str,
94
+ target: str,
95
+ technical_analysis: str,
96
+ poc_description: str,
97
+ poc_script_code: str,
98
+ remediation_steps: str,
99
+ # CVSS Breakdown Components
100
+ attack_vector: str,
101
+ attack_complexity: str,
102
+ privileges_required: str,
103
+ user_interaction: str,
104
+ scope: str,
105
+ confidentiality: str,
106
+ integrity: str,
107
+ availability: str,
108
+ # Optional fields
109
+ endpoint: str | None = None,
110
+ method: str | None = None,
111
+ cve: str | None = None,
112
+ code_file: str | None = None,
113
+ code_before: str | None = None,
114
+ code_after: str | None = None,
115
+ code_diff: str | None = None,
11
116
  ) -> dict[str, Any]:
12
- validation_error = None
13
- if not title or not title.strip():
14
- validation_error = "Title cannot be empty"
15
- elif not content or not content.strip():
16
- validation_error = "Content cannot be empty"
17
- elif not severity or not severity.strip():
18
- validation_error = "Severity cannot be empty"
19
- else:
20
- valid_severities = ["critical", "high", "medium", "low", "info"]
21
- if severity.lower() not in valid_severities:
22
- validation_error = (
23
- f"Invalid severity '{severity}'. Must be one of: {', '.join(valid_severities)}"
24
- )
117
+ validation_errors = _validate_required_fields(
118
+ title=title,
119
+ description=description,
120
+ impact=impact,
121
+ target=target,
122
+ technical_analysis=technical_analysis,
123
+ poc_description=poc_description,
124
+ poc_script_code=poc_script_code,
125
+ remediation_steps=remediation_steps,
126
+ )
127
+
128
+ validation_errors.extend(
129
+ _validate_cvss_parameters(
130
+ attack_vector=attack_vector,
131
+ attack_complexity=attack_complexity,
132
+ privileges_required=privileges_required,
133
+ user_interaction=user_interaction,
134
+ scope=scope,
135
+ confidentiality=confidentiality,
136
+ integrity=integrity,
137
+ availability=availability,
138
+ )
139
+ )
140
+
141
+ if validation_errors:
142
+ return {"success": False, "message": "Validation failed", "errors": validation_errors}
25
143
 
26
- if validation_error:
27
- return {"success": False, "message": validation_error}
144
+ cvss_score, severity, cvss_vector = calculate_cvss_and_severity(
145
+ attack_vector,
146
+ attack_complexity,
147
+ privileges_required,
148
+ user_interaction,
149
+ scope,
150
+ confidentiality,
151
+ integrity,
152
+ availability,
153
+ )
28
154
 
29
155
  try:
30
156
  from strix.telemetry.tracer import get_global_tracer
31
157
 
32
158
  tracer = get_global_tracer()
33
159
  if tracer:
160
+ from strix.llm.dedupe import check_duplicate
161
+
162
+ existing_reports = tracer.get_existing_vulnerabilities()
163
+
164
+ candidate = {
165
+ "title": title,
166
+ "description": description,
167
+ "impact": impact,
168
+ "target": target,
169
+ "technical_analysis": technical_analysis,
170
+ "poc_description": poc_description,
171
+ "poc_script_code": poc_script_code,
172
+ "endpoint": endpoint,
173
+ "method": method,
174
+ }
175
+
176
+ dedupe_result = check_duplicate(candidate, existing_reports)
177
+
178
+ if dedupe_result.get("is_duplicate"):
179
+ duplicate_id = dedupe_result.get("duplicate_id", "")
180
+
181
+ duplicate_title = ""
182
+ for report in existing_reports:
183
+ if report.get("id") == duplicate_id:
184
+ duplicate_title = report.get("title", "Unknown")
185
+ break
186
+
187
+ return {
188
+ "success": False,
189
+ "message": (
190
+ f"Potential duplicate of '{duplicate_title}' "
191
+ f"(id={duplicate_id[:8]}...). Do not re-report the same vulnerability."
192
+ ),
193
+ "duplicate_of": duplicate_id,
194
+ "duplicate_title": duplicate_title,
195
+ "confidence": dedupe_result.get("confidence", 0.0),
196
+ "reason": dedupe_result.get("reason", ""),
197
+ }
198
+
199
+ cvss_breakdown = {
200
+ "attack_vector": attack_vector,
201
+ "attack_complexity": attack_complexity,
202
+ "privileges_required": privileges_required,
203
+ "user_interaction": user_interaction,
204
+ "scope": scope,
205
+ "confidentiality": confidentiality,
206
+ "integrity": integrity,
207
+ "availability": availability,
208
+ }
209
+
34
210
  report_id = tracer.add_vulnerability_report(
35
211
  title=title,
36
- content=content,
212
+ description=description,
37
213
  severity=severity,
214
+ impact=impact,
215
+ target=target,
216
+ technical_analysis=technical_analysis,
217
+ poc_description=poc_description,
218
+ poc_script_code=poc_script_code,
219
+ remediation_steps=remediation_steps,
220
+ cvss=cvss_score,
221
+ cvss_breakdown=cvss_breakdown,
222
+ endpoint=endpoint,
223
+ method=method,
224
+ cve=cve,
225
+ code_file=code_file,
226
+ code_before=code_before,
227
+ code_after=code_after,
228
+ code_diff=code_diff,
38
229
  )
39
230
 
40
231
  return {
41
232
  "success": True,
42
233
  "message": f"Vulnerability report '{title}' created successfully",
43
234
  "report_id": report_id,
44
- "severity": severity.lower(),
235
+ "severity": severity,
236
+ "cvss_score": cvss_score,
45
237
  }
238
+
46
239
  import logging
47
240
 
48
- logging.warning("Global tracer not available - vulnerability report not stored")
241
+ logging.warning("Current tracer not available - vulnerability report not stored")
49
242
 
50
- return { # noqa: TRY300
51
- "success": True,
52
- "message": f"Vulnerability report '{title}' created successfully (not persisted)",
53
- "warning": "Report could not be persisted - tracer unavailable",
54
- }
55
-
56
- except ImportError:
243
+ except (ImportError, AttributeError) as e:
244
+ return {"success": False, "message": f"Failed to create vulnerability report: {e!s}"}
245
+ else:
57
246
  return {
58
247
  "success": True,
59
- "message": f"Vulnerability report '{title}' created successfully (not persisted)",
60
- "warning": "Report could not be persisted - tracer module unavailable",
248
+ "message": f"Vulnerability report '{title}' created (not persisted)",
249
+ "warning": "Report could not be persisted - tracer unavailable",
61
250
  }
62
- except (ValueError, TypeError) as e:
63
- return {"success": False, "message": f"Failed to create vulnerability report: {e!s}"}