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,683 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import typing as t
|
|
4
|
+
import urllib.parse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..errors import ErrorCode, ExecutionError
|
|
8
|
+
from .regex_patterns import validate_path_security
|
|
9
|
+
from .security_logger import SecurityEventLevel, SecurityEventType, get_security_logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SecurePathValidator:
|
|
13
|
+
"""Comprehensive path security validation to prevent directory traversal attacks."""
|
|
14
|
+
|
|
15
|
+
MAX_FILE_SIZE = 100 * 1024 * 1024
|
|
16
|
+
MAX_PATH_LENGTH = 4096
|
|
17
|
+
|
|
18
|
+
# Enhanced dangerous components including encoded variations
|
|
19
|
+
DANGEROUS_COMPONENTS = {
|
|
20
|
+
"..",
|
|
21
|
+
".",
|
|
22
|
+
"~",
|
|
23
|
+
"$",
|
|
24
|
+
"`",
|
|
25
|
+
";",
|
|
26
|
+
"&",
|
|
27
|
+
"|",
|
|
28
|
+
"<",
|
|
29
|
+
">",
|
|
30
|
+
"CON",
|
|
31
|
+
"PRN",
|
|
32
|
+
"AUX",
|
|
33
|
+
"NUL",
|
|
34
|
+
"COM1",
|
|
35
|
+
"COM2",
|
|
36
|
+
"COM3",
|
|
37
|
+
"COM4",
|
|
38
|
+
"COM5",
|
|
39
|
+
"COM6",
|
|
40
|
+
"COM7",
|
|
41
|
+
"COM8",
|
|
42
|
+
"COM9",
|
|
43
|
+
"LPT1",
|
|
44
|
+
"LPT2",
|
|
45
|
+
"LPT3",
|
|
46
|
+
"LPT4",
|
|
47
|
+
"LPT5",
|
|
48
|
+
"LPT6",
|
|
49
|
+
"LPT7",
|
|
50
|
+
"LPT8",
|
|
51
|
+
"LPT9",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Pattern constants removed - now using centralized SAFE_PATTERNS for security validation
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def validate_safe_path(
|
|
58
|
+
cls, path: str | Path, base_directory: Path | None = None
|
|
59
|
+
) -> Path:
|
|
60
|
+
"""
|
|
61
|
+
Comprehensive path validation to prevent directory traversal attacks.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
path: Path to validate (string or Path object)
|
|
65
|
+
base_directory: Optional base directory to constrain path within
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Validated and normalized Path object
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ExecutionError: If path contains malicious patterns or is invalid
|
|
72
|
+
"""
|
|
73
|
+
# Convert to string for pattern checking
|
|
74
|
+
path_str = str(path)
|
|
75
|
+
|
|
76
|
+
# Check for null bytes and dangerous patterns
|
|
77
|
+
cls._check_malicious_patterns(path_str)
|
|
78
|
+
|
|
79
|
+
# Convert to Path and normalize
|
|
80
|
+
try:
|
|
81
|
+
path_obj = Path(path_str)
|
|
82
|
+
normalized = cls.normalize_path(path_obj)
|
|
83
|
+
except (ValueError, OSError) as e:
|
|
84
|
+
raise ExecutionError(
|
|
85
|
+
message=f"Invalid path format: {path_str}",
|
|
86
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
87
|
+
) from e
|
|
88
|
+
|
|
89
|
+
# Validate path length
|
|
90
|
+
if len(str(normalized)) > cls.MAX_PATH_LENGTH:
|
|
91
|
+
raise ExecutionError(
|
|
92
|
+
message=f"Path too long: {len(str(normalized))} > {cls.MAX_PATH_LENGTH}",
|
|
93
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Check dangerous components
|
|
97
|
+
cls._check_dangerous_components(normalized)
|
|
98
|
+
|
|
99
|
+
# Validate within base directory if specified
|
|
100
|
+
if base_directory:
|
|
101
|
+
if not cls.is_within_directory(normalized, base_directory):
|
|
102
|
+
raise ExecutionError(
|
|
103
|
+
message=f"Path outside allowed directory: {normalized} not within {base_directory}",
|
|
104
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return normalized
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def validate_file_path(
|
|
111
|
+
cls, file_path: Path, base_directory: Path | None = None
|
|
112
|
+
) -> Path:
|
|
113
|
+
"""Legacy method - redirects to validate_safe_path for consistency."""
|
|
114
|
+
return cls.validate_safe_path(file_path, base_directory)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def secure_path_join(cls, base: Path, *parts: str) -> Path:
|
|
118
|
+
"""
|
|
119
|
+
Safe alternative to Path.joinpath() that prevents directory traversal.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
base: Base directory path
|
|
123
|
+
*parts: Path components to join
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Safely joined path
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ExecutionError: If any part contains malicious patterns
|
|
130
|
+
"""
|
|
131
|
+
# Validate base path
|
|
132
|
+
validated_base = cls.validate_safe_path(base)
|
|
133
|
+
|
|
134
|
+
# Validate each part for malicious patterns
|
|
135
|
+
for part in parts:
|
|
136
|
+
cls._check_malicious_patterns(part)
|
|
137
|
+
# Don't allow absolute paths or parent directory references
|
|
138
|
+
if Path(part).is_absolute():
|
|
139
|
+
raise ExecutionError(
|
|
140
|
+
message=f"Absolute path not allowed in join: {part}",
|
|
141
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Join paths safely
|
|
145
|
+
result = validated_base.joinpath(*parts)
|
|
146
|
+
|
|
147
|
+
# Ensure result is still within base directory
|
|
148
|
+
if not cls.is_within_directory(result, validated_base):
|
|
149
|
+
raise ExecutionError(
|
|
150
|
+
message=f"Joined path escapes base directory: {result} not within {validated_base}",
|
|
151
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def normalize_path(cls, path: Path) -> Path:
|
|
158
|
+
"""
|
|
159
|
+
Canonical path resolution with security checks.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
path: Path to normalize
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Normalized path with symlinks resolved
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
ExecutionError: If path resolution fails or contains malicious patterns
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
# Resolve symlinks and normalize
|
|
172
|
+
resolved = path.resolve()
|
|
173
|
+
|
|
174
|
+
# Additional validation after resolution
|
|
175
|
+
cls._validate_resolved_path(resolved)
|
|
176
|
+
|
|
177
|
+
return resolved
|
|
178
|
+
|
|
179
|
+
except (OSError, RuntimeError) as e:
|
|
180
|
+
raise ExecutionError(
|
|
181
|
+
message=f"Path normalization failed for {path}: {e}",
|
|
182
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
183
|
+
) from e
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def is_within_directory(cls, path: Path, directory: Path) -> bool:
|
|
187
|
+
"""
|
|
188
|
+
Verify that a path is contained within a directory.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
path: Path to check
|
|
192
|
+
directory: Directory that should contain the path
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True if path is within directory, False otherwise
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
# Resolve both paths to handle symlinks
|
|
199
|
+
resolved_path = path.resolve()
|
|
200
|
+
resolved_directory = directory.resolve()
|
|
201
|
+
|
|
202
|
+
# Check if path is relative to directory
|
|
203
|
+
resolved_path.relative_to(resolved_directory)
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
except (ValueError, OSError):
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def safe_resolve(cls, path: Path, base_directory: Path | None = None) -> Path:
|
|
211
|
+
"""
|
|
212
|
+
Secure path resolution preventing symlink attacks.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
path: Path to resolve
|
|
216
|
+
base_directory: Optional base directory constraint
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Safely resolved path
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
ExecutionError: If resolution fails or path escapes constraints
|
|
223
|
+
"""
|
|
224
|
+
# First validate the input path
|
|
225
|
+
validated_path = cls.validate_safe_path(path, base_directory)
|
|
226
|
+
|
|
227
|
+
# Resolve with additional symlink attack prevention
|
|
228
|
+
resolved = cls.normalize_path(validated_path)
|
|
229
|
+
|
|
230
|
+
# Re-validate after resolution
|
|
231
|
+
if base_directory and not cls.is_within_directory(resolved, base_directory):
|
|
232
|
+
raise ExecutionError(
|
|
233
|
+
message=f"Resolved path escapes base directory: {resolved} not within {base_directory}",
|
|
234
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return resolved
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def _check_malicious_patterns(cls, path_str: str) -> None:
|
|
241
|
+
"""Check for directory traversal and null byte patterns using safe patterns."""
|
|
242
|
+
security_logger = get_security_logger()
|
|
243
|
+
|
|
244
|
+
# URL decode the path to catch encoded attacks
|
|
245
|
+
try:
|
|
246
|
+
decoded = urllib.parse.unquote(path_str, errors="strict")
|
|
247
|
+
except UnicodeDecodeError:
|
|
248
|
+
# If decoding fails, use original string but still check patterns
|
|
249
|
+
decoded = path_str
|
|
250
|
+
|
|
251
|
+
# Check both original and decoded versions using safe patterns
|
|
252
|
+
for check_str in (path_str, decoded):
|
|
253
|
+
validation_results = validate_path_security(check_str)
|
|
254
|
+
|
|
255
|
+
# Check for null byte patterns
|
|
256
|
+
if validation_results["null_bytes"]:
|
|
257
|
+
detected_pattern = validation_results["null_bytes"][
|
|
258
|
+
0
|
|
259
|
+
] # First detected pattern
|
|
260
|
+
security_logger.log_security_event(
|
|
261
|
+
SecurityEventType.PATH_TRAVERSAL_ATTEMPT,
|
|
262
|
+
SecurityEventLevel.CRITICAL,
|
|
263
|
+
f"Null byte pattern detected in path: {path_str}",
|
|
264
|
+
file_path=path_str,
|
|
265
|
+
pattern_type="null_byte",
|
|
266
|
+
detected_pattern=detected_pattern,
|
|
267
|
+
)
|
|
268
|
+
raise ExecutionError(
|
|
269
|
+
message=f"Null byte pattern detected in path: {path_str}",
|
|
270
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Check for directory traversal patterns
|
|
274
|
+
if validation_results["traversal_patterns"]:
|
|
275
|
+
detected_pattern = validation_results["traversal_patterns"][
|
|
276
|
+
0
|
|
277
|
+
] # First detected pattern
|
|
278
|
+
security_logger.log_path_traversal_attempt(
|
|
279
|
+
attempted_path=path_str,
|
|
280
|
+
pattern_type="directory_traversal",
|
|
281
|
+
detected_pattern=detected_pattern,
|
|
282
|
+
)
|
|
283
|
+
raise ExecutionError(
|
|
284
|
+
message=f"Directory traversal pattern detected in path: {path_str}",
|
|
285
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
def _validate_resolved_path(cls, path: Path) -> None:
|
|
290
|
+
"""Additional validation for resolved paths using safe patterns."""
|
|
291
|
+
path_str = str(path)
|
|
292
|
+
|
|
293
|
+
# Check for dangerous patterns that might appear after resolution using safe patterns
|
|
294
|
+
validation_results = validate_path_security(path_str)
|
|
295
|
+
|
|
296
|
+
# Check for parent directory references
|
|
297
|
+
if validation_results["suspicious_patterns"]:
|
|
298
|
+
if (
|
|
299
|
+
"detect_parent_directory_in_path"
|
|
300
|
+
in validation_results["suspicious_patterns"]
|
|
301
|
+
):
|
|
302
|
+
raise ExecutionError(
|
|
303
|
+
message=f"Parent directory reference in resolved path: {path}",
|
|
304
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Check for suspicious traversal patterns in system directories
|
|
308
|
+
suspicious_detected = [
|
|
309
|
+
pattern
|
|
310
|
+
for pattern in validation_results["suspicious_patterns"]
|
|
311
|
+
if pattern
|
|
312
|
+
in ("detect_suspicious_temp_traversal", "detect_suspicious_var_traversal")
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
if suspicious_detected:
|
|
316
|
+
raise ExecutionError(
|
|
317
|
+
message=f"Suspicious path pattern detected: {path}",
|
|
318
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
@classmethod
|
|
322
|
+
def _check_dangerous_components(cls, path: Path) -> None:
|
|
323
|
+
security_logger = get_security_logger()
|
|
324
|
+
|
|
325
|
+
for part in path.parts:
|
|
326
|
+
if part in cls.DANGEROUS_COMPONENTS:
|
|
327
|
+
security_logger.log_dangerous_path_detected(
|
|
328
|
+
path=str(path),
|
|
329
|
+
dangerous_component=part,
|
|
330
|
+
)
|
|
331
|
+
raise ExecutionError(
|
|
332
|
+
message=f"Dangerous path component detected: {part}",
|
|
333
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
@classmethod
|
|
337
|
+
def _validate_within_base_directory(cls, path: Path, base_directory: Path) -> None:
|
|
338
|
+
base_resolved = base_directory.resolve()
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
path.relative_to(base_resolved)
|
|
342
|
+
except ValueError as e:
|
|
343
|
+
raise ExecutionError(
|
|
344
|
+
message=f"Path outside allowed directory: {path} not within {base_resolved}",
|
|
345
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
346
|
+
) from e
|
|
347
|
+
|
|
348
|
+
@classmethod
|
|
349
|
+
def validate_file_size(cls, file_path: Path) -> None:
|
|
350
|
+
try:
|
|
351
|
+
file_size = file_path.stat().st_size
|
|
352
|
+
if file_size > cls.MAX_FILE_SIZE:
|
|
353
|
+
raise ExecutionError(
|
|
354
|
+
message=f"File too large: {file_size} bytes > {cls.MAX_FILE_SIZE} bytes limit",
|
|
355
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
356
|
+
)
|
|
357
|
+
except OSError as e:
|
|
358
|
+
raise ExecutionError(
|
|
359
|
+
message=f"Cannot check file size: {file_path}",
|
|
360
|
+
error_code=ErrorCode.FILE_READ_ERROR,
|
|
361
|
+
) from e
|
|
362
|
+
|
|
363
|
+
@classmethod
|
|
364
|
+
def create_secure_backup_path(
|
|
365
|
+
cls, original_path: Path, base_directory: Path | None = None
|
|
366
|
+
) -> Path:
|
|
367
|
+
validated_original = cls.validate_file_path(original_path, base_directory)
|
|
368
|
+
|
|
369
|
+
backup_path = validated_original.parent / f"{validated_original.name}.backup"
|
|
370
|
+
|
|
371
|
+
validated_backup = cls.validate_file_path(backup_path, base_directory)
|
|
372
|
+
|
|
373
|
+
return validated_backup
|
|
374
|
+
|
|
375
|
+
@classmethod
|
|
376
|
+
def create_secure_temp_file(
|
|
377
|
+
cls,
|
|
378
|
+
suffix: str = ".tmp",
|
|
379
|
+
prefix: str = "crackerjack_",
|
|
380
|
+
directory: Path | None = None,
|
|
381
|
+
purpose: str = "general",
|
|
382
|
+
) -> t.Any:
|
|
383
|
+
"""
|
|
384
|
+
Create a secure temporary file with proper permissions.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
suffix: File suffix
|
|
388
|
+
prefix: File prefix
|
|
389
|
+
directory: Directory to create temp file in (validated if provided)
|
|
390
|
+
purpose: Purpose description for security logging
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Secure temporary file handle
|
|
394
|
+
|
|
395
|
+
Raises:
|
|
396
|
+
ExecutionError: If temp file creation fails
|
|
397
|
+
"""
|
|
398
|
+
security_logger = get_security_logger()
|
|
399
|
+
|
|
400
|
+
# Validate directory if provided
|
|
401
|
+
if directory:
|
|
402
|
+
directory = cls.validate_safe_path(directory)
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
temp_file = tempfile.NamedTemporaryFile(
|
|
406
|
+
mode="w+b", suffix=suffix, prefix=prefix, dir=directory, delete=False
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# Set restrictive permissions (owner read/write only)
|
|
410
|
+
os.chmod(temp_file.name, 0o600)
|
|
411
|
+
|
|
412
|
+
# Log secure temp file creation
|
|
413
|
+
security_logger.log_temp_file_created(
|
|
414
|
+
temp_path=temp_file.name,
|
|
415
|
+
purpose=purpose,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
return temp_file
|
|
419
|
+
|
|
420
|
+
except OSError as e:
|
|
421
|
+
raise ExecutionError(
|
|
422
|
+
message=f"Failed to create secure temporary file: {e}",
|
|
423
|
+
error_code=ErrorCode.FILE_WRITE_ERROR,
|
|
424
|
+
) from e
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class AtomicFileOperations:
|
|
428
|
+
@staticmethod
|
|
429
|
+
def atomic_write(
|
|
430
|
+
file_path: Path, content: str | bytes, base_directory: Path | None = None
|
|
431
|
+
) -> None:
|
|
432
|
+
security_logger = get_security_logger()
|
|
433
|
+
|
|
434
|
+
validated_path = SecurePathValidator.validate_safe_path(
|
|
435
|
+
file_path, base_directory
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
temp_file = None
|
|
439
|
+
try:
|
|
440
|
+
temp_file = SecurePathValidator.create_secure_temp_file(
|
|
441
|
+
prefix="atomic_write_",
|
|
442
|
+
directory=validated_path.parent,
|
|
443
|
+
purpose="atomic_file_write",
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
if isinstance(content, str):
|
|
447
|
+
temp_file.write(content.encode("utf-8"))
|
|
448
|
+
else:
|
|
449
|
+
temp_file.write(content)
|
|
450
|
+
|
|
451
|
+
temp_file.flush()
|
|
452
|
+
os.fsync(temp_file.fileno())
|
|
453
|
+
temp_file.close()
|
|
454
|
+
|
|
455
|
+
temp_path = Path(temp_file.name)
|
|
456
|
+
temp_path.replace(validated_path)
|
|
457
|
+
|
|
458
|
+
# Log successful atomic operation
|
|
459
|
+
security_logger.log_atomic_operation(
|
|
460
|
+
operation="write",
|
|
461
|
+
file_path=str(validated_path),
|
|
462
|
+
success=True,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
except Exception as e:
|
|
466
|
+
if temp_file and hasattr(temp_file, "name"):
|
|
467
|
+
temp_path = Path(temp_file.name)
|
|
468
|
+
if temp_path.exists():
|
|
469
|
+
temp_path.unlink()
|
|
470
|
+
|
|
471
|
+
# Log failed atomic operation
|
|
472
|
+
security_logger.log_atomic_operation(
|
|
473
|
+
operation="write",
|
|
474
|
+
file_path=str(validated_path),
|
|
475
|
+
success=False,
|
|
476
|
+
error=str(e),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
raise ExecutionError(
|
|
480
|
+
message=f"Atomic write failed for {validated_path}: {e}",
|
|
481
|
+
error_code=ErrorCode.FILE_WRITE_ERROR,
|
|
482
|
+
) from e
|
|
483
|
+
|
|
484
|
+
@staticmethod
|
|
485
|
+
def atomic_backup_and_write(
|
|
486
|
+
file_path: Path, new_content: str | bytes, base_directory: Path | None = None
|
|
487
|
+
) -> Path:
|
|
488
|
+
security_logger = get_security_logger()
|
|
489
|
+
|
|
490
|
+
validated_path = SecurePathValidator.validate_safe_path(
|
|
491
|
+
file_path, base_directory
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
if not validated_path.exists():
|
|
495
|
+
raise ExecutionError(
|
|
496
|
+
message=f"File does not exist: {validated_path}",
|
|
497
|
+
error_code=ErrorCode.FILE_READ_ERROR,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
SecurePathValidator.validate_file_size(validated_path)
|
|
501
|
+
|
|
502
|
+
backup_path = SecurePathValidator.create_secure_backup_path(
|
|
503
|
+
validated_path, base_directory
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
try:
|
|
507
|
+
original_content = validated_path.read_bytes()
|
|
508
|
+
|
|
509
|
+
AtomicFileOperations.atomic_write(
|
|
510
|
+
backup_path, original_content, base_directory
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
AtomicFileOperations.atomic_write(
|
|
514
|
+
validated_path, new_content, base_directory
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Log successful backup creation
|
|
518
|
+
security_logger.log_backup_created(
|
|
519
|
+
original_path=str(validated_path),
|
|
520
|
+
backup_path=str(backup_path),
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
return backup_path
|
|
524
|
+
|
|
525
|
+
except Exception as e:
|
|
526
|
+
if backup_path.exists():
|
|
527
|
+
backup_path.unlink()
|
|
528
|
+
|
|
529
|
+
# Log failed backup operation
|
|
530
|
+
security_logger.log_atomic_operation(
|
|
531
|
+
operation="backup_and_write",
|
|
532
|
+
file_path=str(validated_path),
|
|
533
|
+
success=False,
|
|
534
|
+
error=str(e),
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
raise ExecutionError(
|
|
538
|
+
message=f"Atomic backup and write failed: {e}",
|
|
539
|
+
error_code=ErrorCode.FILE_WRITE_ERROR,
|
|
540
|
+
) from e
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class SubprocessPathValidator:
|
|
544
|
+
"""Specialized path validation for subprocess execution contexts."""
|
|
545
|
+
|
|
546
|
+
# Paths that should never be accessible via subprocess
|
|
547
|
+
FORBIDDEN_SUBPROCESS_PATHS = {
|
|
548
|
+
"/etc/passwd",
|
|
549
|
+
"/etc/shadow",
|
|
550
|
+
"/etc/sudoers",
|
|
551
|
+
"/etc/hosts",
|
|
552
|
+
"/boot",
|
|
553
|
+
"/sys",
|
|
554
|
+
"/proc",
|
|
555
|
+
"/dev",
|
|
556
|
+
"/var/log",
|
|
557
|
+
"/usr/bin/sudo",
|
|
558
|
+
"/usr/bin/su",
|
|
559
|
+
"/bin/su",
|
|
560
|
+
"/bin/sudo",
|
|
561
|
+
"/etc/ssh",
|
|
562
|
+
"/root",
|
|
563
|
+
"/var/spool/cron",
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
# Directory patterns removed - now using centralized SAFE_PATTERNS for security validation
|
|
567
|
+
|
|
568
|
+
@classmethod
|
|
569
|
+
def validate_subprocess_cwd(cls, cwd: Path | str | None) -> Path | None:
|
|
570
|
+
"""
|
|
571
|
+
Validate working directory for subprocess execution.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
cwd: Working directory path
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
Validated Path object or None
|
|
578
|
+
|
|
579
|
+
Raises:
|
|
580
|
+
ExecutionError: If path is dangerous for subprocess execution
|
|
581
|
+
"""
|
|
582
|
+
if cwd is None:
|
|
583
|
+
return None
|
|
584
|
+
|
|
585
|
+
# Use base path validation first
|
|
586
|
+
validated_cwd = SecurePathValidator.validate_safe_path(cwd)
|
|
587
|
+
|
|
588
|
+
# Additional subprocess-specific checks
|
|
589
|
+
cwd_str = str(validated_cwd)
|
|
590
|
+
|
|
591
|
+
# Check against forbidden paths
|
|
592
|
+
if cwd_str in cls.FORBIDDEN_SUBPROCESS_PATHS:
|
|
593
|
+
security_logger = get_security_logger()
|
|
594
|
+
security_logger.log_dangerous_path_detected(
|
|
595
|
+
path=cwd_str,
|
|
596
|
+
dangerous_component="forbidden_subprocess_path",
|
|
597
|
+
context="subprocess_cwd_validation",
|
|
598
|
+
)
|
|
599
|
+
raise ExecutionError(
|
|
600
|
+
message=f"Forbidden subprocess working directory: {cwd_str}",
|
|
601
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# Check against dangerous directory patterns using safe patterns
|
|
605
|
+
validation_results = validate_path_security(cwd_str)
|
|
606
|
+
|
|
607
|
+
if validation_results["dangerous_directories"]:
|
|
608
|
+
detected_pattern = validation_results["dangerous_directories"][
|
|
609
|
+
0
|
|
610
|
+
] # First detected pattern
|
|
611
|
+
security_logger = get_security_logger()
|
|
612
|
+
security_logger.log_dangerous_path_detected(
|
|
613
|
+
path=cwd_str,
|
|
614
|
+
dangerous_component=f"pattern:{detected_pattern}",
|
|
615
|
+
context="subprocess_cwd_validation",
|
|
616
|
+
)
|
|
617
|
+
raise ExecutionError(
|
|
618
|
+
message=f"Dangerous subprocess working directory pattern: {cwd_str}",
|
|
619
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
return validated_cwd
|
|
623
|
+
|
|
624
|
+
@classmethod
|
|
625
|
+
def validate_executable_path(cls, executable: str | Path) -> Path:
|
|
626
|
+
"""
|
|
627
|
+
Validate executable path for subprocess execution.
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
executable: Executable path or name
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
Validated Path object
|
|
634
|
+
|
|
635
|
+
Raises:
|
|
636
|
+
ExecutionError: If executable is dangerous or invalid
|
|
637
|
+
"""
|
|
638
|
+
exec_path = Path(executable)
|
|
639
|
+
|
|
640
|
+
# If it's just a command name, don't validate as full path
|
|
641
|
+
if not str(executable).startswith(("/", "./", "../")):
|
|
642
|
+
return exec_path
|
|
643
|
+
|
|
644
|
+
# For full paths, apply full validation
|
|
645
|
+
validated_exec = SecurePathValidator.validate_safe_path(exec_path)
|
|
646
|
+
|
|
647
|
+
# Additional checks for executable paths
|
|
648
|
+
exec_str = str(validated_exec)
|
|
649
|
+
|
|
650
|
+
# Check if trying to execute system-critical files
|
|
651
|
+
dangerous_executables = {
|
|
652
|
+
"/usr/bin/sudo",
|
|
653
|
+
"/bin/sudo",
|
|
654
|
+
"/usr/bin/su",
|
|
655
|
+
"/bin/su",
|
|
656
|
+
"/usr/bin/passwd",
|
|
657
|
+
"/bin/passwd",
|
|
658
|
+
"/usr/sbin/visudo",
|
|
659
|
+
"/usr/bin/ssh",
|
|
660
|
+
"/usr/bin/scp",
|
|
661
|
+
"/usr/bin/rsync",
|
|
662
|
+
"/bin/rm",
|
|
663
|
+
"/usr/bin/rm",
|
|
664
|
+
"/bin/rmdir",
|
|
665
|
+
"/usr/bin/rmdir",
|
|
666
|
+
"/sbin/reboot",
|
|
667
|
+
"/sbin/shutdown",
|
|
668
|
+
"/usr/sbin/reboot",
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if exec_str in dangerous_executables:
|
|
672
|
+
security_logger = get_security_logger()
|
|
673
|
+
security_logger.log_dangerous_path_detected(
|
|
674
|
+
path=exec_str,
|
|
675
|
+
dangerous_component="dangerous_executable",
|
|
676
|
+
context="subprocess_executable_validation",
|
|
677
|
+
)
|
|
678
|
+
raise ExecutionError(
|
|
679
|
+
message=f"Dangerous executable blocked: {exec_str}",
|
|
680
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
return validated_exec
|