Skip to content

Commit

Permalink
Add vulnerabilities REST API endpoint #104 (#203)
Browse files Browse the repository at this point in the history
Signed-off-by: tdruez <[email protected]>
Signed-off-by: Philippe Ombredanne <[email protected]>
Co-authored-by: Philippe Ombredanne <[email protected]>
Co-authored-by: Philippe Ombredanne <[email protected]>
  • Loading branch information
3 people authored Dec 9, 2024
1 parent 8312190 commit 9eb7fb7
Show file tree
Hide file tree
Showing 15 changed files with 540 additions and 90 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ Release notes
analysed are listed and can be selected for "analysis propagation".
https://github.com/aboutcode-org/dejacode/issues/105

- Add vulnerabilities REST API endpoint that mimics the content and features of the
vulnerabilities list view.
Add `risk_score` and `affected_by_vulnerabilities` fields in Package endpoint.
Add `vulnerability_analyses` field in Product and ProductPackage endpoints.
Add `is_vulnerable` and `affected_by` filters in Product, Package, and ProductPackage
endpoints.
Add `risk_score` filter in Package endpoint.
https://github.com/aboutcode-org/dejacode/issues/104

### Version 5.2.1

- Fix the models documentation navigation.
Expand Down
36 changes: 36 additions & 0 deletions component_catalog/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

from component_catalog.admin import ComponentAdmin
from component_catalog.admin import PackageAdmin
from component_catalog.filters import IsVulnerableFilter
from component_catalog.fuzzy import FuzzyPackageNameSearch
from component_catalog.license_expression_dje import get_license_objects
from component_catalog.license_expression_dje import normalize_and_validate_expression
Expand Down Expand Up @@ -54,6 +55,9 @@
from dje.views import SendAboutFilesMixin
from license_library.models import License
from organization.api import OwnerEmbeddedSerializer
from vulnerabilities.api import VulnerabilitySerializer
from vulnerabilities.filters import RISK_SCORE_RANGES
from vulnerabilities.filters import ScoreRangeFilter


class LicenseSummaryMixin:
Expand Down Expand Up @@ -426,6 +430,13 @@ class ComponentFilterSet(DataspacedAPIFilterSet):
name_version = NameVersionFilter(
label="Name:Version",
)
is_vulnerable = IsVulnerableFilter(
field_name="affected_by_vulnerabilities",
)
affected_by = django_filters.CharFilter(
field_name="affected_by_vulnerabilities__vulnerability_id",
label="Affected by (vulnerability_id)",
)

class Meta:
model = Component
Expand All @@ -450,6 +461,8 @@ class Meta:
"last_modified_date",
"name_version",
"keywords",
"is_vulnerable",
"affected_by",
)


Expand Down Expand Up @@ -610,6 +623,15 @@ class PackageSerializer(
required=False,
allow_null=True,
)
affected_by_vulnerabilities = VulnerabilitySerializer(
read_only=True,
many=True,
fields=[
"vulnerability_id",
"api_url",
"uuid",
],
)

class Meta:
model = Package
Expand Down Expand Up @@ -669,6 +691,8 @@ class Meta:
"created_date",
"last_modified_date",
"collect_data",
"risk_score",
"affected_by_vulnerabilities",
)
extra_kwargs = {
"api_url": {
Expand Down Expand Up @@ -777,6 +801,14 @@ class PackageAPIFilterSet(DataspacedAPIFilterSet):
last_modified_date = LastModifiedDateFilter()
fuzzy = FuzzyPackageNameSearch(widget=HiddenInput)
purl = PackageURLFilter(label="Package URL")
is_vulnerable = IsVulnerableFilter(
field_name="affected_by_vulnerabilities",
)
affected_by = django_filters.CharFilter(
field_name="affected_by_vulnerabilities__vulnerability_id",
label="Affected by (vulnerability_id)",
)
risk_score = ScoreRangeFilter(score_ranges=RISK_SCORE_RANGES)

class Meta:
model = Package
Expand All @@ -801,6 +833,9 @@ class Meta:
"last_modified_date",
"fuzzy",
"purl",
"is_vulnerable",
"affected_by",
"risk_score",
)


Expand Down Expand Up @@ -877,6 +912,7 @@ def get_queryset(self):
.prefetch_related(
"component_set__owner",
"licenses__category",
"affected_by_vulnerabilities",
external_references_prefetch,
)
)
Expand Down
40 changes: 40 additions & 0 deletions component_catalog/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from license_library.models import LicenseChoice
from organization.models import Owner
from policy.models import UsagePolicy
from vulnerabilities.tests import make_vulnerability


@override_settings(
Expand Down Expand Up @@ -1034,6 +1035,14 @@ def test_api_package_list_endpoint_filters(self):
self.assertContains(response, self.package1_detail_url)
self.assertNotContains(response, self.package2_detail_url)

self.package1.risk_score = 9.0
self.package1.save()
data = {"risk_score": "critical"}
response = self.client.get(self.package_list_url, data)
self.assertEqual(1, response.data["count"])
self.assertContains(response, self.package1_detail_url)
self.assertNotContains(response, self.package2_detail_url)

def test_api_package_list_endpoint_multiple_char_filters(self):
self.client.login(username="super_user", password="secret")
filters = "?md5={}&md5={}".format(self.package1.md5, self.package2.md5)
Expand Down Expand Up @@ -1325,6 +1334,37 @@ def test_api_package_endpoint_update_put(self):
self.assertEqual(self.base_user, self.package1.created_by)
self.assertEqual(self.super_user, self.package1.last_modified_by)

def test_api_package_endpoint_vulnerabilities_features(self):
self.client.login(username="super_user", password="secret")
vulnerability1 = make_vulnerability(self.dataspace, affecting=self.package1)
vulnerability2 = make_vulnerability(self.dataspace)
self.package1.update(risk_score=9.0)

data = {"is_vulnerable": "yes"}
response = self.client.get(self.package_list_url, data)
self.assertEqual(1, response.data["count"])
self.assertContains(response, self.package1_detail_url)
self.assertNotContains(response, self.package2_detail_url)

results = response.data["results"]
self.assertEqual("9.0", results[0]["risk_score"])
self.assertEqual(
vulnerability1.vulnerability_id,
results[0]["affected_by_vulnerabilities"][0]["vulnerability_id"],
)

data = {"affected_by": vulnerability1.vulnerability_id}
response = self.client.get(self.package_list_url, data)
self.assertEqual(1, response.data["count"])
self.assertContains(response, self.package1_detail_url)
self.assertNotContains(response, self.package2_detail_url)

data = {"affected_by": vulnerability2.vulnerability_id}
response = self.client.get(self.package_list_url, data)
self.assertEqual(0, response.data["count"])
self.assertNotContains(response, self.package1_detail_url)
self.assertNotContains(response, self.package2_detail_url)

def test_api_package_license_choices_fields(self):
self.client.login(username="super_user", password="secret")

Expand Down
2 changes: 2 additions & 0 deletions dejacode/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from product_portfolio.api import ProductPackageViewSet
from product_portfolio.api import ProductViewSet
from reporting.api import ReportViewSet
from vulnerabilities.api import VulnerabilityViewSet
from workflow.api import RequestTemplateViewSet
from workflow.api import RequestViewSet

Expand All @@ -78,6 +79,7 @@
api_router.register("reports", ReportViewSet)
api_router.register("external_references", ExternalReferenceViewSet)
api_router.register("usage_policies", UsagePolicyViewSet)
api_router.register("vulnerabilities", VulnerabilityViewSet)


urlpatterns = [
Expand Down
27 changes: 26 additions & 1 deletion dje/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,32 @@ def get_permissions(self):
return permission_classes + extra_permission


class DataspacedSerializer(serializers.HyperlinkedModelSerializer):
class DynamicFieldsSerializerMixin:
"""
A Serializer mixin that takes an additional `fields` or `exclude_fields`
arguments to customize the field selection.
Inspired by https://www.django-rest-framework.org/api-guide/serializers/#example
"""

def __init__(self, *args, **kwargs):
fields = kwargs.pop("fields", [])
exclude_fields = kwargs.pop("exclude_fields", [])

super().__init__(*args, **kwargs)

if fields:
self.fields = {
field_name: field
for field_name, field in self.fields.items()
if field_name in fields
}

for field_name in exclude_fields:
self.fields.pop(field_name)


class DataspacedSerializer(DynamicFieldsSerializerMixin, serializers.HyperlinkedModelSerializer):
def __init__(self, *args, **kwargs):
"""
Add the `dataspace` attribute from the request User Dataspace.
Expand Down
18 changes: 8 additions & 10 deletions dje/tests/test_outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
from dje.tests import create_user
from product_portfolio.models import Product
from product_portfolio.tests import make_product_package
from vulnerabilities.models import VulnerabilityAnalysis
from vulnerabilities.tests import make_vulnerability
from vulnerabilities.tests import make_vulnerability_analysis


class OutputsTestCase(TestCase):
Expand Down Expand Up @@ -117,21 +117,19 @@ def test_outputs_get_cyclonedx_bom_include_vex(self):
self.assertEqual(vulnerability1.vulnerability_id, bom.vulnerabilities[0].id)
self.assertIsNone(bom.vulnerabilities[0].analysis)

VulnerabilityAnalysis.objects.create(
product_package=product_package1,
vulnerability=vulnerability1,
state=VulnerabilityAnalysis.State.RESOLVED,
justification=VulnerabilityAnalysis.Justification.CODE_NOT_PRESENT,
detail="detail",
dataspace=self.dataspace,
)
analysis1 = make_vulnerability_analysis(product_package1, vulnerability1)
bom = outputs.get_cyclonedx_bom(
instance=self.product1,
user=self.super_user,
include_vex=True,
)
analysis = bom.vulnerabilities[0].analysis
expected = {"detail": "detail", "justification": "code_not_present", "state": "resolved"}
expected = {
"detail": analysis1.detail,
"justification": str(analysis1.justification),
"response": [str(response) for response in analysis1.responses],
"state": str(analysis1.state),
}
self.assertEqual(expected, json.loads(analysis.as_json()))

def test_outputs_get_cyclonedx_bom_json(self):
Expand Down
Loading

0 comments on commit 9eb7fb7

Please sign in to comment.