Files
ansible-testing/methodologies/ansible/scripts/normalize.py
T

103 lines
3.9 KiB
Python

#!/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())