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