@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,115 @@
1
+ /**
2
+ * TUI Findings Panel — Scrollable list of security findings with detail view
3
+ */
4
+ import { Box, Text } from 'ink';
5
+ import { h } from './h.js';
6
+
7
+ const SEVERITY_COLORS = { critical: 'red', high: 'yellow', medium: 'blue', low: 'gray' };
8
+ const SEVERITY_ICONS = { critical: '✖', high: '▲', medium: '●', low: '○' };
9
+ const VISIBLE_ROWS = 15;
10
+
11
+ export function FindingsPanel({ findings, selectedIndex }) {
12
+ if (findings.length === 0) {
13
+ return h(
14
+ Box,
15
+ { padding: 1 },
16
+ h(Text, { color: 'green' }, '✔ No security issues found. Your MCP setup looks clean.')
17
+ );
18
+ }
19
+
20
+ const clamped = Math.min(selectedIndex, findings.length - 1);
21
+ const scrollOffset = Math.max(0, clamped - VISIBLE_ROWS + 3);
22
+ const visible = findings.slice(scrollOffset, scrollOffset + VISIBLE_ROWS);
23
+ const selected = findings[clamped];
24
+
25
+ const rows = visible.map((finding, idx) => {
26
+ const actualIdx = scrollOffset + idx;
27
+ const isSelected = actualIdx === clamped;
28
+ const sev = (finding.severity || 'medium').toLowerCase();
29
+ const color = SEVERITY_COLORS[sev] || 'white';
30
+ const icon = SEVERITY_ICONS[sev] || '●';
31
+
32
+ return h(
33
+ Box,
34
+ { key: finding.rule_id + finding.title + actualIdx },
35
+ h(
36
+ Text,
37
+ { inverse: isSelected, color: isSelected ? 'cyan' : color },
38
+ `${isSelected ? '▸' : ' '} ${icon} ${sev.toUpperCase().padEnd(8)}`
39
+ ),
40
+ h(
41
+ Text,
42
+ { inverse: isSelected, color: isSelected ? 'white' : 'gray' },
43
+ ` ${truncate(finding.title, 70)}`
44
+ )
45
+ );
46
+ });
47
+
48
+ const scrollInfo =
49
+ findings.length > VISIBLE_ROWS
50
+ ? h(
51
+ Text,
52
+ { color: 'gray', dimColor: true },
53
+ `${scrollOffset > 0 ? '↑ ' : ' '}${scrollOffset + VISIBLE_ROWS < findings.length ? '↓ scroll' : ' end'} (${clamped + 1}/${findings.length})`
54
+ )
55
+ : null;
56
+
57
+ const detail = selected
58
+ ? h(
59
+ Box,
60
+ { flexDirection: 'column', borderStyle: 'single', borderColor: 'cyan', paddingX: 1 },
61
+ h(Text, { bold: true, color: 'white' }, ' Detail '),
62
+ h(Text, null, h(Text, { bold: true }, 'Rule:'), ` ${selected.rule_id || '—'}`),
63
+ h(
64
+ Text,
65
+ null,
66
+ h(Text, { bold: true }, 'Severity:'),
67
+ ' ',
68
+ h(
69
+ Text,
70
+ { color: SEVERITY_COLORS[(selected.severity || '').toLowerCase()] },
71
+ selected.severity
72
+ )
73
+ ),
74
+ h(Text, null, h(Text, { bold: true }, 'Server:'), ` ${selected.server_name || '—'}`),
75
+ h(Text, null, h(Text, { bold: true }, 'IDE:'), ` ${selected.ide || '—'}`),
76
+ h(
77
+ Text,
78
+ { wrap: 'wrap' },
79
+ h(Text, { bold: true }, 'Description:'),
80
+ ` ${selected.description || '—'}`
81
+ ),
82
+ selected.recommendation
83
+ ? h(
84
+ Text,
85
+ { wrap: 'wrap' },
86
+ h(Text, { bold: true }, 'Fix:'),
87
+ ` ${selected.recommendation}`
88
+ )
89
+ : null,
90
+ selected.fixable
91
+ ? h(Text, { color: 'green' }, '✔ Auto-fixable — press 4 to go to Fix panel')
92
+ : null
93
+ )
94
+ : null;
95
+
96
+ return h(
97
+ Box,
98
+ { flexDirection: 'column' },
99
+ h(
100
+ Box,
101
+ { flexDirection: 'column', borderStyle: 'single', borderColor: 'gray', paddingX: 1 },
102
+ h(Text, { bold: true, color: 'white' }, ` Findings (${findings.length}) `),
103
+ ...rows,
104
+ scrollInfo
105
+ ),
106
+ detail
107
+ );
108
+ }
109
+
110
+ function truncate(str, maxLen) {
111
+ if (!str) {
112
+ return '';
113
+ }
114
+ return str.length > maxLen ? `${str.slice(0, maxLen - 1)}…` : str;
115
+ }
@@ -0,0 +1,132 @@
1
+ import { Box, Text, useInput } from 'ink';
2
+ /**
3
+ * TUI Fix Panel — Auto-fix interface with interactive selection
4
+ */
5
+ import { useState } from 'react';
6
+ import { applyFixes } from '#core/cli/AutoFixEngine.js';
7
+ import { h } from './h.js';
8
+
9
+ export function FixPanel({ findings, onRescan }) {
10
+ const fixable = findings.filter((f) => f.fixable);
11
+ const [fixResult, setFixResult] = useState(null);
12
+ const [confirming, setConfirming] = useState(false);
13
+
14
+ useInput((input) => {
15
+ if (confirming) {
16
+ if (input === 'y') {
17
+ try {
18
+ setFixResult(applyFixes(findings));
19
+ } catch (err) {
20
+ setFixResult({
21
+ fixed: [],
22
+ skipped: [],
23
+ errors: [{ success: false, error: err.message, finding: { title: 'Apply fixes' } }],
24
+ });
25
+ }
26
+ setConfirming(false);
27
+ }
28
+ if (input === 'n') {
29
+ setConfirming(false);
30
+ }
31
+ return;
32
+ }
33
+
34
+ if (input === 'f' && fixable.length > 0) {
35
+ setConfirming(true);
36
+ }
37
+ if (input === 'u') {
38
+ try {
39
+ setFixResult(applyFixes(findings, { undo: true }));
40
+ } catch (err) {
41
+ setFixResult({
42
+ fixed: [],
43
+ skipped: [],
44
+ errors: [{ success: false, error: err.message, finding: { title: 'Undo fixes' } }],
45
+ });
46
+ }
47
+ }
48
+ if (input === 'r') {
49
+ setFixResult(null);
50
+ onRescan();
51
+ }
52
+ });
53
+
54
+ if (fixable.length === 0 && !fixResult) {
55
+ return h(
56
+ Box,
57
+ { padding: 1, flexDirection: 'column' },
58
+ h(Text, { color: 'green' }, '✔ No auto-fixable issues found.'),
59
+ h(Text, { color: 'gray' }, 'All findings require manual remediation.')
60
+ );
61
+ }
62
+
63
+ const fixRows = fixable.map((finding) =>
64
+ h(
65
+ Box,
66
+ { key: `${finding.rule_id}-${finding.title}` },
67
+ h(Text, { color: 'green' }, ' ● '),
68
+ h(Text, { color: 'white' }, `${finding.fix_type} — `),
69
+ h(Text, { color: 'gray' }, truncate(finding.title, 60))
70
+ )
71
+ );
72
+
73
+ const prompt = confirming
74
+ ? h(
75
+ Text,
76
+ { bold: true, color: 'yellow' },
77
+ `Apply ${fixable.length} fixes? Backups will be created. (y/n)`
78
+ )
79
+ : h(
80
+ Text,
81
+ { color: 'gray' },
82
+ 'Press ',
83
+ h(Text, { bold: true, color: 'cyan' }, 'f'),
84
+ ' to fix all · ',
85
+ h(Text, { bold: true, color: 'cyan' }, 'u'),
86
+ ' to undo · ',
87
+ h(Text, { bold: true, color: 'cyan' }, 'r'),
88
+ ' to rescan'
89
+ );
90
+
91
+ const resultPanel = fixResult
92
+ ? h(
93
+ Box,
94
+ { flexDirection: 'column', borderStyle: 'single', borderColor: 'cyan', paddingX: 1 },
95
+ h(Text, { bold: true, color: 'white' }, ' Results '),
96
+ ...fixResult.fixed.map((fx) =>
97
+ h(Text, { key: `fix-${fx.message}`, color: 'green' }, ` ✔ ${fx.message}`)
98
+ ),
99
+ ...fixResult.errors.map((err) =>
100
+ h(Text, { key: `err-${err.error}`, color: 'red' }, ` ✖ ${err.error || err.reason}`)
101
+ ),
102
+ ...fixResult.skipped.map((sk) =>
103
+ h(Text, { key: `skip-${sk.reason}`, color: 'gray' }, ` ○ Skipped: ${sk.reason}`)
104
+ ),
105
+ h(
106
+ Text,
107
+ { bold: true },
108
+ ` ${fixResult.fixed.length} fixed · ${fixResult.errors.length} errors · ${fixResult.skipped.length} skipped`
109
+ )
110
+ )
111
+ : null;
112
+
113
+ return h(
114
+ Box,
115
+ { flexDirection: 'column' },
116
+ h(
117
+ Box,
118
+ { flexDirection: 'column', borderStyle: 'single', borderColor: 'green', paddingX: 1 },
119
+ h(Text, { bold: true, color: 'white' }, ` Auto-Fix (${fixable.length} fixable) `),
120
+ ...fixRows,
121
+ h(Box, { marginTop: 1 }, prompt)
122
+ ),
123
+ resultPanel
124
+ );
125
+ }
126
+
127
+ function truncate(str, maxLen) {
128
+ if (!str) {
129
+ return '';
130
+ }
131
+ return str.length > maxLen ? `${str.slice(0, maxLen - 1)}…` : str;
132
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * TUI Header — Score display and scan summary
3
+ */
4
+ import { Box, Text } from 'ink';
5
+ import { h } from './h.js';
6
+
7
+ const GRADE_COLORS = { A: 'green', B: 'green', C: 'yellow', D: 'yellow', F: 'red' };
8
+
9
+ export function Header({
10
+ score,
11
+ grade,
12
+ serverCount,
13
+ findingCount,
14
+ flowCount,
15
+ ruleCount,
16
+ elapsedMs,
17
+ }) {
18
+ const gradeColor = GRADE_COLORS[grade] || 'white';
19
+
20
+ const scoreDisplay =
21
+ score !== null
22
+ ? h(
23
+ '',
24
+ null,
25
+ h(Text, { bold: true }, 'Score: '),
26
+ h(Text, { bold: true, color: gradeColor }, `${score}/100 (${grade})`)
27
+ )
28
+ : h(Text, { color: 'gray' }, 'Scanning...');
29
+
30
+ const statsDisplay =
31
+ score !== null
32
+ ? h(
33
+ Text,
34
+ { color: 'gray' },
35
+ `${findingCount} findings · ${flowCount} flows · ${serverCount} servers · ${ruleCount} rules · ${elapsedMs}ms`
36
+ )
37
+ : null;
38
+
39
+ return h(
40
+ Box,
41
+ { flexDirection: 'row', paddingX: 1, justifyContent: 'space-between' },
42
+ h(
43
+ Box,
44
+ null,
45
+ h(Text, { bold: true, color: 'cyan' }, 'mcp-shark'),
46
+ h(Text, { color: 'gray' }, ' │ '),
47
+ scoreDisplay
48
+ ),
49
+ statsDisplay
50
+ );
51
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * TUI Help Bar — Keyboard shortcuts displayed at the bottom
3
+ */
4
+ import { Box, Text } from 'ink';
5
+ import { h } from './h.js';
6
+
7
+ const COMMON_KEYS = [
8
+ { key: 'j/k', desc: 'navigate' },
9
+ { key: 'Tab', desc: 'next panel' },
10
+ { key: '1-4', desc: 'panel' },
11
+ { key: 'r', desc: 'rescan' },
12
+ { key: 'q', desc: 'quit' },
13
+ ];
14
+
15
+ const PANEL_KEYS = {
16
+ findings: [],
17
+ servers: [],
18
+ flows: [],
19
+ fix: [
20
+ { key: 'f', desc: 'fix all' },
21
+ { key: 'u', desc: 'undo' },
22
+ ],
23
+ };
24
+
25
+ export function HelpBar({ activePanel }) {
26
+ const panelSpecific = PANEL_KEYS[activePanel] || [];
27
+ const allKeys = [...panelSpecific, ...COMMON_KEYS];
28
+
29
+ return h(
30
+ Box,
31
+ { paddingX: 1 },
32
+ ...allKeys.map(({ key, desc }, idx) =>
33
+ h(
34
+ Box,
35
+ { key, marginRight: 1 },
36
+ h(Text, { bold: true, color: 'cyan' }, key),
37
+ h(Text, { color: 'gray' }, ` ${desc}`),
38
+ idx < allKeys.length - 1 ? h(Text, { color: 'gray' }, ' │') : null
39
+ )
40
+ )
41
+ );
42
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * TUI Servers Panel — Server inventory with finding counts
3
+ */
4
+ import { Box, Text } from 'ink';
5
+ import { h } from './h.js';
6
+
7
+ export function ServersPanel({ servers, findings, selectedIndex }) {
8
+ if (servers.length === 0) {
9
+ return h(
10
+ Box,
11
+ { padding: 1 },
12
+ h(Text, { color: 'yellow' }, '\u25B2 No MCP servers detected across 15 IDEs.')
13
+ );
14
+ }
15
+
16
+ const clamped = Math.min(selectedIndex, servers.length - 1);
17
+ const selected = servers[clamped];
18
+ const selectedFindings = findings.filter((f) => f.server_name === selected?.name);
19
+
20
+ const rows = servers.map((server, idx) => {
21
+ const isSelected = idx === clamped;
22
+ const count = findings.filter((f) => f.server_name === server.name).length;
23
+ const countColor = count === 0 ? 'green' : count > 3 ? 'red' : 'yellow';
24
+
25
+ return h(
26
+ Box,
27
+ { key: `${server.name}-${server.ide}` },
28
+ h(
29
+ Text,
30
+ { inverse: isSelected, color: isSelected ? 'cyan' : 'white' },
31
+ `${isSelected ? '▸' : ' '} ${server.name.padEnd(25)}`
32
+ ),
33
+ h(Text, { inverse: isSelected, color: isSelected ? 'white' : 'gray' }, server.ide.padEnd(15)),
34
+ h(
35
+ Text,
36
+ { inverse: isSelected, color: isSelected ? 'white' : countColor },
37
+ count === 0 ? '✔ clean' : `${count} issues`
38
+ )
39
+ );
40
+ });
41
+
42
+ const findingRows = selectedFindings.slice(0, 5).map((f) =>
43
+ h(
44
+ Text,
45
+ {
46
+ key: `${f.rule_id}-${f.title}`,
47
+ color: (f.severity || '').toLowerCase() === 'critical' ? 'red' : 'yellow',
48
+ },
49
+ ` ${f.severity?.toUpperCase()} — ${f.title}`
50
+ )
51
+ );
52
+
53
+ const moreText =
54
+ selectedFindings.length > 5
55
+ ? h(Text, { color: 'gray' }, ` ... and ${selectedFindings.length - 5} more`)
56
+ : null;
57
+
58
+ const detail = selected
59
+ ? h(
60
+ Box,
61
+ { flexDirection: 'column', borderStyle: 'single', borderColor: 'cyan', paddingX: 1 },
62
+ h(Text, { bold: true, color: 'white' }, ` ${selected.name} `),
63
+ h(Text, null, h(Text, { bold: true }, 'IDE:'), ` ${selected.ide}`),
64
+ h(Text, null, h(Text, { bold: true }, 'Config:'), ` ${selected.configPath || '—'}`),
65
+ h(
66
+ Text,
67
+ null,
68
+ h(Text, { bold: true }, 'Transport:'),
69
+ ` ${detectTransport(selected.config)}`
70
+ ),
71
+ h(Text, null, h(Text, { bold: true }, 'Command:'), ` ${selected.config?.command || '—'}`),
72
+ h(
73
+ Text,
74
+ null,
75
+ h(Text, { bold: true }, 'Tools:'),
76
+ ` ${Array.isArray(selected.tools) ? selected.tools.length : '?'}`
77
+ ),
78
+ h(Text, null, h(Text, { bold: true }, 'Findings:'), ` ${selectedFindings.length}`),
79
+ selectedFindings.length > 0
80
+ ? h(Box, { flexDirection: 'column' }, ...findingRows, moreText)
81
+ : null
82
+ )
83
+ : null;
84
+
85
+ return h(
86
+ Box,
87
+ { flexDirection: 'column' },
88
+ h(
89
+ Box,
90
+ { flexDirection: 'column', borderStyle: 'single', borderColor: 'gray', paddingX: 1 },
91
+ h(Text, { bold: true, color: 'white' }, ` Servers (${servers.length}) `),
92
+ ...rows
93
+ ),
94
+ detail
95
+ );
96
+ }
97
+
98
+ function detectTransport(config) {
99
+ if (!config) {
100
+ return 'unknown';
101
+ }
102
+ if (config.command) {
103
+ return 'stdio';
104
+ }
105
+ if (config.url) {
106
+ return config.transport || 'http';
107
+ }
108
+ return config.transport || 'unknown';
109
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * TUI Toxic Flows Panel — Cross-server attack path visualization
3
+ */
4
+ import { Box, Text } from 'ink';
5
+ import { h } from './h.js';
6
+
7
+ export function ToxicFlowsPanel({ toxicFlows, selectedIndex }) {
8
+ if (toxicFlows.length === 0) {
9
+ return h(
10
+ Box,
11
+ { padding: 1 },
12
+ h(Text, { color: 'green' }, '✔ No toxic cross-server flows detected.')
13
+ );
14
+ }
15
+
16
+ const clamped = Math.min(selectedIndex, toxicFlows.length - 1);
17
+ const selected = toxicFlows[clamped];
18
+
19
+ const rows = toxicFlows.map((flow, idx) => {
20
+ const isSelected = idx === clamped;
21
+ const riskColor = (flow.risk || '').toLowerCase() === 'high' ? 'red' : 'yellow';
22
+ const flowKey = `${flow.source}-${flow.sink}-${flow.rule || idx}`;
23
+
24
+ return h(
25
+ Box,
26
+ { key: flowKey },
27
+ h(
28
+ Text,
29
+ { inverse: isSelected, color: isSelected ? 'magenta' : riskColor },
30
+ `${isSelected ? '▸' : ' '} ${(flow.risk || 'MED').toUpperCase().padEnd(6)}`
31
+ ),
32
+ h(
33
+ Text,
34
+ { inverse: isSelected, color: isSelected ? 'white' : 'gray' },
35
+ ` ${flow.source || '?'} → ${flow.sink || '?'}`
36
+ ),
37
+ h(
38
+ Text,
39
+ { inverse: isSelected, color: isSelected ? 'white' : 'gray', dimColor: !isSelected },
40
+ ` — ${truncate(flow.scenario || '', 50)}`
41
+ )
42
+ );
43
+ });
44
+
45
+ const caps = selected?.capabilities
46
+ ? Object.entries(selected.capabilities)
47
+ .filter(([, v]) => v)
48
+ .map(([k]) => k)
49
+ .join(', ')
50
+ : '—';
51
+
52
+ const detail = selected
53
+ ? h(
54
+ Box,
55
+ { flexDirection: 'column', borderStyle: 'single', borderColor: 'magenta', paddingX: 1 },
56
+ h(Text, { bold: true, color: 'magenta' }, ' Flow Detail '),
57
+ h(
58
+ Text,
59
+ null,
60
+ h(Text, { bold: true, color: 'red' }, selected.source),
61
+ h(Text, { color: 'gray' }, ' → '),
62
+ h(Text, { bold: true, color: 'red' }, selected.sink)
63
+ ),
64
+ h(
65
+ Text,
66
+ null,
67
+ h(Text, { bold: true }, 'Risk:'),
68
+ ' ',
69
+ h(Text, { color: selected.risk === 'high' ? 'red' : 'yellow' }, selected.risk)
70
+ ),
71
+ h(Text, null, h(Text, { bold: true }, 'Rule:'), ` ${selected.rule || '—'}`),
72
+ h(
73
+ Text,
74
+ { wrap: 'wrap' },
75
+ h(Text, { bold: true }, 'Scenario:'),
76
+ ` ${selected.scenario || '—'}`
77
+ ),
78
+ h(Text, { wrap: 'wrap' }, h(Text, { bold: true }, 'Capabilities:'), ` ${caps}`)
79
+ )
80
+ : null;
81
+
82
+ return h(
83
+ Box,
84
+ { flexDirection: 'column' },
85
+ h(
86
+ Box,
87
+ { flexDirection: 'column', borderStyle: 'single', borderColor: 'magenta', paddingX: 1 },
88
+ h(Text, { bold: true, color: 'white' }, ` Toxic Flows (${toxicFlows.length}) `),
89
+ ...rows
90
+ ),
91
+ detail
92
+ );
93
+ }
94
+
95
+ function truncate(str, maxLen) {
96
+ if (!str) {
97
+ return '';
98
+ }
99
+ return str.length > maxLen ? `${str.slice(0, maxLen - 1)}…` : str;
100
+ }
package/core/tui/h.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * JSX-free helper for React.createElement
3
+ * Keeps TUI code readable without requiring a build step.
4
+ */
5
+ import { Fragment, createElement } from 'react';
6
+
7
+ export const h = createElement;
8
+ export const F = Fragment;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * TUI module barrel file
3
+ */
4
+ export { App } from './App.js';
5
+ export { Header } from './Header.js';
6
+ export { FindingsPanel } from './FindingsPanel.js';
7
+ export { ServersPanel } from './ServersPanel.js';
8
+ export { ToxicFlowsPanel } from './ToxicFlowsPanel.js';
9
+ export { FixPanel } from './FixPanel.js';
10
+ export { HelpBar } from './HelpBar.js';
11
+ export { launchTui } from './render.js';
@@ -0,0 +1,22 @@
1
+ import { render } from 'ink';
2
+ /**
3
+ * TUI Renderer — Entry point for the interactive terminal UI
4
+ * Called by `mcp-shark tui` command
5
+ */
6
+ import { createElement } from 'react';
7
+ import { bootstrapLogger as logger } from '#core/libraries/index.js';
8
+ import { App } from './App.js';
9
+
10
+ /**
11
+ * Launch the interactive TUI
12
+ * @returns {Promise<void>}
13
+ */
14
+ export async function launchTui() {
15
+ try {
16
+ const { waitUntilExit } = render(createElement(App));
17
+ await waitUntilExit();
18
+ } catch (err) {
19
+ logger.error({ err: err.message }, 'TUI failed to start');
20
+ throw err;
21
+ }
22
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mcp-shark/mcp-shark",
3
- "version": "1.5.13",
4
- "description": "Aggregate multiple Model Context Protocol (MCP) servers into a single unified interface with a powerful monitoring UI. Prov deep visibility into every request and response.",
3
+ "version": "1.7.2",
4
+ "description": "Security scanner for AI agent tools. Local static scan of MCP IDE configs (41 rules, toxic flow heuristics, AAuth visibility, auto-fix, tool pinning). Optional proxy + in-browser dashboard: traffic, findings, AAuth Explorer, YARA, Playground. Smart Scan optional (your API token). Rule registry fetch is opt-in.",
5
5
  "type": "module",
6
6
  "main": "./bin/mcp-shark.js",
7
7
  "exports": {
@@ -45,12 +45,13 @@
45
45
  "test": "c8 node --test --test-name-pattern='.*' --test-reporter spec $(find core ui/server -path '*/__tests__/*.test.js' -type f)",
46
46
  "test:ui": "vitest run --coverage",
47
47
  "test:all": "npm run test:coverage && npm run test:ui",
48
+ "generate:rules": "node scripts/generate-rule-index.js",
48
49
  "lint": "biome lint .",
49
50
  "lint:fix": "biome lint --write .",
50
51
  "format": "biome format --write .",
51
52
  "check": "biome check .",
52
53
  "check:fix": "biome check --write .",
53
- "prepare": "husky install || true",
54
+ "prepare": "husky || true",
54
55
  "prepublishOnly": "npm run check:fix && npm run build:ui",
55
56
  "pack:inspect": "npm run prepublishOnly && npm pack && tar -tzf mcp-shark-mcp-shark-*.tgz | head -50 && echo '...' && tar -tzf mcp-shark-mcp-shark-*.tgz | wc -l && echo 'files total'",
56
57
  "pack:list": "npm run prepublishOnly && npm pack && tar -tzf mcp-shark-mcp-shark-*.tgz",
@@ -64,20 +65,19 @@
64
65
  "keywords": [
65
66
  "mcp",
66
67
  "model-context-protocol",
67
- "aggregator",
68
- "server",
69
- "ui",
70
- "monitoring",
71
- "debugging",
72
- "wireshark",
73
- "traffic-capture",
74
- "mcp-server",
75
- "mcp-client",
76
68
  "security",
77
- "audit",
78
- "logging",
79
- "playground",
80
- "smart-scan"
69
+ "scanner",
70
+ "vulnerability",
71
+ "owasp",
72
+ "ai-agent",
73
+ "mcp-server",
74
+ "static-analysis",
75
+ "toxic-flow",
76
+ "auto-fix",
77
+ "lockfile",
78
+ "sarif",
79
+ "cli",
80
+ "devtools"
81
81
  ],
82
82
  "author": "",
83
83
  "license": "SEE LICENSE IN LICENSE",
@@ -85,18 +85,26 @@
85
85
  "node": ">=20.0.0"
86
86
  },
87
87
  "dependencies": {
88
+ "@clack/prompts": "^1.2.0",
88
89
  "@iarna/toml": "^2.2.5",
89
90
  "@modelcontextprotocol/sdk": "^1.20.2",
90
91
  "@tabler/icons-react": "^3.0.0",
91
92
  "animejs": "^3.2.2",
92
93
  "better-sqlite3": "^12.4.1",
94
+ "boxen": "^8.0.1",
95
+ "cli-table3": "^0.6.5",
93
96
  "commander": "^14.0.2",
94
97
  "consola": "^3.4.2",
95
98
  "cors": "^2.8.5",
96
99
  "echarts": "^6.0.0",
97
100
  "echarts-for-react": "^3.0.6",
98
101
  "express": "^4.18.2",
102
+ "ink": "^5.2.1",
103
+ "ink-select-input": "^6.2.0",
104
+ "ink-spinner": "^5.0.0",
105
+ "ink-text-input": "^6.0.0",
99
106
  "jsonrpc-lite": "^2.2.0",
107
+ "kleur": "^4.1.5",
100
108
  "open": "^11.0.0",
101
109
  "react": "^18.2.0",
102
110
  "react-dom": "^18.2.0",