#!/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 mysql
import argparse
from sys                        import argv
from collections                import defaultdict
from ax.ver                     import IsDateVer, MergeVersions
from cve_manager.defines        import DATA_SOURCES, NVD_DATA_SRC, \
	FSTEC_DATA_SRC, VUL_MARKERS
from cve_manager.desc           import ISSUES
from cve_manager.common         import GetDef, NewArgParser, Init
from cve_manager.conf           import DB_CON_SEC, LOCAL_SYS
from cve_manager.parallel       import Parallel
from cve_manager.ignored_pairs  import IgnoredPairs
from cve_manager.software       import SplitSoftware
from cve_issues.mediator        import Mediator
from cve_issues.excluded_issues import ExcludedIssues
from cve_issues.fix             import GetFixRecord
from cve_issues.helpers         import ConvertFSTECVer

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Everything that one needs to update an issues table exc a data to be insert

class Setup():

	def __init__(self, branch, data_source, packages, excluded_issues,
			excluded_products):

		self.branch = branch
		self.data_source = data_source if data_source in DATA_SOURCES \
			else None
		self.packages = packages
		self.excluded_issues = excluded_issues
		self.excluded_products = excluded_products

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

	def is_set(self):

		return bool(self.data_source)

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
	# Check that match between specified package name and product names is
	# excluded

	def IsExcludedMatch(self, package_name, package_url, vendor, product):

		for cpe in ((vendor, product), (vendor, '*')):
			for branch in ('', self.branch):
				for url in ('', package_url):
					if cpe in self.excluded_products.get(branch, {}) \
							.get(package_name, {}) \
							.get(url, set()):
						return True

		return False

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

argparser = argparse.ArgumentParser(description=ISSUES)
argparser.add_argument(
	'--prepare',
	action='store_true',
	help='Recreate issues tables'
	)
argparser.add_argument(
	'-t', '--types',
	metavar='ISSUES_TYPE', type=str, nargs='+', default=[],
	choices=DATA_SOURCES,
	help=f'Issues types ({" or ".join(DATA_SOURCES)}, all by default)'
	)
argparser = NewArgParser(ptype='bpm', base=argparser)
args = argparser.parse_args()

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

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

printer, conf = Init(args)
mysql_config, = conf.Get([DB_CON_SEC])
mediator = Mediator(mysql_config, printer)

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Handle a given portion of vulnerabilities (as a separate process)

VULNERABLE = 0b01
NON_VULNERABLE = 0b10
VULNERABLE_AND_NOT_VULNERABLE = 0b11

def ProcessVulnerabilities(vul, setup, send_pipe, err_event):

	err = ''
	data = defaultdict(set)
	buf = defaultdict(list)
	vulnerability_flag = defaultdict(int)
	vul_marker = VUL_MARKERS.get(setup.data_source, '')
	is_nvd_data_source = setup.data_source == NVD_DATA_SRC
	IsExcluded = setup.excluded_issues.IsExcluded
	branch = setup.branch if setup.branch != LOCAL_SYS \
		else conf.GetLocalSysBranch()

	# Collect data for each vulnerability
	for vul_year, vul_num, software_list in vul:

		if err_event.is_set():
			break

		saved_vul_versions = defaultdict(list)
		vul_id = f'{vul_year}-{vul_num}'
		complete_vul_id = f'{vul_marker}{vul_id}'

		# For each combination of vulnerable products of this vulnerability
		for vul_software_comb in SplitSoftware(software_list):

			buf.clear()
			vulnerability_flag = 0
			there_are_unprocessed_products = False

			# For each product of this combination
			for vul_software in vul_software_comb:

				vulnerable_by_itself = vul_software.IsVulnerable()

				product = vul_software.Product()
				if not product:
					there_are_unprocessed_products = True
					continue

				vendor = vul_software.Vendor()
				vul_ver = vul_software.Ver()
				if not vul_ver:
					there_are_unprocessed_products = True
					continue

				#if not is_nvd_data_source:
				#	vul_ver = ConvertFSTECVer(vul_ver)

				vul_ver_is_date = IsDateVer(vul_ver)
				vul_ver_is_any_ver = not vul_ver_is_date and vul_ver in ('(;)', '-')

				got_vulnerable_packages = False
				product_is_not_processed = True

				# For each package mapped to this product
				for package_id, package_name, package_ver, package_ver_is_date, \
							package_rel, package_url, package_fixes \
						in setup.packages.get(product, []):

					if setup.IsExcludedMatch(package_name, package_url,
							vendor, product):
						continue

					# Looking for a 'fixes' record of this vul for this package
					fix = 'c' if package_fixes and (vul_id in package_fixes) else ''
					# Checking that this issue is excluded
					is_excluded_issue, err = \
						IsExcluded(branch, complete_vul_id, package_name, package_ver)
					if is_excluded_issue == None:
						err_event.set()
						break
					fix += 'e' if is_excluded_issue else ''

					# Complementing the fix str with a result of comparison of versions
					if vul_ver_is_any_ver or vul_ver_is_date == package_ver_is_date:
						fix += GetFixRecord(package_ver, vul_ver, product,
							saved_vul_versions, fix)
					# There is no point in comparing a date-version with a normal version
					else:
						fix += '?'

					if not fix:
						continue

					# Writing the data into temporary container
					buf[(package_id, vul_ver)] = (fix, vulnerable_by_itself)

					if fix == 'V':
						got_vulnerable_packages = True

					product_is_not_processed = False

				# Updating the flag that shows whether some packages of this
				# combination are vulnerable or none of them are not vulnerable
				vulnerability_flag |= \
					VULNERABLE if got_vulnerable_packages else NON_VULNERABLE

				# If some package of this combination is excluded or not mapped
				# than this whole combination can't be considered vulnerable
				if product_is_not_processed:
					there_are_unprocessed_products = True

			for k, v in buf.items():
				fix, vulnerable_by_itself = v
				if not vulnerable_by_itself:
					continue
				package_id, vul_ver = k
				if there_are_unprocessed_products or \
						vulnerability_flag == VULNERABLE_AND_NOT_VULNERABLE:
					fix = 'x' if fix == 'V' else fix + 'x'
				data[(vul_year, vul_num, package_id, fix)].add(vul_ver)

	# Merging vulnerable versions
	data = [k + (' '.join(MergeVersions(list(v))),) for k, v in data.items()]

	# Sending data for all detected issues at once
	if data and not err_event.is_set():
		err = mediator.SendIssues(setup.branch, setup.data_source, data)
		if err:
			err_event.set()

	send_pipe.send([err])

	return

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Detect all CVE issues for a given branch using multiple processes

def DetectIssues(branch, target, vul, excluded_issues, excluded_products):

	# Forming a list of package descriptions
	packages = mediator.GetPackages(branch, target, args.packages)
	if not packages:
		return False

	setup = Setup(branch, target, packages, excluded_issues, excluded_products)
	if not setup.is_set():
		printer.Err(f'Wrong target "{target}"')
		return False

	msg = f'Detecting {target.upper()} issues for ' + \
		('the local system' if branch == LOCAL_SYS else f'{branch} branch')
	printer.LineBegin(msg)

	# Running multiple processes of issues detection
	warnings, ok = Parallel(ProcessVulnerabilities, vul, setup)

	if not ok:
		printer.Err('Some process has terminated with an error')
		return False

	for warn in warnings:
		printer.LineAddExtra(warn)

	printer.Success()

	return True

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

if __name__ == '__main__':

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

	# Determining the issues types
	targets, _ = conf.SelectDataSources(args.types)
	if not targets:
		printer.Err('Can\'t form a list of data sources')
		exit(1)

	# Getting a dict of issues that should not be presented
	excluded_issues = ExcludedIssues(GetDef('CONF_DIR', args.debug))

	# Detecting issues for all given types of issues
	for target in targets:
		# Getting a list of lists of vuls (each list for a separate process)
		vul = mediator.GetVulnerabilities(target)
		if not vul and target != FSTEC_DATA_SRC:
			exit(1)
		# Reading a list of ignored mapping pairs
		printer.LineBegin(f'Loading {target.upper()} ignore list')
		excluded_products, msg = \
			IgnoredPairs(GetDef('CONF_DIR', args.debug), target).Read()
		if excluded_products == None:
			printer.Err(msg)
			exit(1)
		printer.Success(msg)
		# Detecting issues for all given branches
		for branch in branches:
			if args.prepare:
				if not mediator.RecreateIssuesTable(branch, target):
					exit(1)
			elif not mediator.CheckIssuesTable(branch, target):
				exit(1)
			if not DetectIssues(branch, target, vul, excluded_issues, excluded_products) and \
					target != FSTEC_DATA_SRC:
				exit(1)

	exit(0)
