#!/usr/bin/python3
# -*- coding: utf-8 -*-

#	cve-inform : Tool that figures out unpublished vulnerabilities
#	Copyright (C) 2019-2026 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 datetime                   import datetime
from sys                        import argv
from collections                import defaultdict
from cve_inform.const           import TARGETS
from cve_inform.mediators       import Mediators
from cve_inform.process_tasks   import GetUpdPackages, DetectFixes
from cve_inform.err             import Error
from cve_inform.print           import PrintOperationAnnouncement, PrintStatus,\
	PrintIrrelevantEntries, OP_LOOK_FORW, OP_LOOK_BACKW, OP_DETECT_IRR
from cve_inform.distro          import DistroLib
from cve_inform.current_content import CurrentContent
from cve_inform.blogupd         import BlogUpdater

PRODUCTS = sorted(TARGETS.keys())
BRANCHES = sorted({b for branches, _ in TARGETS.values() for b in branches})

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

argparser = argparse.ArgumentParser()
argparser.add_argument(
	'-b', '--branches',
	metavar='BRANCH_NAME', type=str, nargs='+',
	choices=BRANCHES,
	default=BRANCHES,
	help=f'Branches to consider ({", ".join(BRANCHES)}), all by default'
	)
argparser.add_argument(
	'-p', '--products',
	metavar='PRODUCT_NAMES', type=str, nargs='+',
	choices=PRODUCTS,
	default=PRODUCTS,
	help=f'Products to consider ({", ".join(PRODUCTS)}), all by default'
	)
argparser.add_argument(
	'--starting_date',
	metavar='YYYY-MM-DD', type=str,
	help='The date from which the tasks will be processed (can\'t be used '
	'together with the --task_id parameter)'
	)
argparser.add_argument(
	'--vul_ids',
	metavar='VUL_IDS', type=str, nargs='+', default=[],
	help='Vulnerability IDs that should be considered'
	)
argparser.add_argument(
	'--packages',
	metavar='PACKAGE_NAME', type=str, nargs='+',
	help='Packages that should be considered'
	)
argparser.add_argument(
	'--task_id',
	metavar='SIX_DIGITS', type=str,
	help='The task id for creating the markdown file (can\'t be used together '
	'with the --starting_date parameter)'
	)
argparser.add_argument(
	'--host', default=[],
	metavar='SSH_HOSTNAME', type=str, nargs='+',
	help='Chain of SSH hostnames that leads to a computer running cve-manager '
	'and alt-tasks'
	)
argparser.add_argument(
	'-c', '--content_dir',
	metavar='CONTENT_DIR', type=str, default='./content',
	help='Dir with previously generated md files'
	)
argparser.add_argument(
	'-d', '--distro_lists_path',
	metavar='DIR_PATH_OR_GIT_URL', type=str,
	help='Distination of distro package lists (a dir path in a file system '
	'or an URL of a git project)'
	)
argparser.add_argument(
	'--allow_discarded_alt_errata',
	action='store_true',
	help='Include fix vulnerabilities taken from ALT errata even if those '
	'vulnerabilities is not included in the cve-manager basic analysis'
	)
argparser.add_argument(
	'--alt_errata_only',
	action='store_true',
	help='Don\'t use cve-manager at all, use only ALT errata instead'
	)
argparser.add_argument(
	'--look_behind',
	action='store_true',
	help='Search not only for fixes concerning the new tasks, but also search '
	'for missing fixes from the past'
	)
argparser.add_argument(
	'--detect_irrelevant_entries',
	action='store_true',
	help='List fixes entries from the past that have no actuality at the '
	'moment, new posts will not be generated'
	)
argparser.add_argument(
	'--show_posts',
	action='store_true',
	help='Show generated posts in the console (stdout)'
	)
argparser.add_argument(
	'--debug',
	action='store_true',
	help='Run in debug mode'
	)
args = argparser.parse_args()

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

if args.starting_date:
	if args.task_id:
		argparser.print_help()
		exit(1)
	try:
		starting_date = \
			datetime.strptime(args.starting_date, '%Y-%m-%d').date()
	except Exception as e:
		Error(str(e))
else:
	starting_date = None

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

# Read the distro lists
distributions = DistroLib(args.distro_lists_path) if args.distro_lists_path \
	else None

# Read content of the published tasks
current_content = CurrentContent(args.content_dir, args.branches)

# A dict for detected unpublished tasks that fix some vulnerabilities
unpub_tasks_with_fixes = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))

# Interfaces with the "cve-manager" toolkit and with the "alt-tasks" utility
alt_tasks, alt_errata, cve_manager = \
	Mediators(args.host, args.debug, args.alt_errata_only)
mediator = alt_errata if alt_errata else cve_manager

bdu_to_cve_map, err = mediator.GetMappedCVEIds()
if err:
	Error(err)

# For all products (distros)
for product in sorted(args.products):

	branches_of_this_product, distro_name = TARGETS.get(product, ([], ''))

	# For all branches that this distro can be built on
	for branch in [b for b in sorted(args.branches) if b in branches_of_this_product]:

		if args.detect_irrelevant_entries:
			param_variations = (True,)
		elif args.look_behind:
			param_variations = (False, True)
		else:
			param_variations = (False,)

		if not args.detect_irrelevant_entries:
			PrintOperationAnnouncement(OP_LOOK_FORW, product, branch, starting_date)

		distro = distributions.Get(distro_name, branch) if distributions else {}
		if distro == None:
			Error('Can\'t get a list of packages of the '
				f'{branch}:{distro_name} distro')

		# Use task id of a very last published task (not taking the branch
		# into account) if nothing has been published for this branch
		if starting_date:
			last_published_task = None
		else:
			last_published_task = current_content.GetLastPublishedTask(branch)
			if not last_published_task:
				starting_date = current_content.GetDateOfVeryLastPublication()
				if not starting_date:
					Error('Can\'t get the date of the last publication')

		for look_behind in param_variations:

			if look_behind:
				op_type = OP_DETECT_IRR if args.detect_irrelevant_entries \
					else OP_LOOK_BACKW
				PrintOperationAnnouncement(op_type, product, branch, starting_date)

			# Get info about all tasks that have been completed after the last
			# published task
			tasks, warn = alt_tasks.GetTasksInfo(
					branch,
					starting_date,
					last_published_task,
					args.task_id,
					distro,
					current_content.GetFirstTaskDate(product, branch) \
						if look_behind else '')
			if tasks == None:
				Error('; '.join(warn))

			packages_before_and_after_updates = GetUpdPackages(
				tasks, set(args.packages) if args.packages else {})

			ids_of_processed_tasks = {}
			irrelevant_enries = {}

			if packages_before_and_after_updates:

				# Check issues for specified packages of a specified branch
				if cve_manager:
					issues, err = cve_manager.Check(branch,
						packages_before_and_after_updates,
						args.allow_discarded_alt_errata,
						set(args.vul_ids))
				else:
					issues, err = alt_errata.Check(branch,
						packages_before_and_after_updates,
						set(args.vul_ids))
				if issues == None:
					Error(err)
				if err:
					warn.add('WARNING: ' + err)

				# Detect fixes for identified issues
				ids_of_processed_tasks, irrelevant_enries, w = DetectFixes(
						tasks,
						product,
						branch,
						set(args.packages) if args.packages else {},
						issues,
						bdu_to_cve_map,
						current_content.GetVuls(product, branch) if look_behind else {},
						look_behind,
						args.detect_irrelevant_entries,
						unpub_tasks_with_fixes)
				warn |= w

			if args.detect_irrelevant_entries:
				PrintIrrelevantEntries(irrelevant_enries, distro)
			else:
				PrintStatus(ids_of_processed_tasks, look_behind, warn)

if args.detect_irrelevant_entries:
	exit(0)

vul_id_to_package_names_map, err = \
	mediator.GetVulIdsToPackageNamesMap(args.branches)
if err:
	Error(err)

# Information about all new tasks has been collected, now generate and display
# and/or save the blog posts
BlogUpdater(unpub_tasks_with_fixes, bdu_to_cve_map, vul_id_to_package_names_map) \
	.Update(args.content_dir, args.show_posts)

exit(0)
