"""HTML renderer — a self-contained, printable evidence report. No external assets: all CSS is inlined so the file can be attached to an audit workpaper and opened anywhere, including offline. """ from __future__ import annotations from html import escape from typing import TYPE_CHECKING from .. import catalog from ..engine import FAIL, NOT_APPLICABLE, PASS if TYPE_CHECKING: from . import Report _STATUS_LABEL = {PASS: "PASS", FAIL: "FAIL", NOT_APPLICABLE: "N/A"} _STATUS_CLASS = {PASS: "pass", FAIL: "fail", NOT_APPLICABLE: "na"} _SEVERITY_ORDER = {"high": 0, "medium": 1, "low": 2} CSS = """ :root { color-scheme: light dark; } * { box-sizing: border-box; } body { font-family: -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 2rem; line-height: 1.5; color: #1a1a1a; background: #fff; } main { max-width: 60rem; margin: 0 auto; } h1 { margin: 0 0 .25rem; font-size: 1.6rem; } h2 { margin: 2rem 0 .75rem; font-size: 1.25rem; border-bottom: 2px solid #e5e5e5; padding-bottom: .25rem; } .meta { color: #555; font-size: .9rem; margin: 0 0 1rem; } .meta code { background: #f2f2f2; padding: .05rem .3rem; border-radius: 3px; } .note { background: #f7f7f9; border-left: 3px solid #b9b9c6; padding: .6rem .9rem; font-size: .9rem; color: #444; border-radius: 0 4px 4px 0; } table { border-collapse: collapse; width: 100%; font-size: .85rem; margin: .5rem 0; } th, td { border: 1px solid #e0e0e0; padding: .35rem .5rem; text-align: left; vertical-align: top; } th { background: #f5f5f7; } .badge { display: inline-block; font-weight: 700; font-size: .72rem; letter-spacing: .03em; padding: .12rem .5rem; border-radius: 999px; } .badge.pass { background: #e5f6ea; color: #1a7f37; } .badge.fail { background: #fdeaea; color: #c1272d; } .badge.na { background: #eee; color: #666; } .finding { border: 1px solid #e5e5e5; border-radius: 6px; padding: 1rem 1.1rem; margin: .8rem 0; } .finding.fail { border-left: 4px solid #c1272d; } .finding.pass { border-left: 4px solid #1a7f37; } .finding.na { border-left: 4px solid #bbb; } .finding h3 { margin: 0 0 .5rem; font-size: 1.05rem; display: flex; gap: .5rem; align-items: center; } .finding dl { display: grid; grid-template-columns: max-content 1fr; gap: .2rem .8rem; margin: .4rem 0 0; font-size: .9rem; } .finding dt { color: #666; font-weight: 600; } .finding dd { margin: 0; } .summary-pills span { display: inline-block; margin-right: .5rem; font-weight: 600; } footer { margin-top: 3rem; font-size: .8rem; color: #888; border-top: 1px solid #eee; padding-top: .75rem; } @media (prefers-color-scheme: dark) { body { color: #e6e6e6; background: #16171a; } h2 { border-color: #333; } .meta { color: #aaa; } .meta code { background: #26272b; } .note { background: #1e1f24; border-color: #444; color: #bbb; } th, td { border-color: #333; } th { background: #202126; } .finding { border-color: #2d2e33; } .badge.pass { background: #12321d; color: #4ac36a; } .badge.fail { background: #3a1416; color: #ff6b70; } .badge.na { background: #26272b; color: #999; } footer { border-color: #2a2b30; } } """ def _badge(status: str) -> str: return f'{_STATUS_LABEL[status]}' def _evidence_table(rows: list[dict[str, str]]) -> str: shown = rows[:10] headers = list(shown[0].keys()) head = "".join(f"{escape(h)}" for h in headers) body = "".join( "" + "".join(f"{escape(str(r.get(h, '')))}" for h in headers) + "" for r in shown ) extra = ( f"

+{len(rows) - len(shown)} more row(s) omitted.

" if len(rows) > len(shown) else "" ) return f"{head}{body}
{extra}" def render(report: Report) -> str: pkg = report.package counts = report.counts parts: list[str] = [] parts.append(f"

Evidence Report — {escape(pkg.subject)}

") prov = report.provenance tool, rs = prov["tool"], prov["ruleset"] ruleset_meta = "" if rs["sha256"]: rs_ver = f" {escape(rs['version'])}" if rs["version"] else "" ruleset_meta = ( f" · Ruleset {escape(rs['name'])}{rs_ver} " f"sha256:{escape(rs['sha256'][:12])}" ) parts.append( f"

Platform {escape(pkg.platform)} · " f"Source {escape(pkg.path.name)} · " f"Generated {escape(report.generated_at)} · " f"Tool {escape(tool['name'])} {escape(tool['version'])}" f"{ruleset_meta}

" ) parts.append( "

" f"{_badge(FAIL)} {counts[FAIL]} failing" f"{_badge(PASS)} {counts[PASS]} passing" f"{_badge(NOT_APPLICABLE)} {counts[NOT_APPLICABLE]} not applicable" "

" ) parts.append( "

This report presents evidence, not a " "compliance verdict. A failing row means a setting is in a state that does " "not support a control; the final judgment belongs to the organization and " "its auditor.

" ) # Coverage matrix. parts.append("

Control coverage

") cov_rows = "".join( f"{escape(control)}{escape(catalog.framework_of(control))}" f"{_badge(entry['status'])}" f"{escape(', '.join(entry['rules']))}" f"{escape(catalog.describe(control))}" for control, entry in report.coverage.items() ) parts.append( "" f"{cov_rows}
ControlFrameworkStatusChecked byDescription
" ) # Findings. parts.append("

Findings

") ordered = sorted( report.findings, key=lambda f: (f.status != FAIL, _SEVERITY_ORDER.get(f.rule.severity, 1)), ) for finding in ordered: rule = finding.rule rows = [ f"
Rule
{escape(rule.id)} · {escape(rule.severity)}
", f"
Controls
{escape(', '.join(rule.controls) or '—')}
", f"
Result
{escape(finding.reason)}
", ] if rule.rationale: rows.append(f"
Why it matters
{escape(rule.rationale.strip())}
") if finding.status == FAIL and rule.remediation: rows.append(f"
Remediation
{escape(rule.remediation.strip())}
") evidence = ( _evidence_table(finding.evidence) if finding.status == FAIL and finding.evidence else "" ) parts.append( f"
" f"

{_badge(finding.status)} {escape(rule.title)}

" f"
{''.join(rows)}
{evidence}
" ) parts.append( "" ) body = "\n".join(parts) return ( "" "" f"Evidence Report — {escape(pkg.subject)}" f"
{body}
\n" )