#!/usr/bin/env python3 import argparse import datetime as dt import hashlib import json import os import pathlib import re import sys import zipfile import xml.etree.ElementTree as ET from xml.sax.saxutils import escape PROJECT_PACKAGE_PREFIX = "finalfactory.rendezvous." CORE_PROPERTIES_PATH = ( "package/services/metadata/core-properties/core-properties.psmdcp" ) def fail(message: str) -> None: raise SystemExit(message) def load_json(path: pathlib.Path): with path.open(encoding="utf-8") as stream: return json.load(stream) def dependency_inventory(root: pathlib.Path): dependencies = {} for lock_path in sorted(root.glob("**/packages.lock.json")): if any(part in {"bin", "obj", "artifacts"} for part in lock_path.parts): continue lock = load_json(lock_path) for framework in lock.get("dependencies", {}).values(): for package_id, details in framework.items(): resolved = details.get("resolved") if not resolved or package_id.lower().startswith(PROJECT_PACKAGE_PREFIX): continue key = package_id.lower() previous = dependencies.get(key) if previous is not None and previous[1] != resolved: fail( f"Dependency {package_id} resolves to both {previous[1]} and {resolved}." ) dependencies[key] = (package_id, resolved) return [dependencies[key] for key in sorted(dependencies)] def package_license(package_id: str, version: str, overrides): package_root = pathlib.Path( os.environ.get("NUGET_PACKAGES", pathlib.Path.home() / ".nuget" / "packages") ) version_dir = package_root / package_id.lower() / version nuspecs = list(version_dir.glob("*.nuspec")) if len(nuspecs) != 1: fail(f"Expected one restored nuspec for {package_id} {version} in {version_dir}.") root = ET.parse(nuspecs[0]).getroot() license_element = root.find(".//{*}license") if license_element is None or license_element.get("type") != "expression": override = overrides.get(f"{package_id.lower()}/{version}") if override is None: fail(f"{package_id} {version} does not declare an SPDX license expression.") return override["license"] expression = (license_element.text or "").strip() if not expression: fail(f"{package_id} {version} has an empty license expression.") return expression def command_policy(args) -> None: root = pathlib.Path(args.root).resolve() policy = load_json(root / "eng" / "release-policy.json") allowed = set(policy["allowedLicenseExpressions"]) inventory = dependency_inventory(root) observed = [] for package_id, version in inventory: expression = package_license( package_id, version, policy.get("dependencyLicenseOverrides", {}) ) if expression not in allowed: fail( f"Dependency {package_id} {version} uses unapproved license {expression}." ) observed.append({"id": package_id, "version": version, "license": expression}) lite_net_lib = [item for item in observed if item["id"].lower() == "litenetlib"] if lite_net_lib != [{"id": "LiteNetLib", "version": "2.1.4", "license": "MIT"}]: fail(f"LiteNetLib must resolve exactly to the reviewed 2.1.4 release: {lite_net_lib}") print(json.dumps({"dependencies": observed}, indent=2)) def command_audit(args) -> None: document = load_json(pathlib.Path(args.input)) findings = [] def visit(value): if isinstance(value, dict): if value.get("vulnerabilities"): findings.append(value) for child in value.values(): visit(child) elif isinstance(value, list): for child in value: visit(child) visit(document) if findings: fail(f"Locked dependency graph contains known vulnerabilities: {findings}") print("Locked dependency graph has no reported vulnerabilities.") def release_dependency_graph(root: pathlib.Path, server_deps: pathlib.Path): dependencies = {} edges = set() server_document = load_json(server_deps) for package_key, details in server_document.get("libraries", {}).items(): if details.get("type") != "package": continue package_id, version = package_key.rsplit("/", 1) dependencies[package_id.lower()] = (package_id, version) server_target = next(iter(server_document.get("targets", {}).values()), {}) for source_key, details in server_target.items(): source_id = source_key.rsplit("/", 1)[0] source = ( "SPDXRef-Server" if source_id == "FinalFactory.Rendezvous.Server" else source_id.lower() ) for dependency_id in details.get("dependencies", {}): target = { "FinalFactory.Rendezvous.Contracts": "SPDXRef-Contracts", "FinalFactory.Rendezvous.Client": "SPDXRef-Client", }.get(dependency_id, dependency_id.lower()) edges.add((source, target)) lock_roots = ( ("SPDXRef-Client", root / "src/FinalFactory.Rendezvous.Client/packages.lock.json"), ( "SPDXRef-Contracts", root / "src/FinalFactory.Rendezvous.Contracts/packages.lock.json", ), ) for root_id, lock_path in lock_roots: lock = load_json(lock_path) for framework in lock.get("dependencies", {}).values(): for package_id, details in framework.items(): resolved = details.get("resolved") if resolved and not package_id.lower().startswith(PROJECT_PACKAGE_PREFIX): dependencies[package_id.lower()] = (package_id, resolved) if details.get("type") == "Direct": edges.add((root_id, package_id.lower())) source = package_id.lower() for dependency_id in details.get("dependencies", {}): if dependency_id.lower().startswith(PROJECT_PACKAGE_PREFIX): continue edges.add((source, dependency_id.lower())) edges.add(("SPDXRef-Client", "SPDXRef-Contracts")) inventory = [dependencies[key] for key in sorted(dependencies)] return inventory, edges def command_sbom(args) -> None: root = pathlib.Path(args.root).resolve() policy = load_json(root / "eng" / "release-policy.json") overrides = policy.get("dependencyLicenseOverrides", {}) packages = [ { "SPDXID": "SPDXRef-Rendezvous", "name": "FinalFactory.Rendezvous", "versionInfo": args.version, "downloadLocation": "NOASSERTION", "filesAnalyzed": False, "licenseConcluded": "NOASSERTION", "licenseDeclared": "NOASSERTION", "copyrightText": "NOASSERTION", } ] internal_packages = [ ("SPDXRef-Server", "FinalFactory.Rendezvous.Server"), ("SPDXRef-Client", "FinalFactory.Rendezvous.Client"), ("SPDXRef-Contracts", "FinalFactory.Rendezvous.Contracts"), ] for spdx_id, package_id in internal_packages: packages.append( { "SPDXID": spdx_id, "name": package_id, "versionInfo": args.version, "downloadLocation": "NOASSERTION", "filesAnalyzed": False, "licenseConcluded": "NOASSERTION", "licenseDeclared": "NOASSERTION", "copyrightText": "NOASSERTION", } ) inventory, dependency_edges = release_dependency_graph( root, pathlib.Path(args.server_deps) ) dependency_ids = {} for index, (package_id, version) in enumerate( inventory, start=1 ): expression = package_license(package_id, version, overrides) spdx_id = f"SPDXRef-Package-{index}" dependency_ids[package_id.lower()] = spdx_id packages.append( { "SPDXID": spdx_id, "name": package_id, "versionInfo": version, "downloadLocation": "NOASSERTION", "filesAnalyzed": False, "licenseConcluded": expression, "licenseDeclared": expression, "copyrightText": "NOASSERTION", "externalRefs": [ { "referenceCategory": "PACKAGE-MANAGER", "referenceType": "purl", "referenceLocator": f"pkg:nuget/{package_id}@{version}", } ], } ) created = dt.datetime.fromtimestamp( int(args.source_date_epoch), dt.timezone.utc ).strftime("%Y-%m-%dT%H:%M:%SZ") document = { "spdxVersion": "SPDX-2.3", "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": f"FinalFactory.Rendezvous-{args.version}", "documentNamespace": ( "https://git.finalfactory.de/HeiKyu/Rendezvous/sbom/" f"{args.version}/{args.commit}" ), "creationInfo": { "created": created, "creators": ["Tool: eng/release_artifacts.py"], }, "documentDescribes": ["SPDXRef-Rendezvous"], "packages": packages, "relationships": [ { "spdxElementId": "SPDXRef-Rendezvous", "relationshipType": "CONTAINS", "relatedSpdxElement": spdx_id, } for spdx_id, _ in internal_packages ] + [ { "spdxElementId": dependency_ids.get(source, source), "relationshipType": "DEPENDS_ON", "relatedSpdxElement": dependency_ids.get(target, target), } for source, target in sorted(dependency_edges) if source in dependency_ids or source.startswith("SPDXRef-") if target in dependency_ids or target.startswith("SPDXRef-") ], } output = pathlib.Path(args.output) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8") def nuspec_metadata(archive: pathlib.Path): with zipfile.ZipFile(archive) as package: names = package.namelist() nuspecs = [name for name in names if name.endswith(".nuspec")] if len(nuspecs) != 1: fail(f"{archive.name} must contain exactly one nuspec.") root = ET.fromstring(package.read(nuspecs[0])) metadata = root.find(".//{*}metadata") if metadata is None: fail(f"{archive.name} has no package metadata.") values = { child.tag.rsplit("}", 1)[-1]: (child.text or "").strip() for child in metadata if len(child) == 0 } dependencies = { item.get("id"): item.get("version") for item in metadata.findall(".//{*}dependency") } repository = metadata.find("./{*}repository") repository_attributes = {} if repository is None else dict(repository.attrib) return values, dependencies, repository_attributes def verify_checksum_file(release_dir: pathlib.Path, excluded=()) -> None: checksum_path = release_dir / "checksums.sha256" lines = checksum_path.read_text(encoding="utf-8").splitlines() if not lines: fail("checksums.sha256 is empty.") referenced = set() for line in lines: digest, marker, relative = line.partition(" ") if marker != " " or not re.fullmatch(r"[0-9a-f]{64}", digest): fail(f"Malformed checksum line: {line}") target = release_dir / relative if not target.is_file(): fail(f"Checksum references missing artifact: {relative}") actual = hashlib.sha256(target.read_bytes()).hexdigest() if actual != digest: fail(f"Checksum mismatch for {relative}.") referenced.add(relative) expected = { path.name for path in release_dir.iterdir() if path.is_file() and path.name != "checksums.sha256" and path.name not in excluded } if referenced != expected: fail( "Checksum manifest coverage differs. " f"Missing={expected - referenced}; extra={referenced - expected}" ) def command_verify(args) -> None: release_dir = pathlib.Path(args.release_dir).resolve() version = args.version expected = { f"FinalFactory.Rendezvous.Client.{version}.nupkg", f"FinalFactory.Rendezvous.Client.{version}.snupkg", f"FinalFactory.Rendezvous.Contracts.{version}.nupkg", f"FinalFactory.Rendezvous.Contracts.{version}.snupkg", f"FinalFactory.Rendezvous.Server.{version}.linux-x64.tar.gz", f"FinalFactory.Rendezvous.{version}.spdx.json", "CHANGELOG.md", "checksums.sha256", "release-provenance.json", } checksum_exclusions = set() if args.phase in {"publish-ready", "signing-ready", "published"}: expected.add(f"FinalFactory.Rendezvous.Container.{version}.spdx.json") if args.phase in {"signing-ready", "published"}: expected.update({"container-digest.txt", "cosign.pub"}) if args.phase == "published": expected.add("checksums.sha256.bundle") checksum_exclusions.add("checksums.sha256.bundle") actual = {path.name for path in release_dir.iterdir() if path.is_file()} if actual != expected: fail(f"Release artifact set differs. Missing={expected - actual}; extra={actual - expected}") if args.phase in {"publish-ready", "signing-ready", "published"}: container_sbom = load_json( release_dir / f"FinalFactory.Rendezvous.Container.{version}.spdx.json" ) if container_sbom.get("spdxVersion") != "SPDX-2.3": fail("Container inventory must be an SPDX 2.3 document.") if not container_sbom.get("packages"): fail("Container SPDX inventory contains no packages.") policy = load_json(pathlib.Path(args.root) / "eng" / "release-policy.json") forbidden = tuple(fragment.lower() for fragment in policy["forbiddenArtifactNameFragments"]) for artifact in actual: if artifact != "release-provenance.json" and any(fragment in artifact.lower() for fragment in forbidden): fail(f"Forbidden secret-like artifact name: {artifact}") release_sbom = load_json( release_dir / f"FinalFactory.Rendezvous.{version}.spdx.json" ) package_ids = { package.get("name"): package.get("SPDXID") for package in release_sbom.get("packages", []) } relationships = { ( relationship.get("spdxElementId"), relationship.get("relationshipType"), relationship.get("relatedSpdxElement"), ) for relationship in release_sbom.get("relationships", []) } expected_component_edges = { ("SPDXRef-Server", "SPDXRef-Contracts"), ("SPDXRef-Server", package_ids.get("LiteNetLib")), ("SPDXRef-Server", package_ids.get("Microsoft.AspNetCore.OpenApi")), ("SPDXRef-Client", "SPDXRef-Contracts"), ("SPDXRef-Client", package_ids.get("LiteNetLib")), ("SPDXRef-Contracts", package_ids.get("System.Text.Json")), } for source, target in expected_component_edges: if not target or (source, "DEPENDS_ON", target) not in relationships: fail(f"Release SPDX inventory is missing component edge {source} -> {target}.") bcl_id = package_ids.get("Microsoft.Bcl.AsyncInterfaces") if ("SPDXRef-Server", "DEPENDS_ON", bcl_id) in relationships: fail("Release SPDX inventory incorrectly flattens transitive dependencies onto Server.") for package_id in policy["publishedPackages"]: package = release_dir / f"{package_id}.{version}.nupkg" metadata, dependencies, repository = nuspec_metadata(package) if metadata.get("id") != package_id or metadata.get("version") != version: fail(f"{package.name} identity/version metadata is incorrect.") if metadata.get("projectUrl") != "https://git.finalfactory.de/HeiKyu/Rendezvous": fail(f"{package.name} has an incorrect project URL.") if repository.get("url") != "https://git.finalfactory.de/HeiKyu/Rendezvous": fail(f"{package.name} has an incorrect repository URL.") if repository.get("commit") != provenance_commit(release_dir): fail(f"{package.name} does not identify the release commit.") symbol_package = release_dir / f"{package_id}.{version}.snupkg" with zipfile.ZipFile(symbol_package) as symbols: if not any(name.endswith(".pdb") for name in symbols.namelist()): fail(f"{symbol_package.name} contains no portable PDB.") with zipfile.ZipFile(package) as archive: names = set(archive.namelist()) required_entries = { "README.md", "CHANGELOG.md", f"lib/netstandard2.1/{package_id}.dll", } if not required_entries <= names: fail( f"{package.name} is missing required package content: " f"{required_entries - names}" ) forbidden_entries = [ name for name in names if any( fragment in name.lower() for fragment in ( "appsettings", "launchsettings", ".env", "credential", "signing-key", "private-key", ) ) ] if forbidden_entries: fail( f"{package.name} contains deployable configuration or secrets: " f"{forbidden_entries}" ) if package_id.endswith(".Client"): if dependencies.get("LiteNetLib") != "[2.1.4]": fail(f"Client package must pin LiteNetLib exactly to 2.1.4: {dependencies}") contracts_range = dependencies.get("FinalFactory.Rendezvous.Contracts", "") if contracts_range != f"[{version}]": fail(f"Client package does not depend on the matching Contracts version.") provenance = load_json(release_dir / "release-provenance.json") if provenance.get("version") != version: fail("Release provenance does not identify the requested version.") if provenance.get("treeState") != "clean" and not args.allow_dirty: fail("Release provenance must identify a clean tree.") container = provenance.get("containerImage", "") if container.endswith(":latest") or ":latest@" in container or f":{version}" not in container: fail(f"Container reference is mutable or not versioned: {container}") if provenance.get("containerPlatform") != "linux/amd64": fail("Release provenance must pin the linux/amd64 container platform.") for field in ("containerBaseDigests", "releaseBuilderBaseDigests"): images = provenance.get(field, []) if not images or any("@sha256:" not in image or image.endswith(":latest") for image in images): fail(f"Release provenance contains an unpinned build image in {field}: {images}") build_tools = provenance.get("buildTools", []) required_tool_prefixes = ("dotnet", "python=", "tar=", "gzip=", "jq=") observed_tools = [f"dotnet={provenance.get('dotnetSdk', '')}", *build_tools] for prefix in required_tool_prefixes: if not any(tool.startswith(prefix) for tool in observed_tools): fail(f"Release provenance is missing an artifact tool version: {prefix}") if args.phase in {"publish-ready", "signing-ready", "published"}: if not re.fullmatch( r"sha256:[0-9a-f]{64}", provenance.get("containerImageId", "") ): fail("Release provenance is missing the verified local container image ID.") expected_namespace = ( "https://git.finalfactory.de/HeiKyu/Rendezvous/container-sbom/" f"{version}/{provenance_commit(release_dir)}" ) if container_sbom.get("documentNamespace") != expected_namespace: fail("Container SPDX inventory does not identify the release commit.") expected_created = dt.datetime.fromtimestamp( int(provenance.get("sourceDateEpoch", 0)), dt.timezone.utc ).strftime("%Y-%m-%dT%H:%M:%SZ") if container_sbom.get("creationInfo", {}).get("created") != expected_created: fail("Container SPDX timestamp is not normalized to the source epoch.") if args.phase in {"signing-ready", "published"}: digest = (release_dir / "container-digest.txt").read_text( encoding="utf-8" ).strip() if not re.fullmatch( r"git\.finalfactory\.de/heikyu/rendezvous@sha256:[0-9a-f]{64}", digest ): fail(f"Published container digest is invalid: {digest}") if provenance.get("containerDigest") != digest: fail("Published provenance and container digest file differ.") for prefix in ("docker-buildx=", "buildkit="): if not any(tool.startswith(prefix) for tool in build_tools): fail(f"Published provenance is missing container tool version: {prefix}") verify_checksum_file(release_dir, checksum_exclusions) print(f"Verified {args.phase} release artifact set for {version}.") def provenance_commit(release_dir: pathlib.Path) -> str: provenance = load_json(release_dir / "release-provenance.json") commit = provenance.get("commit", "") if not re.fullmatch(r"[0-9a-f]{40}", commit): fail("Release provenance must contain a full Git commit SHA.") return commit def command_consumer(args) -> None: assets = load_json(pathlib.Path(args.assets)) libraries = assets.get("libraries", {}) required = { f"FinalFactory.Rendezvous.Client/{args.version}", f"FinalFactory.Rendezvous.Contracts/{args.version}", "LiteNetLib/2.1.4", } missing = required - set(libraries) if missing: fail(f"Consumer restore is missing exact release dependencies: {missing}") forbidden = [name for name in libraries if name.lower().startswith("litenetlib/1.")] if forbidden: fail(f"Consumer resolved forbidden LiteNetLib 1.x assets: {forbidden}") def command_consumer_config(args) -> None: local_source = escape(str(pathlib.Path(args.local_source).resolve())) configuration = f''' ''' pathlib.Path(args.output).write_text(configuration, encoding="utf-8") def command_source_link(args) -> None: document = load_json(pathlib.Path(args.file)) mappings = document.get("documents", {}) expected = ( "https://git.finalfactory.de/HeiKyu/Rendezvous/raw/commit/" f"{args.commit}/" ) if not mappings or any(not value.startswith(expected) for value in mappings.values()): fail(f"SourceLink mappings do not identify commit {args.commit}: {mappings}") def command_normalize_package(args) -> None: package_path = pathlib.Path(args.package).resolve() if package_path.suffix not in {".nupkg", ".snupkg"}: fail(f"Unsupported NuGet archive extension: {package_path.name}") timestamp = dt.datetime.fromtimestamp( int(args.source_date_epoch), dt.timezone.utc ) zip_timestamp = ( max(timestamp.year, 1980), timestamp.month, timestamp.day, timestamp.hour, timestamp.minute, timestamp.second - (timestamp.second % 2), ) with zipfile.ZipFile(package_path) as source: entries = {name: source.read(name) for name in source.namelist()} if ".signature.p7s" in entries: fail(f"Refusing to rewrite signed package: {package_path.name}") core_paths = [ name for name in entries if name.startswith("package/services/metadata/core-properties/") and name.endswith(".psmdcp") ] if len(core_paths) != 1: fail(f"Expected one NuGet core-properties part in {package_path.name}.") entries[CORE_PROPERTIES_PATH] = entries.pop(core_paths[0]) nuspec_paths = [name for name in entries if name.endswith(".nuspec")] if len(nuspec_paths) != 1: fail(f"Expected one nuspec in {package_path.name}.") nuspec = ET.fromstring(entries[nuspec_paths[0]]) nuspec_namespace = nuspec.tag.partition("}")[0].removeprefix("{") if nuspec_namespace: ET.register_namespace("", nuspec_namespace) package_id = nuspec.findtext(".//{*}id") if package_id == "FinalFactory.Rendezvous.Client": contracts = nuspec.find( ".//{*}dependency[@id='FinalFactory.Rendezvous.Contracts']" ) if contracts is None: fail("Client package has no Contracts dependency to pin.") contracts.set("version", f"[{args.version}]") entries[nuspec_paths[0]] = ET.tostring( nuspec, encoding="utf-8", xml_declaration=True ) relationships_namespace = ( "http://schemas.openxmlformats.org/package/2006/relationships" ) ET.register_namespace("", relationships_namespace) relationships = ET.fromstring(entries["_rels/.rels"]) for relationship in relationships: relationship_type = relationship.get("Type", "") if relationship_type.endswith("/manifest"): relationship.set("Id", "RManifest") elif relationship_type.endswith("/metadata/core-properties"): relationship.set("Id", "RCoreProperties") relationship.set("Target", f"/{CORE_PROPERTIES_PATH}") entries["_rels/.rels"] = ET.tostring( relationships, encoding="utf-8", xml_declaration=True ) temporary = package_path.with_suffix(package_path.suffix + ".normalized") with zipfile.ZipFile(temporary, "w", compression=zipfile.ZIP_STORED) as target: for name in sorted(entries): info = zipfile.ZipInfo(name, date_time=zip_timestamp) info.compress_type = zipfile.ZIP_STORED info.create_system = 3 info.external_attr = 0o100644 << 16 target.writestr(info, entries[name]) temporary.replace(package_path) def command_provenance(args) -> None: versions = ET.parse(pathlib.Path(args.root) / "eng" / "Versions.props") def version_property(name: str) -> str: element = versions.find(f".//{name}") if element is None or not element.text: fail(f"Missing central release property: {name}") return element.text.strip() provenance = { "schemaVersion": 1, "version": args.version, "commit": args.commit, "treeState": args.tree_state, "buildConfiguration": "Release", "sourceDateEpoch": int(args.source_date_epoch), "dotnetSdk": args.dotnet_sdk, "buildTools": sorted(args.build_tool), "containerImage": f"git.finalfactory.de/heikyu/rendezvous:{args.version}", "containerPlatform": "linux/amd64", "containerBaseDigests": args.base_digest, "releaseBuilderBaseDigests": args.builder_base, "packages": [ f"FinalFactory.Rendezvous.Client/{args.version}", f"FinalFactory.Rendezvous.Contracts/{args.version}", ], "compatibility": { "minimumClientVersion": version_property("MinimumClientVersion"), "maximumClientMajorVersion": int(version_property("MaximumClientMajorVersion")), "httpContractVersions": [int(version_property("HttpContractVersion"))], "udpContractVersions": [int(version_property("UdpContractVersion"))], "connectionTicketFormatVersions": [ int(version_property("ConnectionTicketFormatVersion")) ], "liteNetLib": version_property("LiteNetLibVersion"), "gameplayProtocol": "exact-per-tenant", }, } pathlib.Path(args.output).write_text( json.dumps(provenance, indent=2) + "\n", encoding="utf-8" ) def command_record_container_build(args) -> None: path = pathlib.Path(args.provenance) provenance = load_json(path) tools = set(provenance.get("buildTools", [])) tools.add(f"docker-buildx={args.buildx_version}") tools.add(f"buildkit={args.buildkit_version}") provenance["buildTools"] = sorted(tools) provenance["containerPlatform"] = "linux/amd64" if not re.fullmatch(r"sha256:[0-9a-f]{64}", args.image_id): fail(f"Container image ID is invalid: {args.image_id}") provenance["containerImageId"] = args.image_id path.write_text(json.dumps(provenance, indent=2) + "\n", encoding="utf-8") def command_record_container_digest(args) -> None: if not re.fullmatch( r"git\.finalfactory\.de/heikyu/rendezvous@sha256:[0-9a-f]{64}", args.digest, ): fail(f"Published container digest is invalid: {args.digest}") release_dir = pathlib.Path(args.release_dir) provenance_path = release_dir / "release-provenance.json" provenance = load_json(provenance_path) provenance["containerDigest"] = args.digest provenance_path.write_text( json.dumps(provenance, indent=2) + "\n", encoding="utf-8" ) (release_dir / "container-digest.txt").write_text( args.digest + "\n", encoding="utf-8" ) def command_normalize_container_sbom(args) -> None: path = pathlib.Path(args.file) document = load_json(path) created = dt.datetime.fromtimestamp( int(args.source_date_epoch), dt.timezone.utc ).strftime("%Y-%m-%dT%H:%M:%SZ") document["name"] = f"FinalFactory.Rendezvous.Container-{args.version}" document["documentNamespace"] = ( "https://git.finalfactory.de/HeiKyu/Rendezvous/container-sbom/" f"{args.version}/{args.commit}" ) creation = document.setdefault("creationInfo", {}) creation["created"] = created creation["creators"] = ["Tool: Trivy-0.69.3"] if isinstance(document.get("packages"), list): document["packages"] = sorted( document["packages"], key=lambda item: ( item.get("SPDXID", ""), item.get("name", ""), item.get("versionInfo", ""), ), ) if isinstance(document.get("relationships"), list): document["relationships"] = sorted( document["relationships"], key=lambda item: ( item.get("spdxElementId", ""), item.get("relationshipType", ""), item.get("relatedSpdxElement", ""), ), ) path.write_text( json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8" ) def main() -> None: parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command", required=True) policy = subparsers.add_parser("policy") policy.add_argument("--root", required=True) policy.set_defaults(handler=command_policy) audit = subparsers.add_parser("audit") audit.add_argument("--input", required=True) audit.set_defaults(handler=command_audit) sbom = subparsers.add_parser("sbom") sbom.add_argument("--root", required=True) sbom.add_argument("--version", required=True) sbom.add_argument("--commit", required=True) sbom.add_argument("--source-date-epoch", required=True) sbom.add_argument("--server-deps", required=True) sbom.add_argument("--output", required=True) sbom.set_defaults(handler=command_sbom) verify = subparsers.add_parser("verify") verify.add_argument("--root", required=True) verify.add_argument("--release-dir", required=True) verify.add_argument("--version", required=True) verify.add_argument("--allow-dirty", action="store_true") verify.add_argument( "--phase", choices=("build", "publish-ready", "signing-ready", "published"), default="build", ) verify.set_defaults(handler=command_verify) consumer = subparsers.add_parser("consumer") consumer.add_argument("--assets", required=True) consumer.add_argument("--version", required=True) consumer.set_defaults(handler=command_consumer) consumer_config = subparsers.add_parser("consumer-config") consumer_config.add_argument("--local-source", required=True) consumer_config.add_argument("--output", required=True) consumer_config.set_defaults(handler=command_consumer_config) source_link = subparsers.add_parser("source-link") source_link.add_argument("--file", required=True) source_link.add_argument("--commit", required=True) source_link.set_defaults(handler=command_source_link) normalize = subparsers.add_parser("normalize-package") normalize.add_argument("--package", required=True) normalize.add_argument("--source-date-epoch", required=True) normalize.add_argument("--version", required=True) normalize.set_defaults(handler=command_normalize_package) provenance = subparsers.add_parser("provenance") provenance.add_argument("--root", required=True) provenance.add_argument("--version", required=True) provenance.add_argument("--commit", required=True) provenance.add_argument("--tree-state", required=True) provenance.add_argument("--source-date-epoch", required=True) provenance.add_argument("--dotnet-sdk", required=True) provenance.add_argument("--build-tool", action="append", default=[]) provenance.add_argument("--base-digest", action="append", default=[]) provenance.add_argument("--builder-base", action="append", default=[]) provenance.add_argument("--output", required=True) provenance.set_defaults(handler=command_provenance) record_container = subparsers.add_parser("record-container-build") record_container.add_argument("--provenance", required=True) record_container.add_argument("--buildx-version", required=True) record_container.add_argument("--buildkit-version", required=True) record_container.add_argument("--image-id", required=True) record_container.set_defaults(handler=command_record_container_build) record_digest = subparsers.add_parser("record-container-digest") record_digest.add_argument("--release-dir", required=True) record_digest.add_argument("--digest", required=True) record_digest.set_defaults(handler=command_record_container_digest) normalize_container_sbom = subparsers.add_parser("normalize-container-sbom") normalize_container_sbom.add_argument("--file", required=True) normalize_container_sbom.add_argument("--version", required=True) normalize_container_sbom.add_argument("--commit", required=True) normalize_container_sbom.add_argument("--source-date-epoch", required=True) normalize_container_sbom.set_defaults(handler=command_normalize_container_sbom) args = parser.parse_args() args.handler(args) if __name__ == "__main__": main()