diff mbox series

[v5,1/2] cve-check: add option to add additional patched CVEs

Message ID 20230612115743.52686-2-andrej.valek@siemens.com
State New
Headers show
Series [v5,1/2] cve-check: add option to add additional patched CVEs | expand

Commit Message

Andrej Valek June 12, 2023, 11:57 a.m. UTC
- Replace CVE_CHECK_IGNORE with CVE_STATUS + [CVE_STATUS_DETAIL] +
[CVE_STATUS_DESCRIPTION] to be more flexible. CVE_STATUS should
contain flag for each CVE with accepted values "Ignored", "Patched"
or "Unpatched". It allows to add a status for each CVEs.
- Optional CVE_STATUS_DEATAIL flag variable may contain a detailed
status. Possible options for each status:
- Patched
 - fixed-version, backported-patch, cpe-stable-backport or other
- Unpatched
 - vulnerable-investigating or other
- Ignored
 - cpe-incorrect, not-applicable-platform, upstream-wontfix
   not-applicable-config, not-affected or other
- Optional CVE_STATUS_DESCRIPTION flag variable may contain a reason
why the CVE status was used. Both optionals will be added in csv/json
report like a new "detail" an "description" entries
- Settings the same status and reason for multiple CVEs is possible
via CVE_STATUS_GROUPS variable.
- All listed CVEs in CVE_CHECK_IGNORE are copied to CVE_STATUS with
value "Ignored" like a fallback.

Examples of usage:
CVE_STATUS[CVE-1234-0001] = "Ignored" # or "Patched" or "Unpatched"
CVE_STATUS[CVE-1234-0002] = "Ignored"
CVE_STATUS_DETAIL[CVE-1234-0002] = "not-applicable-platform"
CVE_STATUS_DESCRIPTION[CVE-1234-0002] = "Issue only applies on Windows"

CVE_STATUS_GROUPS = "CVE_STATUS_WIN CVE_STATUS_PATCHED"
CVE_STATUS_WIN = "CVE-1234-0001 CVE-1234-0002"
CVE_STATUS_WIN[status] = "Ignored"
CVE_STATUS_DETAIL[detail] = "not-applicable-platform"
CVE_STATUS_WIN[description] = "Issue only applies on Windows"

CVE_STATUS_PATCHED = "CVE-1234-0003 CVE-1234-0004"
CVE_STATUS_PATCHED[status] = "Patched"
CVE_STATUS_DETAIL[detail] = "fixed-version"
CVE_STATUS_PATCHED[description] = "Fixed externally"

Signed-off-by: Andrej Valek <andrej.valek@siemens.com>
Signed-off-by: Peter Marko <peter.marko@siemens.com>
---
 meta/classes/cve-check.bbclass | 89 +++++++++++++++++++++++++++++-----
 meta/lib/oe/cve_check.py       |  6 +++
 2 files changed, 83 insertions(+), 12 deletions(-)

Comments

Richard Purdie June 15, 2023, 12:47 p.m. UTC | #1
On Mon, 2023-06-12 at 13:57 +0200, Andrej Valek via
lists.openembedded.org wrote:
> - Replace CVE_CHECK_IGNORE with CVE_STATUS + [CVE_STATUS_DETAIL] +
> [CVE_STATUS_DESCRIPTION] to be more flexible. CVE_STATUS should
> contain flag for each CVE with accepted values "Ignored", "Patched"
> or "Unpatched". It allows to add a status for each CVEs.
> - Optional CVE_STATUS_DEATAIL flag variable may contain a detailed
> status. Possible options for each status:
> - Patched
>  - fixed-version, backported-patch, cpe-stable-backport or other
> - Unpatched
>  - vulnerable-investigating or other
> - Ignored
>  - cpe-incorrect, not-applicable-platform, upstream-wontfix
>    not-applicable-config, not-affected or other
> - Optional CVE_STATUS_DESCRIPTION flag variable may contain a reason
> why the CVE status was used. Both optionals will be added in csv/json
> report like a new "detail" an "description" entries
> - Settings the same status and reason for multiple CVEs is possible
> via CVE_STATUS_GROUPS variable.
> - All listed CVEs in CVE_CHECK_IGNORE are copied to CVE_STATUS with
> value "Ignored" like a fallback.
> 
> Examples of usage:
> CVE_STATUS[CVE-1234-0001] = "Ignored" # or "Patched" or "Unpatched"
> CVE_STATUS[CVE-1234-0002] = "Ignored"
> CVE_STATUS_DETAIL[CVE-1234-0002] = "not-applicable-platform"
> CVE_STATUS_DESCRIPTION[CVE-1234-0002] = "Issue only applies on Windows"
> 
> CVE_STATUS_GROUPS = "CVE_STATUS_WIN CVE_STATUS_PATCHED"
> CVE_STATUS_WIN = "CVE-1234-0001 CVE-1234-0002"
> CVE_STATUS_WIN[status] = "Ignored"
> CVE_STATUS_DETAIL[detail] = "not-applicable-platform"
> CVE_STATUS_WIN[description] = "Issue only applies on Windows"
> 
> CVE_STATUS_PATCHED = "CVE-1234-0003 CVE-1234-0004"
> CVE_STATUS_PATCHED[status] = "Patched"
> CVE_STATUS_DETAIL[detail] = "fixed-version"
> CVE_STATUS_PATCHED[description] = "Fixed externally"
> 
> Signed-off-by: Andrej Valek <andrej.valek@siemens.com>
> Signed-off-by: Peter Marko <peter.marko@siemens.com>
> ---
>  meta/classes/cve-check.bbclass | 89 +++++++++++++++++++++++++++++-----
>  meta/lib/oe/cve_check.py       |  6 +++
>  2 files changed, 83 insertions(+), 12 deletions(-)

I'm afraid I really don't like this :(. Why?:

* we now have three different pieces of information, "status", "detail"
and "description" when we should only need two

* this needs a group mapping mechanism which is confusing above

* the information is spread over multiple differently named variables

* two pieces of the status information are connected in a hardcoded way

As a counter proposal, consider:

CVE_STATUS[CVE-1234-0001] = "not-applicable-platform: Issue only applies on Windows"
CVE_STATUS[CVE-1234-0002] = "not-applicable-platform: Issue only applies on Windows"
CVE_STATUS[CVE-1234-0003] = "fixed-version: Fixed externally"
CVE_STATUS[CVE-1234-0004] = "fixed-version: Fixed externally"

CVE_CHECK_STATUSMAP[not-applicable-platform] = "Ignored"
CVE_CHECK_STATUSMAP[fixed-version] = "Patched"

which conveys the same information with a slight bit of copy/paste but
not at a level I'd lose sleep over. To me it is a lot more readable.

Thoughts?

Cheers,

Richard
diff mbox series

Patch

diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
index bd9e7e7445..62676ba5bc 100644
--- a/meta/classes/cve-check.bbclass
+++ b/meta/classes/cve-check.bbclass
@@ -70,12 +70,16 @@  CVE_CHECK_COVERAGE ??= "1"
 # Skip CVE Check for packages (PN)
 CVE_CHECK_SKIP_RECIPE ?= ""
 
-# Ingore the check for a given list of CVEs. If a CVE is found,
-# then it is considered patched. The value is a string containing
-# space separated CVE values:
+# Replace NVD DB check status for a given CVE. Each of CVE has to be mentioned
+# separately with optional detail and description for this status.
 #
-# CVE_CHECK_IGNORE = 'CVE-2014-2524 CVE-2018-1234'
+# CVE_STATUS[CVE-1234-0001] = "Ignored" # or "Patched" or "Unpatched"
+# CVE_STATUS[CVE-1234-0002] = "Ignored"
+# CVE_STATUS_DETAIL[CVE-1234-0002] = "not-applicable-platform"
+# CVE_STATUS_DESCRIPTION[CVE-1234-0002] = "Issue only applies on Windows"
 #
+# CVE_CHECK_IGNORE is deprecated and CVE_STATUS has to be used instead.
+# Keep CVE_CHECK_IGNORE until other layers migrate to new variables
 CVE_CHECK_IGNORE ?= ""
 
 # Layers to be excluded
@@ -88,6 +92,47 @@  CVE_CHECK_LAYER_INCLUDELIST ??= ""
 # set to "alphabetical" for version using single alphabetical character as increment release
 CVE_VERSION_SUFFIX ??= ""
 
+python () {
+    # Fallback all CVEs from CVE_CHECK_IGNORE to CVE_STATUS
+    cve_check_ignore = d.getVar("CVE_CHECK_IGNORE")
+    if cve_check_ignore:
+        bb.warn("CVE_CHECK_IGNORE is deprecated in favor of CVE_STATUS")
+        set_cves_statuses(d, d.getVar("CVE_CHECK_IGNORE"), "Ignored")
+
+    # Process CVE_STATUS_GROUPS to set multiple statuses and optional detail or description at once
+    for cve_status_group in (d.getVar("CVE_STATUS_GROUPS") or "").split():
+        cve_group = d.getVar(cve_status_group)
+        if cve_group is not None:
+            set_cves_statuses(d, cve_group,
+                              d.getVarFlag(cve_status_group, "status"),
+                              d.getVarFlag(cve_status_group, "detail"),
+                              d.getVarFlag(cve_status_group, "description"))
+        else:
+            bb.warn("CVE_STATUS_GROUPS contains undefined variable %s" % cve_status_group)
+}
+
+def set_cves_statuses(d, cves, status, detail="", description=""):
+    for cve in cves.split():
+        d.setVarFlag("CVE_STATUS", cve, status)
+        d.setVarFlag("CVE_STATUS_DETAIL", cve, detail)
+        d.setVarFlag("CVE_STATUS_DESCRIPTION", cve, description)
+
+def get_cve_detail(d, cve, status):
+    detail = d.getVarFlag("CVE_STATUS_DETAIL", cve)
+    if detail is not None:
+        if status == "Patched":
+            if detail in ["fixed-version", "backported-patch", "cpe-stable-backport", "other"]:
+                return detail
+        elif status == "Unpatched":
+            if detail in ["vulnerable-investigating", "other"]:
+                return detail
+        else:
+            if detail in ["cpe-incorrect", "not-applicable-platform", "upstream-wontfix",
+                        "not-applicable-config", "not-affected", "other"]:
+                return detail
+        bb.warn('Invalid detail %s for CVE_STATUS[%s] = "%s"' % (detail, cve, status))
+    return ""
+
 def generate_json_report(d, out_path, link_path):
     if os.path.exists(d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")):
         import json
@@ -282,7 +327,13 @@  def check_cves(d, patched_cves):
         bb.note("Recipe has been skipped by cve-check")
         return ([], [], [], [])
 
-    cve_ignore = d.getVar("CVE_CHECK_IGNORE").split()
+    # Convert CVE_STATUS into ignored CVEs and check validity
+    cve_ignore = []
+    for cve, status in (d.getVarFlags("CVE_STATUS") or {}).items():
+        if status == "Ignored":
+            cve_ignore.append(cve)
+        elif status not in ["Patched", "Unpatched"]:
+            bb.error("Unsupported status %s in CVE_STATUS[%s]" % (status, cve))
 
     import sqlite3
     db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro")
@@ -441,20 +492,28 @@  def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
         is_patched = cve in patched
         is_ignored = cve in ignored
 
+        status = "Unpatched"
         if (is_patched or is_ignored) and not report_all:
             continue
+        if is_ignored:
+            status = "Ignored"
+        elif is_patched:
+            status = "Patched"
+        else:
+            # default value of status is Unpatched
+            unpatched_cves.append(cve)
 
         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"))
         write_string += "CVE: %s\n" % cve
-        if is_ignored:
-            write_string += "CVE STATUS: Ignored\n"
-        elif is_patched:
-            write_string += "CVE STATUS: Patched\n"
-        else:
-            unpatched_cves.append(cve)
-            write_string += "CVE STATUS: Unpatched\n"
+        write_string += "CVE STATUS: %s\n" % status
+        detail = get_cve_detail(d, cve, status)
+        if detail:
+            write_string += "CVE DETAIL: %s\n" % detail
+        description = d.getVarFlag("CVE_STATUS_DESCRIPTION", cve)
+        if description:
+            write_string += "CVE DESCRIPTION: %s\n" % description
         write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["summary"]
         write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["scorev2"]
         write_string += "CVSS v3 BASE SCORE: %s\n" % cve_data[cve]["scorev3"]
@@ -576,6 +635,12 @@  def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
             "status" : status,
             "link": issue_link
         }
+        detail = get_cve_detail(d, cve, status)
+        if detail:
+            cve_item["detail"] = detail
+        description = d.getVarFlag("CVE_STATUS_DESCRIPTION", cve)
+        if description:
+            cve_item["description"] = description
         cve_list.append(cve_item)
 
     package_data["issue"] = cve_list
diff --git a/meta/lib/oe/cve_check.py b/meta/lib/oe/cve_check.py
index dbaa0b373a..f47dd9920e 100644
--- a/meta/lib/oe/cve_check.py
+++ b/meta/lib/oe/cve_check.py
@@ -130,6 +130,12 @@  def get_patched_cves(d):
         if not fname_match and not text_match:
             bb.debug(2, "Patch %s doesn't solve CVEs" % patch_file)
 
+    # Search for additional patched CVEs
+    for cve, status in (d.getVarFlags("CVE_STATUS") or {}).items():
+        if status == "Patched":
+            bb.debug(2, "CVE %s is additionally patched" % cve)
+            patched_cves.add(cve)
+
     return patched_cves