crackerjack 0.31.10__py3-none-any.whl → 0.31.12__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.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +288 -705
- crackerjack/__main__.py +22 -8
- crackerjack/agents/__init__.py +0 -3
- crackerjack/agents/architect_agent.py +0 -43
- crackerjack/agents/base.py +1 -9
- crackerjack/agents/coordinator.py +2 -148
- crackerjack/agents/documentation_agent.py +109 -81
- crackerjack/agents/dry_agent.py +122 -97
- crackerjack/agents/formatting_agent.py +3 -16
- crackerjack/agents/import_optimization_agent.py +1174 -130
- crackerjack/agents/performance_agent.py +956 -188
- crackerjack/agents/performance_helpers.py +229 -0
- crackerjack/agents/proactive_agent.py +1 -48
- crackerjack/agents/refactoring_agent.py +516 -246
- crackerjack/agents/refactoring_helpers.py +282 -0
- crackerjack/agents/security_agent.py +393 -90
- crackerjack/agents/test_creation_agent.py +1776 -120
- crackerjack/agents/test_specialist_agent.py +59 -15
- crackerjack/agents/tracker.py +0 -102
- crackerjack/api.py +145 -37
- crackerjack/cli/handlers.py +48 -30
- crackerjack/cli/interactive.py +11 -11
- crackerjack/cli/options.py +66 -4
- crackerjack/code_cleaner.py +808 -148
- crackerjack/config/global_lock_config.py +110 -0
- crackerjack/config/hooks.py +43 -64
- crackerjack/core/async_workflow_orchestrator.py +247 -97
- crackerjack/core/autofix_coordinator.py +192 -109
- crackerjack/core/enhanced_container.py +46 -63
- crackerjack/core/file_lifecycle.py +549 -0
- crackerjack/core/performance.py +9 -8
- crackerjack/core/performance_monitor.py +395 -0
- crackerjack/core/phase_coordinator.py +281 -94
- crackerjack/core/proactive_workflow.py +9 -58
- crackerjack/core/resource_manager.py +501 -0
- crackerjack/core/service_watchdog.py +490 -0
- crackerjack/core/session_coordinator.py +4 -8
- crackerjack/core/timeout_manager.py +504 -0
- crackerjack/core/websocket_lifecycle.py +475 -0
- crackerjack/core/workflow_orchestrator.py +343 -209
- crackerjack/dynamic_config.py +47 -6
- crackerjack/errors.py +3 -4
- crackerjack/executors/async_hook_executor.py +63 -13
- crackerjack/executors/cached_hook_executor.py +14 -14
- crackerjack/executors/hook_executor.py +100 -37
- crackerjack/executors/hook_lock_manager.py +856 -0
- crackerjack/executors/individual_hook_executor.py +120 -86
- crackerjack/intelligence/__init__.py +0 -7
- crackerjack/intelligence/adaptive_learning.py +13 -86
- crackerjack/intelligence/agent_orchestrator.py +15 -78
- crackerjack/intelligence/agent_registry.py +12 -59
- crackerjack/intelligence/agent_selector.py +31 -92
- crackerjack/intelligence/integration.py +1 -41
- crackerjack/interactive.py +9 -9
- crackerjack/managers/async_hook_manager.py +25 -8
- crackerjack/managers/hook_manager.py +9 -9
- crackerjack/managers/publish_manager.py +57 -59
- crackerjack/managers/test_command_builder.py +6 -36
- crackerjack/managers/test_executor.py +9 -61
- crackerjack/managers/test_manager.py +17 -63
- crackerjack/managers/test_manager_backup.py +77 -127
- crackerjack/managers/test_progress.py +4 -23
- crackerjack/mcp/cache.py +5 -12
- crackerjack/mcp/client_runner.py +10 -10
- crackerjack/mcp/context.py +64 -6
- crackerjack/mcp/dashboard.py +14 -11
- crackerjack/mcp/enhanced_progress_monitor.py +55 -55
- crackerjack/mcp/file_monitor.py +72 -42
- crackerjack/mcp/progress_components.py +103 -84
- crackerjack/mcp/progress_monitor.py +122 -49
- crackerjack/mcp/rate_limiter.py +12 -12
- crackerjack/mcp/server_core.py +16 -22
- crackerjack/mcp/service_watchdog.py +26 -26
- crackerjack/mcp/state.py +15 -0
- crackerjack/mcp/tools/core_tools.py +95 -39
- crackerjack/mcp/tools/error_analyzer.py +6 -32
- crackerjack/mcp/tools/execution_tools.py +1 -56
- crackerjack/mcp/tools/execution_tools_backup.py +35 -131
- crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
- crackerjack/mcp/tools/intelligence_tools.py +2 -55
- crackerjack/mcp/tools/monitoring_tools.py +308 -145
- crackerjack/mcp/tools/proactive_tools.py +12 -42
- crackerjack/mcp/tools/progress_tools.py +23 -15
- crackerjack/mcp/tools/utility_tools.py +3 -40
- crackerjack/mcp/tools/workflow_executor.py +40 -60
- crackerjack/mcp/websocket/app.py +0 -3
- crackerjack/mcp/websocket/endpoints.py +206 -268
- crackerjack/mcp/websocket/jobs.py +213 -66
- crackerjack/mcp/websocket/server.py +84 -6
- crackerjack/mcp/websocket/websocket_handler.py +137 -29
- crackerjack/models/config_adapter.py +3 -16
- crackerjack/models/protocols.py +162 -3
- crackerjack/models/resource_protocols.py +454 -0
- crackerjack/models/task.py +3 -3
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +25 -71
- crackerjack/monitoring/regression_prevention.py +28 -87
- crackerjack/orchestration/advanced_orchestrator.py +44 -78
- crackerjack/orchestration/coverage_improvement.py +10 -60
- crackerjack/orchestration/execution_strategies.py +16 -16
- crackerjack/orchestration/test_progress_streamer.py +61 -53
- crackerjack/plugins/base.py +1 -1
- crackerjack/plugins/managers.py +22 -20
- crackerjack/py313.py +65 -21
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +627 -0
- crackerjack/services/cache.py +7 -9
- crackerjack/services/config.py +35 -52
- crackerjack/services/config_integrity.py +5 -16
- crackerjack/services/config_merge.py +542 -0
- crackerjack/services/contextual_ai_assistant.py +17 -19
- crackerjack/services/coverage_ratchet.py +44 -73
- crackerjack/services/debug.py +25 -39
- crackerjack/services/dependency_monitor.py +52 -50
- crackerjack/services/enhanced_filesystem.py +14 -11
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/filesystem.py +1 -12
- crackerjack/services/git.py +71 -47
- crackerjack/services/health_metrics.py +31 -27
- crackerjack/services/initialization.py +276 -428
- crackerjack/services/input_validator.py +760 -0
- crackerjack/services/log_manager.py +16 -16
- crackerjack/services/logging.py +7 -6
- crackerjack/services/metrics.py +43 -43
- crackerjack/services/pattern_cache.py +2 -31
- crackerjack/services/pattern_detector.py +26 -63
- crackerjack/services/performance_benchmarks.py +20 -45
- crackerjack/services/regex_patterns.py +2887 -0
- crackerjack/services/regex_utils.py +537 -0
- crackerjack/services/secure_path_utils.py +683 -0
- crackerjack/services/secure_status_formatter.py +534 -0
- crackerjack/services/secure_subprocess.py +605 -0
- crackerjack/services/security.py +47 -10
- crackerjack/services/security_logger.py +492 -0
- crackerjack/services/server_manager.py +109 -50
- crackerjack/services/smart_scheduling.py +8 -25
- crackerjack/services/status_authentication.py +603 -0
- crackerjack/services/status_security_manager.py +442 -0
- crackerjack/services/thread_safe_status_collector.py +546 -0
- crackerjack/services/tool_version_service.py +1 -23
- crackerjack/services/unified_config.py +36 -58
- crackerjack/services/validation_rate_limiter.py +269 -0
- crackerjack/services/version_checker.py +9 -40
- crackerjack/services/websocket_resource_limiter.py +572 -0
- crackerjack/slash_commands/__init__.py +52 -2
- crackerjack/tools/__init__.py +0 -0
- crackerjack/tools/validate_input_validator_patterns.py +262 -0
- crackerjack/tools/validate_regex_patterns.py +198 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
- crackerjack-0.31.12.dist-info/RECORD +178 -0
- crackerjack/cli/facade.py +0 -104
- crackerjack-0.31.10.dist-info/RECORD +0 -149
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secure status formatter to prevent information disclosure vulnerabilities.
|
|
3
|
+
|
|
4
|
+
This module provides secure sanitization of status responses to prevent
|
|
5
|
+
leaking sensitive system information such as absolute paths, internal URLs,
|
|
6
|
+
configuration details, and other sensitive data.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import tempfile
|
|
10
|
+
import typing as t
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .security_logger import get_security_logger
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StatusVerbosity(str, Enum):
|
|
18
|
+
"""Status output verbosity levels for security-aware responses."""
|
|
19
|
+
|
|
20
|
+
MINIMAL = "minimal" # Only essential operational status
|
|
21
|
+
STANDARD = "standard" # Standard operational information (default)
|
|
22
|
+
DETAILED = "detailed" # More detailed information for debugging
|
|
23
|
+
FULL = "full" # Complete information (for internal use only)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SecureStatusFormatter:
|
|
27
|
+
"""
|
|
28
|
+
Secure status formatter with configurable verbosity levels.
|
|
29
|
+
|
|
30
|
+
Sanitizes sensitive information while preserving necessary operational data.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# Patterns for sensitive information that should be masked or removed
|
|
34
|
+
SENSITIVE_PATTERNS = {
|
|
35
|
+
# Absolute system paths (replace with relative paths)
|
|
36
|
+
"absolute_paths": [
|
|
37
|
+
r"(/[^/\s]*){2,}", # Paths like /Users/username/...
|
|
38
|
+
rf"{tempfile.gettempdir()}/[^\s]*", # Temp directory paths - using tempfile.gettempdir() (B108)
|
|
39
|
+
r"/var/[^\s]*", # Var directory paths
|
|
40
|
+
r"/home/[^\s]*", # Home directory paths
|
|
41
|
+
],
|
|
42
|
+
# URLs with localhost/internal IPs
|
|
43
|
+
"internal_urls": [
|
|
44
|
+
r"https?://localhost:\d+",
|
|
45
|
+
r"https?://127\.0\.0\.1:\d+",
|
|
46
|
+
r"https?://0\.0\.0\.0:\d+",
|
|
47
|
+
r"ws://localhost:\d+",
|
|
48
|
+
r"ws://127\.0\.0\.1:\d+",
|
|
49
|
+
],
|
|
50
|
+
# Tokens and API keys
|
|
51
|
+
"secrets": [
|
|
52
|
+
r"[A-Za-z0-9]{20,}", # Long alphanumeric strings (potential tokens)
|
|
53
|
+
r"[A-Za-z0-9+/]{32,}={0,2}", # Base64-like strings
|
|
54
|
+
],
|
|
55
|
+
# Process IDs and system identifiers
|
|
56
|
+
"system_ids": [
|
|
57
|
+
r"pid:\d+",
|
|
58
|
+
r"process_id:\s*\d+",
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Sensitive keys that should be masked or removed
|
|
63
|
+
SENSITIVE_KEYS = {
|
|
64
|
+
"remove_minimal": {
|
|
65
|
+
"progress_dir",
|
|
66
|
+
"temp_files_count",
|
|
67
|
+
"rate_limiter",
|
|
68
|
+
"config",
|
|
69
|
+
"traceback",
|
|
70
|
+
"processes",
|
|
71
|
+
},
|
|
72
|
+
"remove_standard": {"progress_dir", "traceback"},
|
|
73
|
+
"mask": {
|
|
74
|
+
"project_path",
|
|
75
|
+
"websocket_url",
|
|
76
|
+
"monitor_url",
|
|
77
|
+
"api_key",
|
|
78
|
+
"token",
|
|
79
|
+
"secret",
|
|
80
|
+
"password",
|
|
81
|
+
"auth",
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def __init__(self, project_root: Path | None = None):
|
|
86
|
+
"""
|
|
87
|
+
Initialize secure status formatter.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
project_root: Project root path for relative path conversion
|
|
91
|
+
"""
|
|
92
|
+
self.project_root = project_root or Path.cwd()
|
|
93
|
+
self.security_logger = get_security_logger()
|
|
94
|
+
|
|
95
|
+
def format_status(
|
|
96
|
+
self,
|
|
97
|
+
status_data: dict[str, t.Any],
|
|
98
|
+
verbosity: StatusVerbosity = StatusVerbosity.STANDARD,
|
|
99
|
+
user_context: str | None = None,
|
|
100
|
+
) -> dict[str, t.Any]:
|
|
101
|
+
"""
|
|
102
|
+
Format status data with security sanitization.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
status_data: Raw status data to sanitize
|
|
106
|
+
verbosity: Output verbosity level
|
|
107
|
+
user_context: Optional user context for logging
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Sanitized status data appropriate for the verbosity level
|
|
111
|
+
"""
|
|
112
|
+
self._log_status_access(status_data, verbosity, user_context)
|
|
113
|
+
sanitized = self._prepare_data_for_sanitization(status_data)
|
|
114
|
+
sanitized = self._apply_all_sanitization_steps(sanitized, verbosity)
|
|
115
|
+
return self._add_security_metadata(sanitized, verbosity)
|
|
116
|
+
|
|
117
|
+
def _log_status_access(
|
|
118
|
+
self,
|
|
119
|
+
status_data: dict[str, t.Any],
|
|
120
|
+
verbosity: StatusVerbosity,
|
|
121
|
+
user_context: str | None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Log status access attempt for security monitoring."""
|
|
124
|
+
self.security_logger.log_status_access_attempt(
|
|
125
|
+
endpoint="status_data",
|
|
126
|
+
verbosity_level=verbosity.value,
|
|
127
|
+
user_context=user_context,
|
|
128
|
+
data_keys=list(status_data.keys()),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _prepare_data_for_sanitization(
|
|
132
|
+
self, status_data: dict[str, t.Any]
|
|
133
|
+
) -> dict[str, t.Any]:
|
|
134
|
+
"""Create a deep copy of status data to avoid modifying original."""
|
|
135
|
+
return self._deep_copy_dict(status_data)
|
|
136
|
+
|
|
137
|
+
def _apply_all_sanitization_steps(
|
|
138
|
+
self, data: dict[str, t.Any], verbosity: StatusVerbosity
|
|
139
|
+
) -> dict[str, t.Any]:
|
|
140
|
+
"""Apply all sanitization steps in proper order."""
|
|
141
|
+
data = self._apply_verbosity_filter(data, verbosity)
|
|
142
|
+
return self._sanitize_sensitive_data(data, verbosity)
|
|
143
|
+
|
|
144
|
+
def _add_security_metadata(
|
|
145
|
+
self, data: dict[str, t.Any], verbosity: StatusVerbosity
|
|
146
|
+
) -> dict[str, t.Any]:
|
|
147
|
+
"""Add security metadata to sanitized response."""
|
|
148
|
+
data["_security"] = {
|
|
149
|
+
"sanitized": True,
|
|
150
|
+
"verbosity": verbosity.value,
|
|
151
|
+
"timestamp": self._get_timestamp(),
|
|
152
|
+
}
|
|
153
|
+
return data
|
|
154
|
+
|
|
155
|
+
def _apply_verbosity_filter(
|
|
156
|
+
self, data: dict[str, t.Any], verbosity: StatusVerbosity
|
|
157
|
+
) -> dict[str, t.Any]:
|
|
158
|
+
"""Apply verbosity-based key filtering."""
|
|
159
|
+
|
|
160
|
+
# Keys to remove based on verbosity level
|
|
161
|
+
if verbosity == StatusVerbosity.MINIMAL:
|
|
162
|
+
remove_keys = self.SENSITIVE_KEYS["remove_minimal"]
|
|
163
|
+
elif verbosity == StatusVerbosity.STANDARD:
|
|
164
|
+
remove_keys = self.SENSITIVE_KEYS["remove_standard"]
|
|
165
|
+
else:
|
|
166
|
+
remove_keys = set() # Keep all keys for DETAILED/FULL
|
|
167
|
+
|
|
168
|
+
# Remove sensitive keys
|
|
169
|
+
for key in list(data.keys()):
|
|
170
|
+
if key in remove_keys:
|
|
171
|
+
del data[key]
|
|
172
|
+
|
|
173
|
+
# Recursively apply to nested dictionaries
|
|
174
|
+
for key, value in data.items():
|
|
175
|
+
if isinstance(value, dict):
|
|
176
|
+
data[key] = self._apply_verbosity_filter(value, verbosity)
|
|
177
|
+
|
|
178
|
+
return data
|
|
179
|
+
|
|
180
|
+
def _sanitize_sensitive_data(
|
|
181
|
+
self, data: dict[str, t.Any], verbosity: StatusVerbosity
|
|
182
|
+
) -> dict[str, t.Any]:
|
|
183
|
+
"""Sanitize sensitive data based on patterns and keys."""
|
|
184
|
+
|
|
185
|
+
return self._sanitize_recursive(data, verbosity)
|
|
186
|
+
|
|
187
|
+
def _sanitize_recursive(self, obj: t.Any, verbosity: StatusVerbosity) -> t.Any:
|
|
188
|
+
"""Recursively sanitize data structures."""
|
|
189
|
+
|
|
190
|
+
if isinstance(obj, dict):
|
|
191
|
+
sanitized = {}
|
|
192
|
+
for key, value in obj.items():
|
|
193
|
+
# Sanitize the key
|
|
194
|
+
sanitized_key = self._sanitize_string(key, verbosity)
|
|
195
|
+
|
|
196
|
+
# Sanitize the value
|
|
197
|
+
if (
|
|
198
|
+
key in self.SENSITIVE_KEYS["mask"]
|
|
199
|
+
and verbosity != StatusVerbosity.FULL
|
|
200
|
+
):
|
|
201
|
+
sanitized[sanitized_key] = self._mask_sensitive_value(str(value))
|
|
202
|
+
else:
|
|
203
|
+
sanitized[sanitized_key] = self._sanitize_recursive(
|
|
204
|
+
value, verbosity
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return sanitized
|
|
208
|
+
|
|
209
|
+
elif isinstance(obj, list):
|
|
210
|
+
return [self._sanitize_recursive(item, verbosity) for item in obj]
|
|
211
|
+
|
|
212
|
+
elif isinstance(obj, str):
|
|
213
|
+
return self._sanitize_string(obj, verbosity)
|
|
214
|
+
|
|
215
|
+
return obj
|
|
216
|
+
|
|
217
|
+
def _sanitize_string(self, text: str, verbosity: StatusVerbosity) -> str:
|
|
218
|
+
"""Sanitize string content based on verbosity level."""
|
|
219
|
+
if verbosity == StatusVerbosity.FULL:
|
|
220
|
+
return text # No sanitization for full verbosity
|
|
221
|
+
|
|
222
|
+
return self._apply_string_sanitization_pipeline(text, verbosity)
|
|
223
|
+
|
|
224
|
+
def _apply_string_sanitization_pipeline(
|
|
225
|
+
self, text: str, verbosity: StatusVerbosity
|
|
226
|
+
) -> str:
|
|
227
|
+
"""Apply string sanitization steps in correct order."""
|
|
228
|
+
sanitized = self._sanitize_internal_urls(text)
|
|
229
|
+
sanitized = self._sanitize_paths(sanitized)
|
|
230
|
+
return self._apply_secret_masking_if_needed(sanitized, verbosity)
|
|
231
|
+
|
|
232
|
+
def _apply_secret_masking_if_needed(
|
|
233
|
+
self, text: str, verbosity: StatusVerbosity
|
|
234
|
+
) -> str:
|
|
235
|
+
"""Apply secret masking for minimal verbosity only."""
|
|
236
|
+
if verbosity == StatusVerbosity.MINIMAL:
|
|
237
|
+
return self._mask_potential_secrets(text)
|
|
238
|
+
return text
|
|
239
|
+
|
|
240
|
+
def _sanitize_paths(self, text: str) -> str:
|
|
241
|
+
"""Convert absolute paths to relative paths where possible."""
|
|
242
|
+
from .regex_patterns import SAFE_PATTERNS
|
|
243
|
+
|
|
244
|
+
# Use validated patterns for path detection
|
|
245
|
+
unix_path_pattern = SAFE_PATTERNS.get("detect_absolute_unix_paths")
|
|
246
|
+
|
|
247
|
+
if not unix_path_pattern:
|
|
248
|
+
return self._sanitize_paths_fallback(text)
|
|
249
|
+
|
|
250
|
+
return text
|
|
251
|
+
|
|
252
|
+
def _sanitize_paths_fallback(self, text: str) -> str:
|
|
253
|
+
"""Fallback path sanitization when patterns don't exist."""
|
|
254
|
+
path_patterns = [
|
|
255
|
+
r"/[a-zA-Z0-9_\-\.\/]+", # Unix-style absolute paths
|
|
256
|
+
r"[A-Z]:[\\\/][a-zA-Z0-9_\-\.\\\/]+", # Windows-style absolute paths
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
for pattern_str in path_patterns:
|
|
260
|
+
text = self._process_path_pattern(text, pattern_str)
|
|
261
|
+
|
|
262
|
+
return text
|
|
263
|
+
|
|
264
|
+
def _process_path_pattern(self, text: str, pattern_str: str) -> str:
|
|
265
|
+
"""Process a single path pattern safely."""
|
|
266
|
+
from contextlib import suppress
|
|
267
|
+
|
|
268
|
+
from .regex_patterns import CompiledPatternCache
|
|
269
|
+
|
|
270
|
+
with suppress(Exception):
|
|
271
|
+
compiled = CompiledPatternCache.get_compiled_pattern(pattern_str)
|
|
272
|
+
matches = compiled.findall(text)
|
|
273
|
+
|
|
274
|
+
for match in matches:
|
|
275
|
+
if len(match) > 3: # Only process paths longer than 3 chars
|
|
276
|
+
text = self._replace_path_match(text, match)
|
|
277
|
+
|
|
278
|
+
return text
|
|
279
|
+
|
|
280
|
+
def _replace_path_match(self, text: str, match: str) -> str:
|
|
281
|
+
"""Replace a single path match with relative or redacted version."""
|
|
282
|
+
try:
|
|
283
|
+
abs_path = Path(match)
|
|
284
|
+
if abs_path.is_absolute():
|
|
285
|
+
return self._convert_to_relative_or_redact(text, match, abs_path)
|
|
286
|
+
except (ValueError, OSError):
|
|
287
|
+
# If path operations fail, mask it
|
|
288
|
+
text = text.replace(match, "[REDACTED_PATH]")
|
|
289
|
+
|
|
290
|
+
return text
|
|
291
|
+
|
|
292
|
+
def _convert_to_relative_or_redact(
|
|
293
|
+
self, text: str, match: str, abs_path: Path
|
|
294
|
+
) -> str:
|
|
295
|
+
"""Convert absolute path to relative or redact if outside project."""
|
|
296
|
+
try:
|
|
297
|
+
rel_path = abs_path.relative_to(self.project_root)
|
|
298
|
+
return text.replace(match, f"./{rel_path}")
|
|
299
|
+
except (ValueError, OSError):
|
|
300
|
+
# If not within project, mask it
|
|
301
|
+
return text.replace(match, "[REDACTED_PATH]")
|
|
302
|
+
|
|
303
|
+
def _sanitize_internal_urls(self, text: str) -> str:
|
|
304
|
+
"""Replace internal URLs with generic placeholders."""
|
|
305
|
+
from .regex_patterns import sanitize_internal_urls
|
|
306
|
+
|
|
307
|
+
return sanitize_internal_urls(text)
|
|
308
|
+
|
|
309
|
+
def _mask_potential_secrets(self, text: str) -> str:
|
|
310
|
+
"""Mask strings that might be secrets."""
|
|
311
|
+
if self._should_skip_secret_masking(text):
|
|
312
|
+
return text
|
|
313
|
+
|
|
314
|
+
patterns_to_check = self._get_validated_secret_patterns()
|
|
315
|
+
|
|
316
|
+
if patterns_to_check:
|
|
317
|
+
return self._apply_validated_secret_patterns(text, patterns_to_check)
|
|
318
|
+
return self._apply_fallback_secret_patterns(text)
|
|
319
|
+
|
|
320
|
+
def _should_skip_secret_masking(self, text: str) -> bool:
|
|
321
|
+
"""Check if secret masking should be skipped for already sanitized content."""
|
|
322
|
+
return "[INTERNAL_URL]" in text or "[REDACTED_PATH]" in text
|
|
323
|
+
|
|
324
|
+
def _get_validated_secret_patterns(self) -> list[t.Any]:
|
|
325
|
+
"""Get validated patterns from the safe patterns registry."""
|
|
326
|
+
from .regex_patterns import SAFE_PATTERNS
|
|
327
|
+
|
|
328
|
+
patterns = []
|
|
329
|
+
long_alphanumeric = SAFE_PATTERNS.get("detect_long_alphanumeric_tokens")
|
|
330
|
+
base64_like = SAFE_PATTERNS.get("detect_base64_like_strings")
|
|
331
|
+
|
|
332
|
+
if long_alphanumeric:
|
|
333
|
+
patterns.append(long_alphanumeric)
|
|
334
|
+
if base64_like:
|
|
335
|
+
patterns.append(base64_like)
|
|
336
|
+
|
|
337
|
+
return patterns
|
|
338
|
+
|
|
339
|
+
def _apply_validated_secret_patterns(self, text: str, patterns: list[t.Any]) -> str:
|
|
340
|
+
"""Apply validated patterns for secret detection."""
|
|
341
|
+
for pattern in patterns:
|
|
342
|
+
try:
|
|
343
|
+
text = self._mask_pattern_matches(text, pattern.findall(text))
|
|
344
|
+
except Exception:
|
|
345
|
+
continue # Skip failed pattern matching
|
|
346
|
+
return text
|
|
347
|
+
|
|
348
|
+
def _apply_fallback_secret_patterns(self, text: str) -> str:
|
|
349
|
+
"""Apply fallback patterns when validated patterns don't exist."""
|
|
350
|
+
for pattern_str in self.SENSITIVE_PATTERNS["secrets"]:
|
|
351
|
+
try:
|
|
352
|
+
from .regex_patterns import CompiledPatternCache
|
|
353
|
+
|
|
354
|
+
compiled = CompiledPatternCache.get_compiled_pattern(pattern_str)
|
|
355
|
+
text = self._mask_pattern_matches(text, compiled.findall(text))
|
|
356
|
+
except Exception:
|
|
357
|
+
continue # Skip failed pattern compilation
|
|
358
|
+
return text
|
|
359
|
+
|
|
360
|
+
def _mask_pattern_matches(self, text: str, matches: list[str]) -> str:
|
|
361
|
+
"""Mask matches from pattern matching."""
|
|
362
|
+
for match in matches:
|
|
363
|
+
if self._should_mask_match(match):
|
|
364
|
+
masked = self._create_masked_string(match)
|
|
365
|
+
text = text.replace(match, masked)
|
|
366
|
+
return text
|
|
367
|
+
|
|
368
|
+
def _should_mask_match(self, match: str) -> bool:
|
|
369
|
+
"""Determine if a match should be masked."""
|
|
370
|
+
if len(match) <= 16:
|
|
371
|
+
return False
|
|
372
|
+
# Don't mask if it looks like a URL or path component
|
|
373
|
+
return not any(x in match for x in ("://", "/", "\\", "."))
|
|
374
|
+
|
|
375
|
+
def _create_masked_string(self, text: str) -> str:
|
|
376
|
+
"""Create a masked version of the text."""
|
|
377
|
+
return text[:4] + "*" * (len(text) - 8) + text[-4:]
|
|
378
|
+
|
|
379
|
+
def _mask_sensitive_value(self, value: str) -> str:
|
|
380
|
+
"""Mask a known sensitive value."""
|
|
381
|
+
|
|
382
|
+
if len(value) <= 4:
|
|
383
|
+
return "***"
|
|
384
|
+
elif len(value) <= 8:
|
|
385
|
+
return value[0] + "*" * (len(value) - 1)
|
|
386
|
+
return value[:2] + "*" * (len(value) - 4) + value[-2:]
|
|
387
|
+
|
|
388
|
+
def _deep_copy_dict(self, obj: t.Any) -> t.Any:
|
|
389
|
+
"""Deep copy a dictionary-like object safely."""
|
|
390
|
+
|
|
391
|
+
if isinstance(obj, dict):
|
|
392
|
+
return {key: self._deep_copy_dict(value) for key, value in obj.items()}
|
|
393
|
+
elif isinstance(obj, list):
|
|
394
|
+
return [self._deep_copy_dict(item) for item in obj]
|
|
395
|
+
return obj
|
|
396
|
+
|
|
397
|
+
def _get_timestamp(self) -> float:
|
|
398
|
+
"""Get current timestamp."""
|
|
399
|
+
import time
|
|
400
|
+
|
|
401
|
+
return time.time()
|
|
402
|
+
|
|
403
|
+
def format_error_response(
|
|
404
|
+
self,
|
|
405
|
+
error_message: str,
|
|
406
|
+
verbosity: StatusVerbosity = StatusVerbosity.STANDARD,
|
|
407
|
+
include_details: bool = False,
|
|
408
|
+
) -> dict[str, t.Any]:
|
|
409
|
+
"""
|
|
410
|
+
Format error responses without leaking system details.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
error_message: Original error message
|
|
414
|
+
verbosity: Output verbosity level
|
|
415
|
+
include_details: Whether to include error details
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Sanitized error response
|
|
419
|
+
"""
|
|
420
|
+
error_type = self._classify_error(error_message)
|
|
421
|
+
|
|
422
|
+
if verbosity == StatusVerbosity.MINIMAL:
|
|
423
|
+
return self._create_minimal_error_response(error_type)
|
|
424
|
+
|
|
425
|
+
return self._create_detailed_error_response(
|
|
426
|
+
error_message, error_type, verbosity, include_details
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def _create_minimal_error_response(self, error_type: str) -> dict[str, t.Any]:
|
|
430
|
+
"""Create minimal error response for production use."""
|
|
431
|
+
generic_messages = {
|
|
432
|
+
"connection": "Service temporarily unavailable. Please try again later.",
|
|
433
|
+
"validation": "Invalid request parameters.",
|
|
434
|
+
"permission": "Access denied.",
|
|
435
|
+
"resource": "Requested resource not found.",
|
|
436
|
+
"internal": "An internal error occurred. Please contact support.",
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
"success": False,
|
|
441
|
+
"error": generic_messages.get(error_type, generic_messages["internal"]),
|
|
442
|
+
"timestamp": self._get_timestamp(),
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
def _create_detailed_error_response(
|
|
446
|
+
self,
|
|
447
|
+
error_message: str,
|
|
448
|
+
error_type: str,
|
|
449
|
+
verbosity: StatusVerbosity,
|
|
450
|
+
include_details: bool,
|
|
451
|
+
) -> dict[str, t.Any]:
|
|
452
|
+
"""Create detailed error response with sanitized message."""
|
|
453
|
+
sanitized_message = self._sanitize_string(error_message, verbosity)
|
|
454
|
+
|
|
455
|
+
response: dict[str, t.Any] = {
|
|
456
|
+
"success": False,
|
|
457
|
+
"error": sanitized_message,
|
|
458
|
+
"error_type": error_type,
|
|
459
|
+
"timestamp": self._get_timestamp(),
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if self._should_include_error_details(include_details, verbosity):
|
|
463
|
+
response["details"] = {
|
|
464
|
+
"verbosity": str(verbosity.value),
|
|
465
|
+
"sanitized": verbosity != StatusVerbosity.FULL,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return response
|
|
469
|
+
|
|
470
|
+
def _should_include_error_details(
|
|
471
|
+
self, include_details: bool, verbosity: StatusVerbosity
|
|
472
|
+
) -> bool:
|
|
473
|
+
"""Determine if error details should be included."""
|
|
474
|
+
return include_details and verbosity in (
|
|
475
|
+
StatusVerbosity.DETAILED,
|
|
476
|
+
StatusVerbosity.FULL,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
def _classify_error(self, error_message: str) -> str:
|
|
480
|
+
"""Classify error message type for appropriate handling."""
|
|
481
|
+
|
|
482
|
+
error_patterns = {
|
|
483
|
+
"connection": ["connection", "timeout", "refused", "unavailable"],
|
|
484
|
+
"validation": ["invalid", "validation", "format", "parameter"],
|
|
485
|
+
"permission": ["permission", "access", "denied", "unauthorized"],
|
|
486
|
+
"resource": ["not found", "missing", "does not exist"],
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
error_lower = error_message.lower()
|
|
490
|
+
|
|
491
|
+
for error_type, patterns in error_patterns.items():
|
|
492
|
+
if any(pattern in error_lower for pattern in patterns):
|
|
493
|
+
return error_type
|
|
494
|
+
|
|
495
|
+
return "internal"
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
# Singleton instance for global use
|
|
499
|
+
_secure_formatter: SecureStatusFormatter | None = None
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def get_secure_status_formatter(
|
|
503
|
+
project_root: Path | None = None,
|
|
504
|
+
) -> SecureStatusFormatter:
|
|
505
|
+
"""Get the global secure status formatter instance."""
|
|
506
|
+
|
|
507
|
+
global _secure_formatter
|
|
508
|
+
if _secure_formatter is None:
|
|
509
|
+
_secure_formatter = SecureStatusFormatter(project_root)
|
|
510
|
+
return _secure_formatter
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def format_secure_status(
|
|
514
|
+
status_data: dict[str, t.Any],
|
|
515
|
+
verbosity: StatusVerbosity = StatusVerbosity.STANDARD,
|
|
516
|
+
project_root: Path | None = None,
|
|
517
|
+
user_context: str | None = None,
|
|
518
|
+
) -> dict[str, t.Any]:
|
|
519
|
+
"""
|
|
520
|
+
Convenience function for secure status formatting.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
status_data: Raw status data to sanitize
|
|
524
|
+
verbosity: Output verbosity level
|
|
525
|
+
project_root: Project root for relative path conversion
|
|
526
|
+
user_context: Optional user context for logging
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
Sanitized status data
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
return get_secure_status_formatter(project_root).format_status(
|
|
533
|
+
status_data, verbosity, user_context
|
|
534
|
+
)
|