diff mbox series

cve-check: Classify patched CVEs into 3 statuses

Message ID 20230921090316.10932-1-shin.matsunaga@fujitsu.com
State New
Headers show
Series cve-check: Classify patched CVEs into 3 statuses | expand

Commit Message

Shinji Matsunaga Sept. 21, 2023, 9:03 a.m. UTC
CVEs that are currently considered "Patched" are classified into the following 3 statuses:
1. "Patched"      - means that a patch file that fixed the vulnerability has been applied
2. "Out of range" - means that the package version (PV) is not subject to the vulnerability
3. "Undecidable"  - means that versions cannot be compared to determine if they are affected by the vulnerability

Signed-off-by: Shinji Matsunaga <shin.matsunaga@fujitsu.com>
---
 meta/classes/cve-check.bbclass | 55 +++++++++++++++++++++++-----------
 1 file changed, 38 insertions(+), 17 deletions(-)

Comments

Peter Marko Sept. 21, 2023, 9:45 a.m. UTC | #1
We have recently introduced CVE_CHECK_STATUSMAP which should be used to declare more detailed status information instead of introducing additional statuses.
In this case, "out of range" should be subtype of patched and "undecidable" subtype of unpatched I think.

Peter

-----Original Message-----
From: openembedded-core@lists.openembedded.org <openembedded-core@lists.openembedded.org> On Behalf Of Matsunaga-Shinji via lists.openembedded.org
Sent: Thursday, September 21, 2023 11:03
To: richard.purdie@linuxfoundation.org
Cc: openembedded-core@lists.openembedded.org; shin.matsunaga@fujitsu.com
Subject: [OE-core] [PATCH] cve-check: Classify patched CVEs into 3 statuses

> CVEs that are currently considered "Patched" are classified into the following 3 statuses:
> 1. "Patched"      - means that a patch file that fixed the vulnerability has been applied
> 2. "Out of range" - means that the package version (PV) is not subject to the vulnerability
> 3. "Undecidable"  - means that versions cannot be compared to determine if they are affected by the vulnerability
> 
> Signed-off-by: Shinji Matsunaga <shin.matsunaga@fujitsu.com>
Shinji Matsunaga Oct. 4, 2023, 2:19 a.m. UTC | #2
Sorry for the late reply.

In addition to the changes to meta/classes/cve-check.bbclass,
Does it mean that the following processing needs to be added to meta/conf/cve-check-map.conf?
CVE_CHECK_STATUSMAP[out-of-range] = "Patched"
CVE_CHECK_STATUSMAP[undecidable] = "Unpatched"

Shinji

-----Original Message-----
From: Marko, Peter <Peter.Marko@siemens.com> 
Sent: Thursday, September 21, 2023 6:46 PM
To: Matsunaga, Shinji/松永 慎司 <shin.matsunaga@fujitsu.com>; richard.purdie@linuxfoundation.org
Cc: openembedded-core@lists.openembedded.org
Subject: RE: [OE-core] [PATCH] cve-check: Classify patched CVEs into 3 statuses

We have recently introduced CVE_CHECK_STATUSMAP which should be used to declare more detailed status information instead of introducing additional statuses.
In this case, "out of range" should be subtype of patched and "undecidable" subtype of unpatched I think.

Peter

-----Original Message-----
From: openembedded-core@lists.openembedded.org <openembedded-core@lists.openembedded.org> On Behalf Of Matsunaga-Shinji via lists.openembedded.org
Sent: Thursday, September 21, 2023 11:03
To: richard.purdie@linuxfoundation.org
Cc: openembedded-core@lists.openembedded.org; shin.matsunaga@fujitsu.com
Subject: [OE-core] [PATCH] cve-check: Classify patched CVEs into 3 statuses

> CVEs that are currently considered "Patched" are classified into the following 3 statuses:
> 1. "Patched"      - means that a patch file that fixed the vulnerability has been applied
> 2. "Out of range" - means that the package version (PV) is not subject 
> to the vulnerability 3. "Undecidable"  - means that versions cannot be 
> compared to determine if they are affected by the vulnerability
> 
> Signed-off-by: Shinji Matsunaga <shin.matsunaga@fujitsu.com>
Marta Rybczynska Oct. 4, 2023, 3:59 a.m. UTC | #3
On Thu, 21 Sept 2023, 11:03 Matsunaga-Shinji, <shin.matsunaga@fujitsu.com>
wrote:

> CVEs that are currently considered "Patched" are classified into the
> following 3 statuses:
> 1. "Patched"      - means that a patch file that fixed the vulnerability
> has been applied
> 2. "Out of range" - means that the package version (PV) is not subject to
> the vulnerability
> 3. "Undecidable"  - means that versions cannot be compared to determine if
> they are affected by the vulnerability


Hello,
Thank you for your patch. I'm wondering what you use case is. What do you
do with that data? Currently in YP we aim to do as much as automatic
classification as we can. We only adjust the classification manually when
it is clearly wrong.

Now, in this piece of code I don't see setting up 'out-of-range', while it
is possible to separate the not affected case and the case when we apply a
patch. I do not understand the 'undecideable' classification. Could you
give an exemple of a situation when it makes sense to use it?

On the naming side, I'd prefer 'Not Affected' for out-of-range, because
that term is often used in error conditions. In this case there is no error
at all.

Kind regards,
Marta



>
Peter Marko Oct. 4, 2023, 10:45 a.m. UTC | #4
Yes, that's how we designed this feature.
Peter

-----Original Message-----
From: Shinji Matsunaga (Fujitsu) <shin.matsunaga@fujitsu.com> 
Sent: Wednesday, October 4, 2023 4:19
To: Marko, Peter (ADV D EU SK BFS1) <Peter.Marko@siemens.com>; richard.purdie@linuxfoundation.org
Cc: openembedded-core@lists.openembedded.org
Subject: RE: [OE-core] [PATCH] cve-check: Classify patched CVEs into 3 statuses

> Sorry for the late reply.
>
> In addition to the changes to meta/classes/cve-check.bbclass, Does it mean that the following processing needs to be added to meta/conf/cve-check-map.conf?
> CVE_CHECK_STATUSMAP[out-of-range] = "Patched"
> CVE_CHECK_STATUSMAP[undecidable] = "Unpatched"
>
> Shinji
>
> -----Original Message-----
> From: Marko, Peter <Peter.Marko@siemens.com>
> Sent: Thursday, September 21, 2023 6:46 PM
> To: Matsunaga, Shinji/松永 慎司 <shin.matsunaga@fujitsu.com>; richard.purdie@linuxfoundation.org
> Cc: openembedded-core@lists.openembedded.org
> Subject: RE: [OE-core] [PATCH] cve-check: Classify patched CVEs into 3 statuses
>
> We have recently introduced CVE_CHECK_STATUSMAP which should be used to declare more detailed status information instead of introducing additional statuses.
> In this case, "out of range" should be subtype of patched and "undecidable" subtype of unpatched I think.
>
> Peter
>
> -----Original Message-----
> From: openembedded-core@lists.openembedded.org <openembedded-core@lists.openembedded.org> On Behalf Of Matsunaga-Shinji via lists.openembedded.org
> Sent: Thursday, September 21, 2023 11:03
> To: richard.purdie@linuxfoundation.org
> Cc: openembedded-core@lists.openembedded.org; shin.matsunaga@fujitsu.com
> Subject: [OE-core] [PATCH] cve-check: Classify patched CVEs into 3 statuses
>
> > CVEs that are currently considered "Patched" are classified into the following 3 statuses:
> > 1. "Patched"      - means that a patch file that fixed the vulnerability has been applied
> > 2. "Out of range" - means that the package version (PV) is not subject 
> > to the vulnerability 3. "Undecidable"  - means that versions cannot be 
> > compared to determine if they are affected by the vulnerability
> > 
> > Signed-off-by: Shinji Matsunaga <shin.matsunaga@fujitsu.com>
Shinji Matsunaga Oct. 12, 2023, 7:46 a.m. UTC | #5
Sorry for the late reply.

We want to use "Patched" to make it easy to find the package where the patch file exists
and use "Out of range" to make it easy to find the package where not affected by the vulnerability.

For that, we consider we need to classify vulnerabilities to "Undecidable" that cannot determine if they are affected by the vulnerability, too.

As described above, "Out of range" means that the not affected case.
As you pointed out, We will change the name to "Not affected."

"Undecidable" is the status when "Failed to compare ..." is output.
(see cve-check.bbclass:check_cves())

Here is an example of "Undecidable".
PACKAGE NAME: synergy
PACKAGE VERSION: git
CVE: CVE-2020-15117

The log is as follows:
"WARNING: synergy: Failed to compare git < 1.12.0 for CVE-2020-15117"


Shinji

From: Marta Rybczynska <rybczynska@gmail.com>
Sent: Wednesday, October 4, 2023 12:59 PM
To: Matsunaga, Shinji/松永 慎司 <shin.matsunaga@fujitsu.com>
Cc: Richard Purdie <richard.purdie@linuxfoundation.org>; OE-core <openembedded-core@lists.openembedded.org>
Subject: Re: [OE-core] [PATCH] cve-check: Classify patched CVEs into 3 statuses


On Thu, 21 Sept 2023, 11:03 Matsunaga-Shinji, <shin.matsunaga@fujitsu.com<mailto:shin.matsunaga@fujitsu.com>> wrote:
CVEs that are currently considered "Patched" are classified into the following 3 statuses:
1. "Patched"      - means that a patch file that fixed the vulnerability has been applied
2. "Out of range" - means that the package version (PV) is not subject to the vulnerability
3. "Undecidable"  - means that versions cannot be compared to determine if they are affected by the vulnerability

Hello,
Thank you for your patch. I'm wondering what you use case is. What do you do with that data? Currently in YP we aim to do as much as automatic classification as we can. We only adjust the classification manually when it is clearly wrong.

Now, in this piece of code I don't see setting up 'out-of-range', while it is possible to separate the not affected case and the case when we apply a patch. I do not understand the 'undecideable' classification. Could you give an exemple of a situation when it makes sense to use it?

On the naming side, I'd prefer 'Not Affected' for out-of-range, because that term is often used in error conditions. In this case there is no error at all.

Kind regards,
Marta
diff mbox series

Patch

diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
index 55ae298024..a2456902ce 100644
--- a/meta/classes/cve-check.bbclass
+++ b/meta/classes/cve-check.bbclass
@@ -185,10 +185,10 @@  python do_cve_check () {
                 patched_cves = get_patched_cves(d)
             except FileNotFoundError:
                 bb.fatal("Failure in searching patches")
-            ignored, patched, unpatched, status = check_cves(d, patched_cves)
-            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)
+            ignored, patched, unpatched, out_of_range, undecidable, status = check_cves(d, patched_cves)
+            if patched or unpatched or out_of_range or undecidable or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
+                cve_data = get_cve_info(d, patched + unpatched + ignored + out_of_range + undecidable)
+                cve_write_data(d, patched, unpatched, ignored, out_of_range, undecidable, cve_data, status)
         else:
             bb.note("No CVE database found, skipping CVE check")
 
@@ -308,13 +308,13 @@  def check_cves(d, patched_cves):
     products = d.getVar("CVE_PRODUCT").split()
     # If this has been unset then we're not scanning for CVEs here (for example, image recipes)
     if not products:
-        return ([], [], [], [])
+        return ([], [], [], [], [], [])
     pv = d.getVar("CVE_VERSION").split("+git")[0]
 
     # If the recipe has been skipped/ignored we return empty lists
     if pn in d.getVar("CVE_CHECK_SKIP_RECIPE").split():
         bb.note("Recipe has been skipped by cve-check")
-        return ([], [], [], [])
+        return ([], [], [], [], [], [])
 
     # Convert CVE_STATUS into ignored CVEs and check validity
     cve_ignore = []
@@ -328,6 +328,8 @@  def check_cves(d, patched_cves):
     conn = sqlite3.connect(db_file, uri=True)
 
     # For each of the known product names (e.g. curl has CPEs using curl and libcurl)...
+    cves_out_of_range = []
+    cves_undecidable = []
     for product in products:
         cves_in_product = False
         if ":" in product:
@@ -355,6 +357,7 @@  def check_cves(d, patched_cves):
 
             vulnerable = False
             ignored = False
+            undecidable = False
 
             product_cursor = conn.execute("SELECT * FROM PRODUCTS WHERE ID IS ? AND PRODUCT IS ? AND VENDOR LIKE ?", (cve, product, vendor))
             for row in product_cursor:
@@ -376,7 +379,7 @@  def check_cves(d, patched_cves):
                         except:
                             bb.warn("%s: Failed to compare %s %s %s for %s" %
                                     (product, pv, operator_start, version_start, cve))
-                            vulnerable_start = False
+                            undecidable = True
                     else:
                         vulnerable_start = False
 
@@ -387,10 +390,15 @@  def check_cves(d, patched_cves):
                         except:
                             bb.warn("%s: Failed to compare %s %s %s for %s" %
                                     (product, pv, operator_end, version_end, cve))
-                            vulnerable_end = False
+                            undecidable = True
                     else:
                         vulnerable_end = False
 
+                    if undecidable:
+                        bb.note("%s-%s is undecidable to %s" % (pn, real_pv, cve))
+                        cves_undecidable.append(cve)
+                        break
+
                     if operator_start and operator_end:
                         vulnerable = vulnerable_start and vulnerable_end
                     else:
@@ -406,9 +414,9 @@  def check_cves(d, patched_cves):
                     break
             product_cursor.close()
 
-            if not vulnerable:
+            if not undecidable and not vulnerable:
                 bb.note("%s-%s is not vulnerable to %s" % (pn, real_pv, cve))
-                patched_cves.add(cve)
+                cves_out_of_range.append(cve)
         cve_cursor.close()
 
         if not cves_in_product:
@@ -420,7 +428,7 @@  def check_cves(d, patched_cves):
     if not cves_in_recipe:
         bb.note("No CVE records for products in recipe %s" % (pn))
 
-    return (list(cves_ignored), list(patched_cves), cves_unpatched, cves_status)
+    return (list(cves_ignored), list(patched_cves), cves_unpatched, cves_out_of_range, cves_undecidable, cves_status)
 
 def get_cve_info(d, cves):
     """
@@ -446,7 +454,7 @@  def get_cve_info(d, cves):
     conn.close()
     return cve_data
 
-def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
+def cve_write_data_text(d, patched, unpatched, ignored, out_of_range, undecidable, cve_data):
     """
     Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and
     CVE manifest if enabled.
@@ -470,7 +478,7 @@  def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
         return
 
     # Early exit, the text format does not report packages without CVEs
-    if not patched+unpatched+ignored:
+    if not patched+unpatched+ignored+out_of_range+undecidable:
         return
 
     nvd_link = "https://nvd.nist.gov/vuln/detail/"
@@ -481,6 +489,8 @@  def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
     for cve in sorted(cve_data):
         is_patched = cve in patched
         is_ignored = cve in ignored
+        is_out_of_range = cve in out_of_range
+        is_undecidable = cve in undecidable
 
         status = "Unpatched"
         if (is_patched or is_ignored) and not report_all:
@@ -489,6 +499,10 @@  def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
             status = "Ignored"
         elif is_patched:
             status = "Patched"
+        elif is_out_of_range:
+            status = "Out of range"
+        elif is_undecidable:
+            status = "Undecidable"
         else:
             # default value of status is Unpatched
             unpatched_cves.append(cve)
@@ -559,7 +573,7 @@  def cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_fi
         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):
+def cve_write_data_json(d, patched, unpatched, ignored, out_of_range, undecidable, cve_data, cve_status):
     """
     Prepare CVE data for the JSON format, then write it.
     """
@@ -604,6 +618,9 @@  def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
     for cve in sorted(cve_data):
         is_patched = cve in patched
         is_ignored = cve in ignored
+        is_out_of_range = cve in out_of_range
+        is_undecidable = cve in undecidable
+
         status = "Unpatched"
         if (is_patched or is_ignored) and not report_all:
             continue
@@ -611,6 +628,10 @@  def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
             status = "Ignored"
         elif is_patched:
             status = "Patched"
+        elif is_out_of_range:
+            status = "Out of range"
+        elif is_undecidable:
+            status = "Undecidable"
         else:
             # default value of status is Unpatched
             unpatched_cves.append(cve)
@@ -642,12 +663,12 @@  def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
 
     cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file)
 
-def cve_write_data(d, patched, unpatched, ignored, cve_data, status):
+def cve_write_data(d, patched, unpatched, ignored, out_of_range, undecidable, cve_data, status):
     """
     Write CVE data in each enabled format.
     """
 
     if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1":
-        cve_write_data_text(d, patched, unpatched, ignored, cve_data)
+        cve_write_data_text(d, patched, unpatched, ignored, out_of_range, undecidable, cve_data)
     if d.getVar("CVE_CHECK_FORMAT_JSON") == "1":
-        cve_write_data_json(d, patched, unpatched, ignored, cve_data, status)
+        cve_write_data_json(d, patched, unpatched, ignored, out_of_range, undecidable, cve_data, status)