Skip to content

Commit 14522dd

Browse files
committed
feat(ISV-5882): populate mobster version
Populate SBOM creation data fields with mobster and its current version. Signed-off-by: Martin Jediny <jedinym@proton.me>
1 parent b5d6367 commit 14522dd

11 files changed

Lines changed: 174 additions & 26 deletions

File tree

src/mobster/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
Mobster root module.
3+
"""
4+
5+
import importlib.metadata
6+
7+
8+
def get_mobster_version() -> str:
9+
"""
10+
Get the current mobster version as a string using import.metadata.version
11+
"""
12+
return importlib.metadata.version("mobster")

src/mobster/cmd/augment/handlers.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77

88
from packageurl import PackageURL
99

10+
from mobster import get_mobster_version
1011
from mobster.error import SBOMError
1112
from mobster.image import Image, IndexImage
1213
from mobster.oci.artifact import SBOMFormat
1314
from mobster.release import Component
15+
from mobster.sbom import cyclonedx
1416

1517
logger = logging.getLogger(__name__)
1618

@@ -172,6 +174,16 @@ def _find_image_package(cls, sbom: Any, image: Image) -> SPDXPackage | None:
172174

173175
return None
174176

177+
@classmethod
178+
def _augment_creation_info(cls, creation_info: Any) -> None:
179+
"""
180+
Add Mobster version information to creationInfo.
181+
"""
182+
version = get_mobster_version()
183+
creator = f"Tool: Mobster-{version}"
184+
if creator not in creation_info["creators"]:
185+
creation_info["creators"].append(creator)
186+
175187
@classmethod
176188
def _update_index_image_sbom(
177189
cls, component: Component, index: IndexImage, sbom: Any
@@ -238,6 +250,7 @@ def update_sbom(self, component: Component, image: Image, sbom: Any) -> None:
238250
"""
239251
Update a build-time SBOM with release-time data.
240252
"""
253+
self._augment_creation_info(sbom["creationInfo"])
241254
if isinstance(image, IndexImage):
242255
SPDXVersion2._update_index_image_sbom(component, image, sbom)
243256
elif isinstance(image, Image):
@@ -288,6 +301,7 @@ def _bump_version(self, sbom: Any) -> None:
288301
if sbom["specVersion"] not in ["1.4", "1.5", "1.6"]:
289302
raise SBOMError("Attempted to downgrade an SBOM.")
290303

304+
logger.debug("Bumping CycloneDX version to 1.6")
291305
sbom["$schema"] = "http://cyclonedx.org/schema/bom-1.6.schema.json"
292306
sbom["specVersion"] = "1.6"
293307

@@ -341,6 +355,17 @@ def _update_container_component(
341355
kflx_component, image, arch, cdx_component
342356
)
343357

358+
def _augment_metadata_tools_components(self, metadata: Any) -> None:
359+
"""
360+
Add Mobster version information to metadata.tools.components
361+
"""
362+
if "tools" not in metadata:
363+
metadata["tools"] = {"components": []}
364+
365+
components = metadata["tools"]["components"]
366+
if "Mobster" not in [c["name"] for c in components]:
367+
components.append(cyclonedx.get_tools_component_dict())
368+
344369
def _update_metadata_component(
345370
self, kflx_component: Component, image: Image, sbom: Any
346371
) -> None:
@@ -355,6 +380,8 @@ def _update_metadata_component(
355380
metadata = {"component": component}
356381
sbom["metadata"] = metadata
357382

383+
self._augment_metadata_tools_components(sbom["metadata"])
384+
358385

359386
def construct_purl(
360387
image: Image,

src/mobster/cmd/generate/modelcar.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ async def to_cyclonedx(self, modelcar: Image, base: Image, model: Image) -> Any:
8989

9090
# Create CycloneDX BOM and assign it the root component
9191
document = Bom()
92+
document.metadata.tools.components.add(cyclonedx.get_tools_component())
9293
document.metadata.component = root_component
9394

9495
# Add the base and model components to the BOM

src/mobster/sbom/cyclonedx.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
"""A module for CycloneDX SBOM format"""
22

3+
from typing import Any
4+
35
from cyclonedx.model import HashType
46
from cyclonedx.model.bom_ref import BomRef
57
from cyclonedx.model.component import (
68
Component,
79
ComponentType,
810
)
911

12+
from mobster import get_mobster_version
1013
from mobster.image import Image
1114

1215

@@ -32,3 +35,32 @@ def get_component(image: Image) -> Component:
3235
)
3336

3437
return package
38+
39+
40+
def get_tools_component() -> Component:
41+
"""
42+
Create a metadata.tools CycloneDX component. Inserts current version of
43+
mobster.
44+
45+
Returns:
46+
Component: A metadata.component object.
47+
"""
48+
return Component(
49+
name="Mobster", type=ComponentType.APPLICATION, version=get_mobster_version()
50+
)
51+
52+
53+
def get_tools_component_dict() -> dict[str, Any]:
54+
"""
55+
Create a metadata.tools CycloneDX component. Inserts current version of
56+
mobster.
57+
58+
Returns:
59+
Component: A metadata.component object.
60+
"""
61+
component = get_tools_component()
62+
return {
63+
"name": component.name,
64+
"type": component.type.value,
65+
"version": component.version,
66+
}

src/mobster/sbom/spdx.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
)
1414
from spdx_tools.spdx.model.spdx_no_assertion import SpdxNoAssertion
1515

16+
from mobster import get_mobster_version
1617
from mobster.image import Image
1718

1819

@@ -35,7 +36,7 @@ def get_creation_info(sbom_name: str) -> CreationInfo:
3536
creators=[
3637
Actor(ActorType.ORGANIZATION, "Red Hat"),
3738
Actor(ActorType.TOOL, "Konflux CI"),
38-
Actor(ActorType.TOOL, "Mobster"),
39+
Actor(ActorType.TOOL, f"Mobster-{get_mobster_version()}"),
3940
],
4041
created=datetime.now(timezone.utc),
4142
)

tests/cmd/generate/test_generate_oci_index.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from mobster.cmd.generate.oci_index import GenerateOciIndexCommand
1010
from mobster.image import Image
11+
from tests.conftest import assert_spdx_sbom
1112

1213

1314
@pytest.mark.asyncio
@@ -41,13 +42,7 @@ async def test_generate_oci_index_sbom() -> None:
4142
with open(args.output, encoding="utf8") as result_file:
4243
result = json.load(result_file)
4344

44-
# Copy dynamic values from expected output
45-
result["creationInfo"]["created"] = expected_output["creationInfo"][
46-
"created"
47-
]
48-
result["documentNamespace"] = expected_output["documentNamespace"]
49-
50-
assert result == expected_output
45+
assert_spdx_sbom(result, expected_output)
5146

5247

5348
def test_GenerateOciIndexCommand_get_index_image_relationship() -> None:

tests/cmd/generate/test_modelcar.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pytest
88

99
from mobster.cmd.generate.modelcar import GenerateModelcarCommand
10+
from tests.conftest import assert_cdx_sbom, assert_spdx_sbom
1011

1112

1213
@pytest.mark.asyncio
@@ -64,24 +65,16 @@ async def test_generate_modelcar_sbom(
6465
result = json.load(result_file)
6566

6667
if sbom_type == "spdx":
67-
# Copy dynamic values from expected output
68-
result["creationInfo"]["created"] = expected_output["creationInfo"][
69-
"created"
70-
]
71-
result["documentNamespace"] = expected_output["documentNamespace"]
68+
assert_spdx_sbom(result, expected_output)
7269
if sbom_type == "cyclonedx":
73-
result["serialNumber"] = expected_output["serialNumber"]
74-
result["metadata"]["timestamp"] = expected_output["metadata"][
75-
"timestamp"
76-
]
7770
root_bom_ref = result["metadata"]["component"]["bom-ref"]
7871
patch_bom_ref(
7972
result,
8073
root_bom_ref,
8174
expected_output["metadata"]["component"]["bom-ref"],
8275
)
8376

84-
assert result == expected_output
77+
assert_cdx_sbom(result, expected_output)
8578

8679

8780
def patch_bom_ref(document: Any, old: str, new: str) -> Any:

tests/cmd/test_augment.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
update_sbom,
1616
verify_sbom,
1717
)
18-
from mobster.cmd.augment.handlers import get_purl_digest
18+
from mobster.cmd.augment.handlers import CycloneDXVersion1, get_purl_digest
1919
from mobster.error import SBOMError, SBOMVerificationError
2020
from mobster.image import Image, IndexImage
2121
from mobster.oci.artifact import SBOM, Provenance02
2222
from mobster.oci.cosign import Cosign
2323
from mobster.release import Component, Snapshot
24+
from mobster.sbom import cyclonedx
25+
from tests.conftest import assert_spdx_sbom
2426

2527
TESTDATA_PATH = Path(__file__).parent.parent.joinpath("data/component")
2628

@@ -158,7 +160,7 @@ async def test_augment_execute_singlearch(
158160
expected = prepare_sbom(reference).doc
159161

160162
assert len(cmd.sboms) == 1
161-
assert cmd.sboms[0].doc == expected
163+
assert_spdx_sbom(cmd.sboms[0].doc, expected)
162164

163165
@pytest.mark.asyncio
164166
async def test_augment_execute_multiarch(
@@ -206,8 +208,9 @@ async def test_augment_execute_multiarch(
206208
]
207209

208210
assert len(expected_sboms) == len(cmd.sboms)
209-
for expected, actual in zip(cmd.sboms, expected_sboms, strict=False):
210-
assert expected.doc == actual.doc
211+
212+
for actual, expected in zip(cmd.sboms, expected_sboms, strict=False):
213+
assert_spdx_sbom(actual.doc, expected.doc)
211214

212215
@pytest.mark.asyncio
213216
async def test_augment_execute_cdx_singlearch(
@@ -404,6 +407,14 @@ def verify_component_updated(
404407
if verify_tags:
405408
VerifyCycloneDX.verify_tags(kflx_component, cdx_component)
406409

410+
@staticmethod
411+
def verify_mobster_version_info(sbom: Any) -> None:
412+
"""
413+
Verify that the mobster version info is added to the SBOM metadata.
414+
"""
415+
components = sbom["metadata"]["tools"]["components"]
416+
assert cyclonedx.get_tools_component_dict() in components
417+
407418
@staticmethod
408419
def verify_components_updated(snapshot: Snapshot, sbom: Any) -> None:
409420
"""
@@ -413,6 +424,7 @@ def verify_components_updated(snapshot: Snapshot, sbom: Any) -> None:
413424
VerifyCycloneDX.verify_component_updated(
414425
snapshot, sbom["metadata"]["component"], verify_tags=False
415426
)
427+
VerifyCycloneDX.verify_mobster_version_info(sbom)
416428

417429
for component in sbom.get("components", []):
418430
VerifyCycloneDX.verify_component_updated(
@@ -435,3 +447,12 @@ def test_get_purl_digest(purl_str: str, expected: str | BaseException) -> None:
435447
else:
436448
with pytest.raises(expected): # type: ignore
437449
get_purl_digest(purl_str)
450+
451+
452+
def test_cdx_augment_metadata_tools_components_empty_metadata() -> None:
453+
metadata: dict[str, Any] = {}
454+
CycloneDXVersion1()._augment_metadata_tools_components(metadata)
455+
456+
assert "tools" in metadata
457+
assert "components" in metadata["tools"]
458+
assert len(metadata["tools"]["components"]) == 1

tests/conftest.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from typing import Any
2+
3+
from mobster import get_mobster_version
4+
5+
6+
def assert_spdx_sbom(actual: Any, expected: Any) -> None:
7+
"""
8+
Compare and assert result SPDX SBOM with the expected SBOM, handling
9+
dynamic fields.
10+
11+
Args:
12+
actual (Any): actual generated SBOM dictionary
13+
expected (Any): expected SBOM dictionary
14+
"""
15+
actual["creationInfo"]["created"] = expected["creationInfo"]["created"]
16+
actual["documentNamespace"] = expected["documentNamespace"]
17+
18+
assert (
19+
f"Tool: Mobster-{get_mobster_version()}" in actual["creationInfo"]["creators"]
20+
)
21+
# Remove the Tool: Mobster entry from creators, as it's not in the expected result
22+
actual["creationInfo"]["creators"] = [
23+
creator
24+
for creator in actual["creationInfo"]["creators"]
25+
if "Mobster" not in creator
26+
]
27+
28+
assert actual == expected
29+
30+
31+
def assert_cdx_sbom(actual: Any, expected: Any) -> None:
32+
"""
33+
Compare and assert a result CDX SBOM with the expected SBOM, handling
34+
dynamic fields.
35+
36+
Args:
37+
actual (Any): actual generated SBOM dictionary
38+
expected (Any): expected SBOM dictionary
39+
"""
40+
actual["serialNumber"] = expected["serialNumber"]
41+
actual["metadata"]["timestamp"] = expected["metadata"]["timestamp"]
42+
43+
assert {
44+
"type": "application",
45+
"name": "Mobster",
46+
"version": get_mobster_version(),
47+
} in actual["metadata"]["tools"]["components"]
48+
del actual["metadata"]["tools"]
49+
50+
root_bom_ref = actual["metadata"]["component"]["bom-ref"]
51+
patch_bom_ref(
52+
actual,
53+
root_bom_ref,
54+
expected["metadata"]["component"]["bom-ref"],
55+
)
56+
57+
assert actual == expected
58+
59+
60+
def patch_bom_ref(document: Any, old: str, new: str) -> Any:
61+
document["metadata"]["component"]["bom-ref"] = new
62+
for component in document["components"]:
63+
if component["bom-ref"] == old:
64+
component["bom-ref"] = new
65+
for dependency in document["dependencies"]:
66+
if dependency["ref"] == old:
67+
dependency["ref"] = new
68+
return document

tests/data/index_manifest_sbom.spdx.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
"created": "2025-05-06T13:14:21Z",
55
"creators": [
66
"Organization: Red Hat",
7-
"Tool: Konflux CI",
8-
"Tool: Mobster"
7+
"Tool: Konflux CI"
98
]
109
},
1110
"dataLicense": "CC0-1.0",

0 commit comments

Comments
 (0)