summaryrefslogtreecommitdiffstats
path: root/meta/classes/cve-check.bbclass
diff options
context:
space:
mode:
authorMarta Rybczynska <rybczynska@gmail.com>2022-03-29 14:54:32 +0200
committerRichard Purdie <richard.purdie@linuxfoundation.org>2022-03-30 13:07:41 +0100
commitb3f96e6feab7ef0b2797214631122efefe29dbe8 (patch)
treef76223fe113c38fed28cf8dbe06d3c0632018ea3 /meta/classes/cve-check.bbclass
parent777f1d42b62ab482efa5a24600f4aeba1b156c64 (diff)
downloadpoky-b3f96e6feab7ef0b2797214631122efefe29dbe8.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: d1849a1facd64fa0bcf8336a0ed5fbf71b2e3cb5) Signed-off-by: Marta Rybczynska <marta.rybczynska@huawei.com> Signed-off-by: Alexandre Belloni <alexandre.belloni@bootlin.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta/classes/cve-check.bbclass')
-rw-r--r--meta/classes/cve-check.bbclass51
1 files changed, 40 insertions, 11 deletions
diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
index f574f5daa4..78516d0bb6 100644
--- a/meta/classes/cve-check.bbclass
+++ b/meta/classes/cve-check.bbclass
@@ -55,6 +55,9 @@ CVE_CHECK_FORMAT_TEXT ??= "1"
55# Provide JSON output 55# Provide JSON output
56CVE_CHECK_FORMAT_JSON ??= "1" 56CVE_CHECK_FORMAT_JSON ??= "1"
57 57
58# Check for packages without CVEs (no issues or missing product name)
59CVE_CHECK_COVERAGE ??= "1"
60
58# Skip CVE Check for packages (PN) 61# Skip CVE Check for packages (PN)
59CVE_CHECK_SKIP_RECIPE ?= "" 62CVE_CHECK_SKIP_RECIPE ?= ""
60 63
@@ -114,10 +117,10 @@ python do_cve_check () {
114 patched_cves = get_patched_cves(d) 117 patched_cves = get_patched_cves(d)
115 except FileNotFoundError: 118 except FileNotFoundError:
116 bb.fatal("Failure in searching patches") 119 bb.fatal("Failure in searching patches")
117 ignored, patched, unpatched = check_cves(d, patched_cves) 120 ignored, patched, unpatched, status = check_cves(d, patched_cves)
118 if patched or unpatched: 121 if patched or unpatched or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
119 cve_data = get_cve_info(d, patched + unpatched) 122 cve_data = get_cve_info(d, patched + unpatched)
120 cve_write_data(d, patched, unpatched, ignored, cve_data) 123 cve_write_data(d, patched, unpatched, ignored, cve_data, status)
121 else: 124 else:
122 bb.note("No CVE database found, skipping CVE check") 125 bb.note("No CVE database found, skipping CVE check")
123 126
@@ -207,17 +210,19 @@ def check_cves(d, patched_cves):
207 suffix = d.getVar("CVE_VERSION_SUFFIX") 210 suffix = d.getVar("CVE_VERSION_SUFFIX")
208 211
209 cves_unpatched = [] 212 cves_unpatched = []
213 cves_status = []
214 cves_in_recipe = False
210 # CVE_PRODUCT can contain more than one product (eg. curl/libcurl) 215 # CVE_PRODUCT can contain more than one product (eg. curl/libcurl)
211 products = d.getVar("CVE_PRODUCT").split() 216 products = d.getVar("CVE_PRODUCT").split()
212 # If this has been unset then we're not scanning for CVEs here (for example, image recipes) 217 # If this has been unset then we're not scanning for CVEs here (for example, image recipes)
213 if not products: 218 if not products:
214 return ([], [], []) 219 return ([], [], [], {})
215 pv = d.getVar("CVE_VERSION").split("+git")[0] 220 pv = d.getVar("CVE_VERSION").split("+git")[0]
216 221
217 # If the recipe has been skipped/ignored we return empty lists 222 # If the recipe has been skipped/ignored we return empty lists
218 if pn in d.getVar("CVE_CHECK_SKIP_RECIPE").split(): 223 if pn in d.getVar("CVE_CHECK_SKIP_RECIPE").split():
219 bb.note("Recipe has been skipped by cve-check") 224 bb.note("Recipe has been skipped by cve-check")
220 return ([], [], []) 225 return ([], [], [], [])
221 226
222 cve_ignore = d.getVar("CVE_CHECK_IGNORE").split() 227 cve_ignore = d.getVar("CVE_CHECK_IGNORE").split()
223 228
@@ -227,6 +232,7 @@ def check_cves(d, patched_cves):
227 232
228 # For each of the known product names (e.g. curl has CPEs using curl and libcurl)... 233 # For each of the known product names (e.g. curl has CPEs using curl and libcurl)...
229 for product in products: 234 for product in products:
235 cves_in_product = False
230 if ":" in product: 236 if ":" in product:
231 vendor, product = product.split(":", 1) 237 vendor, product = product.split(":", 1)
232 else: 238 else:
@@ -244,6 +250,11 @@ def check_cves(d, patched_cves):
244 elif cve in patched_cves: 250 elif cve in patched_cves:
245 bb.note("%s has been patched" % (cve)) 251 bb.note("%s has been patched" % (cve))
246 continue 252 continue
253 # Write status once only for each product
254 if not cves_in_product:
255 cves_status.append([product, True])
256 cves_in_product = True
257 cves_in_recipe = True
247 258
248 vulnerable = False 259 vulnerable = False
249 for row in conn.execute("SELECT * FROM PRODUCTS WHERE ID IS ? AND PRODUCT IS ? AND VENDOR LIKE ?", (cve, product, vendor)): 260 for row in conn.execute("SELECT * FROM PRODUCTS WHERE ID IS ? AND PRODUCT IS ? AND VENDOR LIKE ?", (cve, product, vendor)):
@@ -290,9 +301,16 @@ def check_cves(d, patched_cves):
290 # TODO: not patched but not vulnerable 301 # TODO: not patched but not vulnerable
291 patched_cves.add(cve) 302 patched_cves.add(cve)
292 303
304 if not cves_in_product:
305 bb.note("No CVE records found for product %s, pn %s" % (product, pn))
306 cves_status.append([product, False])
307
293 conn.close() 308 conn.close()
294 309
295 return (list(cve_ignore), list(patched_cves), cves_unpatched) 310 if not cves_in_recipe:
311 bb.note("No CVE records for products in recipe %s" % (pn))
312
313 return (list(cve_ignore), list(patched_cves), cves_unpatched, cves_status)
296 314
297def get_cve_info(d, cves): 315def get_cve_info(d, cves):
298 """ 316 """
@@ -323,7 +341,6 @@ def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
323 CVE manifest if enabled. 341 CVE manifest if enabled.
324 """ 342 """
325 343
326
327 cve_file = d.getVar("CVE_CHECK_LOG") 344 cve_file = d.getVar("CVE_CHECK_LOG")
328 fdir_name = d.getVar("FILE_DIRNAME") 345 fdir_name = d.getVar("FILE_DIRNAME")
329 layer = fdir_name.split("/")[-3] 346 layer = fdir_name.split("/")[-3]
@@ -337,6 +354,10 @@ def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
337 if include_layers and layer not in include_layers: 354 if include_layers and layer not in include_layers:
338 return 355 return
339 356
357 # Early exit, the text format does not report packages without CVEs
358 if not patched+unpatched:
359 return
360
340 nvd_link = "https://nvd.nist.gov/vuln/detail/" 361 nvd_link = "https://nvd.nist.gov/vuln/detail/"
341 write_string = "" 362 write_string = ""
342 unpatched_cves = [] 363 unpatched_cves = []
@@ -414,7 +435,7 @@ def cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_fi
414 with open(index_path, "a+") as f: 435 with open(index_path, "a+") as f:
415 f.write("%s\n" % fragment_path) 436 f.write("%s\n" % fragment_path)
416 437
417def cve_write_data_json(d, patched, unpatched, ignored, cve_data): 438def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
418 """ 439 """
419 Prepare CVE data for the JSON format, then write it. 440 Prepare CVE data for the JSON format, then write it.
420 """ 441 """
@@ -436,11 +457,19 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data):
436 457
437 unpatched_cves = [] 458 unpatched_cves = []
438 459
460 product_data = []
461 for s in cve_status:
462 p = {"product": s[0], "cvesInRecord": "Yes"}
463 if s[1] == False:
464 p["cvesInRecord"] = "No"
465 product_data.append(p)
466
439 package_version = "%s%s" % (d.getVar("EXTENDPE"), d.getVar("PV")) 467 package_version = "%s%s" % (d.getVar("EXTENDPE"), d.getVar("PV"))
440 package_data = { 468 package_data = {
441 "name" : d.getVar("PN"), 469 "name" : d.getVar("PN"),
442 "layer" : layer, 470 "layer" : layer,
443 "version" : package_version 471 "version" : package_version,
472 "products": product_data
444 } 473 }
445 cve_list = [] 474 cve_list = []
446 475
@@ -479,7 +508,7 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data):
479 508
480 cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file) 509 cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file)
481 510
482def cve_write_data(d, patched, unpatched, ignored, cve_data): 511def cve_write_data(d, patched, unpatched, ignored, cve_data, status):
483 """ 512 """
484 Write CVE data in each enabled format. 513 Write CVE data in each enabled format.
485 """ 514 """
@@ -487,4 +516,4 @@ def cve_write_data(d, patched, unpatched, ignored, cve_data):
487 if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1": 516 if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1":
488 cve_write_data_text(d, patched, unpatched, ignored, cve_data) 517 cve_write_data_text(d, patched, unpatched, ignored, cve_data)
489 if d.getVar("CVE_CHECK_FORMAT_JSON") == "1": 518 if d.getVar("CVE_CHECK_FORMAT_JSON") == "1":
490 cve_write_data_json(d, patched, unpatched, ignored, cve_data) 519 cve_write_data_json(d, patched, unpatched, ignored, cve_data, status)