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_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