crackerjack 0.33.0__py3-none-any.whl → 0.33.1__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/__main__.py +1350 -34
- crackerjack/adapters/__init__.py +17 -0
- crackerjack/adapters/lsp_client.py +358 -0
- crackerjack/adapters/rust_tool_adapter.py +194 -0
- crackerjack/adapters/rust_tool_manager.py +193 -0
- crackerjack/adapters/skylos_adapter.py +231 -0
- crackerjack/adapters/zuban_adapter.py +560 -0
- crackerjack/agents/base.py +7 -3
- crackerjack/agents/coordinator.py +271 -33
- crackerjack/agents/documentation_agent.py +9 -15
- crackerjack/agents/dry_agent.py +3 -15
- crackerjack/agents/formatting_agent.py +1 -1
- crackerjack/agents/import_optimization_agent.py +36 -180
- crackerjack/agents/performance_agent.py +17 -98
- crackerjack/agents/performance_helpers.py +7 -31
- crackerjack/agents/proactive_agent.py +1 -3
- crackerjack/agents/refactoring_agent.py +16 -85
- crackerjack/agents/refactoring_helpers.py +7 -42
- crackerjack/agents/security_agent.py +9 -48
- crackerjack/agents/test_creation_agent.py +356 -513
- crackerjack/agents/test_specialist_agent.py +0 -4
- crackerjack/api.py +6 -25
- crackerjack/cli/cache_handlers.py +204 -0
- crackerjack/cli/cache_handlers_enhanced.py +683 -0
- crackerjack/cli/facade.py +100 -0
- crackerjack/cli/handlers.py +224 -9
- crackerjack/cli/interactive.py +6 -4
- crackerjack/cli/options.py +642 -55
- crackerjack/cli/utils.py +2 -1
- crackerjack/code_cleaner.py +58 -117
- crackerjack/config/global_lock_config.py +8 -48
- crackerjack/config/hooks.py +53 -62
- crackerjack/core/async_workflow_orchestrator.py +24 -34
- crackerjack/core/autofix_coordinator.py +3 -17
- crackerjack/core/enhanced_container.py +4 -13
- crackerjack/core/file_lifecycle.py +12 -89
- crackerjack/core/performance.py +2 -2
- crackerjack/core/performance_monitor.py +15 -55
- crackerjack/core/phase_coordinator.py +104 -204
- crackerjack/core/resource_manager.py +14 -90
- crackerjack/core/service_watchdog.py +62 -95
- crackerjack/core/session_coordinator.py +149 -0
- crackerjack/core/timeout_manager.py +14 -72
- crackerjack/core/websocket_lifecycle.py +13 -78
- crackerjack/core/workflow_orchestrator.py +171 -174
- crackerjack/docs/INDEX.md +11 -0
- crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
- crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
- crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
- crackerjack/docs/generated/api/SERVICES.md +1252 -0
- crackerjack/documentation/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +765 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +977 -0
- crackerjack/dynamic_config.py +55 -50
- crackerjack/executors/async_hook_executor.py +10 -15
- crackerjack/executors/cached_hook_executor.py +117 -43
- crackerjack/executors/hook_executor.py +8 -34
- crackerjack/executors/hook_lock_manager.py +26 -183
- crackerjack/executors/individual_hook_executor.py +13 -11
- crackerjack/executors/lsp_aware_hook_executor.py +270 -0
- crackerjack/executors/tool_proxy.py +417 -0
- crackerjack/hooks/lsp_hook.py +79 -0
- crackerjack/intelligence/adaptive_learning.py +25 -10
- crackerjack/intelligence/agent_orchestrator.py +2 -5
- crackerjack/intelligence/agent_registry.py +34 -24
- crackerjack/intelligence/agent_selector.py +5 -7
- crackerjack/interactive.py +17 -6
- crackerjack/managers/async_hook_manager.py +0 -1
- crackerjack/managers/hook_manager.py +79 -1
- crackerjack/managers/publish_manager.py +44 -8
- crackerjack/managers/test_command_builder.py +1 -15
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +98 -7
- crackerjack/managers/test_manager_backup.py +10 -9
- crackerjack/mcp/cache.py +2 -2
- crackerjack/mcp/client_runner.py +1 -1
- crackerjack/mcp/context.py +191 -68
- crackerjack/mcp/dashboard.py +7 -5
- crackerjack/mcp/enhanced_progress_monitor.py +31 -28
- crackerjack/mcp/file_monitor.py +30 -23
- crackerjack/mcp/progress_components.py +31 -21
- crackerjack/mcp/progress_monitor.py +50 -53
- crackerjack/mcp/rate_limiter.py +6 -6
- crackerjack/mcp/server_core.py +17 -16
- crackerjack/mcp/service_watchdog.py +2 -1
- crackerjack/mcp/state.py +4 -7
- crackerjack/mcp/task_manager.py +11 -9
- crackerjack/mcp/tools/core_tools.py +173 -32
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +8 -10
- crackerjack/mcp/tools/execution_tools_backup.py +42 -30
- crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
- crackerjack/mcp/tools/intelligence_tools.py +5 -2
- crackerjack/mcp/tools/monitoring_tools.py +33 -70
- crackerjack/mcp/tools/proactive_tools.py +24 -11
- crackerjack/mcp/tools/progress_tools.py +5 -8
- crackerjack/mcp/tools/utility_tools.py +20 -14
- crackerjack/mcp/tools/workflow_executor.py +62 -40
- crackerjack/mcp/websocket/app.py +8 -0
- crackerjack/mcp/websocket/endpoints.py +352 -357
- crackerjack/mcp/websocket/jobs.py +40 -57
- crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
- crackerjack/mcp/websocket/server.py +7 -25
- crackerjack/mcp/websocket/websocket_handler.py +6 -17
- crackerjack/mixins/__init__.py +0 -2
- crackerjack/mixins/error_handling.py +1 -70
- crackerjack/models/config.py +12 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +122 -122
- crackerjack/models/resource_protocols.py +55 -210
- crackerjack/monitoring/ai_agent_watchdog.py +13 -13
- crackerjack/monitoring/metrics_collector.py +426 -0
- crackerjack/monitoring/regression_prevention.py +8 -8
- crackerjack/monitoring/websocket_server.py +643 -0
- crackerjack/orchestration/advanced_orchestrator.py +11 -6
- crackerjack/orchestration/coverage_improvement.py +3 -3
- crackerjack/orchestration/execution_strategies.py +26 -6
- crackerjack/orchestration/test_progress_streamer.py +8 -5
- crackerjack/plugins/base.py +2 -2
- crackerjack/plugins/hooks.py +7 -0
- crackerjack/plugins/managers.py +11 -8
- crackerjack/security/__init__.py +0 -1
- crackerjack/security/audit.py +6 -35
- crackerjack/services/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +615 -0
- crackerjack/services/backup_service.py +2 -2
- crackerjack/services/bounded_status_operations.py +15 -152
- crackerjack/services/cache.py +127 -1
- crackerjack/services/changelog_automation.py +395 -0
- crackerjack/services/config.py +15 -9
- crackerjack/services/config_merge.py +19 -80
- crackerjack/services/config_template.py +506 -0
- crackerjack/services/contextual_ai_assistant.py +48 -22
- crackerjack/services/coverage_badge_service.py +171 -0
- crackerjack/services/coverage_ratchet.py +27 -25
- crackerjack/services/debug.py +3 -3
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +14 -11
- crackerjack/services/documentation_generator.py +491 -0
- crackerjack/services/documentation_service.py +675 -0
- crackerjack/services/enhanced_filesystem.py +6 -5
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/git.py +8 -25
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +11 -30
- crackerjack/services/input_validator.py +5 -97
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +15 -12
- crackerjack/services/logging.py +4 -3
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +19 -87
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +9 -67
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +18 -59
- crackerjack/services/performance_cache.py +20 -81
- crackerjack/services/performance_monitor.py +27 -95
- crackerjack/services/predictive_analytics.py +510 -0
- crackerjack/services/quality_baseline.py +234 -0
- crackerjack/services/quality_baseline_enhanced.py +646 -0
- crackerjack/services/quality_intelligence.py +785 -0
- crackerjack/services/regex_patterns.py +605 -524
- crackerjack/services/regex_utils.py +43 -123
- crackerjack/services/secure_path_utils.py +5 -164
- crackerjack/services/secure_status_formatter.py +30 -141
- crackerjack/services/secure_subprocess.py +11 -92
- crackerjack/services/security.py +9 -41
- crackerjack/services/security_logger.py +12 -24
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- crackerjack/services/thread_safe_status_collector.py +19 -125
- crackerjack/services/unified_config.py +21 -13
- crackerjack/services/validation_rate_limiter.py +5 -54
- crackerjack/services/version_analyzer.py +459 -0
- crackerjack/services/version_checker.py +1 -1
- crackerjack/services/websocket_resource_limiter.py +10 -144
- crackerjack/services/zuban_lsp_service.py +390 -0
- crackerjack/slash_commands/__init__.py +2 -7
- crackerjack/slash_commands/run.md +2 -2
- crackerjack/tools/validate_input_validator_patterns.py +14 -40
- crackerjack/tools/validate_regex_patterns.py +19 -48
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +196 -25
- crackerjack-0.33.1.dist-info/RECORD +229 -0
- crackerjack/CLAUDE.md +0 -207
- crackerjack/RULES.md +0 -380
- crackerjack/py313.py +0 -234
- crackerjack-0.33.0.dist-info/RECORD +0 -187
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -28,31 +28,27 @@ class InitializationService:
|
|
|
28
28
|
self.filesystem = filesystem
|
|
29
29
|
self.git_service = git_service
|
|
30
30
|
self.pkg_path = pkg_path
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
self.config_merge_service = config_merge_service or ConfigMergeService(
|
|
33
33
|
console, filesystem, git_service
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
def initialize_project(self, project_path: str | Path) -> bool:
|
|
37
|
-
"""Protocol method: Initialize project at given path."""
|
|
38
37
|
try:
|
|
39
38
|
result = self.initialize_project_full(Path(project_path))
|
|
40
|
-
|
|
39
|
+
success = result.get("success", False)
|
|
40
|
+
return bool(success)
|
|
41
41
|
except Exception:
|
|
42
42
|
return False
|
|
43
43
|
|
|
44
44
|
def setup_git_hooks(self) -> bool:
|
|
45
|
-
"""Protocol method: Setup git hooks."""
|
|
46
45
|
try:
|
|
47
|
-
# Basic git hooks setup implementation
|
|
48
46
|
return True
|
|
49
47
|
except Exception:
|
|
50
48
|
return False
|
|
51
49
|
|
|
52
50
|
def validate_project_structure(self) -> bool:
|
|
53
|
-
"""Protocol method: Validate project structure."""
|
|
54
51
|
try:
|
|
55
|
-
# Basic project structure validation
|
|
56
52
|
return True
|
|
57
53
|
except Exception:
|
|
58
54
|
return False
|
|
@@ -65,7 +61,6 @@ class InitializationService:
|
|
|
65
61
|
if target_path is None:
|
|
66
62
|
target_path = Path.cwd()
|
|
67
63
|
|
|
68
|
-
# Validate target path for security
|
|
69
64
|
try:
|
|
70
65
|
target_path = validate_and_sanitize_path(target_path, allow_absolute=True)
|
|
71
66
|
except Exception as e:
|
|
@@ -83,7 +78,6 @@ class InitializationService:
|
|
|
83
78
|
config_files = self._get_config_files()
|
|
84
79
|
project_name = target_path.name
|
|
85
80
|
|
|
86
|
-
# Validate project name
|
|
87
81
|
validator = get_input_validator()
|
|
88
82
|
name_result = validator.validate_project_name(project_name)
|
|
89
83
|
if not name_result.valid:
|
|
@@ -93,7 +87,6 @@ class InitializationService:
|
|
|
93
87
|
results["success"] = False
|
|
94
88
|
return results
|
|
95
89
|
|
|
96
|
-
# Use sanitized project name
|
|
97
90
|
sanitized_project_name = name_result.sanitized_value
|
|
98
91
|
|
|
99
92
|
for file_name, merge_strategy in config_files.items():
|
|
@@ -501,7 +494,6 @@ python -m crackerjack - a patch
|
|
|
501
494
|
results: dict[str, t.Any],
|
|
502
495
|
) -> None:
|
|
503
496
|
try:
|
|
504
|
-
# Generate appropriate source content
|
|
505
497
|
if file_name == "CLAUDE.md" and project_name != "crackerjack":
|
|
506
498
|
source_content = self._generate_project_claude_content(project_name)
|
|
507
499
|
else:
|
|
@@ -509,11 +501,9 @@ python -m crackerjack - a patch
|
|
|
509
501
|
source_file, True, project_name
|
|
510
502
|
)
|
|
511
503
|
|
|
512
|
-
# Define markers for this file type
|
|
513
504
|
crackerjack_start_marker = "<!-- CRACKERJACK INTEGRATION START -->"
|
|
514
505
|
crackerjack_end_marker = "<!-- CRACKERJACK INTEGRATION END -->"
|
|
515
506
|
|
|
516
|
-
# Delegate to ConfigMergeService for smart append logic
|
|
517
507
|
merged_content = self.config_merge_service.smart_append_file(
|
|
518
508
|
source_content,
|
|
519
509
|
target_file,
|
|
@@ -522,7 +512,6 @@ python -m crackerjack - a patch
|
|
|
522
512
|
force,
|
|
523
513
|
)
|
|
524
514
|
|
|
525
|
-
# Check if content was actually changed
|
|
526
515
|
if target_file.exists():
|
|
527
516
|
existing_content = target_file.read_text()
|
|
528
517
|
if crackerjack_start_marker in existing_content and not force:
|
|
@@ -531,7 +520,6 @@ python -m crackerjack - a patch
|
|
|
531
520
|
)
|
|
532
521
|
return
|
|
533
522
|
|
|
534
|
-
# Write the merged content
|
|
535
523
|
target_file.write_text(merged_content)
|
|
536
524
|
t.cast("list[str]", results["files_copied"]).append(
|
|
537
525
|
f"{file_name} (appended)"
|
|
@@ -556,8 +544,6 @@ python -m crackerjack - a patch
|
|
|
556
544
|
force: bool,
|
|
557
545
|
results: dict[str, t.Any],
|
|
558
546
|
) -> None:
|
|
559
|
-
"""Smart merge .gitignore patterns using ConfigMergeService."""
|
|
560
|
-
# Define crackerjack .gitignore patterns
|
|
561
547
|
gitignore_patterns = [
|
|
562
548
|
"# Build/Distribution",
|
|
563
549
|
"/build/",
|
|
@@ -652,12 +638,10 @@ python -m crackerjack - a patch
|
|
|
652
638
|
with source_file.open("rb") as f:
|
|
653
639
|
source_config = tomli.load(f)
|
|
654
640
|
|
|
655
|
-
# Delegate to ConfigMergeService for smart merge logic
|
|
656
641
|
merged_config = self.config_merge_service.smart_merge_pyproject(
|
|
657
642
|
source_config, target_file, project_name
|
|
658
643
|
)
|
|
659
644
|
|
|
660
|
-
# Write the merged configuration
|
|
661
645
|
self.config_merge_service.write_pyproject_config(merged_config, target_file)
|
|
662
646
|
|
|
663
647
|
t.cast("list[str]", results["files_copied"]).append(
|
|
@@ -704,11 +688,12 @@ python -m crackerjack - a patch
|
|
|
704
688
|
self._handle_file_processing_error(".pre-commit-config.yaml", e, results)
|
|
705
689
|
|
|
706
690
|
def _load_source_config(self, source_file: Path) -> dict[str, t.Any] | None:
|
|
707
|
-
"""Load and validate source configuration file."""
|
|
708
691
|
with source_file.open() as f:
|
|
709
|
-
|
|
692
|
+
loaded_config = yaml.safe_load(f)
|
|
693
|
+
source_config: dict[str, t.Any] = (
|
|
694
|
+
loaded_config if isinstance(loaded_config, dict) else {}
|
|
695
|
+
)
|
|
710
696
|
|
|
711
|
-
# Ensure source_config is a dict
|
|
712
697
|
if not isinstance(source_config, dict):
|
|
713
698
|
self.console.print(
|
|
714
699
|
"[yellow]⚠️[/yellow] Source .pre-commit-config.yaml is not a dictionary, skipping merge"
|
|
@@ -720,7 +705,6 @@ python -m crackerjack - a patch
|
|
|
720
705
|
def _perform_config_merge(
|
|
721
706
|
self, source_config: dict[str, t.Any], target_file: Path, project_name: str
|
|
722
707
|
) -> dict[str, t.Any]:
|
|
723
|
-
"""Perform the configuration merge using ConfigMergeService."""
|
|
724
708
|
return self.config_merge_service.smart_merge_pre_commit_config(
|
|
725
709
|
source_config, target_file, project_name
|
|
726
710
|
)
|
|
@@ -731,14 +715,15 @@ python -m crackerjack - a patch
|
|
|
731
715
|
merged_config: dict[str, t.Any],
|
|
732
716
|
results: dict[str, t.Any],
|
|
733
717
|
) -> bool:
|
|
734
|
-
"""Check if merge should be skipped due to no changes."""
|
|
735
718
|
if not target_file.exists():
|
|
736
719
|
return False
|
|
737
720
|
|
|
738
721
|
with target_file.open() as f:
|
|
739
|
-
|
|
722
|
+
loaded_config = yaml.safe_load(f)
|
|
723
|
+
old_config: dict[str, t.Any] = (
|
|
724
|
+
loaded_config if isinstance(loaded_config, dict) else {}
|
|
725
|
+
)
|
|
740
726
|
|
|
741
|
-
# Ensure old_config is a dict
|
|
742
727
|
if not isinstance(old_config, dict):
|
|
743
728
|
old_config = {}
|
|
744
729
|
|
|
@@ -758,8 +743,6 @@ python -m crackerjack - a patch
|
|
|
758
743
|
source_config: dict[str, t.Any],
|
|
759
744
|
results: dict[str, t.Any],
|
|
760
745
|
) -> None:
|
|
761
|
-
"""Write merged config and finalize the process."""
|
|
762
|
-
# Write the merged configuration
|
|
763
746
|
self.config_merge_service.write_pre_commit_config(merged_config, target_file)
|
|
764
747
|
|
|
765
748
|
t.cast("list[str]", results["files_copied"]).append(
|
|
@@ -770,7 +753,6 @@ python -m crackerjack - a patch
|
|
|
770
753
|
self._display_merge_success(source_config)
|
|
771
754
|
|
|
772
755
|
def _git_add_config_file(self, target_file: Path) -> None:
|
|
773
|
-
"""Add config file to git with error handling."""
|
|
774
756
|
try:
|
|
775
757
|
self.git_service.add_files([str(target_file)])
|
|
776
758
|
except Exception as e:
|
|
@@ -779,7 +761,6 @@ python -m crackerjack - a patch
|
|
|
779
761
|
)
|
|
780
762
|
|
|
781
763
|
def _display_merge_success(self, source_config: dict[str, t.Any]) -> None:
|
|
782
|
-
"""Display success message with repo count."""
|
|
783
764
|
source_repo_count = len(source_config.get("repos", []))
|
|
784
765
|
self.console.print(
|
|
785
766
|
f"[green]✅[/ green] Merged .pre-commit-config.yaml ({source_repo_count} repos processed)"
|
|
@@ -1,15 +1,3 @@
|
|
|
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
1
|
import json
|
|
14
2
|
import typing as t
|
|
15
3
|
from functools import wraps
|
|
@@ -26,29 +14,21 @@ from .security_logger import (
|
|
|
26
14
|
|
|
27
15
|
|
|
28
16
|
class ValidationConfig(BaseModel):
|
|
29
|
-
"""Configuration for input validation rules."""
|
|
30
|
-
|
|
31
|
-
# String limits
|
|
32
17
|
MAX_STRING_LENGTH: int = Field(default=10000, ge=1)
|
|
33
18
|
MAX_PROJECT_NAME_LENGTH: int = Field(default=255, ge=1)
|
|
34
19
|
MAX_JOB_ID_LENGTH: int = Field(default=128, ge=1)
|
|
35
20
|
MAX_COMMAND_LENGTH: int = Field(default=1000, ge=1)
|
|
36
21
|
|
|
37
|
-
|
|
38
|
-
MAX_JSON_SIZE: int = Field(default=1024 * 1024, ge=1) # 1MB
|
|
22
|
+
MAX_JSON_SIZE: int = Field(default=1024 * 1024, ge=1)
|
|
39
23
|
MAX_JSON_DEPTH: int = Field(default=10, ge=1)
|
|
40
24
|
|
|
41
|
-
# Rate limiting
|
|
42
25
|
MAX_VALIDATION_FAILURES_PER_MINUTE: int = Field(default=10, ge=1)
|
|
43
26
|
|
|
44
|
-
# Pattern validation
|
|
45
27
|
ALLOW_SHELL_METACHARACTERS: bool = Field(default=False)
|
|
46
28
|
STRICT_ALPHANUMERIC_MODE: bool = Field(default=False)
|
|
47
29
|
|
|
48
30
|
|
|
49
31
|
class ValidationResult(BaseModel):
|
|
50
|
-
"""Result of input validation."""
|
|
51
|
-
|
|
52
32
|
valid: bool
|
|
53
33
|
sanitized_value: t.Any = None
|
|
54
34
|
error_message: str = ""
|
|
@@ -57,9 +37,6 @@ class ValidationResult(BaseModel):
|
|
|
57
37
|
|
|
58
38
|
|
|
59
39
|
class InputSanitizer:
|
|
60
|
-
"""Provides secure input sanitization utilities."""
|
|
61
|
-
|
|
62
|
-
# Shell metacharacters that could enable command injection
|
|
63
40
|
SHELL_METACHARACTERS = {
|
|
64
41
|
";",
|
|
65
42
|
"&",
|
|
@@ -85,7 +62,6 @@ class InputSanitizer:
|
|
|
85
62
|
"^",
|
|
86
63
|
}
|
|
87
64
|
|
|
88
|
-
# Dangerous path components
|
|
89
65
|
DANGEROUS_PATH_COMPONENTS = {
|
|
90
66
|
"..",
|
|
91
67
|
".",
|
|
@@ -121,9 +97,6 @@ class InputSanitizer:
|
|
|
121
97
|
"LPT9",
|
|
122
98
|
}
|
|
123
99
|
|
|
124
|
-
# NOTE: SQL and Code injection patterns now use centralized SAFE_PATTERNS
|
|
125
|
-
# from regex_patterns.py for security consistency and testing
|
|
126
|
-
|
|
127
100
|
@classmethod
|
|
128
101
|
def sanitize_string(
|
|
129
102
|
cls,
|
|
@@ -132,29 +105,22 @@ class InputSanitizer:
|
|
|
132
105
|
allow_shell_chars: bool = False,
|
|
133
106
|
strict_alphanumeric: bool = False,
|
|
134
107
|
) -> ValidationResult:
|
|
135
|
-
"""Sanitize string input with configurable restrictions."""
|
|
136
|
-
|
|
137
|
-
# Type validation
|
|
138
108
|
type_result = cls._validate_string_type(value)
|
|
139
109
|
if not type_result.valid:
|
|
140
110
|
return type_result
|
|
141
111
|
|
|
142
|
-
# Length validation
|
|
143
112
|
length_result = cls._validate_string_length(value, max_length)
|
|
144
113
|
if not length_result.valid:
|
|
145
114
|
return length_result
|
|
146
115
|
|
|
147
|
-
# Security validations
|
|
148
116
|
security_result = cls._validate_string_security(value, allow_shell_chars)
|
|
149
117
|
if not security_result.valid:
|
|
150
118
|
return security_result
|
|
151
119
|
|
|
152
|
-
# Pattern validations
|
|
153
120
|
pattern_result = cls._validate_string_patterns(value)
|
|
154
121
|
if not pattern_result.valid:
|
|
155
122
|
return pattern_result
|
|
156
123
|
|
|
157
|
-
# Strict alphanumeric mode
|
|
158
124
|
if strict_alphanumeric and not cls._is_strictly_alphanumeric(value):
|
|
159
125
|
return ValidationResult(
|
|
160
126
|
valid=False,
|
|
@@ -163,7 +129,6 @@ class InputSanitizer:
|
|
|
163
129
|
validation_type="alphanumeric_only",
|
|
164
130
|
)
|
|
165
131
|
|
|
166
|
-
# Basic sanitization (remove leading/trailing whitespace)
|
|
167
132
|
sanitized = value.strip()
|
|
168
133
|
|
|
169
134
|
return ValidationResult(
|
|
@@ -172,7 +137,6 @@ class InputSanitizer:
|
|
|
172
137
|
|
|
173
138
|
@classmethod
|
|
174
139
|
def _validate_string_type(cls, value: t.Any) -> ValidationResult:
|
|
175
|
-
"""Validate that the input is a string."""
|
|
176
140
|
if not isinstance(value, str):
|
|
177
141
|
return ValidationResult(
|
|
178
142
|
valid=False,
|
|
@@ -184,7 +148,6 @@ class InputSanitizer:
|
|
|
184
148
|
|
|
185
149
|
@classmethod
|
|
186
150
|
def _validate_string_length(cls, value: str, max_length: int) -> ValidationResult:
|
|
187
|
-
"""Validate string length."""
|
|
188
151
|
if len(value) > max_length:
|
|
189
152
|
return ValidationResult(
|
|
190
153
|
valid=False,
|
|
@@ -198,8 +161,6 @@ class InputSanitizer:
|
|
|
198
161
|
def _validate_string_security(
|
|
199
162
|
cls, value: str, allow_shell_chars: bool
|
|
200
163
|
) -> ValidationResult:
|
|
201
|
-
"""Validate string security constraints."""
|
|
202
|
-
# Null byte injection check
|
|
203
164
|
if "\x00" in value:
|
|
204
165
|
return ValidationResult(
|
|
205
166
|
valid=False,
|
|
@@ -208,7 +169,6 @@ class InputSanitizer:
|
|
|
208
169
|
validation_type="null_byte_injection",
|
|
209
170
|
)
|
|
210
171
|
|
|
211
|
-
# Control character check
|
|
212
172
|
if any(ord(c) < 32 and c not in "\t\n\r" for c in value):
|
|
213
173
|
return ValidationResult(
|
|
214
174
|
valid=False,
|
|
@@ -217,7 +177,6 @@ class InputSanitizer:
|
|
|
217
177
|
validation_type="control_chars",
|
|
218
178
|
)
|
|
219
179
|
|
|
220
|
-
# Shell metacharacter check
|
|
221
180
|
if not allow_shell_chars:
|
|
222
181
|
found_chars = [c for c in value if c in cls.SHELL_METACHARACTERS]
|
|
223
182
|
if found_chars:
|
|
@@ -232,8 +191,6 @@ class InputSanitizer:
|
|
|
232
191
|
|
|
233
192
|
@classmethod
|
|
234
193
|
def _validate_string_patterns(cls, value: str) -> ValidationResult:
|
|
235
|
-
"""Validate string against security patterns."""
|
|
236
|
-
# SQL injection pattern check using SAFE_PATTERNS
|
|
237
194
|
sql_patterns = [
|
|
238
195
|
"validate_sql_injection_patterns",
|
|
239
196
|
"validate_sql_comment_patterns",
|
|
@@ -250,7 +207,6 @@ class InputSanitizer:
|
|
|
250
207
|
validation_type="sql_injection",
|
|
251
208
|
)
|
|
252
209
|
|
|
253
|
-
# Code injection pattern check using SAFE_PATTERNS
|
|
254
210
|
code_patterns = [
|
|
255
211
|
"validate_code_eval_injection",
|
|
256
212
|
"validate_code_dynamic_access",
|
|
@@ -271,15 +227,12 @@ class InputSanitizer:
|
|
|
271
227
|
|
|
272
228
|
@classmethod
|
|
273
229
|
def _is_strictly_alphanumeric(cls, value: str) -> bool:
|
|
274
|
-
"""Check if string is strictly alphanumeric with allowed characters."""
|
|
275
230
|
return value.replace("-", "").replace("_", "").isalnum()
|
|
276
231
|
|
|
277
232
|
@classmethod
|
|
278
233
|
def sanitize_json(
|
|
279
234
|
cls, value: str, max_size: int = 1024 * 1024, max_depth: int = 10
|
|
280
235
|
) -> ValidationResult:
|
|
281
|
-
"""Sanitize JSON input with size and depth limits."""
|
|
282
|
-
|
|
283
236
|
if len(value) > max_size:
|
|
284
237
|
return ValidationResult(
|
|
285
238
|
valid=False,
|
|
@@ -289,10 +242,8 @@ class InputSanitizer:
|
|
|
289
242
|
)
|
|
290
243
|
|
|
291
244
|
try:
|
|
292
|
-
# Parse JSON to validate structure
|
|
293
245
|
parsed = json.loads(value)
|
|
294
246
|
|
|
295
|
-
# Check nesting depth
|
|
296
247
|
def check_depth(obj: t.Any, current_depth: int = 0) -> int:
|
|
297
248
|
if current_depth > max_depth:
|
|
298
249
|
return current_depth
|
|
@@ -339,17 +290,13 @@ class InputSanitizer:
|
|
|
339
290
|
base_directory: Path | None = None,
|
|
340
291
|
allow_absolute: bool = False,
|
|
341
292
|
) -> ValidationResult:
|
|
342
|
-
"""Sanitize file path with traversal protection."""
|
|
343
|
-
|
|
344
293
|
try:
|
|
345
294
|
path = Path(value)
|
|
346
295
|
|
|
347
|
-
# Check for dangerous components in the original path before resolving
|
|
348
296
|
danger_result = cls._check_dangerous_components(path)
|
|
349
297
|
if not danger_result.valid:
|
|
350
298
|
return danger_result
|
|
351
299
|
|
|
352
|
-
# Handle base directory constraints
|
|
353
300
|
if base_directory:
|
|
354
301
|
base_result = cls._validate_base_directory(
|
|
355
302
|
path, base_directory, allow_absolute
|
|
@@ -358,10 +305,8 @@ class InputSanitizer:
|
|
|
358
305
|
return base_result
|
|
359
306
|
resolved = base_result.sanitized_value
|
|
360
307
|
else:
|
|
361
|
-
# Resolve to absolute path to eliminate .. components if no base directory
|
|
362
308
|
resolved = path.resolve()
|
|
363
309
|
|
|
364
|
-
# Check absolute path restrictions
|
|
365
310
|
absolute_result = cls._validate_absolute_path(
|
|
366
311
|
resolved, allow_absolute, base_directory
|
|
367
312
|
)
|
|
@@ -384,7 +329,6 @@ class InputSanitizer:
|
|
|
384
329
|
|
|
385
330
|
@classmethod
|
|
386
331
|
def _check_dangerous_components(cls, path: Path) -> ValidationResult:
|
|
387
|
-
"""Check for dangerous components in the path."""
|
|
388
332
|
for part in path.parts:
|
|
389
333
|
if part.upper() in cls.DANGEROUS_PATH_COMPONENTS:
|
|
390
334
|
return ValidationResult(
|
|
@@ -399,10 +343,8 @@ class InputSanitizer:
|
|
|
399
343
|
def _validate_base_directory(
|
|
400
344
|
cls, path: Path, base_directory: Path, allow_absolute: bool
|
|
401
345
|
) -> ValidationResult:
|
|
402
|
-
"""Validate path against base directory constraints."""
|
|
403
346
|
base_resolved = base_directory.resolve()
|
|
404
347
|
|
|
405
|
-
# If the path is absolute and doesn't start with base directory, it's invalid
|
|
406
348
|
if path.is_absolute() and not str(path).startswith(str(base_resolved)):
|
|
407
349
|
return ValidationResult(
|
|
408
350
|
valid=False,
|
|
@@ -411,7 +353,6 @@ class InputSanitizer:
|
|
|
411
353
|
validation_type="directory_escape",
|
|
412
354
|
)
|
|
413
355
|
|
|
414
|
-
# If the path is relative, resolve it relative to base directory
|
|
415
356
|
if not path.is_absolute():
|
|
416
357
|
resolved = (base_resolved / path).resolve()
|
|
417
358
|
try:
|
|
@@ -424,7 +365,6 @@ class InputSanitizer:
|
|
|
424
365
|
validation_type="directory_escape",
|
|
425
366
|
)
|
|
426
367
|
else:
|
|
427
|
-
# For absolute paths that start with base directory, resolve normally
|
|
428
368
|
resolved = path.resolve()
|
|
429
369
|
try:
|
|
430
370
|
resolved.relative_to(base_resolved)
|
|
@@ -444,7 +384,6 @@ class InputSanitizer:
|
|
|
444
384
|
def _validate_absolute_path(
|
|
445
385
|
cls, resolved: Path, allow_absolute: bool, base_directory: Path | None
|
|
446
386
|
) -> ValidationResult:
|
|
447
|
-
"""Validate absolute path restrictions."""
|
|
448
387
|
if not allow_absolute and resolved.is_absolute() and base_directory:
|
|
449
388
|
return ValidationResult(
|
|
450
389
|
valid=False,
|
|
@@ -456,8 +395,6 @@ class InputSanitizer:
|
|
|
456
395
|
|
|
457
396
|
|
|
458
397
|
class SecureInputValidator:
|
|
459
|
-
"""Main input validation class with security logging."""
|
|
460
|
-
|
|
461
398
|
def __init__(self, config: ValidationConfig | None = None):
|
|
462
399
|
self.config = config or ValidationConfig()
|
|
463
400
|
self.logger = get_security_logger()
|
|
@@ -465,8 +402,6 @@ class SecureInputValidator:
|
|
|
465
402
|
self._failure_counts: dict[str, int] = {}
|
|
466
403
|
|
|
467
404
|
def validate_project_name(self, name: str) -> ValidationResult:
|
|
468
|
-
"""Validate project name with security constraints."""
|
|
469
|
-
|
|
470
405
|
result = self.sanitizer.sanitize_string(
|
|
471
406
|
name,
|
|
472
407
|
max_length=self.config.MAX_PROJECT_NAME_LENGTH,
|
|
@@ -482,9 +417,6 @@ class SecureInputValidator:
|
|
|
482
417
|
return result
|
|
483
418
|
|
|
484
419
|
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
420
|
job_id_pattern = SAFE_PATTERNS["validate_job_id_format"]
|
|
489
421
|
if not job_id_pattern.test(job_id):
|
|
490
422
|
result = ValidationResult(
|
|
@@ -513,8 +445,6 @@ class SecureInputValidator:
|
|
|
513
445
|
return result
|
|
514
446
|
|
|
515
447
|
def validate_command_args(self, args: t.Any) -> ValidationResult:
|
|
516
|
-
"""Validate command arguments to prevent injection."""
|
|
517
|
-
|
|
518
448
|
if isinstance(args, str):
|
|
519
449
|
result = self.sanitizer.sanitize_string(
|
|
520
450
|
args,
|
|
@@ -522,7 +452,6 @@ class SecureInputValidator:
|
|
|
522
452
|
allow_shell_chars=self.config.ALLOW_SHELL_METACHARACTERS,
|
|
523
453
|
)
|
|
524
454
|
elif isinstance(args, list):
|
|
525
|
-
# Validate each argument in the list
|
|
526
455
|
sanitized_args = []
|
|
527
456
|
for arg in args:
|
|
528
457
|
if not isinstance(arg, str):
|
|
@@ -554,7 +483,7 @@ class SecureInputValidator:
|
|
|
554
483
|
else:
|
|
555
484
|
result = ValidationResult(
|
|
556
485
|
valid=False,
|
|
557
|
-
error_message=f"Command args must be string or list, got {type(args).__name__}",
|
|
486
|
+
error_message=f"Command args must be string or list[t.Any], got {type(args).__name__}",
|
|
558
487
|
security_level=SecurityEventLevel.HIGH,
|
|
559
488
|
validation_type="command_args_type",
|
|
560
489
|
)
|
|
@@ -567,8 +496,6 @@ class SecureInputValidator:
|
|
|
567
496
|
return result
|
|
568
497
|
|
|
569
498
|
def validate_json_payload(self, payload: str) -> ValidationResult:
|
|
570
|
-
"""Validate JSON payload with size and structure limits."""
|
|
571
|
-
|
|
572
499
|
result = self.sanitizer.sanitize_json(
|
|
573
500
|
payload,
|
|
574
501
|
max_size=self.config.MAX_JSON_SIZE,
|
|
@@ -591,8 +518,6 @@ class SecureInputValidator:
|
|
|
591
518
|
base_directory: Path | None = None,
|
|
592
519
|
allow_absolute: bool = False,
|
|
593
520
|
) -> ValidationResult:
|
|
594
|
-
"""Validate file path with traversal protection."""
|
|
595
|
-
|
|
596
521
|
result = self.sanitizer.sanitize_path(path, base_directory, allow_absolute)
|
|
597
522
|
|
|
598
523
|
if not result.valid:
|
|
@@ -603,9 +528,6 @@ class SecureInputValidator:
|
|
|
603
528
|
return result
|
|
604
529
|
|
|
605
530
|
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
531
|
env_var_pattern = SAFE_PATTERNS["validate_env_var_name_format"]
|
|
610
532
|
if not env_var_pattern.test(name):
|
|
611
533
|
result = ValidationResult(
|
|
@@ -619,7 +541,6 @@ class SecureInputValidator:
|
|
|
619
541
|
)
|
|
620
542
|
return result
|
|
621
543
|
|
|
622
|
-
# Validate environment variable value
|
|
623
544
|
result = self.sanitizer.sanitize_string(
|
|
624
545
|
value, max_length=self.config.MAX_STRING_LENGTH, allow_shell_chars=False
|
|
625
546
|
)
|
|
@@ -641,15 +562,12 @@ class SecureInputValidator:
|
|
|
641
562
|
reason: str,
|
|
642
563
|
level: SecurityEventLevel,
|
|
643
564
|
) -> None:
|
|
644
|
-
"""Log validation failure with rate limiting."""
|
|
645
|
-
|
|
646
565
|
self.logger.log_validation_failed(
|
|
647
566
|
validation_type=validation_type,
|
|
648
|
-
file_path=input_value,
|
|
567
|
+
file_path=input_value,
|
|
649
568
|
reason=reason,
|
|
650
569
|
)
|
|
651
570
|
|
|
652
|
-
# Track failure counts for rate limiting
|
|
653
571
|
self._failure_counts[validation_type] = (
|
|
654
572
|
self._failure_counts.get(validation_type, 0) + 1
|
|
655
573
|
)
|
|
@@ -660,9 +578,7 @@ def validation_required(
|
|
|
660
578
|
validate_args: bool = True,
|
|
661
579
|
validate_kwargs: bool = True,
|
|
662
580
|
config: ValidationConfig | None = None,
|
|
663
|
-
):
|
|
664
|
-
"""Decorator to add automatic input validation to functions."""
|
|
665
|
-
|
|
581
|
+
) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]:
|
|
666
582
|
def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
|
667
583
|
@wraps(func)
|
|
668
584
|
def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
|
|
@@ -684,7 +600,6 @@ def validation_required(
|
|
|
684
600
|
def _validate_function_args(
|
|
685
601
|
validator: SecureInputValidator, args: tuple[t.Any, ...]
|
|
686
602
|
) -> None:
|
|
687
|
-
"""Validate string arguments in function args."""
|
|
688
603
|
for i, arg in enumerate(args):
|
|
689
604
|
if isinstance(arg, str):
|
|
690
605
|
result = validator.sanitizer.sanitize_string(arg)
|
|
@@ -698,7 +613,6 @@ def _validate_function_args(
|
|
|
698
613
|
def _validate_function_kwargs(
|
|
699
614
|
validator: SecureInputValidator, kwargs: dict[str, t.Any]
|
|
700
615
|
) -> None:
|
|
701
|
-
"""Validate string values in function kwargs."""
|
|
702
616
|
for key, value in kwargs.items():
|
|
703
617
|
if isinstance(value, str):
|
|
704
618
|
result = validator.sanitizer.sanitize_string(value)
|
|
@@ -712,13 +626,10 @@ def _validate_function_kwargs(
|
|
|
712
626
|
def get_input_validator(
|
|
713
627
|
config: ValidationConfig | None = None,
|
|
714
628
|
) -> SecureInputValidator:
|
|
715
|
-
"""Get configured input validator instance."""
|
|
716
629
|
return SecureInputValidator(config)
|
|
717
630
|
|
|
718
631
|
|
|
719
|
-
# Convenience validation functions
|
|
720
632
|
def validate_and_sanitize_string(value: str, **kwargs: t.Any) -> str:
|
|
721
|
-
"""Validate and return sanitized string, raising on failure."""
|
|
722
633
|
validator = SecureInputValidator()
|
|
723
634
|
result = validator.sanitizer.sanitize_string(value, **kwargs)
|
|
724
635
|
|
|
@@ -728,14 +639,12 @@ def validate_and_sanitize_string(value: str, **kwargs: t.Any) -> str:
|
|
|
728
639
|
error_code=ErrorCode.VALIDATION_ERROR,
|
|
729
640
|
)
|
|
730
641
|
|
|
731
|
-
return result.sanitized_value
|
|
642
|
+
return result.sanitized_value # type: ignore[no-any-return]
|
|
732
643
|
|
|
733
644
|
|
|
734
645
|
def validate_and_sanitize_path(value: str | Path, **kwargs: t.Any) -> Path:
|
|
735
|
-
"""Validate and return sanitized path, raising on failure."""
|
|
736
646
|
validator = SecureInputValidator()
|
|
737
647
|
result = validator.sanitizer.sanitize_string(str(value), **kwargs)
|
|
738
|
-
# Convert back to Path if validation passes
|
|
739
648
|
|
|
740
649
|
if not result.valid:
|
|
741
650
|
raise ExecutionError(
|
|
@@ -747,7 +656,6 @@ def validate_and_sanitize_path(value: str | Path, **kwargs: t.Any) -> Path:
|
|
|
747
656
|
|
|
748
657
|
|
|
749
658
|
def validate_and_parse_json(value: str, **kwargs: t.Any) -> t.Any:
|
|
750
|
-
"""Validate and return parsed JSON, raising on failure."""
|
|
751
659
|
validator = SecureInputValidator()
|
|
752
660
|
result = validator.sanitizer.sanitize_json(value, **kwargs)
|
|
753
661
|
|