#!/usr/bin/env bash # Configure the apparmor.d package # Copyright (C) 2021-2023 Alexandre Pujol # 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; } # Displace files in the package sources # $@ List of files to displace _displace_files() { for path in "$@"; do mv "${ROOT:?}/$path" "${ROOT:?}/$path.apparmor.d" done } # 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/extend" ;; debian|ubuntu|whonix|core) 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/extend" _msg "Displace overwritten files." _displace_files apparmor.d/tunables/global \ apparmor.d/tunables/xdg-user-dirs apparmor.d/abstractions/trash ;; opensuse) LIBEXEC="/{usr/,}libexec" sed -i -e '/Archlinux/d' "$ROOT/apparmor.d/tunables/extend" ;; *) _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 "$@"