Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 36 additions & 0 deletions .github/scripts/notify-publish-safety.rb
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,40 @@ def resume_action(existing_digest, expected_digest, mutable:)
!recovery_text.include?("docker/build-push-action@"),
"image recovery must only verify the already-published image and attestations")

[
"Normalize deterministic attestation SBOM metadata",
"Normalize deterministic SBOM metadata"
].each do |step_name|
job_name = step_name.include?("attestation") ? "attest-binaries" : "release-binaries"
step_text = steps(jobs.fetch(job_name)).find { |step| step["name"] == step_name }&.fetch("run", "") || ""
assert_policy(step_text.include?("creationInfo.created") &&
step_text.include?("documentNamespace") &&
step_text.include?("walk(") &&
step_text.include?("annotationDate") &&
step_text.include?("1970-01-01T00:00:00Z"),
"#{job_name}/#{step_name} must normalize every runtime timestamp")
end

adopt_sbom = steps(jobs.fetch("release-binaries")).find do |step|
step["name"] == "Verify and adopt the immutable staged SBOM in recovery"
end
adopt_text = adopt_sbom&.fetch("run", "") || ""
assert_policy(adopt_sbom &&
adopt_sbom.fetch("if", "").include?("needs.publish-gate.outputs.recovery_mode == 'true'") &&
adopt_text.include?("gh api graphql") &&
adopt_text.include?("databaseId") &&
adopt_text.include?('releases/${release_id}') &&
adopt_text.include?('test "$matches" = 1') &&
adopt_text.include?('releases/assets/${asset_id}') &&
adopt_text.include?("Accept: application/octet-stream") &&
adopt_text.include?('^sha256:[0-9a-f]{64}$') &&
adopt_text.include?("canonical_filter") &&
adopt_text.include?("annotationDate") &&
adopt_text.include?("cmp ") &&
adopt_text.include?('mv "$staged" "$generated"') &&
!adopt_text.match?(/gh release (?:upload|edit|delete)/),
"recovery must adopt an existing SBOM only after exact API-digest and canonical-content verification")

stage_release = steps(jobs.fetch("release-binaries")).find do |step|
step["name"] == "Create or verify the exact draft release and assets"
end
Expand Down Expand Up @@ -533,6 +567,8 @@ def resume_action(existing_digest, expected_digest, mutable:)
rollout_text.include?('Accept: application/octet-stream') &&
rollout_text.include?('test "$matches" = 1') &&
rollout_text.include?('^sha256:[0-9a-f]{64}$') &&
rollout_text.include?("binary_sbom") &&
rollout_text.include?("image_digests_sha256") &&
!rollout_text.include?('gh release download "$RELEASE_TAG"'),
"published rollout must resolve draft assets by validated release and asset IDs")
assert_policy((%w[rollout-verify image-ready] - jobs.fetch("finalize-release").fetch("needs")).empty?,
Expand Down
88 changes: 84 additions & 4 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,11 @@ jobs:
run: |
set -euo pipefail
jq --arg namespace "https://github.com/${GITHUB_REPOSITORY}/sbom/${RELEASE_TAG}/${RELEASE_SHA}" \
'.creationInfo.created = "1970-01-01T00:00:00Z" | .documentNamespace = $namespace' \
'.creationInfo.created = "1970-01-01T00:00:00Z"
| .documentNamespace = $namespace
| walk(if type == "object" and has("annotationDate")
then .annotationDate = "1970-01-01T00:00:00Z"
else . end)' \
notify/dist/SBOM.spdx.json > /tmp/SBOM.spdx.json
mv /tmp/SBOM.spdx.json notify/dist/SBOM.spdx.json

Expand Down Expand Up @@ -888,10 +892,76 @@ jobs:
run: |
set -euo pipefail
jq --arg namespace "https://github.com/${GITHUB_REPOSITORY}/sbom/${RELEASE_TAG}/${RELEASE_SHA}" \
'.creationInfo.created = "1970-01-01T00:00:00Z" | .documentNamespace = $namespace' \
'.creationInfo.created = "1970-01-01T00:00:00Z"
| .documentNamespace = $namespace
| walk(if type == "object" and has("annotationDate")
then .annotationDate = "1970-01-01T00:00:00Z"
else . end)' \
notify/dist/SBOM.spdx.json > /tmp/SBOM.spdx.json
mv /tmp/SBOM.spdx.json notify/dist/SBOM.spdx.json

- name: Verify and adopt the immutable staged SBOM in recovery
if: needs.publish-gate.outputs.recovery_mode == 'true'
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.publish-gate.outputs.release_tag }}
RELEASE_VERSION: ${{ needs.publish-gate.outputs.version }}
RELEASE_SHA: ${{ needs.publish-gate.outputs.release_sha }}
run: |
set -euo pipefail
repository_owner=${GITHUB_REPOSITORY%%/*}
repository_name=${GITHUB_REPOSITORY#*/}
# GraphQL includes drafts; REST by-tag does not.
# shellcheck disable=SC2016
lookup=$(gh api graphql \
-F owner="$repository_owner" -F name="$repository_name" -F tag="$RELEASE_TAG" \
-f query='query($owner: String!, $name: String!, $tag: String!) {
repository(owner: $owner, name: $name) {
release(tagName: $tag) { databaseId }
}
}')
jq -e '.data.repository != null and ((.errors // []) | length == 0)' <<<"$lookup" >/dev/null
release_id=$(jq -r '.data.repository.release.databaseId // empty' <<<"$lookup")
printf '%s\n' "$release_id" | grep -Eq '^[1-9][0-9]*$'
release_json=$(gh api "repos/${GITHUB_REPOSITORY}/releases/${release_id}")
test "$(jq -r .tag_name <<<"$release_json")" = "$RELEASE_TAG"
test "$(jq -r .name <<<"$release_json")" = "vaultsync-notify ${RELEASE_VERSION}"
test "$(jq -r .prerelease <<<"$release_json")" = false
case $(jq -r .draft <<<"$release_json") in true|false) ;; *) exit 1 ;; esac

asset_name=SBOM.spdx.json
matches=$(jq -r --arg name "$asset_name" '[.assets[] | select(.name == $name)] | length' <<<"$release_json")
test "$matches" = 1
asset_id=$(jq -r --arg name "$asset_name" '.assets[] | select(.name == $name) | .id' <<<"$release_json")
asset_digest=$(jq -r --arg name "$asset_name" '.assets[] | select(.name == $name) | .digest' <<<"$release_json")
printf '%s\n' "$asset_id" | grep -Eq '^[1-9][0-9]*$'
printf '%s\n' "$asset_digest" | grep -Eq '^sha256:[0-9a-f]{64}$'
staged=/tmp/vaultsync-notify-staged-SBOM.spdx.json
gh api --method GET -H 'Accept: application/octet-stream' \
"repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" > "$staged"
test "sha256:$(sha256sum "$staged" | awk '{print $1}')" = "$asset_digest"

generated=notify/dist/SBOM.spdx.json
namespace="https://github.com/${GITHUB_REPOSITORY}/sbom/${RELEASE_TAG}/${RELEASE_SHA}"
for candidate in "$generated" "$staged"; do
jq -e --arg namespace "$namespace" '
.spdxVersion == "SPDX-2.3" and
.dataLicense == "CC0-1.0" and
.name == "notify/dist" and
.documentNamespace == $namespace and
.creationInfo.created == "1970-01-01T00:00:00Z" and
(.creationInfo.creators | index("Tool: trivy-0.70.0") != null)
' "$candidate" >/dev/null
done
canonical_filter='walk(if type == "object" and has("annotationDate")
then .annotationDate = "1970-01-01T00:00:00Z"
else . end)'
jq -S "$canonical_filter" "$generated" > /tmp/vaultsync-notify-generated-SBOM.canonical.json
jq -S "$canonical_filter" "$staged" > /tmp/vaultsync-notify-staged-SBOM.canonical.json
cmp /tmp/vaultsync-notify-generated-SBOM.canonical.json \
/tmp/vaultsync-notify-staged-SBOM.canonical.json
mv "$staged" "$generated"

- name: Verify exact binary provenance
env:
GH_TOKEN: ${{ github.token }}
Expand Down Expand Up @@ -1054,7 +1124,10 @@ jobs:
expected="sha256:$(sha256sum "$file" | awk '{print $1}')"
existing=$(jq -r --arg name "$name" '[.assets[] | select(.name == $name) | .digest][0] // empty' <<<"$release_json")
if [ -n "$existing" ]; then
test "$existing" = "$expected"
if [ "$existing" != "$expected" ]; then
echo "immutable release asset digest mismatch for $name: expected $expected, found $existing" >&2
exit 1
fi
else
test "$release_is_draft" = true
gh release upload "$RELEASE_TAG" "$file" --repo "$GITHUB_REPOSITORY"
Expand Down Expand Up @@ -1147,8 +1220,15 @@ jobs:
test "sha256:$(sha256sum "$target" | awk '{print $1}')" = "$asset_digest"
done
(cd /tmp/vaultsync-notify-release && sha256sum -c SHA256SUMS)
sbom_sha=$(sha256sum /tmp/vaultsync-notify-release/SBOM.spdx.json | awk '{print $1}')
image_digests_sha=$(sha256sum /tmp/vaultsync-notify-release/IMAGE-DIGESTS | awk '{print $1}')
jq -e --arg sha "$RELEASE_SHA" --arg digest "$IMAGE_DIGEST" \
'.source_commit == $sha and .image_index_digest == $digest' \
--arg sbom_sha "$sbom_sha" --arg image_digests_sha "$image_digests_sha" '
.source_commit == $sha and
.image_index_digest == $digest and
.binary_sbom == {"name": "SBOM.spdx.json", "sha256": $sbom_sha} and
.image_digests_sha256 == $image_digests_sha
' \
/tmp/vaultsync-notify-release/RELEASE-MANIFEST.json >/dev/null

- name: Prove published upgrade, rollback, and forward recovery
Expand Down
Loading