@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.
- package/README.md +482 -56
- package/bin/mcp-shark.js +146 -52
- package/core/cli/AutoFixEngine.js +93 -0
- package/core/cli/ConfigScanner.js +193 -0
- package/core/cli/DataLoader.js +200 -0
- package/core/cli/DeclarativeRuleEngine.js +363 -0
- package/core/cli/DoctorCommand.js +218 -0
- package/core/cli/FixHandlers.js +222 -0
- package/core/cli/HtmlReportGenerator.js +203 -0
- package/core/cli/IdeConfigPaths.js +175 -0
- package/core/cli/ListCommand.js +255 -0
- package/core/cli/LockCommand.js +164 -0
- package/core/cli/LockDiffEngine.js +152 -0
- package/core/cli/RuleRegistryConfig.js +131 -0
- package/core/cli/ScanCommand.js +244 -0
- package/core/cli/ScanService.js +200 -0
- package/core/cli/SecretDetector.js +92 -0
- package/core/cli/SharkScoreCalculator.js +109 -0
- package/core/cli/ToolClassifications.js +51 -0
- package/core/cli/ToxicFlowAnalyzer.js +212 -0
- package/core/cli/UpdateCommand.js +188 -0
- package/core/cli/WalkthroughGenerator.js +195 -0
- package/core/cli/WatchCommand.js +129 -0
- package/core/cli/YamlRuleEngine.js +197 -0
- package/core/cli/data/rule-packs/aauth-visibility.json +117 -0
- package/core/cli/data/rule-packs/agentic-security-2026.json +180 -0
- package/core/cli/data/rule-packs/general-security.json +173 -0
- package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
- package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
- package/core/cli/data/rule-sources.json +5 -0
- package/core/cli/data/secret-patterns.json +18 -0
- package/core/cli/data/tool-classifications.json +111 -0
- package/core/cli/data/toxic-flow-rules.json +47 -0
- package/core/cli/index.js +23 -0
- package/core/cli/output/Banner.js +52 -0
- package/core/cli/output/Formatter.js +183 -0
- package/core/cli/output/JsonFormatter.js +106 -0
- package/core/cli/output/index.js +16 -0
- package/core/cli/secureRegistryFetch.js +157 -0
- package/core/cli/symbols.js +16 -0
- package/core/configs/environment.js +3 -1
- package/core/configs/index.js +3 -64
- package/core/container/DependencyContainer.js +4 -1
- package/core/mcp-server/index.js +4 -1
- package/core/mcp-server/server/external/all.js +10 -3
- package/core/mcp-server/server/external/config.js +62 -5
- package/core/models/RequestFilters.js +3 -0
- package/core/repositories/PacketRepository.js +16 -0
- package/core/services/AuditService.js +2 -0
- package/core/services/ConfigService.js +9 -1
- package/core/services/ConfigTransformService.js +34 -2
- package/core/services/RequestService.js +58 -5
- package/core/services/ServerManagementService.js +59 -4
- package/core/services/security/StaticRulesService.js +69 -13
- package/core/services/security/TrafficAnalysisService.js +19 -1
- package/core/services/security/TrafficToxicFlowService.js +154 -0
- package/core/services/security/aauthGraph.js +199 -0
- package/core/services/security/aauthParser.js +274 -0
- package/core/services/security/aauthSelfTest.js +346 -0
- package/core/services/security/index.js +2 -1
- package/core/services/security/rules/index.js +25 -59
- package/core/services/security/rules/scans/configPermissions.js +91 -0
- package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
- package/core/services/security/rules/scans/insecureTransport.js +148 -0
- package/core/services/security/rules/scans/missingContainment.js +123 -0
- package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
- package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
- package/core/services/security/toolsListFromTrafficParser.js +70 -0
- package/core/tui/App.js +144 -0
- package/core/tui/FindingsPanel.js +115 -0
- package/core/tui/FixPanel.js +132 -0
- package/core/tui/Header.js +51 -0
- package/core/tui/HelpBar.js +42 -0
- package/core/tui/ServersPanel.js +109 -0
- package/core/tui/ToxicFlowsPanel.js +100 -0
- package/core/tui/h.js +8 -0
- package/core/tui/index.js +11 -0
- package/core/tui/render.js +22 -0
- package/package.json +24 -16
- package/ui/dist/assets/index-D6zDrtMV.js +81 -0
- package/ui/dist/index.html +1 -1
- package/ui/server/controllers/AauthController.js +279 -0
- package/ui/server/controllers/RequestController.js +12 -1
- package/ui/server/controllers/SecurityFindingsController.js +46 -1
- package/ui/server/routes/aauth.js +18 -0
- package/ui/server/routes/requests.js +8 -1
- package/ui/server/routes/security.js +5 -1
- package/ui/server/setup.js +224 -6
- package/ui/server/swagger/paths/components.js +55 -0
- package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
- package/ui/server/swagger/paths.js +2 -2
- package/ui/server/swagger/swagger.js +5 -2
- package/ui/server.js +1 -1
- package/ui/src/App.jsx +26 -52
- package/ui/src/PacketFilters.jsx +31 -1
- package/ui/src/PacketList.jsx +2 -2
- package/ui/src/Security.jsx +10 -0
- package/ui/src/TabNavigation.jsx +8 -0
- package/ui/src/components/AAuthBadge.jsx +92 -0
- package/ui/src/components/AauthExplorer/AauthExplorerGraph.jsx +231 -0
- package/ui/src/components/AauthExplorer/AauthExplorerView.jsx +387 -0
- package/ui/src/components/AauthExplorer/NodeDetailPanel.jsx +272 -0
- package/ui/src/components/App/ActionMenu.jsx +4 -31
- package/ui/src/components/App/ApiDocsButton.jsx +0 -1
- package/ui/src/components/App/ShutdownButton.jsx +0 -1
- package/ui/src/components/App/useAppState.js +19 -26
- package/ui/src/components/DetailsTab/AAuthIdentitySection.jsx +119 -0
- package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +2 -0
- package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +2 -0
- package/ui/src/components/DetectedPathsList.jsx +1 -5
- package/ui/src/components/FileInput.jsx +0 -1
- package/ui/src/components/PacketFilters/AAuthPostureFilter.jsx +81 -0
- package/ui/src/components/RequestRow/RequestRowMain.jsx +7 -1
- package/ui/src/components/Security/AAuthPosturePanel.jsx +360 -0
- package/ui/src/components/Security/ScannerContent.jsx +33 -1
- package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
- package/ui/src/components/Security/securityApi.js +15 -0
- package/ui/src/components/Security/useSecurity.js +60 -3
- package/ui/src/components/ServerControl.jsx +0 -1
- package/ui/src/components/TabNavigation/DesktopTabs.jsx +0 -11
- package/ui/src/components/TabNavigationIcons.jsx +5 -0
- package/ui/src/components/ViewModeTabs.jsx +0 -1
- package/ui/src/utils/animations.js +26 -9
- package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
- package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
- package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
- package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
- package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
- package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
- package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
- package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
- package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
- package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
- package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
- package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
- package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
- package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
- package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
- package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
- package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
- package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
- package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
- package/ui/dist/assets/index-CiCSDYf-.js +0 -97
- package/ui/server/routes/help.js +0 -44
- package/ui/server/swagger/paths/help.js +0 -82
- package/ui/src/HelpGuide/HelpGuideContent.jsx +0 -118
- package/ui/src/HelpGuide/HelpGuideFooter.jsx +0 -59
- package/ui/src/HelpGuide/HelpGuideHeader.jsx +0 -57
- package/ui/src/HelpGuide.jsx +0 -78
- package/ui/src/IntroTour.jsx +0 -154
- package/ui/src/components/App/HelpButton.jsx +0 -90
- package/ui/src/components/TourOverlay.jsx +0 -117
- package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +0 -120
- package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +0 -71
- package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +0 -54
- package/ui/src/components/TourTooltip/useTooltipPosition.js +0 -135
- package/ui/src/components/TourTooltip.jsx +0 -91
- 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
|
+
}
|