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

  1"""Tests for trend mode: discovery, timeline building, rendering, and the CLI."""
  2
  3import json
  4from pathlib import Path
  5
  6import pytest
  7
  8from audit_report import trend
  9from audit_report.cli import main
 10from audit_report.engine import FAIL, PASS, evaluate
 11from audit_report.loader import load_package
 12from audit_report.rules import load_ruleset
 13
 14FIXTURES = Path(__file__).parent / "fixtures"
 15SERIES = FIXTURES / "series"
 16RULESETS = Path("audit_report/rulesets")
 17
 18
 19def _build():
 20    platform, _subject, paths = trend.discover(SERIES)
 21    ruleset = load_ruleset(RULESETS / f"{platform}.yaml")
 22    packages = [load_package(p) for p in paths]
 23    findings = [evaluate(pkg, ruleset) for pkg in packages]
 24    return trend.build_trend(packages, findings)
 25
 26
 27def test_discover_orders_by_date():
 28    platform, subject, paths = trend.discover(SERIES)
 29    assert (platform, subject) == ("aws", "prod")
 30    assert [p.name for p in paths] == [
 31        "aws_audit_prod_2026-01-01",
 32        "aws_audit_prod_2026-02-01",
 33        "aws_audit_prod_2026-03-01",
 34    ]
 35
 36
 37def test_discover_rejects_mixed_series():
 38    # The fixtures root holds aws/github/gitlab packages for several subjects.
 39    with pytest.raises(ValueError, match="multiple series"):
 40        trend.discover(FIXTURES)
 41
 42
 43def test_discover_requires_two(tmp_path):
 44    # A parent directory holding a single package is not a trend.
 45    pkg = tmp_path / "aws_audit_solo_2026-01-01"
 46    pkg.mkdir()
 47    (pkg / "account_security.csv").write_text("root_mfa_enabled\nTrue\n", encoding="utf-8")
 48    with pytest.raises(ValueError, match="at least two"):
 49        trend.discover(tmp_path)
 50
 51
 52def test_discover_no_packages(tmp_path):
 53    with pytest.raises(ValueError, match="no audit-tools packages"):
 54        trend.discover(tmp_path)
 55
 56
 57def test_trend_timeline_and_totals():
 58    report = _build()
 59    assert report.dates == ["2026-01-01", "2026-02-01", "2026-03-01"]
 60    assert report.fails_per_date() == [8, 3, 0]
 61
 62    by_id = {row.rule.id: row for row in report.rows}
 63    # Root MFA: fail, then fixed and stays fixed.
 64    assert by_id["aws.root.mfa"].statuses == [FAIL, PASS, PASS]
 65    # Access-key rotation lags: fixed only in the final package.
 66    assert by_id["aws.iam.key-rotation"].statuses == [FAIL, FAIL, PASS]
 67
 68
 69def test_trend_row_transitions():
 70    report = _build()
 71    by_id = {row.rule.id: row for row in report.rows}
 72    assert by_id["aws.root.mfa"].transitions == 1  # one fail->pass change
 73    assert by_id["aws.s3.no-public-access"].transitions == 1
 74
 75
 76def test_trend_render_markdown():
 77    md = trend.render(_build(), "md")
 78    assert "# Evidence Trend — prod (aws)" in md
 79    assert "Failing total" in md
 80    assert "2026-03-01" in md
 81
 82
 83def test_trend_render_html_self_contained():
 84    html = trend.render(_build(), "html")
 85    assert html.startswith("<!doctype html>")
 86    assert "http://" not in html and "https://" not in html
 87    assert "class='trend'" in html
 88
 89
 90def test_trend_render_json():
 91    data = json.loads(trend.render(_build(), "json"))
 92    assert data["dates"] == ["2026-01-01", "2026-02-01", "2026-03-01"]
 93    assert data["fails_per_date"] == [8, 3, 0]
 94    rules = {r["id"]: r["statuses"] for r in data["rules"]}
 95    assert rules["aws.root.mfa"] == ["fail", "pass", "pass"]
 96
 97
 98def test_cli_trend_mode(tmp_path):
 99    out = tmp_path / "out"
100    code = main([str(SERIES), "--trend", "--format", "md,html,json", "--out", str(out)])
101    assert (out / "trend.md").exists()
102    assert (out / "trend.html").exists()
103    assert (out / "trend.json").exists()
104    # The latest package is clean, so default --fail-on none exits 0.
105    assert code == 0
106
107
108def test_cli_trend_fail_on_uses_latest(tmp_path):
109    # Latest package (2026-03) has no failures, so even --fail-on low passes.
110    code = main([str(SERIES), "--trend", "--format", "json", "--out", str(tmp_path), "--fail-on", "low"])
111    assert code == 0
112
113
114def test_cli_trend_and_baseline_conflict():
115    assert main([str(SERIES), "--trend", "--baseline", str(SERIES), "--format", "json"]) == 2