diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 38ad28c5a..7af3cebab 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,14 @@ v32.6.0 (unreleased) creation REST API. https://github.com/nexB/scancode.io/issues/828 +- Update the ``fetch_vulnerabilities`` pipe to make the API requests by batch of purls. + https://github.com/nexB/scancode.io/issues/835 + +- Add vulnerability support for discovered dependencies. + The dependency data is loaded using the ``find_vulnerabilities`` pipeline backed by + a VulnerableCode database. + https://github.com/nexB/scancode.io/issues/835 + v32.5.0 (2023-08-02) -------------------- diff --git a/scancodeio/context_processors.py b/scancodeio/context_processors.py index efebfa253..842a9482f 100644 --- a/scancodeio/context_processors.py +++ b/scancodeio/context_processors.py @@ -23,10 +23,12 @@ from scancode_config import __version__ as scancode_toolkit_version from scancodeio import __version__ as scancodeio_version +from scancodeio import settings def versions(request): return { "SCANCODEIO_VERSION": scancodeio_version.lstrip("v"), "SCANCODE_TOOLKIT_VERSION": scancode_toolkit_version, + "VULNERABLECODE_URL": settings.VULNERABLECODE_URL, } diff --git a/scanpipe/api/serializers.py b/scanpipe/api/serializers.py index 19f742606..a3182f9e9 100644 --- a/scanpipe/api/serializers.py +++ b/scanpipe/api/serializers.py @@ -363,6 +363,7 @@ class Meta: "datafile_path", "datasource_id", "package_type", + "affected_by_vulnerabilities", ] diff --git a/scanpipe/filters.py b/scanpipe/filters.py index e24dfc355..411450e4a 100644 --- a/scanpipe/filters.py +++ b/scanpipe/filters.py @@ -563,6 +563,7 @@ class DependencyFilterSet(FilterSetUtilsMixin, django_filters.FilterSet): "is_optional", "is_resolved", "datasource_id", + "is_vulnerable", ] search = django_filters.CharFilter( @@ -589,6 +590,7 @@ class DependencyFilterSet(FilterSetUtilsMixin, django_filters.FilterSet): is_runtime = StrictBooleanFilter() is_optional = StrictBooleanFilter() is_resolved = StrictBooleanFilter() + is_vulnerable = IsVulnerable(field_name="affected_by_vulnerabilities") class Meta: model = DiscoveredDependency @@ -607,6 +609,7 @@ class Meta: "is_optional", "is_resolved", "datasource_id", + "is_vulnerable", ] diff --git a/scanpipe/migrations/0040_discovereddependency_affected_by_vulnerabilities.py b/scanpipe/migrations/0040_discovereddependency_affected_by_vulnerabilities.py new file mode 100644 index 000000000..70ddb38eb --- /dev/null +++ b/scanpipe/migrations/0040_discovereddependency_affected_by_vulnerabilities.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.3 on 2023-08-02 10:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("scanpipe", "0039_discoveredpackage_compliance_alert_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="discovereddependency", + name="affected_by_vulnerabilities", + field=models.JSONField(blank=True, default=list), + ), + ] diff --git a/scanpipe/models.py b/scanpipe/models.py index 3bfc6d4b6..9c0881204 100644 --- a/scanpipe/models.py +++ b/scanpipe/models.py @@ -1025,6 +1025,11 @@ def vulnerable_package_count(self): """Return the number of vulnerable packages related to this project.""" return self.discoveredpackages.vulnerable().count() + @cached_property + def vulnerable_dependency_count(self): + """Return the number of vulnerable dependencies related to this project.""" + return self.discovereddependencies.vulnerable().count() + @cached_property def dependency_count(self): """Return the number of dependencies related to this project.""" @@ -2245,11 +2250,31 @@ def __str__(self): return f"{self.from_resource.pk} > {self.to_resource.pk} using {self.map_type}" -class DiscoveredPackageQuerySet(PackageURLQuerySetMixin, ProjectRelatedQuerySet): +class VulnerabilityMixin(models.Model): + """Add the vulnerability related fields and methods.""" + + affected_by_vulnerabilities = models.JSONField(blank=True, default=list) + + @property + def is_vulnerable(self): + """Returns True if this instance is affected by vulnerabilities.""" + return bool(self.affected_by_vulnerabilities) + + class Meta: + abstract = True + + +class VulnerabilityQuerySetMixin: def vulnerable(self): return self.filter(~Q(affected_by_vulnerabilities__in=EMPTY_VALUES)) +class DiscoveredPackageQuerySet( + VulnerabilityQuerySetMixin, PackageURLQuerySetMixin, ProjectRelatedQuerySet +): + pass + + class AbstractPackage(models.Model): """These fields should be kept in line with `packagedcode.models.PackageData`.""" @@ -2446,20 +2471,6 @@ class Meta: abstract = True -class VulnerabilityMixin(models.Model): - """Add the vulnerability related fields and methods.""" - - affected_by_vulnerabilities = models.JSONField(blank=True, default=list) - - @property - def is_vulnerable(self): - """Returns True if this instance is affected by vulnerabilities.""" - return bool(self.affected_by_vulnerabilities) - - class Meta: - abstract = True - - class DiscoveredPackage( ProjectRelatedModel, ExtraDataFieldMixin, @@ -2746,7 +2757,9 @@ def as_cyclonedx(self): ) -class DiscoveredDependencyQuerySet(PackageURLQuerySetMixin, ProjectRelatedQuerySet): +class DiscoveredDependencyQuerySet( + PackageURLQuerySetMixin, VulnerabilityQuerySetMixin, ProjectRelatedQuerySet +): def prefetch_for_serializer(self): """ Optimized prefetching for a QuerySet to be consumed by the @@ -2767,6 +2780,7 @@ class DiscoveredDependency( ProjectRelatedModel, SaveProjectErrorMixin, UpdateFromDataMixin, + VulnerabilityMixin, PackageURLMixin, ): """ diff --git a/scanpipe/pipelines/find_vulnerabilities.py b/scanpipe/pipelines/find_vulnerabilities.py index c2d165883..81e6ad701 100644 --- a/scanpipe/pipelines/find_vulnerabilities.py +++ b/scanpipe/pipelines/find_vulnerabilities.py @@ -26,16 +26,17 @@ class FindVulnerabilities(Pipeline): """ - Find vulnerabilities for discovered packages in the VulnerableCode database. + Find vulnerabilities for packages and dependencies in the VulnerableCode database. - Vulnerability data is stored on each package instance. + Vulnerability data is stored on each package and dependency instance. """ @classmethod def steps(cls): return ( cls.check_vulnerablecode_service_availability, - cls.lookup_vulnerabilities, + cls.lookup_packages_vulnerabilities, + cls.lookup_dependencies_vulnerabilities, ) def check_vulnerablecode_service_availability(self): @@ -46,7 +47,12 @@ def check_vulnerablecode_service_availability(self): if not vulnerablecode.is_available(): raise Exception("VulnerableCode is not available.") - def lookup_vulnerabilities(self): + def lookup_packages_vulnerabilities(self): """Check for vulnerabilities for each of the project's discovered package.""" packages = self.project.discoveredpackages.all() - vulnerablecode.fetch_vulnerabilities(packages) + vulnerablecode.fetch_vulnerabilities(packages, logger=self.log) + + def lookup_dependencies_vulnerabilities(self): + """Check for vulnerabilities for each of the project's discovered dependency.""" + dependencies = self.project.discovereddependencies.filter(is_resolved=True) + vulnerablecode.fetch_vulnerabilities(dependencies, logger=self.log) diff --git a/scanpipe/pipes/vulnerablecode.py b/scanpipe/pipes/vulnerablecode.py index 966bfd15a..0200d6d37 100644 --- a/scanpipe/pipes/vulnerablecode.py +++ b/scanpipe/pipes/vulnerablecode.py @@ -72,11 +72,22 @@ def is_available(): return response.status_code == requests.codes.ok -def get_purls(packages): +def chunked(iterable, chunk_size): """ - Return the PURLs for the given list of `packages`. - Do not include qualifiers nor subpath when `base` is provided. + Break an `iterable` into lists of `chunk_size` length. + + >>> list(chunked([1, 2, 3, 4, 5], 2)) + [[1, 2], [3, 4], [5]] + >>> list(chunked([1, 2, 3, 4, 5], 3)) + [[1, 2, 3], [4, 5]] """ + for index in range(0, len(iterable), chunk_size): + end = index + chunk_size + yield iterable[index:end] + + +def get_purls(packages): + """Return the PURLs for the given list of `packages`.""" return [package_url for package in packages if (package_url := package.package_url)] @@ -168,6 +179,7 @@ def bulk_search_by_purl( data = { "purls": purls, + "vulnerabilities_only": True, } logger.debug(f"VulnerableCode: url={url} purls_count={len(purls)}") @@ -190,32 +202,33 @@ def bulk_search_by_cpes( return request_post(url, data, timeout) -def get_unique_vulnerabilities(packages_data): +def fetch_vulnerabilities(packages, chunk_size=1000, logger=logger.info): """ - Return the unique instance of vulnerabilities for the provided ``packages_data``. - - Note this should be implemented on the VulnerableCode side, see: - https://github.com/nexB/vulnerablecode/issues/1219#issuecomment-1620123301 + Fetch and store vulnerabilities for each provided ``packages``. + The PURLs are used for the lookups in batch of ``chunk_size`` per request. """ - if not packages_data: - return - - unique_vulnerabilities = [] - seen_vulnerability_ids = set() - - for package_entry in packages_data: - for vulnerability in package_entry.get("affected_by_vulnerabilities", []): - vulnerability_id = vulnerability.get("vulnerability_id") - if vulnerability_id not in seen_vulnerability_ids: - unique_vulnerabilities.append(vulnerability) - seen_vulnerability_ids.add(vulnerability_id) - - return unique_vulnerabilities + vulnerabilities_by_purl = {} + for purls_batch in chunked(get_purls(packages), chunk_size): + response_data = bulk_search_by_purl(purls_batch) + for vulnerability_data in response_data: + vulnerabilities_by_purl[vulnerability_data["purl"]] = vulnerability_data -def fetch_vulnerabilities(packages): - """Fetch and store vulnerabilities for each provided ``packages``.""" + unsaved_objects = [] for package in packages: - if packages_data := get_vulnerabilities_by_purl(package.package_url): - if unique_vulnerabilities := get_unique_vulnerabilities(packages_data): - package.update(affected_by_vulnerabilities=unique_vulnerabilities) + if package_data := vulnerabilities_by_purl.get(package.package_url): + if affected_by := package_data.get("affected_by_vulnerabilities", []): + package.affected_by_vulnerabilities = affected_by + unsaved_objects.append(package) + + if unsaved_objects: + model_class = unsaved_objects[0].__class__ + model_class.objects.bulk_update( + objs=unsaved_objects, + fields=["affected_by_vulnerabilities"], + batch_size=1000, + ) + logger( + f"{len(unsaved_objects)} {model_class._meta.verbose_name_plural} updated " + f"with vulnerability data." + ) diff --git a/scanpipe/templates/scanpipe/dependency_list.html b/scanpipe/templates/scanpipe/dependency_list.html index 08d894665..e5ca36135 100644 --- a/scanpipe/templates/scanpipe/dependency_list.html +++ b/scanpipe/templates/scanpipe/dependency_list.html @@ -23,6 +23,11 @@ {{ dependency.purl }} + {% if dependency.is_vulnerable %} + + + + {% endif %} {{ dependency.type }} diff --git a/scanpipe/templates/scanpipe/includes/project_summary_level.html b/scanpipe/templates/scanpipe/includes/project_summary_level.html index 945591f52..dc11c436b 100644 --- a/scanpipe/templates/scanpipe/includes/project_summary_level.html +++ b/scanpipe/templates/scanpipe/includes/project_summary_level.html @@ -23,11 +23,17 @@

Dependencies

-

+

{% if project.dependency_count %} {{ project.dependency_count|intcomma }} + {% if project.vulnerable_dependency_count %} + + {{ project.vulnerable_dependency_count|intcomma }} + + + {% endif %} {% else %} 0 {% endif %} diff --git a/scanpipe/templates/scanpipe/tabset/tab_vulnerabilities.html b/scanpipe/templates/scanpipe/tabset/tab_vulnerabilities.html index 0182dfeac..75dabc4d4 100644 --- a/scanpipe/templates/scanpipe/tabset/tab_vulnerabilities.html +++ b/scanpipe/templates/scanpipe/tabset/tab_vulnerabilities.html @@ -11,7 +11,7 @@ {% for vulnerability in tab_data.fields.affected_by_vulnerabilities.value %} - + {{ vulnerability.vulnerability_id }} diff --git a/scanpipe/tests/data/asgiref-3.3.0_load_inventory_expected.json b/scanpipe/tests/data/asgiref-3.3.0_load_inventory_expected.json index df6816abd..f888f6f1b 100644 --- a/scanpipe/tests/data/asgiref-3.3.0_load_inventory_expected.json +++ b/scanpipe/tests/data/asgiref-3.3.0_load_inventory_expected.json @@ -251,7 +251,8 @@ "for_package_uid": "pkg:pypi/asgiref@3.3.0?uuid=fixed-uid-done-for-testing-5642512d1758", "datafile_path": "asgiref-3.3.0-py3-none-any.whl", "datasource_id": "pypi_wheel", - "package_type": "pypi" + "package_type": "pypi", + "affected_by_vulnerabilities": [] }, { "purl": "pkg:pypi/pytest", @@ -264,7 +265,8 @@ "for_package_uid": "pkg:pypi/asgiref@3.3.0?uuid=fixed-uid-done-for-testing-5642512d1758", "datafile_path": "asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/METADATA", "datasource_id": "pypi_wheel_metadata", - "package_type": "pypi" + "package_type": "pypi", + "affected_by_vulnerabilities": [] }, { "purl": "pkg:pypi/pytest-asyncio", @@ -277,7 +279,8 @@ "for_package_uid": "pkg:pypi/asgiref@3.3.0?uuid=fixed-uid-done-for-testing-5642512d1758", "datafile_path": "asgiref-3.3.0-py3-none-any.whl", "datasource_id": "pypi_wheel", - "package_type": "pypi" + "package_type": "pypi", + "affected_by_vulnerabilities": [] }, { "purl": "pkg:pypi/pytest-asyncio", @@ -290,7 +293,8 @@ "for_package_uid": "pkg:pypi/asgiref@3.3.0?uuid=fixed-uid-done-for-testing-5642512d1758", "datafile_path": "asgiref-3.3.0-py3-none-any.whl-extract/asgiref-3.3.0.dist-info/METADATA", "datasource_id": "pypi_wheel_metadata", - "package_type": "pypi" + "package_type": "pypi", + "affected_by_vulnerabilities": [] } ], "files": [ diff --git a/scanpipe/tests/data/d2d/about_files/expected.json b/scanpipe/tests/data/d2d/about_files/expected.json index 37f0f28df..490668ca3 100644 --- a/scanpipe/tests/data/d2d/about_files/expected.json +++ b/scanpipe/tests/data/d2d/about_files/expected.json @@ -435,23 +435,23 @@ "detected_license_expression_spdx": "Apache-2.0", "license_detections": [ { - "license_expression": "apache-2.0", "matches": [ { "score": 100.0, - "start_line": 2, + "matcher": "2-aho", "end_line": 17, - "matched_length": 119, + "rule_url": "https://github.com/nexB/scancode-toolkit/tree/develop/src/licensedcode/data/rules/apache-2.0_2.RULE", + "start_line": 2, + "matched_text": " * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements. See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership. The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied. See the License for the\n * specific language governing permissions and limitations\n * under the License.", "match_coverage": 100.0, - "matcher": "2-aho", - "license_expression": "apache-2.0", - "rule_identifier": "apache-2.0_2.RULE", + "matched_length": 119, "rule_relevance": 100, - "rule_url": "https://github.com/nexB/scancode-toolkit/tree/develop/src/licensedcode/data/rules/apache-2.0_2.RULE", - "matched_text": " * Licensed to the Apache Software Foundation (ASF) under one\n * or more contributor license agreements. See the NOTICE file\n * distributed with this work for additional information\n * regarding copyright ownership. The ASF licenses this file\n * to you under the Apache License, Version 2.0 (the\n * \"License\"); you may not use this file except in compliance\n * with the License. You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing,\n * software distributed under the License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n * KIND, either express or implied. See the License for the\n * specific language governing permissions and limitations\n * under the License." + "rule_identifier": "apache-2.0_2.RULE", + "license_expression": "apache-2.0" } ], - "identifier": "apache_2_0-4bde3f57-78aa-4201-96bf-531cba09e7de" + "identifier": "apache_2_0-4bde3f57-78aa-4201-96bf-531cba09e7de", + "license_expression": "apache-2.0" } ], "license_clues": [], @@ -465,8 +465,8 @@ "urls": [ { "url": "http://www.apache.org/licenses/LICENSE-2.0", - "start_line": 10, - "end_line": 10 + "end_line": 10, + "start_line": 10 } ], "extra_data": { diff --git a/scanpipe/tests/data/daglib-0.6.0-py3-none-any.whl_scan_codebase.json b/scanpipe/tests/data/daglib-0.6.0-py3-none-any.whl_scan_codebase.json index e9b8663f3..7e2d62afd 100644 --- a/scanpipe/tests/data/daglib-0.6.0-py3-none-any.whl_scan_codebase.json +++ b/scanpipe/tests/data/daglib-0.6.0-py3-none-any.whl_scan_codebase.json @@ -234,7 +234,8 @@ "for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758", "datafile_path": "daglib-0.6.0-py3-none-any.whl", "datasource_id": "pypi_wheel", - "package_type": "pypi" + "package_type": "pypi", + "affected_by_vulnerabilities": [] }, { "purl": "pkg:pypi/dask", @@ -247,7 +248,8 @@ "for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758", "datafile_path": "daglib-0.6.0-py3-none-any.whl-extract/daglib-0.6.0.dist-info/METADATA", "datasource_id": "pypi_wheel_metadata", - "package_type": "pypi" + "package_type": "pypi", + "affected_by_vulnerabilities": [] }, { "purl": "pkg:pypi/graphviz", @@ -260,7 +262,8 @@ "for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758", "datafile_path": "daglib-0.6.0-py3-none-any.whl", "datasource_id": "pypi_wheel", - "package_type": "pypi" + "package_type": "pypi", + "affected_by_vulnerabilities": [] }, { "purl": "pkg:pypi/graphviz", @@ -273,7 +276,8 @@ "for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758", "datafile_path": "daglib-0.6.0-py3-none-any.whl-extract/daglib-0.6.0.dist-info/METADATA", "datasource_id": "pypi_wheel_metadata", - "package_type": "pypi" + "package_type": "pypi", + "affected_by_vulnerabilities": [] }, { "purl": "pkg:pypi/ipycytoscape", @@ -286,7 +290,8 @@ "for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758", "datafile_path": "daglib-0.6.0-py3-none-any.whl", "datasource_id": "pypi_wheel", - "package_type": "pypi" + "package_type": "pypi", + "affected_by_vulnerabilities": [] }, { "purl": "pkg:pypi/ipycytoscape", @@ -299,7 +304,8 @@ "for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758", "datafile_path": "daglib-0.6.0-py3-none-any.whl-extract/daglib-0.6.0.dist-info/METADATA", "datasource_id": "pypi_wheel_metadata", - "package_type": "pypi" + "package_type": "pypi", + "affected_by_vulnerabilities": [] }, { "purl": "pkg:pypi/networkx", @@ -312,7 +318,8 @@ "for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758", "datafile_path": "daglib-0.6.0-py3-none-any.whl", "datasource_id": "pypi_wheel", - "package_type": "pypi" + "package_type": "pypi", + "affected_by_vulnerabilities": [] }, { "purl": "pkg:pypi/networkx", @@ -325,7 +332,8 @@ "for_package_uid": "pkg:pypi/daglib@0.6.0?uuid=fixed-uid-done-for-testing-5642512d1758", "datafile_path": "daglib-0.6.0-py3-none-any.whl-extract/daglib-0.6.0.dist-info/METADATA", "datasource_id": "pypi_wheel_metadata", - "package_type": "pypi" + "package_type": "pypi", + "affected_by_vulnerabilities": [] } ], "files": [ diff --git a/scanpipe/tests/data/is-npm-1.0.0_scan_codebase.json b/scanpipe/tests/data/is-npm-1.0.0_scan_codebase.json index 989a4fd28..7d2a243ef 100644 --- a/scanpipe/tests/data/is-npm-1.0.0_scan_codebase.json +++ b/scanpipe/tests/data/is-npm-1.0.0_scan_codebase.json @@ -119,7 +119,8 @@ "for_package_uid": "pkg:npm/is-npm@1.0.0?uuid=fixed-uid-done-for-testing-5642512d1758", "datafile_path": "is-npm-1.0.0.tgz-extract/package/package.json", "datasource_id": "npm_package_json", - "package_type": "npm" + "package_type": "npm", + "affected_by_vulnerabilities": [] } ], "files": [ diff --git a/scanpipe/tests/test_api.py b/scanpipe/tests/test_api.py index 951ac2834..59903119c 100644 --- a/scanpipe/tests/test_api.py +++ b/scanpipe/tests/test_api.py @@ -828,7 +828,7 @@ def test_scanpipe_api_serializer_get_model_serializer(self): def test_scanpipe_api_serializer_get_serializer_fields(self): self.assertEqual(44, len(get_serializer_fields(DiscoveredPackage))) - self.assertEqual(11, len(get_serializer_fields(DiscoveredDependency))) + self.assertEqual(12, len(get_serializer_fields(DiscoveredDependency))) self.assertEqual(33, len(get_serializer_fields(CodebaseResource))) self.assertEqual(3, len(get_serializer_fields(CodebaseRelation))) self.assertEqual(6, len(get_serializer_fields(ProjectError))) diff --git a/scanpipe/tests/test_pipelines.py b/scanpipe/tests/test_pipelines.py index c5e89b258..adb0aa6ab 100644 --- a/scanpipe/tests/test_pipelines.py +++ b/scanpipe/tests/test_pipelines.py @@ -654,9 +654,9 @@ def test_scanpipe_load_inventory_pipeline_integration_test(self): @mock.patch("scanpipe.pipes.vulnerablecode.is_available") @mock.patch("scanpipe.pipes.vulnerablecode.is_configured") - @mock.patch("scanpipe.pipes.vulnerablecode.get_vulnerabilities_by_purl") + @mock.patch("scanpipe.pipes.vulnerablecode.bulk_search_by_purl") def test_scanpipe_find_vulnerabilities_pipeline_integration_test( - self, mock_get_vulnerabilities, mock_is_configured, mock_is_available + self, mock_bulk_search_by_purl, mock_is_configured, mock_is_available ): pipeline_name = "find_vulnerabilities" project1 = Project.objects.create(name="Analysis") @@ -676,7 +676,7 @@ def test_scanpipe_find_vulnerabilities_pipeline_integration_test( mock_is_available.return_value = True vulnerability_data = [ { - "purl": "pkg:deb/debian/adduser@3.118", + "purl": "pkg:deb/debian/adduser@3.118?arch=all", "affected_by_vulnerabilities": [ { "vulnerability_id": "VCID-cah8-awtr-aaad", @@ -694,7 +694,7 @@ def test_scanpipe_find_vulnerabilities_pipeline_integration_test( ], }, ] - mock_get_vulnerabilities.return_value = vulnerability_data + mock_bulk_search_by_purl.return_value = vulnerability_data exitcode, out = pipeline.execute() self.assertEqual(0, exitcode, msg=out) diff --git a/scanpipe/views.py b/scanpipe/views.py index 8ce6e3f6b..5f462627a 100644 --- a/scanpipe/views.py +++ b/scanpipe/views.py @@ -1158,7 +1158,10 @@ class DiscoveredDependencyListView( paginate_by = settings.SCANCODEIO_PAGINATE_BY.get("dependency", 100) prefetch_related = ["for_package", "datafile_resource"] table_columns = [ - "package_url", + { + "field_name": "package_url", + "filter_fieldname": "is_vulnerable", + }, { "field_name": "type", "label": "Package type", @@ -1582,11 +1585,6 @@ class DiscoveredPackageDetailsView( }, } - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["vulnerablecode_url"] = settings.VULNERABLECODE_URL - return context - class DiscoveredDependencyDetailsView( ConditionalLoginRequired, @@ -1630,6 +1628,11 @@ class DiscoveredDependencyDetailsView( ], "icon_class": "fa-solid fa-plus-square", }, + "vulnerabilities": { + "fields": ["affected_by_vulnerabilities"], + "icon_class": "fa-solid fa-bug", + "template": "scanpipe/tabset/tab_vulnerabilities.html", + }, } def get_context_data(self, **kwargs):