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: