cvetriager: bin/ script

Created new bin/ script to tie all the cvetriager modules together.
This is currently being tested by CVEs picked from
https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=linux+kernel. Includes
small modification to the cl generator.

BUG=chromium:1093363
TEST=python setup.py test

Change-Id: I18b2ec99d49ee4d6ebc9dd3f1f2c273c8b9a83a8
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2301894
Commit-Queue: Wanda Mora <morawand@chromium.org>
Tested-by: Wanda Mora <morawand@chromium.org>
Reviewed-by: Zubin Mithra <zsm@chromium.org>
diff --git a/contrib/cvetriager/bin/triage b/contrib/cvetriager/bin/triage
new file mode 100644
index 0000000..8c17eeb
--- /dev/null
+++ b/contrib/cvetriager/bin/triage
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+#
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Tool that triages a given CVE."""
+
+import argparse
+import logging
+import sys
+import os
+
+from cvelib import logutils, patchapplier, contextgenerator, clgenerator, webscraper, common
+
+
+KERNELS = ['v5.4', 'v4.19', 'v4.14', 'v4.4', 'v3.18', 'v3.14', 'v3.10', 'v3.8']
+ENV_VARS = ['CHROMIUMOS_KERNEL', 'LINUX', 'STABLE', 'STABLE_RC']
+
+
+def get_parser():
+    """Returns an ArgumentParser instance."""
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('cve_number', type=str, help='CVE number to be triaged.')
+    parser.add_argument('--bug', type=str, required=True, help='BUG id to be used.')
+    parser.add_argument('--debug', help='Display debug messages.', action='store_true')
+    parser.add_argument('-rc', '--relevant_commit', help='Display commits referring to CVE fix.',
+                        action='store_true')
+    parser.add_argument('--pull', help='Update kernel repositories.', action='store_true')
+    return parser
+
+
+def main(argv):
+    """Main."""
+    logger = logutils.setuplogging(logging.INFO, 'triage')
+    parser = get_parser()
+    opts = parser.parse_args(argv)
+    cve_num = opts.cve_number
+
+    for env in ENV_VARS:
+        if not os.getenv(env):
+            logger.error(f'${env} not set in virtual environment.')
+            return 1
+
+    patches = {}
+    loglvl = logging.INFO
+
+    if opts.debug:
+        loglvl = logging.DEBUG
+
+    # Change log levels.
+    modules = [webscraper, patchapplier, clgenerator]
+    for m in modules:
+        m.LOGGER.setLevel(loglvl)
+
+    # Allows pull command to execute on each branch.
+    if opts.pull:
+        common.DO_PULL = True
+
+    logger.info('Searching for possible fixes.')
+    commits = webscraper.find_relevant_commits(cve_num)
+
+    if len(commits) == 0:
+        logger.error('No commits found.')
+        return 1
+
+    for commit in commits:
+        logger.info(f'Generating context for {commit}')
+        cg = contextgenerator.ContextGenerator(KERNELS, opts.relevant_commit, loglvl)
+        cg.generate_context(commit)
+        patches[commit] = cg.kernels
+
+    for commit in commits:
+        logger.info(f'Trying to apply patch: {commit}')
+        kernels = patchapplier.apply_patch(commit, opts.bug, patches[commit])
+
+        patched_kernels = []
+
+        # Displays status of kernels after attempting to patch each one.
+        for kern in KERNELS:
+            cros_branch = common.get_cros_branch(kern)
+            if kern not in patches[commit]:
+                logger.info(f'{cros_branch}: FIXED')
+            else:
+                if kernels[kern]:
+                    patched_kernels.append(kern)
+                    logger.info(f'{cros_branch}: MISSING, applies cleanly')
+                else:
+                    logger.info(f'{cros_branch}: MISSING, conflict')
+
+        if len(patched_kernels) != 0:
+            logger.info(f'Generating CLs for {commit}')
+            clgenerator.create_cls(opts.bug, patched_kernels)
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv[1:]))
diff --git a/contrib/cvetriager/cvelib/clgenerator.py b/contrib/cvetriager/cvelib/clgenerator.py
index fa15a20..e75a33c 100644
--- a/contrib/cvetriager/cvelib/clgenerator.py
+++ b/contrib/cvetriager/cvelib/clgenerator.py
@@ -49,9 +49,10 @@
 def do_push(push_cmd, kernel, kernel_path):
     """Pushes to branch."""
     try:
-        output = subprocess.check_output(push_cmd, stderr=subprocess.DEVNULL, cwd=kernel_path)
-    except:
-        raise CLGeneratorException(f'Push failed for {kernel}')
+        output = subprocess.check_output(push_cmd.split(' '), stderr=subprocess.DEVNULL,
+                                         cwd=kernel_path)
+    except subprocess.CalledProcessError:
+        raise CLGeneratorException(f'{kernel} repository needs to be refreshed before pushing.')
 
     return output
 
diff --git a/contrib/cvetriager/setup.py b/contrib/cvetriager/setup.py
index cd8fdd5..13714b4 100644
--- a/contrib/cvetriager/setup.py
+++ b/contrib/cvetriager/setup.py
@@ -16,6 +16,12 @@
     license='BSD-Google',
     packages=['cvelib'],
     zip_safe=False,
-    install_requires=['bs4', 'requests'],
+    install_requires=[
+        'bs4',
+        'requests'
+        ],
     test_suite='tests',
+    scripts=[
+        'bin/triage'
+    ],
 )