diff --git a/README.md b/README.md index 2b970b56..e3ec8db0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ [![shield_gh-workflow-test]][link_gh-workflow-test] [![shield_pypi-version]][link_pypi] [![shield_conda-forge-version]][link_conda-forge] -[![shield_license]][license_file] +[![shiled_rtd]][link_rtd] +[![shield_license]][license_file] [![shield_website]][link_website] [![shield_slack]][link_slack] [![shield_groups]][link_discussion] @@ -55,6 +56,7 @@ See the [LICENSE][license_file] file for the full license. [shield_gh-workflow-test]: https://img.shields.io/github/workflow/status/CycloneDX/cyclonedx-python-lib/Python%20CI/main?logo=GitHub&logoColor=white "build" [shield_pypi-version]: https://img.shields.io/pypi/v/cyclonedx-python-lib?logo=pypi&logoColor=white&label=PyPI "PyPI" [shield_conda-forge-version]: https://img.shields.io/conda/vn/conda-forge/cyclonedx-python-lib?logo=anaconda&logoColor=white&label=conda-forge "conda-forge" +[shiled_rtd]: https://readthedocs.org/projects/cyclonedx-python-library/badge/?version=latest "Read the Docs" [shield_license]: https://img.shields.io/github/license/CycloneDX/cyclonedx-python-lib "license" [shield_website]: https://img.shields.io/badge/https://-cyclonedx.org-blue.svg "homepage" [shield_slack]: https://img.shields.io/badge/slack-join-blue?logo=Slack&logoColor=white "slack join" @@ -63,6 +65,7 @@ See the [LICENSE][license_file] file for the full license. [link_gh-workflow-test]: https://github.com/CycloneDX/cyclonedx-python-lib/actions/workflows/poetry.yml?query=branch%3Amain [link_pypi]: https://pypi.org/project/cyclonedx-python-lib/ [link_conda-forge]: https://anaconda.org/conda-forge/cyclonedx-python-lib +[link_rtd]: https://cyclonedx-python-library.readthedocs.io/en/latest/?badge=latest [link_website]: https://cyclonedx.org/ [link_slack]: https://cyclonedx.org/slack/invite [link_discussion]: https://groups.io/g/CycloneDX diff --git a/cyclonedx/exception/output.py b/cyclonedx/exception/output.py index 57f8af12..58e83976 100644 --- a/cyclonedx/exception/output.py +++ b/cyclonedx/exception/output.py @@ -22,9 +22,10 @@ from . import CycloneDxException -class ComponentVersionRequiredException(CycloneDxException): +class FormatNotSupportedException(CycloneDxException): """ - Exception raised when attempting to output to an SBOM version that mandates a Component has a version, - but one is not available/present. + Exception raised when attempting to output a BOM to a format not supported in the requested version. + + For example, JSON is not supported prior to 1.2. """ pass diff --git a/cyclonedx/model/__init__.py b/cyclonedx/model/__init__.py index 113ed928..fae2284b 100644 --- a/cyclonedx/model/__init__.py +++ b/cyclonedx/model/__init__.py @@ -13,14 +13,14 @@ # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 -# - +# Copyright (c) OWASP Foundation. All Rights Reserved. import hashlib import re import sys import warnings +from datetime import datetime from enum import Enum -from typing import List, Optional, Union +from typing import Iterable, Optional, Set from ..exception.model import InvalidLocaleTypeException, InvalidUriException, NoPropertiesProvidedException, \ MutuallyExclusivePropertiesException, UnknownHashTypeException @@ -51,9 +51,84 @@ def sha1sum(filename: str) -> str: return h.hexdigest() +class DataFlow(Enum): + """ + This is our internal representation of the dataFlowType simple type within the CycloneDX standard. + + .. note:: + See the CycloneDX Schema: https://cyclonedx.org/docs/1.4/xml/#type_dataFlowType + """ + INBOUND = "inbound" + OUTBOUND = "outbound" + BI_DIRECTIONAL = "bi-directional" + UNKNOWN = "unknown" + + +class DataClassification: + """ + This is our internal representation of the `dataClassificationType` complex type within the CycloneDX standard. + + .. note:: + See the CycloneDX Schema for dataClassificationType: + https://cyclonedx.org/docs/1.4/xml/#type_dataClassificationType + """ + + def __init__(self, *, flow: DataFlow, classification: str) -> None: + self.flow = flow + self.classification = classification + + @property + def flow(self) -> DataFlow: + """ + Specifies the flow direction of the data. + + Valid values are: inbound, outbound, bi-directional, and unknown. + + Direction is relative to the service. + + - Inbound flow states that data enters the service + - Outbound flow states that data leaves the service + - Bi-directional states that data flows both ways + - Unknown states that the direction is not known + + Returns: + `DataFlow` + """ + return self._flow + + @flow.setter + def flow(self, flow: DataFlow) -> None: + self._flow = flow + + @property + def classification(self) -> str: + """ + Data classification tags data according to its type, sensitivity, and value if altered, stolen, or destroyed. + + Returns: + `str` + """ + return self._classification + + @classification.setter + def classification(self, classification: str) -> None: + self._classification = classification + + def __eq__(self, other: object) -> bool: + if isinstance(other, DataClassification): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.flow, self.classification)) + + def __repr__(self) -> str: + return f'' + + class Encoding(Enum): """ - This is out internal representation of the encoding simple type within the CycloneDX standard. + This is our internal representation of the encoding simple type within the CycloneDX standard. .. note:: See the CycloneDX Schema: https://cyclonedx.org/docs/1.4/#type_encoding @@ -71,7 +146,7 @@ class AttachedText: DEFAULT_CONTENT_TYPE = 'text/plain' - def __init__(self, content: str, content_type: str = DEFAULT_CONTENT_TYPE, + def __init__(self, *, content: str, content_type: str = DEFAULT_CONTENT_TYPE, encoding: Optional[Encoding] = None) -> None: self.content_type = content_type self.encoding = encoding @@ -122,6 +197,17 @@ def content(self) -> str: def content(self, content: str) -> None: self._content = content + def __eq__(self, other: object) -> bool: + if isinstance(other, AttachedText): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.content, self.content_type, self.encoding)) + + def __repr__(self) -> str: + return f'' + class HashAlgorithm(Enum): """ @@ -191,18 +277,48 @@ def from_composite_str(composite_hash: str) -> 'HashType': raise UnknownHashTypeException(f"Unable to determine hash type from '{composite_hash}'") - def __init__(self, algorithm: HashAlgorithm, hash_value: str) -> None: - self._alg = algorithm - self._content = hash_value + def __init__(self, *, algorithm: HashAlgorithm, hash_value: str) -> None: + self.alg = algorithm + self.content = hash_value + + @property + def alg(self) -> HashAlgorithm: + """ + Specifies the algorithm used to create the hash. - def get_algorithm(self) -> HashAlgorithm: + Returns: + `HashAlgorithm` + """ return self._alg - def get_hash_value(self) -> str: + @alg.setter + def alg(self, alg: HashAlgorithm) -> None: + self._alg = alg + + @property + def content(self) -> str: + """ + Hash value content. + + Returns: + `str` + """ return self._content + @content.setter + def content(self, content: str) -> None: + self._content = content + + def __eq__(self, other: object) -> bool: + if isinstance(other, HashType): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.alg, self.content)) + def __repr__(self) -> str: - return f'' + return f'' class ExternalReferenceType(Enum): @@ -253,10 +369,16 @@ def __init__(self, uri: str) -> None: def __eq__(self, other: object) -> bool: if isinstance(other, XsUri): - return str(self) == str(other) + return hash(other) == hash(self) return False + def __hash__(self) -> int: + return hash(self._uri) + def __repr__(self) -> str: + return f'' + + def __str__(self) -> str: return self._uri @@ -269,61 +391,85 @@ class ExternalReference: See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.3/#type_externalReference """ - def __init__(self, reference_type: ExternalReferenceType, url: Union[str, XsUri], comment: str = '', - hashes: Optional[List[HashType]] = None) -> None: - self._type: ExternalReferenceType = reference_type - self._url = str(url) - self._comment = comment - self._hashes: List[HashType] = hashes if hashes else [] + def __init__(self, *, reference_type: ExternalReferenceType, url: XsUri, comment: Optional[str] = None, + hashes: Optional[Iterable[HashType]] = None) -> None: + self.url = url + self.comment = comment + self.type = reference_type + self.hashes = set(hashes or []) - def add_hash(self, our_hash: HashType) -> None: + @property + def url(self) -> XsUri: """ - Adds a hash that pins/identifies this External Reference. + The URL to the external reference. - Args: - our_hash: - `HashType` instance + Returns: + `XsUri` """ - self._hashes.append(our_hash) + return self._url + + @url.setter + def url(self, url: XsUri) -> None: + self._url = url - def get_comment(self) -> Union[str, None]: + @property + def comment(self) -> Optional[str]: """ - Get the comment for this External Reference. + An optional comment describing the external reference. Returns: - Any comment as a `str` else `None`. + `str` if set else `None` """ return self._comment - def get_hashes(self) -> List[HashType]: - """ - List of cryptographic hashes that identify this External Reference. + @comment.setter + def comment(self, comment: Optional[str]) -> None: + self._comment = comment - Returns: - `List` of `HashType` objects where there are any hashes, else an empty `List`. + @property + def type(self) -> ExternalReferenceType: """ - return self._hashes + Specifies the type of external reference. - def get_reference_type(self) -> ExternalReferenceType: - """ - Get the type of this External Reference. + There are built-in types to describe common references. If a type does not exist for the reference being + referred to, use the "other" type. Returns: - `ExternalReferenceType` that represents the type of this External Reference. + `ExternalReferenceType` """ return self._type - def get_url(self) -> str: + @type.setter + def type(self, type_: ExternalReferenceType) -> None: + self._type = type_ + + @property + def hashes(self) -> Set[HashType]: """ - Get the URL/URI for this External Reference. + The hashes of the external reference (if applicable). Returns: - URI as a `str`. + Set of `HashType` """ - return self._url + return self._hashes + + @hashes.setter + def hashes(self, hashes: Iterable[HashType]) -> None: + self._hashes = set(hashes) + + def __eq__(self, other: object) -> bool: + if isinstance(other, ExternalReference): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash(( + self._type, self._url, self._comment, + tuple(sorted(self._hashes, key=hash)) + )) def __repr__(self) -> str: - return f' {self._hashes}' + return f'' class License: @@ -335,17 +481,17 @@ class License: See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_licenseType """ - def __init__(self, spxd_license_id: Optional[str] = None, license_name: Optional[str] = None, + def __init__(self, *, spdx_license_id: Optional[str] = None, license_name: Optional[str] = None, license_text: Optional[AttachedText] = None, license_url: Optional[XsUri] = None) -> None: - if not spxd_license_id and not license_name: - raise MutuallyExclusivePropertiesException('Either `spxd_license_id` or `license_name` MUST be supplied') - if spxd_license_id and license_name: + if not spdx_license_id and not license_name: + raise MutuallyExclusivePropertiesException('Either `spdx_license_id` or `license_name` MUST be supplied') + if spdx_license_id and license_name: warnings.warn( - 'Both `spxd_license_id` and `license_name` have been supplied - `license_name` will be ignored!', + 'Both `spdx_license_id` and `license_name` have been supplied - `license_name` will be ignored!', RuntimeWarning ) - self.id = spxd_license_id - if not spxd_license_id: + self.id = spdx_license_id + if not spdx_license_id: self.name = license_name else: self.name = None @@ -409,6 +555,17 @@ def url(self) -> Optional[XsUri]: 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 __hash__(self) -> int: + return hash((self.id, self.name, self.text, self.url)) + + def __repr__(self) -> str: + return f'' + class LicenseChoice: """ @@ -419,18 +576,18 @@ class LicenseChoice: See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_licenseChoiceType """ - def __init__(self, license: Optional[License] = None, license_expression: Optional[str] = None) -> None: - if not license and not license_expression: + def __init__(self, *, license_: Optional[License] = None, license_expression: Optional[str] = None) -> None: + if not license_ and not license_expression: raise NoPropertiesProvidedException( 'One of `license` or `license_expression` must be supplied - neither supplied' ) - if license and license_expression: + if license_ and license_expression: warnings.warn( 'Both `license` and `license_expression` have been supplied - `license` will take precedence', RuntimeWarning ) - self.license = license - if not license: + self.license = license_ + if not license_: self.expression = license_expression else: self.expression = None @@ -446,8 +603,8 @@ def license(self) -> Optional[License]: return self._license @license.setter - def license(self, license: Optional[License]) -> None: - self._license = license + def license(self, license_: Optional[License]) -> None: + self._license = license_ @property def expression(self) -> Optional[str]: @@ -465,6 +622,17 @@ def expression(self) -> Optional[str]: 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 __hash__(self) -> int: + return hash((self.license, self.expression)) + + def __repr__(self) -> str: + return f'' + class Property: """ @@ -477,28 +645,51 @@ class Property: Specifies an individual property with a name and value. """ - def __init__(self, name: str, value: str) -> None: - self._name = name - self._value = value + def __init__(self, *, name: str, value: str) -> None: + self.name = name + self.value = value - def get_name(self) -> str: + @property + def name(self) -> str: """ - Get the name of this Property. + The name of the property. + + Duplicate names are allowed, each potentially having a different value. Returns: - Name of this Property as `str`. + `str` """ return self._name - def get_value(self) -> str: + @name.setter + def name(self, name: str) -> None: + self._name = name + + @property + def value(self) -> str: """ - Get the value of this Property. + Value of this Property. Returns: - Value of this Property as `str`. + `str` """ return self._value + @value.setter + def value(self, value: str) -> None: + self._value = value + + def __eq__(self, other: object) -> bool: + if isinstance(other, Property): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.name, self.value)) + + def __repr__(self) -> str: + return f'' + class NoteText: """ @@ -511,7 +702,7 @@ class NoteText: DEFAULT_CONTENT_TYPE: str = 'text/plain' - def __init__(self, content: str, content_type: Optional[str] = None, + def __init__(self, *, content: str, content_type: Optional[str] = None, content_encoding: Optional[Encoding] = None) -> None: self.content = content self.content_type = content_type or NoteText.DEFAULT_CONTENT_TYPE @@ -561,6 +752,17 @@ def encoding(self) -> Optional[Encoding]: def encoding(self, encoding: Optional[Encoding]) -> None: self._encoding = encoding + def __eq__(self, other: object) -> bool: + if isinstance(other, NoteText): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.content, self.content_type, self.encoding)) + + def __repr__(self) -> str: + return f'' + class Note: """ @@ -573,7 +775,7 @@ class Note: _LOCALE_TYPE_REGEX = re.compile(r'^[a-z]{2}(?:\-[A-Z]{2})?$') - def __init__(self, text: NoteText, locale: Optional[str] = None) -> None: + def __init__(self, *, text: NoteText, locale: Optional[str] = None) -> None: self.text = text self.locale = locale @@ -617,6 +819,17 @@ def locale(self, locale: Optional[str]) -> None: f"ISO-3166 (or higher) country code. according to ISO-639 format. Examples include: 'en', 'en-US'." ) + def __eq__(self, other: object) -> bool: + if isinstance(other, Note): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.text, self.locale)) + + def __repr__(self) -> str: + return f'' + class OrganizationalContact: """ @@ -627,14 +840,14 @@ class OrganizationalContact: See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_organizationalContact """ - def __init__(self, name: Optional[str] = None, phone: Optional[str] = None, email: Optional[str] = None) -> None: + def __init__(self, *, name: Optional[str] = None, phone: Optional[str] = None, email: Optional[str] = None) -> None: if not name and not phone and not email: raise NoPropertiesProvidedException( 'One of name, email or phone must be supplied for an OrganizationalContact - none supplied.' ) - self._name: Optional[str] = name - self._email: Optional[str] = email - self._phone: Optional[str] = phone + self.name = name + self.email = email + self.phone = phone @property def name(self) -> Optional[str]: @@ -646,6 +859,10 @@ def name(self) -> Optional[str]: """ return self._name + @name.setter + def name(self, name: Optional[str]) -> None: + self._name = name + @property def email(self) -> Optional[str]: """ @@ -656,6 +873,10 @@ def email(self) -> Optional[str]: """ return self._email + @email.setter + def email(self, email: Optional[str]) -> None: + self._email = email + @property def phone(self) -> Optional[str]: """ @@ -666,6 +887,21 @@ def phone(self) -> Optional[str]: """ return self._phone + @phone.setter + def phone(self, phone: Optional[str]) -> None: + self._phone = phone + + def __eq__(self, other: object) -> bool: + if isinstance(other, OrganizationalContact): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.name, self.phone, self.email)) + + def __repr__(self) -> str: + return f'' + class OrganizationalEntity: """ @@ -676,15 +912,15 @@ class OrganizationalEntity: See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_organizationalEntity """ - def __init__(self, name: Optional[str] = None, urls: Optional[List[XsUri]] = None, - contacts: Optional[List[OrganizationalContact]] = None) -> None: + def __init__(self, *, name: Optional[str] = None, urls: Optional[Iterable[XsUri]] = None, + contacts: Optional[Iterable[OrganizationalContact]] = None) -> None: if not name and not urls and not contacts: raise NoPropertiesProvidedException( 'One of name, urls or contacts must be supplied for an OrganizationalEntity - none supplied.' ) - self._name: Optional[str] = name - self._url: Optional[List[XsUri]] = urls - self._contact: Optional[List[OrganizationalContact]] = contacts + self.name = name + self.url = set(urls or []) + self.contact = set(contacts or []) @property def name(self) -> Optional[str]: @@ -696,26 +932,49 @@ def name(self) -> Optional[str]: """ return self._name + @name.setter + def name(self, name: Optional[str]) -> None: + self._name = name + @property - def urls(self) -> Optional[List[XsUri]]: + def url(self) -> Set[XsUri]: """ Get a list of URLs of the organization. Multiple URLs are allowed. Returns: - `List[XsUri]` if set else `None` + Set of `XsUri` """ return self._url + @url.setter + def url(self, urls: Iterable[XsUri]) -> None: + self._url = set(urls) + @property - def contacts(self) -> Optional[List[OrganizationalContact]]: + def contact(self) -> Set[OrganizationalContact]: """ Get a list of contact person at the organization. Multiple contacts are allowed. Returns: - `List[OrganizationalContact]` if set else `None` + Set of `OrganizationalContact` """ return self._contact + @contact.setter + def contact(self, contacts: Iterable[OrganizationalContact]) -> None: + self._contact = set(contacts) + + def __eq__(self, other: object) -> bool: + if isinstance(other, OrganizationalEntity): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.name, tuple(self.url), tuple(self.contact))) + + def __repr__(self) -> str: + return f'' + class Tool: """ @@ -727,88 +986,206 @@ class Tool: See the CycloneDX Schema for toolType: https://cyclonedx.org/docs/1.3/#type_toolType """ - def __init__(self, vendor: Optional[str] = None, name: Optional[str] = None, version: Optional[str] = None, - hashes: Optional[List[HashType]] = None, - external_references: Optional[List[ExternalReference]] = None) -> None: - self._vendor = vendor - self._name = name - self._version = version - self._hashes: List[HashType] = hashes or [] - self._external_references: List[ExternalReference] = external_references or [] + def __init__(self, *, vendor: Optional[str] = None, name: Optional[str] = None, version: Optional[str] = None, + hashes: Optional[Iterable[HashType]] = None, + external_references: Optional[Iterable[ExternalReference]] = None) -> None: + self.vendor = vendor + self.name = name + self.version = version + self.hashes = set(hashes or []) + self.external_references = set(external_references or []) - def add_external_reference(self, reference: ExternalReference) -> None: + @property + def vendor(self) -> Optional[str]: """ - Add an external reference to this Tool. + The name of the vendor who created the tool. - Args: - reference: - `ExternalReference` to add to this Tool. + Returns: + `str` if set else `None` + """ + return self._vendor + + @vendor.setter + def vendor(self, vendor: Optional[str]) -> None: + self._vendor = vendor + + @property + def name(self) -> Optional[str]: + """ + The name of the tool. Returns: - None + `str` if set else `None` """ - self._external_references.append(reference) + return self._name + + @name.setter + def name(self, name: Optional[str]) -> None: + self._name = name - def add_external_references(self, references: List[ExternalReference]) -> None: + @property + def version(self) -> Optional[str]: """ - Add a list of external reference to this Tool. + The version of the tool. - Args: - references: - List of `ExternalReference` to add to this Tool. + Returns: + `str` if set else `None` + """ + return self._version + + @version.setter + def version(self, version: Optional[str]) -> None: + self._version = version + + @property + def hashes(self) -> Set[HashType]: + """ + The hashes of the tool (if applicable). Returns: - None + Set of `HashType` """ - self._external_references = self._external_references + references + return self._hashes - def get_external_references(self) -> List[ExternalReference]: + @hashes.setter + def hashes(self, hashes: Iterable[HashType]) -> None: + self._hashes = set(hashes) + + @property + def external_references(self) -> Set[ExternalReference]: """ - List of External References that relate to this Tool. + External References provide a way to document systems, sites, and information that may be relevant but which + are not included with the BOM. Returns: - `List` of `ExternalReference` objects where there are, else an empty `List`. + Set of `ExternalReference` """ return self._external_references - def get_hashes(self) -> List[HashType]: + @external_references.setter + def external_references(self, external_references: Iterable[ExternalReference]) -> None: + self._external_references = set(external_references) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Tool): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.vendor, self.name, self.version, tuple(self.hashes), tuple(self.external_references))) + + def __repr__(self) -> str: + return f'' + + +class IdentifiableAction: + """ + This is out internal representation of the `identifiableActionType` complex type. + + .. note:: + See the CycloneDX specification: https://cyclonedx.org/docs/1.4/xml/#type_identifiableActionType + """ + + def __init__(self, *, timestamp: Optional[datetime] = None, name: Optional[str] = None, + email: Optional[str] = None) -> None: + if not timestamp and not name and not email: + raise NoPropertiesProvidedException( + 'At least one of `timestamp`, `name` or `email` must be provided for an `IdentifiableAction`.' + ) + + self.timestamp = timestamp + self.name = name + self.email = email + + @property + def timestamp(self) -> Optional[datetime]: """ - List of cryptographic hashes that identify this version of this Tool. + The timestamp in which the action occurred. Returns: - `List` of `HashType` objects where there are any hashes, else an empty `List`. + `datetime` if set else `None` """ - return self._hashes + return self._timestamp + + @timestamp.setter + def timestamp(self, timestamp: Optional[datetime]) -> None: + self._timestamp = timestamp - def get_name(self) -> Optional[str]: + @property + def name(self) -> Optional[str]: """ - The name of this Tool. + The name of the individual who performed the action. Returns: - `str` representing the name of the Tool + `str` if set else `None` """ return self._name - def get_vendor(self) -> Optional[str]: + @name.setter + def name(self, name: Optional[str]) -> None: + self._name = name + + @property + def email(self) -> Optional[str]: """ - The vendor of this Tool. + The email address of the individual who performed the action. Returns: - `str` representing the vendor of the Tool + `str` if set else `None` """ - return self._vendor + return self._email + + @email.setter + def email(self, email: Optional[str]) -> None: + self._email = email + + def __eq__(self, other: object) -> bool: + if isinstance(other, IdentifiableAction): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.timestamp, self.name, self.email)) + + def __repr__(self) -> str: + return f'' - def get_version(self) -> Optional[str]: + +class Copyright: + """ + This is out internal representation of the `copyrightsType` complex type. + + .. note:: + See the CycloneDX specification: https://cyclonedx.org/docs/1.4/xml/#type_copyrightsType + """ + + def __init__(self, *, text: str) -> None: + self.text = text + + @property + def text(self) -> str: """ - The version of this Tool. + Copyright statement. Returns: - `str` representing the version of the Tool + `str` if set else `None` """ - return self._version + return self._text + + @text.setter + def text(self, text: str) -> None: + self._text = text + + def __eq__(self, other: object) -> bool: + if isinstance(other, Copyright): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash(self.text) def __repr__(self) -> str: - return ''.format(self._vendor, self._name, self._version) + return f'' if sys.version_info >= (3, 8): @@ -821,7 +1198,7 @@ def __repr__(self) -> str: except Exception: __ThisToolVersion = None ThisTool = Tool(vendor='CycloneDX', name='cyclonedx-python-lib', version=__ThisToolVersion or 'UNKNOWN') -ThisTool.add_external_references(references=[ +ThisTool.external_references.update([ ExternalReference( reference_type=ExternalReferenceType.BUILD_SYSTEM, url=XsUri('https://github.com/CycloneDX/cyclonedx-python-lib/actions') diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index d0aa0068..e459f859 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -16,13 +16,13 @@ # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. - from datetime import datetime, timezone -from typing import List, Optional +from typing import Iterable, Optional, Set from uuid import uuid4, UUID -from . import ThisTool, Tool +from . import ExternalReference, OrganizationalContact, OrganizationalEntity, LicenseChoice, Property, ThisTool, Tool from .component import Component +from .service import Service from ..parser import BaseParser @@ -31,56 +31,72 @@ class BomMetaData: This is our internal representation of the metadata complex type within the CycloneDX standard. .. note:: - See the CycloneDX Schema for Bom metadata: https://cyclonedx.org/docs/1.3/#type_metadata + See the CycloneDX Schema for Bom metadata: https://cyclonedx.org/docs/1.4/#type_metadata """ - def __init__(self, tools: Optional[List[Tool]] = None) -> None: + def __init__(self, *, tools: Optional[Iterable[Tool]] = None, + authors: Optional[Iterable[OrganizationalContact]] = None, component: Optional[Component] = None, + manufacture: Optional[OrganizationalEntity] = None, + supplier: Optional[OrganizationalEntity] = None, + licenses: Optional[Iterable[LicenseChoice]] = None, + properties: Optional[Iterable[Property]] = None) -> None: self.timestamp = datetime.now(tz=timezone.utc) - self.tools = tools if tools else [] + self.tools = set(tools or []) + self.authors = set(authors or []) + self.component = component + self.manufacture = manufacture + self.supplier = supplier + self.licenses = set(licenses or []) + self.properties = set(properties or []) if not self.tools: - self.add_tool(ThisTool) + self.tools.add(ThisTool) - self.component: Optional[Component] = None + @property + def timestamp(self) -> datetime: + """ + The date and time (in UTC) when this BomMetaData was created. + + Returns: + `datetime` instance in UTC timezone + """ + return self._timestamp + + @timestamp.setter + def timestamp(self, timestamp: datetime) -> None: + self._timestamp = timestamp @property - def tools(self) -> List[Tool]: + def tools(self) -> Set[Tool]: """ Tools used to create this BOM. Returns: - `List` of `Tool` objects where there are any, else an empty `List`. + `Set` of `Tool` objects. """ return self._tools @tools.setter - def tools(self, tools: List[Tool]) -> None: - self._tools = tools + def tools(self, tools: Iterable[Tool]) -> None: + self._tools = set(tools) - def add_tool(self, tool: Tool) -> None: + @property + def authors(self) -> Set[OrganizationalContact]: """ - Add a Tool definition to this Bom Metadata. The `cyclonedx-python-lib` is automatically added - you do not need - to add this yourself. + The person(s) who created the BOM. - Args: - tool: - Instance of `Tool` that represents the tool you are using. - """ - self._tools.append(tool) + Authors are common in BOMs created through manual processes. - @property - def timestamp(self) -> datetime: - """ - The date and time (in UTC) when this BomMetaData was created. + BOMs created through automated means may not have authors. Returns: - `datetime` instance in UTC timezone + Set of `OrganizationalContact` """ - return self._timestamp + return self._authors - @timestamp.setter - def timestamp(self, timestamp: datetime) -> None: - self._timestamp = timestamp + @authors.setter + def authors(self, authors: Iterable[OrganizationalContact]) -> None: + self._authors = set(authors) @property def component(self) -> Optional[Component]: @@ -106,6 +122,81 @@ def component(self, component: Component) -> None: """ self._component = component + @property + def manufacture(self) -> Optional[OrganizationalEntity]: + """ + The organization that manufactured the component that the BOM describes. + + Returns: + `OrganizationalEntity` if set else `None` + """ + return self._manufacture + + @manufacture.setter + def manufacture(self, manufacture: Optional[OrganizationalEntity]) -> None: + self._manufacture = manufacture + + @property + def supplier(self) -> Optional[OrganizationalEntity]: + """ + The organization that supplied the component that the BOM describes. + + The supplier may often be the manufacturer, but may also be a distributor or repackager. + + Returns: + `OrganizationalEntity` if set else `None` + """ + return self._supplier + + @supplier.setter + def supplier(self, supplier: Optional[OrganizationalEntity]) -> None: + self._supplier = supplier + + @property + def licenses(self) -> Set[LicenseChoice]: + """ + A optional list of statements about how this BOM is licensed. + + Returns: + Set of `LicenseChoice` + """ + return self._licenses + + @licenses.setter + def licenses(self, licenses: Iterable[LicenseChoice]) -> None: + self._licenses = set(licenses) + + @property + def properties(self) -> Set[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. + + Property names of interest to the general public are encouraged to be registered in the CycloneDX Property + Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. Formal registration is OPTIONAL. + + Return: + Set of `Property` + """ + return self._properties + + @properties.setter + def properties(self, properties: Iterable[Property]) -> None: + self._properties = set(properties) + + def __eq__(self, other: object) -> bool: + if isinstance(other, BomMetaData): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash(( + self.timestamp, self.tools, self.component + )) + + def __repr__(self) -> str: + return f'' + class Bom: """ @@ -130,10 +221,12 @@ def from_parser(parser: BaseParser) -> 'Bom': `cyclonedx.model.bom.Bom`: A Bom instance that represents the valid data held in the supplied parser. """ bom = Bom() - bom.add_components(parser.get_components()) + bom.components.update(parser.get_components()) return bom - def __init__(self) -> None: + def __init__(self, *, components: Optional[Iterable[Component]] = None, + services: Optional[Iterable[Service]] = None, + external_references: Optional[Iterable[ExternalReference]] = None) -> None: """ Create a new Bom that you can manually/programmatically add data to later. @@ -142,7 +235,9 @@ def __init__(self) -> None: """ self.uuid = uuid4() self.metadata = BomMetaData() - self._components: List[Component] = [] + self.components = set(components or []) + self.services = set(services or []) + self.external_references = set(external_references or []) @property def uuid(self) -> UUID: @@ -176,58 +271,22 @@ def metadata(self, metadata: BomMetaData) -> None: self._metadata = metadata @property - def components(self) -> List[Component]: + def components(self) -> Set[Component]: """ Get all the Components currently in this Bom. Returns: - List of all Components in this Bom. + Set of `Component` in this Bom """ return self._components @components.setter - def components(self, components: List[Component]) -> None: - self._components = components - - def add_component(self, component: Component) -> None: - """ - Add a Component to this Bom instance. - - Args: - component: - `cyclonedx.model.component.Component` instance to add to this Bom. - - Returns: - None - """ - if not self.has_component(component=component): - self._components.append(component) - - def add_components(self, components: List[Component]) -> None: - """ - Add multiple Components at once to this Bom instance. - - Args: - components: - List of `cyclonedx.model.component.Component` instances to add to this Bom. - - Returns: - None - """ - self.components = self._components + components - - def component_count(self) -> int: - """ - Returns the current count of Components within this Bom. - - Returns: - The number of Components in this Bom as `int`. - """ - return len(self._components) + def components(self, components: Iterable[Component]) -> None: + self._components = set(components) def get_component_by_purl(self, purl: Optional[str]) -> Optional[Component]: """ - Get a Component already in the Bom by it's PURL + Get a Component already in the Bom by its PURL Args: purl: @@ -263,7 +322,35 @@ def has_component(self, component: Component) -> bool: Returns: `bool` - `True` if the supplied Component is part of this Bom, `False` otherwise. """ - return component in self._components + return component in self.components + + @property + def services(self) -> Set[Service]: + """ + Get all the Services currently in this Bom. + + Returns: + Set of `Service` in this BOM + """ + return self._services + + @services.setter + def services(self, services: Iterable[Service]) -> None: + self._services = set(services) + + @property + def external_references(self) -> Set[ExternalReference]: + """ + Provides the ability to document external references related to the BOM or to the project the BOM describes. + + Returns: + Set of `ExternalReference` + """ + return self._external_references + + @external_references.setter + def external_references(self, external_references: Iterable[ExternalReference]) -> None: + self._external_references = set(external_references) def has_vulnerabilities(self) -> bool: """ @@ -273,8 +360,17 @@ def has_vulnerabilities(self) -> bool: `bool` - `True` if at least one `cyclonedx.model.component.Component` has at least one Vulnerability, `False` otherwise. """ - for c in self.components: - if c.has_vulnerabilities(): - return True + return any(c.has_vulnerabilities() for c in self.components) + def __eq__(self, other: object) -> bool: + if isinstance(other, Bom): + return hash(other) == hash(self) return False + + def __hash__(self) -> int: + return hash(( + self.uuid, self.metadata, tuple(self.components), tuple(self.services), tuple(self.external_references) + )) + + def __repr__(self) -> str: + return f'' diff --git a/cyclonedx/model/bom_ref.py b/cyclonedx/model/bom_ref.py new file mode 100644 index 00000000..27eec97b --- /dev/null +++ b/cyclonedx/model/bom_ref.py @@ -0,0 +1,56 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# 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. +from typing import Optional +from uuid import uuid4 + + +class BomRef: + """ + An identifier that can be used to reference objects elsewhere in the BOM. + + This copies a similar pattern used in the CycloneDX Python Library. + + .. note:: + See https://github.com/CycloneDX/cyclonedx-php-library/blob/master/docs/dev/decisions/BomDependencyDataModel.md + """ + + def __init__(self, value: Optional[str] = None) -> None: + self.value = value or str(uuid4()) + + @property + def value(self) -> str: + return self._value + + @value.setter + def value(self, value: str) -> None: + self._value = value + + def __eq__(self, other: object) -> bool: + if isinstance(other, BomRef): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash(self.value) + + def __repr__(self) -> str: + return f' str: + return self.value diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index 8998620c..8d07db45 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -16,19 +16,186 @@ # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. - import warnings from enum import Enum from os.path import exists -from typing import List, Optional -from uuid import uuid4 +from typing import Iterable, Optional, Set # See https://github.com/package-url/packageurl-python/issues/65 from packageurl import PackageURL # type: ignore -from . import ExternalReference, HashAlgorithm, HashType, OrganizationalEntity, sha1sum, LicenseChoice, Property +from . import AttachedText, Copyright, ExternalReference, HashAlgorithm, HashType, IdentifiableAction, LicenseChoice, \ + OrganizationalEntity, Property, sha1sum, XsUri +from .bom_ref import BomRef +from .issue import IssueType from .release_note import ReleaseNotes from .vulnerability import Vulnerability +from ..exception.model import NoPropertiesProvidedException + + +class Commit: + """ + Our internal representation of the `commitType` complex type. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_commitType + """ + + def __init__(self, *, uid: Optional[str] = None, url: Optional[XsUri] = None, + author: Optional[IdentifiableAction] = None, committer: Optional[IdentifiableAction] = None, + message: Optional[str] = None) -> None: + if not uid and not url and not author and not committer and not message: + raise NoPropertiesProvidedException( + 'At least one of `uid`, `url`, `author`, `committer` or `message` must be provided for a `Commit`.' + ) + + self.uid = uid + self.url = url + self.author = author + self.committer = committer + self.message = message + + @property + def uid(self) -> Optional[str]: + """ + A unique identifier of the commit. This may be version control specific. For example, Subversion uses revision + numbers whereas git uses commit hashes. + + Returns: + `str` if set else `None` + """ + return self._uid + + @uid.setter + def uid(self, uid: Optional[str]) -> None: + self._uid = uid + + @property + def url(self) -> Optional[XsUri]: + """ + The URL to the commit. This URL will typically point to a commit in a version control system. + + Returns: + `XsUri` if set else `None` + """ + return self._url + + @url.setter + def url(self, url: Optional[XsUri]) -> None: + self._url = url + + @property + def author(self) -> Optional[IdentifiableAction]: + """ + The author who created the changes in the commit. + + Returns: + `IdentifiableAction` if set else `None` + """ + return self._author + + @author.setter + def author(self, author: Optional[IdentifiableAction]) -> None: + self._author = author + + @property + def committer(self) -> Optional[IdentifiableAction]: + """ + The person who committed or pushed the commit + + Returns: + `IdentifiableAction` if set else `None` + """ + return self._committer + + @committer.setter + def committer(self, committer: Optional[IdentifiableAction]) -> None: + self._committer = committer + + @property + def message(self) -> Optional[str]: + """ + The text description of the contents of the commit. + + Returns: + `str` if set else `None` + """ + return self._message + + @message.setter + def message(self, message: Optional[str]) -> None: + self._message = message + + def __eq__(self, other: object) -> bool: + if isinstance(other, Commit): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.uid, self.url, self.author, self.committer, self.message)) + + def __repr__(self) -> str: + return f'' + + +class ComponentEvidence: + """ + Our internal representation of the `componentEvidenceType` complex type. + + Provides the ability to document evidence collected through various forms of extraction or analysis. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_componentEvidenceType + """ + + def __init__(self, *, licenses: Optional[Iterable[LicenseChoice]] = None, + copyright_: Optional[Iterable[Copyright]] = None) -> None: + if not licenses and not copyright_: + raise NoPropertiesProvidedException( + 'At least one of `licenses` or `copyright_` must be supplied for a `ComponentEvidence`.' + ) + + self.licenses = set(licenses or []) + self.copyright = set(copyright_ or []) + + @property + def licenses(self) -> Set[LicenseChoice]: + """ + Optional list of licenses obtained during analysis. + + Returns: + Set of `LicenseChoice` + """ + return self._licenses + + @licenses.setter + def licenses(self, licenses: Iterable[LicenseChoice]) -> None: + self._licenses = set(licenses) + + @property + def copyright(self) -> Set[Copyright]: + """ + Optional list of copyright statements. + + Returns: + Set of `Copyright` + """ + return self._copyright + + @copyright.setter + def copyright(self, copyright_: Iterable[Copyright]) -> None: + self._copyright = set(copyright_) + + def __eq__(self, other: object) -> bool: + if isinstance(other, ComponentEvidence): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((tuple(self.licenses), tuple(self.copyright))) + + def __repr__(self) -> str: + return f'' class ComponentScope(Enum): @@ -60,6 +227,417 @@ class ComponentType(Enum): OPERATING_SYSTEM = 'operating-system' +class Diff: + """ + Our internal representation of the `diffType` complex type. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_diffType + """ + + def __init__(self, *, text: Optional[AttachedText] = None, url: Optional[XsUri] = None) -> None: + if not text and not url: + raise NoPropertiesProvidedException( + 'At least one of `text` or `url` must be provided for a `Diff`.' + ) + + self.text = text + self.url = url + + @property + def text(self) -> Optional[AttachedText]: + """ + Specifies the optional text of the diff. + + Returns: + `AttachedText` if set else `None` + """ + return self._text + + @text.setter + def text(self, text: Optional[AttachedText]) -> None: + self._text = text + + @property + def url(self) -> Optional[XsUri]: + """ + Specifies the URL to the diff. + + Returns: + `XsUri` if set else `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, Diff): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.text, self.url)) + + def __repr__(self) -> str: + return f'' + + +class PatchClassification(Enum): + """ + Enum object that defines the permissible `patchClassification`s. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_patchClassification + """ + BACKPORT = 'backport' + CHERRY_PICK = 'cherry-pick' + MONKEY = 'monkey' + UNOFFICIAL = 'unofficial' + + +class Patch: + """ + Our internal representation of the `patchType` complex type. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_patchType + """ + + def __init__(self, *, type_: PatchClassification, diff: Optional[Diff] = None, + resolves: Optional[Iterable[IssueType]] = None) -> None: + self.type = type_ + self.diff = diff + self.resolves = set(resolves or []) + + @property + def type(self) -> PatchClassification: + """ + Specifies the purpose for the patch including the resolution of defects, security issues, or new behavior or + functionality. + + Returns: + `PatchClassification` + """ + return self._type + + @type.setter + def type(self, type_: PatchClassification) -> None: + self._type = type_ + + @property + def diff(self) -> Optional[Diff]: + """ + The patch file (or diff) that show changes. + + .. note:: + Refer to https://en.wikipedia.org/wiki/Diff. + + Returns: + `Diff` if set else `None` + """ + return self._diff + + @diff.setter + def diff(self, diff: Optional[Diff]) -> None: + self._diff = diff + + @property + def resolves(self) -> Set[IssueType]: + """ + Optional list of issues resolved by this patch. + + Returns: + Set of `IssueType` + """ + return self._resolves + + @resolves.setter + def resolves(self, resolves: Iterable[IssueType]) -> None: + self._resolves = set(resolves) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Patch): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.type, self.diff, tuple(self.resolves))) + + def __repr__(self) -> str: + return f'' + + +class Pedigree: + """ + Our internal representation of the `pedigreeType` complex type. + + Component pedigree is a way to document complex supply chain scenarios where components are created, distributed, + modified, redistributed, combined with other components, etc. Pedigree supports viewing this complex chain from the + beginning, the end, or anywhere in the middle. It also provides a way to document variants where the exact relation + may not be known. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_pedigreeType + """ + + def __init__(self, *, ancestors: Optional[Iterable['Component']] = None, + descendants: Optional[Iterable['Component']] = None, variants: Optional[Iterable['Component']] = None, + commits: Optional[Iterable[Commit]] = None, patches: Optional[Iterable[Patch]] = None, + notes: Optional[str] = None) -> None: + if not ancestors and not descendants and not variants and not commits and not patches and not notes: + raise NoPropertiesProvidedException( + 'At least one of `ancestors`, `descendants`, `variants`, `commits`, `patches` or `notes` must be ' + 'provided for `Pedigree`' + ) + + self.ancestors = set(ancestors or []) + self.descendants = set(descendants or []) + self.variants = set(variants or []) + self.commits = set(commits or []) + self.patches = set(patches or []) + self.notes = notes + + @property + def ancestors(self) -> Set['Component']: + """ + Describes zero or more components in which a component is derived from. This is commonly used to describe forks + from existing projects where the forked version contains a ancestor node containing the original component it + was forked from. + + For example, Component A is the original component. Component B is the component being used and documented in + the BOM. However, Component B contains a pedigree node with a single ancestor documenting Component A - the + original component from which Component B is derived from. + + Returns: + Set of `Component` + """ + return self._ancestors + + @ancestors.setter + def ancestors(self, ancestors: Iterable['Component']) -> None: + self._ancestors = set(ancestors) + + @property + def descendants(self) -> Set['Component']: + """ + Descendants are the exact opposite of ancestors. This provides a way to document all forks (and their forks) of + an original or root component. + + Returns: + Set of `Component` + """ + return self._descendants + + @descendants.setter + def descendants(self, descendants: Iterable['Component']) -> None: + self._descendants = set(descendants) + + @property + def variants(self) -> Set['Component']: + """ + Variants describe relations where the relationship between the components are not known. For example, if + Component A contains nearly identical code to Component B. They are both related, but it is unclear if one is + derived from the other, or if they share a common ancestor. + + Returns: + Set of `Component` + """ + return self._variants + + @variants.setter + def variants(self, variants: Iterable['Component']) -> None: + self._variants = set(variants) + + @property + def commits(self) -> Set[Commit]: + """ + A list of zero or more commits which provide a trail describing how the component deviates from an ancestor, + descendant, or variant. + + Returns: + Set of `Commit` + """ + return self._commits + + @commits.setter + def commits(self, commits: Iterable[Commit]) -> None: + self._commits = set(commits) + + @property + def patches(self) -> Set[Patch]: + """ + A list of zero or more patches describing how the component deviates from an ancestor, descendant, or variant. + Patches may be complimentary to commits or may be used in place of commits. + + Returns: + Set of `Patch` + """ + return self._patches + + @patches.setter + def patches(self, patches: Iterable[Patch]) -> None: + self._patches = set(patches) + + @property + def notes(self) -> Optional[str]: + """ + Notes, observations, and other non-structured commentary describing the components pedigree. + + Returns: + `str` if set else `None` + """ + return self._notes + + @notes.setter + def notes(self, notes: Optional[str]) -> None: + self._notes = notes + + def __eq__(self, other: object) -> bool: + if isinstance(other, Pedigree): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash(( + tuple(self.ancestors), tuple(self.descendants), tuple(self.variants), tuple(self.commits), + tuple(self.patches), self.notes + )) + + def __repr__(self) -> str: + return f'' + + +class Swid: + """ + Our internal representation of the `swidType` complex type. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_swidType + """ + + def __init__(self, *, tag_id: str, name: str, version: Optional[str] = None, + tag_version: Optional[int] = None, patch: Optional[bool] = None, + text: Optional[AttachedText] = None, url: Optional[XsUri] = None) -> None: + self.tag_id = tag_id + self.name = name + self.version = version + self.tag_version = tag_version + self.patch = patch + self.text = text + self.url = url + + @property + def tag_id(self) -> str: + """ + Maps to the tagId of a SoftwareIdentity. + + Returns: + `str` + """ + return self._tag_id + + @tag_id.setter + def tag_id(self, tag_id: str) -> None: + self._tag_id = tag_id + + @property + def name(self) -> str: + """ + Maps to the name of a SoftwareIdentity. + + Returns: + `str` + """ + return self._name + + @name.setter + def name(self, name: str) -> None: + self._name = name + + @property + def version(self) -> Optional[str]: + """ + Maps to the version of a SoftwareIdentity. + + Returns: + `str` if set else `None`. + """ + return self._version + + @version.setter + def version(self, version: Optional[str]) -> None: + self._version = version + + @property + def tag_version(self) -> Optional[int]: + """ + Maps to the tagVersion of a SoftwareIdentity. + + Returns: + `int` if set else `None` + """ + return self._tag_version + + @tag_version.setter + def tag_version(self, tag_version: Optional[int]) -> None: + self._tag_version = tag_version + + @property + def patch(self) -> Optional[bool]: + """ + Maps to the patch of a SoftwareIdentity. + + Returns: + `bool` if set else `None` + """ + return self._patch + + @patch.setter + def patch(self, patch: Optional[bool]) -> None: + self._patch = patch + + @property + def text(self) -> Optional[AttachedText]: + """ + Specifies the full content of the SWID tag. + + Returns: + `AttachedText` if set 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 SWID file. + + Returns: + `XsUri` if set else `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, Swid): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.tag_id, self.name, self.version, self.tag_version, self.patch, self.text, self.url)) + + def __repr__(self) -> str: + return f'' + + class Component: """ This is our internal representation of a Component within a Bom. @@ -98,22 +676,23 @@ def for_file(absolute_file_path: str, path_for_bom: Optional[str]) -> 'Component ) ) - def __init__(self, name: str, component_type: ComponentType = ComponentType.LIBRARY, + def __init__(self, *, name: str, component_type: ComponentType = ComponentType.LIBRARY, mime_type: Optional[str] = None, bom_ref: Optional[str] = None, supplier: Optional[OrganizationalEntity] = None, author: Optional[str] = None, publisher: Optional[str] = None, group: Optional[str] = None, version: Optional[str] = None, description: Optional[str] = None, scope: Optional[ComponentScope] = None, - hashes: Optional[List[HashType]] = None, licenses: Optional[List[LicenseChoice]] = None, - copyright: Optional[str] = None, purl: Optional[PackageURL] = None, - external_references: Optional[List[ExternalReference]] = None, - properties: Optional[List[Property]] = None, release_notes: Optional[ReleaseNotes] = None, - cpe: Optional[str] = None, + hashes: Optional[Iterable[HashType]] = None, licenses: Optional[Iterable[LicenseChoice]] = None, + copyright_: Optional[str] = None, purl: Optional[PackageURL] = None, + external_references: Optional[Iterable[ExternalReference]] = None, + properties: Optional[Iterable[Property]] = None, release_notes: Optional[ReleaseNotes] = None, + cpe: Optional[str] = None, swid: Optional[Swid] = None, pedigree: Optional[Pedigree] = None, + components: Optional[Iterable['Component']] = None, evidence: Optional[ComponentEvidence] = None, # Deprecated parameters kept for backwards compatibility namespace: Optional[str] = None, license_str: Optional[str] = None ) -> None: self.type = component_type self.mime_type = mime_type - self.bom_ref = bom_ref or str(uuid4()) + self._bom_ref = BomRef(value=bom_ref) self.supplier = supplier self.author = author self.publisher = publisher @@ -122,13 +701,18 @@ def __init__(self, name: str, component_type: ComponentType = ComponentType.LIBR self.version = version self.description = description self.scope = scope - self.hashes = hashes or [] - self.licenses = licenses or [] - self.copyright = copyright - self.purl = purl + self.hashes = set(hashes or []) + self.licenses = set(licenses or []) + self.copyright = copyright_ self.cpe = cpe - self.external_references = external_references if external_references else [] - self.properties = properties + self.purl = purl + self.swid = swid + self.pedigree = pedigree + self.external_references = set(external_references or []) + self.properties = set(properties or []) + self.components = set(components or []) + self.evidence = evidence + self.release_notes = release_notes # Deprecated for 1.4, but kept for some backwards compatibility if namespace: @@ -145,12 +729,9 @@ def __init__(self, name: str, component_type: ComponentType = ComponentType.LIBR 'standard', DeprecationWarning ) if not licenses: - self.licenses = [LicenseChoice(license_expression=license_str)] + self.licenses = {LicenseChoice(license_expression=license_str)} - # Added for 1.4 - self.release_notes = release_notes - - self.__vulnerabilites: List[Vulnerability] = [] + self.__vulnerabilites: Set[Vulnerability] = set() @property def type(self) -> ComponentType: @@ -185,7 +766,7 @@ def mime_type(self, mime_type: Optional[str]) -> None: self._mime_type = mime_type @property - def bom_ref(self) -> Optional[str]: + def bom_ref(self) -> BomRef: """ An optional identifier which can be used to reference the component elsewhere in the BOM. Every bom-ref MUST be unique within the BOM. @@ -193,14 +774,10 @@ def bom_ref(self) -> Optional[str]: If a value was not provided in the constructor, a UUIDv4 will have been assigned. Returns: - `str` as a unique identifiers for this Component + `BomRef` """ return self._bom_ref - @bom_ref.setter - def bom_ref(self, bom_ref: Optional[str]) -> None: - self._bom_ref = bom_ref - @property def supplier(self) -> Optional[OrganizationalEntity]: """ @@ -328,42 +905,32 @@ def scope(self, scope: Optional[ComponentScope]) -> None: self._scope = scope @property - def hashes(self) -> List[HashType]: + def hashes(self) -> Set[HashType]: """ - Optional list of hashes that help specifiy the integrity of this Component. + Optional list of hashes that help specify the integrity of this Component. Returns: - List of `HashType` or `None` + Set of `HashType` """ return self._hashes @hashes.setter - def hashes(self, hashes: List[HashType]) -> None: - self._hashes = hashes - - def add_hash(self, a_hash: HashType) -> None: - """ - Adds a hash that pins/identifies this Component. - - Args: - a_hash: - `HashType` instance - """ - self.hashes = self.hashes + [a_hash] + def hashes(self, hashes: Iterable[HashType]) -> None: + self._hashes = set(hashes) @property - def licenses(self) -> List[LicenseChoice]: + def licenses(self) -> Set[LicenseChoice]: """ A optional list of statements about how this Component is licensed. Returns: - List of `LicenseChoice` else `None` + Set of `LicenseChoice` """ return self._licenses @licenses.setter - def licenses(self, licenses: List[LicenseChoice]) -> None: - self._licenses = licenses + def licenses(self, licenses: Iterable[LicenseChoice]) -> None: + self._licenses = set(licenses) @property def copyright(self) -> Optional[str]: @@ -377,8 +944,23 @@ def copyright(self) -> Optional[str]: return self._copyright @copyright.setter - def copyright(self, copyright: Optional[str]) -> None: - self._copyright = copyright + def copyright(self, copyright_: Optional[str]) -> None: + self._copyright = copyright_ + + @property + def cpe(self) -> Optional[str]: + """ + Specifies a well-formed CPE name that conforms to the CPE 2.2 or 2.3 specification. + See https://nvd.nist.gov/products/cpe + + Returns: + `str` if set else `None` + """ + return self._cpe + + @cpe.setter + def cpe(self, cpe: Optional[str]) -> None: + self._cpe = cpe @property def purl(self) -> Optional[PackageURL]: @@ -398,59 +980,93 @@ def purl(self, purl: Optional[PackageURL]) -> None: self._purl = purl @property - def cpe(self) -> Optional[str]: + def swid(self) -> Optional[Swid]: """ - Specifies a well-formed CPE name that conforms to the CPE 2.2 or 2.3 specification. - See https://nvd.nist.gov/products/cpe + Specifies metadata and content for ISO-IEC 19770-2 Software Identification (SWID) Tags. Returns: - `str` if set else `None` + `Swid` if set else `None` """ - return self._cpe + return self._swid - @cpe.setter - def cpe(self, cpe: Optional[str]) -> None: - self._cpe = cpe + @swid.setter + def swid(self, swid: Optional[Swid]) -> None: + self._swid = swid @property - def external_references(self) -> List[ExternalReference]: + def pedigree(self) -> Optional[Pedigree]: """ - Provides the ability to document external references related to the component or to the project the component - describes. + Component pedigree is a way to document complex supply chain scenarios where components are created, + distributed, modified, redistributed, combined with other components, etc. Returns: - List of `ExternalReference`s + `Pedigree` if set else `None` """ - return self._external_references + return self._pedigree - @external_references.setter - def external_references(self, external_references: List[ExternalReference]) -> None: - self._external_references = external_references + @pedigree.setter + def pedigree(self, pedigree: Optional[Pedigree]) -> None: + self._pedigree = pedigree - def add_external_reference(self, reference: ExternalReference) -> None: + @property + def external_references(self) -> Set[ExternalReference]: """ - Add an `ExternalReference` to this `Component`. + Provides the ability to document external references related to the component or to the project the component + describes. - Args: - reference: - `ExternalReference` instance to add. + Returns: + Set of `ExternalReference` """ - self.external_references = self._external_references + [reference] + return self._external_references + + @external_references.setter + def external_references(self, external_references: Iterable[ExternalReference]) -> None: + self._external_references = set(external_references) @property - def properties(self) -> Optional[List[Property]]: + def properties(self) -> Set[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: - List of `Property` or `None` + Set of `Property` """ return self._properties @properties.setter - def properties(self, properties: Optional[List[Property]]) -> None: - self._properties = properties + def properties(self, properties: Iterable[Property]) -> None: + self._properties = set(properties) + + @property + def components(self) -> Set['Component']: + """ + A list of software and hardware components included in the parent component. This is not a dependency tree. It + provides a way to specify a hierarchical representation of component assemblies, similar to system -> subsystem + -> parts assembly in physical supply chains. + + Returns: + Set of `Component` + """ + return self._components + + @components.setter + def components(self, components: Iterable['Component']) -> None: + self._components = set(components) + + @property + def evidence(self) -> Optional[ComponentEvidence]: + """ + Provides the ability to document evidence collected through various forms of extraction or analysis. + + Returns: + `ComponentEvidence` if set else `None` + """ + return self._evidence + + @evidence.setter + def evidence(self, evidence: Optional[ComponentEvidence]) -> None: + self._evidence = evidence @property def release_notes(self) -> Optional[ReleaseNotes]: @@ -477,14 +1093,14 @@ def add_vulnerability(self, vulnerability: Vulnerability) -> None: Returns: None """ - self.__vulnerabilites.append(vulnerability) + self.__vulnerabilites.add(vulnerability) - def get_vulnerabilities(self) -> List[Vulnerability]: + def get_vulnerabilities(self) -> Set[Vulnerability]: """ Get all the Vulnerabilities for this Component. Returns: - List of `Vulnerability` objects assigned to this Component. + Set of `Vulnerability` """ return self.__vulnerabilites @@ -510,9 +1126,10 @@ def __eq__(self, other: object) -> bool: def __hash__(self) -> int: return hash(( - self.author, self.copyright, self.description, str(self.external_references), self.group, - str(self.hashes), str(self.licenses), self.mime_type, self.name, self.properties, self.publisher, self.purl, - self.release_notes, self.scope, self.supplier, self.type, self.version, self.cpe + self.type, self.mime_type, self.supplier, self.author, self.publisher, self.group, self.name, + self.version, self.description, self.scope, tuple(self.hashes), tuple(self.licenses), self.copyright, + self.cpe, self.purl, self.swid, self.pedigree, tuple(self.external_references), tuple(self.properties), + tuple(self.components), self.evidence, self.release_notes )) def __repr__(self) -> str: diff --git a/cyclonedx/model/issue.py b/cyclonedx/model/issue.py index befd21d7..04ea4f6b 100644 --- a/cyclonedx/model/issue.py +++ b/cyclonedx/model/issue.py @@ -13,9 +13,9 @@ # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 - +# Copyright (c) OWASP Foundation. All Rights Reserved. from enum import Enum -from typing import List, Optional +from typing import Iterable, Optional, Set from . import XsUri from ..exception.model import NoPropertiesProvidedException @@ -26,7 +26,7 @@ class IssueClassification(Enum): This is out internal representation of the enum `issueClassification`. .. note:: - See the CycloneDX Schema definition: hhttps://cyclonedx.org/docs/1.4/xml/#type_issueClassification + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_issueClassification """ DEFECT = 'defect' ENHANCEMENT = 'enhancement' @@ -42,7 +42,7 @@ class IssueTypeSource: See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_issueType """ - def __init__(self, name: Optional[str] = None, url: Optional[XsUri] = None) -> None: + def __init__(self, *, name: Optional[str] = None, url: Optional[XsUri] = None) -> None: if not name and not url: raise NoPropertiesProvidedException( 'Neither `name` nor `url` were provided - at least one must be provided.' @@ -78,6 +78,17 @@ def url(self) -> Optional[XsUri]: def url(self, url: Optional[XsUri]) -> None: self._url = url + def __eq__(self, other: object) -> bool: + if isinstance(other, IssueTypeSource): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.name, self.url)) + + def __repr__(self) -> str: + return f'' + class IssueType: """ @@ -88,178 +99,109 @@ class IssueType: See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_issueType """ - def __init__(self, classification: IssueClassification, id: Optional[str] = None, name: Optional[str] = None, - description: Optional[str] = None, source_name: Optional[str] = None, - source_url: Optional[XsUri] = None, references: Optional[List[XsUri]] = None) -> None: - self._type: IssueClassification = classification - self._id: Optional[str] = id - self._name: Optional[str] = name - self._description: Optional[str] = description - self._source: Optional[IssueTypeSource] = None - self._references: List[XsUri] = references or [] - if source_name or source_url: - self._source = IssueTypeSource( - name=source_name, url=source_url - ) + def __init__(self, *, classification: IssueClassification, id_: Optional[str] = None, name: Optional[str] = None, + description: Optional[str] = None, source: Optional[IssueTypeSource] = None, + references: Optional[Iterable[XsUri]] = None) -> None: + self.type = classification + self.id = id_ + self.name = name + self.description = description + self.source = source + self.references = set(references or []) @property - def source(self) -> Optional[IssueTypeSource]: - return self._source - - @source.setter - def source(self, source: IssueTypeSource) -> None: - self._source = source - - def add_reference(self, reference: XsUri) -> None: - """ - Add a reference URL to this Issue. - - Args: - reference: - `XsUri` Reference URL to add + def type(self) -> IssueClassification: """ - self._references.append(reference) - - def get_classification(self) -> IssueClassification: - """ - Get the classification of this IssueType. + Specifies the type of issue. Returns: - `IssueClassification` that represents the classification of this `IssueType`. + `IssueClassification` """ return self._type - def get_id(self) -> Optional[str]: + @type.setter + def type(self, classification: IssueClassification) -> None: + self._type = classification + + @property + def id(self) -> Optional[str]: """ - Get the ID of this IssueType. + The identifier of the issue assigned by the source of the issue. Returns: - `str` that represents the ID of this `IssueType` if set else `None`. + `str` if set else `None` """ return self._id - def get_name(self) -> Optional[str]: - """ - Get the name of this IssueType. + @id.setter + def id(self, id_: Optional[str]) -> None: + self._id = id_ - Returns: - `str` that represents the name of this `IssueType` if set else `None`. - """ - return self._name - - def get_description(self) -> Optional[str]: + @property + def name(self) -> Optional[str]: """ - Get the description of this IssueType. + The name of the issue. Returns: - `str` that represents the description of this `IssueType` if set else `None`. - """ - return self._description - - def get_source_name(self) -> Optional[str]: + `str` if set else `None` """ - Get the source_name of this IssueType. - - For example, this might be "NVD" or "National Vulnerability Database". + return self._name - Returns: - `str` that represents the source_name of this `IssueType` if set else `None`. - """ - if self._source: - return self._source.name - return None + @name.setter + def name(self, name: Optional[str]) -> None: + self._name = name - def get_source_url(self) -> Optional[XsUri]: + @property + def description(self) -> Optional[str]: """ - Get the source_url of this IssueType. - - For example, this would likely be a URL to the issue on the NVD. + A description of the issue. Returns: - `XsUri` that represents the source_url of this `IssueType` if set else `None`. - """ - if self._source: - return self._source.url - return None - - def get_references(self) -> List[XsUri]: + `str` if set else `None` """ - Get any references for this IssueType. - - References are an arbitrary list of URIs that relate to this issue. + return self._description - Returns: - List of `XsUri` objects. - """ - return self._references + @description.setter + def description(self, description: Optional[str]) -> None: + self._description = description - def set_id(self, id: str) -> None: + @property + def source(self) -> Optional[IssueTypeSource]: """ - Set the ID of this Issue. - - Args: - id: - `str` the Issue ID + The source of this issue. Returns: - None - """ - self._id = id - - def set_name(self, name: str) -> None: + `IssueTypeSource` if set else `None` """ - Set the name of this Issue. - - Args: - name: - `str` the name of this Issue + return self._source - Returns: - None - """ - self._name = name + @source.setter + def source(self, source: Optional[IssueTypeSource]) -> None: + self._source = source - def set_description(self, description: str) -> None: + @property + def references(self) -> Set[XsUri]: """ - Set the description of this Issue. - - Args: - description: - `str` the description of this Issue + Any reference URLs related to this issue. Returns: - None - """ - self._description = description - - def set_source_name(self, source_name: str) -> None: + Set of `XsUri` """ - Set the name of the source of this Issue. + return self._references - Args: - source_name: - `str` For example, this might be "NVD" or "National Vulnerability Database" + @references.setter + def references(self, references: Iterable[XsUri]) -> None: + self._references = set(references) - Returns: - None - """ - if self._source: - self._source.name = source_name - else: - self._source = IssueTypeSource(name=source_name) + def __eq__(self, other: object) -> bool: + if isinstance(other, IssueType): + return hash(other) == hash(self) + return False - def set_source_url(self, source_url: XsUri) -> None: - """ - Set the URL for the source of this Issue. - - Args: - source_url: - `XsUri` For example, this would likely be a URL to the issue on the NVD + def __hash__(self) -> int: + return hash(( + self.type, self.id, self.name, self.description, self.source, tuple(self.references) + )) - Returns: - None - """ - if self._source: - self._source.url = source_url - else: - self._source = IssueTypeSource(url=source_url) + def __repr__(self) -> str: + return f'' diff --git a/cyclonedx/model/release_note.py b/cyclonedx/model/release_note.py index 3a3097a8..a17d3a69 100644 --- a/cyclonedx/model/release_note.py +++ b/cyclonedx/model/release_note.py @@ -16,9 +16,8 @@ # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. - from datetime import datetime -from typing import List, Optional +from typing import Iterable, Optional, Set from ..model import Note, Property, XsUri from ..model.issue import IssueType @@ -32,22 +31,22 @@ class ReleaseNotes: See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/#type_releaseNotesType """ - def __init__(self, type: str, title: Optional[str] = None, featured_image: Optional[XsUri] = None, + def __init__(self, *, type_: str, title: Optional[str] = None, featured_image: Optional[XsUri] = None, social_image: Optional[XsUri] = None, description: Optional[str] = None, - timestamp: Optional[datetime] = None, aliases: Optional[List[str]] = None, - tags: Optional[List[str]] = None, resolves: Optional[List[IssueType]] = None, - notes: Optional[List[Note]] = None, properties: Optional[List[Property]] = None) -> None: - self.type = type + timestamp: Optional[datetime] = None, aliases: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, resolves: Optional[Iterable[IssueType]] = None, + notes: Optional[Iterable[Note]] = None, properties: Optional[Iterable[Property]] = None) -> None: + self.type = type_ self.title = title self.featured_image = featured_image self.social_image = social_image self.description = description self.timestamp = timestamp - self.aliases = aliases - self.tags = tags - self.resolves = resolves - self.notes = notes - self._properties: Optional[List[Property]] = properties or None + self.aliases = set(aliases or []) + self.tags = set(tags or []) + self.resolves = set(resolves or []) + self.notes = set(notes or []) + self.properties = set(properties or []) @property def type(self) -> str: @@ -71,8 +70,8 @@ def type(self) -> str: return self._type @type.setter - def type(self, type: str) -> None: - self._type = type + def type(self, type_: str) -> None: + self._type = type_ @property def title(self) -> Optional[str]: @@ -130,111 +129,89 @@ def timestamp(self, timestamp: Optional[datetime]) -> None: self._timestamp = timestamp @property - def aliases(self) -> Optional[List[str]]: + def aliases(self) -> Set[str]: """ One or more alternate names the release may be referred to. This may include unofficial terms used by development and marketing teams (e.g. code names). + + Returns: + Set of `str` """ return self._aliases @aliases.setter - def aliases(self, aliases: Optional[List[str]]) -> None: - if not aliases: - aliases = None - self._aliases = aliases - - def add_alias(self, alias: str) -> None: - """ - Adds an alias to this Release. - - Args: - alias: - `str` alias - """ - self.aliases = (self.aliases or []) + [alias] + def aliases(self, aliases: Iterable[str]) -> None: + self._aliases = set(aliases) @property - def tags(self) -> Optional[List[str]]: + def tags(self) -> Set[str]: """ One or more tags that may aid in search or retrieval of the release note. + + Returns: + Set of `str` """ return self._tags @tags.setter - def tags(self, tags: Optional[List[str]]) -> None: - if not tags: - tags = None - self._tags = tags - - def add_tag(self, tag: str) -> None: - """ - Add a tag to this Release. - - Args: - tag: - `str` tag to add - """ - self.tags = (self.tags or []) + [tag] + def tags(self, tags: Iterable[str]) -> None: + self._tags = set(tags) @property - def resolves(self) -> Optional[List[IssueType]]: + def resolves(self) -> Set[IssueType]: """ A collection of issues that have been resolved. + + Returns: + Set of `IssueType` """ return self._resolves @resolves.setter - def resolves(self, resolves: Optional[List[IssueType]]) -> None: - if not resolves: - resolves = None - self._resolves = resolves - - def add_resolves(self, issue: IssueType) -> None: - """ - Adds an issue that this Release resolves. - - Args: - issue: - `IssueType` object that is resolved by this Release - """ - self.resolves = (self.resolves or []) + [issue] + def resolves(self, resolves: Iterable[IssueType]) -> None: + self._resolves = set(resolves) @property - def notes(self) -> Optional[List[Note]]: + def notes(self) -> Set[Note]: """ Zero or more release notes containing the locale and content. Multiple note elements may be specified to support release notes in a wide variety of languages. + + Returns: + Set of `Note` """ return self._notes @notes.setter - def notes(self, notes: Optional[List[Note]]) -> None: - if not notes: - notes = None - self._notes = notes - - def add_note(self, note: Note) -> None: - """ - Adds a release note to this Release. - - Args: - note: - `Note` to be added - """ - self.notes = (self.notes or []) + [note] + def notes(self, notes: Iterable[Note]) -> None: + self._notes = set(notes) @property - def properties(self) -> Optional[List[Property]]: + def properties(self) -> Set[Property]: """ Provides the ability to document properties in a name-value store. This provides flexibility to include data not officially supported in the standard without having to use additional namespaces or create extensions. Unlike key-value stores, properties support duplicate names, each potentially having different values. Returns: - List of `Property` or `None` + Set of `Property` """ return self._properties @properties.setter - def properties(self, properties: Optional[List[Property]]) -> None: - self._properties = properties + def properties(self, properties: Iterable[Property]) -> None: + self._properties = set(properties) + + def __eq__(self, other: object) -> bool: + if isinstance(other, ReleaseNotes): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash(( + self.type, self.title, self.featured_image, self.social_image, self.description, self.timestamp, + tuple(self.aliases), tuple(self.tags), tuple(self.resolves), tuple(self.notes), tuple(self.properties) + )) + + def __repr__(self) -> str: + return f'' diff --git a/cyclonedx/model/service.py b/cyclonedx/model/service.py new file mode 100644 index 00000000..5f387973 --- /dev/null +++ b/cyclonedx/model/service.py @@ -0,0 +1,299 @@ +# 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. +from typing import Iterable, Optional, Set + +from . import ExternalReference, DataClassification, LicenseChoice, OrganizationalEntity, Property, XsUri +from .bom_ref import BomRef +from .release_note import ReleaseNotes + +""" +This set of classes represents the data that is possible about known Services. + +.. note:: + See the CycloneDX Schema extension definition https://cyclonedx.org/docs/1.4/xml/#type_servicesType +""" + + +class Service: + """ + Class that models the `service` complex type in the CycloneDX schema. + + .. note:: + See the CycloneDX schema: https://cyclonedx.org/docs/1.4/xml/#type_service + """ + + def __init__(self, *, name: str, bom_ref: Optional[str] = None, provider: Optional[OrganizationalEntity] = None, + group: Optional[str] = None, version: Optional[str] = None, description: Optional[str] = None, + endpoints: Optional[Iterable[XsUri]] = None, authenticated: Optional[bool] = None, + x_trust_boundary: Optional[bool] = None, data: Optional[Iterable[DataClassification]] = None, + licenses: Optional[Iterable[LicenseChoice]] = None, + external_references: Optional[Iterable[ExternalReference]] = None, + properties: Optional[Iterable[Property]] = None, + services: Optional[Iterable['Service']] = None, + release_notes: Optional[ReleaseNotes] = None, + ) -> None: + self._bom_ref = BomRef(value=bom_ref) + self.provider = provider + self.group = group + self.name = name + self.version = version + self.description = description + self.endpoints = set(endpoints or []) + self.authenticated = authenticated + self.x_trust_boundary = x_trust_boundary + self.data = set(data or []) + self.licenses = set(licenses or []) + self.external_references = set(external_references or []) + self.services = set(services or []) + self.release_notes = release_notes + self.properties = set(properties or []) + + @property + def bom_ref(self) -> BomRef: + """ + An optional identifier which can be used to reference the service elsewhere in the BOM. Uniqueness is enforced + within all elements and children of the root-level bom element. + + If a value was not provided in the constructor, a UUIDv4 will have been assigned. + + Returns: + `BomRef` unique identifier for this Service + """ + return self._bom_ref + + @property + def provider(self) -> Optional[OrganizationalEntity]: + """ + Get the The organization that provides the service. + + Returns: + `OrganizationalEntity` if set else `None` + """ + return self._provider + + @provider.setter + def provider(self, provider: Optional[OrganizationalEntity]) -> None: + self._provider = provider + + @property + def group(self) -> Optional[str]: + """ + The grouping name, namespace, or identifier. This will often be a shortened, single name of the company or + project that produced the service or domain name. Whitespace and special characters should be avoided. + + Returns: + `str` if provided else `None` + """ + return self._group + + @group.setter + def group(self, group: Optional[str]) -> None: + self._group = group + + @property + def name(self) -> str: + """ + The name of the service. This will often be a shortened, single name of the service. + + Returns: + `str` + """ + return self._name + + @name.setter + def name(self, name: str) -> None: + self._name = name + + @property + def version(self) -> Optional[str]: + """ + The service version. + + Returns: + `str` if set else `None` + """ + return self._version + + @version.setter + def version(self, version: Optional[str]) -> None: + self._version = version + + @property + def description(self) -> Optional[str]: + """ + Specifies a description for the service. + + Returns: + `str` if set else `None` + """ + return self._description + + @description.setter + def description(self, description: Optional[str]) -> None: + self._description = description + + @property + def endpoints(self) -> Set[XsUri]: + """ + A list of endpoints URI's this service provides. + + Returns: + Set of `XsUri` + """ + return self._endpoints + + @endpoints.setter + def endpoints(self, endpoints: Iterable[XsUri]) -> None: + self._endpoints = set(endpoints) + + @property + def authenticated(self) -> Optional[bool]: + """ + A boolean value indicating if the service requires authentication. A value of true indicates the service + requires authentication prior to use. + + A value of false indicates the service does not require authentication. + + Returns: + `bool` if set else `None` + """ + return self._authenticated + + @authenticated.setter + def authenticated(self, authenticated: Optional[bool]) -> None: + self._authenticated = authenticated + + @property + def x_trust_boundary(self) -> Optional[bool]: + """ + A boolean value indicating if use of the service crosses a trust zone or boundary. A value of true indicates + that by using the service, a trust boundary is crossed. + + A value of false indicates that by using the service, a trust boundary is not crossed. + + Returns: + `bool` if set else `None` + """ + return self._x_trust_boundary + + @x_trust_boundary.setter + def x_trust_boundary(self, x_trust_boundary: Optional[bool]) -> None: + self._x_trust_boundary = x_trust_boundary + + @property + def data(self) -> Set[DataClassification]: + """ + Specifies the data classification. + + Returns: + Set of `DataClassification` + """ + return self._data + + @data.setter + def data(self, data: Iterable[DataClassification]) -> None: + self._data = set(data) + + @property + def licenses(self) -> Set[LicenseChoice]: + """ + A optional list of statements about how this Service is licensed. + + Returns: + Set of `LicenseChoice` + """ + return self._licenses + + @licenses.setter + def licenses(self, licenses: Iterable[LicenseChoice]) -> None: + self._licenses = set(licenses) + + @property + def external_references(self) -> Set[ExternalReference]: + """ + Provides the ability to document external references related to the Service. + + Returns: + Set of `ExternalReference` + """ + return self._external_references + + @external_references.setter + def external_references(self, external_references: Iterable[ExternalReference]) -> None: + self._external_references = set(external_references) + + @property + def services(self) -> Set['Service']: + """ + A list of services included or deployed behind the parent service. + + This is not a dependency tree. + + It provides a way to specify a hierarchical representation of service assemblies. + + Returns: + Set of `Service` + """ + return self._services + + @services.setter + def services(self, services: Iterable['Service']) -> None: + self._services = set(services) + + @property + def release_notes(self) -> Optional[ReleaseNotes]: + """ + Specifies optional release notes. + + Returns: + `ReleaseNotes` or `None` + """ + return self._release_notes + + @release_notes.setter + def release_notes(self, release_notes: Optional[ReleaseNotes]) -> None: + self._release_notes = release_notes + + @property + def properties(self) -> Set[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 = set(properties) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Service): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash(( + self.authenticated, tuple(self.data), self.description, tuple(self.endpoints), + tuple(self.external_references), self.group, tuple(self.licenses), self.name, tuple(self.properties), + self.provider, self.release_notes, tuple(self.services), self.version, self.x_trust_boundary + )) + + def __repr__(self) -> str: + return f'' diff --git a/cyclonedx/model/vulnerability.py b/cyclonedx/model/vulnerability.py index 1a7a9720..96e64c6c 100644 --- a/cyclonedx/model/vulnerability.py +++ b/cyclonedx/model/vulnerability.py @@ -16,17 +16,15 @@ # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. - import re import warnings from datetime import datetime from decimal import Decimal from enum import Enum -from typing import List, Optional, Tuple, Union -from urllib.parse import ParseResult, urlparse -from uuid import uuid4 +from typing import Iterable, Optional, Set, Tuple, Union from . import OrganizationalContact, OrganizationalEntity, Tool, XsUri +from .bom_ref import BomRef from .impact_analysis import ImpactAnalysisAffectedStatus, ImpactAnalysisJustification, ImpactAnalysisResponse, \ ImpactAnalysisState from ..exception.model import MutuallyExclusivePropertiesException, NoPropertiesProvidedException @@ -54,7 +52,7 @@ class BomTargetVersionRange: See the CycloneDX schema: https://cyclonedx.org/docs/1.4/#type_vulnerabilityType """ - def __init__(self, version: Optional[str] = None, version_range: Optional[str] = None, + def __init__(self, *, version: Optional[str] = None, version_range: Optional[str] = None, status: Optional[ImpactAnalysisAffectedStatus] = None) -> None: if not version and not version_range: raise NoPropertiesProvidedException( @@ -106,6 +104,17 @@ def status(self) -> Optional[ImpactAnalysisAffectedStatus]: def status(self, status: Optional[ImpactAnalysisAffectedStatus]) -> None: self._status = status + def __eq__(self, other: object) -> bool: + if isinstance(other, BomTargetVersionRange): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.version, self.range, self.status)) + + def __repr__(self) -> str: + return f'' + class BomTarget: """ @@ -120,9 +129,9 @@ class BomTarget: See the CycloneDX schema: https://cyclonedx.org/docs/1.4/#type_vulnerabilityType """ - def __init__(self, ref: str, versions: Optional[List[BomTargetVersionRange]] = None) -> None: + def __init__(self, *, ref: str, versions: Optional[Iterable[BomTargetVersionRange]] = None) -> None: self.ref = ref - self.versions = versions + self.versions = set(versions or []) @property def ref(self) -> str: @@ -136,15 +145,29 @@ def ref(self, ref: str) -> None: self._ref = ref @property - def versions(self) -> Optional[List[BomTargetVersionRange]]: + def versions(self) -> Set[BomTargetVersionRange]: """ Zero or more individual versions or range of versions. + + Returns: + Set of `BomTargetVersionRange` """ return self._versions @versions.setter - def versions(self, versions: Optional[List[BomTargetVersionRange]]) -> None: - self._versions = versions + def versions(self, versions: Iterable[BomTargetVersionRange]) -> None: + self._versions = set(versions) + + def __eq__(self, other: object) -> bool: + if isinstance(other, BomTarget): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.ref, tuple(self.versions))) + + def __repr__(self) -> str: + return f'' class VulnerabilityAnalysis: @@ -155,9 +178,9 @@ class VulnerabilityAnalysis: See the CycloneDX schema: https://cyclonedx.org/docs/1.4/#type_vulnerabilityType """ - def __init__(self, state: Optional[ImpactAnalysisState] = None, + def __init__(self, *, state: Optional[ImpactAnalysisState] = None, justification: Optional[ImpactAnalysisJustification] = None, - responses: Optional[List[ImpactAnalysisResponse]] = None, + responses: Optional[Iterable[ImpactAnalysisResponse]] = None, detail: Optional[str] = None) -> None: if not state and not justification and not responses and not detail: raise NoPropertiesProvidedException( @@ -166,13 +189,16 @@ def __init__(self, state: Optional[ImpactAnalysisState] = None, ) self.state = state self.justification = justification - self.response = responses + self.response = set(responses or []) self.detail = detail @property def state(self) -> Optional[ImpactAnalysisState]: """ The declared current state of an occurrence of a vulnerability, after automated or manual analysis. + + Returns: + `ImpactAnalysisState` if set else `None` """ return self._state @@ -184,6 +210,9 @@ def state(self, state: Optional[ImpactAnalysisState]) -> None: def justification(self) -> Optional[ImpactAnalysisJustification]: """ The rationale of why the impact analysis state was asserted. + + Returns: + `ImpactAnalysisJustification` if set else `None` """ return self._justification @@ -192,17 +221,20 @@ def justification(self, justification: Optional[ImpactAnalysisJustification]) -> self._justification = justification @property - def response(self) -> Optional[List[ImpactAnalysisResponse]]: + def response(self) -> Set[ImpactAnalysisResponse]: """ A list of responses to the vulnerability by the manufacturer, supplier, or project responsible for the affected component or service. More than one response is allowed. Responses are strongly encouraged for vulnerabilities where the analysis state is exploitable. + + Returns: + Set of `ImpactAnalysisResponse` """ return self._response @response.setter - def response(self, responses: Optional[List[ImpactAnalysisResponse]]) -> None: - self._response = responses + def response(self, responses: Iterable[ImpactAnalysisResponse]) -> None: + self._response = set(responses) @property def detail(self) -> Optional[str]: @@ -210,7 +242,9 @@ def detail(self) -> Optional[str]: A detailed description of the impact including methods used during assessment. If a vulnerability is not exploitable, this field should include specific details on why the component or service is not impacted by this vulnerability. - :return: + + Returns: + `str` if set else `None` """ return self._detail @@ -218,6 +252,17 @@ def detail(self) -> Optional[str]: def detail(self, detail: Optional[str]) -> None: self._detail = detail + def __eq__(self, other: object) -> bool: + if isinstance(other, VulnerabilityAnalysis): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.state, self.justification, tuple(self.response), self.detail)) + + def __repr__(self) -> str: + return f'' + class VulnerabilityAdvisory: """ @@ -227,7 +272,7 @@ class VulnerabilityAdvisory: See the CycloneDX schema: https://cyclonedx.org/docs/1.4/#type_advisoryType """ - def __init__(self, url: XsUri, title: Optional[str] = None) -> None: + def __init__(self, *, url: XsUri, title: Optional[str] = None) -> None: self.title = title self.url = url @@ -253,6 +298,17 @@ def url(self) -> XsUri: def url(self, url: XsUri) -> None: self._url = url + def __eq__(self, other: object) -> bool: + if isinstance(other, VulnerabilityAdvisory): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.title, self.url)) + + def __repr__(self) -> str: + return f'' + class VulnerabilitySource: """ @@ -264,7 +320,7 @@ class VulnerabilitySource: See the CycloneDX schema: https://cyclonedx.org/docs/1.4/#type_vulnerabilitySourceType """ - def __init__(self, name: Optional[str] = None, url: Optional[XsUri] = None) -> None: + def __init__(self, *, name: Optional[str] = None, url: Optional[XsUri] = None) -> None: if not name and not url: raise NoPropertiesProvidedException( 'Either name or url must be provided for a VulnerabilitySource - neither provided' @@ -294,6 +350,17 @@ def url(self) -> Optional[XsUri]: def url(self, url: Optional[XsUri]) -> None: self._url = url + def __eq__(self, other: object) -> bool: + if isinstance(other, VulnerabilitySource): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.name, self.url)) + + def __repr__(self) -> str: + return f'' + class VulnerabilityReference: """ @@ -308,7 +375,7 @@ class VulnerabilityReference: See the CycloneDX schema: https://cyclonedx.org/docs/1.4/#type_vulnerabilityType """ - def __init__(self, id: Optional[str] = None, source: Optional[VulnerabilitySource] = None) -> None: + def __init__(self, *, id: Optional[str] = None, source: Optional[VulnerabilitySource] = None) -> None: if not id and not source: raise NoPropertiesProvidedException( 'Either id or source must be provided for a VulnerabilityReference - neither provided' @@ -338,6 +405,17 @@ def source(self) -> Optional[VulnerabilitySource]: def source(self, source: Optional[VulnerabilitySource]) -> None: self._source = source + def __eq__(self, other: object) -> bool: + if isinstance(other, VulnerabilityReference): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.id, self.source)) + + def __repr__(self) -> str: + return f'' + class VulnerabilityScoreSource(Enum): """ @@ -476,7 +554,7 @@ class VulnerabilityRating: they are redundant if you have the vector (the vector allows you to calculate the scores). """ - def __init__(self, source: Optional[VulnerabilitySource] = None, score: Optional[Decimal] = None, + def __init__(self, *, source: Optional[VulnerabilitySource] = None, score: Optional[Decimal] = None, severity: Optional[VulnerabilitySeverity] = None, method: Optional[VulnerabilityScoreSource] = None, vector: Optional[str] = None, justification: Optional[str] = None, @@ -571,6 +649,17 @@ def justification(self) -> Optional[str]: def justification(self, justification: Optional[str]) -> None: self._justification = justification + def __eq__(self, other: object) -> bool: + if isinstance(other, VulnerabilityRating): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((self.source, self.score, self.severity, self.method, self.vector, self.justification)) + + def __repr__(self) -> str: + return f'' + class VulnerabilityCredits: """ @@ -583,42 +672,53 @@ class VulnerabilityCredits: See the CycloneDX schema: https://cyclonedx.org/docs/1.4/#type_vulnerabilityType """ - def __init__(self, organizations: Optional[List[OrganizationalEntity]] = None, - individuals: Optional[List[OrganizationalContact]] = None) -> None: + def __init__(self, *, organizations: Optional[Iterable[OrganizationalEntity]] = None, + individuals: Optional[Iterable[OrganizationalContact]] = None) -> None: if not organizations and not individuals: raise NoPropertiesProvidedException( 'One of `organizations` or `individuals` must be populated - neither were' ) - self.organizations = organizations - self.individuals = individuals + self.organizations = set(organizations or []) + self.individuals = set(individuals or []) @property - def organizations(self) -> Optional[List[OrganizationalEntity]]: + def organizations(self) -> Set[OrganizationalEntity]: """ The organizations credited with vulnerability discovery. Returns: - List of `OrganizationalEntity` or `None` + Set of `OrganizationalEntity` """ return self._organizations @organizations.setter - def organizations(self, organizations: Optional[List[OrganizationalEntity]]) -> None: - self._organizations = organizations + def organizations(self, organizations: Iterable[OrganizationalEntity]) -> None: + self._organizations = set(organizations) @property - def individuals(self) -> Optional[List[OrganizationalContact]]: + def individuals(self) -> Set[OrganizationalContact]: """ The individuals, not associated with organizations, that are credited with vulnerability discovery. Returns: - List of `OrganizationalContact` or `None` + Set of `OrganizationalContact` """ return self._individuals @individuals.setter - def individuals(self, individuals: Optional[List[OrganizationalContact]]) -> None: - self._individuals = individuals + def individuals(self, individuals: Iterable[OrganizationalContact]) -> None: + self._individuals = set(individuals) + + def __eq__(self, other: object) -> bool: + if isinstance(other, VulnerabilityCredits): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash((tuple(self.organizations), tuple(self.individuals))) + + def __repr__(self) -> str: + return f'' class Vulnerability: @@ -632,36 +732,36 @@ class Vulnerability: See the CycloneDX schema: https://cyclonedx.org/docs/1.4/#type_vulnerabilityType """ - def __init__(self, bom_ref: Optional[str] = None, id: Optional[str] = None, + def __init__(self, *, bom_ref: Optional[str] = None, id: Optional[str] = None, source: Optional[VulnerabilitySource] = None, - references: Optional[List[VulnerabilityReference]] = None, - ratings: Optional[List[VulnerabilityRating]] = None, cwes: Optional[List[int]] = None, + references: Optional[Iterable[VulnerabilityReference]] = None, + ratings: Optional[Iterable[VulnerabilityRating]] = None, cwes: Optional[Iterable[int]] = None, description: Optional[str] = None, detail: Optional[str] = None, recommendation: Optional[str] = None, - advisories: Optional[List[VulnerabilityAdvisory]] = None, created: Optional[datetime] = None, + advisories: Optional[Iterable[VulnerabilityAdvisory]] = None, created: Optional[datetime] = None, published: Optional[datetime] = None, updated: Optional[datetime] = None, credits: Optional[VulnerabilityCredits] = None, - tools: Optional[List[Tool]] = None, analysis: Optional[VulnerabilityAnalysis] = None, - affects_targets: Optional[List[BomTarget]] = None, + tools: Optional[Iterable[Tool]] = None, analysis: Optional[VulnerabilityAnalysis] = None, + affects_targets: Optional[Iterable[BomTarget]] = None, # Deprecated Parameters kept for backwards compatibility source_name: Optional[str] = None, source_url: Optional[str] = None, - recommendations: Optional[List[str]] = None) -> None: - self.bom_ref = bom_ref or str(uuid4()) + recommendations: Optional[Iterable[str]] = None) -> None: + self._bom_ref = BomRef(value=bom_ref) self.id = id self.source = source - self.references = references or [] - self.ratings = ratings or [] - self.cwes = cwes or [] + self.references = set(references or []) + self.ratings = set(ratings or []) + self.cwes = set(cwes or []) self.description = description self.detail = detail self.recommendation = recommendation - self.advisories = advisories or [] + self.advisories = set(advisories or []) self.created = created self.published = published self.updated = updated self.credits = credits - self.tools = tools or [] + self.tools = set(tools or []) self.analysis = analysis - self.affects = affects_targets or [] + self.affects = set(affects_targets or []) if source_name or source_url: warnings.warn('`source_name` and `source_url` are deprecated - use `source`', DeprecationWarning) @@ -671,28 +771,27 @@ def __init__(self, bom_ref: Optional[str] = None, id: Optional[str] = None, if recommendations: warnings.warn('`recommendations` is deprecated - use `recommendation`', DeprecationWarning) if not recommendation: - self.recommendation = recommendations.pop() + self.recommendation = next(iter(recommendations)) @property - def bom_ref(self) -> Optional[str]: + def bom_ref(self) -> BomRef: """ Get the unique reference for this Vulnerability in this BOM. If a value was not provided in the constructor, a UUIDv4 will have been assigned. Returns: - `str` unique identifier for this Vulnerability + `BomRef` """ return self._bom_ref - @bom_ref.setter - def bom_ref(self, bom_ref: Optional[str]) -> None: - self._bom_ref = bom_ref - @property def id(self) -> Optional[str]: """ The identifier that uniquely identifies the vulnerability. For example: CVE-2021-39182. + + Returns: + `str` if set else `None` """ return self._id @@ -704,6 +803,9 @@ def id(self, id: Optional[str]) -> None: def source(self) -> Optional[VulnerabilitySource]: """ The source that published the vulnerability. + + Returns: + `VulnerabilitySource` if set else `None` """ return self._source @@ -712,83 +814,60 @@ def source(self, source: Optional[VulnerabilitySource]) -> None: self._source = source @property - def references(self) -> List[VulnerabilityReference]: + def references(self) -> Set[VulnerabilityReference]: """ Zero or more pointers to vulnerabilities that are the equivalent of the vulnerability specified. Often times, the same vulnerability may exist in multiple sources of vulnerability intelligence, but have different identifiers. References provides a way to correlate vulnerabilities across multiple sources of vulnerability intelligence. + + Returns: + Set of `VulnerabilityReference` """ return self._references @references.setter - def references(self, references: List[VulnerabilityReference]) -> None: - self._references = references - - def add_reference(self, reference: VulnerabilityReference) -> None: - """ - Add an additional reference for this Vulnerability. - - Vulnerabilities may benefit from pointers to vulnerabilities that are the equivalent of the vulnerability - specified. Often times, the same vulnerability may exist in multiple sources of vulnerability intelligence, but - have different identifiers. These references provide a way to correlate vulnerabilities across multiple sources - of vulnerability intelligence. - - Args: - reference: - `VulnerabilityReference` reference to add - """ - self.references = self.references + [reference] + def references(self, references: Iterable[VulnerabilityReference]) -> None: + self._references = set(references) @property - def ratings(self) -> List[VulnerabilityRating]: + def ratings(self) -> Set[VulnerabilityRating]: """ List of vulnerability ratings. + + Returns: + Set of `VulnerabilityRating` """ return self._ratings @ratings.setter - def ratings(self, ratings: List[VulnerabilityRating]) -> None: - self._ratings = ratings - - def add_rating(self, rating: VulnerabilityRating) -> None: - """ - Add a vulnerability rating. - - Args: - rating: - `VulnerabilityRating` - """ - self.ratings = self.ratings + [rating] + def ratings(self, ratings: Iterable[VulnerabilityRating]) -> None: + self._ratings = set(ratings) @property - def cwes(self) -> List[int]: + def cwes(self) -> Set[int]: """ A list of CWE (Common Weakness Enumeration) identifiers. .. note:: See https://cwe.mitre.org/ + + Returns: + Set of `int` """ return self._cwes @cwes.setter - def cwes(self, cwes: List[int]) -> None: - self._cwes = cwes - - def add_cwe(self, cwe: int) -> None: - """ - Add a CWE identifier. - - Args: - cwe: - `int` identifier for the CWE - """ - self.cwes = self.cwes + [cwe] + def cwes(self, cwes: Iterable[int]) -> None: + self._cwes = set(cwes) @property def description(self) -> Optional[str]: """ A description of the vulnerability as provided by the source. + + Returns: + `str` if set else `None` """ return self._description @@ -801,6 +880,9 @@ def detail(self) -> Optional[str]: """ If available, an in-depth description of the vulnerability as provided by the source organization. Details often include examples, proof-of-concepts, and other information useful in understanding root cause. + + Returns: + `str` if set else `None` """ return self._detail @@ -812,6 +894,9 @@ def detail(self, detail: Optional[str]) -> None: def recommendation(self) -> Optional[str]: """ Recommendations of how the vulnerability can be remediated or mitigated. + + Returns: + `str` if set else `None` """ return self._recommendation @@ -820,28 +905,27 @@ def recommendation(self, recommendation: Optional[str]) -> None: self._recommendation = recommendation @property - def advisories(self) -> List[VulnerabilityAdvisory]: + def advisories(self) -> Set[VulnerabilityAdvisory]: """ Advisories relating to the Vulnerability. + + Returns: + Set of `VulnerabilityAdvisory` """ return self._advisories @advisories.setter - def advisories(self, advisories: List[VulnerabilityAdvisory]) -> None: - self._advisories = advisories + def advisories(self, advisories: Iterable[VulnerabilityAdvisory]) -> None: + self._advisories = set(advisories) - def add_advisory(self, advisory: VulnerabilityAdvisory) -> None: + @property + def created(self) -> Optional[datetime]: """ - Add a advisory. + The date and time (timestamp) when the vulnerability record was created in the vulnerability database. - Args: - advisory: - `VulnerabilityAdvisory` + Returns: + `datetime` if set else `None` """ - self.advisories = self.advisories + [advisory] - - @property - def created(self) -> Optional[datetime]: return self._created @created.setter @@ -850,6 +934,12 @@ def created(self, created: Optional[datetime]) -> None: @property def published(self) -> Optional[datetime]: + """ + The date and time (timestamp) when the vulnerability record was first published. + + Returns: + `datetime` if set else `None` + """ return self._published @published.setter @@ -858,6 +948,12 @@ def published(self, published: Optional[datetime]) -> None: @property def updated(self) -> Optional[datetime]: + """ + The date and time (timestamp) when the vulnerability record was last updated. + + Returns: + `datetime` if set else `None` + """ return self._updated @updated.setter @@ -868,6 +964,9 @@ def updated(self, updated: Optional[datetime]) -> None: def credits(self) -> Optional[VulnerabilityCredits]: """ Individuals or organizations credited with the discovery of the vulnerability. + + Returns: + `VulnerabilityCredits` if set else `None` """ return self._credits @@ -876,30 +975,26 @@ def credits(self, credits: Optional[VulnerabilityCredits]) -> None: self._credits = credits @property - def tools(self) -> List[Tool]: + def tools(self) -> Set[Tool]: """ The tool(s) used to identify, confirm, or score the vulnerability. + + Returns: + Set of `Tool` """ return self._tools @tools.setter - def tools(self, tools: List[Tool]) -> None: - self._tools = tools - - def add_tool(self, tool: Tool) -> None: - """ - Add a tool used to identify, confirm, or score the vulnerability. - - Args: - tool: - `Tool` - """ - self.tools = self.tools + [tool] + def tools(self, tools: Iterable[Tool]) -> None: + self._tools = set(tools) @property def analysis(self) -> Optional[VulnerabilityAnalysis]: """ Analysis of the Vulnerability in your context. + + Returns: + `VulnerabilityAnalysis` if set else `None` """ return self._analysis @@ -908,70 +1003,30 @@ def analysis(self, analysis: Optional[VulnerabilityAnalysis]) -> None: self._analysis = analysis @property - def affects(self) -> Optional[List[BomTarget]]: + def affects(self) -> Set[BomTarget]: """ The components or services that are affected by the vulnerability. + + Returns: + Set of `BomTarget` """ return self._affects @affects.setter - def affects(self, affects_targets: Optional[List[BomTarget]]) -> None: - self._affects = affects_targets - - # Methods pre-dating 1.4 that are kept for some backwards compatability - they will be removed in a future release! - def get_source_name(self) -> Optional[str]: - """ - Prior to Schema Version 1.4 when Vulnerabilities were supported by a schema extension, `source_name` and - `source_url` where represented differently in the model. - - .. warning:: - Deprecated - this method will be removed in a future version. - - See `Vulnerability.source.get_name()` - """ - warnings.warn( - 'The source of a Vulnerability is represnted differently in Schema Versions >= 1.4. ' - 'Vulnerability.get_source_name() is deprecated and will be removed in a future release. ' - 'Use Vulnerability.source.get_name()', - DeprecationWarning - ) - if self.source: - return self.source.name - return None - - def get_source_url(self) -> Optional[ParseResult]: - """ - Prior to Schema Version 1.4 when Vulnerabilities were supported by a schema extension, `source_name` and - `source_url` where represented differently in the model. - - .. warning:: - Deprecated - this method will be removed in a future version. - - See `Vulnerability.source.get_url()` - """ - warnings.warn( - 'The source of a Vulnerability is represnted differently in Schema Versions >= 1.4. ' - 'Vulnerability.get_source_name() is deprecated and will be removed in a future release. ' - 'Use Vulnerability.source.get_url()', - DeprecationWarning - ) - if self.source and self.source.url: - return urlparse(str(self.source.url)) - return None - - def get_recommendations(self) -> List[str]: - """ - Prior to Schema Version 1.4 when Vulnerabilities were supported by a schema extension, multiple recommendations - where permissible. - - .. warning:: - Deprecated - this method will be removed in a future version. - - See `Vulnerability.recommendation` - """ - warnings.warn( - 'A Vulnerability has only a single recommendation from Schema Version >= 1.4. ' - 'Vulnerability.get_recommendations() is deprecated and will be removed in a future release.', - DeprecationWarning - ) - return [self.recommendation] if self.recommendation else [] + def affects(self, affects_targets: Iterable[BomTarget]) -> None: + self._affects = set(affects_targets) + + def __eq__(self, other: object) -> bool: + if isinstance(other, Vulnerability): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash(( + self.id, self.source, tuple(self.references), tuple(self.ratings), tuple(self.cwes), self.description, + self.detail, self.recommendation, tuple(self.advisories), self.created, self.published, self.updated, + self.credits, tuple(self.tools), self.analysis, tuple(self.affects) + )) + + def __repr__(self) -> str: + return f'' diff --git a/cyclonedx/output/__init__.py b/cyclonedx/output/__init__.py index aab09eae..050ee622 100644 --- a/cyclonedx/output/__init__.py +++ b/cyclonedx/output/__init__.py @@ -40,8 +40,17 @@ class SchemaVersion(Enum): V1_3: str = 'V1Dot3' V1_4: str = 'V1Dot4' + def to_version(self) -> str: + """ + Return as a version string - e.g. `1.4` -DEFAULT_SCHEMA_VERSION = SchemaVersion.V1_3 + Returns: + `str` version + """ + return f'{self.value[1]}.{self.value[5]}' + + +LATEST_SUPPORTED_SCHEMA_VERSION = SchemaVersion.V1_4 class BaseOutput(ABC): @@ -51,6 +60,11 @@ def __init__(self, bom: Bom, **kwargs: int) -> None: self._bom = bom self._generated: bool = False + @property + @abstractmethod + def schema_version(self) -> SchemaVersion: + pass + @property def generated(self) -> bool: return self._generated @@ -91,7 +105,7 @@ def output_to_file(self, filename: str, allow_overwrite: bool = False) -> None: def get_instance(bom: Bom, output_format: OutputFormat = OutputFormat.XML, - schema_version: SchemaVersion = DEFAULT_SCHEMA_VERSION) -> BaseOutput: + schema_version: SchemaVersion = LATEST_SUPPORTED_SCHEMA_VERSION) -> BaseOutput: """ Helper method to quickly get the correct output class/formatter. diff --git a/cyclonedx/output/json.py b/cyclonedx/output/json.py index 1b7bcdec..0c646891 100644 --- a/cyclonedx/output/json.py +++ b/cyclonedx/output/json.py @@ -16,17 +16,17 @@ # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. - import json from abc import abstractmethod -from typing import Any, Dict, List, Optional, Union +from typing import cast, Any, Dict, List, Optional, Union -from . import BaseOutput +from . import BaseOutput, SchemaVersion from .schema import BaseSchemaVersion, SchemaVersion1Dot0, SchemaVersion1Dot1, SchemaVersion1Dot2, SchemaVersion1Dot3, \ SchemaVersion1Dot4 from .serializer.json import CycloneDxJSONEncoder +from ..exception.output import FormatNotSupportedException from ..model.bom import Bom - +from ..model.component import Component ComponentDict = Dict[str, Union[ str, @@ -41,21 +41,27 @@ def __init__(self, bom: Bom) -> None: super().__init__(bom=bom) self._json_output: str = '' + @property + def schema_version(self) -> SchemaVersion: + return self.schema_version_enum + def generate(self, force_regeneration: bool = False) -> None: if self.generated and not force_regeneration: return schema_uri: Optional[str] = self._get_schema_uri() if not schema_uri: - # JSON not supported! - return + raise FormatNotSupportedException( + f'JSON is not supported by CycloneDX in schema version {self.schema_version.to_version()}' + ) vulnerabilities: Dict[str, List[Dict[Any, Any]]] = {"vulnerabilities": []} - for component in self.get_bom().components: - for vulnerability in component.get_vulnerabilities(): - vulnerabilities['vulnerabilities'].append( - json.loads(json.dumps(vulnerability, cls=CycloneDxJSONEncoder)) - ) + if self.get_bom().components: + for component in cast(List[Component], self.get_bom().components): + for vulnerability in component.get_vulnerabilities(): + vulnerabilities['vulnerabilities'].append( + json.loads(json.dumps(vulnerability, cls=CycloneDxJSONEncoder)) + ) bom_json = json.loads(json.dumps(self.get_bom(), cls=CycloneDxJSONEncoder)) bom_json = json.loads(self._specialise_output_for_schema_version(bom_json=bom_json)) @@ -72,32 +78,38 @@ def _specialise_output_for_schema_version(self, bom_json: Dict[Any, Any]) -> str if not self.bom_supports_metadata(): if 'metadata' in bom_json.keys(): del bom_json['metadata'] - elif not self.bom_metadata_supports_tools(): + + if not self.bom_metadata_supports_tools(): del bom_json['metadata']['tools'] elif not self.bom_metadata_supports_tools_external_references(): for i in range(len(bom_json['metadata']['tools'])): if 'externalReferences' in bom_json['metadata']['tools'][i].keys(): del bom_json['metadata']['tools'][i]['externalReferences'] + if not self.bom_metadata_supports_licenses() and 'licenses' in bom_json['metadata'].keys(): + del bom_json['metadata']['licenses'] + + if not self.bom_metadata_supports_properties() and 'properties' in bom_json['metadata'].keys(): + del bom_json['metadata']['properties'] + # Iterate Components - if 'components' in bom_json.keys(): - for i in range(len(bom_json['components'])): - if not self.component_supports_author() and 'author' in bom_json['components'][i].keys(): - del bom_json['components'][i]['author'] + bom_json = self._recurse_specialise_component(bom_json=bom_json) - if not self.component_supports_mime_type_attribute() \ - and 'mime-type' in bom_json['components'][i].keys(): - del bom_json['components'][i]['mime-type'] + # Iterate Services + if 'services' in bom_json.keys(): + for i in range(len(bom_json['services'])): + if not self.services_supports_properties() and 'properties' in bom_json['services'][i].keys(): + del bom_json['services'][i]['properties'] - if not self.component_supports_release_notes() and 'releaseNotes' in bom_json['components'][i].keys(): - del bom_json['components'][i]['releaseNotes'] - else: - bom_json['components'] = [] + if not self.services_supports_release_notes() and 'releaseNotes' in bom_json['services'][i].keys(): + del bom_json['services'][i]['releaseNotes'] - # Iterate Vulnerabilities - if 'vulnerabilities' in bom_json.keys(): - for i in range(len(bom_json['vulnerabilities'])): - print("Checking " + str(bom_json['vulnerabilities'][i])) + # Iterate externalReferences + if 'externalReferences' in bom_json.keys(): + for i in range(len(bom_json['externalReferences'])): + if not self.external_references_supports_hashes() \ + and 'hashes' in bom_json['externalReferences'][i].keys(): + del bom_json['externalReferences'][i]['hashes'] return json.dumps(bom_json) @@ -119,6 +131,61 @@ def _create_bom_element(self) -> Dict[str, Union[str, int]]: def _get_schema_uri(self) -> Optional[str]: pass + def _recurse_specialise_component(self, bom_json: Dict[Any, Any], base_key: str = 'components') -> Dict[Any, Any]: + if base_key in bom_json.keys(): + for i in range(len(bom_json[base_key])): + if not self.component_supports_mime_type_attribute() \ + and 'mime-type' in bom_json[base_key][i].keys(): + del bom_json[base_key][i]['mime-type'] + + if not self.component_supports_supplier() and 'supplier' in bom_json[base_key][i].keys(): + del bom_json[base_key][i]['supplier'] + + if not self.component_supports_author() and 'author' in bom_json[base_key][i].keys(): + del bom_json[base_key][i]['author'] + + if self.component_version_optional() and bom_json[base_key][i]['version'] == "": + del bom_json[base_key][i]['version'] + + if not self.component_supports_pedigree() and 'pedigree' in bom_json[base_key][i].keys(): + del bom_json[base_key][i]['pedigree'] + elif 'pedigree' in bom_json[base_key][i].keys(): + if 'ancestors' in bom_json[base_key][i]['pedigree'].keys(): + # recurse into ancestors + bom_json[base_key][i]['pedigree'] = self._recurse_specialise_component( + bom_json=bom_json[base_key][i]['pedigree'], base_key='ancestors' + ) + if 'descendants' in bom_json[base_key][i]['pedigree'].keys(): + # recurse into descendants + bom_json[base_key][i]['pedigree'] = self._recurse_specialise_component( + bom_json=bom_json[base_key][i]['pedigree'], base_key='descendants' + ) + if 'variants' in bom_json[base_key][i]['pedigree'].keys(): + # recurse into variants + bom_json[base_key][i]['pedigree'] = self._recurse_specialise_component( + bom_json=bom_json[base_key][i]['pedigree'], base_key='variants' + ) + + if not self.external_references_supports_hashes() and 'externalReferences' \ + in bom_json[base_key][i].keys(): + for j in range(len(bom_json[base_key][i]['externalReferences'])): + del bom_json[base_key][i]['externalReferences'][j]['hashes'] + + if not self.component_supports_properties() and 'properties' in bom_json[base_key][i].keys(): + del bom_json[base_key][i]['properties'] + + # recurse + if 'components' in bom_json[base_key][i].keys(): + bom_json[base_key][i] = self._recurse_specialise_component(bom_json=bom_json[base_key][i]) + + if not self.component_supports_evidence() and 'evidence' in bom_json[base_key][i].keys(): + del bom_json[base_key][i]['evidence'] + + if not self.component_supports_release_notes() and 'releaseNotes' in bom_json[base_key][i].keys(): + del bom_json[base_key][i]['releaseNotes'] + + return bom_json + class JsonV1Dot0(Json, SchemaVersion1Dot0): diff --git a/cyclonedx/output/schema.py b/cyclonedx/output/schema.py index 87e8ca18..781762df 100644 --- a/cyclonedx/output/schema.py +++ b/cyclonedx/output/schema.py @@ -19,9 +19,16 @@ from abc import ABC, abstractmethod +from . import SchemaVersion + class BaseSchemaVersion(ABC): + @property + @abstractmethod + def schema_version_enum(self) -> SchemaVersion: + pass + def bom_supports_metadata(self) -> bool: return True @@ -31,6 +38,24 @@ def bom_metadata_supports_tools(self) -> bool: def bom_metadata_supports_tools_external_references(self) -> bool: return True + def bom_metadata_supports_licenses(self) -> bool: + return True + + def bom_metadata_supports_properties(self) -> bool: + return True + + def bom_supports_services(self) -> bool: + return True + + def bom_supports_external_references(self) -> bool: + return True + + def services_supports_properties(self) -> bool: + return True + + def services_supports_release_notes(self) -> bool: + return True + def bom_supports_vulnerabilities(self) -> bool: return True @@ -40,6 +65,9 @@ def bom_supports_vulnerabilities_via_extension(self) -> bool: def bom_requires_modified(self) -> bool: return False + def component_supports_supplier(self) -> bool: + return True + def component_supports_author(self) -> bool: return True @@ -49,15 +77,36 @@ def component_supports_bom_ref_attribute(self) -> bool: def component_supports_mime_type_attribute(self) -> bool: return True + def license_supports_expression(self) -> bool: + return True + def component_version_optional(self) -> bool: return False + def component_supports_swid(self) -> bool: + return True + + def component_supports_pedigree(self) -> bool: + return True + + def pedigree_supports_patches(self) -> bool: + return True + def component_supports_external_references(self) -> bool: return True + def component_supports_properties(self) -> bool: + return True + + def component_supports_evidence(self) -> bool: + return True + def component_supports_release_notes(self) -> bool: return True + def external_references_supports_hashes(self) -> bool: + return True + @abstractmethod def get_schema_version(self) -> str: raise NotImplementedError @@ -65,6 +114,10 @@ def get_schema_version(self) -> str: class SchemaVersion1Dot4(BaseSchemaVersion): + @property + def schema_version_enum(self) -> SchemaVersion: + return SchemaVersion.V1_4 + def get_schema_version(self) -> str: return '1.4' @@ -74,9 +127,16 @@ def component_version_optional(self) -> bool: class SchemaVersion1Dot3(BaseSchemaVersion): + @property + def schema_version_enum(self) -> SchemaVersion: + return SchemaVersion.V1_3 + def bom_metadata_supports_tools_external_references(self) -> bool: return False + def services_supports_release_notes(self) -> bool: + return False + def bom_supports_vulnerabilities(self) -> bool: return False @@ -95,9 +155,25 @@ def get_schema_version(self) -> str: class SchemaVersion1Dot2(BaseSchemaVersion): + @property + def schema_version_enum(self) -> SchemaVersion: + return SchemaVersion.V1_2 + def bom_metadata_supports_tools_external_references(self) -> bool: return False + def bom_metadata_supports_licenses(self) -> bool: + return False + + def bom_metadata_supports_properties(self) -> bool: + return False + + def services_supports_properties(self) -> bool: + return False + + def services_supports_release_notes(self) -> bool: + return False + def bom_supports_vulnerabilities(self) -> bool: return False @@ -107,21 +183,46 @@ def bom_supports_vulnerabilities_via_extension(self) -> bool: def component_supports_mime_type_attribute(self) -> bool: return False + def component_supports_properties(self) -> bool: + return False + + def component_supports_evidence(self) -> bool: + return False + def component_supports_release_notes(self) -> bool: return False + def external_references_supports_hashes(self) -> bool: + return False + def get_schema_version(self) -> str: return '1.2' class SchemaVersion1Dot1(BaseSchemaVersion): + @property + def schema_version_enum(self) -> SchemaVersion: + return SchemaVersion.V1_1 + def bom_metadata_supports_tools(self) -> bool: return False def bom_metadata_supports_tools_external_references(self) -> bool: return False + def bom_supports_services(self) -> bool: + return False + + def services_supports_properties(self) -> bool: + return False + + def pedigree_supports_patches(self) -> bool: + return False + + def services_supports_release_notes(self) -> bool: + return False + def bom_supports_vulnerabilities(self) -> bool: return False @@ -134,24 +235,55 @@ def bom_supports_metadata(self) -> bool: def component_supports_mime_type_attribute(self) -> bool: return False + def component_supports_supplier(self) -> bool: + return False + def component_supports_author(self) -> bool: return False + def component_supports_swid(self) -> bool: + return False + + def component_supports_properties(self) -> bool: + return False + + def component_supports_evidence(self) -> bool: + return False + def component_supports_release_notes(self) -> bool: return False + def external_references_supports_hashes(self) -> bool: + return False + def get_schema_version(self) -> str: return '1.1' class SchemaVersion1Dot0(BaseSchemaVersion): + @property + def schema_version_enum(self) -> SchemaVersion: + return SchemaVersion.V1_0 + def bom_metadata_supports_tools(self) -> bool: return False def bom_metadata_supports_tools_external_references(self) -> bool: return False + def bom_supports_services(self) -> bool: + return False + + def bom_supports_external_references(self) -> bool: + return False + + def services_supports_properties(self) -> bool: + return False + + def services_supports_release_notes(self) -> bool: + return False + def bom_supports_vulnerabilities(self) -> bool: return False @@ -167,14 +299,35 @@ def component_supports_author(self) -> bool: def component_supports_bom_ref_attribute(self) -> bool: return False + def license_supports_expression(self) -> bool: + return False + def component_supports_mime_type_attribute(self) -> bool: return False + def component_supports_supplier(self) -> bool: + return False + + def component_supports_swid(self) -> bool: + return False + + def component_supports_pedigree(self) -> bool: + return False + def component_supports_external_references(self) -> bool: return False + def component_supports_properties(self) -> bool: + return False + + def component_supports_evidence(self) -> bool: + return False + def component_supports_release_notes(self) -> bool: return False + def external_references_supports_hashes(self) -> bool: + return False + def get_schema_version(self) -> str: return '1.0' diff --git a/cyclonedx/output/serializer/__init__.py b/cyclonedx/output/serializer/__init__.py index e69de29b..308d97e7 100644 --- a/cyclonedx/output/serializer/__init__.py +++ b/cyclonedx/output/serializer/__init__.py @@ -0,0 +1,18 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# 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. diff --git a/cyclonedx/output/serializer/json.py b/cyclonedx/output/serializer/json.py index e1a65267..3598c8ec 100644 --- a/cyclonedx/output/serializer/json.py +++ b/cyclonedx/output/serializer/json.py @@ -16,7 +16,6 @@ # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. - from datetime import datetime from decimal import Decimal from enum import Enum @@ -28,10 +27,12 @@ # See https://github.com/package-url/packageurl-python/issues/65 from packageurl import PackageURL # type: ignore -from cyclonedx.model import XsUri +from ...model import XsUri +from ...model.bom_ref import BomRef +from ...model.component import Component HYPHENATED_ATTRIBUTES = [ - 'bom_ref', 'mime_type' + 'bom_ref', 'mime_type', 'x_trust_boundary' ] PYTHON_TO_JSON_NAME = compile(r'_([a-z])') @@ -39,6 +40,10 @@ class CycloneDxJSONEncoder(JSONEncoder): def default(self, o: Any) -> Any: + # BomRef + if isinstance(o, BomRef): + return str(o) + # datetime if isinstance(o, datetime): return o.isoformat() @@ -51,6 +56,10 @@ def default(self, o: Any) -> Any: if isinstance(o, Enum): return o.value + # Set + if isinstance(o, set): + return list(o) + # UUID if isinstance(o, UUID): return str(o) @@ -77,8 +86,11 @@ def default(self, o: Any) -> Any: elif '_' in new_key: new_key = PYTHON_TO_JSON_NAME.sub(lambda x: x.group(1).upper(), new_key) - # Skip any None values - if v: + # Inject '' for Component.version if it's None + if isinstance(o, Component) and new_key == 'version' and v is None: + d[new_key] = "" + elif v or v is False: + # Skip any None values (exception 'version') if isinstance(v, PackageURL): # Special handling of PackageURL instances which JSON would otherwise automatically encode to # an Array diff --git a/cyclonedx/output/xml.py b/cyclonedx/output/xml.py index c4ba91fc..94192c62 100644 --- a/cyclonedx/output/xml.py +++ b/cyclonedx/output/xml.py @@ -18,16 +18,19 @@ # Copyright (c) OWASP Foundation. All Rights Reserved. import warnings -from typing import List, Optional +from typing import Optional, Set from xml.etree import ElementTree -from . import BaseOutput +from . import BaseOutput, SchemaVersion from .schema import BaseSchemaVersion, SchemaVersion1Dot0, SchemaVersion1Dot1, SchemaVersion1Dot2, SchemaVersion1Dot3, \ SchemaVersion1Dot4 -from ..exception.output import ComponentVersionRequiredException -from ..model import ExternalReference, HashType, OrganizationalEntity, OrganizationalContact, Tool +from ..model import AttachedText, ExternalReference, HashType, IdentifiableAction, LicenseChoice, \ + OrganizationalEntity, OrganizationalContact, Property, Tool from ..model.bom import Bom -from ..model.component import Component +from ..model.bom_ref import BomRef +from ..model.component import Component, Patch +from ..model.release_note import ReleaseNotes +from ..model.service import Service from ..model.vulnerability import Vulnerability, VulnerabilityRating, VulnerabilitySource, BomTargetVersionRange @@ -39,6 +42,10 @@ def __init__(self, bom: Bom) -> None: super().__init__(bom=bom) self._root_bom_element: ElementTree.Element = self._create_bom_element() + @property + def schema_version(self) -> SchemaVersion: + return self.schema_version_enum + def generate(self, force_regeneration: bool = False) -> None: if self.generated and force_regeneration: self._root_bom_element = self._create_bom_element() @@ -51,25 +58,39 @@ def generate(self, force_regeneration: bool = False) -> None: components_element = ElementTree.SubElement(self._root_bom_element, 'components') has_vulnerabilities: bool = False - for component in self.get_bom().components: - component_element = self._add_component_element(component=component) - components_element.append(component_element) - if self.bom_supports_vulnerabilities_via_extension() and component.has_vulnerabilities(): - # Vulnerabilities are only possible when bom-ref is supported by the main CycloneDX schema version - vulnerabilities = ElementTree.SubElement(component_element, 'v:vulnerabilities') - for vulnerability in component.get_vulnerabilities(): - if component.bom_ref: - vulnerabilities.append( - self._get_vulnerability_as_xml_element_pre_1_3(bom_ref=component.bom_ref, - vulnerability=vulnerability) - ) - else: - warnings.warn( - f'Unable to include Vulnerability {str(vulnerability)} in generated BOM as the Component' - f'it relates to ({str(component)}) but it has no bom-ref.' - ) - elif component.has_vulnerabilities(): - has_vulnerabilities = True + if self.get_bom().components: + for component in self.get_bom().components: + component_element = self._add_component_element(component=component) + components_element.append(component_element) + if self.bom_supports_vulnerabilities_via_extension() and component.has_vulnerabilities(): + # Vulnerabilities are only possible when bom-ref is supported by the main CycloneDX schema version + vulnerabilities = ElementTree.SubElement(component_element, 'v:vulnerabilities') + for vulnerability in component.get_vulnerabilities(): + if component.bom_ref: + vulnerabilities.append( + Xml._get_vulnerability_as_xml_element_pre_1_3(bom_ref=component.bom_ref, + vulnerability=vulnerability) + ) + else: + warnings.warn( + f'Unable to include Vulnerability {str(vulnerability)} in generated BOM as the ' + f'Component it relates to ({str(component)}) but it has no bom-ref.' + ) + elif component.has_vulnerabilities(): + has_vulnerabilities = True + + if self.bom_supports_services(): + if self.get_bom().services: + services_element = ElementTree.SubElement(self._root_bom_element, 'services') + for service in self.get_bom().services: + services_element.append(self._add_service_element(service=service)) + + if self.bom_supports_external_references(): + if self.get_bom().external_references: + self._add_external_references_to_element( + ext_refs=self.get_bom().external_references, + element=self._root_bom_element + ) if self.bom_supports_vulnerabilities() and has_vulnerabilities: vulnerabilities_element = ElementTree.SubElement(self._root_bom_element, 'vulnerabilities') @@ -102,6 +123,17 @@ def _create_bom_element(self) -> ElementTree.Element: return ElementTree.Element('bom', root_attributes) + @staticmethod + def _add_identifiable_action_element(identifiable_action: IdentifiableAction, tag_name: str) -> ElementTree.Element: + ia_element = ElementTree.Element(tag_name) + if identifiable_action.timestamp: + ElementTree.SubElement(ia_element, 'timestamp').text = identifiable_action.timestamp.isoformat() + if identifiable_action.name: + ElementTree.SubElement(ia_element, 'name').text = identifiable_action.name + if identifiable_action.email: + ElementTree.SubElement(ia_element, 'email').text = identifiable_action.email + return ia_element + def _add_metadata_element(self) -> None: bom_metadata = self.get_bom().metadata metadata_e = ElementTree.SubElement(self._root_bom_element, 'metadata') @@ -113,21 +145,56 @@ def _add_metadata_element(self) -> None: for tool in bom_metadata.tools: self._add_tool(parent_element=tools_e, tool=tool) + if bom_metadata.authors: + authors_e = ElementTree.SubElement(metadata_e, 'authors') + for author in bom_metadata.authors: + Xml._add_organizational_contact( + parent_element=authors_e, contact=author, tag_name='author' + ) + if bom_metadata.component: metadata_e.append(self._add_component_element(component=bom_metadata.component)) + if bom_metadata.manufacture: + Xml._add_organizational_entity( + parent_element=metadata_e, organization=bom_metadata.manufacture, tag_name='manufacture' + ) + + if bom_metadata.supplier: + Xml._add_organizational_entity( + parent_element=metadata_e, organization=bom_metadata.supplier, tag_name='supplier' + ) + + if self.bom_metadata_supports_licenses() and bom_metadata.licenses: + licenses_e = ElementTree.SubElement(metadata_e, 'licenses') + self._add_licenses_to_element(licenses=bom_metadata.licenses, parent_element=licenses_e) + + if self.bom_metadata_supports_properties() and bom_metadata.properties: + Xml._add_properties_element(properties=bom_metadata.properties, parent_element=metadata_e) + def _add_component_element(self, component: Component) -> ElementTree.Element: element_attributes = {'type': component.type.value} if self.component_supports_bom_ref_attribute() and component.bom_ref: - element_attributes['bom-ref'] = component.bom_ref + element_attributes['bom-ref'] = str(component.bom_ref) if self.component_supports_mime_type_attribute() and component.mime_type: element_attributes['mime-type'] = component.mime_type component_element = ElementTree.Element('component', element_attributes) + # supplier + if self.component_supports_supplier() and component.supplier: + self._add_organizational_entity( + parent_element=component_element, organization=component.supplier, tag_name='supplier' + ) + + # author if self.component_supports_author() and component.author is not None: ElementTree.SubElement(component_element, 'author').text = component.author + # publisher + if component.publisher: + ElementTree.SubElement(component_element, 'publisher').text = component.publisher + # group if component.group: ElementTree.SubElement(component_element, 'group').text = component.group @@ -142,11 +209,17 @@ def _add_component_element(self, component: Component) -> ElementTree.Element: ElementTree.SubElement(component_element, 'version').text = component.version else: if not component.version: - raise ComponentVersionRequiredException( - f'Component "{str(component)}" has no version but the target schema version mandates ' - f'Components have a version specified' - ) - ElementTree.SubElement(component_element, 'version').text = component.version + ElementTree.SubElement(component_element, 'version') + else: + ElementTree.SubElement(component_element, 'version').text = component.version + + # description + if component.description: + ElementTree.SubElement(component_element, 'description').text = component.description + + # scope + if component.scope: + ElementTree.SubElement(component_element, 'scope').text = component.scope.value # hashes if component.hashes: @@ -155,25 +228,13 @@ def _add_component_element(self, component: Component) -> ElementTree.Element: # licenses if component.licenses: licenses_e = ElementTree.SubElement(component_element, 'licenses') - for license in component.licenses: - if license.license: - license_e = ElementTree.SubElement(licenses_e, 'license') - if license.license.id: - ElementTree.SubElement(license_e, 'id').text = license.license.id - elif license.license.name: - ElementTree.SubElement(license_e, 'name').text = license.license.name - if license.license.text: - license_text_e_attrs = {} - if license.license.text.content_type: - license_text_e_attrs['content-type'] = license.license.text.content_type - if license.license.text.encoding: - license_text_e_attrs['encoding'] = license.license.text.encoding.value - ElementTree.SubElement(license_e, 'text', - license_text_e_attrs).text = license.license.text.content - - ElementTree.SubElement(license_e, 'text').text = license.license.id - else: - ElementTree.SubElement(licenses_e, 'expression').text = license.expression + license_output: bool = self._add_licenses_to_element(licenses=component.licenses, parent_element=licenses_e) + if not license_output: + component_element.remove(licenses_e) + + # copyright + if component.copyright: + ElementTree.SubElement(component_element, 'copyright').text = component.copyright # cpe if component.cpe: @@ -183,99 +244,290 @@ def _add_component_element(self, component: Component) -> ElementTree.Element: if component.purl: ElementTree.SubElement(component_element, 'purl').text = component.purl.to_string() + # swid + if self.component_supports_swid() and component.swid: + swid_attrs = { + "tagId": component.swid.tag_id, + "name": component.swid.name + } + if component.swid.version: + swid_attrs['version'] = component.swid.version + if component.swid.tag_version: + swid_attrs['tagVersion'] = str(component.swid.tag_version) + if component.swid.patch is not None: + swid_attrs['patch'] = str(component.swid.patch).lower() + swid_element = ElementTree.SubElement(component_element, 'swid', swid_attrs) + if component.swid.text: + swid_element.append(Xml._add_attached_text(attached_text=component.swid.text)) + if component.swid.url: + ElementTree.SubElement(swid_element, 'url').text = str(component.swid.url) + # modified if self.bom_requires_modified(): ElementTree.SubElement(component_element, 'modified').text = 'false' + # pedigree + if self.component_supports_pedigree() and component.pedigree: + pedigree_element = ElementTree.SubElement(component_element, 'pedigree') + if component.pedigree.ancestors: + ancestors_element = ElementTree.SubElement(pedigree_element, 'ancestors') + for ancestor in component.pedigree.ancestors: + ancestors_element.append(self._add_component_element(component=ancestor)) + if component.pedigree.descendants: + descendants_element = ElementTree.SubElement(pedigree_element, 'descendants') + for descendant in component.pedigree.descendants: + descendants_element.append(self._add_component_element(component=descendant)) + if component.pedigree.variants: + variants_element = ElementTree.SubElement(pedigree_element, 'variants') + for variant in component.pedigree.variants: + variants_element.append(self._add_component_element(component=variant)) + if component.pedigree.commits: + commits_element = ElementTree.SubElement(pedigree_element, 'commits') + for commit in component.pedigree.commits: + commit_element = ElementTree.SubElement(commits_element, 'commit') + if commit.uid: + ElementTree.SubElement(commit_element, 'uid').text = commit.uid + if commit.url: + ElementTree.SubElement(commit_element, 'url').text = str(commit.url) + if commit.author: + commit_element.append(Xml._add_identifiable_action_element( + identifiable_action=commit.author, tag_name='author' + )) + if commit.committer: + commit_element.append(Xml._add_identifiable_action_element( + identifiable_action=commit.committer, tag_name='committer' + )) + if commit.message: + ElementTree.SubElement(commit_element, 'message').text = commit.message + if self.pedigree_supports_patches() and component.pedigree.patches: + patches_element = ElementTree.SubElement(pedigree_element, 'patches') + for patch in component.pedigree.patches: + patches_element.append(Xml.add_patch_element(patch=patch)) + if component.pedigree.notes: + ElementTree.SubElement(pedigree_element, 'notes').text = component.pedigree.notes + # externalReferences if self.component_supports_external_references() and len(component.external_references) > 0: - external_references_e = ElementTree.SubElement(component_element, 'externalReferences') - for ext_ref in component.external_references: - external_reference_e = ElementTree.SubElement( - external_references_e, 'reference', {'type': ext_ref.get_reference_type().value} + self._add_external_references_to_element(ext_refs=component.external_references, element=component_element) + + # properties + if self.component_supports_properties() and component.properties: + Xml._add_properties_element(properties=component.properties, parent_element=component_element) + + # components + if component.components: + components_element = ElementTree.SubElement(component_element, 'components') + for nested_component in component.components: + components_element.append(self._add_component_element(component=nested_component)) + + # evidence + if self.component_supports_evidence() and component.evidence: + evidence_element = ElementTree.SubElement(component_element, 'evidence') + if component.evidence.licenses: + evidence_licenses_element = ElementTree.SubElement(evidence_element, 'licenses') + self._add_licenses_to_element( + licenses=component.evidence.licenses, parent_element=evidence_licenses_element ) - ElementTree.SubElement(external_reference_e, 'url').text = ext_ref.get_url() - - if ext_ref.get_comment(): - ElementTree.SubElement(external_reference_e, 'comment').text = ext_ref.get_comment() - - if len(ext_ref.get_hashes()) > 0: - Xml._add_hashes_to_element(hashes=ext_ref.get_hashes(), element=external_reference_e) + if component.evidence.copyright: + evidence_copyrights_element = ElementTree.SubElement(evidence_element, 'copyright') + for evidence_copyright in component.evidence.copyright: + ElementTree.SubElement(evidence_copyrights_element, 'text').text = evidence_copyright.text # releaseNotes if self.component_supports_release_notes() and component.release_notes: - release_notes_e = ElementTree.SubElement(component_element, 'releaseNotes') - release_notes = component.release_notes - - ElementTree.SubElement(release_notes_e, 'type').text = release_notes.type - if release_notes.title: - ElementTree.SubElement(release_notes_e, 'title').text = release_notes.title - if release_notes.featured_image: - ElementTree.SubElement(release_notes_e, - 'featuredImage').text = str(release_notes.featured_image) - if release_notes.social_image: - ElementTree.SubElement(release_notes_e, - 'socialImage').text = str(release_notes.social_image) - if release_notes.description: - ElementTree.SubElement(release_notes_e, - 'description').text = release_notes.description - if release_notes.timestamp: - ElementTree.SubElement(release_notes_e, 'timestamp').text = release_notes.timestamp.isoformat() - if release_notes.aliases: - release_notes_aliases_e = ElementTree.SubElement(release_notes_e, 'aliases') - for alias in release_notes.aliases: - ElementTree.SubElement(release_notes_aliases_e, 'alias').text = alias - if release_notes.tags: - release_notes_tags_e = ElementTree.SubElement(release_notes_e, 'tags') - for tag in release_notes.tags: - ElementTree.SubElement(release_notes_tags_e, 'tag').text = tag - if release_notes.resolves: - release_notes_resolves_e = ElementTree.SubElement(release_notes_e, 'resolves') - for issue in release_notes.resolves: - issue_e = ElementTree.SubElement( - release_notes_resolves_e, 'issue', {'type': issue.get_classification().value} - ) - if issue.get_id(): - ElementTree.SubElement(issue_e, 'id').text = issue.get_id() - if issue.get_name(): - ElementTree.SubElement(issue_e, 'name').text = issue.get_name() - if issue.get_description(): - ElementTree.SubElement(issue_e, 'description').text = issue.get_description() - if issue.source: - issue_source_e = ElementTree.SubElement(issue_e, 'source') - if issue.source.name: - ElementTree.SubElement(issue_source_e, 'name').text = issue.source.name - if issue.source.url: - ElementTree.SubElement(issue_source_e, 'url').text = str(issue.source.url) - if issue.get_references(): - issue_references_e = ElementTree.SubElement(issue_e, 'references') - for reference in issue.get_references(): - ElementTree.SubElement(issue_references_e, 'url').text = str(reference) - if release_notes.notes: - release_notes_notes_e = ElementTree.SubElement(release_notes_e, 'notes') - for note in release_notes.notes: - note_e = ElementTree.SubElement(release_notes_notes_e, 'note') - if note.locale: - ElementTree.SubElement(note_e, 'locale').text = note.locale - text_attrs = {} - if note.text.content_type: - text_attrs['content-type'] = note.text.content_type - if note.text.encoding: - text_attrs['encoding'] = note.text.encoding.value - ElementTree.SubElement(note_e, 'text', text_attrs).text = note.text.content - if release_notes.properties: - release_notes_properties_e = ElementTree.SubElement(release_notes_e, 'properties') - for prop in release_notes.properties: - ElementTree.SubElement( - release_notes_properties_e, 'property', {'name': prop.get_name()} - ).text = prop.get_value() + Xml._add_release_notes_element(release_notes=component.release_notes, parent_element=component_element) return component_element + def _add_licenses_to_element(self, licenses: Set[LicenseChoice], parent_element: ElementTree.Element) -> bool: + license_output = False + for license_ in licenses: + if license_.license: + license_e = ElementTree.SubElement(parent_element, 'license') + if license_.license.id: + ElementTree.SubElement(license_e, 'id').text = license_.license.id + elif license_.license.name: + ElementTree.SubElement(license_e, 'name').text = license_.license.name + if license_.license.text: + license_text_e_attrs = {} + if license_.license.text.content_type: + license_text_e_attrs['content-type'] = license_.license.text.content_type + if license_.license.text.encoding: + license_text_e_attrs['encoding'] = license_.license.text.encoding.value + ElementTree.SubElement(license_e, 'text', + license_text_e_attrs).text = license_.license.text.content + if license_.license.url: + ElementTree.SubElement(license_e, 'url').text = str(license_.license.url) + + license_output = True + else: + if self.license_supports_expression(): + ElementTree.SubElement(parent_element, 'expression').text = license_.expression + license_output = True + return license_output + + @staticmethod + def _add_release_notes_element(release_notes: ReleaseNotes, parent_element: ElementTree.Element) -> None: + release_notes_e = ElementTree.SubElement(parent_element, 'releaseNotes') + + ElementTree.SubElement(release_notes_e, 'type').text = release_notes.type + if release_notes.title: + ElementTree.SubElement(release_notes_e, 'title').text = release_notes.title + if release_notes.featured_image: + ElementTree.SubElement(release_notes_e, + 'featuredImage').text = str(release_notes.featured_image) + if release_notes.social_image: + ElementTree.SubElement(release_notes_e, + 'socialImage').text = str(release_notes.social_image) + if release_notes.description: + ElementTree.SubElement(release_notes_e, + 'description').text = release_notes.description + if release_notes.timestamp: + ElementTree.SubElement(release_notes_e, 'timestamp').text = release_notes.timestamp.isoformat() + if release_notes.aliases: + release_notes_aliases_e = ElementTree.SubElement(release_notes_e, 'aliases') + for alias in release_notes.aliases: + ElementTree.SubElement(release_notes_aliases_e, 'alias').text = alias + if release_notes.tags: + release_notes_tags_e = ElementTree.SubElement(release_notes_e, 'tags') + for tag in release_notes.tags: + ElementTree.SubElement(release_notes_tags_e, 'tag').text = tag + if release_notes.resolves: + release_notes_resolves_e = ElementTree.SubElement(release_notes_e, 'resolves') + for issue in release_notes.resolves: + issue_e = ElementTree.SubElement( + release_notes_resolves_e, 'issue', {'type': issue.type.value} + ) + if issue.id: + ElementTree.SubElement(issue_e, 'id').text = issue.id + if issue.name: + ElementTree.SubElement(issue_e, 'name').text = issue.name + if issue.description: + ElementTree.SubElement(issue_e, 'description').text = issue.description + if issue.source: + issue_source_e = ElementTree.SubElement(issue_e, 'source') + if issue.source.name: + ElementTree.SubElement(issue_source_e, 'name').text = issue.source.name + if issue.source.url: + ElementTree.SubElement(issue_source_e, 'url').text = str(issue.source.url) + if issue.references: + issue_references_e = ElementTree.SubElement(issue_e, 'references') + for reference in issue.references: + ElementTree.SubElement(issue_references_e, 'url').text = str(reference) + if release_notes.notes: + release_notes_notes_e = ElementTree.SubElement(release_notes_e, 'notes') + for note in release_notes.notes: + note_e = ElementTree.SubElement(release_notes_notes_e, 'note') + if note.locale: + ElementTree.SubElement(note_e, 'locale').text = note.locale + text_attrs = {} + if note.text.content_type: + text_attrs['content-type'] = note.text.content_type + if note.text.encoding: + text_attrs['encoding'] = note.text.encoding.value + ElementTree.SubElement(note_e, 'text', text_attrs).text = note.text.content + if release_notes.properties: + Xml._add_properties_element(properties=release_notes.properties, parent_element=release_notes_e) + + @staticmethod + def add_patch_element(patch: Patch) -> ElementTree.Element: + patch_element = ElementTree.Element('patch', {"type": patch.type.value}) + if patch.diff: + diff_element = ElementTree.SubElement(patch_element, 'diff') + if patch.diff.text: + diff_element.append(Xml._add_attached_text(attached_text=patch.diff.text)) + if patch.diff.url: + ElementTree.SubElement(diff_element, 'url').text = str(patch.diff.url) + + return patch_element + + @staticmethod + def _add_properties_element(properties: Set[Property], parent_element: ElementTree.Element) -> None: + properties_e = ElementTree.SubElement(parent_element, 'properties') + for property_ in properties: + ElementTree.SubElement( + properties_e, 'property', {'name': property_.name} + ).text = property_.value + + def _add_service_element(self, service: Service) -> ElementTree.Element: + element_attributes = {} + if service.bom_ref: + element_attributes['bom-ref'] = str(service.bom_ref) + + service_element = ElementTree.Element('service', element_attributes) + + # provider + if service.provider: + self._add_organizational_entity( + parent_element=service_element, organization=service.provider, tag_name='provider' + ) + + # group + if service.group: + ElementTree.SubElement(service_element, 'group').text = service.group + + # name + ElementTree.SubElement(service_element, 'name').text = service.name + + # version + if service.version: + ElementTree.SubElement(service_element, 'version').text = service.version + + # description + if service.description: + ElementTree.SubElement(service_element, 'description').text = service.description + + # endpoints + if service.endpoints: + endpoints_e = ElementTree.SubElement(service_element, 'endpoints') + for endpoint in service.endpoints: + ElementTree.SubElement(endpoints_e, 'endpoint').text = str(endpoint) + + # authenticated + if isinstance(service.authenticated, bool): + ElementTree.SubElement(service_element, 'authenticated').text = str(service.authenticated).lower() + + # x-trust-boundary + if isinstance(service.x_trust_boundary, bool): + ElementTree.SubElement(service_element, 'x-trust-boundary').text = str(service.x_trust_boundary).lower() + + # data + if service.data: + data_e = ElementTree.SubElement(service_element, 'data') + for data in service.data: + ElementTree.SubElement(data_e, 'classification', {'flow': data.flow.value}).text = data.classification + + # licenses + if service.licenses: + licenses_e = ElementTree.SubElement(service_element, 'licenses') + license_output: bool = self._add_licenses_to_element(licenses=service.licenses, parent_element=licenses_e) + if not license_output: + service_element.remove(licenses_e) + + # externalReferences + if service.external_references: + self._add_external_references_to_element(ext_refs=service.external_references, element=service_element) + + # properties + if service.properties and self.services_supports_properties(): + Xml._add_properties_element(properties=service.properties, parent_element=service_element) + + # services + if service.services: + services_element = ElementTree.SubElement(service_element, 'services') + for sub_service in service.services: + services_element.append(self._add_service_element(service=sub_service)) + + # releaseNotes + if service.release_notes and self.services_supports_release_notes(): + Xml._add_release_notes_element(release_notes=service.release_notes, parent_element=service_element) + + return service_element + def _get_vulnerability_as_xml_element_post_1_4(self, vulnerability: Vulnerability) -> ElementTree.Element: vulnerability_element = ElementTree.Element( 'vulnerability', - {'bom-ref': vulnerability.bom_ref} if vulnerability.bom_ref else {} + {'bom-ref': str(vulnerability.bom_ref)} if vulnerability.bom_ref else {} ) # id @@ -403,10 +655,11 @@ def _get_vulnerability_as_xml_element_post_1_4(self, vulnerability: Vulnerabilit return vulnerability_element - def _get_vulnerability_as_xml_element_pre_1_3(self, bom_ref: str, + @staticmethod + def _get_vulnerability_as_xml_element_pre_1_3(bom_ref: BomRef, vulnerability: Vulnerability) -> ElementTree.Element: vulnerability_element = ElementTree.Element('v:vulnerability', { - 'ref': bom_ref + 'ref': str(bom_ref) }) # id @@ -467,26 +720,37 @@ def _get_vulnerability_as_xml_element_pre_1_3(self, bom_ref: str, return vulnerability_element - @staticmethod - def _add_external_references_to_element(ext_refs: List[ExternalReference], element: ElementTree.Element) -> None: + def _add_external_references_to_element(self, ext_refs: Set[ExternalReference], + element: ElementTree.Element) -> None: ext_refs_element = ElementTree.SubElement(element, 'externalReferences') for external_reference in ext_refs: ext_ref_element = ElementTree.SubElement( - ext_refs_element, 'reference', {'type': external_reference.get_reference_type().value} + ext_refs_element, 'reference', {'type': external_reference.type.value} ) - ElementTree.SubElement(ext_ref_element, 'url').text = external_reference.get_url() - if external_reference.get_comment(): - ElementTree.SubElement(ext_ref_element, 'comment').text = external_reference.get_comment() - if external_reference.get_hashes(): - Xml._add_hashes_to_element(hashes=external_reference.get_hashes(), element=ext_ref_element) + ElementTree.SubElement(ext_ref_element, 'url').text = str(external_reference.url) + if external_reference.comment: + ElementTree.SubElement(ext_ref_element, 'comment').text = external_reference.comment + if self.external_references_supports_hashes() and external_reference.hashes: + Xml._add_hashes_to_element(hashes=external_reference.hashes, element=ext_ref_element) + + @staticmethod + def _add_attached_text(attached_text: AttachedText, tag_name: str = 'text') -> ElementTree.Element: + element_attributes = {} + if attached_text.content_type: + element_attributes['content-type'] = attached_text.content_type + if attached_text.encoding: + element_attributes['encoding'] = attached_text.encoding.value + at_element = ElementTree.Element(tag_name, element_attributes) + at_element.text = attached_text.content + return at_element @staticmethod - def _add_hashes_to_element(hashes: List[HashType], element: ElementTree.Element) -> None: + def _add_hashes_to_element(hashes: Set[HashType], element: ElementTree.Element) -> None: hashes_e = ElementTree.SubElement(element, 'hashes') for h in hashes: ElementTree.SubElement( - hashes_e, 'hash', {'alg': h.get_algorithm().value} - ).text = h.get_hash_value() + hashes_e, 'hash', {'alg': h.alg.value} + ).text = h.content @staticmethod def _add_bom_target_version_range(parent_element: ElementTree.Element, version: BomTargetVersionRange) -> None: @@ -501,18 +765,16 @@ def _add_bom_target_version_range(parent_element: ElementTree.Element, version: def _add_tool(self, parent_element: ElementTree.Element, tool: Tool, tag_name: str = 'tool') -> None: tool_element = ElementTree.SubElement(parent_element, tag_name) - if tool.get_vendor(): - ElementTree.SubElement(tool_element, 'vendor').text = tool.get_vendor() - if tool.get_name(): - ElementTree.SubElement(tool_element, 'name').text = tool.get_name() - if tool.get_version(): - ElementTree.SubElement(tool_element, 'version').text = tool.get_version() - if tool.get_hashes(): - Xml._add_hashes_to_element(hashes=tool.get_hashes(), element=tool_element) - if self.bom_metadata_supports_tools_external_references() and tool.get_external_references(): - Xml._add_external_references_to_element( - ext_refs=tool.get_external_references(), element=tool_element - ) + if tool.vendor: + ElementTree.SubElement(tool_element, 'vendor').text = tool.vendor + if tool.name: + ElementTree.SubElement(tool_element, 'name').text = tool.name + if tool.version: + ElementTree.SubElement(tool_element, 'version').text = tool.version + if tool.hashes: + Xml._add_hashes_to_element(hashes=tool.hashes, element=tool_element) + if self.bom_metadata_supports_tools_external_references() and tool.external_references: + self._add_external_references_to_element(ext_refs=tool.external_references, element=tool_element) @staticmethod def _add_organizational_contact(parent_element: ElementTree.Element, contact: OrganizationalContact, @@ -531,11 +793,11 @@ def _add_organizational_entity(parent_element: ElementTree.Element, organization oe_element = ElementTree.SubElement(parent_element, tag_name) if organization.name: ElementTree.SubElement(oe_element, 'name').text = organization.name - if organization.urls: - for url in organization.urls: + if organization.url: + for url in organization.url: ElementTree.SubElement(oe_element, 'url').text = str(url) - if organization.contacts: - for contact in organization.contacts: + if organization.contact: + for contact in organization.contact: Xml._add_organizational_contact(parent_element=oe_element, contact=contact, tag_name='contact') @staticmethod diff --git a/docs/architecture.rst b/docs/architecture.rst index 3a62f9f2..3cf16d7c 100644 --- a/docs/architecture.rst +++ b/docs/architecture.rst @@ -1,3 +1,17 @@ +.. # 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 + Architecture ============ @@ -20,6 +34,7 @@ When wishing to generate a BOM, the process is as follows: :caption: Contents: modelling + schema-support outputting .. _cyclondex-python: https://pypi.org/project/cyclonedx-bom/ \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst index 5ec4a0ec..5941b19b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1 +1,15 @@ +.. # 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 + .. mdinclude:: ../CHANGELOG.md \ No newline at end of file diff --git a/docs/install.rst b/docs/install.rst index ed72965d..43123c61 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,3 +1,17 @@ +.. # 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 + Installation ============ diff --git a/docs/modelling.rst b/docs/modelling.rst index 68626f4b..119f9b8f 100644 --- a/docs/modelling.rst +++ b/docs/modelling.rst @@ -1,3 +1,17 @@ +.. # 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 + Modelling ========= diff --git a/docs/outputting.rst b/docs/outputting.rst index a8f2ee01..50e800d1 100644 --- a/docs/outputting.rst +++ b/docs/outputting.rst @@ -1,3 +1,17 @@ +.. # 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 + Outputting ========== @@ -9,7 +23,7 @@ We provide two helper methods: * Output to string (for you to do with as you require) * Output directly to a filename you provide -The default output will be XML at Schema Version 1.3. +By default output will be in XML at latest supported schema version - see :py:mod:`cyclonedx.output.LATEST_SUPPORTED_SCHEMA_VERSION`. Supported CycloneDX Schema Versions ----------------------------------- @@ -20,12 +34,12 @@ This library supports the following schema versions: * 1.1 (XML) - `(note, 1.1 schema version has no support for JSON)` * 1.2 (XML, JSON) * 1.3 (XML, JSON) -* 1.4 (XML, JSON) +* 1.4 (XML, JSON) - the latest supported schema version Outputting to JSON ------------------ -The below example relies on the default schema version being 1.3, but sets the output format to JSON. Output is returned +The below example relies on the latest schema version, but sets the output format to JSON. Output is returned as a ``str``. .. code-block:: python diff --git a/docs/schema-support.rst b/docs/schema-support.rst new file mode 100644 index 00000000..bd317868 --- /dev/null +++ b/docs/schema-support.rst @@ -0,0 +1,51 @@ +.. # 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 + +Schema Support +============== + +This library has partial support for the CycloneDX specification (we continue to grow support). + +The following sub-sections aim to explain what support this library provides and any known gaps in support. We do this +by calling out support for data as defined in the latest CycloneDX standard specification, regardless of whether it is +supported in prior versions of the CycloneDX schema. + ++----------------------------+---------------+---------------------------------------------------------------------------------------------------+ +| Data Path | Supported? | Notes | ++============================+===============+===================================================================================================+ +| ``bom[@version]`` | Yes | | ++----------------------------+---------------+---------------------------------------------------------------------------------------------------+ +| ``bom[@serialNumber]`` | Yes | | ++----------------------------+---------------+---------------------------------------------------------------------------------------------------+ +| ``bom.metadata`` | Yes | | ++----------------------------+---------------+---------------------------------------------------------------------------------------------------+ +| ``bom.components`` | Yes | Not supported: ``modified`` (as it is deprecated), ``signature``. | ++----------------------------+---------------+---------------------------------------------------------------------------------------------------+ +| ``bom.services`` | Yes | Not supported: ``signature``. | ++----------------------------+---------------+---------------------------------------------------------------------------------------------------+ +| ``bom.externalReferences`` | Yes | | ++----------------------------+---------------+---------------------------------------------------------------------------------------------------+ +| ``bom.dependencies`` | No | | ++----------------------------+---------------+---------------------------------------------------------------------------------------------------+ +| ``bom.compositions`` | No | | ++----------------------------+---------------+---------------------------------------------------------------------------------------------------+ +| ``bom.properties`` | No | See `schema specification bug 130`_ | ++----------------------------+---------------+---------------------------------------------------------------------------------------------------+ +| ``bom.vulnerabilities`` | Yes | Note: Prior to CycloneDX 1.4, these were present under ``bom.components`` via a schema extension. | ++----------------------------+---------------+---------------------------------------------------------------------------------------------------+ +| ``bom.signature`` | No | | ++----------------------------+---------------+---------------------------------------------------------------------------------------------------+ + + +.. _schema specification bug 130: https://github.com/CycloneDX/specification/issues/130 \ No newline at end of file diff --git a/docs/support.rst b/docs/support.rst index 62b441a2..48773249 100644 --- a/docs/support.rst +++ b/docs/support.rst @@ -1,3 +1,17 @@ +.. # 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 + Support ======= diff --git a/poetry.lock b/poetry.lock index 92b51754..b47adf8f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -156,7 +156,7 @@ format_nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "lxml" -version = "4.7.1" +version = "4.8.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "dev" optional = false @@ -204,14 +204,14 @@ python-versions = "*" [[package]] name = "packageurl-python" -version = "0.9.6" +version = "0.9.9" description = "A purl aka. Package URL parser and builder" category = "main" optional = false python-versions = ">=3.6" [package.extras] -test = ["pytest", "isort"] +test = ["isort", "pytest"] [[package]] name = "packaging" @@ -277,7 +277,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pyparsing" -version = "3.0.6" +version = "3.0.7" description = "Python parsing module" category = "dev" optional = false @@ -343,7 +343,7 @@ testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytes [[package]] name = "typed-ast" -version = "1.5.1" +version = "1.5.2" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false @@ -351,7 +351,7 @@ python-versions = ">=3.6" [[package]] name = "types-setuptools" -version = "57.4.7" +version = "57.4.9" description = "Typing stubs for setuptools" category = "main" optional = false @@ -393,6 +393,18 @@ six = ">=1.9.0,<2" docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +[[package]] +name = "xmldiff" +version = "2.4" +description = "Creates diffs of XML files" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +lxml = ">=3.1.0" +six = "*" + [[package]] name = "zipp" version = "3.6.0" @@ -408,7 +420,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "7d2eb65a2400fc39ea675281879e506d7aeb58689dda1f3b389b875040aae9cc" +content-hash = "6a766bb8018a3c7492f24fb8d7567298cd38cad8253ade1a579871066dcbdf60" [metadata.files] attrs = [ @@ -505,66 +517,67 @@ jsonschema = [ {file = "jsonschema-4.4.0.tar.gz", hash = "sha256:636694eb41b3535ed608fe04129f26542b59ed99808b4f688aa32dcf55317a83"}, ] lxml = [ - {file = "lxml-4.7.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f"}, - {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e"}, - {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17"}, - {file = "lxml-4.7.1-cp27-cp27m-win32.whl", hash = "sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419"}, - {file = "lxml-4.7.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2"}, - {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5"}, - {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d"}, - {file = "lxml-4.7.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175"}, - {file = "lxml-4.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6"}, - {file = "lxml-4.7.1-cp310-cp310-win32.whl", hash = "sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6"}, - {file = "lxml-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e"}, - {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675"}, - {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7"}, - {file = "lxml-4.7.1-cp35-cp35m-win32.whl", hash = "sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5"}, - {file = "lxml-4.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03"}, - {file = "lxml-4.7.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6"}, - {file = "lxml-4.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d"}, - {file = "lxml-4.7.1-cp36-cp36m-win32.whl", hash = "sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d"}, - {file = "lxml-4.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a"}, - {file = "lxml-4.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944"}, - {file = "lxml-4.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b"}, - {file = "lxml-4.7.1-cp37-cp37m-win32.whl", hash = "sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4"}, - {file = "lxml-4.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1"}, - {file = "lxml-4.7.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e"}, - {file = "lxml-4.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9"}, - {file = "lxml-4.7.1-cp38-cp38-win32.whl", hash = "sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd"}, - {file = "lxml-4.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d"}, - {file = "lxml-4.7.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851"}, - {file = "lxml-4.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4"}, - {file = "lxml-4.7.1-cp39-cp39-win32.whl", hash = "sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0"}, - {file = "lxml-4.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"}, - {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"}, + {file = "lxml-4.8.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:e1ab2fac607842ac36864e358c42feb0960ae62c34aa4caaf12ada0a1fb5d99b"}, + {file = "lxml-4.8.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28d1af847786f68bec57961f31221125c29d6f52d9187c01cd34dc14e2b29430"}, + {file = "lxml-4.8.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b92d40121dcbd74831b690a75533da703750f7041b4bf951befc657c37e5695a"}, + {file = "lxml-4.8.0-cp27-cp27m-win32.whl", hash = "sha256:e01f9531ba5420838c801c21c1b0f45dbc9607cb22ea2cf132844453bec863a5"}, + {file = "lxml-4.8.0-cp27-cp27m-win_amd64.whl", hash = "sha256:6259b511b0f2527e6d55ad87acc1c07b3cbffc3d5e050d7e7bcfa151b8202df9"}, + {file = "lxml-4.8.0-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1010042bfcac2b2dc6098260a2ed022968dbdfaf285fc65a3acf8e4eb1ffd1bc"}, + {file = "lxml-4.8.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fa56bb08b3dd8eac3a8c5b7d075c94e74f755fd9d8a04543ae8d37b1612dd170"}, + {file = "lxml-4.8.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:31ba2cbc64516dcdd6c24418daa7abff989ddf3ba6d3ea6f6ce6f2ed6e754ec9"}, + {file = "lxml-4.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:31499847fc5f73ee17dbe1b8e24c6dafc4e8d5b48803d17d22988976b0171f03"}, + {file = "lxml-4.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5f7d7d9afc7b293147e2d506a4596641d60181a35279ef3aa5778d0d9d9123fe"}, + {file = "lxml-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a3c5f1a719aa11866ffc530d54ad965063a8cbbecae6515acbd5f0fae8f48eaa"}, + {file = "lxml-4.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6268e27873a3d191849204d00d03f65c0e343b3bcb518a6eaae05677c95621d1"}, + {file = "lxml-4.8.0-cp310-cp310-win32.whl", hash = "sha256:330bff92c26d4aee79c5bc4d9967858bdbe73fdbdbacb5daf623a03a914fe05b"}, + {file = "lxml-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b2582b238e1658c4061ebe1b4df53c435190d22457642377fd0cb30685cdfb76"}, + {file = "lxml-4.8.0-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2bfc7e2a0601b475477c954bf167dee6d0f55cb167e3f3e7cefad906e7759f6"}, + {file = "lxml-4.8.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a1547ff4b8a833511eeaceacbcd17b043214fcdb385148f9c1bc5556ca9623e2"}, + {file = "lxml-4.8.0-cp35-cp35m-win32.whl", hash = "sha256:a9f1c3489736ff8e1c7652e9dc39f80cff820f23624f23d9eab6e122ac99b150"}, + {file = "lxml-4.8.0-cp35-cp35m-win_amd64.whl", hash = "sha256:530f278849031b0eb12f46cca0e5db01cfe5177ab13bd6878c6e739319bae654"}, + {file = "lxml-4.8.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:078306d19a33920004addeb5f4630781aaeabb6a8d01398045fcde085091a169"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:86545e351e879d0b72b620db6a3b96346921fa87b3d366d6c074e5a9a0b8dadb"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24f5c5ae618395ed871b3d8ebfcbb36e3f1091fd847bf54c4de623f9107942f3"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bbab6faf6568484707acc052f4dfc3802bdb0cafe079383fbaa23f1cdae9ecd4"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7993232bd4044392c47779a3c7e8889fea6883be46281d45a81451acfd704d7e"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6d6483b1229470e1d8835e52e0ff3c6973b9b97b24cd1c116dca90b57a2cc613"}, + {file = "lxml-4.8.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ad4332a532e2d5acb231a2e5d33f943750091ee435daffca3fec0a53224e7e33"}, + {file = "lxml-4.8.0-cp36-cp36m-win32.whl", hash = "sha256:db3535733f59e5605a88a706824dfcb9bd06725e709ecb017e165fc1d6e7d429"}, + {file = "lxml-4.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5f148b0c6133fb928503cfcdfdba395010f997aa44bcf6474fcdd0c5398d9b63"}, + {file = "lxml-4.8.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:8a31f24e2a0b6317f33aafbb2f0895c0bce772980ae60c2c640d82caac49628a"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:719544565c2937c21a6f76d520e6e52b726d132815adb3447ccffbe9f44203c4"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:c0b88ed1ae66777a798dc54f627e32d3b81c8009967c63993c450ee4cbcbec15"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fa9b7c450be85bfc6cd39f6df8c5b8cbd76b5d6fc1f69efec80203f9894b885f"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e9f84ed9f4d50b74fbc77298ee5c870f67cb7e91dcdc1a6915cb1ff6a317476c"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1d650812b52d98679ed6c6b3b55cbb8fe5a5460a0aef29aeb08dc0b44577df85"}, + {file = "lxml-4.8.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:80bbaddf2baab7e6de4bc47405e34948e694a9efe0861c61cdc23aa774fcb141"}, + {file = "lxml-4.8.0-cp37-cp37m-win32.whl", hash = "sha256:6f7b82934c08e28a2d537d870293236b1000d94d0b4583825ab9649aef7ddf63"}, + {file = "lxml-4.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e1fd7d2fe11f1cb63d3336d147c852f6d07de0d0020d704c6031b46a30b02ca8"}, + {file = "lxml-4.8.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:5045ee1ccd45a89c4daec1160217d363fcd23811e26734688007c26f28c9e9e7"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0c1978ff1fd81ed9dcbba4f91cf09faf1f8082c9d72eb122e92294716c605428"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cbf2ff155b19dc4d4100f7442f6a697938bf4493f8d3b0c51d45568d5666b5"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ce13d6291a5f47c1c8dbd375baa78551053bc6b5e5c0e9bb8e39c0a8359fd52f"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11527dc23d5ef44d76fef11213215c34f36af1608074561fcc561d983aeb870"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:60d2f60bd5a2a979df28ab309352cdcf8181bda0cca4529769a945f09aba06f9"}, + {file = "lxml-4.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:62f93eac69ec0f4be98d1b96f4d6b964855b8255c345c17ff12c20b93f247b68"}, + {file = "lxml-4.8.0-cp38-cp38-win32.whl", hash = "sha256:20b8a746a026017acf07da39fdb10aa80ad9877046c9182442bf80c84a1c4696"}, + {file = "lxml-4.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:891dc8f522d7059ff0024cd3ae79fd224752676447f9c678f2a5c14b84d9a939"}, + {file = "lxml-4.8.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b6fc2e2fb6f532cf48b5fed57567ef286addcef38c28874458a41b7837a57807"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:74eb65ec61e3c7c019d7169387d1b6ffcfea1b9ec5894d116a9a903636e4a0b1"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:627e79894770783c129cc5e89b947e52aa26e8e0557c7e205368a809da4b7939"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:545bd39c9481f2e3f2727c78c169425efbfb3fbba6e7db4f46a80ebb249819ca"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5a58d0b12f5053e270510bf12f753a76aaf3d74c453c00942ed7d2c804ca845c"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec4b4e75fc68da9dc0ed73dcdb431c25c57775383fec325d23a770a64e7ebc87"}, + {file = "lxml-4.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5804e04feb4e61babf3911c2a974a5b86f66ee227cc5006230b00ac6d285b3a9"}, + {file = "lxml-4.8.0-cp39-cp39-win32.whl", hash = "sha256:aa0cf4922da7a3c905d000b35065df6184c0dc1d866dd3b86fd961905bbad2ea"}, + {file = "lxml-4.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:dd10383f1d6b7edf247d0960a3db274c07e96cf3a3fc7c41c8448f93eac3fb1c"}, + {file = "lxml-4.8.0-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:2403a6d6fb61c285969b71f4a3527873fe93fd0abe0832d858a17fe68c8fa507"}, + {file = "lxml-4.8.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:986b7a96228c9b4942ec420eff37556c5777bfba6758edcb95421e4a614b57f9"}, + {file = "lxml-4.8.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6fe4ef4402df0250b75ba876c3795510d782def5c1e63890bde02d622570d39e"}, + {file = "lxml-4.8.0-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:f10ce66fcdeb3543df51d423ede7e238be98412232fca5daec3e54bcd16b8da0"}, + {file = "lxml-4.8.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:730766072fd5dcb219dd2b95c4c49752a54f00157f322bc6d71f7d2a31fecd79"}, + {file = "lxml-4.8.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8b99ec73073b37f9ebe8caf399001848fced9c08064effdbfc4da2b5a8d07b93"}, + {file = "lxml-4.8.0.tar.gz", hash = "sha256:f63f62fc60e6228a4ca9abae28228f35e1bd3ce675013d1dfb828688d50c6e23"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, @@ -597,8 +610,8 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] packageurl-python = [ - {file = "packageurl-python-0.9.6.tar.gz", hash = "sha256:c01fbaf62ad2eb791e97158d1f30349e830bee2dd3e9503a87f6c3ffae8d1cf0"}, - {file = "packageurl_python-0.9.6-py3-none-any.whl", hash = "sha256:676dcb8278721df952e2444bfcd8d7bf3518894498050f0c6a5faddbe0860cd0"}, + {file = "packageurl-python-0.9.9.tar.gz", hash = "sha256:872a0434b9a448b3fa97571711f69dd2a3fb72345ad66c90b17d827afea82f09"}, + {file = "packageurl_python-0.9.9-py3-none-any.whl", hash = "sha256:07aa852d1c48b0e86e625f6a32d83f96427739806b269d0f8142788ee807114b"}, ] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, @@ -625,8 +638,8 @@ pyflakes = [ {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, ] pyparsing = [ - {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, - {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pyrsistent = [ {file = "pyrsistent-0.18.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1"}, @@ -668,29 +681,34 @@ tox = [ {file = "tox-3.24.5.tar.gz", hash = "sha256:67e0e32c90e278251fea45b696d0fef3879089ccbe979b0c556d35d5a70e2993"}, ] typed-ast = [ - {file = "typed_ast-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212"}, - {file = "typed_ast-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631"}, - {file = "typed_ast-1.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb"}, - {file = "typed_ast-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08"}, - {file = "typed_ast-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e"}, - {file = "typed_ast-1.5.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695"}, - {file = "typed_ast-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30"}, - {file = "typed_ast-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f"}, - {file = "typed_ast-1.5.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471"}, - {file = "typed_ast-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb"}, - {file = "typed_ast-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb"}, - {file = "typed_ast-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af"}, - {file = "typed_ast-1.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d"}, - {file = "typed_ast-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a"}, - {file = "typed_ast-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32"}, - {file = "typed_ast-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4"}, - {file = "typed_ast-1.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d"}, - {file = "typed_ast-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775"}, - {file = "typed_ast-1.5.1.tar.gz", hash = "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5"}, + {file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"}, + {file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"}, + {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"}, + {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"}, + {file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"}, + {file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"}, + {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"}, + {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"}, + {file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"}, + {file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"}, + {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"}, + {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"}, + {file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"}, + {file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"}, + {file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"}, + {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"}, + {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"}, + {file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"}, + {file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"}, + {file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"}, + {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"}, + {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"}, + {file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"}, + {file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"}, ] types-setuptools = [ - {file = "types-setuptools-57.4.7.tar.gz", hash = "sha256:9677d969b00ec1c14552f5be2b2b47a6fbea4d0ed4de0fdcee18abdaa0cc9267"}, - {file = "types_setuptools-57.4.7-py3-none-any.whl", hash = "sha256:ffda504687ea02d4b7751c0d1df517fbbcdc276836d90849e4f1a5f1ccd79f01"}, + {file = "types-setuptools-57.4.9.tar.gz", hash = "sha256:536ef74744f8e1e4be4fc719887f886e74e4cf3c792b4a06984320be4df450b5"}, + {file = "types_setuptools-57.4.9-py3-none-any.whl", hash = "sha256:948dc6863373750e2cd0b223a84f1fb608414cde5e55cf38ea657b93aeb411d2"}, ] types-toml = [ {file = "types-toml-0.10.4.tar.gz", hash = "sha256:9340e7c1587715581bb13905b3af30b79fe68afaccfca377665d5e63b694129a"}, @@ -705,6 +723,10 @@ virtualenv = [ {file = "virtualenv-20.13.1-py2.py3-none-any.whl", hash = "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7"}, {file = "virtualenv-20.13.1.tar.gz", hash = "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14"}, ] +xmldiff = [ + {file = "xmldiff-2.4-py2.py3-none-any.whl", hash = "sha256:213c2f4c39ed71811a9ceeec1c8bdf2e673e5527261ea11708b3acfa6c2bdb00"}, + {file = "xmldiff-2.4.tar.gz", hash = "sha256:05bea20ce1f2c9678683bcce0c3ba9981f87d92b709d190e018bcbf047eccf63"}, +] zipp = [ {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, diff --git a/pyproject.toml b/pyproject.toml index 9546bc59..2c648b90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ flake8-annotations = {version = "^2.7.0", python = ">= 3.6.2"} flake8-bugbear = "^22.1.11" jsonschema = { version = ">= 4.4.0", python = "> 3.6"} lxml = ">=4.7.0" +xmldiff = ">=2.4" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/base.py b/tests/base.py index 043f2526..10cb89c5 100644 --- a/tests/base.py +++ b/tests/base.py @@ -22,13 +22,14 @@ import sys import xml.etree.ElementTree from datetime import datetime, timezone -from lxml import etree from typing import Any from unittest import TestCase from uuid import uuid4 -from xml.dom import minidom +from lxml import etree from lxml.etree import DocumentInvalid +from xmldiff import main +from xmldiff.actions import MoveNode from cyclonedx.output import SchemaVersion @@ -66,10 +67,19 @@ def assertValidAgainstSchema(self, bom_json: str, schema_version: SchemaVersion) else: self.assertTrue(True, 'JSON Schema Validation is not possible in Python < 3.7') + @staticmethod + def _sort_json_dict(item: object) -> Any: + if isinstance(item, dict): + return sorted((key, BaseJsonTestCase._sort_json_dict(values)) for key, values in item.items()) + if isinstance(item, list): + return sorted(BaseJsonTestCase._sort_json_dict(x) for x in item) + else: + return item + def assertEqualJson(self, a: str, b: str) -> None: self.assertEqual( - json.dumps(json.loads(a), sort_keys=True), - json.dumps(json.loads(b), sort_keys=True) + BaseJsonTestCase._sort_json_dict(json.loads(a)), + BaseJsonTestCase._sort_json_dict(json.loads(b)) ) def assertEqualJsonBom(self, a: str, b: str) -> None: @@ -117,18 +127,20 @@ def assertValidAgainstSchema(self, bom_xml: str, schema_version: SchemaVersion) if not schema_validates: print(xml_schema.error_log.last_error) - self.assertTrue(schema_validates, 'Failed to validate Generated SBOM against XSD Schema') + self.assertTrue(schema_validates, f'Failed to validate Generated SBOM against XSD Schema:' + f'{bom_xml}') def assertEqualXml(self, a: str, b: str) -> None: - da, db = minidom.parseString(a), minidom.parseString(b) - self.assertTrue(self._is_equal_xml_element(da.documentElement, db.documentElement), - 'XML Documents are not equal: \n{}\n{}'.format(da.toxml(), db.toxml())) + diff_results = main.diff_texts(a, b, diff_options={'F': 0.5}) + diff_results = list(filter(lambda o: not isinstance(o, MoveNode), diff_results)) + self.assertEqual(len(diff_results), 0, f'There are XML differences: {diff_results}') def assertEqualXmlBom(self, a: str, b: str, namespace: str) -> None: """ Sanitise some fields such as timestamps which cannot have their values directly compared for equality. """ - ba, bb = xml.etree.ElementTree.fromstring(a), xml.etree.ElementTree.fromstring(b) + ba = xml.etree.ElementTree.fromstring(a, etree.XMLParser(remove_blank_text=True, remove_comments=True)) + bb = xml.etree.ElementTree.fromstring(b, etree.XMLParser(remove_blank_text=True, remove_comments=True)) # Align serialNumbers ba.set('serialNumber', single_uuid) @@ -146,42 +158,13 @@ def assertEqualXmlBom(self, a: str, b: str, namespace: str) -> None: # Align 'this' Tool Version this_tool = ba.find('.//*/{{{}}}tool[{{{}}}version="VERSION"]'.format(namespace, namespace)) - if this_tool: + if this_tool is not None: this_tool.find('./{{{}}}version'.format(namespace)).text = cyclonedx_lib_version this_tool = bb.find('.//*/{{{}}}tool[{{{}}}version="VERSION"]'.format(namespace, namespace)) - if this_tool: + if this_tool is not None: this_tool.find('./{{{}}}version'.format(namespace)).text = cyclonedx_lib_version self.assertEqualXml( xml.etree.ElementTree.tostring(ba, 'unicode'), xml.etree.ElementTree.tostring(bb, 'unicode') ) - - def _is_equal_xml_element(self, a: Any, b: Any) -> bool: - if a.tagName != b.tagName: - return False - if sorted(a.attributes.items()) != sorted(b.attributes.items()): - return False - - """ - Remove any pure whitespace Dom Text Nodes before we compare - - See: https://xml-sig.python.narkive.com/8o0UIicu - """ - for n in a.childNodes: - if n.nodeType == n.TEXT_NODE and n.data.strip() == '': - a.removeChild(n) - for n in b.childNodes: - if n.nodeType == n.TEXT_NODE and n.data.strip() == '': - b.removeChild(n) - - if len(a.childNodes) != len(b.childNodes): - return False - for ac, bc in zip(a.childNodes, b.childNodes): - if ac.nodeType != bc.nodeType: - return False - if ac.nodeType == ac.TEXT_NODE and ac.data != bc.data: - return False - if ac.nodeType == ac.ELEMENT_NODE and not self._is_equal_xml_element(ac, bc): - return False - return True diff --git a/tests/data.py b/tests/data.py new file mode 100644 index 00000000..0ecf6887 --- /dev/null +++ b/tests/data.py @@ -0,0 +1,459 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# 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 base64 +from datetime import datetime, timezone +from decimal import Decimal +from typing import List, Optional + +from packageurl import PackageURL + +from cyclonedx.model import AttachedText, DataClassification, DataFlow, Encoding, ExternalReference, \ + ExternalReferenceType, HashType, LicenseChoice, License, Note, NoteText, OrganizationalContact, \ + OrganizationalEntity, Property, Tool, XsUri +from cyclonedx.model.bom import Bom +from cyclonedx.model.component import Commit, Component, ComponentEvidence, ComponentType, Copyright, Patch, \ + PatchClassification, Pedigree, Swid, ComponentScope +from cyclonedx.model.issue import IssueClassification, IssueType, IssueTypeSource +from cyclonedx.model.release_note import ReleaseNotes +from cyclonedx.model.service import Service +from cyclonedx.model.vulnerability import ImpactAnalysisState, ImpactAnalysisJustification, ImpactAnalysisResponse, \ + ImpactAnalysisAffectedStatus, Vulnerability, VulnerabilityCredits, VulnerabilityRating, VulnerabilitySeverity, \ + VulnerabilitySource, VulnerabilityScoreSource, VulnerabilityAdvisory, VulnerabilityReference, \ + VulnerabilityAnalysis, BomTarget, BomTargetVersionRange + +MOCK_TIMESTAMP: datetime = datetime(2021, 12, 31, 10, 0, 0, 0).replace(tzinfo=timezone.utc) +MOCK_UUID_1 = 'be2c6502-7e9a-47db-9a66-e34f729810a3' +MOCK_UUID_2 = '17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda' +MOCK_UUID_3 = '0b049d09-64c0-4490-a0f5-c84d9aacf857' +MOCK_UUID_4 = 'cd3e9c95-9d41-49e7-9924-8cf0465ae789' +MOCK_UUID_5 = 'bb5911d6-1a1d-41c9-b6e0-46e848d16655' +MOCK_UUID_6 = 'df70b5f1-8f53-47a4-be48-669ae78795e6' + +TEST_UUIDS = [ + MOCK_UUID_1, MOCK_UUID_2, MOCK_UUID_3, MOCK_UUID_4, MOCK_UUID_5, MOCK_UUID_6 +] + + +def get_bom_with_component_setuptools_basic() -> Bom: + return Bom(components=[get_component_setuptools_simple()]) + + +def get_bom_with_component_setuptools_with_cpe() -> Bom: + component = get_component_setuptools_simple() + component.cpe = 'cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*' + return Bom(components=[component]) + + +def get_bom_with_component_setuptools_no_component_version() -> Bom: + return Bom(components=[get_component_setuptools_simple_no_version()]) + + +def get_bom_with_component_setuptools_with_release_notes() -> Bom: + component = get_component_setuptools_simple() + component.release_notes = get_release_notes() + return Bom(components=[component]) + + +def get_bom_with_component_setuptools_complete() -> Bom: + component = get_component_setuptools_simple(bom_ref=MOCK_UUID_6) + component.supplier = get_org_entity_1() + component.publisher = 'CycloneDX' + component.description = 'This component is awesome' + component.scope = ComponentScope.REQUIRED + component.copyright = 'Apache 2.0 baby!' + component.cpe = 'cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*' + component.swid = get_swid_1() + component.pedigree = get_pedigree_1() + component.external_references.add( + get_external_reference_1() + ) + component.properties = get_properties_1() + component.components.update([ + get_component_setuptools_simple(), + get_component_toml_with_hashes_with_references() + ]) + component.evidence = ComponentEvidence(copyright_=[Copyright(text='Commercial'), Copyright(text='Commercial 2')]) + component.release_notes = get_release_notes() + return Bom(components=[component]) + + +def get_bom_with_component_setuptools_with_vulnerability() -> Bom: + bom = Bom() + component = get_component_setuptools_simple() + vulnerability = Vulnerability( + bom_ref='my-vuln-ref-1', id='CVE-2018-7489', source=get_vulnerability_source_nvd(), + references=[ + VulnerabilityReference(id='SOME-OTHER-ID', source=VulnerabilitySource( + name='OSS Index', url=XsUri('https://ossindex.sonatype.org/component/pkg:pypi/setuptools') + )) + ], + ratings=[ + VulnerabilityRating( + source=get_vulnerability_source_nvd(), score=Decimal(9.8), severity=VulnerabilitySeverity.CRITICAL, + method=VulnerabilityScoreSource.CVSS_V3, + vector='AN/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H', justification='Some justification' + ), + VulnerabilityRating( + source=get_vulnerability_source_owasp(), score=Decimal(2.7), severity=VulnerabilitySeverity.LOW, + method=VulnerabilityScoreSource.CVSS_V3, + vector='AV:L/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N', justification='Some other justification' + ) + ], + cwes=[22, 33], description='A description here', detail='Some detail here', + recommendation='Upgrade', + advisories=[ + VulnerabilityAdvisory(url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2018-7489')), + VulnerabilityAdvisory(url=XsUri('http://www.securitytracker.com/id/1040693')) + ], + created=datetime(year=2021, month=9, day=1, hour=10, minute=50, second=42, microsecond=51979, + tzinfo=timezone.utc), + published=datetime(year=2021, month=9, day=2, hour=10, minute=50, second=42, microsecond=51979, + tzinfo=timezone.utc), + updated=datetime(year=2021, month=9, day=3, hour=10, minute=50, second=42, microsecond=51979, + tzinfo=timezone.utc), + credits=VulnerabilityCredits( + organizations=[ + get_org_entity_1() + ], + individuals=[get_org_contact_2()] + ), + tools=[ + Tool(vendor='CycloneDX', name='cyclonedx-python-lib') + ], + analysis=VulnerabilityAnalysis( + state=ImpactAnalysisState.EXPLOITABLE, justification=ImpactAnalysisJustification.REQUIRES_ENVIRONMENT, + responses=[ImpactAnalysisResponse.CAN_NOT_FIX], detail='Some extra detail' + ), + affects_targets=[ + BomTarget( + ref=component.purl.to_string() if component.purl else None, + versions=[BomTargetVersionRange( + version_range='49.0.0 - 54.0.0', status=ImpactAnalysisAffectedStatus.AFFECTED + )] + ) + ] + ) + component.add_vulnerability(vulnerability=vulnerability) + bom.components.add(component) + return bom + + +def get_bom_with_component_toml_1() -> Bom: + return Bom(components=[get_component_toml_with_hashes_with_references()]) + + +def get_bom_just_complete_metadata() -> Bom: + bom = Bom() + bom.metadata.authors = [get_org_contact_1(), get_org_contact_2()] + bom.metadata.component = Component( + name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY + ) + bom.metadata.manufacture = get_org_entity_1() + bom.metadata.supplier = get_org_entity_2() + bom.metadata.licenses = [LicenseChoice(license_=License( + spdx_license_id='Apache-2.0', license_text=AttachedText( + content='VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=', encoding=Encoding.BASE_64 + ), license_url=XsUri('https://www.apache.org/licenses/LICENSE-2.0.txt') + ))] + bom.metadata.properties = get_properties_1() + return bom + + +def get_bom_with_external_references() -> Bom: + bom = Bom(external_references=[ + get_external_reference_1(), get_external_reference_2() + ]) + return bom + + +def get_bom_with_services_simple() -> Bom: + bom = Bom(services=[ + Service(name='my-first-service'), + Service(name='my-second-service') + ]) + bom.metadata.component = Component( + name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY + ) + return bom + + +def get_bom_with_services_complex() -> Bom: + bom = Bom(services=[ + Service( + name='my-first-service', bom_ref='my-specific-bom-ref-for-my-first-service', + provider=get_org_entity_1(), group='a-group', version='1.2.3', + description='Description goes here', endpoints=[ + XsUri('/api/thing/1'), + XsUri('/api/thing/2') + ], + authenticated=False, x_trust_boundary=True, data=[ + DataClassification(flow=DataFlow.OUTBOUND, classification='public') + ], + licenses=[ + LicenseChoice(license_expression='Commercial') + ], + external_references=[ + get_external_reference_1() + ], + properties=get_properties_1(), + release_notes=get_release_notes() + ), + Service(name='my-second-service') + ]) + bom.metadata.component = Component( + name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY + ) + return bom + + +def get_bom_with_nested_services() -> Bom: + bom = Bom(services=[ + Service( + name='my-first-service', bom_ref='my-specific-bom-ref-for-my-first-service', + provider=get_org_entity_1(), group='a-group', version='1.2.3', + description='Description goes here', endpoints=[ + XsUri('/api/thing/1'), + XsUri('/api/thing/2') + ], + authenticated=False, x_trust_boundary=True, data=[ + DataClassification(flow=DataFlow.OUTBOUND, classification='public') + ], + licenses=[ + LicenseChoice(license_expression='Commercial') + ], + external_references=[ + get_external_reference_1() + ], + properties=get_properties_1(), + services=[ + Service( + name='first-nested-service' + ), + Service( + name='second-nested-service', bom_ref='my-specific-bom-ref-for-second-nested-service', + provider=get_org_entity_1(), group='no-group', version='3.2.1', + authenticated=True, x_trust_boundary=False, + ) + ], + release_notes=get_release_notes() + ), + Service( + name='my-second-service', + services=[ + Service( + name='yet-another-nested-service', provider=get_org_entity_1(), group='what-group', version='6.5.4' + ), + Service( + name='another-nested-service', + bom_ref='my-specific-bom-ref-for-another-nested-service', + ) + ], + ) + ]) + bom.metadata.component = Component( + name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY + ) + return bom + + +def get_component_setuptools_simple(bom_ref: Optional[str] = None) -> Component: + return Component( + name='setuptools', version='50.3.2', + bom_ref=bom_ref or 'pkg:pypi/setuptools@50.3.2?extension=tar.gz', + purl=PackageURL( + type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' + ), license_str='MIT License', author='Test Author' + ) + + +def get_component_setuptools_simple_no_version(bom_ref: Optional[str] = None) -> Component: + return Component( + name='setuptools', bom_ref=bom_ref or 'pkg:pypi/setuptools?extension=tar.gz', + purl=PackageURL( + type='pypi', name='setuptools', qualifiers='extension=tar.gz' + ), license_str='MIT License', author='Test Author' + ) + + +def get_component_toml_with_hashes_with_references(bom_ref: Optional[str] = None) -> Component: + return Component( + name='toml', version='0.10.2', bom_ref=bom_ref or 'pkg:pypi/toml@0.10.2?extension=tar.gz', + purl=PackageURL( + type='pypi', name='toml', version='0.10.2', qualifiers='extension=tar.gz' + ), hashes=[ + HashType.from_composite_str('sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b') + ], external_references=[ + get_external_reference_1() + ] + ) + + +def get_external_reference_1() -> ExternalReference: + return ExternalReference( + reference_type=ExternalReferenceType.DISTRIBUTION, + url=XsUri('https://cyclonedx.org'), + comment='No comment', + hashes=[ + HashType.from_composite_str( + 'sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b') + ] + ) + + +def get_external_reference_2() -> ExternalReference: + return ExternalReference( + reference_type=ExternalReferenceType.WEBSITE, + url=XsUri('https://cyclonedx.org') + ) + + +def get_issue_1() -> IssueType: + return IssueType( + classification=IssueClassification.SECURITY, id_='CVE-2021-44228', name='Apache Log3Shell', + description='Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features...', + source=IssueTypeSource(name='NVD', url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2021-44228')), + references=[ + XsUri('https://logging.apache.org/log4j/2.x/security.html'), + XsUri('https://central.sonatype.org/news/20211213_log4shell_help') + ] + ) + + +def get_issue_2() -> IssueType: + return IssueType( + classification=IssueClassification.SECURITY, id_='CVE-2021-44229', name='Apache Log4Shell', + description='Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features...', + source=IssueTypeSource(name='NVD', url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2021-44228')), + references=[ + XsUri('https://logging.apache.org/log4j/2.x/security.html'), + XsUri('https://central.sonatype.org/news/20211213_log4shell_help') + ] + ) + + +def get_org_contact_1() -> OrganizationalContact: + return OrganizationalContact(name='Paul Horton', email='paul.horton@owasp.org') + + +def get_org_contact_2() -> OrganizationalContact: + return OrganizationalContact(name='A N Other', email='someone@somewhere.tld', phone='+44 (0)1234 567890') + + +def get_org_entity_1() -> OrganizationalEntity: + return OrganizationalEntity( + name='CycloneDX', urls=[XsUri('https://cyclonedx.org')], contacts=[get_org_contact_1(), get_org_contact_2()] + ) + + +def get_org_entity_2() -> OrganizationalEntity: + return OrganizationalEntity( + name='Cyclone DX', urls=[XsUri('https://cyclonedx.org/')], contacts=[get_org_contact_2()] + ) + + +def get_pedigree_1() -> Pedigree: + return Pedigree( + ancestors=[ + get_component_setuptools_simple(bom_ref='ccc8d7ee-4b9c-4750-aee0-a72585152291'), + get_component_setuptools_simple_no_version(bom_ref='8a3893b3-9923-4adb-a1d3-47456636ba0a') + ], + descendants=[ + get_component_setuptools_simple_no_version(bom_ref='28b2d8ce-def0-446f-a221-58dee0b44acc'), + get_component_toml_with_hashes_with_references(bom_ref='555ca729-93c6-48f3-956e-bdaa4a2f0bfa') + ], + variants=[ + get_component_toml_with_hashes_with_references(bom_ref='e7abdcca-5ba2-4f29-b2cf-b1e1ef788e66'), + get_component_setuptools_simple(bom_ref='ded1d73e-1fca-4302-b520-f1bc53979958') + ], + commits=[Commit(uid='a-random-uid', message="A commit message")], + patches=[Patch(type_=PatchClassification.BACKPORT)], + notes='Some notes here please' + ) + + +def get_properties_1() -> List[Property]: + return [ + Property(name='key1', value='val1'), + Property(name='key2', value='val2') + ] + + +def get_release_notes() -> ReleaseNotes: + text_content: str = base64.b64encode( + bytes('Some simple plain text', encoding='UTF-8') + ).decode(encoding='UTF-8') + + return ReleaseNotes( + type_='major', title="Release Notes Title", + featured_image=XsUri('https://cyclonedx.org/theme/assets/images/CycloneDX-Twitter-Card.png'), + social_image=XsUri('https://cyclonedx.org/cyclonedx-icon.png'), + description="This release is a test release", timestamp=MOCK_TIMESTAMP, + aliases=[ + "First Test Release" + ], + tags=['test', 'alpha'], + resolves=[get_issue_1()], + notes=[ + Note( + text=NoteText( + content=text_content, content_type='text/plain; charset=UTF-8', + content_encoding=Encoding.BASE_64 + ), locale='en-GB' + ), + Note( + text=NoteText( + content=text_content, content_type='text/plain; charset=UTF-8', + content_encoding=Encoding.BASE_64 + ), locale='en-US' + ) + ], + properties=get_properties_1() + ) + + +def get_swid_1() -> Swid: + return Swid( + tag_id='swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1', name='Test Application', + version='3.4.5', text=AttachedText( + content='PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbm' + 'FtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIg' + 'CiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dH' + 'A6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93' + 'd3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy' + '5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJh' + 'dG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaW' + 'Q9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg==', + content_type='text/xml', encoding=Encoding.BASE_64 + ) + ) + + +def get_swid_2() -> Swid: + return Swid( + tag_id='swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1', name='Test Application', + version='3.4.5', url=XsUri('https://cyclonedx.org') + ) + + +def get_vulnerability_source_nvd() -> VulnerabilitySource: + return VulnerabilitySource(name='NVD', url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2018-7489')) + + +def get_vulnerability_source_owasp() -> VulnerabilitySource: + return VulnerabilitySource(name='OWASP', url=XsUri('https://owasp.org')) diff --git a/tests/fixtures/json/1.2/bom_external_references.json b/tests/fixtures/json/1.2/bom_external_references.json new file mode 100644 index 00000000..78e0db56 --- /dev/null +++ b/tests/fixtures/json/1.2/bom_external_references.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION" + } + ] + }, + "externalReferences": [ + { + "url": "https://cyclonedx.org", + "comment": "No comment", + "type": "distribution" + }, + { + "url": "https://cyclonedx.org", + "type": "website" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/json/1.2/bom_services_complex.json b/tests/fixtures/json/1.2/bom_services_complex.json new file mode 100644 index 00000000..8cc31cd2 --- /dev/null +++ b/tests/fixtures/json/1.2/bom_services_complex.json @@ -0,0 +1,83 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION" + } + ], + "component": { + "type": "library", + "bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda", + "name": "cyclonedx-python-lib", + "version": "1.0.0" + } + }, + "services": [ + { + "bom-ref": "my-specific-bom-ref-for-my-first-service", + "provider": { + "contact": [ + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + }, + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ] + }, + "group": "a-group", + "name": "my-first-service", + "version": "1.2.3", + "description": "Description goes here", + "endpoints": [ + "/api/thing/1", + "/api/thing/2" + ], + "authenticated": false, + "x-trust-boundary": true, + "data": [ + { + "classification": "public", + "flow": "outbound" + } + ], + "licenses": [ + { + "expression": "Commercial" + } + ], + "externalReferences": [ + { + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "type": "distribution", + "url": "https://cyclonedx.org" + } + ] + }, + { + "bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3", + "name": "my-second-service" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/json/1.2/bom_services_nested.json b/tests/fixtures/json/1.2/bom_services_nested.json new file mode 100644 index 00000000..46de1b3b --- /dev/null +++ b/tests/fixtures/json/1.2/bom_services_nested.json @@ -0,0 +1,143 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json", + "bomFormat": "CycloneDX", + "metadata": { + "component": { + "bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789", + "name": "cyclonedx-python-lib", + "type": "library", + "version": "1.0.0" + }, + "timestamp": "2022-01-27T16:16:35.622354+00:00", + "tools": [ + { + "name": "cyclonedx-python-lib", + "vendor": "CycloneDX", + "version": "0.11.0" + } + ] + }, + "serialNumber": "urn:uuid:1d2c4529-8cf8-447d-b2a1-e4ebb610adb9", + "services": [ + { + "authenticated": false, + "bom-ref": "my-specific-bom-ref-for-my-first-service", + "data": [ + { + "classification": "public", + "flow": "outbound" + } + ], + "description": "Description goes here", + "endpoints": [ + "/api/thing/1", + "/api/thing/2" + ], + "externalReferences": [ + { + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "type": "distribution", + "url": "https://cyclonedx.org" + } + ], + "group": "a-group", + "licenses": [ + { + "expression": "Commercial" + } + ], + "name": "my-first-service", + "provider": { + "contact": [ + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + }, + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ] + }, + "services": [ + { + "bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3", + "name": "first-nested-service" + }, + { + "authenticated": true, + "bom-ref": "my-specific-bom-ref-for-second-nested-service", + "group": "no-group", + "name": "second-nested-service", + "provider": { + "contact": [ + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + }, + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ] + }, + "version": "3.2.1", + "x-trust-boundary": false + } + ], + "version": "1.2.3", + "x-trust-boundary": true + }, + { + "bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857", + "name": "my-second-service", + "services": [ + { + "bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda", + "group": "what-group", + "name": "yet-another-nested-service", + "provider": { + "contact": [ + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + }, + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ] + }, + "version": "6.5.4" + }, + { + "bom-ref": "my-specific-bom-ref-for-another-nested-service", + "name": "another-nested-service" + } + ] + } + ], + "specVersion": "1.2", + "version": 1 +} diff --git a/tests/fixtures/json/1.2/bom_services_simple.json b/tests/fixtures/json/1.2/bom_services_simple.json new file mode 100644 index 00000000..db950b0e --- /dev/null +++ b/tests/fixtures/json/1.2/bom_services_simple.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION" + } + ], + "component": { + "type": "library", + "bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857", + "name": "cyclonedx-python-lib", + "version": "1.0.0" + } + }, + "services": [ + { + "bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3", + "name": "my-first-service" + }, + { + "bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda", + "name": "my-second-service" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/bom_v1.2_setuptools.json b/tests/fixtures/json/1.2/bom_setuptools.json similarity index 88% rename from tests/fixtures/bom_v1.2_setuptools.json rename to tests/fixtures/json/1.2/bom_setuptools.json index 5fa21c83..7f674f79 100644 --- a/tests/fixtures/bom_v1.2_setuptools.json +++ b/tests/fixtures/json/1.2/bom_setuptools.json @@ -21,6 +21,11 @@ "author": "Test Author", "name": "setuptools", "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" } ] diff --git a/tests/fixtures/json/1.2/bom_setuptools_complete.json b/tests/fixtures/json/1.2/bom_setuptools_complete.json new file mode 100644 index 00000000..42288aa2 --- /dev/null +++ b/tests/fixtures/json/1.2/bom_setuptools_complete.json @@ -0,0 +1,218 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION" + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "df70b5f1-8f53-47a4-be48-669ae78795e6", + "supplier": { + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ], + "contact": [ + { + "name": "Paul Horton", + "email": "paul.horton@owasp.org" + }, + { + "name": "A N Other", + "email": "someone@somewhere.tld", + "phone": "+44 (0)1234 567890" + } + ] + }, + "author": "Test Author", + "publisher": "CycloneDX", + "name": "setuptools", + "version": "50.3.2", + "description": "This component is awesome", + "scope": "required", + "licenses": [ + { + "expression": "MIT License" + } + ], + "copyright": "Apache 2.0 baby!", + "cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*", + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", + "swid": { + "tagId": "swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1", + "name": "Test Application", + "version": "3.4.5", + "text": { + "contentType": "text/xml", + "encoding": "base64", + "content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg==" + } + }, + "pedigree": { + "ancestors": [ + { + "type": "library", + "bom-ref": "ccc8d7ee-4b9c-4750-aee0-a72585152291", + "author": "Test Author", + "name": "setuptools", + "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" + }, + { + "type": "library", + "bom-ref": "8a3893b3-9923-4adb-a1d3-47456636ba0a", + "author": "Test Author", + "name": "setuptools", + "version": "", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools?extension=tar.gz" + } + ], + "descendants": [ + { + "type": "library", + "bom-ref": "28b2d8ce-def0-446f-a221-58dee0b44acc", + "author": "Test Author", + "name": "setuptools", + "version": "", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools?extension=tar.gz" + }, + { + "type": "library", + "bom-ref": "555ca729-93c6-48f3-956e-bdaa4a2f0bfa", + "name": "toml", + "version": "0.10.2", + "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment" + } + ] + } + ], + "variants": [ + { + "type": "library", + "bom-ref": "e7abdcca-5ba2-4f29-b2cf-b1e1ef788e66", + "name": "toml", + "version": "0.10.2", + "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment" + } + ] + }, + { + "type": "library", + "bom-ref": "ded1d73e-1fca-4302-b520-f1bc53979958", + "author": "Test Author", + "name": "setuptools", + "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" + } + ], + "commits": [ + { + "uid": "a-random-uid", + "message": "A commit message" + } + ], + "patches": [ + { + "type": "backport" + } + ], + "notes": "Some notes here please" + }, + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment" + } + ], + "components": [ + { + "type": "library", + "bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", + "author": "Test Author", + "name": "setuptools", + "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" + }, + { + "type": "library", + "bom-ref": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "name": "toml", + "version": "0.10.2", + "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/bom_v1.2_setuptools_with_cpe.json b/tests/fixtures/json/1.2/bom_setuptools_with_cpe.json similarity index 89% rename from tests/fixtures/bom_v1.2_setuptools_with_cpe.json rename to tests/fixtures/json/1.2/bom_setuptools_with_cpe.json index 78d2c8fe..e4c0cf92 100644 --- a/tests/fixtures/bom_v1.2_setuptools_with_cpe.json +++ b/tests/fixtures/json/1.2/bom_setuptools_with_cpe.json @@ -21,6 +21,11 @@ "author": "Test Author", "name": "setuptools", "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], "cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*", "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" } diff --git a/tests/fixtures/json/1.2/bom_toml_1.json b/tests/fixtures/json/1.2/bom_toml_1.json new file mode 100644 index 00000000..0e07e956 --- /dev/null +++ b/tests/fixtures/json/1.2/bom_toml_1.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION" + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "name": "toml", + "version": "0.10.2", + "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/json/1.2/bom_with_full_metadata.json b/tests/fixtures/json/1.2/bom_with_full_metadata.json new file mode 100644 index 00000000..d479798b --- /dev/null +++ b/tests/fixtures/json/1.2/bom_with_full_metadata.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.2", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION" + } + ], + "authors": [ + { + "name": "Paul Horton", + "email": "paul.horton@owasp.org" + }, + { + "name": "A N Other", + "email": "someone@somewhere.tld", + "phone": "+44 (0)1234 567890" + } + ], + "component": { + "bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857", + "type": "library", + "name": "cyclonedx-python-lib", + "version": "1.0.0" + }, + "manufacture": { + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ], + "contact": [ + { + "name": "Paul Horton", + "email": "paul.horton@owasp.org" + }, + { + "name": "A N Other", + "email": "someone@somewhere.tld", + "phone": "+44 (0)1234 567890" + } + ] + }, + "supplier": { + "name": "Cyclone DX", + "url": [ + "https://cyclonedx.org/" + ], + "contact": [ + { + "name": "A N Other", + "email": "someone@somewhere.tld", + "phone": "+44 (0)1234 567890" + } + ] + } + } +} \ No newline at end of file diff --git a/tests/fixtures/bom_v1.3_toml_with_component_hashes.json b/tests/fixtures/json/1.3/bom_external_references.json similarity index 74% rename from tests/fixtures/bom_v1.3_toml_with_component_hashes.json rename to tests/fixtures/json/1.3/bom_external_references.json index a9f7ef64..9ce40c64 100644 --- a/tests/fixtures/bom_v1.3_toml_with_component_hashes.json +++ b/tests/fixtures/json/1.3/bom_external_references.json @@ -14,19 +14,21 @@ } ] }, - "components": [ + "externalReferences": [ { - "type": "library", - "bom-ref": "pkg:pypi/toml@0.10.2?extension=tar.gz", - "name": "toml", - "version": "0.10.2", - "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "url": "https://cyclonedx.org", + "comment": "No comment", + "type": "distribution", "hashes": [ { "alg": "SHA-256", "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" } ] + }, + { + "url": "https://cyclonedx.org", + "type": "website" } ] } \ No newline at end of file diff --git a/tests/fixtures/json/1.3/bom_services_complex.json b/tests/fixtures/json/1.3/bom_services_complex.json new file mode 100644 index 00000000..64460386 --- /dev/null +++ b/tests/fixtures/json/1.3/bom_services_complex.json @@ -0,0 +1,93 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.3", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION" + } + ], + "component": { + "type": "library", + "bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda", + "name": "cyclonedx-python-lib", + "version": "1.0.0" + } + }, + "services": [ + { + "bom-ref": "my-specific-bom-ref-for-my-first-service", + "provider": { + "contact": [ + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + }, + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ] + }, + "group": "a-group", + "name": "my-first-service", + "version": "1.2.3", + "description": "Description goes here", + "endpoints": [ + "/api/thing/1", + "/api/thing/2" + ], + "authenticated": false, + "x-trust-boundary": true, + "data": [ + { + "classification": "public", + "flow": "outbound" + } + ], + "licenses": [ + { + "expression": "Commercial" + } + ], + "externalReferences": [ + { + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "type": "distribution", + "url": "https://cyclonedx.org" + } + ], + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ] + }, + { + "bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3", + "name": "my-second-service" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/json/1.3/bom_services_nested.json b/tests/fixtures/json/1.3/bom_services_nested.json new file mode 100644 index 00000000..216e52e0 --- /dev/null +++ b/tests/fixtures/json/1.3/bom_services_nested.json @@ -0,0 +1,153 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json", + "bomFormat": "CycloneDX", + "metadata": { + "component": { + "bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789", + "name": "cyclonedx-python-lib", + "type": "library", + "version": "1.0.0" + }, + "timestamp": "2022-01-27T16:16:35.622354+00:00", + "tools": [ + { + "name": "cyclonedx-python-lib", + "vendor": "CycloneDX", + "version": "0.11.0" + } + ] + }, + "serialNumber": "urn:uuid:1d2c4529-8cf8-447d-b2a1-e4ebb610adb9", + "services": [ + { + "authenticated": false, + "bom-ref": "my-specific-bom-ref-for-my-first-service", + "data": [ + { + "classification": "public", + "flow": "outbound" + } + ], + "description": "Description goes here", + "endpoints": [ + "/api/thing/1", + "/api/thing/2" + ], + "externalReferences": [ + { + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "type": "distribution", + "url": "https://cyclonedx.org" + } + ], + "group": "a-group", + "licenses": [ + { + "expression": "Commercial" + } + ], + "name": "my-first-service", + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "provider": { + "contact": [ + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + }, + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ] + }, + "services": [ + { + "bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3", + "name": "first-nested-service" + }, + { + "authenticated": true, + "bom-ref": "my-specific-bom-ref-for-second-nested-service", + "group": "no-group", + "name": "second-nested-service", + "provider": { + "contact": [ + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + }, + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ] + }, + "version": "3.2.1", + "x-trust-boundary": false + } + ], + "version": "1.2.3", + "x-trust-boundary": true + }, + { + "bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857", + "name": "my-second-service", + "services": [ + { + "bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda", + "group": "what-group", + "name": "yet-another-nested-service", + "provider": { + "contact": [ + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + }, + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ] + }, + "version": "6.5.4" + }, + { + "bom-ref": "my-specific-bom-ref-for-another-nested-service", + "name": "another-nested-service" + } + ] + } + ], + "specVersion": "1.3", + "version": 1 +} diff --git a/tests/fixtures/bom_v1.3_with_metadata_component.json b/tests/fixtures/json/1.3/bom_services_simple.json similarity index 72% rename from tests/fixtures/bom_v1.3_with_metadata_component.json rename to tests/fixtures/json/1.3/bom_services_simple.json index 7290dfc7..31bb109b 100644 --- a/tests/fixtures/bom_v1.3_with_metadata_component.json +++ b/tests/fixtures/json/1.3/bom_services_simple.json @@ -14,11 +14,20 @@ } ], "component": { - "bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3", "type": "library", + "bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857", "name": "cyclonedx-python-lib", "version": "1.0.0" } }, - "components": [] + "services": [ + { + "bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3", + "name": "my-first-service" + }, + { + "bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda", + "name": "my-second-service" + } + ] } \ No newline at end of file diff --git a/tests/fixtures/bom_v1.3_setuptools.json b/tests/fixtures/json/1.3/bom_setuptools.json similarity index 86% rename from tests/fixtures/bom_v1.3_setuptools.json rename to tests/fixtures/json/1.3/bom_setuptools.json index 38cf3f7a..4de582cc 100644 --- a/tests/fixtures/bom_v1.3_setuptools.json +++ b/tests/fixtures/json/1.3/bom_setuptools.json @@ -17,15 +17,16 @@ "components": [ { "type": "library", + "bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", + "author": "Test Author", "name": "setuptools", "version": "50.3.2", - "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", - "bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", "licenses": [ { "expression": "MIT License" } - ] + ], + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" } ] } \ No newline at end of file diff --git a/tests/fixtures/json/1.3/bom_setuptools_complete.json b/tests/fixtures/json/1.3/bom_setuptools_complete.json new file mode 100644 index 00000000..813cd334 --- /dev/null +++ b/tests/fixtures/json/1.3/bom_setuptools_complete.json @@ -0,0 +1,262 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.3", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION" + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "df70b5f1-8f53-47a4-be48-669ae78795e6", + "supplier": { + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ], + "contact": [ + { + "name": "Paul Horton", + "email": "paul.horton@owasp.org" + }, + { + "name": "A N Other", + "email": "someone@somewhere.tld", + "phone": "+44 (0)1234 567890" + } + ] + }, + "author": "Test Author", + "publisher": "CycloneDX", + "name": "setuptools", + "version": "50.3.2", + "description": "This component is awesome", + "scope": "required", + "licenses": [ + { + "expression": "MIT License" + } + ], + "copyright": "Apache 2.0 baby!", + "cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*", + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", + "swid": { + "tagId": "swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1", + "name": "Test Application", + "version": "3.4.5", + "text": { + "contentType": "text/xml", + "encoding": "base64", + "content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg==" + } + }, + "pedigree": { + "ancestors": [ + { + "type": "library", + "bom-ref": "ccc8d7ee-4b9c-4750-aee0-a72585152291", + "author": "Test Author", + "name": "setuptools", + "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" + }, + { + "type": "library", + "bom-ref": "8a3893b3-9923-4adb-a1d3-47456636ba0a", + "author": "Test Author", + "name": "setuptools", + "version": "", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools?extension=tar.gz" + } + ], + "descendants": [ + { + "type": "library", + "bom-ref": "28b2d8ce-def0-446f-a221-58dee0b44acc", + "author": "Test Author", + "name": "setuptools", + "version": "", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools?extension=tar.gz" + }, + { + "type": "library", + "bom-ref": "555ca729-93c6-48f3-956e-bdaa4a2f0bfa", + "name": "toml", + "version": "0.10.2", + "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ] + } + ] + } + ], + "variants": [ + { + "type": "library", + "bom-ref": "e7abdcca-5ba2-4f29-b2cf-b1e1ef788e66", + "name": "toml", + "version": "0.10.2", + "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ] + } + ] + }, + { + "type": "library", + "bom-ref": "ded1d73e-1fca-4302-b520-f1bc53979958", + "author": "Test Author", + "name": "setuptools", + "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" + } + ], + "commits": [ + { + "uid": "a-random-uid", + "message": "A commit message" + } + ], + "patches": [ + { + "type": "backport" + } + ], + "notes": "Some notes here please" + }, + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ] + } + ], + "components": [ + { + "type": "library", + "bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", + "author": "Test Author", + "name": "setuptools", + "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" + }, + { + "type": "library", + "bom-ref": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "name": "toml", + "version": "0.10.2", + "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ] + } + ] + } + ], + "evidence": { + "copyright": [ + { + "text": "Commercial" + }, + { + "text": "Commercial 2" + } + ] + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/bom_v1.3_toml_with_component_license.json b/tests/fixtures/json/1.3/bom_setuptools_no_version.json similarity index 73% rename from tests/fixtures/bom_v1.3_toml_with_component_license.json rename to tests/fixtures/json/1.3/bom_setuptools_no_version.json index 3af1102a..971a6462 100644 --- a/tests/fixtures/bom_v1.3_toml_with_component_license.json +++ b/tests/fixtures/json/1.3/bom_setuptools_no_version.json @@ -17,15 +17,16 @@ "components": [ { "type": "library", - "bom-ref": "pkg:pypi/toml@0.10.2?extension=tar.gz", - "name": "toml", - "version": "0.10.2", - "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "bom-ref": "pkg:pypi/setuptools?extension=tar.gz", + "author": "Test Author", + "name": "setuptools", + "version": "", "licenses": [ { "expression": "MIT License" } - ] + ], + "purl": "pkg:pypi/setuptools?extension=tar.gz" } ] } \ No newline at end of file diff --git a/tests/fixtures/bom_v1.3_setuptools_with_cpe.json b/tests/fixtures/json/1.3/bom_setuptools_with_cpe.json similarity index 88% rename from tests/fixtures/bom_v1.3_setuptools_with_cpe.json rename to tests/fixtures/json/1.3/bom_setuptools_with_cpe.json index 14f551f7..c39b5fd4 100644 --- a/tests/fixtures/bom_v1.3_setuptools_with_cpe.json +++ b/tests/fixtures/json/1.3/bom_setuptools_with_cpe.json @@ -17,16 +17,17 @@ "components": [ { "type": "library", + "bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", + "author": "Test Author", "name": "setuptools", "version": "50.3.2", - "cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*", - "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", - "bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", "licenses": [ { "expression": "MIT License" } - ] + ], + "cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*", + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" } ] } \ No newline at end of file diff --git a/tests/fixtures/bom_v1.3_toml_with_component_external_references.json b/tests/fixtures/json/1.3/bom_toml_1.json similarity index 100% rename from tests/fixtures/bom_v1.3_toml_with_component_external_references.json rename to tests/fixtures/json/1.3/bom_toml_1.json diff --git a/tests/fixtures/json/1.3/bom_with_full_metadata.json b/tests/fixtures/json/1.3/bom_with_full_metadata.json new file mode 100644 index 00000000..f23a5703 --- /dev/null +++ b/tests/fixtures/json/1.3/bom_with_full_metadata.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.3", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION" + } + ], + "authors": [ + { + "name": "Paul Horton", + "email": "paul.horton@owasp.org" + }, + { + "name": "A N Other", + "email": "someone@somewhere.tld", + "phone": "+44 (0)1234 567890" + } + ], + "component": { + "bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda", + "type": "library", + "name": "cyclonedx-python-lib", + "version": "1.0.0" + }, + "manufacture": { + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ], + "contact": [ + { + "name": "Paul Horton", + "email": "paul.horton@owasp.org" + }, + { + "name": "A N Other", + "email": "someone@somewhere.tld", + "phone": "+44 (0)1234 567890" + } + ] + }, + "supplier": { + "name": "Cyclone DX", + "url": [ + "https://cyclonedx.org/" + ], + "contact": [ + { + "name": "A N Other", + "email": "someone@somewhere.tld", + "phone": "+44 (0)1234 567890" + } + ] + }, + "licenses": [ + { + "license": { + "id": "Apache-2.0", + "text": { + "contentType": "text/plain", + "encoding": "base64", + "content": "VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=" + }, + "url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + ], + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/json/1.4/bom_external_references.json b/tests/fixtures/json/1.4/bom_external_references.json new file mode 100644 index 00000000..84d69448 --- /dev/null +++ b/tests/fixtures/json/1.4/bom_external_references.json @@ -0,0 +1,68 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION", + "externalReferences": [ + { + "type": "build-system", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions" + }, + { + "type": "distribution", + "url": "https://pypi.org/project/cyclonedx-python-lib/" + }, + { + "type": "documentation", + "url": "https://cyclonedx.github.io/cyclonedx-python-lib/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues" + }, + { + "type": "license", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE" + }, + { + "type": "release-notes", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib" + }, + { + "type": "website", + "url": "https://cyclonedx.org" + } + ] + } + ] + }, + "externalReferences": [ + { + "url": "https://cyclonedx.org", + "comment": "No comment", + "type": "distribution", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ] + }, + { + "url": "https://cyclonedx.org", + "type": "website" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/json/1.4/bom_services_complex.json b/tests/fixtures/json/1.4/bom_services_complex.json new file mode 100644 index 00000000..656a8bbf --- /dev/null +++ b/tests/fixtures/json/1.4/bom_services_complex.json @@ -0,0 +1,186 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION", + "externalReferences": [ + { + "type": "build-system", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions" + }, + { + "type": "distribution", + "url": "https://pypi.org/project/cyclonedx-python-lib/" + }, + { + "type": "documentation", + "url": "https://cyclonedx.github.io/cyclonedx-python-lib/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues" + }, + { + "type": "license", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE" + }, + { + "type": "release-notes", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib" + }, + { + "type": "website", + "url": "https://cyclonedx.org" + } + ] + } + ], + "component": { + "type": "library", + "bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda", + "name": "cyclonedx-python-lib", + "version": "1.0.0" + } + }, + "services": [ + { + "bom-ref": "my-specific-bom-ref-for-my-first-service", + "provider": { + "contact": [ + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + }, + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ] + }, + "group": "a-group", + "name": "my-first-service", + "version": "1.2.3", + "description": "Description goes here", + "endpoints": [ + "/api/thing/1", + "/api/thing/2" + ], + "authenticated": false, + "x-trust-boundary": true, + "data": [ + { + "classification": "public", + "flow": "outbound" + } + ], + "licenses": [ + { + "expression": "Commercial" + } + ], + "externalReferences": [ + { + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "type": "distribution", + "url": "https://cyclonedx.org" + } + ], + "releaseNotes": { + "aliases": [ + "First Test Release" + ], + "description": "This release is a test release", + "featuredImage": "https://cyclonedx.org/theme/assets/images/CycloneDX-Twitter-Card.png", + "notes": [ + { + "locale": "en-GB", + "text": { + "content": "U29tZSBzaW1wbGUgcGxhaW4gdGV4dA==", + "contentType": "text/plain; charset=UTF-8", + "encoding": "base64" + } + }, + { + "locale": "en-US", + "text": { + "content": "U29tZSBzaW1wbGUgcGxhaW4gdGV4dA==", + "contentType": "text/plain; charset=UTF-8", + "encoding": "base64" + } + } + ], + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "resolves": [ + { + "description": "Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features...", + "id": "CVE-2021-44228", + "name": "Apache Log3Shell", + "references": [ + "https://logging.apache.org/log4j/2.x/security.html", + "https://central.sonatype.org/news/20211213_log4shell_help" + ], + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228" + }, + "type": "security" + } + ], + "socialImage": "https://cyclonedx.org/cyclonedx-icon.png", + "tags": [ + "test", + "alpha" + ], + "timestamp": "2021-12-31T10:00:00+00:00", + "title": "Release Notes Title", + "type": "major" + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ] + }, + { + "bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3", + "name": "my-second-service" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/json/1.4/bom_services_nested.json b/tests/fixtures/json/1.4/bom_services_nested.json new file mode 100644 index 00000000..e0e01221 --- /dev/null +++ b/tests/fixtures/json/1.4/bom_services_nested.json @@ -0,0 +1,246 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "metadata": { + "component": { + "bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789", + "name": "cyclonedx-python-lib", + "type": "library", + "version": "1.0.0" + }, + "timestamp": "2022-01-27T16:16:35.622354+00:00", + "tools": [ + { + "externalReferences": [ + { + "type": "build-system", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions" + }, + { + "type": "distribution", + "url": "https://pypi.org/project/cyclonedx-python-lib/" + }, + { + "type": "documentation", + "url": "https://cyclonedx.github.io/cyclonedx-python-lib/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues" + }, + { + "type": "license", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE" + }, + { + "type": "release-notes", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib" + }, + { + "type": "website", + "url": "https://cyclonedx.org" + } + ], + "name": "cyclonedx-python-lib", + "vendor": "CycloneDX", + "version": "0.11.0" + } + ] + }, + "serialNumber": "urn:uuid:1d2c4529-8cf8-447d-b2a1-e4ebb610adb9", + "services": [ + { + "authenticated": false, + "bom-ref": "my-specific-bom-ref-for-my-first-service", + "data": [ + { + "classification": "public", + "flow": "outbound" + } + ], + "description": "Description goes here", + "endpoints": [ + "/api/thing/1", + "/api/thing/2" + ], + "externalReferences": [ + { + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "type": "distribution", + "url": "https://cyclonedx.org" + } + ], + "group": "a-group", + "licenses": [ + { + "expression": "Commercial" + } + ], + "name": "my-first-service", + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "provider": { + "contact": [ + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + }, + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ] + }, + "releaseNotes": { + "aliases": [ + "First Test Release" + ], + "description": "This release is a test release", + "featuredImage": "https://cyclonedx.org/theme/assets/images/CycloneDX-Twitter-Card.png", + "notes": [ + { + "locale": "en-GB", + "text": { + "content": "U29tZSBzaW1wbGUgcGxhaW4gdGV4dA==", + "contentType": "text/plain; charset=UTF-8", + "encoding": "base64" + } + }, + { + "locale": "en-US", + "text": { + "content": "U29tZSBzaW1wbGUgcGxhaW4gdGV4dA==", + "contentType": "text/plain; charset=UTF-8", + "encoding": "base64" + } + } + ], + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "resolves": [ + { + "description": "Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features...", + "id": "CVE-2021-44228", + "name": "Apache Log3Shell", + "references": [ + "https://logging.apache.org/log4j/2.x/security.html", + "https://central.sonatype.org/news/20211213_log4shell_help" + ], + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228" + }, + "type": "security" + } + ], + "socialImage": "https://cyclonedx.org/cyclonedx-icon.png", + "tags": [ + "test", + "alpha" + ], + "timestamp": "2021-12-31T10:00:00+00:00", + "title": "Release Notes Title", + "type": "major" + }, + "services": [ + { + "bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3", + "name": "first-nested-service" + }, + { + "authenticated": true, + "bom-ref": "my-specific-bom-ref-for-second-nested-service", + "group": "no-group", + "name": "second-nested-service", + "provider": { + "contact": [ + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + }, + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ] + }, + "version": "3.2.1", + "x-trust-boundary": false + } + ], + "version": "1.2.3", + "x-trust-boundary": true + }, + { + "bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857", + "name": "my-second-service", + "services": [ + { + "bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda", + "group": "what-group", + "name": "yet-another-nested-service", + "provider": { + "contact": [ + { + "email": "paul.horton@owasp.org", + "name": "Paul Horton" + }, + { + "email": "someone@somewhere.tld", + "name": "A N Other", + "phone": "+44 (0)1234 567890" + } + ], + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ] + }, + "version": "6.5.4" + }, + { + "bom-ref": "my-specific-bom-ref-for-another-nested-service", + "name": "another-nested-service" + } + ] + } + ], + "specVersion": "1.4", + "version": 1 +} diff --git a/tests/fixtures/json/1.4/bom_services_simple.json b/tests/fixtures/json/1.4/bom_services_simple.json new file mode 100644 index 00000000..c23f4c63 --- /dev/null +++ b/tests/fixtures/json/1.4/bom_services_simple.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION", + "externalReferences": [ + { + "type": "build-system", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions" + }, + { + "type": "distribution", + "url": "https://pypi.org/project/cyclonedx-python-lib/" + }, + { + "type": "documentation", + "url": "https://cyclonedx.github.io/cyclonedx-python-lib/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues" + }, + { + "type": "license", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE" + }, + { + "type": "release-notes", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib" + }, + { + "type": "website", + "url": "https://cyclonedx.org" + } + ] + } + ], + "component": { + "type": "library", + "bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857", + "name": "cyclonedx-python-lib", + "version": "1.0.0" + } + }, + "services": [ + { + "bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3", + "name": "my-first-service" + }, + { + "bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda", + "name": "my-second-service" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/bom_v1.4_setuptools_no_version.json b/tests/fixtures/json/1.4/bom_setuptools.json similarity index 85% rename from tests/fixtures/bom_v1.4_setuptools_no_version.json rename to tests/fixtures/json/1.4/bom_setuptools.json index de51e77e..ea89697a 100644 --- a/tests/fixtures/bom_v1.4_setuptools_no_version.json +++ b/tests/fixtures/json/1.4/bom_setuptools.json @@ -51,9 +51,16 @@ "components": [ { "type": "library", + "bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", + "author": "Test Author", "name": "setuptools", - "purl": "pkg:pypi/setuptools?extension=tar.gz", - "bom-ref": "pkg:pypi/setuptools?extension=tar.gz" + "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" } ] } \ No newline at end of file diff --git a/tests/fixtures/json/1.4/bom_setuptools_complete.json b/tests/fixtures/json/1.4/bom_setuptools_complete.json new file mode 100644 index 00000000..b224cee0 --- /dev/null +++ b/tests/fixtures/json/1.4/bom_setuptools_complete.json @@ -0,0 +1,353 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION", + "externalReferences": [ + { + "type": "build-system", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions" + }, + { + "type": "distribution", + "url": "https://pypi.org/project/cyclonedx-python-lib/" + }, + { + "type": "documentation", + "url": "https://cyclonedx.github.io/cyclonedx-python-lib/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues" + }, + { + "type": "license", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE" + }, + { + "type": "release-notes", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib" + }, + { + "type": "website", + "url": "https://cyclonedx.org" + } + ] + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "df70b5f1-8f53-47a4-be48-669ae78795e6", + "supplier": { + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ], + "contact": [ + { + "name": "Paul Horton", + "email": "paul.horton@owasp.org" + }, + { + "name": "A N Other", + "email": "someone@somewhere.tld", + "phone": "+44 (0)1234 567890" + } + ] + }, + "author": "Test Author", + "publisher": "CycloneDX", + "name": "setuptools", + "version": "50.3.2", + "description": "This component is awesome", + "scope": "required", + "licenses": [ + { + "expression": "MIT License" + } + ], + "copyright": "Apache 2.0 baby!", + "cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*", + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", + "swid": { + "tagId": "swidgen-242eb18a-503e-ca37-393b-cf156ef09691_9.1.1", + "name": "Test Application", + "version": "3.4.5", + "text": { + "contentType": "text/xml", + "encoding": "base64", + "content": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg==" + } + }, + "pedigree": { + "ancestors": [ + { + "type": "library", + "bom-ref": "ccc8d7ee-4b9c-4750-aee0-a72585152291", + "author": "Test Author", + "name": "setuptools", + "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" + }, + { + "type": "library", + "bom-ref": "8a3893b3-9923-4adb-a1d3-47456636ba0a", + "author": "Test Author", + "name": "setuptools", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools?extension=tar.gz" + } + ], + "descendants": [ + { + "type": "library", + "bom-ref": "28b2d8ce-def0-446f-a221-58dee0b44acc", + "author": "Test Author", + "name": "setuptools", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools?extension=tar.gz" + }, + { + "type": "library", + "bom-ref": "555ca729-93c6-48f3-956e-bdaa4a2f0bfa", + "name": "toml", + "version": "0.10.2", + "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ] + } + ] + } + ], + "variants": [ + { + "type": "library", + "bom-ref": "e7abdcca-5ba2-4f29-b2cf-b1e1ef788e66", + "name": "toml", + "version": "0.10.2", + "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ] + } + ] + }, + { + "type": "library", + "bom-ref": "ded1d73e-1fca-4302-b520-f1bc53979958", + "author": "Test Author", + "name": "setuptools", + "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" + } + ], + "commits": [ + { + "uid": "a-random-uid", + "message": "A commit message" + } + ], + "patches": [ + { + "type": "backport" + } + ], + "notes": "Some notes here please" + }, + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ] + } + ], + "components": [ + { + "type": "library", + "bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", + "author": "Test Author", + "name": "setuptools", + "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" + }, + { + "type": "library", + "bom-ref": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "name": "toml", + "version": "0.10.2", + "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ] + } + ] + } + ], + "evidence": { + "copyright": [ + { + "text": "Commercial" + }, + { + "text": "Commercial 2" + } + ] + }, + "releaseNotes": { + "type": "major", + "title": "Release Notes Title", + "featuredImage": "https://cyclonedx.org/theme/assets/images/CycloneDX-Twitter-Card.png", + "socialImage": "https://cyclonedx.org/cyclonedx-icon.png", + "description": "This release is a test release", + "timestamp": "2021-12-31T10:00:00+00:00", + "aliases": [ + "First Test Release" + ], + "tags": [ + "test", + "alpha" + ], + "resolves": [ + { + "type": "security", + "id": "CVE-2021-44228", + "name": "Apache Log3Shell", + "description": "Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features...", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228" + }, + "references": [ + "https://logging.apache.org/log4j/2.x/security.html", + "https://central.sonatype.org/news/20211213_log4shell_help" + ] + } + ], + "notes": [ + { + "locale": "en-GB", + "text": { + "contentType": "text/plain; charset=UTF-8", + "encoding": "base64", + "content": "U29tZSBzaW1wbGUgcGxhaW4gdGV4dA==" + } + }, + { + "locale": "en-US", + "text": { + "contentType": "text/plain; charset=UTF-8", + "encoding": "base64", + "content": "U29tZSBzaW1wbGUgcGxhaW4gdGV4dA==" + } + } + ], + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ] + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/bom_v1.4_setuptools.json b/tests/fixtures/json/1.4/bom_setuptools_no_version.json similarity index 87% rename from tests/fixtures/bom_v1.4_setuptools.json rename to tests/fixtures/json/1.4/bom_setuptools_no_version.json index f93f4ca3..7f1fdde4 100644 --- a/tests/fixtures/bom_v1.4_setuptools.json +++ b/tests/fixtures/json/1.4/bom_setuptools_no_version.json @@ -51,10 +51,15 @@ "components": [ { "type": "library", + "bom-ref": "pkg:pypi/setuptools?extension=tar.gz", + "author": "Test Author", "name": "setuptools", - "version": "50.3.2", - "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", - "bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" + "licenses": [ + { + "expression": "MIT License" + } + ], + "purl": "pkg:pypi/setuptools?extension=tar.gz" } ] } \ No newline at end of file diff --git a/tests/fixtures/bom_v1.4_setuptools_with_cpe.json b/tests/fixtures/json/1.4/bom_setuptools_with_cpe.json similarity index 87% rename from tests/fixtures/bom_v1.4_setuptools_with_cpe.json rename to tests/fixtures/json/1.4/bom_setuptools_with_cpe.json index fe9995a8..21a2c950 100644 --- a/tests/fixtures/bom_v1.4_setuptools_with_cpe.json +++ b/tests/fixtures/json/1.4/bom_setuptools_with_cpe.json @@ -51,11 +51,17 @@ "components": [ { "type": "library", + "bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", + "author": "Test Author", "name": "setuptools", "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], "cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*", - "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", - "bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" + "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" } ] } \ No newline at end of file diff --git a/tests/fixtures/bom_v1.4_setuptools_with_release_notes.json b/tests/fixtures/json/1.4/bom_setuptools_with_release_notes.json similarity index 96% rename from tests/fixtures/bom_v1.4_setuptools_with_release_notes.json rename to tests/fixtures/json/1.4/bom_setuptools_with_release_notes.json index 1cb10683..f2511fbd 100644 --- a/tests/fixtures/bom_v1.4_setuptools_with_release_notes.json +++ b/tests/fixtures/json/1.4/bom_setuptools_with_release_notes.json @@ -52,8 +52,14 @@ { "type": "library", "bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", + "author": "Test Author", "name": "setuptools", "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", "releaseNotes": { "type": "major", diff --git a/tests/fixtures/bom_v1.4_setuptools_with_vulnerabilities.json b/tests/fixtures/json/1.4/bom_setuptools_with_vulnerabilities.json similarity index 96% rename from tests/fixtures/bom_v1.4_setuptools_with_vulnerabilities.json rename to tests/fixtures/json/1.4/bom_setuptools_with_vulnerabilities.json index 260af12e..4b9a5db2 100644 --- a/tests/fixtures/bom_v1.4_setuptools_with_vulnerabilities.json +++ b/tests/fixtures/json/1.4/bom_setuptools_with_vulnerabilities.json @@ -52,8 +52,14 @@ { "type": "library", "bom-ref": "pkg:pypi/setuptools@50.3.2?extension=tar.gz", + "author": "Test Author", "name": "setuptools", "version": "50.3.2", + "licenses": [ + { + "expression": "MIT License" + } + ], "purl": "pkg:pypi/setuptools@50.3.2?extension=tar.gz" } ], @@ -126,7 +132,7 @@ "contact": [ { "name": "Paul Horton", - "email": "simplyecommerce@googlemail.com" + "email": "paul.horton@owasp.org" }, { "name": "A N Other", diff --git a/tests/fixtures/json/1.4/bom_toml_1.json b/tests/fixtures/json/1.4/bom_toml_1.json new file mode 100644 index 00000000..8c231dd2 --- /dev/null +++ b/tests/fixtures/json/1.4/bom_toml_1.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION", + "externalReferences": [ + { + "type": "build-system", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions" + }, + { + "type": "distribution", + "url": "https://pypi.org/project/cyclonedx-python-lib/" + }, + { + "type": "documentation", + "url": "https://cyclonedx.github.io/cyclonedx-python-lib/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues" + }, + { + "type": "license", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE" + }, + { + "type": "release-notes", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib" + }, + { + "type": "website", + "url": "https://cyclonedx.org" + } + ] + } + ] + }, + "components": [ + { + "type": "library", + "bom-ref": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "name": "toml", + "version": "0.10.2", + "purl": "pkg:pypi/toml@0.10.2?extension=tar.gz", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "externalReferences": [ + { + "type": "distribution", + "url": "https://cyclonedx.org", + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/json/1.4/bom_with_full_metadata.json b/tests/fixtures/json/1.4/bom_with_full_metadata.json new file mode 100644 index 00000000..0a4ce155 --- /dev/null +++ b/tests/fixtures/json/1.4/bom_with_full_metadata.json @@ -0,0 +1,121 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2021-09-01T10:50:42.051979+00:00", + "tools": [ + { + "vendor": "CycloneDX", + "name": "cyclonedx-python-lib", + "version": "VERSION", + "externalReferences": [ + { + "type": "build-system", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions" + }, + { + "type": "distribution", + "url": "https://pypi.org/project/cyclonedx-python-lib/" + }, + { + "type": "documentation", + "url": "https://cyclonedx.github.io/cyclonedx-python-lib/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues" + }, + { + "type": "license", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE" + }, + { + "type": "release-notes", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib" + }, + { + "type": "website", + "url": "https://cyclonedx.org" + } + ] + } + ], + "authors": [ + { + "name": "Paul Horton", + "email": "paul.horton@owasp.org" + }, + { + "name": "A N Other", + "email": "someone@somewhere.tld", + "phone": "+44 (0)1234 567890" + } + ], + "component": { + "bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3", + "type": "library", + "name": "cyclonedx-python-lib", + "version": "1.0.0" + }, + "manufacture": { + "name": "CycloneDX", + "url": [ + "https://cyclonedx.org" + ], + "contact": [ + { + "name": "Paul Horton", + "email": "paul.horton@owasp.org" + }, + { + "name": "A N Other", + "email": "someone@somewhere.tld", + "phone": "+44 (0)1234 567890" + } + ] + }, + "supplier": { + "name": "Cyclone DX", + "url": [ + "https://cyclonedx.org/" + ], + "contact": [ + { + "name": "A N Other", + "email": "someone@somewhere.tld", + "phone": "+44 (0)1234 567890" + } + ] + }, + "licenses": [ + { + "license": { + "id": "Apache-2.0", + "text": { + "contentType": "text/plain", + "encoding": "base64", + "content": "VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=" + }, + "url": "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + ], + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/xml/1.0/bom_empty.xml b/tests/fixtures/xml/1.0/bom_empty.xml new file mode 100644 index 00000000..b48899c8 --- /dev/null +++ b/tests/fixtures/xml/1.0/bom_empty.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tests/fixtures/bom_v1.0_setuptools.xml b/tests/fixtures/xml/1.0/bom_setuptools.xml similarity index 100% rename from tests/fixtures/bom_v1.0_setuptools.xml rename to tests/fixtures/xml/1.0/bom_setuptools.xml diff --git a/tests/fixtures/xml/1.0/bom_setuptools_complete.xml b/tests/fixtures/xml/1.0/bom_setuptools_complete.xml new file mode 100644 index 00000000..32a3dafb --- /dev/null +++ b/tests/fixtures/xml/1.0/bom_setuptools_complete.xml @@ -0,0 +1,33 @@ + + + + + CycloneDX + setuptools + 50.3.2 + This component is awesome + required + Apache 2.0 baby! + cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:* + pkg:pypi/setuptools@50.3.2?extension=tar.gz + false + + + setuptools + 50.3.2 + pkg:pypi/setuptools@50.3.2?extension=tar.gz + false + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + false + + + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.0/bom_setuptools_no_version.xml b/tests/fixtures/xml/1.0/bom_setuptools_no_version.xml new file mode 100644 index 00000000..10bd0836 --- /dev/null +++ b/tests/fixtures/xml/1.0/bom_setuptools_no_version.xml @@ -0,0 +1,11 @@ + + + + + setuptools + + pkg:pypi/setuptools?extension=tar.gz + false + + + \ No newline at end of file diff --git a/tests/fixtures/bom_v1.0_setuptools_with_cpe.xml b/tests/fixtures/xml/1.0/bom_setuptools_with_cpe.xml similarity index 100% rename from tests/fixtures/bom_v1.0_setuptools_with_cpe.xml rename to tests/fixtures/xml/1.0/bom_setuptools_with_cpe.xml diff --git a/tests/fixtures/xml/1.0/bom_toml_hashes_and_references.xml b/tests/fixtures/xml/1.0/bom_toml_hashes_and_references.xml new file mode 100644 index 00000000..b9fd4a08 --- /dev/null +++ b/tests/fixtures/xml/1.0/bom_toml_hashes_and_references.xml @@ -0,0 +1,14 @@ + + + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + false + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.1/bom_empty.xml b/tests/fixtures/xml/1.1/bom_empty.xml new file mode 100644 index 00000000..09b752a1 --- /dev/null +++ b/tests/fixtures/xml/1.1/bom_empty.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.1/bom_external_references.xml b/tests/fixtures/xml/1.1/bom_external_references.xml new file mode 100644 index 00000000..49ba0114 --- /dev/null +++ b/tests/fixtures/xml/1.1/bom_external_references.xml @@ -0,0 +1,13 @@ + + + + + + https://cyclonedx.org + No comment + + + https://cyclonedx.org + + + \ No newline at end of file diff --git a/tests/fixtures/bom_v1.1_setuptools.xml b/tests/fixtures/xml/1.1/bom_setuptools.xml similarity index 82% rename from tests/fixtures/bom_v1.1_setuptools.xml rename to tests/fixtures/xml/1.1/bom_setuptools.xml index d051ce6b..1fb9f8db 100644 --- a/tests/fixtures/bom_v1.1_setuptools.xml +++ b/tests/fixtures/xml/1.1/bom_setuptools.xml @@ -5,6 +5,9 @@ setuptools 50.3.2 + + MIT License + pkg:pypi/setuptools@50.3.2?extension=tar.gz diff --git a/tests/fixtures/xml/1.1/bom_setuptools_complete.xml b/tests/fixtures/xml/1.1/bom_setuptools_complete.xml new file mode 100644 index 00000000..0a116a29 --- /dev/null +++ b/tests/fixtures/xml/1.1/bom_setuptools_complete.xml @@ -0,0 +1,123 @@ + + + + + CycloneDX + setuptools + 50.3.2 + This component is awesome + required + + MIT License + + Apache 2.0 baby! + cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:* + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + setuptools + + + MIT License + + pkg:pypi/setuptools?extension=tar.gz + + + + + setuptools + + + MIT License + + pkg:pypi/setuptools?extension=tar.gz + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + + + + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + + + + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + + + a-random-uid + A commit message + + + Some notes here please + + + + https://cyclonedx.org + No comment + + + + + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + + + + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.1/bom_setuptools_no_version.xml b/tests/fixtures/xml/1.1/bom_setuptools_no_version.xml new file mode 100644 index 00000000..8880d3c7 --- /dev/null +++ b/tests/fixtures/xml/1.1/bom_setuptools_no_version.xml @@ -0,0 +1,13 @@ + + + + + setuptools + + + MIT License + + pkg:pypi/setuptools?extension=tar.gz + + + \ No newline at end of file diff --git a/tests/fixtures/bom_v1.1_setuptools_with_cpe.xml b/tests/fixtures/xml/1.1/bom_setuptools_with_cpe.xml similarity index 84% rename from tests/fixtures/bom_v1.1_setuptools_with_cpe.xml rename to tests/fixtures/xml/1.1/bom_setuptools_with_cpe.xml index d4657548..88d4b12c 100644 --- a/tests/fixtures/bom_v1.1_setuptools_with_cpe.xml +++ b/tests/fixtures/xml/1.1/bom_setuptools_with_cpe.xml @@ -5,6 +5,9 @@ setuptools 50.3.2 + + MIT License + cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:* pkg:pypi/setuptools@50.3.2?extension=tar.gz diff --git a/tests/fixtures/xml/1.1/bom_setuptools_with_vulnerabilities.xml b/tests/fixtures/xml/1.1/bom_setuptools_with_vulnerabilities.xml new file mode 100644 index 00000000..cb9305c4 --- /dev/null +++ b/tests/fixtures/xml/1.1/bom_setuptools_with_vulnerabilities.xml @@ -0,0 +1,51 @@ + + + + + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + CVE-2018-7489 + + https://nvd.nist.gov/vuln/detail/CVE-2018-7489 + + + + + 9.8 + + Critical + CVSSv3 + AN/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + + + + 2.7 + + Low + CVSSv3 + AV:L/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N + + + + 22 + 33 + + A description here + + Upgrade + + + https://nvd.nist.gov/vuln/detail/CVE-2018-7489 + http://www.securitytracker.com/id/1040693 + + + + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.1/bom_toml_hashes_and_references.xml b/tests/fixtures/xml/1.1/bom_toml_hashes_and_references.xml new file mode 100644 index 00000000..489fb300 --- /dev/null +++ b/tests/fixtures/xml/1.1/bom_toml_hashes_and_references.xml @@ -0,0 +1,19 @@ + + + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.2/bom_external_references.xml b/tests/fixtures/xml/1.2/bom_external_references.xml new file mode 100644 index 00000000..977900cb --- /dev/null +++ b/tests/fixtures/xml/1.2/bom_external_references.xml @@ -0,0 +1,23 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + + + + + https://cyclonedx.org + No comment + + + https://cyclonedx.org + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.2/bom_services_complex.xml b/tests/fixtures/xml/1.2/bom_services_complex.xml new file mode 100644 index 00000000..1dd5f92d --- /dev/null +++ b/tests/fixtures/xml/1.2/bom_services_complex.xml @@ -0,0 +1,60 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + + cyclonedx-python-lib + 1.0.0 + + + + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + a-group + my-first-service + 1.2.3 + Description goes here + + /api/thing/1 + /api/thing/2 + + false + true + + public + + + Commercial + + + + https://cyclonedx.org + No comment + + + + + my-second-service + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.2/bom_services_nested.xml b/tests/fixtures/xml/1.2/bom_services_nested.xml new file mode 100644 index 00000000..60439b6d --- /dev/null +++ b/tests/fixtures/xml/1.2/bom_services_nested.xml @@ -0,0 +1,108 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + + cyclonedx-python-lib + 1.0.0 + + + + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + a-group + my-first-service + 1.2.3 + Description goes here + + /api/thing/1 + /api/thing/2 + + false + true + + public + + + Commercial + + + + https://cyclonedx.org + No comment + + + + + first-nested-service + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + no-group + second-nested-service + 3.2.1 + true + false + + + + + my-second-service + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + what-group + yet-another-nested-service + 6.5.4 + + + another-nested-service + + + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.2/bom_services_simple.xml b/tests/fixtures/xml/1.2/bom_services_simple.xml new file mode 100644 index 00000000..a6cf63c2 --- /dev/null +++ b/tests/fixtures/xml/1.2/bom_services_simple.xml @@ -0,0 +1,26 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + + cyclonedx-python-lib + 1.0.0 + + + + + + my-first-service + + + my-second-service + + + \ No newline at end of file diff --git a/tests/fixtures/bom_v1.2_setuptools.xml b/tests/fixtures/xml/1.2/bom_setuptools.xml similarity index 83% rename from tests/fixtures/bom_v1.2_setuptools.xml rename to tests/fixtures/xml/1.2/bom_setuptools.xml index 7deec2dc..85d33d1a 100644 --- a/tests/fixtures/bom_v1.2_setuptools.xml +++ b/tests/fixtures/xml/1.2/bom_setuptools.xml @@ -12,8 +12,12 @@ + Test Author setuptools 50.3.2 + + MIT License + pkg:pypi/setuptools@50.3.2?extension=tar.gz diff --git a/tests/fixtures/xml/1.2/bom_setuptools_complete.xml b/tests/fixtures/xml/1.2/bom_setuptools_complete.xml new file mode 100644 index 00000000..195ffd0f --- /dev/null +++ b/tests/fixtures/xml/1.2/bom_setuptools_complete.xml @@ -0,0 +1,158 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + Test Author + CycloneDX + setuptools + 50.3.2 + This component is awesome + required + + MIT License + + Apache 2.0 baby! + cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:* + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg== + + + + + Test Author + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + Test Author + setuptools + + + MIT License + + pkg:pypi/setuptools?extension=tar.gz + + + + + Test Author + setuptools + + + MIT License + + pkg:pypi/setuptools?extension=tar.gz + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + + + + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + + + + Test Author + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + + + a-random-uid + A commit message + + + + + + Some notes here please + + + + https://cyclonedx.org + No comment + + + + + Test Author + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + + + + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.2/bom_setuptools_no_version.xml b/tests/fixtures/xml/1.2/bom_setuptools_no_version.xml new file mode 100644 index 00000000..b8461ee9 --- /dev/null +++ b/tests/fixtures/xml/1.2/bom_setuptools_no_version.xml @@ -0,0 +1,24 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + + + + Test Author + setuptools + + + MIT License + + pkg:pypi/setuptools?extension=tar.gz + + + \ No newline at end of file diff --git a/tests/fixtures/bom_v1.2_setuptools_with_cpe.xml b/tests/fixtures/xml/1.2/bom_setuptools_with_cpe.xml similarity index 84% rename from tests/fixtures/bom_v1.2_setuptools_with_cpe.xml rename to tests/fixtures/xml/1.2/bom_setuptools_with_cpe.xml index 6c19439c..02bf3e8e 100644 --- a/tests/fixtures/bom_v1.2_setuptools_with_cpe.xml +++ b/tests/fixtures/xml/1.2/bom_setuptools_with_cpe.xml @@ -12,8 +12,12 @@ + Test Author setuptools 50.3.2 + + MIT License + cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:* pkg:pypi/setuptools@50.3.2?extension=tar.gz diff --git a/tests/fixtures/xml/1.2/bom_setuptools_with_vulnerabilities.xml b/tests/fixtures/xml/1.2/bom_setuptools_with_vulnerabilities.xml new file mode 100644 index 00000000..e295e0a4 --- /dev/null +++ b/tests/fixtures/xml/1.2/bom_setuptools_with_vulnerabilities.xml @@ -0,0 +1,62 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + + + + Test Author + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + CVE-2018-7489 + + https://nvd.nist.gov/vuln/detail/CVE-2018-7489 + + + + + 9.8 + + Critical + CVSSv3 + AN/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + + + + 2.7 + + Low + CVSSv3 + AV:L/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N + + + + 22 + 33 + + A description here + + Upgrade + + + https://nvd.nist.gov/vuln/detail/CVE-2018-7489 + http://www.securitytracker.com/id/1040693 + + + + + + \ No newline at end of file diff --git a/tests/fixtures/bom_v1.3_toml_with_component_hashes.xml b/tests/fixtures/xml/1.2/bom_toml_hashes_and_references.xml similarity index 71% rename from tests/fixtures/bom_v1.3_toml_with_component_hashes.xml rename to tests/fixtures/xml/1.2/bom_toml_hashes_and_references.xml index 5843c1ef..638a77e5 100644 --- a/tests/fixtures/bom_v1.3_toml_with_component_hashes.xml +++ b/tests/fixtures/xml/1.2/bom_toml_hashes_and_references.xml @@ -1,5 +1,5 @@ - + 2021-09-01T10:50:42.051979+00:00 @@ -18,6 +18,12 @@ 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.2/bom_with_full_metadata.xml b/tests/fixtures/xml/1.2/bom_with_full_metadata.xml new file mode 100644 index 00000000..5ac9d81b --- /dev/null +++ b/tests/fixtures/xml/1.2/bom_with_full_metadata.xml @@ -0,0 +1,51 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + + cyclonedx-python-lib + 1.0.0 + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + + Cyclone DX + https://cyclonedx.org/ + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.3/bom_external_references.xml b/tests/fixtures/xml/1.3/bom_external_references.xml new file mode 100644 index 00000000..7219f606 --- /dev/null +++ b/tests/fixtures/xml/1.3/bom_external_references.xml @@ -0,0 +1,26 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + + + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + https://cyclonedx.org + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.3/bom_services_complex.xml b/tests/fixtures/xml/1.3/bom_services_complex.xml new file mode 100644 index 00000000..8d2f93c0 --- /dev/null +++ b/tests/fixtures/xml/1.3/bom_services_complex.xml @@ -0,0 +1,67 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + + cyclonedx-python-lib + 1.0.0 + + + + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + a-group + my-first-service + 1.2.3 + Description goes here + + /api/thing/1 + /api/thing/2 + + false + true + + public + + + Commercial + + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + val1 + val2 + + + + my-second-service + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.3/bom_services_nested.xml b/tests/fixtures/xml/1.3/bom_services_nested.xml new file mode 100644 index 00000000..4ebb2152 --- /dev/null +++ b/tests/fixtures/xml/1.3/bom_services_nested.xml @@ -0,0 +1,115 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + + cyclonedx-python-lib + 1.0.0 + + + + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + a-group + my-first-service + 1.2.3 + Description goes here + + /api/thing/1 + /api/thing/2 + + false + true + + public + + + Commercial + + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + val1 + val2 + + + + first-nested-service + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + no-group + second-nested-service + 3.2.1 + true + false + + + + + my-second-service + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + what-group + yet-another-nested-service + 6.5.4 + + + another-nested-service + + + + + \ No newline at end of file diff --git a/tests/fixtures/bom_v1.3_with_metadata_component.xml b/tests/fixtures/xml/1.3/bom_services_simple.xml similarity index 57% rename from tests/fixtures/bom_v1.3_with_metadata_component.xml rename to tests/fixtures/xml/1.3/bom_services_simple.xml index 1bbe3362..d3feae46 100644 --- a/tests/fixtures/bom_v1.3_with_metadata_component.xml +++ b/tests/fixtures/xml/1.3/bom_services_simple.xml @@ -9,10 +9,18 @@ VERSION - + cyclonedx-python-lib 1.0.0 - + + + + my-first-service + + + my-second-service + + \ No newline at end of file diff --git a/tests/fixtures/bom_v1.3_setuptools.xml b/tests/fixtures/xml/1.3/bom_setuptools.xml similarity index 83% rename from tests/fixtures/bom_v1.3_setuptools.xml rename to tests/fixtures/xml/1.3/bom_setuptools.xml index de7e7a17..0b315196 100644 --- a/tests/fixtures/bom_v1.3_setuptools.xml +++ b/tests/fixtures/xml/1.3/bom_setuptools.xml @@ -12,8 +12,12 @@ + Test Author setuptools 50.3.2 + + MIT License + pkg:pypi/setuptools@50.3.2?extension=tar.gz diff --git a/tests/fixtures/xml/1.3/bom_setuptools_complete.xml b/tests/fixtures/xml/1.3/bom_setuptools_complete.xml new file mode 100644 index 00000000..0a2a149c --- /dev/null +++ b/tests/fixtures/xml/1.3/bom_setuptools_complete.xml @@ -0,0 +1,180 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + Test Author + CycloneDX + setuptools + 50.3.2 + This component is awesome + required + + MIT License + + Apache 2.0 baby! + cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:* + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg== + + + + + Test Author + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + Test Author + setuptools + + + MIT License + + pkg:pypi/setuptools?extension=tar.gz + + + + + Test Author + setuptools + + + MIT License + + pkg:pypi/setuptools?extension=tar.gz + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + + Test Author + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + + + a-random-uid + A commit message + + + + + + Some notes here please + + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + val1 + val2 + + + + Test Author + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + + + + Commercial + Commercial 2 + + + + + \ No newline at end of file diff --git a/tests/fixtures/bom_v1.3_toml_with_component_license.xml b/tests/fixtures/xml/1.3/bom_setuptools_no_version.xml similarity index 69% rename from tests/fixtures/bom_v1.3_toml_with_component_license.xml rename to tests/fixtures/xml/1.3/bom_setuptools_no_version.xml index 1e16f66b..2acc18b4 100644 --- a/tests/fixtures/bom_v1.3_toml_with_component_license.xml +++ b/tests/fixtures/xml/1.3/bom_setuptools_no_version.xml @@ -11,13 +11,14 @@ - - toml - 0.10.2 + + Test Author + setuptools + MIT License - pkg:pypi/toml@0.10.2?extension=tar.gz + pkg:pypi/setuptools?extension=tar.gz \ No newline at end of file diff --git a/tests/fixtures/bom_v1.3_setuptools_with_cpe.xml b/tests/fixtures/xml/1.3/bom_setuptools_with_cpe.xml similarity index 83% rename from tests/fixtures/bom_v1.3_setuptools_with_cpe.xml rename to tests/fixtures/xml/1.3/bom_setuptools_with_cpe.xml index 17ae9d66..402e45fc 100644 --- a/tests/fixtures/bom_v1.3_setuptools_with_cpe.xml +++ b/tests/fixtures/xml/1.3/bom_setuptools_with_cpe.xml @@ -12,9 +12,13 @@ + Test Author setuptools 50.3.2 - cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:* + + MIT License + + cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:* pkg:pypi/setuptools@50.3.2?extension=tar.gz diff --git a/tests/fixtures/bom_v1.3_setuptools_with_vulnerabilities.xml b/tests/fixtures/xml/1.3/bom_setuptools_with_vulnerabilities.xml similarity index 75% rename from tests/fixtures/bom_v1.3_setuptools_with_vulnerabilities.xml rename to tests/fixtures/xml/1.3/bom_setuptools_with_vulnerabilities.xml index 6978c8af..56a5c326 100644 --- a/tests/fixtures/bom_v1.3_setuptools_with_vulnerabilities.xml +++ b/tests/fixtures/xml/1.3/bom_setuptools_with_vulnerabilities.xml @@ -12,8 +12,12 @@ + Test Author setuptools 50.3.2 + + MIT License + pkg:pypi/setuptools@50.3.2?extension=tar.gz @@ -28,24 +32,27 @@ Critical CVSSv3 - AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + AN/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + + 2.7 + Low - OWASP Risk - K9:M1:O0:Z2/D1:X1:W1:L3/C2:I1:A1:T1/F1:R1:S2:P3/50 + CVSSv3 + AV:L/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N - 123 - 456 + 22 + 33 A description here Upgrade - http://www.securityfocus.com/bid/103203 + https://nvd.nist.gov/vuln/detail/CVE-2018-7489 http://www.securitytracker.com/id/1040693 diff --git a/tests/fixtures/bom_v1.3_toml_with_component_external_references.xml b/tests/fixtures/xml/1.3/bom_toml_hashes_and_references.xml similarity index 100% rename from tests/fixtures/bom_v1.3_toml_with_component_external_references.xml rename to tests/fixtures/xml/1.3/bom_toml_hashes_and_references.xml diff --git a/tests/fixtures/xml/1.3/bom_with_full_metadata.xml b/tests/fixtures/xml/1.3/bom_with_full_metadata.xml new file mode 100644 index 00000000..639e270a --- /dev/null +++ b/tests/fixtures/xml/1.3/bom_with_full_metadata.xml @@ -0,0 +1,62 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + + cyclonedx-python-lib + 1.0.0 + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + + Cyclone DX + https://cyclonedx.org/ + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + + + Apache-2.0 + VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE= + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + val1 + val2 + + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.4/bom_external_references.xml b/tests/fixtures/xml/1.4/bom_external_references.xml new file mode 100644 index 00000000..20dea410 --- /dev/null +++ b/tests/fixtures/xml/1.4/bom_external_references.xml @@ -0,0 +1,52 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + https://github.com/CycloneDX/cyclonedx-python-lib/actions + + + https://pypi.org/project/cyclonedx-python-lib/ + + + https://cyclonedx.github.io/cyclonedx-python-lib/ + + + https://github.com/CycloneDX/cyclonedx-python-lib/issues + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md + + + https://github.com/CycloneDX/cyclonedx-python-lib + + + https://cyclonedx.org + + + + + + + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + https://cyclonedx.org + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.4/bom_services_complex.xml b/tests/fixtures/xml/1.4/bom_services_complex.xml new file mode 100644 index 00000000..d7187c56 --- /dev/null +++ b/tests/fixtures/xml/1.4/bom_services_complex.xml @@ -0,0 +1,137 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + https://github.com/CycloneDX/cyclonedx-python-lib/actions + + + https://pypi.org/project/cyclonedx-python-lib/ + + + https://cyclonedx.github.io/cyclonedx-python-lib/ + + + https://github.com/CycloneDX/cyclonedx-python-lib/issues + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md + + + https://github.com/CycloneDX/cyclonedx-python-lib + + + https://cyclonedx.org + + + + + + cyclonedx-python-lib + 1.0.0 + + + + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + a-group + my-first-service + 1.2.3 + Description goes here + + /api/thing/1 + /api/thing/2 + + false + true + + public + + + Commercial + + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + val1 + val2 + + + major + Release Notes Title + https://cyclonedx.org/theme/assets/images/CycloneDX-Twitter-Card.png + https://cyclonedx.org/cyclonedx-icon.png + This release is a test release + 2021-12-31T10:00:00+00:00 + + First Test Release + + + test + alpha + + + + CVE-2021-44228 + Apache Log3Shell + Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features... + + NVD + https://nvd.nist.gov/vuln/detail/CVE-2021-44228 + + + https://logging.apache.org/log4j/2.x/security.html + https://central.sonatype.org/news/20211213_log4shell_help + + + + + + en-GB + U29tZSBzaW1wbGUgcGxhaW4gdGV4dA== + + + en-US + U29tZSBzaW1wbGUgcGxhaW4gdGV4dA== + + + + val1 + val2 + + + + + my-second-service + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.4/bom_services_nested.xml b/tests/fixtures/xml/1.4/bom_services_nested.xml new file mode 100644 index 00000000..2a3348ef --- /dev/null +++ b/tests/fixtures/xml/1.4/bom_services_nested.xml @@ -0,0 +1,185 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + https://github.com/CycloneDX/cyclonedx-python-lib/actions + + + https://pypi.org/project/cyclonedx-python-lib/ + + + https://cyclonedx.github.io/cyclonedx-python-lib/ + + + https://github.com/CycloneDX/cyclonedx-python-lib/issues + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md + + + https://github.com/CycloneDX/cyclonedx-python-lib + + + https://cyclonedx.org + + + + + + cyclonedx-python-lib + 1.0.0 + + + + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + a-group + my-first-service + 1.2.3 + Description goes here + + /api/thing/1 + /api/thing/2 + + false + true + + public + + + Commercial + + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + val1 + val2 + + + + first-nested-service + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + no-group + second-nested-service + 3.2.1 + true + false + + + + major + Release Notes Title + https://cyclonedx.org/theme/assets/images/CycloneDX-Twitter-Card.png + https://cyclonedx.org/cyclonedx-icon.png + This release is a test release + 2021-12-31T10:00:00+00:00 + + First Test Release + + + test + alpha + + + + CVE-2021-44228 + Apache Log3Shell + Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features... + + NVD + https://nvd.nist.gov/vuln/detail/CVE-2021-44228 + + + https://logging.apache.org/log4j/2.x/security.html + https://central.sonatype.org/news/20211213_log4shell_help + + + + + + en-GB + U29tZSBzaW1wbGUgcGxhaW4gdGV4dA== + + + en-US + U29tZSBzaW1wbGUgcGxhaW4gdGV4dA== + + + + val1 + val2 + + + + + my-second-service + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + what-group + yet-another-nested-service + 6.5.4 + + + another-nested-service + + + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.4/bom_services_simple.xml b/tests/fixtures/xml/1.4/bom_services_simple.xml new file mode 100644 index 00000000..8eb59474 --- /dev/null +++ b/tests/fixtures/xml/1.4/bom_services_simple.xml @@ -0,0 +1,52 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + https://github.com/CycloneDX/cyclonedx-python-lib/actions + + + https://pypi.org/project/cyclonedx-python-lib/ + + + https://cyclonedx.github.io/cyclonedx-python-lib/ + + + https://github.com/CycloneDX/cyclonedx-python-lib/issues + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md + + + https://github.com/CycloneDX/cyclonedx-python-lib + + + https://cyclonedx.org + + + + + + cyclonedx-python-lib + 1.0.0 + + + + + + my-first-service + + + my-second-service + + + \ No newline at end of file diff --git a/tests/fixtures/bom_v1.4_setuptools.xml b/tests/fixtures/xml/1.4/bom_setuptools.xml similarity index 93% rename from tests/fixtures/bom_v1.4_setuptools.xml rename to tests/fixtures/xml/1.4/bom_setuptools.xml index 4b225683..45f143b8 100644 --- a/tests/fixtures/bom_v1.4_setuptools.xml +++ b/tests/fixtures/xml/1.4/bom_setuptools.xml @@ -38,8 +38,12 @@ + Test Author setuptools 50.3.2 + + MIT License + pkg:pypi/setuptools@50.3.2?extension=tar.gz diff --git a/tests/fixtures/xml/1.4/bom_setuptools_complete.xml b/tests/fixtures/xml/1.4/bom_setuptools_complete.xml new file mode 100644 index 00000000..39968cf8 --- /dev/null +++ b/tests/fixtures/xml/1.4/bom_setuptools_complete.xml @@ -0,0 +1,248 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + https://github.com/CycloneDX/cyclonedx-python-lib/actions + + + https://pypi.org/project/cyclonedx-python-lib/ + + + https://cyclonedx.github.io/cyclonedx-python-lib/ + + + https://github.com/CycloneDX/cyclonedx-python-lib/issues + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md + + + https://github.com/CycloneDX/cyclonedx-python-lib + + + https://cyclonedx.org + + + + + + + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + Test Author + CycloneDX + setuptools + 50.3.2 + This component is awesome + required + + MIT License + + Apache 2.0 baby! + cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:* + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+CjxTb2Z0d2FyZUlkZW50aXR5IHhtbDpsYW5nPSJFTiIgbmFtZT0iQWNtZSBBcHBsaWNhdGlvbiIgdmVyc2lvbj0iOS4xLjEiIAogdmVyc2lvblNjaGVtZT0ibXVsdGlwYXJ0bnVtZXJpYyIgCiB0YWdJZD0ic3dpZGdlbi1iNTk1MWFjOS00MmMwLWYzODItM2YxZS1iYzdhMmE0NDk3Y2JfOS4xLjEiIAogeG1sbnM9Imh0dHA6Ly9zdGFuZGFyZHMuaXNvLm9yZy9pc28vMTk3NzAvLTIvMjAxNS9zY2hlbWEueHNkIj4gCiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiAKIHhzaTpzY2hlbWFMb2NhdGlvbj0iaHR0cDovL3N0YW5kYXJkcy5pc28ub3JnL2lzby8xOTc3MC8tMi8yMDE1LWN1cnJlbnQvc2NoZW1hLnhzZCBzY2hlbWEueHNkIiA+CiAgPE1ldGEgZ2VuZXJhdG9yPSJTV0lEIFRhZyBPbmxpbmUgR2VuZXJhdG9yIHYwLjEiIC8+IAogIDxFbnRpdHkgbmFtZT0iQWNtZSwgSW5jLiIgcmVnaWQ9ImV4YW1wbGUuY29tIiByb2xlPSJ0YWdDcmVhdG9yIiAvPiAKPC9Tb2Z0d2FyZUlkZW50aXR5Pg== + + + + + Test Author + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + Test Author + setuptools + + MIT License + + pkg:pypi/setuptools?extension=tar.gz + + + + + Test Author + setuptools + + MIT License + + pkg:pypi/setuptools?extension=tar.gz + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + + Test Author + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + + + a-random-uid + A commit message + + + + + + Some notes here please + + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + val1 + val2 + + + + Test Author + setuptools + 50.3.2 + + MIT License + + pkg:pypi/setuptools@50.3.2?extension=tar.gz + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + + + + Commercial + Commercial 2 + + + + major + Release Notes Title + https://cyclonedx.org/theme/assets/images/CycloneDX-Twitter-Card.png + https://cyclonedx.org/cyclonedx-icon.png + This release is a test release + 2021-12-31T10:00:00+00:00 + + First Test Release + + + test + alpha + + + + CVE-2021-44228 + Apache Log3Shell + Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features... + + NVD + https://nvd.nist.gov/vuln/detail/CVE-2021-44228 + + + https://logging.apache.org/log4j/2.x/security.html + https://central.sonatype.org/news/20211213_log4shell_help + + + + + + en-GB + U29tZSBzaW1wbGUgcGxhaW4gdGV4dA== + + + en-US + U29tZSBzaW1wbGUgcGxhaW4gdGV4dA== + + + + val1 + val2 + + + + + \ No newline at end of file diff --git a/tests/fixtures/bom_v1.4_setuptools_no_version.xml b/tests/fixtures/xml/1.4/bom_setuptools_no_version.xml similarity index 93% rename from tests/fixtures/bom_v1.4_setuptools_no_version.xml rename to tests/fixtures/xml/1.4/bom_setuptools_no_version.xml index 1c9923c2..ad9edadd 100644 --- a/tests/fixtures/bom_v1.4_setuptools_no_version.xml +++ b/tests/fixtures/xml/1.4/bom_setuptools_no_version.xml @@ -38,7 +38,11 @@ + Test Author setuptools + + MIT License + pkg:pypi/setuptools?extension=tar.gz diff --git a/tests/fixtures/bom_v1.4_setuptools_with_cpe.xml b/tests/fixtures/xml/1.4/bom_setuptools_with_cpe.xml similarity index 94% rename from tests/fixtures/bom_v1.4_setuptools_with_cpe.xml rename to tests/fixtures/xml/1.4/bom_setuptools_with_cpe.xml index 113848b8..15d8e89a 100644 --- a/tests/fixtures/bom_v1.4_setuptools_with_cpe.xml +++ b/tests/fixtures/xml/1.4/bom_setuptools_with_cpe.xml @@ -38,8 +38,12 @@ + Test Author setuptools 50.3.2 + + MIT License + cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:* pkg:pypi/setuptools@50.3.2?extension=tar.gz diff --git a/tests/fixtures/bom_v1.4_setuptools_with_release_notes.xml b/tests/fixtures/xml/1.4/bom_setuptools_with_release_notes.xml similarity index 96% rename from tests/fixtures/bom_v1.4_setuptools_with_release_notes.xml rename to tests/fixtures/xml/1.4/bom_setuptools_with_release_notes.xml index 8afcbf59..d5640438 100644 --- a/tests/fixtures/bom_v1.4_setuptools_with_release_notes.xml +++ b/tests/fixtures/xml/1.4/bom_setuptools_with_release_notes.xml @@ -38,8 +38,12 @@ + Test Author setuptools 50.3.2 + + MIT License + pkg:pypi/setuptools@50.3.2?extension=tar.gz major diff --git a/tests/fixtures/bom_v1.4_setuptools_with_vulnerabilities.xml b/tests/fixtures/xml/1.4/bom_setuptools_with_vulnerabilities.xml similarity index 96% rename from tests/fixtures/bom_v1.4_setuptools_with_vulnerabilities.xml rename to tests/fixtures/xml/1.4/bom_setuptools_with_vulnerabilities.xml index c2e9c051..c5c95537 100644 --- a/tests/fixtures/bom_v1.4_setuptools_with_vulnerabilities.xml +++ b/tests/fixtures/xml/1.4/bom_setuptools_with_vulnerabilities.xml @@ -38,8 +38,12 @@ + Test Author setuptools 50.3.2 + + MIT License + pkg:pypi/setuptools@50.3.2?extension=tar.gz @@ -108,7 +112,7 @@ https://cyclonedx.org Paul Horton - simplyecommerce@googlemail.com + paul.horton@owasp.org A N Other diff --git a/tests/fixtures/xml/1.4/bom_toml_hashes_and_references.xml b/tests/fixtures/xml/1.4/bom_toml_hashes_and_references.xml new file mode 100644 index 00000000..ada4ef4c --- /dev/null +++ b/tests/fixtures/xml/1.4/bom_toml_hashes_and_references.xml @@ -0,0 +1,58 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + https://github.com/CycloneDX/cyclonedx-python-lib/actions + + + https://pypi.org/project/cyclonedx-python-lib/ + + + https://cyclonedx.github.io/cyclonedx-python-lib/ + + + https://github.com/CycloneDX/cyclonedx-python-lib/issues + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md + + + https://github.com/CycloneDX/cyclonedx-python-lib + + + https://cyclonedx.org + + + + + + + + toml + 0.10.2 + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + pkg:pypi/toml@0.10.2?extension=tar.gz + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + + \ No newline at end of file diff --git a/tests/fixtures/xml/1.4/bom_with_full_metadata.xml b/tests/fixtures/xml/1.4/bom_with_full_metadata.xml new file mode 100644 index 00000000..dd333058 --- /dev/null +++ b/tests/fixtures/xml/1.4/bom_with_full_metadata.xml @@ -0,0 +1,88 @@ + + + + 2021-09-01T10:50:42.051979+00:00 + + + CycloneDX + cyclonedx-python-lib + VERSION + + + https://github.com/CycloneDX/cyclonedx-python-lib/actions + + + https://pypi.org/project/cyclonedx-python-lib/ + + + https://cyclonedx.github.io/cyclonedx-python-lib/ + + + https://github.com/CycloneDX/cyclonedx-python-lib/issues + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md + + + https://github.com/CycloneDX/cyclonedx-python-lib + + + https://cyclonedx.org + + + + + + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + + cyclonedx-python-lib + 1.0.0 + + + CycloneDX + https://cyclonedx.org + + Paul Horton + paul.horton@owasp.org + + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + + Cyclone DX + https://cyclonedx.org/ + + A N Other + someone@somewhere.tld + +44 (0)1234 567890 + + + + + Apache-2.0 + VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE= + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + val1 + val2 + + + + \ No newline at end of file diff --git a/tests/test_bom.py b/tests/test_bom.py index c37fa193..aedc4ddd 100644 --- a/tests/test_bom.py +++ b/tests/test_bom.py @@ -16,24 +16,24 @@ # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. - from unittest import TestCase from cyclonedx.model.bom import Bom, ThisTool, Tool from cyclonedx.model.component import Component, ComponentType +from data import get_bom_with_component_setuptools_with_vulnerability class TestBom(TestCase): def test_bom_metadata_tool_this_tool(self) -> None: - self.assertEqual(ThisTool.get_vendor(), 'CycloneDX') - self.assertEqual(ThisTool.get_name(), 'cyclonedx-python-lib') - self.assertNotEqual(ThisTool.get_version(), 'UNKNOWN') + self.assertEqual(ThisTool.vendor, 'CycloneDX') + self.assertEqual(ThisTool.name, 'cyclonedx-python-lib') + self.assertNotEqual(ThisTool.version, 'UNKNOWN') def test_bom_metadata_tool_multiple_tools(self) -> None: bom = Bom() self.assertEqual(len(bom.metadata.tools), 1) - bom.metadata.add_tool( + bom.metadata.tools.add( Tool(vendor='TestVendor', name='TestTool', version='0.0.0') ) self.assertEqual(len(bom.metadata.tools), 2) @@ -41,8 +41,19 @@ def test_bom_metadata_tool_multiple_tools(self) -> None: def test_metadata_component(self) -> None: metadata = Bom().metadata self.assertTrue(metadata.component is None) - hextech = Component(name='Hextech', version='1.0.0', - component_type=ComponentType.LIBRARY) + hextech = Component(name='Hextech', version='1.0.0', component_type=ComponentType.LIBRARY) metadata.component = hextech self.assertFalse(metadata.component is None) self.assertEquals(metadata.component, hextech) + + def test_empty_bom(self) -> None: + bom = Bom() + self.assertIsNotNone(bom.uuid) + self.assertIsNotNone(bom.metadata) + self.assertFalse(bom.components) + self.assertFalse(bom.services) + self.assertFalse(bom.external_references) + + def test_bom_with_vulnerabilities(self) -> None: + bom = get_bom_with_component_setuptools_with_vulnerability() + self.assertTrue(bom.has_vulnerabilities()) diff --git a/tests/test_component.py b/tests/test_component.py index bcf73d9f..3f9b440e 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -24,6 +24,8 @@ from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component +FIXTURES_DIRECTORY = 'fixtures/xml/1.4' + class TestComponent(TestCase): @@ -94,20 +96,19 @@ def test_purl_with_qualifiers(self) -> None: self.assertEqual(purl.qualifiers, {'extension': 'tar.gz'}) def test_from_file_with_path_for_bom(self) -> None: - test_file = join(dirname(__file__), 'fixtures/bom_v1.3_setuptools.xml') - c = Component.for_file(absolute_file_path=test_file, path_for_bom='fixtures/bom_v1.3_setuptools.xml') - self.assertEqual(c.name, 'fixtures/bom_v1.3_setuptools.xml') - self.assertEqual(c.version, '0.0.0-16932e52ed1e') + test_file = join(dirname(__file__), FIXTURES_DIRECTORY, 'bom_setuptools.xml') + c = Component.for_file(absolute_file_path=test_file, path_for_bom='fixtures/bom_setuptools.xml') + self.assertEqual(c.name, 'fixtures/bom_setuptools.xml') + self.assertEqual(c.version, '0.0.0-38165abddb68') purl = PackageURL( - type='generic', name='fixtures/bom_v1.3_setuptools.xml', version='0.0.0-16932e52ed1e' + type='generic', name='fixtures/bom_setuptools.xml', version='0.0.0-38165abddb68' ) self.assertEqual(c.purl, purl) self.assertEqual(len(c.hashes), 1) def test_has_component_1(self) -> None: bom = Bom() - bom.add_component(component=TestComponent._component) - bom.add_component(component=TestComponent._component_2) + bom.components.update([TestComponent._component, TestComponent._component_2]) self.assertEqual(len(bom.components), 2) self.assertTrue(bom.has_component(component=TestComponent._component_2)) self.assertIsNot(TestComponent._component, TestComponent._component_2) diff --git a/tests/test_e2e_environment.py b/tests/test_e2e_environment.py index e850d821..22b6af3d 100644 --- a/tests/test_e2e_environment.py +++ b/tests/test_e2e_environment.py @@ -16,7 +16,6 @@ # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. - import json import pkg_resources from lxml import etree @@ -39,7 +38,7 @@ class TestE2EEnvironment(TestCase): @classmethod def setUpClass(cls) -> None: cls.bom: Bom = Bom() - cls.bom.add_component( + cls.bom.components.add( Component( name=OUR_PACKAGE_NAME, author=OUR_PACKAGE_AUTHOR, version=OUR_PACKAGE_VERSION, purl=PackageURL(type='pypi', name=OUR_PACKAGE_NAME, version=OUR_PACKAGE_VERSION) diff --git a/tests/test_model.py b/tests/test_model.py index 6f1ae657..86e75b22 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -1,52 +1,131 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# 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 base64 +import datetime +from time import sleep from unittest import TestCase -from cyclonedx.exception.model import InvalidLocaleTypeException, InvalidUriException, UnknownHashTypeException +from cyclonedx.exception.model import InvalidLocaleTypeException, InvalidUriException, UnknownHashTypeException, \ + NoPropertiesProvidedException +from cyclonedx.model import Copyright, Encoding, ExternalReference, ExternalReferenceType, HashAlgorithm, HashType, \ + IdentifiableAction, Note, NoteText, XsUri +from cyclonedx.model.issue import IssueClassification, IssueType, IssueTypeSource -from cyclonedx.model import Encoding, ExternalReference, ExternalReferenceType, HashAlgorithm, HashType, Note, \ - NoteText, XsUri -from cyclonedx.model.issue import IssueClassification, IssueType +class TestModelCopyright(TestCase): -class TestModelExternalReference(TestCase): + def test_same(self) -> None: + copy_1 = Copyright(text='Copyright (c) OWASP Foundation. All Rights Reserved.') + copy_2 = Copyright(text='Copyright (c) OWASP Foundation. All Rights Reserved.') + self.assertEqual(hash(copy_1), hash(copy_2)) + self.assertTrue(copy_1 == copy_2) + + def test_not_same(self) -> None: + copy_1 = Copyright(text='Copyright (c) OWASP Foundation. All Rights Reserved.') + copy_2 = Copyright(text='Copyright (c) OWASP Foundation.') + self.assertNotEqual(hash(copy_1), hash(copy_2)) + self.assertFalse(copy_1 == copy_2) - def test_external_reference_with_str(self) -> None: - e = ExternalReference(reference_type=ExternalReferenceType.VCS, url='https://www.google.com') - self.assertEqual(e.get_reference_type(), ExternalReferenceType.VCS) - self.assertEqual(e.get_url(), 'https://www.google.com') - self.assertEqual(e.get_comment(), '') - self.assertListEqual(e.get_hashes(), []) + +class TestModelExternalReference(TestCase): def test_external_reference_with_xsuri(self) -> None: e = ExternalReference(reference_type=ExternalReferenceType.VCS, url=XsUri('https://www.google.com')) - self.assertEqual(e.get_reference_type(), ExternalReferenceType.VCS) - self.assertEqual(e.get_url(), 'https://www.google.com') - self.assertEqual(e.get_comment(), '') - self.assertListEqual(e.get_hashes(), []) + self.assertEqual(e.type, ExternalReferenceType.VCS) + self.assertEqual(e.url, XsUri('https://www.google.com')) + self.assertIsNone(e.comment) + self.assertSetEqual(e.hashes, set()) + + def test_same(self) -> None: + ref_1 = ExternalReference( + reference_type=ExternalReferenceType.OTHER, + url=XsUri('https://cyclonedx.org'), + comment='No comment' + ) + ref_2 = ExternalReference( + reference_type=ExternalReferenceType.OTHER, + url=XsUri('https://cyclonedx.org'), + comment='No comment' + ) + self.assertNotEqual(id(ref_1), id(ref_2)) + self.assertEqual(hash(ref_1), hash(ref_2)) + self.assertTrue(ref_1 == ref_2) + + def test_not_same(self) -> None: + ref_1 = ExternalReference( + reference_type=ExternalReferenceType.OTHER, + url=XsUri('https://cyclonedx.org'), + comment='No comment' + ) + ref_2 = ExternalReference( + reference_type=ExternalReferenceType.OTHER, + url=XsUri('https://cyclonedx.org/'), + comment='No comment' + ) + self.assertNotEqual(id(ref_1), id(ref_2)) + self.assertNotEqual(hash(ref_1), hash(ref_2)) + self.assertFalse(ref_1 == ref_2) class TestModelHashType(TestCase): def test_hash_type_from_composite_str_1(self) -> None: h = HashType.from_composite_str('sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b') - self.assertEqual(h.get_algorithm(), HashAlgorithm.SHA_256) - self.assertEqual(h.get_hash_value(), '806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b') + self.assertEqual(h.alg, HashAlgorithm.SHA_256) + self.assertEqual(h.content, '806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b') def test_hash_type_from_composite_str_2(self) -> None: h = HashType.from_composite_str('md5:dc26cd71b80d6757139f38156a43c545') - self.assertEqual(h.get_algorithm(), HashAlgorithm.MD5) - self.assertEqual(h.get_hash_value(), 'dc26cd71b80d6757139f38156a43c545') + self.assertEqual(h.alg, HashAlgorithm.MD5) + self.assertEqual(h.content, 'dc26cd71b80d6757139f38156a43c545') def test_hash_type_from_unknown(self) -> None: with self.assertRaises(UnknownHashTypeException): HashType.from_composite_str('unknown:dc26cd71b80d6757139f38156a43c545') +class TestModelIdentifiableAction(TestCase): + + def test_no_params(self) -> None: + with self.assertRaises(NoPropertiesProvidedException): + IdentifiableAction() + + def test_same(self) -> None: + ts = datetime.datetime.utcnow() + ia_1 = IdentifiableAction(timestamp=ts, name='A Name', email='something@somewhere.tld') + ia_2 = IdentifiableAction(timestamp=ts, name='A Name', email='something@somewhere.tld') + self.assertEqual(hash(ia_1), hash(ia_2)) + self.assertTrue(ia_1 == ia_2) + + def test_not_same(self) -> None: + ia_1 = IdentifiableAction(timestamp=datetime.datetime.utcnow(), name='A Name', email='something@somewhere.tld') + sleep(1) + ia_2 = IdentifiableAction(timestamp=datetime.datetime.utcnow(), name='A Name', email='something@somewhere.tld') + self.assertNotEqual(hash(ia_1), hash(ia_2)) + self.assertFalse(ia_1 == ia_2) + + class TestModelIssueType(TestCase): def test_issue_type(self) -> None: it = IssueType( - classification=IssueClassification.SECURITY, id='CVE-2021-44228', name='Apache Log3Shell', + classification=IssueClassification.SECURITY, id_='CVE-2021-44228', name='Apache Log3Shell', description='Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features used in ' 'configuration, log messages, and parameters do not protect against attacker controlled LDAP ' 'and other JNDI related endpoints. An attacker who can control log messages or log message ' @@ -55,17 +134,17 @@ def test_issue_type(self) -> None: 'version 2.16.0, this functionality has been completely removed. Note that this vulnerability ' 'is specific to log4j-core and does not affect log4net, log4cxx, or other Apache Logging ' 'Services projects.', - source_name='NVD', source_url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2021-44228'), + source=IssueTypeSource(name='NVD', url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2021-44228')), references=[ XsUri('https://logging.apache.org/log4j/2.x/security.html'), XsUri('https://central.sonatype.org/news/20211213_log4shell_help') ] ) - self.assertEqual(it.get_classification(), IssueClassification.SECURITY), - self.assertEqual(it.get_id(), 'CVE-2021-44228'), - self.assertEqual(it.get_name(), 'Apache Log3Shell') + self.assertEqual(it.type, IssueClassification.SECURITY), + self.assertEqual(it.id, 'CVE-2021-44228'), + self.assertEqual(it.name, 'Apache Log3Shell') self.assertEqual( - it.get_description(), + it.description, 'Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features used in ' 'configuration, log messages, and parameters do not protect against attacker controlled LDAP ' 'and other JNDI related endpoints. An attacker who can control log messages or log message ' @@ -75,23 +154,18 @@ def test_issue_type(self) -> None: 'is specific to log4j-core and does not affect log4net, log4cxx, or other Apache Logging ' 'Services projects.' ) - self.assertEqual(it.get_source_name(), 'NVD'), - self.assertEqual(str(it.get_source_url()), str(XsUri('https://nvd.nist.gov/vuln/detail/CVE-2021-44228'))) - self.assertEqual(str(it.get_source_url()), str('https://nvd.nist.gov/vuln/detail/CVE-2021-44228')) - self.assertListEqual(list(map(lambda u: str(u), it.get_references())), [ - 'https://logging.apache.org/log4j/2.x/security.html', - 'https://central.sonatype.org/news/20211213_log4shell_help' - ]) - self.assertListEqual(list(map(lambda u: str(u), it.get_references())), [ - 'https://logging.apache.org/log4j/2.x/security.html', - 'https://central.sonatype.org/news/20211213_log4shell_help' - ]) + self.assertEqual(it.source.name, 'NVD'), + self.assertEqual(it.source.url, XsUri('https://nvd.nist.gov/vuln/detail/CVE-2021-44228')) + self.assertSetEqual(it.references, { + XsUri('https://logging.apache.org/log4j/2.x/security.html'), + XsUri('https://central.sonatype.org/news/20211213_log4shell_help') + }) class TestModelNote(TestCase): def test_note_plain_text(self) -> None: - n = Note(text=NoteText('Some simple plain text')) + n = Note(text=NoteText(content='Some simple plain text')) self.assertEqual(n.text.content, 'Some simple plain text') self.assertEqual(n.text.content_type, NoteText.DEFAULT_CONTENT_TYPE) self.assertIsNone(n.locale) diff --git a/tests/test_model_component.py b/tests/test_model_component.py index a150b39a..4ae89060 100644 --- a/tests/test_model_component.py +++ b/tests/test_model_component.py @@ -1,13 +1,60 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# 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 datetime +from typing import List from unittest import TestCase from unittest.mock import Mock, patch -from cyclonedx.model import ExternalReference, ExternalReferenceType -from cyclonedx.model.component import Component, ComponentType +from cyclonedx.exception.model import NoPropertiesProvidedException +from cyclonedx.model import AttachedText, Copyright, ExternalReference, ExternalReferenceType, \ + IdentifiableAction, Property, XsUri +from cyclonedx.model.component import Commit, Component, ComponentEvidence, ComponentType, Diff, Patch, \ + PatchClassification, Pedigree +from data import get_component_setuptools_simple, get_component_setuptools_simple_no_version, \ + get_component_toml_with_hashes_with_references, get_issue_1, get_issue_2, get_pedigree_1, get_swid_1, get_swid_2 + + +class TestModelCommit(TestCase): + + def test_no_parameters(self) -> None: + with self.assertRaises(NoPropertiesProvidedException): + Commit() + + def test_same(self) -> None: + ia_comitter = IdentifiableAction(timestamp=datetime.datetime.utcnow(), name='The Committer') + c1 = Commit(uid='a-random-uid', author=ia_comitter, committer=ia_comitter, message="A commit message") + c2 = Commit(uid='a-random-uid', author=ia_comitter, committer=ia_comitter, message="A commit message") + self.assertEqual(hash(c1), hash(c2)) + self.assertTrue(c1 == c2) + + def test_not_same(self) -> None: + ia_author = IdentifiableAction(timestamp=datetime.datetime.utcnow(), name='The Author') + ia_comitter = IdentifiableAction(timestamp=datetime.datetime.utcnow(), name='The Committer') + c1 = Commit(uid='a-random-uid', author=ia_comitter, committer=ia_comitter, message="A commit message") + c2 = Commit(uid='a-random-uid', author=ia_author, committer=ia_comitter, message="A commit message") + self.assertNotEqual(hash(c1), hash(c2)) + self.assertFalse(c1 == c2) class TestModelComponent(TestCase): - @patch('cyclonedx.model.component.uuid4', return_value='6f266d1c-760f-4552-ae3b-41a9b74232fa') + @patch('cyclonedx.model.bom_ref.uuid4', return_value='6f266d1c-760f-4552-ae3b-41a9b74232fa') def test_empty_basic_component(self, mock_uuid: Mock) -> None: c = Component( name='test-component', version='1.2.3' @@ -16,7 +63,7 @@ def test_empty_basic_component(self, mock_uuid: Mock) -> None: self.assertEqual(c.name, 'test-component') self.assertEqual(c.type, ComponentType.LIBRARY) self.assertIsNone(c.mime_type) - self.assertEqual(c.bom_ref, '6f266d1c-760f-4552-ae3b-41a9b74232fa') + self.assertEqual(str(c.bom_ref), '6f266d1c-760f-4552-ae3b-41a9b74232fa') self.assertIsNone(c.supplier) self.assertIsNone(c.author) self.assertIsNone(c.publisher) @@ -24,17 +71,17 @@ def test_empty_basic_component(self, mock_uuid: Mock) -> None: self.assertEqual(c.version, '1.2.3') self.assertIsNone(c.description) self.assertIsNone(c.scope) - self.assertListEqual(c.hashes, []) - self.assertListEqual(c.licenses, []) + self.assertSetEqual(c.hashes, set()) + self.assertSetEqual(c.licenses, set()) self.assertIsNone(c.copyright) self.assertIsNone(c.purl) - self.assertListEqual(c.external_references, []) - self.assertIsNone(c.properties) + self.assertSetEqual(c.external_references, set()) + self.assertFalse(c.properties) self.assertIsNone(c.release_notes) self.assertEqual(len(c.get_vulnerabilities()), 0) - @patch('cyclonedx.model.component.uuid4', return_value='6f266d1c-760f-4552-ae3b-41a9b74232fa') + @patch('cyclonedx.model.bom_ref.uuid4', return_value='6f266d1c-760f-4552-ae3b-41a9b74232fa') def test_multiple_basic_components(self, mock_uuid: Mock) -> None: c1 = Component( name='test-component', version='1.2.3' @@ -64,9 +111,9 @@ def test_external_references(self) -> None: c = Component( name='test-component', version='1.2.3' ) - c.add_external_reference(ExternalReference( + c.external_references.add(ExternalReference( reference_type=ExternalReferenceType.OTHER, - url='https://cyclonedx.org', + url=XsUri('https://cyclonedx.org'), comment='No comment' )) self.assertEqual(c.name, 'test-component') @@ -97,23 +144,220 @@ def test_empty_basic_component_no_version(self) -> None: self.assertEqual(len(c.hashes), 0) self.assertEqual(len(c.get_vulnerabilities()), 0) - def test_component_equal(self) -> None: + def test_component_equal_1(self) -> None: c = Component( name='test-component', version='1.2.3' ) - c.add_external_reference(ExternalReference( + c.external_references.add(ExternalReference( reference_type=ExternalReferenceType.OTHER, - url='https://cyclonedx.org', + url=XsUri('https://cyclonedx.org'), comment='No comment' )) c2 = Component( name='test-component', version='1.2.3' ) - c2.add_external_reference(ExternalReference( + c2.external_references.add(ExternalReference( reference_type=ExternalReferenceType.OTHER, - url='https://cyclonedx.org', + url=XsUri('https://cyclonedx.org'), comment='No comment' )) self.assertEqual(c, c2) + + def test_component_equal_2(self) -> None: + props: List[Property] = [ + Property(name='prop1', value='val1'), + Property(name='prop2', value='val2') + ] + + c = Component( + name='test-component', version='1.2.3', properties=props + ) + c2 = Component( + name='test-component', version='1.2.3', properties=props + ) + + self.assertEqual(c, c2) + + def test_component_equal_3(self) -> None: + c = Component( + name='test-component', version='1.2.3', properties=[ + Property(name='prop1', value='val1'), + Property(name='prop2', value='val2') + ] + ) + c2 = Component( + name='test-component', version='1.2.3', properties=[ + Property(name='prop3', value='val3'), + Property(name='prop4', value='val4') + ] + ) + + self.assertNotEqual(c, c2) + + def test_same_1(self) -> None: + c1 = get_component_setuptools_simple() + c2 = get_component_setuptools_simple() + self.assertNotEqual(id(c1), id(c2)) + self.assertEqual(hash(c1), hash(c2)) + self.assertTrue(c1 == c2) + + def test_same_2(self) -> None: + c1 = get_component_toml_with_hashes_with_references() + c2 = get_component_toml_with_hashes_with_references() + self.assertNotEqual(id(c1), id(c2)) + self.assertEqual(hash(c1), hash(c2)) + self.assertTrue(c1 == c2) + + def test_same_3(self) -> None: + c1 = get_component_setuptools_simple_no_version() + c2 = get_component_setuptools_simple_no_version() + self.assertNotEqual(id(c1), id(c2)) + self.assertEqual(hash(c1), hash(c2)) + self.assertTrue(c1 == c2) + + def test_not_same_1(self) -> None: + c1 = get_component_setuptools_simple() + c2 = get_component_setuptools_simple_no_version() + self.assertNotEqual(id(c1), id(c2)) + self.assertNotEqual(hash(c1), hash(c2)) + self.assertFalse(c1 == c2) + + +class TestModelComponentEvidence(TestCase): + + def test_no_params(self) -> None: + with self.assertRaises(NoPropertiesProvidedException): + ComponentEvidence() + + def test_same_1(self) -> None: + ce_1 = ComponentEvidence(copyright_=[Copyright(text='Commercial')]) + ce_2 = ComponentEvidence(copyright_=[Copyright(text='Commercial')]) + self.assertEqual(hash(ce_1), hash(ce_2)) + self.assertTrue(ce_1 == ce_2) + + def test_same_2(self) -> None: + ce_1 = ComponentEvidence(copyright_=[Copyright(text='Commercial'), Copyright(text='Commercial 2')]) + ce_2 = ComponentEvidence(copyright_=[Copyright(text='Commercial 2'), Copyright(text='Commercial')]) + self.assertEqual(hash(ce_1), hash(ce_2)) + self.assertTrue(ce_1 == ce_2) + + def test_not_same_1(self) -> None: + ce_1 = ComponentEvidence(copyright_=[Copyright(text='Commercial')]) + ce_2 = ComponentEvidence(copyright_=[Copyright(text='Commercial 2')]) + self.assertNotEqual(hash(ce_1), hash(ce_2)) + self.assertFalse(ce_1 == ce_2) + + +class TestModelDiff(TestCase): + + def test_no_params(self) -> None: + with self.assertRaises(NoPropertiesProvidedException): + Diff() + + def test_same(self) -> None: + at = AttachedText(content='A very long diff') + diff_1 = Diff(text=at, url=XsUri('https://cyclonedx.org')) + diff_2 = Diff(text=at, url=XsUri('https://cyclonedx.org')) + self.assertEqual(hash(diff_1), hash(diff_2)) + self.assertTrue(diff_1 == diff_2) + + def test_not_same(self) -> None: + at = AttachedText(content='A very long diff') + diff_1 = Diff(text=at, url=XsUri('https://cyclonedx.org/')) + diff_2 = Diff(text=at, url=XsUri('https://cyclonedx.org')) + self.assertNotEqual(hash(diff_1), hash(diff_2)) + self.assertFalse(diff_1 == diff_2) + + +class TestModelPatch(TestCase): + + def test_same_1(self) -> None: + p1 = Patch( + type_=PatchClassification.BACKPORT, diff=Diff(url=XsUri('https://cyclonedx.org')), + resolves=[get_issue_1(), get_issue_2()] + ) + p2 = Patch( + type_=PatchClassification.BACKPORT, diff=Diff(url=XsUri('https://cyclonedx.org')), + resolves=[get_issue_2(), get_issue_1()] + ) + self.assertEqual(hash(p1), hash(p2)) + self.assertNotEqual(id(p1), id(p2)) + self.assertTrue(p1 == p2) + + def test_multiple_times_same(self) -> None: + i = 0 + while i < 1000: + p1 = Patch( + type_=PatchClassification.BACKPORT, diff=Diff(url=XsUri('https://cyclonedx.org')), + resolves=[get_issue_1(), get_issue_2()] + ) + p2 = Patch( + type_=PatchClassification.BACKPORT, diff=Diff(url=XsUri('https://cyclonedx.org')), + resolves=[get_issue_2(), get_issue_1(), get_issue_1(), get_issue_1(), get_issue_2()] + ) + self.assertEqual(hash(p1), hash(p2)) + self.assertNotEqual(id(p1), id(p2)) + self.assertTrue(p1 == p2) + + i += 1 + + def test_not_same_1(self) -> None: + p1 = Patch( + type_=PatchClassification.MONKEY, diff=Diff(url=XsUri('https://cyclonedx.org/')), + resolves=[get_issue_1(), get_issue_2()] + ) + p2 = Patch( + type_=PatchClassification.BACKPORT, diff=Diff(url=XsUri('https://cyclonedx.org')), + resolves=[get_issue_2(), get_issue_1()] + ) + self.assertNotEqual(hash(p1), hash(p2)) + self.assertNotEqual(id(p1), id(p2)) + self.assertFalse(p1 == p2) + + +class TestModelPedigree(TestCase): + + def test_no_params(self) -> None: + with self.assertRaises(NoPropertiesProvidedException): + Pedigree() + + def test_same_1(self) -> None: + p1 = get_pedigree_1() + p2 = get_pedigree_1() + self.assertNotEqual(id(p1), id(p2)) + self.assertEqual(hash(p1), hash(p2)) + self.assertTrue(p1 == p2) + + def test_not_same_1(self) -> None: + p1 = get_pedigree_1() + p2 = get_pedigree_1() + p2.notes = 'Some other notes here' + self.assertNotEqual(id(p1), id(p2)) + self.assertNotEqual(hash(p1), hash(p2)) + self.assertFalse(p1 == p2) + + +class TestModelSwid(TestCase): + + def test_same_1(self) -> None: + sw_1 = get_swid_1() + sw_2 = get_swid_1() + self.assertNotEqual(id(sw_1), id(sw_2)) + self.assertEqual(hash(sw_1), hash(sw_2)) + self.assertTrue(sw_1 == sw_2) + + def test_same_2(self) -> None: + sw_1 = get_swid_2() + sw_2 = get_swid_2() + self.assertNotEqual(id(sw_1), id(sw_2)) + self.assertEqual(hash(sw_1), hash(sw_2)) + self.assertTrue(sw_1 == sw_2) + + def test_not_same(self) -> None: + sw_1 = get_swid_1() + sw_2 = get_swid_2() + self.assertNotEqual(id(sw_1), id(sw_2)) + self.assertNotEqual(hash(sw_1), hash(sw_2)) + self.assertFalse(sw_1 == sw_2) diff --git a/tests/test_model_issue.py b/tests/test_model_issue.py new file mode 100644 index 00000000..18fddf1b --- /dev/null +++ b/tests/test_model_issue.py @@ -0,0 +1,63 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# 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. +from unittest import TestCase + +from cyclonedx.exception.model import NoPropertiesProvidedException +from cyclonedx.model import XsUri +from cyclonedx.model.issue import IssueTypeSource + +from data import get_issue_1, get_issue_2 + + +class TestModelIssueType(TestCase): + + def test_same(self) -> None: + i_1 = get_issue_1() + i_2 = get_issue_1() + self.assertNotEqual(id(i_1), id(i_2)) + self.assertEqual(hash(i_1), hash(i_2)) + self.assertTrue(i_1 == i_2) + + def test_not_same(self) -> None: + i_1 = get_issue_1() + i_2 = get_issue_2() + self.assertNotEqual(id(i_1), id(i_2)) + self.assertNotEqual(hash(i_1), hash(i_2)) + self.assertFalse(i_1 == i_2) + + +class TestModelIssueTypeSource(TestCase): + + def test_no_params(self) -> None: + with self.assertRaises(NoPropertiesProvidedException): + IssueTypeSource() + + def test_same(self) -> None: + its_1 = IssueTypeSource(name="The Source", url=XsUri('https://cyclonedx.org')) + its_2 = IssueTypeSource(name="The Source", url=XsUri('https://cyclonedx.org')) + self.assertNotEqual(id(its_1), id(its_2)) + self.assertEqual(hash(its_1), hash(its_2)) + self.assertTrue(its_1 == its_2) + + def test_not_same(self) -> None: + its_1 = IssueTypeSource(name="The Source", url=XsUri('https://cyclonedx.org')) + its_2 = IssueTypeSource(name="Not the Source", url=XsUri('https://cyclonedx.org')) + self.assertNotEqual(id(its_1), id(its_2)) + self.assertNotEqual(hash(its_1), hash(its_2)) + self.assertFalse(its_1 == its_2) diff --git a/tests/test_model_release_note.py b/tests/test_model_release_note.py index 33af0c3c..b544e58f 100644 --- a/tests/test_model_release_note.py +++ b/tests/test_model_release_note.py @@ -1,3 +1,21 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# 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 datetime from unittest import TestCase @@ -8,23 +26,23 @@ class TestModelReleaseNote(TestCase): def test_simple(self) -> None: - rn = ReleaseNotes(type='major') + rn = ReleaseNotes(type_='major') self.assertEqual(rn.type, 'major') self.assertIsNone(rn.title) self.assertIsNone(rn.featured_image) self.assertIsNone(rn.social_image) self.assertIsNone(rn.description) self.assertIsNone(rn.timestamp) - self.assertIsNone(rn.aliases) - self.assertIsNone(rn.tags) - self.assertIsNone(rn.resolves) - self.assertIsNone(rn.notes) - self.assertIsNone(rn.properties) + self.assertFalse(rn.aliases) + self.assertFalse(rn.tags) + self.assertFalse(rn.resolves) + self.assertFalse(rn.notes) + self.assertFalse(rn.properties) def test_complete(self) -> None: timestamp: datetime.datetime = datetime.datetime.utcnow() rn = ReleaseNotes( - type='major', title="Release Notes Title", + type_='major', title="Release Notes Title", featured_image=XsUri('https://cyclonedx.org/theme/assets/images/CycloneDX-Twitter-Card.png'), social_image=XsUri('https://cyclonedx.org/cyclonedx-icon.png'), description="This release is a test release", timestamp=timestamp, @@ -35,8 +53,8 @@ def test_complete(self) -> None: resolves=[], notes=[] ) - rn.add_alias(alias="Release Alpha") - rn.add_tag(tag='testing') + rn.aliases.add("Release Alpha") + rn.tags.add('testing') self.assertEqual(rn.type, 'major') self.assertEqual(rn.title, 'Release Notes Title') @@ -46,8 +64,8 @@ def test_complete(self) -> None: ) self.assertEqual(str(rn.social_image), 'https://cyclonedx.org/cyclonedx-icon.png') self.assertEqual(rn.description, 'This release is a test release') - self.assertListEqual(rn.aliases, ["First Test Release", "Release Alpha"]) - self.assertListEqual(rn.tags, ['test', 'alpha', 'testing']) - self.assertIsNone(rn.resolves) - self.assertIsNone(rn.notes) - self.assertIsNone(rn.properties) + self.assertSetEqual(rn.aliases, {"Release Alpha", "First Test Release"}) + self.assertSetEqual(rn.tags, {'test', 'testing', 'alpha'}) + self.assertSetEqual(rn.resolves, set()) + self.assertFalse(rn.notes) + self.assertSetEqual(rn.properties, set()) diff --git a/tests/test_model_service.py b/tests/test_model_service.py new file mode 100644 index 00000000..555bcad3 --- /dev/null +++ b/tests/test_model_service.py @@ -0,0 +1,71 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# 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. +from unittest import TestCase +from unittest.mock import Mock, patch + +from cyclonedx.model.service import Service + + +class TestModelService(TestCase): + + @patch('cyclonedx.model.bom_ref.uuid4', return_value='77d15ab9-5602-4cca-8ed2-59ae579aafd3') + def test_minimal_service(self, mock_uuid: Mock) -> None: + s = Service(name='my-test-service') + mock_uuid.assert_called() + self.assertEqual(s.name, 'my-test-service') + self.assertEqual(str(s.bom_ref), '77d15ab9-5602-4cca-8ed2-59ae579aafd3') + self.assertIsNone(s.provider) + self.assertIsNone(s.group) + self.assertIsNone(s.version) + self.assertIsNone(s.description) + self.assertFalse(s.endpoints) + self.assertIsNone(s.authenticated) + self.assertIsNone(s.x_trust_boundary) + self.assertFalse(s.data) + self.assertFalse(s.licenses) + self.assertFalse(s.external_references) + self.assertFalse(s.services) + self.assertFalse(s.release_notes) + self.assertFalse(s.properties) + + @patch('cyclonedx.model.bom_ref.uuid4', return_value='859ff614-35a7-4d37-803b-d89130cb2577') + def test_service_with_services(self, mock_uuid: Mock) -> None: + parent_service = Service(name='parent-service') + parent_service.services = [ + Service(name='child-service-1'), + Service(name='child-service-2') + ] + mock_uuid.assert_called() + self.assertEqual(parent_service.name, 'parent-service') + self.assertEqual(str(parent_service.bom_ref), '859ff614-35a7-4d37-803b-d89130cb2577') + self.assertIsNone(parent_service.provider) + self.assertIsNone(parent_service.group) + self.assertIsNone(parent_service.version) + self.assertIsNone(parent_service.description) + self.assertFalse(parent_service.endpoints) + self.assertIsNone(parent_service.authenticated) + self.assertIsNone(parent_service.x_trust_boundary) + self.assertFalse(parent_service.data) + self.assertFalse(parent_service.licenses) + self.assertFalse(parent_service.external_references) + self.assertIsNotNone(parent_service.services) + self.assertEqual(len(parent_service.services), 2) + self.assertIsNone(parent_service.release_notes) + self.assertFalse(parent_service.properties) + self.assertTrue(Service(name='child-service-1') in parent_service.services) diff --git a/tests/test_model_vulnerability.py b/tests/test_model_vulnerability.py index fe302c43..0e239b7c 100644 --- a/tests/test_model_vulnerability.py +++ b/tests/test_model_vulnerability.py @@ -1,3 +1,21 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# 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 unittest from unittest import TestCase from unittest.mock import Mock, patch @@ -11,17 +29,17 @@ class TestModelVulnerability(TestCase): @unittest.skip('Deprecated in Schema Version 1.4') def test_v_rating_scores_empty(self) -> None: vr = VulnerabilityRating() - self.assertFalse(vr.has_score()) + self.assertFalse(vr.score) @unittest.skip('Deprecated in Schema Version 1.4') def test_v_rating_scores_base_only(self) -> None: vr = VulnerabilityRating(score_base=1.0) - self.assertTrue(vr.has_score()) + self.assertTrue(vr.score) @unittest.skip('Deprecated in Schema Version 1.4') def test_v_rating_scores_all(self) -> None: vr = VulnerabilityRating(score_base=1.0, score_impact=3.5, score_exploitability=5.6) - self.assertTrue(vr.has_score()) + self.assertTrue(vr.score) def test_v_severity_from_cvss_scores_single_critical(self) -> None: self.assertEqual( @@ -152,24 +170,24 @@ def test_v_source_get_localised_vector_other_2(self) -> None: 'SOMETHING_OR_OTHER' ) - @patch('cyclonedx.model.vulnerability.uuid4', return_value='0afa65bc-4acd-428b-9e17-8e97b1969745') + @patch('cyclonedx.model.bom_ref.uuid4', return_value='0afa65bc-4acd-428b-9e17-8e97b1969745') def test_empty_vulnerability(self, mock_uuid: Mock) -> None: v = Vulnerability() mock_uuid.assert_called() - self.assertEqual(v.bom_ref, '0afa65bc-4acd-428b-9e17-8e97b1969745') + self.assertEqual(str(v.bom_ref), '0afa65bc-4acd-428b-9e17-8e97b1969745') self.assertIsNone(v.id) self.assertIsNone(v.source) - self.assertListEqual(v.references, []) - self.assertListEqual(v.ratings, []) - self.assertListEqual(v.cwes, []) + self.assertFalse(v.references) + self.assertFalse(v.ratings) + self.assertFalse(v.cwes) self.assertIsNone(v.description) self.assertIsNone(v.detail) self.assertIsNone(v.recommendation) - self.assertListEqual(v.advisories, []) + self.assertFalse(v.advisories) self.assertIsNone(v.created) self.assertIsNone(v.published) self.assertIsNone(v.updated) self.assertIsNone(v.credits) - self.assertListEqual(v.tools, []) + self.assertFalse(v.tools) self.assertIsNone(v.analysis) - self.assertListEqual(v.affects, []) + self.assertFalse(v.affects) diff --git a/tests/test_output_generic.py b/tests/test_output_generic.py index dc293d7c..b0be42e0 100644 --- a/tests/test_output_generic.py +++ b/tests/test_output_generic.py @@ -19,11 +19,10 @@ from unittest import TestCase -from cyclonedx.exception.output import ComponentVersionRequiredException from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component from cyclonedx.output import get_instance, OutputFormat, SchemaVersion -from cyclonedx.output.xml import XmlV1Dot3, Xml +from cyclonedx.output.xml import XmlV1Dot3, XmlV1Dot4 class TestOutputGeneric(TestCase): @@ -31,21 +30,20 @@ class TestOutputGeneric(TestCase): @classmethod def setUpClass(cls) -> None: cls._bom = Bom() - cls._bom.add_component(Component(name='setuptools')) + cls._bom.components.add(Component(name='setuptools')) def test_get_instance_default(self) -> None: i = get_instance(bom=TestOutputGeneric._bom) - self.assertIsInstance(i, XmlV1Dot3) + self.assertIsInstance(i, XmlV1Dot4) - def test_get_instance_xml(self) -> None: + def test_get_instance_xml_default(self) -> None: i = get_instance(bom=TestOutputGeneric._bom, output_format=OutputFormat.XML) - self.assertIsInstance(i, XmlV1Dot3) + self.assertIsInstance(i, XmlV1Dot4) def test_get_instance_xml_v1_3(self) -> None: i = get_instance(bom=TestOutputGeneric._bom, output_format=OutputFormat.XML, schema_version=SchemaVersion.V1_3) self.assertIsInstance(i, XmlV1Dot3) def test_component_no_version_v1_3(self) -> None: - with self.assertRaises(ComponentVersionRequiredException): - outputter: Xml = get_instance(bom=TestOutputGeneric._bom, schema_version=SchemaVersion.V1_3) - outputter.output_as_string() + i = get_instance(bom=TestOutputGeneric._bom, schema_version=SchemaVersion.V1_3) + self.assertIsInstance(i, XmlV1Dot3) diff --git a/tests/test_output_json.py b/tests/test_output_json.py index c424e2f5..2f8d6ab0 100644 --- a/tests/test_output_json.py +++ b/tests/test_output_json.py @@ -16,383 +16,274 @@ # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. -import base64 -from decimal import Decimal -from datetime import datetime, timezone from os.path import dirname, join -from packageurl import PackageURL from unittest.mock import Mock, patch -from cyclonedx.model import Encoding, ExternalReference, ExternalReferenceType, HashType, LicenseChoice, Note, \ - NoteText, OrganizationalContact, OrganizationalEntity, Property, Tool, XsUri +from cyclonedx.exception.output import FormatNotSupportedException from cyclonedx.model.bom import Bom -from cyclonedx.model.component import Component, ComponentType -from cyclonedx.model.issue import IssueClassification, IssueType -from cyclonedx.model.release_note import ReleaseNotes -from cyclonedx.model.vulnerability import ImpactAnalysisState, ImpactAnalysisJustification, ImpactAnalysisResponse, \ - ImpactAnalysisAffectedStatus, Vulnerability, VulnerabilityCredits, VulnerabilityRating, VulnerabilitySeverity, \ - VulnerabilitySource, VulnerabilityScoreSource, VulnerabilityAdvisory, VulnerabilityReference, \ - VulnerabilityAnalysis, BomTarget, BomTargetVersionRange from cyclonedx.output import get_instance, OutputFormat, SchemaVersion -from cyclonedx.output.json import Json, JsonV1Dot4, JsonV1Dot3, JsonV1Dot2 +from data import get_bom_with_component_setuptools_basic, get_bom_with_component_setuptools_with_cpe, \ + get_bom_with_services_simple, get_bom_with_component_toml_1, \ + get_bom_with_component_setuptools_no_component_version, \ + get_bom_with_component_setuptools_with_release_notes, get_bom_with_component_setuptools_with_vulnerability, \ + MOCK_UUID_1, get_bom_just_complete_metadata, MOCK_UUID_2, MOCK_UUID_3, TEST_UUIDS, get_bom_with_services_complex, \ + get_bom_with_nested_services, get_bom_with_component_setuptools_complete, get_bom_with_external_references from tests.base import BaseJsonTestCase class TestOutputJson(BaseJsonTestCase): + def test_bom_external_references_v1_4(self) -> None: + self._validate_json_bom( + bom=get_bom_with_external_references(), schema_version=SchemaVersion.V1_4, + fixture='bom_external_references.json' + ) + + def test_bom_external_references_v1_3(self) -> None: + self._validate_json_bom( + bom=get_bom_with_external_references(), schema_version=SchemaVersion.V1_3, + fixture='bom_external_references.json' + ) + + def test_bom_external_references_v1_2(self) -> None: + self._validate_json_bom( + bom=get_bom_with_external_references(), schema_version=SchemaVersion.V1_2, + fixture='bom_external_references.json' + ) + def test_simple_bom_v1_4(self) -> None: - bom = Bom() - c = Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) - ) - bom.add_component(c) - - outputter = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=SchemaVersion.V1_4) - self.assertIsInstance(outputter, JsonV1Dot4) - with open(join(dirname(__file__), 'fixtures/bom_v1.4_setuptools.json')) as expected_json: - self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_4) - self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string()) - expected_json.close() + self._validate_json_bom( + bom=get_bom_with_component_setuptools_basic(), schema_version=SchemaVersion.V1_4, + fixture='bom_setuptools.json' + ) def test_simple_bom_v1_3(self) -> None: - bom = Bom() - c = Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ), license_str='MIT License' - ) - bom.add_component(c) - - outputter = get_instance(bom=bom, output_format=OutputFormat.JSON) - self.assertIsInstance(outputter, JsonV1Dot3) - with open(join(dirname(__file__), 'fixtures/bom_v1.3_setuptools.json')) as expected_json: - self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string()) - expected_json.close() + self._validate_json_bom( + bom=get_bom_with_component_setuptools_basic(), schema_version=SchemaVersion.V1_3, + fixture='bom_setuptools.json' + ) def test_simple_bom_v1_2(self) -> None: - bom = Bom() - bom.add_component( - Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ), author='Test Author' - ) - ) - outputter = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=SchemaVersion.V1_2) - self.assertIsInstance(outputter, JsonV1Dot2) - with open(join(dirname(__file__), 'fixtures/bom_v1.2_setuptools.json')) as expected_json: - self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_2) - self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string()) - expected_json.close() + self._validate_json_bom( + bom=get_bom_with_component_setuptools_basic(), schema_version=SchemaVersion.V1_2, + fixture='bom_setuptools.json' + ) + + def test_simple_bom_v1_1(self) -> None: + self._validate_json_bom_not_supported( + bom=get_bom_with_component_setuptools_basic(), schema_version=SchemaVersion.V1_1 + ) + + def test_simple_bom_v1_0(self) -> None: + self._validate_json_bom_not_supported( + bom=get_bom_with_component_setuptools_basic(), schema_version=SchemaVersion.V1_0 + ) def test_simple_bom_v1_4_with_cpe(self) -> None: - bom = Bom() - c = Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) - ) - bom.add_component(c) - - outputter = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=SchemaVersion.V1_4) - self.assertIsInstance(outputter, JsonV1Dot4) - with open(join(dirname(__file__), 'fixtures/bom_v1.4_setuptools_with_cpe.json')) as expected_json: - self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_4) - self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string()) - expected_json.close() + self._validate_json_bom( + bom=get_bom_with_component_setuptools_with_cpe(), schema_version=SchemaVersion.V1_4, + fixture='bom_setuptools_with_cpe.json' + ) def test_simple_bom_v1_3_with_cpe(self) -> None: - bom = Bom() - c = Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ), license_str='MIT License' - ) - bom.add_component(c) - - outputter = get_instance(bom=bom, output_format=OutputFormat.JSON) - self.assertIsInstance(outputter, JsonV1Dot3) - with open(join(dirname(__file__), 'fixtures/bom_v1.3_setuptools_with_cpe.json')) as expected_json: - self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string()) - expected_json.close() + self._validate_json_bom( + bom=get_bom_with_component_setuptools_with_cpe(), schema_version=SchemaVersion.V1_3, + fixture='bom_setuptools_with_cpe.json' + ) def test_simple_bom_v1_2_with_cpe(self) -> None: - bom = Bom() - bom.add_component( - Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ), author='Test Author' - ) - ) - outputter = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=SchemaVersion.V1_2) - self.assertIsInstance(outputter, JsonV1Dot2) - with open(join(dirname(__file__), 'fixtures/bom_v1.2_setuptools_with_cpe.json')) as expected_json: - self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_2) - self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string()) - expected_json.close() + self._validate_json_bom( + bom=get_bom_with_component_setuptools_with_cpe(), schema_version=SchemaVersion.V1_2, + fixture='bom_setuptools_with_cpe.json' + ) - def test_bom_v1_3_with_component_hashes(self) -> None: - bom = Bom() - c = Component( - name='toml', version='0.10.2', bom_ref='pkg:pypi/toml@0.10.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='toml', version='0.10.2', qualifiers='extension=tar.gz' - ) - ) - c.add_hash( - HashType.from_composite_str('sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b') - ) - bom.add_component(c) - outputter: Json = get_instance(bom=bom, output_format=OutputFormat.JSON) - self.assertIsInstance(outputter, JsonV1Dot3) - with open(join(dirname(__file__), 'fixtures/bom_v1.3_toml_with_component_hashes.json')) as expected_json: - self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualJsonBom(a=outputter.output_as_string(), b=expected_json.read()) - expected_json.close() + def test_bom_v1_4_full_component(self) -> None: + self.maxDiff = None + self._validate_json_bom( + bom=get_bom_with_component_setuptools_complete(), schema_version=SchemaVersion.V1_4, + fixture='bom_setuptools_complete.json' + ) - def test_bom_v1_3_with_component_external_references(self) -> None: - bom = Bom() - c = Component( - name='toml', version='0.10.2', bom_ref='pkg:pypi/toml@0.10.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='toml', version='0.10.2', qualifiers='extension=tar.gz' - ) - ) - c.add_hash( - HashType.from_composite_str('sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b') - ) - c.add_external_reference( - ExternalReference( - reference_type=ExternalReferenceType.DISTRIBUTION, - url='https://cyclonedx.org', - comment='No comment', - hashes=[ - HashType.from_composite_str( - 'sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b') - ] - ) - ) - bom.add_component(c) - outputter: Json = get_instance(bom=bom, output_format=OutputFormat.JSON) - self.assertIsInstance(outputter, JsonV1Dot3) - with open(join(dirname(__file__), - 'fixtures/bom_v1.3_toml_with_component_external_references.json')) as expected_json: - self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualJsonBom(a=outputter.output_as_string(), b=expected_json.read()) - expected_json.close() + def test_bom_v1_3_full_component(self) -> None: + self._validate_json_bom( + bom=get_bom_with_component_setuptools_complete(), schema_version=SchemaVersion.V1_3, + fixture='bom_setuptools_complete.json' + ) - def test_bom_v1_3_with_component_license(self) -> None: - bom = Bom() - c = Component( - name='toml', version='0.10.2', bom_ref='pkg:pypi/toml@0.10.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='toml', version='0.10.2', qualifiers='extension=tar.gz' - ), license_str='MIT License' - ) - bom.add_component(c) - outputter: Json = get_instance(bom=bom, output_format=OutputFormat.JSON) - self.assertIsInstance(outputter, JsonV1Dot3) - with open(join(dirname(__file__), - 'fixtures/bom_v1.3_toml_with_component_license.json')) as expected_json: - self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualJsonBom(a=outputter.output_as_string(), b=expected_json.read()) - expected_json.close() + def test_bom_v1_2_full_component(self) -> None: + self._validate_json_bom( + bom=get_bom_with_component_setuptools_complete(), schema_version=SchemaVersion.V1_2, + fixture='bom_setuptools_complete.json' + ) + + def test_bom_v1_4_component_hashes_external_references(self) -> None: + self._validate_json_bom( + bom=get_bom_with_component_toml_1(), schema_version=SchemaVersion.V1_4, + fixture='bom_toml_1.json' + ) + + def test_bom_v1_3_component_hashes_external_references(self) -> None: + self._validate_json_bom( + bom=get_bom_with_component_toml_1(), schema_version=SchemaVersion.V1_3, + fixture='bom_toml_1.json' + ) + + def test_bom_v1_2_component_hashes_external_references(self) -> None: + self._validate_json_bom( + bom=get_bom_with_component_toml_1(), schema_version=SchemaVersion.V1_2, + fixture='bom_toml_1.json' + ) + + def test_bom_v1_1_component_hashes_external_references(self) -> None: + self._validate_json_bom_not_supported(bom=get_bom_with_component_toml_1(), schema_version=SchemaVersion.V1_1) + + def test_bom_v1_0_component_hashes_external_references(self) -> None: + self._validate_json_bom_not_supported(bom=get_bom_with_component_toml_1(), schema_version=SchemaVersion.V1_0) def test_bom_v1_4_no_component_version(self) -> None: - bom = Bom() - c = Component( - name='setuptools', bom_ref='pkg:pypi/setuptools?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', qualifiers='extension=tar.gz' - ) - ) - bom.add_component(c) - - outputter = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=SchemaVersion.V1_4) - self.assertIsInstance(outputter, JsonV1Dot4) - with open(join(dirname(__file__), 'fixtures/bom_v1.4_setuptools_no_version.json')) as expected_json: - self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_4) - self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string()) - expected_json.close() + self._validate_json_bom( + bom=get_bom_with_component_setuptools_no_component_version(), schema_version=SchemaVersion.V1_4, + fixture='bom_setuptools_no_version.json' + ) - def test_with_component_release_notes_pre_1_4(self) -> None: - bom = Bom() - c = Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ), release_notes=ReleaseNotes(type='major'), licenses=[LicenseChoice(license_expression='MIT License')] - ) - bom.add_component(c) - outputter: Json = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=SchemaVersion.V1_3) - self.assertIsInstance(outputter, JsonV1Dot3) - with open(join(dirname(__file__), 'fixtures/bom_v1.3_setuptools.json')) as expected_json: - self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string()) - expected_json.close() + def test_bom_v1_3_no_component_version(self) -> None: + self._validate_json_bom( + bom=get_bom_with_component_setuptools_no_component_version(), schema_version=SchemaVersion.V1_3, + fixture='bom_setuptools_no_version.json' + ) - def test_with_component_release_notes_post_1_4(self) -> None: - bom = Bom() - timestamp: datetime = datetime(2021, 12, 31, 10, 0, 0, 0).replace(tzinfo=timezone.utc) - - text_content: str = base64.b64encode( - bytes('Some simple plain text', encoding='UTF-8') - ).decode(encoding='UTF-8') - - c = Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ), - release_notes=ReleaseNotes( - type='major', title="Release Notes Title", - featured_image=XsUri('https://cyclonedx.org/theme/assets/images/CycloneDX-Twitter-Card.png'), - social_image=XsUri('https://cyclonedx.org/cyclonedx-icon.png'), - description="This release is a test release", timestamp=timestamp, - aliases=[ - "First Test Release" - ], - tags=['test', 'alpha'], - resolves=[ - IssueType( - classification=IssueClassification.SECURITY, id='CVE-2021-44228', name='Apache Log3Shell', - description='Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features...', - source_name='NVD', source_url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2021-44228'), - references=[ - XsUri('https://logging.apache.org/log4j/2.x/security.html'), - XsUri('https://central.sonatype.org/news/20211213_log4shell_help') - ] - ) - ], - notes=[ - Note( - text=NoteText( - content=text_content, content_type='text/plain; charset=UTF-8', - content_encoding=Encoding.BASE_64 - ), locale='en-GB' - ), - Note( - text=NoteText( - content=text_content, content_type='text/plain; charset=UTF-8', - content_encoding=Encoding.BASE_64 - ), locale='en-US' - ) - ], - properties=[ - Property(name='key1', value='val1'), - Property(name='key2', value='val2') - ] - ) - ) - bom.add_component(c) - outputter: Json = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=SchemaVersion.V1_4) - self.assertIsInstance(outputter, JsonV1Dot4) - with open(join(dirname(__file__), - 'fixtures/bom_v1.4_setuptools_with_release_notes.json')) as expected_json: - self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string()) - expected_json.close() + def test_bom_v1_4_component_with_release_notes(self) -> None: + self._validate_json_bom( + bom=get_bom_with_component_setuptools_with_release_notes(), schema_version=SchemaVersion.V1_4, + fixture='bom_setuptools_with_release_notes.json' + ) - def test_simple_bom_v1_4_with_vulnerabilities(self) -> None: - bom = Bom() - nvd = VulnerabilitySource(name='NVD', url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2018-7489')) - owasp = VulnerabilitySource(name='OWASP', url=XsUri('https://owasp.org')) - c = Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) - ) - c.add_vulnerability(Vulnerability( - bom_ref='my-vuln-ref-1', id='CVE-2018-7489', source=nvd, - references=[ - VulnerabilityReference(id='SOME-OTHER-ID', source=VulnerabilitySource( - name='OSS Index', url=XsUri('https://ossindex.sonatype.org/component/pkg:pypi/setuptools') - )) - ], - ratings=[ - VulnerabilityRating( - source=nvd, score=Decimal(9.8), severity=VulnerabilitySeverity.CRITICAL, - method=VulnerabilityScoreSource.CVSS_V3, - vector='AN/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H', justification='Some justification' - ), - VulnerabilityRating( - source=owasp, score=Decimal(2.7), severity=VulnerabilitySeverity.LOW, - method=VulnerabilityScoreSource.CVSS_V3, - vector='AV:L/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N', justification='Some other justification' - ) - ], - cwes=[22, 33], description='A description here', detail='Some detail here', - recommendation='Upgrade', - advisories=[ - VulnerabilityAdvisory(url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2018-7489')), - VulnerabilityAdvisory(url=XsUri('http://www.securitytracker.com/id/1040693')) - ], - created=datetime(year=2021, month=9, day=1, hour=10, minute=50, second=42, microsecond=51979, - tzinfo=timezone.utc), - published=datetime(year=2021, month=9, day=2, hour=10, minute=50, second=42, microsecond=51979, - tzinfo=timezone.utc), - updated=datetime(year=2021, month=9, day=3, hour=10, minute=50, second=42, microsecond=51979, - tzinfo=timezone.utc), - credits=VulnerabilityCredits( - organizations=[ - OrganizationalEntity( - name='CycloneDX', urls=[XsUri('https://cyclonedx.org')], contacts=[ - OrganizationalContact(name='Paul Horton', email='simplyecommerce@googlemail.com'), - OrganizationalContact(name='A N Other', email='someone@somewhere.tld', - phone='+44 (0)1234 567890') - ] - ) - ], - individuals=[ - OrganizationalContact(name='A N Other', email='someone@somewhere.tld', phone='+44 (0)1234 567890'), - ] - ), - tools=[ - Tool(vendor='CycloneDX', name='cyclonedx-python-lib') - ], - analysis=VulnerabilityAnalysis( - state=ImpactAnalysisState.EXPLOITABLE, justification=ImpactAnalysisJustification.REQUIRES_ENVIRONMENT, - responses=[ImpactAnalysisResponse.CAN_NOT_FIX], detail='Some extra detail' - ), - affects_targets=[ - BomTarget(ref=c.purl or c.to_package_url().to_string(), versions=[ - BomTargetVersionRange(version_range='49.0.0 - 54.0.0', status=ImpactAnalysisAffectedStatus.AFFECTED) - ]) - ] - )) - bom.add_component(c) - outputter: Json = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=SchemaVersion.V1_4) - self.assertIsInstance(outputter, JsonV1Dot4) - with open(join(dirname(__file__), 'fixtures/bom_v1.4_setuptools_with_vulnerabilities.json')) as expected_json: - self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_4) - self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string()) - expected_json.close() + def test_bom_v1_3_component_with_release_notes(self) -> None: + self._validate_json_bom( + bom=get_bom_with_component_setuptools_with_release_notes(), schema_version=SchemaVersion.V1_3, + fixture='bom_setuptools.json' + ) + + def test_bom_v1_4_component_with_vulnerability(self) -> None: + self._validate_json_bom( + bom=get_bom_with_component_setuptools_with_vulnerability(), schema_version=SchemaVersion.V1_4, + fixture='bom_setuptools_with_vulnerabilities.json' + ) + + def test_bom_v1_3_component_with_vulnerability(self) -> None: + # Vulnerabilities not support in JSON < 1.4 + self._validate_json_bom( + bom=get_bom_with_component_setuptools_with_vulnerability(), schema_version=SchemaVersion.V1_3, + fixture='bom_setuptools.json' + ) + + @patch('cyclonedx.model.bom_ref.uuid4', return_value=MOCK_UUID_1) + def test_bom_v1_4_with_metadata_component(self, mock_uuid: Mock) -> None: + self._validate_json_bom( + bom=get_bom_just_complete_metadata(), schema_version=SchemaVersion.V1_4, + fixture='bom_with_full_metadata.json' + ) + mock_uuid.assert_called() - @patch('cyclonedx.model.component.uuid4', return_value='be2c6502-7e9a-47db-9a66-e34f729810a3') + @patch('cyclonedx.model.bom_ref.uuid4', return_value=MOCK_UUID_2) def test_bom_v1_3_with_metadata_component(self, mock_uuid: Mock) -> None: - bom = Bom() - bom.metadata.component = Component( - name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY + self._validate_json_bom( + bom=get_bom_just_complete_metadata(), schema_version=SchemaVersion.V1_3, + fixture='bom_with_full_metadata.json' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', return_value=MOCK_UUID_3) + def test_bom_v1_2_with_metadata_component(self, mock_uuid: Mock) -> None: + self._validate_json_bom( + bom=get_bom_just_complete_metadata(), schema_version=SchemaVersion.V1_2, + fixture='bom_with_full_metadata.json' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_4_services_simple(self, mock_uuid: Mock) -> None: + self._validate_json_bom( + bom=get_bom_with_services_simple(), schema_version=SchemaVersion.V1_4, + fixture='bom_services_simple.json' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_3_services_simple(self, mock_uuid: Mock) -> None: + self._validate_json_bom( + bom=get_bom_with_services_simple(), schema_version=SchemaVersion.V1_3, + fixture='bom_services_simple.json' ) mock_uuid.assert_called() - outputter = get_instance(bom=bom, output_format=OutputFormat.JSON) - self.assertIsInstance(outputter, JsonV1Dot3) - with open(join(dirname(__file__), 'fixtures/bom_v1.3_with_metadata_component.json')) as expected_json: - self.assertEqualJsonBom(outputter.output_as_string(), expected_json.read()) - maxDiff = None + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_2_services_simple(self, mock_uuid: Mock) -> None: + self._validate_json_bom( + bom=get_bom_with_services_simple(), schema_version=SchemaVersion.V1_2, + fixture='bom_services_simple.json' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_4_services_complex(self, mock_uuid: Mock) -> None: + self._validate_json_bom( + bom=get_bom_with_services_complex(), schema_version=SchemaVersion.V1_4, + fixture='bom_services_complex.json' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_3_services_complex(self, mock_uuid: Mock) -> None: + self._validate_json_bom( + bom=get_bom_with_services_complex(), schema_version=SchemaVersion.V1_3, + fixture='bom_services_complex.json' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_2_services_complex(self, mock_uuid: Mock) -> None: + self._validate_json_bom( + bom=get_bom_with_services_complex(), schema_version=SchemaVersion.V1_2, + fixture='bom_services_complex.json' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_4_services_nested(self, mock_uuid: Mock) -> None: + self._validate_json_bom( + bom=get_bom_with_nested_services(), schema_version=SchemaVersion.V1_4, + fixture='bom_services_nested.json' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_3_services_nested(self, mock_uuid: Mock) -> None: + self._validate_json_bom( + bom=get_bom_with_nested_services(), schema_version=SchemaVersion.V1_3, + fixture='bom_services_nested.json' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_2_services_nested(self, mock_uuid: Mock) -> None: + self._validate_json_bom( + bom=get_bom_with_nested_services(), schema_version=SchemaVersion.V1_2, + fixture='bom_services_nested.json' + ) + mock_uuid.assert_called() + + # Helper methods + def _validate_json_bom(self, bom: Bom, schema_version: SchemaVersion, fixture: str) -> None: + outputter = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=schema_version) + self.assertEqual(outputter.schema_version, schema_version) + with open( + join(dirname(__file__), f'fixtures/json/{schema_version.to_version()}/{fixture}')) as expected_json: + self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=schema_version) + self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string()) + expected_json.close() + + def _validate_json_bom_not_supported(self, bom: Bom, schema_version: SchemaVersion) -> None: + with self.assertRaises(FormatNotSupportedException): + outputter = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=schema_version) + outputter.output_as_string() diff --git a/tests/test_output_xml.py b/tests/test_output_xml.py index c659d425..a2bed4bd 100644 --- a/tests/test_output_xml.py +++ b/tests/test_output_xml.py @@ -16,521 +16,388 @@ # # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. -import base64 -from datetime import datetime, timezone -from decimal import Decimal from os.path import dirname, join -from packageurl import PackageURL from unittest.mock import Mock, patch -from cyclonedx.model import Encoding, ExternalReference, ExternalReferenceType, HashType, Note, NoteText, \ - OrganizationalContact, OrganizationalEntity, Property, Tool, XsUri from cyclonedx.model.bom import Bom -from cyclonedx.model.component import Component, ComponentType -from cyclonedx.model.impact_analysis import ImpactAnalysisState, ImpactAnalysisJustification, ImpactAnalysisResponse, \ - ImpactAnalysisAffectedStatus -from cyclonedx.model.issue import IssueClassification, IssueType -from cyclonedx.model.release_note import ReleaseNotes -from cyclonedx.model.vulnerability import Vulnerability, VulnerabilityCredits, VulnerabilityRating, \ - VulnerabilitySeverity, VulnerabilitySource, VulnerabilityScoreSource, VulnerabilityAdvisory, \ - VulnerabilityReference, VulnerabilityAnalysis, BomTarget, BomTargetVersionRange from cyclonedx.output import get_instance, SchemaVersion -from cyclonedx.output.xml import XmlV1Dot4, XmlV1Dot3, XmlV1Dot2, XmlV1Dot1, XmlV1Dot0, Xml +from data import get_bom_with_component_setuptools_basic, get_bom_with_component_setuptools_with_cpe, \ + get_bom_with_component_toml_1, get_bom_with_component_setuptools_no_component_version, \ + get_bom_with_component_setuptools_with_release_notes, get_bom_with_component_setuptools_with_vulnerability, \ + MOCK_UUID_1, MOCK_UUID_4, MOCK_UUID_5, MOCK_UUID_6, TEST_UUIDS, get_bom_just_complete_metadata, \ + get_bom_with_nested_services, get_bom_with_services_simple, get_bom_with_services_complex, \ + get_bom_with_component_setuptools_complete, get_bom_with_external_references from tests.base import BaseXmlTestCase class TestOutputXml(BaseXmlTestCase): + def test_bom_external_references_v1_4(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_external_references(), schema_version=SchemaVersion.V1_4, + fixture='bom_external_references.xml' + ) + + def test_bom_external_references_v1_3(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_external_references(), schema_version=SchemaVersion.V1_3, + fixture='bom_external_references.xml' + ) + + def test_bom_external_references_v1_2(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_external_references(), schema_version=SchemaVersion.V1_2, + fixture='bom_external_references.xml' + ) + + def test_bom_external_references_v1_1(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_external_references(), schema_version=SchemaVersion.V1_1, + fixture='bom_external_references.xml' + ) + + def test_bom_external_references_v1_0(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_external_references(), schema_version=SchemaVersion.V1_0, + fixture='bom_empty.xml' + ) + def test_simple_bom_v1_4(self) -> None: - bom = Bom() - bom.add_component(Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) - )) - outputter: Xml = get_instance(bom=bom, schema_version=SchemaVersion.V1_4) - self.assertIsInstance(outputter, XmlV1Dot4) - with open(join(dirname(__file__), 'fixtures/bom_v1.4_setuptools.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_4) - self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_basic(), schema_version=SchemaVersion.V1_4, + fixture='bom_setuptools.xml' + ) def test_simple_bom_v1_3(self) -> None: - bom = Bom() - bom.add_component(Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) - )) - outputter: Xml = get_instance(bom=bom) - self.assertIsInstance(outputter, XmlV1Dot3) - with open(join(dirname(__file__), 'fixtures/bom_v1.3_setuptools.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_basic(), schema_version=SchemaVersion.V1_3, + fixture='bom_setuptools.xml' + ) def test_simple_bom_v1_2(self) -> None: - bom = Bom() - bom.add_component(Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) - )) - outputter = get_instance(bom=bom, schema_version=SchemaVersion.V1_2) - self.assertIsInstance(outputter, XmlV1Dot2) - with open(join(dirname(__file__), 'fixtures/bom_v1.2_setuptools.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_2) - self.assertEqualXmlBom(outputter.output_as_string(), expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_basic(), schema_version=SchemaVersion.V1_2, + fixture='bom_setuptools.xml' + ) def test_simple_bom_v1_1(self) -> None: - bom = Bom() - bom.add_component(Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) - )) - outputter = get_instance(bom=bom, schema_version=SchemaVersion.V1_1) - self.assertIsInstance(outputter, XmlV1Dot1) - with open(join(dirname(__file__), 'fixtures/bom_v1.1_setuptools.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_1) - self.assertEqualXmlBom(outputter.output_as_string(), expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_basic(), schema_version=SchemaVersion.V1_1, + fixture='bom_setuptools.xml' + ) def test_simple_bom_v1_0(self) -> None: - bom = Bom() - bom.add_component(Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) - )) - self.assertEqual(len(bom.components), 1) - outputter = get_instance(bom=bom, schema_version=SchemaVersion.V1_0) - self.assertIsInstance(outputter, XmlV1Dot0) - with open(join(dirname(__file__), 'fixtures/bom_v1.0_setuptools.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_0) - self.assertEqualXmlBom(outputter.output_as_string(), expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_basic(), schema_version=SchemaVersion.V1_0, + fixture='bom_setuptools.xml' + ) def test_simple_bom_v1_4_with_cpe(self) -> None: - bom = Bom() - bom.add_component(Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) - )) - outputter: Xml = get_instance(bom=bom, schema_version=SchemaVersion.V1_4) - self.assertIsInstance(outputter, XmlV1Dot4) - with open(join(dirname(__file__), 'fixtures/bom_v1.4_setuptools_with_cpe.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_4) - self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_with_cpe(), schema_version=SchemaVersion.V1_4, + fixture='bom_setuptools_with_cpe.xml' + ) def test_simple_bom_v1_3_with_cpe(self) -> None: - bom = Bom() - bom.add_component(Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) - )) - outputter: Xml = get_instance(bom=bom) - self.assertIsInstance(outputter, XmlV1Dot3) - with open(join(dirname(__file__), 'fixtures/bom_v1.3_setuptools_with_cpe.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_with_cpe(), schema_version=SchemaVersion.V1_3, + fixture='bom_setuptools_with_cpe.xml' + ) def test_simple_bom_v1_2_with_cpe(self) -> None: - bom = Bom() - bom.add_component(Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) - )) - outputter = get_instance(bom=bom, schema_version=SchemaVersion.V1_2) - self.assertIsInstance(outputter, XmlV1Dot2) - with open(join(dirname(__file__), 'fixtures/bom_v1.2_setuptools_with_cpe.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_2) - self.assertEqualXmlBom(outputter.output_as_string(), expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_with_cpe(), schema_version=SchemaVersion.V1_2, + fixture='bom_setuptools_with_cpe.xml' + ) def test_simple_bom_v1_1_with_cpe(self) -> None: - bom = Bom() - bom.add_component(Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) - )) - outputter = get_instance(bom=bom, schema_version=SchemaVersion.V1_1) - self.assertIsInstance(outputter, XmlV1Dot1) - with open(join(dirname(__file__), 'fixtures/bom_v1.1_setuptools_with_cpe.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_1) - self.assertEqualXmlBom(outputter.output_as_string(), expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_with_cpe(), schema_version=SchemaVersion.V1_1, + fixture='bom_setuptools_with_cpe.xml' + ) def test_simple_bom_v1_0_with_cpe(self) -> None: - bom = Bom() - bom.add_component(Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) - )) - self.assertEqual(len(bom.components), 1) - outputter = get_instance(bom=bom, schema_version=SchemaVersion.V1_0) - self.assertIsInstance(outputter, XmlV1Dot0) - with open(join(dirname(__file__), 'fixtures/bom_v1.0_setuptools_with_cpe.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_0) - self.assertEqualXmlBom(outputter.output_as_string(), expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_with_cpe(), schema_version=SchemaVersion.V1_0, + fixture='bom_setuptools_with_cpe.xml' + ) - def test_simple_bom_v1_4_with_vulnerabilities(self) -> None: - bom = Bom() - nvd = VulnerabilitySource(name='NVD', url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2018-7489')) - owasp = VulnerabilitySource(name='OWASP', url=XsUri('https://owasp.org')) - c = Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) + def test_bom_v1_4_full_component(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_complete(), schema_version=SchemaVersion.V1_4, + fixture='bom_setuptools_complete.xml' ) - c.add_vulnerability(Vulnerability( - bom_ref='my-vuln-ref-1', id='CVE-2018-7489', source=nvd, - references=[ - VulnerabilityReference(id='SOME-OTHER-ID', source=VulnerabilitySource( - name='OSS Index', url=XsUri('https://ossindex.sonatype.org/component/pkg:pypi/setuptools') - )) - ], - ratings=[ - VulnerabilityRating( - source=nvd, score=Decimal(9.8), severity=VulnerabilitySeverity.CRITICAL, - method=VulnerabilityScoreSource.CVSS_V3, - vector='AN/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H', justification='Some justification' - ), - VulnerabilityRating( - source=owasp, score=Decimal(2.7), severity=VulnerabilitySeverity.LOW, - method=VulnerabilityScoreSource.CVSS_V3, - vector='AV:L/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N', justification='Some other justification' - ) - ], - cwes=[22, 33], description='A description here', detail='Some detail here', - recommendation='Upgrade', - advisories=[ - VulnerabilityAdvisory(url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2018-7489')), - VulnerabilityAdvisory(url=XsUri('http://www.securitytracker.com/id/1040693')) - ], - created=datetime(year=2021, month=9, day=1, hour=10, minute=50, second=42, microsecond=51979, - tzinfo=timezone.utc), - published=datetime(year=2021, month=9, day=2, hour=10, minute=50, second=42, microsecond=51979, - tzinfo=timezone.utc), - updated=datetime(year=2021, month=9, day=3, hour=10, minute=50, second=42, microsecond=51979, - tzinfo=timezone.utc), - credits=VulnerabilityCredits( - organizations=[ - OrganizationalEntity( - name='CycloneDX', urls=[XsUri('https://cyclonedx.org')], contacts=[ - OrganizationalContact(name='Paul Horton', email='simplyecommerce@googlemail.com'), - OrganizationalContact(name='A N Other', email='someone@somewhere.tld', - phone='+44 (0)1234 567890') - ] - ) - ], - individuals=[ - OrganizationalContact(name='A N Other', email='someone@somewhere.tld', phone='+44 (0)1234 567890'), - ] - ), - tools=[ - Tool(vendor='CycloneDX', name='cyclonedx-python-lib') - ], - analysis=VulnerabilityAnalysis( - state=ImpactAnalysisState.EXPLOITABLE, justification=ImpactAnalysisJustification.REQUIRES_ENVIRONMENT, - responses=[ImpactAnalysisResponse.CAN_NOT_FIX], detail='Some extra detail' - ), - affects_targets=[ - BomTarget(ref=c.bom_ref, versions=[ - BomTargetVersionRange(version_range='49.0.0 - 54.0.0', status=ImpactAnalysisAffectedStatus.AFFECTED) - ]) - ] - )) - bom.add_component(c) - outputter: Xml = get_instance(bom=bom, schema_version=SchemaVersion.V1_4) - self.assertIsInstance(outputter, XmlV1Dot4) - with open(join(dirname(__file__), 'fixtures/bom_v1.4_setuptools_with_vulnerabilities.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_4) - self.assertEqualXmlBom(a=outputter.output_as_string(), - b=expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + def test_bom_v1_3_full_component(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_complete(), schema_version=SchemaVersion.V1_3, + fixture='bom_setuptools_complete.xml' + ) - def test_simple_bom_v1_3_with_vulnerabilities(self) -> None: - bom = Bom() - c = Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) + def test_bom_v1_2_full_component(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_complete(), schema_version=SchemaVersion.V1_2, + fixture='bom_setuptools_complete.xml' ) - c.add_vulnerability(Vulnerability( - id='CVE-2018-7489', source_name='NVD', source_url='https://nvd.nist.gov/vuln/detail/CVE-2018-7489', - ratings=[ - VulnerabilityRating(score_base=9.8, - severity=VulnerabilitySeverity.CRITICAL, method=VulnerabilityScoreSource.CVSS_V3, - vector='AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H'), - VulnerabilityRating(severity=VulnerabilitySeverity.LOW, method=VulnerabilityScoreSource.OWASP, - vector='OWASP/K9:M1:O0:Z2/D1:X1:W1:L3/C2:I1:A1:T1/F1:R1:S2:P3/50', ) - ], - cwes=[123, 456], description='A description here', recommendation='Upgrade', - advisories=[ - VulnerabilityAdvisory(url=XsUri('http://www.securityfocus.com/bid/103203')), - VulnerabilityAdvisory(url=XsUri('http://www.securitytracker.com/id/1040693')) - ] - )) - bom.add_component(c) - outputter: Xml = get_instance(bom=bom) - self.assertIsInstance(outputter, XmlV1Dot3) - with open(join(dirname(__file__), 'fixtures/bom_v1.3_setuptools_with_vulnerabilities.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + def test_bom_v1_1_full_component(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_complete(), schema_version=SchemaVersion.V1_1, + fixture='bom_setuptools_complete.xml' + ) - def test_simple_bom_v1_0_with_vulnerabilities(self) -> None: - bom = Bom() - c = Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ) + def test_bom_v1_0_full_component(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_complete(), schema_version=SchemaVersion.V1_0, + fixture='bom_setuptools_complete.xml' ) - c.add_vulnerability(Vulnerability( - id='CVE-2018-7489', source_name='NVD', source_url='https://nvd.nist.gov/vuln/detail/CVE-2018-7489', - ratings=[ - VulnerabilityRating(score_base=9.8, - severity=VulnerabilitySeverity.CRITICAL, method=VulnerabilityScoreSource.CVSS_V3, - vector='AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H'), - VulnerabilityRating(severity=VulnerabilitySeverity.LOW, method=VulnerabilityScoreSource.OWASP, - vector='OWASP/K9:M1:O0:Z2/D1:X1:W1:L3/C2:I1:A1:T1/F1:R1:S2:P3/50', ) - ], - cwes=[123, 456], description='A description here', recommendations=['Upgrade'], - advisories=[ - 'http://www.securityfocus.com/bid/103203', - 'http://www.securitytracker.com/id/1040693' - ] - )) - bom.add_component(c) - outputter: Xml = get_instance(bom=bom, schema_version=SchemaVersion.V1_0) - self.assertIsInstance(outputter, XmlV1Dot0) - with open(join(dirname(__file__), 'fixtures/bom_v1.0_setuptools.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_0) - self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + def test_bom_v1_4_component_hashes_external_references(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_toml_1(), schema_version=SchemaVersion.V1_4, + fixture='bom_toml_hashes_and_references.xml' + ) - def test_bom_v1_3_with_component_hashes(self) -> None: - bom = Bom() - c = Component( - name='toml', version='0.10.2', bom_ref='pkg:pypi/toml@0.10.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='toml', version='0.10.2', qualifiers='extension=tar.gz' - ) + def test_bom_v1_3_component_hashes_external_references(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_toml_1(), schema_version=SchemaVersion.V1_3, + fixture='bom_toml_hashes_and_references.xml' ) - c.add_hash( - HashType.from_composite_str('sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b') + + def test_bom_v1_2_component_hashes_external_references(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_toml_1(), schema_version=SchemaVersion.V1_2, + fixture='bom_toml_hashes_and_references.xml' ) - bom.add_component(c) - outputter: Xml = get_instance(bom=bom) - self.assertIsInstance(outputter, XmlV1Dot3) - with open(join(dirname(__file__), 'fixtures/bom_v1.3_toml_with_component_hashes.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() - def test_bom_v1_3_with_component_external_references(self) -> None: - bom = Bom() - c = Component( - name='toml', version='0.10.2', bom_ref='pkg:pypi/toml@0.10.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='toml', version='0.10.2', qualifiers='extension=tar.gz' - ) + def test_bom_v1_1_component_hashes_external_references(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_toml_1(), schema_version=SchemaVersion.V1_1, + fixture='bom_toml_hashes_and_references.xml' ) - c.add_hash( - HashType.from_composite_str('sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b') - ) - c.add_external_reference( - ExternalReference( - reference_type=ExternalReferenceType.DISTRIBUTION, - url='https://cyclonedx.org', - comment='No comment', - hashes=[ - HashType.from_composite_str( - 'sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b') - ] - ) + + def test_bom_v1_0_component_hashes_external_references(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_toml_1(), schema_version=SchemaVersion.V1_0, + fixture='bom_toml_hashes_and_references.xml' ) - bom.add_component(c) - outputter: Xml = get_instance(bom=bom) - self.assertIsInstance(outputter, XmlV1Dot3) - with open(join(dirname(__file__), - 'fixtures/bom_v1.3_toml_with_component_external_references.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() - def test_with_component_license(self) -> None: - bom = Bom() - c = Component( - name='toml', version='0.10.2', bom_ref='pkg:pypi/toml@0.10.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='toml', version='0.10.2', qualifiers='extension=tar.gz' - ), license_str='MIT License' - ) - bom.add_component(c) - outputter: Xml = get_instance(bom=bom) - self.assertIsInstance(outputter, XmlV1Dot3) - with open(join(dirname(__file__), - 'fixtures/bom_v1.3_toml_with_component_license.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + def test_bom_v1_4_no_component_version(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_no_component_version(), schema_version=SchemaVersion.V1_4, + fixture='bom_setuptools_no_version.xml' + ) - def test_with_no_component_version_1_4(self) -> None: - bom = Bom() - bom.add_component(Component( - name='setuptools', bom_ref='pkg:pypi/setuptools?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', qualifiers='extension=tar.gz' - ) - )) - outputter: Xml = get_instance(bom=bom, schema_version=SchemaVersion.V1_4) - self.assertIsInstance(outputter, XmlV1Dot4) - with open(join(dirname(__file__), - 'fixtures/bom_v1.4_setuptools_no_version.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_4) - self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + def test_bom_v1_3_no_component_version(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_no_component_version(), schema_version=SchemaVersion.V1_3, + fixture='bom_setuptools_no_version.xml' + ) - def test_with_component_release_notes_pre_1_4(self) -> None: - bom = Bom() - c = Component( - name='toml', version='0.10.2', bom_ref='pkg:pypi/toml@0.10.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='toml', version='0.10.2', qualifiers='extension=tar.gz' - ), release_notes=ReleaseNotes(type='major'), license_str='MIT License' - ) - bom.add_component(c) - outputter: Xml = get_instance(bom=bom, schema_version=SchemaVersion.V1_3) - self.assertIsInstance(outputter, XmlV1Dot3) - with open(join(dirname(__file__), - 'fixtures/bom_v1.3_toml_with_component_license.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_3) - self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() + def test_bom_v1_2_no_component_version(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_no_component_version(), schema_version=SchemaVersion.V1_2, + fixture='bom_setuptools_no_version.xml' + ) - def test_with_component_release_notes_post_1_4(self) -> None: - bom = Bom() - timestamp: datetime = datetime(2021, 12, 31, 10, 0, 0, 0).replace(tzinfo=timezone.utc) - - text_content: str = base64.b64encode( - bytes('Some simple plain text', encoding='UTF-8') - ).decode(encoding='UTF-8') - - c = Component( - name='setuptools', version='50.3.2', bom_ref='pkg:pypi/setuptools@50.3.2?extension=tar.gz', - purl=PackageURL( - type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz' - ), - release_notes=ReleaseNotes( - type='major', title="Release Notes Title", - featured_image=XsUri('https://cyclonedx.org/theme/assets/images/CycloneDX-Twitter-Card.png'), - social_image=XsUri('https://cyclonedx.org/cyclonedx-icon.png'), - description="This release is a test release", timestamp=timestamp, - aliases=[ - "First Test Release" - ], - tags=['test', 'alpha'], - resolves=[ - IssueType( - classification=IssueClassification.SECURITY, id='CVE-2021-44228', name='Apache Log3Shell', - description='Apache Log4j2 2.0-beta9 through 2.12.1 and 2.13.0 through 2.15.0 JNDI features...', - source_name='NVD', source_url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2021-44228'), - references=[ - XsUri('https://logging.apache.org/log4j/2.x/security.html'), - XsUri('https://central.sonatype.org/news/20211213_log4shell_help') - ] - ) - ], - notes=[ - Note( - text=NoteText( - content=text_content, content_type='text/plain; charset=UTF-8', - content_encoding=Encoding.BASE_64 - ), locale='en-GB' - ), - Note( - text=NoteText( - content=text_content, content_type='text/plain; charset=UTF-8', - content_encoding=Encoding.BASE_64 - ), locale='en-US' - ) - ], - properties=[ - Property(name='key1', value='val1'), - Property(name='key2', value='val2') - ] - ) + def test_bom_v1_1_no_component_version(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_no_component_version(), schema_version=SchemaVersion.V1_1, + fixture='bom_setuptools_no_version.xml' ) - bom.add_component(c) - outputter: Xml = get_instance(bom=bom, schema_version=SchemaVersion.V1_4) - self.assertIsInstance(outputter, XmlV1Dot4) - with open(join(dirname(__file__), - 'fixtures/bom_v1.4_setuptools_with_release_notes.xml')) as expected_xml: - self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=SchemaVersion.V1_4) - self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(), - namespace=outputter.get_target_namespace()) - expected_xml.close() - @patch('cyclonedx.model.component.uuid4', return_value='5d82790b-3139-431d-855a-ab63d14a18bb') + def test_bom_v1_0_no_component_version(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_no_component_version(), schema_version=SchemaVersion.V1_0, + fixture='bom_setuptools_no_version.xml' + ) + + def test_bom_v1_4_component_with_release_notes(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_with_release_notes(), schema_version=SchemaVersion.V1_4, + fixture='bom_setuptools_with_release_notes.xml' + ) + + def test_bom_v1_3_component_with_release_notes(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_with_release_notes(), schema_version=SchemaVersion.V1_3, + fixture='bom_setuptools.xml' + ) + + def test_bom_v1_4_component_with_vulnerability(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_with_vulnerability(), schema_version=SchemaVersion.V1_4, + fixture='bom_setuptools_with_vulnerabilities.xml' + ) + + def test_bom_v1_3_component_with_vulnerability(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_with_vulnerability(), schema_version=SchemaVersion.V1_3, + fixture='bom_setuptools_with_vulnerabilities.xml' + ) + + def test_bom_v1_2_component_with_vulnerability(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_with_vulnerability(), schema_version=SchemaVersion.V1_2, + fixture='bom_setuptools_with_vulnerabilities.xml' + ) + + def test_bom_v1_1_component_with_vulnerability(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_with_vulnerability(), schema_version=SchemaVersion.V1_1, + fixture='bom_setuptools_with_vulnerabilities.xml' + ) + + def test_bom_v1_0_component_with_vulnerability(self) -> None: + self._validate_xml_bom( + bom=get_bom_with_component_setuptools_with_vulnerability(), schema_version=SchemaVersion.V1_0, + fixture='bom_setuptools.xml' + ) + + @patch('cyclonedx.model.bom_ref.uuid4', return_value=MOCK_UUID_6) + def test_bom_v1_4_with_metadata_component(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_just_complete_metadata(), schema_version=SchemaVersion.V1_4, + fixture='bom_with_full_metadata.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', return_value=MOCK_UUID_5) def test_bom_v1_3_with_metadata_component(self, mock_uuid: Mock) -> None: - bom = Bom() - bom.metadata.component = Component( - name='cyclonedx-python-lib', version='1.0.0', component_type=ComponentType.LIBRARY + self._validate_xml_bom( + bom=get_bom_just_complete_metadata(), schema_version=SchemaVersion.V1_3, + fixture='bom_with_full_metadata.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', return_value=MOCK_UUID_4) + def test_bom_v1_2_with_metadata_component(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_just_complete_metadata(), schema_version=SchemaVersion.V1_2, + fixture='bom_with_full_metadata.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', return_value=MOCK_UUID_1) + def test_bom_v1_1_with_metadata_component(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_just_complete_metadata(), schema_version=SchemaVersion.V1_1, + fixture='bom_empty.xml' ) mock_uuid.assert_called() - outputter: Xml = get_instance(bom=bom) - self.assertIsInstance(outputter, XmlV1Dot3) - with open(join(dirname(__file__), 'fixtures/bom_v1.3_with_metadata_component.xml')) as expected_xml: - self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(), - namespace=outputter.get_target_namespace()) + + @patch('cyclonedx.model.bom_ref.uuid4', return_value=MOCK_UUID_1) + def test_bom_v1_0_with_metadata_component(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_just_complete_metadata(), schema_version=SchemaVersion.V1_0, + fixture='bom_empty.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_4_services_simple(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_with_services_simple(), schema_version=SchemaVersion.V1_4, + fixture='bom_services_simple.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_3_services_simple(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_with_services_simple(), schema_version=SchemaVersion.V1_3, + fixture='bom_services_simple.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_2_services_simple(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_with_services_simple(), schema_version=SchemaVersion.V1_2, + fixture='bom_services_simple.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_1_services_simple(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_with_services_simple(), schema_version=SchemaVersion.V1_1, + fixture='bom_empty.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_0_services_simple(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_with_services_simple(), schema_version=SchemaVersion.V1_0, + fixture='bom_empty.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_4_services_complex(self, mock_uuid4: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_with_services_complex(), schema_version=SchemaVersion.V1_4, + fixture='bom_services_complex.xml' + ) + mock_uuid4.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_3_services_complex(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_with_services_complex(), schema_version=SchemaVersion.V1_3, + fixture='bom_services_complex.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_2_services_complex(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_with_services_complex(), schema_version=SchemaVersion.V1_2, + fixture='bom_services_complex.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_1_services_complex(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_with_services_complex(), schema_version=SchemaVersion.V1_1, + fixture='bom_empty.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_4_services_nested(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_with_nested_services(), schema_version=SchemaVersion.V1_4, + fixture='bom_services_nested.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_3_services_nested(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_with_nested_services(), schema_version=SchemaVersion.V1_3, + fixture='bom_services_nested.xml' + ) + mock_uuid.assert_called() + + @patch('cyclonedx.model.bom_ref.uuid4', side_effect=TEST_UUIDS) + def test_bom_v1_2_services_nested(self, mock_uuid: Mock) -> None: + self._validate_xml_bom( + bom=get_bom_with_nested_services(), schema_version=SchemaVersion.V1_2, + fixture='bom_services_nested.xml' + ) + mock_uuid.assert_called() + + # Helper methods + def _validate_xml_bom(self, bom: Bom, schema_version: SchemaVersion, fixture: str) -> None: + outputter = get_instance(bom=bom, schema_version=schema_version) + self.assertEqual(outputter.schema_version, schema_version) + with open( + join(dirname(__file__), f'fixtures/xml/{schema_version.to_version()}/{fixture}')) as expected_xml: + self.assertValidAgainstSchema(bom_xml=outputter.output_as_string(), schema_version=schema_version) + self.assertEqualXmlBom( + expected_xml.read(), outputter.output_as_string(), namespace=outputter.get_target_namespace() + ) expected_xml.close() diff --git a/tox.ini b/tox.ini index c902de7d..d14cb059 100644 --- a/tox.ini +++ b/tox.ini @@ -27,6 +27,8 @@ commands_pre = poetry run pip freeze commands = poetry run coverage run --source=cyclonedx -m unittest discover -s tests -v +setenv = + PYTHONHASHSEED = 0 [testenv:mypy{,-locked,-lowest}] commands =