diff options
author | Mariano Lopez <mariano.lopez@linux.intel.com> | 2016-08-24 18:58:35 +0000 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2016-09-16 15:24:02 +0100 |
commit | d0b4ac310bb8d4bfaa4db06a12e404a0f7c7253d (patch) | |
tree | 6eb40fab029b558e9418a2fb348f7024a9be0ba0 | |
parent | 189673244b571ad4b9fb121fbffded0e4568d31b (diff) | |
download | poky-d0b4ac310bb8d4bfaa4db06a12e404a0f7c7253d.tar.gz |
cve-check.bbclass: Add class
This class adds a new task for all the recipes to use
cve-check-tool in order to look for public CVEs affecting
the packages generated.
It is possible to use this class when building an image,
building a recipe, or using the "world" or "universe" cases.
In order to use this class it must be inherited and it will
add the task automatically to every recipe.
[YOCTO #7515]
Co-authored by Ross Burton & Mariano Lopez
(From OE-Core rev: d98338075ec3a66acb8828e74711550d53b4d91b)
Signed-off-by: Mariano Lopez <mariano.lopez@linux.intel.com>
Signed-off-by: Ross Burton <ross.burton@intel.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
-rw-r--r-- | meta/classes/cve-check.bbclass | 267 |
1 files changed, 267 insertions, 0 deletions
diff --git a/meta/classes/cve-check.bbclass b/meta/classes/cve-check.bbclass new file mode 100644 index 0000000000..8251ca7c97 --- /dev/null +++ b/meta/classes/cve-check.bbclass | |||
@@ -0,0 +1,267 @@ | |||
1 | # This class is used to check recipes against public CVEs. | ||
2 | # | ||
3 | # In order to use this class just inherit the class in the | ||
4 | # local.conf file and it will add the cve_check task for | ||
5 | # every recipe. The task can be used per recipe, per image, | ||
6 | # or using the special cases "world" and "universe". The | ||
7 | # cve_check task will print a warning for every unpatched | ||
8 | # CVE found and generate a file in the recipe WORKDIR/cve | ||
9 | # directory. If an image is build it will generate a report | ||
10 | # in DEPLOY_DIR_IMAGE for all the packages used. | ||
11 | # | ||
12 | # Example: | ||
13 | # bitbake -c cve_check openssl | ||
14 | # bitbake core-image-sato | ||
15 | # bitbake -k -c cve_check universe | ||
16 | # | ||
17 | # DISCLAIMER | ||
18 | # | ||
19 | # This class/tool is meant to be used as support and not | ||
20 | # the only method to check against CVEs. Running this tool | ||
21 | # doesn't guarantee your packages are free of CVEs. | ||
22 | |||
23 | CVE_CHECK_DB_DIR ?= "${DL_DIR}/CVE_CHECK" | ||
24 | CVE_CHECK_DB_FILE ?= "${CVE_CHECK_DB_DIR}/nvd.db" | ||
25 | |||
26 | CVE_CHECK_LOCAL_DIR ?= "${WORKDIR}/cve" | ||
27 | CVE_CHECK_LOCAL_FILE ?= "${CVE_CHECK_LOCAL_DIR}/cve.log" | ||
28 | CVE_CHECK_TMP_FILE ?= "${TMPDIR}/cve_check" | ||
29 | |||
30 | CVE_CHECK_DIR ??= "${DEPLOY_DIR}/cve" | ||
31 | CVE_CHECK_MANIFEST ?= "${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}${IMAGE_NAME_SUFFIX}.cve" | ||
32 | CVE_CHECK_COPY_FILES ??= "1" | ||
33 | CVE_CHECK_CREATE_MANIFEST ??= "1" | ||
34 | |||
35 | # Whitelist for packages (PN) | ||
36 | CVE_CHECK_PN_WHITELIST = "\ | ||
37 | glibc-locale \ | ||
38 | " | ||
39 | |||
40 | # Whitelist for CVE and version of package | ||
41 | CVE_CHECK_CVE_WHITELIST = "{\ | ||
42 | 'CVE-2014-2524': ('6.3',), \ | ||
43 | }" | ||
44 | |||
45 | python do_cve_check () { | ||
46 | """ | ||
47 | Check recipe for patched and unpatched CVEs | ||
48 | """ | ||
49 | |||
50 | if os.path.exists(d.getVar("CVE_CHECK_TMP_FILE", True)): | ||
51 | patched_cves = get_patches_cves(d) | ||
52 | patched, unpatched = check_cves(d, patched_cves) | ||
53 | if patched or unpatched: | ||
54 | cve_data = get_cve_info(d, patched + unpatched) | ||
55 | cve_write_data(d, patched, unpatched, cve_data) | ||
56 | else: | ||
57 | bb.note("Failed to update CVE database, skipping CVE check") | ||
58 | } | ||
59 | |||
60 | addtask cve_check after do_unpack before do_build | ||
61 | do_cve_check[depends] = "cve-check-tool-native:do_populate_cve_db" | ||
62 | do_cve_check[nostamp] = "1" | ||
63 | |||
64 | python cve_check_cleanup () { | ||
65 | """ | ||
66 | Delete the file used to gather all the CVE information. | ||
67 | """ | ||
68 | |||
69 | bb.utils.remove(e.data.getVar("CVE_CHECK_TMP_FILE", True)) | ||
70 | } | ||
71 | |||
72 | addhandler cve_check_cleanup | ||
73 | cve_check_cleanup[eventmask] = "bb.cooker.CookerExit" | ||
74 | |||
75 | python cve_check_write_rootfs_manifest () { | ||
76 | """ | ||
77 | Create CVE manifest when building an image | ||
78 | """ | ||
79 | |||
80 | import shutil | ||
81 | |||
82 | if os.path.exists(d.getVar("CVE_CHECK_TMP_FILE", True)): | ||
83 | bb.note("Writing rootfs CVE manifest") | ||
84 | deploy_dir = d.getVar("DEPLOY_DIR_IMAGE", True) | ||
85 | link_name = d.getVar("IMAGE_LINK_NAME", True) | ||
86 | manifest_name = d.getVar("CVE_CHECK_MANIFEST", True) | ||
87 | cve_tmp_file = d.getVar("CVE_CHECK_TMP_FILE", True) | ||
88 | |||
89 | shutil.copyfile(cve_tmp_file, manifest_name) | ||
90 | |||
91 | if manifest_name and os.path.exists(manifest_name): | ||
92 | manifest_link = os.path.join(deploy_dir, "%s.cve" % link_name) | ||
93 | # If we already have another manifest, update symlinks | ||
94 | if os.path.exists(os.path.realpath(manifest_link)): | ||
95 | if d.getVar('RM_OLD_IMAGE', True) == "1": | ||
96 | os.remove(os.path.realpath(manifest_link)) | ||
97 | os.remove(manifest_link) | ||
98 | os.symlink(os.path.basename(manifest_name), manifest_link) | ||
99 | bb.plain("Image CVE report stored in: %s" % manifest_name) | ||
100 | } | ||
101 | |||
102 | ROOTFS_POSTPROCESS_COMMAND_prepend = "${@'cve_check_write_rootfs_manifest; ' if d.getVar('CVE_CHECK_CREATE_MANIFEST', True) == '1' else ''}" | ||
103 | |||
104 | def get_patches_cves(d): | ||
105 | """ | ||
106 | Get patches that solve CVEs using the "CVE: " tag. | ||
107 | """ | ||
108 | |||
109 | import re | ||
110 | |||
111 | pn = d.getVar("PN", True) | ||
112 | cve_match = re.compile("CVE:( CVE\-\d{4}\-\d+)+") | ||
113 | patched_cves = set() | ||
114 | bb.debug(2, "Looking for patches that solves CVEs for %s" % pn) | ||
115 | for url in src_patches(d): | ||
116 | patch_file = bb.fetch.decodeurl(url)[2] | ||
117 | with open(patch_file, "r", encoding="utf-8") as f: | ||
118 | try: | ||
119 | patch_text = f.read() | ||
120 | except UnicodeDecodeError: | ||
121 | bb.debug(1, "Failed to read patch %s using UTF-8 encoding" | ||
122 | " trying with iso8859-1" % patch_file) | ||
123 | f.close() | ||
124 | with open(patch_file, "r", encoding="iso8859-1") as f: | ||
125 | patch_text = f.read() | ||
126 | |||
127 | # Search for the "CVE: " line | ||
128 | match = cve_match.search(patch_text) | ||
129 | if match: | ||
130 | # Get only the CVEs without the "CVE: " tag | ||
131 | cves = patch_text[match.start()+5:match.end()] | ||
132 | for cve in cves.split(): | ||
133 | bb.debug(2, "Patch %s solves %s" % (patch_file, cve)) | ||
134 | patched_cves.add(cve) | ||
135 | else: | ||
136 | bb.debug(2, "Patch %s doesn't solve CVEs" % patch_file) | ||
137 | |||
138 | return patched_cves | ||
139 | |||
140 | def check_cves(d, patched_cves): | ||
141 | """ | ||
142 | Run cve-check-tool looking for patched and unpatched CVEs. | ||
143 | """ | ||
144 | |||
145 | import ast, csv, tempfile, subprocess, io | ||
146 | |||
147 | cves_patched = [] | ||
148 | cves_unpatched = [] | ||
149 | bpn = d.getVar("BPN", True) | ||
150 | pv = d.getVar("PV", True).split("git+")[0] | ||
151 | cves = " ".join(patched_cves) | ||
152 | cve_db_dir = d.getVar("CVE_CHECK_DB_DIR", True) | ||
153 | cve_whitelist = ast.literal_eval(d.getVar("CVE_CHECK_CVE_WHITELIST", True)) | ||
154 | cve_cmd = "cve-check-tool" | ||
155 | cmd = [cve_cmd, "--no-html", "--csv", "--not-affected", "-t", "faux", "-d", cve_db_dir] | ||
156 | |||
157 | # If the recipe has been whitlisted we return empty lists | ||
158 | if d.getVar("PN", True) in d.getVar("CVE_CHECK_PN_WHITELIST", True).split(): | ||
159 | bb.note("Recipe has been whitelisted, skipping check") | ||
160 | return ([], []) | ||
161 | |||
162 | # It is needed to export the proxies to download the database using HTTP | ||
163 | bb.utils.export_proxies(d) | ||
164 | |||
165 | try: | ||
166 | # Write the faux CSV file to be used with cve-check-tool | ||
167 | fd, faux = tempfile.mkstemp(prefix="cve-faux-") | ||
168 | with os.fdopen(fd, "w") as f: | ||
169 | f.write("%s,%s,%s," % (bpn, pv, cves)) | ||
170 | cmd.append(faux) | ||
171 | |||
172 | output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode("utf-8") | ||
173 | bb.debug(2, "Output of command %s:\n%s" % ("\n".join(cmd), output)) | ||
174 | except subprocess.CalledProcessError as e: | ||
175 | bb.warn("Couldn't check for CVEs: %s (output %s)" % (e, e.output)) | ||
176 | finally: | ||
177 | os.remove(faux) | ||
178 | |||
179 | for row in csv.reader(io.StringIO(output)): | ||
180 | # Third row has the unpatched CVEs | ||
181 | if row[2]: | ||
182 | for cve in row[2].split(): | ||
183 | # Skip if the CVE has been whitlisted for the current version | ||
184 | if pv in cve_whitelist.get(cve,[]): | ||
185 | bb.note("%s-%s has been whitelisted for %s" % (bpn, pv, cve)) | ||
186 | else: | ||
187 | cves_unpatched.append(cve) | ||
188 | bb.debug(2, "%s-%s is not patched for %s" % (bpn, pv, cve)) | ||
189 | # Fourth row has patched CVEs | ||
190 | if row[3]: | ||
191 | for cve in row[3].split(): | ||
192 | cves_patched.append(cve) | ||
193 | bb.debug(2, "%s-%s is patched for %s" % (bpn, pv, cve)) | ||
194 | |||
195 | return (cves_patched, cves_unpatched) | ||
196 | |||
197 | def get_cve_info(d, cves): | ||
198 | """ | ||
199 | Get CVE information from the database used by cve-check-tool. | ||
200 | |||
201 | Unfortunately the only way to get CVE info is set the output to | ||
202 | html (hard to parse) or query directly the database. | ||
203 | """ | ||
204 | |||
205 | try: | ||
206 | import sqlite3 | ||
207 | except ImportError: | ||
208 | from pysqlite2 import dbapi2 as sqlite3 | ||
209 | |||
210 | cve_data = {} | ||
211 | db_file = d.getVar("CVE_CHECK_DB_FILE", True) | ||
212 | placeholder = ",".join("?" * len(cves)) | ||
213 | query = "SELECT * FROM NVD WHERE id IN (%s)" % placeholder | ||
214 | conn = sqlite3.connect(db_file) | ||
215 | cur = conn.cursor() | ||
216 | for row in cur.execute(query, tuple(cves)): | ||
217 | cve_data[row[0]] = {} | ||
218 | cve_data[row[0]]["summary"] = row[1] | ||
219 | cve_data[row[0]]["score"] = row[2] | ||
220 | cve_data[row[0]]["modified"] = row[3] | ||
221 | cve_data[row[0]]["vector"] = row[4] | ||
222 | conn.close() | ||
223 | |||
224 | return cve_data | ||
225 | |||
226 | def cve_write_data(d, patched, unpatched, cve_data): | ||
227 | """ | ||
228 | Write CVE information in WORKDIR; and to CVE_CHECK_DIR, and | ||
229 | CVE manifest if enabled. | ||
230 | """ | ||
231 | |||
232 | cve_file = d.getVar("CVE_CHECK_LOCAL_FILE", True) | ||
233 | nvd_link = "https://web.nvd.nist.gov/view/vuln/detail?vulnId=" | ||
234 | write_string = "" | ||
235 | first_alert = True | ||
236 | bb.utils.mkdirhier(d.getVar("CVE_CHECK_LOCAL_DIR", True)) | ||
237 | |||
238 | for cve in sorted(cve_data): | ||
239 | write_string += "PACKAGE NAME: %s\n" % d.getVar("PN", True) | ||
240 | write_string += "PACKAGE VERSION: %s\n" % d.getVar("PV", True) | ||
241 | write_string += "CVE: %s\n" % cve | ||
242 | if cve in patched: | ||
243 | write_string += "CVE STATUS: Patched\n" | ||
244 | else: | ||
245 | write_string += "CVE STATUS: Unpatched\n" | ||
246 | if first_alert: | ||
247 | bb.warn("Found unpatched CVE, for more information check %s" % cve_file) | ||
248 | first_alert = False | ||
249 | write_string += "CVE SUMMARY: %s\n" % cve_data[cve]["summary"] | ||
250 | write_string += "CVSS v2 BASE SCORE: %s\n" % cve_data[cve]["score"] | ||
251 | write_string += "VECTOR: %s\n" % cve_data[cve]["vector"] | ||
252 | write_string += "MORE INFORMATION: %s%s\n\n" % (nvd_link, cve) | ||
253 | |||
254 | with open(cve_file, "w") as f: | ||
255 | bb.note("Writing file %s with CVE information" % cve_file) | ||
256 | f.write(write_string) | ||
257 | |||
258 | if d.getVar("CVE_CHECK_COPY_FILES", True) == "1": | ||
259 | cve_dir = d.getVar("CVE_CHECK_DIR", True) | ||
260 | bb.utils.mkdirhier(cve_dir) | ||
261 | deploy_file = os.path.join(cve_dir, d.getVar("PN", True)) | ||
262 | with open(deploy_file, "w") as f: | ||
263 | f.write(write_string) | ||
264 | |||
265 | if d.getVar("CVE_CHECK_CREATE_MANIFEST", True) == "1": | ||
266 | with open(d.getVar("CVE_CHECK_TMP_FILE", True), "a") as f: | ||
267 | f.write("%s" % write_string) | ||