mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-23 03:27:50 +09:00
No release carried a signature or an SBOM, so users could verify none of the ISO, disk images, kernel, initramfs, manifests and cozypkg binaries they download, and the OpenSSF Scorecard Signed-Releases check and CLOMonitor's signed_releases and sbom checks failed. hack/upload-assets.sh now hands its asset list to the new hack/sign-release-assets.sh before the first upload. That script writes cozystack-source-sbom.spdx.json with syft over the committed tree and the cozypkg binaries, cozystack-checksums.txt with the SHA-256 of every asset and the SBOM, and cozystack-checksums.txt.sigstore.json with a keyless cosign signature over the checksums. The SBOM does not describe the container images or the Talos boot images, which is what its name says; image SBOMs are #2937. The script verifies the bundle against the identities of tags.yaml on a v* tag and promote-rc.yaml on main before returning, so a bad signature fails the job with nothing uploaded, and hack/upload-assets.bats pins the upload list and that order. The two jobs that call make upload_assets get id-token: write, cosign v3.1.3 from the SHA-pinned cosign-installer, and syft from its release tarball checked against a committed SHA-256, because anchore/sbom-action/download-syft runs an install script fetched at run time; hack/ci-tool-installs-contract.bats now covers both. docs/release.md describes the files, what the SBOM covers, the minimum cosign version to verify a download with, what the certificate identity proves, and that promote-rc.yaml is dispatched from main only. Refs #4759 Assisted-by: LLM Signed-off-by: Marian Koreniuk <moriarti@cp.if.ua>
151 lines
6.9 KiB
Bash
Executable File
151 lines
6.9 KiB
Bash
Executable File
#!/usr/bin/env bats
|
|
# The flux CLI, yq, syft and the helm-unittest plugin are build inputs like any
|
|
# image or action, so they are pinned the same way: a fixed version, and for
|
|
# flux, yq and syft bytes checked against the release that version names. A
|
|
# floating install comes back by copy-paste from an older step rather than by
|
|
# anyone deciding to unpin one, which is why this scans every workflow instead
|
|
# of the steps that happened to be fixed. Other downloaded tools are not
|
|
# covered here.
|
|
|
|
load test_helper
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
|
|
WORKFLOWS="$REPO_ROOT/.github/workflows"
|
|
|
|
# Prints file:line:text for every non-comment workflow line matching the ERE.
|
|
workflow_code() {
|
|
grep --recursive --line-number --extended-regexp "$1" "$WORKFLOWS" \
|
|
| grep --invert-match --extended-regexp '^[^:]+:[0-9]+:[[:space:]]*#' || true
|
|
}
|
|
|
|
# Prints file:line for every non-comment download matching the URL ERE that is
|
|
# not followed within three lines by `sha256sum -c -` on the named variable.
|
|
# Each download, not each file: one checked step must not cover another.
|
|
unchecked_downloads() {
|
|
awk -v url="$1" -v sum="$2" '
|
|
FNR == 1 { if (pend) print pend; pend = "" }
|
|
/^[[:space:]]*#/ { next }
|
|
$0 ~ url { if (pend) print pend; pend = FILENAME ":" FNR; n = 0; next }
|
|
pend && /sha256sum -c -/ && index($0, sum) { pend = ""; next }
|
|
pend && ++n >= 3 { print pend; pend = "" }
|
|
END { if (pend) print pend }
|
|
' "$WORKFLOWS"/*.yaml "$WORKFLOWS"/*.yml 2>/dev/null
|
|
}
|
|
|
|
# Fails unless every `KEY: "..."` in the workflows has the same value.
|
|
one_value_of() {
|
|
local values
|
|
values="$(workflow_code "$1: \"" | grep --only-matching "$1: \"[0-9a-f.]*\"" | sort --unique)"
|
|
[ "$(printf '%s\n' "$values" | grep --count .)" -eq 1 ] || { printf '%s\n' "$values"; false; }
|
|
}
|
|
|
|
@test "no workflow executes the fluxcd.io install script" {
|
|
# It runs as root whatever fluxcd.io serves at that moment; the binary it
|
|
# checksums is not the exposure, the unpinned script is.
|
|
hits="$(workflow_code 'fluxcd\.io/install\.sh')"
|
|
[ -z "$hits" ] || { printf '%s\n' "$hits"; false; }
|
|
}
|
|
|
|
@test "every use of the flux setup action is SHA-pinned at one version" {
|
|
uses="$(workflow_code 'uses: fluxcd/flux2/action@')"
|
|
[ -n "$uses" ]
|
|
bad="$(printf '%s\n' "$uses" | grep --invert-match --extended-regexp 'action@[0-9a-f]{40} # v[0-9.]+$' || true)"
|
|
[ -z "$bad" ] || { printf '%s\n' "$bad"; false; }
|
|
|
|
# Without a version input the action resolves the latest release itself, and
|
|
# a digest bump moves the ref and its comment but not the input, so each
|
|
# step's input must sit in its own `with:` and match the tag in the comment.
|
|
# One "sha tag input" triple per step; all steps must agree on it.
|
|
steps="$(awk '
|
|
FNR == 1 { if (pend) print sha, tag, "missing"; pend = 0 }
|
|
/^[[:space:]]*#/ { next }
|
|
/uses: fluxcd\/flux2\/action@/ {
|
|
sha = $0; sub(/.*action@/, "", sha); sub(/ .*/, "", sha)
|
|
tag = $0; sub(/.*# v/, "", tag)
|
|
pend = 1; next
|
|
}
|
|
pend && /version: "/ {
|
|
v = $0; sub(/.*version: "/, "", v); sub(/".*/, "", v)
|
|
print sha, tag, v; pend = 0; next
|
|
}
|
|
pend && /^[[:space:]]*- / { print sha, tag, "missing"; pend = 0 }
|
|
END { if (pend) print sha, tag, "missing" }
|
|
' "$WORKFLOWS"/*.yaml "$WORKFLOWS"/*.yml 2>/dev/null)"
|
|
[ "$(printf '%s\n' "$steps" | grep --count .)" -eq "$(printf '%s\n' "$uses" | grep --count .)" ]
|
|
bad="$(printf '%s\n' "$steps" | awk '$2 != $3' || true)"
|
|
[ -z "$bad" ] || { printf '%s\n' "$bad"; false; }
|
|
[ "$(printf '%s\n' "$steps" | sort --unique | wc -l | tr -d ' ')" -eq 1 ] \
|
|
|| { printf '%s\n' "$steps" | sort --unique; false; }
|
|
}
|
|
|
|
@test "no workflow downloads from a floating latest release" {
|
|
hits="$(workflow_code 'releases/latest/download')"
|
|
[ -z "$hits" ] || { printf '%s\n' "$hits"; false; }
|
|
}
|
|
|
|
@test "every yq download is checksum-verified at one version" {
|
|
[ -n "$(workflow_code 'mikefarah/yq/releases/download')" ]
|
|
unchecked="$(unchecked_downloads 'mikefarah/yq/releases/download' YQ_SHA256)"
|
|
[ -z "$unchecked" ] || { printf '%s\n' "$unchecked"; false; }
|
|
one_value_of YQ_VERSION
|
|
one_value_of YQ_SHA256
|
|
}
|
|
|
|
@test "no workflow installs syft through a script fetched at run time" {
|
|
# anchore/sbom-action and its download-syft action are pinned by commit, but
|
|
# run syft's install.sh from a tag of another repository. The release jobs
|
|
# that need syft hold the app token, the GHCR login and an OIDC token for the
|
|
# identity release signatures are verified against.
|
|
hits="$(workflow_code 'anchore/sbom-action|anchore/syft/.*install\.sh|get\.anchore\.io')"
|
|
[ -z "$hits" ] || { printf '%s\n' "$hits"; false; }
|
|
}
|
|
|
|
@test "every syft download is checksum-verified at one version" {
|
|
[ -n "$(workflow_code 'anchore/syft/releases/download')" ]
|
|
unchecked="$(unchecked_downloads 'anchore/syft/releases/download' SYFT_SHA256)"
|
|
[ -z "$unchecked" ] || { printf '%s\n' "$unchecked"; false; }
|
|
one_value_of SYFT_VERSION
|
|
one_value_of SYFT_SHA256
|
|
}
|
|
|
|
@test "every use of the cosign installer is SHA-pinned at one cosign release" {
|
|
# Without cosign-release the action installs its own default, which moves
|
|
# with a digest bump of the action. The release jobs verify their signature
|
|
# with the cosign docs/release.md asks users for, so the version is named in
|
|
# each step, and all steps agree on it and on the action's commit.
|
|
uses="$(workflow_code 'uses: sigstore/cosign-installer@')"
|
|
[ -n "$uses" ]
|
|
bad="$(printf '%s\n' "$uses" | grep --invert-match --extended-regexp 'installer@[0-9a-f]{40} # v[0-9.]+$' || true)"
|
|
[ -z "$bad" ] || { printf '%s\n' "$bad"; false; }
|
|
|
|
# One "sha release" pair per step, "missing" when the step names no release.
|
|
steps="$(awk '
|
|
FNR == 1 { if (pend) print sha, "missing"; pend = 0 }
|
|
/^[[:space:]]*#/ { next }
|
|
/uses: sigstore\/cosign-installer@/ {
|
|
sha = $0; sub(/.*installer@/, "", sha); sub(/ .*/, "", sha)
|
|
pend = 1; next
|
|
}
|
|
pend && /cosign-release:/ {
|
|
v = $0; sub(/.*cosign-release:[[:space:]]*"?/, "", v); sub(/"?[[:space:]]*$/, "", v)
|
|
print sha, v; pend = 0; next
|
|
}
|
|
pend && /^[[:space:]]*- / { print sha, "missing"; pend = 0 }
|
|
END { if (pend) print sha, "missing" }
|
|
' "$WORKFLOWS"/*.yaml "$WORKFLOWS"/*.yml 2>/dev/null)"
|
|
[ "$(printf '%s\n' "$steps" | grep --count .)" -eq "$(printf '%s\n' "$uses" | grep --count .)" ]
|
|
bad="$(printf '%s\n' "$steps" | grep --invert-match --extended-regexp ' v[0-9]+\.[0-9]+\.[0-9]+$' || true)"
|
|
[ -z "$bad" ] || { printf '%s\n' "$bad"; false; }
|
|
[ "$(printf '%s\n' "$steps" | sort --unique | wc -l | tr -d ' ')" -eq 1 ] \
|
|
|| { printf '%s\n' "$steps" | sort --unique; false; }
|
|
}
|
|
|
|
@test "every helm plugin install names a version" {
|
|
# helm-unittest embeds its own Helm, so an unpinned plugin moves the renderer
|
|
# every chart test runs against without any PR asking for it.
|
|
installs="$(workflow_code 'helm plugin install')"
|
|
[ -n "$installs" ]
|
|
bad="$(printf '%s\n' "$installs" | grep --invert-match -- '--version' || true)"
|
|
[ -z "$bad" ] || { printf '%s\n' "$bad"; false; }
|
|
}
|