|
| 1 | +name: NCS SBOM Pipeline |
| 2 | + |
| 3 | +on: |
| 4 | + push: |
| 5 | + branches: [main] |
| 6 | + pull_request: |
| 7 | + branches: [main] |
| 8 | + workflow_dispatch: |
| 9 | + inputs: |
| 10 | + grype_fail_gate: |
| 11 | + description: Enable Grype fail gate for this run (Stage 1b) |
| 12 | + type: boolean |
| 13 | + default: false |
| 14 | + |
| 15 | + # Stage 3 — enable weekly CVE sweep: |
| 16 | + # schedule: |
| 17 | + # - cron: '0 3 * * 1' |
| 18 | + |
| 19 | +concurrency: |
| 20 | + group: sbom-${{ github.ref }} |
| 21 | + cancel-in-progress: true |
| 22 | + |
| 23 | +permissions: |
| 24 | + contents: read |
| 25 | + actions: read |
| 26 | + security-events: write |
| 27 | + |
| 28 | +env: |
| 29 | + SYFT_VERSION: v1.49.0 |
| 30 | + GRYPE_VERSION: v0.116.0 |
| 31 | + FAIL_ON_SEVERITY: high |
| 32 | + # Stage 1a: report-only. Set to true after tuning .grype.yaml (Stage 1b). |
| 33 | + GRYPE_FAIL_GATE: false |
| 34 | + GRYPE_CONFIG: ncs/nrf/.grype.yaml |
| 35 | + |
| 36 | +jobs: |
| 37 | + sbom: |
| 38 | + runs-on: ubuntu-24.04 |
| 39 | + timeout-minutes: 180 |
| 40 | + steps: |
| 41 | + - name: Checkout west workspace |
| 42 | + uses: nrfconnect/action-checkout-west-update@b3257f7035e900ae50c280fe5759508f547f872d # 22 Jan 2026 |
| 43 | + with: |
| 44 | + git-fetch-depth: 0 |
| 45 | + path: ncs/nrf |
| 46 | + git-ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} |
| 47 | + rebase: ${{ github.event_name == 'pull_request' }} |
| 48 | + |
| 49 | + - name: Resolve NCS version |
| 50 | + id: ncs_version |
| 51 | + working-directory: ncs/nrf |
| 52 | + run: | |
| 53 | + major=$(grep '^VERSION_MAJOR' VERSION | awk -F' = ' '{print $2}') |
| 54 | + minor=$(grep '^VERSION_MINOR' VERSION | awk -F' = ' '{print $2}') |
| 55 | + patch=$(grep '^PATCHLEVEL' VERSION | awk -F' = ' '{print $2}') |
| 56 | + version="v${major}.${minor}.${patch}" |
| 57 | + short_sha="${GITHUB_SHA:0:8}" |
| 58 | + dt_version="${version}-${short_sha}" |
| 59 | + echo "version=${version}" >> "$GITHUB_OUTPUT" |
| 60 | + echo "dt_version=${dt_version}" >> "$GITHUB_OUTPUT" |
| 61 | + echo "NCS ${version} (DT version ${dt_version})" |
| 62 | +
|
| 63 | + - name: Extract west manifest graph |
| 64 | + working-directory: ncs |
| 65 | + run: | |
| 66 | + mkdir -p sbom sbom/projects |
| 67 | + west list -f "{name}|{revision}|{path}|{url}" > sbom/west-projects.csv |
| 68 | + west list -f "{name:30} {revision:12} {path}" > sbom/west-manifest-snapshot.txt |
| 69 | + west manifest --resolve > sbom/west-manifest-resolved.yml |
| 70 | + echo "----- Manifest snapshot -----" |
| 71 | + cat sbom/west-manifest-snapshot.txt |
| 72 | +
|
| 73 | + - name: Install Syft |
| 74 | + run: | |
| 75 | + curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \ |
| 76 | + | sh -s -- -b /usr/local/bin "${SYFT_VERSION}" |
| 77 | + syft version |
| 78 | +
|
| 79 | + - name: Install Grype |
| 80 | + run: | |
| 81 | + curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh \ |
| 82 | + | sh -s -- -b /usr/local/bin "${GRYPE_VERSION}" |
| 83 | + grype version |
| 84 | +
|
| 85 | + - name: Full workspace SBOM — SPDX + CycloneDX (Mode A) |
| 86 | + working-directory: ncs |
| 87 | + run: | |
| 88 | + syft "dir:${GITHUB_WORKSPACE}/ncs" \ |
| 89 | + -o spdx-json=sbom/ncs-full.spdx.json \ |
| 90 | + -o spdx-tag-value=sbom/ncs-full.spdx \ |
| 91 | + -o cyclonedx-json=sbom/ncs-full.cdx.json \ |
| 92 | + --exclude './**/build/**' \ |
| 93 | + --exclude './**/.git/**' \ |
| 94 | + --exclude './**/twister-out/**' |
| 95 | +
|
| 96 | + - name: Per-project SBOMs mapped to west revisions (Mode B) |
| 97 | + run: bash ncs/nrf/scripts/sbom/scan-projects.sh "${GITHUB_WORKSPACE}/ncs" |
| 98 | + |
| 99 | + - name: Annotate per-project SBOMs with west revisions |
| 100 | + working-directory: ncs |
| 101 | + run: bash nrf/scripts/sbom/annotate.sh |
| 102 | + |
| 103 | + - name: Generate license SBOM (west ncs-sbom) |
| 104 | + working-directory: ncs |
| 105 | + run: | |
| 106 | + pip3 install -U pip |
| 107 | + pip3 install -r nrf/scripts/requirements-west-ncs-sbom.txt |
| 108 | + west ncs-sbom \ |
| 109 | + --input-dir nrf/samples/zephyr/basic/blinky \ |
| 110 | + --output-spdx sbom/license-blinky.spdx \ |
| 111 | + --package-supplier "Nordic Semiconductor ASA" |
| 112 | +
|
| 113 | + - name: Vulnerability scan reports (Grype) |
| 114 | + working-directory: ncs |
| 115 | + run: | |
| 116 | + grype sbom:sbom/ncs-full.cdx.json -c "${GITHUB_WORKSPACE}/${GRYPE_CONFIG}" -o table > sbom/grype-report.txt || true |
| 117 | + grype sbom:sbom/ncs-full.cdx.json -c "${GITHUB_WORKSPACE}/${GRYPE_CONFIG}" -o json > sbom/grype-report.json || true |
| 118 | + grype sbom:sbom/ncs-full.cdx.json -c "${GITHUB_WORKSPACE}/${GRYPE_CONFIG}" -o sarif > sbom/grype-results.sarif || true |
| 119 | + cat sbom/grype-report.txt |
| 120 | +
|
| 121 | + - name: Grype CVE gate |
| 122 | + if: >- |
| 123 | + env.GRYPE_FAIL_GATE == 'true' || |
| 124 | + (github.event_name == 'workflow_dispatch' && github.event.inputs.grype_fail_gate == 'true') |
| 125 | + working-directory: ncs |
| 126 | + run: grype sbom:sbom/ncs-full.cdx.json -c "${GITHUB_WORKSPACE}/${GRYPE_CONFIG}" --fail-on "${FAIL_ON_SEVERITY}" |
| 127 | + |
| 128 | + - name: Upload SARIF to GitHub Security tab |
| 129 | + if: always() && hashFiles('ncs/sbom/grype-results.sarif') != '' |
| 130 | + continue-on-error: true |
| 131 | + uses: github/codeql-action/upload-sarif@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 |
| 132 | + with: |
| 133 | + sarif_file: ncs/sbom/grype-results.sarif |
| 134 | + category: grype-ncs-sbom |
| 135 | + |
| 136 | + - name: Package CycloneDX and SPDX zips |
| 137 | + id: package_zips |
| 138 | + if: always() |
| 139 | + working-directory: ncs/sbom |
| 140 | + run: | |
| 141 | + set -euo pipefail |
| 142 | + shopt -s nullglob |
| 143 | +
|
| 144 | + cdx_files=(ncs-full.cdx.json projects/*.cdx.json) |
| 145 | + spdx_files=(ncs-full.spdx ncs-full.spdx.json license-blinky.spdx projects/*.spdx.json) |
| 146 | +
|
| 147 | + if ((${#cdx_files[@]})); then |
| 148 | + zip -r "../ncs-sbom-cyclonedx.zip" "${cdx_files[@]}" |
| 149 | + echo "cyclonedx_zip=ncs/ncs-sbom-cyclonedx.zip" >> "$GITHUB_OUTPUT" |
| 150 | + echo "cyclonedx_size=$(du -h ../ncs-sbom-cyclonedx.zip | cut -f1)" >> "$GITHUB_OUTPUT" |
| 151 | + fi |
| 152 | +
|
| 153 | + if ((${#spdx_files[@]})); then |
| 154 | + zip -r "../ncs-sbom-spdx.zip" "${spdx_files[@]}" |
| 155 | + echo "spdx_zip=ncs/ncs-sbom-spdx.zip" >> "$GITHUB_OUTPUT" |
| 156 | + echo "spdx_size=$(du -h ../ncs-sbom-spdx.zip | cut -f1)" >> "$GITHUB_OUTPUT" |
| 157 | + fi |
| 158 | +
|
| 159 | + - name: Upload CycloneDX zip |
| 160 | + if: always() && steps.package_zips.outputs.cyclonedx_zip != '' |
| 161 | + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 |
| 162 | + with: |
| 163 | + name: ncs-sbom-cyclonedx-${{ steps.ncs_version.outputs.dt_version }} |
| 164 | + path: ncs/ncs-sbom-cyclonedx.zip |
| 165 | + retention-days: 90 |
| 166 | + |
| 167 | + - name: Upload SPDX zip |
| 168 | + if: always() && steps.package_zips.outputs.spdx_zip != '' |
| 169 | + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 |
| 170 | + with: |
| 171 | + name: ncs-sbom-spdx-${{ steps.ncs_version.outputs.dt_version }} |
| 172 | + path: ncs/ncs-sbom-spdx.zip |
| 173 | + retention-days: 90 |
| 174 | + |
| 175 | + - name: Upload SBOM artifacts |
| 176 | + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 |
| 177 | + if: always() |
| 178 | + with: |
| 179 | + name: ncs-sbom-${{ steps.ncs_version.outputs.dt_version }} |
| 180 | + path: ncs/sbom/ |
| 181 | + retention-days: 90 |
| 182 | + if-no-files-found: error |
| 183 | + |
| 184 | + - name: SBOM artifact summary |
| 185 | + if: always() |
| 186 | + working-directory: ncs/sbom |
| 187 | + run: | |
| 188 | + fmt_size() { du -h "$1" 2>/dev/null | cut -f1; } |
| 189 | +
|
| 190 | + cdx_size="$(fmt_size ncs-full.cdx.json)" |
| 191 | + spdx_size="$(fmt_size ncs-full.spdx)" |
| 192 | + spdx_json_size="$(fmt_size ncs-full.spdx.json)" |
| 193 | +
|
| 194 | + { |
| 195 | + echo "## SBOM download bundles" |
| 196 | + echo "" |
| 197 | + echo "| Bundle | Contents | Size | Artifact |" |
| 198 | + echo "|--------|----------|------|----------|" |
| 199 | + if [ -n "${{ steps.package_zips.outputs.cyclonedx_size }}" ]; then |
| 200 | + echo "| **CycloneDX** | \`ncs-full.cdx.json\` + \`projects/*.cdx.json\` | ${{ steps.package_zips.outputs.cyclonedx_size }} | **ncs-sbom-cyclonedx-${{ steps.ncs_version.outputs.dt_version }}** |" |
| 201 | + fi |
| 202 | + if [ -n "${{ steps.package_zips.outputs.spdx_size }}" ]; then |
| 203 | + echo "| **SPDX** | \`ncs-full.spdx\`, \`ncs-full.spdx.json\`, \`license-blinky.spdx\`, \`projects/*.spdx.json\` | ${{ steps.package_zips.outputs.spdx_size }} | **ncs-sbom-spdx-${{ steps.ncs_version.outputs.dt_version }}** |" |
| 204 | + fi |
| 205 | + echo "" |
| 206 | + echo "### Full workspace files" |
| 207 | + echo "" |
| 208 | + echo "| Format | File | Size |" |
| 209 | + echo "|--------|------|------|" |
| 210 | + [ -f ncs-full.cdx.json ] && \ |
| 211 | + echo "| CycloneDX (JSON) | \`ncs-full.cdx.json\` | ${cdx_size} |" |
| 212 | + [ -f ncs-full.spdx ] && \ |
| 213 | + echo "| SPDX (tag-value) | \`ncs-full.spdx\` | ${spdx_size} |" |
| 214 | + [ -f ncs-full.spdx.json ] && \ |
| 215 | + echo "| SPDX (JSON) | \`ncs-full.spdx.json\` | ${spdx_json_size} |" |
| 216 | + echo "" |
| 217 | + echo "> Download the **zip bundles** from the **Artifacts** section on this run page." |
| 218 | + echo "" |
| 219 | + echo "## All SBOM outputs" |
| 220 | + echo "" |
| 221 | + echo "<details>" |
| 222 | + echo "<summary>Complete file listing ($(find . -type f | wc -l) files)</summary>" |
| 223 | + echo "" |
| 224 | + echo "| File | Size |" |
| 225 | + echo "|------|------|" |
| 226 | + find . -type f | sort | while read -r f; do |
| 227 | + echo "| \`${f#./}\` | $(fmt_size "$f") |" |
| 228 | + done |
| 229 | + echo "" |
| 230 | + echo "</details>" |
| 231 | + echo "" |
| 232 | + echo "Full bundle artifact: \`ncs-sbom-${{ steps.ncs_version.outputs.dt_version }}\`" |
| 233 | + } >> "$GITHUB_STEP_SUMMARY" |
0 commit comments