codeframe-ai 0.9.0__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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Code quality analysis tools."""
|
|
2
|
+
|
|
3
|
+
from codeframe.lib.quality.complexity_analyzer import ComplexityAnalyzer
|
|
4
|
+
from codeframe.lib.quality.security_scanner import SecurityScanner
|
|
5
|
+
from codeframe.lib.quality.owasp_patterns import OWASPPatterns
|
|
6
|
+
|
|
7
|
+
__all__ = ["ComplexityAnalyzer", "SecurityScanner", "OWASPPatterns"]
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Complexity analysis using radon.
|
|
2
|
+
|
|
3
|
+
Analyzes code complexity using cyclomatic complexity, Halstead metrics,
|
|
4
|
+
and maintainability index.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
from radon.complexity import cc_visit
|
|
12
|
+
from radon.metrics import mi_visit
|
|
13
|
+
|
|
14
|
+
from codeframe.core.models import ReviewFinding
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ComplexityAnalyzer:
|
|
20
|
+
"""Analyzes code complexity using radon.
|
|
21
|
+
|
|
22
|
+
Thresholds (cyclomatic complexity):
|
|
23
|
+
- 1-5: Simple (A) - no finding
|
|
24
|
+
- 6-10: Moderate (B) - medium severity
|
|
25
|
+
- 11-20: Complex (C) - high severity
|
|
26
|
+
- 21-50: Very Complex (D) - high severity
|
|
27
|
+
- 51+: Extremely Complex (F) - critical severity
|
|
28
|
+
|
|
29
|
+
Function length threshold: >50 lines triggers finding
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# Complexity thresholds
|
|
33
|
+
SIMPLE_THRESHOLD = 5
|
|
34
|
+
MODERATE_THRESHOLD = 10
|
|
35
|
+
COMPLEX_THRESHOLD = 20
|
|
36
|
+
VERY_COMPLEX_THRESHOLD = 50
|
|
37
|
+
|
|
38
|
+
# Function length threshold
|
|
39
|
+
MAX_FUNCTION_LENGTH = 50
|
|
40
|
+
|
|
41
|
+
# Maintainability Index thresholds (0-100, higher is better)
|
|
42
|
+
MI_LOW_THRESHOLD = 20
|
|
43
|
+
MI_MEDIUM_THRESHOLD = 50
|
|
44
|
+
|
|
45
|
+
def __init__(self, project_path: Path):
|
|
46
|
+
"""Initialize ComplexityAnalyzer.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
project_path: Path to project root directory
|
|
50
|
+
"""
|
|
51
|
+
self.project_path = Path(project_path)
|
|
52
|
+
|
|
53
|
+
def analyze_file(self, file_path: Path) -> List[ReviewFinding]:
|
|
54
|
+
"""Analyze a single file for complexity issues.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
file_path: Path to Python file to analyze
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of ReviewFinding objects for complexity issues
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
FileNotFoundError: If file doesn't exist
|
|
64
|
+
"""
|
|
65
|
+
file_path = Path(file_path)
|
|
66
|
+
|
|
67
|
+
if not file_path.exists():
|
|
68
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
69
|
+
|
|
70
|
+
# Skip non-Python files
|
|
71
|
+
if file_path.suffix != ".py":
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
# Read file content
|
|
75
|
+
try:
|
|
76
|
+
code = file_path.read_text()
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"Error reading file {file_path}: {e}")
|
|
79
|
+
return []
|
|
80
|
+
|
|
81
|
+
if not code.strip():
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
findings = []
|
|
85
|
+
|
|
86
|
+
# Analyze cyclomatic complexity
|
|
87
|
+
try:
|
|
88
|
+
complexity_findings = self._analyze_cyclomatic_complexity(file_path, code)
|
|
89
|
+
findings.extend(complexity_findings)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.warning(f"Error analyzing cyclomatic complexity for {file_path}: {e}")
|
|
92
|
+
|
|
93
|
+
# Analyze function length
|
|
94
|
+
try:
|
|
95
|
+
length_findings = self._analyze_function_length(file_path, code)
|
|
96
|
+
findings.extend(length_findings)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.warning(f"Error analyzing function length for {file_path}: {e}")
|
|
99
|
+
|
|
100
|
+
# Analyze maintainability index
|
|
101
|
+
try:
|
|
102
|
+
mi_findings = self._analyze_maintainability_index(file_path, code)
|
|
103
|
+
findings.extend(mi_findings)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.warning(f"Error analyzing maintainability index for {file_path}: {e}")
|
|
106
|
+
|
|
107
|
+
return findings
|
|
108
|
+
|
|
109
|
+
def _analyze_cyclomatic_complexity(self, file_path: Path, code: str) -> List[ReviewFinding]:
|
|
110
|
+
"""Analyze cyclomatic complexity using radon.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
file_path: Path to file being analyzed
|
|
114
|
+
code: File content
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of findings for high complexity
|
|
118
|
+
"""
|
|
119
|
+
findings = []
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
# Get complexity metrics for all functions/classes
|
|
123
|
+
complexity_blocks = cc_visit(code)
|
|
124
|
+
|
|
125
|
+
for block in complexity_blocks:
|
|
126
|
+
# block attributes: name, lineno, col_offset, endline, complexity, classname
|
|
127
|
+
cc = block.complexity
|
|
128
|
+
|
|
129
|
+
# Determine severity based on complexity
|
|
130
|
+
if cc <= self.SIMPLE_THRESHOLD:
|
|
131
|
+
continue # No finding for simple code
|
|
132
|
+
elif cc <= self.MODERATE_THRESHOLD:
|
|
133
|
+
severity = "medium"
|
|
134
|
+
suggestion = "Consider breaking this function into smaller functions"
|
|
135
|
+
elif cc <= self.COMPLEX_THRESHOLD:
|
|
136
|
+
severity = "high"
|
|
137
|
+
suggestion = (
|
|
138
|
+
"This function is too complex. Break it into smaller, focused functions"
|
|
139
|
+
)
|
|
140
|
+
elif cc <= self.VERY_COMPLEX_THRESHOLD:
|
|
141
|
+
severity = "high"
|
|
142
|
+
suggestion = "URGENT: This function is very complex. Refactor immediately to improve maintainability"
|
|
143
|
+
else:
|
|
144
|
+
severity = "critical"
|
|
145
|
+
suggestion = "CRITICAL: This function is extremely complex. Refactor is required before merging"
|
|
146
|
+
|
|
147
|
+
if cc > self.SIMPLE_THRESHOLD:
|
|
148
|
+
message = f"Cyclomatic complexity {cc} (threshold: {self.SIMPLE_THRESHOLD})"
|
|
149
|
+
if block.classname:
|
|
150
|
+
message = f"{block.classname}.{block.name}: {message}"
|
|
151
|
+
else:
|
|
152
|
+
message = f"{block.name}: {message}"
|
|
153
|
+
|
|
154
|
+
findings.append(
|
|
155
|
+
ReviewFinding(
|
|
156
|
+
category="complexity",
|
|
157
|
+
severity=severity,
|
|
158
|
+
file_path=str(file_path),
|
|
159
|
+
line_number=block.lineno,
|
|
160
|
+
message=message,
|
|
161
|
+
suggestion=suggestion,
|
|
162
|
+
tool="radon",
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
except SyntaxError:
|
|
167
|
+
# File has syntax errors, skip complexity analysis
|
|
168
|
+
logger.debug(f"Syntax error in {file_path}, skipping complexity analysis")
|
|
169
|
+
|
|
170
|
+
return findings
|
|
171
|
+
|
|
172
|
+
def _analyze_function_length(self, file_path: Path, code: str) -> List[ReviewFinding]:
|
|
173
|
+
"""Analyze function length.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
file_path: Path to file being analyzed
|
|
177
|
+
code: File content
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of findings for overly long functions
|
|
181
|
+
"""
|
|
182
|
+
findings = []
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
complexity_blocks = cc_visit(code)
|
|
186
|
+
|
|
187
|
+
for block in complexity_blocks:
|
|
188
|
+
# Calculate function length
|
|
189
|
+
if hasattr(block, "endline") and hasattr(block, "lineno"):
|
|
190
|
+
length = block.endline - block.lineno + 1
|
|
191
|
+
|
|
192
|
+
if length > self.MAX_FUNCTION_LENGTH:
|
|
193
|
+
severity = "medium" if length < 100 else "high"
|
|
194
|
+
|
|
195
|
+
message = f"Function length {length} lines (threshold: {self.MAX_FUNCTION_LENGTH})"
|
|
196
|
+
if block.classname:
|
|
197
|
+
message = f"{block.classname}.{block.name}: {message}"
|
|
198
|
+
else:
|
|
199
|
+
message = f"{block.name}: {message}"
|
|
200
|
+
|
|
201
|
+
findings.append(
|
|
202
|
+
ReviewFinding(
|
|
203
|
+
category="complexity",
|
|
204
|
+
severity=severity,
|
|
205
|
+
file_path=str(file_path),
|
|
206
|
+
line_number=block.lineno,
|
|
207
|
+
message=message,
|
|
208
|
+
suggestion="Break this long function into smaller, focused functions",
|
|
209
|
+
tool="radon",
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
except SyntaxError:
|
|
214
|
+
logger.debug(f"Syntax error in {file_path}, skipping length analysis")
|
|
215
|
+
|
|
216
|
+
return findings
|
|
217
|
+
|
|
218
|
+
def _analyze_maintainability_index(self, file_path: Path, code: str) -> List[ReviewFinding]:
|
|
219
|
+
"""Analyze maintainability index.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
file_path: Path to file being analyzed
|
|
223
|
+
code: File content
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
List of findings for low maintainability
|
|
227
|
+
"""
|
|
228
|
+
findings = []
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
# Get maintainability index (0-100, higher is better)
|
|
232
|
+
mi = mi_visit(code, multi=True)
|
|
233
|
+
|
|
234
|
+
if mi < self.MI_LOW_THRESHOLD:
|
|
235
|
+
severity = "high"
|
|
236
|
+
message = f"Very low maintainability index: {mi:.1f}/100"
|
|
237
|
+
suggestion = "This code is very difficult to maintain. Consider refactoring to improve readability"
|
|
238
|
+
elif mi < self.MI_MEDIUM_THRESHOLD:
|
|
239
|
+
severity = "medium"
|
|
240
|
+
message = f"Low maintainability index: {mi:.1f}/100"
|
|
241
|
+
suggestion = "This code could be more maintainable. Consider refactoring"
|
|
242
|
+
else:
|
|
243
|
+
# Good maintainability, no finding
|
|
244
|
+
return findings
|
|
245
|
+
|
|
246
|
+
findings.append(
|
|
247
|
+
ReviewFinding(
|
|
248
|
+
category="complexity",
|
|
249
|
+
severity=severity,
|
|
250
|
+
file_path=str(file_path),
|
|
251
|
+
line_number=1, # File-level metric
|
|
252
|
+
message=message,
|
|
253
|
+
suggestion=suggestion,
|
|
254
|
+
tool="radon",
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
except Exception as e:
|
|
259
|
+
# MI calculation can fail for various reasons
|
|
260
|
+
logger.debug(f"Could not calculate MI for {file_path}: {e}")
|
|
261
|
+
|
|
262
|
+
return findings
|
|
263
|
+
|
|
264
|
+
def analyze_files(self, file_paths: List[Path]) -> List[ReviewFinding]:
|
|
265
|
+
"""Analyze multiple files for complexity issues.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
file_paths: List of file paths to analyze
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
List of all findings from all files
|
|
272
|
+
"""
|
|
273
|
+
all_findings = []
|
|
274
|
+
|
|
275
|
+
for file_path in file_paths:
|
|
276
|
+
try:
|
|
277
|
+
findings = self.analyze_file(file_path)
|
|
278
|
+
all_findings.extend(findings)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.error(f"Error analyzing {file_path}: {e}")
|
|
281
|
+
|
|
282
|
+
return all_findings
|
|
283
|
+
|
|
284
|
+
def calculate_score(self, file_paths: List[Path]) -> float:
|
|
285
|
+
"""Calculate overall complexity score (0-100, higher is better).
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
file_paths: List of file paths to analyze
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Overall complexity score (0-100)
|
|
292
|
+
"""
|
|
293
|
+
if not file_paths:
|
|
294
|
+
return 100.0 # No files = perfect score
|
|
295
|
+
|
|
296
|
+
findings = self.analyze_files(file_paths)
|
|
297
|
+
|
|
298
|
+
# Calculate penalty based on severity
|
|
299
|
+
penalty = 0
|
|
300
|
+
for finding in findings:
|
|
301
|
+
if finding.severity == "critical":
|
|
302
|
+
penalty += 20
|
|
303
|
+
elif finding.severity == "high":
|
|
304
|
+
penalty += 10
|
|
305
|
+
elif finding.severity == "medium":
|
|
306
|
+
penalty += 5
|
|
307
|
+
elif finding.severity == "low":
|
|
308
|
+
penalty += 2
|
|
309
|
+
|
|
310
|
+
# Normalize penalty based on number of files
|
|
311
|
+
penalty_per_file = penalty / len(file_paths)
|
|
312
|
+
|
|
313
|
+
# Calculate score (start at 100, subtract penalties)
|
|
314
|
+
score = max(0, 100 - penalty_per_file)
|
|
315
|
+
|
|
316
|
+
return score
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""OWASP Top 10 pattern detection.
|
|
2
|
+
|
|
3
|
+
Checks for OWASP A03 (Injection) and A07 (Authentication Failures) patterns.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List
|
|
10
|
+
|
|
11
|
+
from codeframe.core.models import ReviewFinding
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OWASPPatterns:
|
|
17
|
+
"""Detects OWASP Top 10 security patterns.
|
|
18
|
+
|
|
19
|
+
Currently implements:
|
|
20
|
+
- A03:2021 - Injection (SQL, NoSQL, Command)
|
|
21
|
+
- A07:2021 - Identification and Authentication Failures
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, project_path: Path):
|
|
25
|
+
"""Initialize OWASPPatterns.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
project_path: Path to project root directory
|
|
29
|
+
"""
|
|
30
|
+
self.project_path = Path(project_path)
|
|
31
|
+
|
|
32
|
+
# Compile regex patterns for performance
|
|
33
|
+
self._compile_patterns()
|
|
34
|
+
|
|
35
|
+
def _compile_patterns(self):
|
|
36
|
+
"""Compile regex patterns for pattern matching."""
|
|
37
|
+
# A03: Injection patterns
|
|
38
|
+
self.sql_concat_pattern = re.compile(
|
|
39
|
+
r'["\']SELECT.*?\+|f["\']SELECT.*?\{|\.format\(.*?SELECT'
|
|
40
|
+
)
|
|
41
|
+
self.sql_fstring_pattern = re.compile(r'f["\'].*?SELECT.*?\{')
|
|
42
|
+
self.nosql_eval_pattern = re.compile(r"\beval\s*\(")
|
|
43
|
+
self.command_injection_pattern = re.compile(r"os\.system\s*\(|subprocess.*shell\s*=\s*True")
|
|
44
|
+
|
|
45
|
+
# A07: Authentication patterns
|
|
46
|
+
self.hardcoded_password_pattern = re.compile(
|
|
47
|
+
r'(password|passwd|pwd|secret|api_key|token)\s*=\s*["\'][^"\']+["\']',
|
|
48
|
+
re.IGNORECASE,
|
|
49
|
+
)
|
|
50
|
+
self.weak_password_pattern = re.compile(r"len\(password\)\s*>=?\s*[1-6]\b")
|
|
51
|
+
|
|
52
|
+
def check_file(self, file_path: Path) -> List[ReviewFinding]:
|
|
53
|
+
"""Check a single file for OWASP patterns.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
file_path: Path to Python file to check
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
List of ReviewFinding objects for OWASP violations
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
FileNotFoundError: If file doesn't exist
|
|
63
|
+
"""
|
|
64
|
+
file_path = Path(file_path)
|
|
65
|
+
|
|
66
|
+
if not file_path.exists():
|
|
67
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
68
|
+
|
|
69
|
+
# Skip non-Python files
|
|
70
|
+
if file_path.suffix != ".py":
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
# Read file content
|
|
74
|
+
try:
|
|
75
|
+
code = file_path.read_text()
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"Error reading file {file_path}: {e}")
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
if not code.strip():
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
findings = []
|
|
84
|
+
|
|
85
|
+
# Check A03: Injection patterns
|
|
86
|
+
findings.extend(self._check_sql_injection(file_path, code))
|
|
87
|
+
findings.extend(self._check_nosql_injection(file_path, code))
|
|
88
|
+
findings.extend(self._check_command_injection(file_path, code))
|
|
89
|
+
|
|
90
|
+
# Check A07: Authentication failures
|
|
91
|
+
findings.extend(self._check_hardcoded_credentials(file_path, code))
|
|
92
|
+
findings.extend(self._check_weak_password_validation(file_path, code))
|
|
93
|
+
|
|
94
|
+
return findings
|
|
95
|
+
|
|
96
|
+
def _check_sql_injection(self, file_path: Path, code: str) -> List[ReviewFinding]:
|
|
97
|
+
"""Check for SQL injection vulnerabilities.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
file_path: Path to file being checked
|
|
101
|
+
code: File content
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of findings for SQL injection issues
|
|
105
|
+
"""
|
|
106
|
+
findings = []
|
|
107
|
+
lines = code.split("\n")
|
|
108
|
+
|
|
109
|
+
for line_no, line in enumerate(lines, start=1):
|
|
110
|
+
# Check for string concatenation in SQL queries
|
|
111
|
+
if self.sql_concat_pattern.search(line) or self.sql_fstring_pattern.search(line):
|
|
112
|
+
# Make sure it's actually a SQL query
|
|
113
|
+
if any(
|
|
114
|
+
keyword in line.upper()
|
|
115
|
+
for keyword in ["SELECT", "INSERT", "UPDATE", "DELETE", "FROM", "WHERE"]
|
|
116
|
+
):
|
|
117
|
+
findings.append(
|
|
118
|
+
ReviewFinding(
|
|
119
|
+
category="security",
|
|
120
|
+
severity="critical",
|
|
121
|
+
file_path=str(file_path),
|
|
122
|
+
line_number=line_no,
|
|
123
|
+
message="[A03] Potential SQL injection vulnerability detected",
|
|
124
|
+
suggestion="Use parameterized queries or an ORM to prevent SQL injection",
|
|
125
|
+
tool="owasp",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return findings
|
|
130
|
+
|
|
131
|
+
def _check_nosql_injection(self, file_path: Path, code: str) -> List[ReviewFinding]:
|
|
132
|
+
"""Check for NoSQL injection vulnerabilities.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
file_path: Path to file being checked
|
|
136
|
+
code: File content
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
List of findings for NoSQL injection issues
|
|
140
|
+
"""
|
|
141
|
+
findings = []
|
|
142
|
+
lines = code.split("\n")
|
|
143
|
+
|
|
144
|
+
for line_no, line in enumerate(lines, start=1):
|
|
145
|
+
# Check for eval() usage (extremely dangerous)
|
|
146
|
+
if self.nosql_eval_pattern.search(line):
|
|
147
|
+
findings.append(
|
|
148
|
+
ReviewFinding(
|
|
149
|
+
category="security",
|
|
150
|
+
severity="critical",
|
|
151
|
+
file_path=str(file_path),
|
|
152
|
+
line_number=line_no,
|
|
153
|
+
message="[A03] eval() usage detected - extremely dangerous",
|
|
154
|
+
suggestion="Never use eval() with user input. Use ast.literal_eval() or JSON parsing instead",
|
|
155
|
+
tool="owasp",
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return findings
|
|
160
|
+
|
|
161
|
+
def _check_command_injection(self, file_path: Path, code: str) -> List[ReviewFinding]:
|
|
162
|
+
"""Check for command injection vulnerabilities.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
file_path: Path to file being checked
|
|
166
|
+
code: File content
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
List of findings for command injection issues
|
|
170
|
+
"""
|
|
171
|
+
findings = []
|
|
172
|
+
lines = code.split("\n")
|
|
173
|
+
|
|
174
|
+
for line_no, line in enumerate(lines, start=1):
|
|
175
|
+
# Check for os.system() or subprocess with shell=True
|
|
176
|
+
if self.command_injection_pattern.search(line):
|
|
177
|
+
findings.append(
|
|
178
|
+
ReviewFinding(
|
|
179
|
+
category="security",
|
|
180
|
+
severity="high",
|
|
181
|
+
file_path=str(file_path),
|
|
182
|
+
line_number=line_no,
|
|
183
|
+
message="[A03] Potential command injection vulnerability",
|
|
184
|
+
suggestion="Use subprocess with shell=False and validate all inputs. Avoid os.system()",
|
|
185
|
+
tool="owasp",
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return findings
|
|
190
|
+
|
|
191
|
+
def _check_hardcoded_credentials(self, file_path: Path, code: str) -> List[ReviewFinding]:
|
|
192
|
+
"""Check for hardcoded credentials.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
file_path: Path to file being checked
|
|
196
|
+
code: File content
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of findings for hardcoded credentials
|
|
200
|
+
"""
|
|
201
|
+
findings = []
|
|
202
|
+
lines = code.split("\n")
|
|
203
|
+
|
|
204
|
+
for line_no, line in enumerate(lines, start=1):
|
|
205
|
+
# Skip comments
|
|
206
|
+
if line.strip().startswith("#"):
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
# Check for hardcoded passwords/secrets
|
|
210
|
+
match = self.hardcoded_password_pattern.search(line)
|
|
211
|
+
if match:
|
|
212
|
+
# Filter out common false positives
|
|
213
|
+
if any(
|
|
214
|
+
fp in line.lower() for fp in ["test", "example", "dummy", "mock", "placeholder"]
|
|
215
|
+
):
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
# Filter out empty strings
|
|
219
|
+
if '""' in line or "''" in line:
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
findings.append(
|
|
223
|
+
ReviewFinding(
|
|
224
|
+
category="security",
|
|
225
|
+
severity="high",
|
|
226
|
+
file_path=str(file_path),
|
|
227
|
+
line_number=line_no,
|
|
228
|
+
message="[A07] Hardcoded credentials detected",
|
|
229
|
+
suggestion="Use environment variables or a secrets manager to store sensitive data",
|
|
230
|
+
tool="owasp",
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return findings
|
|
235
|
+
|
|
236
|
+
def _check_weak_password_validation(self, file_path: Path, code: str) -> List[ReviewFinding]:
|
|
237
|
+
"""Check for weak password validation.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
file_path: Path to file being checked
|
|
241
|
+
code: File content
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
List of findings for weak password validation
|
|
245
|
+
"""
|
|
246
|
+
findings = []
|
|
247
|
+
lines = code.split("\n")
|
|
248
|
+
|
|
249
|
+
for line_no, line in enumerate(lines, start=1):
|
|
250
|
+
# Check for weak password length validation
|
|
251
|
+
if self.weak_password_pattern.search(line):
|
|
252
|
+
findings.append(
|
|
253
|
+
ReviewFinding(
|
|
254
|
+
category="security",
|
|
255
|
+
severity="medium",
|
|
256
|
+
file_path=str(file_path),
|
|
257
|
+
line_number=line_no,
|
|
258
|
+
message="[A07] Weak password validation detected",
|
|
259
|
+
suggestion="Require passwords to be at least 8 characters with complexity requirements",
|
|
260
|
+
tool="owasp",
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
return findings
|
|
265
|
+
|
|
266
|
+
def check_files(self, file_paths: List[Path]) -> List[ReviewFinding]:
|
|
267
|
+
"""Check multiple files for OWASP patterns.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
file_paths: List of file paths to check
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
List of all findings from all files
|
|
274
|
+
"""
|
|
275
|
+
all_findings = []
|
|
276
|
+
|
|
277
|
+
for file_path in file_paths:
|
|
278
|
+
try:
|
|
279
|
+
findings = self.check_file(file_path)
|
|
280
|
+
all_findings.extend(findings)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
logger.error(f"Error checking {file_path}: {e}")
|
|
283
|
+
|
|
284
|
+
return all_findings
|