@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
|
+
import { StaticRulesService } from '#core/services/security/StaticRulesService.js';
|
|
2
|
+
/**
|
|
3
|
+
* Scan Service
|
|
4
|
+
* Orchestrates: config detection → rule analysis → toxic flows → shark score
|
|
5
|
+
* Pure business logic, no HTTP knowledge, no CLI formatting.
|
|
6
|
+
*
|
|
7
|
+
* Server-level checks are registry-driven — add a new check by
|
|
8
|
+
* appending to SERVER_CONFIG_CHECKS, no other edits needed.
|
|
9
|
+
*/
|
|
10
|
+
import { analyzeConfigPermissions } from '#core/services/security/rules/scans/configPermissions.js';
|
|
11
|
+
import { analyzeAllServerToolNames } from '#core/services/security/rules/scans/duplicateToolNames.js';
|
|
12
|
+
import { analyzeServerTransport } from '#core/services/security/rules/scans/insecureTransport.js';
|
|
13
|
+
import { analyzeServerContainment } from '#core/services/security/rules/scans/missingContainment.js';
|
|
14
|
+
import { analyzeServerShellRisk } from '#core/services/security/rules/scans/shellEnvInjection.js';
|
|
15
|
+
import { analyzeServerDefaults } from '#core/services/security/rules/scans/unsafeDefaults.js';
|
|
16
|
+
import { getAllServers, scanIdeConfigs } from './ConfigScanner.js';
|
|
17
|
+
import { detectHardcodedSecrets } from './SecretDetector.js';
|
|
18
|
+
import { calculateSharkScore, countBySeverity } from './SharkScoreCalculator.js';
|
|
19
|
+
import { analyzeToxicFlows } from './ToxicFlowAnalyzer.js';
|
|
20
|
+
import { applyYamlRules, loadYamlRules } from './YamlRuleEngine.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Registry of server-level config checks.
|
|
24
|
+
* Each entry takes (serverName, config) and returns Finding[].
|
|
25
|
+
* To add a new check: import it and append here.
|
|
26
|
+
*/
|
|
27
|
+
const SERVER_CONFIG_CHECKS = [
|
|
28
|
+
analyzeServerContainment,
|
|
29
|
+
analyzeServerShellRisk,
|
|
30
|
+
analyzeServerTransport,
|
|
31
|
+
analyzeServerDefaults,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run a full security scan on all detected MCP configurations
|
|
36
|
+
* @param {object} options
|
|
37
|
+
* @param {string} [options.ide] - Filter to specific IDE
|
|
38
|
+
* @param {boolean} [options.strict] - Count advisory findings in score
|
|
39
|
+
* @param {string} [options.rulesPath] - Path to custom YAML rules
|
|
40
|
+
* @returns {object} Complete scan result
|
|
41
|
+
*/
|
|
42
|
+
export function runScan(options = {}) {
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
|
|
45
|
+
const ideResults = scanIdeConfigs({ ide: options.ide });
|
|
46
|
+
const servers = getAllServers(ideResults);
|
|
47
|
+
|
|
48
|
+
const rulesService = new StaticRulesService(null);
|
|
49
|
+
const ruleMetadata = rulesService.getRuleMetadata();
|
|
50
|
+
|
|
51
|
+
const allFindings = analyzeAllServers(servers, rulesService, ideResults);
|
|
52
|
+
|
|
53
|
+
const yamlRules = loadYamlRules(options.rulesPath);
|
|
54
|
+
if (yamlRules.length > 0) {
|
|
55
|
+
const yamlFindings = applyYamlRules(yamlRules, servers);
|
|
56
|
+
allFindings.push(...yamlFindings);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const toxicFlows = analyzeToxicFlows(servers);
|
|
60
|
+
|
|
61
|
+
const scorableFindings = options.strict
|
|
62
|
+
? allFindings
|
|
63
|
+
: allFindings.filter((f) => f.confidence !== 'possible');
|
|
64
|
+
|
|
65
|
+
const scoreResult = calculateSharkScore(scorableFindings, toxicFlows);
|
|
66
|
+
const severityCounts = countBySeverity(allFindings);
|
|
67
|
+
const elapsedMs = Date.now() - startTime;
|
|
68
|
+
|
|
69
|
+
const totalToolCount = servers.reduce(
|
|
70
|
+
(sum, s) => sum + (Array.isArray(s.tools) ? s.tools.length : 0),
|
|
71
|
+
0
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
ideResults,
|
|
76
|
+
servers,
|
|
77
|
+
findings: allFindings,
|
|
78
|
+
findingsByServer: groupFindingsByServer(allFindings),
|
|
79
|
+
toxicFlows,
|
|
80
|
+
scoreResult,
|
|
81
|
+
severityCounts,
|
|
82
|
+
ruleCount: ruleMetadata.length,
|
|
83
|
+
totalToolCount,
|
|
84
|
+
serverCount: servers.length,
|
|
85
|
+
elapsedMs,
|
|
86
|
+
cleanServers: getCleanServers(servers, allFindings),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Analyze all servers with the static rules engine + config-level checks
|
|
92
|
+
*/
|
|
93
|
+
function analyzeAllServers(servers, rulesService, ideResults) {
|
|
94
|
+
const findings = [];
|
|
95
|
+
|
|
96
|
+
for (const server of servers) {
|
|
97
|
+
const serverFindings = analyzeServer(server, rulesService);
|
|
98
|
+
findings.push(...serverFindings);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const dupFindings = analyzeAllServerToolNames(servers);
|
|
102
|
+
findings.push(...dupFindings);
|
|
103
|
+
|
|
104
|
+
const permFindings = analyzeIdePermissions(ideResults);
|
|
105
|
+
findings.push(...permFindings);
|
|
106
|
+
|
|
107
|
+
return findings;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check permissions on all found IDE config files
|
|
112
|
+
*/
|
|
113
|
+
function analyzeIdePermissions(ideResults) {
|
|
114
|
+
const findings = [];
|
|
115
|
+
for (const ide of ideResults) {
|
|
116
|
+
if (!ide.found || !ide.permissions) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const permFindings = analyzeConfigPermissions(ide.configPath, ide.permissions);
|
|
120
|
+
findings.push(...permFindings);
|
|
121
|
+
}
|
|
122
|
+
return findings;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Analyze a single server's tools with the rules engine
|
|
127
|
+
*/
|
|
128
|
+
function analyzeServer(server, rulesService) {
|
|
129
|
+
const findings = [];
|
|
130
|
+
|
|
131
|
+
const configFindings = runServerConfigChecks(server);
|
|
132
|
+
findings.push(...configFindings);
|
|
133
|
+
|
|
134
|
+
if (Array.isArray(server.tools)) {
|
|
135
|
+
for (const tool of server.tools) {
|
|
136
|
+
const toolObj = typeof tool === 'string' ? { name: tool } : tool;
|
|
137
|
+
const toolFindings = rulesService.analyzeTool(toolObj, server.name);
|
|
138
|
+
for (const f of toolFindings) {
|
|
139
|
+
findings.push({
|
|
140
|
+
...f,
|
|
141
|
+
ide: server.ide,
|
|
142
|
+
config_path: server.configPath,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const serverConfig = server.config || {};
|
|
149
|
+
if (serverConfig.env) {
|
|
150
|
+
const secretFindings = detectHardcodedSecrets(serverConfig.env, server);
|
|
151
|
+
findings.push(...secretFindings);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return findings;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Run all registered server-level config checks against a single server.
|
|
159
|
+
* Iterates SERVER_CONFIG_CHECKS registry — no manual calls needed.
|
|
160
|
+
*/
|
|
161
|
+
function runServerConfigChecks(server) {
|
|
162
|
+
const config = server.config || {};
|
|
163
|
+
const findings = [];
|
|
164
|
+
|
|
165
|
+
for (const check of SERVER_CONFIG_CHECKS) {
|
|
166
|
+
const checkFindings = check(server.name, config);
|
|
167
|
+
for (const f of checkFindings) {
|
|
168
|
+
findings.push({
|
|
169
|
+
...f,
|
|
170
|
+
ide: server.ide,
|
|
171
|
+
config_path: server.configPath,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return findings;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Group findings by server name
|
|
181
|
+
*/
|
|
182
|
+
function groupFindingsByServer(findings) {
|
|
183
|
+
const grouped = {};
|
|
184
|
+
for (const finding of findings) {
|
|
185
|
+
const key = finding.server_name || 'unknown';
|
|
186
|
+
if (!grouped[key]) {
|
|
187
|
+
grouped[key] = [];
|
|
188
|
+
}
|
|
189
|
+
grouped[key].push(finding);
|
|
190
|
+
}
|
|
191
|
+
return grouped;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get list of servers with no findings
|
|
196
|
+
*/
|
|
197
|
+
function getCleanServers(servers, findings) {
|
|
198
|
+
const serversWithFindings = new Set(findings.map((f) => f.server_name));
|
|
199
|
+
return servers.filter((s) => !serversWithFindings.has(s.name)).map((s) => s.name);
|
|
200
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hardcoded Secret Detection
|
|
3
|
+
* Detects API keys, tokens, and credentials hardcoded in MCP server env vars.
|
|
4
|
+
*
|
|
5
|
+
* Patterns are loaded from data/secret-patterns.json (built-in)
|
|
6
|
+
* and merged with user overrides from .mcp-shark/secrets.yaml.
|
|
7
|
+
*/
|
|
8
|
+
import { loadBuiltinJson, loadUserYamlList } from './DataLoader.js';
|
|
9
|
+
|
|
10
|
+
const BUILTIN_PATTERNS = loadBuiltinJson('secret-patterns.json');
|
|
11
|
+
const USER_PATTERNS = loadUserYamlList('secrets.yaml');
|
|
12
|
+
|
|
13
|
+
const SECRET_PATTERNS = compilePatterns([...BUILTIN_PATTERNS, ...USER_PATTERNS]);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compile raw pattern definitions into regex objects
|
|
17
|
+
* @param {Array<{pattern: string, name: string, severity: string}>} rawPatterns
|
|
18
|
+
* @returns {Array<{pattern: RegExp, name: string, severity: string}>}
|
|
19
|
+
*/
|
|
20
|
+
function compilePatterns(rawPatterns) {
|
|
21
|
+
const compiled = [];
|
|
22
|
+
for (const entry of rawPatterns) {
|
|
23
|
+
try {
|
|
24
|
+
compiled.push({
|
|
25
|
+
pattern: new RegExp(entry.pattern),
|
|
26
|
+
name: entry.name,
|
|
27
|
+
severity: entry.severity,
|
|
28
|
+
});
|
|
29
|
+
} catch (_err) {
|
|
30
|
+
// skip malformed patterns from user overrides
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return compiled;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Detect hardcoded secrets in server environment variables
|
|
38
|
+
* @param {object} envVars - Environment variables from server config
|
|
39
|
+
* @param {object} server - Server context (name, ide, configPath)
|
|
40
|
+
* @returns {Array} Findings
|
|
41
|
+
*/
|
|
42
|
+
export function detectHardcodedSecrets(envVars, server) {
|
|
43
|
+
const findings = [];
|
|
44
|
+
if (!envVars || typeof envVars !== 'object') {
|
|
45
|
+
return findings;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
49
|
+
if (typeof value !== 'string') {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (value.startsWith('${')) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (/^\$[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const { pattern, name, severity } of SECRET_PATTERNS) {
|
|
60
|
+
if (pattern.test(value)) {
|
|
61
|
+
const masked = maskSecret(value);
|
|
62
|
+
findings.push({
|
|
63
|
+
rule_id: 'hardcoded-secret',
|
|
64
|
+
category: 'MCP01',
|
|
65
|
+
severity,
|
|
66
|
+
confidence: 'definite',
|
|
67
|
+
title: `${name} hardcoded in config`,
|
|
68
|
+
description: `${key}=${masked} — use environment variable reference instead`,
|
|
69
|
+
server_name: server.name,
|
|
70
|
+
ide: server.ide,
|
|
71
|
+
config_path: server.configPath,
|
|
72
|
+
fixable: true,
|
|
73
|
+
fix_type: 'env_var_replacement',
|
|
74
|
+
fix_data: { key, original: value },
|
|
75
|
+
});
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return findings;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Mask a secret value for display
|
|
86
|
+
*/
|
|
87
|
+
function maskSecret(value) {
|
|
88
|
+
if (value.length <= 8) {
|
|
89
|
+
return '****';
|
|
90
|
+
}
|
|
91
|
+
return `${value.slice(0, 4)}****`;
|
|
92
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shark Score Calculator
|
|
3
|
+
* Transparent, reproducible scoring formula for MCP security posture
|
|
4
|
+
*
|
|
5
|
+
* Formula: Shark Score = max(0, 100 - Σ finding_weights)
|
|
6
|
+
*
|
|
7
|
+
* Finding weights:
|
|
8
|
+
* CRITICAL: 25 points
|
|
9
|
+
* HIGH: 15 points
|
|
10
|
+
* MEDIUM: 5 points
|
|
11
|
+
* LOW: 2 points
|
|
12
|
+
* Toxic flow (HIGH): 10 points
|
|
13
|
+
* Toxic flow (MEDIUM): 5 points
|
|
14
|
+
*
|
|
15
|
+
* Grades:
|
|
16
|
+
* 90-100 A Excellent
|
|
17
|
+
* 75-89 B Good
|
|
18
|
+
* 50-74 C Needs work
|
|
19
|
+
* 25-49 D Poor
|
|
20
|
+
* 0-24 F Critical risk
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const FINDING_WEIGHTS = {
|
|
24
|
+
critical: 25,
|
|
25
|
+
high: 15,
|
|
26
|
+
medium: 5,
|
|
27
|
+
low: 2,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const TOXIC_FLOW_WEIGHTS = {
|
|
31
|
+
high: 10,
|
|
32
|
+
medium: 5,
|
|
33
|
+
low: 2,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const GRADES = [
|
|
37
|
+
{ min: 90, grade: 'A', label: 'Excellent' },
|
|
38
|
+
{ min: 75, grade: 'B', label: 'Good' },
|
|
39
|
+
{ min: 50, grade: 'C', label: 'Needs work' },
|
|
40
|
+
{ min: 25, grade: 'D', label: 'Poor' },
|
|
41
|
+
{ min: 0, grade: 'F', label: 'Critical risk' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Calculate the Shark Score from findings and toxic flows
|
|
46
|
+
* @param {Array} findings - Array of security findings with severity
|
|
47
|
+
* @param {Array} toxicFlows - Array of toxic flow results with risk level
|
|
48
|
+
* @returns {{ score: number, grade: string, label: string, breakdown: object }}
|
|
49
|
+
*/
|
|
50
|
+
export function calculateSharkScore(findings, toxicFlows = []) {
|
|
51
|
+
const confirmedFindings = findings.filter((f) => f.confidence !== 'possible');
|
|
52
|
+
|
|
53
|
+
const findingDeductions = confirmedFindings.reduce((sum, finding) => {
|
|
54
|
+
const severity = (finding.severity || finding.risk_level || '').toLowerCase();
|
|
55
|
+
const weight = FINDING_WEIGHTS[severity] || 0;
|
|
56
|
+
return sum + weight;
|
|
57
|
+
}, 0);
|
|
58
|
+
|
|
59
|
+
const flowDeductions = toxicFlows.reduce((sum, flow) => {
|
|
60
|
+
const risk = (flow.risk || '').toLowerCase();
|
|
61
|
+
const weight = TOXIC_FLOW_WEIGHTS[risk] || 0;
|
|
62
|
+
return sum + weight;
|
|
63
|
+
}, 0);
|
|
64
|
+
|
|
65
|
+
const totalDeductions = findingDeductions + flowDeductions;
|
|
66
|
+
const score = Math.max(0, 100 - totalDeductions);
|
|
67
|
+
const gradeInfo = getGrade(score);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
score,
|
|
71
|
+
grade: gradeInfo.grade,
|
|
72
|
+
label: gradeInfo.label,
|
|
73
|
+
breakdown: {
|
|
74
|
+
findingDeductions,
|
|
75
|
+
flowDeductions,
|
|
76
|
+
totalDeductions,
|
|
77
|
+
confirmedCount: confirmedFindings.length,
|
|
78
|
+
flowCount: toxicFlows.length,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get grade from score
|
|
85
|
+
*/
|
|
86
|
+
function getGrade(score) {
|
|
87
|
+
for (const g of GRADES) {
|
|
88
|
+
if (score >= g.min) {
|
|
89
|
+
return g;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return GRADES[GRADES.length - 1];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Count findings by severity
|
|
97
|
+
* @param {Array} findings
|
|
98
|
+
* @returns {{ critical: number, high: number, medium: number, low: number }}
|
|
99
|
+
*/
|
|
100
|
+
export function countBySeverity(findings) {
|
|
101
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
102
|
+
for (const f of findings) {
|
|
103
|
+
const severity = (f.severity || f.risk_level || '').toLowerCase();
|
|
104
|
+
if (counts[severity] !== undefined) {
|
|
105
|
+
counts[severity] += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return counts;
|
|
109
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Classification Database
|
|
3
|
+
* Maps known MCP server tools to their capability categories.
|
|
4
|
+
*
|
|
5
|
+
* Built-in classifications loaded from data/tool-classifications.json.
|
|
6
|
+
* User overrides merged from .mcp-shark/classifications.yaml.
|
|
7
|
+
*
|
|
8
|
+
* Categories:
|
|
9
|
+
* ingests_untrusted - reads external/public data
|
|
10
|
+
* writes_code - modifies source code, pushes commits
|
|
11
|
+
* sends_external - sends data to external endpoints
|
|
12
|
+
* reads_secrets - accesses sensitive local data
|
|
13
|
+
* modifies_infra - changes infrastructure state
|
|
14
|
+
*/
|
|
15
|
+
import { loadBuiltinJson, loadUserYamlMap } from './DataLoader.js';
|
|
16
|
+
|
|
17
|
+
const BUILTIN = loadBuiltinJson('tool-classifications.json');
|
|
18
|
+
const USER_OVERRIDES = loadUserYamlMap('classifications.yaml');
|
|
19
|
+
|
|
20
|
+
const UNSAFE_CLASSIFICATION_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
21
|
+
|
|
22
|
+
export const TOOL_CLASSIFICATIONS = mergeClassifications(BUILTIN, USER_OVERRIDES);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Deep-merge user overrides into built-in classifications.
|
|
26
|
+
* User entries for existing servers extend (not replace) the tool map.
|
|
27
|
+
* New servers are added wholesale.
|
|
28
|
+
* @param {Record<string, Record<string, string>>} builtin
|
|
29
|
+
* @param {Record<string, unknown>} overrides
|
|
30
|
+
*/
|
|
31
|
+
export function mergeClassifications(builtin, overrides) {
|
|
32
|
+
const merged = { ...builtin };
|
|
33
|
+
for (const [server, tools] of Object.entries(overrides)) {
|
|
34
|
+
if (UNSAFE_CLASSIFICATION_KEYS.has(server)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!tools || typeof tools !== 'object' || Array.isArray(tools)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const safeTools = { ...tools };
|
|
41
|
+
for (const k of UNSAFE_CLASSIFICATION_KEYS) {
|
|
42
|
+
delete safeTools[k];
|
|
43
|
+
}
|
|
44
|
+
if (merged[server]) {
|
|
45
|
+
merged[server] = { ...merged[server], ...safeTools };
|
|
46
|
+
} else {
|
|
47
|
+
merged[server] = { ...safeTools };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return merged;
|
|
51
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Toxic Flow Analyzer
|
|
3
|
+
* Detects dangerous cross-server composition risks by classifying
|
|
4
|
+
* tools by capability and identifying pairs that create attack paths
|
|
5
|
+
* through the shared LLM context window.
|
|
6
|
+
*
|
|
7
|
+
* Flow rules are loaded from data/toxic-flow-rules.json (built-in),
|
|
8
|
+
* optional toxic_flow_rules inside rule-pack JSON under data/rule-packs/ and
|
|
9
|
+
* .mcp-shark/rule-packs/, and user overrides from .mcp-shark/flows.yaml.
|
|
10
|
+
*
|
|
11
|
+
* Catalog references: §1.1, §1.2, §1.3, §1.7, §1.10, §1.12, §1.13, §1.14
|
|
12
|
+
*/
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { loadBuiltinJson, loadToxicFlowRulesFromPacksDir, loadUserYamlList } from './DataLoader.js';
|
|
15
|
+
import { TOOL_CLASSIFICATIONS } from './ToolClassifications.js';
|
|
16
|
+
|
|
17
|
+
const BUILTIN_PACKS_DIR = join(import.meta.dirname, 'data', 'rule-packs');
|
|
18
|
+
const USER_PACKS_DIR = join(process.cwd(), '.mcp-shark', 'rule-packs');
|
|
19
|
+
|
|
20
|
+
const BUILTIN_FLOWS = loadBuiltinJson('toxic-flow-rules.json');
|
|
21
|
+
const PACK_FLOWS_BUILTIN = loadToxicFlowRulesFromPacksDir(BUILTIN_PACKS_DIR);
|
|
22
|
+
const PACK_FLOWS_USER = loadToxicFlowRulesFromPacksDir(USER_PACKS_DIR);
|
|
23
|
+
const USER_FLOWS = loadUserYamlList('flows.yaml');
|
|
24
|
+
const TOXIC_FLOW_RULES = [
|
|
25
|
+
...BUILTIN_FLOWS,
|
|
26
|
+
...PACK_FLOWS_BUILTIN,
|
|
27
|
+
...PACK_FLOWS_USER,
|
|
28
|
+
...USER_FLOWS,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Classify a tool's capability based on known classifications and heuristics
|
|
33
|
+
*/
|
|
34
|
+
function classifyTool(serverName, toolName) {
|
|
35
|
+
const serverClassifications = TOOL_CLASSIFICATIONS[serverName];
|
|
36
|
+
if (serverClassifications?.[toolName]) {
|
|
37
|
+
return serverClassifications[toolName];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const nameLower = (toolName || '').toLowerCase();
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
matchesPatterns(nameLower, ['send_message', 'post_message', 'send_email', 'send_notification'])
|
|
44
|
+
) {
|
|
45
|
+
return 'sends_external';
|
|
46
|
+
}
|
|
47
|
+
if (
|
|
48
|
+
matchesPatterns(nameLower, [
|
|
49
|
+
'write_file',
|
|
50
|
+
'create_file',
|
|
51
|
+
'push',
|
|
52
|
+
'commit',
|
|
53
|
+
'create_pr',
|
|
54
|
+
'create_pull',
|
|
55
|
+
])
|
|
56
|
+
) {
|
|
57
|
+
return 'writes_code';
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
matchesPatterns(nameLower, ['read_file', 'get_file', 'list_dir', 'get_config', 'get_secret'])
|
|
61
|
+
) {
|
|
62
|
+
return 'reads_secrets';
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
matchesPatterns(nameLower, [
|
|
66
|
+
'get_issue',
|
|
67
|
+
'get_ticket',
|
|
68
|
+
'list_messages',
|
|
69
|
+
'search',
|
|
70
|
+
'fetch',
|
|
71
|
+
'scrape',
|
|
72
|
+
])
|
|
73
|
+
) {
|
|
74
|
+
return 'ingests_untrusted';
|
|
75
|
+
}
|
|
76
|
+
if (matchesPatterns(nameLower, ['deploy', 'kubectl', 'docker', 'transfer', 'scale', 'restart'])) {
|
|
77
|
+
return 'modifies_infra';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a name matches any of the patterns
|
|
85
|
+
*/
|
|
86
|
+
function matchesPatterns(name, patterns) {
|
|
87
|
+
for (const pattern of patterns) {
|
|
88
|
+
if (name.includes(pattern)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Classify all tools in a server and return capabilities
|
|
97
|
+
*/
|
|
98
|
+
function classifyServer(server) {
|
|
99
|
+
const capabilities = new Set();
|
|
100
|
+
const classifiedTools = {};
|
|
101
|
+
|
|
102
|
+
const toolNames = extractToolNames(server);
|
|
103
|
+
|
|
104
|
+
for (const toolName of toolNames) {
|
|
105
|
+
const capability = classifyTool(server.name, toolName);
|
|
106
|
+
if (capability) {
|
|
107
|
+
capabilities.add(capability);
|
|
108
|
+
classifiedTools[toolName] = capability;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { capabilities: [...capabilities], classifiedTools };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extract tool names from a server definition
|
|
117
|
+
*/
|
|
118
|
+
function extractToolNames(server) {
|
|
119
|
+
if (Array.isArray(server.tools)) {
|
|
120
|
+
return server.tools.map((t) => t.name || t);
|
|
121
|
+
}
|
|
122
|
+
if (server.tools && typeof server.tools === 'object') {
|
|
123
|
+
return Object.keys(server.tools);
|
|
124
|
+
}
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Interpolate a scenario template string with source/target context.
|
|
130
|
+
* Replaces {source}, {target}, {source_ide}, {target_ide} placeholders.
|
|
131
|
+
*/
|
|
132
|
+
function interpolateScenario(template, src, tgt) {
|
|
133
|
+
return template
|
|
134
|
+
.replace(/\{source_ide\}/g, src.ide || 'IDE')
|
|
135
|
+
.replace(/\{target_ide\}/g, tgt.ide || 'IDE')
|
|
136
|
+
.replace(/\{source\}/g, src.name)
|
|
137
|
+
.replace(/\{target\}/g, tgt.name);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Analyze toxic flows across all servers
|
|
142
|
+
* @param {Array} servers - Flat list of server objects with { name, ide, config, tools }
|
|
143
|
+
* @returns {Array} Array of toxic flow results
|
|
144
|
+
*/
|
|
145
|
+
export function analyzeToxicFlows(servers) {
|
|
146
|
+
const classifiedServers = servers.map((server) => ({
|
|
147
|
+
...server,
|
|
148
|
+
...classifyServer(server),
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
const flows = [];
|
|
152
|
+
|
|
153
|
+
for (const rule of TOXIC_FLOW_RULES) {
|
|
154
|
+
const sources = classifiedServers.filter((s) => s.capabilities.includes(rule.source));
|
|
155
|
+
const targets = classifiedServers.filter((s) => s.capabilities.includes(rule.target));
|
|
156
|
+
|
|
157
|
+
for (const src of sources) {
|
|
158
|
+
for (const tgt of targets) {
|
|
159
|
+
if (src.name === tgt.name) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
flows.push({
|
|
164
|
+
source: src.name,
|
|
165
|
+
sourceIde: src.ide,
|
|
166
|
+
target: tgt.name,
|
|
167
|
+
targetIde: tgt.ide,
|
|
168
|
+
risk: rule.risk,
|
|
169
|
+
title: rule.title,
|
|
170
|
+
scenario: interpolateScenario(rule.scenario, src, tgt),
|
|
171
|
+
catalog: rule.catalog,
|
|
172
|
+
owasp: rule.owasp,
|
|
173
|
+
sourceCapability: rule.source,
|
|
174
|
+
targetCapability: rule.target,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return deduplicateFlows(flows);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Remove duplicate flows (same source, target, IDE pair, capability pair, and title; keep highest risk)
|
|
185
|
+
*/
|
|
186
|
+
function deduplicateFlows(flows) {
|
|
187
|
+
const seen = new Map();
|
|
188
|
+
for (const flow of flows) {
|
|
189
|
+
const key = [
|
|
190
|
+
flow.source,
|
|
191
|
+
flow.sourceIde,
|
|
192
|
+
flow.target,
|
|
193
|
+
flow.targetIde,
|
|
194
|
+
flow.sourceCapability,
|
|
195
|
+
flow.targetCapability,
|
|
196
|
+
flow.title,
|
|
197
|
+
].join('\u2192');
|
|
198
|
+
const existing = seen.get(key);
|
|
199
|
+
if (!existing || riskLevel(flow.risk) > riskLevel(existing.risk)) {
|
|
200
|
+
seen.set(key, flow);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return [...seen.values()];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Convert risk string to numeric level for comparison
|
|
208
|
+
*/
|
|
209
|
+
function riskLevel(risk) {
|
|
210
|
+
const levels = { HIGH: 3, MEDIUM: 2, LOW: 1 };
|
|
211
|
+
return levels[risk] || 0;
|
|
212
|
+
}
|