diff options
| author | Daniel Turull <daniel.turull@ericsson.com> | 2025-04-10 11:48:35 +0200 |
|---|---|---|
| committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2025-04-23 19:50:57 +0100 |
| commit | 13f4119ccff047a73608ea5c03f13cf2f1a590d4 (patch) | |
| tree | 5e19ce916d21227f21baca43cf3678f6ffa7356b | |
| parent | 9b96fdbb0cab02f4a6180e812b02bc9d4c41b1a5 (diff) | |
| download | poky-13f4119ccff047a73608ea5c03f13cf2f1a590d4.tar.gz | |
linux/generate-cve-exclusions: use data from CVEProject
The old script was relying on linuxkernelcves.com that was archived in
May 2024 when kernel.org became a CNA.
The new script reads CVE json files from the datadir that can be either
from the official kernel.org CNA [1] or CVEProject [2]
[1] https://git.kernel.org/pub/scm/linux/security/vulns.git
[2] https://github.com/CVEProject/cvelistV5
(From OE-Core rev: 96ef76d88851a5a397d9fc04e37baa285d9f4074)
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Signed-off-by: Mathieu Dubois-Briand <mathieu.dubois-briand@bootlin.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
| -rwxr-xr-x | meta/recipes-kernel/linux/generate-cve-exclusions.py | 116 |
1 files changed, 85 insertions, 31 deletions
diff --git a/meta/recipes-kernel/linux/generate-cve-exclusions.py b/meta/recipes-kernel/linux/generate-cve-exclusions.py index aa9195aab4..82fb4264e3 100755 --- a/meta/recipes-kernel/linux/generate-cve-exclusions.py +++ b/meta/recipes-kernel/linux/generate-cve-exclusions.py | |||
| @@ -1,7 +1,7 @@ | |||
| 1 | #! /usr/bin/env python3 | 1 | #! /usr/bin/env python3 |
| 2 | 2 | ||
| 3 | # Generate granular CVE status metadata for a specific version of the kernel | 3 | # Generate granular CVE status metadata for a specific version of the kernel |
| 4 | # using data from linuxkernelcves.com. | 4 | # using json data from cvelistV5 or vulns repository |
| 5 | # | 5 | # |
| 6 | # SPDX-License-Identifier: GPL-2.0-only | 6 | # SPDX-License-Identifier: GPL-2.0-only |
| 7 | 7 | ||
| @@ -9,7 +9,8 @@ import argparse | |||
| 9 | import datetime | 9 | import datetime |
| 10 | import json | 10 | import json |
| 11 | import pathlib | 11 | import pathlib |
| 12 | import re | 12 | import os |
| 13 | import glob | ||
| 13 | 14 | ||
| 14 | from packaging.version import Version | 15 | from packaging.version import Version |
| 15 | 16 | ||
| @@ -25,22 +26,75 @@ def parse_version(s): | |||
| 25 | return Version(s) | 26 | return Version(s) |
| 26 | return None | 27 | return None |
| 27 | 28 | ||
| 29 | def get_fixed_versions(cve_info, base_version): | ||
| 30 | ''' | ||
| 31 | Get fixed versionss | ||
| 32 | ''' | ||
| 33 | first_affected = None | ||
| 34 | fixed = None | ||
| 35 | fixed_backport = None | ||
| 36 | next_version = Version(str(base_version) + ".5000") | ||
| 37 | for affected in cve_info["containers"]["cna"]["affected"]: | ||
| 38 | # In case the CVE info is not complete, it might not have default status and therefore | ||
| 39 | # we don't know the status of this CVE. | ||
| 40 | if not "defaultStatus" in affected: | ||
| 41 | return first_affected, fixed, fixed_backport | ||
| 42 | if affected["defaultStatus"] == "affected": | ||
| 43 | for version in affected["versions"]: | ||
| 44 | v = Version(version["version"]) | ||
| 45 | if v == 0: | ||
| 46 | #Skiping non-affected | ||
| 47 | continue | ||
| 48 | if version["status"] == "affected" and not first_affected: | ||
| 49 | first_affected = v | ||
| 50 | elif (version["status"] == "unaffected" and | ||
| 51 | version['versionType'] == "original_commit_for_fix"): | ||
| 52 | fixed = v | ||
| 53 | elif base_version < v and v < next_version: | ||
| 54 | fixed_backport = v | ||
| 55 | elif affected["defaultStatus"] == "unaffected": | ||
| 56 | # Only specific versions are affected. We care only about our base version | ||
| 57 | if "versions" not in affected: | ||
| 58 | continue | ||
| 59 | for version in affected["versions"]: | ||
| 60 | if "versionType" not in version: | ||
| 61 | continue | ||
| 62 | if version["versionType"] == "git": | ||
| 63 | continue | ||
| 64 | v = Version(version["version"]) | ||
| 65 | # in case it is not in our base version | ||
| 66 | less_than = Version(version["lessThan"]) | ||
| 67 | |||
| 68 | if not first_affected: | ||
| 69 | first_affected = v | ||
| 70 | fixed = less_than | ||
| 71 | if base_version < v and v < next_version: | ||
| 72 | first_affected = v | ||
| 73 | fixed = less_than | ||
| 74 | fixed_backport = less_than | ||
| 75 | |||
| 76 | return first_affected, fixed, fixed_backport | ||
| 77 | |||
| 78 | def is_linux_cve(cve_info): | ||
| 79 | '''Return true is the CVE belongs to Linux''' | ||
| 80 | if not "affected" in cve_info["containers"]["cna"]: | ||
| 81 | return False | ||
| 82 | for affected in cve_info["containers"]["cna"]["affected"]: | ||
| 83 | if not "product" in affected: | ||
| 84 | return False | ||
| 85 | if affected["product"] == "Linux" and affected["vendor"] == "Linux": | ||
| 86 | return True | ||
| 87 | return False | ||
| 28 | 88 | ||
| 29 | def main(argp=None): | 89 | def main(argp=None): |
| 30 | parser = argparse.ArgumentParser() | 90 | parser = argparse.ArgumentParser() |
| 31 | parser.add_argument("datadir", type=pathlib.Path, help="Path to a clone of https://github.com/nluedtke/linux_kernel_cves") | 91 | 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") |
| 32 | parser.add_argument("version", type=Version, help="Kernel version number to generate data for, such as 6.1.38") | 92 | parser.add_argument("version", type=Version, help="Kernel version number to generate data for, such as 6.1.38") |
| 33 | 93 | ||
| 34 | args = parser.parse_args(argp) | 94 | args = parser.parse_args(argp) |
| 35 | datadir = args.datadir | 95 | datadir = args.datadir |
| 36 | version = args.version | 96 | version = args.version |
| 37 | base_version = f"{version.major}.{version.minor}" | 97 | base_version = Version(f"{version.major}.{version.minor}") |
| 38 | |||
| 39 | with open(datadir / "data" / "kernel_cves.json", "r") as f: | ||
| 40 | cve_data = json.load(f) | ||
| 41 | |||
| 42 | with open(datadir / "data" / "stream_fixes.json", "r") as f: | ||
| 43 | stream_data = json.load(f) | ||
| 44 | 98 | ||
| 45 | print(f""" | 99 | print(f""" |
| 46 | # Auto-generated CVE metadata, DO NOT EDIT BY HAND. | 100 | # Auto-generated CVE metadata, DO NOT EDIT BY HAND. |
| @@ -55,17 +109,23 @@ python check_kernel_cve_status_version() {{ | |||
| 55 | do_cve_check[prefuncs] += "check_kernel_cve_status_version" | 109 | do_cve_check[prefuncs] += "check_kernel_cve_status_version" |
| 56 | """) | 110 | """) |
| 57 | 111 | ||
| 58 | for cve, data in cve_data.items(): | 112 | # Loop though all CVES and check if they are kernel related, newer than 2015 |
| 59 | if "affected_versions" not in data: | 113 | pattern = os.path.join(datadir, '**', "CVE-20*.json") |
| 60 | print(f"# Skipping {cve}, no affected_versions") | ||
| 61 | print() | ||
| 62 | continue | ||
| 63 | 114 | ||
| 64 | affected = data["affected_versions"] | 115 | files = glob.glob(pattern, recursive=True) |
| 65 | first_affected, fixed = re.search(r"(.+) to (.+)", affected).groups() | 116 | for cve_file in sorted(files): |
| 66 | first_affected = parse_version(first_affected) | 117 | # Get CVE Id |
| 67 | fixed = parse_version(fixed) | 118 | cve = cve_file[cve_file.rfind("/")+1:cve_file.rfind(".json")] |
| 119 | # We process from 2015 data, old request are not properly formated | ||
| 120 | year = cve.split("-")[1] | ||
| 121 | if int(year) < 2015: | ||
| 122 | continue | ||
| 123 | with open(cve_file, 'r', encoding='utf-8') as json_file: | ||
| 124 | cve_info = json.load(json_file) | ||
| 68 | 125 | ||
| 126 | if not is_linux_cve(cve_info): | ||
| 127 | continue | ||
| 128 | first_affected, fixed, backport_ver = get_fixed_versions(cve_info, base_version) | ||
| 69 | if not fixed: | 129 | if not fixed: |
| 70 | print(f"# {cve} has no known resolution") | 130 | print(f"# {cve} has no known resolution") |
| 71 | elif first_affected and version < first_affected: | 131 | elif first_affected and version < first_affected: |
| @@ -75,19 +135,13 @@ do_cve_check[prefuncs] += "check_kernel_cve_status_version" | |||
| 75 | f'CVE_STATUS[{cve}] = "fixed-version: Fixed from version {fixed}"' | 135 | f'CVE_STATUS[{cve}] = "fixed-version: Fixed from version {fixed}"' |
| 76 | ) | 136 | ) |
| 77 | else: | 137 | else: |
| 78 | if cve in stream_data: | 138 | if backport_ver: |
| 79 | backport_data = stream_data[cve] | 139 | if backport_ver <= version: |
| 80 | if base_version in backport_data: | 140 | print( |
| 81 | backport_ver = Version(backport_data[base_version]["fixed_version"]) | 141 | f'CVE_STATUS[{cve}] = "cpe-stable-backport: Backported in {backport_ver}"' |
| 82 | if backport_ver <= version: | 142 | ) |
| 83 | print( | ||
| 84 | f'CVE_STATUS[{cve}] = "cpe-stable-backport: Backported in {backport_ver}"' | ||
| 85 | ) | ||
| 86 | else: | ||
| 87 | # TODO print a note that the kernel needs bumping | ||
| 88 | print(f"# {cve} needs backporting (fixed from {backport_ver})") | ||
| 89 | else: | 143 | else: |
| 90 | print(f"# {cve} needs backporting (fixed from {fixed})") | 144 | print(f"# {cve} needs backporting (fixed from {backport_ver})") |
| 91 | else: | 145 | else: |
| 92 | print(f"# {cve} needs backporting (fixed from {fixed})") | 146 | print(f"# {cve} needs backporting (fixed from {fixed})") |
| 93 | 147 | ||
