Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
ad514f8
Added copa to the images build
bluvulture May 26, 2026
f21770e
Buildkit pinned to version
bluvulture May 26, 2026
8bda2a1
Buildkit startup increased
bluvulture May 26, 2026
fe0c54a
Buildkit startup increased
bluvulture May 26, 2026
f311e6f
Buildkit probe
bluvulture May 26, 2026
a063264
Trivy severity gate
bluvulture May 26, 2026
34f7c61
Trivy severity gate
bluvulture May 26, 2026
348d92f
Trivy severity gate
bluvulture May 26, 2026
7329f31
Trivy severity gate
bluvulture May 26, 2026
5f08420
Trivy severity gate
bluvulture May 26, 2026
12f5c12
Trivy severity gate
bluvulture May 26, 2026
8fdacb9
Applied codereview remarks
bluvulture May 27, 2026
0438ccd
Improvements
bluvulture May 27, 2026
fa172d8
Applied codereview remarks
bluvulture May 27, 2026
e675d55
Added build flag
bluvulture Jun 15, 2026
da9ec18
Add design spec for -hardened image tag
bluvulture Jun 15, 2026
85763f3
Publish plain and -hardened image flavors
bluvulture Jun 15, 2026
3047b81
Rename matrix key imagePatch to hardened; update design spec
berfinyuksel Jun 19, 2026
5567217
Update build matrix: bump v4.1 to v4.2, drop plain-only 5.x entry
berfinyuksel Jun 19, 2026
979c8e6
Refactor: split monolithic build step into three focused steps
berfinyuksel Jun 19, 2026
556b609
Skip Copa/Trivy install and BuildKit for non-hardened matrix jobs
berfinyuksel Jun 19, 2026
53f1c58
Use grep -F for Copa checksum lookup to avoid regex interpretation
berfinyuksel Jun 19, 2026
935cf97
Add if-no-files-found: ignore to aggregated tags artifact upload
berfinyuksel Jun 19, 2026
82ab67e
Merge branch '5.x' into image_copa
berfinyuksel Jun 19, 2026
835aed1
Fix variants.txt inconsistency: use newline-separated format and mapfile
berfinyuksel Jun 19, 2026
e7c5647
Fix jq vulnerability gate silently ignoring parse errors
berfinyuksel Jun 19, 2026
f00a6b1
Fix double docker rmi of PLAIN_IMAGE and HARDENED_IMAGE in cleanup
berfinyuksel Jun 19, 2026
0588547
Fix Copa patch -t using bare tag instead of full image reference
berfinyuksel Jun 19, 2026
53bda96
Update spec: v4.1 → v4.2 to match release matrix
berfinyuksel Jun 19, 2026
1e80144
Treat fail_on_severity as an inclusive threshold, validate values
bluvulture Jul 2, 2026
1e5a9b8
Add spec: plain-always-publish gate, SBOM restoration, hardened packa…
bluvulture Jul 2, 2026
0c97134
Spec: record declined buildx-attestation hybrid for SBOM approach
bluvulture Jul 2, 2026
63a1160
Spec: split scope into PR #247 (gate+SBOM+README) and package-docs fo…
bluvulture Jul 2, 2026
f28da6c
Add implementation plan for plain-always-publish + SBOM (PR #247 scope)
bluvulture Jul 2, 2026
30f17e5
Add non-fatal SBOM attach helper (oras) with stub tests
bluvulture Jul 2, 2026
925e7b6
Add scan-patch-gate script: plain always ships, hardened gated, per-v…
bluvulture Jul 2, 2026
ea021e0
scan-patch-gate: fail closed on bad report, guard hash+SBOM, atomic p…
bluvulture Jul 2, 2026
e85868d
release.yml: unconditional Trivy+oras, plain SBOM, delegate gate to s…
bluvulture Jul 2, 2026
147c4a5
release.yml: attach SBOMs on push, defer gate failure to end, run pro…
bluvulture Jul 2, 2026
571a651
test.yml: add scripts job running actionlint and script unit tests
bluvulture Jul 2, 2026
3e605a2
test.yml: bump actions/checkout v2 -> v5 so the new actionlint gate p…
bluvulture Jul 2, 2026
bd68186
README: document Copa hardening, plain-always-publish gate semantics,…
bluvulture Jul 2, 2026
f008140
release.yml: upload SBOMs as build artifact (guaranteed fallback to t…
bluvulture Jul 2, 2026
4a7c2ee
spec: mark all-or-nothing gate decision superseded by 2026-07-02 spec
bluvulture Jul 2, 2026
0af4eb8
release.yml: restore pimcore/docker repo guard on process-tags
bluvulture Jul 2, 2026
c534a0a
spec/plan: split publish path so plain ships before the gate (Task 8)
bluvulture Jul 16, 2026
4080235
release.yml: push plain before the gate, hardened after (plain always…
bluvulture Jul 16, 2026
ed61f31
release.yml: drop dead ARCH_TAG env from the split push steps
bluvulture Jul 16, 2026
2449e03
release.yml: fix critical/important review findings
bluvulture Jul 16, 2026
65e0aea
docs: correct hardened gate guarantee, SBOM claim, threshold semantic…
bluvulture Jul 16, 2026
62e69c3
tests: assert which image is scanned/SBOM'd, exact hardened tags, gat…
bluvulture Jul 16, 2026
738d320
docs: finish v5.1->v5.2 sweep; correct false gate/SBOM claims echoed …
bluvulture Jul 16, 2026
50bbaa3
Extract fail_on_severity normalization into a tested script (review R1)
bluvulture Jul 16, 2026
4826ba8
tests: cover normalize-severity, gate --ignore-unfixed/--pkg-types, a…
bluvulture Jul 16, 2026
abbfc9e
Merge origin/5.x into image_copa
bluvulture Jul 16, 2026
8f56942
ci: fix shellcheck findings surfaced by actionlint on the runner
bluvulture Jul 16, 2026
5d44536
Address Copilot high-severity review findings
bluvulture Jul 16, 2026
cc0c601
Add publish_hardened dispatch input (default false) to gate hardened …
bluvulture Jul 16, 2026
de0c51f
Add spec: containerd image store so Copa patches locally (test harden…
bluvulture Jul 17, 2026
5b5a823
Add implementation plan: containerd image store for local Copa patching
bluvulture Jul 17, 2026
f240858
scan-patch-gate: pass copa -a only when BUILDKIT_ADDR is set
bluvulture Jul 17, 2026
585fd94
release: enable containerd image store on hardened legs, drop standal…
bluvulture Jul 17, 2026
50caad1
docs: resolve I4 (Copa patches local image via containerd store); doc…
bluvulture Jul 17, 2026
15c6475
docs: fix stale Copa-pulls-from-registry claim contradicting resolved…
bluvulture Jul 17, 2026
21f7569
Final-review fixes: drop dead BUILDKIT_VERSION, fix stale plain-ships…
bluvulture Jul 17, 2026
ab6aa3e
docs: soften 'un-foolable' validation wording; offer digest compariso…
bluvulture Jul 17, 2026
d5e3073
scan-patch-gate: drop partial hardened SBOM on failure; cover post-pa…
bluvulture Jul 20, 2026
d0b9d29
PR comments
bluvulture Jul 20, 2026
c3586e2
Merge branch '5.x' into image_copa
bluvulture Jul 20, 2026
34bce5c
Default publish non-copa true
bluvulture Jul 20, 2026
e29607a
release: restore hardened matrix flag (fix actionlint 'hardened not d…
bluvulture Jul 20, 2026
8d77124
release: fix misleading gate-failure message (no publish claim on dry…
bluvulture Jul 20, 2026
478a026
Add spec: SBOM referrers on the logical multi-arch tags
bluvulture Jul 20, 2026
4b01605
Add plan: SBOM referrers on logical multi-arch tags
bluvulture Jul 20, 2026
5553c6d
plan: use non-empty SBOM files in the attach test (attach-sbom skips …
bluvulture Jul 20, 2026
f9d3eb2
release: extract oras install + process-tags merge into tested script…
bluvulture Jul 20, 2026
c365a9c
release: attach per-arch SBOMs to logical multi-arch tags (oras disco…
bluvulture Jul 20, 2026
a7afc9a
test: assert SBOM referrer targets the LOGICAL tag (close vacuous-ass…
bluvulture Jul 20, 2026
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
19 changes: 19 additions & 0 deletions .github/scripts/attach-sbom.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Attach an SPDX SBOM to a pushed image as an OCI referrer.
# Non-fatal: a registry that rejects referrers must not break publishing.
set -euo pipefail

ref="${1:?usage: attach-sbom.sh <image-ref> <sbom-file>}"
sbom="${2:?usage: attach-sbom.sh <image-ref> <sbom-file>}"

if [ ! -s "$sbom" ]; then
echo "::warning::SBOM '$sbom' missing or empty; skipping attach for ${ref}"
exit 0
fi

if oras attach --artifact-type application/spdx+json "$ref" "${sbom}:application/spdx+json"; then
echo "Attached SBOM ${sbom} to ${ref}"
else
echo "::warning::Failed to attach SBOM to ${ref} (registry may not support OCI referrers)"
fi
exit 0
22 changes: 22 additions & 0 deletions .github/scripts/install-oras.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Install oras (checksum-verified) to /usr/local/bin. Version from ORAS_VERSION.
set -euxo pipefail
: "${ORAS_VERSION:?ORAS_VERSION must be set}"
ORAS_ARCH="$(dpkg --print-architecture)"
curl -fsSL -o oras.tar.gz "https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_${ORAS_ARCH}.tar.gz"
curl -fsSL -o oras_checksums.txt "https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_checksums.txt"
# Match the filename exactly ($2 == f): a substring match can also hit
# a sibling entry like *.tar.gz.sbom.json and return two hashes.
EXPECTED_SHA=$(awk -v f="oras_${ORAS_VERSION}_linux_${ORAS_ARCH}.tar.gz" '$2 == f {print $1}' oras_checksums.txt)
ACTUAL_SHA=$(sha256sum oras.tar.gz | awk '{print $1}')
if [ -z "$EXPECTED_SHA" ]; then
echo "::error::No oras checksum entry for oras_${ORAS_VERSION}_linux_${ORAS_ARCH}.tar.gz"
exit 1
fi
if [ "$EXPECTED_SHA" != "$ACTUAL_SHA" ]; then
echo "::error::oras checksum mismatch! Expected ${EXPECTED_SHA}, got ${ACTUAL_SHA}"
exit 1
fi
tar -xzf oras.tar.gz oras
sudo mv oras /usr/local/bin/oras
rm oras.tar.gz oras_checksums.txt
46 changes: 46 additions & 0 deletions .github/scripts/merge-manifests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Merge per-arch tags pushed THIS run into logical multi-arch manifests, and attach each
# image's per-arch SBOMs to the logical tag as best-effort OCI referrers.
# AGG_FILE lines: "<per-arch-tag>\t<sbom-relpath>".
set -uo pipefail
AGG_FILE="${AGG_FILE:-all_aggregated_tags.txt}"
HERE="$(cd "$(dirname "$0")" && pwd)"

declare -A HAS_AMD64 HAS_ARM64 LOGICAL SBOM_AMD64 SBOM_ARM64
while IFS=$'\t' read -r t sbom; do
[ -z "$t" ] && continue
case "$t" in
*-amd64) lt="${t%-amd64}"; HAS_AMD64["$lt"]=1; LOGICAL["$lt"]=1; SBOM_AMD64["$lt"]="$sbom" ;;
*-arm64) lt="${t%-arm64}"; HAS_ARM64["$lt"]=1; LOGICAL["$lt"]=1; SBOM_ARM64["$lt"]="$sbom" ;;
*) echo "Skipping tag without arch suffix: $t" ;;
esac
done < "$AGG_FILE"

failed=0
for lt in "${!LOGICAL[@]}"; do
if [ -n "${HAS_AMD64[$lt]:-}" ] && [ -n "${HAS_ARM64[$lt]:-}" ]; then
echo "Creating multi-arch manifest: $lt"
if ! docker buildx imagetools create --tag "$lt" "${lt}-amd64" "${lt}-arm64"; then
echo "::error::Failed to create multi-arch manifest for $lt"
failed=1
continue
fi
# Attach each per-arch SBOM to the logical (index) tag so `oras discover <logical-tag>`
# finds it. Best-effort: attach-sbom.sh never fails the job.
for sb in "${SBOM_AMD64[$lt]:-}" "${SBOM_ARM64[$lt]:-}"; do
if [ -n "$sb" ] && [ -f "$sb" ]; then
"$HERE/attach-sbom.sh" "$lt" "$sb"
else
echo "No SBOM file for $lt referrer (path: '${sb:-}') -- skipping (artifact copy still uploaded)"
fi
done
else
echo "Skipping $lt: only one arch pushed this run (amd64=${HAS_AMD64[$lt]:-0} arm64=${HAS_ARM64[$lt]:-0}); previous manifest left unchanged"
fi
done

if [ "$failed" -ne 0 ]; then
echo "::error::One or more multi-arch manifests failed to publish"
exit 1
fi
exit 0
51 changes: 51 additions & 0 deletions .github/scripts/normalize-severity.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Normalise a fail_on_severity value into a validated, inclusive Trivy --severity list.
#
# fail_on_severity is a case-insensitive THRESHOLD: naming a severity also gates
# everything above it (e.g. HIGH -> HIGH,CRITICAL), because Trivy's --severity is an
# exact filter that would otherwise let higher severities slip through. A comma-separated
# set is allowed; the lowest-ranked member wins. NONE disables the gate.
#
# Prints the normalised value to stdout ("NONE" when disabled). Exits 1 (with an
# ::error:: on stderr) when the input names no valid severity.
set -euo pipefail

input="${1:?usage: normalize-severity.sh <fail_on_severity>}"
SEVERITY_ORDER="UNKNOWN LOW MEDIUM HIGH CRITICAL"

if [ "${input^^}" = "NONE" ]; then
echo "NONE"
exit 0
fi

min_rank=-1
IFS=',' read -r -a requested <<< "${input^^}"
for sev in "${requested[@]}"; do
sev="${sev// /}"
[ -z "$sev" ] && continue
rank=-1; i=0
for known in $SEVERITY_ORDER; do
if [ "$known" = "$sev" ]; then rank=$i; fi
i=$((i + 1))
done
if [ "$rank" -lt 0 ]; then
echo "::error::Invalid fail_on_severity value '${sev}'. Allowed: ${SEVERITY_ORDER// /, }, or NONE." >&2
exit 1
fi
if [ "$min_rank" -lt 0 ] || [ "$rank" -lt "$min_rank" ]; then min_rank=$rank; fi
done

if [ "$min_rank" -lt 0 ]; then
echo "::error::fail_on_severity='${input}' names no valid severity. Use ${SEVERITY_ORDER// /, }, or NONE." >&2
exit 1
fi

# Emit the inclusive range from the lowest requested severity up to CRITICAL.
out=""; i=0
for known in $SEVERITY_ORDER; do
if [ "$i" -ge "$min_rank" ]; then
out="${out:+$out,}$known"
fi
i=$((i + 1))
done
echo "$out"
111 changes: 111 additions & 0 deletions .github/scripts/scan-patch-gate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# Per-variant: scan the plain image, patch with Copa (or mirror if nothing fixable),
# gate the hardened image on GATE_SEVERITY, and -- only if it passes -- publish the
# hardened outputs and generate its SPDX SBOM. A gate failure (or scan/patch error)
# writes gate_failed.txt, skips the hardened outputs, and exits 0 so the plain image
# still ships and other variants continue. Genuine infra errors abort (set -e).
set -euo pipefail

variant="${1:?usage: scan-patch-gate.sh <variant>}"
: "${IMAGE_NAME:?}"; : "${ARCH_TAG:?}"; : "${GATE_SEVERITY:?}"
STATE_DIR="${STATE_DIR:-.docker-state}"
SBOM_DIR="${SBOM_DIR:-sboms}"
REPORT_DIR="${REPORT_DIR:-trivy-reports}"
BUILDKIT_ADDR="${BUILDKIT_ADDR:-}"
vdir="${STATE_DIR}/${variant}"
mkdir -p "$SBOM_DIR" "$REPORT_DIR"

PLAIN_IMAGE=$(< "${vdir}/plain_image.txt")
BASE_TAG=$(< "${vdir}/base_tag.txt")
VERSION=$(< "${vdir}/version.txt")
TAG=$(< "${vdir}/tag.txt")
HARDENED_IMAGE="${IMAGE_NAME}:${BASE_TAG}-${VERSION}-hardened-${ARCH_TAG}"
report="/tmp/spg-${variant}.json"

fail_gate() { # <reason> -- record + skip hardened, but let plain ship
echo "::error::${variant}: $1"
{ echo "## Gate failed: ${HARDENED_IMAGE}"; echo ""; echo "$1"; echo ""; } >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
echo "$1" > "${vdir}/gate_failed.txt"
# Remove the hardened image now: we're about to delete the state files the cleanup
# step reads, so it can no longer reclaim it -- avoid leaking large images across
# failed variants on a reused runner.
docker rmi "${HARDENED_IMAGE}" 2>/dev/null || true
rm -f "${vdir}/hardened_image.txt" "${vdir}/hardened_tags.txt" "${vdir}/hardened_sbom.txt"
rm -f "$report"
# If SBOM generation created/truncated its output before failing, drop the partial file
# so the always-runs artifact upload never exposes a hardened SBOM for a variant whose
# gate failed (HARDENED_SBOM is unset for failures before the SBOM step -> no-op).
[ -n "${HARDENED_SBOM:-}" ] && rm -f "${HARDENED_SBOM}"
exit 0
}

echo "Scanning plain image ${PLAIN_IMAGE} for OS vulnerabilities"
trivy image --pkg-types os --ignore-unfixed --format json -o "$report" "${PLAIN_IMAGE}" \
|| fail_gate "Trivy scan of plain image failed"

jq empty "$report" 2>/dev/null || fail_gate "Trivy report of plain image is not valid JSON"

if [ -s "$report" ] && jq -e '.Results[]? | select(.Vulnerabilities != null and (.Vulnerabilities | length > 0))' "$report" > /dev/null; then
# Pass -a only when an address is configured. With the containerd image store
# enabled, BUILDKIT_ADDR is unset and Copa uses its default connection
# (docker driver -> dockerd's embedded BuildKit), which sees the local image.
# Setting BUILDKIT_ADDR (e.g. a standalone buildkitd) restores the -a path.
copa_addr=()
[ -n "${BUILDKIT_ADDR}" ] && copa_addr=(-a "${BUILDKIT_ADDR}")
copa patch -i "${PLAIN_IMAGE}" -r "$report" -t "${HARDENED_IMAGE}" "${copa_addr[@]}" \
|| fail_gate "Copa patch failed"
docker image inspect "${HARDENED_IMAGE}" > /dev/null 2>&1 \
|| fail_gate "Hardened image not found after copa patch"
echo "Successfully patched ${PLAIN_IMAGE} into ${HARDENED_IMAGE}"
else
echo "No fixable OS vulnerabilities found; hardened image mirrors plain"
docker tag "${PLAIN_IMAGE}" "${HARDENED_IMAGE}"
fi
rm -f "$report"

if [ "$GATE_SEVERITY" != "NONE" ]; then
echo "Running post-patch scan (fail on ${GATE_SEVERITY})"
if ! HARDENED_ID=$(docker image inspect "${HARDENED_IMAGE}" --format '{{.Id}}'); then
fail_gate "could not inspect hardened image for report hash"
fi
IMAGE_HASH="${HARDENED_ID#sha256:}"
IMAGE_HASH="${IMAGE_HASH:0:12}"
REPORT_JSON="${REPORT_DIR}/${TAG}-hardened_${IMAGE_HASH}.json"
REPORT_TXT="${REPORT_DIR}/${TAG}-hardened_${IMAGE_HASH}.txt"

trivy image --pkg-types os --ignore-unfixed --severity "$GATE_SEVERITY" \
--format json -o "${REPORT_JSON}" "${HARDENED_IMAGE}" \
|| fail_gate "Trivy gate scan failed"

trivy image --pkg-types os --ignore-unfixed --severity "$GATE_SEVERITY" \
--format table -o "/tmp/spg-${variant}.txt" "${HARDENED_IMAGE}" || true
Comment thread
bluvulture marked this conversation as resolved.
cp "/tmp/spg-${variant}.txt" "${REPORT_TXT}" 2>/dev/null || true
{
echo "## Trivy Scan: ${HARDENED_IMAGE}"
echo ""
echo "### OS Vulnerabilities (${GATE_SEVERITY})"
echo '```'
cat "/tmp/spg-${variant}.txt" 2>/dev/null || echo "No results"
echo '```'
echo ""
} >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
rm -f "/tmp/spg-${variant}.txt"

jq empty "${REPORT_JSON}" 2>/dev/null || fail_gate "Trivy gate report is not valid JSON"
Comment thread
bluvulture marked this conversation as resolved.

if jq -e '.Results[]? | select((.Vulnerabilities // []) | length > 0)' "${REPORT_JSON}" > /dev/null; then
fail_gate "unfixed ${GATE_SEVERITY} vulnerabilities remain after patching"
fi
fi

# Gate passed (or disabled): generate the SBOM first, then publish the markers atomically.
HARDENED_SBOM="${SBOM_DIR}/${BASE_TAG}-${VERSION}-hardened-${ARCH_TAG}.spdx.json"
trivy image --format spdx-json -o "${HARDENED_SBOM}" "${HARDENED_IMAGE}" \
|| fail_gate "hardened SBOM generation failed"
Comment thread
bluvulture marked this conversation as resolved.

while IFS= read -r plain_tag; do
echo "${plain_tag%-"${ARCH_TAG}"}-hardened-${ARCH_TAG}"
done < "${vdir}/plain_tags.txt" > "${vdir}/hardened_tags.txt"
echo "${HARDENED_IMAGE}" > "${vdir}/hardened_image.txt"
echo "${HARDENED_SBOM}" > "${vdir}/hardened_sbom.txt"
echo "Published hardened outputs for ${variant}"
Loading
Loading