diff --git a/tests/check.sh b/tests/check.sh index 02ae71812..25c82e3d1 100644 --- a/tests/check.sh +++ b/tests/check.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # apparmor.d - Full set of apparmor profiles -# Copyright (C) 2024 Alexandre Pujol +# Copyright (C) 2024-2025 Alexandre Pujol # SPDX-License-Identifier: GPL-2.0-only # Usage: make check @@ -8,101 +8,250 @@ 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 - if [[ "$line" =~ ^profile ]]; then - in_profile=true - first_line_after_profile=true + done <"$file" - elif [[ "$line" =~ [[:space:]]+$ ]]; then - _die "$file:$line_number: line has trailing whitespace." + # Results + _res_abi + _res_include + _res_profile + _res_subprofiles + _res_header + _res_vim +} - elif $in_profile; then - if $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." - fi - first_line_after_profile=false +# Guidelines check: https://apparmor.pujol.io/development/guidelines/ - else - local leading_spaces="${line%%[! ]*}" - local num_spaces=${#leading_spaces} +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 +} - 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 +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 +} - if ! $ok; then - _die "$file:$line_number: invalid indentation." +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 - 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 ," "$file"; then - _die "$file does not contain 'abi ,'" +_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 +} -_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 ,'" - echo " - 'profile '" - echo " - 'include if exists '" - 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 " - _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 " - 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 ,'" - echo " - 'include if exists '" - 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 " - _ensure_header "$file" - _ensure_indentation "$file" - _ensure_include "$file" "$include" - _ensure_abi "$file" - _ensure_vim "$file" - done + 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