mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-06 17:15:38 -08:00
265 lines
8.7 KiB
Python
Executable File
265 lines
8.7 KiB
Python
Executable File
import os
|
|
import re
|
|
import json
|
|
import base64
|
|
from pathlib import Path
|
|
from typing import Optional, Tuple
|
|
from logger import mylog
|
|
|
|
# Register NetAlertX directories
|
|
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
|
|
|
# Load MAC/device-type/icon rules from external file
|
|
MAC_TYPE_ICON_PATH = Path(f"{INSTALL_PATH}/back/device_heuristics_rules.json")
|
|
try:
|
|
with open(MAC_TYPE_ICON_PATH, "r", encoding="utf-8") as f:
|
|
MAC_TYPE_ICON_RULES = json.load(f)
|
|
# Precompute base64-encoded icon_html once for each rule
|
|
for rule in MAC_TYPE_ICON_RULES:
|
|
icon_html = rule.get("icon_html", "")
|
|
if icon_html:
|
|
# encode icon_html to base64 string
|
|
b64_bytes = base64.b64encode(icon_html.encode("utf-8"))
|
|
rule["icon_base64"] = b64_bytes.decode("utf-8")
|
|
else:
|
|
rule["icon_base64"] = ""
|
|
except Exception as e:
|
|
MAC_TYPE_ICON_RULES = []
|
|
mylog("none", f"[guess_device_attributes] Failed to load device_heuristics_rules.json: {e}",)
|
|
|
|
|
|
# -----------------------------------------
|
|
# Match device type and base64-encoded icon using MAC prefix and vendor patterns.
|
|
def match_mac_and_vendor(
|
|
mac_clean: str, vendor: str, default_type: str, default_icon: str
|
|
) -> Tuple[str, str]:
|
|
"""
|
|
Match device type and base64-encoded icon using MAC prefix and vendor patterns.
|
|
|
|
Args:
|
|
mac_clean: Cleaned MAC address (uppercase, no colons).
|
|
vendor: Normalized vendor name (lowercase).
|
|
default_type: Fallback device type.
|
|
default_icon: Fallback base64 icon.
|
|
|
|
Returns:
|
|
Tuple containing (device_type, base64_icon)
|
|
"""
|
|
for rule in MAC_TYPE_ICON_RULES:
|
|
dev_type = rule.get("dev_type")
|
|
base64_icon = rule.get("icon_base64", "")
|
|
patterns = rule.get("matching_pattern", [])
|
|
|
|
for pattern in patterns:
|
|
mac_prefix = pattern.get("mac_prefix", "").upper()
|
|
vendor_pattern = pattern.get("vendor", "").lower()
|
|
|
|
if mac_clean.startswith(mac_prefix):
|
|
if not vendor_pattern or vendor_pattern in vendor:
|
|
mylog("debug", "[guess_device_attributes] Matched via MAC+Vendor")
|
|
|
|
type_ = dev_type
|
|
icon = base64_icon or default_icon
|
|
return type_, icon
|
|
|
|
return default_type, default_icon
|
|
|
|
|
|
# ---------------------------------------------------
|
|
# Match device type and base64-encoded icon using vendor patterns.
|
|
def match_vendor(vendor: str, default_type: str, default_icon: str) -> Tuple[str, str]:
|
|
vendor_lc = vendor.lower()
|
|
|
|
for rule in MAC_TYPE_ICON_RULES:
|
|
dev_type = rule.get("dev_type")
|
|
base64_icon = rule.get("icon_base64", "")
|
|
patterns = rule.get("matching_pattern", [])
|
|
|
|
for pattern in patterns:
|
|
# Only apply fallback when no MAC prefix is specified
|
|
# mac_prefix = pattern.get("mac_prefix", "")
|
|
vendor_pattern = pattern.get("vendor", "").lower()
|
|
|
|
if vendor_pattern and vendor_pattern in vendor_lc:
|
|
mylog("debug", "[guess_device_attributes] Matched via Vendor")
|
|
|
|
icon = base64_icon or default_icon
|
|
|
|
return dev_type, icon
|
|
|
|
return default_type, default_icon
|
|
|
|
|
|
# ---------------------------------------------------
|
|
# Match device type and base64-encoded icon using name patterns.
|
|
def match_name(name: str, default_type: str, default_icon: str) -> Tuple[str, str]:
|
|
"""
|
|
Match device type and base64-encoded icon using name patterns from global MAC_TYPE_ICON_RULES.
|
|
|
|
Args:
|
|
name: Normalized device name (lowercase).
|
|
default_type: Fallback device type.
|
|
default_icon: Fallback base64 icon.
|
|
|
|
Returns:
|
|
Tuple containing (device_type, base64_icon)
|
|
"""
|
|
name_lower = name.lower() if name else ""
|
|
|
|
for rule in MAC_TYPE_ICON_RULES:
|
|
dev_type = rule.get("dev_type")
|
|
base64_icon = rule.get("icon_base64", "")
|
|
name_patterns = rule.get("name_pattern", [])
|
|
|
|
for pattern in name_patterns:
|
|
# Use regex search to allow pattern substrings
|
|
if re.search(pattern, name_lower, re.IGNORECASE):
|
|
mylog("debug", "[guess_device_attributes] Matched via Name")
|
|
|
|
type_ = dev_type
|
|
icon = base64_icon or default_icon
|
|
return type_, icon
|
|
|
|
return default_type, default_icon
|
|
|
|
|
|
# -------------------------------------------------------------------------------
|
|
#
|
|
def match_ip(ip: str, default_type: str, default_icon: str) -> Tuple[str, str]:
|
|
"""
|
|
Match device type and base64-encoded icon using IP regex patterns from global JSON.
|
|
|
|
Args:
|
|
ip: Device IP address as string.
|
|
default_type: Fallback device type.
|
|
default_icon: Fallback base64 icon.
|
|
|
|
Returns:
|
|
Tuple containing (device_type, base64_icon)
|
|
"""
|
|
if not ip:
|
|
return default_type, default_icon
|
|
|
|
for rule in MAC_TYPE_ICON_RULES:
|
|
ip_patterns = rule.get("ip_pattern", [])
|
|
dev_type = rule.get("dev_type")
|
|
base64_icon = rule.get("icon_base64", "")
|
|
|
|
for pattern in ip_patterns:
|
|
if re.match(pattern, ip):
|
|
mylog("debug", "[guess_device_attributes] Matched via IP")
|
|
|
|
type_ = dev_type
|
|
icon = base64_icon or default_icon
|
|
return type_, icon
|
|
|
|
return default_type, default_icon
|
|
|
|
|
|
# -------------------------------------------------------------------------------
|
|
# 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]:
|
|
|
|
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"
|
|
name = str(name).lower().strip() if name else "(unknown)"
|
|
mac_clean = mac.replace(":", "").replace("-", "").upper()
|
|
|
|
# # Internet shortcut
|
|
# if mac == "INTERNET":
|
|
# return ICONS.get("globe", default_icon), DEVICE_TYPES.get("Internet", default_type)
|
|
|
|
type_ = None
|
|
icon = None
|
|
|
|
# --- Strict MAC + vendor rule matching from external file ---
|
|
type_, icon = match_mac_and_vendor(mac_clean, vendor, default_type, default_icon)
|
|
|
|
# --- Loose Vendor-based fallback ---
|
|
if not type_ or type_ == default_type:
|
|
type_, icon = match_vendor(vendor, default_type, default_icon)
|
|
|
|
# --- Loose Name-based fallback ---
|
|
if not type_ or type_ == default_type:
|
|
type_, icon = match_name(name, default_type, default_icon)
|
|
|
|
# --- Loose IP-based fallback ---
|
|
if (not type_ or type_ == default_type) or (not icon or icon == default_icon):
|
|
type_, icon = match_ip(ip, default_type, default_icon)
|
|
|
|
# Final fallbacks
|
|
type_ = type_ or default_type
|
|
icon = icon or default_icon
|
|
|
|
mylog("debug", f"[guess_device_attributes] Guessed attributes (icon|type_): ('{icon}'|'{type_}')",)
|
|
return icon, type_
|
|
|
|
|
|
# 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.
|
|
"""
|
|
|
|
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.
|
|
"""
|
|
|
|
_, 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.")
|