#!/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
from collections        import defaultdict
from mysql.connector    import Error as MySQLError
from types              import SimpleNamespace
from cve_manager.common import NewArgParser, Init
from cve_manager.conf   import DB_CON_SEC
from cve_manager.const  import DATA_SOURCES, VUL_MARKERS
from cve_monitor.query  import FatalError, Query

DESCRIPRION = 'Compare issues detected by cve-manager with issues included ' \
	'in ALT errata'
CVE_MANAGER_ISSUES = 'cve_manager_issues'
ALT_ERRATA = 'alt_errata'
MODES = {
	CVE_MANAGER_ISSUES: 'fixed',
	ALT_ERRATA: 'alt_errata_fixed',
	}
FIX_VARIANTS = {v: k for k, v in MODES.items()}

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Parsing the arguments

argparser = argparse.ArgumentParser(description=DESCRIPRION)
argparser.add_argument(
	'-s', '--show_missing',
	metavar='ISSUES', type=str, choices=list(MODES.keys()), required=True,
	help=f'Display either vulnerabilities that are present in cve-manager '
	f'issues and are not present in ALT errata issues (-s {ALT_ERRATA}) '
	f'or vice versa (-s {CVE_MANAGER_ISSUES})'
	)
argparser = NewArgParser(ptype='b', base=argparser)
argparser.add_argument(
	'-t', '--types_of_issues',
	metavar='TYPE_OF_ISSUES', type=str, nargs='+', default=[],
	choices=DATA_SOURCES,
	help=f'Type of issues ({" or ".join(DATA_SOURCES)}, all by default)'
	)
argparser.add_argument(
	'-v', '--verbose',
	action='store_true',
	help='Enable verbose output mode'
	)
argparser = NewArgParser(ptype='mm', base=argparser)
args = argparser.parse_args()

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Initialising the helper objects and reading the configuration file inbetween

printer, conf = Init(args, monitor=True)
mysql_config, = conf.Get([DB_CON_SEC])
query = Query(mysql_config, args.branches, printer)

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Put given entry to the container

def ReadEntry(issues_entry, container):

	vul_id, name, map_name, ver, rel, _, vul_ver, _, _, _, _, _ = issues_entry
	container[(vul_id, name)] = (map_name, ver, rel, vul_ver)

	return

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Compare issues of the specified type with the other type of issues

def Compare(type_of_issues, fix, d):

	other_fix = ''
	for el in FIX_VARIANTS:
		if el != fix:
			other_fix = el
	if not other_fix:
		printer.Err('There should be at least two variants of a fix status')
		exit(1)

	vul_marker = VUL_MARKERS.get(type_of_issues, '??')[:-1]

	def Label(s):
		mode = FIX_VARIANTS.get(s, "???")
		return f'fixed {mode.replace("cve_", "cve-").replace("_", " ")}'

	def NoVulnerabilities(s, branch):
		return f'No {vul_marker} IDs in {Label(s)} for the {branch} branch'

	for branch, other_issues in d.get(other_fix, {}).items():
		count = 0
		if len(other_issues) == 0:
			msg = NoVulnerabilities(other_fix, branch)
			printer.Warn(msg)
			continue
		issues = d.get(fix, {}).get(branch, {})
		if len(issues) == 0:
			msg = NoVulnerabilities(fix, branch)
			printer.Warn(msg)
			continue
		for k, v in other_issues.items():
			if k not in issues:
				count += 1
				vul_id, name = k
				map_name, ver, rel, vul_ver = v
				msg = vul_id
				if args.verbose:
					msg += (f' for {branch} package {name}-{ver}-{rel} '
						f'(vulnerable version is {vul_ver}) ' +
						(f'that is mapped to product "{map_name}"' if map_name
							else 'that isn\'t mapped to any product') + ' '
						f'is present in {Label(other_fix)} '
						f'and not present in {Label(fix)} ')
				printer.LineEnd(msg)
		if args.verbose:
			msg = (f'{count} {vul_marker} ID{"s" if count > 1 else ""} total '
				f'that {"are" if count > 1 else "is"} present in '
				f'{Label(other_fix)} and not present in {Label(fix)}')
			printer.LineEnd(msg + '\n')

	return

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

if __name__ == '__main__':

	# Checking the selected branches and the selected types of issues
	branches, err = conf.SelectBranches(args.branches)
	if not err:
		types_of_issues, err = conf.SelectDataSources(args.types_of_issues)
	if err:
		printer.Err(err)
		exit(1)

	context = SimpleNamespace()
	context.branches = branches
	context.packages = []
	context.years = []
	context.vul_ids = []
	context.order = ''
	context.patch = False
	context.group = False
	context.cure  = False

	try:
		for type_of_issues in types_of_issues:
			d = defaultdict(lambda: defaultdict(dict))
			for fix in FIX_VARIANTS:
				context.fix = fix
				if not query.SetContext(context):
					printer.Err('Context lacks some parameters')
					exit(1)
				issues_label = fix.replace("_", " ") + ' ' \
					f'{type_of_issues.upper()} issues'
				issues, _ = query.Issues(type_of_issues)
				if not issues:
					printer.Warn(f'No {issues_label}')
					continue
				for branch in branches:
					issues_for_this_branch = {}
					for entry in issues.get(branch, []):
						ReadEntry(entry, d[fix][branch])
					if not d[fix][branch]:
						printer.Warn(f'No {issues_label} for {branch} branch')
						continue
			Compare(type_of_issues, MODES[args.show_missing], d)
	except (MySQLError, FatalError) as err:
		printer.Err(str(err))
		exit(1)

	exit(0)
