#!/usr/bin/env bash set -euo pipefail ROOT=$(cd "$(dirname "$0")/../../.." && pwd) OUT="$ROOT/out/mirror/thin" STAGE="$OUT/stage-v1" CREATED="2025-11-23T00:00:00Z" export STAGE CREATED mkdir -p "$STAGE/layers" "$STAGE/indexes" # 1) Seed deterministic content cat > "$STAGE/layers/observations.ndjson" <<'DATA' {"id":"obs-001","purl":"pkg:nuget/Newtonsoft.Json@13.0.3","advisory":"CVE-2025-0001","severity":"medium","source":"vendor-a","timestamp":"2025-11-01T00:00:00Z"} {"id":"obs-002","purl":"pkg:npm/lodash@4.17.21","advisory":"CVE-2024-9999","severity":"high","source":"vendor-b","timestamp":"2025-10-15T00:00:00Z"} DATA cat > "$STAGE/layers/time-anchor.json" <<'DATA' { "authority": "stellaops-airgap-test", "generatedAt": "2025-11-01T00:00:00Z", "anchors": [ { "type": "roughtime", "version": "1", "publicKey": "base64:TEST_KEY_001", "signature": "base64:TEST_SIG_001", "timestamp": "2025-11-01T00:00:00Z", "maxDistanceSeconds": 5 } ] } DATA cat > "$STAGE/indexes/observations.index" <<'DATA' obs-001 layers/observations.ndjson:1 obs-002 layers/observations.ndjson:2 DATA # 2) Build manifest from staged files python - <<'PY' import json, hashlib, os, pathlib root = pathlib.Path(os.environ['STAGE']) created = os.environ['CREATED'] def digest(path: pathlib.Path) -> str: h = hashlib.sha256() with path.open('rb') as f: for chunk in iter(lambda: f.read(8192), b''): h.update(chunk) return 'sha256:' + h.hexdigest() def size(path: pathlib.Path) -> int: return path.stat().st_size layers = [] for path in sorted((root / 'layers').glob('*')): layers.append({ 'path': f"layers/{path.name}", 'size': size(path), 'digest': digest(path) }) indexes = [] for path in sorted((root / 'indexes').glob('*')): indexes.append({ 'name': path.name, 'digest': digest(path) }) manifest = { 'version': '1.0.0', 'created': created, 'layers': layers, 'indexes': indexes } manifest_path = root / 'manifest.json' manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + '\n', encoding='utf-8') PY # 3) Tarball with deterministic metadata pushd "$OUT" >/dev/null rm -f mirror-thin-v1.tar.gz mirror-thin-v1.tar.gz.sha256 mirror-thin-v1.manifest.json mirror-thin-v1.manifest.json.sha256 cp "$STAGE/manifest.json" mirror-thin-v1.manifest.json export GZIP=-n /usr/bin/tar --sort=name --owner=0 --group=0 --numeric-owner --mtime='1970-01-01' -czf mirror-thin-v1.tar.gz -C "$STAGE" . popd >/dev/null # 4) Checksums pushd "$OUT" >/dev/null sha256sum mirror-thin-v1.manifest.json > mirror-thin-v1.manifest.json.sha256 sha256sum mirror-thin-v1.tar.gz > mirror-thin-v1.tar.gz.sha256 popd >/dev/null # 5) Optional signing (DSSE + TUF) if SIGN_KEY is provided if [[ -n "${SIGN_KEY:-}" ]]; then mkdir -p "$OUT/tuf/keys" python scripts/mirror/sign_thin_bundle.py \ --key "$SIGN_KEY" \ --manifest "$OUT/mirror-thin-v1.manifest.json" \ --tar "$OUT/mirror-thin-v1.tar.gz" \ --tuf-dir "$OUT/tuf" fi # 6) Optional OCI archive (MIRROR-CRT-57-001) if [[ "${OCI:-0}" == "1" ]]; then OCI_DIR="$OUT/oci" BLOBS="$OCI_DIR/blobs/sha256" mkdir -p "$BLOBS" # layer = thin tarball LAYER_SHA=$(sha256sum "$OUT/mirror-thin-v1.tar.gz" | awk '{print $1}') cp "$OUT/mirror-thin-v1.tar.gz" "$BLOBS/$LAYER_SHA" LAYER_SIZE=$(stat -c%s "$OUT/mirror-thin-v1.tar.gz") # config = minimal empty config CONFIG_TMP=$(mktemp) echo '{"architecture":"amd64","os":"linux"}' > "$CONFIG_TMP" CONFIG_SHA=$(sha256sum "$CONFIG_TMP" | awk '{print $1}') CONFIG_SIZE=$(stat -c%s "$CONFIG_TMP") cp "$CONFIG_TMP" "$BLOBS/$CONFIG_SHA" rm "$CONFIG_TMP" mkdir -p "$OCI_DIR" cat > "$OCI_DIR/oci-layout" <<'JSON' { "imageLayoutVersion": "1.0.0" } JSON MANIFEST_FILE="$OCI_DIR/manifest.json" cat > "$MANIFEST_FILE" < "$OCI_DIR/index.json" <