@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.
- package/README.md +396 -59
- 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 +224 -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/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/container/DependencyContainer.js +4 -1
- package/core/repositories/PacketRepository.js +16 -0
- package/core/services/AuditService.js +2 -0
- 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/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-CiCSDYf-.js → index-Buah4fNS.js} +32 -32
- package/ui/dist/index.html +1 -1
- package/ui/server/controllers/RequestController.js +9 -1
- package/ui/server/controllers/SecurityFindingsController.js +46 -1
- package/ui/server/routes/requests.js +8 -1
- package/ui/server/routes/security.js +5 -1
- package/ui/server/setup.js +2 -0
- 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 -0
- package/ui/server/swagger/swagger.js +5 -1
- package/ui/src/Security.jsx +10 -0
- package/ui/src/components/Security/ScannerContent.jsx +30 -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/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
|
@@ -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
|
+
}
|