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.

Files changed (155) hide show
  1. crackerjack/CLAUDE.md +288 -705
  2. crackerjack/__main__.py +22 -8
  3. crackerjack/agents/__init__.py +0 -3
  4. crackerjack/agents/architect_agent.py +0 -43
  5. crackerjack/agents/base.py +1 -9
  6. crackerjack/agents/coordinator.py +2 -148
  7. crackerjack/agents/documentation_agent.py +109 -81
  8. crackerjack/agents/dry_agent.py +122 -97
  9. crackerjack/agents/formatting_agent.py +3 -16
  10. crackerjack/agents/import_optimization_agent.py +1174 -130
  11. crackerjack/agents/performance_agent.py +956 -188
  12. crackerjack/agents/performance_helpers.py +229 -0
  13. crackerjack/agents/proactive_agent.py +1 -48
  14. crackerjack/agents/refactoring_agent.py +516 -246
  15. crackerjack/agents/refactoring_helpers.py +282 -0
  16. crackerjack/agents/security_agent.py +393 -90
  17. crackerjack/agents/test_creation_agent.py +1776 -120
  18. crackerjack/agents/test_specialist_agent.py +59 -15
  19. crackerjack/agents/tracker.py +0 -102
  20. crackerjack/api.py +145 -37
  21. crackerjack/cli/handlers.py +48 -30
  22. crackerjack/cli/interactive.py +11 -11
  23. crackerjack/cli/options.py +66 -4
  24. crackerjack/code_cleaner.py +808 -148
  25. crackerjack/config/global_lock_config.py +110 -0
  26. crackerjack/config/hooks.py +43 -64
  27. crackerjack/core/async_workflow_orchestrator.py +247 -97
  28. crackerjack/core/autofix_coordinator.py +192 -109
  29. crackerjack/core/enhanced_container.py +46 -63
  30. crackerjack/core/file_lifecycle.py +549 -0
  31. crackerjack/core/performance.py +9 -8
  32. crackerjack/core/performance_monitor.py +395 -0
  33. crackerjack/core/phase_coordinator.py +281 -94
  34. crackerjack/core/proactive_workflow.py +9 -58
  35. crackerjack/core/resource_manager.py +501 -0
  36. crackerjack/core/service_watchdog.py +490 -0
  37. crackerjack/core/session_coordinator.py +4 -8
  38. crackerjack/core/timeout_manager.py +504 -0
  39. crackerjack/core/websocket_lifecycle.py +475 -0
  40. crackerjack/core/workflow_orchestrator.py +343 -209
  41. crackerjack/dynamic_config.py +47 -6
  42. crackerjack/errors.py +3 -4
  43. crackerjack/executors/async_hook_executor.py +63 -13
  44. crackerjack/executors/cached_hook_executor.py +14 -14
  45. crackerjack/executors/hook_executor.py +100 -37
  46. crackerjack/executors/hook_lock_manager.py +856 -0
  47. crackerjack/executors/individual_hook_executor.py +120 -86
  48. crackerjack/intelligence/__init__.py +0 -7
  49. crackerjack/intelligence/adaptive_learning.py +13 -86
  50. crackerjack/intelligence/agent_orchestrator.py +15 -78
  51. crackerjack/intelligence/agent_registry.py +12 -59
  52. crackerjack/intelligence/agent_selector.py +31 -92
  53. crackerjack/intelligence/integration.py +1 -41
  54. crackerjack/interactive.py +9 -9
  55. crackerjack/managers/async_hook_manager.py +25 -8
  56. crackerjack/managers/hook_manager.py +9 -9
  57. crackerjack/managers/publish_manager.py +57 -59
  58. crackerjack/managers/test_command_builder.py +6 -36
  59. crackerjack/managers/test_executor.py +9 -61
  60. crackerjack/managers/test_manager.py +17 -63
  61. crackerjack/managers/test_manager_backup.py +77 -127
  62. crackerjack/managers/test_progress.py +4 -23
  63. crackerjack/mcp/cache.py +5 -12
  64. crackerjack/mcp/client_runner.py +10 -10
  65. crackerjack/mcp/context.py +64 -6
  66. crackerjack/mcp/dashboard.py +14 -11
  67. crackerjack/mcp/enhanced_progress_monitor.py +55 -55
  68. crackerjack/mcp/file_monitor.py +72 -42
  69. crackerjack/mcp/progress_components.py +103 -84
  70. crackerjack/mcp/progress_monitor.py +122 -49
  71. crackerjack/mcp/rate_limiter.py +12 -12
  72. crackerjack/mcp/server_core.py +16 -22
  73. crackerjack/mcp/service_watchdog.py +26 -26
  74. crackerjack/mcp/state.py +15 -0
  75. crackerjack/mcp/tools/core_tools.py +95 -39
  76. crackerjack/mcp/tools/error_analyzer.py +6 -32
  77. crackerjack/mcp/tools/execution_tools.py +1 -56
  78. crackerjack/mcp/tools/execution_tools_backup.py +35 -131
  79. crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
  80. crackerjack/mcp/tools/intelligence_tools.py +2 -55
  81. crackerjack/mcp/tools/monitoring_tools.py +308 -145
  82. crackerjack/mcp/tools/proactive_tools.py +12 -42
  83. crackerjack/mcp/tools/progress_tools.py +23 -15
  84. crackerjack/mcp/tools/utility_tools.py +3 -40
  85. crackerjack/mcp/tools/workflow_executor.py +40 -60
  86. crackerjack/mcp/websocket/app.py +0 -3
  87. crackerjack/mcp/websocket/endpoints.py +206 -268
  88. crackerjack/mcp/websocket/jobs.py +213 -66
  89. crackerjack/mcp/websocket/server.py +84 -6
  90. crackerjack/mcp/websocket/websocket_handler.py +137 -29
  91. crackerjack/models/config_adapter.py +3 -16
  92. crackerjack/models/protocols.py +162 -3
  93. crackerjack/models/resource_protocols.py +454 -0
  94. crackerjack/models/task.py +3 -3
  95. crackerjack/monitoring/__init__.py +0 -0
  96. crackerjack/monitoring/ai_agent_watchdog.py +25 -71
  97. crackerjack/monitoring/regression_prevention.py +28 -87
  98. crackerjack/orchestration/advanced_orchestrator.py +44 -78
  99. crackerjack/orchestration/coverage_improvement.py +10 -60
  100. crackerjack/orchestration/execution_strategies.py +16 -16
  101. crackerjack/orchestration/test_progress_streamer.py +61 -53
  102. crackerjack/plugins/base.py +1 -1
  103. crackerjack/plugins/managers.py +22 -20
  104. crackerjack/py313.py +65 -21
  105. crackerjack/services/backup_service.py +467 -0
  106. crackerjack/services/bounded_status_operations.py +627 -0
  107. crackerjack/services/cache.py +7 -9
  108. crackerjack/services/config.py +35 -52
  109. crackerjack/services/config_integrity.py +5 -16
  110. crackerjack/services/config_merge.py +542 -0
  111. crackerjack/services/contextual_ai_assistant.py +17 -19
  112. crackerjack/services/coverage_ratchet.py +44 -73
  113. crackerjack/services/debug.py +25 -39
  114. crackerjack/services/dependency_monitor.py +52 -50
  115. crackerjack/services/enhanced_filesystem.py +14 -11
  116. crackerjack/services/file_hasher.py +1 -1
  117. crackerjack/services/filesystem.py +1 -12
  118. crackerjack/services/git.py +71 -47
  119. crackerjack/services/health_metrics.py +31 -27
  120. crackerjack/services/initialization.py +276 -428
  121. crackerjack/services/input_validator.py +760 -0
  122. crackerjack/services/log_manager.py +16 -16
  123. crackerjack/services/logging.py +7 -6
  124. crackerjack/services/metrics.py +43 -43
  125. crackerjack/services/pattern_cache.py +2 -31
  126. crackerjack/services/pattern_detector.py +26 -63
  127. crackerjack/services/performance_benchmarks.py +20 -45
  128. crackerjack/services/regex_patterns.py +2887 -0
  129. crackerjack/services/regex_utils.py +537 -0
  130. crackerjack/services/secure_path_utils.py +683 -0
  131. crackerjack/services/secure_status_formatter.py +534 -0
  132. crackerjack/services/secure_subprocess.py +605 -0
  133. crackerjack/services/security.py +47 -10
  134. crackerjack/services/security_logger.py +492 -0
  135. crackerjack/services/server_manager.py +109 -50
  136. crackerjack/services/smart_scheduling.py +8 -25
  137. crackerjack/services/status_authentication.py +603 -0
  138. crackerjack/services/status_security_manager.py +442 -0
  139. crackerjack/services/thread_safe_status_collector.py +546 -0
  140. crackerjack/services/tool_version_service.py +1 -23
  141. crackerjack/services/unified_config.py +36 -58
  142. crackerjack/services/validation_rate_limiter.py +269 -0
  143. crackerjack/services/version_checker.py +9 -40
  144. crackerjack/services/websocket_resource_limiter.py +572 -0
  145. crackerjack/slash_commands/__init__.py +52 -2
  146. crackerjack/tools/__init__.py +0 -0
  147. crackerjack/tools/validate_input_validator_patterns.py +262 -0
  148. crackerjack/tools/validate_regex_patterns.py +198 -0
  149. {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
  150. crackerjack-0.31.12.dist-info/RECORD +178 -0
  151. crackerjack/cli/facade.py +0 -104
  152. crackerjack-0.31.10.dist-info/RECORD +0 -149
  153. {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
  154. {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
  155. {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