@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
|
@@ -4,6 +4,36 @@ import { ConfigParserFactory } from '#core/services/parsers/ConfigParserFactory.
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_TYPE = 'stdio';
|
|
6
6
|
|
|
7
|
+
// An upstream entry is considered a self-reference (and pruned) when:
|
|
8
|
+
// - its name is in this reserved set, OR
|
|
9
|
+
// - it is an HTTP entry whose host:port resolves to mcp-shark's own
|
|
10
|
+
// listening port. Including such an entry would loop the proxy back
|
|
11
|
+
// onto itself at startup and crash with RunAllExternalServersError.
|
|
12
|
+
const RESERVED_UPSTREAM_NAMES = new Set(['mcp-shark']);
|
|
13
|
+
|
|
14
|
+
function isSelfReferencingEntry(name, cfg, selfPort) {
|
|
15
|
+
if (RESERVED_UPSTREAM_NAMES.has(name)) {
|
|
16
|
+
return { selfRef: true, reason: `reserved name '${name}'` };
|
|
17
|
+
}
|
|
18
|
+
if (!selfPort || !cfg || typeof cfg.url !== 'string') {
|
|
19
|
+
return { selfRef: false };
|
|
20
|
+
}
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = new URL(cfg.url);
|
|
24
|
+
} catch {
|
|
25
|
+
return { selfRef: false };
|
|
26
|
+
}
|
|
27
|
+
const port = Number.parseInt(parsed.port || (parsed.protocol === 'https:' ? '443' : '80'), 10);
|
|
28
|
+
if (port !== selfPort) return { selfRef: false };
|
|
29
|
+
const localHosts = new Set(['127.0.0.1', 'localhost', '0.0.0.0', '::1']);
|
|
30
|
+
if (!localHosts.has(parsed.hostname)) return { selfRef: false };
|
|
31
|
+
return {
|
|
32
|
+
selfRef: true,
|
|
33
|
+
reason: `URL ${parsed.hostname}:${port} points back at mcp-shark's own proxy`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
7
37
|
export class ConfigError extends CompositeError {
|
|
8
38
|
constructor(message, error) {
|
|
9
39
|
super('ConfigError', message, error);
|
|
@@ -37,7 +67,8 @@ function parseConfig(configPath) {
|
|
|
37
67
|
}
|
|
38
68
|
}
|
|
39
69
|
|
|
40
|
-
export function normalizeConfig(configPath) {
|
|
70
|
+
export function normalizeConfig(configPath, options = {}) {
|
|
71
|
+
const { selfPort, logger, allowEmpty = false } = options;
|
|
41
72
|
const parsedConfigResult = parseConfig(configPath);
|
|
42
73
|
if (isError(parsedConfigResult)) {
|
|
43
74
|
return parsedConfigResult;
|
|
@@ -45,29 +76,55 @@ export function normalizeConfig(configPath) {
|
|
|
45
76
|
|
|
46
77
|
const normalized = parserFactory.normalizeToInternalFormat(parsedConfigResult, configPath);
|
|
47
78
|
if (!normalized) {
|
|
79
|
+
if (allowEmpty) return {};
|
|
48
80
|
return new ConfigError('No servers found in config');
|
|
49
81
|
}
|
|
50
82
|
|
|
51
83
|
// Convert to flat map format expected by MCP server
|
|
52
84
|
const out = new Map();
|
|
85
|
+
const pruned = [];
|
|
86
|
+
|
|
87
|
+
function addEntry(name, cfg) {
|
|
88
|
+
const { selfRef, reason } = isSelfReferencingEntry(name, cfg, selfPort);
|
|
89
|
+
if (selfRef) {
|
|
90
|
+
pruned.push({ name, reason });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const type = cfg.type ?? DEFAULT_TYPE;
|
|
94
|
+
out.set(name, { type, ...cfg });
|
|
95
|
+
}
|
|
53
96
|
|
|
54
97
|
// Handle normalized mcpServers format
|
|
55
98
|
if (normalized.mcpServers) {
|
|
56
99
|
for (const [name, cfg] of Object.entries(normalized.mcpServers)) {
|
|
57
|
-
|
|
58
|
-
out.set(name, { type, ...cfg });
|
|
100
|
+
addEntry(name, cfg);
|
|
59
101
|
}
|
|
60
102
|
}
|
|
61
103
|
|
|
62
104
|
// Handle normalized servers format (legacy)
|
|
63
105
|
if (normalized.servers) {
|
|
64
106
|
for (const [name, cfg] of Object.entries(normalized.servers)) {
|
|
65
|
-
|
|
66
|
-
|
|
107
|
+
addEntry(name, cfg);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (pruned.length > 0 && logger?.warn) {
|
|
112
|
+
for (const { name, reason } of pruned) {
|
|
113
|
+
logger.warn(
|
|
114
|
+
{ upstream: name, reason },
|
|
115
|
+
`[MCP-Shark] Skipping self-referencing upstream '${name}': ${reason}`
|
|
116
|
+
);
|
|
67
117
|
}
|
|
68
118
|
}
|
|
69
119
|
|
|
70
120
|
if (out.size === 0) {
|
|
121
|
+
if (allowEmpty) {
|
|
122
|
+
logger?.warn?.(
|
|
123
|
+
{ configPath },
|
|
124
|
+
'[MCP-Shark] No upstream MCP servers configured — running in monitoring-only mode'
|
|
125
|
+
);
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
71
128
|
return new ConfigError('No servers found in config');
|
|
72
129
|
}
|
|
73
130
|
|
|
@@ -19,6 +19,9 @@ export class RequestFilters {
|
|
|
19
19
|
this.limit = data.limit !== undefined ? Number.parseInt(data.limit) : Defaults.DEFAULT_LIMIT;
|
|
20
20
|
this.offset =
|
|
21
21
|
data.offset !== undefined ? Number.parseInt(data.offset) : Defaults.DEFAULT_OFFSET;
|
|
22
|
+
this.aauthPosture = data.aauthPosture || null;
|
|
23
|
+
this.aauthAgent = data.aauthAgent || null;
|
|
24
|
+
this.aauthMission = data.aauthMission || null;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
/**
|
|
@@ -234,4 +234,20 @@ export class PacketRepository {
|
|
|
234
234
|
const stmt = this.db.prepare('SELECT MAX(timestamp_ns) as max_ts FROM packets');
|
|
235
235
|
return stmt.get();
|
|
236
236
|
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Response packets whose JSON-RPC result likely includes a tools array (tools/list success).
|
|
240
|
+
* Used to replay cross-server toxic-flow analysis from captured proxy traffic.
|
|
241
|
+
*/
|
|
242
|
+
listResponsesWithToolsList() {
|
|
243
|
+
const stmt = this.db.prepare(`
|
|
244
|
+
SELECT frame_number, session_id, remote_address, jsonrpc_result, body_json, body_raw, timestamp_ns
|
|
245
|
+
FROM packets
|
|
246
|
+
WHERE direction = 'response'
|
|
247
|
+
AND jsonrpc_result IS NOT NULL
|
|
248
|
+
AND jsonrpc_result LIKE '%"tools"%'
|
|
249
|
+
ORDER BY timestamp_ns ASC
|
|
250
|
+
`);
|
|
251
|
+
return stmt.all();
|
|
252
|
+
}
|
|
237
253
|
}
|
|
@@ -97,6 +97,7 @@ export class AuditService {
|
|
|
97
97
|
frameNumber: result.frameNumber,
|
|
98
98
|
body: options.body,
|
|
99
99
|
sessionId: result.sessionId,
|
|
100
|
+
mcpServerName: options.remoteAddress || null,
|
|
100
101
|
});
|
|
101
102
|
} catch (error) {
|
|
102
103
|
// Log but don't fail the request
|
|
@@ -172,6 +173,7 @@ export class AuditService {
|
|
|
172
173
|
frameNumber: result.frameNumber,
|
|
173
174
|
body: options.body,
|
|
174
175
|
sessionId: result.sessionId,
|
|
176
|
+
mcpServerName: options.remoteAddress || null,
|
|
175
177
|
});
|
|
176
178
|
} catch (error) {
|
|
177
179
|
// Log but don't fail the response
|
|
@@ -201,7 +201,15 @@ export class ConfigService {
|
|
|
201
201
|
? this.transformService.filterServers(baseConvertedConfig, selectedServices)
|
|
202
202
|
: baseConvertedConfig;
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
// A zero-upstream config is a legitimate state (monitoring-only mode):
|
|
205
|
+
// mcp-shark still runs the audit/UI/AAuth surfaces, just without any
|
|
206
|
+
// upstreams to forward to. We only fail here if the *original* config
|
|
207
|
+
// file declared no servers at all (different from "all servers were
|
|
208
|
+
// self-references and got filtered out").
|
|
209
|
+
const originalHadAnyServer =
|
|
210
|
+
Object.keys(originalConfig.mcpServers || {}).length > 0 ||
|
|
211
|
+
Object.keys(originalConfig.servers || {}).length > 0;
|
|
212
|
+
if (!originalHadAnyServer) {
|
|
205
213
|
return {
|
|
206
214
|
success: false,
|
|
207
215
|
error: 'No servers found in config',
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
// mcp-shark must never appear as an upstream of itself. The UI reads the
|
|
2
|
+
// user's editor MCP config (e.g. ~/.cursor/mcp.json) — which legitimately
|
|
3
|
+
// contains an `mcp-shark` entry pointing at the proxy port so the editor
|
|
4
|
+
// routes calls through us — and copies it into ~/.mcp-shark/mcps.json as
|
|
5
|
+
// the proxy's upstream list. Without this filter the proxy ends up with a
|
|
6
|
+
// recursive self-reference that crashes startup.
|
|
7
|
+
const RESERVED_PROXY_NAMES = new Set(['mcp-shark']);
|
|
8
|
+
function isProxySelfReference(name, cfg) {
|
|
9
|
+
if (RESERVED_PROXY_NAMES.has(name)) return true;
|
|
10
|
+
if (!cfg || typeof cfg.url !== 'string') return false;
|
|
11
|
+
let parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = new URL(cfg.url);
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const localHosts = new Set(['127.0.0.1', 'localhost', '0.0.0.0', '::1']);
|
|
18
|
+
if (!localHosts.has(parsed.hostname)) return false;
|
|
19
|
+
const port = Number.parseInt(parsed.port || (parsed.protocol === 'https:' ? '443' : '80'), 10);
|
|
20
|
+
// We don't have access to the runtime port here; the proxy default is
|
|
21
|
+
// 9851 and the UI always starts it on that port unless overridden via
|
|
22
|
+
// env. If the user runs on a different port they can still safely include
|
|
23
|
+
// a custom-named entry; the runtime config.js layer will catch true loops.
|
|
24
|
+
return port === 9851;
|
|
25
|
+
}
|
|
26
|
+
|
|
1
27
|
/**
|
|
2
28
|
* Service for configuration transformations
|
|
3
29
|
* Handles converting, filtering, and updating config structures
|
|
@@ -9,7 +35,9 @@ export class ConfigTransformService {
|
|
|
9
35
|
|
|
10
36
|
/**
|
|
11
37
|
* Convert MCP servers format to servers format
|
|
12
|
-
* Normalizes config first, then converts mcpServers to servers
|
|
38
|
+
* Normalizes config first, then converts mcpServers to servers.
|
|
39
|
+
* Self-referencing entries (mcp-shark itself) are dropped — they would
|
|
40
|
+
* create an infinite proxy loop if written to ~/.mcp-shark/mcps.json.
|
|
13
41
|
*/
|
|
14
42
|
convertMcpServersToServers(config) {
|
|
15
43
|
// Normalize config to ensure consistent format
|
|
@@ -22,12 +50,16 @@ export class ConfigTransformService {
|
|
|
22
50
|
|
|
23
51
|
// Handle normalized servers (legacy format)
|
|
24
52
|
if (normalized.servers) {
|
|
25
|
-
|
|
53
|
+
for (const [name, cfg] of Object.entries(normalized.servers)) {
|
|
54
|
+
if (isProxySelfReference(name, cfg)) continue;
|
|
55
|
+
converted.servers[name] = cfg;
|
|
56
|
+
}
|
|
26
57
|
}
|
|
27
58
|
|
|
28
59
|
// Convert mcpServers to servers format
|
|
29
60
|
if (normalized.mcpServers) {
|
|
30
61
|
for (const [name, cfg] of Object.entries(normalized.mcpServers)) {
|
|
62
|
+
if (isProxySelfReference(name, cfg)) continue;
|
|
31
63
|
const type = cfg.type || (cfg.url ? 'http' : cfg.command ? 'stdio' : 'stdio');
|
|
32
64
|
converted.servers[name] = { type, ...cfg };
|
|
33
65
|
}
|
|
@@ -5,6 +5,53 @@ import { Defaults } from '../constants/Defaults.js';
|
|
|
5
5
|
* HTTP-agnostic: accepts models, returns models
|
|
6
6
|
*/
|
|
7
7
|
import { RequestFilters } from '../models/RequestFilters.js';
|
|
8
|
+
import { parseAauthForPacket } from './security/aauthParser.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Attach an `aauth` field to a packet record by parsing its headers_json.
|
|
12
|
+
* Pure function; no DB writes. Always returns a new object.
|
|
13
|
+
*/
|
|
14
|
+
function enrichWithAauth(packet) {
|
|
15
|
+
if (!packet) {
|
|
16
|
+
return packet;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
return { ...packet, aauth: parseAauthForPacket(packet) };
|
|
20
|
+
} catch {
|
|
21
|
+
return packet;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Filter a list of packets according to AAuth-derived filters that are not
|
|
27
|
+
* supported by the repository (which only knows about DB columns).
|
|
28
|
+
*/
|
|
29
|
+
function applyAauthFilters(packets, filters) {
|
|
30
|
+
if (!filters) {
|
|
31
|
+
return packets;
|
|
32
|
+
}
|
|
33
|
+
const { aauthPosture = null, aauthAgent = null, aauthMission = null } = filters;
|
|
34
|
+
if (!aauthPosture && !aauthAgent && !aauthMission) {
|
|
35
|
+
return packets;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return packets.filter((p) => {
|
|
39
|
+
const aauth = p?.aauth;
|
|
40
|
+
if (!aauth) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (aauthPosture && aauth.posture !== aauthPosture) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (aauthAgent && aauth.agent !== aauthAgent) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (aauthMission && aauth.mission !== aauthMission) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
8
55
|
|
|
9
56
|
export class RequestService {
|
|
10
57
|
constructor(packetRepository) {
|
|
@@ -14,21 +61,27 @@ export class RequestService {
|
|
|
14
61
|
/**
|
|
15
62
|
* Get requests with filters
|
|
16
63
|
* @param {RequestFilters} filters - Typed filter model
|
|
17
|
-
* @returns {Array} Array of packet objects
|
|
64
|
+
* @returns {Array} Array of packet objects, each enriched with an `aauth` block.
|
|
18
65
|
*/
|
|
19
66
|
getRequests(filters) {
|
|
20
67
|
const repoFilters = filters.toRepositoryFilters();
|
|
21
|
-
|
|
68
|
+
const rows = this.packetRepository.queryRequests(repoFilters);
|
|
69
|
+
const enriched = rows.map(enrichWithAauth);
|
|
70
|
+
return applyAauthFilters(enriched, {
|
|
71
|
+
aauthPosture: filters.aauthPosture,
|
|
72
|
+
aauthAgent: filters.aauthAgent,
|
|
73
|
+
aauthMission: filters.aauthMission,
|
|
74
|
+
});
|
|
22
75
|
}
|
|
23
76
|
|
|
24
77
|
/**
|
|
25
78
|
* Get request by frame number
|
|
26
79
|
* @param {number} frameNumber - Frame number
|
|
27
|
-
* @returns {Object|null} Packet object
|
|
80
|
+
* @returns {Object|null} Packet object enriched with an `aauth` block, or null.
|
|
28
81
|
*/
|
|
29
82
|
getRequest(frameNumber) {
|
|
30
83
|
const parsedFrameNumber = Number.parseInt(frameNumber);
|
|
31
|
-
return this.packetRepository.getByFrameNumber(parsedFrameNumber);
|
|
84
|
+
return enrichWithAauth(this.packetRepository.getByFrameNumber(parsedFrameNumber));
|
|
32
85
|
}
|
|
33
86
|
|
|
34
87
|
/**
|
|
@@ -51,6 +104,6 @@ export class RequestService {
|
|
|
51
104
|
offset: Defaults.DEFAULT_OFFSET,
|
|
52
105
|
});
|
|
53
106
|
const repoFilters = exportFilters.toRepositoryFilters();
|
|
54
|
-
return this.packetRepository.queryRequests(repoFilters);
|
|
107
|
+
return this.packetRepository.queryRequests(repoFilters).map(enrichWithAauth);
|
|
55
108
|
}
|
|
56
109
|
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
1
3
|
import { Defaults } from '#core/constants/Defaults.js';
|
|
2
4
|
import { initAuditLogger, startMcpSharkServer } from '#core/mcp-server/index.js';
|
|
3
5
|
|
|
6
|
+
function isSamePath(a, b) {
|
|
7
|
+
if (!a || !b) return false;
|
|
8
|
+
try {
|
|
9
|
+
return path.resolve(a) === path.resolve(b);
|
|
10
|
+
} catch {
|
|
11
|
+
return a === b;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
4
15
|
/**
|
|
5
16
|
* Service for managing MCP Shark server lifecycle
|
|
6
17
|
* Handles server startup, shutdown, and status
|
|
@@ -60,8 +71,34 @@ export class ServerManagementService {
|
|
|
60
71
|
const { fileData, convertedConfig, updatedConfig } = setupResult;
|
|
61
72
|
const mcpsJsonPath = this.configService.getMcpConfigPath();
|
|
62
73
|
|
|
63
|
-
//
|
|
64
|
-
|
|
74
|
+
// The user can legitimately point the Setup panel at the proxy's own
|
|
75
|
+
// config file (~/.mcp-shark/mcps.json) when they just want to (re)start
|
|
76
|
+
// the proxy with whatever is already there — for example after
|
|
77
|
+
// `npm run testbed:up` has written upstreams directly. In that case the
|
|
78
|
+
// input *is* the output and the "patch the editor config" step at the
|
|
79
|
+
// end would rewrite every upstream URL into the proxy-local form
|
|
80
|
+
// (http://localhost:9851/mcp/<name>), turning mcps.json into a
|
|
81
|
+
// self-referential file that the next startup has to strip.
|
|
82
|
+
const sourceIsProxyConfig = isSamePath(fileData.resolvedFilePath, mcpsJsonPath);
|
|
83
|
+
|
|
84
|
+
// If the conversion produced zero upstreams (e.g. the user picked an
|
|
85
|
+
// already-patched ~/.cursor/mcp.json that only contains the mcp-shark
|
|
86
|
+
// self-reference, and no backup was available to restore), don't blow
|
|
87
|
+
// away whatever the proxy already has. A healthy existing mcps.json is
|
|
88
|
+
// strictly more useful than an empty new one.
|
|
89
|
+
const convertedUpstreamCount = Object.keys(convertedConfig?.servers || {}).length;
|
|
90
|
+
const existingHasUpstreams = this._existingMcpsHasUpstreams(mcpsJsonPath);
|
|
91
|
+
const skipWriteToPreserveExisting =
|
|
92
|
+
convertedUpstreamCount === 0 && existingHasUpstreams && !sourceIsProxyConfig;
|
|
93
|
+
|
|
94
|
+
if (skipWriteToPreserveExisting) {
|
|
95
|
+
this.logger?.warn(
|
|
96
|
+
{ path: mcpsJsonPath, source: fileData.resolvedFilePath },
|
|
97
|
+
'Conversion yielded zero upstreams; preserving existing mcps.json instead of overwriting'
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
this.configService.writeConfigAsJson(mcpsJsonPath, convertedConfig);
|
|
101
|
+
}
|
|
65
102
|
|
|
66
103
|
// Stop existing server if running
|
|
67
104
|
if (this.serverInstance?.stop) {
|
|
@@ -86,9 +123,13 @@ export class ServerManagementService {
|
|
|
86
123
|
},
|
|
87
124
|
});
|
|
88
125
|
|
|
89
|
-
// Patch the original config file if it exists
|
|
126
|
+
// Patch the original config file if it exists. Skip this when the
|
|
127
|
+
// source IS our own proxy config — patching it would corrupt the
|
|
128
|
+
// upstream list we just wrote.
|
|
90
129
|
const patchWarning =
|
|
91
|
-
fileData.resolvedFilePath &&
|
|
130
|
+
fileData.resolvedFilePath &&
|
|
131
|
+
this.configService.fileExists(fileData.resolvedFilePath) &&
|
|
132
|
+
!sourceIsProxyConfig
|
|
92
133
|
? this.configPatchingService.patchConfigFile(fileData.resolvedFilePath, updatedConfig)
|
|
93
134
|
.warning || null
|
|
94
135
|
: null;
|
|
@@ -104,6 +145,20 @@ export class ServerManagementService {
|
|
|
104
145
|
};
|
|
105
146
|
}
|
|
106
147
|
|
|
148
|
+
_existingMcpsHasUpstreams(mcpsJsonPath) {
|
|
149
|
+
try {
|
|
150
|
+
if (!this.configService.fileExists(mcpsJsonPath)) return false;
|
|
151
|
+
const content = this.configService.readConfigFile(mcpsJsonPath);
|
|
152
|
+
const parsed = this.configService.tryParseJson(content, mcpsJsonPath);
|
|
153
|
+
if (!parsed) return false;
|
|
154
|
+
const count =
|
|
155
|
+
Object.keys(parsed.servers || {}).length + Object.keys(parsed.mcpServers || {}).length;
|
|
156
|
+
return count > 0;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
107
162
|
/**
|
|
108
163
|
* Start MCP Shark server
|
|
109
164
|
*/
|
|
@@ -1,9 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Static Rules Service
|
|
3
|
-
* Executes pattern-based security rules against MCP server definitions and traffic
|
|
3
|
+
* Executes pattern-based security rules against MCP server definitions and traffic.
|
|
4
|
+
*
|
|
5
|
+
* Combines two rule sources:
|
|
6
|
+
* 1. JS plugin rules (structural/scored — from rules/index.js)
|
|
7
|
+
* 2. Declarative JSON rule packs (pattern-based — from DeclarativeRuleEngine)
|
|
4
8
|
*/
|
|
9
|
+
import { loadDeclarativeRules } from '#core/cli/DeclarativeRuleEngine.js';
|
|
5
10
|
import { getAllRuleMetadata, getEnabledRules } from './rules/index.js';
|
|
6
11
|
|
|
12
|
+
let cachedCombinedRules = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Clear cached combined rules (e.g. after downloading new declarative packs, or before watch re-scan).
|
|
16
|
+
*/
|
|
17
|
+
export function resetStaticRulesCache() {
|
|
18
|
+
cachedCombinedRules = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load and cache the combined set of JS plugin + declarative rules.
|
|
23
|
+
*/
|
|
24
|
+
function getCombinedRules() {
|
|
25
|
+
if (cachedCombinedRules) {
|
|
26
|
+
return cachedCombinedRules;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const jsRules = getEnabledRules();
|
|
30
|
+
const declarativeRules = loadDeclarativeRules().map((rule) => ({
|
|
31
|
+
id: rule.ruleMetadata.id,
|
|
32
|
+
...rule.ruleMetadata,
|
|
33
|
+
analyzeTool: rule.analyzeTool,
|
|
34
|
+
analyzePrompt: rule.analyzePrompt,
|
|
35
|
+
analyzeResource: rule.analyzeResource,
|
|
36
|
+
analyzePacket: rule.analyzePacket,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const ruleMap = new Map();
|
|
40
|
+
for (const rule of jsRules) {
|
|
41
|
+
ruleMap.set(rule.id, rule);
|
|
42
|
+
}
|
|
43
|
+
for (const rule of declarativeRules) {
|
|
44
|
+
ruleMap.set(rule.id, rule);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
cachedCombinedRules = [...ruleMap.values()];
|
|
48
|
+
return cachedCombinedRules;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get combined metadata from JS plugins + declarative packs.
|
|
53
|
+
*/
|
|
54
|
+
function getCombinedMetadata() {
|
|
55
|
+
const jsMetadata = getAllRuleMetadata();
|
|
56
|
+
const declarativeRules = loadDeclarativeRules();
|
|
57
|
+
const declarativeMetadata = declarativeRules.map((r) => r.ruleMetadata);
|
|
58
|
+
|
|
59
|
+
const metaMap = new Map();
|
|
60
|
+
for (const m of jsMetadata) {
|
|
61
|
+
metaMap.set(m.id, m);
|
|
62
|
+
}
|
|
63
|
+
for (const m of declarativeMetadata) {
|
|
64
|
+
metaMap.set(m.id, m);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return [...metaMap.values()];
|
|
68
|
+
}
|
|
69
|
+
|
|
7
70
|
export class StaticRulesService {
|
|
8
71
|
constructor(logger) {
|
|
9
72
|
this.logger = logger;
|
|
@@ -13,7 +76,7 @@ export class StaticRulesService {
|
|
|
13
76
|
* Get all available rule metadata
|
|
14
77
|
*/
|
|
15
78
|
getRuleMetadata() {
|
|
16
|
-
return
|
|
79
|
+
return getCombinedMetadata();
|
|
17
80
|
}
|
|
18
81
|
|
|
19
82
|
/**
|
|
@@ -21,7 +84,7 @@ export class StaticRulesService {
|
|
|
21
84
|
*/
|
|
22
85
|
analyzeTool(tool, serverName = null) {
|
|
23
86
|
const findings = [];
|
|
24
|
-
const rules =
|
|
87
|
+
const rules = getCombinedRules();
|
|
25
88
|
|
|
26
89
|
for (const rule of rules) {
|
|
27
90
|
try {
|
|
@@ -49,7 +112,7 @@ export class StaticRulesService {
|
|
|
49
112
|
*/
|
|
50
113
|
analyzePrompt(prompt, serverName = null) {
|
|
51
114
|
const findings = [];
|
|
52
|
-
const rules =
|
|
115
|
+
const rules = getCombinedRules();
|
|
53
116
|
|
|
54
117
|
for (const rule of rules) {
|
|
55
118
|
try {
|
|
@@ -77,7 +140,7 @@ export class StaticRulesService {
|
|
|
77
140
|
*/
|
|
78
141
|
analyzeResource(resource, serverName = null) {
|
|
79
142
|
const findings = [];
|
|
80
|
-
const rules =
|
|
143
|
+
const rules = getCombinedRules();
|
|
81
144
|
|
|
82
145
|
for (const rule of rules) {
|
|
83
146
|
try {
|
|
@@ -105,7 +168,7 @@ export class StaticRulesService {
|
|
|
105
168
|
*/
|
|
106
169
|
analyzePacket(packet, sessionId = null) {
|
|
107
170
|
const findings = [];
|
|
108
|
-
const rules =
|
|
171
|
+
const rules = getCombinedRules();
|
|
109
172
|
|
|
110
173
|
for (const rule of rules) {
|
|
111
174
|
try {
|
|
@@ -136,21 +199,18 @@ export class StaticRulesService {
|
|
|
136
199
|
const serverName = serverConfig.name || 'unknown';
|
|
137
200
|
const findings = [];
|
|
138
201
|
|
|
139
|
-
// Analyze tools
|
|
140
202
|
if (serverConfig.tools && Array.isArray(serverConfig.tools)) {
|
|
141
203
|
for (const tool of serverConfig.tools) {
|
|
142
204
|
findings.push(...this.analyzeTool(tool, serverName));
|
|
143
205
|
}
|
|
144
206
|
}
|
|
145
207
|
|
|
146
|
-
// Analyze prompts
|
|
147
208
|
if (serverConfig.prompts && Array.isArray(serverConfig.prompts)) {
|
|
148
209
|
for (const prompt of serverConfig.prompts) {
|
|
149
210
|
findings.push(...this.analyzePrompt(prompt, serverName));
|
|
150
211
|
}
|
|
151
212
|
}
|
|
152
213
|
|
|
153
|
-
// Analyze resources
|
|
154
214
|
if (serverConfig.resources && Array.isArray(serverConfig.resources)) {
|
|
155
215
|
for (const resource of serverConfig.resources) {
|
|
156
216
|
findings.push(...this.analyzeResource(resource, serverName));
|
|
@@ -196,22 +256,18 @@ export class StaticRulesService {
|
|
|
196
256
|
};
|
|
197
257
|
|
|
198
258
|
for (const finding of findings) {
|
|
199
|
-
// By severity
|
|
200
259
|
if (summary.bySeverity[finding.severity] !== undefined) {
|
|
201
260
|
summary.bySeverity[finding.severity]++;
|
|
202
261
|
}
|
|
203
262
|
|
|
204
|
-
// By OWASP ID
|
|
205
263
|
if (finding.owasp_id) {
|
|
206
264
|
summary.byOwasp[finding.owasp_id] = (summary.byOwasp[finding.owasp_id] || 0) + 1;
|
|
207
265
|
}
|
|
208
266
|
|
|
209
|
-
// By server
|
|
210
267
|
if (finding.server_name) {
|
|
211
268
|
summary.byServer[finding.server_name] = (summary.byServer[finding.server_name] || 0) + 1;
|
|
212
269
|
}
|
|
213
270
|
|
|
214
|
-
// By type
|
|
215
271
|
if (summary.byType[finding.finding_type] !== undefined) {
|
|
216
272
|
summary.byType[finding.finding_type]++;
|
|
217
273
|
}
|
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
* Hooks into the audit logging pipeline
|
|
5
5
|
*/
|
|
6
6
|
export class TrafficAnalysisService {
|
|
7
|
-
constructor(
|
|
7
|
+
constructor(
|
|
8
|
+
staticRulesService,
|
|
9
|
+
securityFindingsRepository,
|
|
10
|
+
logger,
|
|
11
|
+
trafficToxicFlowService = null
|
|
12
|
+
) {
|
|
8
13
|
this.staticRulesService = staticRulesService;
|
|
9
14
|
this.findingsRepository = securityFindingsRepository;
|
|
10
15
|
this.logger = logger;
|
|
16
|
+
this.trafficToxicFlowService = trafficToxicFlowService;
|
|
11
17
|
this.enabled = true;
|
|
12
18
|
}
|
|
13
19
|
|
|
@@ -96,6 +102,18 @@ export class TrafficAnalysisService {
|
|
|
96
102
|
);
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
if (this.trafficToxicFlowService) {
|
|
106
|
+
try {
|
|
107
|
+
this.trafficToxicFlowService.ingestFromTrafficResponse({
|
|
108
|
+
mcpServerName: packetData.mcpServerName ?? null,
|
|
109
|
+
sessionId: packetData.sessionId ?? null,
|
|
110
|
+
body: packetData.body,
|
|
111
|
+
});
|
|
112
|
+
} catch (err) {
|
|
113
|
+
this.logger?.error({ error: err.message }, 'Traffic toxic flow ingest failed');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
99
117
|
return findings;
|
|
100
118
|
} catch (error) {
|
|
101
119
|
this.logger?.error({ error: error.message }, 'Error analyzing response packet');
|