Skip to content

Option to filter Vulnerable and Non Vulnerable Packages #1760

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
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