clawmoat 0.7.0 → 1.0.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.
Files changed (178) hide show
  1. package/.dockerignore +9 -0
  2. package/CHANGELOG.md +18 -0
  3. package/CONTRIBUTING.md +4 -2
  4. package/DEMO.md +87 -0
  5. package/Dockerfile +5 -18
  6. package/README.md +294 -8
  7. package/SECURITY.md +58 -10
  8. package/THREAT_MODEL.md +129 -0
  9. package/agent/README.md +131 -0
  10. package/agent/index.js +471 -0
  11. package/agent/install-service.sh +94 -0
  12. package/agent/openclaw-hook.js +453 -0
  13. package/agent/provider-setup.js +649 -0
  14. package/agent/setup.js +274 -0
  15. package/assets/BADGE-USAGE.md +20 -0
  16. package/assets/clawmoat-badge.svg +21 -0
  17. package/bin/clawmoat.js +468 -111
  18. package/docs/affiliates/dashboard.html +124 -0
  19. package/docs/affiliates/index.html +236 -0
  20. package/docs/agent-install.html +183 -0
  21. package/docs/ai-agent-security-scanner.html +10 -6
  22. package/docs/badge/index.html +149 -0
  23. package/docs/badge/scanning.svg +23 -0
  24. package/docs/blog/386-malicious-skills.html +262 -0
  25. package/docs/blog/40000-exposed-openclaw-instances.html +201 -0
  26. package/docs/blog/agent-trust-protocol.html +198 -0
  27. package/docs/blog/ai-agent-earns-commissions.html +230 -0
  28. package/docs/blog/bugmageddon-agent-firewall.html +174 -0
  29. package/docs/blog/calculator-math.html +180 -0
  30. package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +229 -0
  31. package/docs/blog/host-guardian-launch.html +18 -8
  32. package/docs/blog/ibm-experts-agent-runtime-protection.html +247 -0
  33. package/docs/blog/index.html +211 -9
  34. package/docs/blog/langchain-security-tutorial.html +18 -8
  35. package/docs/blog/mcp-30-cves-security-crisis.html +286 -0
  36. package/docs/blog/meta-researcher-rogue-agent.html +201 -0
  37. package/docs/blog/microsoft-openclaw-workstation-security.html +235 -0
  38. package/docs/blog/nist-ai-agent-standards-clawmoat.html +377 -0
  39. package/docs/blog/oasis-websocket-hijack.html +212 -0
  40. package/docs/blog/ollama-openclaw-security.html +160 -0
  41. package/docs/blog/openclaw-enterprise-readiness-claw10.html +199 -0
  42. package/docs/blog/openclaw-security-reckoning-2026.html +368 -0
  43. package/docs/blog/owasp-agentic-ai-top10.html +18 -8
  44. package/docs/blog/securing-ai-agents.html +18 -8
  45. package/docs/blog/supply-chain-agents.html +18 -8
  46. package/docs/business/index.html +525 -0
  47. package/docs/business/install.html +261 -0
  48. package/docs/checklist.html +174 -0
  49. package/docs/compare/index.html +122 -0
  50. package/docs/compare/lakera/index.html +62 -0
  51. package/docs/compare/llm-guard/index.html +49 -0
  52. package/docs/compare/snyk-agent-scan/index.html +63 -0
  53. package/docs/compare.html +10 -6
  54. package/docs/dashboard/index.html +520 -0
  55. package/docs/finance/index.html +220 -0
  56. package/docs/guides/business-deployment.html +770 -0
  57. package/docs/hall-of-fame.html +174 -0
  58. package/docs/index.html +447 -154
  59. package/docs/install.sh +557 -0
  60. package/docs/integrations/langchain.html +14 -6
  61. package/docs/integrations/openai.html +14 -6
  62. package/docs/integrations/openclaw.html +55 -7
  63. package/docs/plans/2026-03-26-threat-intel-api.md +255 -0
  64. package/docs/plans/2026-04-14-bugmageddon-marketing-pack.md +329 -0
  65. package/docs/plans/2026-04-14-clawmoat-v1-bugmageddon.md +248 -0
  66. package/docs/plans/2026-04-14-v1-release-update.md +91 -0
  67. package/docs/plans/2026-04-19-supabase-audit.md +68 -0
  68. package/docs/plans/2026-05-12-sales-push.md +303 -0
  69. package/docs/playground/index.html +893 -0
  70. package/docs/playground.html +4 -7
  71. package/docs/privacy-policy/index.html +122 -0
  72. package/docs/rfcs/defense-in-depth.md +467 -0
  73. package/docs/scan/index.html +358 -0
  74. package/docs/services/case-study.html +255 -0
  75. package/docs/services/downloads/install-openclaw.bat +45 -0
  76. package/docs/services/downloads/install-openclaw.command +38 -0
  77. package/docs/services/downloads/install-openclaw.sh +38 -0
  78. package/docs/services/get-started.html +165 -0
  79. package/docs/services/index.html +598 -0
  80. package/docs/services/multi-agent-security.html +284 -0
  81. package/docs/services/one-pager.html +99 -0
  82. package/docs/services/pitch-deck.html +229 -0
  83. package/docs/services/roi-calculator.html +258 -0
  84. package/docs/sitemap.xml +192 -2
  85. package/docs/support/index.html +135 -0
  86. package/docs/templates/customer-service/HEARTBEAT.md +61 -0
  87. package/docs/templates/customer-service/MEMORY.md +89 -0
  88. package/docs/templates/customer-service/SOUL.md +41 -0
  89. package/docs/templates/customer-service/USER.md +56 -0
  90. package/docs/templates/executive/HEARTBEAT.md +86 -0
  91. package/docs/templates/executive/MEMORY.md +92 -0
  92. package/docs/templates/executive/SOUL.md +44 -0
  93. package/docs/templates/executive/USER.md +62 -0
  94. package/docs/templates/finance/HEARTBEAT.md +58 -0
  95. package/docs/templates/finance/MEMORY.md +87 -0
  96. package/docs/templates/finance/SOUL.md +38 -0
  97. package/docs/templates/finance/USER.md +53 -0
  98. package/docs/templates/index.html +115 -0
  99. package/docs/templates/operations/HEARTBEAT.md +63 -0
  100. package/docs/templates/operations/MEMORY.md +68 -0
  101. package/docs/templates/operations/SOUL.md +38 -0
  102. package/docs/templates/operations/USER.md +49 -0
  103. package/docs/templates/sales/HEARTBEAT.md +55 -0
  104. package/docs/templates/sales/MEMORY.md +89 -0
  105. package/docs/templates/sales/SOUL.md +34 -0
  106. package/docs/templates/sales/USER.md +54 -0
  107. package/docs/terms-of-service/index.html +122 -0
  108. package/eslint.config.js +32 -0
  109. package/evals/README.md +29 -0
  110. package/evals/cases.json +390 -0
  111. package/evals/results.md +68 -0
  112. package/evals/run.js +180 -0
  113. package/examples/basic-usage.js +38 -0
  114. package/examples/demo-attack/demo.js +186 -0
  115. package/examples/python-quickstart/README.md +54 -0
  116. package/examples/python-quickstart/clawmoat_client.py +167 -0
  117. package/examples/video-demo/README.md +14 -0
  118. package/examples/video-demo/scene-a-normal.js +29 -0
  119. package/examples/video-demo/scene-b-attack-arrives.js +31 -0
  120. package/examples/video-demo/scene-c-hijack.js +44 -0
  121. package/examples/video-demo/scene-d-clawmoat.js +46 -0
  122. package/integrations/crewai/README.md +32 -0
  123. package/integrations/crewai/clawmoat_crewai/__init__.py +17 -0
  124. package/integrations/crewai/clawmoat_crewai/guard.py +103 -0
  125. package/integrations/crewai/pyproject.toml +21 -0
  126. package/integrations/langchain/README.md +91 -0
  127. package/integrations/langchain/clawmoat_langchain/__init__.py +17 -0
  128. package/integrations/langchain/clawmoat_langchain/callback.py +489 -0
  129. package/integrations/langchain/pyproject.toml +32 -0
  130. package/integrations/litellm/README.md +324 -0
  131. package/integrations/litellm/clawmoat_litellm/__init__.py +21 -0
  132. package/integrations/litellm/clawmoat_litellm/callback.py +329 -0
  133. package/integrations/litellm/clawmoat_litellm/proxy_middleware.py +224 -0
  134. package/integrations/litellm/pyproject.toml +74 -0
  135. package/integrations/openai-agents/README.md +392 -0
  136. package/integrations/openai-agents/clawmoat_openai_agents/__init__.py +20 -0
  137. package/integrations/openai-agents/clawmoat_openai_agents/guardrail.py +431 -0
  138. package/integrations/openai-agents/clawmoat_openai_agents/middleware.py +311 -0
  139. package/integrations/openai-agents/pyproject.toml +76 -0
  140. package/package.json +6 -5
  141. package/plugins/openclaw-adapter/PHASE1.md +439 -0
  142. package/plugins/openclaw-adapter/README.md +103 -0
  143. package/plugins/openclaw-adapter/SPEC.md +1644 -0
  144. package/plugins/openclaw-adapter/package.json +31 -0
  145. package/plugins/openclaw-adapter/src/index.test.ts +226 -0
  146. package/plugins/openclaw-adapter/src/index.ts +140 -0
  147. package/plugins/openclaw-adapter/tsconfig.json +14 -0
  148. package/server/data/threats.json +290 -0
  149. package/server/index.js +224 -10
  150. package/src/adapters/express.js +161 -0
  151. package/src/adapters/index.js +92 -0
  152. package/src/adapters/langchain.js +185 -0
  153. package/src/approval/index.js +456 -0
  154. package/src/ban-scanner.js +200 -0
  155. package/src/boundary-scanner.js +296 -0
  156. package/src/ci-scanner.js +279 -0
  157. package/src/code-scanner.js +245 -0
  158. package/src/enforce.js +166 -0
  159. package/src/finance/index.js +585 -0
  160. package/src/finance/mcp-firewall.js +486 -0
  161. package/src/formatters/json.js +80 -0
  162. package/src/formatters/sarif.js +388 -0
  163. package/src/guardian/alerts.js +34 -3
  164. package/src/guardian/gateway-monitor.js +590 -0
  165. package/src/guardian/index.js +41 -2
  166. package/src/index.js +105 -0
  167. package/src/integrations/agentmesh.js +501 -0
  168. package/src/language-detector.js +201 -0
  169. package/src/mcp-scanner.js +253 -0
  170. package/src/multimodal/index.js +579 -0
  171. package/src/obfuscation-scanner.js +457 -0
  172. package/src/policy-engine.js +402 -0
  173. package/src/scanners/dependency-attacks.js +128 -0
  174. package/src/scanners/prompt-injection.js +18 -0
  175. package/src/scanners/supply-chain.js +14 -0
  176. package/src/templates/default-config.yml +90 -0
  177. package/src/vuln-ops/exploitability.js +46 -0
  178. package/src/watch/live-monitor.js +720 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Language Detection Scanner
3
+ *
4
+ * Detects language of input text and flags anomalies:
5
+ * - Unexpected language switches (English-only agent gets Chinese instructions)
6
+ * - Mixed-language prompt injection attempts
7
+ * - Character set anomalies
8
+ *
9
+ * Stolen from LLM Guard's concept, implemented lightweight for JS (no ML model).
10
+ * Uses Unicode script detection + trigram frequency analysis.
11
+ *
12
+ * @module language-detector
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ // Unicode ranges for major scripts
18
+ const SCRIPTS = {
19
+ latin: { re: /[\u0041-\u005A\u0061-\u007A\u00C0-\u024F\u1E00-\u1EFF]/g, name: 'Latin' },
20
+ cyrillic: { re: /[\u0400-\u04FF\u0500-\u052F]/g, name: 'Cyrillic' },
21
+ chinese: { re: /[\u4E00-\u9FFF\u3400-\u4DBF\u{20000}-\u{2A6DF}]/gu, name: 'Chinese' },
22
+ arabic: { re: /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/g, name: 'Arabic' },
23
+ devanagari: { re: /[\u0900-\u097F]/g, name: 'Devanagari' },
24
+ japanese: { re: /[\u3040-\u309F\u30A0-\u30FF]/g, name: 'Japanese' },
25
+ korean: { re: /[\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F]/g, name: 'Korean' },
26
+ greek: { re: /[\u0370-\u03FF\u1F00-\u1FFF]/g, name: 'Greek' },
27
+ thai: { re: /[\u0E00-\u0E7F]/g, name: 'Thai' },
28
+ hebrew: { re: /[\u0590-\u05FF]/g, name: 'Hebrew' },
29
+ };
30
+
31
+ // Common English trigrams (top 30) for basic language ID
32
+ const ENGLISH_TRIGRAMS = new Set([
33
+ 'the', 'and', 'ing', 'ent', 'ion', 'tio', 'for', 'ati', 'ter', 'hat',
34
+ 'tha', 'ere', 'ate', 'his', 'con', 'res', 'ver', 'all', 'ons', 'nce',
35
+ 'men', 'ith', 'ted', 'ers', 'pro', 'thi', 'wit', 'are', 'ess', 'not',
36
+ ]);
37
+
38
+ /**
39
+ * Detect scripts present in text and their proportions
40
+ * @param {string} text - Text to analyze
41
+ * @returns {Object} { scripts: [{name, count, percentage}], dominant, totalChars }
42
+ */
43
+ function detectScripts(text) {
44
+ const results = [];
45
+ let totalScriptChars = 0;
46
+
47
+ for (const [key, { re, name }] of Object.entries(SCRIPTS)) {
48
+ const matches = text.match(re);
49
+ const count = matches ? matches.length : 0;
50
+ if (count > 0) {
51
+ results.push({ key, name, count });
52
+ totalScriptChars += count;
53
+ }
54
+ }
55
+
56
+ // Sort by count descending
57
+ results.sort((a, b) => b.count - a.count);
58
+
59
+ // Add percentages
60
+ for (const r of results) {
61
+ r.percentage = totalScriptChars > 0 ? Math.round((r.count / totalScriptChars) * 100) : 0;
62
+ }
63
+
64
+ return {
65
+ scripts: results,
66
+ dominant: results.length > 0 ? results[0].name : 'Unknown',
67
+ totalChars: totalScriptChars,
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Simple English confidence score using trigram frequency
73
+ * @param {string} text - Text to check
74
+ * @returns {number} 0-1 confidence that text is English
75
+ */
76
+ function englishConfidence(text) {
77
+ const lower = text.toLowerCase().replace(/[^a-z\s]/g, '');
78
+ if (lower.length < 10) return 0.5; // Too short to tell
79
+
80
+ const words = lower.split(/\s+/).filter(w => w.length >= 3);
81
+ if (words.length === 0) return 0;
82
+
83
+ let trigramHits = 0;
84
+ let totalTrigrams = 0;
85
+
86
+ for (const word of words) {
87
+ for (let i = 0; i <= word.length - 3; i++) {
88
+ totalTrigrams++;
89
+ if (ENGLISH_TRIGRAMS.has(word.substring(i, i + 3))) {
90
+ trigramHits++;
91
+ }
92
+ }
93
+ }
94
+
95
+ return totalTrigrams > 0 ? Math.min(1, trigramHits / totalTrigrams * 3) : 0;
96
+ }
97
+
98
+ /**
99
+ * Scan text for language anomalies
100
+ * @param {string} text - Text to scan
101
+ * @param {Object} [opts] - Options
102
+ * @param {string[]} [opts.expectedLanguages=['latin']] - Expected script keys
103
+ * @param {number} [opts.anomalyThreshold=0.15] - Min percentage of unexpected script to flag
104
+ * @param {boolean} [opts.allowMixed=false] - Allow mixed scripts without flagging
105
+ * @returns {Object} { safe, findings, scripts, dominant }
106
+ */
107
+ function scanLanguage(text, opts = {}) {
108
+ const {
109
+ expectedLanguages = ['latin'],
110
+ anomalyThreshold = 0.15,
111
+ allowMixed = false,
112
+ } = opts;
113
+
114
+ const detection = detectScripts(text);
115
+ const findings = [];
116
+
117
+ if (detection.totalChars < 5) {
118
+ return { safe: true, findings: [], scripts: detection.scripts, dominant: detection.dominant };
119
+ }
120
+
121
+ // Check for unexpected scripts
122
+ const unexpectedScripts = detection.scripts.filter(s => {
123
+ const pct = s.count / detection.totalChars;
124
+ return !expectedLanguages.includes(s.key) && pct >= anomalyThreshold;
125
+ });
126
+
127
+ if (unexpectedScripts.length > 0 && !allowMixed) {
128
+ const names = unexpectedScripts.map(s => `${s.name} (${s.percentage}%)`).join(', ');
129
+ findings.push({
130
+ type: 'language_anomaly',
131
+ subtype: 'unexpected_script',
132
+ severity: 'medium',
133
+ confidence: 0.7,
134
+ evidence: `Unexpected script(s) detected: ${names}. Expected: ${expectedLanguages.join(', ')}`,
135
+ scripts: unexpectedScripts.map(s => s.name),
136
+ recommended_action: 'flag_for_review',
137
+ });
138
+ }
139
+
140
+ // Check for script switching mid-text (potential injection)
141
+ if (detection.scripts.length >= 2) {
142
+ // Look for abrupt transitions — split text into chunks and check script consistency
143
+ const chunks = text.match(/.{1,50}/g) || [];
144
+ let scriptSwitches = 0;
145
+ let lastDominant = null;
146
+
147
+ for (const chunk of chunks) {
148
+ const chunkDetection = detectScripts(chunk);
149
+ if (chunkDetection.dominant !== 'Unknown') {
150
+ if (lastDominant && chunkDetection.dominant !== lastDominant) {
151
+ scriptSwitches++;
152
+ }
153
+ lastDominant = chunkDetection.dominant;
154
+ }
155
+ }
156
+
157
+ if (scriptSwitches >= 3) {
158
+ findings.push({
159
+ type: 'language_anomaly',
160
+ subtype: 'frequent_script_switching',
161
+ severity: 'high',
162
+ confidence: 0.75,
163
+ evidence: `Text switches between scripts ${scriptSwitches} times across ${chunks.length} segments — possible multilingual injection`,
164
+ switches: scriptSwitches,
165
+ recommended_action: 'block',
166
+ });
167
+ }
168
+ }
169
+
170
+ // Check if predominantly non-Latin text contains embedded Latin command-like strings
171
+ if (detection.dominant !== 'Latin' && detection.scripts.some(s => s.key === 'latin')) {
172
+ const latinPortion = text.match(/[a-zA-Z\s]{10,}/g) || [];
173
+ const suspiciousCommands = latinPortion.filter(p =>
174
+ /ignore|override|system|prompt|exec|eval|admin|password|secret|token/i.test(p)
175
+ );
176
+ if (suspiciousCommands.length > 0) {
177
+ findings.push({
178
+ type: 'language_anomaly',
179
+ subtype: 'embedded_command_in_foreign_text',
180
+ severity: 'high',
181
+ confidence: 0.8,
182
+ evidence: `Found command-like Latin text embedded in ${detection.dominant} content: "${suspiciousCommands[0].trim().substring(0, 60)}"`,
183
+ recommended_action: 'block',
184
+ });
185
+ }
186
+ }
187
+
188
+ return {
189
+ safe: findings.length === 0,
190
+ findings,
191
+ scripts: detection.scripts,
192
+ dominant: detection.dominant,
193
+ };
194
+ }
195
+
196
+ module.exports = {
197
+ scanLanguage,
198
+ detectScripts,
199
+ englishConfidence,
200
+ SCRIPTS,
201
+ };
@@ -0,0 +1,253 @@
1
+ /**
2
+ * ClawMoat MCP Scanner — Discover and scan MCP server configurations
3
+ * Finds configs for Claude Code, Claude Desktop, Cursor, VS Code, Windsurf, Gemini CLI
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const HOME = os.homedir();
11
+
12
+ // Known MCP config locations
13
+ const MCP_CONFIG_PATHS = [
14
+ // Claude Desktop
15
+ { name: 'Claude Desktop', path: path.join(HOME, '.claude', 'claude_desktop_config.json') },
16
+ { name: 'Claude Desktop (macOS)', path: path.join(HOME, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') },
17
+ { name: 'Claude Desktop (Windows)', path: path.join(HOME, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json') },
18
+ // Claude Code
19
+ { name: 'Claude Code', path: path.join(HOME, '.claude', 'mcp.json') },
20
+ { name: 'Claude Code (project)', path: '.claude/mcp.json' },
21
+ // Cursor
22
+ { name: 'Cursor', path: path.join(HOME, '.cursor', 'mcp.json') },
23
+ // VS Code
24
+ { name: 'VS Code', path: path.join(HOME, '.vscode', 'mcp.json') },
25
+ { name: 'VS Code (settings)', path: path.join(HOME, '.config', 'Code', 'User', 'settings.json') },
26
+ // Windsurf
27
+ { name: 'Windsurf', path: path.join(HOME, '.windsurf', 'mcp.json') },
28
+ { name: 'Windsurf (codeium)', path: path.join(HOME, '.codeium', 'windsurf', 'mcp_config.json') },
29
+ // Gemini CLI
30
+ { name: 'Gemini CLI', path: path.join(HOME, '.gemini', 'settings.json') },
31
+ // OpenClaw
32
+ { name: 'OpenClaw', path: path.join(HOME, '.openclaw', 'openclaw.json') },
33
+ ];
34
+
35
+ // Dangerous patterns in MCP server configs
36
+ const MCP_RISKS = [
37
+ // Command execution
38
+ { id: 'mcp-arbitrary-cmd', severity: 'critical', pattern: (server) => {
39
+ const cmd = server.command || '';
40
+ const args = (server.args || []).join(' ');
41
+ return /^(bash|sh|zsh|cmd|powershell|node|python|ruby|perl)$/i.test(cmd) && !/mcp|server/i.test(args);
42
+ }, label: 'Arbitrary command execution', fix: 'MCP server runs a generic shell/interpreter. Use a purpose-built MCP server binary instead.' },
43
+
44
+ // Root filesystem access
45
+ { id: 'mcp-root-fs', severity: 'critical', pattern: (server) => {
46
+ const args = JSON.stringify(server.args || []);
47
+ return /["\s]\/["\\s,\]]/.test(args) || /allowedDirectories.*["']\/["']/.test(args);
48
+ }, label: 'Root filesystem access', fix: 'MCP server has access to root filesystem. Restrict to specific directories.' },
49
+
50
+ // Home directory access
51
+ { id: 'mcp-home-fs', severity: 'high', pattern: (server) => {
52
+ const args = JSON.stringify(server.args || []);
53
+ return args.includes(HOME) && /filesystem|fs-|file/i.test(JSON.stringify(server));
54
+ }, label: 'Home directory access via filesystem MCP', fix: 'Restrict filesystem MCP servers to project directories only.' },
55
+
56
+ // Known dangerous MCP servers
57
+ { id: 'mcp-dangerous-server', severity: 'high', pattern: (server) => {
58
+ const full = JSON.stringify(server).toLowerCase();
59
+ return /mcp-shell|mcp-terminal|mcp-exec|mcp-command/.test(full);
60
+ }, label: 'Shell/terminal execution MCP server', fix: 'Shell-access MCP servers give agents unrestricted command execution. Remove or heavily restrict.' },
61
+
62
+ // Env var credential exposure
63
+ { id: 'mcp-env-creds', severity: 'high', pattern: (server) => {
64
+ const env = server.env || {};
65
+ const keys = Object.keys(env);
66
+ return keys.some(k => /key|secret|token|password|auth|credential/i.test(k));
67
+ }, label: 'Credentials in MCP server environment', fix: 'MCP server has API keys/secrets in env vars. These are accessible to any tool the server exposes. Use scoped tokens with minimal permissions.' },
68
+
69
+ // Hardcoded URLs (potential C2)
70
+ { id: 'mcp-external-url', severity: 'medium', pattern: (server) => {
71
+ const full = JSON.stringify(server);
72
+ const urls = full.match(/https?:\/\/[^\s"']+/g) || [];
73
+ return urls.some(u => !/localhost|127\.0\.0\.1|github\.com|npmjs\.com|pypi\.org/.test(u));
74
+ }, label: 'External URL in MCP config', fix: 'MCP server connects to an external URL. Verify this is a trusted endpoint.' },
75
+
76
+ // stdio with no restrictions
77
+ { id: 'mcp-unrestricted-stdio', severity: 'medium', pattern: (server) => {
78
+ const transport = server.transport || 'stdio';
79
+ return transport === 'stdio' && !server.allowedTools && !server.blockedTools;
80
+ }, label: 'Unrestricted stdio MCP server', fix: 'MCP server exposes all tools via stdio with no allowlist/blocklist. Add allowedTools to restrict.' },
81
+
82
+ // npx/pip without pinned version
83
+ { id: 'mcp-unpinned', severity: 'medium', pattern: (server) => {
84
+ const cmd = server.command || '';
85
+ const args = (server.args || []).join(' ');
86
+ if (!/npx|uvx|pip/.test(cmd)) return false;
87
+ // Check if any arg has a version pin (@version or ==version)
88
+ return !/@[\d.]|==[\d.]/.test(args);
89
+ }, label: 'Unpinned MCP server package', fix: 'MCP server installed via npx/uvx without version pin. A supply chain attack could replace the package. Pin to a specific version.' },
90
+
91
+ // Too many tools exposed
92
+ { id: 'mcp-tool-sprawl', severity: 'low', pattern: (server) => {
93
+ // Can't always detect this statically, but flag servers with no tool restrictions
94
+ return !server.allowedTools && !server.blockedTools && !server.disabledTools;
95
+ }, label: 'No tool restrictions configured', fix: 'Consider using allowedTools/blockedTools to limit which MCP tools the agent can invoke.' },
96
+ ];
97
+
98
+ // Known compromised/vulnerable MCP servers
99
+ const KNOWN_VULNERABLE_SERVERS = {
100
+ '@anthropic/mcp-filesystem': { severity: 'medium', note: 'Powerful but broad. Always restrict allowedDirectories.' },
101
+ 'mcp-shell': { severity: 'critical', note: 'Gives agents arbitrary shell access. Almost never appropriate.' },
102
+ 'mcp-terminal': { severity: 'critical', note: 'Gives agents terminal access. Use with extreme caution.' },
103
+ };
104
+
105
+ /**
106
+ * Discover MCP config files on the system
107
+ * @returns {Array<{name: string, path: string, exists: boolean}>}
108
+ */
109
+ function discoverMCPConfigs(extraPaths = []) {
110
+ const allPaths = [...MCP_CONFIG_PATHS, ...extraPaths.map(p => ({ name: 'Custom', path: p }))];
111
+ return allPaths.map(entry => ({
112
+ ...entry,
113
+ exists: fs.existsSync(entry.path),
114
+ }));
115
+ }
116
+
117
+ /**
118
+ * Parse an MCP config file and extract server definitions
119
+ * @param {string} filePath
120
+ * @returns {Array<{name: string, server: object}>}
121
+ */
122
+ function parseMCPConfig(filePath) {
123
+ try {
124
+ const raw = fs.readFileSync(filePath, 'utf8');
125
+ const config = JSON.parse(raw);
126
+ const servers = [];
127
+
128
+ // Claude Desktop / Claude Code format: { mcpServers: { name: { command, args, env } } }
129
+ if (config.mcpServers) {
130
+ for (const [name, server] of Object.entries(config.mcpServers)) {
131
+ servers.push({ name, server, source: 'mcpServers' });
132
+ }
133
+ }
134
+
135
+ // VS Code / Cursor format: { mcp: { servers: { name: { ... } } } }
136
+ if (config.mcp?.servers) {
137
+ for (const [name, server] of Object.entries(config.mcp.servers)) {
138
+ servers.push({ name, server, source: 'mcp.servers' });
139
+ }
140
+ }
141
+
142
+ // Array format: [{ name, command, args }]
143
+ if (Array.isArray(config)) {
144
+ config.forEach((server, i) => {
145
+ servers.push({ name: server.name || `server-${i}`, server, source: 'array' });
146
+ });
147
+ }
148
+
149
+ // OpenClaw format: check for mcp section
150
+ if (config.mcp) {
151
+ if (config.mcp.mcpServers) {
152
+ for (const [name, server] of Object.entries(config.mcp.mcpServers)) {
153
+ servers.push({ name, server, source: 'openclaw.mcp.mcpServers' });
154
+ }
155
+ }
156
+ }
157
+
158
+ return servers;
159
+ } catch (e) {
160
+ return [];
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Scan a single MCP server definition for risks
166
+ * @param {string} serverName
167
+ * @param {object} serverConfig
168
+ * @returns {Array<{id: string, severity: string, label: string, fix: string}>}
169
+ */
170
+ function scanMCPServer(serverName, serverConfig) {
171
+ const findings = [];
172
+
173
+ // Check against risk patterns
174
+ for (const risk of MCP_RISKS) {
175
+ try {
176
+ if (risk.pattern(serverConfig)) {
177
+ findings.push({
178
+ id: risk.id,
179
+ severity: risk.severity,
180
+ label: risk.label,
181
+ fix: risk.fix,
182
+ server: serverName,
183
+ });
184
+ }
185
+ } catch (e) { /* pattern failed, skip */ }
186
+ }
187
+
188
+ // Check against known vulnerable servers
189
+ const cmd = serverConfig.command || '';
190
+ const args = (serverConfig.args || []).join(' ');
191
+ const fullCmd = `${cmd} ${args}`;
192
+ for (const [pkg, info] of Object.entries(KNOWN_VULNERABLE_SERVERS)) {
193
+ if (fullCmd.includes(pkg)) {
194
+ findings.push({
195
+ id: 'mcp-known-vulnerable',
196
+ severity: info.severity,
197
+ label: `Known risky MCP server: ${pkg}`,
198
+ fix: info.note,
199
+ server: serverName,
200
+ });
201
+ }
202
+ }
203
+
204
+ return findings;
205
+ }
206
+
207
+ /**
208
+ * Full MCP scan — discover configs, parse, and scan all servers
209
+ * @param {object} options
210
+ * @returns {object} Scan report
211
+ */
212
+ function scanMCP(options = {}) {
213
+ const { extraPaths = [], verbose = false } = options;
214
+ const report = {
215
+ timestamp: new Date().toISOString(),
216
+ configsFound: [],
217
+ servers: [],
218
+ findings: [],
219
+ summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
220
+ };
221
+
222
+ // Discover configs
223
+ const configs = discoverMCPConfigs(extraPaths);
224
+ report.configsFound = configs.filter(c => c.exists);
225
+
226
+ // Parse and scan each config
227
+ for (const config of report.configsFound) {
228
+ const servers = parseMCPConfig(config.path);
229
+ for (const { name, server, source } of servers) {
230
+ report.servers.push({ name, config: config.name, path: config.path, source });
231
+ const findings = scanMCPServer(name, server);
232
+ for (const f of findings) {
233
+ f.configName = config.name;
234
+ f.configPath = config.path;
235
+ report.findings.push(f);
236
+ report.summary.total++;
237
+ report.summary[f.severity]++;
238
+ }
239
+ }
240
+ }
241
+
242
+ return report;
243
+ }
244
+
245
+ module.exports = {
246
+ discoverMCPConfigs,
247
+ parseMCPConfig,
248
+ scanMCPServer,
249
+ scanMCP,
250
+ MCP_CONFIG_PATHS,
251
+ MCP_RISKS,
252
+ KNOWN_VULNERABLE_SERVERS,
253
+ };