From b45abf424294cb5688b5e857b71ed13e3e4c6732 Mon Sep 17 00:00:00 2001 From: Hakan Dilek Date: Thu, 17 Oct 2024 18:15:26 +0200 Subject: [PATCH 01/20] feat: add bom.definitions with complete model Signed-off-by: Hakan Dilek --- cyclonedx/exception/model.py | 8 + cyclonedx/model/definition.py | 413 ++++++++++++++++++++++++++++++--- tests/test_model_definition.py | 72 +++++- 3 files changed, 464 insertions(+), 29 deletions(-) diff --git a/cyclonedx/exception/model.py b/cyclonedx/exception/model.py index cf354ed2..3484b606 100644 --- a/cyclonedx/exception/model.py +++ b/cyclonedx/exception/model.py @@ -123,3 +123,11 @@ class LicenseExpressionAlongWithOthersException(CycloneDxModelException): See https://github.com/CycloneDX/specification/pull/205 """ pass + + +class InvalidCreIdException(CycloneDxModelException): + """ + Raised when a supplied value for an CRE ID does not meet the format requirements + as defined at https://opencre.org/ + """ + pass diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index 417fd0c1..9f444fce 100644 --- a/cyclonedx/model/definition.py +++ b/cyclonedx/model/definition.py @@ -15,6 +15,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. +import re from typing import TYPE_CHECKING, Any, Iterable, Optional, Union import serializable @@ -22,14 +23,366 @@ from .._internal.bom_ref import bom_ref_from_str from .._internal.compare import ComparableTuple as _ComparableTuple +from ..exception.model import InvalidCreIdException +from ..exception.serialization import SerializationOfUnexpectedValueException from ..serialization import BomRefHelper -from . import ExternalReference +from . import ExternalReference, Property from .bom_ref import BomRef if TYPE_CHECKING: # pragma: no cover pass +@serializable.serializable_class +class CreId(serializable.helpers.BaseHelper): + """ + Helper class that allows us to perform validation on data strings that must conform to + Common Requirements Enumeration (CRE) identifier(s). + + """ + + _VALID_CRE_REGEX = re.compile(r'^CRE:[0-9]+-[0-9]+$') + + def __init__(self, id: str) -> None: + if CreId._VALID_CRE_REGEX.match(id) is None: + raise InvalidCreIdException( + f'Supplied value "{id} does not meet format specification.' + ) + self._id = id + + @property + @serializable.json_name('.') + @serializable.xml_name('.') + def id(self) -> str: + return self._id + + @classmethod + def serialize(cls, o: Any) -> str: + if isinstance(o, cls): + return str(o) + raise SerializationOfUnexpectedValueException( + f'Attempt to serialize a non-CreId: {o!r}') + + @classmethod + def deserialize(cls, o: Any) -> 'CreId': + return cls(id=str(o)) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, CreId): + return hash(other) == hash(self) + return False + + def __lt__(self, other: Any) -> bool: + if isinstance(other, CreId): + return self._id < other._id + return NotImplemented + + def __hash__(self) -> int: + return hash(self._id) + + def __repr__(self) -> str: + return f'' + + def __str__(self) -> str: + return self._id + + +@serializable.serializable_class +class Requirement: + """ + A requirement comprising a standard. + """ + + def __init__( + self, *, + bom_ref: Optional[Union[str, BomRef]] = None, + identifier: Optional[str] = None, + title: Optional[str] = None, + text: Optional[str] = None, + descriptions: Optional[Iterable[str]] = None, + open_cre: Optional[Iterable[CreId]] = None, + parent: Optional[Union[str, BomRef]] = None, + properties: Optional[Iterable[Property]] = None, + external_references: Optional[Iterable[ExternalReference]] = None, + ) -> None: + self._bom_ref = bom_ref_from_str(bom_ref) + self.identifier = identifier + self.title = title + self.text = text + self.descriptions = descriptions or [] # type:ignore[assignment] + self.open_cre = open_cre or [] # type:ignore[assignment] + self.parent = bom_ref_from_str(parent) + self.properties = properties or [] # type:ignore[assignment] + self.external_references = external_references or [] # type:ignore[assignment] + + def __lt__(self, other: Any) -> bool: + if isinstance(other, Requirement): + return (_ComparableTuple((self.bom_ref, self.identifier)) + < _ComparableTuple((other.bom_ref, other.title))) + return NotImplemented + + def __eq__(self, other: object) -> bool: + if isinstance(other, Requirement): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash(( + self.bom_ref, self.identifier, self.title, self.text, tuple(self.descriptions), + tuple(self.open_cre), self.parent, tuple(self.properties), tuple(self.external_references) + )) + + def __repr__(self) -> str: + return f'' + + @property + @serializable.json_name('bom-ref') + @serializable.type_mapping(BomRefHelper) + @serializable.xml_attribute() + @serializable.xml_name('bom-ref') + def bom_ref(self) -> BomRef: + """ + An optional identifier which can be used to reference the requirement elsewhere in the BOM. + Every bom-ref MUST be unique within the BOM. + + Returns: + `BomRef` + """ + return self._bom_ref + + @property + @serializable.xml_sequence(1) + def identifier(self) -> Optional[str]: + """ + Returns: + The identifier of the requirement. + """ + return self._identifier + + @identifier.setter + def identifier(self, identifier: Optional[str]) -> None: + self._identifier = identifier + + @property + @serializable.xml_sequence(2) + def title(self) -> Optional[str]: + """ + Returns: + The title of the requirement. + """ + return self._title + + @title.setter + def title(self, title: Optional[str]) -> None: + self._title = title + + @property + @serializable.xml_sequence(3) + def text(self) -> Optional[str]: + """ + Returns: + The text of the requirement. + """ + return self._text + + @text.setter + def text(self, text: Optional[str]) -> None: + self._text = text + + @property + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'description') + @serializable.xml_sequence(4) + def descriptions(self) -> 'SortedSet[str]': + """ + Returns: + A SortedSet of descriptions of the requirement. + """ + return self._descriptions + + @descriptions.setter + def descriptions(self, descriptions: Iterable[str]) -> None: + self._descriptions = SortedSet(descriptions) + + @property + @serializable.json_name('openCre') + @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'openCre') + @serializable.xml_sequence(5) + def open_cre(self) -> 'SortedSet[CreId]': + """ + CRE is a structured and standardized framework for uniting security standards and guidelines. CRE links each + section of a resource to a shared topic identifier (a Common Requirement). Through this shared topic link, all + resources map to each other. Use of CRE promotes clear and unambiguous communication among stakeholders. + + Returns: + The Common Requirements Enumeration (CRE) identifier(s). + CREs must match regular expression: ^CRE:[0-9]+-[0-9]+$ + """ + return self._open_cre + + @open_cre.setter + def open_cre(self, open_cre: Iterable[CreId]) -> None: + self._open_cre = SortedSet(open_cre) + + @property + @serializable.type_mapping(BomRefHelper) + @serializable.xml_sequence(6) + def parent(self) -> Optional[BomRef]: + """ + Returns: + The optional bom-ref to a parent requirement. This establishes a hierarchy of requirements. Top-level + requirements must not define a parent. Only child requirements should define parents. + """ + return self._parent + + @parent.setter + def parent(self, parent: Optional[Union[str, BomRef]]) -> None: + self._parent = bom_ref_from_str(parent) + + @property + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property') + @serializable.xml_sequence(7) + def properties(self) -> 'SortedSet[Property]': + """ + Provides the ability to document properties in a key/value store. This provides flexibility to include data not + officially supported in the standard without having to use additional namespaces or create extensions. + + Return: + Set of `Property` + """ + return self._properties + + @properties.setter + def properties(self, properties: Iterable[Property]) -> None: + self._properties = SortedSet(properties) + + @property + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference') + @serializable.xml_sequence(8) + def external_references(self) -> 'SortedSet[ExternalReference]': + """ + Provides the ability to document external references related to the component or to the project the component + describes. + + Returns: + Set of `ExternalReference` + """ + return self._external_references + + @external_references.setter + def external_references(self, external_references: Iterable[ExternalReference]) -> None: + self._external_references = SortedSet(external_references) + + +@serializable.serializable_class +class Level: + """ + Level of compliance for a standard. + """ + + def __init__( + self, *, + bom_ref: Optional[Union[str, BomRef]] = None, + identifier: Optional[str] = None, + title: Optional[str] = None, + description: Optional[str] = None, + requirements: Optional[Iterable[Union[str, BomRef]]] = None, + ) -> None: + self._bom_ref = bom_ref_from_str(bom_ref) + self.identifier = identifier + self.title = title + self.description = description + self.requirements = requirements or [] # type:ignore[assignment] + + def __lt__(self, other: Any) -> bool: + if isinstance(other, Level): + return (_ComparableTuple((self.bom_ref, self.identifier)) + < _ComparableTuple((other.bom_ref, other.identifier))) + return NotImplemented + + def __eq__(self, other: object) -> bool: + if isinstance(other, Level): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash(( + self.bom_ref, self.identifier, self.title, self.description, tuple(self.requirements) + )) + + def __repr__(self) -> str: + return f'' + + @property + @serializable.json_name('bom-ref') + @serializable.type_mapping(BomRefHelper) + @serializable.xml_attribute() + @serializable.xml_name('bom-ref') + def bom_ref(self) -> BomRef: + """ + An optional identifier which can be used to reference the level elsewhere in the BOM. + Every bom-ref MUST be unique within the BOM. + + Returns: + `BomRef` + """ + return self._bom_ref + + @property + @serializable.xml_sequence(1) + def identifier(self) -> Optional[str]: + """ + Returns: + The identifier of the level. + """ + return self._identifier + + @identifier.setter + def identifier(self, identifier: Optional[str]) -> None: + self._identifier = identifier + + @property + @serializable.xml_sequence(2) + def title(self) -> Optional[str]: + """ + Returns: + The title of the level. + """ + return self._title + + @title.setter + def title(self, title: Optional[str]) -> None: + self._title = title + + @property + @serializable.xml_sequence(3) + def description(self) -> Optional[str]: + """ + Returns: + The description of the level. + """ + return self._description + + @description.setter + def description(self, description: Optional[str]) -> None: + self._description = description + + @property + @serializable.xml_sequence(4) + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'requirement') + def requirements(self) -> 'SortedSet[BomRef]': + """ + Returns: + A SortedSet of requirements associated with the level. + """ + return self._requirements + + @requirements.setter + def requirements(self, requirements: Iterable[Union[str, BomRef]]) -> None: + self._requirements = SortedSet(map(lambda x: bom_ref_from_str(x), requirements)) + + @serializable.serializable_class class Standard: """ @@ -44,6 +397,8 @@ def __init__( version: Optional[str] = None, description: Optional[str] = None, owner: Optional[str] = None, + requirements: Optional[Iterable[Requirement]] = None, + levels: Optional[Iterable[Level]] = None, external_references: Optional[Iterable['ExternalReference']] = None ) -> None: self._bom_ref = bom_ref_from_str(bom_ref) @@ -51,6 +406,8 @@ def __init__( self.version = version self.description = description self.owner = owner + self.requirements = requirements or [] # type:ignore[assignment] + self.levels = levels or [] # type:ignore[assignment] self.external_references = external_references or [] # type:ignore[assignment] def __lt__(self, other: Any) -> bool: @@ -140,33 +497,33 @@ def owner(self) -> Optional[str]: def owner(self, owner: Optional[str]) -> None: self._owner = owner - # @property - # @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'requirement') - # @serializable.xml_sequence(5) - # def requirements(self) -> 'SortedSet[Requirement]': - # """ - # Returns: - # A SortedSet of requirements comprising the standard. - # """ - # return self._requirements - # - # @requirements.setter - # def requirements(self, requirements: Iterable[Requirement]) -> None: - # self._requirements = SortedSet(requirements) - # - # @property - # @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'level') - # @serializable.xml_sequence(6) - # def levels(self) -> 'SortedSet[Level]': - # """ - # Returns: - # A SortedSet of levels associated with the standard. Some standards have different levels of compliance. - # """ - # return self._levels - # - # @levels.setter - # def levels(self, levels: Iterable[Level]) -> None: - # self._levels = SortedSet(levels) + @property + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'requirement') + @serializable.xml_sequence(5) + def requirements(self) -> 'SortedSet[Requirement]': + """ + Returns: + A SortedSet of requirements comprising the standard. + """ + return self._requirements + + @requirements.setter + def requirements(self, requirements: Iterable[Requirement]) -> None: + self._requirements = SortedSet(requirements) + + @property + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'level') + @serializable.xml_sequence(6) + def levels(self) -> 'SortedSet[Level]': + """ + Returns: + A SortedSet of levels associated with the standard. Some standards have different levels of compliance. + """ + return self._levels + + @levels.setter + def levels(self, levels: Iterable[Level]) -> None: + self._levels = SortedSet(levels) @property @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference') diff --git a/tests/test_model_definition.py b/tests/test_model_definition.py index a464bfcd..8c5ba123 100644 --- a/tests/test_model_definition.py +++ b/tests/test_model_definition.py @@ -18,7 +18,8 @@ from unittest import TestCase -from cyclonedx.model.definition import Definitions, Standard +from cyclonedx.exception.model import InvalidCreIdException +from cyclonedx.model.definition import CreId, Definitions, Level, Requirement, Standard class TestModelDefinitions(TestCase): @@ -65,3 +66,72 @@ def test_equal(self) -> None: tr2 = Definitions() tr2.standards.add(s) self.assertTrue(dr1 == tr2) + + +class TestModelCreId(TestCase): + + def test_different(self) -> None: + id1 = CreId('CRE:123-456') + id2 = CreId('CRE:987-654') + self.assertNotEqual(id(id1), id(id2)) + self.assertNotEqual(hash(id1), hash(id2)) + self.assertFalse(id1 == id2) + + def test_same(self) -> None: + id1 = CreId('CRE:123-456') + id2 = CreId('CRE:123-456') + self.assertNotEqual(id(id1), id(id2)) + self.assertEqual(hash(id1), hash(id2)) + self.assertTrue(id1 == id2) + + def test_invalid_id(self) -> None: + with self.assertRaises(TypeError): + CreId() + with self.assertRaises(InvalidCreIdException): + CreId('') + with self.assertRaises(InvalidCreIdException): + CreId('some string') + with self.assertRaises(InvalidCreIdException): + CreId('123-456') + with self.assertRaises(InvalidCreIdException): + CreId('CRE:123-456-789') + with self.assertRaises(InvalidCreIdException): + CreId('CRE:abc-def') + with self.assertRaises(InvalidCreIdException): + CreId('CRE:123456') + + +class TestModelRequirements(TestCase): + + def test_bom_ref_is_set_from_value(self) -> None: + r = Requirement(bom_ref='123-456') + self.assertIsNotNone(r.bom_ref) + self.assertEqual('123-456', r.bom_ref.value) + + def test_bom_ref_is_set_if_none_given(self) -> None: + r = Requirement() + self.assertIsNotNone(r.bom_ref) + + +class TestModelLevel(TestCase): + + def test_bom_ref_is_set_from_value(self) -> None: + r = Level(bom_ref='123-456') + self.assertIsNotNone(r.bom_ref) + self.assertEqual('123-456', r.bom_ref.value) + + def test_bom_ref_is_set_if_none_given(self) -> None: + r = Level() + self.assertIsNotNone(r.bom_ref) + + +class TestModelStandard(TestCase): + + def test_bom_ref_is_set_from_value(self) -> None: + r = Standard(bom_ref='123-456') + self.assertIsNotNone(r.bom_ref) + self.assertEqual('123-456', r.bom_ref.value) + + def test_bom_ref_is_set_if_none_given(self) -> None: + r = Standard() + self.assertIsNotNone(r.bom_ref) From 467d7f7d03fc5e870f8dc9986b512ee4f773744f Mon Sep 17 00:00:00 2001 From: Hakan Dilek Date: Thu, 17 Oct 2024 18:15:33 +0200 Subject: [PATCH 02/20] feat: add detailed test fixtures related to bom.definitions Signed-off-by: Hakan Dilek --- tests/_data/models.py | 48 +++++- ...nitions_and_detailed_standards-1.0.xml.bin | 4 + ...nitions_and_detailed_standards-1.1.xml.bin | 4 + ...itions_and_detailed_standards-1.2.json.bin | 10 ++ ...nitions_and_detailed_standards-1.2.xml.bin | 6 + ...itions_and_detailed_standards-1.3.json.bin | 10 ++ ...nitions_and_detailed_standards-1.3.xml.bin | 6 + ...itions_and_detailed_standards-1.4.json.bin | 10 ++ ...nitions_and_detailed_standards-1.4.xml.bin | 6 + ...itions_and_detailed_standards-1.5.json.bin | 20 +++ ...nitions_and_detailed_standards-1.5.xml.bin | 10 ++ ...itions_and_detailed_standards-1.6.json.bin | 142 ++++++++++++++++++ ...nitions_and_detailed_standards-1.6.xml.bin | 104 +++++++++++++ 13 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.0.xml.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.1.xml.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.2.json.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.2.xml.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.3.json.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.3.xml.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.4.json.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.4.xml.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.5.json.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.5.xml.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.json.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin diff --git a/tests/_data/models.py b/tests/_data/models.py index ffbf7d4a..0c79b530 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -78,7 +78,7 @@ RelatedCryptoMaterialState, RelatedCryptoMaterialType, ) -from cyclonedx.model.definition import Definitions, Standard +from cyclonedx.model.definition import CreId, Definitions, Level, Requirement, Standard from cyclonedx.model.dependency import Dependency from cyclonedx.model.impact_analysis import ( ImpactAnalysisAffectedStatus, @@ -1310,6 +1310,51 @@ def get_bom_with_definitions_standards() -> Bom: ) +def get_bom_with_definitions_and_detailed_standards() -> Bom: + """ + Returns a BOM with definitions and multiple detailed standards including requirements and levels. + """ + return _make_bom( + definitions=Definitions( + standards=[ + Standard(name='Some Standard', version='1.2.3', description='Some description', bom_ref='some-standard', + owner='Some Owner', external_references=[get_external_reference_1()], + requirements=[ + Requirement(identifier='REQ-1', title='Requirement 1', text='some requirement text', + bom_ref='req-1', descriptions=['Requirement 1 described here', 'and here'], + open_cre=[CreId('CRE:1-2')], properties=[Property(name='key1', value='val1')] + ), + Requirement(identifier='REQ-2', title='Requirement 2', text='some requirement text', + bom_ref='req-2', descriptions=['Requirement 2 described here'], + open_cre=[CreId('CRE:1-2'), CreId('CRE:3-4')], + properties=[Property(name='key2', value='val2')], + parent='req-1' + ), + ], + levels=[ + Level(identifier='LVL-1', title='Level 1', description='Level 1 description', + bom_ref='lvl-1', ), + Level(identifier='LVL-2', title='Level 2', description='Level 2 description', + bom_ref='lvl-2', ) + ]), + Standard(name='Other Standard', version='1.0.0', description='Other description', + bom_ref='other-standard', owner='Other Owner', + external_references=[get_external_reference_2()], + requirements=[ + Requirement(identifier='REQ-3', title='Requirement 3', text='some requirement text', + bom_ref='req-3', descriptions=['Requirement 3 described here', 'and here'], + open_cre=[CreId('CRE:5-6'), CreId('CRE:7-8')], + properties=[Property(name='key3', value='val3')] + ) + ], + levels=[ + Level(identifier='LVL-3', title='Level 3', description='Level 3 description', + bom_ref='lvl-3', ) + ]) + ] + )) + + # --- @@ -1357,4 +1402,5 @@ def get_bom_with_definitions_standards() -> Bom: get_bom_for_issue_630_empty_property, get_bom_with_lifecycles, get_bom_with_definitions_standards, + get_bom_with_definitions_and_detailed_standards, } diff --git a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.0.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.0.xml.bin new file mode 100644 index 00000000..acb06612 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.0.xml.bin @@ -0,0 +1,4 @@ + + + + diff --git a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.1.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.1.xml.bin new file mode 100644 index 00000000..55ef5cda --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.1.xml.bin @@ -0,0 +1,4 @@ + + + + diff --git a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.2.json.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.2.json.bin new file mode 100644 index 00000000..8f473bd3 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.2.json.bin @@ -0,0 +1,10 @@ +{ + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "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/get_bom_with_definitions_and_detailed_standards-1.2.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.2.xml.bin new file mode 100644 index 00000000..df1938ec --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.2.xml.bin @@ -0,0 +1,6 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + diff --git a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.3.json.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.3.json.bin new file mode 100644 index 00000000..02943890 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.3.json.bin @@ -0,0 +1,10 @@ +{ + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "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/get_bom_with_definitions_and_detailed_standards-1.3.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.3.xml.bin new file mode 100644 index 00000000..8341ff60 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.3.xml.bin @@ -0,0 +1,6 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + diff --git a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.4.json.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.4.json.bin new file mode 100644 index 00000000..48f1745d --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.4.json.bin @@ -0,0 +1,10 @@ +{ + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "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/get_bom_with_definitions_and_detailed_standards-1.4.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.4.xml.bin new file mode 100644 index 00000000..d0a7d4c9 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.4.xml.bin @@ -0,0 +1,6 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + diff --git a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.5.json.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.5.json.bin new file mode 100644 index 00000000..57b5e590 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.5.json.bin @@ -0,0 +1,20 @@ +{ + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "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/get_bom_with_definitions_and_detailed_standards-1.5.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.5.xml.bin new file mode 100644 index 00000000..f952637c --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.5.xml.bin @@ -0,0 +1,10 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + val1 + val2 + + diff --git a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.json.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.json.bin new file mode 100644 index 00000000..4dd3ed7c --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.json.bin @@ -0,0 +1,142 @@ +{ + "definitions": { + "standards": [ + { + "bom-ref": "other-standard", + "description": "Other description", + "externalReferences": [ + { + "type": "website", + "url": "https://cyclonedx.org" + } + ], + "levels": [ + { + "bom-ref": "lvl-3", + "description": "Level 3 description", + "identifier": "LVL-3", + "title": "Level 3" + } + ], + "name": "Other Standard", + "owner": "Other Owner", + "requirements": [ + { + "bom-ref": "req-3", + "descriptions": [ + "Requirement 3 described here", + "and here" + ], + "identifier": "REQ-3", + "openCre": [ + "CRE:5-6", + "CRE:7-8" + ], + "properties": [ + { + "name": "key3", + "value": "val3" + } + ], + "text": "some requirement text", + "title": "Requirement 3" + } + ], + "version": "1.0.0" + }, + { + "bom-ref": "some-standard", + "description": "Some description", + "externalReferences": [ + { + "comment": "No comment", + "hashes": [ + { + "alg": "SHA-256", + "content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b" + } + ], + "type": "distribution", + "url": "https://cyclonedx.org" + } + ], + "levels": [ + { + "bom-ref": "lvl-1", + "description": "Level 1 description", + "identifier": "LVL-1", + "title": "Level 1" + }, + { + "bom-ref": "lvl-2", + "description": "Level 2 description", + "identifier": "LVL-2", + "title": "Level 2" + } + ], + "name": "Some Standard", + "owner": "Some Owner", + "requirements": [ + { + "bom-ref": "req-1", + "descriptions": [ + "Requirement 1 described here", + "and here" + ], + "identifier": "REQ-1", + "openCre": [ + "CRE:1-2" + ], + "properties": [ + { + "name": "key1", + "value": "val1" + } + ], + "text": "some requirement text", + "title": "Requirement 1" + }, + { + "bom-ref": "req-2", + "descriptions": [ + "Requirement 2 described here" + ], + "identifier": "REQ-2", + "openCre": [ + "CRE:1-2", + "CRE:3-4" + ], + "parent": "req-1", + "properties": [ + { + "name": "key2", + "value": "val2" + } + ], + "text": "some requirement text", + "title": "Requirement 2" + } + ], + "version": "1.2.3" + } + ] + }, + "metadata": { + "timestamp": "2023-01-07T13:44:32.312678+00:00" + }, + "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/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin new file mode 100644 index 00000000..2ba3df70 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin @@ -0,0 +1,104 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + val1 + val2 + + + + + Other Standard + 1.0.0 + Other description + Other Owner + + + REQ-3 + Requirement 3 + some requirement text + + Requirement 3 described here + and here + + CRE:5-6 + CRE:7-8 + + val3 + + + + + + LVL-3 + Level 3 + Level 3 description + + + + + https://cyclonedx.org + + + + + Some Standard + 1.2.3 + Some description + Some Owner + + + REQ-1 + Requirement 1 + some requirement text + + Requirement 1 described here + and here + + CRE:1-2 + + val1 + + + + REQ-2 + Requirement 2 + some requirement text + + Requirement 2 described here + + CRE:1-2 + CRE:3-4 + req-1 + + val2 + + + + + + LVL-1 + Level 1 + Level 1 description + + + LVL-2 + Level 2 + Level 2 description + + + + + https://cyclonedx.org + No comment + + 806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b + + + + + + + From 40955f54878af188bb84d643c6baabe0ea23458f Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Wed, 15 Jan 2025 17:30:37 +0100 Subject: [PATCH 03/20] tests Signed-off-by: Jan Kowalleck --- tests/_data/models.py | 121 ++++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 41 deletions(-) diff --git a/tests/_data/models.py b/tests/_data/models.py index 0c79b530..f16775df 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -1303,9 +1303,14 @@ def get_bom_with_definitions_standards() -> Bom: """ return _make_bom( definitions=Definitions(standards=[ - Standard(name='Some Standard', version='1.2.3', description='Some description', bom_ref='some-standard', - owner='Some Owner', external_references=[get_external_reference_2()] - ) + Standard( + bom_ref='some-standard', + name='Some Standard', + version='1.2.3', + description='Some description', + owner='Some Owner', + external_references=[get_external_reference_2()] + ) ]) ) @@ -1315,44 +1320,78 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: Returns a BOM with definitions and multiple detailed standards including requirements and levels. """ return _make_bom( - definitions=Definitions( - standards=[ - Standard(name='Some Standard', version='1.2.3', description='Some description', bom_ref='some-standard', - owner='Some Owner', external_references=[get_external_reference_1()], - requirements=[ - Requirement(identifier='REQ-1', title='Requirement 1', text='some requirement text', - bom_ref='req-1', descriptions=['Requirement 1 described here', 'and here'], - open_cre=[CreId('CRE:1-2')], properties=[Property(name='key1', value='val1')] - ), - Requirement(identifier='REQ-2', title='Requirement 2', text='some requirement text', - bom_ref='req-2', descriptions=['Requirement 2 described here'], - open_cre=[CreId('CRE:1-2'), CreId('CRE:3-4')], - properties=[Property(name='key2', value='val2')], - parent='req-1' - ), - ], - levels=[ - Level(identifier='LVL-1', title='Level 1', description='Level 1 description', - bom_ref='lvl-1', ), - Level(identifier='LVL-2', title='Level 2', description='Level 2 description', - bom_ref='lvl-2', ) - ]), - Standard(name='Other Standard', version='1.0.0', description='Other description', - bom_ref='other-standard', owner='Other Owner', - external_references=[get_external_reference_2()], - requirements=[ - Requirement(identifier='REQ-3', title='Requirement 3', text='some requirement text', - bom_ref='req-3', descriptions=['Requirement 3 described here', 'and here'], - open_cre=[CreId('CRE:5-6'), CreId('CRE:7-8')], - properties=[Property(name='key3', value='val3')] - ) - ], - levels=[ - Level(identifier='LVL-3', title='Level 3', description='Level 3 description', - bom_ref='lvl-3', ) - ]) - ] - )) + definitions=Definitions(standards=[ + Standard( + bom_ref='some-standard', + name='Some Standard', + version='1.2.3', + description='Some description', + owner='Some Owner', + external_references=[get_external_reference_1()], + requirements=[ + Requirement( + bom_ref='req-1', + identifier='REQ-1', + title='Requirement 1', + text='some requirement text', + descriptions=['Requirement 1 described here', 'and here'], + open_cre=[CreId('CRE:1-2')], + properties=[Property(name='key1', value='val1')] + ), + Requirement( + bom_ref='req-2', + identifier='REQ-2', + title='Requirement 2', + text='some requirement text', + descriptions=['Requirement 2 described here'], + open_cre=[CreId('CRE:1-2'), CreId('CRE:3-4')], + properties=[Property(name='key2', value='val2')], + parent='req-1' + ), + ], + levels=[ + Level( + bom_ref='lvl-1', + identifier='LVL-1', + title='Level 1', + description='Level 1 description' + ), + Level( + bom_ref='lvl-2', + identifier='LVL-2', + title='Level 2', + description='Level 2 description' + ) + ] + ), + Standard( + bom_ref='other-standard', + name='Other Standard', + version='1.0.0', + description='Other description', + owner='Other Owner', + external_references=[get_external_reference_2()], + requirements=[ + Requirement( + bom_ref='req-3', + identifier='REQ-3', + title='Requirement 3', + text='some requirement text', + descriptions=['Requirement 3 described here', 'and here'], + open_cre=[CreId('CRE:5-6'), CreId('CRE:7-8')], + properties=[Property(name='key3', value='val3')] + ) + ], + levels=[ + Level( + bom_ref='lvl-3', + identifier='LVL-3', + title='Level 3', + description='Level 3 description' + ) + ] + ) + ])) # --- From 604e27837b92c1cba638ed87c7e0337740075b02 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Wed, 15 Jan 2025 17:39:37 +0100 Subject: [PATCH 04/20] tests Signed-off-by: Jan Kowalleck --- tests/test_model_definition.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/test_model_definition.py b/tests/test_model_definition.py index 8c5ba123..e3f8e154 100644 --- a/tests/test_model_definition.py +++ b/tests/test_model_definition.py @@ -18,6 +18,8 @@ from unittest import TestCase +from ddt import ddt, named_data + from cyclonedx.exception.model import InvalidCreIdException from cyclonedx.model.definition import CreId, Definitions, Level, Requirement, Standard @@ -29,13 +31,13 @@ def test_init(self) -> Definitions: dr = Definitions( standards=(s, ), ) + self.assertEqual(1, len(dr.standards)) self.assertIs(s, tuple(dr.standards)[0]) return dr def test_filled(self) -> None: dr = self.test_init() self.assertIsNotNone(dr.standards) - self.assertEqual(1, len(dr.standards)) self.assertTrue(dr) def test_empty(self) -> None: @@ -68,6 +70,7 @@ def test_equal(self) -> None: self.assertTrue(dr1 == tr2) +@ddt class TestModelCreId(TestCase): def test_different(self) -> None: @@ -84,21 +87,21 @@ def test_same(self) -> None: self.assertEqual(hash(id1), hash(id2)) self.assertTrue(id1 == id2) - def test_invalid_id(self) -> None: + def test_invalid_no_id(self) -> None: with self.assertRaises(TypeError): CreId() + + @named_data( + ['empty', ''], + ['arbitrary string', 'some string'], + ['missing prefix', '123-456'], + ['additional part', 'CRE:123-456-789'], + ['no numbers', 'CRE:abc-def'], + ['no delimiter', 'CRE:123456'], + ) + def test_invalid_id(self, wrong_id) -> None: with self.assertRaises(InvalidCreIdException): - CreId('') - with self.assertRaises(InvalidCreIdException): - CreId('some string') - with self.assertRaises(InvalidCreIdException): - CreId('123-456') - with self.assertRaises(InvalidCreIdException): - CreId('CRE:123-456-789') - with self.assertRaises(InvalidCreIdException): - CreId('CRE:abc-def') - with self.assertRaises(InvalidCreIdException): - CreId('CRE:123456') + CreId(wrong_id) class TestModelRequirements(TestCase): From 62ad85c39546a946091d7a2049d9713274817640 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Wed, 15 Jan 2025 19:21:39 +0100 Subject: [PATCH 05/20] impov Signed-off-by: Jan Kowalleck --- cyclonedx/model/definition.py | 90 ++++++++++++------- ...nitions_and_detailed_standards-1.6.xml.bin | 68 +++++++------- 2 files changed, 91 insertions(+), 67 deletions(-) diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index 9f444fce..3bbfb8e5 100644 --- a/cyclonedx/model/definition.py +++ b/cyclonedx/model/definition.py @@ -21,7 +21,7 @@ import serializable from sortedcontainers import SortedSet -from .._internal.bom_ref import bom_ref_from_str +from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str from .._internal.compare import ComparableTuple as _ComparableTuple from ..exception.model import InvalidCreIdException from ..exception.serialization import SerializationOfUnexpectedValueException @@ -69,7 +69,7 @@ def deserialize(cls, o: Any) -> 'CreId': def __eq__(self, other: Any) -> bool: if isinstance(other, CreId): - return hash(other) == hash(self) + return self._id == other._id return False def __lt__(self, other: Any) -> bool: @@ -105,20 +105,28 @@ def __init__( properties: Optional[Iterable[Property]] = None, external_references: Optional[Iterable[ExternalReference]] = None, ) -> None: - self._bom_ref = bom_ref_from_str(bom_ref) + self._bom_ref = _bom_ref_from_str(bom_ref) self.identifier = identifier self.title = title self.text = text - self.descriptions = descriptions or [] # type:ignore[assignment] - self.open_cre = open_cre or [] # type:ignore[assignment] - self.parent = bom_ref_from_str(parent) - self.properties = properties or [] # type:ignore[assignment] - self.external_references = external_references or [] # type:ignore[assignment] + self.descriptions = descriptions or () # type:ignore[assignment] + self.open_cre = open_cre or () # type:ignore[assignment] + self.parent = parent + self.properties = properties or () # type:ignore[assignment] + self.external_references = external_references or () # type:ignore[assignment] def __lt__(self, other: Any) -> bool: if isinstance(other, Requirement): - return (_ComparableTuple((self.bom_ref, self.identifier)) - < _ComparableTuple((other.bom_ref, other.title))) + # all properties are optional - so need to compare all, in hope that one is unique + return _ComparableTuple(( + self.bom_ref, self.identifier, self.title, self.text, _ComparableTuple(self.descriptions), + _ComparableTuple(self.open_cre), self.parent, _ComparableTuple(self.properties), + _ComparableTuple(self.external_references) + )) < _ComparableTuple(( + other.bom_ref, other.identifier, other.title, other.text, _ComparableTuple(other.descriptions), + _ComparableTuple(other.open_cre), other.parent, _ComparableTuple(other.properties), + _ComparableTuple(other.external_references) + )) return NotImplemented def __eq__(self, other: object) -> bool: @@ -127,14 +135,15 @@ def __eq__(self, other: object) -> bool: return False def __hash__(self) -> int: + # all properties are optional - so need to apply all, in hope that one is unique return hash(( self.bom_ref, self.identifier, self.title, self.text, tuple(self.descriptions), tuple(self.open_cre), self.parent, tuple(self.properties), tuple(self.external_references) )) def __repr__(self) -> str: - return f'' + return f'' @property @serializable.json_name('bom-ref') @@ -237,7 +246,7 @@ def parent(self) -> Optional[BomRef]: @parent.setter def parent(self, parent: Optional[Union[str, BomRef]]) -> None: - self._parent = bom_ref_from_str(parent) + self._parent = _bom_ref_from_str(parent) @property @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property') @@ -288,16 +297,20 @@ def __init__( description: Optional[str] = None, requirements: Optional[Iterable[Union[str, BomRef]]] = None, ) -> None: - self._bom_ref = bom_ref_from_str(bom_ref) + self._bom_ref = _bom_ref_from_str(bom_ref) self.identifier = identifier self.title = title self.description = description - self.requirements = requirements or [] # type:ignore[assignment] + self.requirements = requirements or () # type:ignore[assignment] def __lt__(self, other: Any) -> bool: if isinstance(other, Level): - return (_ComparableTuple((self.bom_ref, self.identifier)) - < _ComparableTuple((other.bom_ref, other.identifier))) + # all properties are optional - so need to compare all, in hope that one is unique + return _ComparableTuple(( + self.bom_ref, self.identifier, self.title, self.description, _ComparableTuple(self.requirements) + )) < _ComparableTuple(( + other.bom_ref, other.identifier, other.title, other.description, _ComparableTuple(other.requirements) + )) return NotImplemented def __eq__(self, other: object) -> bool: @@ -306,6 +319,7 @@ def __eq__(self, other: object) -> bool: return False def __hash__(self) -> int: + # all properties are optional - so need to compare all, in hope that one is unique return hash(( self.bom_ref, self.identifier, self.title, self.description, tuple(self.requirements) )) @@ -380,7 +394,7 @@ def requirements(self) -> 'SortedSet[BomRef]': @requirements.setter def requirements(self, requirements: Iterable[Union[str, BomRef]]) -> None: - self._requirements = SortedSet(map(lambda x: bom_ref_from_str(x), requirements)) + self._requirements = SortedSet(map(_bom_ref_from_str, requirements)) @serializable.serializable_class @@ -401,19 +415,27 @@ def __init__( levels: Optional[Iterable[Level]] = None, external_references: Optional[Iterable['ExternalReference']] = None ) -> None: - self._bom_ref = bom_ref_from_str(bom_ref) + self._bom_ref = _bom_ref_from_str(bom_ref) self.name = name self.version = version self.description = description self.owner = owner - self.requirements = requirements or [] # type:ignore[assignment] - self.levels = levels or [] # type:ignore[assignment] - self.external_references = external_references or [] # type:ignore[assignment] + self.requirements = requirements or () # type:ignore[assignment] + self.levels = levels or () # type:ignore[assignment] + self.external_references = external_references or () # type:ignore[assignment] def __lt__(self, other: Any) -> bool: if isinstance(other, Standard): - return (_ComparableTuple((self.bom_ref, self.name, self.version)) - < _ComparableTuple((other.bom_ref, other.name, other.version))) + # all properties are optional - so need to apply all, in hope that one is unique + return _ComparableTuple(( + self.bom_ref, self.name, self.version, self.description, self.owner, + _ComparableTuple(self.requirements), _ComparableTuple(self.levels), + _ComparableTuple(self.external_references) + )) < _ComparableTuple(( + self.bom_ref, self.name, self.version, self.description, self.owner, + _ComparableTuple(self.requirements), _ComparableTuple(self.levels), + _ComparableTuple(self.external_references) + )) return NotImplemented def __eq__(self, other: object) -> bool: @@ -422,8 +444,10 @@ def __eq__(self, other: object) -> bool: return False def __hash__(self) -> int: + # all properties are optional - so need to apply all, in hope that one is unique return hash(( - self.bom_ref, self.name, self.version, self.description, self.owner, tuple(self.external_references) + self.bom_ref, self.name, self.version, self.description, self.owner, + tuple(self.requirements), tuple(self.levels), tuple(self.external_references) )) def __repr__(self) -> str: @@ -570,19 +594,19 @@ def __bool__(self) -> bool: return len(self._standards) > 0 def __eq__(self, other: object) -> bool: - if not isinstance(other, Definitions): - return False - - return self._standards == other._standards + if isinstance(other, Definitions): + return hash(self) == hash(other) + return False def __hash__(self) -> int: - return hash((tuple(self._standards))) + # all properties are optional - so need to apply all, in hope that one is unique + return hash(tuple(self._standards)) def __lt__(self, other: Any) -> bool: if isinstance(other, Definitions): - return (_ComparableTuple(self._standards) - < _ComparableTuple(other.standards)) + # all properties are optional - so need to apply all, in hope that one is unique + return _ComparableTuple(self._standards) < _ComparableTuple(other._standards) return NotImplemented def __repr__(self) -> str: - return '' + return f'' diff --git a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin index 2ba3df70..b878f796 100644 --- a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin @@ -9,40 +9,6 @@ - - Other Standard - 1.0.0 - Other description - Other Owner - - - REQ-3 - Requirement 3 - some requirement text - - Requirement 3 described here - and here - - CRE:5-6 - CRE:7-8 - - val3 - - - - - - LVL-3 - Level 3 - Level 3 description - - - - - https://cyclonedx.org - - - Some Standard 1.2.3 @@ -99,6 +65,40 @@ + + Other Standard + 1.0.0 + Other description + Other Owner + + + REQ-3 + Requirement 3 + some requirement text + + Requirement 3 described here + and here + + CRE:5-6 + CRE:7-8 + + val3 + + + + + + LVL-3 + Level 3 + Level 3 description + + + + + https://cyclonedx.org + + + From c1f4531356fb114bf53cb3e5f69994831eeaa15f Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Fri, 17 Jan 2025 12:24:51 +0100 Subject: [PATCH 06/20] unders Signed-off-by: Jan Kowalleck --- cyclonedx/model/definition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index 3bbfb8e5..0d0281ab 100644 --- a/cyclonedx/model/definition.py +++ b/cyclonedx/model/definition.py @@ -142,8 +142,8 @@ def __hash__(self) -> int: )) def __repr__(self) -> str: - return f'' + return f'' @property @serializable.json_name('bom-ref') @@ -609,4 +609,4 @@ def __lt__(self, other: Any) -> bool: return NotImplemented def __repr__(self) -> str: - return f'' + return f'' From 44741692d25ae4a127f614a5ead3d94c858d00e9 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Sat, 18 Jan 2025 17:25:16 +0100 Subject: [PATCH 07/20] wip Signed-off-by: Jan Kowalleck --- cyclonedx/model/definition.py | 91 +++++++++--------- .../get_bom_v1_6_with_crypto-1.6.xml.bin | 92 ------------------- .../snapshots/get_bom_with_crypto-1.6.xml.bin | 92 ------------------- ...nitions_and_detailed_standards-1.6.xml.bin | 68 +++++++------- 4 files changed, 76 insertions(+), 267 deletions(-) delete mode 100644 tests/_data/snapshots/get_bom_v1_6_with_crypto-1.6.xml.bin delete mode 100644 tests/_data/snapshots/get_bom_with_crypto-1.6.xml.bin diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index 0d0281ab..3c910087 100644 --- a/cyclonedx/model/definition.py +++ b/cyclonedx/model/definition.py @@ -115,31 +115,28 @@ def __init__( self.properties = properties or () # type:ignore[assignment] self.external_references = external_references or () # type:ignore[assignment] + def __comparable_tuple(self) -> _ComparableTuple: + # all properties are optional - so need to compare all, in hope that one is unique + return _ComparableTuple(( + self.bom_ref, self.identifier, + self.title, self.text, + _ComparableTuple(self.descriptions), + _ComparableTuple(self.open_cre), self.parent, _ComparableTuple(self.properties), + _ComparableTuple(self.external_references) + )) + def __lt__(self, other: Any) -> bool: if isinstance(other, Requirement): - # all properties are optional - so need to compare all, in hope that one is unique - return _ComparableTuple(( - self.bom_ref, self.identifier, self.title, self.text, _ComparableTuple(self.descriptions), - _ComparableTuple(self.open_cre), self.parent, _ComparableTuple(self.properties), - _ComparableTuple(self.external_references) - )) < _ComparableTuple(( - other.bom_ref, other.identifier, other.title, other.text, _ComparableTuple(other.descriptions), - _ComparableTuple(other.open_cre), other.parent, _ComparableTuple(other.properties), - _ComparableTuple(other.external_references) - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __eq__(self, other: object) -> bool: if isinstance(other, Requirement): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - # all properties are optional - so need to apply all, in hope that one is unique - return hash(( - self.bom_ref, self.identifier, self.title, self.text, tuple(self.descriptions), - tuple(self.open_cre), self.parent, tuple(self.properties), tuple(self.external_references) - )) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f' Optional[BomRef]: @parent.setter def parent(self, parent: Optional[Union[str, BomRef]]) -> None: - self._parent = _bom_ref_from_str(parent) + self._parent = _bom_ref_from_str(parent, optional=True) @property @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'property') @@ -303,26 +300,24 @@ def __init__( self.description = description self.requirements = requirements or () # type:ignore[assignment] + def __comparable_tuple(self) -> _ComparableTuple: + # all properties are optional - so need to compare all, in hope that one is unique + return _ComparableTuple(( + self.bom_ref, self.identifier, self.title, self.description, _ComparableTuple(self.requirements) + )) + def __lt__(self, other: Any) -> bool: if isinstance(other, Level): - # all properties are optional - so need to compare all, in hope that one is unique - return _ComparableTuple(( - self.bom_ref, self.identifier, self.title, self.description, _ComparableTuple(self.requirements) - )) < _ComparableTuple(( - other.bom_ref, other.identifier, other.title, other.description, _ComparableTuple(other.requirements) - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __eq__(self, other: object) -> bool: if isinstance(other, Level): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - # all properties are optional - so need to compare all, in hope that one is unique - return hash(( - self.bom_ref, self.identifier, self.title, self.description, tuple(self.requirements) - )) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f' _ComparableTuple: + # all properties are optional - so need to apply all, in hope that one is unique + return _ComparableTuple(( + self.bom_ref, + self.name, self.version, self.description, self.owner, + _ComparableTuple(self.requirements), _ComparableTuple(self.levels), + _ComparableTuple(self.external_references) + )) + def __lt__(self, other: Any) -> bool: if isinstance(other, Standard): - # all properties are optional - so need to apply all, in hope that one is unique - return _ComparableTuple(( - self.bom_ref, self.name, self.version, self.description, self.owner, - _ComparableTuple(self.requirements), _ComparableTuple(self.levels), - _ComparableTuple(self.external_references) - )) < _ComparableTuple(( - self.bom_ref, self.name, self.version, self.description, self.owner, - _ComparableTuple(self.requirements), _ComparableTuple(self.levels), - _ComparableTuple(self.external_references) - )) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __eq__(self, other: object) -> bool: if isinstance(other, Standard): - return hash(other) == hash(self) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - # all properties are optional - so need to apply all, in hope that one is unique - return hash(( - self.bom_ref, self.name, self.version, self.description, self.owner, - tuple(self.requirements), tuple(self.levels), tuple(self.external_references) - )) + return hash(self.__comparable_tuple()) def __repr__(self) -> str: return f' None: def __bool__(self) -> bool: return len(self._standards) > 0 + def __comparable_tuple(self) -> _ComparableTuple: + # all properties are optional - so need to apply all, in hope that one is unique + return _ComparableTuple(self._standards) + def __eq__(self, other: object) -> bool: if isinstance(other, Definitions): - return hash(self) == hash(other) + return self.__comparable_tuple() == other.__comparable_tuple() return False def __hash__(self) -> int: - # all properties are optional - so need to apply all, in hope that one is unique - return hash(tuple(self._standards)) + return hash(self.__comparable_tuple()) def __lt__(self, other: Any) -> bool: if isinstance(other, Definitions): - # all properties are optional - so need to apply all, in hope that one is unique - return _ComparableTuple(self._standards) < _ComparableTuple(other._standards) + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __repr__(self) -> str: diff --git a/tests/_data/snapshots/get_bom_v1_6_with_crypto-1.6.xml.bin b/tests/_data/snapshots/get_bom_v1_6_with_crypto-1.6.xml.bin deleted file mode 100644 index 72618139..00000000 --- a/tests/_data/snapshots/get_bom_v1_6_with_crypto-1.6.xml.bin +++ /dev/null @@ -1,92 +0,0 @@ - - - - 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 - - - - - - - - TLS - v1.3 - - protocol - - tls - 1.3 - - - TLS_AES_128_CCM_8_SHA256 - - TLS_AES_128_CCM_8_SHA256 - - - - TLS_AES_128_CCM_SHA256 - - TLS_AES_128_CCM_SHA256 - - - - TLS_AES_128_GCM_SHA256 - - TLS_AES_128_GCM_SHA256 - - - - TLS_AES_256_GCM_SHA384 - - TLS_AES_256_GCM_SHA384 - - - - TLS_CHACHA20_POLY1305_SHA256 - - TLS_CHACHA20_POLY1305_SHA256 - - - - - an-oid-here - - - protocl - tls - - - - - - - diff --git a/tests/_data/snapshots/get_bom_with_crypto-1.6.xml.bin b/tests/_data/snapshots/get_bom_with_crypto-1.6.xml.bin deleted file mode 100644 index 72618139..00000000 --- a/tests/_data/snapshots/get_bom_with_crypto-1.6.xml.bin +++ /dev/null @@ -1,92 +0,0 @@ - - - - 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 - - - - - - - - TLS - v1.3 - - protocol - - tls - 1.3 - - - TLS_AES_128_CCM_8_SHA256 - - TLS_AES_128_CCM_8_SHA256 - - - - TLS_AES_128_CCM_SHA256 - - TLS_AES_128_CCM_SHA256 - - - - TLS_AES_128_GCM_SHA256 - - TLS_AES_128_GCM_SHA256 - - - - TLS_AES_256_GCM_SHA384 - - TLS_AES_256_GCM_SHA384 - - - - TLS_CHACHA20_POLY1305_SHA256 - - TLS_CHACHA20_POLY1305_SHA256 - - - - - an-oid-here - - - protocl - tls - - - - - - - diff --git a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin index b878f796..2ba3df70 100644 --- a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin @@ -9,6 +9,40 @@ + + Other Standard + 1.0.0 + Other description + Other Owner + + + REQ-3 + Requirement 3 + some requirement text + + Requirement 3 described here + and here + + CRE:5-6 + CRE:7-8 + + val3 + + + + + + LVL-3 + Level 3 + Level 3 description + + + + + https://cyclonedx.org + + + Some Standard 1.2.3 @@ -65,40 +99,6 @@ - - Other Standard - 1.0.0 - Other description - Other Owner - - - REQ-3 - Requirement 3 - some requirement text - - Requirement 3 described here - and here - - CRE:5-6 - CRE:7-8 - - val3 - - - - - - LVL-3 - Level 3 - Level 3 description - - - - - https://cyclonedx.org - - - From dd5f563130778e8e285777c17c25977f2059175d Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Sat, 18 Jan 2025 17:40:57 +0100 Subject: [PATCH 08/20] wip Signed-off-by: Jan Kowalleck --- tests/_data/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/_data/models.py b/tests/_data/models.py index f16775df..32518d51 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -1329,7 +1329,7 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: owner='Some Owner', external_references=[get_external_reference_1()], requirements=[ - Requirement( + req1 := Requirement( bom_ref='req-1', identifier='REQ-1', title='Requirement 1', @@ -1346,7 +1346,7 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: descriptions=['Requirement 2 described here'], open_cre=[CreId('CRE:1-2'), CreId('CRE:3-4')], properties=[Property(name='key2', value='val2')], - parent='req-1' + parent=req1.bom_ref ), ], levels=[ From e9f2f9c3e5a31efe9358dd8415919a32869203ce Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Sat, 18 Jan 2025 17:41:34 +0100 Subject: [PATCH 09/20] wip Signed-off-by: Jan Kowalleck --- cyclonedx/model/component.py | 2 +- cyclonedx/model/definition.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index f553d5c0..08e86f57 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -1789,4 +1789,4 @@ def __hash__(self) -> int: def __repr__(self) -> str: return f'' + f'version={self.version}, type={self.type}>' diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index 3c910087..c270e7ac 100644 --- a/cyclonedx/model/definition.py +++ b/cyclonedx/model/definition.py @@ -140,7 +140,7 @@ def __hash__(self) -> int: def __repr__(self) -> str: return f'' + f'text={self.text}, parent={self.parent}>' @property @serializable.json_name('bom-ref') @@ -308,7 +308,7 @@ def __comparable_tuple(self) -> _ComparableTuple: def __lt__(self, other: Any) -> bool: if isinstance(other, Level): - return self.__comparable_tuple() < other.__comparable_tuple() + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __eq__(self, other: object) -> bool: @@ -321,7 +321,7 @@ def __hash__(self) -> int: def __repr__(self) -> str: return f'' + f'description={self.description}>' @property @serializable.json_name('bom-ref') @@ -443,7 +443,7 @@ def __hash__(self) -> int: def __repr__(self) -> str: return f'' + f'description={self.description}, owner={self.owner}>' @property @serializable.json_name('bom-ref') @@ -598,7 +598,7 @@ def __hash__(self) -> int: def __lt__(self, other: Any) -> bool: if isinstance(other, Definitions): - return self.__comparable_tuple() < other.__comparable_tuple() + return self.__comparable_tuple() < other.__comparable_tuple() return NotImplemented def __repr__(self) -> str: From 699aaff4af436536de3727421bba9997e10f817c Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Sat, 18 Jan 2025 17:51:00 +0100 Subject: [PATCH 10/20] wip Signed-off-by: Jan Kowalleck --- cyclonedx/model/definition.py | 2 +- tests/test_model_definition.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index c270e7ac..dcafd347 100644 --- a/cyclonedx/model/definition.py +++ b/cyclonedx/model/definition.py @@ -111,7 +111,7 @@ def __init__( self.text = text self.descriptions = descriptions or () # type:ignore[assignment] self.open_cre = open_cre or () # type:ignore[assignment] - self.parent = parent + self.parent = parent # type:ignore[assignment] self.properties = properties or () # type:ignore[assignment] self.external_references = external_references or () # type:ignore[assignment] diff --git a/tests/test_model_definition.py b/tests/test_model_definition.py index e3f8e154..cf026bbc 100644 --- a/tests/test_model_definition.py +++ b/tests/test_model_definition.py @@ -99,7 +99,7 @@ def test_invalid_no_id(self) -> None: ['no numbers', 'CRE:abc-def'], ['no delimiter', 'CRE:123456'], ) - def test_invalid_id(self, wrong_id) -> None: + def test_invalid_id(self, wrong_id: str) -> None: with self.assertRaises(InvalidCreIdException): CreId(wrong_id) From fc57e1d89c9d8a215e155afaa37bb0373efb1a0a Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Sat, 18 Jan 2025 18:39:03 +0100 Subject: [PATCH 11/20] wip Signed-off-by: Jan Kowalleck --- cyclonedx/model/bom_ref.py | 36 +++++++++++++++++-- cyclonedx/serialization/__init__.py | 14 ++------ tests/__init__.py | 2 +- tests/_data/models.py | 11 +++--- ...itions_and_detailed_standards-1.6.json.bin | 7 ++++ ...nitions_and_detailed_standards-1.6.xml.bin | 7 ++++ 6 files changed, 59 insertions(+), 18 deletions(-) diff --git a/cyclonedx/model/bom_ref.py b/cyclonedx/model/bom_ref.py index faf47cf4..39f0b30e 100644 --- a/cyclonedx/model/bom_ref.py +++ b/cyclonedx/model/bom_ref.py @@ -16,10 +16,20 @@ # Copyright (c) OWASP Foundation. All Rights Reserved. -from typing import Any, Optional +from typing import TYPE_CHECKING, Any, Optional +import serializable +from serializable.helpers import BaseHelper -class BomRef: +from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException + +if TYPE_CHECKING: # pragma: no cover + from typing import TypeVar, Type + + _T = TypeVar('_T', bound='BomRef') + +@serializable.serializable_class +class BomRef(BaseHelper): """ An identifier that can be used to reference objects elsewhere in the BOM. @@ -33,6 +43,8 @@ def __init__(self, value: Optional[str] = None) -> None: self.value = value @property + @serializable.json_name('.') + @serializable.xml_name('.') def value(self) -> Optional[str]: return self._value @@ -67,3 +79,23 @@ def __str__(self) -> str: def __bool__(self) -> bool: return self._value is not None + + # region impl BaseHelper + + @classmethod + def serialize(cls, o: Any) -> Optional[str]: + if isinstance(o, cls): + return o.value + raise SerializationOfUnexpectedValueException( + f'Attempt to serialize a non-BomRef: {o!r}') + + @classmethod + def deserialize(cls: 'Type[_T]', o: Any) -> '_T': + try: + return cls(value=str(o)) + except ValueError as err: + raise CycloneDxDeserializationException( + f'BomRef string supplied does not parse: {o!r}' + ) from err + + # endregion impl BaseHelper diff --git a/cyclonedx/serialization/__init__.py b/cyclonedx/serialization/__init__.py index 427d0bf6..98781e2b 100644 --- a/cyclonedx/serialization/__init__.py +++ b/cyclonedx/serialization/__init__.py @@ -36,24 +36,16 @@ if TYPE_CHECKING: # pragma: no cover from serializable import ViewType - +#TODO: remove, no longer needed class BomRefHelper(BaseHelper): @classmethod def serialize(cls, o: Any) -> Optional[str]: - if isinstance(o, BomRef): - return o.value - raise SerializationOfUnexpectedValueException( - f'Attempt to serialize a non-BomRef: {o!r}') + return BomRef.serialize(o) @classmethod def deserialize(cls, o: Any) -> BomRef: - try: - return BomRef(value=str(o)) - except ValueError as err: - raise CycloneDxDeserializationException( - f'BomRef string supplied does not parse: {o!r}' - ) from err + return BomRef.deserialize(o) class PackageUrl(BaseHelper): diff --git a/tests/__init__.py b/tests/__init__.py index 93111a22..7e54c5ea 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -39,7 +39,7 @@ OWN_DATA_DIRECTORY = path.join(_TESTDATA_DIRECTORY, 'own') SNAPSHOTS_DIRECTORY = path.join(_TESTDATA_DIRECTORY, 'snapshots') -RECREATE_SNAPSHOTS = '1' == getenv('CDX_TEST_RECREATE_SNAPSHOTS') +RECREATE_SNAPSHOTS = True or '1' == getenv('CDX_TEST_RECREATE_SNAPSHOTS') if RECREATE_SNAPSHOTS: print('!!! WILL RECREATE ALL SNAPSHOTS !!!') diff --git a/tests/_data/models.py b/tests/_data/models.py index 32518d51..4fb89b72 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -1338,7 +1338,7 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: open_cre=[CreId('CRE:1-2')], properties=[Property(name='key1', value='val1')] ), - Requirement( + req2 := Requirement( bom_ref='req-2', identifier='REQ-2', title='Requirement 2', @@ -1355,12 +1355,14 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: identifier='LVL-1', title='Level 1', description='Level 1 description' + # no requirements! ), Level( bom_ref='lvl-2', identifier='LVL-2', title='Level 2', - description='Level 2 description' + description='Level 2 description', + requirements=[req1.bom_ref, req2.bom_ref] ) ] ), @@ -1372,7 +1374,7 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: owner='Other Owner', external_references=[get_external_reference_2()], requirements=[ - Requirement( + req3 := Requirement( bom_ref='req-3', identifier='REQ-3', title='Requirement 3', @@ -1387,7 +1389,8 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: bom_ref='lvl-3', identifier='LVL-3', title='Level 3', - description='Level 3 description' + description='Level 3 description', + requirements=[req3.bom_ref] ) ] ) diff --git a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.json.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.json.bin index 4dd3ed7c..d0ef4761 100644 --- a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.json.bin +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.json.bin @@ -15,6 +15,9 @@ "bom-ref": "lvl-3", "description": "Level 3 description", "identifier": "LVL-3", + "requirements": [ + "req-3" + ], "title": "Level 3" } ], @@ -71,6 +74,10 @@ "bom-ref": "lvl-2", "description": "Level 2 description", "identifier": "LVL-2", + "requirements": [ + "req-1", + "req-2" + ], "title": "Level 2" } ], diff --git a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin index 2ba3df70..975fd69d 100644 --- a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin @@ -35,6 +35,9 @@ LVL-3 Level 3 Level 3 description + + req-3 + @@ -87,6 +90,10 @@ LVL-2 Level 2 Level 2 description + + req-1 + req-2 + From b61a127da88a6f26ad64905c066648f14d4678a0 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Sat, 18 Jan 2025 18:40:41 +0100 Subject: [PATCH 12/20] wip Signed-off-by: Jan Kowalleck --- cyclonedx/model/bom_ref.py | 3 ++- cyclonedx/serialization/__init__.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cyclonedx/model/bom_ref.py b/cyclonedx/model/bom_ref.py index 39f0b30e..07147af1 100644 --- a/cyclonedx/model/bom_ref.py +++ b/cyclonedx/model/bom_ref.py @@ -24,10 +24,11 @@ from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException if TYPE_CHECKING: # pragma: no cover - from typing import TypeVar, Type + from typing import Type, TypeVar _T = TypeVar('_T', bound='BomRef') + @serializable.serializable_class class BomRef(BaseHelper): """ diff --git a/cyclonedx/serialization/__init__.py b/cyclonedx/serialization/__init__.py index 98781e2b..9b4e278a 100644 --- a/cyclonedx/serialization/__init__.py +++ b/cyclonedx/serialization/__init__.py @@ -36,8 +36,9 @@ if TYPE_CHECKING: # pragma: no cover from serializable import ViewType -#TODO: remove, no longer needed + class BomRefHelper(BaseHelper): + # TODO: remove, no longer needed @classmethod def serialize(cls, o: Any) -> Optional[str]: From c09a376c1376a6a1daeb00e072b4a3e7ab765531 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Sat, 18 Jan 2025 18:42:57 +0100 Subject: [PATCH 13/20] wip Signed-off-by: Jan Kowalleck --- cyclonedx/model/definition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index dcafd347..ba6284c8 100644 --- a/cyclonedx/model/definition.py +++ b/cyclonedx/model/definition.py @@ -389,7 +389,8 @@ def requirements(self) -> 'SortedSet[BomRef]': @requirements.setter def requirements(self, requirements: Iterable[Union[str, BomRef]]) -> None: - self._requirements = SortedSet(map(_bom_ref_from_str, requirements)) + self._requirements = SortedSet(map(_bom_ref_from_str, # type: ignore[arg-type] + requirements)) @serializable.serializable_class From e1041489f9c962e7d4b78734f59b0764911fe2b8 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Sat, 18 Jan 2025 18:54:41 +0100 Subject: [PATCH 14/20] wip Signed-off-by: Jan Kowalleck --- cyclonedx/model/bom_ref.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cyclonedx/model/bom_ref.py b/cyclonedx/model/bom_ref.py index 07147af1..85bcf501 100644 --- a/cyclonedx/model/bom_ref.py +++ b/cyclonedx/model/bom_ref.py @@ -19,18 +19,17 @@ from typing import TYPE_CHECKING, Any, Optional import serializable -from serializable.helpers import BaseHelper from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException if TYPE_CHECKING: # pragma: no cover from typing import Type, TypeVar - _T = TypeVar('_T', bound='BomRef') + _T_BR = TypeVar('_T_BR', bound='BomRef') @serializable.serializable_class -class BomRef(BaseHelper): +class BomRef(serializable.helpers.BaseHelper): """ An identifier that can be used to reference objects elsewhere in the BOM. @@ -91,7 +90,7 @@ def serialize(cls, o: Any) -> Optional[str]: f'Attempt to serialize a non-BomRef: {o!r}') @classmethod - def deserialize(cls: 'Type[_T]', o: Any) -> '_T': + def deserialize(cls: 'Type[_T_BR]', o: Any) -> '_T_BR': try: return cls(value=str(o)) except ValueError as err: From 633bf74b3707937ce89b311da70c8cd9c7eb5b0c Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jan 2025 12:22:30 +0100 Subject: [PATCH 15/20] wip Signed-off-by: Jan Kowalleck --- cyclonedx/model/definition.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index 8dec519d..978450eb 100644 --- a/cyclonedx/model/definition.py +++ b/cyclonedx/model/definition.py @@ -25,7 +25,6 @@ from .._internal.compare import ComparableTuple as _ComparableTuple from ..exception.model import InvalidCreIdException from ..exception.serialization import SerializationOfUnexpectedValueException -from ..serialization import BomRefHelper from . import ExternalReference, Property from .bom_ref import BomRef @@ -144,7 +143,7 @@ def __repr__(self) -> str: @property @serializable.json_name('bom-ref') - @serializable.type_mapping(BomRefHelper) + @serializable.type_mapping(BomRef) @serializable.xml_attribute() @serializable.xml_name('bom-ref') def bom_ref(self) -> BomRef: @@ -231,7 +230,7 @@ def open_cre(self, open_cre: Iterable[CreId]) -> None: self._open_cre = SortedSet(open_cre) @property - @serializable.type_mapping(BomRefHelper) + @serializable.type_mapping(BomRef) @serializable.xml_sequence(6) def parent(self) -> Optional[BomRef]: """ @@ -325,7 +324,7 @@ def __repr__(self) -> str: @property @serializable.json_name('bom-ref') - @serializable.type_mapping(BomRefHelper) + @serializable.type_mapping(BomRef) @serializable.xml_attribute() @serializable.xml_name('bom-ref') def bom_ref(self) -> BomRef: From 63722552808c3eba7b28c95b39e6bce232ffb313 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jan 2025 12:25:34 +0100 Subject: [PATCH 16/20] wip Signed-off-by: Jan Kowalleck --- cyclonedx/model/definition.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index 978450eb..46f922b0 100644 --- a/cyclonedx/model/definition.py +++ b/cyclonedx/model/definition.py @@ -29,7 +29,9 @@ from .bom_ref import BomRef if TYPE_CHECKING: # pragma: no cover - pass + from typing import Type, TypeVar + + _T_CreId = TypeVar('_T_CreId', bound='CreId') @serializable.serializable_class @@ -63,7 +65,7 @@ def serialize(cls, o: Any) -> str: f'Attempt to serialize a non-CreId: {o!r}') @classmethod - def deserialize(cls, o: Any) -> 'CreId': + def deserialize(cls: 'Type[_T_CreId]', o: Any) -> '_T_CreId': return cls(id=str(o)) def __eq__(self, other: Any) -> bool: From 8a7ca4fa3558b97cbc9492505d9763617cd0f63f Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jan 2025 12:47:29 +0100 Subject: [PATCH 17/20] wip Signed-off-by: Jan Kowalleck --- tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index 7e54c5ea..93111a22 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -39,7 +39,7 @@ OWN_DATA_DIRECTORY = path.join(_TESTDATA_DIRECTORY, 'own') SNAPSHOTS_DIRECTORY = path.join(_TESTDATA_DIRECTORY, 'snapshots') -RECREATE_SNAPSHOTS = True or '1' == getenv('CDX_TEST_RECREATE_SNAPSHOTS') +RECREATE_SNAPSHOTS = '1' == getenv('CDX_TEST_RECREATE_SNAPSHOTS') if RECREATE_SNAPSHOTS: print('!!! WILL RECREATE ALL SNAPSHOTS !!!') From c0b53a40f15e5217279b631523690da50a56c2ed Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 4 Feb 2025 15:07:16 +0100 Subject: [PATCH 18/20] reorder Signed-off-by: Jan Kowalleck --- cyclonedx/model/definition.py | 152 +++++++++++++++++----------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index 46f922b0..de9ab883 100644 --- a/cyclonedx/model/definition.py +++ b/cyclonedx/model/definition.py @@ -116,33 +116,6 @@ def __init__( self.properties = properties or () # type:ignore[assignment] self.external_references = external_references or () # type:ignore[assignment] - def __comparable_tuple(self) -> _ComparableTuple: - # all properties are optional - so need to compare all, in hope that one is unique - return _ComparableTuple(( - self.bom_ref, self.identifier, - self.title, self.text, - _ComparableTuple(self.descriptions), - _ComparableTuple(self.open_cre), self.parent, _ComparableTuple(self.properties), - _ComparableTuple(self.external_references) - )) - - def __lt__(self, other: Any) -> bool: - if isinstance(other, Requirement): - return self.__comparable_tuple() < other.__comparable_tuple() - return NotImplemented - - def __eq__(self, other: object) -> bool: - if isinstance(other, Requirement): - return self.__comparable_tuple() == other.__comparable_tuple() - return False - - def __hash__(self) -> int: - return hash(self.__comparable_tuple()) - - def __repr__(self) -> str: - return f'' - @property @serializable.json_name('bom-ref') @serializable.type_mapping(BomRef) @@ -280,6 +253,33 @@ def external_references(self) -> 'SortedSet[ExternalReference]': def external_references(self, external_references: Iterable[ExternalReference]) -> None: self._external_references = SortedSet(external_references) + def __comparable_tuple(self) -> _ComparableTuple: + # all properties are optional - so need to compare all, in hope that one is unique + return _ComparableTuple(( + self.bom_ref, self.identifier, + self.title, self.text, + _ComparableTuple(self.descriptions), + _ComparableTuple(self.open_cre), self.parent, _ComparableTuple(self.properties), + _ComparableTuple(self.external_references) + )) + + def __lt__(self, other: Any) -> bool: + if isinstance(other, Requirement): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __eq__(self, other: object) -> bool: + if isinstance(other, Requirement): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + @serializable.serializable_class class Level: @@ -301,29 +301,6 @@ def __init__( self.description = description self.requirements = requirements or () # type:ignore[assignment] - def __comparable_tuple(self) -> _ComparableTuple: - # all properties are optional - so need to compare all, in hope that one is unique - return _ComparableTuple(( - self.bom_ref, self.identifier, self.title, self.description, _ComparableTuple(self.requirements) - )) - - def __lt__(self, other: Any) -> bool: - if isinstance(other, Level): - return self.__comparable_tuple() < other.__comparable_tuple() - return NotImplemented - - def __eq__(self, other: object) -> bool: - if isinstance(other, Level): - return self.__comparable_tuple() == other.__comparable_tuple() - return False - - def __hash__(self) -> int: - return hash(self.__comparable_tuple()) - - def __repr__(self) -> str: - return f'' - @property @serializable.json_name('bom-ref') @serializable.type_mapping(BomRef) @@ -393,6 +370,29 @@ def requirements(self, requirements: Iterable[Union[str, BomRef]]) -> None: self._requirements = SortedSet(map(_bom_ref_from_str, # type: ignore[arg-type] requirements)) + def __comparable_tuple(self) -> _ComparableTuple: + # all properties are optional - so need to compare all, in hope that one is unique + return _ComparableTuple(( + self.bom_ref, self.identifier, self.title, self.description, _ComparableTuple(self.requirements) + )) + + def __lt__(self, other: Any) -> bool: + if isinstance(other, Level): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __eq__(self, other: object) -> bool: + if isinstance(other, Level): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + @serializable.serializable_class class Standard: @@ -421,32 +421,6 @@ def __init__( self.levels = levels or () # type:ignore[assignment] self.external_references = external_references or () # type:ignore[assignment] - def __comparable_tuple(self) -> _ComparableTuple: - # all properties are optional - so need to apply all, in hope that one is unique - return _ComparableTuple(( - self.bom_ref, - self.name, self.version, self.description, self.owner, - _ComparableTuple(self.requirements), _ComparableTuple(self.levels), - _ComparableTuple(self.external_references) - )) - - def __lt__(self, other: Any) -> bool: - if isinstance(other, Standard): - return self.__comparable_tuple() < other.__comparable_tuple() - return NotImplemented - - def __eq__(self, other: object) -> bool: - if isinstance(other, Standard): - return self.__comparable_tuple() == other.__comparable_tuple() - return False - - def __hash__(self) -> int: - return hash(self.__comparable_tuple()) - - def __repr__(self) -> str: - return f'' - @property @serializable.json_name('bom-ref') @serializable.type_mapping(BomRef) @@ -556,6 +530,32 @@ def external_references(self) -> 'SortedSet[ExternalReference]': def external_references(self, external_references: Iterable[ExternalReference]) -> None: self._external_references = SortedSet(external_references) + def __comparable_tuple(self) -> _ComparableTuple: + # all properties are optional - so need to apply all, in hope that one is unique + return _ComparableTuple(( + self.bom_ref, + self.name, self.version, self.description, self.owner, + _ComparableTuple(self.requirements), _ComparableTuple(self.levels), + _ComparableTuple(self.external_references) + )) + + def __lt__(self, other: Any) -> bool: + if isinstance(other, Standard): + return self.__comparable_tuple() < other.__comparable_tuple() + return NotImplemented + + def __eq__(self, other: object) -> bool: + if isinstance(other, Standard): + return self.__comparable_tuple() == other.__comparable_tuple() + return False + + def __hash__(self) -> int: + return hash(self.__comparable_tuple()) + + def __repr__(self) -> str: + return f'' + @serializable.serializable_class(name='definitions') class Definitions: From 2b25922ec9e1ee7793f510dcccf5c709856e3634 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 4 Feb 2025 15:25:34 +0100 Subject: [PATCH 19/20] wip Signed-off-by: Jan Kowalleck --- tests/_data/models.py | 26 +++++++++++-------- ...itions_and_detailed_standards-1.6.json.bin | 12 ++++++++- ...nitions_and_detailed_standards-1.6.xml.bin | 8 +++++- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/tests/_data/models.py b/tests/_data/models.py index 4fb89b72..6bba3499 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -1327,7 +1327,6 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: version='1.2.3', description='Some description', owner='Some Owner', - external_references=[get_external_reference_1()], requirements=[ req1 := Requirement( bom_ref='req-1', @@ -1336,7 +1335,11 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: text='some requirement text', descriptions=['Requirement 1 described here', 'and here'], open_cre=[CreId('CRE:1-2')], - properties=[Property(name='key1', value='val1')] + properties=[ + Property(name='key1', value='val1a'), + Property(name='key1', value='val1b'), + ], + external_references=[get_external_reference_2()], ), req2 := Requirement( bom_ref='req-2', @@ -1345,8 +1348,8 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: text='some requirement text', descriptions=['Requirement 2 described here'], open_cre=[CreId('CRE:1-2'), CreId('CRE:3-4')], + parent=req1.bom_ref, properties=[Property(name='key2', value='val2')], - parent=req1.bom_ref ), ], levels=[ @@ -1354,7 +1357,7 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: bom_ref='lvl-1', identifier='LVL-1', title='Level 1', - description='Level 1 description' + description='Level 1 description', # no requirements! ), Level( @@ -1362,9 +1365,10 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: identifier='LVL-2', title='Level 2', description='Level 2 description', - requirements=[req1.bom_ref, req2.bom_ref] - ) - ] + requirements=[req1.bom_ref, req2.bom_ref], + ), + ], + external_references=[get_external_reference_1()], ), Standard( bom_ref='other-standard', @@ -1372,7 +1376,6 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: version='1.0.0', description='Other description', owner='Other Owner', - external_references=[get_external_reference_2()], requirements=[ req3 := Requirement( bom_ref='req-3', @@ -1382,7 +1385,7 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: descriptions=['Requirement 3 described here', 'and here'], open_cre=[CreId('CRE:5-6'), CreId('CRE:7-8')], properties=[Property(name='key3', value='val3')] - ) + ), ], levels=[ Level( @@ -1391,8 +1394,9 @@ def get_bom_with_definitions_and_detailed_standards() -> Bom: title='Level 3', description='Level 3 description', requirements=[req3.bom_ref] - ) - ] + ), + ], + external_references=[get_external_reference_2()], ) ])) diff --git a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.json.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.json.bin index d0ef4761..21195512 100644 --- a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.json.bin +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.json.bin @@ -90,6 +90,12 @@ "Requirement 1 described here", "and here" ], + "externalReferences": [ + { + "type": "website", + "url": "https://cyclonedx.org" + } + ], "identifier": "REQ-1", "openCre": [ "CRE:1-2" @@ -97,7 +103,11 @@ "properties": [ { "name": "key1", - "value": "val1" + "value": "val1a" + }, + { + "name": "key1", + "value": "val1b" } ], "text": "some requirement text", diff --git a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin index 975fd69d..3e6a145e 100644 --- a/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin +++ b/tests/_data/snapshots/get_bom_with_definitions_and_detailed_standards-1.6.xml.bin @@ -62,8 +62,14 @@ CRE:1-2 - val1 + val1a + val1b + + + https://cyclonedx.org + + REQ-2 From 0ea594f5d6cb6a08fe18ee31fb34268613c1fb8f Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Tue, 4 Feb 2025 15:43:27 +0100 Subject: [PATCH 20/20] wip Signed-off-by: Jan Kowalleck --- cyclonedx/model/definition.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index de9ab883..90872e32 100644 --- a/cyclonedx/model/definition.py +++ b/cyclonedx/model/definition.py @@ -117,10 +117,10 @@ def __init__( self.external_references = external_references or () # type:ignore[assignment] @property - @serializable.json_name('bom-ref') @serializable.type_mapping(BomRef) - @serializable.xml_attribute() + @serializable.json_name('bom-ref') @serializable.xml_name('bom-ref') + @serializable.xml_attribute() def bom_ref(self) -> BomRef: """ An optional identifier which can be used to reference the requirement elsewhere in the BOM. @@ -277,8 +277,8 @@ def __hash__(self) -> int: return hash(self.__comparable_tuple()) def __repr__(self) -> str: - return f'' + return f'' @serializable.serializable_class @@ -302,10 +302,10 @@ def __init__( self.requirements = requirements or () # type:ignore[assignment] @property - @serializable.json_name('bom-ref') @serializable.type_mapping(BomRef) - @serializable.xml_attribute() + @serializable.json_name('bom-ref') @serializable.xml_name('bom-ref') + @serializable.xml_attribute() def bom_ref(self) -> BomRef: """ An optional identifier which can be used to reference the level elsewhere in the BOM. @@ -390,8 +390,8 @@ def __hash__(self) -> int: return hash(self.__comparable_tuple()) def __repr__(self) -> str: - return f'' + return f'' @serializable.serializable_class @@ -411,6 +411,7 @@ def __init__( requirements: Optional[Iterable[Requirement]] = None, levels: Optional[Iterable[Level]] = None, external_references: Optional[Iterable['ExternalReference']] = None + # TODO: signature ) -> None: self._bom_ref = _bom_ref_from_str(bom_ref) self.name = name @@ -420,12 +421,13 @@ def __init__( self.requirements = requirements or () # type:ignore[assignment] self.levels = levels or () # type:ignore[assignment] self.external_references = external_references or () # type:ignore[assignment] + # TODO: signature @property - @serializable.json_name('bom-ref') @serializable.type_mapping(BomRef) - @serializable.xml_attribute() + @serializable.json_name('bom-ref') @serializable.xml_name('bom-ref') + @serializable.xml_attribute() def bom_ref(self) -> BomRef: """ An optional identifier which can be used to reference the standard elsewhere in the BOM. Every bom-ref MUST be @@ -530,6 +532,16 @@ def external_references(self) -> 'SortedSet[ExternalReference]': def external_references(self, external_references: Iterable[ExternalReference]) -> None: self._external_references = SortedSet(external_references) + # @property + # @serializable.xml_sequence(8) + # # MUST NOT RENDER FOR XML -- this is JSON only + # def signature(self) -> ...: + # ... + # + # @signature.setter + # def levels(self, signature: ...) -> None: + # ... + def __comparable_tuple(self) -> _ComparableTuple: # all properties are optional - so need to apply all, in hope that one is unique return _ComparableTuple(( @@ -553,7 +565,8 @@ def __hash__(self) -> int: return hash(self.__comparable_tuple()) def __repr__(self) -> str: - return f''