Skip to content

Commit 3263ea8

Browse files
Improve cvssv3 validation (#12440)
* cvssv3 validation * remove validator calls that are no longer needed * fix CVSS2.0 test case * fix CVSS2.0 test case * fix CVSS2.0 test case * add migration * add migration * add TODO for parsers to check/fix * revert testdata change * cleanup * fix tests, cleanup * update parsers that didn't parse cvss correctly yet * fix base score * fix parser * fix typos * fix typos * fix typos * address feedback * rebase migration * rebase migration * rebase migration + serializer
1 parent 008cdff commit 3263ea8

File tree

21 files changed

+264
-90
lines changed

21 files changed

+264
-90
lines changed

docs/content/en/open_source/contributing/how-to-write-a-parser.md

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -169,15 +169,25 @@ Good example:
169169
### Do not parse CVSS by hand (vector, score or severity)
170170

171171
Data can have `CVSS` vectors or scores. Don't write your own CVSS score algorithm.
172-
For parser, we rely on module `cvss`.
172+
For parser, we rely on module `cvss`. But we also have a helper method to validate the vector and extract the base score and severity from it.
173173

174-
It's easy to use and will make the parser aligned with the rest of the code.
174+
```python
175+
from dojo.utils import parse_cvss_data
176+
cvss_data = parse_cvss_data("CVSS:3.0/S:C/C:H/I:H/A:N/AV:P/AC:H/PR:H/UI:R/E:H/RL:O/RC:R/CR:H/IR:X/AR:X/MAC:H/MPR:X/MUI:X/MC:L/MA:X")
177+
if cvss_data:
178+
finding.cvssv3 = cvss_data.get("vector")
179+
finding.cvssv3_score = cvss_data.get("score")
180+
finding.severity = cvss_data.get("severity") # if your tool does generate severity
181+
```
182+
183+
If you need more manual processing, you can parse the `CVSS3` vector directly.
175184

176185
Example of use:
177186

178187
```python
179-
from cvss.cvss3 import CVSS3
180188
import cvss.parser
189+
from cvss import CVSS2, CVSS3
190+
181191
vectors = cvss.parser.parse_cvss_from_text("CVSS:3.0/S:C/C:H/I:H/A:N/AV:P/AC:H/PR:H/UI:R/E:H/RL:O/RC:R/CR:H/IR:X/AR:X/MAC:H/MPR:X/MUI:X/MC:L/MA:X")
182192
if len(vectors) > 0 and type(vectors[0]) is CVSS3:
183193
print(vectors[0].severities()) # this is the 3 severities
@@ -186,17 +196,8 @@ if len(vectors) > 0 and type(vectors[0]) is CVSS3:
186196
severity = vectors[0].severities()[0]
187197
vectors[0].compute_base_score()
188198
cvssv3_score = vectors[0].scores()[0]
189-
print(severity)
190-
print(cvssv3_score)
191-
```
192-
193-
Good example:
194-
195-
```python
196-
vectors = cvss.parser.parse_cvss_from_text(item['cvss_vect'])
197-
if len(vectors) > 0 and type(vectors[0]) is CVSS3:
198-
finding.cvss = vectors[0].clean_vector()
199-
finding.severity = vectors[0].severities()[0] # if your tool does generate severity
199+
finding.severity = severity
200+
finding.cvssv3_score = cvssv3_score
200201
```
201202

202203
Bad example (DIY):
@@ -308,7 +309,7 @@ or like this:
308309
$ ./run-unittest.sh --test-case unittests.tools.test_aqua_parser.TestAquaParser
309310
{{< /highlight >}}
310311

311-
If you want to run all unit tests, simply run `$ docker-compose exec uwsgi bash -c 'python manage.py test unittests -v2'`
312+
If you want to run all parser unit tests, simply run `$ docker-compose exec uwsgi bash -c 'python manage.py test -p "test_*_parser.py" -v2'`
312313

313314
### Endpoint validation
314315

dojo/api_v2/serializers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@
123123
requires_tool_type,
124124
)
125125
from dojo.user.utils import get_configuration_permissions_codenames
126-
from dojo.utils import is_scan_file_too_large, tag_validator
126+
from dojo.utils import is_scan_file_too_large
127+
from dojo.validators import tag_validator
127128

128129
logger = logging.getLogger(__name__)
129130
deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication")
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 5.1.8 on 2025-05-14 06:35
2+
3+
import dojo.validators
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('dojo', '0230_alter_jira_instance_accepted_mapping_resolution_and_more'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='finding',
16+
name='cvssv3',
17+
field=models.TextField(help_text='Common Vulnerability Scoring System version 3 (CVSSv3) score associated with this finding.', max_length=117, null=True, validators=[dojo.validators.cvss3_validator], verbose_name='CVSS v3 vector'),
18+
),
19+
migrations.AlterField(
20+
model_name='finding_template',
21+
name='cvssv3',
22+
field=models.TextField(help_text='Common Vulnerability Scoring System version 3 (CVSSv3) score associated with this finding.', max_length=117, null=True, validators=[dojo.validators.cvss3_validator], verbose_name='CVSS v3 vector'),
23+
),
24+
]

dojo/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@
110110
get_system_setting,
111111
is_finding_groups_enabled,
112112
is_scan_file_too_large,
113-
tag_validator,
114113
)
114+
from dojo.validators import tag_validator
115115
from dojo.widgets import TableCheckboxWidget
116116

117117
logger = logging.getLogger(__name__)

dojo/models.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import hyperlink
1515
import tagulous.admin
1616
from auditlog.registry import auditlog
17-
from cvss import CVSS3
1817
from dateutil.relativedelta import relativedelta
1918
from django import forms
2019
from django.conf import settings
@@ -44,6 +43,8 @@
4443
from tagulous.models import TagField
4544
from tagulous.models.managers import FakeTagRelatedManager
4645

46+
from dojo.validators import cvss3_validator
47+
4748
logger = logging.getLogger(__name__)
4849
deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication")
4950

@@ -2331,12 +2332,11 @@ class Finding(models.Model):
23312332
verbose_name=_("EPSS percentile"),
23322333
help_text=_("EPSS percentile for the CVE. Describes how many CVEs are scored at or below this one."),
23332334
validators=[MinValueValidator(0.0), MaxValueValidator(1.0)])
2334-
cvssv3_regex = RegexValidator(regex=r"^AV:[NALP]|AC:[LH]|PR:[UNLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]", message="CVSS must be entered in format: 'AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H'")
2335-
cvssv3 = models.TextField(validators=[cvssv3_regex],
2335+
cvssv3 = models.TextField(validators=[cvss3_validator],
23362336
max_length=117,
23372337
null=True,
2338-
verbose_name=_("CVSS v3"),
2339-
help_text=_("Common Vulnerability Scoring System version 3 (CVSSv3) score associated with this flaw."))
2338+
verbose_name=_("CVSS v3 vector"),
2339+
help_text=_("Common Vulnerability Scoring System version 3 (CVSSv3) score associated with this finding."))
23402340
cvssv3_score = models.FloatField(null=True,
23412341
blank=True,
23422342
verbose_name=_("CVSSv3 score"),
@@ -2698,11 +2698,17 @@ def save(self, dedupe_option=True, rules_option=True, product_grading_option=Tru
26982698
# Synchronize cvssv3 score using cvssv3 vector
26992699
if self.cvssv3:
27002700
try:
2701-
cvss_object = CVSS3(self.cvssv3)
2702-
# use the environmental score, which is the most refined score
2703-
self.cvssv3_score = cvss_object.scores()[2]
2701+
2702+
cvss_data = parse_cvss_data(self.cvssv3)
2703+
if cvss_data:
2704+
self.cvssv3 = cvss_data.get("vector")
2705+
self.cvssv3_score = cvss_data.get("score")
2706+
27042707
except Exception as ex:
2705-
logger.error("Can't compute cvssv3 score for finding id %i. Invalid cvssv3 vector found: '%s'. Exception: %s", self.id, self.cvssv3, ex)
2708+
logger.warning("Can't compute cvssv3 score for finding id %i. Invalid cvssv3 vector found: '%s'. Exception: %s.", self.id, self.cvssv3, ex)
2709+
# remove invalid cvssv3 vector for new findings, or should we just throw a ValidationError?
2710+
if self.pk is None:
2711+
self.cvssv3 = None
27062712

27072713
self.set_hash_code(dedupe_option)
27082714

@@ -3515,8 +3521,8 @@ class Finding_Template(models.Model):
35153521
blank=False,
35163522
verbose_name="Vulnerability Id",
35173523
help_text="An id of a vulnerability in a security advisory associated with this finding. Can be a Common Vulnerabilities and Exposures (CVE) or from other sources.")
3518-
cvssv3_regex = RegexValidator(regex=r"^AV:[NALP]|AC:[LH]|PR:[UNLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]", message="CVSS must be entered in format: 'AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H'")
3519-
cvssv3 = models.TextField(validators=[cvssv3_regex], max_length=117, null=True)
3524+
cvssv3 = models.TextField(help_text=_("Common Vulnerability Scoring System version 3 (CVSSv3) score associated with this finding."), validators=[cvss3_validator], max_length=117, null=True, verbose_name=_("CVSS v3 vector"))
3525+
35203526
severity = models.CharField(max_length=200, null=True, blank=True)
35213527
description = models.TextField(null=True, blank=True)
35223528
mitigation = models.TextField(null=True, blank=True)
@@ -4632,7 +4638,11 @@ def __str__(self):
46324638
auditlog.register(Notification_Webhooks, exclude_fields=["header_name", "header_value"])
46334639

46344640

4635-
from dojo.utils import calculate_grade, to_str_typed # noqa: E402 # there is issue due to a circular import
4641+
from dojo.utils import ( # noqa: E402 # there is issue due to a circular import
4642+
calculate_grade,
4643+
parse_cvss_data,
4644+
to_str_typed,
4645+
)
46364646

46374647
tagulous.admin.register(Product.tags)
46384648
tagulous.admin.register(Test.tags)

dojo/tools/aqua/parser.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22

33
from dojo.models import Finding
4+
from dojo.utils import parse_cvss_data
45

56

67
class AquaParser:
@@ -201,6 +202,7 @@ def get_item(self, resource, vuln, test):
201202
f"NVD score v2 ({score}) used for classification.\n"
202203
)
203204
severity_justification += "\nNVD v2 vectors: {}".format(vuln.get("nvd_vectors"))
205+
204206
severity_justification += f"\n{used_for_classification}"
205207
severity = self.severity_of(score)
206208
finding = Finding(
@@ -214,14 +216,19 @@ def get_item(self, resource, vuln, test):
214216
severity=severity,
215217
severity_justification=severity_justification,
216218
cwe=0,
217-
cvssv3=cvssv3,
218219
description=description.strip(),
219220
mitigation=fix_version,
220221
references=url,
221222
component_name=resource.get("name"),
222223
component_version=resource.get("version"),
223224
impact=severity,
224225
)
226+
227+
cvss_data = parse_cvss_data(cvssv3)
228+
if cvss_data:
229+
finding.cvssv3 = cvss_data.get("vector")
230+
finding.cvssv3_score = cvss_data.get("score")
231+
225232
if vulnerability_id != "No CVE":
226233
finding.unsaved_vulnerability_ids = [vulnerability_id]
227234
if vuln.get("epss_score"):

dojo/tools/jfrog_xray_unified/parser.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from datetime import datetime
33

44
from dojo.models import Finding
5+
from dojo.utils import parse_cvss_data
56

67

78
class JFrogXrayUnifiedParser:
@@ -134,12 +135,16 @@ def get_item(vulnerability, test):
134135
dynamic_finding=False,
135136
references=references,
136137
impact=severity,
137-
cvssv3=cvssv3,
138138
date=scan_time,
139139
unique_id_from_tool=vulnerability["issue_id"],
140140
tags=tags,
141141
)
142142

143+
cvss_data = parse_cvss_data(cvssv3)
144+
if cvss_data:
145+
finding.cvssv3 = cvss_data.get("vector")
146+
finding.cvssv3_score = cvss_data.get("score")
147+
143148
if vulnerability_id:
144149
finding.unsaved_vulnerability_ids = [vulnerability_id]
145150

dojo/tools/npm_audit_7_plus/parser.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44

55
from dojo.models import Finding
6+
from dojo.utils import parse_cvss_data
67

78
logger = logging.getLogger(__name__)
89

@@ -166,7 +167,10 @@ def get_item(item_node, tree, test):
166167
dojo_finding.cwe = cwe
167168

168169
if (cvssv3 is not None) and (len(cvssv3) > 0):
169-
dojo_finding.cvssv3 = cvssv3
170+
cvss_data = parse_cvss_data(cvssv3)
171+
if cvss_data:
172+
dojo_finding.cvssv3 = cvss_data.get("vector")
173+
dojo_finding.cvssv3_score = cvss_data.get("score")
170174

171175
return dojo_finding
172176

dojo/tools/qualys/csv_parser.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.conf import settings
99

1010
from dojo.models import Endpoint, Finding
11+
from dojo.utils import parse_cvss_data
1112

1213
_logger = logging.getLogger(__name__)
1314

@@ -227,8 +228,13 @@ def build_findings_from_dict(report_findings: [dict]) -> [Finding]:
227228
impact=report_finding["Impact"],
228229
date=date,
229230
vuln_id_from_tool=report_finding["QID"],
230-
cvssv3=cvssv3,
231231
)
232+
# Make sure vector is valid
233+
cvss_data = parse_cvss_data(cvssv3)
234+
if cvss_data:
235+
finding.cvssv3 = cvss_data.get("vector")
236+
finding.cvssv3_score = cvss_data.get("score")
237+
232238
# Qualys reports regression findings as active, but with a Date Last
233239
# Fixed.
234240
if report_finding["Date Last Fixed"]:

dojo/tools/qualys/parser.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from dojo.models import Endpoint, Finding
1010
from dojo.tools.qualys import csv_parser
11+
from dojo.utils import parse_cvss_data
1112

1213
logger = logging.getLogger(__name__)
1314

@@ -352,7 +353,11 @@ def parse_finding(host, tree):
352353
finding.is_mitigated = temp["mitigated"]
353354
finding.active = temp["active"]
354355
if temp.get("CVSS_vector") is not None:
355-
finding.cvssv3 = temp.get("CVSS_vector")
356+
cvss_data = parse_cvss_data(temp.get("CVSS_vector"))
357+
if cvss_data:
358+
finding.cvssv3 = cvss_data.get("vector")
359+
finding.cvssv3_score = cvss_data.get("score")
360+
356361
if temp.get("CVSS_value") is not None:
357362
finding.cvssv3_score = temp.get("CVSS_value")
358363
finding.verified = True

0 commit comments

Comments
 (0)