diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index 7cb0081e..687afaf4 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -44,7 +44,7 @@ from ..serialization import UrnUuidHelper from . import _BOM_LINK_PREFIX, ExternalReference, Property from .bom_ref import BomRef -from .component import Component +from .component import Component, _ComponentValidationHelper from .contact import OrganizationalContact, OrganizationalEntity from .definition import Definitions from .dependency import Dependable, Dependency @@ -863,6 +863,18 @@ def validate(self) -> bool: raise LicenseExpressionAlongWithOthersException( f'Found LicenseExpression along with others licenses in: {elem!r}') + # 4. Validates that each component conforms to CycloneDX 1.7 constraints: + # - root component may not have is_external=true + # - version and version_range are mutually exclusive + # - version_range requires is_external=true + if self.metadata.component and self.metadata.component.is_external: + warn( + f'The Component this BOM is describing {self.metadata.component.purl} must not have is_external=true.', + category=UserWarning, stacklevel=1 + ) + for _c in self._get_all_components(): + _ComponentValidationHelper.validate(_c) + return True def __comparable_tuple(self) -> _ComparableTuple: diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index bc7152ae..bd83ee34 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -34,7 +34,7 @@ from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str from .._internal.compare import ComparablePackageURL as _ComparablePackageURL, ComparableTuple as _ComparableTuple -from ..exception.model import InvalidOmniBorIdException, InvalidSwhidException +from ..exception.model import InvalidOmniBorIdException, InvalidSwhidException, MutuallyExclusivePropertiesException from ..exception.serialization import ( CycloneDxDeserializationException, SerializationOfUnexpectedValueException, @@ -991,6 +991,7 @@ def __init__( publisher: Optional[str] = None, group: Optional[str] = None, version: Optional[str] = None, + version_range: Optional[str] = None, description: Optional[str] = None, scope: Optional[ComponentScope] = None, is_external: Optional[bool] = None, @@ -1025,6 +1026,8 @@ def __init__( self.publisher = publisher self.group = group self.name = name + self.version = version + self.version_range = version_range self.description = description self.scope = scope self.is_external = is_external @@ -1047,7 +1050,6 @@ def __init__( # spec-deprecated properties below self.author = author self.modified = modified - self.version = version @property @serializable.type_mapping(_ComponentTypeSerializationHelper) @@ -1272,6 +1274,34 @@ def version(self, version: Optional[str]) -> None: warn('`@.version`has a maximum length of 1024 from CycloneDX v1.6 onwards.', UserWarning) self._version = version + @property + @serializable.view(SchemaVersion1Dot7) + @serializable.json_name('versionRange') + @serializable.xml_name('versionRange') + @serializable.xml_sequence(8) + @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) + def version_range(self) -> Optional[str]: + """ + For an external component, this specifies the accepted version range. + + The value must adhere to the Package URL Version Range syntax (vers), as defined at + https://github.com/package-url/vers-spec + + May only be used if .isExternal is set to true. + + Must be used exclusively, either 'version' or 'version_range', but not both. + + Returns: + `str` if set, else `None` + """ + return self._version_range + + @version_range.setter + def version_range(self, version_range: Optional[str]) -> None: + if version_range and not 1 <= len(version_range) <= 4096: + warn('`@.version_range`has a minimum length of 1 and a maximum length of 4096 characters.', UserWarning) + self._version_range = version_range + @property @serializable.xml_sequence(9) @serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) @@ -1702,7 +1732,7 @@ def get_pypi_url(self) -> str: def __comparable_tuple(self) -> _ComparableTuple: return _ComparableTuple(( - self.type, self.group, self.name, self.version, + self.type, self.group, self.name, self.version, self.version_range, self.bom_ref.value, None if self.purl is None else _ComparablePackageURL(self.purl), self.swid, self.cpe, _ComparableTuple(self.swhids), @@ -1732,5 +1762,43 @@ def __hash__(self) -> int: return hash(self.__comparable_tuple()) def __repr__(self) -> str: - return f'' + if not self.is_external: + # omit is_external unless it is set to the non-default value (i.e. True) + return f'' + elif self.version_range is None: + return f'' + else: + return f'' + + +class _ComponentValidationHelper: + """ THIS CLASS IS NON-PUBLIC API """ + + @staticmethod + def validate_version_choice(component: Component) -> None: + """ Validates that version and version_range are not both set. """ + if component.version is not None and component.version_range is not None: + raise MutuallyExclusivePropertiesException( + f'Component cannot have both `version` and `version_range` set. Component: {component.name}' + ) + + @staticmethod + def validate_version_range_requirements(component: Component) -> None: + """ Validates that version_range is used correctly with is_external. """ + if component.version_range is not None and not component.is_external: + raise MutuallyExclusivePropertiesException( + f'Component cannot have `version_range` set unless `is_external=true`. Component: {component.name}' + ) + + @staticmethod + def validate(component: Component) -> None: + """ + Validates that the component conforms to CycloneDX 1.7 constraints: + - version and version_range are mutually exclusive + - version_range requires is_external=true + """ + _ComponentValidationHelper.validate_version_choice(component) + _ComponentValidationHelper.validate_version_range_requirements(component) diff --git a/tests/_data/models.py b/tests/_data/models.py index e2052878..709ec03e 100644 --- a/tests/_data/models.py +++ b/tests/_data/models.py @@ -599,8 +599,33 @@ def get_bom_with_external_references() -> Bom: return bom -def get_bom_with_external_component_1_7() -> Bom: - bom = _make_bom(components=[get_component_external()]) +def get_bom_with_external_component_without_version() -> Bom: + bom = _make_bom(components=[get_component_external_without_version()]) + return bom + + +def get_bom_with_external_component_with_version() -> Bom: + bom = _make_bom(components=[get_component_external_with_version()]) + return bom + + +def get_bom_with_external_component_with_version_range() -> Bom: + bom = _make_bom(components=[get_component_external_with_version_range()]) + return bom + + +def get_bom_with_external_component_with_version_and_version_range_invalid() -> Bom: + bom = _make_bom(components=[get_component_external_with_version_and_version_range_invalid()]) + return bom + + +def get_bom_with_non_external_explicit_component_with_version_range_invalid() -> Bom: + bom = _make_bom(components=[get_component_non_external_explicit_with_version_range_invalid()]) + return bom + + +def get_bom_with_non_external_implicit_component_with_version_range_invalid() -> Bom: + bom = _make_bom(components=[get_component_non_external_implicit_with_version_range_invalid()]) return bom @@ -864,16 +889,65 @@ def get_component_setuptools_simple( ) -def get_component_external() -> Component: +def get_component_external_without_version() -> Component: return Component( - name='external-lib', version='1.0.0', + name='external-lib', + type=ComponentType.LIBRARY, + is_external=True, + bom_ref='external-lib', + ) + + +def get_component_external_with_version() -> Component: + return Component( + name='external-lib', + version='1.0.0', type=ComponentType.LIBRARY, is_external=True, - scope=ComponentScope.REQUIRED, bom_ref='external-lib-1.0.0', ) +def get_component_external_with_version_range() -> Component: + return Component( + name='external-lib', + version_range='vers:all/*', + type=ComponentType.LIBRARY, + is_external=True, + bom_ref='external-lib-with-range', + ) + + +def get_component_external_with_version_and_version_range_invalid() -> Component: + return Component( + name='external-lib', + version='1.0.0', + version_range='vers:all/*', + type=ComponentType.LIBRARY, + is_external=True, + bom_ref='external-lib-with-version-and-version-range', + ) + + +def get_component_non_external_explicit_with_version_range_invalid() -> Component: + return Component( + name='internal-lib', + version_range='vers:all/*', + type=ComponentType.LIBRARY, + is_external=False, + bom_ref='internal-lib-with-range', + ) + + +def get_component_non_external_implicit_with_version_range_invalid() -> Component: + return Component( + name='internal-lib', + version_range='vers:all/*', + type=ComponentType.LIBRARY, + bom_ref='internal-lib-with-range', + ) + + def get_component_setuptools_simple_no_version(bom_ref: Optional[str] = None) -> Component: return Component( name='setuptools', bom_ref=bom_ref or 'pkg:pypi/setuptools?extension=tar.gz', @@ -1718,7 +1792,9 @@ def get_bom_for_issue941_nested_dependencies_irreversible_migrate() -> Bom: get_bom_with_licenses, get_bom_with_multiple_licenses, get_bom_for_issue_497_urls, - get_bom_with_external_component_1_7, + get_bom_with_external_component_without_version, + get_bom_with_external_component_with_version, + get_bom_with_external_component_with_version_range, get_bom_for_issue_598_multiple_components_with_purl_qualifiers, get_bom_with_component_setuptools_with_v16_fields, get_bom_for_issue_630_empty_property, diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.0.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.0.xml.bin similarity index 89% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.0.xml.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.0.xml.bin index aaae8337..431fbb72 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.0.xml.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.0.xml.bin @@ -4,7 +4,6 @@ external-lib 1.0.0 - required false diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.1.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.1.xml.bin similarity index 91% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.1.xml.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.1.xml.bin index 06e044d3..41be067a 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.1.xml.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.1.xml.bin @@ -4,7 +4,6 @@ external-lib 1.0.0 - required diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.2.json.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.2.json.bin similarity index 94% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.2.json.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.2.json.bin index fe5a4e0a..08a472a6 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.2.json.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.2.json.bin @@ -3,7 +3,6 @@ { "bom-ref": "external-lib-1.0.0", "name": "external-lib", - "scope": "required", "type": "library", "version": "1.0.0" } diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.2.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.2.xml.bin similarity index 94% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.2.xml.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.2.xml.bin index 266020af..a61e8b68 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.2.xml.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.2.xml.bin @@ -7,7 +7,6 @@ external-lib 1.0.0 - required diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.3.json.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.3.json.bin similarity index 94% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.3.json.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.3.json.bin index 8500a9f7..305418a6 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.3.json.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.3.json.bin @@ -3,7 +3,6 @@ { "bom-ref": "external-lib-1.0.0", "name": "external-lib", - "scope": "required", "type": "library", "version": "1.0.0" } diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.3.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.3.xml.bin similarity index 94% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.3.xml.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.3.xml.bin index 120d5d28..b6c4950d 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.3.xml.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.3.xml.bin @@ -7,7 +7,6 @@ external-lib 1.0.0 - required diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.4.json.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.4.json.bin similarity index 94% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.4.json.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.4.json.bin index c2c3bbea..aefdb0a5 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.4.json.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.4.json.bin @@ -3,7 +3,6 @@ { "bom-ref": "external-lib-1.0.0", "name": "external-lib", - "scope": "required", "type": "library", "version": "1.0.0" } diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.4.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.4.xml.bin similarity index 94% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.4.xml.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.4.xml.bin index 2d85c7de..d625e756 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.4.xml.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.4.xml.bin @@ -7,7 +7,6 @@ external-lib 1.0.0 - required diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.5.json.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.5.json.bin similarity index 95% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.5.json.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.5.json.bin index f3d16896..f1073439 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.5.json.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.5.json.bin @@ -3,7 +3,6 @@ { "bom-ref": "external-lib-1.0.0", "name": "external-lib", - "scope": "required", "type": "library", "version": "1.0.0" } diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.5.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.5.xml.bin similarity index 95% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.5.xml.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.5.xml.bin index f06f8a0b..3d43b3a9 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.5.xml.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.5.xml.bin @@ -7,7 +7,6 @@ external-lib 1.0.0 - required diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.6.json.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.6.json.bin similarity index 95% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.6.json.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.6.json.bin index bf66e848..67195c68 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.6.json.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.6.json.bin @@ -3,7 +3,6 @@ { "bom-ref": "external-lib-1.0.0", "name": "external-lib", - "scope": "required", "type": "library", "version": "1.0.0" } diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.6.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.6.xml.bin similarity index 95% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.6.xml.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.6.xml.bin index c81104a7..67d30f88 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.6.xml.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.6.xml.bin @@ -7,7 +7,6 @@ external-lib 1.0.0 - required diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.7.json.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.7.json.bin similarity index 96% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.7.json.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.7.json.bin index c3c2f85b..3fefcee2 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.7.json.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.7.json.bin @@ -4,7 +4,6 @@ "bom-ref": "external-lib-1.0.0", "isExternal": true, "name": "external-lib", - "scope": "required", "type": "library", "version": "1.0.0" } diff --git a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.7.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.7.xml.bin similarity index 95% rename from tests/_data/snapshots/get_bom_with_external_component_1_7-1.7.xml.bin rename to tests/_data/snapshots/get_bom_with_external_component_with_version-1.7.xml.bin index b16d837f..b4854e27 100644 --- a/tests/_data/snapshots/get_bom_with_external_component_1_7-1.7.xml.bin +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version-1.7.xml.bin @@ -7,7 +7,6 @@ external-lib 1.0.0 - required diff --git a/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.0.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.0.xml.bin new file mode 100644 index 00000000..2e921e5f --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.0.xml.bin @@ -0,0 +1,10 @@ + + + + + external-lib + + false + + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.1.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.1.xml.bin new file mode 100644 index 00000000..a76e0521 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.1.xml.bin @@ -0,0 +1,9 @@ + + + + + external-lib + + + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.2.json.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.2.json.bin new file mode 100644 index 00000000..a8fd6190 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.2.json.bin @@ -0,0 +1,23 @@ +{ + "components": [ + { + "bom-ref": "external-lib-with-range", + "name": "external-lib", + "type": "library", + "version": "" + } + ], + "dependencies": [ + { + "ref": "external-lib-with-range" + } + ], + "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_external_component_with_version_range-1.2.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.2.xml.bin new file mode 100644 index 00000000..954b82a9 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.2.xml.bin @@ -0,0 +1,15 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + external-lib + + + + + + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.3.json.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.3.json.bin new file mode 100644 index 00000000..0e198bc7 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.3.json.bin @@ -0,0 +1,23 @@ +{ + "components": [ + { + "bom-ref": "external-lib-with-range", + "name": "external-lib", + "type": "library", + "version": "" + } + ], + "dependencies": [ + { + "ref": "external-lib-with-range" + } + ], + "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_external_component_with_version_range-1.3.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.3.xml.bin new file mode 100644 index 00000000..090ea857 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.3.xml.bin @@ -0,0 +1,15 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + external-lib + + + + + + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.4.json.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.4.json.bin new file mode 100644 index 00000000..89caf5ae --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.4.json.bin @@ -0,0 +1,22 @@ +{ + "components": [ + { + "bom-ref": "external-lib-with-range", + "name": "external-lib", + "type": "library" + } + ], + "dependencies": [ + { + "ref": "external-lib-with-range" + } + ], + "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_external_component_with_version_range-1.4.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.4.xml.bin new file mode 100644 index 00000000..911dd6f7 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.4.xml.bin @@ -0,0 +1,14 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + external-lib + + + + + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.5.json.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.5.json.bin new file mode 100644 index 00000000..fdb5c099 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.5.json.bin @@ -0,0 +1,32 @@ +{ + "components": [ + { + "bom-ref": "external-lib-with-range", + "name": "external-lib", + "type": "library" + } + ], + "dependencies": [ + { + "ref": "external-lib-with-range" + } + ], + "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_external_component_with_version_range-1.5.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.5.xml.bin new file mode 100644 index 00000000..36be7f3a --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.5.xml.bin @@ -0,0 +1,18 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + external-lib + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.6.json.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.6.json.bin new file mode 100644 index 00000000..bffa7454 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.6.json.bin @@ -0,0 +1,32 @@ +{ + "components": [ + { + "bom-ref": "external-lib-with-range", + "name": "external-lib", + "type": "library" + } + ], + "dependencies": [ + { + "ref": "external-lib-with-range" + } + ], + "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_external_component_with_version_range-1.6.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.6.xml.bin new file mode 100644 index 00000000..a6d46784 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.6.xml.bin @@ -0,0 +1,18 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + external-lib + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.7.json.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.7.json.bin new file mode 100644 index 00000000..fcdd4571 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.7.json.bin @@ -0,0 +1,34 @@ +{ + "components": [ + { + "bom-ref": "external-lib-with-range", + "isExternal": true, + "name": "external-lib", + "type": "library", + "versionRange": "vers:all/*" + } + ], + "dependencies": [ + { + "ref": "external-lib-with-range" + } + ], + "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.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.7.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.7.xml.bin new file mode 100644 index 00000000..7c48d2b4 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_with_version_range-1.7.xml.bin @@ -0,0 +1,19 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + external-lib + vers:all/* + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_without_version-1.0.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.0.xml.bin new file mode 100644 index 00000000..2e921e5f --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.0.xml.bin @@ -0,0 +1,10 @@ + + + + + external-lib + + false + + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_without_version-1.1.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.1.xml.bin new file mode 100644 index 00000000..44e2fd2f --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.1.xml.bin @@ -0,0 +1,9 @@ + + + + + external-lib + + + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_without_version-1.2.json.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.2.json.bin new file mode 100644 index 00000000..3923e132 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.2.json.bin @@ -0,0 +1,23 @@ +{ + "components": [ + { + "bom-ref": "external-lib", + "name": "external-lib", + "type": "library", + "version": "" + } + ], + "dependencies": [ + { + "ref": "external-lib" + } + ], + "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_external_component_without_version-1.2.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.2.xml.bin new file mode 100644 index 00000000..e1166458 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.2.xml.bin @@ -0,0 +1,15 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + external-lib + + + + + + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_without_version-1.3.json.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.3.json.bin new file mode 100644 index 00000000..93221671 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.3.json.bin @@ -0,0 +1,23 @@ +{ + "components": [ + { + "bom-ref": "external-lib", + "name": "external-lib", + "type": "library", + "version": "" + } + ], + "dependencies": [ + { + "ref": "external-lib" + } + ], + "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_external_component_without_version-1.3.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.3.xml.bin new file mode 100644 index 00000000..6567e8b1 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.3.xml.bin @@ -0,0 +1,15 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + external-lib + + + + + + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_without_version-1.4.json.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.4.json.bin new file mode 100644 index 00000000..ab185131 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.4.json.bin @@ -0,0 +1,22 @@ +{ + "components": [ + { + "bom-ref": "external-lib", + "name": "external-lib", + "type": "library" + } + ], + "dependencies": [ + { + "ref": "external-lib" + } + ], + "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_external_component_without_version-1.4.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.4.xml.bin new file mode 100644 index 00000000..dba1982c --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.4.xml.bin @@ -0,0 +1,14 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + external-lib + + + + + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_without_version-1.5.json.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.5.json.bin new file mode 100644 index 00000000..2cb41594 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.5.json.bin @@ -0,0 +1,32 @@ +{ + "components": [ + { + "bom-ref": "external-lib", + "name": "external-lib", + "type": "library" + } + ], + "dependencies": [ + { + "ref": "external-lib" + } + ], + "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_external_component_without_version-1.5.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.5.xml.bin new file mode 100644 index 00000000..8123396b --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.5.xml.bin @@ -0,0 +1,18 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + external-lib + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_without_version-1.6.json.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.6.json.bin new file mode 100644 index 00000000..51ef0fce --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.6.json.bin @@ -0,0 +1,32 @@ +{ + "components": [ + { + "bom-ref": "external-lib", + "name": "external-lib", + "type": "library" + } + ], + "dependencies": [ + { + "ref": "external-lib" + } + ], + "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_external_component_without_version-1.6.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.6.xml.bin new file mode 100644 index 00000000..8ac54fc1 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.6.xml.bin @@ -0,0 +1,18 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + external-lib + + + + + + + val1 + val2 + + diff --git a/tests/_data/snapshots/get_bom_with_external_component_without_version-1.7.json.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.7.json.bin new file mode 100644 index 00000000..fd9ee1f2 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.7.json.bin @@ -0,0 +1,33 @@ +{ + "components": [ + { + "bom-ref": "external-lib", + "isExternal": true, + "name": "external-lib", + "type": "library" + } + ], + "dependencies": [ + { + "ref": "external-lib" + } + ], + "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.7.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.7" +} \ No newline at end of file diff --git a/tests/_data/snapshots/get_bom_with_external_component_without_version-1.7.xml.bin b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.7.xml.bin new file mode 100644 index 00000000..756ae730 --- /dev/null +++ b/tests/_data/snapshots/get_bom_with_external_component_without_version-1.7.xml.bin @@ -0,0 +1,18 @@ + + + + 2023-01-07T13:44:32.312678+00:00 + + + + external-lib + + + + + + + val1 + val2 + + diff --git a/tests/test_model_bom.py b/tests/test_model_bom.py index 030ee4a5..fe319345 100644 --- a/tests/test_model_bom.py +++ b/tests/test_model_bom.py @@ -217,6 +217,13 @@ def test_warning_missing_dependency(self) -> None: self.assertEqual(len(w.warnings), 1) self.assertIn('has no defined dependencies ', str(w.warnings[0])) + def test_warning_root_component_is_external(self) -> None: + with self.assertWarns(expected_warning=UserWarning) as w: + bom = Bom(metadata=BomMetaData(component=Component(name='root_component', is_external=True))) + _ = JsonV1Dot7(bom).output_as_string() + self.assertEqual(len(w.warnings), 1) + self.assertIn('must not have is_external=true', str(w.warnings[0])) + def test_empty_bom_defined_serial(self) -> None: serial_number = uuid4() bom = Bom(serial_number=serial_number) diff --git a/tests/test_model_component.py b/tests/test_model_component.py index f7b8fc80..b3d10236 100644 --- a/tests/test_model_component.py +++ b/tests/test_model_component.py @@ -304,6 +304,27 @@ def test_is_external_sorting(self) -> None: expected_components = reorder(components, expected_order) self.assertListEqual(sorted_components, expected_components) + def test_version_range_comparison(self) -> None: + component_all = Component(name='test-component', is_external=True, version_range='vers:all/*') + component_none = Component(name='test-component', is_external=True, version_range='vers:none/*') + self.assertEqual(component_all, component_all) + self.assertEqual(component_none, component_none) + self.assertNotEqual(component_all, component_none) + self.assertLess(component_all, component_none) + self.assertNotEqual(hash(component_all), hash(component_none)) + + def test_version_range_sorting(self) -> None: + # version is compared before version_range, None sorts as greater than any string + expected_order = [1, 2, 0] + components = [ + Component(name='test_component', is_external=True), + Component(name='test_component', is_external=True, version='1.0.0'), + Component(name='test_component', is_external=True, version_range='vers:all/*'), + ] + sorted_components = sorted(components) + expected_components = reorder(components, expected_order) + self.assertListEqual(sorted_components, expected_components) + def test_nested_components_1(self) -> None: comp_b = Component(name='comp_b') comp_c = Component(name='comp_c') diff --git a/tests/test_output_json.py b/tests/test_output_json.py index b9340a4e..062f38cf 100644 --- a/tests/test_output_json.py +++ b/tests/test_output_json.py @@ -30,6 +30,7 @@ InvalidOmniBorIdException, InvalidSwhidException, LicenseExpressionAlongWithOthersException, + MutuallyExclusivePropertiesException, UnknownComponentDependencyException, ) from cyclonedx.exception.output import FormatNotSupportedException @@ -93,6 +94,7 @@ def test_invalid(self, get_bom: Callable[[], Bom], sv: SchemaVersion) -> None: LicenseExpressionAlongWithOthersException, InvalidOmniBorIdException, InvalidSwhidException, + MutuallyExclusivePropertiesException, UnknownComponentDependencyException, )): return None # expected diff --git a/tests/test_output_xml.py b/tests/test_output_xml.py index 6e887ded..2cb68585 100644 --- a/tests/test_output_xml.py +++ b/tests/test_output_xml.py @@ -29,6 +29,7 @@ InvalidOmniBorIdException, InvalidSwhidException, LicenseExpressionAlongWithOthersException, + MutuallyExclusivePropertiesException, UnknownComponentDependencyException, ) from cyclonedx.model.bom import Bom @@ -81,6 +82,7 @@ def test_invalid(self, get_bom: Callable[[], Bom], sv: SchemaVersion) -> None: LicenseExpressionAlongWithOthersException, InvalidOmniBorIdException, InvalidSwhidException, + MutuallyExclusivePropertiesException, UnknownComponentDependencyException, )): return None # expected