@mayhemx87/mcpscan 0.3.1 → 0.5.0
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.
- package/README.md +7 -3
- package/dist/discovery.js +5 -0
- package/dist/engine.js +22 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ the exact line of the offending server definition.
|
|
|
91
91
|
# .pre-commit-config.yaml
|
|
92
92
|
repos:
|
|
93
93
|
- repo: https://github.com/mayhemx87/mcpscan
|
|
94
|
-
rev: v0.
|
|
94
|
+
rev: v0.5.0
|
|
95
95
|
hooks:
|
|
96
96
|
- id: mcpscan
|
|
97
97
|
```
|
|
@@ -115,11 +115,15 @@ Dedicated MCP config files and editor settings files that embed MCP servers:
|
|
|
115
115
|
| `.amazonq/mcp.json` | Amazon Q |
|
|
116
116
|
| `.roo/mcp.json` | Roo Code |
|
|
117
117
|
| `.kiro/settings/mcp.json` | Kiro |
|
|
118
|
+
| `.trae/mcp.json` | Trae |
|
|
118
119
|
| `.claude/settings.json`, `.claude/settings.local.json` | Claude Code |
|
|
119
120
|
| `.gemini/settings.json` | Gemini CLI |
|
|
121
|
+
| `.zed/settings.json` (`context_servers`) | Zed |
|
|
122
|
+
| `.junie/mcp/mcp.json` | Junie |
|
|
120
123
|
|
|
121
|
-
Both `mcpServers` and VS Code's `servers` keys are understood
|
|
122
|
-
`
|
|
124
|
+
Both `mcpServers` and VS Code's `servers` keys are understood, as is Zed's
|
|
125
|
+
`context_servers` key; `command` and `args` are analyzed together, including
|
|
126
|
+
Zed's legacy nested command format; remote (`url`-based) servers are covered. Nested
|
|
123
127
|
occurrences of these paths are scanned too (any subdirectory can be opened as a
|
|
124
128
|
workspace root). Only `.git/` and `node_modules/` are skipped; `.gitignore` is
|
|
125
129
|
deliberately ignored — an injected config would be exactly the file that's
|
package/dist/discovery.js
CHANGED
|
@@ -11,6 +11,10 @@ const SKIP_DIRS = new Set(['.git', 'node_modules']);
|
|
|
11
11
|
*/
|
|
12
12
|
const MCP_CONFIG_SUFFIXES = [
|
|
13
13
|
'.mcp.json',
|
|
14
|
+
// A bare `mcp.json` in any directory. Besides Claude Code's own file this is
|
|
15
|
+
// the exact path several clients auto-load from their own dot-directory --
|
|
16
|
+
// Trae (`.trae/mcp.json`) among them -- so those need no entry of their own.
|
|
17
|
+
// Keep new clients out of this list unless their filename differs.
|
|
14
18
|
'mcp.json',
|
|
15
19
|
'.amazonq/mcp.json',
|
|
16
20
|
'.cursor/mcp.json',
|
|
@@ -24,6 +28,7 @@ const MCP_CONFIG_SUFFIXES = [
|
|
|
24
28
|
'.claude/settings.json',
|
|
25
29
|
'.claude/settings.local.json',
|
|
26
30
|
'.gemini/settings.json',
|
|
31
|
+
'.zed/settings.json',
|
|
27
32
|
];
|
|
28
33
|
function isMcpConfigPath(relPath) {
|
|
29
34
|
const p = relPath.replace(/\\/g, '/');
|
package/dist/engine.js
CHANGED
|
@@ -53,6 +53,8 @@ function looksLikeLiteralSecret(value) {
|
|
|
53
53
|
return false;
|
|
54
54
|
return SECRET_VALUE_PATTERNS.some(p => p.test(value));
|
|
55
55
|
}
|
|
56
|
+
const REPO_ESCAPE_PATH = /^(?:\/|[A-Za-z]:[\\/]|\\\\|\.\.[\\/])/;
|
|
57
|
+
const PATH_TRAVERSAL_IN_LINE = /(?:^|[\s;|&])\.\.[\\/]/;
|
|
56
58
|
/**
|
|
57
59
|
* Returns true if the command line contains npx/uvx/pipx with an unversioned
|
|
58
60
|
* package (handles compound commands like cmd1 && npx pkg). Runner flags
|
|
@@ -88,8 +90,9 @@ export function isEmbeddedHostFile(filePath) {
|
|
|
88
90
|
}
|
|
89
91
|
/** Extract the server map from any known config shape:
|
|
90
92
|
* - dedicated files: { mcpServers: {...} } or VS Code's { servers: {...} }
|
|
91
|
-
* - embedded hosts: { mcpServers: {...} } (Claude/Gemini settings)
|
|
92
|
-
* { mcp: { servers: {...} } } (VS Code settings.json)
|
|
93
|
+
* - embedded hosts: { mcpServers: {...} } (Claude/Gemini settings),
|
|
94
|
+
* { mcp: { servers: {...} } } (VS Code settings.json), or
|
|
95
|
+
* { context_servers: {...} } (Zed settings.json)
|
|
93
96
|
* Also returns the JSON path to the map so findings can carry line numbers. */
|
|
94
97
|
function extractServers(config) {
|
|
95
98
|
if (config.mcpServers && typeof config.mcpServers === 'object') {
|
|
@@ -102,6 +105,9 @@ function extractServers(config) {
|
|
|
102
105
|
if (mcp && typeof mcp === 'object' && mcp.servers && typeof mcp.servers === 'object') {
|
|
103
106
|
return { servers: mcp.servers, path: ['mcp', 'servers'] };
|
|
104
107
|
}
|
|
108
|
+
if (config.context_servers && typeof config.context_servers === 'object') {
|
|
109
|
+
return { servers: config.context_servers, path: ['context_servers'] };
|
|
110
|
+
}
|
|
105
111
|
return null;
|
|
106
112
|
}
|
|
107
113
|
/**
|
|
@@ -161,8 +167,18 @@ export function analyzeConfig(filePath, content, isGitTracked) {
|
|
|
161
167
|
// Analyze command AND args as one line -- the dominant real-world shape
|
|
162
168
|
// is {"command": "npx", "args": ["-y", "pkg"]}, invisible to any rule
|
|
163
169
|
// that inspects `command` alone (the v0.1.0 gap).
|
|
164
|
-
const
|
|
165
|
-
|
|
170
|
+
const command = typeof server.command === 'string' ? server.command : server.command?.path ?? '';
|
|
171
|
+
// Legacy Zed command.args and the top-level args are merged into one
|
|
172
|
+
// array -- MCP-004's absolute-path check scans `args` directly, so
|
|
173
|
+
// command.args must live there too or it silently evades detection.
|
|
174
|
+
const commandArgs = typeof server.command === 'object' && Array.isArray(server.command.args)
|
|
175
|
+
? server.command.args.filter(a => typeof a === 'string')
|
|
176
|
+
: [];
|
|
177
|
+
const args = [
|
|
178
|
+
...commandArgs,
|
|
179
|
+
...(Array.isArray(server.args) ? server.args.filter(a => typeof a === 'string') : []),
|
|
180
|
+
];
|
|
181
|
+
const line = [command, ...args].join(' ').trim();
|
|
166
182
|
const serverFindings = [];
|
|
167
183
|
// MCP-001: remote URL or network command execution
|
|
168
184
|
if (line && (/https?:\/\//.test(line) || /\b(curl|wget)\b/.test(line))) {
|
|
@@ -202,10 +218,8 @@ export function analyzeConfig(filePath, content, isGitTracked) {
|
|
|
202
218
|
}
|
|
203
219
|
}
|
|
204
220
|
// MCP-004: absolute path or path traversal outside repo
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
args.some(a => a.startsWith('/')) ||
|
|
208
|
-
/(?:^|[\s;|&])\.\.\//.test(line)) {
|
|
221
|
+
if ([command, ...args].some(value => REPO_ESCAPE_PATH.test(value.trimStart())) ||
|
|
222
|
+
PATH_TRAVERSAL_IN_LINE.test(line)) {
|
|
209
223
|
serverFindings.push({
|
|
210
224
|
file: filePath,
|
|
211
225
|
rule_id: 'MCP-004',
|