#! /usr/bin/env python3

# Generate granular CVE status metadata for a specific version of the kernel
# using json data from cvelistV5 or vulns repository
#
# SPDX-License-Identifier: GPL-2.0-only

import argparse
import datetime
import json
import pathlib
import os
import glob
import subprocess

from packaging.version import Version


def parse_version(s):
    """
    Parse the version string and either return a packaging.version.Version, or
    None if the string was unset or "unk".
    """
    if s and s != "unk":
        # packaging.version.Version doesn't approve of versions like v5.12-rc1-dontuse
        s = s.replace("-dontuse", "")
        return Version(s)
    return None

def get_fixed_versions(cve_info, base_version):
    '''
    Get fixed versionss
    '''
    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 == Version('0'):
                    #Skiping non-affected
                    continue
                if version["status"] == "unaffected" and first_affected and v < first_affected:
                    first_affected = Version(f"{v.major}.{v.minor}")
                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

def is_linux_cve(cve_info):
    '''Return true is the CVE belongs to Linux'''
    if not "affected" in cve_info["containers"]["cna"]:
        return False
    for affected in cve_info["containers"]["cna"]["affected"]:
        if not "product" in affected:
            return False
        if affected["product"] == "Linux" and affected["vendor"] == "Linux":
            return True
    return False

def main(argp=None):
    parser = argparse.ArgumentParser()
    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")
    parser.add_argument("version", type=Version, help="Kernel version number to generate data for, such as 6.1.38")

    args = parser.parse_args(argp)
    datadir = args.datadir.resolve()
    version = args.version
    base_version = Version(f"{version.major}.{version.minor}")

    data_version = subprocess.check_output(("git", "describe", "--tags", "HEAD"), cwd=datadir, text=True)

    print(f"""
# Auto-generated CVE metadata, DO NOT EDIT BY HAND.
# Generated at {datetime.datetime.now(datetime.timezone.utc)} for kernel version {version}
# From {datadir.name} {data_version}

python check_kernel_cve_status_version() {{
    this_version = "{version}"
    kernel_version = d.getVar("LINUX_VERSION")
    if kernel_version != this_version:
        bb.warn("Kernel CVE status needs updating: generated for %s but kernel is %s" % (this_version, kernel_version))
}}
do_cve_check[prefuncs] += "check_kernel_cve_status_version"
""")

    # Loop though all CVES and check if they are kernel related, newer than 2015
    pattern = os.path.join(datadir, '**', "CVE-20*.json")

    files = glob.glob(pattern, recursive=True)
    for cve_file in sorted(files):
        # Get CVE Id
        cve = cve_file[cve_file.rfind("/")+1:cve_file.rfind(".json")]
        # We process from 2015 data, old request are not properly formated
        year = cve.split("-")[1]
        if int(year) < 2015:
            continue
        with open(cve_file, 'r', encoding='utf-8') as json_file:
            cve_info = json.load(json_file)

        if not is_linux_cve(cve_info):
            continue
        first_affected, fixed, backport_ver = get_fixed_versions(cve_info, base_version)
        if not fixed:
            print(f"# {cve} has no known resolution")
        elif first_affected and version < first_affected:
            print(f'CVE_STATUS[{cve}] = "fixed-version: only affects {first_affected} onwards"')
        elif fixed <= version:
            print(
                f'CVE_STATUS[{cve}] = "fixed-version: Fixed from version {fixed}"'
            )
        else:
            if backport_ver:
                if backport_ver <= version:
                    print(
                        f'CVE_STATUS[{cve}] = "cpe-stable-backport: Backported in {backport_ver}"'
                    )
                else:
                    print(f"# {cve} may need backporting (fixed from {backport_ver})")
            else:
                print(f"# {cve} needs backporting (fixed from {fixed})")

        print()


if __name__ == "__main__":
    main()
