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):
| |