Skip to content

Commit ea03fa0

Browse files
committed
Adjust tests
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 0c4ebfe commit ea03fa0

File tree

5 files changed

+167
-162
lines changed

5 files changed

+167
-162
lines changed

vulnerabilities/api_v2.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -146,18 +146,31 @@ class AdvisoryV2Serializer(serializers.ModelSerializer):
146146
references = AdvisoryReferenceSerializer(many=True)
147147
severities = AdvisorySeveritySerializer(many=True)
148148
advisory_id = serializers.CharField(source="avid", read_only=True)
149-
ssvc_trees = serializers.SerializerMethodField()
149+
related_ssvc_trees = serializers.SerializerMethodField()
150150

151-
def get_ssvc_trees(self, obj):
152-
ssvc_trees = obj.ssvc_entries.all()
153-
return [
154-
{
155-
"vector": ssvc.vector,
156-
"decision": ssvc.decision,
157-
"options": ssvc.options,
158-
}
159-
for ssvc in ssvc_trees
160-
]
151+
def get_related_ssvc_trees(self, obj):
152+
related_ssvcs = obj.related_ssvcs.all().select_related("source_advisory")
153+
source_ssvcs = obj.source_ssvcs.all().select_related("source_advisory")
154+
155+
seen = set()
156+
result = []
157+
158+
for ssvc in list(related_ssvcs) + list(source_ssvcs):
159+
key = (ssvc.vector, ssvc.source_advisory_id)
160+
if key in seen:
161+
continue
162+
seen.add(key)
163+
164+
result.append(
165+
{
166+
"vector": ssvc.vector,
167+
"decision": ssvc.decision,
168+
"options": ssvc.options,
169+
"source_url": ssvc.source_advisory.url,
170+
}
171+
)
172+
173+
return result
161174

162175
class Meta:
163176
model = AdvisoryV2
@@ -172,7 +185,7 @@ class Meta:
172185
"exploitability",
173186
"weighted_severity",
174187
"risk_score",
175-
"ssvc_trees",
188+
"related_ssvc_trees",
176189
]
177190

178191
def get_aliases(self, obj):

vulnerabilities/pipelines/v2_improvers/collect_ssvc_trees.py

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99

1010
import logging
1111

12+
from django.db.models import Prefetch
1213
from django.db.models import Q
14+
1315
from vulnerabilities.models import SSVC
16+
from vulnerabilities.models import AdvisorySeverity
1417
from vulnerabilities.models import AdvisoryV2
1518
from vulnerabilities.pipelines import VulnerableCodePipeline
1619
from vulnerabilities.pipelines.v2_importers.vulnrichment_importer import VulnrichImporterPipeline
@@ -26,43 +29,60 @@ class CollectSSVCPipeline(VulnerableCodePipeline):
2629
This pipeline collects SSVC from Vulnrichment project and associates them with existing advisories.
2730
"""
2831

29-
pipeline_id = "collect_ssvc_tree_v2"
30-
spdx_license_expression = "CC0-1.0"
32+
pipeline_id = "collect_ssvc_trees"
3133

3234
@classmethod
3335
def steps(cls):
34-
return (
35-
cls.collect_ssvc_data,
36-
)
36+
return (cls.collect_ssvc_data,)
3737

3838
def collect_ssvc_data(self):
39-
vulnrichment_advisories = AdvisoryV2.objects.filter(
40-
datasource_id=VulnrichImporterPipeline.pipeline_id,
39+
vulnrichment_advisories = (
40+
AdvisoryV2.objects.filter(
41+
datasource_id=VulnrichImporterPipeline.pipeline_id,
42+
severities__scoring_system=SCORING_SYSTEMS["ssvc"],
43+
)
44+
.distinct()
45+
.prefetch_related(
46+
Prefetch(
47+
"severities",
48+
queryset=AdvisorySeverity.objects.filter(
49+
scoring_system=SCORING_SYSTEMS["ssvc"]
50+
),
51+
)
52+
)
53+
)
54+
55+
self.log(
56+
f"Found {vulnrichment_advisories.count()} advisories from Vulnrichment with SSVC severities."
4157
)
4258
for advisory in vulnrichment_advisories:
43-
severities = advisory.severities.filter(scoring_system=SCORING_SYSTEMS["ssvc"])
44-
for severity in severities:
59+
self.log(f"Processing advisory: {advisory.advisory_id}")
60+
for severity in advisory.severities.all():
4561
ssvc_vector = severity.scoring_elements
4662
try:
4763
ssvc_tree, decision = convert_vector_to_tree_and_decision(ssvc_vector)
48-
self.log(f"Advisory: {advisory.advisory_id}, SSVC Tree: {ssvc_tree}, Decision: {decision}, vector: {ssvc_vector}")
64+
self.log(
65+
f"Advisory: {advisory.advisory_id}, SSVC Tree: {ssvc_tree}, Decision: {decision}, vector: {ssvc_vector}"
66+
)
4967
ssvc_obj, _ = SSVC.objects.get_or_create(
5068
source_advisory=advisory,
5169
defaults={
5270
"options": ssvc_tree,
5371
"decision": decision,
72+
"vector": ssvc_vector,
5473
},
5574
)
5675
# All advisories that have advisory.advisory_id in their aliases or advisory_id same as advisory.advisory_id
5776
related_advisories = AdvisoryV2.objects.filter(
58-
Q(advisory_id=advisory.advisory_id) |
59-
Q(aliases__alias=advisory.advisory_id)
77+
Q(advisory_id=advisory.advisory_id) | Q(aliases__alias=advisory.advisory_id)
6078
).distinct()
61-
# remove the current advisory from related advisories
6279
related_advisories = related_advisories.exclude(id=advisory.id)
6380
ssvc_obj.related_advisories.set(related_advisories)
6481
except ValueError as e:
65-
logger.error(f"Failed to parse SSVC vector '{ssvc_vector}' for advisory '{advisory}': {e}")
82+
logger.error(
83+
f"Failed to parse SSVC vector '{ssvc_vector}' for advisory '{advisory}': {e}"
84+
)
85+
6686

6787
REVERSE_POINTS = {
6888
"E": ("Exploitation", {"N": "none", "P": "poc", "A": "active"}),
@@ -82,6 +102,7 @@ def collect_ssvc_data(self):
82102

83103
VECTOR_ORDER = ["E", "A", "T", "P", "B", "M"]
84104

105+
85106
def convert_vector_to_tree_and_decision(vector: str):
86107
"""
87108
Convert a given SSVC vector string into a structured tree and decision.
@@ -114,9 +135,12 @@ def convert_vector_to_tree_and_decision(vector: str):
114135
name, mapping = REVERSE_POINTS[key]
115136
options.append({name: mapping[value]})
116137

117-
# Preserve canonical SSVC order
118-
options.sort(key=lambda o: VECTOR_ORDER.index(
119-
next(k for k, _ in REVERSE_POINTS.values() if k == next(iter(o)))
120-
) if False else 0)
138+
options.sort(
139+
key=lambda o: VECTOR_ORDER.index(
140+
next(k for k, _ in REVERSE_POINTS.values() if k == next(iter(o)))
141+
)
142+
if False
143+
else 0
144+
)
121145

122146
return options, decision

vulnerabilities/templates/advisory_detail.html

Lines changed: 49 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,6 @@
4444
</span>
4545
</a>
4646
</li>
47-
<li data-tab="severities-vectors">
48-
<a>
49-
<span>
50-
Severity details ({{ severity_vectors|length }})
51-
</span>
52-
</a>
53-
</li>
5447

5548
{% if advisory.exploits %}
5649
<li data-tab="exploits">
@@ -70,6 +63,16 @@
7063
</a>
7164
</li>
7265

66+
{% if ssvcs %}
67+
<li data-tab="ssvcs">
68+
<a>
69+
<span>
70+
Related SSVCS ({{ ssvcs|length }})
71+
</span>
72+
</a>
73+
</li>
74+
{% endif %}
75+
7376
<!-- <li data-tab="history">
7477
<a>
7578
<span>
@@ -402,102 +405,6 @@
402405
</tr>
403406
{% endfor %}
404407
</div>
405-
406-
<div class="tab-div content" data-content="severities-vectors">
407-
{% for severity_vector in severity_vectors %}
408-
{% if severity_vector.vector.version == '2.0' %}
409-
Vector: {{ severity_vector.vector.vectorString }} Found at <a href="{{ severity_vector.origin }}" target="_blank">{{ severity_vector.origin }}</a>
410-
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth gray-header-border">
411-
<tr>
412-
<th>Exploitability (E)</th>
413-
<th>Access Vector (AV)</th>
414-
<th>Access Complexity (AC)</th>
415-
<th>Authentication (Au)</th>
416-
<th>Confidentiality Impact (C)</th>
417-
<th>Integrity Impact (I)</th>
418-
<th>Availability Impact (A)</th>
419-
</tr>
420-
<tr>
421-
<td>{{ severity_vector.vector.exploitability|cvss_printer:"high,functional,unproven,proof_of_concept,not_defined" }}</td>
422-
<td>{{ severity_vector.vector.accessVector|cvss_printer:"local,adjacent_network,network" }}</td>
423-
<td>{{ severity_vector.vector.accessComplexity|cvss_printer:"high,medium,low" }}</td>
424-
<td>{{ severity_vector.vector.authentication|cvss_printer:"multiple,single,none" }}</td>
425-
<td>{{ severity_vector.vector.confidentialityImpact|cvss_printer:"none,partial,complete" }}</td>
426-
<td>{{ severity_vector.vector.integrityImpact|cvss_printer:"none,partial,complete" }}</td>
427-
<td>{{ severity_vector.vector.availabilityImpact|cvss_printer:"none,partial,complete" }}</td>
428-
</tr>
429-
</table>
430-
{% elif severity_vector.vector.version == '3.1' or severity_vector.vector.version == '3.0'%}
431-
Vector: {{ severity_vector.vector.vectorString }} Found at <a href="{{ severity_vector.origin }}" target="_blank">{{ severity_vector.origin }}</a>
432-
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth gray-header-border">
433-
<tr>
434-
<th>Attack Vector (AV)</th>
435-
<th>Attack Complexity (AC)</th>
436-
<th>Privileges Required (PR)</th>
437-
<th>User Interaction (UI)</th>
438-
<th>Scope (S)</th>
439-
<th>Confidentiality Impact (C)</th>
440-
<th>Integrity Impact (I)</th>
441-
<th>Availability Impact (A)</th>
442-
</tr>
443-
<tr>
444-
<td>{{ severity_vector.vector.attackVector|cvss_printer:"network,adjacent_network,local,physical"}}</td>
445-
<td>{{ severity_vector.vector.attackComplexity|cvss_printer:"low,high" }}</td>
446-
<td>{{ severity_vector.vector.privilegesRequired|cvss_printer:"none,low,high" }}</td>
447-
<td>{{ severity_vector.vector.userInteraction|cvss_printer:"none,required"}}</td>
448-
<td>{{ severity_vector.vector.scope|cvss_printer:"unchanged,changed" }}</td>
449-
<td>{{ severity_vector.vector.confidentialityImpact|cvss_printer:"high,low,none" }}</td>
450-
<td>{{ severity_vector.vector.integrityImpact|cvss_printer:"high,low,none" }}</td>
451-
<td>{{ severity_vector.vector.availabilityImpact|cvss_printer:"high,low,none" }}</td>
452-
</tr>
453-
</table>
454-
{% elif severity_vector.vector.version == '4' %}
455-
Vector: {{ severity_vector.vector.vectorString }} Found at <a href="{{ severity_vector.origin }}" target="_blank">{{ severity_vector.origin }}</a>
456-
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth gray-header-border">
457-
<tr>
458-
<th>Attack Vector (AV)</th>
459-
<th>Attack Complexity (AC)</th>
460-
<th>Attack Requirements (AT)</th>
461-
<th>Privileges Required (PR)</th>
462-
<th>User Interaction (UI)</th>
463-
464-
<th>Vulnerable System Impact Confidentiality (VC)</th>
465-
<th>Vulnerable System Impact Integrity (VI)</th>
466-
<th>Vulnerable System Impact Availability (VA)</th>
467-
468-
<th>Subsequent System Impact Confidentiality (SC)</th>
469-
<th>Subsequent System Impact Integrity (SI)</th>
470-
<th>Subsequent System Impact Availability (SA)</th>
471-
</tr>
472-
<tr>
473-
<td>{{ severity_vector.vector.attackVector|cvss_printer:"network,adjacent,local,physical"}}</td>
474-
<td>{{ severity_vector.vector.attackComplexity|cvss_printer:"low,high" }}</td>
475-
<td>{{ severity_vector.vector.attackRequirement|cvss_printer:"none,present" }}</td>
476-
<td>{{ severity_vector.vector.privilegesRequired|cvss_printer:"none,low,high" }}</td>
477-
<td>{{ severity_vector.vector.userInteraction|cvss_printer:"none,passive,active"}}</td>
478-
479-
<td>{{ severity_vector.vector.vulnerableSystemImpactConfidentiality|cvss_printer:"high,low,none" }}</td>
480-
<td>{{ severity_vector.vector.vulnerableSystemImpactIntegrity|cvss_printer:"high,low,none" }}</td>
481-
<td>{{ severity_vector.vector.vulnerableSystemImpactAvailability|cvss_printer:"high,low,none" }}</td>
482-
483-
<td>{{ severity_vector.vector.subsequentSystemImpactConfidentiality|cvss_printer:"high,low,none" }}</td>
484-
<td>{{ severity_vector.vector.subsequentSystemImpactIntegrity|cvss_printer:"high,low,none" }}</td>
485-
<td>{{ severity_vector.vector.subsequentSystemImpactAvailability|cvss_printer:"high,low,none" }}</td>
486-
</tr>
487-
</table>
488-
{% elif severity_vector.vector.version == 'ssvc' %}
489-
<hr/>
490-
Vector: {{ severity_vector.vector.vectorString }} Found at <a href="{{ severity_vector.origin }}" target="_blank">{{ severity_vector.origin }}</a>
491-
<hr/>
492-
{% endif %}
493-
{% empty %}
494-
<tr>
495-
<td>
496-
There are no known vectors.
497-
</td>
498-
</tr>
499-
{% endfor %}
500-
</div>
501408

502409

503410
<div class="tab-div content" data-content="epss">
@@ -543,6 +450,45 @@
543450
{% endif %}
544451
</div>
545452

453+
<div class="tab-div content" data-content="ssvcs">
454+
{% if ssvcs %}
455+
{% for ssvc in ssvcs %}
456+
<div class="box ssvc-card mb-0 pb-0">
457+
<div>
458+
<p>
459+
Vector: {{ ssvc.vector }}
460+
</p>
461+
<p>
462+
Decision: {{ ssvc.decision }}
463+
</p>
464+
<p>
465+
Source URL:
466+
<a href="{{ ssvc.advisory_url }}" target="_blank">
467+
{{ ssvc.advisory_url }}
468+
<i class="fa fa-external-link fa_link_custom"></i>
469+
</a>
470+
</p>
471+
<p>
472+
Source Advisory:
473+
<a href="{{ ssvc.advisory.get_absolute_url }}">
474+
{{ ssvc.advisory.avid }}
475+
<i class="fa fa-external-link fa_link_custom"></i>
476+
</a>
477+
</p>
478+
<details>
479+
<summary class="is-size-7 has-text-link" style="cursor: pointer;">
480+
View SSVC decision tree
481+
</summary>
482+
<pre>{{ ssvc.options|pprint }}</pre>
483+
</details>
484+
</div>
485+
</div>
486+
{% endfor %}
487+
{% else %}
488+
<p>There are no SSVC decisions available.</p>
489+
{% endif %}
490+
</div>
491+
546492
<!-- <div class="tab-div content" data-content="history">
547493
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
548494
<thead>

vulnerabilities/tests/test_api_v2.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ def setUp(self):
857857

858858
def test_list_with_purl_filter(self):
859859
url = reverse("advisories-package-v2-list")
860-
with self.assertNumQueries(17):
860+
with self.assertNumQueries(19):
861861
response = self.client.get(url, {"purl": "pkg:pypi/sample@1.0.0"})
862862
assert response.status_code == 200
863863
assert "packages" in response.data["results"]
@@ -866,7 +866,7 @@ def test_list_with_purl_filter(self):
866866

867867
def test_bulk_lookup(self):
868868
url = reverse("advisories-package-v2-bulk-lookup")
869-
with self.assertNumQueries(16):
869+
with self.assertNumQueries(18):
870870
response = self.client.post(url, {"purls": ["pkg:pypi/sample@1.0.0"]}, format="json")
871871
assert response.status_code == 200
872872
assert "packages" in response.data
@@ -876,7 +876,7 @@ def test_bulk_lookup(self):
876876
def test_bulk_search_plain(self):
877877
url = reverse("advisories-package-v2-bulk-search")
878878
payload = {"purls": ["pkg:pypi/sample@1.0.0"], "plain_purl": True, "purl_only": False}
879-
with self.assertNumQueries(16):
879+
with self.assertNumQueries(18):
880880
response = self.client.post(url, payload, format="json")
881881
assert response.status_code == 200
882882
assert "packages" in response.data
@@ -885,14 +885,14 @@ def test_bulk_search_plain(self):
885885
def test_bulk_search_purl_only(self):
886886
url = reverse("advisories-package-v2-bulk-search")
887887
payload = {"purls": ["pkg:pypi/sample@1.0.0"], "plain_purl": False, "purl_only": True}
888-
with self.assertNumQueries(14):
888+
with self.assertNumQueries(16):
889889
response = self.client.post(url, payload, format="json")
890890
assert response.status_code == 200
891891
assert "pkg:pypi/sample@1.0.0" in response.data
892892

893893
def test_lookup_single_package(self):
894894
url = reverse("advisories-package-v2-lookup")
895-
with self.assertNumQueries(12):
895+
with self.assertNumQueries(16):
896896
response = self.client.post(url, {"purl": "pkg:pypi/sample@1.0.0"}, format="json")
897897
assert response.status_code == 200
898898
assert any(pkg["purl"] == "pkg:pypi/sample@1.0.0" for pkg in response.data)

0 commit comments

Comments
 (0)