Skip to content

Commit 3137d16

Browse files
committed
feat: add bom.definitions
for #697 Signed-off-by: Hakan Dilek <[email protected]>
1 parent c72d5f4 commit 3137d16

File tree

4 files changed

+339
-0
lines changed

4 files changed

+339
-0
lines changed

cyclonedx/_internal/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,14 @@
2020
!!! ALL SYMBOLS IN HERE ARE INTERNAL.
2121
Everything might change without any notice.
2222
"""
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

Lines changed: 19 additions & 0 deletions
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 DefinitionRepository
4445
from .dependency import Dependable, Dependency
4546
from .license import License, LicenseExpression, LicenseRepository
4647
from .service import Service
@@ -317,6 +318,7 @@ def __init__(
317318
dependencies: Optional[Iterable[Dependency]] = None,
318319
vulnerabilities: Optional[Iterable[Vulnerability]] = None,
319320
properties: Optional[Iterable[Property]] = None,
321+
definitions: Optional[DefinitionRepository] = None,
320322
) -> None:
321323
"""
322324
Create a new Bom that you can manually/programmatically add data to later.
@@ -333,6 +335,7 @@ def __init__(
333335
self.vulnerabilities = vulnerabilities or [] # type:ignore[assignment]
334336
self.dependencies = dependencies or [] # type:ignore[assignment]
335337
self.properties = properties or [] # type:ignore[assignment]
338+
self.definitions = definitions or DefinitionRepository()
336339

337340
@property
338341
@serializable.type_mapping(UrnUuidHelper)
@@ -520,6 +523,22 @@ def vulnerabilities(self) -> 'SortedSet[Vulnerability]':
520523
def vulnerabilities(self, vulnerabilities: Iterable[Vulnerability]) -> None:
521524
self._vulnerabilities = SortedSet(vulnerabilities)
522525

526+
@property
527+
@serializable.view(SchemaVersion1Dot6)
528+
@serializable.xml_sequence(90)
529+
def definitions(self) -> Optional[DefinitionRepository]:
530+
"""
531+
The repository for definitions
532+
533+
Returns:
534+
`DefinitionRepository`
535+
"""
536+
return self._definitions if len(self._definitions) > 0 else None
537+
538+
@definitions.setter
539+
def definitions(self, definitions: DefinitionRepository) -> None:
540+
self._definitions = definitions
541+
523542
# @property
524543
# ...
525544
# @serializable.view(SchemaVersion1Dot5)

cyclonedx/model/definition.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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 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(serialization_types=[
34+
serializable.SerializationType.JSON,
35+
serializable.SerializationType.XML]
36+
)
37+
class Standard:
38+
"""
39+
A standard of regulations, industry or organizational-specific standards, maturity models, best practices,
40+
or any other requirements.
41+
"""
42+
43+
def __init__(
44+
self, *,
45+
bom_ref: Optional[Union[str, BomRef]] = None,
46+
name: Optional[str] = None,
47+
version: Optional[str] = None,
48+
description: Optional[str] = None,
49+
owner: Optional[str] = None,
50+
external_references: Optional[Iterable['ExternalReference']] = None
51+
) -> None:
52+
self._bom_ref = bom_ref_from_str(bom_ref)
53+
self.name = name
54+
self.version = version
55+
self.description = description
56+
self.owner = owner
57+
self.external_references = external_references or [] # type:ignore[assignment]
58+
59+
def __lt__(self, other: Any) -> bool:
60+
if isinstance(other, Standard):
61+
return (_ComparableTuple((self.bom_ref, self.name, self.version))
62+
< _ComparableTuple((other.bom_ref, other.name, other.version)))
63+
return NotImplemented
64+
65+
def __eq__(self, other: object) -> bool:
66+
if isinstance(other, Standard):
67+
return hash(other) == hash(self)
68+
return False
69+
70+
def __hash__(self) -> int:
71+
return hash((
72+
self.bom_ref, self.name, self.version, self.description, self.owner, tuple(self.external_references)
73+
))
74+
75+
def __repr__(self) -> str:
76+
return f'<Standard bom-ref={self.bom_ref}, name={self.name}, version={self.version}, ' \
77+
f'description={self.description}, owner={self.owner}>'
78+
79+
@property
80+
@serializable.json_name('bom-ref')
81+
@serializable.type_mapping(BomRefHelper)
82+
@serializable.xml_attribute()
83+
@serializable.xml_name('bom-ref')
84+
def bom_ref(self) -> BomRef:
85+
"""
86+
An optional identifier which can be used to reference the standard elsewhere in the BOM. Every bom-ref MUST be
87+
unique within the BOM. If a value was not provided in the constructor, a UUIDv4 will have been assigned.
88+
Returns:
89+
`BomRef`
90+
"""
91+
return self._bom_ref
92+
93+
@property
94+
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
95+
@serializable.xml_sequence(2)
96+
def name(self) -> Optional[str]:
97+
"""
98+
Returns:
99+
The name of the standard
100+
"""
101+
return self._name
102+
103+
@name.setter
104+
def name(self, name: Optional[str]) -> None:
105+
self._name = name
106+
107+
@property
108+
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
109+
@serializable.xml_sequence(3)
110+
def version(self) -> Optional[str]:
111+
"""
112+
Returns:
113+
The version of the standard
114+
"""
115+
return self._version
116+
117+
@version.setter
118+
def version(self, version: Optional[str]) -> None:
119+
self._version = version
120+
121+
@property
122+
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
123+
@serializable.xml_sequence(4)
124+
def description(self) -> Optional[str]:
125+
"""
126+
Returns:
127+
The description of the standard
128+
"""
129+
return self._description
130+
131+
@description.setter
132+
def description(self, description: Optional[str]) -> None:
133+
self._description = description
134+
135+
@property
136+
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
137+
@serializable.xml_sequence(5)
138+
def owner(self) -> Optional[str]:
139+
"""
140+
Returns:
141+
The owner of the standard, often the entity responsible for its release.
142+
"""
143+
return self._owner
144+
145+
@owner.setter
146+
def owner(self, owner: Optional[str]) -> None:
147+
self._owner = owner
148+
149+
# @property
150+
# @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'requirement')
151+
# @serializable.xml_sequence(10)
152+
# def requirements(self) -> 'SortedSet[Requirement]':
153+
# """
154+
# Returns:
155+
# A SortedSet of requirements comprising the standard.
156+
# """
157+
# return self._requirements
158+
#
159+
# @requirements.setter
160+
# def requirements(self, requirements: Iterable[Requirement]) -> None:
161+
# self._requirements = SortedSet(requirements)
162+
#
163+
# @property
164+
# @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'level')
165+
# @serializable.xml_sequence(20)
166+
# def levels(self) -> 'SortedSet[Level]':
167+
# """
168+
# Returns:
169+
# A SortedSet of levels associated with the standard. Some standards have different levels of compliance.
170+
# """
171+
# return self._levels
172+
#
173+
# @levels.setter
174+
# def levels(self, levels: Iterable[Level]) -> None:
175+
# self._levels = SortedSet(levels)
176+
177+
@property
178+
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
179+
@serializable.xml_sequence(30)
180+
def external_references(self) -> 'SortedSet[ExternalReference]':
181+
"""
182+
Returns:
183+
A SortedSet of external references associated with the standard.
184+
"""
185+
return self._external_references
186+
187+
@external_references.setter
188+
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
189+
self._external_references = SortedSet(external_references)
190+
191+
192+
@serializable.serializable_class(name='definitions',
193+
serialization_types=[
194+
serializable.SerializationType.JSON,
195+
serializable.SerializationType.XML]
196+
)
197+
class DefinitionRepository:
198+
"""
199+
The repository for definitions
200+
"""
201+
202+
def __init__(
203+
self, *,
204+
standards: Optional[Iterable[Standard]] = None
205+
) -> None:
206+
self.standards = standards or () # type:ignore[assignment]
207+
208+
@property
209+
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'standard')
210+
@serializable.xml_sequence(1)
211+
def standards(self) -> 'SortedSet[Standard]':
212+
"""
213+
Returns:
214+
A SortedSet of Standards
215+
"""
216+
return self._standards
217+
218+
@standards.setter
219+
def standards(self, standards: Iterable[Standard]) -> None:
220+
self._standards = SortedSet(standards)
221+
222+
def __len__(self) -> int:
223+
return len(self._standards)
224+
225+
def __bool__(self) -> bool:
226+
return len(self._standards) > 0
227+
228+
def __eq__(self, other: object) -> bool:
229+
if not isinstance(other, DefinitionRepository):
230+
return False
231+
232+
return self._standards == other._standards
233+
234+
def __hash__(self) -> int:
235+
return hash((tuple(self._standards)))
236+
237+
def __lt__(self, other: Any) -> bool:
238+
if isinstance(other, DefinitionRepository):
239+
return (_ComparableTuple(self._standards)
240+
< _ComparableTuple(other.standards))
241+
return NotImplemented
242+
243+
def __repr__(self) -> str:
244+
return '<Definitions>'
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 DefinitionRepository, Standard
22+
23+
24+
class TestModelDefinitionRepository(TestCase):
25+
26+
def test_init(self) -> DefinitionRepository:
27+
s = Standard(name='test-standard')
28+
dr = DefinitionRepository(
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.assertEqual(1, len(dr))
37+
self.assertTrue(dr)
38+
39+
def test_empty(self) -> None:
40+
dr = DefinitionRepository()
41+
self.assertEqual(0, len(dr))
42+
self.assertFalse(dr)
43+
44+
def test_unequal_different_type(self) -> None:
45+
dr = DefinitionRepository()
46+
self.assertFalse(dr == 'other')
47+
48+
def test_equal_self(self) -> None:
49+
dr = DefinitionRepository()
50+
dr.standards.add(Standard(name='my-standard'))
51+
self.assertTrue(dr == dr)
52+
53+
def test_unequal(self) -> None:
54+
dr1 = DefinitionRepository()
55+
dr1.standards.add(Standard(name='my-standard'))
56+
tr2 = DefinitionRepository()
57+
self.assertFalse(dr1 == tr2)
58+
59+
def test_equal(self) -> None:
60+
s = Standard(name='my-standard')
61+
dr1 = DefinitionRepository()
62+
dr1.standards.add(s)
63+
tr2 = DefinitionRepository()
64+
tr2.standards.add(s)
65+
self.assertTrue(dr1 == tr2)

0 commit comments

Comments
 (0)