Files
Hotel-Booking/Backend/venv/lib/python3.12/site-packages/cyclonedx/model/component.py
Iliyan Angelov 62c1fe5951 updates
2025-12-01 06:50:10 +02:00

1786 lines
57 KiB
Python

# This file is part of CycloneDX Python Library
#
# 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 re
from enum import Enum
from os.path import exists
from typing import Any, Dict, FrozenSet, Iterable, Optional, Set, Type, Union
from warnings import warn
# See https://github.com/package-url/packageurl-python/issues/65
import py_serializable as serializable
from packageurl import PackageURL
from sortedcontainers import SortedSet
from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str
from .._internal.compare import ComparablePackageURL as _ComparablePackageURL, ComparableTuple as _ComparableTuple
from .._internal.hash import file_sha1sum as _file_sha1sum
from ..exception.model import InvalidOmniBorIdException, InvalidSwhidException
from ..exception.serialization import (
CycloneDxDeserializationException,
SerializationOfUnexpectedValueException,
SerializationOfUnsupportedComponentTypeException,
)
from ..schema.schema import (
SchemaVersion1Dot0,
SchemaVersion1Dot1,
SchemaVersion1Dot2,
SchemaVersion1Dot3,
SchemaVersion1Dot4,
SchemaVersion1Dot5,
SchemaVersion1Dot6,
)
from ..serialization import PackageUrl as PackageUrlSH
from . import (
AttachedText,
Copyright,
ExternalReference,
HashAlgorithm,
HashType,
IdentifiableAction,
Property,
XsUri,
_HashTypeRepositorySerializationHelper,
)
from .bom_ref import BomRef
from .contact import OrganizationalContact, OrganizationalEntity
from .crypto import CryptoProperties
from .dependency import Dependable
from .issue import IssueType
from .license import License, LicenseRepository, _LicenseRepositorySerializationHelper
from .release_note import ReleaseNotes
@serializable.serializable_class
class Commit:
"""
Our internal representation of the `commitType` complex type.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/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:
self.uid = uid
self.url = url
self.author = author
self.committer = committer
self.message = message
@property
@serializable.xml_sequence(1)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
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
@serializable.xml_sequence(2)
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
@serializable.xml_sequence(3)
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
@serializable.xml_sequence(4)
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
@serializable.xml_sequence(5)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
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 __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.uid, self.url,
self.author, self.committer,
self.message
))
def __eq__(self, other: object) -> bool:
if isinstance(other, Commit):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, Commit):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Commit uid={self.uid}, url={self.url}, message={self.message}>'
@serializable.serializable_class
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.6/xml/#type_componentEvidenceType
"""
def __init__(
self, *,
licenses: Optional[Iterable[License]] = None,
copyright: Optional[Iterable[Copyright]] = None,
) -> None:
self.licenses = licenses or [] # type:ignore[assignment]
self.copyright = copyright or [] # type:ignore[assignment]
# @property
# ...
# @serializable.view(SchemaVersion1Dot5)
# @serializable.xml_sequence(1)
# def identity(self) -> ...:
# ... # TODO since CDX1.5
#
# @identity.setter
# def identity(self, ...) -> None:
# ... # TODO since CDX1.5
# @property
# ...
# @serializable.view(SchemaVersion1Dot5)
# @serializable.xml_sequence(2)
# def occurrences(self) -> ...:
# ... # TODO since CDX1.5
#
# @occurrences.setter
# def occurrences(self, ...) -> None:
# ... # TODO since CDX1.5
# @property
# ...
# @serializable.view(SchemaVersion1Dot5)
# @serializable.xml_sequence(3)
# def callstack(self) -> ...:
# ... # TODO since CDX1.5
#
# @callstack.setter
# def callstack(self, ...) -> None:
# ... # TODO since CDX1.5
@property
@serializable.type_mapping(_LicenseRepositorySerializationHelper)
@serializable.xml_sequence(4)
def licenses(self) -> LicenseRepository:
"""
Optional list of licenses obtained during analysis.
Returns:
Set of `LicenseChoice`
"""
return self._licenses
@licenses.setter
def licenses(self, licenses: Iterable[License]) -> None:
self._licenses = LicenseRepository(licenses)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'text')
@serializable.xml_sequence(5)
def copyright(self) -> 'SortedSet[Copyright]':
"""
Optional list of copyright statements.
Returns:
Set of `Copyright`
"""
return self._copyright
@copyright.setter
def copyright(self, copyright: Iterable[Copyright]) -> None:
self._copyright = SortedSet(copyright)
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
_ComparableTuple(self.licenses),
_ComparableTuple(self.copyright),
))
def __eq__(self, other: object) -> bool:
if isinstance(other, ComponentEvidence):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<ComponentEvidence id={id(self)}>'
@serializable.serializable_enum
class ComponentScope(str, Enum):
"""
Enum object that defines the permissable 'scopes' for a Component according to the CycloneDX schema.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_scope
"""
# see `_ComponentScopeSerializationHelper.__CASES` for view/case map
REQUIRED = 'required'
OPTIONAL = 'optional'
EXCLUDED = 'excluded' # Only supported in >= 1.1
class _ComponentScopeSerializationHelper(serializable.helpers.BaseHelper):
""" THIS CLASS IS NON-PUBLIC API """
__CASES: Dict[Type[serializable.ViewType], FrozenSet[ComponentScope]] = dict()
__CASES[SchemaVersion1Dot0] = frozenset({
ComponentScope.REQUIRED,
ComponentScope.OPTIONAL,
})
__CASES[SchemaVersion1Dot1] = __CASES[SchemaVersion1Dot0] | {
ComponentScope.EXCLUDED,
}
__CASES[SchemaVersion1Dot2] = __CASES[SchemaVersion1Dot1]
__CASES[SchemaVersion1Dot3] = __CASES[SchemaVersion1Dot2]
__CASES[SchemaVersion1Dot4] = __CASES[SchemaVersion1Dot3]
__CASES[SchemaVersion1Dot5] = __CASES[SchemaVersion1Dot4]
__CASES[SchemaVersion1Dot6] = __CASES[SchemaVersion1Dot5]
@classmethod
def __normalize(cls, cs: ComponentScope, view: Type[serializable.ViewType]) -> Optional[str]:
return cs.value \
if cs in cls.__CASES.get(view, ()) \
else None
@classmethod
def json_normalize(cls, o: Any, *,
view: Optional[Type[serializable.ViewType]],
**__: Any) -> Optional[str]:
assert view is not None
return cls.__normalize(o, view)
@classmethod
def xml_normalize(cls, o: Any, *,
view: Optional[Type[serializable.ViewType]],
**__: Any) -> Optional[str]:
assert view is not None
return cls.__normalize(o, view)
@classmethod
def deserialize(cls, o: Any) -> ComponentScope:
return ComponentScope(o)
@serializable.serializable_enum
class ComponentType(str, Enum):
"""
Enum object that defines the permissible 'types' for a Component according to the CycloneDX schema.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_classification
"""
# see `_ComponentTypeSerializationHelper.__CASES` for view/case map
APPLICATION = 'application'
CONTAINER = 'container' # Only supported in >= 1.2
CRYPTOGRAPHIC_ASSET = 'cryptographic-asset' # Only supported in >= 1.6
DATA = 'data' # Only supported in >= 1.5
DEVICE = 'device'
DEVICE_DRIVER = 'device-driver' # Only supported in >= 1.5
FILE = 'file' # Only supported in >= 1.1
FIRMWARE = 'firmware' # Only supported in >= 1.2
FRAMEWORK = 'framework'
LIBRARY = 'library'
MACHINE_LEARNING_MODEL = 'machine-learning-model' # Only supported in >= 1.5
OPERATING_SYSTEM = 'operating-system'
PLATFORM = 'platform' # Only supported in >= 1.5
class _ComponentTypeSerializationHelper(serializable.helpers.BaseHelper):
""" THIS CLASS IS NON-PUBLIC API """
__CASES: Dict[Type[serializable.ViewType], FrozenSet[ComponentType]] = dict()
__CASES[SchemaVersion1Dot0] = frozenset({
ComponentType.APPLICATION,
ComponentType.DEVICE,
ComponentType.FRAMEWORK,
ComponentType.LIBRARY,
ComponentType.OPERATING_SYSTEM,
})
__CASES[SchemaVersion1Dot1] = __CASES[SchemaVersion1Dot0] | {
ComponentType.FILE,
}
__CASES[SchemaVersion1Dot2] = __CASES[SchemaVersion1Dot1] | {
ComponentType.CONTAINER,
ComponentType.FIRMWARE,
}
__CASES[SchemaVersion1Dot3] = __CASES[SchemaVersion1Dot2]
__CASES[SchemaVersion1Dot4] = __CASES[SchemaVersion1Dot3]
__CASES[SchemaVersion1Dot5] = __CASES[SchemaVersion1Dot4] | {
ComponentType.DATA,
ComponentType.DEVICE_DRIVER,
ComponentType.MACHINE_LEARNING_MODEL,
ComponentType.PLATFORM,
}
__CASES[SchemaVersion1Dot6] = __CASES[SchemaVersion1Dot5] | {
ComponentType.CRYPTOGRAPHIC_ASSET,
}
@classmethod
def __normalize(cls, ct: ComponentType, view: Type[serializable.ViewType]) -> Optional[str]:
if ct in cls.__CASES.get(view, ()):
return ct.value
raise SerializationOfUnsupportedComponentTypeException(f'unsupported {ct!r} for view {view!r}')
@classmethod
def json_normalize(cls, o: Any, *,
view: Optional[Type[serializable.ViewType]],
**__: Any) -> Optional[str]:
assert view is not None
return cls.__normalize(o, view)
@classmethod
def xml_normalize(cls, o: Any, *,
view: Optional[Type[serializable.ViewType]],
**__: Any) -> Optional[str]:
assert view is not None
return cls.__normalize(o, view)
@classmethod
def deserialize(cls, o: Any) -> ComponentType:
return ComponentType(o)
@serializable.serializable_class
class Diff:
"""
Our internal representation of the `diffType` complex type.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_diffType
"""
def __init__(
self, *,
text: Optional[AttachedText] = None,
url: Optional[XsUri] = None,
) -> None:
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 __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.url,
self.text,
))
def __eq__(self, other: object) -> bool:
if isinstance(other, Diff):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, Diff):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Diff url={self.url}>'
@serializable.serializable_enum
class PatchClassification(str, Enum):
"""
Enum object that defines the permissible `patchClassification`s.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_patchClassification
"""
BACKPORT = 'backport'
CHERRY_PICK = 'cherry-pick'
MONKEY = 'monkey'
UNOFFICIAL = 'unofficial'
@serializable.serializable_class
class Patch:
"""
Our internal representation of the `patchType` complex type.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/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 = resolves or [] # type:ignore[assignment]
@property
@serializable.xml_attribute()
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
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'issue')
def resolves(self) -> 'SortedSet[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 = SortedSet(resolves)
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.type, self.diff,
_ComparableTuple(self.resolves)
))
def __eq__(self, other: object) -> bool:
if isinstance(other, Patch):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, Patch):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Patch type={self.type}, id={id(self)}>'
@serializable.serializable_class
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.6/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:
self.ancestors = ancestors or [] # type:ignore[assignment]
self.descendants = descendants or [] # type:ignore[assignment]
self.variants = variants or [] # type:ignore[assignment]
self.commits = commits or [] # type:ignore[assignment]
self.patches = patches or [] # type:ignore[assignment]
self.notes = notes
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'component')
@serializable.xml_sequence(1)
def ancestors(self) -> "SortedSet['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 = SortedSet(ancestors)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'component')
@serializable.xml_sequence(2)
def descendants(self) -> "SortedSet['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 = SortedSet(descendants)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'component')
@serializable.xml_sequence(3)
def variants(self) -> "SortedSet['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 = SortedSet(variants)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'commit')
@serializable.xml_sequence(4)
def commits(self) -> 'SortedSet[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 = SortedSet(commits)
@property
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'patch')
@serializable.xml_sequence(5)
def patches(self) -> 'SortedSet[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 = SortedSet(patches)
@property
@serializable.xml_sequence(6)
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 __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
_ComparableTuple(self.ancestors),
_ComparableTuple(self.descendants),
_ComparableTuple(self.variants),
_ComparableTuple(self.commits),
_ComparableTuple(self.patches),
self.notes
))
def __eq__(self, other: object) -> bool:
if isinstance(other, Pedigree):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Pedigree id={id(self)}, hash={hash(self)}>'
@serializable.serializable_class
class Swid:
"""
Our internal representation of the `swidType` complex type.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/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
@serializable.xml_attribute()
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
@serializable.xml_attribute()
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
@serializable.xml_attribute()
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
@serializable.xml_attribute()
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
@serializable.xml_attribute()
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 __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.tag_id,
self.name, self.version,
self.tag_version,
self.patch,
self.url,
self.text,
))
def __eq__(self, other: object) -> bool:
if isinstance(other, Swid):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Swid tagId={self.tag_id}, name={self.name}, version={self.version}>'
@serializable.serializable_class
class OmniborId(serializable.helpers.BaseHelper):
"""
Helper class that allows us to perform validation on data strings that must conform to
https://www.iana.org/assignments/uri-schemes/prov/gitoid.
"""
_VALID_OMNIBOR_ID_REGEX = re.compile(r'^gitoid:(blob|tree|commit|tag):sha(1|256):([a-z0-9]+)$')
def __init__(self, id: str) -> None:
if OmniborId._VALID_OMNIBOR_ID_REGEX.match(id) is None:
raise InvalidOmniBorIdException(
f'Supplied value "{id} does not meet format specification.'
)
self._id = id
@property
@serializable.json_name('.')
@serializable.xml_name('.')
def id(self) -> str:
return self._id
@classmethod
def serialize(cls, o: Any) -> str:
if isinstance(o, OmniborId):
return str(o)
raise SerializationOfUnexpectedValueException(
f'Attempt to serialize a non-OmniBorId: {o!r}')
@classmethod
def deserialize(cls, o: Any) -> 'OmniborId':
try:
return OmniborId(id=str(o))
except ValueError as err:
raise CycloneDxDeserializationException(
f'OmniBorId string supplied does not parse: {o!r}'
) from err
def __eq__(self, other: Any) -> bool:
if isinstance(other, OmniborId):
return self._id == other._id
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, OmniborId):
return self._id < other._id
return NotImplemented
def __hash__(self) -> int:
return hash(self._id)
def __repr__(self) -> str:
return f'<OmniBorId {self._id}>'
def __str__(self) -> str:
return self._id
@serializable.serializable_class
class Swhid(serializable.helpers.BaseHelper):
"""
Helper class that allows us to perform validation on data strings that must conform to
https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html.
"""
_VALID_SWHID_REGEX = re.compile(r'^swh:1:(cnp|rel|rev|dir|cnt):([0-9a-z]{40})(.*)?$')
def __init__(self, id: str) -> None:
if Swhid._VALID_SWHID_REGEX.match(id) is None:
raise InvalidSwhidException(
f'Supplied value "{id} does not meet format specification.'
)
self._id = id
@property
@serializable.json_name('.')
@serializable.xml_name('.')
def id(self) -> str:
return self._id
@classmethod
def serialize(cls, o: Any) -> str:
if isinstance(o, Swhid):
return str(o)
raise SerializationOfUnexpectedValueException(
f'Attempt to serialize a non-Swhid: {o!r}')
@classmethod
def deserialize(cls, o: Any) -> 'Swhid':
try:
return Swhid(id=str(o))
except ValueError as err:
raise CycloneDxDeserializationException(
f'Swhid string supplied does not parse: {o!r}'
) from err
def __eq__(self, other: Any) -> bool:
if isinstance(other, Swhid):
return self._id == other._id
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, Swhid):
return self._id < other._id
return NotImplemented
def __hash__(self) -> int:
return hash(self._id)
def __repr__(self) -> str:
return f'<Swhid {self._id}>'
def __str__(self) -> str:
return self._id
@serializable.serializable_class
class Component(Dependable):
"""
This is our internal representation of a Component within a Bom.
.. note::
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_component
"""
@staticmethod
def for_file(absolute_file_path: str, path_for_bom: Optional[str]) -> 'Component':
"""
Helper method to create a Component that represents the provided local file as a Component.
Args:
absolute_file_path:
Absolute path to the file you wish to represent
path_for_bom:
Optionally, if supplied this is the path that will be used to identify the file in the BOM
Returns:
`Component` representing the supplied file
"""
if not exists(absolute_file_path):
raise FileExistsError(f'Supplied file path {absolute_file_path!r} does not exist')
sha1_hash: str = _file_sha1sum(absolute_file_path)
return Component(
name=path_for_bom if path_for_bom else absolute_file_path,
version=f'0.0.0-{sha1_hash[0:12]}',
hashes=[
HashType(alg=HashAlgorithm.SHA_1, content=sha1_hash)
],
type=ComponentType.FILE, purl=PackageURL(
type='generic', name=path_for_bom if path_for_bom else absolute_file_path,
version=f'0.0.0-{sha1_hash[0:12]}'
)
)
def __init__(
self, *,
name: str,
type: ComponentType = ComponentType.LIBRARY,
mime_type: Optional[str] = None,
bom_ref: Optional[Union[str, BomRef]] = None,
supplier: Optional[OrganizationalEntity] = None,
publisher: Optional[str] = None,
group: Optional[str] = None,
version: Optional[str] = None,
description: Optional[str] = None,
scope: Optional[ComponentScope] = None,
hashes: Optional[Iterable[HashType]] = None,
licenses: Optional[Iterable[License]] = 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,
modified: bool = False,
manufacturer: Optional[OrganizationalEntity] = None,
authors: Optional[Iterable[OrganizationalContact]] = None,
omnibor_ids: Optional[Iterable[OmniborId]] = None,
swhids: Optional[Iterable[Swhid]] = None,
crypto_properties: Optional[CryptoProperties] = None,
tags: Optional[Iterable[str]] = None,
# Deprecated in v1.6
author: Optional[str] = None,
) -> None:
self.type = type
self.mime_type = mime_type
self._bom_ref = _bom_ref_from_str(bom_ref)
self.supplier = supplier
self.manufacturer = manufacturer
self.authors = authors or [] # type:ignore[assignment]
self.author = author
self.publisher = publisher
self.group = group
self.name = name
self.version = version
self.description = description
self.scope = scope
self.hashes = hashes or [] # type:ignore[assignment]
self.licenses = licenses or [] # type:ignore[assignment]
self.copyright = copyright
self.cpe = cpe
self.purl = purl
self.omnibor_ids = omnibor_ids or [] # type:ignore[assignment]
self.swhids = swhids or [] # type:ignore[assignment]
self.swid = swid
self.modified = modified
self.pedigree = pedigree
self.external_references = external_references or [] # type:ignore[assignment]
self.properties = properties or [] # type:ignore[assignment]
self.components = components or [] # type:ignore[assignment]
self.evidence = evidence
self.release_notes = release_notes
self.crypto_properties = crypto_properties
self.tags = tags or [] # type:ignore[assignment]
if modified:
warn('`.component.modified` is deprecated from CycloneDX v1.3 onwards. '
'Please use `@.pedigree` instead.', DeprecationWarning)
if author:
warn('`.component.author` is deprecated from CycloneDX v1.6 onwards. '
'Please use `@.authors` or `@.manufacturer` instead.', DeprecationWarning)
@property
@serializable.type_mapping(_ComponentTypeSerializationHelper)
@serializable.xml_attribute()
def type(self) -> ComponentType:
"""
Get the type of this Component.
Returns:
Declared type of this Component as `ComponentType`.
"""
return self._type
@type.setter
def type(self, type: ComponentType) -> None:
self._type = type
@property
@serializable.xml_string(serializable.XmlStringSerializationType.TOKEN)
def mime_type(self) -> Optional[str]:
"""
Get any declared mime-type for this Component.
When used on file components, the mime-type can provide additional context about the kind of file being
represented such as an image, font, or executable. Some library or framework components may also have an
associated mime-type.
Returns:
`str` if set else `None`
"""
return self._mime_type
@mime_type.setter
def mime_type(self, mime_type: Optional[str]) -> None:
self._mime_type = mime_type
@property
@serializable.json_name('bom-ref')
@serializable.type_mapping(BomRef)
@serializable.view(SchemaVersion1Dot1)
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_attribute()
@serializable.xml_name('bom-ref')
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.
Returns:
`BomRef`
"""
return self._bom_ref
@property
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(1)
def supplier(self) -> Optional[OrganizationalEntity]:
"""
The organization that supplied the component. The supplier may often be the manufacture, 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
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(2)
def manufacturer(self) -> Optional[OrganizationalEntity]:
"""
The organization that created the component.
Manufacturer is common in components created through automated processes.
Components created through manual means may have `@.authors` instead.
Returns:
`OrganizationalEntity` if set else `None`
"""
return self._manufacturer
@manufacturer.setter
def manufacturer(self, manufacturer: Optional[OrganizationalEntity]) -> None:
self._manufacturer = manufacturer
@property
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'author')
@serializable.xml_sequence(3)
def authors(self) -> 'SortedSet[OrganizationalContact]':
"""
The person(s) who created the component.
Authors are common in components created through manual processes.
Components created through automated means may have `@.manufacturer` instead.
Returns:
`Iterable[OrganizationalContact]` if set else `None`
"""
return self._authors
@authors.setter
def authors(self, authors: Iterable[OrganizationalContact]) -> None:
self._authors = SortedSet(authors)
@property
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6) # todo: this is deprecated in v1.6?
@serializable.xml_sequence(4)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def author(self) -> Optional[str]:
"""
The person(s) or organization(s) that authored the component.
Returns:
`str` if set else `None`
"""
return self._author
@author.setter
def author(self, author: Optional[str]) -> None:
self._author = author
@property
@serializable.xml_sequence(5)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def publisher(self) -> Optional[str]:
"""
The person(s) or organization(s) that published the component
Returns:
`str` if set else `None`
"""
return self._publisher
@publisher.setter
def publisher(self, publisher: Optional[str]) -> None:
self._publisher = publisher
@property
@serializable.xml_sequence(6)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def group(self) -> Optional[str]:
"""
The grouping name or identifier. This will often be a shortened, single name of the company or project that
produced the component, or the source package or domain name. Whitespace and special characters should be
avoided.
Examples include: `apache`, `org.apache.commons`, and `apache.org`.
Returns:
`str` if set else `None`
"""
return self._group
@group.setter
def group(self, group: Optional[str]) -> None:
self._group = group
@property
@serializable.xml_sequence(7)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def name(self) -> str:
"""
The name of the component.
This will often be a shortened, single name of the component.
Examples: `commons-lang3` and `jquery`.
Returns:
`str`
"""
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name
@property
@serializable.include_none(SchemaVersion1Dot0, '')
@serializable.include_none(SchemaVersion1Dot1, '')
@serializable.include_none(SchemaVersion1Dot2, '')
@serializable.include_none(SchemaVersion1Dot3, '')
@serializable.xml_sequence(8)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def version(self) -> Optional[str]:
"""
The component version. The version should ideally comply with semantic versioning but is not enforced.
This is NOT optional for CycloneDX Schema Version < 1.4 but was agreed to default to an empty string where a
version was not supplied for schema versions < 1.4
Returns:
Declared version of this Component as `str` or `None`
"""
return self._version
@version.setter
def version(self, version: Optional[str]) -> None:
if version and len(version) > 1024:
warn('`.component.version`has a maximum length of 1024 from CycloneDX v1.6 onwards.', UserWarning)
self._version = version
@property
@serializable.xml_sequence(9)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def description(self) -> Optional[str]:
"""
Get the description of this Component.
Returns:
`str` if set, else `None`.
"""
return self._description
@description.setter
def description(self, description: Optional[str]) -> None:
self._description = description
@property
@serializable.type_mapping(_ComponentScopeSerializationHelper)
@serializable.xml_sequence(10)
def scope(self) -> Optional[ComponentScope]:
"""
Specifies the scope of the component.
If scope is not specified, 'required' scope should be assumed by the consumer of the BOM.
Returns:
`ComponentScope` or `None`
"""
return self._scope
@scope.setter
def scope(self, scope: Optional[ComponentScope]) -> None:
self._scope = scope
@property
@serializable.type_mapping(_HashTypeRepositorySerializationHelper)
@serializable.xml_sequence(11)
def hashes(self) -> 'SortedSet[HashType]':
"""
Optional list of hashes that help specify the integrity of this Component.
Returns:
Set of `HashType`
"""
return self._hashes
@hashes.setter
def hashes(self, hashes: Iterable[HashType]) -> None:
self._hashes = SortedSet(hashes)
@property
@serializable.view(SchemaVersion1Dot1)
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.type_mapping(_LicenseRepositorySerializationHelper)
@serializable.xml_sequence(12)
def licenses(self) -> LicenseRepository:
"""
A optional list of statements about how this Component is licensed.
Returns:
Set of `LicenseChoice`
"""
return self._licenses
@licenses.setter
def licenses(self, licenses: Iterable[License]) -> None:
self._licenses = LicenseRepository(licenses)
@property
@serializable.xml_sequence(13)
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
def copyright(self) -> Optional[str]:
"""
An optional copyright notice informing users of the underlying claims to copyright ownership in a published
work.
Returns:
`str` or `None`
"""
return self._copyright
@copyright.setter
def copyright(self, copyright: Optional[str]) -> None:
self._copyright = copyright
@property
@serializable.xml_sequence(14)
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
@serializable.type_mapping(PackageUrlSH)
@serializable.xml_sequence(15)
def purl(self) -> Optional[PackageURL]:
"""
Specifies the package-url (PURL).
The purl, if specified, must be valid and conform to the specification defined at:
https://github.com/package-url/purl-spec
Returns:
`PackageURL` or `None`
"""
return self._purl
@purl.setter
def purl(self, purl: Optional[PackageURL]) -> None:
self._purl = purl
@property
@serializable.json_name('omniborId')
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, child_name='omniborId')
@serializable.xml_sequence(16)
def omnibor_ids(self) -> 'SortedSet[OmniborId]':
"""
Specifies the OmniBOR Artifact ID. The OmniBOR, if specified, MUST be valid and conform to the specification
defined at: https://www.iana.org/assignments/uri-schemes/prov/gitoid
Returns:
`Iterable[str]` or `None`
"""
return self._omnibor_ids
@omnibor_ids.setter
def omnibor_ids(self, omnibor_ids: Iterable[OmniborId]) -> None:
self._omnibor_ids = SortedSet(omnibor_ids)
@property
@serializable.json_name('swhid')
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.FLAT, child_name='swhid')
@serializable.xml_sequence(17)
def swhids(self) -> 'SortedSet[Swhid]':
"""
Specifies the Software Heritage persistent identifier (SWHID). The SWHID, if specified, MUST be valid and
conform to the specification defined at:
https://docs.softwareheritage.org/devel/swh-model/persistent-identifiers.html
Returns:
`Iterable[Swhid]` if set else `None`
"""
return self._swhids
@swhids.setter
def swhids(self, swhids: Iterable[Swhid]) -> None:
self._swhids = SortedSet(swhids)
@property
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(18)
def swid(self) -> Optional[Swid]:
"""
Specifies metadata and content for ISO-IEC 19770-2 Software Identification (SWID) Tags.
Returns:
`Swid` if set else `None`
"""
return self._swid
@swid.setter
def swid(self, swid: Optional[Swid]) -> None:
self._swid = swid
@property
@serializable.view(SchemaVersion1Dot0) # todo: Deprecated in v1.3
@serializable.xml_sequence(19)
def modified(self) -> bool:
return self._modified
@modified.setter
def modified(self, modified: bool) -> None:
self._modified = modified
@property
@serializable.view(SchemaVersion1Dot1)
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(20)
def pedigree(self) -> Optional[Pedigree]:
"""
Component pedigree is a way to document complex supply chain scenarios where components are created,
distributed, modified, redistributed, combined with other components, etc.
Returns:
`Pedigree` if set else `None`
"""
return self._pedigree
@pedigree.setter
def pedigree(self, pedigree: Optional[Pedigree]) -> None:
self._pedigree = pedigree
@property
@serializable.view(SchemaVersion1Dot1)
@serializable.view(SchemaVersion1Dot2)
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
@serializable.xml_sequence(21)
def external_references(self) -> 'SortedSet[ExternalReference]':
"""
Provides the ability to document external references related to the component or to the project the component
describes.
Returns:
Set of `ExternalReference`
"""
return self._external_references
@external_references.setter
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
self._external_references = SortedSet(external_references)
@property
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
@serializable.xml_sequence(22)
def properties(self) -> 'SortedSet[Property]':
"""
Provides the ability to document properties in a key/value store. This provides flexibility to include data not
officially supported in the standard without having to use additional namespaces or create extensions.
Return:
Set of `Property`
"""
return self._properties
@properties.setter
def properties(self, properties: Iterable[Property]) -> None:
self._properties = SortedSet(properties)
@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'component')
@serializable.xml_sequence(23)
def components(self) -> "SortedSet['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 = SortedSet(components)
@property
@serializable.view(SchemaVersion1Dot3)
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(24)
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
@serializable.view(SchemaVersion1Dot4)
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(25)
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
# ...
# @serializable.view(SchemaVersion1Dot5)
# @serializable.xml_sequence(22)
# def model_card(self) -> ...:
# ... # TODO since CDX1.5
#
# @model_card.setter
# def model_card(self, ...) -> None:
# ... # TODO since CDX1.5
# @property
# ...
# @serializable.view(SchemaVersion1Dot5)
# @serializable.xml_sequence(23)
# def data(self) -> ...:
# ... # TODO since CDX1.5
#
# @data.setter
# def data(self, ...) -> None:
# ... # TODO since CDX1.5
@property
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_sequence(30)
def crypto_properties(self) -> Optional[CryptoProperties]:
"""
Cryptographic assets have properties that uniquely define them and that make them actionable for further
reasoning. As an example, it makes a difference if one knows the algorithm family (e.g. AES) or the specific
variant or instantiation (e.g. AES-128-GCM). This is because the security level and the algorithm primitive
(authenticated encryption) is only defined by the definition of the algorithm variant. The presence of a weak
cryptographic algorithm like SHA1 vs. HMAC-SHA1 also makes a difference.
Returns:
`CryptoProperties` or `None`
"""
return self._crypto_properties
@crypto_properties.setter
def crypto_properties(self, crypto_properties: Optional[CryptoProperties]) -> None:
self._crypto_properties = crypto_properties
@property
@serializable.view(SchemaVersion1Dot6)
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'tag')
@serializable.xml_sequence(31)
def tags(self) -> 'SortedSet[str]':
"""
Textual strings that aid in discovery, search, and retrieval of the associated object.
Tags often serve as a way to group or categorize similar or related objects by various attributes.
Returns:
`Iterable[str]`
"""
return self._tags
@tags.setter
def tags(self, tags: Iterable[str]) -> None:
self._tags = SortedSet(tags)
def get_all_nested_components(self, include_self: bool = False) -> Set['Component']:
components = set()
if include_self:
components.add(self)
for c in self.components:
components.update(c.get_all_nested_components(include_self=True))
return components
def get_pypi_url(self) -> str:
if self.version:
return f'https://pypi.org/project/{self.name}/{self.version}'
else:
return f'https://pypi.org/project/{self.name}'
def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self.type, self.group, self.name, self.version,
self.bom_ref.value,
None if self.purl is None else _ComparablePackageURL(self.purl),
self.swid, self.cpe, _ComparableTuple(self.swhids),
self.supplier, self.author, self.publisher,
self.description,
self.mime_type, self.scope, _ComparableTuple(self.hashes),
_ComparableTuple(self.licenses), self.copyright,
self.pedigree,
_ComparableTuple(self.external_references), _ComparableTuple(self.properties),
_ComparableTuple(self.components), self.evidence, self.release_notes, self.modified,
_ComparableTuple(self.authors), _ComparableTuple(self.omnibor_ids), self.manufacturer,
self.crypto_properties, _ComparableTuple(self.tags),
))
def __eq__(self, other: object) -> bool:
if isinstance(other, Component):
return self.__comparable_tuple() == other.__comparable_tuple()
return False
def __lt__(self, other: Any) -> bool:
if isinstance(other, Component):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented
def __hash__(self) -> int:
return hash(self.__comparable_tuple())
def __repr__(self) -> str:
return f'<Component bom-ref={self.bom_ref!r}, group={self.group}, name={self.name}, ' \
f'version={self.version}, type={self.type}>'