10 — Change Report Evidence (10_change_report_evidence.py)¶
Demonstrates collecting and rendering auditable ChangeReport evidence (HTML, Markdown, JSON), comparing live unpublished rulebase numbering (OwnedSession) against published cached numbering.
Source Code (examples/10_change_report_evidence.py)¶
examples/10_change_report_evidence.py
"""Example 10 — Auditable change report evidence (`OwnedSession` pending vs published HTML/Markdown/JSON).
Run:
ARODONATA_LAB=home uv run examples/10_change_report_evidence.py
Demonstrates:
1. Creating a baseline setup session and a change session in a lab domain (`Domain5` by default).
2. Collecting and rendering a `ChangeReport` while the session is still unpublished using
`OwnedSession(sid=..., server_ip=...)` (`numbering_source == "owned_session"`).
3. Publishing the session and rendering the final auditable bundle (`html`, `markdown`, `json`)
to `examples/_tmp/`.
4. Reverting the domain to its original baseline via `_session_guard.guarded_session`.
"""
from __future__ import annotations
import asyncio
import logging
import os
import sys
import uuid
from pathlib import Path
from dotenv import load_dotenv
load_dotenv(".env.test", override=True)
load_dotenv(".env.lib", override=True)
load_dotenv(".env.secrets", override=True)
logging.basicConfig(level=logging.WARNING)
sys.path.insert(0, str(Path(__file__).parent))
from _session_guard import guarded_session # noqa: E402
from arodonata import ArodonataClient, ArodonataSettings # noqa: E402
from arodonata.reports import ( # noqa: E402
OwnedSession,
RenderedReportBundle,
RenderOptions,
SessionScope,
)
OUT_DIR = Path(__file__).parent / "_tmp"
def _write_bundle(bundle: RenderedReportBundle, prefix: str) -> None:
OUT_DIR.mkdir(parents=True, exist_ok=True)
if bundle.html is not None:
(OUT_DIR / f"{prefix}.html").write_bytes(bundle.html)
if bundle.markdown is not None:
(OUT_DIR / f"{prefix}.md").write_text(bundle.markdown, encoding="utf-8")
if bundle.json_text is not None:
(OUT_DIR / f"{prefix}.json").write_text(bundle.json_text, encoding="utf-8")
for sess in bundle.report.sessions:
print(
f"{prefix}: session={sess.session_name!r} state={sess.state} "
f"numbering_source={sess.numbering_source} "
f"rules={len(sess.rule_changes)} objects={len(sess.object_changes)}"
)
print(f" -> wrote {OUT_DIR / prefix}.{{html,md,json}}")
async def main() -> None:
"""Create a sample firewall change, generate pending and published evidence reports, and revert."""
settings = ArodonataSettings()
mgmt_name = settings.mgmt_names[0]
domain = os.getenv("TEST_DOMAIN_B", "Domain5")
tag = uuid.uuid4().hex[:6]
options = RenderOptions(
title="Evidence: **RITM-EXAMPLE** — *Firewall Policy Change*",
header_fields={"RITM": "RITM-EXAMPLE", "Requester": "secops"},
)
print(f"[Step 1/5] Capturing baseline revision on {mgmt_name}:{domain}...", flush=True)
async with ArodonataClient(settings=settings) as client, guarded_session(client, mgmt_name, domains=[domain]):
print(f"[Step 2/5] Refreshing rulebase cache for {mgmt_name}:{domain}...", flush=True)
await client.refresh_rulebases(mgmt_name=mgmt_name, domains=[domain], mode="force")
packages = await client.get_policy_packages(mgmt_name, domain, cache_mode="cache")
if not packages or not packages[0].access_layers:
print(f"No policy package/access layer found in {domain}.")
return
layer_name = next(
(lay.layer_name for lay in packages[0].access_layers if lay.layer_name != "Global"),
packages[0].access_layers[0].layer_name,
)
print(
f"[Step 3/5] Opening dedicated session on {mgmt_name}:{domain} and staging host + rule in {layer_name!r}...",
flush=True,
)
sid, server_ip = await client.create_dedicated_session(
mgmt_name,
domain,
session_name=f"ritm-example-{tag}",
session_description="Change report evidence example",
)
try:
s_info = await client.api_call(
mgmt_name,
"show-session",
{},
domain=domain,
sid=sid,
server_ip=server_ip,
)
session_uid = str(s_info.data["uid"])
host_name = f"cr-web-{tag}"
rule_name = f"cr-allow-web-{tag}"
await client.api_call(
mgmt_name,
"add-host",
{"name": host_name, "ipv4-address": "198.51.100.245", "comments": "DMZ web server"},
domain=domain,
sid=sid,
server_ip=server_ip,
)
await client.api_call(
mgmt_name,
"add-access-rule",
{
"layer": layer_name,
"position": "top",
"name": rule_name,
"source": "Any",
"destination": host_name,
"service": "https",
"action": "Accept",
},
domain=domain,
sid=sid,
server_ip=server_ip,
)
# 1. Pending session evidence numbered live via OwnedSession
print("[Step 4/5] Building pending change report via OwnedSession...", flush=True)
owned = OwnedSession(
sid=sid,
server_ip=server_ip,
session_uid=session_uid,
mgmt_name=mgmt_name,
domain=domain,
)
pending_scope = SessionScope(
mgmt_name=mgmt_name,
domain=domain,
session_uids=[session_uid],
owned_sessions=owned,
)
pending_bundle = await client.build_change_report(
[pending_scope],
formats=("html", "markdown", "json"),
options=options,
)
_write_bundle(pending_bundle, "evidence-1-pending")
# 2. Publish the session and refresh rulebase cache
print("[Step 5/5] Publishing session and building published change report...", flush=True)
await client.api_call(
mgmt_name,
"publish",
{},
domain=domain,
sid=sid,
server_ip=server_ip,
wait_for_task=True,
)
finally:
await client.logout_sid(mgmt_name, sid, server_ip=server_ip)
await client.refresh_rulebases(mgmt_name=mgmt_name, domains=[domain], mode="force")
pub_scope = SessionScope(
mgmt_name=mgmt_name,
domain=domain,
session_uids=[session_uid],
)
pub_bundle = await client.build_change_report(
[pub_scope],
formats=("html", "markdown", "json"),
options=options,
)
_write_bundle(pub_bundle, "evidence-2-published")
print(f"Reverting {mgmt_name}:{domain} to baseline revision...", flush=True)
if __name__ == "__main__":
asyncio.run(main())