Skip to content

Commit e966594

Browse files
authored
feat: No-Scan SBOM generation (#5286)
Signed-off-by: joydeep049 <[email protected]>
1 parent 5eb2aa6 commit e966594

File tree

7 files changed

+220
-10
lines changed

7 files changed

+220
-10
lines changed

cve_bin_tool/cli.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,10 @@ def main(argv=None):
11911191
LOGGER.info(
11921192
f"The number of products to process from SBOM - {len(parsed_data)}"
11931193
)
1194+
if args["no_scan"]:
1195+
LOGGER.info(
1196+
"Processing SBOM in no-scan mode - CVE analysis will be skipped"
1197+
)
11941198
for product_info, triage_data in parsed_data.items():
11951199
LOGGER.debug(f"{product_info}, {triage_data}")
11961200
cve_scanner.get_cves(product_info, triage_data)
@@ -1234,11 +1238,11 @@ def main(argv=None):
12341238
LOGGER.info(
12351239
f"Product: {product_info.product} with Version: {product_info.version} not found in Parsed Data, is valid vex file being used?"
12361240
)
1237-
1238-
LOGGER.info("Overall CVE summary: ")
1239-
LOGGER.info(
1240-
f"There are {cve_scanner.products_with_cve} products with known CVEs detected"
1241-
)
1241+
if not args["no_scan"]:
1242+
LOGGER.info("Overall CVE summary: ")
1243+
LOGGER.info(
1244+
f"There are {cve_scanner.products_with_cve} products with known CVEs detected"
1245+
)
12421246

12431247
if cve_scanner.products_with_cve > 0 or args["report"]:
12441248
affected_string = ", ".join(

cve_bin_tool/output_engine/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,7 @@ def output_cves(self, outfile, output_type="console"):
891891
self.sbom_root,
892892
self.strip_scan_dir,
893893
self.logger,
894+
self.no_scan,
894895
)
895896
sbomgen.generate_sbom()
896897

cve_bin_tool/output_engine/console.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ def _output_console_nowrap(
8484
Panel(
8585
"[yellow]⚠️ NO-SCAN MODE[/yellow]\n"
8686
"CVE scanning was disabled. This report shows only the products and versions "
87-
"that were detected, without any vulnerability analysis.",
87+
"that were detected, without any vulnerability analysis. SBOM generation "
88+
"will include all detected products without CVE information.",
8889
title="[yellow]No-Scan Mode Active[/yellow]",
8990
border_style="yellow",
9091
)
@@ -158,7 +159,9 @@ def _output_console_nowrap(
158159
color = summary_color[severity.split("-")[0]]
159160

160161
if all_product_data[product_data] != 0 or no_scan:
161-
if offline:
162+
if no_scan:
163+
latest_stable_version = "NA"
164+
elif offline:
162165
latest_stable_version = "UNKNOWN (offline mode)"
163166
elif no_scan:
164167
latest_stable_version = "N/A (no-scan mode)"

cve_bin_tool/sbom_manager/generate.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def __init__(
3333
sbom_root="CVE-SCAN",
3434
strip_scan_dir=False,
3535
logger: Optional[Logger] = None,
36+
no_scan: bool = False,
3637
):
3738
self.all_product_data = all_product_data
3839
self.all_cve_data = all_cve_data
@@ -42,6 +43,7 @@ def __init__(
4243
self.sbom_root = sbom_root
4344
self.strip_scan_dir = strip_scan_dir
4445
self.logger = logger or LOGGER.getChild(self.__class__.__name__)
46+
self.no_scan = no_scan
4547
self.sbom_packages = {}
4648

4749
def generate_sbom(self) -> None:
@@ -64,6 +66,12 @@ def generate_sbom(self) -> None:
6466
my_package.set_licenseconcluded(license)
6567
my_package.set_supplier("UNKNOWN", "NOASSERTION")
6668

69+
# Add no-scan mode information if applicable
70+
if self.no_scan:
71+
my_package.set_description(
72+
"SBOM generated in no-scan mode - CVE analysis was not performed"
73+
)
74+
6775
# Store package data
6876
self.sbom_packages[
6977
(
@@ -100,7 +108,10 @@ def generate_sbom(self) -> None:
100108
in self.sbom_packages
101109
and product_data.vendor == "unknown"
102110
):
103-
if self.all_cve_data.get(product_data):
111+
# In no-scan mode, we still want to include path information if available
112+
if self.all_cve_data.get(product_data) and self.all_cve_data[
113+
product_data
114+
].get("paths"):
104115
for path in self.all_cve_data[product_data]["paths"]:
105116
if self.strip_scan_dir:
106117
evidence = strip_path(path, self.sbom_root)

test/test_cli.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def test_version(self):
118118
def test_invalid_file_or_directory(self):
119119
"""Test behaviour with an invalid file/directory"""
120120
with pytest.raises(SystemExit) as e:
121-
main(["cve-bin-tool", "non-existant"])
121+
main(["cve-bin-tool", "non-existent"])
122122
assert e.value.args[0] == ERROR_CODES[FileNotFoundError]
123123

124124
def test_null_byte_in_filename(self):
@@ -138,7 +138,7 @@ def test_null_byte_in_filename(self):
138138
assert e.value.args[0] == ERROR_CODES[FileNotFoundError]
139139

140140
def test_invalid_parameter(self):
141-
"""Test that invalid parmeters exit with expected error code.
141+
"""Test that invalid parameters exit with expected error code.
142142
ArgParse calls sys.exit(2) for all errors"""
143143

144144
# no directory specified
@@ -738,6 +738,64 @@ def test_sbom_detection(self, caplog):
738738
"Using CVE Binary Tool SBOM Auto Detection",
739739
) in caplog.record_tuples
740740

741+
def test_sbom_no_scan_mode(self, caplog):
742+
"""Test SBOM processing in no-scan mode"""
743+
SBOM_PATH = Path(__file__).parent.resolve() / "sbom"
744+
745+
with caplog.at_level(logging.INFO):
746+
main(
747+
[
748+
"cve-bin-tool",
749+
"--no-scan",
750+
"--sbom",
751+
"spdx",
752+
"--sbom-file",
753+
str(SBOM_PATH / "spdx_test.spdx"),
754+
]
755+
)
756+
757+
# Check that no-scan mode message is logged
758+
sbom_no_scan_message_found = False
759+
760+
for _, _, log_message in caplog.record_tuples:
761+
if "Processing SBOM in no-scan mode" in log_message:
762+
sbom_no_scan_message_found = True
763+
764+
assert (
765+
sbom_no_scan_message_found
766+
), "Expected SBOM no-scan mode message not found"
767+
768+
# The no-scan mode message is displayed in the console output, not in logs
769+
# We can see from the captured stdout that it's working correctly
770+
771+
def test_directory_no_scan_mode_sbom_generation(self, caplog):
772+
"""Test directory scanning in no-scan mode with SBOM generation"""
773+
# Create a temporary directory with some test files
774+
test_dir = Path(self.tempdir) / "test_scan"
775+
test_dir.mkdir()
776+
777+
# Create a simple test file that would be detected by a checker
778+
test_file = test_dir / "test_file"
779+
test_file.write_text("test content")
780+
781+
with caplog.at_level(logging.INFO):
782+
main(
783+
[
784+
"cve-bin-tool",
785+
"--no-scan",
786+
str(test_dir),
787+
"--sbom",
788+
"spdx",
789+
"--sbom-file",
790+
str(test_dir / "output.spdx"),
791+
]
792+
)
793+
794+
# Check that the scan completed without errors
795+
# In no-scan mode, we expect the scan to complete and generate an SBOM
796+
# even if no products are found
797+
assert "Total files:" in caplog.text
798+
741799
@pytest.mark.skipif(not LONG_TESTS(), reason="Skipping long tests")
742800
def test_console_output_depending_reportlab_existence(self, caplog):
743801
import subprocess

test/test_output_engine.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,7 @@ def test_generate_sbom(self):
979979
sbom_type="spdx",
980980
sbom_format="tag",
981981
sbom_root="CVE-SCAN",
982+
no_scan=False,
982983
)
983984
sbomgen.generate_sbom()
984985

@@ -1027,6 +1028,92 @@ def test_generate_sbom(self):
10271028
actual_packages = [package for package in sbomgen.sbom_packages.values()]
10281029
self.assertEqual(actual_packages, list(expected_packages))
10291030

1031+
def test_generate_sbom_no_scan_mode(self):
1032+
"""Test SBOM generation in no-scan mode"""
1033+
with patch(
1034+
"cve_bin_tool.sbom_manager.generate.SBOMPackage"
1035+
) as mock_sbom_package, patch(
1036+
"cve_bin_tool.sbom_manager.generate.SBOMRelationship"
1037+
):
1038+
mock_package_instance = MagicMock()
1039+
mock_sbom_package.return_value = mock_package_instance
1040+
1041+
sbomgen = SBOMGenerate(
1042+
all_product_data=self.all_product_data,
1043+
all_cve_data=self.MOCK_OUTPUT,
1044+
filename="test.sbom",
1045+
sbom_type="spdx",
1046+
sbom_format="tag",
1047+
sbom_root="CVE-SCAN",
1048+
no_scan=True,
1049+
)
1050+
1051+
# Verify no-scan mode is set
1052+
self.assertTrue(sbomgen.no_scan)
1053+
1054+
sbomgen.generate_sbom()
1055+
1056+
# In no-scan mode, we should still set the description
1057+
mock_package_instance.set_description.assert_called_with(
1058+
"SBOM generated in no-scan mode - CVE analysis was not performed"
1059+
)
1060+
1061+
def test_console_output_no_scan_mode_latest_version(self):
1062+
"""Test that console output shows 'NA' for latest upstream version in no-scan mode"""
1063+
from datetime import datetime
1064+
1065+
from rich.console import Console
1066+
1067+
from cve_bin_tool.output_engine.console import _output_console_nowrap
1068+
1069+
# Create mock data
1070+
all_product_data = {
1071+
ProductInfo(
1072+
vendor="test_vendor", product="test_product", version="1.0.0"
1073+
): 0,
1074+
}
1075+
1076+
all_cve_data = {
1077+
ProductInfo(
1078+
vendor="test_vendor", product="test_product", version="1.0.0"
1079+
): {"cves": [], "paths": {"/path/to/test"}}
1080+
}
1081+
1082+
# Test with no-scan mode
1083+
with patch(
1084+
"cve_bin_tool.output_engine.console.get_latest_upstream_stable_version"
1085+
) as mock_get_version:
1086+
mock_get_version.return_value = "2.0.0"
1087+
1088+
# Capture the output
1089+
from io import StringIO
1090+
1091+
output = StringIO()
1092+
1093+
_output_console_nowrap(
1094+
all_cve_data=all_cve_data,
1095+
all_cve_version_info={},
1096+
scanned_dir="/test",
1097+
time_of_last_update=datetime(2024, 1, 1),
1098+
affected_versions=0,
1099+
exploits=False,
1100+
metrics=False,
1101+
strip_scan_dir=False,
1102+
all_product_data=all_product_data,
1103+
offline=False,
1104+
width=None,
1105+
console=Console(file=output),
1106+
no_scan=True,
1107+
)
1108+
1109+
output_content = output.getvalue()
1110+
1111+
# Verify that 'NA' appears in the output for latest upstream version
1112+
self.assertIn("NA", output_content)
1113+
1114+
# Verify that get_latest_upstream_stable_version was NOT called
1115+
mock_get_version.assert_not_called()
1116+
10301117
def tearDown(self) -> None:
10311118
self.mock_file.close()
10321119

test/test_sbom.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,49 @@ def test_invalid_xml(self, filename: str, sbom_type: str, validate: bool):
265265
)
266266
def test_sbom_detection(self, filename: str, expected_sbom_type: str):
267267
assert sbom_detection(filename) == expected_sbom_type
268+
269+
def test_sbom_generate_no_scan_mode(self):
270+
"""Test SBOM generation in no-scan mode"""
271+
from cve_bin_tool.sbom_manager.generate import SBOMGenerate
272+
273+
# Create sample product data
274+
all_product_data = {
275+
ProductInfo(
276+
vendor="test_vendor", product="test_product", version="1.0.0"
277+
): 0,
278+
ProductInfo(
279+
vendor="another_vendor", product="another_product", version="2.0.0"
280+
): 0,
281+
}
282+
283+
# Create sample CVE data with paths
284+
all_cve_data = {
285+
ProductInfo(
286+
vendor="test_vendor", product="test_product", version="1.0.0"
287+
): {"cves": [], "paths": {"/path/to/test_product"}},
288+
ProductInfo(
289+
vendor="another_vendor", product="another_product", version="2.0.0"
290+
): {"cves": [], "paths": {"/path/to/another_product"}},
291+
}
292+
293+
# Test SBOM generation in no-scan mode
294+
sbom_gen = SBOMGenerate(
295+
all_product_data=all_product_data,
296+
all_cve_data=all_cve_data,
297+
filename="",
298+
sbom_type="spdx",
299+
sbom_format="tag",
300+
sbom_root="TEST-SCAN",
301+
strip_scan_dir=False,
302+
logger=None,
303+
no_scan=True,
304+
)
305+
306+
# Verify no-scan mode is set
307+
assert sbom_gen.no_scan is True
308+
309+
# Test that generate_sbom doesn't crash in no-scan mode
310+
# We can't easily test the actual output without file I/O, but we can test the setup
311+
assert sbom_gen.sbom_packages == {}
312+
assert sbom_gen.all_product_data == all_product_data
313+
assert sbom_gen.all_cve_data == all_cve_data

0 commit comments

Comments
 (0)