mcp-skill-scanner 0.2.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.
- mcp_skill_scanner-0.2.0/PKG-INFO +209 -0
- mcp_skill_scanner-0.2.0/README.md +195 -0
- mcp_skill_scanner-0.2.0/pyproject.toml +37 -0
- mcp_skill_scanner-0.2.0/pyproject.toml.orig +42 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/__init__.py +3 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/cli.py +300 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/corpus.py +47 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/discovery.py +90 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/embedder.py +171 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/engine.py +47 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/errors.py +15 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/model.py +184 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/parsers/__init__.py +22 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/parsers/_common.py +66 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/parsers/_json_pos.py +151 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/parsers/mcp_config.py +138 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/parsers/package_json.py +69 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/parsers/pyproject.py +85 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/parsers/python_source.py +32 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/parsers/requirements.py +50 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/parsers/skill_markdown.py +82 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/parsers/smithery_yaml.py +495 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/py.typed +0 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/rules/__init__.py +130 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/rules/dependency.py +24 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/rules/dependency_registry.py +296 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/rules/mcp_permissions.py +204 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/rules/parsing.py +39 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/rules/remote.py +242 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/rules/semantic.py +121 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/rules/skill_patterns.py +153 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/rules/taint.py +58 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/sarif.py +123 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/sources/__init__.py +28 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/sources/_http.py +186 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/sources/providers.py +235 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/taint/__init__.py +7 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/taint/analyzer.py +412 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/text.py +26 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/triage/__init__.py +6 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/triage/graph.py +220 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/triage/llm.py +61 -0
- mcp_skill_scanner-0.2.0/src/mcpsec/triage/tracing.py +101 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mcp-skill-scanner
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Static security scanner for MCP servers and AI agent skills.
|
|
5
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
6
|
+
Requires-Dist: langgraph>=1.2,<2 ; extra == 'agent'
|
|
7
|
+
Requires-Dist: langfuse>=5.1,<6 ; extra == 'agent'
|
|
8
|
+
Requires-Dist: onnxruntime==1.31.1 ; extra == 'semantic'
|
|
9
|
+
Requires-Dist: tokenizers==0.23.3 ; extra == 'semantic'
|
|
10
|
+
Requires-Python: >=3.13
|
|
11
|
+
Provides-Extra: agent
|
|
12
|
+
Provides-Extra: semantic
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# mcp-security-scanner
|
|
16
|
+
|
|
17
|
+
[](https://github.com/A-Djihadi/mcp-security-scanner/actions/workflows/ci.yml)
|
|
18
|
+
|
|
19
|
+
Static security scanner for MCP servers and AI agent skills.
|
|
20
|
+
|
|
21
|
+
Detects excessive permissions (unrestricted shell, unsanitized SQL), risky
|
|
22
|
+
dependencies, and malicious patterns. Emits SARIF v2.1.0 for CI integration.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
Requires Python 3.13.
|
|
27
|
+
|
|
28
|
+
Install the published package from PyPI:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install mcp-skill-scanner
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Opt into semantic detection with the `[semantic]` extra:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install mcp-skill-scanner[semantic]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or install from source with [`uv`](https://docs.astral.sh/uv/):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/A-Djihadi/mcp-security-scanner.git
|
|
44
|
+
cd mcp-security-scanner
|
|
45
|
+
uv sync
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Scan a directory, write SARIF to a file (default format)
|
|
52
|
+
uv run mcpsec scan . --output results.sarif
|
|
53
|
+
|
|
54
|
+
# Human-readable output
|
|
55
|
+
uv run mcpsec scan . --format text
|
|
56
|
+
|
|
57
|
+
# Only error-level findings
|
|
58
|
+
uv run mcpsec scan . --severity error
|
|
59
|
+
|
|
60
|
+
# Only specific rules
|
|
61
|
+
uv run mcpsec scan . --rules MCP-shell-unrestricted,DEP-unpinned
|
|
62
|
+
|
|
63
|
+
# Exclude a rule
|
|
64
|
+
uv run mcpsec scan . --exclude-rule SKL-shell-execution
|
|
65
|
+
|
|
66
|
+
# Skip paths (globs, repeatable), e.g. to scan a repo that contains test fixtures
|
|
67
|
+
uv run mcpsec scan . --exclude tests/fixtures
|
|
68
|
+
|
|
69
|
+
# Network-backed rules (OSV.dev, PyPI/npm, reachability) -- opt-in
|
|
70
|
+
uv run mcpsec scan . --online
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Options
|
|
74
|
+
|
|
75
|
+
| Flag | Description |
|
|
76
|
+
|------|-------------|
|
|
77
|
+
| `--format sarif\|text` | Output format (default `sarif`). |
|
|
78
|
+
| `--output FILE` | Write output to `FILE` instead of stdout. |
|
|
79
|
+
| `--severity error\|warning\|note` | Only report findings at/above this severity. |
|
|
80
|
+
| `--rules IDS` | Comma-separated rule IDs to run; all others are disabled. |
|
|
81
|
+
| `--exclude-rule IDS` | Comma-separated rule IDs to disable. |
|
|
82
|
+
| `--exclude GLOB` | Skip paths matching `GLOB` (relative to the scan root, matches parent directories too; repeatable). |
|
|
83
|
+
| `--online` | Enable network-backed rules (`DEP-known-vulnerability`, `DEP-outdated`, `DEP-unknown`, `MCP-unreachable`). Only `http(s)` is fetched; private, loopback and link-local addresses are never probed. |
|
|
84
|
+
| `--fail-on error\|warning\|note` | Minimum severity that fails the scan (default `error`). |
|
|
85
|
+
| `--semantic` | Enable semantic malicious-intent detection via the local embedding model (requires the `[semantic]` extra). |
|
|
86
|
+
|
|
87
|
+
### Semantic detection (optional)
|
|
88
|
+
|
|
89
|
+
`--semantic` opts into embedding-based malicious-intent detection for skills
|
|
90
|
+
(`SKL-semantic-malicious`). It is **off by default**: the base install stays
|
|
91
|
+
PyYAML-only, and a default scan never loads the model runtime.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Opt into the heavier install once:
|
|
95
|
+
uv sync --extra semantic # or: pip install mcp-skill-scanner[semantic]
|
|
96
|
+
|
|
97
|
+
# Then scan with semantic detection enabled:
|
|
98
|
+
uv run mcpsec scan . --semantic
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The default embedder is a small, CPU-only ONNX sentence-transformer:
|
|
102
|
+
|
|
103
|
+
- **Model:** `all-MiniLM-L6-v2` (ONNX export `Xenova/all-MiniLM-L6-v2`), Apache-2.0.
|
|
104
|
+
- **Pinned revision:** `751bff37182d3f1213fa05d7196b954e230abad9` (files verified by SHA-256).
|
|
105
|
+
- **Corpus:** a curated, versioned set of malicious-intent anchors
|
|
106
|
+
(`src/mcpsec/corpus.py`, version `1`); the model revision and corpus version
|
|
107
|
+
are recorded on every `SKL-semantic-malicious` finding for reproducibility.
|
|
108
|
+
|
|
109
|
+
**To verify:** the similarity threshold (default `0.85`) and the corpus anchor
|
|
110
|
+
coverage are calibrated against synthetic probes, not a production corpus; they
|
|
111
|
+
should be re-tuned against real-world skill data before relying on this rule as
|
|
112
|
+
a CI gate.
|
|
113
|
+
|
|
114
|
+
### Exit codes
|
|
115
|
+
|
|
116
|
+
| Code | Meaning |
|
|
117
|
+
|------|---------|
|
|
118
|
+
| 0 | Scan completed; no findings at or above `--fail-on`. |
|
|
119
|
+
| 1 | Scan completed; one or more findings at or above `--fail-on`. |
|
|
120
|
+
| 2 | Internal error (bad args, unreadable path, parse error). |
|
|
121
|
+
|
|
122
|
+
## Rules
|
|
123
|
+
|
|
124
|
+
| Rule ID | Level | Detects |
|
|
125
|
+
|---------|-------|---------|
|
|
126
|
+
| `MCP-shell-unrestricted` | error | Server `command` is a shell interpreter (`sh`, `bash`, `zsh`, `dash`, `ksh`, `fish`, `csh`, `powershell`, `pwsh`, `cmd`…), including behind a launcher (`env bash`, `sudo sh`). |
|
|
127
|
+
| `MCP-allow-all` | error | `args` contains `--allow-all` / `--allow-everything` / `-A`. |
|
|
128
|
+
| `MCP-broad-network` | error | `--allow-net` with no host restriction (bare flag or empty value). |
|
|
129
|
+
| `MCP-inline-code` | warning | Interpreter runs code passed on the command line (`python -c`, `node -e`, `ruby -e`, `perl -e`, `php -r`). |
|
|
130
|
+
| `MCP-insecure-transport` | error | Remote MCP server uses plaintext `http://`. |
|
|
131
|
+
| `MCP-header-secret` | warning | A secret is stored inline in a remote server's `headers`. |
|
|
132
|
+
| `MCP-remote-no-auth` | note | Remote MCP server has no auth-bearing header. |
|
|
133
|
+
| `MCP-parse-ambiguous` | note | Server command cannot be statically resolved from the config. |
|
|
134
|
+
| `MCP-unreachable` | note | Remote endpoint refused the connection (`--online`). |
|
|
135
|
+
| `FLOW-shell-injection` | error | Tainted MCP tool input reaches a shell execution sink (Python taint analysis). |
|
|
136
|
+
| `FLOW-sql-injection` | error | Tainted input reaches a SQL query sink. |
|
|
137
|
+
| `FLOW-code-eval` | warning | Tainted input reaches `eval` / `exec` / `compile`. |
|
|
138
|
+
| `MCP-broad-filesystem` | warning | `--allow-read` / `--allow-write` at a root-level path (`/`, `~`, `C:\`, `C:/`) or with no path. |
|
|
139
|
+
| `SKL-shell-execution` | warning | Skill body invokes a shell with interpolated input (f-string / concatenation). |
|
|
140
|
+
| `SKL-remote-code-exec` | error | Skill body pipes remote content to a shell (`curl … \| sh`, `wget … \| bash`, `eval(fetch(…))`). |
|
|
141
|
+
| `SKL-obfuscated-command` | warning | Shell command decodes a `base64` payload (`base64 -d` / `--decode`). |
|
|
142
|
+
| `SKL-sql-concatenation` | error | SQL built by string concatenation/interpolation; parameterized `?` queries do not trigger. |
|
|
143
|
+
| `DEP-unpinned` | warning | A direct dependency whose version spec is unpinned (`*`, `latest`, `^`, `~`, empty). |
|
|
144
|
+
| `DEP-known-vulnerability` | error | Declared version has a known vulnerability in OSV.dev (`--online`). |
|
|
145
|
+
| `DEP-outdated` | warning | Declared version is behind the latest published one (`--online`). |
|
|
146
|
+
| `DEP-unknown` | warning | Name closely resembles a popular package, a typosquat signal (`--online`). |
|
|
147
|
+
| `SKL-semantic-malicious` | warning | Skill prose is semantically similar to a known-malicious reference (opt-in via `--semantic`). |
|
|
148
|
+
|
|
149
|
+
## LLM triage (optional)
|
|
150
|
+
|
|
151
|
+
`mcpsec triage` runs the same scan, then explains each finding and proposes a fix with a
|
|
152
|
+
**local** model (Ollama). It is advisory: findings, levels and the exit code are never
|
|
153
|
+
changed by the model ([ADR-0011](docs/adr/0011-llm-triage-advisory.md)).
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
uv sync --extra agent # LangGraph + Langfuse
|
|
157
|
+
ollama pull qwen2.5-coder:3b
|
|
158
|
+
uv run mcpsec triage . --exclude tests/fixtures # text report
|
|
159
|
+
uv run mcpsec triage . --format sarif --output triage.sarif # SARIF + properties.triage
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
How it works: a LangGraph graph fans out one branch per finding (`Send`); each branch loads
|
|
163
|
+
the file excerpt, asks the model for `explanation`, `fix` and `evidence`, and keeps the answer
|
|
164
|
+
only if the evidence line really exists in the file (one stricter retry, then a rule-based
|
|
165
|
+
explanation). The excerpt is passed as untrusted data, so instructions hidden in a scanned
|
|
166
|
+
skill cannot change the result. Set `LANGFUSE_BASE_URL`, `LANGFUSE_PUBLIC_KEY` and
|
|
167
|
+
`LANGFUSE_SECRET_KEY` to trace every node, model call (tokens) and `grounded` score in a
|
|
168
|
+
self-hosted Langfuse.
|
|
169
|
+
|
|
170
|
+
## CI integration
|
|
171
|
+
|
|
172
|
+
The scanner is deterministic and offline, so it fits a CI gate. This repository
|
|
173
|
+
ships its own GitHub Actions workflow —
|
|
174
|
+
[`.github/workflows/ci.yml`](.github/workflows/ci.yml) — which lints (ruff), runs the
|
|
175
|
+
test suite, **scans this repository with mcpsec itself** and uploads the SARIF to the
|
|
176
|
+
Security tab, and runs a gitleaks secrets scan on every push and pull request.
|
|
177
|
+
|
|
178
|
+
To gate another project on `mcpsec` (actions pinned by commit SHA, as in this repo):
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
jobs:
|
|
182
|
+
mcpsec:
|
|
183
|
+
runs-on: ubuntu-latest
|
|
184
|
+
permissions:
|
|
185
|
+
contents: read
|
|
186
|
+
security-events: write # required to upload SARIF
|
|
187
|
+
steps:
|
|
188
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
189
|
+
with:
|
|
190
|
+
persist-credentials: false
|
|
191
|
+
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
192
|
+
- name: Scan for risky MCP configs and skills
|
|
193
|
+
run: uvx --from git+https://github.com/A-Djihadi/mcp-security-scanner mcpsec scan . --output results.sarif
|
|
194
|
+
- name: Upload SARIF
|
|
195
|
+
if: always()
|
|
196
|
+
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
|
197
|
+
with:
|
|
198
|
+
sarif_file: results.sarif
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`uvx --from git+…` installs mcpsec in an isolated environment (pin it with `@<tag or sha>`
|
|
202
|
+
in real use); `uv run mcpsec` only works inside this repository.
|
|
203
|
+
|
|
204
|
+
The default `--fail-on error` makes `mcpsec` exit non-zero when an error-level
|
|
205
|
+
finding exists, which blocks the build.
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
[MIT](LICENSE) © 2026 Djihadi Ahamdy.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# mcp-security-scanner
|
|
2
|
+
|
|
3
|
+
[](https://github.com/A-Djihadi/mcp-security-scanner/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Static security scanner for MCP servers and AI agent skills.
|
|
6
|
+
|
|
7
|
+
Detects excessive permissions (unrestricted shell, unsanitized SQL), risky
|
|
8
|
+
dependencies, and malicious patterns. Emits SARIF v2.1.0 for CI integration.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Requires Python 3.13.
|
|
13
|
+
|
|
14
|
+
Install the published package from PyPI:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install mcp-skill-scanner
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Opt into semantic detection with the `[semantic]` extra:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install mcp-skill-scanner[semantic]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or install from source with [`uv`](https://docs.astral.sh/uv/):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/A-Djihadi/mcp-security-scanner.git
|
|
30
|
+
cd mcp-security-scanner
|
|
31
|
+
uv sync
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Scan a directory, write SARIF to a file (default format)
|
|
38
|
+
uv run mcpsec scan . --output results.sarif
|
|
39
|
+
|
|
40
|
+
# Human-readable output
|
|
41
|
+
uv run mcpsec scan . --format text
|
|
42
|
+
|
|
43
|
+
# Only error-level findings
|
|
44
|
+
uv run mcpsec scan . --severity error
|
|
45
|
+
|
|
46
|
+
# Only specific rules
|
|
47
|
+
uv run mcpsec scan . --rules MCP-shell-unrestricted,DEP-unpinned
|
|
48
|
+
|
|
49
|
+
# Exclude a rule
|
|
50
|
+
uv run mcpsec scan . --exclude-rule SKL-shell-execution
|
|
51
|
+
|
|
52
|
+
# Skip paths (globs, repeatable), e.g. to scan a repo that contains test fixtures
|
|
53
|
+
uv run mcpsec scan . --exclude tests/fixtures
|
|
54
|
+
|
|
55
|
+
# Network-backed rules (OSV.dev, PyPI/npm, reachability) -- opt-in
|
|
56
|
+
uv run mcpsec scan . --online
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Options
|
|
60
|
+
|
|
61
|
+
| Flag | Description |
|
|
62
|
+
|------|-------------|
|
|
63
|
+
| `--format sarif\|text` | Output format (default `sarif`). |
|
|
64
|
+
| `--output FILE` | Write output to `FILE` instead of stdout. |
|
|
65
|
+
| `--severity error\|warning\|note` | Only report findings at/above this severity. |
|
|
66
|
+
| `--rules IDS` | Comma-separated rule IDs to run; all others are disabled. |
|
|
67
|
+
| `--exclude-rule IDS` | Comma-separated rule IDs to disable. |
|
|
68
|
+
| `--exclude GLOB` | Skip paths matching `GLOB` (relative to the scan root, matches parent directories too; repeatable). |
|
|
69
|
+
| `--online` | Enable network-backed rules (`DEP-known-vulnerability`, `DEP-outdated`, `DEP-unknown`, `MCP-unreachable`). Only `http(s)` is fetched; private, loopback and link-local addresses are never probed. |
|
|
70
|
+
| `--fail-on error\|warning\|note` | Minimum severity that fails the scan (default `error`). |
|
|
71
|
+
| `--semantic` | Enable semantic malicious-intent detection via the local embedding model (requires the `[semantic]` extra). |
|
|
72
|
+
|
|
73
|
+
### Semantic detection (optional)
|
|
74
|
+
|
|
75
|
+
`--semantic` opts into embedding-based malicious-intent detection for skills
|
|
76
|
+
(`SKL-semantic-malicious`). It is **off by default**: the base install stays
|
|
77
|
+
PyYAML-only, and a default scan never loads the model runtime.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Opt into the heavier install once:
|
|
81
|
+
uv sync --extra semantic # or: pip install mcp-skill-scanner[semantic]
|
|
82
|
+
|
|
83
|
+
# Then scan with semantic detection enabled:
|
|
84
|
+
uv run mcpsec scan . --semantic
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The default embedder is a small, CPU-only ONNX sentence-transformer:
|
|
88
|
+
|
|
89
|
+
- **Model:** `all-MiniLM-L6-v2` (ONNX export `Xenova/all-MiniLM-L6-v2`), Apache-2.0.
|
|
90
|
+
- **Pinned revision:** `751bff37182d3f1213fa05d7196b954e230abad9` (files verified by SHA-256).
|
|
91
|
+
- **Corpus:** a curated, versioned set of malicious-intent anchors
|
|
92
|
+
(`src/mcpsec/corpus.py`, version `1`); the model revision and corpus version
|
|
93
|
+
are recorded on every `SKL-semantic-malicious` finding for reproducibility.
|
|
94
|
+
|
|
95
|
+
**To verify:** the similarity threshold (default `0.85`) and the corpus anchor
|
|
96
|
+
coverage are calibrated against synthetic probes, not a production corpus; they
|
|
97
|
+
should be re-tuned against real-world skill data before relying on this rule as
|
|
98
|
+
a CI gate.
|
|
99
|
+
|
|
100
|
+
### Exit codes
|
|
101
|
+
|
|
102
|
+
| Code | Meaning |
|
|
103
|
+
|------|---------|
|
|
104
|
+
| 0 | Scan completed; no findings at or above `--fail-on`. |
|
|
105
|
+
| 1 | Scan completed; one or more findings at or above `--fail-on`. |
|
|
106
|
+
| 2 | Internal error (bad args, unreadable path, parse error). |
|
|
107
|
+
|
|
108
|
+
## Rules
|
|
109
|
+
|
|
110
|
+
| Rule ID | Level | Detects |
|
|
111
|
+
|---------|-------|---------|
|
|
112
|
+
| `MCP-shell-unrestricted` | error | Server `command` is a shell interpreter (`sh`, `bash`, `zsh`, `dash`, `ksh`, `fish`, `csh`, `powershell`, `pwsh`, `cmd`…), including behind a launcher (`env bash`, `sudo sh`). |
|
|
113
|
+
| `MCP-allow-all` | error | `args` contains `--allow-all` / `--allow-everything` / `-A`. |
|
|
114
|
+
| `MCP-broad-network` | error | `--allow-net` with no host restriction (bare flag or empty value). |
|
|
115
|
+
| `MCP-inline-code` | warning | Interpreter runs code passed on the command line (`python -c`, `node -e`, `ruby -e`, `perl -e`, `php -r`). |
|
|
116
|
+
| `MCP-insecure-transport` | error | Remote MCP server uses plaintext `http://`. |
|
|
117
|
+
| `MCP-header-secret` | warning | A secret is stored inline in a remote server's `headers`. |
|
|
118
|
+
| `MCP-remote-no-auth` | note | Remote MCP server has no auth-bearing header. |
|
|
119
|
+
| `MCP-parse-ambiguous` | note | Server command cannot be statically resolved from the config. |
|
|
120
|
+
| `MCP-unreachable` | note | Remote endpoint refused the connection (`--online`). |
|
|
121
|
+
| `FLOW-shell-injection` | error | Tainted MCP tool input reaches a shell execution sink (Python taint analysis). |
|
|
122
|
+
| `FLOW-sql-injection` | error | Tainted input reaches a SQL query sink. |
|
|
123
|
+
| `FLOW-code-eval` | warning | Tainted input reaches `eval` / `exec` / `compile`. |
|
|
124
|
+
| `MCP-broad-filesystem` | warning | `--allow-read` / `--allow-write` at a root-level path (`/`, `~`, `C:\`, `C:/`) or with no path. |
|
|
125
|
+
| `SKL-shell-execution` | warning | Skill body invokes a shell with interpolated input (f-string / concatenation). |
|
|
126
|
+
| `SKL-remote-code-exec` | error | Skill body pipes remote content to a shell (`curl … \| sh`, `wget … \| bash`, `eval(fetch(…))`). |
|
|
127
|
+
| `SKL-obfuscated-command` | warning | Shell command decodes a `base64` payload (`base64 -d` / `--decode`). |
|
|
128
|
+
| `SKL-sql-concatenation` | error | SQL built by string concatenation/interpolation; parameterized `?` queries do not trigger. |
|
|
129
|
+
| `DEP-unpinned` | warning | A direct dependency whose version spec is unpinned (`*`, `latest`, `^`, `~`, empty). |
|
|
130
|
+
| `DEP-known-vulnerability` | error | Declared version has a known vulnerability in OSV.dev (`--online`). |
|
|
131
|
+
| `DEP-outdated` | warning | Declared version is behind the latest published one (`--online`). |
|
|
132
|
+
| `DEP-unknown` | warning | Name closely resembles a popular package, a typosquat signal (`--online`). |
|
|
133
|
+
| `SKL-semantic-malicious` | warning | Skill prose is semantically similar to a known-malicious reference (opt-in via `--semantic`). |
|
|
134
|
+
|
|
135
|
+
## LLM triage (optional)
|
|
136
|
+
|
|
137
|
+
`mcpsec triage` runs the same scan, then explains each finding and proposes a fix with a
|
|
138
|
+
**local** model (Ollama). It is advisory: findings, levels and the exit code are never
|
|
139
|
+
changed by the model ([ADR-0011](docs/adr/0011-llm-triage-advisory.md)).
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
uv sync --extra agent # LangGraph + Langfuse
|
|
143
|
+
ollama pull qwen2.5-coder:3b
|
|
144
|
+
uv run mcpsec triage . --exclude tests/fixtures # text report
|
|
145
|
+
uv run mcpsec triage . --format sarif --output triage.sarif # SARIF + properties.triage
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
How it works: a LangGraph graph fans out one branch per finding (`Send`); each branch loads
|
|
149
|
+
the file excerpt, asks the model for `explanation`, `fix` and `evidence`, and keeps the answer
|
|
150
|
+
only if the evidence line really exists in the file (one stricter retry, then a rule-based
|
|
151
|
+
explanation). The excerpt is passed as untrusted data, so instructions hidden in a scanned
|
|
152
|
+
skill cannot change the result. Set `LANGFUSE_BASE_URL`, `LANGFUSE_PUBLIC_KEY` and
|
|
153
|
+
`LANGFUSE_SECRET_KEY` to trace every node, model call (tokens) and `grounded` score in a
|
|
154
|
+
self-hosted Langfuse.
|
|
155
|
+
|
|
156
|
+
## CI integration
|
|
157
|
+
|
|
158
|
+
The scanner is deterministic and offline, so it fits a CI gate. This repository
|
|
159
|
+
ships its own GitHub Actions workflow —
|
|
160
|
+
[`.github/workflows/ci.yml`](.github/workflows/ci.yml) — which lints (ruff), runs the
|
|
161
|
+
test suite, **scans this repository with mcpsec itself** and uploads the SARIF to the
|
|
162
|
+
Security tab, and runs a gitleaks secrets scan on every push and pull request.
|
|
163
|
+
|
|
164
|
+
To gate another project on `mcpsec` (actions pinned by commit SHA, as in this repo):
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
jobs:
|
|
168
|
+
mcpsec:
|
|
169
|
+
runs-on: ubuntu-latest
|
|
170
|
+
permissions:
|
|
171
|
+
contents: read
|
|
172
|
+
security-events: write # required to upload SARIF
|
|
173
|
+
steps:
|
|
174
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
175
|
+
with:
|
|
176
|
+
persist-credentials: false
|
|
177
|
+
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
|
|
178
|
+
- name: Scan for risky MCP configs and skills
|
|
179
|
+
run: uvx --from git+https://github.com/A-Djihadi/mcp-security-scanner mcpsec scan . --output results.sarif
|
|
180
|
+
- name: Upload SARIF
|
|
181
|
+
if: always()
|
|
182
|
+
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
|
183
|
+
with:
|
|
184
|
+
sarif_file: results.sarif
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`uvx --from git+…` installs mcpsec in an isolated environment (pin it with `@<tag or sha>`
|
|
188
|
+
in real use); `uv run mcpsec` only works inside this repository.
|
|
189
|
+
|
|
190
|
+
The default `--fail-on error` makes `mcpsec` exit non-zero when an error-level
|
|
191
|
+
finding exists, which blocks the build.
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
[MIT](LICENSE) © 2026 Djihadi Ahamdy.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcp-skill-scanner"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Static security scanner for MCP servers and AI agent skills."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = ["pyyaml>=6.0.3"]
|
|
8
|
+
|
|
9
|
+
[project.scripts]
|
|
10
|
+
mcpsec = "mcpsec.cli:main"
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
semantic = [
|
|
14
|
+
"onnxruntime==1.31.1",
|
|
15
|
+
"tokenizers==0.23.3",
|
|
16
|
+
]
|
|
17
|
+
agent = [
|
|
18
|
+
"langgraph>=1.2,<2",
|
|
19
|
+
"langfuse>=5.1,<6",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["uv_build>=0.13.6,<0.14.0"]
|
|
24
|
+
build-backend = "uv_build"
|
|
25
|
+
|
|
26
|
+
[tool.uv.build-backend]
|
|
27
|
+
module-name = "mcpsec"
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
target-version = "py313"
|
|
31
|
+
extend-exclude = ["tests/fixtures"]
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=9.2.0",
|
|
36
|
+
"ruff==0.17.3",
|
|
37
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcp-skill-scanner"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Static security scanner for MCP servers and AI agent skills."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"pyyaml>=6.0.3",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[project.scripts]
|
|
12
|
+
mcpsec = "mcpsec.cli:main"
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
semantic = [
|
|
16
|
+
"onnxruntime==1.31.1",
|
|
17
|
+
"tokenizers==0.23.3",
|
|
18
|
+
]
|
|
19
|
+
# LLM triage (ADR-0011): LangGraph orchestration + Langfuse tracing.
|
|
20
|
+
agent = [
|
|
21
|
+
"langgraph>=1.2,<2",
|
|
22
|
+
"langfuse>=5.1,<6",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["uv_build>=0.13.6,<0.14.0"]
|
|
27
|
+
build-backend = "uv_build"
|
|
28
|
+
|
|
29
|
+
[tool.uv.build-backend]
|
|
30
|
+
module-name = "mcpsec"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=9.2.0",
|
|
35
|
+
"ruff==0.17.3",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
target-version = "py313"
|
|
40
|
+
# Fixtures are deliberately vulnerable or partial code: their line/column
|
|
41
|
+
# positions are asserted by tests, so they are never reformatted or linted.
|
|
42
|
+
extend-exclude = ["tests/fixtures"]
|