● Zion Boggan
repos/Sightline/backend/app/api/routes/evidence.py
zionboggan.com ↗
97 lines · python
History for this file →
1
from __future__ import annotations
2
 
3
import hashlib
4
 
5
from fastapi import APIRouter, Depends, File, Form, HTTPException, Response, UploadFile
6
from sqlalchemy import select
7
from sqlalchemy.orm import Session
8
 
9
from app.api.deps import Perm, get_db, require
10
from app.core.config import settings
11
from app.core.storage import get_storage
12
from app.models import AuditLog, EvidenceFile, Investigation, Source, User
13
 
14
router = APIRouter(tags=["evidence"])
15
 
16
 
17
@router.post("/investigations/{inv_id}/evidence", status_code=201)
18
async def upload_evidence(
19
    inv_id: str,
20
    file: UploadFile = File(...),
21
    description: str | None = Form(default=None),
22
    source_id: str | None = Form(default=None),
23
    db: Session = Depends(get_db),
24
    user: User = Depends(require(Perm.WRITE)),
25
):
26
    if not db.get(Investigation, inv_id):
27
        raise HTTPException(404, "investigation not found")
28
    data = await file.read()
29
    if not data:
30
        raise HTTPException(400, "empty file")
31
    if len(data) > settings.max_upload_bytes:
32
        raise HTTPException(413, f"file exceeds {settings.max_upload_bytes} bytes")
33
    sha = hashlib.sha256(data).hexdigest()
34
    if source_id and not db.get(Source, source_id):
35
        raise HTTPException(400, "unknown source_id")
36
 
37
    blob_ref = get_storage().put(f"evidence/{inv_id}/{sha[:16]}_{file.filename}", data)
38
    ev = EvidenceFile(investigation_id=inv_id, filename=file.filename,
39
                      content_type=file.content_type, size_bytes=len(data), sha256=sha,
40
                      blob_ref=blob_ref, source_id=source_id, description=description,
41
                      uploaded_by=user.id)
42
    db.add(ev)
43
    db.flush()
44
    db.add(AuditLog(actor_user_id=user.id, action="evidence.upload",
45
                    entity_type="evidence_file", entity_id=ev.id,
46
                    meta={"investigation_id": inv_id, "filename": file.filename, "sha256": sha}))
47
    db.commit()
48
    db.refresh(ev)
49
    return {"id": ev.id, "filename": ev.filename, "sha256": ev.sha256, "size_bytes": ev.size_bytes}
50
 
51
 
52
@router.get("/investigations/{inv_id}/evidence")
53
def list_evidence(inv_id: str, db: Session = Depends(get_db), _: User = Depends(require(Perm.READ))):
54
    rows = db.scalars(select(EvidenceFile).where(EvidenceFile.investigation_id == inv_id)
55
                      .order_by(EvidenceFile.created_at.desc())).all()
56
    return [{"id": e.id, "filename": e.filename, "content_type": e.content_type,
57
             "size_bytes": e.size_bytes, "sha256": e.sha256, "source_id": e.source_id,
58
             "description": e.description, "uploaded_by": e.uploaded_by,
59
             "created_at": e.created_at} for e in rows]
60
 
61
 
62
@router.get("/evidence/{evidence_id}")
63
def get_evidence(evidence_id: str, db: Session = Depends(get_db), _: User = Depends(require(Perm.READ))):
64
    ev = db.get(EvidenceFile, evidence_id)
65
    if not ev:
66
        raise HTTPException(404, "evidence not found")
67
    trail = db.scalars(select(AuditLog).where(AuditLog.entity_type == "evidence_file",
68
                                              AuditLog.entity_id == evidence_id)
69
                       .order_by(AuditLog.created_at)).all()
70
    return {"id": ev.id, "investigation_id": ev.investigation_id, "filename": ev.filename,
71
            "content_type": ev.content_type, "size_bytes": ev.size_bytes, "sha256": ev.sha256,
72
            "source_id": ev.source_id, "description": ev.description, "uploaded_by": ev.uploaded_by,
73
            "created_at": ev.created_at,
74
            "chain_of_custody": [{"action": t.action, "actor": t.actor_user_id,
75
                                  "at": t.created_at, "metadata": t.meta} for t in trail]}
76
 
77
 
78
@router.get("/evidence/{evidence_id}/download")
79
def download_evidence(evidence_id: str, db: Session = Depends(get_db),
80
                      user: User = Depends(require(Perm.READ))):
81
    ev = db.get(EvidenceFile, evidence_id)
82
    if not ev:
83
        raise HTTPException(404, "evidence not found")
84
    try:
85
        data = get_storage().get(ev.blob_ref)
86
    except Exception:
87
        raise HTTPException(410, "evidence blob unavailable")
88
    actual = hashlib.sha256(data).hexdigest()
89
    integrity_ok = actual == ev.sha256
90
    db.add(AuditLog(actor_user_id=user.id, action="evidence.download",
91
                    entity_type="evidence_file", entity_id=ev.id,
92
                    meta={"integrity_ok": integrity_ok}))
93
    db.commit()
94
    return Response(content=data, media_type=ev.content_type or "application/octet-stream",
95
                    headers={"Content-Disposition": f'attachment; filename="{ev.filename}"',
96
                             "X-Content-SHA256": ev.sha256,
97
                             "X-Integrity-Verified": str(integrity_ok).lower()})