Repository navigation
Conversation
Generate a CycloneDX Software Bill of Materials (SBOM) automatically in CI for every tagged release, attached as `grass.cyclonedx.json` alongside the tarballs in the release draft. A standalone `sbom.yml` workflow allows ad-hoc generation via `workflow_dispatch`. Local generation is available with `make sbom` (requires [Syft](https://github.com/anchore/syft)). Addresses OSGeo#7215
| md5sum "${GRASS}.tar.xz" > "${GRASS}.tar.xz.md5" | ||
| sha256sum "${GRASS}.tar.xz" > "${GRASS}.tar.xz.sha256" | ||
| - name: Generate CycloneDX SBOM (for tags only) | ||
| if: startsWith(github.ref, 'refs/tags/') |
There was a problem hiding this comment.
@neteler do you want to try triggering this with a release draft?
There was a problem hiding this comment.
I think it makes sense to check stuff already at RC level?
(I am no CI expert)
wenzeslaus
left a comment
There was a problem hiding this comment.
Here is my AI-boosted analysis:
Locally generated SBOM from a clean checkout of this branch with Syft
1.46.0 (same invocation as the workflow) contains 111 components: 71
GitHub Actions parsed from .github/workflows/, 11 Python packages from
man/mkdocs/requirements.txt and python/grass/docs/requirements.txt
(the mkdocs and sphinx documentation toolchains), and file entries.
binder/requirements.txt is not included because Syft skips unpinned
requirements by default.
None of GRASS's dependencies appear: the C libraries (GDAL, PROJ, GEOS,
...) and the Python runtime dependencies (numpy, ...) are not declared
in any format Syft reads. REQUIREMENTS.md is prose, and Syft ignores
[project.dependencies] in pyproject.toml even when pinned (tested);
it only catalogs pinned requirements.txt entries and lockfiles. The
vendored sources in lib/external/ (ccmath, parson, shapelib) are also
not detected, and those are exactly what a source SBOM should list for
CVE tracking. Syft cannot derive dependencies from Dockerfiles (it scans
built images) or from CMake/configure (dependencies are discovered on
the build host, not declared with versions).
The existing Docker image SBOMs (sbom: true in the Docker workflow)
are only partially better: they accurately list OS packages and
pip-installed Python packages, but not components compiled from source
inside the Dockerfiles. The alpine image installs the geospatial stack
via apk, so its SBOM covers it; the debian image builds only PDAL from
source; the ubuntu image (the basis of the latest tag) builds PROJ,
GEOS, GDAL, and PDAL from source, so its SBOM misses the entire core
stack. GRASS itself and the gdal-grass plugin are compiled from source
in all images, so no image SBOM lists GRASS or its version as a
component.
A separate issue is that the release SBOM mismatches the artifact it
accompanies: the tarball is created with --exclude=".gi*", so
.github is not in the release, yet 71 of the 111 components come from
it. Fixing this does not add the missing dependencies, it only removes
the CI tooling from the SBOM. Two options: scan the tarball itself
(file: input; Syft reads tar archives), which describes exactly the
released artifact but works only on tag runs where the tarball exists,
or keep the directory scan and exclude .github/.git via a committed
Syft config file, which works on every run (verified: 13 components
remain, matching the tarball contents).
As it stands, the published SBOM inventories the CI and documentation
toolchain rather than GRASS. Options: (a) a curated CycloneDX fragment
for lib/external/ and the C and Python dependencies, merged into the
generated file — inventory and licensing value only, since a source
release has no resolved versions, and a hand-maintained fragment can
drift from configure/CMake unless a check keeps it in sync; (b) reframe
the PR around the Docker images, where resolved versions exist and SBOMs
are already generated — the work then shifts to the source-built
components in the Dockerfiles (installing them from OS packages where
available, or declaring them separately), with the alpine image already
in good shape and the ubuntu image needing the most; (c) publish the
source SBOM as-is with the .github exclusion and cautious wording in
SECURITY.md. Option (c) is the easiest but does not address the critique
above: the published file would still present about a dozen
documentation-toolchain packages as the complete component inventory of
a GRASS release. SBOMs are consumed by tools and pipelines that never
see the SECURITY.md caveats, so a scanner reading the file would
conclude that a GRASS release contains no C libraries, no Python runtime
dependencies, and no vendored third-party code — a misleadingly clean
result for vulnerability or license scanning.
A detail: this repo squash-merges, so the PR title becomes the commit
message — per the commit message convention it should be capitalized
after the prefix: CI: Add CycloneDX SBOM generation for releases.
| workflow_dispatch: | ||
| inputs: | ||
| source-path: | ||
| description: Path to scan for SBOM generation | ||
| required: false | ||
| default: '.' | ||
| output-file: | ||
| description: Output SBOM file name | ||
| required: false | ||
| default: 'grass.cyclonedx.json' |
There was a problem hiding this comment.
With the release workflow generating the SBOM (and uploading it as a workflow artifact on check runs, if the suggestion there is applied), the remaining use of this workflow would be ad-hoc generation for an arbitrary ref — but workflow_dispatch runs the workflow file from the selected ref, so branches without this file cannot be dispatched anyway. Suggest dropping this file (and the corresponding SECURITY.md bullet) in favor of the release step and make sbom.
If it is kept, the inputs add little: the workflow scans a fresh checkout, so source-path would only produce a partial SBOM of a subdirectory, a case better served by running Syft locally. And output-file has no observable effect here — nothing in this workflow reads the written file; the only retrievable result is the uploaded artifact, whose file name comes from artifact-name, not output-file.
| workflow_dispatch: | |
| inputs: | |
| source-path: | |
| description: Path to scan for SBOM generation | |
| required: false | |
| default: '.' | |
| output-file: | |
| description: Output SBOM file name | |
| required: false | |
| default: 'grass.cyclonedx.json' | |
| workflow_dispatch: |
| -find . -type f \( -name "*.gcda" -o -name "*.gcno" -o -name "*.gcov" \) -delete | ||
| -rm -f .coverage | ||
|
|
||
| sbom: |
There was a problem hiding this comment.
On main, Autotools and CMake are kept equivalent, so a CMake counterpart is needed. Tested locally (CMake configure and cmake --build build --target sbom with Syft 1.46.0): the SBOM is written to the build directory, which keeps the source tree clean and needs no cleaning rules on the CMake side. At the end of the top-level CMakeLists.txt:
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/GRASSConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/GRASSConfigVersion.cmake
DESTINATION ${GRASS_INSTALL_CMAKECONFDIR}/GRASS)
+find_program(SYFT syft)
+if(SYFT)
+ add_custom_target(
+ sbom
+ COMMAND ${SYFT} dir:${CMAKE_SOURCE_DIR} -o
+ cyclonedx-json=${CMAKE_BINARY_DIR}/grass.cdx.json
+ COMMENT "Generating CycloneDX SBOM using Syft")
+ set_target_properties(sbom PROPERTIES FOLDER Misc)
+endif()
+
set_property(GLOBAL PROPERTY USE_FOLDERS YES)| - For Docker images, an SBOM is embedded at build time (`sbom: true` in | ||
| `docker/build-push-action`) and attestations are published via | ||
| `actions/attest-build-provenance`. |
There was a problem hiding this comment.
The Docker workflow uses actions/attest, not actions/attest-build-provenance (only a stale permissions comment mentions the latter). The workflow details are visible in the CI files and go stale when they change; readers here need the facts, not the mechanism:
| - For Docker images, an SBOM is embedded at build time (`sbom: true` in | |
| `docker/build-push-action`) and attestations are published via | |
| `actions/attest-build-provenance`. | |
| - For Docker images, an SBOM is embedded in the image at build time and | |
| build attestations are published. |
| - To generate an SBOM locally, install Syft and run `make sbom` from the | ||
| repository root. |
There was a problem hiding this comment.
If the CMake target from the Makefile comment is added:
| - To generate an SBOM locally, install Syft and run `make sbom` from the | |
| repository root. | |
| - To generate an SBOM locally, install Syft and run `make sbom` from the | |
| repository root, or build the `sbom` target with CMake | |
| (`cmake --build <build dir> --target sbom`). |
Co-authored-by: Vaclav Petras <wenzeslaus@gmail.com>
Background: A software bill of materials (SBOM) declares the inventory of components used to build a software artifact, including any open source and proprietary software components (source: Wikipedia)
Generate a CycloneDX Software Bill of Materials (SBOM) automatically in CI for every tagged release, attached as
grass.cyclonedx.jsonalongside the tarballs in the release draft.A standalone
sbom.ymlworkflow allows ad-hoc generation viaworkflow_dispatch. Local generation is available withmake sbom(requires Syft).The resulting JSON can be visualized in a SBOM analyser tool (e.g. sbom-analyzer)
Addresses #7215
Written with support by opencode (DeepSeek V4 Flash Free via OpenCode Zen)