Skip to content

Commit 637e924

Browse files
committed
feat: add request correlation IDs and address Bandit findings
1 parent 159eecc commit 637e924

5 files changed

Lines changed: 246 additions & 44 deletions

File tree

backend-api/app/api/v1/auth.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ async def google_callback(
280280
status_code=status.HTTP_302_FOUND,
281281
)
282282

283-
try:
283+
try:
284284
user = await user_manager.oauth_callback(
285285
oauth_name="google",
286286
access_token=google_access_token,
@@ -305,13 +305,22 @@ async def google_callback(
305305

306306
# fastapi-users JWTStrategy.write_token is async in the version used by the backend container.
307307
autoaudit_token = await get_jwt_strategy().write_token(user)
308+
308309
redirect_url = _frontend_google_callback_url(
309-
{"access_token": autoaudit_token, "token_type": "bearer"}
310+
{
311+
"access_token": autoaudit_token,
312+
"token_type": "bearer", # nosec B105
313+
}
314+
)
315+
316+
response = RedirectResponse(
317+
redirect_url,
318+
status_code=status.HTTP_302_FOUND,
310319
)
311320

312-
response = RedirectResponse(redirect_url, status_code=status.HTTP_302_FOUND)
313321
response.delete_cookie(
314322
GOOGLE_OAUTH_STATE_COOKIE,
315323
path=f"{settings.API_PREFIX}/auth/google/callback",
316324
)
317-
return response
325+
326+
return response

backend-api/app/api/v1/evidence.py

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
11
import hashlib
22
import json
3-
4-
from fastapi import APIRouter, Depends, UploadFile, File, Form
5-
from fastapi.responses import JSONResponse, RedirectResponse, FileResponse
6-
from sqlalchemy.ext.asyncio import AsyncSession
7-
8-
# Ensure the monorepo /security package is importable both locally and inside Docker
93
import sys
104
from pathlib import Path
115

6+
from fastapi import APIRouter, Depends, File, Form, UploadFile
7+
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
8+
from sqlalchemy.ext.asyncio import AsyncSession
129

10+
11+
# Ensure the monorepo /security package is importable both locally and inside Docker
1312
def _find_security_dir() -> Path | None:
1413
here = Path(__file__).resolve()
14+
1515
for ancestor in here.parents:
1616
candidate = ancestor / "security"
17+
1718
if candidate.exists():
1819
return candidate
20+
1921
return None
2022

2123

2224
SECURITY_DIR = _find_security_dir()
25+
2326
if SECURITY_DIR and str(SECURITY_DIR.parent) not in sys.path:
2427
sys.path.insert(0, str(SECURITY_DIR.parent))
2528

29+
2630
# Reuse existing evidence logic from security package
2731
from security.evidence_ui import app as evidence_ui
2832

@@ -33,6 +37,7 @@ def _find_security_dir() -> Path | None:
3337
from app.services.encryption import encrypt
3438
from app.services.evidence_validator import validate_text
3539

40+
3641
router = APIRouter(prefix="/evidence", tags=["evidence"])
3742

3843

@@ -45,7 +50,7 @@ async def strategies():
4550
- frontend/src/api/client.js -> getEvidenceStrategies()
4651
- GET /v1/evidence/strategies
4752
"""
48-
# Delegate to the existing evidence UI module (security/evidence_ui/app.py).
53+
# Delegate to the existing evidence UI module.
4954
return evidence_ui.api_strategies()
5055

5156

@@ -57,7 +62,7 @@ async def health():
5762

5863
@router.get("/scan-mem")
5964
async def scan_mem():
60-
"""Serve the human-friendly recent scans page (HTML)."""
65+
"""Serve the human-friendly recent scans page."""
6166
return evidence_ui.scan_mem_page()
6267

6368

@@ -92,12 +97,11 @@ async def scan(
9297
- POST /v1/evidence/scan (multipart/form-data)
9398
9499
Responsibilities in this layer:
95-
- (Best-effort) extract text + run validator pre-pass
100+
- Best-effort extract text and run validator pre-pass
96101
- Delegate the actual scanning to security/evidence_ui/app.py
97-
- (Best-effort) store validator output in DB (evidence_validation table)
98-
- Return the original scan response shape so the frontend can render it
102+
- Best-effort store validator output in the database
103+
- Return the original scan response shape
99104
"""
100-
# --- Validator pre-pass (best-effort) ---
101105
extracted_text = ""
102106
validator_payload: dict | None = None
103107
text_hash: str | None = None
@@ -107,61 +111,83 @@ async def scan(
107111
content_bytes = await evidence.read()
108112
await evidence.seek(0)
109113

110-
extracted_text, _preview_path = evidence_ui.extract_text_and_preview_bytes(
111-
evidence.filename or "", content_bytes, evidence_ui.PREVIEWS
114+
extracted_text, _preview_path = (
115+
evidence_ui.extract_text_and_preview_bytes(
116+
evidence.filename or "",
117+
content_bytes,
118+
evidence_ui.PREVIEWS,
119+
)
112120
)
121+
113122
validator_payload = validate_text(strategy_name, extracted_text)
114123

115124
if extracted_text:
116-
text_hash = hashlib.sha256(extracted_text.encode("utf-8", errors="ignore")).hexdigest()
125+
text_hash = hashlib.sha256(
126+
extracted_text.encode("utf-8", errors="ignore")
127+
).hexdigest()
128+
117129
except Exception:
118-
# Do not block scan if validator pre-pass fails.
130+
# Do not block the scan if the validator pre-pass fails.
119131
extracted_text = ""
120132
validator_payload = None
121133
text_hash = None
122134

123-
# Store only a capped excerpt of extracted text to reduce DB bloat.
124-
# If encryption isn't configured, skip encryption but do not break scanning.
135+
# Store only a capped excerpt of extracted text to reduce database bloat.
136+
# If encryption is not configured, skip encryption without breaking scanning.
125137
try:
126138
if extracted_text:
127139
extracted_text_encrypted = encrypt(extracted_text[:20000])
140+
128141
except Exception:
129142
extracted_text_encrypted = None
130143

131-
# delegate to existing implementation
132-
# NOTE: evidence_ui.scan is the "real" scanner implementation.
133-
# We keep this router thin and focused on integration concerns.
144+
# Delegate to the existing scanner implementation.
134145
scan_result = await evidence_ui.scan(
135146
evidence=evidence,
136147
strategy_name=strategy_name,
137148
user_id=str(current_user.id),
138149
)
139150

140-
# --- Append validator to response (without changing existing keys) ---
151+
# Append validator output without changing existing response keys.
141152
ok_value: bool | None = None
142153
response_payload: dict | None = None
143154

144155
if isinstance(scan_result, dict):
145156
response_payload = scan_result
146-
ok_value = bool(response_payload.get("ok")) if "ok" in response_payload else None
157+
158+
if "ok" in response_payload:
159+
ok_value = bool(response_payload.get("ok"))
160+
147161
if ok_value is True and validator_payload is not None:
148162
response_payload["validator"] = validator_payload
163+
149164
elif isinstance(scan_result, JSONResponse):
150165
try:
151-
payload = json.loads((scan_result.body or b"{}").decode("utf-8"))
166+
payload = json.loads(
167+
(scan_result.body or b"{}").decode("utf-8")
168+
)
169+
152170
except Exception:
153171
payload = None
172+
154173
if isinstance(payload, dict):
155174
response_payload = payload
156-
ok_value = bool(payload.get("ok")) if "ok" in payload else None
175+
176+
if "ok" in payload:
177+
ok_value = bool(payload.get("ok"))
178+
157179
if ok_value is True and validator_payload is not None:
158180
payload["validator"] = validator_payload
159-
# Return a new JSONResponse to include validator payload.
160-
scan_result = JSONResponse(payload, status_code=scan_result.status_code)
161181

162-
# --- Persist validator output (best-effort; never blocks scan) ---
182+
scan_result = JSONResponse(
183+
payload,
184+
status_code=scan_result.status_code,
185+
)
186+
187+
# Persist validator output as a best-effort operation.
163188
try:
164-
status = "success" if ok_value is True else "error"
189+
status_value = "success" if ok_value is True else "error"
190+
165191
if validator_payload is not None:
166192
record = EvidenceValidation(
167193
user_id=current_user.id,
@@ -170,14 +196,17 @@ async def scan(
170196
text_hash=text_hash,
171197
extracted_text_encrypted=extracted_text_encrypted,
172198
matches_json=validator_payload,
173-
status=status,
199+
status=status_value,
174200
)
201+
175202
db.add(record)
176203
await db.commit()
204+
177205
except Exception:
178206
try:
179207
await db.rollback()
180-
except Exception:
208+
209+
except Exception: # nosec B110
181210
pass
182211

183212
return scan_result
@@ -187,14 +216,14 @@ async def scan(
187216
async def download_report(
188217
filename: str,
189218
current_user: User = Depends(get_current_user),
190-
):
219+
):
191220
"""
192221
Backend API: download a generated report file.
193-
Requires authentication. Protected against path traversal.
222+
223+
Requires authentication and is protected against path traversal.
194224
195225
The frontend links to this URL using:
196226
- frontend/src/api/client.js -> getEvidenceReportUrl()
197227
- GET /v1/evidence/reports/{filename}
198228
"""
199-
# Reuse existing download handler in security/evidence_ui/app.py
200-
return evidence_ui.download_report(filename)
229+
return evidence_ui.download_report(filename)

backend-api/app/db/init_db.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
"""
77
import asyncio
88

9+
from fastapi_users.password import PasswordHelper
910
from sqlalchemy import select
1011

1112
from app.db.session import async_session_maker
12-
from app.models.user import User, Role
13-
from fastapi_users.password import PasswordHelper
13+
from app.models.user import Role, User
1414

1515

1616
async def init_db():
@@ -22,7 +22,7 @@ async def init_db():
2222
- This script will create OR update a default admin user for local development.
2323
"""
2424
admin_email = "admin@example.com"
25-
admin_password = "admin" # pragma: allowlist secret
25+
admin_password = "admin" # pragma: allowlist secret # nosec B105
2626

2727
password_helper = PasswordHelper()
2828

@@ -31,10 +31,12 @@ async def init_db():
3131
result = await session.execute(
3232
select(User).where(User.email == admin_email)
3333
)
34+
3435
# Be resilient to relationship eager-loads that can duplicate rows.
3536
existing_user = result.unique().scalar_one_or_none()
3637

3738
created = False
39+
3840
if existing_user:
3941
admin_user = existing_user
4042
else:
@@ -63,4 +65,4 @@ async def init_db():
6365

6466

6567
if __name__ == "__main__":
66-
asyncio.run(init_db())
68+
asyncio.run(init_db())

backend-api/pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,8 @@ evidence = [
4040
"fpdf2>=2.8.1",
4141
"tabulate>=0.10.0",
4242
]
43+
44+
[dependency-groups]
45+
dev = [
46+
"bandit>=1.9.4",
47+
]

0 commit comments

Comments
 (0)