Skip to content

Commit 27937af

Browse files
committed
ENH: Add version info, fix JSON escaping, add SBOM validation test
Address review findings from critical analysis: - Add SPDX_VERSION parameter to itk_module() macro for declaring vendored dependency versions. Add versions for 9 modules where the version is extractable from headers: Eigen3 (3.4.90), PNG (1.6.54), ZLIB (2.2.5), TIFF (4.7.0), HDF5 (1.14.5), JPEG (9f), OpenJPEG (2.5.4), MINC (2.4.06), NIFTI (3.0.0). - Fix JSON escaping bug in hasExtractedLicensingInfo: custom license names and texts were written raw without escaping quotes, backslashes, or newlines. - Fix Eigen3 SPDX license expression from "MPL-2.0" to "MPL-2.0 OR Apache-2.0" (Eigen is dual-licensed). - Update NIFTI download URL from nifti.nimh.nih.gov to the current GitHub repository. - Add ITKSBOMValidation.cmake CTest test that validates the generated sbom.spdx.json: checks JSON syntax, required SPDX 2.3 fields, package uniqueness, and extracted license entries.
1 parent 7efe75e commit 27937af

File tree

13 files changed

+277
-56
lines changed

13 files changed

+277
-56
lines changed

CMake/ITKModuleMacros.cmake

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ macro(itk_module _name)
7070
set(ITK_MODULE_${itk-module}_EXCLUDE_FROM_DEFAULT 0)
7171
set(ITK_MODULE_${itk-module}_ENABLE_SHARED 0)
7272
set(ITK_MODULE_${itk-module}_SPDX_LICENSE "")
73+
set(ITK_MODULE_${itk-module}_SPDX_VERSION "")
7374
set(ITK_MODULE_${itk-module}_SPDX_DOWNLOAD_LOCATION "")
7475
set(ITK_MODULE_${itk-module}_SPDX_COPYRIGHT "")
7576
set(ITK_MODULE_${itk-module}_SPDX_CUSTOM_LICENSE_TEXT "")
@@ -79,7 +80,7 @@ macro(itk_module _name)
7980
if(
8081
"${arg}"
8182
MATCHES
82-
"^((|COMPILE_|PRIVATE_|TEST_|)DEPENDS|DESCRIPTION|DEFAULT|FACTORY_NAMES|SPDX_LICENSE|SPDX_DOWNLOAD_LOCATION|SPDX_COPYRIGHT|SPDX_CUSTOM_LICENSE_TEXT|SPDX_CUSTOM_LICENSE_NAME)$"
83+
"^((|COMPILE_|PRIVATE_|TEST_|)DEPENDS|DESCRIPTION|DEFAULT|FACTORY_NAMES|SPDX_LICENSE|SPDX_VERSION|SPDX_DOWNLOAD_LOCATION|SPDX_COPYRIGHT|SPDX_CUSTOM_LICENSE_TEXT|SPDX_CUSTOM_LICENSE_NAME)$"
8384
)
8485
set(_doing "${arg}")
8586
elseif("${arg}" MATCHES "^EXCLUDE_FROM_DEFAULT$")
@@ -112,6 +113,9 @@ macro(itk_module _name)
112113
elseif("${_doing}" MATCHES "^SPDX_LICENSE$")
113114
set(_doing "")
114115
set(ITK_MODULE_${itk-module}_SPDX_LICENSE "${arg}")
116+
elseif("${_doing}" MATCHES "^SPDX_VERSION$")
117+
set(_doing "")
118+
set(ITK_MODULE_${itk-module}_SPDX_VERSION "${arg}")
115119
elseif("${_doing}" MATCHES "^SPDX_DOWNLOAD_LOCATION$")
116120
set(_doing "")
117121
set(ITK_MODULE_${itk-module}_SPDX_DOWNLOAD_LOCATION "${arg}")

CMake/ITKSBOMGeneration.cmake

Lines changed: 86 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
Per-module SPDX metadata is declared in each module's itk-module.cmake via
99
the itk_module() macro arguments:
1010
SPDX_LICENSE - SPDX license identifier (e.g. "Apache-2.0")
11+
SPDX_VERSION - Version of the vendored dependency
1112
SPDX_DOWNLOAD_LOCATION - URL for the upstream source
1213
SPDX_COPYRIGHT - Copyright text
1314
SPDX_CUSTOM_LICENSE_TEXT - Extracted text for custom LicenseRef-* IDs
@@ -38,16 +39,34 @@ endif()
3839
# COPYRIGHT "Copyright Example Inc."
3940
# )
4041
#
41-
define_property(GLOBAL PROPERTY ITK_SBOM_EXTRA_PACKAGES
42-
BRIEF_DOCS "Additional SBOM package entries registered by remote modules."
43-
FULL_DOCS "A list of JSON-formatted package entries for the SBOM."
42+
define_property(
43+
GLOBAL
44+
PROPERTY ITK_SBOM_EXTRA_PACKAGES
45+
BRIEF_DOCS
46+
"Additional SBOM package entries registered by remote modules."
47+
FULL_DOCS
48+
"A list of JSON-formatted package entries for the SBOM."
4449
)
4550

4651
function(itk_sbom_register_package)
4752
set(_options "")
48-
set(_one_value NAME VERSION SPDX_LICENSE DOWNLOAD_LOCATION SUPPLIER COPYRIGHT)
53+
set(
54+
_one_value
55+
NAME
56+
VERSION
57+
SPDX_LICENSE
58+
DOWNLOAD_LOCATION
59+
SUPPLIER
60+
COPYRIGHT
61+
)
4962
set(_multi_value "")
50-
cmake_parse_arguments(_pkg "${_options}" "${_one_value}" "${_multi_value}" ${ARGN})
63+
cmake_parse_arguments(
64+
_pkg
65+
"${_options}"
66+
"${_one_value}"
67+
"${_multi_value}"
68+
${ARGN}
69+
)
5170

5271
if(NOT _pkg_NAME)
5372
message(FATAL_ERROR "itk_sbom_register_package: NAME is required.")
@@ -76,18 +95,38 @@ function(itk_sbom_register_package)
7695
string(APPEND _entry " \"SPDXID\": \"SPDXRef-${_spdx_id}\",\n")
7796
string(APPEND _entry " \"name\": \"${_pkg_NAME}\",\n")
7897
string(APPEND _entry " \"versionInfo\": \"${_pkg_VERSION}\",\n")
79-
string(APPEND _entry " \"downloadLocation\": \"${_pkg_DOWNLOAD_LOCATION}\",\n")
98+
string(
99+
APPEND
100+
_entry
101+
" \"downloadLocation\": \"${_pkg_DOWNLOAD_LOCATION}\",\n"
102+
)
80103
string(APPEND _entry " \"supplier\": \"${_pkg_SUPPLIER}\",\n")
81-
string(APPEND _entry " \"licenseConcluded\": \"${_pkg_SPDX_LICENSE}\",\n")
104+
string(
105+
APPEND
106+
_entry
107+
" \"licenseConcluded\": \"${_pkg_SPDX_LICENSE}\",\n"
108+
)
82109
string(APPEND _entry " \"licenseDeclared\": \"${_pkg_SPDX_LICENSE}\",\n")
83110
string(APPEND _entry " \"copyrightText\": \"${_pkg_COPYRIGHT}\",\n")
84111
string(APPEND _entry " \"filesAnalyzed\": false\n")
85112
string(APPEND _entry " }")
86113

87-
set_property(GLOBAL APPEND PROPERTY ITK_SBOM_EXTRA_PACKAGES "${_entry}")
114+
set_property(
115+
GLOBAL
116+
APPEND
117+
PROPERTY
118+
ITK_SBOM_EXTRA_PACKAGES
119+
"${_entry}"
120+
)
88121

89122
# Also store the SPDX ID for relationship generation
90-
set_property(GLOBAL APPEND PROPERTY ITK_SBOM_EXTRA_SPDX_IDS "SPDXRef-${_spdx_id}")
123+
set_property(
124+
GLOBAL
125+
APPEND
126+
PROPERTY
127+
ITK_SBOM_EXTRA_SPDX_IDS
128+
"SPDXRef-${_spdx_id}"
129+
)
91130
endfunction()
92131

93132
#-----------------------------------------------------------------------------
@@ -114,8 +153,10 @@ function(itk_generate_sbom)
114153
string(TIMESTAMP _sbom_timestamp "%Y-%m-%dT%H:%M:%SZ" UTC)
115154
string(TIMESTAMP _sbom_uid "%Y%m%d%H%M%S" UTC)
116155

117-
set(_sbom_namespace
118-
"https://spdx.org/spdxdocs/ITK-${ITK_VERSION}-${_sbom_uid}")
156+
set(
157+
_sbom_namespace
158+
"https://spdx.org/spdxdocs/ITK-${ITK_VERSION}-${_sbom_uid}"
159+
)
119160

120161
# --- Begin JSON document ---
121162
set(_json "")
@@ -144,11 +185,19 @@ function(itk_generate_sbom)
144185
string(APPEND _json " \"SPDXID\": \"SPDXRef-ITK\",\n")
145186
string(APPEND _json " \"name\": \"ITK\",\n")
146187
string(APPEND _json " \"versionInfo\": \"${ITK_VERSION}\",\n")
147-
string(APPEND _json " \"downloadLocation\": \"https://github.com/InsightSoftwareConsortium/ITK\",\n")
188+
string(
189+
APPEND
190+
_json
191+
" \"downloadLocation\": \"https://github.com/InsightSoftwareConsortium/ITK\",\n"
192+
)
148193
string(APPEND _json " \"supplier\": \"Organization: NumFOCUS\",\n")
149194
string(APPEND _json " \"licenseConcluded\": \"Apache-2.0\",\n")
150195
string(APPEND _json " \"licenseDeclared\": \"Apache-2.0\",\n")
151-
string(APPEND _json " \"copyrightText\": \"Copyright 1999-2019 Insight Software Consortium, Copyright 2020-present NumFOCUS\",\n")
196+
string(
197+
APPEND
198+
_json
199+
" \"copyrightText\": \"Copyright 1999-2019 Insight Software Consortium, Copyright 2020-present NumFOCUS\",\n"
200+
)
152201
string(APPEND _json " \"filesAnalyzed\": false\n")
153202
string(APPEND _json " }")
154203

@@ -169,8 +218,12 @@ function(itk_generate_sbom)
169218
continue()
170219
endif()
171220

221+
set(_pkg_version "${ITK_MODULE_${_mod}_SPDX_VERSION}")
172222
set(_pkg_download "${ITK_MODULE_${_mod}_SPDX_DOWNLOAD_LOCATION}")
173223
set(_pkg_copyright "${ITK_MODULE_${_mod}_SPDX_COPYRIGHT}")
224+
if(NOT _pkg_version)
225+
set(_pkg_version "NOASSERTION")
226+
endif()
174227
if(NOT _pkg_download)
175228
set(_pkg_download "NOASSERTION")
176229
endif()
@@ -201,7 +254,7 @@ function(itk_generate_sbom)
201254
string(APPEND _json " {\n")
202255
string(APPEND _json " \"SPDXID\": \"SPDXRef-${_spdx_id}\",\n")
203256
string(APPEND _json " \"name\": \"${_mod}\",\n")
204-
string(APPEND _json " \"versionInfo\": \"NOASSERTION\",\n")
257+
string(APPEND _json " \"versionInfo\": \"${_pkg_version}\",\n")
205258
string(APPEND _json " \"downloadLocation\": \"${_pkg_download}\",\n")
206259
string(APPEND _json " \"supplier\": \"NOASSERTION\",\n")
207260
string(APPEND _json " \"licenseConcluded\": \"${_pkg_license}\",\n")
@@ -229,12 +282,24 @@ function(itk_generate_sbom)
229282
string(APPEND _json " \"SPDXID\": \"SPDXRef-FFTW\",\n")
230283
string(APPEND _json " \"name\": \"FFTW\",\n")
231284
string(APPEND _json " \"versionInfo\": \"${_fftw_version}\",\n")
232-
string(APPEND _json " \"downloadLocation\": \"https://www.fftw.org\",\n")
285+
string(
286+
APPEND
287+
_json
288+
" \"downloadLocation\": \"https://www.fftw.org\",\n"
289+
)
233290
string(APPEND _json " \"supplier\": \"Organization: MIT\",\n")
234291
string(APPEND _json " \"licenseConcluded\": \"${_fftw_license}\",\n")
235292
string(APPEND _json " \"licenseDeclared\": \"${_fftw_license}\",\n")
236-
string(APPEND _json " \"copyrightText\": \"Copyright Matteo Frigo and Massachusetts Institute of Technology\",\n")
237-
string(APPEND _json " \"description\": \"Fastest Fourier Transform in the West\",\n")
293+
string(
294+
APPEND
295+
_json
296+
" \"copyrightText\": \"Copyright Matteo Frigo and Massachusetts Institute of Technology\",\n"
297+
)
298+
string(
299+
APPEND
300+
_json
301+
" \"description\": \"Fastest Fourier Transform in the West\",\n"
302+
)
238303
string(APPEND _json " \"filesAnalyzed\": false\n")
239304
string(APPEND _json " }")
240305
list(APPEND _thirdparty_spdx_ids "SPDXRef-FFTW")
@@ -296,10 +361,12 @@ function(itk_generate_sbom)
296361
string(APPEND _json ",\n")
297362
endif()
298363
set(_first_custom FALSE)
364+
_itk_sbom_json_escape("${_lic_name}" _lic_name_escaped)
365+
_itk_sbom_json_escape("${_lic_text}" _lic_text_escaped)
299366
string(APPEND _json " {\n")
300367
string(APPEND _json " \"licenseId\": \"${_lic_id}\",\n")
301-
string(APPEND _json " \"name\": \"${_lic_name}\",\n")
302-
string(APPEND _json " \"extractedText\": \"${_lic_text}\"\n")
368+
string(APPEND _json " \"name\": \"${_lic_name_escaped}\",\n")
369+
string(APPEND _json " \"extractedText\": \"${_lic_text_escaped}\"\n")
303370
string(APPEND _json " }")
304371
endforeach()
305372
string(APPEND _json "\n ]")

CMake/ITKSBOMValidation.cmake

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#[=============================================================================[
2+
ITKSBOMValidation.cmake - Validate the generated SPDX SBOM document
3+
4+
Adds a CTest test that validates the SBOM JSON file produced by
5+
ITKSBOMGeneration.cmake. Checks:
6+
1. Valid JSON syntax
7+
2. Required SPDX 2.3 top-level fields present
8+
3. At least one package (ITK itself)
9+
4. DESCRIBES relationship present
10+
#]=============================================================================]
11+
12+
if(NOT ITK_GENERATE_SBOM OR NOT BUILD_TESTING)
13+
return()
14+
endif()
15+
16+
set(_sbom_file "${CMAKE_BINARY_DIR}/sbom.spdx.json")
17+
if(NOT EXISTS "${_sbom_file}")
18+
return()
19+
endif()
20+
21+
add_test(
22+
NAME ITKSBOMValidation
23+
COMMAND
24+
${Python3_EXECUTABLE} -c
25+
"
26+
import json, sys
27+
28+
with open(sys.argv[1]) as f:
29+
doc = json.load(f)
30+
31+
errors = []
32+
33+
# Required SPDX 2.3 fields
34+
for field in ['spdxVersion', 'dataLicense', 'SPDXID', 'name',
35+
'documentNamespace', 'creationInfo', 'packages',
36+
'relationships']:
37+
if field not in doc:
38+
errors.append(f'Missing required field: {field}')
39+
40+
if doc.get('spdxVersion') != 'SPDX-2.3':
41+
errors.append(f'Expected spdxVersion SPDX-2.3, got {doc.get(\"spdxVersion\")}')
42+
43+
if doc.get('dataLicense') != 'CC0-1.0':
44+
errors.append(f'Expected dataLicense CC0-1.0, got {doc.get(\"dataLicense\")}')
45+
46+
# Must have at least ITK package
47+
packages = doc.get('packages', [])
48+
if len(packages) < 1:
49+
errors.append('No packages found')
50+
51+
itk_pkg = next((p for p in packages if p.get('name') == 'ITK'), None)
52+
if itk_pkg is None:
53+
errors.append('ITK package not found')
54+
55+
# Check DESCRIBES relationship exists
56+
rels = doc.get('relationships', [])
57+
describes = [r for r in rels if r.get('relationshipType') == 'DESCRIBES']
58+
if not describes:
59+
errors.append('No DESCRIBES relationship found')
60+
61+
# Validate package SPDX IDs are unique
62+
spdx_ids = [p.get('SPDXID') for p in packages]
63+
dupes = set(x for x in spdx_ids if spdx_ids.count(x) > 1)
64+
if dupes:
65+
errors.append(f'Duplicate SPDX IDs: {dupes}')
66+
67+
# Check hasExtractedLicensingInfo references are valid
68+
extracted = doc.get('hasExtractedLicensingInfo', [])
69+
for entry in extracted:
70+
if 'licenseId' not in entry:
71+
errors.append(f'Extracted license missing licenseId')
72+
if 'extractedText' not in entry:
73+
errors.append(f'Extracted license missing extractedText')
74+
75+
if errors:
76+
for e in errors:
77+
print(f'SBOM ERROR: {e}', file=sys.stderr)
78+
sys.exit(1)
79+
80+
print(f'SBOM valid: {len(packages)} packages, {len(rels)} relationships, '
81+
f'{len(extracted)} extracted licenses')
82+
"
83+
"${_sbom_file}"
84+
)
85+
set_tests_properties(
86+
ITKSBOMValidation
87+
PROPERTIES
88+
LABELS
89+
"SBOM"
90+
)

CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,11 @@ mark_as_advanced(ITK_USE_SYSTEM_LIBRARIES)
249249

250250
#-----------------------------------------------------------------------------
251251
# Generate a Software Bill of Materials (SBOM) in SPDX 2.3 JSON format.
252-
option(ITK_GENERATE_SBOM "Generate SPDX Software Bill of Materials (SBOM) at configure time" ON)
252+
option(
253+
ITK_GENERATE_SBOM
254+
"Generate SPDX Software Bill of Materials (SBOM) at configure time"
255+
ON
256+
)
253257

254258
#-----------------------------------------------------------------------------
255259
# Enable the download and use of BrainWeb datasets.
@@ -753,6 +757,7 @@ include(ITKSBOMGeneration)
753757
if(ITK_GENERATE_SBOM)
754758
itk_generate_sbom()
755759
endif()
760+
include(ITKSBOMValidation)
756761

757762
# Setup clang-tidy for code best-practices enforcement for C++11
758763
include(ITKClangTidySetup)

Modules/ThirdParty/Eigen3/itk-module.cmake

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ itk_module(
99
DEPENDS
1010
DESCRIPTION "${DOCUMENTATION}"
1111
EXCLUDE_FROM_DEFAULT
12-
SPDX_LICENSE "MPL-2.0"
13-
SPDX_DOWNLOAD_LOCATION "https://eigen.tuxfamily.org"
14-
SPDX_COPYRIGHT "Copyright Eigen contributors"
12+
SPDX_LICENSE
13+
"MPL-2.0 OR Apache-2.0"
14+
SPDX_VERSION
15+
"3.4.90"
16+
SPDX_DOWNLOAD_LOCATION
17+
"https://gitlab.com/libeigen/eigen"
18+
SPDX_COPYRIGHT
19+
"Copyright Eigen contributors"
1520
)

Modules/ThirdParty/HDF5/itk-module.cmake

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ HDF5 is a data model, library, and file format for storing and managing data."
77

88
itk_module(
99
ITKHDF5
10-
DEPENDS ITKZLIB
10+
DEPENDS
11+
ITKZLIB
1112
DESCRIPTION "${DOCUMENTATION}"
12-
SPDX_LICENSE "BSD-3-Clause"
13-
SPDX_DOWNLOAD_LOCATION "https://www.hdfgroup.org/solutions/hdf5"
14-
SPDX_COPYRIGHT "Copyright The HDF Group"
13+
SPDX_LICENSE
14+
"BSD-3-Clause"
15+
SPDX_VERSION
16+
"1.14.5"
17+
SPDX_DOWNLOAD_LOCATION
18+
"https://www.hdfgroup.org/solutions/hdf5"
19+
SPDX_COPYRIGHT
20+
"Copyright The HDF Group"
1521
)

Modules/ThirdParty/JPEG/itk-module.cmake

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ library published by the
88
itk_module(
99
ITKJPEG
1010
DESCRIPTION "${DOCUMENTATION}"
11-
SPDX_LICENSE "IJG AND BSD-3-Clause AND Zlib"
12-
SPDX_DOWNLOAD_LOCATION "https://libjpeg-turbo.org"
13-
SPDX_COPYRIGHT "Copyright libjpeg-turbo contributors"
11+
SPDX_LICENSE
12+
"IJG AND BSD-3-Clause AND Zlib"
13+
SPDX_VERSION
14+
"9f"
15+
SPDX_DOWNLOAD_LOCATION
16+
"https://libjpeg-turbo.org"
17+
SPDX_COPYRIGHT
18+
"Copyright libjpeg-turbo contributors"
1419
)

0 commit comments

Comments
 (0)