Skip to content

Commit 958e41d

Browse files
cvssv3 validation
1 parent 73ba1db commit 958e41d

6 files changed

Lines changed: 96 additions & 7 deletions

File tree

dojo/api_v2/serializers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
)
120120
from dojo.user.utils import get_configuration_permissions_codenames
121121
from dojo.utils import is_scan_file_too_large, tag_validator
122+
from dojo.validators import cvss3_validator
122123

123124
logger = logging.getLogger(__name__)
124125
deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication")
@@ -1849,6 +1850,8 @@ def validate(self, data):
18491850
# doing it here instead of in update because update doesn't know if the value changed
18501851
self.process_risk_acceptance(data)
18511852

1853+
cvss3_validator(data.get("cvssv3"), exception_class=RestFrameworkValidationError)
1854+
18521855
return data
18531856

18541857
def validate_severity(self, value: str) -> str:
@@ -1979,6 +1982,8 @@ def validate(self, data):
19791982
msg = "Active findings cannot be risk accepted."
19801983
raise serializers.ValidationError(msg)
19811984

1985+
cvss3_validator(data.get("cvssv3"), exception_class=RestFrameworkValidationError)
1986+
19821987
return data
19831988

19841989
def validate_severity(self, value: str) -> str:
@@ -2005,6 +2010,8 @@ class Meta:
20052010
exclude = ("cve",)
20062011

20072012
def create(self, validated_data):
2013+
cvss3_validator(validated_data.get("cvssv3"), exception_class=RestFrameworkValidationError)
2014+
20082015
to_be_tagged, validated_data = self._pop_tags(validated_data)
20092016

20102017
# Save vulnerability ids and pop them
@@ -2030,9 +2037,12 @@ def create(self, validated_data):
20302037
new_finding_template.save()
20312038

20322039
self._save_tags(new_finding_template, to_be_tagged)
2040+
20332041
return new_finding_template
20342042

20352043
def update(self, instance, validated_data):
2044+
cvss3_validator(validated_data.get("cvssv3"), exception_class=RestFrameworkValidationError)
2045+
20362046
# Save vulnerability ids and pop them
20372047
if "vulnerability_id_template_set" in validated_data:
20382048
vulnerability_id_set = validated_data.pop(

dojo/models.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
from tagulous.models import TagField
4545
from tagulous.models.managers import FakeTagRelatedManager
4646

47+
from dojo.validators import cvss3_validator # to avoid circular import
48+
4749
logger = logging.getLogger(__name__)
4850
deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication")
4951

@@ -2334,8 +2336,9 @@ class Finding(models.Model):
23342336
verbose_name=_("EPSS percentile"),
23352337
help_text=_("EPSS percentile for the CVE. Describes how many CVEs are scored at or below this one."),
23362338
validators=[MinValueValidator(0.0), MaxValueValidator(1.0)])
2337-
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'")
2338-
cvssv3 = models.TextField(validators=[cvssv3_regex],
2339+
# 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'")
2340+
# cvssv3 = models.TextField(validators=[cvssv3_regex],
2341+
cvssv3 = models.TextField(validators=[cvss3_validator],
23392342
max_length=117,
23402343
null=True,
23412344
verbose_name=_("CVSS v3"),
@@ -3521,8 +3524,10 @@ class Finding_Template(models.Model):
35213524
blank=False,
35223525
verbose_name="Vulnerability Id",
35233526
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.")
3524-
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'")
3525-
cvssv3 = models.TextField(validators=[cvssv3_regex], max_length=117, null=True)
3527+
# 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'")
3528+
# cvssv3 = models.TextField(validators=[cvssv3_regex], max_length=117, null=True)
3529+
cvssv3 = models.TextField(validators=[cvss3_validator], max_length=117, null=True)
3530+
35263531
severity = models.CharField(max_length=200, null=True, blank=True)
35273532
description = models.TextField(null=True, blank=True)
35283533
mitigation = models.TextField(null=True, blank=True)

dojo/validators.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import logging
2+
from collections.abc import Callable
3+
4+
import cvss.parser
5+
from cvss import CVSS2, CVSS3, CVSS4
6+
from django.core.exceptions import ValidationError
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
def cvss3_validator(value: str | list[str], exception_class: Callable = ValidationError) -> None:
12+
logger.error("cvss3_validator called with value: %s", value)
13+
cvss_vectors = cvss.parser.parse_cvss_from_text(value)
14+
if len(cvss_vectors) > 0:
15+
vector_obj = cvss_vectors[0]
16+
17+
if isinstance(vector_obj, CVSS3):
18+
# all is good
19+
return
20+
21+
if isinstance(vector_obj, CVSS4):
22+
# CVSS4 is not supported yet by the parse_cvss_from_text function, but let's prepare for it anyway: https://github.com/RedHatProductSecurity/cvss/issues/53
23+
msg = "Unsupported CVSS(4) version detected."
24+
raise exception_class(msg)
25+
if isinstance(vector_obj, CVSS2):
26+
# CVSS2 is not supported yet by the parse_cvss_from_text function, but let's prepare for it anyway: https://github.com/RedHatProductSecurity/cvss/issues/53
27+
msg = "Unsupported CVSS(2) version detected."
28+
raise exception_class(msg)
29+
30+
msg = "Unsupported CVSS version detected."
31+
raise exception_class(msg)
32+
33+
# Explicitly raise an error if no CVSS vectors are found,
34+
# to avoid 'NoneType' errors during severity processing later.
35+
msg = "No CVSS vectors found by cvss.parse_cvss_from_text()"
36+
raise exception_class(msg)

run-unittest.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ then
5151
fi
5252

5353
echo "Running docker compose unit tests with test case $TEST_CASE ..."
54-
# Compose V2 integrates compose functions into the Docker platform, continuing to support
55-
# most of the previous docker-compose features and flags. You can run Compose V2 by
54+
# Compose V2 integrates compose functions into the Docker platform, continuing to support
55+
# most of the previous docker-compose features and flags. You can run Compose V2 by
5656
# replacing the hyphen (-) with a space, using docker compose, instead of docker-compose.
57-
docker compose exec uwsgi bash -c "python manage.py test $TEST_CASE -v2 --keepdb"
57+
docker compose exec uwsgi bash -c "python manage.py test $TEST_CASE -v 3 --keepdb" --debug-mode

unittests/test_rest_framework.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,6 +1281,42 @@ def test_severity_validation(self):
12811281
self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST, "Severity just got set to something invalid")
12821282
self.assertEqual(result.json()["severity"], ["Severity must be one of the following: ['Info', 'Low', 'Medium', 'High', 'Critical']"])
12831283

1284+
# See https://github.com/DefectDojo/django-DefectDojo/issues/8264
1285+
def test_cvss3_validation(self):
1286+
with self.subTest(i=0):
1287+
self.assertEqual(None, Finding.objects.get(id=2).cvssv3)
1288+
result = self.client.patch(self.url + "2/", data={"cvssv3": "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"})
1289+
self.assertEqual(result.status_code, status.HTTP_200_OK)
1290+
self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", Finding.objects.get(id=2).cvssv3)
1291+
1292+
with self.subTest(i=1):
1293+
# extra slash makes it invalid
1294+
result = self.client.patch(self.url + "3/", data={"cvssv3": "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H/"})
1295+
self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST)
1296+
self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"])
1297+
self.assertEqual(None, Finding.objects.get(id=3).cvssv3)
1298+
1299+
with self.subTest(i=2):
1300+
# no CVSS version prefix makes it invalid
1301+
result = self.client.patch(self.url + "3/", data={"cvssv3": "AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"})
1302+
self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST)
1303+
self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"])
1304+
self.assertEqual(None, Finding.objects.get(id=3).cvssv3)
1305+
1306+
with self.subTest(i=3):
1307+
# CVSS4 version makes it invalid
1308+
result = self.client.patch(self.url + "3/", data={"cvssv3": "CVSS:4.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"})
1309+
self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST)
1310+
self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"])
1311+
self.assertEqual(None, Finding.objects.get(id=3).cvssv3)
1312+
1313+
with self.subTest(i=4):
1314+
# CVSS4 version makes it invalid
1315+
result = self.client.patch(self.url + "3/", data={"cvssv3": "CVSS:2.0/AV:N/AC:L/Au:N/C:P/I:P/A:P"})
1316+
self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST)
1317+
self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"])
1318+
self.assertEqual(None, Finding.objects.get(id=3).cvssv3)
1319+
12841320

12851321
class FindingMetadataTest(BaseClass.BaseClassTest):
12861322
fixtures = ["dojo_testdata.json"]

unittests/tools/test_generic_parser.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,8 @@ def test_parse_json(self):
456456
self.assertIn("network", finding.unsaved_tags)
457457
self.assertEqual("3287f2d0-554f-491b-8516-3c349ead8ee5", finding.unique_id_from_tool)
458458
self.assertEqual("TEST1", finding.vuln_id_from_tool)
459+
finding.clean_fields(exclude=["reporter", "numerical_severity", "planned_remediation_date"])
460+
finding.save_no_options()
459461
with self.subTest(i=1):
460462
finding = findings[1]
461463
self.assertEqual("test title2", finding.title)

0 commit comments

Comments
 (0)