strix-agent 0.1.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.
- strix/__init__.py +0 -0
- strix/agents/StrixAgent/__init__.py +4 -0
- strix/agents/StrixAgent/strix_agent.py +60 -0
- strix/agents/StrixAgent/system_prompt.jinja +504 -0
- strix/agents/__init__.py +10 -0
- strix/agents/base_agent.py +394 -0
- strix/agents/state.py +139 -0
- strix/cli/__init__.py +4 -0
- strix/cli/app.py +1124 -0
- strix/cli/assets/cli.tcss +680 -0
- strix/cli/main.py +542 -0
- strix/cli/tool_components/__init__.py +39 -0
- strix/cli/tool_components/agents_graph_renderer.py +129 -0
- strix/cli/tool_components/base_renderer.py +61 -0
- strix/cli/tool_components/browser_renderer.py +107 -0
- strix/cli/tool_components/file_edit_renderer.py +95 -0
- strix/cli/tool_components/finish_renderer.py +32 -0
- strix/cli/tool_components/notes_renderer.py +108 -0
- strix/cli/tool_components/proxy_renderer.py +255 -0
- strix/cli/tool_components/python_renderer.py +34 -0
- strix/cli/tool_components/registry.py +72 -0
- strix/cli/tool_components/reporting_renderer.py +53 -0
- strix/cli/tool_components/scan_info_renderer.py +58 -0
- strix/cli/tool_components/terminal_renderer.py +99 -0
- strix/cli/tool_components/thinking_renderer.py +29 -0
- strix/cli/tool_components/user_message_renderer.py +43 -0
- strix/cli/tool_components/web_search_renderer.py +28 -0
- strix/cli/tracer.py +308 -0
- strix/llm/__init__.py +14 -0
- strix/llm/config.py +19 -0
- strix/llm/llm.py +310 -0
- strix/llm/memory_compressor.py +206 -0
- strix/llm/request_queue.py +63 -0
- strix/llm/utils.py +84 -0
- strix/prompts/__init__.py +113 -0
- strix/prompts/coordination/root_agent.jinja +41 -0
- strix/prompts/vulnerabilities/authentication_jwt.jinja +129 -0
- strix/prompts/vulnerabilities/business_logic.jinja +143 -0
- strix/prompts/vulnerabilities/csrf.jinja +168 -0
- strix/prompts/vulnerabilities/idor.jinja +164 -0
- strix/prompts/vulnerabilities/race_conditions.jinja +194 -0
- strix/prompts/vulnerabilities/rce.jinja +222 -0
- strix/prompts/vulnerabilities/sql_injection.jinja +216 -0
- strix/prompts/vulnerabilities/ssrf.jinja +168 -0
- strix/prompts/vulnerabilities/xss.jinja +221 -0
- strix/prompts/vulnerabilities/xxe.jinja +276 -0
- strix/runtime/__init__.py +19 -0
- strix/runtime/docker_runtime.py +298 -0
- strix/runtime/runtime.py +25 -0
- strix/runtime/tool_server.py +97 -0
- strix/tools/__init__.py +64 -0
- strix/tools/agents_graph/__init__.py +16 -0
- strix/tools/agents_graph/agents_graph_actions.py +610 -0
- strix/tools/agents_graph/agents_graph_actions_schema.xml +223 -0
- strix/tools/argument_parser.py +120 -0
- strix/tools/browser/__init__.py +4 -0
- strix/tools/browser/browser_actions.py +236 -0
- strix/tools/browser/browser_actions_schema.xml +183 -0
- strix/tools/browser/browser_instance.py +533 -0
- strix/tools/browser/tab_manager.py +342 -0
- strix/tools/executor.py +302 -0
- strix/tools/file_edit/__init__.py +4 -0
- strix/tools/file_edit/file_edit_actions.py +141 -0
- strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
- strix/tools/finish/__init__.py +4 -0
- strix/tools/finish/finish_actions.py +167 -0
- strix/tools/finish/finish_actions_schema.xml +45 -0
- strix/tools/notes/__init__.py +14 -0
- strix/tools/notes/notes_actions.py +191 -0
- strix/tools/notes/notes_actions_schema.xml +150 -0
- strix/tools/proxy/__init__.py +20 -0
- strix/tools/proxy/proxy_actions.py +101 -0
- strix/tools/proxy/proxy_actions_schema.xml +267 -0
- strix/tools/proxy/proxy_manager.py +785 -0
- strix/tools/python/__init__.py +4 -0
- strix/tools/python/python_actions.py +47 -0
- strix/tools/python/python_actions_schema.xml +131 -0
- strix/tools/python/python_instance.py +172 -0
- strix/tools/python/python_manager.py +131 -0
- strix/tools/registry.py +196 -0
- strix/tools/reporting/__init__.py +6 -0
- strix/tools/reporting/reporting_actions.py +63 -0
- strix/tools/reporting/reporting_actions_schema.xml +30 -0
- strix/tools/terminal/__init__.py +4 -0
- strix/tools/terminal/terminal_actions.py +53 -0
- strix/tools/terminal/terminal_actions_schema.xml +114 -0
- strix/tools/terminal/terminal_instance.py +231 -0
- strix/tools/terminal/terminal_manager.py +191 -0
- strix/tools/thinking/__init__.py +4 -0
- strix/tools/thinking/thinking_actions.py +18 -0
- strix/tools/thinking/thinking_actions_schema.xml +52 -0
- strix/tools/web_search/__init__.py +4 -0
- strix/tools/web_search/web_search_actions.py +80 -0
- strix/tools/web_search/web_search_actions_schema.xml +83 -0
- strix_agent-0.1.1.dist-info/LICENSE +201 -0
- strix_agent-0.1.1.dist-info/METADATA +200 -0
- strix_agent-0.1.1.dist-info/RECORD +99 -0
- strix_agent-0.1.1.dist-info/WHEEL +4 -0
- strix_agent-0.1.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,206 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
import litellm
|
6
|
+
|
7
|
+
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
10
|
+
|
11
|
+
MAX_TOTAL_TOKENS = 100_000
|
12
|
+
MIN_RECENT_MESSAGES = 15
|
13
|
+
|
14
|
+
SUMMARY_PROMPT_TEMPLATE = """You are an agent performing context
|
15
|
+
condensation for a security agent. Your job is to compress scan data while preserving
|
16
|
+
ALL operationally critical information for continuing the security assessment.
|
17
|
+
|
18
|
+
CRITICAL ELEMENTS TO PRESERVE:
|
19
|
+
- Discovered vulnerabilities and potential attack vectors
|
20
|
+
- Scan results and tool outputs (compressed but maintaining key findings)
|
21
|
+
- Access credentials, tokens, or authentication details found
|
22
|
+
- System architecture insights and potential weak points
|
23
|
+
- Progress made in the assessment
|
24
|
+
- Failed attempts and dead ends (to avoid duplication)
|
25
|
+
- Any decisions made about the testing approach
|
26
|
+
|
27
|
+
COMPRESSION GUIDELINES:
|
28
|
+
- Preserve exact technical details (URLs, paths, parameters, payloads)
|
29
|
+
- Summarize verbose tool outputs while keeping critical findings
|
30
|
+
- Maintain version numbers, specific technologies identified
|
31
|
+
- Keep exact error messages that might indicate vulnerabilities
|
32
|
+
- Compress repetitive or similar findings into consolidated form
|
33
|
+
|
34
|
+
Remember: Another security agent will use this summary to continue the assessment.
|
35
|
+
They must be able to pick up exactly where you left off without losing any
|
36
|
+
operational advantage or context needed to find vulnerabilities.
|
37
|
+
|
38
|
+
CONVERSATION SEGMENT TO SUMMARIZE:
|
39
|
+
{conversation}
|
40
|
+
|
41
|
+
Provide a technically precise summary that preserves all operational security context while
|
42
|
+
keeping the summary concise and to the point."""
|
43
|
+
|
44
|
+
|
45
|
+
def _count_tokens(text: str, model: str) -> int:
|
46
|
+
try:
|
47
|
+
count = litellm.token_counter(model=model, text=text)
|
48
|
+
return int(count)
|
49
|
+
except Exception:
|
50
|
+
logger.exception("Failed to count tokens")
|
51
|
+
return len(text) // 4 # Rough estimate
|
52
|
+
|
53
|
+
|
54
|
+
def _get_message_tokens(msg: dict[str, Any], model: str) -> int:
|
55
|
+
content = msg.get("content", "")
|
56
|
+
if isinstance(content, str):
|
57
|
+
return _count_tokens(content, model)
|
58
|
+
if isinstance(content, list):
|
59
|
+
return sum(
|
60
|
+
_count_tokens(item.get("text", ""), model)
|
61
|
+
for item in content
|
62
|
+
if isinstance(item, dict) and item.get("type") == "text"
|
63
|
+
)
|
64
|
+
return 0
|
65
|
+
|
66
|
+
|
67
|
+
def _extract_message_text(msg: dict[str, Any]) -> str:
|
68
|
+
content = msg.get("content", "")
|
69
|
+
if isinstance(content, str):
|
70
|
+
return content
|
71
|
+
|
72
|
+
if isinstance(content, list):
|
73
|
+
parts = []
|
74
|
+
for item in content:
|
75
|
+
if isinstance(item, dict):
|
76
|
+
if item.get("type") == "text":
|
77
|
+
parts.append(item.get("text", ""))
|
78
|
+
elif item.get("type") == "image_url":
|
79
|
+
parts.append("[IMAGE]")
|
80
|
+
return " ".join(parts)
|
81
|
+
|
82
|
+
return str(content)
|
83
|
+
|
84
|
+
|
85
|
+
def _summarize_messages(
|
86
|
+
messages: list[dict[str, Any]],
|
87
|
+
model: str,
|
88
|
+
) -> dict[str, Any]:
|
89
|
+
if not messages:
|
90
|
+
empty_summary = "<context_summary message_count='0'>{text}</context_summary>"
|
91
|
+
return {
|
92
|
+
"role": "assistant",
|
93
|
+
"content": empty_summary.format(text="No messages to summarize"),
|
94
|
+
}
|
95
|
+
|
96
|
+
formatted = []
|
97
|
+
for msg in messages:
|
98
|
+
role = msg.get("role", "unknown")
|
99
|
+
text = _extract_message_text(msg)
|
100
|
+
formatted.append(f"{role}: {text}")
|
101
|
+
|
102
|
+
conversation = "\n".join(formatted)
|
103
|
+
prompt = SUMMARY_PROMPT_TEMPLATE.format(conversation=conversation)
|
104
|
+
|
105
|
+
try:
|
106
|
+
completion_args = {
|
107
|
+
"model": model,
|
108
|
+
"messages": [{"role": "user", "content": prompt}],
|
109
|
+
}
|
110
|
+
|
111
|
+
response = litellm.completion(**completion_args)
|
112
|
+
summary = response.choices[0].message.content
|
113
|
+
summary_msg = "<context_summary message_count='{count}'>{text}</context_summary>"
|
114
|
+
return {
|
115
|
+
"role": "assistant",
|
116
|
+
"content": summary_msg.format(count=len(messages), text=summary),
|
117
|
+
}
|
118
|
+
except Exception:
|
119
|
+
logger.exception("Failed to summarize messages")
|
120
|
+
return messages[0]
|
121
|
+
|
122
|
+
|
123
|
+
def _handle_images(messages: list[dict[str, Any]], max_images: int) -> None:
|
124
|
+
image_count = 0
|
125
|
+
for msg in reversed(messages):
|
126
|
+
content = msg.get("content", [])
|
127
|
+
if isinstance(content, list):
|
128
|
+
for item in content:
|
129
|
+
if isinstance(item, dict) and item.get("type") == "image_url":
|
130
|
+
if image_count >= max_images:
|
131
|
+
item.update(
|
132
|
+
{
|
133
|
+
"type": "text",
|
134
|
+
"text": "[Previously attached image removed to preserve context]",
|
135
|
+
}
|
136
|
+
)
|
137
|
+
else:
|
138
|
+
image_count += 1
|
139
|
+
|
140
|
+
|
141
|
+
class MemoryCompressor:
|
142
|
+
def __init__(
|
143
|
+
self,
|
144
|
+
max_images: int = 3,
|
145
|
+
model_name: str | None = None,
|
146
|
+
):
|
147
|
+
self.max_images = max_images
|
148
|
+
self.model_name = model_name or os.getenv("STRIX_LLM", "anthropic/claude-sonnet-4-20250514")
|
149
|
+
|
150
|
+
if not self.model_name:
|
151
|
+
raise ValueError("STRIX_LLM environment variable must be set and not empty")
|
152
|
+
|
153
|
+
def compress_history(
|
154
|
+
self,
|
155
|
+
messages: list[dict[str, Any]],
|
156
|
+
) -> list[dict[str, Any]]:
|
157
|
+
"""Compress conversation history to stay within token limits.
|
158
|
+
|
159
|
+
Strategy:
|
160
|
+
1. Handle image limits first
|
161
|
+
2. Keep all system messages
|
162
|
+
3. Keep minimum recent messages
|
163
|
+
4. Summarize older messages when total tokens exceed limit
|
164
|
+
|
165
|
+
The compression preserves:
|
166
|
+
- All system messages unchanged
|
167
|
+
- Most recent messages intact
|
168
|
+
- Critical security context in summaries
|
169
|
+
- Recent images for visual context
|
170
|
+
- Technical details and findings
|
171
|
+
"""
|
172
|
+
if not messages:
|
173
|
+
return messages
|
174
|
+
|
175
|
+
_handle_images(messages, self.max_images)
|
176
|
+
|
177
|
+
system_msgs = []
|
178
|
+
regular_msgs = []
|
179
|
+
for msg in messages:
|
180
|
+
if msg.get("role") == "system":
|
181
|
+
system_msgs.append(msg)
|
182
|
+
else:
|
183
|
+
regular_msgs.append(msg)
|
184
|
+
|
185
|
+
recent_msgs = regular_msgs[-MIN_RECENT_MESSAGES:]
|
186
|
+
old_msgs = regular_msgs[:-MIN_RECENT_MESSAGES]
|
187
|
+
|
188
|
+
# Type assertion since we ensure model_name is not None in __init__
|
189
|
+
model_name: str = self.model_name # type: ignore[assignment]
|
190
|
+
|
191
|
+
total_tokens = sum(
|
192
|
+
_get_message_tokens(msg, model_name) for msg in system_msgs + regular_msgs
|
193
|
+
)
|
194
|
+
|
195
|
+
if total_tokens <= MAX_TOTAL_TOKENS * 0.9:
|
196
|
+
return messages
|
197
|
+
|
198
|
+
compressed = []
|
199
|
+
chunk_size = 10
|
200
|
+
for i in range(0, len(old_msgs), chunk_size):
|
201
|
+
chunk = old_msgs[i : i + chunk_size]
|
202
|
+
summary = _summarize_messages(chunk, model_name)
|
203
|
+
if summary:
|
204
|
+
compressed.append(summary)
|
205
|
+
|
206
|
+
return system_msgs + compressed + recent_msgs
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
import threading
|
4
|
+
import time
|
5
|
+
from typing import Any
|
6
|
+
|
7
|
+
from litellm import ModelResponse, completion
|
8
|
+
from tenacity import retry, stop_after_attempt, wait_exponential
|
9
|
+
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
class LLMRequestQueue:
|
15
|
+
def __init__(self, max_concurrent: int = 6, delay_between_requests: float = 1.0):
|
16
|
+
self.max_concurrent = max_concurrent
|
17
|
+
self.delay_between_requests = delay_between_requests
|
18
|
+
self._semaphore = threading.BoundedSemaphore(max_concurrent)
|
19
|
+
self._last_request_time = 0.0
|
20
|
+
self._lock = threading.Lock()
|
21
|
+
|
22
|
+
async def make_request(self, completion_args: dict[str, Any]) -> ModelResponse:
|
23
|
+
try:
|
24
|
+
while not self._semaphore.acquire(timeout=0.2):
|
25
|
+
await asyncio.sleep(0.1)
|
26
|
+
|
27
|
+
with self._lock:
|
28
|
+
now = time.time()
|
29
|
+
time_since_last = now - self._last_request_time
|
30
|
+
sleep_needed = max(0, self.delay_between_requests - time_since_last)
|
31
|
+
self._last_request_time = now + sleep_needed
|
32
|
+
|
33
|
+
if sleep_needed > 0:
|
34
|
+
await asyncio.sleep(sleep_needed)
|
35
|
+
|
36
|
+
return await self._reliable_request(completion_args)
|
37
|
+
finally:
|
38
|
+
self._semaphore.release()
|
39
|
+
|
40
|
+
@retry( # type: ignore[misc]
|
41
|
+
stop=stop_after_attempt(15),
|
42
|
+
wait=wait_exponential(multiplier=1.2, min=1, max=300),
|
43
|
+
reraise=True,
|
44
|
+
)
|
45
|
+
async def _reliable_request(self, completion_args: dict[str, Any]) -> ModelResponse:
|
46
|
+
response = completion(**completion_args, stream=False)
|
47
|
+
if isinstance(response, ModelResponse):
|
48
|
+
return response
|
49
|
+
self._raise_unexpected_response()
|
50
|
+
raise RuntimeError("Unreachable code")
|
51
|
+
|
52
|
+
def _raise_unexpected_response(self) -> None:
|
53
|
+
raise RuntimeError("Unexpected response type")
|
54
|
+
|
55
|
+
|
56
|
+
_global_queue: LLMRequestQueue | None = None
|
57
|
+
|
58
|
+
|
59
|
+
def get_global_queue() -> LLMRequestQueue:
|
60
|
+
global _global_queue # noqa: PLW0603
|
61
|
+
if _global_queue is None:
|
62
|
+
_global_queue = LLMRequestQueue()
|
63
|
+
return _global_queue
|
strix/llm/utils.py
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
|
5
|
+
def _truncate_to_first_function(content: str) -> str:
|
6
|
+
if not content:
|
7
|
+
return content
|
8
|
+
|
9
|
+
function_starts = [match.start() for match in re.finditer(r"<function=", content)]
|
10
|
+
|
11
|
+
if len(function_starts) >= 2:
|
12
|
+
second_function_start = function_starts[1]
|
13
|
+
|
14
|
+
return content[:second_function_start].rstrip()
|
15
|
+
|
16
|
+
return content
|
17
|
+
|
18
|
+
|
19
|
+
def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
|
20
|
+
content = _fix_stopword(content)
|
21
|
+
|
22
|
+
tool_invocations: list[dict[str, Any]] = []
|
23
|
+
|
24
|
+
fn_regex_pattern = r"<function=([^>]+)>\n?(.*?)</function>"
|
25
|
+
fn_param_regex_pattern = r"<parameter=([^>]+)>(.*?)</parameter>"
|
26
|
+
|
27
|
+
fn_matches = re.finditer(fn_regex_pattern, content, re.DOTALL)
|
28
|
+
|
29
|
+
for fn_match in fn_matches:
|
30
|
+
fn_name = fn_match.group(1)
|
31
|
+
fn_body = fn_match.group(2)
|
32
|
+
|
33
|
+
param_matches = re.finditer(fn_param_regex_pattern, fn_body, re.DOTALL)
|
34
|
+
|
35
|
+
args = {}
|
36
|
+
for param_match in param_matches:
|
37
|
+
param_name = param_match.group(1)
|
38
|
+
param_value = param_match.group(2).strip()
|
39
|
+
args[param_name] = param_value
|
40
|
+
|
41
|
+
tool_invocations.append({"toolName": fn_name, "args": args})
|
42
|
+
|
43
|
+
return tool_invocations if tool_invocations else None
|
44
|
+
|
45
|
+
|
46
|
+
def _fix_stopword(content: str) -> str:
|
47
|
+
if "<function=" in content and content.count("<function=") == 1:
|
48
|
+
if content.endswith("</"):
|
49
|
+
content = content.rstrip() + "function>"
|
50
|
+
elif not content.rstrip().endswith("</function>"):
|
51
|
+
content = content + "\n</function>"
|
52
|
+
return content
|
53
|
+
|
54
|
+
|
55
|
+
def format_tool_call(tool_name: str, args: dict[str, Any]) -> str:
|
56
|
+
xml_parts = [f"<function={tool_name}>"]
|
57
|
+
|
58
|
+
for key, value in args.items():
|
59
|
+
xml_parts.append(f"<parameter={key}>{value}</parameter>")
|
60
|
+
|
61
|
+
xml_parts.append("</function>")
|
62
|
+
|
63
|
+
return "\n".join(xml_parts)
|
64
|
+
|
65
|
+
|
66
|
+
def clean_content(content: str) -> str:
|
67
|
+
if not content:
|
68
|
+
return ""
|
69
|
+
|
70
|
+
content = _fix_stopword(content)
|
71
|
+
|
72
|
+
tool_pattern = r"<function=[^>]+>.*?</function>"
|
73
|
+
cleaned = re.sub(tool_pattern, "", content, flags=re.DOTALL)
|
74
|
+
|
75
|
+
hidden_xml_patterns = [
|
76
|
+
r"<inter_agent_message>.*?</inter_agent_message>",
|
77
|
+
r"<agent_completion_report>.*?</agent_completion_report>",
|
78
|
+
]
|
79
|
+
for pattern in hidden_xml_patterns:
|
80
|
+
cleaned = re.sub(pattern, "", cleaned, flags=re.DOTALL | re.IGNORECASE)
|
81
|
+
|
82
|
+
cleaned = re.sub(r"\n\s*\n", "\n\n", cleaned)
|
83
|
+
|
84
|
+
return cleaned.strip()
|
@@ -0,0 +1,113 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from jinja2 import Environment
|
4
|
+
|
5
|
+
|
6
|
+
def get_available_prompt_modules() -> dict[str, list[str]]:
|
7
|
+
modules_dir = Path(__file__).parent
|
8
|
+
available_modules = {}
|
9
|
+
|
10
|
+
for category_dir in modules_dir.iterdir():
|
11
|
+
if category_dir.is_dir() and not category_dir.name.startswith("__"):
|
12
|
+
category_name = category_dir.name
|
13
|
+
modules = []
|
14
|
+
|
15
|
+
for file_path in category_dir.glob("*.jinja"):
|
16
|
+
module_name = file_path.stem
|
17
|
+
modules.append(module_name)
|
18
|
+
|
19
|
+
if modules:
|
20
|
+
available_modules[category_name] = sorted(modules)
|
21
|
+
|
22
|
+
return available_modules
|
23
|
+
|
24
|
+
|
25
|
+
def get_all_module_names() -> set[str]:
|
26
|
+
all_modules = set()
|
27
|
+
for category_modules in get_available_prompt_modules().values():
|
28
|
+
all_modules.update(category_modules)
|
29
|
+
return all_modules
|
30
|
+
|
31
|
+
|
32
|
+
def validate_module_names(module_names: list[str]) -> dict[str, list[str]]:
|
33
|
+
available_modules = get_all_module_names()
|
34
|
+
valid_modules = []
|
35
|
+
invalid_modules = []
|
36
|
+
|
37
|
+
for module_name in module_names:
|
38
|
+
if module_name in available_modules:
|
39
|
+
valid_modules.append(module_name)
|
40
|
+
else:
|
41
|
+
invalid_modules.append(module_name)
|
42
|
+
|
43
|
+
return {"valid": valid_modules, "invalid": invalid_modules}
|
44
|
+
|
45
|
+
|
46
|
+
def generate_modules_description() -> str:
|
47
|
+
available_modules = get_available_prompt_modules()
|
48
|
+
|
49
|
+
if not available_modules:
|
50
|
+
return "No prompt modules available"
|
51
|
+
|
52
|
+
description_parts = []
|
53
|
+
|
54
|
+
for category, modules in available_modules.items():
|
55
|
+
modules_str = ", ".join(modules)
|
56
|
+
description_parts.append(f"{category} ({modules_str})")
|
57
|
+
|
58
|
+
description = (
|
59
|
+
f"List of prompt modules to load for this agent (max 3). "
|
60
|
+
f"Available modules: {', '.join(description_parts)}. "
|
61
|
+
)
|
62
|
+
|
63
|
+
example_modules = []
|
64
|
+
for modules in available_modules.values():
|
65
|
+
example_modules.extend(modules[:2])
|
66
|
+
if len(example_modules) >= 2:
|
67
|
+
break
|
68
|
+
|
69
|
+
if example_modules:
|
70
|
+
example = f"Example: {example_modules[:2]} for specialized agent"
|
71
|
+
description += example
|
72
|
+
|
73
|
+
return description
|
74
|
+
|
75
|
+
|
76
|
+
def load_prompt_modules(module_names: list[str], jinja_env: Environment) -> dict[str, str]:
|
77
|
+
import logging
|
78
|
+
|
79
|
+
logger = logging.getLogger(__name__)
|
80
|
+
module_content = {}
|
81
|
+
prompts_dir = Path(__file__).parent
|
82
|
+
|
83
|
+
available_modules = get_available_prompt_modules()
|
84
|
+
|
85
|
+
for module_name in module_names:
|
86
|
+
try:
|
87
|
+
module_path = None
|
88
|
+
|
89
|
+
if "/" in module_name:
|
90
|
+
module_path = f"{module_name}.jinja"
|
91
|
+
else:
|
92
|
+
for category, modules in available_modules.items():
|
93
|
+
if module_name in modules:
|
94
|
+
module_path = f"{category}/{module_name}.jinja"
|
95
|
+
break
|
96
|
+
|
97
|
+
if not module_path:
|
98
|
+
root_candidate = f"{module_name}.jinja"
|
99
|
+
if (prompts_dir / root_candidate).exists():
|
100
|
+
module_path = root_candidate
|
101
|
+
|
102
|
+
if module_path and (prompts_dir / module_path).exists():
|
103
|
+
template = jinja_env.get_template(module_path)
|
104
|
+
var_name = module_name.split("/")[-1]
|
105
|
+
module_content[var_name] = template.render()
|
106
|
+
logger.info(f"Loaded prompt module: {module_name} -> {var_name}")
|
107
|
+
else:
|
108
|
+
logger.warning(f"Prompt module not found: {module_name}")
|
109
|
+
|
110
|
+
except (FileNotFoundError, OSError, ValueError) as e:
|
111
|
+
logger.warning(f"Failed to load prompt module {module_name}: {e}")
|
112
|
+
|
113
|
+
return module_content
|
@@ -0,0 +1,41 @@
|
|
1
|
+
<coordination_role>
|
2
|
+
You are a COORDINATION AGENT ONLY. You do NOT perform any security testing, vulnerability assessment, or technical work yourself.
|
3
|
+
|
4
|
+
Your ONLY responsibilities:
|
5
|
+
1. Create specialized agents for specific security tasks
|
6
|
+
2. Monitor agent progress and coordinate between them
|
7
|
+
3. Compile final scan reports from agent findings
|
8
|
+
4. Manage agent communication and dependencies
|
9
|
+
|
10
|
+
CRITICAL RESTRICTIONS:
|
11
|
+
- NEVER perform vulnerability testing or security assessments
|
12
|
+
- NEVER write detailed vulnerability reports (only compile final summaries)
|
13
|
+
- ONLY use agent_graph and finish tools for coordination
|
14
|
+
- You can create agents throughout the scan process, depending on the task and findings, not just at the beginning!
|
15
|
+
</coordination_role>
|
16
|
+
|
17
|
+
<agent_management>
|
18
|
+
BEFORE CREATING AGENTS:
|
19
|
+
1. Analyze the target scope and break into independent tasks
|
20
|
+
2. Check existing agents to avoid duplication
|
21
|
+
3. Create agents with clear, specific objectives to avoid duplication
|
22
|
+
|
23
|
+
AGENT TYPES YOU CAN CREATE:
|
24
|
+
- Reconnaissance: subdomain enum, port scanning, tech identification, etc.
|
25
|
+
- Vulnerability Testing: SQL injection, XSS, auth bypass, IDOR, RCE, SSRF, etc. Can be black-box or white-box.
|
26
|
+
- Direct vulnerability testing agents to implement hierarchical workflow (per finding: discover, verify, report, fix): each one should create validation agents for findings verification, which spawn reporting agents for documentation, which create fix agents for remediation
|
27
|
+
|
28
|
+
COORDINATION GUIDELINES:
|
29
|
+
- Ensure clear task boundaries and success criteria
|
30
|
+
- Terminate redundant agents when objectives overlap
|
31
|
+
- Use message passing for agent communication
|
32
|
+
</agent_management>
|
33
|
+
|
34
|
+
<final_responsibilities>
|
35
|
+
When all agents complete:
|
36
|
+
1. Collect findings from all agents
|
37
|
+
2. Compile a final scan summary report
|
38
|
+
3. Use finish tool to complete the assessment
|
39
|
+
|
40
|
+
Your value is in orchestration, not execution.
|
41
|
+
</final_responsibilities>
|
@@ -0,0 +1,129 @@
|
|
1
|
+
<authentication_jwt_guide>
|
2
|
+
<title>AUTHENTICATION & JWT VULNERABILITIES</title>
|
3
|
+
|
4
|
+
<critical>Authentication flaws lead to complete account takeover. JWT misconfigurations are everywhere.</critical>
|
5
|
+
|
6
|
+
<jwt_structure>
|
7
|
+
header.payload.signature
|
8
|
+
- Header: {"alg":"HS256","typ":"JWT"}
|
9
|
+
- Payload: {"sub":"1234","name":"John","iat":1516239022}
|
10
|
+
- Signature: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
|
11
|
+
</jwt_structure>
|
12
|
+
|
13
|
+
<common_attacks>
|
14
|
+
<algorithm_confusion>
|
15
|
+
RS256 to HS256:
|
16
|
+
- Change RS256 to HS256 in header
|
17
|
+
- Use public key as HMAC secret
|
18
|
+
- Sign token with public key (often in /jwks.json or /.well-known/)
|
19
|
+
</algorithm_confusion>
|
20
|
+
|
21
|
+
<none_algorithm>
|
22
|
+
- Set "alg": "none" in header
|
23
|
+
- Remove signature completely (keep the trailing dot)
|
24
|
+
</none_algorithm>
|
25
|
+
|
26
|
+
<weak_secrets>
|
27
|
+
Common secrets: 'secret', 'password', '123456', 'key', 'jwt_secret', 'your-256-bit-secret'
|
28
|
+
</weak_secrets>
|
29
|
+
|
30
|
+
<kid_manipulation>
|
31
|
+
- SQL Injection: "kid": "key' UNION SELECT 'secret'--"
|
32
|
+
- Command injection: "kid": "|sleep 10"
|
33
|
+
- Path traversal: "kid": "../../../../../../dev/null"
|
34
|
+
</kid_manipulation>
|
35
|
+
</common_attacks>
|
36
|
+
|
37
|
+
<advanced_techniques>
|
38
|
+
<jwk_injection>
|
39
|
+
Embed public key in token header:
|
40
|
+
{"jwk": {"kty": "RSA", "n": "your-public-key-n", "e": "AQAB"}}
|
41
|
+
</jwk_injection>
|
42
|
+
|
43
|
+
<jku_manipulation>
|
44
|
+
Set jku/x5u to attacker-controlled URL hosting malicious JWKS
|
45
|
+
</jku_manipulation>
|
46
|
+
|
47
|
+
<timing_attacks>
|
48
|
+
Extract signature byte-by-byte using verification timing differences
|
49
|
+
</timing_attacks>
|
50
|
+
</advanced_techniques>
|
51
|
+
|
52
|
+
<oauth_vulnerabilities>
|
53
|
+
<authorization_code_theft>
|
54
|
+
- Exploit redirect_uri with open redirects, subdomain takeover, parameter pollution
|
55
|
+
- Missing/predictable state parameter = CSRF
|
56
|
+
- PKCE downgrade: remove code_challenge parameter
|
57
|
+
</authorization_code_theft>
|
58
|
+
</oauth_vulnerabilities>
|
59
|
+
|
60
|
+
<saml_attacks>
|
61
|
+
- Signature exclusion: remove signature element
|
62
|
+
- Signature wrapping: inject assertions
|
63
|
+
- XXE in SAML responses
|
64
|
+
</saml_attacks>
|
65
|
+
|
66
|
+
<session_attacks>
|
67
|
+
- Session fixation: force known session ID
|
68
|
+
- Session puzzling: mix different session objects
|
69
|
+
- Race conditions in session generation
|
70
|
+
</session_attacks>
|
71
|
+
|
72
|
+
<password_reset_flaws>
|
73
|
+
- Predictable tokens: MD5(timestamp), sequential numbers
|
74
|
+
- Host header injection for reset link poisoning
|
75
|
+
- Race condition resets
|
76
|
+
</password_reset_flaws>
|
77
|
+
|
78
|
+
<mfa_bypass>
|
79
|
+
- Response manipulation: change success:false to true
|
80
|
+
- Status code manipulation: 403 to 200
|
81
|
+
- Brute force with no rate limiting
|
82
|
+
- Backup code abuse
|
83
|
+
</mfa_bypass>
|
84
|
+
|
85
|
+
<advanced_bypasses>
|
86
|
+
<unicode_normalization>
|
87
|
+
Different representations: admin@example.com (fullwidth), аdmin@example.com (Cyrillic)
|
88
|
+
</unicode_normalization>
|
89
|
+
|
90
|
+
<authentication_chaining>
|
91
|
+
- JWT + SQLi: kid parameter with SQL injection
|
92
|
+
- OAuth + XSS: steal tokens via XSS
|
93
|
+
- SAML + XXE + SSRF: chain for internal access
|
94
|
+
</authentication_chaining>
|
95
|
+
</advanced_bypasses>
|
96
|
+
|
97
|
+
<tools>
|
98
|
+
- jwt_tool: Comprehensive JWT testing
|
99
|
+
- Check endpoints: /login, /oauth/authorize, /saml/login, /.well-known/openid-configuration, /jwks.json
|
100
|
+
</tools>
|
101
|
+
|
102
|
+
<validation>
|
103
|
+
To confirm authentication flaw:
|
104
|
+
1. Demonstrate account access without credentials
|
105
|
+
2. Show privilege escalation
|
106
|
+
3. Prove token forgery works
|
107
|
+
4. Bypass authentication/2FA requirements
|
108
|
+
5. Maintain persistent access
|
109
|
+
</validation>
|
110
|
+
|
111
|
+
<false_positives>
|
112
|
+
NOT a vulnerability if:
|
113
|
+
- Requires valid credentials
|
114
|
+
- Only affects own session
|
115
|
+
- Proper signature validation
|
116
|
+
- Token expiration enforced
|
117
|
+
- Rate limiting prevents brute force
|
118
|
+
</false_positives>
|
119
|
+
|
120
|
+
<impact>
|
121
|
+
- Account takeover: access other users' accounts
|
122
|
+
- Privilege escalation: user to admin
|
123
|
+
- Token forgery: create valid tokens
|
124
|
+
- Bypass mechanisms: skip auth/2FA
|
125
|
+
- Persistent access: survives logout
|
126
|
+
</impact>
|
127
|
+
|
128
|
+
<remember>Focus on RS256->HS256, weak secrets, and none algorithm first. Modern apps use multiple auth methods simultaneously - find gaps in integration.</remember>
|
129
|
+
</authentication_jwt_guide>
|