Skip to content

Commit 84d17ef

Browse files
author
Test
committed
fix(security): bind release evidence to immutable image digests
1 parent 5650465 commit 84d17ef

12 files changed

Lines changed: 237 additions & 69 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hephaestus": patch
3+
---
4+
5+
Releases now reject high and critical image vulnerabilities unless a disposition is bound to the exact image digest and platform, owned, evidence-backed, and expires within 90 days. Clean verification and recurring rescans authenticate and re-evaluate the same signed release lock.

.github/workflows/ci-docker-build.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,18 @@ on:
3636
description: "Whether an application-server image exists at this commit"
3737
value: ${{ jobs.application-server-build.outputs.manifest-digest != '' || jobs.tag-unchanged-images.outputs.application-server-published == 'true' }}
3838

39+
permissions: {}
40+
3941
jobs:
4042
webapp-build:
4143
name: "Webapp"
4244
if: inputs.should_skip != 'true' && inputs.webapp_changed == 'true'
4345
uses: ./.github/workflows/reusable-docker-build.yml
46+
permissions:
47+
contents: read
48+
packages: write
49+
id-token: write
50+
attestations: write
4451
with:
4552
image-name: "ls1intum/hephaestus/webapp"
4653
docker-file: "./webapp/Dockerfile"
@@ -72,6 +79,11 @@ jobs:
7279
inputs.should_skip != 'true' &&
7380
(inputs.application_server_changed == 'true' || github.event_name != 'pull_request')
7481
uses: ./.github/workflows/reusable-docker-build.yml
82+
permissions:
83+
contents: read
84+
packages: write
85+
id-token: write
86+
attestations: write
7587
with:
7688
image-name: "ls1intum/hephaestus/application-server"
7789
# Paketo Cloud Native Buildpacks via spring-boot:build-image, with Application CDS.
@@ -105,6 +117,11 @@ jobs:
105117
name: "Agent: Pi"
106118
if: inputs.should_skip != 'true' && inputs.agent_images_changed == 'true'
107119
uses: ./.github/workflows/reusable-docker-build.yml
120+
permissions:
121+
contents: read
122+
packages: write
123+
id-token: write
124+
attestations: write
108125
with:
109126
image-name: "ls1intum/hephaestus/agent-pi"
110127
docker-file: "./docker/agents/pi/Dockerfile"
@@ -134,6 +151,11 @@ jobs:
134151
inputs.should_skip != 'true' &&
135152
(inputs.postgres_image_changed == 'true' || github.event_name != 'pull_request')
136153
uses: ./.github/workflows/reusable-docker-build.yml
154+
permissions:
155+
contents: read
156+
packages: write
157+
id-token: write
158+
attestations: write
137159
with:
138160
image-name: "ls1intum/hephaestus/postgres"
139161
docker-file: "./docker/postgres/Dockerfile"

.github/workflows/cicd.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ concurrency:
1111
group: ${{ github.workflow }}-${{ github.ref }}
1212
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
1313

14+
permissions: {}
15+
1416
jobs:
1517
detect-changes:
1618
name: "Detect changes"
@@ -174,6 +176,9 @@ jobs:
174176
github.event_name != 'pull_request'
175177
)
176178
secrets: inherit
179+
permissions:
180+
checks: write
181+
contents: read
177182
with:
178183
should_skip: ${{ needs.detect-changes.outputs.should_skip }}
179184
webapp_changed: ${{ (needs.detect-changes.outputs.webapp == 'true' || needs.detect-changes.outputs.ci-config == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }}
@@ -190,6 +195,9 @@ jobs:
190195
github.event_name != 'pull_request'
191196
)
192197
secrets: inherit
198+
permissions:
199+
contents: read
200+
security-events: write
193201
with:
194202
should_skip: ${{ needs.detect-changes.outputs.should_skip }}
195203

@@ -203,6 +211,11 @@ jobs:
203211
github.event_name != 'pull_request'
204212
)
205213
secrets: inherit
214+
permissions:
215+
contents: read
216+
checks: write
217+
pull-requests: write
218+
statuses: write
206219
with:
207220
should_skip: ${{ needs.detect-changes.outputs.should_skip }}
208221
webapp_changed: ${{ (needs.detect-changes.outputs.webapp == 'true' || needs.detect-changes.outputs.ci-config == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }}
@@ -212,6 +225,8 @@ jobs:
212225
Changesets:
213226
if: github.event_name == 'pull_request'
214227
uses: ./.github/workflows/verify-changesets.yml
228+
permissions:
229+
contents: read
215230

216231
Docker:
217232
uses: ./.github/workflows/ci-docker-build.yml
@@ -230,6 +245,11 @@ jobs:
230245
needs.detect-changes.outputs.ci-config == 'true'
231246
)
232247
secrets: inherit
248+
permissions:
249+
contents: read
250+
packages: write
251+
id-token: write
252+
attestations: write
233253
with:
234254
should_skip: ${{ needs.detect-changes.outputs.should_skip }}
235255
webapp_changed: ${{ (needs.detect-changes.outputs.webapp-image == 'true' || needs.detect-changes.outputs.docker-config == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }}

.github/workflows/release.yml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,15 @@ jobs:
274274
"evidence/$image-$suffix.syft.json" \
275275
"evidence/$image-$suffix.spdx.json" \
276276
"evidence/$image-$suffix.cdx.json" \
277-
"$platform_digest" "$platform" "evidence/$image-$suffix.sbom-summary.json"
277+
"$platform_digest" "$platform" "evidence/$image-$suffix.sbom-validation.json"
278278
trivy image --scanners vuln --format json --output "evidence/$image-$suffix.trivy.json" "$platform_ref"
279-
bun scripts/check-release-vulnerabilities.ts "$image" "evidence/$image-$suffix.trivy.json" security/vulnerability-policy.json "evidence/$image-$suffix.policy.json"
279+
trivy image --scanners license --format json --output "evidence/$image-$suffix.license.json" "$platform_ref"
280+
jq -e --arg ref "$platform_ref" \
281+
'.ArtifactName == $ref and (.Results | type == "array")' \
282+
"evidence/$image-$suffix.license.json" >/dev/null
283+
bun scripts/check-release-vulnerabilities.ts "$image" "$platform" "$platform_digest" "$repository" \
284+
"evidence/$image-$suffix.trivy.json" security/vulnerability-policy.json \
285+
"evidence/$image-$suffix.policy.json"
280286
if [ "$provenance" = first-party ]; then
281287
cosign attest --yes --type spdxjson --predicate "evidence/$image-$suffix.spdx.json" "$platform_ref"
282288
fi
@@ -463,11 +469,18 @@ jobs:
463469
"evidence/$image-$suffix.syft.json" \
464470
"evidence/$image-$suffix.spdx.json" \
465471
"evidence/$image-$suffix.cdx.json" \
466-
"$digest" "$platform" /tmp/sbom-summary.json
467-
cmp "evidence/$image-$suffix.sbom-summary.json" /tmp/sbom-summary.json
472+
"$digest" "$platform" /tmp/sbom-validation.json
473+
cmp "evidence/$image-$suffix.sbom-validation.json" /tmp/sbom-validation.json
468474
for kind in trivy policy; do
469475
test -s "evidence/$image-$suffix.$kind.json"
470476
done
477+
jq -e --arg ref "$repository@$digest" \
478+
'.ArtifactName == $ref and (.Results | type == "array")' \
479+
"evidence/$image-$suffix.license.json" >/dev/null
480+
bun scripts/check-release-vulnerabilities.ts "$image" "$platform" "$digest" "$repository" \
481+
"evidence/$image-$suffix.trivy.json" evidence/vulnerability-policy.json \
482+
/tmp/policy.json
483+
cmp "evidence/$image-$suffix.policy.json" /tmp/policy.json
471484
if [ "$provenance" = first-party ]; then
472485
cosign verify-attestation --type spdxjson \
473486
--certificate-identity 'https://github.com/${{ github.repository }}/.github/workflows/release.yml@refs/heads/main' \

.github/workflows/rescan-release-images.yml

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@ jobs:
2222
- uses: ./.github/actions/setup-release-security-tools
2323
with:
2424
install-syft: "false"
25+
- name: Install cosign
26+
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
2527
- name: Download and verify latest supported release evidence
2628
env:
2729
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2830
run: |
2931
set -euo pipefail
32+
tag=$(gh release view --repo "${{ github.repository }}" --json tagName --jq .tagName)
3033
mkdir release-evidence
31-
gh release download --repo "${{ github.repository }}" --dir release-evidence
34+
gh release download "$tag" --repo "${{ github.repository }}" --dir release-evidence
3235
(cd release-evidence && sha256sum -c SHA256SUMS)
3336
for file in manifest.json release-images.json tool-versions.json trivy-db.json vulnerability-policy.json; do
3437
test -s "release-evidence/$file"
@@ -46,16 +49,31 @@ jobs:
4649
"release-evidence/$image-$suffix.syft.json" \
4750
"release-evidence/$image-$suffix.spdx.json" \
4851
"release-evidence/$image-$suffix.cdx.json" \
49-
"$digest" "$platform" /tmp/sbom-summary.json
50-
cmp "release-evidence/$image-$suffix.sbom-summary.json" /tmp/sbom-summary.json
52+
"$digest" "$platform" /tmp/sbom-validation.json
53+
cmp "release-evidence/$image-$suffix.sbom-validation.json" /tmp/sbom-validation.json
5154
for kind in trivy policy; do
5255
test -s "release-evidence/$image-$suffix.$kind.json"
5356
done
57+
jq -e --arg ref "$repository@$digest" \
58+
'.ArtifactName == $ref and (.Results | type == "array")' \
59+
"release-evidence/$image-$suffix.license.json" >/dev/null
60+
bun scripts/check-release-vulnerabilities.ts "$image" "$platform" "$digest" "$repository" \
61+
"release-evidence/$image-$suffix.trivy.json" \
62+
release-evidence/vulnerability-policy.json /tmp/policy.json
63+
cmp "release-evidence/$image-$suffix.policy.json" /tmp/policy.json
5464
architecture=${platform#*/}
5565
docker buildx imagetools inspect "$repository@$index_digest" --raw |
5666
jq -e --arg architecture "$architecture" --arg digest "$digest" \
5767
'any(.manifests[]; .platform.os == "linux" and .platform.architecture == $architecture and .digest == $digest)' >/dev/null
5868
done
69+
lock="release-$tag.json"
70+
cosign verify-blob \
71+
--bundle "release-evidence/$lock.sigstore.json" \
72+
--certificate-identity 'https://github.com/ls1intum/Hephaestus/.github/workflows/release.yml@refs/heads/main' \
73+
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
74+
"release-evidence/$lock"
75+
bun scripts/release-image-lock.ts "release-evidence/$lock" \
76+
release-evidence/manifest.json "$tag" /tmp/release-lock.env
5977
- name: Rescan immutable subjects
6078
env:
6179
TRIVY_USERNAME: ${{ github.actor }}
@@ -73,7 +91,9 @@ jobs:
7391
while IFS=$'\t' read -r image platform digest repository; do
7492
suffix=${platform//\//-}
7593
trivy image --scanners vuln --format json --output "reports/$image-$suffix.json" "$repository@$digest"
76-
bun scripts/check-release-vulnerabilities.ts "$image" "reports/$image-$suffix.json" security/vulnerability-policy.json "reports/$image-$suffix.policy.json"
94+
bun scripts/check-release-vulnerabilities.ts "$image" "$platform" "$digest" "$repository" \
95+
"reports/$image-$suffix.json" security/vulnerability-policy.json \
96+
"reports/$image-$suffix.policy.json"
7797
done
7898
- name: Upload diagnostic reports
7999
if: always()

bun.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/contributor/release-management.mdx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ digests. It generates a lossless Syft inventory plus SPDX and CycloneDX SBOMs fo
6767
The gate proves that the Syft source metadata identifies the exact digest and architecture, checks that
6868
every discovered package survives both standard-format conversions, and records packages whose license
6969
could not be detected. It also scans each platform manifest with Trivy, signs its SPDX predicate as an OCI
70-
attestation, and verifies the image signature and build provenance. The SBOMs, license summaries, scan
70+
attestation, and verifies the image signature and build provenance. The SBOMs, license reports, scan
7171
reports, applied policy, checksums, and `manifest.json` are GitHub Release assets, so they outlive Actions
7272
artifact retention. Publication requires every artifact to be present, well formed, and bound to its
7373
recorded digest.
@@ -77,9 +77,10 @@ Compose topology. They receive the same per-platform SBOM, license, vulnerabilit
7777
evidence as first-party images. Hephaestus requires its own signatures and build provenance only for
7878
images it builds; it does not misrepresent observed upstream images as Hephaestus-built artifacts.
7979

80-
The version-controlled policy rejects new fixable HIGH or CRITICAL findings. Baselines use exact
81-
image/CVE/package/installed-version fingerprints. Exceptions use the same scope and require an owner,
82-
justification, and future expiry.
80+
The version-controlled policy rejects every HIGH or CRITICAL finding. A residual finding requires an
81+
exception scoped to its image, vulnerability, package, and installed version, with an owner, justification,
82+
evidence URL, and expiry no more than 90 days away. Each exception and policy report records the scanned
83+
platform and digest.
8384

8485
The latest release is rescanned weekly. A policy violation or scan failure is reported on
8586
[the vulnerability response issue](https://github.com/ls1intum/Hephaestus/issues/1369); scanner failure

0 commit comments

Comments
 (0)