Skip to content

Commit b1eab93

Browse files
authored
appcheck-severity-determination-fix Use v4, v3, v2 cvss vectors for severity (#10918)
* appcheck-severity-determination-fix Use v4, v3, v2 cvss vectors for determining severity in that order; update some type hints * appcheck-severity-determination-fix process cvss-base_score-related variables for severity determination first, then fall back to vectors, then default to "Info" * appcheck-severity-determination-fix fix typo
1 parent 45df5ab commit b1eab93

4 files changed

Lines changed: 107 additions & 25 deletions

File tree

dojo/tools/appcheck_web_application_scanner/engines/appcheck.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def extract_request_response(self, finding: Finding, value: dict[str, [str]]) ->
2727
value.pop("Messages")
2828
finding.unsaved_request, finding.unsaved_response = (d.strip() for d in rr_details[0])
2929

30-
def parse_details(self, finding: Finding, value: dict[str, Union[str, dict[str, [str]]]]) -> None:
30+
def parse_details(self, finding: Finding, value: dict[str, Union[str, dict[str, list[str]]]]) -> None:
3131
self.extract_request_response(finding, value)
3232
# super's version adds everything else to the description field
3333
return super().parse_details(finding, value)

dojo/tools/appcheck_web_application_scanner/engines/base.py

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import cvss.parser
66
import dateutil.parser
77
from cpe import CPE
8+
from cvss.exceptions import CVSSError
89
from django.core.exceptions import ImproperlyConfigured
910

1011
from dojo.models import Endpoint, Finding
@@ -41,6 +42,35 @@ def escape_if_needed(x):
4142
return "".join([escape_if_needed(c) for c in s])
4243

4344

45+
def cvss_score_to_severity(score: float, version: int) -> str:
46+
"""
47+
Maps a CVSS score with a given version to a severity level.
48+
Mapping from https://nvd.nist.gov/vuln-metrics/cvss (modified slightly to have "Info" in range [0.0, 0.1) for CVSS
49+
v3/v4)
50+
"""
51+
cvss_score = float(score)
52+
if version == 2:
53+
if cvss_score >= 7.0:
54+
severity = "High"
55+
elif cvss_score >= 4.0:
56+
severity = "Medium"
57+
else:
58+
severity = "Low"
59+
else:
60+
if cvss_score >= 9.0:
61+
severity = "Critical"
62+
elif cvss_score >= 7.0:
63+
severity = "High"
64+
elif cvss_score >= 4.0:
65+
severity = "Medium"
66+
elif cvss_score >= 0.1:
67+
severity = "Low"
68+
else:
69+
severity = "Info"
70+
71+
return severity
72+
73+
4474
#######
4575
# Field parsing helper classes
4676
#######
@@ -122,7 +152,6 @@ class BaseEngineParser:
122152
* status -> active/false_p/risk_accepted (depending on value)
123153
* cves -> unsaved_vulnerability_ids (vulnerability_ids)
124154
* cpe -> component name/version
125-
* cvss_vector -> severity (determined using CVSS package)
126155
* notes -> appended to Finding description
127156
* details -> appended to Finding description
128157
@@ -143,7 +172,6 @@ class BaseEngineParser:
143172
"status": Method("parse_status"),
144173
"cves": Method("parse_cves"),
145174
"cpe": Method("parse_components"),
146-
"cvss_vector": Method("parse_severity"),
147175
# These should be listed after the 'description' entry; they append to it
148176
"notes": Method("parse_notes"),
149177
"details": Method("parse_details")}
@@ -176,7 +204,7 @@ def parse_initial_date(self, finding: Finding, value: str) -> None:
176204
def is_cve(self, c: str) -> bool:
177205
return bool(c and isinstance(c, str) and self.CVE_PATTERN.fullmatch(c))
178206

179-
def parse_cves(self, finding: Finding, value: [str]) -> None:
207+
def parse_cves(self, finding: Finding, value: list[str]) -> None:
180208
finding.unsaved_vulnerability_ids = [c.upper() for c in value if self.is_cve(c)]
181209

182210
#####
@@ -192,19 +220,6 @@ def parse_status(self, finding: Finding, value: str) -> None:
192220
elif value == "acceptable_risk":
193221
finding.risk_accepted = True
194222

195-
#####
196-
# For severity (extracted from cvss vector)
197-
#####
198-
def get_severity(self, value: str) -> Optional[str]:
199-
if cvss_obj := cvss.parser.parse_cvss_from_text(value):
200-
if (severity := cvss_obj[0].severities()[0].title()) in Finding.SEVERITIES:
201-
return severity
202-
return None
203-
204-
def parse_severity(self, finding: Finding, value: str) -> None:
205-
if severity := self.get_severity(value):
206-
finding.severity = severity
207-
208223
#####
209224
# For parsing component data
210225
#####
@@ -217,7 +232,7 @@ def parse_cpe(self, cpe_str: str) -> (Optional[str], Optional[str]):
217232
(cpe_obj.get_version() and cpe_obj.get_version()[0]) or None,
218233
)
219234

220-
def parse_components(self, finding: Finding, value: [str]) -> None:
235+
def parse_components(self, finding: Finding, value: list[str]) -> None:
221236
# Only use the first entry
222237
finding.component_name, finding.component_version = self.parse_cpe(value[0])
223238

@@ -236,12 +251,12 @@ def append_description(self, finding: Finding, addendum: dict[str, str]) -> None
236251
def parse_notes(self, finding: Finding, value: str) -> None:
237252
self.append_description(finding, {"Notes": value})
238253

239-
def extract_details(self, value: Union[str, dict[str, Union[str, dict[str, [str]]]]]) -> dict[str, str]:
254+
def extract_details(self, value: Union[str, dict[str, Union[str, dict[str, list[str]]]]]) -> dict[str, str]:
240255
if isinstance(value, dict):
241256
return {k: v for k, v in value.items() if k != "_meta"}
242257
return {"Details": str(value)}
243258

244-
def parse_details(self, finding: Finding, value: dict[str, Union[str, dict[str, [str]]]]) -> None:
259+
def parse_details(self, finding: Finding, value: dict[str, Union[str, dict[str, list[str]]]]) -> None:
245260
self.append_description(finding, self.extract_details(value))
246261

247262
#####
@@ -282,6 +297,44 @@ def set_endpoints(self, finding: Finding, item: Any) -> None:
282297
endpoints = self.parse_endpoints(item)
283298
finding.unsaved_endpoints.extend(endpoints)
284299

300+
#####
301+
# For severity (extracted from various cvss vectors)
302+
#####
303+
def parse_cvss_vector(self, value: str) -> Optional[str]:
304+
# CVSS4 vectors don't parse with the handy-danty parse method :(
305+
try:
306+
if (severity := cvss.CVSS4(value).severity) in Finding.SEVERITIES:
307+
return severity
308+
except CVSSError:
309+
pass
310+
311+
if cvss_obj := cvss.parser.parse_cvss_from_text(value):
312+
if (severity := cvss_obj[0].severities()[0].title()) in Finding.SEVERITIES:
313+
return severity
314+
return None
315+
316+
def set_severity(self, finding: Finding, item: Any) -> None:
317+
for base_score_entry, cvss_version in [
318+
("cvss_v4_base_score", 4),
319+
("cvss_v3_base_score", 3),
320+
("cvss_base_score", 2),
321+
]:
322+
if base_score := item.get(base_score_entry):
323+
finding.severity = cvss_score_to_severity(base_score, cvss_version)
324+
return
325+
326+
for vector_type in ["cvss_v4_vector", "cvss_v3_vector", "cvss_vector"]:
327+
if vector := item.get(vector_type):
328+
if severity := self.parse_cvss_vector(vector):
329+
finding.severity = severity
330+
return
331+
332+
finding.severity = "Info"
333+
334+
def process_whole_item(self, finding: Finding, item: Any) -> None:
335+
self.set_severity(finding, item)
336+
self.set_endpoints(finding, item)
337+
285338
# Returns the complete field processing map: common fields plus any engine-specific
286339
def get_engine_fields(self) -> dict[str, FieldType]:
287340
return {
@@ -302,7 +355,7 @@ def parse_finding(self, item: dict[str, Any]) -> Tuple[Finding, Tuple]:
302355
# Check first whether the field even exists on this item entry; if not, skip it
303356
if value := item.get(field):
304357
field_handler(self, finding, value)
305-
self.set_endpoints(finding, item)
358+
self.process_whole_item(finding, item)
306359
# Make a note of what scanning engine was used for this Finding
307360
self.append_description(finding, {"Scanning Engine": self.SCANNING_ENGINE})
308361
return finding, self.get_finding_key(finding)

unittests/scans/appcheck_web_application_scanner/appcheck_web_application_scanner_many_vul.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@
514514
"cvss_score": 0.0,
515515
"type": "WEB_APP",
516516
"web_app": "https://example.x73zjffz.com",
517-
"cvss_v4_vector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N",
517+
"cvss_v4_vector": "CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:A/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N",
518518
"mss_confirmed": false,
519519
"category": "web_app",
520520
"description": "[[markup]]This is simply a report of HTTP request methods supported by the web application.",

unittests/tools/test_appcheck_web_application_scanner_parser.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from dojo.tools.appcheck_web_application_scanner.engines.appcheck import AppCheckScanningEngineParser
55
from dojo.tools.appcheck_web_application_scanner.engines.base import (
66
BaseEngineParser,
7+
cvss_score_to_severity,
78
escape_non_printable,
89
strip_markup,
910
)
@@ -96,7 +97,7 @@ def test_appcheck_web_application_scanner_parser_with_many_vuln_has_many_finding
9697
self.assertEqual("a25dae3aff97a06b6923b5fc9cc32826e1fd87ab", finding.unique_id_from_tool)
9798
self.assertEqual("Apache Tomcat < v9.0.0.M10 - External Control of Assumed-Immutable Web Parameter in JSP Servlet (CVE-2016-6796)", finding.title)
9899
self.assertEqual("2024-06-26", finding.date)
99-
self.assertEqual("Medium", finding.severity)
100+
self.assertEqual("High", finding.severity)
100101
self.assertEqual(True, finding.active)
101102
self.assertEqual("GET Request", finding.unsaved_request)
102103
self.assertEqual("Response", finding.unsaved_response)
@@ -121,7 +122,7 @@ def test_appcheck_web_application_scanner_parser_with_many_vuln_has_many_finding
121122
self.assertEqual("02769aa244c456f0aad810354748faaa70d089c1129dc9c5", finding.unique_id_from_tool)
122123
self.assertEqual("Permitted HTTP Methods", finding.title)
123124
self.assertEqual("2024-06-27", finding.date)
124-
self.assertEqual("Low", finding.severity)
125+
self.assertEqual("Medium", finding.severity)
125126
self.assertEqual(True, finding.active)
126127
self.assertIsNone(finding.unsaved_request)
127128
self.assertIsNone(finding.unsaved_response)
@@ -334,8 +335,15 @@ def test_appcheck_web_application_scanner_parser_base_engine_parser(self):
334335
# Invalid cvss vectors
335336
("", None),
336337
("AV:N/AC:H", None),
338+
("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N", "High"),
339+
("CVSS:3.0/AV:L/AC:H/PR:H/UI:R/S:U/C:N/I:N/A:N", None),
340+
("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:L/SI:H/SA:H", "Critical"),
341+
("CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:A/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N", "Medium"),
342+
("CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:A/VC:H/VI:H/VA:H/SC:H/SI:N/SA:H", "High"),
343+
("CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:A/VC:N/VI:N/VA:N/SC:H/SI:N/SA:H", "Low"),
344+
("CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:A/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N", None),
337345
]:
338-
self.assertEqual(severity, engine.get_severity(cvss_vector))
346+
self.assertEqual(severity, engine.parse_cvss_vector(cvss_vector))
339347

340348
# Test component parsing
341349
f = Finding()
@@ -560,3 +568,24 @@ def test_appcheck_web_application_scanner_parser_non_printable_escape(self):
560568
),
561569
]:
562570
self.assertEqual(expected, escape_non_printable(test_string))
571+
572+
def test_appcheck_web_application_scanner_parser_cvss_score_mapping(self):
573+
for cvss_score, version, expected in [
574+
# CVSSv2
575+
(0.0, 2, "Low"), (0.09, 2, "Low"), (0.1, 2, "Low"), (3.9, 2, "Low"),
576+
(4.0, 2, "Medium"), (5.5, 2, "Medium"), (6.9, 2, "Medium"),
577+
(7.0, 2, "High"), (8.3, 2, "High"), (10.0, 2, "High"),
578+
# CVSSv3
579+
(0.0, 3, "Info"), (0.09, 3, "Info"),
580+
(0.1, 3, "Low"), (1.2, 3, "Low"), (3.9, 3, "Low"),
581+
(4.0, 3, "Medium"), (5.4, 3, "Medium"), (6.9, 3, "Medium"),
582+
(7.0, 3, "High"), (8.3, 3, "High"), (8.9, 3, "High"),
583+
(9.0, 3, "Critical"), (9.7, 3, "Critical"), (10.0, 3, "Critical"),
584+
# CVSSv4
585+
(0.0, 4, "Info"), (0.09, 4, "Info"),
586+
(0.1, 4, "Low"), (1.2, 4, "Low"), (3.9, 4, "Low"),
587+
(4.0, 4, "Medium"), (5.4, 4, "Medium"), (6.9, 4, "Medium"),
588+
(7.0, 4, "High"), (8.3, 4, "High"), (8.9, 4, "High"),
589+
(9.0, 4, "Critical"), (9.7, 4, "Critical"), (10.0, 4, "Critical"),
590+
]:
591+
self.assertEqual(expected, cvss_score_to_severity(cvss_score, version))

0 commit comments

Comments
 (0)