trustpact 0.1.0__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.
- trustpact-0.1.0/PKG-INFO +85 -0
- trustpact-0.1.0/README.md +65 -0
- trustpact-0.1.0/pyproject.toml +35 -0
- trustpact-0.1.0/setup.cfg +4 -0
- trustpact-0.1.0/trustpact.egg-info/PKG-INFO +85 -0
- trustpact-0.1.0/trustpact.egg-info/SOURCES.txt +12 -0
- trustpact-0.1.0/trustpact.egg-info/dependency_links.txt +1 -0
- trustpact-0.1.0/trustpact.egg-info/entry_points.txt +2 -0
- trustpact-0.1.0/trustpact.egg-info/requires.txt +2 -0
- trustpact-0.1.0/trustpact.egg-info/top_level.txt +1 -0
- trustpact-0.1.0/trustpact_verify/__init__.py +3 -0
- trustpact-0.1.0/trustpact_verify/cli.py +402 -0
- trustpact-0.1.0/trustpact_verify/registry.py +201 -0
- trustpact-0.1.0/trustpact_verify/scanner.py +309 -0
trustpact-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trustpact
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Behavioral trust scanner for MCP servers and AI agents
|
|
5
|
+
Author-email: Nina Klee <nina@arqon.group>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://trustpact.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/trustpact-ai/trustpact-verify
|
|
9
|
+
Project-URL: Documentation, https://trustpact.ai/docs
|
|
10
|
+
Keywords: mcp,trust,ai-agents,security,aegis
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: httpx>=0.25.0
|
|
19
|
+
Requires-Dist: rich>=13.0.0
|
|
20
|
+
|
|
21
|
+
# trustpact
|
|
22
|
+
|
|
23
|
+
Behavioral trust scanner for MCP servers and AI agents.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install trustpact
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Scan a server from the Smithery registry
|
|
35
|
+
trustpact scan "slack"
|
|
36
|
+
|
|
37
|
+
# Scan a local server spec (JSON)
|
|
38
|
+
trustpact scan server.json
|
|
39
|
+
|
|
40
|
+
# JSON output for CI/CD integration
|
|
41
|
+
trustpact scan server.json --json
|
|
42
|
+
|
|
43
|
+
# Show AEGIS scoring methodology
|
|
44
|
+
trustpact info
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## What It Does
|
|
48
|
+
|
|
49
|
+
TrustPact scans MCP server tool definitions for manipulation patterns and calculates a behavioral trust score using the AEGIS 5-dimensional model:
|
|
50
|
+
|
|
51
|
+
- **Trust Signals (35%)** — metadata, documentation, authentication
|
|
52
|
+
- **Manipulation Risk (25%)** — hidden instructions, poisoning patterns
|
|
53
|
+
- **Protection Level (15%)** — auth, scope, licensing
|
|
54
|
+
- **Vulnerability Index (15%)** — critical exposure surface
|
|
55
|
+
- **Context Modifier (10%)** — runtime context signals
|
|
56
|
+
|
|
57
|
+
### Attack Classes Detected
|
|
58
|
+
|
|
59
|
+
| Class | Description |
|
|
60
|
+
|-------|-------------|
|
|
61
|
+
| SIREN | Hidden instruction injection |
|
|
62
|
+
| PHANTOM | Identity spoofing |
|
|
63
|
+
| HYDRA | Coordinated Sybil attacks |
|
|
64
|
+
| MIRAGE | Capability misrepresentation |
|
|
65
|
+
| LEECH | Data/credential exfiltration |
|
|
66
|
+
| CHIMERA | Code injection, safety bypass |
|
|
67
|
+
|
|
68
|
+
### Trust Tiers
|
|
69
|
+
|
|
70
|
+
| Tier | Score | Meaning |
|
|
71
|
+
|------|-------|---------|
|
|
72
|
+
| SOVEREIGN | 95+ | Highest trust |
|
|
73
|
+
| SENTINEL | 85+ | Proven track record |
|
|
74
|
+
| MASTER | 65+ | Reliable |
|
|
75
|
+
| ADEPT | 40+ | Limited history |
|
|
76
|
+
| FELLOW | 0+ | New or unverified |
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
Proprietary — ARQON GmbH (i.G.)
|
|
81
|
+
|
|
82
|
+
## Links
|
|
83
|
+
|
|
84
|
+
- [trustpact.ai](https://trustpact.ai)
|
|
85
|
+
- Patent Provisional 63/928,604
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# trustpact
|
|
2
|
+
|
|
3
|
+
Behavioral trust scanner for MCP servers and AI agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install trustpact
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Scan a server from the Smithery registry
|
|
15
|
+
trustpact scan "slack"
|
|
16
|
+
|
|
17
|
+
# Scan a local server spec (JSON)
|
|
18
|
+
trustpact scan server.json
|
|
19
|
+
|
|
20
|
+
# JSON output for CI/CD integration
|
|
21
|
+
trustpact scan server.json --json
|
|
22
|
+
|
|
23
|
+
# Show AEGIS scoring methodology
|
|
24
|
+
trustpact info
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## What It Does
|
|
28
|
+
|
|
29
|
+
TrustPact scans MCP server tool definitions for manipulation patterns and calculates a behavioral trust score using the AEGIS 5-dimensional model:
|
|
30
|
+
|
|
31
|
+
- **Trust Signals (35%)** — metadata, documentation, authentication
|
|
32
|
+
- **Manipulation Risk (25%)** — hidden instructions, poisoning patterns
|
|
33
|
+
- **Protection Level (15%)** — auth, scope, licensing
|
|
34
|
+
- **Vulnerability Index (15%)** — critical exposure surface
|
|
35
|
+
- **Context Modifier (10%)** — runtime context signals
|
|
36
|
+
|
|
37
|
+
### Attack Classes Detected
|
|
38
|
+
|
|
39
|
+
| Class | Description |
|
|
40
|
+
|-------|-------------|
|
|
41
|
+
| SIREN | Hidden instruction injection |
|
|
42
|
+
| PHANTOM | Identity spoofing |
|
|
43
|
+
| HYDRA | Coordinated Sybil attacks |
|
|
44
|
+
| MIRAGE | Capability misrepresentation |
|
|
45
|
+
| LEECH | Data/credential exfiltration |
|
|
46
|
+
| CHIMERA | Code injection, safety bypass |
|
|
47
|
+
|
|
48
|
+
### Trust Tiers
|
|
49
|
+
|
|
50
|
+
| Tier | Score | Meaning |
|
|
51
|
+
|------|-------|---------|
|
|
52
|
+
| SOVEREIGN | 95+ | Highest trust |
|
|
53
|
+
| SENTINEL | 85+ | Proven track record |
|
|
54
|
+
| MASTER | 65+ | Reliable |
|
|
55
|
+
| ADEPT | 40+ | Limited history |
|
|
56
|
+
| FELLOW | 0+ | New or unverified |
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
Proprietary — ARQON GmbH (i.G.)
|
|
61
|
+
|
|
62
|
+
## Links
|
|
63
|
+
|
|
64
|
+
- [trustpact.ai](https://trustpact.ai)
|
|
65
|
+
- Patent Provisional 63/928,604
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "trustpact"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Behavioral trust scanner for MCP servers and AI agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{name = "Nina Klee", email = "nina@arqon.group"}]
|
|
13
|
+
keywords = ["mcp", "trust", "ai-agents", "security", "aegis"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Security",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"httpx>=0.25.0",
|
|
23
|
+
"rich>=13.0.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
trustpact = "trustpact_verify.cli:main"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://trustpact.ai"
|
|
31
|
+
Repository = "https://github.com/trustpact-ai/trustpact-verify"
|
|
32
|
+
Documentation = "https://trustpact.ai/docs"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
include = ["trustpact_verify*"]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trustpact
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Behavioral trust scanner for MCP servers and AI agents
|
|
5
|
+
Author-email: Nina Klee <nina@arqon.group>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://trustpact.ai
|
|
8
|
+
Project-URL: Repository, https://github.com/trustpact-ai/trustpact-verify
|
|
9
|
+
Project-URL: Documentation, https://trustpact.ai/docs
|
|
10
|
+
Keywords: mcp,trust,ai-agents,security,aegis
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: httpx>=0.25.0
|
|
19
|
+
Requires-Dist: rich>=13.0.0
|
|
20
|
+
|
|
21
|
+
# trustpact
|
|
22
|
+
|
|
23
|
+
Behavioral trust scanner for MCP servers and AI agents.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install trustpact
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Scan a server from the Smithery registry
|
|
35
|
+
trustpact scan "slack"
|
|
36
|
+
|
|
37
|
+
# Scan a local server spec (JSON)
|
|
38
|
+
trustpact scan server.json
|
|
39
|
+
|
|
40
|
+
# JSON output for CI/CD integration
|
|
41
|
+
trustpact scan server.json --json
|
|
42
|
+
|
|
43
|
+
# Show AEGIS scoring methodology
|
|
44
|
+
trustpact info
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## What It Does
|
|
48
|
+
|
|
49
|
+
TrustPact scans MCP server tool definitions for manipulation patterns and calculates a behavioral trust score using the AEGIS 5-dimensional model:
|
|
50
|
+
|
|
51
|
+
- **Trust Signals (35%)** — metadata, documentation, authentication
|
|
52
|
+
- **Manipulation Risk (25%)** — hidden instructions, poisoning patterns
|
|
53
|
+
- **Protection Level (15%)** — auth, scope, licensing
|
|
54
|
+
- **Vulnerability Index (15%)** — critical exposure surface
|
|
55
|
+
- **Context Modifier (10%)** — runtime context signals
|
|
56
|
+
|
|
57
|
+
### Attack Classes Detected
|
|
58
|
+
|
|
59
|
+
| Class | Description |
|
|
60
|
+
|-------|-------------|
|
|
61
|
+
| SIREN | Hidden instruction injection |
|
|
62
|
+
| PHANTOM | Identity spoofing |
|
|
63
|
+
| HYDRA | Coordinated Sybil attacks |
|
|
64
|
+
| MIRAGE | Capability misrepresentation |
|
|
65
|
+
| LEECH | Data/credential exfiltration |
|
|
66
|
+
| CHIMERA | Code injection, safety bypass |
|
|
67
|
+
|
|
68
|
+
### Trust Tiers
|
|
69
|
+
|
|
70
|
+
| Tier | Score | Meaning |
|
|
71
|
+
|------|-------|---------|
|
|
72
|
+
| SOVEREIGN | 95+ | Highest trust |
|
|
73
|
+
| SENTINEL | 85+ | Proven track record |
|
|
74
|
+
| MASTER | 65+ | Reliable |
|
|
75
|
+
| ADEPT | 40+ | Limited history |
|
|
76
|
+
| FELLOW | 0+ | New or unverified |
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
Proprietary — ARQON GmbH (i.G.)
|
|
81
|
+
|
|
82
|
+
## Links
|
|
83
|
+
|
|
84
|
+
- [trustpact.ai](https://trustpact.ai)
|
|
85
|
+
- Patent Provisional 63/928,604
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
trustpact.egg-info/PKG-INFO
|
|
4
|
+
trustpact.egg-info/SOURCES.txt
|
|
5
|
+
trustpact.egg-info/dependency_links.txt
|
|
6
|
+
trustpact.egg-info/entry_points.txt
|
|
7
|
+
trustpact.egg-info/requires.txt
|
|
8
|
+
trustpact.egg-info/top_level.txt
|
|
9
|
+
trustpact_verify/__init__.py
|
|
10
|
+
trustpact_verify/cli.py
|
|
11
|
+
trustpact_verify/registry.py
|
|
12
|
+
trustpact_verify/scanner.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
trustpact_verify
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""
|
|
2
|
+
trustpact verify — CLI entry point.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
trustpact scan <name_or_url> Scan an MCP server for trust issues
|
|
6
|
+
trustpact info Show AEGIS scoring methodology
|
|
7
|
+
trustpact version Show version
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
from rich.text import Text
|
|
21
|
+
from rich import box
|
|
22
|
+
|
|
23
|
+
from trustpact_verify import __version__
|
|
24
|
+
from trustpact_verify.scanner import (
|
|
25
|
+
scan_server,
|
|
26
|
+
scan_tool_descriptions,
|
|
27
|
+
scan_metadata,
|
|
28
|
+
ScanResult,
|
|
29
|
+
Finding,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
console = Console()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── Colours & symbols ──────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
TIER_STYLES = {
|
|
38
|
+
"SOVEREIGN": ("bold bright_green", "★★★★★"),
|
|
39
|
+
"SENTINEL": ("bold green", "★★★★☆"),
|
|
40
|
+
"MASTER": ("bold yellow", "★★★☆☆"),
|
|
41
|
+
"ADEPT": ("bold bright_red", "★★☆☆☆"),
|
|
42
|
+
"FELLOW": ("bold red", "★☆☆☆☆"),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
SEVERITY_STYLES = {
|
|
46
|
+
"CRITICAL": "bold white on red",
|
|
47
|
+
"HIGH": "bold red",
|
|
48
|
+
"MEDIUM": "yellow",
|
|
49
|
+
"LOW": "dim",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
REC_STYLES = {
|
|
53
|
+
"SAFE": ("bold bright_green", "✓ SAFE — Low risk, reasonable to use"),
|
|
54
|
+
"CAUTION": ("bold yellow", "⚠ CAUTION — Review findings before use"),
|
|
55
|
+
"AVOID": ("bold red", "✗ AVOID — Significant trust concerns"),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _render_header():
|
|
60
|
+
console.print()
|
|
61
|
+
console.print(
|
|
62
|
+
Panel(
|
|
63
|
+
"[bold bright_cyan]TrustPact[/] verify · AEGIS Trust Scanner",
|
|
64
|
+
subtitle=f"v{__version__}",
|
|
65
|
+
style="bright_cyan",
|
|
66
|
+
width=64,
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _render_score(result: ScanResult):
|
|
72
|
+
"""Render the trust score gauge."""
|
|
73
|
+
tier_style, stars = TIER_STYLES.get(result.trust_tier, ("white", "?"))
|
|
74
|
+
|
|
75
|
+
score_text = Text()
|
|
76
|
+
score_text.append(f" Trust Score: ", style="bold")
|
|
77
|
+
score_text.append(f"{result.trust_score:.1f}", style=tier_style)
|
|
78
|
+
score_text.append(f" / 100", style="dim")
|
|
79
|
+
score_text.append(f" {stars}", style=tier_style)
|
|
80
|
+
console.print(score_text)
|
|
81
|
+
|
|
82
|
+
tier_text = Text()
|
|
83
|
+
tier_text.append(f" Trust Tier: ", style="bold")
|
|
84
|
+
tier_text.append(result.trust_tier, style=tier_style)
|
|
85
|
+
console.print(tier_text)
|
|
86
|
+
console.print()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _render_dimensions(result: ScanResult):
|
|
90
|
+
"""Render the 5-dimensional breakdown."""
|
|
91
|
+
dims = result.dimensions
|
|
92
|
+
table = Table(
|
|
93
|
+
title="AEGIS Dimensions",
|
|
94
|
+
box=box.ROUNDED,
|
|
95
|
+
title_style="bold bright_cyan",
|
|
96
|
+
width=64,
|
|
97
|
+
)
|
|
98
|
+
table.add_column("Dimension", style="bold", width=22)
|
|
99
|
+
table.add_column("Value", justify="right", width=10)
|
|
100
|
+
table.add_column("Bar", width=26)
|
|
101
|
+
|
|
102
|
+
rows = [
|
|
103
|
+
("Trust Signals", dims.trust_signals, False),
|
|
104
|
+
("Manipulation Risk", dims.manipulation_risk, True),
|
|
105
|
+
("Protection Level", dims.protection_level, False),
|
|
106
|
+
("Vulnerability Index", dims.vulnerability_index, True),
|
|
107
|
+
("Context Modifier", dims.context_modifier, None),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
for name, value, invert in rows:
|
|
111
|
+
if invert is None:
|
|
112
|
+
# context modifier is -10 to +10
|
|
113
|
+
bar = f"{'▓' * max(0, int(value + 10))}{'░' * max(0, 20 - int(value + 10))}"
|
|
114
|
+
style = "yellow" if abs(value) < 3 else ("green" if value > 0 else "red")
|
|
115
|
+
table.add_row(name, f"{value:+.1f}", Text(bar, style=style))
|
|
116
|
+
else:
|
|
117
|
+
filled = int(value / 5) # 0-20 blocks
|
|
118
|
+
bar = "▓" * filled + "░" * (20 - filled)
|
|
119
|
+
if invert:
|
|
120
|
+
style = "bright_green" if value < 30 else ("yellow" if value < 60 else "red")
|
|
121
|
+
else:
|
|
122
|
+
style = "red" if value < 30 else ("yellow" if value < 60 else "bright_green")
|
|
123
|
+
table.add_row(name, f"{value:.1f}", Text(bar, style=style))
|
|
124
|
+
|
|
125
|
+
console.print(table)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _render_findings(findings: list[Finding]):
|
|
129
|
+
"""Render findings table."""
|
|
130
|
+
if not findings:
|
|
131
|
+
console.print(" [bright_green]No issues detected.[/]")
|
|
132
|
+
console.print()
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
table = Table(
|
|
136
|
+
title=f"Findings ({len(findings)})",
|
|
137
|
+
box=box.ROUNDED,
|
|
138
|
+
title_style="bold yellow",
|
|
139
|
+
width=80,
|
|
140
|
+
show_lines=True,
|
|
141
|
+
)
|
|
142
|
+
table.add_column("Sev", width=8, justify="center")
|
|
143
|
+
table.add_column("Class", width=9)
|
|
144
|
+
table.add_column("Message", width=42)
|
|
145
|
+
table.add_column("Location", width=16, style="dim")
|
|
146
|
+
|
|
147
|
+
# Sort: CRITICAL first, then HIGH, MEDIUM, LOW
|
|
148
|
+
severity_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
|
|
149
|
+
sorted_findings = sorted(findings, key=lambda f: severity_order.get(f.severity, 9))
|
|
150
|
+
|
|
151
|
+
for f in sorted_findings:
|
|
152
|
+
sev_style = SEVERITY_STYLES.get(f.severity, "white")
|
|
153
|
+
table.add_row(
|
|
154
|
+
Text(f.severity, style=sev_style),
|
|
155
|
+
Text(f.attack_class, style="bold"),
|
|
156
|
+
f.message,
|
|
157
|
+
f.location[:16] if f.location else "",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
console.print(table)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _render_recommendation(result: ScanResult):
|
|
164
|
+
"""Render the final recommendation."""
|
|
165
|
+
rec_style, rec_text = REC_STYLES.get(
|
|
166
|
+
result.recommendation, ("white", "? UNKNOWN")
|
|
167
|
+
)
|
|
168
|
+
console.print(
|
|
169
|
+
Panel(
|
|
170
|
+
f"[{rec_style}]{rec_text}[/]",
|
|
171
|
+
title="Recommendation",
|
|
172
|
+
style=rec_style.split()[0] if " " in rec_style else rec_style,
|
|
173
|
+
width=64,
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Summary line
|
|
178
|
+
console.print(
|
|
179
|
+
f" [dim]Scanned {result.tools_scanned} tool(s) · "
|
|
180
|
+
f"{result.critical_count} critical · {result.high_count} high · "
|
|
181
|
+
f"Source: {result.scan_source}[/]"
|
|
182
|
+
)
|
|
183
|
+
console.print()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def render_scan_result(result: ScanResult):
|
|
187
|
+
"""Full formatted output for a scan result."""
|
|
188
|
+
_render_header()
|
|
189
|
+
console.print(f" [bold]Target:[/] {result.target}")
|
|
190
|
+
console.print()
|
|
191
|
+
_render_score(result)
|
|
192
|
+
_render_dimensions(result)
|
|
193
|
+
console.print()
|
|
194
|
+
_render_findings(result.findings)
|
|
195
|
+
console.print()
|
|
196
|
+
_render_recommendation(result)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ── Commands ───────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
def cmd_scan(args):
|
|
202
|
+
"""Scan a server from a JSON spec file or registry name."""
|
|
203
|
+
target = args.target
|
|
204
|
+
|
|
205
|
+
# Try as local JSON file first
|
|
206
|
+
path = Path(target)
|
|
207
|
+
if path.exists() and path.suffix == ".json":
|
|
208
|
+
try:
|
|
209
|
+
data = json.loads(path.read_text())
|
|
210
|
+
except json.JSONDecodeError as e:
|
|
211
|
+
console.print(f"[red]Error parsing {target}: {e}[/]")
|
|
212
|
+
sys.exit(1)
|
|
213
|
+
|
|
214
|
+
tools = data.get("tools", [])
|
|
215
|
+
metadata = data.get("metadata", data.get("server", {}))
|
|
216
|
+
if not metadata.get("name"):
|
|
217
|
+
metadata["name"] = path.stem
|
|
218
|
+
|
|
219
|
+
result = scan_server(tools, metadata)
|
|
220
|
+
if args.json_output:
|
|
221
|
+
_output_json(result)
|
|
222
|
+
else:
|
|
223
|
+
render_scan_result(result)
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# Try fetching from MCP registry
|
|
227
|
+
try:
|
|
228
|
+
from trustpact_verify.registry import fetch_server_spec
|
|
229
|
+
console.print(f" [dim]Fetching server spec for '{target}' from registry...[/]")
|
|
230
|
+
spec = fetch_server_spec(target)
|
|
231
|
+
if spec is None:
|
|
232
|
+
console.print(f"[red]Server '{target}' not found in registry.[/]")
|
|
233
|
+
console.print("[dim]Try: trustpact scan <path-to-spec.json>[/]")
|
|
234
|
+
sys.exit(1)
|
|
235
|
+
|
|
236
|
+
tools = spec.get("tools", [])
|
|
237
|
+
metadata = spec.get("metadata", {})
|
|
238
|
+
metadata["name"] = metadata.get("name", target)
|
|
239
|
+
|
|
240
|
+
result = scan_server(tools, metadata)
|
|
241
|
+
if args.json_output:
|
|
242
|
+
_output_json(result)
|
|
243
|
+
else:
|
|
244
|
+
render_scan_result(result)
|
|
245
|
+
except ImportError:
|
|
246
|
+
console.print(f"[yellow]Registry module not available.[/]")
|
|
247
|
+
console.print(f"[dim]Provide a local JSON spec: trustpact scan server.json[/]")
|
|
248
|
+
sys.exit(1)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
console.print(f"[red]Error fetching '{target}': {e}[/]")
|
|
251
|
+
sys.exit(1)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def cmd_scan_json(args):
|
|
255
|
+
"""Scan from piped JSON input (stdin)."""
|
|
256
|
+
try:
|
|
257
|
+
data = json.load(sys.stdin)
|
|
258
|
+
except json.JSONDecodeError as e:
|
|
259
|
+
console.print(f"[red]Invalid JSON input: {e}[/]")
|
|
260
|
+
sys.exit(1)
|
|
261
|
+
|
|
262
|
+
tools = data.get("tools", [])
|
|
263
|
+
metadata = data.get("metadata", data.get("server", {}))
|
|
264
|
+
if not metadata.get("name"):
|
|
265
|
+
metadata["name"] = "stdin"
|
|
266
|
+
|
|
267
|
+
result = scan_server(tools, metadata)
|
|
268
|
+
|
|
269
|
+
if args.json_output:
|
|
270
|
+
_output_json(result)
|
|
271
|
+
else:
|
|
272
|
+
render_scan_result(result)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _output_json(result: ScanResult):
|
|
276
|
+
"""Output scan result as JSON."""
|
|
277
|
+
output = {
|
|
278
|
+
"target": result.target,
|
|
279
|
+
"trust_score": result.trust_score,
|
|
280
|
+
"trust_tier": result.trust_tier,
|
|
281
|
+
"recommendation": result.recommendation,
|
|
282
|
+
"dimensions": {
|
|
283
|
+
"trust_signals": result.dimensions.trust_signals,
|
|
284
|
+
"manipulation_risk": result.dimensions.manipulation_risk,
|
|
285
|
+
"protection_level": result.dimensions.protection_level,
|
|
286
|
+
"vulnerability_index": result.dimensions.vulnerability_index,
|
|
287
|
+
"context_modifier": result.dimensions.context_modifier,
|
|
288
|
+
},
|
|
289
|
+
"findings": [
|
|
290
|
+
{
|
|
291
|
+
"severity": f.severity,
|
|
292
|
+
"attack_class": f.attack_class,
|
|
293
|
+
"message": f.message,
|
|
294
|
+
"location": f.location,
|
|
295
|
+
}
|
|
296
|
+
for f in result.findings
|
|
297
|
+
],
|
|
298
|
+
"tools_scanned": result.tools_scanned,
|
|
299
|
+
"scan_source": result.scan_source,
|
|
300
|
+
}
|
|
301
|
+
print(json.dumps(output, indent=2))
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def cmd_info(_args):
|
|
305
|
+
"""Show AEGIS scoring info."""
|
|
306
|
+
_render_header()
|
|
307
|
+
|
|
308
|
+
console.print()
|
|
309
|
+
console.print(" [bold]AEGIS Trust Scoring[/] — 5-dimensional behavioral trust assessment")
|
|
310
|
+
console.print()
|
|
311
|
+
|
|
312
|
+
# Tier table
|
|
313
|
+
table = Table(title="Trust Tiers", box=box.ROUNDED, width=60)
|
|
314
|
+
table.add_column("Tier", width=12)
|
|
315
|
+
table.add_column("Score", width=8, justify="center")
|
|
316
|
+
table.add_column("Meaning", width=35)
|
|
317
|
+
|
|
318
|
+
tiers = [
|
|
319
|
+
("SOVEREIGN", "95+", "Highest trust. Fully certified."),
|
|
320
|
+
("SENTINEL", "85+", "Proven track record."),
|
|
321
|
+
("MASTER", "65+", "Reliable with strong history."),
|
|
322
|
+
("ADEPT", "40+", "Functional, limited history."),
|
|
323
|
+
("FELLOW", "0+", "New or unverified."),
|
|
324
|
+
]
|
|
325
|
+
for tier, score, meaning in tiers:
|
|
326
|
+
style, stars = TIER_STYLES[tier]
|
|
327
|
+
table.add_row(Text(f"{stars} {tier}", style=style), score, meaning)
|
|
328
|
+
console.print(table)
|
|
329
|
+
|
|
330
|
+
console.print()
|
|
331
|
+
console.print(" [bold]Dimensions:[/]")
|
|
332
|
+
console.print(" Trust Signals (35%) — metadata, docs, auth")
|
|
333
|
+
console.print(" Manipulation Risk (25%) — poisoning patterns detected")
|
|
334
|
+
console.print(" Protection Level (15%) — auth, scope, licensing")
|
|
335
|
+
console.print(" Vulnerability Index (15%) — critical exposure surface")
|
|
336
|
+
console.print(" Context Modifier (10%) — runtime context signals")
|
|
337
|
+
console.print()
|
|
338
|
+
|
|
339
|
+
console.print(" [bold]Attack Classes:[/]")
|
|
340
|
+
classes = [
|
|
341
|
+
("SIREN", "Emotional manipulation / hidden instructions"),
|
|
342
|
+
("PHANTOM", "Identity spoofing"),
|
|
343
|
+
("HYDRA", "Coordinated Sybil attacks"),
|
|
344
|
+
("MIRAGE", "Capability misrepresentation"),
|
|
345
|
+
("LEECH", "Resource / data extraction"),
|
|
346
|
+
("CHIMERA", "Context-shifting / code injection"),
|
|
347
|
+
]
|
|
348
|
+
for cls, desc in classes:
|
|
349
|
+
console.print(f" [bold]{cls:8s}[/] {desc}")
|
|
350
|
+
console.print()
|
|
351
|
+
|
|
352
|
+
console.print(" [dim]trustpact.ai · AEGIS Trust Standard · Patent Provisional 63/928,604[/]")
|
|
353
|
+
console.print()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def cmd_version(_args):
|
|
357
|
+
print(f"trustpact {__version__}")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ── Main ───────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
def main():
|
|
363
|
+
parser = argparse.ArgumentParser(
|
|
364
|
+
prog="trustpact",
|
|
365
|
+
description="TrustPact — Behavioral trust scanner for MCP servers",
|
|
366
|
+
)
|
|
367
|
+
sub = parser.add_subparsers(dest="command")
|
|
368
|
+
|
|
369
|
+
# scan
|
|
370
|
+
p_scan = sub.add_parser("scan", help="Scan an MCP server spec")
|
|
371
|
+
p_scan.add_argument("target", help="JSON spec file path or registry server name")
|
|
372
|
+
p_scan.add_argument("--json", dest="json_output", action="store_true",
|
|
373
|
+
help="Output results as JSON")
|
|
374
|
+
|
|
375
|
+
# scan-stdin (for piping)
|
|
376
|
+
p_stdin = sub.add_parser("scan-stdin", help="Scan from piped JSON on stdin")
|
|
377
|
+
p_stdin.add_argument("--json", dest="json_output", action="store_true")
|
|
378
|
+
|
|
379
|
+
# info
|
|
380
|
+
sub.add_parser("info", help="Show AEGIS scoring methodology")
|
|
381
|
+
|
|
382
|
+
# version
|
|
383
|
+
sub.add_parser("version", help="Show version")
|
|
384
|
+
|
|
385
|
+
args = parser.parse_args()
|
|
386
|
+
|
|
387
|
+
commands = {
|
|
388
|
+
"scan": cmd_scan,
|
|
389
|
+
"scan-stdin": cmd_scan_json,
|
|
390
|
+
"info": cmd_info,
|
|
391
|
+
"version": cmd_version,
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if args.command not in commands:
|
|
395
|
+
parser.print_help()
|
|
396
|
+
sys.exit(0)
|
|
397
|
+
|
|
398
|
+
commands[args.command](args)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
if __name__ == "__main__":
|
|
402
|
+
main()
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Registry fetcher — pull server specs from public registries.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- Local JSON spec files
|
|
6
|
+
- mcp.run package registry (public API)
|
|
7
|
+
- Smithery registry (smithery.ai)
|
|
8
|
+
- Raw GitHub manifest files
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
TIMEOUT = 15.0
|
|
20
|
+
|
|
21
|
+
# ── Registry endpoints ─────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
MCP_RUN_API = "https://registry.mcp.run/packages"
|
|
24
|
+
SMITHERY_API = "https://registry.smithery.ai/servers"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def fetch_server_spec(name_or_url: str) -> dict[str, Any] | None:
|
|
28
|
+
"""
|
|
29
|
+
Fetch an MCP server spec by name or URL.
|
|
30
|
+
|
|
31
|
+
Tries in order:
|
|
32
|
+
1. Direct URL (if it looks like a URL)
|
|
33
|
+
2. Smithery registry
|
|
34
|
+
3. mcp.run registry
|
|
35
|
+
"""
|
|
36
|
+
if name_or_url.startswith("http://") or name_or_url.startswith("https://"):
|
|
37
|
+
return _fetch_url(name_or_url)
|
|
38
|
+
|
|
39
|
+
# Try Smithery first (has richer tool definitions)
|
|
40
|
+
spec = _fetch_smithery(name_or_url)
|
|
41
|
+
if spec:
|
|
42
|
+
return spec
|
|
43
|
+
|
|
44
|
+
# Try mcp.run
|
|
45
|
+
spec = _fetch_mcp_run(name_or_url)
|
|
46
|
+
if spec:
|
|
47
|
+
return spec
|
|
48
|
+
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _fetch_url(url: str) -> dict[str, Any] | None:
|
|
53
|
+
"""Fetch a spec from a direct URL."""
|
|
54
|
+
try:
|
|
55
|
+
resp = httpx.get(url, timeout=TIMEOUT, follow_redirects=True)
|
|
56
|
+
resp.raise_for_status()
|
|
57
|
+
return resp.json()
|
|
58
|
+
except Exception:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _fetch_smithery(name: str) -> dict[str, Any] | None:
|
|
63
|
+
"""
|
|
64
|
+
Fetch from Smithery registry.
|
|
65
|
+
API: GET /servers?q=<name>
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
resp = httpx.get(
|
|
69
|
+
SMITHERY_API,
|
|
70
|
+
params={"q": name, "pageSize": 5},
|
|
71
|
+
timeout=TIMEOUT,
|
|
72
|
+
follow_redirects=True,
|
|
73
|
+
)
|
|
74
|
+
resp.raise_for_status()
|
|
75
|
+
data = resp.json()
|
|
76
|
+
|
|
77
|
+
servers = data.get("servers", data) if isinstance(data, dict) else data
|
|
78
|
+
if not isinstance(servers, list) or not servers:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
# Find exact or best match
|
|
82
|
+
server = None
|
|
83
|
+
for s in servers:
|
|
84
|
+
s_name = s.get("qualifiedName", s.get("name", ""))
|
|
85
|
+
if s_name.lower() == name.lower() or name.lower() in s_name.lower():
|
|
86
|
+
server = s
|
|
87
|
+
break
|
|
88
|
+
if not server:
|
|
89
|
+
server = servers[0]
|
|
90
|
+
|
|
91
|
+
# Normalize to our format
|
|
92
|
+
tools = server.get("tools", [])
|
|
93
|
+
metadata = {
|
|
94
|
+
"name": server.get("qualifiedName", server.get("name", name)),
|
|
95
|
+
"description": server.get("description", ""),
|
|
96
|
+
"repository": server.get("homepage", server.get("repository", "")),
|
|
97
|
+
"license": server.get("license", ""),
|
|
98
|
+
"tool_count": len(tools),
|
|
99
|
+
"source": "smithery",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# If tools aren't inline, try fetching detail endpoint
|
|
103
|
+
if not tools:
|
|
104
|
+
detail = _fetch_smithery_detail(server.get("qualifiedName", name))
|
|
105
|
+
if detail:
|
|
106
|
+
tools = detail.get("tools", [])
|
|
107
|
+
metadata["tool_count"] = len(tools)
|
|
108
|
+
|
|
109
|
+
return {"tools": tools, "metadata": metadata}
|
|
110
|
+
|
|
111
|
+
except Exception:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _fetch_smithery_detail(qualified_name: str) -> dict | None:
|
|
116
|
+
"""Fetch detailed server info from Smithery."""
|
|
117
|
+
try:
|
|
118
|
+
resp = httpx.get(
|
|
119
|
+
f"{SMITHERY_API}/{qualified_name}",
|
|
120
|
+
timeout=TIMEOUT,
|
|
121
|
+
follow_redirects=True,
|
|
122
|
+
)
|
|
123
|
+
resp.raise_for_status()
|
|
124
|
+
return resp.json()
|
|
125
|
+
except Exception:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _fetch_mcp_run(name: str) -> dict[str, Any] | None:
|
|
130
|
+
"""
|
|
131
|
+
Fetch from mcp.run registry.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
resp = httpx.get(
|
|
135
|
+
MCP_RUN_API,
|
|
136
|
+
params={"q": name},
|
|
137
|
+
timeout=TIMEOUT,
|
|
138
|
+
follow_redirects=True,
|
|
139
|
+
)
|
|
140
|
+
resp.raise_for_status()
|
|
141
|
+
data = resp.json()
|
|
142
|
+
|
|
143
|
+
packages = data if isinstance(data, list) else data.get("packages", [])
|
|
144
|
+
if not packages:
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
# Best match
|
|
148
|
+
pkg = None
|
|
149
|
+
for p in packages:
|
|
150
|
+
p_name = p.get("name", "")
|
|
151
|
+
if p_name.lower() == name.lower() or name.lower() in p_name.lower():
|
|
152
|
+
pkg = p
|
|
153
|
+
break
|
|
154
|
+
if not pkg:
|
|
155
|
+
pkg = packages[0]
|
|
156
|
+
|
|
157
|
+
tools = pkg.get("tools", [])
|
|
158
|
+
metadata = {
|
|
159
|
+
"name": pkg.get("name", name),
|
|
160
|
+
"description": pkg.get("description", ""),
|
|
161
|
+
"repository": pkg.get("repository", pkg.get("homepage", "")),
|
|
162
|
+
"license": pkg.get("license", ""),
|
|
163
|
+
"tool_count": len(tools),
|
|
164
|
+
"source": "mcp.run",
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {"tools": tools, "metadata": metadata}
|
|
168
|
+
|
|
169
|
+
except Exception:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def search_registry(query: str, limit: int = 20) -> list[dict[str, Any]]:
|
|
174
|
+
"""
|
|
175
|
+
Search registries and return a list of server summaries.
|
|
176
|
+
"""
|
|
177
|
+
results = []
|
|
178
|
+
|
|
179
|
+
# Smithery
|
|
180
|
+
try:
|
|
181
|
+
resp = httpx.get(
|
|
182
|
+
SMITHERY_API,
|
|
183
|
+
params={"q": query, "pageSize": min(limit, 50)},
|
|
184
|
+
timeout=TIMEOUT,
|
|
185
|
+
follow_redirects=True,
|
|
186
|
+
)
|
|
187
|
+
resp.raise_for_status()
|
|
188
|
+
data = resp.json()
|
|
189
|
+
servers = data.get("servers", data) if isinstance(data, dict) else data
|
|
190
|
+
if isinstance(servers, list):
|
|
191
|
+
for s in servers[:limit]:
|
|
192
|
+
results.append({
|
|
193
|
+
"name": s.get("qualifiedName", s.get("name", "?")),
|
|
194
|
+
"description": s.get("description", "")[:100],
|
|
195
|
+
"tools": len(s.get("tools", [])),
|
|
196
|
+
"source": "smithery",
|
|
197
|
+
})
|
|
198
|
+
except Exception:
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
return results[:limit]
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AEGIS Trust Scanner — core analysis engine.
|
|
3
|
+
|
|
4
|
+
Scans MCP server tool definitions and metadata for:
|
|
5
|
+
- Tool poisoning patterns (hidden instructions in descriptions)
|
|
6
|
+
- Schema manipulation (Full-Schema Poisoning)
|
|
7
|
+
- Capability misrepresentation (MIRAGE)
|
|
8
|
+
- Identity spoofing indicators (PHANTOM)
|
|
9
|
+
- Resource extraction patterns (LEECH)
|
|
10
|
+
- Sybil/coordination signals (HYDRA)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import json
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── Threat patterns ──────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
POISONING_PATTERNS = [
|
|
24
|
+
# Hidden instructions in tool descriptions
|
|
25
|
+
(r"(?i)ignore\s+(previous|prior|above|all)\s+(instructions?|prompts?|rules?)", "SIREN", "HIGH",
|
|
26
|
+
"Hidden instruction override detected in tool description"),
|
|
27
|
+
(r"(?i)you\s+(must|should|are required to)\s+", "SIREN", "MEDIUM",
|
|
28
|
+
"Directive language in tool description — potential manipulation"),
|
|
29
|
+
(r"(?i)do\s+not\s+(tell|inform|reveal|mention|disclose)", "SIREN", "HIGH",
|
|
30
|
+
"Secrecy instruction detected — likely tool poisoning"),
|
|
31
|
+
(r"(?i)system\s*prompt|system\s*message|<\s*system\s*>", "SIREN", "HIGH",
|
|
32
|
+
"System prompt injection attempt in tool description"),
|
|
33
|
+
(r"(?i)act\s+as\s+(if|though)?|pretend\s+(to\s+be|you)", "PHANTOM", "HIGH",
|
|
34
|
+
"Identity manipulation — tool tries to alter agent persona"),
|
|
35
|
+
(r"(?i)forward\s+.*(token|key|credential|password|secret)", "LEECH", "CRITICAL",
|
|
36
|
+
"Credential exfiltration attempt detected"),
|
|
37
|
+
(r"(?i)send\s+.*(data|info|content)\s+to\s+", "LEECH", "HIGH",
|
|
38
|
+
"Data exfiltration pattern — tool may leak information"),
|
|
39
|
+
(r"(?i)(http|https|ftp)://[^\s\"']+\.(ru|cn|tk|xyz)/", "LEECH", "MEDIUM",
|
|
40
|
+
"Suspicious external URL in tool description"),
|
|
41
|
+
(r"(?i)base64|eval\s*\(|exec\s*\(|__import__", "CHIMERA", "HIGH",
|
|
42
|
+
"Code execution pattern in tool description"),
|
|
43
|
+
(r"(?i)(override|bypass|disable)\s+(safety|security|filter|guard|restriction|protection|limit)", "CHIMERA", "CRITICAL",
|
|
44
|
+
"Safety bypass attempt detected"),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
SCHEMA_RISKS = [
|
|
48
|
+
# Schema-level manipulation (Full-Schema Poisoning)
|
|
49
|
+
# Note: only match param NAMES that are inherently sensitive, not benign names
|
|
50
|
+
# that happen to contain substrings like "token" (e.g. pageToken, nextToken)
|
|
51
|
+
(r"(?i)^(password|api_?key|secret_?key|credential|private_?key|access_?token|auth_?token|bearer_?token)$", "LEECH", "HIGH",
|
|
52
|
+
"Tool parameter requests sensitive credentials"),
|
|
53
|
+
(r"(?i)^(webhook_?url|callback_?url|redirect_?uri|notify_?url|exfil)$", "LEECH", "MEDIUM",
|
|
54
|
+
"Tool accepts external callback URL — potential data exfiltration channel"),
|
|
55
|
+
(r"(?i)^(shell_?command|exec_?command|eval_?code|run_?script|execute|shell|eval_?expr)$", "CHIMERA", "HIGH",
|
|
56
|
+
"Tool parameter suggests arbitrary code execution"),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
METADATA_RISKS = [
|
|
60
|
+
# Server-level signals
|
|
61
|
+
("no_readme", "MIRAGE", "LOW", "No README or documentation — reduced transparency"),
|
|
62
|
+
("no_license", "MIRAGE", "LOW", "No license specified — unclear usage terms"),
|
|
63
|
+
("no_auth", "LEECH", "LOW", "No authentication required — open access server"),
|
|
64
|
+
("excessive_tools", "MIRAGE", "MEDIUM", "Unusually high number of tools — possible capability inflation"),
|
|
65
|
+
("stale_repo", "MIRAGE", "LOW", "Repository not updated in 90+ days"),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class Finding:
|
|
71
|
+
"""A single security finding from the scan."""
|
|
72
|
+
attack_class: str # SIREN, PHANTOM, HYDRA, MIRAGE, LEECH, CHIMERA
|
|
73
|
+
severity: str # CRITICAL, HIGH, MEDIUM, LOW
|
|
74
|
+
message: str
|
|
75
|
+
location: str = "" # where in the server spec this was found
|
|
76
|
+
pattern: str = "" # the matched pattern
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class DimensionScores:
|
|
81
|
+
"""AEGIS 5-dimensional trust breakdown."""
|
|
82
|
+
trust_signals: float = 50.0
|
|
83
|
+
manipulation_risk: float = 0.0 # 0 = no risk, 100 = extreme risk
|
|
84
|
+
protection_level: float = 50.0
|
|
85
|
+
vulnerability_index: float = 50.0 # 0 = not vulnerable, 100 = very vulnerable
|
|
86
|
+
context_modifier: float = 0.0 # -10 to +10
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class ScanResult:
|
|
91
|
+
"""Complete scan result for an MCP server or agent."""
|
|
92
|
+
target: str
|
|
93
|
+
trust_score: float = 50.0
|
|
94
|
+
trust_tier: str = "FELLOW"
|
|
95
|
+
dimensions: DimensionScores = field(default_factory=DimensionScores)
|
|
96
|
+
findings: list[Finding] = field(default_factory=list)
|
|
97
|
+
tools_scanned: int = 0
|
|
98
|
+
recommendation: str = "UNKNOWN"
|
|
99
|
+
scan_source: str = "static_analysis"
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def critical_count(self) -> int:
|
|
103
|
+
return sum(1 for f in self.findings if f.severity == "CRITICAL")
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def high_count(self) -> int:
|
|
107
|
+
return sum(1 for f in self.findings if f.severity == "HIGH")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _score_to_tier(score: float) -> str:
|
|
111
|
+
if score >= 95:
|
|
112
|
+
return "SOVEREIGN"
|
|
113
|
+
if score >= 85:
|
|
114
|
+
return "SENTINEL"
|
|
115
|
+
if score >= 65:
|
|
116
|
+
return "MASTER"
|
|
117
|
+
if score >= 40:
|
|
118
|
+
return "ADEPT"
|
|
119
|
+
return "FELLOW"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def scan_tool_descriptions(tools: list[dict[str, Any]]) -> list[Finding]:
|
|
123
|
+
"""Scan tool descriptions for poisoning patterns."""
|
|
124
|
+
findings = []
|
|
125
|
+
for tool in tools:
|
|
126
|
+
name = tool.get("name", "unknown")
|
|
127
|
+
desc = tool.get("description", "")
|
|
128
|
+
|
|
129
|
+
for pattern, attack_class, severity, message in POISONING_PATTERNS:
|
|
130
|
+
if re.search(pattern, desc):
|
|
131
|
+
findings.append(Finding(
|
|
132
|
+
attack_class=attack_class,
|
|
133
|
+
severity=severity,
|
|
134
|
+
message=message,
|
|
135
|
+
location=f"tool:{name}/description",
|
|
136
|
+
pattern=pattern,
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
# Check input schema for sensitive parameter names
|
|
140
|
+
# Match against param name only (not description) to avoid false positives
|
|
141
|
+
schema = tool.get("inputSchema", tool.get("input_schema", {}))
|
|
142
|
+
props = schema.get("properties", {})
|
|
143
|
+
for param_name, param_def in props.items():
|
|
144
|
+
for pattern, attack_class, severity, message in SCHEMA_RISKS:
|
|
145
|
+
if re.search(pattern, param_name):
|
|
146
|
+
findings.append(Finding(
|
|
147
|
+
attack_class=attack_class,
|
|
148
|
+
severity=severity,
|
|
149
|
+
message=message,
|
|
150
|
+
location=f"tool:{name}/param:{param_name}",
|
|
151
|
+
pattern=pattern,
|
|
152
|
+
))
|
|
153
|
+
|
|
154
|
+
# Also check param descriptions for HIGH-confidence poisoning only
|
|
155
|
+
# Param descriptions are less exploitable than tool descriptions,
|
|
156
|
+
# so only flag the most dangerous patterns (instruction override, secrecy, exfil)
|
|
157
|
+
param_desc = param_def.get("description", "")
|
|
158
|
+
if param_desc:
|
|
159
|
+
PARAM_DESC_PATTERNS = [
|
|
160
|
+
POISONING_PATTERNS[0], # ignore previous instructions
|
|
161
|
+
POISONING_PATTERNS[2], # do not tell/reveal
|
|
162
|
+
POISONING_PATTERNS[3], # system prompt injection
|
|
163
|
+
POISONING_PATTERNS[5], # forward credentials
|
|
164
|
+
POISONING_PATTERNS[6], # send data to
|
|
165
|
+
]
|
|
166
|
+
for pattern, attack_class, severity, msg in PARAM_DESC_PATTERNS:
|
|
167
|
+
if re.search(pattern, param_desc):
|
|
168
|
+
findings.append(Finding(
|
|
169
|
+
attack_class=attack_class,
|
|
170
|
+
severity=severity,
|
|
171
|
+
message=f"Param description: {msg}",
|
|
172
|
+
location=f"tool:{name}/param:{param_name}",
|
|
173
|
+
pattern=pattern,
|
|
174
|
+
))
|
|
175
|
+
|
|
176
|
+
return findings
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def scan_metadata(metadata: dict[str, Any]) -> list[Finding]:
|
|
180
|
+
"""Scan server metadata for risk signals."""
|
|
181
|
+
findings = []
|
|
182
|
+
|
|
183
|
+
if not metadata.get("readme") and not metadata.get("description"):
|
|
184
|
+
findings.append(Finding("MIRAGE", "LOW",
|
|
185
|
+
"No README or documentation — reduced transparency",
|
|
186
|
+
location="server/metadata"))
|
|
187
|
+
|
|
188
|
+
if not metadata.get("license"):
|
|
189
|
+
findings.append(Finding("MIRAGE", "LOW",
|
|
190
|
+
"No license specified — unclear usage terms",
|
|
191
|
+
location="server/metadata"))
|
|
192
|
+
|
|
193
|
+
tool_count = metadata.get("tool_count", 0)
|
|
194
|
+
if tool_count > 25:
|
|
195
|
+
findings.append(Finding("MIRAGE", "MEDIUM",
|
|
196
|
+
f"Unusually high number of tools ({tool_count}) — possible capability inflation",
|
|
197
|
+
location="server/metadata"))
|
|
198
|
+
|
|
199
|
+
if not metadata.get("authentication"):
|
|
200
|
+
findings.append(Finding("LEECH", "LOW",
|
|
201
|
+
"No authentication documented",
|
|
202
|
+
location="server/metadata"))
|
|
203
|
+
|
|
204
|
+
return findings
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def calculate_score(findings: list[Finding], tools: list[dict], metadata: dict) -> ScanResult:
|
|
208
|
+
"""Calculate AEGIS trust score from scan findings."""
|
|
209
|
+
|
|
210
|
+
# Start with base scores
|
|
211
|
+
dims = DimensionScores()
|
|
212
|
+
|
|
213
|
+
# ── Trust Signals (35%) ──
|
|
214
|
+
# Based on metadata quality
|
|
215
|
+
# When source is a registry, some fields may simply not be exposed
|
|
216
|
+
# Treat missing-but-not-applicable fields as neutral
|
|
217
|
+
source = metadata.get("source", "")
|
|
218
|
+
is_registry = source in ("smithery", "mcp.run")
|
|
219
|
+
|
|
220
|
+
has_readme = bool(metadata.get("readme") or metadata.get("description"))
|
|
221
|
+
has_license = bool(metadata.get("license"))
|
|
222
|
+
has_repo = bool(metadata.get("repository"))
|
|
223
|
+
has_auth = bool(metadata.get("authentication"))
|
|
224
|
+
|
|
225
|
+
signal_score = 30.0 # base
|
|
226
|
+
if has_readme:
|
|
227
|
+
signal_score += 20
|
|
228
|
+
if has_license:
|
|
229
|
+
signal_score += 15
|
|
230
|
+
elif is_registry:
|
|
231
|
+
signal_score += 8 # neutral — registry doesn't expose this
|
|
232
|
+
if has_repo:
|
|
233
|
+
signal_score += 20
|
|
234
|
+
if has_auth:
|
|
235
|
+
signal_score += 15
|
|
236
|
+
elif is_registry:
|
|
237
|
+
signal_score += 8 # neutral — registry doesn't expose this
|
|
238
|
+
dims.trust_signals = min(100, signal_score)
|
|
239
|
+
|
|
240
|
+
# ── Manipulation Risk (25%) ──
|
|
241
|
+
severity_weights = {"CRITICAL": 30, "HIGH": 15, "MEDIUM": 5, "LOW": 2}
|
|
242
|
+
risk_score = 0
|
|
243
|
+
for f in findings:
|
|
244
|
+
risk_score += severity_weights.get(f.severity, 0)
|
|
245
|
+
dims.manipulation_risk = min(100, risk_score)
|
|
246
|
+
|
|
247
|
+
# ── Protection Level (15%) ──
|
|
248
|
+
protection = 40.0 # base
|
|
249
|
+
if has_auth:
|
|
250
|
+
protection += 30
|
|
251
|
+
tool_count = len(tools)
|
|
252
|
+
if 1 <= tool_count <= 15:
|
|
253
|
+
protection += 20 # reasonable scope
|
|
254
|
+
if has_license:
|
|
255
|
+
protection += 10
|
|
256
|
+
dims.protection_level = min(100, protection)
|
|
257
|
+
|
|
258
|
+
# ── Vulnerability Index (15%) ──
|
|
259
|
+
vuln = 20.0 # base vulnerability
|
|
260
|
+
critical_findings = sum(1 for f in findings if f.severity == "CRITICAL")
|
|
261
|
+
high_findings = sum(1 for f in findings if f.severity == "HIGH")
|
|
262
|
+
vuln += critical_findings * 25
|
|
263
|
+
vuln += high_findings * 10
|
|
264
|
+
if not has_auth:
|
|
265
|
+
vuln += 10
|
|
266
|
+
dims.vulnerability_index = min(100, vuln)
|
|
267
|
+
|
|
268
|
+
# ── Context Modifier (10%) ──
|
|
269
|
+
dims.context_modifier = 0 # neutral for static analysis
|
|
270
|
+
|
|
271
|
+
# ── Overall Score ──
|
|
272
|
+
# Higher trust_signals and protection = better
|
|
273
|
+
# Higher manipulation_risk and vulnerability = worse
|
|
274
|
+
overall = (
|
|
275
|
+
dims.trust_signals * 0.35
|
|
276
|
+
+ (100 - dims.manipulation_risk) * 0.25
|
|
277
|
+
+ dims.protection_level * 0.15
|
|
278
|
+
+ (100 - dims.vulnerability_index) * 0.15
|
|
279
|
+
+ (50 + dims.context_modifier * 5) * 0.10
|
|
280
|
+
)
|
|
281
|
+
overall = max(0, min(100, overall))
|
|
282
|
+
|
|
283
|
+
tier = _score_to_tier(overall)
|
|
284
|
+
|
|
285
|
+
# Recommendation
|
|
286
|
+
if overall >= 70 and critical_findings == 0:
|
|
287
|
+
recommendation = "SAFE"
|
|
288
|
+
elif overall >= 40 and critical_findings == 0:
|
|
289
|
+
recommendation = "CAUTION"
|
|
290
|
+
else:
|
|
291
|
+
recommendation = "AVOID"
|
|
292
|
+
|
|
293
|
+
return ScanResult(
|
|
294
|
+
target=metadata.get("name", "unknown"),
|
|
295
|
+
trust_score=round(overall, 1),
|
|
296
|
+
trust_tier=tier,
|
|
297
|
+
dimensions=dims,
|
|
298
|
+
findings=findings,
|
|
299
|
+
tools_scanned=len(tools),
|
|
300
|
+
recommendation=recommendation,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def scan_server(tools: list[dict[str, Any]], metadata: dict[str, Any]) -> ScanResult:
|
|
305
|
+
"""Full scan pipeline: analyze tools + metadata, calculate score."""
|
|
306
|
+
findings = []
|
|
307
|
+
findings.extend(scan_tool_descriptions(tools))
|
|
308
|
+
findings.extend(scan_metadata(metadata))
|
|
309
|
+
return calculate_score(findings, tools, metadata)
|