bharatgraph / api /routes /export.py
abinazebinoy's picture
fix(CodeQL#16-17): use validated real_out path for FileResponse and media_type check in export.py
673a953
Raw
History Blame Contribute Delete
3.56 kB
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse
from loguru import logger
from api.dependencies import get_db, get_driver
from ai.dossier_generator import DossierGenerator
from ai.report_hasher import ReportHasher
from ai.multi_investigator import MultiInvestigator
router = APIRouter()
_hasher = None
def get_generator() -> DossierGenerator:
"""BUG-08 FIX: was caching generator with stale driver.
Now creates a fresh generator per request -- driver is always fresh."""
return DossierGenerator(driver=get_driver())
def get_hasher() -> ReportHasher:
global _hasher
if _hasher is None:
_hasher = ReportHasher()
return _hasher
@router.get("/export/pdf/{entity_id}")
def export_pdf(entity_id: str, driver=Depends(get_db)):
logger.info(f"[Export] PDF requested for: {entity_id}")
investigator = MultiInvestigator(driver=driver)
with driver.session() as session:
row = session.run(
"MATCH (n {id: $id}) RETURN n.name AS name",
id=entity_id
).single()
if not row:
raise HTTPException(status_code=404,
detail=f"Entity {entity_id} not found")
entity_name = row["name"] or entity_id
multi_report = investigator.investigate(entity_id, entity_name)
generator = DossierGenerator(driver=driver)
result = generator.generate(
entity_id,
multi_report=multi_report,
output_dir="data/processed",
)
output_path = result.get("pdf_path") or result.get("html_path")
if not output_path or not os.path.exists(output_path):
raise HTTPException(status_code=500,
detail="Dossier generation failed")
# H-07 / CodeQL fix: use file-anchored absolute path
_routes_dir = os.path.dirname(os.path.abspath(__file__))
_project_root = os.path.dirname(os.path.dirname(_routes_dir))
allowed_dir = os.path.realpath(os.path.join(_project_root, "data", "processed"))
real_out = os.path.realpath(output_path)
if not real_out.startswith(allowed_dir + os.sep):
raise HTTPException(status_code=400, detail="Invalid output path")
media_type = (
"application/pdf"
if real_out.endswith(".pdf")
else "text/html"
)
# BUG-17 FIX: wire blockchain audit trail to dossier export.
# AuditChain was fully implemented but never called anywhere.
# README claimed "SHA-256 blockchain audit trail" -- now true.
try:
from blockchain.audit_chain import store_daily_root
from api.dependencies import get_driver as _get_driver
_drv = _get_driver()
if _drv:
store_daily_root(_drv)
except Exception as _bc_err:
logger.debug(f"[Export] Blockchain audit step (optional): {_bc_err}")
# CodeQL #16-17 FIX: use validated real_out path for all file operations
return FileResponse(
real_out,
media_type=media_type,
filename=os.path.basename(real_out),
headers={"X-Report-Hash": result["report_hash"]},
)
@router.get("/verify/{report_hash}")
def verify_report(report_hash: str, driver=Depends(get_db)):
hasher = get_hasher()
result = hasher.lookup_hash(report_hash, driver)
return {
**result,
"hash": report_hash,
"verified_at": datetime.now().isoformat(),
}