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,501 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secrets management for the MCP Registry.
|
|
3
|
+
|
|
4
|
+
Provides secure handling of sensitive data:
|
|
5
|
+
- Masking sensitive values in logs
|
|
6
|
+
- Secure environment variable handling
|
|
7
|
+
- Detection of sensitive keys
|
|
8
|
+
- Redaction utilities
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from typing import Any, Dict, FrozenSet, List, Optional, Pattern, Set
|
|
15
|
+
|
|
16
|
+
# Patterns that indicate a key might contain sensitive data
|
|
17
|
+
SENSITIVE_KEY_PATTERNS: List[Pattern] = [
|
|
18
|
+
re.compile(r"(?i)password"),
|
|
19
|
+
re.compile(r"(?i)passwd"),
|
|
20
|
+
re.compile(r"(?i)secret"),
|
|
21
|
+
re.compile(r"(?i)api[_-]?key"),
|
|
22
|
+
re.compile(r"(?i)apikey"),
|
|
23
|
+
re.compile(r"(?i)auth[_-]?token"),
|
|
24
|
+
re.compile(r"(?i)access[_-]?token"),
|
|
25
|
+
re.compile(r"(?i)bearer"),
|
|
26
|
+
re.compile(r"(?i)credential"),
|
|
27
|
+
re.compile(r"(?i)private[_-]?key"),
|
|
28
|
+
re.compile(r"(?i)ssh[_-]?key"),
|
|
29
|
+
re.compile(r"(?i)encryption[_-]?key"),
|
|
30
|
+
re.compile(r"(?i)signing[_-]?key"),
|
|
31
|
+
re.compile(r"(?i)client[_-]?secret"),
|
|
32
|
+
re.compile(r"(?i)db[_-]?pass"),
|
|
33
|
+
re.compile(r"(?i)database[_-]?password"),
|
|
34
|
+
re.compile(r"(?i)connection[_-]?string"),
|
|
35
|
+
re.compile(r"(?i)conn[_-]?str"),
|
|
36
|
+
re.compile(r"(?i)jwt"),
|
|
37
|
+
re.compile(r"(?i)session[_-]?id"),
|
|
38
|
+
re.compile(r"(?i)cookie"),
|
|
39
|
+
re.compile(r"(?i)oauth"),
|
|
40
|
+
re.compile(r"(?i)_token$"),
|
|
41
|
+
re.compile(r"(?i)_key$"),
|
|
42
|
+
re.compile(r"(?i)_secret$"),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# Exact key names that are always considered sensitive
|
|
46
|
+
SENSITIVE_KEYS: FrozenSet[str] = frozenset(
|
|
47
|
+
[
|
|
48
|
+
"PASSWORD",
|
|
49
|
+
"PASSWD",
|
|
50
|
+
"SECRET",
|
|
51
|
+
"API_KEY",
|
|
52
|
+
"APIKEY",
|
|
53
|
+
"AUTH_TOKEN",
|
|
54
|
+
"ACCESS_TOKEN",
|
|
55
|
+
"REFRESH_TOKEN",
|
|
56
|
+
"BEARER_TOKEN",
|
|
57
|
+
"PRIVATE_KEY",
|
|
58
|
+
"SSH_KEY",
|
|
59
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
60
|
+
"AWS_SESSION_TOKEN",
|
|
61
|
+
"AZURE_CLIENT_SECRET",
|
|
62
|
+
"GCP_PRIVATE_KEY",
|
|
63
|
+
"GITHUB_TOKEN",
|
|
64
|
+
"GITLAB_TOKEN",
|
|
65
|
+
"NPM_TOKEN",
|
|
66
|
+
"PYPI_TOKEN",
|
|
67
|
+
"DATABASE_URL",
|
|
68
|
+
"REDIS_URL",
|
|
69
|
+
"MONGODB_URI",
|
|
70
|
+
"POSTGRES_PASSWORD",
|
|
71
|
+
"MYSQL_PASSWORD",
|
|
72
|
+
"JWT_SECRET",
|
|
73
|
+
"ENCRYPTION_KEY",
|
|
74
|
+
"SIGNING_KEY",
|
|
75
|
+
"MASTER_KEY",
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_sensitive_key(key: str) -> bool:
|
|
81
|
+
"""
|
|
82
|
+
Check if a key name likely contains sensitive data.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
key: The key name to check
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
True if the key is likely sensitive
|
|
89
|
+
"""
|
|
90
|
+
if not key:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
# Check exact matches (case-insensitive)
|
|
94
|
+
if key.upper() in SENSITIVE_KEYS:
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
# Check patterns
|
|
98
|
+
for pattern in SENSITIVE_KEY_PATTERNS:
|
|
99
|
+
if pattern.search(key):
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def mask_sensitive_value(
|
|
106
|
+
value: str,
|
|
107
|
+
visible_prefix: int = 4,
|
|
108
|
+
visible_suffix: int = 0,
|
|
109
|
+
mask_char: str = "*",
|
|
110
|
+
min_mask_length: int = 8,
|
|
111
|
+
max_visible: int = 8,
|
|
112
|
+
) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Mask a sensitive value, optionally showing prefix/suffix.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
value: The value to mask
|
|
118
|
+
visible_prefix: Number of characters to show at the start
|
|
119
|
+
visible_suffix: Number of characters to show at the end
|
|
120
|
+
mask_char: Character to use for masking
|
|
121
|
+
min_mask_length: Minimum number of mask characters to show
|
|
122
|
+
max_visible: Maximum total visible characters
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Masked string
|
|
126
|
+
"""
|
|
127
|
+
if not value:
|
|
128
|
+
return ""
|
|
129
|
+
|
|
130
|
+
# For very short values, mask everything
|
|
131
|
+
if len(value) <= min_mask_length:
|
|
132
|
+
return mask_char * len(value)
|
|
133
|
+
|
|
134
|
+
# Limit visible characters
|
|
135
|
+
total_visible = visible_prefix + visible_suffix
|
|
136
|
+
if total_visible > max_visible:
|
|
137
|
+
# Reduce proportionally
|
|
138
|
+
ratio = max_visible / total_visible
|
|
139
|
+
visible_prefix = int(visible_prefix * ratio)
|
|
140
|
+
visible_suffix = int(visible_suffix * ratio)
|
|
141
|
+
total_visible = visible_prefix + visible_suffix
|
|
142
|
+
|
|
143
|
+
# Ensure we have enough characters to mask
|
|
144
|
+
if len(value) <= total_visible + min_mask_length:
|
|
145
|
+
# Show less prefix
|
|
146
|
+
visible_prefix = max(0, len(value) - min_mask_length - visible_suffix)
|
|
147
|
+
|
|
148
|
+
# Calculate mask length
|
|
149
|
+
mask_length = max(min_mask_length, len(value) - visible_prefix - visible_suffix)
|
|
150
|
+
|
|
151
|
+
# Build the masked string
|
|
152
|
+
parts = []
|
|
153
|
+
if visible_prefix > 0:
|
|
154
|
+
parts.append(value[:visible_prefix])
|
|
155
|
+
parts.append(mask_char * mask_length)
|
|
156
|
+
if visible_suffix > 0 and len(value) > visible_prefix + visible_suffix:
|
|
157
|
+
parts.append(value[-visible_suffix:])
|
|
158
|
+
|
|
159
|
+
return "".join(parts)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class SecretsMask:
|
|
164
|
+
"""
|
|
165
|
+
Configuration for masking secrets in various contexts.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
# Characters to show at start of masked values
|
|
169
|
+
visible_prefix: int = 4
|
|
170
|
+
|
|
171
|
+
# Characters to show at end of masked values
|
|
172
|
+
visible_suffix: int = 0
|
|
173
|
+
|
|
174
|
+
# Character used for masking
|
|
175
|
+
mask_char: str = "*"
|
|
176
|
+
|
|
177
|
+
# Minimum mask length
|
|
178
|
+
min_mask_length: int = 8
|
|
179
|
+
|
|
180
|
+
# Additional patterns to consider sensitive
|
|
181
|
+
additional_patterns: List[Pattern] = field(default_factory=list)
|
|
182
|
+
|
|
183
|
+
# Additional exact keys to consider sensitive
|
|
184
|
+
additional_keys: Set[str] = field(default_factory=set)
|
|
185
|
+
|
|
186
|
+
# Keys to never mask (override)
|
|
187
|
+
safe_keys: Set[str] = field(default_factory=set)
|
|
188
|
+
|
|
189
|
+
def is_sensitive(self, key: str) -> bool:
|
|
190
|
+
"""Check if a key is sensitive according to this configuration."""
|
|
191
|
+
if key in self.safe_keys:
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
if key.upper() in self.additional_keys:
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
for pattern in self.additional_patterns:
|
|
198
|
+
if pattern.search(key):
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
return is_sensitive_key(key)
|
|
202
|
+
|
|
203
|
+
def mask(self, value: str) -> str:
|
|
204
|
+
"""Mask a value according to this configuration."""
|
|
205
|
+
return mask_sensitive_value(
|
|
206
|
+
value,
|
|
207
|
+
visible_prefix=self.visible_prefix,
|
|
208
|
+
visible_suffix=self.visible_suffix,
|
|
209
|
+
mask_char=self.mask_char,
|
|
210
|
+
min_mask_length=self.min_mask_length,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def mask_dict(
|
|
214
|
+
self,
|
|
215
|
+
data: Dict[str, Any],
|
|
216
|
+
recursive: bool = True,
|
|
217
|
+
) -> Dict[str, Any]:
|
|
218
|
+
"""
|
|
219
|
+
Mask sensitive values in a dictionary.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
data: Dictionary to mask
|
|
223
|
+
recursive: Whether to process nested dictionaries
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
New dictionary with sensitive values masked
|
|
227
|
+
"""
|
|
228
|
+
result = {}
|
|
229
|
+
for key, value in data.items():
|
|
230
|
+
if isinstance(value, dict) and recursive:
|
|
231
|
+
result[key] = self.mask_dict(value, recursive=True)
|
|
232
|
+
elif isinstance(value, str) and self.is_sensitive(key):
|
|
233
|
+
result[key] = self.mask(value)
|
|
234
|
+
else:
|
|
235
|
+
result[key] = value
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class SecureEnvironment:
|
|
240
|
+
"""
|
|
241
|
+
Secure wrapper for environment variable handling.
|
|
242
|
+
|
|
243
|
+
Provides safe access to environment variables with automatic
|
|
244
|
+
masking of sensitive values in logs and error messages.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
def __init__(
|
|
248
|
+
self,
|
|
249
|
+
env: Optional[Dict[str, str]] = None,
|
|
250
|
+
mask_config: Optional[SecretsMask] = None,
|
|
251
|
+
):
|
|
252
|
+
"""
|
|
253
|
+
Initialize secure environment.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
env: Dictionary of environment variables (defaults to os.environ)
|
|
257
|
+
mask_config: Configuration for masking (uses defaults if not provided)
|
|
258
|
+
"""
|
|
259
|
+
self._env = dict(env) if env is not None else dict(os.environ)
|
|
260
|
+
self._mask = mask_config or SecretsMask()
|
|
261
|
+
self._accessed_keys: Set[str] = set()
|
|
262
|
+
|
|
263
|
+
def get(
|
|
264
|
+
self,
|
|
265
|
+
key: str,
|
|
266
|
+
default: Optional[str] = None,
|
|
267
|
+
required: bool = False,
|
|
268
|
+
) -> Optional[str]:
|
|
269
|
+
"""
|
|
270
|
+
Get an environment variable value.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
key: Environment variable name
|
|
274
|
+
default: Default value if not found
|
|
275
|
+
required: If True, raise error if not found
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
The value or default
|
|
279
|
+
|
|
280
|
+
Raises:
|
|
281
|
+
KeyError: If required and not found
|
|
282
|
+
"""
|
|
283
|
+
self._accessed_keys.add(key)
|
|
284
|
+
|
|
285
|
+
if key in self._env:
|
|
286
|
+
return self._env[key]
|
|
287
|
+
|
|
288
|
+
if required:
|
|
289
|
+
raise KeyError(f"Required environment variable not set: {key}")
|
|
290
|
+
|
|
291
|
+
return default
|
|
292
|
+
|
|
293
|
+
def get_masked(
|
|
294
|
+
self,
|
|
295
|
+
key: str,
|
|
296
|
+
default: Optional[str] = None,
|
|
297
|
+
) -> Optional[str]:
|
|
298
|
+
"""
|
|
299
|
+
Get an environment variable value, masked if sensitive.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
key: Environment variable name
|
|
303
|
+
default: Default value if not found
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
The value (masked if sensitive) or default
|
|
307
|
+
"""
|
|
308
|
+
value = self.get(key, default)
|
|
309
|
+
if value is None:
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
if self._mask.is_sensitive(key):
|
|
313
|
+
return self._mask.mask(value)
|
|
314
|
+
|
|
315
|
+
return value
|
|
316
|
+
|
|
317
|
+
def set(self, key: str, value: str) -> None:
|
|
318
|
+
"""
|
|
319
|
+
Set an environment variable.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
key: Environment variable name
|
|
323
|
+
value: Value to set
|
|
324
|
+
"""
|
|
325
|
+
self._env[key] = value
|
|
326
|
+
|
|
327
|
+
def unset(self, key: str) -> None:
|
|
328
|
+
"""
|
|
329
|
+
Remove an environment variable.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
key: Environment variable name
|
|
333
|
+
"""
|
|
334
|
+
self._env.pop(key, None)
|
|
335
|
+
|
|
336
|
+
def to_dict(self, mask_sensitive: bool = True) -> Dict[str, str]:
|
|
337
|
+
"""
|
|
338
|
+
Export environment as dictionary.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
mask_sensitive: Whether to mask sensitive values
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Dictionary of environment variables
|
|
345
|
+
"""
|
|
346
|
+
if not mask_sensitive:
|
|
347
|
+
return dict(self._env)
|
|
348
|
+
|
|
349
|
+
return self._mask.mask_dict(self._env, recursive=False)
|
|
350
|
+
|
|
351
|
+
def to_subprocess_env(
|
|
352
|
+
self,
|
|
353
|
+
include_parent: bool = True,
|
|
354
|
+
whitelist: Optional[Set[str]] = None,
|
|
355
|
+
blacklist: Optional[Set[str]] = None,
|
|
356
|
+
) -> Dict[str, str]:
|
|
357
|
+
"""
|
|
358
|
+
Prepare environment for subprocess execution.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
include_parent: Whether to include parent process environment
|
|
362
|
+
whitelist: If set, only include these keys
|
|
363
|
+
blacklist: Keys to exclude
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Dictionary suitable for subprocess env parameter
|
|
367
|
+
"""
|
|
368
|
+
if include_parent:
|
|
369
|
+
result = dict(os.environ)
|
|
370
|
+
result.update(self._env)
|
|
371
|
+
else:
|
|
372
|
+
result = dict(self._env)
|
|
373
|
+
|
|
374
|
+
# Apply whitelist
|
|
375
|
+
if whitelist is not None:
|
|
376
|
+
result = {k: v for k, v in result.items() if k in whitelist}
|
|
377
|
+
|
|
378
|
+
# Apply blacklist
|
|
379
|
+
if blacklist is not None:
|
|
380
|
+
result = {k: v for k, v in result.items() if k not in blacklist}
|
|
381
|
+
|
|
382
|
+
return result
|
|
383
|
+
|
|
384
|
+
def validate(self, required_keys: List[str]) -> List[str]:
|
|
385
|
+
"""
|
|
386
|
+
Validate that required environment variables are set.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
required_keys: List of required key names
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
List of missing key names (empty if all present)
|
|
393
|
+
"""
|
|
394
|
+
missing = []
|
|
395
|
+
for key in required_keys:
|
|
396
|
+
if key not in self._env or not self._env[key]:
|
|
397
|
+
missing.append(key)
|
|
398
|
+
return missing
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def accessed_keys(self) -> Set[str]:
|
|
402
|
+
"""Get the set of keys that have been accessed."""
|
|
403
|
+
return self._accessed_keys.copy()
|
|
404
|
+
|
|
405
|
+
def __contains__(self, key: str) -> bool:
|
|
406
|
+
"""Check if a key exists."""
|
|
407
|
+
return key in self._env
|
|
408
|
+
|
|
409
|
+
def __repr__(self) -> str:
|
|
410
|
+
"""Safe representation that doesn't expose secrets."""
|
|
411
|
+
keys = sorted(self._env.keys())
|
|
412
|
+
return f"SecureEnvironment({len(keys)} variables)"
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# --- Utility Functions ---
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def redact_secrets_in_string(
|
|
419
|
+
text: str,
|
|
420
|
+
patterns: Optional[List[Pattern]] = None,
|
|
421
|
+
replacement: str = "[REDACTED]",
|
|
422
|
+
) -> str:
|
|
423
|
+
"""
|
|
424
|
+
Redact potential secrets in a string.
|
|
425
|
+
|
|
426
|
+
Useful for sanitizing log messages or error output.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
text: The text to redact
|
|
430
|
+
patterns: Custom patterns to match (uses defaults if not provided)
|
|
431
|
+
replacement: Replacement text for redacted values
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Text with secrets redacted
|
|
435
|
+
"""
|
|
436
|
+
# Common patterns that might contain secrets in text
|
|
437
|
+
default_patterns = [
|
|
438
|
+
# API keys (various formats)
|
|
439
|
+
re.compile(r'(?i)(api[_-]?key|apikey)["\s:=]+["\']?([a-zA-Z0-9_\-]{16,})'),
|
|
440
|
+
# Bearer tokens
|
|
441
|
+
re.compile(r"(?i)bearer\s+([a-zA-Z0-9_\-\.]+)"),
|
|
442
|
+
# Basic auth in URLs
|
|
443
|
+
re.compile(r"://([^:]+):([^@]+)@"),
|
|
444
|
+
# Password in query strings
|
|
445
|
+
re.compile(r"(?i)password=([^&\s]+)"),
|
|
446
|
+
# Secret in query strings
|
|
447
|
+
re.compile(r"(?i)secret=([^&\s]+)"),
|
|
448
|
+
# Token in query strings
|
|
449
|
+
re.compile(r"(?i)token=([^&\s]+)"),
|
|
450
|
+
# AWS keys
|
|
451
|
+
re.compile(r"(?i)(AKIA[A-Z0-9]{16})"),
|
|
452
|
+
# Private keys
|
|
453
|
+
re.compile(r"-----BEGIN[A-Z\s]+PRIVATE KEY-----"),
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
patterns_to_use = patterns or default_patterns
|
|
457
|
+
result = text
|
|
458
|
+
|
|
459
|
+
for pattern in patterns_to_use:
|
|
460
|
+
result = pattern.sub(replacement, result)
|
|
461
|
+
|
|
462
|
+
return result
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def create_secure_env_for_provider(
|
|
466
|
+
base_env: Optional[Dict[str, str]] = None,
|
|
467
|
+
provider_env: Optional[Dict[str, str]] = None,
|
|
468
|
+
inherit_parent: bool = True,
|
|
469
|
+
sensitive_key_filter: bool = True,
|
|
470
|
+
) -> SecureEnvironment:
|
|
471
|
+
"""
|
|
472
|
+
Create a secure environment for provider execution.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
base_env: Base environment variables
|
|
476
|
+
provider_env: Provider-specific environment variables
|
|
477
|
+
inherit_parent: Whether to inherit from parent process
|
|
478
|
+
sensitive_key_filter: Whether to filter out inherited sensitive keys
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
SecureEnvironment configured for provider use
|
|
482
|
+
"""
|
|
483
|
+
env_dict: Dict[str, str] = {}
|
|
484
|
+
|
|
485
|
+
# Start with parent environment if requested
|
|
486
|
+
if inherit_parent:
|
|
487
|
+
# Filter sensitive keys from parent env if requested
|
|
488
|
+
for key, value in os.environ.items():
|
|
489
|
+
if sensitive_key_filter and is_sensitive_key(key):
|
|
490
|
+
continue
|
|
491
|
+
env_dict[key] = value
|
|
492
|
+
|
|
493
|
+
# Add base environment (overrides parent)
|
|
494
|
+
if base_env:
|
|
495
|
+
env_dict.update(base_env)
|
|
496
|
+
|
|
497
|
+
# Add provider-specific environment (overrides all)
|
|
498
|
+
if provider_env:
|
|
499
|
+
env_dict.update(provider_env)
|
|
500
|
+
|
|
501
|
+
return SecureEnvironment(env_dict)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Domain services - interfaces for infrastructure operations."""
|
|
2
|
+
|
|
3
|
+
# Re-export exception from canonical location for convenience
|
|
4
|
+
from ..exceptions import ProviderStartError
|
|
5
|
+
from .audit_service import AuditService
|
|
6
|
+
from .image_builder import BuildConfig, get_image_builder, ImageBuilder
|
|
7
|
+
from .provider_launcher import ContainerConfig, ContainerLauncher, DockerLauncher, ProviderLauncher, SubprocessLauncher
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AuditService",
|
|
11
|
+
"ProviderLauncher",
|
|
12
|
+
"SubprocessLauncher",
|
|
13
|
+
"DockerLauncher",
|
|
14
|
+
"ContainerLauncher",
|
|
15
|
+
"ContainerConfig",
|
|
16
|
+
"ImageBuilder",
|
|
17
|
+
"BuildConfig",
|
|
18
|
+
"get_image_builder",
|
|
19
|
+
"ProviderStartError",
|
|
20
|
+
]
|