diff mbox series

[v2] cve-check: Classify patched CVEs into 3 statuses

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

Commit Message

Shinji Matsunaga Oct. 25, 2023, 5:13 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. "Not affected" - means that the package version (PV) is not affected by 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>
Signed-off-by: Shunsuke Tokumoto <s-tokumoto@fujitsu.com>
---

Changes for v2:
   - Fix the status "Out of range" to "Not affected"

 meta/classes/cve-check.bbclass | 55 +++++++++++++++++++++++-----------
 1 file changed, 38 insertions(+), 17 deletions(-)

Comments

Andrej Valek Oct. 25, 2023, 6:44 a.m. UTC | #1
Hi all,

Do we really need a new "not_affected" state? I guess the ignore state 
is exactly designed for those purposes.

Regards,
Andrej

On 25.10.2023 07:13, Matsunaga-Shinji 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. "Not affected" - means that the package version (PV) is not affected by 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>
> Signed-off-by: Shunsuke Tokumoto <s-tokumoto@fujitsu.com>
> ---
>
> Changes for v2:
>     - Fix the status "Out of range" to "Not affected"
>
>   meta/classes/cve-check.bbclass | 55 +++++++++++++++++++++++-----------
>   1 file changed, 38 insertions(+), 17 deletions(-)
>
> diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
> index b55f4299da..502db324df 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, not_affected, undecidable, status = check_cves(d, patched_cves)
> +            if patched or unpatched or not_affected or undecidable or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
> +                cve_data = get_cve_info(d, patched + unpatched + ignored + not_affected + undecidable)
> +                cve_write_data(d, patched, unpatched, ignored, not_affected, 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_not_affected = []
> +    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_not_affected.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_not_affected, cves_undecidable, cves_status)
>   
>   def get_cve_info(d, cves):
>       """
> @@ -447,7 +455,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, not_affected, undecidable, cve_data):
>       """
>       Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and
>       CVE manifest if enabled.
> @@ -471,7 +479,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+not_affected+undecidable:
>           return
>   
>       nvd_link = "https://nvd.nist.gov/vuln/detail/"
> @@ -482,6 +490,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_not_affected = cve in not_affected
> +        is_undecidable = cve in undecidable
>   
>           status = "Unpatched"
>           if (is_patched or is_ignored) and not report_all:
> @@ -490,6 +500,10 @@ def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
>               status = "Ignored"
>           elif is_patched:
>               status = "Patched"
> +        elif is_not_affected:
> +            status = "Not affected"
> +        elif is_undecidable:
> +            status = "Undecidable"
>           else:
>               # default value of status is Unpatched
>               unpatched_cves.append(cve)
> @@ -561,7 +575,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, not_affected, undecidable, cve_data, cve_status):
>       """
>       Prepare CVE data for the JSON format, then write it.
>       """
> @@ -606,6 +620,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_not_affected = cve in not_affected
> +        is_undecidable = cve in undecidable
> +
>           status = "Unpatched"
>           if (is_patched or is_ignored) and not report_all:
>               continue
> @@ -613,6 +630,10 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
>               status = "Ignored"
>           elif is_patched:
>               status = "Patched"
> +        elif is_not_affected:
> +            status = "Not affected"
> +        elif is_undecidable:
> +            status = "Undecidable"
>           else:
>               # default value of status is Unpatched
>               unpatched_cves.append(cve)
> @@ -645,12 +666,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, not_affected, 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, not_affected, 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, not_affected, undecidable, cve_data, status)
>
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#189667): https://lists.openembedded.org/g/openembedded-core/message/189667
> Mute This Topic: https://lists.openembedded.org/mt/102172913/3619876
> Group Owner: openembedded-core+owner@lists.openembedded.org
> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [andrej.v@skyrain.eu]
> -=-=-=-=-=-=-=-=-=-=-=-
>
Marta Rybczynska Oct. 25, 2023, 9:33 a.m. UTC | #2
Hi Andrej,
This is more complex. "Not affected" is also an issue that isn't present in the
code - like when we have a version that has never had the vulnerability.
Those are also currently 'Patched' in cve-check.

This work is in sync with what VEX is doing, is it the use-case
Matsanaga-Shinji?

Regards,
Marta

On Wed, Oct 25, 2023 at 8:44 AM Andrej Valek <andrej.v@skyrain.eu> wrote:
>
> Hi all,
>
> Do we really need a new "not_affected" state? I guess the ignore state
> is exactly designed for those purposes.
>
> Regards,
> Andrej
>
> On 25.10.2023 07:13, Matsunaga-Shinji 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. "Not affected" - means that the package version (PV) is not affected by 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>
> > Signed-off-by: Shunsuke Tokumoto <s-tokumoto@fujitsu.com>
> > ---
> >
> > Changes for v2:
> >     - Fix the status "Out of range" to "Not affected"
> >
> >   meta/classes/cve-check.bbclass | 55 +++++++++++++++++++++++-----------
> >   1 file changed, 38 insertions(+), 17 deletions(-)
> >
> > diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
> > index b55f4299da..502db324df 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, not_affected, undecidable, status = check_cves(d, patched_cves)
> > +            if patched or unpatched or not_affected or undecidable or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
> > +                cve_data = get_cve_info(d, patched + unpatched + ignored + not_affected + undecidable)
> > +                cve_write_data(d, patched, unpatched, ignored, not_affected, 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_not_affected = []
> > +    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_not_affected.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_not_affected, cves_undecidable, cves_status)
> >
> >   def get_cve_info(d, cves):
> >       """
> > @@ -447,7 +455,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, not_affected, undecidable, cve_data):
> >       """
> >       Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and
> >       CVE manifest if enabled.
> > @@ -471,7 +479,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+not_affected+undecidable:
> >           return
> >
> >       nvd_link = "https://nvd.nist.gov/vuln/detail/"
> > @@ -482,6 +490,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_not_affected = cve in not_affected
> > +        is_undecidable = cve in undecidable
> >
> >           status = "Unpatched"
> >           if (is_patched or is_ignored) and not report_all:
> > @@ -490,6 +500,10 @@ def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
> >               status = "Ignored"
> >           elif is_patched:
> >               status = "Patched"
> > +        elif is_not_affected:
> > +            status = "Not affected"
> > +        elif is_undecidable:
> > +            status = "Undecidable"
> >           else:
> >               # default value of status is Unpatched
> >               unpatched_cves.append(cve)
> > @@ -561,7 +575,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, not_affected, undecidable, cve_data, cve_status):
> >       """
> >       Prepare CVE data for the JSON format, then write it.
> >       """
> > @@ -606,6 +620,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_not_affected = cve in not_affected
> > +        is_undecidable = cve in undecidable
> > +
> >           status = "Unpatched"
> >           if (is_patched or is_ignored) and not report_all:
> >               continue
> > @@ -613,6 +630,10 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
> >               status = "Ignored"
> >           elif is_patched:
> >               status = "Patched"
> > +        elif is_not_affected:
> > +            status = "Not affected"
> > +        elif is_undecidable:
> > +            status = "Undecidable"
> >           else:
> >               # default value of status is Unpatched
> >               unpatched_cves.append(cve)
> > @@ -645,12 +666,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, not_affected, 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, not_affected, 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, not_affected, undecidable, cve_data, status)
> >
> > -=-=-=-=-=-=-=-=-=-=-=-
> > Links: You receive all messages sent to this group.
> > View/Reply Online (#189667): https://lists.openembedded.org/g/openembedded-core/message/189667
> > Mute This Topic: https://lists.openembedded.org/mt/102172913/3619876
> > Group Owner: openembedded-core+owner@lists.openembedded.org
> > Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [andrej.v@skyrain.eu]
> > -=-=-=-=-=-=-=-=-=-=-=-
> >
>
Andrej Valek Oct. 25, 2023, 10:18 a.m. UTC | #3
Hi Marta,

That's fine, as I said we designed the "ignore" with status 
"cpe-incorrect" or "ignored" exactly for those purposes. Extending the 
option with "not affected" doesn't make any sense.

You have to set the status to "why is not affected" = "ignored". Which 
completely covers the requested case.

Regards,
Andrej

On 25.10.2023 11:33, Marta Rybczynska wrote:
> Hi Andrej,
> This is more complex. "Not affected" is also an issue that isn't present in the
> code - like when we have a version that has never had the vulnerability.
> Those are also currently 'Patched' in cve-check.
>
> This work is in sync with what VEX is doing, is it the use-case
> Matsanaga-Shinji?
>
> Regards,
> Marta
>
> On Wed, Oct 25, 2023 at 8:44 AM Andrej Valek <andrej.v@skyrain.eu> wrote:
>> Hi all,
>>
>> Do we really need a new "not_affected" state? I guess the ignore state
>> is exactly designed for those purposes.
>>
>> Regards,
>> Andrej
>>
>> On 25.10.2023 07:13, Matsunaga-Shinji 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. "Not affected" - means that the package version (PV) is not affected by 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>
>>> Signed-off-by: Shunsuke Tokumoto <s-tokumoto@fujitsu.com>
>>> ---
>>>
>>> Changes for v2:
>>>      - Fix the status "Out of range" to "Not affected"
>>>
>>>    meta/classes/cve-check.bbclass | 55 +++++++++++++++++++++++-----------
>>>    1 file changed, 38 insertions(+), 17 deletions(-)
>>>
>>> diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
>>> index b55f4299da..502db324df 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, not_affected, undecidable, status = check_cves(d, patched_cves)
>>> +            if patched or unpatched or not_affected or undecidable or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
>>> +                cve_data = get_cve_info(d, patched + unpatched + ignored + not_affected + undecidable)
>>> +                cve_write_data(d, patched, unpatched, ignored, not_affected, 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_not_affected = []
>>> +    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_not_affected.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_not_affected, cves_undecidable, cves_status)
>>>
>>>    def get_cve_info(d, cves):
>>>        """
>>> @@ -447,7 +455,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, not_affected, undecidable, cve_data):
>>>        """
>>>        Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and
>>>        CVE manifest if enabled.
>>> @@ -471,7 +479,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+not_affected+undecidable:
>>>            return
>>>
>>>        nvd_link = "https://nvd.nist.gov/vuln/detail/"
>>> @@ -482,6 +490,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_not_affected = cve in not_affected
>>> +        is_undecidable = cve in undecidable
>>>
>>>            status = "Unpatched"
>>>            if (is_patched or is_ignored) and not report_all:
>>> @@ -490,6 +500,10 @@ def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
>>>                status = "Ignored"
>>>            elif is_patched:
>>>                status = "Patched"
>>> +        elif is_not_affected:
>>> +            status = "Not affected"
>>> +        elif is_undecidable:
>>> +            status = "Undecidable"
>>>            else:
>>>                # default value of status is Unpatched
>>>                unpatched_cves.append(cve)
>>> @@ -561,7 +575,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, not_affected, undecidable, cve_data, cve_status):
>>>        """
>>>        Prepare CVE data for the JSON format, then write it.
>>>        """
>>> @@ -606,6 +620,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_not_affected = cve in not_affected
>>> +        is_undecidable = cve in undecidable
>>> +
>>>            status = "Unpatched"
>>>            if (is_patched or is_ignored) and not report_all:
>>>                continue
>>> @@ -613,6 +630,10 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
>>>                status = "Ignored"
>>>            elif is_patched:
>>>                status = "Patched"
>>> +        elif is_not_affected:
>>> +            status = "Not affected"
>>> +        elif is_undecidable:
>>> +            status = "Undecidable"
>>>            else:
>>>                # default value of status is Unpatched
>>>                unpatched_cves.append(cve)
>>> @@ -645,12 +666,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, not_affected, 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, not_affected, 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, not_affected, undecidable, cve_data, status)
>>>
>>> -=-=-=-=-=-=-=-=-=-=-=-
>>> Links: You receive all messages sent to this group.
>>> View/Reply Online (#189667): https://lists.openembedded.org/g/openembedded-core/message/189667
>>> Mute This Topic: https://lists.openembedded.org/mt/102172913/3619876
>>> Group Owner: openembedded-core+owner@lists.openembedded.org
>>> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [andrej.v@skyrain.eu]
>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>
Marta Rybczynska Oct. 25, 2023, 12:43 p.m. UTC | #4
Hello Andrej,
This patch is splitting the Patched state, not the ignore one. This is not
incorrect CPE or anything else.

Currently Patched means one of two situations: either this issue has never
affected the code base (example: we have version 1.0, issue was introduced
in 2.0 and fixed in 2.1), or the issue has been fixed.

Yes, another reason to say ignore, not affected is a manual analysis
showing a thing like: the issue affects only windows.

Regards,
Marta


On Wed, 25 Oct 2023, 12:18 Andrej Valek, <andrej.v@skyrain.eu> wrote:

> Hi Marta,
>
> That's fine, as I said we designed the "ignore" with status
> "cpe-incorrect" or "ignored" exactly for those purposes. Extending the
> option with "not affected" doesn't make any sense.
>
> You have to set the status to "why is not affected" = "ignored". Which
> completely covers the requested case.
>
> Regards,
> Andrej
>
> On 25.10.2023 11:33, Marta Rybczynska wrote:
> > Hi Andrej,
> > This is more complex. "Not affected" is also an issue that isn't present
> in the
> > code - like when we have a version that has never had the vulnerability.
> > Those are also currently 'Patched' in cve-check.
> >
> > This work is in sync with what VEX is doing, is it the use-case
> > Matsanaga-Shinji?
> >
> > Regards,
> > Marta
> >
> > On Wed, Oct 25, 2023 at 8:44 AM Andrej Valek <andrej.v@skyrain.eu>
> wrote:
> >> Hi all,
> >>
> >> Do we really need a new "not_affected" state? I guess the ignore state
> >> is exactly designed for those purposes.
> >>
> >> Regards,
> >> Andrej
> >>
> >> On 25.10.2023 07:13, Matsunaga-Shinji 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. "Not affected" - means that the package version (PV) is not
> affected by 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>
> >>> Signed-off-by: Shunsuke Tokumoto <s-tokumoto@fujitsu.com>
> >>> ---
> >>>
> >>> Changes for v2:
> >>>      - Fix the status "Out of range" to "Not affected"
> >>>
> >>>    meta/classes/cve-check.bbclass | 55
> +++++++++++++++++++++++-----------
> >>>    1 file changed, 38 insertions(+), 17 deletions(-)
> >>>
> >>> diff --git a/meta/classes/cve-check.bbclass
> b/meta/classes/cve-check.bbclass
> >>> index b55f4299da..502db324df 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, not_affected, undecidable,
> status = check_cves(d, patched_cves)
> >>> +            if patched or unpatched or not_affected or undecidable or
> (d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
> >>> +                cve_data = get_cve_info(d, patched + unpatched +
> ignored + not_affected + undecidable)
> >>> +                cve_write_data(d, patched, unpatched, ignored,
> not_affected, 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_not_affected = []
> >>> +    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_not_affected.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_not_affected, cves_undecidable, cves_status)
> >>>
> >>>    def get_cve_info(d, cves):
> >>>        """
> >>> @@ -447,7 +455,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, not_affected,
> undecidable, cve_data):
> >>>        """
> >>>        Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and
> >>>        CVE manifest if enabled.
> >>> @@ -471,7 +479,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+not_affected+undecidable:
> >>>            return
> >>>
> >>>        nvd_link = "https://nvd.nist.gov/vuln/detail/"
> >>> @@ -482,6 +490,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_not_affected = cve in not_affected
> >>> +        is_undecidable = cve in undecidable
> >>>
> >>>            status = "Unpatched"
> >>>            if (is_patched or is_ignored) and not report_all:
> >>> @@ -490,6 +500,10 @@ def cve_write_data_text(d, patched, unpatched,
> ignored, cve_data):
> >>>                status = "Ignored"
> >>>            elif is_patched:
> >>>                status = "Patched"
> >>> +        elif is_not_affected:
> >>> +            status = "Not affected"
> >>> +        elif is_undecidable:
> >>> +            status = "Undecidable"
> >>>            else:
> >>>                # default value of status is Unpatched
> >>>                unpatched_cves.append(cve)
> >>> @@ -561,7 +575,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, not_affected,
> undecidable, cve_data, cve_status):
> >>>        """
> >>>        Prepare CVE data for the JSON format, then write it.
> >>>        """
> >>> @@ -606,6 +620,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_not_affected = cve in not_affected
> >>> +        is_undecidable = cve in undecidable
> >>> +
> >>>            status = "Unpatched"
> >>>            if (is_patched or is_ignored) and not report_all:
> >>>                continue
> >>> @@ -613,6 +630,10 @@ def cve_write_data_json(d, patched, unpatched,
> ignored, cve_data, cve_status):
> >>>                status = "Ignored"
> >>>            elif is_patched:
> >>>                status = "Patched"
> >>> +        elif is_not_affected:
> >>> +            status = "Not affected"
> >>> +        elif is_undecidable:
> >>> +            status = "Undecidable"
> >>>            else:
> >>>                # default value of status is Unpatched
> >>>                unpatched_cves.append(cve)
> >>> @@ -645,12 +666,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, not_affected,
> 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,
> not_affected, 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,
> not_affected, undecidable, cve_data, status)
> >>>
> >>> -=-=-=-=-=-=-=-=-=-=-=-
> >>> Links: You receive all messages sent to this group.
> >>> View/Reply Online (#189667):
> https://lists.openembedded.org/g/openembedded-core/message/189667
> >>> Mute This Topic: https://lists.openembedded.org/mt/102172913/3619876
> >>> Group Owner: openembedded-core+owner@lists.openembedded.org
> >>> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub
> [andrej.v@skyrain.eu]
> >>> -=-=-=-=-=-=-=-=-=-=-=-
> >>>
>
>
Peter Marko Oct. 25, 2023, 1:09 p.m. UTC | #5
Hello Marta,

Major reason why we introduced CVE_STATUS was exactly to avoid patch like this.
There were ideas to introduce 5 or 10 or 15 different statuses and we decided to keep 3 and introduce “sub-statuses”.
These sub-statuses are listed in cve reports, too.

Currently we have three main statuses:
Patched – common status for all sub-statuses which indicate that component is not vulnerable
Unpatched - common status for all sub-statuses which indicate that component is vulnerable
Ignored - common status for all sub-statuses which indicate that component is vulnerable but not in yocto configuration context

If we don’t like “Patched” we can rename it (e.g. to  “Unaffected”) and have additional sub-statuses under this new name.
Otherwise we start exploding the statuses as someone will “need” additional one soon.

If we really want to introduce these new statues (I hope not), please modify this patch to handle its CVE_STATUS flags, too.
Additionally, I’d drop “Undecidable” and map it to “Unpatched” (so someone needs to analyze as any other open vulnerability report)

Best Regards,
  Peter

From: Marta Rybczynska <rybczynska@gmail.com>
Sent: Wednesday, October 25, 2023 14:44
To: Andrej Valek <andrej.v@skyrain.eu>
Cc: Matsunaga-Shinji <shin.matsunaga@fujitsu.com>; Richard Purdie <richard.purdie@linuxfoundation.org>; OE-core <openembedded-core@lists.openembedded.org>; Shunsuke Tokumoto <s-tokumoto@fujitsu.com>; Marko, Peter (ADV D EU SK BFS1) <Peter.Marko@siemens.com>
Subject: Re: [OE-core] [PATCH v2] cve-check: Classify patched CVEs into 3 statuses

Hello Andrej,
This patch is splitting the Patched state, not the ignore one. This is not incorrect CPE or anything else.

Currently Patched means one of two situations: either this issue has never affected the code base (example: we have version 1.0, issue was introduced in 2.0 and fixed in 2.1), or the issue has been fixed.

Yes, another reason to say ignore, not affected is a manual analysis showing a thing like: the issue affects only windows.

Regards,
Marta

On Wed, 25 Oct 2023, 12:18 Andrej Valek, <andrej.v@skyrain.eu<mailto:andrej.v@skyrain.eu>> wrote:
Hi Marta,

That's fine, as I said we designed the "ignore" with status
"cpe-incorrect" or "ignored" exactly for those purposes. Extending the
option with "not affected" doesn't make any sense.

You have to set the status to "why is not affected" = "ignored". Which
completely covers the requested case.

Regards,
Andrej

On 25.10.2023 11:33, Marta Rybczynska wrote:
> Hi Andrej,
> This is more complex. "Not affected" is also an issue that isn't present in the
> code - like when we have a version that has never had the vulnerability.
> Those are also currently 'Patched' in cve-check.
>
> This work is in sync with what VEX is doing, is it the use-case
> Matsanaga-Shinji?
>
> Regards,
> Marta
>
> On Wed, Oct 25, 2023 at 8:44 AM Andrej Valek <andrej.v@skyrain.eu<mailto:andrej.v@skyrain.eu>> wrote:
>> Hi all,
>>
>> Do we really need a new "not_affected" state? I guess the ignore state
>> is exactly designed for those purposes.
>>
>> Regards,
>> Andrej
>>
>> On 25.10.2023 07:13, Matsunaga-Shinji 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. "Not affected" - means that the package version (PV) is not affected by 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<mailto:shin.matsunaga@fujitsu.com>>
>>> Signed-off-by: Shunsuke Tokumoto <s-tokumoto@fujitsu.com<mailto:s-tokumoto@fujitsu.com>>
>>> ---
>>>
>>> Changes for v2:
>>>      - Fix the status "Out of range" to "Not affected"
>>>
>>>    meta/classes/cve-check.bbclass | 55 +++++++++++++++++++++++-----------
>>>    1 file changed, 38 insertions(+), 17 deletions(-)
>>>
>>> diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
>>> index b55f4299da..502db324df 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, not_affected, undecidable, status = check_cves(d, patched_cves)
>>> +            if patched or unpatched or not_affected or undecidable or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
>>> +                cve_data = get_cve_info(d, patched + unpatched + ignored + not_affected + undecidable)
>>> +                cve_write_data(d, patched, unpatched, ignored, not_affected, 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_not_affected = []
>>> +    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_not_affected.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_not_affected, cves_undecidable, cves_status)
>>>
>>>    def get_cve_info(d, cves):
>>>        """
>>> @@ -447,7 +455,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, not_affected, undecidable, cve_data):
>>>        """
>>>        Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and
>>>        CVE manifest if enabled.
>>> @@ -471,7 +479,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+not_affected+undecidable:
>>>            return
>>>
>>>        nvd_link = "https://nvd.nist.gov/vuln/detail/"
>>> @@ -482,6 +490,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_not_affected = cve in not_affected
>>> +        is_undecidable = cve in undecidable
>>>
>>>            status = "Unpatched"
>>>            if (is_patched or is_ignored) and not report_all:
>>> @@ -490,6 +500,10 @@ def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
>>>                status = "Ignored"
>>>            elif is_patched:
>>>                status = "Patched"
>>> +        elif is_not_affected:
>>> +            status = "Not affected"
>>> +        elif is_undecidable:
>>> +            status = "Undecidable"
>>>            else:
>>>                # default value of status is Unpatched
>>>                unpatched_cves.append(cve)
>>> @@ -561,7 +575,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, not_affected, undecidable, cve_data, cve_status):
>>>        """
>>>        Prepare CVE data for the JSON format, then write it.
>>>        """
>>> @@ -606,6 +620,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_not_affected = cve in not_affected
>>> +        is_undecidable = cve in undecidable
>>> +
>>>            status = "Unpatched"
>>>            if (is_patched or is_ignored) and not report_all:
>>>                continue
>>> @@ -613,6 +630,10 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
>>>                status = "Ignored"
>>>            elif is_patched:
>>>                status = "Patched"
>>> +        elif is_not_affected:
>>> +            status = "Not affected"
>>> +        elif is_undecidable:
>>> +            status = "Undecidable"
>>>            else:
>>>                # default value of status is Unpatched
>>>                unpatched_cves.append(cve)
>>> @@ -645,12 +666,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, not_affected, 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, not_affected, 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, not_affected, undecidable, cve_data, status)
>>>
>>> -=-=-=-=-=-=-=-=-=-=-=-
>>> Links: You receive all messages sent to this group.
>>> View/Reply Online (#189667): https://lists.openembedded.org/g/openembedded-core/message/189667
>>> Mute This Topic: https://lists.openembedded.org/mt/102172913/3619876
>>> Group Owner: openembedded-core+owner@lists.openembedded.org<mailto:openembedded-core%2Bowner@lists.openembedded.org>
>>> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [andrej.v@skyrain.eu<mailto:andrej.v@skyrain.eu>]
>>> -=-=-=-=-=-=-=-=-=-=-=-
>>>
Marta Rybczynska Oct. 25, 2023, 1:31 p.m. UTC | #6
Hello Marko,
I think that we will need to go back to the drawing board and have a
look what we want to report from the CVE check.
I'm not totally happy with the solution proposed here, because it is
adding high-level states. However, it is a step
forward to be able to map our status to VEX. In the RFQ work on SPDX3
(which includes VEX in the "security" profile)
we had a look at that and realized that mapping of the information is
not possible, we're missing information.

The current state of "Ignore" is not great either. You have added
additional context, but still some statuses
do not make sense in Ignore. It is effectively used as an "override"
of everything.

I also have a multi-fetcher in the works, which downloads CVE data
from other sources that NVD. Results are
sometimes in conflict and we'll have to handle that too. And there are
other sources lurking behind too.

So, when the RFQ season is finished (Oct 31st), we'll get back to the
drawing board to design what statuses we really
want and need. I understand you'd like to be a part of this discussion.

Kind regards,
Marta

On Wed, Oct 25, 2023 at 3:09 PM Marko, Peter <Peter.Marko@siemens.com> wrote:
>
> Hello Marta,
>
>
>
> Major reason why we introduced CVE_STATUS was exactly to avoid patch like this.
>
> There were ideas to introduce 5 or 10 or 15 different statuses and we decided to keep 3 and introduce “sub-statuses”.
>
> These sub-statuses are listed in cve reports, too.
>
>
>
> Currently we have three main statuses:
>
> Patched – common status for all sub-statuses which indicate that component is not vulnerable
>
> Unpatched - common status for all sub-statuses which indicate that component is vulnerable
>
> Ignored - common status for all sub-statuses which indicate that component is vulnerable but not in yocto configuration context
>
>
>
> If we don’t like “Patched” we can rename it (e.g. to  “Unaffected”) and have additional sub-statuses under this new name.
>
> Otherwise we start exploding the statuses as someone will “need” additional one soon.
>
>
>
> If we really want to introduce these new statues (I hope not), please modify this patch to handle its CVE_STATUS flags, too.
>
> Additionally, I’d drop “Undecidable” and map it to “Unpatched” (so someone needs to analyze as any other open vulnerability report)
>
>
>
> Best Regards,
>
>   Peter
>
>
>
> From: Marta Rybczynska <rybczynska@gmail.com>
> Sent: Wednesday, October 25, 2023 14:44
> To: Andrej Valek <andrej.v@skyrain.eu>
> Cc: Matsunaga-Shinji <shin.matsunaga@fujitsu.com>; Richard Purdie <richard.purdie@linuxfoundation.org>; OE-core <openembedded-core@lists.openembedded.org>; Shunsuke Tokumoto <s-tokumoto@fujitsu.com>; Marko, Peter (ADV D EU SK BFS1) <Peter.Marko@siemens.com>
> Subject: Re: [OE-core] [PATCH v2] cve-check: Classify patched CVEs into 3 statuses
>
>
>
> Hello Andrej,
>
> This patch is splitting the Patched state, not the ignore one. This is not incorrect CPE or anything else.
>
>
>
> Currently Patched means one of two situations: either this issue has never affected the code base (example: we have version 1.0, issue was introduced in 2.0 and fixed in 2.1), or the issue has been fixed.
>
>
>
> Yes, another reason to say ignore, not affected is a manual analysis showing a thing like: the issue affects only windows.
>
>
>
> Regards,
>
> Marta
>
>
>
> On Wed, 25 Oct 2023, 12:18 Andrej Valek, <andrej.v@skyrain.eu> wrote:
>
> Hi Marta,
>
> That's fine, as I said we designed the "ignore" with status
> "cpe-incorrect" or "ignored" exactly for those purposes. Extending the
> option with "not affected" doesn't make any sense.
>
> You have to set the status to "why is not affected" = "ignored". Which
> completely covers the requested case.
>
> Regards,
> Andrej
>
> On 25.10.2023 11:33, Marta Rybczynska wrote:
> > Hi Andrej,
> > This is more complex. "Not affected" is also an issue that isn't present in the
> > code - like when we have a version that has never had the vulnerability.
> > Those are also currently 'Patched' in cve-check.
> >
> > This work is in sync with what VEX is doing, is it the use-case
> > Matsanaga-Shinji?
> >
> > Regards,
> > Marta
> >
> > On Wed, Oct 25, 2023 at 8:44 AM Andrej Valek <andrej.v@skyrain.eu> wrote:
> >> Hi all,
> >>
> >> Do we really need a new "not_affected" state? I guess the ignore state
> >> is exactly designed for those purposes.
> >>
> >> Regards,
> >> Andrej
> >>
> >> On 25.10.2023 07:13, Matsunaga-Shinji 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. "Not affected" - means that the package version (PV) is not affected by 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>
> >>> Signed-off-by: Shunsuke Tokumoto <s-tokumoto@fujitsu.com>
> >>> ---
> >>>
> >>> Changes for v2:
> >>>      - Fix the status "Out of range" to "Not affected"
> >>>
> >>>    meta/classes/cve-check.bbclass | 55 +++++++++++++++++++++++-----------
> >>>    1 file changed, 38 insertions(+), 17 deletions(-)
> >>>
> >>> diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
> >>> index b55f4299da..502db324df 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, not_affected, undecidable, status = check_cves(d, patched_cves)
> >>> +            if patched or unpatched or not_affected or undecidable or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
> >>> +                cve_data = get_cve_info(d, patched + unpatched + ignored + not_affected + undecidable)
> >>> +                cve_write_data(d, patched, unpatched, ignored, not_affected, 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_not_affected = []
> >>> +    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_not_affected.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_not_affected, cves_undecidable, cves_status)
> >>>
> >>>    def get_cve_info(d, cves):
> >>>        """
> >>> @@ -447,7 +455,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, not_affected, undecidable, cve_data):
> >>>        """
> >>>        Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and
> >>>        CVE manifest if enabled.
> >>> @@ -471,7 +479,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+not_affected+undecidable:
> >>>            return
> >>>
> >>>        nvd_link = "https://nvd.nist.gov/vuln/detail/"
> >>> @@ -482,6 +490,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_not_affected = cve in not_affected
> >>> +        is_undecidable = cve in undecidable
> >>>
> >>>            status = "Unpatched"
> >>>            if (is_patched or is_ignored) and not report_all:
> >>> @@ -490,6 +500,10 @@ def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
> >>>                status = "Ignored"
> >>>            elif is_patched:
> >>>                status = "Patched"
> >>> +        elif is_not_affected:
> >>> +            status = "Not affected"
> >>> +        elif is_undecidable:
> >>> +            status = "Undecidable"
> >>>            else:
> >>>                # default value of status is Unpatched
> >>>                unpatched_cves.append(cve)
> >>> @@ -561,7 +575,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, not_affected, undecidable, cve_data, cve_status):
> >>>        """
> >>>        Prepare CVE data for the JSON format, then write it.
> >>>        """
> >>> @@ -606,6 +620,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_not_affected = cve in not_affected
> >>> +        is_undecidable = cve in undecidable
> >>> +
> >>>            status = "Unpatched"
> >>>            if (is_patched or is_ignored) and not report_all:
> >>>                continue
> >>> @@ -613,6 +630,10 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
> >>>                status = "Ignored"
> >>>            elif is_patched:
> >>>                status = "Patched"
> >>> +        elif is_not_affected:
> >>> +            status = "Not affected"
> >>> +        elif is_undecidable:
> >>> +            status = "Undecidable"
> >>>            else:
> >>>                # default value of status is Unpatched
> >>>                unpatched_cves.append(cve)
> >>> @@ -645,12 +666,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, not_affected, 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, not_affected, 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, not_affected, undecidable, cve_data, status)
> >>>
> >>> -=-=-=-=-=-=-=-=-=-=-=-
> >>> Links: You receive all messages sent to this group.
> >>> View/Reply Online (#189667): https://lists.openembedded.org/g/openembedded-core/message/189667
> >>> Mute This Topic: https://lists.openembedded.org/mt/102172913/3619876
> >>> Group Owner: openembedded-core+owner@lists.openembedded.org
> >>> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [andrej.v@skyrain.eu]
> >>> -=-=-=-=-=-=-=-=-=-=-=-
> >>>
Luca Ceresoli Oct. 26, 2023, 7:28 a.m. UTC | #7
Hello Matsunaga-Shinji,

On Wed, 25 Oct 2023 14:13:44 +0900
"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. "Not affected" - means that the package version (PV) is not affected by 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>
> Signed-off-by: Shunsuke Tokumoto <s-tokumoto@fujitsu.com>

This patch triggers a selftest failure:

AssertionError: 'Not affected' != 'Patched'
- Not affected
+ Patched

https://autobuilder.yoctoproject.org/typhoon/#/builders/127/builds/2329/steps/15/logs/stdio
https://autobuilder.yoctoproject.org/typhoon/#/builders/80/builds/5912/steps/14/logs/stdio
https://autobuilder.yoctoproject.org/typhoon/#/builders/79/builds/5962/steps/14/logs/stdio

Luca
diff mbox series

Patch

diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
index b55f4299da..502db324df 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, not_affected, undecidable, status = check_cves(d, patched_cves)
+            if patched or unpatched or not_affected or undecidable or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
+                cve_data = get_cve_info(d, patched + unpatched + ignored + not_affected + undecidable)
+                cve_write_data(d, patched, unpatched, ignored, not_affected, 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_not_affected = []
+    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_not_affected.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_not_affected, cves_undecidable, cves_status)
 
 def get_cve_info(d, cves):
     """
@@ -447,7 +455,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, not_affected, undecidable, cve_data):
     """
     Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and
     CVE manifest if enabled.
@@ -471,7 +479,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+not_affected+undecidable:
         return
 
     nvd_link = "https://nvd.nist.gov/vuln/detail/"
@@ -482,6 +490,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_not_affected = cve in not_affected
+        is_undecidable = cve in undecidable
 
         status = "Unpatched"
         if (is_patched or is_ignored) and not report_all:
@@ -490,6 +500,10 @@  def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
             status = "Ignored"
         elif is_patched:
             status = "Patched"
+        elif is_not_affected:
+            status = "Not affected"
+        elif is_undecidable:
+            status = "Undecidable"
         else:
             # default value of status is Unpatched
             unpatched_cves.append(cve)
@@ -561,7 +575,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, not_affected, undecidable, cve_data, cve_status):
     """
     Prepare CVE data for the JSON format, then write it.
     """
@@ -606,6 +620,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_not_affected = cve in not_affected
+        is_undecidable = cve in undecidable
+
         status = "Unpatched"
         if (is_patched or is_ignored) and not report_all:
             continue
@@ -613,6 +630,10 @@  def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
             status = "Ignored"
         elif is_patched:
             status = "Patched"
+        elif is_not_affected:
+            status = "Not affected"
+        elif is_undecidable:
+            status = "Undecidable"
         else:
             # default value of status is Unpatched
             unpatched_cves.append(cve)
@@ -645,12 +666,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, not_affected, 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, not_affected, 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, not_affected, undecidable, cve_data, status)