Skip to content

Commit fb3f835

Browse files
authored
fix: serialize dependency graph for nested components (#329)
* tests: regression tests for issue #328 * fix: for issue #328 Signed-off-by: Jan Kowalleck <[email protected]>
1 parent ab862e7 commit fb3f835

File tree

109 files changed

+799
-197
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+799
-197
lines changed

cyclonedx/output/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
import os
2424
from abc import ABC, abstractmethod
2525
from enum import Enum
26-
from typing import cast
26+
from typing import Iterable, Union, cast
2727

2828
from ..model.bom import Bom
29+
from ..model.component import Component
2930

3031

3132
class OutputFormat(str, Enum):
@@ -60,6 +61,11 @@ def __init__(self, bom: Bom, **kwargs: int) -> None:
6061
self._bom = bom
6162
self._generated: bool = False
6263

64+
def _chained_components(self, container: Union[Bom, Component]) -> Iterable[Component]:
65+
for component in container.components:
66+
yield component
67+
yield from self._chained_components(component)
68+
6369
@property
6470
@abstractmethod
6571
def schema_version(self) -> SchemaVersion:

cyclonedx/output/json.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@
1919

2020
import json
2121
from abc import abstractmethod
22-
from typing import Any, Dict, Iterable, List, Optional, Union
22+
from typing import Any, Dict, List, Optional, Union
2323

2424
from ..exception.output import FormatNotSupportedException
2525
from ..model.bom import Bom
26-
from ..model.component import Component
2726
from . import BaseOutput, SchemaVersion
2827
from .schema import (
2928
BaseSchemaVersion,
@@ -66,7 +65,7 @@ def generate(self, force_regeneration: bool = False) -> None:
6665

6766
extras = {}
6867
if self.bom_supports_dependencies():
69-
dep_components: Iterable[Component] = bom.components
68+
dep_components = self._chained_components(bom)
7069
if bom.metadata.component:
7170
dep_components = [bom.metadata.component, *dep_components]
7271
dependencies = []

cyclonedx/output/serializer/json.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def default(self, o: Any) -> Any:
7979
d: Dict[Any, Any] = {}
8080
for k, v in o.__dict__.items():
8181
# Remove leading _ in key names
82-
new_key = k[1:]
82+
new_key = k[1:] if k[0] == '_' else k
8383
if new_key.startswith('_') or '__' in new_key:
8484
continue
8585

cyclonedx/output/xml.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# Copyright (c) OWASP Foundation. All Rights Reserved.
1919

2020
import warnings
21-
from typing import Iterable, Optional
21+
from typing import Optional
2222
from xml.etree import ElementTree
2323

2424
from sortedcontainers import SortedSet
@@ -111,7 +111,7 @@ def generate(self, force_regeneration: bool = False) -> None:
111111
)
112112

113113
if self.bom_supports_dependencies() and (bom.metadata.component or bom.components):
114-
dep_components: Iterable[Component] = bom.components
114+
dep_components = self._chained_components(bom)
115115
if bom.metadata.component:
116116
dep_components = [bom.metadata.component, *dep_components]
117117
dependencies_element = ElementTree.SubElement(self._root_bom_element, 'dependencies')

tests/data.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,10 @@ def get_bom_with_nested_services() -> Bom:
320320

321321

322322
def get_bom_for_issue_275_components() -> Bom:
323+
"""regression test for issue #275
324+
see https://github.com/CycloneDX/cyclonedx-python-lib/issues/275
325+
"""
326+
323327
app = Component(bom_ref=MOCK_UUID_1, name="app", version="1.0.0")
324328
comp_a = Component(bom_ref=MOCK_UUID_2, name="comp_a", version="1.0.0")
325329
comp_b = Component(bom_ref=MOCK_UUID_3, name="comp_b", version="1.0.0")
@@ -338,17 +342,45 @@ def get_bom_for_issue_275_components() -> Bom:
338342

339343

340344
# def get_bom_for_issue_275_services() -> Bom:
341-
# app = Component(name="app", version="1.0.0")
342-
# serv_a = Service(name='Service A')
343-
# serv_b = Service(name='Service B')
344-
# serv_c = Service(name='Service C')
345+
# """regression test for issue #275
346+
# see https://github.com/CycloneDX/cyclonedx-python-lib/issues/275
347+
# """
348+
# app = Component(name="app", version="1.0.0")
349+
# serv_a = Service(name='Service A')
350+
# serv_b = Service(name='Service B')
351+
# serv_c = Service(name='Service C')
345352
#
346-
# serv_b.services.add(serv_c)
347-
# serv_b.dependencies.add(serv_c.bom_ref)
353+
# serv_b.services.add(serv_c)
354+
# serv_b.dependencies.add(serv_c.bom_ref)
348355
#
349-
# bom = Bom(services=[serv_a, serv_b])
350-
# bom.metadata.component = app
351-
# return bom
356+
# bom = Bom(services=[serv_a, serv_b])
357+
# bom.metadata.component = app
358+
# return bom
359+
360+
361+
def get_bom_for_issue_328_components() -> Bom:
362+
"""regression test for issue #328
363+
see https://github.com/CycloneDX/cyclonedx-python-lib/issues/328
364+
"""
365+
366+
comp_root = Component(component_type=ComponentType.APPLICATION,
367+
name='my-project', version='1', bom_ref='my-project')
368+
comp_a = Component(name='A', version='0.1', bom_ref='component-A')
369+
comp_b = Component(name='B', version='1.0', bom_ref='component-B')
370+
comp_c = Component(name='C', version='1.0', bom_ref='component-C')
371+
372+
# Make a tree of components A -> B -> C
373+
comp_a.components = [comp_b]
374+
comp_b.components = [comp_c]
375+
# Declare dependencies the same way: A -> B -> C
376+
comp_a.dependencies = [comp_b.bom_ref]
377+
comp_b.dependencies = [comp_c.bom_ref]
378+
379+
bom = Bom()
380+
bom.metadata.component = comp_root
381+
comp_root.dependencies = [comp_a.bom_ref]
382+
bom.components = [comp_a]
383+
return bom
352384

353385

354386
def get_component_setuptools_complete(include_pedigree: bool = True) -> Component:
@@ -376,7 +408,8 @@ def get_component_setuptools_complete(include_pedigree: bool = True) -> Componen
376408

377409

378410
def get_component_setuptools_simple(
379-
bom_ref: Optional[str] = 'pkg:pypi/[email protected]?extension=tar.gz') -> Component:
411+
bom_ref: Optional[str] = 'pkg:pypi/[email protected]?extension=tar.gz'
412+
) -> Component:
380413
return Component(
381414
name='setuptools', version='50.3.2',
382415
bom_ref=bom_ref,

tests/fixtures/json/1.2/bom_dependencies.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
33
"bomFormat": "CycloneDX",
44
"specVersion": "1.2",
5-
"serialNumber": "urn:uuid:7ddbdb7d-6f51-4ea6-83c1-bf780cece8e2",
5+
"serialNumber": "urn:uuid:6dc366e9-89eb-48cc-9ae8-49a810ce3dcb",
66
"version": 1,
77
"metadata": {
8-
"timestamp": "2023-01-07T11:40:00.491974+00:00",
8+
"timestamp": "2023-01-07T13:45:57.575599+00:00",
99
"tools": [
1010
{
1111
"vendor": "CycloneDX",

tests/fixtures/json/1.2/bom_dependencies_component.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
33
"bomFormat": "CycloneDX",
44
"specVersion": "1.2",
5-
"serialNumber": "urn:uuid:d3c403f8-187f-4c3a-ae93-2d62f50689c0",
5+
"serialNumber": "urn:uuid:fae7d0a5-648d-4058-be92-774ed4f974eb",
66
"version": 1,
77
"metadata": {
8-
"timestamp": "2023-01-07T11:40:00.538713+00:00",
8+
"timestamp": "2023-01-07T13:45:57.597300+00:00",
99
"tools": [
1010
{
1111
"vendor": "CycloneDX",

tests/fixtures/json/1.2/bom_external_references.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
33
"bomFormat": "CycloneDX",
44
"specVersion": "1.2",
5-
"serialNumber": "urn:uuid:06480655-292f-4ab5-8d84-52ac0023a161",
5+
"serialNumber": "urn:uuid:b254c902-deb4-4298-9969-027541ee365c",
66
"version": 1,
77
"metadata": {
8-
"timestamp": "2023-01-07T11:40:00.241228+00:00",
8+
"timestamp": "2023-01-07T13:45:57.467119+00:00",
99
"tools": [
1010
{
1111
"vendor": "CycloneDX",

tests/fixtures/json/1.2/bom_issue_275_components.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
33
"bomFormat": "CycloneDX",
44
"specVersion": "1.2",
5-
"serialNumber": "urn:uuid:d571cf4c-efeb-4603-b744-a58a3e95410a",
5+
"serialNumber": "urn:uuid:e8a76c00-84d4-447e-8bc5-d8a3cefd01f0",
66
"version": 1,
77
"metadata": {
8-
"timestamp": "2023-01-07T11:40:00.654095+00:00",
8+
"timestamp": "2023-01-07T13:45:57.651650+00:00",
99
"tools": [
1010
{
1111
"vendor": "CycloneDX",
@@ -59,6 +59,10 @@
5959
"dependsOn": [
6060
"cd3e9c95-9d41-49e7-9924-8cf0465ae789"
6161
]
62+
},
63+
{
64+
"ref": "cd3e9c95-9d41-49e7-9924-8cf0465ae789",
65+
"dependsOn": []
6266
}
6367
]
6468
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
3+
"bomFormat": "CycloneDX",
4+
"specVersion": "1.2",
5+
"serialNumber": "urn:uuid:0e7ce694-c130-4965-8716-85015b42c729",
6+
"version": 1,
7+
"metadata": {
8+
"timestamp": "2023-01-07T13:52:27.732107+00:00",
9+
"tools": [
10+
{
11+
"vendor": "CycloneDX",
12+
"name": "cyclonedx-python-lib",
13+
"version": "3.1.2"
14+
}
15+
],
16+
"component": {
17+
"type": "application",
18+
"bom-ref": "my-project",
19+
"name": "my-project",
20+
"version": "1"
21+
}
22+
},
23+
"components": [
24+
{
25+
"type": "library",
26+
"bom-ref": "component-A",
27+
"name": "A",
28+
"version": "0.1",
29+
"components": [
30+
{
31+
"type": "library",
32+
"bom-ref": "component-B",
33+
"name": "B",
34+
"version": "1.0",
35+
"components": [
36+
{
37+
"type": "library",
38+
"bom-ref": "component-C",
39+
"name": "C",
40+
"version": "1.0"
41+
}
42+
]
43+
}
44+
]
45+
}
46+
],
47+
"dependencies": [
48+
{
49+
"ref": "my-project",
50+
"dependsOn": [
51+
"component-A"
52+
]
53+
},
54+
{
55+
"ref": "component-A",
56+
"dependsOn": [
57+
"component-B"
58+
]
59+
},
60+
{
61+
"ref": "component-B",
62+
"dependsOn": [
63+
"component-C"
64+
]
65+
},
66+
{
67+
"ref": "component-C",
68+
"dependsOn": []
69+
}
70+
]
71+
}

0 commit comments

Comments
 (0)