diff options
| author | Marta Rybczynska <rybczynska@gmail.com> | 2022-03-29 14:54:31 +0200 |
|---|---|---|
| committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2022-03-30 13:07:41 +0100 |
| commit | 777f1d42b62ab482efa5a24600f4aeba1b156c64 (patch) | |
| tree | 57f34b66df4db825abf4802101e689ca38abd2c0 /meta | |
| parent | bbdf96885dbd8c3f5e2e9f084571ca659a809016 (diff) | |
| download | poky-777f1d42b62ab482efa5a24600f4aeba1b156c64.tar.gz | |
cve-check: add json format
Add an option to output the CVE check in a JSON-based format.
This format is easier to parse in software than the original
text-based one and allows post-processing by other tools.
Output formats are now handed by CVE_CHECK_FORMAT_TEXT and
CVE_CHECK_FORMAT_JSON. Both of them are enabled by default.
The JSON output format gets generated in a similar way to the
text format with the exception of the manifest: appending to
JSON arrays requires parsing the file. Because of that we
first write JSON fragments and then assemble them in one pass
at the end.
(From OE-Core rev: df567de36ae5964bee433ebb97e8bf702034994a)
Signed-off-by: Marta Rybczynska <marta.rybczynska@huawei.com>
Signed-off-by: Alexandre Belloni <alexandre.belloni@bootlin.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta')
| -rw-r--r-- | meta/classes/cve-check.bbclass | 144 | ||||
| -rw-r--r-- | meta/lib/oe/cve_check.py | 16 |
2 files changed, 159 insertions, 1 deletions
diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass index dfad10c22b..f574f5daa4 100644 --- a/meta/classes/cve-check.bbclass +++ b/meta/classes/cve-check.bbclass | |||
| @@ -34,15 +34,27 @@ CVE_CHECK_TMP_FILE ?= "${TMPDIR}/cve_check" | |||
| 34 | CVE_CHECK_SUMMARY_DIR ?= "${LOG_DIR}/cve" | 34 | CVE_CHECK_SUMMARY_DIR ?= "${LOG_DIR}/cve" |
| 35 | CVE_CHECK_SUMMARY_FILE_NAME ?= "cve-summary" | 35 | CVE_CHECK_SUMMARY_FILE_NAME ?= "cve-summary" |
| 36 | CVE_CHECK_SUMMARY_FILE ?= "${CVE_CHECK_SUMMARY_DIR}/${CVE_CHECK_SUMMARY_FILE_NAME}" | 36 | CVE_CHECK_SUMMARY_FILE ?= "${CVE_CHECK_SUMMARY_DIR}/${CVE_CHECK_SUMMARY_FILE_NAME}" |
| 37 | CVE_CHECK_SUMMARY_FILE_NAME_JSON = "cve-summary.json" | ||
| 38 | CVE_CHECK_SUMMARY_INDEX_PATH = "${CVE_CHECK_SUMMARY_DIR}/cve-summary-index.txt" | ||
| 39 | |||
| 40 | CVE_CHECK_LOG_JSON ?= "${T}/cve.json" | ||
| 37 | 41 | ||
| 38 | CVE_CHECK_DIR ??= "${DEPLOY_DIR}/cve" | 42 | CVE_CHECK_DIR ??= "${DEPLOY_DIR}/cve" |
| 39 | CVE_CHECK_RECIPE_FILE ?= "${CVE_CHECK_DIR}/${PN}" | 43 | CVE_CHECK_RECIPE_FILE ?= "${CVE_CHECK_DIR}/${PN}" |
| 44 | CVE_CHECK_RECIPE_FILE_JSON ?= "${CVE_CHECK_DIR}/${PN}_cve.json" | ||
| 40 | CVE_CHECK_MANIFEST ?= "${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${IMAGE_NAME_SUFFIX}.cve" | 45 | CVE_CHECK_MANIFEST ?= "${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${IMAGE_NAME_SUFFIX}.cve" |
| 46 | CVE_CHECK_MANIFEST_JSON ?= "${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${IMAGE_NAME_SUFFIX}.json" | ||
| 41 | CVE_CHECK_COPY_FILES ??= "1" | 47 | CVE_CHECK_COPY_FILES ??= "1" |
| 42 | CVE_CHECK_CREATE_MANIFEST ??= "1" | 48 | CVE_CHECK_CREATE_MANIFEST ??= "1" |
| 43 | 49 | ||
| 44 | CVE_CHECK_REPORT_PATCHED ??= "1" | 50 | CVE_CHECK_REPORT_PATCHED ??= "1" |
| 45 | 51 | ||
| 52 | # Provide text output | ||
| 53 | CVE_CHECK_FORMAT_TEXT ??= "1" | ||
| 54 | |||
| 55 | # Provide JSON output | ||
| 56 | CVE_CHECK_FORMAT_JSON ??= "1" | ||
| 57 | |||
| 46 | # Skip CVE Check for packages (PN) | 58 | # Skip CVE Check for packages (PN) |
| 47 | CVE_CHECK_SKIP_RECIPE ?= "" | 59 | CVE_CHECK_SKIP_RECIPE ?= "" |
| 48 | 60 | ||
| @@ -120,6 +132,7 @@ python cve_check_cleanup () { | |||
| 120 | Delete the file used to gather all the CVE information. | 132 | Delete the file used to gather all the CVE information. |
| 121 | """ | 133 | """ |
| 122 | bb.utils.remove(e.data.getVar("CVE_CHECK_TMP_FILE")) | 134 | bb.utils.remove(e.data.getVar("CVE_CHECK_TMP_FILE")) |
| 135 | bb.utils.remove(e.data.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")) | ||
| 123 | } | 136 | } |
| 124 | 137 | ||
| 125 | addhandler cve_check_cleanup | 138 | addhandler cve_check_cleanup |
| @@ -131,11 +144,15 @@ python cve_check_write_rootfs_manifest () { | |||
| 131 | """ | 144 | """ |
| 132 | 145 | ||
| 133 | import shutil | 146 | import shutil |
| 147 | from oe.cve_check import cve_check_merge_jsons | ||
| 134 | 148 | ||
| 135 | if d.getVar("CVE_CHECK_COPY_FILES") == "1": | 149 | if d.getVar("CVE_CHECK_COPY_FILES") == "1": |
| 136 | deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE") | 150 | deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE") |
| 137 | if os.path.exists(deploy_file): | 151 | if os.path.exists(deploy_file): |
| 138 | bb.utils.remove(deploy_file) | 152 | bb.utils.remove(deploy_file) |
| 153 | deploy_file_json = d.getVar("CVE_CHECK_RECIPE_FILE_JSON") | ||
| 154 | if os.path.exists(deploy_file_json): | ||
| 155 | bb.utils.remove(deploy_file_json) | ||
| 139 | 156 | ||
| 140 | if os.path.exists(d.getVar("CVE_CHECK_TMP_FILE")): | 157 | if os.path.exists(d.getVar("CVE_CHECK_TMP_FILE")): |
| 141 | bb.note("Writing rootfs CVE manifest") | 158 | bb.note("Writing rootfs CVE manifest") |
| @@ -154,6 +171,26 @@ python cve_check_write_rootfs_manifest () { | |||
| 154 | os.remove(manifest_link) | 171 | os.remove(manifest_link) |
| 155 | os.symlink(os.path.basename(manifest_name), manifest_link) | 172 | os.symlink(os.path.basename(manifest_name), manifest_link) |
| 156 | bb.plain("Image CVE report stored in: %s" % manifest_name) | 173 | bb.plain("Image CVE report stored in: %s" % manifest_name) |
| 174 | |||
| 175 | if os.path.exists(d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")): | ||
| 176 | import json | ||
| 177 | bb.note("Generating JSON CVE manifest") | ||
| 178 | deploy_dir = d.getVar("DEPLOY_DIR_IMAGE") | ||
| 179 | link_name = d.getVar("IMAGE_LINK_NAME") | ||
| 180 | manifest_name = d.getVar("CVE_CHECK_MANIFEST_JSON") | ||
| 181 | index_file = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH") | ||
| 182 | manifest = {"version":"1", "package": []} | ||
| 183 | with open(index_file) as f: | ||
| 184 | filename = f.readline() | ||
| 185 | while filename: | ||
| 186 | with open(filename.rstrip()) as j: | ||
| 187 | data = json.load(j) | ||
| 188 | cve_check_merge_jsons(manifest, data) | ||
| 189 | filename = f.readline() | ||
| 190 | |||
| 191 | with open(manifest_name, "w") as f: | ||
| 192 | json.dump(manifest, f, indent=2) | ||
| 193 | bb.plain("Image CVE report stored in: %s" % manifest_name) | ||
| 157 | } | 194 | } |
| 158 | 195 | ||
| 159 | ROOTFS_POSTPROCESS_COMMAND:prepend = "${@'cve_check_write_rootfs_manifest; ' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}" | 196 | ROOTFS_POSTPROCESS_COMMAND:prepend = "${@'cve_check_write_rootfs_manifest; ' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}" |
| @@ -280,7 +317,7 @@ def get_cve_info(d, cves): | |||
| 280 | conn.close() | 317 | conn.close() |
| 281 | return cve_data | 318 | return cve_data |
| 282 | 319 | ||
| 283 | def cve_write_data(d, patched, unpatched, ignored, cve_data): | 320 | def cve_write_data_text(d, patched, unpatched, ignored, cve_data): |
| 284 | """ | 321 | """ |
| 285 | Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and | 322 | Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and |
| 286 | CVE manifest if enabled. | 323 | CVE manifest if enabled. |
| @@ -346,3 +383,108 @@ def cve_write_data(d, patched, unpatched, ignored, cve_data): | |||
| 346 | 383 | ||
| 347 | with open(d.getVar("CVE_CHECK_TMP_FILE"), "a") as f: | 384 | with open(d.getVar("CVE_CHECK_TMP_FILE"), "a") as f: |
| 348 | f.write("%s" % write_string) | 385 | f.write("%s" % write_string) |
| 386 | |||
| 387 | def cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file): | ||
| 388 | """ | ||
| 389 | Write CVE information in the JSON format: to WORKDIR; and to | ||
| 390 | CVE_CHECK_DIR, if CVE manifest if enabled, write fragment | ||
| 391 | files that will be assembled at the end in cve_check_write_rootfs_manifest. | ||
| 392 | """ | ||
| 393 | |||
| 394 | import json | ||
| 395 | |||
| 396 | write_string = json.dumps(output, indent=2) | ||
| 397 | with open(direct_file, "w") as f: | ||
| 398 | bb.note("Writing file %s with CVE information" % direct_file) | ||
| 399 | f.write(write_string) | ||
| 400 | |||
| 401 | if d.getVar("CVE_CHECK_COPY_FILES") == "1": | ||
| 402 | bb.utils.mkdirhier(os.path.dirname(deploy_file)) | ||
| 403 | with open(deploy_file, "w") as f: | ||
| 404 | f.write(write_string) | ||
| 405 | |||
| 406 | if d.getVar("CVE_CHECK_CREATE_MANIFEST") == "1": | ||
| 407 | cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR") | ||
| 408 | index_path = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH") | ||
| 409 | bb.utils.mkdirhier(cvelogpath) | ||
| 410 | fragment_file = os.path.basename(deploy_file) | ||
| 411 | fragment_path = os.path.join(cvelogpath, fragment_file) | ||
| 412 | with open(fragment_path, "w") as f: | ||
| 413 | f.write(write_string) | ||
| 414 | with open(index_path, "a+") as f: | ||
| 415 | f.write("%s\n" % fragment_path) | ||
| 416 | |||
| 417 | def cve_write_data_json(d, patched, unpatched, ignored, cve_data): | ||
| 418 | """ | ||
| 419 | Prepare CVE data for the JSON format, then write it. | ||
| 420 | """ | ||
| 421 | |||
| 422 | output = {"version":"1", "package": []} | ||
| 423 | nvd_link = "https://nvd.nist.gov/vuln/detail/" | ||
| 424 | |||
| 425 | fdir_name = d.getVar("FILE_DIRNAME") | ||
| 426 | layer = fdir_name.split("/")[-3] | ||
| 427 | |||
| 428 | include_layers = d.getVar("CVE_CHECK_LAYER_INCLUDELIST").split() | ||
| 429 | exclude_layers = d.getVar("CVE_CHECK_LAYER_EXCLUDELIST").split() | ||
| 430 | |||
| 431 | if exclude_layers and layer in exclude_layers: | ||
| 432 | return | ||
| 433 | |||
| 434 | if include_layers and layer not in include_layers: | ||
| 435 | return | ||
| 436 | |||
| 437 | unpatched_cves = [] | ||
| 438 | |||
| 439 | package_version = "%s%s" % (d.getVar("EXTENDPE"), d.getVar("PV")) | ||
| 440 | package_data = { | ||
| 441 | "name" : d.getVar("PN"), | ||
| 442 | "layer" : layer, | ||
| 443 | "version" : package_version | ||
| 444 | } | ||
| 445 | cve_list = [] | ||
| 446 | |||
| 447 | for cve in sorted(cve_data): | ||
| 448 | is_patched = cve in patched | ||
| 449 | status = "Unpatched" | ||
| 450 | if is_patched and (d.getVar("CVE_CHECK_REPORT_PATCHED") != "1"): | ||
| 451 | continue | ||
| 452 | if cve in ignored: | ||
| 453 | status = "Ignored" | ||
| 454 | elif is_patched: | ||
| 455 | status = "Patched" | ||
| 456 | else: | ||
| 457 | # default value of status is Unpatched | ||
| 458 | unpatched_cves.append(cve) | ||
| 459 | |||
| 460 | issue_link = "%s%s" % (nvd_link, cve) | ||
| 461 | |||
| 462 | cve_item = { | ||
| 463 | "id" : cve, | ||
| 464 | "summary" : cve_data[cve]["summary"], | ||
| 465 | "scorev2" : cve_data[cve]["scorev2"], | ||
| 466 | "scorev3" : cve_data[cve]["scorev3"], | ||
| 467 | "vector" : cve_data[cve]["vector"], | ||
| 468 | "status" : status, | ||
| 469 | "link": issue_link | ||
| 470 | } | ||
| 471 | cve_list.append(cve_item) | ||
| 472 | |||
| 473 | package_data["issue"] = cve_list | ||
| 474 | output["package"].append(package_data) | ||
| 475 | |||
| 476 | direct_file = d.getVar("CVE_CHECK_LOG_JSON") | ||
| 477 | deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE_JSON") | ||
| 478 | manifest_file = d.getVar("CVE_CHECK_SUMMARY_FILE_NAME_JSON") | ||
| 479 | |||
| 480 | cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file) | ||
| 481 | |||
| 482 | def cve_write_data(d, patched, unpatched, ignored, cve_data): | ||
| 483 | """ | ||
| 484 | Write CVE data in each enabled format. | ||
| 485 | """ | ||
| 486 | |||
| 487 | if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1": | ||
| 488 | cve_write_data_text(d, patched, unpatched, ignored, cve_data) | ||
| 489 | if d.getVar("CVE_CHECK_FORMAT_JSON") == "1": | ||
| 490 | cve_write_data_json(d, patched, unpatched, ignored, cve_data) | ||
diff --git a/meta/lib/oe/cve_check.py b/meta/lib/oe/cve_check.py index 0302beeb4a..e445b7a6ae 100644 --- a/meta/lib/oe/cve_check.py +++ b/meta/lib/oe/cve_check.py | |||
| @@ -146,3 +146,19 @@ def get_cpe_ids(cve_product, version): | |||
| 146 | cpe_ids.append(cpe_id) | 146 | cpe_ids.append(cpe_id) |
| 147 | 147 | ||
| 148 | return cpe_ids | 148 | return cpe_ids |
| 149 | |||
| 150 | def cve_check_merge_jsons(output, data): | ||
| 151 | """ | ||
| 152 | Merge the data in the "package" property to the main data file | ||
| 153 | output | ||
| 154 | """ | ||
| 155 | if output["version"] != data["version"]: | ||
| 156 | bb.error("Version mismatch when merging JSON outputs") | ||
| 157 | return | ||
| 158 | |||
| 159 | for product in output["package"]: | ||
| 160 | if product["name"] == data["package"][0]["name"]: | ||
| 161 | bb.error("Error adding the same package twice") | ||
| 162 | return | ||
| 163 | |||
| 164 | output["package"].append(data["package"][0]) | ||
