#!/usr/bin/env bash
# Configure the apparmor.d package
# Copyright (C) 2021-2023 Alexandre Pujol <alexandre@pujol.io>
# SPDX-License-Identifier: GPL-2.0-only

set -eu

DISTRIBUTION="${DIST:-$(lsb_release --id --short)}"
readonly DISTRIBUTION="${DISTRIBUTION,,}"
readonly ROOT=.build

_die() { printf 'Error: %s\n' "$*" >&2 && exit 1; }
_warning() { printf '   Warning: %s\n' "$*" >&2; }
_title() { printf '%s\n' "$*" >&2; }
_msg() { printf ' - %s\n' "$*" >&2; }

# Process management function to run a function over all the profile files
# $1 The function to run
# $2 Usage message to print
process() {
	local len nprof nproc fct="$1" msg="$2"
	_msg "$msg"
	mapfile -t files < <(find "${ROOT:?}/apparmor.d" -type f)
	len="${#files[@]}"
	nproc=$(nproc)
	((nprof = len / nproc + 1))
	start=0
	end=$nprof
	for ((ii = 0; ii < nproc; ii++)); do
		$fct $start $end "${files[@]}" &
		((start = end + 1))
		((end = end + nprof))
	done
	wait
}

# Initialize a new clean apparmor.d build directory
initialize() {
	rm -rf "${ROOT:?}"
	rsync -a ./apparmor.d "$ROOT"
	rsync -a ./root "$ROOT"
}

# Ignore profiles and files as defined in dists/ignore/
ignore() {
	for name in main.ignore "$DISTRIBUTION.ignore"; do
		[[ -f "dists/ignore/$name" ]] || continue
		_msg "Ignore profiles/files in dists/ignore/$name"
		while read -r profile; do
			[[ "$profile" =~ ^\# ]] && continue
			[[ -z "$profile" ]] && continue
			if [[ -e "${ROOT:?}/$profile" ]]; then
				rm -r "${ROOT:?}/$profile"
			else
				find "$ROOT/apparmor.d" -iname "$profile" -type f -exec rm {} \;
			fi
		done <"dists/ignore/$name"
	done
}

# Synchronise all profiles in a new apparmor.d directory.
synchronise() {
	_msg "Synchronise all profiles."
	mv "${ROOT:?}/apparmor.d/groups/"*/* "${ROOT:?}/apparmor.d/"
	rm -rf "${ROOT:?}/apparmor.d/groups/"
	mv "${ROOT:?}/apparmor.d/profiles-"*-*/* "${ROOT:?}/apparmor.d/"
	rm -rf "${ROOT:?}/apparmor.d/profiles-"*
}

# Set the distribution specificities
configure() {
	case "$DISTRIBUTION" in
	arch | endeavouros | cachyos | manjarolinux)
		_msg "Configure libexec."
		LIBEXEC="/{usr/,}lib"
		sed -i -e '/Debian/d' "$ROOT/apparmor.d/tunables/etc.d/apparmor.d"
		;;

	debian | ubuntu | whonix | core)
		mv "$ROOT/apparmor.d/abstractions/trash.d/complete" "$ROOT/apparmor.d/abstractions/trash"
		case "$DISTRIBUTION" in
		core)
			mkdir -p $ROOT/root/usr/lib/systemd/system/systemd-udevd.service.d/
			cp -a dists/core/systemd-udevd.service $ROOT/root/usr/lib/systemd/system/systemd-udevd.service.d/apparmor.conf
			cp -a apparmor.d/groups/_full/systemd $ROOT/apparmor.d/systemd ;;
		debian | whonix)
			_msg "$DISTRIBUTION does not support abi 3.0 yet."
			find "$ROOT/apparmor.d" -type f -exec sed -e '/abi /d' -i {} \;
			cp -a dists/debian/abstractions/* $ROOT/apparmor.d/abstractions
			cp -a dists/debian/tunables/* $ROOT/apparmor.d/tunables ;;
		esac

		_msg "Configure libexec."
		LIBEXEC="/{usr/,}libexec"
		sed -i -e '/Archlinux/d' "$ROOT/apparmor.d/tunables/etc.d/apparmor.d"
		;;

	opensuse)
		LIBEXEC="/{usr/,}libexec"
		sed -i -e '/Archlinux/d' "$ROOT/apparmor.d/tunables/etc.d/apparmor.d"
		;;

	*) _die "$DISTRIBUTION is not a supported distribution." ;;
	esac
}

# Set flags on some profile
flags() {
	for name in main.flags "$DISTRIBUTION.flags"; do
		_msg "Set profiles flags from dists/flags/$name"

		while read -r profile; do
			IFS=' ' read -r -a manifest <<<"$profile"
			profile="${manifest[0]:-}" flags="${manifest[1]:-}"

			[[ "$profile" =~ ^\# || -z "$profile" ]] && continue
			path="${ROOT:?}/apparmor.d/$profile"
			if [[ ! -f "$path" ]]; then
				_warning "Profile $profile not found"
				continue
			fi

			# If flags is set, overwrite profile flag
			if [[ -n "$flags" ]]; then
				# Remove all flags definition, then set manifest' flags
				sed -e "s/flags=(.*)//" \
					-e "s/ {$/ flags=(${flags//,/ }) {/" \
					-i "$path"
			fi

		done <"dists/flags/$name"
	done
}

# Resolve the variables in the profile attachments
_resolve_attachments() {
	local path="$1"
	declare -A variables

	# Parse the variables in the profile hearder
	variables=(
		[libexec]="$LIBEXEC" [multiarch]="*-linux-gnu*"
		[user_share_dirs]="/home/*/.local/share" [etc_ro]="/{usr/,}etc/"
	)
	mapfile -t lines < <(grep '^@{.*}[ ]*[+=][ ]*.*$' "$path")
	for line in "${lines[@]}"; do
		value="${line##*=}"
		key="${line#^@{}"
		key="${key%%\}*}"
		key="${key/@{/}"
		variables[$key]+="${value}"
	done
	[ -z ${variables[exec_path]+x} ] && return

	# Resolve variable in profile attachments
	entrypoint="${variables[exec_path]}"
	while [[ "$entrypoint" =~ "@{".*"}" ]]; do
		name=${entrypoint#*@\{}
		name="${name%%\}*}"
		value="${variables[$name]# }"
		entrypoint="${entrypoint//@{${name}\}/${value}}"
	done
	entrypoint="${entrypoint# }"

	# If needed nest the attachments
	IFS=" " read -r -a attachments <<<"$entrypoint"
	if [[ "${#attachments[@]}" -ge 2 ]]; then
		res="/{"
		for aare in "${attachments[@]}"; do
			res+="${aare#/},"
		done
		entrypoint="${res%,}}"
	fi
	echo "$entrypoint"
}

# Remove variables in profile attachment to bypass userspace tools restriction
_userspace() {
	local start="$1" end="$2"; shift 2
	files=("$@")
	ii="$start"
	while [[ $ii -le $end && $ii -lt $len ]]; do
		path="${files[$ii]}"
		((ii = ii + 1))
		[[ -f "$path" ]] || continue
		entrypoint="$(_resolve_attachments "$path")"
		[[ -z "$entrypoint" ]] && continue
		name="$(basename "$path")"
		sed -e "s;profile $name @{exec_path};profile $name ${entrypoint[*]};g" \
			-i "$path"
	done
}

# Set complain flag on all profiles
_complain() {
	local start="$1" end="$2"; shift 2
	files=("$@")
	ii="$start"
	while [[ $ii -le $end && $ii -lt $len ]]; do
		path="${files[$ii]}"
		((ii = ii + 1))
		[[ -f "$path" ]] || continue
		mapfile -t flags < <(grep -o -m 1 'flags=(.*)' "$path" | cut -d '(' -f2 | cut -d ')' -f1)
		[[ "${flags[*]}" =~ complain ]] && continue
		flags+=(complain)
		sed -e "s/flags=(.*)//" \
			-e "s/ {$/ flags=(${flags[*]}) {/" \
			-i "$path"
	done
}

# Set AppArmor for full system policy
# See https://gitlab.com/apparmor/apparmor/-/wikis/FullSystemPolicy
full() {
	_msg "Configure AppArmor for full system policy"
	cp -a apparmor.d/groups/_full/init "$ROOT/apparmor.d/"
	cp -a apparmor.d/groups/_full/systemd "$ROOT/apparmor.d/"
	case "$DISTRIBUTION" in
	arch | endeavouros | cachyos | manjarolinux)
		cp -r root/usr/lib/initcpio root/usr/lib/systemd/ "$ROOT/root/usr/lib/"
		;;

	debian | ubuntu | whonix | core)
		cp -r root/usr/share/initramfs-tools "$ROOT/root/usr/share/"
		;;

	*) _die "$DISTRIBUTION is not a supported distribution." ;;
	esac
}

# Print help message
cmd_help() {
	cat <<-_EOF
		./configure [options] - Configure the apparmor.d package

		Options:
		    -f, --full      Set AppArmor for full system policy
		    -c, --complain  Set complain flag on all profiles
		    -h, --help      Print this help message and exit
	_EOF
}

main() {
	local opts err
	FULL=0
	COMPLAIN=0
	small_arg="cfh"
	long_arg="complain,full,help"
	opts="$(getopt -o $small_arg -l $long_arg -n "configure" -- "$@")"
	err=$?
	eval set -- "$opts"
	while true; do case $1 in
		-f|--full) FULL=1; shift ;; 
		-c|--complain) COMPLAIN=1; shift ;;
		-h|--help) shift; cmd_help; exit 0 ;;
		--) shift; break ;;
	esac done
	[[ $err -ne 0 ]] && { cmd_help; exit 1; }

	_title "Set the configuration for $DISTRIBUTION."
	initialize || _die "initializing build directory"
	ignore || _die "removing ignored profiles"
	synchronise || _die "merging profiles"
	configure || _die "configuring distribution"
	process _userspace 'Bypass userspace tools restriction' || _die "bypassing userspace"
	flags || _die "settings flags"
	[[ "$COMPLAIN" == 1 ]] && process _complain 'Set complain flag on all profiles'
	[[ "$FULL" == 1 ]] && full
	return 0
}

main "$@"