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/json.py · raw

 1"""JSON renderer — the coverage result as a machine-readable document.
 2
 3Stable key order so two runs diff cleanly. Suitable for dashboards, ticketing,
 4or gating a pipeline on the coverage percentage.
 5"""
 6
 7from __future__ import annotations
 8
 9import json as _json
10from typing import TYPE_CHECKING
11
12from .. import __version__
13
14if TYPE_CHECKING:
15    from ..coverage import CoverageReport
16
17
18def to_dict(report: CoverageReport) -> dict:
19    return {
20        "subject": report.subject,
21        "generated_at": report.generated_at,
22        "source_count": report.source_count,
23        "tool": {"name": "control-coverage", "version": __version__},
24        "frameworks": [
25            {
26                "framework": fc.catalog.framework,
27                "name": fc.catalog.name,
28                "version": fc.catalog.version,
29                "sha256": fc.catalog.sha256,
30                "catalog_coverage": fc.catalog.coverage,
31                "in_scope": fc.in_scope,
32                "addressed": fc.addressed,
33                "supported": fc.supported,
34                "coverage_pct": fc.coverage_pct,
35                "assured_pct": fc.assured_pct,
36                "counts": fc.counts,
37                "controls": [
38                    {
39                        "id": r.control.id,
40                        "code": r.control.code,
41                        "title": r.control.title,
42                        "family": r.control.family,
43                        "state": r.state,
44                        "owner": r.owner,
45                        "exclusion_reason": r.exclusion_reason,
46                        "checked_by": sorted({o.rule_id for o in r.observations if o.rule_id}),
47                        "sources": sorted({o.source for o in r.observations}),
48                    }
49                    for r in fc.results
50                ],
51            }
52            for fc in report.frameworks
53        ],
54        "orphan_codes": report.orphan_codes,
55    }
56
57
58def render(report: CoverageReport) -> str:
59    return _json.dumps(to_dict(report), indent=2, sort_keys=False) + "\n"