fix(waf): Add LuCI whitelist and moderate sensitivity mode
- Add TRUSTED_PATH_PREFIXES for LuCI, ubus, and CGI paths - Fix moderate mode to always require threshold (3 attempts in 5 min) instead of immediate ban on critical threats - Add WireGuard endpoint whitelist support to prevent VPN peer bans - New script: mitmproxy-sync-wg-endpoints extracts peer IPs from UCI - Bump version to v2.4 Prevents accidental bans from legitimate external LuCI login attempts. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2c7b92219e
commit
ee9a54b0a5
@ -30,6 +30,28 @@ AUTOBAN_FILE = "/data/autoban-requests.log"
|
|||||||
AUTOBAN_CONFIG = "/data/autoban.json"
|
AUTOBAN_CONFIG = "/data/autoban.json"
|
||||||
# Subdomain metrics file
|
# Subdomain metrics file
|
||||||
SUBDOMAIN_METRICS_FILE = "/tmp/secubox-subdomain-metrics.json"
|
SUBDOMAIN_METRICS_FILE = "/tmp/secubox-subdomain-metrics.json"
|
||||||
|
# WireGuard endpoints file (written by host from UCI wireguard config)
|
||||||
|
# Contains public IPs of WireGuard peers that should never be banned
|
||||||
|
WIREGUARD_ENDPOINTS_FILE = "/data/wireguard-endpoints.json"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# TRUSTED PATH WHITELIST - Skip threat detection for legitimate management
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Paths that should NOT trigger threat detection (legitimate admin interfaces)
|
||||||
|
TRUSTED_PATH_PREFIXES = [
|
||||||
|
'/cgi-bin/luci', # OpenWrt LuCI web interface
|
||||||
|
'/luci-static/', # LuCI static assets
|
||||||
|
'/ubus/', # OpenWrt ubus API
|
||||||
|
'/cgi-bin/cgi-', # OpenWrt CGI scripts
|
||||||
|
]
|
||||||
|
|
||||||
|
# Hosts that should have relaxed detection (local management)
|
||||||
|
TRUSTED_HOSTS = [
|
||||||
|
'localhost',
|
||||||
|
'127.0.0.1',
|
||||||
|
'192.168.255.1', # Default SecuBox LAN IP
|
||||||
|
]
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# THREAT DETECTION PATTERNS
|
# THREAT DETECTION PATTERNS
|
||||||
@ -489,6 +511,7 @@ class SecuBoxAnalytics:
|
|||||||
self.blocked_ips = set()
|
self.blocked_ips = set()
|
||||||
self.autoban_config = {}
|
self.autoban_config = {}
|
||||||
self.autoban_requested = set() # Track IPs we've already requested to ban
|
self.autoban_requested = set() # Track IPs we've already requested to ban
|
||||||
|
self.wireguard_endpoints = set() # WireGuard peer endpoint IPs (never ban)
|
||||||
# Attempt tracking for sensitivity-based auto-ban
|
# Attempt tracking for sensitivity-based auto-ban
|
||||||
# Structure: {ip: [(timestamp, severity, reason), ...]}
|
# Structure: {ip: [(timestamp, severity, reason), ...]}
|
||||||
self.threat_attempts = defaultdict(list)
|
self.threat_attempts = defaultdict(list)
|
||||||
@ -508,7 +531,8 @@ class SecuBoxAnalytics:
|
|||||||
self._load_geoip()
|
self._load_geoip()
|
||||||
self._load_blocked_ips()
|
self._load_blocked_ips()
|
||||||
self._load_autoban_config()
|
self._load_autoban_config()
|
||||||
ctx.log.info("SecuBox Analytics addon v2.3 loaded - Enhanced threat detection with subdomain metrics")
|
self._load_wireguard_endpoints()
|
||||||
|
ctx.log.info("SecuBox Analytics addon v2.4 loaded - Enhanced threat detection with WireGuard protection")
|
||||||
|
|
||||||
def _load_geoip(self):
|
def _load_geoip(self):
|
||||||
"""Load GeoIP database if available"""
|
"""Load GeoIP database if available"""
|
||||||
@ -694,6 +718,26 @@ class SecuBoxAnalytics:
|
|||||||
ctx.log.warn(f"Could not load auto-ban config: {e}")
|
ctx.log.warn(f"Could not load auto-ban config: {e}")
|
||||||
self.autoban_config = {'enabled': False}
|
self.autoban_config = {'enabled': False}
|
||||||
|
|
||||||
|
def _load_wireguard_endpoints(self):
|
||||||
|
"""
|
||||||
|
Load WireGuard peer endpoint IPs from config file.
|
||||||
|
These IPs are NEVER banned to ensure VPN tunnel connectivity.
|
||||||
|
|
||||||
|
The host should write this file from UCI wireguard config:
|
||||||
|
/data/wireguard-endpoints.json = {"endpoints": ["1.2.3.4", "5.6.7.8"]}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(WIREGUARD_ENDPOINTS_FILE):
|
||||||
|
with open(WIREGUARD_ENDPOINTS_FILE, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
endpoints = data.get('endpoints', [])
|
||||||
|
if isinstance(endpoints, list):
|
||||||
|
self.wireguard_endpoints = set(endpoints)
|
||||||
|
if self.wireguard_endpoints:
|
||||||
|
ctx.log.info(f"WireGuard endpoints loaded: {len(self.wireguard_endpoints)} IPs protected from auto-ban")
|
||||||
|
except Exception as e:
|
||||||
|
ctx.log.debug(f"Could not load WireGuard endpoints: {e}")
|
||||||
|
|
||||||
def _clean_old_attempts(self, ip: str, window: int):
|
def _clean_old_attempts(self, ip: str, window: int):
|
||||||
"""Remove attempts older than the window for an IP"""
|
"""Remove attempts older than the window for an IP"""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@ -744,7 +788,12 @@ class SecuBoxAnalytics:
|
|||||||
if ip in whitelist:
|
if ip in whitelist:
|
||||||
return False, ''
|
return False, ''
|
||||||
|
|
||||||
# Skip local IPs
|
# Skip WireGuard peer endpoint IPs (critical for VPN connectivity)
|
||||||
|
if ip in self.wireguard_endpoints:
|
||||||
|
return False, ''
|
||||||
|
|
||||||
|
# Skip local/private IPs (includes WireGuard tunnels which typically use 10.x.x.x)
|
||||||
|
# RFC 1918 private ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
|
||||||
if ip.startswith(('10.', '172.16.', '172.17.', '172.18.', '172.19.',
|
if ip.startswith(('10.', '172.16.', '172.17.', '172.18.', '172.19.',
|
||||||
'172.20.', '172.21.', '172.22.', '172.23.', '172.24.',
|
'172.20.', '172.21.', '172.22.', '172.23.', '172.24.',
|
||||||
'172.25.', '172.26.', '172.27.', '172.28.', '172.29.',
|
'172.25.', '172.26.', '172.27.', '172.28.', '172.29.',
|
||||||
@ -866,10 +915,9 @@ class SecuBoxAnalytics:
|
|||||||
# Permissive: always require threshold to be met
|
# Permissive: always require threshold to be met
|
||||||
return self._check_threshold(ip, threshold, window)
|
return self._check_threshold(ip, threshold, window)
|
||||||
|
|
||||||
else: # moderate
|
else: # moderate (default)
|
||||||
# Moderate: critical threats ban immediately, others follow threshold
|
# Moderate: ALWAYS require threshold to be met, never immediate ban
|
||||||
if is_critical_threat:
|
# This prevents accidental bans from single requests (e.g., legitimate LuCI login)
|
||||||
return True, threat_reason
|
|
||||||
return self._check_threshold(ip, threshold, window)
|
return self._check_threshold(ip, threshold, window)
|
||||||
|
|
||||||
def _request_autoban(self, ip: str, reason: str, severity: str = 'high'):
|
def _request_autoban(self, ip: str, reason: str, severity: str = 'high'):
|
||||||
@ -974,8 +1022,29 @@ class SecuBoxAnalytics:
|
|||||||
'device': device
|
'device': device
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _is_trusted_path(self, request: http.Request) -> bool:
|
||||||
|
"""Check if request path is in trusted whitelist (e.g., LuCI management)"""
|
||||||
|
path = request.path.lower()
|
||||||
|
host = request.host.lower() if request.host else ''
|
||||||
|
|
||||||
|
# Check trusted hosts
|
||||||
|
for trusted_host in TRUSTED_HOSTS:
|
||||||
|
if trusted_host in host:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check trusted path prefixes
|
||||||
|
for prefix in TRUSTED_PATH_PREFIXES:
|
||||||
|
if path.startswith(prefix.lower()):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def _detect_bot_behavior(self, request: http.Request) -> dict:
|
def _detect_bot_behavior(self, request: http.Request) -> dict:
|
||||||
"""Detect bot-like behavior based on request patterns"""
|
"""Detect bot-like behavior based on request patterns"""
|
||||||
|
# Skip detection for trusted management paths
|
||||||
|
if self._is_trusted_path(request):
|
||||||
|
return {'is_bot_behavior': False, 'behavior_type': None, 'pattern': None, 'severity': None}
|
||||||
|
|
||||||
path = request.path.lower()
|
path = request.path.lower()
|
||||||
|
|
||||||
for pattern in BOT_BEHAVIOR_PATHS:
|
for pattern in BOT_BEHAVIOR_PATHS:
|
||||||
@ -1021,6 +1090,10 @@ class SecuBoxAnalytics:
|
|||||||
|
|
||||||
def _detect_scan(self, request: http.Request) -> dict:
|
def _detect_scan(self, request: http.Request) -> dict:
|
||||||
"""Comprehensive threat detection with categorized patterns"""
|
"""Comprehensive threat detection with categorized patterns"""
|
||||||
|
# Skip path-based detection for trusted management paths (LuCI, etc.)
|
||||||
|
# Still check for injection attacks in body/params as those are dangerous everywhere
|
||||||
|
is_trusted = self._is_trusted_path(request)
|
||||||
|
|
||||||
path = request.path.lower()
|
path = request.path.lower()
|
||||||
full_url = request.pretty_url.lower()
|
full_url = request.pretty_url.lower()
|
||||||
query = request.query
|
query = request.query
|
||||||
@ -1046,13 +1119,14 @@ class SecuBoxAnalytics:
|
|||||||
combined = ' '.join(search_targets)
|
combined = ' '.join(search_targets)
|
||||||
threats = []
|
threats = []
|
||||||
|
|
||||||
# Check path-based scans
|
# Check path-based scans (skip for trusted paths like LuCI)
|
||||||
for pattern in PATH_SCAN_PATTERNS:
|
if not is_trusted:
|
||||||
if re.search(pattern, path, re.IGNORECASE):
|
for pattern in PATH_SCAN_PATTERNS:
|
||||||
return {
|
if re.search(pattern, path, re.IGNORECASE):
|
||||||
'is_scan': True, 'pattern': pattern, 'type': 'path_scan',
|
return {
|
||||||
'severity': 'medium', 'category': 'reconnaissance'
|
'is_scan': True, 'pattern': pattern, 'type': 'path_scan',
|
||||||
}
|
'severity': 'medium', 'category': 'reconnaissance'
|
||||||
|
}
|
||||||
|
|
||||||
# Check SQL Injection
|
# Check SQL Injection
|
||||||
for pattern in SQL_INJECTION_PATTERNS:
|
for pattern in SQL_INJECTION_PATTERNS:
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Sync WireGuard peer endpoints to mitmproxy WAF whitelist
|
||||||
|
# This ensures VPN peers are never banned by the WAF
|
||||||
|
#
|
||||||
|
# Run this:
|
||||||
|
# - On boot (via init script)
|
||||||
|
# - When WireGuard config changes (via UCI hook)
|
||||||
|
# - Periodically (via cron)
|
||||||
|
|
||||||
|
ENDPOINTS_FILE="/srv/mitmproxy/wireguard-endpoints.json"
|
||||||
|
|
||||||
|
# Extract all WireGuard peer endpoints from UCI
|
||||||
|
get_wg_endpoints() {
|
||||||
|
local endpoints=""
|
||||||
|
|
||||||
|
# Get all wireguard interfaces
|
||||||
|
for iface in $(uci show network 2>/dev/null | grep "proto='wireguard'" | cut -d. -f2); do
|
||||||
|
# Get peers for this interface
|
||||||
|
for peer in $(uci show network 2>/dev/null | grep "network\.@wireguard_${iface}\[" | grep "endpoint_host" | cut -d= -f1); do
|
||||||
|
endpoint=$(uci -q get "$peer" 2>/dev/null | cut -d: -f1)
|
||||||
|
if [ -n "$endpoint" ]; then
|
||||||
|
# Skip if it's a hostname (contains letters)
|
||||||
|
case "$endpoint" in
|
||||||
|
*[a-zA-Z]*)
|
||||||
|
# Resolve hostname to IP
|
||||||
|
resolved=$(nslookup "$endpoint" 2>/dev/null | grep "Address" | tail -1 | awk '{print $2}')
|
||||||
|
if [ -n "$resolved" ] && [ "$resolved" != "#53" ]; then
|
||||||
|
endpoint="$resolved"
|
||||||
|
else
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -n "$endpoints" ]; then
|
||||||
|
endpoints="$endpoints, \"$endpoint\""
|
||||||
|
else
|
||||||
|
endpoints="\"$endpoint\""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Also check direct endpoint_host in wireguard peer sections
|
||||||
|
for peer in $(uci show network 2>/dev/null | grep "\.endpoint_host=" | cut -d= -f1); do
|
||||||
|
endpoint=$(uci -q get "$peer" 2>/dev/null | cut -d: -f1)
|
||||||
|
if [ -n "$endpoint" ]; then
|
||||||
|
case "$endpoint" in
|
||||||
|
*[a-zA-Z]*)
|
||||||
|
resolved=$(nslookup "$endpoint" 2>/dev/null | grep "Address" | tail -1 | awk '{print $2}')
|
||||||
|
if [ -n "$resolved" ] && [ "$resolved" != "#53" ]; then
|
||||||
|
endpoint="$resolved"
|
||||||
|
else
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check if already in list
|
||||||
|
case "$endpoints" in
|
||||||
|
*"$endpoint"*) ;;
|
||||||
|
*)
|
||||||
|
if [ -n "$endpoints" ]; then
|
||||||
|
endpoints="$endpoints, \"$endpoint\""
|
||||||
|
else
|
||||||
|
endpoints="\"$endpoint\""
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "$endpoints"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main
|
||||||
|
endpoints=$(get_wg_endpoints)
|
||||||
|
|
||||||
|
# Write JSON file
|
||||||
|
cat > "$ENDPOINTS_FILE" << EOF
|
||||||
|
{
|
||||||
|
"updated": "$(date -Iseconds)",
|
||||||
|
"endpoints": [$endpoints]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Count endpoints
|
||||||
|
if [ -n "$endpoints" ]; then
|
||||||
|
count=$(echo "$endpoints" | tr ',' '\n' | wc -l)
|
||||||
|
else
|
||||||
|
count=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
logger -t mitmproxy-wg "Synced $count WireGuard endpoint(s) to WAF whitelist"
|
||||||
|
|
||||||
|
# If verbose mode
|
||||||
|
[ "$1" = "-v" ] && cat "$ENDPOINTS_FILE"
|
||||||
Loading…
Reference in New Issue
Block a user