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.bbclass181
1 files changed, 107 insertions, 74 deletions
diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass
index 743bc08a4f..19ed5548b3 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,39 +37,39 @@ 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 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 () {
69 """ 70 """
70 Delete the file used to gather all the CVE information. 71 Delete the file used to gather all the CVE information.
71 """ 72 """
72
73 bb.utils.remove(e.data.getVar("CVE_CHECK_TMP_FILE")) 73 bb.utils.remove(e.data.getVar("CVE_CHECK_TMP_FILE"))
74} 74}
75 75
@@ -163,89 +163,121 @@ def get_patches_cves(d):
163 163
164def check_cves(d, patched_cves): 164def check_cves(d, patched_cves):
165 """ 165 """
166 Run cve-check-tool looking for patched and unpatched CVEs. 166 Connect to the NVD database and find unpatched cves.
167 """ 167 """
168 from distutils.version import LooseVersion
168 169
169 import ast, csv, tempfile, subprocess, io
170
171 cves_patched = []
172 cves_unpatched = [] 170 cves_unpatched = []
173 bpn = d.getVar("CVE_PRODUCT") 171 # CVE_PRODUCT can contain more than one product (eg. curl/libcurl)
172 products = d.getVar("CVE_PRODUCT").split()
174 # If this has been unset then we're not scanning for CVEs here (for example, image recipes) 173 # If this has been unset then we're not scanning for CVEs here (for example, image recipes)
175 if not bpn: 174 if not products:
176 return ([], []) 175 return ([], [])
177 pv = d.getVar("CVE_VERSION").split("+git")[0] 176 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 177
184 # If the recipe has been whitlisted we return empty lists 178 # If the recipe has been whitlisted we return empty lists
185 if d.getVar("PN") in d.getVar("CVE_CHECK_PN_WHITELIST").split(): 179 if d.getVar("PN") in d.getVar("CVE_CHECK_PN_WHITELIST").split():
186 bb.note("Recipe has been whitelisted, skipping check") 180 bb.note("Recipe has been whitelisted, skipping check")
187 return ([], []) 181 return ([], [])
188 182
189 try: 183 old_cve_whitelist = d.getVar("CVE_CHECK_CVE_WHITELIST")
190 # Write the faux CSV file to be used with cve-check-tool 184 if old_cve_whitelist:
191 fd, faux = tempfile.mkstemp(prefix="cve-faux-") 185 bb.warn("CVE_CHECK_CVE_WHITELIST is deprecated, please use CVE_CHECK_WHITELIST.")
192 with os.fdopen(fd, "w") as f: 186 cve_whitelist = d.getVar("CVE_CHECK_WHITELIST").split()
193 for pn in bpn.split(): 187
194 f.write("%s,%s,%s,\n" % (pn, pv, cves)) 188 import sqlite3
195 cmd.append(faux) 189 db_file = d.expand("file:${CVE_CHECK_DB_FILE}?mode=ro")
196 190 conn = sqlite3.connect(db_file, uri=True)
197 output = subprocess.check_output(cmd).decode("utf-8") 191
198 bb.debug(2, "Output of command %s:\n%s" % ("\n".join(cmd), output)) 192 # For each of the known product names (e.g. curl has CPEs using curl and libcurl)...
199 except subprocess.CalledProcessError as e: 193 for product in products:
200 bb.warn("Couldn't check for CVEs: %s (output %s)" % (e, e.output)) 194 if ":" in product:
201 finally: 195 vendor, product = product.split(":", 1)
202 os.remove(faux) 196 else:
203 197 vendor = "%"
204 for row in csv.reader(io.StringIO(output)): 198
205 # Third row has the unpatched CVEs 199 # Find all relevant CVE IDs.
206 if row[2]: 200 for cverow in conn.execute("SELECT DISTINCT ID FROM PRODUCTS WHERE PRODUCT IS ? AND VENDOR LIKE ?", (product, vendor)):
207 for cve in row[2].split(): 201 cve = cverow[0]
208 # Skip if the CVE has been whitlisted for the current version 202
209 if pv in cve_whitelist.get(cve,[]): 203 if cve in cve_whitelist:
210 bb.note("%s-%s has been whitelisted for %s" % (bpn, pv, cve)) 204 bb.note("%s-%s has been whitelisted for %s" % (product, pv, cve))
205 # TODO: this should be in the report as 'whitelisted'
206 patched_cves.add(cve)
207 continue
208 elif cve in patched_cves:
209 bb.note("%s has been patched" % (cve))
210 continue
211
212 vulnerable = False
213 for row in conn.execute("SELECT * FROM PRODUCTS WHERE ID IS ? AND PRODUCT IS ? AND VENDOR LIKE ?", (cve, product, vendor)):
214 (_, _, _, version_start, operator_start, version_end, operator_end) = row
215 #bb.debug(2, "Evaluating row " + str(row))
216
217 if (operator_start == '=' and pv == version_start):
218 vulnerable = True
211 else: 219 else:
220 if operator_start:
221 try:
222 vulnerable_start = (operator_start == '>=' and LooseVersion(pv) >= LooseVersion(version_start))
223 vulnerable_start |= (operator_start == '>' and LooseVersion(pv) > LooseVersion(version_start))
224 except:
225 bb.warn("%s: Failed to compare %s %s %s for %s" %
226 (product, pv, operator_start, version_start, cve))
227 vulnerable_start = False
228 else:
229 vulnerable_start = False
230
231 if operator_end:
232 try:
233 vulnerable_end = (operator_end == '<=' and LooseVersion(pv) <= LooseVersion(version_end))
234 vulnerable_end |= (operator_end == '<' and LooseVersion(pv) < LooseVersion(version_end))
235 except:
236 bb.warn("%s: Failed to compare %s %s %s for %s" %
237 (product, pv, operator_end, version_end, cve))
238 vulnerable_end = False
239 else:
240 vulnerable_end = False
241
242 if operator_start and operator_end:
243 vulnerable = vulnerable_start and vulnerable_end
244 else:
245 vulnerable = vulnerable_start or vulnerable_end
246
247 if vulnerable:
248 bb.note("%s-%s is vulnerable to %s" % (product, pv, cve))
212 cves_unpatched.append(cve) 249 cves_unpatched.append(cve)
213 bb.debug(2, "%s-%s is not patched for %s" % (bpn, pv, cve)) 250 break
214 # Fourth row has patched CVEs 251
215 if row[3]: 252 if not vulnerable:
216 for cve in row[3].split(): 253 bb.note("%s-%s is not vulnerable to %s" % (product, pv, cve))
217 cves_patched.append(cve) 254 # TODO: not patched but not vulnerable
218 bb.debug(2, "%s-%s is patched for %s" % (bpn, pv, cve)) 255 patched_cves.add(cve)
256
257 conn.close()
219 258
220 return (cves_patched, cves_unpatched) 259 return (list(patched_cves), cves_unpatched)
221 260
222def get_cve_info(d, cves): 261def get_cve_info(d, cves):
223 """ 262 """
224 Get CVE information from the database used by cve-check-tool. 263 Get CVE information from the database.
225
226 Unfortunately the only way to get CVE info is set the output to
227 html (hard to parse) or query directly the database.
228 """ 264 """
229 265
230 try: 266 import sqlite3
231 import sqlite3
232 except ImportError:
233 from pysqlite2 import dbapi2 as sqlite3
234 267
235 cve_data = {} 268 cve_data = {}
236 db_file = d.getVar("CVE_CHECK_DB_FILE") 269 conn = sqlite3.connect(d.getVar("CVE_CHECK_DB_FILE"))
237 placeholder = ",".join("?" * len(cves))
238 query = "SELECT * FROM NVD WHERE id IN (%s)" % placeholder
239 conn = sqlite3.connect(db_file)
240 cur = conn.cursor()
241 for row in cur.execute(query, tuple(cves)):
242 cve_data[row[0]] = {}
243 cve_data[row[0]]["summary"] = row[1]
244 cve_data[row[0]]["score"] = row[2]
245 cve_data[row[0]]["modified"] = row[3]
246 cve_data[row[0]]["vector"] = row[4]
247 conn.close()
248 270
271 for cve in cves:
272 for row in conn.execute("SELECT * FROM NVD WHERE ID IS ?", (cve,)):
273 cve_data[row[0]] = {}
274 cve_data[row[0]]["summary"] = row[1]
275 cve_data[row[0]]["scorev2"] = row[2]
276 cve_data[row[0]]["scorev3"] = row[3]
277 cve_data[row[0]]["modified"] = row[4]
278 cve_data[row[0]]["vector"] = row[5]
279
280 conn.close()
249 return cve_data 281 return cve_data
250 282
251def cve_write_data(d, patched, unpatched, cve_data): 283def cve_write_data(d, patched, unpatched, cve_data):
@@ -270,7 +302,8 @@ def cve_write_data(d, patched, unpatched, cve_data):
270 unpatched_cves.append(cve) 302 unpatched_cves.append(cve)
271 write_string += "CVE STATUS: Unpatched\n" 303 write_string += "CVE STATUS: Unpatched\n"
272 write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["summary"] 304 write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["summary"]
273 write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["score"] 305 write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["scorev2"]
306 write_string += "CVSS v3 BASE SCORE: %s\n" % cve_data[cve]["scorev3"]
274 write_string += "VECTOR: %s\n" % cve_data[cve]["vector"] 307 write_string += "VECTOR: %s\n" % cve_data[cve]["vector"]
275 write_string += "MORE INFORMATION: %s%s\n\n" % (nvd_link, cve) 308 write_string += "MORE INFORMATION: %s%s\n\n" % (nvd_link, cve)
276 309