generate-cve-exclusions.py 5.9 KB

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