11import hashlib
22import 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
93import sys
104from 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
1312def _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
2224SECURITY_DIR = _find_security_dir ()
25+
2326if 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
2731from security .evidence_ui import app as evidence_ui
2832
@@ -33,6 +37,7 @@ def _find_security_dir() -> Path | None:
3337from app .services .encryption import encrypt
3438from app .services .evidence_validator import validate_text
3539
40+
3641router = 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" )
5964async 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(
187216async 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 )
0 commit comments