diff --git a/scripts/f4_linter_linux_provisioning.py b/scripts/f4_linter_linux_provisioning.py index 833de76..c39c0be 100644 --- a/scripts/f4_linter_linux_provisioning.py +++ b/scripts/f4_linter_linux_provisioning.py @@ -25,6 +25,7 @@ from __future__ import annotations +import argparse import importlib import json import sys @@ -122,6 +123,9 @@ LINUX_MANIFEST_SCHEMA_VERSION = 1 LINUX_MANIFEST_FILENAME = "f4-linux-tools.json" +EXIT_OK = 0 +EXIT_INVALID = 1 +EXIT_OFFICIAL_EVIDENCE_DRIFT = 2 LINUX_PACKAGE_MANAGER_ARTIFACT_IDS: tuple[str, ...] = ( "deb", @@ -1356,6 +1360,17 @@ def linux_official_distro_evidence_drift_errors() -> list[str]: return errors +def linux_official_distro_evidence_audit_report() -> dict[str, Any]: + """Return a deterministic release-facing official evidence audit report.""" + drift_errors = linux_official_distro_evidence_drift_errors() + return { + "summary": linux_official_distro_evidence_summary(), + "matrix": list(linux_official_distro_evidence_matrix()), + "drift_errors": drift_errors, + "ok": not drift_errors, + } + + def _generated_official_distro_evidence_record( row: Mapping[str, Any], ) -> tuple[LinuxDistroMappingEvidenceRecord | None, str | None]: @@ -2780,3 +2795,59 @@ def _tool_path_errors( except (KeyError, TypeError, ValueError) as exc: errors.append(f"{prefix}: {exc}") return errors + + +class _ContractArgumentParser(argparse.ArgumentParser): + def error(self, message: str) -> None: # type: ignore[override] + self.print_usage(sys.stderr) + print(f"{self.prog}: error: {message}", file=sys.stderr) + raise SystemExit(EXIT_INVALID) + + +def build_parser() -> argparse.ArgumentParser: + parser = _ContractArgumentParser( + prog="f4_linter_linux_provisioning.py", + description="Audit Linux F4 linter provisioning policy metadata.", + ) + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument( + "--official-evidence-audit", + action="store_true", + help="print the official distro evidence audit report as JSON", + ) + mode.add_argument( + "--check-official-evidence-drift", + action="store_true", + help="fail if official distro evidence drift is detected", + ) + return parser + + +def _official_evidence_drift_check_message(drift_errors: list[str]) -> str: + if not drift_errors: + return "PASS: Linux official distro evidence drift audit clean" + lines = ["FAIL: Linux official distro evidence drift detected"] + lines.extend(f"ERROR: {error}" for error in drift_errors) + return "\n".join(lines) + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + if args.official_evidence_audit: + print( + json.dumps( + linux_official_distro_evidence_audit_report(), + indent=2, + sort_keys=True, + ) + ) + return EXIT_OK + + drift_errors = linux_official_distro_evidence_drift_errors() + message = _official_evidence_drift_check_message(drift_errors) + print(message, file=sys.stderr if drift_errors else sys.stdout) + return EXIT_OFFICIAL_EVIDENCE_DRIFT if drift_errors else EXIT_OK + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/packaging/test_f4_linter_linux_provisioning.py b/tests/packaging/test_f4_linter_linux_provisioning.py index a6c948d..7e56745 100644 --- a/tests/packaging/test_f4_linter_linux_provisioning.py +++ b/tests/packaging/test_f4_linter_linux_provisioning.py @@ -14,6 +14,8 @@ from __future__ import annotations import json +import subprocess +import sys from pathlib import Path from types import ModuleType from typing import Any @@ -118,6 +120,23 @@ def _manifest_distro_evidence( return _manifest_tool(manifest, tool_id)["distro_mapping"]["evidence"] +def _run_linux_provisioning_script( + repo_root: Path, + *args: str, +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [ + sys.executable, + str(repo_root / "scripts/f4_linter_linux_provisioning.py"), + *args, + ], + cwd=repo_root, + capture_output=True, + text=True, + check=False, + ) + + def _complete_verified_evidence(evidence: dict[str, Any]) -> dict[str, Any]: promoted = dict(evidence) promoted.update( @@ -367,6 +386,48 @@ def test_official_distro_evidence_drift_errors_are_empty( assert linux_helper.linux_official_distro_evidence_drift_errors() == [] +def test_official_distro_evidence_audit_report_is_clean( + linux_helper: ModuleType, +) -> None: + report = linux_helper.linux_official_distro_evidence_audit_report() + + assert report["ok"] is True + assert report["drift_errors"] == [] + assert report["summary"]["official_override_count"] == 6 + assert len(report["matrix"]) == 6 + assert [(row["artifact_entry_id"], row["tool_id"]) for row in report["matrix"]] == ( + list(DEBIAN_OFFICIAL_EVIDENCE_KEYS) + ) + + +def test_official_distro_evidence_audit_cli_prints_json_report( + repo_root: Path, +) -> None: + result = _run_linux_provisioning_script(repo_root, "--official-evidence-audit") + + assert result.returncode == 0 + assert result.stderr == "" + report = json.loads(result.stdout) + assert report["ok"] is True + assert report["drift_errors"] == [] + assert report["summary"]["official_override_count"] == 6 + assert report["summary"]["non_debian_override_count"] == 0 + assert len(report["matrix"]) == 6 + + +def test_official_distro_evidence_drift_check_cli_passes( + repo_root: Path, +) -> None: + result = _run_linux_provisioning_script( + repo_root, + "--check-official-evidence-drift", + ) + + assert result.returncode == 0 + assert result.stderr == "" + assert result.stdout == "PASS: Linux official distro evidence drift audit clean\n" + + def test_official_distro_evidence_drift_comparison_rejects_mismatched_url( linux_helper: ModuleType, ) -> None: