validate_domain_replication.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. #!/usr/bin/env python3
  2. """Validate a self-contained domain replication implementation package."""
  3. from __future__ import annotations
  4. import argparse
  5. import csv
  6. import hashlib
  7. import json
  8. import re
  9. import sys
  10. from pathlib import Path
  11. ROOT = Path(__file__).resolve().parents[1]
  12. if str(ROOT) not in sys.path:
  13. sys.path.insert(0, str(ROOT))
  14. from app.core.governance.domain_replication import ( # noqa: E402
  15. DomainReplicationError,
  16. evaluate_replication_package,
  17. )
  18. _SHA256 = re.compile(r"^[0-9a-f]{64}$")
  19. def _sha256(path: Path) -> str:
  20. return hashlib.sha256(path.read_bytes()).hexdigest()
  21. def _json(path: Path) -> dict:
  22. value = json.loads(path.read_text(encoding="utf-8"))
  23. if not isinstance(value, dict):
  24. raise DomainReplicationError(f"JSON object required: {path}")
  25. return value
  26. def _resolve(package_dir: Path, relative: str) -> Path:
  27. path = (package_dir / relative).resolve()
  28. if package_dir not in path.parents:
  29. raise DomainReplicationError(f"package path escapes package directory: {relative}")
  30. if not path.is_file():
  31. raise DomainReplicationError(f"package file does not exist: {relative}")
  32. return path
  33. def _rows(path: Path) -> list[dict[str, str]]:
  34. with path.open(encoding="utf-8", newline="") as handle:
  35. return list(csv.DictReader(handle))
  36. def validate_package(package_dir: Path) -> dict:
  37. package_dir = package_dir.resolve()
  38. manifest = _json(package_dir / "manifest.json")
  39. files = manifest.get("files")
  40. if not isinstance(files, dict):
  41. raise DomainReplicationError("manifest.files must be an object")
  42. resolved: dict[str, Path] = {}
  43. required_files = ("template", "evidence", "snapshot", "delta")
  44. optional_files = ("acceptance_report",)
  45. for key in (*required_files, *optional_files):
  46. binding = files.get(key)
  47. if key in optional_files and binding is None:
  48. continue
  49. if not isinstance(binding, dict):
  50. raise DomainReplicationError(f"manifest.files.{key} is required")
  51. path = _resolve(package_dir, str(binding.get("path", "")))
  52. expected = str(binding.get("sha256", ""))
  53. if not _SHA256.fullmatch(expected):
  54. raise DomainReplicationError(f"manifest.files.{key}.sha256 is invalid")
  55. actual = _sha256(path)
  56. if actual != expected:
  57. raise DomainReplicationError(
  58. f"package file digest mismatch for {key}: expected={expected}, actual={actual}"
  59. )
  60. resolved[key] = path
  61. report = evaluate_replication_package(
  62. manifest,
  63. _json(resolved["evidence"]),
  64. template=_json(resolved["template"]),
  65. snapshot_rows=_rows(resolved["snapshot"]),
  66. delta_rows=_rows(resolved["delta"]),
  67. )
  68. if "acceptance_report" in resolved:
  69. rendered = json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n"
  70. if resolved["acceptance_report"].read_text(encoding="utf-8") != rendered:
  71. raise DomainReplicationError(
  72. "acceptance report does not match the deterministic package result"
  73. )
  74. return report
  75. def main(argv: list[str] | None = None) -> int:
  76. parser = argparse.ArgumentParser(description=__doc__)
  77. parser.add_argument("--package-dir", type=Path, required=True)
  78. parser.add_argument("--output", default="-")
  79. args = parser.parse_args(argv)
  80. report = validate_package(args.package_dir)
  81. rendered = json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n"
  82. if args.output == "-":
  83. sys.stdout.write(rendered)
  84. else:
  85. output = Path(args.output)
  86. output.parent.mkdir(parents=True, exist_ok=True)
  87. output.write_text(rendered, encoding="utf-8")
  88. return 0
  89. if __name__ == "__main__":
  90. try:
  91. raise SystemExit(main())
  92. except (DomainReplicationError, OSError, ValueError, json.JSONDecodeError) as exc:
  93. print(f"ERROR: {exc}", file=sys.stderr)
  94. raise SystemExit(2) from exc