- FastAPI backend: auth, findings, dashboard, attack paths, footprint, AI translator, risk calculator, PDF report generator - Next.js frontend: Vault dashboard, login, findings table, finding detail with AI coach, digital footprint, reports - PostgreSQL data model: tenants, users, assets, findings, risk scores, audit reports, attack paths - Docker Compose + Dockerfiles for all services - Demo seed data: Acme Corp with 6 findings and 90-day risk score history - AI Risk Translator (OpenAI/Anthropic) with plain-English business impact - Role-based access: executive / it_admin / trustos_admin - Scope-lock engine: authorization required before any assessment Stage 1-8 complete: Phase 1 Vault Audit product ready
144 lines
4.8 KiB
Python
144 lines
4.8 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func, desc
|
|
from datetime import datetime, timedelta
|
|
from typing import List
|
|
|
|
from app.db.session import get_db
|
|
from app.models.models import Finding, RiskScore, AuditReport, FindingStatus
|
|
from app.schemas.schemas import DashboardResponse, RiskCardData
|
|
from app.core.security import require_executive_or_above
|
|
|
|
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
|
|
|
|
|
@router.get("/{tenant_id}", response_model=DashboardResponse)
|
|
async def get_dashboard(
|
|
tenant_id: str,
|
|
payload: dict = Depends(require_executive_or_above),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
# Enforce tenant isolation
|
|
if payload.get("role") != "trustos_admin" and payload.get("tenant_id") != tenant_id:
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
|
|
# Current score (most recent)
|
|
score_result = await db.execute(
|
|
select(RiskScore)
|
|
.where(RiskScore.tenant_id == tenant_id)
|
|
.order_by(desc(RiskScore.score_date))
|
|
.limit(1)
|
|
)
|
|
current_score_obj = score_result.scalar_one_or_none()
|
|
current_score = current_score_obj.overall_score if current_score_obj else 50.0
|
|
|
|
# Previous score (30 days ago) for delta
|
|
prev_result = await db.execute(
|
|
select(RiskScore)
|
|
.where(
|
|
RiskScore.tenant_id == tenant_id,
|
|
RiskScore.score_date <= datetime.utcnow() - timedelta(days=30)
|
|
)
|
|
.order_by(desc(RiskScore.score_date))
|
|
.limit(1)
|
|
)
|
|
prev_score_obj = prev_result.scalar_one_or_none()
|
|
prev_score = prev_score_obj.overall_score if prev_score_obj else None
|
|
delta = round(current_score - prev_score, 1) if prev_score else None
|
|
|
|
# 90-day trend
|
|
trend_result = await db.execute(
|
|
select(RiskScore)
|
|
.where(
|
|
RiskScore.tenant_id == tenant_id,
|
|
RiskScore.score_date >= datetime.utcnow() - timedelta(days=90)
|
|
)
|
|
.order_by(RiskScore.score_date)
|
|
)
|
|
trend_scores = trend_result.scalars().all()
|
|
score_trend = [
|
|
{"date": rs.score_date.strftime("%Y-%m-%d"), "score": rs.overall_score}
|
|
for rs in trend_scores
|
|
]
|
|
|
|
# Top 3 risks
|
|
top_result = await db.execute(
|
|
select(Finding)
|
|
.where(
|
|
Finding.tenant_id == tenant_id,
|
|
Finding.is_top_risk == True,
|
|
Finding.status.in_([FindingStatus.open, FindingStatus.in_progress])
|
|
)
|
|
.order_by(Finding.created_at)
|
|
.limit(3)
|
|
)
|
|
top_findings = top_result.scalars().all()
|
|
|
|
# If no manually-flagged top risks, fall back to open criticals
|
|
if not top_findings:
|
|
fallback = await db.execute(
|
|
select(Finding)
|
|
.where(
|
|
Finding.tenant_id == tenant_id,
|
|
Finding.status.in_([FindingStatus.open, FindingStatus.in_progress])
|
|
)
|
|
.order_by(Finding.created_at)
|
|
.limit(3)
|
|
)
|
|
top_findings = fallback.scalars().all()
|
|
|
|
top_risks = [
|
|
RiskCardData(
|
|
id=f.id,
|
|
title=f.title,
|
|
ai_summary=f.ai_summary,
|
|
ai_business_impact=f.ai_business_impact,
|
|
ai_impact_level=f.ai_impact_level,
|
|
ai_fix_priority=f.ai_fix_priority,
|
|
severity=f.severity.value,
|
|
category=f.category.value,
|
|
)
|
|
for f in top_findings
|
|
]
|
|
|
|
# Open finding counts
|
|
counts_result = await db.execute(
|
|
select(Finding.severity, func.count(Finding.id))
|
|
.where(
|
|
Finding.tenant_id == tenant_id,
|
|
Finding.status.in_([FindingStatus.open, FindingStatus.in_progress])
|
|
)
|
|
.group_by(Finding.severity)
|
|
)
|
|
counts = {row[0].value: row[1] for row in counts_result}
|
|
total_open = sum(counts.values())
|
|
|
|
# Baseline
|
|
baseline_result = await db.execute(
|
|
select(AuditReport)
|
|
.where(AuditReport.tenant_id == tenant_id, AuditReport.is_baseline == True)
|
|
.order_by(AuditReport.report_date)
|
|
.limit(1)
|
|
)
|
|
baseline = baseline_result.scalar_one_or_none()
|
|
|
|
# Tenant name
|
|
from app.models.models import Tenant
|
|
t_result = await db.execute(select(Tenant).where(Tenant.id == tenant_id))
|
|
tenant = t_result.scalar_one_or_none()
|
|
|
|
return DashboardResponse(
|
|
tenant_name=tenant.name if tenant else "Unknown",
|
|
current_score=current_score,
|
|
previous_score=prev_score,
|
|
score_delta=delta,
|
|
score_trend=score_trend,
|
|
top_risks=top_risks,
|
|
open_critical=counts.get("critical", 0),
|
|
open_high=counts.get("high", 0),
|
|
open_medium=counts.get("medium", 0),
|
|
total_open=total_open,
|
|
baseline_score=baseline.baseline_score if baseline else None,
|
|
baseline_date=baseline.report_date if baseline else None,
|
|
)
|