From 9c6be1f65a7e5bc2803ebff97c10beacb9334de1 Mon Sep 17 00:00:00 2001 From: "nebojsa.ilic" <7668379+bluvulture@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:41:11 +0200 Subject: [PATCH 1/3] release: make the hardened severity gate non-fatal (warn, don't fail the job) A Copa-patch/gate failure now skips only the -hardened tag for that variant and surfaces a ::warning:: + step-summary entry, instead of failing the whole leg. Plain images already shipped, and some stable lines (e.g. EOL Debian bullseye, whose fixed packages are no longer served) can never be fully patched -- those must not turn the pipeline red. Hardened publishing is best-effort. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f17fb9c..8174283 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -376,16 +376,22 @@ jobs: path: aggregated_tags.txt if-no-files-found: ignore - - name: Fail if severity gate failed + - name: Report severity gate results if: ${{ matrix.build.hardened }} run: | + # Hardened publishing is best-effort: a gate / Copa-patch failure skips the + # -hardened tag for that variant but must NOT fail the job -- the plain image + # already shipped, and some stable lines (e.g. EOL Debian releases whose fixed + # packages are no longer served) can never be fully patched. The failure is + # surfaced as a warning and in the step summary (scan-patch-gate.sh records the + # reason there), not as a red pipeline. if compgen -G '.docker-state/*/gate_failed.txt' > /dev/null; then - echo "The following variants failed the severity gate; their -hardened tags were NOT published:" + echo "The following variants failed the severity gate; their -hardened tags were NOT published (plain images shipped as usual):" grep -H . .docker-state/*/gate_failed.txt - echo "::error::One or more variants failed the severity gate; only their -hardened tags were skipped (plain image handling is unaffected)" - exit 1 + echo "::warning::One or more variants failed the severity gate; their -hardened tags were skipped (plain image handling is unaffected)." + else + echo "All hardened variants passed the severity gate." fi - echo "All hardened variants passed the severity gate." process-tags: runs-on: ubuntu-22.04 From b87f0e64013ae2c34c8eacc7d7e13d780b9cae7c Mon Sep 17 00:00:00 2001 From: "nebojsa.ilic" <7668379+bluvulture@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:41:11 +0200 Subject: [PATCH 2/3] attach-sbom: qualify bare Docker Hub refs as docker.io/ for oras oras (unlike the docker CLI) does not default a bare 'repo/name:tag' to docker.io -- it reads the first path segment as the registry host, so 'pimcore/pimcore:tag' became a DNS lookup of host 'pimcore' and every Docker Hub SBOM attach silently failed (only ghcr.io referrers landed). Prefix docker.io/ for bare refs; leave refs that already carry a registry host (ghcr.io/..., localhost:5000/...) untouched. Fixes plain, hardened, and logical-multiarch-tag attaches at once. Mutation-verified. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/scripts/attach-sbom.sh | 10 ++++++++++ .github/scripts/tests/run.sh | 14 ++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/scripts/attach-sbom.sh b/.github/scripts/attach-sbom.sh index befc890..bd24996 100755 --- a/.github/scripts/attach-sbom.sh +++ b/.github/scripts/attach-sbom.sh @@ -6,6 +6,16 @@ set -euo pipefail ref="${1:?usage: attach-sbom.sh }" sbom="${2:?usage: attach-sbom.sh }" +# oras does not apply the docker CLI's implicit docker.io default: a bare +# "repo/name:tag" is parsed with registry = "repo" (DNS lookup fails, attach is +# lost). Qualify bare Docker Hub refs so oras resolves and authenticates against +# docker.io. Refs whose first path segment already looks like a registry host +# (contains a "." or a ":port") are left untouched (e.g. ghcr.io/..., localhost:5000/...). +case "${ref%%/*}" in + *.*|*:*) ;; + *) ref="docker.io/${ref}" ;; +esac + if [ ! -s "$sbom" ]; then echo "::warning::SBOM '$sbom' missing or empty; skipping attach for ${ref}" exit 0 diff --git a/.github/scripts/tests/run.sh b/.github/scripts/tests/run.sh index c015845..e83bf63 100755 --- a/.github/scripts/tests/run.sh +++ b/.github/scripts/tests/run.sh @@ -90,6 +90,16 @@ assert_contains "$out" "Attached" "success prints Attached" orasCallLog="$(cat "$orasLog" 2>/dev/null)" assert_contains "$orasCallLog" "attach --artifact-type application/spdx+json" "oras invoked with attach --artifact-type application/spdx+json" assert_contains "$orasCallLog" "${work}/s.spdx.json:application/spdx+json" "oras blob arg carries :application/spdx+json media-type suffix" +# oras needs a fully-qualified registry: a bare Docker Hub ref must be prefixed docker.io/ +# (else oras reads "pimcore" as the registry host and the attach is silently lost). +assert_contains "$orasCallLog" "attach --artifact-type application/spdx+json docker.io/pimcore/pimcore:php8.5-v5-amd64" "bare Docker Hub ref qualified to docker.io for oras" + +# a ref that already carries a registry host (ghcr.io/...) is left untouched +ghLog="$(mktemp)"; tmpdirs+=("$ghLog") +STUB_ORAS=ok STUB_LOG="$ghLog" "${ROOT}/.github/scripts/attach-sbom.sh" ghcr.io/pimcore/pimcore:php8.5-v5-amd64 "${work}/s.spdx.json" >/dev/null 2>&1 +ghCallLog="$(cat "$ghLog" 2>/dev/null)" +assert_contains "$ghCallLog" "attach --artifact-type application/spdx+json ghcr.io/pimcore/pimcore:php8.5-v5-amd64" "ghcr.io ref left unqualified (already a registry host)" +assert_not_contains "$ghCallLog" "docker.io/ghcr.io" "ghcr.io ref not double-prefixed with docker.io" # failure path is swallowed out="$(STUB_ORAS=fail "${ROOT}/.github/scripts/attach-sbom.sh" pimcore/pimcore:php8.5-v5-amd64 "${work}/s.spdx.json" 2>&1)"; rc=$? @@ -262,8 +272,8 @@ oras_attaches="$(grep -c 'attach' "$logS" 2>/dev/null || true)" # Exact subject binding: each referrer must target the LOGICAL tag (no arch suffix), # keyed to the matching per-arch SBOM. Catches a regression that attaches to the child # (…-amd64/…-arm64) ref instead of the index -- which would defeat the whole feature. -assert_contains "$(cat "$logS")" "attach --artifact-type application/spdx+json pimcore/pimcore:php8.5-v5.2 sboms/php8.5-v5.2-amd64.spdx.json" "S amd64 SBOM attached to the LOGICAL tag" -assert_contains "$(cat "$logS")" "attach --artifact-type application/spdx+json pimcore/pimcore:php8.5-v5.2 sboms/php8.5-v5.2-arm64.spdx.json" "S arm64 SBOM attached to the LOGICAL tag" +assert_contains "$(cat "$logS")" "attach --artifact-type application/spdx+json docker.io/pimcore/pimcore:php8.5-v5.2 sboms/php8.5-v5.2-amd64.spdx.json" "S amd64 SBOM attached to the LOGICAL tag (docker.io-qualified)" +assert_contains "$(cat "$logS")" "attach --artifact-type application/spdx+json docker.io/pimcore/pimcore:php8.5-v5.2 sboms/php8.5-v5.2-arm64.spdx.json" "S arm64 SBOM attached to the LOGICAL tag (docker.io-qualified)" echo; [ "$fail" = "0" ] && echo "ALL TESTS PASSED" || echo "TESTS FAILED" exit "$fail" From 7b0da661e350e3e274f1f2cb1de2e14c1cc409f4 Mon Sep 17 00:00:00 2001 From: "nebojsa.ilic" <7668379+bluvulture@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:50:39 +0200 Subject: [PATCH 3/3] attach-sbom: follow Docker reference rules in registry normalization Addresses review feedback on the docker.io-qualification heuristic: - single-component refs (no slash, e.g. alpine:latest) are official images -> docker.io/library/ (previously the tag colon matched *:* and the ref was left unqualified -> invalid for oras). - localhost is a registry host -> left as-is (previously rewritten to docker.io/localhost/...). localhost:5000/... already matched via the port colon. Our own refs (pimcore/pimcore:tag -> docker.io/..., ghcr.io/... unchanged) are unaffected. Added regression tests for both edge cases. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/scripts/attach-sbom.sh | 20 ++++++++++++++------ .github/scripts/tests/run.sh | 11 +++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/.github/scripts/attach-sbom.sh b/.github/scripts/attach-sbom.sh index bd24996..98b7245 100755 --- a/.github/scripts/attach-sbom.sh +++ b/.github/scripts/attach-sbom.sh @@ -8,12 +8,20 @@ sbom="${2:?usage: attach-sbom.sh }" # oras does not apply the docker CLI's implicit docker.io default: a bare # "repo/name:tag" is parsed with registry = "repo" (DNS lookup fails, attach is -# lost). Qualify bare Docker Hub refs so oras resolves and authenticates against -# docker.io. Refs whose first path segment already looks like a registry host -# (contains a "." or a ":port") are left untouched (e.g. ghcr.io/..., localhost:5000/...). -case "${ref%%/*}" in - *.*|*:*) ;; - *) ref="docker.io/${ref}" ;; +# lost). Normalise to a fully-qualified reference following Docker's rules so oras +# resolves and authenticates correctly: +# - no "/" -> official image, docker.io/library/ +# - first segment contains "." or ":" or is "localhost" -> already a registry host, keep as-is +# (ghcr.io/..., localhost/..., localhost:5000/...) +# - otherwise (namespace/repo[:tag]) -> bare Docker Hub ref, docker.io/ +case "$ref" in + */*) + case "${ref%%/*}" in + *.*|*:*|localhost) ;; + *) ref="docker.io/${ref}" ;; + esac + ;; + *) ref="docker.io/library/${ref}" ;; esac if [ ! -s "$sbom" ]; then diff --git a/.github/scripts/tests/run.sh b/.github/scripts/tests/run.sh index e83bf63..effb080 100755 --- a/.github/scripts/tests/run.sh +++ b/.github/scripts/tests/run.sh @@ -101,6 +101,17 @@ ghCallLog="$(cat "$ghLog" 2>/dev/null)" assert_contains "$ghCallLog" "attach --artifact-type application/spdx+json ghcr.io/pimcore/pimcore:php8.5-v5-amd64" "ghcr.io ref left unqualified (already a registry host)" assert_not_contains "$ghCallLog" "docker.io/ghcr.io" "ghcr.io ref not double-prefixed with docker.io" +# single-component ref (no slash) is an official image -> docker.io/library/ +scLog="$(mktemp)"; tmpdirs+=("$scLog") +STUB_ORAS=ok STUB_LOG="$scLog" "${ROOT}/.github/scripts/attach-sbom.sh" alpine:latest "${work}/s.spdx.json" >/dev/null 2>&1 +assert_contains "$(cat "$scLog")" "attach --artifact-type application/spdx+json docker.io/library/alpine:latest" "single-component ref -> docker.io/library/" + +# localhost is a registry host -> left as-is (not rewritten to Docker Hub) +lhLog="$(mktemp)"; tmpdirs+=("$lhLog") +STUB_ORAS=ok STUB_LOG="$lhLog" "${ROOT}/.github/scripts/attach-sbom.sh" localhost/repo:tag "${work}/s.spdx.json" >/dev/null 2>&1 +assert_contains "$(cat "$lhLog")" "attach --artifact-type application/spdx+json localhost/repo:tag" "localhost recognized as registry host (left as-is)" +assert_not_contains "$(cat "$lhLog")" "docker.io/localhost" "localhost ref not prefixed with docker.io" + # failure path is swallowed out="$(STUB_ORAS=fail "${ROOT}/.github/scripts/attach-sbom.sh" pimcore/pimcore:php8.5-v5-amd64 "${work}/s.spdx.json" 2>&1)"; rc=$? assert_contains "$out" "::warning::" "failure prints warning"