Skip to content
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

Add fixed packages in packages endpoint #784

Merged
merged 6 commits into from
Jul 4, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 63 additions & 6 deletions vulnerabilities/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from urllib.parse import unquote

from django.db.models import Prefetch
from django_filters import rest_framework as filters
from packageurl import PackageURL
from rest_framework import serializers
Expand Down Expand Up @@ -50,7 +51,7 @@ class Meta:
fields = ["url", "purl"]


class MinimalVulnerabilitySerializer(serializers.HyperlinkedModelSerializer):
class VulnSerializerRefsAndSummary(serializers.HyperlinkedModelSerializer):
"""
Used for nesting inside package focused APIs.
"""
Expand All @@ -62,6 +63,31 @@ class Meta:
fields = ["url", "vulnerability_id", "summary", "references"]


class MinimalVulnerabilitySerializer(serializers.HyperlinkedModelSerializer):
"""
Used for nesting inside package focused APIs.
"""

class Meta:
model = Vulnerability
fields = ["url", "vulnerability_id"]


class PackageSerializerFixedVulns(serializers.HyperlinkedModelSerializer):
"""
Used for nesting inside vulnerability focused APIs.
"""

purl = serializers.CharField(source="package_url")
fixing_vulnerabilities = MinimalVulnerabilitySerializer(
many=True, source="resolved_to", read_only=True
)

class Meta:
model = Package
fields = ["url", "purl", "fixing_vulnerabilities"]


class AliasSerializer(serializers.HyperlinkedModelSerializer):
"""
Used for nesting inside package focused APIs.
Expand All @@ -74,7 +100,9 @@ class Meta:

class VulnerabilitySerializer(serializers.HyperlinkedModelSerializer):

fixed_packages = MinimalPackageSerializer(many=True, source="resolved_to", read_only=True)
fixed_packages = MinimalPackageSerializer(
many=True, source="filtered_fixed_packages", read_only=True
)
affected_packages = MinimalPackageSerializer(many=True, source="vulnerable_to", read_only=True)

references = VulnerabilityReferenceSerializer(many=True, source="vulnerabilityreference_set")
Expand All @@ -100,12 +128,13 @@ def to_representation(self, instance):
return data

purl = serializers.CharField(source="package_url")
affected_by_vulnerabilities = MinimalVulnerabilitySerializer(
affected_by_vulnerabilities = VulnSerializerRefsAndSummary(
many=True, source="vulnerable_to", read_only=True
)
fixing_vulnerabilities = MinimalVulnerabilitySerializer(
fixing_vulnerabilities = VulnSerializerRefsAndSummary(
many=True, source="resolved_to", read_only=True
)
fixed_packages = PackageSerializerFixedVulns(many=True, read_only=True)

class Meta:
model = Package
Expand All @@ -119,6 +148,7 @@ class Meta:
"qualifiers",
"subpath",
"affected_by_vulnerabilities",
"fixed_packages",
"fixing_vulnerabilities",
]

Expand All @@ -128,7 +158,7 @@ class PackageFilterSet(filters.FilterSet):

class Meta:
model = Package
fields = ["name", "type", "version", "subpath", "purl"]
fields = ["name", "type", "version", "subpath", "purl", "packagerelatedvulnerability__fix"]

def filter_purl(self, queryset, name, value):
purl = unquote(value)
Expand Down Expand Up @@ -193,7 +223,34 @@ class Meta:


class VulnerabilityViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Vulnerability.objects.all()
def get_fixed_packages_qs(self):
"""
Filter the packages that fixes a vulnerability
on fields like name, namespace and type.
"""
package_filter_data = {"packagerelatedvulnerability__fix": True}

query_params = self.request.query_params
for field_name in ["name", "namespace", "type"]:
value = query_params.get(field_name)
if value:
package_filter_data[field_name] = value

return PackageFilterSet(package_filter_data).qs

def get_queryset(self):
"""
Assign filtered packages queryset from `get_fixed_packages_qs`
to a custom attribute `filtered_fixed_packages`
"""
return Vulnerability.objects.prefetch_related(
Prefetch(
"packages",
queryset=self.get_fixed_packages_qs(),
to_attr="filtered_fixed_packages",
)
)

serializer_class = VulnerabilitySerializer
paginate_by = 50
filter_backends = (filters.DjangoFilterBackend,)
Expand Down
14 changes: 14 additions & 0 deletions vulnerabilities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,20 @@ def resolved_to(self):
"""
return self.vulnerabilities.filter(packagerelatedvulnerability__fix=True)

@property
def fixed_packages(self):
"""
Returns vulnerabilities which are affecting this package.
"""
return Package.objects.filter(
name=self.name,
namespace=self.namespace,
type=self.type,
qualifiers=self.qualifiers,
subpath=self.subpath,
packagerelatedvulnerability__fix=True,
).distinct()

def set_package_url(self, package_url):
"""
Set each field values to the values of the provided `package_url` string
Expand Down
129 changes: 120 additions & 9 deletions vulnerabilities/tests/test_fix_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from vulnerabilities.models import Alias
from vulnerabilities.models import Package
from vulnerabilities.models import PackageRelatedVulnerability
from vulnerabilities.models import Vulnerability
from vulnerabilities.models import VulnerabilityReference
from vulnerabilities.models import VulnerabilityRelatedReference
Expand All @@ -25,6 +26,12 @@ def setUp(self):
summary=str(i),
)
self.vulnerability = Vulnerability.objects.create(summary="test")
self.pkg1 = Package.objects.create(name="flask", type="pypi", version="0.1.2")
self.pkg2 = Package.objects.create(name="flask", type="debian", version="0.1.2")
for pkg in [self.pkg1, self.pkg2]:
PackageRelatedVulnerability.objects.create(
package=pkg, vulnerability=self.vulnerability, fix=True
)

def test_api_status(self):
response = self.client.get("/api/vulnerabilities/", format="json")
Expand All @@ -43,33 +50,76 @@ def test_api_with_single_vulnerability(self):
"vulnerability_id": f"VULCOID-{int_to_base36(self.vulnerability.id).upper()}",
"summary": "test",
"aliases": [],
"fixed_packages": [],
"fixed_packages": [
{
"url": f"http://testserver/api/packages/{self.pkg1.id}",
"purl": "pkg:pypi/[email protected]",
},
{
"url": f"http://testserver/api/packages/{self.pkg2.id}",
"purl": "pkg:debian/[email protected]",
},
],
"affected_packages": [],
"references": [],
}

def test_api_with_single_vulnerability_with_filters(self):
response = self.client.get(
f"/api/vulnerabilities/{self.vulnerability.id}?type=pypi", format="json"
).data
assert response == {
"url": f"http://testserver/api/vulnerabilities/{self.vulnerability.id}",
"vulnerability_id": f"VULCOID-{int_to_base36(self.vulnerability.id).upper()}",
"summary": "test",
"aliases": [],
"fixed_packages": [
{
"url": f"http://testserver/api/packages/{self.pkg1.id}",
"purl": "pkg:pypi/[email protected]",
},
],
"affected_packages": [],
"references": [],
}


class APITestCasePackage(TestCase):
def setUp(self):
vuln = Vulnerability.objects.create(
summary="test-vuln",
)
self.vuln = vuln
for i in range(0, 10):
query_kwargs = dict(
type="generic",
namespace="nginx",
name=f"test-{i}",
name="test",
version=str(i),
qualifiers={},
subpath="",
)
Package.objects.create(**query_kwargs)
vuln_package = Package.objects.create(**query_kwargs)
PackageRelatedVulnerability.objects.create(
package=vuln_package,
vulnerability=vuln,
fix=False,
)
self.vuln_package = vuln_package
query_kwargs = dict(
type="generic",
namespace="nginx",
name="test-vulnDB",
version="1.0",
name="test",
version="11",
qualifiers={},
subpath="",
)
self.package = Package.objects.create(**query_kwargs)
PackageRelatedVulnerability.objects.create(
package=self.package,
vulnerability=vuln,
fix=True,
)

def test_api_status(self):
response = self.client.get("/api/packages/", format="json")
Expand All @@ -79,19 +129,80 @@ def test_api_response(self):
response = self.client.get("/api/packages/", format="json").data
self.assertEqual(response["count"], 11)

def test_api_with_single_vulnerability(self):
def test_api_with_single_vulnerability_and_fixed_package(self):
response = self.client.get(f"/api/packages/{self.package.id}", format="json").data
assert response == {
"url": f"http://testserver/api/packages/{self.package.id}",
"purl": "pkg:generic/nginx/test[email protected]",
"purl": "pkg:generic/nginx/test@11",
"type": "generic",
"namespace": "nginx",
"name": "test-vulnDB",
"version": "1.0",
"name": "test",
"version": "11",
"unresolved_vulnerabilities": [],
"qualifiers": {},
"subpath": "",
"fixed_packages": [
{
"url": f"http://testserver/api/packages/{self.package.id}",
"purl": "pkg:generic/nginx/test@11",
"fixing_vulnerabilities": [
{
"url": f"http://testserver/api/vulnerabilities/{self.vuln.id}",
"vulnerability_id": f"VULCOID-{int_to_base36(self.vuln.id).upper()}",
}
],
}
],
"affected_by_vulnerabilities": [],
"fixing_vulnerabilities": [
{
"url": f"http://testserver/api/vulnerabilities/{self.vuln.id}",
"vulnerability_id": f"VULCOID-{int_to_base36(self.vuln.id).upper()}",
"summary": "test-vuln",
"references": [],
}
],
}

def test_api_with_single_vulnerability_and_vulnerable_package(self):
response = self.client.get(f"/api/packages/{self.vuln_package.id}", format="json").data
assert response == {
"url": f"http://testserver/api/packages/{self.vuln_package.id}",
"purl": "pkg:generic/nginx/test@9",
"type": "generic",
"namespace": "nginx",
"name": "test",
"version": "9",
"unresolved_vulnerabilities": [
{
"url": f"http://testserver/api/vulnerabilities/{self.vuln.id}",
"vulnerability_id": f"VULCOID-{int_to_base36(self.vuln.id).upper()}",
"summary": "test-vuln",
"references": [],
}
],
"qualifiers": {},
"subpath": "",
"fixed_packages": [
{
"url": f"http://testserver/api/packages/{self.package.id}",
"purl": "pkg:generic/nginx/test@11",
"fixing_vulnerabilities": [
{
"url": f"http://testserver/api/vulnerabilities/{self.vuln.id}",
"vulnerability_id": f"VULCOID-{int_to_base36(self.vuln.id).upper()}",
}
],
}
],
"affected_by_vulnerabilities": [
{
"url": f"http://testserver/api/vulnerabilities/{self.vuln.id}",
"vulnerability_id": f"VULCOID-{int_to_base36(self.vuln.id).upper()}",
"summary": "test-vuln",
"references": [],
}
],
"fixing_vulnerabilities": [],
}

Expand Down