diff options
Diffstat (limited to 'import-layers/yocto-poky/meta/classes/cve-check.bbclass')
-rw-r--r-- | import-layers/yocto-poky/meta/classes/cve-check.bbclass | 290 |
1 files changed, 0 insertions, 290 deletions
diff --git a/import-layers/yocto-poky/meta/classes/cve-check.bbclass b/import-layers/yocto-poky/meta/classes/cve-check.bbclass deleted file mode 100644 index 537659df1..000000000 --- a/import-layers/yocto-poky/meta/classes/cve-check.bbclass +++ /dev/null @@ -1,290 +0,0 @@ -# This class is used to check recipes against public CVEs. -# -# In order to use this class just inherit the class in the -# local.conf file and it will add the cve_check task for -# every recipe. The task can be used per recipe, per image, -# or using the special cases "world" and "universe". The -# cve_check task will print a warning for every unpatched -# CVE found and generate a file in the recipe WORKDIR/cve -# directory. If an image is build it will generate a report -# in DEPLOY_DIR_IMAGE for all the packages used. -# -# Example: -# bitbake -c cve_check openssl -# bitbake core-image-sato -# bitbake -k -c cve_check universe -# -# DISCLAIMER -# -# This class/tool is meant to be used as support and not -# the only method to check against CVEs. Running this tool -# doesn't guarantee your packages are free of CVEs. - -# The product name that the CVE database uses. Defaults to BPN, but may need to -# be overriden per recipe (for example tiff.bb sets CVE_PRODUCT=libtiff). -CVE_PRODUCT ??= "${BPN}" -CVE_VERSION ??= "${PV}" - -CVE_CHECK_DB_DIR ?= "${DL_DIR}/CVE_CHECK" -CVE_CHECK_DB_FILE ?= "${CVE_CHECK_DB_DIR}/nvd.db" - -CVE_CHECK_LOG ?= "${T}/cve.log" -CVE_CHECK_TMP_FILE ?= "${TMPDIR}/cve_check" - -CVE_CHECK_DIR ??= "${DEPLOY_DIR}/cve" -CVE_CHECK_MANIFEST ?= "${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${IMAGE_NAME_SUFFIX}.cve" -CVE_CHECK_COPY_FILES ??= "1" -CVE_CHECK_CREATE_MANIFEST ??= "1" - -# Whitelist for packages (PN) -CVE_CHECK_PN_WHITELIST = "\ - glibc-locale \ -" - -# Whitelist for CVE and version of package -CVE_CHECK_CVE_WHITELIST = "{\ - 'CVE-2014-2524': ('6.3','5.2',), \ -}" - -python do_cve_check () { - """ - Check recipe for patched and unpatched CVEs - """ - - if os.path.exists(d.getVar("CVE_CHECK_TMP_FILE")): - patched_cves = get_patches_cves(d) - patched, unpatched = check_cves(d, patched_cves) - if patched or unpatched: - cve_data = get_cve_info(d, patched + unpatched) - cve_write_data(d, patched, unpatched, cve_data) - else: - bb.note("Failed to update CVE database, skipping CVE check") -} - -addtask cve_check after do_unpack before do_build -do_cve_check[depends] = "cve-check-tool-native:do_populate_sysroot cve-check-tool-native:do_populate_cve_db" -do_cve_check[nostamp] = "1" - -python cve_check_cleanup () { - """ - Delete the file used to gather all the CVE information. - """ - - bb.utils.remove(e.data.getVar("CVE_CHECK_TMP_FILE")) -} - -addhandler cve_check_cleanup -cve_check_cleanup[eventmask] = "bb.cooker.CookerExit" - -python cve_check_write_rootfs_manifest () { - """ - Create CVE manifest when building an image - """ - - import shutil - - if d.getVar("CVE_CHECK_COPY_FILES") == "1": - deploy_file = os.path.join(d.getVar("CVE_CHECK_DIR"), d.getVar("PN")) - if os.path.exists(deploy_file): - bb.utils.remove(deploy_file) - - if os.path.exists(d.getVar("CVE_CHECK_TMP_FILE")): - bb.note("Writing rootfs CVE manifest") - deploy_dir = d.getVar("DEPLOY_DIR_IMAGE") - link_name = d.getVar("IMAGE_LINK_NAME") - manifest_name = d.getVar("CVE_CHECK_MANIFEST") - cve_tmp_file = d.getVar("CVE_CHECK_TMP_FILE") - - shutil.copyfile(cve_tmp_file, manifest_name) - - if manifest_name and os.path.exists(manifest_name): - manifest_link = os.path.join(deploy_dir, "%s.cve" % link_name) - # If we already have another manifest, update symlinks - if os.path.exists(os.path.realpath(manifest_link)): - os.remove(manifest_link) - os.symlink(os.path.basename(manifest_name), manifest_link) - bb.plain("Image CVE report stored in: %s" % manifest_name) -} - -ROOTFS_POSTPROCESS_COMMAND_prepend = "${@'cve_check_write_rootfs_manifest; ' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}" -do_rootfs[recrdeptask] += "${@'do_cve_check' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}" - -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 the "CVE: " line - match = cve_match.search(patch_text) - if match: - # 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) - elif not fname_match: - bb.debug(2, "Patch %s doesn't solve CVEs" % patch_file) - - return patched_cves - -def check_cves(d, patched_cves): - """ - Run cve-check-tool looking for patched and unpatched CVEs. - """ - - import ast, csv, tempfile, subprocess, io - - cves_patched = [] - cves_unpatched = [] - bpn = d.getVar("CVE_PRODUCT") - # If this has been unset then we're not scanning for CVEs here (for example, image recipes) - if not bpn: - return ([], []) - pv = d.getVar("CVE_VERSION").split("+git")[0] - cves = " ".join(patched_cves) - cve_db_dir = d.getVar("CVE_CHECK_DB_DIR") - cve_whitelist = ast.literal_eval(d.getVar("CVE_CHECK_CVE_WHITELIST")) - cve_cmd = "cve-check-tool" - cmd = [cve_cmd, "--no-html", "--csv", "--not-affected", "-t", "faux", "-d", cve_db_dir] - - # If the recipe has been whitlisted we return empty lists - if d.getVar("PN") in d.getVar("CVE_CHECK_PN_WHITELIST").split(): - bb.note("Recipe has been whitelisted, skipping check") - return ([], []) - - try: - # Write the faux CSV file to be used with cve-check-tool - fd, faux = tempfile.mkstemp(prefix="cve-faux-") - with os.fdopen(fd, "w") as f: - f.write("%s,%s,%s," % (bpn, pv, cves)) - cmd.append(faux) - - output = subprocess.check_output(cmd).decode("utf-8") - bb.debug(2, "Output of command %s:\n%s" % ("\n".join(cmd), output)) - except subprocess.CalledProcessError as e: - bb.warn("Couldn't check for CVEs: %s (output %s)" % (e, e.output)) - finally: - os.remove(faux) - - for row in csv.reader(io.StringIO(output)): - # Third row has the unpatched CVEs - if row[2]: - for cve in row[2].split(): - # Skip if the CVE has been whitlisted for the current version - if pv in cve_whitelist.get(cve,[]): - bb.note("%s-%s has been whitelisted for %s" % (bpn, pv, cve)) - else: - cves_unpatched.append(cve) - bb.debug(2, "%s-%s is not patched for %s" % (bpn, pv, cve)) - # Fourth row has patched CVEs - if row[3]: - for cve in row[3].split(): - cves_patched.append(cve) - bb.debug(2, "%s-%s is patched for %s" % (bpn, pv, cve)) - - return (cves_patched, cves_unpatched) - -def get_cve_info(d, cves): - """ - Get CVE information from the database used by cve-check-tool. - - Unfortunately the only way to get CVE info is set the output to - html (hard to parse) or query directly the database. - """ - - try: - import sqlite3 - except ImportError: - from pysqlite2 import dbapi2 as sqlite3 - - cve_data = {} - db_file = d.getVar("CVE_CHECK_DB_FILE") - placeholder = ",".join("?" * len(cves)) - query = "SELECT * FROM NVD WHERE id IN (%s)" % placeholder - conn = sqlite3.connect(db_file) - cur = conn.cursor() - for row in cur.execute(query, tuple(cves)): - cve_data[row[0]] = {} - cve_data[row[0]]["summary"] = row[1] - cve_data[row[0]]["score"] = row[2] - cve_data[row[0]]["modified"] = row[3] - cve_data[row[0]]["vector"] = row[4] - conn.close() - - return cve_data - -def cve_write_data(d, patched, unpatched, cve_data): - """ - Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and - CVE manifest if enabled. - """ - - cve_file = d.getVar("CVE_CHECK_LOG") - nvd_link = "https://web.nvd.nist.gov/view/vuln/detail?vulnId=" - write_string = "" - unpatched_cves = [] - bb.utils.mkdirhier(os.path.dirname(cve_file)) - - for cve in sorted(cve_data): - write_string += "PACKAGE NAME: %s\n" % d.getVar("PN") - write_string += "PACKAGE VERSION: %s\n" % d.getVar("PV") - write_string += "CVE: %s\n" % cve - if cve in patched: - write_string += "CVE STATUS: Patched\n" - else: - unpatched_cves.append(cve) - write_string += "CVE STATUS: Unpatched\n" - write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["summary"] - write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["score"] - write_string += "VECTOR: %s\n" % cve_data[cve]["vector"] - write_string += "MORE INFORMATION: %s%s\n\n" % (nvd_link, cve) - - if unpatched_cves: - bb.warn("Found unpatched CVE (%s), for more information check %s" % (" ".join(unpatched_cves),cve_file)) - - with open(cve_file, "w") as f: - bb.note("Writing file %s with CVE information" % cve_file) - f.write(write_string) - - if d.getVar("CVE_CHECK_COPY_FILES") == "1": - cve_dir = d.getVar("CVE_CHECK_DIR") - bb.utils.mkdirhier(cve_dir) - deploy_file = os.path.join(cve_dir, d.getVar("PN")) - with open(deploy_file, "w") as f: - f.write(write_string) - - if d.getVar("CVE_CHECK_CREATE_MANIFEST") == "1": - with open(d.getVar("CVE_CHECK_TMP_FILE"), "a") as f: - f.write("%s" % write_string) |