@mcp-shark/mcp-shark 1.5.13 → 1.6.0
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 +396 -59
- 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 +224 -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/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/container/DependencyContainer.js +4 -1
- package/core/repositories/PacketRepository.js +16 -0
- package/core/services/AuditService.js +2 -0
- 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/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-CiCSDYf-.js → index-Buah4fNS.js} +32 -32
- package/ui/dist/index.html +1 -1
- package/ui/server/controllers/RequestController.js +9 -1
- package/ui/server/controllers/SecurityFindingsController.js +46 -1
- package/ui/server/routes/requests.js +8 -1
- package/ui/server/routes/security.js +5 -1
- package/ui/server/setup.js +2 -0
- 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 -0
- package/ui/server/swagger/swagger.js +5 -1
- package/ui/src/Security.jsx +10 -0
- package/ui/src/components/Security/ScannerContent.jsx +30 -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/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/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<title>MCP Shark</title>
|
|
8
8
|
<link rel="icon" type="image/png" href="/og-image.png" />
|
|
9
9
|
<link rel="apple-touch-icon" href="/og-image.png" />
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-Buah4fNS.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-Cc-IUa83.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
@@ -8,11 +8,18 @@ import { RequestFilters } from '#core/models/RequestFilters.js';
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export class RequestController {
|
|
11
|
-
constructor(
|
|
11
|
+
constructor(
|
|
12
|
+
requestService,
|
|
13
|
+
exportService,
|
|
14
|
+
serializationLib,
|
|
15
|
+
logger,
|
|
16
|
+
trafficToxicFlowService = null
|
|
17
|
+
) {
|
|
12
18
|
this.requestService = requestService;
|
|
13
19
|
this.exportService = exportService;
|
|
14
20
|
this.serializationLib = serializationLib;
|
|
15
21
|
this.logger = logger;
|
|
22
|
+
this.trafficToxicFlowService = trafficToxicFlowService;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
/**
|
|
@@ -90,6 +97,7 @@ export class RequestController {
|
|
|
90
97
|
clearRequests(_req, res) {
|
|
91
98
|
try {
|
|
92
99
|
const result = this.requestService.clearRequests();
|
|
100
|
+
this.trafficToxicFlowService?.clear();
|
|
93
101
|
res.json({
|
|
94
102
|
success: true,
|
|
95
103
|
message: `Cleared ${result.clearedTables.length} table(s): ${result.clearedTables.join(', ')}. All captured traffic has been cleared.`,
|
|
@@ -5,10 +5,16 @@ import { handleError, handleValidationError } from '../utils/errorHandler.js';
|
|
|
5
5
|
* Controller for Security Detection and Findings HTTP endpoints
|
|
6
6
|
*/
|
|
7
7
|
export class SecurityFindingsController {
|
|
8
|
-
constructor(
|
|
8
|
+
constructor(
|
|
9
|
+
securityDetectionService,
|
|
10
|
+
serverManagementService,
|
|
11
|
+
logger,
|
|
12
|
+
trafficToxicFlowService = null
|
|
13
|
+
) {
|
|
9
14
|
this.securityService = securityDetectionService;
|
|
10
15
|
this.serverManagementService = serverManagementService;
|
|
11
16
|
this.logger = logger;
|
|
17
|
+
this.trafficToxicFlowService = trafficToxicFlowService;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
/**
|
|
@@ -198,6 +204,7 @@ export class SecurityFindingsController {
|
|
|
198
204
|
clearFindings = (_req, res) => {
|
|
199
205
|
try {
|
|
200
206
|
const deletedCount = this.securityService.clearAllFindings();
|
|
207
|
+
this.trafficToxicFlowService?.clear();
|
|
201
208
|
return res.json({
|
|
202
209
|
success: true,
|
|
203
210
|
message: `Cleared ${deletedCount} finding${deletedCount !== 1 ? 's' : ''}`,
|
|
@@ -229,4 +236,42 @@ export class SecurityFindingsController {
|
|
|
229
236
|
handleError(error, res, this.logger, 'Error deleting scan findings');
|
|
230
237
|
}
|
|
231
238
|
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Cross-server toxic flows from observed tools/list traffic (live registry + last recompute).
|
|
242
|
+
*/
|
|
243
|
+
getTrafficToxicFlows = (_req, res) => {
|
|
244
|
+
try {
|
|
245
|
+
if (!this.trafficToxicFlowService) {
|
|
246
|
+
return res.status(StatusCodes.SERVICE_UNAVAILABLE).json({
|
|
247
|
+
success: false,
|
|
248
|
+
error: 'Traffic toxic flow service is not available',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return res.json(this.trafficToxicFlowService.getSnapshot());
|
|
252
|
+
} catch (error) {
|
|
253
|
+
handleError(error, res, this.logger, 'Error getting traffic toxic flows');
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Rebuild toxic-flow model from stored response packets (batch replay).
|
|
259
|
+
*/
|
|
260
|
+
replayTrafficToxicFlows = (_req, res) => {
|
|
261
|
+
try {
|
|
262
|
+
if (!this.trafficToxicFlowService) {
|
|
263
|
+
return res.status(StatusCodes.SERVICE_UNAVAILABLE).json({
|
|
264
|
+
success: false,
|
|
265
|
+
error: 'Traffic toxic flow service is not available',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const stats = this.trafficToxicFlowService.rebuildFromDatabase();
|
|
269
|
+
return res.json({
|
|
270
|
+
...this.trafficToxicFlowService.getSnapshot(),
|
|
271
|
+
replay: stats,
|
|
272
|
+
});
|
|
273
|
+
} catch (error) {
|
|
274
|
+
handleError(error, res, this.logger, 'Error replaying traffic toxic flows');
|
|
275
|
+
}
|
|
276
|
+
};
|
|
232
277
|
}
|
|
@@ -5,8 +5,15 @@ export function createRequestsRoutes(container) {
|
|
|
5
5
|
const exportService = container.getService('export');
|
|
6
6
|
const serializationLib = container.getLibrary('serialization');
|
|
7
7
|
const logger = container.getLibrary('logger');
|
|
8
|
+
const trafficToxicFlowService = container.getService('trafficToxicFlow');
|
|
8
9
|
|
|
9
|
-
const controller = new RequestController(
|
|
10
|
+
const controller = new RequestController(
|
|
11
|
+
requestService,
|
|
12
|
+
exportService,
|
|
13
|
+
serializationLib,
|
|
14
|
+
logger,
|
|
15
|
+
trafficToxicFlowService
|
|
16
|
+
);
|
|
10
17
|
|
|
11
18
|
const router = {};
|
|
12
19
|
|
|
@@ -13,10 +13,12 @@ export function createSecurityRoutes(container) {
|
|
|
13
13
|
const yaraEngineService = container.getService('yaraEngine');
|
|
14
14
|
const logger = container.getLibrary('logger');
|
|
15
15
|
|
|
16
|
+
const trafficToxicFlowService = container.getService('trafficToxicFlow');
|
|
16
17
|
const findingsController = new SecurityFindingsController(
|
|
17
18
|
securityDetectionService,
|
|
18
19
|
serverManagementService,
|
|
19
|
-
logger
|
|
20
|
+
logger,
|
|
21
|
+
trafficToxicFlowService
|
|
20
22
|
);
|
|
21
23
|
|
|
22
24
|
const sourcesController = new RuleSourcesController(
|
|
@@ -40,6 +42,8 @@ export function createSecurityRoutes(container) {
|
|
|
40
42
|
router.getScanHistory = findingsController.getScanHistory;
|
|
41
43
|
router.clearFindings = findingsController.clearFindings;
|
|
42
44
|
router.deleteScanFindings = findingsController.deleteScanFindings;
|
|
45
|
+
router.getTrafficToxicFlows = findingsController.getTrafficToxicFlows;
|
|
46
|
+
router.replayTrafficToxicFlows = findingsController.replayTrafficToxicFlows;
|
|
43
47
|
|
|
44
48
|
// YARA engine (RuleSourcesController)
|
|
45
49
|
router.getEngineStatus = sourcesController.getEngineStatus;
|
package/ui/server/setup.js
CHANGED
|
@@ -147,6 +147,8 @@ export function createUIServer() {
|
|
|
147
147
|
app.get('/api/security/history', securityRoutes.getScanHistory);
|
|
148
148
|
app.post('/api/security/findings/clear', securityRoutes.clearFindings);
|
|
149
149
|
app.delete('/api/security/scan/:scanId', securityRoutes.deleteScanFindings);
|
|
150
|
+
app.get('/api/security/traffic-toxic-flows', securityRoutes.getTrafficToxicFlows);
|
|
151
|
+
app.post('/api/security/traffic-toxic-flows/replay', securityRoutes.replayTrafficToxicFlows);
|
|
150
152
|
|
|
151
153
|
// Security routes - YARA engine
|
|
152
154
|
app.get('/api/security/engine/status', securityRoutes.getEngineStatus);
|
|
@@ -72,5 +72,60 @@ export const components = {
|
|
|
72
72
|
results: { type: 'object' },
|
|
73
73
|
},
|
|
74
74
|
},
|
|
75
|
+
TrafficToxicFlowItem: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
risk: { type: 'string', example: 'HIGH' },
|
|
79
|
+
title: { type: 'string' },
|
|
80
|
+
source: { type: 'string' },
|
|
81
|
+
target: { type: 'string' },
|
|
82
|
+
scenario: { type: 'string' },
|
|
83
|
+
sourceIde: { type: 'string' },
|
|
84
|
+
targetIde: { type: 'string' },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
TrafficToxicFlowServerMeta: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
name: { type: 'string' },
|
|
91
|
+
toolCount: { type: 'integer' },
|
|
92
|
+
updatedAt: { type: 'integer' },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
TrafficToxicFlowSnapshot: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
success: { type: 'boolean', example: true },
|
|
99
|
+
toxicFlows: {
|
|
100
|
+
type: 'array',
|
|
101
|
+
items: { $ref: '#/components/schemas/TrafficToxicFlowItem' },
|
|
102
|
+
},
|
|
103
|
+
servers: {
|
|
104
|
+
type: 'array',
|
|
105
|
+
items: { $ref: '#/components/schemas/TrafficToxicFlowServerMeta' },
|
|
106
|
+
},
|
|
107
|
+
computedAt: { type: 'integer', nullable: true },
|
|
108
|
+
lastReplayPacketCount: { type: 'integer' },
|
|
109
|
+
note: { type: 'string' },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
TrafficToxicFlowReplayResponse: {
|
|
113
|
+
allOf: [
|
|
114
|
+
{ $ref: '#/components/schemas/TrafficToxicFlowSnapshot' },
|
|
115
|
+
{
|
|
116
|
+
type: 'object',
|
|
117
|
+
properties: {
|
|
118
|
+
replay: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {
|
|
121
|
+
packetRows: { type: 'integer' },
|
|
122
|
+
serverCount: { type: 'integer' },
|
|
123
|
+
flowCount: { type: 'integer' },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
75
130
|
},
|
|
76
131
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Analysis — traffic-derived toxic flow endpoints
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const securityTrafficFlowsPaths = {
|
|
6
|
+
'/api/security/traffic-toxic-flows': {
|
|
7
|
+
get: {
|
|
8
|
+
tags: ['Security'],
|
|
9
|
+
summary: 'Get toxic flows from proxy traffic',
|
|
10
|
+
description:
|
|
11
|
+
'Returns the latest cross-server toxic-flow heuristics built from tools/list responses observed on the HTTP proxy. In-memory model; use replay to rebuild from the packet database.',
|
|
12
|
+
responses: {
|
|
13
|
+
200: {
|
|
14
|
+
description: 'Snapshot with toxicFlows, servers, computedAt, note',
|
|
15
|
+
content: {
|
|
16
|
+
'application/json': {
|
|
17
|
+
schema: { $ref: '#/components/schemas/TrafficToxicFlowSnapshot' },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
503: {
|
|
22
|
+
description: 'Traffic toxic flow service unavailable',
|
|
23
|
+
content: {
|
|
24
|
+
'application/json': {
|
|
25
|
+
schema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
success: { type: 'boolean', example: false },
|
|
29
|
+
error: { type: 'string' },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
'/api/security/traffic-toxic-flows/replay': {
|
|
39
|
+
post: {
|
|
40
|
+
tags: ['Security'],
|
|
41
|
+
summary: 'Replay toxic flows from stored packets',
|
|
42
|
+
description:
|
|
43
|
+
'Scans stored HTTP response packets for tools/list-style JSON-RPC results and rebuilds the toxic-flow registry.',
|
|
44
|
+
responses: {
|
|
45
|
+
200: {
|
|
46
|
+
description: 'Snapshot plus replay stats (packetRows, serverCount, flowCount)',
|
|
47
|
+
content: {
|
|
48
|
+
'application/json': {
|
|
49
|
+
schema: { $ref: '#/components/schemas/TrafficToxicFlowReplayResponse' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
503: {
|
|
54
|
+
description: 'Traffic toxic flow service unavailable',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -11,6 +11,7 @@ import { helpPaths } from './paths/help.js';
|
|
|
11
11
|
import { logsPaths } from './paths/logs.js';
|
|
12
12
|
import { playgroundPaths } from './paths/playground.js';
|
|
13
13
|
import { requestsPaths } from './paths/requests.js';
|
|
14
|
+
import { securityTrafficFlowsPaths } from './paths/securityTrafficFlows.js';
|
|
14
15
|
import { serverManagementPaths } from './paths/serverManagement.js';
|
|
15
16
|
import { sessionsPaths } from './paths/sessions.js';
|
|
16
17
|
import { settingsPaths } from './paths/settings.js';
|
|
@@ -22,6 +23,7 @@ import { statisticsPaths } from './paths/statistics.js';
|
|
|
22
23
|
*/
|
|
23
24
|
export const paths = {
|
|
24
25
|
...requestsPaths,
|
|
26
|
+
...securityTrafficFlowsPaths,
|
|
25
27
|
...sessionsPaths,
|
|
26
28
|
...conversationsPaths,
|
|
27
29
|
...statisticsPaths,
|
|
@@ -4,7 +4,7 @@ export const swaggerSpec = {
|
|
|
4
4
|
openapi: '3.0.0',
|
|
5
5
|
info: {
|
|
6
6
|
title: 'MCP Shark API',
|
|
7
|
-
version: '1.
|
|
7
|
+
version: '1.6.0',
|
|
8
8
|
description:
|
|
9
9
|
'API documentation for MCP Shark - A powerful monitoring and debugging tool for Model Context Protocol (MCP) servers. Provides deep visibility into every request and response.',
|
|
10
10
|
contact: {
|
|
@@ -31,6 +31,10 @@ export const swaggerSpec = {
|
|
|
31
31
|
{ name: 'Playground', description: 'MCP playground for testing tools and resources' },
|
|
32
32
|
{ name: 'Smart Scan', description: 'AI-powered security analysis for MCP servers' },
|
|
33
33
|
{ name: 'Settings', description: 'Application settings' },
|
|
34
|
+
{
|
|
35
|
+
name: 'Security',
|
|
36
|
+
description: 'Local static analysis findings and traffic-derived toxic flows (HTTP proxy)',
|
|
37
|
+
},
|
|
34
38
|
],
|
|
35
39
|
paths,
|
|
36
40
|
components,
|
package/ui/src/Security.jsx
CHANGED
|
@@ -30,6 +30,11 @@ function Security({ onNavigateToSmartScan, onNavigateToSetup }) {
|
|
|
30
30
|
runningServersCount,
|
|
31
31
|
// Scan complete state
|
|
32
32
|
scanComplete,
|
|
33
|
+
trafficToxicSnapshot,
|
|
34
|
+
trafficToxicLoading,
|
|
35
|
+
trafficToxicError,
|
|
36
|
+
loadTrafficToxicFlows,
|
|
37
|
+
replayTrafficToxicFlows,
|
|
33
38
|
// YARA rules
|
|
34
39
|
communityRules,
|
|
35
40
|
engineStatus,
|
|
@@ -103,6 +108,11 @@ function Security({ onNavigateToSmartScan, onNavigateToSetup }) {
|
|
|
103
108
|
showHistory={showHistory}
|
|
104
109
|
serversAvailable={runningServersCount > 0}
|
|
105
110
|
scanComplete={scanComplete}
|
|
111
|
+
trafficToxicSnapshot={trafficToxicSnapshot}
|
|
112
|
+
trafficToxicLoading={trafficToxicLoading}
|
|
113
|
+
trafficToxicError={trafficToxicError}
|
|
114
|
+
loadTrafficToxicFlows={loadTrafficToxicFlows}
|
|
115
|
+
replayTrafficToxicFlows={replayTrafficToxicFlows}
|
|
106
116
|
/>
|
|
107
117
|
)}
|
|
108
118
|
|
|
@@ -7,7 +7,7 @@ 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
12
|
import CategoryView from './CategoryView/index.js';
|
|
13
13
|
import ErrorDisplay from './ErrorDisplay.jsx';
|
|
@@ -18,6 +18,10 @@ import ScanningProgress from './ScanningProgress.jsx';
|
|
|
18
18
|
import SecurityCharts from './SecurityCharts/index.js';
|
|
19
19
|
import SecuritySummary from './SecuritySummary.jsx';
|
|
20
20
|
import TargetView from './TargetView/index.js';
|
|
21
|
+
import TrafficToxicFlowsPanel from './TrafficToxicFlowsPanel.jsx';
|
|
22
|
+
|
|
23
|
+
/** Keep proxy toxic-flow snapshot aligned when traffic/findings are cleared on other tabs. */
|
|
24
|
+
const TRAFFIC_TOXIC_POLL_MS = 20_000;
|
|
21
25
|
|
|
22
26
|
const VIEW_MODES = [
|
|
23
27
|
{ id: 'dashboard', label: 'Dashboard', icon: IconChartBar },
|
|
@@ -162,11 +166,26 @@ export default function ScannerContent({
|
|
|
162
166
|
showHistory,
|
|
163
167
|
serversAvailable,
|
|
164
168
|
scanComplete,
|
|
169
|
+
trafficToxicSnapshot,
|
|
170
|
+
trafficToxicLoading,
|
|
171
|
+
trafficToxicError,
|
|
172
|
+
loadTrafficToxicFlows,
|
|
173
|
+
replayTrafficToxicFlows,
|
|
165
174
|
}) {
|
|
166
175
|
const [viewMode, setViewMode] = useState('dashboard');
|
|
167
176
|
const hasFindings = findings && findings.length > 0;
|
|
168
177
|
const showEmpty = !error && !scanning && !hasFindings && !showHistory;
|
|
169
178
|
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (error || scanning) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
const id = setInterval(() => {
|
|
184
|
+
loadTrafficToxicFlows();
|
|
185
|
+
}, TRAFFIC_TOXIC_POLL_MS);
|
|
186
|
+
return () => clearInterval(id);
|
|
187
|
+
}, [error, scanning, loadTrafficToxicFlows]);
|
|
188
|
+
|
|
170
189
|
return (
|
|
171
190
|
<div
|
|
172
191
|
style={{
|
|
@@ -184,6 +203,16 @@ export default function ScannerContent({
|
|
|
184
203
|
<StaticAnalysisBanner onNavigateToSmartScan={onNavigateToSmartScan} />
|
|
185
204
|
)}
|
|
186
205
|
|
|
206
|
+
{!error && !scanning && (
|
|
207
|
+
<TrafficToxicFlowsPanel
|
|
208
|
+
snapshot={trafficToxicSnapshot}
|
|
209
|
+
loading={trafficToxicLoading}
|
|
210
|
+
error={trafficToxicError}
|
|
211
|
+
onRefresh={loadTrafficToxicFlows}
|
|
212
|
+
onReplay={replayTrafficToxicFlows}
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
215
|
+
|
|
187
216
|
{/* History View */}
|
|
188
217
|
{showHistory && !scanning && (
|
|
189
218
|
<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();
|