mirror of
https://git.yoctoproject.org/git/poky
synced 2026-01-04 16:10:04 +00:00
While poky master branch has been fixed so that all CVE patch files have the: CVE: CVE-2017-1234556 strings in the patch comments, many older versions of poky and other meta layers are not, but the CVE patches quite often have the CVE id in the patch file name. If the CVE: string also found, there are no duplicates in the report. (From OE-Core rev: 5ee5b0c66627c9e974c838b86e2e659c2f601f2a) Signed-off-by: Mikko Rapeli <mikko.rapeli@bmw.de> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
291 lines
10 KiB
Plaintext
291 lines
10 KiB
Plaintext
# 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_CHECK_DB_DIR ?= "${DL_DIR}/CVE_CHECK"
|
|
CVE_CHECK_DB_FILE ?= "${CVE_CHECK_DB_DIR}/nvd.db"
|
|
|
|
CVE_CHECK_LOCAL_DIR ?= "${WORKDIR}/cve"
|
|
CVE_CHECK_LOCAL_FILE ?= "${CVE_CHECK_LOCAL_DIR}/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")
|
|
pv = d.getVar("PV").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 ([], [])
|
|
|
|
# It is needed to export the proxies to download the database using HTTP
|
|
bb.utils.export_proxies(d)
|
|
|
|
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_LOCAL_FILE")
|
|
nvd_link = "https://web.nvd.nist.gov/view/vuln/detail?vulnId="
|
|
write_string = ""
|
|
unpatched_cves = []
|
|
bb.utils.mkdirhier(d.getVar("CVE_CHECK_LOCAL_DIR"))
|
|
|
|
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)
|