#!/usr/bin/env python3

# Copyright (C) 2005-2021 Canonical Ltd.
# Author: Kees Cook <kees@ubuntu.com>
# Author: Jamie Strandboge <jamie@ubuntu.com>
#
# 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.
from __future__ import print_function

import optparse
import os
import sys
import time
import cve_lib
import support_tool

if sys.version_info[0] == 3:
    from urllib.parse import quote
    from html import escape
else:
    from urllib import quote
    from cgi import escape

import source_map
map = source_map.load()
releases = list(cve_lib.all_releases)


def htmlTableHeader(show_crd=False, show_assignee=False):
    header = "<thead class=\"thead-dark\">\n"
    header += '<tr><th>CVE</th><th>Package</th>'
    if show_crd:
        header += '<th>CRD</th>'
    if show_assignee:
        header += '<th>Assigned To</th>'
    for rel in releases:
        name = rel
        header += "<th>%s</th>" % (cve_lib.release_name(name))
    header += '<th>Links</th><th>Notes</th></tr>'
    header += "\n</thead>"
    return header


parser = optparse.OptionParser()
parser.add_option("-H", "--html", help="Output html", action="store_true")
parser.add_option("-r", "--all-releases", help="Show details for all releases not just those in releases_list in ~/.ubuntu-security-tools.conf", action="store_true")
parser.add_option("-s", "--supported", help="Mark those CVEs that have packages officially supported by Canonical", action="store_true")
parser.add_option("-t", "--untriaged", help="Show only those CVEs do not have a Priority set", action="store_true")
parser.add_option("-S", "--skip-devel", help="Skip CVEs that apply *only* to the current devel release", action="store_true")
parser.add_option("-E", "--skip-embargoed", help="Show only those CVEs *not* embargoed", action="store_true")
parser.add_option("--skip-esm", help="Skip the esm releases", action="store_true")
parser.add_option("--unmask-needed", help="Show CVE status without translating to 'needed'", action="store_true")
parser.add_option("-D", "--only-devel", help="Show only those CVEs in the current devel release", action="store_true")
parser.add_option("-N", "--no-retired", help="Don't report retireable CVEs", action="store_true")
parser.add_option("-p", "--packages", help="Report only on the given packages", action="append", type="string")
parser.add_option("-P", "--pkgfamily", help="Perform pkg family renamings (use argument multiple times for 'linux' and/or 'xen')", action="append", default=[])
parser.add_option("--flavor", help="Show packages affecting flavor", metavar="FLAVOR")
parser.add_option("--only-flavor", help="Show only packages affecting flavor specified with --flavor", action="store_true")
parser.add_option("--support-db", help="Database for support mappings (required for --flavor)", metavar="FILE")
parser.add_option("-d", "--debug", help="Report debug information while loading", action="store_true")
parser.add_option("--only-release", help="Show only packages affecting release", metavar="RELEASE")
parser.add_option("--only-esm", help="Show only packages affecting esm releases")
parser.add_option("--priority", help="Show only packages of a given priority or higher", action="append", type="string")
parser.add_option("--public-date", help="Show only packages with a CRD date newer than requested", metavar="CRD")
parser.add_option("--omit-header", help="Suppress printing the releases header", action="store_true")
parser.add_option("--show-crd", help="Show CRD", action="store_true")
parser.add_option("--show-assignee", help="Show who the CVE is assigned to", action="store_true")
parser.add_option("--show-released-version", help="Show the version a CVE was fixed released if it has the released info", action="store_true")
parser.add_option("--omit-status", help="Omit per-release status", action="store_true", default=False)
parser.add_option("--subproject", help="Show CVE status for a subproject", action="append", default=[])
(opt, args) = parser.parse_args()

# respect release_list from ~/.ubuntu-security-tools.conf
ust_conffile = os.path.expanduser("~/.ubuntu-security-tools.conf")
if not opt.all_releases and os.path.exists(ust_conffile):
    ust_conf = cve_lib.read_config_file(ust_conffile)
    try:
        releases = str(ust_conf["release_list"]).strip().split(' ')
    except:
        pass

# retirement processes cannot be done safely when limiting by package
# or release
if opt.packages or opt.only_release:
    opt.no_retired = True

support_db = None
if opt.flavor:
    if not opt.support_db:
        print("Must specify --support-db with --flavor", file=sys.stderr)
        sys.exit(1)
    if not os.path.exists(os.path.expanduser(opt.support_db)):
        print("'%s' does not exist" % opt.support_db, file=sys.stderr)
        sys.exit(1)
    support_db = support_tool._load_db(os.path.expanduser(opt.support_db))
    releases = cve_lib.flavor_releases

if len(cve_lib.devel_release) > 0:
    if opt.skip_devel:
        releases.remove(cve_lib.devel_release)
    if opt.only_devel:
        releases = [cve_lib.devel_release]
    elif opt.only_release:
        releases = [opt.only_release]

for eol in cve_lib.eol_releases:
    if eol in releases:
        releases.remove(eol)

if opt.skip_esm:
    for esm in cve_lib.esm_releases:
        esm_name = esm + "/esm"
        if esm_name in releases:
            releases.remove(esm_name)

    for esm in cve_lib.esm_apps_releases:
        esm_name = "esm-apps/" + esm
        if esm_name in releases:
            releases.remove(esm_name)

    for esm in cve_lib.esm_infra_releases:
        esm_name = "esm-infra/" + esm
        if esm_name in releases:
            releases.remove(esm_name)

if opt.only_esm:
    releases = [release for release in releases if 'esm' in release]

if len(args) > 0:
    # be smarter so we can handle CVEs as paths
    cves = []
    uems = []
    for cve in args:
        dir = os.path.dirname(cve)
        cve = os.path.basename(cve)
        cves.append(cve)
        # handle embargoed symlink
        if 'embargoed' in dir:
            uems.append(cve)
else:
    (cves, uems) = cve_lib.get_cve_list()
    if opt.skip_embargoed:
        for cve in uems:
            cves.remove(cve)
        uems = []
(table, priority, cves, namemap, cveinfo) = cve_lib.load_table(cves, uems, opt)

if opt.html:
    print('<table id="cves" class="table table-bordered table-hover">')
    print(htmlTableHeader(opt.show_crd, opt.show_assignee))
    print("<tbody>\n")
else:
    prefix = '%-35s'
    # maximum CRD is XXXX-XX-XX XX:XX:XX XXX
    crdformat = '%24s'
    assigneeformat = '%11s'
    relformat = '%14s'
    if not opt.omit_header:
        print(prefix % ' ', end=' ')
        if opt.show_crd:
            print(crdformat % 'CRD', end=' ')
        if opt.show_assignee:
            print(assigneeformat % 'Assigned To', end=' ')
        if not opt.omit_status:
            for rel in releases:
                if '/' in rel:
                    (base, ppa) = rel.split('/')
                    rel = "%s/" % (base)
                    # abbreviate the ppa name
                    for i in ppa.split('-'):
                        rel += i[0]
                print(relformat % rel, end=' ')
        print()

for cve in sorted(cves):
    if cve not in table:
        print('triage: %s' % (cve), file=sys.stderr)
        continue
    if opt.untriaged and priority[cve]['default'] in ['negligible', 'low', 'medium', 'high', 'critical']:
        continue
    if opt.priority and priority[cve]['default'] not in opt.priority:
        continue
    if opt.public_date:
        dategiven = time.strptime(opt.public_date, "%Y-%m-%d")
        try:
            crddate = time.strptime(cveinfo[cve]['PublicDate'], "%Y-%m-%d %H:%M:%S %Z")
        except ValueError:
            try:
                crddate = time.strptime(cveinfo[cve]['PublicDate'], "%Y-%m-%d %H:%M:%S")
            except ValueError:
                crddate = time.strptime(cveinfo[cve]['PublicDate'], "%Y-%m-%d")
        if crddate < dategiven:
            continue
    cve_needed = False
    for pkg in sorted(table[cve].keys()):
        if opt.debug:
            print("%s: Processing '%s'" % (cve, pkg), file=sys.stderr)

        # Skip CVEs for some package if flavor is specified
        in_flavor = True
        if support_db is not None:
            in_flavor = False
            for rel in releases:
                if support_tool._is_flavor('src', support_db, pkg, rel, opt.flavor):
                    if opt.only_flavor:
                        if rel not in table[cve][pkg] and pkg not in map[rel]:
                            # DNE
                            continue
                        elif rel in table[cve][pkg] and table[cve][pkg][rel] in ['released', 'not-affected', 'ignored']:
                            # Nothing to do
                            continue
                        elif support_tool._is_canonical_supported('src', support_db, pkg, rel):
                            # Canonical supports it
                            continue
                    in_flavor = True
        if not in_flavor:
            continue

        action_needed = False   # A package in any release needs updating
        released = False        # A package in any release has been fixed
        supported = False       # A package in any release is supported and needs to be fixed
        partner = False         # A package in any release is partner-supplied
        universe = False        # A package in any release is in universe and needs to be fixed

        mark = dict()
        for rel in releases:
            pkg_rel_supported = False  # Package in *this* release is supported

            mark.setdefault(rel, "")

            # Find any unlisted items and decide if they are DNE or needs-triage
            if rel not in table[cve][pkg]:
                table[cve][pkg].setdefault(rel, 'needs-triage')
                if pkg not in map[rel]:
                    table[cve][pkg][rel] = 'DNE'

            # FIXME: this shouldn't be possible, with the cve_lib checks now...
            if not table[cve][pkg][rel] in ['N/A', 'DNE', 'active', 'ignored', 'not-affected', 'needs-triage', 'needed', 'deferred', 'pending', 'released']:
                print('%s %s %s: bad state "%s"' % (cve, pkg, rel, table[cve][pkg][rel]), file=sys.stderr)
                continue

            # Check if package is supported on this release
            pkgname = pkg
            if pkg in namemap and rel in namemap[pkg]:
                pkgname = namemap[pkg][rel]
            if cve_lib.is_supported(map, pkgname, rel, cveinfo[cve]):
                pkg_rel_supported = True
                if opt.supported:
                    mark[rel] = "*"

            # Is it already released?
            if table[cve][pkg][rel] in ['released']:
                released = True

            # Is is needed still?
            if table[cve][pkg][rel] in ['needed', 'deferred', 'pending', 'needs-triage', 'active']:
                action_needed = True
                # Does this package need attention for a supported release?
                if pkg_rel_supported:
                    supported = True

            # A partner issue?
            if cve_lib.is_partner(map, pkg, rel):
                partner = True

            if opt.debug:
                print("%s: %s: %s (%s, released:%d)" % (cve, pkg, table[cve][pkg][rel], rel, released), file=sys.stderr)

        if action_needed:
            if opt.html:
                show_priority = priority[cve]['default']
                if pkg in priority[cve]:
                    show_priority = priority[cve][pkg]
                print('<tr class="%s">' % (quote(show_priority)), end=' ')
                print('<td class="cve"><a href="%s">%s</a></td>' % (quote(cve), escape(cve)), end=' ')

                # this rigamorole is to add 'pkgpending' to the source package
                # column *only* if all releases are 'pending' (so we can still
                # see the vulnerability priority)
                numpending = 0
                for rel in releases:
                    if table[cve][pkg][rel] == "pending":
                        numpending += 1

                pkgclass = 'pkg'
                if numpending == len(releases):
                    pkgclass += 'pending'

                print('<td class="%s"><a href="pkg/%s.html">%s</a></td>' % (pkgclass, quote(pkg), escape(pkg)), end=' ')
                if opt.show_crd:
                    crd = cveinfo[cve]['PublicDate']
                    print('<td class="crd">%s</td>' % (escape(crd)), end=' ')
                if opt.show_assignee:
                    assignee = cveinfo[cve]['Assigned-to']
                    print('<td class="assignee">%s</td>' % (escape(assignee)), end=' ')

                for rel in releases:
                    if mark[rel] != '*':
                        universe = True
                    pkg_status = table[cve][pkg][rel]
                    if not opt.unmask_needed and table[cve][pkg][rel] in ['active', 'deferred']:
                        pkg_status = 'needed'
                    print('<td class="%s">%s%s</td>' % (quote(pkg_status), escape(pkg_status), escape(mark[rel])), end=' ')
                print('<td style="font-size: small;"><a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=%s">Mitre</a> <a href="https://launchpad.net/bugs/cve/%s">LP</a> <a href="http://security-tracker.debian.org/tracker/%s">Debian</a></td>' % (quote(cve), quote(cve), quote(cve)), end=' ')
                print('<td>', end=' ')
                if opt.supported:
                    if partner:
                        print('<p>PARTNER</p>', end=' ')
                    elif supported:
                        print('<p>SUPPORTED</p>', end=' ')
                    if universe:
                        print('<p>UNIVERSE</p>', end=' ')
                if cve in uems:
                    print('<p>EMBARGOED</p>', end=' ')
                print('</td>', end=' ')
                print('</tr>')
            else:
                print(prefix % ('%s %s:' % (cve, pkg)), end=' ')
                if opt.show_crd:
                    crd = cveinfo[cve]['PublicDate']
                    print(crdformat % crd, end=' ')
                if opt.show_assignee:
                    assignee = cveinfo[cve]['Assigned-to']
                    print(assigneeformat % assignee, end=' ')
                if not opt.omit_status:
                    for rel in releases:
                        pkg_status = table[cve][pkg][rel]
                        if not opt.unmask_needed and table[cve][pkg][rel] in ['active', 'deferred']:
                            pkg_status = 'needed'
                        if opt.show_released_version and pkg_status == 'released':
                            released_version = table[cve][pkg][rel+"_comment"]
                            pkg_status = pkg_status +" "+ released_version

                        print(relformat % ('%s%s' % (pkg_status, mark[rel])), end=' ')
                if action_needed and released:
                    print('[out of sync]', end=' ')
                if opt.supported:
                    if partner:
                        print('PARTNER', end=' ')
                    elif supported:
                        print('SUPPORTED', end=' ')
                    if universe:
                        print('UNIVERSE', end=' ')
                if cve in uems:
                    print('EMBARGOED', end=' ')
                # Priority needs to remain the last item in the list for other
                # scripts that parse ubuntu-table output.
                if pkg in priority[cve]:
                    print(priority[cve][pkg], end='')
                else:
                    print(priority[cve]['default'], end='')
                print()
            cve_needed = True

    if not opt.no_retired and not cve_needed and not opt.skip_devel and cve not in uems:
        print('retire: %s' % (cve), file=sys.stderr)

if opt.html:
    print("</tbody>")
    print("</table>")
