@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
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Symbol Vocabulary
|
|
3
|
+
* Geometric symbols — consistent across all output.
|
|
4
|
+
* Inspired by Biome/Astro/@clack aesthetic.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const S = {
|
|
8
|
+
pass: '◇',
|
|
9
|
+
fail: '◆',
|
|
10
|
+
warn: '▲',
|
|
11
|
+
info: '│',
|
|
12
|
+
dot: '·',
|
|
13
|
+
bar: '─',
|
|
14
|
+
arrow: '→',
|
|
15
|
+
pointer: '▸',
|
|
16
|
+
};
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
StatisticsService,
|
|
34
34
|
TokenService,
|
|
35
35
|
TrafficAnalysisService,
|
|
36
|
+
TrafficToxicFlowService,
|
|
36
37
|
YaraEngineService,
|
|
37
38
|
} from '#core/services/index.js';
|
|
38
39
|
import { ConfigParserFactory } from '#core/services/parsers/ConfigParserFactory.js';
|
|
@@ -160,10 +161,12 @@ export class DependencyContainer {
|
|
|
160
161
|
this._services.yaraEngine,
|
|
161
162
|
libs.logger
|
|
162
163
|
);
|
|
164
|
+
this._services.trafficToxicFlow = new TrafficToxicFlowService(repos.packet, libs.logger);
|
|
163
165
|
this._services.trafficAnalysis = new TrafficAnalysisService(
|
|
164
166
|
this._services.staticRules,
|
|
165
167
|
repos.securityFindings,
|
|
166
|
-
libs.logger
|
|
168
|
+
libs.logger,
|
|
169
|
+
this._services.trafficToxicFlow
|
|
167
170
|
);
|
|
168
171
|
this._services.rulesManager = new RulesManagerService(
|
|
169
172
|
repos.securityRules,
|
|
@@ -234,4 +234,20 @@ export class PacketRepository {
|
|
|
234
234
|
const stmt = this.db.prepare('SELECT MAX(timestamp_ns) as max_ts FROM packets');
|
|
235
235
|
return stmt.get();
|
|
236
236
|
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Response packets whose JSON-RPC result likely includes a tools array (tools/list success).
|
|
240
|
+
* Used to replay cross-server toxic-flow analysis from captured proxy traffic.
|
|
241
|
+
*/
|
|
242
|
+
listResponsesWithToolsList() {
|
|
243
|
+
const stmt = this.db.prepare(`
|
|
244
|
+
SELECT frame_number, session_id, remote_address, jsonrpc_result, body_json, body_raw, timestamp_ns
|
|
245
|
+
FROM packets
|
|
246
|
+
WHERE direction = 'response'
|
|
247
|
+
AND jsonrpc_result IS NOT NULL
|
|
248
|
+
AND jsonrpc_result LIKE '%"tools"%'
|
|
249
|
+
ORDER BY timestamp_ns ASC
|
|
250
|
+
`);
|
|
251
|
+
return stmt.all();
|
|
252
|
+
}
|
|
237
253
|
}
|
|
@@ -97,6 +97,7 @@ export class AuditService {
|
|
|
97
97
|
frameNumber: result.frameNumber,
|
|
98
98
|
body: options.body,
|
|
99
99
|
sessionId: result.sessionId,
|
|
100
|
+
mcpServerName: options.remoteAddress || null,
|
|
100
101
|
});
|
|
101
102
|
} catch (error) {
|
|
102
103
|
// Log but don't fail the request
|
|
@@ -172,6 +173,7 @@ export class AuditService {
|
|
|
172
173
|
frameNumber: result.frameNumber,
|
|
173
174
|
body: options.body,
|
|
174
175
|
sessionId: result.sessionId,
|
|
176
|
+
mcpServerName: options.remoteAddress || null,
|
|
175
177
|
});
|
|
176
178
|
} catch (error) {
|
|
177
179
|
// Log but don't fail the response
|
|
@@ -1,9 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Static Rules Service
|
|
3
|
-
* Executes pattern-based security rules against MCP server definitions and traffic
|
|
3
|
+
* Executes pattern-based security rules against MCP server definitions and traffic.
|
|
4
|
+
*
|
|
5
|
+
* Combines two rule sources:
|
|
6
|
+
* 1. JS plugin rules (structural/scored — from rules/index.js)
|
|
7
|
+
* 2. Declarative JSON rule packs (pattern-based — from DeclarativeRuleEngine)
|
|
4
8
|
*/
|
|
9
|
+
import { loadDeclarativeRules } from '#core/cli/DeclarativeRuleEngine.js';
|
|
5
10
|
import { getAllRuleMetadata, getEnabledRules } from './rules/index.js';
|
|
6
11
|
|
|
12
|
+
let cachedCombinedRules = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Clear cached combined rules (e.g. after downloading new declarative packs, or before watch re-scan).
|
|
16
|
+
*/
|
|
17
|
+
export function resetStaticRulesCache() {
|
|
18
|
+
cachedCombinedRules = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load and cache the combined set of JS plugin + declarative rules.
|
|
23
|
+
*/
|
|
24
|
+
function getCombinedRules() {
|
|
25
|
+
if (cachedCombinedRules) {
|
|
26
|
+
return cachedCombinedRules;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const jsRules = getEnabledRules();
|
|
30
|
+
const declarativeRules = loadDeclarativeRules().map((rule) => ({
|
|
31
|
+
id: rule.ruleMetadata.id,
|
|
32
|
+
...rule.ruleMetadata,
|
|
33
|
+
analyzeTool: rule.analyzeTool,
|
|
34
|
+
analyzePrompt: rule.analyzePrompt,
|
|
35
|
+
analyzeResource: rule.analyzeResource,
|
|
36
|
+
analyzePacket: rule.analyzePacket,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const ruleMap = new Map();
|
|
40
|
+
for (const rule of jsRules) {
|
|
41
|
+
ruleMap.set(rule.id, rule);
|
|
42
|
+
}
|
|
43
|
+
for (const rule of declarativeRules) {
|
|
44
|
+
ruleMap.set(rule.id, rule);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
cachedCombinedRules = [...ruleMap.values()];
|
|
48
|
+
return cachedCombinedRules;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get combined metadata from JS plugins + declarative packs.
|
|
53
|
+
*/
|
|
54
|
+
function getCombinedMetadata() {
|
|
55
|
+
const jsMetadata = getAllRuleMetadata();
|
|
56
|
+
const declarativeRules = loadDeclarativeRules();
|
|
57
|
+
const declarativeMetadata = declarativeRules.map((r) => r.ruleMetadata);
|
|
58
|
+
|
|
59
|
+
const metaMap = new Map();
|
|
60
|
+
for (const m of jsMetadata) {
|
|
61
|
+
metaMap.set(m.id, m);
|
|
62
|
+
}
|
|
63
|
+
for (const m of declarativeMetadata) {
|
|
64
|
+
metaMap.set(m.id, m);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return [...metaMap.values()];
|
|
68
|
+
}
|
|
69
|
+
|
|
7
70
|
export class StaticRulesService {
|
|
8
71
|
constructor(logger) {
|
|
9
72
|
this.logger = logger;
|
|
@@ -13,7 +76,7 @@ export class StaticRulesService {
|
|
|
13
76
|
* Get all available rule metadata
|
|
14
77
|
*/
|
|
15
78
|
getRuleMetadata() {
|
|
16
|
-
return
|
|
79
|
+
return getCombinedMetadata();
|
|
17
80
|
}
|
|
18
81
|
|
|
19
82
|
/**
|
|
@@ -21,7 +84,7 @@ export class StaticRulesService {
|
|
|
21
84
|
*/
|
|
22
85
|
analyzeTool(tool, serverName = null) {
|
|
23
86
|
const findings = [];
|
|
24
|
-
const rules =
|
|
87
|
+
const rules = getCombinedRules();
|
|
25
88
|
|
|
26
89
|
for (const rule of rules) {
|
|
27
90
|
try {
|
|
@@ -49,7 +112,7 @@ export class StaticRulesService {
|
|
|
49
112
|
*/
|
|
50
113
|
analyzePrompt(prompt, serverName = null) {
|
|
51
114
|
const findings = [];
|
|
52
|
-
const rules =
|
|
115
|
+
const rules = getCombinedRules();
|
|
53
116
|
|
|
54
117
|
for (const rule of rules) {
|
|
55
118
|
try {
|
|
@@ -77,7 +140,7 @@ export class StaticRulesService {
|
|
|
77
140
|
*/
|
|
78
141
|
analyzeResource(resource, serverName = null) {
|
|
79
142
|
const findings = [];
|
|
80
|
-
const rules =
|
|
143
|
+
const rules = getCombinedRules();
|
|
81
144
|
|
|
82
145
|
for (const rule of rules) {
|
|
83
146
|
try {
|
|
@@ -105,7 +168,7 @@ export class StaticRulesService {
|
|
|
105
168
|
*/
|
|
106
169
|
analyzePacket(packet, sessionId = null) {
|
|
107
170
|
const findings = [];
|
|
108
|
-
const rules =
|
|
171
|
+
const rules = getCombinedRules();
|
|
109
172
|
|
|
110
173
|
for (const rule of rules) {
|
|
111
174
|
try {
|
|
@@ -136,21 +199,18 @@ export class StaticRulesService {
|
|
|
136
199
|
const serverName = serverConfig.name || 'unknown';
|
|
137
200
|
const findings = [];
|
|
138
201
|
|
|
139
|
-
// Analyze tools
|
|
140
202
|
if (serverConfig.tools && Array.isArray(serverConfig.tools)) {
|
|
141
203
|
for (const tool of serverConfig.tools) {
|
|
142
204
|
findings.push(...this.analyzeTool(tool, serverName));
|
|
143
205
|
}
|
|
144
206
|
}
|
|
145
207
|
|
|
146
|
-
// Analyze prompts
|
|
147
208
|
if (serverConfig.prompts && Array.isArray(serverConfig.prompts)) {
|
|
148
209
|
for (const prompt of serverConfig.prompts) {
|
|
149
210
|
findings.push(...this.analyzePrompt(prompt, serverName));
|
|
150
211
|
}
|
|
151
212
|
}
|
|
152
213
|
|
|
153
|
-
// Analyze resources
|
|
154
214
|
if (serverConfig.resources && Array.isArray(serverConfig.resources)) {
|
|
155
215
|
for (const resource of serverConfig.resources) {
|
|
156
216
|
findings.push(...this.analyzeResource(resource, serverName));
|
|
@@ -196,22 +256,18 @@ export class StaticRulesService {
|
|
|
196
256
|
};
|
|
197
257
|
|
|
198
258
|
for (const finding of findings) {
|
|
199
|
-
// By severity
|
|
200
259
|
if (summary.bySeverity[finding.severity] !== undefined) {
|
|
201
260
|
summary.bySeverity[finding.severity]++;
|
|
202
261
|
}
|
|
203
262
|
|
|
204
|
-
// By OWASP ID
|
|
205
263
|
if (finding.owasp_id) {
|
|
206
264
|
summary.byOwasp[finding.owasp_id] = (summary.byOwasp[finding.owasp_id] || 0) + 1;
|
|
207
265
|
}
|
|
208
266
|
|
|
209
|
-
// By server
|
|
210
267
|
if (finding.server_name) {
|
|
211
268
|
summary.byServer[finding.server_name] = (summary.byServer[finding.server_name] || 0) + 1;
|
|
212
269
|
}
|
|
213
270
|
|
|
214
|
-
// By type
|
|
215
271
|
if (summary.byType[finding.finding_type] !== undefined) {
|
|
216
272
|
summary.byType[finding.finding_type]++;
|
|
217
273
|
}
|
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
* Hooks into the audit logging pipeline
|
|
5
5
|
*/
|
|
6
6
|
export class TrafficAnalysisService {
|
|
7
|
-
constructor(
|
|
7
|
+
constructor(
|
|
8
|
+
staticRulesService,
|
|
9
|
+
securityFindingsRepository,
|
|
10
|
+
logger,
|
|
11
|
+
trafficToxicFlowService = null
|
|
12
|
+
) {
|
|
8
13
|
this.staticRulesService = staticRulesService;
|
|
9
14
|
this.findingsRepository = securityFindingsRepository;
|
|
10
15
|
this.logger = logger;
|
|
16
|
+
this.trafficToxicFlowService = trafficToxicFlowService;
|
|
11
17
|
this.enabled = true;
|
|
12
18
|
}
|
|
13
19
|
|
|
@@ -96,6 +102,18 @@ export class TrafficAnalysisService {
|
|
|
96
102
|
);
|
|
97
103
|
}
|
|
98
104
|
|
|
105
|
+
if (this.trafficToxicFlowService) {
|
|
106
|
+
try {
|
|
107
|
+
this.trafficToxicFlowService.ingestFromTrafficResponse({
|
|
108
|
+
mcpServerName: packetData.mcpServerName ?? null,
|
|
109
|
+
sessionId: packetData.sessionId ?? null,
|
|
110
|
+
body: packetData.body,
|
|
111
|
+
});
|
|
112
|
+
} catch (err) {
|
|
113
|
+
this.logger?.error({ error: err.message }, 'Traffic toxic flow ingest failed');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
99
117
|
return findings;
|
|
100
118
|
} catch (error) {
|
|
101
119
|
this.logger?.error({ error: error.message }, 'Error analyzing response packet');
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-server toxic-flow analysis from observed MCP traffic (tools/list responses).
|
|
3
|
+
* Complements CLI static scan: same analyzeToxicFlows heuristics, fed by proxy captures.
|
|
4
|
+
*/
|
|
5
|
+
import { analyzeToxicFlows } from '#core/cli/ToxicFlowAnalyzer.js';
|
|
6
|
+
import {
|
|
7
|
+
toolsFromJsonrpcResultString,
|
|
8
|
+
toolsFromTrafficResponseBody,
|
|
9
|
+
} from './toolsListFromTrafficParser.js';
|
|
10
|
+
|
|
11
|
+
const TRAFFIC_IDE = 'Traffic';
|
|
12
|
+
const DEBOUNCE_MS = 400;
|
|
13
|
+
|
|
14
|
+
function serverKey(mcpServerName, sessionId) {
|
|
15
|
+
if (mcpServerName && String(mcpServerName).trim()) {
|
|
16
|
+
return String(mcpServerName).trim();
|
|
17
|
+
}
|
|
18
|
+
if (sessionId && String(sessionId).trim()) {
|
|
19
|
+
return `session:${String(sessionId).trim()}`;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class TrafficToxicFlowService {
|
|
25
|
+
constructor(packetRepository, logger) {
|
|
26
|
+
this.packetRepository = packetRepository;
|
|
27
|
+
this.logger = logger;
|
|
28
|
+
/** @type {Map<string, { tools: object[], updatedAt: number }>} */
|
|
29
|
+
this._byServer = new Map();
|
|
30
|
+
this._debounceTimer = null;
|
|
31
|
+
this._lastFlows = [];
|
|
32
|
+
this._lastComputedAt = null;
|
|
33
|
+
this._lastReplayPacketCount = 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Ingest a proxied JSON-RPC response (live path).
|
|
38
|
+
* @param {{ mcpServerName?: string|null, sessionId?: string|null, body?: unknown }} packetData
|
|
39
|
+
*/
|
|
40
|
+
ingestFromTrafficResponse(packetData) {
|
|
41
|
+
const tools = toolsFromTrafficResponseBody(packetData?.body);
|
|
42
|
+
if (!tools?.length) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const key = serverKey(packetData?.mcpServerName, packetData?.sessionId);
|
|
46
|
+
if (!key) {
|
|
47
|
+
this.logger?.debug?.('Traffic toxic flow: skip tools/list — no server name or session');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this._byServer.set(key, { tools, updatedAt: Date.now() });
|
|
51
|
+
this._scheduleRecompute();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_scheduleRecompute() {
|
|
55
|
+
if (this._debounceTimer) {
|
|
56
|
+
clearTimeout(this._debounceTimer);
|
|
57
|
+
}
|
|
58
|
+
this._debounceTimer = setTimeout(() => {
|
|
59
|
+
this._debounceTimer = null;
|
|
60
|
+
this._recomputeNow();
|
|
61
|
+
}, DEBOUNCE_MS);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_serversForAnalyzer() {
|
|
65
|
+
const servers = [];
|
|
66
|
+
for (const [name, entry] of this._byServer.entries()) {
|
|
67
|
+
servers.push({
|
|
68
|
+
name,
|
|
69
|
+
ide: TRAFFIC_IDE,
|
|
70
|
+
config: {},
|
|
71
|
+
tools: entry.tools,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return servers;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_recomputeNow() {
|
|
78
|
+
const servers = this._serversForAnalyzer();
|
|
79
|
+
this._lastFlows = servers.length >= 2 ? analyzeToxicFlows(servers) : [];
|
|
80
|
+
this._lastComputedAt = Date.now();
|
|
81
|
+
this.logger?.debug?.(
|
|
82
|
+
{ serverCount: servers.length, flowCount: this._lastFlows.length },
|
|
83
|
+
'Traffic toxic flows recomputed'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Batch replay: rebuild registry from sqlite packets, then recompute.
|
|
89
|
+
* @returns {{ packetRows: number, serverCount: number, flowCount: number }}
|
|
90
|
+
*/
|
|
91
|
+
rebuildFromDatabase() {
|
|
92
|
+
if (!this.packetRepository?.listResponsesWithToolsList) {
|
|
93
|
+
this.logger?.warn?.('Traffic toxic flow replay: packet repository unavailable');
|
|
94
|
+
return { packetRows: 0, serverCount: 0, flowCount: 0 };
|
|
95
|
+
}
|
|
96
|
+
const rows = this.packetRepository.listResponsesWithToolsList();
|
|
97
|
+
this._byServer.clear();
|
|
98
|
+
this._lastReplayPacketCount = rows.length;
|
|
99
|
+
|
|
100
|
+
for (const row of rows) {
|
|
101
|
+
let tools = toolsFromJsonrpcResultString(row.jsonrpc_result);
|
|
102
|
+
if (!tools?.length) {
|
|
103
|
+
tools = toolsFromTrafficResponseBody(row.body_json || row.body_raw);
|
|
104
|
+
}
|
|
105
|
+
if (!tools?.length) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const key = serverKey(row.remote_address, row.session_id);
|
|
109
|
+
if (!key) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
this._byServer.set(key, { tools, updatedAt: Date.now() });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this._recomputeNow();
|
|
116
|
+
return {
|
|
117
|
+
packetRows: rows.length,
|
|
118
|
+
serverCount: this._byServer.size,
|
|
119
|
+
flowCount: this._lastFlows.length,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getSnapshot() {
|
|
124
|
+
const servers = [];
|
|
125
|
+
for (const [name, entry] of this._byServer.entries()) {
|
|
126
|
+
servers.push({
|
|
127
|
+
name,
|
|
128
|
+
toolCount: entry.tools.length,
|
|
129
|
+
updatedAt: entry.updatedAt,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
success: true,
|
|
134
|
+
toxicFlows: this._lastFlows,
|
|
135
|
+
servers,
|
|
136
|
+
computedAt: this._lastComputedAt,
|
|
137
|
+
lastReplayPacketCount: this._lastReplayPacketCount,
|
|
138
|
+
note:
|
|
139
|
+
'Heuristic cross-server pairs from tools seen in tools/list traffic (HTTP proxy). ' +
|
|
140
|
+
'Not runtime taint tracking. Requires at least two distinct server keys and overlapping capability rules.',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
clear() {
|
|
145
|
+
this._byServer.clear();
|
|
146
|
+
this._lastFlows = [];
|
|
147
|
+
this._lastComputedAt = null;
|
|
148
|
+
this._lastReplayPacketCount = 0;
|
|
149
|
+
if (this._debounceTimer) {
|
|
150
|
+
clearTimeout(this._debounceTimer);
|
|
151
|
+
this._debounceTimer = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Security services exports
|
|
3
3
|
* All security-related services for OWASP vulnerability detection
|
|
4
4
|
*/
|
|
5
|
-
export { StaticRulesService } from './StaticRulesService.js';
|
|
5
|
+
export { StaticRulesService, resetStaticRulesCache } from './StaticRulesService.js';
|
|
6
6
|
export { SecurityDetectionService } from './SecurityDetectionService.js';
|
|
7
7
|
export { TrafficAnalysisService } from './TrafficAnalysisService.js';
|
|
8
|
+
export { TrafficToxicFlowService } from './TrafficToxicFlowService.js';
|
|
8
9
|
export { YaraEngineService } from './YaraEngineService.js';
|
|
9
10
|
export { RulesManagerService } from './RulesManagerService.js';
|
|
10
11
|
export * from './YaraMatchConverter.js';
|
|
@@ -1,71 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Static security rules index
|
|
2
|
+
* Static security rules index — AUTO-GENERATED
|
|
3
|
+
* Do not edit manually. Run: npm run generate:rules
|
|
4
|
+
*
|
|
3
5
|
* Exports all OWASP MCP Top 10 + Agentic Security + General security rules
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
import * as
|
|
8
|
-
import * as
|
|
9
|
-
import * as
|
|
10
|
-
import * as
|
|
11
|
-
import * as
|
|
12
|
-
import * as
|
|
13
|
-
import * as
|
|
14
|
-
import * as
|
|
15
|
-
import * as
|
|
16
|
-
import * as
|
|
17
|
-
|
|
18
|
-
// Agentic Security Initiative (ASI) Rules
|
|
19
|
-
import * as asi01 from './scans/agentic01GoalHijack.js';
|
|
20
|
-
import * as asi02 from './scans/agentic02ToolMisuse.js';
|
|
21
|
-
import * as asi03 from './scans/agentic03IdentityAbuse.js';
|
|
22
|
-
import * as asi04 from './scans/agentic04SupplyChain.js';
|
|
23
|
-
import * as asi05 from './scans/agentic05RCE.js';
|
|
24
|
-
import * as asi06 from './scans/agentic06MemoryPoisoning.js';
|
|
25
|
-
import * as asi07 from './scans/agentic07InsecureCommunication.js';
|
|
26
|
-
import * as asi08 from './scans/agentic08CascadingFailures.js';
|
|
27
|
-
import * as asi09 from './scans/agentic09TrustExploitation.js';
|
|
28
|
-
import * as asi10 from './scans/agentic10RogueAgent.js';
|
|
29
|
-
|
|
30
|
-
// General Security Rules
|
|
31
|
-
import * as cmdInj from './scans/commandInjection.js';
|
|
32
|
-
import * as crossServer from './scans/crossServerShadowing.js';
|
|
33
|
-
import * as secrets from './scans/hardcodedSecrets.js';
|
|
34
|
-
import * as nameAmbig from './scans/toolNameAmbiguity.js';
|
|
8
|
+
import * as agentic05RCE from './scans/agentic05RCE.js';
|
|
9
|
+
import * as commandInjection from './scans/commandInjection.js';
|
|
10
|
+
import * as configPermissions from './scans/configPermissions.js';
|
|
11
|
+
import * as crossServerShadowing from './scans/crossServerShadowing.js';
|
|
12
|
+
import * as duplicateToolNames from './scans/duplicateToolNames.js';
|
|
13
|
+
import * as insecureTransport from './scans/insecureTransport.js';
|
|
14
|
+
import * as mcp05CommandInjection from './scans/mcp05CommandInjection.js';
|
|
15
|
+
import * as missingContainment from './scans/missingContainment.js';
|
|
16
|
+
import * as shellEnvInjection from './scans/shellEnvInjection.js';
|
|
17
|
+
import * as toolNameAmbiguity from './scans/toolNameAmbiguity.js';
|
|
18
|
+
import * as unsafeDefaults from './scans/unsafeDefaults.js';
|
|
35
19
|
|
|
36
20
|
/**
|
|
37
21
|
* All available static rules with their analysis functions
|
|
38
22
|
*/
|
|
39
23
|
export const staticRules = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// Agentic Security Initiative
|
|
53
|
-
'asi01-goal-hijack': asi01,
|
|
54
|
-
'asi02-tool-misuse': asi02,
|
|
55
|
-
'asi03-identity-abuse': asi03,
|
|
56
|
-
'asi04-supply-chain': asi04,
|
|
57
|
-
'asi05-rce': asi05,
|
|
58
|
-
'asi06-memory-poisoning': asi06,
|
|
59
|
-
'asi07-insecure-communication': asi07,
|
|
60
|
-
'asi08-cascading-failures': asi08,
|
|
61
|
-
'asi09-trust-exploitation': asi09,
|
|
62
|
-
'asi10-rogue-agent': asi10,
|
|
63
|
-
|
|
64
|
-
// General Security
|
|
65
|
-
'command-injection': cmdInj,
|
|
66
|
-
'cross-server-shadowing': crossServer,
|
|
67
|
-
'hardcoded-secrets': secrets,
|
|
68
|
-
'tool-name-ambiguity': nameAmbig,
|
|
24
|
+
[agentic05RCE.ruleMetadata.id]: agentic05RCE,
|
|
25
|
+
[commandInjection.ruleMetadata.id]: commandInjection,
|
|
26
|
+
[configPermissions.ruleMetadata.id]: configPermissions,
|
|
27
|
+
[crossServerShadowing.ruleMetadata.id]: crossServerShadowing,
|
|
28
|
+
[duplicateToolNames.ruleMetadata.id]: duplicateToolNames,
|
|
29
|
+
[insecureTransport.ruleMetadata.id]: insecureTransport,
|
|
30
|
+
[mcp05CommandInjection.ruleMetadata.id]: mcp05CommandInjection,
|
|
31
|
+
[missingContainment.ruleMetadata.id]: missingContainment,
|
|
32
|
+
[shellEnvInjection.ruleMetadata.id]: shellEnvInjection,
|
|
33
|
+
[toolNameAmbiguity.ruleMetadata.id]: toolNameAmbiguity,
|
|
34
|
+
[unsafeDefaults.ruleMetadata.id]: unsafeDefaults,
|
|
69
35
|
};
|
|
70
36
|
|
|
71
37
|
/**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config File Permissions Check
|
|
3
|
+
* Detects world-readable MCP config files that may expose secrets.
|
|
4
|
+
* Only applicable on Unix-like systems.
|
|
5
|
+
*/
|
|
6
|
+
import { createRuleAdapter } from '../utils/adapter.js';
|
|
7
|
+
|
|
8
|
+
const RULE_ID = 'config-permissions';
|
|
9
|
+
const OWASP_ID = 'MCP01';
|
|
10
|
+
const RECOMMENDATION =
|
|
11
|
+
'Set config file permissions to 600 (owner read/write only). Run: npx mcp-shark scan --fix';
|
|
12
|
+
|
|
13
|
+
export function scanConfigPermissions(_mcpData = {}) {
|
|
14
|
+
return {
|
|
15
|
+
toolFindings: [],
|
|
16
|
+
resourceFindings: [],
|
|
17
|
+
promptFindings: [],
|
|
18
|
+
notablePatterns: [],
|
|
19
|
+
recommendations: [RECOMMENDATION],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Analyze a config file path for permission issues
|
|
25
|
+
* Called directly by ScanService, not through the adapter
|
|
26
|
+
* @param {string} configPath - Path to the config file
|
|
27
|
+
* @param {string} permissions - Octal permission string (e.g., '644')
|
|
28
|
+
* @returns {Array} Findings
|
|
29
|
+
*/
|
|
30
|
+
export function analyzeConfigPermissions(configPath, permissions) {
|
|
31
|
+
if (process.platform === 'win32') {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
if (!permissions) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const perms = Number.parseInt(permissions, 8);
|
|
39
|
+
const worldReadable = (perms & 0o004) !== 0;
|
|
40
|
+
const groupReadable = (perms & 0o040) !== 0;
|
|
41
|
+
|
|
42
|
+
const findings = [];
|
|
43
|
+
|
|
44
|
+
if (worldReadable) {
|
|
45
|
+
findings.push({
|
|
46
|
+
rule_id: RULE_ID,
|
|
47
|
+
severity: 'high',
|
|
48
|
+
owasp_id: OWASP_ID,
|
|
49
|
+
title: `Config file is world-readable (${permissions})`,
|
|
50
|
+
description: `${configPath} has permissions ${permissions} — any user on this system can read your MCP secrets.`,
|
|
51
|
+
recommendation: RECOMMENDATION,
|
|
52
|
+
config_path: configPath,
|
|
53
|
+
confidence: 'definite',
|
|
54
|
+
fixable: true,
|
|
55
|
+
fix_type: 'chmod',
|
|
56
|
+
fix_data: { oldPerms: permissions },
|
|
57
|
+
});
|
|
58
|
+
} else if (groupReadable) {
|
|
59
|
+
findings.push({
|
|
60
|
+
rule_id: RULE_ID,
|
|
61
|
+
severity: 'medium',
|
|
62
|
+
owasp_id: OWASP_ID,
|
|
63
|
+
title: `Config file is group-readable (${permissions})`,
|
|
64
|
+
description: `${configPath} has permissions ${permissions} — group members can read your MCP secrets.`,
|
|
65
|
+
recommendation: RECOMMENDATION,
|
|
66
|
+
config_path: configPath,
|
|
67
|
+
confidence: 'probable',
|
|
68
|
+
fixable: true,
|
|
69
|
+
fix_type: 'chmod',
|
|
70
|
+
fix_data: { oldPerms: permissions },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return findings;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const adapter = createRuleAdapter(scanConfigPermissions, RULE_ID, OWASP_ID, RECOMMENDATION);
|
|
78
|
+
export const analyzeTool = adapter.analyzeTool;
|
|
79
|
+
export const analyzePrompt = adapter.analyzePrompt;
|
|
80
|
+
export const analyzeResource = adapter.analyzeResource;
|
|
81
|
+
export const analyzePacket = adapter.analyzePacket;
|
|
82
|
+
|
|
83
|
+
export const ruleMetadata = {
|
|
84
|
+
id: RULE_ID,
|
|
85
|
+
name: 'Config File Permissions',
|
|
86
|
+
owasp_id: OWASP_ID,
|
|
87
|
+
severity: 'high',
|
|
88
|
+
description: 'Detects world-readable or group-readable MCP configuration files.',
|
|
89
|
+
source: 'static',
|
|
90
|
+
type: 'general-security',
|
|
91
|
+
};
|