Files
trustos/backend/app/api/routes/dashboard.py
drjones 463dff883b feat: initial TrustOS platform scaffold
- 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
2026-07-05 09:46:08 +00:00

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,
)