#!/usr/bin/env python3
# Copyright 2010, Canonical, Ltd.
# Author: Jamie Strandboge <jamie@canonical.com>
# License: GPLv3
#
# Report items for the security team to sponsor. Used for reporting based on
# https://wiki.ubuntu.com/SecurityTeam/SponsorsQueue
#
# $ report-todo-sponsoring					# all open bugs
# $ report-todo-sponsoring --status New				# untriaged bugs
# $ report-todo-sponsoring --syncs				# requested fake syncs
# $ report-todo-sponsoring --has-patch				# bugs with patches
# $ report-todo-sponsoring --tags 'security-verification'	# bugs needing uploading to ubuntu-security-proposed
# $ report-todo-sponsoring --status Confirmed			# bugs requiring action by the security team
# $ report-todo-sponsoring --status 'Fix Committed'		# bugs to possibly pocket copy
# $ report-todo-sponsoring --verification			# bugs tagged for verification
# $ report-todo-sponsoring --full-report			# wikified report for SecurityTeam/SponsorsQueue
#
# To emulate the old pull-in-progress:
# $ report-todo-sponsoring --status 'In Progress' --team ubuntu-security --has-patch
#
# Can use the following bugs for testing:
# https://launchpad.net/bugs/495638
# https://launchpad.net/bugs/503123
# https://launchpad.net/bugs/503126

from __future__ import print_function

import cve_lib
import optparse
import re
import subprocess
import sys

parser = optparse.OptionParser()
parser.add_option("--debug", help="Verbose processing output", action='store_true')
parser.add_option("--team", help="Find bugs for team (default ubuntu-security-sponsors)", metavar="TEAM", action='store', default='ubuntu-security-sponsors')
parser.add_option("--status", help="Specify status for bugs", metavar="STATUS", action='store', default=None)
parser.add_option("--syncs", help="Show fake sync request bugs", action='store_true')
parser.add_option("--tags", help="Show bugs with tags (space separated list)", metavar="TAGS", action='store', default='')
parser.add_option("--has-patch", help="Show bugs with patches", action='store_true')
parser.add_option("--full-report", help="Full report for sponsorship queue", action='store_true')
parser.add_option("--verification", help="Open bugs tagged for verification", action='store_true')
parser.add_option("--last-message-date", help="Sort bugs by their most recent message", action='store_true')

(opt, args) = parser.parse_args()

def debug(s):
    '''Print debug message'''
    if opt.debug:
        print("DEBUG: %s" % (s), file=sys.stderr)

def sort_by_date_last_message(task):
    return task[1].bug.date_last_message

def sort_by_id(task):
    return task[0]

def print_bugs(bugs, status=None, tags=[], last_message_date=False):
    '''Output a collection of bugs'''
    if last_message_date:
        bugs = sorted(bugs.items(), key=sort_by_date_last_message, reverse=True)
    else:
        bugs = sorted(bugs.items(), key=sort_by_id)

    count = 0
    for (id, task) in bugs:
        tstatus = task.status
        bugid = task.bug.id
        if status:
           if tstatus != status:
               debug("Skipping LP: #%d (%s != %s)" % (bugid, tstatus, status))
               continue

        ttags = task.bug.tags
        if len(tags) > 0:
            found_tag = False
            for t in tags:
                if t in ttags:
                    found_tag = True
            if not found_tag:
                debug("Skipping LP: #%d (could not find tags '%s')" % (bugid, ' '.join(tags)))
                continue

        cves = []
        tcves = task.bug.cves
        if tcves != None:
            for c in tcves:
                cves.append("CVE-" + c.sequence)

        bug_target_name = task.bug_target_name
        if not ' (' in bug_target_name:
            print("Skipping target name=%s (LP: #%d)" % (bug_target_name, bugid), file=sys.stderr)
            continue

        pkg, target = bug_target_name.split(' (', 1)
        target = target.split(')')[0]
        if ' ' in target:
            target, targeted_to = target.split(' ', 1)

        if target and target.lower() != 'ubuntu':
            debug('skipping target "%s" (%s) (LP: #%d)' % (target, pkg, bugid))
            continue

        if tstatus in ['Fix Released', 'Invalid', "Won't Fix"]:
            debug('skipping (pkg:%s status:%s LP: #%d)' % (pkg, tstatus, bugid))
            continue

        if not re.match(r'^[a-z0-9][a-z0-9+\.\-]+$', pkg):
            print("Bad package name '%s' (LP: #%d)" % (pkg, bugid), file=sys.stderr)
            continue

        release = "ubuntu"
        if ':' in id:
            release = id.split(':')[1]
        print("=== Source: %s (%s) ===" % (pkg, release))
        print(" * Status: %s" % tstatus, end=" ")
        if tstatus == "Fix Committed":
            print("(committed on %s)" % str(task.date_fix_committed).split(' ')[0])
        elif tstatus == "In Progress":
            print("(marked 'In Progress' on %s)" % str(task.date_in_progress).split(' ')[0])
        else:
            print("")

        if ttags:
            print(" * Tags: %s" % ' '.join(ttags))
        print(" * URL: https://launchpad.net/bugs/%d" % bugid)

        if len(cves) > 0:
            print(" * CVES:", end=" ")
            for c in cves:
                print("%s" % (c), end=" ")
            print("")

        if last_message_date:
            print(" * Last message: %s" % task.bug.date_last_message);

        print("")

        count += 1

    # How many unique bugs did we report on?
    return count

#
# Connect to Launchpad
#
try:
    import lpl_common
except:
    print("lpl_common.py seems to be missing.  Please create a symlink from $UQT/common/lpl_common.py to $UCT/scripts/", file=sys.stderr)
    sys.exit(1)

# Load configuration
cve_lib.read_config()

# API interface
debug("Connecting to LP ...")
lp = lpl_common.connect()

ubuntu = lp.distributions['ubuntu']
debug("Distribution: %s" % ubuntu)
team = lp.people[opt.team]
debug("Team: %s" % team)

tags = ''
if opt.syncs:
    tags = 'sync'
elif opt.tags:
    tags = opt.tags
# why didn't this work?
#elif opt.verification:
#    tags = 'security-verification verification-needed verification-failed verification-done'

search_status = ['New',
                 'Incomplete',
                 'Confirmed',
                 'Triaged',
                 'In Progress',
                 'Fix Committed']
if opt.status:
    search_status = [opt.status]

debug("Loading bugs ...")
bugs = {}
# get bugs for a specific series, then for 'Ubuntu' if we haven't already added
# the bug
for rel in cve_lib.releases + ['ubuntu']:
    if rel == 'ubuntu':
        obj = ubuntu
    else:
        series = ubuntu.getSeries(name_or_version=rel)
        if not series.active:
            continue
        obj = series

    debug("")
    debug("Gathering bugs for %s" % rel)
    if tags == '':
        task_collection = obj.searchTasks(bug_subscriber=team,
                                          omit_targeted=False,
                                          has_patch=opt.has_patch,
                                          status=search_status)
    else:
        task_collection = obj.searchTasks(bug_subscriber=team,
                                          omit_targeted=False,
                                          tags=tags,
                                          has_patch=opt.has_patch,
                                          status=search_status)

    for task in task_collection:
        bugid = task.bug.id
        if rel != 'ubuntu' or bugid not in bugs:
            bugs["%d:%s" % (bugid, rel)] = task
debug("Found %d bugs across all supported releases" % len(bugs))

if opt.full_report:
    print("== Untriaged ==")
    print_bugs(bugs, "New", last_message_date=opt.last_message_date)

    print("\n== Confirmed (need action) ==")
    print_bugs(bugs, "Confirmed", last_message_date=opt.last_message_date)

    print("\n== Fix Committed (review for pocket copying) ==")
    print_bugs(bugs, "Fix Committed", last_message_date=opt.last_message_date)

    print("\n== Tagged for verification ==")
    print_bugs(bugs, tags=["security-verification"], last_message_date=opt.last_message_date)
    print_bugs(bugs, tags=["verification-needed"], last_message_date=opt.last_message_date)
    print_bugs(bugs, tags=["verification-failed"], last_message_date=opt.last_message_date)
    print_bugs(bugs, tags=["verification-done"], last_message_date=opt.last_message_date)

    print("\n== Has patch ==")
    subprocess.call([sys.argv[0], '--has-patch'])
else:
    vtags = []
    if opt.verification:
        vtags = tags=["security-verification", "verification-needed", "verification-failed", "verification-done"]
    n = print_bugs(bugs, opt.status, tags=vtags, last_message_date=opt.last_message_date)

    if opt.status:
        print("%d bugs found for '%s' with status '%s'" % (n, opt.team, "|".join(search_status)))
    else:
        print("%d open bugs found for '%s'" % (n, opt.team))

sys.exit(0)

