the-bastion/tests/functional/launch_tests_on_instance.sh
Stéphane Lesimple 345a1f951f fix: don't exit with fping host is unreachable
As ping can return unknown exit codes for unknown cases,
just never bail out to avoid taking bad decisions,
as we retry each second maximum, there's no DoS risk
2023-12-05 10:02:52 +01:00

784 lines
26 KiB
Bash
Executable file

#! /usr/bin/env bash
# vim: set filetype=sh ts=4 sw=4 sts=4 et:
# shellcheck disable=SC2086
# shellcheck disable=SC2016
# shellcheck disable=SC2046
set -eu
basedir=$(readlink -f "$(dirname "$0")"/../..)
# shellcheck source=lib/shell/functions.inc
. "$basedir"/lib/shell/functions.inc
opt_remote_etc_bastion=/etc/bastion
opt_remote_basedir=$basedir
opt_skip_consistency_check=0
opt_no_pause_on_fail=0
opt_slowness_factor=1
opt_log_prefix=
opt_module=
opt_post_run=
declare -A capabilities=( [ed25519]=1 [mfa]=1 [mfa-password]=0 [pamtester]=1 [piv]=1 )
# set the helptext now to get the proper default values
help_text=$(cat <<EOF
Test Options:
--skip-consistency-check Speed up tests by skipping the consistency check between every test
--no-pause-on-fail Don't pause when a test fails
--log-prefix=X Prefix all logs by this name
--module=X Only test this module (specify a filename found in \`functional/tests.d/\`), can be specified multiple times
--slowness-factor=X If your test environment is slow, set this to 2, 3 or more to use higher timeouts (default: 1)
--post-run=X Commands to run after we're done testing
Remote OS directory locations:
--remote-etc-bastion=X Override the default remote bastion configuration directory (default: $opt_remote_etc_bastion)
--remote-basedir=X Override the default remote basedir location (default: $opt_remote_basedir)
Specifying features support of the underlying OS of the tested bastion:
--has-ed25519=[0|1] Ed25519 keys are supported (default: ${capabilities[ed25519]})
--has-mfa=[0|1] PAM is usable to check passwords and TOTP (default: ${capabilities[mfa]})
--has-mfa-password=[0|1] PAM is usable to check passwords (default: ${capabilities[mfa-password]})
--has-pamtester=[0|1] The \`pamtester\` binary is available, and PAM is usable (default: ${capabilities[pamtester]})
--has-piv=[0|1] The \`yubico-piv-tool\` binary is available (default: ${capabilities[piv]})
EOF
)
usage() {
if [ "${1:-}" != "light" ]; then
cat <<EOF
Usage: $0 [OPTIONS] <IP> <SSH_Port> <HTTP_Proxy_Port_or_Zero> <Remote_Admin_User_Name> <Admin_User_SSH_Key_Path> <Root_SSH_Key_Path>
EOF
fi
echo "$help_text"
}
while [ -n "${1:-}" ]
do
optval="${1/*=/}"
case "$1" in
--remote-etc-bastion=*)
opt_remote_etc_bastion="$optval"
;;
--remote-basedir=*)
opt_remote_basedir="$optval"
;;
--skip-consistency-check)
opt_skip_consistency_check=1
;;
--no-pause-on-fail)
opt_no_pause_on_fail=1
;;
--slowness-factor=*)
if [[ $optval =~ ^[1-9]$ ]]; then
opt_slowness_factor=$optval
fi
;;
--log-prefix=*)
opt_log_prefix="$optval"
;;
--post-run=*)
opt_post_run="$optval"
;;
--module=*)
if [ ! -e "$basedir/tests/functional/tests.d/$optval" ]; then
echo "Unknown module specified '$optval', supported modules are:"
cd "$basedir/tests/functional/tests.d"
ls -- ???-*.sh
exit 1
fi
opt_module="$opt_module $optval"
;;
--has-*=*)
optname=${1/--has-/}
optname=${optname/=*/}
capabilities[$optname]=$optval
;;
--help)
usage
exit 0
;;
--help-light)
usage light
exit 0
;;
-*)
echo "Unknown option: $1"
usage
exit 1
;;
*) break
;;
esac
shift
done
if [ -n "${7:-}" ]; then
echo "Error: too many parameters"
usage
exit 1
fi
if [ -z "${6:-}" ]; then
echo "Error: missing parameters"
usage
exit 1
fi
remote_ip="$1"
remote_port="$2"
# the var below is used in sourced test files
# shellcheck disable=SC2034
remote_proxy_port="$3"
account0="$4"
user_ssh_key_path="$5"
root_ssh_key_path="$6"
# does ssh work there ?
server_output=$(echo test | nc -w 1 $remote_ip $remote_port)
if echo "$server_output" | grep -q ^SSH-2 ; then
echo SSH to $remote_ip:$remote_port OK
else
echo "Port $remote_port doesn't seem open on $remote_ip, or is not SSH! ($server_output)"
exit 1
fi
# those vars are also used in all our modules
# shellcheck disable=SC2034
{
account1="te3456789012345678stu_Ser.1-"
account2="te23456789012345678sT-user2_"
account3="te23456789012345678St-user3."
account4="Te0123456789012345678StUsEr4"
uid1=9001
uid2=9002
uid3=9003
uid4=9004
group1="te.st_Group1-"
group2="tEst-gr.oup2_"
group3="testgrOup3"
shellaccount="test-shell_"
randomstr=randomstr_pUuGXu3tfhi5WII4_randomstr
mytmpdir=$(mktemp -d -t bastiontest.XXXXXX)
tmp_a=$(mktemp -t bastiontest.XXXXXX)
tmp_b=$(mktemp -t bastiontest.XXXXXX)
trap 'echo CLEANING UP ; rm -rf "$mytmpdir" ; rm -f "$tmp_a" "$tmp_b" ; exit 255' EXIT
account0key1file="$mytmpdir/account0key1file"
account1key1file="$mytmpdir/account1key1file"
account1key2file="$mytmpdir/account1key2file"
account2key1file="$mytmpdir/account2key1file"
account3key1file="$mytmpdir/account3key1file"
account4key1file="$mytmpdir/account4key1file"
rootkeyfile="$mytmpdir/rootkeyfile"
for f in $account1key1file $account1key2file $account2key1file $account3key1file $account4key1file
do
ssh-keygen -N '' -t ecdsa -f $f -q
done
cp $user_ssh_key_path $account0key1file
ssh-keygen -y -f $user_ssh_key_path > $account0key1file.pub
cp $root_ssh_key_path $rootkeyfile
ssh-keygen -y -f $root_ssh_key_path > $rootkeyfile.pub
chmod 400 $account0key1file
jq="jq --raw-output --compact-output --sort-keys"
js="--json-greppable"
default_timeout=$((30 * opt_slowness_factor))
t="timeout --foreground $default_timeout"
tf="timeout --foreground $((default_timeout / 2))"
a0=" $t ssh -F $mytmpdir/ssh_config -i $account0key1file $account0@$remote_ip -p $remote_port -- $js "
a0f="$tf ssh -F $mytmpdir/ssh_config -i $account0key1file $account0@$remote_ip -p $remote_port -- $js "
a1=" $t ssh -F $mytmpdir/ssh_config -i $account1key1file $account1@$remote_ip -p $remote_port -- $js "
a1k2="$t ssh -F $mytmpdir/ssh_config -i $account1key2file $account1@$remote_ip -p $remote_port -- $js "
a2=" $t ssh -F $mytmpdir/ssh_config -i $account2key1file $account2@$remote_ip -p $remote_port -- $js "
a3=" $t ssh -F $mytmpdir/ssh_config -i $account3key1file $account3@$remote_ip -p $remote_port -- $js "
a4=" $t ssh -F $mytmpdir/ssh_config -i $account4key1file $account4@$remote_ip -p $remote_port -- $js "
a4f="$tf ssh -F $mytmpdir/ssh_config -i $account4key1file $account4@$remote_ip -p $remote_port -- $js "
a4np="$t ssh -F $mytmpdir/ssh_config -o PubkeyAuthentication=no $account4@$remote_ip -p $remote_port -- $js "
r0=" $t ssh -F $mytmpdir/ssh_config -i $rootkeyfile root@$remote_ip -p $remote_port -- "
# gpg has a terrible tendency to block on the pseudo-random number generator because it
# reads from /dev/random instead of /dev/urandom for bad reasons. so, just hardcode a pubkey here
admins_gpg_key_pub='
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=hEcM
-----END PGP PUBLIC KEY BLOCK-----
'
# 25305EA2FCA333C4
admins_gpg_key_pub_2='
-----BEGIN PGP PUBLIC KEY BLOCK-----
mI0EZTjYygEEAMbJBg+8/bKtsWif5I/EaoNYhY4dPJ2wc4rg/6JJFTvXQP5hCP5S
9vUyw/PW1Lho8fYNbTOFdgI0lbi0HObTuy1oMPRmBdMFppUbA06RcYImCB+ueZgN
F4TYXtleF26xasOSuf+k7lH8FrSfdnDxE/3+xddWUReTCs+Z5o/odTItABEBAAG0
JWJhc3Rpb24gdGVzdHMgNiA8YmFzdGlvbkBleGFtcGxlLm9yZz6I1AQTAQoAPhYh
BCRiNpSK15lfa7/YoiUwXqL8ozPEBQJlONjKAhsDBQkDwmcABQsJCAcCBhUKCQgL
AgQWAgMBAh4BAheAAAoJECUwXqL8ozPE7QEEAIcgxxBkn66ibzGfHFTwBg5mOEsh
CVOKkLms+5T22EgwgD5IVusYkHuwzPLpzvIHbm49Q2zZpoWzz/D+A8WhlB1hf1hD
MEs/zwyji35LzxENL3sGm+PaADzQpj/2BFNr+KkLvDtP+ly1DqoDsWB5VlKRTcej
fKo/0fnlgVgUH9QWuI0EZTjamwEEAM6tWi1JeLKKn3dXy4W/tgWcG8qkLnk1IBsT
ADRPMhmRpevfDEf93L9E/Nb4hNHOXtI4H93ZI1V3xsqLtZn7Vp5xtf8hRUgySyeJ
BUvcZCSn8t9h7PJi1n88jkyIsuRYrr9AZ1A764PBMHX72zJynRO3kXA9e3qK18y2
wyo4G/F7ABEBAAGItgQYAQoAIBYhBCRiNpSK15lfa7/YoiUwXqL8ozPEBQJlONqb
AhsMAAoJECUwXqL8ozPEKDYD/R5VGtppw6yJ9D92qCGnzNEIlfoasRynQVxr+ogl
rMaesAB0HiKTBmU4WOT4u+7/W5p/bkS/GbJAa34DIi8pYZVj1b9VVfq9ICQFG/+K
/0PeCKsbPCVFNI9giWKWukJ5v0qtzIxIQcAtLJAntX86KAZCTU6Nqnv1gOx1dLXO
tM6t
=Anoc
-----END PGP PUBLIC KEY BLOCK-----
'
# CF27BEC1C8266FFE EC6CEA6719EF3700
admins_gpg_key_pub_double='
-----BEGIN PGP PUBLIC KEY BLOCK-----
mI0EZTjY4gEEALsLQRaWUyfXtD9gtAXmo9Uq1DV9ZInd9xkxvEbLx8PJxsAnD5dV
yK/LfJnY+imd2Wf8C0KJLcTiQX8wjSNc2cuDJB/V8A8Ps/ZijBNSUjrVBvihToUd
GSPTDUr1tR9bH4Cz4olsbsQdThhpGQpDEAGdey2Xf3iMbekuQ7dKX+WLABEBAAG0
JWJhc3Rpb24gdGVzdHMgNyA8YmFzdGlvbkBleGFtcGxlLm9yZz6I1AQTAQoAPhYh
BOlCsHVGNoszKG4QgM8nvsHIJm/+BQJlONjiAhsDBQkDwmcABQsJCAcCBhUKCQgL
AgQWAgMBAh4BAheAAAoJEM8nvsHIJm/++6ID/igoQyP5+wp5UcFL/El8rU0yqGEg
0ZHLtxm+kKXzBgm5CBj2ZYcu1MUSKHJf4EI/0Hgdb62Er9eDHVXYMkrx5qWPZO/K
uGgY5iQECjae1wQXX3EWADttWE15WbzADNCguEUeTcc/eHJ2yR1EYmENSTHRJmSD
U6prPW5pHz6GTKwpuI0EZTjaugEEAM8qqbN3AKkd4FCvzW4RXXdLbXkPcxwX7TT+
XzMmdDagXjB/+GLyY3tCNgyGogLGkP+VvVX3LD5qdKIjqpR1w5NPUfUzCEv6SsIa
Rg9BZrOlwJlagdGkSfY/11TAh8UUX1pq59nHri4jnrYWQyy8CmgCYlHoRV/n60fd
4ql/O4rBABEBAAGItgQYAQoAIBYhBOlCsHVGNoszKG4QgM8nvsHIJm/+BQJlONq6
AhsMAAoJEM8nvsHIJm/+W5MD/Av/zkeVjiP1+XwzqVPB1CCSjormF6t17wHRSpwM
ZFQ2/a4Jxj4W0jl+KcwFB0zGits2sIqACd8CRi8bJiPJXLzqH/JIp5S3CknQoBK+
0XRSi/TZyYD7dI8RpXGdNbf9bD4BnVa6oKAjRxi0ZlE693IZLFHnVeNgqkbCt1Y3
rkZgmI0EZTjY5gEEAKyaPsb3+0YE4gOX1aKIZKwk1F2gYWBrKnVjeev1oEyeQ9Sn
hufIkHC6sRhDgI2lfUKkLdcKt75IMZdpdFHvOvXaMkcyY7+OukZm01vIcLRXi0m6
4L3dFChlvaz5AawS/hoonXMviwkt19kfYbE6t+ILD4ukD3U1OTwe6yjVKhUtABEB
AAG0JWJhc3Rpb24gdGVzdHMgOCA8YmFzdGlvbkBleGFtcGxlLm9yZz6I1AQTAQoA
PhYhBAB98E297Qm/EV76+uxs6mcZ7zcABQJlONjmAhsDBQkDwmcABQsJCAcCBhUK
CQgLAgQWAgMBAh4BAheAAAoJEOxs6mcZ7zcAypUD/i8LVSrXxuDxr0bEGsNsVb5O
8cofaO6wW04AtCags7pQeLuLcVepeSRORtHaoRZsa3SHFSBykdiPB+ll8G4grYja
mYHHXTTUgjYuoiFMUJoFqJkDUACgxBXP9uzFY6mIDdrX63LF9muGWtNdUz0LybOb
ACYlqY9sKUCMB4vV44qpuI0EZTjavgEEAM+rofSi7kH9yyrL3jFGDZRsmrOrGVTu
phUJfdewV8N4xMxj+NPeC955x1L4zb9bi2Ev5kOsM/YGB09v8nUFSoIqWE93NbQL
pMUq7m7aOilIxjjW0O6+iS8bE8gIOzzMweUBvt4bylJtlX8x3hqf94BXhwv3V+4S
YVJC9XGOYv2dABEBAAGItgQYAQoAIBYhBAB98E297Qm/EV76+uxs6mcZ7zcABQJl
ONq+AhsMAAoJEOxs6mcZ7zcAATgD/3tFCp1GszTi935QMXCNZpZY09QdncgXKVy9
jcJ2pnpER/7t2Pm6Zqu0UZdWHuAIP+lPTgL8Bf0UAmF+h7jIOwr0n76NvPiHNW1X
Gx+RVkNPSEbTH0bWdsyV8LE+E0NowoUaVsjWMzq+QZY+wjrURSWC2iLC1yDb+EPd
kTfIwdES
=ZNGF
-----END PGP PUBLIC KEY BLOCK-----
'
};
grant() { success grantcmd $a0 --osh accountGrantCommand --account $account0 --command "$1"; }
revoke() { success revokecmd $a0 --osh accountRevokeCommand --account $account0 --command "$1"; }
cat >"$mytmpdir/ssh_config" <<EOF
StrictHostKeyChecking no
SendEnv LC_*
PubkeyAuthentication yes
PasswordAuthentication no
RequestTTY yes
EOF
if [ "${capabilities[mfa]}" = 1 ] || [ "${capabilities[mfa-password]}" = 1 ]; then
cat >>"$mytmpdir/ssh_config" <<EOF
ChallengeResponseAuthentication yes
KbdInteractiveAuthentication yes
PreferredAuthentications publickey,keyboard-interactive
EOF
else
cat >>"$mytmpdir/ssh_config" <<EOF
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
PreferredAuthentications publickey
EOF
fi
outdir="$mytmpdir/out"
mkdir -p $outdir || exit 1
touch "$outdir/.basename"
# checking which screen syntax works on this OS
screen="screen -L"
if screen -h 2>&1 | grep -q -- -Logfile; then
screen="screen -L -Logfile"
fi
# /checking
testno=0
testcount=0
basename=""
nbfailedret=0
nbfailedgrep=0
nbfailedcon=0
nbfailedlog=0
nbfailedgeneric=0
totalerrors=0
isbad=0
start_time=$(date +%s)
update_totalerrors()
{
(( totalerrors = nbfailedret + nbfailedgrep + nbfailedcon + nbfailedlog + nbfailedgeneric ))
}
prefix()
{
local elapsed=$(( $(date +%s) - start_time))
local min=$(( elapsed / 60 ))
local sec=$(( elapsed - min * 60 ))
local prefixfmt="%b"
update_totalerrors
[ -n "$opt_log_prefix" ] && prefixfmt="%16b "
if [ "$totalerrors" = 0 ]; then
printf "${prefixfmt}%02dm%02d %b[--]%b" "$opt_log_prefix" "$min" "$sec" "$DARKGRAY" "$NOC"
else
printf "${prefixfmt}%02dm%02d %b[%d err]%b" "$opt_log_prefix" "$min" "$sec" "$RED" "$totalerrors" "$NOC"
fi
}
run()
{
# display verbose output about the previous test if it was bad
# we do this here because this way we're sure that all checks have been done for it
# at this stage (retvalshouldbe, json, ...)
if [ "$isbad" = 1 ]; then
if [ -f "$outdir/$basename.script" ]; then
printf "%b%b%b\\n" "$WHITE_ON_BLUE" "[INFO] test script follows" "$NOC"
cat "$outdir/$basename.script"
fi
printf "%b%b%b\\n" "$WHITE_ON_BLUE" "[INFO] output of the command follows" "$NOC"
cat "$outdir/$basename.log"
printf "%b%b%b\\n" "$WHITE_ON_BLUE" "[INFO] returned json follows" "$NOC"
grep "^JSON_OUTPUT=" -- $outdir/$basename.log | cut -d= -f2- | jq --sort-keys .
if [ "$opt_skip_consistency_check" != 1 ]; then
printf "%b%b%b\\n" "$WHITE_ON_BLUE" "[INFO] consistency check follows" "$NOC"
cat "$outdir/$basename.cc"
fi
if test -t 0 && [ "$opt_no_pause_on_fail" != 1 ]; then
printf "%b%b%b\\n" "$WHITE_ON_BLUE" "[INFO] press enter to continue" "$NOC"
read -r _
fi
fi
isbad=0
# now prepare for the current test
testno=$(( testno + 1 ))
[ "$COUNTONLY" = 1 ] && return
name="$modulename"
if [ -z "$name" ]; then
name="main"
fi
case="$1"
shift
basename=$(printf '%04d-%s-%s' $testno $name $case | sed -re "s=/=_=g")
# if we're about to run a script, keep a copy there
if [ -x "$1" ] && [ "$#" -eq 1 ]; then
cp "$1" "$outdir/$basename.script"
fi
printf '%b %b*** [%04d/%04d] %b::%b %b(%b)%b\n' "$(prefix)" "$BOLD_CYAN" "$testno" "$testcount" "$name" "$case" "$NOC$DARKGRAY" "$*" "$NOC"
# if not set, set to zero, see sleepafter()
: "${sleepafter:=0}"
# put an invalid value in this file, should be overwritten. we also use it as a lock file.
echo -1 > $outdir/$basename.retval
# run the test
flock "$outdir/$basename.retval" $screen "$outdir/$basename.log" -D -m -fn -ln bash -c "$* ; echo \$? > $outdir/$basename.retval ; sleep $sleepafter"
flock "$outdir/$basename.retval" true
unset sleepafter
# look for generally bad strings in the output
_bad='at /usr/share/perl|compilation error|compilation aborted|BEGIN failed|gonna crash|/opt/bastion/|sudo:|ontinuing anyway|MAKETESTFAIL'
_badexclude='/etc/shells'
# shellcheck disable=SC2126
if [ "$(grep -qE "$_bad" $outdir/$basename.log | grep -Ev "$_badexclude" | wc -l)" -gt 0 ]; then
nbfailedgeneric=$(( nbfailedgeneric + 1 ))
fail "BAD STRING" "(generic known-bad string found in output)"
fi
# now run consistency check on the target, unless configured otherwise
if [ "$opt_skip_consistency_check" != 1 ]; then
# sleep 1s if sshd has been reloaded
[ "$case" = "sshd_reload" ] && sleep 1
flock "$outdir/$basename.retval" $screen "$outdir/$basename.cc" -D -m -fn -ln $r0 '
/opt/bastion/bin/admin/check-consistency.pl ; echo _RETVAL_CC=$?= ;
grep -Fw -e warn -e die -e code-warning /var/log/bastion/bastion.log | grep -Fv "'"${code_warn_exclude:-__none__}"'" | sed "s/^/_SYSLOG=/" ;
: > /var/log/bastion/bastion.log
'
flock "$outdir/$basename.retval" true
ccret=$( grep _RETVAL_CC= "$outdir/$basename.cc" | cut -d= -f2)
syslogline=$(grep _SYSLOG= "$outdir/$basename.cc" | cut -d= -f2-)
if [ "$ccret" != 0 ]; then
nbfailedcon=$(( nbfailedcon + 1 ))
fail "CONSISTENCY CHECK"
fi
if [ -n "$syslogline" ]; then
nbfailedlog=$(( nbfailedlog + 1 ))
fail "WARN/DIE/CODE-WARN TRIGGERED"
fi
# reset this for the next test
unset code_warn_exclude
fi
}
script() {
section=$1
shift
if [ "$COUNTONLY" = 1 ]; then
run $section true
return
fi
tmpscript=$(mktemp)
echo "#! /usr/bin/env bash" > "$tmpscript"
echo "$*" >> "$tmpscript"
chmod 755 "$tmpscript"
run $section "$tmpscript"
rm -f "$tmpscript"
}
retvalshouldbe()
{
[ "$COUNTONLY" = 1 ] && return
shouldbe=$1
got=$(< $outdir/$basename.retval)
if [ "$got" = "$shouldbe" ] ; then
ok "RETURN VALUE" "($shouldbe)"
else
nbfailedret=$(( nbfailedret + 1 ))
fail "RETURN VALUE" "(got $got instead of $shouldbe)"
fi
}
fail() {
printf '%b %b[FAIL]%b %b\n' "$(prefix)" "$BLACK_ON_RED" "$NOC" "$*"
isbad=1
}
ok() {
printf '%b %b[ OK ]%b %b\n' "$(prefix)" "$BLACK_ON_GREEN" "$NOC" "$*"
}
success()
{
run "$@"
retvalshouldbe 0
}
plgfail()
{
run "$@"
retvalshouldbe 100
}
sleepafter()
{
sleepafter=$(($1 * opt_slowness_factor))
}
ignorecodewarn()
{
code_warn_exclude="$*"
}
get_json()
{
[ "$COUNTONLY" = 1 ] && return
grep "^JSON_OUTPUT=" -- $outdir/$basename.log | tail -n1 | cut -d= -f2-
}
get_stdout()
{
[ "$COUNTONLY" = 1 ] && return
cat $outdir/$basename.log
}
json()
{
[ "$COUNTONLY" = 1 ] && return
local jq1="" jq2="" jq3=""
local splitsort=0
while [ $# -ge 2 ] ; do
if [ "$1" = "--splitsort" ]; then
splitsort=1
shift
continue
elif [ "$1" = "--argjson" ] || [ "$1" = "--arg" ]; then
jq1="$1"
jq2="$2"
jq3="$3"
shift 3
continue
fi
local filter="$1" expected="$2"
shift 2
json=$(get_json)
set +e
if [ -n "$jq3" ]; then
got=$($jq "$jq1" "$jq2" "$jq3" "$filter" <<< "$json")
else
got=$($jq "$filter" <<< "$json")
fi
if [ "$splitsort" = 1 ]; then
expected=$(echo "$expected" | tr " " "\\n" | sort)
got=$($jq ".[]" <<< "$got" | sort)
fi
set -e
if [ -z "$json" ] ; then
nbfailedgrep=$(( nbfailedgrep + 1 ))
fail "JSON VALUE" "(no json found in output, couldn't look for key <$filter>)"
elif [ "$expected" = "$got" ] ; then
ok "JSON VALUE" "($filter => $expected) [$jq1 $jq3 $jq3]"
else
nbfailedgrep=$(( nbfailedgrep + 1 ))
fail "JSON VALUE" "(for key <$filter> wanted <$expected> but got <$got>, with optional params jq1='$jq1' jq2='$jq2' jq3='$jq3')"
fi
done
}
json_document()
{
[ "$COUNTONLY" = 1 ] && return
local fulljson="$1"
local tmpdiff; tmpdiff=$(mktemp)
local diffret=0
diff -u0 <(echo "$fulljson" | jq -S .) <(get_json | jq -S .) > "$tmpdiff"; diffret=$?
if [ "$diffret" = 0 ]; then
ok "JSON DOCUMENT" "(fully matched)"
else
fail "JSON DOCUMENT" "($(awk '{if(NR>3){print}}' "$tmpdiff" | grep -c '^[-+]') lines differ)"
awk '{if(NR>3){print}}' "$tmpdiff"
fi
rm -f "$tmpdiff"
}
pattern()
{
[ "$COUNTONLY" = 1 ] && return
if grep -qE -- "$1" <<< "$2" ; then
ok "PATTERN" "(got '$1' in '$2')"
else
nbfailedgrep=$(( nbfailedgrep + 1 ))
fail "PATTERN" "(wanted '$1' in '$2')"
fi
}
contain()
{
[ "$COUNTONLY" = 1 ] && return
local specialoption=''
if [ "$1" != "REGEX" ] ; then
specialoption='-F'
else
specialoption='-E'
shift
fi
if grep -q $specialoption -- "$1" "$outdir/$basename.log"; then
ok "MUST CONTAIN" "($1)"
else
nbfailedgrep=$(( nbfailedgrep + 1 ))
fail "MUST CONTAIN" "($1)"
fi
}
nocontain()
{
[ "$COUNTONLY" = 1 ] && return
grepit="$1"
if grep -Eq "$grepit" "$outdir/$basename.log"; then
nbfailedgrep=$(( nbfailedgrep + 1 ))
fail "MUST NOT CONTAIN" "(should not have found string '$grepit' in output)"
else
ok "MUST NOT CONTAIN" "($grepit)"
fi
}
configchg()
{
success configchange $r0 perl -pe "$*" -i "$opt_remote_etc_bastion/bastion.conf"
}
configsetquoted()
{
success configset $r0 perl -pe 's=^\\\\x22'"$1"'\\\\x22.+=\\\\x22'"$1"'\\\\x22:\\\\x22'"$2"'\\\\x22,=' -i "$opt_remote_etc_bastion/bastion.conf"
}
configset()
{
success configset $r0 perl -pe 's=^\\\\x22'"$1"'\\\\x22.+=\\\\x22'"$1"'\\\\x22:'"$2"',=' -i "$opt_remote_etc_bastion/bastion.conf"
}
sshclientconfigchg()
{
success sshclientconfigchange $r0 perl -pe "$*" -i /etc/ssh/ssh_config
}
dump_vars_and_funcs()
{
set | grep -v -E '^('\
'testno|section|code_warn_exclude|COPROC_PID|LINES|COLUMNS|PIPESTATUS|_|'\
'BASH_LINENO|basename|case|json|name|tmpscript|grepit|got|isbad|'\
'nbfailedgrep|shouldbe)='
}
runtests()
{
modulename=main
# ensure syslog is clean
ignorecodewarn 'Configuration error' # previous unit tests can provoke this
success syslog_cleanup $r0 "\": > /var/log/bastion/bastion.log\""
# patch the remote bastionCommand to the proper value
configchg 's=^\\\\x22bastionCommand\\\\x22.+=\\\\x22bastionCommand\\\\x22:\\\\x22ssh\\\\x20USER\\\\x40'"$remote_ip"'\\\\x20-p\\\\x20'"$remote_port"'\\\\x20-t\\\\x20--\\\\x22,='
# account1 skips PAM MFA
success account1_nopam $r0 "usermod -a -G bastion-nopam $account0"
# backup the original default configuration on target side
now=$(date +%s)
success backupconfig $r0 "dd if=$opt_remote_etc_bastion/bastion.conf of=$opt_remote_etc_bastion/bastion.conf.bak.$now"
grant accountRevokeCommand
for module in "$(dirname $0)"/tests.d/???-*.sh
do
module="$(readlink -f "$module")"
modulename="$(basename "$module" .sh)"
if [ -n "$opt_module" ]; then
skip=1
for wantedmod in $opt_module
do
if [ "$wantedmod" = "$(basename "$module")" ]; then
skip=0
fi
done
if [ "$skip" = 1 ]; then
echo "### SKIPPING MODULE $modulename"
continue
fi
fi
echo "### RUNNING MODULE $modulename"
dump_vars_and_funcs > "$tmp_a"
module_ret=0
# as this is a loop, we do the shellcheck in a reversed way, see any included module for more info:
# shellcheck disable=SC1090
if source "$module"; then
module_ret=0
else
module_ret=$?
fi
dump_vars_and_funcs > "$tmp_b"
success module_postrun test "$module_ret" = 0
# put the backed up configuration back after each module, just in case the module modified it
modulename=main
success configrestore $r0 "dd if=$opt_remote_etc_bastion/bastion.conf.bak.$now of=$opt_remote_etc_bastion/bastion.conf"
# verify that the env hasn't been modified
success check_env_after_module diff -u "$tmp_a" "$tmp_b"
done
# if the check_env_after_module of the last module fails, we wouldn't get the verbose error,
# craft a test that always work and will notice that the previous one failed, which'll display
# the verbose error information
modulename=main
success "done" true
}
COUNTONLY=0
echo '=== running unit tests ==='
# a while read loop doesn't work well here:
# shellcheck disable=SC2044
for f in $(find "$basedir/tests/unit/" -mindepth 1 -maxdepth 1 -type f -name "*.pl" -print)
do
fbasename=$(basename "$f")
echo "-> $fbasename"
set +e
$r0 perl "$opt_remote_basedir/tests/unit/$fbasename"; ret=$?
set -e
if [ $ret != 0 ]; then
printf "%b%b%b\\n" "$WHITE_ON_RED" "Unit tests failed (ret=$ret) :(" "$NOC"
exit 1
fi
done
COUNTONLY=1
testno=0
echo '=== counting functional tests ==='
runtests
testcount=$testno
echo "=== will run $testcount functional tests ==="
COUNTONLY=0
testno=0
runtests
echo
if [ $((nbfailedret + nbfailedgrep + nbfailedcon + nbfailedgeneric)) -eq 0 ] ; then
printf "%b%b%b\\n" "$BLACK_ON_GREEN" "All tests succeeded :)" "$NOC"
else
(
echo
printf "%b" "$WHITE_ON_RED"
echo "One or more tests failed :("
echo "- $nbfailedret unexpected return values"
echo "- $nbfailedgrep unexpected JSON/text values"
echo "- $nbfailedcon failed consistency checks"
echo "- $nbfailedlog warn/die triggered"
echo "- $nbfailedgeneric generic bad strings found"
printf "%b" "$NOC"
) | tee $outdir/summary
fi
echo
set +e
set +u
update_totalerrors
[ $totalerrors -ge 255 ] && totalerrors=254
rm -rf "$mytmpdir"
trap EXIT
if [ -n "$opt_post_run" ]; then
bash -c "$opt_post_run"
fi
exit $totalerrors