Spaces:
Sleeping
Sleeping
| """ | |
| pdf.py β PDF Report Generator | |
| ================================ | |
| Builds a clean, branded PDF from a scan result dict. | |
| Uses ReportLab Platypus β no external services, no cloud storage. | |
| Called from app.py's GET /report/{id}/pdf endpoint. | |
| """ | |
| import logging | |
| from datetime import datetime, timezone | |
| from reportlab.platypus import ( | |
| SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable | |
| ) | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.pagesizes import letter | |
| from reportlab.lib import colors | |
| logger = logging.getLogger("secretscan.pdf") | |
| # Brand colours | |
| _DARK = colors.HexColor("#0f172a") | |
| _ACCENT = colors.HexColor("#5b7bfe") | |
| _HIGH = colors.HexColor("#f43f5e") | |
| _MEDIUM = colors.HexColor("#fb923c") | |
| _LOW = colors.HexColor("#facc15") | |
| _NONE = colors.HexColor("#22c55e") | |
| _MUTED = colors.HexColor("#64748b") | |
| _BG_LIGHT = colors.HexColor("#f8fafc") | |
| _SEV_COLOR = {"HIGH": _HIGH, "MEDIUM": _MEDIUM, "LOW": _LOW, "NONE": _NONE} | |
| def generate_pdf(scan_id: str, result: dict, out_path: str) -> None: | |
| """ | |
| Write a PDF report to out_path. | |
| Args: | |
| scan_id: UUID of the scan (shown in header). | |
| result: The full build_result() dict from scanner.py. | |
| out_path: Filesystem path to write the .pdf file. | |
| Raises: | |
| Exception: propagates ReportLab errors to the caller. | |
| """ | |
| styles = getSampleStyleSheet() | |
| doc = SimpleDocTemplate( | |
| out_path, | |
| pagesize = letter, | |
| topMargin = 36, | |
| bottomMargin = 36, | |
| leftMargin = 48, | |
| rightMargin = 48, | |
| ) | |
| findings = result.get("findings", []) | |
| summary = result.get("summary", {}) | |
| risk = result.get("risk_level", "NONE") | |
| total = result.get("total_secrets", 0) | |
| source = result.get("source", "") | |
| truncated= result.get("truncated", False) | |
| story: list = [] | |
| # ββ Header ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| story.append(Paragraph( | |
| "π SecretScan Security Report", | |
| ParagraphStyle("Title", parent=styles["Title"], | |
| fontSize=22, textColor=_DARK, spaceAfter=2), | |
| )) | |
| story.append(Paragraph( | |
| f"Scan ID: {scan_id[:8]}β¦ Β· " | |
| f"Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}", | |
| ParagraphStyle("Sub", parent=styles["Normal"], | |
| fontSize=9, textColor=_MUTED, spaceAfter=10), | |
| )) | |
| story.append(HRFlowable(width="100%", thickness=1, color=_ACCENT, spaceAfter=12)) | |
| # ββ Risk banner βββββββββββββββββββββββββββββββββββββββββββ | |
| risk_color = _SEV_COLOR.get(risk, _NONE) | |
| story.append(Paragraph( | |
| f"Overall Risk Level: <b>{risk}</b>", | |
| ParagraphStyle("Risk", parent=styles["Normal"], | |
| fontSize=16, textColor=risk_color, spaceAfter=6), | |
| )) | |
| # ββ Summary table βββββββββββββββββββββββββββββββββββββββββ | |
| table_data = [ | |
| ["Total Secrets", "HIGH", "MEDIUM", "LOW"], | |
| [ | |
| str(total), | |
| str(summary.get("high", 0)), | |
| str(summary.get("medium", 0)), | |
| str(summary.get("low", 0)), | |
| ], | |
| ] | |
| tbl = Table(table_data, colWidths=[120, 80, 80, 80]) | |
| tbl.setStyle(TableStyle([ | |
| ("BACKGROUND", (0, 0), (-1, 0), _DARK), | |
| ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), | |
| ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), | |
| ("FONTSIZE", (0, 0), (-1, -1), 10), | |
| ("ALIGN", (0, 0), (-1, -1), "CENTER"), | |
| ("ROWBACKGROUNDS", (0, 1), (-1, -1), [_BG_LIGHT, colors.white]), | |
| ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#e2e8f0")), | |
| ("TOPPADDING", (0, 0), (-1, -1), 6), | |
| ("BOTTOMPADDING", (0, 0), (-1, -1), 6), | |
| # Colour the HIGH/MEDIUM/LOW header cells | |
| ("TEXTCOLOR", (1, 0), (1, 0), _HIGH), | |
| ("TEXTCOLOR", (2, 0), (2, 0), _MEDIUM), | |
| ("TEXTCOLOR", (3, 0), (3, 0), _LOW), | |
| ])) | |
| story.append(tbl) | |
| story.append(Spacer(1, 14)) | |
| # ββ Source ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if source: | |
| story.append(Paragraph( | |
| f"<b>Source:</b> {source}", | |
| ParagraphStyle("Src", parent=styles["Normal"], | |
| fontSize=9, textColor=_MUTED, spaceAfter=14), | |
| )) | |
| # ββ Free-tier truncation notice βββββββββββββββββββββββββββ | |
| if truncated: | |
| story.append(Paragraph( | |
| "β This report shows a partial view. " | |
| "Upgrade to SecretScan Pro to see all findings and download the full PDF.", | |
| ParagraphStyle("Warn", parent=styles["Normal"], | |
| fontSize=10, textColor=_MEDIUM, | |
| backColor=colors.HexColor("#fff7ed"), | |
| borderPadding=8, spaceAfter=14), | |
| )) | |
| # ββ Findings ββββββββββββββββββββββββββββββββββββββββββββββ | |
| story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#e2e8f0"), | |
| spaceAfter=8)) | |
| if not findings: | |
| story.append(Paragraph( | |
| "β No secrets detected in this scan.", | |
| ParagraphStyle("Good", parent=styles["Normal"], | |
| fontSize=12, textColor=_NONE), | |
| )) | |
| else: | |
| story.append(Paragraph( | |
| f"Findings ({len(findings)} shown)", | |
| ParagraphStyle("H2", parent=styles["Heading2"], | |
| fontSize=13, textColor=_DARK, spaceAfter=8), | |
| )) | |
| for idx, f in enumerate(findings, 1): | |
| sev = f.get("severity", "LOW") | |
| color = _SEV_COLOR.get(sev, _LOW) | |
| # Finding header: index + type + severity badge | |
| story.append(Paragraph( | |
| f"{idx}. <b>{f.get('type', 'Unknown')}</b>" | |
| f" <font color='#{color.hexval()[2:]}' size='9'>[{sev}]</font>", | |
| ParagraphStyle(f"FH{idx}", parent=styles["Normal"], | |
| fontSize=11, textColor=_DARK, spaceAfter=2), | |
| )) | |
| # File + line | |
| story.append(Paragraph( | |
| f"<font color='#64748b' size='9'>" | |
| f"π {f.get('file', '?')} Β· Line {f.get('line', '?')}" | |
| f"</font>", | |
| styles["Normal"], | |
| )) | |
| # Redacted match | |
| if f.get("match"): | |
| story.append(Paragraph( | |
| f"<font color='#64748b' size='8'><i>Match: {f['match']}</i></font>", | |
| styles["Normal"], | |
| )) | |
| # Description | |
| story.append(Paragraph( | |
| f.get("description", ""), | |
| ParagraphStyle(f"FD{idx}", parent=styles["Normal"], | |
| fontSize=9, textColor=_MUTED, spaceAfter=2), | |
| )) | |
| # Fix recommendation | |
| story.append(Paragraph( | |
| f"<b>Fix:</b> {f.get('fix', '')}", | |
| ParagraphStyle(f"FF{idx}", parent=styles["Normal"], | |
| fontSize=9, textColor=_DARK, spaceAfter=10), | |
| )) | |
| story.append(HRFlowable( | |
| width="100%", thickness=0.3, | |
| color=colors.HexColor("#e2e8f0"), spaceAfter=8, | |
| )) | |
| # ββ Footer ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| story.append(Spacer(1, 20)) | |
| story.append(Paragraph( | |
| "Generated by SecretScan β secretscan.io", | |
| ParagraphStyle("Footer", parent=styles["Normal"], | |
| fontSize=8, textColor=_MUTED, alignment=1), | |
| )) | |
| doc.build(story) | |
| logger.info(f"PDF generated: {out_path} ({len(findings)} findings)") | |