summaryrefslogtreecommitdiffstats
path: root/meta/classes
diff options
context:
space:
mode:
authorMarta Rybczynska <rybczynska@gmail.com>2024-08-14 07:30:37 +0200
committerRichard Purdie <richard.purdie@linuxfoundation.org>2024-08-20 14:12:40 +0100
commitfb3f440b7d808d4e29b6ab90e75313d5cf516c36 (patch)
treed11a4884bc55f516c2e2dc2c139998b5cdd039a7 /meta/classes
parentebc872441686e09708a23b0ee1d6d865481fbc09 (diff)
downloadpoky-fb3f440b7d808d4e29b6ab90e75313d5cf516c36.tar.gz
cve-check: annotate CVEs during analysis
Add status information for each CVE under analysis. Previously the information passed between different function of the cve-check class included only tables of patched, unpatched, ignored vulnerabilities and the general status of the recipe. The VEX work requires more information, and we need to pass them between different functions, so that it can be enriched as the analysis progresses. Instead of multiple tables, use a single one with annotations for each CVE encountered. For example, a patched CVE will have: {"abbrev-status": "Patched", "status": "version-not-in-range"} abbrev-status contains the general status (Patched, Unpatched, Ignored and Unknown that will be added in the VEX code) status contains more detailed information that can come from CVE_STATUS and the analysis. Additional fields of the annotation include for example the name of the patch file fixing a given CVE. We also use the annotation in CVE_STATUS to filter out entries that do not apply to the given recipe (From OE-Core rev: 452e605b55ad61c08f4af7089a5a9c576ca28f7d) Signed-off-by: Marta Rybczynska <marta.rybczynska@syslinbit.com> Signed-off-by: Samantha Jalabert <samantha.jalabert@syslinbit.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta/classes')
-rw-r--r--meta/classes/cve-check.bbclass208
1 files changed, 110 insertions, 98 deletions
diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
index bc35a1c53c..0d7c8a5835 100644
--- a/meta/classes/cve-check.bbclass
+++ b/meta/classes/cve-check.bbclass
@@ -189,10 +189,10 @@ python do_cve_check () {
189 patched_cves = get_patched_cves(d) 189 patched_cves = get_patched_cves(d)
190 except FileNotFoundError: 190 except FileNotFoundError:
191 bb.fatal("Failure in searching patches") 191 bb.fatal("Failure in searching patches")
192 ignored, patched, unpatched, status = check_cves(d, patched_cves) 192 cve_data, status = check_cves(d, patched_cves)
193 if patched or unpatched or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status): 193 if len(cve_data) or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
194 cve_data = get_cve_info(d, patched + unpatched + ignored) 194 get_cve_info(d, cve_data)
195 cve_write_data(d, patched, unpatched, ignored, cve_data, status) 195 cve_write_data(d, cve_data, status)
196 else: 196 else:
197 bb.note("No CVE database found, skipping CVE check") 197 bb.note("No CVE database found, skipping CVE check")
198 198
@@ -295,7 +295,51 @@ ROOTFS_POSTPROCESS_COMMAND:prepend = "${@'cve_check_write_rootfs_manifest ' if d
295do_rootfs[recrdeptask] += "${@'do_cve_check' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}" 295do_rootfs[recrdeptask] += "${@'do_cve_check' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}"
296do_populate_sdk[recrdeptask] += "${@'do_cve_check' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}" 296do_populate_sdk[recrdeptask] += "${@'do_cve_check' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}"
297 297
298def check_cves(d, patched_cves): 298def cve_is_ignored(d, cve_data, cve):
299 if cve not in cve_data:
300 return False
301 if cve_data[cve]['abbrev-status'] == "Ignored":
302 return True
303 return False
304
305def cve_is_patched(d, cve_data, cve):
306 if cve not in cve_data:
307 return False
308 if cve_data[cve]['abbrev-status'] == "Patched":
309 return True
310 return False
311
312def cve_update(d, cve_data, cve, entry):
313 # If no entry, just add it
314 if cve not in cve_data:
315 cve_data[cve] = entry
316 return
317 # If we are updating, there might be change in the status
318 bb.debug("Trying CVE entry update for %s from %s to %s" % (cve, cve_data[cve]['abbrev-status'], entry['abbrev-status']))
319 if cve_data[cve]['abbrev-status'] == "Unknown":
320 cve_data[cve] = entry
321 return
322 if cve_data[cve]['abbrev-status'] == entry['abbrev-status']:
323 return
324 # Update like in {'abbrev-status': 'Patched', 'status': 'version-not-in-range'} to {'abbrev-status': 'Unpatched', 'status': 'version-in-range'}
325 if entry['abbrev-status'] == "Unpatched" and cve_data[cve]['abbrev-status'] == "Patched":
326 if entry['status'] == "version-in-range" and cve_data[cve]['status'] == "version-not-in-range":
327 # New result from the scan, vulnerable
328 cve_data[cve] = entry
329 bb.debug("CVE entry %s update from Patched to Unpatched from the scan result" % cve)
330 return
331 if entry['abbrev-status'] == "Patched" and cve_data[cve]['abbrev-status'] == "Unpatched":
332 if entry['status'] == "version-not-in-range" and cve_data[cve]['status'] == "version-in-range":
333 # Range does not match the scan, but we already have a vulnerable match, ignore
334 bb.debug("CVE entry %s update from Patched to Unpatched from the scan result - not applying" % cve)
335 return
336 # If we have an "Ignored", it has a priority
337 if cve_data[cve]['abbrev-status'] == "Ignored":
338 bb.debug("CVE %s not updating because Ignored" % cve)
339 return
340 bb.warn("Unhandled CVE entry update for %s from %s to %s" % (cve, cve_data[cve], entry))
341
342def check_cves(d, cve_data):
299 """ 343 """
300 Connect to the NVD database and find unpatched cves. 344 Connect to the NVD database and find unpatched cves.
301 """ 345 """
@@ -305,28 +349,19 @@ def check_cves(d, patched_cves):
305 real_pv = d.getVar("PV") 349 real_pv = d.getVar("PV")
306 suffix = d.getVar("CVE_VERSION_SUFFIX") 350 suffix = d.getVar("CVE_VERSION_SUFFIX")
307 351
308 cves_unpatched = []
309 cves_ignored = []
310 cves_status = [] 352 cves_status = []
311 cves_in_recipe = False 353 cves_in_recipe = False
312 # CVE_PRODUCT can contain more than one product (eg. curl/libcurl) 354 # CVE_PRODUCT can contain more than one product (eg. curl/libcurl)
313 products = d.getVar("CVE_PRODUCT").split() 355 products = d.getVar("CVE_PRODUCT").split()
314 # If this has been unset then we're not scanning for CVEs here (for example, image recipes) 356 # If this has been unset then we're not scanning for CVEs here (for example, image recipes)
315 if not products: 357 if not products:
316 return ([], [], [], []) 358 return ([], [])
317 pv = d.getVar("CVE_VERSION").split("+git")[0] 359 pv = d.getVar("CVE_VERSION").split("+git")[0]
318 360
319 # If the recipe has been skipped/ignored we return empty lists 361 # If the recipe has been skipped/ignored we return empty lists
320 if pn in d.getVar("CVE_CHECK_SKIP_RECIPE").split(): 362 if pn in d.getVar("CVE_CHECK_SKIP_RECIPE").split():
321 bb.note("Recipe has been skipped by cve-check") 363 bb.note("Recipe has been skipped by cve-check")
322 return ([], [], [], []) 364 return ([], [])
323
324 # Convert CVE_STATUS into ignored CVEs and check validity
325 cve_ignore = []
326 for cve in (d.getVarFlags("CVE_STATUS") or {}):
327 decoded_status = decode_cve_status(d, cve)
328 if 'mapping' in decoded_status and decoded_status['mapping'] == "Ignored":
329 cve_ignore.append(cve)
330 365
331 import sqlite3 366 import sqlite3
332 db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro") 367 db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro")
@@ -345,11 +380,10 @@ def check_cves(d, patched_cves):
345 for cverow in cve_cursor: 380 for cverow in cve_cursor:
346 cve = cverow[0] 381 cve = cverow[0]
347 382
348 if cve in cve_ignore: 383 if cve_is_ignored(d, cve_data, cve):
349 bb.note("%s-%s ignores %s" % (product, pv, cve)) 384 bb.note("%s-%s ignores %s" % (product, pv, cve))
350 cves_ignored.append(cve)
351 continue 385 continue
352 elif cve in patched_cves: 386 elif cve_is_patched(d, cve_data, cve):
353 bb.note("%s has been patched" % (cve)) 387 bb.note("%s has been patched" % (cve))
354 continue 388 continue
355 # Write status once only for each product 389 # Write status once only for each product
@@ -365,7 +399,7 @@ def check_cves(d, patched_cves):
365 for row in product_cursor: 399 for row in product_cursor:
366 (_, _, _, version_start, operator_start, version_end, operator_end) = row 400 (_, _, _, version_start, operator_start, version_end, operator_end) = row
367 #bb.debug(2, "Evaluating row " + str(row)) 401 #bb.debug(2, "Evaluating row " + str(row))
368 if cve in cve_ignore: 402 if cve_is_ignored(d, cve_data, cve):
369 ignored = True 403 ignored = True
370 404
371 version_start = convert_cve_version(version_start) 405 version_start = convert_cve_version(version_start)
@@ -404,16 +438,16 @@ def check_cves(d, patched_cves):
404 if vulnerable: 438 if vulnerable:
405 if ignored: 439 if ignored:
406 bb.note("%s is ignored in %s-%s" % (cve, pn, real_pv)) 440 bb.note("%s is ignored in %s-%s" % (cve, pn, real_pv))
407 cves_ignored.append(cve) 441 cve_update(d, cve_data, cve, {"abbrev-status": "Ignored"})
408 else: 442 else:
409 bb.note("%s-%s is vulnerable to %s" % (pn, real_pv, cve)) 443 bb.note("%s-%s is vulnerable to %s" % (pn, real_pv, cve))
410 cves_unpatched.append(cve) 444 cve_update(d, cve_data, cve, {"abbrev-status": "Unpatched", "status": "version-in-range"})
411 break 445 break
412 product_cursor.close() 446 product_cursor.close()
413 447
414 if not vulnerable: 448 if not vulnerable:
415 bb.note("%s-%s is not vulnerable to %s" % (pn, real_pv, cve)) 449 bb.note("%s-%s is not vulnerable to %s" % (pn, real_pv, cve))
416 patched_cves.add(cve) 450 cve_update(d, cve_data, cve, {"abbrev-status": "Patched", "status": "version-not-in-range"})
417 cve_cursor.close() 451 cve_cursor.close()
418 452
419 if not cves_in_product: 453 if not cves_in_product:
@@ -421,48 +455,45 @@ def check_cves(d, patched_cves):
421 cves_status.append([product, False]) 455 cves_status.append([product, False])
422 456
423 conn.close() 457 conn.close()
424 diff_ignore = list(set(cve_ignore) - set(cves_ignored))
425 if diff_ignore:
426 oe.qa.handle_error("cve_status_not_in_db", "Found CVE (%s) with CVE_STATUS set that are not found in database for this component" % " ".join(diff_ignore), d)
427 458
428 if not cves_in_recipe: 459 if not cves_in_recipe:
429 bb.note("No CVE records for products in recipe %s" % (pn)) 460 bb.note("No CVE records for products in recipe %s" % (pn))
430 461
431 return (list(cves_ignored), list(patched_cves), cves_unpatched, cves_status) 462 return (cve_data, cves_status)
432 463
433def get_cve_info(d, cves): 464def get_cve_info(d, cve_data):
434 """ 465 """
435 Get CVE information from the database. 466 Get CVE information from the database.
436 """ 467 """
437 468
438 import sqlite3 469 import sqlite3
439 470
440 cve_data = {}
441 db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro") 471 db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro")
442 conn = sqlite3.connect(db_file, uri=True) 472 conn = sqlite3.connect(db_file, uri=True)
443 473
444 for cve in cves: 474 for cve in cve_data:
445 cursor = conn.execute("SELECT * FROM NVD WHERE ID IS ?", (cve,)) 475 cursor = conn.execute("SELECT * FROM NVD WHERE ID IS ?", (cve,))
446 for row in cursor: 476 for row in cursor:
447 cve_data[row[0]] = {} 477 # The CVE itdelf has been added already
448 cve_data[row[0]]["summary"] = row[1] 478 if row[0] not in cve_data:
449 cve_data[row[0]]["scorev2"] = row[2] 479 bb.note("CVE record %s not present" % row[0])
450 cve_data[row[0]]["scorev3"] = row[3] 480 continue
451 cve_data[row[0]]["modified"] = row[4] 481 #cve_data[row[0]] = {}
452 cve_data[row[0]]["vector"] = row[5] 482 cve_data[row[0]]["NVD-summary"] = row[1]
453 cve_data[row[0]]["vectorString"] = row[6] 483 cve_data[row[0]]["NVD-scorev2"] = row[2]
484 cve_data[row[0]]["NVD-scorev3"] = row[3]
485 cve_data[row[0]]["NVD-modified"] = row[4]
486 cve_data[row[0]]["NVD-vector"] = row[5]
487 cve_data[row[0]]["NVD-vectorString"] = row[6]
454 cursor.close() 488 cursor.close()
455 conn.close() 489 conn.close()
456 return cve_data
457 490
458def cve_write_data_text(d, patched, unpatched, ignored, cve_data): 491def cve_write_data_text(d, cve_data):
459 """ 492 """
460 Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and 493 Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and
461 CVE manifest if enabled. 494 CVE manifest if enabled.
462 """ 495 """
463 496
464 from oe.cve_check import decode_cve_status
465
466 cve_file = d.getVar("CVE_CHECK_LOG") 497 cve_file = d.getVar("CVE_CHECK_LOG")
467 fdir_name = d.getVar("FILE_DIRNAME") 498 fdir_name = d.getVar("FILE_DIRNAME")
468 layer = fdir_name.split("/")[-3] 499 layer = fdir_name.split("/")[-3]
@@ -479,7 +510,7 @@ def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
479 return 510 return
480 511
481 # Early exit, the text format does not report packages without CVEs 512 # Early exit, the text format does not report packages without CVEs
482 if not patched+unpatched+ignored: 513 if not len(cve_data):
483 return 514 return
484 515
485 nvd_link = "https://nvd.nist.gov/vuln/detail/" 516 nvd_link = "https://nvd.nist.gov/vuln/detail/"
@@ -488,36 +519,29 @@ def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
488 bb.utils.mkdirhier(os.path.dirname(cve_file)) 519 bb.utils.mkdirhier(os.path.dirname(cve_file))
489 520
490 for cve in sorted(cve_data): 521 for cve in sorted(cve_data):
491 is_patched = cve in patched 522 if not report_all and (cve_data[cve]["abbrev-status"] == "Patched" or cve_data[cve]["abbrev-status"] == "Ignored"):
492 is_ignored = cve in ignored
493
494 status = "Unpatched"
495 if (is_patched or is_ignored) and not report_all:
496 continue 523 continue
497 if is_ignored:
498 status = "Ignored"
499 elif is_patched:
500 status = "Patched"
501 else:
502 # default value of status is Unpatched
503 unpatched_cves.append(cve)
504
505 write_string += "LAYER: %s\n" % layer 524 write_string += "LAYER: %s\n" % layer
506 write_string += "PACKAGE NAME: %s\n" % d.getVar("PN") 525 write_string += "PACKAGE NAME: %s\n" % d.getVar("PN")
507 write_string += "PACKAGE VERSION: %s%s\n" % (d.getVar("EXTENDPE"), d.getVar("PV")) 526 write_string += "PACKAGE VERSION: %s%s\n" % (d.getVar("EXTENDPE"), d.getVar("PV"))
508 write_string += "CVE: %s\n" % cve 527 write_string += "CVE: %s\n" % cve
509 write_string += "CVE STATUS: %s\n" % status 528 write_string += "CVE STATUS: %s\n" % cve_data[cve]["abbrev-status"]
510 status_details = decode_cve_status(d, cve) 529
511 if 'detail' in status_details: 530 if 'status' in cve_data[cve]:
512 write_string += "CVE DETAIL: %s\n" % status_details['detail'] 531 write_string += "CVE DETAIL: %s\n" % cve_data[cve]["status"]
513 if 'description' in status_details: 532 if 'justification' in cve_data[cve]:
514 write_string += "CVE DESCRIPTION: %s\n" % status_details['description'] 533 write_string += "CVE DESCRIPTION: %s\n" % cve_data[cve]["justification"]
515 write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["summary"] 534
516 write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["scorev2"] 535 if "NVD-summary" in cve_data[cve]:
517 write_string += "CVSS v3 BASE SCORE: %s\n" % cve_data[cve]["scorev3"] 536 write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["NVD-summary"]
518 write_string += "VECTOR: %s\n" % cve_data[cve]["vector"] 537 write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["NVD-scorev2"]
519 write_string += "VECTORSTRING: %s\n" % cve_data[cve]["vectorString"] 538 write_string += "CVSS v3 BASE SCORE: %s\n" % cve_data[cve]["NVD-scorev3"]
539 write_string += "VECTOR: %s\n" % cve_data[cve]["NVD-vector"]
540 write_string += "VECTORSTRING: %s\n" % cve_data[cve]["NVD-vectorString"]
541
520 write_string += "MORE INFORMATION: %s%s\n\n" % (nvd_link, cve) 542 write_string += "MORE INFORMATION: %s%s\n\n" % (nvd_link, cve)
543 if cve_data[cve]["abbrev-status"] == "Unpatched":
544 unpatched_cves.append(cve)
521 545
522 if unpatched_cves and d.getVar("CVE_CHECK_SHOW_WARNINGS") == "1": 546 if unpatched_cves and d.getVar("CVE_CHECK_SHOW_WARNINGS") == "1":
523 bb.warn("Found unpatched CVE (%s), for more information check %s" % (" ".join(unpatched_cves),cve_file)) 547 bb.warn("Found unpatched CVE (%s), for more information check %s" % (" ".join(unpatched_cves),cve_file))
@@ -569,13 +593,11 @@ def cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_fi
569 with open(index_path, "a+") as f: 593 with open(index_path, "a+") as f:
570 f.write("%s\n" % fragment_path) 594 f.write("%s\n" % fragment_path)
571 595
572def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status): 596def cve_write_data_json(d, cve_data, cve_status):
573 """ 597 """
574 Prepare CVE data for the JSON format, then write it. 598 Prepare CVE data for the JSON format, then write it.
575 """ 599 """
576 600
577 from oe.cve_check import decode_cve_status
578
579 output = {"version":"1", "package": []} 601 output = {"version":"1", "package": []}
580 nvd_link = "https://nvd.nist.gov/vuln/detail/" 602 nvd_link = "https://nvd.nist.gov/vuln/detail/"
581 603
@@ -593,8 +615,6 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
593 if include_layers and layer not in include_layers: 615 if include_layers and layer not in include_layers:
594 return 616 return
595 617
596 unpatched_cves = []
597
598 product_data = [] 618 product_data = []
599 for s in cve_status: 619 for s in cve_status:
600 p = {"product": s[0], "cvesInRecord": "Yes"} 620 p = {"product": s[0], "cvesInRecord": "Yes"}
@@ -609,39 +629,31 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
609 "version" : package_version, 629 "version" : package_version,
610 "products": product_data 630 "products": product_data
611 } 631 }
632
612 cve_list = [] 633 cve_list = []
613 634
614 for cve in sorted(cve_data): 635 for cve in sorted(cve_data):
615 is_patched = cve in patched 636 if not report_all and (cve_data[cve]["abbrev-status"] == "Patched" or cve_data[cve]["abbrev-status"] == "Ignored"):
616 is_ignored = cve in ignored
617 status = "Unpatched"
618 if (is_patched or is_ignored) and not report_all:
619 continue 637 continue
620 if is_ignored:
621 status = "Ignored"
622 elif is_patched:
623 status = "Patched"
624 else:
625 # default value of status is Unpatched
626 unpatched_cves.append(cve)
627
628 issue_link = "%s%s" % (nvd_link, cve) 638 issue_link = "%s%s" % (nvd_link, cve)
629 639
630 cve_item = { 640 cve_item = {
631 "id" : cve, 641 "id" : cve,
632 "summary" : cve_data[cve]["summary"], 642 "status" : cve_data[cve]["abbrev-status"],
633 "scorev2" : cve_data[cve]["scorev2"], 643 "link": issue_link,
634 "scorev3" : cve_data[cve]["scorev3"],
635 "vector" : cve_data[cve]["vector"],
636 "vectorString" : cve_data[cve]["vectorString"],
637 "status" : status,
638 "link": issue_link
639 } 644 }
640 status_details = decode_cve_status(d, cve) 645 if 'NVD-summary' in cve_data[cve]:
641 if 'detail' in status_details: 646 cve_item["summary"] = cve_data[cve]["NVD-summary"]
642 cve_item["detail"] = status_details['detail'] 647 cve_item["scorev2"] = cve_data[cve]["NVD-scorev2"]
643 if 'description' in status_details: 648 cve_item["scorev3"] = cve_data[cve]["NVD-scorev3"]
644 cve_item["description"] = status_details['description'] 649 cve_item["vector"] = cve_data[cve]["NVD-vector"]
650 cve_item["vectorString"] = cve_data[cve]["NVD-vectorString"]
651 if 'status' in cve_data[cve]:
652 cve_item["detail"] = cve_data[cve]["status"]
653 if 'justification' in cve_data[cve]:
654 cve_item["description"] = cve_data[cve]["justification"]
655 if 'resource' in cve_data[cve]:
656 cve_item["patch-file"] = cve_data[cve]["resource"]
645 cve_list.append(cve_item) 657 cve_list.append(cve_item)
646 658
647 package_data["issue"] = cve_list 659 package_data["issue"] = cve_list
@@ -653,12 +665,12 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
653 665
654 cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file) 666 cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file)
655 667
656def cve_write_data(d, patched, unpatched, ignored, cve_data, status): 668def cve_write_data(d, cve_data, status):
657 """ 669 """
658 Write CVE data in each enabled format. 670 Write CVE data in each enabled format.
659 """ 671 """
660 672
661 if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1": 673 if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1":
662 cve_write_data_text(d, patched, unpatched, ignored, cve_data) 674 cve_write_data_text(d, cve_data)
663 if d.getVar("CVE_CHECK_FORMAT_JSON") == "1": 675 if d.getVar("CVE_CHECK_FORMAT_JSON") == "1":
664 cve_write_data_json(d, patched, unpatched, ignored, cve_data, status) 676 cve_write_data_json(d, cve_data, status)