@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,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AAuth self-test traffic synthesizer.
|
|
3
|
+
*
|
|
4
|
+
* Generates realistic AAuth-shaped HTTP packets and writes them through
|
|
5
|
+
* mcp-shark's normal audit path (AuditService.logRequestPacket /
|
|
6
|
+
* logResponsePacket) so they look identical to live captured traffic.
|
|
7
|
+
*
|
|
8
|
+
* Strict scope:
|
|
9
|
+
* - Pure observability fixture. No real outbound network calls.
|
|
10
|
+
* - No cryptographic signing. The Signature/Signature-Input bytes are
|
|
11
|
+
* placeholders meant to be visually inspected, not validated.
|
|
12
|
+
* - The point is to populate the Traffic, Posture, and Explorer views so
|
|
13
|
+
* a developer can see how AAuth signals are surfaced even before any
|
|
14
|
+
* production agent integration is in place.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
const KNOWN_AGENTS = [
|
|
22
|
+
{ id: 'aauth:demo-agent@example.dev', alg: 'ed25519', keyid: 'demo-key-1' },
|
|
23
|
+
{ id: 'aauth:read-only-agent@example.dev', alg: 'ecdsa-p256-sha256', keyid: 'read-key-1' },
|
|
24
|
+
{ id: 'aauth:writer-agent@example.dev', alg: 'ed25519', keyid: 'writer-key-1' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const KNOWN_MISSIONS = [
|
|
28
|
+
'mission:lookup-customer-record',
|
|
29
|
+
'mission:summarize-incident-2026-04',
|
|
30
|
+
'mission:reset-staging-database',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const KNOWN_TOOLS = ['lookup_record', 'summarize_incident', 'protected_query', 'echo'];
|
|
34
|
+
|
|
35
|
+
const ACCESS_MODES = [
|
|
36
|
+
{ mode: 'ps-managed', resource: 'https://api.example.com/records' },
|
|
37
|
+
{ mode: 'identity-based', resource: 'https://api.example.com/profiles' },
|
|
38
|
+
{ mode: 'federated', resource: 'https://api.partner.example.com/exchange' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const PLACEHOLDER_SIG =
|
|
42
|
+
'sig1=:MEUCIQDxPLACEHOLDER0000000000000000000000000000000000000000AAAAAAA=:';
|
|
43
|
+
const PLACEHOLDER_KEY_THUMBPRINT = 'sha-256=:AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AAAAAAAA=:';
|
|
44
|
+
|
|
45
|
+
function pick(list, i) {
|
|
46
|
+
return list[i % list.length];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildSignatureInput(agent) {
|
|
50
|
+
const created = Math.floor(Date.now() / 1000);
|
|
51
|
+
return `sig1=("@method" "@target-uri" "host" "content-digest");keyid="${agent.keyid}";alg="${agent.alg}";created=${created}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildJsonRpcRequest(id, toolName, missionId) {
|
|
55
|
+
return {
|
|
56
|
+
jsonrpc: '2.0',
|
|
57
|
+
id,
|
|
58
|
+
method: 'tools/call',
|
|
59
|
+
params: {
|
|
60
|
+
name: toolName,
|
|
61
|
+
arguments: { mission: missionId, ts: new Date().toISOString() },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildJsonRpcResponse(id, toolName) {
|
|
67
|
+
return {
|
|
68
|
+
jsonrpc: '2.0',
|
|
69
|
+
id,
|
|
70
|
+
result: {
|
|
71
|
+
content: [{ type: 'text', text: `synthetic ${toolName} response` }],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readMcpsConfigSafely() {
|
|
77
|
+
try {
|
|
78
|
+
const p = join(homedir(), '.mcp-shark', 'mcps.json');
|
|
79
|
+
const raw = readFileSync(p, 'utf8');
|
|
80
|
+
const parsed = JSON.parse(raw);
|
|
81
|
+
return parsed && typeof parsed.servers === 'object' ? parsed.servers : {};
|
|
82
|
+
} catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Auto-detect HTTP MCP upstreams from ~/.mcp-shark/mcps.json. We don't actually
|
|
89
|
+
* call them — we use the names/URLs to make synthetic packets feel grounded
|
|
90
|
+
* to the developer ("oh, that's the upstream I configured").
|
|
91
|
+
*/
|
|
92
|
+
export function detectHttpUpstreams() {
|
|
93
|
+
const servers = readMcpsConfigSafely();
|
|
94
|
+
const detected = [];
|
|
95
|
+
for (const [name, def] of Object.entries(servers)) {
|
|
96
|
+
if (!def || typeof def !== 'object') {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (def.type === 'http' && typeof def.url === 'string') {
|
|
100
|
+
detected.push({ name, url: def.url });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return detected;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const SCENARIOS = [
|
|
107
|
+
{
|
|
108
|
+
id: 'signed',
|
|
109
|
+
label: 'Signed AAuth call',
|
|
110
|
+
posture: 'signed',
|
|
111
|
+
buildHeaders(ctx) {
|
|
112
|
+
return {
|
|
113
|
+
host: ctx.host,
|
|
114
|
+
'content-type': 'application/json',
|
|
115
|
+
'user-agent': 'mcp-shark-self-test/1.0',
|
|
116
|
+
signature: PLACEHOLDER_SIG,
|
|
117
|
+
'signature-input': buildSignatureInput(ctx.agent),
|
|
118
|
+
'signature-key': PLACEHOLDER_KEY_THUMBPRINT,
|
|
119
|
+
'aauth-agent': ctx.agent.id,
|
|
120
|
+
'aauth-mission': ctx.mission,
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
buildResponseHeaders(ctx) {
|
|
124
|
+
return {
|
|
125
|
+
'content-type': 'application/json',
|
|
126
|
+
signature: PLACEHOLDER_SIG,
|
|
127
|
+
'signature-input': buildSignatureInput(ctx.agent),
|
|
128
|
+
'signature-key': PLACEHOLDER_KEY_THUMBPRINT,
|
|
129
|
+
'aauth-agent': ctx.agent.id,
|
|
130
|
+
'aauth-mission': ctx.mission,
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
statusCode: 200,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'challenge',
|
|
137
|
+
label: 'AAuth challenge (401)',
|
|
138
|
+
posture: 'none-then-aware',
|
|
139
|
+
buildHeaders(ctx) {
|
|
140
|
+
return {
|
|
141
|
+
host: ctx.host,
|
|
142
|
+
'content-type': 'application/json',
|
|
143
|
+
'user-agent': 'mcp-shark-self-test/1.0',
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
buildResponseHeaders(ctx) {
|
|
147
|
+
const access = ctx.access;
|
|
148
|
+
return {
|
|
149
|
+
'content-type': 'application/json',
|
|
150
|
+
'aauth-requirement': `mode="${access.mode}"; resource="${access.resource}"; jwks_uri="https://example.com/.well-known/aauth/jwks.json"`,
|
|
151
|
+
'www-authenticate': `AAuth realm="example", mode="${access.mode}", resource="${access.resource}"`,
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
statusCode: 401,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'bearer',
|
|
158
|
+
label: 'Bearer token call',
|
|
159
|
+
posture: 'bearer',
|
|
160
|
+
buildHeaders(ctx) {
|
|
161
|
+
return {
|
|
162
|
+
host: ctx.host,
|
|
163
|
+
'content-type': 'application/json',
|
|
164
|
+
'user-agent': 'mcp-shark-self-test/1.0',
|
|
165
|
+
authorization: 'Bearer demo-token-must-be-rotated-9c4f2a',
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
buildResponseHeaders() {
|
|
169
|
+
return {
|
|
170
|
+
'content-type': 'application/json',
|
|
171
|
+
'www-authenticate': 'Bearer realm="example"',
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
statusCode: 200,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: 'bearer-coexist',
|
|
178
|
+
label: 'Bearer + AAuth (drift smell)',
|
|
179
|
+
posture: 'bearer',
|
|
180
|
+
buildHeaders(ctx) {
|
|
181
|
+
return {
|
|
182
|
+
host: ctx.host,
|
|
183
|
+
'content-type': 'application/json',
|
|
184
|
+
'user-agent': 'mcp-shark-self-test/1.0',
|
|
185
|
+
authorization: 'Bearer legacy-token-rotation-required',
|
|
186
|
+
'signature-input': buildSignatureInput(ctx.agent),
|
|
187
|
+
signature: PLACEHOLDER_SIG,
|
|
188
|
+
'aauth-agent': ctx.agent.id,
|
|
189
|
+
};
|
|
190
|
+
},
|
|
191
|
+
buildResponseHeaders() {
|
|
192
|
+
return { 'content-type': 'application/json' };
|
|
193
|
+
},
|
|
194
|
+
statusCode: 200,
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Run a single self-test pass. Synthesizes scenario-driven JSON-RPC traffic
|
|
200
|
+
* for each detected upstream (or a default placeholder triple if none are
|
|
201
|
+
* configured) and writes both request and response packets via the audit
|
|
202
|
+
* service so they appear as normal captured traffic.
|
|
203
|
+
*
|
|
204
|
+
* @param {object} deps
|
|
205
|
+
* @param {object} deps.auditService - mcp-shark AuditService
|
|
206
|
+
* @param {object} deps.logger - pino-style logger
|
|
207
|
+
* @param {object} [opts]
|
|
208
|
+
* @param {number} [opts.rounds=2] - how many rounds (each round runs every scenario once per upstream)
|
|
209
|
+
* @returns {{
|
|
210
|
+
* rounds: number,
|
|
211
|
+
* targets: Array<{name:string,url:string}>,
|
|
212
|
+
* inserted: number,
|
|
213
|
+
* by_scenario: Record<string, number>,
|
|
214
|
+
* by_posture: Record<string, number>,
|
|
215
|
+
* started_at_iso: string,
|
|
216
|
+
* finished_at_iso: string,
|
|
217
|
+
* }}
|
|
218
|
+
*/
|
|
219
|
+
export function runAauthSelfTest({ auditService, logger }, opts = {}) {
|
|
220
|
+
const rounds = Math.max(1, Math.min(10, Number(opts.rounds) || 2));
|
|
221
|
+
const startedAt = new Date();
|
|
222
|
+
|
|
223
|
+
let upstreams = detectHttpUpstreams();
|
|
224
|
+
if (upstreams.length === 0) {
|
|
225
|
+
upstreams = [
|
|
226
|
+
{ name: 'aauth-signed', url: 'http://127.0.0.1:9701/mcp' },
|
|
227
|
+
{ name: 'aauth-challenge', url: 'http://127.0.0.1:9702/mcp' },
|
|
228
|
+
{ name: 'aauth-bearer', url: 'http://127.0.0.1:9703/mcp' },
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let inserted = 0;
|
|
233
|
+
const byScenario = {};
|
|
234
|
+
const byPosture = {};
|
|
235
|
+
let nextJsonRpcId = Date.now();
|
|
236
|
+
|
|
237
|
+
for (let round = 0; round < rounds; round++) {
|
|
238
|
+
for (let u = 0; u < upstreams.length; u++) {
|
|
239
|
+
const upstream = upstreams[u];
|
|
240
|
+
const url = upstream.url;
|
|
241
|
+
let host;
|
|
242
|
+
try {
|
|
243
|
+
host = new URL(url).host;
|
|
244
|
+
} catch {
|
|
245
|
+
host = `${upstream.name}.local`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const upstreamScenarios = chooseScenariosForUpstream(upstream.name);
|
|
249
|
+
|
|
250
|
+
for (const scenario of upstreamScenarios) {
|
|
251
|
+
const agent = pick(KNOWN_AGENTS, round + u);
|
|
252
|
+
const mission = pick(KNOWN_MISSIONS, round + u + scenario.id.length);
|
|
253
|
+
const access = pick(ACCESS_MODES, round + u);
|
|
254
|
+
const tool = pick(KNOWN_TOOLS, round + u);
|
|
255
|
+
const ctx = { agent, mission, access, host, upstream };
|
|
256
|
+
|
|
257
|
+
const jsonrpcId = `self-test-${nextJsonRpcId++}`;
|
|
258
|
+
const reqBody = buildJsonRpcRequest(jsonrpcId, tool, mission);
|
|
259
|
+
const reqHeaders = scenario.buildHeaders(ctx);
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
auditService.logRequestPacket({
|
|
263
|
+
method: 'POST',
|
|
264
|
+
url,
|
|
265
|
+
headers: reqHeaders,
|
|
266
|
+
body: reqBody,
|
|
267
|
+
userAgent: reqHeaders['user-agent'] || 'mcp-shark-self-test/1.0',
|
|
268
|
+
remoteAddress: '127.0.0.1',
|
|
269
|
+
});
|
|
270
|
+
} catch (err) {
|
|
271
|
+
logger?.warn?.(
|
|
272
|
+
{ error: err.message, scenario: scenario.id },
|
|
273
|
+
'self-test request log failed'
|
|
274
|
+
);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const respBody =
|
|
279
|
+
scenario.statusCode === 401
|
|
280
|
+
? {
|
|
281
|
+
jsonrpc: '2.0',
|
|
282
|
+
id: jsonrpcId,
|
|
283
|
+
error: { code: -32001, message: 'AAuth signature required' },
|
|
284
|
+
}
|
|
285
|
+
: buildJsonRpcResponse(jsonrpcId, tool);
|
|
286
|
+
const respHeaders = scenario.buildResponseHeaders(ctx);
|
|
287
|
+
respHeaders.host = host;
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
auditService.logResponsePacket({
|
|
291
|
+
statusCode: scenario.statusCode,
|
|
292
|
+
headers: respHeaders,
|
|
293
|
+
body: respBody,
|
|
294
|
+
jsonrpcId,
|
|
295
|
+
userAgent: reqHeaders['user-agent'] || 'mcp-shark-self-test/1.0',
|
|
296
|
+
remoteAddress: '127.0.0.1',
|
|
297
|
+
});
|
|
298
|
+
} catch (err) {
|
|
299
|
+
logger?.warn?.(
|
|
300
|
+
{ error: err.message, scenario: scenario.id },
|
|
301
|
+
'self-test response log failed'
|
|
302
|
+
);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
inserted += 2;
|
|
307
|
+
byScenario[scenario.id] = (byScenario[scenario.id] || 0) + 1;
|
|
308
|
+
byPosture[scenario.posture] = (byPosture[scenario.posture] || 0) + 1;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const finishedAt = new Date();
|
|
314
|
+
return {
|
|
315
|
+
rounds,
|
|
316
|
+
targets: upstreams,
|
|
317
|
+
inserted,
|
|
318
|
+
by_scenario: byScenario,
|
|
319
|
+
by_posture: byPosture,
|
|
320
|
+
started_at_iso: startedAt.toISOString(),
|
|
321
|
+
finished_at_iso: finishedAt.toISOString(),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Pick the scenarios that match an upstream's apparent role. Server names
|
|
327
|
+
* containing well-known substrings get tailored scenarios so the resulting
|
|
328
|
+
* graph is intuitive (signed-server → signed scenario). Anything else gets
|
|
329
|
+
* the full mix.
|
|
330
|
+
*/
|
|
331
|
+
function chooseScenariosForUpstream(name) {
|
|
332
|
+
const lc = (name || '').toLowerCase();
|
|
333
|
+
if (lc.includes('signed')) {
|
|
334
|
+
return SCENARIOS.filter((s) => s.id === 'signed');
|
|
335
|
+
}
|
|
336
|
+
if (lc.includes('challenge')) {
|
|
337
|
+
return SCENARIOS.filter((s) => s.id === 'challenge');
|
|
338
|
+
}
|
|
339
|
+
if (lc.includes('bearer') && lc.includes('coexist')) {
|
|
340
|
+
return SCENARIOS.filter((s) => s.id === 'bearer-coexist');
|
|
341
|
+
}
|
|
342
|
+
if (lc.includes('bearer')) {
|
|
343
|
+
return SCENARIOS.filter((s) => s.id === 'bearer');
|
|
344
|
+
}
|
|
345
|
+
return SCENARIOS;
|
|
346
|
+
}
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duplicate Tool Names Detection (Config-Level)
|
|
3
|
+
* Detects tools with identical names across different servers,
|
|
4
|
+
* which enables cross-server shadowing attacks.
|
|
5
|
+
* Catalog reference: §1.4 (tool name collision)
|
|
6
|
+
*/
|
|
7
|
+
import { createRuleAdapter } from '../utils/adapter.js';
|
|
8
|
+
|
|
9
|
+
const RULE_ID = 'duplicate-tool-names';
|
|
10
|
+
const OWASP_ID = 'MCP09';
|
|
11
|
+
const RECOMMENDATION =
|
|
12
|
+
'Rename tools to be unique across all servers, or isolate conflicting servers into separate agent sessions.';
|
|
13
|
+
|
|
14
|
+
export function scanDuplicateToolNames(_mcpData = {}) {
|
|
15
|
+
return {
|
|
16
|
+
toolFindings: [],
|
|
17
|
+
resourceFindings: [],
|
|
18
|
+
promptFindings: [],
|
|
19
|
+
notablePatterns: [],
|
|
20
|
+
recommendations: [RECOMMENDATION],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Analyze all servers for duplicate tool names
|
|
26
|
+
* Called directly by ScanService with the full server list
|
|
27
|
+
* @param {Array} servers - Flat array of server objects
|
|
28
|
+
* @returns {Array} Findings for duplicate tool names
|
|
29
|
+
*/
|
|
30
|
+
export function analyzeAllServerToolNames(servers) {
|
|
31
|
+
const toolMap = new Map();
|
|
32
|
+
|
|
33
|
+
for (const server of servers) {
|
|
34
|
+
const tools = Array.isArray(server.tools) ? server.tools : [];
|
|
35
|
+
for (const tool of tools) {
|
|
36
|
+
const name = typeof tool === 'string' ? tool : tool?.name;
|
|
37
|
+
if (!name) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!toolMap.has(name)) {
|
|
41
|
+
toolMap.set(name, []);
|
|
42
|
+
}
|
|
43
|
+
toolMap.get(name).push(server.name);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const findings = [];
|
|
48
|
+
for (const [toolName, serverNames] of toolMap) {
|
|
49
|
+
if (serverNames.length <= 1) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const uniqueServers = [...new Set(serverNames)];
|
|
53
|
+
if (uniqueServers.length <= 1) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
findings.push({
|
|
58
|
+
rule_id: RULE_ID,
|
|
59
|
+
severity: 'high',
|
|
60
|
+
owasp_id: OWASP_ID,
|
|
61
|
+
title: `Duplicate tool "${toolName}" across ${uniqueServers.length} servers`,
|
|
62
|
+
description: `Tool "${toolName}" exists in ${uniqueServers.join(', ')}. An attacker could shadow one server's tool with another.`,
|
|
63
|
+
recommendation: RECOMMENDATION,
|
|
64
|
+
confidence: 'definite',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return findings;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const adapter = createRuleAdapter(scanDuplicateToolNames, RULE_ID, OWASP_ID, RECOMMENDATION);
|
|
72
|
+
export const analyzeTool = adapter.analyzeTool;
|
|
73
|
+
export const analyzePrompt = adapter.analyzePrompt;
|
|
74
|
+
export const analyzeResource = adapter.analyzeResource;
|
|
75
|
+
export const analyzePacket = adapter.analyzePacket;
|
|
76
|
+
|
|
77
|
+
export const ruleMetadata = {
|
|
78
|
+
id: RULE_ID,
|
|
79
|
+
name: 'Duplicate Tool Names',
|
|
80
|
+
owasp_id: OWASP_ID,
|
|
81
|
+
severity: 'high',
|
|
82
|
+
description: 'Detects identical tool names across servers enabling shadowing attacks.',
|
|
83
|
+
source: 'static',
|
|
84
|
+
type: 'general-security',
|
|
85
|
+
};
|