1#! /usr/bin/env python3 2 3# Generate granular CVE status metadata for a specific version of the kernel 4# using json data from cvelistV5 or vulns repository 5# 6# SPDX-License-Identifier: GPL-2.0-only 7 8import argparse 9import datetime 10import json 11import pathlib 12import os 13import glob 14import subprocess 15 16from packaging.version import Version 17 18 19def parse_version(s): 20 """ 21 Parse the version string and either return a packaging.version.Version, or 22 None if the string was unset or "unk". 23 """ 24 if s and s != "unk": 25 # packaging.version.Version doesn't approve of versions like v5.12-rc1-dontuse 26 s = s.replace("-dontuse", "") 27 return Version(s) 28 return None 29 30def get_fixed_versions(cve_info, base_version): 31 ''' 32 Get fixed versionss 33 ''' 34 first_affected = None 35 fixed = None 36 fixed_backport = None 37 next_version = Version(str(base_version) + ".5000") 38 for affected in cve_info["containers"]["cna"]["affected"]: 39 # In case the CVE info is not complete, it might not have default status and therefore 40 # we don't know the status of this CVE. 41 if not "defaultStatus" in affected: 42 return first_affected, fixed, fixed_backport 43 if affected["defaultStatus"] == "affected": 44 for version in affected["versions"]: 45 v = Version(version["version"]) 46 if v == Version('0'): 47 #Skiping non-affected 48 continue 49 if version["status"] == "unaffected" and first_affected and v < first_affected: 50 first_affected = Version(f"{v.major}.{v.minor}") 51 if version["status"] == "affected" and not first_affected: 52 first_affected = v 53 elif (version["status"] == "unaffected" and 54 version['versionType'] == "original_commit_for_fix"): 55 fixed = v 56 elif base_version < v and v < next_version: 57 fixed_backport = v 58 elif affected["defaultStatus"] == "unaffected": 59 # Only specific versions are affected. We care only about our base version 60 if "versions" not in affected: 61 continue 62 for version in affected["versions"]: 63 if "versionType" not in version: 64 continue 65 if version["versionType"] == "git": 66 continue 67 v = Version(version["version"]) 68 # in case it is not in our base version 69 less_than = Version(version["lessThan"]) 70 71 if not first_affected: 72 first_affected = v 73 fixed = less_than 74 if base_version < v and v < next_version: 75 fixed_backport = less_than 76 77 return first_affected, fixed, fixed_backport 78 79def is_linux_cve(cve_info): 80 '''Return true is the CVE belongs to Linux''' 81 if not "affected" in cve_info["containers"]["cna"]: 82 return False 83 for affected in cve_info["containers"]["cna"]["affected"]: 84 if not "product" in affected: 85 return False 86 if affected["product"] == "Linux" and affected["vendor"] == "Linux": 87 return True 88 return False 89 90def main(argp=None): 91 parser = argparse.ArgumentParser() 92 parser.add_argument("datadir", type=pathlib.Path, help="Path to a clone of https://github.com/CVEProject/cvelistV5 or https://git.kernel.org/pub/scm/linux/security/vulns.git") 93 parser.add_argument("version", type=Version, help="Kernel version number to generate data for, such as 6.1.38") 94 95 args = parser.parse_args(argp) 96 datadir = args.datadir.resolve() 97 version = args.version 98 base_version = Version(f"{version.major}.{version.minor}") 99 100 data_version = subprocess.check_output(("git", "describe", "--tags", "HEAD"), cwd=datadir, text=True) 101 102 print(f""" 103# Auto-generated CVE metadata, DO NOT EDIT BY HAND. 104# Generated at {datetime.datetime.now(datetime.timezone.utc)} for kernel version {version} 105# From {datadir.name} {data_version} 106 107python check_kernel_cve_status_version() {{ 108 this_version = "{version}" 109 kernel_version = d.getVar("LINUX_VERSION") 110 if kernel_version != this_version: 111 bb.warn("Kernel CVE status needs updating: generated for %s but kernel is %s" % (this_version, kernel_version)) 112}} 113do_cve_check[prefuncs] += "check_kernel_cve_status_version" 114""") 115 116 # Loop though all CVES and check if they are kernel related, newer than 2015 117 pattern = os.path.join(datadir, '**', "CVE-20*.json") 118 119 files = glob.glob(pattern, recursive=True) 120 for cve_file in sorted(files): 121 # Get CVE Id 122 cve = cve_file[cve_file.rfind("/")+1:cve_file.rfind(".json")] 123 # We process from 2015 data, old request are not properly formated 124 year = cve.split("-")[1] 125 if int(year) < 2015: 126 continue 127 with open(cve_file, 'r', encoding='utf-8') as json_file: 128 cve_info = json.load(json_file) 129 130 if not is_linux_cve(cve_info): 131 continue 132 first_affected, fixed, backport_ver = get_fixed_versions(cve_info, base_version) 133 if not fixed: 134 print(f"# {cve} has no known resolution") 135 elif first_affected and version < first_affected: 136 print(f'CVE_STATUS[{cve}] = "fixed-version: only affects {first_affected} onwards"') 137 elif fixed <= version: 138 print( 139 f'CVE_STATUS[{cve}] = "fixed-version: Fixed from version {fixed}"' 140 ) 141 else: 142 if backport_ver: 143 if backport_ver <= version: 144 print( 145 f'CVE_STATUS[{cve}] = "cpe-stable-backport: Backported in {backport_ver}"' 146 ) 147 else: 148 print(f"# {cve} may need backporting (fixed from {backport_ver})") 149 else: 150 print(f"# {cve} needs backporting (fixed from {fixed})") 151 152 print() 153 154 155if __name__ == "__main__": 156 main() 157