Skip to content

Commit d496695

Browse files
committed
feat: adding support for extension schema that descriptions vulnerability disclosures
Signed-off-by: Paul Horton <[email protected]>
1 parent 866eda7 commit d496695

File tree

7 files changed

+373
-2
lines changed

7 files changed

+373
-2
lines changed

cyclonedx/model/bom.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,10 @@ def get_urn_uuid(self) -> str:
8484

8585
def has_component(self, component: Component) -> bool:
8686
return component in self._components
87+
88+
def has_vulnerabilities(self) -> bool:
89+
for c in self.get_components():
90+
if c.has_vulnerabilities():
91+
return True
92+
93+
return False

cyclonedx/model/component.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919

2020
from enum import Enum
2121
from packageurl import PackageURL
22+
from typing import List
23+
24+
from .vulnerability import Vulnerability
2225

2326
PURL_TYPE_PREFIX = 'pypi'
2427

@@ -51,12 +54,18 @@ class Component:
5154
_description: str = None
5255
_license: str = None
5356

57+
_vulnerabilites: List[Vulnerability] = []
58+
5459
def __init__(self, name: str, version: str, qualifiers: str = None,
5560
component_type: ComponentType = ComponentType.LIBRARY):
5661
self._name = name
5762
self._version = version
5863
self._type = component_type
5964
self._qualifiers = qualifiers
65+
self._vulnerabilites = []
66+
67+
def add_vulnerability(self, vulnerability: Vulnerability):
68+
self._vulnerabilites.append(vulnerability)
6069

6170
def get_author(self) -> str:
6271
return self._author
@@ -82,6 +91,12 @@ def get_type(self) -> ComponentType:
8291
def get_version(self) -> str:
8392
return self._version
8493

94+
def get_vulnerabilities(self) -> List[Vulnerability]:
95+
return self._vulnerabilites
96+
97+
def has_vulnerabilities(self) -> bool:
98+
return len(self._vulnerabilites) != 0
99+
85100
def set_author(self, author: str):
86101
self._author = author
87102

cyclonedx/model/vulnerability.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# encoding: utf-8
2+
3+
# This file is part of CycloneDX Python Lib
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
# SPDX-License-Identifier: Apache-2.0
18+
# Copyright (c) OWASP Foundation. All Rights Reserved.
19+
20+
from enum import Enum
21+
from typing import List, Union
22+
from urllib.parse import ParseResult, urlparse
23+
24+
"""
25+
This set of classes represents the data that is possible under the CycloneDX extension
26+
schema for Vulnerabilties (version 1.0).
27+
28+
See: https://github.com/CycloneDX/specification/blob/master/schema/ext/vulnerability-1.0.xsd
29+
"""
30+
31+
32+
class VulnerabilitySourceType(Enum):
33+
"""
34+
Represents <xs:simpleType name="scoreSourceType">
35+
"""
36+
CVSS_V2 = 'CVSSv2'
37+
CVSS_V3 = 'CVSSv3'
38+
OWASP = 'OWASP Risk'
39+
OPEN_FAIR = 'Open FAIR'
40+
OTHER = 'Other'
41+
42+
43+
class VulnerabilitySeverity(Enum):
44+
"""
45+
Represents <xs:simpleType name="severityType">
46+
"""
47+
NONE = 'None'
48+
LOW = 'Low'
49+
MEDIUM = 'Medium'
50+
HIGH = 'High'
51+
CRITICAL = 'Critical'
52+
UNKNOWN = 'Unknown'
53+
54+
55+
class VulnerabilityRating:
56+
"""
57+
Represents <xs:complexType name="scoreType">
58+
"""
59+
_score_base: float
60+
_score_impact: float
61+
_score_exploitability: float
62+
_severity: VulnerabilitySeverity
63+
_method: VulnerabilitySourceType
64+
_vector: str
65+
66+
def __init__(self, score_base: float = None, score_impact: float = None, score_exploitability=None,
67+
severity: VulnerabilitySeverity = None, method: VulnerabilitySourceType = None,
68+
vector: str = None):
69+
self._score_base = score_base
70+
self._score_impact = score_impact
71+
self._score_exploitability = score_exploitability
72+
self._severity = severity
73+
self._method = method
74+
self._vector = vector
75+
76+
def get_base_score(self) -> float:
77+
return self._score_base
78+
79+
def get_impact_score(self) -> float:
80+
return self._score_impact
81+
82+
def get_exploitability_score(self) -> float:
83+
return self._score_exploitability
84+
85+
def get_severity(self) -> Union[VulnerabilitySeverity, None]:
86+
return self._severity
87+
88+
def get_method(self) -> Union[VulnerabilitySourceType, None]:
89+
return self._method
90+
91+
def get_vector(self) -> Union[str, None]:
92+
return self._vector
93+
94+
def has_score(self) -> bool:
95+
return (None, None, None) != (self._score_base, self._score_impact, self._score_exploitability)
96+
97+
98+
class Vulnerability:
99+
"""
100+
Represents <xs:complexType name="vulnerability">
101+
"""
102+
_id: str
103+
_source_name: str
104+
_source_url: ParseResult
105+
_ratings: List[VulnerabilityRating] = []
106+
_cwes: List[int] = []
107+
_description: str = None
108+
_recommendations: List[str] = []
109+
_advisories: List[str] = []
110+
111+
def __init__(self, id: str, source_name: str = None, source_url: str = None,
112+
ratings: List[VulnerabilityRating] = [], cwes: List[int] = [], description: str = None,
113+
recommendations: List[str] = [], advisories: List[str] = []):
114+
self._id = id
115+
self._source_name = source_name
116+
self._source_url = urlparse(source_url) if source_url else None
117+
self._ratings = ratings
118+
self._cwes = cwes
119+
self._description = description
120+
self._recommendations = recommendations
121+
self._advisories = advisories
122+
123+
def get_id(self) -> str:
124+
return self._id
125+
126+
def get_source_name(self) -> Union[str, None]:
127+
return self._source_name
128+
129+
def get_source_url(self) -> Union[ParseResult, None]:
130+
return self._source_url
131+
132+
def get_ratings(self) -> List[VulnerabilityRating]:
133+
return self._ratings
134+
135+
def get_cwes(self) -> List[int]:
136+
return self._cwes
137+
138+
def get_description(self) -> Union[str, None]:
139+
return self._description
140+
141+
def get_recommendations(self) -> Union[List[str], None]:
142+
return self._recommendations
143+
144+
def get_advisories(self) -> Union[List[str], None]:
145+
return self._advisories
146+
147+
def has_ratings(self) -> bool:
148+
return len(self.get_ratings()) > 0
149+
150+
def has_cwes(self) -> bool:
151+
return len(self._cwes) > 0
152+
153+
def has_recommendations(self) -> bool:
154+
return len(self._recommendations) > 0
155+
156+
def has_advisories(self) -> bool:
157+
return len(self._advisories) > 0

cyclonedx/output/xml.py

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from . import BaseOutput
2323
from .schema import BaseSchemaVersion, SchemaVersion1Dot0, SchemaVersion1Dot1, SchemaVersion1Dot2, SchemaVersion1Dot3
2424
from ..model.component import Component
25+
from ..model.vulnerability import Vulnerability, VulnerabilityRating
2526

2627

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

34+
@staticmethod
35+
def get_vulnerabilities_namespace() -> str:
36+
return 'http://cyclonedx.org/schema/ext/vulnerability/1.0'
37+
3338
def output_as_string(self) -> str:
3439
bom = self._get_bom_root_element()
3540

3641
if self.bom_supports_metadata():
3742
bom = self._add_metadata(bom=bom)
3843

44+
if self.get_bom().has_vulnerabilities():
45+
ElementTree.register_namespace('v', Xml.get_vulnerabilities_namespace())
46+
3947
components = ElementTree.SubElement(bom, 'components')
48+
if self.get_bom().has_vulnerabilities():
49+
vulnerabilities = ElementTree.SubElement(bom, 'v:vulnerabilities')
50+
4051
for component in self.get_bom().get_components():
4152
components.append(self._get_component_as_xml_element(component=component))
53+
if component.has_vulnerabilities() and self.component_supports_bom_ref():
54+
# Vulnerabilities are only possible when bom-ref is supported by the main CycloneDX schema version
55+
for vulnerability in component.get_vulnerabilities():
56+
vulnerabilities.append(self._get_vulnerability_as_xml_element(bom_ref=component.get_purl(),
57+
vulnerability=vulnerability))
4258

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

4561
def _component_supports_bom_ref_attribute(self) -> bool:
4662
return True
4763

4864
def _get_bom_root_element(self) -> ElementTree.Element:
49-
return ElementTree.Element('bom', {'xmlns': self.get_target_namespace(), 'version': '1',
50-
'serialNumber': self.get_bom().get_urn_uuid()})
65+
root_attributes = {
66+
'xmlns': self.get_target_namespace(),
67+
'version': '1',
68+
'serialNumber': self.get_bom().get_urn_uuid()
69+
}
70+
71+
if self.get_bom().has_vulnerabilities():
72+
root_attributes['xmlns:v'] = Xml.get_vulnerabilities_namespace()
73+
74+
return ElementTree.Element('bom', root_attributes)
5175

5276
def _get_component_as_xml_element(self, component: Component) -> ElementTree.Element:
5377
element_attributes = {'type': component.get_type().value}
@@ -91,6 +115,77 @@ def _get_component_as_xml_element(self, component: Component) -> ElementTree.Ele
91115

92116
return component_element
93117

118+
@staticmethod
119+
def _get_vulnerability_as_xml_element(bom_ref: str, vulnerability: Vulnerability) -> ElementTree.Element:
120+
vulnerability_element = ElementTree.Element('v:vulnerability', {
121+
'ref': bom_ref
122+
})
123+
124+
# id
125+
ElementTree.SubElement(vulnerability_element, 'v:id').text = vulnerability.get_id()
126+
127+
# source
128+
if vulnerability.get_source_name():
129+
source_element = ElementTree.SubElement(
130+
vulnerability_element, 'v:source', attrib={'name': vulnerability.get_source_name()}
131+
)
132+
if vulnerability.get_source_url():
133+
ElementTree.SubElement(source_element, 'v:url').text = vulnerability.get_source_url().geturl()
134+
135+
# ratings
136+
if vulnerability.has_ratings():
137+
ratings_element = ElementTree.SubElement(vulnerability_element, 'v:ratings')
138+
rating: VulnerabilityRating
139+
for rating in vulnerability.get_ratings():
140+
rating_element = ElementTree.SubElement(ratings_element, 'v:rating')
141+
142+
# rating.score
143+
if rating.has_score():
144+
score_element = ElementTree.SubElement(rating_element, 'v:score')
145+
if rating.get_base_score():
146+
ElementTree.SubElement(score_element, 'v:base').text = str(rating.get_base_score())
147+
if rating.get_impact_score():
148+
ElementTree.SubElement(score_element, 'v:impact').text = str(rating.get_impact_score())
149+
if rating.get_exploitability_score():
150+
ElementTree.SubElement(score_element,
151+
'v:exploitability').text = str(rating.get_exploitability_score())
152+
153+
# rating.severity
154+
if rating.get_severity():
155+
ElementTree.SubElement(rating_element, 'v:severity').text = rating.get_severity().value
156+
157+
# rating.severity
158+
if rating.get_method():
159+
ElementTree.SubElement(rating_element, 'v:method').text = rating.get_method().value
160+
161+
# rating.vector
162+
if rating.get_vector():
163+
ElementTree.SubElement(rating_element, 'v:vector').text = rating.get_vector()
164+
165+
# cwes
166+
if vulnerability.has_cwes():
167+
cwes_element = ElementTree.SubElement(vulnerability_element, 'v:cwes')
168+
for cwe in vulnerability.get_cwes():
169+
ElementTree.SubElement(cwes_element, 'v:cwe').text = str(cwe)
170+
171+
# description
172+
if vulnerability.get_description():
173+
ElementTree.SubElement(vulnerability_element, 'v:description').text = vulnerability.get_description()
174+
175+
# recommendations
176+
if vulnerability.has_recommendations():
177+
recommendations_element = ElementTree.SubElement(vulnerability_element, 'v:recommendations')
178+
for recommendation in vulnerability.get_recommendations():
179+
ElementTree.SubElement(recommendations_element, 'v:recommendation').text = recommendation
180+
181+
# advisories
182+
if vulnerability.has_advisories():
183+
advisories_element = ElementTree.SubElement(vulnerability_element, 'v:advisories')
184+
for advisory in vulnerability.get_advisories():
185+
ElementTree.SubElement(advisories_element, 'v:advisory').text = advisory
186+
187+
return vulnerability_element
188+
94189
def _add_metadata(self, bom: ElementTree.Element) -> ElementTree.Element:
95190
metadata_e = ElementTree.SubElement(bom, 'metadata')
96191
ElementTree.SubElement(metadata_e, 'timestamp').text = self.get_bom().get_metadata().get_timestamp().isoformat()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" xmlns:v="http://cyclonedx.org/schema/ext/vulnerability/1.0" version="1">
3+
<metadata>
4+
<timestamp>2021-09-01T10:50:42.051979+00:00</timestamp>
5+
</metadata>
6+
<components>
7+
<component type="library" bom-ref="pkg:pypi/[email protected]?extension=tar.gz">
8+
<name>setuptools</name>
9+
<version>50.3.2</version>
10+
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
11+
</component>
12+
</components>
13+
<v:vulnerabilities>
14+
<v:vulnerability ref="pkg:pypi/[email protected]?extension=tar.gz">
15+
<v:id>CVE-2018-7489</v:id>
16+
<v:source name="NVD">
17+
<v:url>https://nvd.nist.gov/vuln/detail/CVE-2018-7489</v:url>
18+
</v:source>
19+
<v:ratings>
20+
<v:rating>
21+
<v:score>
22+
<v:base>9.8</v:base>
23+
<v:impact>5.9</v:impact>
24+
<v:exploitability>3.0</v:exploitability>
25+
</v:score>
26+
<v:severity>Critical</v:severity>
27+
<v:method>CVSSv3</v:method>
28+
<v:vector>AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H</v:vector>
29+
</v:rating>
30+
<v:rating>
31+
<v:severity>Low</v:severity>
32+
<v:method>OWASP Risk</v:method>
33+
<v:vector>OWASP/K9:M1:O0:Z2/D1:X1:W1:L3/C2:I1:A1:T1/F1:R1:S2:P3/50</v:vector>
34+
</v:rating>
35+
</v:ratings>
36+
<v:cwes>
37+
<v:cwe>123</v:cwe>
38+
<v:cwe>456</v:cwe>
39+
</v:cwes>
40+
<v:description>A description here</v:description>
41+
<v:recommendations>
42+
<v:recommendation>Upgrade</v:recommendation>
43+
</v:recommendations>
44+
<v:advisories>
45+
<v:advisory>http://www.securityfocus.com/bid/103203</v:advisory>
46+
<v:advisory>http://www.securitytracker.com/id/1040693</v:advisory>
47+
</v:advisories>
48+
</v:vulnerability>
49+
</v:vulnerabilities>
50+
</bom>

0 commit comments

Comments
 (0)