secubox-openwrt/package/secubox/secubox-app-device-intel/files/usr/sbin/device-intelctl
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

398 lines
13 KiB
Bash

#!/bin/sh
# SecuBox Device Intelligence Controller
# Unified device inventory with aggregation, classification, and emulators
VERSION="1.0.0"
CONFIG="device-intel"
LIB_DIR="/usr/lib/secubox/device-intel"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
log() { echo -e "${GREEN}[INTEL]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
di_ctl_get() { uci -q get ${CONFIG}.$1; }
# Load libraries
. "${LIB_DIR}/functions.sh"
. "${LIB_DIR}/classify.sh"
# ============================================================================
# Commands
# ============================================================================
cmd_list() {
local format="${1:-json}"
log "Aggregating device inventory..."
di_invalidate_cache
local devices=$(di_get_devices)
case "$format" in
json)
echo "$devices"
;;
table)
echo ""
printf "%-19s %-15s %-20s %-12s %-10s %-8s\n" \
"MAC" "IP" "HOSTNAME" "TYPE" "ZONE" "STATUS"
echo "────────────────────────────────────────────────────────────────────────────────────"
echo "$devices" | jsonfilter -e '@[*].mac' 2>/dev/null | while read -r mac; do
[ -z "$mac" ] && continue
local ip=$(echo "$devices" | jsonfilter -e "@[@.mac='${mac}'].ip" 2>/dev/null)
local host=$(echo "$devices" | jsonfilter -e "@[@.mac='${mac}'].hostname" 2>/dev/null)
local dtype=$(echo "$devices" | jsonfilter -e "@[@.mac='${mac}'].device_type" 2>/dev/null)
local zone=$(echo "$devices" | jsonfilter -e "@[@.mac='${mac}'].cg_zone" 2>/dev/null)
local online=$(echo "$devices" | jsonfilter -e "@[@.mac='${mac}'].online" 2>/dev/null)
local status_sym="?"
[ "$online" = "true" ] && status_sym="${GREEN}${NC}" || status_sym="${RED}${NC}"
printf "%-19s %-15s %-20s %-12s %-10s %b\n" \
"$mac" "${ip:--}" "${host:--}" "${dtype:--}" "${zone:--}" "$status_sym"
done
echo ""
;;
esac
}
cmd_show() {
local mac="$1"
if [ -z "$mac" ]; then
echo "Usage: device-intelctl show <mac>"
return 1
fi
local mac_lower=$(echo "$mac" | tr 'A-F' 'a-f')
local devices=$(di_get_devices)
local device=$(echo "$devices" | jsonfilter -e "@[@.mac='${mac_lower}']" 2>/dev/null)
if [ -z "$device" ]; then
error "Device not found: $mac"
return 1
fi
echo ""
echo "=========================================="
echo " Device: $mac_lower"
echo "=========================================="
echo ""
local ip=$(echo "$device" | jsonfilter -e '@.ip' 2>/dev/null)
local hostname=$(echo "$device" | jsonfilter -e '@.hostname' 2>/dev/null)
local label=$(echo "$device" | jsonfilter -e '@.label' 2>/dev/null)
local vendor=$(echo "$device" | jsonfilter -e '@.vendor' 2>/dev/null)
local online=$(echo "$device" | jsonfilter -e '@.online' 2>/dev/null)
local conn=$(echo "$device" | jsonfilter -e '@.connection_type' 2>/dev/null)
local iface=$(echo "$device" | jsonfilter -e '@.iface' 2>/dev/null)
local randomized=$(echo "$device" | jsonfilter -e '@.randomized' 2>/dev/null)
local mg_status=$(echo "$device" | jsonfilter -e '@.mg_status' 2>/dev/null)
local cg_zone=$(echo "$device" | jsonfilter -e '@.cg_zone' 2>/dev/null)
local cg_status=$(echo "$device" | jsonfilter -e '@.cg_status' 2>/dev/null)
local dtype=$(echo "$device" | jsonfilter -e '@.device_type' 2>/dev/null)
local dsrc=$(echo "$device" | jsonfilter -e '@.device_type_source' 2>/dev/null)
local emu=$(echo "$device" | jsonfilter -e '@.emulator_source' 2>/dev/null)
local first_seen=$(echo "$device" | jsonfilter -e '@.first_seen' 2>/dev/null)
local last_seen=$(echo "$device" | jsonfilter -e '@.last_seen' 2>/dev/null)
echo " IP: ${ip:--}"
echo " Hostname: ${hostname:--}"
[ -n "$label" ] && echo " Label: $label"
echo " Vendor: ${vendor:--}"
echo -e " Online: $([ "$online" = "true" ] && echo "${GREEN}Yes${NC}" || echo "${RED}No${NC}")"
echo " Connection: ${conn:--} (${iface:--})"
echo -e " Randomized: $([ "$randomized" = "true" ] && echo "${YELLOW}Yes${NC}" || echo "No")"
echo ""
echo " ── Security ──"
echo " MAC Status: ${mg_status:--}"
echo " NAC Zone: ${cg_zone:--}"
echo " NAC Status: ${cg_status:--}"
echo ""
echo " ── Classification ──"
echo " Device Type: ${dtype:--}"
echo " Source: ${dsrc:--}"
[ -n "$emu" ] && echo " Emulator: $emu"
echo ""
echo " ── Timeline ──"
echo " First Seen: ${first_seen:--}"
echo " Last Seen: ${last_seen:--}"
echo ""
}
cmd_classify() {
local mac="$1"
if [ -z "$mac" ]; then
log "Running batch classification on all devices..."
local devices=$(di_get_devices)
di_classify_all "$devices"
else
local mac_lower=$(echo "$mac" | tr 'A-F' 'a-f')
local devices=$(di_get_devices)
local ip=$(echo "$devices" | jsonfilter -e "@[@.mac='${mac_lower}'].ip" 2>/dev/null)
local hostname=$(echo "$devices" | jsonfilter -e "@[@.mac='${mac_lower}'].hostname" 2>/dev/null)
local vendor=$(echo "$devices" | jsonfilter -e "@[@.mac='${mac_lower}'].vendor" 2>/dev/null)
local emu=$(echo "$devices" | jsonfilter -e "@[@.mac='${mac_lower}'].emulator_source" 2>/dev/null)
local result=$(di_classify_device "$mac_lower" "$ip" "$hostname" "$vendor" "$emu")
local dtype=$(echo "$result" | cut -d'|' -f1)
local dsrc=$(echo "$result" | cut -d'|' -f2)
log "Device $mac_lower classified as: $dtype (via $dsrc)"
fi
}
cmd_set_type() {
local mac="$1" dtype="$2"
if [ -z "$mac" ] || [ -z "$dtype" ]; then
echo "Usage: device-intelctl set-type <mac> <device_type>"
echo "Types: iot_sensor, storage, relay_gateway, compute, io_device, mqtt_device, zigbee_device, usb_peripheral"
return 1
fi
local mac_lower=$(echo "$mac" | tr 'A-F' 'a-f')
local mac_clean=$(echo "$mac_lower" | tr -d ':')
uci set ${CONFIG}.${mac_clean}=device
uci set ${CONFIG}.${mac_clean}.mac="$mac_lower"
uci set ${CONFIG}.${mac_clean}.type="$dtype"
uci commit "$CONFIG"
di_invalidate_cache
log "Device $mac_lower type set to: $dtype"
}
cmd_set_label() {
local mac="$1" label="$2"
if [ -z "$mac" ] || [ -z "$label" ]; then
echo "Usage: device-intelctl set-label <mac> <label>"
return 1
fi
local mac_lower=$(echo "$mac" | tr 'A-F' 'a-f')
local mac_clean=$(echo "$mac_lower" | tr -d ':')
uci set ${CONFIG}.${mac_clean}=device
uci set ${CONFIG}.${mac_clean}.mac="$mac_lower"
uci set ${CONFIG}.${mac_clean}.label="$label"
uci commit "$CONFIG"
di_invalidate_cache
log "Device $mac_lower label set to: $label"
}
cmd_summary() {
local summary=$(di_get_summary)
local total=$(echo "$summary" | jsonfilter -e '@.total' 2>/dev/null)
local online=$(echo "$summary" | jsonfilter -e '@.online' 2>/dev/null)
local mesh=$(echo "$summary" | jsonfilter -e '@.mesh_peers' 2>/dev/null)
echo ""
echo "=========================================="
echo " Device Intelligence v$VERSION"
echo "=========================================="
echo ""
echo " Total Devices: ${total:-0}"
echo -e " Online: ${GREEN}${online:-0}${NC}"
echo -e " Mesh Peers: ${CYAN}${mesh:-0}${NC}"
echo ""
# Emulator status
echo " ── Emulators ──"
for emu in usb mqtt zigbee; do
local enabled=$(di_ctl_get "${emu}.enabled")
if [ "$enabled" = "1" ]; then
echo -e " ${emu}: ${GREEN}enabled${NC}"
else
echo -e " ${emu}: ${RED}disabled${NC}"
fi
done
echo ""
# Data sources
echo " ── Data Sources ──"
[ -f /var/run/mac-guardian/known.db ] && \
echo -e " mac-guardian: ${GREEN}available${NC}" || \
echo -e " mac-guardian: ${RED}not found${NC}"
uci -q get client-guardian.@client[0] >/dev/null 2>&1 && \
echo -e " client-guardian: ${GREEN}available${NC}" || \
echo -e " client-guardian: ${RED}not found${NC}"
[ -f /tmp/dhcp.leases ] && \
echo -e " DHCP leases: ${GREEN}available${NC}" || \
echo -e " DHCP leases: ${RED}not found${NC}"
pgrep -f secubox-p2p >/dev/null 2>&1 && \
echo -e " P2P mesh: ${GREEN}running${NC}" || \
echo -e " P2P mesh: ${RED}stopped${NC}"
echo ""
}
cmd_emulators() {
echo ""
echo " ── Emulator Modules ──"
echo ""
for emu in usb mqtt zigbee; do
local enabled=$(di_ctl_get "${emu}.enabled")
echo -e " ${CYAN}${emu}${NC}:"
echo -e " Enabled: $([ "$enabled" = "1" ] && echo "${GREEN}Yes${NC}" || echo "${RED}No${NC}")"
case "$emu" in
usb)
echo " Source: /sys/bus/usb/devices/"
local count=$(ls -d /sys/bus/usb/devices/[0-9]* 2>/dev/null | wc -l)
echo " Devices: $count USB device(s)"
;;
mqtt)
local host=$(di_ctl_get mqtt.broker_host)
local port=$(di_ctl_get mqtt.broker_port)
echo " Broker: ${host:-127.0.0.1}:${port:-1883}"
pgrep mosquitto >/dev/null 2>&1 && \
echo -e " Status: ${GREEN}broker running${NC}" || \
echo -e " Status: ${RED}broker not found${NC}"
;;
zigbee)
local adapter=$(di_ctl_get zigbee.adapter)
local coord=$(di_ctl_get zigbee.coordinator)
echo " Adapter: ${adapter:-zigbee2mqtt}"
echo " Dongle: ${coord:-/dev/ttyUSB0}"
[ -c "${coord:-/dev/ttyUSB0}" ] 2>/dev/null && \
echo -e " Status: ${GREEN}dongle present${NC}" || \
echo -e " Status: ${RED}dongle not found${NC}"
;;
esac
echo ""
done
}
cmd_mesh_list() {
log "Querying mesh peers for device inventories..."
local peers=$(ubus call luci.secubox-p2p get_peers 2>/dev/null)
if [ -z "$peers" ]; then
warn "No mesh peers available (P2P not running or no peers)"
return
fi
echo "$peers" | jsonfilter -e '@.peers[*]' 2>/dev/null | while read -r peer; do
local name=$(echo "$peer" | jsonfilter -e '@.name' 2>/dev/null)
local addr=$(echo "$peer" | jsonfilter -e '@.address' 2>/dev/null)
local status=$(echo "$peer" | jsonfilter -e '@.status' 2>/dev/null)
echo -e "\n ${CYAN}Peer: ${name}${NC} (${addr}) [${status}]"
if [ "$status" = "online" ]; then
local peer_ip=$(echo "$addr" | cut -d: -f1)
# Query remote device-intel if available
local remote=$(curl -s --connect-timeout 3 \
"http://${peer_ip}/api/device-intel/summary" 2>/dev/null)
if [ -n "$remote" ]; then
local rt=$(echo "$remote" | jsonfilter -e '@.total' 2>/dev/null)
local ro=$(echo "$remote" | jsonfilter -e '@.online' 2>/dev/null)
echo " Devices: ${rt:-?} total, ${ro:-?} online"
else
echo " (device-intel not available on this peer)"
fi
fi
done
echo ""
}
cmd_export() {
local format="${1:-json}"
log "Exporting full device inventory..."
di_invalidate_cache
local devices=$(di_get_devices)
local types=$(di_get_device_types)
local summary=$(di_get_summary)
case "$format" in
json)
printf '{"version":"%s","timestamp":%d,"summary":%s,"device_types":%s,"devices":%s}' \
"$VERSION" "$(date +%s)" "$summary" "$types" "$devices"
;;
*)
error "Unknown format: $format (supported: json)"
return 1
;;
esac
}
cmd_refresh() {
di_invalidate_cache
log "Cache invalidated. Next query will fetch fresh data."
}
cmd_status() {
cmd_summary
}
# ============================================================================
# Main
# ============================================================================
show_help() {
cat << EOF
SecuBox Device Intelligence v$VERSION
Usage: device-intelctl <command> [options]
Commands:
list [json|table] List all devices (default: json)
show <mac> Show detailed info for a device
classify [mac] Run heuristic classification (all or single)
set-type <mac> <type> Override device type
set-label <mac> <label> Set custom label for device
summary Show overview statistics
emulators Show emulator module status
mesh-list Query mesh peers for device counts
export [json] Export full inventory with metadata
refresh Invalidate cache
status Show system status
Device Types:
iot_sensor, storage, relay_gateway, compute, io_device,
mqtt_device, zigbee_device, usb_peripheral, mesh_peer
Examples:
device-intelctl list table
device-intelctl show aa:bb:cc:dd:ee:ff
device-intelctl set-type aa:bb:cc:dd:ee:ff iot_sensor
device-intelctl set-label aa:bb:cc:dd:ee:ff "Living Room Sensor"
device-intelctl classify
device-intelctl export json > /tmp/inventory.json
EOF
}
case "${1:-}" in
list) shift; cmd_list "$@" ;;
show) shift; cmd_show "$@" ;;
classify) shift; cmd_classify "$@" ;;
set-type) shift; cmd_set_type "$@" ;;
set-label) shift; cmd_set_label "$@" ;;
summary) shift; cmd_summary "$@" ;;
emulators) shift; cmd_emulators "$@" ;;
mesh-list) shift; cmd_mesh_list "$@" ;;
export) shift; cmd_export "$@" ;;
refresh) shift; cmd_refresh "$@" ;;
status) shift; cmd_status "$@" ;;
help|--help|-h|'') show_help ;;
*) error "Unknown command: $1"; show_help >&2; exit 1 ;;
esac
exit 0