autoscan / report /html.py
Chris4K's picture
Upload 384 files
a2a5bfd verified
"""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'<span class="badge owasp">{html.escape(o)}</span>'
for o in f["owasp"] if o != "UNMAPPED"
)
rem_html = ""
if f["remediation"]:
rem_html = (f'<div class="remediation"><strong>Fix:</strong> '
f'{html.escape(f["remediation"])}</div>')
return f"""
<div class="finding {sc}">
<div class="finding-header">
<span class="badge {sc}">{html.escape(sev)}</span>
<span class="badge {f['confidence']}">{f['confidence'].upper()}</span>
<span class="badge category-{f['category']}">{f['category'].upper()}</span>
<span class="badge tool">{html.escape(f['tool'])}/{html.escape(f['rule'])}</span>
{owasp_html}
</div>
<div class="message">{html.escape(f['message'])}</div>
<code>{html.escape(f['file'])}:{f['line']}</code>
{rem_html}
</div>
"""
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'<div class="card"><div class="label">{html.escape(k)}</div>'
f'<div class="value">{v}</div></div>'
for k, v in sorted(agg["owasp"].items(), key=lambda x: -x[1])[:8]
)
owasp_section = (
f'<section><h2>Top OWASP Categories</h2>'
f'<div class="summary-grid">{owasp_html}</div></section>'
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'<div class="card"><div class="label">{html.escape(cat_label.get(c, c.title()))}</div>'
f'<div class="value">{agg["category"][c]}</div></div>'
for c in ordered_cats + extra_cats
)
ml_section = (
f'<section><h2>πŸ€– ML / Model Security Findings ({len(ml_findings)})</h2>'
f'{ml_html}</section>'
) if ml_findings or any(t == "ml-security" for t in agg["category"]) else ""
_no_llm_html = '<p style="color:#6b7280">No LLM findings.</p>'
llm_section = (
f'<section><h2>🧠 LLM / AI Findings ({len(llm_findings)})</h2>'
f'{llm_html or _no_llm_html}</section>'
) if llm_findings else ""
_no_sc_html = '<p style="color:#6b7280">No supply-chain findings.</p>'
sc_section = (
f'<section><h2>πŸ”— Supply Chain Findings ({len(sc_findings)})</h2>'
f'{sc_html or _no_sc_html}</section>'
) if sc_findings else ""
other_section = (
f'<section><h2>πŸ“‹ Other Findings ({len(other_findings)})</h2>'
f'{other_html}</section>'
) if other_findings else ""
footer_text = scan_meta.get("footer", "HF Security Scanner")
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Security &amp; Performance Report β€” {html.escape(scan_meta.get('target','?'))}</title>
<style>{REPORT_CSS}</style>
</head>
<body>
<div class="container">
<header class="report-header">
<h1>πŸ” Security &amp; Performance Audit Report</h1>
<div class="meta">
<span><strong>Target:</strong> {html.escape(scan_meta.get('target','?'))}</span>
<span><strong>Targets scanned:</strong> {scan_meta.get('n_targets',1)}</span>
<span><strong>Generated:</strong> {scan_meta.get('timestamp','?')}</span>
</div>
</header>
<section>
<h2>Executive Summary</h2>
<div class="summary-grid">
<div class="card critical"><div class="label">Critical</div><div class="value">{agg['severity'].get('CRITICAL',0)}</div></div>
<div class="card error"><div class="label">High / Error</div><div class="value">{agg['severity'].get('ERROR',0) + agg['severity'].get('HIGH',0)}</div></div>
<div class="card warning"><div class="label">Medium / Warning</div><div class="value">{agg['severity'].get('WARNING',0) + agg['severity'].get('MEDIUM',0)}</div></div>
<div class="card info"><div class="label">Low / Info</div><div class="value">{agg['severity'].get('INFO',0) + agg['severity'].get('LOW',0)}</div></div>
<div class="card confirmed"><div class="label">Confirmed</div><div class="value">{agg['confidence'].get('confirmed',0)}</div></div>
<div class="card likely"><div class="label">Likely</div><div class="value">{agg['confidence'].get('likely',0)}</div></div>
<div class="card possible"><div class="label">Possible FP</div><div class="value">{agg['confidence'].get('possible',0)}</div></div>
{cat_cards}
</div>
</section>
{owasp_section}
{FP_GUIDE_HTML}
<section>
<h2>πŸ” Security Findings ({len(sec_findings)})</h2>
{sec_html or '<p style="color:#6b7280">No security findings.</p>'}
</section>
{ml_section}
{sc_section}
{llm_section}
<section>
<h2>⚑ Performance Findings ({len(perf_findings)})</h2>
{perf_html or '<p style="color:#6b7280">No performance findings.</p>'}
</section>
{other_section}
<footer>{html.escape(footer_text)}</footer>
</div>
</body>
</html>
"""