#!/bin/bash
# Enodia Sentinel — red-team self-test harness.
#
# Simulates each detector's threat signature using SAFE, clearly-labeled
# stand-ins (all tagged "enodia-drill"), so you can watch Sentinel catch them
# live. Nothing here is malicious: no real payloads, no system files touched,
# everything is cleaned up on exit.
#
# Usage:
#   sentinel-redteam            run all drills (default 20s each window)
#   sentinel-redteam <name>...  run only the named drills
#   sentinel-redteam --list     list available drills
#
# Drills: reverse_shell ld_preload deleted_exe new_listener new_suid persistence
#
# Watch it work, in another terminal:
#   sudo tail -f /var/log/enodia-sentinel/events.log
# or one-shot:
#   sudo /usr/local/bin/sentinel.sh --check

set -u
HOLD="${HOLD:-20}"                       # seconds to keep each drill artifact alive
TMP="$(mktemp -d /tmp/enodia-drill.XXXXXX)"
PIDS=()
SANDBOX="$TMP/persistence-sandbox"        # used by the persistence drill
LISTEN_PORT=14444

cleanup() {
    echo "[*] cleaning up drill artifacts..."
    for p in "${PIDS[@]}"; do kill "$p" 2>/dev/null; done
    rm -rf "$TMP"
    echo "[*] done. (Sentinel snapshots in /var/log/enodia-sentinel/ are kept.)"
}
trap cleanup EXIT INT TERM

note() { echo -e "\n=== DRILL: $1 ===\n    $2"; }

# A tiny TCP listener that accepts one connection and holds it open (no EOF, no
# data) for $HOLD seconds. Python is always present and, unlike some nc/ncat
# builds, won't half-close and tear the connection down before detection.
drill_listener() {  # $1 = port
    python3 -c 'import socket,sys,time
s=socket.socket(); s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind(("127.0.0.1",int(sys.argv[1]))); s.listen(1)
try:
    c,_=s.accept(); time.sleep(int(sys.argv[2]))
except Exception: pass' "$1" "$HOLD" >/dev/null 2>&1 &
    PIDS+=("$!")
}

drill_reverse_shell() {
    note reverse_shell "a bash with a TCP socket wired to its stdin/stdout (classic reverse shell)"
    # Local listener stands in for the attacker's box. No remote traffic leaves.
    drill_listener "$LISTEN_PORT"
    sleep 1
    # bash whose fd 0/1/2 are the socket — exactly what `nc -e /bin/bash` produces.
    # It then BLOCKS reading the socket (like a real reverse shell awaiting
    # commands) so the process holding the socket stays a 'bash', not a sleep.
    ( exec -a enodia-drill-rsh bash -c \
        'for i in 1 2 3 4 5 6 7 8 9 10; do exec 3<>/dev/tcp/127.0.0.1/'"$LISTEN_PORT"' && break; sleep 0.3; done
         exec 0<&3 1>&3 2>&3; read -t '"$HOLD"' _line' ) &
    PIDS+=("$!")
    echo "    -> spawned drill reverse shell (pid $!) for ${HOLD}s"
}

drill_ld_preload() {
    note ld_preload "a process running with LD_PRELOAD pointing into /tmp"
    : > "$TMP/evil.so"   # empty file; we never actually load it, just set the env
    ( LD_PRELOAD="$TMP/evil.so" exec -a enodia-drill-preload sleep "$HOLD" ) &
    PIDS+=("$!")
    echo "    -> spawned process with LD_PRELOAD=$TMP/evil.so (pid $!)"
    echo "       NOTE: does NOT touch /etc/ld.so.preload (that would be system-wide)."
}

drill_deleted_exe() {
    note deleted_exe "a process whose executable was deleted from disk (fileless)"
    cp /bin/sleep "$TMP/enodia-drill-ghost" 2>/dev/null
    ( exec "$TMP/enodia-drill-ghost" "$HOLD" ) &
    local p=$!; PIDS+=("$p")
    sleep 0.5
    rm -f "$TMP/enodia-drill-ghost"   # now /proc/<pid>/exe shows "(deleted)" in /tmp
    echo "    -> running pid $p from a now-deleted /tmp binary"
}

drill_new_listener() {
    note new_listener "a new TCP listening port that wasn't in the baseline"
    drill_listener $((LISTEN_PORT+1))
    echo "    -> opened listener on 127.0.0.1:$((LISTEN_PORT+1)) (pid ${PIDS[-1]})"
}

drill_new_suid() {
    note new_suid "a SUID binary appearing in a world-writable dir (/tmp)"
    cp /bin/true "$TMP/enodia-drill-suid" 2>/dev/null
    chmod 4755 "$TMP/enodia-drill-suid" 2>/dev/null
    echo "    -> dropped SUID binary at $TMP/enodia-drill-suid (perms: $(stat -c %A "$TMP/enodia-drill-suid" 2>/dev/null))"
    echo "       (will be detected on the next sweep; kept for ${HOLD}s)"
    ( sleep "$HOLD" ) & PIDS+=("$!")
}

drill_persistence() {
    note persistence "modifying a watched persistence file"
    echo "    NOTE: this drill only fires if you've pointed WATCH_PERSISTENCE at the"
    echo "    sandbox below. To test safely, add this to /etc/enodia-sentinel.conf:"
    echo "        WATCH_PERSISTENCE=\"\$WATCH_PERSISTENCE $SANDBOX\""
    echo "    then restart the service. Otherwise this would require touching real"
    echo "    files like ~/.ssh/authorized_keys, which the harness will NOT do."
    mkdir -p "$SANDBOX"
    echo "# enodia-drill $(date)" >> "$SANDBOX/authorized_keys"
    echo "    -> wrote $SANDBOX/authorized_keys"
}

run() {
    case "$1" in
        reverse_shell) drill_reverse_shell ;;
        ld_preload)    drill_ld_preload ;;
        deleted_exe)   drill_deleted_exe ;;
        new_listener)  drill_new_listener ;;
        new_suid)      drill_new_suid ;;
        persistence)   drill_persistence ;;
        *) echo "unknown drill: $1" >&2; return 1 ;;
    esac
}

ALL=(reverse_shell ld_preload deleted_exe new_listener new_suid persistence)

if [ "${1:-}" = "--list" ]; then printf '%s\n' "${ALL[@]}"; exit 0; fi

echo "######################################################################"
echo "#  ENODIA SENTINEL — RED TEAM DRILL                                  #"
echo "#  All artifacts are labeled 'enodia-drill' and auto-cleaned.        #"
echo "#  Watch detections:  sudo tail -f /var/log/enodia-sentinel/events.log"
echo "######################################################################"

if [ "$#" -gt 0 ]; then targets=("$@"); else targets=("${ALL[@]}"); fi
for d in "${targets[@]}"; do run "$d"; done

echo -e "\n[*] drills live for ${HOLD}s. Triggering a Sentinel sweep check now:"
if [ -x /usr/local/bin/sentinel.sh ]; then
    sudo /usr/local/bin/sentinel.sh --check 2>/dev/null \
        | grep -i drill && echo "    ^ Sentinel saw the drills." \
        || echo "    (run 'sudo sentinel.sh --check' yourself, or watch events.log)"
fi
echo "[*] holding for ${HOLD}s, then cleaning up..."
sleep "$HOLD"
