#!/usr/bin/env python3

# Author: Jamie Strandboge <jamie@ubuntu.com>
# Author: Kees Cook <kees@ubuntu.com>
# Copyright (C) 2005-2016 Canonical Ltd.
#
# This script is distributed under the terms and conditions of the GNU General
# Public License, Version 2 or later. See http://www.gnu.org/copyleft/gpl.html
# for details.

import optparse
import os
import re
import sys
import time

import cve_lib
import source_map

releases = ['upstream'] + cve_lib.all_releases

max_file_size = 10 * 1024 * 1024  # 10MB
cvedir = cve_lib.active_dir
boilerplates = cvedir

parser = optparse.OptionParser()
parser.add_option("-p", "--package", dest="pkgs", help="Package name and optional version where package is fixed (with optional Ubuntu release and version in that release)", metavar="NAME[,VERSION[,RELEASE,RELEASE_VERSION]]", action="append")
parser.add_option("-b", "--bug-url", dest="bug_urls", help="Bug references", metavar="URL", action="append")
parser.add_option("-r", "--reference-url", dest="ref_urls", help="URL references", metavar="URL", action="append")
parser.add_option("-c", "--cve", dest="cve", help="CVE entry", metavar="CVE-YYYY-NNNN")
parser.add_option("-e", "--embargoed", dest="embargoed", help="This is an embargoed entry", action="store_true")
parser.add_option("-y", "--yes", dest="autoconfirm", help="Do not ask for confirmation", action="store_true")
parser.add_option("-P", "--public", help="Record date the CVE went public", metavar="YYYY-MM-DD")
parser.add_option("-C", "--cvss", help="CVSS3.1 rating", metavar="CVSS:3.1/AV:_/AC:_/PR:_/UI:_/S:_/C:_/I:_/A:_")
(options, args) = parser.parse_args()

source = source_map.load()

def pkg_in_rel(pkg,rel):
    if rel in ['upstream']:
        return True
    if rel not in source:
        return False
    return (pkg in source[rel])

def get_releases(pkgname):
    # deep copy
    tmp = []
    for r in releases:
        tmp.append(r)

    # Handle when devel isn't open yet
    if cve_lib.devel_release == '':
        tmp.append('')

    return tmp

def create_or_update_external_subproject_cves(cve, pkgname):
    '''Create or update subproject CVE files'''
    # map releases to lines added
    affected_releases = []
    # we only handle external subproject cves
    for release in cve_lib.external_releases:
        if not pkg_in_rel(pkgname, release):
            continue
        affected_releases.append(release)

    if not options.autoconfirm:
        print("\n")
        for release in affected_releases:
             state = "needs-triage"
             print("%s_%s: %s\n" % (release, pkgname, state))
        print("\nAppend the above to subproject CVEs (y|N)? ")
        ans = sys.stdin.readline().lower()
        print("\n")

    if options.autoconfirm or ans.startswith("y"):
        if affected_releases:
            print('\n================= External subproject update ==================')
            print(" %s - %s: " % (cve, pkgname))

        for release in affected_releases:
            with open(os.path.join(cve_lib.get_external_subproject_cve_dir(release), cve), "a") as f:
                state = "needs-triage"
                entry = "%s_%s: %s\n" % (release, pkgname, state)
                f.write(entry)
                f.close()
                print("    %s" % release)

    else:
        print("Aborted\n")

def update_cve(cve, pkgname, fixed_in=None, fixed_in_release=None, fixed_in_release_version=None):
    '''Update an existing CVE file'''
    with open(os.path.join(cvedir, cve), "r") as f:
        lines = f.read(max_file_size).split('\n')

    skipped = []
    added_lines = ""

    tmp_releases = get_releases(pkgname)

    # If we are using 00boilerplate.<pkgname> and the package is DNE on all
    # current releases, then don't add the the stanza for this release. This
    # allows us to use generic boilerplate names like 00boilerplate.gnutls or
    # 00boilerplate.openjdk without adding useless extra stanzas.
    # TODO: this still doesn't handle adding the contents of the boilerplate
    # to an existing CVE (ie, ./scripts/sctive_edit -p openjdk -c CVE-YYYY-NNNN
    # where CVE-YYYY-NNNN already exists)
    if os.path.exists(os.path.join(boilerplates, '00boilerplate.%s' % pkgname)):
        pkg_exists_somewhere = False
        for r in tmp_releases:
            if r == 'upstream' or (r in cve_lib.eol_releases \
                    and not cve_lib.is_active_esm_release(r)):
                continue
            if pkg_in_rel(pkgname,r):
                pkg_exists_somewhere = True
                break
        if not pkg_exists_somewhere:
            print("skipping '" + pkgname + "' (DNE on all current releases)\n", file=sys.stderr)
            return

    for line in lines:
        for r in tmp_releases:
            if r == cve_lib.devel_release or r == '':
                r = 'devel'
            if not re.match(r'^' + r + ".*:", line):
                continue
            tmp = line.split(':')
            match = "%s_%s" % (r, pkgname)
            if match == tmp[0]:
                skipped.append(r)

    if len(skipped) == 0:
        added_lines += '\nPatches_' + pkgname + ':\n'

    higher_not_affected = False
    for release in tmp_releases:
        # don't add any external releases
        if release in cve_lib.external_releases:
            continue

        r = release
        if r == cve_lib.devel_release or r == '':
            r = 'devel'

        if r in skipped:
             print("skipping '" + pkgname + "' for " + r + " (already included)\n", file=sys.stderr)
        else:
            # skip eol_releases releases without esm support
            if r in cve_lib.eol_releases \
                  and (not cve_lib.is_active_esm_release(r) or r == 'precise'):
                continue
            state = "needs-triage"
            if not pkg_in_rel(pkgname, release):
                continue
            elif cve_lib.is_active_esm_release(r):
                state = "ignored (out of standard support)"
            elif r == 'upstream' and fixed_in is not None:
                state = "released (%s)" % fixed_in
            elif fixed_in_release_version and r == fixed_in_release:
                state = "not-affected (%s)" % fixed_in_release_version
                higher_not_affected = True
            elif higher_not_affected:
                state = "not-affected"
            added_lines += '%s_%s: %s\n' % (r, pkgname, state)

    if len(releases) == len(skipped):
        print("\nNothing to add!\n")
        return

    if not options.autoconfirm:
        print("\n" + added_lines)
        print("\nAppend the above to " + os.path.join(cvedir, cve) + " (y|N)? ")
        ans = sys.stdin.readline().lower()
        print("\n")
    else:
        ans = "y"

    if ans.startswith("y"):
        file = open(os.path.join(cvedir, cve), "a")
        file.write(added_lines)
        file.close()
    else:
        print("Aborted\n")

def create_cve(cve, pkgname, fixed_in=None, fixed_in_release=None, fixed_in_release_version=None):
    '''Create a new CVE file'''
    src = os.path.join(boilerplates, '00boilerplate')
    if os.path.exists(src + "." + pkgname):
        src = src + "." + pkgname
    boiler = open(src, "r")
    lines =  boiler.read(max_file_size).splitlines()
    boiler.close()

    cand_pat = re.compile(r'^Candidate:')
    ref_pat = re.compile(r'^References:')
    bugs_pat = re.compile(r'^Bugs:')
    cvss_pat = re.compile(r'^CVSS:')
    pkg_pat = re.compile(r'^#?[a-z/\-]+_(PKG|%s):' % pkgname)
    patch_pat = re.compile(r'^#?Patches_(PKG|%s):' % pkgname)

    tmp_releases = get_releases(pkgname)
    added = set()

    contents = ""
    higher_not_affected = False
    for line in lines:
        if (cand_pat.search(line)):
            contents += line + os.path.basename(cve) + '\n'
        elif line.startswith('PublicDate:'):
            if options.embargoed:
                if options.public:
                    # use public date as CRD
                    contents += "CRD: %s\n" % options.public
                else:
                    contents += "CRD: <TBD>\n"
            if options.public:
                contents += "PublicDate: %s\n" % options.public
            else:
                # default to today-- this will be refreshed by check-cves
                contents += "PublicDate: %s\n" % time.strftime("%Y-%m-%d", time.gmtime())
        elif (cvss_pat.search(line)):
            if options.cvss:
                contents += 'CVSS: ' + options.cvss + '\n'
            else:
                contents += 'CVSS:\n'
        elif (patch_pat.search(line)):
            contents += '\nPatches_' + pkgname + ':\n'
        elif (pkg_pat.search(line)):
            for release in tmp_releases:
                if release == cve_lib.devel_release or release == '':
                    release = 'devel'

                # skip eol_releases releases without esm support
                if release in cve_lib.eol_releases \
                        and not cve_lib.is_active_esm_release(release):
                    continue

                rel_pat = re.compile('#?' + release + '_')

                if (rel_pat.search(line)):
                    state = "needs-triage"
                    if not pkg_in_rel(pkgname, release):
                        continue
                    elif cve_lib.is_active_esm_release(release):
                        state = "ignored (out of standard support)"
                    elif release == 'upstream' and fixed_in is not None:
                        state = "released (%s)" % fixed_in
                    elif fixed_in_release_version and release == fixed_in_release:
                        state = "not-affected (%s)" % fixed_in_release_version
                        higher_not_affected = True
                    elif higher_not_affected:
                        state = "not-affected"
                    contents += "%s_%s: %s\n" % (release, pkgname, state)
                    added.add(release)
        elif ref_pat.search(line):
            if not re.search(r'N', cve):
                contents += line + "\n https://cve.mitre.org/cgi-bin/cvename.cgi?name=" + cve + "\n"
            else:
                contents += line + "\n"
            if options.ref_urls:
                for i in options.ref_urls:
                   contents += " %s\n" % i
        elif options.bug_urls and bugs_pat.search(line):
            contents += line
            for i in options.bug_urls:
                contents += "\n %s\n" % i
        else:
            contents += line + '\n'

    # check for missing entries which aren't in the boilerplate
    for release in tmp_releases:
        if release == cve_lib.devel_release or release == '':
            release = 'devel'
        if release in added:
            continue
        if release in cve_lib.external_releases:
            continue
        # skip eol_releases releases without esm support
        if release in cve_lib.eol_releases \
           and not cve_lib.is_active_esm_release(release):
            continue
        if pkg_in_rel(pkgname, release):
            state = "needs-triage"
            contents += "%s_%s: %s\n" % (release, pkgname, state)

    # for each line in contents, see if we need to supercede DNE status
    # with needs-triage as boilerplate entries may not be up-to-date with
    # xxx-supported.txt lists
    pkgstatus_pat = re.compile(r'^([/a-z/\-]+)_(.*): (.*)')
    old_contents = contents
    contents = ""
    for line in old_contents.splitlines():
        match = pkgstatus_pat.search(line)
        if match is not None:
            release = match.group(1)
            pkgname = match.group(2)
            status = match.group(3)
            if status == 'DNE' and pkg_in_rel(pkgname, release):
                status = 'needs-triage'
                line = '%s_%s: %s' % (release, pkgname, status)
        contents += line + '\n'

    if not options.autoconfirm:
        print(contents)
        print("\nWrite the above to " + os.path.join(cvedir, cve) + " (y|N)? ")
        ans = sys.stdin.readline().lower()
        print("\n")
    else:
        ans = "y"

    if ans.startswith("y"):
        newfile = open(os.path.join(cvedir, cve), 'w')
        newfile.write(contents)
        newfile.close()
    else:
        print("Aborted\n")



if not options.pkgs:
    parser.print_help()
    sys.exit(1)

if not options.cve:
    parser.print_help()
    sys.exit(1)

cve = options.cve
pkgs = options.pkgs
pat = cve_lib.CVE_RE

if options.embargoed:
    cvedir = cve_lib.embargoed_dir
    pat = re.compile(r'^[\w-]*$')

    if not os.path.islink(cvedir):
        print("embargoed/ is not a symlink. Aborting!\n", file=sys.stderr)
        sys.exit(1)


if not pat.search(cve):
    if options.embargoed:
        print("Bad embargoed entry.  Should be alphanumerics and dashes\n", file=sys.stderr)
    else:
        print("Bad CVE entry.  Should be CVE-XXXX-XXXX\n", file=sys.stderr)
    sys.exit(1)

# more here
pat = re.compile(r'\s')
for p in pkgs:
    tmp_p = p.split(',')
    pkgname = tmp_p[0]
    fixed_in = None
    if len(tmp_p) > 1:
        fixed_in = tmp_p[1]
    fixed_in_release = None
    fixed_in_release_version = None
    if len(tmp_p) > 3:
        fixed_in_release = tmp_p[2]
        fixed_in_release_version = tmp_p[3]

    if pat.search(pkgname):
        print("Bad package name\n", file=sys.stderr)
        sys.exit(1)

    if not os.path.isfile(os.path.join(boilerplates, "00boilerplate")):
        print("Could not find 00boilerplate in " + cvedir + "\n", file=sys.stderr)
        sys.exit(1)

    if (os.path.isfile(os.path.join(cvedir, cve))):
        if not options.autoconfirm:
            print("Found existing " + cve + "...\n\n")
        update_cve(cve, pkgname, fixed_in, fixed_in_release, fixed_in_release_version)
    else:
        if not options.autoconfirm:
            print("Creating new " + cve + "...\n\n")
        create_cve(cve, pkgname, fixed_in, fixed_in_release, fixed_in_release_version)

    create_or_update_external_subproject_cves(cve, pkgname)

sys.exit(0)
