diff options
Diffstat (limited to 'meta/classes/vex.bbclass')
-rw-r--r-- | meta/classes/vex.bbclass | 303 |
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). | ||
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 | from oe.cve_check import extend_cve_status | ||
80 | extend_cve_status(d) | ||
81 | } | ||
82 | |||
83 | def 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 | |||
106 | python 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 | |||
122 | addhandler vex_save_summary_handler | ||
123 | vex_save_summary_handler[eventmask] = "bb.event.BuildCompleted" | ||
124 | |||
125 | python 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 | |||
147 | addtask generate_vex before do_build | ||
148 | |||
149 | python 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 | |||
156 | addhandler vex_cleanup | ||
157 | vex_cleanup[eventmask] = "bb.event.BuildCompleted" | ||
158 | |||
159 | python 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 | |||
214 | ROOTFS_POSTPROCESS_COMMAND:prepend = "vex_write_rootfs_manifest; " | ||
215 | do_rootfs[recrdeptask] += "do_generate_vex " | ||
216 | do_populate_sdk[recrdeptask] += "do_generate_vex " | ||
217 | |||
218 | def 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) | ||