agentsentinel-cli 0.9.6__tar.gz → 0.9.7__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.6 → agentsentinel_cli-0.9.7}/PKG-INFO +47 -20
  2. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/README.md +46 -19
  3. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/cli.py +20 -4
  4. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/redteam/mcp_oauth.py +97 -13
  5. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/redteam/mcp_preauth.py +77 -4
  6. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/pyproject.toml +1 -1
  7. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/.gitignore +0 -0
  8. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/DOCUMENTATION.md +0 -0
  9. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/LICENSE +0 -0
  10. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/__init__.py +0 -0
  11. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/a2a_report.py +0 -0
  12. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/a2a_rules.py +0 -0
  13. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/a2a_scanner.py +0 -0
  14. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/discover.py +0 -0
  15. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/discover_report.py +0 -0
  16. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/fingerprint.py +0 -0
  17. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/frameworks.py +0 -0
  18. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/host_report.py +0 -0
  19. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/host_rules.py +0 -0
  20. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/host_scanner.py +0 -0
  21. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/inspect.py +0 -0
  22. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/inspect_report.py +0 -0
  23. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/mcp_client.py +0 -0
  24. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/mcp_report.py +0 -0
  25. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/mcp_rules.py +0 -0
  26. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/redteam/__init__.py +0 -0
  27. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/redteam/mcp_auth.py +0 -0
  28. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/redteam/mcp_fuzz.py +0 -0
  29. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/redteam/mcp_inject.py +0 -0
  30. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/redteam/mcp_poison.py +0 -0
  31. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/redteam/mcp_recon.py +0 -0
  32. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/redteam/models.py +0 -0
  33. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/redteam/payloads.py +0 -0
  34. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/redteam/report.py +0 -0
  35. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/redteam/transport.py +0 -0
  36. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/report.py +0 -0
  37. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/rules.py +0 -0
  38. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/scanner.py +0 -0
  39. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/secrets.py +0 -0
  40. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/secrets_report.py +0 -0
  41. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/secrets_rules.py +0 -0
  42. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/supply_chain_ai.py +0 -0
  43. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/supply_chain_report.py +0 -0
  44. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/agentsentinel_cli/supply_chain_rules.py +0 -0
  45. {agentsentinel_cli-0.9.6 → agentsentinel_cli-0.9.7}/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.6
3
+ Version: 0.9.7
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
@@ -91,8 +91,9 @@ sentinel host-scan --fail-on HIGH
91
91
 
92
92
  # Active red-team — real attacks, confirmed exploitation
93
93
  sentinel redteam mcp full http://localhost:8000
94
+ sentinel redteam mcp preauth http://localhost:8000 # works with zero credentials
94
95
  sentinel redteam mcp inject http://localhost:8000 --type traverse --type ssrf
95
- sentinel redteam mcp auth http://localhost:8000
96
+ sentinel redteam mcp auth http://localhost:8000 # credential bypass + OAuth 2.0
96
97
  ```
97
98
 
98
99
  ---
@@ -392,20 +393,27 @@ No API key required. No network calls.
392
393
 
393
394
  The active red-team module for MCP servers. Every finding is backed by confirmed evidence from the server's actual response — no heuristics, no noise. If a traversal finding says it read `/etc/passwd`, it read `/etc/passwd`.
394
395
 
396
+ **`preauth` and OAuth probing run with zero credentials** — useful when the target server blocks unauthenticated MCP access entirely.
397
+
395
398
  Requires `httpx`: `pip install "agentsentinel-cli[mcp]"`
396
399
 
397
400
  ```bash
398
- # Full run — all 5 phases, unified report
401
+ # Full run — all 7 phases, unified report
399
402
  sentinel redteam mcp full http://localhost:8000
400
403
  sentinel redteam mcp full http://localhost:8000 --intensity high --format json
401
404
 
402
405
  # Targeted phases
406
+ sentinel redteam mcp preauth http://localhost:8000 # HTTP fingerprint — zero creds required
403
407
  sentinel redteam mcp recon http://localhost:8000 # enumerate attack surface
404
- sentinel redteam mcp auth http://localhost:8000 # auth bypass (5 credential scenarios)
408
+ sentinel redteam mcp auth http://localhost:8000 # credential bypass + OAuth 2.0 attacks
405
409
  sentinel redteam mcp inject http://localhost:8000 # all injection techniques
406
410
  sentinel redteam mcp poison http://localhost:8000 # tool description + result injection
407
411
  sentinel redteam mcp fuzz http://localhost:8000 # schema and type boundary fuzzing
408
412
 
413
+ # Preauth — works even when server blocks unauthenticated MCP
414
+ sentinel redteam mcp preauth http://localhost:8000
415
+ sentinel redteam mcp preauth http://locked-server:8000 # CORS, OAuth metadata, version disclosure
416
+
409
417
  # Surgical injection — pick your techniques
410
418
  sentinel redteam mcp inject http://localhost:8000 --type traverse
411
419
  sentinel redteam mcp inject http://localhost:8000 --type traverse --type ssrf
@@ -425,15 +433,31 @@ sentinel redteam mcp full http://localhost:8000 --fail-on CRITICAL
425
433
  sentinel redteam mcp full http://localhost:8000 --output report.json
426
434
  ```
427
435
 
428
- **Phases:**
429
-
430
- | Phase | Command | What it tests |
431
- |-------|---------|---------------|
432
- | 1 — Recon | `recon` | Tool inventory, resource listing, dangerous capability flags |
433
- | 2 — Auth Bypass | `auth` | 5 credential scenarios: no creds, empty bearer, garbage token, invalid JWT, JWT alg:none |
434
- | 3 — Injection | `inject` | Path traversal, SSRF, command injection, SQL injection payload fired, pattern matched in response |
435
- | 4 — Poison | `poison` | Static: adversarial instructions in tool descriptions. Dynamic: LLM instruction injection via tool parameters |
436
- | 5 — Fuzz | `fuzz` | Stack traces, internal path disclosure, template injection eval, type confusion |
436
+ **Phases (`full` runs all 7):**
437
+
438
+ | Phase | Command | Needs credentials | What it tests |
439
+ |-------|---------|:-----------------:|---------------|
440
+ | 1 — Pre-auth | `preauth` | No | CORS, OAuth metadata, version disclosure, unauthenticated paths, SSE stream, error disclosure |
441
+ | 2 — OAuth | *(auto in `auth`/`full`)* | No | Public client registration, token without secret, PKCE downgrade, scope escalation, X-Agent-Scopes forgery |
442
+ | 3 — Recon | `recon` | Yes | Tool inventory, resource listing, dangerous capability flags with input schemas |
443
+ | 4 — Auth Bypass | `auth` | Optional | 5 credential scenarios: no creds, empty bearer, garbage token, invalid JWT, JWT alg:none |
444
+ | 5 — Injection | `inject` | Yes | Path traversal, SSRF, command injection, SQL injection evidence-confirmed only |
445
+ | 6 — Poison | `poison` | Yes | Adversarial instructions in tool descriptions; LLM injection via tool parameters |
446
+ | 7 — Fuzz | `fuzz` | Yes | Stack traces, path disclosure, template injection eval, type confusion, input reflection |
447
+
448
+ **Pre-auth probes (zero credentials):**
449
+
450
+ | Probe | CRITICAL | HIGH | MEDIUM |
451
+ |-------|----------|------|--------|
452
+ | CORS wildcard + credentials | ✓ | | |
453
+ | CORS wildcard / reflected origin | | ✓ | |
454
+ | Unauthenticated SSE stream | | ✓ | |
455
+ | Public OAuth client registration | | ✓ | |
456
+ | Token issued without `client_secret` | ✓ | | |
457
+ | X-Agent-Scopes header forgery | ✓ | | |
458
+ | PKCE `plain` method accepted | | | ✓ |
459
+ | Server/framework version disclosure | | | ✓ |
460
+ | Unauthenticated `/docs`, `/metrics` | | | ✓ |
437
461
 
438
462
  **Injection techniques (`--type`):**
439
463
 
@@ -457,13 +481,13 @@ sentinel redteam mcp full http://localhost:8000 --output report.json
457
481
 
458
482
  | Severity | Example |
459
483
  |----------|---------|
460
- | CRITICAL | Path traversal confirmed — `/etc/passwd` content in response |
461
- | HIGH | LLM instruction injection — sentinel reflected in clean tool result |
462
- | MEDIUM | Input reflected in error message (injection vector, lower confidence) |
484
+ | CRITICAL | Path traversal confirmed — `/etc/passwd` content in response; OAuth token issued without secret |
485
+ | HIGH | LLM instruction injection — sentinel reflected in clean tool result; CORS wildcard |
486
+ | MEDIUM | Input reflected in error message; PKCE plain method accepted; version disclosure |
463
487
  | LOW | Unexpected content returned on malformed input |
464
- | INFO | Auth enforced on handshake, tool inventory |
488
+ | INFO | Auth enforced on handshake, tool inventory, OAuth metadata discovered |
465
489
 
466
- Every finding includes a **MITRE ATLAS** ID and **OWASP ASI** ID. Use `--verbose` to see full request/response bodies.
490
+ Every finding includes a **Fix** line, a **MITRE ATLAS** ID, and an **OWASP ASI** ID. Confirmed multi-step attack chains are synthesized in the report. Use `--verbose` to see full request/response bodies.
467
491
 
468
492
  ---
469
493
 
@@ -496,10 +520,10 @@ Supported on: `sentinel scan`, `sentinel a2a`, `sentinel mcp scan`, `sentinel su
496
520
  |------------|-----|------------------|
497
521
  | Agent Goal Hijack | ASI01 | `sentinel scan` (PROMPT_INJECTION_VECTOR), `sentinel supply-chain` (SC01), **`sentinel redteam mcp poison`** (confirmed injection) |
498
522
  | Tool Misuse & Exploitation | ASI02 | `sentinel mcp scan`, `sentinel scan`, **`sentinel redteam mcp inject`** (confirmed exploitation) |
499
- | Agent Identity & Privilege Abuse | ASI03 | `sentinel scan` (PRIVILEGE_EXCESS), `sentinel host-scan` (HOST_SHELL_UNRESTRICTED), **`sentinel redteam mcp auth`** (bypass confirmation) |
523
+ | Agent Identity & Privilege Abuse | ASI03 | `sentinel scan` (PRIVILEGE_EXCESS), `sentinel host-scan` (HOST_SHELL_UNRESTRICTED), **`sentinel redteam mcp auth`** (credential bypass + OAuth scope escalation + X-Agent-Scopes forgery) |
500
524
  | **Agentic Supply Chain Compromise** | **ASI04** | **`sentinel supply-chain`** (static + AI semantic analysis), **`sentinel redteam mcp poison`** (static description scan) |
501
525
  | Unexpected Code Execution | ASI05 | `sentinel scan` (CODE_EXECUTION_GRANT), `sentinel mcp scan` (CODE_EXECUTION_TOOL), **`sentinel redteam mcp inject --type cmd`** |
502
- | **Memory & Context Poisoning** | **ASI06** | **`sentinel secrets`** (memory contamination, system prompt leakage), `sentinel host-scan` (HOST_LARGE_MEMORY) |
526
+ | **Memory & Context Poisoning** | **ASI06** | **`sentinel secrets`** (memory contamination, system prompt leakage), `sentinel host-scan` (HOST_LARGE_MEMORY), **`sentinel redteam mcp preauth`** (CORS misconfiguration enabling cross-origin data theft) |
503
527
  | **Insecure Inter-Agent Communication** | **ASI07** | **`sentinel a2a`** (call graph + trust rules) |
504
528
  | Cascading Agent Failures | ASI08 | `sentinel discover` (surface unmonitored agents) |
505
529
  | Rogue Agents | ASI10 | `sentinel discover` (find agents that shouldn't exist), `sentinel host-scan` (HOST_AI_PROCESS_EXPOSED) |
@@ -540,6 +564,9 @@ jobs:
540
564
  - name: Host AI security posture
541
565
  run: sentinel host-scan --fail-on HIGH
542
566
 
567
+ - name: MCP pre-auth probe (zero credentials needed)
568
+ run: sentinel redteam mcp preauth http://localhost:8000 --fail-on HIGH
569
+
543
570
  - name: MCP red-team (active exploitation check)
544
571
  run: sentinel redteam mcp full http://localhost:8000 --fail-on CRITICAL
545
572
  ```
@@ -57,8 +57,9 @@ sentinel host-scan --fail-on HIGH
57
57
 
58
58
  # Active red-team — real attacks, confirmed exploitation
59
59
  sentinel redteam mcp full http://localhost:8000
60
+ sentinel redteam mcp preauth http://localhost:8000 # works with zero credentials
60
61
  sentinel redteam mcp inject http://localhost:8000 --type traverse --type ssrf
61
- sentinel redteam mcp auth http://localhost:8000
62
+ sentinel redteam mcp auth http://localhost:8000 # credential bypass + OAuth 2.0
62
63
  ```
63
64
 
64
65
  ---
@@ -358,20 +359,27 @@ No API key required. No network calls.
358
359
 
359
360
  The active red-team module for MCP servers. Every finding is backed by confirmed evidence from the server's actual response — no heuristics, no noise. If a traversal finding says it read `/etc/passwd`, it read `/etc/passwd`.
360
361
 
362
+ **`preauth` and OAuth probing run with zero credentials** — useful when the target server blocks unauthenticated MCP access entirely.
363
+
361
364
  Requires `httpx`: `pip install "agentsentinel-cli[mcp]"`
362
365
 
363
366
  ```bash
364
- # Full run — all 5 phases, unified report
367
+ # Full run — all 7 phases, unified report
365
368
  sentinel redteam mcp full http://localhost:8000
366
369
  sentinel redteam mcp full http://localhost:8000 --intensity high --format json
367
370
 
368
371
  # Targeted phases
372
+ sentinel redteam mcp preauth http://localhost:8000 # HTTP fingerprint — zero creds required
369
373
  sentinel redteam mcp recon http://localhost:8000 # enumerate attack surface
370
- sentinel redteam mcp auth http://localhost:8000 # auth bypass (5 credential scenarios)
374
+ sentinel redteam mcp auth http://localhost:8000 # credential bypass + OAuth 2.0 attacks
371
375
  sentinel redteam mcp inject http://localhost:8000 # all injection techniques
372
376
  sentinel redteam mcp poison http://localhost:8000 # tool description + result injection
373
377
  sentinel redteam mcp fuzz http://localhost:8000 # schema and type boundary fuzzing
374
378
 
379
+ # Preauth — works even when server blocks unauthenticated MCP
380
+ sentinel redteam mcp preauth http://localhost:8000
381
+ sentinel redteam mcp preauth http://locked-server:8000 # CORS, OAuth metadata, version disclosure
382
+
375
383
  # Surgical injection — pick your techniques
376
384
  sentinel redteam mcp inject http://localhost:8000 --type traverse
377
385
  sentinel redteam mcp inject http://localhost:8000 --type traverse --type ssrf
@@ -391,15 +399,31 @@ sentinel redteam mcp full http://localhost:8000 --fail-on CRITICAL
391
399
  sentinel redteam mcp full http://localhost:8000 --output report.json
392
400
  ```
393
401
 
394
- **Phases:**
395
-
396
- | Phase | Command | What it tests |
397
- |-------|---------|---------------|
398
- | 1 — Recon | `recon` | Tool inventory, resource listing, dangerous capability flags |
399
- | 2 — Auth Bypass | `auth` | 5 credential scenarios: no creds, empty bearer, garbage token, invalid JWT, JWT alg:none |
400
- | 3 — Injection | `inject` | Path traversal, SSRF, command injection, SQL injection payload fired, pattern matched in response |
401
- | 4 — Poison | `poison` | Static: adversarial instructions in tool descriptions. Dynamic: LLM instruction injection via tool parameters |
402
- | 5 — Fuzz | `fuzz` | Stack traces, internal path disclosure, template injection eval, type confusion |
402
+ **Phases (`full` runs all 7):**
403
+
404
+ | Phase | Command | Needs credentials | What it tests |
405
+ |-------|---------|:-----------------:|---------------|
406
+ | 1 — Pre-auth | `preauth` | No | CORS, OAuth metadata, version disclosure, unauthenticated paths, SSE stream, error disclosure |
407
+ | 2 — OAuth | *(auto in `auth`/`full`)* | No | Public client registration, token without secret, PKCE downgrade, scope escalation, X-Agent-Scopes forgery |
408
+ | 3 — Recon | `recon` | Yes | Tool inventory, resource listing, dangerous capability flags with input schemas |
409
+ | 4 — Auth Bypass | `auth` | Optional | 5 credential scenarios: no creds, empty bearer, garbage token, invalid JWT, JWT alg:none |
410
+ | 5 — Injection | `inject` | Yes | Path traversal, SSRF, command injection, SQL injection evidence-confirmed only |
411
+ | 6 — Poison | `poison` | Yes | Adversarial instructions in tool descriptions; LLM injection via tool parameters |
412
+ | 7 — Fuzz | `fuzz` | Yes | Stack traces, path disclosure, template injection eval, type confusion, input reflection |
413
+
414
+ **Pre-auth probes (zero credentials):**
415
+
416
+ | Probe | CRITICAL | HIGH | MEDIUM |
417
+ |-------|----------|------|--------|
418
+ | CORS wildcard + credentials | ✓ | | |
419
+ | CORS wildcard / reflected origin | | ✓ | |
420
+ | Unauthenticated SSE stream | | ✓ | |
421
+ | Public OAuth client registration | | ✓ | |
422
+ | Token issued without `client_secret` | ✓ | | |
423
+ | X-Agent-Scopes header forgery | ✓ | | |
424
+ | PKCE `plain` method accepted | | | ✓ |
425
+ | Server/framework version disclosure | | | ✓ |
426
+ | Unauthenticated `/docs`, `/metrics` | | | ✓ |
403
427
 
404
428
  **Injection techniques (`--type`):**
405
429
 
@@ -423,13 +447,13 @@ sentinel redteam mcp full http://localhost:8000 --output report.json
423
447
 
424
448
  | Severity | Example |
425
449
  |----------|---------|
426
- | CRITICAL | Path traversal confirmed — `/etc/passwd` content in response |
427
- | HIGH | LLM instruction injection — sentinel reflected in clean tool result |
428
- | MEDIUM | Input reflected in error message (injection vector, lower confidence) |
450
+ | CRITICAL | Path traversal confirmed — `/etc/passwd` content in response; OAuth token issued without secret |
451
+ | HIGH | LLM instruction injection — sentinel reflected in clean tool result; CORS wildcard |
452
+ | MEDIUM | Input reflected in error message; PKCE plain method accepted; version disclosure |
429
453
  | LOW | Unexpected content returned on malformed input |
430
- | INFO | Auth enforced on handshake, tool inventory |
454
+ | INFO | Auth enforced on handshake, tool inventory, OAuth metadata discovered |
431
455
 
432
- Every finding includes a **MITRE ATLAS** ID and **OWASP ASI** ID. Use `--verbose` to see full request/response bodies.
456
+ Every finding includes a **Fix** line, a **MITRE ATLAS** ID, and an **OWASP ASI** ID. Confirmed multi-step attack chains are synthesized in the report. Use `--verbose` to see full request/response bodies.
433
457
 
434
458
  ---
435
459
 
@@ -462,10 +486,10 @@ Supported on: `sentinel scan`, `sentinel a2a`, `sentinel mcp scan`, `sentinel su
462
486
  |------------|-----|------------------|
463
487
  | Agent Goal Hijack | ASI01 | `sentinel scan` (PROMPT_INJECTION_VECTOR), `sentinel supply-chain` (SC01), **`sentinel redteam mcp poison`** (confirmed injection) |
464
488
  | Tool Misuse & Exploitation | ASI02 | `sentinel mcp scan`, `sentinel scan`, **`sentinel redteam mcp inject`** (confirmed exploitation) |
465
- | Agent Identity & Privilege Abuse | ASI03 | `sentinel scan` (PRIVILEGE_EXCESS), `sentinel host-scan` (HOST_SHELL_UNRESTRICTED), **`sentinel redteam mcp auth`** (bypass confirmation) |
489
+ | Agent Identity & Privilege Abuse | ASI03 | `sentinel scan` (PRIVILEGE_EXCESS), `sentinel host-scan` (HOST_SHELL_UNRESTRICTED), **`sentinel redteam mcp auth`** (credential bypass + OAuth scope escalation + X-Agent-Scopes forgery) |
466
490
  | **Agentic Supply Chain Compromise** | **ASI04** | **`sentinel supply-chain`** (static + AI semantic analysis), **`sentinel redteam mcp poison`** (static description scan) |
467
491
  | Unexpected Code Execution | ASI05 | `sentinel scan` (CODE_EXECUTION_GRANT), `sentinel mcp scan` (CODE_EXECUTION_TOOL), **`sentinel redteam mcp inject --type cmd`** |
468
- | **Memory & Context Poisoning** | **ASI06** | **`sentinel secrets`** (memory contamination, system prompt leakage), `sentinel host-scan` (HOST_LARGE_MEMORY) |
492
+ | **Memory & Context Poisoning** | **ASI06** | **`sentinel secrets`** (memory contamination, system prompt leakage), `sentinel host-scan` (HOST_LARGE_MEMORY), **`sentinel redteam mcp preauth`** (CORS misconfiguration enabling cross-origin data theft) |
469
493
  | **Insecure Inter-Agent Communication** | **ASI07** | **`sentinel a2a`** (call graph + trust rules) |
470
494
  | Cascading Agent Failures | ASI08 | `sentinel discover` (surface unmonitored agents) |
471
495
  | Rogue Agents | ASI10 | `sentinel discover` (find agents that shouldn't exist), `sentinel host-scan` (HOST_AI_PROCESS_EXPOSED) |
@@ -506,6 +530,9 @@ jobs:
506
530
  - name: Host AI security posture
507
531
  run: sentinel host-scan --fail-on HIGH
508
532
 
533
+ - name: MCP pre-auth probe (zero credentials needed)
534
+ run: sentinel redteam mcp preauth http://localhost:8000 --fail-on HIGH
535
+
509
536
  - name: MCP red-team (active exploitation check)
510
537
  run: sentinel redteam mcp full http://localhost:8000 --fail-on CRITICAL
511
538
  ```
@@ -1166,20 +1166,25 @@ def redteam_mcp_recon(
1166
1166
  @click.option("--output", "output_path", default=None, metavar="FILE")
1167
1167
  @click.option("--verbose", "-v", is_flag=True, default=False)
1168
1168
  @click.option("--fail-on", type=click.Choice(["CRITICAL", "HIGH", "MEDIUM", "LOW"]), default=None)
1169
+ @click.option("--skip-oauth", "skip_oauth", is_flag=True, default=False,
1170
+ help="Skip OAuth tests against external authorization servers (use when AS is out of scope).")
1169
1171
  def redteam_mcp_preauth(
1170
1172
  target: str | None, timeout: float, fmt: str,
1171
1173
  output_path: str | None, verbose: bool, fail_on: str | None,
1174
+ skip_oauth: bool,
1172
1175
  ) -> None:
1173
1176
  """HTTP-layer fingerprinting — runs with zero credentials.
1174
1177
 
1175
1178
  Probes the server before any MCP initialize attempt: CORS config,
1176
1179
  OAuth metadata, version disclosure, unauthenticated paths, SSE stream.
1180
+ Follows the MCP 2025 OAuth discovery chain to find external auth servers.
1177
1181
  Produces findings even when the server blocks unauthenticated MCP access.
1178
1182
 
1179
1183
  \b
1180
1184
  Examples:
1181
1185
  sentinel redteam mcp preauth http://localhost:3000
1182
1186
  sentinel redteam mcp preauth http://localhost:3000 --verbose
1187
+ sentinel redteam mcp preauth http://localhost:3000 --skip-oauth
1183
1188
  """
1184
1189
  import time
1185
1190
  from agentsentinel_cli.redteam.mcp_preauth import run_preauth
@@ -1198,6 +1203,7 @@ def redteam_mcp_preauth(
1198
1203
  preauth_findings, preauth_count = run_preauth(url=target, timeout=timeout, verbose=verbose)
1199
1204
  oauth_findings, oauth_count = run_oauth(
1200
1205
  url=target, original_headers={}, timeout=timeout, verbose=verbose,
1206
+ skip_external=skip_oauth,
1201
1207
  )
1202
1208
  all_findings = preauth_findings + oauth_findings
1203
1209
 
@@ -1229,9 +1235,12 @@ def redteam_mcp_preauth(
1229
1235
  @click.option("--output", "output_path", default=None, metavar="FILE")
1230
1236
  @click.option("--verbose", "-v", is_flag=True, default=False)
1231
1237
  @click.option("--fail-on", type=click.Choice(["CRITICAL", "HIGH", "MEDIUM", "LOW"]), default=None)
1238
+ @click.option("--skip-oauth", "skip_oauth", is_flag=True, default=False,
1239
+ help="Skip OAuth tests against external authorization servers.")
1232
1240
  def redteam_mcp_auth(
1233
1241
  target: str | None, stdio_cmd: str | None, auth_header: str | None,
1234
1242
  timeout: float, fmt: str, output_path: str | None, verbose: bool, fail_on: str | None,
1243
+ skip_oauth: bool,
1235
1244
  ) -> None:
1236
1245
  """Test authentication bypass — calls every tool with invalid credentials.
1237
1246
 
@@ -1281,6 +1290,7 @@ def redteam_mcp_auth(
1281
1290
  from agentsentinel_cli.redteam.mcp_oauth import run_oauth
1282
1291
  oauth_findings, oauth_count = run_oauth(
1283
1292
  url=target, original_headers=headers, timeout=timeout, verbose=verbose,
1293
+ skip_external=skip_oauth,
1284
1294
  )
1285
1295
  findings = findings + oauth_findings
1286
1296
  scenarios_tested += oauth_count
@@ -1557,10 +1567,13 @@ def redteam_mcp_fuzz(
1557
1567
  help="Save complete JSON evidence bundle.")
1558
1568
  @click.option("--verbose", "-v", is_flag=True, default=False)
1559
1569
  @click.option("--fail-on", type=click.Choice(["CRITICAL", "HIGH", "MEDIUM", "LOW"]), default=None)
1570
+ @click.option("--skip-oauth", "skip_oauth", is_flag=True, default=False,
1571
+ help="Skip OAuth tests against external authorization servers.")
1560
1572
  def redteam_mcp_full(
1561
1573
  target: str | None, stdio_cmd: str | None, auth_header: str | None,
1562
1574
  intensity: str, include_dangerous: bool,
1563
1575
  timeout: float, fmt: str, output_path: str | None, verbose: bool, fail_on: str | None,
1576
+ skip_oauth: bool,
1564
1577
  ) -> None:
1565
1578
  """Run all red-team modules in sequence — full engagement.
1566
1579
 
@@ -1610,6 +1623,7 @@ def redteam_mcp_full(
1610
1623
 
1611
1624
  oauth_findings, oauth_count = run_oauth(
1612
1625
  url=target, original_headers=headers, timeout=timeout, verbose=verbose,
1626
+ skip_external=skip_oauth,
1613
1627
  )
1614
1628
  all_findings.extend(oauth_findings)
1615
1629
  total_attacks += oauth_count
@@ -1673,14 +1687,16 @@ def redteam_mcp_full(
1673
1687
  total_attacks += fuzz_count
1674
1688
 
1675
1689
  except McpAuthRequired as exc:
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.")
1678
- console.print(" Use: [bold]--auth-header 'Authorization: Bearer <token>'[/bold]")
1690
+ if fmt == "text":
1691
+ console.print(f"\n[bold yellow]Auth required[/bold yellow] (HTTP {exc.status_code})")
1692
+ console.print(" Preauth + OAuth findings above are still valid without a token.")
1693
+ console.print(" Use: [bold]--auth-header 'Authorization: Bearer <token>'[/bold]")
1679
1694
  if not all_findings:
1680
1695
  sys.exit(1)
1681
1696
  # Fall through — report whatever preauth/oauth found
1682
1697
  except McpError as exc:
1683
- console.print(f"\n[red]Connection failed:[/red] {exc}")
1698
+ if fmt == "text":
1699
+ console.print(f"\n[red]Connection failed:[/red] {exc}")
1684
1700
  if not all_findings:
1685
1701
  sys.exit(1)
1686
1702
 
@@ -32,6 +32,7 @@ def run_oauth(
32
32
  original_headers: dict[str, str],
33
33
  timeout: float,
34
34
  verbose: bool,
35
+ skip_external: bool = False,
35
36
  ) -> tuple[list[RedTeamFinding], int]:
36
37
  """
37
38
  Run OAuth 2.0 attack surface tests.
@@ -45,16 +46,59 @@ def run_oauth(
45
46
  findings: list[RedTeamFinding] = []
46
47
  probes = 0
47
48
  parsed = urllib.parse.urlparse(url.rstrip("/"))
48
- origin = f"{parsed.scheme}://{parsed.netloc}"
49
+ mcp_origin = f"{parsed.scheme}://{parsed.netloc}"
49
50
 
50
51
  with httpx.Client(timeout=timeout, follow_redirects=True) as client:
51
- meta = _discover_metadata(client, origin)
52
+ meta, as_origin, is_external = _discover_metadata(client, mcp_origin)
52
53
 
53
54
  if meta is None:
54
- # No OAuth server found nothing to test
55
- return findings, probes + 3 # count the 3 discovery probes
56
-
57
- probes += 3 # metadata probes
55
+ return findings, probes + 3 # count the discovery probes
56
+
57
+ probes += 3
58
+
59
+ # Report external AS — always emit the INFO, then optionally skip tests
60
+ if is_external:
61
+ findings.append(RedTeamFinding(
62
+ attack_type="oauth",
63
+ severity="INFO",
64
+ title=f"External OAuth AS discovered — {as_origin}",
65
+ tool_name="<auth-server>",
66
+ parameter=None,
67
+ payload=f"GET {mcp_origin}/.well-known/oauth-protected-resource",
68
+ evidence=(
69
+ f"MCP server ({mcp_origin}) delegates authentication to:\n"
70
+ f" Authorization server: {as_origin}\n"
71
+ f" token_endpoint: {meta.get('token_endpoint', 'unknown')}"
72
+ ),
73
+ exploit_scenario=(
74
+ "The MCP server follows the MCP 2025 spec and delegates OAuth to a "
75
+ f"separate authorization server at {as_origin}. "
76
+ "All OAuth attack tests below target that AS directly. "
77
+ "Use --skip-oauth if this AS is out of scope for your engagement."
78
+ ),
79
+ mitre_id="T1078.004",
80
+ owasp_id="ASI06",
81
+ confidence="HIGH",
82
+ ))
83
+
84
+ if skip_external:
85
+ findings.append(RedTeamFinding(
86
+ attack_type="oauth",
87
+ severity="INFO",
88
+ title=f"OAuth tests skipped — external AS out of scope (--skip-oauth)",
89
+ tool_name="<auth-server>",
90
+ parameter=None,
91
+ payload="",
92
+ evidence=f"Authorization server at {as_origin} not tested per --skip-oauth.",
93
+ exploit_scenario=(
94
+ "OAuth attack tests were suppressed because --skip-oauth was set. "
95
+ "Re-run without --skip-oauth if you have authorization to test the AS."
96
+ ),
97
+ mitre_id=None,
98
+ owasp_id=None,
99
+ confidence="HIGH",
100
+ ))
101
+ return findings, probes
58
102
 
59
103
  # Tests that don't need a valid token
60
104
  probes += _test_public_registration(client, meta, findings, verbose)
@@ -71,16 +115,49 @@ def run_oauth(
71
115
  return findings, probes
72
116
 
73
117
 
74
- def _discover_metadata(client, origin: str) -> dict | None:
75
- """Discover OAuth metadata from well-known endpoints."""
76
- paths = [
118
+ def _discover_metadata(
119
+ client,
120
+ mcp_origin: str,
121
+ ) -> tuple[dict | None, str, bool]:
122
+ """
123
+ Follow the MCP 2025 OAuth discovery chain.
124
+
125
+ Returns (metadata, as_origin, is_external):
126
+ metadata — OAuth AS metadata dict, or None if no AS found
127
+ as_origin — base URL where the AS lives (may differ from MCP server)
128
+ is_external — True when the AS is hosted on a different origin
129
+ """
130
+ # Step 1: MCP 2025 spec — oauth-protected-resource on the MCP server
131
+ # advertises which authorization server to use
132
+ try:
133
+ resp = client.get(f"{mcp_origin}/.well-known/oauth-protected-resource")
134
+ if resp.status_code == 200:
135
+ protected = resp.json()
136
+ auth_servers = protected.get("authorization_servers", [])
137
+ if auth_servers:
138
+ as_base = auth_servers[0].rstrip("/")
139
+ as_meta = _fetch_as_metadata(client, as_base)
140
+ if as_meta:
141
+ return as_meta, as_base, _is_different_origin(mcp_origin, as_base)
142
+ except Exception:
143
+ pass
144
+
145
+ # Step 2: Co-located AS — check AS metadata directly on MCP server origin
146
+ as_meta = _fetch_as_metadata(client, mcp_origin)
147
+ if as_meta:
148
+ return as_meta, mcp_origin, False
149
+
150
+ return None, mcp_origin, False
151
+
152
+
153
+ def _fetch_as_metadata(client, base_url: str) -> dict | None:
154
+ """Try standard OAuth AS discovery paths on a given base URL."""
155
+ for path in [
77
156
  "/.well-known/oauth-authorization-server",
78
157
  "/.well-known/openid-configuration",
79
- "/.well-known/oauth-protected-resource",
80
- ]
81
- for path in paths:
158
+ ]:
82
159
  try:
83
- resp = client.get(f"{origin}{path}")
160
+ resp = client.get(f"{base_url}{path}")
84
161
  if resp.status_code == 200:
85
162
  return resp.json()
86
163
  except Exception:
@@ -88,6 +165,13 @@ def _discover_metadata(client, origin: str) -> dict | None:
88
165
  return None
89
166
 
90
167
 
168
+ def _is_different_origin(url1: str, url2: str) -> bool:
169
+ """Return True when url1 and url2 have different scheme+host+port."""
170
+ p1 = urllib.parse.urlparse(url1)
171
+ p2 = urllib.parse.urlparse(url2)
172
+ return p1.netloc != p2.netloc
173
+
174
+
91
175
  def _test_public_registration(
92
176
  client,
93
177
  meta: dict,
@@ -291,6 +291,10 @@ def _probe_oauth_metadata(
291
291
  parsed = urllib.parse.urlparse(base)
292
292
  origin = f"{parsed.scheme}://{parsed.netloc}"
293
293
 
294
+ # Track registration endpoints already reported to avoid duplicate findings
295
+ # when both oauth-authorization-server and openid-configuration expose the same URL
296
+ seen_reg_endpoints: set[str] = set()
297
+
294
298
  for path in _OAUTH_META_PATHS:
295
299
  probes += 1
296
300
  try:
@@ -306,7 +310,68 @@ def _probe_oauth_metadata(
306
310
  except Exception:
307
311
  continue
308
312
 
309
- # Interesting fields
313
+ # ── oauth-protected-resource (MCP 2025 spec) ──────────────────────────
314
+ # This document is on the MCP *resource* server and points to the real AS.
315
+ if path == "/.well-known/oauth-protected-resource":
316
+ auth_servers = meta.get("authorization_servers", [])
317
+ resource_fields: list[str] = []
318
+ for key in ("resource", "scopes_provided", "bearer_methods_supported"):
319
+ if key in meta:
320
+ resource_fields.append(f"{key}: {meta[key]}")
321
+
322
+ if auth_servers:
323
+ as_url = auth_servers[0]
324
+ is_ext = _netloc(origin) != _netloc(as_url)
325
+ placement = "external" if is_ext else "co-located"
326
+ findings.append(RedTeamFinding(
327
+ attack_type="preauth",
328
+ severity="INFO",
329
+ title=(
330
+ f"MCP 2025 OAuth discovery: AS at {as_url} ({placement})"
331
+ ),
332
+ tool_name="<server>",
333
+ parameter=None,
334
+ payload=f"GET {path}",
335
+ evidence=(
336
+ f"authorization_servers: {auth_servers}\n"
337
+ + "\n".join(resource_fields[:3])
338
+ ),
339
+ exploit_scenario=(
340
+ f"The MCP server advertises its authorization server at {as_url}. "
341
+ + (
342
+ "The AS is hosted on a separate origin — OAuth attack tests "
343
+ "should target that AS directly. "
344
+ "Use `sentinel redteam mcp auth <mcp-url>` to follow the chain "
345
+ "and test the external AS."
346
+ if is_ext else
347
+ "The AS is co-located on the same host — OAuth tests against "
348
+ "this origin will reach the authorization server."
349
+ )
350
+ ),
351
+ mitre_id="T1590",
352
+ owasp_id=None,
353
+ confidence="HIGH",
354
+ ))
355
+ else:
356
+ findings.append(RedTeamFinding(
357
+ attack_type="preauth",
358
+ severity="INFO",
359
+ title=f"MCP protected-resource metadata discovered — {path}",
360
+ tool_name="<server>",
361
+ parameter=None,
362
+ payload=f"GET {path}",
363
+ evidence="\n".join(resource_fields[:6]),
364
+ exploit_scenario=(
365
+ "The MCP server exposes its OAuth protected-resource document. "
366
+ "This identifies the authorization server and permitted scopes."
367
+ ),
368
+ mitre_id="T1590",
369
+ owasp_id=None,
370
+ confidence="HIGH",
371
+ ))
372
+ continue # rest of the loop handles AS metadata paths
373
+
374
+ # ── oauth-authorization-server / openid-configuration ─────────────────
310
375
  endpoints: list[str] = []
311
376
  for key in ("authorization_endpoint", "token_endpoint", "registration_endpoint",
312
377
  "introspection_endpoint", "revocation_endpoint"):
@@ -318,8 +383,10 @@ def _probe_oauth_metadata(
318
383
 
319
384
  has_registration = "registration_endpoint" in meta
320
385
  has_implicit = "implicit" in grant_types or "token" in grant_types
386
+ reg_url = meta.get("registration_endpoint", "")
321
387
 
322
- if has_registration:
388
+ if has_registration and reg_url not in seen_reg_endpoints:
389
+ seen_reg_endpoints.add(reg_url)
323
390
  findings.append(RedTeamFinding(
324
391
  attack_type="preauth",
325
392
  severity="HIGH",
@@ -328,7 +395,7 @@ def _probe_oauth_metadata(
328
395
  parameter=None,
329
396
  payload=f"GET {path}",
330
397
  evidence=(
331
- f"registration_endpoint: {meta['registration_endpoint']}\n"
398
+ f"registration_endpoint: {reg_url}\n"
332
399
  + "\n".join(endpoints[:5])
333
400
  ),
334
401
  exploit_scenario=(
@@ -346,6 +413,8 @@ def _probe_oauth_metadata(
346
413
  "Disable public registration unless explicitly required."
347
414
  ),
348
415
  ))
416
+ elif has_registration and reg_url in seen_reg_endpoints:
417
+ pass # same registration_endpoint already reported via another path
349
418
  elif has_implicit:
350
419
  findings.append(RedTeamFinding(
351
420
  attack_type="preauth",
@@ -369,7 +438,6 @@ def _probe_oauth_metadata(
369
438
  ),
370
439
  ))
371
440
  else:
372
- # Just an INFO finding for the metadata itself
373
441
  findings.append(RedTeamFinding(
374
442
  attack_type="preauth",
375
443
  severity="INFO",
@@ -390,6 +458,11 @@ def _probe_oauth_metadata(
390
458
  return probes
391
459
 
392
460
 
461
+ def _netloc(url: str) -> str:
462
+ """Extract scheme+netloc from a URL for origin comparison."""
463
+ return urllib.parse.urlparse(url).netloc
464
+
465
+
393
466
  def _probe_info_paths(
394
467
  client,
395
468
  base: str,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentsentinel-cli"
7
- version = "0.9.6"
7
+ version = "0.9.7"
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"