"""Pro HTML report generator. Self-contained, print-friendly.""" import html import json from .styles import REPORT_CSS, FP_GUIDE_HTML def _sev_class(sev: str) -> str: """Map raw severity to CSS class name.""" return { "CRITICAL": "critical", "ERROR": "error", "HIGH": "error", "WARNING": "warning", "MEDIUM": "warning", "INFO": "info", "LOW": "info", }.get(sev.upper(), "info") def _render_finding(f: dict) -> str: sev = f["severity"].upper() sc = _sev_class(sev) owasp_html = " ".join( f'{html.escape(o)}' for o in f["owasp"] if o != "UNMAPPED" ) rem_html = "" if f["remediation"]: rem_html = (f'
Fix: ' f'{html.escape(f["remediation"])}
') return f"""
{html.escape(sev)} {f['confidence'].upper()} {f['category'].upper()} {html.escape(f['tool'])}/{html.escape(f['rule'])} {owasp_html}
{html.escape(f['message'])}
{html.escape(f['file'])}:{f['line']} {rem_html}
""" def _aggregate(findings: list[dict]) -> dict: by_sev: dict[str, int] = {} by_conf = {"confirmed": 0, "likely": 0, "possible": 0} by_cat: dict[str, int] = {} by_owasp = {} for f in findings: s = f["severity"].upper() by_sev[s] = by_sev.get(s, 0) + 1 by_conf[f["confidence"]] = by_conf.get(f["confidence"], 0) + 1 cat = f.get("category", "security") by_cat[cat] = by_cat.get(cat, 0) + 1 for o in f["owasp"]: if o != "UNMAPPED": by_owasp[o] = by_owasp.get(o, 0) + 1 return {"severity": by_sev, "confidence": by_conf, "category": by_cat, "owasp": by_owasp} def generate_html_report(findings: list[dict], scan_meta: dict) -> str: """Build a self-contained HTML report.""" agg = _aggregate(findings) sec_findings = [f for f in findings if f["category"] == "security"] perf_findings = [f for f in findings if f["category"] == "performance"] ml_findings = [f for f in findings if f["category"] == "ml-security"] llm_findings = [f for f in findings if f["category"] in ("llm", "llm-security")] sc_findings = [f for f in findings if f["category"] == "supply-chain"] # Any category not handled above (future proofing) other_cats = {"security", "performance", "ml-security", "llm", "llm-security", "supply-chain"} other_findings = [f for f in findings if f["category"] not in other_cats] sec_html = "\n".join(_render_finding(f) for f in sec_findings) perf_html = "\n".join(_render_finding(f) for f in perf_findings) ml_html = "\n".join(_render_finding(f) for f in ml_findings) llm_html = "\n".join(_render_finding(f) for f in llm_findings) sc_html = "\n".join(_render_finding(f) for f in sc_findings) other_html = "\n".join(_render_finding(f) for f in other_findings) owasp_html = "".join( f'
{html.escape(k)}
' f'
{v}
' for k, v in sorted(agg["owasp"].items(), key=lambda x: -x[1])[:8] ) owasp_section = ( f'

Top OWASP Categories

' f'
{owasp_html}
' if owasp_html else '' ) # Build per-category summary cards (all categories that have findings) cat_order = ["security", "ml-security", "supply-chain", "llm", "llm-security", "performance"] cat_label = { "security": "Security", "ml-security": "ML Security", "supply-chain": "Supply Chain", "llm": "LLM / AI", "llm-security": "LLM Security", "performance": "Performance", } # Show ordered known categories first, then any extras ordered_cats = [c for c in cat_order if agg["category"].get(c, 0) > 0] extra_cats = [c for c in agg["category"] if c not in cat_order and agg["category"][c] > 0] cat_cards = "".join( f'
{html.escape(cat_label.get(c, c.title()))}
' f'
{agg["category"][c]}
' for c in ordered_cats + extra_cats ) ml_section = ( f'

🤖 ML / Model Security Findings ({len(ml_findings)})

' f'{ml_html}
' ) if ml_findings or any(t == "ml-security" for t in agg["category"]) else "" _no_llm_html = '

No LLM findings.

' llm_section = ( f'

🧠 LLM / AI Findings ({len(llm_findings)})

' f'{llm_html or _no_llm_html}
' ) if llm_findings else "" _no_sc_html = '

No supply-chain findings.

' sc_section = ( f'

🔗 Supply Chain Findings ({len(sc_findings)})

' f'{sc_html or _no_sc_html}
' ) if sc_findings else "" other_section = ( f'

📋 Other Findings ({len(other_findings)})

' f'{other_html}
' ) if other_findings else "" footer_text = scan_meta.get("footer", "HF Security Scanner") return f""" Security & Performance Report — {html.escape(scan_meta.get('target','?'))}

🔐 Security & Performance Audit Report

Target: {html.escape(scan_meta.get('target','?'))} Targets scanned: {scan_meta.get('n_targets',1)} Generated: {scan_meta.get('timestamp','?')}

Executive Summary

Critical
{agg['severity'].get('CRITICAL',0)}
High / Error
{agg['severity'].get('ERROR',0) + agg['severity'].get('HIGH',0)}
Medium / Warning
{agg['severity'].get('WARNING',0) + agg['severity'].get('MEDIUM',0)}
Low / Info
{agg['severity'].get('INFO',0) + agg['severity'].get('LOW',0)}
Confirmed
{agg['confidence'].get('confirmed',0)}
Likely
{agg['confidence'].get('likely',0)}
Possible FP
{agg['confidence'].get('possible',0)}
{cat_cards}
{owasp_section} {FP_GUIDE_HTML}

🔐 Security Findings ({len(sec_findings)})

{sec_html or '

No security findings.

'}
{ml_section} {sc_section} {llm_section}

⚡ Performance Findings ({len(perf_findings)})

{perf_html or '

No performance findings.

'}
{other_section}
"""