Skip to content

Commit a26bfa7

Browse files
cvss4: model + parsers
1 parent 96c8e41 commit a26bfa7

File tree

18 files changed

+234
-155
lines changed

18 files changed

+234
-155
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 5.1.8 on 2025-07-06 07:25
2+
3+
import django.core.validators
4+
import dojo.validators
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('dojo', '0233_remove_test_actual_time_remove_test_estimated_time'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='finding',
17+
name='cvssv4',
18+
field=models.TextField(help_text='Common Vulnerability Scoring System version 4 (CVSSv4) score associated with this finding.', max_length=117, null=True, validators=[dojo.validators.cvss4_validator], verbose_name='CVSS v4 vector'),
19+
),
20+
migrations.AddField(
21+
model_name='finding',
22+
name='cvssv4_score',
23+
field=models.FloatField(blank=True, help_text='Numerical CVSSv4 score for the vulnerability. If the vector is given, the score is updated while saving the finding. The value must be between 0-10.', null=True, validators=[django.core.validators.MinValueValidator(0.0), django.core.validators.MaxValueValidator(10.0)], verbose_name='CVSSv4 score'),
24+
),
25+
]

dojo/models.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from tagulous.models import TagField
4444
from tagulous.models.managers import FakeTagRelatedManager
4545

46-
from dojo.validators import cvss3_validator
46+
from dojo.validators import cvss3_validator, cvss4_validator
4747

4848
logger = logging.getLogger(__name__)
4949
deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication")
@@ -2356,6 +2356,17 @@ class Finding(models.Model):
23562356
help_text=_("Numerical CVSSv3 score for the vulnerability. If the vector is given, the score is updated while saving the finding. The value must be between 0-10."),
23572357
validators=[MinValueValidator(0.0), MaxValueValidator(10.0)])
23582358

2359+
cvssv4 = models.TextField(validators=[cvss4_validator],
2360+
max_length=117,
2361+
null=True,
2362+
verbose_name=_("CVSS v4 vector"),
2363+
help_text=_("Common Vulnerability Scoring System version 4 (CVSSv4) score associated with this finding."))
2364+
cvssv4_score = models.FloatField(null=True,
2365+
blank=True,
2366+
verbose_name=_("CVSSv4 score"),
2367+
help_text=_("Numerical CVSSv4 score for the vulnerability. If the vector is given, the score is updated while saving the finding. The value must be between 0-10."),
2368+
validators=[MinValueValidator(0.0), MaxValueValidator(10.0)])
2369+
23592370
url = models.TextField(null=True,
23602371
blank=True,
23612372
editable=False,
@@ -2712,20 +2723,34 @@ def save(self, dedupe_option=True, rules_option=True, product_grading_option=Tru
27122723
self.numerical_severity = Finding.get_numerical_severity(self.severity)
27132724

27142725
# Synchronize cvssv3 score using cvssv3 vector
2726+
27152727
if self.cvssv3:
27162728
try:
2717-
27182729
cvss_data = parse_cvss_data(self.cvssv3)
27192730
if cvss_data:
2720-
self.cvssv3 = cvss_data.get("vector")
2721-
self.cvssv3_score = cvss_data.get("score")
2731+
self.cvssv3 = cvss_data.get("cvssv3")
2732+
if not self.cvssv3_score:
2733+
self.cvssv3_score = cvss_data.get("cvssv3_score")
27222734

27232735
except Exception as ex:
27242736
logger.warning("Can't compute cvssv3 score for finding id %i. Invalid cvssv3 vector found: '%s'. Exception: %s.", self.id, self.cvssv3, ex)
27252737
# remove invalid cvssv3 vector for new findings, or should we just throw a ValidationError?
27262738
if self.pk is None:
27272739
self.cvssv3 = None
27282740

2741+
# behaviour for CVVS4 is slightly different. Extracting thsi into a method would lead to probabl hard to read code
2742+
if self.cvssv4:
2743+
try:
2744+
cvss_data = parse_cvss_data(self.cvssv4)
2745+
if cvss_data:
2746+
self.cvssv4 = cvss_data.get("cvssv4_vector")
2747+
if not self.cvssv4_score:
2748+
self.cvssv4_score = cvss_data.get("cvssv4_score")
2749+
2750+
except Exception as ex:
2751+
logger.warning("Can't compute cvssv4 score for finding id %i. Invalid cvssv4 vector found: '%s'. Exception: %s.", self.id, self.cvssv4, ex)
2752+
self.cvssv4 = None
2753+
27292754
self.set_hash_code(dedupe_option)
27302755

27312756
if self.pk is None:

dojo/tools/appcheck_web_application_scanner/engines/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ def set_endpoints(self, finding: Finding, item: Any) -> None:
307307
#####
308308
def parse_cvss_vector(self, value: str) -> str | None:
309309
# CVSS4 vectors don't parse with the handy-danty parse method :(
310+
# TODO: This needs a rewrite to use dojo.utils.parse_cvss_data
310311
try:
311312
if (severity := cvss.CVSS4(value).severity) in Finding.SEVERITIES:
312313
return severity

dojo/tools/aqua/parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,8 @@ def get_item(self, resource, vuln, test):
226226

227227
cvss_data = parse_cvss_data(cvssv3)
228228
if cvss_data:
229-
finding.cvssv3 = cvss_data.get("vector")
230-
finding.cvssv3_score = cvss_data.get("score")
229+
finding.cvssv3 = cvss_data.get("cvssv3")
230+
finding.cvssv3_score = cvss_data.get("cvssv3_score")
231231

232232
if vulnerability_id != "No CVE":
233233
finding.unsaved_vulnerability_ids = [vulnerability_id]

dojo/tools/auditjs/parser.py

Lines changed: 30 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,13 @@
11
import json
2+
import logging
23
import re
34
from json.decoder import JSONDecodeError
45

56
# import cvss.parser
6-
from cvss import CVSS2, CVSS3, CVSS4, CVSSError
7-
87
from dojo.models import Finding
8+
from dojo.utils import parse_cvss_data
99

10-
11-
# TEMPORARY: Local implementation until the upstream PR is merged & released: https://github.com/RedHatProductSecurity/cvss/pull/75
12-
def parse_cvss_from_text(text):
13-
"""
14-
Parses CVSS2, CVSS3, and CVSS4 vectors from arbitrary text and returns a list of CVSS objects.
15-
16-
Parses text for substrings that look similar to CVSS vector
17-
and feeds these matches to CVSS constructor.
18-
19-
Args:
20-
text (str): arbitrary text
21-
22-
Returns:
23-
A list of CVSS objects.
24-
25-
"""
26-
# Looks for substrings that resemble CVSS2, CVSS3, or CVSS4 vectors.
27-
# CVSS3 and CVSS4 vectors start with a 'CVSS:x.x/' prefix and are matched by the optional non-capturing group.
28-
# CVSS2 vectors do not include a prefix and are matched by raw vector pattern only.
29-
# Minimum total match length is 26 characters to reduce false positives.
30-
matches = re.compile(r"(?:CVSS:[3-4]\.\d/)?[A-Za-z:/]{26,}").findall(text)
31-
32-
cvsss = set()
33-
for match in matches:
34-
try:
35-
if match.startswith("CVSS:4."):
36-
cvss = CVSS4(match)
37-
elif match.startswith("CVSS:3."):
38-
cvss = CVSS3(match)
39-
else:
40-
cvss = CVSS2(match)
41-
42-
cvsss.add(cvss)
43-
except (CVSSError, KeyError):
44-
pass
45-
46-
return list(cvsss)
10+
logger = logging.getLogger(__name__)
4711

4812

4913
class AuditJSParser:
@@ -107,7 +71,13 @@ def get_findings(self, filename, test):
10771
) = (
10872
cvss_score
10973
) = (
110-
cvss_vector
74+
cvssv3
75+
) = (
76+
cvssv4
77+
) = (
78+
cvssv3_score
79+
) = (
80+
cvssv4_score
11181
) = vulnerability_id = cwe = references = severity = None
11282
# Check mandatory
11383
if (
@@ -127,38 +97,20 @@ def get_findings(self, filename, test):
12797
raise ValueError(msg)
12898
if "cvssScore" in vulnerability:
12999
cvss_score = vulnerability["cvssScore"]
130-
if "cvssVector" in vulnerability:
131-
cvss_vectors = parse_cvss_from_text(
132-
vulnerability["cvssVector"],
133-
)
134-
135-
if len(cvss_vectors) > 0:
136-
vector_obj = cvss_vectors[0]
137-
138-
if isinstance(vector_obj, CVSS4):
139-
description += "\nCVSS V4 Vector:" + vector_obj.clean_vector()
140-
severity = vector_obj.severities()[0]
141-
142-
elif isinstance(vector_obj, CVSS3):
143-
cvss_vector = vector_obj.clean_vector()
144-
severity = vector_obj.severities()[0]
145-
146-
elif isinstance(vector_obj, CVSS2):
147-
description += "\nCVSS V2 Vector:" + vector_obj.clean_vector()
148-
severity = vector_obj.severities()[0]
149-
150-
else:
151-
msg = "Unsupported CVSS version detected in parser."
152-
raise ValueError(msg)
153-
else:
154-
# Explicitly raise an error if no CVSS vectors are found,
155-
# to avoid 'NoneType' errors during severity processing later.
156-
msg = "No CVSS vectors found. Please check that parse_cvss_from_text() correctly parses the provided cvssVector."
157-
raise ValueError(msg)
100+
cvss_data = parse_cvss_data(vulnerability.get("cvssVector"))
101+
if cvss_data:
102+
severity = cvss_data["severity"]
103+
cvssv3 = cvss_data["cvssv3"]
104+
cvssv4 = cvss_data["cvssv4"]
105+
# The score in the report can be different from what the cvss library calulates
106+
if cvss_data["major_version"] == 2:
107+
description += "\nCVSS V2 Vector:" + cvss_data["cvssv2"]
108+
if cvss_data["major_version"] == 3:
109+
cvssv3_score = cvss_score
110+
if cvss_data["major_version"] == 4:
111+
cvssv4_score = cvss_score
158112
else:
159-
# If there is no vector, calculate severity based on
160-
# score and CVSS V3 (AuditJS does not always include
161-
# it)
113+
# If there is no vector, calculate severity based on CVSS score
162114
severity = self.get_severity(cvss_score)
163115
if "cve" in vulnerability:
164116
vulnerability_id = vulnerability["cve"]
@@ -169,10 +121,12 @@ def get_findings(self, filename, test):
169121
title=title,
170122
test=test,
171123
cwe=cwe,
172-
cvssv3=cvss_vector,
173-
cvssv3_score=cvss_score,
174124
description=description,
175125
severity=severity,
126+
cvssv3=cvssv3,
127+
cvssv3_score=cvssv3_score,
128+
cvssv4=cvssv4,
129+
cvssv4_score=cvssv4_score,
176130
references=references,
177131
file_path=file_path,
178132
component_name=component_name,
@@ -181,6 +135,9 @@ def get_findings(self, filename, test):
181135
dynamic_finding=False,
182136
unique_id_from_tool=unique_id_from_tool,
183137
)
138+
logger.debug("Finding fields:")
139+
for field, value in finding.__dict__.items():
140+
logger.debug(" %s: %r", field, value)
184141
if vulnerability_id:
185142
finding.unsaved_vulnerability_ids = [vulnerability_id]
186143

dojo/tools/jfrog_xray_unified/parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ def get_item(vulnerability, test):
142142

143143
cvss_data = parse_cvss_data(cvssv3)
144144
if cvss_data:
145-
finding.cvssv3 = cvss_data.get("vector")
146-
finding.cvssv3_score = cvss_data.get("score")
145+
finding.cvssv3 = cvss_data.get("cvssv3")
146+
finding.cvssv3_score = cvss_data.get("cvssv3_score")
147147

148148
if vulnerability_id:
149149
finding.unsaved_vulnerability_ids = [vulnerability_id]

dojo/tools/npm_audit_7_plus/parser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ def get_item(item_node, tree, test):
169169
if (cvssv3 is not None) and (len(cvssv3) > 0):
170170
cvss_data = parse_cvss_data(cvssv3)
171171
if cvss_data:
172-
dojo_finding.cvssv3 = cvss_data.get("vector")
173-
dojo_finding.cvssv3_score = cvss_data.get("score")
172+
dojo_finding.cvssv3 = cvss_data.get("cvssv3")
173+
dojo_finding.cvssv3_score = cvss_data.get("cvssv3_score")
174174

175175
return dojo_finding
176176

dojo/tools/ptart/assessment_parser.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import dojo.tools.ptart.ptart_parser_tools as ptart_tools
22
from dojo.models import Finding
3+
from dojo.utils import parse_cvss_data
34

45

56
class PTARTAssessmentParser:
@@ -43,10 +44,18 @@ def get_finding(self, assessment, hit):
4344
finding.vuln_id_from_tool = hit.get("id")
4445
finding.cve = hit.get("id")
4546

46-
# Clean up and parse the CVSS vector
47-
cvss_vector = ptart_tools.parse_cvss_vector(hit, self.cvss_type)
47+
cvss_vector = hit.get("cvss_vector", None)
48+
cvss_score = hit.get("cvss_score", None)
4849
if cvss_vector:
49-
finding.cvssv3 = cvss_vector
50+
cvss_data = parse_cvss_data(cvss_vector)
51+
if cvss_data:
52+
finding.cvssv3 = cvss_data["cvssv3"]
53+
finding.cvssv4 = cvss_data["cvssv4"]
54+
# The score in the report can be different from what the cvss library calulates
55+
if cvss_data["major_version"] == 3:
56+
finding.cvssv3_score = cvss_score
57+
if cvss_data["major_version"] == 4:
58+
finding.cvssv4_score = cvss_score
5059

5160
if "labels" in hit:
5261
finding.unsaved_tags = hit["labels"]

dojo/tools/ptart/ptart_parser_tools.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def parse_cvss_vector(hit, cvss_type):
6262
return c.clean_vector()
6363
except cvss.CVSS3Error:
6464
return None
65+
6566
return None
6667

6768

dojo/tools/ptart/retest_parser.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import dojo.tools.ptart.ptart_parser_tools as ptart_tools
22
from dojo.models import Finding
3+
from dojo.utils import parse_cvss_data
34

45

56
def generate_retest_hit_title(hit, original_hit):
@@ -16,13 +17,8 @@ def generate_retest_hit_title(hit, original_hit):
1617

1718

1819
class PTARTRetestParser:
19-
def __init__(self):
20-
self.cvss_type = None
21-
2220
def get_test_data(self, tree):
23-
self.cvss_type = None
2421
if "retests" in tree:
25-
self.cvss_type = tree.get("cvss_type", None)
2622
retests = tree["retests"]
2723
else:
2824
return []
@@ -82,12 +78,18 @@ def get_finding(self, retest, hit):
8278
finding.vuln_id_from_tool = original_hit.get("id")
8379
finding.cve = original_hit.get("id")
8480

85-
cvss_vector = ptart_tools.parse_cvss_vector(
86-
original_hit,
87-
self.cvss_type,
88-
)
81+
cvss_vector = original_hit.get("cvss_vector", None)
82+
cvss_score = original_hit.get("cvss_score", None)
8983
if cvss_vector:
90-
finding.cvssv3 = cvss_vector
84+
cvss_data = parse_cvss_data(cvss_vector)
85+
if cvss_data:
86+
finding.cvssv3 = cvss_data["cvssv3"]
87+
finding.cvssv4 = cvss_data["cvssv4"]
88+
# The score in the report can be different from what the cvss library calulates
89+
if cvss_data["major_version"] == 3:
90+
finding.cvssv3_score = cvss_score
91+
if cvss_data["major_version"] == 4:
92+
finding.cvssv4_score = cvss_score
9193

9294
if "labels" in original_hit:
9395
finding.unsaved_tags = original_hit["labels"]

0 commit comments

Comments
 (0)