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) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 21:41:40 +02:00
parent 4c750f0268
commit 30f3dae5a3
4 changed files with 278 additions and 2 deletions

View File

@@ -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 ───────────────────────────────────────────────

View File

@@ -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'<h3 style="margin-top:32px;font-size:18px;color:#1e293b;border-bottom:2px solid #e2e8f0;padding-bottom:8px;">{esc(label)}</h3>\n'
for f in cat_findings:
if f.get("is_positive"):
findings_html += f"""
<div style="margin:12px 0;padding:12px 16px;background:#f0fdf4;border-left:4px solid #16a34a;border-radius:0 8px 8px 0;">
<span style="color:#16a34a;font-weight:600;">&#10003; {esc(f["title"])}</span>
<span style="color:#166534;font-size:13px;margin-left:8px;">{esc(f["detail"])}</span>
</div>"""
else:
sev_color, sev_bg, sev_text = SEVERITY_COLORS.get(f["severity"], SEVERITY_COLORS["info"])
findings_html += f"""
<div style="margin:16px 0;background:{sev_bg};border:1px solid {sev_color}20;border-radius:12px;overflow:hidden;">
<div style="padding:16px 20px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;">
<span style="background:{sev_color};color:white;padding:2px 10px;border-radius:20px;font-size:12px;font-weight:700;text-transform:uppercase;">{esc(f["severity"])}</span>
<span style="font-weight:700;color:#1e293b;font-size:15px;">{esc(f["title"])}</span>
</div>
<div style="font-size:14px;color:#334155;margin-top:8px;">{esc(f["detail"])}</div>
</div>
</div>"""
# Technologies
tech_html = ""
if technologies:
tech_items = " ".join(
f'<span style="background:#f1f5f9;padding:4px 12px;border-radius:20px;font-size:13px;color:#475569;border:1px solid #e2e8f0;">{esc(t)}</span>'
for t in technologies
)
tech_html = f'<div style="margin-top:16px;"><span style="font-size:13px;color:#64748b;font-weight:600;">Technologies:</span><div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;">{tech_items}</div></div>'
now = datetime.now().strftime("%Y-%m-%d %H:%M")
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Security Audit Report — {esc(domain)}</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f8fafc; color: #1e293b; line-height: 1.6; }}
.container {{ max-width: 800px; margin: 0 auto; padding: 40px 24px; }}
@media print {{ body {{ background: white; }} .container {{ padding: 20px; }} .no-print {{ display: none !important; }} .page-break {{ page-break-before: always; }} }}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div style="text-align:center;margin-bottom:48px;">
<div style="font-size:12px;letter-spacing:3px;color:#94a3b8;text-transform:uppercase;margin-bottom:8px;">&#128274; Website Security Audit Report</div>
<h1 style="font-size:28px;font-weight:800;color:#0f172a;margin-bottom:8px;">{esc(domain)}</h1>
<div style="font-size:14px;color:#64748b;">Confidential — Prepared for {esc(domain)}</div>
<div style="font-size:13px;color:#94a3b8;margin-top:4px;">Report generated on {now}</div>
</div>
<!-- Score Card -->
<div style="background:white;border-radius:16px;box-shadow:0 1px 3px rgba(0,0,0,0.1);padding:32px;margin-bottom:32px;text-align:center;">
<h2 style="font-size:16px;color:#64748b;margin-bottom:24px;">Overall Security Grade</h2>
<div style="display:inline-flex;align-items:center;justify-content:center;width:120px;height:120px;border-radius:50%;background:{grade_color};margin-bottom:16px;">
<span style="font-size:48px;font-weight:900;color:white;">{grade}</span>
</div>
<div style="font-size:14px;color:#64748b;">Security Score: {score}/100</div>
<div style="margin-top:16px;height:8px;background:#e2e8f0;border-radius:4px;overflow:hidden;">
<div style="width:{score}%;height:100%;background:{grade_color};border-radius:4px;"></div>
</div>
<div style="margin-top:20px;display:flex;justify-content:center;gap:16px;flex-wrap:wrap;">
<span style="font-size:13px;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#dc2626;margin-right:4px;"></span>{sev_counts['critical']} Critical</span>
<span style="font-size:13px;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#ea580c;margin-right:4px;"></span>{sev_counts['high']} High</span>
<span style="font-size:13px;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#ca8a04;margin-right:4px;"></span>{sev_counts['medium']} Medium</span>
<span style="font-size:13px;"><span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#2563eb;margin-right:4px;"></span>{sev_counts['low']} Low</span>
</div>
{tech_html}
</div>
<!-- What Could Happen -->
<div class="page-break" style="background:#1e293b;border-radius:16px;padding:32px;margin-bottom:32px;color:white;">
<h2 style="font-size:20px;font-weight:700;color:#f87171;margin-bottom:24px;text-align:center;">&#9888;&#65039; What Could Happen To Your Website</h2>
<div style="background:#0f172a;border-radius:12px;overflow:hidden;border:1px solid #334155;margin-bottom:24px;">
<div style="background:#1e293b;padding:8px 16px;display:flex;align-items:center;gap:8px;border-bottom:1px solid #334155;">
<span style="width:10px;height:10px;border-radius:50%;background:#ef4444;"></span>
<span style="width:10px;height:10px;border-radius:50%;background:#eab308;"></span>
<span style="width:10px;height:10px;border-radius:50%;background:#22c55e;"></span>
<div style="flex:1;margin-left:8px;background:#0f172a;border-radius:6px;padding:4px 12px;">
<span style="font-size:12px;color:#64748b;">&#128274; https://{esc(domain)}</span>
</div>
</div>
<div style="padding:40px 24px;text-align:center;">
<div style="font-size:64px;margin-bottom:16px;">&#128128;</div>
<div style="font-size:28px;font-weight:900;color:#ef4444;margin-bottom:16px;letter-spacing:2px;">WEBSITE COMPROMISED</div>
<div style="font-size:14px;color:#94a3b8;max-width:500px;margin:0 auto;">This website has been hacked. All customer data including names, emails, phone numbers, and payment information has been stolen.</div>
</div>
</div>
<div style="padding:16px;background:#dc262615;border-radius:8px;border:1px solid #dc262630;margin-bottom:24px;">
<p style="font-size:13px;color:#fca5a5;">This is a simulation based on real-world attacks. With the vulnerabilities found on your site, this scenario is technically possible.</p>
</div>
<h3 style="font-size:16px;color:#f1f5f9;margin-bottom:16px;">Business Impact</h3>
<ul style="list-style:none;padding:0;">
<li style="margin-bottom:12px;padding-left:24px;position:relative;font-size:14px;color:#cbd5e1;"><span style="position:absolute;left:0;">&#10060;</span> Reputation Damage — Customers lose trust</li>
<li style="margin-bottom:12px;padding-left:24px;position:relative;font-size:14px;color:#cbd5e1;"><span style="position:absolute;left:0;">&#10060;</span> GDPR Fines — Up to 4% of annual turnover or &euro;20 million</li>
<li style="margin-bottom:12px;padding-left:24px;position:relative;font-size:14px;color:#cbd5e1;"><span style="position:absolute;left:0;">&#10060;</span> Google Blacklist — "This site may be hacked" warning kills traffic</li>
<li style="margin-bottom:12px;padding-left:24px;position:relative;font-size:14px;color:#cbd5e1;"><span style="position:absolute;left:0;">&#10060;</span> Business Downtime — Revenue loss during recovery</li>
<li style="margin-bottom:12px;padding-left:24px;position:relative;font-size:14px;color:#cbd5e1;"><span style="position:absolute;left:0;">&#10060;</span> Legal Liability — Liable for customers' stolen data</li>
</ul>
</div>
<!-- Detailed Findings -->
<div style="background:white;border-radius:16px;box-shadow:0 1px 3px rgba(0,0,0,0.1);padding:32px;margin-bottom:32px;">
<h2 style="font-size:20px;font-weight:700;color:#0f172a;margin-bottom:24px;">Detailed Findings</h2>
{findings_html}
</div>
<!-- Call to Action -->
<div style="background:linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);border-radius:16px;padding:40px;margin-bottom:32px;color:white;">
<h2 style="font-size:24px;font-weight:800;margin-bottom:16px;text-align:center;">&#128737;&#65039; Protect Your Business</h2>
<p style="font-size:15px;color:#e9d5ff;text-align:center;margin-bottom:24px;max-width:600px;margin-left:auto;margin-right:auto;">
Every day these vulnerabilities remain unfixed is another day your business and your customers are at risk.
</p>
<div style="background:rgba(255,255,255,0.1);border-radius:12px;padding:24px;margin-bottom:24px;">
<h3 style="font-size:16px;margin-bottom:12px;">What We Offer</h3>
<ul style="list-style:none;padding:0;">
<li style="margin-bottom:8px;padding-left:24px;position:relative;font-size:14px;color:#e9d5ff;"><span style="position:absolute;left:0;">&#10004;</span> Complete security audit and remediation plan</li>
<li style="margin-bottom:8px;padding-left:24px;position:relative;font-size:14px;color:#e9d5ff;"><span style="position:absolute;left:0;">&#10004;</span> Modern, secure website built with best practices</li>
<li style="margin-bottom:8px;padding-left:24px;position:relative;font-size:14px;color:#e9d5ff;"><span style="position:absolute;left:0;">&#10004;</span> Ongoing security monitoring and maintenance</li>
<li style="margin-bottom:8px;padding-left:24px;position:relative;font-size:14px;color:#e9d5ff;"><span style="position:absolute;left:0;">&#10004;</span> GDPR compliance and data protection</li>
</ul>
</div>
<div style="text-align:center;">
<p style="font-size:14px;color:#c4b5fd;margin-bottom:16px;">Contact us today for a free consultation:</p>
<div style="display:inline-block;background:white;border-radius:12px;padding:20px 32px;text-align:left;">
<div style="font-size:18px;font-weight:700;color:#6d28d9;margin-bottom:8px;">{esc(contact['name'])}</div>
<div style="font-size:14px;color:#475569;margin-bottom:4px;">&#128231; {esc(contact['email'])}</div>
<div style="font-size:14px;color:#475569;margin-bottom:4px;">&#128222; {esc(contact['phone'])}</div>
<div style="font-size:14px;color:#475569;">&#127760; {esc(contact['website'])}</div>
</div>
</div>
</div>
<!-- Disclaimer -->
<div style="text-align:center;padding:24px;font-size:12px;color:#94a3b8;line-height:1.7;">
<p>This report was generated using passive, non-intrusive analysis techniques only. No active exploitation or unauthorized access was attempted.</p>
<p style="margin-top:8px;">&copy; {datetime.now().year} {esc(contact['company'])} &mdash; {esc(contact['tagline'])}</p>
</div>
</div>
</body>
</html>"""
security_report_service = SecurityReportService()

View File

@@ -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 {

View File

@@ -201,8 +201,13 @@
<!-- Tab: Security -->
<div x-show="activeTab === 'security'" class="space-y-6">
<!-- Run Audit button -->
<div class="flex justify-end">
<!-- Action buttons -->
<div class="flex justify-end gap-3">
<button type="button" x-show="prospect.security_audit" @click="openSecurityReport()"
class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none">
<span x-html="$icon('document-text', 'w-4 h-4 mr-2')"></span>
Generate Report
</button>
<button type="button" @click="runSecurityAudit()" :disabled="auditRunning"
class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-yellow-600 border border-transparent rounded-lg hover:bg-yellow-700 focus:outline-none disabled:opacity-50">
<span x-show="!auditRunning" x-html="$icon('shield-check', 'w-4 h-4 mr-2')"></span>