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:
@@ -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 ───────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
241
app/modules/prospecting/services/security_report_service.py
Normal file
241
app/modules/prospecting/services/security_report_service.py
Normal 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;">✓ {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;">🔒 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;">⚠️ 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;">🔒 https://{esc(domain)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:40px 24px;text-align:center;">
|
||||
<div style="font-size:64px;margin-bottom:16px;">💀</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;">❌</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;">❌</span> GDPR Fines — Up to 4% of annual turnover or €20 million</li>
|
||||
<li style="margin-bottom:12px;padding-left:24px;position:relative;font-size:14px;color:#cbd5e1;"><span style="position:absolute;left:0;">❌</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;">❌</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;">❌</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;">🛡️ 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;">✔</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;">✔</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;">✔</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;">✔</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;">📧 {esc(contact['email'])}</div>
|
||||
<div style="font-size:14px;color:#475569;margin-bottom:4px;">📞 {esc(contact['phone'])}</div>
|
||||
<div style="font-size:14px;color:#475569;">🌐 {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;">© {datetime.now().year} {esc(contact['company'])} — {esc(contact['tagline'])}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
security_report_service = SecurityReportService()
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user