From 08df01dc9e7a8604176491c7f460a262f1e815cb Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 29 Apr 2026 17:21:16 +0530 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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( From d759ea498cab39f58e32917d3a1b493b38db5a71 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Tue, 26 May 2026 14:17:01 +0530 Subject: [PATCH 07/13] Fix avid errors Signed-off-by: Tushar Goel --- vulnerabilities/api_v3.py | 10 ++-- ...isory_id_alter_advisoryv2_avid_and_more.py | 48 +++++++++++++++++++ vulnerabilities/models.py | 8 ++-- 3 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 vulnerabilities/migrations/0133_alter_advisoryv2_advisory_id_alter_advisoryv2_avid_and_more.py diff --git a/vulnerabilities/api_v3.py b/vulnerabilities/api_v3.py index 1572eb582..29bfb2746 100644 --- a/vulnerabilities/api_v3.py +++ b/vulnerabilities/api_v3.py @@ -227,7 +227,7 @@ def get_affected_by_vulnerabilities(self, package): result = [] for adv in advisories: - fixed = impact_map.get(adv["avid"]) or [] + fixed = impact_map.get(adv["advisory_uid"]) or [] resource_url = None if request := self.context.get("request", None): @@ -292,7 +292,6 @@ 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,7 +337,7 @@ def get_fixing_vulnerabilities(self, package): { "advisory_id": advisory["advisory_id"], "resource_url": resource_url, - "avid": advisory["avid"], + "advisory_uid": advisory["advisory_uid"], } ) if results: @@ -346,7 +345,7 @@ def get_fixing_vulnerabilities(self, package): advisories_qs = AdvisoryV2.objects.latest_fixed_by_advisories_for_purl(package.package_url) - if not package.type in TYPES_WITH_MULTIPLE_IMPORTERS: + if package.type not in TYPES_WITH_MULTIPLE_IMPORTERS: advisories_ids = advisories_qs.only("id") advisories_ids = list(advisories_ids[:101]) @@ -356,6 +355,7 @@ def get_fixing_vulnerabilities(self, package): results = [] for advisory in advisories_qs: + resource_url = None if request := self.context.get("request", None): resource_url = request.build_absolute_uri(location=advisory.get_absolute_url()) results.append( @@ -363,7 +363,6 @@ 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 @@ -395,7 +394,6 @@ def return_fixing_advisories_data(self, advisories): "advisory_id": advisory.identifier, "advisory_uid": advisory.advisory.avid, "resource_url": resource_url, - "avid": advisory.advisory.avid, } ) diff --git a/vulnerabilities/migrations/0133_alter_advisoryv2_advisory_id_alter_advisoryv2_avid_and_more.py b/vulnerabilities/migrations/0133_alter_advisoryv2_advisory_id_alter_advisoryv2_avid_and_more.py new file mode 100644 index 000000000..8f45487b2 --- /dev/null +++ b/vulnerabilities/migrations/0133_alter_advisoryv2_advisory_id_alter_advisoryv2_avid_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 5.2.11 on 2026-05-26 08:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0132_migrate_advisoryv2_datasource_ids"), + ] + + operations = [ + migrations.AlterField( + model_name="advisoryv2", + name="advisory_id", + field=models.CharField( + db_index=True, + help_text="An advisory is a unique vulnerability identifier in some database, such as PYSEC-2020-2233", + max_length=200, + ), + ), + migrations.AlterField( + model_name="advisoryv2", + name="avid", + field=models.CharField( + help_text="Unique ID for the datasource used for this advisory .e.g.: pysec_importer_v2/PYSEC-2020-2233", + max_length=250, + ), + ), + migrations.AlterField( + model_name="advisoryv2", + name="datasource_id", + field=models.CharField( + db_index=True, + help_text="Unique ID for the datasource used for this advisory .e.g.: nginx", + max_length=50, + ), + ), + migrations.AlterField( + model_name="advisoryv2", + name="pipeline_id", + field=models.CharField( + db_index=True, + help_text="Unique ID for the pipeline used for this advisory .e.g.: nginx_importer_v2", + max_length=50, + ), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index b3ec2b525..8ed6d2520 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -3055,7 +3055,7 @@ class AdvisoryV2(models.Model): # This is similar to a type or a namespace datasource_id = models.CharField( - max_length=200, + max_length=50, blank=False, null=False, db_index=True, @@ -3063,7 +3063,7 @@ class AdvisoryV2(models.Model): ) pipeline_id = models.CharField( - max_length=200, + max_length=50, blank=False, null=False, db_index=True, @@ -3072,7 +3072,7 @@ class AdvisoryV2(models.Model): # This is similar to a name advisory_id = models.CharField( - max_length=500, + max_length=200, blank=False, null=False, unique=False, @@ -3082,7 +3082,7 @@ class AdvisoryV2(models.Model): ) avid = models.CharField( - max_length=500, + max_length=250, blank=False, null=False, help_text="Unique ID for the datasource used for this advisory ." From eb4dcc43fbcd1dc520d3937a3ecb7553bd14e53d Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 27 May 2026 15:57:57 +0530 Subject: [PATCH 08/13] Group advisories at time of advisory insertion Signed-off-by: Tushar Goel --- vulnerabilities/api_v3.py | 62 +++++++++------- .../group_advisories_for_packages.py | 33 +-------- .../v2_improvers/unfurl_version_range.py | 6 ++ vulnerabilities/pipes/advisory.py | 5 ++ vulnerabilities/pipes/group_advisories.py | 59 +++++++++++++++- vulnerabilities/utils.py | 24 ------- vulnerabilities/views.py | 70 +++++++++---------- 7 files changed, 139 insertions(+), 120 deletions(-) diff --git a/vulnerabilities/api_v3.py b/vulnerabilities/api_v3.py index 29bfb2746..beb65f91e 100644 --- a/vulnerabilities/api_v3.py +++ b/vulnerabilities/api_v3.py @@ -231,12 +231,19 @@ def get_affected_by_vulnerabilities(self, package): resource_url = None if request := self.context.get("request", None): - resource_url = adv.pop("resource_url", None) + resource_url = adv.get("resource_url") or None resource_url = request.build_absolute_uri(location=resource_url) result.append( { - **adv, + "advisory_id": adv["identifier"], + "advisory_uid": adv["advisory_uid"], + "aliases": adv["aliases"], + "summary": adv["summary"], + "weighted_severity": adv["weighted_severity"], + "exploitability": adv["exploitability"], + "risk_score": adv["risk_score"], + "ssvc_trees": adv["ssvc_trees"], "fixed_by_packages": fixed, "resource_url": resource_url, } @@ -296,7 +303,7 @@ def get_affected_by_vulnerabilities(self, package): "advisory_uid": advisory.avid, "aliases": [alias.alias for alias in advisory.aliases.all()], "summary": advisory.summary, - "severity": advisory.weighted_severity, + "weighted_severity": advisory.weighted_severity, "exploitability": advisory.exploitability, "risk_score": advisory.risk_score, "fixed_by_packages": fixed_by_packages, @@ -315,21 +322,22 @@ def get_affected_by_vulnerabilities(self, package): return result - if not advisories: - if package.type in TYPES_WITH_MULTIPLE_IMPORTERS: - advisories_qs = advisories_qs.prefetch_related( - "aliases", - "impacted_packages__affecting_packages", - "impacted_packages__fixed_by_packages", - ) - advisories: List[GroupedAdvisory] = merge_and_save_grouped_advisories( - package, advisories_qs, "affecting" - ) - return self.return_advisories_data(package, advisories_qs, advisories) + # if not advisories: + # if package.type in TYPES_WITH_MULTIPLE_IMPORTERS: + # advisories_qs = advisories_qs.prefetch_related( + # "aliases", + # "impacted_packages__affecting_packages", + # "impacted_packages__fixed_by_packages", + # ) + # advisories: List[GroupedAdvisory] = merge_and_save_grouped_advisories( + # package, advisories_qs, "affecting" + # ) + # return self.return_advisories_data(package, advisories_qs, advisories) def get_fixing_vulnerabilities(self, package): advisories = self.context["fixing_advisory_map"].get(package.id, []) results = [] + resource_url = None for advisory in advisories: if request := self.context.get("request", None): resource_url = request.build_absolute_uri(location=advisory["resource_url"]) @@ -367,18 +375,18 @@ def get_fixing_vulnerabilities(self, package): ) return results - if package.type in TYPES_WITH_MULTIPLE_IMPORTERS: - advisories_qs = advisories_qs.prefetch_related( - "aliases", - "impacted_packages__affecting_packages", - "impacted_packages__fixed_by_packages", - ) - if not advisories_qs.exists(): - return [] - advisories: List[GroupedAdvisory] = merge_and_save_grouped_advisories( - package, advisories_qs, "fixing" - ) - return self.return_fixing_advisories_data(advisories) + # if package.type in TYPES_WITH_MULTIPLE_IMPORTERS: + # advisories_qs = advisories_qs.prefetch_related( + # "aliases", + # "impacted_packages__affecting_packages", + # "impacted_packages__fixed_by_packages", + # ) + # if not advisories_qs.exists(): + # return [] + # advisories: List[GroupedAdvisory] = merge_and_save_grouped_advisories( + # package, advisories_qs, "fixing" + # ) + # return self.return_fixing_advisories_data(advisories) def return_fixing_advisories_data(self, advisories): result = [] @@ -429,10 +437,10 @@ def return_advisories_data(self, package, advisories_qs, advisories): "advisory_id": advisory.identifier, "advisory_uid": advisory.advisory.avid, "aliases": [alias.alias for alias in advisory.aliases], + "summary": advisory.advisory.summary, "weighted_severity": advisory.weighted_severity, "exploitability": advisory.exploitability, "risk_score": advisory.risk_score, - "summary": advisory.advisory.summary, "fixed_by_packages": fixed_by_packages, "resource_url": resource_url, "ssvc_trees": advisory.ssvc_trees, diff --git a/vulnerabilities/pipelines/v2_improvers/group_advisories_for_packages.py b/vulnerabilities/pipelines/v2_improvers/group_advisories_for_packages.py index ea6fc9185..b85c2d538 100644 --- a/vulnerabilities/pipelines/v2_improvers/group_advisories_for_packages.py +++ b/vulnerabilities/pipelines/v2_improvers/group_advisories_for_packages.py @@ -7,15 +7,10 @@ # See https://aboutcode.org for more information about nexB OSS projects. # -from typing import List - -from vulnerabilities.models import AdvisoryV2 -from vulnerabilities.models import Group from vulnerabilities.models import PackageV2 from vulnerabilities.pipelines import VulnerableCodePipeline -from vulnerabilities.pipes.group_advisories import delete_and_save_advisory_set +from vulnerabilities.pipes.group_advisories import group_advisory_for_package from vulnerabilities.utils import TYPES_WITH_MULTIPLE_IMPORTERS -from vulnerabilities.utils import merge_advisories class GroupAdvisoriesForPackages(VulnerableCodePipeline): @@ -33,28 +28,4 @@ def group_advisories_for_packages(self): def group_advisoris_for_packages(logger=None): for package in PackageV2.objects.filter(type__in=TYPES_WITH_MULTIPLE_IMPORTERS).iterator(): - logger(f"Grouping advisories for package {package.purl}") - affecting_advisories = AdvisoryV2.objects.latest_affecting_advisories_for_purl( - purl=package.purl - ).prefetch_related( - "aliases", - "impacted_packages__affecting_packages", - "impacted_packages__fixed_by_packages", - ) - - fixed_by_advisories = AdvisoryV2.objects.latest_fixed_by_advisories_for_purl( - purl=package.purl - ).prefetch_related( - "aliases", - "impacted_packages__affecting_packages", - "impacted_packages__fixed_by_packages", - ) - - try: - affected_groups: List[Group] = merge_advisories(affecting_advisories, package) - fixed_by_groups: List[Group] = merge_advisories(fixed_by_advisories, package) - delete_and_save_advisory_set(affected_groups, package, relation="affecting") - delete_and_save_advisory_set(fixed_by_groups, package, relation="fixing") - except Exception as e: - logger(f"Failed rebuilding advisory sets for package {package.purl}: {e!r}") - continue + group_advisory_for_package(package, logger=logger) diff --git a/vulnerabilities/pipelines/v2_improvers/unfurl_version_range.py b/vulnerabilities/pipelines/v2_improvers/unfurl_version_range.py index 9d874c635..3540b45b7 100644 --- a/vulnerabilities/pipelines/v2_improvers/unfurl_version_range.py +++ b/vulnerabilities/pipelines/v2_improvers/unfurl_version_range.py @@ -14,6 +14,7 @@ from aboutcode.pipeline import LoopProgress from django.db.models import F from django.db.models import Q +from django.db import transaction from django.utils import timezone from fetchcode.package_versions import SUPPORTED_ECOSYSTEMS as FETCHCODE_SUPPORTED_ECOSYSTEMS from packageurl import PackageURL @@ -26,6 +27,7 @@ from vulnerabilities.models import PipelineSchedule from vulnerabilities.pipelines import VulnerableCodePipeline from vulnerabilities.pipes.fetchcode_utils import get_versions +from vulnerabilities.pipes.group_advisories import group_advisory_for_package from vulnerabilities.utils import update_purl_version @@ -159,6 +161,7 @@ def get_purl_versions(purl, cached_versions, logger): return cached_versions.get(purl) or [] +@transaction.atomic def bulk_create_with_m2m(purls, impact, relation, logger): """Bulk create PackageV2 and also bulk populate M2M Impact and Package relationships.""" if not purls: @@ -175,6 +178,9 @@ def bulk_create_with_m2m(purls, impact, relation, logger): relation(impacted_package=impact, package=package) for package in affected_packages_v2 ] + for pkg in affected_packages_v2: + group_advisory_for_package(pkg, logger=logger) + try: relation.objects.bulk_create(relations, ignore_conflicts=True) except Exception as e: diff --git a/vulnerabilities/pipes/advisory.py b/vulnerabilities/pipes/advisory.py index 732e2e0ab..d4f45a38c 100644 --- a/vulnerabilities/pipes/advisory.py +++ b/vulnerabilities/pipes/advisory.py @@ -47,6 +47,7 @@ from vulnerabilities.models import VulnerabilityRelatedReference from vulnerabilities.models import VulnerabilitySeverity from vulnerabilities.models import Weakness +from vulnerabilities.pipes.group_advisories import group_advisory_for_package from vulnerabilities.pipes.univers_utils import get_exact_purls_v2 @@ -387,6 +388,10 @@ def insert_advisory_v2( purls=package_fixed_purls ) + for pkg in list(affected_packages_v2) + list(fixed_packages_v2): + logger(f"Grouping advisories for package: {pkg.purl}") + group_advisory_for_package(pkg, logger=logger) + impact.affecting_packages.add(*affected_packages_v2) impact.fixed_by_packages.add(*fixed_packages_v2) diff --git a/vulnerabilities/pipes/group_advisories.py b/vulnerabilities/pipes/group_advisories.py index 983ac3386..d72f0872b 100644 --- a/vulnerabilities/pipes/group_advisories.py +++ b/vulnerabilities/pipes/group_advisories.py @@ -7,14 +7,16 @@ # See https://aboutcode.org for more information about nexB OSS projects. # +from collections import defaultdict +from typing import List + from django.db import transaction +from vulnerabilities.models import AdvisorySet, AdvisoryV2, AdvisorySetMember +from vulnerabilities.models import Group @transaction.atomic def delete_and_save_advisory_set(groups, package, relation=None): - from vulnerabilities.models import AdvisorySet - from vulnerabilities.models import AdvisorySetMember - from vulnerabilities.models import Group AdvisorySet.objects.filter(package=package, relation_type=relation).delete() @@ -50,3 +52,54 @@ def delete_and_save_advisory_set(groups, package, relation=None): ) AdvisorySetMember.objects.bulk_create(membership_to_create) + + +def group_advisory_for_package(package, logger=None): + affecting_advisories = AdvisoryV2.objects.latest_affecting_advisories_for_purl( + purl=package.purl + ).prefetch_related( + "aliases", + "impacted_packages__affecting_packages", + "impacted_packages__fixed_by_packages", + ) + + fixed_by_advisories = AdvisoryV2.objects.latest_fixed_by_advisories_for_purl( + purl=package.purl + ).prefetch_related( + "aliases", + "impacted_packages__affecting_packages", + "impacted_packages__fixed_by_packages", + ) + + try: + affected_groups: List[Group] = merge_advisories(affecting_advisories, package) + fixed_by_groups: List[Group] = merge_advisories(fixed_by_advisories, package) + delete_and_save_advisory_set(affected_groups, package, relation="affecting") + delete_and_save_advisory_set(fixed_by_groups, package, relation="fixing") + logger(f"Successfully rebuilt advisory sets for package {package.purl}") + except Exception as e: + logger(f"Failed rebuilding advisory sets for package {package.purl}: {e!r}") + return + +def merge_advisories(advisories, package): + """ + Merge advisories based on their content hash and identifiers. + """ + from vulnerabilities.utils import compute_advisory_content_hash + from vulnerabilities.utils import get_merged_identifier_groups + + advisories = list(advisories) + + content_hash_map = defaultdict(list) + + for adv in advisories: + content_hash = compute_advisory_content_hash(adv, package) + content_hash_map[content_hash].append(adv) + + final_groups: List[Group] = [] + + for group in content_hash_map.values(): + groups = get_merged_identifier_groups(group) + final_groups.extend(groups) + + return final_groups diff --git a/vulnerabilities/utils.py b/vulnerabilities/utils.py index 48d3b5384..86b6919ad 100644 --- a/vulnerabilities/utils.py +++ b/vulnerabilities/utils.py @@ -44,7 +44,6 @@ from univers.version_range import VersionRange from aboutcode.hashid import build_vcid -from vulnerabilities.pipes.group_advisories import delete_and_save_advisory_set logger = logging.getLogger(__name__) @@ -869,29 +868,6 @@ def compute_patch_checksum(patch_text: str): return hashlib.sha512(patch_text.encode("utf-8")).hexdigest() -def merge_advisories(advisories, package): - """ - Merge advisories based on their content hash and identifiers. - """ - from vulnerabilities.models import Group - - advisories = list(advisories) - - content_hash_map = defaultdict(list) - - for adv in advisories: - content_hash = compute_advisory_content_hash(adv, package) - content_hash_map[content_hash].append(adv) - - final_groups: List[Group] = [] - - for group in content_hash_map.values(): - groups = get_merged_identifier_groups(group) - final_groups.extend(groups) - - return final_groups - - def compute_advisory_content_hash(adv, package): """Compute a content hash for an advisory based on its affected and fixed packages for a given package. This is used to determine if two advisories are the same based on their content.""" diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 3aff06768..d7a23ffab 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -390,41 +390,41 @@ def get_context_data(self, **kwargs): return context - if package.type in TYPES_WITH_MULTIPLE_IMPORTERS: - affecting_advisories = AdvisoryV2.objects.latest_affecting_advisories_for_purl( - purl=package.purl - ) - - fixed_by_advisories = AdvisoryV2.objects.latest_fixed_by_advisories_for_purl( - purl=package.purl - ) - fixed_pkg_details = get_fixed_package_details(package) - context["fixed_package_details"] = fixed_pkg_details - context["grouped"] = True - - affecting_advisories = affecting_advisories.prefetch_related( - "aliases", - "impacted_packages__affecting_packages", - "impacted_packages__fixed_by_packages", - ) - - affected_by_advisories: List[GroupedAdvisory] = merge_and_save_grouped_advisories( - package, affecting_advisories, "affecting" - ) - - fixed_by_advisories = fixed_by_advisories.prefetch_related( - "aliases", - "impacted_packages__affecting_packages", - "impacted_packages__fixed_by_packages", - ) - - fixing_advisories: List[GroupedAdvisory] = merge_and_save_grouped_advisories( - package, fixed_by_advisories, "fixing" - ) - - context["affected_by_advisories_v2"] = affected_by_advisories - context["fixing_advisories_v2"] = fixing_advisories - return context + # if package.type in TYPES_WITH_MULTIPLE_IMPORTERS: + # affecting_advisories = AdvisoryV2.objects.latest_affecting_advisories_for_purl( + # purl=package.purl + # ) + + # fixed_by_advisories = AdvisoryV2.objects.latest_fixed_by_advisories_for_purl( + # purl=package.purl + # ) + # fixed_pkg_details = get_fixed_package_details(package) + # context["fixed_package_details"] = fixed_pkg_details + # context["grouped"] = True + + # affecting_advisories = affecting_advisories.prefetch_related( + # "aliases", + # "impacted_packages__affecting_packages", + # "impacted_packages__fixed_by_packages", + # ) + + # affected_by_advisories: List[GroupedAdvisory] = merge_and_save_grouped_advisories( + # package, affecting_advisories, "affecting" + # ) + + # fixed_by_advisories = fixed_by_advisories.prefetch_related( + # "aliases", + # "impacted_packages__affecting_packages", + # "impacted_packages__fixed_by_packages", + # ) + + # fixing_advisories: List[GroupedAdvisory] = merge_and_save_grouped_advisories( + # package, fixed_by_advisories, "fixing" + # ) + + # context["affected_by_advisories_v2"] = affected_by_advisories + # context["fixing_advisories_v2"] = fixing_advisories + # return context def get_object(self, queryset=None): if queryset is None: From 68972109d65a23264a20871821dd8d455c13ed89 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 27 May 2026 18:11:37 +0530 Subject: [PATCH 09/13] Calculate risk score at time of advisory insertion Signed-off-by: Tushar Goel --- .../v2_improvers/unfurl_version_range.py | 13 +++- vulnerabilities/pipes/advisory.py | 50 +++++++++++- vulnerabilities/pipes/group_advisories.py | 15 +++- vulnerabilities/pipes/risk_score.py | 77 +++++++++++++++++++ 4 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 vulnerabilities/pipes/risk_score.py diff --git a/vulnerabilities/pipelines/v2_improvers/unfurl_version_range.py b/vulnerabilities/pipelines/v2_improvers/unfurl_version_range.py index 3540b45b7..6e8831906 100644 --- a/vulnerabilities/pipelines/v2_improvers/unfurl_version_range.py +++ b/vulnerabilities/pipelines/v2_improvers/unfurl_version_range.py @@ -12,9 +12,9 @@ from traceback import format_exc as traceback_format_exc from aboutcode.pipeline import LoopProgress +from django.db import transaction from django.db.models import F from django.db.models import Q -from django.db import transaction from django.utils import timezone from fetchcode.package_versions import SUPPORTED_ECOSYSTEMS as FETCHCODE_SUPPORTED_ECOSYSTEMS from packageurl import PackageURL @@ -28,6 +28,7 @@ from vulnerabilities.pipelines import VulnerableCodePipeline from vulnerabilities.pipes.fetchcode_utils import get_versions from vulnerabilities.pipes.group_advisories import group_advisory_for_package +from vulnerabilities.pipes.risk_score import compute_package_risk_score from vulnerabilities.utils import update_purl_version @@ -178,15 +179,19 @@ def bulk_create_with_m2m(purls, impact, relation, logger): relation(impacted_package=impact, package=package) for package in affected_packages_v2 ] - for pkg in affected_packages_v2: - group_advisory_for_package(pkg, logger=logger) - try: relation.objects.bulk_create(relations, ignore_conflicts=True) except Exception as e: logger(f"Error creating ImpactedPackage {relation}: {e!r} \n {traceback_format_exc()}") return 0 + for pkg in affected_packages_v2: + group_advisory_for_package(pkg, logger=logger) + risk_score = compute_package_risk_score(pkg) + logger(f"Computed risk score {risk_score} for package {pkg.purl}") + pkg.risk_score = risk_score + pkg.save() + return len(relations) diff --git a/vulnerabilities/pipes/advisory.py b/vulnerabilities/pipes/advisory.py index d4f45a38c..5ec920969 100644 --- a/vulnerabilities/pipes/advisory.py +++ b/vulnerabilities/pipes/advisory.py @@ -48,6 +48,8 @@ from vulnerabilities.models import VulnerabilitySeverity from vulnerabilities.models import Weakness from vulnerabilities.pipes.group_advisories import group_advisory_for_package +from vulnerabilities.pipes.risk_score import compute_advisory_risk_score +from vulnerabilities.pipes.risk_score import compute_package_risk_score from vulnerabilities.pipes.univers_utils import get_exact_purls_v2 @@ -363,6 +365,12 @@ def insert_advisory_v2( if values: getattr(advisory_obj, field_name).add(*values) + weighted_severity, exploitability, risk_score = compute_advisory_risk_score(advisory_obj) + advisory_obj.weighted_severity = round(weighted_severity, 1) if weighted_severity is not None else None + advisory_obj.exploitability = round(exploitability, 1) if exploitability is not None else None + advisory_obj.risk_score = round(risk_score, 1) if risk_score is not None else None + advisory_obj.save() + for affected_pkg in advisory.affected_packages: impact = ImpactedPackage.objects.create( advisory=advisory_obj, @@ -388,9 +396,47 @@ def insert_advisory_v2( purls=package_fixed_purls ) - for pkg in list(affected_packages_v2) + list(fixed_packages_v2): + for pkg in list(affected_packages_v2): + logger(f"Grouping advisories for package: {pkg.purl}") + try: + group_advisory_for_package( + pkg, + logger=logger, + current_advisory=advisory_obj, + current_advisory_relation="affecting", + ) + except Exception as e: + logger( + f"Failed to group advisories for package {pkg.purl}: {e!r} \n {traceback_format_exc()}", + level=logging.ERROR, + ) + try: + risk_score = compute_package_risk_score(pkg, current_advisory_risk_score=advisory_obj.risk_score) + logger(f"Computed risk score {risk_score} for package {pkg.purl}") + pkg.risk_score = risk_score + pkg.save() + pkg.calculate_version_rank + except Exception as e: + logger( + f"Failed to compute risk score for package {pkg.purl}: {e!r} \n {traceback_format_exc()}", + level=logging.ERROR, + ) + + for pkg in list(fixed_packages_v2): logger(f"Grouping advisories for package: {pkg.purl}") - group_advisory_for_package(pkg, logger=logger) + try: + group_advisory_for_package( + pkg, + logger=logger, + current_advisory=advisory_obj, + current_advisory_relation="fixing", + ) + except Exception as e: + logger( + f"Failed to group advisories for package {pkg.purl}: {e!r} \n {traceback_format_exc()}", + level=logging.ERROR, + ) + pkg.calculate_version_rank impact.affecting_packages.add(*affected_packages_v2) impact.fixed_by_packages.add(*fixed_packages_v2) diff --git a/vulnerabilities/pipes/group_advisories.py b/vulnerabilities/pipes/group_advisories.py index d72f0872b..04093a12d 100644 --- a/vulnerabilities/pipes/group_advisories.py +++ b/vulnerabilities/pipes/group_advisories.py @@ -11,7 +11,10 @@ from typing import List from django.db import transaction -from vulnerabilities.models import AdvisorySet, AdvisoryV2, AdvisorySetMember + +from vulnerabilities.models import AdvisorySet +from vulnerabilities.models import AdvisorySetMember +from vulnerabilities.models import AdvisoryV2 from vulnerabilities.models import Group @@ -54,7 +57,9 @@ def delete_and_save_advisory_set(groups, package, relation=None): AdvisorySetMember.objects.bulk_create(membership_to_create) -def group_advisory_for_package(package, logger=None): +def group_advisory_for_package( + package, logger=None, current_advisory=None, current_advisory_relation=None +): affecting_advisories = AdvisoryV2.objects.latest_affecting_advisories_for_purl( purl=package.purl ).prefetch_related( @@ -71,6 +76,11 @@ def group_advisory_for_package(package, logger=None): "impacted_packages__fixed_by_packages", ) + if current_advisory and current_advisory_relation == "affecting": + affecting_advisories = list(affecting_advisories) + [current_advisory] + elif current_advisory and current_advisory_relation == "fixing": + fixed_by_advisories = list(fixed_by_advisories) + [current_advisory] + try: affected_groups: List[Group] = merge_advisories(affecting_advisories, package) fixed_by_groups: List[Group] = merge_advisories(fixed_by_advisories, package) @@ -81,6 +91,7 @@ def group_advisory_for_package(package, logger=None): logger(f"Failed rebuilding advisory sets for package {package.purl}: {e!r}") return + def merge_advisories(advisories, package): """ Merge advisories based on their content hash and identifiers. diff --git a/vulnerabilities/pipes/risk_score.py b/vulnerabilities/pipes/risk_score.py new file mode 100644 index 000000000..24cc19a5b --- /dev/null +++ b/vulnerabilities/pipes/risk_score.py @@ -0,0 +1,77 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# VulnerableCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/aboutcode-org/vulnerablecode for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + +from django.db.models import Max + +from vulnerabilities.models import AdvisoryV2 + +from decimal import Decimal, ROUND_HALF_UP + +def quantize_1(value): + if value is None: + return None + + return Decimal(str(value)).quantize( + Decimal("0.1"), + rounding=ROUND_HALF_UP, + ) + + +def compute_package_risk_score(package, current_advisory_risk_score=None): + """Calculate the risk score for a single PackageV2 object.""" + max_risk = ( + AdvisoryV2.objects.latest_affecting_advisories_for_purl(package.package_url) + .aggregate(max_risk=Max("risk_score")) + .get("max_risk") + ) + # include current advisory risk score in the calculation if provided and is higher than the max risk score from the database + if current_advisory_risk_score is not None: + max_risk = max(max_risk or 0, current_advisory_risk_score) + if max_risk is None: + return None + return round(float(max_risk), 1) + + +def compute_advisory_risk_score(advisory): + """ + Calculate the risk score for a single AdvisoryV2 object. + Returns a tuple of (weighted_severity, exploitability, risk_score). + """ + from vulnerabilities.risk import compute_vulnerability_risk_factors + + weighted_severity = None + exploitability = None + risk_score = None + + references = advisory.references.all() + exploits = advisory.exploits.all() + + severities = list(advisory.severities.all()) + + for rel in advisory.related_advisory_severities.all(): + severities.extend(rel.severities.all()) + + try: + calculated_weighted_severity, calculated_exploitability = ( + compute_vulnerability_risk_factors( + references=references, + severities=severities, + exploits=exploits, + ) + ) + + weighted_severity = calculated_weighted_severity + exploitability = calculated_exploitability + if exploitability and weighted_severity: + risk_score = min(float(exploitability * weighted_severity), 10.0) + risk_score = round(risk_score, 1) + except Exception as e: + risk_score = None + + return quantize_1(weighted_severity), quantize_1(exploitability), quantize_1(risk_score) From 09943a8ab64d63827ad00dee54959b527653f2e4 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 27 May 2026 18:17:18 +0530 Subject: [PATCH 10/13] Add type check for grouping of individual package Signed-off-by: Tushar Goel --- vulnerabilities/pipes/group_advisories.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vulnerabilities/pipes/group_advisories.py b/vulnerabilities/pipes/group_advisories.py index 04093a12d..55672d43b 100644 --- a/vulnerabilities/pipes/group_advisories.py +++ b/vulnerabilities/pipes/group_advisories.py @@ -60,6 +60,14 @@ def delete_and_save_advisory_set(groups, package, relation=None): def group_advisory_for_package( package, logger=None, current_advisory=None, current_advisory_relation=None ): + """ + Group advisories for a given package and save the advisory sets for the package. + """ + from vulnerabilities.utils import TYPES_WITH_MULTIPLE_IMPORTERS + + if package.type not in TYPES_WITH_MULTIPLE_IMPORTERS: + return + affecting_advisories = AdvisoryV2.objects.latest_affecting_advisories_for_purl( purl=package.purl ).prefetch_related( From 2ac09422181316e0547ec2470e0599a7b2a86f26 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 27 May 2026 18:22:12 +0530 Subject: [PATCH 11/13] Add Changelog Signed-off-by: Tushar Goel --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f701c0802..9960233e2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,8 @@ next release --------------------- - WARNING: Vulnerablecode V1 API and UI has stopped supporting Ubuntu OVAL advisories, please shift to V3 API for new Ubuntu advisories. +- WARNING: We will deprecate improver pipelines for calculating package version rank, grouping advisories for packages and calculating risk scores in the next release, we are doing it at advisory import time instead of as separate pipelines, this will improve the performance and consistency of the data. +- Calculate package verion rank, group advisories for packages and package risk score and advisory risk score during import of advisories. - Add attribute ``pipeline_id`` to AdvisoryV2 to track the pipeline that created the advisory, also rename existing ``datasource_id`` and AVIDs. Version v38.6.0 From 6747562fb533d3afaaafeb1df06a58127f4f58ea Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 27 May 2026 20:25:38 +0530 Subject: [PATCH 12/13] Remove group advisories and version rank pipeline Signed-off-by: Tushar Goel --- vulnerabilities/improvers/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/vulnerabilities/improvers/__init__.py b/vulnerabilities/improvers/__init__.py index 5b47a7cf1..b1db301d4 100644 --- a/vulnerabilities/improvers/__init__.py +++ b/vulnerabilities/improvers/__init__.py @@ -11,7 +11,6 @@ from vulnerabilities.improvers import vulnerability_status from vulnerabilities.pipelines import add_cvss31_to_CVEs from vulnerabilities.pipelines import compute_package_risk -from vulnerabilities.pipelines import compute_package_version_rank from vulnerabilities.pipelines import enhance_with_exploitdb from vulnerabilities.pipelines import enhance_with_kev from vulnerabilities.pipelines import enhance_with_metasploit @@ -32,7 +31,6 @@ enhance_with_metasploit as enhance_with_metasploit_v2, ) from vulnerabilities.pipelines.v2_improvers import flag_ghost_packages as flag_ghost_packages_v2 -from vulnerabilities.pipelines.v2_improvers import group_advisories_for_packages from vulnerabilities.pipelines.v2_improvers import reference_collect_commits from vulnerabilities.pipelines.v2_improvers import relate_severities from vulnerabilities.pipelines.v2_improvers import unfurl_version_range as unfurl_version_range_v2 @@ -61,7 +59,6 @@ enhance_with_metasploit.MetasploitImproverPipeline, enhance_with_exploitdb.ExploitDBImproverPipeline, compute_package_risk.ComputePackageRiskPipeline, - compute_package_version_rank.ComputeVersionRankPipeline, add_cvss31_to_CVEs.CVEAdvisoryMappingPipeline, remove_duplicate_advisories.RemoveDuplicateAdvisoriesPipeline, populate_vulnerability_summary_pipeline.PopulateVulnerabilitySummariesPipeline, @@ -75,7 +72,6 @@ collect_ssvc_trees.CollectSSVCPipeline, relate_severities.RelateSeveritiesPipeline, archive_urls.ArchiveImproverPipeline, - group_advisories_for_packages.GroupAdvisoriesForPackages, compute_advisory_todo_v2.ComputeToDo, reference_collect_commits.CollectReferencesFixCommitsPipeline, enhance_with_github_poc.GithubPocsImproverPipeline, From 887602e150f159b49c57cda804ac98334837e2b9 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 27 May 2026 20:28:50 +0530 Subject: [PATCH 13/13] Remove group advisories and version rank pipeline Signed-off-by: Tushar Goel --- vulnerabilities/api_v3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vulnerabilities/api_v3.py b/vulnerabilities/api_v3.py index beb65f91e..973db8132 100644 --- a/vulnerabilities/api_v3.py +++ b/vulnerabilities/api_v3.py @@ -236,7 +236,7 @@ def get_affected_by_vulnerabilities(self, package): result.append( { - "advisory_id": adv["identifier"], + "advisory_id": adv["advisory_id"], "advisory_uid": adv["advisory_uid"], "aliases": adv["aliases"], "summary": adv["summary"],