Skip to content
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

feat: add CPE to component #138

Merged
merged 8 commits into from
Jan 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion cyclonedx/model/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def __init__(self, name: str, component_type: ComponentType = ComponentType.LIBR
copyright: Optional[str] = None, purl: Optional[PackageURL] = None,
external_references: Optional[List[ExternalReference]] = None,
properties: Optional[List[Property]] = None, release_notes: Optional[ReleaseNotes] = None,
cpe: Optional[str] = None,
# Deprecated parameters kept for backwards compatibility
namespace: Optional[str] = None, license_str: Optional[str] = None
) -> None:
Expand All @@ -124,6 +125,7 @@ def __init__(self, name: str, component_type: ComponentType = ComponentType.LIBR
self.licenses = licenses or []
self.copyright = copyright
self.purl = purl
self.cpe = cpe
self.external_references = external_references if external_references else []
self.properties = properties

Expand Down Expand Up @@ -392,6 +394,21 @@ def purl(self) -> Optional[PackageURL]:
def purl(self, purl: Optional[PackageURL]) -> None:
self._purl = purl

@property
def cpe(self) -> Optional[str]:
"""
Specifies a well-formed CPE name that conforms to the CPE 2.2 or 2.3 specification.
See https://nvd.nist.gov/products/cpe

Returns:
`str` if set else `None`
"""
return self._cpe

@cpe.setter
def cpe(self, cpe: Optional[str]) -> None:
self._cpe = cpe

@property
def external_references(self) -> List[ExternalReference]:
"""
Expand Down Expand Up @@ -492,7 +509,7 @@ def __hash__(self) -> int:
return hash((
self.author, self.bom_ref, self.copyright, self.description, str(self.external_references), self.group,
str(self.hashes), str(self.licenses), self.mime_type, self.name, self.properties, self.publisher, self.purl,
self.release_notes, self.scope, self.supplier, self.type, self.version
self.release_notes, self.scope, self.supplier, self.type, self.version, self.cpe
))

def __repr__(self) -> str:
Expand Down
4 changes: 4 additions & 0 deletions cyclonedx/output/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ def _add_component_element(self, component: Component) -> ElementTree.Element:
else:
ElementTree.SubElement(licenses_e, 'expression').text = license.expression

# cpe
if component.cpe:
ElementTree.SubElement(component_element, 'cpe').text = component.cpe

# purl
if component.purl:
ElementTree.SubElement(component_element, 'purl').text = component.purl.to_string()
Expand Down
12 changes: 12 additions & 0 deletions tests/fixtures/bom_v1.0_setuptools_with_cpe.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.0" version="1">
<components>
<component type="library">
<name>setuptools</name>
<version>50.3.2</version>
<cpe>cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*</cpe>
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
<modified>false</modified>
</component>
</components>
</bom>
12 changes: 12 additions & 0 deletions tests/fixtures/bom_v1.1_setuptools_with_cpe.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.1" version="1"
serialNumber="urn:uuid:b409670b-e3e3-4691-b1ee-8eff057d74f5">
<components>
<component type="library" bom-ref="pkg:pypi/[email protected]?extension=tar.gz">
<name>setuptools</name>
<version>50.3.2</version>
<cpe>cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*</cpe>
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
</component>
</components>
</bom>
28 changes: 28 additions & 0 deletions tests/fixtures/bom_v1.2_setuptools_with_cpe.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.2a.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.2",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
]
},
"components": [
{
"type": "library",
"bom-ref": "pkg:pypi/[email protected]?extension=tar.gz",
"author": "Test Author",
"name": "setuptools",
"version": "50.3.2",
"cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*",
"purl": "pkg:pypi/[email protected]?extension=tar.gz"
}
]
}
21 changes: 21 additions & 0 deletions tests/fixtures/bom_v1.2_setuptools_with_cpe.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1">
<metadata>
<timestamp>2021-09-01T10:50:42.051979+00:00</timestamp>
<tools>
<tool>
<vendor>CycloneDX</vendor>
<name>cyclonedx-python-lib</name>
<version>VERSION</version>
</tool>
</tools>
</metadata>
<components>
<component type="library" bom-ref="pkg:pypi/[email protected]?extension=tar.gz">
<name>setuptools</name>
<version>50.3.2</version>
<cpe>cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*</cpe>
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
</component>
</components>
</bom>
32 changes: 32 additions & 0 deletions tests/fixtures/bom_v1.3_setuptools_with_cpe.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.3.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.3",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION"
}
]
},
"components": [
{
"type": "library",
"name": "setuptools",
"version": "50.3.2",
"cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*",
"purl": "pkg:pypi/[email protected]?extension=tar.gz",
"bom-ref": "pkg:pypi/[email protected]?extension=tar.gz",
"licenses": [
{
"expression": "MIT License"
}
]
}
]
}
21 changes: 21 additions & 0 deletions tests/fixtures/bom_v1.3_setuptools_with_cpe.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" version="1">
<metadata>
<timestamp>2021-09-01T10:50:42.051979+00:00</timestamp>
<tools>
<tool>
<vendor>CycloneDX</vendor>
<name>cyclonedx-python-lib</name>
<version>VERSION</version>
</tool>
</tools>
</metadata>
<components>
<component type="library" bom-ref="pkg:pypi/[email protected]?extension=tar.gz">
<name>setuptools</name>
<version>50.3.2</version>
<cpe>cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*</cpe>
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
</component>
</components>
</bom>
61 changes: 61 additions & 0 deletions tests/fixtures/bom_v1.4_setuptools_with_cpe.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2021-09-01T10:50:42.051979+00:00",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-python-lib",
"version": "VERSION",
"externalReferences": [
{
"type": "build-system",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions"
},
{
"type": "distribution",
"url": "https://pypi.org/project/cyclonedx-python-lib/"
},
{
"type": "documentation",
"url": "https://cyclonedx.github.io/cyclonedx-python-lib/"
},
{
"type": "issue-tracker",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues"
},
{
"type": "license",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE"
},
{
"type": "release-notes",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md"
},
{
"type": "vcs",
"url": "https://github.com/CycloneDX/cyclonedx-python-lib"
},
{
"type": "website",
"url": "https://cyclonedx.org"
}
]
}
]
},
"components": [
{
"type": "library",
"name": "setuptools",
"version": "50.3.2",
"cpe": "cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*",
"purl": "pkg:pypi/[email protected]?extension=tar.gz",
"bom-ref": "pkg:pypi/[email protected]?extension=tar.gz"
}
]
}
47 changes: 47 additions & 0 deletions tests/fixtures/bom_v1.4_setuptools_with_cpe.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.4" version="1">
<metadata>
<timestamp>2021-09-01T10:50:42.051979+00:00</timestamp>
<tools>
<tool>
<vendor>CycloneDX</vendor>
<name>cyclonedx-python-lib</name>
<version>VERSION</version>
<externalReferences>
<reference type="build-system">
<url>https://github.com/CycloneDX/cyclonedx-python-lib/actions</url>
</reference>
<reference type="distribution">
<url>https://pypi.org/project/cyclonedx-python-lib/</url>
</reference>
<reference type="documentation">
<url>https://cyclonedx.github.io/cyclonedx-python-lib/</url>
</reference>
<reference type="issue-tracker">
<url>https://github.com/CycloneDX/cyclonedx-python-lib/issues</url>
</reference>
<reference type="license">
<url>https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE</url>
</reference>
<reference type="release-notes">
<url>https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md</url>
</reference>
<reference type="vcs">
<url>https://github.com/CycloneDX/cyclonedx-python-lib</url>
</reference>
<reference type="website">
<url>https://cyclonedx.org</url>
</reference>
</externalReferences>
</tool>
</tools>
</metadata>
<components>
<component type="library" bom-ref="pkg:pypi/[email protected]?extension=tar.gz">
<name>setuptools</name>
<version>50.3.2</version>
<cpe>cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*</cpe>
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
</component>
</components>
</bom>
54 changes: 54 additions & 0 deletions tests/test_output_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,60 @@ def test_simple_bom_v1_2(self) -> None:
self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string())
expected_json.close()

def test_simple_bom_v1_4_with_cpe(self) -> None:
bom = Bom()
c = Component(
name='setuptools', version='50.3.2', bom_ref='pkg:pypi/[email protected]?extension=tar.gz',
cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*',
purl=PackageURL(
type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz'
)
)
bom.add_component(c)

outputter = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=SchemaVersion.V1_4)
self.assertIsInstance(outputter, JsonV1Dot4)
with open(join(dirname(__file__), 'fixtures/bom_v1.4_setuptools_with_cpe.json')) as expected_json:
self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_4)
self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string())
expected_json.close()

def test_simple_bom_v1_3_with_cpe(self) -> None:
bom = Bom()
c = Component(
name='setuptools', version='50.3.2', bom_ref='pkg:pypi/[email protected]?extension=tar.gz',
cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*',
purl=PackageURL(
type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz'
), license_str='MIT License'
)
bom.add_component(c)

outputter = get_instance(bom=bom, output_format=OutputFormat.JSON)
self.assertIsInstance(outputter, JsonV1Dot3)
with open(join(dirname(__file__), 'fixtures/bom_v1.3_setuptools_with_cpe.json')) as expected_json:
self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_3)
self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string())
expected_json.close()

def test_simple_bom_v1_2_with_cpe(self) -> None:
bom = Bom()
bom.add_component(
Component(
name='setuptools', version='50.3.2', bom_ref='pkg:pypi/[email protected]?extension=tar.gz',
cpe='cpe:2.3:a:python:setuptools:50.3.2:*:*:*:*:*:*:*',
purl=PackageURL(
type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz'
), author='Test Author'
)
)
outputter = get_instance(bom=bom, output_format=OutputFormat.JSON, schema_version=SchemaVersion.V1_2)
self.assertIsInstance(outputter, JsonV1Dot2)
with open(join(dirname(__file__), 'fixtures/bom_v1.2_setuptools_with_cpe.json')) as expected_json:
self.assertValidAgainstSchema(bom_json=outputter.output_as_string(), schema_version=SchemaVersion.V1_2)
self.assertEqualJsonBom(expected_json.read(), outputter.output_as_string())
expected_json.close()

def test_bom_v1_3_with_component_hashes(self) -> None:
bom = Bom()
c = Component(
Expand Down
Loading