#!/usr/bin/env python3 """Convert the existing Ansible compliance report to the common report schema.""" import argparse import json import os from pathlib import Path from typing import Any from jsonschema import Draft202012Validator, FormatChecker STATUS_MAP = {True: "passed", False: "failed", "review": "review", "skipped": "skipped"} def environment(name: str, fallback: str = "") -> str: return os.environ.get(name, fallback) def normalize(source: dict[str, Any], raw_path: Path) -> dict[str, Any]: source_meta = source["meta"] results = [] for result in source.get("results", []): requirement = str(result.get("requirement", "")) standards = [] if requirement: standards.append( { "framework": source_meta.get("standard", "IEC 62443-3-3"), "version": source_meta.get("security_level", ""), "control": requirement, } ) results.append( { "id": str(result["test_id"]), "title": str(result.get("description", result["test_id"])), "description": requirement, "status": STATUS_MAP.get(result.get("passed"), "error"), "severity": str(result.get("severity", "info")).lower(), "category": str(result.get("category", "")), "expected": str(result.get("expected", "")), "observed": str(result.get("actual", "")), "remediation": str(result.get("remediation", "")), "standards": standards, "evidence": [{"type": "text", "name": "Ansible observation", "value": str(result.get("actual", ""))}], } ) counts = dict.fromkeys(("passed", "failed", "errors", "skipped", "review"), 0) for result in results: counter = "errors" if result["status"] == "error" else result["status"] counts[counter] += 1 scored = counts["passed"] + counts["failed"] return { "schema_version": "1.0.0", "run": { "id": environment("CI_PIPELINE_ID", source_meta.get("timestamp", "local")), "started_at": source_meta["timestamp"], "source": "gitlab" if environment("CI") else "local", "pipeline_url": environment("CI_PIPELINE_URL"), "commit_sha": environment("CI_COMMIT_SHA"), }, "project": { "id": environment("TEST_PROJECT_ID", "local"), "name": environment("TEST_PROJECT_NAME", "Local test project"), "environment": environment("TEST_ENVIRONMENT", "test"), "customer": environment("TEST_CUSTOMER"), "location": environment("TEST_LOCATION"), }, "tool": {"id": "ansible", "name": "Ansible", "adapter_version": "1.0.0"}, "target": {"id": source_meta["target"], "type": "managed_host", "groups": []}, "summary": { "total": len(results), **counts, "score": round(counts["passed"] / scored * 100, 2) if scored else 0, }, "results": results, "raw_artifacts": [str(raw_path)], } def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("input", type=Path) parser.add_argument("output", type=Path) parser.add_argument("--schema", type=Path, default=Path("schemas/test-report.schema.json")) args = parser.parse_args() source = json.loads(args.input.read_text(encoding="utf-8")) report = normalize(source, args.input) schema = json.loads(args.schema.read_text(encoding="utf-8")) Draft202012Validator(schema, format_checker=FormatChecker()).validate(report) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") print(f"Normalized {len(report['results'])} Ansible results to {args.output}") return 0 if __name__ == "__main__": raise SystemExit(main())