55
66permissions :
77 contents : read
8+ id-token : write
9+ attestations : write
810 packages : write
11+ artifact-metadata : write
912
1013concurrency :
1114 group : server-port-preflight-runtime-patch
@@ -105,6 +108,96 @@ jobs:
105108 "$SOURCE_SHA" "$RELEASE_TAG" "$payload_a" "$image_a" "$image_b" |
106109 tee "$RUNNER_TEMP/reproducibility.txt"
107110
111+ - name : Verify scanner and enforce image security gates
112+ shell : bash
113+ run : |
114+ set -Eeuo pipefail
115+ trivy_version=0.73.0
116+ trivy_sha256=2edd39da482bb4e9831962487b68f68e3928ec3137794757f54d00383d79547b
117+ trivy_source_sha=40c73e5d6166dcc0346a1ab4e94499d1572854e4
118+ trivy_root="${RUNNER_TEMP}/trivy-${trivy_version}"
119+ trivy_asset="${trivy_root}/trivy_${trivy_version}_Linux-64bit.tar.gz"
120+ mkdir -p "$trivy_root"
121+
122+ curl --fail --show-error --silent --location \
123+ "https://github.com/aquasecurity/trivy/releases/download/v${trivy_version}/trivy_${trivy_version}_Linux-64bit.tar.gz" \
124+ --output "$trivy_asset"
125+ printf '%s %s\n' "$trivy_sha256" "$trivy_asset" | sha256sum --check
126+ trivy_attestation_status=verified
127+ attestation_error="${trivy_root}/attestation-error.txt"
128+ if ! gh attestation verify "$trivy_asset" \
129+ --repo aquasecurity/trivy \
130+ --signer-workflow aquasecurity/trivy/.github/workflows/reusable-release.yaml \
131+ --source-ref "refs/tags/v${trivy_version}" \
132+ --source-digest "$trivy_source_sha" \
133+ --signer-digest "$trivy_source_sha" \
134+ --deny-self-hosted-runners \
135+ 2>"$attestation_error"; then
136+ if grep -Fq 'organization has an IP allow list enabled' \
137+ "$attestation_error"; then
138+ trivy_attestation_status=unavailable-source-ip-allow-list
139+ printf '::warning::Trivy provenance API is unavailable because the source organization rejects this hosted-runner IP. Exact asset SHA-256 verification remains mandatory.\n'
140+ else
141+ cat "$attestation_error" >&2
142+ exit 1
143+ fi
144+ fi
145+ tar -xzf "$trivy_asset" -C "$trivy_root"
146+ test "$("${trivy_root}/trivy" --version | awk '/^Version:/ {print $2}')" = \
147+ "$trivy_version"
148+
149+ "${trivy_root}/trivy" image \
150+ --cache-dir "${RUNNER_TEMP}/trivy-cache" \
151+ --scanners vuln,secret \
152+ --format json \
153+ --output "${RUNNER_TEMP}/server-security-scan.json" \
154+ "$TARGET_IMAGE"
155+ jq -e '.SchemaVersion == 2 and (.Results | type == "array")' \
156+ "${RUNNER_TEMP}/server-security-scan.json" >/dev/null
157+ jq -r '
158+ .Results[]?
159+ | .Target as $target
160+ | .Vulnerabilities[]?
161+ | select(.Severity == "CRITICAL" or .Severity == "HIGH")
162+ | [.Severity, .VulnerabilityID, .PkgName, .InstalledVersion,
163+ (.FixedVersion // ""), ($target // "")]
164+ | @tsv
165+ ' "${RUNNER_TEMP}/server-security-scan.json" |
166+ LC_ALL=C sort -u >"${RUNNER_TEMP}/server-critical-high.tsv"
167+ jq -r '
168+ .Results[]?
169+ | .Target as $target
170+ | .Secrets[]?
171+ | [.RuleID, .Category, .Severity, (.Title // ""), ($target // "")]
172+ | @tsv
173+ ' "${RUNNER_TEMP}/server-security-scan.json" |
174+ LC_ALL=C sort -u >"${RUNNER_TEMP}/server-secrets.tsv"
175+ test ! -s "${RUNNER_TEMP}/server-critical-high.tsv"
176+ test ! -s "${RUNNER_TEMP}/server-secrets.tsv"
177+
178+ "${trivy_root}/trivy" image \
179+ --cache-dir "${RUNNER_TEMP}/trivy-cache" \
180+ --format cyclonedx \
181+ --output "${RUNNER_TEMP}/server.cdx.json" \
182+ "$TARGET_IMAGE"
183+ jq -e '
184+ .bomFormat == "CycloneDX"
185+ and ((.specVersion | type) == "string")
186+ and ((.components | type) == "array")
187+ and ((.components | length) > 0)
188+ ' "${RUNNER_TEMP}/server.cdx.json" >/dev/null
189+ printf '%s\n' "$trivy_attestation_status" \
190+ >"${RUNNER_TEMP}/trivy-attestation-verification.txt"
191+ "${trivy_root}/trivy" --version >"${RUNNER_TEMP}/trivy-version.txt"
192+ sha256sum \
193+ "${RUNNER_TEMP}/server-security-scan.json" \
194+ "${RUNNER_TEMP}/server-critical-high.tsv" \
195+ "${RUNNER_TEMP}/server-secrets.tsv" \
196+ "${RUNNER_TEMP}/server.cdx.json" \
197+ "${RUNNER_TEMP}/trivy-attestation-verification.txt" \
198+ "${RUNNER_TEMP}/trivy-version.txt" \
199+ >"${RUNNER_TEMP}/server-security-evidence.sha256"
200+
108201 - name : Publish public semantic-version image
109202 id : publish
110203 shell : bash
@@ -133,7 +226,23 @@ jobs:
133226 tee "$RUNNER_TEMP/published.txt"
134227 printf 'digest=%s\n' "$digest" >>"$GITHUB_OUTPUT"
135228
229+ - name : Attest published image provenance
230+ uses : actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1
231+ with :
232+ subject-name : ghcr.io/pasturestack/server
233+ subject-digest : ${{ steps.publish.outputs.digest }}
234+ push-to-registry : true
235+
236+ - name : Attest published image SBOM
237+ uses : actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1
238+ with :
239+ subject-name : ghcr.io/pasturestack/server
240+ subject-digest : ${{ steps.publish.outputs.digest }}
241+ sbom-path : ${{ runner.temp }}/server.cdx.json
242+ push-to-registry : true
243+
136244 - name : Retain publication evidence
245+ if : always()
137246 uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
138247 with :
139248 name : server-${{ env.RELEASE_TAG }}-publication-evidence
@@ -144,6 +253,13 @@ jobs:
144253 ${{ runner.temp }}/push.log
145254 ${{ runner.temp }}/image-inspect.txt
146255 ${{ runner.temp }}/published.txt
256+ ${{ runner.temp }}/server-security-scan.json
257+ ${{ runner.temp }}/server-critical-high.tsv
258+ ${{ runner.temp }}/server-secrets.tsv
259+ ${{ runner.temp }}/server.cdx.json
260+ ${{ runner.temp }}/server-security-evidence.sha256
261+ ${{ runner.temp }}/trivy-attestation-verification.txt
262+ ${{ runner.temp }}/trivy-version.txt
147263 if-no-files-found : error
148264 retention-days : 30
149265 compression-level : 9
0 commit comments