"""HTML renderer — a self-contained, printable coverage 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 __version__
from ..coverage import (
ASSERTED,
FAILING,
OUT_OF_SCOPE,
SUPPORTED,
UNADDRESSED,
)
if TYPE_CHECKING:
from ..coverage import CoverageReport, FrameworkCoverage
_STATE_LABEL = {
SUPPORTED: "supported",
FAILING: "failing",
ASSERTED: "asserted",
UNADDRESSED: "unaddressed",
OUT_OF_SCOPE: "out of scope",
}
_STATE_CLASS = {
SUPPORTED: "supported",
FAILING: "failing",
ASSERTED: "asserted",
UNADDRESSED: "unaddressed",
OUT_OF_SCOPE: "oos",
}
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: 64rem; 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; }
h3 { margin: 1.4rem 0 .5rem; font-size: 1.02rem; }
.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; }
td.num, th.num { text-align: right; }
.badge { display: inline-block; font-weight: 700; font-size: .72rem; letter-spacing: .02em;
padding: .12rem .5rem; border-radius: 999px; white-space: nowrap; }
.badge.supported { background: #e5f6ea; color: #1a7f37; }
.badge.failing { background: #fdeaea; color: #c1272d; }
.badge.asserted { background: #fff4e0; color: #a8620a; }
.badge.unaddressed { background: #eceaf6; color: #5b4bb0; }
.badge.oos { background: #eee; color: #666; }
.bar { display: flex; height: 1.1rem; border-radius: 4px; overflow: hidden; margin: .4rem 0 .2rem;
border: 1px solid #ddd; }
.bar > span { display: block; }
.bar .supported { background: #35b866; }
.bar .failing { background: #e2565b; }
.bar .asserted { background: #eaa53c; }
.bar .unaddressed { background: #8877d8; }
.bar .oos { background: #cfcfcf; }
.headline { font-size: 1.5rem; font-weight: 700; }
.headline small { font-size: .85rem; font-weight: 500; color: #666; }
.legend { font-size: .78rem; color: #666; display: flex; flex-wrap: wrap; gap: .8rem; margin: .2rem 0 1rem; }
.legend i { display: inline-block; width: .8rem; height: .8rem; border-radius: 2px; vertical-align: -1px; margin-right: .25rem; }
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; }
.headline small, .legend { color: #999; }
.badge.supported { background: #12321d; color: #4ac36a; }
.badge.failing { background: #3a1416; color: #ff6b70; }
.badge.asserted { background: #33260f; color: #e6a94e; }
.badge.unaddressed { background: #211d3a; color: #9d8ef0; }
.badge.oos { background: #26272b; color: #999; }
.bar { border-color: #333; }
footer { border-color: #2a2b30; }
}
"""
_LEGEND_COLORS = {
SUPPORTED: "#35b866",
FAILING: "#e2565b",
ASSERTED: "#eaa53c",
UNADDRESSED: "#8877d8",
OUT_OF_SCOPE: "#cfcfcf",
}
def _badge(state: str) -> str:
return f'{_STATE_LABEL[state]} '
def _bar(fc: FrameworkCoverage) -> str:
counts = fc.counts
total = sum(counts.values()) or 1
segments = []
for state in [SUPPORTED, FAILING, ASSERTED, UNADDRESSED, OUT_OF_SCOPE]:
n = counts[state]
if not n:
continue
pct = 100 * n / total
segments.append(
f' '
)
return '
' + "".join(segments) + "
"
def _legend() -> str:
items = []
for state in [SUPPORTED, FAILING, ASSERTED, UNADDRESSED, OUT_OF_SCOPE]:
items.append(
f' {_STATE_LABEL[state]} '
)
return '' + "".join(items) + "
"
def _checked_by(result) -> str:
if result.state == OUT_OF_SCOPE:
return f"excluded: {escape(result.exclusion_reason)} "
rules = sorted({o.rule_id for o in result.observations if o.rule_id})
return ", ".join(f"{escape(r)}" for r in rules)
def _framework_section(fc: FrameworkCoverage) -> str:
cat = fc.catalog
partial = "" if cat.complete else (
' (partial catalog — coverage is of the shipped subset) '
)
rows = []
for r in fc.results:
rows.append(
""
f"{escape(r.control.id)} "
f"{_badge(r.state)} "
f"{escape(r.control.title)} "
f"{_checked_by(r)} "
" "
)
return (
f"{escape(cat.name)}{partial} "
f'{fc.coverage_pct}% coverage · {fc.addressed}/{fc.in_scope} '
f"in-scope controls addressed · {fc.assured_pct}% assured
"
f"{_bar(fc)}{_legend()}"
"Control Status Description "
"Checked by "
+ "".join(rows)
+ "
"
)
def _summary_table(report: CoverageReport) -> str:
rows = []
for fc in report.frameworks:
c = fc.counts
rows.append(
""
f"{escape(fc.catalog.name)} "
f'{fc.in_scope} '
f'{fc.addressed} '
f'{fc.supported} '
f'{c[FAILING]} '
f'{len(fc.blind_spots)} '
f'{fc.coverage_pct}% '
f'{fc.assured_pct}% '
" "
)
return (
"Framework In scope "
"Addressed Supported "
"Failing Blind spots "
"Coverage Assured "
+ "".join(rows)
+ "
"
)
def _blind_spots(report: CoverageReport) -> str:
total = sum(len(fc.blind_spots) for fc in report.frameworks)
if total == 0:
return "Blind spots No in-scope control is left unaddressed by the corpus.
"
parts = [
"Blind spots ",
(
f"{total} in-scope control(s) are unaddressed — no finding "
"in the corpus maps to them.
"
),
]
for fc in report.frameworks:
spots = fc.blind_spots
if not spots:
continue
parts.append(f"{escape(fc.catalog.name)} ({len(spots)}) ")
for r in spots:
fam = f" · {escape(r.control.family)} " if r.control.family else ""
parts.append(
f"{escape(r.control.id)} — {escape(r.control.title)}{fam} "
)
parts.append(" ")
return "".join(parts)
def render(report: CoverageReport) -> str:
title = report.subject or "Evidence corpus"
frameworks = ", ".join(
f"{fc.catalog.framework} {fc.catalog.version} "
f"(sha256:{fc.catalog.sha256[:12]})"
for fc in report.frameworks
)
body = [
" ",
" ",
f"Control Coverage — {escape(title)} ",
f"",
f"Control Coverage — {escape(title)} ",
(
f'Generated {escape(report.generated_at)} · '
f"Tool control-coverage {escape(__version__)} · "
f"{report.source_count} evidence source(s) · frameworks: {escape(frameworks)}
"
),
(
'Coverage measures how much of a framework the evidence corpus '
"addresses — not whether the organization is compliant. An unaddressed control is "
"a gap in evidence , which may reflect a real control gap or simply a signal "
"not yet collected. The final judgment belongs to the organization and its auditor.
"
),
"Summary ",
_summary_table(report),
_blind_spots(report),
]
for fc in report.frameworks:
body.append(_framework_section(fc))
if report.orphan_codes:
codes = "".join(f"{escape(c)} " for c in report.orphan_codes)
body.append(
"Unmatched control codes The corpus cites these codes, but no loaded "
f"catalog defines them (typos, renamed, or out-of-catalog):
"
)
body.append(
"Generated by control-coverage · Audit Labs. Evidence, not a verdict. "
)
body.append(" ")
return "".join(body)