Skip to content

Commit 08df01d

Browse files
committed
Add SSVC trees, resource URL and max_advisories
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent f641395 commit 08df01d

3 files changed

Lines changed: 151 additions & 19 deletions

File tree

vulnerabilities/api_v3.py

Lines changed: 115 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,19 @@
2323
from rest_framework.reverse import reverse
2424
from rest_framework.throttling import AnonRateThrottle
2525

26+
from vulnerabilities.models import SSVC
2627
from vulnerabilities.models import AdvisoryAlias
2728
from vulnerabilities.models import AdvisoryReference
2829
from vulnerabilities.models import AdvisorySet
2930
from vulnerabilities.models import AdvisorySetMember
3031
from vulnerabilities.models import AdvisorySeverity
3132
from vulnerabilities.models import AdvisoryV2
3233
from vulnerabilities.models import AdvisoryWeakness
33-
from vulnerabilities.models import Group
3434
from vulnerabilities.models import GroupedAdvisory
3535
from vulnerabilities.models import ImpactedPackageAffecting
3636
from vulnerabilities.models import PackageV2
3737
from vulnerabilities.throttling import PermissionBasedUserRateThrottle
3838
from vulnerabilities.utils import TYPES_WITH_MULTIPLE_IMPORTERS
39-
from vulnerabilities.utils import get_advisories_from_groups
4039
from vulnerabilities.utils import merge_and_save_grouped_advisories
4140

4241

@@ -48,6 +47,7 @@ class PackageQuerySerializer(serializers.Serializer):
4847
)
4948
details = serializers.BooleanField(default=False)
5049
ignore_qualifiers_subpath = serializers.BooleanField(default=False)
50+
max_advisories = serializers.IntegerField(default=100, min_value=1, max_value=10000)
5151

5252
def validate(self, data):
5353
if not data["purls"]:
@@ -229,11 +229,17 @@ def get_affected_by_vulnerabilities(self, package):
229229
for adv in advisories:
230230
fixed = impact_map.get(adv["avid"])
231231
adv.pop("avid", None)
232+
resource_url = None
233+
234+
if request := self.context.get("request", None):
235+
resource_url = adv.pop("resource_url", None)
236+
resource_url = request.build_absolute_uri(location=resource_url)
232237

233238
result.append(
234239
{
235240
**adv,
236241
"fixed_by_packages": fixed,
242+
"resource_url": resource_url,
237243
}
238244
)
239245

@@ -247,9 +253,20 @@ def get_affected_by_vulnerabilities(self, package):
247253
advisories_ids = advisories_qs.only("id")
248254

249255
advisories_ids = list(advisories_ids[:101])
250-
if len(advisories_ids) > 100:
256+
if len(advisories_ids) > self.context.get("max_advisories", 100):
251257
return None
252258

259+
advisories_qs = advisories_qs.prefetch_related(
260+
"aliases",
261+
Prefetch(
262+
"related_ssvcs",
263+
queryset=SSVC.objects.select_related("source_advisory").only(
264+
"id", "decision", "options", "vector", "source_advisory__url"
265+
),
266+
to_attr="prefetched_ssvc_trees",
267+
),
268+
)
269+
253270
advisory_by_avid = {adv.avid: adv for adv in advisories_qs}
254271
avids = advisory_by_avid.keys()
255272

@@ -265,8 +282,14 @@ def get_affected_by_vulnerabilities(self, package):
265282

266283
for advisory in advisories_qs:
267284
impact = impact_by_avid.get(advisory.avid)
268-
if not impact:
269-
continue
285+
fixed_by_packages = []
286+
if impact:
287+
fixed_by_packages = [pkg.purl for pkg in impact.fixed_by_packages.all()]
288+
289+
resource_url = None
290+
291+
if request := self.context.get("request", None):
292+
resource_url = request.build_absolute_uri(location=advisory.get_absolute_url())
270293

271294
result.append(
272295
{
@@ -277,7 +300,17 @@ def get_affected_by_vulnerabilities(self, package):
277300
"severity": advisory.weighted_severity,
278301
"exploitability": advisory.exploitability,
279302
"risk_score": advisory.risk_score,
280-
"fixed_by_packages": [pkg.purl for pkg in impact.fixed_by_packages.all()],
303+
"fixed_by_packages": fixed_by_packages,
304+
"resource_url": resource_url,
305+
"ssvc_trees": [
306+
{
307+
"vector": ssvc.vector,
308+
"decision": ssvc.decision,
309+
"options": ssvc.options,
310+
"source_url": ssvc.source_advisory.url,
311+
}
312+
for ssvc in advisory.prefetched_ssvc_trees
313+
],
281314
}
282315
)
283316

@@ -297,25 +330,38 @@ def get_affected_by_vulnerabilities(self, package):
297330

298331
def get_fixing_vulnerabilities(self, package):
299332
advisories = self.context["fixing_advisory_map"].get(package.id, [])
300-
if advisories:
301-
return advisories
333+
results = []
334+
for advisory in advisories:
335+
if request := self.context.get("request", None):
336+
resource_url = request.build_absolute_uri(location=advisory["resource_url"])
337+
results.append(
338+
{
339+
"advisory_id": advisory["advisory_id"],
340+
"resource_url": resource_url,
341+
}
342+
)
343+
if results:
344+
return results
302345

303346
advisories_qs = AdvisoryV2.objects.latest_fixed_by_advisories_for_purl(package.package_url)
304347

305348
if not package.type in TYPES_WITH_MULTIPLE_IMPORTERS:
306349
advisories_ids = advisories_qs.only("id")
307350

308351
advisories_ids = list(advisories_ids[:101])
309-
if len(advisories_ids) > 100:
352+
if len(advisories_ids) > self.context.get("max_advisories", 100):
310353
return None
311354

312355
results = []
313356

314357
for advisory in advisories_qs:
358+
if request := self.context.get("request", None):
359+
resource_url = request.build_absolute_uri(location=advisory.get_absolute_url())
315360
results.append(
316361
{
317362
"advisory_id": advisory.advisory_id.split("/")[-1],
318363
"advisory_uid": advisory.avid,
364+
"resource_url": resource_url,
319365
}
320366
)
321367
return results
@@ -337,10 +383,16 @@ def return_fixing_advisories_data(self, advisories):
337383
result = []
338384
for advisory in advisories:
339385
assert isinstance(advisory, GroupedAdvisory)
386+
resource_url = None
387+
if request := self.context.get("request", None):
388+
resource_url = request.build_absolute_uri(
389+
location=advisory.advisory.get_absolute_url()
390+
)
340391
result.append(
341392
{
342393
"advisory_id": advisory.identifier,
343394
"advisory_uid": advisory.advisory.avid,
395+
"resource_url": resource_url,
344396
}
345397
)
346398

@@ -361,9 +413,15 @@ def return_advisories_data(self, package, advisories_qs, advisories):
361413
result = []
362414
for advisory in advisories:
363415
assert isinstance(advisory, GroupedAdvisory)
416+
resource_url = None
417+
fixed_by_packages = []
418+
if request := self.context.get("request", None):
419+
resource_url = request.build_absolute_uri(
420+
location=advisory.advisory.get_absolute_url()
421+
)
364422
impact = impact_by_avid.get(advisory.advisory.avid)
365423
if not impact:
366-
continue
424+
fixed_by_packages = list(set([pkg.purl for pkg in impact.fixed_by_packages.all()]))
367425

368426
result.append(
369427
{
@@ -374,9 +432,9 @@ def return_advisories_data(self, package, advisories_qs, advisories):
374432
"exploitability": advisory.exploitability,
375433
"risk_score": advisory.risk_score,
376434
"summary": advisory.advisory.summary,
377-
"fixed_by_packages": list(
378-
set([pkg.purl for pkg in impact.fixed_by_packages.all()])
379-
),
435+
"fixed_by_packages": fixed_by_packages,
436+
"resource_url": resource_url,
437+
"ssvc_trees": advisory.ssvc_trees,
380438
}
381439
)
382440

@@ -405,6 +463,7 @@ def create(self, request, *args, **kwargs):
405463
purls = serializer.validated_data["purls"]
406464
details = serializer.validated_data["details"]
407465
ignore_qualifiers_subpath = serializer.validated_data["ignore_qualifiers_subpath"]
466+
max_advisories = serializer.validated_data["max_advisories"]
408467

409468
if not purls:
410469
impacted = ImpactedPackageAffecting.objects.filter(package_id=OuterRef("id"))
@@ -469,6 +528,7 @@ def create(self, request, *args, **kwargs):
469528
"advisory_map": affected_advisory_map,
470529
"impact_map": impact_map,
471530
"fixing_advisory_map": fixing_advisory_map,
531+
"max_advisories": max_advisories,
472532
},
473533
)
474534
return self.get_paginated_response(serializer.data)
@@ -583,7 +643,25 @@ def get_affected_advisories_bulk(packages):
583643
relation_type="affecting",
584644
)
585645
.select_related("primary_advisory")
586-
.prefetch_related(Prefetch("aliases", queryset=AdvisoryAlias.objects.only("alias")))
646+
.prefetch_related(
647+
Prefetch("aliases", queryset=AdvisoryAlias.objects.only("alias")),
648+
Prefetch(
649+
"members",
650+
queryset=AdvisorySetMember.objects.select_related("advisory").prefetch_related(
651+
Prefetch(
652+
"advisory__related_ssvcs",
653+
queryset=SSVC.objects.select_related("source_advisory").only(
654+
"id",
655+
"options",
656+
"decision",
657+
"vector",
658+
"source_advisory__url",
659+
),
660+
to_attr="prefetched_ssvc_trees",
661+
)
662+
),
663+
),
664+
)
587665
.annotate(
588666
max_severity=Max(
589667
"members__advisory__weighted_severity",
@@ -627,6 +705,20 @@ def get_affected_advisories_bulk(packages):
627705
identifier = primary.advisory_id.split("/")[-1]
628706

629707
aliases = [a for a in adv._aliases_cache if a != identifier]
708+
all_ssvc = []
709+
710+
for member in adv.members.all():
711+
all_ssvc.extend(member.advisory.prefetched_ssvc_trees)
712+
713+
for ssvc in all_ssvc:
714+
all_ssvc.append(
715+
{
716+
"vector": ssvc.vector,
717+
"decision": ssvc.decision,
718+
"options": ssvc.options,
719+
"source_url": ssvc.source_advisory.url,
720+
}
721+
)
630722

631723
grouped.append(
632724
{
@@ -637,6 +729,8 @@ def get_affected_advisories_bulk(packages):
637729
"exploitability": exploitability,
638730
"risk_score": risk_score,
639731
"summary": primary.summary,
732+
"resource_url": primary.get_absolute_url(),
733+
"ssvc_trees": all_ssvc,
640734
}
641735
)
642736

@@ -697,17 +791,21 @@ def get_fixing_advisories_bulk(packages):
697791
package_map = defaultdict(list)
698792

699793
for adv in advisory_sets:
700-
package_map[adv.package_id].append(adv.primary_advisory.advisory_id)
794+
package_map[adv.package_id].append(adv.primary_advisory)
701795

702796
result = {}
703797

704798
for package in packages:
705799
groups = package_map.get(package.id, [])
706800
grouped = []
707801

708-
for adv_id in groups:
802+
for advisory in groups:
709803
grouped.append(
710-
{"advisory_id": adv_id.split("/")[-1], "advisory_uid": adv_id.split("/")[-1]}
804+
{
805+
"advisory_id": advisory.advisory_id.split("/")[-1],
806+
"resource_url": advisory.get_absolute_url(),
807+
"advisory_uid": advisory.avid,
808+
}
711809
)
712810

713811
result[package.id] = grouped

vulnerabilities/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from itertools import groupby
2020
from operator import attrgetter
2121
from traceback import format_exc as traceback_format_exc
22-
from typing import List
22+
from typing import Dict, List
2323
from typing import NamedTuple
2424
from typing import Optional
2525
from typing import Set
@@ -3872,6 +3872,7 @@ class GroupedAdvisory(NamedTuple):
38723872
weighted_severity: Optional[float]
38733873
exploitability: Optional[float]
38743874
risk_score: Optional[float]
3875+
ssvc_trees: List[Dict]
38753876

38763877

38773878
class AdvisoryPOC(models.Model):

vulnerabilities/utils.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import urllib3
3636
from cwe2.database import Database
3737
from cwe2.database import InvalidCWEError
38+
from django.db.models import Prefetch
3839
from packageurl import PackageURL
3940
from packageurl.contrib.django.utils import without_empty_values
4041
from univers.version_range import RANGE_CLASS_BY_SCHEMES
@@ -979,10 +980,12 @@ def get_merged_identifier_groups(advisories):
979980
return final_groups
980981

981982

982-
def get_advisories_from_groups(groups):
983+
def get_advisories_from_groups(groups, include_ssvc_trees=False):
983984
"""
984985
Return a list of advisories from the merged groups of advisories.
985986
"""
987+
from vulnerabilities.models import SSVC
988+
from vulnerabilities.models import AdvisoryV2
986989
from vulnerabilities.models import Group
987990
from vulnerabilities.models import GroupedAdvisory
988991

@@ -1016,6 +1019,35 @@ def get_advisories_from_groups(groups):
10161019
identifier = group.primary.advisory_id.split("/")[-1]
10171020
filtered_aliases = [alias for alias in group.aliases if alias.alias != identifier]
10181021

1022+
ssvc_trees = []
1023+
1024+
if include_ssvc_trees:
1025+
1026+
all_advs = [group.primary] + list(group.secondaries)
1027+
1028+
advisories_qs = AdvisoryV2.objects.filter(
1029+
id__in=[adv.id for adv in all_advs]
1030+
).prefetch_related(
1031+
Prefetch(
1032+
"related_ssvcs",
1033+
queryset=SSVC.objects.select_related("source_advisory")
1034+
.only("id", "vector", "decision", "options", "source_advisory__url")
1035+
.distinct(),
1036+
to_attr="ssvc_trees",
1037+
)
1038+
)
1039+
1040+
ssvc_trees = [
1041+
{
1042+
"vector": ssvc.vector,
1043+
"decision": ssvc.decision,
1044+
"options": ssvc.options,
1045+
"url": ssvc.source_advisory.url if ssvc.source_advisory else None,
1046+
}
1047+
for adv in advisories_qs
1048+
for ssvc in adv.ssvc_trees
1049+
]
1050+
10191051
advisories.append(
10201052
GroupedAdvisory(
10211053
aliases=filtered_aliases,
@@ -1024,6 +1056,7 @@ def get_advisories_from_groups(groups):
10241056
weighted_severity=weighted_severity,
10251057
exploitability=exploitability,
10261058
risk_score=risk_score,
1059+
ssvc_trees=ssvc_trees or [],
10271060
)
10281061
)
10291062

0 commit comments

Comments
 (0)