Skip to content

Commit 4402d6e

Browse files
authored
Restore severity details tab and fix bugs (#2059)
Signed-off-by: Tushar Goel <[email protected]>
1 parent 32d9724 commit 4402d6e

File tree

9 files changed

+243
-19
lines changed

9 files changed

+243
-19
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Generated by Django 4.2.25 on 2025-12-19 08:31
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0105_packagecommitpatch_patch_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="advisoryreference",
15+
name="url",
16+
field=models.URLField(help_text="URL to the vulnerability reference", max_length=1024),
17+
),
18+
migrations.AlterField(
19+
model_name="advisoryseverity",
20+
name="value",
21+
field=models.CharField(
22+
help_text="Example: 9.0, Important, High", max_length=50, null=True
23+
),
24+
),
25+
migrations.AlterField(
26+
model_name="advisoryweakness",
27+
name="cwe_id",
28+
field=models.IntegerField(help_text="CWE id", unique=True),
29+
),
30+
migrations.AlterUniqueTogether(
31+
name="advisoryreference",
32+
unique_together={("url", "reference_type")},
33+
),
34+
migrations.AlterUniqueTogether(
35+
name="advisoryseverity",
36+
unique_together={
37+
("url", "scoring_system", "value", "scoring_elements", "published_at")
38+
},
39+
),
40+
migrations.AddConstraint(
41+
model_name="advisoryseverity",
42+
constraint=models.CheckConstraint(
43+
check=models.Q(
44+
models.Q(("value__isnull", False), models.Q(("value", ""), _negated=True)),
45+
models.Q(
46+
("scoring_elements__isnull", False),
47+
models.Q(("scoring_elements", ""), _negated=True),
48+
),
49+
_connector="OR",
50+
),
51+
name="scoring_elements_or_value_must_be_set",
52+
),
53+
),
54+
]

vulnerabilities/models.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2573,7 +2573,8 @@ class AdvisorySeverity(models.Model):
25732573
),
25742574
)
25752575

2576-
value = models.CharField(max_length=50, help_text="Example: 9.0, Important, High")
2576+
# A severity value might be missing and it may just contain scoring_elements only
2577+
value = models.CharField(max_length=50, help_text="Example: 9.0, Important, High", null=True)
25772578

25782579
scoring_elements = models.CharField(
25792580
max_length=150,
@@ -2591,6 +2592,16 @@ class AdvisorySeverity(models.Model):
25912592
class Meta:
25922593
verbose_name_plural = "Advisory severities"
25932594
ordering = ["url", "scoring_system", "value"]
2595+
unique_together = ("url", "scoring_system", "value", "scoring_elements", "published_at")
2596+
constraints = [
2597+
models.CheckConstraint(
2598+
check=(
2599+
Q(value__isnull=False) & ~Q(value="")
2600+
| Q(scoring_elements__isnull=False) & ~Q(scoring_elements="")
2601+
),
2602+
name="scoring_elements_or_value_must_be_set",
2603+
)
2604+
]
25942605

25952606
def to_dict(self):
25962607
return {
@@ -2612,7 +2623,7 @@ class AdvisoryWeakness(models.Model):
26122623
A weakness is a software weakness that is associated with a vulnerability.
26132624
"""
26142625

2615-
cwe_id = models.IntegerField(help_text="CWE id")
2626+
cwe_id = models.IntegerField(help_text="CWE id", unique=True)
26162627

26172628
cwe_by_id = {}
26182629

@@ -2659,7 +2670,6 @@ class AdvisoryReference(models.Model):
26592670
url = models.URLField(
26602671
max_length=1024,
26612672
help_text="URL to the vulnerability reference",
2662-
unique=True,
26632673
)
26642674

26652675
ADVISORY = "advisory"
@@ -2689,6 +2699,7 @@ class AdvisoryReference(models.Model):
26892699

26902700
class Meta:
26912701
ordering = ["reference_id", "url", "reference_type"]
2702+
unique_together = ("url", "reference_type")
26922703

26932704
def __str__(self):
26942705
reference_id = f" {self.reference_id}" if self.reference_id else ""

vulnerabilities/pipelines/v2_importers/gitlab_importer.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
from vulnerabilities.importer import AdvisoryData
2626
from vulnerabilities.importer import AffectedPackageV2
2727
from vulnerabilities.importer import ReferenceV2
28+
from vulnerabilities.importer import VulnerabilitySeverity
2829
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipelineV2
30+
from vulnerabilities.severity_systems import SCORING_SYSTEMS
2931
from vulnerabilities.utils import build_description
3032
from vulnerabilities.utils import get_advisory_url
3133
from vulnerabilities.utils import get_cwe_id
@@ -291,6 +293,31 @@ def parse_gitlab_advisory(
291293
fixed_version_range=fixed_version_range,
292294
)
293295

296+
cvss_v2 = gitlab_advisory.get("cvss_v2")
297+
cvss_v3 = gitlab_advisory.get("cvss_v3")
298+
severities = []
299+
if cvss_v2:
300+
severities.append(
301+
VulnerabilitySeverity(
302+
system=SCORING_SYSTEMS["cvssv2"],
303+
scoring_elements=cvss_v2,
304+
value=None,
305+
url=advisory_url,
306+
)
307+
)
308+
if cvss_v3:
309+
scoring_system = SCORING_SYSTEMS["cvssv3"]
310+
if cvss_v3.startswith("CVSS:3.1/"):
311+
scoring_system = SCORING_SYSTEMS["cvssv3.1"]
312+
severities.append(
313+
VulnerabilitySeverity(
314+
system=scoring_system,
315+
scoring_elements=cvss_v3,
316+
value=None,
317+
url=advisory_url,
318+
)
319+
)
320+
294321
return AdvisoryData(
295322
advisory_id=advisory_id,
296323
aliases=aliases,
@@ -299,6 +326,7 @@ def parse_gitlab_advisory(
299326
date_published=date_published,
300327
affected_packages=[affected_package],
301328
weaknesses=cwe_list,
329+
severities=severities,
302330
url=advisory_url,
303331
original_advisory_text=json.dumps(gitlab_advisory, indent=2, ensure_ascii=False),
304332
)

vulnerabilities/pipelines/v2_importers/npm_importer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def to_advisory_data(self, file: Path) -> Iterable[AdvisoryData]:
8888
severities.append(
8989
VulnerabilitySeverity(
9090
system=CVSSV3,
91+
scoring_elements=cvss_vector,
9192
value=cvss_score,
9293
url=f"https://github.com/nodejs/security-wg/blob/main/vuln/npm/{id}.json",
9394
)
@@ -97,6 +98,7 @@ def to_advisory_data(self, file: Path) -> Iterable[AdvisoryData]:
9798
VulnerabilitySeverity(
9899
system=CVSSV2,
99100
value=cvss_score,
101+
scoring_elements=cvss_vector,
100102
url=f"https://github.com/nodejs/security-wg/blob/main/vuln/npm/{id}.json",
101103
)
102104
)

vulnerabilities/pipelines/v2_improvers/collect_ssvc_trees.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from vulnerabilities.models import AdvisorySeverity
1717
from vulnerabilities.models import AdvisoryV2
1818
from vulnerabilities.pipelines import VulnerableCodePipeline
19-
from vulnerabilities.pipelines.v2_importers.vulnrichment_importer import VulnrichImporterPipeline
2019
from vulnerabilities.severity_systems import SCORING_SYSTEMS
2120

2221
logger = logging.getLogger(__name__)
@@ -38,7 +37,6 @@ def steps(cls):
3837
def collect_ssvc_data(self):
3938
vulnrichment_advisories = (
4039
AdvisoryV2.objects.filter(
41-
datasource_id=VulnrichImporterPipeline.pipeline_id,
4240
severities__scoring_system=SCORING_SYSTEMS["ssvc"],
4341
)
4442
.distinct()
@@ -59,6 +57,7 @@ def collect_ssvc_data(self):
5957
self.log(f"Processing advisory: {advisory.advisory_id}")
6058
for severity in advisory.severities.all():
6159
ssvc_vector = severity.scoring_elements
60+
self.log(f"SSVC Vector found: {ssvc_vector}")
6261
try:
6362
ssvc_tree, decision = convert_vector_to_tree_and_decision(ssvc_vector)
6463
self.log(
@@ -78,7 +77,7 @@ def collect_ssvc_data(self):
7877
).distinct()
7978
related_advisories = related_advisories.exclude(id=advisory.id)
8079
ssvc_obj.related_advisories.set(related_advisories)
81-
except ValueError as e:
80+
except Exception as e:
8281
logger.error(
8382
f"Failed to parse SSVC vector '{ssvc_vector}' for advisory '{advisory}': {e}"
8483
)

vulnerabilities/pipes/advisory.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,17 @@ def get_or_create_advisory_severities(severities: List) -> QuerySet:
8585
severity_objs = []
8686
for severity in severities:
8787
published_at = str(severity.published_at) if severity.published_at else None
88-
sev, _ = AdvisorySeverity.objects.get_or_create(
89-
scoring_system=severity.system.identifier,
90-
value=severity.value,
91-
scoring_elements=severity.scoring_elements,
92-
defaults={
93-
"published_at": published_at,
94-
},
95-
url=severity.url,
96-
)
97-
severity_objs.append(sev)
88+
if severity.scoring_elements or severity.value:
89+
sev, _ = AdvisorySeverity.objects.get_or_create(
90+
scoring_system=severity.system.identifier,
91+
value=severity.value,
92+
scoring_elements=severity.scoring_elements,
93+
defaults={
94+
"published_at": published_at,
95+
},
96+
url=severity.url,
97+
)
98+
severity_objs.append(sev)
9899
return AdvisorySeverity.objects.filter(id__in=[severity.id for severity in severity_objs])
99100

100101

vulnerabilities/risk.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ def get_weighted_severity(severities):
4343
weight = WEIGHT_CONFIG.get(severity_source, DEFAULT_WEIGHT)
4444
max_weight = float(weight) / 10
4545
vul_score = severity.value
46+
if not vul_score:
47+
continue
4648
try:
4749
vul_score = float(vul_score)
4850
vul_score_value = vul_score * max_weight

vulnerabilities/templates/advisory_detail.html

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@
6363
</a>
6464
</li>
6565

66+
<li data-tab="severities-vectors">
67+
<a>
68+
<span>
69+
Severity details ({{ severity_vectors|length }})
70+
</span>
71+
</a>
72+
</li>
73+
6674
{% if ssvcs %}
6775
<li data-tab="ssvcs">
6876
<a>
@@ -450,6 +458,102 @@
450458
{% endif %}
451459
</div>
452460

461+
<div class="tab-div content" data-content="severities-vectors">
462+
{% for severity_vector in severity_vectors %}
463+
{% if severity_vector.vector.version == '2.0' %}
464+
Vector: {{ severity_vector.vector.vectorString }} Found at <a href="{{ severity_vector.origin }}" target="_blank">{{ severity_vector.origin }}</a>
465+
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth gray-header-border">
466+
<tr>
467+
<th>Exploitability (E)</th>
468+
<th>Access Vector (AV)</th>
469+
<th>Access Complexity (AC)</th>
470+
<th>Authentication (Au)</th>
471+
<th>Confidentiality Impact (C)</th>
472+
<th>Integrity Impact (I)</th>
473+
<th>Availability Impact (A)</th>
474+
</tr>
475+
<tr>
476+
<td>{{ severity_vector.vector.exploitability|cvss_printer:"high,functional,unproven,proof_of_concept,not_defined" }}</td>
477+
<td>{{ severity_vector.vector.accessVector|cvss_printer:"local,adjacent_network,network" }}</td>
478+
<td>{{ severity_vector.vector.accessComplexity|cvss_printer:"high,medium,low" }}</td>
479+
<td>{{ severity_vector.vector.authentication|cvss_printer:"multiple,single,none" }}</td>
480+
<td>{{ severity_vector.vector.confidentialityImpact|cvss_printer:"none,partial,complete" }}</td>
481+
<td>{{ severity_vector.vector.integrityImpact|cvss_printer:"none,partial,complete" }}</td>
482+
<td>{{ severity_vector.vector.availabilityImpact|cvss_printer:"none,partial,complete" }}</td>
483+
</tr>
484+
</table>
485+
{% elif severity_vector.vector.version == '3.1' or severity_vector.vector.version == '3.0'%}
486+
Vector: {{ severity_vector.vector.vectorString }} Found at <a href="{{ severity_vector.origin }}" target="_blank">{{ severity_vector.origin }}</a>
487+
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth gray-header-border">
488+
<tr>
489+
<th>Attack Vector (AV)</th>
490+
<th>Attack Complexity (AC)</th>
491+
<th>Privileges Required (PR)</th>
492+
<th>User Interaction (UI)</th>
493+
<th>Scope (S)</th>
494+
<th>Confidentiality Impact (C)</th>
495+
<th>Integrity Impact (I)</th>
496+
<th>Availability Impact (A)</th>
497+
</tr>
498+
<tr>
499+
<td>{{ severity_vector.vector.attackVector|cvss_printer:"network,adjacent_network,local,physical"}}</td>
500+
<td>{{ severity_vector.vector.attackComplexity|cvss_printer:"low,high" }}</td>
501+
<td>{{ severity_vector.vector.privilegesRequired|cvss_printer:"none,low,high" }}</td>
502+
<td>{{ severity_vector.vector.userInteraction|cvss_printer:"none,required"}}</td>
503+
<td>{{ severity_vector.vector.scope|cvss_printer:"unchanged,changed" }}</td>
504+
<td>{{ severity_vector.vector.confidentialityImpact|cvss_printer:"high,low,none" }}</td>
505+
<td>{{ severity_vector.vector.integrityImpact|cvss_printer:"high,low,none" }}</td>
506+
<td>{{ severity_vector.vector.availabilityImpact|cvss_printer:"high,low,none" }}</td>
507+
</tr>
508+
</table>
509+
{% elif severity_vector.vector.version == '4' %}
510+
Vector: {{ severity_vector.vector.vectorString }} Found at <a href="{{ severity_vector.origin }}" target="_blank">{{ severity_vector.origin }}</a>
511+
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth gray-header-border">
512+
<tr>
513+
<th>Attack Vector (AV)</th>
514+
<th>Attack Complexity (AC)</th>
515+
<th>Attack Requirements (AT)</th>
516+
<th>Privileges Required (PR)</th>
517+
<th>User Interaction (UI)</th>
518+
519+
<th>Vulnerable System Impact Confidentiality (VC)</th>
520+
<th>Vulnerable System Impact Integrity (VI)</th>
521+
<th>Vulnerable System Impact Availability (VA)</th>
522+
523+
<th>Subsequent System Impact Confidentiality (SC)</th>
524+
<th>Subsequent System Impact Integrity (SI)</th>
525+
<th>Subsequent System Impact Availability (SA)</th>
526+
</tr>
527+
<tr>
528+
<td>{{ severity_vector.vector.attackVector|cvss_printer:"network,adjacent,local,physical"}}</td>
529+
<td>{{ severity_vector.vector.attackComplexity|cvss_printer:"low,high" }}</td>
530+
<td>{{ severity_vector.vector.attackRequirement|cvss_printer:"none,present" }}</td>
531+
<td>{{ severity_vector.vector.privilegesRequired|cvss_printer:"none,low,high" }}</td>
532+
<td>{{ severity_vector.vector.userInteraction|cvss_printer:"none,passive,active"}}</td>
533+
534+
<td>{{ severity_vector.vector.vulnerableSystemImpactConfidentiality|cvss_printer:"high,low,none" }}</td>
535+
<td>{{ severity_vector.vector.vulnerableSystemImpactIntegrity|cvss_printer:"high,low,none" }}</td>
536+
<td>{{ severity_vector.vector.vulnerableSystemImpactAvailability|cvss_printer:"high,low,none" }}</td>
537+
538+
<td>{{ severity_vector.vector.subsequentSystemImpactConfidentiality|cvss_printer:"high,low,none" }}</td>
539+
<td>{{ severity_vector.vector.subsequentSystemImpactIntegrity|cvss_printer:"high,low,none" }}</td>
540+
<td>{{ severity_vector.vector.subsequentSystemImpactAvailability|cvss_printer:"high,low,none" }}</td>
541+
</tr>
542+
</table>
543+
{% elif severity_vector.vector.version == 'ssvc' %}
544+
<hr/>
545+
Vector: {{ severity_vector.vector.vectorString }} Found at <a href="{{ severity_vector.origin }}" target="_blank">{{ severity_vector.origin }}</a>
546+
<hr/>
547+
{% endif %}
548+
{% empty %}
549+
<tr>
550+
<td>
551+
There are no known vectors.
552+
</td>
553+
</tr>
554+
{% endfor %}
555+
</div>
556+
453557
<div class="tab-div content" data-content="ssvcs">
454558
{% if ssvcs %}
455559
{% for ssvc in ssvcs %}

0 commit comments

Comments
 (0)