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,760 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive input validation framework for security hardening.
|
|
3
|
+
|
|
4
|
+
This module provides defense-in-depth input validation to prevent:
|
|
5
|
+
- Command injection attacks (CWE-77)
|
|
6
|
+
- Path traversal attacks (CWE-22)
|
|
7
|
+
- SQL injection (CWE-89)
|
|
8
|
+
- JSON injection (CWE-91)
|
|
9
|
+
- DoS via malformed input (CWE-400)
|
|
10
|
+
- Code injection (CWE-94)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import typing as t
|
|
15
|
+
from functools import wraps
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, Field
|
|
19
|
+
|
|
20
|
+
from ..errors import ErrorCode, ExecutionError
|
|
21
|
+
from .regex_patterns import SAFE_PATTERNS
|
|
22
|
+
from .security_logger import (
|
|
23
|
+
SecurityEventLevel,
|
|
24
|
+
get_security_logger,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ValidationConfig(BaseModel):
|
|
29
|
+
"""Configuration for input validation rules."""
|
|
30
|
+
|
|
31
|
+
# String limits
|
|
32
|
+
MAX_STRING_LENGTH: int = Field(default=10000, ge=1)
|
|
33
|
+
MAX_PROJECT_NAME_LENGTH: int = Field(default=255, ge=1)
|
|
34
|
+
MAX_JOB_ID_LENGTH: int = Field(default=128, ge=1)
|
|
35
|
+
MAX_COMMAND_LENGTH: int = Field(default=1000, ge=1)
|
|
36
|
+
|
|
37
|
+
# JSON limits
|
|
38
|
+
MAX_JSON_SIZE: int = Field(default=1024 * 1024, ge=1) # 1MB
|
|
39
|
+
MAX_JSON_DEPTH: int = Field(default=10, ge=1)
|
|
40
|
+
|
|
41
|
+
# Rate limiting
|
|
42
|
+
MAX_VALIDATION_FAILURES_PER_MINUTE: int = Field(default=10, ge=1)
|
|
43
|
+
|
|
44
|
+
# Pattern validation
|
|
45
|
+
ALLOW_SHELL_METACHARACTERS: bool = Field(default=False)
|
|
46
|
+
STRICT_ALPHANUMERIC_MODE: bool = Field(default=False)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ValidationResult(BaseModel):
|
|
50
|
+
"""Result of input validation."""
|
|
51
|
+
|
|
52
|
+
valid: bool
|
|
53
|
+
sanitized_value: t.Any = None
|
|
54
|
+
error_message: str = ""
|
|
55
|
+
security_level: SecurityEventLevel = SecurityEventLevel.LOW
|
|
56
|
+
validation_type: str = ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class InputSanitizer:
|
|
60
|
+
"""Provides secure input sanitization utilities."""
|
|
61
|
+
|
|
62
|
+
# Shell metacharacters that could enable command injection
|
|
63
|
+
SHELL_METACHARACTERS = {
|
|
64
|
+
";",
|
|
65
|
+
"&",
|
|
66
|
+
"|",
|
|
67
|
+
"`",
|
|
68
|
+
"$",
|
|
69
|
+
"(",
|
|
70
|
+
")",
|
|
71
|
+
"<",
|
|
72
|
+
">",
|
|
73
|
+
"\n",
|
|
74
|
+
"\r",
|
|
75
|
+
"\\",
|
|
76
|
+
'"',
|
|
77
|
+
"'",
|
|
78
|
+
"*",
|
|
79
|
+
"?",
|
|
80
|
+
"[",
|
|
81
|
+
"]",
|
|
82
|
+
"{",
|
|
83
|
+
"}",
|
|
84
|
+
"~",
|
|
85
|
+
"^",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Dangerous path components
|
|
89
|
+
DANGEROUS_PATH_COMPONENTS = {
|
|
90
|
+
"..",
|
|
91
|
+
".",
|
|
92
|
+
"~",
|
|
93
|
+
"$",
|
|
94
|
+
"`",
|
|
95
|
+
";",
|
|
96
|
+
"&",
|
|
97
|
+
"|",
|
|
98
|
+
"<",
|
|
99
|
+
">",
|
|
100
|
+
"CON",
|
|
101
|
+
"PRN",
|
|
102
|
+
"AUX",
|
|
103
|
+
"NUL",
|
|
104
|
+
"COM1",
|
|
105
|
+
"COM2",
|
|
106
|
+
"COM3",
|
|
107
|
+
"COM4",
|
|
108
|
+
"COM5",
|
|
109
|
+
"COM6",
|
|
110
|
+
"COM7",
|
|
111
|
+
"COM8",
|
|
112
|
+
"COM9",
|
|
113
|
+
"LPT1",
|
|
114
|
+
"LPT2",
|
|
115
|
+
"LPT3",
|
|
116
|
+
"LPT4",
|
|
117
|
+
"LPT5",
|
|
118
|
+
"LPT6",
|
|
119
|
+
"LPT7",
|
|
120
|
+
"LPT8",
|
|
121
|
+
"LPT9",
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# NOTE: SQL and Code injection patterns now use centralized SAFE_PATTERNS
|
|
125
|
+
# from regex_patterns.py for security consistency and testing
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def sanitize_string(
|
|
129
|
+
cls,
|
|
130
|
+
value: t.Any,
|
|
131
|
+
max_length: int = 10000,
|
|
132
|
+
allow_shell_chars: bool = False,
|
|
133
|
+
strict_alphanumeric: bool = False,
|
|
134
|
+
) -> ValidationResult:
|
|
135
|
+
"""Sanitize string input with configurable restrictions."""
|
|
136
|
+
|
|
137
|
+
# Type validation
|
|
138
|
+
type_result = cls._validate_string_type(value)
|
|
139
|
+
if not type_result.valid:
|
|
140
|
+
return type_result
|
|
141
|
+
|
|
142
|
+
# Length validation
|
|
143
|
+
length_result = cls._validate_string_length(value, max_length)
|
|
144
|
+
if not length_result.valid:
|
|
145
|
+
return length_result
|
|
146
|
+
|
|
147
|
+
# Security validations
|
|
148
|
+
security_result = cls._validate_string_security(value, allow_shell_chars)
|
|
149
|
+
if not security_result.valid:
|
|
150
|
+
return security_result
|
|
151
|
+
|
|
152
|
+
# Pattern validations
|
|
153
|
+
pattern_result = cls._validate_string_patterns(value)
|
|
154
|
+
if not pattern_result.valid:
|
|
155
|
+
return pattern_result
|
|
156
|
+
|
|
157
|
+
# Strict alphanumeric mode
|
|
158
|
+
if strict_alphanumeric and not cls._is_strictly_alphanumeric(value):
|
|
159
|
+
return ValidationResult(
|
|
160
|
+
valid=False,
|
|
161
|
+
error_message="Only alphanumeric characters, hyphens, and underscores allowed",
|
|
162
|
+
security_level=SecurityEventLevel.MEDIUM,
|
|
163
|
+
validation_type="alphanumeric_only",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Basic sanitization (remove leading/trailing whitespace)
|
|
167
|
+
sanitized = value.strip()
|
|
168
|
+
|
|
169
|
+
return ValidationResult(
|
|
170
|
+
valid=True, sanitized_value=sanitized, validation_type="string_sanitization"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def _validate_string_type(cls, value: t.Any) -> ValidationResult:
|
|
175
|
+
"""Validate that the input is a string."""
|
|
176
|
+
if not isinstance(value, str):
|
|
177
|
+
return ValidationResult(
|
|
178
|
+
valid=False,
|
|
179
|
+
error_message=f"Expected string, got {type(value).__name__}",
|
|
180
|
+
security_level=SecurityEventLevel.MEDIUM,
|
|
181
|
+
validation_type="type_check",
|
|
182
|
+
)
|
|
183
|
+
return ValidationResult(valid=True, validation_type="type_check")
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def _validate_string_length(cls, value: str, max_length: int) -> ValidationResult:
|
|
187
|
+
"""Validate string length."""
|
|
188
|
+
if len(value) > max_length:
|
|
189
|
+
return ValidationResult(
|
|
190
|
+
valid=False,
|
|
191
|
+
error_message=f"String too long: {len(value)} > {max_length}",
|
|
192
|
+
security_level=SecurityEventLevel.HIGH,
|
|
193
|
+
validation_type="length_check",
|
|
194
|
+
)
|
|
195
|
+
return ValidationResult(valid=True, validation_type="length_check")
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def _validate_string_security(
|
|
199
|
+
cls, value: str, allow_shell_chars: bool
|
|
200
|
+
) -> ValidationResult:
|
|
201
|
+
"""Validate string security constraints."""
|
|
202
|
+
# Null byte injection check
|
|
203
|
+
if "\x00" in value:
|
|
204
|
+
return ValidationResult(
|
|
205
|
+
valid=False,
|
|
206
|
+
error_message="Null byte detected in input",
|
|
207
|
+
security_level=SecurityEventLevel.CRITICAL,
|
|
208
|
+
validation_type="null_byte_injection",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Control character check
|
|
212
|
+
if any(ord(c) < 32 and c not in "\t\n\r" for c in value):
|
|
213
|
+
return ValidationResult(
|
|
214
|
+
valid=False,
|
|
215
|
+
error_message="Control characters detected in input",
|
|
216
|
+
security_level=SecurityEventLevel.HIGH,
|
|
217
|
+
validation_type="control_chars",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Shell metacharacter check
|
|
221
|
+
if not allow_shell_chars:
|
|
222
|
+
found_chars = [c for c in value if c in cls.SHELL_METACHARACTERS]
|
|
223
|
+
if found_chars:
|
|
224
|
+
return ValidationResult(
|
|
225
|
+
valid=False,
|
|
226
|
+
error_message=f"Shell metacharacters detected: {found_chars}",
|
|
227
|
+
security_level=SecurityEventLevel.CRITICAL,
|
|
228
|
+
validation_type="shell_injection",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return ValidationResult(valid=True, validation_type="security_check")
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def _validate_string_patterns(cls, value: str) -> ValidationResult:
|
|
235
|
+
"""Validate string against security patterns."""
|
|
236
|
+
# SQL injection pattern check using SAFE_PATTERNS
|
|
237
|
+
sql_patterns = [
|
|
238
|
+
"validate_sql_injection_patterns",
|
|
239
|
+
"validate_sql_comment_patterns",
|
|
240
|
+
"validate_sql_boolean_injection",
|
|
241
|
+
"validate_sql_server_specific",
|
|
242
|
+
]
|
|
243
|
+
for pattern_name in sql_patterns:
|
|
244
|
+
pattern = SAFE_PATTERNS[pattern_name]
|
|
245
|
+
if pattern.test(value):
|
|
246
|
+
return ValidationResult(
|
|
247
|
+
valid=False,
|
|
248
|
+
error_message="SQL injection pattern detected",
|
|
249
|
+
security_level=SecurityEventLevel.CRITICAL,
|
|
250
|
+
validation_type="sql_injection",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Code injection pattern check using SAFE_PATTERNS
|
|
254
|
+
code_patterns = [
|
|
255
|
+
"validate_code_eval_injection",
|
|
256
|
+
"validate_code_dynamic_access",
|
|
257
|
+
"validate_code_system_commands",
|
|
258
|
+
"validate_code_compilation",
|
|
259
|
+
]
|
|
260
|
+
for pattern_name in code_patterns:
|
|
261
|
+
pattern = SAFE_PATTERNS[pattern_name]
|
|
262
|
+
if pattern.test(value):
|
|
263
|
+
return ValidationResult(
|
|
264
|
+
valid=False,
|
|
265
|
+
error_message="Code injection pattern detected",
|
|
266
|
+
security_level=SecurityEventLevel.CRITICAL,
|
|
267
|
+
validation_type="code_injection",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return ValidationResult(valid=True, validation_type="pattern_check")
|
|
271
|
+
|
|
272
|
+
@classmethod
|
|
273
|
+
def _is_strictly_alphanumeric(cls, value: str) -> bool:
|
|
274
|
+
"""Check if string is strictly alphanumeric with allowed characters."""
|
|
275
|
+
return value.replace("-", "").replace("_", "").isalnum()
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def sanitize_json(
|
|
279
|
+
cls, value: str, max_size: int = 1024 * 1024, max_depth: int = 10
|
|
280
|
+
) -> ValidationResult:
|
|
281
|
+
"""Sanitize JSON input with size and depth limits."""
|
|
282
|
+
|
|
283
|
+
if len(value) > max_size:
|
|
284
|
+
return ValidationResult(
|
|
285
|
+
valid=False,
|
|
286
|
+
error_message=f"JSON too large: {len(value)} > {max_size} bytes",
|
|
287
|
+
security_level=SecurityEventLevel.HIGH,
|
|
288
|
+
validation_type="json_size",
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
# Parse JSON to validate structure
|
|
293
|
+
parsed = json.loads(value)
|
|
294
|
+
|
|
295
|
+
# Check nesting depth
|
|
296
|
+
def check_depth(obj: t.Any, current_depth: int = 0) -> int:
|
|
297
|
+
if current_depth > max_depth:
|
|
298
|
+
return current_depth
|
|
299
|
+
|
|
300
|
+
if isinstance(obj, dict):
|
|
301
|
+
return (
|
|
302
|
+
max(check_depth(v, current_depth + 1) for v in obj.values())
|
|
303
|
+
if obj
|
|
304
|
+
else current_depth
|
|
305
|
+
)
|
|
306
|
+
elif isinstance(obj, list):
|
|
307
|
+
return (
|
|
308
|
+
max(check_depth(item, current_depth + 1) for item in obj)
|
|
309
|
+
if obj
|
|
310
|
+
else current_depth
|
|
311
|
+
)
|
|
312
|
+
return current_depth
|
|
313
|
+
|
|
314
|
+
actual_depth = check_depth(parsed)
|
|
315
|
+
if actual_depth > max_depth:
|
|
316
|
+
return ValidationResult(
|
|
317
|
+
valid=False,
|
|
318
|
+
error_message=f"JSON nesting too deep: {actual_depth} > {max_depth}",
|
|
319
|
+
security_level=SecurityEventLevel.HIGH,
|
|
320
|
+
validation_type="json_depth",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return ValidationResult(
|
|
324
|
+
valid=True, sanitized_value=parsed, validation_type="json_parsing"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
except json.JSONDecodeError as e:
|
|
328
|
+
return ValidationResult(
|
|
329
|
+
valid=False,
|
|
330
|
+
error_message=f"Invalid JSON: {e}",
|
|
331
|
+
security_level=SecurityEventLevel.MEDIUM,
|
|
332
|
+
validation_type="json_syntax",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
@classmethod
|
|
336
|
+
def sanitize_path(
|
|
337
|
+
cls,
|
|
338
|
+
value: str | Path,
|
|
339
|
+
base_directory: Path | None = None,
|
|
340
|
+
allow_absolute: bool = False,
|
|
341
|
+
) -> ValidationResult:
|
|
342
|
+
"""Sanitize file path with traversal protection."""
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
path = Path(value)
|
|
346
|
+
|
|
347
|
+
# Check for dangerous components in the original path before resolving
|
|
348
|
+
danger_result = cls._check_dangerous_components(path)
|
|
349
|
+
if not danger_result.valid:
|
|
350
|
+
return danger_result
|
|
351
|
+
|
|
352
|
+
# Handle base directory constraints
|
|
353
|
+
if base_directory:
|
|
354
|
+
base_result = cls._validate_base_directory(
|
|
355
|
+
path, base_directory, allow_absolute
|
|
356
|
+
)
|
|
357
|
+
if not base_result.valid:
|
|
358
|
+
return base_result
|
|
359
|
+
resolved = base_result.sanitized_value
|
|
360
|
+
else:
|
|
361
|
+
# Resolve to absolute path to eliminate .. components if no base directory
|
|
362
|
+
resolved = path.resolve()
|
|
363
|
+
|
|
364
|
+
# Check absolute path restrictions
|
|
365
|
+
absolute_result = cls._validate_absolute_path(
|
|
366
|
+
resolved, allow_absolute, base_directory
|
|
367
|
+
)
|
|
368
|
+
if not absolute_result.valid:
|
|
369
|
+
return absolute_result
|
|
370
|
+
|
|
371
|
+
return ValidationResult(
|
|
372
|
+
valid=True,
|
|
373
|
+
sanitized_value=resolved,
|
|
374
|
+
validation_type="path_sanitization",
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
except (OSError, ValueError) as e:
|
|
378
|
+
return ValidationResult(
|
|
379
|
+
valid=False,
|
|
380
|
+
error_message=f"Invalid path: {e}",
|
|
381
|
+
security_level=SecurityEventLevel.HIGH,
|
|
382
|
+
validation_type="path_syntax",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
@classmethod
|
|
386
|
+
def _check_dangerous_components(cls, path: Path) -> ValidationResult:
|
|
387
|
+
"""Check for dangerous components in the path."""
|
|
388
|
+
for part in path.parts:
|
|
389
|
+
if part.upper() in cls.DANGEROUS_PATH_COMPONENTS:
|
|
390
|
+
return ValidationResult(
|
|
391
|
+
valid=False,
|
|
392
|
+
error_message=f"Dangerous path component: {part}",
|
|
393
|
+
security_level=SecurityEventLevel.CRITICAL,
|
|
394
|
+
validation_type="path_traversal",
|
|
395
|
+
)
|
|
396
|
+
return ValidationResult(valid=True, validation_type="path_components")
|
|
397
|
+
|
|
398
|
+
@classmethod
|
|
399
|
+
def _validate_base_directory(
|
|
400
|
+
cls, path: Path, base_directory: Path, allow_absolute: bool
|
|
401
|
+
) -> ValidationResult:
|
|
402
|
+
"""Validate path against base directory constraints."""
|
|
403
|
+
base_resolved = base_directory.resolve()
|
|
404
|
+
|
|
405
|
+
# If the path is absolute and doesn't start with base directory, it's invalid
|
|
406
|
+
if path.is_absolute() and not str(path).startswith(str(base_resolved)):
|
|
407
|
+
return ValidationResult(
|
|
408
|
+
valid=False,
|
|
409
|
+
error_message=f"Path outside base directory: {path}",
|
|
410
|
+
security_level=SecurityEventLevel.CRITICAL,
|
|
411
|
+
validation_type="directory_escape",
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# If the path is relative, resolve it relative to base directory
|
|
415
|
+
if not path.is_absolute():
|
|
416
|
+
resolved = (base_resolved / path).resolve()
|
|
417
|
+
try:
|
|
418
|
+
resolved.relative_to(base_resolved)
|
|
419
|
+
except ValueError:
|
|
420
|
+
return ValidationResult(
|
|
421
|
+
valid=False,
|
|
422
|
+
error_message=f"Path outside base directory: {path}",
|
|
423
|
+
security_level=SecurityEventLevel.CRITICAL,
|
|
424
|
+
validation_type="directory_escape",
|
|
425
|
+
)
|
|
426
|
+
else:
|
|
427
|
+
# For absolute paths that start with base directory, resolve normally
|
|
428
|
+
resolved = path.resolve()
|
|
429
|
+
try:
|
|
430
|
+
resolved.relative_to(base_resolved)
|
|
431
|
+
except ValueError:
|
|
432
|
+
return ValidationResult(
|
|
433
|
+
valid=False,
|
|
434
|
+
error_message=f"Path outside base directory: {path}",
|
|
435
|
+
security_level=SecurityEventLevel.CRITICAL,
|
|
436
|
+
validation_type="directory_escape",
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
return ValidationResult(
|
|
440
|
+
valid=True, sanitized_value=resolved, validation_type="base_directory"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
@classmethod
|
|
444
|
+
def _validate_absolute_path(
|
|
445
|
+
cls, resolved: Path, allow_absolute: bool, base_directory: Path | None
|
|
446
|
+
) -> ValidationResult:
|
|
447
|
+
"""Validate absolute path restrictions."""
|
|
448
|
+
if not allow_absolute and resolved.is_absolute() and base_directory:
|
|
449
|
+
return ValidationResult(
|
|
450
|
+
valid=False,
|
|
451
|
+
error_message="Absolute paths not allowed",
|
|
452
|
+
security_level=SecurityEventLevel.HIGH,
|
|
453
|
+
validation_type="absolute_path",
|
|
454
|
+
)
|
|
455
|
+
return ValidationResult(valid=True, validation_type="absolute_path")
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class SecureInputValidator:
|
|
459
|
+
"""Main input validation class with security logging."""
|
|
460
|
+
|
|
461
|
+
def __init__(self, config: ValidationConfig | None = None):
|
|
462
|
+
self.config = config or ValidationConfig()
|
|
463
|
+
self.logger = get_security_logger()
|
|
464
|
+
self.sanitizer = InputSanitizer()
|
|
465
|
+
self._failure_counts: dict[str, int] = {}
|
|
466
|
+
|
|
467
|
+
def validate_project_name(self, name: str) -> ValidationResult:
|
|
468
|
+
"""Validate project name with security constraints."""
|
|
469
|
+
|
|
470
|
+
result = self.sanitizer.sanitize_string(
|
|
471
|
+
name,
|
|
472
|
+
max_length=self.config.MAX_PROJECT_NAME_LENGTH,
|
|
473
|
+
allow_shell_chars=False,
|
|
474
|
+
strict_alphanumeric=True,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
if not result.valid:
|
|
478
|
+
self._log_validation_failure(
|
|
479
|
+
"project_name", name, result.error_message, result.security_level
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
return result
|
|
483
|
+
|
|
484
|
+
def validate_job_id(self, job_id: str) -> ValidationResult:
|
|
485
|
+
"""Validate job ID with strict alphanumeric constraints."""
|
|
486
|
+
|
|
487
|
+
# Job IDs must be alphanumeric with hyphens only using SAFE_PATTERNS
|
|
488
|
+
job_id_pattern = SAFE_PATTERNS["validate_job_id_format"]
|
|
489
|
+
if not job_id_pattern.test(job_id):
|
|
490
|
+
result = ValidationResult(
|
|
491
|
+
valid=False,
|
|
492
|
+
error_message="Job ID must be alphanumeric with hyphens/underscores only",
|
|
493
|
+
security_level=SecurityEventLevel.HIGH,
|
|
494
|
+
validation_type="job_id_format",
|
|
495
|
+
)
|
|
496
|
+
self._log_validation_failure(
|
|
497
|
+
"job_id", job_id, result.error_message, result.security_level
|
|
498
|
+
)
|
|
499
|
+
return result
|
|
500
|
+
|
|
501
|
+
result = self.sanitizer.sanitize_string(
|
|
502
|
+
job_id,
|
|
503
|
+
max_length=self.config.MAX_JOB_ID_LENGTH,
|
|
504
|
+
allow_shell_chars=False,
|
|
505
|
+
strict_alphanumeric=True,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if not result.valid:
|
|
509
|
+
self._log_validation_failure(
|
|
510
|
+
"job_id", job_id, result.error_message, result.security_level
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
return result
|
|
514
|
+
|
|
515
|
+
def validate_command_args(self, args: t.Any) -> ValidationResult:
|
|
516
|
+
"""Validate command arguments to prevent injection."""
|
|
517
|
+
|
|
518
|
+
if isinstance(args, str):
|
|
519
|
+
result = self.sanitizer.sanitize_string(
|
|
520
|
+
args,
|
|
521
|
+
max_length=self.config.MAX_COMMAND_LENGTH,
|
|
522
|
+
allow_shell_chars=self.config.ALLOW_SHELL_METACHARACTERS,
|
|
523
|
+
)
|
|
524
|
+
elif isinstance(args, list):
|
|
525
|
+
# Validate each argument in the list
|
|
526
|
+
sanitized_args = []
|
|
527
|
+
for arg in args:
|
|
528
|
+
if not isinstance(arg, str):
|
|
529
|
+
result = ValidationResult(
|
|
530
|
+
valid=False,
|
|
531
|
+
error_message=f"Command argument must be string, got {type(arg).__name__}",
|
|
532
|
+
security_level=SecurityEventLevel.HIGH,
|
|
533
|
+
validation_type="command_arg_type",
|
|
534
|
+
)
|
|
535
|
+
break
|
|
536
|
+
|
|
537
|
+
arg_result = self.sanitizer.sanitize_string(
|
|
538
|
+
arg,
|
|
539
|
+
max_length=self.config.MAX_COMMAND_LENGTH,
|
|
540
|
+
allow_shell_chars=self.config.ALLOW_SHELL_METACHARACTERS,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
if not arg_result.valid:
|
|
544
|
+
result = arg_result
|
|
545
|
+
break
|
|
546
|
+
|
|
547
|
+
sanitized_args.append(arg_result.sanitized_value)
|
|
548
|
+
else:
|
|
549
|
+
result = ValidationResult(
|
|
550
|
+
valid=True,
|
|
551
|
+
sanitized_value=sanitized_args,
|
|
552
|
+
validation_type="command_args_list",
|
|
553
|
+
)
|
|
554
|
+
else:
|
|
555
|
+
result = ValidationResult(
|
|
556
|
+
valid=False,
|
|
557
|
+
error_message=f"Command args must be string or list, got {type(args).__name__}",
|
|
558
|
+
security_level=SecurityEventLevel.HIGH,
|
|
559
|
+
validation_type="command_args_type",
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
if not result.valid:
|
|
563
|
+
self._log_validation_failure(
|
|
564
|
+
"command_args", str(args), result.error_message, result.security_level
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
return result
|
|
568
|
+
|
|
569
|
+
def validate_json_payload(self, payload: str) -> ValidationResult:
|
|
570
|
+
"""Validate JSON payload with size and structure limits."""
|
|
571
|
+
|
|
572
|
+
result = self.sanitizer.sanitize_json(
|
|
573
|
+
payload,
|
|
574
|
+
max_size=self.config.MAX_JSON_SIZE,
|
|
575
|
+
max_depth=self.config.MAX_JSON_DEPTH,
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
if not result.valid:
|
|
579
|
+
self._log_validation_failure(
|
|
580
|
+
"json_payload",
|
|
581
|
+
payload[:100] + "...",
|
|
582
|
+
result.error_message,
|
|
583
|
+
result.security_level,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
return result
|
|
587
|
+
|
|
588
|
+
def validate_file_path(
|
|
589
|
+
self,
|
|
590
|
+
path: str | Path,
|
|
591
|
+
base_directory: Path | None = None,
|
|
592
|
+
allow_absolute: bool = False,
|
|
593
|
+
) -> ValidationResult:
|
|
594
|
+
"""Validate file path with traversal protection."""
|
|
595
|
+
|
|
596
|
+
result = self.sanitizer.sanitize_path(path, base_directory, allow_absolute)
|
|
597
|
+
|
|
598
|
+
if not result.valid:
|
|
599
|
+
self._log_validation_failure(
|
|
600
|
+
"file_path", str(path), result.error_message, result.security_level
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
return result
|
|
604
|
+
|
|
605
|
+
def validate_environment_var(self, name: str, value: str) -> ValidationResult:
|
|
606
|
+
"""Validate environment variable name and value."""
|
|
607
|
+
|
|
608
|
+
# Environment variable names must be valid identifiers using SAFE_PATTERNS
|
|
609
|
+
env_var_pattern = SAFE_PATTERNS["validate_env_var_name_format"]
|
|
610
|
+
if not env_var_pattern.test(name):
|
|
611
|
+
result = ValidationResult(
|
|
612
|
+
valid=False,
|
|
613
|
+
error_message="Invalid environment variable name format",
|
|
614
|
+
security_level=SecurityEventLevel.MEDIUM,
|
|
615
|
+
validation_type="env_var_name",
|
|
616
|
+
)
|
|
617
|
+
self._log_validation_failure(
|
|
618
|
+
"env_var_name", name, result.error_message, result.security_level
|
|
619
|
+
)
|
|
620
|
+
return result
|
|
621
|
+
|
|
622
|
+
# Validate environment variable value
|
|
623
|
+
result = self.sanitizer.sanitize_string(
|
|
624
|
+
value, max_length=self.config.MAX_STRING_LENGTH, allow_shell_chars=False
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
if not result.valid:
|
|
628
|
+
self._log_validation_failure(
|
|
629
|
+
"env_var_value",
|
|
630
|
+
f"{name}={value}",
|
|
631
|
+
result.error_message,
|
|
632
|
+
result.security_level,
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
return result
|
|
636
|
+
|
|
637
|
+
def _log_validation_failure(
|
|
638
|
+
self,
|
|
639
|
+
validation_type: str,
|
|
640
|
+
input_value: str,
|
|
641
|
+
reason: str,
|
|
642
|
+
level: SecurityEventLevel,
|
|
643
|
+
) -> None:
|
|
644
|
+
"""Log validation failure with rate limiting."""
|
|
645
|
+
|
|
646
|
+
self.logger.log_validation_failed(
|
|
647
|
+
validation_type=validation_type,
|
|
648
|
+
file_path=input_value, # Reusing file_path field for input value
|
|
649
|
+
reason=reason,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# Track failure counts for rate limiting
|
|
653
|
+
self._failure_counts[validation_type] = (
|
|
654
|
+
self._failure_counts.get(validation_type, 0) + 1
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def validation_required(
|
|
659
|
+
*,
|
|
660
|
+
validate_args: bool = True,
|
|
661
|
+
validate_kwargs: bool = True,
|
|
662
|
+
config: ValidationConfig | None = None,
|
|
663
|
+
):
|
|
664
|
+
"""Decorator to add automatic input validation to functions."""
|
|
665
|
+
|
|
666
|
+
def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
|
667
|
+
@wraps(func)
|
|
668
|
+
def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
669
|
+
validator = SecureInputValidator(config)
|
|
670
|
+
|
|
671
|
+
if validate_args:
|
|
672
|
+
_validate_function_args(validator, args)
|
|
673
|
+
|
|
674
|
+
if validate_kwargs:
|
|
675
|
+
_validate_function_kwargs(validator, kwargs)
|
|
676
|
+
|
|
677
|
+
return func(*args, **kwargs)
|
|
678
|
+
|
|
679
|
+
return wrapper
|
|
680
|
+
|
|
681
|
+
return decorator
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _validate_function_args(
|
|
685
|
+
validator: SecureInputValidator, args: tuple[t.Any, ...]
|
|
686
|
+
) -> None:
|
|
687
|
+
"""Validate string arguments in function args."""
|
|
688
|
+
for i, arg in enumerate(args):
|
|
689
|
+
if isinstance(arg, str):
|
|
690
|
+
result = validator.sanitizer.sanitize_string(arg)
|
|
691
|
+
if not result.valid:
|
|
692
|
+
raise ExecutionError(
|
|
693
|
+
message=f"Validation failed for argument {i}: {result.error_message}",
|
|
694
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _validate_function_kwargs(
|
|
699
|
+
validator: SecureInputValidator, kwargs: dict[str, t.Any]
|
|
700
|
+
) -> None:
|
|
701
|
+
"""Validate string values in function kwargs."""
|
|
702
|
+
for key, value in kwargs.items():
|
|
703
|
+
if isinstance(value, str):
|
|
704
|
+
result = validator.sanitizer.sanitize_string(value)
|
|
705
|
+
if not result.valid:
|
|
706
|
+
raise ExecutionError(
|
|
707
|
+
message=f"Validation failed for parameter {key}: {result.error_message}",
|
|
708
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def get_input_validator(
|
|
713
|
+
config: ValidationConfig | None = None,
|
|
714
|
+
) -> SecureInputValidator:
|
|
715
|
+
"""Get configured input validator instance."""
|
|
716
|
+
return SecureInputValidator(config)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
# Convenience validation functions
|
|
720
|
+
def validate_and_sanitize_string(value: str, **kwargs: t.Any) -> str:
|
|
721
|
+
"""Validate and return sanitized string, raising on failure."""
|
|
722
|
+
validator = SecureInputValidator()
|
|
723
|
+
result = validator.sanitizer.sanitize_string(value, **kwargs)
|
|
724
|
+
|
|
725
|
+
if not result.valid:
|
|
726
|
+
raise ExecutionError(
|
|
727
|
+
message=f"String validation failed: {result.error_message}",
|
|
728
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
return result.sanitized_value
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def validate_and_sanitize_path(value: str | Path, **kwargs: t.Any) -> Path:
|
|
735
|
+
"""Validate and return sanitized path, raising on failure."""
|
|
736
|
+
validator = SecureInputValidator()
|
|
737
|
+
result = validator.sanitizer.sanitize_string(str(value), **kwargs)
|
|
738
|
+
# Convert back to Path if validation passes
|
|
739
|
+
|
|
740
|
+
if not result.valid:
|
|
741
|
+
raise ExecutionError(
|
|
742
|
+
message=f"Path validation failed: {result.error_message}",
|
|
743
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
return Path(result.sanitized_value)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def validate_and_parse_json(value: str, **kwargs: t.Any) -> t.Any:
|
|
750
|
+
"""Validate and return parsed JSON, raising on failure."""
|
|
751
|
+
validator = SecureInputValidator()
|
|
752
|
+
result = validator.sanitizer.sanitize_json(value, **kwargs)
|
|
753
|
+
|
|
754
|
+
if not result.valid:
|
|
755
|
+
raise ExecutionError(
|
|
756
|
+
message=f"JSON validation failed: {result.error_message}",
|
|
757
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
return result.sanitized_value
|