@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.
Files changed (158) hide show
  1. package/README.md +482 -56
  2. package/bin/mcp-shark.js +146 -52
  3. package/core/cli/AutoFixEngine.js +93 -0
  4. package/core/cli/ConfigScanner.js +193 -0
  5. package/core/cli/DataLoader.js +200 -0
  6. package/core/cli/DeclarativeRuleEngine.js +363 -0
  7. package/core/cli/DoctorCommand.js +218 -0
  8. package/core/cli/FixHandlers.js +222 -0
  9. package/core/cli/HtmlReportGenerator.js +203 -0
  10. package/core/cli/IdeConfigPaths.js +175 -0
  11. package/core/cli/ListCommand.js +255 -0
  12. package/core/cli/LockCommand.js +164 -0
  13. package/core/cli/LockDiffEngine.js +152 -0
  14. package/core/cli/RuleRegistryConfig.js +131 -0
  15. package/core/cli/ScanCommand.js +244 -0
  16. package/core/cli/ScanService.js +200 -0
  17. package/core/cli/SecretDetector.js +92 -0
  18. package/core/cli/SharkScoreCalculator.js +109 -0
  19. package/core/cli/ToolClassifications.js +51 -0
  20. package/core/cli/ToxicFlowAnalyzer.js +212 -0
  21. package/core/cli/UpdateCommand.js +188 -0
  22. package/core/cli/WalkthroughGenerator.js +195 -0
  23. package/core/cli/WatchCommand.js +129 -0
  24. package/core/cli/YamlRuleEngine.js +197 -0
  25. package/core/cli/data/rule-packs/aauth-visibility.json +117 -0
  26. package/core/cli/data/rule-packs/agentic-security-2026.json +180 -0
  27. package/core/cli/data/rule-packs/general-security.json +173 -0
  28. package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
  29. package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
  30. package/core/cli/data/rule-sources.json +5 -0
  31. package/core/cli/data/secret-patterns.json +18 -0
  32. package/core/cli/data/tool-classifications.json +111 -0
  33. package/core/cli/data/toxic-flow-rules.json +47 -0
  34. package/core/cli/index.js +23 -0
  35. package/core/cli/output/Banner.js +52 -0
  36. package/core/cli/output/Formatter.js +183 -0
  37. package/core/cli/output/JsonFormatter.js +106 -0
  38. package/core/cli/output/index.js +16 -0
  39. package/core/cli/secureRegistryFetch.js +157 -0
  40. package/core/cli/symbols.js +16 -0
  41. package/core/configs/environment.js +3 -1
  42. package/core/configs/index.js +3 -64
  43. package/core/container/DependencyContainer.js +4 -1
  44. package/core/mcp-server/index.js +4 -1
  45. package/core/mcp-server/server/external/all.js +10 -3
  46. package/core/mcp-server/server/external/config.js +62 -5
  47. package/core/models/RequestFilters.js +3 -0
  48. package/core/repositories/PacketRepository.js +16 -0
  49. package/core/services/AuditService.js +2 -0
  50. package/core/services/ConfigService.js +9 -1
  51. package/core/services/ConfigTransformService.js +34 -2
  52. package/core/services/RequestService.js +58 -5
  53. package/core/services/ServerManagementService.js +59 -4
  54. package/core/services/security/StaticRulesService.js +69 -13
  55. package/core/services/security/TrafficAnalysisService.js +19 -1
  56. package/core/services/security/TrafficToxicFlowService.js +154 -0
  57. package/core/services/security/aauthGraph.js +199 -0
  58. package/core/services/security/aauthParser.js +274 -0
  59. package/core/services/security/aauthSelfTest.js +346 -0
  60. package/core/services/security/index.js +2 -1
  61. package/core/services/security/rules/index.js +25 -59
  62. package/core/services/security/rules/scans/configPermissions.js +91 -0
  63. package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
  64. package/core/services/security/rules/scans/insecureTransport.js +148 -0
  65. package/core/services/security/rules/scans/missingContainment.js +123 -0
  66. package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
  67. package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
  68. package/core/services/security/toolsListFromTrafficParser.js +70 -0
  69. package/core/tui/App.js +144 -0
  70. package/core/tui/FindingsPanel.js +115 -0
  71. package/core/tui/FixPanel.js +132 -0
  72. package/core/tui/Header.js +51 -0
  73. package/core/tui/HelpBar.js +42 -0
  74. package/core/tui/ServersPanel.js +109 -0
  75. package/core/tui/ToxicFlowsPanel.js +100 -0
  76. package/core/tui/h.js +8 -0
  77. package/core/tui/index.js +11 -0
  78. package/core/tui/render.js +22 -0
  79. package/package.json +24 -16
  80. package/ui/dist/assets/index-D6zDrtMV.js +81 -0
  81. package/ui/dist/index.html +1 -1
  82. package/ui/server/controllers/AauthController.js +279 -0
  83. package/ui/server/controllers/RequestController.js +12 -1
  84. package/ui/server/controllers/SecurityFindingsController.js +46 -1
  85. package/ui/server/routes/aauth.js +18 -0
  86. package/ui/server/routes/requests.js +8 -1
  87. package/ui/server/routes/security.js +5 -1
  88. package/ui/server/setup.js +224 -6
  89. package/ui/server/swagger/paths/components.js +55 -0
  90. package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
  91. package/ui/server/swagger/paths.js +2 -2
  92. package/ui/server/swagger/swagger.js +5 -2
  93. package/ui/server.js +1 -1
  94. package/ui/src/App.jsx +26 -52
  95. package/ui/src/PacketFilters.jsx +31 -1
  96. package/ui/src/PacketList.jsx +2 -2
  97. package/ui/src/Security.jsx +10 -0
  98. package/ui/src/TabNavigation.jsx +8 -0
  99. package/ui/src/components/AAuthBadge.jsx +92 -0
  100. package/ui/src/components/AauthExplorer/AauthExplorerGraph.jsx +231 -0
  101. package/ui/src/components/AauthExplorer/AauthExplorerView.jsx +387 -0
  102. package/ui/src/components/AauthExplorer/NodeDetailPanel.jsx +272 -0
  103. package/ui/src/components/App/ActionMenu.jsx +4 -31
  104. package/ui/src/components/App/ApiDocsButton.jsx +0 -1
  105. package/ui/src/components/App/ShutdownButton.jsx +0 -1
  106. package/ui/src/components/App/useAppState.js +19 -26
  107. package/ui/src/components/DetailsTab/AAuthIdentitySection.jsx +119 -0
  108. package/ui/src/components/DetailsTab/RequestDetailsSection.jsx +2 -0
  109. package/ui/src/components/DetailsTab/ResponseDetailsSection.jsx +2 -0
  110. package/ui/src/components/DetectedPathsList.jsx +1 -5
  111. package/ui/src/components/FileInput.jsx +0 -1
  112. package/ui/src/components/PacketFilters/AAuthPostureFilter.jsx +81 -0
  113. package/ui/src/components/RequestRow/RequestRowMain.jsx +7 -1
  114. package/ui/src/components/Security/AAuthPosturePanel.jsx +360 -0
  115. package/ui/src/components/Security/ScannerContent.jsx +33 -1
  116. package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
  117. package/ui/src/components/Security/securityApi.js +15 -0
  118. package/ui/src/components/Security/useSecurity.js +60 -3
  119. package/ui/src/components/ServerControl.jsx +0 -1
  120. package/ui/src/components/TabNavigation/DesktopTabs.jsx +0 -11
  121. package/ui/src/components/TabNavigationIcons.jsx +5 -0
  122. package/ui/src/components/ViewModeTabs.jsx +0 -1
  123. package/ui/src/utils/animations.js +26 -9
  124. package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
  125. package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
  126. package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
  127. package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
  128. package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
  129. package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
  130. package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
  131. package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
  132. package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
  133. package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
  134. package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
  135. package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
  136. package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
  137. package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
  138. package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
  139. package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
  140. package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
  141. package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
  142. package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
  143. package/ui/dist/assets/index-CiCSDYf-.js +0 -97
  144. package/ui/server/routes/help.js +0 -44
  145. package/ui/server/swagger/paths/help.js +0 -82
  146. package/ui/src/HelpGuide/HelpGuideContent.jsx +0 -118
  147. package/ui/src/HelpGuide/HelpGuideFooter.jsx +0 -59
  148. package/ui/src/HelpGuide/HelpGuideHeader.jsx +0 -57
  149. package/ui/src/HelpGuide.jsx +0 -78
  150. package/ui/src/IntroTour.jsx +0 -154
  151. package/ui/src/components/App/HelpButton.jsx +0 -90
  152. package/ui/src/components/TourOverlay.jsx +0 -117
  153. package/ui/src/components/TourTooltip/TourTooltipButtons.jsx +0 -120
  154. package/ui/src/components/TourTooltip/TourTooltipHeader.jsx +0 -71
  155. package/ui/src/components/TourTooltip/TourTooltipIcons.jsx +0 -54
  156. package/ui/src/components/TourTooltip/useTooltipPosition.js +0 -135
  157. package/ui/src/components/TourTooltip.jsx +0 -91
  158. 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 port = Number.parseInt(process.env.UI_PORT, 10);
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
 
@@ -1,17 +1,15 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
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 join(homedir(), WORKING_DIRECTORY_NAME);
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,
@@ -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
  );