Skip to content

Commit b893616

Browse files
authored
feat: auto detect for vex and added linkage check (#4415)
* feat: enable auto-detection for vex files * feat: sbom-vex linkage checker for cyclonedx using bom-link * feat: validation for serialNumber
1 parent ec8be1a commit b893616

File tree

6 files changed

+87
-13
lines changed

6 files changed

+87
-13
lines changed

cve_bin_tool/cli.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,7 @@ def main(argv=None):
10251025
total_files: int = 0
10261026
parsed_data: dict[ProductInfo, TriageData] = {}
10271027
vex_product_info: dict[str, str] = {}
1028+
sbom_serial_number = ""
10281029
# Package List parsing
10291030
if args["package_list"]:
10301031
sbom_root = args["package_list"]
@@ -1095,6 +1096,7 @@ def main(argv=None):
10951096
validate=not args["disable_validation_check"],
10961097
)
10971098
parsed_data = sbom_list.parse_sbom()
1099+
sbom_serial_number = sbom_list.serialNumber
10981100
LOGGER.info(
10991101
f"The number of products to process from SBOM - {len(parsed_data)}"
11001102
)
@@ -1103,10 +1105,10 @@ def main(argv=None):
11031105
cve_scanner.get_cves(product_info, triage_data)
11041106

11051107
if args["vex_file"]:
1106-
# for now use cyclonedx as auto detection is not implemented in latest pypi package of lib4vex
1108+
# use auto so that lib4vex can auto-detect the vex type.
11071109
vexdata = VEXParse(
11081110
filename=args["vex_file"],
1109-
vextype="cyclonedx",
1111+
vextype="auto",
11101112
logger=LOGGER,
11111113
)
11121114
parsed_vex_data = vexdata.parse_vex()
@@ -1122,9 +1124,14 @@ def main(argv=None):
11221124
LOGGER.info(
11231125
f"VEX file {args['vex_file']} is not a standalone file and will be used as a triage file"
11241126
)
1125-
# need to do validation on the sbom part
1126-
# need to implement is_linked() function which will check the linkage.
1127-
if args["sbom_file"]:
1127+
# check weather vex is linked with given sbom or not.
1128+
# only check cyclonedx since it have serialNumber.
1129+
if (
1130+
args["sbom_file"]
1131+
and args["sbom"] == "cyclonedx"
1132+
and vexdata.vextype == "cyclonedx"
1133+
and sbom_serial_number not in vexdata.serialNumbers
1134+
):
11281135
LOGGER.warning(
11291136
f"SBOM file: {args['sbom_file']} is not linked to VEX file: {args['vex_file']}."
11301137
)
@@ -1162,6 +1169,7 @@ def main(argv=None):
11621169
"release": args["release"],
11631170
"vendor": args["vendor"],
11641171
"revision_reason": args["revision_reason"],
1172+
"sbom_serial_number": sbom_serial_number,
11651173
}
11661174
elif args["vex_file"]:
11671175
vex_product_info["revision_reason"] = args["revision_reason"]

cve_bin_tool/output_engine/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,7 @@ def output_cves(self, outfile, output_type="console"):
803803
self.vex_type,
804804
self.all_cve_data,
805805
self.vex_product_info["revision_reason"],
806+
self.vex_product_info["sbom_serial_number"],
806807
logger=self.logger,
807808
)
808809
vexgen.generate_vex()

cve_bin_tool/sbom_manager/parse.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
decode_cpe23,
2424
find_product_location,
2525
validate_location,
26+
validate_serialNumber,
2627
)
2728
from cve_bin_tool.validator import validate_cyclonedx, validate_spdx, validate_swid
2829

@@ -58,6 +59,7 @@ def __init__(
5859
self.type = sbom_type
5960
self.logger = logger or LOGGER.getChild(self.__class__.__name__)
6061
self.validate = validate
62+
self.serialNumber = ""
6163

6264
# Connect to the database
6365
self.cvedb = CVEDB(version_check=False)
@@ -253,6 +255,25 @@ def parse_cyclonedx_spdx(self) -> [(str, str, str)]:
253255
sbom_parser = SBOMParser(sbom_type=self.type)
254256
# Load SBOM
255257
sbom_parser.parse_file(self.filename)
258+
doc = sbom_parser.get_document()
259+
uuid = doc.get("uuid", "")
260+
if self.type == "cyclonedx":
261+
parts = uuid.split(":")
262+
if len(parts) == 3 and parts[0] == "urn" and parts[1] == "uuid":
263+
serialNumber = parts[2]
264+
if validate_serialNumber(serialNumber):
265+
self.serialNumber = serialNumber
266+
else:
267+
LOGGER.error(
268+
f"The SBOM file '{self.filename}' has an invalid serial number."
269+
)
270+
return []
271+
else:
272+
LOGGER.error(
273+
f"The SBOM file '{self.filename}' has an invalid serial number."
274+
)
275+
return []
276+
256277
modules = []
257278
if self.validate and self.filename.endswith(".xml"):
258279
# Only for XML files

cve_bin_tool/util.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ def decode_purl(purl: str) -> ProductInfo | None:
391391
return None
392392

393393

394-
def decode_bom_ref(ref: str) -> ProductInfo | None:
394+
def decode_bom_ref(ref: str):
395395
"""
396396
Decodes the BOM reference for each component.
397397
@@ -418,11 +418,29 @@ def decode_bom_ref(ref: str) -> ProductInfo | None:
418418
urn_cdx = re.compile(
419419
r"urn:cdx:(?P<bomSerialNumber>.*?)\/(?P<bom_version>.*?)#(?P<bom_ref>.*)"
420420
)
421+
urn_cdx_with_purl = re.compile(
422+
r"urn:cdx:(?P<bomSerialNumber>[^/]+)\/(?P<bom_version>[^#]+)#(?P<purl>pkg:[^\s]+)"
423+
)
421424
location = "location/to/product"
422-
match = urn_cbt_ext_ref.match(ref) or urn_cbt_ref.match(ref) or urn_cdx.match(ref)
425+
match = (
426+
urn_cdx_with_purl.match(ref)
427+
or urn_cbt_ext_ref.match(ref)
428+
or urn_cbt_ref.match(ref)
429+
or urn_cdx.match(ref)
430+
)
423431
if match:
424432
urn_dict = match.groupdict()
425-
if "bom_ref" in urn_dict: # For urn_cdx match
433+
if "purl" in urn_dict: # For urn_cdx_with_purl match
434+
serialNumber = urn_dict["bomSerialNumber"]
435+
product_info = decode_purl(urn_dict["purl"])
436+
if not validate_serialNumber(serialNumber):
437+
LOGGER.error(
438+
f"The BOM link contains an invalid serial number: '{serialNumber}'"
439+
)
440+
return product_info
441+
else:
442+
return product_info, serialNumber
443+
elif "bom_ref" in urn_dict: # For urn_cdx match
426444
cdx_bom_ref = urn_dict["bom_ref"]
427445
try:
428446
product, version = cdx_bom_ref.rsplit("-", 1)
@@ -466,6 +484,14 @@ def validate_version(version: str) -> bool:
466484
return re.search(cpe_regex, version) is not None
467485

468486

487+
def validate_serialNumber(serialNumber: str) -> bool:
488+
"""
489+
Validates the serial number present in sbom
490+
"""
491+
pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
492+
return re.match(pattern, serialNumber) is not None
493+
494+
469495
class DirWalk:
470496
"""
471497
for filename in DirWalk('*.c').walk(roots):

cve_bin_tool/vex_manager/generate.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def __init__(
4848
vextype: str,
4949
all_cve_data: Dict[ProductInfo, CVEData],
5050
revision_reason: str = "",
51+
sbom_serial_number: str = "",
5152
sbom: Optional[str] = None,
5253
logger: Optional[Logger] = None,
5354
validate: bool = True,
@@ -62,6 +63,7 @@ def __init__(
6263
self.logger = logger or LOGGER.getChild(self.__class__.__name__)
6364
self.validate = validate
6465
self.all_cve_data = all_cve_data
66+
self.sbom_serial_number = sbom_serial_number
6567

6668
def generate_vex(self) -> None:
6769
"""
@@ -155,10 +157,13 @@ def __get_vulnerabilities(self) -> List[Vulnerability]:
155157
else cve.remarks.name
156158
)
157159
# more details will be added using set_value()
158-
bom_version = 1
159-
ref = f"urn:cbt:{bom_version}/{vendor}#{product}:{version}"
160160
if purl is None:
161161
purl = f"pkg:generic/{vendor}/{product}@{version}"
162+
bom_version = 1
163+
if self.sbom_serial_number != "":
164+
ref = f"urn:cdx:{self.sbom_serial_number}/{bom_version}#{purl}"
165+
else:
166+
ref = f"urn:cbt:{bom_version}/{vendor}#{product}:{version}"
162167

163168
vulnerability.set_value("purl", str(purl))
164169
vulnerability.set_value("bom_link", ref)

cve_bin_tool/vex_manager/parse.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class VEXParse:
2020
- vextype (str): The type of VEX file.
2121
- logger: The logger object for logging messages.
2222
- parsed_data: A dictionary to store the parsed data.
23+
- serialNumbers: serialNumbers from the bom_link used to check linkage with sbom.
2324
2425
Methods:
2526
- __init__(self, filename: str, vextype: str, logger=None): Initializes the VEXParse object.
@@ -60,11 +61,16 @@ def __init__(self, filename: str, vextype: str, logger=None):
6061
self.vextype = vextype
6162
self.logger = logger or LOGGER.getChild(self.__class__.__name__)
6263
self.parsed_data = {}
64+
self.serialNumbers = set()
6365

6466
def parse_vex(self) -> DefaultDict[ProductInfo, TriageData]:
6567
"""Parses the VEX file and extracts the necessary fields from the vulnerabilities."""
6668
vexparse = VEXParser(vex_type=self.vextype)
6769
vexparse.parse(self.filename)
70+
if self.vextype == "auto":
71+
self.vextype = vexparse.get_type()
72+
73+
self.logger.info(f"Parsed Vex File: {self.filename} of type: {self.vextype}")
6874
self.logger.debug(f"VEX Vulnerabilities: {vexparse.get_vulnerabilities()}")
6975
self.__process_vulnerabilities(vexparse.get_vulnerabilities())
7076
self.__process_metadata(vexparse.get_metadata())
@@ -101,7 +107,6 @@ def __process_product(self, product) -> None:
101107

102108
def __process_vulnerabilities(self, vulnerabilities) -> None:
103109
""" "processes the vulnerabilities and extracts the necessary fields from the vulnerability."""
104-
# for now cyclonedx is supported with minor tweaks other will be supported later
105110
for vuln in vulnerabilities:
106111
# Extract necessary fields from the vulnerability
107112
cve_id = vuln.get("id")
@@ -110,10 +115,18 @@ def __process_vulnerabilities(self, vulnerabilities) -> None:
110115
response = vuln.get("remediation")
111116
comments = vuln.get("comments")
112117
severity = vuln.get("severity") # Severity is not available in Lib4VEX
113-
# Decode the bom reference for cyclonedx something similar would be done for other formats
118+
# Decode the bom reference for cyclonedx and purl for csaf and openvex
114119
product_info = None
120+
serialNumber = ""
115121
if self.vextype == "cyclonedx":
116-
product_info = decode_bom_ref(vuln.get("bom_link"))
122+
decoded_ref = decode_bom_ref(vuln.get("bom_link"))
123+
if isinstance(decoded_ref, tuple) and not isinstance(
124+
decoded_ref, ProductInfo
125+
):
126+
product_info, serialNumber = decoded_ref
127+
self.serialNumbers.add(serialNumber)
128+
else:
129+
product_info = decoded_ref
117130
elif self.vextype in ["openvex", "csaf"]:
118131
product_info = decode_purl(vuln.get("purl"))
119132
if product_info:

0 commit comments

Comments
 (0)