#!/usr/bin/env python3 """Verify the integrity and declared limits of a local CI evidence pack. This command performs no network requests. Integrity only confirms that the artifacts match the included manifest; it does not prove root cause, safety, authorization, an order, or payment. """ from __future__ import annotations import argparse import hashlib import json import sys from pathlib import Path EXPECTED_SCHEMA = 'constalabs-ci-evidence-pack-v1' REQUIRED_ARTIFACTS = { 'README.md', 'passing.sanitized.log', 'failing.sanitized.log', 'comparison.md', 'summary.json', } def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open('rb') as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b''): digest.update(chunk) return digest.hexdigest() def safe_artifact_path(root: Path, relative: str) -> Path: candidate = Path(relative) if candidate.is_absolute() or '..' in candidate.parts or len(candidate.parts) != 1: raise ValueError(f'unsafe artifact path: {relative!r}') return root / candidate def verify_pack(root: Path) -> dict: if not root.is_dir(): raise ValueError(f'{root}: pack directory not found') manifest_path = root / 'manifest.json' try: manifest = json.loads(manifest_path.read_text(encoding='utf-8')) except FileNotFoundError as exc: raise ValueError('manifest.json is missing') from exc except (UnicodeDecodeError, json.JSONDecodeError) as exc: raise ValueError('manifest.json is not valid UTF-8 JSON') from exc if manifest.get('schema') != EXPECTED_SCHEMA: raise ValueError('unsupported or missing manifest schema') artifacts = manifest.get('artifacts') if not isinstance(artifacts, list): raise ValueError('manifest artifacts must be a list') names = [] verified = [] for item in artifacts: if not isinstance(item, dict): raise ValueError('each artifact entry must be an object') name = item.get('path') expected_bytes = item.get('bytes') expected_hash = item.get('sha256') if not isinstance(name, str) or not name: raise ValueError('artifact path must be a non-empty string') if name in names: raise ValueError(f'duplicate artifact path: {name}') names.append(name) path = safe_artifact_path(root, name) if not path.is_file(): raise ValueError(f'missing artifact: {name}') if not isinstance(expected_bytes, int) or expected_bytes < 0: raise ValueError(f'invalid byte count for {name}') if not isinstance(expected_hash, str) or len(expected_hash) != 64: raise ValueError(f'invalid SHA-256 declaration for {name}') actual_bytes = path.stat().st_size actual_hash = sha256(path) if actual_bytes != expected_bytes: raise ValueError(f'byte-count mismatch: {name}') if actual_hash != expected_hash.lower(): raise ValueError(f'SHA-256 mismatch: {name}') verified.append({'path': name, 'bytes': actual_bytes, 'sha256': actual_hash}) declared = set(names) missing = sorted(REQUIRED_ARTIFACTS - declared) unexpected = sorted(declared - REQUIRED_ARTIFACTS) if missing: raise ValueError('required artifacts missing: ' + ', '.join(missing)) if unexpected: raise ValueError('unexpected artifacts declared: ' + ', '.join(unexpected)) actual_files = {path.name for path in root.iterdir() if path.is_file()} allowed_files = REQUIRED_ARTIFACTS | {'manifest.json'} undeclared = sorted(actual_files - allowed_files) if undeclared: raise ValueError('undeclared files present: ' + ', '.join(undeclared)) try: summary = json.loads((root / 'summary.json').read_text(encoding='utf-8')) except (UnicodeDecodeError, json.JSONDecodeError) as exc: raise ValueError('summary.json is not valid UTF-8 JSON') from exc if summary.get('network_requests') != 0: raise ValueError('summary does not declare zero network requests') if summary.get('root_cause_proven') is not False: raise ValueError('summary must not claim root cause is proven') return { 'schema': EXPECTED_SCHEMA, 'verified_artifacts': verified, 'integrity_verified': True, 'network_requests': 0, 'root_cause_proven': False, 'order_or_payment_proven': False, } def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('pack_directory', type=Path) args = parser.parse_args() try: result = verify_pack(args.pack_directory) except (OSError, ValueError) as exc: print(f'error: {exc}', file=sys.stderr) return 2 print(json.dumps(result, indent=2, sort_keys=True)) return 0 if __name__ == '__main__': raise SystemExit(main())