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.
Files changed (31) hide show
  1. glyph_scan-0.1.0/LICENSE +21 -0
  2. glyph_scan-0.1.0/PKG-INFO +13 -0
  3. glyph_scan-0.1.0/README.md +204 -0
  4. glyph_scan-0.1.0/pyproject.toml +24 -0
  5. glyph_scan-0.1.0/setup.cfg +4 -0
  6. glyph_scan-0.1.0/src/glyph/__init__.py +3 -0
  7. glyph_scan-0.1.0/src/glyph/cli.py +277 -0
  8. glyph_scan-0.1.0/src/glyph/connector/__init__.py +1 -0
  9. glyph_scan-0.1.0/src/glyph/connector/stdio.py +172 -0
  10. glyph_scan-0.1.0/src/glyph/engine/__init__.py +0 -0
  11. glyph_scan-0.1.0/src/glyph/engine/analyzer.py +44 -0
  12. glyph_scan-0.1.0/src/glyph/models/__init__.py +0 -0
  13. glyph_scan-0.1.0/src/glyph/models/config.py +51 -0
  14. glyph_scan-0.1.0/src/glyph/models/finding.py +36 -0
  15. glyph_scan-0.1.0/src/glyph/models/result.py +15 -0
  16. glyph_scan-0.1.0/src/glyph/parser/__init__.py +0 -0
  17. glyph_scan-0.1.0/src/glyph/parser/claude.py +72 -0
  18. glyph_scan-0.1.0/src/glyph/reporter/__init__.py +0 -0
  19. glyph_scan-0.1.0/src/glyph/reporter/human.py +164 -0
  20. glyph_scan-0.1.0/src/glyph/reporter/json_report.py +123 -0
  21. glyph_scan-0.1.0/src/glyph/rules/__init__.py +0 -0
  22. glyph_scan-0.1.0/src/glyph/rules/base.py +25 -0
  23. glyph_scan-0.1.0/src/glyph/rules/credential_exposure.py +154 -0
  24. glyph_scan-0.1.0/src/glyph/rules/tool_poisoning.py +192 -0
  25. glyph_scan-0.1.0/src/glyph/rules/transport_security.py +109 -0
  26. glyph_scan-0.1.0/src/glyph_scan.egg-info/PKG-INFO +13 -0
  27. glyph_scan-0.1.0/src/glyph_scan.egg-info/SOURCES.txt +29 -0
  28. glyph_scan-0.1.0/src/glyph_scan.egg-info/dependency_links.txt +1 -0
  29. glyph_scan-0.1.0/src/glyph_scan.egg-info/entry_points.txt +2 -0
  30. glyph_scan-0.1.0/src/glyph_scan.egg-info/requires.txt +5 -0
  31. glyph_scan-0.1.0/src/glyph_scan.egg-info/top_level.txt +1 -0
@@ -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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
7
+ [![Tests](https://img.shields.io/badge/tests-45%20passing-brightgreen.svg)]()
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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Glyph — MCP Security Scanner"""
2
+
3
+ __version__ = "0.1.0"
@@ -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."""