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: tests/test_reporters.py · raw

  1"""Tests for the report renderers and the CLI entry point."""
  2
  3import json
  4from pathlib import Path
  5
  6import pytest
  7
  8from audit_report import __version__, reporters
  9from audit_report.cli import main
 10from audit_report.engine import evaluate
 11from audit_report.loader import load_package
 12from audit_report.rules import load_ruleset
 13
 14FIXTURES = Path(__file__).parent / "fixtures"
 15RULESETS = Path("audit_report/rulesets")
 16
 17
 18def _report(pkg_name="aws_audit_acme_2026-01-01", ruleset="aws.yaml"):
 19    pkg = load_package(FIXTURES / pkg_name)
 20    rs = load_ruleset(RULESETS / ruleset)
 21    findings = evaluate(pkg, rs)
 22    return reporters.build_report(pkg, findings, rs)
 23
 24
 25def test_markdown_render_contains_sections():
 26    md = reporters.render(_report(), "md")
 27    assert "# Evidence Report — acme" in md
 28    assert "## Control coverage" in md
 29    assert "## Findings" in md
 30    assert "aws.network.no-open-ssh" in md
 31
 32
 33def test_html_render_is_self_contained():
 34    html = reporters.render(_report(), "html")
 35    assert html.startswith("<!doctype html>")
 36    assert "<style>" in html
 37    # No external resource references.
 38    assert "http://" not in html and "https://" not in html
 39    assert "src=" not in html
 40
 41
 42def test_json_render_roundtrips():
 43    data = json.loads(reporters.render(_report(), "json"))
 44    assert data["platform"] == "aws"
 45    assert data["subject"] == "acme"
 46    ids = {f["id"]: f["status"] for f in data["findings"]}
 47    assert ids["aws.iam.console-mfa"] == "fail"
 48
 49
 50def test_json_stamps_tool_and_ruleset_provenance():
 51    data = json.loads(reporters.render(_report(), "json"))
 52    assert data["tool"] == {"name": "audit-report", "version": __version__}
 53    rs = data["ruleset"]
 54    assert rs["name"] == "AWS ITGC ruleset"
 55    assert rs["platform"] == "aws"
 56    assert rs["version"] == "2026.08.0"
 57    assert len(rs["sha256"]) == 64  # full SHA-256 hex digest of the ruleset file
 58
 59
 60def test_markdown_shows_ruleset_provenance():
 61    md = reporters.render(_report(), "md")
 62    assert "**Tool:** audit-report" in md
 63    assert "sha256:" in md
 64
 65
 66def test_html_escapes_evidence(tmp_path):
 67    pkg_dir = tmp_path / "aws_audit_x_2026-01-01"
 68    pkg_dir.mkdir()
 69    (pkg_dir / "open_security_groups.csv").write_text(
 70        "region,group_id,group_name,protocol,from_port,to_port,open_to\n"
 71        "us-east-1,sg-1,<script>x</script>,tcp,22,22,0.0.0.0/0\n",
 72        encoding="utf-8",
 73    )
 74    pkg = load_package(pkg_dir)
 75    report = reporters.build_report(pkg, evaluate(pkg, load_ruleset(RULESETS / "aws.yaml")))
 76    html = reporters.render(report, "html")
 77    assert "<script>x</script>" not in html
 78    assert "&lt;script&gt;" in html
 79
 80
 81def test_unknown_format_raises():
 82    with pytest.raises(ValueError, match="unknown format"):
 83        reporters.render(_report(), "pdf")
 84
 85
 86def test_cli_writes_files_and_exit_code(tmp_path):
 87    out = tmp_path / "out"
 88    code = main(
 89        [
 90            str(FIXTURES / "aws_audit_acme_2026-01-01"),
 91            "--format",
 92            "md,html,json",
 93            "--out",
 94            str(out),
 95            "--fail-on",
 96            "high",
 97        ]
 98    )
 99    assert (out / "report.md").exists()
100    assert (out / "report.html").exists()
101    assert (out / "report.json").exists()
102    # The AWS fixture has high-severity failures (open SSH), so exit is non-zero.
103    assert code == 1
104
105
106def test_cli_fail_on_none_is_zero(tmp_path):
107    code = main(
108        [str(FIXTURES / "aws_audit_acme_2026-01-01"), "--format", "json", "--out", str(tmp_path)]
109    )
110    assert code == 0
111
112
113def test_cli_missing_package_returns_2():
114    assert main([str(FIXTURES / "nope")]) == 2