audit-labs/control-coverage
Control coverage and blind-spot analysis for audit evidence.
clone: git clone https://gitbay.org/audit-labs/control-coverage.git
main: 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"