[master,kirkstone,4/5] cve-check: major class refactor

Message ID 20220628133713.3390786-4-davide.gardenal@huawei.com
State New, archived
Headers show
Series [master,kirkstone,1/5] lib/oe/cve_check: refactor update_symlinks with safer version | expand

Commit Message

Davide Gardenal June 28, 2022, 1:37 p.m. UTC
The rationale behind refactoring the class is to make testing 
and maintainability easier.

This commit includes:
    - bb var refactor for better readability
    - function and program flow refactor for better scalability
      and extensibility
    - better documentation for all the functions
    - minor bug fixes when using specific configurations

Deleted bb vars:
    - CVE_CHECK_LOG
    - CVE_CHECK_TMP_FILE
    - CVE_CHECK_SUMMARY_DIR
    - CVE_CHECK_SUMMARY_FILE_NAME
    - CVE_CHECK_SUMMARY_FILE
    - CVE_CHECK_SUMMARY_FILE_NAME_JSON
    - CVE_CHECK_SUMMARY_INDEX_PATH
    - CVE_CHECK_LOG_JSON
    - CVE_CHECK_RECIPE_FILE
    - CVE_CHECK_RECIPE_FILE_JSON
    - CVE_CHECK_MANIFEST
    - CVE_CHECK_MANIFEST_JSON
    - CVE_CHECK_CREATE_MANIFEST

Renamed bb vars:
    - CVE_CHECK_DIR -> CVE_CHECK_OUTPUT_DIR
    - CVE_CHECK_COPY_FILES -> CVE_CHECK_CREATE_RECIPE_REPORTS

Added bb vars:
    - CVE_CHECK_CREATE_BUILD_REPORT: flag to control if cve-check
      creates a build report or not
    - CVE_CHECK_CREATE_IMAGE_REPORT: flag to control if cve-check
      creates an image report or not
    - CVE_CHECK_TXT_INDEX_FILE: path of the temporary index file
      for the txt output format. Deleted after the build is
      completed
    - CVE_CHECK_TXT_INDEX_DIR: folder path where all the temp
      recipes reports with txt format are store. Deleted after
      the build is completed
    - CVE_CHECK_JSON_INDEX_FILE: same as CVE_CHECK_TXT_INDEX_FILE
      but for the json format
    - CVE_CHECK_JSON_INDEX_DIR: same as CVE_CHECK_TXT_INDEX_DIR
      but for the json format
    - CVE_CHECK_IMAGE_REPORT_FILE_NAME_BASE: name without extension
      of the report for the image
    - CVE_CHECK_BUILD_REPORT_FILE_NAME_BASE: name without extension
      of the report for the entire build
    - CVE_CHECK_RECIPE_FILE_NAME_BASE: name without extension of
      the report for every recipe

Default output structure (with txt and json format enabled):
tmp
|-log
   |-cve
      |-build_reports
      |  |-txt
      |  |  |- build report files with txt format
      |  |-json
      |     |- build report files with json format
      |-image_reports
      |  |-txt
      |  |  |- image report files with txt format
      |  |-json
      |     |- image report file with json format
      |-recipe_reports
      |  |-txt
      |     |- recipe report files with txt format
      |  |-json
      |     |- recipe report files with json format
      |-cve-report.json -> link pointing to the latest json build report
      |-cve-report.txt  -> link pointing to the latest txt build report

Note that a link to the latest image report is present in the
image deploy folder.

Signed-off-by: Davide Gardenal <davide.gardenal@huawei.com>
---
 meta/classes/cve-check.bbclass | 642 +++++++++++++++++++++++++----------------
 1 file changed, 390 insertions(+), 252 deletions(-)

Comments

Richard Purdie June 28, 2022, 6:15 p.m. UTC | #1
On Tue, 2022-06-28 at 15:37 +0200, Davide Gardenal wrote:
> The rationale behind refactoring the class is to make testing 
> and maintainability easier.
> 
> This commit includes:
>     - bb var refactor for better readability
>     - function and program flow refactor for better scalability
>       and extensibility
>     - better documentation for all the functions
>     - minor bug fixes when using specific configurations
> 
> Deleted bb vars:
>     - CVE_CHECK_LOG
>     - CVE_CHECK_TMP_FILE
>     - CVE_CHECK_SUMMARY_DIR
>     - CVE_CHECK_SUMMARY_FILE_NAME
>     - CVE_CHECK_SUMMARY_FILE
>     - CVE_CHECK_SUMMARY_FILE_NAME_JSON
>     - CVE_CHECK_SUMMARY_INDEX_PATH
>     - CVE_CHECK_LOG_JSON
>     - CVE_CHECK_RECIPE_FILE
>     - CVE_CHECK_RECIPE_FILE_JSON
>     - CVE_CHECK_MANIFEST
>     - CVE_CHECK_MANIFEST_JSON
>     - CVE_CHECK_CREATE_MANIFEST
> 
> Renamed bb vars:
>     - CVE_CHECK_DIR -> CVE_CHECK_OUTPUT_DIR
>     - CVE_CHECK_COPY_FILES -> CVE_CHECK_CREATE_RECIPE_REPORTS
> 
> Added bb vars:
>     - CVE_CHECK_CREATE_BUILD_REPORT: flag to control if cve-check
>       creates a build report or not
>     - CVE_CHECK_CREATE_IMAGE_REPORT: flag to control if cve-check
>       creates an image report or not
>     - CVE_CHECK_TXT_INDEX_FILE: path of the temporary index file
>       for the txt output format. Deleted after the build is
>       completed
>     - CVE_CHECK_TXT_INDEX_DIR: folder path where all the temp
>       recipes reports with txt format are store. Deleted after
>       the build is completed
>     - CVE_CHECK_JSON_INDEX_FILE: same as CVE_CHECK_TXT_INDEX_FILE
>       but for the json format
>     - CVE_CHECK_JSON_INDEX_DIR: same as CVE_CHECK_TXT_INDEX_DIR
>       but for the json format
>     - CVE_CHECK_IMAGE_REPORT_FILE_NAME_BASE: name without extension
>       of the report for the image
>     - CVE_CHECK_BUILD_REPORT_FILE_NAME_BASE: name without extension
>       of the report for the entire build
>     - CVE_CHECK_RECIPE_FILE_NAME_BASE: name without extension of
>       the report for every recipe
> 
> Default output structure (with txt and json format enabled):
> tmp
> > -log
>    |-cve
>       |-build_reports
>       |  |-txt
>       |  |  |- build report files with txt format
>       |  |-json
>       |     |- build report files with json format
>       |-image_reports
>       |  |-txt
>       |  |  |- image report files with txt format
>       |  |-json
>       |     |- image report file with json format
>       |-recipe_reports
>       |  |-txt
>       |     |- recipe report files with txt format
>       |  |-json
>       |     |- recipe report files with json format
>       |-cve-report.json -> link pointing to the latest json build report
>       |-cve-report.txt  -> link pointing to the latest txt build report
> 
> Note that a link to the latest image report is present in the
> image deploy folder.
> 
> Signed-off-by: Davide Gardenal <davide.gardenal@huawei.com>
> ---
>  meta/classes/cve-check.bbclass | 642 +++++++++++++++++++++++++----------------
>  1 file changed, 390 insertions(+), 252 deletions(-)

I'm a bit worried about this patchset since it changes lots of
different things and it does it as a flag day, there is no incremental
approach to the patches or backwards compatibility.

The patch commit long says a lot about the mechanics of the change but
not a lot about the reasons for the change and how they benefit the
user or the project?

If we're going to do something that radical, should we just drop the
text based output and use the json output?

Normally where new common functions are created like 2/5 and 3/5, we'd
adjust existing code to use them in the same patch.

I'm also worried about the implications for the CVE code in dunfell and
kirkstone with this level of changes. At some point those codebases may
need to diverge :/

Cheers,

Richard

Patch

diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
index 50b9247f46..5ee53d4c77 100644
--- a/meta/classes/cve-check.bbclass
+++ b/meta/classes/cve-check.bbclass
@@ -19,44 +19,26 @@ 
 # This class/tool is meant to be used as support and not
 # the only method to check against CVEs. Running this tool
 # doesn't guarantee your packages are free of CVEs.
-
+#
+# Variables below are named using the following convention:
+# CVE_CHECK_ -> class prefix (always to use)
+# _DIR -> complete directory path
+# _FILE -> complete file path (including extension)
+# _FILE_NAME -> file name with extension
+# _FILE_NAME_BASE -> file name without extension (used when multiple extensions could be used)
+# For example: CVE_CHECK_IMAGE_REPORT_FILE_NAME_BASE has "_FILE_NAME_BASE" so that's just the file name,
+# without the extension, of the report file. And has "CVE_CHECK_" to indicate that this variable is
+# from the cve-check class
+
+
+# CHECK OPTIONS
 # The product name that the CVE database uses defaults to BPN, but may need to
-# be overriden per recipe (for example tiff.bb sets CVE_PRODUCT=libtiff).
+# be overridden per recipe (for example tiff.bb sets CVE_PRODUCT=libtiff).
 CVE_PRODUCT ??= "${BPN}"
 CVE_VERSION ??= "${PV}"
 
-CVE_CHECK_DB_DIR ?= "${DL_DIR}/CVE_CHECK"
-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_TMP_FILE ?= "${TMPDIR}/cve_check"
-CVE_CHECK_SUMMARY_DIR ?= "${LOG_DIR}/cve"
-CVE_CHECK_SUMMARY_FILE_NAME ?= "cve-summary"
-CVE_CHECK_SUMMARY_FILE ?= "${CVE_CHECK_SUMMARY_DIR}/${CVE_CHECK_SUMMARY_FILE_NAME}"
-CVE_CHECK_SUMMARY_FILE_NAME_JSON = "cve-summary.json"
-CVE_CHECK_SUMMARY_INDEX_PATH = "${CVE_CHECK_SUMMARY_DIR}/cve-summary-index.txt"
-
-CVE_CHECK_LOG_JSON ?= "${T}/cve.json"
-
-CVE_CHECK_DIR ??= "${DEPLOY_DIR}/cve"
-CVE_CHECK_RECIPE_FILE ?= "${CVE_CHECK_DIR}/${PN}"
-CVE_CHECK_RECIPE_FILE_JSON ?= "${CVE_CHECK_DIR}/${PN}_cve.json"
-CVE_CHECK_MANIFEST ?= "${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${IMAGE_NAME_SUFFIX}.cve"
-CVE_CHECK_MANIFEST_JSON ?= "${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${IMAGE_NAME_SUFFIX}.json"
-CVE_CHECK_COPY_FILES ??= "1"
-CVE_CHECK_CREATE_MANIFEST ??= "1"
-
-# Report Patched or Ignored CVEs
-CVE_CHECK_REPORT_PATCHED ??= "1"
-
-CVE_CHECK_SHOW_WARNINGS ??= "1"
-
-# Provide text output
-CVE_CHECK_FORMAT_TEXT ??= "1"
-
-# Provide JSON output
-CVE_CHECK_FORMAT_JSON ??= "1"
+# set to "alphabetical" for version using single alphabetical character as increment release
+CVE_VERSION_SUFFIX ??= ""
 
 # Check for packages without CVEs (no issues or missing product name)
 CVE_CHECK_COVERAGE ??= "1"
@@ -72,66 +54,63 @@  CVE_CHECK_SKIP_RECIPE ?= ""
 #
 CVE_CHECK_IGNORE ?= ""
 
-# Layers to be excluded
-CVE_CHECK_LAYER_EXCLUDELIST ??= ""
 
-# Layers to be included
-CVE_CHECK_LAYER_INCLUDELIST ??= ""
+# DATABASE OPTIONS
+CVE_CHECK_DB_DIR ?= "${DL_DIR}/CVE_CHECK"
+CVE_CHECK_DB_FILE ?= "${CVE_CHECK_DB_DIR}/nvdcve_1.1.db"
+CVE_CHECK_DB_FILE_LOCK ?= "${CVE_CHECK_DB_FILE}.lock"
 
 
-# set to "alphabetical" for version using single alphabetical character as increment release
-CVE_VERSION_SUFFIX ??= ""
+# TEMPORARY FILES
+CVE_CHECK_TXT_INDEX_FILE ?= "${TMPDIR}/cve-report-index_txt.txt"
+CVE_CHECK_TXT_INDEX_DIR ?= "${TMPDIR}/cve-tmp-files_txt"
 
-def generate_json_report(d, out_path, link_path):
-    if os.path.exists(d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")):
-        import json
-        from oe.cve_check import cve_check_merge_jsons, update_symlinks
+CVE_CHECK_JSON_INDEX_FILE ?= "${TMPDIR}/cve-report-index_json.txt"
+CVE_CHECK_JSON_INDEX_DIR ?= "${TMPDIR}/cve-tmp-files_json"
 
-        bb.note("Generating JSON CVE summary")
-        index_file = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")
-        summary = {"version":"1", "package": []}
-        with open(index_file) as f:
-            filename = f.readline()
-            while filename:
-                with open(filename.rstrip()) as j:
-                    data = json.load(j)
-                    cve_check_merge_jsons(summary, data)
-                filename = f.readline()
 
-        with open(out_path, "w") as f:
-            json.dump(summary, f, indent=2)
+# OUTPUT OPTIONS
+# Output directory
+CVE_CHECK_OUTPUT_DIR ?= "${LOG_DIR}/cve"
 
-        update_symlinks(out_path, link_path)
+# File names without extension of the image and build reports
+# Build reports should not contain image specific bb vars line IMAGE_NAME or IMAGE_LINK_NAME
+CVE_CHECK_IMAGE_REPORT_FILE_NAME_BASE ?= "cve-report_${IMAGE_LINK_NAME}"
+CVE_CHECK_BUILD_REPORT_FILE_NAME_BASE ?= "cve-report"
 
-python cve_save_summary_handler () {
-    import shutil
-    import datetime
-    from oe.cve_check import update_symlinks
+# Create a report for each recipe in the build
+CVE_CHECK_CREATE_RECIPE_REPORTS ??= "1"
 
-    cve_tmp_file = d.getVar("CVE_CHECK_TMP_FILE")
+# Name of the cve check per recipe file. If this is changed be sure that every recipe has a different
+# value otherwise they will override each other
+CVE_CHECK_RECIPE_FILE_NAME_BASE ?= "${PN}"
 
-    cve_summary_name = d.getVar("CVE_CHECK_SUMMARY_FILE_NAME")
-    cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR")
-    bb.utils.mkdirhier(cvelogpath)
+# Create a report file for the whole build
+CVE_CHECK_CREATE_BUILD_REPORT ??= "1"
 
-    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)
-        cvefile_link = os.path.join(cvelogpath, cve_summary_name)
-        update_symlinks(cve_summary_file, cvefile_link)
-        bb.plain("Complete CVE report summary created at: %s" % cvefile_link)
-
-    if d.getVar("CVE_CHECK_FORMAT_JSON") == "1":
-        json_summary_link_name = os.path.join(cvelogpath, d.getVar("CVE_CHECK_SUMMARY_FILE_NAME_JSON"))
-        json_summary_name = os.path.join(cvelogpath, "%s-%s.json" % (cve_summary_name, timestamp))
-        generate_json_report(d, json_summary_name, json_summary_link_name)
-        bb.plain("Complete CVE JSON report summary created at: %s" % json_summary_link_name)
-}
+# Create a report file for each image
+CVE_CHECK_CREATE_IMAGE_REPORT ??= "1"
+
+# If set patched CVEs will show in the reports
+CVE_CHECK_REPORT_PATCHED ??= "1"
+
+# If set bitbake will show a warning if unpatched CVEs are found
+CVE_CHECK_SHOW_WARNINGS ??= "1"
+
+# Warning: Disabling one of these options doesn't clear their output folders, disabling both won't produce any files.
+# Provide text output
+CVE_CHECK_FORMAT_TEXT ??= "1"
+# Provide JSON output
+CVE_CHECK_FORMAT_JSON ??= "1"
+
+
+# LAYERS OPTIONS
+# Layers to be excluded
+CVE_CHECK_LAYER_EXCLUDELIST ??= ""
+
+# Layers to be included
+CVE_CHECK_LAYER_INCLUDELIST ??= ""
 
-addhandler cve_save_summary_handler
-cve_save_summary_handler[eventmask] = "bb.event.BuildCompleted"
 
 python do_cve_check () {
     """
@@ -145,6 +124,10 @@  python do_cve_check () {
         except FileNotFoundError:
             bb.fatal("Failure in searching patches")
         ignored, patched, unpatched, status = check_cves(d, patched_cves)
+
+        if unpatched and d.getVar("CVE_CHECK_SHOW_WARNINGS") == "1":
+            bb.warn("Found unpatched CVE (%s)" % (" ".join(unpatched)))
+
         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)
@@ -157,97 +140,62 @@  addtask cve_check before do_build
 do_cve_check[depends] = "cve-update-db-native:do_fetch"
 do_cve_check[nostamp] = "1"
 
-python cve_check_cleanup () {
+python cve_check_write_image_report () {
     """
-    Delete the file used to gather all the CVE information.
+    After 'do_rootfs' task is executed, if CVE_CHECK_CREATE_IMAGE_REPORT is set
+    a complete image report is created.
+    This includes all the information contained in the recipe reports builded by the image.
     """
-    bb.utils.remove(e.data.getVar("CVE_CHECK_TMP_FILE"))
-    bb.utils.remove(e.data.getVar("CVE_CHECK_SUMMARY_INDEX_PATH"))
+    if d.getVar('CVE_CHECK_CREATE_IMAGE_REPORT') == "1":
+        from oe.rootfs import image_list_installed_packages_pn
+        deploy_dir = d.getVar("DEPLOY_DIR_IMAGE")
+        report_name_base = d.getVar("CVE_CHECK_IMAGE_REPORT_FILE_NAME_BASE")
+        report_dir_name = "image_reports"
+        recipes_filter = list(image_list_installed_packages_pn(d))
+
+        if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1":
+            generate_report(d, report_dir_name, report_name_base, "txt", generate_text_report, link_override=deploy_dir, gen_filter=recipes_filter)
+
+        if d.getVar("CVE_CHECK_FORMAT_JSON") == "1":
+            generate_report(d, report_dir_name, report_name_base, "json",
+                            generate_json_report, link_override=deploy_dir,
+                            gen_filter=recipes_filter)
 }
 
-addhandler cve_check_cleanup
-cve_check_cleanup[eventmask] = "bb.cooker.CookerExit"
+ROOTFS_POSTPROCESS_COMMAND:prepend = "${@'cve_check_write_image_report; ' if d.getVar('CVE_CHECK_CREATE_IMAGE_REPORT') == '1' else ''}"
+do_rootfs[recrdeptask] += "${@'do_cve_check' if d.getVar('CVE_CHECK_CREATE_IMAGE_REPORT') == '1' else ''}"
 
-python cve_check_write_rootfs_manifest () {
+python cve_create_build_report_handler () {
     """
-    Create CVE manifest when building an image
+    After the build is completed, if "CVE_CHECK_CREATE_BUILD_REPORT" is set
+    a complete report is created including all CVEs recipe reports information in a single file.
     """
+    if d.getVar("CVE_CHECK_CREATE_BUILD_REPORT") == "1":
+        report_name_base = d.getVar("CVE_CHECK_BUILD_REPORT_FILE_NAME_BASE")
+        build_reports_dir = "build_reports"
 
-    import shutil
-    import json
-    from oe.rootfs import image_list_installed_packages
-    from oe.cve_check import cve_check_merge_jsons, update_symlinks
-
-    if d.getVar("CVE_CHECK_COPY_FILES") == "1":
-        deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE")
-        if os.path.exists(deploy_file):
-            bb.utils.remove(deploy_file)
-        deploy_file_json = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
-        if os.path.exists(deploy_file_json):
-            bb.utils.remove(deploy_file_json)
-
-    # Create a list of relevant recipies
-    recipies = set()
-    for pkg in list(image_list_installed_packages(d)):
-        pkg_info = os.path.join(d.getVar('PKGDATA_DIR'),
-                                'runtime-reverse', pkg)
-        pkg_data = oe.packagedata.read_pkgdatafile(pkg_info)
-        recipies.add(pkg_data["PN"])
-
-    bb.note("Writing rootfs CVE manifest")
-    deploy_dir = d.getVar("DEPLOY_DIR_IMAGE")
-    link_name = d.getVar("IMAGE_LINK_NAME")
-
-    json_data = {"version":"1", "package": []}
-    text_data = ""
-    enable_json = d.getVar("CVE_CHECK_FORMAT_JSON") == "1"
-    enable_text = d.getVar("CVE_CHECK_FORMAT_TEXT") == "1"
-
-    save_pn = d.getVar("PN")
-
-    for pkg in recipies:
-        # To be able to use the CVE_CHECK_RECIPE_FILE variable we have to evaluate
-        # it with the different PN names set each time.
-        d.setVar("PN", pkg)
-        if enable_text:
-            pkgfilepath = d.getVar("CVE_CHECK_RECIPE_FILE")
-            if os.path.exists(pkgfilepath):
-                with open(pkgfilepath) as pfile:
-                    text_data += pfile.read()
-
-        if enable_json:
-            pkgfilepath = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
-            if os.path.exists(pkgfilepath):
-                with open(pkgfilepath) as j:
-                    data = json.load(j)
-                    cve_check_merge_jsons(json_data, data)
-
-    d.setVar("PN", save_pn)
-
-    if enable_text:
-        link_path = os.path.join(deploy_dir, "%s.cve" % link_name)
-        manifest_name = d.getVar("CVE_CHECK_MANIFEST")
-
-        with open(manifest_name, "w") as f:
-            f.write(text_data)
-
-        update_symlinks(manifest_name, link_path)
-        bb.plain("Image CVE report stored in: %s" % manifest_name)
-
-    if enable_json:
-        link_path = os.path.join(deploy_dir, "%s.json" % link_name)
-        manifest_name = d.getVar("CVE_CHECK_MANIFEST_JSON")
-
-        with open(manifest_name, "w") as f:
-            json.dump(json_data, f, indent=2)
-
-        update_symlinks(manifest_name, link_path)
-        bb.plain("Image CVE JSON report stored in: %s" % manifest_name)
+        if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1":
+            generate_report(d, build_reports_dir, report_name_base, "txt", generate_text_report)
+
+        if d.getVar("CVE_CHECK_FORMAT_JSON") == "1":
+            generate_report(d, build_reports_dir, report_name_base, "json", generate_json_report)
 }
 
-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 ''}"
-do_populate_sdk[recrdeptask] += "${@'do_cve_check' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}"
+addhandler cve_create_build_report_handler
+cve_create_build_report_handler[eventmask] = "bb.event.BuildCompleted"
+
+python cve_check_cleanup () {
+    """
+    Delete temporary files on bitbake exit
+    """
+    bb.utils.remove(e.data.getVar("CVE_CHECK_TXT_INDEX_FILE"))
+    bb.utils.remove(e.data.getVar("CVE_CHECK_TXT_INDEX_DIR"), recurse=True)
+    bb.utils.remove(e.data.getVar("CVE_CHECK_JSON_INDEX_FILE"))
+    bb.utils.remove(e.data.getVar("CVE_CHECK_JSON_INDEX_DIR"), recurse=True)
+}
+
+addhandler cve_check_cleanup
+cve_check_cleanup[eventmask] = "bb.cooker.CookerExit"
 
 def check_cves(d, patched_cves):
     """
@@ -392,35 +340,117 @@  def get_cve_info(d, cves):
     conn.close()
     return cve_data
 
-def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
+def create_file_and_update_index(d, content, recipes_tmp_dir, index_file, extension):
     """
-    Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and
-    CVE manifest if enabled.
+    Helper function used to create a file inside recipes_tmp_dir with content in it.
+    Then update the index file with it's path.
+
+    Args:
+        d: Bitbake data store object.
+        content: String with the temporary recipe report.
+        recipes_tmp_dir: Path of the folder containing the temporary recipes reports.
+        index_file: Path of the index file. The index file is used to save all the temporary recipes reports paths.
+        extension: String of the file extension. (Like "txt" or "json")
+
+    Returns:
+        None. Side effects the temporary recipes report file creation and appends its path to the index.
     """
+    import bb
+    bb.utils.mkdirhier(recipes_tmp_dir)
+    fragment_file_name = "%s.%s" % (d.getVar("PN"), extension)
+    fragment_file = os.path.join(recipes_tmp_dir, fragment_file_name)
 
-    cve_file = d.getVar("CVE_CHECK_LOG")
-    fdir_name  = d.getVar("FILE_DIRNAME")
-    layer = fdir_name.split("/")[-3]
+    with open(fragment_file, "w") as f:
+        f.write(content)
+    with open(index_file, "a+") as f:
+        f.write("%s\n" % fragment_file)
 
-    include_layers = d.getVar("CVE_CHECK_LAYER_INCLUDELIST").split()
-    exclude_layers = d.getVar("CVE_CHECK_LAYER_EXCLUDELIST").split()
+def tmp_report_saver_json(d, content):
+    """
+    Helper function used to save temporary information used when
+    assembling a complete image or build report.
+    For JSON reports only.
 
-    report_all = d.getVar("CVE_CHECK_REPORT_PATCHED") == "1"
+    Args:
+        d: Bitbake data store object.
+        content: String that will be wrote to the output file.
 
-    if exclude_layers and layer in exclude_layers:
-        return
+    Returns:
+        None. Side effect from create_file_and_update_index.
+    """
+    recipes_tmp_dir = d.getVar("CVE_CHECK_JSON_INDEX_DIR")
+    index_file = d.getVar("CVE_CHECK_JSON_INDEX_FILE")
+    create_file_and_update_index(d, content, recipes_tmp_dir, index_file, "json")
 
-    if include_layers and layer not in include_layers:
-        return
+def tmp_report_saver_txt(d, content):
+    """
+    Helper function used to save temporary information used when
+    assembling a complete image or build report.
+    For txt reports only.
 
-    # Early exit, the text format does not report packages without CVEs
-    if not patched+unpatched+ignored:
-        return
+    Args:
+        d: Bitbake data store object.
+        content: String that will be wrote to the output file.
 
+    Returns:
+        None. Side effect from create_file_and_update_index.
+    """
+    recipes_tmp_dir = d.getVar("CVE_CHECK_TXT_INDEX_DIR")
+    index_file = d.getVar("CVE_CHECK_TXT_INDEX_FILE")
+    create_file_and_update_index(d, content, recipes_tmp_dir, index_file, "txt")
+
+def save_cve_recipe_report(d, content, format, tmp_report_saver):
+    """
+    Save in a dedicated file the content if "CVE_CHECK_CREATE_RECIPE_REPORTS" is set.
+    If a report flag is set (image or build level) then "tmp_report_saver" is executed passing "content",
+    this should save all the information needed when composing the complete report later.
+
+    Args:
+        d: Bitbake data store object.
+        content: String that will be wrote to the output file.
+        format: String of the output format name. Used as file extension and as the name for the
+            format specific output folder.
+        tmp_report_saver: Function that takes (d, content) and saves `content` to a temporary file.
+            This is used in case of image or build reports are enabled.
+
+    Returns:
+        None. Side effects the recipe report file creation and tmp_report_saver side effect (only in case
+        CVE_CHECK_CREATE_IMAGE_REPORT or CVE_CHECK_CREATE_BUILD_REPORT is set).
+    """
+    if d.getVar("CVE_CHECK_CREATE_RECIPE_REPORTS") == "1":
+
+        recipe_file_name = "%s.%s" % (d.getVar("CVE_CHECK_RECIPE_FILE_NAME_BASE"), format)
+        out_dir = d.getVar("CVE_CHECK_OUTPUT_DIR")
+        recipe_reports_dir = os.path.join(out_dir, "recipes_reports/%s" % format)
+        recipe_file = os.path.join(recipe_reports_dir, recipe_file_name)
+        bb.utils.mkdirhier(os.path.dirname(recipe_file))
+        with open(recipe_file, "w") as f:
+            f.write(content)
+
+    if d.getVar("CVE_CHECK_CREATE_IMAGE_REPORT") == "1" or d.getVar("CVE_CHECK_CREATE_BUILD_REPORT") == 1:
+        tmp_report_saver(d, content)
+
+def generate_txt_cve_recipe_report_content(d, patched, unpatched, ignored, cve_data):
+    """
+    Construct the recipe report content string from the cve raw data.
+
+    Args:
+        d: Bitbake data store object.
+        patched: List of patched CVEs.
+        unpatched: List of unpatched CVEs.
+        ignored: List of ignored CVEs.
+        cve_data: Dictionary containing all the CVEs data.
+
+    Returns:
+        Recipe report content string in txt format.
+    """
+    from oe.utils import get_current_recipe_layer
+
+    layer = get_current_recipe_layer(d)
     nvd_link = "https://nvd.nist.gov/vuln/detail/"
     write_string = ""
     unpatched_cves = []
-    bb.utils.mkdirhier(os.path.dirname(cve_file))
+    report_all = d.getVar("CVE_CHECK_REPORT_PATCHED") == "1"
 
     for cve in sorted(cve_data):
         is_patched = cve in patched
@@ -428,7 +458,7 @@  def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
 
         if (is_patched or is_ignored) and not report_all:
             continue
-
+
         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"))
@@ -446,78 +476,31 @@  def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
         write_string += "VECTOR: %s\n" % cve_data[cve]["vector"]
         write_string += "MORE INFORMATION: %s%s\n\n" % (nvd_link, cve)
 
-    if unpatched_cves and d.getVar("CVE_CHECK_SHOW_WARNINGS") == "1":
-        bb.warn("Found unpatched CVE (%s), for more information check %s" % (" ".join(unpatched_cves),cve_file))
-
-    with open(cve_file, "w") as f:
-        bb.note("Writing file %s with CVE information" % cve_file)
-        f.write(write_string)
-
-    if d.getVar("CVE_CHECK_COPY_FILES") == "1":
-        deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE")
-        bb.utils.mkdirhier(os.path.dirname(deploy_file))
-        with open(deploy_file, "w") as f:
-            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_TMP_FILE"), "a") as f:
-            f.write("%s" % write_string)
+    return write_string
 
-def cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file):
+def generate_json_cve_recipe_report_content(d, patched, unpatched, ignored, cve_data, cve_status):
     """
-    Write CVE information in the JSON format: to WORKDIR; and to
-    CVE_CHECK_DIR, if CVE manifest if enabled, write fragment
-    files that will be assembled at the end in cve_check_write_rootfs_manifest.
+    Construct the recipe report content string from the cve raw data.
+
+    Args:
+        d: Bitbake data store object.
+        patched: List of patched CVEs.
+        unpatched: List of unpatched CVEs.
+        ignored: List of ignored CVEs.
+        cve_data: Dictionary containing all the CVEs data.
+        cve_status: List of products with their CVE status.
+
+    Returns:
+        Recipe report content string in json format.
     """
-
     import json
+    from oe.utils import get_current_recipe_layer
 
-    write_string = json.dumps(output, indent=2)
-    with open(direct_file, "w") as f:
-        bb.note("Writing file %s with CVE information" % direct_file)
-        f.write(write_string)
-
-    if d.getVar("CVE_CHECK_COPY_FILES") == "1":
-        bb.utils.mkdirhier(os.path.dirname(deploy_file))
-        with open(deploy_file, "w") as f:
-            f.write(write_string)
-
-    if d.getVar("CVE_CHECK_CREATE_MANIFEST") == "1":
-        cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR")
-        index_path = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")
-        bb.utils.mkdirhier(cvelogpath)
-        fragment_file = os.path.basename(deploy_file)
-        fragment_path = os.path.join(cvelogpath, fragment_file)
-        with open(fragment_path, "w") as f:
-            f.write(write_string)
-        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):
-    """
-    Prepare CVE data for the JSON format, then write it.
-    """
-
+    layer = get_current_recipe_layer(d)
     output = {"version":"1", "package": []}
     nvd_link = "https://nvd.nist.gov/vuln/detail/"
-
-    fdir_name  = d.getVar("FILE_DIRNAME")
-    layer = fdir_name.split("/")[-3]
-
-    include_layers = d.getVar("CVE_CHECK_LAYER_INCLUDELIST").split()
-    exclude_layers = d.getVar("CVE_CHECK_LAYER_EXCLUDELIST").split()
-
     report_all = d.getVar("CVE_CHECK_REPORT_PATCHED") == "1"
 
-    if exclude_layers and layer in exclude_layers:
-        return
-
-    if include_layers and layer not in include_layers:
-        return
-
     unpatched_cves = []
 
     product_data = []
@@ -566,18 +549,173 @@  def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
     package_data["issue"] = cve_list
     output["package"].append(package_data)
 
-    direct_file = d.getVar("CVE_CHECK_LOG_JSON")
-    deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
-    manifest_file = d.getVar("CVE_CHECK_SUMMARY_FILE_NAME_JSON")
+    return json.dumps(output, indent=2)
 
-    cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file)
+def is_layer_checked(d):
+    """
+        Helper function used to check if the layer of the current recipe
+        is expected to be checked for CVEs. A layer isn't checked if:
+        CVE_CHECK_LAYER_INCLUDELIST exists and the layer is not on the list,
+        or CVE_CHECK_LAYER_EXCLUDELIST exists and the layer is on the list.
+
+        Args:
+            d: Bitbake data store object.
+
+        Returns:
+            True if the layer is ok, otherwise False.
+    """
+    from oe.utils import get_current_recipe_layer
+
+    layer = get_current_recipe_layer(d)
+    include_layers = d.getVar("CVE_CHECK_LAYER_INCLUDELIST").split()
+    exclude_layers = d.getVar("CVE_CHECK_LAYER_EXCLUDELIST").split()
+
+    if (include_layers and layer not in include_layers) or \
+       (exclude_layers and layer in exclude_layers):
+        return False
+    else:
+        return True
 
 def cve_write_data(d, patched, unpatched, ignored, cve_data, status):
     """
-    Write CVE data in each enabled format.
+    Checks if the layer of the current recipe is ok then calls the functions to generate and save
+    the recipe reports in txt and JSON formats only if the relative flags are set.
+
+    Args:
+        d: Bitbake data store object.
+        patched: List of patched CVEs.
+        unpatched: List of unpatched CVEs.
+        ignored: List of ignored CVEs.
+        cve_data: Dictionary containing all the CVEs data.
+        cve_status: List of products with their CVE status.
+
+    Returns:
+        None. Same side effect of save_cve_recipe_report.
+
+    """
+    if is_layer_checked(d):
+        if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1":
+            txt_content = generate_txt_cve_recipe_report_content(d, patched, unpatched, ignored, cve_data)
+            save_cve_recipe_report(d, txt_content, "txt", tmp_report_saver_txt)
+        if d.getVar("CVE_CHECK_FORMAT_JSON") == "1":
+            json_content = generate_json_cve_recipe_report_content(d, patched, unpatched, ignored, cve_data, status)
+            save_cve_recipe_report(d, json_content, "json", tmp_report_saver_json)
+
+def get_content_list(d, index_file, filter=None):
+    """
+    Given the index file path and a filter read all the files listed in the index.
+    If filter is not None use it on the file name (without extension).
+
+    Args:
+        d: Bitbake data store object.
+        index_file: Path of the index file.
+        filter: List of product name, used to filter out the reports to include in the output list.
+
+    Returns:
+        List of strings representing the content of the files read.
+    """
+    output_list = []
+    with open(index_file) as f:
+        file_path = f.readline()
+        while file_path:
+            file_path = file_path.rstrip()
+            # Get the file name without extension
+            file_name = file_path.split("/")[-1].split(".")[0]
+            if filter is None or (filter and file_name in filter):
+                with open(file_path, "r") as j:
+                    output_list.append(j.read())
+            file_path = f.readline()
+
+    return output_list
+
+def generate_json_report(d, report_file, report_link, filter=None):
+    """
+    Generate the JSON reports (image or build level).
+    Store the results in report_file and creates the link from report_link to that.
+    The report can be filtered using a list of file names.
+
+    Args:
+        d: Bitbake data store object.
+        report_file: Path of the report file to create.
+        report_link: Path where to create the link to report_file.
+        filter: List of product name, used to filter out the products to include in report.
+
+    Returns:
+        None. Side effects the report and link creation in json format.
+    """
+    if os.path.exists(d.getVar("CVE_CHECK_JSON_INDEX_FILE")):
+        import json
+        from oe.cve_check import cve_check_merge_jsons, update_symlinks
+
+        index_file = d.getVar("CVE_CHECK_JSON_INDEX_FILE")
+        report_dict = {"version":"1", "package": []}
+        temp_content_list = get_content_list(d, index_file, filter)
+        [cve_check_merge_jsons(report_dict, json.loads(s)) for s in temp_content_list]
+
+        with open(report_file, "w") as f:
+            json.dump(report_dict, f, indent=2)
+
+        update_symlinks(report_file, report_link)
+
+def generate_text_report(d, report_file, report_link, filter=None):
+    """
+    Generate the txt reports (image or build level).
+    Store the results in report_file and creates the link from report_link to that.
+
+    Args:
+        d: Bitbake data store object.
+        report_file: Path of the report file to create.
+        report_link: Path where to create the link to report_file.
+        filter: List of product name, used to filter out the products to include in report.
+
+    Returns:
+        None. Side effects the report and link creation in txt format.
     """
 
-    if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1":
-        cve_write_data_text(d, patched, unpatched, ignored, cve_data)
-    if d.getVar("CVE_CHECK_FORMAT_JSON") == "1":
-        cve_write_data_json(d, patched, unpatched, ignored, cve_data, status)
+    if os.path.exists(d.getVar("CVE_CHECK_TXT_INDEX_FILE")):
+        from oe.cve_check import update_symlinks
+        index_file = d.getVar("CVE_CHECK_TXT_INDEX_FILE")
+        report_out = "".join(get_content_list(d, index_file, filter))
+
+        with open(report_file, "w") as f:
+            f.write(report_out)
+
+        update_symlinks(report_file, report_link)
+
+def generate_report(d, report_dir, report_name_base, extension, generator_func, link_override=None, gen_filter=None):
+    """
+    Form the necessary paths and directory structures to call the generator_func, that generates and saves the report.
+    Args:
+        report_dir: Path of the subfolder that is created inside the out_dir. This will store all format folders (txt and json folders).
+        report_name_base: Name of the report without the extension
+        extension: String of the extension (txt or json are currently used)
+        generator_func: Function used to generate the report. It takes the report output path, a link path to save the output and a list to use as a filter.
+        link_override: Optional argument used to override the standard path of the link (inside CVE_CHECK_OUTPUT_DIR), this will be used
+            instead of out_dir when forming the link path.
+        gen_filter: Optional argument to pass as "filter" to generator_func. List of file names used by generator functions to filter
+            the recipe included in the report
+
+    Return:
+        None. Same side effect as generator_func. Prints where the report link is located.
+    """
+    import bb
+    import shutil
+    import datetime
+
+    out_dir = d.getVar("CVE_CHECK_OUTPUT_DIR")
+    timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
+    build_reports_dir = os.path.join(out_dir, report_dir)
+
+    report_file_name = "%s-%s.%s" % (report_name_base, timestamp, extension)
+    report_folder_dir = os.path.join(build_reports_dir, extension)
+    bb.utils.mkdirhier(report_folder_dir)
+    report_file = os.path.join(report_folder_dir, report_file_name)
+
+    if link_override is None:
+        report_link = os.path.join(out_dir, "%s.%s" % (report_name_base, extension))
+    else:
+        report_link = os.path.join(link_override, "%s.%s" % (report_name_base, extension))
+
+    generator_func(d, report_file, report_link, filter=gen_filter)
+
+    bb.plain("Report created at: %s" % report_link)