| """ |
| PDF Report Generator — Generates LUMINA Mitigation Reports as PDF. |
| |
| Uses fpdf2 (pure Python, no system dependencies) to produce |
| professional, branded PDF reports from mitigation report data. |
| """ |
|
|
| from fpdf import FPDF |
| from datetime import datetime |
| from typing import Dict, Any |
| import logging |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class LuminaPDF(FPDF): |
| """Custom PDF class with LUMINA branding.""" |
|
|
| def header(self): |
| self.set_font("Helvetica", "B", 22) |
| self.set_text_color(30, 41, 59) |
| self.cell(0, 15, "LUMINA", new_x="LMARGIN", new_y="NEXT") |
| self.set_font("Helvetica", "", 10) |
| self.set_text_color(100, 116, 139) |
| self.cell(0, 6, "Cyber Defense Platform | Mitigation Report", new_x="LMARGIN", new_y="NEXT") |
| self.set_draw_color(59, 130, 246) |
| self.set_line_width(0.5) |
| self.line(10, self.get_y() + 3, 200, self.get_y() + 3) |
| self.ln(10) |
|
|
| def footer(self): |
| self.set_y(-20) |
| self.set_font("Helvetica", "I", 8) |
| self.set_text_color(148, 163, 184) |
| self.cell( |
| 0, 10, |
| f"Generated on {datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')} | Page {self.page_no()}/{{nb}} | CONFIDENTIAL", |
| align="C", |
| ) |
|
|
| def section_title(self, title: str): |
| self.ln(4) |
| self.set_font("Helvetica", "B", 13) |
| self.set_text_color(30, 41, 59) |
| self.cell(0, 10, title, new_x="LMARGIN", new_y="NEXT") |
| self.set_draw_color(226, 232, 240) |
| self.line(10, self.get_y(), 200, self.get_y()) |
| self.ln(4) |
|
|
| def body_text(self, text: str): |
| self.set_font("Helvetica", "", 10) |
| self.set_text_color(51, 65, 85) |
| self.multi_cell(0, 6, text) |
| self.ln(3) |
|
|
|
|
| def generate_mitigation_pdf(data: Dict[str, Any]) -> bytes: |
| """Generate a PDF report from mitigation report data. Returns raw PDF bytes.""" |
| pdf = LuminaPDF() |
| pdf.alias_nb_pages() |
| pdf.add_page() |
| pdf.set_auto_page_break(auto=True, margin=25) |
|
|
| |
| risk_colors = { |
| "CRITICAL": (239, 68, 68), |
| "HIGH": (249, 115, 22), |
| "MODERATE": (245, 158, 11), |
| "LOW": (34, 197, 94), |
| } |
|
|
| |
| risk_score = data.get("overall_risk_score", 0) |
| risk_level = data.get("overall_risk_level", "UNKNOWN") |
| color = risk_colors.get(risk_level, (100, 116, 139)) |
|
|
| pdf.set_font("Helvetica", "B", 16) |
| pdf.set_text_color(*color) |
| pdf.cell(0, 12, f"Overall Risk: {risk_score}/100 [{risk_level}]", new_x="LMARGIN", new_y="NEXT") |
| pdf.ln(2) |
|
|
| generated = data.get("generated_at", datetime.utcnow().isoformat()) |
| pdf.set_font("Helvetica", "", 9) |
| pdf.set_text_color(148, 163, 184) |
| pdf.cell(0, 6, f"Report generated: {generated}", new_x="LMARGIN", new_y="NEXT") |
| pdf.ln(4) |
|
|
| |
| pdf.section_title("Executive Summary") |
| pdf.body_text(data.get("executive_summary", "No summary available.")) |
|
|
| |
| pdf.section_title("Threat Breakdown by Category") |
| pdf.set_font("Helvetica", "B", 9) |
| pdf.set_fill_color(241, 245, 249) |
| pdf.set_text_color(51, 65, 85) |
|
|
| col_widths = [35, 28, 28, 30, 28, 35] |
| headers = ["Category", "Total", "High Risk", "Medium Risk", "Low Risk", "Trend"] |
| for i, h in enumerate(headers): |
| pdf.cell(col_widths[i], 8, h, border=1, fill=True, align="C") |
| pdf.ln() |
|
|
| pdf.set_font("Helvetica", "", 9) |
| for row in data.get("threat_breakdown", []): |
| pdf.cell(col_widths[0], 8, str(row.get("category", "")).title(), border=1) |
| pdf.cell(col_widths[1], 8, str(row.get("total_incidents", 0)), border=1, align="C") |
| pdf.cell(col_widths[2], 8, str(row.get("high_risk_count", 0)), border=1, align="C") |
| pdf.cell(col_widths[3], 8, str(row.get("medium_risk_count", 0)), border=1, align="C") |
| pdf.cell(col_widths[4], 8, str(row.get("low_risk_count", 0)), border=1, align="C") |
| trend = str(row.get("trend", "stable")).title() |
| pdf.cell(col_widths[5], 8, trend, border=1, align="C") |
| pdf.ln() |
| pdf.ln(4) |
|
|
| |
| pdf.section_title("Mitigation Recommendations") |
| priority_colors = { |
| "critical": (239, 68, 68), |
| "high": (249, 115, 22), |
| "medium": (245, 158, 11), |
| "low": (34, 197, 94), |
| } |
|
|
| for i, rec in enumerate(data.get("recommendations", []), 1): |
| priority = rec.get("priority", "medium") |
| p_color = priority_colors.get(priority, (100, 116, 139)) |
|
|
| pdf.set_font("Helvetica", "B", 10) |
| pdf.set_text_color(*p_color) |
| pdf.cell(0, 8, f"{i}. [{priority.upper()}] {rec.get('title', '')}", new_x="LMARGIN", new_y="NEXT") |
|
|
| pdf.set_font("Helvetica", "", 9) |
| pdf.set_text_color(71, 85, 105) |
| category = rec.get("category", "") |
| desc = rec.get("description", "") |
| pdf.multi_cell(0, 5, f" Category: {category} | {desc}") |
| pdf.ln(3) |
|
|
| |
| pdf.section_title("Risk Trend Analysis") |
| pdf.body_text(data.get("risk_trend_analysis", "Insufficient data for trend analysis.")) |
|
|
| return pdf.output() |
|
|