New packages: - secubox-threat-analyst: AI-powered threat analysis with CrowdSec integration - luci-app-threat-analyst: LuCI dashboard for threat intelligence - secubox-dns-guard: DNS security monitoring and blocking - secubox-mcp-server: Model Context Protocol server for AI assistant integration Enhancements: - dns-provider: Add DynDNS support (dyndns, get, update, domains commands) - gandi.sh: Full DynDNS with WAN IP detection and record updates - luci-app-dnsguard: Upgrade to v1.1.0 with improved dashboard Infrastructure: - BIND9 DNS setup for secubox.in with CAA records - Wildcard SSL certificates via DNS-01 challenge - HAProxy config fixes for secubox.in subdomains - Mail server setup with Roundcube webmail Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
403 lines
11 KiB
Bash
403 lines
11 KiB
Bash
#!/bin/sh
|
|
# SecuBox DNS Guard - AI-Powered DNS Anomaly Detection
|
|
# Copyright (C) 2026 CyberMind.fr
|
|
#
|
|
# Monitors DNS queries for anomalies using ML pattern detection
|
|
# and LocalAI for intelligent threat analysis
|
|
|
|
CONFIG="dns-guard"
|
|
LIB_DIR="/usr/lib/dns-guard"
|
|
STATE_DIR="/var/lib/dns-guard"
|
|
BLOCKLIST_DIR="/etc/dns-guard/blocklists"
|
|
LOG_TAG="dns-guard"
|
|
|
|
# Source libraries
|
|
. "$LIB_DIR/analyzer.sh"
|
|
. "$LIB_DIR/detector.sh"
|
|
. "$LIB_DIR/blocklist.sh"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: dns-guard <command> [options]
|
|
|
|
Commands:
|
|
run Run single analysis cycle
|
|
daemon Run as background daemon
|
|
status Show agent status and statistics
|
|
analyze Analyze recent DNS queries (no blocking)
|
|
detect Run all detectors on recent queries
|
|
|
|
Block Management:
|
|
list-pending List pending blocks awaiting approval
|
|
approve <id> Approve pending block
|
|
reject <id> Reject pending block
|
|
approve-all Approve all pending blocks
|
|
clear-pending Clear all pending blocks
|
|
show-blocklist Show current AI-generated blocklist
|
|
|
|
Query Analysis:
|
|
check <domain> Check if a domain is suspicious
|
|
stats Show DNS query statistics
|
|
top-domains Show top queried domains
|
|
top-clients Show top DNS clients
|
|
|
|
Configuration: /etc/config/dns-guard
|
|
EOF
|
|
}
|
|
|
|
log_info() { logger -t "$LOG_TAG" "$*"; echo "[INFO] $*"; }
|
|
log_warn() { logger -t "$LOG_TAG" -p warning "$*"; echo "[WARN] $*" >&2; }
|
|
log_error() { logger -t "$LOG_TAG" -p err "$*"; echo "[ERROR] $*" >&2; }
|
|
|
|
uci_get() { uci -q get "${CONFIG}.$1"; }
|
|
|
|
load_config() {
|
|
enabled=$(uci_get main.enabled)
|
|
interval=$(uci_get main.interval)
|
|
localai_url=$(uci_get main.localai_url)
|
|
localai_model=$(uci_get main.localai_model)
|
|
auto_apply_blocks=$(uci_get main.auto_apply_blocks)
|
|
min_confidence=$(uci_get main.min_confidence)
|
|
max_blocks=$(uci_get main.max_blocks_per_cycle)
|
|
alert_retention=$(uci_get main.alert_retention)
|
|
|
|
# Defaults
|
|
[ -z "$interval" ] && interval=60
|
|
[ -z "$min_confidence" ] && min_confidence=80
|
|
[ -z "$max_blocks" ] && max_blocks=10
|
|
[ -z "$alert_retention" ] && alert_retention=24
|
|
|
|
# Ensure state directories exist
|
|
mkdir -p "$STATE_DIR"
|
|
mkdir -p "$BLOCKLIST_DIR"
|
|
}
|
|
|
|
# =============================================================================
|
|
# COMMANDS
|
|
# =============================================================================
|
|
|
|
cmd_status() {
|
|
load_config
|
|
echo "=== DNS Guard Status ==="
|
|
echo ""
|
|
echo "Enabled: $([ "$enabled" = "1" ] && echo "Yes" || echo "No")"
|
|
echo "Interval: ${interval}s"
|
|
echo "LocalAI: $localai_url"
|
|
echo "Model: $localai_model"
|
|
echo ""
|
|
|
|
# Check LocalAI availability
|
|
if wget -q -O /dev/null --timeout=2 "${localai_url}/v1/models" 2>/dev/null; then
|
|
echo "LocalAI Status: ONLINE"
|
|
else
|
|
echo "LocalAI Status: OFFLINE"
|
|
fi
|
|
|
|
echo ""
|
|
echo "Auto-apply blocks: $([ "$auto_apply_blocks" = "1" ] && echo "Yes" || echo "No (queued)")"
|
|
echo "Min confidence: ${min_confidence}%"
|
|
echo "Max blocks/cycle: $max_blocks"
|
|
|
|
echo ""
|
|
echo "=== Detectors ==="
|
|
for detector in dga tunneling rate_anomaly known_bad tld_anomaly; do
|
|
local det_enabled=$(uci -q get "dns-guard.${detector}.enabled")
|
|
local det_desc=$(uci -q get "dns-guard.${detector}.description")
|
|
printf " %-15s %s (%s)\n" "$detector" \
|
|
"$([ "$det_enabled" = "1" ] && echo "[ENABLED]" || echo "[DISABLED]")" \
|
|
"$det_desc"
|
|
done
|
|
|
|
echo ""
|
|
|
|
# Count pending blocks
|
|
local pending_file="$STATE_DIR/pending_blocks.json"
|
|
if [ -f "$pending_file" ]; then
|
|
local count=$(jsonfilter -i "$pending_file" -e '@[*]' 2>/dev/null | wc -l)
|
|
echo "Pending blocks: $count"
|
|
else
|
|
echo "Pending blocks: 0"
|
|
fi
|
|
|
|
# Count active blocks
|
|
local blocklist_file=$(uci_get target_dnsmasq_blocklist.output_path)
|
|
[ -z "$blocklist_file" ] && blocklist_file="/etc/dnsmasq.d/dns-guard-blocklist.conf"
|
|
if [ -f "$blocklist_file" ]; then
|
|
local active=$(grep -c "^address=" "$blocklist_file" 2>/dev/null || echo "0")
|
|
echo "Active blocks: $active"
|
|
else
|
|
echo "Active blocks: 0"
|
|
fi
|
|
|
|
# Alert count (last 24h)
|
|
local alerts_file="$STATE_DIR/alerts.json"
|
|
if [ -f "$alerts_file" ]; then
|
|
local alert_count=$(jsonfilter -i "$alerts_file" -e '@[*]' 2>/dev/null | wc -l)
|
|
echo "Alerts (24h): $alert_count"
|
|
else
|
|
echo "Alerts (24h): 0"
|
|
fi
|
|
|
|
# Last run
|
|
if [ -f "$STATE_DIR/last_run" ]; then
|
|
echo ""
|
|
echo "Last run: $(cat "$STATE_DIR/last_run")"
|
|
fi
|
|
}
|
|
|
|
cmd_run() {
|
|
load_config
|
|
|
|
if [ "$enabled" != "1" ]; then
|
|
log_warn "DNS Guard disabled in config"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Starting analysis cycle..."
|
|
|
|
# 1. Collect DNS queries
|
|
local queries=$(collect_dns_queries)
|
|
local query_count=$(echo "$queries" | wc -l)
|
|
log_info "Collected $query_count DNS queries"
|
|
|
|
if [ "$query_count" -eq 0 ]; then
|
|
log_info "No new queries to analyze"
|
|
date > "$STATE_DIR/last_run"
|
|
return 0
|
|
fi
|
|
|
|
# 2. Run all enabled detectors
|
|
local anomalies=$(run_all_detectors "$queries")
|
|
local anomaly_count=$(echo "$anomalies" | jsonfilter -e '@[*]' 2>/dev/null | wc -l)
|
|
|
|
if [ "$anomaly_count" -eq 0 ]; then
|
|
log_info "No anomalies detected"
|
|
date > "$STATE_DIR/last_run"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Detected $anomaly_count anomalies"
|
|
|
|
# 3. Store alerts
|
|
store_alerts "$anomalies"
|
|
|
|
# 4. Analyze with LocalAI for blocking recommendations
|
|
local analysis=$(analyze_anomalies "$anomalies")
|
|
if [ -z "$analysis" ]; then
|
|
log_warn "AI analysis unavailable, using rule-based blocking only"
|
|
# Fall back to rule-based blocking for high-confidence detections
|
|
local rule_blocks=$(extract_high_confidence_blocks "$anomalies")
|
|
if [ -n "$rule_blocks" ]; then
|
|
process_blocks "$rule_blocks"
|
|
fi
|
|
else
|
|
log_info "AI analysis complete"
|
|
local ai_blocks=$(extract_ai_recommendations "$analysis")
|
|
process_blocks "$ai_blocks"
|
|
fi
|
|
|
|
date > "$STATE_DIR/last_run"
|
|
log_info "Analysis cycle complete"
|
|
}
|
|
|
|
process_blocks() {
|
|
local blocks="$1"
|
|
[ -z "$blocks" ] && return
|
|
|
|
local block_count=$(echo "$blocks" | jsonfilter -e '@[*]' 2>/dev/null | wc -l)
|
|
[ "$block_count" -eq 0 ] && return
|
|
|
|
# Limit blocks per cycle
|
|
if [ "$block_count" -gt "$max_blocks" ]; then
|
|
log_info "Limiting blocks to $max_blocks (found $block_count)"
|
|
blocks=$(echo "$blocks" | jsonfilter -e "@[0:$max_blocks]")
|
|
fi
|
|
|
|
if [ "$auto_apply_blocks" = "1" ]; then
|
|
apply_blocks "$blocks"
|
|
log_info "Applied $block_count domain blocks"
|
|
else
|
|
queue_blocks "$blocks"
|
|
log_info "Queued $block_count domain blocks for approval"
|
|
fi
|
|
}
|
|
|
|
cmd_daemon() {
|
|
load_config
|
|
|
|
if [ "$enabled" != "1" ]; then
|
|
log_error "DNS Guard disabled in config"
|
|
exit 1
|
|
fi
|
|
|
|
log_info "Starting daemon (interval: ${interval}s)"
|
|
|
|
# Cleanup old alerts on startup
|
|
cleanup_old_alerts
|
|
|
|
while true; do
|
|
cmd_run
|
|
sleep "$interval"
|
|
done
|
|
}
|
|
|
|
cmd_analyze() {
|
|
load_config
|
|
log_info "Analyzing DNS queries (no blocking)..."
|
|
|
|
local queries=$(collect_dns_queries)
|
|
local anomalies=$(run_all_detectors "$queries")
|
|
|
|
echo "=== DNS Query Analysis ==="
|
|
echo ""
|
|
echo "Total queries analyzed: $(echo "$queries" | wc -l)"
|
|
echo ""
|
|
|
|
local anomaly_count=$(echo "$anomalies" | jsonfilter -e '@[*]' 2>/dev/null | wc -l)
|
|
echo "Anomalies detected: $anomaly_count"
|
|
echo ""
|
|
|
|
if [ "$anomaly_count" -gt 0 ]; then
|
|
echo "=== Anomaly Details ==="
|
|
echo "$anomalies" | jsonfilter -e '@[*]' 2>/dev/null | while read -r anomaly; do
|
|
local domain=$(echo "$anomaly" | jsonfilter -e '@.domain' 2>/dev/null)
|
|
local type=$(echo "$anomaly" | jsonfilter -e '@.type' 2>/dev/null)
|
|
local confidence=$(echo "$anomaly" | jsonfilter -e '@.confidence' 2>/dev/null)
|
|
local reason=$(echo "$anomaly" | jsonfilter -e '@.reason' 2>/dev/null)
|
|
printf "[%s%%] %-12s %s\n" "$confidence" "$type" "$domain"
|
|
[ -n "$reason" ] && printf " Reason: %s\n" "$reason"
|
|
done
|
|
|
|
echo ""
|
|
echo "=== AI Analysis ==="
|
|
local analysis=$(analyze_anomalies "$anomalies")
|
|
if [ -n "$analysis" ]; then
|
|
echo "$analysis"
|
|
else
|
|
echo "(LocalAI not available)"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
cmd_detect() {
|
|
load_config
|
|
local queries=$(collect_dns_queries)
|
|
run_all_detectors "$queries"
|
|
}
|
|
|
|
cmd_check() {
|
|
local domain="$1"
|
|
[ -z "$domain" ] && { echo "Usage: dns-guard check <domain>"; return 1; }
|
|
|
|
load_config
|
|
echo "=== Domain Check: $domain ==="
|
|
echo ""
|
|
|
|
# Run each detector individually
|
|
local result=$(check_domain_all_detectors "$domain")
|
|
echo "$result"
|
|
|
|
# AI analysis for single domain
|
|
if wget -q -O /dev/null --timeout=2 "${localai_url}/v1/models" 2>/dev/null; then
|
|
echo ""
|
|
echo "=== AI Analysis ==="
|
|
local ai_result=$(analyze_single_domain "$domain")
|
|
echo "$ai_result"
|
|
fi
|
|
}
|
|
|
|
cmd_stats() {
|
|
load_config
|
|
show_query_stats
|
|
}
|
|
|
|
cmd_top_domains() {
|
|
load_config
|
|
local limit="${1:-20}"
|
|
show_top_domains "$limit"
|
|
}
|
|
|
|
cmd_top_clients() {
|
|
load_config
|
|
local limit="${1:-10}"
|
|
show_top_clients "$limit"
|
|
}
|
|
|
|
cmd_list_pending() {
|
|
local pending_file="$STATE_DIR/pending_blocks.json"
|
|
|
|
if [ ! -f "$pending_file" ]; then
|
|
echo "No pending blocks"
|
|
return 0
|
|
fi
|
|
|
|
echo "=== Pending Domain Blocks ==="
|
|
echo ""
|
|
jsonfilter -i "$pending_file" -e '@[*]' 2>/dev/null | while read -r block; do
|
|
local id=$(echo "$block" | jsonfilter -e '@.id' 2>/dev/null)
|
|
local domain=$(echo "$block" | jsonfilter -e '@.domain' 2>/dev/null)
|
|
local type=$(echo "$block" | jsonfilter -e '@.type' 2>/dev/null)
|
|
local confidence=$(echo "$block" | jsonfilter -e '@.confidence' 2>/dev/null)
|
|
local created=$(echo "$block" | jsonfilter -e '@.created' 2>/dev/null)
|
|
printf "[%s] %s (%s, %s%% confidence) - %s\n" "$id" "$domain" "$type" "$confidence" "$created"
|
|
done
|
|
}
|
|
|
|
cmd_approve() {
|
|
local block_id="$1"
|
|
[ -z "$block_id" ] && { echo "Usage: dns-guard approve <id>"; return 1; }
|
|
|
|
load_config
|
|
approve_pending_block "$block_id"
|
|
}
|
|
|
|
cmd_approve_all() {
|
|
load_config
|
|
approve_all_pending_blocks
|
|
}
|
|
|
|
cmd_reject() {
|
|
local block_id="$1"
|
|
[ -z "$block_id" ] && { echo "Usage: dns-guard reject <id>"; return 1; }
|
|
|
|
reject_pending_block "$block_id"
|
|
}
|
|
|
|
cmd_show_blocklist() {
|
|
local blocklist_file=$(uci_get target_dnsmasq_blocklist.output_path)
|
|
[ -z "$blocklist_file" ] && blocklist_file="/etc/dnsmasq.d/dns-guard-blocklist.conf"
|
|
|
|
if [ -f "$blocklist_file" ]; then
|
|
echo "=== DNS Guard Blocklist ==="
|
|
echo "File: $blocklist_file"
|
|
echo ""
|
|
cat "$blocklist_file"
|
|
else
|
|
echo "No blocklist file found"
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# MAIN
|
|
# =============================================================================
|
|
|
|
case "${1:-}" in
|
|
run) cmd_run ;;
|
|
daemon) cmd_daemon ;;
|
|
status) cmd_status ;;
|
|
analyze) cmd_analyze ;;
|
|
detect) cmd_detect ;;
|
|
check) shift; cmd_check "$@" ;;
|
|
stats) cmd_stats ;;
|
|
top-domains) shift; cmd_top_domains "$@" ;;
|
|
top-clients) shift; cmd_top_clients "$@" ;;
|
|
list-pending) cmd_list_pending ;;
|
|
approve) shift; cmd_approve "$@" ;;
|
|
approve-all) cmd_approve_all ;;
|
|
reject) shift; cmd_reject "$@" ;;
|
|
clear-pending) rm -f "$STATE_DIR/pending_blocks.json"; echo "Cleared" ;;
|
|
show-blocklist) cmd_show_blocklist ;;
|
|
help|--help|-h|"") usage ;;
|
|
*) echo "Unknown: $1" >&2; usage >&2; exit 1 ;;
|
|
esac
|