Skip to content

Commit

Permalink
feat: Adding locations in CycloneDX reports (#3989)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mayankrai449 committed Apr 18, 2024
1 parent 3d1c1b7 commit b03681c
Show file tree
Hide file tree
Showing 23 changed files with 420 additions and 91 deletions.
11 changes: 8 additions & 3 deletions cve_bin_tool/input_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ def decode_bom_ref(self, ref) -> ProductInfo:
urn_cdx = re.compile(
r"urn:cdx:(?P<bomSerialNumber>.*?)\/(?P<bom_version>.*?)#(?P<bom_ref>.*)"
)

location = "location/to/product"
if urn_cbt_ext_ref.match(ref):
urn_dict = urn_cbt_ext_ref.match(ref).groupdict()
vendor = urn_dict["vendor"]
Expand Down Expand Up @@ -290,7 +290,9 @@ def decode_bom_ref(self, ref) -> ProductInfo:

product_info = None
if product is not None and self.validate_product(product):
product_info = ProductInfo(vendor.strip(), product.strip(), version.strip())
product_info = ProductInfo(
vendor.strip(), product.strip(), version.strip(), location
)

return product_info

Expand All @@ -314,7 +316,10 @@ def parse_data(self, fields: Set[str], data: Iterable) -> None:

for row in data:
product_info = ProductInfo(
row["vendor"].strip(), row["product"].strip(), row["version"].strip()
row["vendor"].strip(),
row["product"].strip(),
row["version"].strip(),
row.get("location", "location/to/product").strip(),
)
self.parsed_data[product_info][
row.get("cve_number", "").strip() or "default"
Expand Down
5 changes: 4 additions & 1 deletion cve_bin_tool/merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,10 @@ def parse_data_from_json(

for row in json_data:
product_info = ProductInfo(
row["vendor"].strip(), row["product"].strip(), row["version"].strip()
row["vendor"].strip(),
row["product"].strip(),
row["version"].strip(),
row.get("location", "location/to/product").strip(),
)
parsed_data[product_info][row.get("cve_number", "").strip() or "default"] = {
"remarks": Remarks(str(row.get("remarks", "")).strip()),
Expand Down
6 changes: 6 additions & 0 deletions cve_bin_tool/output_engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def output_csv(
"vendor",
"product",
"version",
"location",
"cve_number",
"severity",
"score",
Expand Down Expand Up @@ -917,6 +918,7 @@ def generate_sbom(
sbom_relationships = []
my_package = SBOMPackage()
sbom_relationship = SBOMRelationship()

# Create root package
my_package.initialise()
root_package = f'CVEBINTOOL-{Path(sbom_root).name.replace(".", "-")}'
Expand All @@ -929,13 +931,15 @@ def generate_sbom(
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()
Expand All @@ -950,6 +954,8 @@ def generate_sbom(
in sbom_packages
and product_data.vendor == "unknown"
):
location = product_data.location
my_package.set_evidence(location) # Set location directly
sbom_packages[
(my_package.get_name(), my_package.get_value("version"))
] = my_package.get_package()
Expand Down
2 changes: 2 additions & 0 deletions cve_bin_tool/output_engine/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ def format_output(
"vendor": "haxx"
"product": "curl",
"version": "1.2.1",
"location": "/usr/local/bin/product",
"cve_number": "CVE-1234-1234",
"severity": "LOW",
"score": "1.2",
Expand Down Expand Up @@ -191,6 +192,7 @@ def format_output(
"vendor": product_info.vendor,
"product": product_info.product,
"version": product_info.version,
"location": product_info.location,
"cve_number": cve.cve_number,
"severity": cve.severity,
"score": str(cve.score),
Expand Down
6 changes: 4 additions & 2 deletions cve_bin_tool/parsers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,20 @@ def find_vendor(self, product, version):
vendor_package_pair = self.cve_db.get_vendor_product_pairs(product)
vendorlist: list[ScanInfo] = []
file_path = self.filename
location = file_path
if vendor_package_pair != []:
# To handle multiple vendors, return all combinations of product/vendor mappings
for v in vendor_package_pair:
vendor = v["vendor"]
location = v.get("location", "/usr/local/bin/product")
self.logger.debug(f"{file_path} {product} {version} by {vendor}")
vendorlist.append(
ScanInfo(ProductInfo(vendor, product, version), file_path)
ScanInfo(ProductInfo(vendor, product, version, location), file_path)
)
else:
# Add entry
vendorlist.append(
ScanInfo(ProductInfo("UNKNOWN", product, version), file_path)
ScanInfo(ProductInfo("UNKNOWN", product, version, location), file_path)
)
return vendorlist

Expand Down
5 changes: 4 additions & 1 deletion cve_bin_tool/parsers/java.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,11 @@ def find_vendor(self, product, version):
for pair in vendor_package_pair:
vendor = pair["vendor"]
file_path = self.filename
location = pair.get("location", "/usr/local/bin/product")
self.logger.debug(f"{file_path} {product} {version} by {vendor}")
info.append(ScanInfo(ProductInfo(vendor, product, version), file_path))
info.append(
ScanInfo(ProductInfo(vendor, product, version, location), file_path)
)
return info
return None

Expand Down
5 changes: 4 additions & 1 deletion cve_bin_tool/parsers/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,12 @@ def run_checker(self, filename):
if vendor_package_pair != []:
for pair in vendor_package_pair:
vendor = pair["vendor"]
location = pair.get("location", "/usr/local/bin/product")
file_path = self.filename
self.logger.debug(f"{file_path} is {vendor}.{product} {version}")
yield ScanInfo(ProductInfo(vendor, product, version), file_path)
yield ScanInfo(
ProductInfo(vendor, product, version, location), file_path
)

# There are packages with a METADATA file in them containing different data from what the tool expects
except AttributeError:
Expand Down
43 changes: 37 additions & 6 deletions cve_bin_tool/sbom_manager/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import re
import sys
from collections import defaultdict
from logging import Logger
from pathlib import Path
Expand All @@ -15,7 +16,12 @@
from cve_bin_tool.cvedb import CVEDB
from cve_bin_tool.input_engine import TriageData
from cve_bin_tool.log import LOGGER
from cve_bin_tool.util import ProductInfo, Remarks
from cve_bin_tool.util import (
ProductInfo,
Remarks,
find_product_location,
validate_location,
)
from cve_bin_tool.validator import validate_cyclonedx, validate_spdx

from .swid_parser import SWIDParser
Expand Down Expand Up @@ -80,10 +86,17 @@ def common_prefix_split(self, product, version) -> list[ProductInfo]:
len(common_prefix_vendor) == 1
and common_prefix_vendor[0] != "UNKNOWN"
):
location = find_product_location(common_prefix_product)
if location is None:
location = "NotFound"
if validate_location(location) is False:
raise ValueError(f"Invalid location {location} for {product}")
found_common_prefix = True
for vendor in common_prefix_vendor:
parsed_data.append(
ProductInfo(vendor, common_prefix_product, version)
ProductInfo(
vendor, common_prefix_product, version, location
)
)
break
if not found_common_prefix:
Expand All @@ -97,8 +110,15 @@ def common_prefix_split(self, product, version) -> list[ProductInfo]:
temp = self.get_vendor(sp)
if len(temp) > 1 or (len(temp) == 1 and temp[0] != "UNKNOWN"):
for vendor in temp:
location = find_product_location(sp)
if location is None:
location = "NotFound"
if validate_location(location) is False:
raise ValueError(
f"Invalid location {location} for {product}"
)
# if vendor is not None:
parsed_data.append(ProductInfo(vendor, sp, version))
parsed_data.append(ProductInfo(vendor, sp, version, location))
return parsed_data

def scan_file(self) -> dict[ProductInfo, TriageData]:
Expand Down Expand Up @@ -139,9 +159,21 @@ def scan_file(self) -> dict[ProductInfo, TriageData]:
vendor_set = self.get_vendor(product)
for vendor in vendor_set:
# if vendor is not None:
parsed_data.append(ProductInfo(vendor, product, version))
location = find_product_location(product)
if location is None:
location = "NotFound"
if validate_location(location) is False:
raise ValueError(f"Invalid location {location} for {product}")
parsed_data.append(ProductInfo(vendor, product, version, location))
else:
parsed_data.append(ProductInfo(module_vendor, product, version))
location = find_product_location(product)
if location is None:
location = "NotFound"
if validate_location(location) is False:
raise ValueError(f"Invalid location {location} for {product}")
parsed_data.append(
ProductInfo(module_vendor, product, version, location)
)

for row in parsed_data:
self.sbom_data[row]["default"] = {
Expand Down Expand Up @@ -357,7 +389,6 @@ def decode_purl(self, purl) -> (str | None, str | None, str | None):


if __name__ == "__main__":
import sys

file = sys.argv[1]
sbom = SBOMManager(file)
Expand Down
12 changes: 9 additions & 3 deletions cve_bin_tool/sbom_manager/cyclonedx_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import defusedxml.ElementTree as ET

from cve_bin_tool.util import find_product_location
from cve_bin_tool.validator import validate_cyclonedx


Expand Down Expand Up @@ -38,7 +39,10 @@ def parse_cyclonedx_json(self, sbom_file: str) -> list[list[str]]:
if d["type"] in self.components_supported:
package = d["name"]
version = d["version"]
modules.append([package, version])
location = find_product_location(package)
if location is None:
location = "NotFound"
modules.append([package, version, location])

return modules

Expand Down Expand Up @@ -68,8 +72,10 @@ def parse_cyclonedx_xml(self, sbom_file: str) -> list[list[str]]:
if component_version is None:
raise KeyError(f"Could not find version in {component}")
version = component_version.text
if version is not None:
modules.append([package, version])
location = find_product_location(package)
if location is None:
location = "NotFound"
modules.append([package, version, location])
return modules


Expand Down
6 changes: 5 additions & 1 deletion cve_bin_tool/sbom_manager/spdx_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import yaml

from cve_bin_tool.log import LOGGER
from cve_bin_tool.util import find_product_location
from cve_bin_tool.validator import validate_spdx


Expand Down Expand Up @@ -61,7 +62,10 @@ def parse_spdx_json(self, sbom_file: str) -> list[list[str]]:
package = d["name"]
try:
version = d["versionInfo"]
modules.append([package, version])
location = find_product_location(package)
if location is None:
location = "NotFound"
modules.append([package, version, location])
except KeyError as e:
LOGGER.debug(e, exc_info=True)

Expand Down
49 changes: 49 additions & 0 deletions cve_bin_tool/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import fnmatch
import os
import re
import sys
from enum import Enum
from pathlib import Path
Expand Down Expand Up @@ -153,11 +154,13 @@ class ProductInfo(NamedTuple):
vendor: str
product: str
version: str
location: str
"""

vendor: str
product: str
version: str
location: str


class ScanInfo(NamedTuple):
Expand Down Expand Up @@ -283,6 +286,52 @@ def make_http_requests(attribute, **kwargs):
LOGGER.error(ve)


def find_product_location(product_name):
"""
Find the location of a product in the system.
Returns the location of the product if found, None otherwise.
"""
for path in sys.path:
product_location = Path(path) / product_name
if product_location.exists():
return str(product_location)
parts = product_name.split("-")
for part in parts:
product_location = Path(path) / part
if product_location.exists():
return str(product_location)

known_installation_directories = [
"/usr/local/bin",
"/usr/local/sbin",
"/usr/bin",
"/opt",
"/usr/sbin",
"/usr/local/lib",
"/usr/lib",
"/usr/local/share",
"/usr/share",
"/usr/local/include",
"/usr/include",
]

for directory in known_installation_directories:
product_location = Path(directory) / product_name
if product_location.exists():
return str(product_location)

return None


def validate_location(location: str) -> bool:
"""
Validates the location.
Returns True if the location is valid, False otherwise.
"""
pattern = r"^(?!https?:\/\/)(?=.*[\\/])(?!.*@)[a-zA-Z0-9_\-\\\/\s]+|NotFound$"
return bool(re.match(pattern, location))


class DirWalk:
"""
for filename in DirWalk('*.c').walk(roots):
Expand Down
Loading

0 comments on commit b03681c

Please sign in to comment.