#!/usr/bin/env bash
# selftest.sh - prove the toolkit works on this host.
#
# Builds the sample target, runs every analysis method against it, and checks
# that each one recovers the facts we know are true (the sample's source code
# is the ground truth). Exit 0 = the toolkit is trustworthy on this box.
#
#   ./selftest.sh            # run everything available
#   ./selftest.sh -k         # keep the output directory
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
KEEP=0; [[ "${1:-}" == "-k" ]] && KEEP=1
OUT="$ROOT/out/selftest-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$OUT"
PASS=0; FAIL=0; SKIP=0

ok()   { printf '  \033[32mPASS\033[0m %s\n' "$1"; PASS=$((PASS+1)); }
bad()  { printf '  \033[31mFAIL\033[0m %s\n' "$1"; FAIL=$((FAIL+1)); }
skip() { printf '  \033[33mSKIP\033[0m %s\n' "$1"; SKIP=$((SKIP+1)); }
check() { # check <description> <file> <grep-pattern>
  if grep -qE "$3" "$2" 2>/dev/null; then ok "$1"; else bad "$1 (pattern: $3)"; fi
}

echo "### 0. environment"
command -v strace >/dev/null && ok "strace present" || bad "strace missing (build/00-install-rpms.sh)"
for t in readelf objdump strings; do
  if command -v "$t" >/dev/null; then ok "$t present"
  elif /usr/bin/python3 -c 'import elftools' >/dev/null 2>&1; then
    skip "$t absent (no binutils) - elf-static.sh falls back to pyelftools"
  else
    bad "$t missing and pyelftools absent - install binutils or python3-pyelftools"
  fi
done
for t in gcc make; do
  command -v "$t" >/dev/null && ok "$t present (optional)" \
                             || skip "$t absent - the kit uses prebuilt/ instead"
done
if [[ "$(sysctl -n kernel.yama.ptrace_scope 2>/dev/null || echo 0)" -gt 1 ]]; then
  echo "  NOTE: kernel.yama.ptrace_scope > 1 - strace of non-children may fail"
fi

echo
echo "### 1. build (or install) the sample target"
# A locked-down RHEL 9.6 host often has neither gcc nor make. The kit ships the
# binaries prebuilt for exactly that case, so this step must not be a hard
# requirement - only the analysis steps that follow are.
HAVE_CC=0; command -v gcc >/dev/null 2>&1 || command -v cc >/dev/null 2>&1 && HAVE_CC=1
if command -v make >/dev/null 2>&1; then
  make -C "$ROOT/target" clean >/dev/null 2>&1
  make -C "$ROOT/tools" clean >/dev/null 2>&1
  if make -C "$ROOT/target" >"$OUT/make.log" 2>&1 && make -C "$ROOT/tools" >>"$OUT/make.log" 2>&1; then
    [[ $HAVE_CC -eq 1 ]] && ok "target and interposer built from source" \
                         || ok "target and interposer installed from prebuilt/ (no compiler here)"
  else
    bad "make failed (see $OUT/make.log)"
  fi
else
  if "$ROOT/build/20-use-prebuilt.sh" >"$OUT/make.log" 2>&1; then
    ok "target and interposer installed from prebuilt/ (no make, no compiler)"
  else
    bad "could not install prebuilt binaries (see $OUT/make.log)"
  fi
fi
for b in "$ROOT/target/sample_app" "$ROOT/target/sample_helper" \
         "$ROOT/target/libsampleplugin.so" "$ROOT/tools/libpreload_audit.so"; do
  [[ -x "$b" ]] && ok "present: $(basename "$b")" || bad "missing: $(basename "$b")"
done

cd "$ROOT/target" || exit 1

echo
echo "### 2. static analysis (elf-static.sh)"
"$ROOT/tools/elf-static.sh" ./sample_app > "$OUT/STATIC.md" 2>"$OUT/static.err"
check "detects the dynamic loader"          "$OUT/STATIC.md" 'ld-linux-x86-64|program interpreter'
check "lists libc as NEEDED"                "$OUT/STATIC.md" 'NEEDED.*libc\.so'
check "buckets file-access imports"         "$OUT/STATIC.md" '\*\*File access\*\*'
check "spots dlopen capability"             "$OUT/STATIC.md" 'dlopen'
check "spots exec capability"               "$OUT/STATIC.md" 'execv|popen|fork'
check "finds embedded paths"                "$OUT/STATIC.md" '/tmp/sample_app\.log'
check "reports PIE/RELRO/NX"                "$OUT/STATIC.md" 'RELRO'

echo
echo "### 3. syscall trace (elf-trace.sh)"
"$ROOT/tools/elf-trace.sh" -o "$OUT/trace" -- ./sample_app >"$OUT/trace.out" 2>&1
R="$OUT/trace/REPORT.md"
check "reads /etc/hostname"                 "$R" '/etc/hostname'
check "reads the config file"               "$R" 'sample_app\.conf'
check "records the log file as written"     "$R" '/tmp/sample_app\.log.*(write|create)'
check "sees the temp file created+deleted"  "$R" 'sample_app\.tmpdata.*delete'
check "finds the ENOENT probe"              "$R" '/etc/sample_app/secrets\.conf'
check "builds a process tree"               "$R" 'pid [0-9]+.*sample_helper'
check "catches the popen'd shell"           "$R" '/bin/sh'
check "catches the grandchild uname"        "$R" '/usr/bin/uname'
check "records the dlopen'd plugin"         "$R" 'libsampleplugin\.so'
check "records the TCP connect"             "$R" 'connect'
check "records the unix socket bind"        "$R" 'sample_app\.sock'
for c in files processes network; do
  [[ -s "$OUT/trace/$c.csv" ]] && ok "$c.csv written" || bad "$c.csv missing"
done
# the helper is a *separate* pid reading a file the parent never opens
check "attributes /etc/services to the child" "$OUT/trace/files.csv" '/etc/services'

echo
echo "### 4. libc interposer (elf-preload.sh)"
"$ROOT/tools/elf-preload.sh" -o "$OUT/preload" -- ./sample_app >"$OUT/preload.out" 2>&1
P="$OUT/preload/PRELOAD.md"
check "logs fopen of the config"            "$P" 'sample_app\.conf'
check "logs the dlopen"                     "$P" 'dlopen.*libsampleplugin'
check "logs popen of uname"                 "$P" 'popen.*uname'
check "logs the failed connect"             "$P" 'connect.*127\.0\.0\.1:9'
check "follows the forked child"            "$P" '/etc/services'

echo
echo "### 5. full driver (elf-report.sh)"
"$ROOT/tools/elf-report.sh" -o "$OUT/full" -- ./sample_app >"$OUT/full.out" 2>&1
A="$OUT/full/ANALYSIS.md"
check "ANALYSIS.md produced"                "$A" '^# Analysis'
check "flags the dlopen-only library"       "$A" 'libsampleplugin\.so'
check "lists written files"                 "$A" '/tmp/sample_app\.log'
check "raises the shell-exec flag"          "$A" 'shell was exec'
check "raises the /etc/passwd flag"         "$A" '/etc/passwd'

echo
echo "### 6. runtime snapshot (elf-snapshot.sh)"
sleep 300 & SPID=$!
"$ROOT/tools/elf-snapshot.sh" -p "$SPID" -n 2 -i 1 -o "$OUT/snap" >"$OUT/snap.out" 2>&1
kill "$SPID" 2>/dev/null
S="$OUT/snap/SNAPSHOT.md"
# NB: on RHEL, sleep is part of the coreutils multi-call binary, so /proc/PID/exe
# reads /usr/bin/coreutils. Assert on the cmdline, which is what identifies it.
check "resolves the exe"                    "$S" 'exe *: */'
check "captures the cmdline"                "$S" 'cmdline *:.*sleep'
check "captures the fd table"               "$S" 'Raw fd table'
check "captures mapped libraries"           "$S" 'libc\.so'

echo
echo "### 7. fanotify (elf-fanotify.sh)"
FAT="$ROOT/build/bin/fatrace"; command -v fatrace >/dev/null && FAT="$(command -v fatrace)"
# probe first: fanotify cannot mark overlayfs, so it is simply unavailable in a
# container. That is an environment limit, not a failure of the kit.
FAN_OK=0
if [[ $EUID -eq 0 && -x "$FAT" ]]; then
  "$FAT" > "$OUT/fanprobe.log" 2>/dev/null & FPROBE=$!
  sleep 0.5; cat /etc/hostname >/dev/null 2>&1; sleep 0.5
  kill "$FPROBE" 2>/dev/null; wait "$FPROBE" 2>/dev/null
  [[ -s "$OUT/fanprobe.log" ]] && FAN_OK=1
fi
if [[ $FAN_OK -eq 1 ]]; then
  "$ROOT/tools/elf-fanotify.sh" -o "$OUT/fan" -- ./sample_app >"$OUT/fan.out" 2>&1
  check "sees the target's own opens"       "$OUT/fan/FANOTIFY.md" "sample_app\([0-9]+\).*sample_app\.conf"
elif [[ $EUID -ne 0 || ! -x "$FAT" ]]; then
  skip "fanotify needs root and build/bin/fatrace (build/10-build-sources.sh fatrace)"
else
  skip "fanotify produced no events here (overlayfs/container) - use elf-trace.sh or elf-audit.sh"
fi

echo
echo "### 8. audit rules (elf-audit.sh)"
if [[ $EUID -eq 0 ]] && command -v auditctl >/dev/null && auditctl -s >/dev/null 2>&1; then
  BIN="$PWD/sample_app"
  "$ROOT/tools/elf-audit.sh" start "$BIN" selftestkey >"$OUT/audit.out" 2>&1
  ./sample_app >/dev/null 2>&1
  sleep 1
  "$ROOT/tools/elf-audit.sh" report selftestkey >"$OUT/audit-report.md" 2>&1
  "$ROOT/tools/elf-audit.sh" stop selftestkey >>"$OUT/audit.out" 2>&1
  if grep -qE 'sample_app|hostname' "$OUT/audit-report.md"; then ok "audit recorded the run"
  else skip "audit produced no records (auditd may be disabled or rules rejected)"; fi
else
  skip "auditd not usable here (needs root + a running auditd)"
fi

echo
echo
echo "### 9. worked httpd example (optional)"
# Regression cover for examples/httpd-example.sh: it exercises dlopen discovery,
# a forking+threading process tree and exec'd CGI children against a real daemon,
# which the synthetic sample cannot. Skipped when httpd is not installed.
if command -v httpd >/dev/null 2>&1 && command -v curl >/dev/null 2>&1 && [[ $EUID -eq 0 ]]; then
  if "$ROOT/examples/httpd-example.sh" -o "$OUT/httpd" >"$OUT/httpd.out" 2>&1; then
    H="$OUT/httpd/HTTPD-FINDINGS.md"
    check "finds dlopen'd httpd modules"     "$H" 'mod_mpm_event\.so'
    check "separates threads from processes" "$H" 'thread\(s\) were created'
    check "catches the CGI child"            "$H" "whoami\\.cgi"
    check "catches the CGI's own children"   "$H" '/usr/bin/id'
    check "reports the listening socket"     "$H" 'listening on.*:[0-9]+'
    check "snapshots the live daemon"        "$H" '/proc/PID/maps|Memory-mapped|held open'
  else
    bad "examples/httpd-example.sh failed (see $OUT/httpd.out)"
  fi
else
  skip "httpd example needs root, httpd and curl"
fi

echo "================================================"
printf 'PASS %d   FAIL %d   SKIP %d\n' "$PASS" "$FAIL" "$SKIP"
echo "artifacts: $OUT"
[[ $KEEP -eq 0 && $FAIL -eq 0 ]] && echo "(pass -k to keep large traces; they are kept anyway on failure)"
exit $(( FAIL > 0 ? 1 : 0 ))
