"""The images a Dockerfile builds on, each pinned by digest. bin/verify-base.sh checks their signatures before the build. A FROM naming a stage of the same file is no image, ``scratch`` is none either, and an image without ``@sha256:`` cannot be verified: a tag can move after the check. python -m image_tools.dockerfile ... """ from __future__ import annotations import argparse import re from pathlib import Path _FROM = re.compile(r"^\s*FROM\s+(.*)$", re.IGNORECASE) _DIGEST = re.compile(r"@sha256:[0-9a-f]{64}$") def _instructions(text: str) -> list[str]: return re.sub(r"\\\s*\n", " ", text).splitlines() def external_bases(text: str) -> list[str]: """Every image the file's FROM lines name, once, in order.""" stages: set[str] = set() bases: list[str] = [] for line in _instructions(text): match = _FROM.match(line) if not match: continue words = [w for w in match.group(1).split() if not w.startswith("--")] ref = words[0] if len(words) >= 3 and words[1].lower() == "as": stages.add(words[2].lower()) if ref.lower() in stages or ref == "scratch": continue if not _DIGEST.search(ref): raise ValueError(f"FROM {ref} is not pinned by digest") if ref not in bases: bases.append(ref) return bases def main() -> int: # pragma: no cover - CLI seam parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("dockerfiles", nargs="+") args = parser.parse_args() seen: list[str] = [] for path in args.dockerfiles: for ref in external_bases(Path(path).read_text()): if ref not in seen: seen.append(ref) print(ref) return 0 if __name__ == "__main__": # pragma: no cover raise SystemExit(main())