diff --git a/vulnerabilities/migrations/0106_alter_advisoryreference_url_and_more.py b/vulnerabilities/migrations/0106_alter_advisoryreference_url_and_more.py
new file mode 100644
index 000000000..cd13721e9
--- /dev/null
+++ b/vulnerabilities/migrations/0106_alter_advisoryreference_url_and_more.py
@@ -0,0 +1,54 @@
+# Generated by Django 4.2.25 on 2025-12-19 08:31
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("vulnerabilities", "0105_packagecommitpatch_patch_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="advisoryreference",
+ name="url",
+ field=models.URLField(help_text="URL to the vulnerability reference", max_length=1024),
+ ),
+ migrations.AlterField(
+ model_name="advisoryseverity",
+ name="value",
+ field=models.CharField(
+ help_text="Example: 9.0, Important, High", max_length=50, null=True
+ ),
+ ),
+ migrations.AlterField(
+ model_name="advisoryweakness",
+ name="cwe_id",
+ field=models.IntegerField(help_text="CWE id", unique=True),
+ ),
+ migrations.AlterUniqueTogether(
+ name="advisoryreference",
+ unique_together={("url", "reference_type")},
+ ),
+ migrations.AlterUniqueTogether(
+ name="advisoryseverity",
+ unique_together={
+ ("url", "scoring_system", "value", "scoring_elements", "published_at")
+ },
+ ),
+ migrations.AddConstraint(
+ model_name="advisoryseverity",
+ constraint=models.CheckConstraint(
+ check=models.Q(
+ models.Q(("value__isnull", False), models.Q(("value", ""), _negated=True)),
+ models.Q(
+ ("scoring_elements__isnull", False),
+ models.Q(("scoring_elements", ""), _negated=True),
+ ),
+ _connector="OR",
+ ),
+ name="scoring_elements_or_value_must_be_set",
+ ),
+ ),
+ ]
diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py
index 4c2cf5499..f4b21eb49 100644
--- a/vulnerabilities/models.py
+++ b/vulnerabilities/models.py
@@ -2573,7 +2573,8 @@ class AdvisorySeverity(models.Model):
),
)
- value = models.CharField(max_length=50, help_text="Example: 9.0, Important, High")
+ # A severity value might be missing and it may just contain scoring_elements only
+ value = models.CharField(max_length=50, help_text="Example: 9.0, Important, High", null=True)
scoring_elements = models.CharField(
max_length=150,
@@ -2591,6 +2592,16 @@ class AdvisorySeverity(models.Model):
class Meta:
verbose_name_plural = "Advisory severities"
ordering = ["url", "scoring_system", "value"]
+ unique_together = ("url", "scoring_system", "value", "scoring_elements", "published_at")
+ constraints = [
+ models.CheckConstraint(
+ check=(
+ Q(value__isnull=False) & ~Q(value="")
+ | Q(scoring_elements__isnull=False) & ~Q(scoring_elements="")
+ ),
+ name="scoring_elements_or_value_must_be_set",
+ )
+ ]
def to_dict(self):
return {
@@ -2612,7 +2623,7 @@ class AdvisoryWeakness(models.Model):
A weakness is a software weakness that is associated with a vulnerability.
"""
- cwe_id = models.IntegerField(help_text="CWE id")
+ cwe_id = models.IntegerField(help_text="CWE id", unique=True)
cwe_by_id = {}
@@ -2659,7 +2670,6 @@ class AdvisoryReference(models.Model):
url = models.URLField(
max_length=1024,
help_text="URL to the vulnerability reference",
- unique=True,
)
ADVISORY = "advisory"
@@ -2689,6 +2699,7 @@ class AdvisoryReference(models.Model):
class Meta:
ordering = ["reference_id", "url", "reference_type"]
+ unique_together = ("url", "reference_type")
def __str__(self):
reference_id = f" {self.reference_id}" if self.reference_id else ""
diff --git a/vulnerabilities/pipelines/v2_importers/gitlab_importer.py b/vulnerabilities/pipelines/v2_importers/gitlab_importer.py
index e28ab7520..a32b6a6c3 100644
--- a/vulnerabilities/pipelines/v2_importers/gitlab_importer.py
+++ b/vulnerabilities/pipelines/v2_importers/gitlab_importer.py
@@ -25,7 +25,9 @@
from vulnerabilities.importer import AdvisoryData
from vulnerabilities.importer import AffectedPackageV2
from vulnerabilities.importer import ReferenceV2
+from vulnerabilities.importer import VulnerabilitySeverity
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2
+from vulnerabilities.severity_systems import SCORING_SYSTEMS
from vulnerabilities.utils import build_description
from vulnerabilities.utils import get_advisory_url
from vulnerabilities.utils import get_cwe_id
@@ -291,6 +293,31 @@ def parse_gitlab_advisory(
fixed_version_range=fixed_version_range,
)
+ cvss_v2 = gitlab_advisory.get("cvss_v2")
+ cvss_v3 = gitlab_advisory.get("cvss_v3")
+ severities = []
+ if cvss_v2:
+ severities.append(
+ VulnerabilitySeverity(
+ system=SCORING_SYSTEMS["cvssv2"],
+ scoring_elements=cvss_v2,
+ value=None,
+ url=advisory_url,
+ )
+ )
+ if cvss_v3:
+ scoring_system = SCORING_SYSTEMS["cvssv3"]
+ if cvss_v3.startswith("CVSS:3.1/"):
+ scoring_system = SCORING_SYSTEMS["cvssv3.1"]
+ severities.append(
+ VulnerabilitySeverity(
+ system=scoring_system,
+ scoring_elements=cvss_v3,
+ value=None,
+ url=advisory_url,
+ )
+ )
+
return AdvisoryData(
advisory_id=advisory_id,
aliases=aliases,
@@ -299,6 +326,7 @@ def parse_gitlab_advisory(
date_published=date_published,
affected_packages=[affected_package],
weaknesses=cwe_list,
+ severities=severities,
url=advisory_url,
original_advisory_text=json.dumps(gitlab_advisory, indent=2, ensure_ascii=False),
)
diff --git a/vulnerabilities/pipelines/v2_importers/npm_importer.py b/vulnerabilities/pipelines/v2_importers/npm_importer.py
index bf9de86d1..0e3ff7f13 100644
--- a/vulnerabilities/pipelines/v2_importers/npm_importer.py
+++ b/vulnerabilities/pipelines/v2_importers/npm_importer.py
@@ -88,6 +88,7 @@ def to_advisory_data(self, file: Path) -> Iterable[AdvisoryData]:
severities.append(
VulnerabilitySeverity(
system=CVSSV3,
+ scoring_elements=cvss_vector,
value=cvss_score,
url=f"https://github.com/nodejs/security-wg/blob/main/vuln/npm/{id}.json",
)
@@ -97,6 +98,7 @@ def to_advisory_data(self, file: Path) -> Iterable[AdvisoryData]:
VulnerabilitySeverity(
system=CVSSV2,
value=cvss_score,
+ scoring_elements=cvss_vector,
url=f"https://github.com/nodejs/security-wg/blob/main/vuln/npm/{id}.json",
)
)
diff --git a/vulnerabilities/pipelines/v2_improvers/collect_ssvc_trees.py b/vulnerabilities/pipelines/v2_improvers/collect_ssvc_trees.py
index 4afe8b2a1..2b82b667c 100644
--- a/vulnerabilities/pipelines/v2_improvers/collect_ssvc_trees.py
+++ b/vulnerabilities/pipelines/v2_improvers/collect_ssvc_trees.py
@@ -16,7 +16,6 @@
from vulnerabilities.models import AdvisorySeverity
from vulnerabilities.models import AdvisoryV2
from vulnerabilities.pipelines import VulnerableCodePipeline
-from vulnerabilities.pipelines.v2_importers.vulnrichment_importer import VulnrichImporterPipeline
from vulnerabilities.severity_systems import SCORING_SYSTEMS
logger = logging.getLogger(__name__)
@@ -38,7 +37,6 @@ def steps(cls):
def collect_ssvc_data(self):
vulnrichment_advisories = (
AdvisoryV2.objects.filter(
- datasource_id=VulnrichImporterPipeline.pipeline_id,
severities__scoring_system=SCORING_SYSTEMS["ssvc"],
)
.distinct()
@@ -59,6 +57,7 @@ def collect_ssvc_data(self):
self.log(f"Processing advisory: {advisory.advisory_id}")
for severity in advisory.severities.all():
ssvc_vector = severity.scoring_elements
+ self.log(f"SSVC Vector found: {ssvc_vector}")
try:
ssvc_tree, decision = convert_vector_to_tree_and_decision(ssvc_vector)
self.log(
@@ -78,7 +77,7 @@ def collect_ssvc_data(self):
).distinct()
related_advisories = related_advisories.exclude(id=advisory.id)
ssvc_obj.related_advisories.set(related_advisories)
- except ValueError as e:
+ except Exception as e:
logger.error(
f"Failed to parse SSVC vector '{ssvc_vector}' for advisory '{advisory}': {e}"
)
diff --git a/vulnerabilities/pipes/advisory.py b/vulnerabilities/pipes/advisory.py
index 8f7b33353..b5405ecbf 100644
--- a/vulnerabilities/pipes/advisory.py
+++ b/vulnerabilities/pipes/advisory.py
@@ -85,16 +85,17 @@ def get_or_create_advisory_severities(severities: List) -> QuerySet:
severity_objs = []
for severity in severities:
published_at = str(severity.published_at) if severity.published_at else None
- sev, _ = AdvisorySeverity.objects.get_or_create(
- scoring_system=severity.system.identifier,
- value=severity.value,
- scoring_elements=severity.scoring_elements,
- defaults={
- "published_at": published_at,
- },
- url=severity.url,
- )
- severity_objs.append(sev)
+ if severity.scoring_elements or severity.value:
+ sev, _ = AdvisorySeverity.objects.get_or_create(
+ scoring_system=severity.system.identifier,
+ value=severity.value,
+ scoring_elements=severity.scoring_elements,
+ defaults={
+ "published_at": published_at,
+ },
+ url=severity.url,
+ )
+ severity_objs.append(sev)
return AdvisorySeverity.objects.filter(id__in=[severity.id for severity in severity_objs])
diff --git a/vulnerabilities/risk.py b/vulnerabilities/risk.py
index 04af140f0..0628422bb 100644
--- a/vulnerabilities/risk.py
+++ b/vulnerabilities/risk.py
@@ -43,6 +43,8 @@ def get_weighted_severity(severities):
weight = WEIGHT_CONFIG.get(severity_source, DEFAULT_WEIGHT)
max_weight = float(weight) / 10
vul_score = severity.value
+ if not vul_score:
+ continue
try:
vul_score = float(vul_score)
vul_score_value = vul_score * max_weight
diff --git a/vulnerabilities/templates/advisory_detail.html b/vulnerabilities/templates/advisory_detail.html
index 976ae80de..24a4b0d2c 100644
--- a/vulnerabilities/templates/advisory_detail.html
+++ b/vulnerabilities/templates/advisory_detail.html
@@ -63,6 +63,14 @@
+
+
+
+ Severity details ({{ severity_vectors|length }})
+
+
+
+
{% if ssvcs %}
@@ -450,6 +458,102 @@
{% endif %}
+
+ {% for severity_vector in severity_vectors %}
+ {% if severity_vector.vector.version == '2.0' %}
+ Vector: {{ severity_vector.vector.vectorString }} Found at
{{ severity_vector.origin }}
+
+ {% elif severity_vector.vector.version == '3.1' or severity_vector.vector.version == '3.0'%}
+ Vector: {{ severity_vector.vector.vectorString }} Found at
{{ severity_vector.origin }}
+
+ {% elif severity_vector.vector.version == '4' %}
+ Vector: {{ severity_vector.vector.vectorString }} Found at
{{ severity_vector.origin }}
+
+ {% elif severity_vector.vector.version == 'ssvc' %}
+
+ Vector: {{ severity_vector.vector.vectorString }} Found at
{{ severity_vector.origin }}
+
+ {% endif %}
+ {% empty %}
+
+ |
+ There are no known vectors.
+ |
+
+ {% endfor %}
+
+
{% if ssvcs %}
{% for ssvc in ssvcs %}
diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py
index 7cdc0cd4f..8a77df84b 100644
--- a/vulnerabilities/views.py
+++ b/vulnerabilities/views.py
@@ -429,8 +429,10 @@ def get_context_data(self, **kwargs):
if weakness_object.weakness
]
- valid_severities = self.object.severities.exclude(scoring_system=EPSS.identifier).filter(
- scoring_elements__isnull=False, scoring_system__in=SCORING_SYSTEMS.keys()
+ valid_severities = (
+ self.object.severities.exclude(scoring_system=EPSS.identifier)
+ .filter(scoring_elements__isnull=False, scoring_system__in=SCORING_SYSTEMS.values())
+ .exclude(scoring_elements="")
)
epss_severity = advisory.severities.filter(scoring_system="epss").first()
@@ -445,6 +447,27 @@ def get_context_data(self, **kwargs):
ssvc_entries = []
seen = set()
+ severity_vectors = []
+
+ for severity in valid_severities:
+ try:
+ vector_values_system = SCORING_SYSTEMS.get(severity.scoring_system)
+ if not vector_values_system:
+ logging.error(f"Unknown scoring system: {severity.scoring_system}")
+ continue
+ if vector_values_system.identifier in ["cvssv3.1_qr"]:
+ continue
+ vector_values = vector_values_system.get(severity.scoring_elements)
+ if vector_values:
+ severity_vectors.append({"vector": vector_values, "origin": severity.url})
+ except (
+ CVSS2MalformedError,
+ CVSS3MalformedError,
+ CVSS4MalformedError,
+ NotImplementedError,
+ ):
+ logging.error(f"CVSSMalformedError for {severity.scoring_elements}")
+
def add_ssvc(ssvc):
key = (ssvc.vector, ssvc.source_advisory_id)
if key in seen:
@@ -473,9 +496,9 @@ def add_ssvc(ssvc):
"severities": list(advisory.severities.all()),
"references": list(advisory.references.all()),
"aliases": list(advisory.aliases.all()),
+ "severity_vectors": severity_vectors,
"weaknesses": weaknesses_present_in_db,
"status": advisory.get_status_label,
- # "history": advisory.history,
"epss_data": epss_data,
}
)