- Create secubox-app-vhost-manager package for unified vhost orchestration - Single CLI tool (secubox-vhost) manages HAProxy, DNS, Tor, Mesh, mitmproxy - Unified UCI config (/etc/config/vhosts) as single source of truth - Backend adapters for each component (haproxy.sh, dns.sh, tor.sh, mesh.sh, mitmproxy.sh) - Centralized backend resolution function (backends.sh) - Import tool for existing HAProxy vhosts - Validation of backend reachability before creation Also includes: - FAQ-TROUBLESHOOTING.md with LXC cgroup v1/v2 fixes - Fix mitmproxyctl cgroup v1 -> v2 syntax for container compatibility - HAProxy backend resolution bugfixes CLI commands: secubox-vhost add <domain> <service> <port> [--ssl] [--tor] [--mesh] secubox-vhost remove/list/status/enable/disable/set/sync/validate/import Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
281 lines
7.6 KiB
Bash
281 lines
7.6 KiB
Bash
#!/bin/sh
|
|
# SecuBox Threat Analyst RPCD Handler
|
|
|
|
. /usr/share/libubox/jshn.sh
|
|
|
|
CONFIG="threat-analyst"
|
|
STATE_DIR="/var/lib/threat-analyst"
|
|
LIB_DIR="/usr/lib/threat-analyst"
|
|
|
|
log_info() { logger -t threat-analyst-rpcd "$*"; }
|
|
|
|
uci_get() { uci -q get "${CONFIG}.$1"; }
|
|
|
|
# Source libraries for analysis functions
|
|
[ -f "$LIB_DIR/analyzer.sh" ] && {
|
|
localai_url=$(uci_get main.localai_url)
|
|
localai_model=$(uci_get main.localai_model)
|
|
[ -z "$localai_url" ] && localai_url="http://127.0.0.1:8081"
|
|
[ -z "$localai_model" ] && localai_model="tinyllama-1.1b-chat-v1.0.Q4_K_M"
|
|
. "$LIB_DIR/analyzer.sh"
|
|
. "$LIB_DIR/appliers.sh"
|
|
}
|
|
|
|
case "$1" in
|
|
list)
|
|
cat <<'EOF'
|
|
{
|
|
"status": {},
|
|
"get_threats": {"limit": 50},
|
|
"get_alerts": {"limit": 20},
|
|
"get_pending": {},
|
|
"chat": {"message": "string"},
|
|
"analyze": {},
|
|
"generate_rules": {"target": "string"},
|
|
"approve_rule": {"id": "string"},
|
|
"reject_rule": {"id": "string"},
|
|
"run_cycle": {}
|
|
}
|
|
EOF
|
|
;;
|
|
|
|
call)
|
|
case "$2" in
|
|
status)
|
|
# Get agent status
|
|
enabled=$(uci_get main.enabled)
|
|
interval=$(uci_get main.interval)
|
|
last_run=""
|
|
[ -f "$STATE_DIR/last_run" ] && last_run=$(cat "$STATE_DIR/last_run")
|
|
|
|
# Check LocalAI
|
|
localai_status="offline"
|
|
wget -q -O /dev/null --timeout=2 "${localai_url}/v1/models" 2>/dev/null && localai_status="online"
|
|
|
|
# Check daemon
|
|
daemon_running="false"
|
|
pgrep -f "threat-analyst daemon" >/dev/null 2>&1 && daemon_running="true"
|
|
|
|
# Count pending
|
|
pending_count=0
|
|
[ -f "$STATE_DIR/pending_rules.json" ] && \
|
|
pending_count=$(jsonfilter -i "$STATE_DIR/pending_rules.json" -e '@[*]' 2>/dev/null | wc -l)
|
|
|
|
# Count recent threats and CVE alerts
|
|
threat_count=0
|
|
cve_count=0
|
|
if command -v cscli >/dev/null 2>&1; then
|
|
alerts_json=$(cscli alerts list -o json --since 1h 2>/dev/null)
|
|
threat_count=$(echo "$alerts_json" | jsonfilter -e '@[*]' 2>/dev/null | wc -l)
|
|
# Count CVE-related alerts
|
|
cve_count=$(echo "$alerts_json" | grep -ic 'cve-' 2>/dev/null)
|
|
[ -z "$cve_count" ] && cve_count=0
|
|
fi
|
|
|
|
cat <<EOF
|
|
{
|
|
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
|
"daemon_running": $daemon_running,
|
|
"interval": ${interval:-300},
|
|
"last_run": "$last_run",
|
|
"localai_status": "$localai_status",
|
|
"localai_url": "$localai_url",
|
|
"pending_rules": $pending_count,
|
|
"recent_threats": $threat_count,
|
|
"cve_alerts": $cve_count
|
|
}
|
|
EOF
|
|
;;
|
|
|
|
get_threats)
|
|
read -r input
|
|
limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null)
|
|
[ -z "$limit" ] && limit=50
|
|
|
|
# Get CrowdSec alerts
|
|
alerts='[]'
|
|
if command -v cscli >/dev/null 2>&1; then
|
|
alerts=$(cscli alerts list -o json --limit "$limit" 2>/dev/null || echo '[]')
|
|
fi
|
|
|
|
printf '{"threats":%s}' "$alerts"
|
|
;;
|
|
|
|
get_alerts)
|
|
read -r input
|
|
limit=$(echo "$input" | jsonfilter -e '@.limit' 2>/dev/null)
|
|
[ -z "$limit" ] && limit=20
|
|
|
|
# Get mitmproxy threats
|
|
threats='[]'
|
|
log_file="/srv/mitmproxy/threats.log"
|
|
if [ -f "$log_file" ]; then
|
|
# Build JSON array without jq
|
|
threats=$({
|
|
printf '['
|
|
first=1
|
|
tail -n "$limit" "$log_file" 2>/dev/null | while IFS= read -r line; do
|
|
# Skip empty or invalid JSON
|
|
echo "$line" | jsonfilter -e '@' >/dev/null 2>&1 || continue
|
|
[ $first -eq 0 ] && printf ','
|
|
first=0
|
|
printf '%s' "$line"
|
|
done
|
|
printf ']'
|
|
})
|
|
fi
|
|
|
|
printf '{"alerts":%s}' "$threats"
|
|
;;
|
|
|
|
get_pending)
|
|
pending='[]'
|
|
[ -f "$STATE_DIR/pending_rules.json" ] && pending=$(cat "$STATE_DIR/pending_rules.json")
|
|
printf '{"pending":%s}' "$pending"
|
|
;;
|
|
|
|
chat)
|
|
read -r input
|
|
message=$(echo "$input" | jsonfilter -e '@.message' 2>/dev/null)
|
|
|
|
if [ -z "$message" ]; then
|
|
echo '{"error":"No message provided"}'
|
|
exit 0
|
|
fi
|
|
|
|
# Check LocalAI
|
|
if ! wget -q -O /dev/null --timeout=2 "${localai_url}/v1/models" 2>/dev/null; then
|
|
echo '{"error":"LocalAI not available","suggestion":"Start LocalAI: localaictl start"}'
|
|
exit 0
|
|
fi
|
|
|
|
# Get threat context
|
|
threat_context=""
|
|
if command -v cscli >/dev/null 2>&1; then
|
|
threat_context=$(cscli alerts list -o json --limit 10 2>/dev/null | head -c 3000)
|
|
fi
|
|
|
|
# Build prompt
|
|
prompt="You are SecuBox Threat Analyst, an AI security assistant for an OpenWrt router.
|
|
|
|
Current security context:
|
|
- Recent CrowdSec alerts: $threat_context
|
|
|
|
User message: $message
|
|
|
|
Provide helpful, actionable security advice. If asked about threats, analyze the context. If asked to generate rules, provide specific patterns for mitmproxy/CrowdSec/WAF."
|
|
|
|
# Call LocalAI
|
|
request=$(cat <<AIEOF
|
|
{"model":"$localai_model","messages":[{"role":"system","content":"You are SecuBox Threat Analyst AI."},{"role":"user","content":"$(echo "$prompt" | sed 's/"/\\"/g' | tr '\n' ' ')"}],"max_tokens":1024,"temperature":0.5}
|
|
AIEOF
|
|
)
|
|
|
|
response=$(echo "$request" | wget -q -O - --post-data=- \
|
|
--header="Content-Type: application/json" \
|
|
"${localai_url}/v1/chat/completions" 2>/dev/null)
|
|
|
|
if [ -n "$response" ]; then
|
|
content=$(echo "$response" | jsonfilter -e '@.choices[0].message.content' 2>/dev/null)
|
|
# Escape for JSON
|
|
content=$(echo "$content" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ')
|
|
printf '{"response":"%s"}' "$content"
|
|
else
|
|
echo '{"error":"AI query failed"}'
|
|
fi
|
|
;;
|
|
|
|
analyze)
|
|
# Run analysis without rule generation
|
|
threats=$(collect_threats 2>/dev/null)
|
|
analysis=$(analyze_threats "$threats" 2>/dev/null)
|
|
|
|
if [ -n "$analysis" ]; then
|
|
escaped=$(echo "$analysis" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ')
|
|
printf '{"analysis":"%s"}' "$escaped"
|
|
else
|
|
echo '{"error":"Analysis failed"}'
|
|
fi
|
|
;;
|
|
|
|
generate_rules)
|
|
read -r input
|
|
target=$(echo "$input" | jsonfilter -e '@.target' 2>/dev/null)
|
|
[ -z "$target" ] && target="all"
|
|
|
|
# Source generators
|
|
[ -f "$LIB_DIR/generators.sh" ] && . "$LIB_DIR/generators.sh"
|
|
|
|
threats=$(collect_threats 2>/dev/null)
|
|
analysis=$(analyze_threats "$threats" 2>/dev/null)
|
|
|
|
result='{"rules":{'
|
|
first=1
|
|
|
|
if [ "$target" = "all" ] || [ "$target" = "mitmproxy" ]; then
|
|
[ $first -eq 0 ] && result="${result},"
|
|
first=0
|
|
mitm=$(generate_mitmproxy_filters "$analysis" "$threats" 2>/dev/null | base64 -w 0)
|
|
result="${result}\"mitmproxy\":\"$mitm\""
|
|
fi
|
|
|
|
if [ "$target" = "all" ] || [ "$target" = "crowdsec" ]; then
|
|
[ $first -eq 0 ] && result="${result},"
|
|
first=0
|
|
cs=$(generate_crowdsec_scenario "$analysis" "$threats" 2>/dev/null | base64 -w 0)
|
|
result="${result}\"crowdsec\":\"$cs\""
|
|
fi
|
|
|
|
if [ "$target" = "all" ] || [ "$target" = "waf" ]; then
|
|
[ $first -eq 0 ] && result="${result},"
|
|
first=0
|
|
waf=$(generate_waf_rules "$analysis" "$threats" 2>/dev/null | base64 -w 0)
|
|
result="${result}\"waf\":\"$waf\""
|
|
fi
|
|
|
|
result="${result}}}"
|
|
echo "$result"
|
|
;;
|
|
|
|
approve_rule)
|
|
read -r input
|
|
rule_id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null)
|
|
|
|
if [ -z "$rule_id" ]; then
|
|
echo '{"error":"No rule ID provided"}'
|
|
exit 0
|
|
fi
|
|
|
|
if approve_pending_rule "$rule_id" 2>/dev/null; then
|
|
echo '{"success":true}'
|
|
else
|
|
echo '{"success":false,"error":"Failed to approve rule"}'
|
|
fi
|
|
;;
|
|
|
|
reject_rule)
|
|
read -r input
|
|
rule_id=$(echo "$input" | jsonfilter -e '@.id' 2>/dev/null)
|
|
|
|
if [ -z "$rule_id" ]; then
|
|
echo '{"error":"No rule ID provided"}'
|
|
exit 0
|
|
fi
|
|
|
|
reject_pending_rule "$rule_id" 2>/dev/null
|
|
echo '{"success":true}'
|
|
;;
|
|
|
|
run_cycle)
|
|
# Trigger analysis cycle
|
|
/usr/bin/threat-analyst run >/dev/null 2>&1 &
|
|
echo '{"started":true}'
|
|
;;
|
|
|
|
*)
|
|
echo '{"error":"Unknown method"}'
|
|
;;
|
|
esac
|
|
;;
|
|
esac
|