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

v1.0.0: audit_report/reporters/__init__.py · raw

 1"""Report renderers. Each takes a :class:`Report` and returns a string."""
 2
 3from __future__ import annotations
 4
 5from dataclasses import dataclass
 6from datetime import datetime, timezone
 7
 8from .. import __version__
 9from ..engine import Finding, control_coverage, summarize
10from ..loader import Package
11from ..rules import Ruleset
12from . import html as _html
13from . import json as _json
14from . import markdown as _markdown
15
16
17@dataclass
18class Report:
19    """Everything a renderer needs: the package, findings, and rollups."""
20
21    package: Package
22    findings: list[Finding]
23    generated_at: str
24    ruleset: Ruleset | None = None
25
26    @property
27    def counts(self) -> dict[str, int]:
28        return summarize(self.findings)
29
30    @property
31    def coverage(self) -> dict[str, dict]:
32        return control_coverage(self.findings)
33
34    @property
35    def provenance(self) -> dict[str, dict]:
36        """Tool + ruleset identity, so a report can be tied to what produced it."""
37        rs = self.ruleset
38        return {
39            "tool": {"name": "audit-report", "version": __version__},
40            "ruleset": {
41                "name": rs.name if rs else "",
42                "platform": rs.platform if rs else "",
43                "version": rs.version if rs else "",
44                "sha256": rs.sha256 if rs else "",
45            },
46        }
47
48
49def build_report(
50    package: Package, findings: list[Finding], ruleset: Ruleset | None = None
51) -> Report:
52    """Assemble a :class:`Report` with a UTC generation timestamp."""
53    stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
54    return Report(
55        package=package, findings=findings, generated_at=stamp, ruleset=ruleset
56    )
57
58
59RENDERERS = {
60    "md": _markdown.render,
61    "html": _html.render,
62    "json": _json.render,
63}
64
65# File extension per format (md and markdown both write .md).
66EXTENSIONS = {"md": "md", "html": "html", "json": "json"}
67
68
69def render(report: Report, fmt: str) -> str:
70    """Render *report* in the named format ('md', 'html', or 'json')."""
71    try:
72        return RENDERERS[fmt](report)
73    except KeyError:
74        raise ValueError(f"unknown format: {fmt!r}") from None