Skip to content

Commit

Permalink
Refine the CDX methods implementation and add tests #98
Browse files Browse the repository at this point in the history
Signed-off-by: tdruez <[email protected]>
  • Loading branch information
tdruez committed Dec 2, 2024
1 parent 146c073 commit 88cc98a
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 67 deletions.
19 changes: 13 additions & 6 deletions dje/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,20 @@ def get_cyclonedx_bom(instance, user, include_components=True, include_vex=False

if include_vex:
vulnerability_qs = instance.get_vulnerability_qs(prefetch_related_packages=True)
vulnerabilities = [
vulnerability.as_cyclonedx(
affected_instances=vulnerability.affected_packages.all(),
analyses=vulnerability.vulnerability_analyses.all(),
vulnerabilities = []
for vulnerability in vulnerability_qs:
analysis = None
vulnerability_analyses = vulnerability.vulnerability_analyses.all()
if len(vulnerability_analyses) == 1:
analysis = vulnerability_analyses[0]

vulnerabilities.append(
vulnerability.as_cyclonedx(
affected_instances=vulnerability.affected_packages.all(),
analysis=analysis,
)
)
for vulnerability in vulnerability_qs
]

bom.vulnerabilities = vulnerabilities

return bom
Expand Down
52 changes: 0 additions & 52 deletions product_portfolio/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
from product_portfolio.models import ProductComponent
from product_portfolio.models import ProductPackage
from product_portfolio.models import ScanCodeProject
from vulnerabilities.models import VulnerabilityAnalysis


class NameVersionValidationFormMixin:
Expand Down Expand Up @@ -946,54 +945,3 @@ def submit(self, product, user):
scancodeproject_uuid=scancode_project.uuid,
)
)


class VulnerabilityAnalysisForm(DataspacedModelForm):
responses = forms.MultipleChoiceField(
choices=VulnerabilityAnalysis.Response.choices,
widget=forms.CheckboxSelectMultiple,
required=False,
)

class Meta:
model = VulnerabilityAnalysis
fields = [
"product_package",
"vulnerability",
"state",
"justification",
"responses",
"detail",
]
widgets = {
"product_package": forms.widgets.HiddenInput,
"vulnerability": forms.widgets.HiddenInput,
"detail": forms.Textarea(attrs={"rows": 3}),
}

def __init__(self, user, *args, **kwargs):
super().__init__(user, *args, **kwargs)

responses_model_field = self._meta.model._meta.get_field("responses")
self.fields["responses"].help_text = responses_model_field.help_text

product_package_field = self.fields["product_package"]
perms = ["view_product", "change_product"]
product_package_field.queryset = ProductPackage.objects.product_secured(user, perms=perms)

@property
def helper(self):
helper = FormHelper()
helper.form_method = "post"
helper.form_id = "product-vulnerability-analysis-form"
helper.form_tag = False
helper.modal_title = "Vulnerability analysis"
helper.modal_id = "vulnerability-analysis-modal"
return helper

def clean(self):
main_fields = ["state", "justification", "responses", "detail"]
if not any(self.cleaned_data.get(field_name) for field_name in main_fields):
raise ValidationError(
"At least one of state, justification, responses or detail must be provided."
)
2 changes: 1 addition & 1 deletion product_portfolio/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@
from product_portfolio.forms import ProductPackageInlineForm
from product_portfolio.forms import PullProjectDataForm
from product_portfolio.forms import TableInlineFormSetHelper
from product_portfolio.forms import VulnerabilityAnalysisForm
from product_portfolio.models import RELATION_LICENSE_EXPRESSION_HELP_TEXT
from product_portfolio.models import CodebaseResource
from product_portfolio.models import Product
Expand All @@ -132,6 +131,7 @@
from product_portfolio.models import ProductRelationshipMixin
from product_portfolio.models import ScanCodeProject
from vulnerabilities.filters import ProductVulnerabilityFilterSet
from vulnerabilities.forms import VulnerabilityAnalysisForm
from vulnerabilities.models import AffectedByVulnerabilityMixin
from vulnerabilities.models import Vulnerability
from vulnerabilities.models import VulnerabilityAnalysis
Expand Down
67 changes: 67 additions & 0 deletions vulnerabilities/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# DejaCode is a trademark of nexB Inc.
# SPDX-License-Identifier: AGPL-3.0-only
# See https://github.com/aboutcode-org/dejacode for support or download.
# See https://aboutcode.org for more information about AboutCode FOSS projects.
#

from django import forms
from django.core.exceptions import ValidationError

from crispy_forms.helper import FormHelper

from dje.forms import DataspacedModelForm
from product_portfolio.models import ProductPackage
from vulnerabilities.models import VulnerabilityAnalysis


class VulnerabilityAnalysisForm(DataspacedModelForm):
responses = forms.MultipleChoiceField(
choices=VulnerabilityAnalysis.Response.choices,
widget=forms.CheckboxSelectMultiple,
required=False,
)

class Meta:
model = VulnerabilityAnalysis
fields = [
"product_package",
"vulnerability",
"state",
"justification",
"responses",
"detail",
]
widgets = {
"product_package": forms.widgets.HiddenInput,
"vulnerability": forms.widgets.HiddenInput,
"detail": forms.Textarea(attrs={"rows": 3}),
}

def __init__(self, user, *args, **kwargs):
super().__init__(user, *args, **kwargs)

responses_model_field = self._meta.model._meta.get_field("responses")
self.fields["responses"].help_text = responses_model_field.help_text

product_package_field = self.fields["product_package"]
perms = ["view_product", "change_product"]
product_package_field.queryset = ProductPackage.objects.product_secured(user, perms=perms)

@property
def helper(self):
helper = FormHelper()
helper.form_method = "post"
helper.form_id = "product-vulnerability-analysis-form"
helper.form_tag = False
helper.modal_title = "Vulnerability analysis"
helper.modal_id = "vulnerability-analysis-modal"
return helper

def clean(self):
main_fields = ["state", "justification", "responses", "detail"]
if not any(self.cleaned_data.get(field_name) for field_name in main_fields):
raise ValidationError(
"At least one of state, justification, responses or detail must be provided."
)
7 changes: 3 additions & 4 deletions vulnerabilities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,15 +195,13 @@ def create_from_data(cls, dataspace, data, validate=False, affecting=None):

return instance

def as_cyclonedx(self, affected_instances, analyses=None):
def as_cyclonedx(self, affected_instances, analysis=None):
affects = [
cdx_vulnerability.BomTarget(ref=instance.cyclonedx_bom_ref)
for instance in affected_instances
]

analysis = None
if len(analyses) == 1:
analysis = analyses[0].as_cyclonedx()
analysis = analysis.as_cyclonedx() if analysis else None

source = cdx_vulnerability.VulnerabilitySource(
name="VulnerableCode",
Expand Down Expand Up @@ -512,6 +510,7 @@ def __str__(self):
return f"{self.vulnerability} analysis"

def save(self, *args, **kwargs):
"""Set the product and package fields values from the product_package FK."""
self.product_id = self.product_package.product_id
self.package_id = self.product_package.package_id
super().save(*args, **kwargs)
50 changes: 50 additions & 0 deletions vulnerabilities/tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# DejaCode is a trademark of nexB Inc.
# SPDX-License-Identifier: AGPL-3.0-only
# See https://github.com/aboutcode-org/dejacode for support or download.
# See https://aboutcode.org for more information about AboutCode FOSS projects.
#

from pathlib import Path

from django.test import TestCase

from dje.models import Dataspace
from dje.tests import create_superuser
from product_portfolio.tests import make_product
from product_portfolio.tests import make_product_package
from vulnerabilities.forms import VulnerabilityAnalysisForm
from vulnerabilities.tests import make_vulnerability


class VulnerabilitiesFormsTestCase(TestCase):
data = Path(__file__).parent / "data"

def setUp(self):
self.dataspace = Dataspace.objects.create(name="nexB")
self.super_user = create_superuser("super_user", self.dataspace)

def test_vulnerability_forms_vulnerability_analysis_save(self):
vulnerability1 = make_vulnerability(dataspace=self.dataspace)
product_package1 = make_product_package(make_product(self.dataspace))

data = {
"product_package": product_package1,
"vulnerability": vulnerability1,
}

form = VulnerabilityAnalysisForm(user=self.super_user, data=data)
self.assertFalse(form.is_valid())
msg = "At least one of state, justification, responses or detail must be provided."
self.assertEqual({"__all__": [msg]}, form.errors)

data["detail"] = "Analysis detail"
form = VulnerabilityAnalysisForm(user=self.super_user, data=data)
self.assertTrue(form.is_valid())
analysis = form.save()
self.assertEqual(vulnerability1, analysis.vulnerability)
self.assertEqual(product_package1, analysis.product_package)
self.assertEqual(product_package1.product, analysis.product)
self.assertEqual(product_package1.package, analysis.package)
self.assertEqual(data["detail"], analysis.detail)
55 changes: 54 additions & 1 deletion vulnerabilities/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
from dejacode_toolkit.vulnerablecode import VulnerableCode
from dje.models import Dataspace
from product_portfolio.tests import make_product
from product_portfolio.tests import make_product_package
from vulnerabilities.models import Vulnerability
from vulnerabilities.models import VulnerabilityAnalysis
from vulnerabilities.tests import make_vulnerability


class VulnerabilitiesFetchTestCase(TestCase):
class VulnerabilitiesModelsTestCase(TestCase):
data = Path(__file__).parent / "data"

def setUp(self):
Expand Down Expand Up @@ -204,3 +206,54 @@ def test_vulnerability_model_as_cyclonedx(self):
# expected_location.write_text(results)

self.assertJSONEqual(results, expected_location.read_text())

product1 = make_product(self.dataspace)
product_package1 = make_product_package(product1, package=package1)
analysis = VulnerabilityAnalysis(
product_package=product_package1,
vulnerability=vulnerability1,
state=VulnerabilityAnalysis.State.RESOLVED,
justification=VulnerabilityAnalysis.Justification.CODE_NOT_PRESENT,
responses=[
VulnerabilityAnalysis.Response.CAN_NOT_FIX,
VulnerabilityAnalysis.Response.ROLLBACK,
],
detail="detail",
dataspace=self.dataspace,
)
vulnerability1_as_cdx = vulnerability1.as_cyclonedx(
affected_instances=[package1], analysis=analysis
)
as_dict = json.loads(vulnerability1_as_cdx.as_json())
expected = {
"detail": "detail",
"justification": "code_not_present",
"response": ["can_not_fix", "rollback"],
"state": "resolved",
}
self.assertEqual(expected, as_dict["analysis"])

def test_vulnerability_model_vulnerability_analysis_save(self):
vulnerability1 = make_vulnerability(dataspace=self.dataspace)
product_package1 = make_product_package(make_product(self.dataspace))

analysis = VulnerabilityAnalysis(
product_package=product_package1,
vulnerability=vulnerability1,
dataspace=self.dataspace,
)

msg = "At least one of state, justification, responses or detail must be provided."
with self.assertRaisesMessage(ValueError, msg):
analysis.save()

analysis.state = VulnerabilityAnalysis.State.RESOLVED
analysis.save()

# Refresh from db
analysis = VulnerabilityAnalysis.objects.get(pk=analysis.pk)
self.assertEqual(vulnerability1, analysis.vulnerability)
self.assertEqual(product_package1, analysis.product_package)
self.assertEqual(product_package1.product, analysis.product)
self.assertEqual(product_package1.package, analysis.package)
self.assertEqual(VulnerabilityAnalysis.State.RESOLVED, analysis.state)
3 changes: 0 additions & 3 deletions vulnerabilities/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
# See https://aboutcode.org for more information about AboutCode FOSS projects.
#

from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse

Expand All @@ -17,8 +16,6 @@
from vulnerabilities.models import Vulnerability
from vulnerabilities.tests import make_vulnerability

User = get_user_model()


class VulnerabilityViewsTestCase(TestCase):
def setUp(self):
Expand Down

0 comments on commit 88cc98a

Please sign in to comment.