#!/usr/bin/env python3 """Require immutable, architecture-declared production container images.""" from __future__ import annotations from dataclasses import dataclass import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent PRODUCTION_DOCKERFILES = (ROOT / "server" / "Dockerfile",) PRODUCTION_COMPOSE_FILES = (ROOT / "server" / "docker-compose.yml",) SUPPORTED_PLATFORMS = frozenset({"linux/amd64", "linux/arm64"}) FROM_RE = re.compile( r"^\s*FROM(?:\s+--platform=(?:\S+))?\s+(?P\S+)", re.IGNORECASE, ) IMAGE_RE = re.compile(r"^\s*image\s*:\s*(?P[^\s#]+)\s*(?:#.*)?$") PLATFORMS_RE = re.compile(r"^\s*#\s*Platforms\s*:\s*(?P.+?)\s*$", re.IGNORECASE) PINNED_REFERENCE_RE = re.compile( r"^(?P[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[0-9]+)?" r"(?:/[a-z0-9]+(?:[._-][a-z0-9]+)*)*)" r":(?P[A-Za-z0-9_][A-Za-z0-9_.-]{0,127})" r"@sha256:(?P[0-9a-f]{64})$" ) @dataclass(frozen=True) class ImageReference: path: Path line_number: int reference: str platforms: frozenset[str] | None def _adjacent_platforms(lines: list[str], reference_index: int) -> frozenset[str] | None: for index in range(reference_index - 1, -1, -1): stripped = lines[index].strip() if not stripped: break if not stripped.startswith("#"): break match = PLATFORMS_RE.match(lines[index]) if match: values = (value.strip() for value in match.group("platforms").split(",")) return frozenset(value for value in values if value) return None def iter_dockerfile_references(path: Path): lines = path.read_text(encoding="utf-8").splitlines() for index, line in enumerate(lines): match = FROM_RE.match(line) if not match: continue reference = match.group("reference") if reference.lower() == "scratch": continue yield ImageReference( path=path, line_number=index + 1, reference=reference, platforms=_adjacent_platforms(lines, index), ) def iter_compose_references(path: Path): lines = path.read_text(encoding="utf-8").splitlines() for index, line in enumerate(lines): match = IMAGE_RE.match(line) if not match: continue yield ImageReference( path=path, line_number=index + 1, reference=match.group("reference").strip("'\""), platforms=_adjacent_platforms(lines, index), ) def validate_image(image: ImageReference) -> list[str]: violations = [] match = PINNED_REFERENCE_RE.fullmatch(image.reference) if not match: violations.append( "external images must use a readable tag and full lowercase sha256 digest" ) elif match.group("tag").lower() == "latest": violations.append("the readable image tag must not be latest") if image.platforms is None: violations.append( "an adjacent '# Platforms:' declaration is required for each external image" ) elif image.platforms != SUPPORTED_PLATFORMS: expected = ", ".join(sorted(SUPPORTED_PLATFORMS)) actual = ", ".join(sorted(image.platforms)) or "none" violations.append(f"platforms must be exactly {expected}; found {actual}") return violations def _display_path(path: Path) -> Path: try: return path.resolve().relative_to(ROOT) except ValueError: return path def check_paths(dockerfiles: list[Path], compose_files: list[Path]) -> list[str]: images = [] for path in dockerfiles: images.extend(iter_dockerfile_references(path)) for path in compose_files: images.extend(iter_compose_references(path)) violations = [] for image in images: for reason in validate_image(image): violations.append( f"{_display_path(image.path)}:{image.line_number}: " f"{image.reference!r}: {reason}" ) return violations def main(argv: list[str] | None = None) -> int: args = list(sys.argv[1:] if argv is None else argv) if args: dockerfiles = [Path(value) for value in args if Path(value).name == "Dockerfile"] compose_files = [Path(value) for value in args if Path(value).name != "Dockerfile"] else: dockerfiles = list(PRODUCTION_DOCKERFILES) compose_files = list(PRODUCTION_COMPOSE_FILES) violations = check_paths(dockerfiles, compose_files) if violations: print("Mutable or malformed production container references:", file=sys.stderr) for violation in violations: print(f" {violation}", file=sys.stderr) return 1 image_count = sum( 1 for path in dockerfiles for _ in iter_dockerfile_references(path) ) + sum(1 for path in compose_files for _ in iter_compose_references(path)) print( f"Production container pins verified ({image_count} external images; " f"platforms: {', '.join(sorted(SUPPORTED_PLATFORMS))})." ) return 0 if __name__ == "__main__": raise SystemExit(main())