diff --git a/src/sbomnix/cdx.py b/src/sbomnix/cdx.py index 3cdc3aa..1b9beb1 100644 --- a/src/sbomnix/cdx.py +++ b/src/sbomnix/cdx.py @@ -4,12 +4,17 @@ # # SPDX-License-Identifier: Apache-2.0 -""" CycloneDX utils """ +################################################################################ +# CycloneDX + +""" Module for generating cdx entries """ import re + from reuse._licenses import LICENSE_MAP as SPDX_LICENSES from common.utils import LOG, LOG_SPAM +from vulnxscan.utils import _vuln_source, _vuln_url def _drv_to_cdx_licenses_entry(drv, column_name, cdx_license_type): @@ -128,3 +133,27 @@ def _drv_to_cdx_dependency(drv, deps_list, uid="store_path"): if deps_list: dependency["dependsOn"] = deps_list return dependency + + +def _vuln_to_cdx_vuln(vuln): + """Return cdx vulnerability entry from vulnix row""" + vulnerability = {} + vulnerability["bom-ref"] = vuln.store_path + vulnerability["id"] = vuln.vuln_id + source = {} + source["url"] = _vuln_url(vuln) + source["name"] = _vuln_source(vuln) + vulnerability["source"] = source + vulnerability["ratings"] = [] + # If the vulnerability is still being assessed, it will be missing a valid number + if vuln.severity != "": + rating = {} + rating["source"] = source + rating["score"] = vuln.severity + vulnerability["ratings"].append(rating) + vulnerability["tools"] = [] + for scanner in vuln.scanner: + tool = {} + tool["name"] = scanner + vulnerability["tools"].append(tool) + return vulnerability diff --git a/src/sbomnix/sbomdb.py b/src/sbomnix/sbomdb.py index abac19b..164d179 100644 --- a/src/sbomnix/sbomdb.py +++ b/src/sbomnix/sbomdb.py @@ -4,10 +4,11 @@ # # SPDX-License-Identifier: Apache-2.0 -# pylint: disable=too-many-instance-attributes, too-many-arguments +# pylint: disable=too-many-instance-attributes, too-many-arguments, too-many-locals """ Module for generating SBOMs in various formats """ +from tempfile import NamedTemporaryFile import uuid import logging import json @@ -18,10 +19,11 @@ import numpy as np from reuse._licenses import LICENSE_MAP as SPDX_LICENSES from nixgraph.graph import NixDependencies -from sbomnix.cdx import _drv_to_cdx_component, _drv_to_cdx_dependency +from sbomnix.cdx import _drv_to_cdx_component, _drv_to_cdx_dependency, _vuln_to_cdx_vuln from sbomnix.nix import Store, find_deriver from sbomnix.meta import Meta from common.utils import LOG, df_to_csv_file, get_py_pkg_version +from vulnxscan.vulnscan import VulnScan ############################################################################### @@ -168,7 +170,7 @@ def to_cdx(self, cdx_path, printinfo=True): """Export sbomdb to cyclonedx json file""" cdx = {} cdx["bomFormat"] = "CycloneDX" - cdx["specVersion"] = "1.3" + cdx["specVersion"] = "1.4" cdx["version"] = 1 cdx["serialNumber"] = f"urn:uuid:{self.uuid}" cdx["metadata"] = {} @@ -202,6 +204,39 @@ def to_cdx(self, cdx_path, printinfo=True): deps = self._lookup_dependencies(drv, uid=self.uid) dependency = _drv_to_cdx_dependency(drv, deps, uid=self.uid) cdx["dependencies"].append(dependency) + scanner = VulnScan() + scanner.scan_vulnix(self.target_deriver, self.buildtime) + # Write incomplete sbom to a temporary path, then perform a vulnerability scan + with NamedTemporaryFile( + delete=False, prefix="vulnxscan_", suffix=".json" + ) as fcdx: + self._write_json(fcdx.name, cdx, printinfo=False) + scanner.scan_grype(fcdx.name) + scanner.scan_osv(fcdx.name) + cdx["vulnerabilities"] = [] + # Union all scans into a single dataframe + vulns = pd.concat( + [scanner.df_grype, scanner.df_osv, scanner.df_vulnix], + ignore_index=True, + ) + # Concat adds a modified column, remove + vulns.drop("modified", axis=1, inplace=True) + # Deduplicate repeated vulnerabilities, making the scanner column into an array + vuln_grouped = vulns.groupby( + ["package", "version", "severity", "vuln_id"], + as_index=False, + ).agg({"scanner": pd.Series.unique}) + # Do a join so we have access to bom-ref + vulnix_components = pd.merge( + left=vuln_grouped, + right=self.df_sbomdb, + how="left", + left_on=["package", "version"], + right_on=["pname", "version"], + ) + for vuln in vulnix_components.itertuples(): + vulnix_vuln = _vuln_to_cdx_vuln(vuln) + cdx["vulnerabilities"].append(vulnix_vuln) self._write_json(cdx_path, cdx, printinfo) def to_spdx(self, spdx_path, printinfo=True): diff --git a/src/vulnxscan/utils.py b/src/vulnxscan/utils.py index 7982d77..0619ab5 100644 --- a/src/vulnxscan/utils.py +++ b/src/vulnxscan/utils.py @@ -63,11 +63,19 @@ def _vuln_url(row): nvd_url = "https://nvd.nist.gov/vuln/detail/" if "cve" in row.vuln_id.lower(): return f"{nvd_url}{row.vuln_id}" - if row.osv: + if getattr(row, "osv", False) or ("osv" in getattr(row, "scanner", [])): return f"{osv_url}{row.vuln_id}" return "" +def _vuln_source(row): + if "cve" in row.vuln_id.lower(): + return "NVD" + if getattr(row, "osv", False) or ("osv" in getattr(row, "scanner", [])): + return "OSV" + return "" + + def _is_patched(row): if row.vuln_id and str(row.vuln_id).lower() in str(row.patches).lower(): patches = row.patches.split()