diff --git a/.github/scripts/cask-pr-handoff-workflow.test.sh b/.github/scripts/cask-pr-handoff-workflow.test.sh index 8773e9f53b..663c3ee98b 100755 --- a/.github/scripts/cask-pr-handoff-workflow.test.sh +++ b/.github/scripts/cask-pr-handoff-workflow.test.sh @@ -70,6 +70,40 @@ assert_not_contains() { fi } +# Run the real loop up to PR discovery. An invalid published Desktop archive must +# never reach either the open-PR handoff or the already-merged recovery path, while +# the independently verified CLI cask remains deliverable. +archive_gate="${tmp_dir}/archive-gate.sh" +awk ' + /^ for name in ksail ksail-desktop; do/ { in_loop = 1 } + in_loop && /^ matches=/ { exit } + in_loop { line = $0; sub(/^ /, "", line); print line } + END { print " printf '\''delivered:%s\\n'\'' \"$name\""; print "done" } +' "${homebrew_block}" >"${archive_gate}" +gate_root="${tmp_dir}/archive-gate" +mkdir -p "${gate_root}/.github/scripts" +cat >"${gate_root}/.github/scripts/verify-published-desktop-archive.sh" <<'EOF' +#!/usr/bin/env bash +printf 'BLOCKED: published Desktop bundle has an invalid resource seal\n' >&2 +exit 1 +EOF +chmod +x "${gate_root}/.github/scripts/verify-published-desktop-archive.sh" +( + cd "${gate_root}" + TAG=v7.212.40 TAP=devantler-tech/homebrew-tap handoff_failed=0 + GITHUB_STEP_SUMMARY="${tmp_dir}/archive-gate-summary" + # shellcheck source=/dev/null + source "${archive_gate}" + [[ "${handoff_failed}" -eq 1 ]] || fail 'invalid Desktop archive must make the handoff fail' +) >"${tmp_dir}/archive-gate.log" 2>&1 || { + cat "${tmp_dir}/archive-gate.log" >&2 + fail 'invalid published Desktop archive reached cask handoff' +} +grep -Fxq 'delivered:ksail' "${tmp_dir}/archive-gate.log" || fail 'Desktop rejection blocked the CLI cask' +if grep -Fxq 'delivered:ksail-desktop' "${tmp_dir}/archive-gate.log"; then + fail 'invalid published Desktop archive reached open or merged PR recovery' +fi + race_root="${tmp_dir}/merged-race" mkdir -p "${race_root}/.github/scripts" "${race_root}/evidence" cat >"${race_root}/.github/scripts/find-merged-cask-pr-handoff.sh" <<'EOF' diff --git a/.github/scripts/verify-published-desktop-archive.sh b/.github/scripts/verify-published-desktop-archive.sh new file mode 100755 index 0000000000..605595a5c5 --- /dev/null +++ b/.github/scripts/verify-published-desktop-archive.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Verify the immutable Desktop download before delivering or recovering its cask. +set -euo pipefail + +if [[ $# != 1 || ! "$1" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then + printf 'Usage: verify-published-desktop-archive.sh TAG\n' >&2 + exit 2 +fi + +tag="$1" +asset="KSail_${tag#v}_darwin_arm64.zip" +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_dir}/../.." && pwd)" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +export GH_TOKEN="${KSAIL_CASK_READ_TOKEN:-${GH_TOKEN:-}}" + +"${script_dir}/cask-read-api.sh" "repos/devantler-tech/ksail/releases/tags/${tag}" >"${work}/release.json" +if ! digest="$(jq -er --arg tag "$tag" --arg asset "$asset" ' + select(.tag_name == $tag and .draft == false and .immutable == true) + | select(.published_at | type == "string" and length > 0) + | [.assets[] | select(.name == $asset)] + | select(length == 1) | .[0] + | select(.size | type == "number" and . > 0 and floor == .) + | .digest | select(type == "string" and test("^sha256:[0-9a-f]{64}$")) +' "${work}/release.json")"; then + printf 'BLOCKED: immutable published Desktop asset evidence is incomplete\n' >&2 + exit 1 +fi + +# The destination is fixed by the product and validated tag, never by response text. +archive="${work}/desktop.zip" +curl --fail --location --silent --show-error --max-time 180 \ + --proto '=https' --proto-redir '=https' \ + --output "$archive" \ + "https://github.com/devantler-tech/ksail/releases/download/${tag}/${asset}" +actual="$(shasum -a 256 "$archive" | awk '{print $1}')" +if [[ "sha256:${actual}" != "$digest" ]]; then + printf 'BLOCKED: downloaded Desktop archive does not match its published digest\n' >&2 + exit 1 +fi + +bash "${repo_root}/desktop/scripts/verify-macos-archive.sh" "$archive" +printf 'PASS: published Desktop archive %s has a valid resource seal (%s)\n' "$tag" "$actual" diff --git a/.github/scripts/verify-published-desktop-archive.test.sh b/.github/scripts/verify-published-desktop-archive.test.sh new file mode 100755 index 0000000000..1224bc1669 --- /dev/null +++ b/.github/scripts/verify-published-desktop-archive.test.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Exercise publication recovery with real macOS resource seals and ZIP extraction. +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +[[ "$(uname -s)" == Darwin ]] || exit 2 +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/KSail.app/Contents/MacOS" "$work/KSail.app/Contents/Resources" "$work/bin" +cp /usr/bin/true "$work/KSail.app/Contents/MacOS/KSail" +cat >"$work/KSail.app/Contents/Info.plist" <<'EOF' + + +CFBundleExecutableKSail +CFBundleIdentifiertech.devantler.ksail.signature-fixture +CFBundlePackageTypeAPPL + +EOF +printf 'Original license\n' >"$work/KSail.app/Contents/Resources/LICENSE" +codesign --force --sign - --timestamp=none "$work/KSail.app" 2>"$work/sign.log" +ditto -c -k --keepParent "$work/KSail.app" "$work/valid.zip" +printf 'Changed after signing\n' >"$work/KSail.app/Contents/Resources/LICENSE" +ditto -c -k --keepParent "$work/KSail.app" "$work/invalid.zip" + +# Replace transport only; keep the real digest, extraction and signature checks. +cat >"$work/bin/gh" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +[[ "$*" == 'api repos/devantler-tech/ksail/releases/tags/v7.212.40' ]] || exit 2 +cat "$RELEASE_FIXTURE" +EOF +cat >"$work/bin/curl" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +output="" url="" +while (($#)); do + case "$1" in + --output) output="$2"; shift 2 ;; + https://*) url="$1"; shift ;; + *) shift ;; + esac +done +[[ "$url" == https://github.com/devantler-tech/ksail/releases/download/v7.212.40/KSail_7.212.40_darwin_arm64.zip && -n "$output" ]] || exit 2 +cp "$ARCHIVE_FIXTURE" "$output" +EOF +chmod +x "$work/bin/gh" "$work/bin/curl" +export PATH="$work/bin:$PATH" +export RELEASE_FIXTURE="$work/release.json" ARCHIVE_FIXTURE="$work/valid.zip" + +# Describe the actual fixture publication without using production parsing code. +publication() { + local digest + digest="$(shasum -a 256 "$1" | awk '{print $1}')" + jq -n --arg digest "sha256:$digest" '{tag_name:"v7.212.40",draft:false, + immutable:true,published_at:"2026-11-11T00:00:00Z", + assets:[{name:"KSail_7.212.40_darwin_arm64.zip",size:1,digest:$digest}]}' >"$RELEASE_FIXTURE" +} + +publication "$ARCHIVE_FIXTURE" +bash "${script_dir}/verify-published-desktop-archive.sh" v7.212.40 >"$work/valid.log" 2>&1 +grep -q '^PASS: published Desktop archive' "$work/valid.log" +printf 'PASS: valid immutable publication\n' + +ARCHIVE_FIXTURE="$work/invalid.zip" +if bash "${script_dir}/verify-published-desktop-archive.sh" v7.212.40 >"$work/digest.log" 2>&1; then + printf 'FAIL: download with a different checksum was accepted\n' >&2 + exit 1 +fi +grep -q 'does not match its published digest' "$work/digest.log" +printf 'PASS: changed download rejected\n' + +# Even a checksum-matching publication must fail when its signed resources changed. +publication "$ARCHIVE_FIXTURE" +if bash "${script_dir}/verify-published-desktop-archive.sh" v7.212.40 >"$work/seal.log" 2>&1; then + printf 'FAIL: checksum-matching invalid resource seal was accepted\n' >&2 + exit 1 +fi +grep -q 'sealed resource is missing or invalid' "$work/seal.log" +printf 'PASS: checksum-matching invalid resource seal rejected\n' diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 8e618c9378..041306a8cd 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -1065,6 +1065,13 @@ jobs: for name in ksail ksail-desktop; do branch="goreleaser/${name}" + # Check the actual immutable download before either open-PR delivery or + # merged-PR recovery. A checksum match alone cannot prove a valid seal. + if [[ "$name" == ksail-desktop ]] && ! .github/scripts/verify-published-desktop-archive.sh "$TAG"; then + echo "::error::Published Desktop archive failed signature verification; leaving its cask untouched" + handoff_failed=1 + continue + fi # `|| true` keeps the job fail-soft: a transient API failure (5xx, rate limit, # network blip) must not abort the step under `set -e` after the release is already public — # invalid evidence below then warns and skips, same as the no-PR-found case. diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cb07761fb6..4ea4a3b4f6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -452,6 +452,9 @@ jobs: - 'pkg/svc/installer/cni/calico/Dockerfile' - '.github/workflows/ci.yaml' cask-pr-handoff: + - '.github/scripts/verify-published-desktop-archive.sh' + - '.github/scripts/verify-published-desktop-archive.test.sh' + - 'desktop/scripts/verify-macos-archive.sh' - '.github/scripts/cask-read-api.sh' - '.github/scripts/cask-read-api.test.sh' - 'server.json' @@ -725,6 +728,22 @@ jobs: .github/actions/ksail-test-workload/remote-resource-guard.test.sh .github/actions/ksail-test-workload/install-with-recovery.test.sh + desktop-recovery-signature: + name: 🔐 Verify Published Desktop Signature + runs-on: macos-latest + timeout-minutes: 5 + needs: [changes] + if: needs.changes.outputs.cask-pr-handoff == 'true' + permissions: + contents: read + steps: + - name: 📄 Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Verify real resource seals in publication recovery + run: bash .github/scripts/verify-published-desktop-archive.test.sh + cask-pr-handoff: name: 🔐 Validate Cask PR Handoff runs-on: ubuntu-latest @@ -763,6 +782,8 @@ jobs: .github/scripts/upload-release-assets.test.sh \ .github/scripts/validate-cask-pr-handoff.sh \ .github/scripts/validate-cask-pr-handoff.test.sh \ + .github/scripts/verify-published-desktop-archive.sh \ + .github/scripts/verify-published-desktop-archive.test.sh \ .github/scripts/wait-for-github-api-budget.sh \ .github/scripts/wait-for-github-api-budget.test.sh \ .github/scripts/cask-pr-handoff-workflow.test.sh @@ -3032,6 +3053,7 @@ jobs: test-cask-rendering, release-graph-audit, cask-pr-handoff, + desktop-recovery-signature, native-home-paths, windows-notice-staging, eks-smoke-scripts, @@ -3079,6 +3101,7 @@ jobs: ${{ needs.test-cask-rendering.result }} ${{ needs.release-graph-audit.result }} ${{ needs.cask-pr-handoff.result }} + ${{ needs.desktop-recovery-signature.result }} ${{ needs.native-home-paths.result }} ${{ needs.windows-notice-staging.result }} ${{ needs.eks-smoke-scripts.result }} diff --git a/pkg/client/kubescape/release_graph_test.go b/pkg/client/kubescape/release_graph_test.go index 5a1f9f4a9b..c0c2d5106b 100644 --- a/pkg/client/kubescape/release_graph_test.go +++ b/pkg/client/kubescape/release_graph_test.go @@ -422,7 +422,7 @@ func auditedReleaseConfigDigests() map[string]string { ".github/actions/release-admission/action.yaml": "a8ac217c7e2195fee64b364748a6724b99aa0aa4a9988127084f1d6b45953f0c", "scripts/stage-webui.sh": "56f99977b9e5026eedd693e59da4e0cd82d0167e94d0ce4b9c7bb5ce2cd8ed3c", ".github/actions/free-disk-space/free-disk-space.sh": "2dd12fcf3779137ca1cb5f21194947f6a10d3f94f8fb438a1a29ec9021fbdc30", - ".github/workflows/cd.yaml#without-uses": "04ab2010cd4425e47ccadbff1dbadf421e496120b3961dd72496bc0451bb4f1a", + ".github/workflows/cd.yaml#without-uses": "7235813459ca01e063df01e523341533ed62c8f7c4c29428a08efbcff2f20206", } }