Skip to content

Commit e017e35

Browse files
authored
Add a verify-project CLI management command (#1903)
Signed-off-by: tdruez <[email protected]>
1 parent 5c5a897 commit e017e35

File tree

3 files changed

+174
-0
lines changed

3 files changed

+174
-0
lines changed

docs/command-line-interface.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,3 +651,35 @@ instead of the default JSON output.
651651
$ run scan_codebase codebase/ --format cyclonedx > bom.json
652652
653653
See the :ref:`cli_output` for more information about supported output formats.
654+
655+
`$ scanpipe verify-project --project PROJECT`
656+
---------------------------------------------
657+
658+
Verifies the analysis results of a project against expected package and dependency
659+
counts.
660+
This command is designed to ensure that a project’s scan results meet specific
661+
expectations — for example, that a minimum number of packages or dependencies were
662+
discovered, and that no unexpected vulnerabilities were introduced.
663+
664+
Optional arguments:
665+
666+
- ``--packages`` Minimum number of discovered packages expected.
667+
668+
- ``--vulnerable-packages`` Minimum number of vulnerable packages expected.
669+
670+
- ``--dependencies`` Minimum number of discovered dependencies expected.
671+
672+
- ``--vulnerable-dependencies`` Minimum number of vulnerable dependencies expected.
673+
674+
If any of these expectations are not met, the command exits with a non-zero status
675+
and prints a summary of all issues found.
676+
677+
Example usage:
678+
679+
.. code-block:: bash
680+
681+
$ scanpipe verify-project --project my_project --packages 100 --dependencies 50
682+
683+
.. tip::
684+
This command is particularly useful for **CI/CD pipelines** that need to validate
685+
SBOM or vulnerability scan results against known baselines.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
#
3+
# http://nexb.com and https://github.com/aboutcode-org/scancode.io
4+
# The ScanCode.io software is licensed under the Apache License version 2.0.
5+
# Data generated with ScanCode.io is provided as-is without warranties.
6+
# ScanCode is a trademark of nexB Inc.
7+
#
8+
# You may not use this software except in compliance with the License.
9+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
10+
# Unless required by applicable law or agreed to in writing, software distributed
11+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
12+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
13+
# specific language governing permissions and limitations under the License.
14+
#
15+
# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES
16+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
17+
# ScanCode.io should be considered or used as legal advice. Consult an Attorney
18+
# for any legal advice.
19+
#
20+
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
21+
# Visit https://github.com/aboutcode-org/scancode.io for support and download.
22+
23+
from django.core.management import CommandError
24+
25+
from scanpipe.management.commands import ProjectCommand
26+
27+
28+
class Command(ProjectCommand):
29+
help = "Verify project analysis results against expected counts"
30+
31+
def add_arguments(self, parser):
32+
super().add_arguments(parser)
33+
parser.add_argument(
34+
"--packages",
35+
type=int,
36+
default=0,
37+
help="Minimum number of packages expected (default: 0)",
38+
)
39+
parser.add_argument(
40+
"--vulnerable-packages",
41+
type=int,
42+
default=0,
43+
help="Minimum number of vulnerable packages expected (default: 0)",
44+
)
45+
parser.add_argument(
46+
"--dependencies",
47+
type=int,
48+
default=0,
49+
help="Minimum number of dependencies expected (default: 0)",
50+
)
51+
parser.add_argument(
52+
"--vulnerable-dependencies",
53+
type=int,
54+
default=0,
55+
help="Minimum number of vulnerable dependencies expected (default: 0)",
56+
)
57+
58+
def handle(self, *args, **options):
59+
super().handle(*args, **options)
60+
61+
expected_packages = options["packages"]
62+
expected_vulnerable_packages = options["vulnerable_packages"]
63+
expected_dependencies = options["dependencies"]
64+
expected_vulnerable_dependencies = options["vulnerable_dependencies"]
65+
66+
project = self.project
67+
packages = project.discoveredpackages
68+
package_count = packages.count()
69+
vulnerable_package_count = packages.vulnerable().count()
70+
dependencies = project.discovereddependencies.all()
71+
dependency_count = dependencies.count()
72+
vulnerable_dependency_count = dependencies.vulnerable().count()
73+
74+
errors = []
75+
76+
if package_count < expected_packages:
77+
errors.append(
78+
f"Expected at least {expected_packages} packages, found {package_count}"
79+
)
80+
if vulnerable_package_count < expected_vulnerable_packages:
81+
errors.append(
82+
f"Expected at least {expected_vulnerable_packages} vulnerable packages,"
83+
f" found {vulnerable_package_count}"
84+
)
85+
if dependency_count < expected_dependencies:
86+
errors.append(
87+
f"Expected at least {expected_dependencies} dependencies, "
88+
f"found {dependency_count}"
89+
)
90+
if vulnerable_dependency_count < expected_vulnerable_dependencies:
91+
errors.append(
92+
f"Expected at least {expected_vulnerable_dependencies} "
93+
f"vulnerable dependencies, found {vulnerable_dependency_count}"
94+
)
95+
96+
if errors:
97+
raise CommandError("Project verification failed:\n" + "\n".join(errors))
98+
99+
self.stdout.write("Project verification passed.", self.style.SUCCESS)

scanpipe/tests/test_commands.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1371,6 +1371,49 @@ def test_scanpipe_management_command_report(self):
13711371
expected = ("project1", "file.ext", "file", "file.ext", "requires-review")
13721372
self.assertEqual(expected, row1[0:5])
13731373

1374+
def test_scanpipe_management_command_verify_project(self):
1375+
project = make_project(name="my_project")
1376+
make_package(project, package_url="pkg:generic/[email protected]")
1377+
make_dependency(project)
1378+
1379+
out = StringIO()
1380+
call_command(
1381+
"verify-project",
1382+
"--project",
1383+
project.name,
1384+
"--packages",
1385+
"1",
1386+
"--vulnerable-packages",
1387+
"0",
1388+
"--dependencies",
1389+
"1",
1390+
"--vulnerable-dependencies",
1391+
"0",
1392+
stdout=out,
1393+
)
1394+
self.assertIn("Project verification passed.", out.getvalue())
1395+
1396+
out = StringIO()
1397+
expected = (
1398+
"Project verification failed:\n"
1399+
"Expected at least 5 packages, found 1\n"
1400+
"Expected at least 10 vulnerable packages, found 0\n"
1401+
"Expected at least 5 dependencies, found 1"
1402+
)
1403+
with self.assertRaisesMessage(CommandError, expected):
1404+
call_command(
1405+
"verify-project",
1406+
"--project",
1407+
project.name,
1408+
"--packages",
1409+
"5",
1410+
"--vulnerable-packages",
1411+
"10",
1412+
"--dependencies",
1413+
"5",
1414+
stdout=out,
1415+
)
1416+
13741417

13751418
class ScanPipeManagementCommandMixinTest(TestCase):
13761419
class CreateProjectCommand(

0 commit comments

Comments
 (0)