Files
Christian Manivong d139d66c3d
CI / Tests, lint (push) Successful in 18s
feat: judge, sign and verify netOrk's images in one place (NetOrk/netork#1390)
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.
2026-10-22 16:23:00 +02:00

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())