Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/command-line-interface.rst
Original file line number Diff line number Diff line change
Expand Up @@ -651,3 +651,35 @@ instead of the default JSON output.
$ run scan_codebase codebase/ --format cyclonedx > bom.json

See the :ref:`cli_output` for more information about supported output formats.

`$ scanpipe verify-project --project PROJECT`
---------------------------------------------

Verifies the analysis results of a project against expected package and dependency
counts.
This command is designed to ensure that a project’s scan results meet specific
expectations — for example, that a minimum number of packages or dependencies were
discovered, and that no unexpected vulnerabilities were introduced.

Optional arguments:

- ``--packages`` Minimum number of discovered packages expected.

- ``--vulnerable-packages`` Minimum number of vulnerable packages expected.

- ``--dependencies`` Minimum number of discovered dependencies expected.

- ``--vulnerable-dependencies`` Minimum number of vulnerable dependencies expected.

If any of these expectations are not met, the command exits with a non-zero status
and prints a summary of all issues found.

Example usage:

.. code-block:: bash

$ scanpipe verify-project --project my_project --packages 100 --dependencies 50

.. tip::
This command is particularly useful for **CI/CD pipelines** that need to validate
SBOM or vulnerability scan results against known baselines.
99 changes: 99 additions & 0 deletions scanpipe/management/commands/verify-project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# SPDX-License-Identifier: Apache-2.0
#
# http://nexb.com and https://github.com/aboutcode-org/scancode.io
# The ScanCode.io software is licensed under the Apache License version 2.0.
# Data generated with ScanCode.io is provided as-is without warranties.
# ScanCode is a trademark of nexB Inc.
#
# You may not use this software except in compliance with the License.
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.
#
# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
# ScanCode.io should be considered or used as legal advice. Consult an Attorney
# for any legal advice.
#
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
# Visit https://github.com/aboutcode-org/scancode.io for support and download.

from django.core.management import CommandError

from scanpipe.management.commands import ProjectCommand


class Command(ProjectCommand):
help = "Verify project analysis results against expected counts"

def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument(
"--packages",
type=int,
default=0,
help="Minimum number of packages expected (default: 0)",
)
parser.add_argument(
"--vulnerable-packages",
type=int,
default=0,
help="Minimum number of vulnerable packages expected (default: 0)",
)
parser.add_argument(
"--dependencies",
type=int,
default=0,
help="Minimum number of dependencies expected (default: 0)",
)
parser.add_argument(
"--vulnerable-dependencies",
type=int,
default=0,
help="Minimum number of vulnerable dependencies expected (default: 0)",
)

def handle(self, *args, **options):
super().handle(*args, **options)

expected_packages = options["packages"]
expected_vulnerable_packages = options["vulnerable_packages"]
expected_dependencies = options["dependencies"]
expected_vulnerable_dependencies = options["vulnerable_dependencies"]

project = self.project
packages = project.discoveredpackages
package_count = packages.count()
vulnerable_package_count = packages.vulnerable().count()
dependencies = project.discovereddependencies.all()
dependency_count = dependencies.count()
vulnerable_dependency_count = dependencies.vulnerable().count()

errors = []

if package_count < expected_packages:
errors.append(
f"Expected at least {expected_packages} packages, found {package_count}"
)
if vulnerable_package_count < expected_vulnerable_packages:
errors.append(
f"Expected at least {expected_vulnerable_packages} vulnerable packages,"
f" found {vulnerable_package_count}"
)
if dependency_count < expected_dependencies:
errors.append(
f"Expected at least {expected_dependencies} dependencies, "
f"found {dependency_count}"
)
if vulnerable_dependency_count < expected_vulnerable_dependencies:
errors.append(
f"Expected at least {expected_vulnerable_dependencies} "
f"vulnerable dependencies, found {vulnerable_dependency_count}"
)

if errors:
raise CommandError("Project verification failed:\n" + "\n".join(errors))

self.stdout.write("Project verification passed.", self.style.SUCCESS)
43 changes: 43 additions & 0 deletions scanpipe/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,49 @@ def test_scanpipe_management_command_report(self):
expected = ("project1", "file.ext", "file", "file.ext", "requires-review")
self.assertEqual(expected, row1[0:5])

def test_scanpipe_management_command_verify_project(self):
project = make_project(name="my_project")
make_package(project, package_url="pkg:generic/[email protected]")
make_dependency(project)

out = StringIO()
call_command(
"verify-project",
"--project",
project.name,
"--packages",
"1",
"--vulnerable-packages",
"0",
"--dependencies",
"1",
"--vulnerable-dependencies",
"0",
stdout=out,
)
self.assertIn("Project verification passed.", out.getvalue())

out = StringIO()
expected = (
"Project verification failed:\n"
"Expected at least 5 packages, found 1\n"
"Expected at least 10 vulnerable packages, found 0\n"
"Expected at least 5 dependencies, found 1"
)
with self.assertRaisesMessage(CommandError, expected):
call_command(
"verify-project",
"--project",
project.name,
"--packages",
"5",
"--vulnerable-packages",
"10",
"--dependencies",
"5",
stdout=out,
)


class ScanPipeManagementCommandMixinTest(TestCase):
class CreateProjectCommand(
Expand Down