diff options
Diffstat (limited to 'meta/classes/cve-report.bbclass')
-rw-r--r-- | meta/classes/cve-report.bbclass | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/meta/classes/cve-report.bbclass b/meta/classes/cve-report.bbclass new file mode 100644 index 0000000000..35d58d0821 --- /dev/null +++ b/meta/classes/cve-report.bbclass @@ -0,0 +1,216 @@ +# Class to inherit when you want to generate a CVE reports. +# +# Generates package list file and package CVE report. +# +# Example: +# echo 'INHERIT += "cve-report"' >> conf/local.conf +# bitbake -c report_cve core-image-minimal +# +# Variables to be passed to "cvert-*" scripts: +# +# CVE_REPORT_MODE[foss] +# Path to the CVE FOSS report to be generated. +# +# CVE_REPORT_MODE[restore] +# Path to the CVE dump data file. +# +# E.g. for multiple MACHINEs: +# (1) generate CVE dump: +# cvert-update --store /path/to/cvedump $TEMP/nvdfeed +# (2) for mach in $(get_machine_list); do +# (source oe-init-build-env "build-$mach"; +# echo 'CVE_REPORT_MODE[restore] = "/path/to/cvedump"' >> conf/local.conf; +# echo 'CVE_REPORT_MODE[foss] = "/path/to/report-foss-'${mach}'"' >> conf/local.conf; +# MACHINE=$mach bitbake -c report_cve core-image-minimal) +# done +# +# CVE_REPORT_MODE[offline] +# Either "0" or "1". Offline mode ("--offline" parameter for cvert-* scripts). +# +# CVE_REPORT_MODE[feeddir] +# Path to the NVD feed directory. +# +# CVE_REPORT_MODE[packagelist] +# Path to the package list file to be generated. +# +# CVE_REPORT_MODE[packageonly] +# Either "0" or "1". Generate package list file, then stop. +# +# CVE_REPORT_MODE[blacklist] +# Ignore specific class. +# + +CVE_REPORT_MODE[foss] ?= "${LOG_DIR}/cvert/report-foss.txt" +CVE_REPORT_MODE[offline] ?= "0" +CVE_REPORT_MODE[feeddir] ?= "${LOG_DIR}/nvdfeeds" +CVE_REPORT_MODE[packagelist] ?= "${LOG_DIR}/cvert/package.lst" +CVE_REPORT_MODE[packageonly] ?= "0" +CVE_REPORT_MODE[blacklist] ?= "native,nativesdk,cross,crosssdk,cross-canadian,packagegroup,image" + +CVE_PRODUCT ??= "${BPN}" +CVE_VERSION ??= "${PV}" + +addhandler generate_report_handler +generate_report_handler[eventmask] = "bb.event.BuildCompleted" + +def cvert_update(d): + """Update NVD storage and prepare CVE dump""" + + import tempfile + import subprocess + + bb.utils.export_proxies(d) + + dump = os.path.join(d.getVar("LOG_DIR"), "cvedump") + + bb.note("Updating CVE database: %s" % dump) + + cmd = [ + "cvert-update", + "--store", dump, + "--debug", + d.getVarFlag("CVE_REPORT_MODE", "feeddir") + ] + + if d.getVarFlag("CVE_REPORT_MODE", "offline") != "0": + cmd.append("--offline") + + try: + bb.debug(2, "Call '%s'" % " ".join(cmd)) + output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode() + bb.debug(2, "Output: %s" % output) + except subprocess.CalledProcessError as e: + bb.error("Failed to run cvert-update: '%s'\n%s: %s" % (" ".join(cmd), e, e.output)) + + return dump + +# copied from cve-check.bbclass +def get_patches_cves(d): + """Get patches that solve CVEs using the "CVE: " tag""" + + import re + + pn = d.getVar("PN") + cve_match = re.compile("CVE:( CVE\-\d{4}\-\d+)+") + + # Matches last CVE-1234-211432 in the file name, also if written + # with small letters. Not supporting multiple CVE id's in a single + # file name. + cve_file_name_match = re.compile(".*([Cc][Vv][Ee]\-\d{4}\-\d+)") + + patched_cves = set() + bb.debug(2, "Looking for patches that solves CVEs for %s" % pn) + for url in src_patches(d): + patch_file = bb.fetch.decodeurl(url)[2] + + # Check patch file name for CVE ID + fname_match = cve_file_name_match.search(patch_file) + if fname_match: + cve = fname_match.group(1).upper() + patched_cves.add(cve) + bb.debug(2, "Found CVE %s from patch file name %s" % (cve, patch_file)) + + with open(patch_file, "r", encoding="utf-8") as f: + try: + patch_text = f.read() + except UnicodeDecodeError: + bb.debug(1, "Failed to read patch %s using UTF-8 encoding" + " trying with iso8859-1" % patch_file) + f.close() + with open(patch_file, "r", encoding="iso8859-1") as f: + patch_text = f.read() + + # Search for one or more "CVE: " lines + text_match = False + for match in cve_match.finditer(patch_text): + # Get only the CVEs without the "CVE: " tag + cves = patch_text[match.start()+5:match.end()] + for cve in cves.split(): + bb.debug(2, "Patch %s solves %s" % (patch_file, cve)) + patched_cves.add(cve) + text_match = True + + if not fname_match and not text_match: + bb.debug(2, "Patch %s doesn't solve CVEs" % patch_file) + + return patched_cves + + +python generate_report_handler() { + if d.getVarFlag("CVE_REPORT_MODE", "packageonly") != "0": + return + + import subprocess + + restore = d.getVarFlag("CVE_REPORT_MODE", "restore") + + if not restore: + restore = cvert_update(d) + + if os.path.exists(d.getVarFlag("CVE_REPORT_MODE", "packagelist")): + report_foss = d.getVarFlag("CVE_REPORT_MODE", "foss") + + bb.note("Generating CVE FOSS report: %s" % report_foss) + + cmd = [ + "cvert-foss", + "--restore", restore, + "--output", report_foss, + d.getVarFlag("CVE_REPORT_MODE", "packagelist") + ] + + try: + bb.debug(2, "Call '%s'" % " ".join(cmd)) + output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode() + bb.debug(2, "Output: %s" % output) + except subprocess.CalledProcessError as e: + bb.error("Failed to run cvert-foss: '%s'\n%s: %s" % (" ".join(cmd), e, e.output)) +} + +addhandler build_started +build_started[eventmask] = "bb.event.BuildStarted" + +python build_started() { + packagelist = d.getVarFlag("CVE_REPORT_MODE", "packagelist") + bb.utils.remove(packagelist) + bb.utils.mkdirhier(os.path.dirname(packagelist)) + bb.note("Package list: ", packagelist) +} + +addtask do_report_cve after do_report_patched + +do_report_cve[recrdeptask] = "do_report_cve do_report_patched" +do_report_cve[recideptask] = "do_${BB_DEFAULT_TASK}" +do_report_cve[nostamp] = "1" + +do_report_cve() { + : +} + +python do_report_patched() { + if not d.getVar("SRC_URI"): + return + + cve_product = d.getVar("CVE_PRODUCT") + + if not cve_product: + return + + cve_version = d.getVar("CVE_VERSION") + patched_cves = get_patches_cves(d) + + with open(d.getVarFlag("CVE_REPORT_MODE", "packagelist"), "a") as fil: + fil.write("%s,%s,%s\n" % (cve_product, cve_version, " ".join(patched_cves))) + bb.debug(2, "Append to package-list: '%s,%s,%s'" % (cve_product, cve_version, " ".join(patched_cves))) +} + +addtask do_report_patched after do_unpack before do_build + +do_report_patched[nostamp] = "1" + +python() { + for b in d.getVarFlag("CVE_REPORT_MODE", "blacklist").split(","): + if bb.data.inherits_class(b, d): + bb.build.deltask("do_report_patched", d) + break +} |