All of lore.kernel.org
 help / color / mirror / Atom feed
* [meta-oe][PATCH] cve-check: add coverage statistics on recipes without CVEs
@ 2021-08-10 10:35 rybczynska
  0 siblings, 0 replies; only message in thread
From: rybczynska @ 2021-08-10 10:35 UTC (permalink / raw)
  To: openembedded-core; +Cc: Marta Rybczynska, Marta Rybczynska

Until now the CVE checker was giving information about CVEs found for
a product (or more products) contained in a receipe. However, there was
no easy way to find out which products or receipes have no CVEs. Having
no reported CVEs might mean there are simply none, but can also mean
a product name (CPE) mismatch.

This patch adds CVE_CHECK_COVERAGE option enabling a new type of
statistics. A new file (*.cves_coverage) shows if a receipe has any
CVEs found in the NVD database, and if so, for which products.

This option is expected to help with an identification of receipes with
mismatched CPEs, issues in the database and more.

An example entry:
LAYER: meta
PACKAGE NAME: libsdl2-native
PACKAGE VERSION: 2.0.14
CVES FOUND IN RECIPE: Yes
    PRODUCT: simple_directmedia_layer (Yes)
    PRODUCT: sdl (No)

Signed-of-by: Marta Rybczynska <marta.rybczynska@huawei.com>
---
 meta/classes/cve-check.bbclass | 115 ++++++++++++++++++++++++++++-----
 1 file changed, 98 insertions(+), 17 deletions(-)

diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
index 6582f97151..c483e2cefc 100644
--- a/meta/classes/cve-check.bbclass
+++ b/meta/classes/cve-check.bbclass
@@ -30,19 +30,26 @@ CVE_CHECK_DB_FILE ?= "${CVE_CHECK_DB_DIR}/nvdcve_1.1.db"
 CVE_CHECK_DB_FILE_LOCK ?= "${CVE_CHECK_DB_FILE}.lock"
 
 CVE_CHECK_LOG ?= "${T}/cve.log"
+CVE_CHECK_COVERAGE_FILE = "${T}/cves_coverage.log"
 CVE_CHECK_TMP_FILE ?= "${TMPDIR}/cve_check"
+CVE_CHECK_COVERAGE_TMP_FILE ?= "${TMPDIR}/cves_coverage"
 CVE_CHECK_SUMMARY_DIR ?= "${LOG_DIR}/cve"
 CVE_CHECK_SUMMARY_FILE_NAME ?= "cve-summary"
+CVE_CHECK_COVERAGE_SUMMARY_FILE_NAME ?= "coverage-summary"
 CVE_CHECK_SUMMARY_FILE ?= "${CVE_CHECK_SUMMARY_DIR}/${CVE_CHECK_SUMMARY_FILE_NAME}"
 
 CVE_CHECK_DIR ??= "${DEPLOY_DIR}/cve"
 CVE_CHECK_RECIPE_FILE ?= "${CVE_CHECK_DIR}/${PN}"
 CVE_CHECK_MANIFEST ?= "${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${IMAGE_NAME_SUFFIX}.cve"
+CVE_CHECK_COVERAGE_MANIFEST ?= "${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${IMAGE_NAME_SUFFIX}.cves_coverage"
 CVE_CHECK_COPY_FILES ??= "1"
 CVE_CHECK_CREATE_MANIFEST ??= "1"
 
 CVE_CHECK_REPORT_PATCHED ??= "1"
 
+# Do a check for packages without CVEs (no issues or wrong product name)
+CVE_CHECK_COVERAGE ??= "1"
+
 # Whitelist for packages (PN)
 CVE_CHECK_PN_WHITELIST ?= ""
 
@@ -59,7 +66,6 @@ CVE_CHECK_LAYER_EXCLUDELIST ??= ""
 # Layers to be included
 CVE_CHECK_LAYER_INCLUDELIST ??= ""
 
-
 # set to "alphabetical" for version using single alphabetical character as increment release
 CVE_VERSION_SUFFIX ??= ""
 
@@ -73,18 +79,13 @@ python cve_save_summary_handler () {
     cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR")
     bb.utils.mkdirhier(cvelogpath)
 
-    timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
-    cve_summary_file = os.path.join(cvelogpath, "%s-%s.txt" % (cve_summary_name, timestamp))
-
-    if os.path.exists(cve_tmp_file):
-        shutil.copyfile(cve_tmp_file, cve_summary_file)
+    save_status_file(d, cve_tmp_file, cvelogpath, cve_summary_name, timestamp)
 
-        if cve_summary_file and os.path.exists(cve_summary_file):
-            cvefile_link = os.path.join(cvelogpath, cve_summary_name)
+    if (d.getVar("CVE_CHECK_COVERAGE") == "1"):
+    cve_tmp_file = d.getVar("CVE_CHECK_COVERAGE_TMP_FILE")
+        cve_summary_name = d.getVar("CVE_CHECK_COVERAGE_SUMMARY_FILE_NAME")
 
-            if os.path.exists(os.path.realpath(cvefile_link)):
-                os.remove(cvefile_link)
-            os.symlink(os.path.basename(cve_summary_file), cvefile_link)
+        save_status_file(d, cve_tmp_file, cvelogpath, cve_summary_name, timestamp)
 }
 
 addhandler cve_save_summary_handler
@@ -100,10 +101,12 @@ python do_cve_check () {
             patched_cves = get_patches_cves(d)
         except FileNotFoundError:
             bb.fatal("Failure in searching patches")
-        whitelisted, patched, unpatched = check_cves(d, patched_cves)
+        whitelisted, patched, unpatched, status = check_cves(d, patched_cves)
         if patched or unpatched:
             cve_data = get_cve_info(d, patched + unpatched)
-            cve_write_data(d, patched, unpatched, whitelisted, cve_data)
+            cve_write_data(d, patched, unpatched, whitelisted, cve_data, status)
+        else:
+            cve_write_data(d, [], [], [], {}, status)
     else:
         bb.note("No CVE database found, skipping CVE check")
 
@@ -118,6 +121,7 @@ python cve_check_cleanup () {
     Delete the file used to gather all the CVE information.
     """
     bb.utils.remove(e.data.getVar("CVE_CHECK_TMP_FILE"))
+    bb.utils.remove(e.data.getVar("CVE_CHECK_COVERAGE_TMP_FILE"))
 }
 
 addhandler cve_check_cleanup
@@ -151,11 +155,43 @@ python cve_check_write_rootfs_manifest () {
                 os.remove(manifest_link)
             os.symlink(os.path.basename(manifest_name), manifest_link)
             bb.plain("Image CVE report stored in: %s" % manifest_name)
+
+    if os.path.exists(d.getVar("CVE_CHECK_COVERAGE_TMP_FILE")):
+        bb.note("Writing rootfs CVE status")
+        deploy_dir = d.getVar("DEPLOY_DIR_IMAGE")
+        link_name = d.getVar("IMAGE_LINK_NAME")
+        manifest_name = d.getVar("CVE_CHECK_COVERAGE_MANIFEST")
+        cve_tmp_file = d.getVar("CVE_CHECK_COVERAGE_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.cves_coverage" % 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 status 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 save_status_file(d, tmp_file, logpath, summary_name, timestamp):
+    import shutil
+
+    summary_file = os.path.join(logpath, "%s-%s.txt" % (summary_name, timestamp))
+
+    if os.path.exists(tmp_file):
+        shutil.copyfile(tmp_file, summary_file)
+
+        if summary_file and os.path.exists(summary_file):
+            file_link = os.path.join(logpath, summary_name)
+
+            if os.path.exists(os.path.realpath(file_link)):
+                os.remove(file_link)
+            os.symlink(os.path.basename(summary_file), file_link)
+
 def get_patches_cves(d):
     """
     Get patches that solve CVEs using the "CVE: " tag.
@@ -226,17 +262,20 @@ def check_cves(d, patched_cves):
     suffix = d.getVar("CVE_VERSION_SUFFIX")
 
     cves_unpatched = []
+    cves_status = [False, []]
+    cves_found_recipe = False
+
     # CVE_PRODUCT can contain more than one product (eg. curl/libcurl)
     products = d.getVar("CVE_PRODUCT").split()
     # If this has been unset then we're not scanning for CVEs here (for example, image recipes)
     if not products:
-        return ([], [], [])
+        return ([], [], [], [])
     pv = d.getVar("CVE_VERSION").split("+git")[0]
 
     # If the recipe has been whitelisted we return empty lists
     if pn in d.getVar("CVE_CHECK_PN_WHITELIST").split():
         bb.note("Recipe has been whitelisted, skipping check")
-        return ([], [], [])
+        return ([], [], [], [])
 
     old_cve_whitelist =  d.getVar("CVE_CHECK_CVE_WHITELIST")
     if old_cve_whitelist:
@@ -249,6 +288,7 @@ def check_cves(d, patched_cves):
 
     # For each of the known product names (e.g. curl has CPEs using curl and libcurl)...
     for product in products:
+        cves_found_product = False
         if ":" in product:
             vendor, product = product.split(":", 1)
         else:
@@ -267,6 +307,13 @@ def check_cves(d, patched_cves):
                 bb.note("%s has been patched" % (cve))
                 continue
 
+            # Write status once only for each product
+            if not cves_found_product:
+                cves_status[0] = True
+                cves_status[1].append([product, True])
+                cves_found_product = True
+                cves_found_recipe = True
+
             vulnerable = False
             for row in conn.execute("SELECT * FROM PRODUCTS WHERE ID IS ? AND PRODUCT IS ? AND VENDOR LIKE ?", (cve, product, vendor)):
                 (_, _, _, version_start, operator_start, version_end, operator_end) = row
@@ -312,9 +359,16 @@ def check_cves(d, patched_cves):
                 # TODO: not patched but not vulnerable
                 patched_cves.add(cve)
 
+        if not cves_found_product:
+            bb.note("No CVE records found for product %s, pn %s" % (product, pn))
+            cves_status[1].append([product, False])
+
     conn.close()
 
-    return (list(cve_whitelist), list(patched_cves), cves_unpatched)
+    if not cves_found_recipe:
+        bb.note("No CVE records for products in recipe %s" % (pn))
+
+    return (list(cve_whitelist), list(patched_cves), cves_unpatched, cves_status)
 
 def get_cve_info(d, cves):
     """
@@ -338,7 +392,7 @@ def get_cve_info(d, cves):
     conn.close()
     return cve_data
 
-def cve_write_data(d, patched, unpatched, whitelisted, cve_data):
+def cve_write_data(d, patched, unpatched, whitelisted, cve_data, status):
     """
     Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and
     CVE manifest if enabled.
@@ -404,3 +458,30 @@ def cve_write_data(d, patched, unpatched, whitelisted, cve_data):
 
             with open(d.getVar("CVE_CHECK_TMP_FILE"), "a") as f:
                 f.write("%s" % write_string)
+
+    if (d.getVar("CVE_CHECK_COVERAGE") == "1") and status:
+        cve_status_file = d.getVar("CVE_CHECK_COVERAGE_FILE")
+
+        write_string = ""
+        bb.utils.mkdirhier(os.path.dirname(cve_status_file))
+
+        write_string += "LAYER: %s\n" % layer
+        write_string += "PACKAGE NAME: %s\n" % d.getVar("PN")
+        write_string += "PACKAGE VERSION: %s%s\n" % (d.getVar("EXTENDPE"), d.getVar("PV"))
+        write_string += "CVES FOUND IN RECIPE: %s\n" % ("No" if status[0] == False else "Yes")
+
+        for st in status[1]:
+            write_string += "    PRODUCT: %s (%s) \n" % (st[0], "No" if st[1] == False else "Yes")
+
+        write_string += "\n"
+
+        with open(cve_status_file, "w") as f:
+            bb.note("Writing file %s with status" % cve_status_file)
+            f.write(write_string)
+
+        if d.getVar("CVE_CHECK_CREATE_MANIFEST") == "1":
+            cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR")
+            bb.utils.mkdirhier(cvelogpath)
+
+            with open(d.getVar("CVE_CHECK_COVERAGE_TMP_FILE"), "a") as f:
+                f.write("%s" % write_string)
-- 
2.30.2


^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2021-08-10 10:35 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-08-10 10:35 [meta-oe][PATCH] cve-check: add coverage statistics on recipes without CVEs rybczynska

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.