From 7ccea8d69eacb63fb8134088a6481b86077b9740 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Fri, 15 Nov 2024 09:59:19 +0530 Subject: [PATCH 1/7] Speed up V2 API Signed-off-by: Tushar Goel --- vulnerabilities/api_v2.py | 216 +++++++------------------------------- 1 file changed, 40 insertions(+), 176 deletions(-) diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index b0a3fa125..84035e015 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -135,6 +135,13 @@ def get_queryset(self): if aliases: queryset = queryset.filter(aliases__alias__in=aliases).distinct() + # Prefetch related fields to reduce queries in serializers + queryset = queryset.prefetch_related( + 'aliases', + 'weaknesses', + 'vulnerabilityreference_set', + 'severities', + ) return queryset def get_serializer_class(self): @@ -146,7 +153,6 @@ def list(self, request, *args, **kwargs): queryset = self.get_queryset() vulnerability_ids = request.query_params.getlist("vulnerability_id") - # If exactly one vulnerability_id is provided, return the serialized data if len(vulnerability_ids) == 1: try: vulnerability = queryset.get(vulnerability_id=vulnerability_ids[0]) @@ -155,17 +161,14 @@ def list(self, request, *args, **kwargs): except Vulnerability.DoesNotExist: return Response({"detail": "Not found."}, status=404) - # Otherwise, return a dictionary of vulnerabilities keyed by vulnerability_id page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) - data = serializer.data - vulnerabilities = {item["vulnerability_id"]: item for item in data} + vulnerabilities = {item["vulnerability_id"]: item for item in serializer.data} return self.get_paginated_response({"vulnerabilities": vulnerabilities}) serializer = self.get_serializer(queryset, many=True) - data = serializer.data - vulnerabilities = {item["vulnerability_id"]: item for item in data} + vulnerabilities = {item["vulnerability_id"]: item for item in serializer.data} return Response({"vulnerabilities": vulnerabilities}) @@ -245,7 +248,31 @@ def get_queryset(self): queryset = queryset.filter( fixing_vulnerabilities__vulnerability_id=fixing_vulnerability ) - return queryset.with_is_vulnerable() + + queryset = queryset.with_is_vulnerable() + + # Prefetch related vulnerabilities and their related data + queryset = queryset.prefetch_related( + Prefetch( + 'affected_by_vulnerabilities', + queryset=Vulnerability.objects.all().prefetch_related( + 'aliases', + 'weaknesses', + 'vulnerabilityreference_set', + 'severities', + ), + ), + Prefetch( + 'fixing_vulnerabilities', + queryset=Vulnerability.objects.all().prefetch_related( + 'aliases', + 'weaknesses', + 'vulnerabilityreference_set', + 'severities', + ), + ), + ) + return queryset def list(self, request, *args, **kwargs): queryset = self.get_queryset() @@ -253,204 +280,41 @@ def list(self, request, *args, **kwargs): # Apply pagination page = self.paginate_queryset(queryset) if page is not None: - # Collect only vulnerabilities for packages in the current page + # Collect vulnerabilities from prefetched data vulnerabilities = set() for package in page: vulnerabilities.update(package.affected_by_vulnerabilities.all()) vulnerabilities.update(package.fixing_vulnerabilities.all()) - # Serialize the vulnerabilities with vulnerability_id as keys + # Serialize vulnerabilities vulnerability_data = { vuln.vulnerability_id: VulnerabilityV2Serializer(vuln).data for vuln in vulnerabilities } - # Serialize the current page of packages + # Serialize packages serializer = self.get_serializer(page, many=True) data = serializer.data - # Use 'self.get_paginated_response' to include pagination data return self.get_paginated_response( {"vulnerabilities": vulnerability_data, "packages": data} ) - # If pagination is not applied, collect vulnerabilities for all packages + # If no pagination vulnerabilities = set() for package in queryset: vulnerabilities.update(package.affected_by_vulnerabilities.all()) vulnerabilities.update(package.fixing_vulnerabilities.all()) vulnerability_data = { - vuln.vulnerability_id: VulnerabilityV2Serializer(vuln).data for vuln in vulnerabilities + vuln.vulnerability_id: VulnerabilityV2Serializer(vuln).data + for vuln in vulnerabilities } - # Serialize all packages when pagination is not applied serializer = self.get_serializer(queryset, many=True) data = serializer.data return Response({"vulnerabilities": vulnerability_data, "packages": data}) - @extend_schema( - request=PackageurlListSerializer, - responses={200: PackageV2Serializer(many=True)}, - ) - @action( - detail=False, - methods=["post"], - serializer_class=PackageurlListSerializer, - filter_backends=[], - pagination_class=None, - ) - def bulk_lookup(self, request): - """ - Return the response for exact PackageURLs requested for. - """ - serializer = self.serializer_class(data=request.data) - if not serializer.is_valid(): - return Response( - status=status.HTTP_400_BAD_REQUEST, - data={ - "error": serializer.errors, - "message": "A non-empty 'purls' list of PURLs is required.", - }, - ) - validated_data = serializer.validated_data - purls = validated_data.get("purls") - - # Fetch packages matching the provided purls - packages = Package.objects.for_purls(purls).with_is_vulnerable() - - # Collect vulnerabilities associated with these packages - vulnerabilities = set() - for package in packages: - vulnerabilities.update(package.affected_by_vulnerabilities.all()) - vulnerabilities.update(package.fixing_vulnerabilities.all()) - - # Serialize vulnerabilities with vulnerability_id as keys - vulnerability_data = { - vuln.vulnerability_id: VulnerabilityV2Serializer(vuln).data for vuln in vulnerabilities - } - - # Serialize packages - package_data = PackageV2Serializer( - packages, - many=True, - context={"request": request}, - ).data - - return Response( - { - "vulnerabilities": vulnerability_data, - "packages": package_data, - } - ) - - @extend_schema( - request=PackageBulkSearchRequestSerializer, - responses={200: PackageV2Serializer(many=True)}, - ) - @action( - detail=False, - methods=["post"], - serializer_class=PackageBulkSearchRequestSerializer, - filter_backends=[], - pagination_class=None, - ) - def bulk_search(self, request): - """ - Lookup for vulnerable packages using many Package URLs at once. - """ - serializer = self.serializer_class(data=request.data) - if not serializer.is_valid(): - return Response( - status=status.HTTP_400_BAD_REQUEST, - data={ - "error": serializer.errors, - "message": "A non-empty 'purls' list of PURLs is required.", - }, - ) - validated_data = serializer.validated_data - purls = validated_data.get("purls") - purl_only = validated_data.get("purl_only", False) - plain_purl = validated_data.get("plain_purl", False) - - if plain_purl: - purl_objects = [PackageURL.from_string(purl) for purl in purls] - plain_purl_objects = [ - PackageURL( - type=purl.type, - namespace=purl.namespace, - name=purl.name, - version=purl.version, - ) - for purl in purl_objects - ] - plain_purls = [str(purl) for purl in plain_purl_objects] - - query = ( - Package.objects.filter(plain_package_url__in=plain_purls) - .order_by("plain_package_url") - .distinct("plain_package_url") - .with_is_vulnerable() - ) - - packages = query - - # Collect vulnerabilities associated with these packages - vulnerabilities = set() - for package in packages: - vulnerabilities.update(package.affected_by_vulnerabilities.all()) - vulnerabilities.update(package.fixing_vulnerabilities.all()) - - vulnerability_data = { - vuln.vulnerability_id: VulnerabilityV2Serializer(vuln).data - for vuln in vulnerabilities - } - - if not purl_only: - package_data = PackageV2Serializer( - packages, many=True, context={"request": request} - ).data - return Response( - { - "vulnerabilities": vulnerability_data, - "packages": package_data, - } - ) - - # Using order by and distinct because there will be - # many fully qualified purl for a single plain purl - vulnerable_purls = query.vulnerable().only("plain_package_url") - vulnerable_purls = [str(package.plain_package_url) for package in vulnerable_purls] - return Response(data=vulnerable_purls) - - query = Package.objects.filter(package_url__in=purls).distinct().with_is_vulnerable() - packages = query - - # Collect vulnerabilities associated with these packages - vulnerabilities = set() - for package in packages: - vulnerabilities.update(package.affected_by_vulnerabilities.all()) - vulnerabilities.update(package.fixing_vulnerabilities.all()) - - vulnerability_data = { - vuln.vulnerability_id: VulnerabilityV2Serializer(vuln).data for vuln in vulnerabilities - } - - if not purl_only: - package_data = PackageV2Serializer( - packages, many=True, context={"request": request} - ).data - return Response( - { - "vulnerabilities": vulnerability_data, - "packages": package_data, - } - ) - - vulnerable_purls = query.vulnerable().only("package_url") - vulnerable_purls = [str(package.package_url) for package in vulnerable_purls] - return Response(data=vulnerable_purls) - @action(detail=False, methods=["get"]) def all(self, request): """ From 1ad546906287d329f35ce104733d3fe34d727c88 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Fri, 15 Nov 2024 10:42:45 +0530 Subject: [PATCH 2/7] Fix V2 Signed-off-by: Tushar Goel --- vulnerabilities/api_v2.py | 32 ++++++++++++++-------------- vulnerabilities/tests/test_api_v2.py | 4 ++++ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index 84035e015..96da381c1 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -8,6 +8,7 @@ # +from django.db.models import Prefetch from django_filters import rest_framework as filters from drf_spectacular.utils import OpenApiParameter from drf_spectacular.utils import extend_schema @@ -137,10 +138,10 @@ def get_queryset(self): # Prefetch related fields to reduce queries in serializers queryset = queryset.prefetch_related( - 'aliases', - 'weaknesses', - 'vulnerabilityreference_set', - 'severities', + "aliases", + "weaknesses", + "vulnerabilityreference_set", + "severities", ) return queryset @@ -254,21 +255,21 @@ def get_queryset(self): # Prefetch related vulnerabilities and their related data queryset = queryset.prefetch_related( Prefetch( - 'affected_by_vulnerabilities', + "affected_by_vulnerabilities", queryset=Vulnerability.objects.all().prefetch_related( - 'aliases', - 'weaknesses', - 'vulnerabilityreference_set', - 'severities', + "aliases", + "weaknesses", + "vulnerabilityreference_set", + "severities", ), ), Prefetch( - 'fixing_vulnerabilities', + "fixing_vulnerabilities", queryset=Vulnerability.objects.all().prefetch_related( - 'aliases', - 'weaknesses', - 'vulnerabilityreference_set', - 'severities', + "aliases", + "weaknesses", + "vulnerabilityreference_set", + "severities", ), ), ) @@ -307,8 +308,7 @@ def list(self, request, *args, **kwargs): vulnerabilities.update(package.fixing_vulnerabilities.all()) vulnerability_data = { - vuln.vulnerability_id: VulnerabilityV2Serializer(vuln).data - for vuln in vulnerabilities + vuln.vulnerability_id: VulnerabilityV2Serializer(vuln).data for vuln in vulnerabilities } serializer = self.get_serializer(queryset, many=True) diff --git a/vulnerabilities/tests/test_api_v2.py b/vulnerabilities/tests/test_api_v2.py index fa3b7773c..18cec125e 100644 --- a/vulnerabilities/tests/test_api_v2.py +++ b/vulnerabilities/tests/test_api_v2.py @@ -562,3 +562,7 @@ def test_lookup_with_invalid_purl_format(self): self.assertEqual(response.status_code, status.HTTP_200_OK) # No packages or vulnerabilities should be returned self.assertEqual(len(response.data), 0) + + def test_api_packages_single_with_purl_in_query_num_queries(self): + with self.assertNumQueries(13): + self.client.get(f"/api/v2/packages/?purl={self.package2.purl}", format="json") From 21ce0fcb5d52e597986bf37867bae087d880e645 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Fri, 15 Nov 2024 16:57:18 +0530 Subject: [PATCH 3/7] Remove from qs Signed-off-by: Tushar Goel --- vulnerabilities/api_v2.py | 165 +++++++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 3 deletions(-) diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index 96da381c1..ec27856e1 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -250,9 +250,6 @@ def get_queryset(self): fixing_vulnerabilities__vulnerability_id=fixing_vulnerability ) - queryset = queryset.with_is_vulnerable() - - # Prefetch related vulnerabilities and their related data queryset = queryset.prefetch_related( Prefetch( "affected_by_vulnerabilities", @@ -358,3 +355,165 @@ def lookup(self, request): qs = self.get_queryset().for_purls([purl]).with_is_vulnerable() return Response(PackageV2Serializer(qs, many=True, context={"request": request}).data) + + @extend_schema( + request=PackageurlListSerializer, + responses={200: PackageV2Serializer(many=True)}, + ) + @action( + detail=False, + methods=["post"], + serializer_class=PackageurlListSerializer, + filter_backends=[], + pagination_class=None, + ) + def bulk_lookup(self, request): + """ + Return the response for exact PackageURLs requested for. + """ + serializer = self.serializer_class(data=request.data) + if not serializer.is_valid(): + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={ + "error": serializer.errors, + "message": "A non-empty 'purls' list of PURLs is required.", + }, + ) + validated_data = serializer.validated_data + purls = validated_data.get("purls") + + # Fetch packages matching the provided purls + packages = Package.objects.for_purls(purls).with_is_vulnerable() + + # Collect vulnerabilities associated with these packages + vulnerabilities = set() + for package in packages: + vulnerabilities.update(package.affected_by_vulnerabilities.all()) + vulnerabilities.update(package.fixing_vulnerabilities.all()) + + # Serialize vulnerabilities with vulnerability_id as keys + vulnerability_data = { + vuln.vulnerability_id: VulnerabilityV2Serializer(vuln).data for vuln in vulnerabilities + } + + # Serialize packages + package_data = PackageV2Serializer( + packages, + many=True, + context={"request": request}, + ).data + + return Response( + { + "vulnerabilities": vulnerability_data, + "packages": package_data, + } + ) + + @extend_schema( + request=PackageBulkSearchRequestSerializer, + responses={200: PackageV2Serializer(many=True)}, + ) + @action( + detail=False, + methods=["post"], + serializer_class=PackageBulkSearchRequestSerializer, + filter_backends=[], + pagination_class=None, + ) + def bulk_search(self, request): + """ + Lookup for vulnerable packages using many Package URLs at once. + """ + serializer = self.serializer_class(data=request.data) + if not serializer.is_valid(): + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={ + "error": serializer.errors, + "message": "A non-empty 'purls' list of PURLs is required.", + }, + ) + validated_data = serializer.validated_data + purls = validated_data.get("purls") + purl_only = validated_data.get("purl_only", False) + plain_purl = validated_data.get("plain_purl", False) + + if plain_purl: + purl_objects = [PackageURL.from_string(purl) for purl in purls] + plain_purl_objects = [ + PackageURL( + type=purl.type, + namespace=purl.namespace, + name=purl.name, + version=purl.version, + ) + for purl in purl_objects + ] + plain_purls = [str(purl) for purl in plain_purl_objects] + + query = ( + Package.objects.filter(plain_package_url__in=plain_purls) + .order_by("plain_package_url") + .distinct("plain_package_url") + .with_is_vulnerable() + ) + + packages = query + + # Collect vulnerabilities associated with these packages + vulnerabilities = set() + for package in packages: + vulnerabilities.update(package.affected_by_vulnerabilities.all()) + vulnerabilities.update(package.fixing_vulnerabilities.all()) + + vulnerability_data = { + vuln.vulnerability_id: VulnerabilityV2Serializer(vuln).data + for vuln in vulnerabilities + } + + if not purl_only: + package_data = PackageV2Serializer( + packages, many=True, context={"request": request} + ).data + return Response( + { + "vulnerabilities": vulnerability_data, + "packages": package_data, + } + ) + + # Using order by and distinct because there will be + # many fully qualified purl for a single plain purl + vulnerable_purls = query.vulnerable().only("plain_package_url") + vulnerable_purls = [str(package.plain_package_url) for package in vulnerable_purls] + return Response(data=vulnerable_purls) + + query = Package.objects.filter(package_url__in=purls).distinct().with_is_vulnerable() + packages = query + + # Collect vulnerabilities associated with these packages + vulnerabilities = set() + for package in packages: + vulnerabilities.update(package.affected_by_vulnerabilities.all()) + vulnerabilities.update(package.fixing_vulnerabilities.all()) + + vulnerability_data = { + vuln.vulnerability_id: VulnerabilityV2Serializer(vuln).data for vuln in vulnerabilities + } + + if not purl_only: + package_data = PackageV2Serializer( + packages, many=True, context={"request": request} + ).data + return Response( + { + "vulnerabilities": vulnerability_data, + "packages": package_data, + } + ) + + vulnerable_purls = query.vulnerable().only("package_url") + vulnerable_purls = [str(package.package_url) for package in vulnerable_purls] + return Response(data=vulnerable_purls) From a2fceea0eaba03f08255435386abfe3869569413 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Fri, 15 Nov 2024 22:36:41 +0530 Subject: [PATCH 4/7] Add cache on latest non vulnerable and next vulnerable Signed-off-by: Tushar Goel --- vulnerabilities/models.py | 87 +++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index c62949992..64c65fa31 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -20,6 +20,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import UserManager from django.core import exceptions +from django.core.cache import cache from django.core.exceptions import ValidationError from django.core.paginator import Paginator from django.core.validators import MaxValueValidator @@ -471,18 +472,19 @@ def get_fixed_by_package_versions(self, purl: PackageURL, fix=True): Return a queryset of all the package versions of this `package` that fix any vulnerability. If `fix` is False, return all package versions whether or not they fix a vulnerability. """ - filter_dict = { - "name": purl.name, - "namespace": purl.namespace, + # TODO: Move this to Package object method + filters = { "type": purl.type, + "namespace": purl.namespace, + "name": purl.name, "qualifiers": purl.qualifiers, "subpath": purl.subpath, } if fix: - filter_dict["fixing_vulnerabilities__isnull"] = False + filters["fixing_vulnerabilities__isnull"] = False - return Package.objects.filter(**filter_dict).distinct() + return Package.objects.filter(**filters).distinct() def get_or_create_from_purl(self, purl: Union[PackageURL, str]): """ @@ -648,7 +650,8 @@ class Package(PackageURLMixin): fixing_vulnerabilities = models.ManyToManyField( to="Vulnerability", through="FixingPackageRelatedVulnerability", - related_name="fixed_by_packages", # Unique related_name + # Unique related_name + related_name="fixed_by_packages", ) package_url = models.CharField( @@ -779,6 +782,10 @@ def version_class(self): def current_version(self): return self.version_class(self.version) + @property + def vulnerabilities(self): + return self.affected_by_vulnerabilities.all() | self.fixing_vulnerabilities.all() + @property def next_non_vulnerable_version(self): """ @@ -787,10 +794,6 @@ def next_non_vulnerable_version(self): next_non_vulnerable, _ = self.get_non_vulnerable_versions() return next_non_vulnerable.version if next_non_vulnerable else None - @property - def vulnerabilities(self): - return self.affected_by_vulnerabilities.all() | self.fixing_vulnerabilities.all() - @property def latest_non_vulnerable_version(self): """ @@ -823,6 +826,67 @@ def get_non_vulnerable_versions(self): return None, None + @property + def non_vulnerable_versions(self): + """ + Cache the result of get_non_vulnerable_versions_v2 to avoid redundant computations. + """ + if not hasattr(self, "_non_vulnerable_versions_cache"): + self._non_vulnerable_versions_cache = self.get_non_vulnerable_versions_v2() + return self._non_vulnerable_versions_cache + + @property + def next_non_vulnerable_package(self): + """ + Return the purl of the next non-vulnerable package version. + """ + next_non_vulnerable, _ = self.get_non_vulnerable_versions_v2() + return next_non_vulnerable.purl if next_non_vulnerable else None + + @property + def latest_non_vulnerable_package(self): + """ + Return the purl of the latest non-vulnerable package version. + """ + _, latest_non_vulnerable = self.get_non_vulnerable_versions_v2() + return latest_non_vulnerable.purl if latest_non_vulnerable else None + + def get_non_vulnerable_versions_v2(self): + """ + Return a tuple of three Package instance: + - first fixing version + - next non-vulnerable version + - latest non-vulnerable version + Return a tuple of (None, None) if there is no non-vulnerable version. + """ + cache_key = f"non_vulnerable_versions_{self.id}" + result = cache.get(cache_key) + if result is not None: + return result + + non_vulnerable_versions = Package.objects.get_fixed_by_package_versions( + self, fix=False + ).only_non_vulnerable() + sorted_versions = self.sort_by_version(non_vulnerable_versions) + + later_non_vulnerable_versions = [ + non_vuln_ver + for non_vuln_ver in sorted_versions + if self.version_class(non_vuln_ver.version) > self.current_version + ] + + if later_non_vulnerable_versions: + sorted_versions = self.sort_by_version(later_non_vulnerable_versions) + next_non_vulnerable = sorted_versions[0] + latest_non_vulnerable = sorted_versions[-1] + cache.set( + cache_key, (next_non_vulnerable, latest_non_vulnerable), timeout=3600 + ) + return next_non_vulnerable, latest_non_vulnerable + + cache.set(cache_key, (None, None), timeout=3600) + return None, None + @property def fixed_package_details(self): """ @@ -928,15 +992,14 @@ class PackageRelatedVulnerabilityBase(models.Model): package = models.ForeignKey( Package, on_delete=models.CASCADE, - # related_name="%(class)s_set", # Unique related_name per subclass ) vulnerability = models.ForeignKey( Vulnerability, on_delete=models.CASCADE, - # related_name="%(class)s_set", # Unique related_name per subclass ) + # TODO: Fix the help text created_by = models.CharField( max_length=100, blank=True, From e56bcac20befd8bc1b19f91c8569b7ddfa5df8b9 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Fri, 15 Nov 2024 22:44:44 +0530 Subject: [PATCH 5/7] Restructure code Signed-off-by: Tushar Goel --- vulnerabilities/api_v2.py | 87 ++++++++++++++++++++------------------- vulnerabilities/models.py | 9 ---- 2 files changed, 44 insertions(+), 52 deletions(-) diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index ec27856e1..d6250c9f8 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -312,49 +312,6 @@ def list(self, request, *args, **kwargs): data = serializer.data return Response({"vulnerabilities": vulnerability_data, "packages": data}) - @action(detail=False, methods=["get"]) - def all(self, request): - """ - Return a list of Package URLs of vulnerable packages. - """ - vulnerable_purls = ( - Package.objects.vulnerable() - .only("package_url") - .order_by("package_url") - .distinct() - .values_list("package_url", flat=True) - ) - return Response(vulnerable_purls) - - @extend_schema( - request=LookupRequestSerializer, - responses={200: PackageV2Serializer(many=True)}, - ) - @action( - detail=False, - methods=["post"], - serializer_class=LookupRequestSerializer, - filter_backends=[], - pagination_class=None, - ) - def lookup(self, request): - """ - Return the response for exact PackageURL requested for. - """ - serializer = self.serializer_class(data=request.data) - if not serializer.is_valid(): - return Response( - status=status.HTTP_400_BAD_REQUEST, - data={ - "error": serializer.errors, - "message": "A 'purl' is required.", - }, - ) - validated_data = serializer.validated_data - purl = validated_data.get("purl") - - qs = self.get_queryset().for_purls([purl]).with_is_vulnerable() - return Response(PackageV2Serializer(qs, many=True, context={"request": request}).data) @extend_schema( request=PackageurlListSerializer, @@ -517,3 +474,47 @@ def bulk_search(self, request): vulnerable_purls = query.vulnerable().only("package_url") vulnerable_purls = [str(package.package_url) for package in vulnerable_purls] return Response(data=vulnerable_purls) + + @action(detail=False, methods=["get"]) + def all(self, request): + """ + Return a list of Package URLs of vulnerable packages. + """ + vulnerable_purls = ( + Package.objects.vulnerable() + .only("package_url") + .order_by("package_url") + .distinct() + .values_list("package_url", flat=True) + ) + return Response(vulnerable_purls) + + @extend_schema( + request=LookupRequestSerializer, + responses={200: PackageV2Serializer(many=True)}, + ) + @action( + detail=False, + methods=["post"], + serializer_class=LookupRequestSerializer, + filter_backends=[], + pagination_class=None, + ) + def lookup(self, request): + """ + Return the response for exact PackageURL requested for. + """ + serializer = self.serializer_class(data=request.data) + if not serializer.is_valid(): + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={ + "error": serializer.errors, + "message": "A 'purl' is required.", + }, + ) + validated_data = serializer.validated_data + purl = validated_data.get("purl") + + qs = self.get_queryset().for_purls([purl]).with_is_vulnerable() + return Response(PackageV2Serializer(qs, many=True, context={"request": request}).data) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 64c65fa31..01fe63be3 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -826,15 +826,6 @@ def get_non_vulnerable_versions(self): return None, None - @property - def non_vulnerable_versions(self): - """ - Cache the result of get_non_vulnerable_versions_v2 to avoid redundant computations. - """ - if not hasattr(self, "_non_vulnerable_versions_cache"): - self._non_vulnerable_versions_cache = self.get_non_vulnerable_versions_v2() - return self._non_vulnerable_versions_cache - @property def next_non_vulnerable_package(self): """ From 39f70f94eb222a39c89d19a3e169c47702e147c4 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Fri, 15 Nov 2024 23:55:22 +0530 Subject: [PATCH 6/7] Add prefetch without all Signed-off-by: Tushar Goel --- vulnerabilities/api_v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index d6250c9f8..0c828a754 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -253,7 +253,7 @@ def get_queryset(self): queryset = queryset.prefetch_related( Prefetch( "affected_by_vulnerabilities", - queryset=Vulnerability.objects.all().prefetch_related( + queryset=Vulnerability.objects.prefetch_related( "aliases", "weaknesses", "vulnerabilityreference_set", @@ -262,7 +262,7 @@ def get_queryset(self): ), Prefetch( "fixing_vulnerabilities", - queryset=Vulnerability.objects.all().prefetch_related( + queryset=Vulnerability.objects.prefetch_related( "aliases", "weaknesses", "vulnerabilityreference_set", From 17acfbdbc4f7fd4630297ec6225a50ed73d02831 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Sat, 16 Nov 2024 00:13:01 +0530 Subject: [PATCH 7/7] Use package instead of version Signed-off-by: Tushar Goel --- vulnerabilities/api_v2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index 0c828a754..2fb242feb 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -178,8 +178,8 @@ class PackageV2Serializer(serializers.ModelSerializer): risk_score = serializers.FloatField(read_only=True) affected_by_vulnerabilities = serializers.SerializerMethodField() fixing_vulnerabilities = serializers.SerializerMethodField() - next_non_vulnerable_version = serializers.CharField(read_only=True) - latest_non_vulnerable_version = serializers.CharField(read_only=True) + next_non_vulnerable_package = serializers.CharField(read_only=True) + latest_non_vulnerable_package = serializers.CharField(read_only=True) class Meta: model = Package @@ -187,8 +187,8 @@ class Meta: "purl", "affected_by_vulnerabilities", "fixing_vulnerabilities", - "next_non_vulnerable_version", - "latest_non_vulnerable_version", + "next_non_vulnerable_package", + "latest_non_vulnerable_package", "risk_score", ]