diff options
Diffstat (limited to 'scripts/cvert-kernel')
| -rwxr-xr-x | scripts/cvert-kernel | 379 |
1 files changed, 379 insertions, 0 deletions
diff --git a/scripts/cvert-kernel b/scripts/cvert-kernel new file mode 100755 index 0000000..adf2692 --- /dev/null +++ b/scripts/cvert-kernel | |||
| @@ -0,0 +1,379 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # Copyright (c) 2018 by Cisco Systems, Inc. | ||
| 4 | # | ||
| 5 | # This program is free software; you can redistribute it and/or modify | ||
| 6 | # it under the terms of the GNU General Public License version 2 as | ||
| 7 | # published by the Free Software Foundation. | ||
| 8 | # | ||
| 9 | # This program is distributed in the hope that it will be useful, | ||
| 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 12 | # GNU General Public License for more details. | ||
| 13 | # | ||
| 14 | # You should have received a copy of the GNU General Public License along | ||
| 15 | # with this program; if not, write to the Free Software Foundation, Inc., | ||
| 16 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
| 17 | # | ||
| 18 | |||
| 19 | """ Generate CVE report for the given Linux kernel GIT branch | ||
| 20 | """ | ||
| 21 | |||
| 22 | import re | ||
| 23 | import sys | ||
| 24 | import argparse | ||
| 25 | import textwrap | ||
| 26 | import subprocess | ||
| 27 | import logging | ||
| 28 | import logging.config | ||
| 29 | import cvert | ||
| 30 | |||
| 31 | |||
| 32 | def report_kernel(): | ||
| 33 | """Generate Linux kernel CVE report""" | ||
| 34 | |||
| 35 | parser = argparse.ArgumentParser( | ||
| 36 | formatter_class=argparse.RawDescriptionHelpFormatter, | ||
| 37 | description=textwrap.dedent(""" | ||
| 38 | Generate CVE report for the Linux kernel. | ||
| 39 | Inspect Linux kernel GIT tree and find all CVE patches commits. | ||
| 40 | """), | ||
| 41 | epilog=textwrap.dedent(""" | ||
| 42 | @ run examples: | ||
| 43 | |||
| 44 | # Download (update) NVD feeds in "nvdfeed" directory | ||
| 45 | # and prepare the report for the "kernel-sources" directory | ||
| 46 | %% %(prog)s --feed-dir nvdfeed --output report-kernel.txt kernel-sources | ||
| 47 | |||
| 48 | # Use existed NVD feeds in "nvdfeed" directory | ||
| 49 | # and prepare the report for the "kernel-sources" directory | ||
| 50 | %% %(prog)s --offline --feed-dir nvdfeed --output report-kernel.txt kernel-sources | ||
| 51 | |||
| 52 | # (faster) Restore CVE dump from "cvedump" (must exist) | ||
| 53 | # and prepare the report for the "kernel-sources" directory | ||
| 54 | %% %(prog)s --restore cvedump --output report-kernel.txt kernel-sources | ||
| 55 | |||
| 56 | # Restore CVE dump from "cvedump" (must exist) | ||
| 57 | # and prepare the extended report for the "kernel-sources" directory | ||
| 58 | %% %(prog)s --restore cvedump --show-description --show-reference --output report-kernel.txt kernel-sources | ||
| 59 | |||
| 60 | @ report example output (NVD resource): | ||
| 61 | |||
| 62 | # 2018-10-10 15:41:52,213 %% CVERT %% INFO %% kernel version: 4.9.132 | ||
| 63 | . patched | 3.3 | CVE-2017-17807 | nvd: KEYS: add missing permission check for request_key() destination | ||
| 64 | unpatched | 3.3 | CVE-2017-17864 | | ||
| 65 | unpatched | 4.4 | CVE-2016-9604 | | ||
| 66 | unpatched | 4.4 | CVE-2017-12153 | | ||
| 67 | unpatched | 4.4 | CVE-2017-14051 | | ||
| 68 | . patched | 4.6 | CVE-2017-8924 | nvd: USB: serial: io_ti: fix information leak in completion handler | ||
| 69 | unpatched | 4.7 | CVE-2017-17449 | | ||
| 70 | . patched | 4.7 | CVE-2017-18203 | nvd: dm: fix race between dm_get_from_kobject() and __dm_destroy() | ||
| 71 | . patched | 4.7 | CVE-2017-18224 | nvd: ocfs2: ip_alloc_sem should be taken in ocfs2_get_block() | ||
| 72 | . patched | 4.7 | CVE-2018-1065 | nvd: netfilter: add back stackpointer size checks | ||
| 73 | ... | ||
| 74 | |||
| 75 | @ report example output (NVD+LKC resource): | ||
| 76 | |||
| 77 | # 2018-10-10 15:46:05,902 %% CVERT %% INFO %% kernel version: 4.9.132 | ||
| 78 | . patched | 3.3 | CVE-2017-17807 | nvd: KEYS: add missing permission check for request_key() destination | ||
| 79 | unpatched | 3.3 | CVE-2017-17864 | | ||
| 80 | . patched | 4.4 | CVE-2016-9604 | lkc: a5c6e0a76817a3751f58d761aaff7c0b0c4001ff | ||
| 81 | . patched | 4.4 | CVE-2017-12153 | lkc: c820441a7a52e3626aede8df94069a50a9e4efdb | ||
| 82 | . patched | 4.4 | CVE-2017-14051 | lkc: 2a913aecc4f746ce15eb1bec98b134aff4190ae2 | ||
| 83 | . patched | 4.6 | CVE-2017-8924 | nvd: USB: serial: io_ti: fix information leak in completion handler | ||
| 84 | . patched | 4.7 | CVE-2017-17449 | lkc: 0b18782288a2f1c2a25e85d2553c15ea83bb5802 | ||
| 85 | . patched | 4.7 | CVE-2017-18203 | nvd: dm: fix race between dm_get_from_kobject() and __dm_destroy() | ||
| 86 | . patched | 4.7 | CVE-2017-18224 | nvd: ocfs2: ip_alloc_sem should be taken in ocfs2_get_block() | ||
| 87 | . patched | 4.7 | CVE-2018-1065 | nvd: netfilter: add back stackpointer size checks | ||
| 88 | ... | ||
| 89 | """)) | ||
| 90 | |||
| 91 | group = parser.add_mutually_exclusive_group(required=True) | ||
| 92 | group.add_argument("-f", "--feed-dir", help="feeds directory") | ||
| 93 | group.add_argument("-d", "--restore", help="load CVE data structures from file", | ||
| 94 | metavar="FILENAME") | ||
| 95 | parser.add_argument("--offline", help="do not update from NVD site", | ||
| 96 | action="store_true") | ||
| 97 | parser.add_argument("-o", "--output", help="save report to the file") | ||
| 98 | parser.add_argument("-k", "--kernel-ver", help='overwrite kernel version, ' | ||
| 99 | 'default is "make kernelversion"', | ||
| 100 | metavar="VERSION") | ||
| 101 | parser.add_argument("--show-description", help='show "Description" in the report', | ||
| 102 | action="store_true") | ||
| 103 | parser.add_argument("--show-reference", help='show "Reference" in the report', | ||
| 104 | action="store_true") | ||
| 105 | parser.add_argument("-r", "--resource", help='resources: e.g.: "--resource nvd lkc"', | ||
| 106 | nargs="+", default=["nvd"]) | ||
| 107 | parser.add_argument("--patched", help="patched CVE IDs", | ||
| 108 | nargs="+", default=[]) | ||
| 109 | parser.add_argument("--debug", help="print debug messages", | ||
| 110 | action="store_true") | ||
| 111 | |||
| 112 | parser.add_argument("kernel_dir", help="kernel GIT directory", | ||
| 113 | metavar="kernel-dir") | ||
| 114 | |||
| 115 | args = parser.parse_args() | ||
| 116 | |||
| 117 | logging.config.dictConfig(cvert.logconfig(args.debug)) | ||
| 118 | |||
| 119 | kernel_dir = args.kernel_dir | ||
| 120 | |||
| 121 | if args.restore: | ||
| 122 | cve_struct = cvert.load_cve(args.restore) | ||
| 123 | elif args.feed_dir: | ||
| 124 | cve_struct = cvert.update_feeds(args.feed_dir, args.offline) | ||
| 125 | |||
| 126 | if not cve_struct and args.offline: | ||
| 127 | parser.error("No CVEs found. Try to turn off offline mode or use other file to restore.") | ||
| 128 | |||
| 129 | if args.output: | ||
| 130 | output = open(args.output, "w") | ||
| 131 | else: | ||
| 132 | output = sys.stdout | ||
| 133 | |||
| 134 | if args.kernel_ver: | ||
| 135 | kernel_ver = args.kernel_ver | ||
| 136 | else: | ||
| 137 | kernel_ver = get_version(kernel_dir) | ||
| 138 | |||
| 139 | logging.info("kernel version: %s", kernel_ver) | ||
| 140 | |||
| 141 | if "lkc" in args.resource: | ||
| 142 | lkc_ctx = prepare_lkc() | ||
| 143 | |||
| 144 | commits = get_commits(kernel_dir) | ||
| 145 | report = cvert.generate_report({"linux_kernel": {kernel_ver: list(args.patched)}}, cve_struct) | ||
| 146 | |||
| 147 | for cve in report: | ||
| 148 | cve["comment"] = "" | ||
| 149 | |||
| 150 | if "nvd" in args.resource and cve["status"] == "unpatched": | ||
| 151 | process_nvd(cve, kernel_dir, commits) | ||
| 152 | |||
| 153 | if "lkc" in args.resource and cve["status"] == "unpatched": | ||
| 154 | process_lkc(cve, kernel_dir, commits, lkc_ctx) | ||
| 155 | |||
| 156 | print_report(report, | ||
| 157 | show_description=args.show_description, | ||
| 158 | show_reference=args.show_reference, | ||
| 159 | output=output) | ||
| 160 | |||
| 161 | if args.output: | ||
| 162 | output.close() | ||
| 163 | |||
| 164 | |||
| 165 | def process_nvd(cve, kernel_dir, commits): | ||
| 166 | """Process NVD references. | ||
| 167 | |||
| 168 | Sometimes NVD "Reference" contains a link to the commit in the GIT | ||
| 169 | upstream mainline. | ||
| 170 | |||
| 171 | First, look if we have that commit ID in the current local GIT | ||
| 172 | branch. It happens if you created local branch after CVE has been | ||
| 173 | fixed in mainline. | ||
| 174 | |||
| 175 | Otherwise, don't give up, and try to find the same headline | ||
| 176 | commit. It works if local GIT tree has up-to-date mainline branch | ||
| 177 | fetched. | ||
| 178 | |||
| 179 | That is all we can do having only mainline commit ID. | ||
| 180 | |||
| 181 | """ | ||
| 182 | |||
| 183 | for url in cve["reference"]: | ||
| 184 | iden = get_iden(url) | ||
| 185 | |||
| 186 | if iden and iden in commits["iden"]: | ||
| 187 | mark_patched(cve, "nvd: {0}".format(iden.decode())) | ||
| 188 | return | ||
| 189 | |||
| 190 | head = get_headline(kernel_dir, iden) | ||
| 191 | |||
| 192 | if head and head in commits["head"]: | ||
| 193 | mark_patched(cve, "nvd: {0}".format(head.decode())) | ||
| 194 | return | ||
| 195 | |||
| 196 | |||
| 197 | def process_lkc(cve, kernel_dir, commits, ctx): | ||
| 198 | """Process Linux Kernel CVEs approach | ||
| 199 | |||
| 200 | [https://github.com/nluedtke/linux_kernel_cves] | ||
| 201 | |||
| 202 | "kernel" context contains mainline "fixes" commit ID. So, check | ||
| 203 | with that first. | ||
| 204 | |||
| 205 | "stream" context contains backported "cmd_id" for other upstream | ||
| 206 | branches. Check accordingly. | ||
| 207 | |||
| 208 | If no commit ID found in local current branch, than look for the | ||
| 209 | same headline commits. Rarely backported commit has different | ||
| 210 | headline, so look at the "cmt_msg" first. | ||
| 211 | |||
| 212 | """ | ||
| 213 | |||
| 214 | iden_candidats = [] | ||
| 215 | |||
| 216 | try: | ||
| 217 | iden = ctx["kernel"][cve["CVE"]]["fixes"].encode() | ||
| 218 | except KeyError: | ||
| 219 | logging.debug("%s: commit ID not found", cve["CVE"]) | ||
| 220 | iden = None | ||
| 221 | |||
| 222 | if iden: | ||
| 223 | iden_candidats.append(iden) | ||
| 224 | |||
| 225 | if iden in commits["iden"]: | ||
| 226 | mark_patched(cve, "lkc: {0}".format(iden.decode())) | ||
| 227 | return | ||
| 228 | |||
| 229 | if cve["CVE"] in ctx["stream"]: | ||
| 230 | for kver in ctx["stream"][cve["CVE"]]: | ||
| 231 | try: | ||
| 232 | iden = ctx["stream"][cve["CVE"]][kver]["cmt_id"].encode() | ||
| 233 | except KeyError: | ||
| 234 | logging.debug("%s: commit ID not found", cve["CVE"]) | ||
| 235 | iden = None | ||
| 236 | |||
| 237 | if iden: | ||
| 238 | iden_candidats.append(iden) | ||
| 239 | |||
| 240 | if iden in commits["iden"]: | ||
| 241 | mark_patched(cve, "lkc: {0}".format(iden.decode())) | ||
| 242 | return | ||
| 243 | |||
| 244 | try: | ||
| 245 | head = ctx["kernel"][cve["CVE"]]["cmt_msg"].encode() | ||
| 246 | except KeyError: | ||
| 247 | logging.debug("%s: commit message header not found", cve["CVE"]) | ||
| 248 | head = None | ||
| 249 | |||
| 250 | if head and head in commits["head"]: | ||
| 251 | mark_patched(cve, "lkc: {0}".format(head.decode())) | ||
| 252 | return | ||
| 253 | |||
| 254 | # last chance | ||
| 255 | for iden in iden_candidats: | ||
| 256 | head = get_headline(kernel_dir, iden) | ||
| 257 | |||
| 258 | if head and head in commits["head"]: | ||
| 259 | mark_patched(cve, "lkc: {0}".format(head.decode())) | ||
| 260 | return | ||
| 261 | |||
| 262 | |||
| 263 | def prepare_lkc(): | ||
| 264 | """Prepare LKC context.""" | ||
| 265 | |||
| 266 | import urllib.request | ||
| 267 | import json | ||
| 268 | |||
| 269 | ctx = {} | ||
| 270 | url_base = "https://github.com/nluedtke/linux_kernel_cves/raw/master" | ||
| 271 | |||
| 272 | with urllib.request.urlopen(url_base + "/kernel_cves.json") as url: | ||
| 273 | ctx["kernel"] = json.loads(url.read().decode()) | ||
| 274 | |||
| 275 | with urllib.request.urlopen(url_base + "/stream_fixes.json") as url: | ||
| 276 | ctx["stream"] = json.loads(url.read().decode()) | ||
| 277 | |||
| 278 | return ctx | ||
| 279 | |||
| 280 | |||
| 281 | def mark_patched(cve, comment=""): | ||
| 282 | """Put a "patched" mark for the CVE.""" | ||
| 283 | |||
| 284 | cve["status"] = "patched" | ||
| 285 | cve["comment"] = comment | ||
| 286 | |||
| 287 | |||
| 288 | def get_version(kernel_dir): | ||
| 289 | """Return kernel version.""" | ||
| 290 | |||
| 291 | return subprocess.check_output( | ||
| 292 | ["make", "kernelversion"], | ||
| 293 | cwd=kernel_dir, | ||
| 294 | stderr=subprocess.DEVNULL | ||
| 295 | ).decode().rstrip() | ||
| 296 | |||
| 297 | |||
| 298 | def get_commits(kernel_dir): | ||
| 299 | """Return GIT commits dict.""" | ||
| 300 | |||
| 301 | commits = {"iden": [], "head": []} | ||
| 302 | |||
| 303 | for gitlog in subprocess.check_output(["git", "log", "--format=%H %s"], | ||
| 304 | cwd=kernel_dir, | ||
| 305 | stderr=subprocess.DEVNULL | ||
| 306 | ).splitlines(): | ||
| 307 | oneline = gitlog.split(maxsplit=1) | ||
| 308 | |||
| 309 | if len(oneline) > 1: | ||
| 310 | commits["head"].append(oneline[1]) | ||
| 311 | else: | ||
| 312 | commits["head"].append(b"") | ||
| 313 | |||
| 314 | commits["iden"].append(oneline[0]) | ||
| 315 | |||
| 316 | return commits | ||
| 317 | |||
| 318 | |||
| 319 | def get_iden(url): | ||
| 320 | """Return kernel commit ID from URL.""" | ||
| 321 | |||
| 322 | commit_re = [ | ||
| 323 | r"^http://git\.kernel\.org/cgit/linux/kernel/git/torvalds/linux\.git/commit/\?id=(.+)$", | ||
| 324 | r"^https?://github\.com/torvalds/linux/commit/(.+)$" | ||
| 325 | ] | ||
| 326 | |||
| 327 | for regexp in commit_re: | ||
| 328 | matched = re.match(regexp, url) | ||
| 329 | |||
| 330 | if matched: | ||
| 331 | return matched.group(1) | ||
| 332 | |||
| 333 | return None | ||
| 334 | |||
| 335 | |||
| 336 | def get_headline(kernel_dir, iden): | ||
| 337 | """Return commit headline.""" | ||
| 338 | |||
| 339 | if not iden: | ||
| 340 | return None | ||
| 341 | |||
| 342 | try: | ||
| 343 | head = subprocess.check_output(["git", "show", | ||
| 344 | "--no-patch", | ||
| 345 | "--format=%s", | ||
| 346 | iden], | ||
| 347 | cwd=kernel_dir, | ||
| 348 | stderr=subprocess.DEVNULL | ||
| 349 | ).rstrip() | ||
| 350 | except subprocess.CalledProcessError: | ||
| 351 | logging.debug("%s: commit ID not found", iden) | ||
| 352 | head = None | ||
| 353 | |||
| 354 | return head | ||
| 355 | |||
| 356 | |||
| 357 | def print_report(report, width=70, show_description=False, show_reference=False, output=sys.stdout): | ||
| 358 | """Output kernel report.""" | ||
| 359 | |||
| 360 | for cve in report: | ||
| 361 | print("{0:>9s} | {1:>4s} | {2:18s} | {3}".format(cve["status"], cve["CVSS"], | ||
| 362 | cve["CVE"], cve["comment"]), | ||
| 363 | file=output) | ||
| 364 | |||
| 365 | if show_description: | ||
| 366 | print("{0:>9s} + {1}".format(" ", "Description"), file=output) | ||
| 367 | |||
| 368 | for lin in textwrap.wrap(cve["description"], width=width): | ||
| 369 | print("{0:>9s} {1}".format(" ", lin), file=output) | ||
| 370 | |||
| 371 | if show_reference: | ||
| 372 | print("{0:>9s} + {1}".format(" ", "Reference"), file=output) | ||
| 373 | |||
| 374 | for url in cve["reference"]: | ||
| 375 | print("{0:>9s} {1}".format(" ", url), file=output) | ||
| 376 | |||
| 377 | |||
| 378 | if __name__ == "__main__": | ||
| 379 | report_kernel() | ||
