From afe0ba90ec211a89fdb57adc719ba09a0a6f92a1 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 25 Sep 2023 09:38:04 +0200 Subject: [PATCH] fix #365 Signed-off-by: Jan Kowalleck --- cyclonedx/factory/license.py | 24 ++-- cyclonedx/model/__init__.py | 181 +--------------------------- cyclonedx/model/bom.py | 11 +- cyclonedx/model/component.py | 20 +-- cyclonedx/model/dependency.py | 4 +- cyclonedx/model/license.py | 181 ++++++++++++++++++++++++++++ cyclonedx/model/service.py | 10 +- cyclonedx/output/json.py | 4 +- cyclonedx/serialization/__init__.py | 33 ++++- tests/data.py | 17 +-- 10 files changed, 252 insertions(+), 233 deletions(-) create mode 100644 cyclonedx/model/license.py diff --git a/cyclonedx/factory/license.py b/cyclonedx/factory/license.py index 5d21c012..d869e8a0 100644 --- a/cyclonedx/factory/license.py +++ b/cyclonedx/factory/license.py @@ -17,8 +17,9 @@ from typing import Optional +from ..model.license import License, LicenseChoice, Expression from ..exception.factory import InvalidLicenseExpressionException, InvalidSpdxLicenseException -from ..model import AttachedText, License, LicenseChoice, XsUri +from ..model import AttachedText, XsUri from ..spdx import fixup_id as spdx_fixup, is_compound_expression as is_spdx_compound_expression @@ -51,40 +52,39 @@ def make_with_name(self, name: str, *, text: Optional[AttachedText] = None, url: class LicenseChoiceFactory: - """Factory for :class:`cyclonedx.model.LicenseChoice`.""" + """Factory for :type:`cyclonedx.model.LicenseChoice`.""" def __init__(self, *, license_factory: LicenseFactory) -> None: self.license_factory = license_factory def make_from_string(self, expression_or_name_or_spdx: str) -> LicenseChoice: - """Make a :class:`cyclonedx.model.LicenseChoice` from a string. + """Make a :type:`cyclonedx.model.LicenseChoice` from a string. Priority: SPDX license ID, SPDX license expression, named license """ try: - return LicenseChoice(license=self.license_factory.make_with_id(expression_or_name_or_spdx)) + return self.license_factory.make_with_id(expression_or_name_or_spdx) except InvalidSpdxLicenseException: pass try: return self.make_with_compound_expression(expression_or_name_or_spdx) except InvalidLicenseExpressionException: pass - return LicenseChoice(license=self.license_factory.make_with_name(expression_or_name_or_spdx)) + return self.license_factory.make_with_name(expression_or_name_or_spdx) - def make_with_compound_expression(self, compound_expression: str) -> LicenseChoice: - """Make a :class:`cyclonedx.model.LicenseChoice` with a compound expression. + def make_with_compound_expression(self, compound_expression: str) -> Expression: + """Make a :class:`cyclonedx.model.LicenseExpression` with a compound expression. Utilizes :func:`cyclonedx.spdx.is_compound_expression`. :raises InvalidLicenseExpressionException: if `expression` is not known/supported license expression """ if is_spdx_compound_expression(compound_expression): - return LicenseChoice(expression=compound_expression) + return Expression(compound_expression) raise InvalidLicenseExpressionException(compound_expression) def make_with_license(self, name_or_spdx: str, *, license_text: Optional[AttachedText] = None, - license_url: Optional[XsUri] = None) -> LicenseChoice: - """Make a :class:`cyclonedx.model.LicenseChoice` with a license (name or SPDX-ID).""" - return LicenseChoice(license=self.license_factory.make_from_string( - name_or_spdx, license_text=license_text, license_url=license_url)) + license_url: Optional[XsUri] = None) -> License: + """Make a :class:`cyclonedx.model.License`.""" + return self.license_factory.make_from_string(name_or_spdx, license_text=license_text, license_url=license_url) diff --git a/cyclonedx/model/__init__.py b/cyclonedx/model/__init__.py index 47f709a1..1f0c4a00 100644 --- a/cyclonedx/model/__init__.py +++ b/cyclonedx/model/__init__.py @@ -17,7 +17,6 @@ import hashlib import re -import warnings from datetime import datetime, timezone from enum import Enum from typing import Any, Iterable, Optional, Tuple, TypeVar @@ -29,7 +28,6 @@ from ..exception.model import ( InvalidLocaleTypeException, InvalidUriException, - MutuallyExclusivePropertiesException, NoPropertiesProvidedException, UnknownHashTypeException, ) @@ -449,14 +447,14 @@ def uri(self) -> str: return self._uri @classmethod - def serialize(cls, o: object) -> str: + def serialize(cls, o: object, t: 'SerializationType') -> str: if isinstance(o, XsUri): return str(o) raise ValueError(f'Attempt to serialize a non-XsUri: {o.__class__}') @classmethod - def deserialize(cls, o: object) -> 'XsUri': + def deserialize(cls, o: object, t: 'SerializationType') -> 'XsUri': try: return XsUri(uri=str(o)) except ValueError: @@ -584,181 +582,6 @@ def __repr__(self) -> str: return f'' -@serializable.serializable_class -class License: - """ - This is our internal representation of `licenseType` complex type that can be used in multiple places within - a CycloneDX BOM document. - - .. note:: - See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_licenseType - """ - - def __init__(self, *, id: Optional[str] = None, name: Optional[str] = None, - text: Optional[AttachedText] = None, url: Optional[XsUri] = None) -> None: - if not id and not name: - raise MutuallyExclusivePropertiesException('Either `id` or `name` MUST be supplied') - if id and name: - warnings.warn( - 'Both `id` and `name` have been supplied - `name` will be ignored!', - RuntimeWarning - ) - self.id = id - if not id: - self.name = name - else: - self.name = None - self.text = text - self.url = url - - @property - def id(self) -> Optional[str]: - """ - A valid SPDX license ID - - Returns: - `str` or `None` - """ - return self._id - - @id.setter - def id(self, id: Optional[str]) -> None: - self._id = id - - @property - def name(self) -> Optional[str]: - """ - If SPDX does not define the license used, this field may be used to provide the license name. - - Returns: - `str` or `None` - """ - return self._name - - @name.setter - def name(self, name: Optional[str]) -> None: - self._name = name - - @property - def text(self) -> Optional[AttachedText]: - """ - Specifies the optional full text of the attachment - - Returns: - `AttachedText` else `None` - """ - return self._text - - @text.setter - def text(self, text: Optional[AttachedText]) -> None: - self._text = text - - @property - def url(self) -> Optional[XsUri]: - """ - The URL to the attachment file. If the attachment is a license or BOM, an externalReference should also be - specified for completeness. - - Returns: - `XsUri` or `None` - """ - return self._url - - @url.setter - def url(self, url: Optional[XsUri]) -> None: - self._url = url - - def __eq__(self, other: object) -> bool: - if isinstance(other, License): - return hash(other) == hash(self) - return False - - def __lt__(self, other: Any) -> bool: - if isinstance(other, License): - return ComparableTuple((self.id, self.name)) < ComparableTuple((other.id, other.name)) - return NotImplemented - - def __hash__(self) -> int: - return hash((self.id, self.name, self.text, self.url)) - - def __repr__(self) -> str: - return f'' - - -@serializable.serializable_class -class LicenseChoice: - """ - This is our internal representation of `licenseChoiceType` complex type that can be used in multiple places within - a CycloneDX BOM document. - - .. note:: - See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_licenseChoiceType - """ - - def __init__(self, *, license: Optional[License] = None, expression: Optional[str] = None) -> None: - if not license and not expression: - raise NoPropertiesProvidedException( - 'One of `license` or `expression` must be supplied - neither supplied' - ) - if license and expression: - warnings.warn( - 'Both `license` and `expression` have been supplied - `license` will take precedence', - RuntimeWarning - ) - self.license = license - if not license: - self.expression = expression - else: - self.expression = None - - @property - def license(self) -> Optional[License]: - """ - License definition - - Returns: - `License` or `None` - """ - return self._license - - @license.setter - def license(self, license: Optional[License]) -> None: - self._license = license - - @property - def expression(self) -> Optional[str]: - """ - A valid SPDX license expression (not enforced). - - Refer to https://spdx.org/specifications for syntax requirements. - - Returns: - `str` or `None` - """ - return self._expression - - @expression.setter - def expression(self, expression: Optional[str]) -> None: - self._expression = expression - - def __eq__(self, other: object) -> bool: - if isinstance(other, LicenseChoice): - return hash(other) == hash(self) - return False - - def __lt__(self, other: Any) -> bool: - if isinstance(other, LicenseChoice): - return ComparableTuple((self.license, self.expression)) < ComparableTuple( - (other.license, other.expression)) - return NotImplemented - - def __hash__(self) -> int: - return hash((self.license, self.expression)) - - def __repr__(self) -> str: - return f'' - - @serializable.serializable_class class Property: """ diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index 5c559a08..6e604293 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -24,7 +24,8 @@ import serializable from sortedcontainers import SortedSet -from cyclonedx.serialization import UrnUuidHelper +from cyclonedx.serialization import UrnUuidHelper, LicenseRepositoryHelper +from .license import LicenseRepository, LicenseChoice from ..exception.model import UnknownComponentDependencyException from ..parser import BaseParser @@ -37,7 +38,6 @@ ) from . import ( ExternalReference, - LicenseChoice, OrganizationalContact, OrganizationalEntity, Property, @@ -195,9 +195,8 @@ def supplier(self, supplier: Optional[OrganizationalEntity]) -> None: @property @serializable.view(SchemaVersion1Dot3) @serializable.view(SchemaVersion1Dot4) - @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'licenses') - @serializable.xml_sequence(7) - def licenses(self) -> "SortedSet[LicenseChoice]": + @serializable.type_mapping(LicenseRepositoryHelper) + def licenses(self) -> LicenseRepository: """ A optional list of statements about how this BOM is licensed. @@ -208,7 +207,7 @@ def licenses(self) -> "SortedSet[LicenseChoice]": @licenses.setter def licenses(self, licenses: Iterable[LicenseChoice]) -> None: - self._licenses = SortedSet(licenses) + self._licenses = LicenseRepository(licenses) @property @serializable.view(SchemaVersion1Dot3) diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index f3fd386d..8cfe7563 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -28,6 +28,7 @@ from packageurl import PackageURL from sortedcontainers import SortedSet +from .license import LicenseChoice, LicenseRepository from ..exception.model import NoPropertiesProvidedException from ..schema.schema import ( SchemaVersion1Dot0, @@ -36,7 +37,7 @@ SchemaVersion1Dot3, SchemaVersion1Dot4, ) -from ..serialization import BomRefHelper, PackageUrl +from ..serialization import BomRefHelper, PackageUrl, LicenseRepositoryHelper from . import ( AttachedText, ComparableTuple, @@ -45,11 +46,10 @@ HashAlgorithm, HashType, IdentifiableAction, - LicenseChoice, OrganizationalEntity, Property, XsUri, - sha1sum, + sha1sum ) from .bom_ref import BomRef from .dependency import Dependable @@ -196,8 +196,8 @@ def __init__(self, *, licenses: Optional[Iterable[LicenseChoice]] = None, self.copyright = copyright or [] # type: ignore @property - @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'license') - def licenses(self) -> "SortedSet[LicenseChoice]": + @serializable.type_mapping(LicenseRepositoryHelper) + def licenses(self) -> LicenseRepository: """ Optional list of licenses obtained during analysis. @@ -208,7 +208,7 @@ def licenses(self) -> "SortedSet[LicenseChoice]": @licenses.setter def licenses(self, licenses: Iterable[LicenseChoice]) -> None: - self._licenses = SortedSet(licenses) + self._licenses = LicenseRepository(licenses) @property @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'text') @@ -809,7 +809,7 @@ def __init__(self, *, name: str, type: ComponentType = ComponentType.LIBRARY, 'standard', DeprecationWarning ) if not licenses: - self.licenses = [LicenseChoice(expression=license_str)] # type: ignore + self.licenses = [LicenseExpression(license_str)] # type: ignore self.__dependencies: "SortedSet[BomRef]" = SortedSet() @@ -1032,9 +1032,9 @@ def hashes(self, hashes: Iterable[HashType]) -> None: @serializable.view(SchemaVersion1Dot2) @serializable.view(SchemaVersion1Dot3) @serializable.view(SchemaVersion1Dot4) - @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'licenses') + @serializable.type_mapping(LicenseRepositoryHelper) @serializable.xml_sequence(10) - def licenses(self) -> "SortedSet[LicenseChoice]": + def licenses(self) -> LicenseRepository: """ A optional list of statements about how this Component is licensed. @@ -1045,7 +1045,7 @@ def licenses(self) -> "SortedSet[LicenseChoice]": @licenses.setter def licenses(self, licenses: Iterable[LicenseChoice]) -> None: - self._licenses = SortedSet(licenses) + self._licenses = LicenseRepository(licenses) @property @serializable.xml_sequence(11) diff --git a/cyclonedx/model/dependency.py b/cyclonedx/model/dependency.py index d275b160..01c36dbd 100644 --- a/cyclonedx/model/dependency.py +++ b/cyclonedx/model/dependency.py @@ -32,14 +32,14 @@ class DependencyDependencies(serializable.BaseHelper): # type: ignore @classmethod - def serialize(cls, o: object) -> List[str]: + def serialize(cls, o: object, t: 'SerializationType') -> List[str]: if isinstance(o, SortedSet): return list(map(lambda i: str(i.ref), o)) raise ValueError(f'Attempt to serialize a non-Dependency: {o.__class__}') @classmethod - def deserialize(cls, o: object) -> Set["Dependency"]: + def deserialize(cls, o: object, t: 'SerializationType') -> Set["Dependency"]: dependencies: Set["Dependency"] = set() if isinstance(o, list): for v in o: diff --git a/cyclonedx/model/license.py b/cyclonedx/model/license.py new file mode 100644 index 00000000..a5c1d080 --- /dev/null +++ b/cyclonedx/model/license.py @@ -0,0 +1,181 @@ +# encoding: utf-8 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +import warnings +from typing import Any, Optional, TypeAlias, Union + +import serializable +from sortedcontainers import SortedSet + +from . import AttachedText, XsUri, ComparableTuple +from ..exception.model import MutuallyExclusivePropertiesException + +""" +License related things +""" + + + +@serializable.serializable_class +class License: + """ + This is our internal representation of `licenseType` complex type that can be used in multiple places within + a CycloneDX BOM document. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_licenseType + """ + + def __init__(self, *, id: Optional[str] = None, name: Optional[str] = None, + text: Optional[AttachedText] = None, url: Optional[XsUri] = None) -> None: + if not id and not name: + raise MutuallyExclusivePropertiesException('Either `id` or `name` MUST be supplied') + if id and name: + warnings.warn( + 'Both `id` and `name` have been supplied - `name` will be ignored!', + RuntimeWarning + ) + self._id = id + self._name = name if not id else None + self._text = text + self._url = url + + @property + def id(self) -> Optional[str]: + """ + A valid SPDX license ID + + Returns: + `str` or `None` + """ + return self._id + + @id.setter + def id(self, id: Optional[str]) -> None: + self._id = id + + @property + def name(self) -> Optional[str]: + """ + If SPDX does not define the license used, this field may be used to provide the license name. + + Returns: + `str` or `None` + """ + return self._name + + @name.setter + def name(self, name: Optional[str]) -> None: + self._name = name + + @property + def text(self) -> Optional[AttachedText]: + """ + Specifies the optional full text of the attachment + + Returns: + `AttachedText` else `None` + """ + return self._text + + @text.setter + def text(self, text: Optional[AttachedText]) -> None: + self._text = text + + @property + def url(self) -> Optional[XsUri]: + """ + The URL to the attachment file. If the attachment is a license or BOM, an externalReference should also be + specified for completeness. + + Returns: + `XsUri` or `None` + """ + return self._url + + @url.setter + def url(self, url: Optional[XsUri]) -> None: + self._url = url + + def __eq__(self, other: object) -> bool: + if isinstance(other, License): + return hash(other) == hash(self) + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, License): + return ComparableTuple((self._id, self._name)) < ComparableTuple((other._id, other._name)) + return NotImplemented + + def __hash__(self) -> int: + return hash((self._id, self._name, self._text, self._url)) + + def __repr__(self) -> str: + return f'' + + +@serializable.serializable_class +class Expression: + """ + This is our internal representation of `licenseType`'s expression type that can be used in multiple places within + a CycloneDX BOM document. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_licenseType + """ + + def __init__(self, value: str) -> None: + self._value = value + + @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) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Expression): + return self._value == other._value + return NotImplemented + + def __lt__(self, other: Any) -> bool: + if isinstance(other, Expression): + return self._value < other._value + return NotImplemented + + def __repr__(self) -> str: + return f'' + + +LicenseChoice: TypeAlias = Union[Expression, License] + + +class LicenseRepository(SortedSet[LicenseChoice]): + pass diff --git a/cyclonedx/model/service.py b/cyclonedx/model/service.py index 7dbbccf7..5d82fb83 100644 --- a/cyclonedx/model/service.py +++ b/cyclonedx/model/service.py @@ -21,14 +21,14 @@ import serializable from sortedcontainers import SortedSet -from cyclonedx.serialization import BomRefHelper +from cyclonedx.serialization import BomRefHelper, LicenseRepositoryHelper +from .license import LicenseRepository, LicenseChoice from ..schema.schema import SchemaVersion1Dot3, SchemaVersion1Dot4 from . import ( ComparableTuple, DataClassification, ExternalReference, - LicenseChoice, OrganizationalEntity, Property, XsUri, @@ -248,9 +248,9 @@ def data(self, data: Iterable[DataClassification]) -> None: self._data = SortedSet(data) @property - @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'licenses') + @serializable.type_mapping(LicenseRepositoryHelper) @serializable.xml_sequence(10) - def licenses(self) -> "SortedSet[LicenseChoice]": + def licenses(self) -> LicenseRepository: """ A optional list of statements about how this Service is licensed. @@ -261,7 +261,7 @@ def licenses(self) -> "SortedSet[LicenseChoice]": @licenses.setter def licenses(self, licenses: Iterable[LicenseChoice]) -> None: - self._licenses = SortedSet(licenses) + self._licenses = LicenseRepository(licenses) @property @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference') diff --git a/cyclonedx/output/json.py b/cyclonedx/output/json.py index 83f83b27..1a9389e0 100644 --- a/cyclonedx/output/json.py +++ b/cyclonedx/output/json.py @@ -65,7 +65,7 @@ def generate(self, force_regeneration: bool = False) -> None: _view = SCHEMA_VERSIONS.get(self.schema_version_enum) if self.generated and force_regeneration: self.get_bom().validate() - bom_json = json.loads(self.get_bom().as_json(view_=_view)) # type: ignore + bom_json = json.loads(self.get_bom().as_json(view_=_view)) # type: dict bom_json.update(_json_core) self._json_output = json.dumps(bom_json) self.generated = True @@ -74,7 +74,7 @@ def generate(self, force_regeneration: bool = False) -> None: return else: self.get_bom().validate() - bom_json = json.loads(self.get_bom().as_json(view_=_view)) # type: ignore + bom_json = json.loads(self.get_bom().as_json(view_=_view)) # type: dict bom_json.update(_json_core) self._json_output = json.dumps(bom_json) self.generated = True diff --git a/cyclonedx/serialization/__init__.py b/cyclonedx/serialization/__init__.py index 7e84379e..99a0124c 100644 --- a/cyclonedx/serialization/__init__.py +++ b/cyclonedx/serialization/__init__.py @@ -18,26 +18,30 @@ Set of helper classes for use with ``serializable`` when conducting (de-)serialization. """ +import warnings +from typing import List from uuid import UUID # See https://github.com/package-url/packageurl-python/issues/65 from packageurl import PackageURL +from serializable import SerializationType from serializable.helpers import BaseHelper +from ..model.license import Expression, LicenseChoice from ..model.bom_ref import BomRef class BomRefHelper(BaseHelper): @classmethod - def serialize(cls, o: object) -> str: + def serialize(cls, o: object, t: SerializationType) -> str: if isinstance(o, BomRef): return o.value raise ValueError(f'Attempt to serialize a non-BomRef: {o.__class__}') @classmethod - def deserialize(cls, o: object) -> BomRef: + def deserialize(cls, o: object, t: SerializationType) -> BomRef: try: return BomRef(value=str(o)) except ValueError: @@ -47,14 +51,14 @@ def deserialize(cls, o: object) -> BomRef: class PackageUrl(BaseHelper): @classmethod - def serialize(cls, o: object) -> str: + def serialize(cls, o: object, t: SerializationType) -> str: if isinstance(o, PackageURL): return str(o.to_string()) raise ValueError(f'Attempt to serialize a non-PackageURL: {o.__class__}') @classmethod - def deserialize(cls, o: object) -> PackageURL: + def deserialize(cls, o: object, t: SerializationType) -> PackageURL: try: return PackageURL.from_string(purl=str(o)) except ValueError: @@ -64,15 +68,32 @@ def deserialize(cls, o: object) -> PackageURL: class UrnUuidHelper(BaseHelper): @classmethod - def serialize(cls, o: object) -> str: + def serialize(cls, o: object, t: SerializationType) -> str: if isinstance(o, UUID): return o.urn raise ValueError(f'Attempt to serialize a non-UUID: {o.__class__}') @classmethod - def deserialize(cls, o: object) -> UUID: + def deserialize(cls, o: object, t: SerializationType) -> UUID: try: return UUID(str(o)) except ValueError: raise ValueError(f'UUID string supplied ({o}) does not parse!') + + +class LicenseRepositoryHelper(BaseHelper): + @classmethod + def serialize(cls, o: object, t: SerializationType): + # need to call `list(o)`, because `o` could be any iterable. + licenses: List[LicenseChoice] = list(o) # type: ignore[call-overload] + expr = next((l for l in licenses if isinstance(l, Expression)), None) + if expr: + if len(licenses) > 1: + warnings.warn(f'Licenses: found an expression {expr!r}, dropping the rest of: {licenses!r}', RuntimeWarning) + return [expr] + return [{'license': l} for l in licenses] if t is SerializationType.JSON else licenses + + @classmethod + def deserialize(cls, o: object, t: SerializationType): + pass diff --git a/tests/data.py b/tests/data.py index 51e7679b..e6a2ee62 100644 --- a/tests/data.py +++ b/tests/data.py @@ -35,7 +35,6 @@ ExternalReferenceType, HashType, License, - LicenseChoice, Note, NoteText, OrganizationalContact, @@ -251,11 +250,11 @@ def get_bom_just_complete_metadata() -> Bom: bom.metadata.component = get_component_setuptools_complete() bom.metadata.manufacture = get_org_entity_1() bom.metadata.supplier = get_org_entity_2() - bom.metadata.licenses = [LicenseChoice(license=License( + bom.metadata.licenses = [License( id='Apache-2.0', text=AttachedText( content='VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=', encoding=Encoding.BASE_64 ), url=XsUri('https://www.apache.org/licenses/LICENSE-2.0.txt') - ))] + )] bom.metadata.properties = get_properties_1() return bom @@ -290,9 +289,7 @@ def get_bom_with_services_complex() -> Bom: authenticated=False, x_trust_boundary=True, data=[ DataClassification(flow=DataFlow.OUTBOUND, classification='public') ], - licenses=[ - LicenseChoice(expression='Commercial') - ], + licenses=[License(name='Commercial')], external_references=[ get_external_reference_1() ], @@ -319,9 +316,7 @@ def get_bom_with_nested_services() -> Bom: authenticated=False, x_trust_boundary=True, data=[ DataClassification(flow=DataFlow.OUTBOUND, classification='public') ], - licenses=[ - LicenseChoice(expression='Commercial') - ], + licenses=[License(name='Commercial')], external_references=[ get_external_reference_1() ], @@ -456,7 +451,7 @@ def get_component_setuptools_simple( purl=PackageURL( type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' ), - licenses=[LicenseChoice(expression='MIT License')], + licenses=[License(id='MIT')], author='Test Author' ) @@ -467,7 +462,7 @@ def get_component_setuptools_simple_no_version(bom_ref: Optional[str] = None) -> purl=PackageURL( type='pypi', name='setuptools', qualifiers='extension=tar.gz' ), - licenses=[LicenseChoice(expression='MIT License')], + licenses=[License(id='MIT')], author='Test Author' )