Skip to content
9 changes: 9 additions & 0 deletions vulnerabilities/api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ class PackageV2FilterSet(filters.FilterSet):
)
fixing_vulnerability = filters.CharFilter(field_name="fixing_vulnerabilities__vulnerability_id")
purl = filters.CharFilter(field_name="package_url")
is_vulnerable = filters.BooleanFilter(method="filter_is_vulnerable")

def filter_is_vulnerable(self, queryset, name, value):
return queryset.filter(is_vulnerable=value)


class PackageV2ViewSet(viewsets.ReadOnlyModelViewSet):
Expand All @@ -273,6 +277,7 @@ def get_queryset(self):
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")
is_vulnerable = self.request.query_params.get("is_vulnerable")

if package_purls:
queryset = queryset.filter(package_url__in=package_purls)
Expand All @@ -284,6 +289,10 @@ def get_queryset(self):
queryset = queryset.filter(
fixing_vulnerabilities__vulnerability_id=fixing_vulnerability
)
if is_vulnerable is not None:
queryset = queryset.with_is_vulnerable()
is_vulnerable = is_vulnerable.lower() == "true"
queryset = queryset.filter(is_vulnerable=is_vulnerable)
return queryset.with_is_vulnerable()

def list(self, request, *args, **kwargs):
Expand Down
8 changes: 8 additions & 0 deletions vulnerabilities/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ class PackageSearchForm(forms.Form):
attrs={"placeholder": "Package name, purl or purl fragment"},
),
)
vulnerable_only = forms.ChoiceField(
required=False,
choices=[
("", "All Packages"),
("true", "Vulnerable Only"),
("false", "Non-Vulnerable Only"),
],
)


class VulnerabilitySearchForm(forms.Form):
Expand Down
33 changes: 32 additions & 1 deletion vulnerabilities/templates/packages.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,37 @@
<div>
{{ page_obj.paginator.count|intcomma }} results
</div>
<div class="dropdown is-hoverable">
<div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu">
<span class="icon is-small">
<i class="fa fa-filter" aria-hidden="true"></i>
</span>
<span>
{% if request.GET.vulnerable_only == 'true' %}
Vulnerable Only
{% elif request.GET.vulnerable_only == 'false' %}
Non-Vulnerable Only
{% else %}
All Packages
{% endif %}
</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
<a href="?search={{ search }}" class="dropdown-item {% if not request.GET.vulnerable_only %}is-active{% endif %}">
All Packages
</a>
<a href="?search={{ search }}&vulnerable_only=true" class="dropdown-item {% if request.GET.vulnerable_only == 'true' %}is-active{% endif %}">
Vulnerable Only
</a>
<a href="?search={{ search }}&vulnerable_only=false" class="dropdown-item {% if request.GET.vulnerable_only == 'false' %}is-active{% endif %}">
Non-Vulnerable Only
</a>
</div>
</div>
</div>
{% if is_paginated %}
{% include 'includes/pagination.html' with page_obj=page_obj %}
{% endif %}
Expand Down Expand Up @@ -81,4 +112,4 @@

</section>
{% endif %}
{% endblock %}
{% endblock %}
21 changes: 21 additions & 0 deletions vulnerabilities/tests/test_api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,27 @@ def test_list_packages(self):
all(vuln_id in response.data["results"]["vulnerabilities"] for vuln_id in package_vulns)
)

def test_filter_packages_by_vulnerability_status(self):
vulnerability = Vulnerability.objects.create(
vulnerability_id="VCID-FILTER", summary="Test vulnerability for is_vulnerable filter"
)
self.package1.affected_by_vulnerabilities.add(vulnerability)
url = reverse("package-v2-list")
response = self.client.get(url, {"is_vulnerable": "true"}, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn("results", response.data)
self.assertIn("packages", response.data["results"])
package_purls = [pkg["purl"] for pkg in response.data["results"]["packages"]]
self.assertIn(self.package1.package_url, package_purls)
self.assertNotIn(self.package2.package_url, package_purls)
response = self.client.get(url, {"is_vulnerable": "false"}, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn("results", response.data)
self.assertIn("packages", response.data["results"])
package_purls = [pkg["purl"] for pkg in response.data["results"]["packages"]]
self.assertNotIn(self.package1.package_url, package_purls)
self.assertIn(self.package2.package_url, package_purls)

def test_filter_packages_by_purl(self):
"""
Test filtering packages by one or more PURLs.
Expand Down
23 changes: 23 additions & 0 deletions vulnerabilities/tests/test_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,29 @@ def test_package_detail_view(self):
package = PackageDetails(kwargs={"purl": "pkg:nginx/[email protected]"}).get_object()
assert package.purl == "pkg:nginx/[email protected]"

def test_package_vulnerability_filter(self):
vulnerability = Vulnerability.objects.create(
vulnerability_id="VCID-TEST", summary="Test Vulnerability for filtering"
)
vulnerable_package = Package.objects.get(package_url="pkg:nginx/[email protected]")
AffectedByPackageRelatedVulnerability.objects.create(
vulnerability=vulnerability, package=vulnerable_package, created_by="test"
)
response = self.client.get("/packages/search?search=nginx&vulnerable_only=true")
self.assertEqual(response.status_code, 200)
self.assertIn(vulnerable_package.purl, str(response.content))
self.assertNotIn("pkg:nginx/[email protected]", str(response.content))

response = self.client.get("/packages/search?search=nginx&vulnerable_only=false")
self.assertEqual(response.status_code, 200)
self.assertNotIn(vulnerable_package.purl, str(response.content))
self.assertIn("pkg:nginx/[email protected]", str(response.content))

response = self.client.get("/packages/search?search=nginx")
self.assertEqual(response.status_code, 200)
self.assertIn(vulnerable_package.purl, str(response.content))
self.assertIn("pkg:nginx/[email protected]", str(response.content))

def test_package_view_with_purl_fragment(self):
qs = PackageSearch().get_queryset(query="[email protected]")
pkgs = list(qs)
Expand Down
18 changes: 16 additions & 2 deletions vulnerabilities/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,27 @@ def get_queryset(self, query=None):
Make a best effort approach to find matching packages either based
on exact purl, partial purl or just name and namespace.
"""
query = query or self.request.GET.get("search") or ""
return (
if query is not None:
queryset = (
self.model.objects.search(query)
.with_vulnerability_counts()
.prefetch_related()
.order_by("package_url")
)
return queryset
query = self.request.GET.get("search") or ""
queryset = (
self.model.objects.search(query)
.with_vulnerability_counts()
.prefetch_related()
.order_by("package_url")
)
vulnerable_only = self.request.GET.get("vulnerable_only", "")
if vulnerable_only in ["true", "false"]:
queryset = queryset.with_is_vulnerable()
queryset = queryset.filter(is_vulnerable=vulnerable_only == "true")

return queryset


class VulnerabilitySearch(ListView):
Expand Down