gwc-pybundle 2.1.2__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 gwc-pybundle might be problematic. Click here for more details.
- gwc_pybundle-2.1.2.dist-info/METADATA +903 -0
- gwc_pybundle-2.1.2.dist-info/RECORD +82 -0
- gwc_pybundle-2.1.2.dist-info/WHEEL +5 -0
- gwc_pybundle-2.1.2.dist-info/entry_points.txt +2 -0
- gwc_pybundle-2.1.2.dist-info/licenses/LICENSE.md +25 -0
- gwc_pybundle-2.1.2.dist-info/top_level.txt +1 -0
- pybundle/__init__.py +0 -0
- pybundle/__main__.py +4 -0
- pybundle/cli.py +546 -0
- pybundle/context.py +404 -0
- pybundle/doctor.py +148 -0
- pybundle/filters.py +228 -0
- pybundle/manifest.py +77 -0
- pybundle/packaging.py +45 -0
- pybundle/policy.py +132 -0
- pybundle/profiles.py +454 -0
- pybundle/roadmap_model.py +42 -0
- pybundle/roadmap_scan.py +328 -0
- pybundle/root_detect.py +14 -0
- pybundle/runner.py +180 -0
- pybundle/steps/__init__.py +26 -0
- pybundle/steps/ai_context.py +791 -0
- pybundle/steps/api_docs.py +219 -0
- pybundle/steps/asyncio_analysis.py +358 -0
- pybundle/steps/bandit.py +72 -0
- pybundle/steps/base.py +20 -0
- pybundle/steps/blocking_call_detection.py +291 -0
- pybundle/steps/call_graph.py +219 -0
- pybundle/steps/compileall.py +76 -0
- pybundle/steps/config_docs.py +319 -0
- pybundle/steps/config_validation.py +302 -0
- pybundle/steps/container_image.py +294 -0
- pybundle/steps/context_expand.py +272 -0
- pybundle/steps/copy_pack.py +293 -0
- pybundle/steps/coverage.py +101 -0
- pybundle/steps/cprofile_step.py +166 -0
- pybundle/steps/dependency_sizes.py +136 -0
- pybundle/steps/django_checks.py +214 -0
- pybundle/steps/dockerfile_lint.py +282 -0
- pybundle/steps/dockerignore.py +311 -0
- pybundle/steps/duplication.py +103 -0
- pybundle/steps/env_completeness.py +269 -0
- pybundle/steps/env_var_usage.py +253 -0
- pybundle/steps/error_refs.py +204 -0
- pybundle/steps/event_loop_patterns.py +280 -0
- pybundle/steps/exception_patterns.py +190 -0
- pybundle/steps/fastapi_integration.py +250 -0
- pybundle/steps/flask_debugging.py +312 -0
- pybundle/steps/git_analytics.py +315 -0
- pybundle/steps/handoff_md.py +176 -0
- pybundle/steps/import_time.py +175 -0
- pybundle/steps/interrogate.py +106 -0
- pybundle/steps/license_scan.py +96 -0
- pybundle/steps/line_profiler.py +117 -0
- pybundle/steps/link_validation.py +287 -0
- pybundle/steps/logging_analysis.py +233 -0
- pybundle/steps/memory_profile.py +176 -0
- pybundle/steps/migration_history.py +336 -0
- pybundle/steps/mutation_testing.py +141 -0
- pybundle/steps/mypy.py +103 -0
- pybundle/steps/orm_optimization.py +316 -0
- pybundle/steps/pip_audit.py +45 -0
- pybundle/steps/pipdeptree.py +62 -0
- pybundle/steps/pylance.py +562 -0
- pybundle/steps/pytest.py +66 -0
- pybundle/steps/query_pattern_analysis.py +334 -0
- pybundle/steps/radon.py +161 -0
- pybundle/steps/repro_md.py +161 -0
- pybundle/steps/rg_scans.py +78 -0
- pybundle/steps/roadmap.py +153 -0
- pybundle/steps/ruff.py +117 -0
- pybundle/steps/secrets_detection.py +235 -0
- pybundle/steps/security_headers.py +309 -0
- pybundle/steps/shell.py +74 -0
- pybundle/steps/slow_tests.py +178 -0
- pybundle/steps/sqlalchemy_validation.py +269 -0
- pybundle/steps/test_flakiness.py +184 -0
- pybundle/steps/tree.py +116 -0
- pybundle/steps/type_coverage.py +277 -0
- pybundle/steps/unused_deps.py +211 -0
- pybundle/steps/vulture.py +167 -0
- pybundle/tools.py +63 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Step: FastAPI Integration
|
|
3
|
+
Validate FastAPI OpenAPI schema and endpoint documentation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Any, Optional
|
|
9
|
+
|
|
10
|
+
from .base import Step, StepResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FastAPIIntegrationStep(Step):
|
|
14
|
+
"""Validate FastAPI schema and endpoint completeness."""
|
|
15
|
+
|
|
16
|
+
name = "fastapi schema"
|
|
17
|
+
|
|
18
|
+
def run(self, ctx: "BundleContext") -> StepResult: # type: ignore[name-defined]
|
|
19
|
+
"""Validate FastAPI integration."""
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
start = time.time()
|
|
23
|
+
|
|
24
|
+
root = ctx.root
|
|
25
|
+
|
|
26
|
+
# Find FastAPI app
|
|
27
|
+
app_info = self._find_fastapi_app(root)
|
|
28
|
+
if not app_info:
|
|
29
|
+
elapsed = int(time.time() - start)
|
|
30
|
+
return StepResult(
|
|
31
|
+
self.name, "SKIP", elapsed, "No FastAPI application found"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Analyze endpoints
|
|
35
|
+
endpoints = self._analyze_endpoints(root, app_info)
|
|
36
|
+
|
|
37
|
+
# Generate report
|
|
38
|
+
lines = [
|
|
39
|
+
"=" * 80,
|
|
40
|
+
"FASTAPI INTEGRATION REPORT",
|
|
41
|
+
"=" * 80,
|
|
42
|
+
"",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
lines.extend(
|
|
46
|
+
[
|
|
47
|
+
"SUMMARY",
|
|
48
|
+
"=" * 80,
|
|
49
|
+
f"FastAPI app location: {app_info['file']}:{app_info['name']}",
|
|
50
|
+
f"Total endpoints: {len(endpoints)}",
|
|
51
|
+
"",
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Endpoint analysis
|
|
56
|
+
lines.extend(
|
|
57
|
+
[
|
|
58
|
+
"ENDPOINTS",
|
|
59
|
+
"=" * 80,
|
|
60
|
+
"",
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
documented = 0
|
|
65
|
+
undocumented = 0
|
|
66
|
+
|
|
67
|
+
for endpoint in sorted(endpoints, key=lambda e: e["path"]):
|
|
68
|
+
doc_status = "✓" if endpoint["has_docstring"] else "⚠"
|
|
69
|
+
documented += 1 if endpoint["has_docstring"] else 0
|
|
70
|
+
undocumented += 0 if endpoint["has_docstring"] else 1
|
|
71
|
+
|
|
72
|
+
lines.append(f"{doc_status} {endpoint['method']:6} {endpoint['path']}")
|
|
73
|
+
if endpoint["has_docstring"]:
|
|
74
|
+
lines.append(f" Summary: {endpoint['docstring_first_line']}")
|
|
75
|
+
else:
|
|
76
|
+
lines.append(" ⚠ Missing docstring (won't appear in OpenAPI)")
|
|
77
|
+
|
|
78
|
+
lines.append("")
|
|
79
|
+
|
|
80
|
+
# Statistics
|
|
81
|
+
lines.extend(
|
|
82
|
+
[
|
|
83
|
+
"DOCUMENTATION STATISTICS",
|
|
84
|
+
"-" * 80,
|
|
85
|
+
f"Documented endpoints: {documented}/{len(endpoints)} ({100*documented//len(endpoints)}%)" if endpoints else "No endpoints found",
|
|
86
|
+
f"Undocumented endpoints: {undocumented}/{len(endpoints)}" if endpoints else "",
|
|
87
|
+
"",
|
|
88
|
+
]
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Response models
|
|
92
|
+
lines.extend(
|
|
93
|
+
[
|
|
94
|
+
"RESPONSE MODELS",
|
|
95
|
+
"=" * 80,
|
|
96
|
+
"",
|
|
97
|
+
]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
models_count = 0
|
|
101
|
+
for endpoint in endpoints:
|
|
102
|
+
if endpoint.get("response_model"):
|
|
103
|
+
models_count += 1
|
|
104
|
+
lines.append(f" {endpoint['method']:6} {endpoint['path']}")
|
|
105
|
+
lines.append(f" Response: {endpoint['response_model']}")
|
|
106
|
+
|
|
107
|
+
if models_count == 0:
|
|
108
|
+
lines.append(" ℹ No response models found")
|
|
109
|
+
|
|
110
|
+
lines.append("")
|
|
111
|
+
|
|
112
|
+
# Recommendations
|
|
113
|
+
lines.extend(
|
|
114
|
+
[
|
|
115
|
+
"=" * 80,
|
|
116
|
+
"BEST PRACTICES & RECOMMENDATIONS",
|
|
117
|
+
"=" * 80,
|
|
118
|
+
"",
|
|
119
|
+
"1. DOCUMENTATION",
|
|
120
|
+
" ✓ Add docstrings to all endpoint handlers",
|
|
121
|
+
" ✓ Use triple-quoted docstrings for OpenAPI summary",
|
|
122
|
+
" ✓ Include examples in docstrings for clarity",
|
|
123
|
+
"",
|
|
124
|
+
"2. RESPONSE MODELS",
|
|
125
|
+
" ✓ Define Pydantic models for all responses",
|
|
126
|
+
" ✓ Use response_model parameter in @app.get/post/etc",
|
|
127
|
+
" ✓ Include status_code for non-200 responses",
|
|
128
|
+
"",
|
|
129
|
+
"3. REQUEST VALIDATION",
|
|
130
|
+
" ✓ Use Pydantic models for request bodies",
|
|
131
|
+
" ✓ Use Path, Query, Header for parameter validation",
|
|
132
|
+
" ✓ Provide examples in Field descriptions",
|
|
133
|
+
"",
|
|
134
|
+
"4. ERROR HANDLING",
|
|
135
|
+
" ✓ Define responses for 400, 404, 500, etc",
|
|
136
|
+
" ✓ Use HTTPException with detail messages",
|
|
137
|
+
" ✓ Document possible error responses in docstrings",
|
|
138
|
+
"",
|
|
139
|
+
"5. SECURITY",
|
|
140
|
+
" ✓ Use security=[] parameter for protected endpoints",
|
|
141
|
+
" ✓ Document security requirements in OpenAPI",
|
|
142
|
+
" ✓ Validate bearer tokens and API keys",
|
|
143
|
+
"",
|
|
144
|
+
"6. TESTING",
|
|
145
|
+
" ✓ Use TestClient for endpoint testing",
|
|
146
|
+
" ✓ Test all response status codes",
|
|
147
|
+
" ✓ Test request validation (happy path + errors)",
|
|
148
|
+
"",
|
|
149
|
+
]
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Write report
|
|
153
|
+
output = "\n".join(lines)
|
|
154
|
+
dest = ctx.workdir / "logs" / "150_fastapi_schema.txt"
|
|
155
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
dest.write_text(output, encoding="utf-8")
|
|
157
|
+
|
|
158
|
+
elapsed = int(time.time() - start)
|
|
159
|
+
return StepResult(self.name, "OK", elapsed, "")
|
|
160
|
+
|
|
161
|
+
def _find_fastapi_app(self, root: Path) -> Optional[Dict[str, Any]]:
|
|
162
|
+
"""Find FastAPI app instance."""
|
|
163
|
+
python_files = list(root.rglob("*.py"))
|
|
164
|
+
|
|
165
|
+
for py_file in python_files:
|
|
166
|
+
if any(
|
|
167
|
+
part in py_file.parts
|
|
168
|
+
for part in ["venv", ".venv", "env", "__pycache__", "site-packages"]
|
|
169
|
+
):
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
source = py_file.read_text(encoding="utf-8", errors="ignore")
|
|
174
|
+
|
|
175
|
+
# Look for FastAPI imports and app creation
|
|
176
|
+
if "from fastapi import" in source or "import fastapi" in source:
|
|
177
|
+
if "app = FastAPI" in source or "app: FastAPI" in source:
|
|
178
|
+
# Extract app variable name
|
|
179
|
+
import re
|
|
180
|
+
|
|
181
|
+
match = re.search(r"(\w+)\s*=\s*FastAPI\(", source)
|
|
182
|
+
if match:
|
|
183
|
+
return {
|
|
184
|
+
"file": str(py_file.relative_to(root)),
|
|
185
|
+
"name": match.group(1),
|
|
186
|
+
}
|
|
187
|
+
except (OSError, UnicodeDecodeError):
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def _analyze_endpoints(self, root: Path, app_info: Dict) -> List[Dict[str, Any]]:
|
|
193
|
+
"""Analyze FastAPI endpoints."""
|
|
194
|
+
endpoints = []
|
|
195
|
+
|
|
196
|
+
# Look for route decorators in the app file
|
|
197
|
+
app_file = root / app_info["file"]
|
|
198
|
+
if not app_file.exists():
|
|
199
|
+
return endpoints
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
source = app_file.read_text(encoding="utf-8", errors="ignore")
|
|
203
|
+
|
|
204
|
+
# Look for decorators: @app.get, @app.post, etc.
|
|
205
|
+
import re
|
|
206
|
+
|
|
207
|
+
# Pattern: @app.method("path")
|
|
208
|
+
pattern = r"@(?:app|router)\.(get|post|put|delete|patch|head|options)\(['\"]([^'\"]+)['\"]"
|
|
209
|
+
for match in re.finditer(pattern, source):
|
|
210
|
+
method, path = match.groups()
|
|
211
|
+
endpoints.append(
|
|
212
|
+
{
|
|
213
|
+
"method": method.upper(),
|
|
214
|
+
"path": path,
|
|
215
|
+
"has_docstring": False,
|
|
216
|
+
"docstring_first_line": "",
|
|
217
|
+
"response_model": None,
|
|
218
|
+
}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Check for docstrings in functions following decorators
|
|
222
|
+
lines = source.split("\n")
|
|
223
|
+
for i, line in enumerate(lines):
|
|
224
|
+
if re.search(r"@(?:app|router)\.(get|post|put|delete|patch|head|options)", line):
|
|
225
|
+
# Look for function definition
|
|
226
|
+
for j in range(i + 1, min(i + 5, len(lines))):
|
|
227
|
+
if "def " in lines[j]:
|
|
228
|
+
# Check for docstring
|
|
229
|
+
if j + 1 < len(lines):
|
|
230
|
+
next_line = lines[j + 1].strip()
|
|
231
|
+
if next_line.startswith('"""') or next_line.startswith("'''"):
|
|
232
|
+
docstring = next_line.replace('"""', "").replace("'''", "").strip()
|
|
233
|
+
if endpoints:
|
|
234
|
+
endpoints[-1]["has_docstring"] = True
|
|
235
|
+
endpoints[-1]["docstring_first_line"] = docstring
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
# Check for response_model
|
|
239
|
+
for i, line in enumerate(lines):
|
|
240
|
+
if "response_model=" in line and endpoints:
|
|
241
|
+
import re
|
|
242
|
+
|
|
243
|
+
match = re.search(r"response_model=(\w+)", line)
|
|
244
|
+
if match:
|
|
245
|
+
endpoints[-1]["response_model"] = match.group(1)
|
|
246
|
+
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
return endpoints
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Step: Flask Debugging
|
|
3
|
+
Detect Flask debug mode and other security issues in Flask apps.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Tuple, Optional
|
|
10
|
+
|
|
11
|
+
from .base import Step, StepResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FlaskDebuggingStep(Step):
|
|
15
|
+
"""Detect Flask debug mode and security issues."""
|
|
16
|
+
|
|
17
|
+
name = "flask debugging"
|
|
18
|
+
|
|
19
|
+
def run(self, ctx: "BundleContext") -> StepResult: # type: ignore[name-defined]
|
|
20
|
+
"""Check Flask debugging configuration."""
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
start = time.time()
|
|
24
|
+
|
|
25
|
+
root = ctx.root
|
|
26
|
+
|
|
27
|
+
# Find Flask app
|
|
28
|
+
flask_info = self._find_flask_app(root)
|
|
29
|
+
if not flask_info:
|
|
30
|
+
elapsed = int(time.time() - start)
|
|
31
|
+
return StepResult(
|
|
32
|
+
self.name, "SKIP", elapsed, "No Flask application found"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Analyze Flask configuration
|
|
36
|
+
config_issues = self._analyze_flask_config(root, flask_info)
|
|
37
|
+
|
|
38
|
+
# Generate report
|
|
39
|
+
lines = [
|
|
40
|
+
"=" * 80,
|
|
41
|
+
"FLASK SECURITY & DEBUGGING REPORT",
|
|
42
|
+
"=" * 80,
|
|
43
|
+
"",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
lines.extend(
|
|
47
|
+
[
|
|
48
|
+
"SUMMARY",
|
|
49
|
+
"=" * 80,
|
|
50
|
+
f"Flask app location: {flask_info['file']}:{flask_info['name']}",
|
|
51
|
+
"",
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Configuration issues
|
|
56
|
+
lines.extend(
|
|
57
|
+
[
|
|
58
|
+
"SECURITY ISSUES",
|
|
59
|
+
"=" * 80,
|
|
60
|
+
"",
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if config_issues:
|
|
65
|
+
for level, issue, detail in config_issues:
|
|
66
|
+
icon = "✗" if level == "ERROR" else "⚠"
|
|
67
|
+
lines.append(f"{icon} [{level}] {issue}")
|
|
68
|
+
if detail:
|
|
69
|
+
lines.append(f" {detail}")
|
|
70
|
+
lines.append("")
|
|
71
|
+
else:
|
|
72
|
+
lines.append("✓ No critical security issues detected")
|
|
73
|
+
lines.append("")
|
|
74
|
+
|
|
75
|
+
# Debug mode analysis
|
|
76
|
+
lines.extend(
|
|
77
|
+
[
|
|
78
|
+
"DEBUG MODE ANALYSIS",
|
|
79
|
+
"-" * 80,
|
|
80
|
+
"",
|
|
81
|
+
]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
debug_findings = self._check_debug_mode(root, flask_info)
|
|
85
|
+
for finding in debug_findings:
|
|
86
|
+
lines.append(f" • {finding}")
|
|
87
|
+
|
|
88
|
+
lines.append("")
|
|
89
|
+
|
|
90
|
+
# Routes analysis
|
|
91
|
+
lines.extend(
|
|
92
|
+
[
|
|
93
|
+
"ROUTES ANALYSIS",
|
|
94
|
+
"=" * 80,
|
|
95
|
+
"",
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
routes = self._extract_routes(root, flask_info)
|
|
100
|
+
if routes:
|
|
101
|
+
lines.append(f"Found {len(routes)} route(s):")
|
|
102
|
+
lines.append("")
|
|
103
|
+
for method, path, handler in routes:
|
|
104
|
+
lines.append(f" {method:6} {path:40} -> {handler}")
|
|
105
|
+
else:
|
|
106
|
+
lines.append(" ℹ No routes found via static analysis")
|
|
107
|
+
|
|
108
|
+
lines.append("")
|
|
109
|
+
|
|
110
|
+
# Recommendations
|
|
111
|
+
lines.extend(
|
|
112
|
+
[
|
|
113
|
+
"=" * 80,
|
|
114
|
+
"FLASK DEPLOYMENT BEST PRACTICES",
|
|
115
|
+
"=" * 80,
|
|
116
|
+
"",
|
|
117
|
+
"1. DEBUG MODE",
|
|
118
|
+
" ✗ NEVER use DEBUG=True in production",
|
|
119
|
+
" ✓ Use environment variables to control debug mode",
|
|
120
|
+
" ✓ Set DEBUG=False in production configuration",
|
|
121
|
+
" ✓ Use app.config.from_object('config.Production')",
|
|
122
|
+
"",
|
|
123
|
+
"2. SECRET KEY",
|
|
124
|
+
" ✓ Use strong, random SECRET_KEY (os.urandom(24))",
|
|
125
|
+
" ✓ Store SECRET_KEY in environment variables",
|
|
126
|
+
" ✓ Never hardcode secrets in source code",
|
|
127
|
+
" ✓ Rotate SECRET_KEY regularly in production",
|
|
128
|
+
"",
|
|
129
|
+
"3. SESSION SECURITY",
|
|
130
|
+
" ✓ Set SESSION_COOKIE_SECURE=True",
|
|
131
|
+
" ✓ Set SESSION_COOKIE_HTTPONLY=True",
|
|
132
|
+
" ✓ Set SESSION_COOKIE_SAMESITE='Lax' or 'Strict'",
|
|
133
|
+
" ✓ Use secure session backends (Redis, database)",
|
|
134
|
+
"",
|
|
135
|
+
"4. CORS & HEADERS",
|
|
136
|
+
" ✓ Use Flask-CORS with restricted origins",
|
|
137
|
+
" ✓ Set X-Frame-Options: DENY",
|
|
138
|
+
" ✓ Set X-Content-Type-Options: nosniff",
|
|
139
|
+
" ✓ Set X-XSS-Protection: 1; mode=block",
|
|
140
|
+
" ✓ Set Strict-Transport-Security (HSTS)",
|
|
141
|
+
"",
|
|
142
|
+
"5. ERROR HANDLING",
|
|
143
|
+
" ✓ Implement custom 404, 500 error handlers",
|
|
144
|
+
" ✓ Log errors securely (no sensitive data)",
|
|
145
|
+
" ✓ Hide stack traces in production",
|
|
146
|
+
" ✓ Return generic error messages to users",
|
|
147
|
+
"",
|
|
148
|
+
"6. DEPENDENCIES",
|
|
149
|
+
" ✓ Use pip-audit to check for vulnerabilities",
|
|
150
|
+
" ✓ Keep Flask and extensions updated",
|
|
151
|
+
" ✓ Review security advisories regularly",
|
|
152
|
+
"",
|
|
153
|
+
"7. DEPLOYMENT",
|
|
154
|
+
" ✓ Use WSGI server (Gunicorn, uWSGI, Waitress)",
|
|
155
|
+
" ✓ Run behind reverse proxy (Nginx, Apache)",
|
|
156
|
+
" ✓ Use HTTPS with valid certificate",
|
|
157
|
+
" ✓ Enable security headers in reverse proxy",
|
|
158
|
+
"",
|
|
159
|
+
]
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Write report
|
|
163
|
+
output = "\n".join(lines)
|
|
164
|
+
dest = ctx.workdir / "logs" / "151_flask_checks.txt"
|
|
165
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
dest.write_text(output, encoding="utf-8")
|
|
167
|
+
|
|
168
|
+
elapsed = int(time.time() - start)
|
|
169
|
+
return StepResult(self.name, "OK", elapsed, "")
|
|
170
|
+
|
|
171
|
+
def _find_flask_app(self, root: Path) -> Optional[Dict[str, str]]:
|
|
172
|
+
"""Find Flask app instance."""
|
|
173
|
+
python_files = list(root.rglob("*.py"))
|
|
174
|
+
|
|
175
|
+
for py_file in python_files:
|
|
176
|
+
if any(
|
|
177
|
+
part in py_file.parts
|
|
178
|
+
for part in ["venv", ".venv", "env", "__pycache__", "site-packages"]
|
|
179
|
+
):
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
source = py_file.read_text(encoding="utf-8", errors="ignore")
|
|
184
|
+
|
|
185
|
+
# Look for Flask imports and app creation
|
|
186
|
+
if "from flask import" in source or "import flask" in source:
|
|
187
|
+
if "Flask(" in source:
|
|
188
|
+
# Extract app variable name
|
|
189
|
+
match = re.search(r"(\w+)\s*=\s*Flask\(", source)
|
|
190
|
+
if match:
|
|
191
|
+
return {
|
|
192
|
+
"file": str(py_file.relative_to(root)),
|
|
193
|
+
"name": match.group(1),
|
|
194
|
+
}
|
|
195
|
+
except (OSError, UnicodeDecodeError):
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
def _analyze_flask_config(
|
|
201
|
+
self, root: Path, flask_info: Dict[str, str]
|
|
202
|
+
) -> List[Tuple[str, str, str]]:
|
|
203
|
+
"""Analyze Flask configuration for security issues."""
|
|
204
|
+
issues = []
|
|
205
|
+
|
|
206
|
+
# Check for hardcoded secrets
|
|
207
|
+
python_files = list(root.rglob("*.py"))
|
|
208
|
+
for py_file in python_files:
|
|
209
|
+
if any(
|
|
210
|
+
part in py_file.parts
|
|
211
|
+
for part in ["venv", ".venv", "env", "__pycache__", "site-packages"]
|
|
212
|
+
):
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
source = py_file.read_text(encoding="utf-8", errors="ignore")
|
|
217
|
+
|
|
218
|
+
# Check for hardcoded SECRET_KEY
|
|
219
|
+
if re.search(
|
|
220
|
+
r"['\"]SECRET_KEY['\"]?\s*[:=]\s*['\"](?!<|{{)", source
|
|
221
|
+
):
|
|
222
|
+
issues.append(
|
|
223
|
+
(
|
|
224
|
+
"ERROR",
|
|
225
|
+
"Hardcoded SECRET_KEY detected",
|
|
226
|
+
f"File: {py_file.relative_to(root)}",
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Check for DEBUG=True in code
|
|
231
|
+
if re.search(r"DEBUG\s*=\s*True", source):
|
|
232
|
+
issues.append(
|
|
233
|
+
(
|
|
234
|
+
"ERROR",
|
|
235
|
+
"DEBUG=True found in code",
|
|
236
|
+
f"File: {py_file.relative_to(root)} - Use environment variables instead",
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
except (OSError, UnicodeDecodeError):
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
return issues
|
|
244
|
+
|
|
245
|
+
def _check_debug_mode(self, root: Path, flask_info: Dict[str, str]) -> List[str]:
|
|
246
|
+
"""Check debug mode configuration."""
|
|
247
|
+
findings = []
|
|
248
|
+
|
|
249
|
+
app_file = root / flask_info["file"]
|
|
250
|
+
if app_file.exists():
|
|
251
|
+
try:
|
|
252
|
+
source = app_file.read_text(encoding="utf-8", errors="ignore")
|
|
253
|
+
|
|
254
|
+
if "app.run(debug=True)" in source:
|
|
255
|
+
findings.append(
|
|
256
|
+
"⚠ app.run(debug=True) found - ensure this is only in development"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if "FLASK_ENV" in source:
|
|
260
|
+
findings.append(
|
|
261
|
+
"✓ FLASK_ENV environment variable is used"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if "os.getenv('DEBUG')" in source or "os.environ.get('DEBUG')" in source:
|
|
265
|
+
findings.append(
|
|
266
|
+
"✓ DEBUG mode controlled via environment variable"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if not any(
|
|
270
|
+
pattern in source
|
|
271
|
+
for pattern in ["debug=", "FLASK_ENV", "DEBUG", "app.run()"]
|
|
272
|
+
):
|
|
273
|
+
findings.append(
|
|
274
|
+
"ℹ No explicit debug configuration found - verify Flask configuration chain"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
except (OSError, UnicodeDecodeError):
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
return findings
|
|
281
|
+
|
|
282
|
+
def _extract_routes(self, root: Path, flask_info: Dict[str, str]) -> List[Tuple[str, str, str]]:
|
|
283
|
+
"""Extract Flask routes."""
|
|
284
|
+
routes = []
|
|
285
|
+
|
|
286
|
+
app_file = root / flask_info["file"]
|
|
287
|
+
if not app_file.exists():
|
|
288
|
+
return routes
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
source = app_file.read_text(encoding="utf-8", errors="ignore")
|
|
292
|
+
lines = source.split("\n")
|
|
293
|
+
|
|
294
|
+
# Pattern: @app.route or @app.get, @app.post, etc.
|
|
295
|
+
for i, line in enumerate(lines):
|
|
296
|
+
route_match = re.search(r"@(?:app|blueprint)\.(route|get|post|put|delete)\(['\"]([^'\"]+)", line)
|
|
297
|
+
if route_match:
|
|
298
|
+
method = route_match.group(1).upper()
|
|
299
|
+
path = route_match.group(2)
|
|
300
|
+
|
|
301
|
+
# Find handler function
|
|
302
|
+
for j in range(i + 1, min(i + 3, len(lines))):
|
|
303
|
+
func_match = re.search(r"def\s+(\w+)\s*\(", lines[j])
|
|
304
|
+
if func_match:
|
|
305
|
+
handler = func_match.group(1)
|
|
306
|
+
routes.append((method, path, handler))
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
except Exception:
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
return routes
|