Skip to content

Improve cvssv3 validation #12440

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions docs/content/en/open_source/contributing/how-to-write-a-parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,25 @@ 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
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")
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 `CVSS3` vector directly.

Example of use:

```python
from 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
Expand All @@ -186,17 +196,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):
Expand Down Expand Up @@ -313,7 +314,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

Expand Down
3 changes: 2 additions & 1 deletion dojo/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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', '0230_alter_jira_instance_accepted_mapping_resolution_and_more'),
]

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'),
),
]
2 changes: 1 addition & 1 deletion dojo/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
34 changes: 22 additions & 12 deletions dojo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
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
Expand Down Expand Up @@ -44,6 +43,8 @@
from tagulous.models import TagField
from tagulous.models.managers import FakeTagRelatedManager

from dojo.validators import cvss3_validator

logger = logging.getLogger(__name__)
deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication")

Expand Down Expand Up @@ -2331,12 +2332,11 @@ 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,
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"),
Expand Down Expand Up @@ -2698,11 +2698,17 @@ 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)
# use the environmental score, which is the most refined score
self.cvssv3_score = cvss_object.scores()[2]

cvss_data = parse_cvss_data(self.cvssv3)
if cvss_data:
self.cvssv3 = cvss_data.get("vector")
self.cvssv3_score = cvss_data.get("score")

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)
# 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)

Expand Down Expand Up @@ -3515,8 +3521,8 @@ 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)
description = models.TextField(null=True, blank=True)
mitigation = models.TextField(null=True, blank=True)
Expand Down Expand Up @@ -4632,7 +4638,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)
Expand Down
9 changes: 8 additions & 1 deletion dojo/tools/aqua/parser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json

from dojo.models import Finding
from dojo.utils import parse_cvss_data


class AquaParser:
Expand Down Expand Up @@ -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(
Expand All @@ -214,14 +216,19 @@ 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,
component_name=resource.get("name"),
component_version=resource.get("version"),
impact=severity,
)

cvss_data = parse_cvss_data(cvssv3)
if cvss_data:
finding.cvssv3 = 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"):
Expand Down
7 changes: 6 additions & 1 deletion dojo/tools/jfrog_xray_unified/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import datetime

from dojo.models import Finding
from dojo.utils import parse_cvss_data


class JFrogXrayUnifiedParser:
Expand Down Expand Up @@ -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.cvssv3 = cvss_data.get("vector")
finding.cvssv3_score = cvss_data.get("score")

if vulnerability_id:
finding.unsaved_vulnerability_ids = [vulnerability_id]

Expand Down
6 changes: 5 additions & 1 deletion dojo/tools/npm_audit_7_plus/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging

from dojo.models import Finding
from dojo.utils import parse_cvss_data

logger = logging.getLogger(__name__)

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

if (cvssv3 is not None) and (len(cvssv3) > 0):
dojo_finding.cvssv3 = cvssv3
cvss_data = parse_cvss_data(cvssv3)
if cvss_data:
dojo_finding.cvssv3 = cvss_data.get("vector")
dojo_finding.cvssv3_score = cvss_data.get("score")

return dojo_finding

Expand Down
8 changes: 7 additions & 1 deletion dojo/tools/qualys/csv_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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.cvssv3 = 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"]:
Expand Down
7 changes: 6 additions & 1 deletion dojo/tools/qualys/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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")
cvss_data = parse_cvss_data(temp.get("CVSS_vector"))
if cvss_data:
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")
finding.verified = True
Expand Down
6 changes: 5 additions & 1 deletion dojo/tools/sonatype/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"]
cvss_data = parse_cvss_data(security_issue["cvssVector"])
if cvss_data:
finding.cvssv3 = cvss_data.get("vector")
finding.cvssv3_score = cvss_data.get("score")

if "pathnames" in component:
finding.file_path = " ".join(component["pathnames"])[:1000]
Expand Down
4 changes: 2 additions & 2 deletions dojo/tools/sysdig_cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dojo/tools/sysdig_reports/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion dojo/tools/trivy/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging

from dojo.models import Finding
from dojo.utils import parse_cvss_data

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -166,12 +167,19 @@ 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:
if cvssclass.get("V3Score") is not None:
severity = self.convert_cvss_score(cvssclass.get("V3Score"))
cvssv3 = dict(cvssclass).get("V3Vector")
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:
Expand Down Expand Up @@ -216,6 +224,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],
Expand Down
Loading