summaryrefslogtreecommitdiffstats
path: root/meta/classes/cve-check.bbclass
diff options
context:
space:
mode:
Diffstat (limited to 'meta/classes/cve-check.bbclass')
-rw-r--r--meta/classes/cve-check.bbclass556
1 files changed, 407 insertions, 149 deletions
diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
index 112ee3379d..56ba8bceef 100644
--- a/meta/classes/cve-check.bbclass
+++ b/meta/classes/cve-check.bbclass
@@ -1,3 +1,9 @@
1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: MIT
5#
6
1# This class is used to check recipes against public CVEs. 7# This class is used to check recipes against public CVEs.
2# 8#
3# In order to use this class just inherit the class in the 9# In order to use this class just inherit the class in the
@@ -20,13 +26,13 @@
20# the only method to check against CVEs. Running this tool 26# the only method to check against CVEs. Running this tool
21# doesn't guarantee your packages are free of CVEs. 27# doesn't guarantee your packages are free of CVEs.
22 28
23# The product name that the CVE database uses. Defaults to BPN, but may need to 29# The product name that the CVE database uses defaults to BPN, but may need to
24# be overriden per recipe (for example tiff.bb sets CVE_PRODUCT=libtiff). 30# be overriden per recipe (for example tiff.bb sets CVE_PRODUCT=libtiff).
25CVE_PRODUCT ??= "${BPN}" 31CVE_PRODUCT ??= "${BPN}"
26CVE_VERSION ??= "${PV}" 32CVE_VERSION ??= "${PV}"
27 33
28CVE_CHECK_DB_DIR ?= "${DL_DIR}/CVE_CHECK" 34CVE_CHECK_DB_DIR ?= "${DL_DIR}/CVE_CHECK"
29CVE_CHECK_DB_FILE ?= "${CVE_CHECK_DB_DIR}/nvdcve_1.1.db" 35CVE_CHECK_DB_FILE ?= "${CVE_CHECK_DB_DIR}/nvdcve_2-1.db"
30CVE_CHECK_DB_FILE_LOCK ?= "${CVE_CHECK_DB_FILE}.lock" 36CVE_CHECK_DB_FILE_LOCK ?= "${CVE_CHECK_DB_FILE}.lock"
31 37
32CVE_CHECK_LOG ?= "${T}/cve.log" 38CVE_CHECK_LOG ?= "${T}/cve.log"
@@ -34,38 +40,115 @@ CVE_CHECK_TMP_FILE ?= "${TMPDIR}/cve_check"
34CVE_CHECK_SUMMARY_DIR ?= "${LOG_DIR}/cve" 40CVE_CHECK_SUMMARY_DIR ?= "${LOG_DIR}/cve"
35CVE_CHECK_SUMMARY_FILE_NAME ?= "cve-summary" 41CVE_CHECK_SUMMARY_FILE_NAME ?= "cve-summary"
36CVE_CHECK_SUMMARY_FILE ?= "${CVE_CHECK_SUMMARY_DIR}/${CVE_CHECK_SUMMARY_FILE_NAME}" 42CVE_CHECK_SUMMARY_FILE ?= "${CVE_CHECK_SUMMARY_DIR}/${CVE_CHECK_SUMMARY_FILE_NAME}"
43CVE_CHECK_SUMMARY_FILE_NAME_JSON = "cve-summary.json"
44CVE_CHECK_SUMMARY_INDEX_PATH = "${CVE_CHECK_SUMMARY_DIR}/cve-summary-index.txt"
45
46CVE_CHECK_LOG_JSON ?= "${T}/cve.json"
37 47
38CVE_CHECK_DIR ??= "${DEPLOY_DIR}/cve" 48CVE_CHECK_DIR ??= "${DEPLOY_DIR}/cve"
39CVE_CHECK_RECIPE_FILE ?= "${CVE_CHECK_DIR}/${PN}" 49CVE_CHECK_RECIPE_FILE ?= "${CVE_CHECK_DIR}/${PN}"
40CVE_CHECK_MANIFEST ?= "${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${IMAGE_NAME_SUFFIX}.cve" 50CVE_CHECK_RECIPE_FILE_JSON ?= "${CVE_CHECK_DIR}/${PN}_cve.json"
51CVE_CHECK_MANIFEST ?= "${IMGDEPLOYDIR}/${IMAGE_NAME}.cve"
52CVE_CHECK_MANIFEST_JSON ?= "${IMGDEPLOYDIR}/${IMAGE_NAME}.json"
41CVE_CHECK_COPY_FILES ??= "1" 53CVE_CHECK_COPY_FILES ??= "1"
42CVE_CHECK_CREATE_MANIFEST ??= "1" 54CVE_CHECK_CREATE_MANIFEST ??= "1"
43 55
56# Report Patched or Ignored CVEs
44CVE_CHECK_REPORT_PATCHED ??= "1" 57CVE_CHECK_REPORT_PATCHED ??= "1"
45 58
46# Whitelist for packages (PN) 59CVE_CHECK_SHOW_WARNINGS ??= "1"
47CVE_CHECK_PN_WHITELIST ?= "" 60
61# Provide text output
62CVE_CHECK_FORMAT_TEXT ??= "1"
63
64# Provide JSON output
65CVE_CHECK_FORMAT_JSON ??= "1"
48 66
49# Whitelist for CVE. If a CVE is found, then it is considered patched. 67# Check for packages without CVEs (no issues or missing product name)
50# The value is a string containing space separated CVE values: 68CVE_CHECK_COVERAGE ??= "1"
69
70# Skip CVE Check for packages (PN)
71CVE_CHECK_SKIP_RECIPE ?= ""
72
73# Replace NVD DB check status for a given CVE. Each of CVE has to be mentioned
74# separately with optional detail and description for this status.
75#
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#
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"
51# 88#
52# CVE_CHECK_WHITELIST = 'CVE-2014-2524 CVE-2018-1234' 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"
53# 92#
54CVE_CHECK_WHITELIST ?= "" 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
95CVE_CHECK_IGNORE ?= ""
55 96
56# Layers to be excluded 97# Layers to be excluded
57CVE_CHECK_LAYER_EXCLUDELIST ??= "" 98CVE_CHECK_LAYER_EXCLUDELIST ??= ""
58 99
59# Layers to be included 100# Layers to be included
60CVE_CHECK_LAYER_INCLUDELIST ??= "" 101CVE_CHECK_LAYER_INCLUDELIST ??= ""
61 102
62 103
63# set to "alphabetical" for version using single alphabetical character as increament release 104# set to "alphabetical" for version using single alphabetical character as increment release
64CVE_VERSION_SUFFIX ??= "" 105CVE_VERSION_SUFFIX ??= ""
65 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
125def generate_json_report(d, out_path, link_path):
126 if os.path.exists(d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")):
127 import json
128 from oe.cve_check import cve_check_merge_jsons, update_symlinks
129
130 bb.note("Generating JSON CVE summary")
131 index_file = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")
132 summary = {"version":"1", "package": []}
133 with open(index_file) as f:
134 filename = f.readline()
135 while filename:
136 with open(filename.rstrip()) as j:
137 data = json.load(j)
138 cve_check_merge_jsons(summary, data)
139 filename = f.readline()
140
141 summary["package"].sort(key=lambda d: d['name'])
142
143 with open(out_path, "w") as f:
144 json.dump(summary, f, indent=2)
145
146 update_symlinks(out_path, link_path)
147
66python cve_save_summary_handler () { 148python cve_save_summary_handler () {
67 import shutil 149 import shutil
68 import datetime 150 import datetime
151 from oe.cve_check import update_symlinks
69 152
70 cve_tmp_file = d.getVar("CVE_CHECK_TMP_FILE") 153 cve_tmp_file = d.getVar("CVE_CHECK_TMP_FILE")
71 154
@@ -78,13 +161,15 @@ python cve_save_summary_handler () {
78 161
79 if os.path.exists(cve_tmp_file): 162 if os.path.exists(cve_tmp_file):
80 shutil.copyfile(cve_tmp_file, cve_summary_file) 163 shutil.copyfile(cve_tmp_file, cve_summary_file)
81 164 cvefile_link = os.path.join(cvelogpath, cve_summary_name)
82 if cve_summary_file and os.path.exists(cve_summary_file): 165 update_symlinks(cve_summary_file, cvefile_link)
83 cvefile_link = os.path.join(cvelogpath, cve_summary_name) 166 bb.plain("Complete CVE report summary created at: %s" % cvefile_link)
84 167
85 if os.path.exists(os.path.realpath(cvefile_link)): 168 if d.getVar("CVE_CHECK_FORMAT_JSON") == "1":
86 os.remove(cvefile_link) 169 json_summary_link_name = os.path.join(cvelogpath, d.getVar("CVE_CHECK_SUMMARY_FILE_NAME_JSON"))
87 os.symlink(os.path.basename(cve_summary_file), cvefile_link) 170 json_summary_name = os.path.join(cvelogpath, "%s-%s.json" % (cve_summary_name, timestamp))
171 generate_json_report(d, json_summary_name, json_summary_link_name)
172 bb.plain("Complete CVE JSON report summary created at: %s" % json_summary_link_name)
88} 173}
89 174
90addhandler cve_save_summary_handler 175addhandler cve_save_summary_handler
@@ -94,23 +179,25 @@ python do_cve_check () {
94 """ 179 """
95 Check recipe for patched and unpatched CVEs 180 Check recipe for patched and unpatched CVEs
96 """ 181 """
182 from oe.cve_check import get_patched_cves
97 183
98 if os.path.exists(d.getVar("CVE_CHECK_DB_FILE")): 184 with bb.utils.fileslocked([d.getVar("CVE_CHECK_DB_FILE_LOCK")], shared=True):
99 try: 185 if os.path.exists(d.getVar("CVE_CHECK_DB_FILE")):
100 patched_cves = get_patches_cves(d) 186 try:
101 except FileNotFoundError: 187 patched_cves = get_patched_cves(d)
102 bb.fatal("Failure in searching patches") 188 except FileNotFoundError:
103 whitelisted, patched, unpatched = check_cves(d, patched_cves) 189 bb.fatal("Failure in searching patches")
104 if patched or unpatched: 190 ignored, patched, unpatched, status = check_cves(d, patched_cves)
105 cve_data = get_cve_info(d, patched + unpatched) 191 if patched or unpatched or (d.getVar("CVE_CHECK_COVERAGE") == "1" and status):
106 cve_write_data(d, patched, unpatched, whitelisted, cve_data) 192 cve_data = get_cve_info(d, patched + unpatched + ignored)
107 else: 193 cve_write_data(d, patched, unpatched, ignored, cve_data, status)
108 bb.note("No CVE database found, skipping CVE check") 194 else:
195 bb.note("No CVE database found, skipping CVE check")
109 196
110} 197}
111 198
112addtask cve_check before do_build after do_fetch 199addtask cve_check before do_build
113do_cve_check[depends] = "cve-update-db-native:do_fetch" 200do_cve_check[depends] = "cve-update-nvd2-native:do_fetch"
114do_cve_check[nostamp] = "1" 201do_cve_check[nostamp] = "1"
115 202
116python cve_check_cleanup () { 203python cve_check_cleanup () {
@@ -118,10 +205,11 @@ python cve_check_cleanup () {
118 Delete the file used to gather all the CVE information. 205 Delete the file used to gather all the CVE information.
119 """ 206 """
120 bb.utils.remove(e.data.getVar("CVE_CHECK_TMP_FILE")) 207 bb.utils.remove(e.data.getVar("CVE_CHECK_TMP_FILE"))
208 bb.utils.remove(e.data.getVar("CVE_CHECK_SUMMARY_INDEX_PATH"))
121} 209}
122 210
123addhandler cve_check_cleanup 211addhandler cve_check_cleanup
124cve_check_cleanup[eventmask] = "bb.cooker.CookerExit" 212cve_check_cleanup[eventmask] = "bb.event.BuildCompleted"
125 213
126python cve_check_write_rootfs_manifest () { 214python cve_check_write_rootfs_manifest () {
127 """ 215 """
@@ -129,116 +217,113 @@ python cve_check_write_rootfs_manifest () {
129 """ 217 """
130 218
131 import shutil 219 import shutil
220 import json
221 from oe.rootfs import image_list_installed_packages
222 from oe.cve_check import cve_check_merge_jsons, update_symlinks
132 223
133 if d.getVar("CVE_CHECK_COPY_FILES") == "1": 224 if d.getVar("CVE_CHECK_COPY_FILES") == "1":
134 deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE") 225 deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE")
135 if os.path.exists(deploy_file): 226 if os.path.exists(deploy_file):
136 bb.utils.remove(deploy_file) 227 bb.utils.remove(deploy_file)
137 228 deploy_file_json = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
138 if os.path.exists(d.getVar("CVE_CHECK_TMP_FILE")): 229 if os.path.exists(deploy_file_json):
139 bb.note("Writing rootfs CVE manifest") 230 bb.utils.remove(deploy_file_json)
140 deploy_dir = d.getVar("DEPLOY_DIR_IMAGE") 231
141 link_name = d.getVar("IMAGE_LINK_NAME") 232 # Create a list of relevant recipies
233 recipies = set()
234 for pkg in list(image_list_installed_packages(d)):
235 pkg_info = os.path.join(d.getVar('PKGDATA_DIR'),
236 'runtime-reverse', pkg)
237 pkg_data = oe.packagedata.read_pkgdatafile(pkg_info)
238 recipies.add(pkg_data["PN"])
239
240 bb.note("Writing rootfs CVE manifest")
241 deploy_dir = d.getVar("IMGDEPLOYDIR")
242 link_name = d.getVar("IMAGE_LINK_NAME")
243
244 json_data = {"version":"1", "package": []}
245 text_data = ""
246 enable_json = d.getVar("CVE_CHECK_FORMAT_JSON") == "1"
247 enable_text = d.getVar("CVE_CHECK_FORMAT_TEXT") == "1"
248
249 save_pn = d.getVar("PN")
250
251 for pkg in recipies:
252 # To be able to use the CVE_CHECK_RECIPE_FILE variable we have to evaluate
253 # it with the different PN names set each time.
254 d.setVar("PN", pkg)
255 if enable_text:
256 pkgfilepath = d.getVar("CVE_CHECK_RECIPE_FILE")
257 if os.path.exists(pkgfilepath):
258 with open(pkgfilepath) as pfile:
259 text_data += pfile.read()
260
261 if enable_json:
262 pkgfilepath = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
263 if os.path.exists(pkgfilepath):
264 with open(pkgfilepath) as j:
265 data = json.load(j)
266 cve_check_merge_jsons(json_data, data)
267
268 d.setVar("PN", save_pn)
269
270 if enable_text:
271 link_path = os.path.join(deploy_dir, "%s.cve" % link_name)
142 manifest_name = d.getVar("CVE_CHECK_MANIFEST") 272 manifest_name = d.getVar("CVE_CHECK_MANIFEST")
143 cve_tmp_file = d.getVar("CVE_CHECK_TMP_FILE")
144 273
145 shutil.copyfile(cve_tmp_file, manifest_name) 274 with open(manifest_name, "w") as f:
275 f.write(text_data)
146 276
147 if manifest_name and os.path.exists(manifest_name): 277 update_symlinks(manifest_name, link_path)
148 manifest_link = os.path.join(deploy_dir, "%s.cve" % link_name) 278 bb.plain("Image CVE report stored in: %s" % manifest_name)
149 # If we already have another manifest, update symlinks
150 if os.path.exists(os.path.realpath(manifest_link)):
151 os.remove(manifest_link)
152 os.symlink(os.path.basename(manifest_name), manifest_link)
153 bb.plain("Image CVE report stored in: %s" % manifest_name)
154}
155 279
156ROOTFS_POSTPROCESS_COMMAND_prepend = "${@'cve_check_write_rootfs_manifest; ' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}" 280 if enable_json:
157do_rootfs[recrdeptask] += "${@'do_cve_check' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}" 281 link_path = os.path.join(deploy_dir, "%s.json" % link_name)
282 manifest_name = d.getVar("CVE_CHECK_MANIFEST_JSON")
158 283
159def get_patches_cves(d): 284 with open(manifest_name, "w") as f:
160 """ 285 json.dump(json_data, f, indent=2)
161 Get patches that solve CVEs using the "CVE: " tag.
162 """
163 286
164 import re 287 update_symlinks(manifest_name, link_path)
165 288 bb.plain("Image CVE JSON report stored in: %s" % manifest_name)
166 pn = d.getVar("PN") 289}
167 cve_match = re.compile("CVE:( CVE\-\d{4}\-\d+)+")
168
169 # Matches last CVE-1234-211432 in the file name, also if written
170 # with small letters. Not supporting multiple CVE id's in a single
171 # file name.
172 cve_file_name_match = re.compile(".*([Cc][Vv][Ee]\-\d{4}\-\d+)")
173
174 patched_cves = set()
175 bb.debug(2, "Looking for patches that solves CVEs for %s" % pn)
176 for url in src_patches(d):
177 patch_file = bb.fetch.decodeurl(url)[2]
178
179 if not os.path.isfile(patch_file):
180 bb.error("File Not found: %s" % patch_file)
181 raise FileNotFoundError
182
183 # Check patch file name for CVE ID
184 fname_match = cve_file_name_match.search(patch_file)
185 if fname_match:
186 cve = fname_match.group(1).upper()
187 patched_cves.add(cve)
188 bb.debug(2, "Found CVE %s from patch file name %s" % (cve, patch_file))
189
190 with open(patch_file, "r", encoding="utf-8") as f:
191 try:
192 patch_text = f.read()
193 except UnicodeDecodeError:
194 bb.debug(1, "Failed to read patch %s using UTF-8 encoding"
195 " trying with iso8859-1" % patch_file)
196 f.close()
197 with open(patch_file, "r", encoding="iso8859-1") as f:
198 patch_text = f.read()
199
200 # Search for one or more "CVE: " lines
201 text_match = False
202 for match in cve_match.finditer(patch_text):
203 # Get only the CVEs without the "CVE: " tag
204 cves = patch_text[match.start()+5:match.end()]
205 for cve in cves.split():
206 bb.debug(2, "Patch %s solves %s" % (patch_file, cve))
207 patched_cves.add(cve)
208 text_match = True
209
210 if not fname_match and not text_match:
211 bb.debug(2, "Patch %s doesn't solve CVEs" % patch_file)
212 290
213 return patched_cves 291ROOTFS_POSTPROCESS_COMMAND:prepend = "${@'cve_check_write_rootfs_manifest ' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}"
292do_rootfs[recrdeptask] += "${@'do_cve_check' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}"
293do_populate_sdk[recrdeptask] += "${@'do_cve_check' if d.getVar('CVE_CHECK_CREATE_MANIFEST') == '1' else ''}"
214 294
215def check_cves(d, patched_cves): 295def check_cves(d, patched_cves):
216 """ 296 """
217 Connect to the NVD database and find unpatched cves. 297 Connect to the NVD database and find unpatched cves.
218 """ 298 """
219 from oe.cve_check import Version 299 from oe.cve_check import Version, convert_cve_version, decode_cve_status
220 300
221 pn = d.getVar("PN") 301 pn = d.getVar("PN")
222 real_pv = d.getVar("PV") 302 real_pv = d.getVar("PV")
223 suffix = d.getVar("CVE_VERSION_SUFFIX") 303 suffix = d.getVar("CVE_VERSION_SUFFIX")
224 304
225 cves_unpatched = [] 305 cves_unpatched = []
306 cves_ignored = []
307 cves_status = []
308 cves_in_recipe = False
226 # CVE_PRODUCT can contain more than one product (eg. curl/libcurl) 309 # CVE_PRODUCT can contain more than one product (eg. curl/libcurl)
227 products = d.getVar("CVE_PRODUCT").split() 310 products = d.getVar("CVE_PRODUCT").split()
228 # If this has been unset then we're not scanning for CVEs here (for example, image recipes) 311 # If this has been unset then we're not scanning for CVEs here (for example, image recipes)
229 if not products: 312 if not products:
230 return ([], [], []) 313 return ([], [], [], [])
231 pv = d.getVar("CVE_VERSION").split("+git")[0] 314 pv = d.getVar("CVE_VERSION").split("+git")[0]
232 315
233 # If the recipe has been whitlisted we return empty lists 316 # If the recipe has been skipped/ignored we return empty lists
234 if pn in d.getVar("CVE_CHECK_PN_WHITELIST").split(): 317 if pn in d.getVar("CVE_CHECK_SKIP_RECIPE").split():
235 bb.note("Recipe has been whitelisted, skipping check") 318 bb.note("Recipe has been skipped by cve-check")
236 return ([], [], []) 319 return ([], [], [], [])
237 320
238 old_cve_whitelist = d.getVar("CVE_CHECK_CVE_WHITELIST") 321 # Convert CVE_STATUS into ignored CVEs and check validity
239 if old_cve_whitelist: 322 cve_ignore = []
240 bb.warn("CVE_CHECK_CVE_WHITELIST is deprecated, please use CVE_CHECK_WHITELIST.") 323 for cve in (d.getVarFlags("CVE_STATUS") or {}):
241 cve_whitelist = d.getVar("CVE_CHECK_WHITELIST").split() 324 decoded_status, _, _ = decode_cve_status(d, cve)
325 if decoded_status == "Ignored":
326 cve_ignore.append(cve)
242 327
243 import sqlite3 328 import sqlite3
244 db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro") 329 db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro")
@@ -246,28 +331,42 @@ def check_cves(d, patched_cves):
246 331
247 # For each of the known product names (e.g. curl has CPEs using curl and libcurl)... 332 # For each of the known product names (e.g. curl has CPEs using curl and libcurl)...
248 for product in products: 333 for product in products:
334 cves_in_product = False
249 if ":" in product: 335 if ":" in product:
250 vendor, product = product.split(":", 1) 336 vendor, product = product.split(":", 1)
251 else: 337 else:
252 vendor = "%" 338 vendor = "%"
253 339
254 # Find all relevant CVE IDs. 340 # Find all relevant CVE IDs.
255 for cverow in conn.execute("SELECT DISTINCT ID FROM PRODUCTS WHERE PRODUCT IS ? AND VENDOR LIKE ?", (product, vendor)): 341 cve_cursor = conn.execute("SELECT DISTINCT ID FROM PRODUCTS WHERE PRODUCT IS ? AND VENDOR LIKE ?", (product, vendor))
342 for cverow in cve_cursor:
256 cve = cverow[0] 343 cve = cverow[0]
257 344
258 if cve in cve_whitelist: 345 if cve in cve_ignore:
259 bb.note("%s-%s has been whitelisted for %s" % (product, pv, cve)) 346 bb.note("%s-%s ignores %s" % (product, pv, cve))
260 # TODO: this should be in the report as 'whitelisted' 347 cves_ignored.append(cve)
261 patched_cves.add(cve)
262 continue 348 continue
263 elif cve in patched_cves: 349 elif cve in patched_cves:
264 bb.note("%s has been patched" % (cve)) 350 bb.note("%s has been patched" % (cve))
265 continue 351 continue
352 # Write status once only for each product
353 if not cves_in_product:
354 cves_status.append([product, True])
355 cves_in_product = True
356 cves_in_recipe = True
266 357
267 vulnerable = False 358 vulnerable = False
268 for row in conn.execute("SELECT * FROM PRODUCTS WHERE ID IS ? AND PRODUCT IS ? AND VENDOR LIKE ?", (cve, product, vendor)): 359 ignored = False
360
361 product_cursor = conn.execute("SELECT * FROM PRODUCTS WHERE ID IS ? AND PRODUCT IS ? AND VENDOR LIKE ?", (cve, product, vendor))
362 for row in product_cursor:
269 (_, _, _, version_start, operator_start, version_end, operator_end) = row 363 (_, _, _, version_start, operator_start, version_end, operator_end) = row
270 #bb.debug(2, "Evaluating row " + str(row)) 364 #bb.debug(2, "Evaluating row " + str(row))
365 if cve in cve_ignore:
366 ignored = True
367
368 version_start = convert_cve_version(version_start)
369 version_end = convert_cve_version(version_end)
271 370
272 if (operator_start == '=' and pv == version_start) or version_start == '-': 371 if (operator_start == '=' and pv == version_start) or version_start == '-':
273 vulnerable = True 372 vulnerable = True
@@ -300,18 +399,33 @@ def check_cves(d, patched_cves):
300 vulnerable = vulnerable_start or vulnerable_end 399 vulnerable = vulnerable_start or vulnerable_end
301 400
302 if vulnerable: 401 if vulnerable:
303 bb.note("%s-%s is vulnerable to %s" % (pn, real_pv, cve)) 402 if ignored:
304 cves_unpatched.append(cve) 403 bb.note("%s is ignored in %s-%s" % (cve, pn, real_pv))
404 cves_ignored.append(cve)
405 else:
406 bb.note("%s-%s is vulnerable to %s" % (pn, real_pv, cve))
407 cves_unpatched.append(cve)
305 break 408 break
409 product_cursor.close()
306 410
307 if not vulnerable: 411 if not vulnerable:
308 bb.note("%s-%s is not vulnerable to %s" % (pn, real_pv, cve)) 412 bb.note("%s-%s is not vulnerable to %s" % (pn, real_pv, cve))
309 # TODO: not patched but not vulnerable
310 patched_cves.add(cve) 413 patched_cves.add(cve)
414 cve_cursor.close()
415
416 if not cves_in_product:
417 bb.note("No CVE records found for product %s, pn %s" % (product, pn))
418 cves_status.append([product, False])
311 419
312 conn.close() 420 conn.close()
421 diff_ignore = list(set(cve_ignore) - set(cves_ignored))
422 if diff_ignore:
423 oe.qa.handle_error("cve_status_not_in_db", "Found CVE (%s) with CVE_STATUS set that are not found in database for this component" % " ".join(diff_ignore), d)
313 424
314 return (list(cve_whitelist), list(patched_cves), cves_unpatched) 425 if not cves_in_recipe:
426 bb.note("No CVE records for products in recipe %s" % (pn))
427
428 return (list(cves_ignored), list(patched_cves), cves_unpatched, cves_status)
315 429
316def get_cve_info(d, cves): 430def get_cve_info(d, cves):
317 """ 431 """
@@ -321,26 +435,30 @@ def get_cve_info(d, cves):
321 import sqlite3 435 import sqlite3
322 436
323 cve_data = {} 437 cve_data = {}
324 conn = sqlite3.connect(d.getVar("CVE_CHECK_DB_FILE")) 438 db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro")
439 conn = sqlite3.connect(db_file, uri=True)
325 440
326 for cve in cves: 441 for cve in cves:
327 for row in conn.execute("SELECT * FROM NVD WHERE ID IS ?", (cve,)): 442 cursor = conn.execute("SELECT * FROM NVD WHERE ID IS ?", (cve,))
443 for row in cursor:
328 cve_data[row[0]] = {} 444 cve_data[row[0]] = {}
329 cve_data[row[0]]["summary"] = row[1] 445 cve_data[row[0]]["summary"] = row[1]
330 cve_data[row[0]]["scorev2"] = row[2] 446 cve_data[row[0]]["scorev2"] = row[2]
331 cve_data[row[0]]["scorev3"] = row[3] 447 cve_data[row[0]]["scorev3"] = row[3]
332 cve_data[row[0]]["modified"] = row[4] 448 cve_data[row[0]]["modified"] = row[4]
333 cve_data[row[0]]["vector"] = row[5] 449 cve_data[row[0]]["vector"] = row[5]
334 450 cve_data[row[0]]["vectorString"] = row[6]
451 cursor.close()
335 conn.close() 452 conn.close()
336 return cve_data 453 return cve_data
337 454
338def cve_write_data(d, patched, unpatched, whitelisted, cve_data): 455def cve_write_data_text(d, patched, unpatched, ignored, cve_data):
339 """ 456 """
340 Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and 457 Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and
341 CVE manifest if enabled. 458 CVE manifest if enabled.
342 """ 459 """
343 460
461 from oe.cve_check import decode_cve_status
344 462
345 cve_file = d.getVar("CVE_CHECK_LOG") 463 cve_file = d.getVar("CVE_CHECK_LOG")
346 fdir_name = d.getVar("FILE_DIRNAME") 464 fdir_name = d.getVar("FILE_DIRNAME")
@@ -349,55 +467,195 @@ def cve_write_data(d, patched, unpatched, whitelisted, cve_data):
349 include_layers = d.getVar("CVE_CHECK_LAYER_INCLUDELIST").split() 467 include_layers = d.getVar("CVE_CHECK_LAYER_INCLUDELIST").split()
350 exclude_layers = d.getVar("CVE_CHECK_LAYER_EXCLUDELIST").split() 468 exclude_layers = d.getVar("CVE_CHECK_LAYER_EXCLUDELIST").split()
351 469
470 report_all = d.getVar("CVE_CHECK_REPORT_PATCHED") == "1"
471
352 if exclude_layers and layer in exclude_layers: 472 if exclude_layers and layer in exclude_layers:
353 return 473 return
354 474
355 if include_layers and layer not in include_layers: 475 if include_layers and layer not in include_layers:
356 return 476 return
357 477
358 nvd_link = "https://web.nvd.nist.gov/view/vuln/detail?vulnId=" 478 # Early exit, the text format does not report packages without CVEs
479 if not patched+unpatched+ignored:
480 return
481
482 nvd_link = "https://nvd.nist.gov/vuln/detail/"
359 write_string = "" 483 write_string = ""
360 unpatched_cves = [] 484 unpatched_cves = []
361 bb.utils.mkdirhier(os.path.dirname(cve_file)) 485 bb.utils.mkdirhier(os.path.dirname(cve_file))
362 486
363 for cve in sorted(cve_data): 487 for cve in sorted(cve_data):
364 is_patched = cve in patched 488 is_patched = cve in patched
365 if is_patched and (d.getVar("CVE_CHECK_REPORT_PATCHED") != "1"): 489 is_ignored = cve in ignored
490
491 status = "Unpatched"
492 if (is_patched or is_ignored) and not report_all:
366 continue 493 continue
494 if is_ignored:
495 status = "Ignored"
496 elif is_patched:
497 status = "Patched"
498 else:
499 # default value of status is Unpatched
500 unpatched_cves.append(cve)
501
367 write_string += "LAYER: %s\n" % layer 502 write_string += "LAYER: %s\n" % layer
368 write_string += "PACKAGE NAME: %s\n" % d.getVar("PN") 503 write_string += "PACKAGE NAME: %s\n" % d.getVar("PN")
369 write_string += "PACKAGE VERSION: %s%s\n" % (d.getVar("EXTENDPE"), d.getVar("PV")) 504 write_string += "PACKAGE VERSION: %s%s\n" % (d.getVar("EXTENDPE"), d.getVar("PV"))
370 write_string += "CVE: %s\n" % cve 505 write_string += "CVE: %s\n" % cve
371 if cve in whitelisted: 506 write_string += "CVE STATUS: %s\n" % status
372 write_string += "CVE STATUS: Whitelisted\n" 507 _, detail, description = decode_cve_status(d, cve)
373 elif is_patched: 508 if detail:
374 write_string += "CVE STATUS: Patched\n" 509 write_string += "CVE DETAIL: %s\n" % detail
375 else: 510 if description:
376 unpatched_cves.append(cve) 511 write_string += "CVE DESCRIPTION: %s\n" % description
377 write_string += "CVE STATUS: Unpatched\n"
378 write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["summary"] 512 write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["summary"]
379 write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["scorev2"] 513 write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["scorev2"]
380 write_string += "CVSS v3 BASE SCORE: %s\n" % cve_data[cve]["scorev3"] 514 write_string += "CVSS v3 BASE SCORE: %s\n" % cve_data[cve]["scorev3"]
381 write_string += "VECTOR: %s\n" % cve_data[cve]["vector"] 515 write_string += "VECTOR: %s\n" % cve_data[cve]["vector"]
516 write_string += "VECTORSTRING: %s\n" % cve_data[cve]["vectorString"]
382 write_string += "MORE INFORMATION: %s%s\n\n" % (nvd_link, cve) 517 write_string += "MORE INFORMATION: %s%s\n\n" % (nvd_link, cve)
383 518
384 if unpatched_cves: 519 if unpatched_cves and d.getVar("CVE_CHECK_SHOW_WARNINGS") == "1":
385 bb.warn("Found unpatched CVE (%s), for more information check %s" % (" ".join(unpatched_cves),cve_file)) 520 bb.warn("Found unpatched CVE (%s), for more information check %s" % (" ".join(unpatched_cves),cve_file))
386 521
387 if write_string: 522 with open(cve_file, "w") as f:
388 with open(cve_file, "w") as f: 523 bb.note("Writing file %s with CVE information" % cve_file)
389 bb.note("Writing file %s with CVE information" % cve_file) 524 f.write(write_string)
525
526 if d.getVar("CVE_CHECK_COPY_FILES") == "1":
527 deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE")
528 bb.utils.mkdirhier(os.path.dirname(deploy_file))
529 with open(deploy_file, "w") as f:
390 f.write(write_string) 530 f.write(write_string)
391 531
392 if d.getVar("CVE_CHECK_COPY_FILES") == "1": 532 if d.getVar("CVE_CHECK_CREATE_MANIFEST") == "1":
393 deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE") 533 cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR")
394 bb.utils.mkdirhier(os.path.dirname(deploy_file)) 534 bb.utils.mkdirhier(cvelogpath)
395 with open(deploy_file, "w") as f: 535
396 f.write(write_string) 536 with open(d.getVar("CVE_CHECK_TMP_FILE"), "a") as f:
537 f.write("%s" % write_string)
538
539def cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file):
540 """
541 Write CVE information in the JSON format: to WORKDIR; and to
542 CVE_CHECK_DIR, if CVE manifest if enabled, write fragment
543 files that will be assembled at the end in cve_check_write_rootfs_manifest.
544 """
545
546 import json
547
548 write_string = json.dumps(output, indent=2)
549 with open(direct_file, "w") as f:
550 bb.note("Writing file %s with CVE information" % direct_file)
551 f.write(write_string)
552
553 if d.getVar("CVE_CHECK_COPY_FILES") == "1":
554 bb.utils.mkdirhier(os.path.dirname(deploy_file))
555 with open(deploy_file, "w") as f:
556 f.write(write_string)
557
558 if d.getVar("CVE_CHECK_CREATE_MANIFEST") == "1":
559 cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR")
560 index_path = d.getVar("CVE_CHECK_SUMMARY_INDEX_PATH")
561 bb.utils.mkdirhier(cvelogpath)
562 fragment_file = os.path.basename(deploy_file)
563 fragment_path = os.path.join(cvelogpath, fragment_file)
564 with open(fragment_path, "w") as f:
565 f.write(write_string)
566 with open(index_path, "a+") as f:
567 f.write("%s\n" % fragment_path)
568
569def cve_write_data_json(d, patched, unpatched, ignored, cve_data, cve_status):
570 """
571 Prepare CVE data for the JSON format, then write it.
572 """
573
574 from oe.cve_check import decode_cve_status
575
576 output = {"version":"1", "package": []}
577 nvd_link = "https://nvd.nist.gov/vuln/detail/"
397 578
398 if d.getVar("CVE_CHECK_CREATE_MANIFEST") == "1": 579 fdir_name = d.getVar("FILE_DIRNAME")
399 cvelogpath = d.getVar("CVE_CHECK_SUMMARY_DIR") 580 layer = fdir_name.split("/")[-3]
400 bb.utils.mkdirhier(cvelogpath) 581
582 include_layers = d.getVar("CVE_CHECK_LAYER_INCLUDELIST").split()
583 exclude_layers = d.getVar("CVE_CHECK_LAYER_EXCLUDELIST").split()
584
585 report_all = d.getVar("CVE_CHECK_REPORT_PATCHED") == "1"
586
587 if exclude_layers and layer in exclude_layers:
588 return
589
590 if include_layers and layer not in include_layers:
591 return
592
593 unpatched_cves = []
594
595 product_data = []
596 for s in cve_status:
597 p = {"product": s[0], "cvesInRecord": "Yes"}
598 if s[1] == False:
599 p["cvesInRecord"] = "No"
600 product_data.append(p)
601
602 package_version = "%s%s" % (d.getVar("EXTENDPE"), d.getVar("PV"))
603 package_data = {
604 "name" : d.getVar("PN"),
605 "layer" : layer,
606 "version" : package_version,
607 "products": product_data
608 }
609 cve_list = []
610
611 for cve in sorted(cve_data):
612 is_patched = cve in patched
613 is_ignored = cve in ignored
614 status = "Unpatched"
615 if (is_patched or is_ignored) and not report_all:
616 continue
617 if is_ignored:
618 status = "Ignored"
619 elif is_patched:
620 status = "Patched"
621 else:
622 # default value of status is Unpatched
623 unpatched_cves.append(cve)
624
625 issue_link = "%s%s" % (nvd_link, cve)
626
627 cve_item = {
628 "id" : cve,
629 "summary" : cve_data[cve]["summary"],
630 "scorev2" : cve_data[cve]["scorev2"],
631 "scorev3" : cve_data[cve]["scorev3"],
632 "vector" : cve_data[cve]["vector"],
633 "vectorString" : cve_data[cve]["vectorString"],
634 "status" : status,
635 "link": issue_link
636 }
637 _, detail, description = decode_cve_status(d, cve)
638 if detail:
639 cve_item["detail"] = detail
640 if description:
641 cve_item["description"] = description
642 cve_list.append(cve_item)
643
644 package_data["issue"] = cve_list
645 output["package"].append(package_data)
646
647 direct_file = d.getVar("CVE_CHECK_LOG_JSON")
648 deploy_file = d.getVar("CVE_CHECK_RECIPE_FILE_JSON")
649 manifest_file = d.getVar("CVE_CHECK_SUMMARY_FILE_NAME_JSON")
650
651 cve_check_write_json_output(d, output, direct_file, deploy_file, manifest_file)
652
653def cve_write_data(d, patched, unpatched, ignored, cve_data, status):
654 """
655 Write CVE data in each enabled format.
656 """
401 657
402 with open(d.getVar("CVE_CHECK_TMP_FILE"), "a") as f: 658 if d.getVar("CVE_CHECK_FORMAT_TEXT") == "1":
403 f.write("%s" % write_string) 659 cve_write_data_text(d, patched, unpatched, ignored, cve_data)
660 if d.getVar("CVE_CHECK_FORMAT_JSON") == "1":
661 cve_write_data_json(d, patched, unpatched, ignored, cve_data, status)