agentsentinel-cli 0.9.4__tar.gz → 0.9.5__tar.gz
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.
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/PKG-INFO +1 -1
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/mcp_fuzz.py +23 -5
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/mcp_inject.py +29 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/mcp_poison.py +72 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/mcp_recon.py +15 -2
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/models.py +1 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/payloads.py +8 -3
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/report.py +88 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/pyproject.toml +1 -1
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/.gitignore +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/DOCUMENTATION.md +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/LICENSE +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/README.md +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/__init__.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/a2a_report.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/a2a_rules.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/a2a_scanner.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/cli.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/discover.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/discover_report.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/fingerprint.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/frameworks.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/host_report.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/host_rules.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/host_scanner.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/inspect.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/inspect_report.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/mcp_client.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/mcp_report.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/mcp_rules.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/__init__.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/mcp_auth.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/transport.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/report.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/rules.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/scanner.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/secrets.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/secrets_report.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/secrets_rules.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/supply_chain_ai.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/supply_chain_report.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/supply_chain_rules.py +0 -0
- {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/suppress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentsentinel-cli
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.5
|
|
4
4
|
Summary: AI agent and MCP server security scanner — discovery, static analysis, supply chain audit, and multi-agent trust analysis
|
|
5
5
|
Project-URL: Homepage, https://github.com/jaydenaung/agentsentinel-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/jaydenaung/agentsentinel-cli
|
|
@@ -138,9 +138,13 @@ def _emit(
|
|
|
138
138
|
if is_reflection:
|
|
139
139
|
severity, title = "MEDIUM", "Input reflected in error response (no HTML encoding)"
|
|
140
140
|
scenario = (
|
|
141
|
-
f"Tool '{tool_name}' echoes user-controlled input without
|
|
142
|
-
"
|
|
143
|
-
"
|
|
141
|
+
f"Tool '{tool_name}' echoes user-controlled input without sanitization via parameter '{param}'. "
|
|
142
|
+
"Reflected content enters any connected agent's context window directly, making this an "
|
|
143
|
+
"injection vector for adversarial instructions embedded in the input."
|
|
144
|
+
)
|
|
145
|
+
remediation = (
|
|
146
|
+
"Sanitize user input before including it in error messages. "
|
|
147
|
+
"Strip or HTML-encode special characters in all error responses."
|
|
144
148
|
)
|
|
145
149
|
elif is_trace:
|
|
146
150
|
severity, title = "HIGH", "Unhandled exception — stack trace leaked"
|
|
@@ -149,21 +153,34 @@ def _emit(
|
|
|
149
153
|
"Stack traces reveal internal paths, library versions, and code structure "
|
|
150
154
|
"that attackers use to craft further exploits."
|
|
151
155
|
)
|
|
156
|
+
remediation = (
|
|
157
|
+
"Catch all exceptions server-side and return a generic error message. "
|
|
158
|
+
"Never expose stack traces, internal paths, or library names to clients."
|
|
159
|
+
)
|
|
152
160
|
elif is_template:
|
|
153
161
|
severity, title = "HIGH", "Template/expression injection confirmed"
|
|
154
162
|
scenario = (
|
|
155
|
-
f"Tool '{tool_name}' evaluated a template expression. "
|
|
163
|
+
f"Tool '{tool_name}' evaluated a template expression in user-controlled input. "
|
|
156
164
|
"Server-side template injection can escalate to arbitrary code execution."
|
|
157
165
|
)
|
|
166
|
+
remediation = (
|
|
167
|
+
"Disable server-side template evaluation of user-controlled strings. "
|
|
168
|
+
"Use a templating engine that escapes by default and never eval user input."
|
|
169
|
+
)
|
|
158
170
|
elif is_path:
|
|
159
171
|
severity, title = "MEDIUM", "Internal file path leaked in error response"
|
|
160
172
|
scenario = (
|
|
161
|
-
f"Tool '{tool_name}' exposes an internal filesystem path. "
|
|
173
|
+
f"Tool '{tool_name}' exposes an internal filesystem path in its error response. "
|
|
162
174
|
"Attackers use path disclosure to target specific files in traversal attacks."
|
|
163
175
|
)
|
|
176
|
+
remediation = (
|
|
177
|
+
"Sanitize error messages to exclude internal filesystem paths. "
|
|
178
|
+
"Return 'invalid input' without path context."
|
|
179
|
+
)
|
|
164
180
|
else:
|
|
165
181
|
severity, title = "LOW", "Unexpected information in fuzz response"
|
|
166
182
|
scenario = f"Tool '{tool_name}' returned unexpected content in response to malformed input."
|
|
183
|
+
remediation = "Review what the tool returns on unexpected input and tighten input validation."
|
|
167
184
|
|
|
168
185
|
param_str = f".{param}" if param else ""
|
|
169
186
|
findings.append(RedTeamFinding(
|
|
@@ -178,6 +195,7 @@ def _emit(
|
|
|
178
195
|
mitre_id="T1592.002",
|
|
179
196
|
owasp_id="ASI03",
|
|
180
197
|
confidence="HIGH",
|
|
198
|
+
remediation=remediation,
|
|
181
199
|
request_body={"tool": tool_name, "payload": repr(raw_payload)[:100]} if verbose else None,
|
|
182
200
|
response_body=result.raw_response[:500] if verbose else None,
|
|
183
201
|
))
|
|
@@ -26,6 +26,11 @@ _TECHNIQUE_META: dict[str, dict] = {
|
|
|
26
26
|
"severity": "CRITICAL",
|
|
27
27
|
"title": "Path traversal — arbitrary file read",
|
|
28
28
|
"scenario": "Attacker can read arbitrary files from the server filesystem via parameter '{param}'.",
|
|
29
|
+
"remediation": (
|
|
30
|
+
"Restrict file access to an explicit allowlist of base directories. "
|
|
31
|
+
"Reject paths containing `..` sequences and absolute paths outside the allowed root. "
|
|
32
|
+
"Use os.path.realpath() and verify the resolved path starts with the allowed base."
|
|
33
|
+
),
|
|
29
34
|
},
|
|
30
35
|
"ssrf": {
|
|
31
36
|
"mitre": "T1090.002",
|
|
@@ -33,6 +38,11 @@ _TECHNIQUE_META: dict[str, dict] = {
|
|
|
33
38
|
"severity": "CRITICAL",
|
|
34
39
|
"title": "SSRF — server-side request forgery",
|
|
35
40
|
"scenario": "Attacker can make the server issue requests to internal network addresses via '{param}'.",
|
|
41
|
+
"remediation": (
|
|
42
|
+
"Validate and allowlist permitted URL schemes and hosts. "
|
|
43
|
+
"Block RFC-1918 ranges (169.254.x.x, 10.x.x.x, 172.16-31.x.x, 192.168.x.x) and localhost. "
|
|
44
|
+
"Resolve DNS before allowlist checks to prevent TOCTOU bypasses."
|
|
45
|
+
),
|
|
36
46
|
},
|
|
37
47
|
"cmd": {
|
|
38
48
|
"mitre": "T1059",
|
|
@@ -40,6 +50,11 @@ _TECHNIQUE_META: dict[str, dict] = {
|
|
|
40
50
|
"severity": "CRITICAL",
|
|
41
51
|
"title": "Command injection — OS command execution",
|
|
42
52
|
"scenario": "Attacker can execute arbitrary OS commands on the host via parameter '{param}'.",
|
|
53
|
+
"remediation": (
|
|
54
|
+
"Never pass user-controlled input to a shell. "
|
|
55
|
+
"Use subprocess with an explicit argument list (no shell=True). "
|
|
56
|
+
"If shell execution is required, validate against a strict allowlist."
|
|
57
|
+
),
|
|
43
58
|
},
|
|
44
59
|
"sqli": {
|
|
45
60
|
"mitre": "T1190",
|
|
@@ -47,6 +62,11 @@ _TECHNIQUE_META: dict[str, dict] = {
|
|
|
47
62
|
"severity": "HIGH",
|
|
48
63
|
"title": "SQL injection — database query manipulation",
|
|
49
64
|
"scenario": "Attacker can manipulate backend SQL queries through parameter '{param}'.",
|
|
65
|
+
"remediation": (
|
|
66
|
+
"Use parameterized queries or a query builder. "
|
|
67
|
+
"Never interpolate user input into SQL strings. "
|
|
68
|
+
"Apply input length and character-set validation as a second layer."
|
|
69
|
+
),
|
|
50
70
|
},
|
|
51
71
|
"llm": {
|
|
52
72
|
"mitre": "AML.T0051.000",
|
|
@@ -57,6 +77,10 @@ _TECHNIQUE_META: dict[str, dict] = {
|
|
|
57
77
|
"Tool result containing adversarial instructions flows into any connected LLM context "
|
|
58
78
|
"via parameter '{param}'. Attacker-controlled input can override agent behaviour."
|
|
59
79
|
),
|
|
80
|
+
"remediation": (
|
|
81
|
+
"Do not reflect user-controlled parameter values verbatim in tool responses. "
|
|
82
|
+
"Validate inputs and reject or strip LLM instruction patterns before processing."
|
|
83
|
+
),
|
|
60
84
|
},
|
|
61
85
|
}
|
|
62
86
|
|
|
@@ -140,6 +164,10 @@ def run_inject(
|
|
|
140
164
|
mitre_id=meta["mitre"],
|
|
141
165
|
owasp_id=meta["owasp"],
|
|
142
166
|
confidence="MEDIUM",
|
|
167
|
+
remediation=(
|
|
168
|
+
"Do not echo raw user input in error messages. "
|
|
169
|
+
"Return a generic error that excludes the parameter value."
|
|
170
|
+
),
|
|
143
171
|
request_body={"tool": tool.name, "arguments": args} if verbose else None,
|
|
144
172
|
response_body=result.raw_response[:500] if verbose else None,
|
|
145
173
|
))
|
|
@@ -156,6 +184,7 @@ def run_inject(
|
|
|
156
184
|
mitre_id=meta["mitre"],
|
|
157
185
|
owasp_id=meta["owasp"],
|
|
158
186
|
confidence="HIGH",
|
|
187
|
+
remediation=meta.get("remediation"),
|
|
159
188
|
request_body={"tool": tool.name, "arguments": args} if verbose else None,
|
|
160
189
|
response_body=result.raw_response[:500] if verbose else None,
|
|
161
190
|
))
|
|
@@ -62,10 +62,67 @@ def run_poison(
|
|
|
62
62
|
|
|
63
63
|
_static_description_scan(session, findings)
|
|
64
64
|
attack_count += _dynamic_result_injection(session, findings, verbose)
|
|
65
|
+
_dedup_error_echo(findings)
|
|
65
66
|
|
|
66
67
|
return findings, attack_count
|
|
67
68
|
|
|
68
69
|
|
|
70
|
+
def _dedup_error_echo(findings: list[RedTeamFinding]) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Merge same-root-cause error echo findings into one finding.
|
|
73
|
+
When multiple tools share the same parameter name and the same issue
|
|
74
|
+
(error echo), listing them separately inflates counts without adding
|
|
75
|
+
signal. Merge into one finding that lists all affected tools.
|
|
76
|
+
"""
|
|
77
|
+
# Group by (attack_type, severity, confidence, parameter, title-template)
|
|
78
|
+
# where title contains "error response" — that's our dedup signal.
|
|
79
|
+
from collections import defaultdict
|
|
80
|
+
groups: dict[tuple, list[int]] = defaultdict(list)
|
|
81
|
+
for i, f in enumerate(findings):
|
|
82
|
+
if "error response" in f.title.lower() and f.attack_type == "poison":
|
|
83
|
+
key = (f.attack_type, f.severity, f.confidence, f.parameter or "")
|
|
84
|
+
groups[key].append(i)
|
|
85
|
+
|
|
86
|
+
# Only merge groups with 2+ findings; process in reverse order to preserve indices
|
|
87
|
+
to_remove: set[int] = set()
|
|
88
|
+
for key, indices in groups.items():
|
|
89
|
+
if len(indices) < 2:
|
|
90
|
+
continue
|
|
91
|
+
primary_idx = indices[0]
|
|
92
|
+
primary = findings[primary_idx]
|
|
93
|
+
affected_tools = [findings[i].tool_name for i in indices]
|
|
94
|
+
param = key[3]
|
|
95
|
+
|
|
96
|
+
findings[primary_idx] = RedTeamFinding(
|
|
97
|
+
attack_type=primary.attack_type,
|
|
98
|
+
severity=primary.severity,
|
|
99
|
+
title=(
|
|
100
|
+
f"Input reflected in error responses (injection vector) — "
|
|
101
|
+
f"{len(affected_tools)} tools affected"
|
|
102
|
+
),
|
|
103
|
+
tool_name=", ".join(affected_tools),
|
|
104
|
+
parameter=primary.parameter,
|
|
105
|
+
payload=primary.payload,
|
|
106
|
+
evidence=primary.evidence,
|
|
107
|
+
exploit_scenario=(
|
|
108
|
+
f"Tools [{', '.join(affected_tools)}] all echo user-controlled input verbatim "
|
|
109
|
+
f"in error messages via the '{param}' parameter. Adversarial LLM instructions "
|
|
110
|
+
"injected through any of these tools will appear in a connected agent's context "
|
|
111
|
+
"window inside an error message. Whether the agent's LLM acts on error-wrapped "
|
|
112
|
+
"instructions depends on its alignment and system prompt."
|
|
113
|
+
),
|
|
114
|
+
mitre_id=primary.mitre_id,
|
|
115
|
+
owasp_id=primary.owasp_id,
|
|
116
|
+
confidence=primary.confidence,
|
|
117
|
+
remediation=primary.remediation,
|
|
118
|
+
)
|
|
119
|
+
to_remove.update(indices[1:])
|
|
120
|
+
|
|
121
|
+
# Remove merged duplicates (in reverse order to keep indices stable)
|
|
122
|
+
for i in sorted(to_remove, reverse=True):
|
|
123
|
+
findings.pop(i)
|
|
124
|
+
|
|
125
|
+
|
|
69
126
|
def _static_description_scan(
|
|
70
127
|
session: RedTeamSession,
|
|
71
128
|
findings: list[RedTeamFinding],
|
|
@@ -95,6 +152,11 @@ def _static_description_scan(
|
|
|
95
152
|
mitre_id="AML.T0051.001",
|
|
96
153
|
owasp_id="ASI04",
|
|
97
154
|
confidence="HIGH",
|
|
155
|
+
remediation=(
|
|
156
|
+
"Audit all tool descriptions before deployment. Remove any instructions "
|
|
157
|
+
"targeting LLM behavior ('ignore previous instructions', role overrides, etc.). "
|
|
158
|
+
"Descriptions should only describe what the tool does, never direct the model."
|
|
159
|
+
),
|
|
98
160
|
))
|
|
99
161
|
break # One finding per tool
|
|
100
162
|
|
|
@@ -117,6 +179,7 @@ def _static_description_scan(
|
|
|
117
179
|
mitre_id="AML.T0051.001",
|
|
118
180
|
owasp_id="ASI04",
|
|
119
181
|
confidence="MEDIUM",
|
|
182
|
+
remediation="Review tool description manually and remove any covert instruction patterns.",
|
|
120
183
|
))
|
|
121
184
|
break
|
|
122
185
|
|
|
@@ -180,6 +243,10 @@ def _dynamic_result_injection(
|
|
|
180
243
|
"will appear in any connected agent's context window inside an error message. "
|
|
181
244
|
"Exploitation depends on whether the agent's LLM processes error content as instructions."
|
|
182
245
|
)
|
|
246
|
+
remediation = (
|
|
247
|
+
f"Do not echo raw user input in error messages. Return a generic error "
|
|
248
|
+
f"(e.g. 'invalid path') that excludes the parameter value."
|
|
249
|
+
)
|
|
183
250
|
else:
|
|
184
251
|
severity = "HIGH"
|
|
185
252
|
confidence = "HIGH"
|
|
@@ -190,6 +257,10 @@ def _dynamic_result_injection(
|
|
|
190
257
|
"calling this tool. This enables goal hijacking without access to "
|
|
191
258
|
"the agent or its system prompt."
|
|
192
259
|
)
|
|
260
|
+
remediation = (
|
|
261
|
+
"Do not reflect user-controlled parameter values in tool responses. "
|
|
262
|
+
"Validate inputs and reject LLM instruction patterns before processing."
|
|
263
|
+
)
|
|
193
264
|
|
|
194
265
|
findings.append(RedTeamFinding(
|
|
195
266
|
attack_type="poison",
|
|
@@ -207,6 +278,7 @@ def _dynamic_result_injection(
|
|
|
207
278
|
mitre_id="AML.T0051.000",
|
|
208
279
|
owasp_id="ASI01",
|
|
209
280
|
confidence=confidence,
|
|
281
|
+
remediation=remediation,
|
|
210
282
|
request_body={"tool": tool.name, "arguments": args} if verbose else None,
|
|
211
283
|
response_body=result.raw_response[:500] if verbose else None,
|
|
212
284
|
))
|
|
@@ -85,6 +85,13 @@ def run_recon(session: RedTeamSession, verbose: bool = False) -> tuple[list[RedT
|
|
|
85
85
|
|
|
86
86
|
# HIGH: dangerous tools exposed
|
|
87
87
|
if dangerous:
|
|
88
|
+
dangerous_tools_info = [t for t in info.tools if t.is_dangerous]
|
|
89
|
+
tool_details: list[str] = []
|
|
90
|
+
for t in dangerous_tools_info:
|
|
91
|
+
params = list(t.input_schema.get("properties", {}).keys())
|
|
92
|
+
param_str = f"({', '.join(params)})" if params else "(no params)"
|
|
93
|
+
tool_details.append(f"{t.name} {param_str}")
|
|
94
|
+
|
|
88
95
|
findings.append(RedTeamFinding(
|
|
89
96
|
attack_type="recon",
|
|
90
97
|
severity="HIGH",
|
|
@@ -92,14 +99,20 @@ def run_recon(session: RedTeamSession, verbose: bool = False) -> tuple[list[RedT
|
|
|
92
99
|
tool_name="<server>",
|
|
93
100
|
parameter=None,
|
|
94
101
|
payload=None,
|
|
95
|
-
evidence="
|
|
102
|
+
evidence="\n".join(tool_details),
|
|
96
103
|
exploit_scenario=(
|
|
97
104
|
"These tools perform write/delete/execute operations. "
|
|
98
|
-
"Prompt injection into any connected agent can invoke them directly."
|
|
105
|
+
"Prompt injection into any connected agent can invoke them directly. "
|
|
106
|
+
"Use --include-dangerous to test these tools actively."
|
|
99
107
|
),
|
|
100
108
|
mitre_id="AML.T0040",
|
|
101
109
|
owasp_id="ASI02",
|
|
102
110
|
confidence="HIGH",
|
|
111
|
+
remediation=(
|
|
112
|
+
"Apply the principle of least privilege: remove tools the server does not need. "
|
|
113
|
+
"Gate dangerous tools behind scope headers (X-Agent-Scopes) or separate "
|
|
114
|
+
"authenticated endpoints so agents cannot invoke them via prompt injection."
|
|
115
|
+
),
|
|
103
116
|
))
|
|
104
117
|
|
|
105
118
|
recon = ReconData(
|
|
@@ -73,6 +73,7 @@ class RedTeamFinding:
|
|
|
73
73
|
mitre_id: str | None
|
|
74
74
|
owasp_id: str | None
|
|
75
75
|
confidence: str = "HIGH" # HIGH | MEDIUM | LOW
|
|
76
|
+
remediation: str | None = None # what to fix
|
|
76
77
|
request_body: dict | None = None # full request (verbose mode)
|
|
77
78
|
response_body: str | None = None # full response (verbose mode)
|
|
78
79
|
|
|
@@ -291,10 +291,15 @@ def find_evidence(technique: str, text: str) -> str | None:
|
|
|
291
291
|
for pattern in DETECTION.get(technique, []):
|
|
292
292
|
m = pattern.search(text)
|
|
293
293
|
if m:
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
# Start at the beginning of the line containing the match so the
|
|
295
|
+
# snippet never starts mid-word (m.start()-40 could land mid-token).
|
|
296
|
+
line_start = text.rfind("\n", 0, m.start())
|
|
297
|
+
start = (line_start + 1) if line_start >= 0 else 0
|
|
298
|
+
# Cap how far back we go — don't include 10 unrelated lines
|
|
299
|
+
start = max(start, m.start() - 80)
|
|
300
|
+
end = min(len(text), m.end() + 120)
|
|
296
301
|
snippet = text[start:end].replace("\n", " ").strip()
|
|
297
|
-
return snippet[:
|
|
302
|
+
return snippet[:300]
|
|
298
303
|
return None
|
|
299
304
|
|
|
300
305
|
|
|
@@ -47,6 +47,7 @@ def print_redteam_result(result: RedTeamResult, verbose: bool = False) -> None:
|
|
|
47
47
|
console.print()
|
|
48
48
|
_print_header(result)
|
|
49
49
|
_print_findings(result.findings, verbose)
|
|
50
|
+
_print_attack_chains(result.findings)
|
|
50
51
|
_print_summary(result)
|
|
51
52
|
|
|
52
53
|
|
|
@@ -112,6 +113,9 @@ def _print_finding(f: RedTeamFinding, verbose: bool) -> None:
|
|
|
112
113
|
|
|
113
114
|
console.print(f" [dim]Scenario: [/dim] {f.exploit_scenario}")
|
|
114
115
|
|
|
116
|
+
if f.remediation and f.severity not in ("INFO",):
|
|
117
|
+
console.print(f" [dim]Fix: [/dim] [green]{f.remediation}[/green]")
|
|
118
|
+
|
|
115
119
|
refs: list[str] = []
|
|
116
120
|
if f.mitre_id:
|
|
117
121
|
refs.append(f"MITRE: {f.mitre_id}")
|
|
@@ -128,6 +132,89 @@ def _print_finding(f: RedTeamFinding, verbose: bool) -> None:
|
|
|
128
132
|
console.print(f" [dim]Response: [/dim] [dim]{f.response_body[:200]}[/dim]")
|
|
129
133
|
|
|
130
134
|
|
|
135
|
+
def _print_attack_chains(findings: list[RedTeamFinding]) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Synthesize multi-step attack chains from confirmed findings.
|
|
138
|
+
Only emits when two or more findings chain into a meaningful escalation path.
|
|
139
|
+
"""
|
|
140
|
+
chains: list[tuple[str, str]] = []
|
|
141
|
+
|
|
142
|
+
attack_types = {f.attack_type for f in findings}
|
|
143
|
+
severities = {f.severity for f in findings}
|
|
144
|
+
confirmed_critical = {f.attack_type for f in findings if f.severity == "CRITICAL"}
|
|
145
|
+
|
|
146
|
+
# Detect tool names from recon findings
|
|
147
|
+
all_tool_names: set[str] = set()
|
|
148
|
+
for f in findings:
|
|
149
|
+
if f.attack_type == "recon" and f.severity == "HIGH":
|
|
150
|
+
# Evidence line contains "tool_name (params)" entries
|
|
151
|
+
for part in f.evidence.split("\n"):
|
|
152
|
+
name = part.split("(")[0].strip()
|
|
153
|
+
if name:
|
|
154
|
+
all_tool_names.add(name)
|
|
155
|
+
all_tool_names.add(f.tool_name)
|
|
156
|
+
|
|
157
|
+
has_shell = any(
|
|
158
|
+
"shell" in n or "exec" in n or "cmd" in n or "run" in n
|
|
159
|
+
for n in all_tool_names
|
|
160
|
+
)
|
|
161
|
+
has_write = any(
|
|
162
|
+
"write" in n or "append" in n or "create" in n or "delete" in n or "upload" in n
|
|
163
|
+
for n in all_tool_names
|
|
164
|
+
)
|
|
165
|
+
has_traverse = "traverse" in confirmed_critical
|
|
166
|
+
has_auth_bypass = any(
|
|
167
|
+
f.attack_type == "auth" and f.severity in ("CRITICAL", "HIGH")
|
|
168
|
+
for f in findings
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if has_traverse and has_shell:
|
|
172
|
+
chains.append((
|
|
173
|
+
"CRITICAL",
|
|
174
|
+
"Read + Execute — Full host compromise path\n"
|
|
175
|
+
" Step 1: path traversal on read_file → read ~/.ssh/id_rsa, .env, API keys\n"
|
|
176
|
+
" Step 2: prompt inject agent → invoke execute_shell with attacker payload\n"
|
|
177
|
+
" Impact: credential theft + arbitrary code execution on the host",
|
|
178
|
+
))
|
|
179
|
+
elif has_traverse and has_write:
|
|
180
|
+
chains.append((
|
|
181
|
+
"CRITICAL",
|
|
182
|
+
"Read + Write — Data exfiltration and persistence\n"
|
|
183
|
+
" Step 1: path traversal → read sensitive files (keys, configs, DB creds)\n"
|
|
184
|
+
" Step 2: write tools → drop backdoor file or overwrite config\n"
|
|
185
|
+
" Impact: persistent access and data exfiltration",
|
|
186
|
+
))
|
|
187
|
+
elif has_traverse:
|
|
188
|
+
chains.append((
|
|
189
|
+
"HIGH",
|
|
190
|
+
"Read-only traversal — Reconnaissance and credential theft\n"
|
|
191
|
+
" Step 1: path traversal → read /etc/passwd, ~/.ssh/id_rsa, .env files\n"
|
|
192
|
+
" Impact: credential theft enabling lateral movement",
|
|
193
|
+
))
|
|
194
|
+
|
|
195
|
+
if has_auth_bypass and (has_shell or has_write or has_traverse):
|
|
196
|
+
chains.append((
|
|
197
|
+
"CRITICAL",
|
|
198
|
+
"Unauthenticated access + dangerous tools — Zero-credential exploitation\n"
|
|
199
|
+
" Auth bypass confirmed → all dangerous tools callable without credentials\n"
|
|
200
|
+
" Impact: any tool capability exploitable by an unauthenticated attacker",
|
|
201
|
+
))
|
|
202
|
+
|
|
203
|
+
if not chains:
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
console.print()
|
|
207
|
+
console.rule("[bold red]ATTACK CHAINS[/bold red]", style="red")
|
|
208
|
+
console.print()
|
|
209
|
+
for sev, chain_text in chains:
|
|
210
|
+
color = "bold red" if sev == "CRITICAL" else "bold orange1"
|
|
211
|
+
lines = chain_text.split("\n")
|
|
212
|
+
console.print(f" [{color}]▶ {sev}[/{color}] {lines[0]}")
|
|
213
|
+
for line in lines[1:]:
|
|
214
|
+
console.print(f" [dim]{line}[/dim]")
|
|
215
|
+
console.print()
|
|
216
|
+
|
|
217
|
+
|
|
131
218
|
def _print_summary(result: RedTeamResult) -> None:
|
|
132
219
|
console.print()
|
|
133
220
|
console.rule("[bold white]ATTACK SUMMARY[/bold white]", style="bright_blue")
|
|
@@ -181,6 +268,7 @@ def as_redteam_json(result: RedTeamResult) -> str:
|
|
|
181
268
|
"payload": f.payload,
|
|
182
269
|
"evidence": f.evidence,
|
|
183
270
|
"exploit_scenario": f.exploit_scenario,
|
|
271
|
+
"remediation": f.remediation,
|
|
184
272
|
"mitre_id": f.mitre_id,
|
|
185
273
|
"owasp_id": f.owasp_id,
|
|
186
274
|
"confidence": f.confidence,
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentsentinel-cli"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.5"
|
|
8
8
|
description = "AI agent and MCP server security scanner — discovery, static analysis, supply chain audit, and multi-agent trust analysis"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/supply_chain_report.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|