@mcp-shark/mcp-shark 1.5.13 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +396 -59
  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 +224 -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/agentic-security-2026.json +180 -0
  26. package/core/cli/data/rule-packs/general-security.json +173 -0
  27. package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
  28. package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
  29. package/core/cli/data/rule-sources.json +5 -0
  30. package/core/cli/data/secret-patterns.json +18 -0
  31. package/core/cli/data/tool-classifications.json +111 -0
  32. package/core/cli/data/toxic-flow-rules.json +47 -0
  33. package/core/cli/index.js +23 -0
  34. package/core/cli/output/Banner.js +52 -0
  35. package/core/cli/output/Formatter.js +183 -0
  36. package/core/cli/output/JsonFormatter.js +106 -0
  37. package/core/cli/output/index.js +16 -0
  38. package/core/cli/secureRegistryFetch.js +157 -0
  39. package/core/cli/symbols.js +16 -0
  40. package/core/container/DependencyContainer.js +4 -1
  41. package/core/repositories/PacketRepository.js +16 -0
  42. package/core/services/AuditService.js +2 -0
  43. package/core/services/security/StaticRulesService.js +69 -13
  44. package/core/services/security/TrafficAnalysisService.js +19 -1
  45. package/core/services/security/TrafficToxicFlowService.js +154 -0
  46. package/core/services/security/index.js +2 -1
  47. package/core/services/security/rules/index.js +25 -59
  48. package/core/services/security/rules/scans/configPermissions.js +91 -0
  49. package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
  50. package/core/services/security/rules/scans/insecureTransport.js +148 -0
  51. package/core/services/security/rules/scans/missingContainment.js +123 -0
  52. package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
  53. package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
  54. package/core/services/security/toolsListFromTrafficParser.js +70 -0
  55. package/core/tui/App.js +144 -0
  56. package/core/tui/FindingsPanel.js +115 -0
  57. package/core/tui/FixPanel.js +132 -0
  58. package/core/tui/Header.js +51 -0
  59. package/core/tui/HelpBar.js +42 -0
  60. package/core/tui/ServersPanel.js +109 -0
  61. package/core/tui/ToxicFlowsPanel.js +100 -0
  62. package/core/tui/h.js +8 -0
  63. package/core/tui/index.js +11 -0
  64. package/core/tui/render.js +22 -0
  65. package/package.json +24 -16
  66. package/ui/dist/assets/{index-CiCSDYf-.js → index-Buah4fNS.js} +32 -32
  67. package/ui/dist/index.html +1 -1
  68. package/ui/server/controllers/RequestController.js +9 -1
  69. package/ui/server/controllers/SecurityFindingsController.js +46 -1
  70. package/ui/server/routes/requests.js +8 -1
  71. package/ui/server/routes/security.js +5 -1
  72. package/ui/server/setup.js +2 -0
  73. package/ui/server/swagger/paths/components.js +55 -0
  74. package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
  75. package/ui/server/swagger/paths.js +2 -0
  76. package/ui/server/swagger/swagger.js +5 -1
  77. package/ui/src/Security.jsx +10 -0
  78. package/ui/src/components/Security/ScannerContent.jsx +30 -1
  79. package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
  80. package/ui/src/components/Security/securityApi.js +15 -0
  81. package/ui/src/components/Security/useSecurity.js +60 -3
  82. package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
  83. package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
  84. package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
  85. package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
  86. package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
  87. package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
  88. package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
  89. package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
  90. package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
  91. package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
  92. package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
  93. package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
  94. package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
  95. package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
  96. package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
  97. package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
  98. package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
  99. package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
  100. package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
@@ -0,0 +1,111 @@
1
+ {
2
+ "mcp-server-github": {
3
+ "get_issue": "ingests_untrusted",
4
+ "get_pull_request": "ingests_untrusted",
5
+ "search_issues": "ingests_untrusted",
6
+ "list_issues": "ingests_untrusted",
7
+ "search_code": "ingests_untrusted",
8
+ "get_file_contents": "reads_secrets",
9
+ "create_pull_request": "writes_code",
10
+ "push_files": "writes_code",
11
+ "create_issue": "writes_code",
12
+ "update_issue": "writes_code",
13
+ "create_or_update_file": "writes_code",
14
+ "fork_repository": "writes_code",
15
+ "create_branch": "writes_code"
16
+ },
17
+ "mcp-server-slack": {
18
+ "list_messages": "ingests_untrusted",
19
+ "get_channel_history": "ingests_untrusted",
20
+ "search_messages": "ingests_untrusted",
21
+ "send_message": "sends_external",
22
+ "post_message": "sends_external",
23
+ "upload_file": "sends_external"
24
+ },
25
+ "mcp-server-filesystem": {
26
+ "read_file": "reads_secrets",
27
+ "read_multiple_files": "reads_secrets",
28
+ "list_directory": "reads_secrets",
29
+ "search_files": "reads_secrets",
30
+ "get_file_info": "reads_secrets",
31
+ "write_file": "writes_code",
32
+ "create_directory": "writes_code",
33
+ "move_file": "writes_code",
34
+ "edit_file": "writes_code"
35
+ },
36
+ "mcp-server-git": {
37
+ "git_log": "reads_secrets",
38
+ "git_diff": "reads_secrets",
39
+ "git_status": "reads_secrets",
40
+ "git_show": "reads_secrets",
41
+ "git_commit": "writes_code",
42
+ "git_add": "writes_code",
43
+ "git_push": "writes_code",
44
+ "git_init": "writes_code"
45
+ },
46
+ "mcp-server-postgres": {
47
+ "query": "reads_secrets",
48
+ "list_tables": "reads_secrets",
49
+ "describe_table": "reads_secrets"
50
+ },
51
+ "mcp-server-sqlite": {
52
+ "read_query": "reads_secrets",
53
+ "write_query": "writes_code",
54
+ "list_tables": "reads_secrets"
55
+ },
56
+ "mcp-server-fetch": {
57
+ "fetch": "ingests_untrusted"
58
+ },
59
+ "mcp-server-brave-search": {
60
+ "brave_web_search": "ingests_untrusted",
61
+ "brave_local_search": "ingests_untrusted"
62
+ },
63
+ "mcp-server-puppeteer": {
64
+ "navigate": "ingests_untrusted",
65
+ "screenshot": "ingests_untrusted",
66
+ "click": "ingests_untrusted",
67
+ "evaluate": "writes_code"
68
+ },
69
+ "@playwright/mcp": {
70
+ "browser_navigate": "ingests_untrusted",
71
+ "browser_snapshot": "ingests_untrusted",
72
+ "browser_click": "ingests_untrusted",
73
+ "browser_type": "ingests_untrusted"
74
+ },
75
+ "mcp-server-kubernetes": {
76
+ "kubectl_get": "reads_secrets",
77
+ "kubectl_apply": "modifies_infra",
78
+ "kubectl_delete": "modifies_infra",
79
+ "kubectl_scale": "modifies_infra",
80
+ "kubectl_patch": "modifies_infra"
81
+ },
82
+ "heroku-mcp-server": {
83
+ "list_apps": "reads_secrets",
84
+ "transfer_app": "modifies_infra",
85
+ "scale_formation": "modifies_infra",
86
+ "create_app": "modifies_infra",
87
+ "delete_app": "modifies_infra"
88
+ },
89
+ "mcp-server-docker": {
90
+ "list_containers": "reads_secrets",
91
+ "container_run": "modifies_infra",
92
+ "container_stop": "modifies_infra",
93
+ "container_remove": "modifies_infra",
94
+ "image_pull": "modifies_infra"
95
+ },
96
+ "google-docs-mcp": {
97
+ "get_document": "ingests_untrusted",
98
+ "search_documents": "ingests_untrusted",
99
+ "create_document": "writes_code"
100
+ },
101
+ "jira-mcp-server": {
102
+ "get_issue": "ingests_untrusted",
103
+ "search_issues": "ingests_untrusted",
104
+ "get_ticket": "ingests_untrusted",
105
+ "create_issue": "writes_code"
106
+ },
107
+ "mcp-server-memory": {
108
+ "store": "writes_code",
109
+ "retrieve": "reads_secrets"
110
+ }
111
+ }
@@ -0,0 +1,47 @@
1
+ [
2
+ {
3
+ "source": "ingests_untrusted",
4
+ "target": "writes_code",
5
+ "risk": "HIGH",
6
+ "title": "Prompt injection \u2192 code modification",
7
+ "scenario": "A {source_ide} message with prompt injection processed by {source} could cause your agent to push malicious code via {target}.",
8
+ "catalog": "\u00a71.3, \u00a71.10, \u00a71.12",
9
+ "owasp": "MCP03 + MCP10"
10
+ },
11
+ {
12
+ "source": "ingests_untrusted",
13
+ "target": "sends_external",
14
+ "risk": "HIGH",
15
+ "title": "Prompt injection \u2192 data exfiltration",
16
+ "scenario": "Untrusted content ingested by {source} could instruct the agent to exfiltrate sensitive data through {target}.",
17
+ "catalog": "\u00a71.2",
18
+ "owasp": "MCP03 + MCP06"
19
+ },
20
+ {
21
+ "source": "reads_secrets",
22
+ "target": "sends_external",
23
+ "risk": "HIGH",
24
+ "title": "Secret theft via external channel",
25
+ "scenario": "Your agent can read sensitive files via {source} and exfiltrate them through {target}.",
26
+ "catalog": "\u00a71.1, \u00a71.14",
27
+ "owasp": "MCP01 + MCP10"
28
+ },
29
+ {
30
+ "source": "ingests_untrusted",
31
+ "target": "modifies_infra",
32
+ "risk": "HIGH",
33
+ "title": "Prompt injection \u2192 infrastructure takeover",
34
+ "scenario": "Attacker-controlled content from {source} could cause infrastructure changes via {target}.",
35
+ "catalog": "\u00a71.13",
36
+ "owasp": "MCP03 + MCP05"
37
+ },
38
+ {
39
+ "source": "reads_secrets",
40
+ "target": "ingests_untrusted",
41
+ "risk": "MEDIUM",
42
+ "title": "Sensitive data leakage to untrusted channel",
43
+ "scenario": "Sensitive data from {source} could leak into context shared with untrusted content from {target}.",
44
+ "catalog": "\u00a71.7",
45
+ "owasp": "MCP10"
46
+ }
47
+ ]
@@ -0,0 +1,23 @@
1
+ /**
2
+ * CLI module barrel file
3
+ */
4
+ export { applyFixes, renderFixResults } from './AutoFixEngine.js';
5
+ export { scanIdeConfigs, getAllServers } from './ConfigScanner.js';
6
+ export { loadBuiltinJson, loadUserYamlList, loadUserYamlMap } from './DataLoader.js';
7
+ export { loadDeclarativeRules } from './DeclarativeRuleEngine.js';
8
+ export { executeDoctor } from './DoctorCommand.js';
9
+ export { applyFix, createEnvExample, undoFixes } from './FixHandlers.js';
10
+ export { generateHtmlReport } from './HtmlReportGenerator.js';
11
+ export { executeList } from './ListCommand.js';
12
+ export { computeDiff, renderDiff, hashToolDefinition } from './LockDiffEngine.js';
13
+ export { executeLock, executeLockVerify, executeDiff } from './LockCommand.js';
14
+ export { executeScan } from './ScanCommand.js';
15
+ export { runScan } from './ScanService.js';
16
+ export { detectHardcodedSecrets } from './SecretDetector.js';
17
+ export { S } from './symbols.js';
18
+ export { calculateSharkScore, countBySeverity } from './SharkScoreCalculator.js';
19
+ export { analyzeToxicFlows } from './ToxicFlowAnalyzer.js';
20
+ export { executeUpdateRules } from './UpdateCommand.js';
21
+ export { executeWatch } from './WatchCommand.js';
22
+ export { generateWalkthroughs } from './WalkthroughGenerator.js';
23
+ export { loadYamlRules, applyYamlRules } from './YamlRuleEngine.js';
@@ -0,0 +1,52 @@
1
+ /**
2
+ * CLI banner — clean, text-only
3
+ */
4
+ import { readFileSync } from 'node:fs';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import kleur from 'kleur';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ const rootDir = resolve(__dirname, '..', '..', '..');
12
+
13
+ function getVersion() {
14
+ try {
15
+ const pkgPath = join(rootDir, 'package.json');
16
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
17
+ return pkg.version;
18
+ } catch (_err) {
19
+ return 'unknown';
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Display the scan banner — minimal, Biome-style
25
+ */
26
+ export function displayScanBanner() {
27
+ const version = getVersion();
28
+
29
+ console.log('');
30
+ console.log(` ${kleur.bold('mcp-shark')} ${kleur.dim(`v${version}`)}`);
31
+ console.log(kleur.dim(' ─────────────────────────────────────'));
32
+ console.log('');
33
+ }
34
+
35
+ /**
36
+ * Display the serve banner (existing ASCII art style)
37
+ */
38
+ export function displayServeBanner() {
39
+ const version = getVersion();
40
+ const banner = `
41
+ ███╗ ███╗ ██████╗ ██████╗ ███████╗██╗ ██╗ █████╗ ██████╗ ██╗ ██╗
42
+ ████╗ ████║██╔════╝██╔══██╗ ██╔════╝██║ ██║██╔══██╗██╔══██╗██║ ██╔╝
43
+ ██╔████╔██║██║ ██████╔╝ ███████╗███████║███████║██████╔╝█████╔╝
44
+ ██║╚██╔╝██║██║ ██╔═══╝ ╚════██║██╔══██║██╔══██║██╔══██╗██╔═██╗
45
+ ██║ ╚═╝ ██║╚██████╗██║ ███████║██║ ██║██║ ██║██║ ██║██║ ██╗
46
+ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
47
+
48
+ Aggregate multiple MCP servers into a unified interface
49
+ Version: ${version} | Homepage: https://mcpshark.sh
50
+ `;
51
+ console.log(banner);
52
+ }
@@ -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
+ }