Skip to content

Commit 773064e

Browse files
committed
Merge branch 'main' into 798-new-ui
Reference: #798 Signed-off-by: John M. Horan johnmhoran@gmail.com
2 parents cda5c42 + c062c71 commit 773064e

6 files changed

Lines changed: 244 additions & 59 deletions

File tree

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ Version v30.0.0
5252
- Package endpoint will give fixed packages of only those that
5353
matches type, name, namespace, subpath and qualifiers of the package queried.
5454

55+
- Paginated initial listings to display a small number of records
56+
and provided page per size with a maximum limit of 100 records per page.
57+
58+
- Add fixed packages in vulnerabilities details in packages endpoint.
59+
60+
- Add bulk search support for CPEs.
61+
5562
Other:
5663

5764
- we dropped calver to use a plain semver.

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ cryptography==36.0.2
2121
decorator==5.1.1
2222
defusedxml==0.7.1
2323
distro==1.7.0
24-
Django==4.0.6
24+
Django==4.0.7
2525
django-environ==0.8.1
2626
django-filter==21.1
2727
django-widget-tweaks==1.4.12

vulnerabilities/api.py

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,15 @@ class VulnSerializerRefsAndSummary(serializers.HyperlinkedModelSerializer):
5656
Used for nesting inside package focused APIs.
5757
"""
5858

59+
fixed_packages = MinimalPackageSerializer(
60+
many=True, source="filtered_fixed_packages", read_only=True
61+
)
62+
5963
references = VulnerabilityReferenceSerializer(many=True, source="vulnerabilityreference_set")
6064

6165
class Meta:
6266
model = Vulnerability
63-
fields = ["url", "vulnerability_id", "summary", "references"]
67+
fields = ["url", "vulnerability_id", "summary", "references", "fixed_packages"]
6468

6569

6670
class MinimalVulnerabilitySerializer(serializers.HyperlinkedModelSerializer):
@@ -73,21 +77,6 @@ class Meta:
7377
fields = ["url", "vulnerability_id"]
7478

7579

76-
class PackageSerializerFixedVulns(serializers.HyperlinkedModelSerializer):
77-
"""
78-
Used for nesting inside vulnerability focused APIs.
79-
"""
80-
81-
purl = serializers.CharField(source="package_url")
82-
fixing_vulnerabilities = MinimalVulnerabilitySerializer(
83-
many=True, source="resolved_to", read_only=True
84-
)
85-
86-
class Meta:
87-
model = Package
88-
fields = ["url", "purl", "fixing_vulnerabilities"]
89-
90-
9180
class AliasSerializer(serializers.HyperlinkedModelSerializer):
9281
"""
9382
Used for nesting inside package focused APIs.
@@ -128,13 +117,57 @@ def to_representation(self, instance):
128117
return data
129118

130119
purl = serializers.CharField(source="package_url")
131-
affected_by_vulnerabilities = VulnSerializerRefsAndSummary(
132-
many=True, source="vulnerable_to", read_only=True
133-
)
134-
fixing_vulnerabilities = VulnSerializerRefsAndSummary(
135-
many=True, source="resolved_to", read_only=True
136-
)
137-
fixed_packages = PackageSerializerFixedVulns(many=True, read_only=True)
120+
121+
affected_by_vulnerabilities = serializers.SerializerMethodField("get_affected_vulnerabilities")
122+
123+
fixing_vulnerabilities = serializers.SerializerMethodField("get_fixed_vulnerabilities")
124+
125+
def get_fixed_packages(self, package):
126+
"""
127+
Return a queryset of all packages that fixes a vulnerability with
128+
same type, namespace, name, subpath and qualifiers of the `package`
129+
"""
130+
return Package.objects.filter(
131+
name=package.name,
132+
namespace=package.namespace,
133+
type=package.type,
134+
qualifiers=package.qualifiers,
135+
subpath=package.subpath,
136+
packagerelatedvulnerability__fix=True,
137+
).distinct()
138+
139+
def get_vulnerabilities_for_a_package(self, package, fix):
140+
"""
141+
Return a queryset of vulnerabilities related to the given `package`.
142+
Return vulnerabilities that affects the `package` if given `fix` flag is False,
143+
otherwise return vulnerabilities fixed by the `package`.
144+
"""
145+
fixed_packages = self.get_fixed_packages(package=package)
146+
qs = package.vulnerabilities.filter(packagerelatedvulnerability__fix=fix)
147+
qs = qs.prefetch_related(
148+
Prefetch(
149+
"packages",
150+
queryset=fixed_packages,
151+
to_attr="filtered_fixed_packages",
152+
)
153+
)
154+
return VulnSerializerRefsAndSummary(
155+
instance=qs,
156+
many=True,
157+
context={"request": self.context["request"]},
158+
).data
159+
160+
def get_fixed_vulnerabilities(self, package):
161+
"""
162+
Return a queryset of vulnerabilities fixed in the given `package`.
163+
"""
164+
return self.get_vulnerabilities_for_a_package(package=package, fix=True)
165+
166+
def get_affected_vulnerabilities(self, package):
167+
"""
168+
Return a queryset of vulnerabilities that affects the given `package`.
169+
"""
170+
return self.get_vulnerabilities_for_a_package(package=package, fix=False)
138171

139172
class Meta:
140173
model = Package
@@ -148,7 +181,6 @@ class Meta:
148181
"qualifiers",
149182
"subpath",
150183
"affected_by_vulnerabilities",
151-
"fixed_packages",
152184
"fixing_vulnerabilities",
153185
]
154186

@@ -177,7 +209,6 @@ def filter_purl(self, queryset, name, value):
177209
class PackageViewSet(viewsets.ReadOnlyModelViewSet):
178210
queryset = Package.objects.all()
179211
serializer_class = PackageSerializer
180-
paginate_by = 50
181212
filter_backends = (filters.DjangoFilterBackend,)
182213
filterset_class = PackageFilterSet
183214

@@ -252,7 +283,6 @@ def get_queryset(self):
252283
)
253284

254285
serializer_class = VulnerabilitySerializer
255-
paginate_by = 50
256286
filter_backends = (filters.DjangoFilterBackend,)
257287
filterset_class = VulnerabilityFilterSet
258288

@@ -270,10 +300,33 @@ class CPEViewSet(viewsets.ReadOnlyModelViewSet):
270300
vulnerabilityreference__reference_id__startswith="cpe"
271301
).distinct()
272302
serializer_class = VulnerabilitySerializer
273-
paginate_by = 50
274303
filter_backends = (filters.DjangoFilterBackend,)
275304
filterset_class = CPEFilterSet
276305

306+
@action(detail=False, methods=["post"])
307+
def bulk_search(self, request):
308+
"""
309+
This endpoint is used to search for vulnerabilities by more than one CPE.
310+
"""
311+
response = []
312+
cpes = request.data.get("cpes", []) or []
313+
if not cpes or not isinstance(cpes, list):
314+
return Response(
315+
status=400,
316+
data={"Error": "A non-empty 'cpe' list of package URLs is required."},
317+
)
318+
for cpe in cpes:
319+
if not cpe.startswith("cpe"):
320+
return Response(status=400, data={"Error": f"Invalid CPE: {cpe}"})
321+
vulnerabilitiesResponse = Vulnerability.objects.filter(
322+
vulnerabilityreference__reference_id__in=cpes
323+
).distinct()
324+
return Response(
325+
VulnerabilitySerializer(
326+
vulnerabilitiesResponse, many=True, context={"request": request}
327+
).data
328+
)
329+
277330

278331
class AliasFilterSet(filters.FilterSet):
279332
alias = filters.CharFilter(method="filter_alias")
@@ -286,6 +339,5 @@ def filter_alias(self, queryset, name, value):
286339
class AliasViewSet(viewsets.ReadOnlyModelViewSet):
287340
queryset = Vulnerability.objects.all()
288341
serializer_class = VulnerabilitySerializer
289-
paginate_by = 50
290342
filter_backends = (filters.DjangoFilterBackend,)
291343
filterset_class = AliasFilterSet

vulnerabilities/pagination.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from rest_framework.pagination import PageNumberPagination
2+
3+
4+
class SmallResultSetPagination(PageNumberPagination):
5+
page_size_query_param = "page_size"
6+
max_page_size = 100

0 commit comments

Comments
 (0)