tests: rewrite and expand the profile check to more files.

Rewrite: Speed up the checking by not using grep anymore and only using bash, also make it parallel

Revisit the way result are shown.

Expand: Also scan for mapping files and abstaction completion. Adapt the scan accordingly.
This commit is contained in:
Alexandre Pujol 2025-06-01 23:58:02 +02:00
parent 55e4b27c2b
commit 71a473712c
No known key found for this signature in database
GPG key ID: C5469996F0DF68EC

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# apparmor.d - Full set of apparmor profiles # apparmor.d - Full set of apparmor profiles
# Copyright (C) 2024 Alexandre Pujol <alexandre@pujol.io> # Copyright (C) 2024-2025 Alexandre Pujol <alexandre@pujol.io>
# SPDX-License-Identifier: GPL-2.0-only # SPDX-License-Identifier: GPL-2.0-only
# Usage: make check # Usage: make check
@ -8,55 +8,184 @@
set -eu -o pipefail set -eu -o pipefail
readonly APPARMORD="apparmor.d" RES=$(mktemp)
readonly HEADERS=( echo "false" >"$RES"
"# apparmor.d - Full set of apparmor profiles" MAX_JOBS=$(nproc)
"# Copyright (C) " declare WITH_CHECK
"# SPDX-License-Identifier: GPL-2.0-only" 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"; }
_die() { _warn() {
echo -e "\033[1;31m ✗ Error: \033[0m$*" local type="$1" file="$2"
exit 1 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"
} }
_ensure_header() { _in_array() {
local file="$1" local item needle="$1"
for header in "${HEADERS[@]}"; do shift
if ! grep -q "^$header" "$file"; then for item in "$@"; do
_die "$file does not contain '$header'" if [[ "${item}" == "${needle}" ]]; then
return 0
fi fi
done done
return 1
} }
_ensure_indentation() { _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 file="$1"
local in_profile=false
local first_line_after_profile=true
local line_number=0 local line_number=0
while IFS= read -r line; do while IFS= read -r line; do
line_number=$((line_number + 1)) line_number=$((line_number + 1))
if [[ "$line" =~ $'\t' ]]; then # Guidelines check
_die "$file:$line_number: tabs are not allowed." _check_abi
_check_include
_check_profile
_check_subprofiles
# Style check
if [[ $line_number -lt 10 ]]; then
_check_header
fi 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 <abi/4.0>,'
_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 <abi/4.0>,'"
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 if [[ "$line" =~ ^profile ]]; then
in_profile=true _CHECK_IN_PROFILE=true
first_line_after_profile=true _CHECK_FIRST_LINE_AFTER_PROFILE=true
elif [[ "$line" =~ [[:space:]]+$ ]]; then elif $_CHECK_IN_PROFILE; then
_die "$file:$line_number: line has trailing whitespace." if $_CHECK_FIRST_LINE_AFTER_PROFILE; then
elif $in_profile; then
if $first_line_after_profile; then
local leading_spaces="${line%%[! ]*}" local leading_spaces="${line%%[! ]*}"
local num_spaces=${#leading_spaces} local num_spaces=${#leading_spaces}
if ((num_spaces != 2)); then if ((num_spaces != 2)); then
_die "$file: profile must have a two-space indentation." _err style "$file:$line_number" "profile must have a two-space indentation"
fi fi
first_line_after_profile=false _CHECK_FIRST_LINE_AFTER_PROFILE=false
else else
local leading_spaces="${line%%[! ]*}" local leading_spaces="${line%%[! ]*}"
@ -76,33 +205,53 @@ _ensure_indentation() {
done done
if ! $ok; then if ! $ok; then
_die "$file:$line_number: invalid indentation." _err style "$file:$line_number" "invalid indentation"
fi fi
fi fi
fi fi
fi fi
done <"$file"
}
_ensure_include() {
local file="$1"
local include="$2"
if ! grep -q "^ *${include}$" "$file"; then
_die "$file does not contain '$include'"
fi
} }
_ensure_abi() { _CHEK_IN_SUBPROFILE=false
local file="$1" declare -A _RES_SUBPROFILES
if ! grep -q "^ *abi <abi/4.0>," "$file"; then _check_subprofiles() {
_die "$file does not contain 'abi <abi/4.0>,'" _is_enabled subprofiles || return 0
if [[ "$line" =~ ^(' ')+'profile '(.*)' {' ]]; then
indentation="${BASH_REMATCH[1]}"
subprofile="${BASH_REMATCH[2]}"
subprofile="${subprofile%% *}"
include="${indentation}include if exists <local/${name}_${subprofile}>"
_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 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
}
_ensure_vim() { readonly VIM_SYNTAX="# vim:syntax=apparmor"
local file="$1" RES_VIM=false
if ! grep -q "^# vim:syntax=apparmor" "$file"; then _check_vim() {
_die "$file does not contain '# vim:syntax=apparmor'" _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 fi
} }
@ -117,69 +266,60 @@ check_sbin() {
} }
check_profiles() { check_profiles() {
echo -e "\033[1m ⋅ \033[0mChecking if all profiles contain:" _msg "Checking profiles"
echo " - apparmor.d header & license" mapfile -t files < <(
echo " - Check indentation: 2 spaces" find "$APPARMORD" \( -path "$APPARMORD/abstractions" -o -path "$APPARMORD/local" -o -path "$APPARMORD/tunables" -o -path "$APPARMORD/mappings" \) \
echo " - Check for trailing whitespaces" -prune -o -type f -print
echo " - 'abi <abi/4.0>,'" )
echo " - 'profile <profile_name>'" jobs=0
echo " - 'include if exists <local/*>'" WITH_CHECK=(abi include profile header tabs trailing indentation subprofiles vim)
echo " - include if exists local for subprofiles" for file in "${files[@]}"; do
echo " - vim:syntax=apparmor" (
directories=("$APPARMORD/groups/*" "$APPARMORD/profiles-*-*")
# shellcheck disable=SC2068
for dir in ${directories[@]}; do
for file in $(find "$dir" -maxdepth 1 -type f); do
case "$file" in */README.md) continue ;; esac
name="$(basename "$file")" name="$(basename "$file")"
name="${name/.apparmor.d/}" name="${name/.apparmor.d/}"
include="include if exists <local/$name>" include="include if exists <local/$name>"
_ensure_header "$file" _check "$file"
_ensure_indentation "$file" ) &
_ensure_include "$file" "$include" _wait jobs
_ensure_abi "$file"
_ensure_vim "$file"
if ! grep -q "^profile $name" "$file"; then
_die "$name does not contain 'profile $name'"
fi
mapfile -t subrofiles < <(grep "^ *profile*" "$file" | awk '{print $2}')
for subprofile in "${subrofiles[@]}"; do
include="include if exists <local/${name}_${subprofile}>"
if ! grep -q "^ *${include}$" "$file"; then
_die "$name: $name//$subprofile does not contain '$include'"
fi
done
done
done done
wait
} }
check_abstractions() { check_abstractions() {
echo -e "\033[1m ⋅ \033[0mChecking if all abstractions contain:" _msg "Checking abstractions"
echo " - apparmor.d header & license" mapfile -t files < <(find "$APPARMORD/abstractions" -type f -not -path "$APPARMORD/abstractions/*.d/*")
echo " - Check indentation: 2 spaces" jobs=0
echo " - Check for trailing whitespaces" WITH_CHECK=(abi include header tabs trailing indentation vim)
echo " - 'abi <abi/4.0>,'" for file in "${files[@]}"; do
echo " - 'include if exists <abstractions/*.d>'" (
echo " - vim:syntax=apparmor"
directories=(
"$APPARMORD/abstractions/" "$APPARMORD/abstractions/app/"
"$APPARMORD/abstractions/attached/"
"$APPARMORD/abstractions/bus/" "$APPARMORD/abstractions/common/"
)
for dir in "${directories[@]}"; do
for file in $(find "$dir" -maxdepth 1 -type f); do
name="$(basename "$file")" name="$(basename "$file")"
root="${dir/${APPARMORD}\/abstractions\//}" absdir="${file/${APPARMORD}\//}"
include="include if exists <abstractions/${root}${name}.d>" include="include if exists <${absdir}.d>"
_ensure_header "$file" _check "$file"
_ensure_indentation "$file" ) &
_ensure_include "$file" "$include" _wait jobs
_ensure_abi "$file"
_ensure_vim "$file"
done done
wait
mapfile -t files < <(
find "$APPARMORD/abstractions" -type f -path "$APPARMORD/abstractions/*.d/*"
find "$APPARMORD/mappings" -type f
)
# shellcheck disable=SC2034
jobs=0
WITH_CHECK=(header tabs trailing indentation vim)
for file in "${files[@]}"; do
_check "$file" &
_wait jobs
done done
wait
} }
check_sbin check_sbin
check_profiles check_profiles
check_abstractions check_abstractions
FAIL=$(cat "$RES")
if [[ "$FAIL" == "true" ]]; then
exit 1
fi