"""
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'