From 30f3dae5a397812d40e0fbd85e9e8e3a94398281 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Wed, 1 Apr 2026 21:41:40 +0200 Subject: [PATCH] feat(prospecting): add security audit report generation (Workstream 2B) - SecurityReportService generates standalone branded HTML reports from stored audit data (grade badge, simulated hacked site, detailed findings, business impact, call-to-action with contact info) - GET /security-audit/report/{prospect_id} returns HTMLResponse - "Generate Report" button on prospect detail security tab opens report in new browser tab (printable to PDF) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../routes/api/admin_enrichment.py | 26 ++ .../services/security_report_service.py | 241 ++++++++++++++++++ .../static/admin/js/prospect-detail.js | 4 + .../prospecting/admin/prospect-detail.html | 9 +- 4 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 app/modules/prospecting/services/security_report_service.py 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'

{esc(label)}

\n' + + for f in cat_findings: + if f.get("is_positive"): + findings_html += f""" +
+ ✓ {esc(f["title"])} + {esc(f["detail"])} +
""" + else: + sev_color, sev_bg, sev_text = SEVERITY_COLORS.get(f["severity"], SEVERITY_COLORS["info"]) + findings_html += f""" +
+
+
+ {esc(f["severity"])} + {esc(f["title"])} +
+
{esc(f["detail"])}
+
+
""" + + # Technologies + tech_html = "" + if technologies: + tech_items = " ".join( + f'{esc(t)}' + for t in technologies + ) + tech_html = f'
Technologies:
{tech_items}
' + + now = datetime.now().strftime("%Y-%m-%d %H:%M") + + return f""" + + + + + Security Audit Report — {esc(domain)} + + + +
+ + +
+
🔒 Website Security Audit Report
+

{esc(domain)}

+
Confidential — Prepared for {esc(domain)}
+
Report generated on {now}
+
+ + +
+

Overall Security Grade

+
+ {grade} +
+
Security Score: {score}/100
+
+
+
+
+ {sev_counts['critical']} Critical + {sev_counts['high']} High + {sev_counts['medium']} Medium + {sev_counts['low']} Low +
+ {tech_html} +
+ + +
+

⚠️ What Could Happen To Your Website

+
+
+ + + +
+ 🔒 https://{esc(domain)} +
+
+
+
💀
+
WEBSITE COMPROMISED
+
This website has been hacked. All customer data including names, emails, phone numbers, and payment information has been stolen.
+
+
+
+

This is a simulation based on real-world attacks. With the vulnerabilities found on your site, this scenario is technically possible.

+
+

Business Impact

+
    +
  • Reputation Damage — Customers lose trust
  • +
  • GDPR Fines — Up to 4% of annual turnover or €20 million
  • +
  • Google Blacklist — "This site may be hacked" warning kills traffic
  • +
  • Business Downtime — Revenue loss during recovery
  • +
  • Legal Liability — Liable for customers' stolen data
  • +
+
+ + +
+

Detailed Findings

+ {findings_html} +
+ + +
+

🛡️ Protect Your Business

+

+ Every day these vulnerabilities remain unfixed is another day your business and your customers are at risk. +

+
+

What We Offer

+
    +
  • Complete security audit and remediation plan
  • +
  • Modern, secure website built with best practices
  • +
  • Ongoing security monitoring and maintenance
  • +
  • GDPR compliance and data protection
  • +
+
+
+

Contact us today for a free consultation:

+
+
{esc(contact['name'])}
+
📧 {esc(contact['email'])}
+
📞 {esc(contact['phone'])}
+
🌐 {esc(contact['website'])}
+
+
+
+ + +
+

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'])}

+
+ +
+ +""" + + +security_report_service = SecurityReportService() diff --git a/app/modules/prospecting/static/admin/js/prospect-detail.js b/app/modules/prospecting/static/admin/js/prospect-detail.js index 232cab21..861d7471 100644 --- a/app/modules/prospecting/static/admin/js/prospect-detail.js +++ b/app/modules/prospecting/static/admin/js/prospect-detail.js @@ -117,6 +117,10 @@ function prospectDetail(prospectId) { } }, + openSecurityReport() { + window.open('/api/v1/admin/prospecting/enrichment/security-audit/report/' + this.prospectId, '_blank'); + }, + async runSecurityAudit() { this.auditRunning = true; try { diff --git a/app/modules/prospecting/templates/prospecting/admin/prospect-detail.html b/app/modules/prospecting/templates/prospecting/admin/prospect-detail.html index 155fc329..2001f737 100644 --- a/app/modules/prospecting/templates/prospecting/admin/prospect-detail.html +++ b/app/modules/prospecting/templates/prospecting/admin/prospect-detail.html @@ -201,8 +201,13 @@
- -
+ +
+