1*4882a593Smuzhiyunimport collections 2*4882a593Smuzhiyunimport re 3*4882a593Smuzhiyunimport itertools 4*4882a593Smuzhiyunimport functools 5*4882a593Smuzhiyun 6*4882a593Smuzhiyun_Version = collections.namedtuple( 7*4882a593Smuzhiyun "_Version", ["release", "patch_l", "pre_l", "pre_v"] 8*4882a593Smuzhiyun) 9*4882a593Smuzhiyun 10*4882a593Smuzhiyun@functools.total_ordering 11*4882a593Smuzhiyunclass Version(): 12*4882a593Smuzhiyun 13*4882a593Smuzhiyun def __init__(self, version, suffix=None): 14*4882a593Smuzhiyun 15*4882a593Smuzhiyun suffixes = ["alphabetical", "patch"] 16*4882a593Smuzhiyun 17*4882a593Smuzhiyun if str(suffix) == "alphabetical": 18*4882a593Smuzhiyun version_pattern = r"""r?v?(?:(?P<release>[0-9]+(?:[-\.][0-9]+)*)(?P<patch>[-_\.]?(?P<patch_l>[a-z]))?(?P<pre>[-_\.]?(?P<pre_l>(rc|alpha|beta|pre|preview|dev))[-_\.]?(?P<pre_v>[0-9]+)?)?)(.*)?""" 19*4882a593Smuzhiyun elif str(suffix) == "patch": 20*4882a593Smuzhiyun version_pattern = r"""r?v?(?:(?P<release>[0-9]+(?:[-\.][0-9]+)*)(?P<patch>[-_\.]?(p|patch)(?P<patch_l>[0-9]+))?(?P<pre>[-_\.]?(?P<pre_l>(rc|alpha|beta|pre|preview|dev))[-_\.]?(?P<pre_v>[0-9]+)?)?)(.*)?""" 21*4882a593Smuzhiyun else: 22*4882a593Smuzhiyun version_pattern = r"""r?v?(?:(?P<release>[0-9]+(?:[-\.][0-9]+)*)(?P<pre>[-_\.]?(?P<pre_l>(rc|alpha|beta|pre|preview|dev))[-_\.]?(?P<pre_v>[0-9]+)?)?)(.*)?""" 23*4882a593Smuzhiyun regex = re.compile(r"^\s*" + version_pattern + r"\s*$", re.VERBOSE | re.IGNORECASE) 24*4882a593Smuzhiyun 25*4882a593Smuzhiyun match = regex.search(version) 26*4882a593Smuzhiyun if not match: 27*4882a593Smuzhiyun raise Exception("Invalid version: '{0}'".format(version)) 28*4882a593Smuzhiyun 29*4882a593Smuzhiyun self._version = _Version( 30*4882a593Smuzhiyun release=tuple(int(i) for i in match.group("release").replace("-",".").split(".")), 31*4882a593Smuzhiyun patch_l=match.group("patch_l") if str(suffix) in suffixes and match.group("patch_l") else "", 32*4882a593Smuzhiyun pre_l=match.group("pre_l"), 33*4882a593Smuzhiyun pre_v=match.group("pre_v") 34*4882a593Smuzhiyun ) 35*4882a593Smuzhiyun 36*4882a593Smuzhiyun self._key = _cmpkey( 37*4882a593Smuzhiyun self._version.release, 38*4882a593Smuzhiyun self._version.patch_l, 39*4882a593Smuzhiyun self._version.pre_l, 40*4882a593Smuzhiyun self._version.pre_v 41*4882a593Smuzhiyun ) 42*4882a593Smuzhiyun 43*4882a593Smuzhiyun def __eq__(self, other): 44*4882a593Smuzhiyun if not isinstance(other, Version): 45*4882a593Smuzhiyun return NotImplemented 46*4882a593Smuzhiyun return self._key == other._key 47*4882a593Smuzhiyun 48*4882a593Smuzhiyun def __gt__(self, other): 49*4882a593Smuzhiyun if not isinstance(other, Version): 50*4882a593Smuzhiyun return NotImplemented 51*4882a593Smuzhiyun return self._key > other._key 52*4882a593Smuzhiyun 53*4882a593Smuzhiyundef _cmpkey(release, patch_l, pre_l, pre_v): 54*4882a593Smuzhiyun # remove leading 0 55*4882a593Smuzhiyun _release = tuple( 56*4882a593Smuzhiyun reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) 57*4882a593Smuzhiyun ) 58*4882a593Smuzhiyun 59*4882a593Smuzhiyun _patch = patch_l.upper() 60*4882a593Smuzhiyun 61*4882a593Smuzhiyun if pre_l is None and pre_v is None: 62*4882a593Smuzhiyun _pre = float('inf') 63*4882a593Smuzhiyun else: 64*4882a593Smuzhiyun _pre = float(pre_v) if pre_v else float('-inf') 65*4882a593Smuzhiyun return _release, _patch, _pre 66*4882a593Smuzhiyun 67*4882a593Smuzhiyun 68*4882a593Smuzhiyundef get_patched_cves(d): 69*4882a593Smuzhiyun """ 70*4882a593Smuzhiyun Get patches that solve CVEs using the "CVE: " tag. 71*4882a593Smuzhiyun """ 72*4882a593Smuzhiyun 73*4882a593Smuzhiyun import re 74*4882a593Smuzhiyun import oe.patch 75*4882a593Smuzhiyun 76*4882a593Smuzhiyun pn = d.getVar("PN") 77*4882a593Smuzhiyun cve_match = re.compile("CVE:( CVE\-\d{4}\-\d+)+") 78*4882a593Smuzhiyun 79*4882a593Smuzhiyun # Matches the last "CVE-YYYY-ID" in the file name, also if written 80*4882a593Smuzhiyun # in lowercase. Possible to have multiple CVE IDs in a single 81*4882a593Smuzhiyun # file name, but only the last one will be detected from the file name. 82*4882a593Smuzhiyun # However, patch files contents addressing multiple CVE IDs are supported 83*4882a593Smuzhiyun # (cve_match regular expression) 84*4882a593Smuzhiyun 85*4882a593Smuzhiyun cve_file_name_match = re.compile(".*([Cc][Vv][Ee]\-\d{4}\-\d+)") 86*4882a593Smuzhiyun 87*4882a593Smuzhiyun patched_cves = set() 88*4882a593Smuzhiyun bb.debug(2, "Looking for patches that solves CVEs for %s" % pn) 89*4882a593Smuzhiyun for url in oe.patch.src_patches(d): 90*4882a593Smuzhiyun patch_file = bb.fetch.decodeurl(url)[2] 91*4882a593Smuzhiyun 92*4882a593Smuzhiyun # Remote compressed patches may not be unpacked, so silently ignore them 93*4882a593Smuzhiyun if not os.path.isfile(patch_file): 94*4882a593Smuzhiyun bb.warn("%s does not exist, cannot extract CVE list" % patch_file) 95*4882a593Smuzhiyun continue 96*4882a593Smuzhiyun 97*4882a593Smuzhiyun # Check patch file name for CVE ID 98*4882a593Smuzhiyun fname_match = cve_file_name_match.search(patch_file) 99*4882a593Smuzhiyun if fname_match: 100*4882a593Smuzhiyun cve = fname_match.group(1).upper() 101*4882a593Smuzhiyun patched_cves.add(cve) 102*4882a593Smuzhiyun bb.debug(2, "Found CVE %s from patch file name %s" % (cve, patch_file)) 103*4882a593Smuzhiyun 104*4882a593Smuzhiyun with open(patch_file, "r", encoding="utf-8") as f: 105*4882a593Smuzhiyun try: 106*4882a593Smuzhiyun patch_text = f.read() 107*4882a593Smuzhiyun except UnicodeDecodeError: 108*4882a593Smuzhiyun bb.debug(1, "Failed to read patch %s using UTF-8 encoding" 109*4882a593Smuzhiyun " trying with iso8859-1" % patch_file) 110*4882a593Smuzhiyun f.close() 111*4882a593Smuzhiyun with open(patch_file, "r", encoding="iso8859-1") as f: 112*4882a593Smuzhiyun patch_text = f.read() 113*4882a593Smuzhiyun 114*4882a593Smuzhiyun # Search for one or more "CVE: " lines 115*4882a593Smuzhiyun text_match = False 116*4882a593Smuzhiyun for match in cve_match.finditer(patch_text): 117*4882a593Smuzhiyun # Get only the CVEs without the "CVE: " tag 118*4882a593Smuzhiyun cves = patch_text[match.start()+5:match.end()] 119*4882a593Smuzhiyun for cve in cves.split(): 120*4882a593Smuzhiyun bb.debug(2, "Patch %s solves %s" % (patch_file, cve)) 121*4882a593Smuzhiyun patched_cves.add(cve) 122*4882a593Smuzhiyun text_match = True 123*4882a593Smuzhiyun 124*4882a593Smuzhiyun if not fname_match and not text_match: 125*4882a593Smuzhiyun bb.debug(2, "Patch %s doesn't solve CVEs" % patch_file) 126*4882a593Smuzhiyun 127*4882a593Smuzhiyun return patched_cves 128*4882a593Smuzhiyun 129*4882a593Smuzhiyun 130*4882a593Smuzhiyundef get_cpe_ids(cve_product, version): 131*4882a593Smuzhiyun """ 132*4882a593Smuzhiyun Get list of CPE identifiers for the given product and version 133*4882a593Smuzhiyun """ 134*4882a593Smuzhiyun 135*4882a593Smuzhiyun version = version.split("+git")[0] 136*4882a593Smuzhiyun 137*4882a593Smuzhiyun cpe_ids = [] 138*4882a593Smuzhiyun for product in cve_product.split(): 139*4882a593Smuzhiyun # CVE_PRODUCT in recipes may include vendor information for CPE identifiers. If not, 140*4882a593Smuzhiyun # use wildcard for vendor. 141*4882a593Smuzhiyun if ":" in product: 142*4882a593Smuzhiyun vendor, product = product.split(":", 1) 143*4882a593Smuzhiyun else: 144*4882a593Smuzhiyun vendor = "*" 145*4882a593Smuzhiyun 146*4882a593Smuzhiyun cpe_id = 'cpe:2.3:a:{}:{}:{}:*:*:*:*:*:*:*'.format(vendor, product, version) 147*4882a593Smuzhiyun cpe_ids.append(cpe_id) 148*4882a593Smuzhiyun 149*4882a593Smuzhiyun return cpe_ids 150*4882a593Smuzhiyun 151*4882a593Smuzhiyundef cve_check_merge_jsons(output, data): 152*4882a593Smuzhiyun """ 153*4882a593Smuzhiyun Merge the data in the "package" property to the main data file 154*4882a593Smuzhiyun output 155*4882a593Smuzhiyun """ 156*4882a593Smuzhiyun if output["version"] != data["version"]: 157*4882a593Smuzhiyun bb.error("Version mismatch when merging JSON outputs") 158*4882a593Smuzhiyun return 159*4882a593Smuzhiyun 160*4882a593Smuzhiyun for product in output["package"]: 161*4882a593Smuzhiyun if product["name"] == data["package"][0]["name"]: 162*4882a593Smuzhiyun bb.error("Error adding the same package twice") 163*4882a593Smuzhiyun return 164*4882a593Smuzhiyun 165*4882a593Smuzhiyun output["package"].append(data["package"][0]) 166*4882a593Smuzhiyun 167*4882a593Smuzhiyundef update_symlinks(target_path, link_path): 168*4882a593Smuzhiyun """ 169*4882a593Smuzhiyun Update a symbolic link link_path to point to target_path. 170*4882a593Smuzhiyun Remove the link and recreate it if exist and is different. 171*4882a593Smuzhiyun """ 172*4882a593Smuzhiyun if link_path != target_path and os.path.exists(target_path): 173*4882a593Smuzhiyun if os.path.exists(os.path.realpath(link_path)): 174*4882a593Smuzhiyun os.remove(link_path) 175*4882a593Smuzhiyun os.symlink(os.path.basename(target_path), link_path) 176*4882a593Smuzhiyun 177*4882a593Smuzhiyun 178*4882a593Smuzhiyundef convert_cve_version(version): 179*4882a593Smuzhiyun """ 180*4882a593Smuzhiyun This function converts from CVE format to Yocto version format. 181*4882a593Smuzhiyun eg 8.3_p1 -> 8.3p1, 6.2_rc1 -> 6.2-rc1 182*4882a593Smuzhiyun 183*4882a593Smuzhiyun Unless it is redefined using CVE_VERSION in the recipe, 184*4882a593Smuzhiyun cve_check uses the version in the name of the recipe (${PV}) 185*4882a593Smuzhiyun to check vulnerabilities against a CVE in the database downloaded from NVD. 186*4882a593Smuzhiyun 187*4882a593Smuzhiyun When the version has an update, i.e. 188*4882a593Smuzhiyun "p1" in OpenSSH 8.3p1, 189*4882a593Smuzhiyun "-rc1" in linux kernel 6.2-rc1, 190*4882a593Smuzhiyun the database stores the version as version_update (8.3_p1, 6.2_rc1). 191*4882a593Smuzhiyun Therefore, we must transform this version before comparing to the 192*4882a593Smuzhiyun recipe version. 193*4882a593Smuzhiyun 194*4882a593Smuzhiyun In this case, the parameter of the function is 8.3_p1. 195*4882a593Smuzhiyun If the version uses the Release Candidate format, "rc", 196*4882a593Smuzhiyun this function replaces the '_' by '-'. 197*4882a593Smuzhiyun If the version uses the Update format, "p", 198*4882a593Smuzhiyun this function removes the '_' completely. 199*4882a593Smuzhiyun """ 200*4882a593Smuzhiyun import re 201*4882a593Smuzhiyun 202*4882a593Smuzhiyun matches = re.match('^([0-9.]+)_((p|rc)[0-9]+)$', version) 203*4882a593Smuzhiyun 204*4882a593Smuzhiyun if not matches: 205*4882a593Smuzhiyun return version 206*4882a593Smuzhiyun 207*4882a593Smuzhiyun version = matches.group(1) 208*4882a593Smuzhiyun update = matches.group(2) 209*4882a593Smuzhiyun 210*4882a593Smuzhiyun if matches.group(3) == "rc": 211*4882a593Smuzhiyun return version + '-' + update 212*4882a593Smuzhiyun 213*4882a593Smuzhiyun return version + update 214*4882a593Smuzhiyun 215