Initial release: ghost_protocol privacy toolkit

This commit is contained in:
ghost
2026-03-04 09:13:16 -05:00
commit 973cede31f
99 changed files with 12158 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# /usr/local/bin/opsec-banner-cache.sh — Background WAN IP Cache Updater
# Updates /var/cache/opsec/wan_ip every 5 minutes (via cron)
# Never blocks shell startup — the banner reads from cache only
CACHE_DIR="/var/cache/opsec"
CACHE_FILE="${CACHE_DIR}/wan_ip"
ENDPOINTS="https://icanhazip.com https://ifconfig.me https://api.ipify.org"
mkdir -p "$CACHE_DIR"
chmod 755 "$CACHE_DIR"
PUB_IP=""
# Try via Tor first if running
if pgrep -x tor >/dev/null 2>&1 && ss -tln 2>/dev/null | grep -q ':9050 '; then
for ep in $ENDPOINTS; do
PUB_IP=$(curl -s --max-time 10 --socks5-hostname 127.0.0.1:9050 "$ep" 2>/dev/null | tr -d '[:space:]')
[ -n "$PUB_IP" ] && break
done
fi
# Fallback to direct if Tor failed or not running
if [ -z "$PUB_IP" ]; then
for ep in $ENDPOINTS; do
PUB_IP=$(curl -s --max-time 5 "$ep" 2>/dev/null | tr -d '[:space:]')
[ -n "$PUB_IP" ] && break
done
fi
# Only write if we got a valid-looking IP
if echo "$PUB_IP" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "$PUB_IP" > "$CACHE_FILE"
chmod 644 "$CACHE_FILE"
fi
+320
View File
@@ -0,0 +1,320 @@
#!/bin/bash
# /usr/local/bin/opsec-banner.sh — Terminal OPSEC Status Banner
# Mirrors the Conky desktop widget layout and Widget theme colors
# Modes: compact | full | off (configured via OPSEC_BANNER in opsec.conf)
OPSEC_CONF="/etc/opsec/opsec.conf"
[ -f "$OPSEC_CONF" ] && . "$OPSEC_CONF"
BANNER_MODE="${OPSEC_BANNER:-compact}"
[ -n "$1" ] && BANNER_MODE="$1"
[ "$BANNER_MODE" = "off" ] && exit 0
# ─── Widget theme colors (matching Conky widget) ────────────────────────────
R=$'\e[0m'
BLD=$'\e[1m'
C0=$'\e[38;5;135m' # ALERT/BAD — #B55AFC purple
C1=$'\e[38;5;204m' # SECURE/GOOD — #FF63BE hot pink
C2=$'\e[38;5;117m' # INFO/NEUTRAL — #85E7FF cyan
C4=$'\e[38;5;32m' # STRUCTURAL — #268BD2 blue
C5=$'\e[38;5;45m' # TITLE/ACCENT — #07CAF9 bright cyan
C6=$'\e[38;5;117m' # LABELS — #85E7FF cyan
C7=$'\e[38;5;189m' # VALUES — #ECDEF7 lavender
C8=$'\e[38;5;135m' # SECTION HDR — #B55AFC purple
C9=$'\e[38;5;66m' # METADATA — #4A6A7A dark grey
# ─── DETECT STATE ────────────────────────────────────────────────────────────
ADVANCED=false
[ -f /var/run/opsec-advanced.enabled ] && ADVANCED=true
BREAKGLASS=false
if [ -f /var/run/opsec-breakglass.active ]; then
BREAKGLASS=true
_bge=$(cat /var/run/opsec-breakglass.active 2>/dev/null)
_bgn=$(date +%s)
BG_REM=""
[ -n "$_bge" ] && [ "$_bge" -gt "$_bgn" ] 2>/dev/null && BG_REM="$(( (_bge - _bgn) / 60 ))m"
fi
# ─── HELPERS ─────────────────────────────────────────────────────────────────
HR=" ${C4}$(printf '─%.0s' {1..48})${R}"
# ─── READ CACHE ──────────────────────────────────────────────────────────────
CACHE_FILE="/tmp/.opsec-cache/netinfo"
PUB_IP="" PUB_GEO="" ROUTED_TOR=false EXIT_COUNTRY="" EXIT_CHANGE_TIME="" TOR_BOOTSTRAP="" TOR_PHASE=""
if [ -f "$CACHE_FILE" ]; then
_cr() { grep "^${1}=" "$CACHE_FILE" 2>/dev/null | head -1 | cut -d'"' -f2; }
PUB_IP=$(_cr PUB_IP)
PUB_GEO=$(_cr PUB_GEO)
ROUTED_TOR=$(_cr ROUTED_TOR)
EXIT_COUNTRY=$(_cr EXIT_COUNTRY)
EXIT_CHANGE_TIME=$(_cr EXIT_CHANGE_TIME)
TOR_BOOTSTRAP=$(_cr TOR_BOOTSTRAP)
TOR_PHASE=$(_cr TOR_PHASE)
PUB_IP=$(echo "$PUB_IP" | tr -cd '0-9.')
EXIT_COUNTRY=$(echo "$EXIT_COUNTRY" | tr -cd 'A-Za-z')
EXIT_CHANGE_TIME=$(echo "$EXIT_CHANGE_TIME" | tr -cd '0-9')
TOR_BOOTSTRAP=$(echo "$TOR_BOOTSTRAP" | tr -cd '0-9')
fi
# ─── GATHER LIVE STATUS ─────────────────────────────────────────────────────
LOCAL_IP=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1)
[ -z "$LOCAL_IP" ] && LOCAL_IP="No route"
VPN_IF=$(ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | grep -E '^(tun|wg)' | head -1)
VPN_OK=false; VPN_IP=""
if [ -n "$VPN_IF" ]; then
VPN_OK=true
VPN_IP=$(ip -4 addr show "$VPN_IF" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1)
fi
PRIMARY_IF=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1)
MAC_OK=false; CUR_MAC=""
if [ -n "$PRIMARY_IF" ]; then
CUR_MAC=$(ip link show "$PRIMARY_IF" 2>/dev/null | awk '/ether/{print $2}')
_oct=$((16#$(echo "$CUR_MAC" | cut -d: -f1))) 2>/dev/null
(( _oct & 2 )) 2>/dev/null && MAC_OK=true
fi
# ═════════════════════════════════════════════════════════════════════════════
# COMPACT BANNER
# ═════════════════════════════════════════════════════════════════════════════
render_compact() {
_si() {
local ok=$1 lbl="$2"
if $ok; then
echo -n "${C6}${lbl}${C1}${BLD}+${R}"
else
echo -n "${C6}${lbl}${C0}${BLD}x${R}"
fi
}
local _tor_ok=false; ss -tln 2>/dev/null | grep -q ':9050 ' && _tor_ok=true
local _ks_ok=false
[ -f /var/run/opsec-killswitch.enabled ] && _ks_ok=true
iptables -L GP_FW >/dev/null 2>&1 && _ks_ok=true
local _dns_ok=false
local _ds=$(awk '/^nameserver/{print $2;exit}' /etc/resolv.conf 2>/dev/null)
case "$_ds" in 127.0.0.1|9.9.9.9|149.112.*|1.1.1.1|1.0.0.1) _dns_ok=true ;; esac
local _v6_ok=false
[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" = "1" ] && _v6_ok=true
echo ""
if $BREAKGLASS; then
echo " ${C0}${BLD}⚠ BREAKGLASS${R} ${C0}KS bypassed${BG_REM:+ (${BG_REM})}${R}"
fi
if $ADVANCED; then
local sigil="${C1}${BLD}▲ ADVANCED${R}"
local ind="$(_si $_tor_ok TOR) $(_si $_ks_ok KS) $(_si $_dns_ok DNS) $(_si $MAC_OK MAC) $(_si $_v6_ok v6)"
$VPN_OK && ind="$ind $(_si $VPN_OK VPN)"
echo " ${sigil} ${ind} ${C9}${LOCAL_IP}${R}"
else
local sigil="${C2}── STANDARD${R}"
local ind="$(_si $_dns_ok DNS) $(_si $MAC_OK MAC)"
$VPN_OK && ind="$ind $(_si $VPN_OK VPN)"
echo " ${sigil} ${ind} ${C9}${LOCAL_IP}${R}"
fi
echo ""
}
# ═════════════════════════════════════════════════════════════════════════════
# FULL BANNER — mirrors Conky widget
# ═════════════════════════════════════════════════════════════════════════════
render_full() {
echo ""
# ── HEADER ──
echo "$HR"
if $ADVANCED; then
echo " ${C5}${BLD} OPSEC STATUS${R}"
echo " ${C1}${BLD} ▲ ADVANCED ▲${R}"
else
echo " ${C5}${BLD} OPSEC STATUS${R}"
echo " ${C2} ── STANDARD ──${R}"
fi
echo "$HR"
# Breakglass warning
if $BREAKGLASS; then
echo " ${C0}${BLD} ⚠ BREAKGLASS — Kill switch bypassed ${BG_REM:+(${BG_REM})}${R}"
echo "$HR"
fi
# ── SYSTEM ──
echo " ${C8}${BLD} ▌SYSTEM${R}"
echo " ${C6} HOST ${C7}$(hostname)${R}$(printf '%*s' 1 '')${C6}UP ${C7}$(uptime -p 2>/dev/null | sed 's/^up //' | sed 's/ hours\?/h/;s/ minutes\?/m/;s/ days\?/d/;s/, */ /g' || uptime | awk -F'up ' '{print $2}' | awk -F, '{print $1}')${R}"
local _cpu _mem _memtot _memp
_cpu=$(awk '/^cpu /{u=$2+$4; t=$2+$4+$5; if(t>0) printf "%.0f", u*100/t}' /proc/stat 2>/dev/null)
_mem=$(free -h 2>/dev/null | awk '/^Mem:/{print $3}')
_memtot=$(free -h 2>/dev/null | awk '/^Mem:/{print $2}')
_memp=$(free 2>/dev/null | awk '/^Mem:/{if($2>0) printf "%.0f", $3*100/$2}')
echo " ${C6} CPU ${C7}${_cpu:-0}%${R}$(printf '%*s' 1 '')${C6}RAM ${C7}${_memp:-0}% ${C9}(${_mem:-?}/${_memtot:-?})${R}"
# ── NETWORK ──
echo "$HR"
echo " ${C8}${BLD} ▌NETWORK${R}"
echo " ${C6} LOCAL IP ${C7}${LOCAL_IP}${R}"
# External IP
if [ -n "$PUB_IP" ]; then
if [ -n "$PUB_GEO" ]; then
echo " ${C6} EXT IP ${C7}${PUB_IP} ${C9}(${PUB_GEO})${R}"
else
echo " ${C6} EXT IP ${C7}${PUB_IP}${R}"
fi
else
echo " ${C6} EXT IP ${C0}UNAVAILABLE${R}"
fi
# VPN
if $VPN_OK; then
echo " ${C6} VPN ${C1}ACTIVE ${C7}${VPN_IP} ${C9}(${VPN_IF})${R}"
fi
# MAC
if [ -n "$CUR_MAC" ]; then
if $MAC_OK; then
echo " ${C6} MAC ${C1}RANDOM ${C9}(${CUR_MAC})${R}"
else
echo " ${C6} MAC ${C0}HWADDR ${C9}(${CUR_MAC})${R}"
fi
else
echo " ${C6} MAC ${C9}NO IFACE${R}"
fi
# ── HARDENING (advanced only) ──
if $ADVANCED; then
echo "$HR"
echo " ${C8}${BLD} ▌HARDENING${R}"
local _v6=$(sysctl -n net.ipv6.conf.all.disable_ipv6 2>/dev/null || echo "0")
[ "$_v6" = "1" ] && echo " ${C6} IPv6 ${C1}BLOCKED${R}" || echo " ${C6} IPv6 ${C0}LEAKING${R}"
if lsattr /etc/resolv.conf 2>/dev/null | grep -q 'i'; then
echo " ${C6} DNSLK ${C1}LOCKED ${C9}(immutable)${R}"
else
echo " ${C6} DNSLK ${C0}UNLOCKED${R}"
fi
local _iso=$(grep "IsolateDestAddr" /etc/tor/torrc 2>/dev/null | head -1)
[ -n "$_iso" ] && echo " ${C6} ISOL ${C1}ACTIVE ${C9}(stream isolation)${R}"
local _pad=$(grep "ConnectionPadding" /etc/tor/torrc 2>/dev/null | awk '{print $2}')
[ "$_pad" = "1" ] && echo " ${C6} TPAD ${C1}ACTIVE ${C9}(traffic padding)${R}"
local _bl=$(grep "ExcludeExitNodes" /etc/tor/torrc 2>/dev/null | sed 's/ExcludeExitNodes //' | tr -d '{}' | tr ',' ' ')
[ -n "$_bl" ] && echo " ${C6} BLOCK ${C0}$(echo "$_bl" | tr ' ' ',' | sed 's/,$//')${R}"
local _core=$(sysctl -n kernel.core_pattern 2>/dev/null)
[[ "$_core" == *"/bin/false"* ]] && echo " ${C6} CORE ${C1}BLOCKED${R}" || echo " ${C6} CORE ${C0}ENABLED${R}"
local _swap=$(swapon --show=SIZE --noheadings 2>/dev/null | head -1)
[ -z "$_swap" ] && echo " ${C6} SWAP ${C1}OFF${R}" || echo " ${C6} SWAP ${C0}ACTIVE ${C9}(${_swap})${R}"
local _bootc=0
for _svc in opsec-boot-advanced opsec-mac-randomize opsec-hostname-randomize opsec-killswitch; do
systemctl is-enabled "$_svc" 2>/dev/null | grep -q enabled && _bootc=$((_bootc + 1))
done
if [ "$_bootc" -eq 4 ]; then
echo " ${C6} BOOT ${C1}PERSIST ${C9}(${_bootc}/4)${R}"
elif [ "$_bootc" -gt 0 ]; then
echo " ${C6} BOOT ${C2}PARTIAL ${C9}(${_bootc}/4)${R}"
else
echo " ${C6} BOOT ${C0}NONE ${C9}(${_bootc}/4)${R}"
fi
local _prof="${PROFILE_NAME:-}"
[ -n "$_prof" ] && echo " ${C6} PROF ${C2}${_prof}${R}"
fi
# ── TOR STATUS (advanced only) ──
if $ADVANCED; then
echo "$HR"
echo " ${C8}${BLD} ▌TOR STATUS${R}"
local _tor_svc=false _tor_socks=false _ks_armed=false _tor_routed=false
systemctl is-active tor >/dev/null 2>&1 && _tor_svc=true
ss -tln 2>/dev/null | grep -q ':9050 ' && _tor_socks=true
$ADVANCED && _ks_armed=true
[ "$ROUTED_TOR" = "true" ] && _tor_routed=true
if $_tor_svc && $_tor_socks && $_ks_armed && $_tor_routed; then
echo " ${C6} STATE ${C1}PROTECTED${R}"
elif $_tor_svc && ! $_tor_socks; then
echo " ${C6} STATE ${C2}BOOTSTRAP ${C9}(${TOR_BOOTSTRAP:-0}% ${TOR_PHASE:-connecting})${R}"
elif ! $_tor_svc; then
echo " ${C6} STATE ${C0}EXPOSED ${C9}(tor down)${R}"
elif ! $_ks_armed; then
echo " ${C6} STATE ${C0}EXPOSED ${C9}(kill switch off)${R}"
else
echo " ${C6} STATE ${C0}EXPOSED ${C9}(not routed)${R}"
fi
if [ -n "$EXIT_COUNTRY" ] && [ ${#EXIT_COUNTRY} -le 3 ]; then
echo " ${C6} EXIT ${C2}${EXIT_COUNTRY}${R}"
elif $_tor_socks; then
echo " ${C6} EXIT ${C9}resolving...${R}"
else
echo " ${C6} EXIT ${C9}${R}"
fi
if [ -n "$EXIT_CHANGE_TIME" ] && [ "$EXIT_CHANGE_TIME" -gt 0 ] 2>/dev/null; then
local _now=$(date +%s)
local _age=$(( _now - EXIT_CHANGE_TIME ))
if [ "$_age" -lt 60 ]; then
echo " ${C6} CIRCUIT ${C2}${_age}s ago${R}"
elif [ "$_age" -lt 3600 ]; then
echo " ${C6} CIRCUIT ${C2}$(( _age / 60 ))m ago${R}"
else
echo " ${C6} CIRCUIT ${C2}$(( _age / 3600 ))h $(( (_age % 3600) / 60 ))m ago${R}"
fi
else
echo " ${C6} CIRCUIT ${C9}${R}"
fi
local _dns_server=$(awk '/^nameserver/{print $2;exit}' /etc/resolv.conf 2>/dev/null)
local _dns_immutable=false
lsattr /etc/resolv.conf 2>/dev/null | grep -q 'i' && _dns_immutable=true
local _nm_locked=false
[ -f /etc/NetworkManager/conf.d/opsec-dns-lock.conf ] && _nm_locked=true
if [ "$_dns_server" = "127.0.0.1" ] && $_dns_immutable && $_nm_locked; then
echo " ${C6} DNS ${C1}SECURE ${C9}(tor + locked)${R}"
elif [ "$_dns_server" = "127.0.0.1" ] && $_dns_immutable; then
echo " ${C6} DNS ${C2}SECURE ${C9}(tor, NM unlocked)${R}"
elif [ "$_dns_server" = "127.0.0.1" ]; then
echo " ${C6} DNS ${C2}PARTIAL ${C9}(tor, not locked)${R}"
elif echo "$_dns_server" | grep -qE '^(9\.9\.9\.9|149\.112|1\.1\.1\.1|1\.0\.0\.1)'; then
echo " ${C6} DNS ${C2}PRIVACY ${C9}(${_dns_server})${R}"
else
echo " ${C6} DNS ${C0}LEAKED ${C9}(${_dns_server})${R}"
fi
fi
# ── MODE / LEVEL ──
echo "$HR"
if $ADVANCED; then
echo " ${C6} MODE ${C1}ADVANCED${R}"
else
echo " ${C6} MODE ${C0}STANDARD${R}"
fi
local _lvl="${DEPLOYMENT_LEVEL:-}"
[ -n "$_lvl" ] && echo " ${C6} LEVEL ${C7}${_lvl}${R}"
echo "$HR"
echo " ${C9} // $(date +%H:%M:%S) //${R}"
echo ""
}
# ═════════════════════════════════════════════════════════════════════════════
# DISPATCH
# ═════════════════════════════════════════════════════════════════════════════
case "$BANNER_MODE" in
compact) render_compact ;;
full) render_full ;;
esac
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
# /usr/local/bin/opsec-boot-init.sh — Level-aware boot initializer
# Called by opsec-boot-advanced.service at boot
# Standard levels: apply base privacy hardening only
# Paranoid levels: activate full ghost mode
set -euo pipefail
BOOT_MARKER="/etc/opsec/boot-advanced.enabled"
STATE_FILE="/var/run/opsec-advanced.enabled"
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
OPSEC_CONF="/etc/opsec/opsec.conf"
# Source shared library if available
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
opsec_load_config 2>/dev/null || true
fi
# Load config directly if lib not available
if [ -f "$OPSEC_CONF" ]; then
. "$OPSEC_CONF"
fi
LEVEL_TYPE="${LEVEL_TYPE:-standard}"
DEPLOYMENT_LEVEL="${DEPLOYMENT_LEVEL:-bare-metal-standard}"
echo "[opsec-boot] Level: ${DEPLOYMENT_LEVEL} (type: ${LEVEL_TYPE})"
# Paranoid levels: always activate full ghost mode
if [ "$LEVEL_TYPE" = "paranoid" ]; then
echo "[opsec-boot] Paranoid level detected — activating full ghost mode"
touch "$STATE_FILE"
if [ -x /usr/local/bin/opsec-mode.sh ]; then
/usr/local/bin/opsec-mode.sh on
else
echo "[opsec-boot] ERROR: opsec-mode.sh not found"
exit 1
fi
echo "[opsec-boot] Ghost mode activated at boot (paranoid level)"
exit 0
fi
# Standard levels: check boot marker, apply base or full accordingly
if [ -f "$BOOT_MARKER" ]; then
echo "[opsec-boot] Boot marker detected — activating ghost mode"
touch "$STATE_FILE"
if [ -x /usr/local/bin/opsec-mode.sh ]; then
/usr/local/bin/opsec-mode.sh on
else
echo "[opsec-boot] ERROR: opsec-mode.sh not found"
exit 1
fi
echo "[opsec-boot] Ghost mode activated at boot (boot marker)"
else
echo "[opsec-boot] Standard level, no boot marker — applying base privacy hardening"
# Apply base hardening inline (cannot source opsec-mode.sh — its exit kills the caller)
echo "[opsec-boot] Applying inline base hardening..."
sysctl -w net.ipv6.conf.all.disable_ipv6=1 >/dev/null 2>&1 || true
sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null 2>&1 || true
swapoff -a 2>/dev/null || true
sysctl -w kernel.core_pattern='|/bin/false' >/dev/null 2>&1 || true
# Set privacy DNS
base_dns="${BASE_DNS:-quad9}"
chattr -i /etc/resolv.conf 2>/dev/null || true
case "$base_dns" in
quad9)
printf "nameserver 9.9.9.9\nnameserver 149.112.112.112\n" > /etc/resolv.conf
;;
cloudflare)
printf "nameserver 1.1.1.1\nnameserver 1.0.0.1\n" > /etc/resolv.conf
;;
esac
echo "[opsec-boot] Base privacy hardening applied"
fi
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
# OPSEC Status Check
echo "===== OPSEC Status Check ====="
echo ""
# Check for running VPN
echo "[*] VPN Status:"
if pgrep -x openvpn > /dev/null; then
echo " [+] OpenVPN is running"
else
echo " [-] OpenVPN is NOT running"
fi
# Check for Tor
echo ""
echo "[*] Tor Status:"
if systemctl is-active tor >/dev/null 2>&1; then
echo " [+] Tor is running"
else
echo " [-] Tor is NOT running"
fi
# Check current connections
echo ""
echo "[*] Active Connections:"
CONNECTIONS=$(ss -tupn | grep ESTAB | wc -l)
echo " [*] $CONNECTIONS established connections"
# Check listening services
echo ""
echo "[*] Listening Services:"
LISTENERS=$(ss -tupln | grep LISTEN | wc -l)
echo " [*] $LISTENERS listening services"
# Check DNS
echo ""
echo "[*] DNS Configuration:"
grep "nameserver" /etc/resolv.conf | head -3
# Check for history files
echo ""
echo "[*] History Files:"
for hist in ~/.bash_history ~/.zsh_history ~/.python_history ~/.mysql_history; do
if [ -f "$hist" ]; then
SIZE=$(stat -c%s "$hist" 2>/dev/null)
echo " [!] $hist exists (size: $SIZE bytes)"
fi
done
# Check MAC address
echo ""
echo "[*] Network Interfaces:"
for iface in $(ip -o link show | awk -F': ' '{print $2}' | grep -v lo); do
MAC=$(ip link show $iface | awk '/ether/ {print $2}')
echo " [*] $iface: $MAC"
done
# Check timezone
echo ""
echo "[*] System Timezone:"
echo " [*] $(timedatectl | grep "Time zone" | awk '{print $3}')"
# Check for running security tools
echo ""
echo "[*] Running Security Tools:"
# Tool inventory is configurable via OPSEC_TOOL_CHECK in opsec.conf
OPSEC_TOOL_CHECK="${OPSEC_TOOL_CHECK:-curl wget openssl gpg tor}"
for tool in $OPSEC_TOOL_CHECK; do
if pgrep -f $tool > /dev/null; then
echo " [!] $tool is running"
fi
done
echo ""
echo "===== Check Complete ====="
+1044
View File
File diff suppressed because it is too large Load Diff
+377
View File
@@ -0,0 +1,377 @@
#!/bin/bash
# /usr/local/bin/opsec-harden.sh — System hardening apply/revert
# Usage: sudo opsec-harden.sh apply|revert
#
# Reads settings from /etc/opsec/opsec.conf
# Handles: core dumps, swap, timezone, locale, screen lock, WebRTC,
# USB blocking, clipboard auto-clear, traffic jitter
## NOTE: Do NOT use "set -euo pipefail" — it causes silent crashes
## when commands like sysctl, dconf, tc, etc. return non-zero.
## Use explicit error handling instead (|| true).
if [ "$EUID" -ne 0 ]; then
echo "[-] Please run as root"
exit 1
fi
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
BACKUP_DIR="/etc/opsec/.harden-backup"
# Source shared library
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
else
# Minimal fallback
opsec_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
opsec_red() { echo -e "\033[38;5;196m[-] $*\033[0m"; }
opsec_yellow() { echo -e "\033[38;5;214m[*] $*\033[0m"; }
opsec_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
opsec_load_config() {
[ -f /etc/opsec/opsec.conf ] && . /etc/opsec/opsec.conf
}
fi
mkdir -p "$BACKUP_DIR"
opsec_load_config 2>/dev/null || true
# ─── CORE DUMPS ────────────────────────────────────────────────────────────────
apply_core_dumps() {
if [ "${HARDEN_CORE_DUMPS:-1}" = "1" ]; then
opsec_info "Disabling core dumps..."
# sysctl
sysctl -w kernel.core_pattern='|/bin/false' >/dev/null 2>&1
sysctl -w fs.suid_dumpable=0 >/dev/null 2>&1
# limits.d
cat > /etc/security/limits.d/opsec-coredump.conf << 'EOF'
# OPSEC: Disable core dumps
* hard core 0
* soft core 0
EOF
# systemd coredump
mkdir -p /etc/systemd/coredump.conf.d
cat > /etc/systemd/coredump.conf.d/opsec.conf << 'EOF'
[Coredump]
Storage=none
ProcessSizeMax=0
EOF
opsec_green "Core dumps disabled"
fi
}
revert_core_dumps() {
sysctl -w kernel.core_pattern='core' >/dev/null 2>&1
sysctl -w fs.suid_dumpable=1 >/dev/null 2>&1
rm -f /etc/security/limits.d/opsec-coredump.conf
rm -f /etc/systemd/coredump.conf.d/opsec.conf
opsec_green "Core dumps restored"
}
# ─── SWAP ──────────────────────────────────────────────────────────────────────
apply_swap() {
if [ "${HARDEN_SWAP:-1}" = "1" ]; then
opsec_info "Disabling swap..."
swapoff -a 2>/dev/null || true
# Comment out swap entries in fstab (backup first)
if [ ! -f "$BACKUP_DIR/fstab.swap" ]; then
grep -E '^\s*[^#].*\sswap\s' /etc/fstab > "$BACKUP_DIR/fstab.swap" 2>/dev/null || true
fi
sed -i '/\sswap\s/s/^/#OPSEC# /' /etc/fstab 2>/dev/null || true
opsec_green "Swap disabled"
fi
}
revert_swap() {
sed -i 's/^#OPSEC# //' /etc/fstab 2>/dev/null || true
swapon -a 2>/dev/null || true
opsec_green "Swap restored"
}
# ─── IPv6 ──────────────────────────────────────────────────────────────────────
apply_ipv6() {
if [ "${HARDEN_IPV6:-1}" = "1" ]; then
opsec_info "Disabling IPv6..."
sysctl -w net.ipv6.conf.all.disable_ipv6=1 >/dev/null
sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null
opsec_green "IPv6 disabled"
fi
}
revert_ipv6() {
sysctl -w net.ipv6.conf.all.disable_ipv6=0 >/dev/null
sysctl -w net.ipv6.conf.default.disable_ipv6=0 >/dev/null
opsec_green "IPv6 restored"
}
# ─── TIMEZONE SPOOF ────────────────────────────────────────────────────────────
apply_timezone() {
if [ "${HARDEN_TIMEZONE_SPOOF:-0}" = "1" ]; then
local tz="${HARDEN_TIMEZONE_VALUE:-UTC}"
opsec_info "Spoofing timezone to ${tz}..."
# Backup current timezone
timedatectl show -p Timezone --value > "$BACKUP_DIR/timezone.orig" 2>/dev/null || true
timedatectl set-timezone "$tz" 2>/dev/null || {
ln -sf "/usr/share/zoneinfo/${tz}" /etc/localtime
}
opsec_green "Timezone set to ${tz}"
fi
}
revert_timezone() {
if [ -f "$BACKUP_DIR/timezone.orig" ]; then
local orig_tz
orig_tz=$(cat "$BACKUP_DIR/timezone.orig")
timedatectl set-timezone "$orig_tz" 2>/dev/null || true
rm -f "$BACKUP_DIR/timezone.orig"
opsec_green "Timezone restored to ${orig_tz}"
fi
}
# ─── LOCALE SPOOF ─────────────────────────────────────────────────────────────
apply_locale() {
if [ "${HARDEN_LOCALE_SPOOF:-0}" = "1" ]; then
local loc="${HARDEN_LOCALE_VALUE:-en_US.UTF-8}"
opsec_info "Spoofing locale to ${loc}..."
# Backup
locale > "$BACKUP_DIR/locale.orig" 2>/dev/null || true
export LANG="$loc"
export LC_ALL="$loc"
echo "LANG=${loc}" > /etc/default/locale.opsec
opsec_green "Locale set to ${loc}"
fi
}
revert_locale() {
rm -f /etc/default/locale.opsec
if [ -f "$BACKUP_DIR/locale.orig" ]; then
rm -f "$BACKUP_DIR/locale.orig"
fi
opsec_green "Locale restored"
}
# ─── SCREEN LOCK ───────────────────────────────────────────────────────────────
apply_screen_lock() {
if [ "${HARDEN_SCREEN_LOCK:-1}" = "1" ]; then
local timeout="${HARDEN_SCREEN_LOCK_TIMEOUT:-300}"
opsec_info "Setting screen lock timeout to ${timeout}s..."
# Try GNOME dconf (runs as user via sudo)
local real_user="${SUDO_USER:-$USER}"
if command -v dconf >/dev/null 2>&1; then
su - "$real_user" -c "
dconf write /org/gnome/desktop/session/idle-delay 'uint32 ${timeout}' 2>/dev/null
dconf write /org/gnome/desktop/screensaver/lock-enabled 'true' 2>/dev/null
dconf write /org/gnome/desktop/screensaver/lock-delay 'uint32 0' 2>/dev/null
" 2>/dev/null || true
fi
opsec_green "Screen lock set to ${timeout}s"
fi
}
revert_screen_lock() {
local real_user="${SUDO_USER:-$USER}"
if command -v dconf >/dev/null 2>&1; then
su - "$real_user" -c "
dconf write /org/gnome/desktop/session/idle-delay 'uint32 900' 2>/dev/null
" 2>/dev/null || true
fi
opsec_green "Screen lock restored to default"
}
# ─── WEBRTC BLOCKING ──────────────────────────────────────────────────────────
apply_webrtc() {
if [ "${LEAK_WEBRTC_BLOCK:-1}" = "1" ]; then
opsec_info "Blocking WebRTC..."
local real_user="${SUDO_USER:-$USER}"
local user_home
user_home=$(eval echo "~${real_user}")
# Firefox profiles
for profile_dir in "${user_home}"/.mozilla/firefox/*.default* "${user_home}"/.mozilla/firefox/*.opsec*; do
[ -d "$profile_dir" ] || continue
cat >> "${profile_dir}/user.js" << 'EOF'
// OPSEC: Disable WebRTC IP leak
user_pref("media.peerconnection.enabled", false);
user_pref("media.peerconnection.turn.disable", true);
user_pref("media.peerconnection.use_document_iceservers", false);
user_pref("media.peerconnection.video.enabled", false);
user_pref("media.peerconnection.identity.timeout", 1);
EOF
done
# Brave/Chromium policies
mkdir -p /etc/brave/policies/managed /etc/chromium/policies/managed
cat > /etc/brave/policies/managed/opsec-webrtc.json << 'EOF'
{ "WebRtcIPHandling": "disable_non_proxied_udp" }
EOF
cp /etc/brave/policies/managed/opsec-webrtc.json /etc/chromium/policies/managed/ 2>/dev/null || true
opsec_green "WebRTC blocked (Firefox + Brave/Chromium)"
fi
}
revert_webrtc() {
local real_user="${SUDO_USER:-$USER}"
local user_home
user_home=$(eval echo "~${real_user}")
for profile_dir in "${user_home}"/.mozilla/firefox/*.default* "${user_home}"/.mozilla/firefox/*.opsec*; do
[ -d "$profile_dir" ] || continue
sed -i '/OPSEC: Disable WebRTC/,/peerconnection\.identity/d' "${profile_dir}/user.js" 2>/dev/null || true
done
rm -f /etc/brave/policies/managed/opsec-webrtc.json
rm -f /etc/chromium/policies/managed/opsec-webrtc.json
opsec_green "WebRTC restrictions removed"
}
# ─── USB BLOCKING ─────────────────────────────────────────────────────────────
apply_usb_block() {
if [ "${LEAK_USB_BLOCK:-0}" = "1" ]; then
opsec_info "Blocking new USB devices..."
echo 0 > /sys/bus/usb/drivers_autoprobe 2>/dev/null || true
opsec_green "USB auto-probe disabled (new devices blocked)"
fi
}
revert_usb_block() {
echo 1 > /sys/bus/usb/drivers_autoprobe 2>/dev/null || true
opsec_green "USB auto-probe restored"
}
# ─── CLIPBOARD AUTO-CLEAR ─────────────────────────────────────────────────────
apply_clipboard_clear() {
if [ "${HARDEN_CLIPBOARD_CLEAR:-0}" = "1" ]; then
opsec_info "Starting clipboard auto-clear..."
# Launch background clearer (clears clipboard every 30 seconds)
local pidfile="/var/run/opsec-clipboard.pid"
if [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then
opsec_yellow "Clipboard cleaner already running"
return
fi
(
while true; do
sleep 30
# Clear X clipboard
if command -v xclip >/dev/null 2>&1; then
echo -n "" | xclip -selection clipboard 2>/dev/null || true
echo -n "" | xclip -selection primary 2>/dev/null || true
elif command -v xsel >/dev/null 2>&1; then
xsel --clipboard --clear 2>/dev/null || true
xsel --primary --clear 2>/dev/null || true
fi
done
) &
echo $! > "$pidfile"
opsec_green "Clipboard auto-clear active (every 30s)"
fi
}
revert_clipboard_clear() {
local pidfile="/var/run/opsec-clipboard.pid"
if [ -f "$pidfile" ]; then
kill "$(cat "$pidfile")" 2>/dev/null || true
rm -f "$pidfile"
fi
opsec_green "Clipboard auto-clear stopped"
}
# ─── TRAFFIC JITTER ───────────────────────────────────────────────────────────
apply_traffic_jitter() {
if [ "${TRAFFIC_JITTER_ENABLED:-0}" = "1" ]; then
local ms="${TRAFFIC_JITTER_MS:-50}"
opsec_info "Adding ${ms}ms traffic jitter..."
# Find primary outbound interface
local iface
iface=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1)
if [ -n "$iface" ]; then
# Remove existing qdisc first
tc qdisc del dev "$iface" root 2>/dev/null || true
tc qdisc add dev "$iface" root netem delay "${ms}ms" "${ms}ms" distribution normal 2>/dev/null || {
opsec_yellow "tc not available — jitter skipped"
return
}
echo "$iface" > "$BACKUP_DIR/jitter-iface"
opsec_green "Traffic jitter active on ${iface} (${ms}ms)"
else
opsec_yellow "No outbound interface found for jitter"
fi
fi
}
revert_traffic_jitter() {
if [ -f "$BACKUP_DIR/jitter-iface" ]; then
local iface
iface=$(cat "$BACKUP_DIR/jitter-iface")
tc qdisc del dev "$iface" root 2>/dev/null || true
rm -f "$BACKUP_DIR/jitter-iface"
opsec_green "Traffic jitter removed from ${iface}"
fi
}
# ─── MAIN ──────────────────────────────────────────────────────────────────────
do_apply() {
echo ""
opsec_hdr "APPLYING SYSTEM HARDENING"
echo ""
apply_core_dumps
apply_swap
apply_ipv6
apply_timezone
apply_locale
apply_screen_lock
apply_webrtc
# Ghost-mode-only features — skip when called from mode_base (OPSEC_BASE_ONLY=1)
if [ "${OPSEC_BASE_ONLY:-0}" != "1" ]; then
apply_usb_block
apply_clipboard_clear
apply_traffic_jitter
else
opsec_info "Base-only mode — skipping USB block, clipboard clear, traffic jitter"
fi
echo ""
opsec_green "All hardening measures applied"
}
do_revert() {
echo ""
opsec_hdr "REVERTING SYSTEM HARDENING"
echo ""
revert_core_dumps
revert_swap
revert_ipv6
revert_timezone
revert_locale
revert_screen_lock
revert_webrtc
revert_usb_block
revert_clipboard_clear
revert_traffic_jitter
echo ""
opsec_green "All hardening measures reverted"
}
case "${1:-}" in
apply) do_apply ;;
revert) do_revert ;;
*)
echo "Usage: $(basename "$0") apply|revert"
exit 1
;;
esac
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
# OPSEC Hostname Randomization
# Config-aware: reads HOSTNAME_PATTERN and HOSTNAME_CUSTOM_PREFIX from /etc/opsec/opsec.conf
# Patterns: desktop (desktop-XXXX), random (8 random chars), custom (PREFIX-XXXX)
if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# Source config if available
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
opsec_load_config 2>/dev/null || true
else
opsec_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
opsec_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
opsec_dim() { echo -e "\033[38;5;244m $*\033[0m"; }
fi
# Config values with defaults
PATTERN="${HOSTNAME_PATTERN:-desktop}"
PREFIX="${HOSTNAME_CUSTOM_PREFIX:-}"
RAND_HEX=$(head -c 2 /dev/urandom | od -An -tx1 | tr -d ' ')
OLD_HOSTNAME=$(hostname)
case "$PATTERN" in
desktop)
NEW_HOSTNAME="desktop-${RAND_HEX}"
;;
random)
NEW_HOSTNAME=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' ')
;;
custom)
NEW_HOSTNAME="${PREFIX}-${RAND_HEX}"
;;
*)
NEW_HOSTNAME="desktop-${RAND_HEX}"
;;
esac
opsec_info "Randomizing hostname..."
opsec_dim "Old: ${OLD_HOSTNAME}"
opsec_dim "New: ${NEW_HOSTNAME} (pattern: ${PATTERN})"
# Set hostname via hostnamectl (persistent)
hostnamectl set-hostname "$NEW_HOSTNAME" 2>/dev/null || {
echo "$NEW_HOSTNAME" > /etc/hostname
hostname "$NEW_HOSTNAME"
}
# Update /etc/hosts to match
if grep -q "$OLD_HOSTNAME" /etc/hosts 2>/dev/null; then
sed -i "s/$OLD_HOSTNAME/$NEW_HOSTNAME/g" /etc/hosts
fi
# Ensure localhost entries exist
if ! grep -q "127.0.0.1.*$NEW_HOSTNAME" /etc/hosts; then
sed -i "/127\.0\.0\.1/s/$/ $NEW_HOSTNAME/" /etc/hosts
fi
opsec_green "Hostname randomized to: ${NEW_HOSTNAME}"
+261
View File
@@ -0,0 +1,261 @@
#!/bin/bash
# OPSEC Kill Switch — iptables rules to block non-Tor/VPN traffic
# Config-aware: reads KILLSWITCH_* settings from /etc/opsec/opsec.conf
# Usage: opsec-killswitch.sh on|off|status
if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# Source config if available
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
opsec_load_config 2>/dev/null || true
else
opsec_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
opsec_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
fi
# Config values with defaults
ALLOW_DHCP="${KILLSWITCH_ALLOW_DHCP:-1}"
ALLOW_OPENVPN="${KILLSWITCH_ALLOW_OPENVPN:-1}"
ALLOW_WIREGUARD="${KILLSWITCH_ALLOW_WIREGUARD:-1}"
EXTRA_PORTS="${KILLSWITCH_EXTRA_PORTS:-}"
TRANS_PORT="${TOR_TRANS_PORT:-9040}"
DNS_PORT="${TOR_DNS_PORT:-5353}"
CHAIN="GP_FW"
TOR_UID=$(id -u debian-tor 2>/dev/null || id -u tor 2>/dev/null || echo "")
KS_LOG="/run/opsec/debug.log"
mkdir -p /run/opsec 2>/dev/null || true
KS_ERR=$(mktemp /run/opsec/.ks-err.XXXXXX 2>/dev/null || mktemp /tmp/.ks-err.XXXXXX)
KS_FAIL=0
trap 'rm -f "$KS_ERR"' EXIT
ks_log() { echo "[$(date -Is)] [killswitch] $*" >> "$KS_LOG"; }
# Wrapper that logs failures and tracks failure count
ipt() {
if ! iptables "$@" 2>>"$KS_ERR"; then
ks_log "FAILED: iptables $*$(cat "$KS_ERR" 2>/dev/null)"
cat "$KS_ERR" >&2 2>/dev/null
KS_FAIL=$((KS_FAIL + 1))
: > "$KS_ERR" 2>/dev/null
return 1
fi
: > "$KS_ERR" 2>/dev/null
return 0
}
ipt6() {
if ! ip6tables "$@" 2>>"$KS_ERR"; then
ks_log "FAILED: ip6tables $*$(cat "$KS_ERR" 2>/dev/null)"
KS_FAIL=$((KS_FAIL + 1))
: > "$KS_ERR" 2>/dev/null
return 1
fi
: > "$KS_ERR" 2>/dev/null
return 0
}
ks_filter_on() {
opsec_info "Arming OPSEC kill switch (filter-only)..."
ks_log "=== KILL SWITCH FILTER-ONLY ==="
ks_log "TOR_UID=${TOR_UID:-NONE}"
ks_log "ALLOW_DHCP=${ALLOW_DHCP} ALLOW_OPENVPN=${ALLOW_OPENVPN} ALLOW_WIREGUARD=${ALLOW_WIREGUARD}"
ks_log "EXTRA_PORTS=${EXTRA_PORTS:-none}"
iptables -N "$CHAIN" 2>/dev/null || iptables -F "$CHAIN"
ipt -A "$CHAIN" -o lo -j ACCEPT
ipt -A "$CHAIN" -i lo -j ACCEPT
ipt -A "$CHAIN" -d 127.0.0.0/8 -j ACCEPT
ipt -A "$CHAIN" -m state --state ESTABLISHED,RELATED -j ACCEPT
if [ -n "$TOR_UID" ]; then
ipt -A "$CHAIN" -m owner --uid-owner "$TOR_UID" -j ACCEPT
else
ks_log "WARNING: No TOR_UID — Tor will be blocked by filter!"
fi
ipt -A "$CHAIN" -o tun+ -j ACCEPT
ipt -A "$CHAIN" -i tun+ -j ACCEPT
ipt -A "$CHAIN" -o wg+ -j ACCEPT
ipt -A "$CHAIN" -i wg+ -j ACCEPT
[ "$ALLOW_DHCP" = "1" ] && ipt -A "$CHAIN" -p udp --dport 67:68 --sport 67:68 -j ACCEPT
[ "$ALLOW_OPENVPN" = "1" ] && { ipt -A "$CHAIN" -p udp --dport 1194 -j ACCEPT; ipt -A "$CHAIN" -p tcp --dport 1194 -j ACCEPT; }
[ "$ALLOW_WIREGUARD" = "1" ] && ipt -A "$CHAIN" -p udp --dport 51820 -j ACCEPT
if [ -n "$EXTRA_PORTS" ]; then
local IFS=','
for rule in $EXTRA_PORTS; do
local proto=$(echo "$rule" | cut -d: -f1)
local port=$(echo "$rule" | cut -d: -f2)
[ -n "$proto" ] && [ -n "$port" ] && ipt -A "$CHAIN" -p "$proto" --dport "$port" -j ACCEPT
done
fi
ipt -A "$CHAIN" -j DROP
iptables -D OUTPUT -j "$CHAIN" 2>/dev/null
ipt -I OUTPUT 1 -j "$CHAIN"
# IPv6
ip6tables -N "$CHAIN" 2>/dev/null || ip6tables -F "$CHAIN"
ipt6 -A "$CHAIN" -o lo -j ACCEPT
ipt6 -A "$CHAIN" -j DROP
ip6tables -D OUTPUT -j "$CHAIN" 2>/dev/null
ipt6 -I OUTPUT 1 -j "$CHAIN"
ks_log "Filter-only lockdown active (no NAT)"
opsec_green "Kill switch filter ARMED (pre-bootstrap)"
}
ks_nat_on() {
opsec_info "Arming NAT transparent proxy..."
ks_log "=== NAT PROXY ON ==="
local NAT_CHAIN="GP_NAT"
# Remove jump before flush to prevent brief bypass window
iptables -t nat -D OUTPUT -j "$NAT_CHAIN" 2>/dev/null
iptables -t nat -N "$NAT_CHAIN" 2>/dev/null || iptables -t nat -F "$NAT_CHAIN"
# Tor UID RETURN
[ -n "$TOR_UID" ] && ipt -t nat -A "$NAT_CHAIN" -p tcp -m owner --uid-owner "$TOR_UID" -j RETURN
# VPN RETURN — BEFORE DNS redirect
ipt -t nat -A "$NAT_CHAIN" -o tun+ -j RETURN
ipt -t nat -A "$NAT_CHAIN" -o wg+ -j RETURN
# DNS redirect
ipt -t nat -A "$NAT_CHAIN" -p udp --dport 53 -j REDIRECT --to-ports "$DNS_PORT"
ipt -t nat -A "$NAT_CHAIN" -p tcp --dport 53 -j REDIRECT --to-ports "$DNS_PORT"
# Loopback RETURN
ipt -t nat -A "$NAT_CHAIN" -d 127.0.0.0/8 -j RETURN
# TCP catch-all TransPort
ipt -t nat -A "$NAT_CHAIN" -p tcp -j REDIRECT --to-ports "$TRANS_PORT"
# Install jump
ipt -t nat -I OUTPUT 1 -j "$NAT_CHAIN"
# Verify chain was populated (at least 3 rules: DNS redirect + TCP catch-all + others)
local nat_count
nat_count=$(iptables -t nat -L "$NAT_CHAIN" --line-numbers 2>/dev/null | tail -n +3 | wc -l)
ks_log "NAT chain rule count: ${nat_count}"
if [ "$nat_count" -lt 3 ]; then
ks_log "WARNING: NAT chain has only ${nat_count} rules — falling back to direct OUTPUT rules"
opsec_info "NAT chain underpopulated (${nat_count} rules) — using direct OUTPUT fallback"
# Remove the jump to the empty/broken chain
iptables -t nat -D OUTPUT -j "$NAT_CHAIN" 2>/dev/null
iptables -t nat -F "$NAT_CHAIN" 2>/dev/null
iptables -t nat -X "$NAT_CHAIN" 2>/dev/null
# Direct OUTPUT rules as fallback
[ -n "$TOR_UID" ] && iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner "$TOR_UID" -j RETURN 2>/dev/null
iptables -t nat -A OUTPUT -o tun+ -j RETURN 2>/dev/null
iptables -t nat -A OUTPUT -o wg+ -j RETURN 2>/dev/null
iptables -t nat -A OUTPUT -p udp --dport 53 -j REDIRECT --to-ports "$DNS_PORT" 2>/dev/null
iptables -t nat -A OUTPUT -p tcp --dport 53 -j REDIRECT --to-ports "$DNS_PORT" 2>/dev/null
iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN 2>/dev/null
iptables -t nat -A OUTPUT -p tcp -j REDIRECT --to-ports "$TRANS_PORT" 2>/dev/null
local fb_count
fb_count=$(iptables -t nat -L OUTPUT --line-numbers 2>/dev/null | tail -n +3 | wc -l)
ks_log "Fallback OUTPUT NAT rule count: ${fb_count}"
fi
if [ "$KS_FAIL" -gt 0 ]; then
ks_log "WARNING: ${KS_FAIL} iptables commands failed during NAT setup"
opsec_info "WARNING: ${KS_FAIL} iptables rule(s) failed — check /run/opsec/debug.log"
return 1
fi
ks_log "NAT transparent proxy armed"
opsec_green "Kill switch fully ARMED — Tor + NAT active"
}
ks_on() {
ks_filter_on
ks_nat_on
}
ks_off() {
opsec_info "Disarming OPSEC kill switch..."
ks_log "=== KILL SWITCH OFF ==="
# Filter chain
iptables -D OUTPUT -j "$CHAIN" 2>/dev/null
iptables -F "$CHAIN" 2>/dev/null
iptables -X "$CHAIN" 2>/dev/null
# NAT chain (config-independent teardown)
local NAT_CHAIN="GP_NAT"
iptables -t nat -D OUTPUT -j "$NAT_CHAIN" 2>/dev/null
iptables -t nat -F "$NAT_CHAIN" 2>/dev/null
iptables -t nat -X "$NAT_CHAIN" 2>/dev/null
# Clean up fallback direct OUTPUT NAT rules (if chain approach failed during arm)
# Loop-delete to catch duplicates — keeps removing until no match left
local _fb_cleaned=0
while iptables -t nat -D OUTPUT -p tcp -j REDIRECT --to-ports "$TRANS_PORT" 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
while iptables -t nat -D OUTPUT -p udp --dport 53 -j REDIRECT --to-ports "$DNS_PORT" 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
while iptables -t nat -D OUTPUT -p tcp --dport 53 -j REDIRECT --to-ports "$DNS_PORT" 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
while iptables -t nat -D OUTPUT -d 127.0.0.0/8 -j RETURN 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
while iptables -t nat -D OUTPUT -o tun+ -j RETURN 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
while iptables -t nat -D OUTPUT -o wg+ -j RETURN 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
if [ -n "$TOR_UID" ]; then
while iptables -t nat -D OUTPUT -p tcp -m owner --uid-owner "$TOR_UID" -j RETURN 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
fi
[ "$_fb_cleaned" -gt 0 ] && ks_log "Cleaned ${_fb_cleaned} fallback NAT OUTPUT rules"
# IPv6
ip6tables -D OUTPUT -j "$CHAIN" 2>/dev/null
ip6tables -F "$CHAIN" 2>/dev/null
ip6tables -X "$CHAIN" 2>/dev/null
# Flush conntrack to clear stale NAT entries
conntrack -F 2>/dev/null && ks_log "conntrack flushed" || true
ks_log "Kill switch disarmed"
opsec_green "Kill switch DISARMED — normal traffic allowed"
}
ks_status() {
if iptables -L "$CHAIN" >/dev/null 2>&1; then
opsec_green "Kill switch is ARMED"
echo ""
echo "IPv4 rules:"
iptables -L "$CHAIN" -v -n --line-numbers
echo ""
echo "NAT chain:"
if iptables -t nat -L GP_NAT >/dev/null 2>&1; then
iptables -t nat -L GP_NAT -v -n --line-numbers
else
echo " (not armed)"
fi
echo ""
echo "IPv6 rules:"
ip6tables -L "$CHAIN" -v -n --line-numbers 2>/dev/null
else
opsec_info "Kill switch is OFF"
fi
}
case "${1}" in
on) ks_on ;;
filter-on) ks_filter_on ;;
nat-on) ks_nat_on ;;
off) ks_off ;;
status) ks_status ;;
*)
echo "Usage: $(basename "$0") on|off|filter-on|nat-on|status"
exit 1
;;
esac
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
# /usr/local/bin/opsec-log-rotate.sh — Secure log rotation and cleanup
# Truncates sensitive logs, shreds rotated files, clears journald + shell histories
# Runs via cron (configurable LOG_ROTATION_HOURS in /etc/opsec/opsec.conf)
set -euo pipefail
if [ "$EUID" -ne 0 ]; then
echo "[-] Please run as root"
exit 1
fi
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
_HAS_LIB=false
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
opsec_load_config 2>/dev/null || true
_HAS_LIB=true
else
opsec_green() { echo "[+] $*"; }
opsec_info() { echo "[~] $*"; }
fi
LOG_FILE="/var/log/opsec-log-rotate.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
log() { echo "[$TIMESTAMP] $*" >> "$LOG_FILE"; }
log "Starting secure log rotation"
# ─── TRUNCATE SENSITIVE SYSTEM LOGS ────────────────────────────────────────────
SENSITIVE_LOGS=(
/var/log/auth.log
/var/log/syslog
/var/log/kern.log
/var/log/daemon.log
/var/log/messages
/var/log/user.log
/var/log/mail.log
/var/log/debug
/var/log/wtmp
/var/log/btmp
/var/log/lastlog
/var/log/faillog
)
for logfile in "${SENSITIVE_LOGS[@]}"; do
if [ -f "$logfile" ]; then
truncate -s 0 "$logfile" 2>/dev/null || true
log "Truncated: $logfile"
fi
done
# ─── SHRED ROTATED LOG FILES (SSD-aware) ─────────────────────────────────────
for rotated in /var/log/*.gz /var/log/*.1 /var/log/*.old; do
if [ -f "$rotated" ]; then
if [ "$_HAS_LIB" = "true" ]; then
opsec_secure_delete "$rotated"
else
shred -fuz "$rotated" 2>/dev/null || rm -f "$rotated"
fi
log "Wiped: $rotated"
fi
done
# ─── CLEAR JOURNALD ───────────────────────────────────────────────────────────
if command -v journalctl >/dev/null 2>&1; then
journalctl --vacuum-time=1h 2>/dev/null || true
log "Journald vacuumed (1h retention)"
fi
# ─── CLEAR SHELL HISTORIES ────────────────────────────────────────────────────
for user_home in /home/* /root; do
[ -d "$user_home" ] || continue
for hist_file in .bash_history .zsh_history .python_history .psql_history .mysql_history .lesshst .viminfo; do
if [ -f "${user_home}/${hist_file}" ]; then
if [ "$_HAS_LIB" = "true" ]; then
opsec_secure_delete "${user_home}/${hist_file}"
else
shred -fuz "${user_home}/${hist_file}" 2>/dev/null || truncate -s 0 "${user_home}/${hist_file}" 2>/dev/null || true
fi
log "Cleared: ${user_home}/${hist_file}"
fi
done
done
# ─── CLEAR RECENTLY USED ──────────────────────────────────────────────────────
for user_home in /home/* /root; do
[ -d "$user_home" ] || continue
rm -f "${user_home}/.local/share/recently-used.xbel" 2>/dev/null || true
done
# ─── CLEAR TEMP FILES ─────────────────────────────────────────────────────────
find /tmp -type f -mmin +60 -delete 2>/dev/null || true
find /var/tmp -type f -mmin +60 -delete 2>/dev/null || true
log "Secure log rotation complete"
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# opsec-mode-toggle — Keyboard shortcut wrapper for opsec-mode on/off
# Toggles between standard and advanced OPSEC mode
# Bind to: Ctrl+Alt+\
STATE_FILE="/var/run/opsec-advanced.enabled"
BOOTSTRAP_STOP="/var/run/opsec-bootstrap-stop"
DEBUG_LOG="/var/log/opsec-debug.log"
toggle_log() {
echo "[$(date -Is)] [toggle] $*" >> "$DEBUG_LOG" 2>/dev/null || true
}
if [ -f "$STATE_FILE" ]; then
# Turning OFF — signal bootstrap loop to stop, then run mode_off
toggle_log "Toggling OFF (state file exists)"
# Create stop signal so mode_on's bootstrap loop exits immediately
# (needs root — use pkexec for a one-liner)
pkexec bash -c "touch ${BOOTSTRAP_STOP} && /usr/local/bin/opsec-mode.sh off"
toggle_log "Toggle OFF complete (exit: $?)"
else
toggle_log "Toggling ON (no state file)"
pkexec /usr/local/bin/opsec-mode.sh on
toggle_log "Toggle ON complete (exit: $?)"
fi
+1210
View File
File diff suppressed because it is too large Load Diff
+119
View File
@@ -0,0 +1,119 @@
#!/bin/bash
# /usr/local/bin/opsec-monitor.sh — Background connection monitor
# Watches for non-Tor/non-VPN outbound connections, new listening ports,
# unexpected DNS queries. Sends desktop notifications + logs.
# Usage: sudo opsec-monitor.sh start|stop|status
set -euo pipefail
PIDFILE="/var/run/opsec-monitor.pid"
LOGFILE="/var/log/opsec-monitor.log"
INTERVAL=10
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
else
opsec_green() { echo "[+] $*"; }
opsec_red() { echo "[-] $*"; }
opsec_info() { echo "[~] $*"; }
fi
notify() {
local msg="$1" urgency="${2:-normal}"
local real_user="${SUDO_USER:-$USER}"
# Desktop notification
su - "$real_user" -c "DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u "$real_user")/bus notify-send -u '$urgency' 'OPSEC Monitor' '$msg'" 2>/dev/null || true
# Log
echo "[$(date '+%H:%M:%S')] $msg" >> "$LOGFILE"
}
monitor_loop() {
local known_listeners=""
local tor_uid
tor_uid=$(id -u debian-tor 2>/dev/null || id -u tor 2>/dev/null || echo "")
while true; do
# ─── Check for non-Tor/VPN outbound connections ────────────────────
local suspicious
suspicious=$(ss -tunp 2>/dev/null | grep ESTAB | grep -v '127.0.0.1' | grep -v '::1' | \
grep -v "tun\|wg\|tor\|${tor_uid:-NOOP}" | \
grep -v ':9050\|:5353\|:1194\|:51820' || true)
if [ -n "$suspicious" ]; then
local count
count=$(echo "$suspicious" | wc -l)
notify "ALERT: ${count} non-Tor/VPN outbound connection(s) detected" "critical"
fi
# ─── Check for new listening ports ─────────────────────────────────
local current_listeners
current_listeners=$(ss -tlnp 2>/dev/null | tail -n +2 | awk '{print $4}' | sort)
if [ -n "$known_listeners" ] && [ "$current_listeners" != "$known_listeners" ]; then
local new_ports
new_ports=$(comm -13 <(echo "$known_listeners") <(echo "$current_listeners") 2>/dev/null || true)
if [ -n "$new_ports" ]; then
notify "New listening port(s): ${new_ports}" "critical"
fi
fi
known_listeners="$current_listeners"
# ─── Check for unexpected DNS queries ──────────────────────────────
local dns_leaks
dns_leaks=$(ss -tunp 2>/dev/null | grep ':53 ' | grep -v '127.0.0.1' | grep -v '::1' || true)
if [ -n "$dns_leaks" ]; then
notify "DNS LEAK: Non-local DNS query detected" "critical"
fi
sleep "$INTERVAL"
done
}
do_start() {
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
opsec_info "Monitor already running (PID: $(cat "$PIDFILE"))"
return
fi
monitor_loop &
echo $! > "$PIDFILE"
opsec_green "Monitor started (PID: $!, logging to ${LOGFILE})"
}
do_stop() {
if [ -f "$PIDFILE" ]; then
local pid
pid=$(cat "$PIDFILE")
kill "$pid" 2>/dev/null || true
# Kill child processes
pkill -P "$pid" 2>/dev/null || true
rm -f "$PIDFILE"
opsec_green "Monitor stopped"
else
opsec_info "Monitor not running"
fi
}
do_status() {
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
opsec_green "Monitor running (PID: $(cat "$PIDFILE"))"
if [ -f "$LOGFILE" ]; then
echo ""
echo "Last 10 log entries:"
tail -10 "$LOGFILE" 2>/dev/null || true
fi
else
opsec_info "Monitor not running"
fi
}
case "${1:-}" in
start) do_start ;;
stop) do_stop ;;
status) do_status ;;
*)
echo "Usage: $(basename "$0") start|stop|status"
exit 1
;;
esac
+291
View File
@@ -0,0 +1,291 @@
#!/bin/bash
# /usr/local/bin/opsec-preflight.sh — OPSEC Preflight Verification Gate
# Verifies all required security controls are active for the current mode.
#
# Usage:
# opsec-preflight.sh Human-readable HUD
# opsec-preflight.sh --score Pass/fail count
# opsec-preflight.sh --enforce Exit 1 if any applicable check fails
# opsec-preflight.sh --base Only run base checks (standard mode)
# opsec-preflight.sh --full Run base + ghost checks
# opsec-preflight.sh --base --enforce Base checks, exit 1 on fail
# opsec-preflight.sh --full --enforce Full checks, exit 1 on fail
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
OPSEC_CONF="/etc/opsec/opsec.conf"
PREFLIGHT_LOG="/var/log/opsec-preflight.log"
# Source library
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
opsec_load_config 2>/dev/null || true
elif [ -f "$OPSEC_CONF" ]; then
. "$OPSEC_CONF"
fi
# Fallback colors if lib not loaded
type opsec_green >/dev/null 2>&1 || {
opsec_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
opsec_red() { echo -e "\033[38;5;196m[-] $*\033[0m"; }
opsec_yellow() { echo -e "\033[38;5;214m[*] $*\033[0m"; }
opsec_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
opsec_cyan() { echo -e "\033[38;5;51m[>] $*\033[0m"; }
opsec_dim() { echo -e "\033[38;5;244m $*\033[0m"; }
opsec_hdr() { echo -e "\033[38;5;51m━━━ \033[38;5;201m$*\033[38;5;51m ━━━\033[0m"; }
}
# ─── PARSE ARGS ───────────────────────────────────────────────────────────────
MODE_SCORE=false
MODE_ENFORCE=false
CHECK_LEVEL=""
while [ $# -gt 0 ]; do
case "$1" in
--score) MODE_SCORE=true ;;
--enforce) MODE_ENFORCE=true ;;
--base) CHECK_LEVEL="base" ;;
--full) CHECK_LEVEL="full" ;;
*) echo "Usage: opsec-preflight.sh [--score|--enforce] [--base|--full]"; exit 1 ;;
esac
shift
done
# Auto-detect check level if not specified
if [ -z "$CHECK_LEVEL" ]; then
if [ -f /var/run/opsec-advanced.enabled ]; then
CHECK_LEVEL="full"
else
CHECK_LEVEL="base"
fi
fi
# ─── ENVIRONMENT ──────────────────────────────────────────────────────────────
LEVEL="${DEPLOYMENT_LEVEL:-bare-metal-standard}"
LTYPE="${LEVEL_TYPE:-standard}"
is_cloud() {
case "$LEVEL" in
cloud-*) return 0 ;;
*) return 1 ;;
esac
}
is_bare_metal() {
case "$LEVEL" in
bare-metal-*) return 0 ;;
*) return 1 ;;
esac
}
# ─── CHECK ENGINE ─────────────────────────────────────────────────────────────
PASS=0
FAIL=0
SKIP=0
FAILURES=""
check() {
local name="$1" result="$2" detail="${3:-}"
if [ "$result" = "pass" ]; then
PASS=$((PASS + 1))
$MODE_SCORE || opsec_green "PASS ${name}${detail:+ ${detail}}"
elif [ "$result" = "fail" ]; then
FAIL=$((FAIL + 1))
FAILURES="${FAILURES}\n - ${name}${detail:+: ${detail}}"
$MODE_SCORE || opsec_red "FAIL ${name}${detail:+ ${detail}}"
elif [ "$result" = "skip" ]; then
SKIP=$((SKIP + 1))
$MODE_SCORE || opsec_dim "SKIP ${name}${detail:+ (${detail})}"
fi
}
# ─── BASE CHECKS (always run) ────────────────────────────────────────────────
run_base_checks() {
$MODE_SCORE || opsec_cyan "Base Privacy Checks"
# 1. MAC randomized (bare-metal only)
if is_bare_metal; then
local pif cur_mac first_octet first_dec
pif=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1)
if [ -n "$pif" ]; then
cur_mac=$(ip link show "$pif" 2>/dev/null | awk '/ether/ {print $2}')
first_octet=$(echo "$cur_mac" | cut -d: -f1)
first_dec=$((16#${first_octet})) 2>/dev/null || first_dec=0
if (( first_dec & 2 )); then
check "MAC randomized" "pass" "${pif}: ${cur_mac}"
else
check "MAC randomized" "fail" "${pif}: ${cur_mac} (hardware address)"
fi
else
check "MAC randomized" "skip" "no primary interface"
fi
else
check "MAC randomized" "skip" "cloud deployment"
fi
# 2. IPv6 disabled
local ipv6_all
ipv6_all=$(sysctl -n net.ipv6.conf.all.disable_ipv6 2>/dev/null || echo "0")
if [ "$ipv6_all" = "1" ]; then
check "IPv6 disabled" "pass"
else
check "IPv6 disabled" "fail" "net.ipv6.conf.all.disable_ipv6=${ipv6_all}"
fi
# 3. Core dumps disabled
local core_pat
core_pat=$(sysctl -n kernel.core_pattern 2>/dev/null)
if [[ "$core_pat" == *"/bin/false"* ]] || [[ "$core_pat" == *"devnull"* ]]; then
check "Core dumps disabled" "pass"
else
check "Core dumps disabled" "fail" "core_pattern=${core_pat}"
fi
# 4. Swap disabled
local swap_count
swap_count=$(swapon --show --noheadings 2>/dev/null | wc -l)
if [ "$swap_count" -eq 0 ]; then
check "Swap disabled" "pass"
else
check "Swap disabled" "fail" "${swap_count} swap device(s) active"
fi
# 5. DNS is privacy resolver (not ISP)
local dns_server
dns_server=$(awk '/^nameserver/ {print $2; exit}' /etc/resolv.conf 2>/dev/null)
case "$dns_server" in
127.0.0.1|127.0.0.53|9.9.9.9|149.112.112.112|1.1.1.1|1.0.0.1)
check "DNS privacy resolver" "pass" "${dns_server}"
;;
*)
check "DNS privacy resolver" "fail" "${dns_server} (possibly ISP)"
;;
esac
# 6. WebRTC blocked (if Firefox present)
if [ -d "$HOME/.mozilla/firefox" ] || [ -d "/root/.mozilla/firefox" ]; then
local userjs_found=false
local profile_dirs
profile_dirs=$(find /home/*/.mozilla/firefox /root/.mozilla/firefox -maxdepth 1 -name '*.default*' 2>/dev/null | head -3)
for pd in $profile_dirs; do
if [ -f "$pd/user.js" ] && grep -q "media.peerconnection.enabled.*false" "$pd/user.js" 2>/dev/null; then
userjs_found=true
break
fi
done
if $userjs_found; then
check "WebRTC blocked" "pass" "user.js configured"
else
check "WebRTC blocked" "fail" "no user.js with WebRTC disable found"
fi
else
check "WebRTC blocked" "skip" "Firefox not detected"
fi
}
# ─── GHOST CHECKS (only when ghost mode active) ──────────────────────────────
run_ghost_checks() {
$MODE_SCORE || opsec_cyan "Ghost Mode Checks"
# 1. VPN or Tor active
local vpn_if tor_ok=false
vpn_if=$(ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | grep -E '^(tun|wg)' | head -1)
ss -tln 2>/dev/null | grep -q ':9050 ' && tor_ok=true
if [ -n "$vpn_if" ] || $tor_ok; then
local detail=""
[ -n "$vpn_if" ] && detail="VPN:${vpn_if}"
$tor_ok && detail="${detail:+${detail} + }Tor:9050"
check "VPN/Tor active" "pass" "$detail"
else
check "VPN/Tor active" "fail" "no tun/wg interface, no Tor on :9050"
fi
# 2. Kill switch armed
if [ -f /var/run/opsec-breakglass.active ]; then
check "Kill switch armed" "fail" "BREAK-GLASS active — kill switch intentionally dropped"
elif iptables -L GP_FW >/dev/null 2>&1; then
check "Kill switch armed" "pass"
else
check "Kill switch armed" "fail" "GP_FW chain not found"
fi
# 3. DNS locked to Tor
local dns_server
dns_server=$(awk '/^nameserver/ {print $2; exit}' /etc/resolv.conf 2>/dev/null)
if [ "$dns_server" = "127.0.0.1" ]; then
check "DNS locked to Tor" "pass"
else
check "DNS locked to Tor" "fail" "DNS=${dns_server} (expected 127.0.0.1)"
fi
# 4. Hostname randomized (bare-metal only)
if is_bare_metal; then
local cur_hostname
cur_hostname=$(hostname)
# Check if hostname looks randomized (not default patterns like kali, debian, localhost)
case "$cur_hostname" in
kali|debian|localhost|ubuntu|parrot)
check "Hostname randomized" "fail" "hostname=${cur_hostname} (appears default)"
;;
*)
check "Hostname randomized" "pass" "${cur_hostname}"
;;
esac
else
check "Hostname randomized" "skip" "cloud deployment"
fi
# 5. resolv.conf immutable
if lsattr /etc/resolv.conf 2>/dev/null | grep -q 'i'; then
check "resolv.conf immutable" "pass"
else
check "resolv.conf immutable" "fail" "chattr +i not set"
fi
}
# ─── EXECUTE CHECKS ──────────────────────────────────────────────────────────
if ! $MODE_SCORE; then
echo ""
opsec_hdr "OPSEC PREFLIGHT — ${CHECK_LEVEL^^}"
opsec_dim "Level: ${LEVEL} (${LTYPE})"
echo ""
fi
run_base_checks
if [ "$CHECK_LEVEL" = "full" ]; then
$MODE_SCORE || echo ""
run_ghost_checks
fi
# ─── RESULTS ──────────────────────────────────────────────────────────────────
TOTAL=$((PASS + FAIL))
if $MODE_SCORE; then
echo "${PASS}/${TOTAL} passed"
[ "$FAIL" -gt 0 ] && exit 1
exit 0
fi
echo ""
opsec_hdr "PREFLIGHT RESULT"
if [ "$FAIL" -eq 0 ]; then
opsec_green "ALL CHECKS PASSED (${PASS}/${TOTAL})"
[ "$SKIP" -gt 0 ] && opsec_dim "${SKIP} checks skipped (not applicable)"
else
opsec_red "FAILED: ${FAIL}/${TOTAL} checks"
opsec_dim "Passed: ${PASS} | Failed: ${FAIL} | Skipped: ${SKIP}"
echo -e "\033[38;5;196mFailures:${FAILURES}\033[0m"
# Log failures
echo "[$(date -Is)] PREFLIGHT ${CHECK_LEVEL^^}: ${PASS}/${TOTAL} passed, ${FAIL} failed${FAILURES}" >> "$PREFLIGHT_LOG" 2>/dev/null || true
fi
echo ""
if $MODE_ENFORCE && [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0
+115
View File
@@ -0,0 +1,115 @@
#!/bin/bash
# /usr/local/bin/opsec-sentry.sh — Continuous Threat Sentry Manager
# Manages the sentry daemon for background threat detection
# Usage: opsec-sentry.sh start|stop|status|restart
set -euo pipefail
SENTRY_BIN="${SENTRY_BIN:-/usr/local/bin/sentry-daemon}"
PID_FILE="/var/run/opsec-sentry.pid"
LOG_FILE="/var/log/opsec-sentry.log"
ALERT_HOOK=""
# Source config for alert hook if available
OPSEC_CONF="/etc/opsec/opsec.conf"
[ -f "$OPSEC_CONF" ] && . "$OPSEC_CONF"
# Color output helpers
_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
_red() { echo -e "\033[38;5;196m[-] $*\033[0m"; }
_yellow() { echo -e "\033[38;5;214m[*] $*\033[0m"; }
_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
is_running() {
[ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null
}
sentry_start() {
if is_running; then
_yellow "Sentry already running (PID $(cat "$PID_FILE"))"
return 0
fi
if [ ! -x "$SENTRY_BIN" ]; then
_red "Sentry binary not found at ${SENTRY_BIN}"
return 1
fi
local cmd="$SENTRY_BIN --daemon --sentry-pid $PID_FILE --sentry-log $LOG_FILE"
[ -n "$ALERT_HOOK" ] && cmd="$cmd --alert-hook $ALERT_HOOK"
# Run sentry in daemon mode
$cmd &
local pid=$!
echo "$pid" > "$PID_FILE"
# Verify it started
sleep 1
if kill -0 "$pid" 2>/dev/null; then
_green "Sentry daemon started (PID ${pid})"
_info "Log: ${LOG_FILE}"
else
rm -f "$PID_FILE"
_red "Sentry failed to start — check ${LOG_FILE}"
return 1
fi
}
sentry_stop() {
if ! is_running; then
_yellow "Sentry not running"
rm -f "$PID_FILE"
return 0
fi
local pid
pid=$(cat "$PID_FILE" 2>/dev/null)
kill "$pid" 2>/dev/null
# Wait for graceful shutdown
local i=0
while kill -0 "$pid" 2>/dev/null && [ $i -lt 10 ]; do
sleep 1
i=$((i + 1))
done
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null
fi
rm -f "$PID_FILE"
_green "Sentry daemon stopped"
}
sentry_status() {
if is_running; then
local pid
pid=$(cat "$PID_FILE" 2>/dev/null)
_green "Sentry ACTIVE (PID ${pid})"
if [ -f "$LOG_FILE" ]; then
local lines
lines=$(wc -l < "$LOG_FILE" 2>/dev/null || echo "0")
_info "Log entries: ${lines}"
local last
last=$(tail -1 "$LOG_FILE" 2>/dev/null)
[ -n "$last" ] && _info "Last: ${last}"
fi
else
_yellow "Sentry NOT running"
rm -f "$PID_FILE"
fi
}
case "${1:-}" in
start) sentry_start ;;
stop) sentry_stop ;;
restart)
sentry_stop
sleep 1
sentry_start
;;
status) sentry_status ;;
*)
echo "Usage: opsec-sentry.sh start|stop|status|restart"
exit 1
;;
esac
+123
View File
@@ -0,0 +1,123 @@
#!/bin/bash
# /usr/local/bin/opsec-ssh-check.sh — SSH Honeypot Detection
# Checks target SSH banners for known honeypot signatures
# Alias: ssh-safe
#
# Usage: opsec-ssh-check.sh <host> [port]
set -euo pipefail
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
else
opsec_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
opsec_red() { echo -e "\033[38;5;196m[-] $*\033[0m"; }
opsec_yellow() { echo -e "\033[38;5;214m[*] $*\033[0m"; }
opsec_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
opsec_cyan() { echo -e "\033[38;5;51m[>] $*\033[0m"; }
opsec_hdr() { echo -e "\033[38;5;51m━━━ \033[38;5;201m$*\033[38;5;51m ━━━\033[0m"; }
fi
HOST="${1:-}"
PORT="${2:-22}"
if [ -z "$HOST" ]; then
echo "Usage: $(basename "$0") <host> [port]"
echo " ssh-safe <host> [port]"
exit 1
fi
opsec_hdr "SSH HONEYPOT CHECK: ${HOST}:${PORT}"
echo ""
SUSPICIOUS=0
# ─── GRAB BANNER ───────────────────────────────────────────────────────────────
opsec_info "Grabbing SSH banner..."
BANNER=$(timeout 5 bash -c "echo '' | nc -w 3 $HOST $PORT 2>/dev/null" || true)
if [ -z "$BANNER" ]; then
opsec_yellow "No banner received — port may be filtered or service is not SSH"
exit 1
fi
opsec_cyan "Banner: ${BANNER}"
echo ""
# ─── KNOWN HONEYPOT SIGNATURES ─────────────────────────────────────────────────
# Cowrie
if echo "$BANNER" | grep -qiE 'SSH-2\.0-OpenSSH_6\.(0|1|2|6)p1.*Debian'; then
opsec_red "COWRIE SIGNATURE: Old OpenSSH version commonly used by Cowrie honeypot"
SUSPICIOUS=$((SUSPICIOUS + 3))
fi
# Kippo
if echo "$BANNER" | grep -qi 'SSH-1\.99-OpenSSH_5\.1p1'; then
opsec_red "KIPPO SIGNATURE: SSH-1.99 with OpenSSH_5.1p1 is a known Kippo default"
SUSPICIOUS=$((SUSPICIOUS + 3))
fi
# HonSSH
if echo "$BANNER" | grep -qi 'HonSSH'; then
opsec_red "HONSH SIGNATURE: Banner explicitly mentions HonSSH"
SUSPICIOUS=$((SUSPICIOUS + 5))
fi
# Generic old version check
if echo "$BANNER" | grep -qE 'OpenSSH_[345]\.' ; then
opsec_yellow "SUSPICIOUS: Very old OpenSSH version (common in honeypots)"
SUSPICIOUS=$((SUSPICIOUS + 2))
fi
# Unusual SSH protocol version
if echo "$BANNER" | grep -q 'SSH-1\.'; then
opsec_yellow "SUSPICIOUS: SSHv1 protocol (deprecated, often honeypot)"
SUSPICIOUS=$((SUSPICIOUS + 2))
fi
# ─── KEY EXCHANGE CHECK ────────────────────────────────────────────────────────
opsec_info "Checking key exchange algorithms..."
KEX_OUTPUT=$(timeout 5 ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o BatchMode=yes -o ConnectTimeout=3 -v -p "$PORT" "check@${HOST}" 2>&1 || true)
# Check for weak/unusual key types
if echo "$KEX_OUTPUT" | grep -qi 'ssh-dss'; then
opsec_yellow "SUSPICIOUS: DSA host key (often seen in honeypots)"
SUSPICIOUS=$((SUSPICIOUS + 1))
fi
# Check for unusually fast key exchange (honeypots often respond instantly)
if echo "$KEX_OUTPUT" | grep -qi 'Connection reset\|Connection refused'; then
opsec_info "Connection dropped (may be rate-limited or filtered)"
fi
# ─── TIMING CHECK ─────────────────────────────────────────────────────────────
opsec_info "Checking authentication timing..."
AUTH_START=$(date +%s%N)
timeout 3 ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o BatchMode=yes -o ConnectTimeout=3 -p "$PORT" "probe@${HOST}" 2>/dev/null || true
AUTH_END=$(date +%s%N)
AUTH_MS=$(( (AUTH_END - AUTH_START) / 1000000 ))
if [ "$AUTH_MS" -lt 50 ] && [ "$AUTH_MS" -gt 0 ]; then
opsec_yellow "SUSPICIOUS: Unusually fast auth response (${AUTH_MS}ms) — possible honeypot"
SUSPICIOUS=$((SUSPICIOUS + 1))
else
opsec_info "Auth response time: ${AUTH_MS}ms"
fi
# ─── VERDICT ───────────────────────────────────────────────────────────────────
echo ""
if [ "$SUSPICIOUS" -ge 3 ]; then
opsec_red "VERDICT: HIGH RISK — Likely honeypot (score: ${SUSPICIOUS})"
opsec_red "DO NOT connect to this host for operations"
elif [ "$SUSPICIOUS" -ge 1 ]; then
opsec_yellow "VERDICT: MODERATE RISK — Some anomalies detected (score: ${SUSPICIOUS})"
opsec_yellow "Proceed with caution"
else
opsec_green "VERDICT: LOW RISK — No honeypot signatures detected (score: ${SUSPICIOUS})"
fi
echo ""
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# OPSEC Toggle — single entry point for keyboard shortcut + desktop file
# Uses pkexec directly on opsec-mode.sh for proper polkit policy matching
OPSEC_MODE="/usr/local/bin/opsec-mode.sh"
STATE_FILE="/var/run/opsec-advanced.enabled"
[ ! -x "$OPSEC_MODE" ] && exit 1
if [ -f "$STATE_FILE" ]; then
# Turning OFF — opsec-mode.sh off handles bootstrap stop signal internally
if [ "$EUID" -eq 0 ] 2>/dev/null; then
"$OPSEC_MODE" off
else
pkexec "$OPSEC_MODE" off
fi
else
# Turning ON — pkexec prompts for auth via polkit policy
if [ "$EUID" -eq 0 ] 2>/dev/null; then
"$OPSEC_MODE" on
else
pkexec "$OPSEC_MODE" on
fi
fi
+180
View File
@@ -0,0 +1,180 @@
#!/bin/bash
# /usr/local/bin/opsec-traffic-blend.sh — Decoy Traffic Blending Daemon
# Generates background browsing noise through Tor/VPN to blend tool traffic
# with normal-looking web activity. Ghost mode only.
# Usage: opsec-traffic-blend.sh start|stop|status
set -euo pipefail
PID_FILE="/var/run/opsec-traffic-blend.pid"
LOG_FILE="/var/log/opsec-traffic-blend.log"
# Source config
OPSEC_CONF="/etc/opsec/opsec.conf"
[ -f "$OPSEC_CONF" ] && . "$OPSEC_CONF"
SOCKS_PORT="${TOR_SOCKS_PORT:-9050}"
# Mean interval in seconds (Poisson distribution approximation)
BLEND_INTERVAL="${TRAFFIC_BLEND_INTERVAL:-30}"
# Color helpers
_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
_red() { echo -e "\033[38;5;196m[-] $*\033[0m"; }
_yellow() { echo -e "\033[38;5;214m[*] $*\033[0m"; }
# Benign URLs to simulate normal browsing (mix of news, tech, social)
DECOY_URLS=(
"https://en.wikipedia.org/wiki/Special:Random"
"https://www.bbc.com/news"
"https://news.ycombinator.com"
"https://www.reuters.com"
"https://stackoverflow.com/questions"
"https://github.com/trending"
"https://www.reddit.com/r/technology/.json"
"https://www.weather.gov"
"https://httpbin.org/get"
"https://www.kernel.org"
"https://www.python.org"
"https://www.debian.org"
"https://archive.org"
"https://www.mozilla.org"
"https://duckduckgo.com/?q=weather"
"https://lite.cnn.com"
"https://text.npr.org"
)
# Random delay using Poisson-like distribution (exponential inter-arrival)
poisson_delay() {
# Approximate exponential distribution using bash
# -ln(U) * mean where U is uniform(0,1)
local mean="$1"
local rand
rand=$((RANDOM % 1000 + 1))
# Approximate: -ln(rand/1000) * mean
# Using awk for floating point
awk -v r="$rand" -v m="$mean" 'BEGIN {
u = r / 1000.0;
if (u < 0.001) u = 0.001;
delay = -log(u) * m;
if (delay < 5) delay = 5;
if (delay > 120) delay = 120;
printf "%d\n", delay;
}'
}
is_running() {
[ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null
}
blend_loop() {
local url_count=${#DECOY_URLS[@]}
echo "[$(date -Is)] Traffic blend daemon started (mean interval: ${BLEND_INTERVAL}s)" >> "$LOG_FILE"
while true; do
# Pick a random URL
local idx=$((RANDOM % url_count))
local url="${DECOY_URLS[$idx]}"
# Determine proxy method
local curl_opts=("--silent" "--output" "/dev/null" "--max-time" "15")
# Use Tor SOCKS if available
if ss -tln 2>/dev/null | grep -q ":${SOCKS_PORT} "; then
curl_opts+=("--socks5-hostname" "127.0.0.1:${SOCKS_PORT}")
fi
# Add realistic headers
curl_opts+=("-H" "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0")
curl_opts+=("-H" "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
curl_opts+=("-H" "Accept-Language: en-US,en;q=0.5")
# Make the request
curl "${curl_opts[@]}" "$url" 2>/dev/null || true
# Calculate next delay (Poisson-distributed)
local delay
delay=$(poisson_delay "$BLEND_INTERVAL")
sleep "$delay"
done
}
blend_start() {
if ! [ -f /var/run/opsec-advanced.enabled ]; then
_yellow "Traffic blending requires ghost mode to be active"
return 1
fi
if is_running; then
_yellow "Traffic blend already running (PID $(cat "$PID_FILE"))"
return 0
fi
# Start the blend loop as a background process
blend_loop &
local pid=$!
echo "$pid" > "$PID_FILE"
disown "$pid"
sleep 1
if kill -0 "$pid" 2>/dev/null; then
_green "Traffic blend started (PID ${pid}, mean interval ${BLEND_INTERVAL}s)"
else
rm -f "$PID_FILE"
_red "Traffic blend failed to start"
return 1
fi
}
blend_stop() {
if ! is_running; then
_yellow "Traffic blend not running"
rm -f "$PID_FILE"
return 0
fi
local pid
pid=$(cat "$PID_FILE" 2>/dev/null)
kill "$pid" 2>/dev/null || true
# Wait for process to stop
local i=0
while kill -0 "$pid" 2>/dev/null && [ $i -lt 5 ]; do
sleep 1
i=$((i + 1))
done
kill -9 "$pid" 2>/dev/null || true
rm -f "$PID_FILE"
echo "[$(date -Is)] Traffic blend daemon stopped" >> "$LOG_FILE"
_green "Traffic blend stopped"
}
blend_status() {
if is_running; then
local pid
pid=$(cat "$PID_FILE" 2>/dev/null)
_green "Traffic blend ACTIVE (PID ${pid})"
_yellow "Mean interval: ${BLEND_INTERVAL}s"
else
_yellow "Traffic blend NOT running"
rm -f "$PID_FILE"
fi
}
case "${1:-}" in
start) blend_start ;;
stop) blend_stop ;;
restart)
blend_stop
sleep 1
blend_start
;;
status) blend_status ;;
*)
echo "Usage: opsec-traffic-blend.sh start|stop|status|restart"
exit 1
;;
esac
+127
View File
@@ -0,0 +1,127 @@
#!/bin/bash
# OPSEC Widget Launcher — positions and launches the Conky widget
# Usage: opsec-widget-launch.sh [position]
#
# Positions:
# tl = top-left tc = top-center tr = top-right
# bl = bottom-left bc = bottom-center br = bottom-right
#
# Default: tr (top-right)
# Uses the theme system — regenerates config from /etc/opsec/themes/
CONKY_DIR="$HOME/.config/conky"
CONF="$CONKY_DIR/conky-opsec-widget.conf"
POS="${1:-tr}"
# Map shorthand to conky alignment + gaps
case "$POS" in
tl) ALIGN="top_left"; GAP_X=15; GAP_Y=15 ;;
tc) ALIGN="top_middle"; GAP_X=0; GAP_Y=15 ;;
tr) ALIGN="top_right"; GAP_X=15; GAP_Y=60 ;;
bl) ALIGN="bottom_left"; GAP_X=15; GAP_Y=60 ;;
bc) ALIGN="bottom_middle"; GAP_X=0; GAP_Y=60 ;;
br) ALIGN="bottom_right"; GAP_X=15; GAP_Y=60 ;;
*)
echo "Usage: $(basename "$0") [tl|tc|tr|bl|bc|br]"
echo ""
echo " tl = top-left tc = top-center tr = top-right"
echo " bl = bottom-left bc = bottom-center br = bottom-right"
exit 1
;;
esac
# Kill existing widget and stale cache daemon
killall conky 2>/dev/null
pkill -f 'conky-opsec-cache\.sh' 2>/dev/null
rm -f /tmp/.opsec-cache/netinfo 2>/dev/null
sleep 0.3
# Load theme from opsec config
OPSEC_CONF="/etc/opsec/opsec.conf"
THEME="default"
[ -f "$OPSEC_CONF" ] && THEME=$(grep "^WIDGET_THEME=" "$OPSEC_CONF" 2>/dev/null | cut -d'"' -f2)
[ -z "$THEME" ] && THEME="default"
THEME_FILE="/etc/opsec/themes/${THEME}.theme"
if [ -f "$THEME_FILE" ]; then
. "$THEME_FILE"
else
echo "Theme '${THEME}' not found, using defaults"
THEME_LABEL="Default"
CONKY_BG="0d1117"
CONKY_COLOR0="df2020"; CONKY_COLOR1="33ff33"; CONKY_COLOR2="3a8fd6"
CONKY_COLOR3="0d1117"; CONKY_COLOR4="1f6feb"; CONKY_COLOR5="58a6ff"
CONKY_COLOR6="79c0ff"; CONKY_COLOR7="c9d1d9"; CONKY_COLOR8="1f6feb"
CONKY_COLOR9="484f58"
fi
# Generate config with theme colors and chosen position
mkdir -p "$CONKY_DIR"
cat > "$CONF" << EOF
-- OPSEC Status Widget — OPSEC Status Widget
-- Theme: ${THEME} (${THEME_LABEL:-Custom})
-- Position: ${ALIGN}
conky.config = {
alignment = '${ALIGN}',
gap_x = ${GAP_X},
gap_y = ${GAP_Y},
minimum_width = 400,
minimum_height = 200,
maximum_width = 420,
own_window = true,
own_window_type = 'normal',
own_window_transparent = false,
own_window_argb_visual = true,
own_window_argb_value = 210,
own_window_colour = '${CONKY_BG:-0d1117}',
own_window_hints = 'undecorated,below,sticky,skip_taskbar,skip_pager',
xinerama_head = 0,
double_buffer = true,
draw_shades = true,
default_shade_color = '000000',
draw_outline = false,
draw_borders = true,
border_inner_margin = 12,
border_outer_margin = 4,
border_width = 1,
border_colour = '${CONKY_COLOR4:-1f6feb}',
stippled_borders = 0,
use_xft = true,
font = 'JetBrains Mono:size=10',
override_utf8_locale = true,
default_color = 'b0b0b0',
color0 = '${CONKY_COLOR0:-df2020}',
color1 = '${CONKY_COLOR1:-33ff33}',
color2 = '${CONKY_COLOR2:-3a8fd6}',
color3 = '${CONKY_COLOR3:-0d1117}',
color4 = '${CONKY_COLOR4:-1f6feb}',
color5 = '${CONKY_COLOR5:-58a6ff}',
color6 = '${CONKY_COLOR6:-79c0ff}',
color7 = '${CONKY_COLOR7:-c9d1d9}',
color8 = '${CONKY_COLOR8:-1f6feb}',
color9 = '${CONKY_COLOR9:-484f58}',
update_interval = 3,
total_run_times = 0,
cpu_avg_samples = 2,
no_buffers = true,
text_buffer_size = 8192,
short_units = true,
};
conky.text = [[
\${execpi 5 ~/.config/conky/conky-opsec-status.sh}
]];
EOF
# Launch
conky -c "$CONF" &
disown
echo "OPSEC widget launched: $ALIGN (theme: $THEME)"
+120
View File
@@ -0,0 +1,120 @@
#!/bin/bash
# /usr/local/bin/opsec-wifi-check.sh — Wireless Evil Twin Detection
# Periodic scan for AP changes, open networks, BSSID anomalies
# Usage: opsec-wifi-check.sh [scan|watch]
set -euo pipefail
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
else
opsec_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
opsec_red() { echo -e "\033[38;5;196m[-] $*\033[0m"; }
opsec_yellow() { echo -e "\033[38;5;214m[*] $*\033[0m"; }
opsec_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
opsec_cyan() { echo -e "\033[38;5;51m[>] $*\033[0m"; }
opsec_hdr() { echo -e "\033[38;5;51m━━━ \033[38;5;201m$*\033[38;5;51m ━━━\033[0m"; }
fi
STATE_DIR="/var/run/opsec-wifi"
mkdir -p "$STATE_DIR"
notify() {
local msg="$1" urgency="${2:-normal}"
local real_user="${SUDO_USER:-$USER}"
su - "$real_user" -c "DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u "$real_user")/bus notify-send -u '$urgency' 'OPSEC WiFi' '$msg'" 2>/dev/null || true
}
do_scan() {
opsec_hdr "WIRELESS SECURITY SCAN"
echo ""
# Check if connected to WiFi
local connected_ssid connected_bssid connected_security
connected_ssid=$(nmcli -t -f active,ssid dev wifi 2>/dev/null | grep '^yes:' | cut -d: -f2)
connected_bssid=$(nmcli -t -f active,bssid dev wifi 2>/dev/null | grep '^yes:' | cut -d: -f2-)
connected_security=$(nmcli -t -f active,security dev wifi 2>/dev/null | grep '^yes:' | cut -d: -f2)
if [ -z "$connected_ssid" ]; then
opsec_info "Not connected to any WiFi network"
return
fi
opsec_cyan "Connected: ${connected_ssid} (${connected_bssid})"
echo ""
# ─── Check for open/unencrypted network ────────────────────────────────
if [ -z "$connected_security" ] || [ "$connected_security" = "--" ]; then
opsec_red "WARNING: Connected to OPEN (unencrypted) network!"
notify "Connected to OPEN WiFi: ${connected_ssid}" "critical"
elif echo "$connected_security" | grep -qi 'WEP'; then
opsec_red "WARNING: WEP encryption (trivially crackable)"
notify "WiFi using WEP: ${connected_ssid}" "critical"
else
opsec_green "Encryption: ${connected_security}"
fi
# ─── Scan for duplicate SSIDs (evil twin indicators) ───────────────────
opsec_info "Scanning for duplicate SSIDs..."
local scan_results
scan_results=$(nmcli -t -f ssid,bssid,signal,security dev wifi list --rescan yes 2>/dev/null || true)
if [ -n "$scan_results" ]; then
# Find APs with same SSID as connected but different BSSID
local duplicates
duplicates=$(echo "$scan_results" | grep "^${connected_ssid}:" | grep -v "${connected_bssid}" || true)
if [ -n "$duplicates" ]; then
local dup_count
dup_count=$(echo "$duplicates" | wc -l)
opsec_yellow "ALERT: ${dup_count} other AP(s) broadcasting '${connected_ssid}':"
echo "$duplicates" | while IFS=: read -r ssid bssid signal security; do
echo -e " \033[38;5;214m BSSID: ${bssid} Signal: ${signal} Security: ${security}\033[0m"
done
notify "Evil twin risk: ${dup_count} duplicate AP(s) for ${connected_ssid}" "critical"
else
opsec_green "No duplicate SSIDs detected"
fi
fi
# ─── BSSID change detection ────────────────────────────────────────────
local state_file="${STATE_DIR}/${connected_ssid}.bssid"
if [ -f "$state_file" ]; then
local prev_bssid
prev_bssid=$(cat "$state_file")
if [ "$prev_bssid" != "$connected_bssid" ]; then
opsec_red "BSSID CHANGED for '${connected_ssid}'!"
opsec_red " Previous: ${prev_bssid}"
opsec_red " Current: ${connected_bssid}"
notify "BSSID changed for ${connected_ssid}: ${prev_bssid}${connected_bssid}" "critical"
else
opsec_green "BSSID consistent with last scan"
fi
fi
echo "$connected_bssid" > "$state_file"
echo ""
}
do_watch() {
opsec_info "Starting continuous WiFi monitoring (Ctrl+C to stop)..."
while true; do
do_scan
echo ""
opsec_info "Next scan in 60 seconds..."
sleep 60
done
}
case "${1:-scan}" in
scan) do_scan ;;
watch) do_watch ;;
*)
echo "Usage: $(basename "$0") [scan|watch]"
echo ""
echo " scan — One-time wireless security scan (default)"
echo " watch — Continuous monitoring (60s interval)"
exit 1
;;
esac