Skip to content

Commit

Permalink
feat: support services in XML BOMs
Browse files Browse the repository at this point in the history
feat: support nested services in JSON and XML BOMs

Signed-off-by: Paul Horton <[email protected]>
  • Loading branch information
madpah committed Jan 31, 2022
1 parent a35d540 commit 9edf6c9
Show file tree
Hide file tree
Showing 22 changed files with 1,699 additions and 220 deletions.
54 changes: 51 additions & 3 deletions cyclonedx/model/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ def __init__(self, name: str, bom_ref: Optional[str] = None, provider: Optional[
licenses: Optional[List[LicenseChoice]] = None,
external_references: Optional[List[ExternalReference]] = None,
properties: Optional[List[Property]] = None,
# services: Optional[List[Service]] = None, -- I have no clue how to do this,
# commenting out so someone else can
services: Optional[List['Service']] = None,
release_notes: Optional[ReleaseNotes] = None,
) -> None:
self.bom_ref = bom_ref or str(uuid4())
Expand All @@ -59,7 +58,7 @@ def __init__(self, name: str, bom_ref: Optional[str] = None, provider: Optional[
self.data = data
self.licenses = licenses or []
self.external_references = external_references or []
# self.services = services -- no clue
self.services = services
self.release_notes = release_notes
self.properties = properties

Expand Down Expand Up @@ -264,6 +263,40 @@ def add_external_reference(self, reference: ExternalReference) -> None:
"""
self.external_references = self._external_references + [reference]

@property
def services(self) -> Optional[List['Service']]:
"""
A list of services included or deployed behind the parent service.
This is not a dependency tree.
It provides a way to specify a hierarchical representation of service assemblies.
Returns:
List of `Service`s or `None`
"""
return self._services

@services.setter
def services(self, services: Optional[List['Service']]) -> None:
self._services = services

def has_service(self, service: 'Service') -> bool:
"""
Check whether this Service contains the given Service.
Args:
service:
The instance of `cyclonedx.model.service.Service` to check if this Service contains.
Returns:
`bool` - `True` if the supplied Service is part of this Service, `False` otherwise.
"""
if not self.services:
return False

return service in self.services

@property
def release_notes(self) -> Optional[ReleaseNotes]:
"""
Expand Down Expand Up @@ -292,3 +325,18 @@ def properties(self) -> Optional[List[Property]]:
@properties.setter
def properties(self, properties: Optional[List[Property]]) -> None:
self._properties = properties

def __eq__(self, other: object) -> bool:
if isinstance(other, Service):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((
self.authenticated, self.data, self.description, str(self.endpoints),
str(self.external_references), self.group, str(self.licenses), self.name, self.properties, self.provider,
self.release_notes, str(self.services), self.version, self.x_trust_boundary
))

def __repr__(self) -> str:
return f'<Service name={self.name}, version={self.version}, bom-ref={self.bom_ref}>'
252 changes: 179 additions & 73 deletions cyclonedx/output/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
from . import BaseOutput, SchemaVersion
from .schema import BaseSchemaVersion, SchemaVersion1Dot0, SchemaVersion1Dot1, SchemaVersion1Dot2, SchemaVersion1Dot3, \
SchemaVersion1Dot4
from ..model import ExternalReference, HashType, OrganizationalEntity, OrganizationalContact, Tool
from ..model import ExternalReference, HashType, OrganizationalEntity, OrganizationalContact, Property, Tool
from ..model.bom import Bom
from ..model.component import Component
from ..model.release_note import ReleaseNotes
from ..model.service import Service
from ..model.vulnerability import Vulnerability, VulnerabilityRating, VulnerabilitySource, BomTargetVersionRange


Expand Down Expand Up @@ -75,13 +77,19 @@ def generate(self, force_regeneration: bool = False) -> None:
elif component.has_vulnerabilities():
has_vulnerabilities = True

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):
for vulnerability in component.get_vulnerabilities():
vulnerabilities_element.append(
self._get_vulnerability_as_xml_element_post_1_4(vulnerability=vulnerability)
)
if self.bom_supports_services():
if self.get_bom().services:
services_element = ElementTree.SubElement(self._root_bom_element, 'services')
for service in cast(List[Service], self.get_bom().services):
services_element.append(self._add_service_element(service=service))

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):
for vulnerability in component.get_vulnerabilities():
vulnerabilities_element.append(
self._get_vulnerability_as_xml_element_post_1_4(vulnerability=vulnerability)
)

self.generated = True

Expand Down Expand Up @@ -213,74 +221,172 @@ def _add_component_element(self, component: Component) -> ElementTree.Element:

# releaseNotes
if self.component_supports_release_notes() and component.release_notes:
release_notes_e = ElementTree.SubElement(component_element, 'releaseNotes')
release_notes = component.release_notes

ElementTree.SubElement(release_notes_e, 'type').text = release_notes.type
if release_notes.title:
ElementTree.SubElement(release_notes_e, 'title').text = release_notes.title
if release_notes.featured_image:
ElementTree.SubElement(release_notes_e,
'featuredImage').text = str(release_notes.featured_image)
if release_notes.social_image:
ElementTree.SubElement(release_notes_e,
'socialImage').text = str(release_notes.social_image)
if release_notes.description:
ElementTree.SubElement(release_notes_e,
'description').text = release_notes.description
if release_notes.timestamp:
ElementTree.SubElement(release_notes_e, 'timestamp').text = release_notes.timestamp.isoformat()
if release_notes.aliases:
release_notes_aliases_e = ElementTree.SubElement(release_notes_e, 'aliases')
for alias in release_notes.aliases:
ElementTree.SubElement(release_notes_aliases_e, 'alias').text = alias
if release_notes.tags:
release_notes_tags_e = ElementTree.SubElement(release_notes_e, 'tags')
for tag in release_notes.tags:
ElementTree.SubElement(release_notes_tags_e, 'tag').text = tag
if release_notes.resolves:
release_notes_resolves_e = ElementTree.SubElement(release_notes_e, 'resolves')
for issue in release_notes.resolves:
issue_e = ElementTree.SubElement(
release_notes_resolves_e, 'issue', {'type': issue.get_classification().value}
)
if issue.get_id():
ElementTree.SubElement(issue_e, 'id').text = issue.get_id()
if issue.get_name():
ElementTree.SubElement(issue_e, 'name').text = issue.get_name()
if issue.get_description():
ElementTree.SubElement(issue_e, 'description').text = issue.get_description()
if issue.source:
issue_source_e = ElementTree.SubElement(issue_e, 'source')
if issue.source.name:
ElementTree.SubElement(issue_source_e, 'name').text = issue.source.name
if issue.source.url:
ElementTree.SubElement(issue_source_e, 'url').text = str(issue.source.url)
if issue.get_references():
issue_references_e = ElementTree.SubElement(issue_e, 'references')
for reference in issue.get_references():
ElementTree.SubElement(issue_references_e, 'url').text = str(reference)
if release_notes.notes:
release_notes_notes_e = ElementTree.SubElement(release_notes_e, 'notes')
for note in release_notes.notes:
note_e = ElementTree.SubElement(release_notes_notes_e, 'note')
if note.locale:
ElementTree.SubElement(note_e, 'locale').text = note.locale
text_attrs = {}
if note.text.content_type:
text_attrs['content-type'] = note.text.content_type
if note.text.encoding:
text_attrs['encoding'] = note.text.encoding.value
ElementTree.SubElement(note_e, 'text', text_attrs).text = note.text.content
if release_notes.properties:
release_notes_properties_e = ElementTree.SubElement(release_notes_e, 'properties')
for prop in release_notes.properties:
ElementTree.SubElement(
release_notes_properties_e, 'property', {'name': prop.get_name()}
).text = prop.get_value()
Xml._add_release_notes_element(release_notes=component.release_notes, parent_element=component_element)

return component_element

@staticmethod
def _add_release_notes_element(release_notes: ReleaseNotes, parent_element: ElementTree.Element) -> None:
release_notes_e = ElementTree.SubElement(parent_element, 'releaseNotes')

ElementTree.SubElement(release_notes_e, 'type').text = release_notes.type
if release_notes.title:
ElementTree.SubElement(release_notes_e, 'title').text = release_notes.title
if release_notes.featured_image:
ElementTree.SubElement(release_notes_e,
'featuredImage').text = str(release_notes.featured_image)
if release_notes.social_image:
ElementTree.SubElement(release_notes_e,
'socialImage').text = str(release_notes.social_image)
if release_notes.description:
ElementTree.SubElement(release_notes_e,
'description').text = release_notes.description
if release_notes.timestamp:
ElementTree.SubElement(release_notes_e, 'timestamp').text = release_notes.timestamp.isoformat()
if release_notes.aliases:
release_notes_aliases_e = ElementTree.SubElement(release_notes_e, 'aliases')
for alias in release_notes.aliases:
ElementTree.SubElement(release_notes_aliases_e, 'alias').text = alias
if release_notes.tags:
release_notes_tags_e = ElementTree.SubElement(release_notes_e, 'tags')
for tag in release_notes.tags:
ElementTree.SubElement(release_notes_tags_e, 'tag').text = tag
if release_notes.resolves:
release_notes_resolves_e = ElementTree.SubElement(release_notes_e, 'resolves')
for issue in release_notes.resolves:
issue_e = ElementTree.SubElement(
release_notes_resolves_e, 'issue', {'type': issue.get_classification().value}
)
if issue.get_id():
ElementTree.SubElement(issue_e, 'id').text = issue.get_id()
if issue.get_name():
ElementTree.SubElement(issue_e, 'name').text = issue.get_name()
if issue.get_description():
ElementTree.SubElement(issue_e, 'description').text = issue.get_description()
if issue.source:
issue_source_e = ElementTree.SubElement(issue_e, 'source')
if issue.source.name:
ElementTree.SubElement(issue_source_e, 'name').text = issue.source.name
if issue.source.url:
ElementTree.SubElement(issue_source_e, 'url').text = str(issue.source.url)
if issue.get_references():
issue_references_e = ElementTree.SubElement(issue_e, 'references')
for reference in issue.get_references():
ElementTree.SubElement(issue_references_e, 'url').text = str(reference)
if release_notes.notes:
release_notes_notes_e = ElementTree.SubElement(release_notes_e, 'notes')
for note in release_notes.notes:
note_e = ElementTree.SubElement(release_notes_notes_e, 'note')
if note.locale:
ElementTree.SubElement(note_e, 'locale').text = note.locale
text_attrs = {}
if note.text.content_type:
text_attrs['content-type'] = note.text.content_type
if note.text.encoding:
text_attrs['encoding'] = note.text.encoding.value
ElementTree.SubElement(note_e, 'text', text_attrs).text = note.text.content
if release_notes.properties:
Xml._add_properties_element(properties=release_notes.properties, parent_element=release_notes_e)

@staticmethod
def _add_properties_element(properties: List[Property], parent_element: ElementTree.Element) -> None:
properties_e = ElementTree.SubElement(parent_element, 'properties')
for property in properties:
ElementTree.SubElement(
properties_e, 'property', {'name': property.get_name()}
).text = property.get_value()

def _add_service_element(self, service: Service) -> ElementTree.Element:
element_attributes = {}
if service.bom_ref:
element_attributes['bom-ref'] = service.bom_ref

service_element = ElementTree.Element('service', element_attributes)

# provider
if service.provider:
self._add_organizational_entity(
parent_element=service_element, organization=service.provider, tag_name='provider'
)

# group
if service.group:
ElementTree.SubElement(service_element, 'group').text = service.group

# name
ElementTree.SubElement(service_element, 'name').text = service.name

# version
if service.version:
ElementTree.SubElement(service_element, 'version').text = service.version

# description
if service.description:
ElementTree.SubElement(service_element, 'description').text = service.description

# endpoints
if service.endpoints:
endpoints_e = ElementTree.SubElement(service_element, 'endpoints')
for endpoint in service.endpoints:
ElementTree.SubElement(endpoints_e, 'endpoint').text = str(endpoint)

# authenticated
if isinstance(service.authenticated, bool):
ElementTree.SubElement(service_element, 'authenticated').text = str(service.authenticated).lower()

# x-trust-boundary
if isinstance(service.x_trust_boundary, bool):
ElementTree.SubElement(service_element, 'x-trust-boundary').text = str(service.x_trust_boundary).lower()

# data
if service.data:
data_e = ElementTree.SubElement(service_element, 'data')
for data in service.data:
ElementTree.SubElement(data_e, 'classification', {'flow': data.flow.value}).text = data.classification

# licenses
if service.licenses:
licenses_e = ElementTree.SubElement(service_element, 'licenses')
for license in service.licenses:
if license.license:
license_e = ElementTree.SubElement(licenses_e, 'license')
if license.license.id:
ElementTree.SubElement(license_e, 'id').text = license.license.id
elif license.license.name:
ElementTree.SubElement(license_e, 'name').text = license.license.name
if license.license.text:
license_text_e_attrs = {}
if license.license.text.content_type:
license_text_e_attrs['content-type'] = license.license.text.content_type
if license.license.text.encoding:
license_text_e_attrs['encoding'] = license.license.text.encoding.value
ElementTree.SubElement(license_e, 'text',
license_text_e_attrs).text = license.license.text.content

ElementTree.SubElement(license_e, 'text').text = license.license.id
else:
ElementTree.SubElement(licenses_e, 'expression').text = license.expression

# externalReferences
if service.external_references:
self._add_external_references_to_element(ext_refs=service.external_references, element=service_element)

# properties
if service.properties and self.services_supports_properties():
Xml._add_properties_element(properties=service.properties, parent_element=service_element)

# services
if service.services:
services_element = ElementTree.SubElement(service_element, 'services')
for sub_service in service.services:
services_element.append(self._add_service_element(service=sub_service))

# releaseNotes
if service.release_notes and self.services_supports_release_notes():
Xml._add_release_notes_element(release_notes=service.release_notes, parent_element=service_element)

return service_element

def _get_vulnerability_as_xml_element_post_1_4(self, vulnerability: Vulnerability) -> ElementTree.Element:
vulnerability_element = ElementTree.Element(
'vulnerability',
Expand Down
Loading

0 comments on commit 9edf6c9

Please sign in to comment.