diff options
Diffstat (limited to 'meta/classes')
-rw-r--r-- | meta/classes/vex.bbclass | 310 |
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). | ||
26 | CVE_PRODUCT ??= "${BPN}" | ||
27 | CVE_VERSION ??= "${PV}" | ||
28 | |||
29 | CVE_CHECK_SUMMARY_DIR ?= "${LOG_DIR}/cve" | ||
30 | |||
31 | CVE_CHECK_SUMMARY_FILE_NAME_JSON = "cve-summary.json" | ||
32 | CVE_CHECK_SUMMARY_INDEX_PATH = "${CVE_CHECK_SUMMARY_DIR}/cve-summary-index.txt" | ||
33 | |||
34 | CVE_CHECK_DIR ??= "${DEPLOY_DIR}/cve" | ||
35 | CVE_CHECK_RECIPE_FILE_JSON ?= "${CVE_CHECK_DIR}/${PN}_cve.json" | ||
36 | CVE_CHECK_MANIFEST_JSON ?= "${IMGDEPLOYDIR}/${IMAGE_NAME}.json" | ||
37 | |||
38 | # Skip CVE Check for packages (PN) | ||
39 | CVE_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 | ||
63 | CVE_CHECK_IGNORE ?= "" | ||
64 | |||
65 | # Layers to be excluded | ||
66 | CVE_CHECK_LAYER_EXCLUDELIST ??= "" | ||
67 | |||
68 | # Layers to be included | ||
69 | CVE_CHECK_LAYER_INCLUDELIST ??= "" | ||
70 | |||
71 | |||
72 | # set to "alphabetical" for version using single alphabetical character as increment release | ||
73 | CVE_VERSION_SUFFIX ??= "" | ||
74 | |||
75 | python () { | ||
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 | |||
96 | def 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 | |||
119 | python 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 | |||
135 | addhandler vex_save_summary_handler | ||
136 | vex_save_summary_handler[eventmask] = "bb.event.BuildCompleted" | ||
137 | |||
138 | python 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 | |||
160 | addtask generate_vex before do_build | ||
161 | |||
162 | python 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 | |||
169 | addhandler vex_cleanup | ||
170 | vex_cleanup[eventmask] = "bb.event.BuildCompleted" | ||
171 | |||
172 | python 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 | |||
225 | ROOTFS_POSTPROCESS_COMMAND:prepend = "vex_write_rootfs_manifest; " | ||
226 | do_rootfs[recrdeptask] += "do_generate_vex " | ||
227 | do_populate_sdk[recrdeptask] += "do_generate_vex " | ||
228 | |||
229 | def 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) | ||