Skip to content

Commit

Permalink
feat: disjunctive license acknowledgement (#591)
Browse files Browse the repository at this point in the history
---------

Signed-off-by: Jan Kowalleck <[email protected]>
  • Loading branch information
jkowalleck authored Apr 19, 2024
1 parent ae3f79c commit 9bf1839
Show file tree
Hide file tree
Showing 13 changed files with 237 additions and 86 deletions.
163 changes: 117 additions & 46 deletions cyclonedx/model/license.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,30 @@
from . import AttachedText, XsUri


@serializable.serializable_enum
class LicenseAcknowledgement(str, Enum):
"""
This is our internal representation of the `type_licenseAcknowledgementEnumerationType` ENUM type
within the CycloneDX standard.
.. note::
Introduced in CycloneDX v1.6
.. note::
See the CycloneDX Schema for hashType:
https://cyclonedx.org/docs/1.6/#type_licenseAcknowledgementEnumerationType
"""

CONCLUDED = 'concluded'
DECLARED = 'declared'


# In an error, the name of the enum was `LicenseExpressionAcknowledgement`.
# Even though this was changed, there might be some downstream usage of this symbol, so we keep it around ...
LicenseExpressionAcknowledgement = LicenseAcknowledgement
"""Deprecated alias for :class:`LicenseAcknowledgement`"""


@serializable.serializable_class(name='license')
class DisjunctiveLicense:
"""
Expand All @@ -43,8 +67,12 @@ class DisjunctiveLicense:
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/json/#components_items_licenses
"""

def __init__(self, *, id: Optional[str] = None, name: Optional[str] = None,
text: Optional[AttachedText] = None, url: Optional[XsUri] = None) -> None:
def __init__(
self, *,
id: Optional[str] = None, name: Optional[str] = None,
text: Optional[AttachedText] = None, url: Optional[XsUri] = None,
acknowledgement: Optional[LicenseAcknowledgement] = None
) -> None:
if not id and not name:
raise MutuallyExclusivePropertiesException('Either `id` or `name` MUST be supplied')
if id and name:
Expand All @@ -56,6 +84,7 @@ def __init__(self, *, id: Optional[str] = None, name: Optional[str] = None,
self._name = name if not id else None
self._text = text
self._url = url
self._acknowledgement = acknowledgement

@property
@serializable.xml_sequence(1)
Expand Down Expand Up @@ -129,14 +158,62 @@ def url(self, url: Optional[XsUri]) -> None:
# @property
# ...
# @serializable.view(SchemaVersion1Dot5)
# @serializable.xml_sequence(4)
# @serializable.view(SchemaVersion1Dot6)
# @serializable.xml_sequence(5)
# def licensing(self) -> ...:
# ... # TODO since CDX1.5
#
# @licensing.setter
# 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.json_name('bom-ref')
# @serializable.type_mapping(BomRefHelper)
# @serializable.view(SchemaVersion1Dot5)
# @serializable.view(SchemaVersion1Dot6)
# @serializable.xml_attribute()
# @serializable.xml_name('bom-ref')
# def bom_ref(self) -> BomRef:
# ... # TODO since CDX1.5

@property
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_attribute()
def acknowledgement(self) -> Optional[LicenseAcknowledgement]:
"""
Declared licenses and concluded licenses represent two different stages in the licensing process within
software development.
Declared licenses refer to the initial intention of the software authors regarding the
licensing terms under which their code is released. On the other hand, concluded licenses are the result of a
comprehensive analysis of the project's codebase to identify and confirm the actual licenses of the components
used, which may differ from the initially declared licenses. While declared licenses provide an upfront
indication of the licensing intentions, concluded licenses offer a more thorough understanding of the actual
licensing within a project, facilitating proper compliance and risk management. Observed licenses are defined
in evidence.licenses. Observed licenses form the evidence necessary to substantiate a concluded license.
Returns:
`LicenseAcknowledgement` or `None`
"""
return self._acknowledgement

@acknowledgement.setter
def acknowledgement(self, acknowledgement: Optional[LicenseAcknowledgement]) -> None:
self._acknowledgement = acknowledgement

def __eq__(self, other: object) -> bool:
if isinstance(other, DisjunctiveLicense):
return hash(other) == hash(self)
Expand All @@ -154,30 +231,12 @@ def __lt__(self, other: Any) -> bool:
return NotImplemented

def __hash__(self) -> int:
return hash((self._id, self._name, self._text, self._url))
return hash((self._id, self._name, self._text, self._url, self._acknowledgement))

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


@serializable.serializable_enum
class LicenseExpressionAcknowledgement(str, Enum):
"""
This is our internal representation of the `type_licenseAcknowledgementEnumerationType` ENUM type
within the CycloneDX standard.
.. note::
Introduced in CycloneDX v1.6
.. note::
See the CycloneDX Schema for hashType:
https://cyclonedx.org/docs/1.6/#type_licenseAcknowledgementEnumerationType
"""

CONCLUDED = 'concluded'
DECLARED = 'declared'


@serializable.serializable_class(name='expression')
class LicenseExpression:
"""
Expand All @@ -189,15 +248,43 @@ class LicenseExpression:
https://cyclonedx.org/docs/1.4/json/#components_items_licenses_items_expression
"""

def __init__(self, value: str,
acknowledgement: Optional[LicenseExpressionAcknowledgement] = None) -> None:
def __init__(
self, value: str,
acknowledgement: Optional[LicenseAcknowledgement] = None
) -> None:
self._value = value
self._acknowledgement = acknowledgement

@property
@serializable.xml_name('.')
@serializable.json_name('expression')
def value(self) -> str:
"""
Value of this LicenseExpression.
Returns:
`str`
"""
return self._value

@value.setter
def value(self, value: str) -> None:
self._value = value
self.acknowledgement = acknowledgement

# @property
# @serializable.json_name('bom-ref')
# @serializable.type_mapping(BomRefHelper)
# @serializable.view(SchemaVersion1Dot5)
# @serializable.view(SchemaVersion1Dot6)
# @serializable.xml_attribute()
# @serializable.xml_name('bom-ref')
# def bom_ref(self) -> BomRef:
# ... # TODO since CDX1.5

@property
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_attribute()
def acknowledgement(self) -> Optional[LicenseExpressionAcknowledgement]:
def acknowledgement(self) -> Optional[LicenseAcknowledgement]:
"""
Declared licenses and concluded licenses represent two different stages in the licensing process within
software development.
Expand All @@ -211,36 +298,20 @@ def acknowledgement(self) -> Optional[LicenseExpressionAcknowledgement]:
in evidence.licenses. Observed licenses form the evidence necessary to substantiate a concluded license.
Returns:
`LicenseExpressionAcknowledgement` or `None`
`LicenseAcknowledgement` or `None`
"""
return self._acknowledgement

@acknowledgement.setter
def acknowledgement(self, acknowledgement: Optional[LicenseExpressionAcknowledgement]) -> None:
def acknowledgement(self, acknowledgement: Optional[LicenseAcknowledgement]) -> None:
self._acknowledgement = acknowledgement

@property
@serializable.xml_name('.')
@serializable.json_name('expression')
def value(self) -> str:
"""
Value of this LicenseExpression.
Returns:
`str`
"""
return self._value

@value.setter
def value(self, value: str) -> None:
self._value = value

def __hash__(self) -> int:
return hash(self._value)
return hash((self._value, self._acknowledgement))

def __eq__(self, other: object) -> bool:
if isinstance(other, LicenseExpression):
return self._value == other._value
return hash(other) == hash(self)
return False

def __lt__(self, other: Any) -> bool:
Expand Down
20 changes: 13 additions & 7 deletions tests/_data/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
ImpactAnalysisState,
)
from cyclonedx.model.issue import IssueClassification, IssueType, IssueTypeSource
from cyclonedx.model.license import DisjunctiveLicense, License, LicenseExpression, LicenseExpressionAcknowledgement
from cyclonedx.model.license import DisjunctiveLicense, License, LicenseAcknowledgement, LicenseExpression
from cyclonedx.model.release_note import ReleaseNotes
from cyclonedx.model.service import Service
from cyclonedx.model.vulnerability import (
Expand Down Expand Up @@ -948,20 +948,26 @@ def get_bom_with_licenses() -> Bom:
components=[
Component(name='c-with-expression', type=ComponentType.LIBRARY, bom_ref='C1',
licenses=[LicenseExpression(value='Apache-2.0 OR MIT',
acknowledgement=LicenseExpressionAcknowledgement.CONCLUDED)]),
acknowledgement=LicenseAcknowledgement.CONCLUDED)]),
Component(name='c-with-SPDX', type=ComponentType.LIBRARY, bom_ref='C2',
licenses=[DisjunctiveLicense(id='Apache-2.0')]),
licenses=[DisjunctiveLicense(id='Apache-2.0',
url=XsUri('https://www.apache.org/licenses/LICENSE-2.0.html'),
acknowledgement=LicenseAcknowledgement.CONCLUDED)]),
Component(name='c-with-name', type=ComponentType.LIBRARY, bom_ref='C3',
licenses=[DisjunctiveLicense(name='(c) ACME Inc.')]),
licenses=[DisjunctiveLicense(name='some commercial license',
text=AttachedText(content='this is a license text'))]),
],
services=[
Service(name='s-with-expression', bom_ref='S1',
licenses=[LicenseExpression(value='Apache-2.0 OR MIT',
acknowledgement=LicenseExpressionAcknowledgement.DECLARED)]),
acknowledgement=LicenseAcknowledgement.DECLARED)]),
Service(name='s-with-SPDX', bom_ref='S2',
licenses=[DisjunctiveLicense(id='Apache-2.0')]),
licenses=[DisjunctiveLicense(id='Apache-2.0',
url=XsUri('https://www.apache.org/licenses/LICENSE-2.0.html'),
acknowledgement=LicenseAcknowledgement.DECLARED)]),
Service(name='s-with-name', bom_ref='S3',
licenses=[DisjunctiveLicense(name='(c) ACME Inc.')]),
licenses=[DisjunctiveLicense(name='some commercial license',
text=AttachedText(content='this is a license text'))]),
])


Expand Down
4 changes: 3 additions & 1 deletion tests/_data/snapshots/get_bom_with_licenses-1.1.xml.bin
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<licenses>
<license>
<id>Apache-2.0</id>
<url>https://www.apache.org/licenses/LICENSE-2.0.html</url>
</license>
</licenses>
</component>
Expand All @@ -22,7 +23,8 @@
<version/>
<licenses>
<license>
<name>(c) ACME Inc.</name>
<name>some commercial license</name>
<text content-type="text/plain">this is a license text</text>
</license>
</licenses>
</component>
Expand Down
18 changes: 14 additions & 4 deletions tests/_data/snapshots/get_bom_with_licenses-1.2.json.bin
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"licenses": [
{
"license": {
"id": "Apache-2.0"
"id": "Apache-2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
}
}
],
Expand All @@ -29,7 +30,11 @@
"licenses": [
{
"license": {
"name": "(c) ACME Inc."
"name": "some commercial license",
"text": {
"content": "this is a license text",
"contentType": "text/plain"
}
}
}
],
Expand Down Expand Up @@ -91,7 +96,8 @@
"licenses": [
{
"license": {
"id": "Apache-2.0"
"id": "Apache-2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
}
}
],
Expand All @@ -111,7 +117,11 @@
"licenses": [
{
"license": {
"name": "(c) ACME Inc."
"name": "some commercial license",
"text": {
"content": "this is a license text",
"contentType": "text/plain"
}
}
}
],
Expand Down
8 changes: 6 additions & 2 deletions tests/_data/snapshots/get_bom_with_licenses-1.2.xml.bin
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<licenses>
<license>
<id>Apache-2.0</id>
<url>https://www.apache.org/licenses/LICENSE-2.0.html</url>
</license>
</licenses>
</component>
Expand All @@ -41,7 +42,8 @@
<version/>
<licenses>
<license>
<name>(c) ACME Inc.</name>
<name>some commercial license</name>
<text content-type="text/plain">this is a license text</text>
</license>
</licenses>
</component>
Expand All @@ -52,6 +54,7 @@
<licenses>
<license>
<id>Apache-2.0</id>
<url>https://www.apache.org/licenses/LICENSE-2.0.html</url>
</license>
</licenses>
</service>
Expand All @@ -65,7 +68,8 @@
<name>s-with-name</name>
<licenses>
<license>
<name>(c) ACME Inc.</name>
<name>some commercial license</name>
<text content-type="text/plain">this is a license text</text>
</license>
</licenses>
</service>
Expand Down
Loading

0 comments on commit 9bf1839

Please sign in to comment.