Skip to content

Commit 6d8f6e9

Browse files
mprpicclaude
authored andcommitted
test(sbom): add SPDX 2.3 schema validation with spdx-tools
Validate generated SBOMs against the SPDX 2.3 spec in all SBOM tests using the spdx-tools library. Closes: #1007 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Martin Prpič <mprpic@redhat.com>
1 parent 3e7c564 commit 6d8f6e9

2 files changed

Lines changed: 22 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ test = [
7272
"coverage[toml]!=4.4,>=4.0",
7373
"pytest",
7474
"requests-mock",
75+
"spdx-tools",
7576
"twine>=6.1.0",
7677
"hatchling",
7778
"hatch-vcs",
@@ -204,7 +205,7 @@ exclude = [
204205

205206
[[tool.mypy.overrides]]
206207
# packages without typing annotations and stubs
207-
module = ["hatchling", "hatchling.build", "license_expression", "pyproject_hooks", "requests_mock", "resolver", "stevedore"]
208+
module = ["hatchling", "hatchling.build", "license_expression", "pyproject_hooks", "requests_mock", "resolver", "spdx_tools.*", "stevedore"]
208209
ignore_missing_imports = true
209210

210211
[tool.basedpyright]

tests/test_sbom.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
import json
22
import pathlib
3+
import typing
34

45
from conftest import make_sbom_ctx
56
from packaging.requirements import Requirement
67
from packaging.version import Version
8+
from spdx_tools.spdx.parser.jsonlikedict.json_like_dict_parser import (
9+
JsonLikeDictParser,
10+
)
11+
from spdx_tools.spdx.validation.document_validator import validate_full_spdx_document
712

813
from fromager import sbom
914
from fromager.packagesettings import SbomSettings
1015

1116

17+
def _validate_spdx(doc: dict[str, typing.Any]) -> None:
18+
"""Validate an SBOM dict against the SPDX 2.3 spec using spdx-tools."""
19+
parsed = JsonLikeDictParser().parse(doc)
20+
errors = validate_full_spdx_document(parsed, spdx_version="SPDX-2.3")
21+
assert not errors, "\n".join(e.validation_message for e in errors)
22+
23+
1224
def test_generate_sbom_structure(tmp_path: pathlib.Path) -> None:
1325
"""Verify the generated SBOM has the required SPDX 2.3 fields."""
1426
ctx = make_sbom_ctx(tmp_path, sbom_settings=SbomSettings())
@@ -26,6 +38,7 @@ def test_generate_sbom_structure(tmp_path: pathlib.Path) -> None:
2638
assert "creationInfo" in doc
2739
assert doc["creationInfo"]["created"]
2840
assert any("fromager" in c for c in doc["creationInfo"]["creators"])
41+
_validate_spdx(doc)
2942

3043

3144
def test_generate_sbom_default_settings(tmp_path: pathlib.Path) -> None:
@@ -43,6 +56,7 @@ def test_generate_sbom_default_settings(tmp_path: pathlib.Path) -> None:
4356
assert doc["documentNamespace"] == (
4457
"https://spdx.org/spdxdocs/my-package-2.0.0.spdx.json"
4558
)
59+
_validate_spdx(doc)
4660

4761

4862
def test_generate_sbom_custom_settings(tmp_path: pathlib.Path) -> None:
@@ -67,6 +81,7 @@ def test_generate_sbom_custom_settings(tmp_path: pathlib.Path) -> None:
6781
creators = doc["creationInfo"]["creators"]
6882
assert "Organization: ExampleCo" in creators
6983
assert any("fromager" in c for c in creators)
84+
_validate_spdx(doc)
7085

7186

7287
def test_generate_sbom_purl_override(tmp_path: pathlib.Path) -> None:
@@ -86,6 +101,7 @@ def test_generate_sbom_purl_override(tmp_path: pathlib.Path) -> None:
86101
ext_refs = pkg["externalRefs"]
87102
assert len(ext_refs) == 1
88103
assert ext_refs[0]["referenceLocator"] == "pkg:generic/test-pkg@1.0.0"
104+
_validate_spdx(doc)
89105

90106

91107
def test_generate_sbom_default_purl(tmp_path: pathlib.Path) -> None:
@@ -99,6 +115,7 @@ def test_generate_sbom_default_purl(tmp_path: pathlib.Path) -> None:
99115

100116
pkg = doc["packages"][0]
101117
assert pkg["externalRefs"][0]["referenceLocator"] == "pkg:pypi/test@0.1.0"
118+
_validate_spdx(doc)
102119

103120

104121
def test_generate_sbom_canonicalizes_name(tmp_path: pathlib.Path) -> None:
@@ -114,6 +131,7 @@ def test_generate_sbom_canonicalizes_name(tmp_path: pathlib.Path) -> None:
114131
assert pkg["name"] == "my-package"
115132
assert doc["name"] == "my-package-1.0.0"
116133
assert pkg["externalRefs"][0]["referenceLocator"] == "pkg:pypi/my-package@1.0.0"
134+
_validate_spdx(doc)
117135

118136

119137
def test_generate_sbom_describes_relationship(tmp_path: pathlib.Path) -> None:
@@ -130,6 +148,7 @@ def test_generate_sbom_describes_relationship(tmp_path: pathlib.Path) -> None:
130148
assert rels[0]["spdxElementId"] == "SPDXRef-DOCUMENT"
131149
assert rels[0]["relationshipType"] == "DESCRIBES"
132150
assert rels[0]["relatedSpdxElement"] == "SPDXRef-wheel"
151+
_validate_spdx(doc)
133152

134153

135154
def test_write_sbom_creates_file(tmp_path: pathlib.Path) -> None:
@@ -150,6 +169,7 @@ def test_write_sbom_creates_file(tmp_path: pathlib.Path) -> None:
150169

151170
content = json.loads(result.read_text())
152171
assert content["spdxVersion"] == "SPDX-2.3"
172+
_validate_spdx(content)
153173

154174

155175
def test_write_sbom_preserves_existing_files(tmp_path: pathlib.Path) -> None:

0 commit comments

Comments
 (0)