From 61de768f264943821d83fa96d0965e6ee6c81e41 Mon Sep 17 00:00:00 2001 From: Hakan Dilek Date: Thu, 17 Oct 2024 16:27:56 +0200 Subject: [PATCH 1/4] feat: add bom.definitions for #697 Signed-off-by: Hakan Dilek --- cyclonedx/_internal/bom_ref.py | 33 +++++ cyclonedx/model/bom.py | 18 +++ cyclonedx/model/definition.py | 230 +++++++++++++++++++++++++++++++++ tests/test_model_definition.py | 67 ++++++++++ 4 files changed, 348 insertions(+) create mode 100644 cyclonedx/_internal/bom_ref.py create mode 100644 cyclonedx/model/definition.py create mode 100644 tests/test_model_definition.py diff --git a/cyclonedx/_internal/bom_ref.py b/cyclonedx/_internal/bom_ref.py new file mode 100644 index 00000000..c0943da5 --- /dev/null +++ b/cyclonedx/_internal/bom_ref.py @@ -0,0 +1,33 @@ +# This file is part of CycloneDX Python Library +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + + +""" +!!! ALL SYMBOLS IN HERE ARE INTERNAL. +Everything might change without any notice. +""" + +from typing import Optional, Union + +from ..model.bom_ref import BomRef + + +def bom_ref_from_str(bom_ref: Optional[Union[str, BomRef]]) -> BomRef: + if isinstance(bom_ref, BomRef): + return bom_ref + else: + return BomRef(value=str(bom_ref) if bom_ref else None) diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index 94cdd83e..c4ba090a 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -41,6 +41,7 @@ from .bom_ref import BomRef from .component import Component from .contact import OrganizationalContact, OrganizationalEntity +from .definition import Definitions from .dependency import Dependable, Dependency from .license import License, LicenseExpression, LicenseRepository from .lifecycle import Lifecycle, LifecycleRepository, _LifecycleRepositoryHelper @@ -327,6 +328,7 @@ def __init__( dependencies: Optional[Iterable[Dependency]] = None, vulnerabilities: Optional[Iterable[Vulnerability]] = None, properties: Optional[Iterable[Property]] = None, + definitions: Optional[Definitions] = None, ) -> None: """ Create a new Bom that you can manually/programmatically add data to later. @@ -343,6 +345,7 @@ def __init__( self.vulnerabilities = vulnerabilities or [] # type:ignore[assignment] self.dependencies = dependencies or [] # type:ignore[assignment] self.properties = properties or [] # type:ignore[assignment] + self.definitions = definitions or Definitions() @property @serializable.type_mapping(UrnUuidHelper) @@ -552,6 +555,21 @@ def vulnerabilities(self, vulnerabilities: Iterable[Vulnerability]) -> None: # def formulation(self, ...) -> None: # ... # TODO Since CDX 1.5 + @property + @serializable.view(SchemaVersion1Dot6) + @serializable.xml_sequence(110) + def definitions(self) -> Optional[Definitions]: + """ + The repository for definitions + Returns: + `Definitions` + """ + return self._definitions if len(self._definitions.standards) > 0 else None + + @definitions.setter + def definitions(self, definitions: Definitions) -> None: + self._definitions = definitions + def get_component_by_purl(self, purl: Optional['PackageURL']) -> Optional[Component]: """ Get a Component already in the Bom by its PURL diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py new file mode 100644 index 00000000..0bde33c3 --- /dev/null +++ b/cyclonedx/model/definition.py @@ -0,0 +1,230 @@ +# This file is part of CycloneDX Python Library +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +from typing import TYPE_CHECKING, Any, Iterable, Optional, Union + +import serializable +from sortedcontainers import SortedSet + +from .._internal.bom_ref import bom_ref_from_str +from .._internal.compare import ComparableTuple as _ComparableTuple +from ..serialization import BomRefHelper +from . import ExternalReference +from .bom_ref import BomRef + +if TYPE_CHECKING: # pragma: no cover + pass + + +@serializable.serializable_class +class Standard: + """ + A standard of regulations, industry or organizational-specific standards, maturity models, best practices, + or any other requirements. + """ + + def __init__( + self, *, + bom_ref: Optional[Union[str, BomRef]] = None, + name: Optional[str] = None, + version: Optional[str] = None, + description: Optional[str] = None, + owner: Optional[str] = None, + external_references: Optional[Iterable['ExternalReference']] = None + ) -> None: + self._bom_ref = bom_ref_from_str(bom_ref) + self.name = name + self.version = version + self.description = description + self.owner = owner + 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))) + return NotImplemented + + def __eq__(self, other: object) -> bool: + if isinstance(other, Standard): + return hash(other) == hash(self) + return False + + def __hash__(self) -> int: + return hash(( + self.bom_ref, self.name, self.version, self.description, self.owner, 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 standard elsewhere in the BOM. Every bom-ref MUST be + unique within the BOM. If a value was not provided in the constructor, a UUIDv4 will have been assigned. + Returns: + `BomRef` + """ + return self._bom_ref + + @property + @serializable.xml_sequence(1) + def name(self) -> Optional[str]: + """ + Returns: + The name of the standard + """ + return self._name + + @name.setter + def name(self, name: Optional[str]) -> None: + self._name = name + + @property + @serializable.xml_sequence(2) + def version(self) -> Optional[str]: + """ + Returns: + The version of the standard + """ + return self._version + + @version.setter + def version(self, version: Optional[str]) -> None: + self._version = version + + @property + @serializable.xml_sequence(3) + def description(self) -> Optional[str]: + """ + Returns: + The description of the standard + """ + return self._description + + @description.setter + def description(self, description: Optional[str]) -> None: + self._description = description + + @property + @serializable.xml_sequence(4) + def owner(self) -> Optional[str]: + """ + Returns: + The owner of the standard, often the entity responsible for its release. + """ + return self._owner + + @owner.setter + 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, 'reference') + @serializable.xml_sequence(7) + def external_references(self) -> 'SortedSet[ExternalReference]': + """ + Returns: + A SortedSet of external references associated with the standard. + """ + 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(name='definitions') +class Definitions: + """ + The repository for definitions + """ + + def __init__( + self, *, + standards: Optional[Iterable[Standard]] = None + ) -> None: + self.standards = standards or () # type:ignore[assignment] + + @property + @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'standard') + @serializable.xml_sequence(1) + def standards(self) -> 'SortedSet[Standard]': + """ + Returns: + A SortedSet of Standards + """ + return self._standards + + @standards.setter + def standards(self, standards: Iterable[Standard]) -> None: + self._standards = SortedSet(standards) + + 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 + + def __hash__(self) -> int: + return hash((tuple(self._standards))) + + def __lt__(self, other: Any) -> bool: + if isinstance(other, Definitions): + return (_ComparableTuple(self._standards) + < _ComparableTuple(other.standards)) + return NotImplemented + + def __repr__(self) -> str: + return '' diff --git a/tests/test_model_definition.py b/tests/test_model_definition.py new file mode 100644 index 00000000..5a1b80f3 --- /dev/null +++ b/tests/test_model_definition.py @@ -0,0 +1,67 @@ +# This file is part of CycloneDX Python Library +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + + +from unittest import TestCase + +from cyclonedx.model.definition import Definitions, Standard + + +class TestModelDefinitionRepository(TestCase): + + def test_init(self) -> Definitions: + s = Standard(name='test-standard') + dr = Definitions( + standards=(s, ), + ) + 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: + dr = Definitions() + self.assertIsNotNone(dr.standards) + self.assertEqual(0, len(dr.standards)) + self.assertFalse(dr) + + def test_unequal_different_type(self) -> None: + dr = Definitions() + self.assertFalse(dr == 'other') + + def test_equal_self(self) -> None: + dr = Definitions() + dr.standards.add(Standard(name='my-standard')) + self.assertTrue(dr == dr) + + def test_unequal(self) -> None: + dr1 = Definitions() + dr1.standards.add(Standard(name='my-standard')) + tr2 = Definitions() + self.assertFalse(dr1 == tr2) + + def test_equal(self) -> None: + s = Standard(name='my-standard') + dr1 = Definitions() + dr1.standards.add(s) + tr2 = Definitions() + tr2.standards.add(s) + self.assertTrue(dr1 == tr2) From 446d0a07b001450e9fef1915ab593e9c185abdc0 Mon Sep 17 00:00:00 2001 From: Hakan Dilek Date: Mon, 21 Oct 2024 16:16:25 +0200 Subject: [PATCH 2/4] feat: add test fixtures related to bom.definitions for #697 Signed-off-by: Hakan Dilek --- tests/_data/models.py | 17 ++++++++- ...bom_with_definitions_standards-1.0.xml.bin | 4 ++ ...bom_with_definitions_standards-1.1.xml.bin | 4 ++ ...om_with_definitions_standards-1.2.json.bin | 10 +++++ ...bom_with_definitions_standards-1.2.xml.bin | 6 +++ ...om_with_definitions_standards-1.3.json.bin | 10 +++++ ...bom_with_definitions_standards-1.3.xml.bin | 6 +++ ...om_with_definitions_standards-1.4.json.bin | 10 +++++ ...bom_with_definitions_standards-1.4.xml.bin | 6 +++ ...om_with_definitions_standards-1.5.json.bin | 20 ++++++++++ ...bom_with_definitions_standards-1.5.xml.bin | 10 +++++ ...om_with_definitions_standards-1.6.json.bin | 37 +++++++++++++++++++ ...bom_with_definitions_standards-1.6.xml.bin | 25 +++++++++++++ 13 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 tests/_data/snapshots/get_bom_with_definitions_standards-1.0.xml.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_standards-1.1.xml.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_standards-1.2.json.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_standards-1.2.xml.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_standards-1.3.json.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_standards-1.3.xml.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_standards-1.4.json.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_standards-1.4.xml.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_standards-1.5.json.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_standards-1.5.xml.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_standards-1.6.json.bin create mode 100644 tests/_data/snapshots/get_bom_with_definitions_standards-1.6.xml.bin diff --git a/tests/_data/models.py b/tests/_data/models.py index ab1805eb..d005b2b1 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -78,6 +78,7 @@ RelatedCryptoMaterialState, RelatedCryptoMaterialType, ) +from cyclonedx.model.definition import Definitions, Standard from cyclonedx.model.dependency import Dependency from cyclonedx.model.impact_analysis import ( ImpactAnalysisAffectedStatus, @@ -1285,7 +1286,20 @@ def get_bom_with_lifecycles() -> Bom: description='Integration testing specific to the runtime platform'), ], component=Component(name='app', type=ComponentType.APPLICATION, bom_ref='my-app'), - ), + ) + ) + + +def get_bom_with_definitions_standards() -> Bom: + """ + Returns a BOM with definitions and standards only. + """ + 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()] + ) + ]) ) @@ -1335,4 +1349,5 @@ def get_bom_with_lifecycles() -> Bom: get_bom_with_component_setuptools_with_v16_fields, get_bom_for_issue_630_empty_property, get_bom_with_lifecycles, + get_bom_with_definitions_standards, } diff --git a/tests/_data/snapshots/get_bom_with_definitions_standards-1.0.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_standards-1.0.xml.bin new file mode 100644 index 00000000..acb06612 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_standards-1.0.xml.bin @@ -0,0 +1,4 @@ + + + + diff --git a/tests/_data/snapshots/get_bom_with_definitions_standards-1.1.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_standards-1.1.xml.bin new file mode 100644 index 00000000..55ef5cda --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_standards-1.1.xml.bin @@ -0,0 +1,4 @@ + + + + diff --git a/tests/_data/snapshots/get_bom_with_definitions_standards-1.2.json.bin b/tests/_data/snapshots/get_bom_with_definitions_standards-1.2.json.bin new file mode 100644 index 00000000..8f473bd3 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_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_standards-1.2.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_standards-1.2.xml.bin new file mode 100644 index 00000000..df1938ec --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_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_standards-1.3.json.bin b/tests/_data/snapshots/get_bom_with_definitions_standards-1.3.json.bin new file mode 100644 index 00000000..02943890 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_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_standards-1.3.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_standards-1.3.xml.bin new file mode 100644 index 00000000..8341ff60 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_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_standards-1.4.json.bin b/tests/_data/snapshots/get_bom_with_definitions_standards-1.4.json.bin new file mode 100644 index 00000000..48f1745d --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_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_standards-1.4.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_standards-1.4.xml.bin new file mode 100644 index 00000000..d0a7d4c9 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_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_standards-1.5.json.bin b/tests/_data/snapshots/get_bom_with_definitions_standards-1.5.json.bin new file mode 100644 index 00000000..57b5e590 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_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_standards-1.5.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_standards-1.5.xml.bin new file mode 100644 index 00000000..f952637c --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_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_standards-1.6.json.bin b/tests/_data/snapshots/get_bom_with_definitions_standards-1.6.json.bin new file mode 100644 index 00000000..9fba8848 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_standards-1.6.json.bin @@ -0,0 +1,37 @@ +{ + "definitions": { + "standards": [ + { + "bom-ref": "some-standard", + "description": "Some description", + "externalReferences": [ + { + "type": "website", + "url": "https://cyclonedx.org" + } + ], + "name": "Some Standard", + "owner": "Some Owner", + "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_standards-1.6.xml.bin b/tests/_data/snapshots/get_bom_with_definitions_standards-1.6.xml.bin new file mode 100644 index 00000000..b983bdf6 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_definitions_standards-1.6.xml.bin @@ -0,0 +1,25 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + val1 + val2 + + + + + Some Standard + 1.2.3 + Some description + Some Owner + + + https://cyclonedx.org + + + + + + From eef6eea55ea6946494933c2d29b99baa9e948604 Mon Sep 17 00:00:00 2001 From: Hakan Dilek Date: Thu, 17 Oct 2024 18:15:26 +0200 Subject: [PATCH 3/4] feat: add bom.definitions with complete model Signed-off-by: Hakan Dilek --- cyclonedx/model/definition.py | 430 ++++++++++++++++++++++++++++++--- tests/test_model_definition.py | 35 ++- 2 files changed, 436 insertions(+), 29 deletions(-) diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index 0bde33c3..9119e23f 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,383 @@ from .._internal.bom_ref import bom_ref_from_str from .._internal.compare import ComparableTuple as _ComparableTuple +from ..exception.model import CycloneDxModelException +from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException from ..serialization import BomRefHelper -from . import ExternalReference +from . import ExternalReference, Property from .bom_ref import BomRef if TYPE_CHECKING: # pragma: no cover 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 + + +@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, CreId): + return str(o) + raise SerializationOfUnexpectedValueException( + f'Attempt to serialize a non-CreId: {o!r}') + + @classmethod + def deserialize(cls, o: Any) -> 'CreId': + try: + return CreId(id=str(o)) + except ValueError as err: + raise CycloneDxDeserializationException( + f'CreId string supplied does not parse: {o!r}' + ) from err + + 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. + + If a value was not provided in the constructor, a UUIDv4 will have been assigned. + + 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. + + If a value was not provided in the constructor, a UUIDv4 will have been assigned. + + 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(bom_ref_from_str, requirements)) + + @serializable.serializable_class class Standard: """ @@ -44,6 +414,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 +423,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: @@ -139,33 +513,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 5a1b80f3..508df561 100644 --- a/tests/test_model_definition.py +++ b/tests/test_model_definition.py @@ -18,7 +18,7 @@ from unittest import TestCase -from cyclonedx.model.definition import Definitions, Standard +from cyclonedx.model.definition import CreId, Definitions, InvalidCreIdException, Standard class TestModelDefinitionRepository(TestCase): @@ -65,3 +65,36 @@ 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') From 8145a51ca5e9d567b4557d95daaca5b1eb8942fb Mon Sep 17 00:00:00 2001 From: Hakan Dilek Date: Thu, 17 Oct 2024 18:15:33 +0200 Subject: [PATCH 4/4] 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 d005b2b1..85b8610a 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, @@ -1303,6 +1303,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', ) + ]) + ] + )) + + # --- @@ -1350,4 +1395,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 + + + + + + +