From 05f1ba183648262563a827f3127c31a43fa0bef4 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Mon, 28 Oct 2024 15:26:43 +0530 Subject: [PATCH 01/13] Add V2 API endpoints Reference: https://github.com/aboutcode-org/vulnerablecode/issues/1572 Signed-off-by: Tushar Goel --- vulnerabilities/api.py | 109 +++++++++++++++++++++++++++++++++++++++++ vulnerablecode/urls.py | 7 +++ 2 files changed, 116 insertions(+) diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py index b8bb703a6..8acd17e62 100644 --- a/vulnerabilities/api.py +++ b/vulnerabilities/api.py @@ -691,3 +691,112 @@ class AliasViewSet(VulnerabilityViewSet): """ filterset_class = AliasFilterSet + +class WeaknessV2Serializer(serializers.ModelSerializer): + cwe_id = serializers.CharField() + name = serializers.CharField() + description = serializers.CharField() + + class Meta: + model = Weakness + fields = ["cwe_id", "name", "description"] + + +class VulnerabilityReferenceV2Serializer(serializers.ModelSerializer): + url = serializers.CharField() + reference_type = serializers.CharField() + reference_id = serializers.CharField() + + class Meta: + model = VulnerabilityReference + fields = ["url", "reference_type", "reference_id"] + +class VulnerabilityV2Serializer(serializers.ModelSerializer): + aliases = serializers.SerializerMethodField() + severities = serializers.SerializerMethodField() + weaknesses = WeaknessV2Serializer(many=True) + references = VulnerabilityReferenceV2Serializer(many=True, source='vulnerabilityreference_set') + + class Meta: + model = Vulnerability + fields = [ + "vulnerability_id", + "aliases", + "summary", + "severities", + "weaknesses", + "references", + ] + + def get_aliases(self, obj): + return [alias.alias for alias in obj.aliases.all()] + + def get_severities(self, obj): + #TODO: Need data model changes + return [] + + +class VulnerabilityV2ViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Vulnerability.objects.all() + serializer_class = VulnerabilityV2Serializer + + def list(self, request, *args, **kwargs): + queryset = self.get_queryset() + # Apply pagination + 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} + # Use 'self.get_paginated_response' to include pagination data + return self.get_paginated_response({'vulnerabilities': vulnerabilities}) + + # If pagination is not applied + serializer = self.get_serializer(queryset, many=True) + data = serializer.data + vulnerabilities = {item['vulnerability_id']: item for item in data} + return Response({'vulnerabilities': vulnerabilities}) + + +class PackageV2Serializer(serializers.ModelSerializer): + purl = serializers.CharField(source='package_url') + 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) + + class Meta: + model = Package + fields = [ + 'purl', + 'affected_by_vulnerabilities', + 'fixing_vulnerabilities', + 'next_non_vulnerable_version', + 'latest_non_vulnerable_version', + ] + + def get_affected_by_vulnerabilities(self, obj): + return [vuln.vulnerability_id for vuln in obj.affected_by_vulnerabilities.all()] + + def get_fixing_vulnerabilities(self, obj): + return [vuln.vulnerability_id for vuln in obj.fixing_vulnerabilities.all()] + + +class PackageV2ViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Package.objects.all() + serializer_class = PackageV2Serializer + + def list(self, request, *args, **kwargs): + queryset = self.get_queryset().with_is_vulnerable() + # Apply pagination + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + data = serializer.data + # Use 'self.get_paginated_response' to include pagination data + return self.get_paginated_response({'purls': data}) + + # If pagination is not applied + serializer = self.get_serializer(queryset, many=True) + data = serializer.data + return Response({'purls': data}) diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index 51d303138..5aa3e80fc 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -20,6 +20,8 @@ from vulnerabilities.api import CPEViewSet from vulnerabilities.api import PackageViewSet from vulnerabilities.api import VulnerabilityViewSet +from vulnerabilities.api import PackageV2ViewSet +from vulnerabilities.api import VulnerabilityV2ViewSet from vulnerabilities.views import ApiUserCreateView from vulnerabilities.views import HomePage from vulnerabilities.views import PackageDetails @@ -43,6 +45,10 @@ def __init__(self, *args, **kwargs): api_router.register("cpes", CPEViewSet, basename="cpe") api_router.register("aliases", AliasViewSet, basename="alias") +api_v2_router = OptionalSlashRouter() +api_v2_router.register("vulnerabilities", VulnerabilityV2ViewSet, basename="vulnerability-v2") +api_v2_router.register("packages", PackageV2ViewSet, basename="package-v2") + urlpatterns = [ path( "robots.txt", @@ -98,6 +104,7 @@ def __init__(self, *args, **kwargs): TemplateView.as_view(template_name="tos.html"), name="api_tos", ), + path('api/v2/', include(api_v2_router.urls)), path( "admin/", admin.site.urls, From e389ce5fa44154eb45619677a5f56ad2ae1fd8a3 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Thu, 7 Nov 2024 17:39:45 +0530 Subject: [PATCH 02/13] Add Filter set Signed-off-by: Tushar Goel --- vulnerabilities/api.py | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py index 8acd17e62..7bee5223d 100644 --- a/vulnerabilities/api.py +++ b/vulnerabilities/api.py @@ -701,6 +701,16 @@ class Meta: model = Weakness fields = ["cwe_id", "name", "description"] +class VulnerabilityFilter(filters.FilterSet): + vulnerability_id = filters.CharFilter(field_name='vulnerability_id', lookup_expr='exact') + vulnerability_id__in = filters.BaseInFilter(field_name='vulnerability_id', lookup_expr='in') + alias = filters.CharFilter(field_name='aliases__alias', lookup_expr='exact') + alias__in = filters.BaseInFilter(field_name='aliases__alias', lookup_expr='in') + + class Meta: + model = Vulnerability + fields = ['vulnerability_id', 'vulnerability_id__in', 'alias', 'alias__in'] + class VulnerabilityReferenceV2Serializer(serializers.ModelSerializer): url = serializers.CharField() @@ -711,11 +721,11 @@ class Meta: model = VulnerabilityReference fields = ["url", "reference_type", "reference_id"] -class VulnerabilityV2Serializer(serializers.ModelSerializer): +class VulnerabilityV2Serializer(BaseResourceSerializer): aliases = serializers.SerializerMethodField() - severities = serializers.SerializerMethodField() weaknesses = WeaknessV2Serializer(many=True) references = VulnerabilityReferenceV2Serializer(many=True, source='vulnerabilityreference_set') + severities = VulnerabilitySeveritySerializer(many=True) class Meta: model = Vulnerability @@ -732,8 +742,7 @@ def get_aliases(self, obj): return [alias.alias for alias in obj.aliases.all()] def get_severities(self, obj): - #TODO: Need data model changes - return [] + return obj.severities class VulnerabilityV2ViewSet(viewsets.ReadOnlyModelViewSet): @@ -758,6 +767,28 @@ def list(self, request, *args, **kwargs): return Response({'vulnerabilities': vulnerabilities}) +class PackageFilter(filters.FilterSet): + purl = filters.CharFilter(field_name='package_url', lookup_expr='exact') + purl__in = filters.BaseInFilter(field_name='package_url', lookup_expr='in') + affected_by_vulnerability = filters.CharFilter( + field_name='affected_by_vulnerabilities__vulnerability_id', + lookup_expr='exact' + ) + fixing_vulnerability = filters.CharFilter( + field_name='fixing_vulnerabilities__vulnerability_id', + lookup_expr='exact' + ) + + class Meta: + model = Package + fields = [ + 'purl', + 'purl__in', + 'affected_by_vulnerability', + 'fixing_vulnerability', + ] + + class PackageV2Serializer(serializers.ModelSerializer): purl = serializers.CharField(source='package_url') affected_by_vulnerabilities = serializers.SerializerMethodField() @@ -785,6 +816,7 @@ def get_fixing_vulnerabilities(self, obj): class PackageV2ViewSet(viewsets.ReadOnlyModelViewSet): queryset = Package.objects.all() serializer_class = PackageV2Serializer + filterset_class = PackageFilter def list(self, request, *args, **kwargs): queryset = self.get_queryset().with_is_vulnerable() From 2d0a10b22aa8dd19714de691f774294fe086826e Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Thu, 7 Nov 2024 23:16:20 +0530 Subject: [PATCH 03/13] Add tests for v2 API Signed-off-by: Tushar Goel --- vulnerabilities/api.py | 131 +++++++----- vulnerabilities/tests/test_v2_api.py | 295 +++++++++++++++++++++++++++ vulnerablecode/urls.py | 6 +- 3 files changed, 379 insertions(+), 53 deletions(-) create mode 100644 vulnerabilities/tests/test_v2_api.py diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py index 7bee5223d..6328d4c92 100644 --- a/vulnerabilities/api.py +++ b/vulnerabilities/api.py @@ -692,6 +692,7 @@ class AliasViewSet(VulnerabilityViewSet): filterset_class = AliasFilterSet + class WeaknessV2Serializer(serializers.ModelSerializer): cwe_id = serializers.CharField() name = serializers.CharField() @@ -701,16 +702,6 @@ class Meta: model = Weakness fields = ["cwe_id", "name", "description"] -class VulnerabilityFilter(filters.FilterSet): - vulnerability_id = filters.CharFilter(field_name='vulnerability_id', lookup_expr='exact') - vulnerability_id__in = filters.BaseInFilter(field_name='vulnerability_id', lookup_expr='in') - alias = filters.CharFilter(field_name='aliases__alias', lookup_expr='exact') - alias__in = filters.BaseInFilter(field_name='aliases__alias', lookup_expr='in') - - class Meta: - model = Vulnerability - fields = ['vulnerability_id', 'vulnerability_id__in', 'alias', 'alias__in'] - class VulnerabilityReferenceV2Serializer(serializers.ModelSerializer): url = serializers.CharField() @@ -721,10 +712,11 @@ class Meta: model = VulnerabilityReference fields = ["url", "reference_type", "reference_id"] + class VulnerabilityV2Serializer(BaseResourceSerializer): aliases = serializers.SerializerMethodField() weaknesses = WeaknessV2Serializer(many=True) - references = VulnerabilityReferenceV2Serializer(many=True, source='vulnerabilityreference_set') + references = VulnerabilityReferenceV2Serializer(many=True, source="vulnerabilityreference_set") severities = VulnerabilitySeveritySerializer(many=True) class Meta: @@ -745,52 +737,74 @@ def get_severities(self, obj): return obj.severities +class VulnerabilityListSerializer(serializers.ModelSerializer): + url = serializers.SerializerMethodField() + + class Meta: + model = Vulnerability + fields = ["vulnerability_id", "url"] + + def get_url(self, obj): + request = self.context.get("request") + return reverse( + "vulnerability-v2-detail", + kwargs={"vulnerability_id": obj.vulnerability_id}, + request=request, + ) + + class VulnerabilityV2ViewSet(viewsets.ReadOnlyModelViewSet): queryset = Vulnerability.objects.all() serializer_class = VulnerabilityV2Serializer + lookup_field = "vulnerability_id" + + def get_queryset(self): + queryset = super().get_queryset() + vulnerability_ids = self.request.query_params.getlist("vulnerability_id") + aliases = self.request.query_params.getlist("alias") + + if vulnerability_ids: + queryset = queryset.filter(vulnerability_id__in=vulnerability_ids) + + if aliases: + queryset = queryset.filter(aliases__alias__in=aliases).distinct() + + return queryset + + def get_serializer_class(self): + if self.action == "list": + return VulnerabilityListSerializer + return super().get_serializer_class() def list(self, request, *args, **kwargs): queryset = self.get_queryset() - # Apply pagination + 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]) + serializer = self.get_serializer(vulnerability) + return Response(serializer.data) + 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} - # Use 'self.get_paginated_response' to include pagination data - return self.get_paginated_response({'vulnerabilities': vulnerabilities}) + vulnerabilities = {item["vulnerability_id"]: item for item in data} + return self.get_paginated_response({"vulnerabilities": vulnerabilities}) - # If pagination is not applied serializer = self.get_serializer(queryset, many=True) data = serializer.data - vulnerabilities = {item['vulnerability_id']: item for item in data} - return Response({'vulnerabilities': vulnerabilities}) - - -class PackageFilter(filters.FilterSet): - purl = filters.CharFilter(field_name='package_url', lookup_expr='exact') - purl__in = filters.BaseInFilter(field_name='package_url', lookup_expr='in') - affected_by_vulnerability = filters.CharFilter( - field_name='affected_by_vulnerabilities__vulnerability_id', - lookup_expr='exact' - ) - fixing_vulnerability = filters.CharFilter( - field_name='fixing_vulnerabilities__vulnerability_id', - lookup_expr='exact' - ) - - class Meta: - model = Package - fields = [ - 'purl', - 'purl__in', - 'affected_by_vulnerability', - 'fixing_vulnerability', - ] + vulnerabilities = {item["vulnerability_id"]: item for item in data} + return Response({"vulnerabilities": vulnerabilities}) class PackageV2Serializer(serializers.ModelSerializer): - purl = serializers.CharField(source='package_url') + purl = serializers.CharField(source="package_url") affected_by_vulnerabilities = serializers.SerializerMethodField() fixing_vulnerabilities = serializers.SerializerMethodField() next_non_vulnerable_version = serializers.CharField(read_only=True) @@ -799,11 +813,11 @@ class PackageV2Serializer(serializers.ModelSerializer): class Meta: model = Package fields = [ - 'purl', - 'affected_by_vulnerabilities', - 'fixing_vulnerabilities', - 'next_non_vulnerable_version', - 'latest_non_vulnerable_version', + "purl", + "affected_by_vulnerabilities", + "fixing_vulnerabilities", + "next_non_vulnerable_version", + "latest_non_vulnerable_version", ] def get_affected_by_vulnerabilities(self, obj): @@ -816,19 +830,36 @@ def get_fixing_vulnerabilities(self, obj): class PackageV2ViewSet(viewsets.ReadOnlyModelViewSet): queryset = Package.objects.all() serializer_class = PackageV2Serializer - filterset_class = PackageFilter + + def get_queryset(self): + queryset = super().get_queryset() + package_purls = self.request.query_params.getlist("purl") + affected_by_vulnerability = self.request.query_params.get("affected_by_vulnerability") + fixing_vulnerability = self.request.query_params.get("fixing_vulnerability") + + if package_purls: + queryset = queryset.filter(package_url__in=package_purls) + if affected_by_vulnerability: + queryset = queryset.filter( + affected_by_vulnerabilities__vulnerability_id=affected_by_vulnerability + ) + if fixing_vulnerability: + queryset = queryset.filter( + fixing_vulnerabilities__vulnerability_id=fixing_vulnerability + ) + return queryset.with_is_vulnerable() def list(self, request, *args, **kwargs): - queryset = self.get_queryset().with_is_vulnerable() + queryset = self.get_queryset() # Apply pagination page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) data = serializer.data # Use 'self.get_paginated_response' to include pagination data - return self.get_paginated_response({'purls': data}) + return self.get_paginated_response({"purls": data}) # If pagination is not applied serializer = self.get_serializer(queryset, many=True) data = serializer.data - return Response({'purls': data}) + return Response({"purls": data}) diff --git a/vulnerabilities/tests/test_v2_api.py b/vulnerabilities/tests/test_v2_api.py new file mode 100644 index 000000000..99199a322 --- /dev/null +++ b/vulnerabilities/tests/test_v2_api.py @@ -0,0 +1,295 @@ +# tests.py +from django.urls import reverse +from packageurl import PackageURL +from rest_framework import status +from rest_framework.test import APIClient +from rest_framework.test import APITestCase + +from vulnerabilities.api import PackageV2Serializer +from vulnerabilities.api import VulnerabilityListSerializer +from vulnerabilities.models import Alias +from vulnerabilities.models import ApiUser +from vulnerabilities.models import Package +from vulnerabilities.models import Vulnerability +from vulnerabilities.models import VulnerabilityReference +from vulnerabilities.models import Weakness + + +class VulnerabilityV2ViewSetTest(APITestCase): + def setUp(self): + # Create vulnerabilities + self.vuln1 = Vulnerability.objects.create( + vulnerability_id="VCID-1234", summary="Test vulnerability 1" + ) + self.vuln2 = Vulnerability.objects.create( + vulnerability_id="VCID-5678", summary="Test vulnerability 2" + ) + + # Create aliases + Alias.objects.create(alias="CVE-2021-1234", vulnerability=self.vuln1) + Alias.objects.create(alias="CVE-2021-5678", vulnerability=self.vuln2) + + # Create weaknesses + self.weakness1 = Weakness.objects.create(cwe_id=79) + self.weakness1.vulnerabilities.add(self.vuln1) + + self.weakness2 = Weakness.objects.create(cwe_id=89) + self.weakness2.vulnerabilities.add(self.vuln2) + + # Create references + self.reference1 = VulnerabilityReference.objects.create( + url="https://example.com/ref1", reference_type="advisory", reference_id="REF-1" + ) + self.reference1.vulnerabilities.add(self.vuln1) + + self.reference2 = VulnerabilityReference.objects.create( + url="https://example.com/ref2", reference_type="exploit", reference_id="REF-2" + ) + self.reference2.vulnerabilities.add(self.vuln2) + self.user = ApiUser.objects.create_api_user(username="e@mail.com") + self.auth = f"Token {self.user.auth_token.key}" + self.client = APIClient(enforce_csrf_checks=True) + self.client.credentials(HTTP_AUTHORIZATION=self.auth) + + def test_list_vulnerabilities(self): + """ + Test listing vulnerabilities without filters. + Should return a list of vulnerabilities with IDs and URLs. + """ + url = reverse("vulnerability-v2-list") + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("vulnerabilities", response.data["results"]) + self.assertEqual(len(response.data["results"]["vulnerabilities"]), 2) + self.assertTrue("url" in response.data["results"]["vulnerabilities"]["VCID-1234"]) + + def test_retrieve_vulnerability_detail(self): + """ + Test retrieving vulnerability details by vulnerability_id. + """ + url = reverse("vulnerability-v2-detail", kwargs={"vulnerability_id": "VCID-1234"}) + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["vulnerability_id"], "VCID-1234") + self.assertEqual(response.data["summary"], "Test vulnerability 1") + self.assertEqual(response.data["aliases"], ["CVE-2021-1234"]) + self.assertEqual(len(response.data["weaknesses"]), 1) + self.assertEqual(len(response.data["references"]), 1) + + def test_filter_vulnerability_by_vulnerability_id(self): + """ + Test filtering vulnerabilities by vulnerability_id. + """ + url = reverse("vulnerability-v2-list") + response = self.client.get(url, {"vulnerability_id": "VCID-1234"}, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["vulnerability_id"], "VCID-1234") + + def test_filter_vulnerability_by_alias(self): + """ + Test filtering vulnerabilities by alias. + """ + url = reverse("vulnerability-v2-list") + response = self.client.get(url, {"alias": "CVE-2021-5678"}, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["results"]["vulnerabilities"]["VCID-5678"]["vulnerability_id"], + "VCID-5678", + ) + + def test_filter_vulnerabilities_multiple_ids(self): + """ + Test filtering vulnerabilities by multiple vulnerability_ids. + """ + url = reverse("vulnerability-v2-list") + response = self.client.get( + url, {"vulnerability_id": ["VCID-1234", "VCID-5678"]}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["results"]["vulnerabilities"]), 2) + + def test_filter_vulnerabilities_multiple_aliases(self): + """ + Test filtering vulnerabilities by multiple aliases. + """ + url = reverse("vulnerability-v2-list") + response = self.client.get( + url, {"alias": ["CVE-2021-1234", "CVE-2021-5678"]}, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["results"]["vulnerabilities"]), 2) + + def test_invalid_vulnerability_id(self): + """ + Test retrieving a vulnerability with an invalid vulnerability_id. + Should return 404 Not Found. + """ + url = reverse("vulnerability-v2-detail", kwargs={"vulnerability_id": "VCID-9999"}) + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_get_url_in_serializer(self): + """ + Test that the serializer correctly includes the URL field. + """ + vulnerability = Vulnerability.objects.get(vulnerability_id="VCID-1234") + serializer = VulnerabilityListSerializer(vulnerability, context={"request": None}) + self.assertIn("url", serializer.data) + self.assertEqual(serializer.data["vulnerability_id"], "VCID-1234") + + def test_list_vulnerabilities_pagination(self): + """ + Test listing vulnerabilities with pagination. + """ + # Create additional vulnerabilities to trigger pagination + for i in range(3, 15): + Vulnerability.objects.create( + vulnerability_id=f"VCID-{i}", summary=f"Test vulnerability {i}" + ) + + url = reverse("vulnerability-v2-list") + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("results", response.data) + self.assertIn("next", response.data) + self.assertIn("previous", response.data) + self.assertEqual(len(response.data["results"]), 1) # Assuming default page size is 10 + + +class PackageV2ViewSetTest(APITestCase): + def setUp(self): + # Create packages + self.package1 = Package.objects.create( + package_url="pkg:pypi/django@3.2", name="django", version="3.2", type="pypi" + ) + self.package2 = Package.objects.create( + package_url="pkg:npm/lodash@4.17.20", name="lodash", version="4.17.20", type="npm" + ) + + # Create vulnerabilities + self.vuln1 = Vulnerability.objects.create( + vulnerability_id="VCID-1234", summary="Test vulnerability 1" + ) + self.vuln2 = Vulnerability.objects.create( + vulnerability_id="VCID-5678", summary="Test vulnerability 2" + ) + + # Associate packages with vulnerabilities + self.package1.affected_by_vulnerabilities.add(self.vuln1) + self.package2.fixing_vulnerabilities.add(self.vuln2) + + def test_list_packages(self): + """ + Test listing packages without filters. + Should return a list of packages with their details. + """ + url = reverse("package-v2-list") + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("purls", response.data["results"]) + self.assertEqual(len(response.data["results"]["purls"]), 2) + + def test_filter_packages_by_purl(self): + """ + Test filtering packages by one or more PURLs. + """ + url = reverse("package-v2-list") + response = self.client.get(url, {"purl": "pkg:pypi/django@3.2"}, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["results"]["purls"]), 1) + self.assertEqual(response.data["results"]["purls"][0]["purl"], "pkg:pypi/django@3.2") + + def test_filter_packages_by_affected_vulnerability(self): + """ + Test filtering packages by affected_by_vulnerability. + """ + url = reverse("package-v2-list") + response = self.client.get(url, {"affected_by_vulnerability": "VCID-1234"}, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["results"]["purls"]), 1) + self.assertEqual(response.data["results"]["purls"][0]["purl"], "pkg:pypi/django@3.2") + + def test_filter_packages_by_fixing_vulnerability(self): + """ + Test filtering packages by fixing_vulnerability. + """ + url = reverse("package-v2-list") + response = self.client.get(url, {"fixing_vulnerability": "VCID-5678"}, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["results"]["purls"]), 1) + self.assertEqual(response.data["results"]["purls"][0]["purl"], "pkg:npm/lodash@4.17.20") + + def test_package_serializer_fields(self): + """ + Test that the PackageV2Serializer returns the correct fields. + """ + package = Package.objects.get(package_url="pkg:pypi/django@3.2") + serializer = PackageV2Serializer(package) + data = serializer.data + self.assertIn("purl", data) + self.assertIn("affected_by_vulnerabilities", data) + self.assertIn("fixing_vulnerabilities", data) + self.assertIn("next_non_vulnerable_version", data) + self.assertIn("latest_non_vulnerable_version", data) + self.assertEqual(data["purl"], "pkg:pypi/django@3.2") + self.assertEqual(data["affected_by_vulnerabilities"], ["VCID-1234"]) + self.assertEqual(data["fixing_vulnerabilities"], []) + + def test_list_packages_pagination(self): + """ + Test listing packages with pagination. + """ + # Create additional packages to trigger pagination + for i in range(3, 15): + Package.objects.create( + package_url=f"pkg:pypi/package{i}@1.0.{i}", + name=f"package{i}", + version=f"1.0.{i}", + type="pypi", + ) + + url = reverse("package-v2-list") + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("results", response.data) + self.assertIn("next", response.data) + self.assertIn("previous", response.data) + self.assertEqual(len(response.data["results"]), 1) # Assuming default page size is 10 + + def test_invalid_vulnerability_filter(self): + """ + Test filtering packages with an invalid vulnerability ID. + Should return an empty list. + """ + url = reverse("package-v2-list") + response = self.client.get(url, {"affected_by_vulnerability": "VCID-9999"}, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["results"]["purls"]), 0) + + def test_invalid_purl_filter(self): + """ + Test filtering packages with an invalid PURL. + Should return an empty list. + """ + url = reverse("package-v2-list") + response = self.client.get(url, {"purl": "pkg:nonexistent/package@1.0.0"}, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data["results"]["purls"]), 0) + + def test_get_affected_by_vulnerabilities(self): + """ + Test the get_affected_by_vulnerabilities method in the serializer. + """ + package = Package.objects.get(package_url="pkg:pypi/django@3.2") + serializer = PackageV2Serializer() + vulnerabilities = serializer.get_affected_by_vulnerabilities(package) + self.assertEqual(vulnerabilities, ["VCID-1234"]) + + def test_get_fixing_vulnerabilities(self): + """ + Test the get_fixing_vulnerabilities method in the serializer. + """ + package = Package.objects.get(package_url="pkg:npm/lodash@4.17.20") + serializer = PackageV2Serializer() + vulnerabilities = serializer.get_fixing_vulnerabilities(package) + self.assertEqual(vulnerabilities, ["VCID-5678"]) diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index 5aa3e80fc..4d5bebf0a 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -18,10 +18,10 @@ from vulnerabilities.api import AliasViewSet from vulnerabilities.api import CPEViewSet -from vulnerabilities.api import PackageViewSet -from vulnerabilities.api import VulnerabilityViewSet from vulnerabilities.api import PackageV2ViewSet +from vulnerabilities.api import PackageViewSet from vulnerabilities.api import VulnerabilityV2ViewSet +from vulnerabilities.api import VulnerabilityViewSet from vulnerabilities.views import ApiUserCreateView from vulnerabilities.views import HomePage from vulnerabilities.views import PackageDetails @@ -104,7 +104,7 @@ def __init__(self, *args, **kwargs): TemplateView.as_view(template_name="tos.html"), name="api_tos", ), - path('api/v2/', include(api_v2_router.urls)), + path("api/v2/", include(api_v2_router.urls)), path( "admin/", admin.site.urls, From 91657107d2872d1b60f7f743d4d1bc617aa5478d Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Thu, 7 Nov 2024 23:18:29 +0530 Subject: [PATCH 04/13] Fix tests Signed-off-by: Tushar Goel --- vulnerabilities/tests/test_v2_api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vulnerabilities/tests/test_v2_api.py b/vulnerabilities/tests/test_v2_api.py index 99199a322..4eb527de2 100644 --- a/vulnerabilities/tests/test_v2_api.py +++ b/vulnerabilities/tests/test_v2_api.py @@ -177,6 +177,10 @@ def setUp(self): # Associate packages with vulnerabilities self.package1.affected_by_vulnerabilities.add(self.vuln1) self.package2.fixing_vulnerabilities.add(self.vuln2) + self.user = ApiUser.objects.create_api_user(username="e@mail.com") + self.auth = f"Token {self.user.auth_token.key}" + self.client = APIClient(enforce_csrf_checks=True) + self.client.credentials(HTTP_AUTHORIZATION=self.auth) def test_list_packages(self): """ From 849e605b40c705146bfb825fbf668e70a20c2ab1 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Thu, 7 Nov 2024 23:27:09 +0530 Subject: [PATCH 05/13] Add v2 API file Signed-off-by: Tushar Goel --- vulnerabilities/api.py | 172 ----------------------------------- vulnerabilities/v2_api.py | 183 ++++++++++++++++++++++++++++++++++++++ vulnerablecode/urls.py | 4 +- 3 files changed, 185 insertions(+), 174 deletions(-) create mode 100644 vulnerabilities/v2_api.py diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py index 6328d4c92..b8bb703a6 100644 --- a/vulnerabilities/api.py +++ b/vulnerabilities/api.py @@ -691,175 +691,3 @@ class AliasViewSet(VulnerabilityViewSet): """ filterset_class = AliasFilterSet - - -class WeaknessV2Serializer(serializers.ModelSerializer): - cwe_id = serializers.CharField() - name = serializers.CharField() - description = serializers.CharField() - - class Meta: - model = Weakness - fields = ["cwe_id", "name", "description"] - - -class VulnerabilityReferenceV2Serializer(serializers.ModelSerializer): - url = serializers.CharField() - reference_type = serializers.CharField() - reference_id = serializers.CharField() - - class Meta: - model = VulnerabilityReference - fields = ["url", "reference_type", "reference_id"] - - -class VulnerabilityV2Serializer(BaseResourceSerializer): - aliases = serializers.SerializerMethodField() - weaknesses = WeaknessV2Serializer(many=True) - references = VulnerabilityReferenceV2Serializer(many=True, source="vulnerabilityreference_set") - severities = VulnerabilitySeveritySerializer(many=True) - - class Meta: - model = Vulnerability - fields = [ - "vulnerability_id", - "aliases", - "summary", - "severities", - "weaknesses", - "references", - ] - - def get_aliases(self, obj): - return [alias.alias for alias in obj.aliases.all()] - - def get_severities(self, obj): - return obj.severities - - -class VulnerabilityListSerializer(serializers.ModelSerializer): - url = serializers.SerializerMethodField() - - class Meta: - model = Vulnerability - fields = ["vulnerability_id", "url"] - - def get_url(self, obj): - request = self.context.get("request") - return reverse( - "vulnerability-v2-detail", - kwargs={"vulnerability_id": obj.vulnerability_id}, - request=request, - ) - - -class VulnerabilityV2ViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Vulnerability.objects.all() - serializer_class = VulnerabilityV2Serializer - lookup_field = "vulnerability_id" - - def get_queryset(self): - queryset = super().get_queryset() - vulnerability_ids = self.request.query_params.getlist("vulnerability_id") - aliases = self.request.query_params.getlist("alias") - - if vulnerability_ids: - queryset = queryset.filter(vulnerability_id__in=vulnerability_ids) - - if aliases: - queryset = queryset.filter(aliases__alias__in=aliases).distinct() - - return queryset - - def get_serializer_class(self): - if self.action == "list": - return VulnerabilityListSerializer - return super().get_serializer_class() - - 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]) - serializer = self.get_serializer(vulnerability) - return Response(serializer.data) - 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} - 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} - return Response({"vulnerabilities": vulnerabilities}) - - -class PackageV2Serializer(serializers.ModelSerializer): - purl = serializers.CharField(source="package_url") - 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) - - class Meta: - model = Package - fields = [ - "purl", - "affected_by_vulnerabilities", - "fixing_vulnerabilities", - "next_non_vulnerable_version", - "latest_non_vulnerable_version", - ] - - def get_affected_by_vulnerabilities(self, obj): - return [vuln.vulnerability_id for vuln in obj.affected_by_vulnerabilities.all()] - - def get_fixing_vulnerabilities(self, obj): - return [vuln.vulnerability_id for vuln in obj.fixing_vulnerabilities.all()] - - -class PackageV2ViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Package.objects.all() - serializer_class = PackageV2Serializer - - def get_queryset(self): - queryset = super().get_queryset() - package_purls = self.request.query_params.getlist("purl") - affected_by_vulnerability = self.request.query_params.get("affected_by_vulnerability") - fixing_vulnerability = self.request.query_params.get("fixing_vulnerability") - - if package_purls: - queryset = queryset.filter(package_url__in=package_purls) - if affected_by_vulnerability: - queryset = queryset.filter( - affected_by_vulnerabilities__vulnerability_id=affected_by_vulnerability - ) - if fixing_vulnerability: - queryset = queryset.filter( - fixing_vulnerabilities__vulnerability_id=fixing_vulnerability - ) - return queryset.with_is_vulnerable() - - def list(self, request, *args, **kwargs): - queryset = self.get_queryset() - # Apply pagination - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - data = serializer.data - # Use 'self.get_paginated_response' to include pagination data - return self.get_paginated_response({"purls": data}) - - # If pagination is not applied - serializer = self.get_serializer(queryset, many=True) - data = serializer.data - return Response({"purls": data}) diff --git a/vulnerabilities/v2_api.py b/vulnerabilities/v2_api.py new file mode 100644 index 000000000..1c3ed2481 --- /dev/null +++ b/vulnerabilities/v2_api.py @@ -0,0 +1,183 @@ +from rest_framework import serializers +from rest_framework import viewsets +from rest_framework.response import Response +from rest_framework.reverse import reverse + +from vulnerabilities.models import Package +from vulnerabilities.models import Vulnerability +from vulnerabilities.models import VulnerabilityReference +from vulnerabilities.models import Weakness +from vulnerabilities.models import VulnerabilitySeverity + +from vulnerabilities.api import VulnerabilitySeveritySerializer + +class WeaknessV2Serializer(serializers.ModelSerializer): + cwe_id = serializers.CharField() + name = serializers.CharField() + description = serializers.CharField() + + class Meta: + model = Weakness + fields = ["cwe_id", "name", "description"] + + +class VulnerabilityReferenceV2Serializer(serializers.ModelSerializer): + url = serializers.CharField() + reference_type = serializers.CharField() + reference_id = serializers.CharField() + + class Meta: + model = VulnerabilityReference + fields = ["url", "reference_type", "reference_id"] + + +class VulnerabilityV2Serializer(serializers.ModelSerializer): + aliases = serializers.SerializerMethodField() + weaknesses = WeaknessV2Serializer(many=True) + references = VulnerabilityReferenceV2Serializer(many=True, source="vulnerabilityreference_set") + severities = VulnerabilitySeveritySerializer(many=True) + + class Meta: + model = Vulnerability + fields = [ + "vulnerability_id", + "aliases", + "summary", + "severities", + "weaknesses", + "references", + ] + + def get_aliases(self, obj): + return [alias.alias for alias in obj.aliases.all()] + + def get_severities(self, obj): + return obj.severities + + +class VulnerabilityListSerializer(serializers.ModelSerializer): + url = serializers.SerializerMethodField() + + class Meta: + model = Vulnerability + fields = ["vulnerability_id", "url"] + + def get_url(self, obj): + request = self.context.get("request") + return reverse( + "vulnerability-v2-detail", + kwargs={"vulnerability_id": obj.vulnerability_id}, + request=request, + ) + + +class VulnerabilityV2ViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Vulnerability.objects.all() + serializer_class = VulnerabilityV2Serializer + lookup_field = "vulnerability_id" + + def get_queryset(self): + queryset = super().get_queryset() + vulnerability_ids = self.request.query_params.getlist("vulnerability_id") + aliases = self.request.query_params.getlist("alias") + + if vulnerability_ids: + queryset = queryset.filter(vulnerability_id__in=vulnerability_ids) + + if aliases: + queryset = queryset.filter(aliases__alias__in=aliases).distinct() + + return queryset + + def get_serializer_class(self): + if self.action == "list": + return VulnerabilityListSerializer + return super().get_serializer_class() + + 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]) + serializer = self.get_serializer(vulnerability) + return Response(serializer.data) + 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} + 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} + return Response({"vulnerabilities": vulnerabilities}) + + +class PackageV2Serializer(serializers.ModelSerializer): + purl = serializers.CharField(source="package_url") + 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) + + class Meta: + model = Package + fields = [ + "purl", + "affected_by_vulnerabilities", + "fixing_vulnerabilities", + "next_non_vulnerable_version", + "latest_non_vulnerable_version", + ] + + def get_affected_by_vulnerabilities(self, obj): + return [vuln.vulnerability_id for vuln in obj.affected_by_vulnerabilities.all()] + + def get_fixing_vulnerabilities(self, obj): + return [vuln.vulnerability_id for vuln in obj.fixing_vulnerabilities.all()] + + +class PackageV2ViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Package.objects.all() + serializer_class = PackageV2Serializer + + def get_queryset(self): + queryset = super().get_queryset() + package_purls = self.request.query_params.getlist("purl") + affected_by_vulnerability = self.request.query_params.get("affected_by_vulnerability") + fixing_vulnerability = self.request.query_params.get("fixing_vulnerability") + + if package_purls: + queryset = queryset.filter(package_url__in=package_purls) + if affected_by_vulnerability: + queryset = queryset.filter( + affected_by_vulnerabilities__vulnerability_id=affected_by_vulnerability + ) + if fixing_vulnerability: + queryset = queryset.filter( + fixing_vulnerabilities__vulnerability_id=fixing_vulnerability + ) + return queryset.with_is_vulnerable() + + def list(self, request, *args, **kwargs): + queryset = self.get_queryset() + # Apply pagination + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + data = serializer.data + # Use 'self.get_paginated_response' to include pagination data + return self.get_paginated_response({"purls": data}) + + # If pagination is not applied + serializer = self.get_serializer(queryset, many=True) + data = serializer.data + return Response({"purls": data}) diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index 4d5bebf0a..d21f5181f 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -18,9 +18,9 @@ from vulnerabilities.api import AliasViewSet from vulnerabilities.api import CPEViewSet -from vulnerabilities.api import PackageV2ViewSet +from vulnerabilities.v2_api import PackageV2ViewSet from vulnerabilities.api import PackageViewSet -from vulnerabilities.api import VulnerabilityV2ViewSet +from vulnerabilities.v2_api import VulnerabilityV2ViewSet from vulnerabilities.api import VulnerabilityViewSet from vulnerabilities.views import ApiUserCreateView from vulnerabilities.views import HomePage From 7b14634ceda6c82f6073ac1d6d477ed91691eb44 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Thu, 7 Nov 2024 23:27:48 +0530 Subject: [PATCH 06/13] Fix formatting Signed-off-by: Tushar Goel --- vulnerabilities/v2_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vulnerabilities/v2_api.py b/vulnerabilities/v2_api.py index 1c3ed2481..18887d693 100644 --- a/vulnerabilities/v2_api.py +++ b/vulnerabilities/v2_api.py @@ -3,13 +3,13 @@ from rest_framework.response import Response from rest_framework.reverse import reverse +from vulnerabilities.api import VulnerabilitySeveritySerializer from vulnerabilities.models import Package from vulnerabilities.models import Vulnerability from vulnerabilities.models import VulnerabilityReference -from vulnerabilities.models import Weakness from vulnerabilities.models import VulnerabilitySeverity +from vulnerabilities.models import Weakness -from vulnerabilities.api import VulnerabilitySeveritySerializer class WeaknessV2Serializer(serializers.ModelSerializer): cwe_id = serializers.CharField() From c73f6212e48a0853c634d4039d5faea43140dc03 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Thu, 7 Nov 2024 23:27:59 +0530 Subject: [PATCH 07/13] Fix formatting Signed-off-by: Tushar Goel --- vulnerablecode/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index d21f5181f..3676c8a66 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -18,10 +18,10 @@ from vulnerabilities.api import AliasViewSet from vulnerabilities.api import CPEViewSet -from vulnerabilities.v2_api import PackageV2ViewSet from vulnerabilities.api import PackageViewSet -from vulnerabilities.v2_api import VulnerabilityV2ViewSet from vulnerabilities.api import VulnerabilityViewSet +from vulnerabilities.v2_api import PackageV2ViewSet +from vulnerabilities.v2_api import VulnerabilityV2ViewSet from vulnerabilities.views import ApiUserCreateView from vulnerabilities.views import HomePage from vulnerabilities.views import PackageDetails From 208658e95096faff7f061f8339274c6e4a55e7a8 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Thu, 7 Nov 2024 23:29:31 +0530 Subject: [PATCH 08/13] Fix tests import Signed-off-by: Tushar Goel --- vulnerabilities/tests/test_v2_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vulnerabilities/tests/test_v2_api.py b/vulnerabilities/tests/test_v2_api.py index 4eb527de2..167271018 100644 --- a/vulnerabilities/tests/test_v2_api.py +++ b/vulnerabilities/tests/test_v2_api.py @@ -5,14 +5,14 @@ from rest_framework.test import APIClient from rest_framework.test import APITestCase -from vulnerabilities.api import PackageV2Serializer -from vulnerabilities.api import VulnerabilityListSerializer from vulnerabilities.models import Alias from vulnerabilities.models import ApiUser from vulnerabilities.models import Package from vulnerabilities.models import Vulnerability from vulnerabilities.models import VulnerabilityReference from vulnerabilities.models import Weakness +from vulnerabilities.v2_api import PackageV2Serializer +from vulnerabilities.v2_api import VulnerabilityListSerializer class VulnerabilityV2ViewSetTest(APITestCase): From 76c13a80d3fac1d48b41df30f821720733c8d010 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Thu, 7 Nov 2024 23:39:27 +0530 Subject: [PATCH 09/13] Add copyright headers Signed-off-by: Tushar Goel --- vulnerabilities/tests/test_v2_api.py | 10 +++++++++- vulnerabilities/v2_api.py | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/vulnerabilities/tests/test_v2_api.py b/vulnerabilities/tests/test_v2_api.py index 167271018..ad20d292b 100644 --- a/vulnerabilities/tests/test_v2_api.py +++ b/vulnerabilities/tests/test_v2_api.py @@ -1,4 +1,12 @@ -# tests.py +# +# 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.urls import reverse from packageurl import PackageURL from rest_framework import status diff --git a/vulnerabilities/v2_api.py b/vulnerabilities/v2_api.py index 18887d693..883602678 100644 --- a/vulnerabilities/v2_api.py +++ b/vulnerabilities/v2_api.py @@ -1,3 +1,12 @@ +# +# 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 rest_framework import serializers from rest_framework import viewsets from rest_framework.response import Response From a97b5bd9487f7405bcc18432d568d157eef97d61 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Mon, 11 Nov 2024 17:15:51 +0530 Subject: [PATCH 10/13] Address review comment Signed-off-by: Tushar Goel --- vulnerabilities/api_v2.py | 192 +++++++++++++++++++++++++++ vulnerabilities/tests/test_v2_api.py | 4 +- vulnerablecode/urls.py | 4 +- 3 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 vulnerabilities/api_v2.py diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py new file mode 100644 index 000000000..883602678 --- /dev/null +++ b/vulnerabilities/api_v2.py @@ -0,0 +1,192 @@ +# +# 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 rest_framework import serializers +from rest_framework import viewsets +from rest_framework.response import Response +from rest_framework.reverse import reverse + +from vulnerabilities.api import VulnerabilitySeveritySerializer +from vulnerabilities.models import Package +from vulnerabilities.models import Vulnerability +from vulnerabilities.models import VulnerabilityReference +from vulnerabilities.models import VulnerabilitySeverity +from vulnerabilities.models import Weakness + + +class WeaknessV2Serializer(serializers.ModelSerializer): + cwe_id = serializers.CharField() + name = serializers.CharField() + description = serializers.CharField() + + class Meta: + model = Weakness + fields = ["cwe_id", "name", "description"] + + +class VulnerabilityReferenceV2Serializer(serializers.ModelSerializer): + url = serializers.CharField() + reference_type = serializers.CharField() + reference_id = serializers.CharField() + + class Meta: + model = VulnerabilityReference + fields = ["url", "reference_type", "reference_id"] + + +class VulnerabilityV2Serializer(serializers.ModelSerializer): + aliases = serializers.SerializerMethodField() + weaknesses = WeaknessV2Serializer(many=True) + references = VulnerabilityReferenceV2Serializer(many=True, source="vulnerabilityreference_set") + severities = VulnerabilitySeveritySerializer(many=True) + + class Meta: + model = Vulnerability + fields = [ + "vulnerability_id", + "aliases", + "summary", + "severities", + "weaknesses", + "references", + ] + + def get_aliases(self, obj): + return [alias.alias for alias in obj.aliases.all()] + + def get_severities(self, obj): + return obj.severities + + +class VulnerabilityListSerializer(serializers.ModelSerializer): + url = serializers.SerializerMethodField() + + class Meta: + model = Vulnerability + fields = ["vulnerability_id", "url"] + + def get_url(self, obj): + request = self.context.get("request") + return reverse( + "vulnerability-v2-detail", + kwargs={"vulnerability_id": obj.vulnerability_id}, + request=request, + ) + + +class VulnerabilityV2ViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Vulnerability.objects.all() + serializer_class = VulnerabilityV2Serializer + lookup_field = "vulnerability_id" + + def get_queryset(self): + queryset = super().get_queryset() + vulnerability_ids = self.request.query_params.getlist("vulnerability_id") + aliases = self.request.query_params.getlist("alias") + + if vulnerability_ids: + queryset = queryset.filter(vulnerability_id__in=vulnerability_ids) + + if aliases: + queryset = queryset.filter(aliases__alias__in=aliases).distinct() + + return queryset + + def get_serializer_class(self): + if self.action == "list": + return VulnerabilityListSerializer + return super().get_serializer_class() + + 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]) + serializer = self.get_serializer(vulnerability) + return Response(serializer.data) + 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} + 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} + return Response({"vulnerabilities": vulnerabilities}) + + +class PackageV2Serializer(serializers.ModelSerializer): + purl = serializers.CharField(source="package_url") + 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) + + class Meta: + model = Package + fields = [ + "purl", + "affected_by_vulnerabilities", + "fixing_vulnerabilities", + "next_non_vulnerable_version", + "latest_non_vulnerable_version", + ] + + def get_affected_by_vulnerabilities(self, obj): + return [vuln.vulnerability_id for vuln in obj.affected_by_vulnerabilities.all()] + + def get_fixing_vulnerabilities(self, obj): + return [vuln.vulnerability_id for vuln in obj.fixing_vulnerabilities.all()] + + +class PackageV2ViewSet(viewsets.ReadOnlyModelViewSet): + queryset = Package.objects.all() + serializer_class = PackageV2Serializer + + def get_queryset(self): + queryset = super().get_queryset() + package_purls = self.request.query_params.getlist("purl") + affected_by_vulnerability = self.request.query_params.get("affected_by_vulnerability") + fixing_vulnerability = self.request.query_params.get("fixing_vulnerability") + + if package_purls: + queryset = queryset.filter(package_url__in=package_purls) + if affected_by_vulnerability: + queryset = queryset.filter( + affected_by_vulnerabilities__vulnerability_id=affected_by_vulnerability + ) + if fixing_vulnerability: + queryset = queryset.filter( + fixing_vulnerabilities__vulnerability_id=fixing_vulnerability + ) + return queryset.with_is_vulnerable() + + def list(self, request, *args, **kwargs): + queryset = self.get_queryset() + # Apply pagination + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + data = serializer.data + # Use 'self.get_paginated_response' to include pagination data + return self.get_paginated_response({"purls": data}) + + # If pagination is not applied + serializer = self.get_serializer(queryset, many=True) + data = serializer.data + return Response({"purls": data}) diff --git a/vulnerabilities/tests/test_v2_api.py b/vulnerabilities/tests/test_v2_api.py index ad20d292b..b8d619ba7 100644 --- a/vulnerabilities/tests/test_v2_api.py +++ b/vulnerabilities/tests/test_v2_api.py @@ -19,8 +19,8 @@ from vulnerabilities.models import Vulnerability from vulnerabilities.models import VulnerabilityReference from vulnerabilities.models import Weakness -from vulnerabilities.v2_api import PackageV2Serializer -from vulnerabilities.v2_api import VulnerabilityListSerializer +from vulnerabilities.api_v2 import PackageV2Serializer +from vulnerabilities.api_v2 import VulnerabilityListSerializer class VulnerabilityV2ViewSetTest(APITestCase): diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index 3676c8a66..d92d6ce40 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -20,8 +20,8 @@ from vulnerabilities.api import CPEViewSet from vulnerabilities.api import PackageViewSet from vulnerabilities.api import VulnerabilityViewSet -from vulnerabilities.v2_api import PackageV2ViewSet -from vulnerabilities.v2_api import VulnerabilityV2ViewSet +from vulnerabilities.api_v2 import PackageV2ViewSet +from vulnerabilities.api_v2 import VulnerabilityV2ViewSet from vulnerabilities.views import ApiUserCreateView from vulnerabilities.views import HomePage from vulnerabilities.views import PackageDetails From 4ca07a99c8c43fe427a6af83af55fdf0c3838763 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Mon, 11 Nov 2024 17:16:36 +0530 Subject: [PATCH 11/13] Address review comment Signed-off-by: Tushar Goel --- vulnerabilities/api_v2.py | 2 + vulnerabilities/v2_api.py | 192 -------------------------------------- 2 files changed, 2 insertions(+), 192 deletions(-) delete mode 100644 vulnerabilities/v2_api.py diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index 883602678..1633cb230 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -7,6 +7,7 @@ # See https://aboutcode.org for more information about nexB OSS projects. # + from rest_framework import serializers from rest_framework import viewsets from rest_framework.response import Response @@ -35,6 +36,7 @@ class VulnerabilityReferenceV2Serializer(serializers.ModelSerializer): reference_type = serializers.CharField() reference_id = serializers.CharField() + class Meta: model = VulnerabilityReference fields = ["url", "reference_type", "reference_id"] diff --git a/vulnerabilities/v2_api.py b/vulnerabilities/v2_api.py deleted file mode 100644 index 883602678..000000000 --- a/vulnerabilities/v2_api.py +++ /dev/null @@ -1,192 +0,0 @@ -# -# 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 rest_framework import serializers -from rest_framework import viewsets -from rest_framework.response import Response -from rest_framework.reverse import reverse - -from vulnerabilities.api import VulnerabilitySeveritySerializer -from vulnerabilities.models import Package -from vulnerabilities.models import Vulnerability -from vulnerabilities.models import VulnerabilityReference -from vulnerabilities.models import VulnerabilitySeverity -from vulnerabilities.models import Weakness - - -class WeaknessV2Serializer(serializers.ModelSerializer): - cwe_id = serializers.CharField() - name = serializers.CharField() - description = serializers.CharField() - - class Meta: - model = Weakness - fields = ["cwe_id", "name", "description"] - - -class VulnerabilityReferenceV2Serializer(serializers.ModelSerializer): - url = serializers.CharField() - reference_type = serializers.CharField() - reference_id = serializers.CharField() - - class Meta: - model = VulnerabilityReference - fields = ["url", "reference_type", "reference_id"] - - -class VulnerabilityV2Serializer(serializers.ModelSerializer): - aliases = serializers.SerializerMethodField() - weaknesses = WeaknessV2Serializer(many=True) - references = VulnerabilityReferenceV2Serializer(many=True, source="vulnerabilityreference_set") - severities = VulnerabilitySeveritySerializer(many=True) - - class Meta: - model = Vulnerability - fields = [ - "vulnerability_id", - "aliases", - "summary", - "severities", - "weaknesses", - "references", - ] - - def get_aliases(self, obj): - return [alias.alias for alias in obj.aliases.all()] - - def get_severities(self, obj): - return obj.severities - - -class VulnerabilityListSerializer(serializers.ModelSerializer): - url = serializers.SerializerMethodField() - - class Meta: - model = Vulnerability - fields = ["vulnerability_id", "url"] - - def get_url(self, obj): - request = self.context.get("request") - return reverse( - "vulnerability-v2-detail", - kwargs={"vulnerability_id": obj.vulnerability_id}, - request=request, - ) - - -class VulnerabilityV2ViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Vulnerability.objects.all() - serializer_class = VulnerabilityV2Serializer - lookup_field = "vulnerability_id" - - def get_queryset(self): - queryset = super().get_queryset() - vulnerability_ids = self.request.query_params.getlist("vulnerability_id") - aliases = self.request.query_params.getlist("alias") - - if vulnerability_ids: - queryset = queryset.filter(vulnerability_id__in=vulnerability_ids) - - if aliases: - queryset = queryset.filter(aliases__alias__in=aliases).distinct() - - return queryset - - def get_serializer_class(self): - if self.action == "list": - return VulnerabilityListSerializer - return super().get_serializer_class() - - 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]) - serializer = self.get_serializer(vulnerability) - return Response(serializer.data) - 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} - 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} - return Response({"vulnerabilities": vulnerabilities}) - - -class PackageV2Serializer(serializers.ModelSerializer): - purl = serializers.CharField(source="package_url") - 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) - - class Meta: - model = Package - fields = [ - "purl", - "affected_by_vulnerabilities", - "fixing_vulnerabilities", - "next_non_vulnerable_version", - "latest_non_vulnerable_version", - ] - - def get_affected_by_vulnerabilities(self, obj): - return [vuln.vulnerability_id for vuln in obj.affected_by_vulnerabilities.all()] - - def get_fixing_vulnerabilities(self, obj): - return [vuln.vulnerability_id for vuln in obj.fixing_vulnerabilities.all()] - - -class PackageV2ViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Package.objects.all() - serializer_class = PackageV2Serializer - - def get_queryset(self): - queryset = super().get_queryset() - package_purls = self.request.query_params.getlist("purl") - affected_by_vulnerability = self.request.query_params.get("affected_by_vulnerability") - fixing_vulnerability = self.request.query_params.get("fixing_vulnerability") - - if package_purls: - queryset = queryset.filter(package_url__in=package_purls) - if affected_by_vulnerability: - queryset = queryset.filter( - affected_by_vulnerabilities__vulnerability_id=affected_by_vulnerability - ) - if fixing_vulnerability: - queryset = queryset.filter( - fixing_vulnerabilities__vulnerability_id=fixing_vulnerability - ) - return queryset.with_is_vulnerable() - - def list(self, request, *args, **kwargs): - queryset = self.get_queryset() - # Apply pagination - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - data = serializer.data - # Use 'self.get_paginated_response' to include pagination data - return self.get_paginated_response({"purls": data}) - - # If pagination is not applied - serializer = self.get_serializer(queryset, many=True) - data = serializer.data - return Response({"purls": data}) From a0533384ba813d2ab252771e6ee1b2a7922fc3b6 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Mon, 11 Nov 2024 17:17:37 +0530 Subject: [PATCH 12/13] Address review comment Signed-off-by: Tushar Goel --- vulnerabilities/api_v2.py | 1 - vulnerabilities/tests/{test_v2_api.py => test_api_v2.py} | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) rename vulnerabilities/tests/{test_v2_api.py => test_api_v2.py} (100%) diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index 1633cb230..232d365cc 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -36,7 +36,6 @@ class VulnerabilityReferenceV2Serializer(serializers.ModelSerializer): reference_type = serializers.CharField() reference_id = serializers.CharField() - class Meta: model = VulnerabilityReference fields = ["url", "reference_type", "reference_id"] diff --git a/vulnerabilities/tests/test_v2_api.py b/vulnerabilities/tests/test_api_v2.py similarity index 100% rename from vulnerabilities/tests/test_v2_api.py rename to vulnerabilities/tests/test_api_v2.py index b8d619ba7..a2a8bda98 100644 --- a/vulnerabilities/tests/test_v2_api.py +++ b/vulnerabilities/tests/test_api_v2.py @@ -13,14 +13,14 @@ from rest_framework.test import APIClient from rest_framework.test import APITestCase +from vulnerabilities.api_v2 import PackageV2Serializer +from vulnerabilities.api_v2 import VulnerabilityListSerializer from vulnerabilities.models import Alias from vulnerabilities.models import ApiUser from vulnerabilities.models import Package from vulnerabilities.models import Vulnerability from vulnerabilities.models import VulnerabilityReference from vulnerabilities.models import Weakness -from vulnerabilities.api_v2 import PackageV2Serializer -from vulnerabilities.api_v2 import VulnerabilityListSerializer class VulnerabilityV2ViewSetTest(APITestCase): From fbae9377caa2afdd6c06be174ae7d760b5fff75c Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Mon, 11 Nov 2024 17:19:05 +0530 Subject: [PATCH 13/13] Address review comment Signed-off-by: Tushar Goel --- vulnerabilities/api_v2.py | 4 ++-- vulnerabilities/tests/test_api_v2.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index 232d365cc..e8361c092 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -185,9 +185,9 @@ def list(self, request, *args, **kwargs): serializer = self.get_serializer(page, many=True) data = serializer.data # Use 'self.get_paginated_response' to include pagination data - return self.get_paginated_response({"purls": data}) + return self.get_paginated_response({"packages": data}) # If pagination is not applied serializer = self.get_serializer(queryset, many=True) data = serializer.data - return Response({"purls": data}) + return Response({"packages": data}) diff --git a/vulnerabilities/tests/test_api_v2.py b/vulnerabilities/tests/test_api_v2.py index a2a8bda98..eeaa18776 100644 --- a/vulnerabilities/tests/test_api_v2.py +++ b/vulnerabilities/tests/test_api_v2.py @@ -198,8 +198,8 @@ def test_list_packages(self): url = reverse("package-v2-list") response = self.client.get(url, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn("purls", response.data["results"]) - self.assertEqual(len(response.data["results"]["purls"]), 2) + self.assertIn("packages", response.data["results"]) + self.assertEqual(len(response.data["results"]["packages"]), 2) def test_filter_packages_by_purl(self): """ @@ -208,8 +208,8 @@ def test_filter_packages_by_purl(self): url = reverse("package-v2-list") response = self.client.get(url, {"purl": "pkg:pypi/django@3.2"}, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data["results"]["purls"]), 1) - self.assertEqual(response.data["results"]["purls"][0]["purl"], "pkg:pypi/django@3.2") + self.assertEqual(len(response.data["results"]["packages"]), 1) + self.assertEqual(response.data["results"]["packages"][0]["purl"], "pkg:pypi/django@3.2") def test_filter_packages_by_affected_vulnerability(self): """ @@ -218,8 +218,8 @@ def test_filter_packages_by_affected_vulnerability(self): url = reverse("package-v2-list") response = self.client.get(url, {"affected_by_vulnerability": "VCID-1234"}, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data["results"]["purls"]), 1) - self.assertEqual(response.data["results"]["purls"][0]["purl"], "pkg:pypi/django@3.2") + self.assertEqual(len(response.data["results"]["packages"]), 1) + self.assertEqual(response.data["results"]["packages"][0]["purl"], "pkg:pypi/django@3.2") def test_filter_packages_by_fixing_vulnerability(self): """ @@ -228,8 +228,8 @@ def test_filter_packages_by_fixing_vulnerability(self): url = reverse("package-v2-list") response = self.client.get(url, {"fixing_vulnerability": "VCID-5678"}, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data["results"]["purls"]), 1) - self.assertEqual(response.data["results"]["purls"][0]["purl"], "pkg:npm/lodash@4.17.20") + self.assertEqual(len(response.data["results"]["packages"]), 1) + self.assertEqual(response.data["results"]["packages"][0]["purl"], "pkg:npm/lodash@4.17.20") def test_package_serializer_fields(self): """ @@ -276,7 +276,7 @@ def test_invalid_vulnerability_filter(self): url = reverse("package-v2-list") response = self.client.get(url, {"affected_by_vulnerability": "VCID-9999"}, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data["results"]["purls"]), 0) + self.assertEqual(len(response.data["results"]["packages"]), 0) def test_invalid_purl_filter(self): """ @@ -286,7 +286,7 @@ def test_invalid_purl_filter(self): url = reverse("package-v2-list") response = self.client.get(url, {"purl": "pkg:nonexistent/package@1.0.0"}, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data["results"]["purls"]), 0) + self.assertEqual(len(response.data["results"]["packages"]), 0) def test_get_affected_by_vulnerabilities(self): """