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
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 (
|
||||
<button
|
||||
key={d.id}
|
||||
@@ -248,8 +249,16 @@ function PendingDevicesPanel({ onNodeApproved }: { onNodeApproved: (nodeId: stri
|
||||
{showIpBelow && (
|
||||
<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">
|
||||
{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" />}
|
||||
{hasHttp && <ServiceBadge label="HTTP" color="#00d4ff" />}
|
||||
{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 }) {
|
||||
return (
|
||||
<span
|
||||
|
||||
Reference in New Issue
Block a user