python-qv 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- {python_qv-0.1.2 → python_qv-0.1.3}/PKG-INFO +9 -2
- {python_qv-0.1.2 → python_qv-0.1.3}/README.md +8 -1
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/cli_reference.md +25 -0
- python_qv-0.1.3/docs/rules.md +444 -0
- python_qv-0.1.3/examples/fastapi_issues/main.py +61 -0
- python_qv-0.1.3/examples/fastapi_issues/pyproject.toml +9 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/pyproject.toml +1 -1
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/__init__.py +1 -1
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/cli/main.py +84 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/core/engine.py +4 -0
- python_qv-0.1.3/src/qv/frameworks/__init__.py +19 -0
- python_qv-0.1.3/src/qv/frameworks/base.py +25 -0
- python_qv-0.1.3/src/qv/frameworks/fastapi.py +2219 -0
- python_qv-0.1.3/src/qv/frameworks/sqlalchemy.py +1580 -0
- python_qv-0.1.3/src/qv/rules/registry.py +783 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/test_cli.py +50 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/test_examples.py +17 -0
- python_qv-0.1.3/tests/test_fastapi.py +960 -0
- python_qv-0.1.3/tests/test_sqlalchemy.py +636 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/uv.lock +739 -737
- python_qv-0.1.2/docs/rules.md +0 -94
- python_qv-0.1.2/src/qv/frameworks/__init__.py +0 -1
- python_qv-0.1.2/src/qv/rules/registry.py +0 -169
- {python_qv-0.1.2 → python_qv-0.1.3}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/.github/ISSUE_TEMPLATE/rule_proposal.yml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/.github/pull_request_template.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/.github/workflows/ci.yml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/.github/workflows/docs.yml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/.github/workflows/publish.yml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/.gitignore +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/.pre-commit-config.yaml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/.pre-commit-hooks.yaml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/.readthedocs.yaml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/CHANGELOG.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/CONTRIBUTING.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/SECURITY.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/action.yml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/architecture.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/assets/favicon.svg +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/assets/logo.svg +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/ci_integration.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/configuration.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/contributing.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/getting_started.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/html_report.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/index.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/pre_commit.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/remediation.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/requirements.txt +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/tree.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/docs/tui.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/README.md +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/all_in_one_unhealthy/Dockerfile +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/all_in_one_unhealthy/cycle_a.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/all_in_one_unhealthy/cycle_b.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/all_in_one_unhealthy/main.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/all_in_one_unhealthy/pyproject.toml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/circular_imports/module_a.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/circular_imports/module_b.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/circular_imports/pyproject.toml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/dead_modules/active_feature.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/dead_modules/main.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/dead_modules/pyproject.toml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/dead_modules/unused_legacy_module.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/deprecated_stdlib/legacy_app.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/deprecated_stdlib/pyproject.toml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/environment_drift/.github/workflows/ci.yml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/environment_drift/Dockerfile +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/environment_drift/pyproject.toml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/missing_dependencies/main.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/missing_dependencies/pyproject.toml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/missing_metadata/pyproject.toml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/unresolved_imports/pyproject.toml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/unresolved_imports/service.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/unused_dependencies/app.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/unused_dependencies/pyproject.toml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/vulnerable_dependencies/app.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/vulnerable_dependencies/pyproject.toml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/examples/vulnerable_dependencies/requirements.txt +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/mkdocs.yml +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/__main__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/ci/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/dependencies/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/dependencies/analyzer.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/docker/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/environment/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/environment/drift.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/imports/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/imports/analyzer.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/packaging/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/packaging/analyzer.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/security/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/security/analyzer.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/security/lockfile_parser.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/analyzers/security/osv_client.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/cli/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/core/analyzer.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/core/config.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/core/context.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/core/models.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/core/project.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/integrations/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/remediation/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/remediation/engine.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/remediation/models.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/reporters/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/reporters/base.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/reporters/github_annotator.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/reporters/html_reporter.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/reporters/json_reporter.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/reporters/sarif.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/reporters/terminal.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/tui/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/tui/app.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/visualizers/__init__.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/src/qv/visualizers/tree.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/analyzers/test_dependencies.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/analyzers/test_environment.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/analyzers/test_imports.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/analyzers/test_packaging.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/analyzers/test_security.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/conftest.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/reporters/test_github_annotator.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/reporters/test_html.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/reporters/test_reporters.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/test_config.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/test_lockfile_parser.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/test_models.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/test_pre_commit_hooks.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/test_remediation.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/test_rules.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/test_tree.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tests/test_tui.py +0 -0
- {python_qv-0.1.2 → python_qv-0.1.3}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-qv
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Diagnose why a Python project is unhealthy — explain root cause and suggest safe fixes.
|
|
5
5
|
Project-URL: Homepage, https://github.com/inzamol/qv
|
|
6
6
|
Project-URL: Repository, https://github.com/inzamol/qv
|
|
@@ -199,8 +199,10 @@ Run `qv inspect` (or `qv ui`) for a full terminal dashboard:
|
|
|
199
199
|
| | `IMP-002` | Unresolved relative or internal module imports | `ERROR` |
|
|
200
200
|
| **Packaging** | `PKG-001` | Missing PEP 621 metadata (name, version, etc.) | `WARNING` |
|
|
201
201
|
| | `PKG-002` | Invalid syntax or malformed keys in `pyproject.toml` | `ERROR` |
|
|
202
|
+
| **FastAPI Doctor** | `FAP-001`–`FAP-038` | Async blocking calls, CPU starvation in endpoints, insecure CORS, missing timeouts, lifecycle anti-patterns, Pydantic v2 migrations | `ERROR` / `WARNING` |
|
|
203
|
+
| **SQL & Database** | `SQL-001`–`SQL-030` | N+1 queries in loops, session leaks, SQL injection, sync DB in async loop, missing eager loading, pool starvation, 2.0 syntax | `ERROR` / `WARNING` |
|
|
202
204
|
|
|
203
|
-
👉 *See
|
|
205
|
+
👉 *See all 80+ rules and remediation steps in the [Rules Catalog](docs/rules.md).*
|
|
204
206
|
|
|
205
207
|
---
|
|
206
208
|
|
|
@@ -331,6 +333,11 @@ qv environment
|
|
|
331
333
|
|
|
332
334
|
# Check AST & imports only (circular import loops, unresolvable modules)
|
|
333
335
|
qv architecture
|
|
336
|
+
|
|
337
|
+
# Check framework-specific issues (FastAPI, SQLAlchemy, SQLModel)
|
|
338
|
+
qv framework
|
|
339
|
+
qv framework --name fastapi
|
|
340
|
+
qv framework --name sqlalchemy
|
|
334
341
|
```
|
|
335
342
|
|
|
336
343
|
---
|
|
@@ -159,8 +159,10 @@ Run `qv inspect` (or `qv ui`) for a full terminal dashboard:
|
|
|
159
159
|
| | `IMP-002` | Unresolved relative or internal module imports | `ERROR` |
|
|
160
160
|
| **Packaging** | `PKG-001` | Missing PEP 621 metadata (name, version, etc.) | `WARNING` |
|
|
161
161
|
| | `PKG-002` | Invalid syntax or malformed keys in `pyproject.toml` | `ERROR` |
|
|
162
|
+
| **FastAPI Doctor** | `FAP-001`–`FAP-038` | Async blocking calls, CPU starvation in endpoints, insecure CORS, missing timeouts, lifecycle anti-patterns, Pydantic v2 migrations | `ERROR` / `WARNING` |
|
|
163
|
+
| **SQL & Database** | `SQL-001`–`SQL-030` | N+1 queries in loops, session leaks, SQL injection, sync DB in async loop, missing eager loading, pool starvation, 2.0 syntax | `ERROR` / `WARNING` |
|
|
162
164
|
|
|
163
|
-
👉 *See
|
|
165
|
+
👉 *See all 80+ rules and remediation steps in the [Rules Catalog](docs/rules.md).*
|
|
164
166
|
|
|
165
167
|
---
|
|
166
168
|
|
|
@@ -291,6 +293,11 @@ qv environment
|
|
|
291
293
|
|
|
292
294
|
# Check AST & imports only (circular import loops, unresolvable modules)
|
|
293
295
|
qv architecture
|
|
296
|
+
|
|
297
|
+
# Check framework-specific issues (FastAPI, SQLAlchemy, SQLModel)
|
|
298
|
+
qv framework
|
|
299
|
+
qv framework --name fastapi
|
|
300
|
+
qv framework --name sqlalchemy
|
|
294
301
|
```
|
|
295
302
|
|
|
296
303
|
---
|
|
@@ -194,3 +194,28 @@ Scans source code for circular imports and unresolved internal modules.
|
|
|
194
194
|
```bash
|
|
195
195
|
qv architecture [PATH]
|
|
196
196
|
```
|
|
197
|
+
|
|
198
|
+
### 8.4 `qv framework` (alias: `qv frameworks`)
|
|
199
|
+
Runs framework-specific diagnostic rules (e.g. FastAPI async blocking calls, SQLAlchemy N+1 queries, unclosed sessions, SQL injection, pool leaks).
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
qv framework [PATH] [OPTIONS]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Options:
|
|
206
|
+
- `-n, --name [fastapi|sqlalchemy|sql|all]`: Filter analysis to a specific framework (default: all detected frameworks).
|
|
207
|
+
- `--json`: Output framework scan results as structured JSON.
|
|
208
|
+
- `--sarif`: Output framework scan results in SARIF v2.1.0 format.
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
```bash
|
|
212
|
+
# Scan current project for all framework issues
|
|
213
|
+
qv framework
|
|
214
|
+
|
|
215
|
+
# Scan specifically for FastAPI issues
|
|
216
|
+
qv framework --name fastapi
|
|
217
|
+
|
|
218
|
+
# Scan specifically for SQLAlchemy / SQL database issues
|
|
219
|
+
qv framework --name sqlalchemy
|
|
220
|
+
```
|
|
221
|
+
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
# Diagnostic Rules Catalog
|
|
2
|
+
|
|
3
|
+
`qv` rule IDs are stable public identifiers. This catalog lists all supported diagnostic checks, their default severities, and remediation strategies.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Dependency Rules
|
|
8
|
+
|
|
9
|
+
### 1.1 DEP-001: Dependency Constraint Conflict
|
|
10
|
+
- **Default Severity:** `ERROR`
|
|
11
|
+
- **Description:** Two or more declared or transitive dependencies have incompatible version constraints.
|
|
12
|
+
- **Remediation:** Upgrade the conflicting package or loosen the version pin to satisfy all requirements.
|
|
13
|
+
|
|
14
|
+
### 1.2 DEP-002: Missing Dependency Declaration
|
|
15
|
+
- **Default Severity:** `ERROR`
|
|
16
|
+
- **Description:** A third-party package is imported in project source code but is not declared in `pyproject.toml` or requirements files.
|
|
17
|
+
- **Remediation:** Add the missing package to your project dependencies (`uv add <package>` or `pip install <package>`).
|
|
18
|
+
|
|
19
|
+
### 1.3 DEP-003: Unused Declared Dependency
|
|
20
|
+
- **Default Severity:** `WARNING`
|
|
21
|
+
- **Description:** A package is declared as a direct dependency, but no import statements were detected across project source files.
|
|
22
|
+
- **Remediation:** Verify if this dependency is required at runtime or remove it to keep dependencies lean.
|
|
23
|
+
|
|
24
|
+
### 1.4 DEP-004: Python Compatibility Mismatch
|
|
25
|
+
- **Default Severity:** `WARNING`
|
|
26
|
+
- **Description:** A package requires a Python version incompatible with the target project runtime.
|
|
27
|
+
- **Remediation:** Upgrade your target Python version or install a version of the package compatible with your environment.
|
|
28
|
+
|
|
29
|
+
### 1.5 DEP-005: Installed/Declaration Mismatch
|
|
30
|
+
- **Default Severity:** `WARNING`
|
|
31
|
+
- **Description:** The package version installed in the virtualenv does not satisfy the constraint declared in your project manifest.
|
|
32
|
+
- **Remediation:** Synchronize your virtual environment using `uv sync`, `poetry install`, or `pip install -r requirements.txt`.
|
|
33
|
+
|
|
34
|
+
### 1.6 DEP-006: Vulnerable Transitive Dependency
|
|
35
|
+
- **Default Severity:** `ERROR`
|
|
36
|
+
- **Description:** A security vulnerability has been identified in a direct or transitive dependency.
|
|
37
|
+
- **Remediation:** Update the vulnerable dependency or apply security patches.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 2. Environment & Drift Rules
|
|
42
|
+
|
|
43
|
+
### 2.1 ENV-001: Python Version Drift
|
|
44
|
+
- **Default Severity:** `WARNING`
|
|
45
|
+
- **Description:** The active Python interpreter version differs from the project's target runtime specification.
|
|
46
|
+
- **Remediation:** Rebuild your virtual environment using the configured target Python version.
|
|
47
|
+
|
|
48
|
+
### 2.2 ENV-002: Docker Runtime Drift
|
|
49
|
+
- **Default Severity:** `WARNING`
|
|
50
|
+
- **Description:** The Python version specified in `Dockerfile` or `compose.yml` differs from the project's target runtime.
|
|
51
|
+
- **Remediation:** Update the base image tag in `Dockerfile` (e.g. `FROM python:3.12-slim`).
|
|
52
|
+
|
|
53
|
+
### 2.3 ENV-003: CI Runtime Drift
|
|
54
|
+
- **Default Severity:** `WARNING`
|
|
55
|
+
- **Description:** CI workflow matrix does not test the Python versions declared in project support.
|
|
56
|
+
- **Remediation:** Align your CI test matrix with `pyproject.toml` supported versions.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 3. Import & Architecture Rules
|
|
61
|
+
|
|
62
|
+
### 3.1 IMP-001: Circular Import Detected
|
|
63
|
+
- **Default Severity:** `ERROR`
|
|
64
|
+
- **Description:** An import cycle exists between two or more local modules, risking runtime `ImportError` or partially initialized modules.
|
|
65
|
+
- **Remediation:** Break the cycle by refactoring shared logic into a separate module or using deferred imports inside functions.
|
|
66
|
+
|
|
67
|
+
### 3.2 IMP-002: Unresolved Local Import
|
|
68
|
+
- **Default Severity:** `ERROR`
|
|
69
|
+
- **Description:** A local module imported in source code could not be resolved on the project's Python path.
|
|
70
|
+
- **Remediation:** Check the module name and ensure source directories are on the Python path or package root.
|
|
71
|
+
|
|
72
|
+
### 3.3 IMP-003: Unused / Orphan Local Module
|
|
73
|
+
- **Default Severity:** `WARNING`
|
|
74
|
+
- **Description:** A Python source file exists in the project but is never imported or referenced by any other module, entry point, or test file.
|
|
75
|
+
- **Remediation:** Review if the module is dead/obsolete and can be safely removed, or export it in package `__init__.py`.
|
|
76
|
+
|
|
77
|
+
### 3.4 IMP-004: Deprecated or Removed Standard Library Module
|
|
78
|
+
- **Default Severity:** `ERROR`
|
|
79
|
+
- **Description:** A standard library module imported in source code was deprecated or removed in modern Python 3.11-3.13 (PEP 594).
|
|
80
|
+
- **Remediation:** Replace the removed stdlib module with its modern replacement (e.g. `importlib` instead of `imp`, `subprocess` instead of `pipes`).
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 4. Packaging Rules
|
|
85
|
+
|
|
86
|
+
### 4.1 PKG-001: Missing Package Metadata
|
|
87
|
+
- **Default Severity:** `WARNING`
|
|
88
|
+
- **Description:** Essential packaging metadata (such as project name, version, or description) is missing from `pyproject.toml`.
|
|
89
|
+
- **Remediation:** Provide standard PEP 621 fields under the `[project]` table.
|
|
90
|
+
|
|
91
|
+
### 4.2 PKG-002: Invalid Project Configuration
|
|
92
|
+
- **Default Severity:** `ERROR`
|
|
93
|
+
- **Description:** `pyproject.toml` contains syntax errors or invalid configuration tables.
|
|
94
|
+
- **Remediation:** Fix TOML syntax and ensure configuration follows PEP 621 specifications.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 5. FastAPI Framework Rules (`FAP-xxx`)
|
|
99
|
+
|
|
100
|
+
### 5.1 FAP-001: Blocking Call or CPU-Bound Operation in Async Endpoint
|
|
101
|
+
- **Default Severity:** `ERROR`
|
|
102
|
+
- **Description:** Synchronous blocking operations (such as `time.sleep()`, synchronous `requests`, `urllib.request.urlopen()`, `subprocess.run()`, or synchronous database queries) or heavy CPU-bound hashing (`bcrypt.hashpw`, `passlib`, `hashlib.pbkdf2_hmac`) called inside an `async def` FastAPI route handler stall the entire asyncio event loop.
|
|
103
|
+
- **Remediation:** Use async non-blocking alternatives (e.g. `asyncio.sleep()`, `httpx.AsyncClient()`), delegate work to worker threads via `anyio.to_thread.run_sync()`, or declare the route as regular synchronous `def` so FastAPI executes it in a worker threadpool.
|
|
104
|
+
|
|
105
|
+
### 5.2 FAP-002: Blocking Dependency in Async Path
|
|
106
|
+
- **Default Severity:** `WARNING`
|
|
107
|
+
- **Description:** An `async def` route handler depends on a dependency (`Depends(...)`) that executes synchronous blocking I/O calls or CPU-heavy computations.
|
|
108
|
+
- **Remediation:** Refactor the dependency function to use non-blocking async libraries or wrap blocking logic with a threadpool executor.
|
|
109
|
+
|
|
110
|
+
### 5.3 FAP-003: Missing Response Model or Return Type Annotation
|
|
111
|
+
- **Default Severity:** `WARNING`
|
|
112
|
+
- **Description:** FastAPI route handler does not declare a `response_model` argument in its route decorator or provide a return type annotation, disabling OpenAPI schema generation and automated response serialization/filtering.
|
|
113
|
+
- **Remediation:** Add a return type annotation (`def get_item(...) -> ItemResponse:`) or set `response_model=ItemResponse` in the decorator.
|
|
114
|
+
|
|
115
|
+
### 5.4 FAP-004: Insecure CORS or Debug Configuration
|
|
116
|
+
- **Default Severity:** `ERROR`
|
|
117
|
+
- **Description:** `CORSMiddleware` configured with wildcard `allow_origins=["*"]` and `allow_credentials=True`, insecure plaintext HTTP `tokenUrl` in `OAuth2PasswordBearer`, or FastAPI application instantiated with `debug=True`. Wildcard origins with credentials violate browser security policies and expose authentication state.
|
|
118
|
+
- **Remediation:** Specify explicit trusted origins when credentials are enabled, use HTTPS or relative paths for OAuth2 token URLs, and disable debug mode in production.
|
|
119
|
+
|
|
120
|
+
### 5.5 FAP-005: Missing HTTP Client Timeout Configuration
|
|
121
|
+
- **Default Severity:** `WARNING`
|
|
122
|
+
- **Description:** HTTP client (such as `httpx.AsyncClient`, `httpx.Client`, or `aiohttp.ClientSession`) instantiated without an explicit timeout or with `timeout=None`.
|
|
123
|
+
- **Remediation:** Pass an explicit timeout parameter (e.g. `httpx.AsyncClient(timeout=10.0)`) to prevent hung connections and resource starvation.
|
|
124
|
+
|
|
125
|
+
### 5.6 FAP-006: Route Path Parameter Template Mismatch
|
|
126
|
+
- **Default Severity:** `ERROR`
|
|
127
|
+
- **Description:** A path parameter defined in the route URL template (e.g. `@app.get("/users/{user_id}")`) does not match any parameter in the handler function signature, causing runtime 422 Unprocessable Entity errors.
|
|
128
|
+
- **Remediation:** Ensure all `{param}` placeholders in route URL decorators match parameter names in the endpoint function signature.
|
|
129
|
+
|
|
130
|
+
### 5.7 FAP-007: Unsafe Yield Dependency Without Try/Finally
|
|
131
|
+
- **Default Severity:** `WARNING`
|
|
132
|
+
- **Description:** A generator dependency using `yield` does not wrap cleanup code in a `try...finally` block. If an exception occurs during request processing, teardown code after `yield` will not execute, leading to database connection or resource leaks.
|
|
133
|
+
- **Remediation:** Wrap the `yield` and teardown cleanup inside a `try...finally` block.
|
|
134
|
+
|
|
135
|
+
### 5.8 FAP-008: Deprecated `@app.on_event` Lifecycle Hook
|
|
136
|
+
- **Default Severity:** `WARNING`
|
|
137
|
+
- **Description:** Using deprecated `@app.on_event("startup")` or `@app.on_event("shutdown")` event hooks instead of modern lifespan context managers.
|
|
138
|
+
- **Remediation:** Migrate lifecycle logic to `@asynccontextmanager async def lifespan(app: FastAPI)` and pass `lifespan` to `FastAPI(lifespan=lifespan)`.
|
|
139
|
+
|
|
140
|
+
### 5.9 FAP-009: Untyped or Unvalidated Request Body
|
|
141
|
+
- **Default Severity:** `WARNING`
|
|
142
|
+
- **Description:** Route handler accepts an untyped parameter (such as `dict`, `Any`, or untyped `Body(...)`) instead of a strict Pydantic model, bypassing schema validation and API documentation.
|
|
143
|
+
- **Remediation:** Create a Pydantic `BaseModel` schema and use it as the type annotation for request payloads.
|
|
144
|
+
|
|
145
|
+
### 5.10 FAP-010: Shadowed or Duplicate Route Endpoint
|
|
146
|
+
- **Default Severity:** `ERROR`
|
|
147
|
+
- **Description:** Multiple route handlers register the same HTTP method and path on the same router/app instance, causing one endpoint to silently shadow the other.
|
|
148
|
+
- **Remediation:** Ensure all routes have distinct paths or combine the logic into a single handler.
|
|
149
|
+
|
|
150
|
+
### 5.11 FAP-011: Mutable Default Value in Route Parameter
|
|
151
|
+
- **Default Severity:** `WARNING`
|
|
152
|
+
- **Description:** Route handler parameter uses a mutable default value (such as `list`, `dict`, or `set`), which can leak state across concurrent requests.
|
|
153
|
+
- **Remediation:** Use `None` as the default value (e.g. `filters: list[str] | None = None`) or use `Field(default_factory=list)`.
|
|
154
|
+
|
|
155
|
+
### 5.12 FAP-012: Invalid Exception Handler Signature
|
|
156
|
+
- **Default Severity:** `ERROR`
|
|
157
|
+
- **Description:** Custom `@app.exception_handler` function does not accept the required `(request: Request, exc: Exception)` signature, causing runtime crashes when exceptions occur.
|
|
158
|
+
- **Remediation:** Update the handler function signature to accept exactly `(request: Request, exc: Exception)`.
|
|
159
|
+
|
|
160
|
+
### 5.13 FAP-013: Untracked `asyncio.create_task` in Route Handler
|
|
161
|
+
- **Default Severity:** `WARNING`
|
|
162
|
+
- **Description:** Spawning raw `asyncio.create_task()` directly inside route handlers without background task management, which can lead to unhandled crashes or premature task collection.
|
|
163
|
+
- **Remediation:** Use FastAPI's built-in `BackgroundTasks` (`background_tasks.add_task(...)`) for reliable request-scoped task execution.
|
|
164
|
+
|
|
165
|
+
### 5.14 FAP-014: Potential Path Traversal in `FileResponse`
|
|
166
|
+
- **Default Severity:** `WARNING`
|
|
167
|
+
- **Description:** Passing an unsanitized path parameter or query string directly to `FileResponse()`, creating an arbitrary file read vulnerability.
|
|
168
|
+
- **Remediation:** Validate that the resolved file path is inside an allowed base directory using `path.is_relative_to(base_dir)`.
|
|
169
|
+
|
|
170
|
+
### 5.15 FAP-015: Blocking File I/O in Async Endpoint
|
|
171
|
+
- **Default Severity:** `ERROR`
|
|
172
|
+
- **Description:** Synchronous `open()`, `.read()`, or `.write()` calls inside `async def` routes block the asyncio event loop.
|
|
173
|
+
- **Remediation:** Use `aiofiles`, `anyio.Path`, or run file operations in a synchronous `def` route.
|
|
174
|
+
|
|
175
|
+
### 5.16 FAP-016: Sensitive Field Exposure in Schema
|
|
176
|
+
- **Default Severity:** `ERROR`
|
|
177
|
+
- **Description:** Pydantic model declares sensitive fields (e.g. `password`, `hashed_password`, `secret_key`) without `Field(exclude=True)` or output filtering.
|
|
178
|
+
- **Remediation:** Exclude sensitive fields from output schemas or mark them with `Field(exclude=True)`.
|
|
179
|
+
|
|
180
|
+
### 5.17 FAP-017: Non-Standard HTTP Status Code on POST or DELETE
|
|
181
|
+
- **Default Severity:** `WARNING`
|
|
182
|
+
- **Description:** `@app.post(...)` or `@app.delete(...)` routes return `200 OK` by default instead of explicit REST status codes (`201 Created` or `204 No Content`).
|
|
183
|
+
- **Remediation:** Declare `status_code=status.HTTP_201_CREATED` or `status.HTTP_204_NO_CONTENT`.
|
|
184
|
+
|
|
185
|
+
### 5.18 FAP-018: Global In-Memory State Mutation in Route Handler
|
|
186
|
+
- **Default Severity:** `ERROR`
|
|
187
|
+
- **Description:** Mutating module-level global variables or dictionaries inside route handlers without locks causes concurrency race conditions.
|
|
188
|
+
- **Remediation:** Persist shared state in a database/cache or guard operations with `asyncio.Lock`.
|
|
189
|
+
|
|
190
|
+
### 5.19 FAP-019: Insecure Cookie Configuration
|
|
191
|
+
- **Default Severity:** `ERROR`
|
|
192
|
+
- **Description:** `response.set_cookie()` called with `httponly=False` or `secure=False`, leaving authentication cookies vulnerable to XSS and interception.
|
|
193
|
+
- **Remediation:** Always set `httponly=True` and `secure=True` for authentication and session cookies.
|
|
194
|
+
|
|
195
|
+
### 5.20 FAP-020: Potential Open Redirect in `RedirectResponse`
|
|
196
|
+
- **Default Severity:** `WARNING`
|
|
197
|
+
- **Description:** Initializing `RedirectResponse` directly with user-supplied URL inputs without whitelist validation.
|
|
198
|
+
- **Remediation:** Verify that redirect URLs are relative paths or match an allowed domain whitelist.
|
|
199
|
+
|
|
200
|
+
### 5.21 FAP-021: Router Included Without Tags or Prefix
|
|
201
|
+
- **Default Severity:** `WARNING`
|
|
202
|
+
- **Description:** `app.include_router()` called without `tags` or `prefix`, leading to disorganized OpenAPI documentation.
|
|
203
|
+
- **Remediation:** Specify `prefix` and `tags` when mounting routers.
|
|
204
|
+
|
|
205
|
+
### 5.22 FAP-022: WebSocket Route Missing `await websocket.accept()`
|
|
206
|
+
- **Default Severity:** `ERROR`
|
|
207
|
+
- **Description:** WebSocket route handler attempts to process messages before completing the connection handshake with `await websocket.accept()`.
|
|
208
|
+
- **Remediation:** Call `await websocket.accept()` before reading or writing data.
|
|
209
|
+
|
|
210
|
+
### 5.23 FAP-023: Deprecated Pydantic v1 `class Config`
|
|
211
|
+
- **Default Severity:** `WARNING`
|
|
212
|
+
- **Description:** Using legacy inner `class Config:` inside Pydantic models instead of modern `model_config = ConfigDict(...)`.
|
|
213
|
+
- **Remediation:** Migrate inner configuration to `model_config = ConfigDict(...)`.
|
|
214
|
+
|
|
215
|
+
### 5.24 FAP-024: Redundant Duplicate Dependency Declaration
|
|
216
|
+
- **Default Severity:** `WARNING`
|
|
217
|
+
- **Description:** Multiple parameters in the same route handler declare identical `Depends(...)` targets.
|
|
218
|
+
- **Remediation:** Consolidate redundant dependencies into a single parameter.
|
|
219
|
+
|
|
220
|
+
### 5.25 FAP-025: Returned HTTPException Instance Instead of Raise
|
|
221
|
+
- **Default Severity:** `ERROR`
|
|
222
|
+
- **Description:** Returning an `HTTPException` instance in a route handler returns a `200 OK` HTTP response with the serialized exception object instead of raising an error.
|
|
223
|
+
- **Remediation:** Use `raise HTTPException(...)` instead of `return HTTPException(...)`.
|
|
224
|
+
|
|
225
|
+
### 5.26 FAP-026: Deprecated Pydantic v1 `@validator` or `@root_validator`
|
|
226
|
+
- **Default Severity:** `WARNING`
|
|
227
|
+
- **Description:** Using deprecated Pydantic v1 `@validator` or `@root_validator` decorators instead of modern Pydantic v2 `@field_validator` or `@model_validator`.
|
|
228
|
+
- **Remediation:** Migrate to `@field_validator` or `@model_validator` from `pydantic`.
|
|
229
|
+
|
|
230
|
+
### 5.27 FAP-027: Dependency Parameter Missing Type Annotation
|
|
231
|
+
- **Default Severity:** `WARNING`
|
|
232
|
+
- **Description:** Route parameter initialized with `Depends(...)`, `Query(...)`, or `Path(...)` does not specify a type annotation, disabling schema generation and static type checking.
|
|
233
|
+
- **Remediation:** Add an explicit type annotation (e.g. `param: Type = Depends(...)` or `param: Annotated[Type, Depends(...)]`).
|
|
234
|
+
|
|
235
|
+
### 5.28 FAP-028: Raw `json.loads` Called on Request Body
|
|
236
|
+
- **Default Severity:** `WARNING`
|
|
237
|
+
- **Description:** Calling `json.loads(await request.body())` is an anti-pattern when FastAPI/Starlette provide the optimized built-in `await request.json()` method.
|
|
238
|
+
- **Remediation:** Replace `json.loads(await request.body())` with `await request.json()`.
|
|
239
|
+
|
|
240
|
+
### 5.29 FAP-029: StreamingResponse Initialized Without `media_type`
|
|
241
|
+
- **Default Severity:** `WARNING`
|
|
242
|
+
- **Description:** Instantiating `StreamingResponse` without specifying `media_type` can cause clients to misinterpret the streamed content format.
|
|
243
|
+
- **Remediation:** Pass an explicit `media_type` argument (e.g. `StreamingResponse(stream, media_type='application/json')`).
|
|
244
|
+
|
|
245
|
+
### 5.30 FAP-030: Missing Route Summary or Docstring for OpenAPI Documentation
|
|
246
|
+
- **Default Severity:** `WARNING`
|
|
247
|
+
- **Description:** Public route endpoint does not provide a docstring, `summary`, or `description`, resulting in sparse and incomplete OpenAPI documentation.
|
|
248
|
+
- **Remediation:** Add a function docstring or pass `summary=...` in the route decorator.
|
|
249
|
+
|
|
250
|
+
### 5.31 FAP-031: Prefer `typing.Annotated` Over Parameter Default Assignment
|
|
251
|
+
- **Default Severity:** `INFO`
|
|
252
|
+
- **Description:** Using default parameter assignments (e.g. `db: Session = Depends(...)`) can cause issues with type checkers and testing. Modern FastAPI strongly recommends `Annotated[Session, Depends(...)]`.
|
|
253
|
+
- **Remediation:** Refactor parameter to `param: Annotated[Type, Depends(...)]`.
|
|
254
|
+
|
|
255
|
+
### 5.32 FAP-032: Redundant `jsonable_encoder` With `response_model`
|
|
256
|
+
- **Default Severity:** `WARNING`
|
|
257
|
+
- **Description:** Calling `jsonable_encoder()` in a route handler that already specifies `response_model` causes redundant double serialization, reducing throughput.
|
|
258
|
+
- **Remediation:** Return raw objects/dictionaries and allow FastAPI's `response_model` to handle serialization.
|
|
259
|
+
|
|
260
|
+
### 5.33 FAP-033: Missing `from_attributes=True` in ORM Response Schema
|
|
261
|
+
- **Default Severity:** `WARNING`
|
|
262
|
+
- **Description:** Pydantic response models for ORM/database entities missing `model_config = ConfigDict(from_attributes=True)` trigger validation errors at runtime.
|
|
263
|
+
- **Remediation:** Add `model_config = ConfigDict(from_attributes=True)` to the schema.
|
|
264
|
+
|
|
265
|
+
### 5.34 FAP-034: Missing `await` on `request.json()` or `request.body()`
|
|
266
|
+
- **Default Severity:** `ERROR`
|
|
267
|
+
- **Description:** Calling `request.json()` or `request.body()` without `await` inside an `async def` handler assigns an un-awaited coroutine object instead of the payload.
|
|
268
|
+
- **Remediation:** Add `await` before `request.json()` or `request.body()`.
|
|
269
|
+
|
|
270
|
+
### 5.35 FAP-035: Raw `Exception` Raised Instead of `HTTPException`
|
|
271
|
+
- **Default Severity:** `WARNING`
|
|
272
|
+
- **Description:** Raising generic `Exception`, `ValueError`, or `RuntimeError` produces unhandled `500 Internal Server Error` responses instead of structured REST responses.
|
|
273
|
+
- **Remediation:** Raise `HTTPException(status_code=..., detail=...)`.
|
|
274
|
+
|
|
275
|
+
### 5.36 FAP-036: Mutating `app.state` Inside Route Handler
|
|
276
|
+
- **Default Severity:** `WARNING`
|
|
277
|
+
- **Description:** Mutating `request.app.state` or `app.state` in per-request handlers introduces concurrency race conditions.
|
|
278
|
+
- **Remediation:** Initialize shared state during `lifespan` startup.
|
|
279
|
+
|
|
280
|
+
### 5.37 FAP-037: `SecurityScopes` Declared With `Depends` Instead of `Security`
|
|
281
|
+
- **Default Severity:** `WARNING`
|
|
282
|
+
- **Description:** Declaring a `SecurityScopes` parameter with `Depends(...)` does not pass scopes. Use `Security(..., scopes=[...])`.
|
|
283
|
+
- **Remediation:** Use `Security(dependency, scopes=[...])`.
|
|
284
|
+
|
|
285
|
+
### 5.38 FAP-038: Hardcoded HTTP Status Code Integer
|
|
286
|
+
- **Default Severity:** `INFO`
|
|
287
|
+
- **Description:** Using integer literals for `status_code` (e.g. `status_code=201`) reduces readability compared to `status.HTTP_201_CREATED`.
|
|
288
|
+
- **Remediation:** Import `status` from `fastapi` and use named constants (e.g. `status.HTTP_201_CREATED`).
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 6. SQLAlchemy & SQL Database Rules
|
|
293
|
+
|
|
294
|
+
### 6.1 SQL-001: Potential N+1 Database Query in Loop
|
|
295
|
+
- **Default Severity:** `WARNING`
|
|
296
|
+
- **Description:** Executing queries (`session.execute`, `session.query`, `session.get`) inside `for` or `while` loops triggers N+1 database roundtrips.
|
|
297
|
+
- **Remediation:** Batch load records using `where(Model.id.in_(ids))` or configure eager loading with `joinedload`/`selectinload`.
|
|
298
|
+
|
|
299
|
+
### 6.2 SQL-002: Session Instantiated Without Context Manager or Cleanup
|
|
300
|
+
- **Default Severity:** `WARNING`
|
|
301
|
+
- **Description:** Creating a `Session` or `SessionLocal()` without a `with` block or `try...finally: session.close()` leaks active database connections.
|
|
302
|
+
- **Remediation:** Use `with SessionLocal() as session:` or a dependency yield pattern.
|
|
303
|
+
|
|
304
|
+
### 6.3 SQL-003: Synchronous DB Operation in Async Event Loop
|
|
305
|
+
- **Default Severity:** `ERROR`
|
|
306
|
+
- **Description:** Invoking synchronous `create_engine()` or synchronous session operations inside `async def` functions blocks the event loop.
|
|
307
|
+
- **Remediation:** Use `create_async_engine()` and `AsyncSession` with async drivers (e.g. `asyncpg`, `aiosqlite`).
|
|
308
|
+
|
|
309
|
+
### 6.4 SQL-004: Raw SQL String Interpolation (SQL Injection Risk)
|
|
310
|
+
- **Default Severity:** `ERROR`
|
|
311
|
+
- **Description:** Formatting SQL query strings using f-strings, `%`, or `.format()` inside `text()` or `cursor.execute()` introduces critical SQL injection vulnerabilities.
|
|
312
|
+
- **Remediation:** Use bound parameters (`text("SELECT * FROM users WHERE id = :id"), {"id": user_val}`).
|
|
313
|
+
|
|
314
|
+
### 6.5 SQL-005: Legacy SQLAlchemy 1.x `session.query()` Syntax
|
|
315
|
+
- **Default Severity:** `WARNING`
|
|
316
|
+
- **Description:** Using legacy `session.query(...)` in SQLAlchemy 2.0 projects instead of 2.0-style `select()` statements.
|
|
317
|
+
- **Remediation:** Migrate to `session.scalars(select(Model).where(...))`.
|
|
318
|
+
|
|
319
|
+
### 6.6 SQL-006: Missing Relationship Eager Loading Strategy in Async Session
|
|
320
|
+
- **Default Severity:** `WARNING`
|
|
321
|
+
- **Description:** Accessing lazy-loaded ORM relationships in async sessions triggers `MissingGreenlet` errors at runtime.
|
|
322
|
+
- **Remediation:** Configure `lazy="selectin"` on relationships or apply `.options(selectinload(Model.relation))`.
|
|
323
|
+
|
|
324
|
+
### 6.7 SQL-007: Uncommitted Transaction in Mutation Function
|
|
325
|
+
- **Default Severity:** `WARNING`
|
|
326
|
+
- **Description:** A function calls `session.add(...)` or `session.delete(...)` but never executes `session.commit()` or `with session.begin():`.
|
|
327
|
+
- **Remediation:** Call `session.commit()` or wrap in `with session.begin():`.
|
|
328
|
+
|
|
329
|
+
### 6.8 SQL-008: `create_engine` Missing `pool_pre_ping` Connection Health Check
|
|
330
|
+
- **Default Severity:** `WARNING`
|
|
331
|
+
- **Description:** Database engines created without `pool_pre_ping=True` risk stale connection disconnects when idle connections are closed by servers/firewalls.
|
|
332
|
+
- **Remediation:** Add `pool_pre_ping=True` to `create_engine()` / `create_async_engine()`.
|
|
333
|
+
|
|
334
|
+
### 6.9 SQL-009: `expire_on_commit=True` in `AsyncSession`
|
|
335
|
+
- **Default Severity:** `WARNING`
|
|
336
|
+
- **Description:** Leaving `expire_on_commit=True` in `AsyncSession` or `async_sessionmaker` causes `MissingGreenlet` errors when accessing committed model attributes.
|
|
337
|
+
- **Remediation:** Specify `expire_on_commit=False`.
|
|
338
|
+
|
|
339
|
+
### 6.10 SQL-010: Hardcoded Database Credentials in Connection URL
|
|
340
|
+
- **Default Severity:** `ERROR`
|
|
341
|
+
- **Description:** Plaintext passwords and connection secrets are committed directly into Python source code.
|
|
342
|
+
- **Remediation:** Load database credentials from environment variables (`os.getenv("DATABASE_URL")`).
|
|
343
|
+
|
|
344
|
+
### 6.11 SQL-011: Unbounded `SELECT` Query Without Limit or Pagination
|
|
345
|
+
- **Default Severity:** `WARNING`
|
|
346
|
+
- **Description:** Calling `.all()` on database queries without `.limit()` or pagination clauses risks out-of-memory crashes on large tables.
|
|
347
|
+
- **Remediation:** Add `.limit(PAGE_SIZE)` and pagination parameters.
|
|
348
|
+
|
|
349
|
+
### 6.12 SQL-012: Relationship Cascade Delete Without ForeignKey `ondelete="CASCADE"`
|
|
350
|
+
- **Default Severity:** `INFO`
|
|
351
|
+
- **Description:** `cascade="all, delete-orphan"` on ORM relationships without database-level `ondelete="CASCADE"` on the foreign key forces slow Python-side row-by-row deletion.
|
|
352
|
+
- **Remediation:** Add `ondelete="CASCADE"` to the `ForeignKey` definition.
|
|
353
|
+
|
|
354
|
+
### 6.13 SQL-013: Session Flush or Commit Called Inside Loop
|
|
355
|
+
- **Default Severity:** `WARNING`
|
|
356
|
+
- **Description:** Calling `session.flush()` or `session.commit()` inside tight loops creates excessive database network roundtrips.
|
|
357
|
+
- **Remediation:** Commit once after the loop or use bulk insert statements (`session.execute(insert(Model), batch)`).
|
|
358
|
+
|
|
359
|
+
### 6.14 SQL-014: Deprecated `declarative_base()` Function
|
|
360
|
+
- **Default Severity:** `WARNING`
|
|
361
|
+
- **Description:** Using legacy `Base = declarative_base()` instead of modern SQLAlchemy 2.0 `class Base(DeclarativeBase): pass`.
|
|
362
|
+
- **Remediation:** Subclass `DeclarativeBase` from `sqlalchemy.orm`.
|
|
363
|
+
|
|
364
|
+
### 6.15 SQL-015: SQLite Engine Missing `check_same_thread=False` in Multi-Threaded Application
|
|
365
|
+
- **Default Severity:** `WARNING`
|
|
366
|
+
- **Description:** Using SQLite engines across threads in web servers without `connect_args={"check_same_thread": False}` causes `ProgrammingError`.
|
|
367
|
+
- **Remediation:** Set `connect_args={"check_same_thread": False}` when instantiating SQLite engines.
|
|
368
|
+
|
|
369
|
+
### 6.16 SQL-016: Missing Database Migration Configuration (Alembic)
|
|
370
|
+
- **Default Severity:** `INFO`
|
|
371
|
+
- **Description:** SQLAlchemy ORM models are declared in the codebase, but no Alembic migrations directory or `alembic.ini` file exists.
|
|
372
|
+
- **Remediation:** Initialize database migrations using `alembic init alembic`.
|
|
373
|
+
|
|
374
|
+
### 6.17 SQL-017: `Mapped[...]` Attribute Missing `mapped_column()` in 2.0 Declarative Model
|
|
375
|
+
- **Default Severity:** `WARNING`
|
|
376
|
+
- **Description:** Using legacy `Column(...)` or untyped field definitions alongside `Mapped[...]` type annotations in SQLAlchemy 2.0 declarative models.
|
|
377
|
+
- **Remediation:** Replace `col: Mapped[int] = Column(Integer)` with `col: Mapped[int] = mapped_column()`.
|
|
378
|
+
|
|
379
|
+
### 6.18 SQL-018: Excessive Connection Pool Size in Single Application Instance
|
|
380
|
+
- **Default Severity:** `WARNING`
|
|
381
|
+
- **Description:** Configuring `create_engine` with `pool_size` or `max_overflow` > 50 in a single application worker risks exhausting database `max_connections`.
|
|
382
|
+
- **Remediation:** Keep `pool_size` between 5 and 20 per worker and scale using an external connection pooler like pgBouncer.
|
|
383
|
+
|
|
384
|
+
### 6.19 SQL-019: `NullPool` Configured in Persistent Web Application
|
|
385
|
+
- **Default Severity:** `WARNING`
|
|
386
|
+
- **Description:** Setting `poolclass=NullPool` in a persistent web server disables connection pooling, forcing a new TCP handshake and SSL negotiation on every request.
|
|
387
|
+
- **Remediation:** Use default `QueuePool` for persistent web applications; reserve `NullPool` for ephemeral AWS Lambda/serverless environments.
|
|
388
|
+
|
|
389
|
+
### 6.20 SQL-020: Declarative ORM Model Missing Primary Key Definition
|
|
390
|
+
- **Default Severity:** `ERROR`
|
|
391
|
+
- **Description:** A concrete model inheriting from `Base` / `DeclarativeBase` does not declare a primary key column, causing runtime ORM identity map failures.
|
|
392
|
+
- **Remediation:** Mark at least one column with `primary_key=True` or `mapped_column(primary_key=True)`.
|
|
393
|
+
|
|
394
|
+
### 6.21 SQL-021: Missing `session.rollback()` in Database Exception Handler
|
|
395
|
+
- **Default Severity:** `WARNING`
|
|
396
|
+
- **Description:** An `except` block catches errors around database operations but fails to call `session.rollback()`, leaving the session in an unusable invalid transaction state.
|
|
397
|
+
- **Remediation:** Add `session.rollback()` inside `except` handlers or wrap operations in `with session.begin():`.
|
|
398
|
+
|
|
399
|
+
### 6.22 SQL-022: Large Binary or Heavy Text Column Without `deferred()` Strategy
|
|
400
|
+
- **Default Severity:** `INFO`
|
|
401
|
+
- **Description:** Model includes heavy `LargeBinary`, `BLOB`, or `BYTEA` columns that are eagerly loaded on every `SELECT *`, consuming excessive application memory.
|
|
402
|
+
- **Remediation:** Wrap the column in `deferred(Column(LargeBinary))` or use `.options(load_only(...))`.
|
|
403
|
+
|
|
404
|
+
### 6.23 SQL-023: `ForeignKey` Column Defined Without `index=True`
|
|
405
|
+
- **Default Severity:** `WARNING`
|
|
406
|
+
- **Description:** Foreign key column created without an index, resulting in slow sequential table scans during `JOIN` queries, foreign key lookups, and cascade operations.
|
|
407
|
+
- **Remediation:** Add `index=True` to ForeignKey column definitions (e.g. `Column(Integer, ForeignKey("users.id"), index=True)`).
|
|
408
|
+
|
|
409
|
+
### 6.24 SQL-024: Insecure Unencrypted Remote Database Connection URL
|
|
410
|
+
- **Default Severity:** `WARNING`
|
|
411
|
+
- **Description:** Remote database connection URL targeting external hosts does not specify SSL encryption parameters (`sslmode=require` or `ssl=true`).
|
|
412
|
+
- **Remediation:** Append `?sslmode=require` (PostgreSQL) or `?ssl=true` to remote connection strings.
|
|
413
|
+
|
|
414
|
+
### 6.25 SQL-025: Deprecated `engine.execute()` or `engine.scalar()` Direct Call
|
|
415
|
+
- **Default Severity:** `ERROR`
|
|
416
|
+
- **Description:** Invoking `engine.execute(...)` or `engine.scalar(...)` directly is removed in SQLAlchemy 2.0.
|
|
417
|
+
- **Remediation:** Acquire an explicit connection: `with engine.connect() as conn: result = conn.execute(stmt)`.
|
|
418
|
+
|
|
419
|
+
### 6.26 SQL-026: `AsyncSession` Instantiated Without Async Context Manager or `await close()`
|
|
420
|
+
- **Default Severity:** `WARNING`
|
|
421
|
+
- **Description:** `AsyncSession` created in an `async def` function without `async with` or explicit `await session.close()`, causing leaked database connections.
|
|
422
|
+
- **Remediation:** Use `async with AsyncSessionLocal() as session:` or ensure `await session.close()` is called in a `finally` block.
|
|
423
|
+
|
|
424
|
+
### 6.27 SQL-027: Unsafe Concurrent Numeric Balance or Counter Update Without `with_for_update()`
|
|
425
|
+
- **Default Severity:** `WARNING`
|
|
426
|
+
- **Description:** Querying a record and mutating numeric balances or stock quantities (`balance -= amount`) without row-level locking causes lost-update race conditions.
|
|
427
|
+
- **Remediation:** Lock rows using `select(...).with_for_update()` or use atomic SQL increments `update(Account).values(balance=Account.balance - amount)`.
|
|
428
|
+
|
|
429
|
+
### 6.28 SQL-028: Thread-Local `scoped_session` Used in Async Context
|
|
430
|
+
- **Default Severity:** `ERROR`
|
|
431
|
+
- **Description:** Using thread-local `scoped_session` in `asyncio` code causes concurrent coroutines to unsafely share session instances.
|
|
432
|
+
- **Remediation:** Use `async_scoped_session(..., scopefunc=asyncio.current_task)` or FastAPI per-request dependency injection.
|
|
433
|
+
|
|
434
|
+
### 6.29 SQL-029: Declarative ORM Model Missing `__tablename__` Definition
|
|
435
|
+
- **Default Severity:** `ERROR`
|
|
436
|
+
- **Description:** Concrete declarative model class inheriting from `Base` does not declare `__tablename__` or `__table__`.
|
|
437
|
+
- **Remediation:** Define `__tablename__ = "table_name"` on the model (or set `__abstract__ = True` for reusable mixins).
|
|
438
|
+
|
|
439
|
+
### 6.30 SQL-030: Direct DBAPI `raw_connection()` Used Without Cleanup
|
|
440
|
+
- **Default Severity:** `WARNING`
|
|
441
|
+
- **Description:** Calling `engine.raw_connection()` bypasses connection pool lifecycle management and leaks connections if not closed explicitly.
|
|
442
|
+
- **Remediation:** Wrap in `try...finally`: `raw_conn = engine.raw_connection(); try: ... finally: raw_conn.close()`.
|
|
443
|
+
|
|
444
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import requests
|
|
5
|
+
from fastapi import Depends, FastAPI
|
|
6
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
7
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
8
|
+
|
|
9
|
+
# FAP-004: debug=True
|
|
10
|
+
app = FastAPI(debug=True)
|
|
11
|
+
|
|
12
|
+
# FAP-004: Insecure plaintext HTTP OAuth2 token URL
|
|
13
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="http://auth.example.com/token")
|
|
14
|
+
|
|
15
|
+
# FAP-004: Insecure CORS wildcard with allow_credentials=True
|
|
16
|
+
app.add_middleware(
|
|
17
|
+
CORSMiddleware,
|
|
18
|
+
allow_origins=["*"],
|
|
19
|
+
allow_credentials=True,
|
|
20
|
+
allow_methods=["*"],
|
|
21
|
+
allow_headers=["*"],
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# FAP-007: Unsafe yield dependency without try...finally
|
|
26
|
+
def get_db_session():
|
|
27
|
+
db = {"connected": True}
|
|
28
|
+
yield db
|
|
29
|
+
# If request fails, this close() is never reached
|
|
30
|
+
db["connected"] = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def blocking_sync_dependency():
|
|
34
|
+
time.sleep(1)
|
|
35
|
+
return {"user": "alice"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# FAP-008: Deprecated on_event startup hook
|
|
39
|
+
@app.on_event("startup")
|
|
40
|
+
async def startup_event():
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# FAP-001: Blocking calls in async route
|
|
45
|
+
# FAP-002: Dependent on blocking_sync_dependency
|
|
46
|
+
# FAP-003: Missing response_model / return annotation
|
|
47
|
+
# FAP-006: Route template mismatch ({item_id} missing in function arguments)
|
|
48
|
+
@app.get("/items/{item_id}")
|
|
49
|
+
async def get_items(
|
|
50
|
+
auth=Depends(blocking_sync_dependency), # noqa: B008
|
|
51
|
+
db=Depends(get_db_session), # noqa: B008
|
|
52
|
+
):
|
|
53
|
+
# Blocking calls in async def
|
|
54
|
+
time.sleep(2)
|
|
55
|
+
requests.get("https://api.example.com/data")
|
|
56
|
+
|
|
57
|
+
# FAP-005: Missing timeout on HTTP client
|
|
58
|
+
async with httpx.AsyncClient() as client:
|
|
59
|
+
resp = await client.get("https://httpbin.org/get")
|
|
60
|
+
|
|
61
|
+
return {"auth": auth, "data": resp.status_code}
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-qv"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "Diagnose why a Python project is unhealthy — explain root cause and suggest safe fixes."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|