Skip to content

Commit 61de768

Browse files
committed
feat: add bom.definitions
for CycloneDX#697 Signed-off-by: Hakan Dilek <[email protected]>
1 parent 10c4ec2 commit 61de768

File tree

4 files changed

+348
-0
lines changed

4 files changed

+348
-0
lines changed

cyclonedx/_internal/bom_ref.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# This file is part of CycloneDX Python Library
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
# Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
19+
"""
20+
!!! ALL SYMBOLS IN HERE ARE INTERNAL.
21+
Everything might change without any notice.
22+
"""
23+
24+
from typing import Optional, Union
25+
26+
from ..model.bom_ref import BomRef
27+
28+
29+
def bom_ref_from_str(bom_ref: Optional[Union[str, BomRef]]) -> BomRef:
30+
if isinstance(bom_ref, BomRef):
31+
return bom_ref
32+
else:
33+
return BomRef(value=str(bom_ref) if bom_ref else None)

cyclonedx/model/bom.py

+18
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from .bom_ref import BomRef
4242
from .component import Component
4343
from .contact import OrganizationalContact, OrganizationalEntity
44+
from .definition import Definitions
4445
from .dependency import Dependable, Dependency
4546
from .license import License, LicenseExpression, LicenseRepository
4647
from .lifecycle import Lifecycle, LifecycleRepository, _LifecycleRepositoryHelper
@@ -327,6 +328,7 @@ def __init__(
327328
dependencies: Optional[Iterable[Dependency]] = None,
328329
vulnerabilities: Optional[Iterable[Vulnerability]] = None,
329330
properties: Optional[Iterable[Property]] = None,
331+
definitions: Optional[Definitions] = None,
330332
) -> None:
331333
"""
332334
Create a new Bom that you can manually/programmatically add data to later.
@@ -343,6 +345,7 @@ def __init__(
343345
self.vulnerabilities = vulnerabilities or [] # type:ignore[assignment]
344346
self.dependencies = dependencies or [] # type:ignore[assignment]
345347
self.properties = properties or [] # type:ignore[assignment]
348+
self.definitions = definitions or Definitions()
346349

347350
@property
348351
@serializable.type_mapping(UrnUuidHelper)
@@ -552,6 +555,21 @@ def vulnerabilities(self, vulnerabilities: Iterable[Vulnerability]) -> None:
552555
# def formulation(self, ...) -> None:
553556
# ... # TODO Since CDX 1.5
554557

558+
@property
559+
@serializable.view(SchemaVersion1Dot6)
560+
@serializable.xml_sequence(110)
561+
def definitions(self) -> Optional[Definitions]:
562+
"""
563+
The repository for definitions
564+
Returns:
565+
`Definitions`
566+
"""
567+
return self._definitions if len(self._definitions.standards) > 0 else None
568+
569+
@definitions.setter
570+
def definitions(self, definitions: Definitions) -> None:
571+
self._definitions = definitions
572+
555573
def get_component_by_purl(self, purl: Optional['PackageURL']) -> Optional[Component]:
556574
"""
557575
Get a Component already in the Bom by its PURL

cyclonedx/model/definition.py

+230
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# This file is part of CycloneDX Python Library
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
# Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
from typing import TYPE_CHECKING, Any, Iterable, Optional, Union
19+
20+
import serializable
21+
from sortedcontainers import SortedSet
22+
23+
from .._internal.bom_ref import bom_ref_from_str
24+
from .._internal.compare import ComparableTuple as _ComparableTuple
25+
from ..serialization import BomRefHelper
26+
from . import ExternalReference
27+
from .bom_ref import BomRef
28+
29+
if TYPE_CHECKING: # pragma: no cover
30+
pass
31+
32+
33+
@serializable.serializable_class
34+
class Standard:
35+
"""
36+
A standard of regulations, industry or organizational-specific standards, maturity models, best practices,
37+
or any other requirements.
38+
"""
39+
40+
def __init__(
41+
self, *,
42+
bom_ref: Optional[Union[str, BomRef]] = None,
43+
name: Optional[str] = None,
44+
version: Optional[str] = None,
45+
description: Optional[str] = None,
46+
owner: Optional[str] = None,
47+
external_references: Optional[Iterable['ExternalReference']] = None
48+
) -> None:
49+
self._bom_ref = bom_ref_from_str(bom_ref)
50+
self.name = name
51+
self.version = version
52+
self.description = description
53+
self.owner = owner
54+
self.external_references = external_references or [] # type:ignore[assignment]
55+
56+
def __lt__(self, other: Any) -> bool:
57+
if isinstance(other, Standard):
58+
return (_ComparableTuple((self.bom_ref, self.name, self.version))
59+
< _ComparableTuple((other.bom_ref, other.name, other.version)))
60+
return NotImplemented
61+
62+
def __eq__(self, other: object) -> bool:
63+
if isinstance(other, Standard):
64+
return hash(other) == hash(self)
65+
return False
66+
67+
def __hash__(self) -> int:
68+
return hash((
69+
self.bom_ref, self.name, self.version, self.description, self.owner, tuple(self.external_references)
70+
))
71+
72+
def __repr__(self) -> str:
73+
return f'<Standard bom-ref={self.bom_ref}, name={self.name}, version={self.version}, ' \
74+
f'description={self.description}, owner={self.owner}>'
75+
76+
@property
77+
@serializable.json_name('bom-ref')
78+
@serializable.type_mapping(BomRefHelper)
79+
@serializable.xml_attribute()
80+
@serializable.xml_name('bom-ref')
81+
def bom_ref(self) -> BomRef:
82+
"""
83+
An optional identifier which can be used to reference the standard elsewhere in the BOM. Every bom-ref MUST be
84+
unique within the BOM. If a value was not provided in the constructor, a UUIDv4 will have been assigned.
85+
Returns:
86+
`BomRef`
87+
"""
88+
return self._bom_ref
89+
90+
@property
91+
@serializable.xml_sequence(1)
92+
def name(self) -> Optional[str]:
93+
"""
94+
Returns:
95+
The name of the standard
96+
"""
97+
return self._name
98+
99+
@name.setter
100+
def name(self, name: Optional[str]) -> None:
101+
self._name = name
102+
103+
@property
104+
@serializable.xml_sequence(2)
105+
def version(self) -> Optional[str]:
106+
"""
107+
Returns:
108+
The version of the standard
109+
"""
110+
return self._version
111+
112+
@version.setter
113+
def version(self, version: Optional[str]) -> None:
114+
self._version = version
115+
116+
@property
117+
@serializable.xml_sequence(3)
118+
def description(self) -> Optional[str]:
119+
"""
120+
Returns:
121+
The description of the standard
122+
"""
123+
return self._description
124+
125+
@description.setter
126+
def description(self, description: Optional[str]) -> None:
127+
self._description = description
128+
129+
@property
130+
@serializable.xml_sequence(4)
131+
def owner(self) -> Optional[str]:
132+
"""
133+
Returns:
134+
The owner of the standard, often the entity responsible for its release.
135+
"""
136+
return self._owner
137+
138+
@owner.setter
139+
def owner(self, owner: Optional[str]) -> None:
140+
self._owner = owner
141+
142+
# @property
143+
# @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'requirement')
144+
# @serializable.xml_sequence(5)
145+
# def requirements(self) -> 'SortedSet[Requirement]':
146+
# """
147+
# Returns:
148+
# A SortedSet of requirements comprising the standard.
149+
# """
150+
# return self._requirements
151+
#
152+
# @requirements.setter
153+
# def requirements(self, requirements: Iterable[Requirement]) -> None:
154+
# self._requirements = SortedSet(requirements)
155+
#
156+
# @property
157+
# @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'level')
158+
# @serializable.xml_sequence(6)
159+
# def levels(self) -> 'SortedSet[Level]':
160+
# """
161+
# Returns:
162+
# A SortedSet of levels associated with the standard. Some standards have different levels of compliance.
163+
# """
164+
# return self._levels
165+
#
166+
# @levels.setter
167+
# def levels(self, levels: Iterable[Level]) -> None:
168+
# self._levels = SortedSet(levels)
169+
170+
@property
171+
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
172+
@serializable.xml_sequence(7)
173+
def external_references(self) -> 'SortedSet[ExternalReference]':
174+
"""
175+
Returns:
176+
A SortedSet of external references associated with the standard.
177+
"""
178+
return self._external_references
179+
180+
@external_references.setter
181+
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
182+
self._external_references = SortedSet(external_references)
183+
184+
185+
@serializable.serializable_class(name='definitions')
186+
class Definitions:
187+
"""
188+
The repository for definitions
189+
"""
190+
191+
def __init__(
192+
self, *,
193+
standards: Optional[Iterable[Standard]] = None
194+
) -> None:
195+
self.standards = standards or () # type:ignore[assignment]
196+
197+
@property
198+
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'standard')
199+
@serializable.xml_sequence(1)
200+
def standards(self) -> 'SortedSet[Standard]':
201+
"""
202+
Returns:
203+
A SortedSet of Standards
204+
"""
205+
return self._standards
206+
207+
@standards.setter
208+
def standards(self, standards: Iterable[Standard]) -> None:
209+
self._standards = SortedSet(standards)
210+
211+
def __bool__(self) -> bool:
212+
return len(self._standards) > 0
213+
214+
def __eq__(self, other: object) -> bool:
215+
if not isinstance(other, Definitions):
216+
return False
217+
218+
return self._standards == other._standards
219+
220+
def __hash__(self) -> int:
221+
return hash((tuple(self._standards)))
222+
223+
def __lt__(self, other: Any) -> bool:
224+
if isinstance(other, Definitions):
225+
return (_ComparableTuple(self._standards)
226+
< _ComparableTuple(other.standards))
227+
return NotImplemented
228+
229+
def __repr__(self) -> str:
230+
return '<Definitions>'

tests/test_model_definition.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# This file is part of CycloneDX Python Library
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
# Copyright (c) OWASP Foundation. All Rights Reserved.
17+
18+
19+
from unittest import TestCase
20+
21+
from cyclonedx.model.definition import Definitions, Standard
22+
23+
24+
class TestModelDefinitionRepository(TestCase):
25+
26+
def test_init(self) -> Definitions:
27+
s = Standard(name='test-standard')
28+
dr = Definitions(
29+
standards=(s, ),
30+
)
31+
self.assertIs(s, tuple(dr.standards)[0])
32+
return dr
33+
34+
def test_filled(self) -> None:
35+
dr = self.test_init()
36+
self.assertIsNotNone(dr.standards)
37+
self.assertEqual(1, len(dr.standards))
38+
self.assertTrue(dr)
39+
40+
def test_empty(self) -> None:
41+
dr = Definitions()
42+
self.assertIsNotNone(dr.standards)
43+
self.assertEqual(0, len(dr.standards))
44+
self.assertFalse(dr)
45+
46+
def test_unequal_different_type(self) -> None:
47+
dr = Definitions()
48+
self.assertFalse(dr == 'other')
49+
50+
def test_equal_self(self) -> None:
51+
dr = Definitions()
52+
dr.standards.add(Standard(name='my-standard'))
53+
self.assertTrue(dr == dr)
54+
55+
def test_unequal(self) -> None:
56+
dr1 = Definitions()
57+
dr1.standards.add(Standard(name='my-standard'))
58+
tr2 = Definitions()
59+
self.assertFalse(dr1 == tr2)
60+
61+
def test_equal(self) -> None:
62+
s = Standard(name='my-standard')
63+
dr1 = Definitions()
64+
dr1.standards.add(s)
65+
tr2 = Definitions()
66+
tr2.standards.add(s)
67+
self.assertTrue(dr1 == tr2)

0 commit comments

Comments
 (0)