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.
- 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 +50 -9
- 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.13.dist-info}/METADATA +197 -12
- crackerjack-0.31.13.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.13.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/entry_points.txt +0 -0
- {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)
|
crackerjack/services/security.py
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
"
|
|
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(
|