feat(dashboard): Phase 13 — Executive Dashboard
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
124
backend/app/routers/executive_dashboard.py
Normal file
124
backend/app/routers/executive_dashboard.py
Normal 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)
|
||||
Reference in New Issue
Block a user