From 20d9473f38f39dc893eb4a9aadf6c7a69bf7c4a3 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Mon, 9 Mar 2026 22:22:38 +0100 Subject: [PATCH] feat: detect virtual machines from MAC OUI and show badge on pending devices Backend: - Add MAC OUI lookup table (QEMU 52:54:00, Proxmox bc:24:11, VMware, VBox, Hyper-V) - suggest_node_type() now accepts mac param and uses OUI as a low-priority hint Frontend: - Detect VM type from MAC prefix on pending device cards - Show colored badge (QEMU / PVE / VMware / VBox / Hyper-V) in orange with tooltip --- backend/app/services/fingerprint.py | 27 ++++++++++++++++++++-- backend/app/services/scanner.py | 2 +- frontend/src/components/panels/Sidebar.tsx | 25 +++++++++++++++++++- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/backend/app/services/fingerprint.py b/backend/app/services/fingerprint.py index 38b0f0a..76777be 100644 --- a/backend/app/services/fingerprint.py +++ b/backend/app/services/fingerprint.py @@ -55,6 +55,25 @@ def fingerprint_ports(open_ports: list[dict[str, Any]]) -> list[dict[str, Any]]: return results +# Known OUI prefixes for virtual machines / hypervisors (lowercase, colon-separated) +_MAC_OUI_TYPES: dict[str, str] = { + "52:54:00": "vm", # QEMU/KVM (used by Proxmox VMs) + "bc:24:11": "vm", # Proxmox official OUI (VMs and LXC, Proxmox 7.3+) + "00:50:56": "vm", # VMware + "00:0c:29": "vm", # VMware Workstation / Fusion + "08:00:27": "vm", # VirtualBox + "00:15:5d": "vm", # Hyper-V +} + + +def suggest_type_from_mac(mac: str | None) -> str | None: + """Return a suggested node type from MAC OUI, or None if unknown.""" + if not mac: + return None + prefix = mac.lower()[:8] + return _MAC_OUI_TYPES.get(prefix) + + _PORT_TYPE_HINTS: dict[int, str] = { # Proxmox 8006: "proxmox", @@ -85,8 +104,8 @@ _PORT_TYPE_HINTS: dict[int, str] = { } -def suggest_node_type(open_ports: list[dict[str, Any]]) -> str: - """Suggest a node type based on the most specific matched signature.""" +def suggest_node_type(open_ports: list[dict[str, Any]], mac: str | None = None) -> str: + """Suggest a node type based on matched signatures and MAC OUI.""" priority = ["proxmox", "nas", "router", "lxc", "vm", "server", "ap", "camera", "iot", "switch"] found: set[str] = set() for p in open_ports: @@ -97,6 +116,10 @@ def suggest_node_type(open_ports: list[dict[str, Any]]) -> str: found.add(sig["suggested_node_type"]) if port in _PORT_TYPE_HINTS: found.add(_PORT_TYPE_HINTS[port]) + # MAC OUI is a lower-priority hint — only used if ports give no better answer + mac_type = suggest_type_from_mac(mac) + if mac_type: + found.add(mac_type) for t in priority: if t in found: return t diff --git a/backend/app/services/scanner.py b/backend/app/services/scanner.py index 5d0d00c..85a27ea 100644 --- a/backend/app/services/scanner.py +++ b/backend/app/services/scanner.py @@ -113,7 +113,7 @@ async def run_scan(ranges: list[str], db: AsyncSession, run_id: str) -> None: for host in hosts: services = fingerprint_ports(host["open_ports"]) - suggested_type = suggest_node_type(host["open_ports"]) + suggested_type = suggest_node_type(host["open_ports"], host.get("mac")) # Update existing pending device or create a new one existing_result = await db.execute( diff --git a/frontend/src/components/panels/Sidebar.tsx b/frontend/src/components/panels/Sidebar.tsx index 651abf5..3b37f7b 100644 --- a/frontend/src/components/panels/Sidebar.tsx +++ b/frontend/src/components/panels/Sidebar.tsx @@ -235,6 +235,7 @@ function PendingDevicesPanel({ onNodeApproved }: { onNodeApproved: (nodeId: stri const hasHttp = d.services.some((s) => s.port === 80) const hasHttps = d.services.some((s) => s.port === 443) const otherCount = d.services.filter((s) => s.port !== 22 && s.port !== 80 && s.port !== 443).length + const virtualBadge = detectVirtualBadge(d.mac) return (