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,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