audit-labs/control-coverage

Control coverage and blind-spot analysis for audit evidence.

clone: git clone https://gitbay.org/audit-labs/control-coverage.git

v1.0.0: control_coverage/reporters/markdown.py · raw

  1"""Markdown renderer — the coverage matrix and blind-spot list, auditor-facing."""
  2
  3from __future__ import annotations
  4
  5from typing import TYPE_CHECKING
  6
  7from .. import __version__
  8from ..coverage import (
  9    ASSERTED,
 10    FAILING,
 11    OUT_OF_SCOPE,
 12    STATE_ORDER,
 13    SUPPORTED,
 14    UNADDRESSED,
 15)
 16
 17if TYPE_CHECKING:
 18    from ..coverage import CoverageReport, FrameworkCoverage
 19
 20_STATE_LABEL = {
 21    SUPPORTED: "supported",
 22    FAILING: "failing",
 23    ASSERTED: "asserted",
 24    UNADDRESSED: "unaddressed",
 25    OUT_OF_SCOPE: "out of scope",
 26}
 27_STATE_MARK = {
 28    SUPPORTED: "",
 29    FAILING: "",
 30    ASSERTED: "",
 31    UNADDRESSED: "",
 32    OUT_OF_SCOPE: "",
 33}
 34
 35
 36def _evidence_note(result) -> str:
 37    """A short 'checked by' cell: rule ids or the exclusion reason."""
 38    if result.state == OUT_OF_SCOPE:
 39        return f"_excluded: {result.exclusion_reason}_"
 40    if not result.observations:
 41        return ""
 42    rules = sorted({o.rule_id for o in result.observations if o.rule_id})
 43    return ", ".join(f"`{r}`" for r in rules)
 44
 45
 46def _framework_section(fc: FrameworkCoverage) -> list[str]:
 47    cat = fc.catalog
 48    counts = fc.counts
 49    out: list[str] = []
 50    out.append(f"## {cat.name}")
 51    out.append("")
 52    suffix = "" if cat.complete else " _(partial catalog — coverage is of the shipped subset)_"
 53    out.append(
 54        f"**Coverage {fc.coverage_pct}%** ({fc.addressed}/{fc.in_scope} in-scope "
 55        f"controls addressed) · **assured {fc.assured_pct}%** "
 56        f"({fc.supported} supported){suffix}"
 57    )
 58    out.append("")
 59    out.append(
 60        "| " + " · ".join(
 61            f"{_STATE_MARK[s]} {counts[s]} {_STATE_LABEL[s]}"
 62            for s in STATE_ORDER
 63            if counts[s]
 64        ) + " |"
 65    )
 66    out.append("|" + "---|")
 67    out.append("")
 68    out.append("| Control | Status | Description | Checked by |")
 69    out.append("| --- | --- | --- | --- |")
 70    for r in fc.results:
 71        mark = _STATE_MARK[r.state]
 72        label = _STATE_LABEL[r.state]
 73        out.append(
 74            f"| **{r.control.id}** | {mark} {label} | {r.control.title} | {_evidence_note(r)} |"
 75        )
 76    out.append("")
 77    return out
 78
 79
 80def _blind_spots(report: CoverageReport) -> list[str]:
 81    out: list[str] = ["## Blind spots", ""]
 82    total = sum(len(fc.blind_spots) for fc in report.frameworks)
 83    if total == 0:
 84        out.append("_No in-scope control is left unaddressed by the corpus._")
 85        out.append("")
 86        return out
 87    out.append(
 88        f"{total} in-scope control(s) are **unaddressed** — no finding in the corpus "
 89        "maps to them. These are the framework requirements the evidence does not "
 90        "look at yet."
 91    )
 92    out.append("")
 93    for fc in report.frameworks:
 94        spots = fc.blind_spots
 95        if not spots:
 96            continue
 97        out.append(f"### {fc.catalog.name} ({len(spots)})")
 98        out.append("")
 99        for r in spots:
100            fam = f" · _{r.control.family}_" if r.control.family else ""
101            out.append(f"- **{r.control.id}** — {r.control.title}{fam}")
102        out.append("")
103    return out
104
105
106def render(report: CoverageReport) -> str:
107    out: list[str] = []
108    title = report.subject or "Evidence corpus"
109    out.append(f"# Control Coverage — {title}")
110    out.append("")
111    out.append(f"- **Generated:** {report.generated_at}")
112    out.append(f"- **Tool:** control-coverage {__version__}")
113    out.append(f"- **Corpus:** {report.source_count} evidence source(s)")
114    frameworks = ", ".join(
115        f"{fc.catalog.framework} {fc.catalog.version} (`sha256:{fc.catalog.sha256[:12]}`)"
116        for fc in report.frameworks
117    )
118    out.append(f"- **Frameworks:** {frameworks}")
119    out.append("")
120    out.append(
121        "> Coverage measures how much of a framework the evidence corpus addresses — "
122        "not whether the organization is compliant. An unaddressed control is a gap "
123        "in *evidence*, which may reflect a real gap in *controls* or simply a signal "
124        "not yet collected. The final judgment belongs to the organization and its auditor."
125    )
126    out.append("")
127
128    # Headline table across frameworks.
129    out.append("## Summary")
130    out.append("")
131    out.append("| Framework | In scope | Addressed | Supported | Failing | Blind spots | Coverage | Assured |")
132    out.append("| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |")
133    for fc in report.frameworks:
134        c = fc.counts
135        out.append(
136            f"| {fc.catalog.name} | {fc.in_scope} | {fc.addressed} | {fc.supported} "
137            f"| {c[FAILING]} | {len(fc.blind_spots)} | {fc.coverage_pct}% | {fc.assured_pct}% |"
138        )
139    out.append("")
140
141    out.extend(_blind_spots(report))
142
143    for fc in report.frameworks:
144        out.extend(_framework_section(fc))
145
146    if report.orphan_codes:
147        out.append("## Unmatched control codes")
148        out.append("")
149        out.append(
150            "The corpus cites these control codes, but no loaded catalog defines them. "
151            "They are typos, renamed controls, or controls outside the bundled catalogs:"
152        )
153        out.append("")
154        for code in report.orphan_codes:
155            out.append(f"- `{code}`")
156        out.append("")
157
158    return "\n".join(out).rstrip() + "\n"