From 7e3018c9d043ffddc25fd5efbb37cb244172169a Mon Sep 17 00:00:00 2001 From: Peter Schuster Date: Mon, 16 Mar 2026 17:10:45 +0100 Subject: [PATCH] feat: add properties for licenses Signed-off-by: Peter Schuster --- cyclonedx/model/license.py | 36 ++++++++++++------- tests/_data/models.py | 8 +++++ .../get_bom_with_licenses-1.0.xml.bin | 5 +++ .../get_bom_with_licenses-1.1.xml.bin | 12 +++++++ .../get_bom_with_licenses-1.2.json.bin | 21 +++++++++++ .../get_bom_with_licenses-1.2.xml.bin | 13 +++++++ .../get_bom_with_licenses-1.3.json.bin | 21 +++++++++++ .../get_bom_with_licenses-1.3.xml.bin | 13 +++++++ .../get_bom_with_licenses-1.4.json.bin | 20 +++++++++++ .../get_bom_with_licenses-1.4.xml.bin | 12 +++++++ .../get_bom_with_licenses-1.5.json.bin | 36 +++++++++++++++++++ .../get_bom_with_licenses-1.5.xml.bin | 19 ++++++++++ .../get_bom_with_licenses-1.6.json.bin | 36 +++++++++++++++++++ .../get_bom_with_licenses-1.6.xml.bin | 19 ++++++++++ .../get_bom_with_licenses-1.7.json.bin | 36 +++++++++++++++++++ .../get_bom_with_licenses-1.7.xml.bin | 19 ++++++++++ tests/test_model_license.py | 20 ++++++++++- 17 files changed, 333 insertions(+), 13 deletions(-) diff --git a/cyclonedx/model/license.py b/cyclonedx/model/license.py index 9d32160e2..b6e36f571 100644 --- a/cyclonedx/model/license.py +++ b/cyclonedx/model/license.py @@ -20,6 +20,7 @@ License related things """ +from collections.abc import Iterable from enum import Enum from json import loads as json_loads from typing import TYPE_CHECKING, Any, Optional, Union @@ -34,7 +35,7 @@ from ..exception.model import MutuallyExclusivePropertiesException from ..exception.serialization import CycloneDxDeserializationException from ..schema.schema import SchemaVersion1Dot5, SchemaVersion1Dot6, SchemaVersion1Dot7 -from . import AttachedText, XsUri +from . import AttachedText, Property, XsUri from .bom_ref import BomRef @@ -85,6 +86,7 @@ def __init__( id: Optional[str] = None, name: Optional[str] = None, text: Optional[AttachedText] = None, url: Optional[XsUri] = None, acknowledgement: Optional[LicenseAcknowledgement] = None, + properties: Optional[Iterable[Property]] = None, ) -> None: if not id and not name: raise MutuallyExclusivePropertiesException('Either `id` or `name` MUST be supplied') @@ -99,6 +101,7 @@ def __init__( self._text = text self._url = url self._acknowledgement = acknowledgement + self._properties = SortedSet(properties or []) @property @serializable.view(SchemaVersion1Dot5) @@ -200,17 +203,25 @@ def url(self, url: Optional[XsUri]) -> None: # def licensing(self, ...) -> None: # ... # TODO since CDX1.5 - # @property - # ... - # @serializable.view(SchemaVersion1Dot5) - # @serializable.view(SchemaVersion1Dot6) - # @serializable.xml_sequence(6) - # def properties(self) -> ...: - # ... # TODO since CDX1.5 - # - # @licensing.setter - # def properties(self, ...) -> None: - # ... # TODO since CDX1.5 + @property + @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) + @serializable.view(SchemaVersion1Dot7) + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property') + @serializable.xml_sequence(6) + def properties(self) -> 'SortedSet[Property]': + """ + Provides the ability to document properties in a key/value store. This provides flexibility to include data not + officially supported in the standard without having to use additional namespaces or create extensions. + + Return: + Set of `Property` + """ + return self._properties + + @properties.setter + def properties(self, properties: Iterable[Property]) -> None: + self._properties = SortedSet(properties) @property @serializable.view(SchemaVersion1Dot6) @@ -245,6 +256,7 @@ def __comparable_tuple(self) -> _ComparableTuple: self._url, self._text, self._bom_ref.value, + _ComparableTuple(self._properties), )) def __eq__(self, other: object) -> bool: diff --git a/tests/_data/models.py b/tests/_data/models.py index 43d62570e..55a5cdb9a 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -1082,6 +1082,14 @@ def get_bom_with_licenses() -> Bom: DisjunctiveLicense(name='some additional', text=AttachedText(content='this is additional license text')), ]), + Component(name='c-with-license-properties', type=ComponentType.LIBRARY, bom_ref='C4', + licenses=[ + DisjunctiveLicense(id='Apache-2.0', + properties=[Property(name='key1', value='val1'), + Property(name='key2', value='val2')]), + DisjunctiveLicense(name='some other license', + properties=[Property(name='myname', value='proprietary')]), + ]), ], services=[ Service(name='s-with-expression', bom_ref='S1', diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.0.xml.bin b/tests/_data/snapshots/get_bom_with_licenses-1.0.xml.bin index 1b308eafe..89f5c8166 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.0.xml.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.0.xml.bin @@ -11,6 +11,11 @@ false + + c-with-license-properties + + false + c-with-name diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.1.xml.bin b/tests/_data/snapshots/get_bom_with_licenses-1.1.xml.bin index e6f6adcaa..5519f41aa 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.1.xml.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.1.xml.bin @@ -18,6 +18,18 @@ Apache-2.0 OR MIT + + c-with-license-properties + + + + Apache-2.0 + + + some other license + + + c-with-name diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.2.json.bin b/tests/_data/snapshots/get_bom_with_licenses-1.2.json.bin index c88a0812a..e016afff5 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.2.json.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.2.json.bin @@ -25,6 +25,24 @@ "type": "library", "version": "" }, + { + "bom-ref": "C4", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "name": "some other license" + } + } + ], + "name": "c-with-license-properties", + "type": "library", + "version": "" + }, { "bom-ref": "C3", "licenses": [ @@ -62,6 +80,9 @@ { "ref": "C3" }, + { + "ref": "C4" + }, { "ref": "S1" }, diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.2.xml.bin b/tests/_data/snapshots/get_bom_with_licenses-1.2.xml.bin index 996e5716f..85a4054ed 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.2.xml.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.2.xml.bin @@ -30,6 +30,18 @@ Apache-2.0 OR MIT + + c-with-license-properties + + + + Apache-2.0 + + + some other license + + + c-with-name @@ -79,6 +91,7 @@ + diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.3.json.bin b/tests/_data/snapshots/get_bom_with_licenses-1.3.json.bin index a5407c588..46c9b296d 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.3.json.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.3.json.bin @@ -25,6 +25,24 @@ "type": "library", "version": "" }, + { + "bom-ref": "C4", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "name": "some other license" + } + } + ], + "name": "c-with-license-properties", + "type": "library", + "version": "" + }, { "bom-ref": "C3", "licenses": [ @@ -62,6 +80,9 @@ { "ref": "C3" }, + { + "ref": "C4" + }, { "ref": "S1" }, diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.3.xml.bin b/tests/_data/snapshots/get_bom_with_licenses-1.3.xml.bin index 1b53ee51e..5a5ab04d0 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.3.xml.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.3.xml.bin @@ -35,6 +35,18 @@ Apache-2.0 OR MIT + + c-with-license-properties + + + + Apache-2.0 + + + some other license + + + c-with-name @@ -84,6 +96,7 @@ + diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.4.json.bin b/tests/_data/snapshots/get_bom_with_licenses-1.4.json.bin index a082d8a3d..c084a6934 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.4.json.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.4.json.bin @@ -23,6 +23,23 @@ "name": "c-with-expression", "type": "library" }, + { + "bom-ref": "C4", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "name": "some other license" + } + } + ], + "name": "c-with-license-properties", + "type": "library" + }, { "bom-ref": "C3", "licenses": [ @@ -59,6 +76,9 @@ { "ref": "C3" }, + { + "ref": "C4" + }, { "ref": "S1" }, diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.4.xml.bin b/tests/_data/snapshots/get_bom_with_licenses-1.4.xml.bin index 6d81479e8..7a3131097 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.4.xml.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.4.xml.bin @@ -32,6 +32,17 @@ Apache-2.0 OR MIT + + c-with-license-properties + + + Apache-2.0 + + + some other license + + + c-with-name @@ -80,6 +91,7 @@ + diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.5.json.bin b/tests/_data/snapshots/get_bom_with_licenses-1.5.json.bin index a8b28b10a..b4d897131 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.5.json.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.5.json.bin @@ -23,6 +23,39 @@ "name": "c-with-expression", "type": "library" }, + { + "bom-ref": "C4", + "licenses": [ + { + "license": { + "id": "Apache-2.0", + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ] + } + }, + { + "license": { + "name": "some other license", + "properties": [ + { + "name": "myname", + "value": "proprietary" + } + ] + } + } + ], + "name": "c-with-license-properties", + "type": "library" + }, { "bom-ref": "C3", "licenses": [ @@ -59,6 +92,9 @@ { "ref": "C3" }, + { + "ref": "C4" + }, { "ref": "S1" }, diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.5.xml.bin b/tests/_data/snapshots/get_bom_with_licenses-1.5.xml.bin index fc2bedfd2..4cb534ccc 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.5.xml.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.5.xml.bin @@ -32,6 +32,24 @@ Apache-2.0 OR MIT + + c-with-license-properties + + + Apache-2.0 + + val1 + val2 + + + + some other license + + proprietary + + + + c-with-name @@ -80,6 +98,7 @@ + diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.6.json.bin b/tests/_data/snapshots/get_bom_with_licenses-1.6.json.bin index 4e6ef33ff..e626d7bbb 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.6.json.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.6.json.bin @@ -25,6 +25,39 @@ "name": "c-with-expression", "type": "library" }, + { + "bom-ref": "C4", + "licenses": [ + { + "license": { + "id": "Apache-2.0", + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ] + } + }, + { + "license": { + "name": "some other license", + "properties": [ + { + "name": "myname", + "value": "proprietary" + } + ] + } + } + ], + "name": "c-with-license-properties", + "type": "library" + }, { "bom-ref": "C3", "licenses": [ @@ -61,6 +94,9 @@ { "ref": "C3" }, + { + "ref": "C4" + }, { "ref": "S1" }, diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.6.xml.bin b/tests/_data/snapshots/get_bom_with_licenses-1.6.xml.bin index 49b31f469..527a1ce3a 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.6.xml.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.6.xml.bin @@ -32,6 +32,24 @@ Apache-2.0 OR MIT + + c-with-license-properties + + + Apache-2.0 + + val1 + val2 + + + + some other license + + proprietary + + + + c-with-name @@ -80,6 +98,7 @@ + diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.7.json.bin b/tests/_data/snapshots/get_bom_with_licenses-1.7.json.bin index f095a4692..4f5e710ab 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.7.json.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.7.json.bin @@ -25,6 +25,39 @@ "name": "c-with-expression", "type": "library" }, + { + "bom-ref": "C4", + "licenses": [ + { + "license": { + "id": "Apache-2.0", + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ] + } + }, + { + "license": { + "name": "some other license", + "properties": [ + { + "name": "myname", + "value": "proprietary" + } + ] + } + } + ], + "name": "c-with-license-properties", + "type": "library" + }, { "bom-ref": "C3", "licenses": [ @@ -61,6 +94,9 @@ { "ref": "C3" }, + { + "ref": "C4" + }, { "ref": "S1" }, diff --git a/tests/_data/snapshots/get_bom_with_licenses-1.7.xml.bin b/tests/_data/snapshots/get_bom_with_licenses-1.7.xml.bin index b9e91e6db..8b28e1972 100644 --- a/tests/_data/snapshots/get_bom_with_licenses-1.7.xml.bin +++ b/tests/_data/snapshots/get_bom_with_licenses-1.7.xml.bin @@ -32,6 +32,24 @@ Apache-2.0 OR MIT + + c-with-license-properties + + + Apache-2.0 + + val1 + val2 + + + + some other license + + proprietary + + + + c-with-name @@ -80,6 +98,7 @@ + diff --git a/tests/test_model_license.py b/tests/test_model_license.py index 11443e485..a21b8741e 100644 --- a/tests/test_model_license.py +++ b/tests/test_model_license.py @@ -21,7 +21,7 @@ from unittest.mock import MagicMock from cyclonedx.exception.model import MutuallyExclusivePropertiesException -from cyclonedx.model import AttachedText, XsUri +from cyclonedx.model import AttachedText, Property, XsUri from cyclonedx.model.license import DisjunctiveLicense, LicenseExpression from tests import reorder @@ -81,6 +81,24 @@ def test_equal(self) -> None: self.assertNotEqual(a, c) self.assertNotEqual(a, 'foo') + def test_create_with_properties(self) -> None: + properties = [Property(name='key1', value='value1')] + license = DisjunctiveLicense(id='MIT', properties=properties) + self.assertEqual(1, len(license.properties)) + + def test_set_properties(self) -> None: + license = DisjunctiveLicense(id='MIT') + self.assertEqual(0, len(license.properties)) + license.properties = [Property(name='key1', value='value1')] + self.assertEqual(1, len(license.properties)) + + def test_equal_with_properties(self) -> None: + a = DisjunctiveLicense(id='MIT', properties=[Property(name='key1', value='value1')]) + b = DisjunctiveLicense(id='MIT', properties=[Property(name='key1', value='value1')]) + c = DisjunctiveLicense(id='MIT') + self.assertEqual(a, b) + self.assertNotEqual(a, c) + class TestModelLicenseExpression(TestCase): def test_create(self) -> None: