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 import logging
from fastapi import APIRouter, Depends, Path, Query from fastapi import APIRouter, Depends, Path, Query
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api 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 ( from app.modules.prospecting.services.security_audit_service import (
security_audit_service, 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.prospecting.services.stats_service import stats_service
from app.modules.tenancy.schemas.auth import UserContext from app.modules.tenancy.schemas.auth import UserContext
@@ -152,6 +156,28 @@ def compute_scores_batch(
return ScoreComputeBatchResponse(scored=count) 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 ─────────────────────────────────────────────── # ── 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() { async runSecurityAudit() {
this.auditRunning = true; this.auditRunning = true;
try { try {

View File

@@ -201,8 +201,13 @@
<!-- Tab: Security --> <!-- Tab: Security -->
<div x-show="activeTab === 'security'" class="space-y-6"> <div x-show="activeTab === 'security'" class="space-y-6">
<!-- Run Audit button --> <!-- Action buttons -->
<div class="flex justify-end"> <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" <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"> 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> <span x-show="!auditRunning" x-html="$icon('shield-check', 'w-4 h-4 mr-2')"></span>