Skip to content

Commit

Permalink
feat: SBOM Generation (Fixes #1697) (#2817)
Browse files Browse the repository at this point in the history
The start of the journey

* fixes #1697 

Co-authored-by: Terri Oda <terri.oda@intel.com>
  • Loading branch information
anthonyharrison and terriko authored Apr 4, 2023
1 parent 968957d commit 71e528b
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 13 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ For more details, see our [documentation](https://cve-bin-tool.readthedocs.io/en
- [Go](#go)
- [Swift](#swift)
- [Python](#python)
- [SBOM Generation](#sbom-generation)
- [Limitations](#limitations)
- [Requirements](#requirements)
- [Feedback & Contributions](#feedback--contributions)
Expand Down Expand Up @@ -224,6 +225,12 @@ Output:
Lists backported fixes if available from Linux distribution
<a href="https://github.com/intel/cve-bin-tool/blob/main/doc/MANUAL.md#--affected-versions">--affected-versions</a> Lists versions of product affected by a given CVE (to facilitate upgrades)
<a href="https://github.com/intel/cve-bin-tool/blob/main/doc/MANUAL.md#--vex-vex_file">--vex VEX</a> Provide vulnerability exchange (vex) filename
<a href="https://github.com/intel/cve-bin-tool/blob/main/doc/MANUAL.md#--sbom-output-sbom_output">--sbom-output SBOM_OUTPUT</a>
provide software bill of materials (sbom) filename to generate
<a href="https://github.com/intel/cve-bin-tool/blob/main/doc/MANUAL.md#--sbom-type">--sbom-type {spdx,cyclonedx}</a>
specify type of software bill of materials (sbom) to generate (default: spdx)
<a href="https://github.com/intel/cve-bin-tool/blob/main/doc/MANUAL.md#--sbom-format">--sbom-format {tag,json,yaml}</a>
specify format of software bill of materials (sbom) to generate (default: tag)

Merge Report:
Arguments related to Intermediate and Merged Reports
Expand Down Expand Up @@ -478,6 +485,18 @@ The tool supports the scanning of the contents of any Wheel package files (indic

The `--package-list` option can be used with a Python dependencies file `requirements.txt` to find the vulnerabilities in the list of components.

## SBOM Generation

To generate a software bill of materials file (SBOM) ensure these options are included:

```bash
cve-bin-tool --sbom-type <sbom_type> --sbom-format <sbom-format> --sbom-output <sbom_filename> <other scan options as required>
```

Valid SBOM types are [SPDX](https://spdx.dev/specifications/) and [CycloneDX](https://cyclonedx.org/specification/overview/).

The generated SBOM will include product name, version and supplier (where available). License information is not provided.

## Limitations

This scanner does not attempt to exploit issues or examine the code in greater
Expand Down
30 changes: 30 additions & 0 deletions cve_bin_tool/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,26 @@ def main(argv=None):
help="Provide vulnerability exchange (vex) filename",
default="",
)
output_group.add_argument(
"--sbom-output",
action="store",
help="Provide software bill of materials (sbom) filename to generate",
default="",
)
output_group.add_argument(
"--sbom-type",
action="store",
default="spdx",
choices=["spdx", "cyclonedx"],
help="specify type of software bill of materials (sbom) to generate (default: spdx)",
)
output_group.add_argument(
"--sbom-format",
action="store",
default="tag",
choices=["tag", "json", "yaml"],
help="specify format of software bill of materials (sbom) to generate (default: tag)",
)

parser.add_argument(
"-e",
Expand Down Expand Up @@ -783,6 +803,9 @@ def main(argv=None):
if args["exploits"]:
cvedb_orig.get_cache_exploits()

# Root package for generated SBOM. Will be updated to reflect input data
sbom_root = "CVE-SCAN"

with CVEScanner(
score=score,
check_exploits=args["exploits"],
Expand All @@ -795,6 +818,7 @@ def main(argv=None):

# Package List parsing
if args["package_list"]:
sbom_root = args["package_list"]
package_list = PackageListParser(
args["package_list"], error_mode=error_mode
)
Expand Down Expand Up @@ -825,6 +849,7 @@ def main(argv=None):
LOGGER.debug(f"{product_info}, {triage_data}")
cve_scanner.get_cves(product_info, triage_data)
if args["directory"]:
sbom_root = args["directory"]
version_scanner = VersionScanner(
should_extract=args["extract"],
exclude_folders=args["exclude"],
Expand Down Expand Up @@ -856,6 +881,7 @@ def main(argv=None):
cve_scanner = merge_cve_scanner

if args["sbom_file"]:
sbom_root = args["sbom_file"]
# Process SBOM file
sbom_list = SBOMManager(
args["sbom_file"],
Expand Down Expand Up @@ -905,6 +931,10 @@ def main(argv=None):
exploits=args["exploits"],
detailed=args["detailed"],
vex_filename=args["vex"],
sbom_filename=args["sbom_output"],
sbom_type=args["sbom_type"],
sbom_format=args["sbom_format"],
sbom_root=sbom_root,
)

if not args["quiet"]:
Expand Down
83 changes: 83 additions & 0 deletions cve_bin_tool/output_engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
from pathlib import Path
from typing import IO, Any

from lib4sbom.data.package import SBOMPackage
from lib4sbom.data.relationship import SBOMRelationship
from lib4sbom.generator import SBOMGenerator
from lib4sbom.sbom import SBOM

from ..cve_scanner import CVEData
from ..cvedb import CVEDB
from ..error_handler import ErrorHandler, ErrorMode
Expand Down Expand Up @@ -538,6 +543,10 @@ def __init__(
vex_filename: str = "",
exploits: bool = False,
all_product_data=None,
sbom_filename: str = "",
sbom_type: str = "spdx",
sbom_format: str = "tag",
sbom_root: str = "CVE_SBOM",
):
self.logger = logger or LOGGER.getChild(self.__class__.__name__)
self.all_cve_version_info = all_cve_version_info
Expand All @@ -558,6 +567,10 @@ def __init__(
self.vex_filename = vex_filename
self.exploits = exploits
self.all_product_data = all_product_data
self.sbom_filename = sbom_filename
self.sbom_type = sbom_type
self.sbom_format = sbom_format
self.sbom_root = sbom_root

def output_cves(self, outfile, output_type="console"):
"""Output a list of CVEs
Expand Down Expand Up @@ -631,6 +644,14 @@ def output_cves(self, outfile, output_type="console"):

if self.vex_filename != "":
self.generate_vex(self.all_cve_data, self.vex_filename)
if self.sbom_filename != "":
self.generate_sbom(
self.all_product_data,
filename=self.sbom_filename,
sbom_type=self.sbom_type,
sbom_format=self.sbom_format,
sbom_root=self.sbom_root,
)

def generate_vex(self, all_cve_data: dict[ProductInfo, CVEData], filename: str):
analysis_state = {
Expand Down Expand Up @@ -730,6 +751,68 @@ def generate_vex(self, all_cve_data: dict[ProductInfo, CVEData], filename: str):
with open(filename, "w") as outfile:
json.dump(vex_output, outfile, indent=" ")

def generate_sbom(
self,
all_product_data,
filename="",
sbom_type="spdx",
sbom_format="tag",
sbom_root="CVE-SCAN",
):
# Create SBOM
sbom_packages = {}
sbom_relationships = []
my_package = SBOMPackage()
sbom_relationship = SBOMRelationship()
# Create root package
my_package.initialise()
root_package = f'CVEBINTOOL-{Path(sbom_root).name.replace(".","-")}'
parent = f"SBOM_{root_package}"
my_package.set_name(root_package)
my_package.set_type("application")
my_package.set_filesanalysis(False)
my_package.set_downloadlocation(sbom_root)
license = "NOASSERTION"
my_package.set_licensedeclared(license)
my_package.set_licenseconcluded(license)
my_package.set_supplier("UNKNOWN", "NOASSERTION")
# Store package data
sbom_packages[
(my_package.get_name(), my_package.get_value("version"))
] = my_package.get_package()
sbom_relationship.initialise()
sbom_relationship.set_relationship(parent, "DESCRIBES", root_package)
sbom_relationships.append(sbom_relationship.get_relationship())
# Add dependent products
for product_data in all_product_data:
my_package.initialise()
my_package.set_name(product_data.product)
my_package.set_version(product_data.version)
if product_data.vendor != "UNKNOWN":
my_package.set_supplier("Organization", product_data.vendor)
my_package.set_licensedeclared(license)
my_package.set_licenseconcluded(license)
sbom_packages[
(my_package.get_name(), my_package.get_value("version"))
] = my_package.get_package()
sbom_relationship.initialise()
sbom_relationship.set_relationship(
root_package, "DEPENDS_ON", product_data.product
)
sbom_relationships.append(sbom_relationship.get_relationship())

# Generate SBOM
my_sbom = SBOM()
my_sbom.add_packages(sbom_packages)
my_sbom.add_relationships(sbom_relationships)
my_generator = SBOMGenerator(
sbom_type=sbom_type,
format=sbom_format,
application="cve-bin-tool",
version=VERSION,
)
my_generator.generate(parent, my_sbom.get_sbom(), filename=filename)

def output_file_wrapper(self, output_types=["console"]):
for output_type in output_types:
self.output_file(output_type)
Expand Down
10 changes: 7 additions & 3 deletions cve_bin_tool/parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,18 @@ def run_checker(self, filename):
def find_vendor(self, product, version):
vendor_package_pair = self.cve_db.get_vendor_product_pairs(product)
vendorlist: list[ScanInfo] = []
file_path = self.filename
if vendor_package_pair != []:
# To handle multiple vendors, return all combinations of product/vendor mappings
for v in vendor_package_pair:
vendor = v["vendor"]
file_path = self.filename
self.logger.debug(f"{file_path} {product} {version} by {vendor}")
vendorlist.append(
ScanInfo(ProductInfo(vendor, product, version), file_path)
)
return vendorlist if len(vendorlist) > 0 else None
return None
else:
# Add entry
vendorlist.append(
ScanInfo(ProductInfo("UNKNOWN", product, version), file_path)
)
return vendorlist
18 changes: 12 additions & 6 deletions cve_bin_tool/sbom_manager/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ def scan_file(self) -> dict[ProductInfo, TriageData]:
if version != "":
# Now add vendor to create product record....
# print (f"Find vendor for {product} {version}")
vendor = self.get_vendor(product)
if vendor is not None:
vendor_set = self.get_vendor(product)
for vendor in vendor_set:
# if vendor is not None:
parsed_data.append(ProductInfo(vendor, product, version))
# print(vendor,product,version)

Expand All @@ -88,12 +89,17 @@ def scan_file(self) -> dict[ProductInfo, TriageData]:
LOGGER.debug(f"SBOM Data {self.sbom_data}")
return self.sbom_data

def get_vendor(self, product: str) -> str | None:
def get_vendor(self, product: str) -> list:
vendorlist: list[str] = []
vendor_package_pair = self.cvedb.get_vendor_product_pairs(product)
if vendor_package_pair != []:
vendor = vendor_package_pair[0]["vendor"]
return vendor
return None
# To handle multiple vendors, return all combinations of product/vendor mappings
for v in vendor_package_pair:
vendor = v["vendor"]
vendorlist.append(vendor)
else:
vendorlist.append("UNKNOWN")
return vendorlist


if __name__ == "__main__":
Expand Down
39 changes: 39 additions & 0 deletions doc/MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
- [-b [<distro_name>-<distro_version_name>], --backport-fix [<distro_name>-<distro_version_name>]](#-b-distro_name-distro_version_name---backport-fix-distro_name-distro_version_name)
- [--affected-versions](#--affected-versions)
- [--vex VEX_FILE](#--vex-vex_file)
- [--sbom-output SBOM_OUTPUT](#--sbom-output-sbom_output)
- [--sbom-type {spdx,cyclonedx}](#--sbom-type)
- [--sbom-format {tag,json,yaml}](#--sbom-format)
- [Output verbosity](#output-verbosity)
- [Quiet Mode](#quiet-mode)
- [Logging modes](#logging-modes)
Expand Down Expand Up @@ -128,6 +131,12 @@ which is useful if you're trying the latest code from
Lists backported fixes if available from Linux distribution
--affected-versions Lists versions of product affected by a given CVE (to facilitate upgrades)
--vex VEX Provide vulnerability exchange (vex) filename
--sbom-output SBOM_OUTPUT
provide software bill of materials (sbom) filename to generate
--sbom-type {spdx,cyclonedx}
specify type of software bill of materials (sbom) to generate (default: spdx)
--sbom-format {tag,json,yaml}
specify format of software bill of materials (sbom) to generate (default: tag)

Merge Report:
Arguments related to Intermediate and Merged Reports
Expand Down Expand Up @@ -948,6 +957,36 @@ file which contains all the reported vulnerabilities detected by the scan. This
updated (outside of the CVE Binary tool) to record the results of a triage activity
and can be used as a file with `--input-file` parameter.

### --sbom-output SBOM_OUTPUT

This option allows you to specify the filename for a Software Bill of Material (SBOM) file which contains all of the
components detected by the scan. The generated file can be used as a subsequent input to the CVE Binary tool with `--sbom-file` parameter.

It is recommended that the following filename conventions are followed in combination with the `--sbom-type` and `--sbom-format` parameters.

| SBOM Type | SBOM Format | Filename extension |
|-----------|-------------| ---------------|
| SPDX | TagValue | .spdx |
| SPDX | JSON | .spdx.json |
| SPDX | YAML | .spdx.yaml |
| SPDX | YAML | .spdx.yml |
| CycloneDX | JSON | .json |

### --sbom-type

This option is used in combination with the `--sbom-output` parameter and allows you to specify the
type of Software Bill of Material (SBOM) to be generated. SBOMs can be generated in either [SPDX](https://www.spdx.org)
or [CycloneDX](https://www.cyclonedx.org) formats.

If this option is not specified, an SPDX SBOM will be generated.

### --sbom-format

This option is used in combination with the `--sbom-output` and `--sbom-type` parameters and allows you to specify the
format of Software Bill of Material (SBOM) to be generated. TagValue, JSON and YAML formats are supported for SPDX SBOMs; all CycloneDX SBOMS are generated in JSON format.

If this option is not specified, the SBOM will be generated in TagValue format (SPDX) or JSON (CycloneDX) formats.

### Output verbosity

As well as the modes above, there are two other output options to decrease or increase the number of messages printed:
Expand Down
3 changes: 2 additions & 1 deletion requirements.csv
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ python,urllib3
google,gsutil
skontar,cvss
python_not_in_db,packaging
python_not_in_db,importlib_resources
python_not_in_db,importlib_resources
python_not_in_db,lib4sbom
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ gsutil
cvss
packaging<22.0
importlib_resources; python_version < "3.9"
lib4sbom>=0.3.0
12 changes: 9 additions & 3 deletions test/test_sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ def test_valid_spdx_file(
self, filename: str, spdx_parsed_data: dict[ProductInfo, TriageData]
):
sbom_engine = SBOMManager(filename, sbom_type="spdx")
assert sbom_engine.scan_file() == spdx_parsed_data
scan_result = sbom_engine.scan_file()
for p in spdx_parsed_data:
assert p in scan_result

@pytest.mark.parametrize(
"filename, cyclonedx_parsed_data",
Expand All @@ -103,7 +105,9 @@ def test_valid_cyclonedx_file(
self, filename: str, cyclonedx_parsed_data: dict[ProductInfo, TriageData]
):
sbom_engine = SBOMManager(filename, sbom_type="cyclonedx")
assert sbom_engine.scan_file() == cyclonedx_parsed_data
scan_result = sbom_engine.scan_file()
for p in cyclonedx_parsed_data:
assert p in scan_result

@pytest.mark.parametrize(
"filename, swid_parsed_data",
Expand All @@ -113,7 +117,9 @@ def test_valid_swid_file(
self, filename: str, swid_parsed_data: dict[ProductInfo, TriageData]
):
sbom_engine = SBOMManager(filename, sbom_type="swid")
assert sbom_engine.scan_file() == swid_parsed_data
scan_result = sbom_engine.scan_file()
for p in swid_parsed_data:
assert p in scan_result

@pytest.mark.parametrize(
"filename, sbom_type, validate",
Expand Down

0 comments on commit 71e528b

Please sign in to comment.