audit-labs/audit-report

Turn audit-tools evidence packages into control-mapped, auditor-ready reports.

clone: git clone https://gitbay.org/audit-labs/audit-report.git

b94aafccf922b8730bd7e5bad28d49dc8408b794

unsigned

author: Christian Cleberg <hello@cleberg.net> · 2026-08-09T01:32:54Z

Pin CI actions to SHA, refactor diff/rules complexity, tidy tests
 .github/workflows/release.yml |  4 +-
 audit_report/diff.py          | 97 ++++++++++++++++++++++++-------------------
 audit_report/rules.py         | 51 +++++++++++++----------
 tests/test_diff.py            |  3 +-
 tests/test_reporters.py       |  6 ++-
 tests/test_trend.py           |  3 +-
 6 files changed, 93 insertions(+), 71 deletions(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index dd9ff4c..9fc5bda 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,11 +11,11 @@ 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
-        run: uvx twine check dist/*
+        run: uvx twine@7.0.0 check dist/*
       - uses: actions/upload-artifact@v4
         with:
           name: dist
diff --git a/audit_report/diff.py b/audit_report/diff.py
index a6c96c5..c9e728d 100644
--- a/audit_report/diff.py
+++ b/audit_report/diff.py
@@ -185,6 +185,33 @@ def _md_table(rows: list[dict[str, str]]) -> list[str]:
     return out
 
 
+def _render_delta_md(delta, category: str) -> list[str]:
+    rule = delta.rule
+    out = [
+        f"### {rule.title}",
+        "",
+        f"- **Rule:** `{rule.id}` · **Severity:** {rule.severity}",
+        f"- **Controls:** {', '.join(rule.controls) or '—'}",
+        f"- **Change:** {_transition(delta)}",
+    ]
+    if delta.new_reason:
+        out.append(f"- **Now:** {delta.new_reason}")
+    if category == REGRESSED and rule.remediation:
+        out.append(f"- **Remediation:** {rule.remediation.strip()}")
+    out.append("")
+    if delta.evidence_added:
+        out.append("**Newly failing rows:**")
+        out.append("")
+        out.extend(_md_table(delta.evidence_added))
+        out.append("")
+    if delta.evidence_removed:
+        out.append("**No longer failing rows:**")
+        out.append("")
+        out.extend(_md_table(delta.evidence_removed))
+        out.append("")
+    return out
+
+
 def _render_md(diff: DiffReport) -> str:
     counts = diff.counts
     out: list[str] = []
@@ -213,27 +240,7 @@ def _render_md(diff: DiffReport) -> str:
         out.append(f"## {_CATEGORY_HEADING[category]}")
         out.append("")
         for delta in rows:
-            rule = delta.rule
-            out.append(f"### {rule.title}")
-            out.append("")
-            out.append(f"- **Rule:** `{rule.id}` · **Severity:** {rule.severity}")
-            out.append(f"- **Controls:** {', '.join(rule.controls) or '—'}")
-            out.append(f"- **Change:** {_transition(delta)}")
-            if delta.new_reason:
-                out.append(f"- **Now:** {delta.new_reason}")
-            if category == REGRESSED and rule.remediation:
-                out.append(f"- **Remediation:** {rule.remediation.strip()}")
-            out.append("")
-            if delta.evidence_added:
-                out.append("**Newly failing rows:**")
-                out.append("")
-                out.extend(_md_table(delta.evidence_added))
-                out.append("")
-            if delta.evidence_removed:
-                out.append("**No longer failing rows:**")
-                out.append("")
-                out.extend(_md_table(delta.evidence_removed))
-                out.append("")
+            out.extend(_render_delta_md(delta, category))
 
     unchanged = diff.counts[UNCHANGED]
     if unchanged:
@@ -275,6 +282,31 @@ table.added caption { color: #c1272d; } table.removed caption { color: #1a7f37;
 )
 
 
+def _render_delta_html(delta, category: str) -> str:
+    rule = delta.rule
+    body = [
+        f"<h3>{escape(rule.title)}</h3>",
+        (
+            f"<dl><dt>Rule</dt><dd><code>{escape(rule.id)}</code> · "
+            f"{escape(rule.severity)}</dd>"
+        ),
+        f"<dt>Controls</dt><dd>{escape(', '.join(rule.controls) or '—')}</dd>",
+        f"<dt>Change</dt><dd class='transition'>{escape(_transition(delta))}</dd>",
+    ]
+    if delta.new_reason:
+        body.append(f"<dt>Now</dt><dd>{escape(delta.new_reason)}</dd>")
+    if category == REGRESSED and rule.remediation:
+        body.append(f"<dt>Remediation</dt><dd>{escape(rule.remediation.strip())}</dd>")
+    body.append("</dl>")
+    if delta.evidence_added:
+        body.append(_html_table(delta.evidence_added, "Newly failing rows", "added"))
+    if delta.evidence_removed:
+        body.append(
+            _html_table(delta.evidence_removed, "No longer failing rows", "removed")
+        )
+    return f"<div class='delta {category}'>{''.join(body)}</div>"
+
+
 def _render_html(diff: DiffReport) -> str:
     counts = diff.counts
     parts: list[str] = []
@@ -307,28 +339,7 @@ def _render_html(diff: DiffReport) -> str:
             continue
         parts.append(f"<h2>{escape(_CATEGORY_HEADING[category])}</h2>")
         for delta in rows:
-            rule = delta.rule
-            body = [
-                f"<h3>{escape(rule.title)}</h3>",
-                (
-                    f"<dl><dt>Rule</dt><dd><code>{escape(rule.id)}</code> · "
-                    f"{escape(rule.severity)}</dd>"
-                ),
-                f"<dt>Controls</dt><dd>{escape(', '.join(rule.controls) or '—')}</dd>",
-                f"<dt>Change</dt><dd class='transition'>{escape(_transition(delta))}</dd>",
-            ]
-            if delta.new_reason:
-                body.append(f"<dt>Now</dt><dd>{escape(delta.new_reason)}</dd>")
-            if category == REGRESSED and rule.remediation:
-                body.append(f"<dt>Remediation</dt><dd>{escape(rule.remediation.strip())}</dd>")
-            body.append("</dl>")
-            if delta.evidence_added:
-                body.append(_html_table(delta.evidence_added, "Newly failing rows", "added"))
-            if delta.evidence_removed:
-                body.append(
-                    _html_table(delta.evidence_removed, "No longer failing rows", "removed")
-                )
-            parts.append(f"<div class='delta {category}'>{''.join(body)}</div>")
+            parts.append(_render_delta_html(delta, category))
 
     if counts[UNCHANGED]:
         parts.append(f"<p class='meta'>{counts[UNCHANGED]} rule(s) unchanged.</p>")
diff --git a/audit_report/rules.py b/audit_report/rules.py
index 14f7013..5a32cfe 100644
--- a/audit_report/rules.py
+++ b/audit_report/rules.py
@@ -71,19 +71,19 @@ def _as_number(value: str) -> float | None:
         return None
 
 
-def match(condition: dict, row: dict[str, str]) -> bool:
-    """Return True if *condition* holds for *row*.
-
-    Raises ``ValueError`` on a malformed condition so ruleset bugs surface
-    loudly rather than silently evaluating to False.
-    """
-    if "all" in condition:
-        return all(match(c, row) for c in condition["all"])
-    if "any" in condition:
-        return any(match(c, row) for c in condition["any"])
-    if "not" in condition:
-        return not match(condition["not"], row)
-
+def _compare_numeric(op: str, raw: str, value) -> bool:
+    left, right = _as_number(raw), _as_number(str(value))
+    if left is None or right is None:
+        return False
+    return {
+        "gt": left > right,
+        "gte": left >= right,
+        "lt": left < right,
+        "lte": left <= right,
+    }[op]
+
+
+def _match_leaf(condition: dict, row: dict[str, str]) -> bool:
     column = condition.get("column")
     op = condition.get("op")
     if column is None or op is None:
@@ -109,19 +109,26 @@ def match(condition: dict, row: dict[str, str]) -> bool:
         choices = {str(v).strip().lower() for v in (value or [])}
         return (norm in choices) if op == "in" else (norm not in choices)
     if op in ("gt", "gte", "lt", "lte"):
-        left, right = _as_number(raw), _as_number(str(value))
-        if left is None or right is None:
-            return False
-        return {
-            "gt": left > right,
-            "gte": left >= right,
-            "lt": left < right,
-            "lte": left <= right,
-        }[op]
+        return _compare_numeric(op, raw, value)
 
     raise ValueError(f"unknown operator: {op!r}")
 
 
+def match(condition: dict, row: dict[str, str]) -> bool:
+    """Return True if *condition* holds for *row*.
+
+    Raises ``ValueError`` on a malformed condition so ruleset bugs surface
+    loudly rather than silently evaluating to False.
+    """
+    if "all" in condition:
+        return all(match(c, row) for c in condition["all"])
+    if "any" in condition:
+        return any(match(c, row) for c in condition["any"])
+    if "not" in condition:
+        return not match(condition["not"], row)
+    return _match_leaf(condition, row)
+
+
 def load_ruleset(path: str | Path) -> Ruleset:
     """Parse a ruleset YAML file into a :class:`Ruleset`, validating each rule."""
     text = Path(path).read_text(encoding="utf-8")
diff --git a/tests/test_diff.py b/tests/test_diff.py
index 3811501..687e975 100644
--- a/tests/test_diff.py
+++ b/tests/test_diff.py
@@ -67,7 +67,8 @@ def test_diff_render_markdown():
 def test_diff_render_html_self_contained():
     html = diff.render(_build(), "html")
     assert html.startswith("<!doctype html>")
-    assert "http://" not in html and "https://" not in html
+    assert "http://" not in html
+    assert "https://" not in html
     assert "Evidence Drift" in html
 
 
diff --git a/tests/test_reporters.py b/tests/test_reporters.py
index 35d9f77..81e1255 100644
--- a/tests/test_reporters.py
+++ b/tests/test_reporters.py
@@ -35,7 +35,8 @@ def test_html_render_is_self_contained():
     assert html.startswith("<!doctype html>")
     assert "<style>" in html
     # No external resource references.
-    assert "http://" not in html and "https://" not in html
+    assert "http://" not in html
+    assert "https://" not in html
     assert "src=" not in html
 
 
@@ -79,8 +80,9 @@ def test_html_escapes_evidence(tmp_path):
 
 
 def test_unknown_format_raises():
+    report = _report()
     with pytest.raises(ValueError, match="unknown format"):
-        reporters.render(_report(), "pdf")
+        reporters.render(report, "pdf")
 
 
 def test_cli_writes_files_and_exit_code(tmp_path):
diff --git a/tests/test_trend.py b/tests/test_trend.py
index 26fb3d0..a77d158 100644
--- a/tests/test_trend.py
+++ b/tests/test_trend.py
@@ -83,7 +83,8 @@ def test_trend_render_markdown():
 def test_trend_render_html_self_contained():
     html = trend.render(_build(), "html")
     assert html.startswith("<!doctype html>")
-    assert "http://" not in html and "https://" not in html
+    assert "http://" not in html
+    assert "https://" not in html
     assert "class='trend'" in html