Skip to content

Commit 9bf1839

Browse files
authored
feat: disjunctive license acknowledgement (#591)
--------- Signed-off-by: Jan Kowalleck <[email protected]>
1 parent ae3f79c commit 9bf1839

13 files changed

+237
-86
lines changed

cyclonedx/model/license.py

Lines changed: 117 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,30 @@
3333
from . import AttachedText, XsUri
3434

3535

36+
@serializable.serializable_enum
37+
class LicenseAcknowledgement(str, Enum):
38+
"""
39+
This is our internal representation of the `type_licenseAcknowledgementEnumerationType` ENUM type
40+
within the CycloneDX standard.
41+
42+
.. note::
43+
Introduced in CycloneDX v1.6
44+
45+
.. note::
46+
See the CycloneDX Schema for hashType:
47+
https://cyclonedx.org/docs/1.6/#type_licenseAcknowledgementEnumerationType
48+
"""
49+
50+
CONCLUDED = 'concluded'
51+
DECLARED = 'declared'
52+
53+
54+
# In an error, the name of the enum was `LicenseExpressionAcknowledgement`.
55+
# Even though this was changed, there might be some downstream usage of this symbol, so we keep it around ...
56+
LicenseExpressionAcknowledgement = LicenseAcknowledgement
57+
"""Deprecated alias for :class:`LicenseAcknowledgement`"""
58+
59+
3660
@serializable.serializable_class(name='license')
3761
class DisjunctiveLicense:
3862
"""
@@ -43,8 +67,12 @@ class DisjunctiveLicense:
4367
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/json/#components_items_licenses
4468
"""
4569

46-
def __init__(self, *, id: Optional[str] = None, name: Optional[str] = None,
47-
text: Optional[AttachedText] = None, url: Optional[XsUri] = None) -> None:
70+
def __init__(
71+
self, *,
72+
id: Optional[str] = None, name: Optional[str] = None,
73+
text: Optional[AttachedText] = None, url: Optional[XsUri] = None,
74+
acknowledgement: Optional[LicenseAcknowledgement] = None
75+
) -> None:
4876
if not id and not name:
4977
raise MutuallyExclusivePropertiesException('Either `id` or `name` MUST be supplied')
5078
if id and name:
@@ -56,6 +84,7 @@ def __init__(self, *, id: Optional[str] = None, name: Optional[str] = None,
5684
self._name = name if not id else None
5785
self._text = text
5886
self._url = url
87+
self._acknowledgement = acknowledgement
5988

6089
@property
6190
@serializable.xml_sequence(1)
@@ -129,14 +158,62 @@ def url(self, url: Optional[XsUri]) -> None:
129158
# @property
130159
# ...
131160
# @serializable.view(SchemaVersion1Dot5)
132-
# @serializable.xml_sequence(4)
161+
# @serializable.view(SchemaVersion1Dot6)
162+
# @serializable.xml_sequence(5)
133163
# def licensing(self) -> ...:
134164
# ... # TODO since CDX1.5
135165
#
136166
# @licensing.setter
137167
# def licensing(self, ...) -> None:
138168
# ... # TODO since CDX1.5
139169

170+
# @property
171+
# ...
172+
# @serializable.view(SchemaVersion1Dot5)
173+
# @serializable.view(SchemaVersion1Dot6)
174+
# @serializable.xml_sequence(6)
175+
# def properties(self) -> ...:
176+
# ... # TODO since CDX1.5
177+
#
178+
# @licensing.setter
179+
# def properties(self, ...) -> None:
180+
# ... # TODO since CDX1.5
181+
182+
# @property
183+
# @serializable.json_name('bom-ref')
184+
# @serializable.type_mapping(BomRefHelper)
185+
# @serializable.view(SchemaVersion1Dot5)
186+
# @serializable.view(SchemaVersion1Dot6)
187+
# @serializable.xml_attribute()
188+
# @serializable.xml_name('bom-ref')
189+
# def bom_ref(self) -> BomRef:
190+
# ... # TODO since CDX1.5
191+
192+
@property
193+
@serializable.view(SchemaVersion1Dot6)
194+
@serializable.xml_attribute()
195+
def acknowledgement(self) -> Optional[LicenseAcknowledgement]:
196+
"""
197+
Declared licenses and concluded licenses represent two different stages in the licensing process within
198+
software development.
199+
200+
Declared licenses refer to the initial intention of the software authors regarding the
201+
licensing terms under which their code is released. On the other hand, concluded licenses are the result of a
202+
comprehensive analysis of the project's codebase to identify and confirm the actual licenses of the components
203+
used, which may differ from the initially declared licenses. While declared licenses provide an upfront
204+
indication of the licensing intentions, concluded licenses offer a more thorough understanding of the actual
205+
licensing within a project, facilitating proper compliance and risk management. Observed licenses are defined
206+
in evidence.licenses. Observed licenses form the evidence necessary to substantiate a concluded license.
207+
208+
Returns:
209+
`LicenseAcknowledgement` or `None`
210+
"""
211+
return self._acknowledgement
212+
213+
@acknowledgement.setter
214+
def acknowledgement(self, acknowledgement: Optional[LicenseAcknowledgement]) -> None:
215+
self._acknowledgement = acknowledgement
216+
140217
def __eq__(self, other: object) -> bool:
141218
if isinstance(other, DisjunctiveLicense):
142219
return hash(other) == hash(self)
@@ -154,30 +231,12 @@ def __lt__(self, other: Any) -> bool:
154231
return NotImplemented
155232

156233
def __hash__(self) -> int:
157-
return hash((self._id, self._name, self._text, self._url))
234+
return hash((self._id, self._name, self._text, self._url, self._acknowledgement))
158235

159236
def __repr__(self) -> str:
160237
return f'<License id={self._id!r}, name={self._name!r}>'
161238

162239

163-
@serializable.serializable_enum
164-
class LicenseExpressionAcknowledgement(str, Enum):
165-
"""
166-
This is our internal representation of the `type_licenseAcknowledgementEnumerationType` ENUM type
167-
within the CycloneDX standard.
168-
169-
.. note::
170-
Introduced in CycloneDX v1.6
171-
172-
.. note::
173-
See the CycloneDX Schema for hashType:
174-
https://cyclonedx.org/docs/1.6/#type_licenseAcknowledgementEnumerationType
175-
"""
176-
177-
CONCLUDED = 'concluded'
178-
DECLARED = 'declared'
179-
180-
181240
@serializable.serializable_class(name='expression')
182241
class LicenseExpression:
183242
"""
@@ -189,15 +248,43 @@ class LicenseExpression:
189248
https://cyclonedx.org/docs/1.4/json/#components_items_licenses_items_expression
190249
"""
191250

192-
def __init__(self, value: str,
193-
acknowledgement: Optional[LicenseExpressionAcknowledgement] = None) -> None:
251+
def __init__(
252+
self, value: str,
253+
acknowledgement: Optional[LicenseAcknowledgement] = None
254+
) -> None:
255+
self._value = value
256+
self._acknowledgement = acknowledgement
257+
258+
@property
259+
@serializable.xml_name('.')
260+
@serializable.json_name('expression')
261+
def value(self) -> str:
262+
"""
263+
Value of this LicenseExpression.
264+
265+
Returns:
266+
`str`
267+
"""
268+
return self._value
269+
270+
@value.setter
271+
def value(self, value: str) -> None:
194272
self._value = value
195-
self.acknowledgement = acknowledgement
273+
274+
# @property
275+
# @serializable.json_name('bom-ref')
276+
# @serializable.type_mapping(BomRefHelper)
277+
# @serializable.view(SchemaVersion1Dot5)
278+
# @serializable.view(SchemaVersion1Dot6)
279+
# @serializable.xml_attribute()
280+
# @serializable.xml_name('bom-ref')
281+
# def bom_ref(self) -> BomRef:
282+
# ... # TODO since CDX1.5
196283

197284
@property
198285
@serializable.view(SchemaVersion1Dot6)
199286
@serializable.xml_attribute()
200-
def acknowledgement(self) -> Optional[LicenseExpressionAcknowledgement]:
287+
def acknowledgement(self) -> Optional[LicenseAcknowledgement]:
201288
"""
202289
Declared licenses and concluded licenses represent two different stages in the licensing process within
203290
software development.
@@ -211,36 +298,20 @@ def acknowledgement(self) -> Optional[LicenseExpressionAcknowledgement]:
211298
in evidence.licenses. Observed licenses form the evidence necessary to substantiate a concluded license.
212299
213300
Returns:
214-
`LicenseExpressionAcknowledgement` or `None`
301+
`LicenseAcknowledgement` or `None`
215302
"""
216303
return self._acknowledgement
217304

218305
@acknowledgement.setter
219-
def acknowledgement(self, acknowledgement: Optional[LicenseExpressionAcknowledgement]) -> None:
306+
def acknowledgement(self, acknowledgement: Optional[LicenseAcknowledgement]) -> None:
220307
self._acknowledgement = acknowledgement
221308

222-
@property
223-
@serializable.xml_name('.')
224-
@serializable.json_name('expression')
225-
def value(self) -> str:
226-
"""
227-
Value of this LicenseExpression.
228-
229-
Returns:
230-
`str`
231-
"""
232-
return self._value
233-
234-
@value.setter
235-
def value(self, value: str) -> None:
236-
self._value = value
237-
238309
def __hash__(self) -> int:
239-
return hash(self._value)
310+
return hash((self._value, self._acknowledgement))
240311

241312
def __eq__(self, other: object) -> bool:
242313
if isinstance(other, LicenseExpression):
243-
return self._value == other._value
314+
return hash(other) == hash(self)
244315
return False
245316

246317
def __lt__(self, other: Any) -> bool:

tests/_data/models.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
ImpactAnalysisState,
8787
)
8888
from cyclonedx.model.issue import IssueClassification, IssueType, IssueTypeSource
89-
from cyclonedx.model.license import DisjunctiveLicense, License, LicenseExpression, LicenseExpressionAcknowledgement
89+
from cyclonedx.model.license import DisjunctiveLicense, License, LicenseAcknowledgement, LicenseExpression
9090
from cyclonedx.model.release_note import ReleaseNotes
9191
from cyclonedx.model.service import Service
9292
from cyclonedx.model.vulnerability import (
@@ -948,20 +948,26 @@ def get_bom_with_licenses() -> Bom:
948948
components=[
949949
Component(name='c-with-expression', type=ComponentType.LIBRARY, bom_ref='C1',
950950
licenses=[LicenseExpression(value='Apache-2.0 OR MIT',
951-
acknowledgement=LicenseExpressionAcknowledgement.CONCLUDED)]),
951+
acknowledgement=LicenseAcknowledgement.CONCLUDED)]),
952952
Component(name='c-with-SPDX', type=ComponentType.LIBRARY, bom_ref='C2',
953-
licenses=[DisjunctiveLicense(id='Apache-2.0')]),
953+
licenses=[DisjunctiveLicense(id='Apache-2.0',
954+
url=XsUri('https://www.apache.org/licenses/LICENSE-2.0.html'),
955+
acknowledgement=LicenseAcknowledgement.CONCLUDED)]),
954956
Component(name='c-with-name', type=ComponentType.LIBRARY, bom_ref='C3',
955-
licenses=[DisjunctiveLicense(name='(c) ACME Inc.')]),
957+
licenses=[DisjunctiveLicense(name='some commercial license',
958+
text=AttachedText(content='this is a license text'))]),
956959
],
957960
services=[
958961
Service(name='s-with-expression', bom_ref='S1',
959962
licenses=[LicenseExpression(value='Apache-2.0 OR MIT',
960-
acknowledgement=LicenseExpressionAcknowledgement.DECLARED)]),
963+
acknowledgement=LicenseAcknowledgement.DECLARED)]),
961964
Service(name='s-with-SPDX', bom_ref='S2',
962-
licenses=[DisjunctiveLicense(id='Apache-2.0')]),
965+
licenses=[DisjunctiveLicense(id='Apache-2.0',
966+
url=XsUri('https://www.apache.org/licenses/LICENSE-2.0.html'),
967+
acknowledgement=LicenseAcknowledgement.DECLARED)]),
963968
Service(name='s-with-name', bom_ref='S3',
964-
licenses=[DisjunctiveLicense(name='(c) ACME Inc.')]),
969+
licenses=[DisjunctiveLicense(name='some commercial license',
970+
text=AttachedText(content='this is a license text'))]),
965971
])
966972

967973

tests/_data/snapshots/get_bom_with_licenses-1.1.xml.bin

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<licenses>
88
<license>
99
<id>Apache-2.0</id>
10+
<url>https://www.apache.org/licenses/LICENSE-2.0.html</url>
1011
</license>
1112
</licenses>
1213
</component>
@@ -22,7 +23,8 @@
2223
<version/>
2324
<licenses>
2425
<license>
25-
<name>(c) ACME Inc.</name>
26+
<name>some commercial license</name>
27+
<text content-type="text/plain">this is a license text</text>
2628
</license>
2729
</licenses>
2830
</component>

tests/_data/snapshots/get_bom_with_licenses-1.2.json.bin

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"licenses": [
66
{
77
"license": {
8-
"id": "Apache-2.0"
8+
"id": "Apache-2.0",
9+
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
910
}
1011
}
1112
],
@@ -29,7 +30,11 @@
2930
"licenses": [
3031
{
3132
"license": {
32-
"name": "(c) ACME Inc."
33+
"name": "some commercial license",
34+
"text": {
35+
"content": "this is a license text",
36+
"contentType": "text/plain"
37+
}
3338
}
3439
}
3540
],
@@ -91,7 +96,8 @@
9196
"licenses": [
9297
{
9398
"license": {
94-
"id": "Apache-2.0"
99+
"id": "Apache-2.0",
100+
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
95101
}
96102
}
97103
],
@@ -111,7 +117,11 @@
111117
"licenses": [
112118
{
113119
"license": {
114-
"name": "(c) ACME Inc."
120+
"name": "some commercial license",
121+
"text": {
122+
"content": "this is a license text",
123+
"contentType": "text/plain"
124+
}
115125
}
116126
}
117127
],

tests/_data/snapshots/get_bom_with_licenses-1.2.xml.bin

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<licenses>
2727
<license>
2828
<id>Apache-2.0</id>
29+
<url>https://www.apache.org/licenses/LICENSE-2.0.html</url>
2930
</license>
3031
</licenses>
3132
</component>
@@ -41,7 +42,8 @@
4142
<version/>
4243
<licenses>
4344
<license>
44-
<name>(c) ACME Inc.</name>
45+
<name>some commercial license</name>
46+
<text content-type="text/plain">this is a license text</text>
4547
</license>
4648
</licenses>
4749
</component>
@@ -52,6 +54,7 @@
5254
<licenses>
5355
<license>
5456
<id>Apache-2.0</id>
57+
<url>https://www.apache.org/licenses/LICENSE-2.0.html</url>
5558
</license>
5659
</licenses>
5760
</service>
@@ -65,7 +68,8 @@
6568
<name>s-with-name</name>
6669
<licenses>
6770
<license>
68-
<name>(c) ACME Inc.</name>
71+
<name>some commercial license</name>
72+
<text content-type="text/plain">this is a license text</text>
6973
</license>
7074
</licenses>
7175
</service>

0 commit comments

Comments
 (0)