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
This commit is contained in:
@@ -55,6 +55,25 @@ def fingerprint_ports(open_ports: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|||||||
return results
|
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] = {
|
_PORT_TYPE_HINTS: dict[int, str] = {
|
||||||
# Proxmox
|
# Proxmox
|
||||||
8006: "proxmox",
|
8006: "proxmox",
|
||||||
@@ -85,8 +104,8 @@ _PORT_TYPE_HINTS: dict[int, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def suggest_node_type(open_ports: list[dict[str, Any]]) -> str:
|
def suggest_node_type(open_ports: list[dict[str, Any]], mac: str | None = None) -> str:
|
||||||
"""Suggest a node type based on the most specific matched signature."""
|
"""Suggest a node type based on matched signatures and MAC OUI."""
|
||||||
priority = ["proxmox", "nas", "router", "lxc", "vm", "server", "ap", "camera", "iot", "switch"]
|
priority = ["proxmox", "nas", "router", "lxc", "vm", "server", "ap", "camera", "iot", "switch"]
|
||||||
found: set[str] = set()
|
found: set[str] = set()
|
||||||
for p in open_ports:
|
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"])
|
found.add(sig["suggested_node_type"])
|
||||||
if port in _PORT_TYPE_HINTS:
|
if port in _PORT_TYPE_HINTS:
|
||||||
found.add(_PORT_TYPE_HINTS[port])
|
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:
|
for t in priority:
|
||||||
if t in found:
|
if t in found:
|
||||||
return t
|
return t
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ async def run_scan(ranges: list[str], db: AsyncSession, run_id: str) -> None:
|
|||||||
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
services = fingerprint_ports(host["open_ports"])
|
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
|
# Update existing pending device or create a new one
|
||||||
existing_result = await db.execute(
|
existing_result = await db.execute(
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ function PendingDevicesPanel({ onNodeApproved }: { onNodeApproved: (nodeId: stri
|
|||||||
const hasHttp = d.services.some((s) => s.port === 80)
|
const hasHttp = d.services.some((s) => s.port === 80)
|
||||||
const hasHttps = d.services.some((s) => s.port === 443)
|
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 otherCount = d.services.filter((s) => s.port !== 22 && s.port !== 80 && s.port !== 443).length
|
||||||
|
const virtualBadge = detectVirtualBadge(d.mac)
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={d.id}
|
key={d.id}
|
||||||
@@ -248,8 +249,16 @@ function PendingDevicesPanel({ onNodeApproved }: { onNodeApproved: (nodeId: stri
|
|||||||
{showIpBelow && (
|
{showIpBelow && (
|
||||||
<div className="font-mono text-muted-foreground truncate pl-3 text-[10px] mt-0.5">{d.ip}</div>
|
<div className="font-mono text-muted-foreground truncate pl-3 text-[10px] mt-0.5">{d.ip}</div>
|
||||||
)}
|
)}
|
||||||
{(hasSsh || hasHttp || hasHttps || otherCount > 0) && (
|
{(hasSsh || hasHttp || hasHttps || otherCount > 0 || virtualBadge) && (
|
||||||
<div className="flex items-center gap-1 pl-3 mt-1.5 flex-wrap">
|
<div className="flex items-center gap-1 pl-3 mt-1.5 flex-wrap">
|
||||||
|
{virtualBadge && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span><ServiceBadge label={virtualBadge.label} color="#ff6e00" /></span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">{virtualBadge.title}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{hasSsh && <ServiceBadge label="SSH" color="#a855f7" />}
|
{hasSsh && <ServiceBadge label="SSH" color="#a855f7" />}
|
||||||
{hasHttp && <ServiceBadge label="HTTP" color="#00d4ff" />}
|
{hasHttp && <ServiceBadge label="HTTP" color="#00d4ff" />}
|
||||||
{hasHttps && <ServiceBadge label="HTTPS" color="#39d353" />}
|
{hasHttps && <ServiceBadge label="HTTPS" color="#39d353" />}
|
||||||
@@ -402,6 +411,20 @@ function ScanHistoryPanel() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAC_OUI: Record<string, { label: string; title: string }> = {
|
||||||
|
'52:54:00': { label: 'QEMU', title: 'QEMU/KVM Virtual Machine' },
|
||||||
|
'bc:24:11': { label: 'PVE', title: 'Proxmox Virtual Machine or LXC' },
|
||||||
|
'00:50:56': { label: 'VMware', title: 'VMware Virtual Machine' },
|
||||||
|
'00:0c:29': { label: 'VMware', title: 'VMware Virtual Machine' },
|
||||||
|
'08:00:27': { label: 'VBox', title: 'VirtualBox Virtual Machine' },
|
||||||
|
'00:15:5d': { label: 'Hyper-V', title: 'Hyper-V Virtual Machine' },
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectVirtualBadge(mac: string | null) {
|
||||||
|
if (!mac) return null
|
||||||
|
return MAC_OUI[mac.toLowerCase().slice(0, 8)] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
function ServiceBadge({ label, color }: { label: string; color: string }) {
|
function ServiceBadge({ label, color }: { label: string; color: string }) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
|
|||||||
Reference in New Issue
Block a user