Pin external actions, images, toolchains, native archives, and tvOS engine artifacts; enforce fail-closed CI checks and keep website privacy disclosures aligned with shipped behavior.
156 lines
5.1 KiB
Python
Executable File
156 lines
5.1 KiB
Python
Executable File
#!/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<reference>\S+)",
|
|
re.IGNORECASE,
|
|
)
|
|
IMAGE_RE = re.compile(r"^\s*image\s*:\s*(?P<reference>[^\s#]+)\s*(?:#.*)?$")
|
|
PLATFORMS_RE = re.compile(r"^\s*#\s*Platforms\s*:\s*(?P<platforms>.+?)\s*$", re.IGNORECASE)
|
|
PINNED_REFERENCE_RE = re.compile(
|
|
r"^(?P<name>[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[0-9]+)?"
|
|
r"(?:/[a-z0-9]+(?:[._-][a-z0-9]+)*)*)"
|
|
r":(?P<tag>[A-Za-z0-9_][A-Za-z0-9_.-]{0,127})"
|
|
r"@sha256:(?P<digest>[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())
|