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
This commit is contained in:
143
backend/app/api/routes/dashboard.py
Normal file
143
backend/app/api/routes/dashboard.py
Normal file
@@ -0,0 +1,143 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user