#!/bin/bash -ue
# autopkgtest for chkrootkit and its cron.daily script
# nb, this edits /etc/chkrootkit.conf so take care
# Depends: chkrootkit, bash (arrays), grep, util-linux (ionice)
#
# nb: Open the output (eg via the .build file) in emacs' org-mode
# to make it easier to read.

MY_DIR="$(realpath "$0")"
MY_DIR="${MY_DIR%/*}"

CONFIG="/etc/chkrootkit/chkrootkit.conf"

echo "* Running ${0##*/} $* (from: $MY_DIR)..."
echo "** env"
MY_BUILD_DIR="${PWD%/src}"
env
echo "MY_BUILD_DIR=$MY_BUILD_DIR"

overall_status="PASS"
failed_runs=() # array of FAILed run_tests

### run a test of chkrootkit
#   $1 is the name for the test (any string)
#   debian/tests/$1.expected is a file of patterns (regexp):
#     expect pattern must appear in the the output (checked with grep -f)
#   $3 $4... are what we run
run_test(){
		local name="$1"
		shift
		local banner="Testing: $name ($*)"

		local expected="$MY_DIR/$name.expected"
		# $output needs to be stable across calls as the 'tee $output'
		# from the cron.daily script can be flagged by chkwtmp if the
		# build is run from inside sbuild inside tmux.
		local output="output"
		local result="ERROR" # overridden by first test in the while loop
		local missing=()
		local pattern

		echo "** $banner ..."
		echo "*** Output"
		"$@" 2>&1 | tee "$output"
		echo "**** Files in log"
		ls -lah /var/log/chkrootkit/
		echo "*** Test of content of output follows..."

		if [ ! -r "$expected" ]; then
				echo "**** SKIPPED: missing file: $expected"
				echo "No file $expected (issue with tests)" >&2
				result="FAIL"
				expected=/dev/null
		elif [ ! -s "$expected" ]; then
				# $expected_patterns is there but is empty (-s means size > 0) then we expect output to be empty too
				echo "**** Expected is empty, so output should be empty"
				if [ ! -s "$output" ]; then
						echo "Output is indeed empty: PASS"
						result="PASS"
				else
						echo "Output is non-empty: FAIL"
						result="FAIL"
						overall_status="FAIL"
						cat "$output"
				fi
		else
				# non-zero $expected_patterns, so we test each line
				while read -r pattern; do
						echo "**** Test for '$pattern'"
						if grep --extended-regexp --regexp="$pattern" "$output"; then
								if [ "$result" = "ERROR" ]; then
										# first pattern
										result="PASS"
								fi
								echo "OK"
						else
								echo "<No match (FAIL)>"
								missing+=("$pattern")
								result="FAIL"
								overall_status="FAIL"
								echo ""
						fi
				done < "$expected"
		fi
		echo "** $result: $banner done: $result"
		if [ "$result" = FAIL ]; then
				echo "*** FAIL was with config set to:"
				cat $CONFIG || ls -la /etc/chkrootkit

				echo "*** Reason(s) for failure follows"
				echo -e "Result: $result\n"
				for pattern in "${missing[@]}"; do
						echo "Missing: $pattern"
				done
		fi
		# Additional grep to avoid listing 'known' unmatched lines:
		# exactly which of the items below are output depends on how you
		# invoked autopkgtest, and what else is running at the time:
		#
		#   1. autopkgtest runs the test from
		#     PWD=/tmp/autopkgtest.GVTrJD/build.XXX/src and
		#     /tmp/autopkgtest.GVTrJD/build.XXX/src.pc/YYY.patch are
		#     flagged as 'suspicious'. (such lines could be included in
		#     the .expected file but presumably if we got rid of all
		#     patches they would disappear)
		#
		#   2. chkutmp flags anything running inside screen/tmux as
		#     lacking a tty. This can include the whole sbuild process
		#     (doesn't happen on salsa.debian.org) (chkutmp starts these
		#     lines with a !, and when DIFF_MODE=true you get an
		#     additional [+ -] if the filter was changed/invalid)
		#
		#   3. when running in a schroot, chkdirs thinks BTRFS is in use
		#     and gives a 'WARNING' (doesn't happen on salsa)
		#
		#   4. various tests do not filter out 'PACKET SNIFFER' reports
		#     from ifpromisc (produced when a network manager is running,
		#     eg salsa uses dhclient)
		#
		#   5. when running in lxc (as on salsa, but not within a
		#     schroot), /tmp/autopkgtest-reboot* scripts will be flagged
		#     as 'suspicious'
		local unmatched="$(grep -v --extended-regexp --file="$expected" \
														 "$output" \
														| grep -v "$MY_BUILD_DIR" \
														| grep -v --extended-regexp "^[+ -]?(!|The tty of the following process\(es\) was not found in /var/run/utmp:|chkdirs: Warning: Possible LKM Trojan installed$|WARNING: It seems you are using BTRFS, if this is true chkdirs can't help you to find hidden files/dirs$|Output from ifpromisc:$|.+: PACKET SNIFFER|/tmp/autopkgtest-reboot)" \
														|| true)"
		if [ ! -z "$unmatched" ]; then
				echo "Unexpected (unmatched) lines follow (for info):"
				echo "$unmatched"
		fi
		rm -f "$output"
		return 0
}

echo "* Setting up the testsuite"
echo "** README"
echo "The purpose of this tests is to check that the actual output is as we expect.

This tests that both chkrootkit (directly invoked) and its cron.daily
cronjob work with various combinations of options.

- Each test has a file listing regexps: each listed regexp must match
  against the output or the test will fail: the 'fix' will often be
  to update the .expected file
- (these are in debian/test/*.expected)
- Output not matched by any such regexp is listed (with some known
  exceptions removed), but does not cause failure
- This testsuite is designed to run in a sbuild schroot or via the CI
  pipeline on salsa.debian.org: you might need to adjust the
  debian/test/*.expected files if running in some other way.
"

echo "** Ensuring chkrootkit finds as much to test as we can"
MADE=""

# nb: in.rexedcs is not included as it is flagged as infected iff it is found
for binary in \
		amd biff cron crontab fingerd in.fingerd gpm hdparm \
		inetd in.identd inetdconf init killall lsdopreload \
		lsof mail mingetty named in.pop2d in.pop3d write \
		pstree rpcinfo rlogind in.rshd slogin sendmail sshd syslogd \
		tcpd tcpdump telnetd timed traceroute; do
		for path in /bin/$binary /sbin/$binary /etc/$binary.conf; do
				if [ ! -e "$path" ]; then
						echo "Making $path"
						cat > "$path" <<EOF
#!/bin/sh
echo "Ensuring $binary exists for chkrootkit test"
exit 0
EOF
						chmod +x "$path"
						MADE="$MADE $path"
				else
						echo "$path: exists"
				fi
		done
done
echo "Done"

## Make some false positives
# 1. something executable under /tmp
FALSE_POSITIVES=/tmp/test-chkrootkit-false-positive
echo "# chkrootkit thinks this could be a Linux.Xor.DDoS rootkit" > $FALSE_POSITIVES
chmod +x $FALSE_POSITIVES

# 2. some 'suspicious' dotfiles in /usr/lib
DOTFILES="/usr/lib/.aaa /.aaa-this-is-not-flagged /usr/lib/.1 /lib/..."
DOTDIRS="/usr/lib/.DIR-aaa /.DIR-aaa-this-is-not-flagged /usr/lib/.1DIR /lib/...DIR"

## Accident prevention:
# we are going to overwrite some paths - in theory this test is run
# in a chroot or other throwaway system, but in case it isn't (eg by accident), save (for later
# restoral) the system files we are about to overwrite.
SYSTEM_FILES="$CONFIG  /etc/chkrootkit/chkrootkit.ignore /var/log/chkrootkit/log.expected /var/log/chkrootkit/log.today /var/log/chkrootkit/log.today.raw"
for f in $SYSTEM_FILES; do
		if [ -f "$f" ]; then
				echo "Preserving existing $f as $f.orig"
				mv "$f" "$f.orig"
		fi
done
####

echo "* Testing: the main binary"
# Standalone running of the main binary
# no hidden files in /usr/lib
run_test "chkrootkit-0-full"  /usr/sbin/chkrootkit

# make some more false positives
touch $DOTFILES
mkdir $DOTDIRS
mkdir /usr/lib/.bbb
run_test "chkrootkit-1-full"  /usr/sbin/chkrootkit
run_test "chkrootkit-2-quiet" /usr/sbin/chkrootkit -q


# options
echo "* Testing: filtering of sniffer (-s)"
run_test "chkrootkit-sniffer-01-full" chkrootkit sniffer

# can't test 'chkrootkit -q' as output will be one "PACKET SNIFFER"
# line for any network manager that happens to be running, or empty if
# there are none.

# prints 'not found'
run_test "chkrootkit-sniffer-02-full-with-s" chkrootkit -s "(PACKET SNIFFER|not promisc)"  sniffer

# fully empty, whatever is running
run_test "chkrootkit-sniffer-03-quiet-with-s" chkrootkit -q -s "PACKET SNIFFER" sniffer


echo "* Testing: the daily cron job gives no output when disabled"
[ ! -e $CONFIG ] || echo >&2 "This should not happen - $CONFIG should have been moved above?"
run_test "cron-1-with-no-config" /etc/cron.daily/chkrootkit

echo "RUN_DAILY=false" > $CONFIG
run_test "cron-2-disabled" /etc/cron.daily/chkrootkit

echo "* Testing: the daily cron job (without diff mode, full output)"
echo "RUN_DAILY=true
DIFF_MODE=false
" > $CONFIG

run_test "cron-no-diff-mode-01-full" /etc/cron.daily/chkrootkit

echo ".*" > /etc/chkrootkit/chkrootkit.ignore
echo ".aaa" > /etc/test-ignore # hides .aaa and .DIR-aaa
echo "FILTER='sed s!^/usr/lib/.b!CHANGED-IN-FILTER_!'
IGNORE_FILE=/etc/test-ignore
DIFF_MODE=false
" >> $CONFIG                   # $FILTER changes .bbb

run_test "cron-no-diff-mode-02-full-filter-and-ignore" /etc/cron.daily/chkrootkit


echo "* Testing: the daily cron job (without diff mode, quiet output)"
echo "RUN_DAILY=true
DIFF_MODE=false
RUN_DAILY_OPTS=-q
" > $CONFIG
: > /etc/chkrootkit/chkrootkit.ignore
rm -f /etc/test-ignore
run_test "cron-no-diff-mode-03-quiet" /etc/cron.daily/chkrootkit

# ionice => same output as previous
if [ ! -x /usr/bin/ionice ]; then
		echo >&2 "error - tests assume ionice is installed. update tests"
fi
echo "IONICE=\"\"
IGNORE_FILE=/nonexistant" >> $CONFIG
run_test "cron-no-diff-mode-04-quiet-no-ionice" /etc/cron.daily/chkrootkit

echo ".*" > /etc/chkrootkit/chkrootkit.ignore
echo "\.aaa$" > /etc/test-ignore # hides .aaa but not .DIR-aaa
echo "FILTER='sed s!^/usr/lib/.b!CHANGED-IN-FILTER_!'
IGNORE_FILE=/etc/test-ignore
" >> $CONFIG

run_test "cron-no-diff-mode-05-quiet-filter-and-ignore" /etc/cron.daily/chkrootkit

echo "FILTER='sed s/this/is/invalid/sed/and/will/be/ignored'" >> $CONFIG
# results of 6 are
# - still subject to same IGNORE_FILE as 05
# - similar to 02 but with an extra 'warning' about invalid FILTER at the top
run_test "cron-no-diff-mode-06-quiet-invalid-filter-is-ignored"  /etc/cron.daily/chkrootkit

echo "* Testing: the daily cron job (with DIFF_MODE, full output)"
echo "RUN_DAILY=true
DIFF_MODE=true
" > $CONFIG
: > /etc/chkrootkit/chkrootkit.ignore
rm /etc/test-ignore
run_test "cron-with-diff-mode-01-full" /etc/cron.daily/chkrootkit

# Re-running gives the same output
run_test "cron-with-diff-mode-02-full-rerun" /etc/cron.daily/chkrootkit

# ...until the admin updates log.expected
cp -f /var/log/chkrootkit/log.today /var/log/chkrootkit/log.expected
run_test "cron-with-diff-mode-03-full-after-update" /etc/cron.daily/chkrootkit

# disabling ionice should not cause a change in output
echo "IONICE=\"\"" >> $CONFIG
run_test "cron-with-diff-mode-04-full-no-ionice" /etc/cron.daily/chkrootkit

echo "\.aaa" > /etc/chkrootkit/chkrootkit.ignore
cat   >> $CONFIG <<EOF
FILTER="\$FILTER -e 's/.b$/CHANGED-IN-FILTER_/'"
EOF
run_test "cron-with-diff-mode-05-full-filter-and-ignore" /etc/cron.daily/chkrootkit


echo "* Testing: the daily cron job (diff mode, quiet output)"
echo "RUN_DAILY=true
RUN_DAILY_OPTS='-q'
DIFF_MODE=true
" > $CONFIG
# reset as if started from scratch
rm -f /var/log/chkrootkit/log.expected
: > /etc/chkrootkit/chkrootkit.ignore
run_test "cron-with-diff-mode-06-quiet" /etc/cron.daily/chkrootkit
run_test "cron-with-diff-mode-07-quiet-rerun" /etc/cron.daily/chkrootkit
cp -f /var/log/chkrootkit/log.today /var/log/chkrootkit/log.expected
run_test "cron-with-diff-mode-08-quiet-after-update" /etc/cron.daily/chkrootkit

echo "aa[a-z]$" > /etc/chkrootkit/chkrootkit.ignore
cat   >> $CONFIG <<EOF
FILTER="\$FILTER -e 's/.b$/CHANGED-IN-FILTER_/'"
EOF

run_test "cron-with-diff-mode-09-quiet-filter-and-ignore" /etc/cron.daily/chkrootkit

echo "FILTER='sed s/this/is/invalid/sed/and/will/be/ignored/with/diff/mode'" >> $CONFIG
run_test "cron-with-diff-mode-10-quiet-invalid-filter-is-ignored"  /etc/cron.daily/chkrootkit










echo "* Closing down the testsuite"
for f in $SYSTEM_FILES; do
		if [ -f "$f.orig" ]; then
				echo "Restoring $f from $f.orig"
				mv -f "$f.orig" "$f" || echo "Failed to restore $f?"
		fi
done
rm -f $DOTFILES $FALSE_POSITIVES /etc/test-ignore $MADE
rmdir $DOTDIRS /usr/lib/.bbb
echo "DONE"
echo "* ${0##*/}: $overall_status"
if [ "$overall_status" = "PASS" ]; then
		exit 0
else
		exit 1
fi
