agentsentinel-cli 0.9.0__tar.gz → 0.9.2__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 (41) hide show
  1. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/PKG-INFO +60 -5
  2. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/README.md +59 -4
  3. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/cli.py +5 -5
  4. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/host_report.py +25 -2
  5. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/host_rules.py +12 -3
  6. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/host_scanner.py +105 -0
  7. agentsentinel_cli-0.9.2/publish.sh +31 -0
  8. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/pyproject.toml +1 -1
  9. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/tmp/note.md +1 -1
  10. agentsentinel_cli-0.9.2/tmp/publish.sh +31 -0
  11. agentsentinel_cli-0.9.2/tmp/pypi.md +53 -0
  12. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/.gitignore +0 -0
  13. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/DOCUMENTATION.md +0 -0
  14. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/LICENSE +0 -0
  15. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/__init__.py +0 -0
  16. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/a2a_report.py +0 -0
  17. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/a2a_rules.py +0 -0
  18. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/a2a_scanner.py +0 -0
  19. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/discover.py +0 -0
  20. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/discover_report.py +0 -0
  21. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/fingerprint.py +0 -0
  22. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/frameworks.py +0 -0
  23. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/inspect.py +0 -0
  24. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/inspect_report.py +0 -0
  25. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/mcp_client.py +0 -0
  26. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/mcp_report.py +0 -0
  27. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/mcp_rules.py +0 -0
  28. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/report.py +0 -0
  29. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/rules.py +0 -0
  30. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/scanner.py +0 -0
  31. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/secrets.py +0 -0
  32. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/secrets_report.py +0 -0
  33. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/secrets_rules.py +0 -0
  34. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/supply_chain_ai.py +0 -0
  35. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/supply_chain_report.py +0 -0
  36. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/supply_chain_rules.py +0 -0
  37. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/agentsentinel_cli/suppress.py +0 -0
  38. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/tmp/test-mcp-agent/README.md +0 -0
  39. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/tmp/test-mcp-agent/langchain_agent.py +0 -0
  40. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/tmp/test-mcp-agent/mcp_server.py +0 -0
  41. {agentsentinel_cli-0.9.0 → agentsentinel_cli-0.9.2}/tmp/test-mcp-agent/requirements.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentsentinel-cli
3
- Version: 0.9.0
3
+ Version: 0.9.2
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
@@ -59,6 +59,7 @@ pipx install agentsentinel-cli
59
59
  | `sentinel secrets` | Are credentials or PII exposed in these files? |
60
60
  | `sentinel inspect` | What framework, model, and role is this agent? |
61
61
  | `sentinel a2a` | Are multi-agent trust boundaries safe? |
62
+ | `sentinel host-scan` | What is my local AI security posture? |
62
63
 
63
64
  ---
64
65
 
@@ -82,6 +83,10 @@ sentinel a2a ./agents/
82
83
  # Secrets and credentials
83
84
  sentinel secrets .
84
85
  sentinel secrets ~/.claude/projects/ # scan Claude Code memory
86
+
87
+ # Local AI security posture — no network calls
88
+ sentinel host-scan
89
+ sentinel host-scan --fail-on HIGH
85
90
  ```
86
91
 
87
92
  ---
@@ -318,6 +323,53 @@ Covers **ASI07** (Insecure Inter-Agent Communication).
318
323
 
319
324
  ---
320
325
 
326
+ ### `sentinel host-scan` — local AI security posture audit
327
+
328
+ Audits your machine's AI security posture without any network calls. Reads Claude Code and Claude Desktop configurations, shell credential files, macOS privacy permissions (TCC), system security settings, and running AI processes.
329
+
330
+ ```bash
331
+ sentinel host-scan
332
+ sentinel host-scan --format json
333
+ sentinel host-scan --fail-on HIGH
334
+ sentinel host-scan --ignore-rule HOST_LARGE_MEMORY
335
+ ```
336
+
337
+ **What it checks:**
338
+ - **Claude Code** — `allowedTools` (Bash bypass), MCP server configs, shell hooks
339
+ - **Claude Desktop** — MCP server configs
340
+ - **Third-party AI tools** — Cursor (`~/.cursor/mcp.json`), Windsurf (`~/.codeium/windsurf/mcp_config.json`), Continue.dev (`~/.continue/config.json`), Gemini CLI (`~/.gemini/settings.json`), VS Code (`mcp.servers` in `settings.json`) — all MCP server configs audited with the same rules
341
+ - **Shell configs** — hardcoded AI API keys in `.zshrc`, `.bashrc`, `.zprofile`, etc.
342
+ - **macOS TCC permissions** — Full Disk Access, Screen Recording, Accessibility granted to AI apps
343
+ - **macOS system security** — SIP, FileVault, Gatekeeper status
344
+ - **Exposed AI processes** — AI-related processes listening on non-localhost network interfaces
345
+ - **Memory footprint** — Claude Code conversation memory size in `~/.claude/projects/`
346
+
347
+ **Rules:**
348
+
349
+ | Rule | Severity | Category | What it catches |
350
+ |------|----------|----------|-----------------|
351
+ | `HOST_SHELL_UNRESTRICTED` | CRITICAL | config | `Bash` in `allowedTools` — shell runs without confirmation prompt |
352
+ | `HOST_SIP_DISABLED` | CRITICAL | system | macOS System Integrity Protection is off |
353
+ | `HOST_API_KEY_IN_SHELL` | HIGH | data_exposure | AI API keys hardcoded in shell config files |
354
+ | `HOST_MCP_EXFIL_PATH` | HIGH | config | MCP server has both filesystem access and network capability |
355
+ | `HOST_FDA_AI_APP` | HIGH | permissions | Full Disk Access granted to an AI app or its terminal |
356
+ | `HOST_SCREEN_RECORDING_AI` | HIGH | permissions | Screen Recording permission granted to an AI app |
357
+ | `HOST_AI_PROCESS_EXPOSED` | HIGH | network | AI-related process listening on a non-localhost interface |
358
+ | `HOST_FILEVAULT_OFF` | HIGH | system | FileVault disk encryption is disabled |
359
+ | `HOST_ACCESSIBILITY_AI` | MEDIUM | permissions | Accessibility permission granted to an AI app |
360
+ | `HOST_HOOKS_SHELL` | MEDIUM | config | Claude Code shell hooks that could interpolate AI output |
361
+ | `HOST_MCP_BROAD_FS` | MEDIUM | config | MCP server configured with home-dir or root-level path |
362
+ | `HOST_MCP_SENSITIVE_PATH` | MEDIUM | config | MCP server has access to `~/.ssh`, `~/.aws`, `~/.kube`, or Keychain |
363
+ | `HOST_MANY_MCP_SERVERS` | MEDIUM | config | 8+ MCP servers across all detected AI tools — large prompt injection attack surface |
364
+ | `HOST_GATEKEEPER_OFF` | MEDIUM | system | Gatekeeper disabled — unsigned binaries run without warning |
365
+ | `HOST_LARGE_MEMORY` | LOW | data_exposure | Claude Code memory files exceed 50 MB of accumulated conversation data |
366
+
367
+ Every finding includes a **remediation** step. The posture score (0–100) uses the same deduction weights as other sentinel commands: CRITICAL −40, HIGH −20, MEDIUM −10, LOW −5.
368
+
369
+ No API key required. No network calls.
370
+
371
+ ---
372
+
321
373
  ## Finding suppression
322
374
 
323
375
  Use `--ignore-rule` to suppress findings by rule ID. Suppressed findings are excluded from `--fail-on` evaluation — they don't break CI gates.
@@ -347,13 +399,13 @@ Supported on: `sentinel scan`, `sentinel a2a`, `sentinel mcp scan`, `sentinel su
347
399
  |------------|-----|------------------|
348
400
  | Agent Goal Hijack | ASI01 | `sentinel scan` (PROMPT_INJECTION_VECTOR), `sentinel supply-chain` (SC01) |
349
401
  | Tool Misuse & Exploitation | ASI02 | `sentinel mcp scan`, `sentinel scan` |
350
- | Agent Identity & Privilege Abuse | ASI03 | `sentinel scan` (PRIVILEGE_EXCESS) |
402
+ | Agent Identity & Privilege Abuse | ASI03 | `sentinel scan` (PRIVILEGE_EXCESS), `sentinel host-scan` (HOST_SHELL_UNRESTRICTED) |
351
403
  | **Agentic Supply Chain Compromise** | **ASI04** | **`sentinel supply-chain`** (static + AI semantic analysis) |
352
404
  | Unexpected Code Execution | ASI05 | `sentinel scan` (CODE_EXECUTION_GRANT), `sentinel mcp scan` (CODE_EXECUTION_TOOL) |
353
- | **Memory & Context Poisoning** | **ASI06** | **`sentinel secrets`** (memory contamination, system prompt leakage) |
405
+ | **Memory & Context Poisoning** | **ASI06** | **`sentinel secrets`** (memory contamination, system prompt leakage), `sentinel host-scan` (HOST_LARGE_MEMORY) |
354
406
  | **Insecure Inter-Agent Communication** | **ASI07** | **`sentinel a2a`** (call graph + trust rules) |
355
407
  | Cascading Agent Failures | ASI08 | `sentinel discover` (surface unmonitored agents) |
356
- | Rogue Agents | ASI10 | `sentinel discover` (find agents that shouldn't exist) |
408
+ | Rogue Agents | ASI10 | `sentinel discover` (find agents that shouldn't exist), `sentinel host-scan` (HOST_AI_PROCESS_EXPOSED) |
357
409
 
358
410
  ---
359
411
 
@@ -387,6 +439,9 @@ jobs:
387
439
 
388
440
  - name: Multi-agent trust analysis
389
441
  run: sentinel a2a ./agents/ --fail-on HIGH
442
+
443
+ - name: Host AI security posture
444
+ run: sentinel host-scan --fail-on HIGH
390
445
  ```
391
446
 
392
447
  Use `.sentinelignore` at the repo root to suppress accepted risks without weakening the gate:
@@ -401,7 +456,7 @@ NO_AUTH # server is behind an authenticated reverse proxy
401
456
  ## Requirements
402
457
 
403
458
  - Python 3.10+
404
- - No API key required for: `sentinel discover`, `sentinel mcp scan`, `sentinel supply-chain`, `sentinel scan`, `sentinel secrets`, `sentinel inspect --no-ai`, `sentinel a2a`
459
+ - No API key required for: `sentinel discover`, `sentinel mcp scan`, `sentinel supply-chain`, `sentinel scan`, `sentinel secrets`, `sentinel inspect --no-ai`, `sentinel a2a`, `sentinel host-scan`
405
460
  - `ANTHROPIC_API_KEY` required for: `sentinel supply-chain --ai`, `sentinel inspect` (AI summary)
406
461
 
407
462
  ---
@@ -25,6 +25,7 @@ pipx install agentsentinel-cli
25
25
  | `sentinel secrets` | Are credentials or PII exposed in these files? |
26
26
  | `sentinel inspect` | What framework, model, and role is this agent? |
27
27
  | `sentinel a2a` | Are multi-agent trust boundaries safe? |
28
+ | `sentinel host-scan` | What is my local AI security posture? |
28
29
 
29
30
  ---
30
31
 
@@ -48,6 +49,10 @@ sentinel a2a ./agents/
48
49
  # Secrets and credentials
49
50
  sentinel secrets .
50
51
  sentinel secrets ~/.claude/projects/ # scan Claude Code memory
52
+
53
+ # Local AI security posture — no network calls
54
+ sentinel host-scan
55
+ sentinel host-scan --fail-on HIGH
51
56
  ```
52
57
 
53
58
  ---
@@ -284,6 +289,53 @@ Covers **ASI07** (Insecure Inter-Agent Communication).
284
289
 
285
290
  ---
286
291
 
292
+ ### `sentinel host-scan` — local AI security posture audit
293
+
294
+ Audits your machine's AI security posture without any network calls. Reads Claude Code and Claude Desktop configurations, shell credential files, macOS privacy permissions (TCC), system security settings, and running AI processes.
295
+
296
+ ```bash
297
+ sentinel host-scan
298
+ sentinel host-scan --format json
299
+ sentinel host-scan --fail-on HIGH
300
+ sentinel host-scan --ignore-rule HOST_LARGE_MEMORY
301
+ ```
302
+
303
+ **What it checks:**
304
+ - **Claude Code** — `allowedTools` (Bash bypass), MCP server configs, shell hooks
305
+ - **Claude Desktop** — MCP server configs
306
+ - **Third-party AI tools** — Cursor (`~/.cursor/mcp.json`), Windsurf (`~/.codeium/windsurf/mcp_config.json`), Continue.dev (`~/.continue/config.json`), Gemini CLI (`~/.gemini/settings.json`), VS Code (`mcp.servers` in `settings.json`) — all MCP server configs audited with the same rules
307
+ - **Shell configs** — hardcoded AI API keys in `.zshrc`, `.bashrc`, `.zprofile`, etc.
308
+ - **macOS TCC permissions** — Full Disk Access, Screen Recording, Accessibility granted to AI apps
309
+ - **macOS system security** — SIP, FileVault, Gatekeeper status
310
+ - **Exposed AI processes** — AI-related processes listening on non-localhost network interfaces
311
+ - **Memory footprint** — Claude Code conversation memory size in `~/.claude/projects/`
312
+
313
+ **Rules:**
314
+
315
+ | Rule | Severity | Category | What it catches |
316
+ |------|----------|----------|-----------------|
317
+ | `HOST_SHELL_UNRESTRICTED` | CRITICAL | config | `Bash` in `allowedTools` — shell runs without confirmation prompt |
318
+ | `HOST_SIP_DISABLED` | CRITICAL | system | macOS System Integrity Protection is off |
319
+ | `HOST_API_KEY_IN_SHELL` | HIGH | data_exposure | AI API keys hardcoded in shell config files |
320
+ | `HOST_MCP_EXFIL_PATH` | HIGH | config | MCP server has both filesystem access and network capability |
321
+ | `HOST_FDA_AI_APP` | HIGH | permissions | Full Disk Access granted to an AI app or its terminal |
322
+ | `HOST_SCREEN_RECORDING_AI` | HIGH | permissions | Screen Recording permission granted to an AI app |
323
+ | `HOST_AI_PROCESS_EXPOSED` | HIGH | network | AI-related process listening on a non-localhost interface |
324
+ | `HOST_FILEVAULT_OFF` | HIGH | system | FileVault disk encryption is disabled |
325
+ | `HOST_ACCESSIBILITY_AI` | MEDIUM | permissions | Accessibility permission granted to an AI app |
326
+ | `HOST_HOOKS_SHELL` | MEDIUM | config | Claude Code shell hooks that could interpolate AI output |
327
+ | `HOST_MCP_BROAD_FS` | MEDIUM | config | MCP server configured with home-dir or root-level path |
328
+ | `HOST_MCP_SENSITIVE_PATH` | MEDIUM | config | MCP server has access to `~/.ssh`, `~/.aws`, `~/.kube`, or Keychain |
329
+ | `HOST_MANY_MCP_SERVERS` | MEDIUM | config | 8+ MCP servers across all detected AI tools — large prompt injection attack surface |
330
+ | `HOST_GATEKEEPER_OFF` | MEDIUM | system | Gatekeeper disabled — unsigned binaries run without warning |
331
+ | `HOST_LARGE_MEMORY` | LOW | data_exposure | Claude Code memory files exceed 50 MB of accumulated conversation data |
332
+
333
+ Every finding includes a **remediation** step. The posture score (0–100) uses the same deduction weights as other sentinel commands: CRITICAL −40, HIGH −20, MEDIUM −10, LOW −5.
334
+
335
+ No API key required. No network calls.
336
+
337
+ ---
338
+
287
339
  ## Finding suppression
288
340
 
289
341
  Use `--ignore-rule` to suppress findings by rule ID. Suppressed findings are excluded from `--fail-on` evaluation — they don't break CI gates.
@@ -313,13 +365,13 @@ Supported on: `sentinel scan`, `sentinel a2a`, `sentinel mcp scan`, `sentinel su
313
365
  |------------|-----|------------------|
314
366
  | Agent Goal Hijack | ASI01 | `sentinel scan` (PROMPT_INJECTION_VECTOR), `sentinel supply-chain` (SC01) |
315
367
  | Tool Misuse & Exploitation | ASI02 | `sentinel mcp scan`, `sentinel scan` |
316
- | Agent Identity & Privilege Abuse | ASI03 | `sentinel scan` (PRIVILEGE_EXCESS) |
368
+ | Agent Identity & Privilege Abuse | ASI03 | `sentinel scan` (PRIVILEGE_EXCESS), `sentinel host-scan` (HOST_SHELL_UNRESTRICTED) |
317
369
  | **Agentic Supply Chain Compromise** | **ASI04** | **`sentinel supply-chain`** (static + AI semantic analysis) |
318
370
  | Unexpected Code Execution | ASI05 | `sentinel scan` (CODE_EXECUTION_GRANT), `sentinel mcp scan` (CODE_EXECUTION_TOOL) |
319
- | **Memory & Context Poisoning** | **ASI06** | **`sentinel secrets`** (memory contamination, system prompt leakage) |
371
+ | **Memory & Context Poisoning** | **ASI06** | **`sentinel secrets`** (memory contamination, system prompt leakage), `sentinel host-scan` (HOST_LARGE_MEMORY) |
320
372
  | **Insecure Inter-Agent Communication** | **ASI07** | **`sentinel a2a`** (call graph + trust rules) |
321
373
  | Cascading Agent Failures | ASI08 | `sentinel discover` (surface unmonitored agents) |
322
- | Rogue Agents | ASI10 | `sentinel discover` (find agents that shouldn't exist) |
374
+ | Rogue Agents | ASI10 | `sentinel discover` (find agents that shouldn't exist), `sentinel host-scan` (HOST_AI_PROCESS_EXPOSED) |
323
375
 
324
376
  ---
325
377
 
@@ -353,6 +405,9 @@ jobs:
353
405
 
354
406
  - name: Multi-agent trust analysis
355
407
  run: sentinel a2a ./agents/ --fail-on HIGH
408
+
409
+ - name: Host AI security posture
410
+ run: sentinel host-scan --fail-on HIGH
356
411
  ```
357
412
 
358
413
  Use `.sentinelignore` at the repo root to suppress accepted risks without weakening the gate:
@@ -367,7 +422,7 @@ NO_AUTH # server is behind an authenticated reverse proxy
367
422
  ## Requirements
368
423
 
369
424
  - Python 3.10+
370
- - No API key required for: `sentinel discover`, `sentinel mcp scan`, `sentinel supply-chain`, `sentinel scan`, `sentinel secrets`, `sentinel inspect --no-ai`, `sentinel a2a`
425
+ - No API key required for: `sentinel discover`, `sentinel mcp scan`, `sentinel supply-chain`, `sentinel scan`, `sentinel secrets`, `sentinel inspect --no-ai`, `sentinel a2a`, `sentinel host-scan`
371
426
  - `ANTHROPIC_API_KEY` required for: `sentinel supply-chain --ai`, `sentinel inspect` (AI summary)
372
427
 
373
428
  ---
@@ -866,7 +866,7 @@ def a2a(
866
866
 
867
867
  # ── sentinel host ─────────────────────────────────────────────────────────────
868
868
 
869
- @main.command(name="host")
869
+ @main.command(name="host-scan")
870
870
  @click.option("--format", "fmt", type=click.Choice(["text", "json"]), default="text",
871
871
  help="Output format.")
872
872
  @click.option("--fail-on", type=click.Choice(["CRITICAL", "HIGH", "MEDIUM", "LOW"]),
@@ -889,10 +889,10 @@ def host(
889
889
 
890
890
  \b
891
891
  Examples:
892
- sentinel host
893
- sentinel host --format json
894
- sentinel host --fail-on HIGH
895
- sentinel host --ignore-rule HOST_LARGE_MEMORY
892
+ sentinel host-scan
893
+ sentinel host-scan --format json
894
+ sentinel host-scan --fail-on HIGH
895
+ sentinel host-scan --ignore-rule HOST_LARGE_MEMORY
896
896
  """
897
897
  from agentsentinel_cli.host_scanner import scan_host
898
898
  from agentsentinel_cli.host_rules import run_host_rules, host_posture_score
@@ -61,7 +61,7 @@ def print_host_result(ctx: HostContext, findings: list[HostFinding], score: int)
61
61
  console.print()
62
62
  console.print(Panel.fit(
63
63
  "[bold white]AgentSentinel — Host AI Security Posture[/bold white]\n"
64
- "[dim]Local AI tools · macOS privacy permissions · system security[/dim]",
64
+ "[dim]AI tools · privacy permissions · system security[/dim]",
65
65
  border_style="bright_blue",
66
66
  padding=(0, 2),
67
67
  ))
@@ -94,12 +94,22 @@ def print_host_result(ctx: HostContext, findings: list[HostFinding], score: int)
94
94
  else:
95
95
  console.print(" [dim]Claude Desktop config not found[/dim]")
96
96
 
97
- # MCP servers table
97
+ if ctx.vendor_configs:
98
+ console.print()
99
+ for vc in ctx.vendor_configs:
100
+ console.print(
101
+ f" [bold white]{vc.display_name}[/bold white] [dim]{vc.path}[/dim]\n"
102
+ f" [dim] {len(vc.mcp_servers)} MCP server(s)[/dim]"
103
+ )
104
+
105
+ # MCP servers table — all sources combined
98
106
  all_servers = []
99
107
  if ctx.claude_code:
100
108
  all_servers.extend((s, "Claude Code") for s in ctx.claude_code.mcp_servers)
101
109
  if ctx.claude_desktop:
102
110
  all_servers.extend((s, "Desktop") for s in ctx.claude_desktop.mcp_servers)
111
+ for vc in ctx.vendor_configs:
112
+ all_servers.extend((s, vc.display_name) for s in vc.mcp_servers)
103
113
 
104
114
  if all_servers:
105
115
  console.print()
@@ -224,6 +234,14 @@ def as_host_json(ctx: HostContext, findings: list[HostFinding], score: int) -> s
224
234
  "filesystem_paths": s.filesystem_paths,
225
235
  "env_keys": s.env_keys,
226
236
  })
237
+ for vc in ctx.vendor_configs:
238
+ for s in vc.mcp_servers:
239
+ all_mcp.append({
240
+ "name": s.name, "source": vc.vendor,
241
+ "has_network_access": s.has_network_access,
242
+ "filesystem_paths": s.filesystem_paths,
243
+ "env_keys": s.env_keys,
244
+ })
227
245
 
228
246
  return json.dumps({
229
247
  "scan_type": "host",
@@ -236,6 +254,11 @@ def as_host_json(ctx: HostContext, findings: list[HostFinding], score: int) -> s
236
254
  "claude_desktop": {
237
255
  "found": ctx.claude_desktop is not None,
238
256
  },
257
+ "vendor_tools": [
258
+ {"vendor": vc.vendor, "display_name": vc.display_name,
259
+ "mcp_server_count": len(vc.mcp_servers)}
260
+ for vc in ctx.vendor_configs
261
+ ],
239
262
  "mcp_servers": all_mcp,
240
263
  "memory": {
241
264
  "file_count": ctx.memory_file_count,
@@ -65,6 +65,8 @@ def _all_mcp_servers(ctx: HostContext) -> list[McpServerConfig]:
65
65
  servers.extend(ctx.claude_code.mcp_servers)
66
66
  if ctx.claude_desktop:
67
67
  servers.extend(ctx.claude_desktop.mcp_servers)
68
+ for vc in ctx.vendor_configs:
69
+ servers.extend(vc.mcp_servers)
68
70
  return servers
69
71
 
70
72
 
@@ -375,7 +377,7 @@ def _rule_sensitive_path(ctx: HostContext) -> HostFinding | None:
375
377
 
376
378
 
377
379
  def _rule_many_mcp_servers(ctx: HostContext) -> HostFinding | None:
378
- """MEDIUM: large number of MCP servers configured expands the attack surface."""
380
+ """MEDIUM: large number of MCP servers across all AI tools expands the attack surface."""
379
381
  seen: set[str] = set()
380
382
  unique = []
381
383
  for s in _all_mcp_servers(ctx):
@@ -385,18 +387,25 @@ def _rule_many_mcp_servers(ctx: HostContext) -> HostFinding | None:
385
387
  if len(unique) < 8:
386
388
  return None
387
389
  names = ", ".join(s.name for s in unique[:6])
390
+ tools: list[str] = []
391
+ if ctx.claude_code:
392
+ tools.append("Claude Code")
393
+ if ctx.claude_desktop:
394
+ tools.append("Claude Desktop")
395
+ tools.extend(vc.display_name for vc in ctx.vendor_configs)
396
+ tool_str = ", ".join(tools) if tools else "your AI tools"
388
397
  return HostFinding(
389
398
  severity="MEDIUM",
390
399
  rule_id="HOST_MANY_MCP_SERVERS",
391
400
  category="config",
392
401
  message=(
393
- f"{len(unique)} MCP servers are configured. "
402
+ f"{len(unique)} MCP servers are configured across {tool_str}. "
394
403
  "Each server is an independent prompt injection entry point. "
395
404
  "The more servers installed, the larger the blast radius of a single compromise."
396
405
  ),
397
406
  detail=f"Servers: {names}{'…' if len(unique) > 6 else ''}",
398
407
  remediation=(
399
- "Remove MCP servers you do not actively use. "
408
+ "Remove MCP servers you do not actively use across all AI tools. "
400
409
  "Review each server's capabilities and remove any that duplicate functionality."
401
410
  ),
402
411
  )
@@ -7,6 +7,7 @@ No network calls — all checks are local and read-only.
7
7
 
8
8
  import dataclasses
9
9
  import json
10
+ import os
10
11
  import re
11
12
  import subprocess
12
13
  from pathlib import Path
@@ -37,6 +38,15 @@ class ClaudeDesktopConfig:
37
38
  mcp_servers: list[McpServerConfig]
38
39
 
39
40
 
41
+ @dataclasses.dataclass
42
+ class VendorConfig:
43
+ """Configuration found for a third-party AI coding tool or agent runtime."""
44
+ vendor: str # "cursor" | "windsurf" | "continue" | "gemini_cli" | "vscode"
45
+ display_name: str # human-readable
46
+ path: Path
47
+ mcp_servers: list[McpServerConfig]
48
+
49
+
40
50
  @dataclasses.dataclass
41
51
  class TccPermission:
42
52
  app_name: str
@@ -59,6 +69,7 @@ class HostContext:
59
69
  """Aggregated host AI security posture data — passed to every rule."""
60
70
  claude_code: ClaudeCodeSettings | None
61
71
  claude_desktop: ClaudeDesktopConfig | None
72
+ vendor_configs: list[VendorConfig]
62
73
  memory_file_count: int
63
74
  memory_total_bytes: int
64
75
  shell_key_findings: list[tuple[str, str, str]] # (key_type, file_path, redacted_snippet)
@@ -389,6 +400,98 @@ def _scan_exposed_processes() -> list[ExposedProcess]:
389
400
  return exposed
390
401
 
391
402
 
403
+ # ── Third-party AI tool configs ───────────────────────────────────────────────
404
+
405
+ def _dig(data: object, keys: list[str]) -> object:
406
+ """Navigate a nested dict by key path; returns None if any key is absent."""
407
+ for k in keys:
408
+ if not isinstance(data, dict):
409
+ return None
410
+ data = data.get(k) # type: ignore[union-attr]
411
+ if data is None:
412
+ return None
413
+ return data
414
+
415
+
416
+ def _parse_vendor_mcp(raw: dict, key_path: list[str], list_format: bool) -> list[McpServerConfig]:
417
+ """Extract MCP server configs from a vendor settings file.
418
+
419
+ list_format=True handles Continue.dev's array format;
420
+ False handles the dict format used by Cursor, Windsurf, Gemini CLI, and VS Code.
421
+ """
422
+ data = _dig(raw, key_path)
423
+ if not data:
424
+ return []
425
+ if list_format and isinstance(data, list):
426
+ return [
427
+ _analyze_mcp_server(item.get("name", f"server_{i}"), item)
428
+ for i, item in enumerate(data)
429
+ if isinstance(item, dict)
430
+ ]
431
+ if not list_format and isinstance(data, dict):
432
+ return [_analyze_mcp_server(k, v) for k, v in data.items() if isinstance(v, dict)]
433
+ return []
434
+
435
+
436
+ def _read_vendor_configs() -> list[VendorConfig]:
437
+ """Discover MCP server configs for Cursor, Windsurf, Continue.dev, Gemini CLI, and VS Code."""
438
+ home = Path.home()
439
+ appdata = Path(os.environ["APPDATA"]) if "APPDATA" in os.environ else None
440
+
441
+ # (vendor_id, display_name, candidate_paths, mcp_json_key_path, list_format)
442
+ # key_path navigates nested keys: ["mcp", "servers"] → raw["mcp"]["servers"]
443
+ # list_format: True = array of {name, command, args}, False = dict of {name: {command, args}}
444
+ specs: list[tuple[str, str, list[Path], list[str], bool]] = [
445
+ ("cursor", "Cursor", [
446
+ home / ".cursor" / "mcp.json",
447
+ home / "Library" / "Application Support" / "Cursor" / "User" / "settings.json",
448
+ home / ".config" / "Cursor" / "User" / "settings.json",
449
+ *([appdata / "Cursor" / "User" / "settings.json"] if appdata else []),
450
+ ], ["mcpServers"], False),
451
+
452
+ ("windsurf", "Windsurf", [
453
+ home / ".codeium" / "windsurf" / "mcp_config.json",
454
+ home / "Library" / "Application Support" / "Windsurf" / "User" / "settings.json",
455
+ home / ".config" / "Windsurf" / "User" / "settings.json",
456
+ *([appdata / "Windsurf" / "User" / "settings.json"] if appdata else []),
457
+ ], ["mcpServers"], False),
458
+
459
+ ("continue", "Continue.dev", [
460
+ home / ".continue" / "config.json",
461
+ ], ["mcpServers"], True),
462
+
463
+ ("gemini_cli", "Gemini CLI", [
464
+ home / ".gemini" / "settings.json",
465
+ ], ["mcpServers"], False),
466
+
467
+ ("vscode", "VS Code", [
468
+ home / "Library" / "Application Support" / "Code" / "User" / "settings.json",
469
+ home / ".config" / "Code" / "User" / "settings.json",
470
+ *([appdata / "Code" / "User" / "settings.json"] if appdata else []),
471
+ ], ["mcp", "servers"], False),
472
+ ]
473
+
474
+ configs: list[VendorConfig] = []
475
+ for vendor_id, display_name, candidate_paths, key_path, list_fmt in specs:
476
+ for path in candidate_paths:
477
+ if not path.exists():
478
+ continue
479
+ try:
480
+ raw = json.loads(path.read_text())
481
+ except (json.JSONDecodeError, OSError):
482
+ continue
483
+ servers = _parse_vendor_mcp(raw, key_path, list_fmt)
484
+ if servers:
485
+ configs.append(VendorConfig(
486
+ vendor=vendor_id,
487
+ display_name=display_name,
488
+ path=path,
489
+ mcp_servers=servers,
490
+ ))
491
+ break # use first matching path per vendor
492
+ return configs
493
+
494
+
392
495
  # ── Main entry point ──────────────────────────────────────────────────────────
393
496
 
394
497
  def scan_host() -> HostContext:
@@ -397,6 +500,7 @@ def scan_host() -> HostContext:
397
500
 
398
501
  claude_code = _read_claude_code_settings()
399
502
  claude_desktop = _read_claude_desktop_config()
503
+ vendor_configs = _read_vendor_configs()
400
504
  mem_count, mem_bytes = _scan_memory_files()
401
505
  shell_keys = _scan_shell_configs()
402
506
  tcc_perms, tcc_err = _read_tcc_permissions()
@@ -406,6 +510,7 @@ def scan_host() -> HostContext:
406
510
  return HostContext(
407
511
  claude_code=claude_code,
408
512
  claude_desktop=claude_desktop,
513
+ vendor_configs=vendor_configs,
409
514
  memory_file_count=mem_count,
410
515
  memory_total_bytes=mem_bytes,
411
516
  shell_key_findings=shell_keys,
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ VERSION="0.9.2"
5
+ TWINE_USERNAME="__token__"
6
+ TWINE_PASSWORD="pypi-AgEIcHlwaS5vcmcCJDIyMGE5ZmVlLTQzZjctNDNlZS04NjNmLThhYTJlODVlNDFiYwACKlszLCIwMDRjN2Y1MS00YmQ4LTQwNDAtYmI3MS0zMjk1MWQ3MWM4NWQiXQAABiD4KieIB_qwkmr9tGmf_2M0fWCV0bMHI4d7jG8fMuPdnw"
7
+
8
+ export TWINE_USERNAME TWINE_PASSWORD
9
+
10
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
11
+ cd "$REPO_ROOT"
12
+
13
+ echo "==> Building agentsentinel-cli v${VERSION}"
14
+
15
+ # Update version in pyproject.toml
16
+ sed -i '' "s/^version = .*/version = \"${VERSION}\"/" pyproject.toml
17
+ echo " pyproject.toml version set to ${VERSION}"
18
+
19
+ # Clean previous build artifacts
20
+ rm -rf dist/ build/ src/*.egg-info agentsentinel_cli.egg-info
21
+ echo " dist/ cleaned"
22
+
23
+ # Build sdist + wheel
24
+ python3 -m build
25
+ echo " build complete"
26
+
27
+ # Upload to PyPI
28
+ echo "==> Uploading to PyPI"
29
+ twine upload dist/agentsentinel_cli-${VERSION}*
30
+
31
+ echo "==> Done — https://pypi.org/project/agentsentinel-cli/${VERSION}/"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentsentinel-cli"
7
- version = "0.9.0"
7
+ version = "0.9.2"
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"
@@ -1,6 +1,6 @@
1
1
  # Notes
2
2
  ls dist/
3
-
3
+ rm -rf dist/&
4
4
  python3.11 -m build
5
5
  twine upload dist/agentsentinel_cli-0.7.3* --username __token__ --password $PYPI_TOKEN
6
6
 
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ VERSION="0.9.2"
5
+ TWINE_USERNAME="__token__"
6
+ TWINE_PASSWORD="pypi-AgEIcHlwaS5vcmcCJDIyMGE5ZmVlLTQzZjctNDNlZS04NjNmLThhYTJlODVlNDFiYwACKlszLCIwMDRjN2Y1MS00YmQ4LTQwNDAtYmI3MS0zMjk1MWQ3MWM4NWQiXQAABiD4KieIB_qwkmr9tGmf_2M0fWCV0bMHI4d7jG8fMuPdnw"
7
+
8
+ export TWINE_USERNAME TWINE_PASSWORD
9
+
10
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
11
+ cd "$REPO_ROOT"
12
+
13
+ echo "==> Building agentsentinel-cli v${VERSION}"
14
+
15
+ # Update version in pyproject.toml
16
+ sed -i '' "s/^version = .*/version = \"${VERSION}\"/" pyproject.toml
17
+ echo " pyproject.toml version set to ${VERSION}"
18
+
19
+ # Clean previous build artifacts
20
+ rm -rf dist/ build/ src/*.egg-info agentsentinel_cli.egg-info
21
+ echo " dist/ cleaned"
22
+
23
+ # Build sdist + wheel
24
+ pyproject-build
25
+ echo " build complete"
26
+
27
+ # Upload to PyPI
28
+ echo "==> Uploading to PyPI"
29
+ twine upload dist/agentsentinel_cli-${VERSION}*
30
+
31
+ echo "==> Done — https://pypi.org/project/agentsentinel-cli/${VERSION}/"
@@ -0,0 +1,53 @@
1
+ # PyPI Upload Steps for agentsentinel-cli
2
+
3
+ ## Prerequisites
4
+
5
+ - `twine` installed via Homebrew: `brew install twine`
6
+ - A PyPI account with an API token (generate at https://pypi.org/manage/account/token/)
7
+
8
+ ## Steps
9
+
10
+ ### 1. Bump the version
11
+
12
+ Edit `pyproject.toml`:
13
+
14
+ ```toml
15
+ version = "0.9.0" # increment as appropriate
16
+ ```
17
+
18
+ ### 2. Build the distributions
19
+
20
+ Use `pipx run build` to avoid the Homebrew externally-managed-environment restriction:
21
+
22
+ ```bash
23
+ pipx run build
24
+ ```
25
+
26
+ This produces two files in `dist/`:
27
+ - `agentsentinel_cli-<version>-py3-none-any.whl`
28
+ - `agentsentinel_cli-<version>.tar.gz`
29
+
30
+ ### 3. Upload to PyPI
31
+
32
+ Pass credentials directly on the command line (twine cannot prompt interactively in this terminal):
33
+
34
+ ```bash
35
+ twine upload dist/agentsentinel_cli-<version>* -u __token__ -p pypi-YOUR_TOKEN_HERE
36
+ ```
37
+
38
+ Replace `pypi-YOUR_TOKEN_HERE` with your actual PyPI API token.
39
+
40
+ ### 4. Verify
41
+
42
+ Check the release is live at:
43
+ ```
44
+ https://pypi.org/project/agentsentinel-cli/
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Notes
50
+
51
+ - `python3 -m build` fails because `/opt/homebrew/bin/python3` (3.12) and `pip` (3.11) are mismatched — use `pipx run build` instead.
52
+ - `twine upload` without `-u`/`-p` flags will hang trying to prompt for a password — always pass them inline.
53
+ - Old dist files are not automatically removed. The `dist/` folder accumulates across versions; only the files matching `<version>` are uploaded.