From 5a25e18049e34958947673b1c3a46d24792fa483 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Tue, 13 May 2025 23:00:59 +0200 Subject: [PATCH 01/21] cvssv3 validation --- dojo/api_v2/serializers.py | 12 +++++ dojo/models.py | 13 ++++-- dojo/validators.py | 36 +++++++++++++++ run-unittest.sh | 6 +-- unittests/test_rest_framework.py | 64 ++++++++------------------ unittests/tools/test_generic_parser.py | 2 + 6 files changed, 80 insertions(+), 53 deletions(-) create mode 100644 dojo/validators.py diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index 085a2c544ea..c1b740f2fe5 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -124,6 +124,7 @@ ) from dojo.user.utils import get_configuration_permissions_codenames from dojo.utils import is_scan_file_too_large, tag_validator +from dojo.validators import cvss3_validator logger = logging.getLogger(__name__) deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication") @@ -1799,6 +1800,8 @@ def validate(self, data): # doing it here instead of in update because update doesn't know if the value changed self.process_risk_acceptance(data) + cvss3_validator(data.get("cvssv3"), exception_class=RestFrameworkValidationError) + return data def validate_severity(self, value: str) -> str: @@ -1927,6 +1930,8 @@ def validate(self, data): msg = "Active findings cannot be risk accepted." raise serializers.ValidationError(msg) + cvss3_validator(data.get("cvssv3"), exception_class=RestFrameworkValidationError) + return data def validate_severity(self, value: str) -> str: @@ -1953,6 +1958,9 @@ class Meta: exclude = ("cve",) def create(self, validated_data): + cvss3_validator(validated_data.get("cvssv3"), exception_class=RestFrameworkValidationError) + + to_be_tagged, validated_data = self._pop_tags(validated_data) # Save vulnerability ids and pop them if "vulnerability_id_template_set" in validated_data: @@ -1974,9 +1982,13 @@ def create(self, validated_data): ) new_finding_template.save() + self._save_tags(new_finding_template, to_be_tagged) + return new_finding_template def update(self, instance, validated_data): + cvss3_validator(validated_data.get("cvssv3"), exception_class=RestFrameworkValidationError) + # Save vulnerability ids and pop them if "vulnerability_id_template_set" in validated_data: vulnerability_id_set = validated_data.pop( diff --git a/dojo/models.py b/dojo/models.py index caea5a53894..5279b4ce815 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -44,6 +44,8 @@ from tagulous.models import TagField from tagulous.models.managers import FakeTagRelatedManager +from dojo.validators import cvss3_validator # to avoid circular import + logger = logging.getLogger(__name__) deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication") @@ -2331,8 +2333,9 @@ class Finding(models.Model): verbose_name=_("EPSS percentile"), help_text=_("EPSS percentile for the CVE. Describes how many CVEs are scored at or below this one."), validators=[MinValueValidator(0.0), MaxValueValidator(1.0)]) - 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'") - cvssv3 = models.TextField(validators=[cvssv3_regex], + # 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'") + # cvssv3 = models.TextField(validators=[cvssv3_regex], + cvssv3 = models.TextField(validators=[cvss3_validator], max_length=117, null=True, verbose_name=_("CVSS v3"), @@ -3515,8 +3518,10 @@ class Finding_Template(models.Model): blank=False, verbose_name="Vulnerability Id", 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.") - 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'") - cvssv3 = models.TextField(validators=[cvssv3_regex], max_length=117, null=True) + # 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'") + # cvssv3 = models.TextField(validators=[cvssv3_regex], max_length=117, null=True) + cvssv3 = models.TextField(validators=[cvss3_validator], max_length=117, null=True) + severity = models.CharField(max_length=200, null=True, blank=True) description = models.TextField(null=True, blank=True) mitigation = models.TextField(null=True, blank=True) diff --git a/dojo/validators.py b/dojo/validators.py new file mode 100644 index 00000000000..d4d0a9d9b82 --- /dev/null +++ b/dojo/validators.py @@ -0,0 +1,36 @@ +import logging +from collections.abc import Callable + +import cvss.parser +from cvss import CVSS2, CVSS3, CVSS4 +from django.core.exceptions import ValidationError + +logger = logging.getLogger(__name__) + + +def cvss3_validator(value: str | list[str], exception_class: Callable = ValidationError) -> None: + logger.error("cvss3_validator called with value: %s", value) + cvss_vectors = cvss.parser.parse_cvss_from_text(value) + if len(cvss_vectors) > 0: + vector_obj = cvss_vectors[0] + + if isinstance(vector_obj, CVSS3): + # all is good + return + + if isinstance(vector_obj, CVSS4): + # 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 + msg = "Unsupported CVSS(4) version detected." + raise exception_class(msg) + if isinstance(vector_obj, CVSS2): + # 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 + msg = "Unsupported CVSS(2) version detected." + raise exception_class(msg) + + msg = "Unsupported CVSS version detected." + raise exception_class(msg) + + # Explicitly raise an error if no CVSS vectors are found, + # to avoid 'NoneType' errors during severity processing later. + msg = "No CVSS vectors found by cvss.parse_cvss_from_text()" + raise exception_class(msg) diff --git a/run-unittest.sh b/run-unittest.sh index 6aaa8c78cb4..32c657af947 100755 --- a/run-unittest.sh +++ b/run-unittest.sh @@ -51,7 +51,7 @@ then fi echo "Running docker compose unit tests with test case $TEST_CASE ..." -# Compose V2 integrates compose functions into the Docker platform, continuing to support -# most of the previous docker-compose features and flags. You can run Compose V2 by +# Compose V2 integrates compose functions into the Docker platform, continuing to support +# most of the previous docker-compose features and flags. You can run Compose V2 by # replacing the hyphen (-) with a space, using docker compose, instead of docker-compose. -docker compose exec uwsgi bash -c "python manage.py test $TEST_CASE -v2 --keepdb" +docker compose exec uwsgi bash -c "python manage.py test $TEST_CASE -v 3 --keepdb" --debug-mode diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index 0f04e7b7799..819660420cb 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -1282,68 +1282,40 @@ def test_severity_validation(self): self.assertEqual(result.json()["severity"], ["Severity must be one of the following: ['Info', 'Low', 'Medium', 'High', 'Critical']"]) # See https://github.com/DefectDojo/django-DefectDojo/issues/8264 - # Capturing current behavior which might not be the desired one yet def test_cvss3_validation(self): with self.subTest(i=0): self.assertEqual(None, Finding.objects.get(id=2).cvssv3) - 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", "cvssv3_score": 3}) + 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"}) self.assertEqual(result.status_code, status.HTTP_200_OK) - finding = Finding.objects.get(id=2) - self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", finding.cvssv3) - self.assertEqual(8.8, finding.cvssv3_score) + 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) with self.subTest(i=1): # extra slash makes it invalid - 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/", "cvssv3_score": 3}) - self.assertEqual(result.status_code, status.HTTP_200_OK) - finding = Finding.objects.get(id=3) - self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H/", finding.cvssv3) - self.assertEqual(3, finding.cvssv3_score) + 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/"}) + self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"]) + self.assertEqual(None, Finding.objects.get(id=3).cvssv3) with self.subTest(i=2): # no CVSS version prefix makes it invalid - 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", "cvssv3_score": 4}) - self.assertEqual(result.status_code, status.HTTP_200_OK) - finding = Finding.objects.get(id=3) - self.assertEqual("AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", finding.cvssv3) - # invalid vector, so no calculated score - self.assertEqual(4, finding.cvssv3_score) + 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"}) + self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"]) + self.assertEqual(None, Finding.objects.get(id=3).cvssv3) with self.subTest(i=3): # CVSS4 version makes it invalid - 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", "cvssv3_score": 5}) - self.assertEqual(result.status_code, status.HTTP_200_OK) - finding = Finding.objects.get(id=3) - self.assertEqual("CVSS:4.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", finding.cvssv3) - # invalid vector, so no calculated score - self.assertEqual(5, finding.cvssv3_score) + 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"}) + self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"]) + self.assertEqual(None, Finding.objects.get(id=3).cvssv3) with self.subTest(i=4): - # CVSS2 style vector makes not supported - result = self.client.patch(self.url + "3/", data={"cvssv3": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "cvssv3_score": 6}) - self.assertEqual(result.status_code, status.HTTP_200_OK) - finding = Finding.objects.get(id=3) - self.assertEqual("AV:N/AC:L/Au:N/C:P/I:P/A:P", finding.cvssv3) - # invalid vector, so no calculated score - self.assertEqual(6, finding.cvssv3_score) - - with self.subTest(i=5): - # CVSS2 prefix makes it invalid - result = self.client.patch(self.url + "3/", data={"cvssv3": "CVSS:2.0/AV:N/AC:L/Au:N/C:P/I:P/A:P", "cvssv3_score": 7}) - self.assertEqual(result.status_code, status.HTTP_200_OK) - finding = Finding.objects.get(id=3) - self.assertEqual("CVSS:2.0/AV:N/AC:L/Au:N/C:P/I:P/A:P", finding.cvssv3) - # invalid vector, so no calculated score - self.assertEqual(7, finding.cvssv3_score) - - with self.subTest(i=6): - # try to put rubbish in there - result = self.client.patch(self.url + "4/", data={"cvssv3": "happy little vector", "cvssv3_score": 3}) + # CVSS4 version makes it invalid + result = self.client.patch(self.url + "3/", data={"cvssv3": "CVSS:2.0/AV:N/AC:L/Au:N/C:P/I:P/A:P"}) self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) - finding = Finding.objects.get(id=4) - self.assertEqual(None, finding.cvssv3) - # invalid request, so no score at all - self.assertEqual(None, finding.cvssv3_score) + self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"]) + self.assertEqual(None, Finding.objects.get(id=3).cvssv3) class FindingMetadataTest(BaseClass.BaseClassTest): diff --git a/unittests/tools/test_generic_parser.py b/unittests/tools/test_generic_parser.py index 0b3d441e76a..9ea84e3db74 100644 --- a/unittests/tools/test_generic_parser.py +++ b/unittests/tools/test_generic_parser.py @@ -456,6 +456,8 @@ def test_parse_json(self): self.assertIn("network", finding.unsaved_tags) self.assertEqual("3287f2d0-554f-491b-8516-3c349ead8ee5", finding.unique_id_from_tool) self.assertEqual("TEST1", finding.vuln_id_from_tool) + finding.clean_fields(exclude=["reporter", "numerical_severity", "planned_remediation_date"]) + finding.save_no_options() with self.subTest(i=1): finding = findings[1] self.assertEqual("test title2", finding.title) From a171fe0280d2e7d46d4ad73fcda1b57484e10b87 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Tue, 13 May 2025 23:22:13 +0200 Subject: [PATCH 02/21] remove validator calls that are no longer needed --- dojo/api_v2/serializers.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index c1b740f2fe5..e60a70caa31 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -124,7 +124,6 @@ ) from dojo.user.utils import get_configuration_permissions_codenames from dojo.utils import is_scan_file_too_large, tag_validator -from dojo.validators import cvss3_validator logger = logging.getLogger(__name__) deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication") @@ -1800,7 +1799,7 @@ def validate(self, data): # doing it here instead of in update because update doesn't know if the value changed self.process_risk_acceptance(data) - cvss3_validator(data.get("cvssv3"), exception_class=RestFrameworkValidationError) + # cvss3_validator(data.get("cvssv3"), exception_class=RestFrameworkValidationError) return data @@ -1930,7 +1929,7 @@ def validate(self, data): msg = "Active findings cannot be risk accepted." raise serializers.ValidationError(msg) - cvss3_validator(data.get("cvssv3"), exception_class=RestFrameworkValidationError) + # cvss3_validator(data.get("cvssv3"), exception_class=RestFrameworkValidationError) return data @@ -1958,7 +1957,7 @@ class Meta: exclude = ("cve",) def create(self, validated_data): - cvss3_validator(validated_data.get("cvssv3"), exception_class=RestFrameworkValidationError) + # cvss3_validator(validated_data.get("cvssv3"), exception_class=RestFrameworkValidationError) to_be_tagged, validated_data = self._pop_tags(validated_data) @@ -1987,7 +1986,7 @@ def create(self, validated_data): return new_finding_template def update(self, instance, validated_data): - cvss3_validator(validated_data.get("cvssv3"), exception_class=RestFrameworkValidationError) + # cvss3_validator(validated_data.get("cvssv3"), exception_class=RestFrameworkValidationError) # Save vulnerability ids and pop them if "vulnerability_id_template_set" in validated_data: From 493077600bf8a4c0236dd46d487558e4ccf55bf5 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Tue, 13 May 2025 23:26:41 +0200 Subject: [PATCH 03/21] fix CVSS2.0 test case --- unittests/test_rest_framework.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index 819660420cb..0d38e16d0d8 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -1311,10 +1311,10 @@ def test_cvss3_validation(self): self.assertEqual(None, Finding.objects.get(id=3).cvssv3) with self.subTest(i=4): - # CVSS4 version makes it invalid - result = self.client.patch(self.url + "3/", data={"cvssv3": "CVSS:2.0/AV:N/AC:L/Au:N/C:P/I:P/A:P"}) + # CVSS2 style vector makes it invalid + result = self.client.patch(self.url + "3/", data={"cvssv3": "AV:N/AC:L/Au:N/C:P/I:P/A:P"}) self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"]) + self.assertEqual(result.json()["cvssv3"], ["Unsupported CVSS(2) version detected."]) self.assertEqual(None, Finding.objects.get(id=3).cvssv3) From 10a9a7d9173c2e5c07480bc22e6bc0795acde976 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Tue, 13 May 2025 23:26:58 +0200 Subject: [PATCH 04/21] fix CVSS2.0 test case --- dojo/validators.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dojo/validators.py b/dojo/validators.py index d4d0a9d9b82..f65a00de8f8 100644 --- a/dojo/validators.py +++ b/dojo/validators.py @@ -23,7 +23,6 @@ def cvss3_validator(value: str | list[str], exception_class: Callable = Validati msg = "Unsupported CVSS(4) version detected." raise exception_class(msg) if isinstance(vector_obj, CVSS2): - # 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 msg = "Unsupported CVSS(2) version detected." raise exception_class(msg) From 7d6b0011203b6e591664d47f2a5a4081d4ad9a1e Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Tue, 13 May 2025 23:29:39 +0200 Subject: [PATCH 05/21] fix CVSS2.0 test case --- unittests/test_rest_framework.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index 0d38e16d0d8..69a2babe9a0 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -1311,12 +1311,19 @@ def test_cvss3_validation(self): self.assertEqual(None, Finding.objects.get(id=3).cvssv3) with self.subTest(i=4): - # CVSS2 style vector makes it invalid + # CVSS2 style vector makes not supported result = self.client.patch(self.url + "3/", data={"cvssv3": "AV:N/AC:L/Au:N/C:P/I:P/A:P"}) self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(result.json()["cvssv3"], ["Unsupported CVSS(2) version detected."]) self.assertEqual(None, Finding.objects.get(id=3).cvssv3) + with self.subTest(i=5): + # CVSS2 prefix makes it invalid + result = self.client.patch(self.url + "3/", data={"cvssv3": "CVSS:2.0/AV:N/AC:L/Au:N/C:P/I:P/A:P"}) + self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"]) + self.assertEqual(None, Finding.objects.get(id=3).cvssv3) + class FindingMetadataTest(BaseClass.BaseClassTest): fixtures = ["dojo_testdata.json"] From 2fef3874961fdc2f3a6e3f2202115d5d87c42fed Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Wed, 14 May 2025 08:36:02 +0200 Subject: [PATCH 06/21] add migration --- ...ng_cvssv3_alter_finding_template_cvssv3.py | 24 +++++++++++++++++++ run-unittest.sh | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 dojo/db_migrations/0229_alter_finding_cvssv3_alter_finding_template_cvssv3.py diff --git a/dojo/db_migrations/0229_alter_finding_cvssv3_alter_finding_template_cvssv3.py b/dojo/db_migrations/0229_alter_finding_cvssv3_alter_finding_template_cvssv3.py new file mode 100644 index 00000000000..5b3ca12ede3 --- /dev/null +++ b/dojo/db_migrations/0229_alter_finding_cvssv3_alter_finding_template_cvssv3.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.8 on 2025-05-14 06:35 + +import dojo.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0228_alter_jira_username_password'), + ] + + operations = [ + migrations.AlterField( + model_name='finding', + name='cvssv3', + field=models.TextField(help_text='Common Vulnerability Scoring System version 3 (CVSSv3) score associated with this flaw.', max_length=117, null=True, validators=[dojo.validators.cvss3_validator], verbose_name='CVSS v3'), + ), + migrations.AlterField( + model_name='finding_template', + name='cvssv3', + field=models.TextField(max_length=117, null=True, validators=[dojo.validators.cvss3_validator]), + ), + ] diff --git a/run-unittest.sh b/run-unittest.sh index 32c657af947..e1956a981df 100755 --- a/run-unittest.sh +++ b/run-unittest.sh @@ -54,4 +54,4 @@ echo "Running docker compose unit tests with test case $TEST_CASE ..." # Compose V2 integrates compose functions into the Docker platform, continuing to support # most of the previous docker-compose features and flags. You can run Compose V2 by # replacing the hyphen (-) with a space, using docker compose, instead of docker-compose. -docker compose exec uwsgi bash -c "python manage.py test $TEST_CASE -v 3 --keepdb" --debug-mode +docker compose exec uwsgi bash -c "python manage.py test $TEST_CASE -v2 --keepdb" From dc696b1d785bbe5ac93304f9b0e8c212d702a474 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Wed, 14 May 2025 08:41:38 +0200 Subject: [PATCH 07/21] add migration --- ...29_alter_finding_cvssv3_alter_finding_template_cvssv3.py | 6 +++--- dojo/models.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dojo/db_migrations/0229_alter_finding_cvssv3_alter_finding_template_cvssv3.py b/dojo/db_migrations/0229_alter_finding_cvssv3_alter_finding_template_cvssv3.py index 5b3ca12ede3..0e51a069b34 100644 --- a/dojo/db_migrations/0229_alter_finding_cvssv3_alter_finding_template_cvssv3.py +++ b/dojo/db_migrations/0229_alter_finding_cvssv3_alter_finding_template_cvssv3.py @@ -14,11 +14,11 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='finding', name='cvssv3', - field=models.TextField(help_text='Common Vulnerability Scoring System version 3 (CVSSv3) score associated with this flaw.', max_length=117, null=True, validators=[dojo.validators.cvss3_validator], verbose_name='CVSS v3'), + 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'), ), migrations.AlterField( model_name='finding_template', name='cvssv3', - field=models.TextField(max_length=117, null=True, validators=[dojo.validators.cvss3_validator]), + 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'), ), - ] + ] \ No newline at end of file diff --git a/dojo/models.py b/dojo/models.py index 5279b4ce815..27dc41c26cb 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -2338,8 +2338,8 @@ class Finding(models.Model): cvssv3 = models.TextField(validators=[cvss3_validator], max_length=117, null=True, - verbose_name=_("CVSS v3"), - help_text=_("Common Vulnerability Scoring System version 3 (CVSSv3) score associated with this flaw.")) + verbose_name=_("CVSS v3 vector"), + help_text=_("Common Vulnerability Scoring System version 3 (CVSSv3) score associated with this finding.")) cvssv3_score = models.FloatField(null=True, blank=True, verbose_name=_("CVSSv3 score"), @@ -3520,7 +3520,7 @@ class Finding_Template(models.Model): 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.") # 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'") # cvssv3 = models.TextField(validators=[cvssv3_regex], max_length=117, null=True) - cvssv3 = models.TextField(validators=[cvss3_validator], max_length=117, null=True) + 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")) severity = models.CharField(max_length=200, null=True, blank=True) description = models.TextField(null=True, blank=True) From 797033601b19cc9b66d29addd297bac97739a3a6 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Wed, 14 May 2025 09:29:38 +0200 Subject: [PATCH 08/21] add TODO for parsers to check/fix --- dojo/fixtures/dojo_testdata.json | 1 + dojo/tools/aqua/parser.py | 4 ++-- dojo/tools/blackduck_binary_analysis/parser.py | 2 +- dojo/tools/cyberwatch_galeax/parser.py | 4 ++-- dojo/tools/jfrog_xray_unified/parser.py | 2 +- dojo/tools/npm_audit_7_plus/parser.py | 2 +- dojo/tools/qualys/csv_parser.py | 2 +- dojo/tools/qualys/parser.py | 2 +- dojo/tools/sonatype/parser.py | 2 +- dojo/tools/trivy/parser.py | 2 +- dojo/tools/veracode/json_parser.py | 2 +- 11 files changed, 13 insertions(+), 12 deletions(-) diff --git a/dojo/fixtures/dojo_testdata.json b/dojo/fixtures/dojo_testdata.json index b35d570eaab..350064e46f5 100644 --- a/dojo/fixtures/dojo_testdata.json +++ b/dojo/fixtures/dojo_testdata.json @@ -1146,6 +1146,7 @@ "pk": 2, "model": "dojo.finding", "fields": { + "cvssv3": "happy little vector", "last_reviewed_by": null, "reviewers": [], "static_finding": false, diff --git a/dojo/tools/aqua/parser.py b/dojo/tools/aqua/parser.py index e78d03086da..4447ca06c60 100644 --- a/dojo/tools/aqua/parser.py +++ b/dojo/tools/aqua/parser.py @@ -170,7 +170,7 @@ def get_item(self, resource, vuln, test): severity_justification += "\nAqua severity classification: {}".format(vuln.get("aqua_severity_classification")) severity_justification += "\nAqua scoring system: {}".format(vuln.get("aqua_scoring_system")) if "nvd_score_v3" in vuln: - cvssv3 = vuln.get("nvd_vectors_v3") + cvssv3 = vuln.get("nvd_vectors_v3") # TODO: VECTOR if "aqua_score" in vuln: if score is None: score = vuln.get("aqua_score") @@ -193,7 +193,7 @@ def get_item(self, resource, vuln, test): ) severity_justification += "\nNVD v3 vectors: {}".format(vuln.get("nvd_vectors_v3")) # Add the CVSS3 to Finding - cvssv3 = vuln.get("nvd_vectors_v3") + cvssv3 = vuln.get("nvd_vectors_v3") # TODO: VECTOR if "nvd_score" in vuln: if score is None: score = vuln.get("nvd_score") diff --git a/dojo/tools/blackduck_binary_analysis/parser.py b/dojo/tools/blackduck_binary_analysis/parser.py index b0ccd0b9642..f611e3415a9 100644 --- a/dojo/tools/blackduck_binary_analysis/parser.py +++ b/dojo/tools/blackduck_binary_analysis/parser.py @@ -43,7 +43,7 @@ def ingest_findings(self, sorted_findings, test): cwe = 1357 title = self.format_title(i) description = self.format_description(i) - cvss_v3 = True + cvss_v3 = True # TODO: VECTOR if str(i.cvss_vector_v3) != "": cvss_vectors = "{}{}".format( "CVSS:3.1/", diff --git a/dojo/tools/cyberwatch_galeax/parser.py b/dojo/tools/cyberwatch_galeax/parser.py index 02c0d6f85ea..804cfec2583 100644 --- a/dojo/tools/cyberwatch_galeax/parser.py +++ b/dojo/tools/cyberwatch_galeax/parser.py @@ -197,7 +197,7 @@ def build_findings_for_cve(self, cve_code, c_data, test): description = c_data["description"] impact = c_data["impact"] references = c_data["references"] - cvssv3 = c_data["cvssv3"] + cvssv3 = c_data["cvssv3"] # TODO: VECTOR cvssv3_score = c_data["cvssv3_score"] products = c_data["products"] @@ -515,7 +515,7 @@ def parse_cvss(self, cvss_v3_vector, json_data): if cvss_v3_vector: vectors = cvss.parser.parse_cvss_from_text(cvss_v3_vector) if vectors and isinstance(vectors[0], CVSS3): - cvssv3 = vectors[0].clean_vector() + cvssv3 = vectors[0].clean_vector() # TODO: VECTOR cvssv3_score = vectors[0].scores()[0] severity = vectors[0].severities()[0] return cvssv3, cvssv3_score, severity diff --git a/dojo/tools/jfrog_xray_unified/parser.py b/dojo/tools/jfrog_xray_unified/parser.py index 3f394cce345..71ab35fdbf6 100644 --- a/dojo/tools/jfrog_xray_unified/parser.py +++ b/dojo/tools/jfrog_xray_unified/parser.py @@ -75,7 +75,7 @@ def get_item(vulnerability, test): vulnerability_id = worstCve["cve"] if "cvss_v3_vector" in worstCve: cvss_v3 = worstCve["cvss_v3_vector"] - cvssv3 = cvss_v3 + cvssv3 = cvss_v3 # TODO: VECTOR if "cvss_v2_vector" in worstCve: cvss_v2 = worstCve["cvss_v2_vector"] diff --git a/dojo/tools/npm_audit_7_plus/parser.py b/dojo/tools/npm_audit_7_plus/parser.py index 38024110bc4..a536ae472a7 100644 --- a/dojo/tools/npm_audit_7_plus/parser.py +++ b/dojo/tools/npm_audit_7_plus/parser.py @@ -165,7 +165,7 @@ def get_item(item_node, tree, test): cwe = int(cwe.split("-")[1]) dojo_finding.cwe = cwe - if (cvssv3 is not None) and (len(cvssv3) > 0): + if (cvssv3 is not None) and (len(cvssv3) > 0): # TODO: VECTOR dojo_finding.cvssv3 = cvssv3 return dojo_finding diff --git a/dojo/tools/qualys/csv_parser.py b/dojo/tools/qualys/csv_parser.py index 9fd19bc46d6..38ba632e4e7 100644 --- a/dojo/tools/qualys/csv_parser.py +++ b/dojo/tools/qualys/csv_parser.py @@ -197,7 +197,7 @@ def build_findings_from_dict(report_findings: [dict]) -> [Finding]: # Clean up the CVE data appropriately cve_list = _clean_cve_data(cve_data) - if "CVSS3 Base" in report_finding: + if "CVSS3 Base" in report_finding: # TODO: VECTOR cvssv3 = _extract_cvss_vectors( report_finding["CVSS3 Base"], report_finding["CVSS3 Temporal"], ) diff --git a/dojo/tools/qualys/parser.py b/dojo/tools/qualys/parser.py index 03b6c301710..e0fe1936be7 100644 --- a/dojo/tools/qualys/parser.py +++ b/dojo/tools/qualys/parser.py @@ -352,7 +352,7 @@ def parse_finding(host, tree): finding.is_mitigated = temp["mitigated"] finding.active = temp["active"] if temp.get("CVSS_vector") is not None: - finding.cvssv3 = temp.get("CVSS_vector") + finding.cvssv3 = temp.get("CVSS_vector") # TODO: VECTOR if temp.get("CVSS_value") is not None: finding.cvssv3_score = temp.get("CVSS_value") finding.verified = True diff --git a/dojo/tools/sonatype/parser.py b/dojo/tools/sonatype/parser.py index b82f1937c77..2a9a00d4bdb 100644 --- a/dojo/tools/sonatype/parser.py +++ b/dojo/tools/sonatype/parser.py @@ -63,7 +63,7 @@ def get_finding(security_issue, component, test): finding.cwe = security_issue["cwe"] if "cvssVector" in security_issue: - finding.cvssv3 = security_issue["cvssVector"] + finding.cvssv3 = security_issue["cvssVector"] # TODO: VECTOR if "pathnames" in component: finding.file_path = " ".join(component["pathnames"])[:1000] diff --git a/dojo/tools/trivy/parser.py b/dojo/tools/trivy/parser.py index fbe87bd64cd..275697a1745 100644 --- a/dojo/tools/trivy/parser.py +++ b/dojo/tools/trivy/parser.py @@ -171,7 +171,7 @@ def get_result_items(self, test, results, service_name=None, artifact_name=""): if cvssclass is not None: if cvssclass.get("V3Score") is not None: severity = self.convert_cvss_score(cvssclass.get("V3Score")) - cvssv3 = dict(cvssclass).get("V3Vector") + cvssv3 = dict(cvssclass).get("V3Vector") # TODO: VECTOR elif cvssclass.get("V2Score") is not None: severity = self.convert_cvss_score(cvssclass.get("V2Score")) else: diff --git a/dojo/tools/veracode/json_parser.py b/dojo/tools/veracode/json_parser.py index 2650d5af02b..e7e16fb695c 100644 --- a/dojo/tools/veracode/json_parser.py +++ b/dojo/tools/veracode/json_parser.py @@ -126,7 +126,7 @@ def create_finding_from_details(self, finding_details, scan_type, policy_violate if uncleaned_cvss := finding_details.get("cvss"): if isinstance(uncleaned_cvss, str): if uncleaned_cvss.startswith(("CVSS:3.1/", "CVSS:3.0/")): - finding.cvssv3 = CVSS3(str(uncleaned_cvss)).clean_vector(output_prefix=True) + finding.cvssv3 = CVSS3(str(uncleaned_cvss)).clean_vector(output_prefix=True) # TODO: VECTOR elif not uncleaned_cvss.startswith("CVSS"): finding.cvssv3 = CVSS3(f"CVSS:3.1/{uncleaned_cvss}").clean_vector(output_prefix=True) elif isinstance(uncleaned_cvss, float | int): From c0005808f646271d2b711d1366578744c79b5696 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Wed, 14 May 2025 10:38:39 +0200 Subject: [PATCH 09/21] revert testdata change --- dojo/fixtures/dojo_testdata.json | 1 - 1 file changed, 1 deletion(-) diff --git a/dojo/fixtures/dojo_testdata.json b/dojo/fixtures/dojo_testdata.json index 350064e46f5..b35d570eaab 100644 --- a/dojo/fixtures/dojo_testdata.json +++ b/dojo/fixtures/dojo_testdata.json @@ -1146,7 +1146,6 @@ "pk": 2, "model": "dojo.finding", "fields": { - "cvssv3": "happy little vector", "last_reviewed_by": null, "reviewers": [], "static_finding": false, From 6ca84f19a3bf09bb72f67f73d04517b2ddba89be Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Thu, 15 May 2025 20:07:06 +0200 Subject: [PATCH 10/21] cleanup --- unittests/tools/test_generic_parser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/unittests/tools/test_generic_parser.py b/unittests/tools/test_generic_parser.py index 9ea84e3db74..b6b83da6719 100644 --- a/unittests/tools/test_generic_parser.py +++ b/unittests/tools/test_generic_parser.py @@ -456,8 +456,7 @@ def test_parse_json(self): self.assertIn("network", finding.unsaved_tags) self.assertEqual("3287f2d0-554f-491b-8516-3c349ead8ee5", finding.unique_id_from_tool) self.assertEqual("TEST1", finding.vuln_id_from_tool) - finding.clean_fields(exclude=["reporter", "numerical_severity", "planned_remediation_date"]) - finding.save_no_options() + # finding.clean_fields(exclude=["reporter", "numerical_severity", "planned_remediation_date"]) with self.subTest(i=1): finding = findings[1] self.assertEqual("test title2", finding.title) From dc9a2b3467f8c8690643e740758b6cd337036e3b Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Fri, 16 May 2025 17:41:31 +0200 Subject: [PATCH 11/21] fix tests, cleanup --- dojo/api_v2/serializers.py | 10 +---- dojo/forms.py | 2 +- dojo/utils.py | 18 -------- dojo/validators.py | 20 ++++++++- tests/finding_test.py | 52 +++++++++++++++++------ unittests/test_finding_model.py | 1 + unittests/test_rest_framework.py | 71 ++++++++++++++++++++++++-------- 7 files changed, 117 insertions(+), 57 deletions(-) diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index e60a70caa31..bba67e35136 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -123,7 +123,8 @@ requires_tool_type, ) from dojo.user.utils import get_configuration_permissions_codenames -from dojo.utils import is_scan_file_too_large, tag_validator +from dojo.utils import is_scan_file_too_large +from dojo.validators import tag_validator logger = logging.getLogger(__name__) deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication") @@ -1799,8 +1800,6 @@ def validate(self, data): # doing it here instead of in update because update doesn't know if the value changed self.process_risk_acceptance(data) - # cvss3_validator(data.get("cvssv3"), exception_class=RestFrameworkValidationError) - return data def validate_severity(self, value: str) -> str: @@ -1929,8 +1928,6 @@ def validate(self, data): msg = "Active findings cannot be risk accepted." raise serializers.ValidationError(msg) - # cvss3_validator(data.get("cvssv3"), exception_class=RestFrameworkValidationError) - return data def validate_severity(self, value: str) -> str: @@ -1957,7 +1954,6 @@ class Meta: exclude = ("cve",) def create(self, validated_data): - # cvss3_validator(validated_data.get("cvssv3"), exception_class=RestFrameworkValidationError) to_be_tagged, validated_data = self._pop_tags(validated_data) @@ -1986,8 +1982,6 @@ def create(self, validated_data): return new_finding_template def update(self, instance, validated_data): - # cvss3_validator(validated_data.get("cvssv3"), exception_class=RestFrameworkValidationError) - # Save vulnerability ids and pop them if "vulnerability_id_template_set" in validated_data: vulnerability_id_set = validated_data.pop( diff --git a/dojo/forms.py b/dojo/forms.py index e76a1987ee1..19f1fe6962d 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -110,8 +110,8 @@ get_system_setting, is_finding_groups_enabled, is_scan_file_too_large, - tag_validator, ) +from dojo.validators import tag_validator from dojo.widgets import TableCheckboxWidget logger = logging.getLogger(__name__) diff --git a/dojo/utils.py b/dojo/utils.py index 81f282c2c09..729ba51cb42 100644 --- a/dojo/utils.py +++ b/dojo/utils.py @@ -25,7 +25,6 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed -from django.core.exceptions import ValidationError from django.core.paginator import Paginator from django.db.models import Case, Count, IntegerField, Q, Sum, Value, When from django.db.models.query import QuerySet @@ -2655,20 +2654,3 @@ def generate_file_response_from_file_path( response["Content-Disposition"] = f'attachment; filename="{full_file_name}"' response["Content-Length"] = file_size return response - - -def tag_validator(value: str | list[str], exception_class: Callable = ValidationError) -> None: - TAG_PATTERN = re.compile(r'[ ,\'"]') - error_messages = [] - - if isinstance(value, list): - error_messages.extend(f"Invalid tag: '{tag}'. Tags should not contain spaces, commas, or quotes." for tag in value if TAG_PATTERN.search(tag)) - elif isinstance(value, str): - if TAG_PATTERN.search(value): - error_messages.append(f"Invalid tag: '{value}'. Tags should not contain spaces, commas, or quotes.") - else: - error_messages.append(f"Value must be a string or list of strings: {value} - {type(value)}.") - - if error_messages: - logger.debug(f"Tag validation failed: {error_messages}") - raise exception_class(error_messages) diff --git a/dojo/validators.py b/dojo/validators.py index f65a00de8f8..e6e3a784b2b 100644 --- a/dojo/validators.py +++ b/dojo/validators.py @@ -1,4 +1,5 @@ import logging +import re from collections.abc import Callable import cvss.parser @@ -8,6 +9,23 @@ logger = logging.getLogger(__name__) +def tag_validator(value: str | list[str], exception_class: Callable = ValidationError) -> None: + TAG_PATTERN = re.compile(r'[ ,\'"]') + error_messages = [] + + if isinstance(value, list): + error_messages.extend(f"Invalid tag: '{tag}'. Tags should not contain spaces, commas, or quotes." for tag in value if TAG_PATTERN.search(tag)) + elif isinstance(value, str): + if TAG_PATTERN.search(value): + error_messages.append(f"Invalid tag: '{value}'. Tags should not contain spaces, commas, or quotes.") + else: + error_messages.append(f"Value must be a string or list of strings: {value} - {type(value)}.") + + if error_messages: + logger.debug(f"Tag validation failed: {error_messages}") + raise exception_class(error_messages) + + def cvss3_validator(value: str | list[str], exception_class: Callable = ValidationError) -> None: logger.error("cvss3_validator called with value: %s", value) cvss_vectors = cvss.parser.parse_cvss_from_text(value) @@ -31,5 +49,5 @@ def cvss3_validator(value: str | list[str], exception_class: Callable = Validati # Explicitly raise an error if no CVSS vectors are found, # to avoid 'NoneType' errors during severity processing later. - msg = "No CVSS vectors found by cvss.parse_cvss_from_text()" + msg = "No valid CVSS vectors found by cvss.parse_cvss_from_text()" raise exception_class(msg) diff --git a/tests/finding_test.py b/tests/finding_test.py index 0f7ec62a5dc..d55c269333e 100644 --- a/tests/finding_test.py +++ b/tests/finding_test.py @@ -112,7 +112,6 @@ def test_edit_finding(self): driver.find_element(By.ID, "dropdownMenu1").click() # Click on `Edit Finding` driver.find_element(By.LINK_TEXT, "Edit Finding").click() - # Change: 'Severity' and 'cvssv3' # finding Severity Select(driver.find_element(By.ID, "id_severity")).select_by_visible_text("Critical") # finding Vulnerability Ids @@ -165,6 +164,8 @@ def _edit_finding_cvssv3_and_assert( self.assertEqual(str(expected_cvssv3_score), driver.find_element(By.ID, "id_cvssv3_score").get_attribute("value")) else: self.assertTrue(self.is_error_message_present(text=error_message)) + self.assertEqual(expected_cvssv3_value, driver.find_element(By.ID, "id_cvssv3").get_attribute("value")) + self.assertEqual(str(expected_cvssv3_score), driver.find_element(By.ID, "id_cvssv3_score").get_attribute("value")) # See https://github.com/DefectDojo/django-DefectDojo/issues/8264 # Capturing current behavior which might not be the desired one yet @@ -184,8 +185,9 @@ def test_edit_finding_cvssv3_valid_vector_no_prefix(self): cvssv3_value="AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", cvssv3_score="2", expected_cvssv3_value="AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", - expected_cvssv3_score="2.0", - expect_success=True, + expected_cvssv3_score="2", + expect_success=False, + error_message="No valid CVSS vectors found by cvss.parse_cvss_from_text()", ) @on_exception_html_source_logger @@ -194,29 +196,53 @@ def test_edit_finding_cvssv3_valid_vector_with_trailing_slash(self): cvssv3_value="CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H/", cvssv3_score="3", expected_cvssv3_value="CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H/", - expected_cvssv3_score="3.0", - expect_success=True, + expected_cvssv3_score="3", + expect_success=False, + error_message="No valid CVSS vectors found by cvss.parse_cvss_from_text()", ) @on_exception_html_source_logger - def test_edit_finding_cvssv3_with_v2_vector(self): + def test_edit_finding_cvssv3_with_v2_vector_invalid_due_to_prefix(self): self._edit_finding_cvssv3_and_assert( cvssv3_value="CVSS:2.0/AV:N/AC:L/Au:N/C:P/I:P/A:P", cvssv3_score="4", expected_cvssv3_value="CVSS:2.0/AV:N/AC:L/Au:N/C:P/I:P/A:P", - expected_cvssv3_score="4.0", - expect_success=True, + expected_cvssv3_score="4", + expect_success=False, + error_message="No valid CVSS vectors found by cvss.parse_cvss_from_text()", + ) + + @on_exception_html_source_logger + def test_edit_finding_cvssv3_with_v2_vector(self): + self._edit_finding_cvssv3_and_assert( + cvssv3_value="AV:N/AC:L/Au:N/C:P/I:P/A:P", + cvssv3_score="4", + expected_cvssv3_value="AV:N/AC:L/Au:N/C:P/I:P/A:P", + expected_cvssv3_score="4", + expect_success=False, + error_message="Unsupported CVSS(2) version detected.", + ) + + @on_exception_html_source_logger + def test_edit_finding_cvssv3_with_v4_vector(self): + self._edit_finding_cvssv3_and_assert( + cvssv3_value="CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/S:U/C:H/I:H/A:H", + cvssv3_score="5", + expected_cvssv3_value="CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/S:U/C:H/I:H/A:H", + expected_cvssv3_score="5", + expect_success=False, + error_message="No valid CVSS vectors found by cvss.parse_cvss_from_text()", ) @on_exception_html_source_logger def test_edit_finding_cvssv3_with_rubbish(self): self._edit_finding_cvssv3_and_assert( cvssv3_value="happy little vector", - cvssv3_score="4", - expected_cvssv3_value=None, - expected_cvssv3_score=None, + cvssv3_score="5", + expected_cvssv3_value="happy little vector", + expected_cvssv3_score="5", expect_success=False, - error_message="CVSS must be entered in format: 'AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H'", + error_message="No valid CVSS vectors found by cvss.parse_cvss_from_text()", ) def test_add_image(self): @@ -654,6 +680,8 @@ def add_finding_tests_to_suite(suite, *, jira=False, github=False, block_executi suite.addTest(FindingTest("test_edit_finding_cvssv3_valid_vector_no_prefix")) suite.addTest(FindingTest("test_edit_finding_cvssv3_valid_vector_with_trailing_slash")) suite.addTest(FindingTest("test_edit_finding_cvssv3_with_v2_vector")) + suite.addTest(FindingTest("test_edit_finding_cvssv3_with_v2_vector_invalid_due_to_prefix")) + suite.addTest(FindingTest("test_edit_finding_cvssv3_with_v4_vector")) suite.addTest(FindingTest("test_edit_finding_cvssv3_with_rubbish")) suite.addTest(FindingTest("test_add_note_to_finding")) suite.addTest(FindingTest("test_add_image")) diff --git a/unittests/test_finding_model.py b/unittests/test_finding_model.py index aa6971accb0..bc65ffd1096 100644 --- a/unittests/test_finding_model.py +++ b/unittests/test_finding_model.py @@ -318,6 +318,7 @@ def test_get_references_with_links_markdown(self): # See https://github.com/DefectDojo/django-DefectDojo/issues/8264 # Capturing current behavior which might not be the desired one yet + # This test saves vectors without any validation. This is capturing current behavior. def test_cvssv3(self): """Tests if the CVSSv3 score is calculated correctly""" user, _ = User.objects.get_or_create(username="admin") diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index 69a2babe9a0..3cf133f0fe6 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -1281,48 +1281,85 @@ def test_severity_validation(self): self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST, "Severity just got set to something invalid") self.assertEqual(result.json()["severity"], ["Severity must be one of the following: ['Info', 'Low', 'Medium', 'High', 'Critical']"]) - # See https://github.com/DefectDojo/django-DefectDojo/issues/8264 def test_cvss3_validation(self): with self.subTest(i=0): self.assertEqual(None, Finding.objects.get(id=2).cvssv3) - 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"}) + 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", "cvssv3_score": 3}) self.assertEqual(result.status_code, status.HTTP_200_OK) - 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) + finding = Finding.objects.get(id=2) + # valid so vector must be set and score calculated + self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", finding.cvssv3) + self.assertEqual(8.8, finding.cvssv3_score) with self.subTest(i=1): # extra slash makes it invalid - 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/"}) + 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/", "cvssv3_score": 3}) self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"]) - self.assertEqual(None, Finding.objects.get(id=3).cvssv3) + finding = Finding.objects.get(id=3) + self.assertEqual(result.json()["cvssv3"], ["No valid CVSS vectors found by cvss.parse_cvss_from_text()"]) + # invalid vector, so no calculated score and no score stored + self.assertEqual(None, finding.cvssv3) + self.assertEqual(None, finding.cvssv3_score) with self.subTest(i=2): # no CVSS version prefix makes it invalid - 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"}) + 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", "cvssv3_score": 4}) self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"]) - self.assertEqual(None, Finding.objects.get(id=3).cvssv3) + finding = Finding.objects.get(id=3) + self.assertEqual(result.json()["cvssv3"], ["No valid CVSS vectors found by cvss.parse_cvss_from_text()"]) + # invalid vector, so no calculated score and no score stored + self.assertEqual(None, finding.cvssv3) + self.assertEqual(None, finding.cvssv3_score) with self.subTest(i=3): # CVSS4 version makes it invalid - 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"}) + 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", "cvssv3_score": 5}) self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"]) - self.assertEqual(None, Finding.objects.get(id=3).cvssv3) + self.assertEqual(result.json()["cvssv3"], ["No valid CVSS vectors found by cvss.parse_cvss_from_text()"]) + finding = Finding.objects.get(id=3) + # invalid vector, so no calculated score and no score stored + self.assertEqual(None, finding.cvssv3) + self.assertEqual(None, finding.cvssv3_score) with self.subTest(i=4): # CVSS2 style vector makes not supported - result = self.client.patch(self.url + "3/", data={"cvssv3": "AV:N/AC:L/Au:N/C:P/I:P/A:P"}) + result = self.client.patch(self.url + "3/", data={"cvssv3": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "cvssv3_score": 6}) self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(result.json()["cvssv3"], ["Unsupported CVSS(2) version detected."]) - self.assertEqual(None, Finding.objects.get(id=3).cvssv3) + finding = Finding.objects.get(id=3) + # invalid vector, so no calculated score and no score stored + self.assertEqual(None, finding.cvssv3) + self.assertEqual(None, finding.cvssv3_score) with self.subTest(i=5): # CVSS2 prefix makes it invalid - result = self.client.patch(self.url + "3/", data={"cvssv3": "CVSS:2.0/AV:N/AC:L/Au:N/C:P/I:P/A:P"}) + result = self.client.patch(self.url + "3/", data={"cvssv3": "CVSS:2.0/AV:N/AC:L/Au:N/C:P/I:P/A:P", "cvssv3_score": 7}) self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(result.json()["cvssv3"], ["No CVSS vectors found by cvss.parse_cvss_from_text()"]) - self.assertEqual(None, Finding.objects.get(id=3).cvssv3) + self.assertEqual(result.json()["cvssv3"], ["No valid CVSS vectors found by cvss.parse_cvss_from_text()"]) + finding = Finding.objects.get(id=3) + # invalid vector, so no calculated score and no score stored + self.assertEqual(None, finding.cvssv3) + self.assertEqual(None, finding.cvssv3_score) + + with self.subTest(i=6): + # try to put rubbish in there + result = self.client.patch(self.url + "4/", data={"cvssv3": "happy little vector", "cvssv3_score": 3}) + self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(result.json()["cvssv3"], ["No valid CVSS vectors found by cvss.parse_cvss_from_text()"]) + finding = Finding.objects.get(id=4) + # invalid vector, so no calculated score and no score stored + self.assertEqual(None, finding.cvssv3) + self.assertEqual(None, finding.cvssv3_score) + + with self.subTest(i=7): + # CVSS4 prefix makes it invalid + result = self.client.patch(self.url + "3/", data={"cvssv3": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/S:U/C:H/I:H/A:H", "cvssv3_score": 7}) + self.assertEqual(result.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(result.json()["cvssv3"], ["No valid CVSS vectors found by cvss.parse_cvss_from_text()"]) + finding = Finding.objects.get(id=3) + # invalid vector, so no calculated score and no score stored + self.assertEqual(None, finding.cvssv3) + self.assertEqual(None, finding.cvssv3_score) class FindingMetadataTest(BaseClass.BaseClassTest): From 30e5775827c950c1a7a6c20e30f584ac9cdfc675 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Fri, 16 May 2025 21:28:37 +0200 Subject: [PATCH 12/21] update parsers that didn't parse cvss correctly yet --- .../contributing/how-to-write-a-parser.md | 27 +++++++++---------- dojo/models.py | 13 ++++----- dojo/tools/aqua/parser.py | 13 ++++++--- .../tools/blackduck_binary_analysis/parser.py | 2 +- dojo/tools/cyberwatch_galeax/parser.py | 4 +-- dojo/tools/jfrog_xray_unified/parser.py | 9 +++++-- dojo/tools/npm_audit_7_plus/parser.py | 8 ++++-- dojo/tools/qualys/csv_parser.py | 10 +++++-- dojo/tools/qualys/parser.py | 7 ++++- dojo/tools/sonatype/parser.py | 6 ++++- dojo/tools/trivy/parser.py | 10 ++++++- dojo/tools/veracode/json_parser.py | 2 +- dojo/utils.py | 17 ++++++++++++ 13 files changed, 90 insertions(+), 38 deletions(-) diff --git a/docs/content/en/open_source/contributing/how-to-write-a-parser.md b/docs/content/en/open_source/contributing/how-to-write-a-parser.md index 3cb4f031d84..6bba47a9cd5 100644 --- a/docs/content/en/open_source/contributing/how-to-write-a-parser.md +++ b/docs/content/en/open_source/contributing/how-to-write-a-parser.md @@ -169,14 +169,22 @@ Good example: ### Do not parse CVSS by hand (vector, score or severity) Data can have `CVSS` vectors or scores. Don't write your own CVSS score algorithm. -For parser, we rely on module `cvss`. +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. -It's easy to use and will make the parser aligned with the rest of the code. +```python +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") +if cvss_data: + finding.cvss3 = cvss_data.get("vector") + finding.cvssv3_score = cvss_data.get("score") + finding.severity = cvss_data.get("severity") # if your tool does generate severity +``` + +If you need more manual processing, you can parse the `CVSS` vector directly. Example of use: ```python -from cvss.cvss3 import CVSS3 +from dojo.utils import cvss.cvss3 import CVSS3 import cvss.parser 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") if len(vectors) > 0 and type(vectors[0]) is CVSS3: @@ -186,17 +194,8 @@ if len(vectors) > 0 and type(vectors[0]) is CVSS3: severity = vectors[0].severities()[0] vectors[0].compute_base_score() cvssv3_score = vectors[0].scores()[0] - print(severity) - print(cvssv3_score) -``` - -Good example: - -```python -vectors = cvss.parser.parse_cvss_from_text(item['cvss_vect']) -if len(vectors) > 0 and type(vectors[0]) is CVSS3: - finding.cvss = vectors[0].clean_vector() - finding.severity = vectors[0].severities()[0] # if your tool does generate severity + finding.severity = severity + finding.cvssv3_score = cvssv3_score ``` Bad example (DIY): diff --git a/dojo/models.py b/dojo/models.py index 27dc41c26cb..36b94efbcd7 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -10,11 +10,11 @@ from pathlib import Path from uuid import uuid4 +import cvss.parser import dateutil import hyperlink import tagulous.admin from auditlog.registry import auditlog -from cvss import CVSS3 from dateutil.relativedelta import relativedelta from django import forms from django.conf import settings @@ -2333,8 +2333,6 @@ class Finding(models.Model): verbose_name=_("EPSS percentile"), help_text=_("EPSS percentile for the CVE. Describes how many CVEs are scored at or below this one."), validators=[MinValueValidator(0.0), MaxValueValidator(1.0)]) - # 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'") - # cvssv3 = models.TextField(validators=[cvssv3_regex], cvssv3 = models.TextField(validators=[cvss3_validator], max_length=117, null=True, @@ -2701,11 +2699,12 @@ def save(self, dedupe_option=True, rules_option=True, product_grading_option=Tru # Synchronize cvssv3 score using cvssv3 vector if self.cvssv3: try: - cvss_object = CVSS3(self.cvssv3) + cvss_vector = cvss.parser.parse_cvss_from_text(self.cvssv3) # use the environmental score, which is the most refined score - self.cvssv3_score = cvss_object.scores()[2] + self.cvssv3_score = cvss_vector.scores()[2] except Exception as ex: - logger.error("Can't compute cvssv3 score for finding id %i. Invalid cvssv3 vector found: '%s'. Exception: %s", self.id, self.cvssv3, ex) + logger.warning("Can't compute cvssv3 score for finding id %i. Invalid cvssv3 vector found: '%s'. Exception: %s.", self.id, self.cvssv3, ex) + # should we set self.cvssv3 to None here to avoid storing invalid vectors? it would also remove invalid vectors on existing findings... self.set_hash_code(dedupe_option) @@ -3518,8 +3517,6 @@ class Finding_Template(models.Model): blank=False, verbose_name="Vulnerability Id", 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.") - # 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'") - # cvssv3 = models.TextField(validators=[cvssv3_regex], max_length=117, null=True) 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")) severity = models.CharField(max_length=200, null=True, blank=True) diff --git a/dojo/tools/aqua/parser.py b/dojo/tools/aqua/parser.py index 4447ca06c60..ae464c67fe7 100644 --- a/dojo/tools/aqua/parser.py +++ b/dojo/tools/aqua/parser.py @@ -1,6 +1,7 @@ import json from dojo.models import Finding +from dojo.utils import parse_cvss_data class AquaParser: @@ -170,7 +171,7 @@ def get_item(self, resource, vuln, test): severity_justification += "\nAqua severity classification: {}".format(vuln.get("aqua_severity_classification")) severity_justification += "\nAqua scoring system: {}".format(vuln.get("aqua_scoring_system")) if "nvd_score_v3" in vuln: - cvssv3 = vuln.get("nvd_vectors_v3") # TODO: VECTOR + cvssv3 = vuln.get("nvd_vectors_v3") if "aqua_score" in vuln: if score is None: score = vuln.get("aqua_score") @@ -193,7 +194,7 @@ def get_item(self, resource, vuln, test): ) severity_justification += "\nNVD v3 vectors: {}".format(vuln.get("nvd_vectors_v3")) # Add the CVSS3 to Finding - cvssv3 = vuln.get("nvd_vectors_v3") # TODO: VECTOR + cvssv3 = vuln.get("nvd_vectors_v3") if "nvd_score" in vuln: if score is None: score = vuln.get("nvd_score") @@ -201,6 +202,7 @@ def get_item(self, resource, vuln, test): f"NVD score v2 ({score}) used for classification.\n" ) severity_justification += "\nNVD v2 vectors: {}".format(vuln.get("nvd_vectors")) + severity_justification += f"\n{used_for_classification}" severity = self.severity_of(score) finding = Finding( @@ -214,7 +216,6 @@ def get_item(self, resource, vuln, test): severity=severity, severity_justification=severity_justification, cwe=0, - cvssv3=cvssv3, description=description.strip(), mitigation=fix_version, references=url, @@ -222,6 +223,12 @@ def get_item(self, resource, vuln, test): component_version=resource.get("version"), impact=severity, ) + + cvss_data = parse_cvss_data(cvssv3) + if cvss_data: + finding.cvss3 = cvss_data.get("vector") + finding.cvssv3_score = cvss_data.get("score") + if vulnerability_id != "No CVE": finding.unsaved_vulnerability_ids = [vulnerability_id] if vuln.get("epss_score"): diff --git a/dojo/tools/blackduck_binary_analysis/parser.py b/dojo/tools/blackduck_binary_analysis/parser.py index f611e3415a9..b0ccd0b9642 100644 --- a/dojo/tools/blackduck_binary_analysis/parser.py +++ b/dojo/tools/blackduck_binary_analysis/parser.py @@ -43,7 +43,7 @@ def ingest_findings(self, sorted_findings, test): cwe = 1357 title = self.format_title(i) description = self.format_description(i) - cvss_v3 = True # TODO: VECTOR + cvss_v3 = True if str(i.cvss_vector_v3) != "": cvss_vectors = "{}{}".format( "CVSS:3.1/", diff --git a/dojo/tools/cyberwatch_galeax/parser.py b/dojo/tools/cyberwatch_galeax/parser.py index 804cfec2583..02c0d6f85ea 100644 --- a/dojo/tools/cyberwatch_galeax/parser.py +++ b/dojo/tools/cyberwatch_galeax/parser.py @@ -197,7 +197,7 @@ def build_findings_for_cve(self, cve_code, c_data, test): description = c_data["description"] impact = c_data["impact"] references = c_data["references"] - cvssv3 = c_data["cvssv3"] # TODO: VECTOR + cvssv3 = c_data["cvssv3"] cvssv3_score = c_data["cvssv3_score"] products = c_data["products"] @@ -515,7 +515,7 @@ def parse_cvss(self, cvss_v3_vector, json_data): if cvss_v3_vector: vectors = cvss.parser.parse_cvss_from_text(cvss_v3_vector) if vectors and isinstance(vectors[0], CVSS3): - cvssv3 = vectors[0].clean_vector() # TODO: VECTOR + cvssv3 = vectors[0].clean_vector() cvssv3_score = vectors[0].scores()[0] severity = vectors[0].severities()[0] return cvssv3, cvssv3_score, severity diff --git a/dojo/tools/jfrog_xray_unified/parser.py b/dojo/tools/jfrog_xray_unified/parser.py index 71ab35fdbf6..19b30a7dcd3 100644 --- a/dojo/tools/jfrog_xray_unified/parser.py +++ b/dojo/tools/jfrog_xray_unified/parser.py @@ -2,6 +2,7 @@ from datetime import datetime from dojo.models import Finding +from dojo.utils import parse_cvss_data class JFrogXrayUnifiedParser: @@ -75,7 +76,7 @@ def get_item(vulnerability, test): vulnerability_id = worstCve["cve"] if "cvss_v3_vector" in worstCve: cvss_v3 = worstCve["cvss_v3_vector"] - cvssv3 = cvss_v3 # TODO: VECTOR + cvssv3 = cvss_v3 if "cvss_v2_vector" in worstCve: cvss_v2 = worstCve["cvss_v2_vector"] @@ -134,12 +135,16 @@ def get_item(vulnerability, test): dynamic_finding=False, references=references, impact=severity, - cvssv3=cvssv3, date=scan_time, unique_id_from_tool=vulnerability["issue_id"], tags=tags, ) + cvss_data = parse_cvss_data(cvssv3) + if cvss_data: + finding.cvss3 = cvss_data.get("vector") + finding.cvssv3_score = cvss_data.get("score") + if vulnerability_id: finding.unsaved_vulnerability_ids = [vulnerability_id] diff --git a/dojo/tools/npm_audit_7_plus/parser.py b/dojo/tools/npm_audit_7_plus/parser.py index a536ae472a7..71d2325e316 100644 --- a/dojo/tools/npm_audit_7_plus/parser.py +++ b/dojo/tools/npm_audit_7_plus/parser.py @@ -165,8 +165,12 @@ def get_item(item_node, tree, test): cwe = int(cwe.split("-")[1]) dojo_finding.cwe = cwe - if (cvssv3 is not None) and (len(cvssv3) > 0): # TODO: VECTOR - dojo_finding.cvssv3 = cvssv3 + if (cvssv3 is not None) and (len(cvssv3) > 0): + from dojo.utils import parse_cvss_data + cvss_data = parse_cvss_data(cvssv3) + if cvss_data: + dojo_finding.cvss3 = cvss_data.get("vector") + dojo_finding.cvssv3_score = cvss_data.get("score") return dojo_finding diff --git a/dojo/tools/qualys/csv_parser.py b/dojo/tools/qualys/csv_parser.py index 38ba632e4e7..fea1ed0327a 100644 --- a/dojo/tools/qualys/csv_parser.py +++ b/dojo/tools/qualys/csv_parser.py @@ -8,6 +8,7 @@ from django.conf import settings from dojo.models import Endpoint, Finding +from dojo.utils import parse_cvss_data _logger = logging.getLogger(__name__) @@ -197,7 +198,7 @@ def build_findings_from_dict(report_findings: [dict]) -> [Finding]: # Clean up the CVE data appropriately cve_list = _clean_cve_data(cve_data) - if "CVSS3 Base" in report_finding: # TODO: VECTOR + if "CVSS3 Base" in report_finding: cvssv3 = _extract_cvss_vectors( report_finding["CVSS3 Base"], report_finding["CVSS3 Temporal"], ) @@ -227,8 +228,13 @@ def build_findings_from_dict(report_findings: [dict]) -> [Finding]: impact=report_finding["Impact"], date=date, vuln_id_from_tool=report_finding["QID"], - cvssv3=cvssv3, ) + # Make sure vector is valid + cvss_data = parse_cvss_data(cvssv3) + if cvss_data: + finding.cvss3 = cvss_data.get("vector") + finding.cvssv3_score = cvss_data.get("score") + # Qualys reports regression findings as active, but with a Date Last # Fixed. if report_finding["Date Last Fixed"]: diff --git a/dojo/tools/qualys/parser.py b/dojo/tools/qualys/parser.py index e0fe1936be7..2d66455d34d 100644 --- a/dojo/tools/qualys/parser.py +++ b/dojo/tools/qualys/parser.py @@ -8,6 +8,7 @@ from dojo.models import Endpoint, Finding from dojo.tools.qualys import csv_parser +from dojo.utils import parse_cvss_data logger = logging.getLogger(__name__) @@ -352,7 +353,11 @@ def parse_finding(host, tree): finding.is_mitigated = temp["mitigated"] finding.active = temp["active"] if temp.get("CVSS_vector") is not None: - finding.cvssv3 = temp.get("CVSS_vector") # TODO: VECTOR + cvss_data = parse_cvss_data(temp.get("CVSS_vector")) + if cvss_data: + finding.cvss3 = cvss_data.get("vector") + finding.cvss3_score = cvss_data.get("base_score") + if temp.get("CVSS_value") is not None: finding.cvssv3_score = temp.get("CVSS_value") finding.verified = True diff --git a/dojo/tools/sonatype/parser.py b/dojo/tools/sonatype/parser.py index 2a9a00d4bdb..6ff0f18a1fc 100644 --- a/dojo/tools/sonatype/parser.py +++ b/dojo/tools/sonatype/parser.py @@ -2,6 +2,7 @@ from dojo.models import Finding from dojo.tools.sonatype.identifier import ComponentIdentifier +from dojo.utils import parse_cvss_data class SonatypeParser: @@ -63,7 +64,10 @@ def get_finding(security_issue, component, test): finding.cwe = security_issue["cwe"] if "cvssVector" in security_issue: - finding.cvssv3 = security_issue["cvssVector"] # TODO: VECTOR + cvss_data = parse_cvss_data(security_issue["cvssVector"]) + if cvss_data: + finding.cvss3 = cvss_data.get("vector") + finding.cvssv3_score = cvss_data.get("score") if "pathnames" in component: finding.file_path = " ".join(component["pathnames"])[:1000] diff --git a/dojo/tools/trivy/parser.py b/dojo/tools/trivy/parser.py index 275697a1745..91a6c6a4284 100644 --- a/dojo/tools/trivy/parser.py +++ b/dojo/tools/trivy/parser.py @@ -4,6 +4,7 @@ import logging from dojo.models import Finding +from dojo.utils import parse_cvss_data logger = logging.getLogger(__name__) @@ -171,7 +172,13 @@ def get_result_items(self, test, results, service_name=None, artifact_name=""): if cvssclass is not None: if cvssclass.get("V3Score") is not None: severity = self.convert_cvss_score(cvssclass.get("V3Score")) - cvssv3 = dict(cvssclass).get("V3Vector") # TODO: VECTOR + cvssv3_string = dict(cvssclass).get("V3Vector") + cvss_data = parse_cvss_data(cvssv3_string) + if cvss_data: + cvssv3 = cvss_data.get("vector") + cvssv3_score = cvss_data.get("score") + elif cvssclass.get("V3Score") is not None: + cvssv3_score = cvssclass.get("V3Score") elif cvssclass.get("V2Score") is not None: severity = self.convert_cvss_score(cvssclass.get("V2Score")) else: @@ -216,6 +223,7 @@ def get_result_items(self, test, results, service_name=None, artifact_name=""): component_name=package_name, component_version=package_version, cvssv3=cvssv3, + cvssv3_score=cvssv3_score, static_finding=True, dynamic_finding=False, tags=[vul_type, target_class], diff --git a/dojo/tools/veracode/json_parser.py b/dojo/tools/veracode/json_parser.py index e7e16fb695c..2650d5af02b 100644 --- a/dojo/tools/veracode/json_parser.py +++ b/dojo/tools/veracode/json_parser.py @@ -126,7 +126,7 @@ def create_finding_from_details(self, finding_details, scan_type, policy_violate if uncleaned_cvss := finding_details.get("cvss"): if isinstance(uncleaned_cvss, str): if uncleaned_cvss.startswith(("CVSS:3.1/", "CVSS:3.0/")): - finding.cvssv3 = CVSS3(str(uncleaned_cvss)).clean_vector(output_prefix=True) # TODO: VECTOR + finding.cvssv3 = CVSS3(str(uncleaned_cvss)).clean_vector(output_prefix=True) elif not uncleaned_cvss.startswith("CVSS"): finding.cvssv3 = CVSS3(f"CVSS:3.1/{uncleaned_cvss}").clean_vector(output_prefix=True) elif isinstance(uncleaned_cvss, float | int): diff --git a/dojo/utils.py b/dojo/utils.py index 729ba51cb42..f8a4995028c 100644 --- a/dojo/utils.py +++ b/dojo/utils.py @@ -15,11 +15,13 @@ import bleach import crum +import cvss.parser import hyperlink import vobject from asteval import Interpreter from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cvss.cvss3 import CVSS3 from dateutil.parser import parse from dateutil.relativedelta import MO, SU, relativedelta from django.conf import settings @@ -2654,3 +2656,18 @@ def generate_file_response_from_file_path( response["Content-Disposition"] = f'attachment; filename="{full_file_name}"' response["Content-Length"] = file_size return response + + +def parse_cvss_data(cvss_vector_string: str) -> dict: + if not cvss_vector_string: + return {} + + vectors = cvss.parser.parse_cvss_from_text(cvss_vector_string) + if len(vectors) > 0 and type(vectors[0]) is CVSS3: + return { + "vector": vectors[0].clean_vector(), + "severity": vectors[0].severities()[0], + "base_score": vectors[0].base_score(), + } + logger.debug("No valid CVSS3 vector found in %s", cvss_vector_string) + return {} From ef7d221363c878159ed0c02b228c5f0f33500036 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Fri, 16 May 2025 21:44:36 +0200 Subject: [PATCH 13/21] fix base score --- dojo/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dojo/utils.py b/dojo/utils.py index f8a4995028c..12081050e02 100644 --- a/dojo/utils.py +++ b/dojo/utils.py @@ -2667,7 +2667,7 @@ def parse_cvss_data(cvss_vector_string: str) -> dict: return { "vector": vectors[0].clean_vector(), "severity": vectors[0].severities()[0], - "base_score": vectors[0].base_score(), + "base_score": vectors[0].scores()[0], } logger.debug("No valid CVSS3 vector found in %s", cvss_vector_string) return {} From 3b0300d936f3312e04db91a8a2f1ae169744536e Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Fri, 16 May 2025 21:48:58 +0200 Subject: [PATCH 14/21] fix parser --- dojo/tools/qualys/parser.py | 2 +- dojo/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dojo/tools/qualys/parser.py b/dojo/tools/qualys/parser.py index 2d66455d34d..ff91cda7e2c 100644 --- a/dojo/tools/qualys/parser.py +++ b/dojo/tools/qualys/parser.py @@ -356,7 +356,7 @@ def parse_finding(host, tree): cvss_data = parse_cvss_data(temp.get("CVSS_vector")) if cvss_data: finding.cvss3 = cvss_data.get("vector") - finding.cvss3_score = cvss_data.get("base_score") + finding.cvss3_score = cvss_data.get("score") if temp.get("CVSS_value") is not None: finding.cvssv3_score = temp.get("CVSS_value") diff --git a/dojo/utils.py b/dojo/utils.py index 12081050e02..f5906626a31 100644 --- a/dojo/utils.py +++ b/dojo/utils.py @@ -2666,8 +2666,8 @@ def parse_cvss_data(cvss_vector_string: str) -> dict: if len(vectors) > 0 and type(vectors[0]) is CVSS3: return { "vector": vectors[0].clean_vector(), + "score": vectors[0].scores()[0], "severity": vectors[0].severities()[0], - "base_score": vectors[0].scores()[0], } logger.debug("No valid CVSS3 vector found in %s", cvss_vector_string) return {} From 206796f2ff8bb17c6f1f27fb4aeb81a6df9053c1 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Fri, 16 May 2025 22:57:54 +0200 Subject: [PATCH 15/21] fix typos --- dojo/models.py | 16 +++++++++++----- dojo/tools/qualys/csv_parser.py | 2 +- dojo/tools/qualys/parser.py | 7 +++++-- dojo/tools/sysdig_cli/parser.py | 4 ++-- dojo/tools/sysdig_reports/parser.py | 2 +- dojo/tools/trivy/parser.py | 1 + dojo/utils.py | 2 +- 7 files changed, 22 insertions(+), 12 deletions(-) diff --git a/dojo/models.py b/dojo/models.py index 36b94efbcd7..a30e1d80acb 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -10,7 +10,6 @@ from pathlib import Path from uuid import uuid4 -import cvss.parser import dateutil import hyperlink import tagulous.admin @@ -2699,9 +2698,12 @@ def save(self, dedupe_option=True, rules_option=True, product_grading_option=Tru # Synchronize cvssv3 score using cvssv3 vector if self.cvssv3: try: - cvss_vector = cvss.parser.parse_cvss_from_text(self.cvssv3) - # use the environmental score, which is the most refined score - self.cvssv3_score = cvss_vector.scores()[2] + + cvss_data = parse_cvss_data(self.cvssv3) + if cvss_data: + self.cvss3 = cvss_data.get("vector") + self.cvssv3_score = cvss_data.get("score") + except Exception as ex: logger.warning("Can't compute cvssv3 score for finding id %i. Invalid cvssv3 vector found: '%s'. Exception: %s.", self.id, self.cvssv3, ex) # should we set self.cvssv3 to None here to avoid storing invalid vectors? it would also remove invalid vectors on existing findings... @@ -4634,7 +4636,11 @@ def __str__(self): auditlog.register(Notification_Webhooks, exclude_fields=["header_name", "header_value"]) -from dojo.utils import calculate_grade, to_str_typed # noqa: E402 # there is issue due to a circular import +from dojo.utils import ( # noqa: E402 # there is issue due to a circular import + calculate_grade, + parse_cvss_data, + to_str_typed, +) tagulous.admin.register(Product.tags) tagulous.admin.register(Test.tags) diff --git a/dojo/tools/qualys/csv_parser.py b/dojo/tools/qualys/csv_parser.py index fea1ed0327a..f0672df9c87 100644 --- a/dojo/tools/qualys/csv_parser.py +++ b/dojo/tools/qualys/csv_parser.py @@ -232,7 +232,7 @@ def build_findings_from_dict(report_findings: [dict]) -> [Finding]: # Make sure vector is valid cvss_data = parse_cvss_data(cvssv3) if cvss_data: - finding.cvss3 = cvss_data.get("vector") + finding.cvssv3 = cvss_data.get("vector") finding.cvssv3_score = cvss_data.get("score") # Qualys reports regression findings as active, but with a Date Last diff --git a/dojo/tools/qualys/parser.py b/dojo/tools/qualys/parser.py index ff91cda7e2c..3d6acfb60ee 100644 --- a/dojo/tools/qualys/parser.py +++ b/dojo/tools/qualys/parser.py @@ -352,11 +352,14 @@ def parse_finding(host, tree): finding.mitigated = temp["mitigation_date"] finding.is_mitigated = temp["mitigated"] finding.active = temp["active"] + logger.debug("CVSS_Vector: %s", temp.get("CVSS_vector")) if temp.get("CVSS_vector") is not None: + logger.debug("CVSS_Vector: %s", temp.get("CVSS_vector")) cvss_data = parse_cvss_data(temp.get("CVSS_vector")) + logger.debug("cvss_data: %s", cvss_data) if cvss_data: - finding.cvss3 = cvss_data.get("vector") - finding.cvss3_score = cvss_data.get("score") + finding.cvssv3 = cvss_data.get("vector") + finding.cvssv3_score = cvss_data.get("score") if temp.get("CVSS_value") is not None: finding.cvssv3_score = temp.get("CVSS_value") diff --git a/dojo/tools/sysdig_cli/parser.py b/dojo/tools/sysdig_cli/parser.py index 60d76bf57f9..dd8cf91f790 100644 --- a/dojo/tools/sysdig_cli/parser.py +++ b/dojo/tools/sysdig_cli/parser.py @@ -108,7 +108,7 @@ def parse_json(self, data, test): finding.cvssv3_score = vulnCvssScore vectors = cvss.parser.parse_cvss_from_text(vulnCvssVector) if len(vectors) > 0 and isinstance(vectors[0], CVSS3): - finding.cvss = vectors[0].clean_vector() + finding.cvssv3 = vectors[0].clean_vector() except ValueError: continue @@ -164,7 +164,7 @@ def parse_csv(self, arr_data, test): finding.cvssv3_score = float(row.cvss_score) vectors = cvss.parser.parse_cvss_from_text(row.cvss_vector) if len(vectors) > 0 and isinstance(vectors[0], CVSS3): - finding.cvss = vectors[0].clean_vector() + finding.cvssv3 = vectors[0].clean_vector() except ValueError: continue finding.risk_accepted = row.risk_accepted diff --git a/dojo/tools/sysdig_reports/parser.py b/dojo/tools/sysdig_reports/parser.py index 0160e285e8a..c488c307ea2 100644 --- a/dojo/tools/sysdig_reports/parser.py +++ b/dojo/tools/sysdig_reports/parser.py @@ -221,7 +221,7 @@ def parse_csv(self, arr_data, test): finding.cvssv3_score = float(row.cvss_score) vectors = cvss.parser.parse_cvss_from_text(row.cvss_vector) if len(vectors) > 0 and isinstance(vectors[0], CVSS3): - finding.cvss = vectors[0].clean_vector() + finding.cvssv3 = vectors[0].clean_vector() except ValueError: continue finding.risk_accepted = row.risk_accepted diff --git a/dojo/tools/trivy/parser.py b/dojo/tools/trivy/parser.py index 91a6c6a4284..f06a4de1395 100644 --- a/dojo/tools/trivy/parser.py +++ b/dojo/tools/trivy/parser.py @@ -167,6 +167,7 @@ def get_result_items(self, test, results, service_name=None, artifact_name=""): severity_source = vuln.get("SeveritySource", None) cvss = vuln.get("CVSS", None) cvssv3 = None + cvssv3_score = None if severity_source is not None and cvss is not None: cvssclass = cvss.get(severity_source, None) if cvssclass is not None: diff --git a/dojo/utils.py b/dojo/utils.py index f5906626a31..a7d57b6c915 100644 --- a/dojo/utils.py +++ b/dojo/utils.py @@ -2666,7 +2666,7 @@ def parse_cvss_data(cvss_vector_string: str) -> dict: if len(vectors) > 0 and type(vectors[0]) is CVSS3: return { "vector": vectors[0].clean_vector(), - "score": vectors[0].scores()[0], + "score": vectors[0].scores()[2], # environmental score is the most detailed one "severity": vectors[0].severities()[0], } logger.debug("No valid CVSS3 vector found in %s", cvss_vector_string) From 2cc4ee6f035b04b305161fc7a7e448ff0fbb96eb Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Fri, 16 May 2025 23:04:52 +0200 Subject: [PATCH 16/21] fix typos --- .../en/open_source/contributing/how-to-write-a-parser.md | 4 ++-- dojo/models.py | 2 +- dojo/tools/aqua/parser.py | 2 +- dojo/tools/jfrog_xray_unified/parser.py | 2 +- dojo/tools/npm_audit_7_plus/parser.py | 2 +- dojo/tools/qualys/parser.py | 3 --- dojo/tools/sonatype/parser.py | 2 +- 7 files changed, 7 insertions(+), 10 deletions(-) diff --git a/docs/content/en/open_source/contributing/how-to-write-a-parser.md b/docs/content/en/open_source/contributing/how-to-write-a-parser.md index 6bba47a9cd5..0364e626799 100644 --- a/docs/content/en/open_source/contributing/how-to-write-a-parser.md +++ b/docs/content/en/open_source/contributing/how-to-write-a-parser.md @@ -174,7 +174,7 @@ For parser, we rely on module `cvss`. But we also have a helper method to valida ```python 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") if cvss_data: - finding.cvss3 = cvss_data.get("vector") + finding.cvssv3 = cvss_data.get("vector") finding.cvssv3_score = cvss_data.get("score") finding.severity = cvss_data.get("severity") # if your tool does generate severity ``` @@ -312,7 +312,7 @@ or like this: $ ./run-unittest.sh --test-case unittests.tools.test_aqua_parser.TestAquaParser {{< /highlight >}} -If you want to run all unit tests, simply run `$ docker-compose exec uwsgi bash -c 'python manage.py test unittests -v2'` +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'` ### Endpoint validation diff --git a/dojo/models.py b/dojo/models.py index a30e1d80acb..dbca00cba70 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -2701,7 +2701,7 @@ def save(self, dedupe_option=True, rules_option=True, product_grading_option=Tru cvss_data = parse_cvss_data(self.cvssv3) if cvss_data: - self.cvss3 = cvss_data.get("vector") + self.cvssv3 = cvss_data.get("vector") self.cvssv3_score = cvss_data.get("score") except Exception as ex: diff --git a/dojo/tools/aqua/parser.py b/dojo/tools/aqua/parser.py index ae464c67fe7..bf6b104ab68 100644 --- a/dojo/tools/aqua/parser.py +++ b/dojo/tools/aqua/parser.py @@ -226,7 +226,7 @@ def get_item(self, resource, vuln, test): cvss_data = parse_cvss_data(cvssv3) if cvss_data: - finding.cvss3 = cvss_data.get("vector") + finding.cvssv3 = cvss_data.get("vector") finding.cvssv3_score = cvss_data.get("score") if vulnerability_id != "No CVE": diff --git a/dojo/tools/jfrog_xray_unified/parser.py b/dojo/tools/jfrog_xray_unified/parser.py index 19b30a7dcd3..9690cbe4ab5 100644 --- a/dojo/tools/jfrog_xray_unified/parser.py +++ b/dojo/tools/jfrog_xray_unified/parser.py @@ -142,7 +142,7 @@ def get_item(vulnerability, test): cvss_data = parse_cvss_data(cvssv3) if cvss_data: - finding.cvss3 = cvss_data.get("vector") + finding.cvssv3 = cvss_data.get("vector") finding.cvssv3_score = cvss_data.get("score") if vulnerability_id: diff --git a/dojo/tools/npm_audit_7_plus/parser.py b/dojo/tools/npm_audit_7_plus/parser.py index 71d2325e316..3ad4a37461f 100644 --- a/dojo/tools/npm_audit_7_plus/parser.py +++ b/dojo/tools/npm_audit_7_plus/parser.py @@ -169,7 +169,7 @@ def get_item(item_node, tree, test): from dojo.utils import parse_cvss_data cvss_data = parse_cvss_data(cvssv3) if cvss_data: - dojo_finding.cvss3 = cvss_data.get("vector") + dojo_finding.cvssv3 = cvss_data.get("vector") dojo_finding.cvssv3_score = cvss_data.get("score") return dojo_finding diff --git a/dojo/tools/qualys/parser.py b/dojo/tools/qualys/parser.py index 3d6acfb60ee..dded935b7ad 100644 --- a/dojo/tools/qualys/parser.py +++ b/dojo/tools/qualys/parser.py @@ -352,11 +352,8 @@ def parse_finding(host, tree): finding.mitigated = temp["mitigation_date"] finding.is_mitigated = temp["mitigated"] finding.active = temp["active"] - logger.debug("CVSS_Vector: %s", temp.get("CVSS_vector")) if temp.get("CVSS_vector") is not None: - logger.debug("CVSS_Vector: %s", temp.get("CVSS_vector")) cvss_data = parse_cvss_data(temp.get("CVSS_vector")) - logger.debug("cvss_data: %s", cvss_data) if cvss_data: finding.cvssv3 = cvss_data.get("vector") finding.cvssv3_score = cvss_data.get("score") diff --git a/dojo/tools/sonatype/parser.py b/dojo/tools/sonatype/parser.py index 6ff0f18a1fc..aeb5a0e3e77 100644 --- a/dojo/tools/sonatype/parser.py +++ b/dojo/tools/sonatype/parser.py @@ -66,7 +66,7 @@ def get_finding(security_issue, component, test): if "cvssVector" in security_issue: cvss_data = parse_cvss_data(security_issue["cvssVector"]) if cvss_data: - finding.cvss3 = cvss_data.get("vector") + finding.cvssv3 = cvss_data.get("vector") finding.cvssv3_score = cvss_data.get("score") if "pathnames" in component: From 6ed693acf7341085ad6a5863d675bd5f24d52511 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Fri, 16 May 2025 23:12:22 +0200 Subject: [PATCH 17/21] fix typos --- dojo/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dojo/models.py b/dojo/models.py index dbca00cba70..12905f1177a 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -43,7 +43,7 @@ from tagulous.models import TagField from tagulous.models.managers import FakeTagRelatedManager -from dojo.validators import cvss3_validator # to avoid circular import +from dojo.validators import cvss3_validator logger = logging.getLogger(__name__) deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication") From 171458887197e3600b934b68d754223ac8741ee0 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Thu, 29 May 2025 12:51:20 +0200 Subject: [PATCH 18/21] address feedback --- .../en/open_source/contributing/how-to-write-a-parser.md | 6 ++++-- dojo/models.py | 4 +++- dojo/tools/npm_audit_7_plus/parser.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/content/en/open_source/contributing/how-to-write-a-parser.md b/docs/content/en/open_source/contributing/how-to-write-a-parser.md index 0364e626799..5a2b743e093 100644 --- a/docs/content/en/open_source/contributing/how-to-write-a-parser.md +++ b/docs/content/en/open_source/contributing/how-to-write-a-parser.md @@ -172,6 +172,7 @@ Data can have `CVSS` vectors or scores. Don't write your own CVSS score algorith 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. ```python +from dojo.utils import parse_cvss_data 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") if cvss_data: finding.cvssv3 = cvss_data.get("vector") @@ -179,13 +180,14 @@ if cvss_data: finding.severity = cvss_data.get("severity") # if your tool does generate severity ``` -If you need more manual processing, you can parse the `CVSS` vector directly. +If you need more manual processing, you can parse the `CVSS3` vector directly. Example of use: ```python -from dojo.utils import cvss.cvss3 import CVSS3 import cvss.parser +from cvss import CVSS2, CVSS3 + 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") if len(vectors) > 0 and type(vectors[0]) is CVSS3: print(vectors[0].severities()) # this is the 3 severities diff --git a/dojo/models.py b/dojo/models.py index 12905f1177a..da5690c0be6 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -2706,7 +2706,9 @@ def save(self, dedupe_option=True, rules_option=True, product_grading_option=Tru except Exception as ex: logger.warning("Can't compute cvssv3 score for finding id %i. Invalid cvssv3 vector found: '%s'. Exception: %s.", self.id, self.cvssv3, ex) - # should we set self.cvssv3 to None here to avoid storing invalid vectors? it would also remove invalid vectors on existing findings... + # remove invalid cvssv3 vector for new findings, or should we just throw a ValidationError? + if self.pk is None: + self.cvssv3 = None self.set_hash_code(dedupe_option) diff --git a/dojo/tools/npm_audit_7_plus/parser.py b/dojo/tools/npm_audit_7_plus/parser.py index 3ad4a37461f..93f04111689 100644 --- a/dojo/tools/npm_audit_7_plus/parser.py +++ b/dojo/tools/npm_audit_7_plus/parser.py @@ -3,6 +3,7 @@ import logging from dojo.models import Finding +from dojo.utils import parse_cvss_data logger = logging.getLogger(__name__) @@ -166,7 +167,6 @@ def get_item(item_node, tree, test): dojo_finding.cwe = cwe if (cvssv3 is not None) and (len(cvssv3) > 0): - from dojo.utils import parse_cvss_data cvss_data = parse_cvss_data(cvssv3) if cvss_data: dojo_finding.cvssv3 = cvss_data.get("vector") From 443210e264dbaedf938915bcf6678b94a3720a9e Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Thu, 29 May 2025 12:55:04 +0200 Subject: [PATCH 19/21] rebase migration --- ...ng_cvssv3_alter_finding_template_cvssv3.py | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 dojo/db_migrations/0229_alter_finding_cvssv3_alter_finding_template_cvssv3.py diff --git a/dojo/db_migrations/0229_alter_finding_cvssv3_alter_finding_template_cvssv3.py b/dojo/db_migrations/0229_alter_finding_cvssv3_alter_finding_template_cvssv3.py deleted file mode 100644 index 0e51a069b34..00000000000 --- a/dojo/db_migrations/0229_alter_finding_cvssv3_alter_finding_template_cvssv3.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.1.8 on 2025-05-14 06:35 - -import dojo.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dojo', '0228_alter_jira_username_password'), - ] - - operations = [ - migrations.AlterField( - model_name='finding', - name='cvssv3', - 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'), - ), - migrations.AlterField( - model_name='finding_template', - name='cvssv3', - 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'), - ), - ] \ No newline at end of file From 1bfcc81bf9a26e6944fa51484ffb266cf9d63ca8 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Thu, 29 May 2025 13:06:12 +0200 Subject: [PATCH 20/21] rebase migration --- ...ng_cvssv3_alter_finding_template_cvssv3.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 dojo/db_migrations/0230_alter_finding_cvssv3_alter_finding_template_cvssv3.py diff --git a/dojo/db_migrations/0230_alter_finding_cvssv3_alter_finding_template_cvssv3.py b/dojo/db_migrations/0230_alter_finding_cvssv3_alter_finding_template_cvssv3.py new file mode 100644 index 00000000000..d36f09ec06e --- /dev/null +++ b/dojo/db_migrations/0230_alter_finding_cvssv3_alter_finding_template_cvssv3.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.8 on 2025-05-14 06:35 + +import dojo.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0229_alter_finding_unique_id_from_tool'), + ] + + operations = [ + migrations.AlterField( + model_name='finding', + name='cvssv3', + 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'), + ), + migrations.AlterField( + model_name='finding_template', + name='cvssv3', + 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'), + ), + ] From 15fc291dd25c3c17911a6efb81b36552305d0ba4 Mon Sep 17 00:00:00 2001 From: Valentijn Scholten Date: Sun, 22 Jun 2025 12:02:16 +0200 Subject: [PATCH 21/21] rebase migration + serializer --- dojo/api_v2/serializers.py | 4 ---- ...231_alter_finding_cvssv3_alter_finding_template_cvssv3.py} | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) rename dojo/db_migrations/{0230_alter_finding_cvssv3_alter_finding_template_cvssv3.py => 0231_alter_finding_cvssv3_alter_finding_template_cvssv3.py} (91%) diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index bba67e35136..cc94b9151bc 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -1955,8 +1955,6 @@ class Meta: def create(self, validated_data): - to_be_tagged, validated_data = self._pop_tags(validated_data) - # Save vulnerability ids and pop them if "vulnerability_id_template_set" in validated_data: vulnerability_id_set = validated_data.pop( @@ -1977,8 +1975,6 @@ def create(self, validated_data): ) new_finding_template.save() - self._save_tags(new_finding_template, to_be_tagged) - return new_finding_template def update(self, instance, validated_data): diff --git a/dojo/db_migrations/0230_alter_finding_cvssv3_alter_finding_template_cvssv3.py b/dojo/db_migrations/0231_alter_finding_cvssv3_alter_finding_template_cvssv3.py similarity index 91% rename from dojo/db_migrations/0230_alter_finding_cvssv3_alter_finding_template_cvssv3.py rename to dojo/db_migrations/0231_alter_finding_cvssv3_alter_finding_template_cvssv3.py index d36f09ec06e..e8278db9f1b 100644 --- a/dojo/db_migrations/0230_alter_finding_cvssv3_alter_finding_template_cvssv3.py +++ b/dojo/db_migrations/0231_alter_finding_cvssv3_alter_finding_template_cvssv3.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ('dojo', '0229_alter_finding_unique_id_from_tool'), + ('dojo', '0230_alter_jira_instance_accepted_mapping_resolution_and_more'), ] operations = [