Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ env:
GORELEASER_VERSION: "v2.17.0"
SYFT_VERSION: "v1.46.0"
COSIGN_VERSION: "v3.0.6"
HUB_IMAGE: ghcr.io/ardurai/sith-hub

jobs:
release:
Expand All @@ -26,6 +27,7 @@ jobs:
contents: write
id-token: write
attestations: write
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
Expand Down Expand Up @@ -97,6 +99,104 @@ jobs:
- name: Sign Homebrew formula
run: cosign sign-blob --yes --bundle=dist/sith.rb.sigstore.json dist/sith.rb

- name: Stage immutable hub image context
env:
IMAGE_CONTEXT: ${{ runner.temp }}/sith-hub-image
run: |
set -euo pipefail
install -d "$IMAGE_CONTEXT/bin/linux/amd64" "$IMAGE_CONTEXT/bin/linux/arm64"
tar -xzf "dist/sith_${VERSION}_linux_amd64.tar.gz" -C "$IMAGE_CONTEXT/bin/linux/amd64" sith
tar -xzf "dist/sith_${VERSION}_linux_arm64.tar.gz" -C "$IMAGE_CONTEXT/bin/linux/arm64" sith
install -m 0644 Containerfile "$IMAGE_CONTEXT/Containerfile"
test -x "$IMAGE_CONTEXT/bin/linux/amd64/sith"
test -x "$IMAGE_CONTEXT/bin/linux/arm64/sith"

- name: Set up QEMU for hub image platforms
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3

- name: Set up Buildx for hub image platforms
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3

- name: Authenticate to the GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Guard hub image tag against overwrite
env:
HUB_TAG: ${{ env.HUB_IMAGE }}:${{ github.ref_name }}
run: |
set -euo pipefail
if inspection="$(docker manifest inspect "$HUB_TAG" 2>&1)"; then
echo "::error::hub image tag already exists; immutable release tags cannot be overwritten"
exit 1
fi
case "$inspection" in
*"manifest unknown"*|*"no such manifest"*) ;;
*)
echo "::error::could not establish whether the hub image tag exists"
exit 1
;;
esac

- name: Publish immutable multi-platform hub image
id: hub_image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: ${{ runner.temp }}/sith-hub-image
file: ${{ runner.temp }}/sith-hub-image/Containerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.HUB_IMAGE }}:${{ github.ref_name }}
provenance: false
sbom: false

- name: Sign and verify published hub image
env:
HUB_DIGEST: ${{ steps.hub_image.outputs.digest }}
run: |
set -euo pipefail
test -n "$HUB_DIGEST"
image="${HUB_IMAGE}@${HUB_DIGEST}"
identity="https://github.com/${GITHUB_REPOSITORY}/.github/workflows/release.yml@refs/tags/${GITHUB_REF_NAME}"
cosign sign --yes "$image"
cosign verify \
--certificate-identity "$identity" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$image" >/dev/null
printf '%s\n' "$image" > "dist/sith_${VERSION}_hub.image"
cosign sign-blob --yes \
--bundle="dist/sith_${VERSION}_hub.image.sigstore.json" \
"dist/sith_${VERSION}_hub.image"

- name: Generate hub image SPDX SBOM
env:
HUB_DIGEST: ${{ steps.hub_image.outputs.digest }}
run: |
set -euo pipefail
test -n "$HUB_DIGEST"
syft "${HUB_IMAGE}@${HUB_DIGEST}" \
--output "spdx-json=dist/sith_${VERSION}_hub.spdx.json"
Comment on lines +174 to +181

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant workflow section with line numbers.
sed -n '150,240p' .github/workflows/release.yml

echo
echo "---- syft usage in repo ----"
rg -n "syft .*--platform|syft " .github/workflows . || true

Repository: ArdurAI/sith

Length of output: 4225


🌐 Web query:

Syft default behavior when analyzing a multi-arch image index with no --platform flag documented default linux/amd64

💡 Result:

When analyzing a multi-architecture container image index without an explicitly provided --platform flag, Syft defaults to analyzing the linux/amd64 manifest [1]. While Syft provides a --platform flag to allow users to specify a target architecture (e.g., linux/arm64) [2][3], in the absence of this configuration, the tool automatically targets the linux/amd64 architecture for unspecific image references [1]. Historically, this behavior has been a point of discussion in the community regarding how Syft handles multi-platform images and OCI layouts [4][5][6], but the documented default remains linux/amd64 for multi-arch image indexes [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Gather syft platform/docs references from public docs if available via common README snippets in repo references.
rg -n "platform" .github/workflows/release.yml README.md docs . 2>/dev/null | head -n 200 || true

Repository: ArdurAI/sith

Length of output: 38044


🌐 Web query:

Syft analyze multi-platform image index without --platform which manifest is selected default linux/amd64 or current platform

💡 Result:

When analyzing a multi-platform container image index without specifying the --platform flag, Syft does not strictly default to linux/amd64. Instead, Syft uses the architecture of the host machine (the environment where the tool is being executed) as the default platform [1][2]. While some older documentation or specific contexts may have referenced linux/amd64, updates to Syft and its underlying library, stereoscope, have aligned the default behavior to match the host architecture [1][2]. If your host machine is running on an arm64 architecture, Syft will attempt to pull the linux/arm64 manifest by default [1]. Key points regarding this behavior: - Host-based selection: Syft detects the host platform and uses that for manifest selection unless explicitly overridden by the --platform flag [1][2]. - Explicit override: To ensure a specific platform is analyzed, you must use the --platform flag (e.g., --platform linux/amd64), which forces Syft to target that specific manifest regardless of the host environment [3][4]. - Limitations: Syft currently analyzes only the single manifest resolved by the platform selection (either the default host-based one or the user-specified one); it does not automatically aggregate dependencies from all manifests within a multi-platform index into a single SBOM [5][6].

Citations:


🌐 Web query:

anchore syft platform flag image index default platform documentation

💡 Result:

When scanning container images with Anchore Syft, the --platform flag is used to specify the operating system and architecture of the image to be analyzed [1][2]. Default Behavior: When scanning multi-platform images or image references that do not specify a platform, Syft defaults to targeting the linux/amd64 manifest [1]. Using the --platform Flag: The --platform flag allows you to override this default and target a specific architecture or OS/architecture combination [1][3]. The flag accepts values in formats similar to those used by Docker, such as: - Architecture only: --platform arm64 - OS/Architecture combination: --platform linux/arm64 - Architecture variants: --platform linux/arm/v8 Configuration: You can also set the platform via the configuration file or an environment variable (SYFT_PLATFORM) [2][4]. Important Limitations: Syft currently analyzes a single specific platform's manifest at a time when scanning an image index (or multi-arch manifest list) [5][6]. It is unable to automatically combine multiple architectures into a single unified SBOM in one command [5]. If you need an SBOM that covers all architectures in a multi-platform image, you must perform separate scans for each desired platform and manage the resulting SBOMs accordingly [7].

Citations:


Generate separate SBOMs for each hub platform

syft "${HUB_IMAGE}@${HUB_DIGEST}" resolves and scans only one platform from the multi-arch hub index, but the attestation is published against the index digest itself. That makes the SBOM appear to cover both linux/amd64 and linux/arm64 when it only describes a single manifest. Generate and attest one SBOM per platform instead, matching the existing per-arch release pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 174 - 181, Update the “Generate
hub image SPDX SBOM” workflow step to resolve and scan the hub image separately
for linux/amd64 and linux/arm64 rather than scanning the multi-architecture
index via HUB_DIGEST. Produce distinct platform-specific SPDX files and ensure
the corresponding attestation/publishing steps reference each per-platform SBOM,
following the existing per-arch release pattern.


- name: Attest hub image build provenance
id: hub_provenance
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-name: ${{ env.HUB_IMAGE }}
subject-digest: ${{ steps.hub_image.outputs.digest }}
push-to-registry: true

- name: Attest hub image SBOM
id: hub_sbom
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
with:
subject-name: ${{ env.HUB_IMAGE }}
subject-digest: ${{ steps.hub_image.outputs.digest }}
sbom-path: dist/sith_${{ env.VERSION }}_hub.spdx.json
push-to-registry: true

- name: Generate SLSA build provenance
id: provenance
uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1
Expand Down Expand Up @@ -139,16 +239,25 @@ jobs:
SBOM_DARWIN_ARM64_BUNDLE: ${{ steps.sbom_darwin_arm64.outputs.bundle-path }}
SBOM_LINUX_AMD64_BUNDLE: ${{ steps.sbom_linux_amd64.outputs.bundle-path }}
SBOM_LINUX_ARM64_BUNDLE: ${{ steps.sbom_linux_arm64.outputs.bundle-path }}
HUB_PROVENANCE_BUNDLE: ${{ steps.hub_provenance.outputs.bundle-path }}
HUB_SBOM_BUNDLE: ${{ steps.hub_sbom.outputs.bundle-path }}
run: |
set -euo pipefail
install -m 0644 "$PROVENANCE_BUNDLE" "dist/sith_${VERSION}_provenance.sigstore.json"
install -m 0644 "$SBOM_DARWIN_AMD64_BUNDLE" "dist/sith_${VERSION}_darwin_amd64.sbom.sigstore.json"
install -m 0644 "$SBOM_DARWIN_ARM64_BUNDLE" "dist/sith_${VERSION}_darwin_arm64.sbom.sigstore.json"
install -m 0644 "$SBOM_LINUX_AMD64_BUNDLE" "dist/sith_${VERSION}_linux_amd64.sbom.sigstore.json"
install -m 0644 "$SBOM_LINUX_ARM64_BUNDLE" "dist/sith_${VERSION}_linux_arm64.sbom.sigstore.json"
install -m 0644 "$HUB_PROVENANCE_BUNDLE" "dist/sith_${VERSION}_hub.provenance.sigstore.json"
install -m 0644 "$HUB_SBOM_BUNDLE" "dist/sith_${VERSION}_hub.sbom.sigstore.json"
gh release upload "$GITHUB_REF_NAME" \
dist/sith.rb \
dist/sith.rb.sigstore.json \
"dist/sith_${VERSION}_hub.image" \
"dist/sith_${VERSION}_hub.image.sigstore.json" \
"dist/sith_${VERSION}_hub.spdx.json" \
"dist/sith_${VERSION}_hub.provenance.sigstore.json" \
"dist/sith_${VERSION}_hub.sbom.sigstore.json" \
"dist/sith_${VERSION}_provenance.sigstore.json" \
"dist/sith_${VERSION}_darwin_amd64.sbom.sigstore.json" \
"dist/sith_${VERSION}_darwin_arm64.sbom.sigstore.json" \
Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ test-scripts: ## Run focused safety tests for operator-facing shell harnesses
bash tests/scripts/release_tag_identity_guide_test.sh
bash tests/scripts/release_tag_policy_test.sh
bash tests/scripts/release_pr_gate_policy_test.sh
bash tests/scripts/release_hub_image_policy_test.sh

perf: ## Enforce the warm-cache TUI p95 latency budget without race overhead
go test -count=1 -run '^TestWarmViewP95UnderOneHundredMilliseconds$$' ./internal/tui
Expand Down Expand Up @@ -142,7 +143,7 @@ run: build ## Build then run sith version

ci: fmt-check vet lint vuln test test-scripts perf e2e build ## Run the full CI gate locally

release-check: ## Build and verify the reproducible multi-platform release snapshot twice
release-check: ## Build, verify, and package the reproducible multi-platform release snapshot twice
@command -v "$(GORELEASER)" >/dev/null || { echo "goreleaser is required" >&2; exit 1; }
@command -v syft >/dev/null || { echo "syft is required" >&2; exit 1; }
@set -e; tmp="$$(mktemp -d)"; trap 'rm -rf "$$tmp"' EXIT; \
Expand All @@ -155,6 +156,7 @@ release-check: ## Build and verify the reproducible multi-platform release snaps
"$(GORELEASER)" release --snapshot --clean --skip=sign; \
go run ./tools/releasecheck verify --dist dist; \
go run ./tools/releasecheck formula --dist dist --output dist/sith.rb; \
DOCKER_BIN="$(DOCKER)" hack/verify-release-hub-image.sh --dist dist; \
go run ./tools/releasecheck digests --dist dist > "$$tmp/second.sha256"; \
diff -u "$$tmp/first.sha256" "$$tmp/second.sha256"

Expand Down
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,14 +270,17 @@ The no-network setting applies only to those isolated image checks. A deployed h
allowlisted egress to its configured runtime dependencies, including its database and, when
enabled, the pinned OIDC discovery and JWKS endpoints.

This is not a published image reference. The fail-closed [`charts/sith-hub`](charts/sith-hub)
chart requires an explicit immutable `repository@sha256:...` image reference and refuses tags,
especially `latest`; it invokes `sith hub migrate` in a separate short-lived Job before the
non-owner hub Deployment starts. Its defaults intentionally cannot install until a release-bound
hub image and operator-provided Secret references exist; it never renders secret material. The
Hub OCI images are published only by a completed, signed release tag. The release attaches the
exact immutable digest as `sith_<version>_hub.image`; follow the
[release verification guide](docs/RELEASE.md#verify-a-hub-oci-image) before supplying it to the
fail-closed [`charts/sith-hub`](charts/sith-hub) chart. The chart requires an explicit
`repository@sha256:...` image reference and refuses tags, especially `latest`; it invokes `sith hub
migrate` in a separate short-lived Job before the non-owner hub Deployment starts. Its defaults
intentionally cannot install until an operator provides that digest and the existing Secret
references; it never renders secret material. Older releases can lack this image artifact. The
chart permits only fixed `light` and `heavy` resource profiles, which retain identical security,
credential, and RBAC controls. This first F9.3a profile slice does not claim a public image,
in-chart database, or HA; those parent-F9.3 topology and custody capabilities need later evidence.
credential, and RBAC controls. This first F9.3a profile slice does not claim in-chart database or
HA topology; those parent-F9.3 capabilities need later evidence.

`sith serve --mcp` exposes `fleet.inventory`, `fleet.health`, `fleet.correlate`, and
`fleet.cve-search` over MCP Streamable HTTP. All four tools are cache-only and carry
Expand Down
62 changes: 52 additions & 10 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ Sith releases are immutable, tag-driven builds from `main`. Stable releases use
the beta channel uses `vMAJOR.MINOR.PATCH-beta.N` and never replaces the latest stable release. The release job creates a draft,
builds four archives with GoReleaser, emits an SPDX 2.3 SBOM for each archive with Syft, signs the
archives, SBOMs, and checksum manifest with keyless Cosign, and creates GitHub SLSA provenance plus
one SBOM attestation per platform. The draft becomes public only after every step succeeds.
one SBOM attestation per platform. It also publishes the tag's multi-architecture hub image by its
manifest digest, signs it with keyless Cosign, and creates separate provenance and SPDX SBOM
attestations for that digest. The draft becomes public only after every step succeeds.

The workflow follows the primary guidance for [GitHub artifact attestations](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations),
[GoReleaser reproducible Go builds](https://goreleaser.com/customization/builds/builders/go/#reproducible-builds),
Expand All @@ -29,6 +31,45 @@ URLs or hashes. The release workflow signs the formula itself. The tap's own rep
verifies that signature and the signed checksum manifest before importing the formula, so the Sith
release token never needs cross-repository write access.

## Verify a hub OCI image

Hub images are published only by a signed Sith release tag and must be consumed by immutable
manifest digest. A release includes a signed `sith_<version>_hub.image` file whose only line is the
digest address; do not substitute the convenient version tag or add `latest` to a Helm value.

```bash
tag=vX.Y.Z # use a tag released after hub-image publication is enabled
version=${tag#v}
gh release download "$tag" --repo ArdurAI/sith \
--pattern "sith_${version}_hub.image" \
--dir "sith-$version"
image=$(cat "sith-$version/sith_${version}_hub.image")
case "$image" in ghcr.io/ardurai/sith-hub@sha256:*) ;; *) exit 1 ;; esac
```

Verify the keyless image signature against the exact tag workflow identity, then verify GitHub
provenance and the SPDX SBOM attestation. These commands require registry access; the later air-gap
workflow consumes mirrored, pre-verified material rather than weakening this verification boundary.

```bash
identity="https://github.com/ArdurAI/sith/.github/workflows/release.yml@refs/tags/${tag}"
cosign verify \
--certificate-identity "$identity" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$image"
gh attestation verify "oci://$image" \
--repo ArdurAI/sith \
--signer-workflow ArdurAI/sith/.github/workflows/release.yml
gh attestation verify "oci://$image" \
--repo ArdurAI/sith \
--signer-workflow ArdurAI/sith/.github/workflows/release.yml \
--predicate-type https://spdx.dev/Document/v2.3
```

The existing Helm chart remains fail-closed: it accepts this digest and names of pre-materialized
runtime and migration Secrets only. It neither creates secret data nor supplies a KMS provider,
database, ingress, or mutable image reference.

Comment on lines +34 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Verify the signed digest evidence before consuming it.

The workflow emits sith_<version>_hub.image.sigstore.json, but this guide downloads only sith_<version>_hub.image and never runs cosign verify-blob on it. Download the bundle and verify it against the exact release-tag workflow identity before passing $image to Helm; otherwise the documented “signed digest evidence” check is skipped.

Suggested verification
 gh release download "$tag" --repo ArdurAI/sith \
   --pattern "sith_${version}_hub.image" \
+  --pattern "sith_${version}_hub.image.sigstore.json" \
   --dir "sith-$version"
 image=$(cat "sith-$version/sith_${version}_hub.image")
+cosign verify-blob \
+  --bundle "sith-$version/sith_${version}_hub.image.sigstore.json" \
+  --certificate-identity "https://github.com/ArdurAI/sith/.github/workflows/release.yml@refs/tags/${tag}" \
+  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
+  "sith-$version/sith_${version}_hub.image"
 case "$image" in ghcr.io/ardurai/sith-hub@sha256:*) ;; *) exit 1 ;; esac
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Verify a hub OCI image
Hub images are published only by a signed Sith release tag and must be consumed by immutable
manifest digest. A release includes a signed `sith_<version>_hub.image` file whose only line is the
digest address; do not substitute the convenient version tag or add `latest` to a Helm value.
```bash
tag=vX.Y.Z # use a tag released after hub-image publication is enabled
version=${tag#v}
gh release download "$tag" --repo ArdurAI/sith \
--pattern "sith_${version}_hub.image" \
--dir "sith-$version"
image=$(cat "sith-$version/sith_${version}_hub.image")
case "$image" in ghcr.io/ardurai/sith-hub@sha256:*) ;; *) exit 1 ;; esac
```
Verify the keyless image signature against the exact tag workflow identity, then verify GitHub
provenance and the SPDX SBOM attestation. These commands require registry access; the later air-gap
workflow consumes mirrored, pre-verified material rather than weakening this verification boundary.
```bash
identity="https://github.com/ArdurAI/sith/.github/workflows/release.yml@refs/tags/${tag}"
cosign verify \
--certificate-identity "$identity" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"$image"
gh attestation verify "oci://$image" \
--repo ArdurAI/sith \
--signer-workflow ArdurAI/sith/.github/workflows/release.yml
gh attestation verify "oci://$image" \
--repo ArdurAI/sith \
--signer-workflow ArdurAI/sith/.github/workflows/release.yml \
--predicate-type https://spdx.dev/Document/v2.3
```
The existing Helm chart remains fail-closed: it accepts this digest and names of pre-materialized
runtime and migration Secrets only. It neither creates secret data nor supplies a KMS provider,
database, ingress, or mutable image reference.
## Verify a hub OCI image
Hub images are published only by a signed Sith release tag and must be consumed by immutable
manifest digest. A release includes a signed `sith_<version>_hub.image` file whose only line is the
digest address; do not substitute the convenient version tag or add `latest` to a Helm value.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/RELEASE.md` around lines 34 - 72, Update the “Verify a hub OCI image”
workflow to download the release’s sith_${version}_hub.image.sigstore.json
bundle alongside the digest file, then run cosign verify-blob against the digest
file using that bundle and the exact tag workflow identity before assigning or
consuming $image. Preserve the existing digest-format validation and subsequent
image signature, provenance, and SBOM checks.

## Verify a release

Set the release and platform, then download its assets:
Expand Down Expand Up @@ -98,14 +139,15 @@ No network is an isolated image-check constraint, not the operational hub policy
must allow only narrowly scoped egress to configured runtime dependencies, including the database
and, where enabled, the pinned OIDC discovery and JWKS endpoints.

No OCI image is published by this repository yet. Consumers must not infer a mutable image tag
from a release archive. The [`charts/sith-hub`](../charts/sith-hub) chart accepts only an explicit
immutable `repository@sha256:...` reference, and its defaults intentionally fail until an operator
provides that reference and existing Secret names. Image publishing/signing/attestation will be
added as a separate release-boundary change before any public deployment guidance. Its fixed
`light` and `heavy` profiles alter only the reviewed resource envelope; both preserve the same
digest, Secret-reference, migration, RBAC, and workload-hardening contract. This first F9.3a
slice is not a claim of the parent feature's future in-chart database, HA, or cloud-KMS topology.
Hub OCI images are published only as a release-boundary artifact, never by a pull request or local
build. Consumers must not infer a mutable image tag from a release archive. The
[`charts/sith-hub`](../charts/sith-hub) chart accepts only an explicit immutable
`repository@sha256:...` reference, and its defaults intentionally fail until an operator provides
that reference and existing Secret names. Older releases can lack the hub-image assets; use the
digest address attached to a release that includes them. Its fixed `light` and `heavy` profiles
alter only the reviewed resource envelope; both preserve the same digest, Secret-reference,
migration, RBAC, and workload-hardening contract. This first F9.3a slice is not a claim of the
parent feature's future in-chart database, HA, or cloud-KMS topology.

## Maintainer release procedure

Expand Down Expand Up @@ -190,6 +232,6 @@ Homebrew token is stored in Sith.
## Cost and operational notes

The incremental cost is GitHub-hosted runner time for four cross-builds, two snapshot builds on
each PR, Syft scans, and five attestations on a tag. Fulcio and Rekor use Sigstore's public-good
each PR, Syft scans, and seven attestations on a tag. Fulcio and Rekor use Sigstore's public-good
service for this public repository. Releases create no runtime cloud service, NAT egress path, or
persistent signing infrastructure.
94 changes: 94 additions & 0 deletions hack/verify-release-hub-image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env bash

# SPDX-License-Identifier: Apache-2.0

set -euo pipefail

usage() {
printf 'usage: %s --dist <release-distribution-directory>\n' "${0##*/}" >&2
exit 2
}

if [[ "$#" != 2 || "$1" != "--dist" ]]; then
usage
fi

readonly REPOSITORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
readonly DIST_DIRECTORY="$(cd "$2" && pwd -P)"
Comment on lines +16 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Split readonly declaration from assignment (SC2155).

Under set -e, a failing cd "$2" here would be masked since the exit status of the compound readonly VAR=$(...) statement is that of readonly, not the subshell — leaving DIST_DIRECTORY empty instead of failing immediately with a clear diagnostic.

🔧 Suggested fix
-readonly REPOSITORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
-readonly DIST_DIRECTORY="$(cd "$2" && pwd -P)"
+REPOSITORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
+readonly REPOSITORY_ROOT
+DIST_DIRECTORY="$(cd "$2" && pwd -P)"
+readonly DIST_DIRECTORY
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
readonly REPOSITORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
readonly DIST_DIRECTORY="$(cd "$2" && pwd -P)"
REPOSITORY_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
readonly REPOSITORY_ROOT
DIST_DIRECTORY="$(cd "$2" && pwd -P)"
readonly DIST_DIRECTORY
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 16-16: Declare and assign separately to avoid masking return values.

(SC2155)


[warning] 17-17: Declare and assign separately to avoid masking return values.

(SC2155)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/verify-release-hub-image.sh` around lines 16 - 17, Split the readonly
declarations for REPOSITORY_ROOT and DIST_DIRECTORY from their command
substitutions: assign each resolved directory path first, then apply readonly in
separate statements. Preserve the existing path calculations while ensuring a
failing cd, especially for the "$2" distribution directory, propagates under set
-e instead of being masked.

Source: Linters/SAST tools

readonly DOCKER_BIN="${DOCKER_BIN:-docker}"

command -v "$DOCKER_BIN" >/dev/null
command -v python3 >/dev/null

shopt -s nullglob
amd64_archives=("${DIST_DIRECTORY}"/sith_*_linux_amd64.tar.gz)
arm64_archives=("${DIST_DIRECTORY}"/sith_*_linux_arm64.tar.gz)
if [[ "${#amd64_archives[@]}" != 1 || "${#arm64_archives[@]}" != 1 ]]; then
printf 'expected exactly one Linux archive per supported architecture in %s\n' "$DIST_DIRECTORY" >&2
exit 1
fi

context_directory="$(mktemp -d)"
builder="sith-release-hub-${RANDOM}-$$"
cleanup() {
"$DOCKER_BIN" buildx rm --force "$builder" >/dev/null 2>&1 || true
rm -rf "$context_directory"
}
trap cleanup EXIT
install -d -m 0755 "$context_directory/bin/linux/amd64" "$context_directory/bin/linux/arm64"
tar -xzf "${amd64_archives[0]}" -C "$context_directory/bin/linux/amd64" sith
tar -xzf "${arm64_archives[0]}" -C "$context_directory/bin/linux/arm64" sith
install -m 0644 "$REPOSITORY_ROOT/Containerfile" "$context_directory/Containerfile"
test -x "$context_directory/bin/linux/amd64/sith"
test -x "$context_directory/bin/linux/arm64/sith"

oci_layout="$context_directory/sith-hub.oci"
"$DOCKER_BIN" buildx create --driver docker-container --name "$builder" >/dev/null
"$DOCKER_BIN" buildx inspect --builder "$builder" --bootstrap >/dev/null
"$DOCKER_BIN" buildx build \
--builder "$builder" \
--platform linux/amd64,linux/arm64 \
--provenance=false \
--sbom=false \
--file "$context_directory/Containerfile" \
--output "type=oci,dest=${oci_layout}" \
"$context_directory"

oci_directory="$context_directory/oci-layout"
install -d -m 0755 "$oci_directory"
tar -xf "$oci_layout" -C "$oci_directory"
python3 - "$oci_directory" <<'PY'
import json
import sys
from pathlib import Path

root = Path(sys.argv[1])
index_media_types = {
"application/vnd.oci.image.index.v1+json",
"application/vnd.docker.distribution.manifest.list.v2+json",
}

def indexed_platforms(index):
platforms = set()
for manifest in index.get("manifests", []):
if manifest.get("mediaType") in index_media_types:
algorithm, digest = manifest["digest"].split(":", 1)
with (root / "blobs" / algorithm / digest).open(encoding="utf-8") as nested_file:
platforms.update(indexed_platforms(json.load(nested_file)))
continue
platform = manifest.get("platform", {})
os_name = platform.get("os")
architecture = platform.get("architecture")
if os_name is not None and architecture is not None:
platforms.add((os_name, architecture))
return platforms

with (root / "index.json").open(encoding="utf-8") as index_file:
platforms = indexed_platforms(json.load(index_file))

expected = {("linux", "amd64"), ("linux", "arm64")}
if platforms != expected:
raise SystemExit(f"OCI layout platforms = {sorted(platforms)!r}, want {sorted(expected)!r}")
PY

printf 'release hub OCI layout verified from release archives\n'
Loading