From 8949bcb5673c7534a8017cda8912387cfe306e4d Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Sun, 6 Jul 2025 17:08:11 +1000 Subject: [PATCH] icon and device type guessing from @slammingprogramming --- server/scan/device_handling.py | 406 ++++++++++++++++++++++++--------- 1 file changed, 297 insertions(+), 109 deletions(-) diff --git a/server/scan/device_handling.py b/server/scan/device_handling.py index 64604c3d..e44b8a6e 100755 --- a/server/scan/device_handling.py +++ b/server/scan/device_handling.py @@ -1,13 +1,14 @@ import sys +from typing import Optional, List, Tuple, Dict +import subprocess +import conf +import os +import re # Register NetAlertX directories INSTALL_PATH="/app" sys.path.extend([f"{INSTALL_PATH}/server"]) -import subprocess -import conf -import os -import re from helper import timeNowTZ, get_setting, get_setting_value, list_to_where, check_IP_format, sanitize_SQL_input from logger import mylog from const import vendorsPath, vendorsPathNewest, sql_generateGuid @@ -439,8 +440,6 @@ def update_devices_data_from_scan (db): default_icon = get_setting_value('NEWDEV_devIcon') - - if get_setting_value('NEWDEV_replace_preset_icon'): query = f"""SELECT * FROM Devices WHERE devIcon in ('', 'null', '{default_icon}') @@ -647,118 +646,307 @@ def query_MAC_vendor (pMAC): # Icons #=============================================================================== #------------------------------------------------------------------------------- -# Base64 encoded HTML string for FontAwesome icons -icons = { - "globe": "PGkgY2xhc3M9ImZhcyBmYS1nbG9iZSI+PC9pPg==", # globe icon - "phone": "PGkgY2xhc3M9ImZhcyBmYS1tb2JpbGUtYWx0Ij48L2k+", - "laptop": "PGkgY2xhc3M9ImZhIGZhLWxhcHRvcCI+PC9pPg==", - "printer": "PGkgY2xhc3M9ImZhIGZhLXByaW50ZXIiPjwvaT4=", - "router": "PGkgY2xhc3M9ImZhcyBmYS1yYW5kb20iPjwvaT4=", - "tv": "PGkgY2xhc3M9ImZhIGZhLXR2Ij48L2k+", - "desktop": "PGkgY2xhc3M9ImZhIGZhLWRlc2t0b3AiPjwvaT4=", - "tablet": "PGkgY2xhc3M9ImZhIGZhLXRhYmxldCI+PC9pPg==", - "watch": "PGkgY2xhc3M9ImZhIGZhLXdhbmNoIj48L2k+", - "camera": "PGkgY2xhc3M9ImZhIGZhLWNhbWVyYSI+PC9pPg==", - "home": "PGkgY2xhc3M9ImZhIGZhLWhvbWUiPjwvaT4=", - "apple": "PGkgY2xhc3M9ImZhYiBmYS1hcHBsZSI+PC9pPg==", - "ethernet": "PGkgY2xhc3M9ImZhcyBmYS1ldGhlcm5ldCI+PC9pPg==", - "google": "PGkgY2xhc3M9ImZhYiBmYS1nb29nbGUiPjwvaT4=", - "raspberry": "PGkgY2xhc3M9ImZhYiBmYS1yYXNwYmVycnktcGkiPjwvaT4=", - "microchip": "PGkgY2xhc3M9ImZhcyBmYS1taWNyb2NoaXAiPjwvaT4=" +# Base64 encoded HTML strings for FontAwesome icons, now with an extended icons dictionary for broader device coverage +ICONS = { + "globe": "PGkgY2xhc3M9ImZhcyBmYS1nbG9iZSI+PC9pPg==", # Internet or global network + "phone": "PGkgY2xhc3M9ImZhcyBmYS1tb2JpbGUtYWx0Ij48L2k+", # Smartphone + "laptop": "PGkgY2xhc3M9ImZhIGZhLWxhcHRvcCI+PC9pPg==", # Laptop + "printer": "PGkgY2xhc3M9ImZhIGZhLXByaW50ZXIiPjwvaT4=", # Printer + "router": "PGkgY2xhc3M9ImZhcyBmYS1yYW5kb20iPjwvaT4=", # Router or network switch + "tv": "PGkgY2xhc3M9ImZhIGZhLXR2Ij48L2k+", # Television + "desktop": "PGkgY2xhc3M9ImZhIGZhLWRlc2t0b3AiPjwvaT4=", # Desktop PC + "tablet": "PGkgY2xhc3M9ImZhIGZhLXRhYmxldCI+PC9pPg==", # Tablet + "watch": "PGkgY2xhc3M9ImZhcyBmYS1jbG9jayI+PC9pPg==", # Fallback to clock since smartwatch is nonfree in FontAwesome + "camera": "PGkgY2xhc3M9ImZhIGZhLWNhbWVyYSI+PC9pPg==", # Camera or webcam + "home": "PGkgY2xhc3M9ImZhIGZhLWhvbWUiPjwvaT4=", # Smart home device + "apple": "PGkgY2xhc3M9ImZhYiBmYS1hcHBsZSI+PC9pPg==", # Apple device + "ethernet": "PGkgY2xhc3M9ImZhcyBmYS1uZXR3b3JrLXdpcmVkIj48L2k+", # Free alternative for ethernet icon in FontAwesome + "google": "PGkgY2xhc3M9ImZhYiBmYS1nb29nbGUiPjwvaT4=", # Google device + "raspberry": "PGkgY2xhc3M9ImZhYiBmYS1yYXNwYmVycnktcGkiPjwvaT4=", # Raspberry Pi + "microchip": "PGkgY2xhc3M9ImZhcyBmYS1taWNyb2NoaXAiPjwvaT4=", # IoT or embedded device + "server": "PGkgY2xhc3M9ImZhcyBmYS1zZXJ2ZXIiPjwvaT4=", # Server + "gamepad": "PGkgY2xhc3M9ImZhcyBmYS1nYW1lcGFkIj48L2k+", # Gaming console + "lightbulb": "PGkgY2xhc3M9ImZhcyBmYS1saWdodGJ1bGIiPjwvaT4=", # Smart light + "speaker": "PGkgY2xhc3M9ImZhcyBmYS12b2x1bWUtdXAiPjwvaT4=", # Free speaker alt icon for smart speakers in FontAwesome + "lock": "PGkgY2xhc3M9ImZhcyBmYS1sb2NrIj48L2k+", # Security device +} + +# Extended device types for comprehensive classification +DEVICE_TYPES = { + "Internet": "Internet Gateway", + "Phone": "Smartphone", + "Laptop": "Laptop", + "Printer": "Printer", + "Router": "Router", + "TV": "Television", + "Desktop": "Desktop PC", + "Tablet": "Tablet", + "Smartwatch": "Smartwatch", + "Camera": "Camera", + "SmartHome": "Smart Home Device", + "Server": "Server", + "GamingConsole": "Gaming Console", + "IoT": "IoT Device", + "NetworkSwitch": "Network Switch", + "AccessPoint": "Access Point", + "SmartLight": "Smart Light", + "SmartSpeaker": "Smart Speaker", + "SecurityDevice": "Security Device", + "Unknown": "Unknown Device", } #------------------------------------------------------------------------------- -# Guess device icon -def guess_icon(vendor, mac, ip, name, default): - - mylog('debug', [f"[guess_icon] Guessing icon for (vendor|mac|ip|name): ('{vendor}'|'{mac}'|{ip}|{name})"]) - - result = default - mac = str(mac).upper() if mac else "00:00:00:00:00:00" - vendor = str(vendor).lower() if vendor else "unknown" - name = str(name).lower() if name else "(unknown)" +# Guess device attributes such as type of device and associated device icon +def guess_device_attributes( + vendor: Optional[str], + mac: Optional[str], + ip: Optional[str], + name: Optional[str], + default_icon: str, + default_type: str + ) -> Tuple[str, str]: + """ + Guess the appropriate FontAwesome icon and device type based on device attributes. - # Guess icon based on vendor - if any(brand in vendor for brand in {"samsung", "motorola"}): - result = icons.get("phone") - elif "dell" in vendor: - result = icons.get("laptop") - elif "hp" in vendor: - result = icons.get("printer") - elif "cisco" in vendor: - result = icons.get("router") - elif "lg" in vendor: - result = icons.get("tv") - elif "raspberry" in vendor: - result = icons.get("raspberry") - elif "apple" in vendor: - result = icons.get("apple") - elif "google" in vendor: - result = icons.get("google") - elif "ubiquiti" in vendor: - result = icons.get("router") - elif any(brand in vendor for brand in {"espressif"}): - result = icons.get("microchip") + Args: + vendor: Device vendor name. + mac: Device MAC address. + ip: Device IP address. + name: Device name. + default_icon: Default icon to return if no match is found. + default_type: Default type to return if no match is found. - # Guess icon based on MAC address patterns - elif mac == "INTERNET": - result = icons.get("globe") - elif mac.startswith("00:1A:79"): # Apple - result = icons.get("apple") - elif mac.startswith("B0:BE:83"): # Apple - result = icons.get("apple") - elif mac.startswith("00:1B:63"): # Sony - result = icons.get("tablet") - elif mac.startswith("74:AC:B9"): # Unifi - result = icons.get("ethernet") - - - # Guess icon based on name - elif 'google' in name: - result = icons.get("google") - elif 'desktop' in name: - result = icons.get("desktop") - elif 'raspberry' in name: - result = icons.get("raspberry") - - # Guess icon based on IP address ranges - elif ip.startswith("192.168.1."): - result = icons.get("laptop") + Returns: + Tuple[str, str]: A tuple containing the guessed icon (Base64-encoded HTML string) + and the guessed device type (string). + """ + mylog('debug', f"[guess_device_attributes] Guessing attributes for (vendor|mac|ip|name): ('{vendor}'|'{mac}'|'{ip}'|'{name}')") + # Normalize inputs + vendor = str(vendor).lower().strip() if vendor else "unknown" + mac = str(mac).upper().strip() if mac else "00:00:00:00:00:00" + ip = str(ip).strip() if ip else "169.254.0.0" # APIPA address for unknown IPs per RFC 3927 + name = str(name).lower().strip() if name else "(unknown)" - - return result + # --- Icon Guessing Logic --- + if mac == "INTERNET": + icon = ICONS.get("globe", default_icon) + else: + # Vendor-based icon guessing + icon_vendor_patterns = { + "apple": "apple", + "samsung|motorola|xiaomi|huawei": "phone", + "dell|lenovo|asus|acer": "laptop", + "hp|epson|canon|brother": "printer", + "cisco|ubiquiti|netgear|tp-link|d-link|mikrotik": "router", + "lg|samsung electronics|sony|vizio": "tv", + "raspberry pi": "raspberry", + "google": "google", + "espressif|particle": "microchip", + "intel|amd": "desktop", + "amazon": "speaker", + "philips hue|lifx": "lightbulb", + "aruba|meraki": "ethernet", + "qnap|synology": "server", + "nintendo|sony interactive|microsoft": "gamepad", + "ring|blink|arlo": "camera", + "nest": "home", + } + for pattern, icon_key in icon_vendor_patterns.items(): + if re.search(pattern, vendor, re.IGNORECASE): + icon = ICONS.get(icon_key, default_icon) + break + else: + # MAC-based icon guessing + mac_clean = mac.replace(':', '').replace('-', '').upper() + icon_mac_patterns = { + "001A79|B0BE83|BC926B": "apple", + "001B63|BC4C4C": "tablet", + "74ACB9|002468": "ethernet", + "B827EB": "raspberry", + "001422|001874": "desktop", + "001CBF|002186": "server", + } + for pattern_str, icon_key in icon_mac_patterns.items(): + patterns = [p.replace(':', '').replace('-', '').upper() for p in pattern_str.split('|')] + if any(mac_clean.startswith(p) for p in patterns): + icon = ICONS.get(icon_key, default_icon) + break + else: + # Name-based icon guessing + icon_name_patterns = { + "iphone|ipad|macbook|imac": "apple", + "pixel|galaxy|redmi": "phone", + "laptop|notebook": "laptop", + "printer|print": "printer", + "router|gateway|ap|access[ -]?point": "router", + "tv|television|smarttv": "tv", + "desktop|pc|computer": "desktop", + "tablet|pad": "tablet", + "watch|wear": "watch", + "camera|cam|webcam": "camera", + "echo|alexa|dot": "speaker", + "hue|lifx|bulb": "lightbulb", + "server|nas": "server", + "playstation|xbox|switch": "gamepad", + "raspberry|pi": "raspberry", + "google|chromecast|nest": "google", + "doorbell|lock|security": "lock", + } + for pattern, icon_key in icon_name_patterns.items(): + if re.search(pattern, name, re.IGNORECASE): + icon = ICONS.get(icon_key, default_icon) + break + else: + # IP-based icon guessing + icon_ip_patterns = { + r"^192\.168\.[0-1]\.1$": "router", + r"^10\.0\.0\.1$": "router", + r"^192\.168\.[0-1]\.[2-9]$": "desktop", + r"^192\.168\.[0-1]\.1\d{2}$": "phone", + } + for pattern, icon_key in icon_ip_patterns.items(): + if re.match(pattern, ip): + icon = ICONS.get(icon_key, default_icon) + break + else: + icon = default_icon -#------------------------------------------------------------------------------- -# Guess device type -def guess_type(vendor, mac, ip, name, default): - result = default - mac = str(mac).upper() if mac else "00:00:00:00:00:00" - vendor = str(vendor).lower() if vendor else "unknown" - name = str(name).lower() if name else "(unknown)" + # --- Type Guessing Logic --- + if mac == "INTERNET": + type_ = DEVICE_TYPES.get("Internet", default_type) + else: + # Vendor-based type guessing + type_vendor_patterns = { + "apple|samsung|motorola|xiaomi|huawei": "Phone", + "dell|lenovo|asus|acer|hp": "Laptop", + "epson|canon|brother": "Printer", + "cisco|ubiquiti|netgear|tp-link|d-link|mikrotik|aruba|meraki": "Router", + "lg|samsung electronics|sony|vizio": "TV", + "raspberry pi": "IoT", + "google|nest": "SmartHome", + "espressif|particle": "IoT", + "intel|amd": "Desktop", + "amazon": "SmartSpeaker", + "philips hue|lifx": "SmartLight", + "qnap|synology": "Server", + "nintendo|sony interactive|microsoft": "GamingConsole", + "ring|blink|arlo": "Camera", + } + for pattern, type_key in type_vendor_patterns.items(): + if re.search(pattern, vendor, re.IGNORECASE): + type_ = DEVICE_TYPES.get(type_key, default_type) + break + else: + # MAC-based type guessing + mac_clean = mac.replace(':', '').replace('-', '').upper() + type_mac_patterns = { + "00:1A:79|B0:BE:83|BC:92:6B": "Phone", + "00:1B:63|BC:4C:4C": "Tablet", + "74:AC:B9|00:24:68": "AccessPoint", + "B8:27:EB": "IoT", + "00:14:22|00:18:74": "Desktop", + "00:1C:BF|00:21:86": "Server", + } + for pattern_str, type_key in type_mac_patterns.items(): + patterns = [p.replace(':', '').replace('-', '').upper() for p in pattern_str.split('|')] + if any(mac_clean.startswith(p) for p in patterns): + type_ = DEVICE_TYPES.get(type_key, default_type) + break + else: + # Name-based type guessing + type_name_patterns = { + "iphone|ipad": "Phone", + "macbook|imac": "Laptop", + "pixel|galaxy|redmi": "Phone", + "laptop|notebook": "Laptop", + "printer|print": "Printer", + "router|gateway|ap|access[ -]?point": "Router", + "tv|television|smarttv": "TV", + "desktop|pc|computer": "Desktop", + "tablet|pad": "Tablet", + "watch|wear": "Smartwatch", + "camera|cam|webcam": "Camera", + "echo|alexa|dot": "SmartSpeaker", + "hue|lifx|bulb": "SmartLight", + "server|nas": "Server", + "playstation|xbox|switch": "GamingConsole", + "raspberry|pi": "IoT", + "google|chromecast|nest": "SmartHome", + "doorbell|lock|security": "SecurityDevice", + } + for pattern, type_key in type_name_patterns.items(): + if re.search(pattern, name, re.IGNORECASE): + type_ = DEVICE_TYPES.get(type_key, default_type) + break + else: + # IP-based type guessing + type_ip_patterns = { + r"^192\.168\.[0-1]\.1$": "Router", + r"^10\.0\.0\.1$": "Router", + r"^192\.168\.[0-1]\.[2-9]$": "Desktop", + r"^192\.168\.[0-1]\.1\d{2}$": "Phone", + } + for pattern, type_key in type_ip_patterns.items(): + if re.match(pattern, ip): + type_ = DEVICE_TYPES.get(type_key, default_type) + break + else: + type_ = default_type - # Guess icon based on vendor - if any(brand in vendor for brand in {"samsung", "motorola"}): - result = "Phone" - elif "cisco" in vendor: - result = "Router" - elif "lg" in vendor: - result = "TV" - elif "google" in vendor: - result = "Phone" - elif "ubiquiti" in vendor: - result = "Router" + return icon, type_ - # Guess type based on MAC address patterns - elif mac == "INTERNET": - result = "Internet" - - # Guess type based on name - elif 'google' in name: - result = "Phone" +# Deprecated functions with redirects (To be removed once all calls for these have been adjusted to use the updated function) +def guess_icon( + vendor: Optional[str], + mac: Optional[str], + ip: Optional[str], + name: Optional[str], + default: str + ) -> str: + """ + [DEPRECATED] Guess the appropriate FontAwesome icon for a device based on its attributes. + Use guess_device_attributes instead. + + Args: + vendor: Device vendor name. + mac: Device MAC address. + ip: Device IP address. + name: Device name. + default: Default icon to return if no match is found. + + Returns: + str: Base64-encoded FontAwesome icon HTML string. + """ - # Guess type based on IP address ranges - elif ip == ("192.168.1.1"): - result = "Router" + icon, _ = guess_device_attributes(vendor, mac, ip, name, default, "unknown_type") + return icon + +def guess_type( + vendor: Optional[str], + mac: Optional[str], + ip: Optional[str], + name: Optional[str], + default: str + ) -> str: + """ + [DEPRECATED] Guess the device type based on its attributes. + Use guess_device_attributes instead. + + Args: + vendor: Device vendor name. + mac: Device MAC address. + ip: Device IP address. + name: Device name. + default: Default type to return if no match is found. + + Returns: + str: Device type from DEVICE_TYPES dictionary. + """ - return result + _, type_ = guess_device_attributes(vendor, mac, ip, name, "unknown_icon", default) + return type_ + +# Handler for when this is run as a program instead of called as a module. +if __name__ == "__main__": + mylog('error', "This module is not intended to be run directly.")