Skip to content

Commit 8365125

Browse files
authored
Merge pull request #606 from Chris0Jeky/ops/103-sbom-release-provenance
OPS-11: SBOM generation and release provenance workflow
2 parents 499ba2f + ccd4958 commit 8365125

File tree

6 files changed

+398
-14
lines changed

6 files changed

+398
-14
lines changed

.github/workflows/ci-release.yml

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# =============================================================================
2-
# CI Release — release lane for SBOM/provenance placeholder and container
2+
# CI Release — release lane for SBOM/provenance generation and container
33
# build verification. Complements release-security.yml with a lighter-weight
44
# build-verification pass suitable for tag/release automation.
55
#
6+
# SBOM/provenance: implemented in OPS-11 (#103) via reusable-sbom-provenance.yml
67
# Follow-through:
7-
# - OPS-11 (#103): SBOM/provenance generation and attestation policy
88
# - SEC-09 (#106) / OPS-17 (#148): dependency vulnerability policy + automation
99
# =============================================================================
1010

@@ -76,15 +76,16 @@ jobs:
7676
- Backend: restore + build (Release) passed
7777
- Frontend: npm ci + vite build passed
7878
79-
### SBOM / Provenance (placeholder)
80-
81-
SBOM generation and provenance attestation are not yet wired.
82-
Follow-through tracked in:
83-
- `#103` OPS-11 SBOM/provenance workflow
84-
- `#106` SEC-09 dependency vulnerability policy
85-
- `#148` OPS-17 dependency update automation
79+
SBOM and provenance artifacts are generated in the `sbom-provenance` job.
8680
EOF
8781
82+
sbom-provenance:
83+
name: SBOM and Release Provenance
84+
uses: ./.github/workflows/reusable-sbom-provenance.yml
85+
with:
86+
artifact-name: release-sbom-provenance
87+
retention-days: 90
88+
8889
container-images:
8990
name: Container Image Artifacts
9091
uses: ./.github/workflows/reusable-container-images.yml

.github/workflows/ci-required.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@
3737
#
3838
# ci-release.yml release lane (tag/release/manual)
3939
# ├── release build verification (backend + frontend)
40-
# ├── SBOM/provenance placeholder (follow-through: #103, #106, #148)
40+
# ├── reusable-sbom-provenance.yml (CycloneDX SBOM + SLSA provenance, #103)
4141
# └── reusable-container-images.yml
4242
#
4343
# release-security.yml release security deep scan (tag/release/manual)
4444
# ├── dependency inventory + vulnerability scan + enforcement
45+
# ├── reusable-sbom-provenance.yml (CycloneDX SBOM + SLSA provenance, #103)
4546
# └── reusable-container-images.yml
4647
#
4748
# pages-frontend.yml GitHub Pages deploy (main push, frontend paths)

.github/workflows/release-security.yml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# =============================================================================
2-
# Release Security — deep dependency inventory and vulnerability signal lane.
3-
# Runs on tag push, release publish, and manual dispatch.
2+
# Release Security — deep dependency inventory, vulnerability signal, and SBOM
3+
# generation lane. Runs on tag push, release publish, and manual dispatch.
44
# For lighter release build verification, see ci-release.yml.
55
# Topology context documented in ci-required.yml header.
6+
# SBOM/provenance: OPS-11 (#103) via reusable-sbom-provenance.yml
67
# =============================================================================
78

89
name: Release Security
@@ -126,8 +127,9 @@ jobs:
126127
- container image artifact generation and checksum output via reusable container workflow
127128
128129
Planned follow-through:
129-
- OPS-11 (`#103`): SBOM/provenance generation and attestation policy hardening
130130
- SEC-09 (`#106`) / OPS-17 (`#148`): dependency vulnerability policy + automation tightening
131+
132+
SBOM and provenance artifacts are generated in the `sbom-provenance` job.
131133
EOF
132134
133135
- name: Upload dependency and security artifacts
@@ -138,6 +140,13 @@ jobs:
138140
path: /tmp/release-security
139141
if-no-files-found: ignore
140142

143+
sbom-provenance:
144+
name: SBOM and Release Provenance
145+
uses: ./.github/workflows/reusable-sbom-provenance.yml
146+
with:
147+
artifact-name: release-security-sbom-provenance
148+
retention-days: 90
149+
141150
container-images:
142151
name: Container Image Artifacts
143152
uses: ./.github/workflows/reusable-container-images.yml
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
# =============================================================================
2+
# Reusable SBOM and Release Provenance
3+
#
4+
# Generates CycloneDX SBOMs for backend (.NET) and frontend (npm) dependencies,
5+
# plus a build provenance manifest capturing build environment metadata.
6+
#
7+
# Implements: OPS-11 (#103)
8+
# =============================================================================
9+
10+
name: Reusable SBOM and Provenance
11+
12+
on:
13+
workflow_call:
14+
inputs:
15+
dotnet-version:
16+
required: false
17+
type: string
18+
default: 8.0.x
19+
node-version:
20+
required: false
21+
type: string
22+
default: 24.13.1
23+
artifact-name:
24+
required: false
25+
type: string
26+
default: sbom-provenance-artifacts
27+
retention-days:
28+
required: false
29+
type: number
30+
default: 90
31+
fail-on-error:
32+
description: Fail the workflow if SBOM generation encounters errors
33+
required: false
34+
type: boolean
35+
default: false
36+
37+
permissions:
38+
contents: read
39+
40+
jobs:
41+
sbom-provenance:
42+
name: SBOM and Provenance
43+
runs-on: ubuntu-latest
44+
timeout-minutes: 20
45+
env:
46+
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
47+
SBOM_DIR: ${{ github.workspace }}/artifacts/sbom-provenance
48+
steps:
49+
- name: Checkout
50+
uses: actions/checkout@v6
51+
52+
- name: Setup .NET
53+
uses: actions/setup-dotnet@v5
54+
with:
55+
dotnet-version: ${{ inputs.dotnet-version }}
56+
cache: true
57+
cache-dependency-path: |
58+
backend/Taskdeck.sln
59+
backend/**/*.csproj
60+
61+
- name: Setup Node
62+
uses: actions/setup-node@v6
63+
with:
64+
node-version: ${{ inputs.node-version }}
65+
cache: npm
66+
cache-dependency-path: frontend/taskdeck-web/package-lock.json
67+
68+
- name: Create output directory
69+
run: mkdir -p "$SBOM_DIR"
70+
71+
- name: Restore backend dependencies
72+
run: dotnet restore backend/Taskdeck.sln
73+
74+
- name: Install CycloneDX .NET tool
75+
run: dotnet tool install --global CycloneDX
76+
77+
- name: Generate backend SBOM (CycloneDX)
78+
id: backend_sbom
79+
shell: bash
80+
env:
81+
SBOM_VERSION: ${{ github.ref_name }}
82+
run: |
83+
set +e
84+
dotnet CycloneDX backend/Taskdeck.sln \
85+
--output "$SBOM_DIR" \
86+
--filename backend-sbom.json \
87+
--json \
88+
--include-project-references \
89+
--set-type application \
90+
--set-name "Taskdeck-Backend" \
91+
--set-version "$SBOM_VERSION" 2> "$SBOM_DIR/backend-sbom.stderr"
92+
exit_code=$?
93+
printf '%s' "$exit_code" > "$SBOM_DIR/backend-sbom.exitcode"
94+
if [ "$exit_code" -ne 0 ]; then
95+
echo "::warning::Backend SBOM generation exited with code $exit_code"
96+
else
97+
echo "Backend SBOM generated successfully"
98+
fi
99+
exit "$exit_code"
100+
continue-on-error: true
101+
102+
- name: Install frontend dependencies
103+
working-directory: frontend/taskdeck-web
104+
run: npm ci
105+
106+
- name: Generate frontend SBOM (CycloneDX)
107+
id: frontend_sbom
108+
shell: bash
109+
run: |
110+
set +e
111+
npx --yes @cyclonedx/cyclonedx-npm \
112+
--output-file "$SBOM_DIR/frontend-sbom.json" \
113+
--spec-version 1.5 \
114+
--omit dev \
115+
--package-dir frontend/taskdeck-web 2> "$SBOM_DIR/frontend-sbom.stderr"
116+
exit_code=$?
117+
printf '%s' "$exit_code" > "$SBOM_DIR/frontend-sbom.exitcode"
118+
if [ "$exit_code" -ne 0 ]; then
119+
echo "::warning::Frontend SBOM generation exited with code $exit_code"
120+
else
121+
echo "Frontend SBOM generated successfully"
122+
fi
123+
exit "$exit_code"
124+
continue-on-error: true
125+
126+
- name: Generate build provenance manifest
127+
shell: bash
128+
env:
129+
PROVENANCE_REF_NAME: ${{ github.ref_name }}
130+
PROVENANCE_REPO: ${{ github.repository }}
131+
PROVENANCE_REF: ${{ github.ref }}
132+
PROVENANCE_SHA: ${{ github.sha }}
133+
PROVENANCE_WORKFLOW: ${{ github.workflow }}
134+
PROVENANCE_RUN_ID: ${{ github.run_id }}
135+
PROVENANCE_RUN_NUMBER: ${{ github.run_number }}
136+
PROVENANCE_RUN_ATTEMPT: ${{ github.run_attempt }}
137+
PROVENANCE_ACTOR: ${{ github.actor }}
138+
PROVENANCE_EVENT: ${{ github.event_name }}
139+
PROVENANCE_DOTNET: ${{ inputs.dotnet-version }}
140+
PROVENANCE_NODE: ${{ inputs.node-version }}
141+
run: |
142+
node -e "
143+
const provenance = {
144+
_type: 'https://slsa.dev/provenance/v1',
145+
subject: [{ name: 'Taskdeck', version: process.env.PROVENANCE_REF_NAME }],
146+
predicateType: 'https://slsa.dev/provenance/v1',
147+
predicate: {
148+
buildDefinition: {
149+
buildType: 'https://github.com/' + process.env.PROVENANCE_REPO + '/blob/main/.github/workflows/reusable-sbom-provenance.yml',
150+
resolvedDependencies: [{
151+
uri: 'git+https://github.com/' + process.env.PROVENANCE_REPO + '@' + process.env.PROVENANCE_REF,
152+
digest: { sha1: process.env.PROVENANCE_SHA }
153+
}]
154+
},
155+
runDetails: {
156+
builder: { id: 'https://github.com/' + process.env.PROVENANCE_REPO + '/actions/runs/' + process.env.PROVENANCE_RUN_ID },
157+
metadata: {
158+
invocationId: process.env.PROVENANCE_RUN_ID + '/' + process.env.PROVENANCE_RUN_ATTEMPT,
159+
startedOn: new Date().toISOString()
160+
},
161+
byproducts: [
162+
{ name: 'backend-sbom.json', mediaType: 'application/vnd.cyclonedx+json' },
163+
{ name: 'frontend-sbom.json', mediaType: 'application/vnd.cyclonedx+json' }
164+
]
165+
}
166+
},
167+
buildMetadata: {
168+
repository: process.env.PROVENANCE_REPO,
169+
ref: process.env.PROVENANCE_REF,
170+
sha: process.env.PROVENANCE_SHA,
171+
workflow: process.env.PROVENANCE_WORKFLOW,
172+
runId: process.env.PROVENANCE_RUN_ID,
173+
runNumber: process.env.PROVENANCE_RUN_NUMBER,
174+
runAttempt: process.env.PROVENANCE_RUN_ATTEMPT,
175+
actor: process.env.PROVENANCE_ACTOR,
176+
eventName: process.env.PROVENANCE_EVENT,
177+
runner: { os: 'ubuntu-latest', arch: 'x64' },
178+
tools: {
179+
dotnetVersion: process.env.PROVENANCE_DOTNET,
180+
nodeVersion: process.env.PROVENANCE_NODE,
181+
cyclonedxDotnet: 'latest',
182+
cyclonedxNpm: 'latest'
183+
}
184+
}
185+
};
186+
require('fs').writeFileSync(
187+
process.env.SBOM_DIR + '/build-provenance.json',
188+
JSON.stringify(provenance, null, 2) + '\n'
189+
);
190+
console.log('Build provenance manifest generated');
191+
"
192+
193+
- name: Generate artifact checksums
194+
shell: bash
195+
run: |
196+
cd "$SBOM_DIR"
197+
sha256sum ./*.json > checksums.sha256 2>/dev/null || true
198+
echo "Checksums:"
199+
cat checksums.sha256
200+
201+
- name: Write SBOM summary
202+
shell: bash
203+
env:
204+
SUMMARY_REF: ${{ github.ref }}
205+
SUMMARY_SHA: ${{ github.sha }}
206+
SUMMARY_RUN_ID: ${{ github.run_id }}
207+
SUMMARY_ARTIFACT_NAME: ${{ inputs.artifact-name }}
208+
SUMMARY_RETENTION: ${{ inputs.retention-days }}
209+
run: |
210+
backend_exit=$(cat "$SBOM_DIR/backend-sbom.exitcode" 2>/dev/null || echo "N/A")
211+
frontend_exit=$(cat "$SBOM_DIR/frontend-sbom.exitcode" 2>/dev/null || echo "N/A")
212+
213+
backend_status="passed"
214+
frontend_status="passed"
215+
if [ "$backend_exit" != "0" ]; then backend_status="failed (exit $backend_exit)"; fi
216+
if [ "$frontend_exit" != "0" ]; then frontend_status="failed (exit $frontend_exit)"; fi
217+
218+
cat <<EOF >> "$GITHUB_STEP_SUMMARY"
219+
## SBOM and Release Provenance
220+
221+
| Artifact | Format | Status |
222+
|----------|--------|--------|
223+
| Backend SBOM | CycloneDX JSON | $backend_status |
224+
| Frontend SBOM | CycloneDX JSON | $frontend_status |
225+
| Build Provenance | SLSA v1 JSON | generated |
226+
| Checksums | SHA-256 | generated |
227+
228+
**Ref:** \`$SUMMARY_REF\` | **SHA:** \`$SUMMARY_SHA\` | **Run:** \`$SUMMARY_RUN_ID\`
229+
230+
Artifacts are uploaded as \`$SUMMARY_ARTIFACT_NAME\` with $SUMMARY_RETENTION-day retention.
231+
232+
Policy: see \`docs/ops/SBOM_RELEASE_PROVENANCE.md\`
233+
EOF
234+
235+
- name: Enforce SBOM generation success
236+
if: inputs.fail-on-error
237+
shell: bash
238+
run: |
239+
backend_exit=$(cat "$SBOM_DIR/backend-sbom.exitcode" 2>/dev/null || echo "1")
240+
frontend_exit=$(cat "$SBOM_DIR/frontend-sbom.exitcode" 2>/dev/null || echo "1")
241+
if [ "$backend_exit" != "0" ] || [ "$frontend_exit" != "0" ]; then
242+
echo "::error::SBOM generation failed. Backend exit: $backend_exit, Frontend exit: $frontend_exit"
243+
echo "Review stderr artifacts for details."
244+
exit 1
245+
fi
246+
247+
- name: Upload SBOM and provenance artifacts
248+
if: always()
249+
uses: actions/upload-artifact@v7
250+
with:
251+
name: ${{ inputs.artifact-name }}
252+
path: ${{ env.SBOM_DIR }}
253+
if-no-files-found: warn
254+
retention-days: ${{ inputs.retention-days }}

0 commit comments

Comments
 (0)