#!/usr/bin/sh

# Script to concatenate rules files found in a base fapolicyd rules directory
# to form a single /etc/fapolicyd/compiled.rules file suitable for loading
# into the daemon

# When forming the interim rules file, both empty lines and comment
# lines (starting with # or <whitespace>#) are stripped as the source files
# are processed.
#
# Having formed the interim rules file, the script checks if the file is empty
# or is identical to the existing /etc/fapolicyd/compiled.rules and if either
# of these cases are true, it does not replace the existing file.
#

# Variables
#
# DestinationFile:
#   Destination rules file
# SourceRulesDir:
#   Directory location to find component rule files
# TmpRules:
#   Temporary interim rules file in the destination directory
# RuleFiles:
#   Temporary list of rule source files
# SortedRules:
#   Temporary naturally sorted list of rule source files
# RawRules:
#   Temporary concatenation of rule source files before filtering
# ASuffix:
#   Suffix for previous fapolicyd.rules file if this script replaces it.
#   The file is left in the destination directory with suffix with $ASuffix

OldDestinationFile=/etc/fapolicyd/fapolicyd.rules
DestinationFile=/etc/fapolicyd/compiled.rules
DestinationDir=${DestinationFile%/*}
SourceRulesDir=/etc/fapolicyd/rules.d
TmpRules=
RuleFiles=
SortedRules=
RawRules=
ASuffix="prev"
OnlyCheck=0
LoadRules=0
RETVAL=0
usage="Usage: $0 [--check|--load]"

# Delete the interim file on faults
cleanup_tmp() {
	if [ "${TmpRules}" != "" ] ; then
		rm -f "${TmpRules}"
	fi
	if [ "${RuleFiles}" != "" ] ; then
		rm -f "${RuleFiles}"
	fi
	if [ "${SortedRules}" != "" ] ; then
		rm -f "${SortedRules}"
	fi
	if [ "${RawRules}" != "" ] ; then
		rm -f "${RawRules}"
	fi
}

fail_tmp() {
	echo "$0: $1" >&2
	cleanup_tmp
	exit 1
}

ruleset_hash() {
	sha256sum "$1" 2>/dev/null | awk '{print $1}'
}

emit_ruleset_hash() {
	hash=$(ruleset_hash "$1")
	if [ "${hash}" = "" ] ; then
		return 1
	fi
	echo "$0: Ruleset identity: ${hash}"
	return 0
}

trap 'cleanup_tmp; exit 1' HUP INT QUIT PIPE TERM

try_load() {
	pid=$(pidof fapolicyd)
	if [ $LoadRules -eq 1 ] && [ "$pid" != "" ] ; then
		kill -HUP "$pid"
		RETVAL=$?
	fi
}

while [ $# -ge 1 ]
do
	if [ "$1" = "--check" ] ; then
		OnlyCheck=1
	elif [ "$1" = "--load" ] ; then
		LoadRules=1
	else
		echo "$usage"
		exit 1
	fi
	shift
done

# Check environment
if [ ! -d "${SourceRulesDir}" ]; then
	echo "$0: No rules directory - ${SourceRulesDir}"
	try_load
	exit 1
fi

# Preserve the no-rules migration behavior while ignoring non-rule files.
# Source names are later read one line at a time, so whitespace is retained.
if ! first_rule=$(find "${SourceRulesDir}" -mindepth 1 -maxdepth 1 \
	! -name '.*' \( -type f -o -type l \) -name '*.rules' -print -quit); then
	echo "$0: Cannot list rules in ${SourceRulesDir}" >&2
	exit 1
elif [ "${first_rule}" = "" ] ; then
	echo "No rules in ${SourceRulesDir}"
	# won't call this an error as they may not have migrated
	exit 0
elif [ -e "${OldDestinationFile}" ] ; then
	echo "Error - both old and new rules exist. Delete one or the other"
	exit 1
fi

# Create the interim rules file ensuring its access modes protect it
# from normal users and strip empty lines and comment lines.
umask 0137
RuleFiles=$(mktemp "${DestinationDir}/.fagenrules.XXXXXXXX") ||
	fail_tmp "Cannot create temporary rule file list in ${DestinationDir}"
SortedRules=$(mktemp "${DestinationDir}/.fagenrules.XXXXXXXX") ||
	fail_tmp "Cannot create sorted rule file list in ${DestinationDir}"
RawRules=$(mktemp "${DestinationDir}/.fagenrules.XXXXXXXX") ||
	fail_tmp "Cannot create temporary raw rules file in ${DestinationDir}"
TmpRules=$(mktemp "${DestinationDir}/.compiled.rules.XXXXXXXX") ||
	fail_tmp "Cannot create temporary rules file in ${DestinationDir}"

if ! find "${SourceRulesDir}" -mindepth 1 -maxdepth 1 \
	! -name '.*' \( -type f -o -type l \) -name '*.rules' -print \
	> "${RuleFiles}" ; then
	fail_tmp "Cannot list rules in ${SourceRulesDir}"
fi
# Keep the documented ls -v-style ordering without splitting whitespace names.
if ! sort -V "${RuleFiles}" > "${SortedRules}" ; then
	fail_tmp "Cannot sort rules in ${SourceRulesDir}"
fi

while IFS= read -r rules || [ "${rules}" != "" ] ; do
	if ! cat "${rules}" >> "${RawRules}" ; then
		fail_tmp "Cannot read rule file ${rules}"
	fi
	if ! printf '\n' >> "${RawRules}" ; then
		fail_tmp "Cannot write temporary rules file ${RawRules}"
	fi
done < "${SortedRules}"

if ! printf '## This file is automatically generated from %s\n' \
	"${SourceRulesDir}" >> "${TmpRules}" ; then
	fail_tmp "Cannot write temporary rules file ${TmpRules}"
fi
if ! awk '
BEGIN   {
        rest = 0;
} {
        if (length($0) < 1) { next; }
        if (match($0, "^[[:space:]]*#")) { next; }
        rules[rest++] = $0;
}
END     {
        for (i = 0; i < rest; i++) { printf "%s\n", rules[i]; }
}' "${RawRules}" >> "${TmpRules}" ; then
	fail_tmp "Cannot filter rules in ${RawRules}"
fi

fapolicyd-cli --check-rules "${TmpRules}"
err=$?
if [ $err -ne 0 ]; then
    echo "Rules file content:"
    cat -n "${TmpRules}"
    cleanup_tmp
    exit $err
fi

# If the same then quit
if cmp -s "${TmpRules}" "${DestinationFile}" > /dev/null 2>&1 ; then
	echo "$0: No change"
	emit_ruleset_hash "${DestinationFile}" || fail_tmp \
		"Failed to hash final ruleset ${DestinationFile}"
	cleanup_tmp
	try_load
	exit $RETVAL
elif [ $OnlyCheck -eq 1 ] ; then
	echo "$0: Rules have changed and should be updated"
	cleanup_tmp
	exit 0
fi

# Otherwise prepare the new file without touching the installed rules. If any
# step fails before the final rename, the previous compiled rules remain active.
chmod 0644 "${TmpRules}" || fail_tmp "Cannot set mode on ${TmpRules}"
chgrp fapolicyd "${TmpRules}" || fail_tmp "Cannot set group on ${TmpRules}"
hash=$(ruleset_hash "${TmpRules}") ||
	fail_tmp "Failed to hash candidate ruleset ${TmpRules}"
if [ "${hash}" = "" ] ; then
	fail_tmp "Failed to hash candidate ruleset ${TmpRules}"
fi
sync "${TmpRules}" || fail_tmp "Failed to fsync ${TmpRules}"

if [ -f "${DestinationFile}" ]; then
	cp -p "${DestinationFile}" "${DestinationFile}.${ASuffix}" ||
		fail_tmp "Cannot backup ${DestinationFile}"
fi

mv -f "${TmpRules}" "${DestinationFile}" ||
	fail_tmp "Cannot install ${DestinationFile}"
TmpRules=

# Restore context on MLS system.
if [ -x /usr/sbin/restorecon ] ; then
	/usr/sbin/restorecon -F "${DestinationFile}"
fi
sync "${DestinationDir}" 2>/dev/null ||
	echo "$0: Warning - failed to fsync ${DestinationDir}" >&2

echo "$0: Ruleset identity: ${hash}"

cleanup_tmp
try_load
exit $RETVAL
