summaryrefslogtreecommitdiffstats
path: root/meta/classes/vex.bbclass
diff options
context:
space:
mode:
Diffstat (limited to 'meta/classes/vex.bbclass')
-rw-r--r--meta/classes/vex.bbclass303
1 files changed, 303 insertions, 0 deletions
diff --git a/meta/classes/vex.bbclass b/meta/classes/vex.bbclass
new file mode 100644
index 0000000000..402d8e0d96
--- /dev/null
+++ b/meta/classes/vex.bbclass
@@ -0,0 +1,303 @@
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 from oe.cve_check import extend_cve_status
80 extend_cve_status(d)
81}
82
83def generate_json_report(d, out_path, link_path):
84 if os.path.exists(d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")):
85 import json
86 from oe.cve_check import cve_check_merge_jsons, update_symlinks
87
88 bb.note("Generating JSON CVE summary")
89 index_file = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")
90 summary = {"version":"1", "package": []}
91 with open(index_file) as f:
92 filename = f.readline()
93 while filename:
94 with open(filename.rstrip()) as j:
95 data = json.load(j)
96 cve_check_merge_jsons(summary, data)
97 filename = f.readline()
98
99 summary["package"].sort(key=lambda d: d['name'])
100
101 with open(out_path, "w") as f:
102 json.dump(summary, f, indent=2)
103
104 update_symlinks(out_path, link_path)
105
106python vex_save_summary_handler () {
107 import shutil
108 import datetime
109 from oe.cve_check import update_symlinks
110
111 cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR")
112
113 bb.utils.mkdirhier(cvelogpath)
114 timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
115
116 json_summary_link_name = os.path.join(cvelogpath, d.getVar("CVE_CHECK_SUMMARY_FILE_NAME_JSON"))
117 json_summary_name = os.path.join(cvelogpath, "cve-summary-%s.json" % (timestamp))
118 generate_json_report(d, json_summary_name, json_summary_link_name)
119 bb.plain("Complete CVE JSON report summary created at: %s" % json_summary_link_name)
120}
121
122addhandler vex_save_summary_handler
123vex_save_summary_handler[eventmask] = "bb.event.BuildCompleted"
124
125python do_generate_vex () {
126 """
127 Generate metadata needed for vulnerability checking for
128 the current recipe
129 """
130 from oe.cve_check import get_patched_cves
131
132 try:
133 patched_cves = get_patched_cves(d)
134 cves_status = []
135 products = d.getVar("CVE_PRODUCT").split()
136 for product in products:
137 if ":" in product:
138 _, product = product.split(":", 1)
139 cves_status.append([product, False])
140
141 except FileNotFoundError:
142 bb.fatal("Failure in searching patches")
143
144 cve_write_data_json(d, patched_cves, cves_status)
145}
146
147addtask generate_vex before do_build
148
149python vex_cleanup () {
150 """
151 Delete the file used to gather all the CVE information.
152 """
153 bb.utils.remove(e.data.getVar("CVE_CHECK_SUMMARY_INDEX_PATH"))
154}
155
156addhandler vex_cleanup
157vex_cleanup[eventmask] = "bb.event.BuildCompleted"
158
159python vex_write_rootfs_manifest () {
160 """
161 Create VEX/CVE manifest when building an image
162 """
163
164 import json
165 from oe.rootfs import image_list_installed_packages
166 from oe.cve_check import cve_check_merge_jsons, update_symlinks
167
168 deploy_file_json = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
169 if os.path.exists(deploy_file_json):
170 bb.utils.remove(deploy_file_json)
171
172 # Create a list of relevant recipies
173 recipies = set()
174 for pkg in list(image_list_installed_packages(d)):
175 pkg_info = os.path.join(d.getVar('PKGDATA_DIR'),
176 'runtime-reverse', pkg)
177 pkg_data = oe.packagedata.read_pkgdatafile(pkg_info)
178 recipies.add(pkg_data["PN"])
179
180 bb.note("Writing rootfs VEX manifest")
181 deploy_dir = d.getVar("IMGDEPLOYDIR")
182 link_name = d.getVar("IMAGE_LINK_NAME")
183
184 json_data = {"version":"1", "package": []}
185 text_data = ""
186
187 save_pn = d.getVar("PN")
188
189 for pkg in recipies:
190 # To be able to use the CVE_CHECK_RECIPE_FILE_JSON variable we have to evaluate
191 # it with the different PN names set each time.
192 d.setVar("PN", pkg)
193
194 pkgfilepath = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
195 if os.path.exists(pkgfilepath):
196 with open(pkgfilepath) as j:
197 data = json.load(j)
198 cve_check_merge_jsons(json_data, data)
199 else:
200 bb.warn("Missing cve file for %s" % pkg)
201
202 d.setVar("PN", save_pn)
203
204 link_path = os.path.join(deploy_dir, "%s.json" % link_name)
205 manifest_name = d.getVar("CVE_CHECK_MANIFEST_JSON")
206
207 with open(manifest_name, "w") as f:
208 json.dump(json_data, f, indent=2)
209
210 update_symlinks(manifest_name, link_path)
211 bb.plain("Image VEX JSON report stored in: %s" % manifest_name)
212}
213
214ROOTFS_POSTPROCESS_COMMAND:prepend = "vex_write_rootfs_manifest; "
215do_rootfs[recrdeptask] += "do_generate_vex "
216do_populate_sdk[recrdeptask] += "do_generate_vex "
217
218def cve_write_data_json(d, cve_data, cve_status):
219 """
220 Prepare CVE data for the JSON format, then write it.
221 Done for each recipe.
222 """
223
224 from oe.cve_check import get_cpe_ids
225 import json
226
227 output = {"version":"1", "package": []}
228 nvd_link = "https://nvd.nist.gov/vuln/detail/"
229
230 fdir_name = d.getVar("FILE_DIRNAME")
231 layer = fdir_name.split("/")[-3]
232
233 include_layers = d.getVar("CVE_CHECK_LAYER_INCLUDELIST").split()
234 exclude_layers = d.getVar("CVE_CHECK_LAYER_EXCLUDELIST").split()
235
236 if exclude_layers and layer in exclude_layers:
237 return
238
239 if include_layers and layer not in include_layers:
240 return
241
242 product_data = []
243 for s in cve_status:
244 p = {"product": s[0], "cvesInRecord": "Yes"}
245 if s[1] == False:
246 p["cvesInRecord"] = "No"
247 product_data.append(p)
248 product_data = list({p['product']:p for p in product_data}.values())
249
250 package_version = "%s%s" % (d.getVar("EXTENDPE"), d.getVar("PV"))
251 cpes = get_cpe_ids(d.getVar("CVE_PRODUCT"), d.getVar("CVE_VERSION"))
252 package_data = {
253 "name" : d.getVar("PN"),
254 "layer" : layer,
255 "version" : package_version,
256 "products": product_data,
257 "cpes": cpes
258 }
259
260 cve_list = []
261
262 for cve in sorted(cve_data):
263 issue_link = "%s%s" % (nvd_link, cve)
264
265 cve_item = {
266 "id" : cve,
267 "status" : cve_data[cve]["abbrev-status"],
268 "link": issue_link,
269 }
270 if 'NVD-summary' in cve_data[cve]:
271 cve_item["summary"] = cve_data[cve]["NVD-summary"]
272 cve_item["scorev2"] = cve_data[cve]["NVD-scorev2"]
273 cve_item["scorev3"] = cve_data[cve]["NVD-scorev3"]
274 cve_item["scorev4"] = cve_data[cve]["NVD-scorev4"]
275 cve_item["vector"] = cve_data[cve]["NVD-vector"]
276 cve_item["vectorString"] = cve_data[cve]["NVD-vectorString"]
277 if 'status' in cve_data[cve]:
278 cve_item["detail"] = cve_data[cve]["status"]
279 if 'justification' in cve_data[cve]:
280 cve_item["description"] = cve_data[cve]["justification"]
281 if 'resource' in cve_data[cve]:
282 cve_item["patch-file"] = cve_data[cve]["resource"]
283 cve_list.append(cve_item)
284
285 package_data["issue"] = cve_list
286 output["package"].append(package_data)
287
288 deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
289
290 write_string = json.dumps(output, indent=2)
291
292 cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR")
293 index_path = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")
294 bb.utils.mkdirhier(cvelogpath)
295 bb.utils.mkdirhier(os.path.dirname(deploy_file))
296 fragment_file = os.path.basename(deploy_file)
297 fragment_path = os.path.join(cvelogpath, fragment_file)
298 with open(fragment_path, "w") as f:
299 f.write(write_string)
300 with open(deploy_file, "w") as f:
301 f.write(write_string)
302 with open(index_path, "a+") as f:
303 f.write("%s\n" % fragment_path)