feat(dashboard): Phase 13 — Executive Dashboard
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

PostureSnapshot model, Alembic migration (b039exec), schemas, service
aggregating all phases (coverage/risk/operations/knowledge/MTTD), and
router at /api/v1/dashboard with executive view, KPIs, coverage-by-tactic,
posture-history, posture-snapshot, and activity-feed endpoints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-20 16:20:21 +02:00
parent 41a0c536bb
commit ab591d30c4
8 changed files with 997 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
"""Phase 13: Executive Dashboard router."""
from typing import List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.database import get_db
from app.dependencies.auth import get_current_user, require_any_role
from app.schemas.executive_dashboard_schema import (
PostureSnapshotOut,
ExecutiveSummary,
KpiBlock,
CoverageByTactic,
PostureHistoryEntry,
ActivityEntry,
)
import app.services.executive_dashboard_service as svc
router = APIRouter(prefix="/dashboard", tags=["Executive Dashboard"])
@router.get("/executive", response_model=ExecutiveSummary)
def executive_view(
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""
Full executive view — snapshot, 30-day trends, top risks,
coverage by tactic, and recent activity feed.
"""
data = svc.get_executive_summary(db)
snap = data["snapshot"]
return ExecutiveSummary(
snapshot=PostureSnapshotOut.model_validate(snap),
coverage_trend=data["coverage_trend"],
risk_trend=data["risk_trend"],
top_risks=data["top_risks"],
coverage_by_tactic=data["coverage_by_tactic"],
recent_activity=data["recent_activity"],
)
@router.get("/kpis", response_model=KpiBlock)
def kpis(
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""Compact KPI block — live aggregation without persisting a snapshot."""
live = svc.get_live_kpis(db)
# Try to find today's snapshot id; fall back to None
from datetime import date
from app.models.executive_dashboard import PostureSnapshot
today_snap = db.query(PostureSnapshot).filter(
PostureSnapshot.snapshot_date == date.today()
).first()
return KpiBlock(
coverage_pct=live["coverage_pct"],
avg_risk_score=live["avg_risk_score"],
critical_count=live["critical_count"],
open_queue_items=live["open_queue_items"],
orphan_techniques=live["orphan_techniques"],
mttd_avg_seconds=live.get("mttd_avg_seconds"),
detection_rate_30d=live.get("detection_rate_30d"),
playbook_count=live["playbook_count"],
lesson_count=live["lesson_count"],
snapshot_date=live["snapshot_date"],
snapshot_id=today_snap.id if today_snap else None,
)
@router.get("/coverage-by-tactic", response_model=List[CoverageByTactic])
def coverage_by_tactic(
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""Per-tactic validated / partial / not_covered breakdown."""
return svc.get_coverage_by_tactic(db)
@router.get("/posture-history", response_model=List[PostureHistoryEntry])
def posture_history(
days: int = Query(30, ge=1, le=365),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""Historical posture snapshots for trend charts (default last 30 days)."""
snaps = svc.get_posture_history(db, days=days)
return [
PostureHistoryEntry(
snapshot_date=s.snapshot_date,
coverage_pct=s.coverage_pct,
avg_risk_score=s.avg_risk_score,
critical_count=s.critical_count,
open_queue_items=s.open_queue_items,
)
for s in snaps
]
@router.post("/posture-snapshot", response_model=PostureSnapshotOut, status_code=201)
def create_posture_snapshot(
db: Session = Depends(get_db),
user=Depends(require_any_role("admin", "red_lead", "blue_lead")),
):
"""
Take (or refresh) today's posture snapshot — admin / leads only.
Aggregates live data from all phases into a single PostureSnapshot row.
"""
snap = svc.take_posture_snapshot(db, created_by=user.id)
return PostureSnapshotOut.model_validate(snap)
@router.get("/activity", response_model=List[ActivityEntry])
def recent_activity(
limit: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""Recent activity feed — tests, attack-path executions, OSINT signals."""
return svc.get_recent_activity(db, limit=limit)