From 7d21ce9e4a467c739ca1472f7ee26708c4574a83 Mon Sep 17 00:00:00 2001 From: Jacob Wahlman Date: Thu, 1 Aug 2024 21:54:26 +0200 Subject: [PATCH 1/6] Added support for identity in ComponentEvidence Signed-off-by: Jacob Wahlman --- cyclonedx/exception/model.py | 7 + cyclonedx/model/component.py | 29 ++-- cyclonedx/model/evidence.py | 302 +++++++++++++++++++++++++++++++++++ 3 files changed, 328 insertions(+), 10 deletions(-) create mode 100644 cyclonedx/model/evidence.py diff --git a/cyclonedx/exception/model.py b/cyclonedx/exception/model.py index dd476671..776ee8bd 100644 --- a/cyclonedx/exception/model.py +++ b/cyclonedx/exception/model.py @@ -54,6 +54,13 @@ class InvalidNistQuantumSecurityLevelException(CycloneDxModelException): pass +class InvalidEvidenceConfidenceScore(CycloneDxModelException): + """ + Raised when an invalid value is provided for a confidence score for Component Evidence. + """ + pass + + class InvalidOmniBorIdException(CycloneDxModelException): """ Raised when a supplied value for an OmniBOR ID does not meet the format requirements diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index 5287030b..0bdfc695 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -59,6 +59,7 @@ from .contact import OrganizationalContact, OrganizationalEntity from .crypto import CryptoProperties from .dependency import Dependable +from .evidence import EvidenceIdentity from .issue import IssueType from .license import License, LicenseRepository from .release_note import ReleaseNotes @@ -204,6 +205,7 @@ class ComponentEvidence: def __init__( self, *, + identity: Optional[EvidenceIdentity] = None, licenses: Optional[Iterable[License]] = None, copyright: Optional[Iterable[Copyright]] = None, ) -> None: @@ -212,19 +214,26 @@ def __init__( 'At least one of `licenses` or `copyright` must be supplied for a `ComponentEvidence`.' ) + self.identity = identity 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(1) + def identity(self) -> Optional[EvidenceIdentity]: + """ + Optional list of evidence that substantiates the identity of a component. + + Returns: + `EvidenceIdentity` or `None` + """ + + return self._identity + + @identity.setter + def identity(self, identity: Optional[EvidenceIdentity]) -> None: + self._identity = identity # @property # ... diff --git a/cyclonedx/model/evidence.py b/cyclonedx/model/evidence.py new file mode 100644 index 00000000..b93da3ef --- /dev/null +++ b/cyclonedx/model/evidence.py @@ -0,0 +1,302 @@ +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +from enum import Enum +from typing import Any, Dict, FrozenSet, Iterable, Optional, Type + +import serializable + +from ..exception.model import InvalidEvidenceConfidenceScore +from ..schema.schema import SchemaVersion1Dot5, SchemaVersion1Dot6 + + +@serializable.serializable_enum +class EvidenceIdentityField(str, Enum): + """ + Enum object that defines the permissable 'field' for a EvidenceIdentity according to the CycloneDX schema. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.5/xml/#type_identityFieldType + """ + # see `_EvidenceIdentityFieldSerializationHelper.__CASES` for view/case map + GROUP = 'group' + NAME = 'name' + VERSION = 'version' + PURL = 'purl' + CPE = 'cpe' + SWID = 'swid' + HASH = 'hash' + + +class _EvidenceIdentityFieldSerializationHelper(serializable.helpers.BaseHelper): + """ THIS CLASS IS NON-PUBLIC API """ + + __CASES: Dict[Type[serializable.ViewType], FrozenSet[EvidenceIdentityField]] = dict() + __CASES[SchemaVersion1Dot5] = frozenset({ + EvidenceIdentityField.GROUP, + EvidenceIdentityField.NAME, + EvidenceIdentityField.VERSION, + EvidenceIdentityField.PURL, + EvidenceIdentityField.CPE, + EvidenceIdentityField.SWID, + EvidenceIdentityField.HASH, + }) + __CASES[SchemaVersion1Dot6] = __CASES[SchemaVersion1Dot5] + + @classmethod + def __normalize(cls, cs: EvidenceIdentityField, 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) -> EvidenceIdentityField: + return EvidenceIdentityField(o) + + +@serializable.serializable_enum +class EvidenceTechnique(str, Enum): + """ + Enum object that defines the permissable 'technique' for a EvidenceMethod according to the CycloneDX schema. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.5/xml/#type_componentEvidenceType + """ + # see `_EvidenceTechniqueSerializationHelper.__CASES` for view/case map + SOURCE_CODE_ANALYSIS = 'source-code-analysis' + BINARY_ANALYSIS = 'binary-analysis' + MANIFEST_ANALYSIS = 'manifest-analysis' + AST_FINGERPRINT = 'ast-fingerprint' + HASH_COMPARISON = 'hash-comparison' + INSTRUMENTATION = 'instrumentation' + DYNAMIC_ANALISYS = 'dynamic-analysis' + FILENAME = 'filename' + ATTESTATION = 'attestation' + OTHER = 'other' + + +class _EvidenceTechniqueSerializationHelper(serializable.helpers.BaseHelper): + """ THIS CLASS IS NON-PUBLIC API """ + + __CASES: Dict[Type[serializable.ViewType], FrozenSet[EvidenceTechnique]] = dict() + __CASES[SchemaVersion1Dot5] = frozenset({ + EvidenceTechnique.SOURCE_CODE_ANALYSIS, + EvidenceTechnique.BINARY_ANALYSIS, + EvidenceTechnique.MANIFEST_ANALYSIS, + EvidenceTechnique.AST_FINGERPRINT, + EvidenceTechnique.HASH_COMPARISON, + EvidenceTechnique.INSTRUMENTATION, + EvidenceTechnique.DYNAMIC_ANALISYS, + EvidenceTechnique.FILENAME, + EvidenceTechnique.ATTESTATION, + EvidenceTechnique.OTHER, + }) + __CASES[SchemaVersion1Dot6] = __CASES[SchemaVersion1Dot5] + + @classmethod + def __normalize(cls, cs: EvidenceTechnique, 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) -> EvidenceTechnique: + return EvidenceTechnique(o) + + +@serializable.serializable_class +class EvidenceMethod: + """ + Our internal representation of the Method in `componentEvidenceType` complex type. + + Provides the ability to document method for how evidence was collected through + various forms of extraction or analysis. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_componentEvidenceType + """ + + def __init__(self, *, technique: EvidenceTechnique, confidence: int, value: Optional[str] = None) -> None: + self.technique = technique + self.confidence = confidence + self.value = value + + @property + @serializable.type_mapping(_EvidenceTechniqueSerializationHelper) + def technique(self) -> Optional[EvidenceTechnique]: + """ + The evidence technique of the component which the evidence describes. + + Returns: + `EvidenceTechnique` or `None` + """ + return self._technique + + @technique.setter + def technique(self, technique: Optional[EvidenceTechnique]) -> None: + self._technique = technique + + @property + def confidence(self) -> int: + """ + The overall confidence of the evidence from 0 - 1, where 1 is 100% confidence. + + Confidence is specific to the technique used. Each technique of analysis can have independent confidence. + + Returns: + `int` + """ + return self._confidence + + @confidence.setter + def confidence(self, confidence: int) -> None: + if confidence < 0 or confidence > 1: + raise InvalidEvidenceConfidenceScore( + 'Evidence confidence score must be (0 <= value <= 1)' + ) + + self._confidence = confidence + + @property + def value(self) -> Optional[str]: + """ + The value or contents of the evidence. + + Returns: + `str` or `None` + """ + return self._value + + @value.setter + def value(self, value: Optional[str]) -> None: + self._value = value + + +@serializable.serializable_class +class EvidenceIdentity: + """ + Our internal representation of the Identity in `componentEvidenceType` complex type. + + Provides the ability to document component identity as part of the evidence collected + through various forms of extraction or analysis. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_componentEvidenceType + """ + + def __init__( + self, *, + field: EvidenceIdentityField, + confidence: Optional[int] = None, + methods: Optional[Iterable[EvidenceMethod]] = None, + tools: Optional[Iterable[str]] + ) -> None: + self.field = field + self.confidence = confidence or None + self.methods = methods or None + self.tools = tools or None + + @property + @serializable.type_mapping(_EvidenceIdentityFieldSerializationHelper) + def field(self) -> Optional[EvidenceIdentityField]: + """ + The identity field of the component which the evidence describes. + + Returns: + `EvidenceIdentityField` or `None` + """ + return self._field + + @field.setter + def field(self, field: Optional[EvidenceIdentityField]) -> None: + self._field = field + + @property + def confidence(self) -> Optional[int]: + """ + The overall confidence of the evidence from 0 - 1, where 1 is 100% confidence. + + Returns: + `int` or `None` + """ + return self._confidence + + @confidence.setter + def confidence(self, confidence: Optional[int]) -> None: + if confidence is not None and (confidence < 0 or confidence > 1): + raise InvalidEvidenceConfidenceScore( + 'Evidence confidence score must be (0 <= value <= 1)' + ) + + self._confidence = confidence + + @property + @serializable.type_mapping(EvidenceMethod) + def methods(self) -> Optional[Iterable[EvidenceMethod]]: + """ + Optional list of methods used to extract and/or analyze the evidence. + + Returns: + `Iterable[EvidenceMethod]` or `None` + """ + return self._methods + + @methods.setter + def methods(self, methods: Optional[Iterable[EvidenceMethod]]) -> None: + self._methods = methods + + @property + def tools(self) -> Optional[Iterable[str]]: + """ + Optional list of tools used to extract and/or analyze the evidence. + + Returns: + `Iterable[str]` or `None` + """ + return self._tools + + @tools.setter + def tools(self, tools: Optional[Iterable[str]]) -> None: + self._tools = tools From 1942d3c02b0184cc6ccf878331b9212f7e202a01 Mon Sep 17 00:00:00 2001 From: Jacob Wahlman Date: Sat, 3 Aug 2024 17:10:07 +0200 Subject: [PATCH 2/6] Implemented equality check and SortedSet on iterables Signed-off-by: Jacob Wahlman --- cyclonedx/model/component.py | 5 +- cyclonedx/model/evidence.py | 80 ++++++++++++++++++++++-------- tests/_data/models.py | 10 ++++ tests/test_model_component.py | 91 +++++++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 21 deletions(-) diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index 0bdfc695..657f25d9 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -209,7 +209,7 @@ def __init__( licenses: Optional[Iterable[License]] = None, copyright: Optional[Iterable[Copyright]] = None, ) -> None: - if not licenses and not copyright: + if not licenses and not copyright and not identity: raise NoPropertiesProvidedException( 'At least one of `licenses` or `copyright` must be supplied for a `ComponentEvidence`.' ) @@ -220,6 +220,7 @@ def __init__( @property @serializable.view(SchemaVersion1Dot5) + @serializable.view(SchemaVersion1Dot6) @serializable.xml_sequence(1) def identity(self) -> Optional[EvidenceIdentity]: """ @@ -295,7 +296,7 @@ def __eq__(self, other: object) -> bool: return False def __hash__(self) -> int: - return hash((tuple(self.licenses), tuple(self.copyright))) + return hash((self.identity, tuple(self.licenses), tuple(self.copyright))) def __repr__(self) -> str: return f'' diff --git a/cyclonedx/model/evidence.py b/cyclonedx/model/evidence.py index b93da3ef..7529daab 100644 --- a/cyclonedx/model/evidence.py +++ b/cyclonedx/model/evidence.py @@ -19,7 +19,9 @@ from typing import Any, Dict, FrozenSet, Iterable, Optional, Type import serializable +from sortedcontainers import SortedSet +from .._internal.compare import ComparableTuple as _ComparableTuple from ..exception.model import InvalidEvidenceConfidenceScore from ..schema.schema import SchemaVersion1Dot5, SchemaVersion1Dot6 @@ -158,7 +160,7 @@ class EvidenceMethod: See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_componentEvidenceType """ - def __init__(self, *, technique: EvidenceTechnique, confidence: int, value: Optional[str] = None) -> None: + def __init__(self, *, technique: EvidenceTechnique, confidence: float, value: Optional[str] = None) -> None: self.technique = technique self.confidence = confidence self.value = value @@ -179,19 +181,19 @@ def technique(self, technique: Optional[EvidenceTechnique]) -> None: self._technique = technique @property - def confidence(self) -> int: + def confidence(self) -> float: """ The overall confidence of the evidence from 0 - 1, where 1 is 100% confidence. Confidence is specific to the technique used. Each technique of analysis can have independent confidence. Returns: - `int` + `float` """ return self._confidence @confidence.setter - def confidence(self, confidence: int) -> None: + def confidence(self, confidence: float) -> None: if confidence < 0 or confidence > 1: raise InvalidEvidenceConfidenceScore( 'Evidence confidence score must be (0 <= value <= 1)' @@ -213,6 +215,26 @@ def value(self) -> Optional[str]: def value(self, value: Optional[str]) -> None: self._value = value + def __eq__(self, other: object) -> bool: + if isinstance(other, EvidenceMethod): + return hash(other) == hash(self) + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, EvidenceMethod): + return _ComparableTuple(( + self.technique, self.confidence, self.value + )) < _ComparableTuple(( + other.technique, other.confidence, other.value + )) + return NotImplemented + + def __hash__(self) -> int: + return hash((self.technique, self.confidence, self.value)) + + def __repr__(self) -> str: + return f'' + @serializable.serializable_class class EvidenceIdentity: @@ -229,14 +251,14 @@ class EvidenceIdentity: def __init__( self, *, field: EvidenceIdentityField, - confidence: Optional[int] = None, + confidence: Optional[float] = None, methods: Optional[Iterable[EvidenceMethod]] = None, - tools: Optional[Iterable[str]] + tools: Optional[Iterable[str]] = None, ) -> None: self.field = field self.confidence = confidence or None - self.methods = methods or None - self.tools = tools or None + self.methods = methods or [] # type:ignore[assignment] + self.tools = tools or [] # type:ignore[assignment] @property @serializable.type_mapping(_EvidenceIdentityFieldSerializationHelper) @@ -254,17 +276,17 @@ def field(self, field: Optional[EvidenceIdentityField]) -> None: self._field = field @property - def confidence(self) -> Optional[int]: + def confidence(self) -> Optional[float]: """ The overall confidence of the evidence from 0 - 1, where 1 is 100% confidence. Returns: - `int` or `None` + `float` or `None` """ return self._confidence @confidence.setter - def confidence(self, confidence: Optional[int]) -> None: + def confidence(self, confidence: Optional[float]) -> None: if confidence is not None and (confidence < 0 or confidence > 1): raise InvalidEvidenceConfidenceScore( 'Evidence confidence score must be (0 <= value <= 1)' @@ -274,29 +296,49 @@ def confidence(self, confidence: Optional[int]) -> None: @property @serializable.type_mapping(EvidenceMethod) - def methods(self) -> Optional[Iterable[EvidenceMethod]]: + def methods(self) -> 'SortedSet[EvidenceMethod]': """ Optional list of methods used to extract and/or analyze the evidence. Returns: - `Iterable[EvidenceMethod]` or `None` + `SortedSet[EvidenceMethod]` """ return self._methods @methods.setter - def methods(self, methods: Optional[Iterable[EvidenceMethod]]) -> None: - self._methods = methods + def methods(self, methods: Iterable[EvidenceMethod]) -> None: + self._methods = SortedSet(methods) @property - def tools(self) -> Optional[Iterable[str]]: + def tools(self) -> 'SortedSet[str]': """ Optional list of tools used to extract and/or analyze the evidence. Returns: - `Iterable[str]` or `None` + `SortedSet[str]` """ return self._tools @tools.setter - def tools(self, tools: Optional[Iterable[str]]) -> None: - self._tools = tools + def tools(self, tools: Iterable[str]) -> None: + self._tools = SortedSet(tools) + + def __eq__(self, other: object) -> bool: + if isinstance(other, EvidenceIdentity): + return hash(other) == hash(self) + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, EvidenceIdentity): + return _ComparableTuple(( + self.field, self.confidence, self.methods, self.tools + )) < _ComparableTuple(( + other.field, other.confidence, other.methods, other.tools + )) + return NotImplemented + + def __hash__(self) -> int: + return hash((self.field, self.confidence, tuple(self.methods), tuple(self.tools))) + + def __repr__(self) -> str: + return f'' diff --git a/tests/_data/models.py b/tests/_data/models.py index 150739a0..4aa48919 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -79,6 +79,7 @@ RelatedCryptoMaterialType, ) from cyclonedx.model.dependency import Dependency +from cyclonedx.model.evidence import EvidenceIdentity, EvidenceIdentityField, EvidenceMethod, EvidenceTechnique from cyclonedx.model.impact_analysis import ( ImpactAnalysisAffectedStatus, ImpactAnalysisJustification, @@ -930,6 +931,15 @@ def get_swid_2() -> Swid: ) +def get_evidence_identity() -> EvidenceIdentity: + return EvidenceIdentity( + field=EvidenceIdentityField.NAME, confidence=0.5, methods=[ + EvidenceMethod(technique=EvidenceTechnique.FILENAME, confidence=0.5) + ], + tools=['cyclonedx-python-lib'] + ) + + def get_vulnerability_source_nvd() -> VulnerabilitySource: return VulnerabilitySource(name='NVD', url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2018-7489')) diff --git a/tests/test_model_component.py b/tests/test_model_component.py index 8ad7014c..0eacbb04 100644 --- a/tests/test_model_component.py +++ b/tests/test_model_component.py @@ -40,6 +40,7 @@ PatchClassification, Pedigree, ) +from cyclonedx.model.evidence import EvidenceIdentity, EvidenceIdentityField, EvidenceMethod, EvidenceTechnique from cyclonedx.model.issue import IssueClassification, IssueType from tests import reorder from tests._data.models import ( @@ -302,12 +303,102 @@ def test_same_2(self) -> None: self.assertEqual(hash(ce_1), hash(ce_2)) self.assertTrue(ce_1 == ce_2) + def test_same_3(self) -> None: + ce_1 = ComponentEvidence( + identity=EvidenceIdentity( + field=EvidenceIdentityField.NAME, + confidence=0.5, + methods=[EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5)] + ) + ) + ce_2 = ComponentEvidence( + identity=EvidenceIdentity( + field=EvidenceIdentityField.NAME, + confidence=0.5, + methods=[EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5)] + ) + ) + self.assertEqual(hash(ce_1), hash(ce_2)) + self.assertTrue(ce_1 == ce_2) + + def test_same_4(self) -> None: + ce_1 = ComponentEvidence( + identity=EvidenceIdentity( + field=EvidenceIdentityField.NAME, + confidence=0.5, + methods=[ + EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5), + EvidenceMethod(technique=EvidenceTechnique.FILENAME, confidence=0.2) + ] + ) + ) + ce_2 = ComponentEvidence( + identity=EvidenceIdentity( + field=EvidenceIdentityField.NAME, + confidence=0.5, + methods=[ + EvidenceMethod(technique=EvidenceTechnique.FILENAME, confidence=0.2), + EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5) + ] + ) + ) + self.assertEqual(hash(ce_1), hash(ce_2)) + self.assertTrue(ce_1 == ce_2) + def test_not_same_1(self) -> None: ce_1 = ComponentEvidence(copyright=[Copyright(text='Commercial')]) ce_2 = ComponentEvidence(copyright=[Copyright(text='Commercial 2')]) self.assertNotEqual(hash(ce_1), hash(ce_2)) self.assertFalse(ce_1 == ce_2) + def test_not_same_2(self) -> None: + ce_1 = ComponentEvidence( + identity=EvidenceIdentity( + field=EvidenceIdentityField.NAME, + confidence=0.5, + methods=[ + EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5), + EvidenceMethod(technique=EvidenceTechnique.BINARY_ANALYSIS, confidence=0.7) + ] + ) + ) + ce_2 = ComponentEvidence( + identity=EvidenceIdentity( + field=EvidenceIdentityField.NAME, + confidence=0.5, + methods=[ + EvidenceMethod(technique=EvidenceTechnique.BINARY_ANALYSIS, confidence=0.5), + EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5) + ] + ) + ) + self.assertNotEqual(hash(ce_1), hash(ce_2)) + + def test_not_same_3(self) -> None: + ce_1 = ComponentEvidence( + identity=EvidenceIdentity( + field=EvidenceIdentityField.NAME, + confidence=0.5, + methods=[ + EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5), + EvidenceMethod(technique=EvidenceTechnique.BINARY_ANALYSIS, confidence=0.7) + ] + ) + ) + ce_2 = ComponentEvidence( + identity=EvidenceIdentity( + field=EvidenceIdentityField.HASH, + confidence=0.5, + methods=[ + EvidenceMethod(technique=EvidenceTechnique.BINARY_ANALYSIS, confidence=0.7), + EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5) + ] + ) + ) + self.assertNotEqual(hash(ce_1), hash(ce_2)) + self.assertFalse(ce_1 == ce_2) + self.assertFalse(ce_1 == ce_2) + class TestModelDiff(TestCase): From be8a55f87f802cf267547c2f92fed14c3f2c9769 Mon Sep 17 00:00:00 2001 From: Jacob Wahlman Date: Sat, 3 Aug 2024 18:56:14 +0200 Subject: [PATCH 3/6] Refactored to match the expected schema type Signed-off-by: Jacob Wahlman --- cyclonedx/exception/model.py | 9 +- cyclonedx/model/component.py | 363 +++++++++++++++++++++++++++++++++- cyclonedx/model/evidence.py | 344 -------------------------------- tests/_data/models.py | 13 +- tests/test_model_component.py | 111 ++++++++--- 5 files changed, 453 insertions(+), 387 deletions(-) delete mode 100644 cyclonedx/model/evidence.py diff --git a/cyclonedx/exception/model.py b/cyclonedx/exception/model.py index 776ee8bd..09e0656d 100644 --- a/cyclonedx/exception/model.py +++ b/cyclonedx/exception/model.py @@ -54,13 +54,20 @@ class InvalidNistQuantumSecurityLevelException(CycloneDxModelException): pass -class InvalidEvidenceConfidenceScore(CycloneDxModelException): +class InvalidComponentIdentityEvidenceConfidenceScore(CycloneDxModelException): """ Raised when an invalid value is provided for a confidence score for Component Evidence. """ pass +class InvalidComponentIdentityEvidenceMethodConfidenceScore(CycloneDxModelException): + """ + Raised when an invalid value is provided for a confidence score for Component Evidence Method. + """ + pass + + class InvalidOmniBorIdException(CycloneDxModelException): """ Raised when a supplied value for an OmniBOR ID does not meet the format requirements diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index 657f25d9..2990c390 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -28,7 +28,13 @@ 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, NoPropertiesProvidedException +from ..exception.model import ( + InvalidComponentIdentityEvidenceConfidenceScore, + InvalidComponentIdentityEvidenceMethodConfidenceScore, + InvalidOmniBorIdException, + InvalidSwhidException, + NoPropertiesProvidedException, +) from ..exception.serialization import ( CycloneDxDeserializationException, SerializationOfUnexpectedValueException, @@ -59,7 +65,6 @@ from .contact import OrganizationalContact, OrganizationalEntity from .crypto import CryptoProperties from .dependency import Dependable -from .evidence import EvidenceIdentity from .issue import IssueType from .license import License, LicenseRepository from .release_note import ReleaseNotes @@ -192,6 +197,343 @@ def __repr__(self) -> str: return f'' +@serializable.serializable_enum +class ComponentIdentityEvidenceField(str, Enum): + """ + Enum object that defines the permissable 'field' for a `ComponentIdentityEvidence` + according to the CycloneDX schema. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_identityFieldType + """ + # see `_ComponentIdentityEvidenceFieldSerializationHelper.__CASES` for view/case map + GROUP = 'group' + NAME = 'name' + VERSION = 'version' + PURL = 'purl' + CPE = 'cpe' + OMNIBOR_ID = 'omniborId' + SWHID = 'swhid' + SWID = 'swid' + HASH = 'hash' + + +class _ComponentIdentityEvidenceFieldSerializationHelper(serializable.helpers.BaseHelper): + """ THIS CLASS IS NON-PUBLIC API """ + + __CASES: Dict[Type[serializable.ViewType], FrozenSet[ComponentIdentityEvidenceField]] = dict() + __CASES[SchemaVersion1Dot5] = frozenset({ + ComponentIdentityEvidenceField.GROUP, + ComponentIdentityEvidenceField.NAME, + ComponentIdentityEvidenceField.VERSION, + ComponentIdentityEvidenceField.PURL, + ComponentIdentityEvidenceField.CPE, + ComponentIdentityEvidenceField.SWID, + ComponentIdentityEvidenceField.HASH, + }) + __CASES[SchemaVersion1Dot6] = __CASES[SchemaVersion1Dot5] | { + ComponentIdentityEvidenceField.OMNIBOR_ID, + ComponentIdentityEvidenceField.SWHID + } + + @classmethod + def __normalize(cls, cs: ComponentIdentityEvidenceField, 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) -> ComponentIdentityEvidenceField: + return ComponentIdentityEvidenceField(o) + + +@serializable.serializable_enum +class ComponentIdentityEvidenceMethodTechnique(str, Enum): + """ + Enum object that defines the permissable 'technique' for a `ComponentIdentityEvidenceMethod` + according to the CycloneDX schema. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_componentIdentityEvidenceType + """ + # see `_ComponentIdentityEvidenceMethodTechniqueSerializationHelper.__CASES` for view/case map + SOURCE_CODE_ANALYSIS = 'source-code-analysis' + BINARY_ANALYSIS = 'binary-analysis' + MANIFEST_ANALYSIS = 'manifest-analysis' + AST_FINGERPRINT = 'ast-fingerprint' + HASH_COMPARISON = 'hash-comparison' + INSTRUMENTATION = 'instrumentation' + DYNAMIC_ANALISYS = 'dynamic-analysis' + FILENAME = 'filename' + ATTESTATION = 'attestation' + OTHER = 'other' + + +class _ComponentIdentityEvidenceMethodTechniqueSerializationHelper(serializable.helpers.BaseHelper): + """ THIS CLASS IS NON-PUBLIC API """ + + __CASES: Dict[Type[serializable.ViewType], FrozenSet[ComponentIdentityEvidenceMethodTechnique]] = dict() + __CASES[SchemaVersion1Dot5] = frozenset({ + ComponentIdentityEvidenceMethodTechnique.SOURCE_CODE_ANALYSIS, + ComponentIdentityEvidenceMethodTechnique.BINARY_ANALYSIS, + ComponentIdentityEvidenceMethodTechnique.MANIFEST_ANALYSIS, + ComponentIdentityEvidenceMethodTechnique.AST_FINGERPRINT, + ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + ComponentIdentityEvidenceMethodTechnique.INSTRUMENTATION, + ComponentIdentityEvidenceMethodTechnique.DYNAMIC_ANALISYS, + ComponentIdentityEvidenceMethodTechnique.FILENAME, + ComponentIdentityEvidenceMethodTechnique.ATTESTATION, + ComponentIdentityEvidenceMethodTechnique.OTHER, + }) + __CASES[SchemaVersion1Dot6] = __CASES[SchemaVersion1Dot5] + + @classmethod + def __normalize( + cls, + cs: ComponentIdentityEvidenceMethodTechnique, + 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) -> ComponentIdentityEvidenceMethodTechnique: + return ComponentIdentityEvidenceMethodTechnique(o) + + +@serializable.serializable_class +class ComponentIdentityEvidenceMethod: + """ + Our internal representation of the Method in `componentIdentityEvidenceType` complex type. + + Provides the ability to document method for how evidence was collected through + various forms of extraction or analysis. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_componentIdentityEvidenceType + """ + + def __init__( + self, *, + technique: ComponentIdentityEvidenceMethodTechnique, + confidence: float, + value: Optional[str] = None + ) -> None: + self.technique = technique + self.confidence = confidence + self.value = value + + @property + @serializable.type_mapping(_ComponentIdentityEvidenceMethodTechniqueSerializationHelper) + def technique(self) -> Optional[ComponentIdentityEvidenceMethodTechnique]: + """ + The evidence technique used by the method of the component which the evidence describes. + + Returns: + `ComponentIdentityEvidenceMethodTechnique` or `None` + """ + return self._technique + + @technique.setter + def technique(self, technique: Optional[ComponentIdentityEvidenceMethodTechnique]) -> None: + self._technique = technique + + @property + def confidence(self) -> float: + """ + The overall confidence of the evidence from 0 - 1, where 1 is 100% confidence. + + Confidence is specific to the technique used. Each technique of analysis can have independent confidence. + + Returns: + `float` + """ + return self._confidence + + @confidence.setter + def confidence(self, confidence: float) -> None: + if confidence < 0 or confidence > 1: + raise InvalidComponentIdentityEvidenceMethodConfidenceScore( + 'Evidence method confidence score must be (0 <= value <= 1)' + ) + + self._confidence = confidence + + @property + def value(self) -> Optional[str]: + """ + The value or contents of the evidence. + + Returns: + `str` or `None` + """ + return self._value + + @value.setter + def value(self, value: Optional[str]) -> None: + self._value = value + + def __eq__(self, other: object) -> bool: + if isinstance(other, ComponentIdentityEvidenceMethod): + return hash(other) == hash(self) + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, ComponentIdentityEvidenceMethod): + return _ComparableTuple(( + self.technique, self.confidence, self.value + )) < _ComparableTuple(( + other.technique, other.confidence, other.value + )) + return NotImplemented + + def __hash__(self) -> int: + return hash((self.technique, self.confidence, self.value)) + + def __repr__(self) -> str: + return ( + f'' + ) + + +@serializable.serializable_class +class ComponentIdentityEvidence: + """ + Our internal representation of the `componentIdentityEvidence` complex type. + + Provides the ability to document component identity as part of the evidence collected + through various forms of extraction or analysis. + + .. note:: + See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.6/xml/#type_componentIdentityEvidenceType + """ + + def __init__( + self, *, + field: ComponentIdentityEvidenceField, + confidence: Optional[float] = None, + methods: Optional[Iterable[ComponentIdentityEvidenceMethod]] = None, + tools: Optional[Iterable[str]] = None, + ) -> None: + self.field = field + self.confidence = confidence or None + self.methods = methods or [] # type:ignore[assignment] + self.tools = tools or [] # type:ignore[assignment] + + @property + @serializable.type_mapping(_ComponentIdentityEvidenceFieldSerializationHelper) + def field(self) -> Optional[ComponentIdentityEvidenceField]: + """ + The identity field of the component which the evidence describes. + + Returns: + `ComponentIdentityEvidenceField` or `None` + """ + return self._field + + @field.setter + def field(self, field: Optional[ComponentIdentityEvidenceField]) -> None: + self._field = field + + @property + def confidence(self) -> Optional[float]: + """ + The overall confidence of the evidence from 0 - 1, where 1 is 100% confidence. + + Returns: + `float` or `None` + """ + return self._confidence + + @confidence.setter + def confidence(self, confidence: Optional[float]) -> None: + if confidence is not None and (confidence < 0 or confidence > 1): + raise InvalidComponentIdentityEvidenceConfidenceScore( + 'Evidence confidence score must be (0 <= value <= 1)' + ) + + self._confidence = confidence + + @property + @serializable.type_mapping(ComponentIdentityEvidenceMethod) + def methods(self) -> 'SortedSet[ComponentIdentityEvidenceMethod]': + """ + Optional list of methods used to extract and/or analyze the evidence. + + Returns: + `SortedSet[ComponentIdentityEvidenceMethod]` + """ + return self._methods + + @methods.setter + def methods(self, methods: Iterable[ComponentIdentityEvidenceMethod]) -> None: + self._methods = SortedSet(methods) + + @property + def tools(self) -> 'SortedSet[str]': + """ + Optional list of tools used to extract and/or analyze the evidence. + + Returns: + `SortedSet[str]` + """ + return self._tools + + @tools.setter + def tools(self, tools: Iterable[str]) -> None: + self._tools = SortedSet(tools) + + def __eq__(self, other: object) -> bool: + if isinstance(other, ComponentIdentityEvidence): + return hash(other) == hash(self) + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, ComponentIdentityEvidence): + return _ComparableTuple(( + self.field, self.confidence, self.methods, self.tools + )) < _ComparableTuple(( + other.field, other.confidence, other.methods, other.tools + )) + return NotImplemented + + def __hash__(self) -> int: + return hash((self.field, self.confidence, tuple(self.methods), tuple(self.tools))) + + def __repr__(self) -> str: + return f'' + + @serializable.serializable_class class ComponentEvidence: """ @@ -205,7 +547,7 @@ class ComponentEvidence: def __init__( self, *, - identity: Optional[EvidenceIdentity] = None, + identity: Optional[ComponentIdentityEvidence] = None, licenses: Optional[Iterable[License]] = None, copyright: Optional[Iterable[Copyright]] = None, ) -> None: @@ -222,18 +564,18 @@ def __init__( @serializable.view(SchemaVersion1Dot5) @serializable.view(SchemaVersion1Dot6) @serializable.xml_sequence(1) - def identity(self) -> Optional[EvidenceIdentity]: + def identity(self) -> Optional[ComponentIdentityEvidence]: """ Optional list of evidence that substantiates the identity of a component. Returns: - `EvidenceIdentity` or `None` + `ComponentIdentityEvidence` or `None` """ return self._identity @identity.setter - def identity(self, identity: Optional[EvidenceIdentity]) -> None: + def identity(self, identity: Optional[ComponentIdentityEvidence]) -> None: self._identity = identity # @property @@ -295,6 +637,15 @@ def __eq__(self, other: object) -> bool: return hash(other) == hash(self) return False + def __lt__(self, other: Any) -> bool: + if isinstance(other, ComponentEvidence): + return _ComparableTuple(( + self.identity, self.licenses, self.copyright + )) < _ComparableTuple(( + other.identity, other.licenses, other.copyright + )) + return NotImplemented + def __hash__(self) -> int: return hash((self.identity, tuple(self.licenses), tuple(self.copyright))) diff --git a/cyclonedx/model/evidence.py b/cyclonedx/model/evidence.py deleted file mode 100644 index 7529daab..00000000 --- a/cyclonedx/model/evidence.py +++ /dev/null @@ -1,344 +0,0 @@ -# This file is part of CycloneDX Python Lib -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) OWASP Foundation. All Rights Reserved. - -from enum import Enum -from typing import Any, Dict, FrozenSet, Iterable, Optional, Type - -import serializable -from sortedcontainers import SortedSet - -from .._internal.compare import ComparableTuple as _ComparableTuple -from ..exception.model import InvalidEvidenceConfidenceScore -from ..schema.schema import SchemaVersion1Dot5, SchemaVersion1Dot6 - - -@serializable.serializable_enum -class EvidenceIdentityField(str, Enum): - """ - Enum object that defines the permissable 'field' for a EvidenceIdentity according to the CycloneDX schema. - - .. note:: - See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.5/xml/#type_identityFieldType - """ - # see `_EvidenceIdentityFieldSerializationHelper.__CASES` for view/case map - GROUP = 'group' - NAME = 'name' - VERSION = 'version' - PURL = 'purl' - CPE = 'cpe' - SWID = 'swid' - HASH = 'hash' - - -class _EvidenceIdentityFieldSerializationHelper(serializable.helpers.BaseHelper): - """ THIS CLASS IS NON-PUBLIC API """ - - __CASES: Dict[Type[serializable.ViewType], FrozenSet[EvidenceIdentityField]] = dict() - __CASES[SchemaVersion1Dot5] = frozenset({ - EvidenceIdentityField.GROUP, - EvidenceIdentityField.NAME, - EvidenceIdentityField.VERSION, - EvidenceIdentityField.PURL, - EvidenceIdentityField.CPE, - EvidenceIdentityField.SWID, - EvidenceIdentityField.HASH, - }) - __CASES[SchemaVersion1Dot6] = __CASES[SchemaVersion1Dot5] - - @classmethod - def __normalize(cls, cs: EvidenceIdentityField, 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) -> EvidenceIdentityField: - return EvidenceIdentityField(o) - - -@serializable.serializable_enum -class EvidenceTechnique(str, Enum): - """ - Enum object that defines the permissable 'technique' for a EvidenceMethod according to the CycloneDX schema. - - .. note:: - See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.5/xml/#type_componentEvidenceType - """ - # see `_EvidenceTechniqueSerializationHelper.__CASES` for view/case map - SOURCE_CODE_ANALYSIS = 'source-code-analysis' - BINARY_ANALYSIS = 'binary-analysis' - MANIFEST_ANALYSIS = 'manifest-analysis' - AST_FINGERPRINT = 'ast-fingerprint' - HASH_COMPARISON = 'hash-comparison' - INSTRUMENTATION = 'instrumentation' - DYNAMIC_ANALISYS = 'dynamic-analysis' - FILENAME = 'filename' - ATTESTATION = 'attestation' - OTHER = 'other' - - -class _EvidenceTechniqueSerializationHelper(serializable.helpers.BaseHelper): - """ THIS CLASS IS NON-PUBLIC API """ - - __CASES: Dict[Type[serializable.ViewType], FrozenSet[EvidenceTechnique]] = dict() - __CASES[SchemaVersion1Dot5] = frozenset({ - EvidenceTechnique.SOURCE_CODE_ANALYSIS, - EvidenceTechnique.BINARY_ANALYSIS, - EvidenceTechnique.MANIFEST_ANALYSIS, - EvidenceTechnique.AST_FINGERPRINT, - EvidenceTechnique.HASH_COMPARISON, - EvidenceTechnique.INSTRUMENTATION, - EvidenceTechnique.DYNAMIC_ANALISYS, - EvidenceTechnique.FILENAME, - EvidenceTechnique.ATTESTATION, - EvidenceTechnique.OTHER, - }) - __CASES[SchemaVersion1Dot6] = __CASES[SchemaVersion1Dot5] - - @classmethod - def __normalize(cls, cs: EvidenceTechnique, 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) -> EvidenceTechnique: - return EvidenceTechnique(o) - - -@serializable.serializable_class -class EvidenceMethod: - """ - Our internal representation of the Method in `componentEvidenceType` complex type. - - Provides the ability to document method for how evidence was collected through - various forms of extraction or analysis. - - .. note:: - See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_componentEvidenceType - """ - - def __init__(self, *, technique: EvidenceTechnique, confidence: float, value: Optional[str] = None) -> None: - self.technique = technique - self.confidence = confidence - self.value = value - - @property - @serializable.type_mapping(_EvidenceTechniqueSerializationHelper) - def technique(self) -> Optional[EvidenceTechnique]: - """ - The evidence technique of the component which the evidence describes. - - Returns: - `EvidenceTechnique` or `None` - """ - return self._technique - - @technique.setter - def technique(self, technique: Optional[EvidenceTechnique]) -> None: - self._technique = technique - - @property - def confidence(self) -> float: - """ - The overall confidence of the evidence from 0 - 1, where 1 is 100% confidence. - - Confidence is specific to the technique used. Each technique of analysis can have independent confidence. - - Returns: - `float` - """ - return self._confidence - - @confidence.setter - def confidence(self, confidence: float) -> None: - if confidence < 0 or confidence > 1: - raise InvalidEvidenceConfidenceScore( - 'Evidence confidence score must be (0 <= value <= 1)' - ) - - self._confidence = confidence - - @property - def value(self) -> Optional[str]: - """ - The value or contents of the evidence. - - Returns: - `str` or `None` - """ - return self._value - - @value.setter - def value(self, value: Optional[str]) -> None: - self._value = value - - def __eq__(self, other: object) -> bool: - if isinstance(other, EvidenceMethod): - return hash(other) == hash(self) - return False - - def __lt__(self, other: Any) -> bool: - if isinstance(other, EvidenceMethod): - return _ComparableTuple(( - self.technique, self.confidence, self.value - )) < _ComparableTuple(( - other.technique, other.confidence, other.value - )) - return NotImplemented - - def __hash__(self) -> int: - return hash((self.technique, self.confidence, self.value)) - - def __repr__(self) -> str: - return f'' - - -@serializable.serializable_class -class EvidenceIdentity: - """ - Our internal representation of the Identity in `componentEvidenceType` complex type. - - Provides the ability to document component identity as part of the evidence collected - through various forms of extraction or analysis. - - .. note:: - See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.4/xml/#type_componentEvidenceType - """ - - def __init__( - self, *, - field: EvidenceIdentityField, - confidence: Optional[float] = None, - methods: Optional[Iterable[EvidenceMethod]] = None, - tools: Optional[Iterable[str]] = None, - ) -> None: - self.field = field - self.confidence = confidence or None - self.methods = methods or [] # type:ignore[assignment] - self.tools = tools or [] # type:ignore[assignment] - - @property - @serializable.type_mapping(_EvidenceIdentityFieldSerializationHelper) - def field(self) -> Optional[EvidenceIdentityField]: - """ - The identity field of the component which the evidence describes. - - Returns: - `EvidenceIdentityField` or `None` - """ - return self._field - - @field.setter - def field(self, field: Optional[EvidenceIdentityField]) -> None: - self._field = field - - @property - def confidence(self) -> Optional[float]: - """ - The overall confidence of the evidence from 0 - 1, where 1 is 100% confidence. - - Returns: - `float` or `None` - """ - return self._confidence - - @confidence.setter - def confidence(self, confidence: Optional[float]) -> None: - if confidence is not None and (confidence < 0 or confidence > 1): - raise InvalidEvidenceConfidenceScore( - 'Evidence confidence score must be (0 <= value <= 1)' - ) - - self._confidence = confidence - - @property - @serializable.type_mapping(EvidenceMethod) - def methods(self) -> 'SortedSet[EvidenceMethod]': - """ - Optional list of methods used to extract and/or analyze the evidence. - - Returns: - `SortedSet[EvidenceMethod]` - """ - return self._methods - - @methods.setter - def methods(self, methods: Iterable[EvidenceMethod]) -> None: - self._methods = SortedSet(methods) - - @property - def tools(self) -> 'SortedSet[str]': - """ - Optional list of tools used to extract and/or analyze the evidence. - - Returns: - `SortedSet[str]` - """ - return self._tools - - @tools.setter - def tools(self, tools: Iterable[str]) -> None: - self._tools = SortedSet(tools) - - def __eq__(self, other: object) -> bool: - if isinstance(other, EvidenceIdentity): - return hash(other) == hash(self) - return False - - def __lt__(self, other: Any) -> bool: - if isinstance(other, EvidenceIdentity): - return _ComparableTuple(( - self.field, self.confidence, self.methods, self.tools - )) < _ComparableTuple(( - other.field, other.confidence, other.methods, other.tools - )) - return NotImplemented - - def __hash__(self) -> int: - return hash((self.field, self.confidence, tuple(self.methods), tuple(self.tools))) - - def __repr__(self) -> str: - return f'' diff --git a/tests/_data/models.py b/tests/_data/models.py index 4aa48919..d82d2090 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -47,6 +47,10 @@ Commit, Component, ComponentEvidence, + ComponentIdentityEvidence, + ComponentIdentityEvidenceField, + ComponentIdentityEvidenceMethod, + ComponentIdentityEvidenceMethodTechnique, ComponentScope, ComponentType, Diff, @@ -79,7 +83,6 @@ RelatedCryptoMaterialType, ) from cyclonedx.model.dependency import Dependency -from cyclonedx.model.evidence import EvidenceIdentity, EvidenceIdentityField, EvidenceMethod, EvidenceTechnique from cyclonedx.model.impact_analysis import ( ImpactAnalysisAffectedStatus, ImpactAnalysisJustification, @@ -931,10 +934,10 @@ def get_swid_2() -> Swid: ) -def get_evidence_identity() -> EvidenceIdentity: - return EvidenceIdentity( - field=EvidenceIdentityField.NAME, confidence=0.5, methods=[ - EvidenceMethod(technique=EvidenceTechnique.FILENAME, confidence=0.5) +def get_evidence_identity() -> ComponentIdentityEvidence: + return ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, confidence=0.5, methods=[ + ComponentIdentityEvidenceMethod(technique=ComponentIdentityEvidenceMethodTechnique.FILENAME, confidence=0.5) ], tools=['cyclonedx-python-lib'] ) diff --git a/tests/test_model_component.py b/tests/test_model_component.py index 0eacbb04..6a821262 100644 --- a/tests/test_model_component.py +++ b/tests/test_model_component.py @@ -34,13 +34,16 @@ Commit, Component, ComponentEvidence, + ComponentIdentityEvidence, + ComponentIdentityEvidenceField, + ComponentIdentityEvidenceMethod, + ComponentIdentityEvidenceMethodTechnique, ComponentType, Diff, Patch, PatchClassification, Pedigree, ) -from cyclonedx.model.evidence import EvidenceIdentity, EvidenceIdentityField, EvidenceMethod, EvidenceTechnique from cyclonedx.model.issue import IssueClassification, IssueType from tests import reorder from tests._data.models import ( @@ -305,17 +308,27 @@ def test_same_2(self) -> None: def test_same_3(self) -> None: ce_1 = ComponentEvidence( - identity=EvidenceIdentity( - field=EvidenceIdentityField.NAME, + identity=ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, confidence=0.5, - methods=[EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5)] + methods=[ + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ) + ] ) ) ce_2 = ComponentEvidence( - identity=EvidenceIdentity( - field=EvidenceIdentityField.NAME, + identity=ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, confidence=0.5, - methods=[EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5)] + methods=[ + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ) + ] ) ) self.assertEqual(hash(ce_1), hash(ce_2)) @@ -323,22 +336,34 @@ def test_same_3(self) -> None: def test_same_4(self) -> None: ce_1 = ComponentEvidence( - identity=EvidenceIdentity( - field=EvidenceIdentityField.NAME, + identity=ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, confidence=0.5, methods=[ - EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5), - EvidenceMethod(technique=EvidenceTechnique.FILENAME, confidence=0.2) + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ), + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.FILENAME, + confidence=0.2 + ) ] ) ) ce_2 = ComponentEvidence( - identity=EvidenceIdentity( - field=EvidenceIdentityField.NAME, + identity=ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, confidence=0.5, methods=[ - EvidenceMethod(technique=EvidenceTechnique.FILENAME, confidence=0.2), - EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5) + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.FILENAME, + confidence=0.2 + ), + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ) ] ) ) @@ -353,22 +378,34 @@ def test_not_same_1(self) -> None: def test_not_same_2(self) -> None: ce_1 = ComponentEvidence( - identity=EvidenceIdentity( - field=EvidenceIdentityField.NAME, + identity=ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, confidence=0.5, methods=[ - EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5), - EvidenceMethod(technique=EvidenceTechnique.BINARY_ANALYSIS, confidence=0.7) + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ), + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.BINARY_ANALYSIS, + confidence=0.7 + ) ] ) ) ce_2 = ComponentEvidence( - identity=EvidenceIdentity( - field=EvidenceIdentityField.NAME, + identity=ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, confidence=0.5, methods=[ - EvidenceMethod(technique=EvidenceTechnique.BINARY_ANALYSIS, confidence=0.5), - EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5) + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.BINARY_ANALYSIS, + confidence=0.5 + ), + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ) ] ) ) @@ -376,22 +413,34 @@ def test_not_same_2(self) -> None: def test_not_same_3(self) -> None: ce_1 = ComponentEvidence( - identity=EvidenceIdentity( - field=EvidenceIdentityField.NAME, + identity=ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, confidence=0.5, methods=[ - EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5), - EvidenceMethod(technique=EvidenceTechnique.BINARY_ANALYSIS, confidence=0.7) + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ), + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.BINARY_ANALYSIS, + confidence=0.7 + ) ] ) ) ce_2 = ComponentEvidence( - identity=EvidenceIdentity( - field=EvidenceIdentityField.HASH, + identity=ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.HASH, confidence=0.5, methods=[ - EvidenceMethod(technique=EvidenceTechnique.BINARY_ANALYSIS, confidence=0.7), - EvidenceMethod(technique=EvidenceTechnique.HASH_COMPARISON, confidence=0.5) + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.BINARY_ANALYSIS, + confidence=0.7 + ), + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ) ] ) ) From 22e1d8d9109892e618689462625395dfa443bb68 Mon Sep 17 00:00:00 2001 From: Jacob Wahlman Date: Sun, 4 Aug 2024 15:39:16 +0200 Subject: [PATCH 4/6] Added XML serializations decorators, enum test cases Signed-off-by: Jacob Wahlman --- cyclonedx/model/component.py | 37 +-- tests/_data/models.py | 13 - ...ComponentIdentityEvidenceField-1.0.xml.bin | 4 + ...ComponentIdentityEvidenceField-1.1.xml.bin | 4 + ...omponentIdentityEvidenceField-1.2.json.bin | 17 ++ ...ComponentIdentityEvidenceField-1.2.xml.bin | 13 + ...omponentIdentityEvidenceField-1.3.json.bin | 17 ++ ...ComponentIdentityEvidenceField-1.3.xml.bin | 13 + ...omponentIdentityEvidenceField-1.4.json.bin | 17 ++ ...ComponentIdentityEvidenceField-1.4.xml.bin | 14 ++ ...omponentIdentityEvidenceField-1.5.json.bin | 73 ++++++ ...ComponentIdentityEvidenceField-1.5.xml.bin | 53 +++++ ...omponentIdentityEvidenceField-1.6.json.bin | 99 ++++++++ ...ComponentIdentityEvidenceField-1.6.xml.bin | 53 +++++ tests/test_enums.py | 42 +++- tests/test_model_component.py | 224 ++++++++++-------- 16 files changed, 561 insertions(+), 132 deletions(-) create mode 100644 tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.0.xml.bin create mode 100644 tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.1.xml.bin create mode 100644 tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.json.bin create mode 100644 tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.xml.bin create mode 100644 tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.json.bin create mode 100644 tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.xml.bin create mode 100644 tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.json.bin create mode 100644 tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.xml.bin create mode 100644 tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.json.bin create mode 100644 tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.xml.bin create mode 100644 tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.json.bin create mode 100644 tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.xml.bin diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index 2990c390..6b106c31 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -212,8 +212,8 @@ class ComponentIdentityEvidenceField(str, Enum): VERSION = 'version' PURL = 'purl' CPE = 'cpe' - OMNIBOR_ID = 'omniborId' - SWHID = 'swhid' + OMNIBOR_ID = 'omniborId' # Only supported in >= 1.6 + SWHID = 'swhid' # Only supported in >= 1.6 SWID = 'swid' HASH = 'hash' @@ -307,9 +307,11 @@ def __normalize( cs: ComponentIdentityEvidenceMethodTechnique, view: Type[serializable.ViewType] ) -> Optional[str]: - return cs.value \ - if cs in cls.__CASES.get(view, ()) \ - else None + return ( + cs + if cs in cls.__CASES.get(view, ()) + else ComponentIdentityEvidenceMethodTechnique.OTHER + ).value @classmethod def json_normalize(cls, o: Any, *, @@ -354,6 +356,7 @@ def __init__( @property @serializable.type_mapping(_ComponentIdentityEvidenceMethodTechniqueSerializationHelper) + @serializable.xml_sequence(1) def technique(self) -> Optional[ComponentIdentityEvidenceMethodTechnique]: """ The evidence technique used by the method of the component which the evidence describes. @@ -368,6 +371,7 @@ def technique(self, technique: Optional[ComponentIdentityEvidenceMethodTechnique self._technique = technique @property + @serializable.xml_sequence(2) def confidence(self) -> float: """ The overall confidence of the evidence from 0 - 1, where 1 is 100% confidence. @@ -389,6 +393,7 @@ def confidence(self, confidence: float) -> None: self._confidence = confidence @property + @serializable.xml_sequence(3) def value(self) -> Optional[str]: """ The value or contents of the evidence. @@ -452,6 +457,7 @@ def __init__( @property @serializable.type_mapping(_ComponentIdentityEvidenceFieldSerializationHelper) + @serializable.xml_sequence(1) def field(self) -> Optional[ComponentIdentityEvidenceField]: """ The identity field of the component which the evidence describes. @@ -466,6 +472,7 @@ def field(self, field: Optional[ComponentIdentityEvidenceField]) -> None: self._field = field @property + @serializable.xml_sequence(2) def confidence(self) -> Optional[float]: """ The overall confidence of the evidence from 0 - 1, where 1 is 100% confidence. @@ -486,6 +493,7 @@ def confidence(self, confidence: Optional[float]) -> None: @property @serializable.type_mapping(ComponentIdentityEvidenceMethod) + @serializable.xml_sequence(3) def methods(self) -> 'SortedSet[ComponentIdentityEvidenceMethod]': """ Optional list of methods used to extract and/or analyze the evidence. @@ -500,6 +508,7 @@ def methods(self, methods: Iterable[ComponentIdentityEvidenceMethod]) -> None: self._methods = SortedSet(methods) @property + @serializable.xml_sequence(4) def tools(self) -> 'SortedSet[str]': """ Optional list of tools used to extract and/or analyze the evidence. @@ -547,36 +556,36 @@ class ComponentEvidence: def __init__( self, *, - identity: Optional[ComponentIdentityEvidence] = None, + identity: Optional[Iterable[ComponentIdentityEvidence]] = None, licenses: Optional[Iterable[License]] = None, copyright: Optional[Iterable[Copyright]] = None, ) -> None: if not licenses and not copyright and not identity: raise NoPropertiesProvidedException( - 'At least one of `licenses` or `copyright` must be supplied for a `ComponentEvidence`.' + 'At least one of `licenses`, `copyright` or `identity` must be supplied for a `ComponentEvidence`.' ) - self.identity = identity + self.identity = identity or [] # type:ignore[assignment] self.licenses = licenses or [] # type:ignore[assignment] self.copyright = copyright or [] # type:ignore[assignment] @property @serializable.view(SchemaVersion1Dot5) @serializable.view(SchemaVersion1Dot6) + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'identity') @serializable.xml_sequence(1) - def identity(self) -> Optional[ComponentIdentityEvidence]: + def identity(self) -> 'SortedSet[ComponentIdentityEvidence]': """ Optional list of evidence that substantiates the identity of a component. Returns: - `ComponentIdentityEvidence` or `None` + Set of `ComponentIdentityEvidence` """ - return self._identity @identity.setter - def identity(self, identity: Optional[ComponentIdentityEvidence]) -> None: - self._identity = identity + def identity(self, identity: Iterable[ComponentIdentityEvidence]) -> None: + self._identity = SortedSet(identity) # @property # ... @@ -647,7 +656,7 @@ def __lt__(self, other: Any) -> bool: return NotImplemented def __hash__(self) -> int: - return hash((self.identity, tuple(self.licenses), tuple(self.copyright))) + return hash((tuple(self.identity), tuple(self.licenses), tuple(self.copyright))) def __repr__(self) -> str: return f'' diff --git a/tests/_data/models.py b/tests/_data/models.py index d82d2090..150739a0 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -47,10 +47,6 @@ Commit, Component, ComponentEvidence, - ComponentIdentityEvidence, - ComponentIdentityEvidenceField, - ComponentIdentityEvidenceMethod, - ComponentIdentityEvidenceMethodTechnique, ComponentScope, ComponentType, Diff, @@ -934,15 +930,6 @@ def get_swid_2() -> Swid: ) -def get_evidence_identity() -> ComponentIdentityEvidence: - return ComponentIdentityEvidence( - field=ComponentIdentityEvidenceField.NAME, confidence=0.5, methods=[ - ComponentIdentityEvidenceMethod(technique=ComponentIdentityEvidenceMethodTechnique.FILENAME, confidence=0.5) - ], - tools=['cyclonedx-python-lib'] - ) - - def get_vulnerability_source_nvd() -> VulnerabilitySource: return VulnerabilitySource(name='NVD', url=XsUri('https://nvd.nist.gov/vuln/detail/CVE-2018-7489')) diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.0.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.0.xml.bin new file mode 100644 index 00000000..acb06612 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.0.xml.bin @@ -0,0 +1,4 @@ + + + + diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.1.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.1.xml.bin new file mode 100644 index 00000000..55ef5cda --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.1.xml.bin @@ -0,0 +1,4 @@ + + + + diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.json.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.json.bin new file mode 100644 index 00000000..fafe615c --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.json.bin @@ -0,0 +1,17 @@ +{ + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00", + "tools": [ + { + "name": "cyclonedx-python-lib", + "vendor": "CycloneDX", + "version": "TESTING" + } + ] + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.2" +} diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.xml.bin new file mode 100644 index 00000000..bc36ede0 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.xml.bin @@ -0,0 +1,13 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + CycloneDX + cyclonedx-python-lib + TESTING + + + + diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.json.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.json.bin new file mode 100644 index 00000000..d2e65ac1 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.json.bin @@ -0,0 +1,17 @@ +{ + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00", + "tools": [ + { + "name": "cyclonedx-python-lib", + "vendor": "CycloneDX", + "version": "TESTING" + } + ] + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.3" +} diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.xml.bin new file mode 100644 index 00000000..1ebd391f --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.xml.bin @@ -0,0 +1,13 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + CycloneDX + cyclonedx-python-lib + TESTING + + + + diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.json.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.json.bin new file mode 100644 index 00000000..513c4f0f --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.json.bin @@ -0,0 +1,17 @@ +{ + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00", + "tools": [ + { + "name": "cyclonedx-python-lib", + "vendor": "CycloneDX", + "version": "TESTING" + } + ] + }, + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4" +} diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.xml.bin new file mode 100644 index 00000000..21093ed6 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.xml.bin @@ -0,0 +1,14 @@ + + + + + 2023-01-07T13:44:32.312678+00:00 + + + CycloneDX + cyclonedx-python-lib + TESTING + + + + diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.json.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.json.bin new file mode 100644 index 00000000..b533e71c --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.json.bin @@ -0,0 +1,73 @@ +{ + "components": [ + { + "bom-ref": "dummy", + "name": "dummy", + "type": "application", + "evidence": { + "identity": { + "field": "group" + } + } + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00", + "tools": [ + { + "externalReferences": [ + { + "type": "build-system", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions" + }, + { + "type": "distribution", + "url": "https://pypi.org/project/cyclonedx-python-lib/" + }, + { + "type": "documentation", + "url": "https://cyclonedx-python-library.readthedocs.io/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues" + }, + { + "type": "license", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE" + }, + { + "type": "release-notes", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib" + }, + { + "type": "website", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/#readme" + } + ], + "name": "cyclonedx-python-lib", + "vendor": "CycloneDX", + "version": "TESTING" + } + ] + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5" +} diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.xml.bin new file mode 100644 index 00000000..67d8e9bd --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.xml.bin @@ -0,0 +1,53 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + CycloneDX + cyclonedx-python-lib + TESTING + + + https://github.com/CycloneDX/cyclonedx-python-lib/actions + + + https://pypi.org/project/cyclonedx-python-lib/ + + + https://cyclonedx-python-library.readthedocs.io/ + + + https://github.com/CycloneDX/cyclonedx-python-lib/issues + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md + + + https://github.com/CycloneDX/cyclonedx-python-lib + + + https://github.com/CycloneDX/cyclonedx-python-lib/#readme + + + + + + + + dummy + + + group + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.json.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.json.bin new file mode 100644 index 00000000..62c96b5d --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.json.bin @@ -0,0 +1,99 @@ +{ + "components": [ + { + "bom-ref": "dummy", + "name": "dummy", + "type": "application", + "evidence": { + "identity": [ + { + "field": "group" + }, + { + "field": "name" + }, + { + "field": "version" + }, + { + "field": "purl" + }, + { + "field": "cpe" + }, + { + "field": "omniborId" + }, + { + "field": "swhid" + }, + { + "field": "swid" + }, + { + "field": "hash" + } + ] + } + } + ], + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00", + "tools": [ + { + "externalReferences": [ + { + "type": "build-system", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions" + }, + { + "type": "distribution", + "url": "https://pypi.org/project/cyclonedx-python-lib/" + }, + { + "type": "documentation", + "url": "https://cyclonedx-python-library.readthedocs.io/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues" + }, + { + "type": "license", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE" + }, + { + "type": "release-notes", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib" + }, + { + "type": "website", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/#readme" + } + ], + "name": "cyclonedx-python-lib", + "vendor": "CycloneDX", + "version": "TESTING" + } + ] + }, + "properties": [ + { + "name": "key1", + "value": "val1" + }, + { + "name": "key2", + "value": "val2" + } + ], + "serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", + "version": 1, + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6" +} diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.xml.bin new file mode 100644 index 00000000..bc7e7b68 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.xml.bin @@ -0,0 +1,53 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + CycloneDX + cyclonedx-python-lib + TESTING + + + https://github.com/CycloneDX/cyclonedx-python-lib/actions + + + https://pypi.org/project/cyclonedx-python-lib/ + + + https://cyclonedx-python-library.readthedocs.io/ + + + https://github.com/CycloneDX/cyclonedx-python-lib/issues + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md + + + https://github.com/CycloneDX/cyclonedx-python-lib + + + https://github.com/CycloneDX/cyclonedx-python-lib/#readme + + + + + + + + dummy + + + group + + + + + + val1 + val2 + + diff --git a/tests/test_enums.py b/tests/test_enums.py index ba5c6c5f..fec8fa9d 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -29,7 +29,16 @@ from cyclonedx.exception.serialization import SerializationOfUnsupportedComponentTypeException from cyclonedx.model import AttachedText, ExternalReference, HashType, XsUri from cyclonedx.model.bom import Bom -from cyclonedx.model.component import Component, Patch, Pedigree +from cyclonedx.model.component import ( + Component, + ComponentEvidence, + ComponentIdentityEvidence, + ComponentIdentityEvidenceField, + ComponentIdentityEvidenceMethod, + ComponentIdentityEvidenceMethodTechnique, + Patch, + Pedigree, +) from cyclonedx.model.issue import IssueType from cyclonedx.model.license import DisjunctiveLicense from cyclonedx.model.service import DataClassification, Service @@ -355,6 +364,37 @@ def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, super()._test_cases_render(bom, of, sv) +@ddt +class TestEnumComponentIdentityEvidenceField(_EnumTestCase): + + @idata(set(chain( + # dp_cases_from_xml_schemas(f"./{SCHEMA_NS}simpleType[@name='componentIdentityEvidence']"), + dp_cases_from_json_schemas('definitions', 'componentIdentityEvidence', 'properties', 'field'), + ))) + def test_knows_value(self, value: str) -> None: + super()._test_knows_value(ComponentIdentityEvidenceField, value) + + @named_data(*NAMED_OF_SV) + @patch('cyclonedx.model.ThisTool._version', 'TESTING') + def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: + bom = _make_bom(components=[ + Component(name='dummy', type=ComponentType.LIBRARY, bom_ref='dummy', evidence=ComponentEvidence( + identity=[ + ComponentIdentityEvidence( + field=cief, + confidence=0.5, + methods=[ + ComponentIdentityEvidenceMethod(technique=ciemt, confidence=0.5) + for ciemt in ComponentIdentityEvidenceMethodTechnique + ] + ) + ] + )) + for cief in ComponentIdentityEvidenceField + ]) + super()._test_cases_render(bom, of, sv) + + @ddt class TestEnumImpactAnalysisJustification(_EnumTestCase): diff --git a/tests/test_model_component.py b/tests/test_model_component.py index 6a821262..047eb342 100644 --- a/tests/test_model_component.py +++ b/tests/test_model_component.py @@ -308,64 +308,72 @@ def test_same_2(self) -> None: def test_same_3(self) -> None: ce_1 = ComponentEvidence( - identity=ComponentIdentityEvidence( - field=ComponentIdentityEvidenceField.NAME, - confidence=0.5, - methods=[ - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, - confidence=0.5 - ) - ] - ) + identity=[ + ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, + confidence=0.5, + methods=[ + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ) + ] + ) + ] ) ce_2 = ComponentEvidence( - identity=ComponentIdentityEvidence( - field=ComponentIdentityEvidenceField.NAME, - confidence=0.5, - methods=[ - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, - confidence=0.5 - ) - ] - ) + identity=[ + ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, + confidence=0.5, + methods=[ + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ) + ] + ) + ] ) self.assertEqual(hash(ce_1), hash(ce_2)) self.assertTrue(ce_1 == ce_2) def test_same_4(self) -> None: ce_1 = ComponentEvidence( - identity=ComponentIdentityEvidence( - field=ComponentIdentityEvidenceField.NAME, - confidence=0.5, - methods=[ - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, - confidence=0.5 - ), - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.FILENAME, - confidence=0.2 - ) - ] - ) + identity=[ + ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, + confidence=0.5, + methods=[ + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ), + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.FILENAME, + confidence=0.2 + ) + ] + ) + ] ) ce_2 = ComponentEvidence( - identity=ComponentIdentityEvidence( - field=ComponentIdentityEvidenceField.NAME, - confidence=0.5, - methods=[ - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.FILENAME, - confidence=0.2 - ), - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, - confidence=0.5 - ) - ] - ) + identity=[ + ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, + confidence=0.5, + methods=[ + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.FILENAME, + confidence=0.2 + ), + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ) + ] + ) + ] ) self.assertEqual(hash(ce_1), hash(ce_2)) self.assertTrue(ce_1 == ce_2) @@ -378,71 +386,79 @@ def test_not_same_1(self) -> None: def test_not_same_2(self) -> None: ce_1 = ComponentEvidence( - identity=ComponentIdentityEvidence( - field=ComponentIdentityEvidenceField.NAME, - confidence=0.5, - methods=[ - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, - confidence=0.5 - ), - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.BINARY_ANALYSIS, - confidence=0.7 - ) - ] - ) + identity=[ + ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, + confidence=0.5, + methods=[ + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ), + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.BINARY_ANALYSIS, + confidence=0.7 + ) + ] + ) + ] ) ce_2 = ComponentEvidence( - identity=ComponentIdentityEvidence( - field=ComponentIdentityEvidenceField.NAME, - confidence=0.5, - methods=[ - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.BINARY_ANALYSIS, - confidence=0.5 - ), - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, - confidence=0.5 - ) - ] - ) + identity=[ + ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, + confidence=0.5, + methods=[ + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.BINARY_ANALYSIS, + confidence=0.5 + ), + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ) + ] + ) + ] ) self.assertNotEqual(hash(ce_1), hash(ce_2)) def test_not_same_3(self) -> None: ce_1 = ComponentEvidence( - identity=ComponentIdentityEvidence( - field=ComponentIdentityEvidenceField.NAME, - confidence=0.5, - methods=[ - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, - confidence=0.5 - ), - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.BINARY_ANALYSIS, - confidence=0.7 - ) - ] - ) + identity=[ + ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.NAME, + confidence=0.5, + methods=[ + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ), + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.BINARY_ANALYSIS, + confidence=0.7 + ) + ] + ) + ] ) ce_2 = ComponentEvidence( - identity=ComponentIdentityEvidence( - field=ComponentIdentityEvidenceField.HASH, - confidence=0.5, - methods=[ - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.BINARY_ANALYSIS, - confidence=0.7 - ), - ComponentIdentityEvidenceMethod( - technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, - confidence=0.5 - ) - ] - ) + identity=[ + ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField.HASH, + confidence=0.5, + methods=[ + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.BINARY_ANALYSIS, + confidence=0.7 + ), + ComponentIdentityEvidenceMethod( + technique=ComponentIdentityEvidenceMethodTechnique.HASH_COMPARISON, + confidence=0.5 + ) + ] + ) + ] ) self.assertNotEqual(hash(ce_1), hash(ce_2)) self.assertFalse(ce_1 == ce_2) From 27d4470cd2ad886e2cfd83c77aba6ea020643f11 Mon Sep 17 00:00:00 2001 From: Jacob Wahlman Date: Mon, 26 Aug 2024 14:39:14 +0200 Subject: [PATCH 5/6] Fixed issues with invalid test structures for field enum Signed-off-by: Jacob Wahlman --- cyclonedx/exception/serialization.py | 9 +++ cyclonedx/model/component.py | 14 ++-- ...ComponentIdentityEvidenceField-1.0.xml.bin | 8 +- ...ComponentIdentityEvidenceField-1.1.xml.bin | 7 +- ...omponentIdentityEvidenceField-1.2.json.bin | 15 +++- ...ComponentIdentityEvidenceField-1.2.xml.bin | 9 +++ ...omponentIdentityEvidenceField-1.3.json.bin | 22 +++++- ...ComponentIdentityEvidenceField-1.3.xml.bin | 14 ++++ ...omponentIdentityEvidenceField-1.4.json.bin | 55 ++++++++++++- ...ComponentIdentityEvidenceField-1.4.xml.bin | 40 +++++++++- ...omponentIdentityEvidenceField-1.5.json.bin | 21 +++-- ...ComponentIdentityEvidenceField-1.5.xml.bin | 11 ++- ...omponentIdentityEvidenceField-1.6.json.bin | 32 +++++--- ...ComponentIdentityEvidenceField-1.6.xml.bin | 32 +++++++- tests/test_enums.py | 78 +++++++++++++++---- 15 files changed, 319 insertions(+), 48 deletions(-) diff --git a/cyclonedx/exception/serialization.py b/cyclonedx/exception/serialization.py index 565b36c8..9c947344 100644 --- a/cyclonedx/exception/serialization.py +++ b/cyclonedx/exception/serialization.py @@ -44,6 +44,15 @@ class SerializationOfUnsupportedComponentTypeException(CycloneDxSerializationExc """ +class SerializationOfUnsupportedComponentIdentityEvidenceFieldException(CycloneDxSerializationException): + """ + Raised when attempting serializing/normalizing a :py:class:`cyclonedx.model.component.Component` + to a :py:class:`cyclonedx.schema.schema.BaseSchemaVersion` + which does not support that :py:class:`cyclonedx.model.component.ComponentIdentityEvidenceField` + . + """ + + class SerializationOfUnexpectedValueException(CycloneDxSerializationException, ValueError): """ Raised when attempting serializing/normalizing a type that is not expected there. diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index 6b106c31..682ad4c5 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -38,6 +38,7 @@ from ..exception.serialization import ( CycloneDxDeserializationException, SerializationOfUnexpectedValueException, + SerializationOfUnsupportedComponentIdentityEvidenceFieldException, SerializationOfUnsupportedComponentTypeException, ) from ..schema.schema import ( @@ -237,10 +238,11 @@ class _ComponentIdentityEvidenceFieldSerializationHelper(serializable.helpers.Ba } @classmethod - def __normalize(cls, cs: ComponentIdentityEvidenceField, view: Type[serializable.ViewType]) -> Optional[str]: - return cs.value \ - if cs in cls.__CASES.get(view, ()) \ - else None + def __normalize(cls, cief: ComponentIdentityEvidenceField, view: Type[serializable.ViewType]) -> Optional[str]: + if cief in cls.__CASES.get(view, ()): + return cief.value + raise SerializationOfUnsupportedComponentIdentityEvidenceFieldException( + f'unsupported {cief!r} for view {view!r}') @classmethod def json_normalize(cls, o: Any, *, @@ -492,7 +494,7 @@ def confidence(self, confidence: Optional[float]) -> None: self._confidence = confidence @property - @serializable.type_mapping(ComponentIdentityEvidenceMethod) + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'method') @serializable.xml_sequence(3) def methods(self) -> 'SortedSet[ComponentIdentityEvidenceMethod]': """ @@ -572,7 +574,7 @@ def __init__( @property @serializable.view(SchemaVersion1Dot5) @serializable.view(SchemaVersion1Dot6) - @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'identity') + @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'identity') @serializable.xml_sequence(1) def identity(self) -> 'SortedSet[ComponentIdentityEvidence]': """ diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.0.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.0.xml.bin index acb06612..068b881e 100644 --- a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.0.xml.bin +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.0.xml.bin @@ -1,4 +1,10 @@ - + + + dummy + + false + + diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.1.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.1.xml.bin index 55ef5cda..6212e7a1 100644 --- a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.1.xml.bin +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.1.xml.bin @@ -1,4 +1,9 @@ - + + + dummy + + + diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.json.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.json.bin index fafe615c..2161f054 100644 --- a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.json.bin +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.json.bin @@ -1,4 +1,17 @@ { + "components": [ + { + "bom-ref": "dummy", + "name": "dummy", + "type": "library", + "version": "" + } + ], + "dependencies": [ + { + "ref": "dummy" + } + ], "metadata": { "timestamp": "2023-01-07T13:44:32.312678+00:00", "tools": [ @@ -14,4 +27,4 @@ "$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.2" -} +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.xml.bin index bc36ede0..9be884da 100644 --- a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.xml.bin +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.xml.bin @@ -10,4 +10,13 @@ + + + dummy + + + + + + diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.json.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.json.bin index d2e65ac1..05a843a5 100644 --- a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.json.bin +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.json.bin @@ -1,4 +1,24 @@ { + "components": [ + { + "bom-ref": "dummy", + "evidence": { + "copyright": [ + { + "text": "Dummy" + } + ] + }, + "name": "dummy", + "type": "library", + "version": "" + } + ], + "dependencies": [ + { + "ref": "dummy" + } + ], "metadata": { "timestamp": "2023-01-07T13:44:32.312678+00:00", "tools": [ @@ -14,4 +34,4 @@ "$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.3" -} +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.xml.bin index 1ebd391f..d8a52a53 100644 --- a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.xml.bin +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.xml.bin @@ -10,4 +10,18 @@ + + + dummy + + + + Dummy + + + + + + + diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.json.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.json.bin index 513c4f0f..a3baa399 100644 --- a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.json.bin +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.json.bin @@ -1,8 +1,61 @@ { + "components": [ + { + "bom-ref": "dummy", + "evidence": { + "copyright": [ + { + "text": "Dummy" + } + ] + }, + "name": "dummy", + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy" + } + ], "metadata": { "timestamp": "2023-01-07T13:44:32.312678+00:00", "tools": [ { + "externalReferences": [ + { + "type": "build-system", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions" + }, + { + "type": "distribution", + "url": "https://pypi.org/project/cyclonedx-python-lib/" + }, + { + "type": "documentation", + "url": "https://cyclonedx-python-library.readthedocs.io/" + }, + { + "type": "issue-tracker", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues" + }, + { + "type": "license", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE" + }, + { + "type": "release-notes", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md" + }, + { + "type": "vcs", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib" + }, + { + "type": "website", + "url": "https://github.com/CycloneDX/cyclonedx-python-lib/#readme" + } + ], "name": "cyclonedx-python-lib", "vendor": "CycloneDX", "version": "TESTING" @@ -14,4 +67,4 @@ "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.4" -} +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.xml.bin index 21093ed6..e5530a75 100644 --- a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.xml.bin +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.xml.bin @@ -1,4 +1,3 @@ - @@ -8,7 +7,46 @@ CycloneDX cyclonedx-python-lib TESTING + + + https://github.com/CycloneDX/cyclonedx-python-lib/actions + + + https://pypi.org/project/cyclonedx-python-lib/ + + + https://cyclonedx-python-library.readthedocs.io/ + + + https://github.com/CycloneDX/cyclonedx-python-lib/issues + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE + + + https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md + + + https://github.com/CycloneDX/cyclonedx-python-lib + + + https://github.com/CycloneDX/cyclonedx-python-lib/#readme + + + + + dummy + + + Dummy + + + + + + + diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.json.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.json.bin index b533e71c..45796819 100644 --- a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.json.bin +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.json.bin @@ -2,13 +2,20 @@ "components": [ { "bom-ref": "dummy", - "name": "dummy", - "type": "application", "evidence": { - "identity": { - "field": "group" - } - } + "copyright": [ + { + "text": "Dummy" + } + ] + }, + "name": "dummy", + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy" } ], "metadata": { @@ -70,4 +77,4 @@ "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.5" -} +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.xml.bin index 67d8e9bd..aedaed06 100644 --- a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.xml.bin +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.xml.bin @@ -37,15 +37,18 @@ - + dummy - - group - + + Dummy + + + + val1 val2 diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.json.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.json.bin index 62c96b5d..d79dc1d4 100644 --- a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.json.bin +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.json.bin @@ -2,27 +2,30 @@ "components": [ { "bom-ref": "dummy", - "name": "dummy", - "type": "application", "evidence": { + "copyright": [ + { + "text": "Dummy" + } + ], "identity": [ { - "field": "group" + "field": "cpe" }, { - "field": "name" + "field": "group" }, { - "field": "version" + "field": "hash" }, { - "field": "purl" + "field": "name" }, { - "field": "cpe" + "field": "omniborId" }, { - "field": "omniborId" + "field": "purl" }, { "field": "swhid" @@ -31,10 +34,17 @@ "field": "swid" }, { - "field": "hash" + "field": "version" } ] - } + }, + "name": "dummy", + "type": "library" + } + ], + "dependencies": [ + { + "ref": "dummy" } ], "metadata": { @@ -96,4 +106,4 @@ "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.6" -} +} \ No newline at end of file diff --git a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.xml.bin index bc7e7b68..7bdd5e99 100644 --- a/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.xml.bin +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.xml.bin @@ -37,15 +37,45 @@ - + dummy + + cpe + group + + hash + + + name + + + omniborId + + + purl + + + swhid + + + swid + + + version + + + Dummy + + + + val1 val2 diff --git a/tests/test_enums.py b/tests/test_enums.py index fec8fa9d..ab551c77 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -26,7 +26,10 @@ from ddt import ddt, idata, named_data from cyclonedx.exception import MissingOptionalDependencyException -from cyclonedx.exception.serialization import SerializationOfUnsupportedComponentTypeException +from cyclonedx.exception.serialization import ( + SerializationOfUnsupportedComponentIdentityEvidenceFieldException, + SerializationOfUnsupportedComponentTypeException, +) from cyclonedx.model import AttachedText, ExternalReference, HashType, XsUri from cyclonedx.model.bom import Bom from cyclonedx.model.component import ( @@ -34,8 +37,7 @@ ComponentEvidence, ComponentIdentityEvidence, ComponentIdentityEvidenceField, - ComponentIdentityEvidenceMethod, - ComponentIdentityEvidenceMethodTechnique, + Copyright, Patch, Pedigree, ) @@ -364,12 +366,32 @@ def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, super()._test_cases_render(bom, of, sv) +class _DP_ComponentIdentityEvidenceField(): # noqa: N801 + XML_SCHEMA_XPATH = f"./{SCHEMA_NS}simpleType[@name='identityFieldType']" + JSON_SCHEMA_POINTER = ('definitions', 'componentIdentityEvidence', 'properties', 'field') + + @classmethod + def unsupported_cases(cls) -> Generator[ + Tuple[str, OutputFormat, SchemaVersion, ComponentIdentityEvidenceField], None, None + ]: + for name, of, sv in NAMED_OF_SV: + if OutputFormat.XML is of: + schema_cases = set(dp_cases_from_xml_schema(SCHEMA_XML[sv], cls.XML_SCHEMA_XPATH)) + elif OutputFormat.JSON is of: + schema_cases = set(dp_cases_from_json_schema(SCHEMA_JSON[sv], cls.JSON_SCHEMA_POINTER)) + else: + raise ValueError(f'unexpected of: {of!r}') + for cief in ComponentIdentityEvidenceField: + if cief.value not in schema_cases: + yield f'{name}-{cief.name}', of, sv, cief + + @ddt class TestEnumComponentIdentityEvidenceField(_EnumTestCase): @idata(set(chain( - # dp_cases_from_xml_schemas(f"./{SCHEMA_NS}simpleType[@name='componentIdentityEvidence']"), - dp_cases_from_json_schemas('definitions', 'componentIdentityEvidence', 'properties', 'field'), + dp_cases_from_xml_schemas(_DP_ComponentIdentityEvidenceField.XML_SCHEMA_XPATH), + dp_cases_from_json_schemas(*_DP_ComponentIdentityEvidenceField.JSON_SCHEMA_POINTER), ))) def test_knows_value(self, value: str) -> None: super()._test_knows_value(ComponentIdentityEvidenceField, value) @@ -377,23 +399,53 @@ def test_knows_value(self, value: str) -> None: @named_data(*NAMED_OF_SV) @patch('cyclonedx.model.ThisTool._version', 'TESTING') def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: + if OutputFormat.XML is of: + schema_cases = set(dp_cases_from_xml_schema(SCHEMA_XML[sv], _DP_ComponentIdentityEvidenceField.XML_SCHEMA_XPATH)) + elif OutputFormat.JSON is of: + schema_cases = set(dp_cases_from_json_schema(SCHEMA_JSON[sv], _DP_ComponentIdentityEvidenceField.JSON_SCHEMA_POINTER)) + else: + raise ValueError(f'unexpected of: {of!r}') + bom = _make_bom(components=[ - Component(name='dummy', type=ComponentType.LIBRARY, bom_ref='dummy', evidence=ComponentEvidence( + Component(bom_ref='dummy', name='dummy', evidence=ComponentEvidence( + copyright=[ + Copyright(text="Dummy") + ], identity=[ ComponentIdentityEvidence( - field=cief, - confidence=0.5, - methods=[ - ComponentIdentityEvidenceMethod(technique=ciemt, confidence=0.5) - for ciemt in ComponentIdentityEvidenceMethodTechnique - ] + field=ComponentIdentityEvidenceField(cief), ) + for cief in schema_cases + if cief in schema_cases ] )) - for cief in ComponentIdentityEvidenceField ]) super()._test_cases_render(bom, of, sv) + @named_data(*_DP_ComponentIdentityEvidenceField.unsupported_cases()) + @patch('cyclonedx.model.ThisTool._version', 'TESTING') + def test_cases_render_raises_on_unsupported(self, of: OutputFormat, sv: SchemaVersion, + cief: ComponentIdentityEvidenceField, + *_: Any, **__: Any) -> None: + # componentIdentityEvidence type was not added until 1.5 + if sv.value < (1, 5): + return True + + bom = _make_bom(components=[ + Component(bom_ref='dummy', name='dummy', evidence=ComponentEvidence( + copyright=[ + Copyright(text="Dummy") + ], + identity=[ + ComponentIdentityEvidence( + field=cief, + ) + ] + )) + ]) + with self.assertRaises(SerializationOfUnsupportedComponentIdentityEvidenceFieldException): + super()._test_cases_render(bom, of, sv) + @ddt class TestEnumImpactAnalysisJustification(_EnumTestCase): From 71ef5d3fb25b8b3331fa1b078cdf002434038114 Mon Sep 17 00:00:00 2001 From: Jacob Wahlman Date: Mon, 26 Aug 2024 15:08:50 +0200 Subject: [PATCH 6/6] Minor formatting fixues in tests Signed-off-by: Jacob Wahlman --- tests/test_enums.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_enums.py b/tests/test_enums.py index ab551c77..b0b35341 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -400,16 +400,18 @@ def test_knows_value(self, value: str) -> None: @patch('cyclonedx.model.ThisTool._version', 'TESTING') def test_cases_render_valid(self, of: OutputFormat, sv: SchemaVersion, *_: Any, **__: Any) -> None: if OutputFormat.XML is of: - schema_cases = set(dp_cases_from_xml_schema(SCHEMA_XML[sv], _DP_ComponentIdentityEvidenceField.XML_SCHEMA_XPATH)) + schema_cases = set(dp_cases_from_xml_schema( + SCHEMA_XML[sv], _DP_ComponentIdentityEvidenceField.XML_SCHEMA_XPATH)) elif OutputFormat.JSON is of: - schema_cases = set(dp_cases_from_json_schema(SCHEMA_JSON[sv], _DP_ComponentIdentityEvidenceField.JSON_SCHEMA_POINTER)) + schema_cases = set(dp_cases_from_json_schema( + SCHEMA_JSON[sv], _DP_ComponentIdentityEvidenceField.JSON_SCHEMA_POINTER)) else: raise ValueError(f'unexpected of: {of!r}') - + bom = _make_bom(components=[ Component(bom_ref='dummy', name='dummy', evidence=ComponentEvidence( copyright=[ - Copyright(text="Dummy") + Copyright(text='Dummy') ], identity=[ ComponentIdentityEvidence( @@ -430,11 +432,11 @@ def test_cases_render_raises_on_unsupported(self, of: OutputFormat, sv: SchemaVe # componentIdentityEvidence type was not added until 1.5 if sv.value < (1, 5): return True - + bom = _make_bom(components=[ Component(bom_ref='dummy', name='dummy', evidence=ComponentEvidence( copyright=[ - Copyright(text="Dummy") + Copyright(text='Dummy') ], identity=[ ComponentIdentityEvidence(