audit-labs/control-coverage
Control coverage and blind-spot analysis for audit evidence.
clone: git clone https://gitbay.org/audit-labs/control-coverage.git
v0.1.0: control_coverage/crosswalk.py · raw
1"""Crosswalk — which controls each piece of evidence supports, across frameworks.
2
3One check is rarely worth one control. Enforced 2FA is evidence for SOC 2 CC6.1,
4ISO A.5.17, and NIST IA-2 at once. This module inverts the coverage result to show
5that leverage: for every check (rule) in the corpus, the set of controls it
6addresses and the frameworks it spans.
7
8It then answers a practical question auditors and evidence-owners both ask — *what
9is the smallest set of checks that still covers everything?* — with a greedy
10set-cover over the addressed controls. The result is an ordered "minimal evidence
11set": collect these few checks and you have touched every control the full corpus
12touches, which is what you want when scoping a walkthrough or a sample.
13
14"Addressed" here matches the coverage engine: a control any finding maps to,
15whatever the finding's outcome.
16"""
17
18from __future__ import annotations
19
20from dataclasses import dataclass, field
21
22from .coverage import CoverageReport
23
24
25@dataclass
26class EvidenceItem:
27 """One check and the controls it supports across frameworks."""
28
29 rule_id: str
30 title: str
31 controls: list[str] # full FRAMEWORK:ID codes, sorted
32 frameworks: list[str] # framework short codes it spans, sorted
33
34 @property
35 def count(self) -> int:
36 return len(self.controls)
37
38
39@dataclass
40class CoverStep:
41 """One pick in the greedy minimal-evidence set."""
42
43 rule_id: str
44 new_controls: int # controls this pick added that were not yet covered
45 cumulative: int
46 cumulative_pct: float
47
48
49@dataclass
50class Crosswalk:
51 subject: str
52 generated_at: str
53 items: list[EvidenceItem] = field(default_factory=list)
54 cover: list[CoverStep] = field(default_factory=list)
55 universe_size: int = 0
56
57 @property
58 def multi_framework(self) -> list[EvidenceItem]:
59 """Checks that earn coverage in more than one framework at once."""
60 return [i for i in self.items if len(i.frameworks) > 1]
61
62
63def build(report: CoverageReport) -> Crosswalk:
64 """Invert a coverage report into a crosswalk and a minimal evidence set."""
65 rule_controls: dict[str, set[str]] = {}
66 rule_title: dict[str, str] = {}
67 rule_frameworks: dict[str, set[str]] = {}
68 universe: set[str] = set()
69
70 for fc in report.frameworks:
71 for r in fc.results:
72 if not r.addressed:
73 continue
74 code = r.control.code
75 universe.add(code)
76 for obs in r.observations:
77 if not obs.rule_id:
78 continue
79 rule_controls.setdefault(obs.rule_id, set()).add(code)
80 rule_title.setdefault(obs.rule_id, obs.title)
81 rule_frameworks.setdefault(obs.rule_id, set()).add(fc.catalog.framework)
82
83 items = [
84 EvidenceItem(
85 rule_id=rid,
86 title=rule_title.get(rid, ""),
87 controls=sorted(codes),
88 frameworks=sorted(rule_frameworks.get(rid, set())),
89 )
90 for rid, codes in rule_controls.items()
91 ]
92 # Most leverage first; rule id breaks ties for stable output.
93 items.sort(key=lambda i: (-i.count, i.rule_id))
94
95 cover = _greedy_cover(rule_controls, universe)
96 return Crosswalk(
97 subject=report.subject,
98 generated_at=report.generated_at,
99 items=items,
100 cover=cover,
101 universe_size=len(universe),
102 )
103
104
105def _greedy_cover(rule_controls: dict[str, set[str]], universe: set[str]) -> list[CoverStep]:
106 remaining = set(universe)
107 pool = {rid: set(codes) for rid, codes in rule_controls.items()}
108 total = len(universe) or 1
109 steps: list[CoverStep] = []
110
111 while remaining:
112 best_rule, best_gain = None, 0
113 for rid in sorted(pool):
114 gain = len(pool[rid] & remaining)
115 if gain > best_gain:
116 best_rule, best_gain = rid, gain
117 if not best_rule: # nothing left can cover the remainder
118 break
119 remaining -= pool[best_rule]
120 del pool[best_rule]
121 cumulative = len(universe) - len(remaining)
122 steps.append(
123 CoverStep(
124 rule_id=best_rule,
125 new_controls=best_gain,
126 cumulative=cumulative,
127 cumulative_pct=round(100 * cumulative / total, 1),
128 )
129 )
130 return steps
131
132
133# --- renderers -------------------------------------------------------------
134
135
136def to_dict(xw: Crosswalk) -> dict:
137 return {
138 "subject": xw.subject,
139 "generated_at": xw.generated_at,
140 "universe_size": xw.universe_size,
141 "minimal_evidence_set": [
142 {
143 "rule_id": s.rule_id,
144 "new_controls": s.new_controls,
145 "cumulative": s.cumulative,
146 "cumulative_pct": s.cumulative_pct,
147 }
148 for s in xw.cover
149 ],
150 "evidence": [
151 {
152 "rule_id": i.rule_id,
153 "title": i.title,
154 "frameworks": i.frameworks,
155 "controls": i.controls,
156 "count": i.count,
157 }
158 for i in xw.items
159 ],
160 }
161
162
163def render_json(xw: Crosswalk) -> str:
164 import json
165
166 return json.dumps(to_dict(xw), indent=2, sort_keys=False) + "\n"
167
168
169def render_markdown(xw: Crosswalk) -> str:
170 out: list[str] = []
171 out.append(f"# Evidence Crosswalk — {xw.subject or 'Evidence corpus'}")
172 out.append("")
173 out.append(f"- **Generated:** {xw.generated_at}")
174 out.append(f"- **Addressed controls:** {xw.universe_size}")
175 out.append(f"- **Checks in corpus:** {len(xw.items)}")
176 out.append(f"- **Checks spanning multiple frameworks:** {len(xw.multi_framework)}")
177 out.append("")
178
179 out.append("## Minimal evidence set")
180 out.append("")
181 if xw.cover:
182 out.append(
183 f"The {len(xw.cover)} check(s) below cover all {xw.universe_size} addressed "
184 "controls — the smallest set that touches everything the full corpus does."
185 )
186 out.append("")
187 out.append("| # | Check | New controls | Cumulative | % of addressed |")
188 out.append("| ---: | --- | ---: | ---: | ---: |")
189 for n, s in enumerate(xw.cover, 1):
190 out.append(
191 f"| {n} | `{s.rule_id}` | +{s.new_controls} | {s.cumulative} | {s.cumulative_pct}% |"
192 )
193 else:
194 out.append("_No addressed controls to cover._")
195 out.append("")
196
197 out.append("## Evidence leverage")
198 out.append("")
199 out.append("Each check and the controls it supports, most leverage first.")
200 out.append("")
201 out.append("| Check | Frameworks | # | Controls |")
202 out.append("| --- | --- | ---: | --- |")
203 for i in xw.items:
204 codes = ", ".join(f"`{c}`" for c in i.controls)
205 fws = ", ".join(i.frameworks)
206 out.append(f"| `{i.rule_id}` | {fws} | {i.count} | {codes} |")
207 out.append("")
208
209 return "\n".join(out).rstrip() + "\n"
210
211
212_XW_CSS = """
213.fw { display: inline-block; font-size: .7rem; font-weight: 700; letter-spacing: .02em;
214 padding: .08rem .4rem; border-radius: 4px; margin-right: .25rem; background: #eef; color: #33488c; }
215.track { position: relative; background: #eee; border-radius: 4px; height: 1rem; min-width: 5rem; }
216.track > span { position: absolute; left: 0; top: 0; bottom: 0; background: #35b866; border-radius: 4px; }
217.codes code { font-size: .78rem; }
218@media (prefers-color-scheme: dark) {
219 .fw { background: #22243a; color: #9fb0f0; }
220 .track { background: #26272b; }
221}
222"""
223
224
225def render_html(xw: Crosswalk) -> str:
226 from html import escape
227
228 from .reporters.html import CSS
229
230 title = xw.subject or "Evidence corpus"
231 body = [
232 "<!doctype html><html lang='en'><head><meta charset='utf-8'>",
233 "<meta name='viewport' content='width=device-width, initial-scale=1'>",
234 f"<title>Evidence Crosswalk — {escape(title)}</title>",
235 f"<style>{CSS}{_XW_CSS}</style></head><body><main>",
236 f"<h1>Evidence Crosswalk — {escape(title)}</h1>",
237 (
238 f'<p class="meta">Generated {escape(xw.generated_at)} · '
239 f"{xw.universe_size} addressed control(s) · {len(xw.items)} check(s) · "
240 f"{len(xw.multi_framework)} spanning multiple frameworks</p>"
241 ),
242 "<h2>Minimal evidence set</h2>",
243 ]
244 if xw.cover:
245 body.append(
246 f"<p>The {len(xw.cover)} check(s) below cover all {xw.universe_size} addressed "
247 "controls — the smallest set that touches everything the full corpus does.</p>"
248 )
249 body.append(
250 "<table><thead><tr><th class='num'>#</th><th>Check</th>"
251 "<th class='num'>New</th><th class='num'>Cumulative</th>"
252 "<th>% of addressed</th></tr></thead><tbody>"
253 )
254 for n, s in enumerate(xw.cover, 1):
255 body.append(
256 "<tr>"
257 f'<td class="num">{n}</td>'
258 f"<td><code>{escape(s.rule_id)}</code></td>"
259 f'<td class="num">+{s.new_controls}</td>'
260 f'<td class="num">{s.cumulative}</td>'
261 f'<td><div class="track" title="{s.cumulative_pct}%">'
262 f'<span style="width:{s.cumulative_pct:.1f}%"></span></div> {s.cumulative_pct}%</td>'
263 "</tr>"
264 )
265 body.append("</tbody></table>")
266 else:
267 body.append("<p>No addressed controls to cover.</p>")
268
269 body.append("<h2>Evidence leverage</h2>")
270 body.append("<p>Each check and the controls it supports, most leverage first.</p>")
271 body.append(
272 "<table><thead><tr><th>Check</th><th>Frameworks</th><th class='num'>#</th>"
273 "<th>Controls</th></tr></thead><tbody>"
274 )
275 for i in xw.items:
276 fws = "".join(f'<span class="fw">{escape(f)}</span>' for f in i.frameworks)
277 codes = ", ".join(f"<code>{escape(c)}</code>" for c in i.controls)
278 body.append(
279 "<tr>"
280 f"<td><code>{escape(i.rule_id)}</code></td>"
281 f"<td>{fws}</td>"
282 f'<td class="num">{i.count}</td>'
283 f'<td class="codes">{codes}</td>'
284 "</tr>"
285 )
286 body.append("</tbody></table>")
287 body.append(
288 "<footer>Generated by control-coverage · Audit Labs. Evidence, not a verdict.</footer>"
289 )
290 body.append("</main></body></html>")
291 return "".join(body)