Skip to content

Commit c6481a5

Browse files
authored
Dependency Check: Support CVSS v3 (#12828)
1 parent 87be21f commit c6481a5

2 files changed

Lines changed: 129 additions & 33 deletions

File tree

dojo/tools/dependency_check/parser.py

Lines changed: 99 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from packageurl import PackageURL
1010

1111
from dojo.models import Finding
12+
from dojo.utils import parse_cvss_data
1213

1314
logger = logging.getLogger(__name__)
1415

@@ -23,6 +24,54 @@ class DependencyCheckParser:
2324
"critical": "Critical",
2425
}
2526

27+
CVSS_V3_MAPPINGS = {
28+
"attackVector": {
29+
"NETWORK": "N",
30+
"ADJACENT": "A",
31+
"LOCAL": "L",
32+
"PHYSICAL": "P",
33+
"N": "N",
34+
"A": "A",
35+
"L": "L",
36+
"P": "P",
37+
},
38+
"attackComplexity": {"LOW": "L", "HIGH": "H", "L": "L", "H": "H"},
39+
"privilegesRequired": {
40+
"NONE": "N",
41+
"LOW": "L",
42+
"HIGH": "H",
43+
"N": "N",
44+
"L": "L",
45+
"H": "H",
46+
},
47+
"userInteraction": {"NONE": "N", "REQUIRED": "R", "N": "N", "R": "R"},
48+
"scope": {"UNCHANGED": "U", "CHANGED": "C", "U": "U", "C": "C"},
49+
"confidentialityImpact": {
50+
"NONE": "N",
51+
"LOW": "L",
52+
"HIGH": "H",
53+
"N": "N",
54+
"L": "L",
55+
"H": "H",
56+
},
57+
"integrityImpact": {
58+
"NONE": "N",
59+
"LOW": "L",
60+
"HIGH": "H",
61+
"N": "N",
62+
"L": "L",
63+
"H": "H",
64+
},
65+
"availabilityImpact": {
66+
"NONE": "N",
67+
"LOW": "L",
68+
"HIGH": "H",
69+
"N": "N",
70+
"L": "L",
71+
"H": "H",
72+
},
73+
}
74+
2675
def add_finding(self, finding, dupes):
2776
key_str = "|".join(
2877
[
@@ -164,6 +213,55 @@ def get_component_name_and_version_from_dependency(
164213

165214
return None, None
166215

216+
def get_severity_and_cvss_meta(self, vulnerability, namespace) -> dict:
217+
# Get the base severity from the report
218+
severity = vulnerability.findtext(f"{namespace}severity")
219+
cvssv3 = None
220+
cvssv3_score = None
221+
# Attempt to add the CVSSv3 score, and update the severity accordingly
222+
if (cvssv3_node := vulnerability.find(namespace + "cvssV3")) is not None:
223+
try:
224+
vector_parts = [
225+
f"AV:{self.CVSS_V3_MAPPINGS['attackVector'][cvssv3_node.findtext(f'{namespace}attackVector')]}",
226+
f"AC:{self.CVSS_V3_MAPPINGS['attackComplexity'][cvssv3_node.findtext(f'{namespace}attackComplexity')]}",
227+
f"PR:{self.CVSS_V3_MAPPINGS['privilegesRequired'][cvssv3_node.findtext(f'{namespace}privilegesRequired')]}",
228+
f"UI:{self.CVSS_V3_MAPPINGS['userInteraction'][cvssv3_node.findtext(f'{namespace}userInteraction')]}",
229+
f"S:{self.CVSS_V3_MAPPINGS['scope'][cvssv3_node.findtext(f'{namespace}scope')]}",
230+
f"C:{self.CVSS_V3_MAPPINGS['confidentialityImpact'][cvssv3_node.findtext(f'{namespace}confidentialityImpact')]}",
231+
f"I:{self.CVSS_V3_MAPPINGS['integrityImpact'][cvssv3_node.findtext(f'{namespace}integrityImpact')]}",
232+
f"A:{self.CVSS_V3_MAPPINGS['availabilityImpact'][cvssv3_node.findtext(f'{namespace}availabilityImpact')]}",
233+
]
234+
version = cvssv3_node.findtext("version") or "3.1"
235+
vector = f"CVSS:{version}/" + "/".join(vector_parts)
236+
if cvss_data := parse_cvss_data(vector):
237+
cvssv3 = cvss_data.get("vector")
238+
cvssv3_score = cvss_data.get("score")
239+
severity = cvss_data.get("severity")
240+
except Exception as e:
241+
# Only log the error - there is not much we can do to recover from this
242+
logger.debug(e)
243+
elif (cvssv2_node := vulnerability.find(namespace + "cvssV2")) is not None:
244+
severity = cvssv2_node.findtext(f"{namespace}severity").lower().capitalize()
245+
246+
# handle if the severity have something not in the mapping
247+
# default to 'Medium' and produce warnings in logs
248+
if severity:
249+
if severity.strip().lower() not in self.SEVERITY_MAPPING:
250+
logger.warning(
251+
f"Warning: Unknow severity value detected '{severity}'. Bypass to 'Medium' value",
252+
)
253+
severity = "Medium"
254+
else:
255+
severity = self.SEVERITY_MAPPING[severity.strip().lower()]
256+
else:
257+
severity = "Medium"
258+
259+
return {
260+
"severity": severity,
261+
"cvssv3": cvssv3,
262+
"cvssv3_score": cvssv3_score,
263+
}
264+
167265
def get_finding_from_vulnerability(
168266
self, dependency, related_dependency, vulnerability, test, namespace,
169267
):
@@ -238,36 +336,6 @@ def get_finding_from_vulnerability(
238336
# some changes in v6.0.0 around CVSS version information
239337
# https://github.com/jeremylong/DependencyCheck/pull/2781
240338

241-
cvssv2_node = vulnerability.find(namespace + "cvssV2")
242-
cvssv3_node = vulnerability.find(namespace + "cvssV3")
243-
severity = vulnerability.findtext(f"{namespace}severity")
244-
if not severity:
245-
if cvssv3_node is not None:
246-
severity = (
247-
cvssv3_node.findtext(f"{namespace}baseSeverity")
248-
.lower()
249-
.capitalize()
250-
)
251-
elif cvssv2_node is not None:
252-
severity = (
253-
cvssv2_node.findtext(f"{namespace}severity")
254-
.lower()
255-
.capitalize()
256-
)
257-
258-
# handle if the severity have something not in the mapping
259-
# default to 'Medium' and produce warnings in logs
260-
if severity:
261-
if severity.strip().lower() not in self.SEVERITY_MAPPING:
262-
logger.warning(
263-
f"Warning: Unknow severity value detected '{severity}'. Bypass to 'Medium' value",
264-
)
265-
severity = "Medium"
266-
else:
267-
severity = self.SEVERITY_MAPPING[severity.strip().lower()]
268-
else:
269-
severity = "Medium"
270-
271339
reference_detail = None
272340
references_node = vulnerability.find(namespace + "references")
273341

@@ -315,7 +383,6 @@ def get_finding_from_vulnerability(
315383
test=test,
316384
cwe=cwe,
317385
description=description,
318-
severity=severity,
319386
mitigation=mitigation,
320387
mitigated=mitigated,
321388
is_mitigated=is_Mitigated,
@@ -326,6 +393,7 @@ def get_finding_from_vulnerability(
326393
references=reference_detail,
327394
component_name=component_name,
328395
component_version=component_version,
396+
**self.get_severity_and_cvss_meta(vulnerability, namespace),
329397
)
330398

331399
if vulnerability_id:

unittests/tools/test_dependency_check_parser.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def test_parse_file_with_single_vulnerability_has_single_finding(self):
3636
with self.subTest(i=i):
3737
self.assertEqual(items[i].title, "org.owasp:library:6.7.8 | CVE-0000-0001")
3838
self.assertEqual(items[i].severity, "Medium")
39+
self.assertEqual(items[i].cvssv3, None)
40+
self.assertEqual(items[i].cvssv3_score, None)
3941
self.assertEqual(items[i].component_name, "org.owasp:library")
4042
self.assertEqual(items[i].component_version, "6.7.8")
4143
self.assertEqual(
@@ -73,6 +75,8 @@ def test_parse_file_with_multiple_vulnerabilities_has_multiple_findings(self):
7375
items[0].description,
7476
)
7577
self.assertEqual(items[0].severity, "High")
78+
self.assertEqual(items[0].cvssv3, None)
79+
self.assertEqual(items[0].cvssv3_score, None)
7680
self.assertEqual(items[0].file_path, "adapter-ear1.ear: dom4j-2.1.1.jar")
7781
self.assertEqual(
7882
items[0].mitigation,
@@ -97,6 +101,8 @@ def test_parse_file_with_multiple_vulnerabilities_has_multiple_findings(self):
97101
items[1].description,
98102
)
99103
self.assertEqual(items[1].severity, "High")
104+
self.assertEqual(items[1].cvssv3, None)
105+
self.assertEqual(items[1].cvssv3_score, None)
100106
self.assertEqual(items[1].file_path, "adapter-ear8.ear: dom4j-2.1.1.jar")
101107
self.assertEqual(
102108
items[1].mitigation,
@@ -119,6 +125,8 @@ def test_parse_file_with_multiple_vulnerabilities_has_multiple_findings(self):
119125
items[2].description,
120126
)
121127
self.assertEqual(items[2].severity, "High")
128+
self.assertEqual(items[2].cvssv3, None)
129+
self.assertEqual(items[2].cvssv3_score, None)
122130
self.assertEqual(items[2].file_path, "adapter-ear1.ear: dom4j-extensions-2.1.1.jar")
123131
self.assertEqual(
124132
items[2].mitigation,
@@ -137,6 +145,8 @@ def test_parse_file_with_multiple_vulnerabilities_has_multiple_findings(self):
137145
# assert fails due to special characters, not too important
138146
# self.assertEqual(items[1].description, "Affected versions of `yargs-parser` are vulnerable to prototype pollution. Arguments are not properly sanitized, allowing an attacker to modify the prototype of `Object`, causing the addition or modification of an existing property that will exist on all objects.Parsing the argument `--foo.__proto__.bar baz'` adds a `bar` property with value `baz` to all objects. This is only exploitable if attackers have control over the arguments being passed to `yargs-parser`.")
139147
self.assertEqual(items[3].severity, "Low")
148+
self.assertEqual(items[3].cvssv3, None)
149+
self.assertEqual(items[3].cvssv3_score, None)
140150
self.assertEqual(items[3].file_path, "yargs-parser:5.0.0")
141151
self.assertEqual(
142152
items[3].mitigation, "Update yargs-parser:5.0.0 to at least the version recommended in the description",
@@ -163,6 +173,8 @@ def test_parse_file_with_multiple_vulnerabilities_has_multiple_findings(self):
163173
items[4].description,
164174
)
165175
self.assertEqual(items[4].severity, "High")
176+
self.assertEqual(items[4].cvssv3, "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N")
177+
self.assertEqual(items[4].cvssv3_score, 7.5)
166178
self.assertEqual(items[4].file_path, "yargs-parser:5.0.0")
167179
self.assertEqual(
168180
items[4].mitigation, "Update yargs-parser:5.0.0 to at least the version recommended in the description",
@@ -187,6 +199,8 @@ def test_parse_file_with_multiple_vulnerabilities_has_multiple_findings(self):
187199
items[5].description,
188200
)
189201
self.assertEqual(items[5].severity, "High")
202+
self.assertEqual(items[5].cvssv3, "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H")
203+
self.assertEqual(items[5].cvssv3_score, 7.5)
190204
self.assertEqual(items[5].file_path, "yargs-parser:5.0.0")
191205
self.assertEqual(
192206
items[5].mitigation, "Update yargs-parser:5.0.0 to at least the version recommended in the description",
@@ -199,6 +213,8 @@ def test_parse_file_with_multiple_vulnerabilities_has_multiple_findings(self):
199213
self.assertEqual(items[6].component_name, "org.dom4j:dom4j")
200214
self.assertEqual(items[6].component_version, "2.1.1.redhat-00001")
201215
self.assertEqual(items[6].severity, "High")
216+
self.assertEqual(items[6].cvssv3, None)
217+
self.assertEqual(items[6].cvssv3_score, None)
202218
self.assertEqual(items[6].file_path, "adapter-ear2.ear: dom4j-2.1.1.jar")
203219
self.assertEqual(
204220
items[6].mitigation,
@@ -213,6 +229,8 @@ def test_parse_file_with_multiple_vulnerabilities_has_multiple_findings(self):
213229
self.assertEqual(items[7].component_name, "dom4j")
214230
self.assertEqual(items[7].component_version, "2.1.1")
215231
self.assertEqual(items[7].severity, "High")
232+
self.assertEqual(items[7].cvssv3, None)
233+
self.assertEqual(items[7].cvssv3_score, None)
216234
self.assertEqual(
217235
items[7].mitigation, "Update dom4j:2.1.1 to at least the version recommended in the description",
218236
)
@@ -226,6 +244,8 @@ def test_parse_file_with_multiple_vulnerabilities_has_multiple_findings(self):
226244
self.assertEqual(items[8].component_name, "jquery")
227245
self.assertEqual(items[8].component_version, "3.1.1")
228246
self.assertEqual(items[8].severity, "High")
247+
self.assertEqual(items[8].cvssv3, None)
248+
self.assertEqual(items[8].cvssv3_score, None)
229249
self.assertEqual(
230250
items[8].mitigation, "Update jquery:3.1.1 to at least the version recommended in the description",
231251
)
@@ -239,8 +259,10 @@ def test_parse_file_with_multiple_vulnerabilities_has_multiple_findings(self):
239259
"**This vulnerability is mitigated and/or suppressed:** Document on why we are suppressing this vulnerability is missing!\nUpdate jquery:3.1.1 to at least the version recommended in the description",
240260
)
241261
self.assertEqual(items[9].tags, ["suppressed", "no_suppression_document"])
242-
self.assertEqual(items[10].severity, "Critical")
243-
self.assertEqual(items[10].is_mitigated, True)
262+
self.assertEqual(items[9].severity, "Critical")
263+
self.assertEqual(items[9].cvssv3, "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H")
264+
self.assertEqual(items[9].cvssv3_score, 9.8)
265+
self.assertEqual(items[9].is_mitigated, True)
244266

245267
with self.subTest(i=10):
246268
self.assertEqual(items[10].active, False)
@@ -250,6 +272,8 @@ def test_parse_file_with_multiple_vulnerabilities_has_multiple_findings(self):
250272
)
251273
self.assertEqual(items[10].tags, "suppressed")
252274
self.assertEqual(items[10].severity, "Critical")
275+
self.assertEqual(items[10].cvssv3, "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H")
276+
self.assertEqual(items[10].cvssv3_score, 9.8)
253277
self.assertEqual(items[10].is_mitigated, True)
254278

255279
def test_parse_java_6_5_3(self):
@@ -269,6 +293,8 @@ def test_parse_java_6_5_3(self):
269293
items[i].description,
270294
)
271295
self.assertEqual(items[i].severity, "Low")
296+
self.assertEqual(items[i].cvssv3, "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N")
297+
self.assertEqual(items[i].cvssv3_score, 3.7)
272298
self.assertEqual(items[i].file_path, "log4j-api-2.12.4.jar")
273299
self.assertEqual(items[i].date, datetime(2022, 1, 15, 14, 31, 13, 42600, tzinfo=UTC))
274300

@@ -295,6 +321,8 @@ def test_parse_file_pr6439(self):
295321
items[0].description,
296322
)
297323
self.assertEqual(items[0].severity, "Critical")
324+
self.assertEqual(items[0].cvssv3, "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H")
325+
self.assertEqual(items[0].cvssv3_score, 9.8)
298326
self.assertEqual(items[0].file_path, "activemq-broker-5.16.5.jar")
299327
self.assertIn(
300328
"**This vulnerability is mitigated and/or suppressed:** Ist eine Dependency vom CXF. Der im Finding erwähnte Bug ist seit Version 1.0",

0 commit comments

Comments
 (0)