Skip to content

Commit 857ab86

Browse files
authored
Add a --fail-on-vulnerabilities in check-compliance command (#1702)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 17d338f commit 857ab86

File tree

6 files changed

+113
-37
lines changed

6 files changed

+113
-37
lines changed

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
Changelog
22
=========
33

4+
v35.1.0 (unreleased)
5+
--------------------
6+
7+
- Add a ``--fail-on-vulnerabilities`` option in ``check-compliance`` management command.
8+
When this option is enabled, the command will exit with a non-zero status if known
9+
vulnerabilities are detected in discovered packages and dependencies.
10+
Requires the ``find_vulnerabilities`` pipeline to be executed beforehand.
11+
https://github.com/aboutcode-org/scancode.io/pull/1702
12+
413
v35.0.0 (2025-06-23)
514
--------------------
615

docs/command-line-interface.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,10 @@ Optional arguments:
497497
- ``--fail-level {ERROR,WARNING,MISSING}`` Compliance alert level that will cause the
498498
command to exit with a non-zero status. Default is ERROR.
499499

500+
- ``--fail-on-vulnerabilities`` Exit with a non-zero status if known vulnerabilities
501+
are detected in discovered packages and dependencies.
502+
Requires the ``find_vulnerabilities`` pipeline to be executed beforehand.
503+
500504
`$ scanpipe archive-project --project PROJECT`
501505
----------------------------------------------
502506

scanpipe/management/commands/check-compliance.py

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,31 +45,64 @@ def add_arguments(self, parser):
4545
"non-zero status. Default is ERROR."
4646
),
4747
)
48+
parser.add_argument(
49+
"--fail-on-vulnerabilities",
50+
action="store_true",
51+
help=(
52+
"Exit with a non-zero status if known vulnerabilities are detected in "
53+
"discovered packages and dependencies. "
54+
"Requires the `find_vulnerabilities` pipeline to be executed "
55+
"beforehand."
56+
),
57+
)
4858

4959
def handle(self, *args, **options):
5060
super().handle(*args, **options)
51-
fail_level = options["fail_level"]
52-
compliance_alerts = get_project_compliance_alerts(self.project, fail_level)
61+
exit_code = 0
62+
63+
if self.check_compliance(options["fail_level"]):
64+
exit_code = 1
65+
66+
if options["fail_on_vulnerabilities"] and self.check_vulnerabilities():
67+
exit_code = 1
5368

54-
compliance_alerts_count = sum(
55-
len(issues_by_severity)
56-
for model_alerts in compliance_alerts.values()
57-
for issues_by_severity in model_alerts.values()
69+
sys.exit(exit_code)
70+
71+
def check_compliance(self, fail_level):
72+
alerts = get_project_compliance_alerts(self.project, fail_level)
73+
count = sum(
74+
len(issues) for model in alerts.values() for issues in model.values()
5875
)
59-
if not compliance_alerts_count:
60-
sys.exit(0)
6176

62-
if self.verbosity > 0:
63-
msg = [
64-
f"{compliance_alerts_count} compliance issues detected on this project."
65-
]
66-
for label, issues in compliance_alerts.items():
67-
msg.append(f"[{label}]")
68-
for severity, entries in issues.items():
69-
msg.append(f" > {severity.upper()}: {len(entries)}")
77+
if count and self.verbosity > 0:
78+
self.stderr.write(f"{count} compliance issues detected.")
79+
for label, model in alerts.items():
80+
self.stderr.write(f"[{label}]")
81+
for severity, entries in model.items():
82+
self.stderr.write(f" > {severity.upper()}: {len(entries)}")
7083
if self.verbosity > 1:
71-
msg.append(" " + "\n ".join(entries))
84+
self.stderr.write(" " + "\n ".join(entries))
85+
86+
return count > 0
7287

73-
self.stderr.write("\n".join(msg))
88+
def check_vulnerabilities(self):
89+
packages = self.project.discoveredpackages.vulnerable_ordered()
90+
dependencies = self.project.discovereddependencies.vulnerable_ordered()
91+
92+
vulnerable_records = list(packages) + list(dependencies)
93+
count = len(vulnerable_records)
94+
95+
if self.verbosity > 0:
96+
if count:
97+
self.stderr.write(f"{count} vulnerable records found:")
98+
for entry in vulnerable_records:
99+
self.stderr.write(str(entry))
100+
vulnerability_ids = [
101+
vulnerability.get("vulnerability_id")
102+
for vulnerability in entry.affected_by_vulnerabilities
103+
]
104+
self.stderr.write(" > " + ", ".join(vulnerability_ids))
105+
else:
106+
self.stdout.write("No vulnerabilities found")
74107

75-
sys.exit(1)
108+
return count > 0

scanpipe/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3150,6 +3150,13 @@ class VulnerabilityQuerySetMixin:
31503150
def vulnerable(self):
31513151
return self.filter(~Q(affected_by_vulnerabilities__in=EMPTY_VALUES))
31523152

3153+
def vulnerable_ordered(self):
3154+
return (
3155+
self.vulnerable()
3156+
.only_package_url_fields(extra=["affected_by_vulnerabilities"])
3157+
.order_by_package_url()
3158+
)
3159+
31533160

31543161
class DiscoveredPackageQuerySet(
31553162
VulnerabilityQuerySetMixin,

scanpipe/pipes/output.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -567,21 +567,11 @@ def to_xlsx(project):
567567

568568

569569
def add_vulnerabilities_sheet(workbook, project):
570-
vulnerable_packages_queryset = (
571-
DiscoveredPackage.objects.project(project)
572-
.vulnerable()
573-
.only_package_url_fields(extra=["affected_by_vulnerabilities"])
574-
.order_by_package_url()
575-
)
576-
vulnerable_dependencies_queryset = (
577-
DiscoveredDependency.objects.project(project)
578-
.vulnerable()
579-
.only_package_url_fields(extra=["affected_by_vulnerabilities"])
580-
.order_by_package_url()
581-
)
570+
vulnerable_packages = project.discoveredpackages.vulnerable_ordered()
571+
vulnerable_dependencies = project.discovereddependencies.vulnerable_ordered()
582572
vulnerable_querysets = [
583-
vulnerable_packages_queryset,
584-
vulnerable_dependencies_queryset,
573+
vulnerable_packages,
574+
vulnerable_dependencies,
585575
]
586576

587577
vulnerability_fields = [

scanpipe/tests/test_commands.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from scanpipe.pipes import flag
5050
from scanpipe.pipes import purldb
5151
from scanpipe.tests import filter_warnings
52+
from scanpipe.tests import make_dependency
5253
from scanpipe.tests import make_mock_response
5354
from scanpipe.tests import make_package
5455
from scanpipe.tests import make_project
@@ -1196,9 +1197,7 @@ def test_scanpipe_management_command_check_compliance(self):
11961197
call_command("check-compliance", *options, stderr=out)
11971198
self.assertEqual(cm.exception.code, 1)
11981199
out_value = out.getvalue().strip()
1199-
expected = (
1200-
"1 compliance issues detected on this project.\n[packages]\n > ERROR: 1"
1201-
)
1200+
expected = "1 compliance issues detected.\n[packages]\n > ERROR: 1"
12021201
self.assertEqual(expected, out_value)
12031202

12041203
out = StringIO()
@@ -1208,12 +1207,46 @@ def test_scanpipe_management_command_check_compliance(self):
12081207
self.assertEqual(cm.exception.code, 1)
12091208
out_value = out.getvalue().strip()
12101209
expected = (
1211-
"2 compliance issues detected on this project."
1210+
"2 compliance issues detected."
12121211
"\n[packages]\n > ERROR: 1"
12131212
"\n[resources]\n > WARNING: 1"
12141213
)
12151214
self.assertEqual(expected, out_value)
12161215

1216+
def test_scanpipe_management_command_check_compliance_vulnerabilities(self):
1217+
project = make_project(name="my_project")
1218+
package1 = make_package(project, package_url="pkg:generic/name@1.0")
1219+
1220+
out = StringIO()
1221+
options = ["--project", project.name, "--fail-on-vulnerabilities"]
1222+
with self.assertRaises(SystemExit) as cm:
1223+
call_command("check-compliance", *options, stdout=out)
1224+
self.assertEqual(cm.exception.code, 0)
1225+
out_value = out.getvalue().strip()
1226+
self.assertEqual("No vulnerabilities found", out_value)
1227+
1228+
vulnerability_data = [{"vulnerability_id": "VCID-cah8-awtr-aaad"}]
1229+
package1.update(affected_by_vulnerabilities=vulnerability_data)
1230+
make_dependency(
1231+
project,
1232+
dependency_uid="dependency1",
1233+
affected_by_vulnerabilities=vulnerability_data,
1234+
)
1235+
out = StringIO()
1236+
options = ["--project", project.name, "--fail-on-vulnerabilities"]
1237+
with self.assertRaises(SystemExit) as cm:
1238+
call_command("check-compliance", *options, stderr=out)
1239+
self.assertEqual(cm.exception.code, 1)
1240+
out_value = out.getvalue().strip()
1241+
expected = (
1242+
"2 vulnerable records found:\n"
1243+
"pkg:generic/name@1.0\n"
1244+
" > VCID-cah8-awtr-aaad\n"
1245+
"dependency1\n"
1246+
" > VCID-cah8-awtr-aaad"
1247+
)
1248+
self.assertEqual(expected, out_value)
1249+
12171250
def test_scanpipe_management_command_report(self):
12181251
label1 = "label1"
12191252
project1 = make_project("project1", labels=[label1])

0 commit comments

Comments
 (0)