CVEs that are currently considered "Patched" are classified into the following
3 statuses:
1. "Patched" - means that a patch file that fixed the vulnerability has
been applied
2. "Not affected" - means that the package version (PV) is not affected by the
vulnerability
3. "Undecidable" - means that versions cannot be compared to determine if they
are affected by the vulnerability
Signed-off-by: Shinji Matsunaga <[email protected]>
Signed-off-by: Shunsuke Tokumoto <[email protected]>
---
Changes for v2:
- Fix the status "Out of range" to "Not affected"
meta/classes/cve-check.bbclass | 55 +++++++++++++++++++++++-----------
1 file changed, 38 insertions(+), 17 deletions(-)
diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
index b55f4299da..502db324df 100644
--- a/meta/classes/cve-check.bbclass
+++ b/meta/classes/cve-check.bbclass
@@ -185,10 +185,10 @@ python do_cve_check () {
patched_cves = get_patched_cves(d)
except FileNotFoundError:
bb.fatal("Failure in searching patches")
- ignored, patched, unpatched, status = check_cves(d, patched_cves)
- if patched or unpatched or (d.getVar("CVE_CHECK_COVERAGE") == "1"
and status):
- cve_data = get_cve_info(d, patched + unpatched + ignored)
- cve_write_data(d, patched, unpatched, ignored, cve_data,
status)
+ ignored, patched, unpatched, not_affected, undecidable, status =
check_cves(d, patched_cves)
+ if patched or unpatched or not_affected or undecidable or
(d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
+ cve_data = get_cve_info(d, patched + unpatched + ignored +
not_affected + undecidable)
+ cve_write_data(d, patched, unpatched, ignored, not_affected,
undecidable, cve_data, status)
else:
bb.note("No CVE database found, skipping CVE check")
@@ -308,13 +308,13 @@ def check_cves(d, patched_cves):
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 skipped/ignored we return empty lists
if pn in d.getVar("CVE_CHECK_SKIP_RECIPE").split():
bb.note("Recipe has been skipped by cve-check")
- return ([], [], [], [])
+ return ([], [], [], [], [], [])
# Convert CVE_STATUS into ignored CVEs and check validity
cve_ignore = []
@@ -328,6 +328,8 @@ def check_cves(d, patched_cves):
conn = sqlite3.connect(db_file, uri=True)
# For each of the known product names (e.g. curl has CPEs using curl and
libcurl)...
+ cves_not_affected = []
+ cves_undecidable = []
for product in products:
cves_in_product = False
if ":" in product:
@@ -355,6 +357,7 @@ def check_cves(d, patched_cves):
vulnerable = False
ignored = False
+ undecidable = False
product_cursor = conn.execute("SELECT * FROM PRODUCTS WHERE ID IS ?
AND PRODUCT IS ? AND VENDOR LIKE ?", (cve, product, vendor))
for row in product_cursor:
@@ -376,7 +379,7 @@ def check_cves(d, patched_cves):
except:
bb.warn("%s: Failed to compare %s %s %s for %s" %
(product, pv, operator_start,
version_start, cve))
- vulnerable_start = False
+ undecidable = True
else:
vulnerable_start = False
@@ -387,10 +390,15 @@ def check_cves(d, patched_cves):
except:
bb.warn("%s: Failed to compare %s %s %s for %s" %
(product, pv, operator_end, version_end,
cve))
- vulnerable_end = False
+ undecidable = True
else:
vulnerable_end = False
+ if undecidable:
+ bb.note("%s-%s is undecidable to %s" % (pn, real_pv,
cve))
+ cves_undecidable.append(cve)
+ break
+
if operator_start and operator_end:
vulnerable = vulnerable_start and vulnerable_end
else:
@@ -406,9 +414,9 @@ def check_cves(d, patched_cves):
break
product_cursor.close()
- if not vulnerable:
+ if not undecidable and not vulnerable:
bb.note("%s-%s is not vulnerable to %s" % (pn, real_pv, cve))
- patched_cves.add(cve)
+ cves_not_affected.append(cve)
cve_cursor.close()
if not cves_in_product:
@@ -420,7 +428,7 @@ def check_cves(d, patched_cves):
if not cves_in_recipe:
bb.note("No CVE records for products in recipe %s" % (pn))
- return (list(cves_ignored), list(patched_cves), cves_unpatched,
cves_status)
+ return (list(cves_ignored), list(patched_cves), cves_unpatched,
cves_not_affected, cves_undecidable, cves_status)
def get_cve_info(d, cves):
"""
@@ -447,7 +455,7 @@ def get_cve_info(d, cves):
conn.close()
return cve_data
-def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
+def cve_write_data_text(d, patched, unpatched, ignored, not_affected,
undecidable, cve_data):
"""
Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and
CVE manifest if enabled.
@@ -471,7 +479,7 @@ def cve_write_data_text(d, patched, unpatched, ignored,
cve_data):
return
# Early exit, the text format does not report packages without CVEs
- if not patched+unpatched+ignored:
+ if not patched+unpatched+ignored+not_affected+undecidable:
return
nvd_link = "https://nvd.nist.gov/vuln/detail/"
@@ -482,6 +490,8 @@ def cve_write_data_text(d, patched, unpatched, ignored,
cve_data):
for cve in sorted(cve_data):
is_patched = cve in patched
is_ignored = cve in ignored
+ is_not_affected = cve in not_affected
+ is_undecidable = cve in undecidable
status = "Unpatched"
if (is_patched or is_ignored) and not report_all:
@@ -490,6 +500,10 @@ def cve_write_data_text(d, patched, unpatched, ignored,
cve_data):
status = "Ignored"
elif is_patched:
status = "Patched"
+ elif is_not_affected:
+ status = "Not affected"
+ elif is_undecidable:
+ status = "Undecidable"
else:
# default value of status is Unpatched
unpatched_cves.append(cve)
@@ -561,7 +575,7 @@ def cve_check_write_json_output(d, output, direct_file,
deploy_file, manifest_fi
with open(index_path, "a+") as f:
f.write("%s\n" % fragment_path)
-def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
+def cve_write_data_json(d, patched, unpatched, ignored, not_affected,
undecidable, cve_data, cve_status):
"""
Prepare CVE data for the JSON format, then write it.
"""
@@ -606,6 +620,9 @@ def cve_write_data_json(d, patched, unpatched, ignored,
cve_data, cve_status):
for cve in sorted(cve_data):
is_patched = cve in patched
is_ignored = cve in ignored
+ is_not_affected = cve in not_affected
+ is_undecidable = cve in undecidable
+
status = "Unpatched"
if (is_patched or is_ignored) and not report_all:
continue
@@ -613,6 +630,10 @@ def cve_write_data_json(d, patched, unpatched, ignored,
cve_data, cve_status):
status = "Ignored"
elif is_patched:
status = "Patched"
+ elif is_not_affected:
+ status = "Not affected"
+ elif is_undecidable:
+ status = "Undecidable"
else:
# default value of status is Unpatched
unpatched_cves.append(cve)
@@ -645,12 +666,12 @@ def cve_write_data_json(d, patched, unpatched, ignored,
cve_data, cve_status):
cve_check_write_json_output(d, output, direct_file, deploy_file,
manifest_file)
-def cve_write_data(d, patched, unpatched, ignored, cve_data, status):
+def cve_write_data(d, patched, unpatched, ignored, not_affected, undecidable,
cve_data, status):
"""
Write CVE data in each enabled format.
"""
if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1":
- cve_write_data_text(d, patched, unpatched, ignored, cve_data)
+ cve_write_data_text(d, patched, unpatched, ignored, not_affected,
undecidable, cve_data)
if d.getVar("CVE_CHECK_FORMAT_JSON") == "1":
- cve_write_data_json(d, patched, unpatched, ignored, cve_data, status)
+ cve_write_data_json(d, patched, unpatched, ignored, not_affected,
undecidable, cve_data, status)