secubox-openwrt/package/secubox/secubox-app-device-intel/files/usr/lib/secubox/device-intel/functions.sh
CyberMind-FR f4157811c5 fix(device-intel): Fix empty vendor column and add OUI emoji display
- Fix wrong mac-guardian DB path (clients.db -> known.db) in 3 files
- Add standalone OUI vendor fallback for ARP/DHCP-only devices
- Expand oui.tsv from ~30 to 100+ entries (GL.iNet, Bosch, Samsung, Docker, etc.)
- Add vendorDisplay() with emoji prefixes for MAC types:
  container, virtual, randomized, IoT, mesh peer

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 18:33:10 +01:00

405 lines
13 KiB
Bash

#!/bin/sh
# SecuBox Device Intelligence — Core Aggregation Library
# Sources data from mac-guardian, client-guardian, DHCP, P2P, exposure, and emulators
. /lib/functions.sh
DI_CONFIG="device-intel"
DI_CACHE_DIR="/tmp/device-intel"
DI_CACHE_FILE="${DI_CACHE_DIR}/cache-devices.json"
DI_EMULATOR_DIR="/usr/lib/secubox/device-intel/emulators"
# ============================================================================
# Config helpers
# ============================================================================
di_uci_get() {
uci -q get ${DI_CONFIG}.$1
}
di_cache_valid() {
local ttl=$(di_uci_get main.cache_ttl)
ttl=${ttl:-30}
[ ! -f "$DI_CACHE_FILE" ] && return 1
local now=$(date +%s)
local mtime=$(date -r "$DI_CACHE_FILE" +%s 2>/dev/null)
[ -z "$mtime" ] && return 1
local age=$((now - mtime))
[ "$age" -lt "$ttl" ] && return 0
return 1
}
di_invalidate_cache() {
rm -f "${DI_CACHE_DIR}"/cache*.json 2>/dev/null
}
# ============================================================================
# Data Source Collectors
# ============================================================================
# Collect mac-guardian client data → pipe-delimited lines
# Format: mac|vendor|iface|hostname|status|randomized|first_seen|last_seen
di_collect_mac_guardian() {
local db="/var/run/mac-guardian/known.db"
local oui_db="/usr/lib/secubox/mac-guardian/oui.tsv"
[ ! -f "$db" ] && return
while IFS='|' read -r mac oui first_seen last_seen iface hostname status rest; do
[ -z "$mac" ] && continue
local vendor=""
if [ -n "$oui" ] && [ -f "$oui_db" ]; then
vendor=$(grep -i "^${oui}" "$oui_db" 2>/dev/null | cut -f2 | head -1)
fi
# Detect randomized MAC (local bit set)
local randomized=0
local second_char=$(echo "$mac" | cut -c2)
case "$second_char" in
2|3|6|7|a|b|e|f|A|B|E|F) randomized=1 ;;
esac
echo "mg|${mac}|${vendor}|${iface}|${hostname}|${status}|${randomized}|${first_seen}|${last_seen}"
done < "$db"
}
# Collect client-guardian data via UCI
# Format: cg|mac|name|zone|status|first_seen|last_seen
di_collect_client_guardian() {
local idx=0
while uci -q get client-guardian.@client[$idx] >/dev/null 2>&1; do
local mac=$(uci -q get client-guardian.@client[$idx].mac)
local name=$(uci -q get client-guardian.@client[$idx].name)
local zone=$(uci -q get client-guardian.@client[$idx].zone)
local status=$(uci -q get client-guardian.@client[$idx].status)
local first_seen=$(uci -q get client-guardian.@client[$idx].first_seen)
local last_seen=$(uci -q get client-guardian.@client[$idx].last_seen)
[ -n "$mac" ] && echo "cg|${mac}|${name}|${zone}|${status}|${first_seen}|${last_seen}"
idx=$((idx + 1))
done
}
# Collect ARP table for IP-to-MAC resolution and online detection
# Format: arp|mac|ip|iface
di_collect_arp() {
while read -r ip hw flags mac mask iface; do
[ "$ip" = "IP" ] && continue # skip header
[ "$mac" = "00:00:00:00:00:00" ] && continue
[ "$flags" = "0x0" ] && continue # incomplete entry
echo "arp|${mac}|${ip}|${iface}"
done < /proc/net/arp
}
# Collect DHCP lease data
# Format: dhcp|mac|ip|hostname|expires
di_collect_dhcp() {
local lease_file="/tmp/dhcp.leases"
[ ! -f "$lease_file" ] && return
while read -r expires mac ip hostname clientid; do
[ -z "$mac" ] && continue
[ "$mac" = "*" ] && continue
echo "dhcp|${mac}|${ip}|${hostname}|${expires}"
done < "$lease_file"
}
# Collect P2P mesh peers
# Format: p2p|id|address|name|status|services
di_collect_p2p_peers() {
# Try ubus first
local peers=$(ubus call luci.secubox-p2p get_peers 2>/dev/null)
[ -z "$peers" ] && return
echo "$peers" | jsonfilter -e '@.peers[*]' 2>/dev/null | while read -r peer_json; do
local id=$(echo "$peer_json" | jsonfilter -e '@.id' 2>/dev/null)
local addr=$(echo "$peer_json" | jsonfilter -e '@.address' 2>/dev/null)
local name=$(echo "$peer_json" | jsonfilter -e '@.name' 2>/dev/null)
local status=$(echo "$peer_json" | jsonfilter -e '@.status' 2>/dev/null)
[ -n "$id" ] && echo "p2p|${id}|${addr}|${name}|${status}"
done
}
# Collect exposure scan data (listening services by IP)
# Format: exp|ip|port|service_name
di_collect_exposure() {
# Simple port scan from /proc/net/tcp
local hex_ports=""
while read -r sl local_addr rem_addr st rest; do
[ "$st" != "0A" ] && continue # 0A = LISTEN
local hex_port=$(echo "$local_addr" | cut -d: -f2)
local port=$(printf "%d" "0x${hex_port}" 2>/dev/null)
local hex_ip=$(echo "$local_addr" | cut -d: -f1)
# Determine bind address
local bind="local"
[ "$hex_ip" = "00000000" ] && bind="all"
echo "exp|${bind}|${port}"
done < /proc/net/tcp 2>/dev/null
}
# ============================================================================
# Emulator Collection
# ============================================================================
di_collect_emulators() {
for emu in usb mqtt zigbee; do
local enabled=$(di_uci_get "${emu}.enabled")
[ "$enabled" != "1" ] && continue
local script="${DI_EMULATOR_DIR}/${emu}.sh"
[ -f "$script" ] && {
. "$script"
emulate_${emu} 2>/dev/null
}
done
}
# ============================================================================
# Merge & Aggregate
# ============================================================================
# Merge all sources into a unified device map keyed by MAC
# Output: JSON array of device objects
di_aggregate_devices() {
local tmp_dir="${DI_CACHE_DIR}/collect_$$"
mkdir -p "$tmp_dir"
# Collect from all sources
di_collect_mac_guardian > "${tmp_dir}/mg.dat" 2>/dev/null
di_collect_client_guardian > "${tmp_dir}/cg.dat" 2>/dev/null
di_collect_dhcp > "${tmp_dir}/dhcp.dat" 2>/dev/null
di_collect_arp > "${tmp_dir}/arp.dat" 2>/dev/null
di_collect_p2p_peers > "${tmp_dir}/p2p.dat" 2>/dev/null
di_collect_exposure > "${tmp_dir}/exp.dat" 2>/dev/null
di_collect_emulators > "${tmp_dir}/emu.dat" 2>/dev/null
# Build unified device map
# Step 1: Extract all unique MACs
local all_macs=$(cat "${tmp_dir}"/*.dat 2>/dev/null | \
awk -F'|' '{print tolower($2)}' | \
grep -E '^[0-9a-f]{2}(:[0-9a-f]{2}){5}$' | \
sort -u)
# Step 2: Build JSON output
local first=1
printf '['
for mac in $all_macs; do
[ $first -eq 1 ] && first=0 || printf ','
# Defaults
local ip="" hostname="" vendor="" iface="" online="false"
local mg_status="" cg_zone="" cg_status="" randomized="false"
local first_seen="" last_seen="" label="" device_type=""
local device_type_source="" emulator_source=""
local source_node="local"
# Mac-Guardian data
local mg=$(grep "^mg|${mac}|" "${tmp_dir}/mg.dat" 2>/dev/null | head -1)
if [ -n "$mg" ]; then
vendor=$(echo "$mg" | cut -d'|' -f3)
iface=$(echo "$mg" | cut -d'|' -f4)
hostname=$(echo "$mg" | cut -d'|' -f5)
mg_status=$(echo "$mg" | cut -d'|' -f6)
local rand_flag=$(echo "$mg" | cut -d'|' -f7)
[ "$rand_flag" = "1" ] && randomized="true"
first_seen=$(echo "$mg" | cut -d'|' -f8)
last_seen=$(echo "$mg" | cut -d'|' -f9)
fi
# Client-Guardian data (format: cg|mac|name|zone|status|first_seen|last_seen)
local cg=$(grep -i "^cg|${mac}|" "${tmp_dir}/cg.dat" 2>/dev/null | head -1)
if [ -n "$cg" ]; then
local cg_name=$(echo "$cg" | cut -d'|' -f3)
[ -n "$cg_name" ] && [ -z "$hostname" ] && hostname="$cg_name"
cg_zone=$(echo "$cg" | cut -d'|' -f4)
cg_status=$(echo "$cg" | cut -d'|' -f5)
local cg_first=$(echo "$cg" | cut -d'|' -f6)
local cg_last=$(echo "$cg" | cut -d'|' -f7)
[ -n "$cg_first" ] && [ -z "$first_seen" ] && first_seen="$cg_first"
[ -n "$cg_last" ] && [ -z "$last_seen" ] && last_seen="$cg_last"
fi
# ARP data — provides IP and online detection
local arp=$(grep -i "|${mac}|" "${tmp_dir}/arp.dat" 2>/dev/null | head -1)
if [ -n "$arp" ]; then
local arp_ip=$(echo "$arp" | cut -d'|' -f3)
[ -n "$arp_ip" ] && [ -z "$ip" ] && ip="$arp_ip"
local arp_iface=$(echo "$arp" | cut -d'|' -f4)
[ -n "$arp_iface" ] && [ -z "$iface" ] && iface="$arp_iface"
online="true" # present in ARP = reachable
fi
# DHCP data
local dhcp=$(grep "^dhcp|${mac}|" "${tmp_dir}/dhcp.dat" 2>/dev/null | head -1)
if [ -n "$dhcp" ]; then
local dhcp_ip=$(echo "$dhcp" | cut -d'|' -f3)
[ -n "$dhcp_ip" ] && [ -z "$ip" ] && ip="$dhcp_ip"
local dhcp_host=$(echo "$dhcp" | cut -d'|' -f4)
[ -n "$dhcp_host" ] && [ "$dhcp_host" != "*" ] && [ -z "$hostname" ] && hostname="$dhcp_host"
local dhcp_exp=$(echo "$dhcp" | cut -d'|' -f5)
# Active lease = online
local now=$(date +%s)
[ -n "$dhcp_exp" ] && [ "$dhcp_exp" -gt "$now" ] 2>/dev/null && online="true"
fi
# Emulator data
local emu=$(grep "^emu|${mac}|" "${tmp_dir}/emu.dat" 2>/dev/null | head -1)
if [ -n "$emu" ]; then
emulator_source=$(echo "$emu" | cut -d'|' -f3)
local emu_label=$(echo "$emu" | cut -d'|' -f4)
[ -n "$emu_label" ] && [ -z "$hostname" ] && hostname="$emu_label"
fi
# User override from UCI
local mac_clean=$(echo "$mac" | tr -d ':')
local user_type=$(uci -q get ${DI_CONFIG}.${mac_clean}.type)
local user_label=$(uci -q get ${DI_CONFIG}.${mac_clean}.label)
[ -n "$user_label" ] && label="$user_label"
# OUI vendor lookup fallback (for devices not in mac-guardian)
if [ -z "$vendor" ]; then
local oui_prefix=$(echo "$mac" | cut -d: -f1-3 | tr 'a-f' 'A-F')
local oui_file="/usr/lib/secubox/mac-guardian/oui.tsv"
if [ -n "$oui_prefix" ] && [ -f "$oui_file" ]; then
vendor=$(grep -i "^${oui_prefix} " "$oui_file" 2>/dev/null | cut -f2 | head -1)
fi
fi
# Determine connection type
local connection_type="ethernet"
case "$iface" in
wlan*|phy*) connection_type="wifi" ;;
esac
# Classify device
if [ -n "$user_type" ]; then
device_type="$user_type"
device_type_source="user"
elif [ -n "$emulator_source" ]; then
device_type="${emulator_source}_device"
device_type_source="emulator"
fi
# JSON output
printf '{'
printf '"mac":"%s"' "$mac"
printf ',"ip":"%s"' "$ip"
printf ',"hostname":"%s"' "$hostname"
[ -n "$label" ] && printf ',"label":"%s"' "$label"
printf ',"vendor":"%s"' "$vendor"
printf ',"online":%s' "$online"
printf ',"connection_type":"%s"' "$connection_type"
printf ',"iface":"%s"' "$iface"
printf ',"randomized":%s' "$randomized"
[ -n "$mg_status" ] && printf ',"mg_status":"%s"' "$mg_status"
[ -n "$cg_zone" ] && printf ',"cg_zone":"%s"' "$cg_zone"
[ -n "$cg_status" ] && printf ',"cg_status":"%s"' "$cg_status"
[ -n "$device_type" ] && printf ',"device_type":"%s"' "$device_type"
[ -n "$device_type_source" ] && printf ',"device_type_source":"%s"' "$device_type_source"
[ -n "$emulator_source" ] && printf ',"emulator_source":"%s"' "$emulator_source"
printf ',"source_node":"%s"' "$source_node"
[ -n "$first_seen" ] && printf ',"first_seen":"%s"' "$first_seen"
[ -n "$last_seen" ] && printf ',"last_seen":"%s"' "$last_seen"
printf '}'
done
# Add P2P mesh peers as pseudo-devices
if [ -s "${tmp_dir}/p2p.dat" ]; then
while IFS='|' read -r src id addr name status rest; do
[ -z "$id" ] && continue
[ $first -eq 1 ] && first=0 || printf ','
printf '{'
printf '"mac":"mesh-%s"' "$id"
printf ',"ip":"%s"' "$(echo "$addr" | cut -d: -f1)"
printf ',"hostname":"%s"' "$name"
printf ',"online":%s' "$([ "$status" = "online" ] && echo true || echo false)"
printf ',"device_type":"mesh_peer"'
printf ',"device_type_source":"p2p"'
printf ',"source_node":"mesh"'
printf '}'
done < "${tmp_dir}/p2p.dat"
fi
# Add emulator-only entries (no MAC)
if [ -s "${tmp_dir}/emu.dat" ]; then
grep "^emu|synth-" "${tmp_dir}/emu.dat" 2>/dev/null | while IFS='|' read -r src synth_id emu_type emu_name emu_model emu_caps; do
[ -z "$synth_id" ] && continue
[ $first -eq 1 ] && first=0 || printf ','
printf '{'
printf '"mac":"%s"' "$synth_id"
printf ',"hostname":"%s"' "$emu_name"
printf ',"vendor":"%s"' "$emu_model"
printf ',"online":true'
printf ',"device_type":"%s_device"' "$emu_type"
printf ',"device_type_source":"emulator"'
printf ',"emulator_source":"%s"' "$emu_type"
printf ',"source_node":"local"'
printf '}'
done
fi
printf ']'
# Cleanup
rm -rf "$tmp_dir"
}
# Get cached or fresh device list
di_get_devices() {
mkdir -p "$DI_CACHE_DIR"
if di_cache_valid; then
cat "$DI_CACHE_FILE"
else
di_aggregate_devices | tee "$DI_CACHE_FILE"
fi
}
# ============================================================================
# Summary Statistics
# ============================================================================
di_get_summary() {
local devices=$(di_get_devices)
local total=0 online=0 mesh=0
total=$(echo "$devices" | jsonfilter -e '@[*].mac' 2>/dev/null | wc -l)
online=$(echo "$devices" | jsonfilter -e '@[*].online' 2>/dev/null | grep -c "true")
mesh=$(echo "$devices" | jsonfilter -e '@[*].device_type' 2>/dev/null | grep -c "mesh_peer")
printf '{"total":%d,"online":%d,"mesh_peers":%d}' \
"$total" "$online" "$mesh"
}
# ============================================================================
# Device Type Registry
# ============================================================================
di_get_device_types() {
printf '['
local first=1
config_load "$DI_CONFIG"
get_type() {
local section="$1"
local name icon color
config_get name "$section" name
config_get icon "$section" icon
config_get color "$section" color
[ -z "$name" ] && return
[ $first -eq 1 ] && first=0 || printf ','
printf '{"id":"%s","name":"%s","icon":"%s","color":"%s"}' \
"$section" "$name" "$icon" "$color"
}
config_foreach get_type device_type
printf ']'
}