From 08df01dc9e7a8604176491c7f460a262f1e815cb Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 29 Apr 2026 17:21:16 +0530 Subject: [PATCH 1/6] Add SSVC trees, resource URL and max_advisories Signed-off-by: Tushar Goel --- vulnerabilities/api_v3.py | 132 +++++++++++++++++++++++++++++++++----- vulnerabilities/models.py | 3 +- vulnerabilities/utils.py | 35 +++++++++- 3 files changed, 151 insertions(+), 19 deletions(-) diff --git a/vulnerabilities/api_v3.py b/vulnerabilities/api_v3.py index 634476ddf..420b8bcab 100644 --- a/vulnerabilities/api_v3.py +++ b/vulnerabilities/api_v3.py @@ -23,6 +23,7 @@ from rest_framework.reverse import reverse from rest_framework.throttling import AnonRateThrottle +from vulnerabilities.models import SSVC from vulnerabilities.models import AdvisoryAlias from vulnerabilities.models import AdvisoryReference from vulnerabilities.models import AdvisorySet @@ -30,13 +31,11 @@ from vulnerabilities.models import AdvisorySeverity from vulnerabilities.models import AdvisoryV2 from vulnerabilities.models import AdvisoryWeakness -from vulnerabilities.models import Group from vulnerabilities.models import GroupedAdvisory from vulnerabilities.models import ImpactedPackageAffecting from vulnerabilities.models import PackageV2 from vulnerabilities.throttling import PermissionBasedUserRateThrottle from vulnerabilities.utils import TYPES_WITH_MULTIPLE_IMPORTERS -from vulnerabilities.utils import get_advisories_from_groups from vulnerabilities.utils import merge_and_save_grouped_advisories @@ -48,6 +47,7 @@ class PackageQuerySerializer(serializers.Serializer): ) details = serializers.BooleanField(default=False) ignore_qualifiers_subpath = serializers.BooleanField(default=False) + max_advisories = serializers.IntegerField(default=100, min_value=1, max_value=10000) def validate(self, data): if not data["purls"]: @@ -229,11 +229,17 @@ def get_affected_by_vulnerabilities(self, package): for adv in advisories: fixed = impact_map.get(adv["avid"]) adv.pop("avid", None) + resource_url = None + + if request := self.context.get("request", None): + resource_url = adv.pop("resource_url", None) + resource_url = request.build_absolute_uri(location=resource_url) result.append( { **adv, "fixed_by_packages": fixed, + "resource_url": resource_url, } ) @@ -247,9 +253,20 @@ def get_affected_by_vulnerabilities(self, package): advisories_ids = advisories_qs.only("id") advisories_ids = list(advisories_ids[:101]) - if len(advisories_ids) > 100: + if len(advisories_ids) > self.context.get("max_advisories", 100): return None + advisories_qs = advisories_qs.prefetch_related( + "aliases", + Prefetch( + "related_ssvcs", + queryset=SSVC.objects.select_related("source_advisory").only( + "id", "decision", "options", "vector", "source_advisory__url" + ), + to_attr="prefetched_ssvc_trees", + ), + ) + advisory_by_avid = {adv.avid: adv for adv in advisories_qs} avids = advisory_by_avid.keys() @@ -265,8 +282,14 @@ def get_affected_by_vulnerabilities(self, package): for advisory in advisories_qs: impact = impact_by_avid.get(advisory.avid) - if not impact: - continue + fixed_by_packages = [] + if impact: + fixed_by_packages = [pkg.purl for pkg in impact.fixed_by_packages.all()] + + resource_url = None + + if request := self.context.get("request", None): + resource_url = request.build_absolute_uri(location=advisory.get_absolute_url()) result.append( { @@ -277,7 +300,17 @@ def get_affected_by_vulnerabilities(self, package): "severity": advisory.weighted_severity, "exploitability": advisory.exploitability, "risk_score": advisory.risk_score, - "fixed_by_packages": [pkg.purl for pkg in impact.fixed_by_packages.all()], + "fixed_by_packages": fixed_by_packages, + "resource_url": resource_url, + "ssvc_trees": [ + { + "vector": ssvc.vector, + "decision": ssvc.decision, + "options": ssvc.options, + "source_url": ssvc.source_advisory.url, + } + for ssvc in advisory.prefetched_ssvc_trees + ], } ) @@ -297,8 +330,18 @@ def get_affected_by_vulnerabilities(self, package): def get_fixing_vulnerabilities(self, package): advisories = self.context["fixing_advisory_map"].get(package.id, []) - if advisories: - return advisories + results = [] + for advisory in advisories: + if request := self.context.get("request", None): + resource_url = request.build_absolute_uri(location=advisory["resource_url"]) + results.append( + { + "advisory_id": advisory["advisory_id"], + "resource_url": resource_url, + } + ) + if results: + return results advisories_qs = AdvisoryV2.objects.latest_fixed_by_advisories_for_purl(package.package_url) @@ -306,16 +349,19 @@ def get_fixing_vulnerabilities(self, package): advisories_ids = advisories_qs.only("id") advisories_ids = list(advisories_ids[:101]) - if len(advisories_ids) > 100: + if len(advisories_ids) > self.context.get("max_advisories", 100): return None results = [] for advisory in advisories_qs: + if request := self.context.get("request", None): + resource_url = request.build_absolute_uri(location=advisory.get_absolute_url()) results.append( { "advisory_id": advisory.advisory_id.split("/")[-1], "advisory_uid": advisory.avid, + "resource_url": resource_url, } ) return results @@ -337,10 +383,16 @@ def return_fixing_advisories_data(self, advisories): result = [] for advisory in advisories: assert isinstance(advisory, GroupedAdvisory) + resource_url = None + if request := self.context.get("request", None): + resource_url = request.build_absolute_uri( + location=advisory.advisory.get_absolute_url() + ) result.append( { "advisory_id": advisory.identifier, "advisory_uid": advisory.advisory.avid, + "resource_url": resource_url, } ) @@ -361,9 +413,15 @@ def return_advisories_data(self, package, advisories_qs, advisories): result = [] for advisory in advisories: assert isinstance(advisory, GroupedAdvisory) + resource_url = None + fixed_by_packages = [] + if request := self.context.get("request", None): + resource_url = request.build_absolute_uri( + location=advisory.advisory.get_absolute_url() + ) impact = impact_by_avid.get(advisory.advisory.avid) if not impact: - continue + fixed_by_packages = list(set([pkg.purl for pkg in impact.fixed_by_packages.all()])) result.append( { @@ -374,9 +432,9 @@ def return_advisories_data(self, package, advisories_qs, advisories): "exploitability": advisory.exploitability, "risk_score": advisory.risk_score, "summary": advisory.advisory.summary, - "fixed_by_packages": list( - set([pkg.purl for pkg in impact.fixed_by_packages.all()]) - ), + "fixed_by_packages": fixed_by_packages, + "resource_url": resource_url, + "ssvc_trees": advisory.ssvc_trees, } ) @@ -405,6 +463,7 @@ def create(self, request, *args, **kwargs): purls = serializer.validated_data["purls"] details = serializer.validated_data["details"] ignore_qualifiers_subpath = serializer.validated_data["ignore_qualifiers_subpath"] + max_advisories = serializer.validated_data["max_advisories"] if not purls: impacted = ImpactedPackageAffecting.objects.filter(package_id=OuterRef("id")) @@ -469,6 +528,7 @@ def create(self, request, *args, **kwargs): "advisory_map": affected_advisory_map, "impact_map": impact_map, "fixing_advisory_map": fixing_advisory_map, + "max_advisories": max_advisories, }, ) return self.get_paginated_response(serializer.data) @@ -583,7 +643,25 @@ def get_affected_advisories_bulk(packages): relation_type="affecting", ) .select_related("primary_advisory") - .prefetch_related(Prefetch("aliases", queryset=AdvisoryAlias.objects.only("alias"))) + .prefetch_related( + Prefetch("aliases", queryset=AdvisoryAlias.objects.only("alias")), + Prefetch( + "members", + queryset=AdvisorySetMember.objects.select_related("advisory").prefetch_related( + Prefetch( + "advisory__related_ssvcs", + queryset=SSVC.objects.select_related("source_advisory").only( + "id", + "options", + "decision", + "vector", + "source_advisory__url", + ), + to_attr="prefetched_ssvc_trees", + ) + ), + ), + ) .annotate( max_severity=Max( "members__advisory__weighted_severity", @@ -627,6 +705,20 @@ def get_affected_advisories_bulk(packages): identifier = primary.advisory_id.split("/")[-1] aliases = [a for a in adv._aliases_cache if a != identifier] + all_ssvc = [] + + for member in adv.members.all(): + all_ssvc.extend(member.advisory.prefetched_ssvc_trees) + + for ssvc in all_ssvc: + all_ssvc.append( + { + "vector": ssvc.vector, + "decision": ssvc.decision, + "options": ssvc.options, + "source_url": ssvc.source_advisory.url, + } + ) grouped.append( { @@ -637,6 +729,8 @@ def get_affected_advisories_bulk(packages): "exploitability": exploitability, "risk_score": risk_score, "summary": primary.summary, + "resource_url": primary.get_absolute_url(), + "ssvc_trees": all_ssvc, } ) @@ -697,7 +791,7 @@ def get_fixing_advisories_bulk(packages): package_map = defaultdict(list) for adv in advisory_sets: - package_map[adv.package_id].append(adv.primary_advisory.advisory_id) + package_map[adv.package_id].append(adv.primary_advisory) result = {} @@ -705,9 +799,13 @@ def get_fixing_advisories_bulk(packages): groups = package_map.get(package.id, []) grouped = [] - for adv_id in groups: + for advisory in groups: grouped.append( - {"advisory_id": adv_id.split("/")[-1], "advisory_uid": adv_id.split("/")[-1]} + { + "advisory_id": advisory.advisory_id.split("/")[-1], + "resource_url": advisory.get_absolute_url(), + "advisory_uid": advisory.avid, + } ) result[package.id] = grouped diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 69253f54b..8cfdf2335 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -19,7 +19,7 @@ from itertools import groupby from operator import attrgetter from traceback import format_exc as traceback_format_exc -from typing import List +from typing import Dict, List from typing import NamedTuple from typing import Optional from typing import Set @@ -3872,6 +3872,7 @@ class GroupedAdvisory(NamedTuple): weighted_severity: Optional[float] exploitability: Optional[float] risk_score: Optional[float] + ssvc_trees: List[Dict] class AdvisoryPOC(models.Model): diff --git a/vulnerabilities/utils.py b/vulnerabilities/utils.py index 2e618a920..48d3b5384 100644 --- a/vulnerabilities/utils.py +++ b/vulnerabilities/utils.py @@ -35,6 +35,7 @@ import urllib3 from cwe2.database import Database from cwe2.database import InvalidCWEError +from django.db.models import Prefetch from packageurl import PackageURL from packageurl.contrib.django.utils import without_empty_values from univers.version_range import RANGE_CLASS_BY_SCHEMES @@ -979,10 +980,12 @@ def get_merged_identifier_groups(advisories): return final_groups -def get_advisories_from_groups(groups): +def get_advisories_from_groups(groups, include_ssvc_trees=False): """ Return a list of advisories from the merged groups of advisories. """ + from vulnerabilities.models import SSVC + from vulnerabilities.models import AdvisoryV2 from vulnerabilities.models import Group from vulnerabilities.models import GroupedAdvisory @@ -1016,6 +1019,35 @@ def get_advisories_from_groups(groups): identifier = group.primary.advisory_id.split("/")[-1] filtered_aliases = [alias for alias in group.aliases if alias.alias != identifier] + ssvc_trees = [] + + if include_ssvc_trees: + + all_advs = [group.primary] + list(group.secondaries) + + advisories_qs = AdvisoryV2.objects.filter( + id__in=[adv.id for adv in all_advs] + ).prefetch_related( + Prefetch( + "related_ssvcs", + queryset=SSVC.objects.select_related("source_advisory") + .only("id", "vector", "decision", "options", "source_advisory__url") + .distinct(), + to_attr="ssvc_trees", + ) + ) + + ssvc_trees = [ + { + "vector": ssvc.vector, + "decision": ssvc.decision, + "options": ssvc.options, + "url": ssvc.source_advisory.url if ssvc.source_advisory else None, + } + for adv in advisories_qs + for ssvc in adv.ssvc_trees + ] + advisories.append( GroupedAdvisory( aliases=filtered_aliases, @@ -1024,6 +1056,7 @@ def get_advisories_from_groups(groups): weighted_severity=weighted_severity, exploitability=exploitability, risk_score=risk_score, + ssvc_trees=ssvc_trees or [], ) ) From bee349145057f5dc7f6d9e153b1f154b4eb04842 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 29 Apr 2026 17:29:23 +0530 Subject: [PATCH 2/6] Fix formatting issues Signed-off-by: Tushar Goel --- vulnerabilities/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 8cfdf2335..b3ec2b525 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -19,7 +19,8 @@ from itertools import groupby from operator import attrgetter from traceback import format_exc as traceback_format_exc -from typing import Dict, List +from typing import Dict +from typing import List from typing import NamedTuple from typing import Optional from typing import Set From 557ab9e68bbba6bacc52b225a91a1bb2ef57224c Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 29 Apr 2026 18:23:23 +0530 Subject: [PATCH 3/6] Fix SSVC trees issue Signed-off-by: Tushar Goel --- vulnerabilities/api_v3.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vulnerabilities/api_v3.py b/vulnerabilities/api_v3.py index 420b8bcab..3cc093bd9 100644 --- a/vulnerabilities/api_v3.py +++ b/vulnerabilities/api_v3.py @@ -710,8 +710,10 @@ def get_affected_advisories_bulk(packages): for member in adv.members.all(): all_ssvc.extend(member.advisory.prefetched_ssvc_trees) + ssvcs = [] + for ssvc in all_ssvc: - all_ssvc.append( + ssvcs.append( { "vector": ssvc.vector, "decision": ssvc.decision, @@ -730,7 +732,7 @@ def get_affected_advisories_bulk(packages): "risk_score": risk_score, "summary": primary.summary, "resource_url": primary.get_absolute_url(), - "ssvc_trees": all_ssvc, + "ssvc_trees": ssvcs, } ) From c9a7003960287d9ecff9f8b29e479602794b7c8b Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Thu, 30 Apr 2026 21:51:49 +0530 Subject: [PATCH 4/6] Add unique SSVC trees only Signed-off-by: Tushar Goel --- vulnerabilities/api_v3.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vulnerabilities/api_v3.py b/vulnerabilities/api_v3.py index 3cc093bd9..11385c17c 100644 --- a/vulnerabilities/api_v3.py +++ b/vulnerabilities/api_v3.py @@ -262,7 +262,7 @@ def get_affected_by_vulnerabilities(self, package): "related_ssvcs", queryset=SSVC.objects.select_related("source_advisory").only( "id", "decision", "options", "vector", "source_advisory__url" - ), + ).distinct("source_advisory__url"), to_attr="prefetched_ssvc_trees", ), ) @@ -656,7 +656,7 @@ def get_affected_advisories_bulk(packages): "decision", "vector", "source_advisory__url", - ), + ).distinct("source_advisory__url"), to_attr="prefetched_ssvc_trees", ) ), From 3a9496a85f77cf2668d7e5996695780bbe04c6ff Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Fri, 1 May 2026 12:58:18 +0530 Subject: [PATCH 5/6] Fix formatting issues Signed-off-by: Tushar Goel --- vulnerabilities/api_v3.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/vulnerabilities/api_v3.py b/vulnerabilities/api_v3.py index 11385c17c..7736f6408 100644 --- a/vulnerabilities/api_v3.py +++ b/vulnerabilities/api_v3.py @@ -260,9 +260,9 @@ def get_affected_by_vulnerabilities(self, package): "aliases", Prefetch( "related_ssvcs", - queryset=SSVC.objects.select_related("source_advisory").only( - "id", "decision", "options", "vector", "source_advisory__url" - ).distinct("source_advisory__url"), + queryset=SSVC.objects.select_related("source_advisory") + .only("id", "decision", "options", "vector", "source_advisory__url") + .distinct("source_advisory__url"), to_attr="prefetched_ssvc_trees", ), ) @@ -650,13 +650,15 @@ def get_affected_advisories_bulk(packages): queryset=AdvisorySetMember.objects.select_related("advisory").prefetch_related( Prefetch( "advisory__related_ssvcs", - queryset=SSVC.objects.select_related("source_advisory").only( + queryset=SSVC.objects.select_related("source_advisory") + .only( "id", "options", "decision", "vector", "source_advisory__url", - ).distinct("source_advisory__url"), + ) + .distinct("source_advisory__url"), to_attr="prefetched_ssvc_trees", ) ), From 5e98ecd978dc862730c8d35f05724f63b51b214d Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Fri, 1 May 2026 19:22:38 +0530 Subject: [PATCH 6/6] Add avid in api Signed-off-by: Tushar Goel --- vulnerabilities/api_v3.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/vulnerabilities/api_v3.py b/vulnerabilities/api_v3.py index 7736f6408..1572eb582 100644 --- a/vulnerabilities/api_v3.py +++ b/vulnerabilities/api_v3.py @@ -227,8 +227,7 @@ def get_affected_by_vulnerabilities(self, package): result = [] for adv in advisories: - fixed = impact_map.get(adv["avid"]) - adv.pop("avid", None) + fixed = impact_map.get(adv["avid"]) or [] resource_url = None if request := self.context.get("request", None): @@ -293,6 +292,7 @@ def get_affected_by_vulnerabilities(self, package): result.append( { + "avid": advisory.avid, "advisory_id": advisory.advisory_id.split("/")[-1], "advisory_uid": advisory.avid, "aliases": [alias.alias for alias in advisory.aliases.all()], @@ -338,6 +338,7 @@ def get_fixing_vulnerabilities(self, package): { "advisory_id": advisory["advisory_id"], "resource_url": resource_url, + "avid": advisory["avid"], } ) if results: @@ -362,6 +363,7 @@ def get_fixing_vulnerabilities(self, package): "advisory_id": advisory.advisory_id.split("/")[-1], "advisory_uid": advisory.avid, "resource_url": resource_url, + "avid": advisory.avid, } ) return results @@ -393,6 +395,7 @@ def return_fixing_advisories_data(self, advisories): "advisory_id": advisory.identifier, "advisory_uid": advisory.advisory.avid, "resource_url": resource_url, + "avid": advisory.advisory.avid, } ) @@ -420,7 +423,7 @@ def return_advisories_data(self, package, advisories_qs, advisories): location=advisory.advisory.get_absolute_url() ) impact = impact_by_avid.get(advisory.advisory.avid) - if not impact: + if impact: fixed_by_packages = list(set([pkg.purl for pkg in impact.fixed_by_packages.all()])) result.append(