Files
ai-contract-platform/yuqei-infra/scripts/build-infrastructure-image-bundle.py
T

1376 lines
56 KiB
Python

#!/usr/bin/env python3
"""Build a deterministic, offline-verifiable infrastructure image bundle manifest.
The tool is deliberately a pure file validator. It never invokes Docker and
never contacts a registry. Registry manifest bytes and Docker archives must
already be present below ``--artifact-root`` and are verified before a bundle
manifest is emitted.
"""
from __future__ import annotations
import argparse
import gzip
import hashlib
import io
import json
import os
import re
import stat
import sys
import tarfile
from datetime import datetime
from pathlib import Path, PurePosixPath
from typing import Any, Mapping
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from linux_release_common import ( # noqa: E402
REQUIRED_TOOL_VERSIONS,
TRIVY_DB_REPOSITORY_TRANSPORT,
ReleaseValidationError,
load_policy,
parse_rfc3339,
validate_trivy_db_metadata,
validate_trivy_license_scan,
validate_trivy_scan,
)
SOURCE_SET_SCHEMA = "yuqei.infrastructure-image-source-set.v2"
BUNDLE_SCHEMA = "yuqei.infrastructure-image-bundle.v2"
APPLICATION_MANIFEST_SCHEMA = "yuqei.installable-release-manifest.v1"
IMPORT_PLAN_SCHEMA = "yuqei.infrastructure-docker-image-import-plan.v1"
TARGET = {"os": "linux", "architecture": "amd64", "platform": "linux/amd64"}
DOCKER_CLI = "/usr/bin/docker"
DOCKER_HOST = "unix:///var/run/docker.sock"
MAX_JSON_BYTES = 8 * 1024 * 1024
MAX_ARCHIVE_MEMBERS = 100_000
MAX_LAYER_MEMBERS = 200_000
MAX_UNCOMPRESSED_LAYER_BYTES = 8 * 1024 * 1024 * 1024
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
REFERENCE_RE = re.compile(
r"^[a-z0-9](?:[a-z0-9._:/-]{0,510}[a-z0-9])?@sha256:[0-9a-f]{64}$"
)
TAG_RE = re.compile(r"^[a-z0-9](?:[a-z0-9._/-]{0,510}[a-z0-9])?:[^@\s]{1,128}$")
CONTENT_ADDRESSED_MEMBER_RE = re.compile(r"^blobs/sha256/([0-9a-f]{64})$")
LEGACY_CONFIG_MEMBER_RE = re.compile(r"^([0-9a-f]{64})\.json$")
LEGACY_LAYER_MEMBER_RE = re.compile(r"^([0-9a-f]{64})/layer\.tar$")
CONTENT_ADDRESSED_ARCHIVE_PROFILE = "content_addressed_gzip_sha256_v1"
LEGACY_ARCHIVE_PROFILE = "legacy_docker_save_uncompressed_v1"
APPLICATION_SPECS = (
("contract-web", "CONTRACT_WEB_IMAGE"),
("contract-api", "CONTRACT_API_IMAGE"),
("ai-platform-api", "AI_PLATFORM_API_IMAGE"),
("ai-worker", "AI_WORKER_IMAGE"),
)
INFRASTRUCTURE_SPECS = (
("postgres-runtime", "POSTGRES_IMAGE", ("postgres",)),
("postgres-client", "POSTGRES_CLIENT_IMAGE", ("postgres-init-databases",)),
("redis", "REDIS_IMAGE", ("redis", "ai-redis")),
("minio", "MINIO_IMAGE", ("minio",)),
("minio-client", "MINIO_MC_IMAGE", ("minio-backup",)),
("nginx-edge", "NGINX_IMAGE", ("edge",)),
("backup-utility", "BACKUP_UTILITY_IMAGE", ("contract-files-backup",)),
)
ALL_IMAGE_KEYS = tuple(key for _role, key in APPLICATION_SPECS) + tuple(
key for _role, key, _services in INFRASTRUCTURE_SPECS
)
class BundleValidationError(RuntimeError):
"""Raised when a staged image artifact is incomplete or ambiguous."""
def canonical_json_bytes(value: object) -> bytes:
try:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
except (TypeError, ValueError) as exc:
raise BundleValidationError("value cannot be encoded as canonical JSON") from exc
def canonical_sha256(value: object) -> str:
return hashlib.sha256(canonical_json_bytes(value)).hexdigest()
def file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _reject_constant(value: str) -> None:
raise BundleValidationError(f"non-finite JSON value is forbidden: {value}")
def _reject_duplicate_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise BundleValidationError(f"duplicate JSON key is forbidden: {key}")
result[key] = value
return result
def load_json(path: Path, *, label: str) -> dict[str, Any]:
target = _regular_file(path, label=label, max_bytes=MAX_JSON_BYTES)
try:
payload = json.loads(
target.read_text(encoding="utf-8-sig"),
object_pairs_hook=_reject_duplicate_pairs,
parse_constant=_reject_constant,
)
except BundleValidationError:
raise
except (OSError, UnicodeError, ValueError) as exc:
raise BundleValidationError(f"{label} is not strict UTF-8 JSON") from exc
if not isinstance(payload, dict):
raise BundleValidationError(f"{label} must be a JSON object")
return payload
def _absolute_lexical(path: Path) -> Path:
return Path(os.path.abspath(os.fspath(path)))
def _reject_symlink_components(path: Path, *, label: str) -> None:
absolute = _absolute_lexical(path)
current = Path(absolute.anchor)
for part in absolute.parts[1:]:
current /= part
try:
if current.exists() and current.is_symlink():
raise BundleValidationError(f"{label} contains a symbolic link: {current}")
except OSError as exc:
raise BundleValidationError(f"cannot inspect {label} path") from exc
def _regular_file(path: Path, *, label: str, max_bytes: int | None = None) -> Path:
lexical = _absolute_lexical(path)
_reject_symlink_components(lexical, label=label)
try:
resolved = lexical.resolve(strict=True)
metadata = resolved.stat()
except OSError as exc:
raise BundleValidationError(f"{label} is missing") from exc
if lexical.is_symlink() or not stat.S_ISREG(metadata.st_mode):
raise BundleValidationError(f"{label} must be a regular file")
if metadata.st_nlink != 1:
raise BundleValidationError(f"{label} must not be hard linked")
if max_bytes is not None and (metadata.st_size <= 0 or metadata.st_size > max_bytes):
raise BundleValidationError(f"{label} has an invalid size")
return resolved
def _artifact_root(path: Path) -> Path:
lexical = _absolute_lexical(path)
_reject_symlink_components(lexical, label="artifact root")
try:
resolved = lexical.resolve(strict=True)
except OSError as exc:
raise BundleValidationError("artifact root is missing") from exc
if lexical.is_symlink() or not resolved.is_dir():
raise BundleValidationError("artifact root must be a real directory")
return resolved
def _relative_path(value: object, *, label: str) -> str:
if not isinstance(value, str) or not value or "\\" in value or "\x00" in value:
raise BundleValidationError(f"{label} must be a canonical POSIX relative path")
pure = PurePosixPath(value)
if pure.is_absolute() or pure.as_posix() != value or any(
part in {"", ".", ".."} for part in pure.parts
):
raise BundleValidationError(f"{label} is unsafe")
return value
def _artifact_file(root: Path, value: object, *, label: str) -> tuple[Path, str]:
relative = _relative_path(value, label=label)
lexical = root.joinpath(*PurePosixPath(relative).parts)
target = _regular_file(lexical, label=label)
try:
target.relative_to(root)
except ValueError as exc:
raise BundleValidationError(f"{label} escapes the artifact root") from exc
return target, relative
def _digest(value: object, *, label: str) -> str:
if not isinstance(value, str) or not DIGEST_RE.fullmatch(value):
raise BundleValidationError(f"{label} must be a sha256 digest")
return value
def _reference(value: object, *, label: str) -> str:
if not isinstance(value, str) or not REFERENCE_RE.fullmatch(value):
raise BundleValidationError(f"{label} must be an immutable image reference")
return value
def _tag(value: object, *, label: str) -> str:
if not isinstance(value, str) or not TAG_RE.fullmatch(value):
raise BundleValidationError(f"{label} must contain an explicit image tag")
tag = value.rsplit(":", 1)[1].casefold()
if tag == "latest":
raise BundleValidationError(f"{label} must not use latest")
return value
def _embedded_hash(payload: Mapping[str, Any], field: str, *, label: str) -> None:
expected = payload.get(field)
if not isinstance(expected, str) or not SHA256_RE.fullmatch(expected):
raise BundleValidationError(f"{label} has no valid {field}")
unsigned = {key: value for key, value in payload.items() if key != field}
if canonical_sha256(unsigned) != expected:
raise BundleValidationError(f"{label} payload hash mismatch")
def _file_record(root: Path, value: object, *, label: str) -> tuple[Path, dict[str, Any]]:
if not isinstance(value, dict) or set(value) != {"path", "sha256", "size_bytes"}:
raise BundleValidationError(f"{label} record fields are missing or unknown")
target, relative = _artifact_file(root, value.get("path"), label=label)
size = value.get("size_bytes")
digest = value.get("sha256")
if not isinstance(size, int) or isinstance(size, bool) or size <= 0:
raise BundleValidationError(f"{label} size is invalid")
if not isinstance(digest, str) or not SHA256_RE.fullmatch(digest):
raise BundleValidationError(f"{label} SHA-256 is invalid")
if target.stat().st_size != size or file_sha256(target) != digest:
raise BundleValidationError(f"{label} size or SHA-256 mismatch")
return target, {"path": relative, "sha256": digest, "size_bytes": size}
def _extended_file_record(
root: Path,
value: object,
*,
extra_fields: set[str],
label: str,
) -> tuple[Path, dict[str, Any], Mapping[str, Any]]:
if not isinstance(value, dict) or set(value) != {
"path",
"sha256",
"size_bytes",
*extra_fields,
}:
raise BundleValidationError(f"{label} record fields are missing or unknown")
base = {key: value[key] for key in ("path", "sha256", "size_bytes")}
target, normalized = _file_record(root, base, label=label)
return target, normalized, value
def _validate_database_transport(value: object) -> dict[str, Any]:
if not isinstance(value, dict):
raise BundleValidationError("Trivy database repository transport must be an object")
expected_fields = {
"reference",
"transport",
"schema_version",
"resolved_digest",
"media_type",
"stable_during_download",
"before_download_inspect_sha256",
"after_download_inspect_sha256",
}
if set(value) != expected_fields:
raise BundleValidationError("Trivy database repository transport fields drifted")
if (
value.get("reference") != TRIVY_DB_REPOSITORY_TRANSPORT["reference"]
or value.get("transport") != TRIVY_DB_REPOSITORY_TRANSPORT["transport"]
or value.get("schema_version")
!= TRIVY_DB_REPOSITORY_TRANSPORT["schema_version"]
or value.get("media_type")
not in TRIVY_DB_REPOSITORY_TRANSPORT["allowed_media_types"]
or value.get("stable_during_download") is not True
or not DIGEST_RE.fullmatch(str(value.get("resolved_digest", "")))
or any(
not SHA256_RE.fullmatch(str(value.get(field, "")))
for field in (
"before_download_inspect_sha256",
"after_download_inspect_sha256",
)
)
):
raise BundleValidationError("Trivy database repository transport evidence drifted")
return dict(value)
def _validate_security_gate(
root: Path,
value: object,
*,
application_manifest: Mapping[str, Any],
) -> tuple[dict[str, Any], dict[str, Any], Mapping[str, Any]]:
if not isinstance(value, dict) or set(value) != {
"policy",
"database",
"scanner",
"scanner_version",
}:
raise BundleValidationError("infrastructure security gate fields are missing or unknown")
if (
value.get("scanner") != "trivy"
or value.get("scanner_version") != REQUIRED_TOOL_VERSIONS["trivy"]
):
raise BundleValidationError("infrastructure scanner identity is not pinned")
policy_path, policy_record = _file_record(
root,
value.get("policy"),
label="infrastructure vulnerability policy",
)
application_policy = application_manifest.get("policy")
if (
not isinstance(application_policy, Mapping)
or application_policy.get("sha256") != policy_record["sha256"]
):
raise BundleValidationError(
"infrastructure security policy differs from the application build policy"
)
try:
policy = load_policy(policy_path)
except (ReleaseValidationError, OSError) as exc:
raise BundleValidationError(
f"infrastructure vulnerability policy is invalid: {exc}"
) from exc
database_path, database_record, raw_database_record = _extended_file_record(
root,
value.get("database"),
extra_fields={"validated_at", "repository_transport"},
label="infrastructure Trivy database metadata",
)
try:
validated_at = parse_rfc3339(
raw_database_record.get("validated_at"),
label="infrastructure database.validated_at",
)
database_summary = validate_trivy_db_metadata(
load_json(database_path, label="infrastructure Trivy database metadata"),
policy,
at=validated_at,
)
except (ReleaseValidationError, BundleValidationError) as exc:
raise BundleValidationError(
f"infrastructure Trivy database evidence is invalid: {exc}"
) from exc
database_transport = _validate_database_transport(
raw_database_record.get("repository_transport")
)
return (
policy,
{
"scanner": "trivy",
"scanner_version": REQUIRED_TOOL_VERSIONS["trivy"],
"policy": policy_record,
"database": {
**database_record,
"validated_at": raw_database_record["validated_at"],
"repository_transport": database_transport,
"summary": database_summary,
},
},
load_json(database_path, label="infrastructure Trivy database metadata"),
)
def _safe_tar_name(value: str, *, label: str) -> str:
if "\\" in value or "\x00" in value:
raise BundleValidationError(f"{label} contains an unsafe member path")
pure = PurePosixPath(value)
if pure.is_absolute() or any(part in {"", ".", ".."} for part in pure.parts):
raise BundleValidationError(f"{label} contains an unsafe member path")
return pure.as_posix()
class _HashingLimitedReader:
def __init__(self, stream: Any, *, label: str) -> None:
self._stream = stream
self._label = label
self._digest = hashlib.sha256()
self.size = 0
def read(self, size: int = -1) -> bytes:
data = self._stream.read(size)
if data:
self.size += len(data)
if self.size > MAX_UNCOMPRESSED_LAYER_BYTES:
raise BundleValidationError(f"{self._label} exceeds the uncompressed size limit")
self._digest.update(data)
return data
def sha256_digest(self) -> str:
return "sha256:" + self._digest.hexdigest()
class _PrefixedReader:
"""Replay bytes consumed for format detection without buffering a layer."""
def __init__(self, prefix: bytes, stream: Any) -> None:
self._prefix = io.BytesIO(prefix)
self._stream = stream
def read(self, size: int = -1) -> bytes:
if size is None or size < 0:
return self._prefix.read() + self._stream.read()
if size == 0:
return b""
head = self._prefix.read(size)
if len(head) == size:
return head
return head + self._stream.read(size - len(head))
def _validate_layer_link_target(value: object, *, label: str) -> str:
# Layer links are image-root metadata, not host filesystem paths. Absolute
# symlinks (for example /bin/busybox) and parent-relative symlinks are
# common in genuine Linux base images, so rejecting them creates false
# negatives. Docker remains responsible for safe extraction; this pure-file
# validator only rejects malformed/control-bearing link metadata.
if (
not isinstance(value, str)
or not value
or len(value.encode("utf-8", errors="surrogatepass")) > 4096
or "\x00" in value
or any(ord(character) < 32 or ord(character) == 127 for character in value)
):
raise BundleValidationError(f"{label} contains an invalid link target")
return value
def _content_addressed_member_digest(name: str) -> str | None:
match = CONTENT_ADDRESSED_MEMBER_RE.fullmatch(name)
return f"sha256:{match.group(1)}" if match else None
def _docker_archive_profile(
*,
config_name: str,
expected_config_hex: str,
layers: list[str],
) -> str:
"""Lock one archive layout from its config member and reject mixed layouts."""
content_addressed_config = f"blobs/sha256/{expected_config_hex}"
legacy_config = f"{expected_config_hex}.json"
if config_name == content_addressed_config:
if any(CONTENT_ADDRESSED_MEMBER_RE.fullmatch(layer) is None for layer in layers):
raise BundleValidationError(
"content-addressed Docker archive config requires every layer to use "
"the blobs/sha256/<64lower> layout"
)
return CONTENT_ADDRESSED_ARCHIVE_PROFILE
if config_name == legacy_config and LEGACY_CONFIG_MEMBER_RE.fullmatch(config_name):
if any(LEGACY_LAYER_MEMBER_RE.fullmatch(layer) is None for layer in layers):
raise BundleValidationError(
"legacy Docker archive config requires every layer to use the "
"<64lower>/layer.tar layout"
)
return LEGACY_ARCHIVE_PROFILE
raise BundleValidationError(
"Docker archive config member does not match a supported archive profile"
)
def _inspect_uncompressed_layer(
stream: Any,
*,
expected_diff_id: str,
label: str,
) -> dict[str, Any]:
reader = _HashingLimitedReader(stream, label=label)
names: set[str] = set()
folded: set[str] = set()
member_count = 0
link_count = 0
try:
with tarfile.open(fileobj=reader, mode="r|") as layer:
for member in layer:
member_count += 1
if member_count > MAX_LAYER_MEMBERS:
raise BundleValidationError(f"{label} has too many members")
name = _safe_tar_name(member.name, label=label)
if name in names or name.casefold() in folded:
raise BundleValidationError(f"{label} has duplicate member paths")
names.add(name)
folded.add(name.casefold())
if member.issym():
_validate_layer_link_target(member.linkname, label=label)
link_count += 1
elif member.islnk():
hardlink_target = _validate_layer_link_target(member.linkname, label=label)
_safe_tar_name(hardlink_target, label=f"{label} hardlink")
link_count += 1
elif not (member.isfile() or member.isdir()):
raise BundleValidationError(f"{label} contains a link or special entry")
while reader.read(1024 * 1024):
pass
except BundleValidationError:
raise
except (tarfile.TarError, OSError, EOFError) as exc:
raise BundleValidationError(f"{label} is not a valid uncompressed tar layer") from exc
if member_count == 0:
raise BundleValidationError(f"{label} is an empty tar layer")
observed = reader.sha256_digest()
if observed != expected_diff_id:
raise BundleValidationError(f"{label} diff ID mismatch")
return {
"diff_id": observed,
"uncompressed_size_bytes": reader.size,
"member_count": member_count,
"link_count": link_count,
}
def _inspect_docker_archive_layer(
stream: Any,
*,
expected_diff_id: str,
expected_blob_digest: str | None,
label: str,
) -> dict[str, Any]:
try:
compressed_reader = _HashingLimitedReader(stream, label=f"{label} archive blob")
prefix = compressed_reader.read(2)
replay = _PrefixedReader(prefix, compressed_reader)
if prefix == b"\x1f\x8b":
with gzip.GzipFile(fileobj=replay, mode="rb") as expanded:
summary = {
"archive_encoding": "gzip",
**_inspect_uncompressed_layer(
expanded,
expected_diff_id=expected_diff_id,
label=label,
),
}
else:
summary = {
"archive_encoding": "uncompressed",
**_inspect_uncompressed_layer(
replay,
expected_diff_id=expected_diff_id,
label=label,
),
}
while compressed_reader.read(1024 * 1024):
pass
observed_blob_digest = compressed_reader.sha256_digest()
if expected_blob_digest is not None and observed_blob_digest != expected_blob_digest:
raise BundleValidationError(f"{label} content-addressed blob digest mismatch")
return {
**summary,
"archive_blob_digest": observed_blob_digest,
"archive_blob_size_bytes": compressed_reader.size,
"content_addressed": expected_blob_digest is not None,
}
except BundleValidationError:
raise
except (OSError, EOFError, gzip.BadGzipFile) as exc:
raise BundleValidationError(f"{label} is not a valid gzip layer") from exc
def _inspect_registry_blob(
path: Path,
*,
media_type: str,
expected_diff_id: str,
label: str,
) -> dict[str, Any]:
try:
with path.open("rb") as raw:
if media_type in {
"application/vnd.docker.image.rootfs.diff.tar.gzip",
"application/vnd.oci.image.layer.v1.tar+gzip",
}:
with gzip.GzipFile(fileobj=raw, mode="rb") as expanded:
summary = _inspect_uncompressed_layer(
expanded,
expected_diff_id=expected_diff_id,
label=label,
)
elif media_type in {
"application/vnd.oci.image.layer.v1.tar",
"application/vnd.docker.image.rootfs.diff.tar",
}:
summary = _inspect_uncompressed_layer(
raw,
expected_diff_id=expected_diff_id,
label=label,
)
else:
raise BundleValidationError(f"{label} uses an unsupported layer media type")
except BundleValidationError:
raise
except (OSError, EOFError, gzip.BadGzipFile) as exc:
raise BundleValidationError(f"{label} cannot be decompressed") from exc
return {"media_type": media_type, **summary}
def inspect_docker_archive(
path: Path,
*,
expected_repo_tag: str,
expected_config_digest: str,
) -> dict[str, Any]:
"""Inspect a Docker save archive without extracting it or invoking Docker."""
try:
with tarfile.open(path, mode="r:*") as archive:
members = archive.getmembers()
if not members or len(members) > MAX_ARCHIVE_MEMBERS:
raise BundleValidationError("Docker archive has an invalid member count")
by_name: dict[str, tarfile.TarInfo] = {}
folded: set[str] = set()
for member in members:
name = _safe_tar_name(member.name, label="Docker archive")
if name in by_name or name.casefold() in folded:
raise BundleValidationError("Docker archive has duplicate member paths")
folded.add(name.casefold())
if not (member.isfile() or member.isdir()):
raise BundleValidationError("Docker archive contains a link or special entry")
by_name[name] = member
manifest_member = by_name.get("manifest.json")
if manifest_member is None or not manifest_member.isfile():
raise BundleValidationError("Docker archive has no manifest.json")
stream = archive.extractfile(manifest_member)
if stream is None:
raise BundleValidationError("Docker archive manifest cannot be read")
manifest_raw = stream.read(MAX_JSON_BYTES + 1)
if len(manifest_raw) > MAX_JSON_BYTES:
raise BundleValidationError("Docker archive manifest is too large")
try:
manifest = json.loads(
manifest_raw.decode("utf-8"),
object_pairs_hook=_reject_duplicate_pairs,
parse_constant=_reject_constant,
)
except BundleValidationError:
raise
except (UnicodeError, ValueError) as exc:
raise BundleValidationError("Docker archive manifest is invalid") from exc
if not isinstance(manifest, list) or len(manifest) != 1 or not isinstance(manifest[0], dict):
raise BundleValidationError("Docker archive must contain exactly one image")
item = manifest[0]
if set(item) != {"Config", "RepoTags", "Layers"}:
raise BundleValidationError("Docker archive manifest fields are invalid")
repo_tags = item.get("RepoTags")
if repo_tags != [expected_repo_tag]:
raise BundleValidationError("Docker archive repository tag is not exact")
config_name = _safe_tar_name(item.get("Config"), label="Docker config")
config_member = by_name.get(config_name)
if config_member is None or not config_member.isfile():
raise BundleValidationError("Docker archive config is missing")
config_stream = archive.extractfile(config_member)
if config_stream is None:
raise BundleValidationError("Docker archive config cannot be read")
config_raw = config_stream.read(MAX_JSON_BYTES + 1)
if len(config_raw) > MAX_JSON_BYTES:
raise BundleValidationError("Docker archive config is too large")
observed_config_digest = "sha256:" + hashlib.sha256(config_raw).hexdigest()
if observed_config_digest != expected_config_digest:
raise BundleValidationError("Docker archive config digest mismatch")
expected_config_hex = expected_config_digest.removeprefix("sha256:")
try:
config = json.loads(
config_raw.decode("utf-8"),
object_pairs_hook=_reject_duplicate_pairs,
parse_constant=_reject_constant,
)
except BundleValidationError:
raise
except (UnicodeError, ValueError) as exc:
raise BundleValidationError("Docker archive config JSON is invalid") from exc
if not isinstance(config, dict) or config.get("os") != "linux" or config.get("architecture") != "amd64":
raise BundleValidationError("Docker archive platform must be linux/amd64")
rootfs = config.get("rootfs")
diff_ids = rootfs.get("diff_ids") if isinstance(rootfs, dict) else None
if (
not isinstance(rootfs, dict)
or rootfs.get("type") != "layers"
or not isinstance(diff_ids, list)
or not diff_ids
or any(not isinstance(item, str) or not DIGEST_RE.fullmatch(item) for item in diff_ids)
):
raise BundleValidationError("Docker archive config rootfs diff IDs are invalid")
layers = item.get("Layers")
if not isinstance(layers, list) or not layers or any(not isinstance(layer, str) for layer in layers):
raise BundleValidationError("Docker archive layer inventory is invalid")
normalized_layers = [_safe_tar_name(layer, label="Docker layer") for layer in layers]
if len(normalized_layers) != len(set(normalized_layers)):
raise BundleValidationError("Docker archive repeats a layer path")
archive_profile = _docker_archive_profile(
config_name=config_name,
expected_config_hex=expected_config_hex,
layers=normalized_layers,
)
if any(name not in by_name or not by_name[name].isfile() for name in normalized_layers):
raise BundleValidationError("Docker archive is missing a declared layer")
if len(normalized_layers) != len(diff_ids):
raise BundleValidationError("Docker archive layer order/count differs from config diff IDs")
layer_inspections: list[dict[str, Any]] = []
for sequence, (name, diff_id) in enumerate(
zip(normalized_layers, diff_ids, strict=True), start=1
):
layer_stream = archive.extractfile(by_name[name])
if layer_stream is None:
raise BundleValidationError("Docker archive layer cannot be read")
expected_blob_digest = (
_content_addressed_member_digest(name)
if archive_profile == CONTENT_ADDRESSED_ARCHIVE_PROFILE
else None
)
layer_inspection = _inspect_docker_archive_layer(
layer_stream,
expected_diff_id=diff_id,
expected_blob_digest=expected_blob_digest,
label=f"Docker layer {sequence}",
)
if (
archive_profile == CONTENT_ADDRESSED_ARCHIVE_PROFILE
and layer_inspection["archive_encoding"] != "gzip"
):
raise BundleValidationError(
"content-addressed Docker archive layers must be gzip-compressed blobs"
)
if (
archive_profile == LEGACY_ARCHIVE_PROFILE
and layer_inspection["archive_encoding"] != "uncompressed"
):
raise BundleValidationError(
"legacy Docker archive layers must be uncompressed layer.tar files"
)
layer_inspections.append(
{
"sequence": sequence,
"path": name,
**layer_inspection,
}
)
except (tarfile.TarError, OSError) as exc:
raise BundleValidationError("Docker archive cannot be inspected") from exc
return {
"archive_profile": archive_profile,
"config_digest": observed_config_digest,
"config_size_bytes": len(config_raw),
"expected_repo_tag": expected_repo_tag,
"platform": TARGET["platform"],
"layer_count": len(normalized_layers),
"diff_ids": list(diff_ids),
"layers": layer_inspections,
}
def _validate_registry_manifest(
path: Path,
*,
expected_manifest_digest: str,
expected_config_digest: str,
) -> dict[str, Any]:
raw = path.read_bytes()
if "sha256:" + hashlib.sha256(raw).hexdigest() != expected_manifest_digest:
raise BundleValidationError("registry manifest digest mismatch")
try:
payload = json.loads(
raw.decode("utf-8"),
object_pairs_hook=_reject_duplicate_pairs,
parse_constant=_reject_constant,
)
except BundleValidationError:
raise
except (UnicodeError, ValueError) as exc:
raise BundleValidationError("registry manifest is invalid JSON") from exc
if not isinstance(payload, dict):
raise BundleValidationError("registry manifest must be an object")
media_type = payload.get("mediaType")
if media_type not in {
"application/vnd.docker.distribution.manifest.v2+json",
"application/vnd.oci.image.manifest.v1+json",
}:
raise BundleValidationError("registry artifact is not a resolved image manifest")
config = payload.get("config")
if (
not isinstance(config, dict)
or config.get("digest") != expected_config_digest
or not isinstance(config.get("size"), int)
or isinstance(config.get("size"), bool)
or config["size"] <= 0
):
raise BundleValidationError("registry manifest config digest mismatch")
layers = payload.get("layers")
if not isinstance(layers, list) or not layers:
raise BundleValidationError("registry manifest layer inventory is missing")
normalized_layers: list[dict[str, Any]] = []
for layer in layers:
if (
not isinstance(layer, dict)
or not DIGEST_RE.fullmatch(str(layer.get("digest", "")))
or not isinstance(layer.get("size"), int)
or isinstance(layer.get("size"), bool)
or layer["size"] <= 0
or not isinstance(layer.get("mediaType"), str)
):
raise BundleValidationError("registry manifest has an invalid layer digest")
normalized_layers.append(
{
"digest": layer["digest"],
"size_bytes": layer["size"],
"media_type": layer["mediaType"],
}
)
return {
"media_type": media_type,
"config_size_bytes": config["size"],
"layer_count": len(layers),
"layers": normalized_layers,
}
def _validate_sbom(
path: Path,
*,
role: str,
expected_manifest_digest: str,
) -> dict[str, Any]:
payload = load_json(path, label=f"{role} SBOM")
if payload.get("spdxVersion") != "SPDX-2.3" or payload.get("SPDXID") != "SPDXRef-DOCUMENT":
raise BundleValidationError(f"{role} SBOM must be an SPDX-2.3 document")
described = payload.get("documentDescribes")
packages = payload.get("packages")
if not isinstance(described, list) or len(described) != 1 or not isinstance(packages, list) or not packages:
raise BundleValidationError(f"{role} SBOM must identify exactly one image root package")
package_ids: set[str] = set()
root_packages: list[Mapping[str, Any]] = []
for package in packages:
if not isinstance(package, dict):
raise BundleValidationError(f"{role} SBOM package inventory is invalid")
spdx_id = package.get("SPDXID")
if not isinstance(spdx_id, str) or not spdx_id or spdx_id in package_ids:
raise BundleValidationError(f"{role} SBOM package identity is invalid")
package_ids.add(spdx_id)
if spdx_id == described[0]:
root_packages.append(package)
if len(root_packages) != 1 or root_packages[0].get("versionInfo") != expected_manifest_digest:
raise BundleValidationError(f"{role} SBOM root does not bind the image manifest digest")
creation = payload.get("creationInfo")
creators = creation.get("creators") if isinstance(creation, dict) else None
if not isinstance(creators, list) or not any("syft" in str(item).casefold() for item in creators):
raise BundleValidationError(f"{role} SBOM generator identity is missing")
return {
"spdx_version": "SPDX-2.3",
"package_count": len(packages),
"root_package_spdx_id": described[0],
"root_manifest_digest": expected_manifest_digest,
}
def _validate_vulnerability_scan(
path: Path,
*,
role: str,
expected_config_digest: str,
expected_artifact_basename: str,
policy: dict[str, Any],
evaluated_at: datetime,
) -> dict[str, Any]:
payload = load_json(path, label=f"{role} vulnerability scan")
try:
summary = validate_trivy_scan(
payload,
policy,
expected_config_digest=expected_config_digest,
expected_artifact_basename=expected_artifact_basename,
role=role,
at=evaluated_at,
)
return {**summary, "config_digest": expected_config_digest}
except ReleaseValidationError as exc:
raise BundleValidationError(
f"{role} vulnerability gate failed: {exc}"
) from exc
def _validate_license_scan(
path: Path,
*,
role: str,
expected_config_digest: str,
expected_artifact_basename: str,
) -> dict[str, Any]:
payload = load_json(path, label=f"{role} license scan")
try:
summary = validate_trivy_license_scan(
payload,
expected_config_digest=expected_config_digest,
expected_artifact_basename=expected_artifact_basename,
)
except ReleaseValidationError as exc:
raise BundleValidationError(f"{role} license gate failed: {exc}") from exc
return {**summary, "config_digest": expected_config_digest}
def _validate_application_manifest(root: Path, payload: Mapping[str, Any]) -> tuple[dict[str, Any], list[dict[str, Any]]]:
if payload.get("schema_version") != APPLICATION_MANIFEST_SCHEMA:
raise BundleValidationError("unsupported application installable manifest schema")
_embedded_hash(payload, "manifest_payload_sha256", label="application manifest")
candidate = payload.get("candidate")
if not isinstance(candidate, dict):
raise BundleValidationError("application manifest candidate is missing")
images = payload.get("images")
if not isinstance(images, list) or len(images) != len(APPLICATION_SPECS):
raise BundleValidationError("application manifest must contain four images")
overrides: dict[str, str] = {}
normalized: list[dict[str, Any]] = []
for raw, (role, environment_key) in zip(images, APPLICATION_SPECS, strict=True):
if not isinstance(raw, dict) or raw.get("role") != role or raw.get("environment_key") != environment_key:
raise BundleValidationError("application image role or order drifted")
if raw.get("platform") != TARGET["platform"]:
raise BundleValidationError("application image platform must be linux/amd64")
config_digest = _digest(raw.get("config_digest"), label=f"{role} config digest")
manifest_digest = _digest(raw.get("manifest_digest"), label=f"{role} manifest digest")
archive_path, archive_record = _file_record(
root, raw.get("docker_archive"), label=f"{role} Docker archive"
)
expected_tag = _tag(raw.get("image_ref"), label=f"{role} image tag")
inspected = inspect_docker_archive(
archive_path,
expected_repo_tag=expected_tag,
expected_config_digest=config_digest,
)
overrides[environment_key] = config_digest
normalized.append(
{
"role": role,
"environment_key": environment_key,
"manifest_digest": manifest_digest,
"config_digest": config_digest,
"docker_archive": archive_record,
"expected_repo_tag": expected_tag,
"archive_profile": inspected["archive_profile"],
"archive_inspection": inspected,
}
)
return dict(candidate), normalized
def build_bundle(
*,
artifact_root: Path,
source_set: Mapping[str, Any],
application_manifest: Mapping[str, Any],
) -> dict[str, Any]:
root = _artifact_root(artifact_root)
if source_set.get("schema_version") != SOURCE_SET_SCHEMA:
raise BundleValidationError("unsupported infrastructure source-set schema")
_embedded_hash(source_set, "source_set_payload_sha256", label="infrastructure source set")
if set(source_set) != {
"schema_version",
"candidate",
"target",
"security_gate",
"images",
"source_set_payload_sha256",
}:
raise BundleValidationError("infrastructure source-set fields are missing or unknown")
if source_set.get("target") != TARGET:
raise BundleValidationError("infrastructure source target must be linux/amd64")
source_candidate = source_set.get("candidate")
if not isinstance(source_candidate, dict) or set(source_candidate) != {"commit", "tag"}:
raise BundleValidationError("infrastructure source candidate is invalid")
app_candidate, application_images = _validate_application_manifest(
root, application_manifest
)
if app_candidate.get("commit") != source_candidate.get("commit") or app_candidate.get("tag") != source_candidate.get("tag"):
raise BundleValidationError("application and infrastructure candidate bindings differ")
policy, security_gate, database_payload = _validate_security_gate(
root,
source_set.get("security_gate"),
application_manifest=application_manifest,
)
raw_images = source_set.get("images")
if not isinstance(raw_images, list) or len(raw_images) != len(INFRASTRUCTURE_SPECS):
raise BundleValidationError("infrastructure source set must contain exactly seven images")
seen_archives: set[str] = set()
seen_configs: set[str] = set()
seen_security_artifacts: set[str] = {
security_gate["policy"]["path"],
security_gate["database"]["path"],
}
seen_registry_blobs: set[str] = set()
infrastructure_images: list[dict[str, Any]] = []
import_images: list[dict[str, Any]] = []
aggregate_vulnerability_counts = {
bucket: {
severity: 0
for severity in policy["vulnerability_scan"]["thresholds"]
}
for bucket in ("total", "exempted", "blocking")
}
applied_waiver_ids: set[str] = set()
overrides = {item["environment_key"]: item["config_digest"] for item in application_images}
for sequence, (raw, (role, environment_key, services)) in enumerate(
zip(raw_images, INFRASTRUCTURE_SPECS, strict=True), start=1
):
if not isinstance(raw, dict):
raise BundleValidationError("infrastructure image entries must be objects")
expected_fields = {
"role",
"environment_key",
"services",
"canonical_source_tag",
"canonical_reference",
"transport_reference",
"manifest_digest",
"config_digest",
"registry_manifest",
"registry_blobs",
"docker_archive",
"expected_repo_tag",
"sbom",
"vulnerability_scan",
"license_scan",
}
if set(raw) != expected_fields:
raise BundleValidationError(f"{role} fields are missing or unknown")
if raw.get("role") != role or raw.get("environment_key") != environment_key or raw.get("services") != list(services):
raise BundleValidationError("infrastructure image role, service mapping, or order drifted")
source_tag = _tag(raw.get("canonical_source_tag"), label=f"{role} source tag")
expected_tag = _tag(raw.get("expected_repo_tag"), label=f"{role} offline tag")
manifest_digest = _digest(raw.get("manifest_digest"), label=f"{role} manifest digest")
config_digest = _digest(raw.get("config_digest"), label=f"{role} config digest")
canonical_reference = _reference(raw.get("canonical_reference"), label=f"{role} canonical reference")
transport_reference = _reference(raw.get("transport_reference"), label=f"{role} transport reference")
if canonical_reference.rsplit("@", 1)[1] != manifest_digest or transport_reference.rsplit("@", 1)[1] != manifest_digest:
raise BundleValidationError(f"{role} references do not match the resolved manifest digest")
if canonical_reference.split("@", 1)[0] != source_tag.rsplit(":", 1)[0]:
raise BundleValidationError(f"{role} canonical source repository drifted")
manifest_path, manifest_record = _file_record(
root, raw.get("registry_manifest"), label=f"{role} registry manifest"
)
registry = _validate_registry_manifest(
manifest_path,
expected_manifest_digest=manifest_digest,
expected_config_digest=config_digest,
)
archive_path, archive_record = _file_record(
root, raw.get("docker_archive"), label=f"{role} Docker archive"
)
if archive_record["path"] in seen_archives or config_digest in seen_configs:
raise BundleValidationError("infrastructure archives and config digests must be unique")
seen_archives.add(archive_record["path"])
seen_configs.add(config_digest)
inspected = inspect_docker_archive(
archive_path,
expected_repo_tag=expected_tag,
expected_config_digest=config_digest,
)
if registry["config_size_bytes"] != inspected["config_size_bytes"]:
raise BundleValidationError(
f"{role} registry manifest config size differs from the Docker archive"
)
raw_blobs = raw.get("registry_blobs")
if not isinstance(raw_blobs, list) or len(raw_blobs) != registry["layer_count"]:
raise BundleValidationError(f"{role} registry blob inventory is incomplete")
registry_blobs: list[dict[str, Any]] = []
for layer_sequence, (raw_blob, layer_identity, diff_id) in enumerate(
zip(raw_blobs, registry["layers"], inspected["diff_ids"], strict=True),
start=1,
):
blob_path, blob_record = _file_record(
root,
raw_blob,
label=f"{role} registry layer blob {layer_sequence}",
)
if blob_record["path"] in seen_registry_blobs:
raise BundleValidationError("registry layer blob paths must be unique")
seen_registry_blobs.add(blob_record["path"])
if (
"sha256:" + blob_record["sha256"] != layer_identity["digest"]
or blob_record["size_bytes"] != layer_identity["size_bytes"]
):
raise BundleValidationError(
f"{role} registry layer blob digest/size differs from the manifest"
)
registry_blobs.append(
{
"sequence": layer_sequence,
**blob_record,
"manifest_digest": layer_identity["digest"],
"manifest_size_bytes": layer_identity["size_bytes"],
"inspection": _inspect_registry_blob(
blob_path,
media_type=layer_identity["media_type"],
expected_diff_id=diff_id,
label=f"{role} registry layer blob {layer_sequence}",
),
}
)
sbom_path, sbom_record = _file_record(
root, raw.get("sbom"), label=f"{role} SBOM"
)
(
vulnerability_path,
vulnerability_record,
raw_vulnerability_record,
) = _extended_file_record(
root,
raw.get("vulnerability_scan"),
extra_fields={
"scanner",
"scanner_version",
"waiver_evaluated_at",
"passed",
},
label=f"{role} vulnerability scan",
)
license_path, license_record, raw_license_record = _extended_file_record(
root,
raw.get("license_scan"),
extra_fields={"scanner", "scanner_version"},
label=f"{role} license scan",
)
security_paths = {
sbom_record["path"],
vulnerability_record["path"],
license_record["path"],
}
if len(security_paths) != 3 or seen_security_artifacts.intersection(security_paths):
raise BundleValidationError("infrastructure security artifact paths must be unique")
seen_security_artifacts.update(security_paths)
sbom_summary = _validate_sbom(
sbom_path,
role=role,
expected_manifest_digest=manifest_digest,
)
if (
raw_vulnerability_record.get("scanner") != "trivy"
or raw_vulnerability_record.get("scanner_version")
!= REQUIRED_TOOL_VERSIONS["trivy"]
or raw_vulnerability_record.get("passed") is not True
or raw_license_record.get("scanner") != "trivy"
or raw_license_record.get("scanner_version")
!= REQUIRED_TOOL_VERSIONS["trivy"]
):
raise BundleValidationError(f"{role} scanner identity or pass state drifted")
try:
scan_evaluated_at = parse_rfc3339(
raw_vulnerability_record.get("waiver_evaluated_at"),
label=f"{role} vulnerability waiver_evaluated_at",
)
validate_trivy_db_metadata(
database_payload,
policy,
at=scan_evaluated_at,
)
except ReleaseValidationError as exc:
raise BundleValidationError(
f"{role} vulnerability scan database/freshness evidence is invalid: {exc}"
) from exc
vulnerability_summary = _validate_vulnerability_scan(
vulnerability_path,
role=role,
expected_config_digest=config_digest,
expected_artifact_basename=archive_path.name,
policy=policy,
evaluated_at=scan_evaluated_at,
)
license_summary = _validate_license_scan(
license_path,
role=role,
expected_config_digest=config_digest,
expected_artifact_basename=archive_path.name,
)
for bucket in aggregate_vulnerability_counts:
for severity in aggregate_vulnerability_counts[bucket]:
aggregate_vulnerability_counts[bucket][severity] += (
vulnerability_summary["counts"][bucket][severity]
)
applied_waiver_ids.update(vulnerability_summary["waiver_ids"])
image = {
"sequence": sequence,
"role": role,
"environment_key": environment_key,
"services": list(services),
"platform": TARGET["platform"],
"canonical_source_tag": source_tag,
"canonical_reference": canonical_reference,
"transport_reference": transport_reference,
"manifest_digest": manifest_digest,
"config_digest": config_digest,
"registry_manifest": manifest_record,
"registry_blobs": registry_blobs,
"docker_archive": archive_record,
"expected_repo_tag": expected_tag,
"archive_profile": inspected["archive_profile"],
"archive_inspection": inspected,
"archive_import_validation": "requires_controlled_docker_load_and_inspect_receipt",
"sbom": sbom_record,
"sbom_summary": sbom_summary,
"vulnerability_scan": {
**vulnerability_record,
"scanner": "trivy",
"scanner_version": REQUIRED_TOOL_VERSIONS["trivy"],
"waiver_evaluated_at": raw_vulnerability_record[
"waiver_evaluated_at"
],
"passed": True,
},
"vulnerability_scan_summary": vulnerability_summary,
"license_scan": {
**license_record,
"scanner": "trivy",
"scanner_version": REQUIRED_TOOL_VERSIONS["trivy"],
},
"license_scan_summary": license_summary,
}
infrastructure_images.append(image)
import_images.append(
{
"sequence": sequence,
"role": role,
"environment_key": environment_key,
"archive": archive_record,
"archive_profile": inspected["archive_profile"],
"load_command_argv": [
DOCKER_CLI,
"--host",
DOCKER_HOST,
"image",
"load",
"--input",
archive_record["path"],
],
"tag_command_argv": [
DOCKER_CLI,
"--host",
DOCKER_HOST,
"image",
"tag",
config_digest,
expected_tag,
],
"inspect_command_argv": [
DOCKER_CLI,
"--host",
DOCKER_HOST,
"image",
"inspect",
"--format",
"{{json .}}",
expected_tag,
],
"expected_loaded_image_id": config_digest,
"expected_manifest_digest": manifest_digest,
"expected_platform": TARGET["platform"],
"offline_compose_override": config_digest,
"file_validation_status": "passed",
"controlled_docker_load_and_inspect_receipt_required": True,
}
)
overrides[environment_key] = config_digest
if tuple(overrides) != ALL_IMAGE_KEYS or set(overrides) != set(ALL_IMAGE_KEYS):
raise BundleValidationError("offline override map does not cover exactly all eleven images")
security_gate.update(
{
"thresholds": policy["vulnerability_scan"]["thresholds"],
"exemptions": policy["vulnerability_scan"]["exemptions"],
"vulnerability_counts": aggregate_vulnerability_counts,
"waiver_ids": sorted(applied_waiver_ids),
"passed": True,
"fail_closed": True,
}
)
unsigned: dict[str, Any] = {
"schema_version": BUNDLE_SCHEMA,
"candidate": {"commit": source_candidate["commit"], "tag": source_candidate["tag"]},
"target": TARGET,
"application_images": application_images,
"infrastructure_images": infrastructure_images,
"security_gate": security_gate,
"offline_compose_overrides": overrides,
"all_release_image_keys": list(ALL_IMAGE_KEYS),
"docker_import_plan": {
"schema_version": IMPORT_PLAN_SCHEMA,
"implicit_preloaded_images_allowed": False,
"network_access_required": False,
"docker_required_for_validation": False,
"controlled_docker_load_and_inspect_receipt_required": True,
"import_verified": False,
"images": import_images,
},
"claims_not_made": [
"This file-only validator did not contact a registry or invoke Docker.",
"A controlled Docker load/tag/inspect receipt is still required before import is verified.",
"This bundle does not prove installation, snapshot capture, restore, UAT, production, or public-release approval.",
],
}
unsigned["bundle_payload_sha256"] = canonical_sha256(unsigned)
return unsigned
def _write_exclusive(path: Path, payload: Mapping[str, Any]) -> None:
target = _absolute_lexical(path)
if target.exists() or target.is_symlink():
raise BundleValidationError("refusing to overwrite bundle output")
target.parent.mkdir(parents=True, exist_ok=True)
data = canonical_json_bytes(payload) + b"\n"
descriptor = os.open(target, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
try:
with os.fdopen(descriptor, "wb") as stream:
descriptor = -1
stream.write(data)
stream.flush()
os.fsync(stream.fileno())
finally:
if descriptor >= 0:
os.close(descriptor)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--artifact-root", type=Path, required=True)
parser.add_argument("--source-set", type=Path, required=True)
parser.add_argument("--application-manifest", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
payload = build_bundle(
artifact_root=args.artifact_root,
source_set=load_json(args.source_set, label="infrastructure source set"),
application_manifest=load_json(
args.application_manifest, label="application installable manifest"
),
)
_write_exclusive(args.output, payload)
except (BundleValidationError, OSError) as exc:
print(
canonical_json_bytes(
{"schema_version": BUNDLE_SCHEMA, "status": "rejected", "error": str(exc)}
).decode("utf-8"),
file=sys.stderr,
)
return 2
print(canonical_json_bytes({"status": "verified", "output": str(args.output)}).decode("utf-8"))
return 0
if __name__ == "__main__":
raise SystemExit(main())