@mcp-shark/mcp-shark 1.5.13 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +482 -56
  2. package/bin/mcp-shark.js +146 -52
  3. package/core/cli/AutoFixEngine.js +93 -0
  4. package/core/cli/ConfigScanner.js +193 -0
  5. package/core/cli/DataLoader.js +200 -0
  6. package/core/cli/DeclarativeRuleEngine.js +363 -0
  7. package/core/cli/DoctorCommand.js +218 -0
  8. package/core/cli/FixHandlers.js +222 -0
  9. package/core/cli/HtmlReportGenerator.js +203 -0
  10. package/core/cli/IdeConfigPaths.js +175 -0
  11. package/core/cli/ListCommand.js +255 -0
  12. package/core/cli/LockCommand.js +164 -0
  13. package/core/cli/LockDiffEngine.js +152 -0
  14. package/core/cli/RuleRegistryConfig.js +131 -0
  15. package/core/cli/ScanCommand.js +244 -0
  16. package/core/cli/ScanService.js +200 -0
  17. package/core/cli/SecretDetector.js +92 -0
  18. package/core/cli/SharkScoreCalculator.js +109 -0
  19. package/core/cli/ToolClassifications.js +51 -0
  20. package/core/cli/ToxicFlowAnalyzer.js +212 -0
  21. package/core/cli/UpdateCommand.js +188 -0
  22. package/core/cli/WalkthroughGenerator.js +195 -0
  23. package/core/cli/WatchCommand.js +129 -0
  24. package/core/cli/YamlRuleEngine.js +197 -0
  25. package/core/cli/data/rule-packs/aauth-visibility.json +117 -0
  26. package/core/cli/data/rule-packs/agentic-security-2026.json +180 -0
  27. package/core/cli/data/rule-packs/general-security.json +173 -0
  28. package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
  29. package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
  30. package/core/cli/data/rule-sources.json +5 -0
  31. package/core/cli/data/secret-patterns.json +18 -0
  32. package/core/cli/data/tool-classifications.json +111 -0
  33. package/core/cli/data/toxic-flow-rules.json +47 -0
  34. package/core/cli/index.js +23 -0
  35. package/core/cli/output/Banner.js +52 -0
  36. package/core/cli/output/Formatter.js +183 -0
  37. package/core/cli/output/JsonFormatter.js +106 -0
  38. package/core/cli/output/index.js +16 -0
  39. package/core/cli/secureRegistryFetch.js +157 -0
  40. package/core/cli/symbols.js +16 -0
  41. package/core/configs/environment.js +3 -1
  42. package/core/configs/index.js +3 -64
  43. package/core/container/DependencyContainer.js +4 -1
  44. package/core/mcp-server/index.js +4 -1
  45. package/core/mcp-server/server/external/all.js +10 -3
  46. package/core/mcp-server/server/external/config.js +62 -5
  47. package/core/models/RequestFilters.js +3 -0
  48. package/core/repositories/PacketRepository.js +16 -0
  49. package/core/services/AuditService.js +2 -0
  50. package/core/services/ConfigService.js +9 -1
  51. package/core/services/ConfigTransformService.js +34 -2
  52. package/core/services/RequestService.js +58 -5
  53. package/core/services/ServerManagementService.js +59 -4
  54. package/core/services/security/StaticRulesService.js +69 -13
  55. package/core/services/security/TrafficAnalysisService.js +19 -1
  56. package/core/services/security/TrafficToxicFlowService.js +154 -0
  57. package/core/services/security/aauthGraph.js +199 -0
  58. package/core/services/security/aauthParser.js +274 -0
  59. package/core/services/security/aauthSelfTest.js +346 -0
  60. package/core/services/security/index.js +2 -1
  61. package/core/services/security/rules/index.js +25 -59
  62. package/core/services/security/rules/scans/configPermissions.js +91 -0
  63. package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
  64. package/core/services/security/rules/scans/insecureTransport.js +148 -0
  65. package/core/services/security/rules/scans/missingContainment.js +123 -0
  66. package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
  67. package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
  68. package/core/services/security/toolsListFromTrafficParser.js +70 -0
  69. package/core/tui/App.js +144 -0
  70. package/core/tui/FindingsPanel.js +115 -0
  71. package/core/tui/FixPanel.js +132 -0
  72. package/core/tui/Header.js +51 -0
  73. package/core/tui/HelpBar.js +42 -0
  74. package/core/tui/ServersPanel.js +109 -0
  75. package/core/tui/ToxicFlowsPanel.js +100 -0
  76. package/core/tui/h.js +8 -0
  77. package/core/tui/index.js +11 -0
  78. package/core/tui/render.js +22 -0
  79. package/package.json +24 -16
  80. package/ui/dist/assets/index-D6zDrtMV.js +81 -0
  81. package/ui/dist/index.html +1 -1
  82. package/ui/server/controllers/AauthController.js +279 -0
  83. package/ui/server/controllers/RequestController.js +12 -1
  84. package/ui/server/controllers/SecurityFindingsController.js +46 -1
  85. package/ui/server/routes/aauth.js +18 -0
  86. package/ui/server/routes/requests.js +8 -1
  87. package/ui/server/routes/security.js +5 -1
  88. package/ui/server/setup.js +224 -6
  89. package/ui/server/swagger/paths/components.js +55 -0
  90. package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
  91. package/ui/server/swagger/paths.js +2 -2
  92. package/ui/server/swagger/swagger.js +5 -2
  93. package/ui/server.js +1 -1
  94. package/ui/src/App.jsx +26 -52
  95. package/ui/src/PacketFilters.jsx +31 -1
  96. package/ui/src/PacketList.jsx +2 -2
  97. package/ui/src/Security.jsx +10 -0
  98. package/ui/src/TabNavigation.jsx +8 -0
  99. package/ui/src/components/AAuthBadge.jsx +92 -0
  100. package/ui/src/components/AauthExplorer/AauthExplorerGraph.jsx +231 -0
  101. package/ui/src/components/AauthExplorer/AauthExplorerView.jsx +387 -0
  102. package/ui/src/components/AauthExplorer/NodeDetailPanel.jsx +272 -0
  103. package/ui/src/components/App/ActionMenu.jsx +4 -31
  104. package/ui/src/components/App/ApiDocsButton.jsx +0 -1
  105. package/ui/src/components/App/ShutdownButton.jsx +0 -1
  106. package/ui/src/components/App/useAppState.js +19 -26
  107. package/ui/src/components/DetailsTab/AAuthIdentitySection.jsx +119 -0
  108. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +2 -0
  109. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +2 -0
  110. package/ui/src/components/DetectedPathsList.jsx +1 -5
  111. package/ui/src/components/FileInput.jsx +0 -1
  112. package/ui/src/components/PacketFilters/AAuthPostureFilter.jsx +81 -0
  113. package/ui/src/components/RequestRow/RequestRowMain.jsx +7 -1
  114. package/ui/src/components/Security/AAuthPosturePanel.jsx +360 -0
  115. package/ui/src/components/Security/ScannerContent.jsx +33 -1
  116. package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
  117. package/ui/src/components/Security/securityApi.js +15 -0
  118. package/ui/src/components/Security/useSecurity.js +60 -3
  119. package/ui/src/components/ServerControl.jsx +0 -1
  120. package/ui/src/components/TabNavigation/DesktopTabs.jsx +0 -11
  121. package/ui/src/components/TabNavigationIcons.jsx +5 -0
  122. package/ui/src/components/ViewModeTabs.jsx +0 -1
  123. package/ui/src/utils/animations.js +26 -9
  124. package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
  125. package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
  126. package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
  127. package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
  128. package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
  129. package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
  130. package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
  131. package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
  132. package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
  133. package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
  134. package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
  135. package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
  136. package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
  137. package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
  138. package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
  139. package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
  140. package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
  141. package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
  142. package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
  143. package/ui/dist/assets/index-CiCSDYf-.js +0 -97
  144. package/ui/server/routes/help.js +0 -44
  145. package/ui/server/swagger/paths/help.js +0 -82
  146. package/ui/src/HelpGuide/HelpGuideContent.jsx +0 -118
  147. package/ui/src/HelpGuide/HelpGuideFooter.jsx +0 -59
  148. package/ui/src/HelpGuide/HelpGuideHeader.jsx +0 -57
  149. package/ui/src/HelpGuide.jsx +0 -78
  150. package/ui/src/IntroTour.jsx +0 -154
  151. package/ui/src/components/App/HelpButton.jsx +0 -90
  152. package/ui/src/components/TourOverlay.jsx +0 -117
  153. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +0 -120
  154. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +0 -71
  155. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +0 -54
  156. package/ui/src/components/TourTooltip/useTooltipPosition.js +0 -135
  157. package/ui/src/components/TourTooltip.jsx +0 -91
  158. package/ui/src/config/tourSteps.jsx +0 -140
@@ -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
- const type = cfg.type ?? DEFAULT_TYPE;
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
- const type = cfg.type ?? DEFAULT_TYPE;
66
- out.set(name, { type, ...cfg });
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
- if (Object.keys(convertedConfig.servers || {}).length === 0) {
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
- converted.servers = { ...normalized.servers };
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 (raw from repository)
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
- return this.packetRepository.queryRequests(repoFilters);
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 or null if not found
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
- // Write converted config to MCP Shark config path
64
- this.configService.writeConfigAsJson(mcpsJsonPath, convertedConfig);
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 && this.configService.fileExists(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 getAllRuleMetadata();
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 = getEnabledRules();
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 = getEnabledRules();
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 = getEnabledRules();
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 = getEnabledRules();
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(staticRulesService, securityFindingsRepository, logger) {
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');