diff --git a/app/modules/prospecting/routes/api/admin_enrichment.py b/app/modules/prospecting/routes/api/admin_enrichment.py index d39421a1..fe3bdca4 100644 --- a/app/modules/prospecting/routes/api/admin_enrichment.py +++ b/app/modules/prospecting/routes/api/admin_enrichment.py @@ -10,6 +10,7 @@ catch "batch" as a string before trying to parse it as int → 422. import logging from fastapi import APIRouter, Depends, Path, Query +from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api @@ -34,6 +35,9 @@ from app.modules.prospecting.services.scoring_service import scoring_service from app.modules.prospecting.services.security_audit_service import ( security_audit_service, ) +from app.modules.prospecting.services.security_report_service import ( + security_report_service, +) from app.modules.prospecting.services.stats_service import stats_service from app.modules.tenancy.schemas.auth import UserContext @@ -152,6 +156,28 @@ def compute_scores_batch( return ScoreComputeBatchResponse(scored=count) +# ── Report endpoints ──────────────────────────────────────────────────────── + + +@router.get("/security-audit/report/{prospect_id}", response_class=HTMLResponse) +def security_audit_report( + prospect_id: int = Path(...), + db: Session = Depends(get_db), + current_admin: UserContext = Depends(get_current_admin_api), +): + """Generate branded HTML security audit report.""" + prospect = prospect_service.get_by_id(db, prospect_id) + if not prospect.security_audit: + from app.exceptions.base import ResourceNotFoundException + + raise ResourceNotFoundException("SecurityAudit", str(prospect_id)) + html = security_report_service.generate_html_report( + audit=prospect.security_audit, + domain=prospect.domain_name, + ) + return HTMLResponse(content=html) + + # ── Single-prospect endpoints ─────────────────────────────────────────────── diff --git a/app/modules/prospecting/services/security_report_service.py b/app/modules/prospecting/services/security_report_service.py new file mode 100644 index 00000000..c52ed408 --- /dev/null +++ b/app/modules/prospecting/services/security_report_service.py @@ -0,0 +1,241 @@ +# app/modules/prospecting/services/security_report_service.py +""" +Generate branded HTML security audit reports from stored audit data. + +Produces a standalone HTML document suitable for viewing in a browser, +printing to PDF, or emailing to prospects. Reports include: +- Security grade and score +- "What could happen" fear section with simulated hacked site +- Detailed findings grouped by category +- Business impact summary +- Call to action with contact info +""" + +import html as html_module +import json +import logging +from datetime import datetime + +from app.modules.prospecting.models import ProspectSecurityAudit + +logger = logging.getLogger(__name__) + +SEVERITY_COLORS = { + "critical": ("#dc2626", "#fef2f2", "#991b1b"), + "high": ("#ea580c", "#fff7ed", "#9a3412"), + "medium": ("#ca8a04", "#fefce8", "#854d0e"), + "low": ("#2563eb", "#eff6ff", "#1e40af"), + "info": ("#6b7280", "#f9fafb", "#374151"), +} + +GRADE_COLORS = { + "A+": "#16a34a", "A": "#22c55e", "B": "#eab308", + "C": "#f97316", "D": "#ef4444", "F": "#991b1b", +} + +CATEGORY_LABELS = { + "transport": "Transport Security (HTTPS/SSL)", + "headers": "Security Headers", + "exposure": "Information Exposure", + "cookies": "Cookie Security", + "config": "Server Configuration", + "technology": "Technology & Versions", +} + +DEFAULT_CONTACT = { + "name": "Samir Boulahtit", + "email": "contact@wizard.lu", + "phone": "+352 XXX XXX XXX", + "company": "Wizard", + "website": "https://wizard.lu", + "tagline": "Professional Web Development & Security", +} + + +class SecurityReportService: + """Generate branded HTML security audit reports.""" + + def generate_html_report( + self, + audit: ProspectSecurityAudit, + domain: str, + contact: dict | None = None, + ) -> str: + """Generate a standalone HTML report from stored audit data.""" + contact = contact or DEFAULT_CONTACT + esc = html_module.escape + + findings = json.loads(audit.findings_json) if audit.findings_json else [] + technologies = json.loads(audit.technologies_json) if audit.technologies_json else [] + grade = audit.grade + score = audit.score + grade_color = GRADE_COLORS.get(grade, "#6b7280") + + # Severity counts + sev_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0} + for f in findings: + if not f.get("is_positive") and f["severity"] in sev_counts: + sev_counts[f["severity"]] += 1 + + # Group findings by category + categories = ["transport", "headers", "exposure", "cookies", "config", "technology"] + grouped = {cat: [f for f in findings if f["category"] == cat] for cat in categories} + + # Build findings HTML + findings_html = "" + for cat in categories: + cat_findings = grouped.get(cat, []) + if not cat_findings: + continue + label = CATEGORY_LABELS.get(cat, cat) + findings_html += f'
This is a simulation based on real-world attacks. With the vulnerabilities found on your site, this scenario is technically possible.
++ Every day these vulnerabilities remain unfixed is another day your business and your customers are at risk. +
+Contact us today for a free consultation:
+This report was generated using passive, non-intrusive analysis techniques only. No active exploitation or unauthorized access was attempted.
+© {datetime.now().year} {esc(contact['company'])} — {esc(contact['tagline'])}
+