CI / Tests, lint (push) Successful in 18s
The CIS gate, the SBOM, the cosign signature and the matcher's verdict lived in NetOrk/base-os, the head of the chain. Every link below it -- base-python, engine, ui, satellite, the service images -- needs the same, so the tools move here, and every image repository pins them by commit. - image_tools: cis.py, cis_os.sh, sbom.py and verify.py from base-os. The image's own repository holds its cis.toml and packages.allow; both default to the working directory. - bin/sign.sh signs whatever platforms a reference holds: base-os's index of amd64 and arm64 (EXPECT_PLATFORMS) or a dev image's single manifest (image_tools/platforms.py). - bin/verify-base.sh checks every FROM image of a Dockerfile against cosign.pub before a build; one without a digest is refused (image_tools/dockerfile.py). - bin/install-cosign.sh installs cosign v3.1.3 for amd64 and arm64 runners, checked against the release checksums. - actions/verify-base, judge, sign wrap them for the workflows. - cosign.pub is the public half of the COSIGN_PRIVATE_KEY_B64 org secret, the key rotated on 2026-10-22.
60 lines
1.8 KiB
Python
60 lines
1.8 KiB
Python
"""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 <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())
|