1368 lines
45 KiB
Python
1368 lines
45 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.
|
|
|
|
|
|
"""
|
|
This set of classes represents the data that is possible about known Vulnerabilities.
|
|
|
|
Prior to CycloneDX schema version 1.4, vulnerabilities were possible in XML versions ONLY of the standard through
|
|
a schema extension: https://cyclonedx.org/ext/vulnerability.
|
|
|
|
Since CycloneDX schema version 1.4, this has become part of the core schema.
|
|
|
|
.. note::
|
|
See the CycloneDX Schema extension definition https://cyclonedx.org/docs/1.6/#type_vulnerabilitiesType
|
|
"""
|
|
|
|
|
|
import re
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from enum import Enum
|
|
from typing import Any, Dict, FrozenSet, Iterable, Optional, Tuple, Type, Union
|
|
|
|
import py_serializable as serializable
|
|
from sortedcontainers import SortedSet
|
|
|
|
from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str
|
|
from .._internal.compare import ComparableTuple as _ComparableTuple
|
|
from ..exception.model import MutuallyExclusivePropertiesException, NoPropertiesProvidedException
|
|
from ..schema.schema import SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6
|
|
from . import Property, XsUri
|
|
from .bom_ref import BomRef
|
|
from .contact import OrganizationalContact, OrganizationalEntity
|
|
from .impact_analysis import (
|
|
ImpactAnalysisAffectedStatus,
|
|
ImpactAnalysisJustification,
|
|
ImpactAnalysisResponse,
|
|
ImpactAnalysisState,
|
|
)
|
|
from .tool import Tool, ToolRepository, _ToolRepositoryHelper
|
|
|
|
|
|
@serializable.serializable_class
|
|
class BomTargetVersionRange:
|
|
"""
|
|
Class that represents either a version or version range and its affected status.
|
|
|
|
`version` and `version_range` are mutually exclusive.
|
|
|
|
.. note::
|
|
See the CycloneDX schema: https://cyclonedx.org/docs/1.6/xml/#type_vulnerabilityType
|
|
"""
|
|
|
|
def __init__(
|
|
self, *,
|
|
version: Optional[str] = None,
|
|
range: Optional[str] = None,
|
|
status: Optional[ImpactAnalysisAffectedStatus] = None,
|
|
) -> None:
|
|
if not version and not range:
|
|
raise NoPropertiesProvidedException(
|
|
'One of version or range must be provided for BomTargetVersionRange - neither provided.'
|
|
)
|
|
if version and range:
|
|
raise MutuallyExclusivePropertiesException(
|
|
'Either version or range should be provided for BomTargetVersionRange - both provided.'
|
|
)
|
|
self.version = version
|
|
self.range = range
|
|
self.status = status
|
|
|
|
@property
|
|
@serializable.xml_sequence(1)
|
|
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
|
|
def version(self) -> Optional[str]:
|
|
"""
|
|
A single version of a component or service.
|
|
"""
|
|
return self._version
|
|
|
|
@version.setter
|
|
def version(self, version: Optional[str]) -> None:
|
|
self._version = version
|
|
|
|
@property
|
|
@serializable.xml_sequence(2)
|
|
def range(self) -> Optional[str]:
|
|
"""
|
|
A version range specified in Package URL Version Range syntax (vers) which is defined at
|
|
https://github.com/package-url/purl-spec/VERSION-RANGE-SPEC.rst
|
|
|
|
.. note::
|
|
The VERSION-RANGE-SPEC from Package URL is not a formalised standard at the time of writing and this no
|
|
validation of conformance with this draft standard is performed.
|
|
"""
|
|
return self._range
|
|
|
|
@range.setter
|
|
def range(self, range: Optional[str]) -> None:
|
|
self._range = range
|
|
|
|
@property
|
|
@serializable.xml_sequence(3)
|
|
def status(self) -> Optional[ImpactAnalysisAffectedStatus]:
|
|
"""
|
|
The vulnerability status for the version or range of versions.
|
|
"""
|
|
return self._status
|
|
|
|
@status.setter
|
|
def status(self, status: Optional[ImpactAnalysisAffectedStatus]) -> None:
|
|
self._status = status
|
|
|
|
def __comparable_tuple(self) -> _ComparableTuple:
|
|
return _ComparableTuple((
|
|
self.version, self.range, self.status
|
|
))
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, BomTargetVersionRange):
|
|
return self.__comparable_tuple() == other.__comparable_tuple()
|
|
return False
|
|
|
|
def __lt__(self, other: Any) -> bool:
|
|
if isinstance(other, BomTargetVersionRange):
|
|
return self.__comparable_tuple() < other.__comparable_tuple()
|
|
return NotImplemented
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(self.__comparable_tuple())
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<BomTargetVersionRange version={self.version}, version_range={self.range}, status={self.status}>'
|
|
|
|
|
|
@serializable.serializable_class
|
|
class BomTarget:
|
|
"""
|
|
Class that represents referencing a Component or Service in a BOM.
|
|
|
|
Aims to represent the sub-element `target` of the complex type `vulnerabilityType`.
|
|
|
|
You can either create a `cyclonedx.model.bom.Bom` yourself programmatically, or generate a `cyclonedx.model.bom.Bom`
|
|
from a `cyclonedx.parser.BaseParser` implementation.
|
|
|
|
.. note::
|
|
See the CycloneDX schema: https://cyclonedx.org/docs/1.6/#type_vulnerabilityType
|
|
"""
|
|
|
|
def __init__(
|
|
self, *,
|
|
ref: str,
|
|
versions: Optional[Iterable[BomTargetVersionRange]] = None,
|
|
) -> None:
|
|
self.ref = ref
|
|
self.versions = versions or [] # type:ignore[assignment]
|
|
|
|
@property
|
|
@serializable.xml_sequence(1)
|
|
def ref(self) -> str:
|
|
"""
|
|
Reference to a component or service by the objects `bom-ref`.
|
|
"""
|
|
return self._ref
|
|
|
|
@ref.setter
|
|
def ref(self, ref: str) -> None:
|
|
self._ref = ref
|
|
|
|
@property
|
|
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'version')
|
|
@serializable.xml_sequence(2)
|
|
def versions(self) -> 'SortedSet[BomTargetVersionRange]':
|
|
"""
|
|
Zero or more individual versions or range of versions.
|
|
|
|
Returns:
|
|
Set of `BomTargetVersionRange`
|
|
"""
|
|
return self._versions
|
|
|
|
@versions.setter
|
|
def versions(self, versions: Iterable[BomTargetVersionRange]) -> None:
|
|
self._versions = SortedSet(versions)
|
|
|
|
def __comparable_tuple(self) -> _ComparableTuple:
|
|
return _ComparableTuple((
|
|
self.ref,
|
|
_ComparableTuple(self.versions)
|
|
))
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, BomTarget):
|
|
return self.__comparable_tuple() == other.__comparable_tuple()
|
|
return False
|
|
|
|
def __lt__(self, other: Any) -> bool:
|
|
if isinstance(other, BomTarget):
|
|
return self.__comparable_tuple() < other.__comparable_tuple()
|
|
return NotImplemented
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(self.__comparable_tuple())
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<BomTarget ref={self.ref}>'
|
|
|
|
|
|
@serializable.serializable_class
|
|
class VulnerabilityAnalysis:
|
|
"""
|
|
Class that models the `analysis` sub-element of the `vulnerabilityType` complex type.
|
|
|
|
.. note::
|
|
See the CycloneDX schema: https://cyclonedx.org/docs/1.6/xml/#type_vulnerabilityType
|
|
"""
|
|
|
|
def __init__(
|
|
self, *,
|
|
state: Optional[ImpactAnalysisState] = None,
|
|
justification: Optional[ImpactAnalysisJustification] = None,
|
|
responses: Optional[Iterable[ImpactAnalysisResponse]] = None,
|
|
detail: Optional[str] = None,
|
|
first_issued: Optional[datetime] = None,
|
|
last_updated: Optional[datetime] = None,
|
|
) -> None:
|
|
self.state = state
|
|
self.justification = justification
|
|
self.responses = responses or [] # type:ignore[assignment]
|
|
self.detail = detail
|
|
self.first_issued = first_issued
|
|
self.last_updated = last_updated
|
|
|
|
@property
|
|
@serializable.xml_sequence(1)
|
|
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
|
|
|
|
@state.setter
|
|
def state(self, state: Optional[ImpactAnalysisState]) -> None:
|
|
self._state = state
|
|
|
|
@property
|
|
@serializable.xml_sequence(2)
|
|
def justification(self) -> Optional[ImpactAnalysisJustification]:
|
|
"""
|
|
The rationale of why the impact analysis state was asserted.
|
|
|
|
Returns:
|
|
`ImpactAnalysisJustification` if set else `None`
|
|
"""
|
|
return self._justification
|
|
|
|
@justification.setter
|
|
def justification(self, justification: Optional[ImpactAnalysisJustification]) -> None:
|
|
self._justification = justification
|
|
|
|
@property
|
|
@serializable.json_name('response')
|
|
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'response')
|
|
@serializable.xml_sequence(3)
|
|
def responses(self) -> 'SortedSet[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._responses
|
|
|
|
@responses.setter
|
|
def responses(self, responses: Iterable[ImpactAnalysisResponse]) -> None:
|
|
self._responses = SortedSet(responses)
|
|
|
|
@property
|
|
@serializable.xml_sequence(4)
|
|
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.
|
|
|
|
Returns:
|
|
`str` if set else `None`
|
|
"""
|
|
return self._detail
|
|
|
|
@detail.setter
|
|
def detail(self, detail: Optional[str]) -> None:
|
|
self._detail = detail
|
|
|
|
@property
|
|
@serializable.view(SchemaVersion1Dot5)
|
|
@serializable.view(SchemaVersion1Dot6)
|
|
@serializable.type_mapping(serializable.helpers.XsdDateTime)
|
|
@serializable.xml_sequence(5)
|
|
def first_issued(self) -> Optional[datetime]:
|
|
return self._first_issued
|
|
|
|
@first_issued.setter
|
|
def first_issued(self, first_issue: Optional[datetime]) -> None:
|
|
self._first_issued = first_issue
|
|
|
|
@property
|
|
@serializable.view(SchemaVersion1Dot5)
|
|
@serializable.view(SchemaVersion1Dot6)
|
|
@serializable.type_mapping(serializable.helpers.XsdDateTime)
|
|
@serializable.xml_sequence(6)
|
|
def last_updated(self) -> Optional[datetime]:
|
|
return self._last_updated
|
|
|
|
@last_updated.setter
|
|
def last_updated(self, last_updated: Optional[datetime]) -> None:
|
|
self._last_updated = last_updated
|
|
|
|
def __comparable_tuple(self) -> _ComparableTuple:
|
|
return _ComparableTuple((
|
|
self.state, self.justification,
|
|
_ComparableTuple(self.responses),
|
|
self.detail,
|
|
self.first_issued, self.last_updated
|
|
))
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, VulnerabilityAnalysis):
|
|
return self.__comparable_tuple() == other.__comparable_tuple()
|
|
return False
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(self.__comparable_tuple())
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<VulnerabilityAnalysis state={self.state}, justification={self.justification}>'
|
|
|
|
|
|
@serializable.serializable_class
|
|
class VulnerabilityAdvisory:
|
|
"""
|
|
Class that models the `advisoryType` complex type.
|
|
|
|
.. note::
|
|
See the CycloneDX schema: https://cyclonedx.org/docs/1.6/#type_advisoryType
|
|
"""
|
|
|
|
def __init__(
|
|
self, *,
|
|
url: XsUri,
|
|
title: Optional[str] = None,
|
|
) -> None:
|
|
self.title = title
|
|
self.url = url
|
|
|
|
@property
|
|
@serializable.xml_sequence(1)
|
|
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
|
|
def title(self) -> Optional[str]:
|
|
"""
|
|
The title of this advisory.
|
|
"""
|
|
return self._title
|
|
|
|
@title.setter
|
|
def title(self, title: Optional[str]) -> None:
|
|
self._title = title
|
|
|
|
@property
|
|
@serializable.xml_sequence(2)
|
|
def url(self) -> XsUri:
|
|
"""
|
|
The url of this advisory.
|
|
"""
|
|
return self._url
|
|
|
|
@url.setter
|
|
def url(self, url: XsUri) -> None:
|
|
self._url = url
|
|
|
|
def __comparable_tuple(self) -> _ComparableTuple:
|
|
return _ComparableTuple((
|
|
self.title, self.url
|
|
))
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, VulnerabilityAdvisory):
|
|
return self.__comparable_tuple() == other.__comparable_tuple()
|
|
return False
|
|
|
|
def __lt__(self, other: Any) -> bool:
|
|
if isinstance(other, VulnerabilityAdvisory):
|
|
return self.__comparable_tuple() < other.__comparable_tuple()
|
|
return NotImplemented
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(self.__comparable_tuple())
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<VulnerabilityAdvisory url={self.url}, title={self.title}>'
|
|
|
|
|
|
@serializable.serializable_class
|
|
class VulnerabilitySource:
|
|
"""
|
|
Class that models the `vulnerabilitySourceType` complex type.
|
|
|
|
This type is used for multiple purposes in the CycloneDX schema.
|
|
|
|
.. note::
|
|
See the CycloneDX schema: https://cyclonedx.org/docs/1.6/xml/#type_vulnerabilitySourceType
|
|
"""
|
|
|
|
def __init__(
|
|
self, *,
|
|
name: Optional[str] = None,
|
|
url: Optional[XsUri] = None,
|
|
) -> None:
|
|
self.name = name
|
|
self.url = url
|
|
|
|
@property
|
|
@serializable.xml_sequence(1)
|
|
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
|
|
def name(self) -> Optional[str]:
|
|
"""
|
|
Name of this Source.
|
|
"""
|
|
return self._name
|
|
|
|
@name.setter
|
|
def name(self, name: Optional[str]) -> None:
|
|
self._name = name
|
|
|
|
@property
|
|
@serializable.xml_sequence(2)
|
|
def url(self) -> Optional[XsUri]:
|
|
"""
|
|
The url of this Source.
|
|
"""
|
|
return self._url
|
|
|
|
@url.setter
|
|
def url(self, url: Optional[XsUri]) -> None:
|
|
self._url = url
|
|
|
|
def __comparable_tuple(self) -> _ComparableTuple:
|
|
return _ComparableTuple((
|
|
self.name, self.url
|
|
))
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, VulnerabilitySource):
|
|
return self.__comparable_tuple() == other.__comparable_tuple()
|
|
return False
|
|
|
|
def __lt__(self, other: Any) -> bool:
|
|
if isinstance(other, VulnerabilitySource):
|
|
return self.__comparable_tuple() < other.__comparable_tuple()
|
|
return NotImplemented
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(self.__comparable_tuple())
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<VulnerabilityAdvisory name={self.name}, url={self.url}>'
|
|
|
|
|
|
@serializable.serializable_class
|
|
class VulnerabilityReference:
|
|
"""
|
|
Class that models the nested `reference` within the `vulnerabilityType` complex type.
|
|
|
|
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.
|
|
|
|
.. note::
|
|
See the CycloneDX schema: https://cyclonedx.org/docs/1.6/xml/#type_vulnerabilityType
|
|
|
|
.. note::
|
|
Properties ``id`` and ``source`` are mandatory.
|
|
|
|
History:
|
|
* In v1.4 JSON scheme, both properties were mandatory
|
|
https://github.com/CycloneDX/specification/blob/d570ffb8956d796585b9574e57598c42ee9de770/schema/bom-1.4.schema.json#L1455-L1474
|
|
* In v1.4 XML schema, both properties were optional
|
|
https://github.com/CycloneDX/specification/blob/d570ffb8956d796585b9574e57598c42ee9de770/schema/bom-1.4.xsd#L1788-L1797
|
|
* In v1.5 XML schema, both were mandatory
|
|
https://github.com/CycloneDX/specification/blob/d570ffb8956d796585b9574e57598c42ee9de770/schema/bom-1.5.xsd#L3364-L3374
|
|
|
|
Decision:
|
|
Since CycloneDXCoreWorkingGroup chose JSON schema as the dominant schema, the one that serves as first spec
|
|
implementation, and since XML schema was "fixed" to work same as JSON schema, we'd consider it canon/spec that
|
|
both properties were always mandatory.
|
|
"""
|
|
|
|
def __init__(
|
|
self, *,
|
|
id: str,
|
|
source: VulnerabilitySource,
|
|
) -> None:
|
|
self.id = id
|
|
self.source = source
|
|
|
|
@property
|
|
@serializable.xml_sequence(1)
|
|
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
|
|
def id(self) -> str:
|
|
"""
|
|
The identifier that uniquely identifies the vulnerability in the associated Source. For example: CVE-2021-39182.
|
|
"""
|
|
return self._id
|
|
|
|
@id.setter
|
|
def id(self, id: str) -> None:
|
|
self._id = id
|
|
|
|
@property
|
|
@serializable.xml_sequence(2)
|
|
def source(self) -> VulnerabilitySource:
|
|
"""
|
|
The source that published the vulnerability.
|
|
"""
|
|
return self._source
|
|
|
|
@source.setter
|
|
def source(self, source: VulnerabilitySource) -> None:
|
|
self._source = source
|
|
|
|
def __comparable_tuple(self) -> _ComparableTuple:
|
|
return _ComparableTuple((
|
|
self.id, self.source
|
|
))
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, VulnerabilityReference):
|
|
return self.__comparable_tuple() == other.__comparable_tuple()
|
|
return False
|
|
|
|
def __lt__(self, other: Any) -> bool:
|
|
if isinstance(other, VulnerabilityReference):
|
|
return self.__comparable_tuple() < other.__comparable_tuple()
|
|
return NotImplemented
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(self.__comparable_tuple())
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<VulnerabilityReference id={self.id}, source={self.source}>'
|
|
|
|
|
|
@serializable.serializable_enum
|
|
class VulnerabilityScoreSource(str, Enum):
|
|
"""
|
|
Enum object that defines the permissible source types for a Vulnerability's score.
|
|
|
|
.. note::
|
|
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/#type_scoreSourceType
|
|
|
|
.. note::
|
|
No explicit carry-over from the former schema extension:
|
|
https://github.com/CycloneDX/specification/blob/master/schema/ext/vulnerability-1.0.xsd
|
|
"""
|
|
# see `_VulnerabilityScoreSourceSerializationHelper.__CASES` for view/case map
|
|
CVSS_V2 = 'CVSSv2'
|
|
CVSS_V3 = 'CVSSv3'
|
|
CVSS_V3_1 = 'CVSSv31'
|
|
CVSS_V4 = 'CVSSv4' # Only supported in >= 1.5
|
|
OWASP = 'OWASP' # Name change in 1.4
|
|
SSVC = 'SSVC' # Only supported in >= 1.5
|
|
# --
|
|
OTHER = 'other'
|
|
|
|
@staticmethod
|
|
def get_from_vector(vector: str) -> 'VulnerabilityScoreSource':
|
|
"""
|
|
Attempt to derive the correct SourceType from an attack vector.
|
|
|
|
For example, often attack vector strings are prefixed with the scheme in question - such
|
|
that __CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N__ would be the vector
|
|
__AV:L/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N__ under the __CVSS 3__ scheme.
|
|
|
|
Returns:
|
|
Always returns an instance of `VulnerabilityScoreSource`. `VulnerabilityScoreSource.OTHER` is
|
|
returned if the scheme is not obvious or known to us.
|
|
"""
|
|
if vector.startswith('CVSS:3.'):
|
|
return VulnerabilityScoreSource.CVSS_V3
|
|
elif vector.startswith('CVSS:2.'):
|
|
return VulnerabilityScoreSource.CVSS_V2
|
|
elif vector.startswith('OWASP'):
|
|
return VulnerabilityScoreSource.OWASP
|
|
else:
|
|
return VulnerabilityScoreSource.OTHER
|
|
|
|
def get_localised_vector(self, vector: str) -> str:
|
|
"""
|
|
This method will remove any Source Scheme type from the supplied vector, returning just the vector.
|
|
|
|
.. Note::
|
|
Currently supports CVSS 3.x, CVSS 2.x and OWASP schemes.
|
|
|
|
Returns:
|
|
The vector without any scheme prefix as a `str`.
|
|
"""
|
|
if self == VulnerabilityScoreSource.CVSS_V3 and vector.startswith('CVSS:3.'):
|
|
return re.sub('^CVSS:3\\.\\d/?', '', vector)
|
|
|
|
if self == VulnerabilityScoreSource.CVSS_V2 and vector.startswith('CVSS:2.'):
|
|
return re.sub('^CVSS:2\\.\\d/?', '', vector)
|
|
|
|
if self == VulnerabilityScoreSource.OWASP and vector.startswith('OWASP'):
|
|
return re.sub('^OWASP/?', '', vector)
|
|
|
|
return vector
|
|
|
|
def get_value_pre_1_4(self) -> str:
|
|
"""
|
|
Some of the enum values changed in 1.4 of the CycloneDX spec. This method allows us to
|
|
backport some of the changes for pre-1.4.
|
|
|
|
Returns:
|
|
`str`
|
|
"""
|
|
if self == VulnerabilityScoreSource.OWASP:
|
|
return 'OWASP Risk'
|
|
return self.value # type:ignore[no-any-return]
|
|
|
|
|
|
class _VulnerabilityScoreSourceSerializationHelper(serializable.helpers.BaseHelper):
|
|
""" THIS CLASS IS NON-PUBLIC API """
|
|
|
|
__CASES: Dict[Type[serializable.ViewType], FrozenSet[VulnerabilityScoreSource]] = dict()
|
|
__CASES[SchemaVersion1Dot4] = frozenset({
|
|
VulnerabilityScoreSource.CVSS_V2,
|
|
VulnerabilityScoreSource.CVSS_V3,
|
|
VulnerabilityScoreSource.CVSS_V3_1,
|
|
VulnerabilityScoreSource.OWASP,
|
|
VulnerabilityScoreSource.OTHER,
|
|
})
|
|
__CASES[SchemaVersion1Dot5] = __CASES[SchemaVersion1Dot4] | {
|
|
VulnerabilityScoreSource.CVSS_V4,
|
|
VulnerabilityScoreSource.SSVC
|
|
}
|
|
__CASES[SchemaVersion1Dot6] = __CASES[SchemaVersion1Dot5]
|
|
|
|
@classmethod
|
|
def __normalize(cls, vss: VulnerabilityScoreSource, view: Type[serializable.ViewType]) -> str:
|
|
return (
|
|
vss
|
|
if vss in cls.__CASES.get(view, ())
|
|
else VulnerabilityScoreSource.OTHER
|
|
).value
|
|
|
|
@classmethod
|
|
def json_normalize(cls, o: Any, *,
|
|
view: Optional[Type[serializable.ViewType]],
|
|
**__: Any) -> str:
|
|
assert view is not None
|
|
return cls.__normalize(o, view)
|
|
|
|
@classmethod
|
|
def xml_normalize(cls, o: Any, *,
|
|
view: Optional[Type[serializable.ViewType]],
|
|
**__: Any) -> str:
|
|
assert view is not None
|
|
return cls.__normalize(o, view)
|
|
|
|
@classmethod
|
|
def deserialize(cls, o: Any) -> VulnerabilityScoreSource:
|
|
return VulnerabilityScoreSource(o)
|
|
|
|
|
|
@serializable.serializable_enum
|
|
class VulnerabilitySeverity(str, Enum):
|
|
"""
|
|
Class that defines the permissible severities for a Vulnerability.
|
|
|
|
.. note::
|
|
See the CycloneDX schema: https://cyclonedx.org/docs/1.6/#type_severityType
|
|
"""
|
|
NONE = 'none'
|
|
INFO = 'info' # Only >= 1.4
|
|
LOW = 'low'
|
|
MEDIUM = 'medium'
|
|
HIGH = 'high'
|
|
CRITICAL = 'critical'
|
|
UNKNOWN = 'unknown'
|
|
|
|
@staticmethod
|
|
def get_from_cvss_scores(scores: Union[Tuple[float, ...], float, None]) -> 'VulnerabilitySeverity':
|
|
"""
|
|
Derives the Severity of a Vulnerability from it's declared CVSS scores.
|
|
|
|
Args:
|
|
scores: A `tuple` of CVSS scores. CVSS scoring system allows for up to three separate scores.
|
|
|
|
Returns:
|
|
Always returns an instance of `VulnerabilitySeverity`.
|
|
"""
|
|
if type(scores) is float:
|
|
scores = (scores,)
|
|
|
|
if scores is None:
|
|
return VulnerabilitySeverity.UNKNOWN
|
|
|
|
max_cvss_score: float
|
|
if isinstance(scores, tuple):
|
|
max_cvss_score = max(scores)
|
|
else:
|
|
max_cvss_score = float(scores)
|
|
|
|
if max_cvss_score >= 9.0:
|
|
return VulnerabilitySeverity.CRITICAL
|
|
elif max_cvss_score >= 7.0:
|
|
return VulnerabilitySeverity.HIGH
|
|
elif max_cvss_score >= 4.0:
|
|
return VulnerabilitySeverity.MEDIUM
|
|
elif max_cvss_score > 0.0:
|
|
return VulnerabilitySeverity.LOW
|
|
else:
|
|
return VulnerabilitySeverity.NONE
|
|
|
|
|
|
@serializable.serializable_class
|
|
class VulnerabilityRating:
|
|
"""
|
|
Class that models the `ratingType` complex element CycloneDX core schema.
|
|
|
|
This class previously modelled the `scoreType` complexe type in the schema extension used prior to schema version
|
|
1.4 - see https://github.com/CycloneDX/specification/blob/master/schema/ext/vulnerability-1.0.xsd.
|
|
|
|
.. note::
|
|
See `ratingType` in https://cyclonedx.org/docs/1.6/xml/#ratingType
|
|
|
|
.. warning::
|
|
As part of implementing support for CycloneDX schema version 1.4, the three score types defined in the schema
|
|
extension used prior to 1.4 have been deprecated. The deprecated `score_base` should loosely be equivalent to
|
|
the new `score` in 1.4 schema. Both `score_impact` and `score_exploitability` are deprecated and removed as
|
|
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,
|
|
severity: Optional[VulnerabilitySeverity] = None,
|
|
method: Optional[VulnerabilityScoreSource] = None,
|
|
vector: Optional[str] = None,
|
|
justification: Optional[str] = None,
|
|
) -> None:
|
|
self.source = source
|
|
self.score = score
|
|
self.severity = severity
|
|
self.method = method
|
|
self.vector = vector
|
|
self.justification = justification
|
|
|
|
if vector and method:
|
|
self.vector = method.get_localised_vector(vector=vector)
|
|
|
|
@property
|
|
@serializable.xml_sequence(1)
|
|
def source(self) -> Optional[VulnerabilitySource]:
|
|
"""
|
|
The source that published the vulnerability.
|
|
"""
|
|
return self._source
|
|
|
|
@source.setter
|
|
def source(self, source: Optional[VulnerabilitySource]) -> None:
|
|
self._source = source
|
|
|
|
@property
|
|
@serializable.string_format('.1f')
|
|
@serializable.xml_sequence(2)
|
|
def score(self) -> Optional[Decimal]:
|
|
"""
|
|
The numerical score of the rating.
|
|
"""
|
|
return self._score
|
|
|
|
@score.setter
|
|
def score(self, score: Optional[Decimal]) -> None:
|
|
self._score = score
|
|
|
|
@property
|
|
@serializable.xml_sequence(3)
|
|
def severity(self) -> Optional[VulnerabilitySeverity]:
|
|
"""
|
|
The textual representation of the severity that corresponds to the numerical score of the rating.
|
|
"""
|
|
return self._severity
|
|
|
|
@severity.setter
|
|
def severity(self, severity: Optional[VulnerabilitySeverity]) -> None:
|
|
self._severity = severity
|
|
|
|
@property
|
|
@serializable.type_mapping(_VulnerabilityScoreSourceSerializationHelper)
|
|
@serializable.xml_sequence(4)
|
|
def method(self) -> Optional[VulnerabilityScoreSource]:
|
|
"""
|
|
The risk scoring methodology/standard used.
|
|
"""
|
|
return self._method
|
|
|
|
@method.setter
|
|
def method(self, score_source: Optional[VulnerabilityScoreSource]) -> None:
|
|
self._method = score_source
|
|
|
|
@property
|
|
@serializable.xml_sequence(5)
|
|
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
|
|
def vector(self) -> Optional[str]:
|
|
"""
|
|
The textual representation of the metric values used to score the vulnerability - also known as the vector.
|
|
"""
|
|
return self._vector
|
|
|
|
@vector.setter
|
|
def vector(self, vector: Optional[str]) -> None:
|
|
self._vector = vector
|
|
|
|
@property
|
|
@serializable.xml_sequence(6)
|
|
def justification(self) -> Optional[str]:
|
|
"""
|
|
An optional reason for rating the vulnerability as it was.
|
|
"""
|
|
return self._justification
|
|
|
|
@justification.setter
|
|
def justification(self, justification: Optional[str]) -> None:
|
|
self._justification = justification
|
|
|
|
def __comparable_tuple(self) -> _ComparableTuple:
|
|
return _ComparableTuple((
|
|
self.severity, self.score or 0,
|
|
self.source, self.method, self.vector,
|
|
self.justification
|
|
))
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, VulnerabilityRating):
|
|
return self.__comparable_tuple() == other.__comparable_tuple()
|
|
return False
|
|
|
|
def __lt__(self, other: Any) -> bool:
|
|
if isinstance(other, VulnerabilityRating):
|
|
return self.__comparable_tuple() < other.__comparable_tuple()
|
|
return NotImplemented
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(self.__comparable_tuple())
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<VulnerabilityRating severity={self.severity} score={self.score}, ' \
|
|
f'source={self.source} method={self.method} vector={self.vector}' \
|
|
f'justification={self.justification}>'
|
|
|
|
|
|
@serializable.serializable_class
|
|
class VulnerabilityCredits:
|
|
"""
|
|
Class that models the `credits` of `vulnerabilityType` complex type in the CycloneDX schema (version >= 1.4).
|
|
|
|
This class also provides data support for schema versions < 1.4 where Vulnerabilites were possible through a schema
|
|
extension (in XML only).
|
|
|
|
.. note::
|
|
See the CycloneDX schema: https://cyclonedx.org/docs/1.6/xml/#type_vulnerabilityType
|
|
"""
|
|
|
|
def __init__(
|
|
self, *,
|
|
organizations: Optional[Iterable[OrganizationalEntity]] = None,
|
|
individuals: Optional[Iterable[OrganizationalContact]] = None,
|
|
) -> None:
|
|
self.organizations = organizations or [] # type:ignore[assignment]
|
|
self.individuals = individuals or [] # type:ignore[assignment]
|
|
|
|
@property
|
|
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'organization')
|
|
@serializable.xml_sequence(1)
|
|
def organizations(self) -> 'SortedSet[OrganizationalEntity]':
|
|
"""
|
|
The organizations credited with vulnerability discovery.
|
|
|
|
Returns:
|
|
Set of `OrganizationalEntity`
|
|
"""
|
|
return self._organizations
|
|
|
|
@organizations.setter
|
|
def organizations(self, organizations: Iterable[OrganizationalEntity]) -> None:
|
|
self._organizations = SortedSet(organizations)
|
|
|
|
@property
|
|
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'individual')
|
|
@serializable.xml_sequence(2)
|
|
def individuals(self) -> 'SortedSet[OrganizationalContact]':
|
|
"""
|
|
The individuals, not associated with organizations, that are credited with vulnerability discovery.
|
|
|
|
Returns:
|
|
Set of `OrganizationalContact`
|
|
"""
|
|
return self._individuals
|
|
|
|
@individuals.setter
|
|
def individuals(self, individuals: Iterable[OrganizationalContact]) -> None:
|
|
self._individuals = SortedSet(individuals)
|
|
|
|
def __comparable_tuple(self) -> _ComparableTuple:
|
|
return _ComparableTuple((
|
|
_ComparableTuple(self.organizations),
|
|
_ComparableTuple(self.individuals)
|
|
))
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, VulnerabilityCredits):
|
|
return self.__comparable_tuple() == other.__comparable_tuple()
|
|
return False
|
|
|
|
def __lt__(self, other: Any) -> bool:
|
|
if isinstance(other, VulnerabilityCredits):
|
|
return self.__comparable_tuple() < other.__comparable_tuple()
|
|
return NotImplemented
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(self.__comparable_tuple())
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<VulnerabilityCredits id={id(self)}>'
|
|
|
|
|
|
@serializable.serializable_class
|
|
class Vulnerability:
|
|
"""
|
|
Class that models the `vulnerabilityType` complex type in the CycloneDX schema (version >= 1.4).
|
|
|
|
This class also provides data support for schema versions < 1.4 where Vulnerabilites were possible through a schema
|
|
extension (in XML only).
|
|
|
|
.. note::
|
|
See the CycloneDX schema: https://cyclonedx.org/docs/1.6/#type_vulnerabilityType
|
|
"""
|
|
|
|
def __init__(
|
|
self, *,
|
|
bom_ref: Optional[Union[str, BomRef]] = None,
|
|
id: Optional[str] = None,
|
|
source: Optional[VulnerabilitySource] = 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,
|
|
workaround: Optional[str] = None,
|
|
advisories: Optional[Iterable[VulnerabilityAdvisory]] = None,
|
|
created: Optional[datetime] = None,
|
|
published: Optional[datetime] = None,
|
|
updated: Optional[datetime] = None,
|
|
credits: Optional[VulnerabilityCredits] = None,
|
|
tools: Optional[Union[Iterable[Tool], ToolRepository]] = None,
|
|
analysis: Optional[VulnerabilityAnalysis] = None,
|
|
affects: Optional[Iterable[BomTarget]] = None,
|
|
properties: Optional[Iterable[Property]] = None,
|
|
) -> None:
|
|
self._bom_ref = _bom_ref_from_str(bom_ref)
|
|
self.id = id
|
|
self.source = source
|
|
self.references = references or [] # type:ignore[assignment]
|
|
self.ratings = ratings or [] # type:ignore[assignment]
|
|
self.cwes = cwes or [] # type:ignore[assignment]
|
|
self.description = description
|
|
self.detail = detail
|
|
self.recommendation = recommendation
|
|
self.workaround = workaround
|
|
self.advisories = advisories or [] # type:ignore[assignment]
|
|
self.created = created
|
|
self.published = published
|
|
self.updated = updated
|
|
self.credits = credits
|
|
self.tools = tools or [] # type:ignore[assignment]
|
|
self.analysis = analysis
|
|
self.affects = affects or [] # type:ignore[assignment]
|
|
self.properties = properties or [] # type:ignore[assignment]
|
|
|
|
@property
|
|
@serializable.json_name('bom-ref')
|
|
@serializable.type_mapping(BomRef)
|
|
@serializable.xml_attribute()
|
|
@serializable.xml_name('bom-ref')
|
|
def bom_ref(self) -> BomRef:
|
|
"""
|
|
Get the unique reference for this Vulnerability in this BOM.
|
|
|
|
Returns:
|
|
`BomRef`
|
|
"""
|
|
return self._bom_ref
|
|
|
|
@property
|
|
@serializable.xml_sequence(1)
|
|
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
|
|
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
|
|
|
|
@id.setter
|
|
def id(self, id: Optional[str]) -> None:
|
|
self._id = id
|
|
|
|
@property
|
|
@serializable.xml_sequence(2)
|
|
def source(self) -> Optional[VulnerabilitySource]:
|
|
"""
|
|
The source that published the vulnerability.
|
|
|
|
Returns:
|
|
`VulnerabilitySource` if set else `None`
|
|
"""
|
|
return self._source
|
|
|
|
@source.setter
|
|
def source(self, source: Optional[VulnerabilitySource]) -> None:
|
|
self._source = source
|
|
|
|
@property
|
|
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
|
|
@serializable.xml_sequence(3)
|
|
def references(self) -> 'SortedSet[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: Iterable[VulnerabilityReference]) -> None:
|
|
self._references = SortedSet(references)
|
|
|
|
@property
|
|
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'rating')
|
|
@serializable.xml_sequence(4)
|
|
def ratings(self) -> 'SortedSet[VulnerabilityRating]':
|
|
"""
|
|
List of vulnerability ratings.
|
|
|
|
Returns:
|
|
Set of `VulnerabilityRating`
|
|
"""
|
|
return self._ratings
|
|
|
|
@ratings.setter
|
|
def ratings(self, ratings: Iterable[VulnerabilityRating]) -> None:
|
|
self._ratings = SortedSet(ratings)
|
|
|
|
@property
|
|
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'cwe')
|
|
@serializable.xml_sequence(5)
|
|
def cwes(self) -> 'SortedSet[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: Iterable[int]) -> None:
|
|
self._cwes = SortedSet(cwes)
|
|
|
|
@property
|
|
@serializable.xml_sequence(6)
|
|
def description(self) -> Optional[str]:
|
|
"""
|
|
A description of the vulnerability as provided by the source.
|
|
|
|
Returns:
|
|
`str` if set else `None`
|
|
"""
|
|
return self._description
|
|
|
|
@description.setter
|
|
def description(self, description: Optional[str]) -> None:
|
|
self._description = description
|
|
|
|
@property
|
|
@serializable.xml_sequence(7)
|
|
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
|
|
|
|
@detail.setter
|
|
def detail(self, detail: Optional[str]) -> None:
|
|
self._detail = detail
|
|
|
|
@property
|
|
@serializable.xml_sequence(8)
|
|
def recommendation(self) -> Optional[str]:
|
|
"""
|
|
Recommendations of how the vulnerability can be remediated or mitigated.
|
|
|
|
Returns:
|
|
`str` if set else `None`
|
|
"""
|
|
return self._recommendation
|
|
|
|
@recommendation.setter
|
|
def recommendation(self, recommendation: Optional[str]) -> None:
|
|
self._recommendation = recommendation
|
|
|
|
@property
|
|
@serializable.view(SchemaVersion1Dot5)
|
|
@serializable.view(SchemaVersion1Dot6)
|
|
@serializable.xml_sequence(9)
|
|
def workaround(self) -> Optional[str]:
|
|
"""
|
|
A bypass, usually temporary, of the vulnerability that reduces its likelihood and/or impact.
|
|
Workarounds often involve changes to configuration or deployments.
|
|
|
|
Returns:
|
|
`str` if set else `None`
|
|
"""
|
|
return self._workaround
|
|
|
|
@workaround.setter
|
|
def workaround(self, workaround: Optional[str]) -> None:
|
|
self._workaround = workaround
|
|
|
|
# @property
|
|
# @serializable.view(SchemaVersion1Dot5)
|
|
# @serializable.xml_sequence(10)
|
|
# def proof_of_concept(self) -> ...:
|
|
# ... # TODO since CDX 1.5
|
|
#
|
|
# @proof_of_concept.setter
|
|
# def proof_of_concept(self, ...) -> None:
|
|
# ... # TODO since CDX 1.5
|
|
|
|
@property
|
|
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'advisory')
|
|
@serializable.xml_sequence(11)
|
|
def advisories(self) -> 'SortedSet[VulnerabilityAdvisory]':
|
|
"""
|
|
Advisories relating to the Vulnerability.
|
|
|
|
Returns:
|
|
Set of `VulnerabilityAdvisory`
|
|
"""
|
|
return self._advisories
|
|
|
|
@advisories.setter
|
|
def advisories(self, advisories: Iterable[VulnerabilityAdvisory]) -> None:
|
|
self._advisories = SortedSet(advisories)
|
|
|
|
@property
|
|
@serializable.type_mapping(serializable.helpers.XsdDateTime)
|
|
@serializable.xml_sequence(12)
|
|
def created(self) -> Optional[datetime]:
|
|
"""
|
|
The date and time (timestamp) when the vulnerability record was created in the vulnerability database.
|
|
|
|
Returns:
|
|
`datetime` if set else `None`
|
|
"""
|
|
return self._created
|
|
|
|
@created.setter
|
|
def created(self, created: Optional[datetime]) -> None:
|
|
self._created = created
|
|
|
|
@property
|
|
@serializable.type_mapping(serializable.helpers.XsdDateTime)
|
|
@serializable.xml_sequence(13)
|
|
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
|
|
def published(self, published: Optional[datetime]) -> None:
|
|
self._published = published
|
|
|
|
@property
|
|
@serializable.type_mapping(serializable.helpers.XsdDateTime)
|
|
@serializable.xml_sequence(14)
|
|
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
|
|
def updated(self, updated: Optional[datetime]) -> None:
|
|
self._updated = updated
|
|
|
|
# @property
|
|
# @serializable.view(SchemaVersion1Dot5)
|
|
# @serializable.xml_sequence(15)
|
|
# def rejected(self) -> ...:
|
|
# ... # TODO since CDX 1.5
|
|
#
|
|
# @rejected.setter
|
|
# def rejected(self, ...) -> None:
|
|
# ... # TODO since CDX 1.5
|
|
|
|
@property
|
|
@serializable.xml_sequence(16)
|
|
def credits(self) -> Optional[VulnerabilityCredits]:
|
|
"""
|
|
Individuals or organizations credited with the discovery of the vulnerability.
|
|
|
|
Returns:
|
|
`VulnerabilityCredits` if set else `None`
|
|
"""
|
|
return self._credits
|
|
|
|
@credits.setter
|
|
def credits(self, credits: Optional[VulnerabilityCredits]) -> None:
|
|
self._credits = credits
|
|
|
|
@property
|
|
@serializable.type_mapping(_ToolRepositoryHelper)
|
|
@serializable.xml_sequence(17)
|
|
def tools(self) -> ToolRepository:
|
|
"""
|
|
Tools used to create this BOM.
|
|
|
|
Returns:
|
|
:class:`ToolRepository` object.
|
|
"""
|
|
return self._tools
|
|
|
|
@tools.setter
|
|
def tools(self, tools: Union[Iterable[Tool], ToolRepository]) -> None:
|
|
self._tools = tools \
|
|
if isinstance(tools, ToolRepository) \
|
|
else ToolRepository(tools=tools)
|
|
|
|
@property
|
|
@serializable.xml_sequence(18)
|
|
def analysis(self) -> Optional[VulnerabilityAnalysis]:
|
|
"""
|
|
Analysis of the Vulnerability in your context.
|
|
|
|
Returns:
|
|
`VulnerabilityAnalysis` if set else `None`
|
|
"""
|
|
return self._analysis
|
|
|
|
@analysis.setter
|
|
def analysis(self, analysis: Optional[VulnerabilityAnalysis]) -> None:
|
|
self._analysis = analysis
|
|
|
|
@property
|
|
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'target')
|
|
@serializable.xml_sequence(19)
|
|
def affects(self) -> 'SortedSet[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: Iterable[BomTarget]) -> None:
|
|
self._affects = SortedSet(affects_targets)
|
|
|
|
@property
|
|
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property')
|
|
@serializable.xml_sequence(20)
|
|
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)
|
|
|
|
def __comparable_tuple(self) -> _ComparableTuple:
|
|
return _ComparableTuple((
|
|
self.id, self.bom_ref.value,
|
|
self.source, _ComparableTuple(self.references),
|
|
_ComparableTuple(self.ratings), _ComparableTuple(self.cwes), self.description,
|
|
self.detail, self.recommendation, self.workaround, _ComparableTuple(self.advisories),
|
|
self.created, self.published, self.updated,
|
|
self.credits, self.tools, self.analysis,
|
|
_ComparableTuple(self.affects),
|
|
_ComparableTuple(self.properties)
|
|
))
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, Vulnerability):
|
|
return self.__comparable_tuple() == other.__comparable_tuple()
|
|
return False
|
|
|
|
def __lt__(self, other: Any) -> bool:
|
|
if isinstance(other, Vulnerability):
|
|
return self.__comparable_tuple() < other.__comparable_tuple()
|
|
return NotImplemented
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(self.__comparable_tuple())
|
|
|
|
def __repr__(self) -> str:
|
|
return f'<Vulnerability bom-ref={self.bom_ref.value}, id={self.id}>'
|