xref: /openbmc/openbmc/poky/meta/recipes-kernel/linux/generate-cve-exclusions.py (revision c9537f57ab488bf5d90132917b0184e2527970a5)
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