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.bbclass142
1 files changed, 87 insertions, 55 deletions
diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
index 743bc08a4f..c00d2910be 100644
--- a/meta/classes/cve-check.bbclass
+++ b/meta/classes/cve-check.bbclass
@@ -26,7 +26,7 @@ CVE_PRODUCT ??= "${BPN}"
26CVE_VERSION ??= "${PV}" 26CVE_VERSION ??= "${PV}"
27 27
28CVE_CHECK_DB_DIR ?= "${DL_DIR}/CVE_CHECK" 28CVE_CHECK_DB_DIR ?= "${DL_DIR}/CVE_CHECK"
29CVE_CHECK_DB_FILE ?= "${CVE_CHECK_DB_DIR}/nvd.db" 29CVE_CHECK_DB_FILE ?= "${CVE_CHECK_DB_DIR}/nvdcve_1.0.db"
30 30
31CVE_CHECK_LOG ?= "${T}/cve.log" 31CVE_CHECK_LOG ?= "${T}/cve.log"
32CVE_CHECK_TMP_FILE ?= "${TMPDIR}/cve_check" 32CVE_CHECK_TMP_FILE ?= "${TMPDIR}/cve_check"
@@ -37,32 +37,33 @@ CVE_CHECK_COPY_FILES ??= "1"
37CVE_CHECK_CREATE_MANIFEST ??= "1" 37CVE_CHECK_CREATE_MANIFEST ??= "1"
38 38
39# Whitelist for packages (PN) 39# Whitelist for packages (PN)
40CVE_CHECK_PN_WHITELIST = "\ 40CVE_CHECK_PN_WHITELIST ?= ""
41 glibc-locale \
42"
43 41
44# Whitelist for CVE and version of package 42# Whitelist for CVE. If a CVE is found, then it is considered patched.
45CVE_CHECK_CVE_WHITELIST = "{\ 43# The value is a string containing space separated CVE values:
46 'CVE-2014-2524': ('6.3','5.2',), \ 44#
47}" 45# CVE_CHECK_WHITELIST = 'CVE-2014-2524 CVE-2018-1234'
46#
47CVE_CHECK_WHITELIST ?= ""
48 48
49python do_cve_check () { 49python do_cve_check () {
50 """ 50 """
51 Check recipe for patched and unpatched CVEs 51 Check recipe for patched and unpatched CVEs
52 """ 52 """
53 53
54 if os.path.exists(d.getVar("CVE_CHECK_TMP_FILE")): 54 if os.path.exists(d.getVar("CVE_CHECK_DB_FILE")):
55 patched_cves = get_patches_cves(d) 55 patched_cves = get_patches_cves(d)
56 patched, unpatched = check_cves(d, patched_cves) 56 patched, unpatched = check_cves(d, patched_cves)
57 if patched or unpatched: 57 if patched or unpatched:
58 cve_data = get_cve_info(d, patched + unpatched) 58 cve_data = get_cve_info(d, patched + unpatched)
59 cve_write_data(d, patched, unpatched, cve_data) 59 cve_write_data(d, patched, unpatched, cve_data)
60 else: 60 else:
61 bb.note("Failed to update CVE database, skipping CVE check") 61 bb.note("No CVE database found, skipping CVE check")
62
62} 63}
63 64
64addtask cve_check after do_unpack before do_build 65addtask cve_check after do_unpack before do_build
65do_cve_check[depends] = "cve-check-tool-native:do_populate_sysroot cve-check-tool-native:do_populate_cve_db" 66do_cve_check[depends] = "cve-update-db-native:do_populate_cve_db"
66do_cve_check[nostamp] = "1" 67do_cve_check[nostamp] = "1"
67 68
68python cve_check_cleanup () { 69python cve_check_cleanup () {
@@ -163,65 +164,94 @@ def get_patches_cves(d):
163 164
164def check_cves(d, patched_cves): 165def check_cves(d, patched_cves):
165 """ 166 """
166 Run cve-check-tool looking for patched and unpatched CVEs. 167 Connect to the NVD database and find unpatched cves.
167 """ 168 """
168
169 import ast, csv, tempfile, subprocess, io 169 import ast, csv, tempfile, subprocess, io
170 from distutils.version import LooseVersion
170 171
171 cves_patched = []
172 cves_unpatched = [] 172 cves_unpatched = []
173 bpn = d.getVar("CVE_PRODUCT") 173 # CVE_PRODUCT can contain more than one product (eg. curl/libcurl)
174 products = d.getVar("CVE_PRODUCT").split()
174 # If this has been unset then we're not scanning for CVEs here (for example, image recipes) 175 # If this has been unset then we're not scanning for CVEs here (for example, image recipes)
175 if not bpn: 176 if not products:
176 return ([], []) 177 return ([], [])
177 pv = d.getVar("CVE_VERSION").split("+git")[0] 178 pv = d.getVar("CVE_VERSION").split("+git")[0]
178 cves = " ".join(patched_cves)
179 cve_db_dir = d.getVar("CVE_CHECK_DB_DIR")
180 cve_whitelist = ast.literal_eval(d.getVar("CVE_CHECK_CVE_WHITELIST"))
181 cve_cmd = "cve-check-tool"
182 cmd = [cve_cmd, "--no-html", "--skip-update", "--csv", "--not-affected", "-t", "faux", "-d", cve_db_dir]
183 179
184 # If the recipe has been whitlisted we return empty lists 180 # If the recipe has been whitlisted we return empty lists
185 if d.getVar("PN") in d.getVar("CVE_CHECK_PN_WHITELIST").split(): 181 if d.getVar("PN") in d.getVar("CVE_CHECK_PN_WHITELIST").split():
186 bb.note("Recipe has been whitelisted, skipping check") 182 bb.note("Recipe has been whitelisted, skipping check")
187 return ([], []) 183 return ([], [])
188 184
189 try: 185 old_cve_whitelist = d.getVar("CVE_CHECK_CVE_WHITELIST")
190 # Write the faux CSV file to be used with cve-check-tool 186 if old_cve_whitelist:
191 fd, faux = tempfile.mkstemp(prefix="cve-faux-") 187 bb.warn("CVE_CHECK_CVE_WHITELIST is deprecated, please use CVE_CHECK_WHITELIST.")
192 with os.fdopen(fd, "w") as f: 188 cve_whitelist = d.getVar("CVE_CHECK_WHITELIST").split()
193 for pn in bpn.split(): 189
194 f.write("%s,%s,%s,\n" % (pn, pv, cves)) 190 import sqlite3
195 cmd.append(faux) 191 db_file = d.getVar("CVE_CHECK_DB_FILE")
196 192 conn = sqlite3.connect(db_file)
197 output = subprocess.check_output(cmd).decode("utf-8") 193
198 bb.debug(2, "Output of command %s:\n%s" % ("\n".join(cmd), output)) 194 for product in products:
199 except subprocess.CalledProcessError as e: 195 c = conn.cursor()
200 bb.warn("Couldn't check for CVEs: %s (output %s)" % (e, e.output)) 196 if ":" in product:
201 finally: 197 vendor, product = product.split(":", 1)
202 os.remove(faux) 198 c.execute("SELECT * FROM PRODUCTS WHERE PRODUCT IS ? AND VENDOR IS ?", (product, vendor))
203 199 else:
204 for row in csv.reader(io.StringIO(output)): 200 c.execute("SELECT * FROM PRODUCTS WHERE PRODUCT IS ?", (product,))
205 # Third row has the unpatched CVEs 201
206 if row[2]: 202 for row in c:
207 for cve in row[2].split(): 203 cve = row[0]
208 # Skip if the CVE has been whitlisted for the current version 204 version_start = row[3]
209 if pv in cve_whitelist.get(cve,[]): 205 operator_start = row[4]
210 bb.note("%s-%s has been whitelisted for %s" % (bpn, pv, cve)) 206 version_end = row[5]
207 operator_end = row[6]
208
209 if cve in cve_whitelist:
210 bb.note("%s-%s has been whitelisted for %s" % (product, pv, cve))
211 elif cve in patched_cves:
212 bb.note("%s has been patched" % (cve))
213 else:
214 to_append = False
215 if (operator_start == '=' and pv == version_start):
216 cves_unpatched.append(cve)
211 else: 217 else:
218 if operator_start:
219 try:
220 to_append_start = (operator_start == '>=' and LooseVersion(pv) >= LooseVersion(version_start))
221 to_append_start |= (operator_start == '>' and LooseVersion(pv) > LooseVersion(version_start))
222 except:
223 bb.note("%s: Failed to compare %s %s %s for %s" %
224 (product, pv, operator_start, version_start, cve))
225 to_append_start = False
226 else:
227 to_append_start = False
228
229 if operator_end:
230 try:
231 to_append_end = (operator_end == '<=' and LooseVersion(pv) <= LooseVersion(version_end))
232 to_append_end |= (operator_end == '<' and LooseVersion(pv) < LooseVersion(version_end))
233 except:
234 bb.note("%s: Failed to compare %s %s %s for %s" %
235 (product, pv, operator_end, version_end, cve))
236 to_append_end = False
237 else:
238 to_append_end = False
239
240 if operator_start and operator_end:
241 to_append = to_append_start and to_append_end
242 else:
243 to_append = to_append_start or to_append_end
244
245 if to_append:
212 cves_unpatched.append(cve) 246 cves_unpatched.append(cve)
213 bb.debug(2, "%s-%s is not patched for %s" % (bpn, pv, cve)) 247 bb.debug(2, "%s-%s is not patched for %s" % (product, pv, cve))
214 # Fourth row has patched CVEs 248 conn.close()
215 if row[3]:
216 for cve in row[3].split():
217 cves_patched.append(cve)
218 bb.debug(2, "%s-%s is patched for %s" % (bpn, pv, cve))
219 249
220 return (cves_patched, cves_unpatched) 250 return (list(patched_cves), cves_unpatched)
221 251
222def get_cve_info(d, cves): 252def get_cve_info(d, cves):
223 """ 253 """
224 Get CVE information from the database used by cve-check-tool. 254 Get CVE information from the database.
225 255
226 Unfortunately the only way to get CVE info is set the output to 256 Unfortunately the only way to get CVE info is set the output to
227 html (hard to parse) or query directly the database. 257 html (hard to parse) or query directly the database.
@@ -241,9 +271,10 @@ def get_cve_info(d, cves):
241 for row in cur.execute(query, tuple(cves)): 271 for row in cur.execute(query, tuple(cves)):
242 cve_data[row[0]] = {} 272 cve_data[row[0]] = {}
243 cve_data[row[0]]["summary"] = row[1] 273 cve_data[row[0]]["summary"] = row[1]
244 cve_data[row[0]]["score"] = row[2] 274 cve_data[row[0]]["scorev2"] = row[2]
245 cve_data[row[0]]["modified"] = row[3] 275 cve_data[row[0]]["scorev3"] = row[3]
246 cve_data[row[0]]["vector"] = row[4] 276 cve_data[row[0]]["modified"] = row[4]
277 cve_data[row[0]]["vector"] = row[5]
247 conn.close() 278 conn.close()
248 279
249 return cve_data 280 return cve_data
@@ -270,7 +301,8 @@ def cve_write_data(d, patched, unpatched, cve_data):
270 unpatched_cves.append(cve) 301 unpatched_cves.append(cve)
271 write_string += "CVE STATUS: Unpatched\n" 302 write_string += "CVE STATUS: Unpatched\n"
272 write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["summary"] 303 write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["summary"]
273 write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["score"] 304 write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["scorev2"]
305 write_string += "CVSS v3 BASE SCORE: %s\n" % cve_data[cve]["scorev3"]
274 write_string += "VECTOR: %s\n" % cve_data[cve]["vector"] 306 write_string += "VECTOR: %s\n" % cve_data[cve]["vector"]
275 write_string += "MORE INFORMATION: %s%s\n\n" % (nvd_link, cve) 307 write_string += "MORE INFORMATION: %s%s\n\n" % (nvd_link, cve)
276 308