Skip to content

Commit

Permalink
feat: support for bom.externalReferences in JSON and XML #124
Browse files Browse the repository at this point in the history
Signed-off-by: Paul Horton <phorton@sonatype.com>
  • Loading branch information
madpah committed Feb 2, 2022
1 parent 32c0139 commit 1b733d7
Show file tree
Hide file tree
Showing 15 changed files with 372 additions and 16 deletions.
33 changes: 31 additions & 2 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from typing import cast, List, Optional
from uuid import uuid4, UUID

from . import ThisTool, Tool
from . import ExternalReference, ThisTool, Tool
from .component import Component
from .service import Service
from ..parser import BaseParser
Expand Down Expand Up @@ -149,7 +149,8 @@ def from_parser(parser: BaseParser) -> 'Bom':
bom.add_components(parser.get_components())
return bom

def __init__(self, components: Optional[List[Component]] = None, services: Optional[List[Service]] = None) -> None:
def __init__(self, components: Optional[List[Component]] = None, services: Optional[List[Service]] = None,
external_references: Optional[List[ExternalReference]] = None) -> None:
"""
Create a new Bom that you can manually/programmatically add data to later.
Expand All @@ -160,6 +161,7 @@ def __init__(self, components: Optional[List[Component]] = None, services: Optio
self.metadata = BomMetaData()
self.components = components
self.services = services
self.external_references = external_references

@property
def uuid(self) -> UUID:
Expand Down Expand Up @@ -360,6 +362,33 @@ def service_count(self) -> int:

return len(self.services)

@property
def external_references(self) -> Optional[List[ExternalReference]]:
"""
Provides the ability to document external references related to the BOM or to the project the BOM describes.
Returns:
List of `ExternalReference` else `None`
"""
return self._external_references

@external_references.setter
def external_references(self, external_references: Optional[List[ExternalReference]]) -> None:
self._external_references = external_references

def add_external_reference(self, external_reference: ExternalReference) -> None:
"""
Add an external reference to this Bom.
Args:
external_reference:
`ExternalReference` to add to this Bom.
Returns:
None
"""
self.external_references = (self.external_references or []) + [external_reference]

def has_vulnerabilities(self) -> bool:
"""
Check whether this Bom has any declared vulnerabilities.
Expand Down
7 changes: 7 additions & 0 deletions cyclonedx/output/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ def _specialise_output_for_schema_version(self, bom_json: Dict[Any, Any]) -> str
if not self.services_supports_release_notes() and 'releaseNotes' in bom_json['services'][i].keys():
del bom_json['services'][i]['releaseNotes']

# Iterate externalReferences
if 'externalReferences' in bom_json.keys():
for i in range(len(bom_json['externalReferences'])):
if not self.external_references_supports_hashes() \
and 'hashes' in bom_json['externalReferences'][i].keys():
del bom_json['externalReferences'][i]['hashes']

# Iterate Vulnerabilities
if 'vulnerabilities' in bom_json.keys():
for i in range(len(bom_json['vulnerabilities'])):
Expand Down
6 changes: 6 additions & 0 deletions cyclonedx/output/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def bom_metadata_supports_tools_external_references(self) -> bool:
def bom_supports_services(self) -> bool:
return True

def bom_supports_external_references(self) -> bool:
return True

def services_supports_properties(self) -> bool:
return True

Expand Down Expand Up @@ -236,6 +239,9 @@ def bom_metadata_supports_tools_external_references(self) -> bool:
def bom_supports_services(self) -> bool:
return False

def bom_supports_external_references(self) -> bool:
return False

def services_supports_properties(self) -> bool:
return False

Expand Down
20 changes: 8 additions & 12 deletions cyclonedx/output/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ def generate(self, force_regeneration: bool = False) -> None:
for service in cast(List[Service], self.get_bom().services):
services_element.append(self._add_service_element(service=service))

if self.bom_supports_external_references():
if self.get_bom().external_references:
self._add_external_references_to_element(
ext_refs=cast(List[ExternalReference], self.get_bom().external_references),
element=self._root_bom_element
)

if self.bom_supports_vulnerabilities() and has_vulnerabilities:
vulnerabilities_element = ElementTree.SubElement(self._root_bom_element, 'vulnerabilities')
for component in cast(List[Component], self.get_bom().components):
Expand Down Expand Up @@ -276,18 +283,7 @@ def _add_component_element(self, component: Component) -> ElementTree.Element:

# externalReferences
if self.component_supports_external_references() and len(component.external_references) > 0:
external_references_e = ElementTree.SubElement(component_element, 'externalReferences')
for ext_ref in component.external_references:
external_reference_e = ElementTree.SubElement(
external_references_e, 'reference', {'type': ext_ref.get_reference_type().value}
)
ElementTree.SubElement(external_reference_e, 'url').text = ext_ref.get_url()

if ext_ref.get_comment():
ElementTree.SubElement(external_reference_e, 'comment').text = ext_ref.get_comment()

if self.external_references_supports_hashes() and len(ext_ref.get_hashes()) > 0:
Xml._add_hashes_to_element(hashes=ext_ref.get_hashes(), element=external_reference_e)
self._add_external_references_to_element(ext_refs=component.external_references, element=component_element)

# releaseNotes
if self.component_supports_release_notes() and component.release_notes:
Expand Down
14 changes: 14 additions & 0 deletions tests/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ def get_bom_just_complete_metadata() -> Bom:
return bom


def get_bom_with_external_references() -> Bom:
bom = Bom(external_references=[
get_external_reference_1(), get_external_reference_2()
])
return bom


def get_bom_with_services_simple() -> Bom:
bom = Bom(services=[
Service(name='my-first-service'),
Expand Down Expand Up @@ -288,6 +295,13 @@ def get_external_reference_1() -> ExternalReference:
)


def get_external_reference_2() -> ExternalReference:
return ExternalReference(
reference_type=ExternalReferenceType.WEBSITE,
url='https://cyclonedx.org'
)


def get_issue_1() -> IssueType:
return IssueType(
classification=IssueClassification.SECURITY, id='CVE-2021-44228', name='Apache Log3Shell',
Expand Down
29 changes: 29 additions & 0 deletions tests/fixtures/json/1.2/bom_external_references.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"$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": [],
"externalReferences": [
{
"url": "https://cyclonedx.org",
"comment": "No comment",
"type": "distribution"
},
{
"url": "https://cyclonedx.org",
"type": "website"
}
]
}
35 changes: 35 additions & 0 deletions tests/fixtures/json/1.3/bom_external_references.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"$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": [],
"externalReferences": [
{
"url": "https://cyclonedx.org",
"comment": "No comment",
"type": "distribution",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
]
},
{
"url": "https://cyclonedx.org",
"type": "website"
}
]
}
69 changes: 69 additions & 0 deletions tests/fixtures/json/1.4/bom_external_references.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"$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": [],
"externalReferences": [
{
"url": "https://cyclonedx.org",
"comment": "No comment",
"type": "distribution",
"hashes": [
{
"alg": "SHA-256",
"content": "806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"
}
]
},
{
"url": "https://cyclonedx.org",
"type": "website"
}
]
}
13 changes: 13 additions & 0 deletions tests/fixtures/xml/1.1/bom_external_references.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.1" version="1">
<components />
<externalReferences>
<reference type="distribution">
<url>https://cyclonedx.org</url>
<comment>No comment</comment>
</reference>
<reference type="website">
<url>https://cyclonedx.org</url>
</reference>
</externalReferences>
</bom>
23 changes: 23 additions & 0 deletions tests/fixtures/xml/1.2/bom_external_references.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?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 />
<externalReferences>
<reference type="distribution">
<url>https://cyclonedx.org</url>
<comment>No comment</comment>
</reference>
<reference type="website">
<url>https://cyclonedx.org</url>
</reference>
</externalReferences>
</bom>
26 changes: 26 additions & 0 deletions tests/fixtures/xml/1.3/bom_external_references.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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 />
<externalReferences>
<reference type="distribution">
<url>https://cyclonedx.org</url>
<comment>No comment</comment>
<hashes>
<hash alg="SHA-256">806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b</hash>
</hashes>
</reference>
<reference type="website">
<url>https://cyclonedx.org</url>
</reference>
</externalReferences>
</bom>
Loading

0 comments on commit 1b733d7

Please sign in to comment.