vex.bbclass 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. #
  2. # Copyright OpenEmbedded Contributors
  3. #
  4. # SPDX-License-Identifier: MIT
  5. #
  6. # This class is used to generate metadata needed by external
  7. # tools to check for vulnerabilities, for example CVEs.
  8. #
  9. # In order to use this class just inherit the class in the
  10. # local.conf file and it will add the generate_vex task for
  11. # every recipe. If an image is build it will generate a report
  12. # in DEPLOY_DIR_IMAGE for all the packages used, it will also
  13. # generate a file for all recipes used in the build.
  14. #
  15. # Variables use CVE_CHECK prefix to keep compatibility with
  16. # the cve-check class
  17. #
  18. # Example:
  19. # bitbake -c generate_vex openssl
  20. # bitbake core-image-sato
  21. # bitbake -k -c generate_vex universe
  22. #
  23. # The product name that the CVE database uses defaults to BPN, but may need to
  24. # be overriden per recipe (for example tiff.bb sets CVE_PRODUCT=libtiff).
  25. CVE_PRODUCT ??= "${BPN}"
  26. CVE_VERSION ??= "${PV}"
  27. CVE_CHECK_SUMMARY_DIR ?= "${LOG_DIR}/cve"
  28. CVE_CHECK_SUMMARY_FILE_NAME_JSON = "cve-summary.json"
  29. CVE_CHECK_SUMMARY_INDEX_PATH = "${CVE_CHECK_SUMMARY_DIR}/cve-summary-index.txt"
  30. CVE_CHECK_DIR ??= "${DEPLOY_DIR}/cve"
  31. CVE_CHECK_RECIPE_FILE_JSON ?= "${CVE_CHECK_DIR}/${PN}_cve.json"
  32. CVE_CHECK_MANIFEST_JSON ?= "${IMGDEPLOYDIR}/${IMAGE_NAME}.json"
  33. # Skip CVE Check for packages (PN)
  34. CVE_CHECK_SKIP_RECIPE ?= ""
  35. # Replace NVD DB check status for a given CVE. Each of CVE has to be mentioned
  36. # separately with optional detail and description for this status.
  37. #
  38. # CVE_STATUS[CVE-1234-0001] = "not-applicable-platform: Issue only applies on Windows"
  39. # CVE_STATUS[CVE-1234-0002] = "fixed-version: Fixed externally"
  40. #
  41. # Settings the same status and reason for multiple CVEs is possible
  42. # via CVE_STATUS_GROUPS variable.
  43. #
  44. # CVE_STATUS_GROUPS = "CVE_STATUS_WIN CVE_STATUS_PATCHED"
  45. #
  46. # CVE_STATUS_WIN = "CVE-1234-0001 CVE-1234-0003"
  47. # CVE_STATUS_WIN[status] = "not-applicable-platform: Issue only applies on Windows"
  48. # CVE_STATUS_PATCHED = "CVE-1234-0002 CVE-1234-0004"
  49. # CVE_STATUS_PATCHED[status] = "fixed-version: Fixed externally"
  50. #
  51. # All possible CVE statuses could be found in cve-check-map.conf
  52. # CVE_CHECK_STATUSMAP[not-applicable-platform] = "Ignored"
  53. # CVE_CHECK_STATUSMAP[fixed-version] = "Patched"
  54. #
  55. # CVE_CHECK_IGNORE is deprecated and CVE_STATUS has to be used instead.
  56. # Keep CVE_CHECK_IGNORE until other layers migrate to new variables
  57. CVE_CHECK_IGNORE ?= ""
  58. # Layers to be excluded
  59. CVE_CHECK_LAYER_EXCLUDELIST ??= ""
  60. # Layers to be included
  61. CVE_CHECK_LAYER_INCLUDELIST ??= ""
  62. # set to "alphabetical" for version using single alphabetical character as increment release
  63. CVE_VERSION_SUFFIX ??= ""
  64. python () {
  65. if bb.data.inherits_class("cve-check", d):
  66. raise bb.parse.SkipRecipe("Skipping recipe: found incompatible combination of cve-check and vex enabled at the same time.")
  67. # Fallback all CVEs from CVE_CHECK_IGNORE to CVE_STATUS
  68. cve_check_ignore = d.getVar("CVE_CHECK_IGNORE")
  69. if cve_check_ignore:
  70. bb.warn("CVE_CHECK_IGNORE is deprecated in favor of CVE_STATUS")
  71. for cve in (d.getVar("CVE_CHECK_IGNORE") or "").split():
  72. d.setVarFlag("CVE_STATUS", cve, "ignored")
  73. # Process CVE_STATUS_GROUPS to set multiple statuses and optional detail or description at once
  74. for cve_status_group in (d.getVar("CVE_STATUS_GROUPS") or "").split():
  75. cve_group = d.getVar(cve_status_group)
  76. if cve_group is not None:
  77. for cve in cve_group.split():
  78. d.setVarFlag("CVE_STATUS", cve, d.getVarFlag(cve_status_group, "status"))
  79. else:
  80. bb.warn("CVE_STATUS_GROUPS contains undefined variable %s" % cve_status_group)
  81. }
  82. def generate_json_report(d, out_path, link_path):
  83. if os.path.exists(d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")):
  84. import json
  85. from oe.cve_check import cve_check_merge_jsons, update_symlinks
  86. bb.note("Generating JSON CVE summary")
  87. index_file = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")
  88. summary = {"version":"1", "package": []}
  89. with open(index_file) as f:
  90. filename = f.readline()
  91. while filename:
  92. with open(filename.rstrip()) as j:
  93. data = json.load(j)
  94. cve_check_merge_jsons(summary, data)
  95. filename = f.readline()
  96. summary["package"].sort(key=lambda d: d['name'])
  97. with open(out_path, "w") as f:
  98. json.dump(summary, f, indent=2)
  99. update_symlinks(out_path, link_path)
  100. python vex_save_summary_handler () {
  101. import shutil
  102. import datetime
  103. from oe.cve_check import update_symlinks
  104. cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR")
  105. bb.utils.mkdirhier(cvelogpath)
  106. timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
  107. json_summary_link_name = os.path.join(cvelogpath, d.getVar("CVE_CHECK_SUMMARY_FILE_NAME_JSON"))
  108. json_summary_name = os.path.join(cvelogpath, "cve-summary-%s.json" % (timestamp))
  109. generate_json_report(d, json_summary_name, json_summary_link_name)
  110. bb.plain("Complete CVE JSON report summary created at: %s" % json_summary_link_name)
  111. }
  112. addhandler vex_save_summary_handler
  113. vex_save_summary_handler[eventmask] = "bb.event.BuildCompleted"
  114. python do_generate_vex () {
  115. """
  116. Generate metadata needed for vulnerability checking for
  117. the current recipe
  118. """
  119. from oe.cve_check import get_patched_cves
  120. try:
  121. patched_cves = get_patched_cves(d)
  122. cves_status = []
  123. products = d.getVar("CVE_PRODUCT").split()
  124. for product in products:
  125. if ":" in product:
  126. _, product = product.split(":", 1)
  127. cves_status.append([product, False])
  128. except FileNotFoundError:
  129. bb.fatal("Failure in searching patches")
  130. cve_write_data_json(d, patched_cves, cves_status)
  131. }
  132. addtask generate_vex before do_build
  133. python vex_cleanup () {
  134. """
  135. Delete the file used to gather all the CVE information.
  136. """
  137. bb.utils.remove(e.data.getVar("CVE_CHECK_SUMMARY_INDEX_PATH"))
  138. }
  139. addhandler vex_cleanup
  140. vex_cleanup[eventmask] = "bb.event.BuildCompleted"
  141. python vex_write_rootfs_manifest () {
  142. """
  143. Create VEX/CVE manifest when building an image
  144. """
  145. import json
  146. from oe.rootfs import image_list_installed_packages
  147. from oe.cve_check import cve_check_merge_jsons, update_symlinks
  148. deploy_file_json = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
  149. if os.path.exists(deploy_file_json):
  150. bb.utils.remove(deploy_file_json)
  151. # Create a list of relevant recipies
  152. recipies = set()
  153. for pkg in list(image_list_installed_packages(d)):
  154. pkg_info = os.path.join(d.getVar('PKGDATA_DIR'),
  155. 'runtime-reverse', pkg)
  156. pkg_data = oe.packagedata.read_pkgdatafile(pkg_info)
  157. recipies.add(pkg_data["PN"])
  158. bb.note("Writing rootfs VEX manifest")
  159. deploy_dir = d.getVar("IMGDEPLOYDIR")
  160. link_name = d.getVar("IMAGE_LINK_NAME")
  161. json_data = {"version":"1", "package": []}
  162. text_data = ""
  163. save_pn = d.getVar("PN")
  164. for pkg in recipies:
  165. # To be able to use the CVE_CHECK_RECIPE_FILE_JSON variable we have to evaluate
  166. # it with the different PN names set each time.
  167. d.setVar("PN", pkg)
  168. pkgfilepath = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
  169. if os.path.exists(pkgfilepath):
  170. with open(pkgfilepath) as j:
  171. data = json.load(j)
  172. cve_check_merge_jsons(json_data, data)
  173. d.setVar("PN", save_pn)
  174. link_path = os.path.join(deploy_dir, "%s.json" % link_name)
  175. manifest_name = d.getVar("CVE_CHECK_MANIFEST_JSON")
  176. with open(manifest_name, "w") as f:
  177. json.dump(json_data, f, indent=2)
  178. update_symlinks(manifest_name, link_path)
  179. bb.plain("Image VEX JSON report stored in: %s" % manifest_name)
  180. }
  181. ROOTFS_POSTPROCESS_COMMAND:prepend = "vex_write_rootfs_manifest; "
  182. do_rootfs[recrdeptask] += "do_generate_vex "
  183. do_populate_sdk[recrdeptask] += "do_generate_vex "
  184. def cve_write_data_json(d, cve_data, cve_status):
  185. """
  186. Prepare CVE data for the JSON format, then write it.
  187. Done for each recipe.
  188. """
  189. from oe.cve_check import get_cpe_ids
  190. import json
  191. output = {"version":"1", "package": []}
  192. nvd_link = "https://nvd.nist.gov/vuln/detail/"
  193. fdir_name = d.getVar("FILE_DIRNAME")
  194. layer = fdir_name.split("/")[-3]
  195. include_layers = d.getVar("CVE_CHECK_LAYER_INCLUDELIST").split()
  196. exclude_layers = d.getVar("CVE_CHECK_LAYER_EXCLUDELIST").split()
  197. if exclude_layers and layer in exclude_layers:
  198. return
  199. if include_layers and layer not in include_layers:
  200. return
  201. product_data = []
  202. for s in cve_status:
  203. p = {"product": s[0], "cvesInRecord": "Yes"}
  204. if s[1] == False:
  205. p["cvesInRecord"] = "No"
  206. product_data.append(p)
  207. product_data = list({p['product']:p for p in product_data}.values())
  208. package_version = "%s%s" % (d.getVar("EXTENDPE"), d.getVar("PV"))
  209. cpes = get_cpe_ids(d.getVar("CVE_PRODUCT"), d.getVar("CVE_VERSION"))
  210. package_data = {
  211. "name" : d.getVar("PN"),
  212. "layer" : layer,
  213. "version" : package_version,
  214. "products": product_data,
  215. "cpes": cpes
  216. }
  217. cve_list = []
  218. for cve in sorted(cve_data):
  219. issue_link = "%s%s" % (nvd_link, cve)
  220. cve_item = {
  221. "id" : cve,
  222. "status" : cve_data[cve]["abbrev-status"],
  223. "link": issue_link,
  224. }
  225. if 'NVD-summary' in cve_data[cve]:
  226. cve_item["summary"] = cve_data[cve]["NVD-summary"]
  227. cve_item["scorev2"] = cve_data[cve]["NVD-scorev2"]
  228. cve_item["scorev3"] = cve_data[cve]["NVD-scorev3"]
  229. cve_item["scorev4"] = cve_data[cve]["NVD-scorev4"]
  230. cve_item["vector"] = cve_data[cve]["NVD-vector"]
  231. cve_item["vectorString"] = cve_data[cve]["NVD-vectorString"]
  232. if 'status' in cve_data[cve]:
  233. cve_item["detail"] = cve_data[cve]["status"]
  234. if 'justification' in cve_data[cve]:
  235. cve_item["description"] = cve_data[cve]["justification"]
  236. if 'resource' in cve_data[cve]:
  237. cve_item["patch-file"] = cve_data[cve]["resource"]
  238. cve_list.append(cve_item)
  239. package_data["issue"] = cve_list
  240. output["package"].append(package_data)
  241. deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
  242. write_string = json.dumps(output, indent=2)
  243. cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR")
  244. index_path = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")
  245. bb.utils.mkdirhier(cvelogpath)
  246. fragment_file = os.path.basename(deploy_file)
  247. fragment_path = os.path.join(cvelogpath, fragment_file)
  248. with open(fragment_path, "w") as f:
  249. f.write(write_string)
  250. with open(index_path, "a+") as f:
  251. f.write("%s\n" % fragment_path)