Initial release: ghost_protocol privacy toolkit
This commit is contained in:
Executable
+35
@@ -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
|
||||
Executable
+320
@@ -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
|
||||
Executable
+82
@@ -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
|
||||
Executable
+76
@@ -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 ====="
|
||||
Executable
+1044
File diff suppressed because it is too large
Load Diff
Executable
+377
@@ -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
|
||||
Executable
+64
@@ -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}"
|
||||
Executable
+261
@@ -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
|
||||
Executable
+97
@@ -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"
|
||||
Executable
+25
@@ -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
|
||||
Executable
+1210
File diff suppressed because it is too large
Load Diff
Executable
+119
@@ -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
|
||||
Executable
+291
@@ -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
|
||||
Executable
+115
@@ -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
|
||||
Executable
+123
@@ -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 ""
|
||||
Executable
+23
@@ -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
|
||||
Executable
+180
@@ -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
|
||||
Executable
+127
@@ -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)"
|
||||
Executable
+120
@@ -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
|
||||
Reference in New Issue
Block a user