@@ -34,6 +34,13 @@ CVE_VERSION ??= "${PV}"
# Possible database sources: NVD1, NVD2, FKIE
NVD_DB_VERSION ?= "FKIE"
+# CVE Check using kernel CNA and compiled files
+CVE_CHECK_KERNEL ?= "1"
+# CVE Check using kernel compiled files
+CVE_CHECK_KERNEL_CONFIG ?= "0"
+# Location of the linux-vulns data
+CVE_CHECK_KERNEL_DB_DIR ?= "${DL_DIR}/CVE_CHECK/vulns"
+
# Use different file names for each database source, as they synchronize at different moments, so may be slightly different
CVE_CHECK_DB_FILENAME ?= "${@'nvdcve_2-2.db' if d.getVar('NVD_DB_VERSION') == 'NVD2' else 'nvdcve_1-3.db' if d.getVar('NVD_DB_VERSION') == 'NVD1' else 'nvdfkie_1-1.db'}"
CVE_CHECK_DB_FETCHER ?= "${@'cve-update-nvd2-native' if d.getVar('NVD_DB_VERSION') == 'NVD2' else 'cve-update-db-native'}"
@@ -111,6 +118,9 @@ python () {
if nvd_database_type not in ("NVD1", "NVD2", "FKIE"):
bb.erroronce("Malformed NVD_DB_VERSION, must be one of: NVD1, NVD2, FKIE. Defaulting to NVD2")
d.setVar("NVD_DB_VERSION", "NVD2")
+
+ from oe.cve_check import extend_cve_kernel_config
+ extend_cve_kernel_config(d, "do_cve_check")
}
def generate_json_report(d, out_path, link_path):
@@ -161,15 +171,24 @@ python do_cve_check () {
"""
Check recipe for patched and unpatched CVEs
"""
- from oe.cve_check import get_patched_cves
+ from oe.cve_check import get_patched_cves, get_kernel_cves
with bb.utils.fileslocked([d.getVar("CVE_CHECK_DB_FILE_LOCK")], shared=True):
+ cve_data = {}
+ # Add all reported CVES from linux-vulns
+ cve_check_kernel = d.getVar("CVE_CHECK_KERNEL")
+ if "linux_kernel" in d.getVar("CVE_PRODUCT") and cve_check_kernel == "1":
+ kernel_unpatched_cves, kernel_patched_cves = get_kernel_cves(d)
+ cve_data.update(kernel_patched_cves)
+ cve_data.update(kernel_unpatched_cves)
if os.path.exists(d.getVar("CVE_CHECK_DB_FILE")):
try:
patched_cves = get_patched_cves(d)
+ # Update cve_data, this will cover the manually reported with CVE_STATUS
+ cve_data.update(patched_cves)
except FileNotFoundError:
bb.fatal("Failure in searching patches")
- cve_data, status = check_cves(d, patched_cves)
+ cve_data, status = check_cves(d, cve_data)
if len(cve_data) or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
get_cve_info(d, cve_data)
cve_write_data(d, cve_data, status)
@@ -41,6 +41,9 @@ SPDX_MULTILIB_SSTATE_ARCHS ??= "${SSTATE_ARCHS}"
python () {
from oe.cve_check import extend_cve_status
extend_cve_status(d)
+
+ from oe.cve_check import extend_cve_kernel_config
+ extend_cve_kernel_config(d, "do_create_spdx")
}
def create_spdx_source_deps(d):
@@ -26,6 +26,14 @@
CVE_PRODUCT ??= "${BPN}"
CVE_VERSION ??= "${PV}"
+CVE_CHECK_KERNEL_DB_DIR ?= "${DL_DIR}/CVE_CHECK/vulns"
+# CVE Check using kernel CNA and compiled files
+CVE_CHECK_KERNEL ?= "1"
+# CVE Check using kernel compiled files
+CVE_CHECK_KERNEL_CONFIG ?= "0"
+# Location of the linux-vulns data
+CVE_CHECK_KERNEL_DB_DIR ?= "${DL_DIR}/CVE_CHECK/vulns"
+
CVE_CHECK_SUMMARY_DIR ?= "${LOG_DIR}/cve"
CVE_CHECK_SUMMARY_FILE_NAME_JSON = "cve-summary.json"
@@ -78,6 +86,9 @@ python () {
from oe.cve_check import extend_cve_status
extend_cve_status(d)
+
+ from oe.cve_check import extend_cve_kernel_config
+ extend_cve_kernel_config(d, "do_generate_vex")
}
def generate_json_report(d, out_path, link_path):
@@ -47,6 +47,7 @@ class Version():
self._version.pre_l,
self._version.pre_v
)
+ self.version = version
def __eq__(self, other):
if not isinstance(other, Version):
@@ -58,6 +59,9 @@ class Version():
return NotImplemented
return self._key > other._key
+ def __str__(self) -> str:
+ return self.version
+
def _cmpkey(release, patch_l, pre_l, pre_v):
# remove leading 0
_release = tuple(
@@ -202,8 +206,14 @@ def get_patched_cves(d):
"affected-product": decoded_status["product"],
}
- return patched_cves
+ # If we are parsing the kernel, check compiled files
+ cve_check_kernel = d.getVar("CVE_CHECK_KERNEL")
+ if "linux_kernel" in d.getVar("CVE_PRODUCT") and cve_check_kernel == "1":
+ bb.debug(1, "Checking kernel CVEs")
+ _kernel_unpatched_cves, kernel_patched_cves = get_kernel_cves(d)
+ patched_cves.update(kernel_patched_cves)
+ return patched_cves
def get_cpe_ids(cve_product, version):
"""
@@ -376,3 +386,202 @@ def extend_cve_status(d):
d.setVarFlag("CVE_STATUS", cve, d.getVarFlag(cve_status_group, "status"))
else:
bb.warn("CVE_STATUS_GROUPS contains undefined variable %s" % cve_status_group)
+
+def extend_cve_kernel_config(d, task):
+ pn = d.getVar('PN')
+ # For kernel CVEs, add required dependencies
+ if "linux_kernel" in d.getVar("CVE_PRODUCT"):
+ if d.getVar("CVE_CHECK_KERNEL_CONFIG") == "1":
+ bb.debug(1, "Checking kernel CVEs using kernel config")
+ depends = f" {pn}:do_save_compiled_files "
+ d.appendVarFlag(task, "depends", depends)
+ d.setVar('CVE_CHECK_KERNEL','1')
+ d.setVar('SPDX_INCLUDE_COMPILED_SOURCE','1')
+ if d.getVar("CVE_CHECK_KERNEL") == "1":
+ bb.debug(1, "Checking kernel CVEs using linux-vulns")
+ d.appendVarFlag(task, "depends", " linux-vulns:do_fetch ")
+
+def get_kernel_cves(d):
+ """
+ Get CVEs for the kernel
+ """
+ import glob
+ import json
+ patched_cves = {}
+ unpatched_cves = {}
+ datadir = f"{d.getVar('CVE_CHECK_KERNEL_DB_DIR')}/cve/published/"
+ version_str = d.getVar("LINUX_VERSION")
+ check_config = d.getVar("CVE_CHECK_KERNEL_CONFIG")
+ version = Version(version_str)
+ base_version = Version(".".join(version_str.split(".")[0:2]))
+
+ # Check all CVES from kernel vulns
+ pattern = os.path.join(datadir, '**', f"CVE-*.json")
+ cve_files = glob.glob(pattern, recursive=True)
+ not_applicable_config = 0
+ fixed_as_later_backport = 0
+ for cve_file in cve_files:
+ cve_info = {}
+ with open(cve_file, "r") as f:
+ cve_info = json.load(f)
+
+ if len(cve_info) == 0:
+ bb.error(f"Not valid data in {cve_file}. Aborting")
+ break
+ cve_id = cve_info["cveMetadata"]["cveID"]
+
+ first_affected, fixed, backport_ver = get_kernel_fixed_versions(cve_info, base_version)
+ if not fixed:
+ if check_config == "1":
+ is_affected, affected_files = check_kernel_compiled_files(d, cve_info)
+ else:
+ is_affected = True
+ if not is_affected and len(affected_files) > 0:
+ bb.debug(1, f"{cve_id} - not applicable configuration since affected files not compiled: {affected_files}")
+ patched_cves[cve_id] = {
+ "abbrev-status": "Ignored",
+ "status": "not-applicable-config",
+ "justification": f"Source code not compiled by config. {affected_files}"
+ }
+ else:
+ bb.debug(1, f"{cve_id} not fixed usptream")
+ description = cve_info["containers"]["cna"]["descriptions"][0]["value"]
+ unpatched_cves[cve_id] = {
+ "abbrev-status": "Unpatched",
+ "status": "version-in-range",
+ "summary": description,
+ "justification": f"No fix available upstream"
+ }
+ elif first_affected and version < first_affected:
+ bb.debug(1, f'{cve_id} - "fixed-version: only affects {first_affected} onwards"')
+ patched_cves[cve_id] = {
+ "abbrev-status": "Patched",
+ "status": "fixed-version",
+ "justification": f"only affects {first_affected} onwards"
+ }
+ elif fixed <= version:
+ bb.debug(1, f'{cve_id} - "fixed-version: Fixed from version {fixed}"')
+ patched_cves[cve_id] = {
+ "abbrev-status": "Patched",
+ "status": "fixed-version",
+ "justification": f"fixed-version: Fixed from version {fixed}"
+ }
+ else:
+ if backport_ver:
+ if backport_ver <= version:
+ bb.debug(1, f'{cve_id} - "cpe-stable-backport: Backported in {backport_ver}"')
+ patched_cves[cve_id] = {
+ "abbrev-status": "Patched",
+ "status": "cpe-stable-backport",
+ "justification": f"Backported in {backport_ver}"
+ }
+ else:
+ bb.debug(1, f"{cve_id}: needs backporting (fixed from {backport_ver})")
+ description = cve_info["containers"]["cna"]["descriptions"][0]["value"]
+ unpatched_cves[cve_id] = {
+ "abbrev-status": "Unpatched",
+ "status": "version-in-range",
+ "summary": description,
+ "justification": f"Needs backporting (fixed from {backport_ver})"
+ }
+ fixed_as_later_backport += 1
+ else:
+ # Check if file affected
+ if check_config == "1":
+ is_affected, affected_files = check_kernel_compiled_files(d, cve_info)
+ else:
+ is_affected = True
+ if not is_affected and len(affected_files) > 0:
+ bb.debug(1, f"{cve_id} - not applicable configuration since affected files not compiled: {affected_files}")
+ patched_cves[cve_id] = {
+ "abbrev-status": "Ignored",
+ "status": "not-applicable-config",
+ "justification": f"Source code not compiled by config. {affected_files}"
+ }
+ not_applicable_config +=1
+ else:
+ bb.debug(1, f"{cve_id}: needs backporting (fixed from {fixed})")
+ description = cve_info["containers"]["cna"]["descriptions"][0]["value"]
+ unpatched_cves[cve_id] = {
+ "abbrev-status": "Unpatched",
+ "status": "version-in-range",
+ "summary": description,
+ "justification": f"Needs backporting (fixed from {fixed})"
+ }
+ if len(cve_files) > 0:
+ bb.debug(1, f"Total CVEs ignored due to not applicable config {not_applicable_config}")
+ bb.debug(1, f"Total vulnerable CVEs: {len(unpatched_cves)}")
+ bb.debug(1, f"Total CVEs already backported in {base_version}: {fixed_as_later_backport}")
+ return unpatched_cves, patched_cves
+
+def check_kernel_compiled_files(d, cve_info):
+ """
+ Return if a CVE affected us depending on compiled files
+ """
+ import json
+ files_affected = []
+ kfiles = []
+ is_affected = False
+ with open(d.getVar('KERNEL_SRC_FILES'), 'r') as file:
+ for item in json.load(file):
+ kfiles.append(item['file'].replace(f"{d.getVar('S')}/",""))
+
+ for item in cve_info['containers']['cna']['affected']:
+ if item["defaultStatus"] == "affected":
+ if "programFiles" in item:
+ files = item['programFiles']
+ files_affected.extend(files)
+
+ if len(files_affected) > 0:
+ for f in files_affected:
+ if f in kfiles:
+ bb.debug(1, f"File match: {f}")
+ is_affected = True
+ return is_affected, files_affected
+
+def get_kernel_fixed_versions(cve_info, base_version):
+ '''
+ Get fixed versions for a given CVE
+ '''
+ first_affected = None
+ fixed = None
+ fixed_backport = None
+ next_version = Version(str(base_version) + ".5000")
+ for affected in cve_info["containers"]["cna"]["affected"]:
+ # In case the CVE info is not complete, it might not have default status and therefore
+ # we don't know the status of this CVE.
+ if not "defaultStatus" in affected:
+ return first_affected, fixed, fixed_backport
+ if affected["defaultStatus"] == "affected":
+ for version in affected["versions"]:
+ v = Version(version["version"])
+ if v == 0:
+ # Skiping non-affected
+ continue
+ if version["status"] == "affected" and not first_affected:
+ first_affected = v
+ elif (version["status"] == "unaffected" and
+ version['versionType'] == "original_commit_for_fix"):
+ fixed = v
+ elif base_version < v and v < next_version:
+ fixed_backport = v
+ elif affected["defaultStatus"] == "unaffected":
+ # Only specific versions are affected. We care only about our base version
+ if "versions" not in affected:
+ continue
+ for version in affected["versions"]:
+ if "versionType" not in version:
+ continue
+ if version["versionType"] == "git":
+ continue
+ v = Version(version["version"])
+ # in case it is not in our base version
+ less_than = Version(version["lessThan"])
+
+ if not first_affected:
+ first_affected = v
+ fixed = less_than
+ if base_version < v and v < next_version:
+ fixed_backport = less_than
+
+ return first_affected, fixed, fixed_backport