166 lines
4.8 KiB
Bash
166 lines
4.8 KiB
Bash
#!/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" <<JSON
|
|
{
|
|
"schemaVersion": 2,
|
|
"config": {
|
|
"mediaType": "application/vnd.oci.image.config.v1+json",
|
|
"size": $CONFIG_SIZE,
|
|
"digest": "sha256:$CONFIG_SHA"
|
|
},
|
|
"layers": [
|
|
{
|
|
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
|
"size": $LAYER_SIZE,
|
|
"digest": "sha256:$LAYER_SHA",
|
|
"annotations": {"org.stellaops.bundle.type": "mirror-thin-v1"}
|
|
}
|
|
]
|
|
}
|
|
JSON
|
|
MANIFEST_SHA=$(sha256sum "$MANIFEST_FILE" | awk '{print $1}')
|
|
MANIFEST_SIZE=$(stat -c%s "$MANIFEST_FILE")
|
|
cat > "$OCI_DIR/index.json" <<JSON
|
|
{
|
|
"schemaVersion": 2,
|
|
"manifests": [
|
|
{
|
|
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
|
"digest": "sha256:$MANIFEST_SHA",
|
|
"size": $MANIFEST_SIZE,
|
|
"annotations": {"org.opencontainers.image.ref.name": "mirror-thin-v1"}
|
|
}
|
|
]
|
|
}
|
|
JSON
|
|
fi
|
|
|
|
# 7) Verification
|
|
python scripts/mirror/verify_thin_bundle.py "$OUT/mirror-thin-v1.manifest.json" "$OUT/mirror-thin-v1.tar.gz"
|
|
|
|
echo "mirror-thin-v1 built at $OUT"
|