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

Pin CI actions to SHA, simplify main(), tidy exceptions and tests
 .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(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index dd9ff4c..b9f5577 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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
diff --git a/control_coverage/cli.py b/control_coverage/cli.py
index b559524..49e02e4 100644
--- a/control_coverage/cli.py
+++ b/control_coverage/cli.py
@@ -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()]
diff --git a/control_coverage/coverage.py b/control_coverage/coverage.py
index f6b6f7d..8b0000f 100644
--- a/control_coverage/coverage.py
+++ b/control_coverage/coverage.py
@@ -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
diff --git a/control_coverage/trend.py b/control_coverage/trend.py
index 8d42e93..e321259 100644
--- a/control_coverage/trend.py
+++ b/control_coverage/trend.py
@@ -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
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 4f8e53a..45b41e6 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -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):
diff --git a/tests/test_crosswalk.py b/tests/test_crosswalk.py
index 54dae6e..718354a 100644
--- a/tests/test_crosswalk.py
+++ b/tests/test_crosswalk.py
@@ -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
diff --git a/tests/test_reporters.py b/tests/test_reporters.py
index ec28c99..bf229b2 100644
--- a/tests/test_reporters.py
+++ b/tests/test_reporters.py
@@ -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")
diff --git a/tests/test_trend.py b/tests/test_trend.py
index aad7c25..f8797ba 100644
--- a/tests/test_trend.py
+++ b/tests/test_trend.py
@@ -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