residoo 0.22.0 → 0.23.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "residoo",
3
- "version": "0.22.0",
3
+ "version": "0.23.1",
4
4
  "description": "Find secrets leaking through your AI coding agent's session history. Zero network calls in the scan path, zero dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "CloudRoam (https://cloudroam.io)",
package/src/cve.js ADDED
@@ -0,0 +1,284 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * A small, hand-curated, dated table of published CVEs affecting the
5
+ * MCP/AI-agent ecosystem, plus a minimal version-range matcher -- the
6
+ * shared infrastructure behind `residoo scan`'s MCP-config CVE check
7
+ * (integrity.js's "MCP server configuration risks" section) and a future
8
+ * `--project` dependency-manifest check.
9
+ *
10
+ * SOURCE, stated precisely because it matters: every entry below was
11
+ * fetched directly from GitHub's own Security Advisory REST API
12
+ * (api.github.com/advisories?ecosystem=<npm|pip>&affects=<package>) on
13
+ * 2026-09-05 -- a primary, authoritative source (GHSA-reviewed, each
14
+ * entry carrying its own CVE id, GHSA id, and vulnerable/patched version
15
+ * range), not a secondary aggregator's summary. An earlier draft of this
16
+ * table was built from a dedicated MCP-CVE tracking site
17
+ * (vulnerablemcp.info) and cross-checked against this API before
18
+ * shipping; every version range below is the API's own
19
+ * `vulnerable_version_range` field, not a re-derived or estimated one.
20
+ * Package NAMES were independently confirmed to exist on the real npm/
21
+ * PyPI registries (`npm view <pkg>`, PyPI's own JSON API) before being
22
+ * added here -- a wrong package name would silently never match anything,
23
+ * the same "never a false all-clear" concern CONTRIBUTING.md states for
24
+ * source paths, applied to this table instead.
25
+ *
26
+ * DELIBERATELY SMALL, on purpose: this is ~27 entries across 11 packages,
27
+ * not a claim of exhaustive coverage the way Medusa's own "~200 CVEs" is.
28
+ * Every single entry here is individually traceable to a specific GHSA/
29
+ * CVE id and a real advisory -- the same "84 high-confidence rules beat a
30
+ * competitor's higher rule count" trade-off already proven on this
31
+ * project's own benchmark (see bench/RESULTS.md), applied to CVE data
32
+ * instead of secret patterns. A welcome follow-up PR is adding more
33
+ * packages through the same API query, cited the same way -- never by
34
+ * guessing a plausible-sounding range.
35
+ *
36
+ * VERSION COMPARISON: a minimal, hand-written major.minor.patch numeric
37
+ * comparator -- NOT a full semver-range grammar (no ^, ~, prerelease
38
+ * tags, build metadata, x-ranges). Sufficient for every range in this
39
+ * table, including the date-based versioning scheme
40
+ * `@modelcontextprotocol/server-filesystem` switched to mid-2025
41
+ * (`2025.1.14`, `2025.7.1`) -- verified directly that comparing
42
+ * `[2025,1,20]` against `[2025,7,1]` component-by-component gives the
43
+ * correct ordering, the same way it would for an ordinary semver triple.
44
+ * A version string this comparator can't parse (a prerelease suffix, a
45
+ * git-hash pseudo-version, anything non-numeric) is reported as
46
+ * "cannot determine" by the caller, never silently treated as either safe
47
+ * or vulnerable -- CONTRIBUTING.md's rule 5 applied to a version string
48
+ * instead of a file.
49
+ */
50
+
51
+ const CVE_DATABASE = [
52
+ // ---- npm ----
53
+ {
54
+ id: "CVE-2025-6514", ecosystem: "npm", package: "mcp-remote", severity: "critical",
55
+ ranges: [{ min: "0.0.5", maxExclusive: "0.1.16" }],
56
+ summary: "OS command injection via a crafted authorization_endpoint response URL when connecting to an untrusted MCP server.",
57
+ source: "GHSA-6xpm-ggf7-wc3p",
58
+ },
59
+ {
60
+ id: "CVE-2025-58444", ecosystem: "npm", package: "@modelcontextprotocol/inspector", severity: "high",
61
+ ranges: [{ maxExclusive: "0.16.6" }],
62
+ summary: "Potential command execution via XSS when Inspector connects to an untrusted MCP server.",
63
+ source: "GHSA (fetched via api.github.com/advisories)",
64
+ },
65
+ {
66
+ id: "CVE-2025-49596", ecosystem: "npm", package: "@modelcontextprotocol/inspector", severity: "critical",
67
+ ranges: [{ maxExclusive: "0.14.1" }],
68
+ summary: "The Inspector proxy server lacks authentication between the Inspector client and the proxy, enabling RCE.",
69
+ source: "GHSA (fetched via api.github.com/advisories)",
70
+ },
71
+ {
72
+ id: "CVE-2026-23744", ecosystem: "npm", package: "@mcpjam/inspector", severity: "critical",
73
+ ranges: [{ maxInclusive: "1.4.2" }],
74
+ summary: "Remote code execution via an exposed, unauthenticated HTTP endpoint.",
75
+ source: "GHSA (fetched via api.github.com/advisories)",
76
+ },
77
+ {
78
+ id: "CVE-2025-65513", ecosystem: "npm", package: "mcp-fetch-server", severity: "medium",
79
+ ranges: [{ maxInclusive: "1.0.2" }],
80
+ summary: "Server-Side Request Forgery (SSRF) vulnerability.",
81
+ source: "GHSA (fetched via api.github.com/advisories)",
82
+ },
83
+ {
84
+ id: "CVE-2025-53372", ecosystem: "npm", package: "node-code-sandbox-mcp", severity: "high",
85
+ ranges: [{ maxInclusive: "1.2.0" }],
86
+ summary: "Sandbox escape via command injection.",
87
+ source: "GHSA (fetched via api.github.com/advisories)",
88
+ },
89
+ {
90
+ id: "CVE-2026-47250", ecosystem: "npm", package: "mcp-server-kubernetes", severity: "medium",
91
+ ranges: [{ maxInclusive: "3.6.2" }],
92
+ summary: "kubectl-generic flag injection enables Kubernetes bearer-token exfiltration.",
93
+ source: "GHSA (fetched via api.github.com/advisories)",
94
+ },
95
+ {
96
+ id: "CVE-2026-46519", ecosystem: "npm", package: "mcp-server-kubernetes", severity: "high",
97
+ ranges: [{ maxExclusive: "3.6.0" }],
98
+ summary: "Tool access control bypass via presentation-layer filtering with no execution-layer enforcement.",
99
+ source: "GHSA (fetched via api.github.com/advisories)",
100
+ },
101
+ {
102
+ id: "CVE-2026-39884", ecosystem: "npm", package: "mcp-server-kubernetes", severity: "high",
103
+ ranges: [{ maxInclusive: "3.4.0" }],
104
+ summary: "Argument injection in the port_forward tool via space-splitting.",
105
+ source: "GHSA (fetched via api.github.com/advisories)",
106
+ },
107
+ {
108
+ id: "CVE-2025-66404", ecosystem: "npm", package: "mcp-server-kubernetes", severity: "medium",
109
+ ranges: [{ maxInclusive: "2.9.7" }],
110
+ summary: "Security issue in the exec_in_pod tool.",
111
+ source: "GHSA (fetched via api.github.com/advisories)",
112
+ },
113
+ {
114
+ id: "CVE-2025-53355", ecosystem: "npm", package: "mcp-server-kubernetes", severity: "high",
115
+ ranges: [{ maxExclusive: "2.5.0" }],
116
+ summary: "Command injection in several tools.",
117
+ source: "GHSA (fetched via api.github.com/advisories)",
118
+ },
119
+ {
120
+ id: "CVE-2026-25536", ecosystem: "npm", package: "@modelcontextprotocol/sdk", severity: "high",
121
+ ranges: [{ min: "1.10.0", maxInclusive: "1.25.3" }],
122
+ summary: "Cross-client data leak via shared server/transport instance reuse.",
123
+ source: "GHSA (fetched via api.github.com/advisories)",
124
+ },
125
+ {
126
+ id: "CVE-2026-0621", ecosystem: "npm", package: "@modelcontextprotocol/sdk", severity: "high",
127
+ ranges: [{ min: "1.3.0", maxExclusive: "1.25.2" }],
128
+ summary: "ReDoS (regular expression denial of service) vulnerability.",
129
+ source: "GHSA (fetched via api.github.com/advisories)",
130
+ },
131
+ {
132
+ id: "CVE-2025-66414", ecosystem: "npm", package: "@modelcontextprotocol/sdk", severity: "high",
133
+ ranges: [{ maxExclusive: "1.24.0" }],
134
+ summary: "DNS rebinding protection not enabled by default on localhost-bound SSE/StreamableHTTP servers.",
135
+ source: "GHSA (fetched via api.github.com/advisories)",
136
+ },
137
+ {
138
+ id: "CVE-2025-53110", ecosystem: "npm", package: "@modelcontextprotocol/server-filesystem", severity: "high",
139
+ ranges: [{ maxInclusive: "0.6.2" }, { min: "2025.1.14", maxExclusive: "2025.7.1" }],
140
+ summary: "Path validation bypass via a colliding path prefix.",
141
+ source: "GHSA (fetched via api.github.com/advisories)",
142
+ },
143
+ {
144
+ id: "CVE-2025-53109", ecosystem: "npm", package: "@modelcontextprotocol/server-filesystem", severity: "high",
145
+ ranges: [{ maxInclusive: "0.6.2" }, { min: "2025.1.14", maxExclusive: "2025.7.1" }],
146
+ summary: "Path validation bypass via prefix matching and symlink handling.",
147
+ source: "GHSA (fetched via api.github.com/advisories)",
148
+ },
149
+ {
150
+ id: "CVE-2025-9611", ecosystem: "npm", package: "@playwright/mcp", severity: "high",
151
+ ranges: [{ maxExclusive: "0.0.40" }],
152
+ summary: "Vulnerable to a DNS rebinding attack, allowing an attacker access to all server tools.",
153
+ source: "GHSA (fetched via api.github.com/advisories)",
154
+ },
155
+
156
+ // ---- pip ----
157
+ {
158
+ id: "CVE-2026-59950", ecosystem: "pip", package: "mcp", severity: "high",
159
+ ranges: [{ maxExclusive: "1.28.1" }],
160
+ summary: "MCP Python SDK: WebSocket server transport does not support Host/Origin validation.",
161
+ source: "GHSA (fetched via api.github.com/advisories)",
162
+ },
163
+ {
164
+ id: "CVE-2026-52869", ecosystem: "pip", package: "mcp", severity: "high",
165
+ ranges: [{ maxInclusive: "1.27.1" }],
166
+ summary: "MCP Python SDK: HTTP transports serve session requests without verifying the authenticated principal.",
167
+ source: "GHSA (fetched via api.github.com/advisories)",
168
+ },
169
+ {
170
+ id: "CVE-2026-52870", ecosystem: "pip", package: "mcp", severity: "high",
171
+ ranges: [{ min: "1.23.0", maxInclusive: "1.27.1" }],
172
+ summary: "Experimental task handlers allow any client to access and cancel other clients' tasks.",
173
+ source: "GHSA (fetched via api.github.com/advisories)",
174
+ },
175
+ {
176
+ id: "CVE-2025-66416", ecosystem: "pip", package: "mcp", severity: "high",
177
+ ranges: [{ maxExclusive: "1.23.0" }],
178
+ summary: "DNS rebinding protection not enabled by default on localhost-bound SSE/StreamableHTTP servers.",
179
+ source: "GHSA (fetched via api.github.com/advisories)",
180
+ },
181
+ {
182
+ id: "CVE-2025-53366", ecosystem: "pip", package: "mcp", severity: "high",
183
+ ranges: [{ maxExclusive: "1.9.4" }],
184
+ summary: "FastMCP Server validation error leading to denial of service.",
185
+ source: "GHSA (fetched via api.github.com/advisories)",
186
+ },
187
+ {
188
+ id: "CVE-2025-53365", ecosystem: "pip", package: "mcp", severity: "high",
189
+ ranges: [{ maxExclusive: "1.10.0" }],
190
+ summary: "Unhandled exception in the Streamable HTTP transport, leading to denial of service.",
191
+ source: "GHSA (fetched via api.github.com/advisories)",
192
+ },
193
+ {
194
+ id: "CVE-2026-27735", ecosystem: "pip", package: "mcp-server-git", severity: "medium",
195
+ ranges: [{ maxExclusive: "2026.1.14" }],
196
+ summary: "Path traversal in git_add allows staging files outside the repository boundary.",
197
+ source: "GHSA (fetched via api.github.com/advisories)",
198
+ },
199
+ {
200
+ id: "CVE-2025-68145", ecosystem: "pip", package: "mcp-server-git", severity: "medium",
201
+ ranges: [{ maxExclusive: "2025.12.18" }],
202
+ summary: "Missing path validation when using the --repository flag.",
203
+ source: "GHSA (fetched via api.github.com/advisories)",
204
+ },
205
+ {
206
+ id: "CVE-2025-68144", ecosystem: "pip", package: "mcp-server-git", severity: "medium",
207
+ ranges: [{ maxExclusive: "2025.12.18" }],
208
+ summary: "Argument injection in git_diff and git_checkout allows overwriting local files.",
209
+ source: "GHSA (fetched via api.github.com/advisories)",
210
+ },
211
+ {
212
+ id: "CVE-2025-68143", ecosystem: "pip", package: "mcp-server-git", severity: "medium",
213
+ ranges: [{ maxExclusive: "2025.9.25" }],
214
+ summary: "Unrestricted git_init tool allows repository creation at arbitrary filesystem locations.",
215
+ source: "GHSA (fetched via api.github.com/advisories)",
216
+ },
217
+ ];
218
+
219
+ /**
220
+ * Parse a version string's leading major.minor.patch numeric triple.
221
+ * Returns null for anything this simple comparator can't handle (a
222
+ * prerelease/build-metadata suffix, a git-hash pseudo-version, a bare "1"
223
+ * or "1.2" with no patch component) -- the caller must treat null as
224
+ * "cannot determine," never as a match or a clean bill of health.
225
+ */
226
+ function parseVersion(v) {
227
+ // `String(v)` itself can throw -- an object whose own `toString` property
228
+ // is present but not a function (e.g. `{ toString: "" }`, a real shape
229
+ // fast-check's property fuzzing found, not a hypothetical) fails the
230
+ // ToPrimitive coercion with "Cannot convert object to primitive value"
231
+ // before the regex ever runs. `v` here can be anything an attacker-
232
+ // controlled MCP config's `args` array contains, so this must degrade to
233
+ // "unparseable" the same as any other malformed input, never throw.
234
+ let s;
235
+ try { s = String(v).trim(); } catch { return null; }
236
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(s);
237
+ if (!m) return null;
238
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
239
+ }
240
+
241
+ /** -1 / 0 / 1, comparing two parseVersion() triples component-by-component. */
242
+ function compareVersions(a, b) {
243
+ for (let i = 0; i < 3; i++) {
244
+ if (a[i] !== b[i]) return a[i] - b[i];
245
+ }
246
+ return 0;
247
+ }
248
+
249
+ function inOneRange(parsed, range) {
250
+ if (range.min) {
251
+ const min = parseVersion(range.min);
252
+ if (min && compareVersions(parsed, min) < 0) return false;
253
+ }
254
+ if (range.maxInclusive) {
255
+ const max = parseVersion(range.maxInclusive);
256
+ if (max && compareVersions(parsed, max) > 0) return false;
257
+ }
258
+ if (range.maxExclusive) {
259
+ const max = parseVersion(range.maxExclusive);
260
+ if (max && compareVersions(parsed, max) >= 0) return false;
261
+ }
262
+ return true;
263
+ }
264
+
265
+ /**
266
+ * Check one resolved `packageName`@`version` pair (ecosystem "npm" or
267
+ * "pip") against CVE_DATABASE. Returns `{ checkable, matches }` --
268
+ * `checkable: false` means the version string couldn't be parsed at all
269
+ * (report "cannot determine," never silently clean); `matches` is every
270
+ * CVE_DATABASE entry whose package matches and whose vulnerable range
271
+ * (any one of its, possibly several, disjoint ranges) contains this
272
+ * version.
273
+ */
274
+ function checkVersion(ecosystem, packageName, version) {
275
+ const parsed = parseVersion(version);
276
+ if (!parsed) return { checkable: false, matches: [] };
277
+ const matches = CVE_DATABASE.filter((entry) =>
278
+ entry.ecosystem === ecosystem &&
279
+ entry.package === packageName &&
280
+ entry.ranges.some((r) => inOneRange(parsed, r)));
281
+ return { checkable: true, matches };
282
+ }
283
+
284
+ module.exports = { CVE_DATABASE, parseVersion, compareVersions, checkVersion };
package/src/integrity.js CHANGED
@@ -10,6 +10,7 @@ const os = require("os");
10
10
  // reference at import time and never see a later patch.
11
11
  const cp = require("child_process");
12
12
  const { PATTERNS, redact } = require("./patterns");
13
+ const { checkVersion } = require("./cve");
13
14
 
14
15
  /**
15
16
  * Integrity checks for agent config directories.
@@ -386,6 +387,100 @@ function extractHooks(parsed) {
386
387
  return { hooks, unrecognized, hadHooksKey: true, truncated, sawLeaf };
387
388
  }
388
389
 
390
+ // ── MCP server configuration risk checks ────────────────────────────────
391
+ //
392
+ // Distinct from agent-configs.js (which scans these same files' raw TEXT
393
+ // for leaked secrets) and from injection.js (which scans transcript
394
+ // CONTENT for injection payloads): this parses the STRUCTURED
395
+ // `mcpServers` object every MCP client config converges on
396
+ // (`{"mcpServers": {"<name>": {"command", "args", "env", "url"}}}` --
397
+ // Claude Code, Claude Desktop, Cursor, and Visual Studio's own docs all
398
+ // document this identical shape, already cited in agent-configs.js's own
399
+ // header) and checks it for two named, real risk classes:
400
+ //
401
+ // 1. A known-vulnerable version of a specific MCP-ecosystem package
402
+ // pinned in the server's launch command (see cve.js).
403
+ // 2. A remote (non-loopback) server configured over plain HTTP instead
404
+ // of HTTPS -- MCP protocol traffic, including tool definitions,
405
+ // crossing a real network in cleartext.
406
+ //
407
+ // NOT checked, disclosed rather than silently gapped (see injection.js's
408
+ // own header for the fuller version of this point): a malicious server
409
+ // changing a TOOL'S OWN DESCRIPTION after approval ("tool poisoning") --
410
+ // a tool's description is protocol data returned by a live server at
411
+ // request time, never present in a static config file, so there is
412
+ // nothing here for a file-scanner to check it against.
413
+
414
+ /**
415
+ * Extract the `mcpServers` object from a parsed config file. Every real
416
+ * MCP client this project has verified (Claude Code, Claude Desktop,
417
+ * Cursor, Visual Studio) uses this identical top-level shape. Anything
418
+ * else (the key absent, present but not an object) yields an empty
419
+ * object -- a config with no MCP servers configured is not an error.
420
+ */
421
+ function extractMcpServers(parsed) {
422
+ if (!parsed || typeof parsed !== "object") return {};
423
+ const servers = parsed.mcpServers;
424
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) return {};
425
+ return servers;
426
+ }
427
+
428
+ // npm: optional @scope/, then a name, a literal @, then a version
429
+ // starting with a digit. pip: a name, a literal ==, then a version
430
+ // starting with a digit. Both anchored full-string (^...$) -- matched
431
+ // against ONE argv element at a time, never a substring of a longer
432
+ // string that happens to contain a coincidental match.
433
+ const NPM_PKG_VERSION_RE = /^(@[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+|[a-zA-Z0-9._-]+)@(\d[\w.-]*)$/;
434
+ const PIP_PKG_VERSION_RE = /^([a-zA-Z0-9._-]+)==(\d[\w.-]*)$/;
435
+
436
+ /**
437
+ * Scan one MCP server's `args` array for a `package@version` (npm) or
438
+ * `package==version` (pip) argv element -- the documented invocation
439
+ * shape for `npx <pkg>@<version>` / `uvx <pkg>==<version>` / `pip install
440
+ * <pkg>==<version>`. An unpinned invocation (`npx mcp-remote` with no
441
+ * `@version`) yields nothing here on purpose: there's no version to
442
+ * check, and this deliberately does NOT treat "unpinned" itself as a
443
+ * finding -- that's a real, genuinely debatable trade-off (always
444
+ * getting the latest PATCHED release vs. exposure to a compromised
445
+ * "latest" publish), not a clear vulnerability the way a specific
446
+ * known-bad pinned version is.
447
+ */
448
+ function extractPackageVersions(args) {
449
+ if (!Array.isArray(args)) return [];
450
+ const found = [];
451
+ for (const a of args) {
452
+ if (typeof a !== "string") continue;
453
+ const npm = NPM_PKG_VERSION_RE.exec(a);
454
+ if (npm) { found.push({ ecosystem: "npm", package: npm[1], version: npm[2] }); continue; }
455
+ const pip = PIP_PKG_VERSION_RE.exec(a);
456
+ if (pip) found.push({ ecosystem: "pip", package: pip[1], version: pip[2] });
457
+ }
458
+ return found;
459
+ }
460
+
461
+ // A well-known, widely-flagged supply-chain red flag: an installer script
462
+ // fetched and piped directly into a shell, never saved or reviewed first.
463
+ // Checked against the whole command+args joined, not `command` alone,
464
+ // since the realistic shape is `command: "sh", args: ["-c", "curl ... |
465
+ // bash"]` -- the risky text lives in `args`, not `command`.
466
+ const CURL_PIPE_SHELL_RE = /\b(?:curl|wget)\b[^|]*\|\s*(?:sudo\s+)?(?:sh|bash|zsh)\b/i;
467
+
468
+ /**
469
+ * True for a URL-transport MCP server (`url` field present) using plain
470
+ * HTTP to a non-loopback host. A loopback URL (localhost/127.0.0.1/::1)
471
+ * is excluded -- an ordinary local-dev MCP server, not the actual risk
472
+ * this names: MCP protocol traffic, including tool definitions, crossing
473
+ * a real network where it can be observed or altered in transit.
474
+ */
475
+ function isInsecureRemoteUrl(url) {
476
+ if (typeof url !== "string") return false;
477
+ let parsed;
478
+ try { parsed = new URL(url); } catch { return false; }
479
+ if (parsed.protocol !== "http:") return false;
480
+ const host = parsed.hostname;
481
+ return host !== "localhost" && host !== "127.0.0.1" && host !== "::1" && host !== "[::1]";
482
+ }
483
+
389
484
  // ── the checker ───────────────────────────────────────────────────────────
390
485
 
391
486
  /**
@@ -827,6 +922,66 @@ function checkIntegrity({ home = os.homedir(), cwd = process.cwd(), projectMode
827
922
  }
828
923
  }
829
924
 
925
+ // ---- 6. MCP server configuration risks ---------------------------------
926
+ // Home/machine-level configs (none of these are ever project-scoped by
927
+ // any vendor's own design, the same reasoning section 5 states) plus
928
+ // the two genuinely project-scoped MCP config locations, which use `cwd`
929
+ // unconditionally -- `cwd` already equals the project root in project
930
+ // mode (see this function's own docstring), so no separate branch is
931
+ // needed for them.
932
+ const mcpConfigFiles = [];
933
+ if (!projectMode) {
934
+ mcpConfigFiles.push(path.join(home, ".claude.json"));
935
+ const desktopConfig =
936
+ process.platform === "darwin" ? path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") :
937
+ process.platform === "win32" ? path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json") :
938
+ null; // Linux: no official build, unofficial ports disagree -- see agent-configs.js's own header
939
+ if (desktopConfig) mcpConfigFiles.push(desktopConfig);
940
+ mcpConfigFiles.push(path.join(home, ".cursor", "mcp.json"));
941
+ mcpConfigFiles.push(path.join(home, ".kiro", "settings", "mcp.json"));
942
+ mcpConfigFiles.push(path.join(home, ".mcp.json"));
943
+ }
944
+ mcpConfigFiles.push(path.join(cwd, ".mcp.json"));
945
+ mcpConfigFiles.push(path.join(cwd, ".vs", "mcp.json"));
946
+
947
+ for (const file of mcpConfigFiles) {
948
+ const text = readOrReport(file);
949
+ if (text === null) continue;
950
+
951
+ let parsed;
952
+ try { parsed = JSON.parse(text); } catch {
953
+ add("warn", "unparseable-config", file, "exists in a known MCP config location but is not valid JSON; the client's own loader would also choke on it, so corruption or tampering is worth a look");
954
+ continue;
955
+ }
956
+
957
+ const servers = extractMcpServers(parsed);
958
+ for (const [name, def] of Object.entries(servers)) {
959
+ if (!def || typeof def !== "object") continue;
960
+ const label = safePreview(name, 60);
961
+
962
+ for (const { ecosystem, package: pkg, version } of extractPackageVersions(def.args)) {
963
+ const { checkable, matches } = checkVersion(ecosystem, pkg, version);
964
+ if (!checkable) continue; // a version string this comparator can't parse: unverified, not silently clean, but nothing more to say than that
965
+ for (const m of matches) {
966
+ add("warn", "mcp-known-cve", file,
967
+ `MCP server "${label}" pins ${pkg}@${version}, matching ${m.id} (${m.severity}): ${m.summary} Upgrade past the vulnerable range.`);
968
+ }
969
+ }
970
+
971
+ if (isInsecureRemoteUrl(def.url)) {
972
+ add("warn", "mcp-insecure-transport", file,
973
+ `MCP server "${label}" connects to ${safePreview(def.url, 80)} over plain HTTP -- protocol traffic, including tool definitions, can be observed or altered in transit. Use an HTTPS URL if the server supports one.`);
974
+ }
975
+
976
+ const commandLine = [def.command, ...(Array.isArray(def.args) ? def.args : [])]
977
+ .filter((x) => typeof x === "string").join(" ");
978
+ if (commandLine && CURL_PIPE_SHELL_RE.test(commandLine)) {
979
+ add("warn", "mcp-curl-pipe-shell", file,
980
+ `MCP server "${label}" launch command fetches a script and pipes it directly into a shell: "${safePreview(commandLine)}". Review and pin to a specific, reviewed version instead of trusting whatever the URL currently serves.`);
981
+ }
982
+ }
983
+ }
984
+
830
985
  return {
831
986
  findings,
832
987
  filesChecked,