@mcp-shark/mcp-shark 1.5.13 → 1.6.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 (100) hide show
  1. package/README.md +396 -59
  2. package/bin/mcp-shark.js +146 -52
  3. package/core/cli/AutoFixEngine.js +93 -0
  4. package/core/cli/ConfigScanner.js +193 -0
  5. package/core/cli/DataLoader.js +200 -0
  6. package/core/cli/DeclarativeRuleEngine.js +363 -0
  7. package/core/cli/DoctorCommand.js +218 -0
  8. package/core/cli/FixHandlers.js +222 -0
  9. package/core/cli/HtmlReportGenerator.js +203 -0
  10. package/core/cli/IdeConfigPaths.js +175 -0
  11. package/core/cli/ListCommand.js +224 -0
  12. package/core/cli/LockCommand.js +164 -0
  13. package/core/cli/LockDiffEngine.js +152 -0
  14. package/core/cli/RuleRegistryConfig.js +131 -0
  15. package/core/cli/ScanCommand.js +244 -0
  16. package/core/cli/ScanService.js +200 -0
  17. package/core/cli/SecretDetector.js +92 -0
  18. package/core/cli/SharkScoreCalculator.js +109 -0
  19. package/core/cli/ToolClassifications.js +51 -0
  20. package/core/cli/ToxicFlowAnalyzer.js +212 -0
  21. package/core/cli/UpdateCommand.js +188 -0
  22. package/core/cli/WalkthroughGenerator.js +195 -0
  23. package/core/cli/WatchCommand.js +129 -0
  24. package/core/cli/YamlRuleEngine.js +197 -0
  25. package/core/cli/data/rule-packs/agentic-security-2026.json +180 -0
  26. package/core/cli/data/rule-packs/general-security.json +173 -0
  27. package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
  28. package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
  29. package/core/cli/data/rule-sources.json +5 -0
  30. package/core/cli/data/secret-patterns.json +18 -0
  31. package/core/cli/data/tool-classifications.json +111 -0
  32. package/core/cli/data/toxic-flow-rules.json +47 -0
  33. package/core/cli/index.js +23 -0
  34. package/core/cli/output/Banner.js +52 -0
  35. package/core/cli/output/Formatter.js +183 -0
  36. package/core/cli/output/JsonFormatter.js +106 -0
  37. package/core/cli/output/index.js +16 -0
  38. package/core/cli/secureRegistryFetch.js +157 -0
  39. package/core/cli/symbols.js +16 -0
  40. package/core/container/DependencyContainer.js +4 -1
  41. package/core/repositories/PacketRepository.js +16 -0
  42. package/core/services/AuditService.js +2 -0
  43. package/core/services/security/StaticRulesService.js +69 -13
  44. package/core/services/security/TrafficAnalysisService.js +19 -1
  45. package/core/services/security/TrafficToxicFlowService.js +154 -0
  46. package/core/services/security/index.js +2 -1
  47. package/core/services/security/rules/index.js +25 -59
  48. package/core/services/security/rules/scans/configPermissions.js +91 -0
  49. package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
  50. package/core/services/security/rules/scans/insecureTransport.js +148 -0
  51. package/core/services/security/rules/scans/missingContainment.js +123 -0
  52. package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
  53. package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
  54. package/core/services/security/toolsListFromTrafficParser.js +70 -0
  55. package/core/tui/App.js +144 -0
  56. package/core/tui/FindingsPanel.js +115 -0
  57. package/core/tui/FixPanel.js +132 -0
  58. package/core/tui/Header.js +51 -0
  59. package/core/tui/HelpBar.js +42 -0
  60. package/core/tui/ServersPanel.js +109 -0
  61. package/core/tui/ToxicFlowsPanel.js +100 -0
  62. package/core/tui/h.js +8 -0
  63. package/core/tui/index.js +11 -0
  64. package/core/tui/render.js +22 -0
  65. package/package.json +24 -16
  66. package/ui/dist/assets/{index-CiCSDYf-.js → index-Buah4fNS.js} +32 -32
  67. package/ui/dist/index.html +1 -1
  68. package/ui/server/controllers/RequestController.js +9 -1
  69. package/ui/server/controllers/SecurityFindingsController.js +46 -1
  70. package/ui/server/routes/requests.js +8 -1
  71. package/ui/server/routes/security.js +5 -1
  72. package/ui/server/setup.js +2 -0
  73. package/ui/server/swagger/paths/components.js +55 -0
  74. package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
  75. package/ui/server/swagger/paths.js +2 -0
  76. package/ui/server/swagger/swagger.js +5 -1
  77. package/ui/src/Security.jsx +10 -0
  78. package/ui/src/components/Security/ScannerContent.jsx +30 -1
  79. package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
  80. package/ui/src/components/Security/securityApi.js +15 -0
  81. package/ui/src/components/Security/useSecurity.js +60 -3
  82. package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
  83. package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
  84. package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
  85. package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
  86. package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
  87. package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
  88. package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
  89. package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
  90. package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
  91. package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
  92. package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
  93. package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
  94. package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
  95. package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
  96. package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
  97. package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
  98. package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
  99. package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
  100. package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Duplicate Tool Names Detection (Config-Level)
3
+ * Detects tools with identical names across different servers,
4
+ * which enables cross-server shadowing attacks.
5
+ * Catalog reference: §1.4 (tool name collision)
6
+ */
7
+ import { createRuleAdapter } from '../utils/adapter.js';
8
+
9
+ const RULE_ID = 'duplicate-tool-names';
10
+ const OWASP_ID = 'MCP09';
11
+ const RECOMMENDATION =
12
+ 'Rename tools to be unique across all servers, or isolate conflicting servers into separate agent sessions.';
13
+
14
+ export function scanDuplicateToolNames(_mcpData = {}) {
15
+ return {
16
+ toolFindings: [],
17
+ resourceFindings: [],
18
+ promptFindings: [],
19
+ notablePatterns: [],
20
+ recommendations: [RECOMMENDATION],
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Analyze all servers for duplicate tool names
26
+ * Called directly by ScanService with the full server list
27
+ * @param {Array} servers - Flat array of server objects
28
+ * @returns {Array} Findings for duplicate tool names
29
+ */
30
+ export function analyzeAllServerToolNames(servers) {
31
+ const toolMap = new Map();
32
+
33
+ for (const server of servers) {
34
+ const tools = Array.isArray(server.tools) ? server.tools : [];
35
+ for (const tool of tools) {
36
+ const name = typeof tool === 'string' ? tool : tool?.name;
37
+ if (!name) {
38
+ continue;
39
+ }
40
+ if (!toolMap.has(name)) {
41
+ toolMap.set(name, []);
42
+ }
43
+ toolMap.get(name).push(server.name);
44
+ }
45
+ }
46
+
47
+ const findings = [];
48
+ for (const [toolName, serverNames] of toolMap) {
49
+ if (serverNames.length <= 1) {
50
+ continue;
51
+ }
52
+ const uniqueServers = [...new Set(serverNames)];
53
+ if (uniqueServers.length <= 1) {
54
+ continue;
55
+ }
56
+
57
+ findings.push({
58
+ rule_id: RULE_ID,
59
+ severity: 'high',
60
+ owasp_id: OWASP_ID,
61
+ title: `Duplicate tool "${toolName}" across ${uniqueServers.length} servers`,
62
+ description: `Tool "${toolName}" exists in ${uniqueServers.join(', ')}. An attacker could shadow one server's tool with another.`,
63
+ recommendation: RECOMMENDATION,
64
+ confidence: 'definite',
65
+ });
66
+ }
67
+
68
+ return findings;
69
+ }
70
+
71
+ const adapter = createRuleAdapter(scanDuplicateToolNames, RULE_ID, OWASP_ID, RECOMMENDATION);
72
+ export const analyzeTool = adapter.analyzeTool;
73
+ export const analyzePrompt = adapter.analyzePrompt;
74
+ export const analyzeResource = adapter.analyzeResource;
75
+ export const analyzePacket = adapter.analyzePacket;
76
+
77
+ export const ruleMetadata = {
78
+ id: RULE_ID,
79
+ name: 'Duplicate Tool Names',
80
+ owasp_id: OWASP_ID,
81
+ severity: 'high',
82
+ description: 'Detects identical tool names across servers enabling shadowing attacks.',
83
+ source: 'static',
84
+ type: 'general-security',
85
+ };
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Insecure Transport Detection
3
+ * Detects MCP servers using HTTP instead of HTTPS, or SSE/WebSocket
4
+ * connections without TLS, exposing traffic to interception.
5
+ * Catalog reference: §1.7
6
+ */
7
+ import { createRuleAdapter } from '../utils/adapter.js';
8
+ import { toolToText } from '../utils/text.js';
9
+
10
+ const RULE_ID = 'insecure-transport';
11
+ const OWASP_ID = 'MCP07';
12
+ const RECOMMENDATION =
13
+ 'Use HTTPS/WSS for all remote MCP server connections. localhost HTTP is acceptable for local-only servers.';
14
+
15
+ const HTTP_URL_IN_TEXT = /http:\/\/[^\s'"<>]+/gi;
16
+ const WS_URL_IN_TEXT = /ws:\/\/[^\s'"<>]+/gi;
17
+
18
+ /**
19
+ * True when URL host is loopback / local bind only (exact host match, not localhost.evil.com).
20
+ */
21
+ function isLocalhostUrl(urlString) {
22
+ try {
23
+ const u = new URL(urlString);
24
+ const h = u.hostname.toLowerCase();
25
+ return (
26
+ h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0' || h === '[::1]' || h === '::1'
27
+ );
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ function textHasNonLocalHttp(text) {
34
+ for (const m of text.matchAll(HTTP_URL_IN_TEXT)) {
35
+ if (!isLocalhostUrl(m[0])) {
36
+ return true;
37
+ }
38
+ }
39
+ return false;
40
+ }
41
+
42
+ function textHasNonLocalWs(text) {
43
+ for (const m of text.matchAll(WS_URL_IN_TEXT)) {
44
+ if (!isLocalhostUrl(m[0])) {
45
+ return true;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+
51
+ export function scanInsecureTransport(mcpData = {}) {
52
+ const results = {
53
+ toolFindings: [],
54
+ resourceFindings: [],
55
+ promptFindings: [],
56
+ notablePatterns: [],
57
+ recommendations: [RECOMMENDATION],
58
+ };
59
+
60
+ for (const tool of mcpData.tools || []) {
61
+ const text = toolToText(tool);
62
+
63
+ if (textHasNonLocalHttp(text)) {
64
+ results.toolFindings.push({
65
+ issueType: 'Insecure Transport (HTTP)',
66
+ name: tool?.name || 'tool',
67
+ severity: 'medium',
68
+ reasons: [
69
+ `Tool "${tool?.name || 'unknown'}" references a non-localhost HTTP URL. Traffic can be intercepted.`,
70
+ ],
71
+ tags: ['transport', 'tls'],
72
+ safeUseNotes: RECOMMENDATION,
73
+ });
74
+ }
75
+
76
+ if (textHasNonLocalWs(text)) {
77
+ results.toolFindings.push({
78
+ issueType: 'Insecure Transport (WebSocket)',
79
+ name: tool?.name || 'tool',
80
+ severity: 'medium',
81
+ reasons: [
82
+ `Tool "${tool?.name || 'unknown'}" references a non-localhost WS URL. Traffic can be intercepted.`,
83
+ ],
84
+ tags: ['transport', 'websocket'],
85
+ safeUseNotes: RECOMMENDATION,
86
+ });
87
+ }
88
+ }
89
+
90
+ return results;
91
+ }
92
+
93
+ /**
94
+ * Analyze a server config for insecure transport
95
+ * Called directly by ScanService
96
+ */
97
+ export function analyzeServerTransport(serverName, config) {
98
+ if (!config?.url) {
99
+ return [];
100
+ }
101
+
102
+ const findings = [];
103
+ const urlStr = config.url;
104
+
105
+ if (urlStr.startsWith('http://') && !isLocalhostUrl(urlStr)) {
106
+ findings.push({
107
+ rule_id: RULE_ID,
108
+ severity: 'high',
109
+ owasp_id: OWASP_ID,
110
+ title: `Insecure transport: ${serverName} uses HTTP`,
111
+ description: `${serverName} connects to ${config.url} over plain HTTP. Credentials and data are transmitted in cleartext.`,
112
+ recommendation: RECOMMENDATION,
113
+ server_name: serverName,
114
+ confidence: 'definite',
115
+ });
116
+ }
117
+
118
+ if (urlStr.startsWith('ws://') && !isLocalhostUrl(urlStr)) {
119
+ findings.push({
120
+ rule_id: RULE_ID,
121
+ severity: 'high',
122
+ owasp_id: OWASP_ID,
123
+ title: `Insecure transport: ${serverName} uses WS`,
124
+ description: `${serverName} connects to ${config.url} over plain WebSocket. Upgrade to WSS.`,
125
+ recommendation: RECOMMENDATION,
126
+ server_name: serverName,
127
+ confidence: 'definite',
128
+ });
129
+ }
130
+
131
+ return findings;
132
+ }
133
+
134
+ const adapter = createRuleAdapter(scanInsecureTransport, RULE_ID, OWASP_ID, RECOMMENDATION);
135
+ export const analyzeTool = adapter.analyzeTool;
136
+ export const analyzePrompt = adapter.analyzePrompt;
137
+ export const analyzeResource = adapter.analyzeResource;
138
+ export const analyzePacket = adapter.analyzePacket;
139
+
140
+ export const ruleMetadata = {
141
+ id: RULE_ID,
142
+ name: 'Insecure Transport',
143
+ owasp_id: OWASP_ID,
144
+ severity: 'medium',
145
+ description: 'Detects non-localhost HTTP/WS connections that expose traffic to interception.',
146
+ source: 'static',
147
+ type: 'general-security',
148
+ };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Missing Directory Containment Detection
3
+ * Detects filesystem-type MCP servers without directory restrictions,
4
+ * granting full filesystem access to the agent.
5
+ * Catalog reference: §1.1 (unrestricted file access)
6
+ */
7
+ import { createRuleAdapter } from '../utils/adapter.js';
8
+ import { toolToText } from '../utils/text.js';
9
+
10
+ const RULE_ID = 'missing-containment';
11
+ const OWASP_ID = 'MCP10';
12
+ const RECOMMENDATION =
13
+ 'Restrict filesystem servers to specific directories using allowlist args (e.g., --allow-dir /path/to/project).';
14
+
15
+ const FS_SERVER_PATTERNS = [
16
+ /filesystem/i,
17
+ /file.?system/i,
18
+ /fs.?server/i,
19
+ /read_file|write_file|list_directory/i,
20
+ ];
21
+
22
+ const CONTAINMENT_INDICATORS = [
23
+ /--allow/i,
24
+ /--root/i,
25
+ /--sandbox/i,
26
+ /allowedDirectories/i,
27
+ /rootPath/i,
28
+ /basePath/i,
29
+ ];
30
+
31
+ function isFilesystemServer(serverName, config) {
32
+ const text = `${serverName} ${JSON.stringify(config || {})}`;
33
+ for (const pattern of FS_SERVER_PATTERNS) {
34
+ if (pattern.test(text)) {
35
+ return true;
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+
41
+ function hasContainment(config) {
42
+ const text = JSON.stringify(config || {});
43
+ for (const pattern of CONTAINMENT_INDICATORS) {
44
+ if (pattern.test(text)) {
45
+ return true;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+
51
+ export function scanMissingContainment(mcpData = {}) {
52
+ const results = {
53
+ toolFindings: [],
54
+ resourceFindings: [],
55
+ promptFindings: [],
56
+ notablePatterns: [],
57
+ recommendations: [RECOMMENDATION],
58
+ };
59
+
60
+ for (const tool of mcpData.tools || []) {
61
+ const text = toolToText(tool);
62
+ const hasFilesystem = FS_SERVER_PATTERNS.some((p) => p.test(text));
63
+ if (hasFilesystem) {
64
+ const contained = CONTAINMENT_INDICATORS.some((p) => p.test(text));
65
+ if (!contained) {
66
+ results.toolFindings.push({
67
+ issueType: 'Missing Directory Containment',
68
+ name: tool?.name || 'tool',
69
+ severity: 'medium',
70
+ reasons: [
71
+ `Tool "${tool?.name || 'unknown'}" has filesystem access without directory containment.`,
72
+ ],
73
+ tags: ['containment', 'filesystem'],
74
+ safeUseNotes: RECOMMENDATION,
75
+ });
76
+ }
77
+ }
78
+ }
79
+
80
+ return results;
81
+ }
82
+
83
+ /**
84
+ * Analyze a server config for missing containment
85
+ * Called directly by ScanService
86
+ */
87
+ export function analyzeServerContainment(serverName, config) {
88
+ if (!isFilesystemServer(serverName, config)) {
89
+ return [];
90
+ }
91
+ if (hasContainment(config)) {
92
+ return [];
93
+ }
94
+
95
+ return [
96
+ {
97
+ rule_id: RULE_ID,
98
+ severity: 'medium',
99
+ owasp_id: OWASP_ID,
100
+ title: `No directory containment on ${serverName}`,
101
+ description: `${serverName} has filesystem access without directory restrictions. The agent can read/write any file.`,
102
+ recommendation: RECOMMENDATION,
103
+ server_name: serverName,
104
+ confidence: 'possible',
105
+ },
106
+ ];
107
+ }
108
+
109
+ const adapter = createRuleAdapter(scanMissingContainment, RULE_ID, OWASP_ID, RECOMMENDATION);
110
+ export const analyzeTool = adapter.analyzeTool;
111
+ export const analyzePrompt = adapter.analyzePrompt;
112
+ export const analyzeResource = adapter.analyzeResource;
113
+ export const analyzePacket = adapter.analyzePacket;
114
+
115
+ export const ruleMetadata = {
116
+ id: RULE_ID,
117
+ name: 'Missing Directory Containment',
118
+ owasp_id: OWASP_ID,
119
+ severity: 'medium',
120
+ description: 'Detects filesystem servers without directory restrictions.',
121
+ source: 'static',
122
+ type: 'general-security',
123
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Shell/Environment Injection Detection
3
+ * Detects MCP server configs using shell: true or unsanitized
4
+ * environment variable interpolation in commands, which can
5
+ * enable command injection via environment variables.
6
+ * Catalog reference: §1.9 (env-based command injection)
7
+ */
8
+ import { createRuleAdapter } from '../utils/adapter.js';
9
+
10
+ const RULE_ID = 'shell-env-injection';
11
+ const OWASP_ID = 'MCP05';
12
+ const RECOMMENDATION =
13
+ 'Avoid shell: true in server spawn configs. Use direct command arrays instead of shell string interpolation.';
14
+
15
+ export function scanShellEnvInjection(_mcpData = {}) {
16
+ return {
17
+ toolFindings: [],
18
+ resourceFindings: [],
19
+ promptFindings: [],
20
+ notablePatterns: [],
21
+ recommendations: [RECOMMENDATION],
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Analyze a server config for shell injection risks
27
+ * Called directly by ScanService
28
+ * @param {string} serverName
29
+ * @param {object} config - Server config with command, args, env
30
+ * @returns {Array} Findings
31
+ */
32
+ export function analyzeServerShellRisk(serverName, config) {
33
+ const findings = [];
34
+
35
+ if (!config) {
36
+ return findings;
37
+ }
38
+
39
+ const command = config.command || '';
40
+ const args = config.args || [];
41
+ const argsStr = Array.isArray(args) ? args.join(' ') : String(args);
42
+ const fullCommand = `${command} ${argsStr}`;
43
+
44
+ if (hasShellInterpolation(fullCommand)) {
45
+ findings.push({
46
+ rule_id: RULE_ID,
47
+ severity: 'high',
48
+ owasp_id: OWASP_ID,
49
+ title: `Shell interpolation in ${serverName} command`,
50
+ description: `Server "${serverName}" command contains shell variable interpolation that could be exploited if env vars are attacker-controlled.`,
51
+ recommendation: RECOMMENDATION,
52
+ server_name: serverName,
53
+ confidence: 'probable',
54
+ });
55
+ }
56
+
57
+ if (hasPipeOrChain(fullCommand)) {
58
+ findings.push({
59
+ rule_id: RULE_ID,
60
+ severity: 'critical',
61
+ owasp_id: OWASP_ID,
62
+ title: `Shell pipe/chain in ${serverName} command`,
63
+ description: `Server "${serverName}" command uses shell pipes or chains (|, &&, ;) which are dangerous with shell: true.`,
64
+ recommendation: 'Remove shell operators from server command. Use direct process spawning.',
65
+ server_name: serverName,
66
+ confidence: 'definite',
67
+ });
68
+ }
69
+
70
+ return findings;
71
+ }
72
+
73
+ /**
74
+ * Check for shell variable interpolation patterns
75
+ */
76
+ function hasShellInterpolation(text) {
77
+ return /\$\([^)]+\)/.test(text) || /`[^`]+`/.test(text);
78
+ }
79
+
80
+ /**
81
+ * Check for shell pipe or chain operators
82
+ */
83
+ function hasPipeOrChain(text) {
84
+ return /[|&;]/.test(text) && !/https?:\/\//.test(text);
85
+ }
86
+
87
+ const adapter = createRuleAdapter(scanShellEnvInjection, RULE_ID, OWASP_ID, RECOMMENDATION);
88
+ export const analyzeTool = adapter.analyzeTool;
89
+ export const analyzePrompt = adapter.analyzePrompt;
90
+ export const analyzeResource = adapter.analyzeResource;
91
+ export const analyzePacket = adapter.analyzePacket;
92
+
93
+ export const ruleMetadata = {
94
+ id: RULE_ID,
95
+ name: 'Shell/Environment Injection',
96
+ owasp_id: OWASP_ID,
97
+ severity: 'high',
98
+ description: 'Detects shell interpolation and pipe operators in server commands.',
99
+ source: 'static',
100
+ type: 'general-security',
101
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Unsafe Default Configuration Detection
3
+ * Detects MCP servers running with insecure default settings
4
+ * such as debug mode, verbose logging of secrets, or disabled auth.
5
+ */
6
+ import { createRuleAdapter } from '../utils/adapter.js';
7
+ import { toolToText } from '../utils/text.js';
8
+
9
+ const RULE_ID = 'unsafe-defaults';
10
+ const OWASP_ID = 'MCP07';
11
+ const RECOMMENDATION =
12
+ 'Disable debug mode and verbose logging in production. Enable authentication on all server endpoints.';
13
+
14
+ const UNSAFE_DEFAULT_PATTERNS = [
15
+ { pattern: /debug[=:]\s*true/i, label: 'debug mode enabled' },
16
+ { pattern: /--debug\b/i, label: 'debug flag' },
17
+ { pattern: /NODE_ENV[=:]\s*development/i, label: 'NODE_ENV=development' },
18
+ { pattern: /verbose[=:]\s*true/i, label: 'verbose logging enabled' },
19
+ { pattern: /log[_-]?level[=:]\s*(debug|trace)/i, label: 'debug-level logging' },
20
+ { pattern: /auth[=:]\s*false/i, label: 'authentication disabled' },
21
+ { pattern: /--no-auth\b/i, label: 'no-auth flag' },
22
+ { pattern: /disable[_-]?auth/i, label: 'auth disabled' },
23
+ { pattern: /tls[=:]\s*false/i, label: 'TLS disabled' },
24
+ { pattern: /--insecure\b/i, label: 'insecure flag' },
25
+ ];
26
+
27
+ export function scanUnsafeDefaults(mcpData = {}) {
28
+ const results = {
29
+ toolFindings: [],
30
+ resourceFindings: [],
31
+ promptFindings: [],
32
+ notablePatterns: [],
33
+ recommendations: [RECOMMENDATION],
34
+ };
35
+
36
+ for (const tool of mcpData.tools || []) {
37
+ const text = toolToText(tool);
38
+ for (const { pattern, label } of UNSAFE_DEFAULT_PATTERNS) {
39
+ if (pattern.test(text)) {
40
+ results.toolFindings.push({
41
+ issueType: 'Unsafe Default Configuration',
42
+ name: tool?.name || 'tool',
43
+ severity: 'medium',
44
+ reasons: [
45
+ `Tool "${tool?.name || 'unknown'}" has ${label} — this is insecure for production use.`,
46
+ ],
47
+ tags: ['defaults', 'configuration'],
48
+ safeUseNotes: RECOMMENDATION,
49
+ });
50
+ break;
51
+ }
52
+ }
53
+ }
54
+
55
+ return results;
56
+ }
57
+
58
+ /**
59
+ * Analyze a server config for unsafe defaults
60
+ * Called directly by ScanService
61
+ */
62
+ export function analyzeServerDefaults(serverName, config) {
63
+ const text = JSON.stringify(config || {});
64
+ const findings = [];
65
+
66
+ for (const { pattern, label } of UNSAFE_DEFAULT_PATTERNS) {
67
+ if (pattern.test(text)) {
68
+ findings.push({
69
+ rule_id: RULE_ID,
70
+ severity: 'medium',
71
+ owasp_id: OWASP_ID,
72
+ title: `Unsafe default: ${label} on ${serverName}`,
73
+ description: `${serverName} has ${label}. This may expose sensitive data or weaken security.`,
74
+ recommendation: RECOMMENDATION,
75
+ server_name: serverName,
76
+ confidence: 'probable',
77
+ });
78
+ }
79
+ }
80
+
81
+ return findings;
82
+ }
83
+
84
+ const adapter = createRuleAdapter(scanUnsafeDefaults, RULE_ID, OWASP_ID, RECOMMENDATION);
85
+ export const analyzeTool = adapter.analyzeTool;
86
+ export const analyzePrompt = adapter.analyzePrompt;
87
+ export const analyzeResource = adapter.analyzeResource;
88
+ export const analyzePacket = adapter.analyzePacket;
89
+
90
+ export const ruleMetadata = {
91
+ id: RULE_ID,
92
+ name: 'Unsafe Default Configuration',
93
+ owasp_id: OWASP_ID,
94
+ severity: 'medium',
95
+ description:
96
+ 'Detects servers running with insecure default settings like debug mode or disabled auth.',
97
+ source: 'static',
98
+ type: 'general-security',
99
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Extract MCP tools/list (or equivalent) tool arrays from proxied JSON-RPC traffic.
3
+ */
4
+
5
+ function normalizeTools(tools) {
6
+ if (!Array.isArray(tools)) {
7
+ return null;
8
+ }
9
+ const out = [];
10
+ for (const t of tools) {
11
+ if (!t || typeof t !== 'object') {
12
+ continue;
13
+ }
14
+ const name = t.name;
15
+ if (typeof name !== 'string' || !name) {
16
+ continue;
17
+ }
18
+ out.push({
19
+ name,
20
+ description: typeof t.description === 'string' ? t.description : null,
21
+ });
22
+ }
23
+ return out.length > 0 ? out : null;
24
+ }
25
+
26
+ function parseJson(value) {
27
+ if (value == null) {
28
+ return null;
29
+ }
30
+ if (typeof value === 'object') {
31
+ return value;
32
+ }
33
+ if (typeof value !== 'string' || value === '') {
34
+ return null;
35
+ }
36
+ try {
37
+ return JSON.parse(value);
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Parse stored jsonrpc_result column (stringified `result` object).
45
+ * @param {string|null} jsonrpcResultStr
46
+ * @returns {Array<{name: string, description: string|null}>|null}
47
+ */
48
+ export function toolsFromJsonrpcResultString(jsonrpcResultStr) {
49
+ const obj = parseJson(jsonrpcResultStr);
50
+ if (!obj || !Array.isArray(obj.tools)) {
51
+ return null;
52
+ }
53
+ return normalizeTools(obj.tools);
54
+ }
55
+
56
+ /**
57
+ * Parse live or raw response body for tools/list success.
58
+ * @param {string|object|null|undefined} body
59
+ * @returns {Array<{name: string, description: string|null}>|null}
60
+ */
61
+ export function toolsFromTrafficResponseBody(body) {
62
+ const parsed = parseJson(body);
63
+ if (!parsed || typeof parsed !== 'object') {
64
+ return null;
65
+ }
66
+ if (parsed.result && Array.isArray(parsed.result.tools)) {
67
+ return normalizeTools(parsed.result.tools);
68
+ }
69
+ return null;
70
+ }