Skip to content

Commit 3d413f2

Browse files
committed
Optimize V3 API
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent bd13ee9 commit 3d413f2

3 files changed

Lines changed: 202 additions & 95 deletions

File tree

vulnerabilities/api_v3.py

Lines changed: 171 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10+
from collections import defaultdict
1011
from typing import List
1112
from urllib.parse import urlencode
1213

@@ -21,6 +22,7 @@
2122
from rest_framework.reverse import reverse
2223
from rest_framework.throttling import AnonRateThrottle
2324

25+
from vulnerabilities.models import AdvisoryAlias
2426
from vulnerabilities.models import AdvisoryReference
2527
from vulnerabilities.models import AdvisorySet
2628
from vulnerabilities.models import AdvisorySetMember
@@ -216,6 +218,26 @@ def get_fixing_vulnerabilities_url(self, obj):
216218

217219
def get_affected_by_vulnerabilities(self, package):
218220
"""Return a dictionary with advisory as keys and their details, including fixed_by_packages."""
221+
advisories = self.context["advisory_map"].get(package.id, [])
222+
impact_map = self.context["impact_map"].get(package.id, {})
223+
224+
if advisories:
225+
result = []
226+
227+
for adv in advisories:
228+
fixed = impact_map.get(adv["avid"])
229+
if not fixed:
230+
continue
231+
232+
result.append(
233+
{
234+
**adv,
235+
"fixed_by_packages": fixed,
236+
}
237+
)
238+
239+
return result
240+
219241
advisories_qs = AdvisoryV2.objects.latest_affecting_advisories_for_purl(package.package_url)
220242

221243
advisories = []
@@ -250,56 +272,35 @@ def get_affected_by_vulnerabilities(self, package):
250272
"advisory_id": advisory.advisory_id.split("/")[-1],
251273
"aliases": [alias.alias for alias in advisory.aliases.all()],
252274
"summary": advisory.summary,
253-
"fixed_by_packages": [pkg.purl for pkg in impact.fixed_by_packages.all()],
254275
"severity": advisory.weighted_severity,
255276
"exploitability": advisory.exploitability,
256277
"risk_score": advisory.risk_score,
278+
"fixed_by_packages": [pkg.purl for pkg in impact.fixed_by_packages.all()],
257279
}
258280
)
259281

260282
return result
261283

262-
is_grouped = AdvisorySet.objects.filter(package=package, relation_type="affecting").exists()
263-
264-
if is_grouped:
265-
affected_by_advisories_qs = (
266-
AdvisorySet.objects.filter(package=package, relation_type="affecting")
267-
.select_related("primary_advisory")
268-
.prefetch_related(
269-
Prefetch(
270-
"members",
271-
queryset=AdvisorySetMember.objects.filter(is_primary=False).select_related(
272-
"advisory"
273-
),
274-
to_attr="secondary_members",
275-
)
284+
if not advisories:
285+
if package.type in TYPES_WITH_MULTIPLE_IMPORTERS:
286+
advisories_qs = advisories_qs.prefetch_related(
287+
"aliases",
288+
"impacted_packages__affecting_packages",
289+
"impacted_packages__fixed_by_packages",
276290
)
277-
)
278-
279-
affected_groups = [
280-
Group(
281-
aliases=list(adv.aliases.all()),
282-
primary=adv.primary_advisory,
283-
secondaries=[member.advisory for member in adv.secondary_members],
291+
advisories: List[GroupedAdvisory] = merge_and_save_grouped_advisories(
292+
package, advisories_qs, "affecting"
284293
)
285-
for adv in affected_by_advisories_qs
286-
]
294+
return self.return_advisories_data(package, advisories_qs, advisories)
287295

288-
advisories: List[GroupedAdvisory] = get_advisories_from_groups(affected_groups)
289-
return self.return_advisories_data(package, advisories_qs, advisories)
296+
def get_fixing_vulnerabilities(self, package):
297+
fixing_advisories = AdvisorySet.objects.filter(
298+
package=package, relation_type="fixing"
299+
).values_list("primary_advisory__advisory_id", flat=True)
290300

291-
if package.type in TYPES_WITH_MULTIPLE_IMPORTERS:
292-
advisories_qs = advisories_qs.prefetch_related(
293-
"aliases",
294-
"impacted_packages__affecting_packages",
295-
"impacted_packages__fixed_by_packages",
296-
)
297-
advisories: List[GroupedAdvisory] = merge_and_save_grouped_advisories(
298-
package, advisories_qs, "affecting"
299-
)
300-
return self.return_advisories_data(package, advisories_qs, advisories)
301+
if fixing_advisories:
302+
return [{"advisory_id": adv_id.split("/")[-1]} for adv_id in fixing_advisories]
301303

302-
def get_fixing_vulnerabilities(self, package):
303304
advisories_qs = AdvisoryV2.objects.latest_fixed_by_advisories_for_purl(package.package_url)
304305

305306
if not package.type in TYPES_WITH_MULTIPLE_IMPORTERS:
@@ -319,37 +320,6 @@ def get_fixing_vulnerabilities(self, package):
319320
)
320321
return results
321322

322-
advisories = []
323-
324-
is_grouped = AdvisorySet.objects.filter(package=package, relation_type="fixing").exists()
325-
326-
if is_grouped:
327-
fixing_advisories_qs = (
328-
AdvisorySet.objects.filter(package=package, relation_type="fixing")
329-
.select_related("primary_advisory")
330-
.prefetch_related(
331-
Prefetch(
332-
"members",
333-
queryset=AdvisorySetMember.objects.filter(is_primary=False).select_related(
334-
"advisory"
335-
),
336-
to_attr="secondary_members",
337-
)
338-
)
339-
)
340-
341-
fixing_groups = [
342-
Group(
343-
aliases=list(adv.aliases.all()),
344-
primary=adv.primary_advisory,
345-
secondaries=[member.advisory for member in adv.secondary_members],
346-
)
347-
for adv in fixing_advisories_qs
348-
]
349-
350-
advisories: List[GroupedAdvisory] = get_advisories_from_groups(fixing_groups)
351-
return self.return_fixing_advisories_data(advisories)
352-
353323
if package.type in TYPES_WITH_MULTIPLE_IMPORTERS:
354324
advisories_qs = advisories_qs.prefetch_related(
355325
"aliases",
@@ -409,11 +379,11 @@ def return_advisories_data(self, package, advisories_qs, advisories):
409379
return result
410380

411381
def get_next_non_vulnerable_version(self, package):
412-
if next_non_vulnerable := package.get_non_vulnerable_versions()[0]:
382+
if next_non_vulnerable := package.next_non_vulnerable_version:
413383
return next_non_vulnerable.version
414384

415385
def get_latest_non_vulnerable_version(self, package):
416-
if latest_non_vulnerable := package.get_non_vulnerable_versions()[-1]:
386+
if latest_non_vulnerable := package.latest_non_vulnerable_version:
417387
return latest_non_vulnerable.version
418388

419389

@@ -464,13 +434,11 @@ def create(self, request, *args, **kwargs):
464434
query = (
465435
PackageV2.objects.filter(plain_package_url__in=plain_purls)
466436
.values_list("plain_package_url", flat=True)
467-
.distinct()
468437
.order_by("plain_package_url")
469438
)
470439
else:
471440
query = (
472441
PackageV2.objects.filter(package_url__in=purls)
473-
.distinct()
474442
.order_by("package_url")
475443
.values_list("package_url", flat=True)
476444
)
@@ -479,20 +447,20 @@ def create(self, request, *args, **kwargs):
479447
return self.get_paginated_response(page)
480448

481449
if ignore_qualifiers_subpath:
482-
query = (
483-
PackageV2.objects.filter(plain_package_url__in=plain_purls)
484-
.order_by("plain_package_url")
485-
.distinct("plain_package_url")
450+
query = PackageV2.objects.filter(plain_package_url__in=plain_purls).order_by(
451+
"plain_package_url"
486452
)
487453
else:
488-
query = (
489-
PackageV2.objects.filter(package_url__in=purls)
490-
.order_by("package_url")
491-
.distinct("package_url")
492-
)
454+
query = PackageV2.objects.filter(package_url__in=purls).order_by("package_url")
493455

494456
page = self.paginate_queryset(query)
495-
serializer = self.get_serializer(page, many=True, context={"request": request})
457+
advisory_map = get_grouped_advisories_bulk(page)
458+
impact_map = get_impacts_bulk(page)
459+
serializer = self.get_serializer(
460+
page,
461+
many=True,
462+
context={"request": request, "advisory_map": advisory_map, "impact_map": impact_map},
463+
)
496464
return self.get_paginated_response(serializer.data)
497465

498466

@@ -592,3 +560,124 @@ class FixingAdvisoriesViewSet(PackageAdvisoriesViewSet):
592560
class AffectedByAdvisoriesViewSet(PackageAdvisoriesViewSet):
593561
relation = "impacted_packages__affecting_packages__package_url"
594562
serializer_class = AffectedByAdvisoryV3Serializer
563+
564+
565+
def get_grouped_advisories_bulk(packages):
566+
package_ids = [p.id for p in packages]
567+
568+
advisory_sets = list(
569+
AdvisorySet.objects.filter(
570+
package_id__in=package_ids,
571+
relation_type="affecting",
572+
)
573+
.select_related("primary_advisory", "package")
574+
.prefetch_related(
575+
Prefetch("aliases", queryset=AdvisoryAlias.objects.only("alias")),
576+
Prefetch(
577+
"members",
578+
queryset=AdvisorySetMember.objects.filter(is_primary=False)
579+
.select_related("advisory")
580+
.only(
581+
"advisory__avid",
582+
"advisory__weighted_severity",
583+
"advisory__exploitability",
584+
),
585+
to_attr="secondary_members",
586+
),
587+
)
588+
.only(
589+
"id",
590+
"package_id",
591+
"primary_advisory__avid",
592+
"primary_advisory__summary",
593+
"primary_advisory__weighted_severity",
594+
"primary_advisory__exploitability",
595+
"primary_advisory__advisory_id",
596+
)
597+
)
598+
599+
package_map = defaultdict(list)
600+
for adv in advisory_sets:
601+
adv._aliases_cache = [a.alias for a in adv.aliases.all()]
602+
package_map[adv.package_id].append(adv)
603+
604+
result = {}
605+
606+
for package in packages:
607+
groups = package_map.get(package.id, [])
608+
grouped = []
609+
610+
for adv in groups:
611+
primary = adv.primary_advisory
612+
secondaries = [m.advisory for m in adv.secondary_members]
613+
614+
max_sev = primary.weighted_severity or 0.0
615+
max_exp = primary.exploitability or 0.0
616+
617+
for sec in secondaries:
618+
if sec.weighted_severity:
619+
max_sev = max(max_sev, sec.weighted_severity)
620+
if sec.exploitability:
621+
max_exp = max(max_exp, sec.exploitability)
622+
623+
weighted_severity = round(max_sev, 1) if max_sev else None
624+
exploitability = max_exp or None
625+
626+
risk_score = None
627+
if exploitability and weighted_severity:
628+
risk_score = round(min(exploitability * weighted_severity, 10.0), 1)
629+
630+
identifier = primary.advisory_id.split("/")[-1]
631+
632+
aliases = [a for a in adv._aliases_cache if a != identifier]
633+
634+
grouped.append(
635+
{
636+
"avid": primary.avid,
637+
"advisory_id": identifier,
638+
"aliases": aliases,
639+
"weighted_severity": weighted_severity,
640+
"exploitability": exploitability,
641+
"risk_score": risk_score,
642+
"summary": primary.summary,
643+
}
644+
)
645+
646+
result[package.id] = grouped
647+
648+
return result
649+
650+
651+
def get_impacts_bulk(packages):
652+
package_ids = [p.id for p in packages]
653+
654+
impacts = (
655+
ImpactedPackageAffecting.objects.filter(package_id__in=package_ids)
656+
.select_related("impacted_package__advisory")
657+
.prefetch_related(
658+
Prefetch(
659+
"impacted_package__fixed_by_packages",
660+
queryset=PackageV2.objects.only("package_url"),
661+
)
662+
)
663+
.only(
664+
"package_id",
665+
"impacted_package_id",
666+
"impacted_package__advisory_id",
667+
"impacted_package__advisory__avid",
668+
)
669+
)
670+
671+
impact_map = defaultdict(dict)
672+
fixed_cache = {}
673+
674+
for impact in impacts:
675+
ip = impact.impacted_package
676+
avid = ip.advisory.avid
677+
678+
if ip.id not in fixed_cache:
679+
fixed_cache[ip.id] = list({pkg.purl for pkg in ip.fixed_by_packages.all()})
680+
681+
impact_map[impact.package_id][avid] = fixed_cache[ip.id]
682+
683+
return impact_map

0 commit comments

Comments
 (0)