#!/usr/bin/python3

#	cve-manager : CVE management tool
#	Copyright (C) 2017-2022 Alexey Appolonov
#
#	This program is free software: you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation, either version 3 of the License, or
#	(at your option) any later version.
#
#	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, see <http://www.gnu.org/licenses/>.

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

import argparse
from sys                 import argv
from time                import sleep
from mysql.connector     import Error as mysql_err
from cve_manager.desc    import MONITOR
from cve_manager.defines import DATA_SOURCES, NVD_DATA_SRC
from cve_manager.common  import GetDef, NewArgParser, Init
from cve_manager.conf    import DB_CON_SEC, COMMON_SEC
from cve_monitor.distro  import DistroPackages
from cve_monitor.query   import FatalError, Query
from cve_monitor.render  import Renderer

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Initializing global objects and parsing the arguments

argparser = argparse.ArgumentParser(description=MONITOR)
argparser.add_argument(
	'--show',
	metavar='WHAT_YOU_WANT_TO_SEE', type=str,
	choices=['issues', 'issues_history', 'comp', 'vul_desc', 'map',
	'map_history', 'ex'], default='issues',
	help='Choose to get issues tables ("--show issues", default), issues '
	'history or package names map history tables ("--show issues_history" or '
	'"--show map_history"), or perform comparisment between branches '
	'("--show comp"), or get full descriptions of a searched vulnerabilities '
	'("--show vul_desc"), or get all mapped pairs of names ("--show map"), '
	'or get usage examples ("--show ex")'
	)
argparser.add_argument(
	'-t', '--itype',
	metavar='ISSUES_TYPE', type=str, nargs=1,
	choices=DATA_SOURCES,
	help=f'Monitor {" or ".join(DATA_SOURCES)} issues (all by default)'
	)
argparser.add_argument(
	'-f', '--fix',
	metavar='FIX_STATUS', type=str,
	choices=['all', 'fixed', 'unfixed', 'unclear'], default='all',
	help='Monitor "fixed", "unfixed" or "all" issues ("all" by default)'
	)
argparser = NewArgParser(ptype='bp', base=argparser)
argparser.add_argument(
	'--distro_list_src',
	metavar='DISTRO_NAME', type=str,
	help='Monitor packages of specified distribution represented with a list '
	'of names of source packages'
	)
argparser.add_argument(
	'--distro_list_bin',
	metavar='DISTRO_NAME', type=str,
	help='Monitor packages of specified distribution represented with a list '
	'of names of binary packages'
	)
argparser.add_argument(
	'-v', '--vul_ids',
	metavar='VUL_ID', type=str, nargs='+',
	help='Show information for given IDs of vulnerabilities'
	)
argparser.add_argument(
	'-y', '--years',
	metavar='YEAR', type=str, nargs='+',
	help='Years of the issues to monitor'
	)
argparser.add_argument(
	'-o', '--order',
	metavar='ORDER_BY', type=str,
	choices=['id', 'package', 'score2', 'score3', 'fix'],
	help='Order query results by vulnerability ID ("id"), '
	'package name ("package"), CVSS v2 score ("score2"), '
	'CVSS v3 score ("score3") or fix status ("fix")'
	)
argparser.add_argument(
	'--group',
	action='store_true',
	help='Group queried issues by package name'
	)
argparser.add_argument(
	'--cure',
	action='store_true',
	help='Add cure suggestions'
	)
argparser.add_argument(
	'--patch',
	action='store_true',
	help='Add urls with patches (works only with reports on unfixed issues)'
	)
argparser.add_argument(
	'-w', '--write_files',
	action='store_true',
	help='Save reports in files created in a directory that is stated in the '
	'cve-monitor conf file, use specified title (see "--title") as a file name '
	'prefix'
	)
argparser.add_argument(
	'-m', '--mail',
	action='store_true',
	help='Send emails in accordance with a specified title (see "--title") '
	'on addresses listed in the cve-mail conf file'
	)
argparser.add_argument(
	'--title',
	metavar='TITLE', type=str,
	help='Report title - an additional parameter required for the "--mail" and '
	'"--write_files" parameters'
	)
argparser.add_argument(
	'--unite',
	action='store_true',
	help='Unite all reports into single email'
	)
argparser = NewArgParser(ptype='mm', base=argparser)
args = argparser.parse_args()

if len(argv) < 2:
	argparser.print_help()
	exit(1)

# Showing usage examples
if args.show == 'examples':
	PrintExamples()
	exit(0)

SPLIT_REPORTS = not args.unite and any([args.mail, args.write_files])

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Initialising a printer and reading the configuration file

printer, conf = Init(args, monitor=True)
mysql_config, common_params = conf.Get([DB_CON_SEC, COMMON_SEC])

# DB interface and reports renderer
query  = Query(mysql_config, printer)
render = Renderer(printer)
rprinter = render.printer

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Showing usage examples

def PrintExamples():

	exe = 'cve-monitor'
	examples = ((
		'Show all issues for the "Sisyphus" branch and "p8" branch',
			'--show issues -b Sisyphus p8'),(
		'Show unfixed issues for "pluma" and "firefox" packages of the "p7" branch',
			'--show issues -f unfixed -b p7 -p pluma firefox'),(
		'Show fixed issues dating 2016 and 2017 for all branches',
			'--show issues -f fixed -y 2016 2017'),(
		'Show CVEs that unfixed in "c8" branch but fixed in "Sisyphus" and "p8" branch',
			f'--show comp -b c8 p8 Sisyphus -t {NVD_DATA_SRC} -f fixed'),(
		'Show all cve info for "vlc" package',
			'--show vul_desc -p vlc'),(
		'Show information about the CVE-2017-0001',
			'--show vul_desc -v CVE-2017-0001'),(
		'Show mapped packages for the branch "Sisyphus"',
			'--show map -b Sisyphus'))

	for desc, params in examples:
		print(f'{desc}:\n\t{exe} {params}')

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Showing issues tables with the given filters applied

def ShowIssues(type_of_issues):

	seniors = conf.GetSeniors(args.branches)
	issues, suggestions = query.Issues(type_of_issues, seniors)

	for branch in args.branches:
		if SPLIT_REPORTS:
			rprinter.PageBegin(f'{type_of_issues}_{branch}')
		issues_for_this_branch = issues.get(branch)
		render.Issues(type_of_issues, branch, issues_for_this_branch,
			suggestions.get(branch))

	return

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Showing a diff between history tables and issues/src tables

def ShowHistory(type_of_history, type_of_issues):

	entries = query.History(type_of_history, type_of_issues)

	for branch in args.branches:
		if SPLIT_REPORTS:
			rprinter.PageBegin(f'{type_of_issues}_{branch}')
		entries_for_this_branch = entries.get(branch, [])
		params = [type_of_issues, branch, entries_for_this_branch]
		if type_of_history == 'issues':
			render.Issues(*params, this_is_new_issues=True)
		else:
			render.ChangesOfMap(*params)

	return

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Showing full descriptions of searched vulnerabilities

def ShowVulDesc(type_of_issues):

	entries = query.Entries(type_of_issues)

	if any(entry.get('desc', []) for entry in entries.values()):
		RenderIssues = render.NVDEntries if type_of_issues == NVD_DATA_SRC \
			else render.FSTECEntries
		for k, v in entries.items():
			branch, package, _, _ = k
			RenderIssues(v['desc'], v['fix'], branch, package)
	else:
		rprinter.LineEnd('There are no entries that satisfy given request')

	return

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Showing comparison between first branch and the rest of a given branches

def ShowComp(type_of_issues):

	diff = query.CompareBranches(type_of_issues, args.branches,
		printing=(not args.mail))

	for branch in args.branches[1:]:
		if SPLIT_REPORTS:
			rprinter.PageBegin(f'{type_of_issues}_{branch}')
		diff_for_this_branch = diff.get(branch)
		render.Diff(type_of_issues, args.branches[0], branch, diff_for_this_branch)

	return

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Showing mapped packages

def ShowMap(type_of_issues):

	for branch in args.branches:
		if SPLIT_REPORTS:
			rprinter.PageBegin(f'{type_of_issues}_{branch}')
		pairs = query.PackagesMap(type_of_issues, branch)
		n_packages, n_products = query.MapCount(branch, type_of_issues)
		render.Map(type_of_issues, branch, pairs)
		render.MapCount(n_packages, n_products)

	return

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

if __name__ == '__main__':

	# Checking the arguments
	err = ''
	if args.show == 'issues_history' and args.patch:
		err = '"--patch" flag can\'t be used with "--show issues_history" ' \
			'parameter'
	elif args.show == 'comp' and len(args.branches) < 2:
		err = 'At least two branches must be specified when comparing branches'
	elif args.group and args.order == 'id':
		err = '"--group" flag and "--order id" parameter can\'t be used ' \
			'simultaneously'
	elif args.group or args.patch:
		if args.fix != 'unfixed':
			err = '"--group" and "--patch" flags can be used only ' \
				'for "unfixed" fix status ("-f unfixed")'
		elif args.group and args.patch:
			err = '"--group" and "--patch" flags can\'t be used ' \
				'simultaneously'
	if err:
		printer.Err(err)
		exit(1)

	# Matching the values with the field names of the *_issues tables
	if args.order:
		if args.order == 'id':
			args.order = 'vul_id'
		elif args.order == 'package':
			args.order = 'name'
		elif args.order == 'score2':
			args.order = 'cvss_score_v2'
		elif args.order == 'score3':
			args.order = 'cvss_score_v3'
		elif args.order != 'fix':
			args.order = ''

	# There is a history of unfixed issues only
	if args.show == 'issues_history':
		args.fix = 'unfixed'

	# Forming a list of branches that will be processed
	args.branches, err = conf.SelectBranches(args.branches)
	if err or not args.branches:
		printer.Err(err if err else 'Can\'t form a list of branch names')
		exit(1)

	# If mail report requested
	mailer = None
	if args.mail:
		from cve_monitor.mail import Mailer
		mailer = Mailer(GetDef('CONF_FILE_MAIL', args.debug))
		# Enabling a buffering of an output
		rprinter.PageBegin()

	# If writing to a file is requested
	fwriter = None
	if args.write_files:
		from cve_monitor.file_writer import FWriter
		fwriter = FWriter(common_params.get('out_dir', ''))
		# Enabling a buffering of an output
		rprinter.PageBegin()

	# Taking distro list into account
	args.distro = None
	if args.distro_list_src or args.distro_list_bin:
		if args.distro_list_src and args.distro_list_bin:
			printer.Err('"--distro_list_src" and "--distro_list_bin" '
				'params can\'t be used simultaneously')
			exit(1)
		home_dir = common_params['download']
		is_src_distro_list = bool(args.distro_list_src)
		args.distro = args.distro_list_src if is_src_distro_list \
			else args.distro_list_bin
		packages = {}
		for branch in args.branches[:(None if args.show != 'comp' else 1)]:
			distro_packages, err = DistroPackages(
				home_dir, is_src_distro_list, branch, args.distro)
			packages[branch if args.show != 'comp' else ''] = \
				list(set(args.packages) | distro_packages)
			if err:
				printer.Err(err)
				exit(1)
		args.packages = packages
	else:
		args.packages = {'': args.packages} if args.packages else {}

	# Setting the context
	args.all_branches, _ = conf.SelectBranches()
	if not query.SetContext(args) or not render.SetContext(args):
		printer.Err('Context lacks some parameters')
		exit(1)

	# Defining the types of issues that will be monitored
	types_of_issues, _ = conf.SelectDataSources(args.itype)
	for type_of_issues in types_of_issues:
		if not query.TableExists(type_of_issues + '_vul_import'):
			printer.Err('The database lacks some of the basic tables '
				'for the specified type of issues')
			exit(1)

	# For all types of issues
	try:
		for type_of_issues in types_of_issues:
			if not any([mailer, fwriter]):
				render.Title(type_of_issues)
			if args.show == 'comp':
				ShowComp(type_of_issues)
			elif args.show == 'vul_desc':
				ShowVulDesc(type_of_issues)
			elif args.show == 'map':
				ShowMap(type_of_issues)
			elif args.show in ('issues_history', 'map_history'):
				type_of_history = args.show[:-len('_history')]
				ShowHistory(type_of_history, type_of_issues)
			else:
				ShowIssues(type_of_issues)
	except (mysql_err, FatalError) as err:
		printer.Err(str(err))
		exit(1)

	# Sending emails and/or writing into files (if requested)
	if mailer or fwriter:

		# Send report on a given branch and return an err msg if err occurred
		def Report(type_of_issues=None, branch=None):
			report = rprinter.PageEnd(f'{type_of_issues}_{branch}')
			if not report:
				return ''
			printer.LineEnd(report)
			if SPLIT_REPORTS and args.show == 'comp':
				branch = f'{args.branches[0]}-{branch}'
				if args.distro:
					branch = f'({branch})'
			for worker in (fwriter, mailer):
				if not worker:
					continue
				err = worker.Execute(args.title, report, type_of_issues, branch,
					args.distro)
				if err:
					return err
			return ''
		# - nested func

		# One branch per email
		err = ''
		if SPLIT_REPORTS and args.show != 'vul_desc':
			for type_of_issues in types_of_issues:
				for i, branch in enumerate(args.branches):
					if i == 0 and args.show == 'comp':
						continue
					err = Report(type_of_issues, branch)
					if err:
						break
					sleep(5)
		# All branches in a single email
		else:
			err = Report()
		if err:
			printer.Err(err)
			exit(1)

	exit(0)
