summaryrefslogtreecommitdiff
path: root/checklists
diff options
context:
space:
mode:
authorNatalia <124304+nessita@users.noreply.github.com>2026-04-15 22:01:17 -0300
committerJacob Walls <jacobtylerwalls@gmail.com>2026-04-19 16:47:14 +0300
commit7760fe66ab10a9fad618163787119dcfb876b089 (patch)
tree21267f3a2e2c7b08603f85b2c1ac6eadc9a38f82 /checklists
parentab5429a66410966cebbce2616e216abf02f50b32 (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.
Diffstat (limited to 'checklists')
-rw-r--r--checklists/admin.py62
-rw-r--r--checklists/migrations/0004_securityissue_cvss_v3_score_and_more.py68
-rw-r--r--checklists/migrations/0005_populate_cvss_v4_fields.py37
-rw-r--r--checklists/migrations/0006_remove_securityissue_attack_complexity_and_more.py85
-rw-r--r--checklists/models.py342
-rw-r--r--checklists/tests/test_models.py111
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 = {