Commit 4adb893704
Unsigned
Layout: unified · split
.github/workflows/release.yml +2 −2
| @@ -11,7 +11,7 @@ jobs: | |||
| 11 | steps: | 11 | steps: |
| 12 | - uses: actions/checkout@v5 | 12 | - uses: actions/checkout@v5 |
| 13 | - name: Install uv | 13 | - name: Install uv |
| 14 | uses: astral-sh/setup-uv@v6 | 14 | uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 |
| 15 | - name: Build | 15 | - name: Build |
| 16 | run: uv build | 16 | run: uv build |
| 17 | - name: Check | 17 | - name: Check |
| @@ -33,4 +33,4 @@ jobs: | |||
| 33 | name: dist | 33 | name: dist |
| 34 | path: dist/ | 34 | path: dist/ |
| 35 | - name: Publish to PyPI | 35 | - name: Publish to PyPI |
| 36 | uses: pypa/gh-action-pypi-publish@release/v1 | 36 | uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 |
control_coverage/cli.py +8 −4
| @@ -117,11 +117,11 @@ def _now() -> str: | |||
| 117 | return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") | 117 | return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") |
| 118 | 118 | ||
| 119 | 119 | ||
| 120 | def _build_report(paths, args, scp, names, subject): | 120 | def _build_report(paths, scp, names, subject): |
| 121 | """Load a corpus from *paths* and evaluate it into a CoverageReport.""" | 121 | """Load a corpus from *paths* and evaluate it into a CoverageReport.""" |
| 122 | try: | 122 | try: |
| 123 | observations = corpus.load_corpus(paths) | 123 | observations = corpus.load_corpus(paths) |
| 124 | except (ValueError, FileNotFoundError, OSError) as exc: | 124 | except (ValueError, OSError) as exc: |
| 125 | raise SystemExit(f"error: {exc}") from None | 125 | raise SystemExit(f"error: {exc}") from None |
| 126 | catalogs = catalog.load_frameworks(names) | 126 | catalogs = catalog.load_frameworks(names) |
| 127 | return evaluate(catalogs, observations, scope=scp, subject=subject, generated_at=_now()) | 127 | return evaluate(catalogs, observations, scope=scp, subject=subject, generated_at=_now()) |
| @@ -135,7 +135,7 @@ def main(argv: list[str] | None = None) -> int: | |||
| 135 | 135 | ||
| 136 | try: | 136 | try: |
| 137 | observations = corpus.load_corpus(args.reports) | 137 | observations = corpus.load_corpus(args.reports) |
| 138 | except (ValueError, FileNotFoundError, OSError) as exc: | 138 | except (ValueError, OSError) as exc: |
| 139 | raise SystemExit(f"error: {exc}") from None | 139 | raise SystemExit(f"error: {exc}") from None |
| 140 | 140 | ||
| 141 | scp = scope.load(args.scope) if args.scope else scope.empty() | 141 | scp = scope.load(args.scope) if args.scope else scope.empty() |
| @@ -159,6 +159,10 @@ def main(argv: list[str] | None = None) -> int: | |||
| 159 | _print_blind_spots(report) | 159 | _print_blind_spots(report) |
| 160 | return _exit_code(report, args.fail_under) | 160 | return _exit_code(report, args.fail_under) |
| 161 | 161 | ||
| 162 | return _default_mode(args, report, subject) | ||
| 163 | |||
| 164 | |||
| 165 | def _default_mode(args, report, subject) -> int: | ||
| 162 | formats = [f.strip() for f in args.format.split(",") if f.strip()] | 166 | formats = [f.strip() for f in args.format.split(",") if f.strip()] |
| 163 | if args.out: | 167 | if args.out: |
| 164 | out_dir = Path(args.out) | 168 | out_dir = Path(args.out) |
| @@ -180,7 +184,7 @@ def main(argv: list[str] | None = None) -> int: | |||
| 180 | def _trend_mode(args, scp, names, subject, current) -> int: | 184 | def _trend_mode(args, scp, names, subject, current) -> int: |
| 181 | from . import trend | 185 | from . import trend |
| 182 | 186 | ||
| 183 | baseline = _build_report([args.baseline], args, scp, names, subject) | 187 | baseline = _build_report([args.baseline], scp, names, subject) |
| 184 | tr = trend.compare(baseline, current) | 188 | tr = trend.compare(baseline, current) |
| 185 | 189 | ||
| 186 | formats = [f.strip() for f in args.format.split(",") if f.strip()] | 190 | formats = [f.strip() for f in args.format.split(",") if f.strip()] |
control_coverage/coverage.py +1 −1
| @@ -74,7 +74,7 @@ class FrameworkCoverage: | |||
| 74 | 74 | ||
| 75 | @property | 75 | @property |
| 76 | def counts(self) -> dict[str, int]: | 76 | def counts(self) -> dict[str, int]: |
| 77 | counts = {s: 0 for s in STATE_ORDER} | 77 | counts = dict.fromkeys(STATE_ORDER, 0) |
| 78 | for r in self.results: | 78 | for r in self.results: |
| 79 | counts[r.state] += 1 | 79 | counts[r.state] += 1 |
| 80 | return counts | 80 | return counts |
control_coverage/trend.py +1 −1
| @@ -81,7 +81,7 @@ class FrameworkTrend: | |||
| 81 | 81 | ||
| 82 | @property | 82 | @property |
| 83 | def counts(self) -> dict[str, int]: | 83 | def counts(self) -> dict[str, int]: |
| 84 | counts = {c: 0 for c in CATEGORY_ORDER} | 84 | counts = dict.fromkeys(CATEGORY_ORDER, 0) |
| 85 | for d in self.deltas: | 85 | for d in self.deltas: |
| 86 | counts[d.category] += 1 | 86 | counts[d.category] += 1 |
| 87 | return counts | 87 | return counts |
tests/test_cli.py +2 −1
| @@ -85,7 +85,8 @@ def test_trend_html_output(tmp_path): | |||
| 85 | cli.main([GITHUB, AWS, "--framework", "SOC2", "--baseline", BASELINE, | 85 | cli.main([GITHUB, AWS, "--framework", "SOC2", "--baseline", BASELINE, |
| 86 | "--format", "html,json", "--out", str(tmp_path)]) | 86 | "--format", "html,json", "--out", str(tmp_path)]) |
| 87 | names = {p.name for p in tmp_path.iterdir()} | 87 | names = {p.name for p in tmp_path.iterdir()} |
| 88 | assert "trend.html" in names and "trend.json" in names | 88 | assert "trend.html" in names |
| 89 | assert "trend.json" in names | ||
| 89 | 90 | ||
| 90 | 91 | ||
| 91 | def test_crosswalk_mode(capsys): | 92 | def test_crosswalk_mode(capsys): |
tests/test_crosswalk.py +2 −1
| @@ -63,5 +63,6 @@ def test_html_is_self_contained(): | |||
| 63 | html = crosswalk.render_html(_crosswalk(["SOC2", "ISO", "NIST"])) | 63 | html = crosswalk.render_html(_crosswalk(["SOC2", "ISO", "NIST"])) |
| 64 | assert html.startswith("<!doctype html>") | 64 | assert html.startswith("<!doctype html>") |
| 65 | assert "<style>" in html | 65 | assert "<style>" in html |
| 66 | assert "http://" not in html and "https://" not in html | 66 | assert "http://" not in html |
| 67 | assert "https://" not in html | ||
| 67 | assert "github.org.require-2fa" in html | 68 | assert "github.org.require-2fa" in html |
tests/test_reporters.py +4 −2
| @@ -47,7 +47,8 @@ def test_html_is_self_contained(): | |||
| 47 | html = reporters.render(_report(), "html") | 47 | html = reporters.render(_report(), "html") |
| 48 | assert html.startswith("<!doctype html>") | 48 | assert html.startswith("<!doctype html>") |
| 49 | assert "<style>" in html | 49 | assert "<style>" in html |
| 50 | assert "http://" not in html and "https://" not in html # no external assets | 50 | assert "http://" not in html # no external assets |
| 51 | assert "https://" not in html | ||
| 51 | 52 | ||
| 52 | 53 | ||
| 53 | def test_soa_lists_applicability_and_status(): | 54 | def test_soa_lists_applicability_and_status(): |
| @@ -60,5 +61,6 @@ def test_soa_lists_applicability_and_status(): | |||
| 60 | def test_unknown_format_raises(): | 61 | def test_unknown_format_raises(): |
| 61 | import pytest | 62 | import pytest |
| 62 | 63 | ||
| 64 | report = _report() | ||
| 63 | with pytest.raises(ValueError, match="unknown format"): | 65 | with pytest.raises(ValueError, match="unknown format"): |
| 64 | reporters.render(_report(), "pdf") | 66 | reporters.render(report, "pdf") |
tests/test_trend.py +2 −1
| @@ -71,5 +71,6 @@ def test_html_is_self_contained(): | |||
| 71 | html = trend.render_html(_compare()) | 71 | html = trend.render_html(_compare()) |
| 72 | assert html.startswith("<!doctype html>") | 72 | assert html.startswith("<!doctype html>") |
| 73 | assert "<style>" in html | 73 | assert "<style>" in html |
| 74 | assert "http://" not in html and "https://" not in html | 74 | assert "http://" not in html |
| 75 | assert "https://" not in html | ||
| 75 | assert "CC9.2" in html # a changed control shows up | 76 | assert "CC9.2" in html # a changed control shows up |