summaryrefslogtreecommitdiffstats
path: root/meta/classes
diff options
context:
space:
mode:
authorMarta Rybczynska <rybczynska@gmail.com>2024-08-14 07:30:39 +0200
committerRichard Purdie <richard.purdie@linuxfoundation.org>2024-08-20 14:12:40 +0100
commit3859ff591568ac8879be602379ded9762d5fec26 (patch)
treecec9ae5115ac8399a6c5cbce0437a63e805ae1f5 /meta/classes
parent6e742bcb4f21179e87b1895aa3dc56c6bf9f7773 (diff)
downloadpoky-3859ff591568ac8879be602379ded9762d5fec26.tar.gz
vex.bbclass: add a new class
The "vex" class generates the minimum information that is necessary for VEX generation by an external CVE checking tool. It is a drop-in replacement of "cve-check". It uses the same variables from recipes to make the migration and backporting easier. The goal of this class is to allow generation of the CVE list of an image or distribution on-demand, including the latest information from vulnerability databases. Vulnerability data changes every day, so a status generated at build becomes out-of-date very soon. Research done for this work shows that the current VEX formats (CSAF and OpenVEX) do not provide enough information to generate such rolling information. Instead, we extract the needed data from recipe annotations (package names, CPEs, versions, CVE patches applied...) and store for later use in the format that is an extension of the CVE-check JSON output format. This output can be then used (separately or with SPDX of the same build) by an external tool to generate the vulnerability annotation and VEX statements in standard formats. (From OE-Core rev: 6352ad93a72e67d6dfa82e870222518a97c426fa) Signed-off-by: Marta Rybczynska <marta.rybczynska@syslinbit.com> Signed-off-by: Samantha Jalabert <samantha.jalabert@syslinbit.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta/classes')
-rw-r--r--meta/classes/vex.bbclass310
1 files changed, 310 insertions, 0 deletions
diff --git a/meta/classes/vex.bbclass b/meta/classes/vex.bbclass
new file mode 100644
index 0000000000..bb16e2a529
--- /dev/null
+++ b/meta/classes/vex.bbclass
@@ -0,0 +1,310 @@
1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: MIT
5#
6
7# This class is used to generate metadata needed by external
8# tools to check for vulnerabilities, for example CVEs.
9#
10# In order to use this class just inherit the class in the
11# local.conf file and it will add the generate_vex task for
12# every recipe. If an image is build it will generate a report
13# in DEPLOY_DIR_IMAGE for all the packages used, it will also
14# generate a file for all recipes used in the build.
15#
16# Variables use CVE_CHECK prefix to keep compatibility with
17# the cve-check class
18#
19# Example:
20# bitbake -c generate_vex openssl
21# bitbake core-image-sato
22# bitbake -k -c generate_vex universe
23#
24# The product name that the CVE database uses defaults to BPN, but may need to
25# be overriden per recipe (for example tiff.bb sets CVE_PRODUCT=libtiff).
26CVE_PRODUCT ??= "${BPN}"
27CVE_VERSION ??= "${PV}"
28
29CVE_CHECK_SUMMARY_DIR ?= "${LOG_DIR}/cve"
30
31CVE_CHECK_SUMMARY_FILE_NAME_JSON = "cve-summary.json"
32CVE_CHECK_SUMMARY_INDEX_PATH = "${CVE_CHECK_SUMMARY_DIR}/cve-summary-index.txt"
33
34CVE_CHECK_DIR ??= "${DEPLOY_DIR}/cve"
35CVE_CHECK_RECIPE_FILE_JSON ?= "${CVE_CHECK_DIR}/${PN}_cve.json"
36CVE_CHECK_MANIFEST_JSON ?= "${IMGDEPLOYDIR}/${IMAGE_NAME}.json"
37
38# Skip CVE Check for packages (PN)
39CVE_CHECK_SKIP_RECIPE ?= ""
40
41# Replace NVD DB check status for a given CVE. Each of CVE has to be mentioned
42# separately with optional detail and description for this status.
43#
44# CVE_STATUS[CVE-1234-0001] = "not-applicable-platform: Issue only applies on Windows"
45# CVE_STATUS[CVE-1234-0002] = "fixed-version: Fixed externally"
46#
47# Settings the same status and reason for multiple CVEs is possible
48# via CVE_STATUS_GROUPS variable.
49#
50# CVE_STATUS_GROUPS = "CVE_STATUS_WIN CVE_STATUS_PATCHED"
51#
52# CVE_STATUS_WIN = "CVE-1234-0001 CVE-1234-0003"
53# CVE_STATUS_WIN[status] = "not-applicable-platform: Issue only applies on Windows"
54# CVE_STATUS_PATCHED = "CVE-1234-0002 CVE-1234-0004"
55# CVE_STATUS_PATCHED[status] = "fixed-version: Fixed externally"
56#
57# All possible CVE statuses could be found in cve-check-map.conf
58# CVE_CHECK_STATUSMAP[not-applicable-platform] = "Ignored"
59# CVE_CHECK_STATUSMAP[fixed-version] = "Patched"
60#
61# CVE_CHECK_IGNORE is deprecated and CVE_STATUS has to be used instead.
62# Keep CVE_CHECK_IGNORE until other layers migrate to new variables
63CVE_CHECK_IGNORE ?= ""
64
65# Layers to be excluded
66CVE_CHECK_LAYER_EXCLUDELIST ??= ""
67
68# Layers to be included
69CVE_CHECK_LAYER_INCLUDELIST ??= ""
70
71
72# set to "alphabetical" for version using single alphabetical character as increment release
73CVE_VERSION_SUFFIX ??= ""
74
75python () {
76 if bb.data.inherits_class("cve-check", d):
77 raise bb.parse.SkipRecipe("Skipping recipe: found incompatible combination of cve-check and vex enabled at the same time.")
78
79 # Fallback all CVEs from CVE_CHECK_IGNORE to CVE_STATUS
80 cve_check_ignore = d.getVar("CVE_CHECK_IGNORE")
81 if cve_check_ignore:
82 bb.warn("CVE_CHECK_IGNORE is deprecated in favor of CVE_STATUS")
83 for cve in (d.getVar("CVE_CHECK_IGNORE") or "").split():
84 d.setVarFlag("CVE_STATUS", cve, "ignored")
85
86 # Process CVE_STATUS_GROUPS to set multiple statuses and optional detail or description at once
87 for cve_status_group in (d.getVar("CVE_STATUS_GROUPS") or "").split():
88 cve_group = d.getVar(cve_status_group)
89 if cve_group is not None:
90 for cve in cve_group.split():
91 d.setVarFlag("CVE_STATUS", cve, d.getVarFlag(cve_status_group, "status"))
92 else:
93 bb.warn("CVE_STATUS_GROUPS contains undefined variable %s" % cve_status_group)
94}
95
96def generate_json_report(d, out_path, link_path):
97 if os.path.exists(d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")):
98 import json
99 from oe.cve_check import cve_check_merge_jsons, update_symlinks
100
101 bb.note("Generating JSON CVE summary")
102 index_file = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")
103 summary = {"version":"1", "package": []}
104 with open(index_file) as f:
105 filename = f.readline()
106 while filename:
107 with open(filename.rstrip()) as j:
108 data = json.load(j)
109 cve_check_merge_jsons(summary, data)
110 filename = f.readline()
111
112 summary["package"].sort(key=lambda d: d['name'])
113
114 with open(out_path, "w") as f:
115 json.dump(summary, f, indent=2)
116
117 update_symlinks(out_path, link_path)
118
119python vex_save_summary_handler () {
120 import shutil
121 import datetime
122 from oe.cve_check import update_symlinks
123
124 cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR")
125
126 bb.utils.mkdirhier(cvelogpath)
127 timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
128
129 json_summary_link_name = os.path.join(cvelogpath, d.getVar("CVE_CHECK_SUMMARY_FILE_NAME_JSON"))
130 json_summary_name = os.path.join(cvelogpath, "cve-summary-%s.json" % (timestamp))
131 generate_json_report(d, json_summary_name, json_summary_link_name)
132 bb.plain("Complete CVE JSON report summary created at: %s" % json_summary_link_name)
133}
134
135addhandler vex_save_summary_handler
136vex_save_summary_handler[eventmask] = "bb.event.BuildCompleted"
137
138python do_generate_vex () {
139 """
140 Generate metadata needed for vulnerability checking for
141 the current recipe
142 """
143 from oe.cve_check import get_patched_cves
144
145 try:
146 patched_cves = get_patched_cves(d)
147 cves_status = []
148 products = d.getVar("CVE_PRODUCT").split()
149 for product in products:
150 if ":" in product:
151 _, product = product.split(":", 1)
152 cves_status.append([product, False])
153
154 except FileNotFoundError:
155 bb.fatal("Failure in searching patches")
156
157 cve_write_data_json(d, patched_cves, cves_status)
158}
159
160addtask generate_vex before do_build
161
162python vex_cleanup () {
163 """
164 Delete the file used to gather all the CVE information.
165 """
166 bb.utils.remove(e.data.getVar("CVE_CHECK_SUMMARY_INDEX_PATH"))
167}
168
169addhandler vex_cleanup
170vex_cleanup[eventmask] = "bb.event.BuildCompleted"
171
172python vex_write_rootfs_manifest () {
173 """
174 Create VEX/CVE manifest when building an image
175 """
176
177 import json
178 from oe.rootfs import image_list_installed_packages
179 from oe.cve_check import cve_check_merge_jsons, update_symlinks
180
181 deploy_file_json = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
182 if os.path.exists(deploy_file_json):
183 bb.utils.remove(deploy_file_json)
184
185 # Create a list of relevant recipies
186 recipies = set()
187 for pkg in list(image_list_installed_packages(d)):
188 pkg_info = os.path.join(d.getVar('PKGDATA_DIR'),
189 'runtime-reverse', pkg)
190 pkg_data = oe.packagedata.read_pkgdatafile(pkg_info)
191 recipies.add(pkg_data["PN"])
192
193 bb.note("Writing rootfs VEX manifest")
194 deploy_dir = d.getVar("IMGDEPLOYDIR")
195 link_name = d.getVar("IMAGE_LINK_NAME")
196
197 json_data = {"version":"1", "package": []}
198 text_data = ""
199
200 save_pn = d.getVar("PN")
201
202 for pkg in recipies:
203 # To be able to use the CVE_CHECK_RECIPE_FILE_JSON variable we have to evaluate
204 # it with the different PN names set each time.
205 d.setVar("PN", pkg)
206
207 pkgfilepath = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
208 if os.path.exists(pkgfilepath):
209 with open(pkgfilepath) as j:
210 data = json.load(j)
211 cve_check_merge_jsons(json_data, data)
212
213 d.setVar("PN", save_pn)
214
215 link_path = os.path.join(deploy_dir, "%s.json" % link_name)
216 manifest_name = d.getVar("CVE_CHECK_MANIFEST_JSON")
217
218 with open(manifest_name, "w") as f:
219 json.dump(json_data, f, indent=2)
220
221 update_symlinks(manifest_name, link_path)
222 bb.plain("Image VEX JSON report stored in: %s" % manifest_name)
223}
224
225ROOTFS_POSTPROCESS_COMMAND:prepend = "vex_write_rootfs_manifest; "
226do_rootfs[recrdeptask] += "do_generate_vex "
227do_populate_sdk[recrdeptask] += "do_generate_vex "
228
229def cve_write_data_json(d, cve_data, cve_status):
230 """
231 Prepare CVE data for the JSON format, then write it.
232 Done for each recipe.
233 """
234
235 from oe.cve_check import get_cpe_ids
236 import json
237
238 output = {"version":"1", "package": []}
239 nvd_link = "https://nvd.nist.gov/vuln/detail/"
240
241 fdir_name = d.getVar("FILE_DIRNAME")
242 layer = fdir_name.split("/")[-3]
243
244 include_layers = d.getVar("CVE_CHECK_LAYER_INCLUDELIST").split()
245 exclude_layers = d.getVar("CVE_CHECK_LAYER_EXCLUDELIST").split()
246
247 if exclude_layers and layer in exclude_layers:
248 return
249
250 if include_layers and layer not in include_layers:
251 return
252
253 product_data = []
254 for s in cve_status:
255 p = {"product": s[0], "cvesInRecord": "Yes"}
256 if s[1] == False:
257 p["cvesInRecord"] = "No"
258 product_data.append(p)
259 product_data = list({p['product']:p for p in product_data}.values())
260
261 package_version = "%s%s" % (d.getVar("EXTENDPE"), d.getVar("PV"))
262 cpes = get_cpe_ids(d.getVar("CVE_PRODUCT"), d.getVar("CVE_VERSION"))
263 package_data = {
264 "name" : d.getVar("PN"),
265 "layer" : layer,
266 "version" : package_version,
267 "products": product_data,
268 "cpes": cpes
269 }
270
271 cve_list = []
272
273 for cve in sorted(cve_data):
274 issue_link = "%s%s" % (nvd_link, cve)
275
276 cve_item = {
277 "id" : cve,
278 "status" : cve_data[cve]["abbrev-status"],
279 "link": issue_link,
280 }
281 if 'NVD-summary' in cve_data[cve]:
282 cve_item["summary"] = cve_data[cve]["NVD-summary"]
283 cve_item["scorev2"] = cve_data[cve]["NVD-scorev2"]
284 cve_item["scorev3"] = cve_data[cve]["NVD-scorev3"]
285 cve_item["vector"] = cve_data[cve]["NVD-vector"]
286 cve_item["vectorString"] = cve_data[cve]["NVD-vectorString"]
287 if 'status' in cve_data[cve]:
288 cve_item["detail"] = cve_data[cve]["status"]
289 if 'justification' in cve_data[cve]:
290 cve_item["description"] = cve_data[cve]["justification"]
291 if 'resource' in cve_data[cve]:
292 cve_item["patch-file"] = cve_data[cve]["resource"]
293 cve_list.append(cve_item)
294
295 package_data["issue"] = cve_list
296 output["package"].append(package_data)
297
298 deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
299
300 write_string = json.dumps(output, indent=2)
301
302 cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR")
303 index_path = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")
304 bb.utils.mkdirhier(cvelogpath)
305 fragment_file = os.path.basename(deploy_file)
306 fragment_path = os.path.join(cvelogpath, fragment_file)
307 with open(fragment_path, "w") as f:
308 f.write(write_string)
309 with open(index_path, "a+") as f:
310 f.write("%s\n" % fragment_path)