Skip to content

Commit 1b8c21f

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

File tree

3 files changed

+389
-0
lines changed

3 files changed

+389
-0
lines changed

cyclonedx/model/bom.py

Lines changed: 20 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, _DefinitionRepositoryHelper
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,23 @@ def vulnerabilities(self) -> 'SortedSet[Vulnerability]':
520523
def vulnerabilities(self, vulnerabilities: Iterable[Vulnerability]) -> None:
521524
self._vulnerabilities = SortedSet(vulnerabilities)
522525

526+
@property
527+
@serializable.type_mapping(_DefinitionRepositoryHelper)
528+
@serializable.view(SchemaVersion1Dot6)
529+
@serializable.xml_sequence(90)
530+
def definitions(self) -> Optional[DefinitionRepository]:
531+
"""
532+
The repository for definitions
533+
534+
Returns:
535+
`DefinitionRepository`
536+
"""
537+
return self._definitions
538+
539+
@definitions.setter
540+
def definitions(self, definitions: DefinitionRepository) -> None:
541+
self._definitions = definitions
542+
523543
# @property
524544
# ...
525545
# @serializable.view(SchemaVersion1Dot5)

cyclonedx/model/definition.py

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
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, Dict, Iterable, List, Optional, Type, Union
19+
from xml.etree.ElementTree import Element # nosec B405
20+
21+
import serializable
22+
from serializable.helpers import BaseHelper
23+
from sortedcontainers import SortedSet
24+
25+
from .._internal.compare import ComparableTuple as _ComparableTuple
26+
from ..exception.serialization import CycloneDxDeserializationException
27+
from ..serialization import BomRefHelper
28+
from . import ExternalReference
29+
from .bom_ref import BomRef
30+
31+
if TYPE_CHECKING: # pragma: no cover
32+
from serializable import ObjectMetadataLibrary, ViewType
33+
34+
35+
def bom_ref_or_str(bom_ref: Optional[Union[str, BomRef]]) -> BomRef:
36+
if isinstance(bom_ref, BomRef):
37+
return bom_ref
38+
else:
39+
return BomRef(value=str(bom_ref) if bom_ref else None)
40+
41+
42+
@serializable.serializable_class(serialization_types=[
43+
serializable.SerializationType.JSON,
44+
serializable.SerializationType.XML]
45+
)
46+
class Standard:
47+
"""
48+
A standard of regulations, industry or organizational-specific standards, maturity models, best practices,
49+
or any other requirements.
50+
"""
51+
52+
def __init__(
53+
self, *,
54+
bom_ref: Optional[Union[str, BomRef]] = None,
55+
name: Optional[str] = None,
56+
version: Optional[str] = None,
57+
description: Optional[str] = None,
58+
owner: Optional[str] = None,
59+
external_references: Optional[Iterable['ExternalReference']] = None
60+
) -> None:
61+
self._bom_ref = bom_ref_or_str(bom_ref)
62+
self.name = name
63+
self.version = version
64+
self.description = description
65+
self.owner = owner
66+
self.external_references = external_references or [] # type:ignore[assignment]
67+
68+
def __lt__(self, other: Any) -> bool:
69+
if isinstance(other, Standard):
70+
return (_ComparableTuple((self.bom_ref, self.name, self.version))
71+
< _ComparableTuple((other.bom_ref, other.name, other.version)))
72+
return NotImplemented
73+
74+
def __eq__(self, other: object) -> bool:
75+
if isinstance(other, Standard):
76+
return hash(other) == hash(self)
77+
return False
78+
79+
def __hash__(self) -> int:
80+
return hash((
81+
self.bom_ref, self.name, self.version, self.description, self.owner, tuple(self.external_references)
82+
))
83+
84+
def __repr__(self) -> str:
85+
return f'<Standard bom-ref={self.bom_ref}, name={self.name}, version={self.version}, ' \
86+
f'description={self.description}, owner={self.owner}>'
87+
88+
@property
89+
@serializable.json_name('bom-ref')
90+
@serializable.type_mapping(BomRefHelper)
91+
@serializable.xml_attribute()
92+
@serializable.xml_name('bom-ref')
93+
def bom_ref(self) -> BomRef:
94+
"""
95+
An optional identifier which can be used to reference the standard elsewhere in the BOM. Every bom-ref MUST be
96+
unique within the BOM. If a value was not provided in the constructor, a UUIDv4 will have been assigned.
97+
Returns:
98+
`BomRef`
99+
"""
100+
return self._bom_ref
101+
102+
@property
103+
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
104+
@serializable.xml_sequence(2)
105+
def name(self) -> Optional[str]:
106+
"""
107+
Returns:
108+
The name of the standard
109+
"""
110+
return self._name
111+
112+
@name.setter
113+
def name(self, name: Optional[str]) -> None:
114+
self._name = name
115+
116+
@property
117+
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
118+
@serializable.xml_sequence(3)
119+
def version(self) -> Optional[str]:
120+
"""
121+
Returns:
122+
The version of the standard
123+
"""
124+
return self._version
125+
126+
@version.setter
127+
def version(self, version: Optional[str]) -> None:
128+
self._version = version
129+
130+
@property
131+
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
132+
@serializable.xml_sequence(4)
133+
def description(self) -> Optional[str]:
134+
"""
135+
Returns:
136+
The description of the standard
137+
"""
138+
return self._description
139+
140+
@description.setter
141+
def description(self, description: Optional[str]) -> None:
142+
self._description = description
143+
144+
@property
145+
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING)
146+
@serializable.xml_sequence(5)
147+
def owner(self) -> Optional[str]:
148+
"""
149+
Returns:
150+
The owner of the standard, often the entity responsible for its release.
151+
"""
152+
return self._owner
153+
154+
@owner.setter
155+
def owner(self, owner: Optional[str]) -> None:
156+
self._owner = owner
157+
158+
# @property
159+
# @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'requirement')
160+
# @serializable.xml_sequence(10)
161+
# def requirements(self) -> 'SortedSet[Requirement]':
162+
# """
163+
# Returns:
164+
# A SortedSet of requirements comprising the standard.
165+
# """
166+
# return self._requirements
167+
#
168+
# @requirements.setter
169+
# def requirements(self, requirements: Iterable[Requirement]) -> None:
170+
# self._requirements = SortedSet(requirements)
171+
#
172+
# @property
173+
# @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'level')
174+
# @serializable.xml_sequence(20)
175+
# def levels(self) -> 'SortedSet[Level]':
176+
# """
177+
# Returns:
178+
# A SortedSet of levels associated with the standard. Some standards have different levels of compliance.
179+
# """
180+
# return self._levels
181+
#
182+
# @levels.setter
183+
# def levels(self, levels: Iterable[Level]) -> None:
184+
# self._levels = SortedSet(levels)
185+
186+
@property
187+
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
188+
@serializable.xml_sequence(30)
189+
def external_references(self) -> 'SortedSet[ExternalReference]':
190+
"""
191+
Returns:
192+
A SortedSet of external references associated with the standard.
193+
"""
194+
return self._external_references
195+
196+
@external_references.setter
197+
def external_references(self, external_references: Iterable[ExternalReference]) -> None:
198+
self._external_references = SortedSet(external_references)
199+
200+
201+
class DefinitionRepository:
202+
"""
203+
The repository for definitions
204+
"""
205+
206+
def __init__(
207+
self, *,
208+
standards: Optional[Iterable[Standard]] = None
209+
) -> None:
210+
self.standards = standards or () # type:ignore[assignment]
211+
212+
@property
213+
def standards(self) -> 'SortedSet[Standard]':
214+
"""
215+
Returns:
216+
A SortedSet of Standards
217+
"""
218+
return self._standards
219+
220+
@standards.setter
221+
def standards(self, standards: Iterable[Standard]) -> None:
222+
self._standards = SortedSet(standards)
223+
224+
def __len__(self) -> int:
225+
return len(self._standards)
226+
227+
def __bool__(self) -> bool:
228+
return len(self._standards) > 0
229+
230+
def __eq__(self, other: object) -> bool:
231+
if not isinstance(other, DefinitionRepository):
232+
return False
233+
234+
return self._standards == other._standards
235+
236+
def __hash__(self) -> int:
237+
return hash((tuple(self._standards)))
238+
239+
def __lt__(self, other: Any) -> bool:
240+
if isinstance(other, DefinitionRepository):
241+
return (_ComparableTuple(self._standards)
242+
< _ComparableTuple(other.standards))
243+
return NotImplemented
244+
245+
def __repr__(self) -> str:
246+
return '<Definitions>'
247+
248+
249+
class _DefinitionRepositoryHelper(BaseHelper):
250+
"""
251+
Helper class for serializing and deserializing a Definitions.
252+
"""
253+
254+
@classmethod
255+
def json_normalize(cls, o: DefinitionRepository, *,
256+
view: Optional[Type['ViewType']],
257+
**__: Any) -> Any:
258+
elem: Dict[str, Any] = {}
259+
if o.standards:
260+
elem['standards'] = tuple(o.standards)
261+
return elem or None
262+
263+
@classmethod
264+
def json_denormalize(cls, o: Union[List[Dict[str, Any]], Dict[str, Any]],
265+
**__: Any) -> DefinitionRepository:
266+
standards = None
267+
if isinstance(o, Dict):
268+
standards = map(lambda c: Standard.from_json(c), # type:ignore[attr-defined]
269+
o.get('standards', ()))
270+
return DefinitionRepository(standards=standards)
271+
272+
@classmethod
273+
def xml_normalize(cls, o: DefinitionRepository, *,
274+
element_name: str,
275+
view: Optional[Type['ViewType']],
276+
xmlns: Optional[str],
277+
**__: Any) -> Optional[Element]:
278+
elem = Element(element_name)
279+
if o.standards:
280+
elem_s = Element(f'{{{xmlns}}}standards' if xmlns else 'standards')
281+
elem_s.extend(
282+
si.as_xml( # type:ignore[attr-defined]
283+
view_=view, as_string=False, element_name='standard', xmlns=xmlns)
284+
for si in o.standards)
285+
elem.append(elem_s)
286+
return elem \
287+
if len(elem) > 0 \
288+
else None
289+
290+
@classmethod
291+
def xml_denormalize(cls, o: Element, *,
292+
default_ns: Optional[str],
293+
prop_info: 'ObjectMetadataLibrary.SerializableProperty',
294+
ctx: Type[Any],
295+
**kwargs: Any) -> DefinitionRepository:
296+
standards = None
297+
for e in o:
298+
tag = e.tag if default_ns is None else e.tag.replace(f'{{{default_ns}}}', '')
299+
if tag == 'standards':
300+
standards = map(lambda s: Standard.from_xml( # type:ignore[attr-defined]
301+
s, default_ns), e)
302+
else:
303+
raise CycloneDxDeserializationException(f'unexpected: {e!r}')
304+
return DefinitionRepository(standards=standards)

0 commit comments

Comments
 (0)