#!/bin/bash
# run-shellcheck
# stop on any error
set -e
# stop on undefined variable
set -u

mytmpdir=$(mktemp -d -t debian-cis-test.XXXXXX)
totalerrors=255

cleanup_and_exit() {
    rm -rf "$mytmpdir"
    if [ "$totalerrors" -eq 255 ]; then
        fatal "RUNTIME ERROR"
    fi
    exit "$totalerrors"
}
trap "cleanup_and_exit" EXIT HUP INT

outdir="$mytmpdir/out"
mkdir -p "$outdir" || exit 1

tests_list=""
testno=0
testcount=0

skip_tests=0
dismiss_count=0
nbfailedret=0
nbfailedgrep=0
nbfailedconsist=0
listfailedret=""
listfailedgrep=""
listfailedconsist=""

usecase=""
usecase_name=""
usecase_name_root=""
usecase_name_sudo=""
declare -a REGISTERED_TESTS

#####################
# Utility functions #
#####################
# in case a fatal event occurs, fatal logs and exits with return code 1
fatal() {
    printf "\033[1;91m###### \033[0m\n" >&2
    printf "%b %b\n" "\033[1;91mFATAL\033[0m" "$*" >&2
    printf "%b \n" "\033[1;91mEXIT TEST SUITE WITH FAILURE\033[0m" >&2
    trap - EXIT
    exit 255
}
# prints that a test failed
fail() {
    printf "%b %b\n" "\033[1;30m\033[41m[FAIL]\033[0m" "$*" >&2
}
# prints that a test succeded
ok() {
    printf "%b %b\n" "\033[30m\033[42m[ OK ]\033[0m" "$*" >&2
}

skip() {
    printf "%b %b\n" "\033[30m\e[43m[SKIP]\033[0m" "$*" >&2
}
# retrieves audit script logfile
get_stdout() {
    cat "$outdir"/"$usecase_name".log
}

# Reset the list of test assertions
clear_registered_tests() {
    unset REGISTERED_TESTS
    declare -a REGISTERED_TESTS
    # shellcheck disable=2034
    dismiss_count=0
    skip_tests=0
}

# Generates a formated test name
make_usecase_name() {
    usecase=$1
    shift
    role=$1
    usecase_name=$(printf '%03d-%s-%s-%s' "$testno" "$name" "$usecase" "$role" | sed -re "s=/=_=g")
    echo -n "$usecase_name"
}

# Plays the registered test suite
play_registered_tests() {
    if [ "$skip_tests" -eq 1 ]; then
        return
    fi
    usecase_name=$1
    if [[ "${REGISTERED_TESTS[*]}" ]]; then
        export numtest=${#REGISTERED_TESTS[@]}
        for t in "${!REGISTERED_TESTS[@]}"; do
            ${REGISTERED_TESTS[$t]}
        done
    fi
}

# Plays comparison tests to ensure that root and sudo exection have the same output
play_consistency_tests() {
    consist_test=0
    printf "\033[34m*** [%03d] %s::%s Root/Sudo Consistency Tests\033[0m\n" "$testno" "$test_file" "$usecase"
    retfile_root=$outdir/${usecase_name_root}.retval
    retfile_sudo=$outdir/${usecase_name_sudo}.retval
    cmp "$retfile_root" "$retfile_sudo" && ret=0 || ret=1
    if [[ ! 0 -eq $ret ]]; then
        fail "$name" return values differ
        diff "$retfile_root" "$retfile_sudo" || true
        consist_test=1
    else
        ok "$name return values are equal"

    fi
    retfile_root=$outdir/${usecase_name_root}.log
    retfile_sudo=$outdir/${usecase_name_sudo}.log
    cmp "$retfile_root" "$retfile_sudo" && ret=0 || ret=1
    if [[ ! 0 -eq $ret ]]; then
        fail "$name" logs differ
        diff "$retfile_root" "$retfile_sudo" || true
        consist_test=1
    else
        ok "$name logs are identical"
    fi

    if [ 1 -eq "$consist_test" ]; then
        nbfailedconsist=$((nbfailedconsist + 1))
        listfailedconsist="$listfailedconsist $(make_usecase_name "$usecase" consist)"
    fi
}

# Actually runs one single audit script
_run() {
    usecase_name=$1
    shift
    printf "\033[34m*** [%03d] %s \033[0m(%s)\n" "$testno" "$usecase_name" "$*"
    bash -c "$*" >"$outdir/$usecase_name.log" 2>"$outdir/${usecase_name}_err.log" && true
    echo $? >"$outdir/$usecase_name.retval"
    ret=$(<"$outdir"/"$usecase_name".retval)
    get_stdout
    if [ -s "$outdir/${usecase_name}_err.log" ]; then
        echo ">>> stderr follows"
        cat "$outdir/${usecase_name}_err.log"
        echo "<<< end of stderr"
    fi
}

# Load assertion functions for functionnal tests
if [ ! -f "$(dirname "$0")"/lib.sh ]; then
    fatal "Cannot locate lib.sh"
fi
# shellcheck source=../tests/lib.sh
. "$(dirname "$0")"/lib.sh

###################
# Execution start #
###################
printf "\033[1;36m###\n### %s\n### \033[0m\n" "Starting debian-cis functional testing"

. /etc/default/cis-hardening

# if no scripts were passed as arguments, list all available test scenarii to be played
if [ $# -eq 0 ]; then
    tests_list=$(ls -v "$(dirname "$0")"/hardening/)
    testcount=$(wc -l <<<"$tests_list")
else
    tests_list="$*"
    testcount=$#
fi

for test_file in $tests_list; do
    test_file_path=$(dirname "$0")/hardening/"$test_file"
    if [ ! -f "$test_file_path" ]; then
        fatal "Test file \"$test_file\" does not exist"
    fi
    # script var is used inside test files
    # shellcheck disable=2034
    script="$(basename "$test_file" .sh)"
    # source test scenario file to add `test_audit` func
    # shellcheck disable=1090
    . "$test_file_path"
    testno=$((testno + 1))
    # shellcheck disable=2001
    name="$(echo "${test_file%%.sh}" | sed 's/\d+\.\d+_//')"
    printf "\033[1;36m### [%03d/%03d] %s \033[0m\n" "$testno" "$testcount" "$test_file"
    # test_audit is the function defined in $test_file, that carries the actual functional tests for this script
    test_audit
    # reset var names
    usecase_name=""
    usecase_name_root=""
    usecase_name_sudo=""
    unset -f test_audit
    echo ""
done

stderrunexpected=""
for file in "$outdir"/*_err.log; do
    if [ -s "$file" ]; then
        stderrunexpected="$stderrunexpected $(basename "$file")"
    fi
done

printf "\033[1;36m###\n### %s \033[0m\n" "Test report"
if [ $((nbfailedret + nbfailedgrep + nbfailedconsist)) -eq 0 ] && [ -z "$stderrunexpected" ]; then
    echo -e "\033[42m\033[30mAll tests succeeded :)\033[0m"
    echo -e "\033[42m\033[30mStderr is empty :)\033[0m"

else
    (
        echo -e "\033[41mOne or more tests failed :(\033[0m"
        echo -e "- $nbfailedret unexpected return values ${listfailedret}"
        echo -e "- $nbfailedgrep unexpected text values $listfailedgrep"
        echo -e "- $nbfailedconsist root/sudo consistency $listfailedconsist"
        echo -e "- stderr detected on $stderrunexpected"
    ) | tee "$outdir"/summary
fi
echo

set +e
set +u
totalerrors=$((nbfailedret + nbfailedgrep + nbfailedconsist))
# leave `exit 255` for runtime errors
[ $totalerrors -ge 255 ] && totalerrors=254
exit $totalerrors
