diff --git a/cyclonedx/exception/model.py b/cyclonedx/exception/model.py index cf354ed2..e301112c 100644 --- a/cyclonedx/exception/model.py +++ b/cyclonedx/exception/model.py @@ -56,6 +56,20 @@ class InvalidNistQuantumSecurityLevelException(CycloneDxModelException): pass +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/exception/serialization.py b/cyclonedx/exception/serialization.py index 2c53beb5..57b68e20 100644 --- a/cyclonedx/exception/serialization.py +++ b/cyclonedx/exception/serialization.py @@ -46,6 +46,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 89e7020d..22e4cec9 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -28,10 +28,17 @@ 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, + SerializationOfUnsupportedComponentIdentityEvidenceFieldException, SerializationOfUnsupportedComponentTypeException, ) from ..schema.schema import ( @@ -191,6 +198,353 @@ 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' # Only supported in >= 1.6 + SWHID = 'swhid' # Only supported in >= 1.6 + 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, 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, *, + 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 + if cs in cls.__CASES.get(view, ()) + else ComponentIdentityEvidenceMethodTechnique.OTHER + ).value + + @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) + @serializable.xml_sequence(1) + 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 + @serializable.xml_sequence(2) + 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 + @serializable.xml_sequence(3) + 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) + @serializable.xml_sequence(1) + 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 + @serializable.xml_sequence(2) + 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.xml_array(serializable.XmlArraySerializationType.NESTED, 'method') + @serializable.xml_sequence(3) + 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 + @serializable.xml_sequence(4) + 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: """ @@ -204,27 +558,36 @@ class ComponentEvidence: def __init__( self, *, + identity: Optional[Iterable[ComponentIdentityEvidence]] = None, 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`.' + 'At least one of `licenses`, `copyright` or `identity` must be supplied for a `ComponentEvidence`.' ) + 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.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.view(SchemaVersion1Dot6) + @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'identity') + @serializable.xml_sequence(1) + def identity(self) -> 'SortedSet[ComponentIdentityEvidence]': + """ + Optional list of evidence that substantiates the identity of a component. + + Returns: + Set of `ComponentIdentityEvidence` + """ + return self._identity + + @identity.setter + def identity(self, identity: Iterable[ComponentIdentityEvidence]) -> None: + self._identity = SortedSet(identity) # @property # ... @@ -285,8 +648,17 @@ 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((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/snapshots/enum_ComponentIdentityEvidenceField-1.0.xml.bin b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.0.xml.bin new file mode 100644 index 00000000..068b881e --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.0.xml.bin @@ -0,0 +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 new file mode 100644 index 00000000..6212e7a1 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.1.xml.bin @@ -0,0 +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 new file mode 100644 index 00000000..2161f054 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.json.bin @@ -0,0 +1,30 @@ +{ + "components": [ + { + "bom-ref": "dummy", + "name": "dummy", + "type": "library", + "version": "" + } + ], + "dependencies": [ + { + "ref": "dummy" + } + ], + "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" +} \ 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 new file mode 100644 index 00000000..9be884da --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.2.xml.bin @@ -0,0 +1,22 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + CycloneDX + cyclonedx-python-lib + TESTING + + + + + + dummy + + + + + + + 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..05a843a5 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.json.bin @@ -0,0 +1,37 @@ +{ + "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": [ + { + "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" +} \ 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 new file mode 100644 index 00000000..d8a52a53 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.3.xml.bin @@ -0,0 +1,27 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + CycloneDX + cyclonedx-python-lib + TESTING + + + + + + dummy + + + + Dummy + + + + + + + + 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..a3baa399 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.json.bin @@ -0,0 +1,70 @@ +{ + "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" + } + ] + }, + "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" +} \ 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 new file mode 100644 index 00000000..e5530a75 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.4.xml.bin @@ -0,0 +1,52 @@ + + + + 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 + + + Dummy + + + + + + + + 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..45796819 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.json.bin @@ -0,0 +1,80 @@ +{ + "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" + } + ] + }, + "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" +} \ 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 new file mode 100644 index 00000000..aedaed06 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.5.xml.bin @@ -0,0 +1,56 @@ + + + + 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 + + + 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 new file mode 100644 index 00000000..d79dc1d4 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.json.bin @@ -0,0 +1,109 @@ +{ + "components": [ + { + "bom-ref": "dummy", + "evidence": { + "copyright": [ + { + "text": "Dummy" + } + ], + "identity": [ + { + "field": "cpe" + }, + { + "field": "group" + }, + { + "field": "hash" + }, + { + "field": "name" + }, + { + "field": "omniborId" + }, + { + "field": "purl" + }, + { + "field": "swhid" + }, + { + "field": "swid" + }, + { + "field": "version" + } + ] + }, + "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" + } + ] + }, + "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" +} \ 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 new file mode 100644 index 00000000..7bdd5e99 --- /dev/null +++ b/tests/_data/snapshots/enum_ComponentIdentityEvidenceField-1.6.xml.bin @@ -0,0 +1,83 @@ + + + + 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 + + + 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 c963c499..ee7f197a 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -28,10 +28,21 @@ 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 Component, Patch, Pedigree +from cyclonedx.model.component import ( + Component, + ComponentEvidence, + ComponentIdentityEvidence, + ComponentIdentityEvidenceField, + Copyright, + Patch, + Pedigree, +) from cyclonedx.model.issue import IssueType from cyclonedx.model.license import DisjunctiveLicense from cyclonedx.model.service import DataClassification, Service @@ -357,6 +368,89 @@ 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(_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) + + @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(bom_ref='dummy', name='dummy', evidence=ComponentEvidence( + copyright=[ + Copyright(text='Dummy') + ], + identity=[ + ComponentIdentityEvidence( + field=ComponentIdentityEvidenceField(cief), + ) + for cief in schema_cases + if cief in schema_cases + ] + )) + ]) + 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): diff --git a/tests/test_model_component.py b/tests/test_model_component.py index c25fdc91..c919a0e9 100644 --- a/tests/test_model_component.py +++ b/tests/test_model_component.py @@ -34,6 +34,10 @@ Commit, Component, ComponentEvidence, + ComponentIdentityEvidence, + ComponentIdentityEvidenceField, + ComponentIdentityEvidenceMethod, + ComponentIdentityEvidenceMethodTechnique, ComponentType, Diff, Patch, @@ -302,12 +306,164 @@ 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=[ + 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 + ) + ] + ) + ] + ) + 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 + ) + ] + ) + ] + ) + 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 + ) + ] + ) + ] + ) + 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=[ + 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 + ) + ] + ) + ] + ) + 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 + ) + ] + ) + ] + ) + 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 + ) + ] + ) + ] + ) + self.assertNotEqual(hash(ce_1), hash(ce_2)) + self.assertFalse(ce_1 == ce_2) + self.assertFalse(ce_1 == ce_2) + class TestModelDiff(TestCase):