Files
trustos/backend/app/services/report_generator.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

140 lines
6.0 KiB
Python

"""
PDF report generator for Vault Audit Reports.
Uses Jinja2 + WeasyPrint to produce branded PDFs.
"""
import os
import json
import logging
from datetime import datetime
from pathlib import Path
from jinja2 import Environment, PackageLoader, select_autoescape, DictLoader
from app.db.session import AsyncSessionLocal
from app.models.models import AuditReport, Tenant, Finding, FindingStatus
from app.core.config import settings
from sqlalchemy import select, desc
logger = logging.getLogger(__name__)
REPORT_HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body { font-family: 'Segoe UI', sans-serif; color: #1f2328; background: #ffffff; margin: 40px; }
.header { border-bottom: 3px solid #1a1f2e; padding-bottom: 20px; margin-bottom: 30px; }
.logo { font-size: 28px; font-weight: 800; color: #1a1f2e; letter-spacing: -1px; }
.logo span { color: #3b82d4; }
.report-title { font-size: 22px; font-weight: 600; margin-top: 10px; }
.meta { color: #57606a; font-size: 13px; margin-top: 6px; }
.score-block { background: #1a1f2e; color: white; padding: 24px 30px; border-radius: 8px; margin: 24px 0; display: inline-block; min-width: 200px; }
.score-value { font-size: 52px; font-weight: 800; color: #3b82d4; line-height: 1; }
.score-label { font-size: 13px; color: #94a3b8; margin-top: 4px; }
h2 { font-size: 18px; font-weight: 700; color: #1a1f2e; border-left: 4px solid #3b82d4; padding-left: 12px; margin-top: 32px; }
.finding { border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px; margin: 12px 0; }
.finding.critical { border-left: 4px solid #dc2626; }
.finding.high { border-left: 4px solid #ea580c; }
.finding.medium { border-left: 4px solid #d97706; }
.finding.low { border-left: 4px solid #65a30d; }
.finding-title { font-weight: 600; font-size: 15px; }
.finding-summary { color: #374151; margin-top: 6px; font-size: 13px; }
.badge { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; margin-left: 8px; }
.badge.critical { background: #fee2e2; color: #991b1b; }
.badge.high { background: #ffedd5; color: #9a3412; }
.badge.medium { background: #fef3c7; color: #92400e; }
.badge.low { background: #dcfce7; color: #166534; }
.exec-summary { background: #f7f8fa; border-left: 3px solid #3b82d4; padding: 16px 20px; margin: 20px 0; font-size: 14px; line-height: 1.6; }
.footer { margin-top: 60px; padding-top: 16px; border-top: 1px solid #e5e7eb; font-size: 11px; color: #57606a; text-align: center; }
.confidential { background: #fef3c7; border: 1px solid #fcd34d; padding: 8px 16px; font-size: 12px; color: #78350f; border-radius: 4px; margin-bottom: 20px; }
</style>
</head>
<body>
<div class="confidential">⚠ CONFIDENTIAL — This report contains sensitive security information. Do not distribute without authorization.</div>
<div class="header">
<div class="logo">Trust<span>OS</span></div>
<div class="report-title">{{ report.title }}</div>
<div class="meta">Vault Audit Report · {{ tenant.name }} · Generated {{ report.report_date.strftime('%B %d, %Y') }}</div>
</div>
<div class="score-block">
<div class="score-value">{{ report.baseline_score | int }}</div>
<div class="score-label">Cyber Health Score at Audit Date<br><small>100 = Optimal · 0 = Critical Risk</small></div>
</div>
{% if report.executive_summary %}
<h2>Executive Summary</h2>
<div class="exec-summary">{{ report.executive_summary }}</div>
{% endif %}
{% if report.scope_description %}
<h2>Scope</h2>
<p style="font-size:14px; line-height:1.6;">{{ report.scope_description }}</p>
{% endif %}
<h2>Key Findings</h2>
{% for f in findings %}
<div class="finding {{ f.severity }}">
<div class="finding-title">{{ f.title }} <span class="badge {{ f.severity }}">{{ f.severity | upper }}</span></div>
{% if f.ai_summary %}
<div class="finding-summary">{{ f.ai_summary }}</div>
{% endif %}
{% if f.ai_business_impact %}
<div class="finding-summary" style="margin-top:8px; color:#6b7280;"><strong>Business Impact:</strong> {{ f.ai_business_impact }}</div>
{% endif %}
</div>
{% endfor %}
<div class="footer">
TrustOS · The AI Operating System for Cyber Resilience · trustos.com<br>
This report is a point-in-time assessment. Continuous monitoring is required to maintain current accuracy.
</div>
</body>
</html>
"""
async def generate_pdf_for_report(report_id: str):
"""Generate a branded PDF for a Vault Audit Report and store the path."""
async with AsyncSessionLocal() as db:
try:
r_result = await db.execute(select(AuditReport).where(AuditReport.id == report_id))
report = r_result.scalar_one_or_none()
if not report:
return
t_result = await db.execute(select(Tenant).where(Tenant.id == report.tenant_id))
tenant = t_result.scalar_one_or_none()
# Get findings snapshot
f_result = await db.execute(
select(Finding)
.where(
Finding.tenant_id == report.tenant_id,
Finding.status.in_([FindingStatus.open, FindingStatus.in_progress])
)
.order_by(Finding.created_at)
.limit(20)
)
findings = f_result.scalars().all()
# Render HTML
env = Environment(loader=DictLoader({"report.html": REPORT_HTML_TEMPLATE}))
template = env.get_template("report.html")
html = template.render(report=report, tenant=tenant, findings=findings)
# Write PDF
storage = Path(settings.STORAGE_PATH) / "reports"
storage.mkdir(parents=True, exist_ok=True)
pdf_path = storage / f"vault-audit-{report_id}.pdf"
from weasyprint import HTML as WH
WH(string=html).write_pdf(str(pdf_path))
report.pdf_path = str(pdf_path)
await db.commit()
logger.info(f"PDF generated: {pdf_path}")
except Exception as e:
logger.error(f"PDF generation failed for report {report_id}: {e}")