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:
parent
55e4b27c2b
commit
71a473712c
1 changed files with 259 additions and 119 deletions
338
tests/check.sh
338
tests/check.sh
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
# 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
|
||||
|
||||
# Usage: make check
|
||||
|
|
@ -8,55 +8,184 @@
|
|||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly APPARMORD="apparmor.d"
|
||||
readonly HEADERS=(
|
||||
"# apparmor.d - Full set of apparmor profiles"
|
||||
"# Copyright (C) "
|
||||
"# SPDX-License-Identifier: GPL-2.0-only"
|
||||
)
|
||||
|
||||
_die() {
|
||||
echo -e "\033[1;31m ✗ Error: \033[0m$*"
|
||||
exit 1
|
||||
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"
|
||||
}
|
||||
|
||||
_ensure_header() {
|
||||
local file="$1"
|
||||
for header in "${HEADERS[@]}"; do
|
||||
if ! grep -q "^$header" "$file"; then
|
||||
_die "$file does not contain '$header'"
|
||||
_in_array() {
|
||||
local item needle="$1"
|
||||
shift
|
||||
for item in "$@"; do
|
||||
if [[ "${item}" == "${needle}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
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 in_profile=false
|
||||
local first_line_after_profile=true
|
||||
local line_number=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
line_number=$((line_number + 1))
|
||||
|
||||
if [[ "$line" =~ $'\t' ]]; then
|
||||
_die "$file:$line_number: tabs are not allowed."
|
||||
# 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 <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
|
||||
in_profile=true
|
||||
first_line_after_profile=true
|
||||
_CHECK_IN_PROFILE=true
|
||||
_CHECK_FIRST_LINE_AFTER_PROFILE=true
|
||||
|
||||
elif [[ "$line" =~ [[:space:]]+$ ]]; then
|
||||
_die "$file:$line_number: line has trailing whitespace."
|
||||
|
||||
elif $in_profile; then
|
||||
if $first_line_after_profile; then
|
||||
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
|
||||
_die "$file: profile must have a two-space indentation."
|
||||
_err style "$file:$line_number" "profile must have a two-space indentation"
|
||||
fi
|
||||
first_line_after_profile=false
|
||||
_CHECK_FIRST_LINE_AFTER_PROFILE=false
|
||||
|
||||
else
|
||||
local leading_spaces="${line%%[! ]*}"
|
||||
|
|
@ -76,33 +205,53 @@ _ensure_indentation() {
|
|||
done
|
||||
|
||||
if ! $ok; then
|
||||
_die "$file:$line_number: invalid indentation."
|
||||
_err style "$file:$line_number" "invalid indentation"
|
||||
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() {
|
||||
local file="$1"
|
||||
if ! grep -q "^ *abi <abi/4.0>," "$file"; then
|
||||
_die "$file does not contain 'abi <abi/4.0>,'"
|
||||
_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 <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
|
||||
}
|
||||
_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() {
|
||||
local file="$1"
|
||||
if ! grep -q "^# vim:syntax=apparmor" "$file"; then
|
||||
_die "$file does not contain '# vim:syntax=apparmor'"
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -117,69 +266,60 @@ check_sbin() {
|
|||
}
|
||||
|
||||
check_profiles() {
|
||||
echo -e "\033[1m ⋅ \033[0mChecking if all profiles contain:"
|
||||
echo " - apparmor.d header & license"
|
||||
echo " - Check indentation: 2 spaces"
|
||||
echo " - Check for trailing whitespaces"
|
||||
echo " - 'abi <abi/4.0>,'"
|
||||
echo " - 'profile <profile_name>'"
|
||||
echo " - 'include if exists <local/*>'"
|
||||
echo " - include if exists local for subprofiles"
|
||||
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
|
||||
_msg "Checking profiles"
|
||||
mapfile -t files < <(
|
||||
find "$APPARMORD" \( -path "$APPARMORD/abstractions" -o -path "$APPARMORD/local" -o -path "$APPARMORD/tunables" -o -path "$APPARMORD/mappings" \) \
|
||||
-prune -o -type f -print
|
||||
)
|
||||
jobs=0
|
||||
WITH_CHECK=(abi include profile header tabs trailing indentation subprofiles vim)
|
||||
for file in "${files[@]}"; do
|
||||
(
|
||||
name="$(basename "$file")"
|
||||
name="${name/.apparmor.d/}"
|
||||
include="include if exists <local/$name>"
|
||||
_ensure_header "$file"
|
||||
_ensure_indentation "$file"
|
||||
_ensure_include "$file" "$include"
|
||||
_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
|
||||
_check "$file"
|
||||
) &
|
||||
_wait jobs
|
||||
done
|
||||
wait
|
||||
}
|
||||
|
||||
check_abstractions() {
|
||||
echo -e "\033[1m ⋅ \033[0mChecking if all abstractions contain:"
|
||||
echo " - apparmor.d header & license"
|
||||
echo " - Check indentation: 2 spaces"
|
||||
echo " - Check for trailing whitespaces"
|
||||
echo " - 'abi <abi/4.0>,'"
|
||||
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
|
||||
_msg "Checking abstractions"
|
||||
mapfile -t files < <(find "$APPARMORD/abstractions" -type f -not -path "$APPARMORD/abstractions/*.d/*")
|
||||
jobs=0
|
||||
WITH_CHECK=(abi include header tabs trailing indentation vim)
|
||||
for file in "${files[@]}"; do
|
||||
(
|
||||
name="$(basename "$file")"
|
||||
root="${dir/${APPARMORD}\/abstractions\//}"
|
||||
include="include if exists <abstractions/${root}${name}.d>"
|
||||
_ensure_header "$file"
|
||||
_ensure_indentation "$file"
|
||||
_ensure_include "$file" "$include"
|
||||
_ensure_abi "$file"
|
||||
_ensure_vim "$file"
|
||||
absdir="${file/${APPARMORD}\//}"
|
||||
include="include if exists <${absdir}.d>"
|
||||
_check "$file"
|
||||
) &
|
||||
_wait jobs
|
||||
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
|
||||
wait
|
||||
}
|
||||
|
||||
check_sbin
|
||||
check_profiles
|
||||
check_abstractions
|
||||
|
||||
FAIL=$(cat "$RES")
|
||||
if [[ "$FAIL" == "true" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue