Skip to content

Commit

Permalink
Merge pull request #5 from CycloneDX/feat/support-schema-extension-vu…
Browse files Browse the repository at this point in the history
…lnerability-1.0

FEATURE: add support for Vulnerability Disclosures
  • Loading branch information
madpah authored Sep 15, 2021
2 parents 866eda7 + d5aabcf commit 6914272
Show file tree
Hide file tree
Showing 8 changed files with 410 additions and 4 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ parser = EnvironmentParser()

### Modelling

You can create a BOM Model from either an Parser instance or manually using the methods avaialbel directly on the `Bom` class.
You can create a BOM Model from either a Parser instance or manually using the methods avaialbel directly on the `Bom` class.

The model also supports definition of vulnerabilities for output using the CycloneDX schema extension for
[Vulnerability Disclosures](https://cyclonedx.org/use-cases/#vulnerability-disclosure) as of version 0.3.0.

**Note:** Known vulnerabilities associated with Components can be sourced from various data sources, but this library
will not source them for you. Perhaps look at [Jake](https://github.com/sonatype-nexus-community/jake) if you're interested in this.

#### Example from a Parser

Expand Down
7 changes: 7 additions & 0 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,10 @@ def get_urn_uuid(self) -> str:

def has_component(self, component: Component) -> bool:
return component in self._components

def has_vulnerabilities(self) -> bool:
for c in self.get_components():
if c.has_vulnerabilities():
return True

return False
15 changes: 15 additions & 0 deletions cyclonedx/model/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

from enum import Enum
from packageurl import PackageURL
from typing import List

from .vulnerability import Vulnerability

PURL_TYPE_PREFIX = 'pypi'

Expand Down Expand Up @@ -51,12 +54,18 @@ class Component:
_description: str = None
_license: str = None

_vulnerabilites: List[Vulnerability] = []

def __init__(self, name: str, version: str, qualifiers: str = None,
component_type: ComponentType = ComponentType.LIBRARY):
self._name = name
self._version = version
self._type = component_type
self._qualifiers = qualifiers
self._vulnerabilites = []

def add_vulnerability(self, vulnerability: Vulnerability):
self._vulnerabilites.append(vulnerability)

def get_author(self) -> str:
return self._author
Expand All @@ -82,6 +91,12 @@ def get_type(self) -> ComponentType:
def get_version(self) -> str:
return self._version

def get_vulnerabilities(self) -> List[Vulnerability]:
return self._vulnerabilites

def has_vulnerabilities(self) -> bool:
return len(self._vulnerabilites) != 0

def set_author(self, author: str):
self._author = author

Expand Down
157 changes: 157 additions & 0 deletions cyclonedx/model/vulnerability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# encoding: utf-8

# This file is part of CycloneDX Python Lib
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

from enum import Enum
from typing import List, Union
from urllib.parse import ParseResult, urlparse

"""
This set of classes represents the data that is possible under the CycloneDX extension
schema for Vulnerabilties (version 1.0).
See: https://github.com/CycloneDX/specification/blob/master/schema/ext/vulnerability-1.0.xsd
"""


class VulnerabilitySourceType(Enum):
"""
Represents <xs:simpleType name="scoreSourceType">
"""
CVSS_V2 = 'CVSSv2'
CVSS_V3 = 'CVSSv3'
OWASP = 'OWASP Risk'
OPEN_FAIR = 'Open FAIR'
OTHER = 'Other'


class VulnerabilitySeverity(Enum):
"""
Represents <xs:simpleType name="severityType">
"""
NONE = 'None'
LOW = 'Low'
MEDIUM = 'Medium'
HIGH = 'High'
CRITICAL = 'Critical'
UNKNOWN = 'Unknown'


class VulnerabilityRating:
"""
Represents <xs:complexType name="scoreType">
"""
_score_base: float
_score_impact: float
_score_exploitability: float
_severity: VulnerabilitySeverity
_method: VulnerabilitySourceType
_vector: str

def __init__(self, score_base: float = None, score_impact: float = None, score_exploitability=None,
severity: VulnerabilitySeverity = None, method: VulnerabilitySourceType = None,
vector: str = None):
self._score_base = score_base
self._score_impact = score_impact
self._score_exploitability = score_exploitability
self._severity = severity
self._method = method
self._vector = vector

def get_base_score(self) -> float:
return self._score_base

def get_impact_score(self) -> float:
return self._score_impact

def get_exploitability_score(self) -> float:
return self._score_exploitability

def get_severity(self) -> Union[VulnerabilitySeverity, None]:
return self._severity

def get_method(self) -> Union[VulnerabilitySourceType, None]:
return self._method

def get_vector(self) -> Union[str, None]:
return self._vector

def has_score(self) -> bool:
return (None, None, None) != (self._score_base, self._score_impact, self._score_exploitability)


class Vulnerability:
"""
Represents <xs:complexType name="vulnerability">
"""
_id: str
_source_name: str
_source_url: ParseResult
_ratings: List[VulnerabilityRating] = []
_cwes: List[int] = []
_description: str = None
_recommendations: List[str] = []
_advisories: List[str] = []

def __init__(self, id: str, source_name: str = None, source_url: str = None,
ratings: List[VulnerabilityRating] = [], cwes: List[int] = [], description: str = None,
recommendations: List[str] = [], advisories: List[str] = []):
self._id = id
self._source_name = source_name
self._source_url = urlparse(source_url) if source_url else None
self._ratings = ratings
self._cwes = cwes
self._description = description
self._recommendations = recommendations
self._advisories = advisories

def get_id(self) -> str:
return self._id

def get_source_name(self) -> Union[str, None]:
return self._source_name

def get_source_url(self) -> Union[ParseResult, None]:
return self._source_url

def get_ratings(self) -> List[VulnerabilityRating]:
return self._ratings

def get_cwes(self) -> List[int]:
return self._cwes

def get_description(self) -> Union[str, None]:
return self._description

def get_recommendations(self) -> Union[List[str], None]:
return self._recommendations

def get_advisories(self) -> Union[List[str], None]:
return self._advisories

def has_ratings(self) -> bool:
return len(self.get_ratings()) > 0

def has_cwes(self) -> bool:
return len(self._cwes) > 0

def has_recommendations(self) -> bool:
return len(self._recommendations) > 0

def has_advisories(self) -> bool:
return len(self._advisories) > 0
103 changes: 100 additions & 3 deletions cyclonedx/output/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from . import BaseOutput
from .schema import BaseSchemaVersion, SchemaVersion1Dot0, SchemaVersion1Dot1, SchemaVersion1Dot2, SchemaVersion1Dot3
from ..model.component import Component
from ..model.vulnerability import Vulnerability, VulnerabilityRating


class Xml(BaseOutput, BaseSchemaVersion):
Expand All @@ -30,24 +31,49 @@ class Xml(BaseOutput, BaseSchemaVersion):
def get_target_namespace(self) -> str:
return 'http://cyclonedx.org/schema/bom/{}'.format(self.get_schema_version())

@staticmethod
def get_vulnerabilities_namespace() -> str:
return 'http://cyclonedx.org/schema/ext/vulnerability/1.0'

def output_as_string(self) -> str:
bom = self._get_bom_root_element()

if self.bom_supports_metadata():
bom = self._add_metadata(bom=bom)

if self.get_bom().has_vulnerabilities():
ElementTree.register_namespace('v', Xml.get_vulnerabilities_namespace())

components = ElementTree.SubElement(bom, 'components')
# if self.get_bom().has_vulnerabilities():
# vulnerabilities = ElementTree.SubElement(bom, 'v:vulnerabilities')

for component in self.get_bom().get_components():
components.append(self._get_component_as_xml_element(component=component))
component_element = self._get_component_as_xml_element(component=component)
components.append(component_element)
if component.has_vulnerabilities() and self.component_supports_bom_ref():
# Vulnerabilities are only possible when bom-ref is supported by the main CycloneDX schema version
vulnerabilities = ElementTree.SubElement(component_element, 'v:vulnerabilities')
for vulnerability in component.get_vulnerabilities():
vulnerabilities.append(self._get_vulnerability_as_xml_element(bom_ref=component.get_purl(),
vulnerability=vulnerability))

return Xml.XML_VERSION_DECLARATION + ElementTree.tostring(bom, 'unicode')

def _component_supports_bom_ref_attribute(self) -> bool:
return True

def _get_bom_root_element(self) -> ElementTree.Element:
return ElementTree.Element('bom', {'xmlns': self.get_target_namespace(), 'version': '1',
'serialNumber': self.get_bom().get_urn_uuid()})
root_attributes = {
'xmlns': self.get_target_namespace(),
'version': '1',
'serialNumber': self.get_bom().get_urn_uuid()
}

if self.get_bom().has_vulnerabilities():
root_attributes['xmlns:v'] = Xml.get_vulnerabilities_namespace()

return ElementTree.Element('bom', root_attributes)

def _get_component_as_xml_element(self, component: Component) -> ElementTree.Element:
element_attributes = {'type': component.get_type().value}
Expand Down Expand Up @@ -91,6 +117,77 @@ def _get_component_as_xml_element(self, component: Component) -> ElementTree.Ele

return component_element

@staticmethod
def _get_vulnerability_as_xml_element(bom_ref: str, vulnerability: Vulnerability) -> ElementTree.Element:
vulnerability_element = ElementTree.Element('v:vulnerability', {
'ref': bom_ref
})

# id
ElementTree.SubElement(vulnerability_element, 'v:id').text = vulnerability.get_id()

# source
if vulnerability.get_source_name():
source_element = ElementTree.SubElement(
vulnerability_element, 'v:source', attrib={'name': vulnerability.get_source_name()}
)
if vulnerability.get_source_url():
ElementTree.SubElement(source_element, 'v:url').text = vulnerability.get_source_url().geturl()

# ratings
if vulnerability.has_ratings():
ratings_element = ElementTree.SubElement(vulnerability_element, 'v:ratings')
rating: VulnerabilityRating
for rating in vulnerability.get_ratings():
rating_element = ElementTree.SubElement(ratings_element, 'v:rating')

# rating.score
if rating.has_score():
score_element = ElementTree.SubElement(rating_element, 'v:score')
if rating.get_base_score():
ElementTree.SubElement(score_element, 'v:base').text = str(rating.get_base_score())
if rating.get_impact_score():
ElementTree.SubElement(score_element, 'v:impact').text = str(rating.get_impact_score())
if rating.get_exploitability_score():
ElementTree.SubElement(score_element,
'v:exploitability').text = str(rating.get_exploitability_score())

# rating.severity
if rating.get_severity():
ElementTree.SubElement(rating_element, 'v:severity').text = rating.get_severity().value

# rating.severity
if rating.get_method():
ElementTree.SubElement(rating_element, 'v:method').text = rating.get_method().value

# rating.vector
if rating.get_vector():
ElementTree.SubElement(rating_element, 'v:vector').text = rating.get_vector()

# cwes
if vulnerability.has_cwes():
cwes_element = ElementTree.SubElement(vulnerability_element, 'v:cwes')
for cwe in vulnerability.get_cwes():
ElementTree.SubElement(cwes_element, 'v:cwe').text = str(cwe)

# description
if vulnerability.get_description():
ElementTree.SubElement(vulnerability_element, 'v:description').text = vulnerability.get_description()

# recommendations
if vulnerability.has_recommendations():
recommendations_element = ElementTree.SubElement(vulnerability_element, 'v:recommendations')
for recommendation in vulnerability.get_recommendations():
ElementTree.SubElement(recommendations_element, 'v:recommendation').text = recommendation

# advisories
if vulnerability.has_advisories():
advisories_element = ElementTree.SubElement(vulnerability_element, 'v:advisories')
for advisory in vulnerability.get_advisories():
ElementTree.SubElement(advisories_element, 'v:advisory').text = advisory

return vulnerability_element

def _add_metadata(self, bom: ElementTree.Element) -> ElementTree.Element:
metadata_e = ElementTree.SubElement(bom, 'metadata')
ElementTree.SubElement(metadata_e, 'timestamp').text = self.get_bom().get_metadata().get_timestamp().isoformat()
Expand Down
Loading

0 comments on commit 6914272

Please sign in to comment.