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.
Files changed (43) hide show
  1. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/PKG-INFO +1 -1
  2. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/mcp_fuzz.py +23 -5
  3. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/mcp_inject.py +29 -0
  4. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/mcp_poison.py +72 -0
  5. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/mcp_recon.py +15 -2
  6. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/models.py +1 -0
  7. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/payloads.py +8 -3
  8. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/report.py +88 -0
  9. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/pyproject.toml +1 -1
  10. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/.gitignore +0 -0
  11. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/DOCUMENTATION.md +0 -0
  12. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/LICENSE +0 -0
  13. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/README.md +0 -0
  14. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/__init__.py +0 -0
  15. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/a2a_report.py +0 -0
  16. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/a2a_rules.py +0 -0
  17. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/a2a_scanner.py +0 -0
  18. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/cli.py +0 -0
  19. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/discover.py +0 -0
  20. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/discover_report.py +0 -0
  21. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/fingerprint.py +0 -0
  22. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/frameworks.py +0 -0
  23. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/host_report.py +0 -0
  24. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/host_rules.py +0 -0
  25. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/host_scanner.py +0 -0
  26. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/inspect.py +0 -0
  27. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/inspect_report.py +0 -0
  28. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/mcp_client.py +0 -0
  29. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/mcp_report.py +0 -0
  30. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/mcp_rules.py +0 -0
  31. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/__init__.py +0 -0
  32. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/mcp_auth.py +0 -0
  33. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/redteam/transport.py +0 -0
  34. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/report.py +0 -0
  35. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/rules.py +0 -0
  36. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/scanner.py +0 -0
  37. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/secrets.py +0 -0
  38. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/secrets_report.py +0 -0
  39. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/secrets_rules.py +0 -0
  40. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/supply_chain_ai.py +0 -0
  41. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/supply_chain_report.py +0 -0
  42. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.5}/agentsentinel_cli/supply_chain_rules.py +0 -0
  43. {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.4
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 encoding via parameter '{param}'. "
142
- "In an agent context, reflected content enters the LLM context window directly. "
143
- "In a web UI context, this would be a stored/reflected XSS vector."
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=", ".join(dangerous),
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
- start = max(0, m.start() - 40)
295
- end = min(len(text), m.end() + 80)
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[:200]
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.4"
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"