agentsentinel-cli 0.9.1__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.
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/PKG-INFO +60 -5
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/README.md +59 -4
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/host_report.py +25 -2
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/host_rules.py +12 -3
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/host_scanner.py +105 -0
- agentsentinel_cli-0.9.2/publish.sh +31 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/pyproject.toml +1 -1
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/tmp/note.md +1 -1
- agentsentinel_cli-0.9.2/tmp/publish.sh +31 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/.gitignore +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/DOCUMENTATION.md +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/LICENSE +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/__init__.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/a2a_report.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/a2a_rules.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/a2a_scanner.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/cli.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/discover.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/discover_report.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/fingerprint.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/frameworks.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/inspect.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/inspect_report.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/mcp_client.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/mcp_report.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/mcp_rules.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/report.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/rules.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/scanner.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/secrets.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/secrets_report.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/secrets_rules.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/supply_chain_ai.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/supply_chain_report.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/supply_chain_rules.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/suppress.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/tmp/pypi.md +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/tmp/test-mcp-agent/README.md +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/tmp/test-mcp-agent/langchain_agent.py +0 -0
- {agentsentinel_cli-0.9.1 → agentsentinel_cli-0.9.2}/tmp/test-mcp-agent/mcp_server.py +0 -0
- {agentsentinel_cli-0.9.1 → 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.
|
|
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
|
---
|
|
@@ -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]
|
|
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
|
-
|
|
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
|
|
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.
|
|
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"
|
|
@@ -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}/"
|
|
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.1 → agentsentinel_cli-0.9.2}/agentsentinel_cli/supply_chain_report.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|