103 lines
3.9 KiB
Python
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()) |