agentsentinel-cli 0.9.4__tar.gz → 0.9.6__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 (45) hide show
  1. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/PKG-INFO +1 -1
  2. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/cli.py +99 -11
  3. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/redteam/mcp_fuzz.py +23 -5
  4. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/redteam/mcp_inject.py +29 -0
  5. agentsentinel_cli-0.9.6/agentsentinel_cli/redteam/mcp_oauth.py +523 -0
  6. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/redteam/mcp_poison.py +72 -0
  7. agentsentinel_cli-0.9.6/agentsentinel_cli/redteam/mcp_preauth.py +614 -0
  8. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/redteam/mcp_recon.py +15 -2
  9. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/redteam/models.py +1 -0
  10. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/redteam/payloads.py +8 -3
  11. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/redteam/report.py +90 -0
  12. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/pyproject.toml +1 -1
  13. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/.gitignore +0 -0
  14. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/DOCUMENTATION.md +0 -0
  15. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/LICENSE +0 -0
  16. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/README.md +0 -0
  17. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/__init__.py +0 -0
  18. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/a2a_report.py +0 -0
  19. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/a2a_rules.py +0 -0
  20. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/a2a_scanner.py +0 -0
  21. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/discover.py +0 -0
  22. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/discover_report.py +0 -0
  23. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/fingerprint.py +0 -0
  24. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/frameworks.py +0 -0
  25. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/host_report.py +0 -0
  26. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/host_rules.py +0 -0
  27. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/host_scanner.py +0 -0
  28. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/inspect.py +0 -0
  29. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/inspect_report.py +0 -0
  30. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/mcp_client.py +0 -0
  31. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/mcp_report.py +0 -0
  32. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/mcp_rules.py +0 -0
  33. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/redteam/__init__.py +0 -0
  34. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/redteam/mcp_auth.py +0 -0
  35. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/redteam/transport.py +0 -0
  36. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/report.py +0 -0
  37. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/rules.py +0 -0
  38. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/scanner.py +0 -0
  39. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/secrets.py +0 -0
  40. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/secrets_report.py +0 -0
  41. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/secrets_rules.py +0 -0
  42. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/supply_chain_ai.py +0 -0
  43. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/supply_chain_report.py +0 -0
  44. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/agentsentinel_cli/supply_chain_rules.py +0 -0
  45. {agentsentinel_cli-0.9.4 → agentsentinel_cli-0.9.6}/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.6
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
@@ -1157,6 +1157,66 @@ def redteam_mcp_recon(
1157
1157
  _check_exit(findings, fail_on)
1158
1158
 
1159
1159
 
1160
+ # ── sentinel redteam mcp preauth ─────────────────────────────────────────────
1161
+
1162
+ @redteam_mcp_group.command("preauth")
1163
+ @click.argument("target", required=False, metavar="URL")
1164
+ @click.option("--timeout", default=15.0, show_default=True, metavar="SECONDS")
1165
+ @click.option("--format", "fmt", type=click.Choice(["text", "json"]), default="text")
1166
+ @click.option("--output", "output_path", default=None, metavar="FILE")
1167
+ @click.option("--verbose", "-v", is_flag=True, default=False)
1168
+ @click.option("--fail-on", type=click.Choice(["CRITICAL", "HIGH", "MEDIUM", "LOW"]), default=None)
1169
+ def redteam_mcp_preauth(
1170
+ target: str | None, timeout: float, fmt: str,
1171
+ output_path: str | None, verbose: bool, fail_on: str | None,
1172
+ ) -> None:
1173
+ """HTTP-layer fingerprinting — runs with zero credentials.
1174
+
1175
+ Probes the server before any MCP initialize attempt: CORS config,
1176
+ OAuth metadata, version disclosure, unauthenticated paths, SSE stream.
1177
+ Produces findings even when the server blocks unauthenticated MCP access.
1178
+
1179
+ \b
1180
+ Examples:
1181
+ sentinel redteam mcp preauth http://localhost:3000
1182
+ sentinel redteam mcp preauth http://localhost:3000 --verbose
1183
+ """
1184
+ import time
1185
+ from agentsentinel_cli.redteam.mcp_preauth import run_preauth
1186
+ from agentsentinel_cli.redteam.mcp_oauth import run_oauth
1187
+ from agentsentinel_cli.redteam.models import RedTeamResult
1188
+ from agentsentinel_cli.redteam.report import print_redteam_result, as_redteam_json
1189
+
1190
+ if not target:
1191
+ console.print("[red]Error:[/red] provide a URL.")
1192
+ console.print(" Example: [dim]sentinel redteam mcp preauth http://localhost:3000[/dim]")
1193
+ sys.exit(1)
1194
+
1195
+ target = _normalize_url(target)
1196
+ t0 = time.monotonic()
1197
+
1198
+ preauth_findings, preauth_count = run_preauth(url=target, timeout=timeout, verbose=verbose)
1199
+ oauth_findings, oauth_count = run_oauth(
1200
+ url=target, original_headers={}, timeout=timeout, verbose=verbose,
1201
+ )
1202
+ all_findings = preauth_findings + oauth_findings
1203
+
1204
+ result = RedTeamResult(
1205
+ target=target, server_name="unknown", server_version="unknown",
1206
+ transport="http", modules_run=["preauth", "oauth"], findings=all_findings,
1207
+ tool_count=0, attack_count=preauth_count + oauth_count,
1208
+ duration_s=time.monotonic() - t0,
1209
+ )
1210
+
1211
+ if fmt == "json":
1212
+ click.echo(as_redteam_json(result))
1213
+ else:
1214
+ print_redteam_result(result, verbose)
1215
+
1216
+ _save_output(output_path, result)
1217
+ _check_exit(all_findings, fail_on)
1218
+
1219
+
1160
1220
  # ── sentinel redteam mcp auth ─────────────────────────────────────────────────
1161
1221
 
1162
1222
  @redteam_mcp_group.command("auth")
@@ -1216,9 +1276,18 @@ def redteam_mcp_auth(
1216
1276
  timeout=timeout, verbose=verbose,
1217
1277
  )
1218
1278
 
1279
+ # OAuth 2.0 attack surface (HTTP-layer, no session needed)
1280
+ if target and not stdio_cmd:
1281
+ from agentsentinel_cli.redteam.mcp_oauth import run_oauth
1282
+ oauth_findings, oauth_count = run_oauth(
1283
+ url=target, original_headers=headers, timeout=timeout, verbose=verbose,
1284
+ )
1285
+ findings = findings + oauth_findings
1286
+ scenarios_tested += oauth_count
1287
+
1219
1288
  result = RedTeamResult(
1220
1289
  target=display, server_name=server_name, server_version=server_version,
1221
- transport=transport, modules_run=["auth"], findings=findings,
1290
+ transport=transport, modules_run=["auth", "oauth"], findings=findings,
1222
1291
  tool_count=tool_count, attack_count=scenarios_tested,
1223
1292
  duration_s=time.monotonic() - t0,
1224
1293
  )
@@ -1507,6 +1576,8 @@ def redteam_mcp_full(
1507
1576
  """
1508
1577
  import time
1509
1578
  from agentsentinel_cli.redteam.transport import RedTeamSession
1579
+ from agentsentinel_cli.redteam.mcp_preauth import run_preauth
1580
+ from agentsentinel_cli.redteam.mcp_oauth import run_oauth
1510
1581
  from agentsentinel_cli.redteam.mcp_recon import run_recon
1511
1582
  from agentsentinel_cli.redteam.mcp_auth import run_auth_bypass
1512
1583
  from agentsentinel_cli.redteam.mcp_inject import run_inject
@@ -1529,7 +1600,21 @@ def redteam_mcp_full(
1529
1600
 
1530
1601
  t0 = time.monotonic()
1531
1602
 
1532
- # ── Phase 1–5: run inside a single persistent session ────────────────────
1603
+ # ── Phase 1–2: HTTP-layer probes no session or credentials required ────
1604
+ if target and not stdio_cmd:
1605
+ preauth_findings, preauth_count = run_preauth(
1606
+ url=target, timeout=timeout, verbose=verbose,
1607
+ )
1608
+ all_findings.extend(preauth_findings)
1609
+ total_attacks += preauth_count
1610
+
1611
+ oauth_findings, oauth_count = run_oauth(
1612
+ url=target, original_headers=headers, timeout=timeout, verbose=verbose,
1613
+ )
1614
+ all_findings.extend(oauth_findings)
1615
+ total_attacks += oauth_count
1616
+
1617
+ # ── Phase 3–7: run inside a single persistent MCP session ───────────────
1533
1618
  try:
1534
1619
  with RedTeamSession(url=target, stdio_cmd=stdio_cmd,
1535
1620
  extra_headers=headers, timeout=timeout) as session:
@@ -1554,11 +1639,11 @@ def redteam_mcp_full(
1554
1639
  transient=True,
1555
1640
  ) as progress:
1556
1641
 
1557
- task = progress.add_task("Phase 1/5 — recon…", total=None)
1642
+ task = progress.add_task("Phase 3/7 — recon…", total=None)
1558
1643
  recon_findings, _ = run_recon(session, verbose)
1559
1644
  all_findings.extend(recon_findings)
1560
1645
 
1561
- progress.update(task, description="Phase 2/5 — auth bypass…")
1646
+ progress.update(task, description="Phase 4/7 — auth bypass…")
1562
1647
  auth_findings, auth_scenarios = run_auth_bypass(
1563
1648
  url=target, stdio_cmd=stdio_cmd,
1564
1649
  original_headers=headers, timeout=timeout, verbose=verbose,
@@ -1566,10 +1651,9 @@ def redteam_mcp_full(
1566
1651
  all_findings.extend(auth_findings)
1567
1652
  total_attacks += auth_scenarios
1568
1653
 
1569
- progress.update(task, description="Phase 3/5 — injection…")
1654
+ progress.update(task, description="Phase 5/7 — injection…")
1570
1655
  inject_findings, inject_count = run_inject(
1571
1656
  session,
1572
- # LLM injection is handled by poison phase — no duplication
1573
1657
  techniques=["traverse", "ssrf", "cmd", "sqli"],
1574
1658
  intensity=intensity,
1575
1659
  include_dangerous=include_dangerous,
@@ -1578,30 +1662,34 @@ def redteam_mcp_full(
1578
1662
  all_findings.extend(inject_findings)
1579
1663
  total_attacks += inject_count
1580
1664
 
1581
- progress.update(task, description="Phase 4/5 — poisoning…")
1665
+ progress.update(task, description="Phase 6/7 — poisoning…")
1582
1666
  poison_findings, poison_count = run_poison(session, verbose)
1583
1667
  all_findings.extend(poison_findings)
1584
1668
  total_attacks += poison_count
1585
1669
 
1586
- progress.update(task, description="Phase 5/5 — fuzzing…")
1670
+ progress.update(task, description="Phase 7/7 — fuzzing…")
1587
1671
  fuzz_findings, fuzz_count = run_fuzz(session, verbose)
1588
1672
  all_findings.extend(fuzz_findings)
1589
1673
  total_attacks += fuzz_count
1590
1674
 
1591
1675
  except McpAuthRequired as exc:
1592
1676
  console.print(f"\n[bold yellow]Auth required[/bold yellow] (HTTP {exc.status_code})")
1677
+ console.print(" Preauth + OAuth findings above are still valid without a token.")
1593
1678
  console.print(" Use: [bold]--auth-header 'Authorization: Bearer <token>'[/bold]")
1594
- sys.exit(1)
1679
+ if not all_findings:
1680
+ sys.exit(1)
1681
+ # Fall through — report whatever preauth/oauth found
1595
1682
  except McpError as exc:
1596
1683
  console.print(f"\n[red]Connection failed:[/red] {exc}")
1597
- sys.exit(1)
1684
+ if not all_findings:
1685
+ sys.exit(1)
1598
1686
 
1599
1687
  result = RedTeamResult(
1600
1688
  target=display,
1601
1689
  server_name=server_name,
1602
1690
  server_version=server_version,
1603
1691
  transport=transport,
1604
- modules_run=["recon", "auth", "inject", "poison", "fuzz"],
1692
+ modules_run=["preauth", "oauth", "recon", "auth", "inject", "poison", "fuzz"],
1605
1693
  findings=all_findings,
1606
1694
  tool_count=tool_count,
1607
1695
  attack_count=total_attacks,
@@ -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
  ))