-
-
Notifications
You must be signed in to change notification settings - Fork 50
feat: add support for Lifecycles in BOM metadata #698
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
2f01d08
feat: add support for lifecycles in BOM metadata
Churro 8cff89e
review feedback
Churro 028db25
Merge branch 'main' into feat/lifecycles
Churro a4deeaf
rename classes
Churro 3a3b5e9
rework `cyclonedx.model.lifecycle._LifecycleRepositoryHelper.xml_deno…
jkowalleck 2cf3dd8
fix repr
jkowalleck 17f3445
typos and docs
jkowalleck 52f6579
fix setter
jkowalleck 0a44190
condense code
jkowalleck 0e842ad
fix test
jkowalleck df0fdf0
Merge pull request #1 from CycloneDX/Churro/feat/lifecycles
Churro 86ed153
Merge branch 'main' into feat/lifecycles
Churro bdbb93e
fix tests for v8.0.0
Churro bc74a86
fix issues after merge
Churro File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
# This file is part of CycloneDX Python Library | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
# SPDX-License-Identifier: Apache-2.0 | ||
# Copyright (c) OWASP Foundation. All Rights Reserved. | ||
|
||
""" | ||
This set of classes represents the lifecycles types in the CycloneDX standard. | ||
|
||
.. note:: | ||
Introduced in CycloneDX v1.5 | ||
|
||
.. note:: | ||
See the CycloneDX Schema for lifecycles: https://cyclonedx.org/docs/1.5/#metadata_lifecycles | ||
""" | ||
|
||
from enum import Enum | ||
from json import loads as json_loads | ||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union | ||
from xml.etree.ElementTree import Element # nosec B405 | ||
|
||
import serializable | ||
from serializable.helpers import BaseHelper | ||
from sortedcontainers import SortedSet | ||
|
||
from .._internal.compare import ComparableTuple as _ComparableTuple | ||
from ..exception.serialization import CycloneDxDeserializationException | ||
|
||
if TYPE_CHECKING: # pragma: no cover | ||
from serializable import ViewType | ||
|
||
|
||
@serializable.serializable_enum | ||
class LifecyclePhase(str, Enum): | ||
DESIGN = 'design' | ||
PREBUILD = 'pre-build' | ||
BUILD = 'build' | ||
POSTBUILD = 'post-build' | ||
OPERATIONS = 'operations' | ||
DISCOVERY = 'discovery' | ||
DECOMISSION = 'decommission' | ||
|
||
|
||
@serializable.serializable_class | ||
class PredefinedLifecycle: | ||
""" | ||
Object that defines pre-defined phases in the product lifecycle. | ||
|
||
.. note:: | ||
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.5/#metadata_lifecycles | ||
""" | ||
|
||
def __init__(self, phase: LifecyclePhase) -> None: | ||
self._phase = phase | ||
|
||
@property | ||
def phase(self) -> LifecyclePhase: | ||
return self._phase | ||
|
||
@phase.setter | ||
def phase(self, phase: LifecyclePhase) -> None: | ||
self._phase = phase | ||
|
||
def __hash__(self) -> int: | ||
return hash(self._phase) | ||
|
||
def __eq__(self, other: object) -> bool: | ||
if isinstance(other, PredefinedLifecycle): | ||
return hash(other) == hash(self) | ||
return False | ||
|
||
def __lt__(self, other: Any) -> bool: | ||
if isinstance(other, PredefinedLifecycle): | ||
return self._phase < other._phase | ||
if isinstance(other, NamedLifecycle): | ||
return True # put PredefinedLifecycle before any NamedLifecycle | ||
return NotImplemented | ||
|
||
def __repr__(self) -> str: | ||
return f'<PredefinedLifecycle name={self._phase}>' | ||
|
||
|
||
@serializable.serializable_class | ||
class NamedLifecycle: | ||
def __init__(self, name: str, *, description: Optional[str] = None) -> None: | ||
self._name = name | ||
self._description = description | ||
|
||
@property | ||
@serializable.xml_sequence(1) | ||
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) | ||
def name(self) -> str: | ||
""" | ||
Name of the lifecycle phase. | ||
|
||
Returns: | ||
`str` | ||
""" | ||
return self._name | ||
|
||
@name.setter | ||
def name(self, name: str) -> None: | ||
self._name = name | ||
|
||
@property | ||
@serializable.xml_sequence(2) | ||
@serializable.xml_string(serializable.XmlStringSerializationType.NORMALIZED_STRING) | ||
def description(self) -> Optional[str]: | ||
""" | ||
Description of the lifecycle phase. | ||
|
||
Returns: | ||
`str` | ||
""" | ||
return self._description | ||
|
||
@description.setter | ||
def description(self, description: Optional[str]) -> None: | ||
self._description = description | ||
|
||
def __hash__(self) -> int: | ||
return hash((self._name, self._description)) | ||
|
||
def __eq__(self, other: object) -> bool: | ||
if isinstance(other, NamedLifecycle): | ||
return hash(other) == hash(self) | ||
return False | ||
|
||
def __lt__(self, other: Any) -> bool: | ||
if isinstance(other, NamedLifecycle): | ||
return _ComparableTuple((self._name, self._description)) < _ComparableTuple( | ||
(other._name, other._description) | ||
) | ||
if isinstance(other, PredefinedLifecycle): | ||
return False # put NamedLifecycle after any PredefinedLifecycle | ||
return NotImplemented | ||
|
||
def __repr__(self) -> str: | ||
return f'<NamedLifecycle name={self._name}>' | ||
|
||
|
||
Lifecycle = Union[PredefinedLifecycle, NamedLifecycle] | ||
"""TypeAlias for a union of supported lifecycle models. | ||
|
||
- :class:`PredefinedLifecycle` | ||
- :class:`NamedLifecycle` | ||
""" | ||
|
||
if TYPE_CHECKING: # pragma: no cover | ||
# workaround for https://github.com/python/mypy/issues/5264 | ||
# this code path is taken when static code analysis or documentation tools runs through. | ||
class LifecycleRepository(SortedSet[Lifecycle]): | ||
"""Collection of :class:`Lifecycle`. | ||
|
||
This is a `set`, not a `list`. Order MUST NOT matter here. | ||
""" | ||
|
||
else: | ||
|
||
class LifecycleRepository(SortedSet): | ||
"""Collection of :class:`Lifecycle`. | ||
|
||
This is a `set`, not a `list`. Order MUST NOT matter here. | ||
""" | ||
|
||
|
||
class _LifecycleRepositoryHelper(BaseHelper): | ||
@classmethod | ||
def json_normalize(cls, o: LifecycleRepository, *, | ||
view: Optional[Type['ViewType']], | ||
**__: Any) -> Any: | ||
if len(o) == 0: | ||
return None | ||
|
||
return [json_loads(li.as_json( # type:ignore[union-attr] | ||
view_=view)) for li in o] | ||
|
||
@classmethod | ||
def json_denormalize(cls, o: List[Dict[str, Any]], | ||
**__: Any) -> LifecycleRepository: | ||
repo = LifecycleRepository() | ||
for li in o: | ||
if 'phase' in li: | ||
repo.add(PredefinedLifecycle.from_json(li)) # type:ignore[attr-defined] | ||
elif 'name' in li: | ||
repo.add(NamedLifecycle.from_json(li)) # type:ignore[attr-defined] | ||
else: | ||
raise CycloneDxDeserializationException(f'unexpected: {li!r}') | ||
|
||
return repo | ||
|
||
@classmethod | ||
def xml_normalize(cls, o: LifecycleRepository, *, | ||
element_name: str, | ||
view: Optional[Type['ViewType']], | ||
xmlns: Optional[str], | ||
**__: Any) -> Optional[Element]: | ||
if len(o) == 0: | ||
return None | ||
|
||
elem = Element(element_name) | ||
for li in o: | ||
elem.append(li.as_xml( # type:ignore[union-attr] | ||
view_=view, as_string=False, element_name='lifecycle', xmlns=xmlns)) | ||
|
||
return elem | ||
|
||
@classmethod | ||
def xml_denormalize(cls, o: Element, | ||
default_ns: Optional[str], | ||
**__: Any) -> LifecycleRepository: | ||
repo = LifecycleRepository() | ||
|
||
for li in o: | ||
tag = li.tag if default_ns is None else li.tag.replace(f'{{{default_ns}}}', '') | ||
|
||
if tag == 'lifecycle': | ||
stages = list(li) | ||
|
||
predefined_lifecycle = next((el for el in stages if 'phase' in el.tag), None) | ||
named_lifecycle = next((el for el in stages if 'name' in el.tag), None) | ||
if predefined_lifecycle is not None: | ||
repo.add(PredefinedLifecycle.from_xml(li, default_ns)) # type:ignore[attr-defined] | ||
elif named_lifecycle is not None: | ||
repo.add(NamedLifecycle.from_xml(li, default_ns)) # type:ignore[attr-defined] | ||
else: | ||
raise CycloneDxDeserializationException(f'unexpected: {li!r}') | ||
|
||
return repo |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
<?xml version="1.0" ?> | ||
<bom xmlns="http://cyclonedx.org/schema/bom/1.0" version="1"> | ||
<components/> | ||
</bom> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
<?xml version="1.0" ?> | ||
<bom xmlns="http://cyclonedx.org/schema/bom/1.1" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1"> | ||
<components/> | ||
</bom> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"metadata": { | ||
"timestamp": "2023-01-07T13:44:32.312678+00:00", | ||
"tools": [ | ||
{ | ||
"name": "cyclonedx-python-lib", | ||
"vendor": "CycloneDX", | ||
"version": "TESTING" | ||
} | ||
] | ||
}, | ||
"serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac", | ||
"version": 1, | ||
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json", | ||
"bomFormat": "CycloneDX", | ||
"specVersion": "1.2" | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?xml version="1.0" ?> | ||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1"> | ||
<metadata> | ||
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp> | ||
<tools> | ||
<tool> | ||
<vendor>CycloneDX</vendor> | ||
<name>cyclonedx-python-lib</name> | ||
<version>TESTING</version> | ||
</tool> | ||
</tools> | ||
</metadata> | ||
</bom> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.