summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarta Rybczynska <rybczynska@gmail.com>2022-03-29 14:54:32 +0200
committerRichard Purdie <richard.purdie@linuxfoundation.org>2022-06-11 10:06:09 +0100
commit9868f9914901210e4586194750905e93256fb372 (patch)
tree228923bd0697fb75a5b3fa532faa536242af6c7c
parentf2d12bc50bc744afee0a9f9f393335ae6b44dbcc (diff)
downloadpoky-9868f9914901210e4586194750905e93256fb372.tar.gz
cve-check: add coverage statistics on recipes with/without CVEs
Until now the CVE checker was giving information about CVEs found for a product (or more products) contained in a recipe. However, there was no easy way to find out which products or recipes 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. Then we use the new JSON format to report the information. The legacy text mode report does not contain it. This option is expected to help with an identification of recipes with mismatched CPEs, issues in the database and more. This work is based on [1], but adding the JSON format makes it easier to implement, without additional result files. [1] https://lists.openembedded.org/g/openembedded-core/message/159873 (From OE-Core rev: c63d06becc340270573bdef2630749db1f5230d4) Signed-off-by: Marta Rybczynska <marta.rybczynska@huawei.com> Signed-off-by: Alexandre Belloni <alexandre.belloni@bootlin.com> (cherry picked from commit d1849a1facd64fa0bcf8336a0ed5fbf71b2e3cb5) Signed-off-by: Steve Sakoman <steve@sakoman.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
-rw-r--r--meta/classes/cve-check.bbclass48
1 files changed, 37 insertions, 11 deletions
diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
index 48f75456f2..894cebaaa4 100644
--- a/meta/classes/cve-check.bbclass
+++ b/meta/classes/cve-check.bbclass
@@ -56,6 +56,9 @@ CVE_CHECK_FORMAT_TEXT ??= "1"
56# Provide JSON output - disabled by default for backward compatibility 56# Provide JSON output - disabled by default for backward compatibility
57CVE_CHECK_FORMAT_JSON ??= "0" 57CVE_CHECK_FORMAT_JSON ??= "0"
58 58
59# Check for packages without CVEs (no issues or missing product name)
60CVE_CHECK_COVERAGE ??= "1"
61
59# Whitelist for packages (PN) 62# Whitelist for packages (PN)
60CVE_CHECK_PN_WHITELIST ?= "" 63CVE_CHECK_PN_WHITELIST ?= ""
61 64
@@ -137,10 +140,10 @@ python do_cve_check () {
137 patched_cves = get_patches_cves(d) 140 patched_cves = get_patches_cves(d)
138 except FileNotFoundError: 141 except FileNotFoundError:
139 bb.fatal("Failure in searching patches") 142 bb.fatal("Failure in searching patches")
140 whitelisted, patched, unpatched = check_cves(d, patched_cves) 143 whitelisted, patched, unpatched, status = check_cves(d, patched_cves)
141 if patched or unpatched: 144 if patched or unpatched or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
142 cve_data = get_cve_info(d, patched + unpatched) 145 cve_data = get_cve_info(d, patched + unpatched)
143 cve_write_data(d, patched, unpatched, whitelisted, cve_data) 146 cve_write_data(d, patched, unpatched, whitelisted, cve_data, status)
144 else: 147 else:
145 bb.note("No CVE database found, skipping CVE check") 148 bb.note("No CVE database found, skipping CVE check")
146 149
@@ -312,17 +315,19 @@ def check_cves(d, patched_cves):
312 suffix = d.getVar("CVE_VERSION_SUFFIX") 315 suffix = d.getVar("CVE_VERSION_SUFFIX")
313 316
314 cves_unpatched = [] 317 cves_unpatched = []
318 cves_status = []
319 cves_in_recipe = False
315 # CVE_PRODUCT can contain more than one product (eg. curl/libcurl) 320 # CVE_PRODUCT can contain more than one product (eg. curl/libcurl)
316 products = d.getVar("CVE_PRODUCT").split() 321 products = d.getVar("CVE_PRODUCT").split()
317 # If this has been unset then we're not scanning for CVEs here (for example, image recipes) 322 # If this has been unset then we're not scanning for CVEs here (for example, image recipes)
318 if not products: 323 if not products:
319 return ([], [], []) 324 return ([], [], [], [])
320 pv = d.getVar("CVE_VERSION").split("+git")[0] 325 pv = d.getVar("CVE_VERSION").split("+git")[0]
321 326
322 # If the recipe has been whitelisted we return empty lists 327 # If the recipe has been whitelisted we return empty lists
323 if pn in d.getVar("CVE_CHECK_PN_WHITELIST").split(): 328 if pn in d.getVar("CVE_CHECK_PN_WHITELIST").split():
324 bb.note("Recipe has been whitelisted, skipping check") 329 bb.note("Recipe has been whitelisted, skipping check")
325 return ([], [], []) 330 return ([], [], [], [])
326 331
327 cve_whitelist = d.getVar("CVE_CHECK_WHITELIST").split() 332 cve_whitelist = d.getVar("CVE_CHECK_WHITELIST").split()
328 333
@@ -332,6 +337,7 @@ def check_cves(d, patched_cves):
332 337
333 # For each of the known product names (e.g. curl has CPEs using curl and libcurl)... 338 # For each of the known product names (e.g. curl has CPEs using curl and libcurl)...
334 for product in products: 339 for product in products:
340 cves_in_product = False
335 if ":" in product: 341 if ":" in product:
336 vendor, product = product.split(":", 1) 342 vendor, product = product.split(":", 1)
337 else: 343 else:
@@ -349,6 +355,11 @@ def check_cves(d, patched_cves):
349 elif cve in patched_cves: 355 elif cve in patched_cves:
350 bb.note("%s has been patched" % (cve)) 356 bb.note("%s has been patched" % (cve))
351 continue 357 continue
358 # Write status once only for each product
359 if not cves_in_product:
360 cves_status.append([product, True])
361 cves_in_product = True
362 cves_in_recipe = True
352 363
353 vulnerable = False 364 vulnerable = False
354 for row in conn.execute("SELECT * FROM PRODUCTS WHERE ID IS ? AND PRODUCT IS ? AND VENDOR LIKE ?", (cve, product, vendor)): 365 for row in conn.execute("SELECT * FROM PRODUCTS WHERE ID IS ? AND PRODUCT IS ? AND VENDOR LIKE ?", (cve, product, vendor)):
@@ -395,9 +406,13 @@ def check_cves(d, patched_cves):
395 # TODO: not patched but not vulnerable 406 # TODO: not patched but not vulnerable
396 patched_cves.add(cve) 407 patched_cves.add(cve)
397 408
409 if not cves_in_product:
410 bb.note("No CVE records found for product %s, pn %s" % (product, pn))
411 cves_status.append([product, False])
412
398 conn.close() 413 conn.close()
399 414
400 return (list(cve_whitelist), list(patched_cves), cves_unpatched) 415 return (list(cve_whitelist), list(patched_cves), cves_unpatched, cves_status)
401 416
402def get_cve_info(d, cves): 417def get_cve_info(d, cves):
403 """ 418 """
@@ -428,7 +443,6 @@ def cve_write_data_text(d, patched, unpatched, whitelisted, cve_data):
428 CVE manifest if enabled. 443 CVE manifest if enabled.
429 """ 444 """
430 445
431
432 cve_file = d.getVar("CVE_CHECK_LOG") 446 cve_file = d.getVar("CVE_CHECK_LOG")
433 fdir_name = d.getVar("FILE_DIRNAME") 447 fdir_name = d.getVar("FILE_DIRNAME")
434 layer = fdir_name.split("/")[-3] 448 layer = fdir_name.split("/")[-3]
@@ -442,6 +456,10 @@ def cve_write_data_text(d, patched, unpatched, whitelisted, cve_data):
442 if include_layers and layer not in include_layers: 456 if include_layers and layer not in include_layers:
443 return 457 return
444 458
459 # Early exit, the text format does not report packages without CVEs
460 if not patched+unpatched:
461 return
462
445 nvd_link = "https://nvd.nist.gov/vuln/detail/" 463 nvd_link = "https://nvd.nist.gov/vuln/detail/"
446 write_string = "" 464 write_string = ""
447 unpatched_cves = [] 465 unpatched_cves = []
@@ -518,7 +536,7 @@ def cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_fi
518 with open(index_path, "a+") as f: 536 with open(index_path, "a+") as f:
519 f.write("%s\n" % fragment_path) 537 f.write("%s\n" % fragment_path)
520 538
521def cve_write_data_json(d, patched, unpatched, ignored, cve_data): 539def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
522 """ 540 """
523 Prepare CVE data for the JSON format, then write it. 541 Prepare CVE data for the JSON format, then write it.
524 """ 542 """
@@ -540,11 +558,19 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data):
540 558
541 unpatched_cves = [] 559 unpatched_cves = []
542 560
561 product_data = []
562 for s in cve_status:
563 p = {"product": s[0], "cvesInRecord": "Yes"}
564 if s[1] == False:
565 p["cvesInRecord"] = "No"
566 product_data.append(p)
567
543 package_version = "%s%s" % (d.getVar("EXTENDPE"), d.getVar("PV")) 568 package_version = "%s%s" % (d.getVar("EXTENDPE"), d.getVar("PV"))
544 package_data = { 569 package_data = {
545 "name" : d.getVar("PN"), 570 "name" : d.getVar("PN"),
546 "layer" : layer, 571 "layer" : layer,
547 "version" : package_version 572 "version" : package_version,
573 "products": product_data
548 } 574 }
549 cve_list = [] 575 cve_list = []
550 576
@@ -583,7 +609,7 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data):
583 609
584 cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file) 610 cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file)
585 611
586def cve_write_data(d, patched, unpatched, ignored, cve_data): 612def cve_write_data(d, patched, unpatched, ignored, cve_data, status):
587 """ 613 """
588 Write CVE data in each enabled format. 614 Write CVE data in each enabled format.
589 """ 615 """
@@ -591,4 +617,4 @@ def cve_write_data(d, patched, unpatched, ignored, cve_data):
591 if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1": 617 if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1":
592 cve_write_data_text(d, patched, unpatched, ignored, cve_data) 618 cve_write_data_text(d, patched, unpatched, ignored, cve_data)
593 if d.getVar("CVE_CHECK_FORMAT_JSON") == "1": 619 if d.getVar("CVE_CHECK_FORMAT_JSON") == "1":
594 cve_write_data_json(d, patched, unpatched, ignored, cve_data) 620 cve_write_data_json(d, patched, unpatched, ignored, cve_data, status)