mcp-hangar 0.2.0__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_hangar/__init__.py +139 -0
- mcp_hangar/application/__init__.py +1 -0
- mcp_hangar/application/commands/__init__.py +67 -0
- mcp_hangar/application/commands/auth_commands.py +118 -0
- mcp_hangar/application/commands/auth_handlers.py +296 -0
- mcp_hangar/application/commands/commands.py +59 -0
- mcp_hangar/application/commands/handlers.py +189 -0
- mcp_hangar/application/discovery/__init__.py +21 -0
- mcp_hangar/application/discovery/discovery_metrics.py +283 -0
- mcp_hangar/application/discovery/discovery_orchestrator.py +497 -0
- mcp_hangar/application/discovery/lifecycle_manager.py +315 -0
- mcp_hangar/application/discovery/security_validator.py +414 -0
- mcp_hangar/application/event_handlers/__init__.py +50 -0
- mcp_hangar/application/event_handlers/alert_handler.py +191 -0
- mcp_hangar/application/event_handlers/audit_handler.py +203 -0
- mcp_hangar/application/event_handlers/knowledge_base_handler.py +120 -0
- mcp_hangar/application/event_handlers/logging_handler.py +69 -0
- mcp_hangar/application/event_handlers/metrics_handler.py +152 -0
- mcp_hangar/application/event_handlers/persistent_audit_store.py +217 -0
- mcp_hangar/application/event_handlers/security_handler.py +604 -0
- mcp_hangar/application/mcp/tooling.py +158 -0
- mcp_hangar/application/ports/__init__.py +9 -0
- mcp_hangar/application/ports/observability.py +237 -0
- mcp_hangar/application/queries/__init__.py +52 -0
- mcp_hangar/application/queries/auth_handlers.py +237 -0
- mcp_hangar/application/queries/auth_queries.py +118 -0
- mcp_hangar/application/queries/handlers.py +227 -0
- mcp_hangar/application/read_models/__init__.py +11 -0
- mcp_hangar/application/read_models/provider_views.py +139 -0
- mcp_hangar/application/sagas/__init__.py +11 -0
- mcp_hangar/application/sagas/group_rebalance_saga.py +137 -0
- mcp_hangar/application/sagas/provider_failover_saga.py +266 -0
- mcp_hangar/application/sagas/provider_recovery_saga.py +172 -0
- mcp_hangar/application/services/__init__.py +9 -0
- mcp_hangar/application/services/provider_service.py +208 -0
- mcp_hangar/application/services/traced_provider_service.py +211 -0
- mcp_hangar/bootstrap/runtime.py +328 -0
- mcp_hangar/context.py +178 -0
- mcp_hangar/domain/__init__.py +117 -0
- mcp_hangar/domain/contracts/__init__.py +57 -0
- mcp_hangar/domain/contracts/authentication.py +225 -0
- mcp_hangar/domain/contracts/authorization.py +229 -0
- mcp_hangar/domain/contracts/event_store.py +178 -0
- mcp_hangar/domain/contracts/metrics_publisher.py +59 -0
- mcp_hangar/domain/contracts/persistence.py +383 -0
- mcp_hangar/domain/contracts/provider_runtime.py +146 -0
- mcp_hangar/domain/discovery/__init__.py +20 -0
- mcp_hangar/domain/discovery/conflict_resolver.py +267 -0
- mcp_hangar/domain/discovery/discovered_provider.py +185 -0
- mcp_hangar/domain/discovery/discovery_service.py +412 -0
- mcp_hangar/domain/discovery/discovery_source.py +192 -0
- mcp_hangar/domain/events.py +433 -0
- mcp_hangar/domain/exceptions.py +525 -0
- mcp_hangar/domain/model/__init__.py +70 -0
- mcp_hangar/domain/model/aggregate.py +58 -0
- mcp_hangar/domain/model/circuit_breaker.py +152 -0
- mcp_hangar/domain/model/event_sourced_api_key.py +413 -0
- mcp_hangar/domain/model/event_sourced_provider.py +423 -0
- mcp_hangar/domain/model/event_sourced_role_assignment.py +268 -0
- mcp_hangar/domain/model/health_tracker.py +183 -0
- mcp_hangar/domain/model/load_balancer.py +185 -0
- mcp_hangar/domain/model/provider.py +810 -0
- mcp_hangar/domain/model/provider_group.py +656 -0
- mcp_hangar/domain/model/tool_catalog.py +105 -0
- mcp_hangar/domain/policies/__init__.py +19 -0
- mcp_hangar/domain/policies/provider_health.py +187 -0
- mcp_hangar/domain/repository.py +249 -0
- mcp_hangar/domain/security/__init__.py +85 -0
- mcp_hangar/domain/security/input_validator.py +710 -0
- mcp_hangar/domain/security/rate_limiter.py +387 -0
- mcp_hangar/domain/security/roles.py +237 -0
- mcp_hangar/domain/security/sanitizer.py +387 -0
- mcp_hangar/domain/security/secrets.py +501 -0
- mcp_hangar/domain/services/__init__.py +20 -0
- mcp_hangar/domain/services/audit_service.py +376 -0
- mcp_hangar/domain/services/image_builder.py +328 -0
- mcp_hangar/domain/services/provider_launcher.py +1046 -0
- mcp_hangar/domain/value_objects.py +1138 -0
- mcp_hangar/errors.py +818 -0
- mcp_hangar/fastmcp_server.py +1105 -0
- mcp_hangar/gc.py +134 -0
- mcp_hangar/infrastructure/__init__.py +79 -0
- mcp_hangar/infrastructure/async_executor.py +133 -0
- mcp_hangar/infrastructure/auth/__init__.py +37 -0
- mcp_hangar/infrastructure/auth/api_key_authenticator.py +388 -0
- mcp_hangar/infrastructure/auth/event_sourced_store.py +567 -0
- mcp_hangar/infrastructure/auth/jwt_authenticator.py +360 -0
- mcp_hangar/infrastructure/auth/middleware.py +340 -0
- mcp_hangar/infrastructure/auth/opa_authorizer.py +243 -0
- mcp_hangar/infrastructure/auth/postgres_store.py +659 -0
- mcp_hangar/infrastructure/auth/projections.py +366 -0
- mcp_hangar/infrastructure/auth/rate_limiter.py +311 -0
- mcp_hangar/infrastructure/auth/rbac_authorizer.py +323 -0
- mcp_hangar/infrastructure/auth/sqlite_store.py +624 -0
- mcp_hangar/infrastructure/command_bus.py +112 -0
- mcp_hangar/infrastructure/discovery/__init__.py +110 -0
- mcp_hangar/infrastructure/discovery/docker_source.py +289 -0
- mcp_hangar/infrastructure/discovery/entrypoint_source.py +249 -0
- mcp_hangar/infrastructure/discovery/filesystem_source.py +383 -0
- mcp_hangar/infrastructure/discovery/kubernetes_source.py +247 -0
- mcp_hangar/infrastructure/event_bus.py +260 -0
- mcp_hangar/infrastructure/event_sourced_repository.py +443 -0
- mcp_hangar/infrastructure/event_store.py +396 -0
- mcp_hangar/infrastructure/knowledge_base/__init__.py +259 -0
- mcp_hangar/infrastructure/knowledge_base/contracts.py +202 -0
- mcp_hangar/infrastructure/knowledge_base/memory.py +177 -0
- mcp_hangar/infrastructure/knowledge_base/postgres.py +545 -0
- mcp_hangar/infrastructure/knowledge_base/sqlite.py +513 -0
- mcp_hangar/infrastructure/metrics_publisher.py +36 -0
- mcp_hangar/infrastructure/observability/__init__.py +10 -0
- mcp_hangar/infrastructure/observability/langfuse_adapter.py +534 -0
- mcp_hangar/infrastructure/persistence/__init__.py +33 -0
- mcp_hangar/infrastructure/persistence/audit_repository.py +371 -0
- mcp_hangar/infrastructure/persistence/config_repository.py +398 -0
- mcp_hangar/infrastructure/persistence/database.py +333 -0
- mcp_hangar/infrastructure/persistence/database_common.py +330 -0
- mcp_hangar/infrastructure/persistence/event_serializer.py +280 -0
- mcp_hangar/infrastructure/persistence/event_upcaster.py +166 -0
- mcp_hangar/infrastructure/persistence/in_memory_event_store.py +150 -0
- mcp_hangar/infrastructure/persistence/recovery_service.py +312 -0
- mcp_hangar/infrastructure/persistence/sqlite_event_store.py +386 -0
- mcp_hangar/infrastructure/persistence/unit_of_work.py +409 -0
- mcp_hangar/infrastructure/persistence/upcasters/README.md +13 -0
- mcp_hangar/infrastructure/persistence/upcasters/__init__.py +7 -0
- mcp_hangar/infrastructure/query_bus.py +153 -0
- mcp_hangar/infrastructure/saga_manager.py +401 -0
- mcp_hangar/logging_config.py +209 -0
- mcp_hangar/metrics.py +1007 -0
- mcp_hangar/models.py +31 -0
- mcp_hangar/observability/__init__.py +54 -0
- mcp_hangar/observability/health.py +487 -0
- mcp_hangar/observability/metrics.py +319 -0
- mcp_hangar/observability/tracing.py +433 -0
- mcp_hangar/progress.py +542 -0
- mcp_hangar/retry.py +613 -0
- mcp_hangar/server/__init__.py +120 -0
- mcp_hangar/server/__main__.py +6 -0
- mcp_hangar/server/auth_bootstrap.py +340 -0
- mcp_hangar/server/auth_cli.py +335 -0
- mcp_hangar/server/auth_config.py +305 -0
- mcp_hangar/server/bootstrap.py +735 -0
- mcp_hangar/server/cli.py +161 -0
- mcp_hangar/server/config.py +224 -0
- mcp_hangar/server/context.py +215 -0
- mcp_hangar/server/http_auth_middleware.py +165 -0
- mcp_hangar/server/lifecycle.py +467 -0
- mcp_hangar/server/state.py +117 -0
- mcp_hangar/server/tools/__init__.py +16 -0
- mcp_hangar/server/tools/discovery.py +186 -0
- mcp_hangar/server/tools/groups.py +75 -0
- mcp_hangar/server/tools/health.py +301 -0
- mcp_hangar/server/tools/provider.py +939 -0
- mcp_hangar/server/tools/registry.py +320 -0
- mcp_hangar/server/validation.py +113 -0
- mcp_hangar/stdio_client.py +229 -0
- mcp_hangar-0.2.0.dist-info/METADATA +347 -0
- mcp_hangar-0.2.0.dist-info/RECORD +160 -0
- mcp_hangar-0.2.0.dist-info/WHEEL +4 -0
- mcp_hangar-0.2.0.dist-info/entry_points.txt +2 -0
- mcp_hangar-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sanitizer module for the MCP Registry.
|
|
3
|
+
|
|
4
|
+
Provides sanitization utilities for:
|
|
5
|
+
- Command arguments (prevent injection)
|
|
6
|
+
- Environment variables (prevent injection)
|
|
7
|
+
- Log messages (prevent log injection)
|
|
8
|
+
- File paths (prevent traversal)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import html
|
|
12
|
+
import re
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
import unicodedata
|
|
15
|
+
|
|
16
|
+
# Characters that could enable injection attacks
|
|
17
|
+
SHELL_METACHARACTERS = set(";&|`$(){}[]<>!#*?~\n\r\t\0\\'\"")
|
|
18
|
+
|
|
19
|
+
# Patterns for control characters and dangerous sequences
|
|
20
|
+
CONTROL_CHAR_PATTERN = re.compile(r"[\x00-\x1f\x7f-\x9f]")
|
|
21
|
+
NEWLINE_PATTERN = re.compile(r"[\r\n]")
|
|
22
|
+
NULL_BYTE_PATTERN = re.compile(r"\x00")
|
|
23
|
+
PATH_TRAVERSAL_PATTERN = re.compile(r"\.\.[\\/]")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Sanitizer:
|
|
27
|
+
"""
|
|
28
|
+
Comprehensive sanitizer for security-critical operations.
|
|
29
|
+
|
|
30
|
+
Provides methods to sanitize various types of inputs to prevent
|
|
31
|
+
injection attacks, log injection, and path traversal.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Configurable limits
|
|
35
|
+
MAX_ARGUMENT_LENGTH = 4096
|
|
36
|
+
MAX_PATH_LENGTH = 4096
|
|
37
|
+
MAX_LOG_MESSAGE_LENGTH = 10000
|
|
38
|
+
MAX_ENV_VALUE_LENGTH = 32768
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
max_argument_length: int = MAX_ARGUMENT_LENGTH,
|
|
43
|
+
max_path_length: int = MAX_PATH_LENGTH,
|
|
44
|
+
max_log_message_length: int = MAX_LOG_MESSAGE_LENGTH,
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Initialize sanitizer with configuration.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
max_argument_length: Maximum length for command arguments
|
|
51
|
+
max_path_length: Maximum length for file paths
|
|
52
|
+
max_log_message_length: Maximum length for log messages
|
|
53
|
+
"""
|
|
54
|
+
self.max_argument_length = max_argument_length
|
|
55
|
+
self.max_path_length = max_path_length
|
|
56
|
+
self.max_log_message_length = max_log_message_length
|
|
57
|
+
|
|
58
|
+
def sanitize_command_argument(
|
|
59
|
+
self,
|
|
60
|
+
argument: str,
|
|
61
|
+
allow_spaces: bool = True,
|
|
62
|
+
allow_quotes: bool = False,
|
|
63
|
+
replacement: str = "_",
|
|
64
|
+
) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Sanitize a command-line argument to prevent shell injection.
|
|
67
|
+
|
|
68
|
+
This method removes or replaces characters that could be interpreted
|
|
69
|
+
by the shell as metacharacters.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
argument: The argument to sanitize
|
|
73
|
+
allow_spaces: Whether to allow space characters
|
|
74
|
+
allow_quotes: Whether to allow quote characters
|
|
75
|
+
replacement: Character to replace dangerous chars with
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Sanitized argument string
|
|
79
|
+
|
|
80
|
+
Note:
|
|
81
|
+
This is a defense-in-depth measure. Always use subprocess with
|
|
82
|
+
shell=False and pass arguments as a list.
|
|
83
|
+
"""
|
|
84
|
+
if not isinstance(argument, str):
|
|
85
|
+
argument = str(argument)
|
|
86
|
+
|
|
87
|
+
# Truncate if too long
|
|
88
|
+
if len(argument) > self.max_argument_length:
|
|
89
|
+
argument = argument[: self.max_argument_length]
|
|
90
|
+
|
|
91
|
+
# Remove null bytes
|
|
92
|
+
argument = argument.replace("\0", "")
|
|
93
|
+
|
|
94
|
+
# Remove control characters
|
|
95
|
+
argument = CONTROL_CHAR_PATTERN.sub(replacement, argument)
|
|
96
|
+
|
|
97
|
+
# Build set of allowed metacharacters
|
|
98
|
+
dangerous = SHELL_METACHARACTERS.copy()
|
|
99
|
+
if allow_spaces:
|
|
100
|
+
dangerous.discard(" ")
|
|
101
|
+
if allow_quotes:
|
|
102
|
+
dangerous.discard('"')
|
|
103
|
+
dangerous.discard("'")
|
|
104
|
+
|
|
105
|
+
# Replace dangerous characters
|
|
106
|
+
result = []
|
|
107
|
+
for char in argument:
|
|
108
|
+
if char in dangerous:
|
|
109
|
+
result.append(replacement)
|
|
110
|
+
else:
|
|
111
|
+
result.append(char)
|
|
112
|
+
|
|
113
|
+
return "".join(result)
|
|
114
|
+
|
|
115
|
+
def sanitize_command_list(
|
|
116
|
+
self,
|
|
117
|
+
command: List[str],
|
|
118
|
+
allow_spaces: bool = True,
|
|
119
|
+
) -> List[str]:
|
|
120
|
+
"""
|
|
121
|
+
Sanitize an entire command list.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
command: List of command arguments
|
|
125
|
+
allow_spaces: Whether to allow spaces in arguments
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List of sanitized arguments
|
|
129
|
+
"""
|
|
130
|
+
return [self.sanitize_command_argument(arg, allow_spaces=allow_spaces) for arg in command]
|
|
131
|
+
|
|
132
|
+
def sanitize_environment_value(
|
|
133
|
+
self,
|
|
134
|
+
value: str,
|
|
135
|
+
allow_newlines: bool = False,
|
|
136
|
+
) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Sanitize an environment variable value.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
value: The value to sanitize
|
|
142
|
+
allow_newlines: Whether to allow newline characters
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Sanitized value string
|
|
146
|
+
"""
|
|
147
|
+
if not isinstance(value, str):
|
|
148
|
+
value = str(value)
|
|
149
|
+
|
|
150
|
+
# Truncate if too long
|
|
151
|
+
if len(value) > self.MAX_ENV_VALUE_LENGTH:
|
|
152
|
+
value = value[: self.MAX_ENV_VALUE_LENGTH]
|
|
153
|
+
|
|
154
|
+
# Remove null bytes (always dangerous)
|
|
155
|
+
value = value.replace("\0", "")
|
|
156
|
+
|
|
157
|
+
# Optionally remove newlines
|
|
158
|
+
if not allow_newlines:
|
|
159
|
+
value = NEWLINE_PATTERN.sub(" ", value)
|
|
160
|
+
|
|
161
|
+
return value
|
|
162
|
+
|
|
163
|
+
def sanitize_environment_dict(
|
|
164
|
+
self,
|
|
165
|
+
env: Dict[str, str],
|
|
166
|
+
allow_newlines: bool = False,
|
|
167
|
+
) -> Dict[str, str]:
|
|
168
|
+
"""
|
|
169
|
+
Sanitize all environment variable values in a dictionary.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
env: Dictionary of environment variables
|
|
173
|
+
allow_newlines: Whether to allow newlines in values
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Dictionary with sanitized values
|
|
177
|
+
"""
|
|
178
|
+
return {key: self.sanitize_environment_value(value, allow_newlines) for key, value in env.items()}
|
|
179
|
+
|
|
180
|
+
def sanitize_path(
|
|
181
|
+
self,
|
|
182
|
+
path: str,
|
|
183
|
+
allow_absolute: bool = False,
|
|
184
|
+
allow_hidden: bool = True,
|
|
185
|
+
) -> str:
|
|
186
|
+
"""
|
|
187
|
+
Sanitize a file path to prevent path traversal attacks.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
path: The path to sanitize
|
|
191
|
+
allow_absolute: Whether to allow absolute paths
|
|
192
|
+
allow_hidden: Whether to allow hidden files (starting with .)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Sanitized path string
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
ValueError: If the path is invalid or dangerous
|
|
199
|
+
"""
|
|
200
|
+
if not isinstance(path, str):
|
|
201
|
+
path = str(path)
|
|
202
|
+
|
|
203
|
+
# Truncate if too long
|
|
204
|
+
if len(path) > self.max_path_length:
|
|
205
|
+
raise ValueError(f"Path exceeds maximum length ({self.max_path_length})")
|
|
206
|
+
|
|
207
|
+
# Remove null bytes
|
|
208
|
+
path = path.replace("\0", "")
|
|
209
|
+
|
|
210
|
+
# Normalize unicode to detect obfuscation
|
|
211
|
+
path = unicodedata.normalize("NFKC", path)
|
|
212
|
+
|
|
213
|
+
# Check for path traversal
|
|
214
|
+
if PATH_TRAVERSAL_PATTERN.search(path) or ".." in path.split("/"):
|
|
215
|
+
raise ValueError("Path contains traversal sequences")
|
|
216
|
+
|
|
217
|
+
# Check for absolute paths
|
|
218
|
+
if not allow_absolute:
|
|
219
|
+
if path.startswith("/") or (len(path) > 1 and path[1] == ":"):
|
|
220
|
+
raise ValueError("Absolute paths are not allowed")
|
|
221
|
+
|
|
222
|
+
# Check for hidden files
|
|
223
|
+
if not allow_hidden:
|
|
224
|
+
parts = path.replace("\\", "/").split("/")
|
|
225
|
+
for part in parts:
|
|
226
|
+
if part.startswith(".") and part not in (".", ".."):
|
|
227
|
+
raise ValueError("Hidden files/directories are not allowed")
|
|
228
|
+
|
|
229
|
+
# Check for control characters
|
|
230
|
+
if CONTROL_CHAR_PATTERN.search(path):
|
|
231
|
+
raise ValueError("Path contains control characters")
|
|
232
|
+
|
|
233
|
+
return path
|
|
234
|
+
|
|
235
|
+
def sanitize_log_message(
|
|
236
|
+
self,
|
|
237
|
+
message: str,
|
|
238
|
+
max_length: Optional[int] = None,
|
|
239
|
+
) -> str:
|
|
240
|
+
"""
|
|
241
|
+
Sanitize a message for safe logging.
|
|
242
|
+
|
|
243
|
+
Prevents log injection attacks by:
|
|
244
|
+
- Escaping newlines
|
|
245
|
+
- Escaping control characters
|
|
246
|
+
- Truncating long messages
|
|
247
|
+
- Normalizing unicode
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
message: The message to sanitize
|
|
251
|
+
max_length: Optional override for max length
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Sanitized message string
|
|
255
|
+
"""
|
|
256
|
+
if not isinstance(message, str):
|
|
257
|
+
message = str(message)
|
|
258
|
+
|
|
259
|
+
max_len = max_length or self.max_log_message_length
|
|
260
|
+
|
|
261
|
+
# Truncate if too long
|
|
262
|
+
if len(message) > max_len:
|
|
263
|
+
message = message[:max_len] + "...[truncated]"
|
|
264
|
+
|
|
265
|
+
# Normalize unicode
|
|
266
|
+
message = unicodedata.normalize("NFKC", message)
|
|
267
|
+
|
|
268
|
+
# Replace newlines with visible escape sequences
|
|
269
|
+
message = message.replace("\r\n", "\\r\\n")
|
|
270
|
+
message = message.replace("\r", "\\r")
|
|
271
|
+
message = message.replace("\n", "\\n")
|
|
272
|
+
|
|
273
|
+
# Replace other control characters with their escape representation
|
|
274
|
+
def escape_control(match):
|
|
275
|
+
char = match.group(0)
|
|
276
|
+
if char == "\t":
|
|
277
|
+
return "\\t"
|
|
278
|
+
return f"\\x{ord(char):02x}"
|
|
279
|
+
|
|
280
|
+
message = CONTROL_CHAR_PATTERN.sub(escape_control, message)
|
|
281
|
+
|
|
282
|
+
return message
|
|
283
|
+
|
|
284
|
+
def sanitize_for_json(self, value: Any) -> Any:
|
|
285
|
+
"""
|
|
286
|
+
Sanitize a value for safe JSON serialization.
|
|
287
|
+
|
|
288
|
+
Handles nested structures recursively.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
value: The value to sanitize
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Sanitized value safe for JSON
|
|
295
|
+
"""
|
|
296
|
+
if isinstance(value, str):
|
|
297
|
+
# Remove null bytes and control chars except common whitespace
|
|
298
|
+
result = value.replace("\0", "")
|
|
299
|
+
# Keep tabs and newlines but remove other control chars
|
|
300
|
+
result = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]", "", result)
|
|
301
|
+
return result
|
|
302
|
+
elif isinstance(value, dict):
|
|
303
|
+
return {self.sanitize_for_json(k): self.sanitize_for_json(v) for k, v in value.items()}
|
|
304
|
+
elif isinstance(value, list):
|
|
305
|
+
return [self.sanitize_for_json(item) for item in value]
|
|
306
|
+
elif isinstance(value, (int, float, bool, type(None))):
|
|
307
|
+
return value
|
|
308
|
+
else:
|
|
309
|
+
# Convert unknown types to string and sanitize
|
|
310
|
+
return self.sanitize_for_json(str(value))
|
|
311
|
+
|
|
312
|
+
def escape_html(self, value: str) -> str:
|
|
313
|
+
"""
|
|
314
|
+
Escape HTML special characters.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
value: The string to escape
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
HTML-escaped string
|
|
321
|
+
"""
|
|
322
|
+
return html.escape(value, quote=True)
|
|
323
|
+
|
|
324
|
+
def mask_value(
|
|
325
|
+
self,
|
|
326
|
+
value: str,
|
|
327
|
+
visible_chars: int = 4,
|
|
328
|
+
mask_char: str = "*",
|
|
329
|
+
) -> str:
|
|
330
|
+
"""
|
|
331
|
+
Mask a sensitive value, showing only first few characters.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
value: The value to mask
|
|
335
|
+
visible_chars: Number of characters to show at the start
|
|
336
|
+
mask_char: Character to use for masking
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Masked string
|
|
340
|
+
"""
|
|
341
|
+
if not value:
|
|
342
|
+
return ""
|
|
343
|
+
|
|
344
|
+
if len(value) <= visible_chars:
|
|
345
|
+
return mask_char * len(value)
|
|
346
|
+
|
|
347
|
+
return value[:visible_chars] + mask_char * (len(value) - visible_chars)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# --- Convenience Functions ---
|
|
351
|
+
|
|
352
|
+
# Global sanitizer instance with default settings
|
|
353
|
+
_default_sanitizer = Sanitizer()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def sanitize_command_argument(
|
|
357
|
+
argument: str,
|
|
358
|
+
allow_spaces: bool = True,
|
|
359
|
+
allow_quotes: bool = False,
|
|
360
|
+
) -> str:
|
|
361
|
+
"""Sanitize a command argument using default sanitizer."""
|
|
362
|
+
return _default_sanitizer.sanitize_command_argument(argument, allow_spaces, allow_quotes)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def sanitize_environment_value(
|
|
366
|
+
value: str,
|
|
367
|
+
allow_newlines: bool = False,
|
|
368
|
+
) -> str:
|
|
369
|
+
"""Sanitize an environment value using default sanitizer."""
|
|
370
|
+
return _default_sanitizer.sanitize_environment_value(value, allow_newlines)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def sanitize_log_message(
|
|
374
|
+
message: str,
|
|
375
|
+
max_length: Optional[int] = None,
|
|
376
|
+
) -> str:
|
|
377
|
+
"""Sanitize a log message using default sanitizer."""
|
|
378
|
+
return _default_sanitizer.sanitize_log_message(message, max_length)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def sanitize_path(
|
|
382
|
+
path: str,
|
|
383
|
+
allow_absolute: bool = False,
|
|
384
|
+
allow_hidden: bool = True,
|
|
385
|
+
) -> str:
|
|
386
|
+
"""Sanitize a file path using default sanitizer."""
|
|
387
|
+
return _default_sanitizer.sanitize_path(path, allow_absolute, allow_hidden)
|