aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/cvert-kernel
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/cvert-kernel')
-rwxr-xr-xscripts/cvert-kernel379
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 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2018 by Cisco Systems, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+""" Generate CVE report for the given Linux kernel GIT branch
+"""
+
+import re
+import sys
+import argparse
+import textwrap
+import subprocess
+import logging
+import logging.config
+import cvert
+
+
+def report_kernel():
+ """Generate Linux kernel CVE report"""
+
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description=textwrap.dedent("""
+ Generate CVE report for the Linux kernel.
+ Inspect Linux kernel GIT tree and find all CVE patches commits.
+ """),
+ epilog=textwrap.dedent("""
+ @ run examples:
+
+ # Download (update) NVD feeds in "nvdfeed" directory
+ # and prepare the report for the "kernel-sources" directory
+ %% %(prog)s --feed-dir nvdfeed --output report-kernel.txt kernel-sources
+
+ # Use existed NVD feeds in "nvdfeed" directory
+ # and prepare the report for the "kernel-sources" directory
+ %% %(prog)s --offline --feed-dir nvdfeed --output report-kernel.txt kernel-sources
+
+ # (faster) Restore CVE dump from "cvedump" (must exist)
+ # and prepare the report for the "kernel-sources" directory
+ %% %(prog)s --restore cvedump --output report-kernel.txt kernel-sources
+
+ # Restore CVE dump from "cvedump" (must exist)
+ # and prepare the extended report for the "kernel-sources" directory
+ %% %(prog)s --restore cvedump --show-description --show-reference --output report-kernel.txt kernel-sources
+
+ @ report example output (NVD resource):
+
+ # 2018-10-10 15:41:52,213 %% CVERT %% INFO %% kernel version: 4.9.132
+ . patched | 3.3 | CVE-2017-17807 | nvd: KEYS: add missing permission check for request_key() destination
+ unpatched | 3.3 | CVE-2017-17864 |
+ unpatched | 4.4 | CVE-2016-9604 |
+ unpatched | 4.4 | CVE-2017-12153 |
+ unpatched | 4.4 | CVE-2017-14051 |
+ . patched | 4.6 | CVE-2017-8924 | nvd: USB: serial: io_ti: fix information leak in completion handler
+ unpatched | 4.7 | CVE-2017-17449 |
+ . patched | 4.7 | CVE-2017-18203 | nvd: dm: fix race between dm_get_from_kobject() and __dm_destroy()
+ . patched | 4.7 | CVE-2017-18224 | nvd: ocfs2: ip_alloc_sem should be taken in ocfs2_get_block()
+ . patched | 4.7 | CVE-2018-1065 | nvd: netfilter: add back stackpointer size checks
+ ...
+
+ @ report example output (NVD+LKC resource):
+
+ # 2018-10-10 15:46:05,902 %% CVERT %% INFO %% kernel version: 4.9.132
+ . patched | 3.3 | CVE-2017-17807 | nvd: KEYS: add missing permission check for request_key() destination
+ unpatched | 3.3 | CVE-2017-17864 |
+ . patched | 4.4 | CVE-2016-9604 | lkc: a5c6e0a76817a3751f58d761aaff7c0b0c4001ff
+ . patched | 4.4 | CVE-2017-12153 | lkc: c820441a7a52e3626aede8df94069a50a9e4efdb
+ . patched | 4.4 | CVE-2017-14051 | lkc: 2a913aecc4f746ce15eb1bec98b134aff4190ae2
+ . patched | 4.6 | CVE-2017-8924 | nvd: USB: serial: io_ti: fix information leak in completion handler
+ . patched | 4.7 | CVE-2017-17449 | lkc: 0b18782288a2f1c2a25e85d2553c15ea83bb5802
+ . patched | 4.7 | CVE-2017-18203 | nvd: dm: fix race between dm_get_from_kobject() and __dm_destroy()
+ . patched | 4.7 | CVE-2017-18224 | nvd: ocfs2: ip_alloc_sem should be taken in ocfs2_get_block()
+ . patched | 4.7 | CVE-2018-1065 | nvd: netfilter: add back stackpointer size checks
+ ...
+ """))
+
+ group = parser.add_mutually_exclusive_group(required=True)
+ group.add_argument("-f", "--feed-dir", help="feeds directory")
+ group.add_argument("-d", "--restore", help="load CVE data structures from file",
+ metavar="FILENAME")
+ parser.add_argument("--offline", help="do not update from NVD site",
+ action="store_true")
+ parser.add_argument("-o", "--output", help="save report to the file")
+ parser.add_argument("-k", "--kernel-ver", help='overwrite kernel version, '
+ 'default is "make kernelversion"',
+ metavar="VERSION")
+ parser.add_argument("--show-description", help='show "Description" in the report',
+ action="store_true")
+ parser.add_argument("--show-reference", help='show "Reference" in the report',
+ action="store_true")
+ parser.add_argument("-r", "--resource", help='resources: e.g.: "--resource nvd lkc"',
+ nargs="+", default=["nvd"])
+ parser.add_argument("--patched", help="patched CVE IDs",
+ nargs="+", default=[])
+ parser.add_argument("--debug", help="print debug messages",
+ action="store_true")
+
+ parser.add_argument("kernel_dir", help="kernel GIT directory",
+ metavar="kernel-dir")
+
+ args = parser.parse_args()
+
+ logging.config.dictConfig(cvert.logconfig(args.debug))
+
+ kernel_dir = args.kernel_dir
+
+ if args.restore:
+ cve_struct = cvert.load_cve(args.restore)
+ elif args.feed_dir:
+ cve_struct = cvert.update_feeds(args.feed_dir, args.offline)
+
+ if not cve_struct and args.offline:
+ parser.error("No CVEs found. Try to turn off offline mode or use other file to restore.")
+
+ if args.output:
+ output = open(args.output, "w")
+ else:
+ output = sys.stdout
+
+ if args.kernel_ver:
+ kernel_ver = args.kernel_ver
+ else:
+ kernel_ver = get_version(kernel_dir)
+
+ logging.info("kernel version: %s", kernel_ver)
+
+ if "lkc" in args.resource:
+ lkc_ctx = prepare_lkc()
+
+ commits = get_commits(kernel_dir)
+ report = cvert.generate_report({"linux_kernel": {kernel_ver: list(args.patched)}}, cve_struct)
+
+ for cve in report:
+ cve["comment"] = ""
+
+ if "nvd" in args.resource and cve["status"] == "unpatched":
+ process_nvd(cve, kernel_dir, commits)
+
+ if "lkc" in args.resource and cve["status"] == "unpatched":
+ process_lkc(cve, kernel_dir, commits, lkc_ctx)
+
+ print_report(report,
+ show_description=args.show_description,
+ show_reference=args.show_reference,
+ output=output)
+
+ if args.output:
+ output.close()
+
+
+def process_nvd(cve, kernel_dir, commits):
+ """Process NVD references.
+
+ Sometimes NVD "Reference" contains a link to the commit in the GIT
+ upstream mainline.
+
+ First, look if we have that commit ID in the current local GIT
+ branch. It happens if you created local branch after CVE has been
+ fixed in mainline.
+
+ Otherwise, don't give up, and try to find the same headline
+ commit. It works if local GIT tree has up-to-date mainline branch
+ fetched.
+
+ That is all we can do having only mainline commit ID.
+
+ """
+
+ for url in cve["reference"]:
+ iden = get_iden(url)
+
+ if iden and iden in commits["iden"]:
+ mark_patched(cve, "nvd: {0}".format(iden.decode()))
+ return
+
+ head = get_headline(kernel_dir, iden)
+
+ if head and head in commits["head"]:
+ mark_patched(cve, "nvd: {0}".format(head.decode()))
+ return
+
+
+def process_lkc(cve, kernel_dir, commits, ctx):
+ """Process Linux Kernel CVEs approach
+
+ [https://github.com/nluedtke/linux_kernel_cves]
+
+ "kernel" context contains mainline "fixes" commit ID. So, check
+ with that first.
+
+ "stream" context contains backported "cmd_id" for other upstream
+ branches. Check accordingly.
+
+ If no commit ID found in local current branch, than look for the
+ same headline commits. Rarely backported commit has different
+ headline, so look at the "cmt_msg" first.
+
+ """
+
+ iden_candidats = []
+
+ try:
+ iden = ctx["kernel"][cve["CVE"]]["fixes"].encode()
+ except KeyError:
+ logging.debug("%s: commit ID not found", cve["CVE"])
+ iden = None
+
+ if iden:
+ iden_candidats.append(iden)
+
+ if iden in commits["iden"]:
+ mark_patched(cve, "lkc: {0}".format(iden.decode()))
+ return
+
+ if cve["CVE"] in ctx["stream"]:
+ for kver in ctx["stream"][cve["CVE"]]:
+ try:
+ iden = ctx["stream"][cve["CVE"]][kver]["cmt_id"].encode()
+ except KeyError:
+ logging.debug("%s: commit ID not found", cve["CVE"])
+ iden = None
+
+ if iden:
+ iden_candidats.append(iden)
+
+ if iden in commits["iden"]:
+ mark_patched(cve, "lkc: {0}".format(iden.decode()))
+ return
+
+ try:
+ head = ctx["kernel"][cve["CVE"]]["cmt_msg"].encode()
+ except KeyError:
+ logging.debug("%s: commit message header not found", cve["CVE"])
+ head = None
+
+ if head and head in commits["head"]:
+ mark_patched(cve, "lkc: {0}".format(head.decode()))
+ return
+
+ # last chance
+ for iden in iden_candidats:
+ head = get_headline(kernel_dir, iden)
+
+ if head and head in commits["head"]:
+ mark_patched(cve, "lkc: {0}".format(head.decode()))
+ return
+
+
+def prepare_lkc():
+ """Prepare LKC context."""
+
+ import urllib.request
+ import json
+
+ ctx = {}
+ url_base = "https://github.com/nluedtke/linux_kernel_cves/raw/master"
+
+ with urllib.request.urlopen(url_base + "/kernel_cves.json") as url:
+ ctx["kernel"] = json.loads(url.read().decode())
+
+ with urllib.request.urlopen(url_base + "/stream_fixes.json") as url:
+ ctx["stream"] = json.loads(url.read().decode())
+
+ return ctx
+
+
+def mark_patched(cve, comment=""):
+ """Put a "patched" mark for the CVE."""
+
+ cve["status"] = "patched"
+ cve["comment"] = comment
+
+
+def get_version(kernel_dir):
+ """Return kernel version."""
+
+ return subprocess.check_output(
+ ["make", "kernelversion"],
+ cwd=kernel_dir,
+ stderr=subprocess.DEVNULL
+ ).decode().rstrip()
+
+
+def get_commits(kernel_dir):
+ """Return GIT commits dict."""
+
+ commits = {"iden": [], "head": []}
+
+ for gitlog in subprocess.check_output(["git", "log", "--format=%H %s"],
+ cwd=kernel_dir,
+ stderr=subprocess.DEVNULL
+ ).splitlines():
+ oneline = gitlog.split(maxsplit=1)
+
+ if len(oneline) > 1:
+ commits["head"].append(oneline[1])
+ else:
+ commits["head"].append(b"")
+
+ commits["iden"].append(oneline[0])
+
+ return commits
+
+
+def get_iden(url):
+ """Return kernel commit ID from URL."""
+
+ commit_re = [
+ r"^http://git\.kernel\.org/cgit/linux/kernel/git/torvalds/linux\.git/commit/\?id=(.+)$",
+ r"^https?://github\.com/torvalds/linux/commit/(.+)$"
+ ]
+
+ for regexp in commit_re:
+ matched = re.match(regexp, url)
+
+ if matched:
+ return matched.group(1)
+
+ return None
+
+
+def get_headline(kernel_dir, iden):
+ """Return commit headline."""
+
+ if not iden:
+ return None
+
+ try:
+ head = subprocess.check_output(["git", "show",
+ "--no-patch",
+ "--format=%s",
+ iden],
+ cwd=kernel_dir,
+ stderr=subprocess.DEVNULL
+ ).rstrip()
+ except subprocess.CalledProcessError:
+ logging.debug("%s: commit ID not found", iden)
+ head = None
+
+ return head
+
+
+def print_report(report, width=70, show_description=False, show_reference=False, output=sys.stdout):
+ """Output kernel report."""
+
+ for cve in report:
+ print("{0:>9s} | {1:>4s} | {2:18s} | {3}".format(cve["status"], cve["CVSS"],
+ cve["CVE"], cve["comment"]),
+ file=output)
+
+ if show_description:
+ print("{0:>9s} + {1}".format(" ", "Description"), file=output)
+
+ for lin in textwrap.wrap(cve["description"], width=width):
+ print("{0:>9s} {1}".format(" ", lin), file=output)
+
+ if show_reference:
+ print("{0:>9s} + {1}".format(" ", "Reference"), file=output)
+
+ for url in cve["reference"]:
+ print("{0:>9s} {1}".format(" ", url), file=output)
+
+
+if __name__ == "__main__":
+ report_kernel()