From b16e260dd2f52c9c2b9db24b6ce1727b949729bc Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Sat, 26 Oct 2024 13:49:30 -0400 Subject: [PATCH 01/12] feat: add helper method to generate bom_link Signed-off-by: Saquib Saifee --- cyclonedx/model/bom.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index 03809f2d..f9becaaa 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -37,7 +37,7 @@ SchemaVersion1Dot6, ) from ..serialization import LicenseRepositoryHelper, UrnUuidHelper -from . import ExternalReference, Property +from . import ExternalReference, Property, XsUri from .bom_ref import BomRef from .component import Component from .contact import OrganizationalContact, OrganizationalEntity @@ -665,6 +665,21 @@ def register_dependency(self, target: Dependable, depends_on: Optional[Iterable[ def urn(self) -> str: return f'urn:cdx:{self.serial_number}/{self.version}' + def get_bom_link(self, bom_ref: Union[str, BomRef]) -> 'XsUri': + """ + Generate a BOM-Link URI. + + Args: + bom_ref: The unique identifier of the component, service, or vulnerability within the BOM. + + Returns: + XsUri: Instance of XsUri with the generated BOM-Link URI. + + .. note: + See the CycloneDX Schema for BOM-Link: https://cyclonedx.org/capabilities/bomlink + """ + return XsUri(f'{self.urn}#{bom_ref}') + def validate(self) -> bool: """ Perform data-model level validations to make sure we have some known data integrity prior to attempting output From 2ce5d8fb55d089f1d5148b24aa3efd2d91eb4350 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Sat, 26 Oct 2024 21:24:32 -0400 Subject: [PATCH 02/12] chore: direct reference of type hint Signed-off-by: Saquib Saifee --- cyclonedx/model/bom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index f9becaaa..363a51b2 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -665,7 +665,7 @@ def register_dependency(self, target: Dependable, depends_on: Optional[Iterable[ def urn(self) -> str: return f'urn:cdx:{self.serial_number}/{self.version}' - def get_bom_link(self, bom_ref: Union[str, BomRef]) -> 'XsUri': + def get_bom_link(self, bom_ref: Union[str, BomRef]) -> XsUri: """ Generate a BOM-Link URI. From ec569e2fd77aa24ca0a3e19863d9b3dae5250a57 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Sat, 26 Oct 2024 21:38:06 -0400 Subject: [PATCH 03/12] feat: add XsUri cls method to generate bom_link Signed-off-by: Saquib Saifee --- cyclonedx/model/__init__.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/cyclonedx/model/__init__.py b/cyclonedx/model/__init__.py index 6b77dbb7..1b93f83f 100644 --- a/cyclonedx/model/__init__.py +++ b/cyclonedx/model/__init__.py @@ -27,7 +27,8 @@ from enum import Enum from functools import reduce from json import loads as json_loads -from typing import Any, Dict, FrozenSet, Generator, Iterable, List, Optional, Tuple, Type +from typing import Any, Dict, FrozenSet, Generator, Iterable, List, Optional, Tuple, Type, Union +from uuid import UUID from warnings import warn from xml.etree.ElementTree import Element as XmlElement # nosec B405 @@ -767,6 +768,23 @@ def deserialize(cls, o: Any) -> 'XsUri': f'XsUri string supplied does not parse: {o!r}' ) from err + @classmethod + def make_bom_link(cls, serialnumber: Union[UUID, str], version: int = 1, bom_ref: Optional[str] = None) -> 'XsUri': + """ + Generate a BOM-Link URI. + + Args: + serialnumber (Union[UUID, str]): Unique identifier for the BOM, either as a UUID or a string. + version (int, optional): Version number of the BOM-Link. Defaults to 1. + bom_ref (Optional[str], optional): Reference to a specific component in the BOM. Defaults to None. + + Returns: + XsUri: Instance of XsUri with the generated BOM-Link URI. + """ + bom_ref_part = f'#{bom_ref}' if bom_ref else '' + uri = f'urn:cdx:{serialnumber}/{version}{bom_ref_part}' + return cls(uri) + @serializable.serializable_class class ExternalReference: From f98666baf482f211c7ee85e5c56b0b89ff0fbeae Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Sun, 27 Oct 2024 12:53:37 -0400 Subject: [PATCH 04/12] chore(test): add unit tests for bom_link Signed-off-by: Saquib Saifee --- cyclonedx/model/__init__.py | 16 +++++++++++----- tests/test_model.py | 14 ++++++++++++++ tests/test_model_bom.py | 10 +++++++++- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/cyclonedx/model/__init__.py b/cyclonedx/model/__init__.py index 1b93f83f..f08870de 100644 --- a/cyclonedx/model/__init__.py +++ b/cyclonedx/model/__init__.py @@ -52,6 +52,7 @@ SchemaVersion1Dot5, SchemaVersion1Dot6, ) +from .bom_ref import BomRef @serializable.serializable_enum @@ -769,20 +770,25 @@ def deserialize(cls, o: Any) -> 'XsUri': ) from err @classmethod - def make_bom_link(cls, serialnumber: Union[UUID, str], version: int = 1, bom_ref: Optional[str] = None) -> 'XsUri': + def make_bom_link( + cls, + serial_number: Union[UUID, str], + version: int = 1, + bom_ref: Optional[Union[str, BomRef]] = None + ) -> 'XsUri': """ Generate a BOM-Link URI. Args: - serialnumber (Union[UUID, str]): Unique identifier for the BOM, either as a UUID or a string. - version (int, optional): Version number of the BOM-Link. Defaults to 1. - bom_ref (Optional[str], optional): Reference to a specific component in the BOM. Defaults to None. + serial_number: The unique serial number of the BOM. + version: The version of the BOM. The default version is 1. + bom_ref: The unique identifier of the component, service, or vulnerability within the BOM. Returns: XsUri: Instance of XsUri with the generated BOM-Link URI. """ bom_ref_part = f'#{bom_ref}' if bom_ref else '' - uri = f'urn:cdx:{serialnumber}/{version}{bom_ref_part}' + uri = f'urn:cdx:{serial_number}/{version}{bom_ref_part}' return cls(uri) diff --git a/tests/test_model.py b/tests/test_model.py index c101d8e0..48d6a45d 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -19,6 +19,7 @@ import datetime from enum import Enum from unittest import TestCase +from uuid import uuid4 from ddt import ddt, named_data @@ -545,6 +546,19 @@ def test_sort(self) -> None: expected_uris = reorder(uris, expected_order) self.assertListEqual(sorted_uris, expected_uris) + def test_make_bom_link_without_bom_ref(self) -> None: + serial_number = uuid4() + version = 2 + bom_link = XsUri.make_bom_link(serial_number, version) + self.assertEqual(bom_link, XsUri(f'urn:cdx:{serial_number}/{version}')) + + def test_make_bom_link_with_bom_ref(self) -> None: + serial_number = uuid4() + version = 2 + bom_ref = 'componentA' + bom_link = XsUri.make_bom_link(serial_number, version, bom_ref) + self.assertEqual(bom_link, XsUri(f'urn:cdx:{serial_number}/{version}#{bom_ref}')) + class TestModelProperty(TestCase): diff --git a/tests/test_model_bom.py b/tests/test_model_bom.py index 74046a09..ab80b2e4 100644 --- a/tests/test_model_bom.py +++ b/tests/test_model_bom.py @@ -23,7 +23,7 @@ from ddt import ddt, named_data from cyclonedx.exception.model import LicenseExpressionAlongWithOthersException -from cyclonedx.model import Property +from cyclonedx.model import Property, XsUri from cyclonedx.model.bom import Bom, BomMetaData from cyclonedx.model.bom_ref import BomRef from cyclonedx.model.component import Component, ComponentType @@ -292,3 +292,11 @@ def test_regression_issue_539(self) -> None: self.assertEqual(1, len(d.dependencies)) self.assertIs(component2.bom_ref, d.dependencies[0].ref) # endregion assert component1 + + def test_get_bom_link(self) -> None: + serial_number = uuid4() + version = 1 + bom_ref = 'componentA' + bom = Bom(serial_number=serial_number, version=1) + bom_link = bom.get_bom_link(bom_ref=bom_ref) + self.assertEqual(bom_link, XsUri(f'urn:cdx:{serial_number}/{version}#{bom_ref}')) From e79b1b377657612e700ec7c56da4c73c16b1f1d0 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Sun, 27 Oct 2024 13:07:04 -0400 Subject: [PATCH 05/12] refactor: utilize XsUri.make_bom_link Signed-off-by: Saquib Saifee --- cyclonedx/model/bom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index 363a51b2..93b4e29f 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -678,7 +678,7 @@ def get_bom_link(self, bom_ref: Union[str, BomRef]) -> XsUri: .. note: See the CycloneDX Schema for BOM-Link: https://cyclonedx.org/capabilities/bomlink """ - return XsUri(f'{self.urn}#{bom_ref}') + return XsUri.make_bom_link(self.serial_number, self.version, bom_ref) def validate(self) -> bool: """ From b5a6fe852e7ad246dcb09312fd14514562b5da97 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Mon, 28 Oct 2024 09:45:57 -0400 Subject: [PATCH 06/12] refactor: remove method based on feedback Signed-off-by: Saquib Saifee --- cyclonedx/model/bom.py | 17 +---------------- tests/test_model_bom.py | 10 +--------- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index d2c583f3..65c578fb 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -37,7 +37,7 @@ SchemaVersion1Dot6, ) from ..serialization import LicenseRepositoryHelper, UrnUuidHelper -from . import ExternalReference, Property, XsUri +from . import ExternalReference, Property from .bom_ref import BomRef from .component import Component from .contact import OrganizationalContact, OrganizationalEntity @@ -665,21 +665,6 @@ def register_dependency(self, target: Dependable, depends_on: Optional[Iterable[ def urn(self) -> str: return f'urn:cdx:{self.serial_number}/{self.version}' - def get_bom_link(self, bom_ref: Union[str, BomRef]) -> XsUri: - """ - Generate a BOM-Link URI. - - Args: - bom_ref: The unique identifier of the component, service, or vulnerability within the BOM. - - Returns: - XsUri: Instance of XsUri with the generated BOM-Link URI. - - .. note: - See the CycloneDX Schema for BOM-Link: https://cyclonedx.org/capabilities/bomlink - """ - return XsUri.make_bom_link(self.serial_number, self.version, bom_ref) - def validate(self) -> bool: """ Perform data-model level validations to make sure we have some known data integrity prior to attempting output diff --git a/tests/test_model_bom.py b/tests/test_model_bom.py index ab80b2e4..74046a09 100644 --- a/tests/test_model_bom.py +++ b/tests/test_model_bom.py @@ -23,7 +23,7 @@ from ddt import ddt, named_data from cyclonedx.exception.model import LicenseExpressionAlongWithOthersException -from cyclonedx.model import Property, XsUri +from cyclonedx.model import Property from cyclonedx.model.bom import Bom, BomMetaData from cyclonedx.model.bom_ref import BomRef from cyclonedx.model.component import Component, ComponentType @@ -292,11 +292,3 @@ def test_regression_issue_539(self) -> None: self.assertEqual(1, len(d.dependencies)) self.assertIs(component2.bom_ref, d.dependencies[0].ref) # endregion assert component1 - - def test_get_bom_link(self) -> None: - serial_number = uuid4() - version = 1 - bom_ref = 'componentA' - bom = Bom(serial_number=serial_number, version=1) - bom_link = bom.get_bom_link(bom_ref=bom_ref) - self.assertEqual(bom_link, XsUri(f'urn:cdx:{serial_number}/{version}#{bom_ref}')) From e53ceed4ddf4f2df02186bb19b3a62ab7225dc36 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Mon, 28 Oct 2024 10:46:00 -0400 Subject: [PATCH 07/12] refactor: update the unit tests based on feedback Signed-off-by: Saquib Saifee --- tests/test_model.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/test_model.py b/tests/test_model.py index 48d6a45d..2230a9f3 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -19,7 +19,7 @@ import datetime from enum import Enum from unittest import TestCase -from uuid import uuid4 +from uuid import UUID from ddt import ddt, named_data @@ -43,6 +43,7 @@ Property, XsUri, ) +from cyclonedx.model.bom_ref import BomRef from cyclonedx.model.contact import OrganizationalContact from cyclonedx.model.issue import IssueClassification, IssueType, IssueTypeSource from tests import reorder @@ -547,17 +548,13 @@ def test_sort(self) -> None: self.assertListEqual(sorted_uris, expected_uris) def test_make_bom_link_without_bom_ref(self) -> None: - serial_number = uuid4() - version = 2 - bom_link = XsUri.make_bom_link(serial_number, version) - self.assertEqual(bom_link, XsUri(f'urn:cdx:{serial_number}/{version}')) + bom_link = XsUri.make_bom_link(UUID('e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d'), 2) + self.assertEqual(bom_link.uri, 'urn:cdx:e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d/2') def test_make_bom_link_with_bom_ref(self) -> None: - serial_number = uuid4() - version = 2 - bom_ref = 'componentA' - bom_link = XsUri.make_bom_link(serial_number, version, bom_ref) - self.assertEqual(bom_link, XsUri(f'urn:cdx:{serial_number}/{version}#{bom_ref}')) + bom_link = XsUri.make_bom_link(UUID('e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d'), + 2, BomRef('componentA#sub-componentB%2')) + self.assertEqual(bom_link.uri, 'urn:cdx:e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d/2#componentA#sub-componentB%2') class TestModelProperty(TestCase): From 8f0b78c1f4644b3e579cbc55d425f740a0bd0dcf Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Mon, 28 Oct 2024 12:13:58 -0400 Subject: [PATCH 08/12] fix: encode the reserved char, based on feedback Signed-off-by: Saquib Saifee --- cyclonedx/model/__init__.py | 2 +- tests/test_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cyclonedx/model/__init__.py b/cyclonedx/model/__init__.py index f08870de..39d57973 100644 --- a/cyclonedx/model/__init__.py +++ b/cyclonedx/model/__init__.py @@ -787,7 +787,7 @@ def make_bom_link( Returns: XsUri: Instance of XsUri with the generated BOM-Link URI. """ - bom_ref_part = f'#{bom_ref}' if bom_ref else '' + bom_ref_part = f'#{str(bom_ref).replace("%", "%25").replace("#", "%23")}' if bom_ref else '' uri = f'urn:cdx:{serial_number}/{version}{bom_ref_part}' return cls(uri) diff --git a/tests/test_model.py b/tests/test_model.py index 2230a9f3..52e86e8f 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -554,7 +554,7 @@ def test_make_bom_link_without_bom_ref(self) -> None: def test_make_bom_link_with_bom_ref(self) -> None: bom_link = XsUri.make_bom_link(UUID('e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d'), 2, BomRef('componentA#sub-componentB%2')) - self.assertEqual(bom_link.uri, 'urn:cdx:e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d/2#componentA#sub-componentB%2') + self.assertEqual(bom_link.uri, 'urn:cdx:e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d/2#componentA%23sub-componentB%252') class TestModelProperty(TestCase): From 42fe4701ec9170d9af22d8d18cb274a94c389e31 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Mon, 28 Oct 2024 12:23:26 -0400 Subject: [PATCH 09/12] perf: utilize urllib for encoding Signed-off-by: Saquib Saifee --- cyclonedx/model/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cyclonedx/model/__init__.py b/cyclonedx/model/__init__.py index 39d57973..f06a0ec6 100644 --- a/cyclonedx/model/__init__.py +++ b/cyclonedx/model/__init__.py @@ -28,6 +28,7 @@ from functools import reduce from json import loads as json_loads from typing import Any, Dict, FrozenSet, Generator, Iterable, List, Optional, Tuple, Type, Union +from urllib.parse import quote from uuid import UUID from warnings import warn from xml.etree.ElementTree import Element as XmlElement # nosec B405 @@ -787,7 +788,7 @@ def make_bom_link( Returns: XsUri: Instance of XsUri with the generated BOM-Link URI. """ - bom_ref_part = f'#{str(bom_ref).replace("%", "%25").replace("#", "%23")}' if bom_ref else '' + bom_ref_part = f'#{quote(str(bom_ref))}' if bom_ref else '' uri = f'urn:cdx:{serial_number}/{version}{bom_ref_part}' return cls(uri) From 88c97e8a3fcf410b9e64cd9340eda95e64d3e0df Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Mon, 28 Oct 2024 12:48:17 -0400 Subject: [PATCH 10/12] chore: improve readability Signed-off-by: Saquib Saifee --- cyclonedx/model/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cyclonedx/model/__init__.py b/cyclonedx/model/__init__.py index f06a0ec6..f0d6c8d4 100644 --- a/cyclonedx/model/__init__.py +++ b/cyclonedx/model/__init__.py @@ -28,7 +28,7 @@ from functools import reduce from json import loads as json_loads from typing import Any, Dict, FrozenSet, Generator, Iterable, List, Optional, Tuple, Type, Union -from urllib.parse import quote +from urllib.parse import quote as url_quote from uuid import UUID from warnings import warn from xml.etree.ElementTree import Element as XmlElement # nosec B405 @@ -788,9 +788,8 @@ def make_bom_link( Returns: XsUri: Instance of XsUri with the generated BOM-Link URI. """ - bom_ref_part = f'#{quote(str(bom_ref))}' if bom_ref else '' - uri = f'urn:cdx:{serial_number}/{version}{bom_ref_part}' - return cls(uri) + bom_ref_part = f'#{url_quote(str(bom_ref))}' if bom_ref else '' + return cls(f'urn:cdx:{serial_number}/{version}{bom_ref_part}') @serializable.serializable_class From ea6ab0a576f70b57e8a330bc7ae7d94b9c7449f5 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Mon, 28 Oct 2024 13:27:59 -0400 Subject: [PATCH 11/12] feat: add XsUri.is_bom_link to validate bom_link Signed-off-by: Saquib Saifee --- cyclonedx/model/__init__.py | 9 +++++++++ tests/test_model.py | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/cyclonedx/model/__init__.py b/cyclonedx/model/__init__.py index f0d6c8d4..dc110edb 100644 --- a/cyclonedx/model/__init__.py +++ b/cyclonedx/model/__init__.py @@ -791,6 +791,15 @@ def make_bom_link( bom_ref_part = f'#{url_quote(str(bom_ref))}' if bom_ref else '' return cls(f'urn:cdx:{serial_number}/{version}{bom_ref_part}') + def is_bom_link(self) -> bool: + """ + Check if the URI is a BOM-Link. + + Returns: + `bool` + """ + return self._uri.startswith('urn:cdx:') + @serializable.serializable_class class ExternalReference: diff --git a/tests/test_model.py b/tests/test_model.py index 52e86e8f..46232611 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -556,6 +556,10 @@ def test_make_bom_link_with_bom_ref(self) -> None: 2, BomRef('componentA#sub-componentB%2')) self.assertEqual(bom_link.uri, 'urn:cdx:e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d/2#componentA%23sub-componentB%252') + def test_is_bom_link(self) -> None: + self.assertTrue(XsUri('urn:cdx:e5a93409-fd7c-4ffa-bf7f-6dc1630b1b9d/2').is_bom_link()) + self.assertFalse(XsUri('http://example.com/resource').is_bom_link()) + class TestModelProperty(TestCase): From 3e95ac4cd6108d7947edf72facd5bbe3ea82ea8d Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Mon, 28 Oct 2024 13:53:56 -0400 Subject: [PATCH 12/12] refactor: create a private module-level constant for bom_link_prefix Signed-off-by: Saquib Saifee --- cyclonedx/model/__init__.py | 6 ++++-- cyclonedx/model/bom.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cyclonedx/model/__init__.py b/cyclonedx/model/__init__.py index dc110edb..121bde24 100644 --- a/cyclonedx/model/__init__.py +++ b/cyclonedx/model/__init__.py @@ -55,6 +55,8 @@ ) from .bom_ref import BomRef +_BOM_LINK_PREFIX = 'urn:cdx:' + @serializable.serializable_enum class DataFlow(str, Enum): @@ -789,7 +791,7 @@ def make_bom_link( XsUri: Instance of XsUri with the generated BOM-Link URI. """ bom_ref_part = f'#{url_quote(str(bom_ref))}' if bom_ref else '' - return cls(f'urn:cdx:{serial_number}/{version}{bom_ref_part}') + return cls(f'{_BOM_LINK_PREFIX}{serial_number}/{version}{bom_ref_part}') def is_bom_link(self) -> bool: """ @@ -798,7 +800,7 @@ def is_bom_link(self) -> bool: Returns: `bool` """ - return self._uri.startswith('urn:cdx:') + return self._uri.startswith(_BOM_LINK_PREFIX) @serializable.serializable_class diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index 65c578fb..81b2161b 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -37,7 +37,7 @@ SchemaVersion1Dot6, ) from ..serialization import LicenseRepositoryHelper, UrnUuidHelper -from . import ExternalReference, Property +from . import _BOM_LINK_PREFIX, ExternalReference, Property from .bom_ref import BomRef from .component import Component from .contact import OrganizationalContact, OrganizationalEntity @@ -663,7 +663,7 @@ def register_dependency(self, target: Dependable, depends_on: Optional[Iterable[ self.register_dependency(target=_d2, depends_on=None) def urn(self) -> str: - return f'urn:cdx:{self.serial_number}/{self.version}' + return f'{_BOM_LINK_PREFIX}{self.serial_number}/{self.version}' def validate(self) -> bool: """