summaryrefslogtreecommitdiffstats
path: root/meta/classes
diff options
context:
space:
mode:
authorBenjamin Robin (Schneider Electric) <benjamin.robin@bootlin.com>2025-11-21 10:54:10 +0100
committerSteve Sakoman <steve@sakoman.com>2025-12-01 07:34:55 -0800
commitcf3b1a7e6df0434b2b60870305150389937072e7 (patch)
treee685283ca849f0e7ff7f26a580df5c1207c67ce5 /meta/classes
parent976648aa6087a8bd815bf9b1e2bae3d1e8f3600b (diff)
downloadpoky-cf3b1a7e6df0434b2b60870305150389937072e7.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. When back-porting this feature, the do_generate_vex() had to be modified to use the "old" get_patched_cves() API. (From OE-Core rev: 123a60bc19987e99d511b1f515e118022949be7e) 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> (cherry picked from commit 6352ad93a72e67d6dfa82e870222518a97c426fa) Signed-off-by: Benjamin Robin (Schneider Electric) <benjamin.robin@bootlin.com> Signed-off-by: Steve Sakoman <steve@sakoman.com>
Diffstat (limited to 'meta/classes')
-rw-r--r--meta/classes/vex.bbclass327
1 files changed, 327 insertions, 0 deletions
diff --git a/meta/classes/vex.bbclass b/meta/classes/vex.bbclass
new file mode 100644
index 0000000000..73dd9338a1
--- /dev/null
+++ b/meta/classes/vex.bbclass
@@ -0,0 +1,327 @@
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, decode_cve_status
144
145 cves_status = []
146 products = d.getVar("CVE_PRODUCT").split()
147 for product in products:
148 if ":" in product:
149 _, product = product.split(":", 1)
150 cves_status.append([product, False])
151
152 patched_cves = get_patched_cves(d)
153 cve_data = {}
154 for cve_id in (d.getVarFlags("CVE_STATUS") or {}):
155 mapping, detail, description = decode_cve_status(d, cve_id)
156 if not mapping or not detail:
157 bb.warn(f"Skipping {cve_id} — missing or unknown CVE status")
158 continue
159 cve_data[cve_id] = {
160 "abbrev-status": mapping,
161 "status": detail,
162 "justification": description
163 }
164 patched_cves.discard(cve_id)
165
166 # decode_cve_status is decoding CVE_STATUS, so patch files need to be hardcoded
167 for cve_id in patched_cves:
168 # fix-file-included is not available in scarthgap
169 cve_data[cve_id] = {
170 "abbrev-status": "Patched",
171 "status": "backported-patch",
172 }
173
174 cve_write_data_json(d, cve_data, cves_status)
175}
176
177addtask generate_vex before do_build
178
179python vex_cleanup () {
180 """
181 Delete the file used to gather all the CVE information.
182 """
183 bb.utils.remove(e.data.getVar("CVE_CHECK_SUMMARY_INDEX_PATH"))
184}
185
186addhandler vex_cleanup
187vex_cleanup[eventmask] = "bb.event.BuildCompleted"
188
189python vex_write_rootfs_manifest () {
190 """
191 Create VEX/CVE manifest when building an image
192 """
193
194 import json
195 from oe.rootfs import image_list_installed_packages
196 from oe.cve_check import cve_check_merge_jsons, update_symlinks
197
198 deploy_file_json = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
199 if os.path.exists(deploy_file_json):
200 bb.utils.remove(deploy_file_json)
201
202 # Create a list of relevant recipies
203 recipies = set()
204 for pkg in list(image_list_installed_packages(d)):
205 pkg_info = os.path.join(d.getVar('PKGDATA_DIR'),
206 'runtime-reverse', pkg)
207 pkg_data = oe.packagedata.read_pkgdatafile(pkg_info)
208 recipies.add(pkg_data["PN"])
209
210 bb.note("Writing rootfs VEX manifest")
211 deploy_dir = d.getVar("IMGDEPLOYDIR")
212 link_name = d.getVar("IMAGE_LINK_NAME")
213
214 json_data = {"version":"1", "package": []}
215 text_data = ""
216
217 save_pn = d.getVar("PN")
218
219 for pkg in recipies:
220 # To be able to use the CVE_CHECK_RECIPE_FILE_JSON variable we have to evaluate
221 # it with the different PN names set each time.
222 d.setVar("PN", pkg)
223
224 pkgfilepath = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
225 if os.path.exists(pkgfilepath):
226 with open(pkgfilepath) as j:
227 data = json.load(j)
228 cve_check_merge_jsons(json_data, data)
229
230 d.setVar("PN", save_pn)
231
232 link_path = os.path.join(deploy_dir, "%s.json" % link_name)
233 manifest_name = d.getVar("CVE_CHECK_MANIFEST_JSON")
234
235 with open(manifest_name, "w") as f:
236 json.dump(json_data, f, indent=2)
237
238 update_symlinks(manifest_name, link_path)
239 bb.plain("Image VEX JSON report stored in: %s" % manifest_name)
240}
241
242ROOTFS_POSTPROCESS_COMMAND:prepend = "vex_write_rootfs_manifest; "
243do_rootfs[recrdeptask] += "do_generate_vex "
244do_populate_sdk[recrdeptask] += "do_generate_vex "
245
246def cve_write_data_json(d, cve_data, cve_status):
247 """
248 Prepare CVE data for the JSON format, then write it.
249 Done for each recipe.
250 """
251
252 from oe.cve_check import get_cpe_ids
253 import json
254
255 output = {"version":"1", "package": []}
256 nvd_link = "https://nvd.nist.gov/vuln/detail/"
257
258 fdir_name = d.getVar("FILE_DIRNAME")
259 layer = fdir_name.split("/")[-3]
260
261 include_layers = d.getVar("CVE_CHECK_LAYER_INCLUDELIST").split()
262 exclude_layers = d.getVar("CVE_CHECK_LAYER_EXCLUDELIST").split()
263
264 if exclude_layers and layer in exclude_layers:
265 return
266
267 if include_layers and layer not in include_layers:
268 return
269
270 product_data = []
271 for s in cve_status:
272 p = {"product": s[0], "cvesInRecord": "Yes"}
273 if s[1] == False:
274 p["cvesInRecord"] = "No"
275 product_data.append(p)
276 product_data = list({p['product']:p for p in product_data}.values())
277
278 package_version = "%s%s" % (d.getVar("EXTENDPE"), d.getVar("PV"))
279 cpes = get_cpe_ids(d.getVar("CVE_PRODUCT"), d.getVar("CVE_VERSION"))
280 package_data = {
281 "name" : d.getVar("PN"),
282 "layer" : layer,
283 "version" : package_version,
284 "products": product_data,
285 "cpes": cpes
286 }
287
288 cve_list = []
289
290 for cve in sorted(cve_data):
291 issue_link = "%s%s" % (nvd_link, cve)
292
293 cve_item = {
294 "id" : cve,
295 "status" : cve_data[cve]["abbrev-status"],
296 "link": issue_link,
297 }
298 if 'NVD-summary' in cve_data[cve]:
299 cve_item["summary"] = cve_data[cve]["NVD-summary"]
300 cve_item["scorev2"] = cve_data[cve]["NVD-scorev2"]
301 cve_item["scorev3"] = cve_data[cve]["NVD-scorev3"]
302 cve_item["vector"] = cve_data[cve]["NVD-vector"]
303 cve_item["vectorString"] = cve_data[cve]["NVD-vectorString"]
304 if 'status' in cve_data[cve]:
305 cve_item["detail"] = cve_data[cve]["status"]
306 if 'justification' in cve_data[cve]:
307 cve_item["description"] = cve_data[cve]["justification"]
308 if 'resource' in cve_data[cve]:
309 cve_item["patch-file"] = cve_data[cve]["resource"]
310 cve_list.append(cve_item)
311
312 package_data["issue"] = cve_list
313 output["package"].append(package_data)
314
315 deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
316
317 write_string = json.dumps(output, indent=2)
318
319 cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR")
320 index_path = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")
321 bb.utils.mkdirhier(cvelogpath)
322 fragment_file = os.path.basename(deploy_file)
323 fragment_path = os.path.join(cvelogpath, fragment_file)
324 with open(fragment_path, "w") as f:
325 f.write(write_string)
326 with open(index_path, "a+") as f:
327 f.write("%s\n" % fragment_path)