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:
drjones
2026-07-05 09:46:08 +00:00
commit 463dff883b
64 changed files with 11679 additions and 0 deletions

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