Skip to content

Commit d189f2c

Browse files
authored
BREAKING CHANGE: added new model BomRef unlocking logic later to ensure uniquness and dependency references (#174)
Signed-off-by: Paul Horton <[email protected]>
1 parent 020fcf0 commit d189f2c

27 files changed

+192
-185
lines changed

cyclonedx/model/bom_ref.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# encoding: utf-8
2+
3+
# This file is part of CycloneDX Python Lib
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
# SPDX-License-Identifier: Apache-2.0
18+
# Copyright (c) OWASP Foundation. All Rights Reserved.
19+
from typing import Optional
20+
from uuid import uuid4
21+
22+
23+
class BomRef:
24+
"""
25+
An identifier that can be used to reference objects elsewhere in the BOM.
26+
27+
This copies a similar pattern used in the CycloneDX Python Library.
28+
29+
.. note::
30+
See https://github.com/CycloneDX/cyclonedx-php-library/blob/master/docs/dev/decisions/BomDependencyDataModel.md
31+
"""
32+
33+
def __init__(self, value: Optional[str] = None) -> None:
34+
self.value = value or str(uuid4())
35+
36+
@property
37+
def value(self) -> str:
38+
return self._value
39+
40+
@value.setter
41+
def value(self, value: str) -> None:
42+
self._value = value
43+
44+
def __eq__(self, other: object) -> bool:
45+
if isinstance(other, BomRef):
46+
return hash(other) == hash(self)
47+
return False
48+
49+
def __hash__(self) -> int:
50+
return hash(self.value)
51+
52+
def __repr__(self) -> str:
53+
return self.value

cyclonedx/model/component.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
from enum import Enum
2121
from os.path import exists
2222
from typing import Iterable, Optional, Set
23-
from uuid import uuid4
2423

2524
# See https://github.com/package-url/packageurl-python/issues/65
2625
from packageurl import PackageURL # type: ignore
2726

2827
from . import AttachedText, Copyright, ExternalReference, HashAlgorithm, HashType, IdentifiableAction, LicenseChoice, \
2928
OrganizationalEntity, Property, sha1sum, XsUri
29+
from .bom_ref import BomRef
3030
from .issue import IssueType
3131
from .release_note import ReleaseNotes
3232
from .vulnerability import Vulnerability
@@ -692,7 +692,7 @@ def __init__(self, *, name: str, component_type: ComponentType = ComponentType.L
692692
) -> None:
693693
self.type = component_type
694694
self.mime_type = mime_type
695-
self.bom_ref = bom_ref or str(uuid4())
695+
self._bom_ref = BomRef(value=bom_ref)
696696
self.supplier = supplier
697697
self.author = author
698698
self.publisher = publisher
@@ -766,22 +766,18 @@ def mime_type(self, mime_type: Optional[str]) -> None:
766766
self._mime_type = mime_type
767767

768768
@property
769-
def bom_ref(self) -> str:
769+
def bom_ref(self) -> BomRef:
770770
"""
771771
An optional identifier which can be used to reference the component elsewhere in the BOM. Every bom-ref MUST be
772772
unique within the BOM.
773773
774774
If a value was not provided in the constructor, a UUIDv4 will have been assigned.
775775
776776
Returns:
777-
`str` as a unique identifiers for this Component
777+
`BomRef`
778778
"""
779779
return self._bom_ref
780780

781-
@bom_ref.setter
782-
def bom_ref(self, bom_ref: str) -> None:
783-
self._bom_ref = bom_ref
784-
785781
@property
786782
def supplier(self) -> Optional[OrganizationalEntity]:
787783
"""

cyclonedx/model/service.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
# SPDX-License-Identifier: Apache-2.0
1616
# Copyright (c) OWASP Foundation. All Rights Reserved.
1717
from typing import Iterable, Optional, Set
18-
from uuid import uuid4
1918

2019
from . import ExternalReference, DataClassification, LicenseChoice, OrganizationalEntity, Property, XsUri
20+
from .bom_ref import BomRef
2121
from .release_note import ReleaseNotes
2222

2323
"""
@@ -46,7 +46,7 @@ def __init__(self, *, name: str, bom_ref: Optional[str] = None, provider: Option
4646
services: Optional[Iterable['Service']] = None,
4747
release_notes: Optional[ReleaseNotes] = None,
4848
) -> None:
49-
self.bom_ref = bom_ref or str(uuid4())
49+
self._bom_ref = BomRef(value=bom_ref)
5050
self.provider = provider
5151
self.group = group
5252
self.name = name
@@ -63,22 +63,18 @@ def __init__(self, *, name: str, bom_ref: Optional[str] = None, provider: Option
6363
self.properties = set(properties or [])
6464

6565
@property
66-
def bom_ref(self) -> Optional[str]:
66+
def bom_ref(self) -> BomRef:
6767
"""
6868
An optional identifier which can be used to reference the service elsewhere in the BOM. Uniqueness is enforced
6969
within all elements and children of the root-level bom element.
7070
7171
If a value was not provided in the constructor, a UUIDv4 will have been assigned.
7272
7373
Returns:
74-
`str` unique identifier for this Service
74+
`BomRef` unique identifier for this Service
7575
"""
7676
return self._bom_ref
7777

78-
@bom_ref.setter
79-
def bom_ref(self, bom_ref: Optional[str]) -> None:
80-
self._bom_ref = bom_ref
81-
8278
@property
8379
def provider(self) -> Optional[OrganizationalEntity]:
8480
"""

cyclonedx/output/serializer/json.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
#
1717
# SPDX-License-Identifier: Apache-2.0
1818
# Copyright (c) OWASP Foundation. All Rights Reserved.
19-
2019
from datetime import datetime
2120
from decimal import Decimal
2221
from enum import Enum
@@ -28,8 +27,9 @@
2827
# See https://github.com/package-url/packageurl-python/issues/65
2928
from packageurl import PackageURL # type: ignore
3029

31-
from cyclonedx.model import XsUri
32-
from cyclonedx.model.component import Component
30+
from ...model import XsUri
31+
from ...model.bom_ref import BomRef
32+
from ...model.component import Component
3333

3434
HYPHENATED_ATTRIBUTES = [
3535
'bom_ref', 'mime_type', 'x_trust_boundary'
@@ -40,6 +40,10 @@
4040
class CycloneDxJSONEncoder(JSONEncoder):
4141

4242
def default(self, o: Any) -> Any:
43+
# BomRef
44+
if isinstance(o, BomRef):
45+
return str(o)
46+
4347
# datetime
4448
if isinstance(o, datetime):
4549
return o.isoformat()

cyclonedx/output/xml.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from ..model import AttachedText, ExternalReference, HashType, IdentifiableAction, LicenseChoice, \
2828
OrganizationalEntity, OrganizationalContact, Property, Tool
2929
from ..model.bom import Bom
30+
from ..model.bom_ref import BomRef
3031
from ..model.component import Component, Patch
3132
from ..model.release_note import ReleaseNotes
3233
from ..model.service import Service
@@ -174,7 +175,7 @@ def _add_metadata_element(self) -> None:
174175
def _add_component_element(self, component: Component) -> ElementTree.Element:
175176
element_attributes = {'type': component.type.value}
176177
if self.component_supports_bom_ref_attribute() and component.bom_ref:
177-
element_attributes['bom-ref'] = component.bom_ref
178+
element_attributes['bom-ref'] = str(component.bom_ref)
178179
if self.component_supports_mime_type_attribute() and component.mime_type:
179180
element_attributes['mime-type'] = component.mime_type
180181

@@ -450,7 +451,7 @@ def _add_properties_element(properties: Set[Property], parent_element: ElementTr
450451
def _add_service_element(self, service: Service) -> ElementTree.Element:
451452
element_attributes = {}
452453
if service.bom_ref:
453-
element_attributes['bom-ref'] = service.bom_ref
454+
element_attributes['bom-ref'] = str(service.bom_ref)
454455

455456
service_element = ElementTree.Element('service', element_attributes)
456457

@@ -654,10 +655,10 @@ def _get_vulnerability_as_xml_element_post_1_4(self, vulnerability: Vulnerabilit
654655
return vulnerability_element
655656

656657
@staticmethod
657-
def _get_vulnerability_as_xml_element_pre_1_3(bom_ref: str,
658+
def _get_vulnerability_as_xml_element_pre_1_3(bom_ref: BomRef,
658659
vulnerability: Vulnerability) -> ElementTree.Element:
659660
vulnerability_element = ElementTree.Element('v:vulnerability', {
660-
'ref': bom_ref
661+
'ref': str(bom_ref)
661662
})
662663

663664
# id

tests/data.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@
4545
MOCK_UUID_5 = 'bb5911d6-1a1d-41c9-b6e0-46e848d16655'
4646
MOCK_UUID_6 = 'df70b5f1-8f53-47a4-be48-669ae78795e6'
4747

48+
TEST_UUIDS = [
49+
MOCK_UUID_1, MOCK_UUID_2, MOCK_UUID_3, MOCK_UUID_4, MOCK_UUID_5, MOCK_UUID_6
50+
]
51+
4852

4953
def get_bom_with_component_setuptools_basic() -> Bom:
5054
return Bom(components=[get_component_setuptools_simple()])

tests/fixtures/json/1.2/bom_services_complex.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
],
1616
"component": {
1717
"type": "library",
18-
"bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
18+
"bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
1919
"name": "cyclonedx-python-lib",
2020
"version": "1.0.0"
2121
}
@@ -76,7 +76,7 @@
7676
]
7777
},
7878
{
79-
"bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
79+
"bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3",
8080
"name": "my-second-service"
8181
}
8282
]

tests/fixtures/json/1.2/bom_services_nested.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"bomFormat": "CycloneDX",
44
"metadata": {
55
"component": {
6-
"bom-ref": "bb5911d6-1a1d-41c9-b6e0-46e848d16655",
6+
"bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
77
"name": "cyclonedx-python-lib",
88
"type": "library",
99
"version": "1.0.0"
@@ -72,7 +72,7 @@
7272
},
7373
"services": [
7474
{
75-
"bom-ref": "df70b5f1-8f53-47a4-be48-669ae78795e6",
75+
"bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3",
7676
"name": "first-nested-service"
7777
},
7878
{
@@ -105,11 +105,11 @@
105105
"x-trust-boundary": true
106106
},
107107
{
108-
"bom-ref": "df70b5f1-8f53-47a4-be48-669ae78795e6",
108+
"bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
109109
"name": "my-second-service",
110110
"services": [
111111
{
112-
"bom-ref": "df70b5f1-8f53-47a4-be48-669ae78795e6",
112+
"bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
113113
"group": "what-group",
114114
"name": "yet-another-nested-service",
115115
"provider": {

tests/fixtures/json/1.2/bom_services_simple.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,18 @@
1515
],
1616
"component": {
1717
"type": "library",
18-
"bom-ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
18+
"bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
1919
"name": "cyclonedx-python-lib",
2020
"version": "1.0.0"
2121
}
2222
},
2323
"services": [
2424
{
25-
"bom-ref": "bb5911d6-1a1d-41c9-b6e0-46e848d16655",
25+
"bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3",
2626
"name": "my-first-service"
2727
},
2828
{
29-
"bom-ref": "bb5911d6-1a1d-41c9-b6e0-46e848d16655",
29+
"bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
3030
"name": "my-second-service"
3131
}
3232
]

tests/fixtures/json/1.3/bom_services_complex.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
],
1616
"component": {
1717
"type": "library",
18-
"bom-ref": "bb5911d6-1a1d-41c9-b6e0-46e848d16655",
18+
"bom-ref": "17e3b199-dc0b-42ef-bfdd-1fa81a1e3eda",
1919
"name": "cyclonedx-python-lib",
2020
"version": "1.0.0"
2121
}
@@ -86,7 +86,7 @@
8686
]
8787
},
8888
{
89-
"bom-ref": "0b049d09-64c0-4490-a0f5-c84d9aacf857",
89+
"bom-ref": "be2c6502-7e9a-47db-9a66-e34f729810a3",
9090
"name": "my-second-service"
9191
}
9292
]

0 commit comments

Comments
 (0)