Skip to content

Commit 45527d9

Browse files
committed
Add changelog entry and unit test for check_vulnerabilities #101
Signed-off-by: Thomas Druez <[email protected]>
1 parent 675c8a6 commit 45527d9

File tree

3 files changed

+70
-78
lines changed

3 files changed

+70
-78
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Changelog
44
v31.1.0 (unreleased)
55
--------------------
66

7+
- Add a new "check vulnerabilities" pipeline to lookup vulnerabilities in the
8+
VulnerableCode database for all project discovered packages.
9+
Vulnerability data is stored in the extra_data field of each package.
10+
More details about VulnerableCode at https://github.com/nexB/vulnerablecode/
11+
https://github.com/nexB/scancode.io/issues/101
12+
713
- Generate SBOM (Software Bill of Materials) compliant with the SPDX 2.3 specification
814
as a new downloadable output.
915
https://github.com/nexB/scancode.io/issues/389

scanpipe/pipes/vulnerablecode.py

Lines changed: 20 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -58,32 +58,40 @@ def is_configured():
5858
return False
5959

6060

61-
def is_service_available(label, session, url, raise_exceptions):
61+
def is_available():
6262
"""
63-
Base function that checks if a configured integration service is available.
63+
Returns True if the configured VulnerableCode server is available.
6464
"""
65+
if not is_configured():
66+
return False
67+
6568
try:
66-
response = session.head(url)
69+
response = session.head(VULNERABLECODE_API_URL)
6770
response.raise_for_status()
6871
except requests.exceptions.RequestException as request_exception:
6972
logger.debug(f"{label} is_available() error: {request_exception}")
70-
if raise_exceptions:
71-
raise
7273
return False
7374

7475
return response.status_code == requests.codes.ok
7576

7677

77-
def is_available(raise_exceptions=False):
78+
def get_base_purl(purl):
7879
"""
79-
Returns True if the configured VulnerableCode server is available.
80+
Returns the `purl` without qualifiers and subpath.
8081
"""
81-
if not is_configured():
82-
return False
82+
return purl.split("?")[0]
8383

84-
return is_service_available(
85-
label, session, VULNERABLECODE_API_URL, raise_exceptions
86-
)
84+
85+
def get_purls(packages, base=False):
86+
"""
87+
Returns the PURLs for the given list of `packages`.
88+
Do not include qualifiers nor subpath when `base` is provided.
89+
"""
90+
return [
91+
get_base_purl(package_url) if base else package_url
92+
for package in packages
93+
if (package_url := package.package_url)
94+
]
8795

8896

8997
def request_get(
@@ -123,13 +131,6 @@ def request_post(
123131
logger.debug(f"{label} [Exception] {exception}")
124132

125133

126-
def get_base_purl(purl):
127-
"""
128-
Returns the `purl` without the qualifiers and the subpath.
129-
"""
130-
return purl.split("?")[0]
131-
132-
133134
def _get_vulnerabilities(
134135
url,
135136
field_name,
@@ -213,62 +214,3 @@ def bulk_search_by_cpes(
213214

214215
logger.debug(f"VulnerableCode: url={url} cpes_count={len(cpes)}")
215216
return request_post(url, data, timeout)
216-
217-
218-
def get_purls(packages):
219-
"""
220-
Returns the PURLs for the given list of `packages`.
221-
List comprehension is not used on purpose to avoid crafting each
222-
PURL twice.
223-
"""
224-
purls = []
225-
for package in packages:
226-
package_url = package.package_url
227-
if package_url:
228-
purls.append(package_url)
229-
return purls
230-
231-
232-
def get_vulnerable_purls(packages):
233-
"""
234-
Returns a list of PURLs for which at least one `affected_by_vulnerabilities`
235-
was found in the VulnerableCodeDB for the given list of `packages`.
236-
"""
237-
purls = get_purls(packages)
238-
239-
if not purls:
240-
return []
241-
242-
search_results = bulk_search_by_purl(purls, timeout=5)
243-
if not search_results:
244-
return []
245-
246-
return [
247-
entry.get("purl")
248-
for entry in search_results
249-
if entry.get("affected_by_vulnerabilities")
250-
]
251-
252-
253-
def get_vulnerable_cpes(components):
254-
"""
255-
Returns a list of vulnerable CPEs found in the VulnerableCodeDB for the given list
256-
of `components`.
257-
"""
258-
cpes = [component.cpe for component in components if component.cpe]
259-
260-
if not cpes:
261-
return []
262-
263-
search_results = bulk_search_by_cpes(cpes, timeout=5)
264-
if not search_results:
265-
return []
266-
267-
vulnerable_cpes = [
268-
reference.get("reference_id")
269-
for entry in search_results
270-
for reference in entry.get("references")
271-
if reference.get("reference_id").startswith("cpe")
272-
]
273-
274-
return list(set(vulnerable_cpes))

scanpipe/tests/test_pipelines.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@
3535

3636
from scancode.cli_test_utils import purl_with_fake_uuid
3737

38+
from scanpipe.models import DiscoveredPackage
3839
from scanpipe.models import Project
3940
from scanpipe.pipelines import Pipeline
4041
from scanpipe.pipelines import is_pipeline
4142
from scanpipe.pipelines import root_filesystems
4243
from scanpipe.pipes import output
4344
from scanpipe.tests import FIXTURES_REGEN
45+
from scanpipe.tests import package_data1
4446
from scanpipe.tests.pipelines.do_nothing import DoNothing
4547
from scanpipe.tests.pipelines.steps_as_attribute import StepsAsAttribute
4648

@@ -580,3 +582,45 @@ def test_scanpipe_load_inventory_pipeline_integration_test(self):
580582
self.data_location / "asgiref-3.3.0_load_inventory_expected.json"
581583
)
582584
self.assertPipelineResultEqual(expected_file, result_file)
585+
586+
@mock.patch("scanpipe.pipes.vulnerablecode.is_available")
587+
@mock.patch("scanpipe.pipes.vulnerablecode.is_configured")
588+
@mock.patch("scanpipe.pipes.vulnerablecode.get_vulnerabilities_by_purl")
589+
def test_scanpipe_check_vulnerabilities_pipeline_integration_test(
590+
self, mock_get_vulnerabilities, mock_is_configured, mock_is_available
591+
):
592+
pipeline_name = "check_vulnerabilities"
593+
project1 = Project.objects.create(name="Analysis")
594+
package1 = DiscoveredPackage.create_from_data(project1, package_data1)
595+
596+
run = project1.add_pipeline(pipeline_name)
597+
pipeline = run.make_pipeline_instance()
598+
mock_is_configured.return_value = False
599+
mock_is_available.return_value = False
600+
exitcode, out = pipeline.execute()
601+
self.assertEqual(1, exitcode, msg=out)
602+
self.assertIn("VulnerableCode is not configured.", out)
603+
604+
run = project1.add_pipeline(pipeline_name)
605+
pipeline = run.make_pipeline_instance()
606+
mock_is_configured.return_value = True
607+
mock_is_available.return_value = True
608+
vulnerability_data = [
609+
{
610+
"purl": "pkg:deb/debian/[email protected]",
611+
"affected_by_vulnerabilities": [
612+
{
613+
"vulnerability_id": "VCID-cah8-awtr-aaad",
614+
"summary": "An issue was discovered.",
615+
}
616+
],
617+
}
618+
]
619+
mock_get_vulnerabilities.return_value = vulnerability_data
620+
621+
exitcode, out = pipeline.execute()
622+
self.assertEqual(0, exitcode, msg=out)
623+
624+
package1.refresh_from_db()
625+
expected = {"discovered_vulnerabilities": vulnerability_data}
626+
self.assertEqual(expected, package1.extra_data)

0 commit comments

Comments
 (0)