@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,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal output formatter for scan results
|
|
3
|
+
* Renders findings, toxic flows, and summaries with colors
|
|
4
|
+
*/
|
|
5
|
+
import kleur from 'kleur';
|
|
6
|
+
import { S } from '../symbols.js';
|
|
7
|
+
|
|
8
|
+
const SEVERITY_COLORS = {
|
|
9
|
+
critical: (text) => kleur.bgRed().white().bold(` ${text} `),
|
|
10
|
+
high: (text) => kleur.red().bold(text),
|
|
11
|
+
medium: (text) => kleur.yellow(text),
|
|
12
|
+
low: (text) => kleur.blue(text),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const SEVERITY_LABELS = {
|
|
16
|
+
critical: 'CRIT',
|
|
17
|
+
high: 'HIGH',
|
|
18
|
+
medium: 'MED ',
|
|
19
|
+
low: 'LOW ',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format a single finding for terminal output
|
|
24
|
+
*/
|
|
25
|
+
export function formatFinding(finding) {
|
|
26
|
+
const severity = (finding.severity || finding.risk_level || 'medium').toLowerCase();
|
|
27
|
+
const label = SEVERITY_LABELS[severity] || 'INFO';
|
|
28
|
+
const colorFn = SEVERITY_COLORS[severity] || kleur.gray;
|
|
29
|
+
const confidence = finding.confidence === 'possible' ? 'advisory' : 'confirmed';
|
|
30
|
+
const confidenceColor = confidence === 'confirmed' ? kleur.white : kleur.gray;
|
|
31
|
+
|
|
32
|
+
const ruleId = finding.rule_id || finding.category || '';
|
|
33
|
+
const ruleDisplay = ruleId.toUpperCase().replace(/-/g, '').slice(0, 5);
|
|
34
|
+
|
|
35
|
+
const message = finding.title || finding.description || finding.message || '';
|
|
36
|
+
|
|
37
|
+
return ` ${colorFn(label)} ${kleur.dim(ruleDisplay)} ${message} ${confidenceColor(confidence)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format findings grouped by server
|
|
42
|
+
*/
|
|
43
|
+
export function formatServerFindings(serverName, ideName, findings) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
const header = kleur.bold(` ${serverName}`) + kleur.dim(` (${ideName})`);
|
|
46
|
+
lines.push(header);
|
|
47
|
+
|
|
48
|
+
for (const finding of findings) {
|
|
49
|
+
lines.push(formatFinding(finding));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
lines.push('');
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Format clean servers summary
|
|
58
|
+
*/
|
|
59
|
+
export function formatCleanServers(cleanServerNames) {
|
|
60
|
+
if (cleanServerNames.length === 0) {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
const names = cleanServerNames.join(', ');
|
|
64
|
+
return ` ${names} ${kleur.green(`${S.bar} clean ${S.pass}`)}\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format toxic flow section
|
|
69
|
+
*/
|
|
70
|
+
export function formatToxicFlows(flows) {
|
|
71
|
+
if (flows.length === 0) {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const lines = [];
|
|
76
|
+
lines.push(` ${kleur.dim(S.bar.repeat(65))}`);
|
|
77
|
+
lines.push(` ${kleur.bold('Toxic Flows')}`);
|
|
78
|
+
lines.push(` ${kleur.dim(S.bar.repeat(65))}`);
|
|
79
|
+
lines.push('');
|
|
80
|
+
|
|
81
|
+
for (const flow of flows) {
|
|
82
|
+
const riskColor = flow.risk === 'HIGH' ? kleur.red : kleur.yellow;
|
|
83
|
+
lines.push(
|
|
84
|
+
` ${riskColor(`${S.warn} ${flow.risk}`)} ${kleur.bold(flow.source)} ${kleur.dim(S.arrow)} ${kleur.bold(flow.target)}`
|
|
85
|
+
);
|
|
86
|
+
lines.push(` ${flow.scenario}`);
|
|
87
|
+
lines.push(` ${kleur.dim(`(Catalog ${flow.catalog})`)}`);
|
|
88
|
+
lines.push('');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
lines.push(` ${kleur.dim(S.bar.repeat(65))}`);
|
|
92
|
+
return lines.join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Format the Shark Score display
|
|
97
|
+
*/
|
|
98
|
+
export function formatSharkScore(scoreResult) {
|
|
99
|
+
const { score, grade } = scoreResult;
|
|
100
|
+
|
|
101
|
+
const gradeColors = {
|
|
102
|
+
A: kleur.green,
|
|
103
|
+
B: kleur.green,
|
|
104
|
+
C: kleur.yellow,
|
|
105
|
+
D: kleur.red,
|
|
106
|
+
F: kleur.bgRed().white,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const colorFn = gradeColors[grade] || kleur.white;
|
|
110
|
+
return ` Shark Score: ${colorFn(`${score}/100 (${grade})`)}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Format the summary counts line
|
|
115
|
+
*/
|
|
116
|
+
export function formatSummaryCounts(counts, flowCount) {
|
|
117
|
+
const parts = [];
|
|
118
|
+
if (counts.critical > 0) {
|
|
119
|
+
parts.push(kleur.red(`${counts.critical} critical`));
|
|
120
|
+
}
|
|
121
|
+
if (counts.high > 0) {
|
|
122
|
+
parts.push(kleur.red(`${counts.high} high`));
|
|
123
|
+
}
|
|
124
|
+
if (counts.medium > 0) {
|
|
125
|
+
parts.push(kleur.yellow(`${counts.medium} medium`));
|
|
126
|
+
}
|
|
127
|
+
if (counts.low > 0) {
|
|
128
|
+
parts.push(kleur.blue(`${counts.low} low`));
|
|
129
|
+
}
|
|
130
|
+
if (flowCount > 0) {
|
|
131
|
+
parts.push(kleur.red(`${flowCount} toxic flows`));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (parts.length === 0) {
|
|
135
|
+
return ` ${kleur.green(`${S.pass} No issues found`)}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return ` ${parts.join(kleur.dim(` ${S.dot} `))}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Format completion timing
|
|
143
|
+
*/
|
|
144
|
+
export function formatTiming(elapsedMs, serverCount, ruleCount, toolCount) {
|
|
145
|
+
const seconds = (elapsedMs / 1000).toFixed(1);
|
|
146
|
+
return kleur.dim(
|
|
147
|
+
` Completed in ${seconds}s ${S.dot} ${serverCount} servers ${S.dot} ${ruleCount} rules ${S.dot} ${toolCount} tools checked`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Format next steps suggestions
|
|
153
|
+
*/
|
|
154
|
+
export function formatNextSteps(hasFixable, hasFlows) {
|
|
155
|
+
const lines = [];
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push(kleur.dim(' Next steps:'));
|
|
158
|
+
|
|
159
|
+
if (hasFixable) {
|
|
160
|
+
lines.push(` ${kleur.cyan('npx mcp-shark scan --fix')} Auto-fix fixable issues`);
|
|
161
|
+
}
|
|
162
|
+
if (hasFlows) {
|
|
163
|
+
lines.push(` ${kleur.cyan('npx mcp-shark scan --walkthrough')} See full attack chains`);
|
|
164
|
+
}
|
|
165
|
+
lines.push(` ${kleur.cyan('npx mcp-shark lock')} Pin tool definitions`);
|
|
166
|
+
|
|
167
|
+
return lines.join('\n');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Format IDE discovery line
|
|
172
|
+
*/
|
|
173
|
+
export function formatIdeDiscovery(ideResults) {
|
|
174
|
+
const found = ideResults.filter((ide) => ide.found);
|
|
175
|
+
const names = found.map((ide) => ide.name);
|
|
176
|
+
const serverCount = found.reduce((sum, ide) => sum + ide.serverCount, 0);
|
|
177
|
+
|
|
178
|
+
if (names.length === 0) {
|
|
179
|
+
return ` ${kleur.yellow(`${S.warn} No MCP configurations found`)}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return kleur.dim(` Scanning ${serverCount} servers across ${names.join(', ')}...`);
|
|
183
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON and SARIF output formatters for scan results
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format scan results as JSON
|
|
7
|
+
*/
|
|
8
|
+
export function formatAsJson(scanResult) {
|
|
9
|
+
return JSON.stringify(scanResult, null, 2);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Format scan results as SARIF v2.1.0
|
|
14
|
+
* Static Analysis Results Interchange Format for CI/CD integration
|
|
15
|
+
*/
|
|
16
|
+
export function formatAsSarif(scanResult) {
|
|
17
|
+
const sarifRules = buildSarifRules(scanResult.findings);
|
|
18
|
+
const sarifResults = buildSarifResults(scanResult.findings);
|
|
19
|
+
|
|
20
|
+
const sarif = {
|
|
21
|
+
$schema:
|
|
22
|
+
'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json',
|
|
23
|
+
version: '2.1.0',
|
|
24
|
+
runs: [
|
|
25
|
+
{
|
|
26
|
+
tool: {
|
|
27
|
+
driver: {
|
|
28
|
+
name: 'mcp-shark',
|
|
29
|
+
version: scanResult.version || '1.0.0',
|
|
30
|
+
informationUri: 'https://mcpshark.sh',
|
|
31
|
+
rules: sarifRules,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
results: sarifResults,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return JSON.stringify(sarif, null, 2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build SARIF rule descriptors from findings
|
|
44
|
+
*/
|
|
45
|
+
function buildSarifRules(findings) {
|
|
46
|
+
const ruleMap = new Map();
|
|
47
|
+
|
|
48
|
+
for (const finding of findings) {
|
|
49
|
+
const ruleId = finding.rule_id || finding.category || 'unknown';
|
|
50
|
+
if (ruleMap.has(ruleId)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ruleMap.set(ruleId, {
|
|
55
|
+
id: ruleId,
|
|
56
|
+
name: finding.title || ruleId,
|
|
57
|
+
shortDescription: {
|
|
58
|
+
text: finding.title || finding.description || ruleId,
|
|
59
|
+
},
|
|
60
|
+
defaultConfiguration: {
|
|
61
|
+
level: mapSeverityToSarif(finding.severity || finding.risk_level),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return [...ruleMap.values()];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build SARIF result entries from findings
|
|
71
|
+
*/
|
|
72
|
+
function buildSarifResults(findings) {
|
|
73
|
+
return findings.map((finding) => ({
|
|
74
|
+
ruleId: finding.rule_id || finding.category || 'unknown',
|
|
75
|
+
level: mapSeverityToSarif(finding.severity || finding.risk_level),
|
|
76
|
+
message: {
|
|
77
|
+
text: finding.description || finding.title || finding.message || '',
|
|
78
|
+
},
|
|
79
|
+
locations: [
|
|
80
|
+
{
|
|
81
|
+
physicalLocation: {
|
|
82
|
+
artifactLocation: {
|
|
83
|
+
uri: finding.config_path || finding.server_name || 'unknown',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
properties: {
|
|
89
|
+
confidence: finding.confidence || 'probable',
|
|
90
|
+
serverName: finding.server_name || null,
|
|
91
|
+
},
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Map internal severity to SARIF level
|
|
97
|
+
*/
|
|
98
|
+
function mapSeverityToSarif(severity) {
|
|
99
|
+
const map = {
|
|
100
|
+
critical: 'error',
|
|
101
|
+
high: 'error',
|
|
102
|
+
medium: 'warning',
|
|
103
|
+
low: 'note',
|
|
104
|
+
};
|
|
105
|
+
return map[(severity || '').toLowerCase()] || 'warning';
|
|
106
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI output module barrel file
|
|
3
|
+
*/
|
|
4
|
+
export { displayScanBanner, displayServeBanner } from './Banner.js';
|
|
5
|
+
export {
|
|
6
|
+
formatFinding,
|
|
7
|
+
formatServerFindings,
|
|
8
|
+
formatCleanServers,
|
|
9
|
+
formatToxicFlows,
|
|
10
|
+
formatSharkScore,
|
|
11
|
+
formatSummaryCounts,
|
|
12
|
+
formatTiming,
|
|
13
|
+
formatNextSteps,
|
|
14
|
+
formatIdeDiscovery,
|
|
15
|
+
} from './Formatter.js';
|
|
16
|
+
export { formatAsJson, formatAsSarif } from './JsonFormatter.js';
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hardened HTTP fetch for rule registry and pack downloads.
|
|
3
|
+
* - HTTPS only by default (HTTP only with MCP_SHARK_INSECURE_HTTP_REGISTRY=1)
|
|
4
|
+
* - No userinfo in URLs (prevents credential injection)
|
|
5
|
+
* - Manual redirect handling with re-validation each hop (mitigates redirect-to-internal SSRF)
|
|
6
|
+
* - Response size cap (mitigates memory exhaustion)
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
const MAX_REDIRECTS = 5;
|
|
11
|
+
const DEFAULT_MAX_BYTES = 25 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} urlString
|
|
15
|
+
* @returns {string} normalized href
|
|
16
|
+
*/
|
|
17
|
+
export function assertAllowedRegistryUrl(urlString) {
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = new URL(urlString);
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error('Invalid URL');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const allowHttp = process.env.MCP_SHARK_INSECURE_HTTP_REGISTRY === '1';
|
|
26
|
+
|
|
27
|
+
if (parsed.protocol === 'https:') {
|
|
28
|
+
// ok
|
|
29
|
+
} else if (parsed.protocol === 'http:' && allowHttp) {
|
|
30
|
+
// lab / air-gapped mirrors only — explicit env required
|
|
31
|
+
} else {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'Registry URLs must use HTTPS. For trusted internal HTTP mirrors only, set MCP_SHARK_INSECURE_HTTP_REGISTRY=1.'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!parsed.hostname) {
|
|
38
|
+
throw new Error('Registry URL must have a hostname');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (parsed.username !== '' || parsed.password !== '') {
|
|
42
|
+
throw new Error('Registry URL must not embed credentials');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return parsed.href;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fetch JSON with size limit and safe redirects.
|
|
50
|
+
* @param {string} initialUrl
|
|
51
|
+
* @param {number} [maxBytes]
|
|
52
|
+
* @returns {Promise<object>}
|
|
53
|
+
*/
|
|
54
|
+
export async function fetchUtf8Secure(initialUrl, maxBytes = DEFAULT_MAX_BYTES) {
|
|
55
|
+
let url = assertAllowedRegistryUrl(initialUrl);
|
|
56
|
+
|
|
57
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
|
58
|
+
const response = await fetch(url, {
|
|
59
|
+
redirect: 'manual',
|
|
60
|
+
headers: {
|
|
61
|
+
Accept: 'application/json, text/plain;q=0.9, */*;q=0.8',
|
|
62
|
+
'User-Agent': 'mcp-shark-rule-update',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (response.status >= 300 && response.status < 400) {
|
|
67
|
+
const location = response.headers.get('location');
|
|
68
|
+
if (!location || hop === MAX_REDIRECTS) {
|
|
69
|
+
throw new Error('Too many HTTP redirects or missing Location header');
|
|
70
|
+
}
|
|
71
|
+
url = assertAllowedRegistryUrl(new URL(location, url).href);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return readBodyWithLimit(response, maxBytes);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
throw new Error('Redirect loop');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} initialUrl
|
|
87
|
+
* @param {number} [maxBytes]
|
|
88
|
+
* @returns {Promise<object>}
|
|
89
|
+
*/
|
|
90
|
+
export async function fetchJsonSecure(initialUrl, maxBytes = DEFAULT_MAX_BYTES) {
|
|
91
|
+
const text = await fetchUtf8Secure(initialUrl, maxBytes);
|
|
92
|
+
return JSON.parse(text);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {Response} response
|
|
97
|
+
* @param {number} maxBytes
|
|
98
|
+
* @returns {Promise<string>}
|
|
99
|
+
*/
|
|
100
|
+
async function readBodyWithLimit(response, maxBytes) {
|
|
101
|
+
if (!response.body) {
|
|
102
|
+
const text = await response.text();
|
|
103
|
+
if (Buffer.byteLength(text, 'utf8') > maxBytes) {
|
|
104
|
+
throw new Error('Response body exceeds size limit');
|
|
105
|
+
}
|
|
106
|
+
return text;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const reader = response.body.getReader();
|
|
110
|
+
const chunks = [];
|
|
111
|
+
let total = 0;
|
|
112
|
+
|
|
113
|
+
while (true) {
|
|
114
|
+
const { done, value } = await reader.read();
|
|
115
|
+
if (done) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
total += value.length;
|
|
119
|
+
if (total > maxBytes) {
|
|
120
|
+
throw new Error('Response body exceeds size limit');
|
|
121
|
+
}
|
|
122
|
+
chunks.push(value);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const SAFE_PACK_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {string} id
|
|
132
|
+
*/
|
|
133
|
+
export function assertSafePackId(id) {
|
|
134
|
+
if (typeof id !== 'string' || !SAFE_PACK_ID.test(id)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
'Pack id must be 1–128 chars: letters, digits, dot, underscore, hyphen; no path segments.'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} expectedHex
|
|
143
|
+
* @param {string} utf8Body
|
|
144
|
+
*/
|
|
145
|
+
export function assertSha256(expectedHex, utf8Body) {
|
|
146
|
+
if (!expectedHex || typeof expectedHex !== 'string') {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const normalized = expectedHex.trim().toLowerCase();
|
|
150
|
+
if (!/^[a-f0-9]{64}$/.test(normalized)) {
|
|
151
|
+
throw new Error('Invalid sha256 format in manifest');
|
|
152
|
+
}
|
|
153
|
+
const actual = createHash('sha256').update(utf8Body, 'utf8').digest('hex');
|
|
154
|
+
if (actual !== normalized) {
|
|
155
|
+
throw new Error('Downloaded pack failed SHA-256 verification');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -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
|
+
};
|
|
@@ -12,10 +12,12 @@ export const Environment = {
|
|
|
12
12
|
},
|
|
13
13
|
/**
|
|
14
14
|
* Get UI server port
|
|
15
|
+
* Honours UI_PORT first, then the documented MCP_SHARK_PORT alias.
|
|
15
16
|
* @returns {number} UI server port (default: 9853)
|
|
16
17
|
*/
|
|
17
18
|
getUiPort() {
|
|
18
|
-
const
|
|
19
|
+
const raw = process.env.UI_PORT ?? process.env.MCP_SHARK_PORT;
|
|
20
|
+
const port = Number.parseInt(raw, 10);
|
|
19
21
|
return Number.isNaN(port) ? Server.DEFAULT_UI_PORT : port;
|
|
20
22
|
},
|
|
21
23
|
|
package/core/configs/index.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
2
|
-
import { homedir } from 'node:os';
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
2
|
import { dirname, join } from 'node:path';
|
|
3
|
+
import { Environment } from './environment.js';
|
|
4
4
|
|
|
5
5
|
export { Environment } from './environment.js';
|
|
6
6
|
|
|
7
|
-
const WORKING_DIRECTORY_NAME = '.mcp-shark';
|
|
8
7
|
const MCP_CONFIG_NAME = 'mcps.json';
|
|
9
8
|
const APP_DB_DIR_NAME = 'db';
|
|
10
9
|
const APP_DB_FILE_NAME = 'mcp-shark.sqlite';
|
|
11
|
-
const HELP_STATE_NAME = 'help-state.json';
|
|
12
10
|
|
|
13
11
|
export function getWorkingDirectory() {
|
|
14
|
-
return
|
|
12
|
+
return Environment.getMcpSharkHome();
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
export function getDatabasePath() {
|
|
@@ -61,62 +59,3 @@ export function ensureDirectoryExists(filePath) {
|
|
|
61
59
|
mkdirSync(dir, { recursive: true });
|
|
62
60
|
}
|
|
63
61
|
}
|
|
64
|
-
|
|
65
|
-
export function getHelpStatePath() {
|
|
66
|
-
return join(getWorkingDirectory(), HELP_STATE_NAME);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function readHelpState() {
|
|
70
|
-
try {
|
|
71
|
-
const helpStatePath = getHelpStatePath();
|
|
72
|
-
if (existsSync(helpStatePath)) {
|
|
73
|
-
const content = readFileSync(helpStatePath, 'utf8');
|
|
74
|
-
const state = JSON.parse(content);
|
|
75
|
-
// Ensure we have the expected structure
|
|
76
|
-
return {
|
|
77
|
-
dismissed: state.dismissed || false,
|
|
78
|
-
tourCompleted: state.tourCompleted || false,
|
|
79
|
-
dismissedAt: state.dismissedAt || null,
|
|
80
|
-
version: state.version || '1.0.0',
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
dismissed: false,
|
|
85
|
-
tourCompleted: false,
|
|
86
|
-
dismissedAt: null,
|
|
87
|
-
version: '1.0.0',
|
|
88
|
-
};
|
|
89
|
-
} catch (_error) {
|
|
90
|
-
// Error reading help state - return defaults
|
|
91
|
-
return {
|
|
92
|
-
dismissed: false,
|
|
93
|
-
tourCompleted: false,
|
|
94
|
-
dismissedAt: null,
|
|
95
|
-
version: '1.0.0',
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function writeHelpState(state) {
|
|
101
|
-
try {
|
|
102
|
-
const helpStatePath = getHelpStatePath();
|
|
103
|
-
prepareAppDataSpaces(); // Ensure directory exists
|
|
104
|
-
|
|
105
|
-
// Merge with existing state to preserve other fields
|
|
106
|
-
const existingState = readHelpState();
|
|
107
|
-
const newState = {
|
|
108
|
-
...existingState,
|
|
109
|
-
...state,
|
|
110
|
-
dismissedAt:
|
|
111
|
-
state.dismissed || state.tourCompleted
|
|
112
|
-
? new Date().toISOString()
|
|
113
|
-
: existingState.dismissedAt,
|
|
114
|
-
version: '1.0.0',
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
writeFileSync(helpStatePath, JSON.stringify(newState, null, 2));
|
|
118
|
-
return true;
|
|
119
|
-
} catch (_error) {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
@@ -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,
|
package/core/mcp-server/index.js
CHANGED
|
@@ -142,7 +142,10 @@ export async function startMcpSharkServer(options = {}) {
|
|
|
142
142
|
throw new Error('auditLogger is required. Call initAuditLogger() and pass it in options.');
|
|
143
143
|
}
|
|
144
144
|
const auditLogger = providedAuditLogger;
|
|
145
|
-
const externalServersResult = await runAllExternalServers(serverLogger, configPath
|
|
145
|
+
const externalServersResult = await runAllExternalServers(serverLogger, configPath, {
|
|
146
|
+
selfPort: port,
|
|
147
|
+
allowEmpty: true,
|
|
148
|
+
});
|
|
146
149
|
|
|
147
150
|
if (isError(externalServersResult)) {
|
|
148
151
|
serverLogger.error(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CompositeError, getErrors } from '#core/libraries/ErrorLibrary.js';
|
|
1
|
+
import { CompositeError, getErrors, isError } from '#core/libraries/ErrorLibrary.js';
|
|
2
2
|
import { normalizeConfig } from './config.js';
|
|
3
3
|
import { buildKv } from './kv.js';
|
|
4
4
|
import { runExternalServer } from './single/run.js';
|
|
@@ -10,8 +10,15 @@ export class RunAllExternalServersError extends CompositeError {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export async function runAllExternalServers(logger, parsedConfig) {
|
|
14
|
-
const configs = normalizeConfig(parsedConfig);
|
|
13
|
+
export async function runAllExternalServers(logger, parsedConfig, options = {}) {
|
|
14
|
+
const configs = normalizeConfig(parsedConfig, { ...options, logger });
|
|
15
|
+
if (isError(configs)) {
|
|
16
|
+
return new RunAllExternalServersError(
|
|
17
|
+
`Failed to normalize upstream config: ${configs.message}`,
|
|
18
|
+
configs,
|
|
19
|
+
[configs]
|
|
20
|
+
);
|
|
21
|
+
}
|
|
15
22
|
const results = await Promise.all(
|
|
16
23
|
Object.entries(configs).map(([name, config]) => runExternalServer({ logger, name, config }))
|
|
17
24
|
);
|