@mcp-shark/mcp-shark 1.5.13 → 1.7.2

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 (158) hide show
  1. package/README.md +482 -56
  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 +255 -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/aauth-visibility.json +117 -0
  26. package/core/cli/data/rule-packs/agentic-security-2026.json +180 -0
  27. package/core/cli/data/rule-packs/general-security.json +173 -0
  28. package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
  29. package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
  30. package/core/cli/data/rule-sources.json +5 -0
  31. package/core/cli/data/secret-patterns.json +18 -0
  32. package/core/cli/data/tool-classifications.json +111 -0
  33. package/core/cli/data/toxic-flow-rules.json +47 -0
  34. package/core/cli/index.js +23 -0
  35. package/core/cli/output/Banner.js +52 -0
  36. package/core/cli/output/Formatter.js +183 -0
  37. package/core/cli/output/JsonFormatter.js +106 -0
  38. package/core/cli/output/index.js +16 -0
  39. package/core/cli/secureRegistryFetch.js +157 -0
  40. package/core/cli/symbols.js +16 -0
  41. package/core/configs/environment.js +3 -1
  42. package/core/configs/index.js +3 -64
  43. package/core/container/DependencyContainer.js +4 -1
  44. package/core/mcp-server/index.js +4 -1
  45. package/core/mcp-server/server/external/all.js +10 -3
  46. package/core/mcp-server/server/external/config.js +62 -5
  47. package/core/models/RequestFilters.js +3 -0
  48. package/core/repositories/PacketRepository.js +16 -0
  49. package/core/services/AuditService.js +2 -0
  50. package/core/services/ConfigService.js +9 -1
  51. package/core/services/ConfigTransformService.js +34 -2
  52. package/core/services/RequestService.js +58 -5
  53. package/core/services/ServerManagementService.js +59 -4
  54. package/core/services/security/StaticRulesService.js +69 -13
  55. package/core/services/security/TrafficAnalysisService.js +19 -1
  56. package/core/services/security/TrafficToxicFlowService.js +154 -0
  57. package/core/services/security/aauthGraph.js +199 -0
  58. package/core/services/security/aauthParser.js +274 -0
  59. package/core/services/security/aauthSelfTest.js +346 -0
  60. package/core/services/security/index.js +2 -1
  61. package/core/services/security/rules/index.js +25 -59
  62. package/core/services/security/rules/scans/configPermissions.js +91 -0
  63. package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
  64. package/core/services/security/rules/scans/insecureTransport.js +148 -0
  65. package/core/services/security/rules/scans/missingContainment.js +123 -0
  66. package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
  67. package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
  68. package/core/services/security/toolsListFromTrafficParser.js +70 -0
  69. package/core/tui/App.js +144 -0
  70. package/core/tui/FindingsPanel.js +115 -0
  71. package/core/tui/FixPanel.js +132 -0
  72. package/core/tui/Header.js +51 -0
  73. package/core/tui/HelpBar.js +42 -0
  74. package/core/tui/ServersPanel.js +109 -0
  75. package/core/tui/ToxicFlowsPanel.js +100 -0
  76. package/core/tui/h.js +8 -0
  77. package/core/tui/index.js +11 -0
  78. package/core/tui/render.js +22 -0
  79. package/package.json +24 -16
  80. package/ui/dist/assets/index-D6zDrtMV.js +81 -0
  81. package/ui/dist/index.html +1 -1
  82. package/ui/server/controllers/AauthController.js +279 -0
  83. package/ui/server/controllers/RequestController.js +12 -1
  84. package/ui/server/controllers/SecurityFindingsController.js +46 -1
  85. package/ui/server/routes/aauth.js +18 -0
  86. package/ui/server/routes/requests.js +8 -1
  87. package/ui/server/routes/security.js +5 -1
  88. package/ui/server/setup.js +224 -6
  89. package/ui/server/swagger/paths/components.js +55 -0
  90. package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
  91. package/ui/server/swagger/paths.js +2 -2
  92. package/ui/server/swagger/swagger.js +5 -2
  93. package/ui/server.js +1 -1
  94. package/ui/src/App.jsx +26 -52
  95. package/ui/src/PacketFilters.jsx +31 -1
  96. package/ui/src/PacketList.jsx +2 -2
  97. package/ui/src/Security.jsx +10 -0
  98. package/ui/src/TabNavigation.jsx +8 -0
  99. package/ui/src/components/AAuthBadge.jsx +92 -0
  100. package/ui/src/components/AauthExplorer/AauthExplorerGraph.jsx +231 -0
  101. package/ui/src/components/AauthExplorer/AauthExplorerView.jsx +387 -0
  102. package/ui/src/components/AauthExplorer/NodeDetailPanel.jsx +272 -0
  103. package/ui/src/components/App/ActionMenu.jsx +4 -31
  104. package/ui/src/components/App/ApiDocsButton.jsx +0 -1
  105. package/ui/src/components/App/ShutdownButton.jsx +0 -1
  106. package/ui/src/components/App/useAppState.js +19 -26
  107. package/ui/src/components/DetailsTab/AAuthIdentitySection.jsx +119 -0
  108. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +2 -0
  109. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +2 -0
  110. package/ui/src/components/DetectedPathsList.jsx +1 -5
  111. package/ui/src/components/FileInput.jsx +0 -1
  112. package/ui/src/components/PacketFilters/AAuthPostureFilter.jsx +81 -0
  113. package/ui/src/components/RequestRow/RequestRowMain.jsx +7 -1
  114. package/ui/src/components/Security/AAuthPosturePanel.jsx +360 -0
  115. package/ui/src/components/Security/ScannerContent.jsx +33 -1
  116. package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
  117. package/ui/src/components/Security/securityApi.js +15 -0
  118. package/ui/src/components/Security/useSecurity.js +60 -3
  119. package/ui/src/components/ServerControl.jsx +0 -1
  120. package/ui/src/components/TabNavigation/DesktopTabs.jsx +0 -11
  121. package/ui/src/components/TabNavigationIcons.jsx +5 -0
  122. package/ui/src/components/ViewModeTabs.jsx +0 -1
  123. package/ui/src/utils/animations.js +26 -9
  124. package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
  125. package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
  126. package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
  127. package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
  128. package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
  129. package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
  130. package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
  131. package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
  132. package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
  133. package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
  134. package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
  135. package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
  136. package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
  137. package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
  138. package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
  139. package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
  140. package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
  141. package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
  142. package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
  143. package/ui/dist/assets/index-CiCSDYf-.js +0 -97
  144. package/ui/server/routes/help.js +0 -44
  145. package/ui/server/swagger/paths/help.js +0 -82
  146. package/ui/src/HelpGuide/HelpGuideContent.jsx +0 -118
  147. package/ui/src/HelpGuide/HelpGuideFooter.jsx +0 -59
  148. package/ui/src/HelpGuide/HelpGuideHeader.jsx +0 -57
  149. package/ui/src/HelpGuide.jsx +0 -78
  150. package/ui/src/IntroTour.jsx +0 -154
  151. package/ui/src/components/App/HelpButton.jsx +0 -90
  152. package/ui/src/components/TourOverlay.jsx +0 -117
  153. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +0 -120
  154. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +0 -71
  155. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +0 -54
  156. package/ui/src/components/TourTooltip/useTooltipPosition.js +0 -135
  157. package/ui/src/components/TourTooltip.jsx +0 -91
  158. package/ui/src/config/tourSteps.jsx +0 -140
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Data Loader
3
+ * Loads built-in JSON data files and merges with user YAML overrides
4
+ * from .mcp-shark/ directory. Keeps hardcoded values in config, not code.
5
+ */
6
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+
9
+ const DATA_DIR = join(import.meta.dirname, 'data');
10
+ const USER_CONFIG_DIR = '.mcp-shark';
11
+
12
+ /**
13
+ * Load a built-in JSON data file from core/cli/data/
14
+ * @param {string} filename - e.g. 'secret-patterns.json'
15
+ * @returns {any} Parsed JSON content
16
+ */
17
+ export function loadBuiltinJson(filename) {
18
+ const filePath = join(DATA_DIR, filename);
19
+ const content = readFileSync(filePath, 'utf-8');
20
+ return JSON.parse(content);
21
+ }
22
+
23
+ /**
24
+ * Load user YAML overrides from .mcp-shark/<filename> relative to cwd
25
+ * Returns null if file does not exist.
26
+ * @param {string} filename - e.g. 'secrets.yaml'
27
+ * @returns {string|null} Raw file content or null
28
+ */
29
+ function loadUserYamlContent(filename) {
30
+ const filePath = join(process.cwd(), USER_CONFIG_DIR, filename);
31
+ if (!existsSync(filePath)) {
32
+ return null;
33
+ }
34
+ return readFileSync(filePath, 'utf-8');
35
+ }
36
+
37
+ /**
38
+ * Load user overrides as a list of objects (for secrets, flows)
39
+ * Expects YAML format:
40
+ * - pattern: "^foo"
41
+ * name: "Foo Key"
42
+ * severity: high
43
+ * @param {string} filename
44
+ * @returns {Array<object>}
45
+ */
46
+ export function loadUserYamlList(filename) {
47
+ const content = loadUserYamlContent(filename);
48
+ if (!content) {
49
+ return [];
50
+ }
51
+ return parseYamlList(content);
52
+ }
53
+
54
+ /**
55
+ * Collect `toxic_flow_rules` arrays from rule-pack JSON files in a directory
56
+ * (built-in `core/cli/data/rule-packs` and/or `.mcp-shark/rule-packs`).
57
+ * @param {string} dirPath absolute or cwd-relative directory
58
+ * @returns {Array<object>}
59
+ */
60
+ export function loadToxicFlowRulesFromPacksDir(dirPath) {
61
+ if (!existsSync(dirPath)) {
62
+ return [];
63
+ }
64
+ const out = [];
65
+ for (const file of readdirSync(dirPath)) {
66
+ if (!file.endsWith('.json')) {
67
+ continue;
68
+ }
69
+ try {
70
+ const pack = JSON.parse(readFileSync(join(dirPath, file), 'utf-8'));
71
+ if (!pack?.schema_version || !Array.isArray(pack.rules)) {
72
+ continue;
73
+ }
74
+ const extra = pack.toxic_flow_rules;
75
+ if (Array.isArray(extra)) {
76
+ out.push(...extra);
77
+ }
78
+ } catch {
79
+ // skip malformed pack files
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+
85
+ /**
86
+ * Load user overrides as a nested map (for classifications)
87
+ * Expects YAML format:
88
+ * mcp-server-notion:
89
+ * read_page: ingests_untrusted
90
+ * @param {string} filename
91
+ * @returns {object}
92
+ */
93
+ export function loadUserYamlMap(filename) {
94
+ const content = loadUserYamlContent(filename);
95
+ if (!content) {
96
+ return {};
97
+ }
98
+ return parseYamlNestedMap(content);
99
+ }
100
+
101
+ /**
102
+ * Parse YAML list of objects.
103
+ * Each item starts with "- key: value" and subsequent keys at +2 indent.
104
+ */
105
+ function parseYamlList(content) {
106
+ const items = [];
107
+ let current = null;
108
+
109
+ for (const rawLine of content.split('\n')) {
110
+ const line = stripComment(rawLine);
111
+ if (!line.trim()) {
112
+ continue;
113
+ }
114
+
115
+ const listMatch = line.match(/^- (\w[\w_]*)\s*:\s*(.*)$/);
116
+ if (listMatch) {
117
+ if (current) {
118
+ items.push(current);
119
+ }
120
+ current = {};
121
+ current[listMatch[1]] = unquote(listMatch[2]);
122
+ continue;
123
+ }
124
+
125
+ const propMatch = line.match(/^\s{2,}(\w[\w_]*)\s*:\s*(.*)$/);
126
+ if (propMatch && current) {
127
+ current[propMatch[1]] = unquote(propMatch[2]);
128
+ }
129
+ }
130
+
131
+ if (current) {
132
+ items.push(current);
133
+ }
134
+
135
+ return items;
136
+ }
137
+
138
+ /**
139
+ * Parse YAML nested map (two levels deep).
140
+ * top_key:
141
+ * sub_key: value
142
+ */
143
+ function parseYamlNestedMap(content) {
144
+ const result = {};
145
+ let currentSection = null;
146
+
147
+ for (const rawLine of content.split('\n')) {
148
+ const line = stripComment(rawLine);
149
+ if (!line.trim()) {
150
+ continue;
151
+ }
152
+
153
+ const indent = line.length - line.trimStart().length;
154
+ const trimmed = line.trim();
155
+ const colonIdx = trimmed.indexOf(':');
156
+ if (colonIdx === -1) {
157
+ continue;
158
+ }
159
+
160
+ const key = trimmed.slice(0, colonIdx).trim();
161
+ const value = trimmed.slice(colonIdx + 1).trim();
162
+
163
+ if (indent === 0) {
164
+ if (value) {
165
+ result[key] = unquote(value);
166
+ } else {
167
+ result[key] = {};
168
+ currentSection = key;
169
+ }
170
+ } else if (currentSection && typeof result[currentSection] === 'object') {
171
+ result[currentSection][key] = unquote(value);
172
+ }
173
+ }
174
+
175
+ return result;
176
+ }
177
+
178
+ /**
179
+ * Remove inline YAML comments (respecting quoted strings)
180
+ */
181
+ function stripComment(line) {
182
+ const trimmed = line.trimEnd();
183
+ const hashIdx = trimmed.indexOf(' #');
184
+ if (hashIdx === -1) {
185
+ return trimmed;
186
+ }
187
+ const beforeHash = trimmed.slice(0, hashIdx);
188
+ const quoteCount = (beforeHash.match(/"/g) || []).length;
189
+ if (quoteCount % 2 === 0) {
190
+ return beforeHash;
191
+ }
192
+ return trimmed;
193
+ }
194
+
195
+ /**
196
+ * Remove surrounding quotes from a YAML value
197
+ */
198
+ function unquote(value) {
199
+ return value.replace(/^["']|["']$/g, '');
200
+ }
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Declarative Rule Engine
3
+ * Loads JSON rule packs and compiles them into the same analyzeTool/analyzePrompt/
4
+ * analyzeResource/analyzePacket interface that JS rule plugins export.
5
+ *
6
+ * Rule packs are loaded from:
7
+ * 1. Built-in: core/cli/data/rule-packs/*.json (shipped with package)
8
+ * 2. User: .mcp-shark/rule-packs/*.json (local overrides / downloads)
9
+ *
10
+ * This allows OWASP 2027+ and new vulnerability catalogs to be added
11
+ * without writing any JavaScript.
12
+ *
13
+ * Options: pass `{ builtinOnly: true }` to load only shipped packs (tests avoid cwd overrides).
14
+ */
15
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import {
18
+ packetToText,
19
+ promptToText,
20
+ resourceToText,
21
+ toolToText,
22
+ } from '#core/services/security/rules/utils/text.js';
23
+
24
+ const BUILTIN_PACKS_DIR = join(import.meta.dirname, 'data', 'rule-packs');
25
+ const USER_PACKS_DIR = join(process.cwd(), '.mcp-shark', 'rule-packs');
26
+
27
+ /**
28
+ * Load all rule packs and compile them into rule objects.
29
+ * Returns an array of rule objects with the same shape as JS rule plugins:
30
+ * { ruleMetadata, analyzeTool, analyzePrompt, analyzeResource, analyzePacket }
31
+ *
32
+ * @param {{ builtinOnly?: boolean }} [options]
33
+ * @returns {Array<object>}
34
+ */
35
+ export function loadDeclarativeRules(options = {}) {
36
+ const packs = options.builtinOnly ? loadPacksFromDir(BUILTIN_PACKS_DIR) : loadAllPacks();
37
+ const rules = [];
38
+
39
+ for (const pack of packs) {
40
+ const packRules = Array.isArray(pack.rules) ? pack.rules : [];
41
+ for (const ruleDef of packRules) {
42
+ const compiled = compileRule(ruleDef);
43
+ if (compiled) {
44
+ rules.push(compiled);
45
+ }
46
+ }
47
+ }
48
+
49
+ return deduplicateRules(rules);
50
+ }
51
+
52
+ /**
53
+ * Load JSON packs from built-in and user directories.
54
+ * User packs with the same pack_id override built-in packs.
55
+ */
56
+ function loadAllPacks() {
57
+ const builtinPacks = loadPacksFromDir(BUILTIN_PACKS_DIR);
58
+ const userPacks = loadPacksFromDir(USER_PACKS_DIR);
59
+
60
+ const packMap = new Map();
61
+ for (const pack of builtinPacks) {
62
+ packMap.set(pack.pack_id, pack);
63
+ }
64
+ for (const pack of userPacks) {
65
+ packMap.set(pack.pack_id, pack);
66
+ }
67
+
68
+ return [...packMap.values()];
69
+ }
70
+
71
+ /**
72
+ * Load all .json rule pack files from a directory.
73
+ */
74
+ function loadPacksFromDir(dirPath) {
75
+ if (!existsSync(dirPath)) {
76
+ return [];
77
+ }
78
+
79
+ const files = readdirSync(dirPath).filter((f) => f.endsWith('.json'));
80
+ const packs = [];
81
+
82
+ for (const file of files) {
83
+ try {
84
+ const content = readFileSync(join(dirPath, file), 'utf-8');
85
+ const pack = JSON.parse(content);
86
+ if (pack.schema_version && Array.isArray(pack.rules)) {
87
+ packs.push(pack);
88
+ }
89
+ } catch (_err) {
90
+ // skip malformed pack files
91
+ }
92
+ }
93
+
94
+ return packs;
95
+ }
96
+
97
+ /**
98
+ * Compile a single declarative rule definition into a rule object.
99
+ */
100
+ function compileRule(ruleDef) {
101
+ const patterns = compilePatterns(ruleDef.patterns || []);
102
+ const excludePatterns = compilePatterns(ruleDef.exclude_patterns || []);
103
+ const toolNamePatterns = compileToolNamePatterns(ruleDef.tool_name_patterns || []);
104
+ const paramPatterns = compilePatterns(ruleDef.param_patterns || []);
105
+ const escalationPatterns = compilePatterns(ruleDef.severity_escalation_patterns || []);
106
+ const scope = new Set(ruleDef.scope || ['tool', 'prompt', 'resource', 'packet']);
107
+ const matchMode = ruleDef.match_mode || 'all_matches';
108
+ const severityOverrides = ruleDef.severity_overrides || {};
109
+ const textField = ruleDef.text_field || null;
110
+
111
+ const ruleMetadata = {
112
+ id: ruleDef.id,
113
+ name: ruleDef.name,
114
+ owasp_id: ruleDef.owasp_id,
115
+ severity: ruleDef.severity,
116
+ description: ruleDef.description,
117
+ source: 'declarative',
118
+ type: ruleDef.type,
119
+ };
120
+
121
+ const ctx = {
122
+ ruleDef,
123
+ patterns,
124
+ excludePatterns,
125
+ toolNamePatterns,
126
+ paramPatterns,
127
+ escalationPatterns,
128
+ scope,
129
+ matchMode,
130
+ severityOverrides,
131
+ textField,
132
+ ruleMetadata,
133
+ };
134
+
135
+ return {
136
+ ruleMetadata,
137
+ analyzeTool: scope.has('tool') ? (tool) => analyzeEntity(ctx, 'tool', tool, toolToText) : noOp,
138
+ analyzePrompt: scope.has('prompt')
139
+ ? (prompt) => analyzeEntity(ctx, 'prompt', prompt, promptToText)
140
+ : noOp,
141
+ analyzeResource: scope.has('resource')
142
+ ? (resource) => analyzeEntity(ctx, 'resource', resource, resourceToText)
143
+ : noOp,
144
+ analyzePacket: scope.has('packet')
145
+ ? (packet) => analyzeEntity(ctx, 'packet', packet, packetToText)
146
+ : noOp,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Analyze a single entity against a compiled rule's patterns.
152
+ */
153
+ function analyzeEntity(ctx, entityType, entity, textFn) {
154
+ const text =
155
+ ctx.textField && entityType === 'tool' ? entity?.[ctx.textField] || '' : textFn(entity);
156
+
157
+ if (!text) {
158
+ return [];
159
+ }
160
+
161
+ if (hasExcludeMatch(ctx.excludePatterns, text)) {
162
+ return [];
163
+ }
164
+
165
+ const findings = [];
166
+ const severity = ctx.severityOverrides[entityType] || ctx.ruleDef.severity;
167
+ const entityName = entity?.name || entity?.uri || entityType;
168
+
169
+ if (entityType === 'tool' && ctx.toolNamePatterns.length > 0) {
170
+ const toolName = entity?.name || '';
171
+ const nameFindings = matchToolName(ctx, toolName, entityName);
172
+ findings.push(...nameFindings);
173
+ }
174
+
175
+ if (entityType === 'tool' && ctx.paramPatterns.length > 0) {
176
+ const paramFindings = matchParams(ctx, entity, entityName);
177
+ findings.push(...paramFindings);
178
+ }
179
+
180
+ const textFindings = matchText(ctx, text, entityType, entityName, severity);
181
+ findings.push(...textFindings);
182
+
183
+ if (findings.length > 0 && ctx.escalationPatterns.length > 0) {
184
+ applyEscalation(ctx.escalationPatterns, text, findings);
185
+ }
186
+
187
+ return findings;
188
+ }
189
+
190
+ /**
191
+ * Match text against the rule's main patterns.
192
+ */
193
+ function matchText(ctx, text, entityType, entityName, severity) {
194
+ const { patterns, matchMode, ruleDef } = ctx;
195
+ const matched = [];
196
+
197
+ for (const p of patterns) {
198
+ p.regex.lastIndex = 0;
199
+ const match = text.match(p.regex);
200
+ if (match) {
201
+ matched.push({ snippet: match[0], label: p.label });
202
+ if (matchMode === 'first') {
203
+ break;
204
+ }
205
+ }
206
+ }
207
+
208
+ if (matched.length === 0) {
209
+ return [];
210
+ }
211
+
212
+ const evidence = matched.map((m) => m.label || m.snippet);
213
+ const description = `${ruleDef.name} detected in ${entityType} "${entityName}": ${evidence.join(', ')}`;
214
+
215
+ return [buildFinding(ctx, severity, entityName, description, entityType)];
216
+ }
217
+
218
+ /**
219
+ * Match tool name against tool_name_patterns.
220
+ */
221
+ function matchToolName(ctx, toolName, entityName) {
222
+ const findings = [];
223
+ for (const p of ctx.toolNamePatterns) {
224
+ p.regex.lastIndex = 0;
225
+ if (p.regex.test(toolName)) {
226
+ const description = `Suspicious tool name "${toolName}" matches ${p.label}`;
227
+ findings.push(
228
+ buildFinding(ctx, p.severity || ctx.ruleDef.severity, entityName, description, 'tool')
229
+ );
230
+ break;
231
+ }
232
+ }
233
+ return findings;
234
+ }
235
+
236
+ /**
237
+ * Match tool parameter names against param_patterns.
238
+ */
239
+ function matchParams(ctx, tool, entityName) {
240
+ const schema = tool?.input_schema || tool?.inputSchema || {};
241
+ const props = schema.properties || {};
242
+ const findings = [];
243
+
244
+ for (const [paramName, paramDef] of Object.entries(props)) {
245
+ for (const p of ctx.paramPatterns) {
246
+ p.regex.lastIndex = 0;
247
+ if (p.regex.test(paramName)) {
248
+ const desc = (paramDef?.description || '').toLowerCase();
249
+ const hasGuard =
250
+ desc.includes('relative') || desc.includes('within') || desc.includes('allowed');
251
+ if (!hasGuard) {
252
+ const description = `Unsanitized ${p.label} "${paramName}" in tool "${entityName}"`;
253
+ findings.push(buildFinding(ctx, 'medium', entityName, description, 'tool'));
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ return findings;
260
+ }
261
+
262
+ /**
263
+ * Check if any exclude pattern matches the text.
264
+ */
265
+ function hasExcludeMatch(excludePatterns, text) {
266
+ for (const p of excludePatterns) {
267
+ p.regex.lastIndex = 0;
268
+ if (p.regex.test(text)) {
269
+ return true;
270
+ }
271
+ }
272
+ return false;
273
+ }
274
+
275
+ /**
276
+ * Apply severity escalation: if any escalation pattern matches,
277
+ * upgrade all findings to the escalation severity.
278
+ */
279
+ function applyEscalation(escalationPatterns, text, findings) {
280
+ for (const p of escalationPatterns) {
281
+ p.regex.lastIndex = 0;
282
+ if (p.regex.test(text)) {
283
+ for (const f of findings) {
284
+ f.severity = p.severity;
285
+ }
286
+ break;
287
+ }
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Build a single finding in mcp-shark format.
293
+ */
294
+ function buildFinding(ctx, severity, entityName, description, targetType) {
295
+ return {
296
+ rule_id: ctx.ruleDef.id,
297
+ severity,
298
+ owasp_id: ctx.ruleDef.owasp_id,
299
+ title: `${ctx.ruleDef.name}: ${entityName}`,
300
+ description,
301
+ evidence: entityName,
302
+ recommendation: ctx.ruleDef.recommendation,
303
+ target_type: targetType,
304
+ target_name: entityName,
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Compile pattern entries (string or {regex, label, flags}) to RegExp objects.
310
+ */
311
+ function compilePatterns(entries) {
312
+ const compiled = [];
313
+ for (const entry of entries) {
314
+ try {
315
+ if (typeof entry === 'string') {
316
+ compiled.push({ regex: new RegExp(entry, 'i'), label: null });
317
+ } else {
318
+ const flags = entry.flags !== undefined ? entry.flags : 'i';
319
+ compiled.push({ regex: new RegExp(entry.regex, flags), label: entry.label || null });
320
+ }
321
+ } catch (_err) {
322
+ // skip malformed patterns
323
+ }
324
+ }
325
+ return compiled;
326
+ }
327
+
328
+ /**
329
+ * Compile tool name patterns with their per-pattern severity.
330
+ */
331
+ function compileToolNamePatterns(entries) {
332
+ const compiled = [];
333
+ for (const entry of entries) {
334
+ try {
335
+ compiled.push({
336
+ regex: new RegExp(entry.regex, 'i'),
337
+ label: entry.label || null,
338
+ severity: entry.severity || null,
339
+ });
340
+ } catch (_err) {
341
+ // skip malformed patterns
342
+ }
343
+ }
344
+ return compiled;
345
+ }
346
+
347
+ /**
348
+ * Deduplicate rules by ID (later packs / user overrides win).
349
+ */
350
+ function deduplicateRules(rules) {
351
+ const map = new Map();
352
+ for (const rule of rules) {
353
+ map.set(rule.ruleMetadata.id, rule);
354
+ }
355
+ return [...map.values()];
356
+ }
357
+
358
+ /**
359
+ * No-op for out-of-scope entity types.
360
+ */
361
+ function noOp() {
362
+ return [];
363
+ }