#!/usr/bin/python3

#	cve-manager : CVE management tool
#	Copyright (C) 2017-2025 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
import gc
from sys                 import argv
from time                import sleep
from mysql.connector     import Error as mysql_err
from cve_manager.common  import GetDef, NewArgParser, Init
from cve_manager.conf    import DB_CON_SEC, COMMON_SEC
from cve_manager.const   import DATA_SOURCES, NVD_DATA_SRC
from cve_manager.desc    import MONITOR
from cve_manager.package import Package
from cve_monitor.distro  import DistroPackages
from cve_monitor.query   import FatalError, Query
from cve_monitor.render  import Renderer

CT_PLAIN = 'plain'
CT_HTML  = 'html'
CONTENT_TYPES = [CT_PLAIN, CT_HTML]
FIX_CHOICES = ['all', 'fixed', 'unfixed', 'unclear', 'alt_errata_fixed',
	'alt_errata_unfixed']

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Parsing the arguments and initializing global objects

argparser = argparse.ArgumentParser(description=MONITOR)
argparser.add_argument(
	'--show',
	metavar='WHAT_YOU_WANT_TO_SEE', type=str,
	choices=['issues', 'issues_history', 'comp', 'vul_desc', 'names_map',
	'map_history', 'ids_of_all_issues', 'bdu_to_cve_map', 'ex'],
	default='issues',
	help='Choose to get issues tables ("--show issues", default), issues '
	'history tables ("--show issues_history"), or get changes of package names '
	'to product names map ("--show map_history"), or get a list of package '
	'names and all vulnerability IDs related to these packages '
	'("--show ids_of_all_issues") or perform a comparison between branches '
	'("--show comp"), or get full descriptions of a searched vulnerabilities '
	'("--show vul_desc"), or get all mapped pairs of names ("--show names_map")'
	', or get a map of BDU IDs to CVE IDs ("--show bdu_to_cve_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=FIX_CHOICES, default='all',
	help=f'Monitor {", ".join(FIX_CHOICES)} ("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',
	metavar='CONTENT_TYPE', type=str, nargs='?', choices=CONTENT_TYPES,
	const=CT_PLAIN,
	help='Send emails in accordance with a specified title (see "--title") ' \
	'on addresses listed in the cve-mail conf file, available types are ' + \
	', '.join(CONTENT_TYPES)
	)
argparser.add_argument(
	'--title',
	metavar='TITLE', type=str,
	help='Report title - an additional parameter required for the "--mail" and '
	'"--write_files" parameters'
	)
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)

# Initialising a printer and reading the configuration file
printer, conf = Init(args, monitor=True,
	content_type_html=(args.mail==CT_HTML))
mysql_config, common_params = conf.Get([DB_CON_SEC, COMMON_SEC])

# 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)

# Initialising the DB interface and the reports renderer
query  = Query(mysql_config, args.branches, printer)
render = Renderer(printer, content_type=args.mail)
rprinter = render.printer

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Show the 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 names_map -b Sisyphus'))

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

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Send report on a given branch and return an err msg if err occurred

first_report_sent = False

def SendOrWriteReport(workers, type_of_issues=None, branch=None, err_log=[]):

	global first_report_sent

	report = rprinter.PageEnd()

	if not report:
		return ''

	printer.LineEnd(report, end=('' if args.mail==CT_HTML else None))

	if not workers:
		report = ''
		gc.collect()
		return ''

	if args.show == 'comp':
		branch = f'{args.branches[0]}-{branch}'
		if args.distro:
			branch = f'({branch})'

	for worker in workers:
		err = worker.Execute(args.title, report, type_of_issues, branch,
			args.distro)
		if err:
			err_log.append(err)

	report = ''
	gc.collect()

	# Sleep for a few seconds between reports
	if first_report_sent:
		sleep(5)
	first_report_sent = True

	return '; '.join(err_log)

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

def ShowIssues(workers):

	err_log = []

	for type_of_issues in types_of_issues:

		if not workers:
			render.Title(type_of_issues)

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

		for branch in args.branches:

			rprinter.PageBegin()

			issues_for_this_branch = issues.get(branch)
			render.Issues(type_of_issues, branch, issues_for_this_branch,
				suggestions.get(branch))

			SendOrWriteReport(workers, type_of_issues, branch, err_log)

	return '; '.join(err_log)

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

def ShowHistory(workers):

	err_log = []
	type_of_history = args.show[:-len('_history')]

	for type_of_issues in types_of_issues:

		if not workers:
			render.Title(type_of_issues)

		entries = query.History(type_of_history, type_of_issues)

		for branch in args.branches:

			rprinter.PageBegin()

			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)

			SendOrWriteReport(workers, type_of_issues, branch, err_log)

	return '; '.join(err_log)

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Show a full descriptions of searched vulnerabilities

def ShowVulDesc(workers):

	rprinter.PageBegin()

	for type_of_issues in types_of_issues:

		if not workers:
			render.Title(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 SendOrWriteReport(workers)

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

def ShowComp(type_of_issues):

	err_log = []

	for type_of_issues in types_of_issues:

		if not workers:
			render.Title(type_of_issues)

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

		for branch in args.branches[1:]:

			rprinter.PageBegin()

			render.Diff(type_of_issues, args.branches[0], branch,
				diff.get(branch))

			SendOrWriteReport(workers, type_of_issues, branch, err_log)

	return '; '.join(err_log)

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Show mapped packages

def ShowPackagesToProductsMap(type_of_issues):

	err_log = []

	for type_of_issues in types_of_issues:

		if not workers:
			render.Title(type_of_issues)

		for branch in args.branches:

			rprinter.PageBegin()

			pairs = query.PackagesToProductsMap(type_of_issues, branch)
			n_packages, n_products = query.MapCount(branch, type_of_issues)
			render.Map(pairs, type_of_issues, branch, 'product names')
			render.MapCount(n_packages, n_products)

			SendOrWriteReport(workers, type_of_issues, branch, err_log)

	return '; '.join(err_log)

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Show IDs of all issues of a specified type related to packages of specified
# branches

def ShowIDsOfAllIssues(workers):

	err_log = []

	for type_of_issues in types_of_issues:

		for branch in args.branches:

			rprinter.PageBegin()

			pairs = query.IDsOfAllIssues(branch, type_of_issues)
			render.Map(pairs, type_of_issues, branch, 'issues')

			SendOrWriteReport(workers, type_of_issues, branch, err_log)

	return '; '.join(err_log)

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Show CVE IDs mapped to BDU IDs

def ShowMappedCVEIds(workers):

	rprinter.PageBegin()

	render.Map(query.MappedCVEIds())

	return SendOrWriteReport(workers)

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

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 not args.fix.endswith('unfixed'):
			err = '"--group" and "--patch" flags can be used only ' \
				'for "unfixed" and "alt_errata_unfixed" fix status ' \
				'("-f unfixed" or "-f alt_errata_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'

	# If mail report requested
	mailer = None
	if args.mail:
		if not args.title:
			printer.Err('The recipients of the mail are not specified')
			exit(1)
		from cve_monitor.mail import Mailer
		mail_conf_file_path = GetDef('CONF_FILE_MAIL', args.debug)
		mailer = Mailer(mail_conf_file_path, args.mail)

	# 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', ''))

	workers = [el for el in (mailer, fwriter) if el]

	# 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)
			if err:
				printer.Err(err)
				exit(1)
			if not is_src_distro_list:
				bin_packages = {bin_package.Name() for bin_package in
					[Package(full_name=p) for p in distro_packages]}
				distro_packages, err = query.SrcPackages(branch, bin_packages)
				if err:
					printer.Err(err)
					exit(1)
			packages[branch if args.show != 'comp' else ''] = \
				list(set(args.packages) | distro_packages)
		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)

	# Pick a right function for a requested operation, and then run it
	if args.show == 'comp':
		f = ShowComp
	elif args.show == 'vul_desc':
		f = ShowVulDesc
	elif args.show == 'names_map':
		f = ShowPackagesToProductsMap
	elif args.show in ('issues_history', 'map_history'):
		f = ShowHistory
	elif args.show == 'ids_of_all_issues':
		f = ShowIDsOfAllIssues
	elif args.show == 'bdu_to_cve_map':
		f = ShowMappedCVEIds
	else:
		f = ShowIssues
	try:
		err = f(workers)
		if err:
			printer.Err(err)
			exit(1)
	except (mysql_err, FatalError) as err:
		printer.Err(str(err))
		exit(1)

	exit(0)
