glyph-scan 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.
- glyph_scan-0.1.0/LICENSE +21 -0
- glyph_scan-0.1.0/PKG-INFO +13 -0
- glyph_scan-0.1.0/README.md +204 -0
- glyph_scan-0.1.0/pyproject.toml +24 -0
- glyph_scan-0.1.0/setup.cfg +4 -0
- glyph_scan-0.1.0/src/glyph/__init__.py +3 -0
- glyph_scan-0.1.0/src/glyph/cli.py +277 -0
- glyph_scan-0.1.0/src/glyph/connector/__init__.py +1 -0
- glyph_scan-0.1.0/src/glyph/connector/stdio.py +172 -0
- glyph_scan-0.1.0/src/glyph/engine/__init__.py +0 -0
- glyph_scan-0.1.0/src/glyph/engine/analyzer.py +44 -0
- glyph_scan-0.1.0/src/glyph/models/__init__.py +0 -0
- glyph_scan-0.1.0/src/glyph/models/config.py +51 -0
- glyph_scan-0.1.0/src/glyph/models/finding.py +36 -0
- glyph_scan-0.1.0/src/glyph/models/result.py +15 -0
- glyph_scan-0.1.0/src/glyph/parser/__init__.py +0 -0
- glyph_scan-0.1.0/src/glyph/parser/claude.py +72 -0
- glyph_scan-0.1.0/src/glyph/reporter/__init__.py +0 -0
- glyph_scan-0.1.0/src/glyph/reporter/human.py +164 -0
- glyph_scan-0.1.0/src/glyph/reporter/json_report.py +123 -0
- glyph_scan-0.1.0/src/glyph/rules/__init__.py +0 -0
- glyph_scan-0.1.0/src/glyph/rules/base.py +25 -0
- glyph_scan-0.1.0/src/glyph/rules/credential_exposure.py +154 -0
- glyph_scan-0.1.0/src/glyph/rules/tool_poisoning.py +192 -0
- glyph_scan-0.1.0/src/glyph/rules/transport_security.py +109 -0
- glyph_scan-0.1.0/src/glyph_scan.egg-info/PKG-INFO +13 -0
- glyph_scan-0.1.0/src/glyph_scan.egg-info/SOURCES.txt +29 -0
- glyph_scan-0.1.0/src/glyph_scan.egg-info/dependency_links.txt +1 -0
- glyph_scan-0.1.0/src/glyph_scan.egg-info/entry_points.txt +2 -0
- glyph_scan-0.1.0/src/glyph_scan.egg-info/requires.txt +5 -0
- glyph_scan-0.1.0/src/glyph_scan.egg-info/top_level.txt +1 -0
glyph_scan-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Haseeb Khalid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: glyph-scan
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP security scanner — read the runes before your agent steps on them — find vulnerabilities in AI agent tool configurations
|
|
5
|
+
Author-email: Haseeb Khalid <haseebkhalid1507@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: click>=8.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
13
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# 🔮 Glyph
|
|
2
|
+
|
|
3
|
+
**Read the runes before your agent steps on them.**
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
[]()
|
|
8
|
+
|
|
9
|
+
An MCP security scanner that finds tool poisoning, credential leaks, and insecure transports in your AI agent configurations — before attackers do.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## The Problem
|
|
14
|
+
|
|
15
|
+
MCP (Model Context Protocol) is how AI agents connect to tools. Claude Desktop, Cursor, pi, Windsurf — they all use it. There are now **16,000+ MCP servers** in the ecosystem.
|
|
16
|
+
|
|
17
|
+
[66% of them have security findings.](https://agentseal.org/blog/mcp-server-security-findings)
|
|
18
|
+
|
|
19
|
+
Tool poisoning — hiding malicious instructions in tool descriptions — has a **91% success rate** against production AI agents. The [first malicious MCP server](https://semgrep.dev/blog/2025/so-the-first-malicious-mcp-server-has-been-found-on-npm-what-does-this-mean-for-mcp-security/) was found on npm in September 2025, silently BCC'ing every email to an attacker.
|
|
20
|
+
|
|
21
|
+
Nobody audits these servers before connecting. Glyph does.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## What It Does
|
|
26
|
+
|
|
27
|
+
🔴 **Tool Poisoning Detection** — Finds hidden instructions, prompt injection, unicode tricks, base64 payloads, and behavioral directives buried in tool descriptions
|
|
28
|
+
|
|
29
|
+
🔴 **Credential Exposure** — Catches hardcoded API keys (OpenAI, AWS, GitHub, Anthropic), tokens, and secrets that should be in environment variables
|
|
30
|
+
|
|
31
|
+
🟡 **Transport Security** — Flags MCP servers running over plain HTTP, missing TLS, or lacking authentication
|
|
32
|
+
|
|
33
|
+
🔵 **Live Scanning** — Connects to real MCP servers via JSON-RPC, pulls actual tool definitions, and scans them in real time
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install glyph
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Scan a config file:
|
|
44
|
+
```bash
|
|
45
|
+
glyph scan ~/.config/claude/claude_desktop_config.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
That's it. Results in seconds.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Static Scan — Check Config Files
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Scan a single config
|
|
58
|
+
glyph scan config.json
|
|
59
|
+
|
|
60
|
+
# Scan a directory (finds all .json files)
|
|
61
|
+
glyph scan ~/.config/claude/
|
|
62
|
+
|
|
63
|
+
# JSON output for CI/CD
|
|
64
|
+
glyph scan config.json --format json
|
|
65
|
+
|
|
66
|
+
# Only show high severity and above
|
|
67
|
+
glyph scan config.json --severity high
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Live Scan — Connect to Real Servers
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Connect to servers defined in config, pull live tool definitions, scan
|
|
74
|
+
glyph live config.json
|
|
75
|
+
|
|
76
|
+
# Custom timeout for slow server startups
|
|
77
|
+
glyph live config.json --timeout 60
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Live scanning spawns each stdio server, performs the MCP handshake, requests tool definitions via `tools/list`, scans the actual descriptions, then terminates the connection. No tools are executed — just inspected.
|
|
81
|
+
|
|
82
|
+
### Exit Codes
|
|
83
|
+
|
|
84
|
+
| Code | Meaning |
|
|
85
|
+
|------|---------|
|
|
86
|
+
| `0` | Clean — no findings |
|
|
87
|
+
| `1` | Findings detected |
|
|
88
|
+
| `2` | Critical findings detected |
|
|
89
|
+
|
|
90
|
+
Use in CI: `glyph scan config.json || echo "Security issues found"`
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Detection Rules
|
|
95
|
+
|
|
96
|
+
| Rule | Severity | What It Catches |
|
|
97
|
+
|------|----------|----------------|
|
|
98
|
+
| `tool-poisoning` | 🔴 CRITICAL/HIGH | Hidden instructions in tool descriptions, prompt injection patterns, unicode obfuscation, base64 payloads, system prompt overrides, excessive description length |
|
|
99
|
+
| `credential-exposure` | 🔴 CRITICAL | Hardcoded OpenAI, AWS, GitHub, Anthropic keys and tokens. Allows env var references (`${VAR}`) |
|
|
100
|
+
| `transport-security` | 🟡 HIGH/MEDIUM | HTTP without TLS, SSE without encryption, network transports missing authentication |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Example Output
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
🔒 Glyph — MCP Security Scanner
|
|
108
|
+
|
|
109
|
+
Scanning: config.json
|
|
110
|
+
Found 3 servers
|
|
111
|
+
|
|
112
|
+
━━━ Findings ━━━
|
|
113
|
+
|
|
114
|
+
🔴 CRITICAL: Hardcoded OpenAI API Key
|
|
115
|
+
Rule: credential-exposure
|
|
116
|
+
Location: server "gpt-tools"
|
|
117
|
+
Fix: Use environment variable reference ${OPENAI_API_KEY} instead
|
|
118
|
+
|
|
119
|
+
🟡 HIGH: Potential tool poisoning detected
|
|
120
|
+
Rule: tool-poisoning
|
|
121
|
+
Location: tool "email_helper" in server "utils"
|
|
122
|
+
Fix: Review tool description for hidden instructions
|
|
123
|
+
|
|
124
|
+
🟡 HIGH: Insecure transport
|
|
125
|
+
Rule: transport-security
|
|
126
|
+
Location: server "remote-api"
|
|
127
|
+
Fix: Use HTTPS instead of HTTP
|
|
128
|
+
|
|
129
|
+
━━━ Summary ━━━
|
|
130
|
+
Scanned: 1 config, 3 servers
|
|
131
|
+
Findings: 1 critical, 2 high, 0 medium, 0 low
|
|
132
|
+
Status: FAIL (CRITICAL)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## How Live Scanning Works
|
|
138
|
+
|
|
139
|
+
Glyph implements a minimal MCP client that performs the standard JSON-RPC handshake:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
Glyph → Server: initialize (identify as scanner)
|
|
143
|
+
Server → Glyph: capabilities + server info
|
|
144
|
+
|
|
145
|
+
Glyph → Server: tools/list (enumerate all tools)
|
|
146
|
+
Server → Glyph: tool definitions (name, description, schema)
|
|
147
|
+
|
|
148
|
+
Glyph: Scan descriptions → Report findings → Disconnect
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
No tools are invoked. Glyph only inspects definitions — it never executes tool calls.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Roadmap
|
|
156
|
+
|
|
157
|
+
- [ ] **Permission Audit Rule** — Flag dangerous capability combinations (filesystem + network = exfiltration risk)
|
|
158
|
+
- [ ] **Supply Chain Rule** — Detect typosquatting and known malicious MCP packages
|
|
159
|
+
- [ ] **Semantic Analysis** — Sentence embeddings to catch sophisticated poisoning that bypasses pattern matching
|
|
160
|
+
- [ ] **LLM Classification** — Optional deep analysis using LLM for context-aware threat detection
|
|
161
|
+
- [ ] **GitHub Action** — `glyph-action` for CI/CD pipeline integration
|
|
162
|
+
- [ ] **SARIF Output** — Standard format for security tool integration
|
|
163
|
+
- [ ] **SSE/HTTP Transport** — Live scanning for network-based MCP servers
|
|
164
|
+
- [ ] **MCP Server Mode** — Run Glyph itself as an MCP server so AI agents can scan their own configs
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## ⚠️ Security Notice
|
|
169
|
+
|
|
170
|
+
Glyph's live scanning spawns MCP servers defined in config files. **A malicious config can contain arbitrary commands.** Only scan configs you trust, or run Glyph inside a container/sandbox:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Sandboxed scan (recommended for untrusted configs)
|
|
174
|
+
docker run --rm -v $(pwd)/config.json:/scan/config.json glyph scan /scan/config.json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Static scanning (`glyph scan`) only reads JSON files — no code execution. Live scanning (`glyph live`) spawns processes. Know the difference.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Development
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
git clone https://github.com/HaseebKhalid1507/glyph.git
|
|
185
|
+
cd glyph
|
|
186
|
+
pip install -e ".[dev]"
|
|
187
|
+
pytest tests/ -v
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Contributing
|
|
193
|
+
|
|
194
|
+
Issues and PRs welcome. If you find a new MCP attack pattern, open an issue.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Author
|
|
199
|
+
|
|
200
|
+
Built by [Haseeb Khalid](https://github.com/HaseebKhalid1507) — security engineer, agent builder, rune reader.
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.setuptools.packages.find]
|
|
6
|
+
where = ["src"]
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "glyph-scan"
|
|
10
|
+
version = "0.1.0"
|
|
11
|
+
description = "MCP security scanner — read the runes before your agent steps on them — find vulnerabilities in AI agent tool configurations"
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
authors = [{name = "Haseeb Khalid", email = "haseebkhalid1507@gmail.com"}]
|
|
15
|
+
dependencies = ["click>=8.0"]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
glyph = "glyph.cli:main"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = ["pytest>=7.0", "pytest-cov"]
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Glyph CLI interface."""
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from .engine.analyzer import AnalysisEngine
|
|
8
|
+
from .reporter.human import HumanReporter
|
|
9
|
+
from .reporter.json_report import JsonReporter
|
|
10
|
+
from .parser.claude import ClaudeDesktopParser
|
|
11
|
+
from .rules.tool_poisoning import ToolPoisoningRule
|
|
12
|
+
from .rules.credential_exposure import CredentialExposureRule
|
|
13
|
+
from .rules.transport_security import TransportSecurityRule
|
|
14
|
+
from .models.finding import Severity
|
|
15
|
+
from .models.config import Tool, TransportType
|
|
16
|
+
from .connector.stdio import StdioConnector
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_all_rules():
|
|
20
|
+
"""Get all available security rules."""
|
|
21
|
+
return [
|
|
22
|
+
ToolPoisoningRule(),
|
|
23
|
+
CredentialExposureRule(),
|
|
24
|
+
TransportSecurityRule()
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def discover_configs(target_path: Path) -> list[Path]:
|
|
29
|
+
"""Discover MCP config files in target path."""
|
|
30
|
+
if target_path.is_file():
|
|
31
|
+
return [target_path]
|
|
32
|
+
elif target_path.is_dir():
|
|
33
|
+
# Find all JSON files in directory
|
|
34
|
+
json_files = []
|
|
35
|
+
for pattern in ["*.json"]:
|
|
36
|
+
json_files.extend(target_path.glob(pattern))
|
|
37
|
+
return sorted(json_files)
|
|
38
|
+
else:
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_config_file(file_path: Path):
|
|
43
|
+
"""Parse a configuration file, auto-detecting format."""
|
|
44
|
+
# For now we only support Claude Desktop format
|
|
45
|
+
parser = ClaudeDesktopParser()
|
|
46
|
+
return parser.parse_file(file_path)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def filter_by_severity(results, min_severity: Severity):
|
|
50
|
+
"""Filter scan results to only include findings of minimum severity or higher."""
|
|
51
|
+
filtered_results = []
|
|
52
|
+
|
|
53
|
+
for result in results:
|
|
54
|
+
filtered_findings = [
|
|
55
|
+
f for f in result.findings
|
|
56
|
+
if f.severity >= min_severity
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# Create new result with filtered findings
|
|
60
|
+
from .models.result import ScanResult
|
|
61
|
+
filtered_result = ScanResult(
|
|
62
|
+
config=result.config,
|
|
63
|
+
findings=filtered_findings,
|
|
64
|
+
rules_applied=result.rules_applied,
|
|
65
|
+
scan_time=result.scan_time
|
|
66
|
+
)
|
|
67
|
+
filtered_results.append(filtered_result)
|
|
68
|
+
|
|
69
|
+
return filtered_results
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@click.group()
|
|
73
|
+
def cli():
|
|
74
|
+
"""Glyph — MCP Security Scanner"""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@cli.command()
|
|
79
|
+
@click.argument('target', type=click.Path(exists=True, path_type=Path))
|
|
80
|
+
@click.option('--format', 'output_format',
|
|
81
|
+
type=click.Choice(['human', 'json'], case_sensitive=False),
|
|
82
|
+
default='human',
|
|
83
|
+
help='Output format (default: human)')
|
|
84
|
+
@click.option('--severity',
|
|
85
|
+
type=click.Choice(['critical', 'high', 'medium', 'low', 'info'], case_sensitive=False),
|
|
86
|
+
help='Only show findings of this severity or higher')
|
|
87
|
+
def scan(target, output_format, severity):
|
|
88
|
+
"""Scan MCP configuration files for security issues."""
|
|
89
|
+
|
|
90
|
+
# Parse severity filter if provided
|
|
91
|
+
min_severity = None
|
|
92
|
+
if severity:
|
|
93
|
+
min_severity = Severity(severity.lower())
|
|
94
|
+
|
|
95
|
+
# Discover config files
|
|
96
|
+
config_files = discover_configs(target)
|
|
97
|
+
if not config_files:
|
|
98
|
+
click.echo(f"Error: No configuration files found in {target}", err=True)
|
|
99
|
+
sys.exit(2)
|
|
100
|
+
|
|
101
|
+
# Parse configurations
|
|
102
|
+
configs = []
|
|
103
|
+
for file_path in config_files:
|
|
104
|
+
try:
|
|
105
|
+
config = parse_config_file(file_path)
|
|
106
|
+
if config:
|
|
107
|
+
configs.append(config)
|
|
108
|
+
else:
|
|
109
|
+
click.echo(f"Warning: Could not parse {file_path}", err=True)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
click.echo(f"Error parsing {file_path}: {e}", err=True)
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
if not configs:
|
|
115
|
+
click.echo("Error: No valid configuration files found", err=True)
|
|
116
|
+
sys.exit(2)
|
|
117
|
+
|
|
118
|
+
# Run analysis
|
|
119
|
+
rules = get_all_rules()
|
|
120
|
+
engine = AnalysisEngine(rules)
|
|
121
|
+
results = engine.analyze_all(configs)
|
|
122
|
+
|
|
123
|
+
# Apply severity filter if specified
|
|
124
|
+
if min_severity:
|
|
125
|
+
results = filter_by_severity(results, min_severity)
|
|
126
|
+
|
|
127
|
+
# Generate report
|
|
128
|
+
if output_format.lower() == 'json':
|
|
129
|
+
reporter = JsonReporter()
|
|
130
|
+
report = reporter.generate(results)
|
|
131
|
+
click.echo(report)
|
|
132
|
+
|
|
133
|
+
# Extract exit code from JSON report for proper CLI behavior
|
|
134
|
+
try:
|
|
135
|
+
report_data = json.loads(report)
|
|
136
|
+
exit_code = report_data.get('summary', {}).get('exit_code', 0)
|
|
137
|
+
sys.exit(exit_code)
|
|
138
|
+
except:
|
|
139
|
+
sys.exit(0)
|
|
140
|
+
else:
|
|
141
|
+
reporter = HumanReporter()
|
|
142
|
+
report = reporter.generate(results)
|
|
143
|
+
click.echo(report)
|
|
144
|
+
|
|
145
|
+
# Determine exit code from findings
|
|
146
|
+
has_critical = any(
|
|
147
|
+
f.severity == Severity.CRITICAL
|
|
148
|
+
for result in results
|
|
149
|
+
for f in result.findings
|
|
150
|
+
)
|
|
151
|
+
has_findings = any(result.findings for result in results)
|
|
152
|
+
|
|
153
|
+
if has_critical:
|
|
154
|
+
sys.exit(2)
|
|
155
|
+
elif has_findings:
|
|
156
|
+
sys.exit(1)
|
|
157
|
+
else:
|
|
158
|
+
sys.exit(0)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@cli.command()
|
|
162
|
+
@click.argument('config_file', type=click.Path(exists=True, path_type=Path))
|
|
163
|
+
@click.option('--format', 'output_format',
|
|
164
|
+
type=click.Choice(['human', 'json'], case_sensitive=False),
|
|
165
|
+
default='human',
|
|
166
|
+
help='Output format (default: human)')
|
|
167
|
+
@click.option('--timeout',
|
|
168
|
+
default=30,
|
|
169
|
+
help='Timeout in seconds for server connections (default: 30)')
|
|
170
|
+
@click.option('--severity',
|
|
171
|
+
type=click.Choice(['critical', 'high', 'medium', 'low', 'info'], case_sensitive=False),
|
|
172
|
+
help='Only show findings of this severity or higher')
|
|
173
|
+
def live(config_file, output_format, timeout, severity):
|
|
174
|
+
"""Connect to MCP servers and scan with live tool definitions."""
|
|
175
|
+
|
|
176
|
+
# Parse severity filter if provided
|
|
177
|
+
min_severity = None
|
|
178
|
+
if severity:
|
|
179
|
+
min_severity = Severity(severity.lower())
|
|
180
|
+
|
|
181
|
+
# Parse the config file
|
|
182
|
+
try:
|
|
183
|
+
config = parse_config_file(config_file)
|
|
184
|
+
if not config:
|
|
185
|
+
click.echo(f"Error: Could not parse {config_file}", err=True)
|
|
186
|
+
sys.exit(2)
|
|
187
|
+
except Exception as e:
|
|
188
|
+
click.echo(f"Error parsing {config_file}: {e}", err=True)
|
|
189
|
+
sys.exit(2)
|
|
190
|
+
|
|
191
|
+
# Connect to stdio servers and pull live tools
|
|
192
|
+
live_servers = []
|
|
193
|
+
for server in config.servers:
|
|
194
|
+
if server.transport.type == TransportType.STDIO:
|
|
195
|
+
if not server.transport.command:
|
|
196
|
+
click.echo(f"Warning: Server '{server.name}' has stdio transport but no command", err=True)
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
# Prepare command with args
|
|
200
|
+
command = server.transport.command.copy() if server.transport.command else []
|
|
201
|
+
if server.transport.args:
|
|
202
|
+
command.extend(server.transport.args)
|
|
203
|
+
|
|
204
|
+
click.echo(f"Connecting to {server.name}...")
|
|
205
|
+
try:
|
|
206
|
+
connector = StdioConnector(command, env=server.env_vars, timeout=timeout)
|
|
207
|
+
raw_tools = connector.connect()
|
|
208
|
+
|
|
209
|
+
# Convert raw tools to Tool objects
|
|
210
|
+
tools = []
|
|
211
|
+
for raw_tool in raw_tools:
|
|
212
|
+
tool = Tool(
|
|
213
|
+
name=raw_tool.get("name", "unknown"),
|
|
214
|
+
description=raw_tool.get("description", ""),
|
|
215
|
+
server_name=server.name,
|
|
216
|
+
schema=raw_tool.get("inputSchema", {})
|
|
217
|
+
)
|
|
218
|
+
tools.append(tool)
|
|
219
|
+
|
|
220
|
+
# Update server with live tools
|
|
221
|
+
server.tools = tools
|
|
222
|
+
click.echo(f"✓ Connected to {server.name}, pulled {len(tools)} tools")
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
click.echo(f"Warning: Failed to connect to {server.name}: {e}", err=True)
|
|
226
|
+
# Continue with next server
|
|
227
|
+
|
|
228
|
+
live_servers.append(server)
|
|
229
|
+
|
|
230
|
+
# Update config with enriched servers
|
|
231
|
+
config.servers = live_servers
|
|
232
|
+
|
|
233
|
+
# Run analysis on the enriched config
|
|
234
|
+
rules = get_all_rules()
|
|
235
|
+
engine = AnalysisEngine(rules)
|
|
236
|
+
results = [engine.analyze(config)]
|
|
237
|
+
|
|
238
|
+
# Apply severity filter if specified
|
|
239
|
+
if min_severity:
|
|
240
|
+
results = filter_by_severity(results, min_severity)
|
|
241
|
+
|
|
242
|
+
# Generate report
|
|
243
|
+
if output_format.lower() == 'json':
|
|
244
|
+
reporter = JsonReporter()
|
|
245
|
+
report = reporter.generate(results)
|
|
246
|
+
click.echo(report)
|
|
247
|
+
|
|
248
|
+
# Extract exit code from JSON report for proper CLI behavior
|
|
249
|
+
try:
|
|
250
|
+
report_data = json.loads(report)
|
|
251
|
+
exit_code = report_data.get('summary', {}).get('exit_code', 0)
|
|
252
|
+
sys.exit(exit_code)
|
|
253
|
+
except:
|
|
254
|
+
sys.exit(0)
|
|
255
|
+
else:
|
|
256
|
+
reporter = HumanReporter()
|
|
257
|
+
report = reporter.generate(results)
|
|
258
|
+
click.echo(report)
|
|
259
|
+
|
|
260
|
+
# Determine exit code from findings
|
|
261
|
+
has_critical = any(
|
|
262
|
+
f.severity == Severity.CRITICAL
|
|
263
|
+
for result in results
|
|
264
|
+
for f in result.findings
|
|
265
|
+
)
|
|
266
|
+
has_findings = any(result.findings for result in results)
|
|
267
|
+
|
|
268
|
+
if has_critical:
|
|
269
|
+
sys.exit(2)
|
|
270
|
+
elif has_findings:
|
|
271
|
+
sys.exit(1)
|
|
272
|
+
else:
|
|
273
|
+
sys.exit(0)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
if __name__ == "__main__":
|
|
277
|
+
cli()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP server connectors."""
|