Skip to content

Commit 136e286

Browse files
authored
feat: sbom auto detection for command line (#3734)
1 parent b5ce10d commit 136e286

File tree

4 files changed

+101
-4
lines changed

4 files changed

+101
-4
lines changed

cve_bin_tool/cli.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
from cve_bin_tool.merge import MergeReports
6969
from cve_bin_tool.output_engine import OutputEngine
7070
from cve_bin_tool.package_list_parser import PackageListParser
71+
from cve_bin_tool.sbom_detection import sbom_detection
7172
from cve_bin_tool.sbom_manager import SBOMManager
7273
from cve_bin_tool.util import ProductInfo
7374
from cve_bin_tool.version import VERSION
@@ -192,7 +193,7 @@ def main(argv=None):
192193
input_group.add_argument(
193194
"--sbom",
194195
action="store",
195-
default="spdx",
196+
default="",
196197
choices=["spdx", "cyclonedx", "swid"],
197198
help="specify type of software bill of materials (sbom) (default: spdx)",
198199
)
@@ -887,7 +888,14 @@ def main(argv=None):
887888
and args["directory"]
888889
and Path(args["directory"]).is_file()
889890
):
890-
if (
891+
type = sbom_detection(args["directory"])
892+
if not args["sbom_file"] and not args["sbom"] and type is not None:
893+
LOGGER.info("Using CVE Binary Tool SBOM Auto Detection")
894+
args["sbom"] = type
895+
args["sbom_file"] = args["directory"]
896+
args["directory"] = ""
897+
898+
elif (
891899
args["directory"].endswith(".csv")
892900
or args["directory"].endswith(".json")
893901
or args["directory"].endswith(".vex")

cve_bin_tool/sbom_detection.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright (C) 2024 Intel Corporation
2+
# SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
import json
5+
6+
import defusedxml.ElementTree as ET
7+
8+
from cve_bin_tool.validator import validate_cyclonedx, validate_swid
9+
10+
11+
def sbom_detection(file_path: str) -> str:
12+
"""
13+
Identifies SBOM type of file based on its format and schema.
14+
15+
Args:
16+
file_path (str): The path to the file.
17+
18+
Returns:
19+
str: The detected SBOM type (spdx, cyclonedx, swid) or None.
20+
"""
21+
try:
22+
with open(file_path) as file:
23+
if ".spdx" in file_path:
24+
return "spdx"
25+
26+
elif file_path.endswith(".json"):
27+
data = json.load(file)
28+
if (
29+
"bomFormat" in data
30+
and "specVersion" in data
31+
and data["bomFormat"] == "CycloneDX"
32+
):
33+
return "cyclonedx"
34+
35+
else:
36+
return None
37+
38+
elif file_path.endswith(".xml"):
39+
tree = ET.parse(file_path)
40+
root = tree.getroot()
41+
root_tag = root.tag.split("}")[-1] if "}" in root.tag else root.tag
42+
if root_tag == "bom" and validate_cyclonedx(file_path):
43+
return "cyclonedx"
44+
elif root_tag == "SoftwareIdentity" and validate_swid(file_path):
45+
return "swid"
46+
else:
47+
return None
48+
else:
49+
return None
50+
51+
except (json.JSONDecodeError, ET.ParseError):
52+
return None

test/test_cli.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ def test_EPSS_percentile(self, capsys, caplog):
595595
if my_test_filename_pathlib.exists():
596596
my_test_filename_pathlib.unlink()
597597

598-
@pytest.mark.skip(reason="Temporarily disabled -- may need data changes")
598+
# @pytest.mark.skip(reason="Temporarily disabled -- may need data changes")
599599
def test_SBOM(self, caplog):
600600
# check sbom file option
601601
SBOM_PATH = Path(__file__).parent.resolve() / "sbom"
@@ -617,6 +617,23 @@ def test_SBOM(self, caplog):
617617
"There are 1 products with known CVEs detected",
618618
) in caplog.record_tuples
619619

620+
def test_sbom_detection(self, caplog):
621+
SBOM_PATH = Path(__file__).parent.resolve() / "sbom"
622+
623+
with caplog.at_level(logging.INFO):
624+
main(
625+
[
626+
"cve-bin-tool",
627+
str(SBOM_PATH / "swid_test.xml"),
628+
]
629+
)
630+
631+
assert (
632+
"cve_bin_tool",
633+
logging.INFO,
634+
"Using CVE Binary Tool SBOM Auto Detection",
635+
) in caplog.record_tuples
636+
620637
@pytest.mark.skipif(not LONG_TESTS(), reason="Skipping long tests")
621638
def test_console_output_depending_reportlab_existence(self, caplog):
622639
import subprocess
@@ -701,7 +718,6 @@ def test_console_output_depending_reportlab_existence(self, caplog):
701718
"severity : high",
702719
"input_file : test/test_json.py",
703720
"update : daily",
704-
"sbom : spdx",
705721
"log_level : info",
706722
"nvd_api_key : ",
707723
"offline : False",

test/test_sbom.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pytest
88

99
from cve_bin_tool.input_engine import TriageData
10+
from cve_bin_tool.sbom_detection import sbom_detection
1011
from cve_bin_tool.sbom_manager import Remarks, SBOMManager
1112
from cve_bin_tool.util import ProductInfo
1213

@@ -142,3 +143,23 @@ def test_invalid_xml(self, filename: str, sbom_type: str, validate: bool):
142143
"""
143144
sbom_engine = SBOMManager(filename, sbom_type, validate=validate)
144145
assert sbom_engine.scan_file() == {}
146+
147+
@pytest.mark.parametrize(
148+
"filename, expected_sbom_type",
149+
(
150+
(str(SBOM_PATH / "cyclonedx_test.json"), "cyclonedx"),
151+
(str(SBOM_PATH / "cyclonedx_test2.json"), "cyclonedx"),
152+
(str(SBOM_PATH / "cyclonedx_mixed_test.json"), "cyclonedx"),
153+
(str(SBOM_PATH / "cyclonedx_test.xml"), "cyclonedx"),
154+
(str(SBOM_PATH / "spdx_test.spdx"), "spdx"),
155+
(str(SBOM_PATH / "spdx_test.spdx.rdf"), "spdx"),
156+
(str(SBOM_PATH / "spdx_test.spdx.yaml"), "spdx"),
157+
(str(SBOM_PATH / "spdx_test.spdx.rdf"), "spdx"),
158+
(str(SBOM_PATH / "spdx_test.spdx.yml"), "spdx"),
159+
(str(SBOM_PATH / "spdx_mixed_test.spdx.json"), "spdx"),
160+
(str(SBOM_PATH / "swid_test.xml"), "swid"),
161+
(str(SBOM_PATH / "bad.csv"), None),
162+
),
163+
)
164+
def test_sbom_detection(self, filename: str, expected_sbom_type: str):
165+
assert sbom_detection(filename) == expected_sbom_type

0 commit comments

Comments
 (0)