#!/usr/bin/env bash # apparmor.d - Full set of apparmor profiles # Copyright (C) 2024-2025 Alexandre Pujol # SPDX-License-Identifier: GPL-2.0-only # Usage: make check # shellcheck disable=SC2044 set -eu -o pipefail RES=$(mktemp) echo "false" >"$RES" MAX_JOBS=$(nproc) declare WITH_CHECK readonly MAX_JOBS APPARMORD="apparmor.d" readonly reset="\033[0m" fgRed="\033[0;31m" fgYellow="\033[0;33m" fgWhite="\033[0;37m" BgWhite="\033[1;37m" _msg() { printf '%b%s%b\n' "$BgWhite" "$*" "$reset"; } _warn() { local type="$1" file="$2" shift 2 printf '%bwarning%b %s(%b%s%b): %s\n' "$fgYellow" "$reset" "$type" "$fgWhite" "$file" "$reset" "$*" } _err() { local type="$1" file="$2" shift 2 printf ' %berror%b %s(%b%s%b): %s\n' "$fgRed" "$reset" "$type" "$fgWhite" "$file" "$reset" "$*" echo "true" >"$RES" } _in_array() { local item needle="$1" shift for item in "$@"; do if [[ "${item}" == "${needle}" ]]; then return 0 fi done return 1 } _is_enabled() { _in_array "$1" "${WITH_CHECK[@]}" } _wait() { local -n job=$1 job=$((job + 1)) if ((job >= MAX_JOBS)); then wait -n job=$((job - 1)) fi } _check() { local file="$1" local line_number=0 while IFS= read -r line; do line_number=$((line_number + 1)) # Guidelines check _check_abi _check_include _check_profile _check_subprofiles # Style check if [[ $line_number -lt 10 ]]; then _check_header fi _check_tabs _check_trailing _check_indentation _check_vim done <"$file" # Results _res_abi _res_include _res_profile _res_subprofiles _res_header _res_vim } # Guidelines check: https://apparmor.pujol.io/development/guidelines/ RES_ABI=false readonly ABI_SYNTAX='abi ,' _check_abi() { _is_enabled abi || return 0 if [[ "$line" =~ ^' '*"$ABI_SYNTAX" ]]; then RES_ABI=true fi } _res_abi() { _is_enabled abi || return 0 if ! $RES_ABI; then _err guideline "$file" "missing 'abi ,'" fi } RES_INCLUDE=false _check_include() { _is_enabled include || return 0 if [[ "$line" =~ ^.*"${include}"$ ]]; then RES_INCLUDE=true fi } _res_include() { _is_enabled include || return 0 if ! $RES_INCLUDE; then _err guideline "$file" "missing '$include'" fi } RES_PROFILE=false _check_profile() { _is_enabled profile || return 0 if [[ "$line" =~ ^"profile $name" ]]; then RES_PROFILE=true fi } _res_profile() { _is_enabled profile || return 0 if ! $RES_PROFILE; then _err guideline "$file" "missing profile name: 'profile $name'" fi } # Style check readonly HEADERS=( "# apparmor.d - Full set of apparmor profiles" "# Copyright (C) " "# SPDX-License-Identifier: GPL-2.0-only" ) _RES_HEADER=(false false false) _check_header() { _is_enabled header || return 0 for idx in "${!HEADERS[@]}"; do if [[ "$line" == "${HEADERS[$idx]}"* ]]; then _RES_HEADER[idx]=true break fi done } _res_header() { _is_enabled header || return 0 for idx in "${!_RES_HEADER[@]}"; do if ${_RES_HEADER[$idx]}; then continue fi _err style "$file" "missing header: '${HEADERS[$idx]}'" done } _check_tabs() { _is_enabled tabs || return 0 if [[ "$line" =~ $'\t' ]]; then _err style "$file:$line_number" "tabs are not allowed" fi } _check_trailing() { _is_enabled trailing || return 0 if [[ "$line" =~ [[:space:]]+$ ]]; then _err style "$file:$line_number" "line has trailing whitespace" fi } _CHECK_IN_PROFILE=false _CHECK_FIRST_LINE_AFTER_PROFILE=true _check_indentation() { _is_enabled indentation || return 0 if [[ "$line" =~ ^profile ]]; then _CHECK_IN_PROFILE=true _CHECK_FIRST_LINE_AFTER_PROFILE=true elif $_CHECK_IN_PROFILE; then if $_CHECK_FIRST_LINE_AFTER_PROFILE; then local leading_spaces="${line%%[! ]*}" local num_spaces=${#leading_spaces} if ((num_spaces != 2)); then _err style "$file:$line_number" "profile must have a two-space indentation" fi _CHECK_FIRST_LINE_AFTER_PROFILE=false else local leading_spaces="${line%%[! ]*}" local num_spaces=${#leading_spaces} if ((num_spaces % 2 != 0)); then ok=false for offset in 5 11; do num_spaces=$((num_spaces - offset)) if ((num_spaces < 0)); then break fi if ((num_spaces % 2 == 0)); then ok=true break fi done if ! $ok; then _err style "$file:$line_number" "invalid indentation" fi fi fi fi } _CHEK_IN_SUBPROFILE=false declare -A _RES_SUBPROFILES _check_subprofiles() { _is_enabled subprofiles || return 0 if [[ "$line" =~ ^(' ')+'profile '(.*)' {' ]]; then indentation="${BASH_REMATCH[1]}" subprofile="${BASH_REMATCH[2]}" subprofile="${subprofile%% *}" include="${indentation}include if exists " _RES_SUBPROFILES["$subprofile"]="$name//$subprofile does not contain '$include'" _CHEK_IN_SUBPROFILE=true elif $_CHEK_IN_SUBPROFILE; then if [[ "$line" == *"$include" ]]; then _RES_SUBPROFILES["$subprofile"]=true fi fi } _res_subprofiles() { _is_enabled subprofiles || return 0 for msg in "${_RES_SUBPROFILES[@]}"; do if [[ $msg == true ]]; then continue fi _err guideline "$file" "$msg" done } readonly VIM_SYNTAX="# vim:syntax=apparmor" RES_VIM=false _check_vim() { _is_enabled vim || return 0 if [[ "$line" =~ ^"$VIM_SYNTAX" ]]; then RES_VIM=true fi } _res_vim() { _is_enabled vim || return 0 if ! $RES_VIM; then _err style "$file" "missing vim syntax: '$VIM_SYNTAX'" fi } check_sbin() { echo -e "\033[1m ⋅ \033[0mEnsuring '@{sbin}' is used in all profiles:" while IFS= read -r name; do mapfile -t files < <(grep --files-with-matches --recursive -E "(^|[[:space:]])@{bin}/$name([[:space:]]|$)" apparmor.d) for file in "${files[@]}"; do _die "$file contains '@{bin}/$name' instead of '@{sbin}/$name'" done done