Skip to content

Commit 5121e31

Browse files
authored
Merge pull request #1970 from aboutcode-org/unmerge-affected-range
Add ImpactedPackage model to track affected and fixed packages
2 parents b560955 + 4ef60d2 commit 5121e31

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+959
-1058
lines changed

vulnerabilities/api_v2.py

Lines changed: 133 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from vulnerabilities.models import AdvisoryWeakness
3232
from vulnerabilities.models import CodeFix
3333
from vulnerabilities.models import CodeFixV2
34+
from vulnerabilities.models import ImpactedPackage
3435
from vulnerabilities.models import Package
3536
from vulnerabilities.models import PackageV2
3637
from vulnerabilities.models import PipelineRun
@@ -42,6 +43,10 @@
4243
from vulnerabilities.throttling import PermissionBasedUserRateThrottle
4344

4445

46+
class CharInFilter(filters.BaseInFilter, filters.CharFilter):
47+
pass
48+
49+
4550
class WeaknessV2Serializer(serializers.ModelSerializer):
4651
cwe_id = serializers.CharField()
4752
name = serializers.CharField()
@@ -306,8 +311,8 @@ class AdvisoryPackageV2Serializer(serializers.ModelSerializer):
306311
risk_score = serializers.FloatField(read_only=True)
307312
affected_by_vulnerabilities = serializers.SerializerMethodField()
308313
fixing_vulnerabilities = serializers.SerializerMethodField()
309-
next_non_vulnerable_version = serializers.CharField(read_only=True)
310-
latest_non_vulnerable_version = serializers.CharField(read_only=True)
314+
next_non_vulnerable_version = serializers.SerializerMethodField()
315+
latest_non_vulnerable_version = serializers.SerializerMethodField()
311316

312317
class Meta:
313318
model = Package
@@ -320,36 +325,36 @@ class Meta:
320325
"risk_score",
321326
]
322327

323-
def get_affected_by_vulnerabilities(self, obj):
324-
"""
325-
Return a dictionary with vulnerabilities as keys and their details, including fixed_by_packages.
326-
"""
328+
def get_affected_by_vulnerabilities(self, package):
329+
"""Return a dictionary with advisory as keys and their details, including fixed_by_packages."""
327330
result = {}
328331
request = self.context.get("request")
329-
for adv in getattr(obj, "prefetched_affected_advisories", []):
330-
fixed_by_package = adv.fixed_by_packages.first()
331-
purl = None
332-
if fixed_by_package:
333-
purl = fixed_by_package.package_url
334-
# Get code fixed for a vulnerability
335-
code_fixes = CodeFixV2.objects.filter(advisory=adv).distinct()
332+
for impact in package.affected_in_impacts.all():
333+
advisory = impact.advisory
334+
fixed_by_packages = [pkg.purl for pkg in impact.fixed_by_packages.all()]
335+
code_fixes = CodeFixV2.objects.filter(advisory=advisory).distinct()
336336
code_fix_urls = [
337337
reverse("advisory-codefix-detail", args=[code_fix.id], request=request)
338338
for code_fix in code_fixes
339339
]
340-
341-
result[adv.avid] = {
342-
"advisory_id": adv.avid,
343-
"fixed_by_packages": purl,
340+
result[advisory.avid] = {
341+
"advisory_id": advisory.avid,
342+
"fixed_by_packages": fixed_by_packages,
344343
"code_fixes": code_fix_urls,
345344
}
345+
346346
return result
347347

348-
def get_fixing_vulnerabilities(self, obj):
349-
# Ghost package should not fix any vulnerability.
350-
if obj.is_ghost:
351-
return []
352-
return [adv.avid for adv in obj.fixing_advisories.all()]
348+
def get_fixing_vulnerabilities(self, package):
349+
return [impact.advisory.avid for impact in package.fixed_in_impacts.all()]
350+
351+
def get_next_non_vulnerable_version(self, package):
352+
if next_non_vulnerable := package.get_non_vulnerable_versions()[0]:
353+
return next_non_vulnerable.version
354+
355+
def get_latest_non_vulnerable_version(self, package):
356+
if latest_non_vulnerable := package.get_non_vulnerable_versions()[-1]:
357+
return latest_non_vulnerable.version
353358

354359

355360
class PackageurlListSerializer(serializers.Serializer):
@@ -381,9 +386,24 @@ class PackageV2FilterSet(filters.FilterSet):
381386

382387

383388
class AdvisoryPackageV2FilterSet(filters.FilterSet):
384-
affected_by_vulnerability = filters.CharFilter(field_name="affected_by_advisory__advisory_id")
385-
fixing_vulnerability = filters.CharFilter(field_name="fixing_advisories__advisory_id")
386-
purl = filters.CharFilter(field_name="package_url")
389+
affected_by_advisory = filters.CharFilter(
390+
field_name="affected_in_impacts__advisory__avid",
391+
label="Affected By Advisory ID",
392+
help_text="Filter packages affected by a specific Advisory ID.",
393+
)
394+
395+
fixing_advisory = filters.CharFilter(
396+
field_name="fixed_in_impacts__advisory__avid",
397+
label="Fixed By Advisory ID",
398+
help_text="Filter packages fixed by a specific Advisory ID.",
399+
)
400+
401+
purls = CharInFilter(
402+
field_name="package_url",
403+
lookup_expr="in",
404+
label="Package URL",
405+
help_text="Filter by one or more Package URLs. Multi-value supported (comma-separated).",
406+
)
387407

388408

389409
class PackageV2ViewSet(viewsets.ReadOnlyModelViewSet):
@@ -968,56 +988,54 @@ def get_view_name(self):
968988

969989

970990
class AdvisoriesPackageV2ViewSet(viewsets.ReadOnlyModelViewSet):
971-
queryset = PackageV2.objects.all().prefetch_related(
972-
Prefetch(
973-
"affected_by_advisories",
974-
queryset=AdvisoryV2.objects.prefetch_related("fixed_by_packages"),
975-
to_attr="prefetched_affected_advisories",
976-
)
977-
)
991+
queryset = PackageV2.objects.all()
978992
serializer_class = AdvisoryPackageV2Serializer
979-
filter_backends = (filters.DjangoFilterBackend,)
993+
filter_backends = [filters.DjangoFilterBackend]
980994
filterset_class = AdvisoryPackageV2FilterSet
981995

982996
def get_queryset(self):
983-
queryset = super().get_queryset()
984-
package_purls = self.request.query_params.getlist("purl")
985-
affected_by_advisory = self.request.query_params.get("affected_by_advisory")
986-
fixing_advisory = self.request.query_params.get("fixing_advisory")
987-
if package_purls:
988-
queryset = queryset.filter(package_url__in=package_purls)
989-
if affected_by_advisory:
990-
queryset = queryset.filter(affected_by_advisories__advisory_id=affected_by_advisory)
991-
if fixing_advisory:
992-
queryset = queryset.filter(fixing_advisories__advisory=fixing_advisory)
993-
return queryset.with_is_vulnerable()
997+
return (
998+
super()
999+
.get_queryset()
1000+
.prefetch_related(
1001+
Prefetch(
1002+
"affected_in_impacts",
1003+
queryset=ImpactedPackage.objects.select_related("advisory").prefetch_related(
1004+
"fixed_by_packages",
1005+
),
1006+
),
1007+
Prefetch(
1008+
"fixed_in_impacts",
1009+
queryset=ImpactedPackage.objects.select_related("advisory"),
1010+
),
1011+
)
1012+
.with_is_vulnerable()
1013+
)
9941014

9951015
def list(self, request, *args, **kwargs):
996-
queryset = self.get_queryset()
997-
# Apply pagination
998-
page = self.paginate_queryset(queryset)
1016+
filtered_queryset = self.filter_queryset(self.get_queryset())
1017+
page = self.paginate_queryset(filtered_queryset)
1018+
1019+
advisories = set()
9991020
if page is not None:
1000-
# Collect only vulnerabilities for packages in the current page
1001-
advisories = set()
10021021
for package in page:
1003-
advisories.update(package.affected_by_advisories.all())
1004-
advisories.update(package.fixing_advisories.all())
1022+
advisories.update({impact.advisory for impact in package.affected_in_impacts.all()})
1023+
advisories.update({impact.advisory for impact in package.fixed_in_impacts.all()})
10051024

10061025
# Serialize the vulnerabilities with advisory_id and advisory label as keys
10071026
advisory_data = {f"{adv.avid}": AdvisoryV2Serializer(adv).data for adv in advisories}
10081027

10091028
# Serialize the current page of packages
10101029
serializer = self.get_serializer(page, many=True)
10111030
data = serializer.data
1012-
print(data)
1031+
10131032
# Use 'self.get_paginated_response' to include pagination data
10141033
return self.get_paginated_response({"advisories": advisory_data, "packages": data})
10151034

10161035
# If pagination is not applied, collect vulnerabilities for all packages
1017-
advisories = set()
10181036
for package in queryset:
1019-
advisories.update(package.affected_by_vulnerabilities.all())
1020-
advisories.update(package.fixing_vulnerabilities.all())
1037+
advisories.update({impact.advisory for impact in package.affected_in_impacts.all()})
1038+
advisories.update({impact.advisory for impact in package.fixed_in_impacts.all()})
10211039

10221040
advisory_data = {f"{adv.avid}": AdvisoryV2Serializer(adv).data for adv in advisories}
10231041

@@ -1053,13 +1071,28 @@ def bulk_lookup(self, request):
10531071
purls = validated_data.get("purls")
10541072

10551073
# Fetch packages matching the provided purls
1056-
packages = PackageV2.objects.for_purls(purls).with_is_vulnerable()
1074+
packages = (
1075+
PackageV2.objects.for_purls(purls)
1076+
.prefetch_related(
1077+
Prefetch(
1078+
"affected_in_impacts",
1079+
queryset=ImpactedPackage.objects.select_related("advisory").prefetch_related(
1080+
"fixed_by_packages",
1081+
),
1082+
),
1083+
Prefetch(
1084+
"fixed_in_impacts",
1085+
queryset=ImpactedPackage.objects.select_related("advisory"),
1086+
),
1087+
)
1088+
.with_is_vulnerable()
1089+
)
10571090

10581091
# Collect vulnerabilities associated with these packages
10591092
advisories = set()
10601093
for package in packages:
1061-
advisories.update(package.affected_by_advisories.all())
1062-
advisories.update(package.fixing_advisories.all())
1094+
advisories.update({impact.advisory for impact in package.affected_in_impacts.all()})
1095+
advisories.update({impact.advisory for impact in package.fixed_in_impacts.all()})
10631096

10641097
# Serialize vulnerabilities with vulnerability_id as keys
10651098
advisory_data = {adv.avid: AdvisoryV2Serializer(adv).data for adv in advisories}
@@ -1124,6 +1157,20 @@ def bulk_search(self, request):
11241157
PackageV2.objects.filter(plain_package_url__in=plain_purls)
11251158
.order_by("plain_package_url")
11261159
.distinct("plain_package_url")
1160+
.prefetch_related(
1161+
Prefetch(
1162+
"affected_in_impacts",
1163+
queryset=ImpactedPackage.objects.select_related(
1164+
"advisory"
1165+
).prefetch_related(
1166+
"fixed_by_packages",
1167+
),
1168+
),
1169+
Prefetch(
1170+
"fixed_in_impacts",
1171+
queryset=ImpactedPackage.objects.select_related("advisory"),
1172+
),
1173+
)
11271174
.with_is_vulnerable()
11281175
)
11291176

@@ -1132,14 +1179,16 @@ def bulk_search(self, request):
11321179
# Collect vulnerabilities associated with these packages
11331180
advisories = set()
11341181
for package in packages:
1135-
advisories.update(package.affected_by_advisories.all())
1136-
advisories.update(package.fixing_advisories.all())
1182+
advisories.update({impact.advisory for impact in package.affected_in_impacts.all()})
1183+
advisories.update({impact.advisory for impact in package.fixed_in_impacts.all()})
11371184

11381185
advisory_data = {adv.avid: AdvisoryV2Serializer(adv).data for adv in advisories}
11391186

11401187
if not purl_only:
11411188
package_data = AdvisoryPackageV2Serializer(
1142-
packages, many=True, context={"request": request}
1189+
packages,
1190+
many=True,
1191+
context={"request": request},
11431192
).data
11441193
return Response(
11451194
{
@@ -1154,20 +1203,39 @@ def bulk_search(self, request):
11541203
vulnerable_purls = [str(package.plain_package_url) for package in vulnerable_purls]
11551204
return Response(data=vulnerable_purls)
11561205

1157-
query = PackageV2.objects.filter(package_url__in=purls).distinct().with_is_vulnerable()
1206+
query = (
1207+
PackageV2.objects.filter(package_url__in=purls)
1208+
.order_by("plain_package_url")
1209+
.distinct("plain_package_url")
1210+
.prefetch_related(
1211+
Prefetch(
1212+
"affected_in_impacts",
1213+
queryset=ImpactedPackage.objects.select_related("advisory").prefetch_related(
1214+
"fixed_by_packages",
1215+
),
1216+
),
1217+
Prefetch(
1218+
"fixed_in_impacts",
1219+
queryset=ImpactedPackage.objects.select_related("advisory"),
1220+
),
1221+
)
1222+
.with_is_vulnerable()
1223+
)
11581224
packages = query
11591225

11601226
# Collect vulnerabilities associated with these packages
11611227
advisories = set()
11621228
for package in packages:
1163-
advisories.update(package.affected_by_advisories.all())
1164-
advisories.update(package.fixing_advisories.all())
1229+
advisories.update({impact.advisory for impact in package.affected_in_impacts.all()})
1230+
advisories.update({impact.advisory for impact in package.fixed_in_impacts.all()})
11651231

11661232
advisory_data = {adv.advisory_id: AdvisoryV2Serializer(adv).data for adv in advisories}
11671233

11681234
if not purl_only:
11691235
package_data = AdvisoryPackageV2Serializer(
1170-
packages, many=True, context={"request": request}
1236+
packages,
1237+
many=True,
1238+
context={"request": request},
11711239
).data
11721240
return Response(
11731241
{

0 commit comments

Comments
 (0)