#!/bin/bash

# Log file for cc scripts
CC_LOGFILE="/var/log/cc-scripts.log"
# error file for cc scripts - if exists, something went wrong
CC_ERRFILE="/var/log/cc-scripts.err"
# if exists, everything applied correctly but at least one setting (e.g. a
# GRUB cmdline change) needs a reboot before it's actually in effect - this
# is a distinct, non-error state from $CC_ERRFILE, not a lesser version of it
CC_REBOOTFILE="/var/log/cc-scripts.reboot-required"

# This is the function to be used instead of echo. Prefixed with the calling
# script's own basename so a log file interleaving several scripts' output
# stays attributable line-by-line, e.g. when `apply` dispatches them in
# sequence into the same $CC_LOGFILE.
# $@ data to be echoed
# return: nothing, but output on screen (and log file, once cc_start_logging
# has redirected our stdout there)
cc_echo() {
	echo "$(basename "$0"): $*"
}

# Both `apply` (which writes to /etc, /var/log, and calls systemctl) and
# `check` (which, despite being read-only, still needs to read root-only
# files like grub.cfg - typically mode 600 - to check them) require root.
# Without this guard, a non-root `check` invocation doesn't fail outright:
# individual check_* functions instead report spurious FAILs from
# "Permission denied" reading a file they can't access, which looks exactly
# like a real compliance finding rather than the actual problem (forgetting
# sudo). Exits directly rather than going through cc_exit: neither script's
# own error-recording machinery (writing to $CC_ERRFILE, etc.) can be trusted
# to work without root either.
cc_require_root() {
	if [ "$(id -u)" -ne 0 ]; then
		echo "$(basename "$0") must be run as root" >&2
		exit 1
	fi
}

# Start teeing this process's own stdout/stderr into $CC_LOGFILE, once. Idempotent
# and safe to call from both `apply` (before dispatching scripts, so every
# dispatched subprocess inherits the redirect already active) and a script's
# own standalone entry point (when invoked directly, with no `apply` parent).
cc_start_logging() {
	if [ -n "${CC_LOGGING_STARTED:-}" ]; then
		return 0
	fi
	exec > >(tee -a "$CC_LOGFILE") 2>&1
	export CC_LOGGING_STARTED=1
	if [ "$CC_ROTATED_PREVIOUS_RUN" = "1" ]; then
		cc_echo "(previous run's log/error files rotated to *.1)"
	fi
}

# List every grub.cfg this system's bootloader might actually use: the
# standard BIOS-style path, plus any vendor-specific EFI ones already
# present under the ESP (the directory name varies by product - "sles",
# "sle_rt", ... - globbed rather than hardcoded, the same "test -e ... &&
# grub2-mkconfig" pattern the guide's own manual FIPS instructions use for
# whichever ones exist, rather than assuming a specific name). A kernel
# cmdline change written to only one of these, on a system that actually
# boots via a different one, would silently never take effect - confirmed
# as a real gap on a real target: an Agama-installed box can have an EFI
# System Partition mounted at /boot/efi (this package's own storage
# profile creates one) even while it happens to currently boot via BIOS
# compatibility, so a script that only ever checked/regenerated
# /boot/grub2/grub.cfg was silently assuming a boot path it hadn't
# actually verified.
cc_grub_cfg_paths() {
	local f
	[ -e "/boot/grub2/grub.cfg" ] && echo "/boot/grub2/grub.cfg"
	for f in /boot/efi/EFI/*/grub.cfg; do
		[ -e "$f" ] && echo "$f"
	done
}

# Regenerate every grub.cfg cc_grub_cfg_paths finds.
cc_grub_regenerate() {
	local f
	while IFS= read -r f; do
		grub2-mkconfig -o "$f"
	done < <(cc_grub_cfg_paths)
}

# Does every grub.cfg cc_grub_cfg_paths finds contain $1? False (with
# nothing printed) if there are no such files at all - callers should
# already have confirmed $GRUB_DEFAULT/grub2-mkconfig exist before relying
# on this meaning anything.
cc_grub_cfg_has() {
	local param="$1"
	local f
	local found=0
	while IFS= read -r f; do
		found=1
		grep -q "$param" "$f" || return 1
	done < <(cc_grub_cfg_paths)
	[ "$found" = 1 ]
}

# Replace a given file with a new file and back up the old file
# The function ensures that the permissions are kept
# $1: new file that will be overwritten over the old old one (i.e. source)
# $2: file to be replaced (i.e. destination)
cc_replace() {
	local src=$1
	local dst=$2
	local date
	local mode
	local owner

	if [ -z "$src" ] || [ -z "$dst" ]; then
		echo "Missing input parameters" >&2
		return 1
	fi

	if [ ! -e "$src" ] || [ ! -e "$dst" ]; then
		echo "Files missing" >&2
		return 1
	fi

	date=$(date +"%Y%m%d%H%M%S")
	mode=$(stat -L -c '%a' "$dst")
	owner=$(stat -L -c '%U:%G' "$dst")

	# Create backup
	cp "$dst" "$dst.$date"
	chmod "$mode" "$dst.$date"
	chown "$owner" "$dst.$date"

	# copy the new file
	cp -p "$src" "$dst"
	chmod "$mode" "$dst"
	chown "$owner" "$dst"
}

# Exit handler - to be used instead of exit()
# It uses the same parameter as exit()
# $1: exit code - 0 (fully compliant), 1 (a genuine failure), or 2 (applied
#     correctly, but needs a reboot before it's actually in effect - see
#     $CC_REBOOTFILE above; this is our own convention, not borrowed from any
#     command this package happens to call - e.g. fips-mode-setup's own exit
#     codes mean something different and aren't reused here on purpose)
# return: function NEVER returns
cc_exit() {
	local code=$1
	local errfile_dir

	if [ "$code" -eq 2 ]; then
		cc_echo "applied correctly, but a reboot is required before it takes full effect"
		if [ -d "/mnt/var/log" ]; then
			CC_REBOOTFILE="/mnt/$CC_REBOOTFILE"
		fi
		errfile_dir=$(dirname "$CC_REBOOTFILE")
		if [ ! -d "$errfile_dir" ]; then
			mkdir -p "$errfile_dir" || {
				echo "Directory for $CC_REBOOTFILE cannot be created"
				exit 255
			}
		fi
		touch "$CC_REBOOTFILE"
	elif [ "$code" -ne 0 ]; then
		cc_echo "non-recoverable ERROR exit code $code"
		if [ -d "/mnt/var/log" ]; then
			if [ -f "$CC_ERRFILE" ]; then
				mv "$CC_ERRFILE" "/mnt/$CC_ERRFILE"
			fi
			CC_ERRFILE="/mnt/$CC_ERRFILE"
		fi
		if [ ! -d "$(dirname "$CC_ERRFILE")" ]; then
			mkdir -p "$(dirname "$CC_ERRFILE")" || {
				echo "Directory for $CC_ERRFILE cannot be created"
				exit 255
			}
		fi
		touch "$CC_ERRFILE"
	fi

	exit "$code"
}

# Report any error occurred during CC config
# in case there is an error, it returns with error code 1 - autoyast should
# cause a rerun of the script to block. A pending-reboot state (no error, but
# $CC_REBOOTFILE present) is reported distinctly and exits 2 - it isn't a
# failure, but it also isn't "successfully established" yet either.
report_error() {
	if [ -e "$CC_ERRFILE" ]; then
		cc_echo "FAILURE: CC configuration performed with at least one error"
		cc_echo "FAILURE: Check contents of $CC_LOGFILE for details"
		cc_echo "FAILURE: You may reboot now but the system is NOT in the evaluated configuration"
		cc_echo "FAILURE: You may also check the log file on a different console"
		exit 1
	fi

	if [ -e "$CC_REBOOTFILE" ]; then
		cc_echo "Common Criteria Evaluated Configuration applied, but a reboot is"
		cc_echo "required before it is fully in effect"
		exit 2
	fi

	cc_echo "Common Criteria Evaluated Configuration"
	cc_echo "successfully established"
}

# If this is the top-level `apply` driver sourcing us (as opposed to an
# individual sub-script sourcing us on its own, e.g. for standalone testing,
# or `check` sourcing every script for its check_* functions), rotate the
# previous run's log/error/reboot-required files before anything from this
# run gets logged. Without this, a single historical failure (or a stale
# reboot-required marker from before the reboot actually happened) would
# stick around forever, silently making report_error report the same
# outcome on every subsequent run even after it's no longer true - and the
# log would otherwise grow unbounded, mixing every run together with no
# clear boundary between them. Keeps exactly one previous generation (*.1);
# sub-scripts sourcing this file directly are unaffected, since they must
# not wipe log entries already written earlier in the same `apply` pass.
# `check` never reaches this at all in practice: it never calls
# cc_start_logging, so nothing it does ever touches these files in the
# first place.
CC_ROTATED_PREVIOUS_RUN=0
if [ "$(basename "$0")" = "apply" ]; then
	if [ -e "$CC_LOGFILE" ] || [ -e "$CC_ERRFILE" ] || [ -e "$CC_REBOOTFILE" ]; then
		[ -e "$CC_LOGFILE" ] && mv -f "$CC_LOGFILE" "$CC_LOGFILE.1"
		[ -e "$CC_ERRFILE" ] && mv -f "$CC_ERRFILE" "$CC_ERRFILE.1"
		[ -e "$CC_REBOOTFILE" ] && mv -f "$CC_REBOOTFILE" "$CC_REBOOTFILE.1"
		CC_ROTATED_PREVIOUS_RUN=1
	fi
fi
