#!/usr/bin/env python3 from __future__ import annotations import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] ACTION_SHA_RE = re.compile( r"^\s*(?:-\s*)?uses:\s*([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_./-]+)?)@([0-9a-f]{40})\s*(?:#.*)?$" ) ACTION_USES_RE = re.compile(r"^\s*(?:-\s*)?uses:\s*(\S+)") DOCKER_FROM_RE = re.compile(r"^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)", re.IGNORECASE) COMPOSE_IMAGE_RE = re.compile(r"^\s*image:\s*[\"']?([^\"'\s]+)") def _workflow_files() -> list[Path]: files: list[Path] = [] for root in (ROOT / ".gitea/workflows", ROOT / ".github/workflows"): if root.is_dir(): files.extend(sorted(root.glob("*.yml"))) files.extend(sorted(root.glob("*.yaml"))) return files def _check_workflows(violations: list[str]) -> None: for path in _workflow_files(): for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): match = ACTION_USES_RE.match(line) if not match: continue value = match.group(1) if value.startswith("./"): continue if value.startswith("docker://"): image = value.removeprefix("docker://") if "@sha256:" not in image: violations.append( f"{path.relative_to(ROOT)}:{number}: docker action is not digest-pinned" ) continue if not ACTION_SHA_RE.match(line): violations.append( f"{path.relative_to(ROOT)}:{number}: action must use a full 40-char commit SHA: {value}" ) def _check_dockerfiles(violations: list[str]) -> None: dockerfiles = ( ROOT / "backend/Dockerfile", ROOT / "frontend/Dockerfile", ROOT / "deploy/unraid/Dockerfile.all-in-one", ) for path in dockerfiles: if not path.is_file(): continue for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): match = DOCKER_FROM_RE.match(line) if not match: continue image = match.group(1) if image == "scratch": continue if "${" in image: violations.append( f"{path.relative_to(ROOT)}:{number}: dynamic external base image is not allowed" ) continue if "@sha256:" not in image: violations.append( f"{path.relative_to(ROOT)}:{number}: base image must be digest-pinned: {image}" ) def _check_compose(violations: list[str]) -> None: for relative in ("docker-compose.yml", "docker-compose.unraid.yml"): path = ROOT / relative if not path.is_file(): continue for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): match = COMPOSE_IMAGE_RE.match(line) if not match: continue image = match.group(1) if image.startswith(("geointel-", "${")): continue if "@sha256:" not in image: violations.append( f"{relative}:{number}: external image must be digest-pinned: {image}" ) def main() -> int: violations: list[str] = [] _check_workflows(violations) _check_dockerfiles(violations) _check_compose(violations) if violations: print("Supply-chain pin violations:", file=sys.stderr) for violation in violations: print(f" - {violation}", file=sys.stderr) return 1 print("Supply-chain pins passed.") return 0 if __name__ == "__main__": raise SystemExit(main())