mcp-ssh-vps 0.4.1__py3-none-any.whl
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.
- mcp_ssh_vps-0.4.1.dist-info/METADATA +482 -0
- mcp_ssh_vps-0.4.1.dist-info/RECORD +47 -0
- mcp_ssh_vps-0.4.1.dist-info/WHEEL +5 -0
- mcp_ssh_vps-0.4.1.dist-info/entry_points.txt +4 -0
- mcp_ssh_vps-0.4.1.dist-info/licenses/LICENSE +21 -0
- mcp_ssh_vps-0.4.1.dist-info/top_level.txt +1 -0
- sshmcp/__init__.py +3 -0
- sshmcp/cli.py +473 -0
- sshmcp/config.py +155 -0
- sshmcp/core/__init__.py +5 -0
- sshmcp/core/container.py +291 -0
- sshmcp/models/__init__.py +15 -0
- sshmcp/models/command.py +69 -0
- sshmcp/models/file.py +102 -0
- sshmcp/models/machine.py +139 -0
- sshmcp/monitoring/__init__.py +0 -0
- sshmcp/monitoring/alerts.py +464 -0
- sshmcp/prompts/__init__.py +7 -0
- sshmcp/prompts/backup.py +151 -0
- sshmcp/prompts/deploy.py +115 -0
- sshmcp/prompts/monitor.py +146 -0
- sshmcp/resources/__init__.py +7 -0
- sshmcp/resources/logs.py +99 -0
- sshmcp/resources/metrics.py +204 -0
- sshmcp/resources/status.py +160 -0
- sshmcp/security/__init__.py +7 -0
- sshmcp/security/audit.py +314 -0
- sshmcp/security/rate_limiter.py +221 -0
- sshmcp/security/totp.py +392 -0
- sshmcp/security/validator.py +234 -0
- sshmcp/security/whitelist.py +169 -0
- sshmcp/server.py +632 -0
- sshmcp/ssh/__init__.py +6 -0
- sshmcp/ssh/async_client.py +247 -0
- sshmcp/ssh/client.py +464 -0
- sshmcp/ssh/executor.py +79 -0
- sshmcp/ssh/forwarding.py +368 -0
- sshmcp/ssh/pool.py +343 -0
- sshmcp/ssh/shell.py +518 -0
- sshmcp/ssh/transfer.py +461 -0
- sshmcp/tools/__init__.py +13 -0
- sshmcp/tools/commands.py +226 -0
- sshmcp/tools/files.py +220 -0
- sshmcp/tools/helpers.py +321 -0
- sshmcp/tools/history.py +372 -0
- sshmcp/tools/processes.py +214 -0
- sshmcp/tools/servers.py +484 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Command whitelist management."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import TYPE_CHECKING, Dict, List, Pattern
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from sshmcp.models.machine import MachinesConfig
|
|
10
|
+
|
|
11
|
+
logger = structlog.get_logger()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CommandWhitelist:
|
|
15
|
+
"""
|
|
16
|
+
Manages command whitelist patterns with compiled regex caching.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
"""Initialize whitelist manager."""
|
|
21
|
+
self._patterns: Dict[str, List[Pattern[str]]] = {}
|
|
22
|
+
self._forbidden: Dict[str, List[Pattern[str]]] = {}
|
|
23
|
+
|
|
24
|
+
def load_patterns(
|
|
25
|
+
self,
|
|
26
|
+
machine_name: str,
|
|
27
|
+
allowed: List[str],
|
|
28
|
+
forbidden: List[str],
|
|
29
|
+
) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Load and compile patterns for a machine.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
machine_name: Name of the machine.
|
|
35
|
+
allowed: List of allowed command patterns.
|
|
36
|
+
forbidden: List of forbidden command patterns.
|
|
37
|
+
"""
|
|
38
|
+
self._patterns[machine_name] = []
|
|
39
|
+
self._forbidden[machine_name] = []
|
|
40
|
+
|
|
41
|
+
for pattern in allowed:
|
|
42
|
+
try:
|
|
43
|
+
compiled = re.compile(pattern, re.IGNORECASE)
|
|
44
|
+
self._patterns[machine_name].append(compiled)
|
|
45
|
+
except re.error as e:
|
|
46
|
+
logger.error(
|
|
47
|
+
"invalid_allowed_pattern",
|
|
48
|
+
machine=machine_name,
|
|
49
|
+
pattern=pattern,
|
|
50
|
+
error=str(e),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
for pattern in forbidden:
|
|
54
|
+
try:
|
|
55
|
+
compiled = re.compile(pattern, re.IGNORECASE)
|
|
56
|
+
self._forbidden[machine_name].append(compiled)
|
|
57
|
+
except re.error as e:
|
|
58
|
+
logger.error(
|
|
59
|
+
"invalid_forbidden_pattern",
|
|
60
|
+
machine=machine_name,
|
|
61
|
+
pattern=pattern,
|
|
62
|
+
error=str(e),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
logger.info(
|
|
66
|
+
"whitelist_loaded",
|
|
67
|
+
machine=machine_name,
|
|
68
|
+
allowed_count=len(self._patterns[machine_name]),
|
|
69
|
+
forbidden_count=len(self._forbidden[machine_name]),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def is_allowed(self, machine_name: str, command: str) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Check if command is allowed for machine.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
machine_name: Name of the machine.
|
|
78
|
+
command: Command to check.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
True if command is allowed.
|
|
82
|
+
"""
|
|
83
|
+
command = command.strip()
|
|
84
|
+
|
|
85
|
+
# Check forbidden first
|
|
86
|
+
if machine_name in self._forbidden:
|
|
87
|
+
for pattern in self._forbidden[machine_name]:
|
|
88
|
+
if pattern.match(command):
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
# If no allowed patterns, allow all (except forbidden)
|
|
92
|
+
if machine_name not in self._patterns or not self._patterns[machine_name]:
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
# Check allowed patterns
|
|
96
|
+
for pattern in self._patterns[machine_name]:
|
|
97
|
+
if pattern.match(command):
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def is_forbidden(self, machine_name: str, command: str) -> bool:
|
|
103
|
+
"""
|
|
104
|
+
Check if command is explicitly forbidden.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
machine_name: Name of the machine.
|
|
108
|
+
command: Command to check.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
True if command is forbidden.
|
|
112
|
+
"""
|
|
113
|
+
if machine_name not in self._forbidden:
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
command = command.strip()
|
|
117
|
+
for pattern in self._forbidden[machine_name]:
|
|
118
|
+
if pattern.match(command):
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def get_patterns(self, machine_name: str) -> Dict[str, int]:
|
|
124
|
+
"""
|
|
125
|
+
Get pattern counts for a machine.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
machine_name: Name of the machine.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Dictionary with allowed and forbidden pattern counts.
|
|
132
|
+
"""
|
|
133
|
+
return {
|
|
134
|
+
"allowed": len(self._patterns.get(machine_name, [])),
|
|
135
|
+
"forbidden": len(self._forbidden.get(machine_name, [])),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Global whitelist instance
|
|
140
|
+
_whitelist: CommandWhitelist | None = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_whitelist() -> CommandWhitelist:
|
|
144
|
+
"""Get or create the global whitelist."""
|
|
145
|
+
global _whitelist
|
|
146
|
+
if _whitelist is None:
|
|
147
|
+
_whitelist = CommandWhitelist()
|
|
148
|
+
return _whitelist
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def init_whitelist(config: "MachinesConfig") -> CommandWhitelist: # type: ignore
|
|
152
|
+
"""
|
|
153
|
+
Initialize whitelist from configuration.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
config: MachinesConfig with machine definitions.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Initialized CommandWhitelist.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
whitelist = get_whitelist()
|
|
163
|
+
for machine in config.machines:
|
|
164
|
+
whitelist.load_patterns(
|
|
165
|
+
machine.name,
|
|
166
|
+
machine.security.allowed_commands,
|
|
167
|
+
machine.security.forbidden_commands,
|
|
168
|
+
)
|
|
169
|
+
return whitelist
|