Skip to content

Commit 35ccdd1

Browse files
authored
fix: ToolRepository serialize migrated tools deduplicated (#686)
Signed-off-by: Jan Kowalleck <[email protected]>
1 parent e00af17 commit 35ccdd1

File tree

36 files changed

+820
-515
lines changed

36 files changed

+820
-515
lines changed

cyclonedx/model/tool.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
# Copyright (c) OWASP Foundation. All Rights Reserved.
1717

1818

19-
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Type, Union
19+
from itertools import chain
20+
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Type, Union
2021
from warnings import warn
2122
from xml.etree.ElementTree import Element # nosec B405
2223

@@ -265,12 +266,14 @@ def __hash__(self) -> int:
265266
class _ToolRepositoryHelper(BaseHelper):
266267

267268
@staticmethod
268-
def __all_as_tools(o: ToolRepository) -> Tuple[Tool, ...]:
269-
return (
270-
*o.tools,
271-
*map(Tool.from_component, o.components),
272-
*map(Tool.from_service, o.services),
273-
)
269+
def __all_as_tools(o: ToolRepository) -> 'SortedSet[Tool]':
270+
# use a set here, so the collection gets deduplicated.
271+
# use SortedSet set here, so the order stays reproducible.
272+
return SortedSet(chain(
273+
o.tools,
274+
map(Tool.from_component, o.components),
275+
map(Tool.from_service, o.services),
276+
))
274277

275278
@staticmethod
276279
def __supports_components_and_services(view: Any) -> bool:
@@ -284,7 +287,8 @@ def json_normalize(cls, o: ToolRepository, *,
284287
view: Optional[Type['ViewType']],
285288
**__: Any) -> Any:
286289
if len(o.tools) > 0 or not cls.__supports_components_and_services(view):
287-
return cls.__all_as_tools(o) or None
290+
ts = cls.__all_as_tools(o)
291+
return tuple(ts) if ts else None
288292
elem: Dict[str, Any] = {}
289293
if o.components:
290294
elem['components'] = tuple(o.components)

tests/_data/models.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,31 @@ def get_bom_with_tools_with_component_and_service_and_tools_irreversible_migrate
11721172
return _make_bom(metadata=BomMetaData(tools=tools))
11731173

11741174

1175+
def get_bom_with_tools_with_component_and_service_and_duplicated_tools_irreversible_migrate() -> Bom:
1176+
"""on serialization, it is expected that only tools are emitted, and that they are deduplicated"""
1177+
tools = ToolRepository()
1178+
tcomp = tools.components
1179+
tserv = tools.services
1180+
ttools = tools.tools
1181+
tcomp.update((
1182+
this_component(),
1183+
Component(name='test-component'),
1184+
Component(type=ComponentType.APPLICATION,
1185+
group='acme',
1186+
name='other-component'),
1187+
))
1188+
tserv.update((
1189+
Service(name='test-service'),
1190+
Service(group='acme',
1191+
name='other-service'),
1192+
))
1193+
ttools.clear()
1194+
# duplicate components and services as tools
1195+
ttools.update(map(Tool.from_component, tcomp))
1196+
ttools.update(map(Tool.from_service, tserv))
1197+
return _make_bom(metadata=BomMetaData(tools=tools))
1198+
1199+
11751200
def get_bom_for_issue_497_urls() -> Bom:
11761201
"""regression test for issue #497
11771202
see https://github.com/CycloneDX/cyclonedx-python-lib/issues/497
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" ?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.0" version="1">
3+
<components/>
4+
</bom>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" ?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.1" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
3+
<components/>
4+
</bom>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"metadata": {
3+
"timestamp": "2023-01-07T13:44:32.312678+00:00",
4+
"tools": [
5+
{
6+
"name": "cyclonedx-python-lib",
7+
"vendor": "CycloneDX",
8+
"version": "TESTING"
9+
},
10+
{
11+
"name": "other-component",
12+
"vendor": "acme"
13+
},
14+
{
15+
"name": "other-service",
16+
"vendor": "acme"
17+
},
18+
{
19+
"name": "test-component"
20+
},
21+
{
22+
"name": "test-service"
23+
}
24+
]
25+
},
26+
"serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac",
27+
"version": 1,
28+
"$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json",
29+
"bomFormat": "CycloneDX",
30+
"specVersion": "1.2"
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" ?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
3+
<metadata>
4+
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp>
5+
<tools>
6+
<tool>
7+
<vendor>CycloneDX</vendor>
8+
<name>cyclonedx-python-lib</name>
9+
<version>TESTING</version>
10+
</tool>
11+
<tool>
12+
<vendor>acme</vendor>
13+
<name>other-component</name>
14+
</tool>
15+
<tool>
16+
<vendor>acme</vendor>
17+
<name>other-service</name>
18+
</tool>
19+
<tool>
20+
<name>test-component</name>
21+
</tool>
22+
<tool>
23+
<name>test-service</name>
24+
</tool>
25+
</tools>
26+
</metadata>
27+
</bom>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"metadata": {
3+
"timestamp": "2023-01-07T13:44:32.312678+00:00",
4+
"tools": [
5+
{
6+
"name": "cyclonedx-python-lib",
7+
"vendor": "CycloneDX",
8+
"version": "TESTING"
9+
},
10+
{
11+
"name": "other-component",
12+
"vendor": "acme"
13+
},
14+
{
15+
"name": "other-service",
16+
"vendor": "acme"
17+
},
18+
{
19+
"name": "test-component"
20+
},
21+
{
22+
"name": "test-service"
23+
}
24+
]
25+
},
26+
"serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac",
27+
"version": 1,
28+
"$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json",
29+
"bomFormat": "CycloneDX",
30+
"specVersion": "1.3"
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" ?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
3+
<metadata>
4+
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp>
5+
<tools>
6+
<tool>
7+
<vendor>CycloneDX</vendor>
8+
<name>cyclonedx-python-lib</name>
9+
<version>TESTING</version>
10+
</tool>
11+
<tool>
12+
<vendor>acme</vendor>
13+
<name>other-component</name>
14+
</tool>
15+
<tool>
16+
<vendor>acme</vendor>
17+
<name>other-service</name>
18+
</tool>
19+
<tool>
20+
<name>test-component</name>
21+
</tool>
22+
<tool>
23+
<name>test-service</name>
24+
</tool>
25+
</tools>
26+
</metadata>
27+
</bom>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"metadata": {
3+
"timestamp": "2023-01-07T13:44:32.312678+00:00",
4+
"tools": [
5+
{
6+
"externalReferences": [
7+
{
8+
"type": "build-system",
9+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions"
10+
},
11+
{
12+
"type": "distribution",
13+
"url": "https://pypi.org/project/cyclonedx-python-lib/"
14+
},
15+
{
16+
"type": "documentation",
17+
"url": "https://cyclonedx-python-library.readthedocs.io/"
18+
},
19+
{
20+
"type": "issue-tracker",
21+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues"
22+
},
23+
{
24+
"type": "license",
25+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE"
26+
},
27+
{
28+
"type": "release-notes",
29+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md"
30+
},
31+
{
32+
"type": "vcs",
33+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib"
34+
},
35+
{
36+
"type": "website",
37+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/#readme"
38+
}
39+
],
40+
"name": "cyclonedx-python-lib",
41+
"vendor": "CycloneDX",
42+
"version": "TESTING"
43+
},
44+
{
45+
"name": "other-component",
46+
"vendor": "acme"
47+
},
48+
{
49+
"name": "other-service",
50+
"vendor": "acme"
51+
},
52+
{
53+
"name": "test-component"
54+
},
55+
{
56+
"name": "test-service"
57+
}
58+
]
59+
},
60+
"serialNumber": "urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac",
61+
"version": 1,
62+
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
63+
"bomFormat": "CycloneDX",
64+
"specVersion": "1.4"
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?xml version="1.0" ?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" serialNumber="urn:uuid:1441d33a-e0fc-45b5-af3b-61ee52a88bac" version="1">
3+
<metadata>
4+
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp>
5+
<tools>
6+
<tool>
7+
<vendor>CycloneDX</vendor>
8+
<name>cyclonedx-python-lib</name>
9+
<version>TESTING</version>
10+
<externalReferences>
11+
<reference type="build-system">
12+
<url>https://github.com/CycloneDX/cyclonedx-python-lib/actions</url>
13+
</reference>
14+
<reference type="distribution">
15+
<url>https://pypi.org/project/cyclonedx-python-lib/</url>
16+
</reference>
17+
<reference type="documentation">
18+
<url>https://cyclonedx-python-library.readthedocs.io/</url>
19+
</reference>
20+
<reference type="issue-tracker">
21+
<url>https://github.com/CycloneDX/cyclonedx-python-lib/issues</url>
22+
</reference>
23+
<reference type="license">
24+
<url>https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE</url>
25+
</reference>
26+
<reference type="release-notes">
27+
<url>https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md</url>
28+
</reference>
29+
<reference type="vcs">
30+
<url>https://github.com/CycloneDX/cyclonedx-python-lib</url>
31+
</reference>
32+
<reference type="website">
33+
<url>https://github.com/CycloneDX/cyclonedx-python-lib/#readme</url>
34+
</reference>
35+
</externalReferences>
36+
</tool>
37+
<tool>
38+
<vendor>acme</vendor>
39+
<name>other-component</name>
40+
</tool>
41+
<tool>
42+
<vendor>acme</vendor>
43+
<name>other-service</name>
44+
</tool>
45+
<tool>
46+
<name>test-component</name>
47+
</tool>
48+
<tool>
49+
<name>test-service</name>
50+
</tool>
51+
</tools>
52+
</metadata>
53+
</bom>

0 commit comments

Comments
 (0)