@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,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,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.
|
|
4
|
-
"description": "
|
|
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
|
|
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
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
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",
|