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
main: 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"