summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrej Valek <andrej.valek@siemens.com>2023-06-23 13:14:56 +0200
committerRichard Purdie <richard.purdie@linuxfoundation.org>2023-07-19 23:25:01 +0100
commitbe9883a92bad0fe4c1e9c7302c93dea4ac680f8c (patch)
tree6d9d35acbb91f98016956168b4ea90f9b9ce0764
parentebb8b39463cef3c3d0f90f054c433b2f5256cb1a (diff)
downloadpoky-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.bbclass81
-rw-r--r--meta/conf/bitbake.conf1
-rw-r--r--meta/conf/cve-check-map.conf28
-rw-r--r--meta/lib/oe/cve_check.py25
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)
71CVE_CHECK_SKIP_RECIPE ?= "" 71CVE_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
79CVE_CHECK_IGNORE ?= "" 95CVE_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
89CVE_VERSION_SUFFIX ??= "" 105CVE_VERSION_SUFFIX ??= ""
90 106
107python () {
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
91def generate_json_report(d, out_path, link_path): 125def 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
831include conf/documentation.conf 831include conf/documentation.conf
832include conf/licenses.conf 832include conf/licenses.conf
833require conf/sanity.conf 833require conf/sanity.conf
834require 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)
4CVE_CHECK_STATUSMAP[patched] = "Patched"
5# use when this class does not detect backported patch (e.g. vendor kernel repo with cherry-picked CVE patch)
6CVE_CHECK_STATUSMAP[backported-patch] = "Patched"
7# use when NVD DB does not mention patched versions of stable/LTS branches which have upstream CVE backports
8CVE_CHECK_STATUSMAP[cpe-stable-backport] = "Patched"
9# use when NVD DB does not mention correct version or does not mention any verion at all
10CVE_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
13CVE_CHECK_STATUSMAP[unpatched] = "Unpatched"
14# use when CVE is confirmed by upstream but fix is still not available
15CVE_CHECK_STATUSMAP[vulnerable-investigating] = "Unpatched"
16
17# used for migration from old concept, do not use for new vulnerabilities
18CVE_CHECK_STATUSMAP[ignored] = "Ignored"
19# use when NVD DB wrongly indicates vulnerability which is actually for a different component
20CVE_CHECK_STATUSMAP[cpe-incorrect] = "Ignored"
21# use when upstream does not accept the report as a vulnerability (e.g. works as designed)
22CVE_CHECK_STATUSMAP[disputed] = "Ignored"
23# use when vulnerability depends on build or runtime configuration which is not used
24CVE_CHECK_STATUSMAP[not-applicable-config] = "Ignored"
25# use when vulnerability affects other platform (e.g. Windows or Debian)
26CVE_CHECK_STATUSMAP[not-applicable-platform] = "Ignored"
27# use when upstream acknowledged the vulnerability but does not plan to fix it
28CVE_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
228def 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)