Skip to content

Commit b7f98b4

Browse files
committed
Handle multiple AVIDs
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 0828277 commit b7f98b4

File tree

9 files changed

+226
-160
lines changed

9 files changed

+226
-160
lines changed

vulnerabilities/api_v2.py

Lines changed: 106 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ def get_fixing_vulnerabilities(self, obj):
332332
return [vuln.vulnerability_id for vuln in obj.fixing_vulnerabilities.all()]
333333

334334

335-
class AdvisoryPackageV2Serializer(serializers.ModelSerializer):
335+
class PackageV3Serializer(serializers.ModelSerializer):
336336
purl = serializers.CharField(source="package_url")
337337
risk_score = serializers.FloatField(read_only=True)
338338
affected_by_vulnerabilities = serializers.SerializerMethodField()
@@ -353,26 +353,38 @@ class Meta:
353353

354354
def get_affected_by_vulnerabilities(self, package):
355355
"""Return a dictionary with advisory as keys and their details, including fixed_by_packages."""
356+
impacts = package.affected_in_impacts.select_related("advisory").prefetch_related(
357+
"fixed_by_packages"
358+
)
359+
360+
avids = {impact.advisory.avid for impact in impacts if impact.advisory_id}
361+
362+
latest_advisories = AdvisoryV2.objects.latest_for_avids(avids)
363+
advisory_by_avid = {adv.avid: adv for adv in latest_advisories}
364+
356365
result = {}
357-
request = self.context.get("request")
358-
for impact in package.affected_in_impacts.all():
359-
advisory = impact.advisory
366+
367+
for impact in impacts:
368+
avid = impact.advisory.avid
369+
advisory = advisory_by_avid.get(avid)
370+
if not advisory:
371+
continue
360372
fixed_by_packages = [pkg.purl for pkg in impact.fixed_by_packages.all()]
361-
code_fixes = CodeFixV2.objects.filter(advisory=advisory).distinct()
362-
code_fix_urls = [
363-
reverse("advisory-codefix-detail", args=[code_fix.id], request=request)
364-
for code_fix in code_fixes
365-
]
366373
result[advisory.avid] = {
367374
"advisory_id": advisory.avid,
368375
"fixed_by_packages": fixed_by_packages,
369-
"code_fixes": code_fix_urls,
370376
}
371377

372378
return result
373379

374380
def get_fixing_vulnerabilities(self, package):
375-
return [impact.advisory.avid for impact in package.fixed_in_impacts.all()]
381+
impacts = package.fixed_in_impacts.select_related("advisory")
382+
383+
avids = {impact.advisory.avid for impact in impacts if impact.advisory_id}
384+
385+
latest_advisories = AdvisoryV2.objects.latest_for_avids(avids)
386+
387+
return [adv.avid for adv in latest_advisories]
376388

377389
def get_next_non_vulnerable_version(self, package):
378390
if next_non_vulnerable := package.get_non_vulnerable_versions()[0]:
@@ -1013,9 +1025,9 @@ def get_view_name(self):
10131025
return "Pipeline Jobs"
10141026

10151027

1016-
class AdvisoriesPackageV2ViewSet(viewsets.ReadOnlyModelViewSet):
1028+
class PackageV3ViewSet(viewsets.ReadOnlyModelViewSet):
10171029
queryset = PackageV2.objects.all()
1018-
serializer_class = AdvisoryPackageV2Serializer
1030+
serializer_class = PackageV3Serializer
10191031
filter_backends = [filters.DjangoFilterBackend]
10201032
filterset_class = AdvisoryPackageV2FilterSet
10211033

@@ -1039,35 +1051,42 @@ def get_queryset(self):
10391051
)
10401052

10411053
def list(self, request, *args, **kwargs):
1042-
filtered_queryset = self.filter_queryset(self.get_queryset())
1043-
page = self.paginate_queryset(filtered_queryset)
1054+
queryset = self.filter_queryset(self.get_queryset())
1055+
page = self.paginate_queryset(queryset)
10441056

1045-
advisories = set()
1046-
if page is not None:
1047-
for package in page:
1048-
advisories.update({impact.advisory for impact in package.affected_in_impacts.all()})
1049-
advisories.update({impact.advisory for impact in package.fixed_in_impacts.all()})
1057+
packages = page if page is not None else queryset
10501058

1051-
# Serialize the vulnerabilities with advisory_id and advisory label as keys
1052-
advisory_data = {f"{adv.avid}": AdvisoryV2Serializer(adv).data for adv in advisories}
1059+
avids = set()
10531060

1054-
# Serialize the current page of packages
1055-
serializer = self.get_serializer(page, many=True)
1056-
data = serializer.data
1061+
for package in packages:
1062+
for impact in package.affected_in_impacts.all():
1063+
if impact.advisory_id:
1064+
avids.add(impact.advisory.avid)
10571065

1058-
# Use 'self.get_paginated_response' to include pagination data
1059-
return self.get_paginated_response({"advisories": advisory_data, "packages": data})
1066+
for impact in package.fixed_in_impacts.all():
1067+
if impact.advisory_id:
1068+
avids.add(impact.advisory.avid)
10601069

1061-
# If pagination is not applied, collect vulnerabilities for all packages
1062-
for package in queryset:
1063-
advisories.update({impact.advisory for impact in package.affected_in_impacts.all()})
1064-
advisories.update({impact.advisory for impact in package.fixed_in_impacts.all()})
1070+
latest_advisories = AdvisoryV2.objects.latest_for_avids(avids)
10651071

1066-
advisory_data = {f"{adv.avid}": AdvisoryV2Serializer(adv).data for adv in advisories}
1072+
advisory_data = {adv.avid: AdvisoryV2Serializer(adv).data for adv in latest_advisories}
10671073

1068-
serializer = self.get_serializer(queryset, many=True)
1069-
data = serializer.data
1070-
return Response({"advisories": advisory_data, "packages": data})
1074+
serializer = self.get_serializer(packages, many=True)
1075+
1076+
if page is not None:
1077+
return self.get_paginated_response(
1078+
{
1079+
"packages": serializer.data,
1080+
"advisories": advisory_data,
1081+
}
1082+
)
1083+
1084+
return Response(
1085+
{
1086+
"packages": serializer.data,
1087+
"advisories": advisory_data,
1088+
}
1089+
)
10711090

10721091
@extend_schema(
10731092
request=PackageurlListSerializer,
@@ -1093,17 +1112,16 @@ def bulk_lookup(self, request):
10931112
"message": "A non-empty 'purls' list of PURLs is required.",
10941113
},
10951114
)
1096-
validated_data = serializer.validated_data
1097-
purls = validated_data.get("purls")
10981115

1099-
# Fetch packages matching the provided purls
1116+
purls = serializer.validated_data.get("purls")
1117+
11001118
packages = (
11011119
PackageV2.objects.for_purls(purls)
11021120
.prefetch_related(
11031121
Prefetch(
11041122
"affected_in_impacts",
11051123
queryset=ImpactedPackage.objects.select_related("advisory").prefetch_related(
1106-
"fixed_by_packages",
1124+
"fixed_by_packages"
11071125
),
11081126
),
11091127
Prefetch(
@@ -1114,26 +1132,34 @@ def bulk_lookup(self, request):
11141132
.with_is_vulnerable()
11151133
)
11161134

1117-
# Collect vulnerabilities associated with these packages
1118-
advisories = set()
1135+
avids = set()
1136+
11191137
for package in packages:
1120-
advisories.update({impact.advisory for impact in package.affected_in_impacts.all()})
1121-
advisories.update({impact.advisory for impact in package.fixed_in_impacts.all()})
1138+
for impact in package.affected_in_impacts.all():
1139+
if impact.advisory_id:
1140+
avids.add(impact.advisory.avid)
11221141

1123-
# Serialize vulnerabilities with vulnerability_id as keys
1124-
advisory_data = {adv.avid: AdvisoryV2Serializer(adv).data for adv in advisories}
1142+
for impact in package.fixed_in_impacts.all():
1143+
if impact.advisory_id:
1144+
avids.add(impact.advisory.avid)
11251145

1126-
# Serialize packages
1127-
package_data = AdvisoryPackageV2Serializer(
1146+
latest_advisories = AdvisoryV2.objects.latest_for_avids(avids)
1147+
1148+
advisory_data = {
1149+
adv.avid: AdvisoryV2Serializer(adv, context={"request": request}).data
1150+
for adv in latest_advisories
1151+
}
1152+
1153+
package_data = PackageV3Serializer(
11281154
packages,
11291155
many=True,
11301156
context={"request": request},
11311157
).data
11321158

11331159
return Response(
11341160
{
1135-
"advisories": advisory_data,
11361161
"packages": package_data,
1162+
"advisories": advisory_data,
11371163
}
11381164
)
11391165

@@ -1161,6 +1187,7 @@ def bulk_search(self, request):
11611187
"message": "A non-empty 'purls' list of PURLs is required.",
11621188
},
11631189
)
1190+
11641191
validated_data = serializer.validated_data
11651192
purls = validated_data.get("purls")
11661193
purl_only = validated_data.get("purl_only", False)
@@ -1202,24 +1229,31 @@ def bulk_search(self, request):
12021229

12031230
packages = query
12041231

1205-
# Collect vulnerabilities associated with these packages
1206-
advisories = set()
1232+
avids = set()
12071233
for package in packages:
1208-
advisories.update({impact.advisory for impact in package.affected_in_impacts.all()})
1209-
advisories.update({impact.advisory for impact in package.fixed_in_impacts.all()})
1210-
1211-
advisory_data = {adv.avid: AdvisoryV2Serializer(adv).data for adv in advisories}
1234+
for impact in package.affected_in_impacts.all():
1235+
if impact.advisory_id:
1236+
avids.add(impact.advisory.avid)
1237+
for impact in package.fixed_in_impacts.all():
1238+
if impact.advisory_id:
1239+
avids.add(impact.advisory.avid)
1240+
1241+
latest_advisories = AdvisoryV2.objects.latest_for_avids(avids)
1242+
advisory_data = {
1243+
adv.avid: AdvisoryV2Serializer(adv, context={"request": request}).data
1244+
for adv in latest_advisories
1245+
}
12121246

12131247
if not purl_only:
1214-
package_data = AdvisoryPackageV2Serializer(
1248+
package_data = PackageV3Serializer(
12151249
packages,
12161250
many=True,
12171251
context={"request": request},
12181252
).data
12191253
return Response(
12201254
{
1221-
"advisories": advisory_data,
12221255
"packages": package_data,
1256+
"advisories": advisory_data,
12231257
}
12241258
)
12251259

@@ -1249,24 +1283,31 @@ def bulk_search(self, request):
12491283
)
12501284
packages = query
12511285

1252-
# Collect vulnerabilities associated with these packages
1253-
advisories = set()
1286+
avids = set()
12541287
for package in packages:
1255-
advisories.update({impact.advisory for impact in package.affected_in_impacts.all()})
1256-
advisories.update({impact.advisory for impact in package.fixed_in_impacts.all()})
1257-
1258-
advisory_data = {adv.advisory_id: AdvisoryV2Serializer(adv).data for adv in advisories}
1288+
for impact in package.affected_in_impacts.all():
1289+
if impact.advisory_id:
1290+
avids.add(impact.advisory.avid)
1291+
for impact in package.fixed_in_impacts.all():
1292+
if impact.advisory_id:
1293+
avids.add(impact.advisory.avid)
1294+
1295+
latest_advisories = AdvisoryV2.objects.latest_for_avids(avids)
1296+
advisory_data = {
1297+
adv.avid: AdvisoryV2Serializer(adv, context={"request": request}).data
1298+
for adv in latest_advisories
1299+
}
12591300

12601301
if not purl_only:
1261-
package_data = AdvisoryPackageV2Serializer(
1302+
package_data = PackageV3Serializer(
12621303
packages,
12631304
many=True,
12641305
context={"request": request},
12651306
).data
12661307
return Response(
12671308
{
1268-
"advisories": advisory_data,
12691309
"packages": package_data,
1310+
"advisories": advisory_data,
12701311
}
12711312
)
12721313

@@ -1316,6 +1357,4 @@ def lookup(self, request):
13161357
purl = validated_data.get("purl")
13171358

13181359
qs = self.get_queryset().for_purls([purl]).with_is_vulnerable()
1319-
return Response(
1320-
AdvisoryPackageV2Serializer(qs, many=True, context={"request": request}).data
1321-
)
1360+
return Response(PackageV3Serializer(qs, many=True, context={"request": request}).data)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 4.2.25 on 2025-12-29 08:17
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0107_remove_advisoryv2_date_imported"),
10+
]
11+
12+
operations = [
13+
migrations.AddIndex(
14+
model_name="advisoryv2",
15+
index=models.Index(
16+
fields=["avid", "-date_collected", "-id"], name="advisory_latest_by_avid_idx"
17+
),
18+
),
19+
]

0 commit comments

Comments
 (0)