From a0f993d8b26c5c011dd87351d0a959151d6652c4 Mon Sep 17 00:00:00 2001 From: ziadhany Date: Tue, 9 Apr 2024 17:14:36 +0200 Subject: [PATCH] Fix Export VEX View Rename VEX model Add VEX Form Fix UI bug and add the model to dataspace Add basic VEX mapping for CycloneDX Automate VEX creation Add the basic Vex Form Add the skeleton view and form for vex Add Product VEX List view and update tab_vex Add the basic for vex model Add the basic skeleton for vex export Signed-off-by: ziadhany --- component_catalog/forms.py | 1 + component_catalog/models.py | 6 + .../component_catalog/package_details.html | 1 + .../component_catalog/package_vex_form.html | 4 + component_catalog/urls.py | 1 + component_catalog/views.py | 6 +- dejacode_toolkit/tests/test_vex.py | 424 ++++++++++++++ dejacode_toolkit/tests/testfiles/vex.json | 96 +++ dejacode_toolkit/vex.py | 248 ++++++++ dejacode_toolkit/vulnerablecode.py | 3 + dje/views.py | 44 ++ product_portfolio/forms.py | 28 + .../migrations/0005_productpackagevex.py | 125 ++++ ...se_productpackagevex_responses_and_more.py | 37 ++ product_portfolio/models.py | 76 +++ .../product_portfolio/product_details.html | 4 + .../product_portfolio/tabs/tab_vex.html | 50 ++ product_portfolio/tests/test_views.py | 18 + product_portfolio/tests/testfiles/vex.json | 546 ++++++++++++++++++ product_portfolio/urls.py | 13 + product_portfolio/views.py | 65 +++ 21 files changed, 1795 insertions(+), 1 deletion(-) create mode 100644 component_catalog/templates/component_catalog/package_vex_form.html create mode 100644 dejacode_toolkit/tests/test_vex.py create mode 100644 dejacode_toolkit/tests/testfiles/vex.json create mode 100644 dejacode_toolkit/vex.py create mode 100644 product_portfolio/migrations/0005_productpackagevex.py create mode 100644 product_portfolio/migrations/0006_rename_response_productpackagevex_responses_and_more.py create mode 100644 product_portfolio/templates/product_portfolio/tabs/tab_vex.html create mode 100644 product_portfolio/tests/testfiles/vex.json diff --git a/component_catalog/forms.py b/component_catalog/forms.py index f5304fc7..51f45699 100644 --- a/component_catalog/forms.py +++ b/component_catalog/forms.py @@ -51,6 +51,7 @@ from product_portfolio.models import Product from product_portfolio.models import ProductComponent from product_portfolio.models import ProductPackage +from product_portfolio.models import ProductPackageVEX class SetKeywordsChoicesFormMixin: diff --git a/component_catalog/models.py b/component_catalog/models.py index 18dda9cb..a083e5f3 100644 --- a/component_catalog/models.py +++ b/component_catalog/models.py @@ -745,6 +745,9 @@ def get_export_spdx_url(self): def get_export_cyclonedx_url(self): return self.get_url("export_cyclonedx") + def get_export_vex_url(self): + return self.get_url("export_vex") + def get_about_files(self): """ Return the list of all AboutCode files from all the Packages @@ -1914,6 +1917,9 @@ def get_export_spdx_url(self): def get_export_cyclonedx_url(self): return self.get_url("export_cyclonedx") + def get_export_vex_url(self): + return self.get_url("export_vex") + @classmethod def get_identifier_fields(cls): """ diff --git a/component_catalog/templates/component_catalog/package_details.html b/component_catalog/templates/component_catalog/package_details.html index dedc3d38..4d73ea38 100644 --- a/component_catalog/templates/component_catalog/package_details.html +++ b/component_catalog/templates/component_catalog/package_details.html @@ -39,6 +39,7 @@ 1.5 1.4 + diff --git a/component_catalog/templates/component_catalog/package_vex_form.html b/component_catalog/templates/component_catalog/package_vex_form.html new file mode 100644 index 00000000..75bca7b1 --- /dev/null +++ b/component_catalog/templates/component_catalog/package_vex_form.html @@ -0,0 +1,4 @@ +{% extends "object_form.html" %} +{% block javascripts %} + {{ block.super }} +{% endblock %} \ No newline at end of file diff --git a/component_catalog/urls.py b/component_catalog/urls.py index ba9298ee..ceeac411 100644 --- a/component_catalog/urls.py +++ b/component_catalog/urls.py @@ -29,6 +29,7 @@ from dje.views import DataspacedDeleteView from dje.views import ExportCycloneDXBOMView from dje.views import ExportSPDXDocumentView +from dje.views import ExportVEXView from dje.views import MultiSendAboutFilesView from dje.views import SendAboutFilesView diff --git a/component_catalog/views.py b/component_catalog/views.py index 5df07c87..e215b1d7 100644 --- a/component_catalog/views.py +++ b/component_catalog/views.py @@ -40,6 +40,7 @@ from django.views.decorators.csrf import csrf_protect from django.views.decorators.http import require_POST from django.views.generic import FormView +from django.views.generic import UpdateView from django.views.generic.edit import BaseFormView from crispy_forms.utils import render_crispy_form @@ -75,6 +76,7 @@ from dejacode_toolkit.scancodeio import ScanCodeIO from dejacode_toolkit.scancodeio import get_package_download_url from dejacode_toolkit.scancodeio import get_scan_results_as_file_url +from dejacode_toolkit.vex import create_auto_vex from dejacode_toolkit.vulnerablecode import VulnerableCode from dje import tasks from dje.client_data import add_client_data @@ -105,6 +107,7 @@ from policy.models import UsagePolicy from product_portfolio.models import ProductComponent from product_portfolio.models import ProductPackage +from product_portfolio.models import ProductPackageVEX License = apps.get_model("license_library", "License") @@ -857,7 +860,6 @@ def get_vulnerabilities_tab_fields(self, vulnerabilities): vulnerability_fields = self.get_vulnerability_fields(vulnerability, dataspace) fields.extend(vulnerability_fields) vulnerabilities_count += 1 - return fields, vulnerabilities_count def get_context_data(self, **kwargs): @@ -1452,6 +1454,8 @@ def get_vulnerabilities_tab_fields(self, vulnerabilities): fields = [] vulnerabilities_count = 0 + create_auto_vex(self.object, vulnerabilities) + for entry in vulnerabilities: unresolved = entry.get("affected_by_vulnerabilities", []) for vulnerability in unresolved: diff --git a/dejacode_toolkit/tests/test_vex.py b/dejacode_toolkit/tests/test_vex.py new file mode 100644 index 00000000..bd93c0de --- /dev/null +++ b/dejacode_toolkit/tests/test_vex.py @@ -0,0 +1,424 @@ +# +# 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/nexB/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + + +import json +from unittest import mock + +from django.contrib.auth import get_user_model +from django.test import TestCase + +from cyclonedx.output.json import CycloneDxJSONEncoder + +from component_catalog.models import Package +from dejacode_toolkit import vex +from dejacode_toolkit.vex import VEXCycloneDX +from dejacode_toolkit.vex import vulnerability_format_vcic_to_cyclonedx +from dje.models import Dataspace +from dje.tests import create_superuser +from dje.tests import create_user +from product_portfolio.models import Product +from product_portfolio.models import ProductPackage +from product_portfolio.models import ProductPackageVEX + +User = get_user_model() + + +class VEXTestCase(TestCase): + def setUp(self): + self.nexb_dataspace = Dataspace.objects.create(name="nexB") + self.nexb_user = User.objects.create_superuser( + "nexb_user", "test@test.com", "t3st", self.nexb_dataspace + ) + self.basic_user = create_user("basic_user", self.nexb_dataspace) + self.product1 = Product.objects.create( + name="Product1 With Space", version="1.0", dataspace=self.nexb_dataspace + ) + self.package1 = Package.objects.create(filename="package1", dataspace=self.nexb_dataspace) + + self.productpacakge1 = ProductPackage.objects.create( + product=self.product1, package=self.package1, dataspace=self.nexb_dataspace + ) + self.vex1 = ProductPackageVEX.objects.create( + dataspace=self.productpacakge1.dataspace, + productpackage=self.productpacakge1, + vulnerability_id="VCID-111c-u9bh-aaac", + responses=["CNF"], + justification="CNP", + detail=( + "Automated dataflow analysis and manual " + "code review indicates that the vulnerable code is not reachable," + " either directly or indirectly." + ), + ) + + def test_create_auto_vex1(self): + vulnerabilities = [ + { + "affected_by_vulnerabilities": [ + { + "url": "http://public.vulnerablecode.io/api/vulnerabilities/121332", + "vulnerability_id": "VCID-111c-u9bh-aaac", + } + ] + }, + { + "affected_by_vulnerabilities": [ + { + "url": "https://public.vulnerablecode.io/api/vulnerabilities/121331", + "vulnerability_id": "VCID-uxf9-7c97-aaaj", + } + ] + }, + ] + assert ProductPackageVEX.objects.count() == 0 + vex.create_auto_vex(self.package1, vulnerabilities) + assert ProductPackageVEX.objects.count() == 2 + + # run create_auto_vex agian and make sure that the databse ignore errors + vex.create_auto_vex(self.package1, vulnerabilities) + assert ProductPackageVEX.objects.count() == 2 + + def test_create_auto_vex2(self): + # duplicated vulnerability + vulnerabilities = [ + { + "affected_by_vulnerabilities": [ + { + "url": "http://public.vulnerablecode.io/api/vulnerabilities/121332", + "vulnerability_id": "VCID-111c-u9bh-aaac", + } + ] + }, + { + "affected_by_vulnerabilities": [ + { + "url": "http://public.vulnerablecode.io/api/vulnerabilities/121332", + "vulnerability_id": "VCID-111c-u9bh-aaac", + } + ] + }, + ] + assert ProductPackageVEX.objects.count() == 0 + vex.create_auto_vex(self.package1, vulnerabilities) + assert ProductPackageVEX.objects.count() == 1 + + def test_get_references_and_rating(self): + references = [ + { + "reference_url": "https://nvd.nist.gov/vuln/detail/CVE-2017-1000136", + "reference_id": "CVE-2017-1000136", + "scores": [ + { + "value": "5.0", + "scoring_system": "cvssv2", + "scoring_elements": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + }, + { + "value": "5.3", + "scoring_system": "cvssv3", + "scoring_elements": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + }, + ], + "url": "https://nvd.nist.gov/vuln/detail/CVE-2017-1000136", + } + ] + ref, rate = vex.get_references_and_rating(references) + assert 1 == 1 + + def test_vulnerability_format_vcic_to_cyclonedx(self): + vcio_vulnerability = { + "url": "http://public.vulnerablecode.io/api/vulnerabilities/121332", + "vulnerability_id": "VCID-111c-u9bh-aaac", + "summary": "Mahara 1.8 before 1.8.6 and 1.9 before 1.9.4 and 1.10 before 1.10.1 and 15.04 before 15.04.0 are vulnerable to old sessions not being invalidated after a password change.", + "aliases": [{"alias": "CVE-2017-1000136"}], + "fixed_packages": [], + "affected_packages": [], + "references": [ + { + "reference_url": "https://bugs.launchpad.net/mahara/+bug/1363873", + "reference_id": "", + "scores": [], + "url": "https://bugs.launchpad.net/mahara/+bug/1363873", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.10.0:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.10.0:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.10.0:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.10:rc1:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.10:rc1:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.10:rc1:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:15.04:rc1:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:15.04:rc1:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:15.04:rc1:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:15.04:rc2:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:15.04:rc2:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:15.04:rc2:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.0:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8.0:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.0:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.1:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8.1:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.1:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.2:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8.2:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.2:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.3:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8.3:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.3:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.4:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8.4:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.4:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.5:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8.5:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.5:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8:rc1:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8:rc1:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8:rc1:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8:rc2:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8:rc2:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8:rc2:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.0:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.9.0:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.0:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.1:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.9.1:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.1:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.2:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.9.2:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.2:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.3:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.9.3:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.3:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9:rc1:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.9:rc1:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9:rc1:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/detail/CVE-2017-1000136", + "reference_id": "CVE-2017-1000136", + "scores": [ + { + "value": "4.3", + "scoring_system": "cvssv2", + "scoring_elements": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + }, + { + "value": "6.5", + "scoring_system": "cvssv3", + "scoring_elements": "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N", + }, + ], + "url": "https://nvd.nist.gov/vuln/detail/CVE-2017-1000136", + }, + ], + "weaknesses": [ + { + "cwe_id": 613, + "name": "Insufficient Session Expiration", + "description": "According to WASC, Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization.", + } + ], + "resource_url": "http://public.vulnerablecode.io/vulnerabilities/VCID-111c-u9bh-aaac", + } + vulnerability = vulnerability_format_vcic_to_cyclonedx(vcio_vulnerability, self.vex1) + + def test_VEXCycloneDX_export(self): + vcio_vulnerability = { + "url": "http://public.vulnerablecode.io/api/vulnerabilities/121332", + "vulnerability_id": "VCID-111c-u9bh-aaac", + "summary": "Mahara 1.8 before 1.8.6 and 1.9 before 1.9.4 and 1.10 before 1.10.1 and 15.04 before 15.04.0 are vulnerable to old sessions not being invalidated after a password change.", + "aliases": [{"alias": "CVE-2017-1000136"}], + "fixed_packages": [], + "affected_packages": [], + "references": [ + { + "reference_url": "https://bugs.launchpad.net/mahara/+bug/1363873", + "reference_id": "", + "scores": [], + "url": "https://bugs.launchpad.net/mahara/+bug/1363873", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.10.0:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.10.0:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.10.0:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.10:rc1:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.10:rc1:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.10:rc1:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:15.04:rc1:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:15.04:rc1:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:15.04:rc1:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:15.04:rc2:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:15.04:rc2:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:15.04:rc2:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.0:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8.0:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.0:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.1:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8.1:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.1:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.2:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8.2:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.2:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.3:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8.3:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.3:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.4:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8.4:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.4:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.5:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8.5:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8.5:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8:rc1:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8:rc1:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8:rc1:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8:rc2:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.8:rc2:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.8:rc2:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.0:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.9.0:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.0:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.1:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.9.1:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.1:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.2:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.9.2:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.2:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.3:*:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.9.3:*:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9.3:*:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9:rc1:*:*:*:*:*:*", + "reference_id": "cpe:2.3:a:mahara:mahara:1.9:rc1:*:*:*:*:*:*", + "scores": [], + "url": "https://nvd.nist.gov/vuln/search/results?adv_search=true&isCpeNameSearch=true&query=cpe:2.3:a:mahara:mahara:1.9:rc1:*:*:*:*:*:*", + }, + { + "reference_url": "https://nvd.nist.gov/vuln/detail/CVE-2017-1000136", + "reference_id": "CVE-2017-1000136", + "scores": [ + { + "value": "4.3", + "scoring_system": "cvssv2", + "scoring_elements": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + }, + { + "value": "6.5", + "scoring_system": "cvssv3", + "scoring_elements": "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N", + }, + ], + "url": "https://nvd.nist.gov/vuln/detail/CVE-2017-1000136", + }, + ], + "weaknesses": [ + { + "cwe_id": 613, + "name": "Insufficient Session Expiration", + "description": "According to WASC, Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization.", + } + ], + "resource_url": "http://public.vulnerablecode.io/vulnerabilities/VCID-111c-u9bh-aaac", + } + print(VEXCycloneDX.export([vcio_vulnerability], [self.vex1])) diff --git a/dejacode_toolkit/tests/testfiles/vex.json b/dejacode_toolkit/tests/testfiles/vex.json new file mode 100644 index 00000000..14c5067b --- /dev/null +++ b/dejacode_toolkit/tests/testfiles/vex.json @@ -0,0 +1,96 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "vulnerabilities": [ + { + "id": "CVE-2020-25649", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-25649" + }, + "references": [ + { + "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-1048302", + "source": { + "name": "SNYK", + "url": "https://security.snyk.io/vuln/SNYK-JAVA-COMFASTERXMLJACKSONCORE-1048302" + } + } + ], + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N&version=3.1" + }, + "score": 7.5, + "severity": "high", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N" + }, + { + "source": { + "name": "SNYK", + "url": "https://security.snyk.io/vuln/SNYK-JAVA-COMFASTERXMLJACKSONCORE-1048302" + }, + "score": 8.2, + "severity": "high", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N" + }, + { + "source": { + "name": "Acme Inc", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N&version=3.1" + }, + "score": 0.0, + "severity": "none", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N" + } + ], + "cwes": [ + 611 + ], + "description": "com.fasterxml.jackson.core:jackson-databind is a library which contains the general-purpose data-binding functionality and tree-model for Jackson Data Processor.\n\nAffected versions of this package are vulnerable to XML External Entity (XXE) Injection. A flaw was found in FasterXML Jackson Databind, where it does not have entity expansion secured properly in the DOMDeserializer class. The highest threat from this vulnerability is data integrity.", + "detail": "XXE Injection is a type of attack against an application that parses XML input. XML is a markup language that defines a set of rules for encoding documents in a format that is both human-readable and machine-readable. By default, many XML processors allow specification of an external entity, a URI that is dereferenced and evaluated during XML processing. When an XML document is being parsed, the parser can make a request and include the content at the specified URI inside of the XML document.\n\nAttacks can include disclosing local files, which may contain sensitive data such as passwords or private user data, using file: schemes or relative paths in the system identifier.", + "recommendation": "Upgrade com.fasterxml.jackson.core:jackson-databind to version 2.6.7.4, 2.9.10.7, 2.10.5.1 or higher.", + "advisories": [ + { + "title": "GitHub Commit", + "url": "https://github.com/FasterXML/jackson-databind/commit/612f971b78c60202e9cd75a299050c8f2d724a59" + }, + { + "title": "GitHub Issue", + "url": "https://github.com/FasterXML/jackson-databind/issues/2589" + }, + { + "title": "RedHat Bugzilla Bug", + "url": "https://bugzilla.redhat.com/show_bug.cgi?id=1887664" + } + ], + "created": "2020-12-03T00:00:00.000Z", + "published": "2020-12-03T00:00:00.000Z", + "updated": "2021-10-26T00:00:00.000Z", + "credits": { + "individuals": [ + { + "name": "Bartosz Baranowski" + } + ] + }, + "analysis": { + "state": "not_affected", + "justification": "code_not_reachable", + "response": ["will_not_fix", "update"], + "detail": "Automated dataflow analysis and manual code review indicates that the vulnerable code is not reachable, either directly or indirectly." + }, + "affects": [ + { + "ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.10.0?type=jar" + } + ] + } + ] +} \ No newline at end of file diff --git a/dejacode_toolkit/vex.py b/dejacode_toolkit/vex.py new file mode 100644 index 00000000..20d2e090 --- /dev/null +++ b/dejacode_toolkit/vex.py @@ -0,0 +1,248 @@ +import json +from dataclasses import dataclass +from typing import List +from typing import Literal +from typing import Optional + +from cyclonedx.model.vulnerability import BomTarget +from cyclonedx.model.vulnerability import BomTargetVersionRange +from cyclonedx.model.vulnerability import ImpactAnalysisAffectedStatus +from cyclonedx.model.vulnerability import ImpactAnalysisJustification +from cyclonedx.model.vulnerability import ImpactAnalysisResponse +from cyclonedx.model.vulnerability import ImpactAnalysisState +from cyclonedx.model.vulnerability import Vulnerability +from cyclonedx.model.vulnerability import VulnerabilityAdvisory +from cyclonedx.model.vulnerability import VulnerabilityAnalysis +from cyclonedx.model.vulnerability import VulnerabilityCredits +from cyclonedx.model.vulnerability import VulnerabilityRating +from cyclonedx.model.vulnerability import VulnerabilityReference +from cyclonedx.model.vulnerability import VulnerabilityScoreSource +from cyclonedx.model.vulnerability import VulnerabilitySeverity +from cyclonedx.model.vulnerability import VulnerabilitySource +from cyclonedx.output.json import CycloneDxJSONEncoder + +from product_portfolio.models import ProductPackage +from product_portfolio.models import ProductPackageVEX + +SCORING_SYSTEMS_CYCLONEDX = { # FIXME + "cvssv3.1": VulnerabilityScoreSource.CVSS_V3_1, + "cvssv3": VulnerabilityScoreSource.CVSS_V3, + "cvssv2": VulnerabilityScoreSource.CVSS_V2, +} + +SEVERITIES_CYCLONEDX = { # FIXME + "critical": VulnerabilitySeverity.CRITICAL, + "important": VulnerabilitySeverity.HIGH, + "moderate": VulnerabilitySeverity.MEDIUM, + "low": VulnerabilitySeverity.LOW, +} + +STATUS_VEX_CYCLONEDX = { + "affected": ImpactAnalysisAffectedStatus.AFFECTED, + "not_affected": ImpactAnalysisAffectedStatus.UNAFFECTED, + "unkown": ImpactAnalysisAffectedStatus.UNKNOWN, + # "Fixed": +} + +STATE_VEX_CYCLONEDX = { + "R": ImpactAnalysisState.RESOLVED, + "RWP": ImpactAnalysisState.RESOLVED_WITH_PEDIGREE, + "E": ImpactAnalysisState.EXPLOITABLE, + "IT": ImpactAnalysisState.IN_TRIAGE, + "FP": ImpactAnalysisState.FALSE_POSITIVE, + "NA": ImpactAnalysisState.NOT_AFFECTED, +} + +JUSTIFICATION_VEX_CYCLONEDX = { + "CNP": ImpactAnalysisJustification.CODE_NOT_PRESENT, + "CNR": ImpactAnalysisJustification.CODE_NOT_REACHABLE, + "PP": ImpactAnalysisJustification.PROTECTED_AT_PERIMITER, + "PR": ImpactAnalysisJustification.PROTECTED_AT_RUNTIME, + "PC": ImpactAnalysisJustification.PROTECTED_BY_COMPILER, + "PMC": ImpactAnalysisJustification.PROTECTED_BY_MITIGATING_CONTROL, + "RC": ImpactAnalysisJustification.REQUIRES_CONFIGURATION, + "RD": ImpactAnalysisJustification.REQUIRES_DEPENDENCY, + "RE": ImpactAnalysisJustification.REQUIRES_ENVIRONMENT, +} + +RESPONSES_VEX_CYCLONEDX = { + "CNF": ImpactAnalysisResponse.CAN_NOT_FIX, + "RB": ImpactAnalysisResponse.ROLLBACK, + "U": ImpactAnalysisResponse.UPDATE, + "WNF": ImpactAnalysisResponse.WILL_NOT_FIX, + "WA": ImpactAnalysisResponse.WORKAROUND_AVAILABLE, +} + + +def create_auto_vex(package, vulnerabilities): + """ + automatically create a VEX for each product package that has a vulnerability + """ + vex_objects = [] + vulnerability_ids = [] + for entry in vulnerabilities: + unresolved = entry.get("affected_by_vulnerabilities", []) + for vulnerability in unresolved: + vulnerability_id = vulnerability.get("vulnerability_id") + if vulnerability_id: + vulnerability_ids.append(vulnerability_id) + + productpackages = ProductPackage.objects.filter(package=package) + for productpackage in productpackages: + for vulnerability_id in vulnerability_ids: + vex_objects.append( + ProductPackageVEX( + dataspace=productpackage.dataspace, + productpackage=productpackage, + vulnerability_id=vulnerability_id, + ) + ) + + ProductPackageVEX.objects.bulk_create(vex_objects, ignore_conflicts=True) + + +def vulnerability_format_vcic_to_cyclonedx(vcio_vulnerability, vex: ProductPackageVEX): + """ + Change the VCIO format of the vulnerability to CycloneDX and add the vex vulnerability + """ + + vulnerability_source = VulnerabilitySource( + url=vcio_vulnerability.get("url"), + ) + + references = vcio_vulnerability.get("references") or [] + vulnerability_reference, vulnerability_ratings = get_references_and_rating(references) + + state = STATE_VEX_CYCLONEDX.get(vex.state) + justification = JUSTIFICATION_VEX_CYCLONEDX.get(vex.justification) + + responses = [] + for vex_res in vex.responses or []: + response = RESPONSES_VEX_CYCLONEDX.get(vex_res) + if response: + responses.append(response) + + vulnerability_analysis = VulnerabilityAnalysis( + state=state, + responses=responses, + justification=justification, + detail=vex.detail, + ) + + # vulnerability_advisory = VulnerabilityAdvisory() # ignore + # vulnerability_credits = VulnerabilityCredits() # ignore + + bom_targets = [] + # versions = BomTargetVersionRange(version= None, version_range= None, status= None) + # status = ImpactAnalysisAffectedStatus.AFFECTED + # bom_target = BomTarget(ref="" ,versions=[] ) + + # property = Property() + # tool = Tool() + + weaknesses = vcio_vulnerability.get("weaknesses") or [] + cwes = get_cwes(weaknesses) + + vulnerability = Vulnerability( + # bom_ref="", + # id="", + source=vulnerability_source, + references=vulnerability_reference, + ratings=vulnerability_ratings, + cwes=cwes, + description=vcio_vulnerability.get("summary") or "", + # detail=detail, + # recommendation="", + # advisories=advisories, + # created=created, + # published=published, + # updated=updated, + # credits=credits, + # tools=tools, + # properties=properties, + # Deprecated Parameters kept for backwards compatibility + analysis=vulnerability_analysis, + # affects_targets=bom_targets, + # source_name=source_name, + # source_url=source_url, + # recommendations=recommendations, + ) + return vulnerability + + +def get_cwes(weaknesses): + """ + >>> get_cwes([{"cwe_id": 613,"name": "..."}]}) + [613] + >>> get_cwes([{"cwe_id": 613,"name": "..."}, {"cwe_id": 79,"name": "..."}]) + [613, 79] + >>> get_cwes([]) + [] + """ + cwes = [] + for weakness in weaknesses: + cwe_id = weakness.get("cwe_id") + if cwe_id: + cwes.append(cwe_id) + return cwes + + +def get_references_and_rating(references): + cyclonedx_references, cyclonedx_rating = [], [] + for ref in references: + source = VulnerabilitySource(url=ref.get("url")) + cyclonedx_references.append( + VulnerabilityReference(id=ref.get("reference_id"), source=source) + ) + + for score in ref.get("scores") or []: + vul_source = VulnerabilitySource(url=ref.get("reference_url")) + + score_val = score.get("score") + vul_severity = SEVERITIES_CYCLONEDX.get(score_val) + + scoring_system = score.get("scoring_system") + vul_method = SCORING_SYSTEMS_CYCLONEDX.get(scoring_system) + + # score_base = score.get("value") + vul_rating = VulnerabilityRating( + source=vul_source, + score=None, + severity=vul_severity, + method=vul_method, + vector=score.get("scoring_elements"), + # score_base=float(score_base) if score_base else None, + ) + cyclonedx_rating.append(vul_rating) + + return cyclonedx_references, cyclonedx_rating + + +@dataclass +class VEXCycloneDX: + """https://github.com/CycloneDX/bom-examples/tree/master/VEX""" + + vulnerabilities: List[Vulnerability] + bomFormat: str = "CycloneDX" + specVersion: str = "1.4" + version: int = 1 + + @classmethod + def export(self, vcio_vulnerabilities, vexs): + self.vulnerabilities = [] + for vcio_vulnerability, vex in zip(vcio_vulnerabilities, vexs): + cyclonedx_vulnerability = vulnerability_format_vcic_to_cyclonedx( + vcio_vulnerability, vex + ) + self.vulnerabilities.append( + json.loads(json.dumps(cyclonedx_vulnerability, cls=CycloneDxJSONEncoder)) + ) + + return json.dumps( + { + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "vulnerabilities": self.vulnerabilities, + } + ) diff --git a/dejacode_toolkit/vulnerablecode.py b/dejacode_toolkit/vulnerablecode.py index 7cefb243..01735e30 100644 --- a/dejacode_toolkit/vulnerablecode.py +++ b/dejacode_toolkit/vulnerablecode.py @@ -5,9 +5,12 @@ # See https://github.com/nexB/dejacode for support or download. # See https://aboutcode.org for more information about AboutCode FOSS projects. # +import datetime from django.core.cache import caches +from cyclonedx.model.vulnerability import Vulnerability + from dejacode_toolkit import BaseService from dejacode_toolkit import logger diff --git a/dje/views.py b/dje/views.py index 8438d206..5a02e195 100644 --- a/dje/views.py +++ b/dje/views.py @@ -75,6 +75,7 @@ from component_catalog.license_expression_dje import get_license_objects from dejacode_toolkit.purldb import PurlDB from dejacode_toolkit.scancodeio import ScanCodeIO +from dejacode_toolkit.vex import VEXCycloneDX from dejacode_toolkit.vulnerablecode import VulnerableCode from dje import outputs from dje.copier import COPY_DEFAULT_EXCLUDE @@ -116,6 +117,7 @@ from dje.utils import has_permission from dje.utils import queryset_to_changelist_href from dje.utils import str_to_id_list +from product_portfolio.models import ProductPackageVEX License = apps.get_model("license_library", "License") Request = apps.get_model("workflow", "Request") @@ -2364,3 +2366,45 @@ def get(self, request, *args, **kwargs): filename=outputs.get_cyclonedx_filename(instance), content_type="application/json", ) + + +class ExportVEXView( + LoginRequiredMixin, + DataspaceScopeMixin, + GetDataspacedObjectMixin, + BaseDetailView, +): + def get(self, request, *args, **kwargs): + product = self.get_object() + filename = safe_filename( + f"dejacode_{product.dataspace.name}_{product._meta.model_name}_vex.json" + ) + vulnerablecode = VulnerableCode(self.request.user) + + if not vulnerablecode.is_configured(): + raise Http404 + + vexs = ProductPackageVEX.objects.filter( + productpackage__product=product, dataspace=product.dataspace + ) + + vulnerabilities = [] + for vex in vexs: + url = f"{vulnerablecode.api_url}vulnerabilities/" + vulnerability = vulnerablecode.get_vulnerabilities( + url, field_name="vulnerability_id", field_value=vex.vulnerability_id + ) + vulnerabilities.append(vulnerability[0]) + + if not vulnerabilities: + return + + vex_json = VEXCycloneDX.export(vulnerabilities, vexs) + response = FileResponse( + vex_json, + filename=filename, + content_type="application/json", + ) + response["Content-Disposition"] = f'attachment; filename="{filename}"' + + return response diff --git a/product_portfolio/forms.py b/product_portfolio/forms.py index 29d3681a..f5507a5d 100644 --- a/product_portfolio/forms.py +++ b/product_portfolio/forms.py @@ -51,6 +51,7 @@ from product_portfolio.models import Product from product_portfolio.models import ProductComponent from product_portfolio.models import ProductPackage +from product_portfolio.models import ProductPackageVEX from product_portfolio.models import ScanCodeProject @@ -944,3 +945,30 @@ def submit(self, product, user): scancodeproject_uuid=scancode_project.uuid, ) ) + + +class PackageVEXForm(DataspacedModelForm): + class Meta: + model = ProductPackageVEX + fields = ["state", "responses", "justification", "detail", "ignored"] + widgets = { + "detail": forms.Textarea(attrs={"rows": 3}), + } + + RESPONSES_VEX_CHOICES = [ + ("CNF", "CAN_NOT_FIX"), + ("RB", "ROLLBACK"), + ("U", "UPDATE"), + ("WNF", "WILL_NOT_FIX"), + ("WA", "WORKAROUND_AVAILABLE"), + ] + + responses = forms.MultipleChoiceField( + label="Response:", + choices=RESPONSES_VEX_CHOICES, + widget=forms.CheckboxSelectMultiple, + required=False, + ) + + helper = FormHelper() + helper.add_input(Submit("submit", "Submit", css_class="btn-success")) diff --git a/product_portfolio/migrations/0005_productpackagevex.py b/product_portfolio/migrations/0005_productpackagevex.py new file mode 100644 index 00000000..271e60cb --- /dev/null +++ b/product_portfolio/migrations/0005_productpackagevex.py @@ -0,0 +1,125 @@ +# Generated by Django 5.0.3 on 2024-04-26 19:57 + +import django.contrib.postgres.fields +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("dje", "0002_initial"), + ("product_portfolio", "0004_alter_scancodeproject_type"), + ] + + operations = [ + migrations.CreateModel( + name="ProductPackageVEX", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, editable=False, verbose_name="UUID" + ), + ), + ( + "vulnerability_id", + models.CharField(help_text="VCID-xxxx-xxxx-xxxx", max_length=50), + ), + ( + "state", + models.CharField( + choices=[ + ("R", "RESOLVED"), + ("RWP", "RESOLVED_WITH_PEDIGREE"), + ("E", "EXPLOITABLE"), + ("IT", "IN_TRIAGE"), + ("FP", "FALSE_POSITIVE"), + ("NA", "NOT_AFFECTED"), + ], + help_text="The rationale of why the impact analysis state was asserted.", + max_length=3, + ), + ), + ( + "response", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + blank=True, + choices=[ + ("CNF", "CAN_NOT_FIX"), + ("RB", "ROLLBACK"), + ("U", "UPDATE"), + ("WNF", "WILL_NOT_FIX"), + ("WA", "WORKAROUND_AVAILABLE"), + ], + max_length=3, + ), + help_text="A response to the vulnerability by the manufacturer, supplier, or project responsible for the affected component or service", + max_length=20, + null=True, + size=None, + ), + ), + ( + "justification", + models.CharField( + choices=[ + ("CNP", "CODE_NOT_PRESENT"), + ("PMC", "PROTECTED_BY_MITIGATING_CONTROL"), + ("RC", "REQUIRES_CONFIGURATION"), + ("PC", "PROTECTED_BY_COMPILER"), + ("RE", "REQUIRES_ENVIRONMENT"), + ("CNR", "CODE_NOT_REACHABLE"), + ("RD", "REQUIRES_DEPENDENCY"), + ("PR", "PROTECTED_AT_RUNTIME"), + ("PP", "PROTECTED_AT_PERIMITER"), + ], + help_text="The rationale of why the impact analysis state was asserted.", + max_length=3, + ), + ), + ( + "detail", + models.CharField(help_text="Additional notes to explain the VEX."), + ), + ( + "ignored", + models.BooleanField( + default=False, help_text="Ignore VEX for this vulnerability" + ), + ), + ( + "dataspace", + models.ForeignKey( + editable=False, + help_text="A Dataspace is an independent, exclusive set of DejaCode data, which can be either nexB master reference data or installation-specific data.", + on_delete=django.db.models.deletion.PROTECT, + to="dje.dataspace", + ), + ), + ( + "productpackage", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="product_portfolio.productpackage", + ), + ), + ], + options={ + "unique_together": { + ("productpackage", "vulnerability_id", "dataspace") + }, + }, + ), + ] diff --git a/product_portfolio/migrations/0006_rename_response_productpackagevex_responses_and_more.py b/product_portfolio/migrations/0006_rename_response_productpackagevex_responses_and_more.py new file mode 100644 index 00000000..d6d56cd5 --- /dev/null +++ b/product_portfolio/migrations/0006_rename_response_productpackagevex_responses_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 5.0.3 on 2024-04-27 07:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("product_portfolio", "0005_productpackagevex"), + ] + + operations = [ + migrations.RenameField( + model_name="productpackagevex", + old_name="response", + new_name="responses", + ), + migrations.AlterField( + model_name="productpackagevex", + name="justification", + field=models.CharField( + choices=[ + ("PMC", "PROTECTED_BY_MITIGATING_CONTROL"), + ("PR", "PROTECTED_AT_RUNTIME"), + ("RE", "REQUIRES_ENVIRONMENT"), + ("RD", "REQUIRES_DEPENDENCY"), + ("PP", "PROTECTED_AT_PERIMITER"), + ("CNP", "CODE_NOT_PRESENT"), + ("PC", "PROTECTED_BY_COMPILER"), + ("CNR", "CODE_NOT_REACHABLE"), + ("RC", "REQUIRES_CONFIGURATION"), + ], + help_text="The rationale of why the impact analysis state was asserted.", + max_length=3, + ), + ), + ] diff --git a/product_portfolio/models.py b/product_portfolio/models.py index e33d83f5..3569fc53 100644 --- a/product_portfolio/models.py +++ b/product_portfolio/models.py @@ -10,6 +10,7 @@ from pathlib import Path from django.conf import settings +from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.db import models from django.utils.functional import cached_property @@ -1247,3 +1248,78 @@ def notify(self, verb, description): recipient=self.created_by, description=description, ) + + +class ProductPackageVEX(DataspacedModel): + """ + Vulnerability Exploitability eXchange for + Single Product, Single Version, Single/* Vulnerability, Single Status + ( every Vulnerability and package should have a VEX ) + """ + + productpackage = models.ForeignKey(ProductPackage, on_delete=models.CASCADE) + vulnerability_id = models.CharField( + help_text="VCID-xxxx-xxxx-xxxx", max_length=50, null=False, blank=False + ) + + STATE_VEX_CHOICES = [ + ("R", "RESOLVED"), + ("RWP", "RESOLVED_WITH_PEDIGREE"), + ("E", "EXPLOITABLE"), + ("IT", "IN_TRIAGE"), + ("FP", "FALSE_POSITIVE"), + ("NA", "NOT_AFFECTED"), + ] + + state = models.CharField( + max_length=3, + help_text="The rationale of why the impact analysis state was asserted.", + choices=STATE_VEX_CHOICES, + ) + + RESPONSES_VEX_CHOICES = [ + ("CNF", "CAN_NOT_FIX"), + ("RB", "ROLLBACK"), + ("U", "UPDATE"), + ("WNF", "WILL_NOT_FIX"), + ("WA", "WORKAROUND_AVAILABLE"), + ] + + responses = ArrayField( + models.CharField( + max_length=3, + choices=RESPONSES_VEX_CHOICES, + blank=True, + ), + help_text="A response to the vulnerability by the manufacturer, " + "supplier, or project responsible for the affected component or service", + null=True, + max_length=20, + ) + + JUSTIFICATION_VEX_CHOICES = { + ("CNP", "CODE_NOT_PRESENT"), + ("CNR", "CODE_NOT_REACHABLE"), + ("PP", "PROTECTED_AT_PERIMITER"), + ("PR", "PROTECTED_AT_RUNTIME"), + ("PC", "PROTECTED_BY_COMPILER"), + ("PMC", "PROTECTED_BY_MITIGATING_CONTROL"), + ("RC", "REQUIRES_CONFIGURATION"), + ("RD", "REQUIRES_DEPENDENCY"), + ("RE", "REQUIRES_ENVIRONMENT"), + } + + justification = models.CharField( + max_length=3, + choices=JUSTIFICATION_VEX_CHOICES, + help_text="The rationale of why the impact analysis state was asserted.", + ) + + detail = models.CharField(help_text="Additional notes to explain the VEX.") + ignored = models.BooleanField(help_text="Ignore VEX for this vulnerability", default=False) + + class Meta: + unique_together = ("productpackage", "vulnerability_id", "dataspace") + + def __str__(self): + return f"{str(self.productpackage)} / {str(self.vulnerability_id)}" diff --git a/product_portfolio/templates/product_portfolio/product_details.html b/product_portfolio/templates/product_portfolio/product_details.html index bf375938..bf6599d1 100644 --- a/product_portfolio/templates/product_portfolio/product_details.html +++ b/product_portfolio/templates/product_portfolio/product_details.html @@ -73,6 +73,10 @@ 1.5 1.4 + + + VEX document + diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_vex.html b/product_portfolio/templates/product_portfolio/tabs/tab_vex.html new file mode 100644 index 00000000..64a568ff --- /dev/null +++ b/product_portfolio/templates/product_portfolio/tabs/tab_vex.html @@ -0,0 +1,50 @@ +{% load i18n %} +{% load as_icon from dje_tags %} +{% load urlize_target_blank from dje_tags %} +{% spaceless %} + + + + + + + + + + + + + + {% for vex in values.vex_items %} + {% cycle 'table-odd' '' as rowcolors silent %} + + + + {% include 'product_portfolio/includes/productrelation_element.html' with relation=vex.productpackage %} + + + + + + + + {% empty %} + + {% endfor %} + +
+ {% trans 'Package' %} + + {% trans 'Vulnerabilities' %} + + {% trans 'Status' %} + + {% trans 'Action' %} + + {% trans 'Impact' %} + + {% trans 'Notes' %} +
+ + {{ vex.vulnerability_id }}{{ vex.get_status_display }}{{ vex.action }}{{ vex.impact }}{{ vex.notes }}
No results.
+{% endspaceless %} \ No newline at end of file diff --git a/product_portfolio/tests/test_views.py b/product_portfolio/tests/test_views.py index 8a77b349..9c4fa507 100644 --- a/product_portfolio/tests/test_views.py +++ b/product_portfolio/tests/test_views.py @@ -2919,3 +2919,21 @@ def test_pull_project_data_from_scancodeio_task_can_start_import(self): "ERROR:dje.tasks:Cannot start import", ] self.assertEqual(expected, cm.output) + + @mock.patch("dejacode_toolkit.vulnerablecode.VulnerableCode.get_vulnerable_purls") + @mock.patch("dejacode_toolkit.vulnerablecode.VulnerableCode.is_configured") + def test_vex_export(self, mock_is_configured, mock_vulnerable_purls): + purl = "pkg:pypi/django@2.1" + mock_is_configured.return_value = True + + self.package1.set_package_url(purl) + self.package1.save() + ProductPackage.objects.create( + product=self.product1, package=self.package1, dataspace=self.dataspace + ) + + self.dataspace.enable_vulnerablecodedb_access = True + self.dataspace.save() + + mock_vulnerable_purls.return_value = [purl] + # assert VEXCycloneDX(..).export(self.product1) == {} diff --git a/product_portfolio/tests/testfiles/vex.json b/product_portfolio/tests/testfiles/vex.json new file mode 100644 index 00000000..397b39d5 --- /dev/null +++ b/product_portfolio/tests/testfiles/vex.json @@ -0,0 +1,546 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata" : { + "timestamp" : "2022-03-03T00:00:00Z", + "component" : { + "name" : "GHI", + "version": "17.4", + "type" : "application", + "bom-ref" : "product-GHI" + } + }, + "vulnerabilities": [ + { + "id": "CVE-2020-11896", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11896" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H&version=3.1" + }, + "score": 10.0, + "severity": "critical", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H" + } + ], + "analysis": { + "state": "in_triage" + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11897", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11897" + }, + "ratings": [ + { + "source": { + "name": "Example Company", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N&version=3.1" + }, + "score": 0.0, + "severity": "none", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N" + } + ], + "analysis": { + "state": "not_affected", + "justification": "code_not_present", + "detail": "IPv6 is not supported and code is not present." + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11898", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11898" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H&version=3.1" + }, + "score": 9.1, + "severity": "critical", + "method": "CVSSv31", + "vector": "AAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H" + } + ], + "analysis": { + "state": "exploitable", + "detail": "We are working to integrate the upstream patches in our code." + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11899", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11899" + }, + "ratings": [ + { + "source": { + "name": "Example Company", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N&version=3.1" + }, + "score": 0.0, + "severity": "none", + "method": "CVSSv31", + "vector": "AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N" + } + ], + "analysis": { + "state": "not_affected", + "justification": "code_not_present", + "detail": "IPv6 is not supported and code is not present." + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11900", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11900" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H&version=3.1" + }, + "score": 8.2, + "severity": "high", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H" + } + ], + "analysis": { + "state": "in_triage" + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11901", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11901" + }, + "ratings": [ + { + "source": { + "name": "Example Company", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N&version=3.1" + }, + "score": 0.0, + "severity": "none", + "method": "CVSSv31", + "vector": "AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N" + } + ], + "analysis": { + "state": "resolved" + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11902", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11902" + }, + "ratings": [ + { + "source": { + "name": "Example Company", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N&version=3.1" + }, + "score": 0.0, + "severity": "none", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N" + } + ], + "analysis": { + "state": "not_affected", + "justification": "code_not_present", + "detail": "IPv6 is not supported and code is not present." + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11903", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11903" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N&version=3.1" + }, + "score": 6.5, + "severity": "medium", + "method": "CVSSv31", + "vector": "AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N" + } + ], + "analysis": { + "state": "in_triage" + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11904", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11904" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L&version=3.1" + }, + "score": 7.3, + "severity": "high", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L" + } + ], + "analysis": { + "state": "in_triage" + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11905", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11905" + }, + "ratings": [ + { + "source": { + "name": "Example Company", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N&version=3.1" + }, + "score": 0.0, + "severity": "none", + "method": "CVSSv31", + "vector": "AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N" + } + ], + "analysis": { + "state": "not_affected", + "justification": "code_not_present", + "detail": "IPv6 is not supported and code is not present." + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11906", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11906" + }, + "ratings": [ + { + "source": { + "name": "Example Company", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N&version=3.1" + }, + "score": 0.0, + "severity": "none", + "method": "CVSSv31", + "vector": "AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N" + } + ], + "analysis": { + "state": "not_affected", + "justification": "code_not_present", + "detail": "This code was re-written. The vulnerable code is not present." + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11907", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11907" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L&version=3.1" + }, + "score": 6.3, + "severity": "medium", + "method": "CVSSv31", + "vector": "AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L" + } + ], + "analysis": { + "state": "exploitable", + "detail": "We are working to integrate the upstream patches in our code." + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11908", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11908" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L&version=3.1" + }, + "score": 4.3, + "severity": "medium", + "method": "CVSSv31", + "vector": "AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" + } + ], + "analysis": { + "state": "in_triage" + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11909", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11909" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N&version=3.1" + }, + "score": 5.3, + "severity": "medium", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N" + } + ], + "analysis": { + "state": "exploitable", + "detail": "We are working to integrate the upstream patches in our code." + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11910", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11910" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N&version=3.1" + }, + "score": 5.3, + "severity": "medium", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N" + } + ], + "analysis": { + "state": "exploitable", + "detail": "We are working to integrate the upstream patches in our code." + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11911", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11911" + }, + "ratings": [ + { + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N&version=3.1" + }, + "score": 5.3, + "severity": "medium", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N" + } + ], + "analysis": { + "state": "exploitable", + "detail": "We are working to integrate the upstream patches in our code." + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11912", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11912" + }, + "ratings": [ + { + "source": { + "name": "Example Company", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N&version=3.1" + }, + "score": 0.0, + "severity": "none", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N" + } + ], + "analysis": { + "state": "resolved" + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11913", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11913" + }, + "ratings": [ + { + "source": { + "name": "Example Company", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N&version=3.1" + }, + "score": 0.0, + "severity": "none", + "method": "CVSSv31", + "vector": "AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N" + } + ], + "analysis": { + "state": "not_affected", + "justification": "code_not_present", + "detail": "IPv6 is not supported and code is not present." + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + }, + { + "id": "CVE-2020-11914", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-11914" + }, + "ratings": [ + { + "source": { + "name": "Example Company", + "url": "https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?vector=AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N&version=3.1" + }, + "score": 0.0, + "severity": "none", + "method": "CVSSv31", + "vector": "AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:N/MI:N/MA:N" + } + ], + "analysis": { + "state": "resolved" + }, + "affects": [ + { + "ref": "product-GHI" + } + ] + } + ] +} \ No newline at end of file diff --git a/product_portfolio/urls.py b/product_portfolio/urls.py index c63227c9..f4e73969 100644 --- a/product_portfolio/urls.py +++ b/product_portfolio/urls.py @@ -13,11 +13,13 @@ from product_portfolio.views import LoadSBOMsView from product_portfolio.views import ManageComponentGridView from product_portfolio.views import ManagePackageGridView +from product_portfolio.views import PackageVEXUpdateView from product_portfolio.views import ProductAddView from product_portfolio.views import ProductDeleteView from product_portfolio.views import ProductDetailsView from product_portfolio.views import ProductExportCycloneDXBOMView from product_portfolio.views import ProductExportSPDXDocumentView +from product_portfolio.views import ProductExportVEXView from product_portfolio.views import ProductListView from product_portfolio.views import ProductSendAboutFilesView from product_portfolio.views import ProductTabCodebaseView @@ -80,6 +82,7 @@ def product_path(path_segment, view): *product_path("about_files", ProductSendAboutFilesView.as_view()), *product_path("export_spdx", ProductExportSPDXDocumentView.as_view()), *product_path("export_cyclonedx", ProductExportCycloneDXBOMView.as_view()), + *product_path("export_vex", ProductExportVEXView.as_view()), *product_path("attribution", AttributionView.as_view()), *product_path("change", ProductUpdateView.as_view()), *product_path("delete", ProductDeleteView.as_view()), @@ -113,4 +116,14 @@ def product_path(path_segment, view): ProductListView.as_view(), name="product_list", ), + path( + "/productpackage///vex_change", + PackageVEXUpdateView.as_view(), + name="package_vex_change", + ), + path( + "", + ProductListView.as_view(), + name="productpackagevex_list", + ), ] diff --git a/product_portfolio/views.py b/product_portfolio/views.py index 2cd95eaf..ebc28645 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -79,6 +79,7 @@ from dje.views import DataspaceScopeMixin from dje.views import ExportCycloneDXBOMView from dje.views import ExportSPDXDocumentView +from dje.views import ExportVEXView from dje.views import GetDataspacedObjectMixin from dje.views import Header from dje.views import LicenseDataForBuilderMixin @@ -101,6 +102,7 @@ from product_portfolio.forms import ImportFromScanForm from product_portfolio.forms import ImportManifestsForm from product_portfolio.forms import LoadSBOMsForm +from product_portfolio.forms import PackageVEXForm from product_portfolio.forms import ProductComponentForm from product_portfolio.forms import ProductComponentInlineForm from product_portfolio.forms import ProductCustomComponentForm @@ -114,6 +116,7 @@ from product_portfolio.models import Product from product_portfolio.models import ProductComponent from product_portfolio.models import ProductPackage +from product_portfolio.models import ProductPackageVEX from product_portfolio.models import ScanCodeProject @@ -281,6 +284,16 @@ class ProductDetailsView( "last_modified_by", ], }, + "vex": { + "fields": [ + "productpackage", + "vulnerability_id", + "status", + "action", + "impact", + "notes", + ], + }, } def get_queryset(self): @@ -515,6 +528,22 @@ def tab_inventory(self): ], } + def tab_vex(self): + productpackage_qs = self.filter_productpackage.qs.order_by("feature", "package") + vex_items = ProductPackageVEX.objects.filter(productpackage__in=productpackage_qs) + label = f'VEX List {vex_items.count()}' + + tab_context = { + "vex_items": vex_items, + } + + return { + "label": format_html(label), + "fields": [ + (None, tab_context, None, "product_portfolio/tabs/tab_vex.html"), + ], + } + @staticmethod def inject_scan_data(scancodeio, feature_grouped, dataspace_uuid): download_urls = [ @@ -1431,6 +1460,10 @@ class ProductExportCycloneDXBOMView(BaseProductView, ExportCycloneDXBOMView): pass +class ProductExportVEXView(BaseProductView, ExportVEXView): + pass + + @login_required def scan_all_packages_view(request, dataspace, name, version=""): user = request.user @@ -1958,3 +1991,35 @@ def scancodeio_project_status_view(request, scancodeproject_uuid): } return TemplateResponse(request, template, context) + + +class PackageVEXUpdateView( + DataspacedUpdateView, +): + model = ProductPackageVEX + form_class = PackageVEXForm + permission_required = "component_catalog.change_package" + template_name = "component_catalog/package_vex_form.html" + slug_url_kwarg = ("vulnerability_id", "productpackage_id") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + opts = self.model._meta + context.update( + { + "verbose_name": opts.verbose_name, + "verbose_name_plural": opts.verbose_name_plural, + "list_url": reverse(f"{opts.app_label}:{opts.model_name}_list"), + } + ) + return context + + def get_success_url(self): + return reverse( + "product_portfolio:package_vex_change", + kwargs={ + "dataspace": self.kwargs["dataspace"], + "productpackage_id": self.kwargs["productpackage_id"], + "vulnerability_id": self.kwargs["vulnerability_id"], + }, + )