#!/usr/bin/env bash
#
# virtualizor_security_scan.sh
# ================================================================
# SOFTACULOUS VIRTUALIZOR - Official Compromise Detection & Containment Tool
# ================================================================
#
# Detects and safely contains the known indicators of compromise from the
# 2026-08-29 BGP route hijack incident (tracked internally as the
# "nerat/widdow" campaign), which was used to serve a malicious update
# package to some nodes during the hijack window.
#
# Run this as root on any node you have not yet confirmed clean:
#     bash virtualizor_security_scan.sh
#
# What this does NOT do:
#   - Does not stop, restart, or otherwise touch libvirtd, docker, networking,
#     or any VM/container process. Nothing here can cause VM or host downtime.
#   - Does not delete anything outright. Every artifact is copied into an
#     evidence directory, and the live copy is renamed aside (quarantined),
#     never deleted - fully reversible if anything here is ever in question.
#   - Does not modify anything beyond the exact, named indicators below. No
#     wildcard cleanup, no "looks suspicious" guessing.
#
# A negative result (no IOCs found) does not by itself prove a node was never
# compromised - it means none of the KNOWN indicators below are present.
#
# This same detection logic also ships as an ongoing, scheduled check inside
# the panel itself (Admin -> Security -> Security Analyzer), which runs
# hourly and surfaces findings in the panel UI. This script is the immediate,
# run-it-yourself version for right now, independent of whether you've
# already applied that update.
#
# DO NOT put this script on a cron. Unlike the panel's own hourly check
# (which writes nothing to disk when a node is clean), every run of THIS
# script unconditionally creates a new evidence folder and a new log file,
# clean or not, with no cleanup/rotation - scheduled runs will accumulate
# indefinitely and eventually fill the disk. Run it by hand when you need it.
# ================================================================

set -u
umask 077

TS="$(date -u +%Y%m%dT%H%M%SZ)"
HOST="$(hostname -f 2>/dev/null || hostname)"
EVID="/var/virtualizor/security_analyzer/scan_evidence/${TS}"
LOG="/var/virtualizor/log/security_scan_${TS}.log"

# --- Known indicators (2026-08-29 BGP hijack / "nerat-widdow" campaign) -------
UNIT_NAME='java-jre-update.service'
UNIT_PATH="/etc/systemd/system/${UNIT_NAME}"
RAT_PATH='/usr/lib/jvm/.cache/jre-runtime.dat'
RAT_SHA256='b81a4e1fab9fc4e404d57224fe71e2c143aa93942bd46998789bdc944a7870c7'
MARKER_PATHS=( "/usr/lib/jvm/.cache/.installed" "/tmp/widdow.jar" )
MAL_SSH_KEY='AAAAC3NzaC1lZDI1NTE5AAAAIP13pPAm5jmInLQYD3XNb3HwrW4cAKDcphoT4kSKrnte'
CORE_FILES_WATCHED=( "/usr/local/virtualizor/globals.php" "/usr/local/virtualizor/_universal.php" "/usr/local/virtualizor/zzvirtservice" )
INJECTED_STRINGS=( "cdn.nerat.cc/installer/widdow.jar" "connect.ne-rat.xyz" "jre-runtime.dat" )
PROCESS_PATTERN='\b(jre-runtime\.dat|widdow|connect\.ne-rat\.xyz|nerat)\b'
C2_DOMAINS=( "cdn.nerat.cc" "connect.ne-rat.xyz" )
# -------------------------------------------------------------------------------

mkdir -p "$(dirname "$LOG")"
exec > >(tee -a "$LOG") 2>&1

echo "================================================================"
echo " VIRTUALIZOR SECURITY SCAN - known-compromise detection"
echo "================================================================"
echo "UTC:       $(date -u --iso-8601=seconds)"
echo "Host:      $HOST"
echo "Log:       $LOG"
echo "Evidence:  $EVID"
echo

if [ "$(id -u)" -ne 0 ]; then
	echo "ERROR: this script must be run as root."
	exit 1
fi

mkdir -p "$EVID"

ioc_found=0
actions=0
errors=0

note_ioc() { ioc_found=1; echo "[IOC]    $*"; }
action()   { actions=$((actions+1)); echo "[ACTION] $*"; }
fail()     { errors=$((errors+1)); echo "[ERROR]  $*"; }

# --- 1. Preserve evidence before touching anything ------------------------
echo "--- 1. Capturing evidence ---"
{
	uname -a
	date -u --iso-8601=seconds
	systemctl status "$UNIT_NAME" --no-pager 2>&1 || true
	systemctl cat "$UNIT_NAME" 2>&1 || true
} > "$EVID/system-state.txt" 2>&1

for f in "$UNIT_PATH" "$RAT_PATH" "${MARKER_PATHS[@]}" "${CORE_FILES_WATCHED[@]}"; do
	[ -e "$f" ] && { cp -a --parents "$f" "$EVID/" 2>/dev/null || fail "Could not preserve $f"; }
done
for f in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do
	[ -f "$f" ] && cp -a --parents "$f" "$EVID/" 2>/dev/null
done

# --- 2. Rogue systemd unit: stop, disable, quarantine (never delete) ------
echo
echo "--- 2. Checking for the known rogue systemd unit ---"
if [ -f "$UNIT_PATH" ]; then
	note_ioc "$UNIT_NAME is present"

	systemctl stop "$UNIT_NAME" 2>/dev/null || true
	systemctl disable "$UNIT_NAME" 2>/dev/null || true

	# Quarantine whatever the unit's WorkingDirectory pointed at too, before
	# touching the unit file itself.
	work_dir="$(systemctl show -p WorkingDirectory --value "$UNIT_NAME" 2>/dev/null)"
	if [ -n "$work_dir" ] && [ "$work_dir" != "/" ] && [ -d "$work_dir" ]; then
		mv "$work_dir" "$EVID/$(basename "$work_dir")_payload" \
			&& action "Quarantined referenced directory: $work_dir" \
			|| fail "Could not quarantine $work_dir"
	fi

	mv "$UNIT_PATH" "${UNIT_PATH}.quarantined" \
		&& action "Renamed $UNIT_PATH -> ${UNIT_PATH}.quarantined (not deleted)" \
		|| fail "Could not rename $UNIT_PATH"

	systemctl daemon-reload 2>/dev/null || true
	action "Ran systemctl daemon-reload (does not restart other units)"
else
	echo "[OK] $UNIT_NAME not present."
fi

# --- 3. Known RAT payload / installer marker: quarantine, verify hash -----
echo
echo "--- 3. Checking for the known payload and installer marker ---"
if [ -f "$RAT_PATH" ]; then
	note_ioc "Payload found at $RAT_PATH"
	actual_hash="$(sha256sum "$RAT_PATH" | awk '{print $1}')"
	if [ "$actual_hash" = "$RAT_SHA256" ]; then
		echo "[MATCH] SHA256 matches the known sample exactly."
	else
		echo "[WARN]  A file is present at this known path but its hash differs ($actual_hash) - preserved and quarantined anyway."
	fi
	mv "$RAT_PATH" "$EVID/$(basename "$RAT_PATH").quarantined" \
		&& action "Quarantined $RAT_PATH" \
		|| fail "Could not quarantine $RAT_PATH"
else
	echo "[OK] No file at the known payload path."
fi

marker_found=0
for m in "${MARKER_PATHS[@]}"; do
	[ -f "$m" ] || continue
	marker_found=1
	note_ioc "Known marker/staged payload found at $m"
	mv "$m" "$EVID/$(basename "$m").quarantined" \
		&& action "Quarantined $m" \
		|| fail "Could not quarantine $m"
done
[ "$marker_found" -eq 0 ] && echo "[OK] No known marker/staged payload files present."

# --- 4. Known backdoor SSH key: remove from authorized_keys ---------------
echo
echo "--- 4. Checking authorized_keys for the known backdoor key ---"
key_removed=0
for f in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do
	[ -f "$f" ] || continue
	if grep -qF "$MAL_SSH_KEY" "$f"; then
		note_ioc "Known attacker SSH key found in $f"
		cp -a "$f" "$EVID/$(basename "$f").before" 2>/dev/null
		sed -i "\\|$MAL_SSH_KEY|d" "$f" && chmod 600 "$f" \
			&& { action "Removed known attacker key from $f"; key_removed=1; } \
			|| fail "Could not remove key from $f"
	fi
done
[ "$key_removed" -eq 0 ] && echo "[OK] Known attacker SSH key not present."

# --- 5. Known malicious process pattern: terminate -------------------------
# Excludes the scanner's own pgrep/pkill invocations from the match list - on some shells the
# subprocess running this exact pgrep command is itself a live process whose command line
# contains the pattern text, which pgrep would otherwise match against itself.
echo
echo "--- 5. Checking for known malicious process patterns ---"
proc_matches="$(pgrep -af "$PROCESS_PATTERN" 2>/dev/null | grep -v -e 'pgrep -af' -e 'pkill -' || true)"
if [ -n "$proc_matches" ]; then
	note_ioc "A known malicious process pattern is running"
	printf '%s\n' "$proc_matches" > "$EVID/matched_processes.txt" 2>/dev/null
	pkill -TERM -f "$PROCESS_PATTERN" 2>/dev/null || true
	sleep 2
	pkill -KILL -f "$PROCESS_PATTERN" 2>/dev/null || true
	action "Terminated processes matching the known pattern"
else
	echo "[OK] No known malicious process pattern running."
fi

# --- 6. Injected code inside core Virtualizor files: DETECT ONLY -----------
# Deliberately not auto-fixed - the panel needs these files to run, and a
# safe repair means restoring known-good content (via a verified update),
# not blindly stripping lines. Flag loudly for manual review instead.
echo
echo "--- 6. Checking core Virtualizor files for injected content ---"
core_tamper=0
for f in "${CORE_FILES_WATCHED[@]}"; do
	[ -f "$f" ] || continue
	for s in "${INJECTED_STRINGS[@]}"; do
		if grep -qF "$s" "$f"; then
			note_ioc "Suspicious content matching '$s' found in $f"
			core_tamper=1
		fi
	done
done
if [ "$core_tamper" -eq 1 ]; then
	echo "[WARN] Core file tampering detected. NOT auto-modified - restore these files"
	echo "       from a known-good copy or by reinstalling Virtualizor, then re-run this scan."
else
	echo "[OK] No known injected strings found in watched core files."
fi

# --- 6b. Sinkhole the known C2 domain(s) via /etc/hosts --------------------
# Runs every scan regardless of whether anything else was found above - there is no legitimate
# reason any node would ever need to resolve these domains, so blocking resolution is safe,
# reversible (just remove the line), and does not touch any process, VM, or service. This is
# defense-in-depth for a persistence mechanism we have not identified yet: even an artifact none
# of the checks above catch still can't reach its C2 if the hostname resolves to localhost.
# Idempotent - only ever appends a missing line, never modifies or removes anything else in the file.
echo
echo "--- 6b. Ensuring known C2 domain(s) are sinkholed via /etc/hosts ---"
sinkhole_added=0
for d in "${C2_DOMAINS[@]}"; do
	if grep -qE "^127\.0\.0\.1[[:space:]]+${d//./\\.}([[:space:]]|\$)" /etc/hosts 2>/dev/null; then
		echo "[OK] $d already sinkholed in /etc/hosts."
		continue
	fi
	cp -a /etc/hosts "$EVID/hosts.before" 2>/dev/null
	echo "127.0.0.1	${d}	# blocked by Virtualizor Security Analyzer - known nerat/widdow C2 domain" >> /etc/hosts \
		&& { note_ioc "Sinkholed C2 domain: $d"; action "Added /etc/hosts entry redirecting $d to 127.0.0.1"; sinkhole_added=1; } \
		|| fail "Could not add /etc/hosts entry for $d"
done

# --- 7. Post-scan verification ---------------------------------------------
echo
echo "--- 7. Post-scan verification ---"
if [ -f /usr/local/virtualizor/globals.php ] && [ -x /usr/local/emps/bin/php ]; then
	/usr/local/emps/bin/php -l /usr/local/virtualizor/globals.php >/dev/null 2>&1 \
		&& echo "[OK] globals.php syntax valid." \
		|| fail "globals.php syntax check failed - needs manual review."
fi

# Excludes this script itself and the Security Analyzer feature's own signature/planning files,
# which legitimately reference these strings as detection data, not as evidence of compromise.
post_hits="$(grep -RFl \
	-e "cdn.nerat.cc" -e "connect.ne-rat.xyz" -e "widdow.jar" -e "jre-runtime.dat" -e "java-jre-update" -e "$MAL_SSH_KEY" \
	--exclude="virtualizor_security_scan.sh" \
	--exclude="security_analyzer.md" \
	--exclude-dir="security_analyzer" \
	--exclude-dir=".claude" \
	/usr/local/virtualizor /etc/systemd/system /etc/cron.d /etc/cron.daily /etc/cron.hourly \
	/etc/cron.weekly /etc/cron.monthly /var/spool/cron /root/.ssh /home 2>/dev/null || true)"

if [ -n "$post_hits" ]; then
	echo "[WARN] Known indicator strings still found outside the evidence directory:"
	echo "$post_hits"
else
	echo "[OK] No known indicator strings remain in live paths checked."
fi

if [ -n "$(pgrep -af "$PROCESS_PATTERN" 2>/dev/null | grep -v -e 'pgrep -af' -e 'pkill -' || true)" ]; then
	echo "[WARN] A known malicious process pattern is STILL running after containment."
else
	echo "[OK] No known malicious process pattern running."
fi

# --- Result ------------------------------------------------------------------
echo
echo "================================================================"
echo " RESULT"
echo "================================================================"
echo "Known indicators found this run: $ioc_found"
echo "Containment actions performed:   $actions"
echo "Errors:                          $errors"
echo "Evidence preserved at:           $EVID"
echo "Full log:                        $LOG"
echo

if [ "$errors" -ne 0 ] || [ -n "$post_hits" ] || [ "$core_tamper" -eq 1 ]; then
	echo "STATUS: NEEDS MANUAL REVIEW"
	echo "Some indicators could not be fully resolved automatically - see the warnings above."
	exit 2
elif [ "$ioc_found" -eq 1 ]; then
	echo "STATUS: KNOWN INDICATORS CONTAINED"
	echo "This node showed signs of the known Aug 29 2026 compromise and has been contained."
	echo "IMPORTANT: rotate root/API credentials on this node as a precaution - containment"
	echo "does not by itself prove nothing else was touched during the compromise window."
	exit 0
else
	echo "STATUS: NO KNOWN INDICATORS DETECTED"
	echo "IMPORTANT: this checks for KNOWN indicators only. It does not prove this node was"
	echo "never compromised - it means none of the currently-known signatures are present."
	exit 0
fi
