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

v0.1.0: audit_report/reporters/markdown.py · raw

  1"""Markdown renderer — the auditor-facing evidence report."""
  2
  3from __future__ import annotations
  4
  5from typing import TYPE_CHECKING
  6
  7from .. import catalog
  8from ..engine import FAIL, NOT_APPLICABLE, PASS
  9
 10if TYPE_CHECKING:
 11    from . import Report
 12
 13_STATUS_LABEL = {PASS: "PASS", FAIL: "FAIL", NOT_APPLICABLE: "N/A"}
 14_SEVERITY_ORDER = {"high": 0, "medium": 1, "low": 2}
 15
 16
 17def _evidence_table(rows: list[dict[str, str]]) -> list[str]:
 18    """Render up to a handful of evidence rows as a Markdown table."""
 19    if not rows:
 20        return []
 21    shown = rows[:10]
 22    headers = list(shown[0].keys())
 23    lines = [
 24        "| " + " | ".join(headers) + " |",
 25        "| " + " | ".join("---" for _ in headers) + " |",
 26    ]
 27    for row in shown:
 28        lines.append("| " + " | ".join(str(row.get(h, "")) for h in headers) + " |")
 29    if len(rows) > len(shown):
 30        lines.append(f"\n_+{len(rows) - len(shown)} more row(s) omitted._")
 31    return lines
 32
 33
 34def render(report: Report) -> str:
 35    pkg = report.package
 36    counts = report.counts
 37    out: list[str] = []
 38
 39    out.append(f"# Evidence Report — {pkg.subject}")
 40    out.append("")
 41    out.append(f"- **Platform:** {pkg.platform}")
 42    out.append(f"- **Subject:** {pkg.subject}")
 43    out.append(f"- **Source package:** `{pkg.path.name}`")
 44    out.append(f"- **Generated:** {report.generated_at}")
 45    prov = report.provenance
 46    tool, rs = prov["tool"], prov["ruleset"]
 47    out.append(f"- **Tool:** {tool['name']} {tool['version']}")
 48    if rs["sha256"]:
 49        rs_ver = f" {rs['version']}" if rs["version"] else ""
 50        out.append(
 51            f"- **Ruleset:** {rs['name']}{rs_ver} "
 52            f"(`sha256:{rs['sha256'][:12]}`)"
 53        )
 54    out.append(
 55        f"- **Result:** {counts[FAIL]} failing · {counts[PASS]} passing · "
 56        f"{counts[NOT_APPLICABLE]} not applicable"
 57    )
 58    out.append("")
 59    out.append(
 60        "> This report presents *evidence*, not a compliance verdict. A failing "
 61        "row means a setting is in a state that does not support a control; the "
 62        "final judgment belongs to the organization and its auditor."
 63    )
 64    out.append("")
 65
 66    # Control coverage matrix.
 67    out.append("## Control coverage")
 68    out.append("")
 69    out.append("| Control | Framework | Status | Checked by | Description |")
 70    out.append("| --- | --- | --- | --- | --- |")
 71    for control, entry in report.coverage.items():
 72        out.append(
 73            f"| {control} | {catalog.framework_of(control)} | "
 74            f"{_STATUS_LABEL[entry['status']]} | {', '.join(entry['rules'])} | "
 75            f"{catalog.describe(control)} |"
 76        )
 77    out.append("")
 78
 79    # Findings, failures first then by severity.
 80    out.append("## Findings")
 81    out.append("")
 82    ordered = sorted(
 83        report.findings,
 84        key=lambda f: (f.status != FAIL, _SEVERITY_ORDER.get(f.rule.severity, 1)),
 85    )
 86    for finding in ordered:
 87        rule = finding.rule
 88        out.append(f"### {_STATUS_LABEL[finding.status]} · {rule.title}")
 89        out.append("")
 90        out.append(f"- **Rule:** `{rule.id}`  ·  **Severity:** {rule.severity}")
 91        out.append(f"- **Controls:** {', '.join(rule.controls) or ''}")
 92        out.append(f"- **Result:** {finding.reason}")
 93        if rule.rationale:
 94            out.append(f"- **Why it matters:** {rule.rationale.strip()}")
 95        if finding.status == FAIL and rule.remediation:
 96            out.append(f"- **Remediation:** {rule.remediation.strip()}")
 97        out.append("")
 98        if finding.status == FAIL and finding.evidence:
 99            out.append("**Evidence:**")
100            out.append("")
101            out.extend(_evidence_table(finding.evidence))
102            out.append("")
103
104    return "\n".join(out).rstrip() + "\n"