Add egress drill + sid-verified harness and IR runbooks (roadmap v0.8)
Red-team harness: every detector now has a named drill with an expected signature/sid, shown by `sentinel-redteam --list`. Add the missing `egress` drill (the one detector without one) — explicit-only, since it is the sole drill that emits real outbound traffic. After firing, the harness runs `enodia-sentinel check` and reports PASS/MISS per poll detection, turning it into a shared regression check. Add docs/RUNBOOKS.md: confirm/preserve/contain/recover playbooks for reverse shells & egress, persistence, trojaned binaries / package tampering, hidden listeners & rootkits, and sensor tampering — each tied to the real signatures, sids, and commands. Linked from README and the OPERATIONS alert workflow. Both close the remaining smaller v0.8 roadmap items. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f2d896a2d7
commit
4015ec872b
5 changed files with 335 additions and 14 deletions
|
|
@ -8,11 +8,13 @@
|
|||
# everything is cleaned up on exit.
|
||||
#
|
||||
# Usage:
|
||||
# sentinel-redteam run all drills (default 20s each window)
|
||||
# sentinel-redteam run the default drills (20s each window)
|
||||
# sentinel-redteam <name>... run only the named drills
|
||||
# sentinel-redteam --list list available drills
|
||||
# sentinel-redteam --list list drills with their expected detector + sid
|
||||
#
|
||||
# Drills: reverse_shell ld_preload deleted_exe new_listener new_suid persistence
|
||||
# Every current detector has a named drill with an expected signature/sid (see
|
||||
# --list), so this doubles as a shared regression suite: after firing, it runs
|
||||
# `enodia-sentinel check` and reports PASS/MISS for each poll detection.
|
||||
#
|
||||
# Watch it work, in another terminal:
|
||||
# sudo tail -f /var/log/enodia-sentinel/events.log
|
||||
|
|
@ -25,6 +27,24 @@ TMP="$(mktemp -d /tmp/enodia-drill.XXXXXX)"
|
|||
PIDS=()
|
||||
SANDBOX="$TMP/persistence-sandbox" # used by the persistence drill
|
||||
LISTEN_PORT=14444
|
||||
# egress drill target — the ONE drill that makes a real outbound connection.
|
||||
EGRESS_HOST="${EGRESS_HOST:-1.1.1.1}"
|
||||
EGRESS_PORT="${EGRESS_PORT:-53}"
|
||||
|
||||
# Drill metadata: name -> "signature sid kind", where kind is how it's verified:
|
||||
# poll = the one-shot `check` should catch it
|
||||
# event = only the eBPF monitor sees it (vanishes between polls)
|
||||
# cond = conditional (needs extra setup; see the drill's note)
|
||||
declare -A DRILL_SIG=(
|
||||
[reverse_shell]="reverse_shell 100010 poll"
|
||||
[ld_preload]="ld_preload 100011 poll"
|
||||
[deleted_exe]="deleted_exe 100012 poll"
|
||||
[new_listener]="new_listener 100013 poll"
|
||||
[new_suid]="new_suid 100014 poll"
|
||||
[persistence]="persistence 100015 cond"
|
||||
[egress]="egress 100016 poll"
|
||||
[ebpf_exec]="exec_anomaly 100001/100002 event"
|
||||
)
|
||||
|
||||
cleanup() {
|
||||
echo "[*] cleaning up drill artifacts..."
|
||||
|
|
@ -110,6 +130,22 @@ drill_persistence() {
|
|||
echo " -> wrote $SANDBOX/authorized_keys"
|
||||
}
|
||||
|
||||
drill_egress() {
|
||||
note egress "an interpreter (python) holding an outbound TCP connection to a public IP"
|
||||
echo " WARNING: unlike the other drills, this makes ONE real outbound TCP"
|
||||
echo " connection to a public host (${EGRESS_HOST}:${EGRESS_PORT}). No data is sent;"
|
||||
echo " only the handshake. Set EGRESS_HOST/EGRESS_PORT to retarget, or skip it."
|
||||
# python3 is an 'interpreter' to the egress detector; an established socket
|
||||
# to a globally-routable IP is exactly the C2/exfil shape it flags (sid 100016).
|
||||
python3 -c 'import socket,sys,time
|
||||
s=socket.socket()
|
||||
try:
|
||||
s.connect((sys.argv[1], int(sys.argv[2]))); time.sleep(int(sys.argv[3]))
|
||||
except Exception: pass' "$EGRESS_HOST" "$EGRESS_PORT" "$HOLD" >/dev/null 2>&1 &
|
||||
PIDS+=("$!")
|
||||
echo " -> python3 (pid $!) connected to ${EGRESS_HOST}:${EGRESS_PORT} for ${HOLD}s"
|
||||
}
|
||||
|
||||
drill_ebpf_exec() {
|
||||
note ebpf_exec "short-lived /tmp exec + reverse-shell-pattern argv (only the eBPF monitor catches these)"
|
||||
cp /bin/true "$TMP/enodia-drill-exec" 2>/dev/null
|
||||
|
|
@ -130,14 +166,29 @@ run() {
|
|||
new_listener) drill_new_listener ;;
|
||||
new_suid) drill_new_suid ;;
|
||||
persistence) drill_persistence ;;
|
||||
egress) drill_egress ;;
|
||||
ebpf_exec) drill_ebpf_exec ;;
|
||||
*) echo "unknown drill: $1" >&2; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# egress is excluded from the default run because it is the only drill that
|
||||
# emits real network traffic; name it explicitly to include it.
|
||||
ALL=(reverse_shell ld_preload deleted_exe new_listener new_suid persistence ebpf_exec)
|
||||
KNOWN=(reverse_shell ld_preload deleted_exe new_listener new_suid persistence egress ebpf_exec)
|
||||
|
||||
if [ "${1:-}" = "--list" ]; then printf '%s\n' "${ALL[@]}"; exit 0; fi
|
||||
if [ "${1:-}" = "--list" ]; then
|
||||
printf '%-15s %-16s %-7s %s\n' DRILL SIGNATURE SID KIND
|
||||
for d in "${KNOWN[@]}"; do
|
||||
read -r sig sid kind <<<"${DRILL_SIG[$d]}"
|
||||
printf '%-15s %-16s %-7s %s\n' "$d" "$sig" "$sid" "$kind"
|
||||
done
|
||||
echo
|
||||
echo "kinds: poll=caught by 'check'; event=eBPF-only; cond=needs extra setup"
|
||||
echo "note: rootcheck (100022-25) and posture (100040-47) are verified by"
|
||||
echo " 'enodia-sentinel rootcheck' / 'posture check', not attack drills."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "######################################################################"
|
||||
echo "# ENODIA SENTINEL — RED TEAM DRILL #"
|
||||
|
|
@ -148,14 +199,31 @@ 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. Run a one-shot poll check now:"
|
||||
# --- verification: run one poll check and confirm each expected signature ----
|
||||
echo -e "\n[*] drills live for ${HOLD}s. Verifying with a one-shot poll check..."
|
||||
if command -v enodia-sentinel >/dev/null 2>&1; then
|
||||
enodia-sentinel check 2>/dev/null | grep -i 'drill\|/tmp/enodia' \
|
||||
&& echo " ^ Sentinel's poll detectors saw the drills." \
|
||||
|| echo " (no poll hits yet — watch /var/log/enodia-sentinel/events.log)"
|
||||
sleep 1
|
||||
OUT="$(enodia-sentinel check 2>/dev/null)"
|
||||
pass=0; miss=0
|
||||
for d in "${targets[@]}"; do
|
||||
read -r sig sid kind <<<"${DRILL_SIG[$d]}"
|
||||
case "$kind" in
|
||||
poll)
|
||||
if grep -qw "$sig" <<<"$OUT"; then
|
||||
printf ' PASS %-15s %s (sid %s)\n' "$d" "$sig" "$sid"; pass=$((pass+1))
|
||||
else
|
||||
printf ' MISS %-15s expected %s (sid %s)\n' "$d" "$sig" "$sid"; miss=$((miss+1))
|
||||
fi ;;
|
||||
event)
|
||||
printf ' INFO %-15s %s — eBPF-only; see events.log (sid %s)\n' "$d" "$sig" "$sid" ;;
|
||||
cond)
|
||||
printf ' INFO %-15s %s — conditional; see the drill note (sid %s)\n' "$d" "$sig" "$sid" ;;
|
||||
esac
|
||||
done
|
||||
echo " ---- ${pass} passed, ${miss} missed (poll detections) ----"
|
||||
[ "$miss" -gt 0 ] && echo " MISS can be a real regression OR a timing/baseline gap — re-check events.log."
|
||||
else
|
||||
echo " (enodia-sentinel not on PATH — watch /var/log/enodia-sentinel/events.log)"
|
||||
fi
|
||||
echo " The 'ebpf_exec' drill only shows up with the eBPF monitor (service as root)."
|
||||
echo "[*] holding for ${HOLD}s, then cleaning up..."
|
||||
sleep "$HOLD"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue