feat: judge, sign and verify netOrk's images in one place (NetOrk/netork#1390)
CI / Tests, lint (push) Successful in 18s
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.
This commit is contained in:
commit
d139d66c3d
27 files changed
+2085
No files matched your search
@@ -0,0 +1,59 @@
|
||||
"""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())
|
||||
Reference in new issue
Block a user