Skip to content

Commit 269ee15

Browse files
authored
feat: add CPE to component (#138)
* Added CPE to component Setting CPE was missing for component, now it is possible to set CPE and output CPE for a component. Signed-off-by: Jens Lucius <[email protected]> * Fixing problems with CPE addition - Fixed styling errors - Added reference to CPE Spec - Adding CPE parameter as last parameter to not break arguments Signed-off-by: Jens Lucius <[email protected]> * Again fixes for Style and CPE reference Missing in the last commit Signed-off-by: Jens Lucius <[email protected]> * Added CPE as argument before deprecated arguments Signed-off-by: Jens Lucius <[email protected]> * Added testing for CPE addition and error fixing - Added output tests for CPE in XML and JSON - Fixes style error in components - Fixes order for CPE output in XML (CPE has to come before PURL) Signed-off-by: Jens Lucius <[email protected]> * Fixed output tests CPE was still in the wrong position in one of the tests - fixed Signed-off-by: Jens Lucius <[email protected]> * Fixed minor test fixtures issues - cpe was still in wrong position in 1.2 JSON - Indentation fixed in 1.4 JSON Signed-off-by: Jens Lucius <[email protected]> * Fixed missing comma in JSON 1.2 test file Signed-off-by: Jens Lucius <[email protected]>
1 parent dec63de commit 269ee15

12 files changed

+396
-1
lines changed

cyclonedx/model/component.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def __init__(self, name: str, component_type: ComponentType = ComponentType.LIBR
106106
copyright: Optional[str] = None, purl: Optional[PackageURL] = None,
107107
external_references: Optional[List[ExternalReference]] = None,
108108
properties: Optional[List[Property]] = None, release_notes: Optional[ReleaseNotes] = None,
109+
cpe: Optional[str] = None,
109110
# Deprecated parameters kept for backwards compatibility
110111
namespace: Optional[str] = None, license_str: Optional[str] = None
111112
) -> None:
@@ -124,6 +125,7 @@ def __init__(self, name: str, component_type: ComponentType = ComponentType.LIBR
124125
self.licenses = licenses or []
125126
self.copyright = copyright
126127
self.purl = purl
128+
self.cpe = cpe
127129
self.external_references = external_references if external_references else []
128130
self.properties = properties
129131

@@ -392,6 +394,21 @@ def purl(self) -> Optional[PackageURL]:
392394
def purl(self, purl: Optional[PackageURL]) -> None:
393395
self._purl = purl
394396

397+
@property
398+
def cpe(self) -> Optional[str]:
399+
"""
400+
Specifies a well-formed CPE name that conforms to the CPE 2.2 or 2.3 specification.
401+
See https://nvd.nist.gov/products/cpe
402+
403+
Returns:
404+
`str` if set else `None`
405+
"""
406+
return self._cpe
407+
408+
@cpe.setter
409+
def cpe(self, cpe: Optional[str]) -> None:
410+
self._cpe = cpe
411+
395412
@property
396413
def external_references(self) -> List[ExternalReference]:
397414
"""
@@ -492,7 +509,7 @@ def __hash__(self) -> int:
492509
return hash((
493510
self.author, self.bom_ref, self.copyright, self.description, str(self.external_references), self.group,
494511
str(self.hashes), str(self.licenses), self.mime_type, self.name, self.properties, self.publisher, self.purl,
495-
self.release_notes, self.scope, self.supplier, self.type, self.version
512+
self.release_notes, self.scope, self.supplier, self.type, self.version, self.cpe
496513
))
497514

498515
def __repr__(self) -> str:

cyclonedx/output/xml.py

+4
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ def _add_component_element(self, component: Component) -> ElementTree.Element:
175175
else:
176176
ElementTree.SubElement(licenses_e, 'expression').text = license.expression
177177

178+
# cpe
179+
if component.cpe:
180+
ElementTree.SubElement(component_element, 'cpe').text = component.cpe
181+
178182
# purl
179183
if component.purl:
180184
ElementTree.SubElement(component_element, 'purl').text = component.purl.to_string()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.0" version="1">
3+
<components>
4+
<component type="library">
5+
<name>setuptools</name>
6+
<version>50.3.2</version>
7+
<cpe>cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*</cpe>
8+
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
9+
<modified>false</modified>
10+
</component>
11+
</components>
12+
</bom>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.1" version="1"
3+
serialNumber="urn:uuid:b409670b-e3e3-4691-b1ee-8eff057d74f5">
4+
<components>
5+
<component type="library" bom-ref="pkg:pypi/[email protected]?extension=tar.gz">
6+
<name>setuptools</name>
7+
<version>50.3.2</version>
8+
<cpe>cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*</cpe>
9+
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
10+
</component>
11+
</components>
12+
</bom>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json",
3+
"bomFormat": "CycloneDX",
4+
"specVersion": "1.2",
5+
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
6+
"version": 1,
7+
"metadata": {
8+
"timestamp": "2021-09-01T10:50:42.051979+00:00",
9+
"tools": [
10+
{
11+
"vendor": "CycloneDX",
12+
"name": "cyclonedx-python-lib",
13+
"version": "VERSION"
14+
}
15+
]
16+
},
17+
"components": [
18+
{
19+
"type": "library",
20+
"bom-ref": "pkg:pypi/[email protected]?extension=tar.gz",
21+
"author": "Test Author",
22+
"name": "setuptools",
23+
"version": "50.3.2",
24+
"cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*",
25+
"purl": "pkg:pypi/[email protected]?extension=tar.gz"
26+
}
27+
]
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1">
3+
<metadata>
4+
<timestamp>2021-09-01T10:50:42.051979+00:00</timestamp>
5+
<tools>
6+
<tool>
7+
<vendor>CycloneDX</vendor>
8+
<name>cyclonedx-python-lib</name>
9+
<version>VERSION</version>
10+
</tool>
11+
</tools>
12+
</metadata>
13+
<components>
14+
<component type="library" bom-ref="pkg:pypi/[email protected]?extension=tar.gz">
15+
<name>setuptools</name>
16+
<version>50.3.2</version>
17+
<cpe>cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*</cpe>
18+
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
19+
</component>
20+
</components>
21+
</bom>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json",
3+
"bomFormat": "CycloneDX",
4+
"specVersion": "1.3",
5+
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
6+
"version": 1,
7+
"metadata": {
8+
"timestamp": "2021-09-01T10:50:42.051979+00:00",
9+
"tools": [
10+
{
11+
"vendor": "CycloneDX",
12+
"name": "cyclonedx-python-lib",
13+
"version": "VERSION"
14+
}
15+
]
16+
},
17+
"components": [
18+
{
19+
"type": "library",
20+
"name": "setuptools",
21+
"version": "50.3.2",
22+
"cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*",
23+
"purl": "pkg:pypi/[email protected]?extension=tar.gz",
24+
"bom-ref": "pkg:pypi/[email protected]?extension=tar.gz",
25+
"licenses": [
26+
{
27+
"expression": "MIT License"
28+
}
29+
]
30+
}
31+
]
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" version="1">
3+
<metadata>
4+
<timestamp>2021-09-01T10:50:42.051979+00:00</timestamp>
5+
<tools>
6+
<tool>
7+
<vendor>CycloneDX</vendor>
8+
<name>cyclonedx-python-lib</name>
9+
<version>VERSION</version>
10+
</tool>
11+
</tools>
12+
</metadata>
13+
<components>
14+
<component type="library" bom-ref="pkg:pypi/[email protected]?extension=tar.gz">
15+
<name>setuptools</name>
16+
<version>50.3.2</version>
17+
<cpe>cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*</cpe>
18+
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
19+
</component>
20+
</components>
21+
</bom>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
3+
"bomFormat": "CycloneDX",
4+
"specVersion": "1.4",
5+
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
6+
"version": 1,
7+
"metadata": {
8+
"timestamp": "2021-09-01T10:50:42.051979+00:00",
9+
"tools": [
10+
{
11+
"vendor": "CycloneDX",
12+
"name": "cyclonedx-python-lib",
13+
"version": "VERSION",
14+
"externalReferences": [
15+
{
16+
"type": "build-system",
17+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions"
18+
},
19+
{
20+
"type": "distribution",
21+
"url": "https://pypi.org/project/cyclonedx-python-lib/"
22+
},
23+
{
24+
"type": "documentation",
25+
"url": "https://cyclonedx.github.io/cyclonedx-python-lib/"
26+
},
27+
{
28+
"type": "issue-tracker",
29+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues"
30+
},
31+
{
32+
"type": "license",
33+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE"
34+
},
35+
{
36+
"type": "release-notes",
37+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md"
38+
},
39+
{
40+
"type": "vcs",
41+
"url": "https://github.com/CycloneDX/cyclonedx-python-lib"
42+
},
43+
{
44+
"type": "website",
45+
"url": "https://cyclonedx.org"
46+
}
47+
]
48+
}
49+
]
50+
},
51+
"components": [
52+
{
53+
"type": "library",
54+
"name": "setuptools",
55+
"version": "50.3.2",
56+
"cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*",
57+
"purl": "pkg:pypi/[email protected]?extension=tar.gz",
58+
"bom-ref": "pkg:pypi/[email protected]?extension=tar.gz"
59+
}
60+
]
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" version="1">
3+
<metadata>
4+
<timestamp>2021-09-01T10:50:42.051979+00:00</timestamp>
5+
<tools>
6+
<tool>
7+
<vendor>CycloneDX</vendor>
8+
<name>cyclonedx-python-lib</name>
9+
<version>VERSION</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.github.io/cyclonedx-python-lib/</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://cyclonedx.org</url>
34+
</reference>
35+
</externalReferences>
36+
</tool>
37+
</tools>
38+
</metadata>
39+
<components>
40+
<component type="library" bom-ref="pkg:pypi/[email protected]?extension=tar.gz">
41+
<name>setuptools</name>
42+
<version>50.3.2</version>
43+
<cpe>cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*</cpe>
44+
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
45+
</component>
46+
</components>
47+
</bom>

tests/test_output_json.py

+54
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,60 @@ def test_simple_bom_v1_2(self) -> None:
9090
self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string())
9191
expected_json.close()
9292

93+
def test_simple_bom_v1_4_with_cpe(self) -> None:
94+
bom = Bom()
95+
c = Component(
96+
name='setuptools', version='50.3.2', bom_ref='pkg:pypi/[email protected]?extension=tar.gz',
97+
cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*',
98+
purl=PackageURL(
99+
type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz'
100+
)
101+
)
102+
bom.add_component(c)
103+
104+
outputter = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=SchemaVersion.V1_4)
105+
self.assertIsInstance(outputter, JsonV1Dot4)
106+
with open(join(dirname(__file__), 'fixtures/bom_v1.4_setuptools_with_cpe.json')) as expected_json:
107+
self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_4)
108+
self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string())
109+
expected_json.close()
110+
111+
def test_simple_bom_v1_3_with_cpe(self) -> None:
112+
bom = Bom()
113+
c = Component(
114+
name='setuptools', version='50.3.2', bom_ref='pkg:pypi/[email protected]?extension=tar.gz',
115+
cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*',
116+
purl=PackageURL(
117+
type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz'
118+
), license_str='MIT License'
119+
)
120+
bom.add_component(c)
121+
122+
outputter = get_instance(bom=bom, output_format=OutputFormat.JSON)
123+
self.assertIsInstance(outputter, JsonV1Dot3)
124+
with open(join(dirname(__file__), 'fixtures/bom_v1.3_setuptools_with_cpe.json')) as expected_json:
125+
self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_3)
126+
self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string())
127+
expected_json.close()
128+
129+
def test_simple_bom_v1_2_with_cpe(self) -> None:
130+
bom = Bom()
131+
bom.add_component(
132+
Component(
133+
name='setuptools', version='50.3.2', bom_ref='pkg:pypi/[email protected]?extension=tar.gz',
134+
cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*',
135+
purl=PackageURL(
136+
type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz'
137+
), author='Test Author'
138+
)
139+
)
140+
outputter = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=SchemaVersion.V1_2)
141+
self.assertIsInstance(outputter, JsonV1Dot2)
142+
with open(join(dirname(__file__), 'fixtures/bom_v1.2_setuptools_with_cpe.json')) as expected_json:
143+
self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_2)
144+
self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string())
145+
expected_json.close()
146+
93147
def test_bom_v1_3_with_component_hashes(self) -> None:
94148
bom = Bom()
95149
c = Component(

0 commit comments

Comments
 (0)