@mcp-shark/mcp-shark 1.5.13 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +482 -56
  2. package/bin/mcp-shark.js +146 -52
  3. package/core/cli/AutoFixEngine.js +93 -0
  4. package/core/cli/ConfigScanner.js +193 -0
  5. package/core/cli/DataLoader.js +200 -0
  6. package/core/cli/DeclarativeRuleEngine.js +363 -0
  7. package/core/cli/DoctorCommand.js +218 -0
  8. package/core/cli/FixHandlers.js +222 -0
  9. package/core/cli/HtmlReportGenerator.js +203 -0
  10. package/core/cli/IdeConfigPaths.js +175 -0
  11. package/core/cli/ListCommand.js +255 -0
  12. package/core/cli/LockCommand.js +164 -0
  13. package/core/cli/LockDiffEngine.js +152 -0
  14. package/core/cli/RuleRegistryConfig.js +131 -0
  15. package/core/cli/ScanCommand.js +244 -0
  16. package/core/cli/ScanService.js +200 -0
  17. package/core/cli/SecretDetector.js +92 -0
  18. package/core/cli/SharkScoreCalculator.js +109 -0
  19. package/core/cli/ToolClassifications.js +51 -0
  20. package/core/cli/ToxicFlowAnalyzer.js +212 -0
  21. package/core/cli/UpdateCommand.js +188 -0
  22. package/core/cli/WalkthroughGenerator.js +195 -0
  23. package/core/cli/WatchCommand.js +129 -0
  24. package/core/cli/YamlRuleEngine.js +197 -0
  25. package/core/cli/data/rule-packs/aauth-visibility.json +117 -0
  26. package/core/cli/data/rule-packs/agentic-security-2026.json +180 -0
  27. package/core/cli/data/rule-packs/general-security.json +173 -0
  28. package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
  29. package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
  30. package/core/cli/data/rule-sources.json +5 -0
  31. package/core/cli/data/secret-patterns.json +18 -0
  32. package/core/cli/data/tool-classifications.json +111 -0
  33. package/core/cli/data/toxic-flow-rules.json +47 -0
  34. package/core/cli/index.js +23 -0
  35. package/core/cli/output/Banner.js +52 -0
  36. package/core/cli/output/Formatter.js +183 -0
  37. package/core/cli/output/JsonFormatter.js +106 -0
  38. package/core/cli/output/index.js +16 -0
  39. package/core/cli/secureRegistryFetch.js +157 -0
  40. package/core/cli/symbols.js +16 -0
  41. package/core/configs/environment.js +3 -1
  42. package/core/configs/index.js +3 -64
  43. package/core/container/DependencyContainer.js +4 -1
  44. package/core/mcp-server/index.js +4 -1
  45. package/core/mcp-server/server/external/all.js +10 -3
  46. package/core/mcp-server/server/external/config.js +62 -5
  47. package/core/models/RequestFilters.js +3 -0
  48. package/core/repositories/PacketRepository.js +16 -0
  49. package/core/services/AuditService.js +2 -0
  50. package/core/services/ConfigService.js +9 -1
  51. package/core/services/ConfigTransformService.js +34 -2
  52. package/core/services/RequestService.js +58 -5
  53. package/core/services/ServerManagementService.js +59 -4
  54. package/core/services/security/StaticRulesService.js +69 -13
  55. package/core/services/security/TrafficAnalysisService.js +19 -1
  56. package/core/services/security/TrafficToxicFlowService.js +154 -0
  57. package/core/services/security/aauthGraph.js +199 -0
  58. package/core/services/security/aauthParser.js +274 -0
  59. package/core/services/security/aauthSelfTest.js +346 -0
  60. package/core/services/security/index.js +2 -1
  61. package/core/services/security/rules/index.js +25 -59
  62. package/core/services/security/rules/scans/configPermissions.js +91 -0
  63. package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
  64. package/core/services/security/rules/scans/insecureTransport.js +148 -0
  65. package/core/services/security/rules/scans/missingContainment.js +123 -0
  66. package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
  67. package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
  68. package/core/services/security/toolsListFromTrafficParser.js +70 -0
  69. package/core/tui/App.js +144 -0
  70. package/core/tui/FindingsPanel.js +115 -0
  71. package/core/tui/FixPanel.js +132 -0
  72. package/core/tui/Header.js +51 -0
  73. package/core/tui/HelpBar.js +42 -0
  74. package/core/tui/ServersPanel.js +109 -0
  75. package/core/tui/ToxicFlowsPanel.js +100 -0
  76. package/core/tui/h.js +8 -0
  77. package/core/tui/index.js +11 -0
  78. package/core/tui/render.js +22 -0
  79. package/package.json +24 -16
  80. package/ui/dist/assets/index-D6zDrtMV.js +81 -0
  81. package/ui/dist/index.html +1 -1
  82. package/ui/server/controllers/AauthController.js +279 -0
  83. package/ui/server/controllers/RequestController.js +12 -1
  84. package/ui/server/controllers/SecurityFindingsController.js +46 -1
  85. package/ui/server/routes/aauth.js +18 -0
  86. package/ui/server/routes/requests.js +8 -1
  87. package/ui/server/routes/security.js +5 -1
  88. package/ui/server/setup.js +224 -6
  89. package/ui/server/swagger/paths/components.js +55 -0
  90. package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
  91. package/ui/server/swagger/paths.js +2 -2
  92. package/ui/server/swagger/swagger.js +5 -2
  93. package/ui/server.js +1 -1
  94. package/ui/src/App.jsx +26 -52
  95. package/ui/src/PacketFilters.jsx +31 -1
  96. package/ui/src/PacketList.jsx +2 -2
  97. package/ui/src/Security.jsx +10 -0
  98. package/ui/src/TabNavigation.jsx +8 -0
  99. package/ui/src/components/AAuthBadge.jsx +92 -0
  100. package/ui/src/components/AauthExplorer/AauthExplorerGraph.jsx +231 -0
  101. package/ui/src/components/AauthExplorer/AauthExplorerView.jsx +387 -0
  102. package/ui/src/components/AauthExplorer/NodeDetailPanel.jsx +272 -0
  103. package/ui/src/components/App/ActionMenu.jsx +4 -31
  104. package/ui/src/components/App/ApiDocsButton.jsx +0 -1
  105. package/ui/src/components/App/ShutdownButton.jsx +0 -1
  106. package/ui/src/components/App/useAppState.js +19 -26
  107. package/ui/src/components/DetailsTab/AAuthIdentitySection.jsx +119 -0
  108. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +2 -0
  109. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +2 -0
  110. package/ui/src/components/DetectedPathsList.jsx +1 -5
  111. package/ui/src/components/FileInput.jsx +0 -1
  112. package/ui/src/components/PacketFilters/AAuthPostureFilter.jsx +81 -0
  113. package/ui/src/components/RequestRow/RequestRowMain.jsx +7 -1
  114. package/ui/src/components/Security/AAuthPosturePanel.jsx +360 -0
  115. package/ui/src/components/Security/ScannerContent.jsx +33 -1
  116. package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
  117. package/ui/src/components/Security/securityApi.js +15 -0
  118. package/ui/src/components/Security/useSecurity.js +60 -3
  119. package/ui/src/components/ServerControl.jsx +0 -1
  120. package/ui/src/components/TabNavigation/DesktopTabs.jsx +0 -11
  121. package/ui/src/components/TabNavigationIcons.jsx +5 -0
  122. package/ui/src/components/ViewModeTabs.jsx +0 -1
  123. package/ui/src/utils/animations.js +26 -9
  124. package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
  125. package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
  126. package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
  127. package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
  128. package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
  129. package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
  130. package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
  131. package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
  132. package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
  133. package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
  134. package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
  135. package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
  136. package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
  137. package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
  138. package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
  139. package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
  140. package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
  141. package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
  142. package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
  143. package/ui/dist/assets/index-CiCSDYf-.js +0 -97
  144. package/ui/server/routes/help.js +0 -44
  145. package/ui/server/swagger/paths/help.js +0 -82
  146. package/ui/src/HelpGuide/HelpGuideContent.jsx +0 -118
  147. package/ui/src/HelpGuide/HelpGuideFooter.jsx +0 -59
  148. package/ui/src/HelpGuide/HelpGuideHeader.jsx +0 -57
  149. package/ui/src/HelpGuide.jsx +0 -78
  150. package/ui/src/IntroTour.jsx +0 -154
  151. package/ui/src/components/App/HelpButton.jsx +0 -90
  152. package/ui/src/components/TourOverlay.jsx +0 -117
  153. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +0 -120
  154. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +0 -71
  155. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +0 -54
  156. package/ui/src/components/TourTooltip/useTooltipPosition.js +0 -135
  157. package/ui/src/components/TourTooltip.jsx +0 -91
  158. package/ui/src/config/tourSteps.jsx +0 -140
@@ -0,0 +1,360 @@
1
+ import { IconFlask, IconKey, IconRefresh } from '@tabler/icons-react';
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ import { colors, fonts } from '../../theme';
4
+
5
+ const POSTURE_META = [
6
+ { id: 'signed', label: 'Signed', dotColor: colors.success },
7
+ { id: 'aauth-aware', label: 'AAuth-aware', dotColor: colors.accentBlue },
8
+ { id: 'bearer', label: 'Bearer', dotColor: colors.warning },
9
+ { id: 'none', label: 'No auth', dotColor: colors.textTertiary },
10
+ ];
11
+
12
+ function formatPercent(part, total) {
13
+ if (!total) {
14
+ return '0%';
15
+ }
16
+ return `${Math.round((part / total) * 100)}%`;
17
+ }
18
+
19
+ /**
20
+ * Captured-traffic AAuth posture panel.
21
+ *
22
+ * Read-only summary backed by GET /api/aauth/posture, plus a one-click
23
+ * "Generate sample data" button (POST /api/aauth/self-test) that inserts
24
+ * FAKE AAuth packets so a developer can preview the views before any
25
+ * AAuth-aware MCP exists in their stack. Synthetic packets are tagged
26
+ * `user-agent: mcp-shark-self-test/1.0` and treated everywhere else as
27
+ * regular captured traffic.
28
+ *
29
+ * Always describes signals as observed; never implies cryptographic
30
+ * verification.
31
+ */
32
+ export default function AAuthPosturePanel({ onTrafficGenerated } = {}) {
33
+ const [data, setData] = useState(null);
34
+ const [_upstreams, setUpstreams] = useState([]);
35
+ const [error, setError] = useState(null);
36
+ const [loading, setLoading] = useState(false);
37
+ const [running, setRunning] = useState(false);
38
+ const [lastRun, setLastRun] = useState(null);
39
+
40
+ const load = useCallback(async () => {
41
+ setLoading(true);
42
+ setError(null);
43
+ try {
44
+ const [postureRes, upstreamsRes] = await Promise.all([
45
+ fetch('/api/aauth/posture'),
46
+ fetch('/api/aauth/upstreams'),
47
+ ]);
48
+ if (!postureRes.ok) {
49
+ throw new Error(`HTTP ${postureRes.status}`);
50
+ }
51
+ const json = await postureRes.json();
52
+ setData(json);
53
+ if (upstreamsRes.ok) {
54
+ const u = await upstreamsRes.json();
55
+ setUpstreams(u.upstreams || []);
56
+ }
57
+ } catch (err) {
58
+ setError(err.message || 'Failed to load AAuth posture');
59
+ } finally {
60
+ setLoading(false);
61
+ }
62
+ }, []);
63
+
64
+ const generateSampleTraffic = useCallback(async () => {
65
+ setRunning(true);
66
+ setError(null);
67
+ try {
68
+ const res = await fetch('/api/aauth/self-test', {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({ rounds: 2 }),
72
+ });
73
+ if (!res.ok) {
74
+ throw new Error(`HTTP ${res.status}`);
75
+ }
76
+ const result = await res.json();
77
+ setLastRun(result);
78
+ await load();
79
+ if (typeof onTrafficGenerated === 'function') {
80
+ onTrafficGenerated(result);
81
+ }
82
+ } catch (err) {
83
+ setError(err.message || 'Failed to generate sample traffic');
84
+ } finally {
85
+ setRunning(false);
86
+ }
87
+ }, [load, onTrafficGenerated]);
88
+
89
+ useEffect(() => {
90
+ load();
91
+ }, [load]);
92
+
93
+ if (error && !data) {
94
+ return null;
95
+ }
96
+ // Render even with zero traffic so the self-test button is discoverable.
97
+ if (!data) {
98
+ return null;
99
+ }
100
+
101
+ return (
102
+ <div
103
+ style={{
104
+ marginBottom: '16px',
105
+ padding: '14px 16px',
106
+ background: colors.bgCard,
107
+ border: `1px solid ${colors.borderLight}`,
108
+ borderRadius: '8px',
109
+ boxShadow: `0 1px 2px ${colors.shadowSm}`,
110
+ }}
111
+ >
112
+ <div
113
+ style={{
114
+ display: 'flex',
115
+ alignItems: 'center',
116
+ justifyContent: 'space-between',
117
+ gap: '12px',
118
+ marginBottom: '10px',
119
+ }}
120
+ >
121
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
122
+ <IconKey size={16} stroke={1.5} style={{ color: colors.accentBlue }} />
123
+ <span
124
+ style={{
125
+ fontSize: '13px',
126
+ fontFamily: fonts.body,
127
+ fontWeight: 600,
128
+ color: colors.textPrimary,
129
+ }}
130
+ >
131
+ AAuth Posture
132
+ </span>
133
+ <span
134
+ style={{
135
+ fontSize: '11px',
136
+ fontFamily: fonts.body,
137
+ color: colors.textTertiary,
138
+ }}
139
+ >
140
+ {data.total_packets > 0
141
+ ? `observed in ${data.total_packets} packet${data.total_packets === 1 ? '' : 's'} · not verified`
142
+ : 'no AAuth signals observed yet'}
143
+ </span>
144
+ </div>
145
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
146
+ <button
147
+ type="button"
148
+ onClick={generateSampleTraffic}
149
+ disabled={running}
150
+ title="Inserts fake AAuth packets so you can see how mcp-shark visualizes them. Not real traffic — synthetic packets are tagged with user-agent mcp-shark-self-test/1.0."
151
+ aria-label="Generate fake sample AAuth traffic for demo purposes"
152
+ style={{
153
+ display: 'inline-flex',
154
+ alignItems: 'center',
155
+ gap: '6px',
156
+ padding: '4px 10px',
157
+ background: colors.warning,
158
+ border: `1px solid ${colors.warning}`,
159
+ borderRadius: '6px',
160
+ color: '#fff',
161
+ fontSize: '11px',
162
+ fontFamily: fonts.body,
163
+ fontWeight: 500,
164
+ cursor: running ? 'wait' : 'pointer',
165
+ opacity: running ? 0.7 : 1,
166
+ }}
167
+ >
168
+ <IconFlask size={12} stroke={1.5} />
169
+ {running ? 'Generating…' : 'Generate sample data'}
170
+ </button>
171
+ <button
172
+ type="button"
173
+ onClick={load}
174
+ disabled={loading}
175
+ aria-label="Refresh AAuth posture"
176
+ style={{
177
+ display: 'inline-flex',
178
+ alignItems: 'center',
179
+ gap: '6px',
180
+ padding: '4px 10px',
181
+ background: 'transparent',
182
+ border: `1px solid ${colors.borderLight}`,
183
+ borderRadius: '6px',
184
+ color: colors.textSecondary,
185
+ fontSize: '11px',
186
+ fontFamily: fonts.body,
187
+ cursor: loading ? 'wait' : 'pointer',
188
+ }}
189
+ >
190
+ <IconRefresh size={12} stroke={1.5} />
191
+ Refresh
192
+ </button>
193
+ </div>
194
+ </div>
195
+
196
+ {data.total_packets === 0 && (
197
+ <div
198
+ style={{
199
+ padding: '12px',
200
+ marginBottom: '8px',
201
+ background: colors.bgSecondary,
202
+ border: `1px dashed ${colors.borderLight}`,
203
+ borderRadius: '6px',
204
+ fontSize: '12px',
205
+ fontFamily: fonts.body,
206
+ color: colors.textSecondary,
207
+ lineHeight: 1.5,
208
+ }}
209
+ >
210
+ mcp-shark hasn&rsquo;t observed any AAuth-bearing traffic yet. To see what this view looks
211
+ like with data, click <strong>Generate sample data</strong> &mdash; it inserts fake AAuth
212
+ packets (tagged{' '}
213
+ <code style={{ fontFamily: fonts.mono }}>user-agent: mcp-shark-self-test/1.0</code>) for
214
+ demo purposes only.
215
+ </div>
216
+ )}
217
+
218
+ {lastRun && (
219
+ <div
220
+ style={{
221
+ padding: '8px 10px',
222
+ marginBottom: '8px',
223
+ background: `${colors.warning}1A`,
224
+ border: `1px solid ${colors.warning}55`,
225
+ borderRadius: '6px',
226
+ fontSize: '11px',
227
+ fontFamily: fonts.body,
228
+ color: colors.textSecondary,
229
+ }}
230
+ >
231
+ <strong>Sample data inserted:</strong> {lastRun.inserted} fake packets across{' '}
232
+ {lastRun.targets?.length || 0} upstream
233
+ {(lastRun.targets?.length || 0) === 1 ? '' : 's'} · postures:{' '}
234
+ {Object.entries(lastRun.by_posture || {})
235
+ .map(([k, v]) => `${k}=${v}`)
236
+ .join(', ')}
237
+ </div>
238
+ )}
239
+
240
+ <div
241
+ style={{
242
+ display: 'grid',
243
+ gridTemplateColumns: 'repeat(4, 1fr)',
244
+ gap: '10px',
245
+ marginBottom: '10px',
246
+ }}
247
+ >
248
+ {POSTURE_META.map((p) => {
249
+ const count = data.counts?.[p.id] || 0;
250
+ const pct = formatPercent(count, data.total_packets);
251
+ return (
252
+ <div
253
+ key={p.id}
254
+ style={{
255
+ padding: '10px 12px',
256
+ background: colors.bgSecondary,
257
+ borderRadius: '6px',
258
+ border: `1px solid ${colors.borderLight}`,
259
+ }}
260
+ >
261
+ <div
262
+ style={{ display: 'flex', alignItems: 'center', gap: '6px', marginBottom: '6px' }}
263
+ >
264
+ <span
265
+ aria-hidden="true"
266
+ style={{
267
+ width: '8px',
268
+ height: '8px',
269
+ borderRadius: '50%',
270
+ background: p.dotColor,
271
+ display: 'inline-block',
272
+ }}
273
+ />
274
+ <span
275
+ style={{
276
+ fontSize: '11px',
277
+ color: colors.textSecondary,
278
+ fontFamily: fonts.body,
279
+ textTransform: 'uppercase',
280
+ letterSpacing: '0.04em',
281
+ }}
282
+ >
283
+ {p.label}
284
+ </span>
285
+ </div>
286
+ <div
287
+ style={{
288
+ fontSize: '20px',
289
+ fontFamily: fonts.body,
290
+ fontWeight: 600,
291
+ color: colors.textPrimary,
292
+ lineHeight: 1.1,
293
+ }}
294
+ >
295
+ {count}
296
+ </div>
297
+ <div
298
+ style={{
299
+ fontSize: '11px',
300
+ fontFamily: fonts.mono,
301
+ color: colors.textTertiary,
302
+ }}
303
+ >
304
+ {pct}
305
+ </div>
306
+ </div>
307
+ );
308
+ })}
309
+ </div>
310
+
311
+ {(data.unique_agents?.length > 0 || data.unique_missions?.length > 0) && (
312
+ <div
313
+ style={{
314
+ paddingTop: '8px',
315
+ borderTop: `1px dashed ${colors.borderLight}`,
316
+ display: 'flex',
317
+ gap: '24px',
318
+ flexWrap: 'wrap',
319
+ fontSize: '11px',
320
+ fontFamily: fonts.body,
321
+ color: colors.textSecondary,
322
+ }}
323
+ >
324
+ {data.unique_agents?.length > 0 && (
325
+ <span>
326
+ <strong style={{ color: colors.textPrimary }}>{data.unique_agents.length}</strong>{' '}
327
+ unique agent{data.unique_agents.length === 1 ? '' : 's'}
328
+ </span>
329
+ )}
330
+ {data.unique_missions?.length > 0 && (
331
+ <span>
332
+ <strong style={{ color: colors.textPrimary }}>{data.unique_missions.length}</strong>{' '}
333
+ mission{data.unique_missions.length === 1 ? '' : 's'} observed
334
+ </span>
335
+ )}
336
+ </div>
337
+ )}
338
+
339
+ <div
340
+ style={{
341
+ marginTop: '8px',
342
+ fontSize: '11px',
343
+ fontFamily: fonts.body,
344
+ color: colors.textTertiary,
345
+ }}
346
+ >
347
+ AAuth signals are recorded as observed only. Learn more at{' '}
348
+ <a
349
+ href="https://www.aauth.dev"
350
+ target="_blank"
351
+ rel="noreferrer noopener"
352
+ style={{ color: colors.accentBlue }}
353
+ >
354
+ aauth.dev
355
+ </a>
356
+ .
357
+ </div>
358
+ </div>
359
+ );
360
+ }
@@ -7,8 +7,9 @@ import {
7
7
  IconSparkles,
8
8
  IconTool,
9
9
  } from '@tabler/icons-react';
10
- import { useState } from 'react';
10
+ import { useEffect, useState } from 'react';
11
11
  import { colors, fonts } from '../../theme';
12
+ import AAuthPosturePanel from './AAuthPosturePanel.jsx';
12
13
  import CategoryView from './CategoryView/index.js';
13
14
  import ErrorDisplay from './ErrorDisplay.jsx';
14
15
  import FindingsTable from './FindingsTable.jsx';
@@ -18,6 +19,10 @@ import ScanningProgress from './ScanningProgress.jsx';
18
19
  import SecurityCharts from './SecurityCharts/index.js';
19
20
  import SecuritySummary from './SecuritySummary.jsx';
20
21
  import TargetView from './TargetView/index.js';
22
+ import TrafficToxicFlowsPanel from './TrafficToxicFlowsPanel.jsx';
23
+
24
+ /** Keep proxy toxic-flow snapshot aligned when traffic/findings are cleared on other tabs. */
25
+ const TRAFFIC_TOXIC_POLL_MS = 20_000;
21
26
 
22
27
  const VIEW_MODES = [
23
28
  { id: 'dashboard', label: 'Dashboard', icon: IconChartBar },
@@ -162,11 +167,26 @@ export default function ScannerContent({
162
167
  showHistory,
163
168
  serversAvailable,
164
169
  scanComplete,
170
+ trafficToxicSnapshot,
171
+ trafficToxicLoading,
172
+ trafficToxicError,
173
+ loadTrafficToxicFlows,
174
+ replayTrafficToxicFlows,
165
175
  }) {
166
176
  const [viewMode, setViewMode] = useState('dashboard');
167
177
  const hasFindings = findings && findings.length > 0;
168
178
  const showEmpty = !error && !scanning && !hasFindings && !showHistory;
169
179
 
180
+ useEffect(() => {
181
+ if (error || scanning) {
182
+ return undefined;
183
+ }
184
+ const id = setInterval(() => {
185
+ loadTrafficToxicFlows();
186
+ }, TRAFFIC_TOXIC_POLL_MS);
187
+ return () => clearInterval(id);
188
+ }, [error, scanning, loadTrafficToxicFlows]);
189
+
170
190
  return (
171
191
  <div
172
192
  style={{
@@ -184,6 +204,18 @@ export default function ScannerContent({
184
204
  <StaticAnalysisBanner onNavigateToSmartScan={onNavigateToSmartScan} />
185
205
  )}
186
206
 
207
+ {!error && !scanning && <AAuthPosturePanel />}
208
+
209
+ {!error && !scanning && (
210
+ <TrafficToxicFlowsPanel
211
+ snapshot={trafficToxicSnapshot}
212
+ loading={trafficToxicLoading}
213
+ error={trafficToxicError}
214
+ onRefresh={loadTrafficToxicFlows}
215
+ onReplay={replayTrafficToxicFlows}
216
+ />
217
+ )}
218
+
187
219
  {/* History View */}
188
220
  {showHistory && !scanning && (
189
221
  <ScanHistory
@@ -0,0 +1,253 @@
1
+ import { IconDatabase, IconGitBranch, IconRefresh } from '@tabler/icons-react';
2
+ import { colors, fonts } from '../../theme';
3
+
4
+ function riskColor(risk) {
5
+ const r = (risk || '').toUpperCase();
6
+ if (r === 'HIGH') {
7
+ return colors.error;
8
+ }
9
+ if (r === 'MEDIUM') {
10
+ return colors.warning;
11
+ }
12
+ return colors.textSecondary;
13
+ }
14
+
15
+ export default function TrafficToxicFlowsPanel({ snapshot, loading, error, onRefresh, onReplay }) {
16
+ const flows = snapshot?.toxicFlows ?? [];
17
+ const servers = snapshot?.servers ?? [];
18
+ const replay = snapshot?.replay;
19
+
20
+ return (
21
+ <div
22
+ style={{
23
+ marginBottom: '20px',
24
+ padding: '14px 16px',
25
+ background: colors.bgSecondary,
26
+ border: `1px solid ${colors.borderLight}`,
27
+ borderRadius: '8px',
28
+ }}
29
+ >
30
+ <div
31
+ style={{
32
+ display: 'flex',
33
+ alignItems: 'flex-start',
34
+ justifyContent: 'space-between',
35
+ gap: '12px',
36
+ flexWrap: 'wrap',
37
+ marginBottom: '10px',
38
+ }}
39
+ >
40
+ <div style={{ display: 'flex', alignItems: 'center', gap: '10px', minWidth: 0 }}>
41
+ <IconGitBranch
42
+ size={18}
43
+ stroke={1.5}
44
+ style={{ color: colors.textMuted, flexShrink: 0 }}
45
+ />
46
+ <div>
47
+ <div
48
+ style={{
49
+ fontSize: '13px',
50
+ fontWeight: 600,
51
+ color: colors.textPrimary,
52
+ fontFamily: fonts.body,
53
+ }}
54
+ >
55
+ Toxic flows (proxy traffic)
56
+ </div>
57
+ <div
58
+ style={{
59
+ fontSize: '11px',
60
+ color: colors.textSecondary,
61
+ fontFamily: fonts.body,
62
+ marginTop: '2px',
63
+ maxWidth: '720px',
64
+ lineHeight: 1.45,
65
+ }}
66
+ >
67
+ Heuristic cross-server pairs from <strong>tools/list</strong> responses seen through
68
+ the HTTP proxy. Run MCP clients through mcp-shark, trigger tool discovery, then
69
+ refresh or replay from the packet database. Clearing findings or all captured traffic
70
+ resets this in-memory model.
71
+ </div>
72
+ </div>
73
+ </div>
74
+ <div style={{ display: 'flex', gap: '8px', flexShrink: 0 }}>
75
+ <button
76
+ type="button"
77
+ disabled={loading}
78
+ onClick={() => onRefresh()}
79
+ style={{
80
+ display: 'flex',
81
+ alignItems: 'center',
82
+ gap: '6px',
83
+ padding: '6px 12px',
84
+ fontSize: '11px',
85
+ fontWeight: 500,
86
+ fontFamily: fonts.body,
87
+ background: colors.buttonSecondary,
88
+ border: `1px solid ${colors.borderLight}`,
89
+ borderRadius: '4px',
90
+ cursor: loading ? 'wait' : 'pointer',
91
+ color: colors.textPrimary,
92
+ }}
93
+ >
94
+ <IconRefresh size={14} stroke={1.5} />
95
+ Refresh
96
+ </button>
97
+ <button
98
+ type="button"
99
+ disabled={loading}
100
+ onClick={() => onReplay()}
101
+ style={{
102
+ display: 'flex',
103
+ alignItems: 'center',
104
+ gap: '6px',
105
+ padding: '6px 12px',
106
+ fontSize: '11px',
107
+ fontWeight: 500,
108
+ fontFamily: fonts.body,
109
+ background: colors.buttonPrimary,
110
+ border: 'none',
111
+ borderRadius: '4px',
112
+ cursor: loading ? 'wait' : 'pointer',
113
+ color: colors.textInverse,
114
+ }}
115
+ >
116
+ <IconDatabase size={14} stroke={1.5} />
117
+ Replay from DB
118
+ </button>
119
+ </div>
120
+ </div>
121
+
122
+ {error && (
123
+ <div
124
+ style={{
125
+ fontSize: '12px',
126
+ color: colors.error,
127
+ background: colors.errorBg,
128
+ padding: '8px 10px',
129
+ borderRadius: '4px',
130
+ marginBottom: '10px',
131
+ fontFamily: fonts.body,
132
+ }}
133
+ >
134
+ {error}
135
+ </div>
136
+ )}
137
+
138
+ {loading && !snapshot && !error && (
139
+ <div style={{ fontSize: '12px', color: colors.textSecondary, fontFamily: fonts.body }}>
140
+ Loading…
141
+ </div>
142
+ )}
143
+
144
+ {snapshot && !error && (
145
+ <div style={{ fontSize: '11px', color: colors.textSecondary, fontFamily: fonts.body }}>
146
+ {servers.length > 0 ? (
147
+ <span>
148
+ <strong style={{ color: colors.textPrimary }}>{servers.length}</strong> server
149
+ {servers.length !== 1 ? 's' : ''} with tool metadata
150
+ {snapshot.computedAt
151
+ ? ` · updated ${new Date(snapshot.computedAt).toLocaleString()}`
152
+ : ''}
153
+ {replay != null
154
+ ? ` · replay scanned ${replay.packetRows} packet row${replay.packetRows !== 1 ? 's' : ''}`
155
+ : ''}
156
+ </span>
157
+ ) : (
158
+ <span>
159
+ No <code style={{ fontFamily: fonts.mono }}>tools/list</code> traffic captured yet.
160
+ Use the proxy, open a client that lists tools, then Refresh or Replay from DB.
161
+ </span>
162
+ )}
163
+ </div>
164
+ )}
165
+
166
+ {flows.length > 0 && (
167
+ <ul
168
+ style={{
169
+ listStyle: 'none',
170
+ margin: '12px 0 0',
171
+ padding: 0,
172
+ display: 'flex',
173
+ flexDirection: 'column',
174
+ gap: '10px',
175
+ }}
176
+ >
177
+ {flows.map((flow, idx) => (
178
+ <li
179
+ key={`${flow.source}-${flow.target}-${flow.title}-${idx}`}
180
+ style={{
181
+ padding: '10px 12px',
182
+ background: colors.bgCard,
183
+ border: `1px solid ${colors.borderLight}`,
184
+ borderRadius: '6px',
185
+ }}
186
+ >
187
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
188
+ <span
189
+ style={{
190
+ fontSize: '10px',
191
+ fontWeight: 700,
192
+ letterSpacing: '0.04em',
193
+ color: riskColor(flow.risk),
194
+ fontFamily: fonts.body,
195
+ }}
196
+ >
197
+ {(flow.risk || '').toUpperCase()}
198
+ </span>
199
+ <span
200
+ style={{
201
+ fontSize: '12px',
202
+ fontWeight: 600,
203
+ color: colors.textPrimary,
204
+ fontFamily: fonts.body,
205
+ }}
206
+ >
207
+ {flow.title}
208
+ </span>
209
+ </div>
210
+ <div
211
+ style={{
212
+ fontSize: '11px',
213
+ color: colors.accentBlue,
214
+ fontFamily: fonts.mono,
215
+ marginTop: '4px',
216
+ }}
217
+ >
218
+ {flow.source} → {flow.target}
219
+ </div>
220
+ {flow.scenario && (
221
+ <div
222
+ style={{
223
+ fontSize: '11px',
224
+ color: colors.textSecondary,
225
+ fontFamily: fonts.body,
226
+ marginTop: '6px',
227
+ lineHeight: 1.45,
228
+ }}
229
+ >
230
+ {flow.scenario}
231
+ </div>
232
+ )}
233
+ </li>
234
+ ))}
235
+ </ul>
236
+ )}
237
+
238
+ {snapshot && servers.length > 0 && (
239
+ <div
240
+ style={{
241
+ marginTop: '12px',
242
+ fontSize: '10px',
243
+ color: colors.textTertiary,
244
+ fontFamily: fonts.body,
245
+ lineHeight: 1.4,
246
+ }}
247
+ >
248
+ {snapshot.note}
249
+ </div>
250
+ )}
251
+ </div>
252
+ );
253
+ }
@@ -66,6 +66,21 @@ export async function postClearFindings() {
66
66
  return response.json();
67
67
  }
68
68
 
69
+ /** Cross-server toxic flows from observed tools/list proxy traffic */
70
+ export async function fetchTrafficToxicFlows() {
71
+ const response = await fetch('/api/security/traffic-toxic-flows');
72
+ return response.json();
73
+ }
74
+
75
+ /** Rebuild toxic-flow model from stored response packets */
76
+ export async function postReplayTrafficToxicFlows() {
77
+ const response = await fetch('/api/security/traffic-toxic-flows/replay', {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ });
81
+ return response.json();
82
+ }
83
+
69
84
  export async function fetchCommunityRules() {
70
85
  const response = await fetch('/api/security/community-rules');
71
86
  return response.json();