fix(supply-chain): verify CI and production inputs
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.
This commit is contained in:
Executable
+155
@@ -0,0 +1,155 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user