crackerjack 0.31.10__py3-none-any.whl → 0.31.13__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 +50 -9
  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.13.dist-info}/METADATA +197 -12
  150. crackerjack-0.31.13.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.13.dist-info}/WHEEL +0 -0
  154. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/entry_points.txt +0 -0
  155. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,605 @@
1
+ """
2
+ Secure subprocess execution with environment sanitization and command validation.
3
+
4
+ This module provides production-ready security for all subprocess operations in Crackerjack,
5
+ implementing comprehensive validation, sanitization, and logging to prevent injection attacks.
6
+ """
7
+
8
+ import os
9
+ import re
10
+ import subprocess
11
+ import time
12
+ import typing as t
13
+ from pathlib import Path
14
+
15
+ from .security_logger import SecurityEventLevel, SecurityEventType, get_security_logger
16
+
17
+
18
+ class SecurityError(Exception):
19
+ """Raised when a security violation is detected."""
20
+
21
+ pass
22
+
23
+
24
+ class CommandValidationError(SecurityError):
25
+ """Raised when command validation fails."""
26
+
27
+ pass
28
+
29
+
30
+ class EnvironmentValidationError(SecurityError):
31
+ """Raised when environment validation fails."""
32
+
33
+ pass
34
+
35
+
36
+ class SubprocessSecurityConfig:
37
+ """Configuration for secure subprocess execution."""
38
+
39
+ def __init__(
40
+ self,
41
+ max_command_length: int = 10000,
42
+ max_arg_length: int = 4096,
43
+ max_env_var_length: int = 32768,
44
+ max_env_vars: int = 1000,
45
+ allowed_executables: set[str] | None = None,
46
+ blocked_executables: set[str] | None = None,
47
+ max_timeout: float = 3600, # 1 hour max
48
+ enable_path_validation: bool = True,
49
+ enable_command_logging: bool = True,
50
+ ):
51
+ self.max_command_length = max_command_length
52
+ self.max_arg_length = max_arg_length
53
+ self.max_env_var_length = max_env_var_length
54
+ self.max_env_vars = max_env_vars
55
+ self.allowed_executables = allowed_executables or set()
56
+ self.blocked_executables = blocked_executables or {
57
+ "rm",
58
+ "rmdir",
59
+ "del",
60
+ "format",
61
+ "fdisk",
62
+ "mkfs",
63
+ "dd",
64
+ "shred",
65
+ "wipe",
66
+ "nc",
67
+ "netcat",
68
+ "telnet",
69
+ "ftp",
70
+ "tftp",
71
+ "curl",
72
+ "wget",
73
+ "ssh",
74
+ "scp",
75
+ "rsync",
76
+ "sudo",
77
+ "su",
78
+ "doas",
79
+ "eval",
80
+ "exec",
81
+ "source",
82
+ ".",
83
+ "bash",
84
+ "sh",
85
+ "zsh",
86
+ "fish",
87
+ "csh",
88
+ }
89
+ self.max_timeout = max_timeout
90
+ self.enable_path_validation = enable_path_validation
91
+ self.enable_command_logging = enable_command_logging
92
+
93
+
94
+ class SecureSubprocessExecutor:
95
+ """Secure subprocess executor with comprehensive validation and logging."""
96
+
97
+ def __init__(self, config: SubprocessSecurityConfig | None = None):
98
+ self.config = config or SubprocessSecurityConfig()
99
+ self.security_logger = get_security_logger()
100
+
101
+ # Dangerous patterns for command injection detection
102
+ self.dangerous_patterns = [
103
+ r"[;&|`$(){}[\]<>*?~]", # Shell metacharacters
104
+ r"\.\./", # Path traversal
105
+ r"\$\{.*\}", # Variable expansion
106
+ r"`.*`", # Command substitution
107
+ r"\$\(.*\)", # Command substitution
108
+ r">\s*/", # Redirect to system paths
109
+ r"<\s*/", # Redirect from system paths
110
+ ]
111
+
112
+ # Environment variables that should never be passed through
113
+ self.dangerous_env_vars = {
114
+ "LD_PRELOAD",
115
+ "DYLD_INSERT_LIBRARIES",
116
+ "DYLD_LIBRARY_PATH",
117
+ "LD_LIBRARY_PATH",
118
+ "PYTHONPATH",
119
+ "PATH",
120
+ "IFS",
121
+ "PS4",
122
+ "BASH_ENV",
123
+ "ENV",
124
+ "SHELLOPTS",
125
+ "BASHOPTS",
126
+ }
127
+
128
+ # Minimal safe environment variables
129
+ self.safe_env_vars = {
130
+ "HOME",
131
+ "USER",
132
+ "USERNAME",
133
+ "LOGNAME",
134
+ "LANG",
135
+ "LC_ALL",
136
+ "LC_CTYPE",
137
+ "TERM",
138
+ "TMPDIR",
139
+ "TMP",
140
+ "TEMP",
141
+ }
142
+
143
+ def execute_secure(
144
+ self,
145
+ command: list[str],
146
+ cwd: Path | str | None = None,
147
+ env: dict[str, str] | None = None,
148
+ timeout: float | None = None,
149
+ input_data: str | bytes | None = None,
150
+ capture_output: bool = True,
151
+ text: bool = True,
152
+ check: bool = False,
153
+ **kwargs: t.Any,
154
+ ) -> subprocess.CompletedProcess[str]:
155
+ """
156
+ Execute a subprocess with comprehensive security validation.
157
+
158
+ Args:
159
+ command: Command and arguments as list
160
+ cwd: Working directory (validated for path traversal)
161
+ env: Environment variables (will be sanitized)
162
+ timeout: Maximum execution time
163
+ input_data: Input to pass to subprocess
164
+ capture_output: Whether to capture stdout/stderr
165
+ text: Whether to use text mode
166
+ check: Whether to raise on non-zero exit
167
+ **kwargs: Additional subprocess.run arguments
168
+
169
+ Returns:
170
+ CompletedProcess result
171
+
172
+ Raises:
173
+ SecurityError: If security validation fails
174
+ CommandValidationError: If command validation fails
175
+ EnvironmentValidationError: If environment validation fails
176
+ """
177
+ start_time = time.time()
178
+
179
+ try:
180
+ return self._execute_with_validation(
181
+ command,
182
+ cwd,
183
+ env,
184
+ timeout,
185
+ input_data,
186
+ capture_output,
187
+ text,
188
+ check,
189
+ kwargs,
190
+ start_time,
191
+ )
192
+
193
+ except subprocess.TimeoutExpired:
194
+ self._handle_timeout_error(command, timeout, start_time)
195
+ raise
196
+
197
+ except subprocess.CalledProcessError as e:
198
+ self._handle_process_error(command, e)
199
+ raise
200
+
201
+ except Exception as e:
202
+ self._handle_unexpected_error(command, e)
203
+ raise
204
+
205
+ def _execute_with_validation(
206
+ self,
207
+ command: list[str],
208
+ cwd: Path | str | None,
209
+ env: dict[str, str] | None,
210
+ timeout: float | None,
211
+ input_data: str | bytes | None,
212
+ capture_output: bool,
213
+ text: bool,
214
+ check: bool,
215
+ kwargs: dict[str, t.Any],
216
+ start_time: float,
217
+ ) -> subprocess.CompletedProcess[str]:
218
+ """Execute subprocess with validation and logging."""
219
+ # Validate and sanitize all inputs
220
+ execution_params = self._prepare_execution_params(command, cwd, env, timeout)
221
+
222
+ # Log and execute subprocess
223
+ result = self._execute_subprocess(
224
+ execution_params, input_data, capture_output, text, check, kwargs
225
+ )
226
+
227
+ # Log success
228
+ self._log_successful_execution(execution_params, result, start_time)
229
+ return result
230
+
231
+ def _prepare_execution_params(
232
+ self,
233
+ command: list[str],
234
+ cwd: Path | str | None,
235
+ env: dict[str, str] | None,
236
+ timeout: float | None,
237
+ ) -> dict[str, t.Any]:
238
+ """Prepare and validate all execution parameters."""
239
+ return {
240
+ "command": self._validate_command(command),
241
+ "cwd": self._validate_cwd(cwd),
242
+ "env": self._sanitize_environment(env),
243
+ "timeout": self._validate_timeout(timeout),
244
+ }
245
+
246
+ def _execute_subprocess(
247
+ self,
248
+ params: dict[str, t.Any],
249
+ input_data: str | bytes | None,
250
+ capture_output: bool,
251
+ text: bool,
252
+ check: bool,
253
+ kwargs: dict[str, t.Any],
254
+ ) -> subprocess.CompletedProcess[str]:
255
+ """Execute subprocess with validated parameters."""
256
+ if self.config.enable_command_logging:
257
+ self.security_logger.log_subprocess_execution(
258
+ command=params["command"],
259
+ cwd=str(params["cwd"]) if params["cwd"] else None,
260
+ env_vars_count=len(params["env"]),
261
+ timeout=params["timeout"],
262
+ )
263
+
264
+ return subprocess.run(
265
+ params["command"],
266
+ cwd=params["cwd"],
267
+ env=params["env"],
268
+ timeout=params["timeout"],
269
+ input=input_data,
270
+ capture_output=capture_output,
271
+ text=text,
272
+ check=check,
273
+ **kwargs,
274
+ )
275
+
276
+ def _log_successful_execution(
277
+ self,
278
+ params: dict[str, t.Any],
279
+ result: subprocess.CompletedProcess[str],
280
+ start_time: float,
281
+ ) -> None:
282
+ """Log successful subprocess execution."""
283
+ execution_time = time.time() - start_time
284
+ if self.config.enable_command_logging:
285
+ self.security_logger.log_security_event(
286
+ SecurityEventType.SUBPROCESS_EXECUTION,
287
+ SecurityEventLevel.LOW,
288
+ f"Subprocess completed successfully in {execution_time:.2f}s",
289
+ command_preview=params["command"][:3],
290
+ execution_time=execution_time,
291
+ exit_code=result.returncode,
292
+ )
293
+
294
+ def _handle_timeout_error(
295
+ self, command: list[str], timeout: float | None, start_time: float
296
+ ) -> None:
297
+ """Handle subprocess timeout errors."""
298
+ execution_time = time.time() - start_time
299
+ self.security_logger.log_subprocess_timeout(
300
+ command=command,
301
+ timeout_seconds=timeout or self.config.max_timeout,
302
+ actual_duration=execution_time,
303
+ )
304
+
305
+ def _handle_process_error(
306
+ self, command: list[str], error: subprocess.CalledProcessError
307
+ ) -> None:
308
+ """Handle subprocess called process errors."""
309
+ self.security_logger.log_subprocess_failure(
310
+ command=command,
311
+ exit_code=error.returncode,
312
+ error_output=str(error.stderr)[:200] if error.stderr else "",
313
+ )
314
+
315
+ def _handle_unexpected_error(self, command: list[str], error: Exception) -> None:
316
+ """Handle unexpected subprocess errors."""
317
+ self.security_logger.log_security_event(
318
+ SecurityEventType.SUBPROCESS_FAILURE,
319
+ SecurityEventLevel.HIGH,
320
+ f"Unexpected subprocess error: {str(error)[:200]}",
321
+ command_preview=command[:3] if command else [],
322
+ error_type=type(error).__name__,
323
+ error_message=str(error)[:200],
324
+ )
325
+
326
+ def _validate_command(self, command: list[str]) -> list[str]:
327
+ """Validate command arguments for security issues."""
328
+ self._validate_command_structure(command)
329
+
330
+ validated_command, issues = self._validate_command_arguments(command)
331
+ self._validate_executable_permissions(validated_command, issues)
332
+
333
+ self._handle_validation_results(command, issues)
334
+ return validated_command
335
+
336
+ def _validate_command_structure(self, command: list[str]) -> None:
337
+ """Validate basic command structure."""
338
+ if not command:
339
+ raise CommandValidationError("Command cannot be empty")
340
+
341
+ # Check overall command length
342
+ total_length = sum(len(arg) for arg in command)
343
+ if total_length > self.config.max_command_length:
344
+ raise CommandValidationError(
345
+ f"Command too long: {total_length} > {self.config.max_command_length}"
346
+ )
347
+
348
+ def _validate_command_arguments(
349
+ self, command: list[str]
350
+ ) -> tuple[list[str], list[str]]:
351
+ """Validate individual command arguments."""
352
+ validated_command = []
353
+ issues = []
354
+
355
+ for i, arg in enumerate(command):
356
+ # Check argument length
357
+ if len(arg) > self.config.max_arg_length:
358
+ issues.append(
359
+ f"Argument {i} too long: {len(arg)} > {self.config.max_arg_length}"
360
+ )
361
+ continue
362
+
363
+ # Check for injection patterns
364
+ if self._has_dangerous_patterns(arg, i, issues):
365
+ continue
366
+
367
+ validated_command.append(arg)
368
+
369
+ return validated_command, issues
370
+
371
+ def _has_dangerous_patterns(self, arg: str, index: int, issues: list[str]) -> bool:
372
+ """Check if argument has dangerous patterns."""
373
+ for pattern in self.dangerous_patterns:
374
+ if re.search(pattern, arg):
375
+ issues.append(
376
+ f"Dangerous pattern '{pattern}' in argument {index}: {arg[:50]}"
377
+ )
378
+ return True
379
+ return False
380
+
381
+ def _validate_executable_permissions(
382
+ self, validated_command: list[str], issues: list[str]
383
+ ) -> None:
384
+ """Validate executable allowlist/blocklist."""
385
+ if not validated_command:
386
+ return
387
+
388
+ executable = Path(validated_command[0]).name
389
+
390
+ if (
391
+ self.config.allowed_executables
392
+ and executable not in self.config.allowed_executables
393
+ ):
394
+ issues.append(f"Executable '{executable}' not in allowlist")
395
+
396
+ if executable in self.config.blocked_executables:
397
+ issues.append(f"Executable '{executable}' is blocked")
398
+
399
+ def _handle_validation_results(self, command: list[str], issues: list[str]) -> None:
400
+ """Handle validation results and logging."""
401
+ validation_passed = len(issues) == 0
402
+ if self.config.enable_command_logging:
403
+ self.security_logger.log_subprocess_command_validation(
404
+ command=command,
405
+ validation_result=validation_passed,
406
+ issues=issues,
407
+ )
408
+
409
+ if issues:
410
+ # Block dangerous commands
411
+ self.security_logger.log_dangerous_command_blocked(
412
+ command=command,
413
+ reason="Command validation failed",
414
+ dangerous_patterns=issues,
415
+ )
416
+ raise CommandValidationError(
417
+ f"Command validation failed: {'; '.join(issues)}"
418
+ )
419
+
420
+ def _validate_cwd(self, cwd: Path | str | None) -> Path | None:
421
+ """Validate working directory for path traversal."""
422
+ if cwd is None:
423
+ return None
424
+
425
+ if not self.config.enable_path_validation:
426
+ return Path(cwd) if isinstance(cwd, str) else cwd
427
+
428
+ cwd_path = Path(cwd) if isinstance(cwd, str) else cwd
429
+
430
+ try:
431
+ # Resolve to absolute path and check for traversal
432
+ resolved_path = cwd_path.resolve()
433
+
434
+ # Check for dangerous path components
435
+ path_str = str(resolved_path)
436
+ if ".." in path_str or path_str.startswith(
437
+ ("/etc", "/usr/bin", "/bin", "/sbin")
438
+ ):
439
+ self.security_logger.log_path_traversal_attempt(
440
+ attempted_path=path_str,
441
+ base_directory=None,
442
+ )
443
+ raise CommandValidationError(f"Dangerous working directory: {path_str}")
444
+
445
+ return resolved_path
446
+
447
+ except (OSError, ValueError) as e:
448
+ raise CommandValidationError(f"Invalid working directory '{cwd}': {e}")
449
+
450
+ def _sanitize_environment(self, env: dict[str, str] | None) -> dict[str, str]:
451
+ """Sanitize environment variables."""
452
+ if env is None:
453
+ env = os.environ.copy()
454
+
455
+ self._validate_environment_size(env)
456
+
457
+ filtered_vars = []
458
+ sanitized_env = self._filter_environment_variables(env, filtered_vars)
459
+
460
+ self._add_safe_environment_variables(sanitized_env)
461
+ self._log_environment_sanitization(len(env), len(sanitized_env), filtered_vars)
462
+
463
+ return sanitized_env
464
+
465
+ def _validate_environment_size(self, env: dict[str, str]) -> None:
466
+ """Validate environment variable count limits."""
467
+ if len(env) > self.config.max_env_vars:
468
+ self.security_logger.log_security_event(
469
+ SecurityEventType.INPUT_SIZE_EXCEEDED,
470
+ SecurityEventLevel.HIGH,
471
+ f"Too many environment variables: {len(env)} > {self.config.max_env_vars}",
472
+ actual_count=len(env),
473
+ max_count=self.config.max_env_vars,
474
+ )
475
+ raise EnvironmentValidationError(
476
+ f"Too many environment variables: {len(env)} > {self.config.max_env_vars}"
477
+ )
478
+
479
+ def _filter_environment_variables(
480
+ self, env: dict[str, str], filtered_vars: list[str]
481
+ ) -> dict[str, str]:
482
+ """Filter environment variables for security."""
483
+ sanitized_env = {}
484
+
485
+ for key, value in env.items():
486
+ if self._is_dangerous_environment_key(key, value, filtered_vars):
487
+ continue
488
+
489
+ if self._is_environment_value_too_long(key, value, filtered_vars):
490
+ continue
491
+
492
+ if self._has_environment_injection(key, value, filtered_vars):
493
+ continue
494
+
495
+ sanitized_env[key] = value
496
+
497
+ return sanitized_env
498
+
499
+ def _is_dangerous_environment_key(
500
+ self, key: str, value: str, filtered_vars: list[str]
501
+ ) -> bool:
502
+ """Check if environment key is dangerous."""
503
+ if key in self.dangerous_env_vars:
504
+ filtered_vars.append(key)
505
+ self.security_logger.log_environment_variable_filtered(
506
+ variable_name=key,
507
+ reason="dangerous environment variable",
508
+ value_preview=value[:50] if value else "",
509
+ )
510
+ return True
511
+ return False
512
+
513
+ def _is_environment_value_too_long(
514
+ self, key: str, value: str, filtered_vars: list[str]
515
+ ) -> bool:
516
+ """Check if environment value exceeds length limits."""
517
+ if len(value) > self.config.max_env_var_length:
518
+ filtered_vars.append(key)
519
+ self.security_logger.log_environment_variable_filtered(
520
+ variable_name=key,
521
+ reason=f"value too long: {len(value)} > {self.config.max_env_var_length}",
522
+ value_preview=value[:50],
523
+ )
524
+ return True
525
+ return False
526
+
527
+ def _has_environment_injection(
528
+ self, key: str, value: str, filtered_vars: list[str]
529
+ ) -> bool:
530
+ """Check if environment value has injection patterns."""
531
+ for pattern in self.dangerous_patterns[:3]: # Check first 3 most dangerous
532
+ if re.search(pattern, value):
533
+ filtered_vars.append(key)
534
+ self.security_logger.log_environment_variable_filtered(
535
+ variable_name=key,
536
+ reason=f"dangerous pattern '{pattern}' in value",
537
+ value_preview=value[:50],
538
+ )
539
+ return True
540
+ return False
541
+
542
+ def _add_safe_environment_variables(self, sanitized_env: dict[str, str]) -> None:
543
+ """Add essential safe environment variables."""
544
+ for safe_var in self.safe_env_vars:
545
+ if safe_var not in sanitized_env and safe_var in os.environ:
546
+ sanitized_env[safe_var] = os.environ[safe_var]
547
+
548
+ def _log_environment_sanitization(
549
+ self, original_count: int, sanitized_count: int, filtered_vars: list[str]
550
+ ) -> None:
551
+ """Log environment sanitization results."""
552
+ if self.config.enable_command_logging:
553
+ self.security_logger.log_subprocess_environment_sanitized(
554
+ original_count=original_count,
555
+ sanitized_count=sanitized_count,
556
+ filtered_vars=filtered_vars,
557
+ )
558
+
559
+ def _validate_timeout(self, timeout: float | None) -> float | None:
560
+ """Validate timeout value."""
561
+ if timeout is None:
562
+ return None
563
+
564
+ if timeout <= 0:
565
+ raise CommandValidationError(f"Timeout must be positive: {timeout}")
566
+
567
+ if timeout > self.config.max_timeout:
568
+ self.security_logger.log_security_event(
569
+ SecurityEventType.INPUT_SIZE_EXCEEDED,
570
+ SecurityEventLevel.MEDIUM,
571
+ f"Timeout too large: {timeout} > {self.config.max_timeout}",
572
+ requested_timeout=timeout,
573
+ max_timeout=self.config.max_timeout,
574
+ )
575
+ raise CommandValidationError(
576
+ f"Timeout too large: {timeout} > {self.config.max_timeout}"
577
+ )
578
+
579
+ return timeout
580
+
581
+
582
+ # Global secure executor instance
583
+ _global_executor: SecureSubprocessExecutor | None = None
584
+
585
+
586
+ def get_secure_executor(
587
+ config: SubprocessSecurityConfig | None = None,
588
+ ) -> SecureSubprocessExecutor:
589
+ """Get the global secure subprocess executor."""
590
+ global _global_executor
591
+ if _global_executor is None:
592
+ _global_executor = SecureSubprocessExecutor(config)
593
+ return _global_executor
594
+
595
+
596
+ def execute_secure_subprocess(
597
+ command: list[str],
598
+ **kwargs: t.Any,
599
+ ) -> subprocess.CompletedProcess[str]:
600
+ """
601
+ Convenience function for secure subprocess execution.
602
+
603
+ This is the recommended way to execute subprocesses in Crackerjack.
604
+ """
605
+ return get_secure_executor().execute_secure(command, **kwargs)
@@ -1,19 +1,20 @@
1
1
  import os
2
- import re
3
2
  import tempfile
4
3
  from contextlib import suppress
5
4
  from pathlib import Path
6
5
 
7
6
  from crackerjack.errors import FileError, SecurityError
7
+ from crackerjack.services.regex_patterns import SAFE_PATTERNS
8
8
 
9
9
 
10
10
  class SecurityService:
11
- TOKEN_PATTERNS = [
12
- (r"pypi-[a-zA-Z0-9_-]{12,}", "pypi-****"),
13
- (r"ghp_[a-zA-Z0-9]{20,}", "ghp_****"),
14
- (r"[a-zA-Z0-9_-]{20,}", "****"),
15
- (r"(token[=:]\s*)['\"][^'\"]+['\"]", r"\1'****'"),
16
- (r"(password[=:]\s*)['\"][^'\"]+['\"]", r"\1'****'"),
11
+ # Security token masking patterns - now using validated patterns from regex_patterns.py
12
+ TOKEN_PATTERN_NAMES = [
13
+ "mask_pypi_token",
14
+ "mask_github_token",
15
+ "mask_generic_long_token",
16
+ "mask_token_assignment",
17
+ "mask_password_assignment",
17
18
  ]
18
19
 
19
20
  SENSITIVE_ENV_VARS = {
@@ -27,11 +28,31 @@ class SecurityService:
27
28
  }
28
29
 
29
30
  def mask_tokens(self, text: str) -> str:
31
+ """
32
+ Mask sensitive tokens in text using validated regex patterns.
33
+
34
+ This method applies security token masking patterns to hide:
35
+ - PyPI authentication tokens (pypi-*)
36
+ - GitHub personal access tokens (ghp_*)
37
+ - Generic long tokens (32+ characters)
38
+ - Token assignments (token="value")
39
+ - Password assignments (password="value")
40
+ - Environment variable values
41
+
42
+ Returns masked text with sensitive data replaced by "**** or similar.
43
+ """
30
44
  if not text:
31
45
  return text
46
+
32
47
  masked_text = text
33
- for pattern, replacement in self.TOKEN_PATTERNS:
34
- masked_text = re.sub(pattern, replacement, masked_text, flags=re.IGNORECASE)
48
+
49
+ # Apply validated token masking patterns
50
+ for pattern_name in self.TOKEN_PATTERN_NAMES:
51
+ if pattern_name in SAFE_PATTERNS:
52
+ pattern = SAFE_PATTERNS[pattern_name]
53
+ masked_text = pattern.apply(masked_text)
54
+
55
+ # Also mask sensitive environment variable values
35
56
  for env_var in self.SENSITIVE_ENV_VARS:
36
57
  value = os.getenv(env_var)
37
58
  if value and len(value) > 8:
@@ -116,7 +137,7 @@ class SecurityService:
116
137
  if any(sensitive in key.upper() for sensitive in self.SENSITIVE_ENV_VARS):
117
138
  if value:
118
139
  env_summary[key] = (
119
- "****" if len(value) <= 8 else f"{value[:2]}...{value[-2:]}"
140
+ "* ** *" if len(value) <= 8 else f"{value[:2]}...{value[-2:]}"
120
141
  )
121
142
  else:
122
143
  env_summary[key] = "(empty)"
@@ -126,14 +147,30 @@ class SecurityService:
126
147
  return env_summary
127
148
 
128
149
  def validate_token_format(self, token: str, token_type: str | None = None) -> bool:
150
+ """
151
+ Validate token format for known token types.
152
+
153
+ Args:
154
+ token: The token string to validate
155
+ token_type: Optional token type ("pypi", "github", or None)
156
+
157
+ Returns:
158
+ True if the token appears to be valid for the specified type
159
+ """
129
160
  if not token:
130
161
  return False
131
162
  if len(token) < 8:
132
163
  return False
164
+
133
165
  if token_type and token_type.lower() == "pypi":
166
+ # PyPI tokens start with "pypi-" (not "pypi -" which was a typo)
134
167
  return token.startswith("pypi-") and len(token) >= 16
168
+
135
169
  if token_type and token_type.lower() == "github":
170
+ # GitHub personal access tokens: ghp_ + 36 chars = 40 total
136
171
  return token.startswith("ghp_") and len(token) == 40
172
+
173
+ # Generic validation for unknown token types
137
174
  return len(token) >= 16 and not token.isspace()
138
175
 
139
176
  def create_secure_command_env(