crackerjack 0.30.3__py3-none-any.whl → 0.31.7__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 +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +227 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +170 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +657 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +409 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +618 -928
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +585 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +826 -0
- crackerjack/dynamic_config.py +94 -103
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +433 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +443 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +114 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +621 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +372 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +217 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +565 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/coverage_improvement.py +223 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +358 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +356 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +421 -0
- crackerjack/services/git.py +176 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +873 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.7.dist-info/METADATA +742 -0
- crackerjack-0.31.7.dist-info/RECORD +149 -0
- crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/crackerjack.py +0 -3805
- crackerjack/pyproject.toml +0 -286
- crackerjack-0.30.3.dist-info/METADATA +0 -1290
- crackerjack-0.30.3.dist-info/RECORD +0 -16
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .base import (
|
|
5
|
+
AgentContext,
|
|
6
|
+
FixResult,
|
|
7
|
+
Issue,
|
|
8
|
+
IssueType,
|
|
9
|
+
SubAgent,
|
|
10
|
+
agent_registry,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SecurityAgent(SubAgent):
|
|
15
|
+
def __init__(self, context: AgentContext) -> None:
|
|
16
|
+
super().__init__(context)
|
|
17
|
+
self.security_patterns = {
|
|
18
|
+
"hardcoded_temp_paths": r"(?:/tmp/|/temp/|C:\\temp\\|C:\\tmp\\)",
|
|
19
|
+
"shell_injection": r"shell=True|os\.system\(|subprocess\.call\([^)]*shell=True",
|
|
20
|
+
"path_traversal": r"\.\./|\.\.\\",
|
|
21
|
+
"hardcoded_secrets": r"(?:password|secret|key|token)\s*=\s*['\"][^'\"]+['\"]",
|
|
22
|
+
"unsafe_yaml": r"yaml\.load\([^)]*\)",
|
|
23
|
+
"eval_usage": r"\beval\s*\(",
|
|
24
|
+
"exec_usage": r"\bexec\s*\(",
|
|
25
|
+
"pickle_usage": r"\bpickle\.loads?\s*\(",
|
|
26
|
+
"sql_injection": r"(?:execute|query)\s*\([^)]*%[sd]",
|
|
27
|
+
"weak_crypto": r"(?:md5|sha1)\s*\(",
|
|
28
|
+
"insecure_random": r"random\.random\(\)|random\.choice\(",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def get_supported_types(self) -> set[IssueType]:
|
|
32
|
+
return {IssueType.SECURITY}
|
|
33
|
+
|
|
34
|
+
async def can_handle(self, issue: Issue) -> float:
|
|
35
|
+
if issue.type not in self.get_supported_types():
|
|
36
|
+
return 0.0
|
|
37
|
+
|
|
38
|
+
message_lower = issue.message.lower()
|
|
39
|
+
|
|
40
|
+
if any(
|
|
41
|
+
keyword in message_lower
|
|
42
|
+
for keyword in (
|
|
43
|
+
"bandit",
|
|
44
|
+
"security",
|
|
45
|
+
"vulnerability",
|
|
46
|
+
"hardcoded",
|
|
47
|
+
"shell=true",
|
|
48
|
+
"b108",
|
|
49
|
+
"b602",
|
|
50
|
+
"b301",
|
|
51
|
+
"b506",
|
|
52
|
+
"unsafe",
|
|
53
|
+
"injection",
|
|
54
|
+
)
|
|
55
|
+
):
|
|
56
|
+
return 1.0
|
|
57
|
+
|
|
58
|
+
for pattern in self.security_patterns.values():
|
|
59
|
+
if re.search(pattern, issue.message, re.IGNORECASE):
|
|
60
|
+
return 0.9
|
|
61
|
+
|
|
62
|
+
if issue.file_path and any(
|
|
63
|
+
keyword in issue.file_path.lower()
|
|
64
|
+
for keyword in ("security", "auth", "crypto", "password")
|
|
65
|
+
):
|
|
66
|
+
return 0.7
|
|
67
|
+
|
|
68
|
+
if issue.type == IssueType.SECURITY:
|
|
69
|
+
return 0.6
|
|
70
|
+
|
|
71
|
+
return 0.0
|
|
72
|
+
|
|
73
|
+
async def analyze_and_fix(self, issue: Issue) -> FixResult:
|
|
74
|
+
self.log(f"Analyzing security issue: {issue.message}")
|
|
75
|
+
|
|
76
|
+
fixes_applied: list[str] = []
|
|
77
|
+
files_modified: list[str] = []
|
|
78
|
+
recommendations: list[str] = []
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
vulnerability_type = self._identify_vulnerability_type(issue)
|
|
82
|
+
self.log(f"Identified vulnerability type: {vulnerability_type}")
|
|
83
|
+
|
|
84
|
+
fixes_applied, files_modified = await self._apply_vulnerability_fixes(
|
|
85
|
+
vulnerability_type,
|
|
86
|
+
issue,
|
|
87
|
+
fixes_applied,
|
|
88
|
+
files_modified,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
fixes_applied, files_modified = await self._apply_additional_fixes(
|
|
92
|
+
issue,
|
|
93
|
+
fixes_applied,
|
|
94
|
+
files_modified,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
success = len(fixes_applied) > 0
|
|
98
|
+
confidence = 0.85 if success else 0.4
|
|
99
|
+
|
|
100
|
+
if not success:
|
|
101
|
+
recommendations = self._get_security_recommendations()
|
|
102
|
+
|
|
103
|
+
return FixResult(
|
|
104
|
+
success=success,
|
|
105
|
+
confidence=confidence,
|
|
106
|
+
fixes_applied=fixes_applied,
|
|
107
|
+
files_modified=files_modified,
|
|
108
|
+
recommendations=recommendations,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
self.log(f"Error fixing security issue: {e}", "ERROR")
|
|
113
|
+
return self._create_error_fix_result(e)
|
|
114
|
+
|
|
115
|
+
async def _apply_vulnerability_fixes(
|
|
116
|
+
self,
|
|
117
|
+
vulnerability_type: str,
|
|
118
|
+
issue: Issue,
|
|
119
|
+
fixes_applied: list[str],
|
|
120
|
+
files_modified: list[str],
|
|
121
|
+
) -> tuple[list[str], list[str]]:
|
|
122
|
+
vulnerability_fix_map = {
|
|
123
|
+
"hardcoded_temp_paths": self._fix_hardcoded_temp_paths,
|
|
124
|
+
"shell_injection": self._fix_shell_injection,
|
|
125
|
+
"hardcoded_secrets": self._fix_hardcoded_secrets,
|
|
126
|
+
"unsafe_yaml": self._fix_unsafe_yaml,
|
|
127
|
+
"eval_usage": self._fix_eval_usage,
|
|
128
|
+
"weak_crypto": self._fix_weak_crypto,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if fix_method := vulnerability_fix_map.get(vulnerability_type):
|
|
132
|
+
fixes = await fix_method(issue)
|
|
133
|
+
fixes_applied.extend(fixes["fixes"])
|
|
134
|
+
files_modified.extend(fixes["files"])
|
|
135
|
+
|
|
136
|
+
return fixes_applied, files_modified
|
|
137
|
+
|
|
138
|
+
async def _apply_additional_fixes(
|
|
139
|
+
self,
|
|
140
|
+
issue: Issue,
|
|
141
|
+
fixes_applied: list[str],
|
|
142
|
+
files_modified: list[str],
|
|
143
|
+
) -> tuple[list[str], list[str]]:
|
|
144
|
+
if not fixes_applied:
|
|
145
|
+
bandit_fixes = await self._run_bandit_analysis()
|
|
146
|
+
fixes_applied.extend(bandit_fixes)
|
|
147
|
+
|
|
148
|
+
if issue.file_path:
|
|
149
|
+
file_fixes = await self._fix_file_security_issues(issue.file_path)
|
|
150
|
+
fixes_applied.extend(file_fixes["fixes"])
|
|
151
|
+
if file_fixes["fixes"]:
|
|
152
|
+
files_modified.append(issue.file_path)
|
|
153
|
+
|
|
154
|
+
return fixes_applied, files_modified
|
|
155
|
+
|
|
156
|
+
def _get_security_recommendations(self) -> list[str]:
|
|
157
|
+
return [
|
|
158
|
+
"Use tempfile module for temporary file creation",
|
|
159
|
+
"Avoid shell=True in subprocess calls",
|
|
160
|
+
"Use environment variables for secrets",
|
|
161
|
+
"Implement proper input validation",
|
|
162
|
+
"Use safe_load() instead of load() for YAML",
|
|
163
|
+
"Consider using cryptographically secure random module",
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
def _create_error_fix_result(self, error: Exception) -> FixResult:
|
|
167
|
+
return FixResult(
|
|
168
|
+
success=False,
|
|
169
|
+
confidence=0.0,
|
|
170
|
+
remaining_issues=[f"Failed to fix security issue: {error}"],
|
|
171
|
+
recommendations=[
|
|
172
|
+
"Manual security review may be required",
|
|
173
|
+
"Consider running bandit security scanner",
|
|
174
|
+
"Review code for common security anti-patterns",
|
|
175
|
+
],
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def _identify_vulnerability_type(self, issue: Issue) -> str:
|
|
179
|
+
message = issue.message
|
|
180
|
+
|
|
181
|
+
if "B108" in message:
|
|
182
|
+
return "hardcoded_temp_paths"
|
|
183
|
+
if "B602" in message or "shell=True" in message:
|
|
184
|
+
return "shell_injection"
|
|
185
|
+
if "B301" in message or "pickle" in message.lower():
|
|
186
|
+
return "pickle_usage"
|
|
187
|
+
if "B506" in message or "yaml.load" in message:
|
|
188
|
+
return "unsafe_yaml"
|
|
189
|
+
|
|
190
|
+
for pattern_name, pattern in self.security_patterns.items():
|
|
191
|
+
if re.search(pattern, message, re.IGNORECASE):
|
|
192
|
+
return pattern_name
|
|
193
|
+
|
|
194
|
+
return "unknown"
|
|
195
|
+
|
|
196
|
+
async def _fix_hardcoded_temp_paths(self, issue: Issue) -> dict[str, list[str]]:
|
|
197
|
+
fixes: list[str] = []
|
|
198
|
+
files: list[str] = []
|
|
199
|
+
|
|
200
|
+
if not issue.file_path:
|
|
201
|
+
return {"fixes": fixes, "files": files}
|
|
202
|
+
|
|
203
|
+
file_path = Path(issue.file_path)
|
|
204
|
+
if not file_path.exists():
|
|
205
|
+
return {"fixes": fixes, "files": files}
|
|
206
|
+
|
|
207
|
+
content = self.context.get_file_content(file_path)
|
|
208
|
+
if not content:
|
|
209
|
+
return {"fixes": fixes, "files": files}
|
|
210
|
+
|
|
211
|
+
lines = content.split("\n")
|
|
212
|
+
lines, modified = self._process_temp_path_fixes(lines)
|
|
213
|
+
|
|
214
|
+
if modified:
|
|
215
|
+
if self.context.write_file_content(file_path, "\n".join(lines)):
|
|
216
|
+
fixes.append(f"Fixed hardcoded temp paths in {issue.file_path}")
|
|
217
|
+
files.append(str(file_path))
|
|
218
|
+
self.log(f"Fixed hardcoded temp paths in {issue.file_path}")
|
|
219
|
+
|
|
220
|
+
return {"fixes": fixes, "files": files}
|
|
221
|
+
|
|
222
|
+
def _process_temp_path_fixes(self, lines: list[str]) -> tuple[list[str], bool]:
|
|
223
|
+
modified = False
|
|
224
|
+
|
|
225
|
+
lines, import_added = self._ensure_tempfile_import(lines)
|
|
226
|
+
if import_added:
|
|
227
|
+
modified = True
|
|
228
|
+
|
|
229
|
+
lines, paths_replaced = self._replace_hardcoded_temp_paths(lines)
|
|
230
|
+
if paths_replaced:
|
|
231
|
+
modified = True
|
|
232
|
+
|
|
233
|
+
return lines, modified
|
|
234
|
+
|
|
235
|
+
def _ensure_tempfile_import(self, lines: list[str]) -> tuple[list[str], bool]:
|
|
236
|
+
has_tempfile_import = any("import tempfile" in line for line in lines)
|
|
237
|
+
if has_tempfile_import:
|
|
238
|
+
return lines, False
|
|
239
|
+
|
|
240
|
+
import_section_end = 0
|
|
241
|
+
for i, line in enumerate(lines):
|
|
242
|
+
if line.strip().startswith(("import ", "from ")):
|
|
243
|
+
import_section_end = i + 1
|
|
244
|
+
elif line.strip() == "" and import_section_end > 0:
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
lines.insert(import_section_end, "import tempfile")
|
|
248
|
+
return lines, True
|
|
249
|
+
|
|
250
|
+
def _replace_hardcoded_temp_paths(self, lines: list[str]) -> tuple[list[str], bool]:
|
|
251
|
+
replacements = [
|
|
252
|
+
(r'Path\("/tmp/([^"]+)"\)', r'Path(tempfile.gettempdir()) / "\1"'),
|
|
253
|
+
(r'"/tmp/([^"]+)"', r'str(Path(tempfile.gettempdir()) / "\1")'),
|
|
254
|
+
(r"'/tmp/([^']+)'", r"str(Path(tempfile.gettempdir()) / '\1')"),
|
|
255
|
+
(
|
|
256
|
+
r'Path\("/test/path"\)',
|
|
257
|
+
r"Path(tempfile.gettempdir()) / 'test-path'",
|
|
258
|
+
),
|
|
259
|
+
(r'"/test/path"', r'str(Path(tempfile.gettempdir()) / "test-path")'),
|
|
260
|
+
(r"'/test/path'", r"str(Path(tempfile.gettempdir()) / 'test-path')"),
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
modified = False
|
|
264
|
+
for pattern, replacement in replacements:
|
|
265
|
+
new_content = "\n".join(lines)
|
|
266
|
+
if re.search(pattern, new_content):
|
|
267
|
+
lines = re.sub(pattern, replacement, new_content).split("\n")
|
|
268
|
+
modified = True
|
|
269
|
+
|
|
270
|
+
return lines, modified
|
|
271
|
+
|
|
272
|
+
async def _fix_shell_injection(self, issue: Issue) -> dict[str, list[str]]:
|
|
273
|
+
fixes: list[str] = []
|
|
274
|
+
files: list[str] = []
|
|
275
|
+
|
|
276
|
+
if not issue.file_path:
|
|
277
|
+
return {"fixes": fixes, "files": files}
|
|
278
|
+
|
|
279
|
+
file_path = Path(issue.file_path)
|
|
280
|
+
content = self.context.get_file_content(file_path)
|
|
281
|
+
if not content:
|
|
282
|
+
return {"fixes": fixes, "files": files}
|
|
283
|
+
|
|
284
|
+
original_content = content
|
|
285
|
+
|
|
286
|
+
patterns = [
|
|
287
|
+
(
|
|
288
|
+
r"subprocess\.run\(([^,]+),\s*shell=True\)",
|
|
289
|
+
r"subprocess.run(\1.split())",
|
|
290
|
+
),
|
|
291
|
+
(
|
|
292
|
+
r"subprocess\.call\(([^,]+),\s*shell=True\)",
|
|
293
|
+
r"subprocess.call(\1.split())",
|
|
294
|
+
),
|
|
295
|
+
(
|
|
296
|
+
r"subprocess\.Popen\(([^,]+),\s*shell=True\)",
|
|
297
|
+
r"subprocess.Popen(\1.split())",
|
|
298
|
+
),
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
for pattern, replacement in patterns:
|
|
302
|
+
content = re.sub(pattern, replacement, content)
|
|
303
|
+
|
|
304
|
+
if content != original_content:
|
|
305
|
+
if self.context.write_file_content(file_path, content):
|
|
306
|
+
fixes.append(
|
|
307
|
+
f"Fixed shell injection vulnerability in {issue.file_path}",
|
|
308
|
+
)
|
|
309
|
+
files.append(str(file_path))
|
|
310
|
+
self.log(f"Fixed shell injection in {issue.file_path}")
|
|
311
|
+
|
|
312
|
+
return {"fixes": fixes, "files": files}
|
|
313
|
+
|
|
314
|
+
async def _fix_hardcoded_secrets(self, issue: Issue) -> dict[str, list[str]]:
|
|
315
|
+
fixes: list[str] = []
|
|
316
|
+
files: list[str] = []
|
|
317
|
+
|
|
318
|
+
if not issue.file_path:
|
|
319
|
+
return {"fixes": fixes, "files": files}
|
|
320
|
+
|
|
321
|
+
file_path = Path(issue.file_path)
|
|
322
|
+
content = self.context.get_file_content(file_path)
|
|
323
|
+
if not content:
|
|
324
|
+
return {"fixes": fixes, "files": files}
|
|
325
|
+
|
|
326
|
+
lines = content.split("\n")
|
|
327
|
+
lines, modified = self._process_hardcoded_secrets_in_lines(lines)
|
|
328
|
+
|
|
329
|
+
if modified:
|
|
330
|
+
if self.context.write_file_content(file_path, "\n".join(lines)):
|
|
331
|
+
fixes.append(f"Fixed hardcoded secrets in {issue.file_path}")
|
|
332
|
+
files.append(str(file_path))
|
|
333
|
+
self.log(f"Fixed hardcoded secrets in {issue.file_path}")
|
|
334
|
+
|
|
335
|
+
return {"fixes": fixes, "files": files}
|
|
336
|
+
|
|
337
|
+
def _process_hardcoded_secrets_in_lines(
|
|
338
|
+
self,
|
|
339
|
+
lines: list[str],
|
|
340
|
+
) -> tuple[list[str], bool]:
|
|
341
|
+
modified = False
|
|
342
|
+
|
|
343
|
+
lines, import_added = self._ensure_os_import(lines)
|
|
344
|
+
if import_added:
|
|
345
|
+
modified = True
|
|
346
|
+
|
|
347
|
+
for i, line in enumerate(lines):
|
|
348
|
+
if self._line_contains_hardcoded_secret(line):
|
|
349
|
+
new_line = self._replace_hardcoded_secret_with_env_var(line)
|
|
350
|
+
if new_line != line:
|
|
351
|
+
lines[i] = new_line
|
|
352
|
+
modified = True
|
|
353
|
+
|
|
354
|
+
return lines, modified
|
|
355
|
+
|
|
356
|
+
def _ensure_os_import(self, lines: list[str]) -> tuple[list[str], bool]:
|
|
357
|
+
has_os_import = any("import os" in line for line in lines)
|
|
358
|
+
if has_os_import:
|
|
359
|
+
return lines, False
|
|
360
|
+
|
|
361
|
+
for i, line in enumerate(lines):
|
|
362
|
+
if line.strip().startswith(("import ", "from ")):
|
|
363
|
+
lines.insert(i, "import os")
|
|
364
|
+
return lines, True
|
|
365
|
+
|
|
366
|
+
return lines, False
|
|
367
|
+
|
|
368
|
+
def _line_contains_hardcoded_secret(self, line: str) -> bool:
|
|
369
|
+
return bool(
|
|
370
|
+
re.search(
|
|
371
|
+
r'(password|secret|key|token)\s*=\s*[\'"][^\'"]+[\'"]',
|
|
372
|
+
line,
|
|
373
|
+
re.IGNORECASE,
|
|
374
|
+
),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def _replace_hardcoded_secret_with_env_var(self, line: str) -> str:
|
|
378
|
+
match = re.search(r"(\w+)\s*=", line)
|
|
379
|
+
if match:
|
|
380
|
+
var_name = match.group(1)
|
|
381
|
+
env_var_name = var_name.upper()
|
|
382
|
+
return f"{var_name} = os.getenv('{env_var_name}', '')"
|
|
383
|
+
return line
|
|
384
|
+
|
|
385
|
+
async def _fix_unsafe_yaml(self, issue: Issue) -> dict[str, list[str]]:
|
|
386
|
+
fixes: list[str] = []
|
|
387
|
+
files: list[str] = []
|
|
388
|
+
|
|
389
|
+
if not issue.file_path:
|
|
390
|
+
return {"fixes": fixes, "files": files}
|
|
391
|
+
|
|
392
|
+
file_path = Path(issue.file_path)
|
|
393
|
+
content = self.context.get_file_content(file_path)
|
|
394
|
+
if not content:
|
|
395
|
+
return {"fixes": fixes, "files": files}
|
|
396
|
+
|
|
397
|
+
original_content = content
|
|
398
|
+
|
|
399
|
+
content = re.sub(r"\byaml\.load\(", "yaml.safe_load(", content)
|
|
400
|
+
|
|
401
|
+
if content != original_content:
|
|
402
|
+
if self.context.write_file_content(file_path, content):
|
|
403
|
+
fixes.append(f"Fixed unsafe YAML loading in {issue.file_path}")
|
|
404
|
+
files.append(str(file_path))
|
|
405
|
+
self.log(f"Fixed unsafe YAML loading in {issue.file_path}")
|
|
406
|
+
|
|
407
|
+
return {"fixes": fixes, "files": files}
|
|
408
|
+
|
|
409
|
+
async def _fix_eval_usage(self, issue: Issue) -> dict[str, list[str]]:
|
|
410
|
+
fixes: list[str] = []
|
|
411
|
+
files: list[str] = []
|
|
412
|
+
|
|
413
|
+
fixes.append(
|
|
414
|
+
f"Identified eval() usage in {issue.file_path} - manual review required",
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
return {"fixes": fixes, "files": files}
|
|
418
|
+
|
|
419
|
+
async def _fix_weak_crypto(self, issue: Issue) -> dict[str, list[str]]:
|
|
420
|
+
fixes: list[str] = []
|
|
421
|
+
files: list[str] = []
|
|
422
|
+
|
|
423
|
+
if not issue.file_path:
|
|
424
|
+
return {"fixes": fixes, "files": files}
|
|
425
|
+
|
|
426
|
+
file_path = Path(issue.file_path)
|
|
427
|
+
content = self.context.get_file_content(file_path)
|
|
428
|
+
if not content:
|
|
429
|
+
return {"fixes": fixes, "files": files}
|
|
430
|
+
|
|
431
|
+
original_content = content
|
|
432
|
+
|
|
433
|
+
replacements = [
|
|
434
|
+
(r"\bhashlib\.md5\(", "hashlib.sha256("),
|
|
435
|
+
(r"\bhashlib\.sha1\(", "hashlib.sha256("),
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
for pattern, replacement in replacements:
|
|
439
|
+
content = re.sub(pattern, replacement, content)
|
|
440
|
+
|
|
441
|
+
if content != original_content:
|
|
442
|
+
if self.context.write_file_content(file_path, content):
|
|
443
|
+
fixes.append(f"Upgraded weak cryptographic hashes in {issue.file_path}")
|
|
444
|
+
files.append(str(file_path))
|
|
445
|
+
self.log(f"Fixed weak crypto in {issue.file_path}")
|
|
446
|
+
|
|
447
|
+
return {"fixes": fixes, "files": files}
|
|
448
|
+
|
|
449
|
+
async def _run_bandit_analysis(self) -> list[str]:
|
|
450
|
+
fixes: list[str] = []
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
returncode, _, _ = await self.run_command(
|
|
454
|
+
["uv", "run", "bandit", "-r", "crackerjack/", "-f", "txt"],
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
if returncode == 0:
|
|
458
|
+
fixes.append("Bandit security scan completed successfully")
|
|
459
|
+
else:
|
|
460
|
+
fixes.append("Bandit identified security issues for review")
|
|
461
|
+
|
|
462
|
+
except Exception as e:
|
|
463
|
+
self.log(f"Bandit analysis failed: {e}", "WARN")
|
|
464
|
+
|
|
465
|
+
return fixes
|
|
466
|
+
|
|
467
|
+
async def _fix_file_security_issues(self, file_path: str) -> dict[str, list[str]]:
|
|
468
|
+
fixes: list[str] = []
|
|
469
|
+
files: list[str] = []
|
|
470
|
+
|
|
471
|
+
try:
|
|
472
|
+
path = Path(file_path)
|
|
473
|
+
if not self._is_valid_file_path(path):
|
|
474
|
+
return {"fixes": fixes, "files": files}
|
|
475
|
+
|
|
476
|
+
content = self.context.get_file_content(path)
|
|
477
|
+
if not content:
|
|
478
|
+
return {"fixes": fixes, "files": files}
|
|
479
|
+
|
|
480
|
+
original_content = content
|
|
481
|
+
content = await self._apply_security_fixes_to_content(content)
|
|
482
|
+
|
|
483
|
+
if content != original_content:
|
|
484
|
+
if self.context.write_file_content(path, content):
|
|
485
|
+
fixes.append(f"Applied general security fixes to {file_path}")
|
|
486
|
+
files.append(file_path)
|
|
487
|
+
self.log(f"Applied security fixes to {file_path}")
|
|
488
|
+
|
|
489
|
+
except Exception as e:
|
|
490
|
+
self.log(f"Error fixing file security issues in {file_path}: {e}", "ERROR")
|
|
491
|
+
|
|
492
|
+
return {"fixes": fixes, "files": files}
|
|
493
|
+
|
|
494
|
+
def _is_valid_file_path(self, path: Path) -> bool:
|
|
495
|
+
return path.exists() and path.is_file()
|
|
496
|
+
|
|
497
|
+
async def _apply_security_fixes_to_content(self, content: str) -> str:
|
|
498
|
+
content = await self._fix_insecure_random_usage(content)
|
|
499
|
+
return self._remove_debug_prints_with_secrets(content)
|
|
500
|
+
|
|
501
|
+
async def _fix_insecure_random_usage(self, content: str) -> str:
|
|
502
|
+
if not re.search(r"random\.(?:random|choice)\(", content):
|
|
503
|
+
return content
|
|
504
|
+
|
|
505
|
+
content = self._add_secrets_import_if_needed(content)
|
|
506
|
+
|
|
507
|
+
return re.sub(r"random\.choice\(([^)]+)\)", r"secrets.choice(\1)", content)
|
|
508
|
+
|
|
509
|
+
def _add_secrets_import_if_needed(self, content: str) -> str:
|
|
510
|
+
if "import secrets" in content:
|
|
511
|
+
return content
|
|
512
|
+
|
|
513
|
+
lines = content.split("\n")
|
|
514
|
+
for i, line in enumerate(lines):
|
|
515
|
+
if line.strip().startswith(("import ", "from ")):
|
|
516
|
+
lines.insert(i + 1, "import secrets")
|
|
517
|
+
break
|
|
518
|
+
return "\n".join(lines)
|
|
519
|
+
|
|
520
|
+
def _remove_debug_prints_with_secrets(self, content: str) -> str:
|
|
521
|
+
return re.sub(
|
|
522
|
+
r"print\s*\([^)]*(?:password|secret|key|token)[^)]*\)",
|
|
523
|
+
"",
|
|
524
|
+
content,
|
|
525
|
+
flags=re.IGNORECASE,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
agent_registry.register(SecurityAgent)
|