diff options
author | Andrej Valek <andrej.valek@siemens.com> | 2023-06-23 13:14:56 +0200 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2023-07-19 23:25:01 +0100 |
commit | be9883a92bad0fe4c1e9c7302c93dea4ac680f8c (patch) | |
tree | 6d9d35acbb91f98016956168b4ea90f9b9ce0764 | |
parent | ebb8b39463cef3c3d0f90f054c433b2f5256cb1a (diff) | |
download | poky-be9883a92bad0fe4c1e9c7302c93dea4ac680f8c.tar.gz |
cve-check: add option to add additional patched CVEs
- Replace CVE_CHECK_IGNORE with CVE_STATUS to be more flexible.
The CVE_STATUS should contain an information about status wich
is decoded in 3 items:
- generic status: "Ignored", "Patched" or "Unpatched"
- more detailed status enum
- description: free text describing reason for status
Examples of usage:
CVE_STATUS[CVE-1234-0001] = "not-applicable-platform: Issue only applies on Windows"
CVE_STATUS[CVE-1234-0002] = "fixed-version: Fixed externally"
CVE_CHECK_STATUSMAP[not-applicable-platform] = "Ignored"
CVE_CHECK_STATUSMAP[fixed-version] = "Patched"
(From OE-Core rev: 34f682a24b7075b12ec308154b937ad118d69fe5)
Signed-off-by: Andrej Valek <andrej.valek@siemens.com>
Signed-off-by: Peter Marko <peter.marko@siemens.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
-rw-r--r-- | meta/classes/cve-check.bbclass | 81 | ||||
-rw-r--r-- | meta/conf/bitbake.conf | 1 | ||||
-rw-r--r-- | meta/conf/cve-check-map.conf | 28 | ||||
-rw-r--r-- | meta/lib/oe/cve_check.py | 25 |
4 files changed, 122 insertions, 13 deletions
diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass index f7abaf4f0c..c1f1ea0fd6 100644 --- a/meta/classes/cve-check.bbclass +++ b/meta/classes/cve-check.bbclass | |||
@@ -70,12 +70,28 @@ CVE_CHECK_COVERAGE ??= "1" | |||
70 | # Skip CVE Check for packages (PN) | 70 | # Skip CVE Check for packages (PN) |
71 | CVE_CHECK_SKIP_RECIPE ?= "" | 71 | CVE_CHECK_SKIP_RECIPE ?= "" |
72 | 72 | ||
73 | # Ingore the check for a given list of CVEs. If a CVE is found, | 73 | # Replace NVD DB check status for a given CVE. Each of CVE has to be mentioned |
74 | # then it is considered patched. The value is a string containing | 74 | # separately with optional detail and description for this status. |
75 | # space separated CVE values: | ||
76 | # | 75 | # |
77 | # CVE_CHECK_IGNORE = 'CVE-2014-2524 CVE-2018-1234' | 76 | # CVE_STATUS[CVE-1234-0001] = "not-applicable-platform: Issue only applies on Windows" |
77 | # CVE_STATUS[CVE-1234-0002] = "fixed-version: Fixed externally" | ||
78 | # | 78 | # |
79 | # Settings the same status and reason for multiple CVEs is possible | ||
80 | # via CVE_STATUS_GROUPS variable. | ||
81 | # | ||
82 | # CVE_STATUS_GROUPS = "CVE_STATUS_WIN CVE_STATUS_PATCHED" | ||
83 | # | ||
84 | # CVE_STATUS_WIN = "CVE-1234-0001 CVE-1234-0003" | ||
85 | # CVE_STATUS_WIN[status] = "not-applicable-platform: Issue only applies on Windows" | ||
86 | # CVE_STATUS_PATCHED = "CVE-1234-0002 CVE-1234-0004" | ||
87 | # CVE_STATUS_PATCHED[status] = "fixed-version: Fixed externally" | ||
88 | # | ||
89 | # All possible CVE statuses could be found in cve-check-map.conf | ||
90 | # CVE_CHECK_STATUSMAP[not-applicable-platform] = "Ignored" | ||
91 | # CVE_CHECK_STATUSMAP[fixed-version] = "Patched" | ||
92 | # | ||
93 | # CVE_CHECK_IGNORE is deprecated and CVE_STATUS has to be used instead. | ||
94 | # Keep CVE_CHECK_IGNORE until other layers migrate to new variables | ||
79 | CVE_CHECK_IGNORE ?= "" | 95 | CVE_CHECK_IGNORE ?= "" |
80 | 96 | ||
81 | # Layers to be excluded | 97 | # Layers to be excluded |
@@ -88,6 +104,24 @@ CVE_CHECK_LAYER_INCLUDELIST ??= "" | |||
88 | # set to "alphabetical" for version using single alphabetical character as increment release | 104 | # set to "alphabetical" for version using single alphabetical character as increment release |
89 | CVE_VERSION_SUFFIX ??= "" | 105 | CVE_VERSION_SUFFIX ??= "" |
90 | 106 | ||
107 | python () { | ||
108 | # Fallback all CVEs from CVE_CHECK_IGNORE to CVE_STATUS | ||
109 | cve_check_ignore = d.getVar("CVE_CHECK_IGNORE") | ||
110 | if cve_check_ignore: | ||
111 | bb.warn("CVE_CHECK_IGNORE is deprecated in favor of CVE_STATUS") | ||
112 | for cve in (d.getVar("CVE_CHECK_IGNORE") or "").split(): | ||
113 | d.setVarFlag("CVE_STATUS", cve, "ignored") | ||
114 | |||
115 | # Process CVE_STATUS_GROUPS to set multiple statuses and optional detail or description at once | ||
116 | for cve_status_group in (d.getVar("CVE_STATUS_GROUPS") or "").split(): | ||
117 | cve_group = d.getVar(cve_status_group) | ||
118 | if cve_group is not None: | ||
119 | for cve in cve_group.split(): | ||
120 | d.setVarFlag("CVE_STATUS", cve, d.getVarFlag(cve_status_group, "status")) | ||
121 | else: | ||
122 | bb.warn("CVE_STATUS_GROUPS contains undefined variable %s" % cve_status_group) | ||
123 | } | ||
124 | |||
91 | def generate_json_report(d, out_path, link_path): | 125 | def generate_json_report(d, out_path, link_path): |
92 | if os.path.exists(d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")): | 126 | if os.path.exists(d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")): |
93 | import json | 127 | import json |
@@ -260,7 +294,7 @@ def check_cves(d, patched_cves): | |||
260 | """ | 294 | """ |
261 | Connect to the NVD database and find unpatched cves. | 295 | Connect to the NVD database and find unpatched cves. |
262 | """ | 296 | """ |
263 | from oe.cve_check import Version, convert_cve_version | 297 | from oe.cve_check import Version, convert_cve_version, decode_cve_status |
264 | 298 | ||
265 | pn = d.getVar("PN") | 299 | pn = d.getVar("PN") |
266 | real_pv = d.getVar("PV") | 300 | real_pv = d.getVar("PV") |
@@ -282,7 +316,12 @@ def check_cves(d, patched_cves): | |||
282 | bb.note("Recipe has been skipped by cve-check") | 316 | bb.note("Recipe has been skipped by cve-check") |
283 | return ([], [], [], []) | 317 | return ([], [], [], []) |
284 | 318 | ||
285 | cve_ignore = d.getVar("CVE_CHECK_IGNORE").split() | 319 | # Convert CVE_STATUS into ignored CVEs and check validity |
320 | cve_ignore = [] | ||
321 | for cve in (d.getVarFlags("CVE_STATUS") or {}): | ||
322 | decoded_status, _, _ = decode_cve_status(d, cve) | ||
323 | if decoded_status == "Ignored": | ||
324 | cve_ignore.append(cve) | ||
286 | 325 | ||
287 | import sqlite3 | 326 | import sqlite3 |
288 | db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro") | 327 | db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro") |
@@ -413,6 +452,8 @@ def cve_write_data_text(d, patched, unpatched, ignored, cve_data): | |||
413 | CVE manifest if enabled. | 452 | CVE manifest if enabled. |
414 | """ | 453 | """ |
415 | 454 | ||
455 | from oe.cve_check import decode_cve_status | ||
456 | |||
416 | cve_file = d.getVar("CVE_CHECK_LOG") | 457 | cve_file = d.getVar("CVE_CHECK_LOG") |
417 | fdir_name = d.getVar("FILE_DIRNAME") | 458 | fdir_name = d.getVar("FILE_DIRNAME") |
418 | layer = fdir_name.split("/")[-3] | 459 | layer = fdir_name.split("/")[-3] |
@@ -441,20 +482,27 @@ def cve_write_data_text(d, patched, unpatched, ignored, cve_data): | |||
441 | is_patched = cve in patched | 482 | is_patched = cve in patched |
442 | is_ignored = cve in ignored | 483 | is_ignored = cve in ignored |
443 | 484 | ||
485 | status = "Unpatched" | ||
444 | if (is_patched or is_ignored) and not report_all: | 486 | if (is_patched or is_ignored) and not report_all: |
445 | continue | 487 | continue |
488 | if is_ignored: | ||
489 | status = "Ignored" | ||
490 | elif is_patched: | ||
491 | status = "Patched" | ||
492 | else: | ||
493 | # default value of status is Unpatched | ||
494 | unpatched_cves.append(cve) | ||
446 | 495 | ||
447 | write_string += "LAYER: %s\n" % layer | 496 | write_string += "LAYER: %s\n" % layer |
448 | write_string += "PACKAGE NAME: %s\n" % d.getVar("PN") | 497 | write_string += "PACKAGE NAME: %s\n" % d.getVar("PN") |
449 | write_string += "PACKAGE VERSION: %s%s\n" % (d.getVar("EXTENDPE"), d.getVar("PV")) | 498 | write_string += "PACKAGE VERSION: %s%s\n" % (d.getVar("EXTENDPE"), d.getVar("PV")) |
450 | write_string += "CVE: %s\n" % cve | 499 | write_string += "CVE: %s\n" % cve |
451 | if is_ignored: | 500 | write_string += "CVE STATUS: %s\n" % status |
452 | write_string += "CVE STATUS: Ignored\n" | 501 | _, detail, description = decode_cve_status(d, cve) |
453 | elif is_patched: | 502 | if detail: |
454 | write_string += "CVE STATUS: Patched\n" | 503 | write_string += "CVE DETAIL: %s\n" % detail |
455 | else: | 504 | if description: |
456 | unpatched_cves.append(cve) | 505 | write_string += "CVE DESCRIPTION: %s\n" % description |
457 | write_string += "CVE STATUS: Unpatched\n" | ||
458 | write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["summary"] | 506 | write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["summary"] |
459 | write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["scorev2"] | 507 | write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["scorev2"] |
460 | write_string += "CVSS v3 BASE SCORE: %s\n" % cve_data[cve]["scorev3"] | 508 | write_string += "CVSS v3 BASE SCORE: %s\n" % cve_data[cve]["scorev3"] |
@@ -516,6 +564,8 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status): | |||
516 | Prepare CVE data for the JSON format, then write it. | 564 | Prepare CVE data for the JSON format, then write it. |
517 | """ | 565 | """ |
518 | 566 | ||
567 | from oe.cve_check import decode_cve_status | ||
568 | |||
519 | output = {"version":"1", "package": []} | 569 | output = {"version":"1", "package": []} |
520 | nvd_link = "https://nvd.nist.gov/vuln/detail/" | 570 | nvd_link = "https://nvd.nist.gov/vuln/detail/" |
521 | 571 | ||
@@ -576,6 +626,11 @@ def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status): | |||
576 | "status" : status, | 626 | "status" : status, |
577 | "link": issue_link | 627 | "link": issue_link |
578 | } | 628 | } |
629 | _, detail, description = decode_cve_status(d, cve) | ||
630 | if detail: | ||
631 | cve_item["detail"] = detail | ||
632 | if description: | ||
633 | cve_item["description"] = description | ||
579 | cve_list.append(cve_item) | 634 | cve_list.append(cve_item) |
580 | 635 | ||
581 | package_data["issue"] = cve_list | 636 | package_data["issue"] = cve_list |
diff --git a/meta/conf/bitbake.conf b/meta/conf/bitbake.conf index 8daaaad615..475d6523bb 100644 --- a/meta/conf/bitbake.conf +++ b/meta/conf/bitbake.conf | |||
@@ -831,6 +831,7 @@ include conf/distro/defaultsetup.conf | |||
831 | include conf/documentation.conf | 831 | include conf/documentation.conf |
832 | include conf/licenses.conf | 832 | include conf/licenses.conf |
833 | require conf/sanity.conf | 833 | require conf/sanity.conf |
834 | require conf/cve-check-map.conf | ||
834 | 835 | ||
835 | ################################################################## | 836 | ################################################################## |
836 | # Weak variables (usually to retain backwards compatibility) | 837 | # Weak variables (usually to retain backwards compatibility) |
diff --git a/meta/conf/cve-check-map.conf b/meta/conf/cve-check-map.conf new file mode 100644 index 0000000000..17b0f15571 --- /dev/null +++ b/meta/conf/cve-check-map.conf | |||
@@ -0,0 +1,28 @@ | |||
1 | # Possible options for CVE statuses | ||
2 | |||
3 | # used by this class internally when fix is detected (NVD DB version check or CVE patch file) | ||
4 | CVE_CHECK_STATUSMAP[patched] = "Patched" | ||
5 | # use when this class does not detect backported patch (e.g. vendor kernel repo with cherry-picked CVE patch) | ||
6 | CVE_CHECK_STATUSMAP[backported-patch] = "Patched" | ||
7 | # use when NVD DB does not mention patched versions of stable/LTS branches which have upstream CVE backports | ||
8 | CVE_CHECK_STATUSMAP[cpe-stable-backport] = "Patched" | ||
9 | # use when NVD DB does not mention correct version or does not mention any verion at all | ||
10 | CVE_CHECK_STATUSMAP[fixed-version] = "Patched" | ||
11 | |||
12 | # used internally by this class if CVE vulnerability is detected which is not marked as fixed or ignored | ||
13 | CVE_CHECK_STATUSMAP[unpatched] = "Unpatched" | ||
14 | # use when CVE is confirmed by upstream but fix is still not available | ||
15 | CVE_CHECK_STATUSMAP[vulnerable-investigating] = "Unpatched" | ||
16 | |||
17 | # used for migration from old concept, do not use for new vulnerabilities | ||
18 | CVE_CHECK_STATUSMAP[ignored] = "Ignored" | ||
19 | # use when NVD DB wrongly indicates vulnerability which is actually for a different component | ||
20 | CVE_CHECK_STATUSMAP[cpe-incorrect] = "Ignored" | ||
21 | # use when upstream does not accept the report as a vulnerability (e.g. works as designed) | ||
22 | CVE_CHECK_STATUSMAP[disputed] = "Ignored" | ||
23 | # use when vulnerability depends on build or runtime configuration which is not used | ||
24 | CVE_CHECK_STATUSMAP[not-applicable-config] = "Ignored" | ||
25 | # use when vulnerability affects other platform (e.g. Windows or Debian) | ||
26 | CVE_CHECK_STATUSMAP[not-applicable-platform] = "Ignored" | ||
27 | # use when upstream acknowledged the vulnerability but does not plan to fix it | ||
28 | CVE_CHECK_STATUSMAP[upstream-wontfix] = "Ignored" | ||
diff --git a/meta/lib/oe/cve_check.py b/meta/lib/oe/cve_check.py index dbaa0b373a..5bf3caac47 100644 --- a/meta/lib/oe/cve_check.py +++ b/meta/lib/oe/cve_check.py | |||
@@ -130,6 +130,13 @@ def get_patched_cves(d): | |||
130 | if not fname_match and not text_match: | 130 | if not fname_match and not text_match: |
131 | bb.debug(2, "Patch %s doesn't solve CVEs" % patch_file) | 131 | bb.debug(2, "Patch %s doesn't solve CVEs" % patch_file) |
132 | 132 | ||
133 | # Search for additional patched CVEs | ||
134 | for cve in (d.getVarFlags("CVE_STATUS") or {}): | ||
135 | decoded_status, _, _ = decode_cve_status(d, cve) | ||
136 | if decoded_status == "Patched": | ||
137 | bb.debug(2, "CVE %s is additionally patched" % cve) | ||
138 | patched_cves.add(cve) | ||
139 | |||
133 | return patched_cves | 140 | return patched_cves |
134 | 141 | ||
135 | 142 | ||
@@ -218,3 +225,21 @@ def convert_cve_version(version): | |||
218 | 225 | ||
219 | return version + update | 226 | return version + update |
220 | 227 | ||
228 | def decode_cve_status(d, cve): | ||
229 | """ | ||
230 | Convert CVE_STATUS into status, detail and description. | ||
231 | """ | ||
232 | status = d.getVarFlag("CVE_STATUS", cve) | ||
233 | if status is None: | ||
234 | return ("", "", "") | ||
235 | |||
236 | status_split = status.split(':', 1) | ||
237 | detail = status_split[0] | ||
238 | description = status_split[1].strip() if (len(status_split) > 1) else "" | ||
239 | |||
240 | status_mapping = d.getVarFlag("CVE_CHECK_STATUSMAP", detail) | ||
241 | if status_mapping is None: | ||
242 | bb.warn('Invalid detail %s for CVE_STATUS[%s] = "%s", fallback to Unpatched' % (detail, cve, status)) | ||
243 | status_mapping = "Unpatched" | ||
244 | |||
245 | return (status_mapping, detail, description) | ||