Skip to content

Commit 5d8305d

Browse files
Address third-round review on workflows + SECURITY.md
Critical: - release-sign: verify cosign sign-blob outputs are non-empty before upload — cosign exits 0 on partial writes, which would otherwise ship a zero-byte .sig/.pem to a published release. Important: - release-sign: verify dispatched tag both exists on origin and has a published release before signing; refuse to sign arbitrary refs from workflow_dispatch. - release-sign: refuse to clobber existing signed assets on re-run. --clobber silently overwrote them; now fails loudly with a copy-paste cleanup hint so an operator decides whether to delete first. - release-sign: tighten semver regex so v1.2.3.foo is rejected (the prior `[.-]` alternation let it through). - trivy: fail loudly when SARIF generation produced no file. The prior hashFiles guard skipped upload silently, leaving the code-scanning dashboard stale on scanner crash. - trivy + shellcheck: add fork-PR fallback jobs that run the scanner without SARIF upload / PR-comment posting, so external contributions still get gated on secrets, vulns, misconfig, and shell warnings. - build-image: cosign verify the just-signed image against Rekor to catch transient Fulcio/Rekor write failures that cosign sign itself doesn't propagate. Cleanup: - build-image: rewrite "provenance/sbom no-ops bij push: false" comment so a future maintainer doesn't strip the unconditional flags; add id-token: write to the local-permission-escalation note. - scorecard: log the trigger and whether publish_results actually took effect, so a stale badge isn't silently caused by an unpublishable branch_protection_rule run. - SECURITY.md: document which release asset to cosign verify (the signed tar.gz, not GitHub's auto-generated Source code archive); switch the MinBZK CIO-office reference from backticks to quotes since it isn't a GitHub path. - CONTRIBUTING.md: soften the "moet groen / moeten zijn opgelost" language since branch protection isn't enforcing the gate yet. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 20631ec commit 5d8305d

7 files changed

Lines changed: 174 additions & 13 deletions

File tree

.github/workflows/build-image.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ jobs:
6666
type=raw,value=latest,enable={{is_default_branch}}
6767
6868
# PR builds zijn amd64-only om CI-tijd te besparen; arm64-regressies
69-
# komen pas op main aan het licht. provenance/sbom zijn no-ops bij
70-
# push: false (buildx hangt attestations alleen aan een registry-push).
69+
# komen pas op main aan het licht. provenance/sbom blijven aan staan;
70+
# buildx negeert ze stilzwijgend bij push: false en hangt ze alleen
71+
# aan de registry-push op main/tag — laat ze hier dus niet weg.
7172
- name: Build and push image
7273
id: build
7374
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
@@ -100,3 +101,18 @@ jobs:
100101
exit 1
101102
fi
102103
cosign sign --yes "${REGISTRY}/${IMAGE_NAME}@${DIGEST}"
104+
105+
# Verify the signature we just wrote is actually retrievable from
106+
# Rekor. Catches transient Fulcio/Rekor write failures that cosign
107+
# sign itself didn't propagate (rare, but ships an unsigned image).
108+
- name: Verify image signature
109+
if: github.event_name != 'pull_request'
110+
env:
111+
DIGEST: ${{ steps.build.outputs.digest }}
112+
REPO_FULL: ${{ github.repository }}
113+
run: |
114+
set -euo pipefail
115+
cosign verify \
116+
--certificate-identity-regexp "https://github.com/${REPO_FULL}/.github/workflows/build-image.yml@.*" \
117+
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
118+
"${REGISTRY}/${IMAGE_NAME}@${DIGEST}"

.github/workflows/release-sign.yml

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,34 @@ jobs:
3939
echo "Resolved tag is empty (event=$EVENT_NAME)" >&2
4040
exit 1
4141
fi
42-
# Restrict to safe semver-ish tags so downstream filenames and
42+
# Restrict to strict semver tags so downstream filenames and
4343
# --prefix paths cannot contain slashes, spaces, or shell metachars.
44-
if ! printf '%s' "$RESOLVED" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$'; then
44+
# Prerelease starts with `-`, build-metadata with `+`; reject the
45+
# `v1.2.3.foo` shape that an alternation `[.-]` would let through.
46+
if ! printf '%s' "$RESOLVED" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then
4547
echo "Tag '$RESOLVED' does not match expected semver pattern" >&2
4648
exit 1
4749
fi
4850
echo "name=$RESOLVED" >> "$GITHUB_OUTPUT"
4951
52+
- name: Verify tag refers to a published release
53+
env:
54+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55+
TAG: ${{ steps.tag.outputs.name }}
56+
REPO_FULL: ${{ github.repository }}
57+
run: |
58+
set -euo pipefail
59+
# workflow_dispatch can target arbitrary tags (incl. moved or typo'd
60+
# ones). Refuse to sign anything that isn't a published release.
61+
if ! git ls-remote --exit-code --tags "https://github.com/${REPO_FULL}.git" "refs/tags/${TAG}" >/dev/null; then
62+
echo "Tag '${TAG}' does not exist on origin" >&2
63+
exit 1
64+
fi
65+
if ! gh release view "${TAG}" --repo "${REPO_FULL}" >/dev/null 2>&1; then
66+
echo "Tag '${TAG}' has no corresponding published release" >&2
67+
exit 1
68+
fi
69+
5070
- name: Checkout tag
5171
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
5272
with:
@@ -86,6 +106,14 @@ jobs:
86106
--output-signature "${ARCHIVE}.sig" \
87107
--output-certificate "${ARCHIVE}.pem" \
88108
"${ARCHIVE}"
109+
# cosign exits 0 even if a partial write produced a zero-byte
110+
# signature or certificate; verify both before we ship them.
111+
for f in "${ARCHIVE}.sig" "${ARCHIVE}.pem"; do
112+
if [ ! -s "$f" ]; then
113+
echo "Empty signing output: $f" >&2
114+
exit 1
115+
fi
116+
done
89117
90118
- name: Upload assets to release
91119
env:
@@ -96,12 +124,22 @@ jobs:
96124
run: |
97125
set -euo pipefail
98126
ARCHIVE="${REPO}-${TAG}.tar.gz"
127+
# Refuse to overwrite signed assets silently. A re-run for the same
128+
# tag should fail loudly so an operator decides whether to delete
129+
# the existing assets first (and document why).
130+
EXISTING="$(gh release view "${TAG}" --repo "${REPO_FULL}" --json assets --jq '.assets[].name')"
131+
for f in "${ARCHIVE}" "${ARCHIVE}.sig" "${ARCHIVE}.pem" "${ARCHIVE}.sha256"; do
132+
if printf '%s\n' "${EXISTING}" | grep -Fxq "$f"; then
133+
echo "Refusing to clobber existing release asset: $f" >&2
134+
echo "Delete it manually with: gh release delete-asset ${TAG} $f --repo ${REPO_FULL}" >&2
135+
exit 1
136+
fi
137+
done
99138
gh release upload "${TAG}" \
100139
"${ARCHIVE}" \
101140
"${ARCHIVE}.sig" \
102141
"${ARCHIVE}.pem" \
103142
"${ARCHIVE}.sha256" \
104-
--clobber \
105143
--repo "${REPO_FULL}"
106144
107145
# Verify all four expected assets actually landed on the release.

.github/workflows/scorecard.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ jobs:
4040
# action skips publishing otherwise.
4141
publish_results: true
4242

43+
- name: "Log run mode"
44+
env:
45+
EVENT_NAME: ${{ github.event_name }}
46+
REF: ${{ github.ref }}
47+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
48+
run: |
49+
set -euo pipefail
50+
PUBLISHED="no (badge will use last published run)"
51+
if [ "$EVENT_NAME" = "push" ] || [ "$EVENT_NAME" = "schedule" ]; then
52+
if [ "$REF" = "refs/heads/${DEFAULT_BRANCH}" ] || [ "$EVENT_NAME" = "schedule" ]; then
53+
PUBLISHED="yes"
54+
fi
55+
fi
56+
echo "Trigger: $EVENT_NAME on $REF"
57+
echo "publish_results effective: $PUBLISHED"
58+
4359
- name: "Validate SARIF output is non-empty"
4460
run: |
4561
set -euo pipefail

.github/workflows/shellcheck.yml

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ jobs:
1414
name: differential-shellcheck
1515
runs-on: ubuntu-latest
1616
timeout-minutes: 10
17-
# Skip on PRs from forks: GitHub strips security-events / pull-requests
18-
# write permissions, which would make SARIF upload + PR-comment posting fail.
17+
# Same-repo PRs, push: full differential run with SARIF upload + PR comment.
1918
if: |
2019
(github.event_name != 'pull_request') ||
2120
(github.event.pull_request.head.repo.full_name == github.repository)
@@ -37,3 +36,31 @@ jobs:
3736
strict-check-on-push: true
3837
env:
3938
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39+
40+
shellcheck-fork-pr:
41+
name: shellcheck (fork PR fallback)
42+
runs-on: ubuntu-latest
43+
timeout-minutes: 10
44+
# Fork PRs cannot upload SARIF or post PR comments. Run a plain
45+
# shellcheck across all tracked shell scripts so external contributions
46+
# still get gated on shell warnings/errors.
47+
if: |
48+
github.event_name == 'pull_request' &&
49+
github.event.pull_request.head.repo.full_name != github.repository
50+
permissions:
51+
contents: read
52+
steps:
53+
- name: Checkout
54+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
55+
with:
56+
persist-credentials: false
57+
58+
- name: Run shellcheck on tracked shell scripts
59+
run: |
60+
set -euo pipefail
61+
mapfile -d '' files < <(git ls-files -z '*.sh' '*.bash')
62+
if [ ${#files[@]} -eq 0 ]; then
63+
echo "No shell scripts found; nothing to check."
64+
exit 0
65+
fi
66+
shellcheck --severity=warning "${files[@]}"

.github/workflows/trivy.yml

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ jobs:
1616
name: Trivy filesystem scan
1717
runs-on: ubuntu-latest
1818
timeout-minutes: 15
19-
# Skip on PRs from forks: GitHub strips security-events permissions
20-
# from forked-PR runs, which would make the SARIF upload fail.
19+
# Same-repo PRs, push, schedule: full SARIF run with code-scanning upload.
2120
if: |
2221
(github.event_name != 'pull_request') ||
2322
(github.event.pull_request.head.repo.full_name == github.repository)
@@ -53,9 +52,49 @@ jobs:
5352
ignore-unfixed: true
5453
scanners: vuln,secret,misconfig
5554

55+
# Fail loudly if the scanner crashed and produced no SARIF; otherwise
56+
# the upload step would silently no-op and the dashboard would show
57+
# stale results without anyone noticing.
58+
- name: Verify SARIF output exists
59+
if: always()
60+
run: |
61+
set -euo pipefail
62+
if [ ! -s trivy-results.sarif ]; then
63+
echo "trivy-results.sarif is missing or empty" >&2
64+
exit 1
65+
fi
66+
5667
- name: Upload to code-scanning
57-
if: always() && hashFiles('trivy-results.sarif') != ''
68+
if: always()
5869
uses: github/codeql-action/upload-sarif@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
5970
with:
6071
sarif_file: trivy-results.sarif
6172
category: trivy
73+
74+
scan-fork-pr:
75+
name: Trivy filesystem scan (fork PR fallback)
76+
runs-on: ubuntu-latest
77+
timeout-minutes: 15
78+
# Fork PRs cannot upload SARIF (GitHub strips security-events from
79+
# the read-only token). Run trivy with exit-code only so PR-time
80+
# secret/vuln/misconfig gating still applies to external contributors.
81+
if: |
82+
github.event_name == 'pull_request' &&
83+
github.event.pull_request.head.repo.full_name != github.repository
84+
permissions:
85+
contents: read
86+
steps:
87+
- name: Checkout
88+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
89+
with:
90+
persist-credentials: false
91+
92+
- name: Trivy fail-on-findings (CRITICAL/HIGH)
93+
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
94+
with:
95+
scan-type: fs
96+
format: table
97+
severity: CRITICAL,HIGH
98+
ignore-unfixed: true
99+
scanners: vuln,secret,misconfig
100+
exit-code: '1'

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ We volgen de bijdragerichtlijnen van het Ministerie van Binnenlandse Zaken en Ko
99
1. Open een issue of reageer op een bestaand issue voordat je begint.
1010
2. Fork de repository en maak een feature-branch.
1111
3. Open een pull request naar `main` met een duidelijke beschrijving.
12-
4. CI moet groen zijn. Alle review-comments moeten zijn opgelost voor merge.
12+
4. CI is verwacht groen en review-comments verwacht opgelost voor merge (afdwinging via branch protection wordt later toegevoegd).
1313

1414
## Gedragscode
1515

SECURITY.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ Voor verantwoorde melding van kwetsbaarheden volgen we het beleid van het Minist
77

88
Vermeld in je melding **beide** referenties zodat de melding via MinBZK CIO-office bij de juiste maintainers terechtkomt:
99

10-
1. `MinBZK/CIO-office github security response` — conform het MinBZK-beleid
11-
2. de repository `RijksICTGilde/hackathon-claude-code` — voor directe routering naar de maintainers
10+
1. "MinBZK/CIO-office github security response" — conform het MinBZK-beleid (vrije-tekstreferentie, geen GitHub-pad)
11+
2. de repository [`RijksICTGilde/hackathon-claude-code`](https://github.com/RijksICTGilde/hackathon-claude-code) — voor directe routering naar de maintainers
1212

1313
## Reactietermijn
1414

@@ -25,4 +25,29 @@ Conform het MinBZK-beleid (gebaseerd op NCSC) streven we naar:
2525
- Misbruik de kwetsbaarheid niet verder dan nodig om het bestaan ervan aan te tonen.
2626
- Wijzig of verwijder geen data op systemen.
2727

28+
## Verifiëren van release-artefacten
29+
30+
Bij elke gepubliceerde release tekent de `release-sign` workflow het bron-archief met cosign keyless. De release bevat vier assets:
31+
32+
- `<repo>-<tag>.tar.gz` — het ondertekende bron-archief
33+
- `<repo>-<tag>.tar.gz.sig` — handtekening
34+
- `<repo>-<tag>.tar.gz.pem` — Sigstore-certificaat
35+
- `<repo>-<tag>.tar.gz.sha256` — SHA256-checksum
36+
37+
**Belangrijk:** verifieer alleen het `<repo>-<tag>.tar.gz` asset uit de release. GitHub's automatisch gegenereerde "Source code (tar.gz)" download is een ander archief en heeft een andere checksum — die handtekening werkt daar niet op.
38+
39+
```bash
40+
TAG=v1.2.3
41+
REPO=hackathon-claude-code
42+
gh release download "$TAG" --repo RijksICTGilde/$REPO \
43+
--pattern "$REPO-$TAG.tar.gz*"
44+
sha256sum -c "$REPO-$TAG.tar.gz.sha256"
45+
cosign verify-blob \
46+
--certificate "$REPO-$TAG.tar.gz.pem" \
47+
--signature "$REPO-$TAG.tar.gz.sig" \
48+
--certificate-identity-regexp "https://github.com/RijksICTGilde/$REPO/.github/workflows/release-sign.yml@.*" \
49+
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
50+
"$REPO-$TAG.tar.gz"
51+
```
52+
2853
Zie het [volledige MinBZK-beleid](https://github.com/MinBZK/.github/blob/main/SECURITY.md) voor de complete tekst, do's en don'ts, en wat wij beloven.

0 commit comments

Comments
 (0)