audit-labs/control-coverage
Control coverage and blind-spot analysis for audit evidence.
clone: git clone https://gitbay.org/audit-labs/control-coverage.git
4adb893704fe2f443ff48d6983da69e4295eaace
unsigned
author: Christian Cleberg <hello@cleberg.net> · 2026-08-09T01:32:54Z
.github/workflows/release.yml | 4 ++-- control_coverage/cli.py | 12 ++++++++---- control_coverage/coverage.py | 2 +- control_coverage/trend.py | 2 +- tests/test_cli.py | 3 ++- tests/test_crosswalk.py | 3 ++- tests/test_reporters.py | 6 ++++-- tests/test_trend.py | 3 ++- 8 files changed, 22 insertions(+), 13 deletions(-) @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 - name: Build run: uv build - name: Check @@ -33,4 +33,4 @@ jobs: name: dist path: dist/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 @@ -117,11 +117,11 @@ def _now() -> str: return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") -def _build_report(paths, args, scp, names, subject): +def _build_report(paths, scp, names, subject): """Load a corpus from *paths* and evaluate it into a CoverageReport.""" try: observations = corpus.load_corpus(paths) - except (ValueError, FileNotFoundError, OSError) as exc: + except (ValueError, OSError) as exc: raise SystemExit(f"error: {exc}") from None catalogs = catalog.load_frameworks(names) return evaluate(catalogs, observations, scope=scp, subject=subject, generated_at=_now()) @@ -135,7 +135,7 @@ def main(argv: list[str] | None = None) -> int: try: observations = corpus.load_corpus(args.reports) - except (ValueError, FileNotFoundError, OSError) as exc: + except (ValueError, OSError) as exc: raise SystemExit(f"error: {exc}") from None scp = scope.load(args.scope) if args.scope else scope.empty() @@ -159,6 +159,10 @@ def main(argv: list[str] | None = None) -> int: _print_blind_spots(report) return _exit_code(report, args.fail_under) + return _default_mode(args, report, subject) + + +def _default_mode(args, report, subject) -> int: formats = [f.strip() for f in args.format.split(",") if f.strip()] if args.out: out_dir = Path(args.out) @@ -180,7 +184,7 @@ def main(argv: list[str] | None = None) -> int: def _trend_mode(args, scp, names, subject, current) -> int: from . import trend - baseline = _build_report([args.baseline], args, scp, names, subject) + baseline = _build_report([args.baseline], scp, names, subject) tr = trend.compare(baseline, current) formats = [f.strip() for f in args.format.split(",") if f.strip()] @@ -74,7 +74,7 @@ class FrameworkCoverage: @property def counts(self) -> dict[str, int]: - counts = {s: 0 for s in STATE_ORDER} + counts = dict.fromkeys(STATE_ORDER, 0) for r in self.results: counts[r.state] += 1 return counts @@ -81,7 +81,7 @@ class FrameworkTrend: @property def counts(self) -> dict[str, int]: - counts = {c: 0 for c in CATEGORY_ORDER} + counts = dict.fromkeys(CATEGORY_ORDER, 0) for d in self.deltas: counts[d.category] += 1 return counts @@ -85,7 +85,8 @@ def test_trend_html_output(tmp_path): cli.main([GITHUB, AWS, "--framework", "SOC2", "--baseline", BASELINE, "--format", "html,json", "--out", str(tmp_path)]) names = {p.name for p in tmp_path.iterdir()} - assert "trend.html" in names and "trend.json" in names + assert "trend.html" in names + assert "trend.json" in names def test_crosswalk_mode(capsys): @@ -63,5 +63,6 @@ def test_html_is_self_contained(): html = crosswalk.render_html(_crosswalk(["SOC2", "ISO", "NIST"])) assert html.startswith("<!doctype html>") assert "<style>" in html - assert "http://" not in html and "https://" not in html + assert "http://" not in html + assert "https://" not in html assert "github.org.require-2fa" in html @@ -47,7 +47,8 @@ def test_html_is_self_contained(): html = reporters.render(_report(), "html") assert html.startswith("<!doctype html>") assert "<style>" in html - assert "http://" not in html and "https://" not in html # no external assets + assert "http://" not in html # no external assets + assert "https://" not in html def test_soa_lists_applicability_and_status(): @@ -60,5 +61,6 @@ def test_soa_lists_applicability_and_status(): def test_unknown_format_raises(): import pytest + report = _report() with pytest.raises(ValueError, match="unknown format"): - reporters.render(_report(), "pdf") + reporters.render(report, "pdf") @@ -71,5 +71,6 @@ def test_html_is_self_contained(): html = trend.render_html(_compare()) assert html.startswith("<!doctype html>") assert "<style>" in html - assert "http://" not in html and "https://" not in html + assert "http://" not in html + assert "https://" not in html assert "CC9.2" in html # a changed control shows up