diff options
| author | Natalia <124304+nessita@users.noreply.github.com> | 2026-04-15 22:01:17 -0300 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2026-04-19 16:47:14 +0300 |
| commit | 7760fe66ab10a9fad618163787119dcfb876b089 (patch) | |
| tree | 21267f3a2e2c7b08603f85b2c1ac6eadc9a38f82 | |
| parent | ab5429a66410966cebbce2616e216abf02f50b32 (diff) | |
[checklists] Updated SecurityIssue model to track CVSS 3.1 and 4.0 scores.
This work also drops all unused fields specifics to the CVSS score.
Fixes #2592.
| -rw-r--r-- | checklists/admin.py | 62 | ||||
| -rw-r--r-- | checklists/migrations/0004_securityissue_cvss_v3_score_and_more.py | 68 | ||||
| -rw-r--r-- | checklists/migrations/0005_populate_cvss_v4_fields.py | 37 | ||||
| -rw-r--r-- | checklists/migrations/0006_remove_securityissue_attack_complexity_and_more.py | 85 | ||||
| -rw-r--r-- | checklists/models.py | 342 | ||||
| -rw-r--r-- | checklists/tests/test_models.py | 111 |
6 files changed, 390 insertions, 315 deletions
diff --git a/checklists/admin.py b/checklists/admin.py index 48d61e7b..11298547 100644 --- a/checklists/admin.py +++ b/checklists/admin.py @@ -103,8 +103,8 @@ class SecurityIssueAdmin(admin.ModelAdmin): list_filter = ["severity", "release"] search_fields = ["cve_year_number", "summary", "description", "commit_hash_main"] readonly_fields = [ - "cvss_base_severity", - "cvss_vector", + "cvss_v3_severity", + "cvss_v4_severity", ] inlines = [SecurityIssueReleasesThroughInline] formfield_overrides = { @@ -134,55 +134,23 @@ class SecurityIssueAdmin(admin.ModelAdmin): }, ), ( - "CVSS 4.0 Fields - Base Metrics - Exploitability", + "CVSS Scores", { - "fields": ( - "attack_vector", - "attack_complexity", - "attack_requirements", - "privileges_required", - "user_interaction", + "description": ( + "<p>Use an approved calculator to get the vector string and score, " + "then enter both here. Calculators: " + '<a href="https://www.first.org/cvss/calculator/3.1">CVSS v3.1</a> ' + '<a href="https://www.first.org/cvss/calculator/4.0">CVSS v4.0</a> ' + "</p>" ), - "classes": ["collapse"], - }, - ), - ( - "CVSS 4.0 Fields - Base Metrics - Vulnerable System Impact", - { "fields": ( - "vuln_confidentiality_impact", - "sub_confidentiality_impact", - "vuln_integrity_impact", - "sub_integrity_impact", - "vuln_availability_impact", - "sub_availability_impact", + "cvss_v3_vector_string", + "cvss_v3_score", + "cvss_v3_severity", + "cvss_v4_vector_string", + "cvss_v4_score", + "cvss_v4_severity", ), - "classes": ["collapse"], - }, - ), - ( - "CVSS 4.0 Fields - Supplemental Metrics", - { - "fields": ( - "safety", - "automatable", - "recovery", - "value_density", - "vulnerability_response_effort", - "provider_urgency", - ), - "classes": ["collapse"], - }, - ), - ( - "CVSS 4.0 Fields - Score and Vector", - { - "fields": ( - "cvss_base_score", - "cvss_base_severity", - "cvss_vector", - ), - "classes": ["collapse"], }, ), ( diff --git a/checklists/migrations/0004_securityissue_cvss_v3_score_and_more.py b/checklists/migrations/0004_securityissue_cvss_v3_score_and_more.py new file mode 100644 index 00000000..85d82c53 --- /dev/null +++ b/checklists/migrations/0004_securityissue_cvss_v3_score_and_more.py @@ -0,0 +1,68 @@ +# Generated by Django 6.0.4 on 2026-04-15 18:10 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("checklists", "0003_alter_securityissue_blogdescription"), + ] + + operations = [ + migrations.AddField( + model_name="securityissue", + name="cvss_v3_score", + field=models.DecimalField( + blank=True, + decimal_places=1, + help_text="Base score (0.0-10.0) from the CVSS v3.1 calculator.", + max_digits=3, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(10), + ], + verbose_name="CVSS v3.1 Score", + ), + ), + migrations.AddField( + model_name="securityissue", + name="cvss_v3_vector_string", + field=models.CharField( + blank=True, + default="", + help_text="CVSS v3.1 vector string. Example: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + max_length=256, + verbose_name="CVSS v3.1 Vector String", + ), + ), + migrations.AddField( + model_name="securityissue", + name="cvss_v4_score", + field=models.DecimalField( + blank=True, + decimal_places=1, + help_text="Base score (0.0-10.0) from the CVSS v4.0 calculator.", + max_digits=3, + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(10), + ], + verbose_name="CVSS v4.0 Score", + ), + ), + migrations.AddField( + model_name="securityissue", + name="cvss_v4_vector_string", + field=models.CharField( + blank=True, + default="", + help_text="CVSS v4.0 vector string. Example: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N", + max_length=256, + verbose_name="CVSS v4.0 Vector String", + ), + ), + ] diff --git a/checklists/migrations/0005_populate_cvss_v4_fields.py b/checklists/migrations/0005_populate_cvss_v4_fields.py new file mode 100644 index 00000000..0fe10471 --- /dev/null +++ b/checklists/migrations/0005_populate_cvss_v4_fields.py @@ -0,0 +1,37 @@ +from django.db import migrations + + +def populate_cvss_v4_fields(apps, schema_editor): + SecurityIssue = apps.get_model("checklists", "SecurityIssue") + for issue in SecurityIssue.objects.exclude(cvss_base_score=0): + issue.cvss_v4_score = issue.cvss_base_score + issue.cvss_v4_vector_string = "/".join( + [ + "CVSS:4.0", + f"AV:{issue.attack_vector}", + f"AC:{issue.attack_complexity}", + f"AT:{issue.attack_requirements}", + f"PR:{issue.privileges_required}", + f"UI:{issue.user_interaction}", + f"VC:{issue.vuln_confidentiality_impact}", + f"SC:{issue.sub_confidentiality_impact}", + f"VI:{issue.vuln_integrity_impact}", + f"SI:{issue.sub_integrity_impact}", + f"VA:{issue.vuln_availability_impact}", + f"SA:{issue.sub_availability_impact}", + ] + ) + issue.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("checklists", "0004_securityissue_cvss_v3_score_and_more"), + ] + + operations = [ + migrations.RunPython( + populate_cvss_v4_fields, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/checklists/migrations/0006_remove_securityissue_attack_complexity_and_more.py b/checklists/migrations/0006_remove_securityissue_attack_complexity_and_more.py new file mode 100644 index 00000000..9bd9d501 --- /dev/null +++ b/checklists/migrations/0006_remove_securityissue_attack_complexity_and_more.py @@ -0,0 +1,85 @@ +# Generated by Django 6.0.4 on 2026-04-15 18:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("checklists", "0005_populate_cvss_v4_fields"), + ] + + operations = [ + migrations.RemoveField( + model_name="securityissue", + name="attack_complexity", + ), + migrations.RemoveField( + model_name="securityissue", + name="attack_requirements", + ), + migrations.RemoveField( + model_name="securityissue", + name="attack_vector", + ), + migrations.RemoveField( + model_name="securityissue", + name="automatable", + ), + migrations.RemoveField( + model_name="securityissue", + name="cvss_base_score", + ), + migrations.RemoveField( + model_name="securityissue", + name="privileges_required", + ), + migrations.RemoveField( + model_name="securityissue", + name="provider_urgency", + ), + migrations.RemoveField( + model_name="securityissue", + name="recovery", + ), + migrations.RemoveField( + model_name="securityissue", + name="safety", + ), + migrations.RemoveField( + model_name="securityissue", + name="sub_availability_impact", + ), + migrations.RemoveField( + model_name="securityissue", + name="sub_confidentiality_impact", + ), + migrations.RemoveField( + model_name="securityissue", + name="sub_integrity_impact", + ), + migrations.RemoveField( + model_name="securityissue", + name="user_interaction", + ), + migrations.RemoveField( + model_name="securityissue", + name="value_density", + ), + migrations.RemoveField( + model_name="securityissue", + name="vuln_availability_impact", + ), + migrations.RemoveField( + model_name="securityissue", + name="vuln_confidentiality_impact", + ), + migrations.RemoveField( + model_name="securityissue", + name="vuln_integrity_impact", + ), + migrations.RemoveField( + model_name="securityissue", + name="vulnerability_response_effort", + ), + ] diff --git a/checklists/models.py b/checklists/models.py index 79e007bf..d369f35f 100644 --- a/checklists/models.py +++ b/checklists/models.py @@ -29,74 +29,6 @@ def cve_sort_key(field="cve_year_number", *, desc=False): return year, number -# CVSS metrics choices. - -CVSS_ATTACK_VECTOR_CHOICES = [ # AV - ("N", "Network"), - ("A", "Adjacent"), - ("L", "Local"), - ("P", "Physical"), -] -CVSS_ATTACK_COMPLEXITY_CHOICES = [ # AC - ("L", "Low"), - ("H", "High"), -] -CVSS_ATTACK_REQUIREMENTS_CHOICES = [ # AT - ("N", "None"), - ("P", "Present"), -] -CVSS_PRIVILEGES_REQUIRED_CHOICES = [ # PR - ("N", "None"), - ("L", "Low"), - ("H", "High"), -] -CVSS_USER_INTERACTION_CHOICES = [ # UI - ("N", "None"), - ("P", "Passive"), - ("A", "Active"), -] - -CVSS_IMPACT_CHOICES = [ - ("N", "None"), - ("L", "Low"), - ("H", "High"), -] - -CVSS_SAFETY_CHOICES = [ # S - ("X", "Not Defined"), - ("N", "Negligible"), - ("P", "Present"), -] -CVSS_AUTOMATABLE_CHOICES = [ # AU - ("X", "Not Defined"), - ("N", "No"), - ("Y", "Yes"), -] -CVSS_RECOVERY_CHOICES = [ # R - ("X", "Not Defined"), - ("A", "Automatic"), - ("U", "User"), - ("I", "Irrecoverable"), -] -CVSS_VALUE_DENSITY_CHOICES = [ # V - ("X", "Not Defined"), - ("D", "Diffuse"), - ("C", "Concentrated"), -] -CVSS_VULNERABILITY_RESPONSE_EFFORT_CHOICES = [ # RE - ("X", "Not Defined"), - ("L", "Low"), - ("M", "Moderate"), - ("H", "High"), -] -CVSS_PROVIDER_URGENCY_CHOICES = [ # U - ("X", "Not Defined"), - ("CLEAR", "Clear"), - ("GREEN", "Green"), - ("AMBER", "Amber"), - ("RED", "Red"), -] - DESCRIPTION_HELP_TEXT = """Written in present tense. Used in CVE metadata. Single `backticks` for inline code. @@ -136,6 +68,20 @@ def get_cve_default(): return f"CVE-{datetime.date.today().year}-{get_random_string(5)}" +def get_cvss_severity(score): + if not score: + return "NONE" + score = float(score) + if score < 4: + return "LOW" + elif score < 7: + return "MEDIUM" + elif score < 9: + return "HIGH" + else: + return "CRITICAL" + + class ReleaserManager(models.Manager): def get_by_natural_key(self, username): return self.get(user__username=username) @@ -598,145 +544,45 @@ class SecurityIssue(models.Model): ), ) - # CVSS 4.0 Fields. Base Metrics. - - # Exploitability Metrics. - attack_vector = models.CharField( - "CVSS Attack Vector", - max_length=16, - choices=CVSS_ATTACK_VECTOR_CHOICES, - default="N", - help_text="The context by which vulnerability exploitation is possible (AV)", - ) - attack_complexity = models.CharField( - "CVSS Attack Complexity", - max_length=8, - choices=CVSS_ATTACK_COMPLEXITY_CHOICES, - default="L", - help_text="Conditions beyond attacker control required to exploit (AC)", - ) - attack_requirements = models.CharField( - "CVSS Attack Requirements", - max_length=8, - choices=CVSS_ATTACK_REQUIREMENTS_CHOICES, - default="N", - help_text="Preconditions for attack to be successful (AT)", - ) - privileges_required = models.CharField( - "CVSS Privileges Required", - max_length=8, - choices=CVSS_PRIVILEGES_REQUIRED_CHOICES, - default="N", - help_text="Level of privileges needed to exploit (PR)", - ) - user_interaction = models.CharField( - "CVSS User Interaction", - max_length=8, - choices=CVSS_USER_INTERACTION_CHOICES, - default="N", - help_text="Whether user interaction is required (UI)", - ) - - # Vulnerable System Impact Metrics and Subsequent System Impact Metrics. - vuln_confidentiality_impact = models.CharField( - "CVSS Confidentiality Impact", - max_length=8, - choices=CVSS_IMPACT_CHOICES, - default="N", - help_text="Impact on confidentiality of information (VC)", - ) - sub_confidentiality_impact = models.CharField( - "CVSS Subsequent Confidentiality Impact", - max_length=8, - choices=CVSS_IMPACT_CHOICES, - default="N", - help_text="Subsequent impact on confidentiality (SC)", - ) - vuln_integrity_impact = models.CharField( - "CVSS Integrity Impact", - max_length=8, - choices=CVSS_IMPACT_CHOICES, - default="N", - help_text="Impact on integrity of information (VI)", - ) - sub_integrity_impact = models.CharField( - "CVSS Subsequent Integrity Impact", - max_length=8, - choices=CVSS_IMPACT_CHOICES, - default="N", - help_text="Subsequent impact on integrity of information (SI)", - ) - vuln_availability_impact = models.CharField( - "CVSS Availability Impact", - max_length=8, - choices=CVSS_IMPACT_CHOICES, - default="N", - help_text="Impact on availability of system (VA)", - ) - sub_availability_impact = models.CharField( - "CVSS Subsequent Availability Impact", - max_length=8, - choices=CVSS_IMPACT_CHOICES, - default="N", - help_text="Subsequent impact on availability of system (SA)", - ) - - # CVSS 4.0 Fields. Supplemental Metrics. - safety = models.CharField( - "CVSS Safety", - max_length=16, - choices=CVSS_SAFETY_CHOICES, - default="X", - help_text="Potential impact on safety of humans or environment (S)", - ) - automatable = models.CharField( - "CVSS Automatable", - max_length=16, - choices=CVSS_AUTOMATABLE_CHOICES, - default="X", - help_text="Ease of automation for exploit (AU)", - ) - recovery = models.CharField( - "CVSS Recovery", - max_length=16, - choices=CVSS_RECOVERY_CHOICES, - default="X", - help_text="Ease of recovery from the vulnerability (R)", - ) - value_density = models.CharField( - "CVSS Value Density", - max_length=16, - choices=CVSS_VALUE_DENSITY_CHOICES, - default="X", - help_text="Control gained over resources with a single exploitation event (V)", - ) - vulnerability_response_effort = models.CharField( - "CVSS Response Effort", - max_length=16, - choices=CVSS_VULNERABILITY_RESPONSE_EFFORT_CHOICES, - default="X", - help_text="Effort needed by provider to respond (RE)", - ) - provider_urgency = models.CharField( - "CVSS Urgency", - max_length=16, - choices=CVSS_PROVIDER_URGENCY_CHOICES, - default="X", - help_text="Urgency perceived by provider to respond (U)", + # CVSS Scores. + cvss_v3_vector_string = models.CharField( + "CVSS v3.1 Vector String", + max_length=256, + blank=True, + default="", + help_text=( + "CVSS v3.1 vector string. Example: " + "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N" + ), ) - - cvss_base_score = models.PositiveSmallIntegerField( - "CVSS Base Score", - default=0, + cvss_v3_score = models.DecimalField( + "CVSS v3.1 Score", + max_digits=3, + decimal_places=1, + null=True, + blank=True, validators=[MinValueValidator(0), MaxValueValidator(10)], + help_text="Base score (0.0-10.0) from the CVSS v3.1 calculator.", + ) + cvss_v4_vector_string = models.CharField( + "CVSS v4.0 Vector String", + max_length=256, + blank=True, + default="", help_text=( - "Base score (0–10) calculated from the CVSS v4.0 metrics.</br>" - "This value should be computed from the CVSS selected metric " - "fields using the official CVSS v4.0 formula.</br>See " - '<a href="https://www.first.org/cvss/calculator/4-0">' - "https://www.first.org/cvss/calculator/4-0</a>" + "CVSS v4.0 vector string. Example: " + "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N" ), ) + cvss_v4_score = models.DecimalField( + "CVSS v4.0 Score", + max_digits=3, + decimal_places=1, + null=True, + blank=True, + validators=[MinValueValidator(0), MaxValueValidator(10)], + help_text="Base score (0.0-10.0) from the CVSS v4.0 calculator.", + ) release = models.ForeignKey( SecurityRelease, @@ -774,37 +620,12 @@ class SecurityIssue(models.Model): ) @property - def cvss_base_severity(self): - if self.cvss_base_score == 0: - return "NONE" - elif self.cvss_base_score < 4: - return "LOW" - elif self.cvss_base_score < 7: - return "MEDIUM" - elif self.cvss_base_score < 9: - return "HIGH" - else: - return "CRITICAL" + def cvss_v3_severity(self): + return get_cvss_severity(self.cvss_v3_score) @property - def cvss_vector(self): - # Default when all values are default: - # CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N - parts = [ - "CVSS:4.0", - f"AV:{self.attack_vector}", - f"AC:{self.attack_complexity}", - f"AT:{self.attack_requirements}", - f"PR:{self.privileges_required}", - f"UI:{self.user_interaction}", - f"VC:{self.vuln_confidentiality_impact}", - f"SC:{self.sub_confidentiality_impact}", - f"VI:{self.vuln_integrity_impact}", - f"SI:{self.sub_integrity_impact}", - f"VA:{self.vuln_availability_impact}", - f"SA:{self.sub_availability_impact}", - ] - return "/".join(parts) + def cvss_v4_severity(self): + return get_cvss_severity(self.cvss_v4_score) @property def headline_for_blogpost(self): @@ -930,6 +751,28 @@ class SecurityIssue(models.Model): }, }, ] + if self.cvss_v3_vector_string and self.cvss_v3_score is not None: + metrics.append( + { + "cvssV3_1": { + "version": "3.1", + "vectorString": self.cvss_v3_vector_string, + "baseScore": float(self.cvss_v3_score), + "baseSeverity": self.cvss_v3_severity, + }, + } + ) + if self.cvss_v4_vector_string and self.cvss_v4_score is not None: + metrics.append( + { + "cvssV4_0": { + "version": "4.0", + "vectorString": self.cvss_v4_vector_string, + "baseScore": float(self.cvss_v4_score), + "baseSeverity": self.cvss_v4_severity, + }, + } + ) details = { "title": self.summary.replace("`", ""), "metrics": metrics, @@ -1013,42 +856,5 @@ class SecurityIssue(models.Model): def cve_minified_json(self): return json.dumps(self.cve_data, sort_keys=True, separators=(",", ":")) - def calculate_cvss_base_score(self): - """Implements CVSS v4.0 Base Score calculation (per FIRST.org spec). - - Unused for now, could be used to provide a suggestion or default value. - - """ - # Numeric mappings from the v4.0 spec - AV = {"N": 0.85, "A": 0.62, "L": 0.55, "P": 0.2} - AC = {"L": 0.77, "H": 0.44} - PR = {"N": 0.85, "L": 0.62, "H": 0.27} - UI = {"N": 0.85, "A": 0.62, "P": 0.85} - IMP = {"N": 0.0, "LOW": 0.22, "HIGH": 0.56} - - av = AV[self.attack_vector] - ac = AC[self.attack_complexity] - pr = PR[self.privileges_required] - ui = UI[self.user_interaction] - c = IMP[self.vuln_confidentiality_impact] - i = IMP[self.vuln_integrity_impact] - a = IMP[self.vuln_availability_impact] - - # Exploitability Subscore - exploitability = 8.22 * av * ac * pr * ui - - # Impact Subscore - impact_subscore = 1 - ((1 - c) * (1 - i) * (1 - a)) - - # Base score formula (official v4.0) - base_score = 0 - if impact_subscore > 0: - base_score = min(impact_subscore + exploitability, 10) - - import math - - # Round up to one decimal per spec - return math.ceil(base_score * 10) / 10.0 - def get_absolute_url(self): return reverse("checklists:cve_json_record", args=[self.cve_year_number]) diff --git a/checklists/tests/test_models.py b/checklists/tests/test_models.py index bbf9c795..3dd92499 100644 --- a/checklists/tests/test_models.py +++ b/checklists/tests/test_models.py @@ -18,6 +18,7 @@ from checklists.models import ( SecurityIssue, SecurityIssueReleasesThrough, SecurityRelease, + get_cvss_severity, ) from checklists.tests.factory import Factory @@ -848,6 +849,116 @@ class SecurityIssueReleaseThroughTestCase(TestCase): self.assertEqual(through2.commit_hash, "") +class GetCvssSeverityTests(TestCase): + def test_none_for_null_score(self): + self.assertEqual(get_cvss_severity(None), "NONE") + + def test_none_for_zero_score(self): + self.assertEqual(get_cvss_severity(0), "NONE") + + def test_low(self): + self.assertEqual(get_cvss_severity(0.1), "LOW") + self.assertEqual(get_cvss_severity(3.9), "LOW") + + def test_medium(self): + self.assertEqual(get_cvss_severity(4.0), "MEDIUM") + self.assertEqual(get_cvss_severity(6.9), "MEDIUM") + + def test_high(self): + self.assertEqual(get_cvss_severity(7.0), "HIGH") + self.assertEqual(get_cvss_severity(8.9), "HIGH") + + def test_critical(self): + self.assertEqual(get_cvss_severity(9.0), "CRITICAL") + self.assertEqual(get_cvss_severity(10.0), "CRITICAL") + + +class CvssMetricsInCveDataTests(TestCase): + factory = Factory() + + def _make_issue(self, **kwargs): + checklist = self.factory.make_security_checklist(releases=[]) + return self.factory.make_security_issue(checklist, **kwargs) + + def _get_metrics(self, issue): + return issue.cve_data["metrics"] + + def test_only_django_severity_when_no_cvss(self): + issue = self._make_issue() + metrics = self._get_metrics(issue) + self.assertEqual(len(metrics), 1) + self.assertIn("other", metrics[0]) + self.assertEqual(metrics[0]["other"]["type"], "Django severity rating") + + def test_v3_added_when_both_v3_fields_set(self): + issue = self._make_issue( + cvss_v3_vector_string="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + cvss_v3_score="7.5", + ) + metrics = self._get_metrics(issue) + self.assertEqual(len(metrics), 2) + cvss = metrics[1]["cvssV3_1"] + self.assertEqual(cvss["version"], "3.1") + self.assertEqual( + cvss["vectorString"], + "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + ) + self.assertEqual(cvss["baseScore"], 7.5) + self.assertEqual(cvss["baseSeverity"], "HIGH") + + def test_v4_added_when_both_v4_fields_set(self): + vector = "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N" + issue = self._make_issue(cvss_v4_vector_string=vector, cvss_v4_score="8.7") + metrics = self._get_metrics(issue) + self.assertEqual(len(metrics), 2) + cvss = metrics[1]["cvssV4_0"] + self.assertEqual(cvss["version"], "4.0") + self.assertEqual(cvss["vectorString"], vector) + self.assertEqual(cvss["baseScore"], 8.7) + self.assertEqual(cvss["baseSeverity"], "HIGH") + + def test_both_v3_and_v4_added(self): + vector_v3 = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N" + vector_v4 = "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N" + issue = self._make_issue( + cvss_v3_vector_string=vector_v3, + cvss_v3_score="7.5", + cvss_v4_vector_string=vector_v4, + cvss_v4_score="8.7", + ) + metrics = self._get_metrics(issue) + self.assertEqual(len(metrics), 3) + self.assertIn("other", metrics[0]) + self.assertIn("cvssV3_1", metrics[1]) + self.assertIn("cvssV4_0", metrics[2]) + + def test_v3_omitted_when_score_missing(self): + vector = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N" + issue = self._make_issue(cvss_v3_vector_string=vector) + metrics = self._get_metrics(issue) + self.assertEqual(len(metrics), 1) + self.assertNotIn("cvssV3_1", metrics[0]) + + def test_v3_omitted_when_vector_missing(self): + issue = self._make_issue(cvss_v3_score="7.5") + metrics = self._get_metrics(issue) + self.assertEqual(len(metrics), 1) + self.assertNotIn("cvssV3_1", metrics[0]) + + def test_v4_omitted_when_score_missing(self): + vector = "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N" + issue = self._make_issue(cvss_v4_vector_string=vector) + metrics = self._get_metrics(issue) + self.assertEqual(len(metrics), 1) + self.assertNotIn("cvssV4_0", metrics[0]) + + def test_v4_omitted_when_vector_missing(self): + issue = self._make_issue(cvss_v4_score="8.7") + metrics = self._get_metrics(issue) + self.assertEqual(len(metrics), 1) + self.assertNotIn("cvssV4_0", metrics[0]) + + class PreReleaseChecklistTestCase(BaseChecklistTestCaseMixin, TestCase): checklist_class = PreRelease status_to_version = { |
