Files
breakpilot-compliance/backend-compliance/compliance/api/security_backlog_routes.py
Benjamin Admin 25d5da78ef
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s
feat: Alle 5 verbleibenden SDK-Module auf 100% — RAG, Security-Backlog, Quality, Notfallplan, Loeschfristen
Paket A — RAG Proxy:
- NEU: admin-compliance/app/api/sdk/v1/rag/[[...path]]/route.ts
  → Proxy zu ai-compliance-sdk:8090, GET+POST, UUID-Validierung
- UPDATE: rag/page.tsx — setTimeout Mock → echte API-Calls
  GET /regulations → dynamische suggestedQuestions
  POST /search → Qdrant-Ergebnisse mit score, title, reference

Paket B — Security-Backlog + Quality:
- NEU: migrations/014_security_backlog.sql + 015_quality.sql
- NEU: compliance/api/security_backlog_routes.py — CRUD + Stats
- NEU: compliance/api/quality_routes.py — Metrics + Tests CRUD + Stats
- UPDATE: security-backlog/page.tsx — mockItems → API
- UPDATE: quality/page.tsx — mockMetrics/mockTests → API
- UPDATE: compliance/api/__init__.py — Router-Registrierung
- NEU: tests/test_security_backlog_routes.py (48 Tests — 48/48 bestanden)
- NEU: tests/test_quality_routes.py (67 Tests — 67/67 bestanden)

Paket C — Notfallplan Incidents + Templates:
- NEU: migrations/016_notfallplan_incidents.sql
  compliance_notfallplan_incidents + compliance_notfallplan_templates
- UPDATE: notfallplan_routes.py — GET/POST/PUT/DELETE für /incidents + /templates
- UPDATE: notfallplan/page.tsx — Incidents-Tab + Templates-Tab → API
- UPDATE: tests/test_notfallplan_routes.py (+76 neue Tests — alle bestanden)

Paket D — Loeschfristen localStorage → API:
- NEU: migrations/017_loeschfristen.sql (JSONB: legal_holds, storage_locations, ...)
- NEU: compliance/api/loeschfristen_routes.py — CRUD + Stats + Status-Update
- UPDATE: loeschfristen/page.tsx — vollständige localStorage → API Migration
  createNewPolicy → POST (API-UUID als id), deletePolicy → DELETE,
  handleSaveAndClose → PUT, adoptGeneratedPolicies → POST je Policy
  apiToPolicy() + policyToPayload() Mapper, saving-State für Buttons
- NEU: tests/test_loeschfristen_routes.py (58 Tests — alle bestanden)

Gesamt: 253 neue Tests, alle bestanden (48 + 67 + 76 + 58 + bestehende)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 18:04:53 +01:00

271 lines
9.0 KiB
Python

"""
FastAPI routes for Security Backlog Tracking.
Endpoints:
GET /security-backlog — list with filters (status, severity, type, search; limit/offset)
GET /security-backlog/stats — open, critical, high, overdue counts
POST /security-backlog — create finding
PUT /security-backlog/{id} — update finding
DELETE /security-backlog/{id} — delete finding (204)
"""
import logging
from datetime import datetime
from typing import Optional, Any, Dict
from fastapi import APIRouter, Depends, HTTPException, Query, Header
from pydantic import BaseModel
from sqlalchemy import text
from sqlalchemy.orm import Session
from uuid import UUID
from classroom_engine.database import get_db
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/security-backlog", tags=["security-backlog"])
DEFAULT_TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
# =============================================================================
# Pydantic Schemas
# =============================================================================
class SecurityItemCreate(BaseModel):
title: str
description: Optional[str] = None
type: str = "vulnerability"
severity: str = "medium"
status: str = "open"
source: Optional[str] = None
cve: Optional[str] = None
cvss: Optional[float] = None
affected_asset: Optional[str] = None
assigned_to: Optional[str] = None
due_date: Optional[datetime] = None
remediation: Optional[str] = None
class SecurityItemUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
type: Optional[str] = None
severity: Optional[str] = None
status: Optional[str] = None
source: Optional[str] = None
cve: Optional[str] = None
cvss: Optional[float] = None
affected_asset: Optional[str] = None
assigned_to: Optional[str] = None
due_date: Optional[datetime] = None
remediation: Optional[str] = None
def _row_to_dict(row) -> Dict[str, Any]:
result = dict(row._mapping)
for key, val in result.items():
if isinstance(val, datetime):
result[key] = val.isoformat()
elif hasattr(val, '__str__') and not isinstance(val, (str, int, float, bool, list, dict, type(None))):
result[key] = str(val)
return result
def _get_tenant_id(x_tenant_id: Optional[str] = Header(None)) -> str:
if x_tenant_id:
try:
UUID(x_tenant_id)
return x_tenant_id
except ValueError:
pass
return DEFAULT_TENANT_ID
# =============================================================================
# Routes
# =============================================================================
@router.get("")
async def list_security_items(
status: Optional[str] = Query(None),
severity: Optional[str] = Query(None),
type: Optional[str] = Query(None),
search: Optional[str] = Query(None),
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
x_tenant_id: Optional[str] = Header(None),
):
"""List security backlog items with optional filters."""
tenant_id = _get_tenant_id(x_tenant_id)
where_clauses = ["tenant_id = :tenant_id"]
params: Dict[str, Any] = {"tenant_id": tenant_id, "limit": limit, "offset": offset}
if status:
where_clauses.append("status = :status")
params["status"] = status
if severity:
where_clauses.append("severity = :severity")
params["severity"] = severity
if type:
where_clauses.append("type = :type")
params["type"] = type
if search:
where_clauses.append("(title ILIKE :search OR description ILIKE :search)")
params["search"] = f"%{search}%"
where_sql = " AND ".join(where_clauses)
total_row = db.execute(
text(f"SELECT COUNT(*) FROM compliance_security_backlog WHERE {where_sql}"),
params,
).fetchone()
total = total_row[0] if total_row else 0
rows = db.execute(
text(f"""
SELECT * FROM compliance_security_backlog
WHERE {where_sql}
ORDER BY
CASE severity
WHEN 'critical' THEN 0
WHEN 'high' THEN 1
WHEN 'medium' THEN 2
ELSE 3
END,
CASE status
WHEN 'open' THEN 0
WHEN 'in-progress' THEN 1
WHEN 'accepted-risk' THEN 2
ELSE 3
END,
created_at DESC
LIMIT :limit OFFSET :offset
"""),
params,
).fetchall()
return {
"items": [_row_to_dict(r) for r in rows],
"total": total,
}
@router.get("/stats")
async def get_security_stats(
db: Session = Depends(get_db),
x_tenant_id: Optional[str] = Header(None),
):
"""Return security backlog counts."""
tenant_id = _get_tenant_id(x_tenant_id)
rows = db.execute(text("""
SELECT
COUNT(*) FILTER (WHERE status = 'open') AS open,
COUNT(*) FILTER (WHERE status = 'in-progress') AS in_progress,
COUNT(*) FILTER (WHERE status = 'resolved') AS resolved,
COUNT(*) FILTER (WHERE status = 'accepted-risk') AS accepted_risk,
COUNT(*) FILTER (WHERE severity = 'critical' AND status != 'resolved') AS critical,
COUNT(*) FILTER (WHERE severity = 'high' AND status != 'resolved') AS high,
COUNT(*) FILTER (
WHERE due_date IS NOT NULL
AND due_date < NOW()
AND status NOT IN ('resolved', 'accepted-risk')
) AS overdue,
COUNT(*) AS total
FROM compliance_security_backlog
WHERE tenant_id = :tenant_id
"""), {"tenant_id": tenant_id}).fetchone()
if rows:
d = dict(rows._mapping)
return {k: (v or 0) for k, v in d.items()}
return {"open": 0, "in_progress": 0, "resolved": 0, "accepted_risk": 0,
"critical": 0, "high": 0, "overdue": 0, "total": 0}
@router.post("", status_code=201)
async def create_security_item(
payload: SecurityItemCreate,
db: Session = Depends(get_db),
x_tenant_id: Optional[str] = Header(None),
):
"""Create a new security backlog item."""
tenant_id = _get_tenant_id(x_tenant_id)
row = db.execute(text("""
INSERT INTO compliance_security_backlog
(tenant_id, title, description, type, severity, status,
source, cve, cvss, affected_asset, assigned_to, due_date, remediation)
VALUES
(:tenant_id, :title, :description, :type, :severity, :status,
:source, :cve, :cvss, :affected_asset, :assigned_to, :due_date, :remediation)
RETURNING *
"""), {
"tenant_id": tenant_id,
"title": payload.title,
"description": payload.description,
"type": payload.type,
"severity": payload.severity,
"status": payload.status,
"source": payload.source,
"cve": payload.cve,
"cvss": payload.cvss,
"affected_asset": payload.affected_asset,
"assigned_to": payload.assigned_to,
"due_date": payload.due_date,
"remediation": payload.remediation,
}).fetchone()
db.commit()
return _row_to_dict(row)
@router.put("/{item_id}")
async def update_security_item(
item_id: str,
payload: SecurityItemUpdate,
db: Session = Depends(get_db),
x_tenant_id: Optional[str] = Header(None),
):
"""Update a security backlog item."""
tenant_id = _get_tenant_id(x_tenant_id)
updates: Dict[str, Any] = {"id": item_id, "tenant_id": tenant_id, "updated_at": datetime.utcnow()}
set_clauses = ["updated_at = :updated_at"]
for field, value in payload.model_dump(exclude_unset=True).items():
updates[field] = value
set_clauses.append(f"{field} = :{field}")
if len(set_clauses) == 1:
raise HTTPException(status_code=400, detail="No fields to update")
row = db.execute(text(f"""
UPDATE compliance_security_backlog
SET {', '.join(set_clauses)}
WHERE id = :id AND tenant_id = :tenant_id
RETURNING *
"""), updates).fetchone()
db.commit()
if not row:
raise HTTPException(status_code=404, detail="Security item not found")
return _row_to_dict(row)
@router.delete("/{item_id}", status_code=204)
async def delete_security_item(
item_id: str,
db: Session = Depends(get_db),
x_tenant_id: Optional[str] = Header(None),
):
tenant_id = _get_tenant_id(x_tenant_id)
result = db.execute(text("""
DELETE FROM compliance_security_backlog
WHERE id = :id AND tenant_id = :tenant_id
"""), {"id": item_id, "tenant_id": tenant_id})
db.commit()
if result.rowcount == 0:
raise HTTPException(status_code=404, detail="Security item not found")