From 88634aeb1d606df21fd55a8c78b14b2d55821b0d Mon Sep 17 00:00:00 2001 From: Pouzor Date: Sun, 8 Mar 2026 11:55:26 +0100 Subject: [PATCH] feat: add Camera node type + flag RTSP/camera ports during scan - New 'camera' node type with Camera icon (frontend + types) - Scanner fingerprint: camera ports (554, 8554, 37777, 34567, 2020) now suggest 'camera' instead of 'iot' - service_signatures.json: all camera/NVR entries updated to suggested_node_type=camera - 'camera' added to priority list in suggest_node_type (above iot) --- backend/app/services/fingerprint.py | 14 ++++++------- backend/data/service_signatures.json | 20 +++++++++---------- backend/tests/test_fingerprint.py | 15 ++++++++++++++ .../src/components/canvas/nodes/index.tsx | 3 ++- .../src/components/canvas/nodes/nodeTypes.ts | 3 ++- frontend/src/types/__tests__/types.test.ts | 2 +- frontend/src/types/index.ts | 2 ++ 7 files changed, 39 insertions(+), 20 deletions(-) diff --git a/backend/app/services/fingerprint.py b/backend/app/services/fingerprint.py index 4470776..38b0f0a 100644 --- a/backend/app/services/fingerprint.py +++ b/backend/app/services/fingerprint.py @@ -66,12 +66,12 @@ _PORT_TYPE_HINTS: dict[int, str] = { # Routers / network devices 8291: "router", # MikroTik Winbox 179: "router", # BGP - # Cameras / RTSP → iot - 554: "iot", - 8554: "iot", - 37777: "iot", # Dahua - 34567: "iot", # Amcrest - 2020: "iot", # Tapo + # Cameras / RTSP + 554: "camera", + 8554: "camera", + 37777: "camera", # Dahua + 34567: "camera", # Amcrest + 2020: "camera", # Tapo # Smart-home / MQTT → iot 1883: "iot", 8883: "iot", @@ -87,7 +87,7 @@ _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.""" - priority = ["proxmox", "nas", "router", "lxc", "vm", "server", "ap", "iot", "switch"] + priority = ["proxmox", "nas", "router", "lxc", "vm", "server", "ap", "camera", "iot", "switch"] found: set[str] = set() for p in open_ports: port = p["port"] diff --git a/backend/data/service_signatures.json b/backend/data/service_signatures.json index 25d9ea6..412b14c 100644 --- a/backend/data/service_signatures.json +++ b/backend/data/service_signatures.json @@ -45,20 +45,20 @@ {"port": 8883, "protocol": "tcp", "banner_regex": null, "service_name": "MQTT Broker TLS", "icon": "radio", "category": "iot", "suggested_node_type": "iot"}, {"port": 6052, "protocol": "tcp", "banner_regex": null, "service_name": "ESPHome", "icon": "cpu", "category": "iot", "suggested_node_type": "iot"}, {"port": 1880, "protocol": "tcp", "banner_regex": null, "service_name": "Node-RED", "icon": "git-branch", "category": "automation", "suggested_node_type": "iot"}, - {"port": 8971, "protocol": "tcp", "banner_regex": null, "service_name": "Frigate NVR", "icon": "camera", "category": "nvr", "suggested_node_type": "iot"}, - {"port": 10443, "protocol": "tcp", "banner_regex": null, "service_name": "Scrypted", "icon": "camera", "category": "nvr", "suggested_node_type": "iot"}, - {"port": 5000, "protocol": "tcp", "banner_regex": "frigate", "service_name": "Frigate NVR", "icon": "camera", "category": "nvr", "suggested_node_type": "iot"}, + {"port": 8971, "protocol": "tcp", "banner_regex": null, "service_name": "Frigate NVR", "icon": "camera", "category": "nvr", "suggested_node_type": "camera"}, + {"port": 10443, "protocol": "tcp", "banner_regex": null, "service_name": "Scrypted", "icon": "camera", "category": "nvr", "suggested_node_type": "camera"}, + {"port": 5000, "protocol": "tcp", "banner_regex": "frigate", "service_name": "Frigate NVR", "icon": "camera", "category": "nvr", "suggested_node_type": "camera"}, {"port": 8081, "protocol": "tcp", "banner_regex": "iobroker|ioBroker", "service_name": "ioBroker", "icon": "cpu", "category": "automation", "suggested_node_type": "iot"}, {"port": 8080, "protocol": "tcp", "banner_regex": "Domoticz|domoticz", "service_name": "Domoticz", "icon": "home", "category": "automation", "suggested_node_type": "iot"}, {"port": 5683, "protocol": "udp", "banner_regex": null, "service_name": "CoAP (IoT)", "icon": "radio", "category": "iot", "suggested_node_type": "iot"}, - {"port": 554, "protocol": "tcp", "banner_regex": null, "service_name": "RTSP (Camera)", "icon": "camera", "category": "camera", "suggested_node_type": "iot"}, - {"port": 8554, "protocol": "tcp", "banner_regex": null, "service_name": "RTSP Alt (Camera)", "icon": "camera", "category": "camera", "suggested_node_type": "iot"}, - {"port": 37777, "protocol": "tcp", "banner_regex": null, "service_name": "Dahua Camera SDK", "icon": "camera", "category": "camera", "suggested_node_type": "iot"}, - {"port": 34567, "protocol": "tcp", "banner_regex": null, "service_name": "Amcrest / Dahua Camera", "icon": "camera", "category": "camera", "suggested_node_type": "iot"}, - {"port": 8000, "protocol": "tcp", "banner_regex": "[Hh]ikvision|[Dd]ahua", "service_name": "IP Camera SDK", "icon": "camera", "category": "camera", "suggested_node_type": "iot"}, - {"port": 2020, "protocol": "tcp", "banner_regex": null, "service_name": "TP-Link Tapo Camera", "icon": "camera", "category": "camera", "suggested_node_type": "iot"}, - {"port": 9000, "protocol": "tcp", "banner_regex": "[Rr]eolink", "service_name": "Reolink Camera", "icon": "camera", "category": "camera", "suggested_node_type": "iot"}, + {"port": 554, "protocol": "tcp", "banner_regex": null, "service_name": "RTSP (Camera)", "icon": "camera", "category": "camera", "suggested_node_type": "camera"}, + {"port": 8554, "protocol": "tcp", "banner_regex": null, "service_name": "RTSP Alt (Camera)", "icon": "camera", "category": "camera", "suggested_node_type": "camera"}, + {"port": 37777, "protocol": "tcp", "banner_regex": null, "service_name": "Dahua Camera SDK", "icon": "camera", "category": "camera", "suggested_node_type": "camera"}, + {"port": 34567, "protocol": "tcp", "banner_regex": null, "service_name": "Amcrest / Dahua Camera", "icon": "camera", "category": "camera", "suggested_node_type": "camera"}, + {"port": 8000, "protocol": "tcp", "banner_regex": "[Hh]ikvision|[Dd]ahua", "service_name": "IP Camera SDK", "icon": "camera", "category": "camera", "suggested_node_type": "camera"}, + {"port": 2020, "protocol": "tcp", "banner_regex": null, "service_name": "TP-Link Tapo Camera", "icon": "camera", "category": "camera", "suggested_node_type": "camera"}, + {"port": 9000, "protocol": "tcp", "banner_regex": "[Rr]eolink", "service_name": "Reolink Camera", "icon": "camera", "category": "camera", "suggested_node_type": "camera"}, {"port": 8291, "protocol": "tcp", "banner_regex": null, "service_name": "MikroTik Winbox", "icon": "router", "category": "network", "suggested_node_type": "router"}, {"port": 8880, "protocol": "tcp", "banner_regex": null, "service_name": "UniFi HTTP Portal", "icon": "wifi", "category": "network", "suggested_node_type": "ap"}, diff --git a/backend/tests/test_fingerprint.py b/backend/tests/test_fingerprint.py index 159e595..0b060f0 100644 --- a/backend/tests/test_fingerprint.py +++ b/backend/tests/test_fingerprint.py @@ -116,3 +116,18 @@ def test_suggest_node_type_priority_proxmox_over_server(): # both proxmox and server ports open → proxmox wins (higher priority) result = suggest_node_type([{"port": 80, "protocol": "tcp"}, {"port": 8006, "protocol": "tcp"}]) assert result == "proxmox" + + +def test_suggest_node_type_camera_from_rtsp_port(): + # RTSP port 554 → camera (via _PORT_TYPE_HINTS) + result = suggest_node_type([{"port": 554, "protocol": "tcp"}]) + assert result == "camera" + + +def test_suggest_node_type_camera_from_signature(): + # patch _load to return a camera sig so we test the sig path too + with patch("app.services.fingerprint._load", return_value=[ + {"port": 554, "protocol": "tcp", "banner_regex": None, "service_name": "RTSP", "icon": "camera", "category": "camera", "suggested_node_type": "camera"} + ]): + result = suggest_node_type([{"port": 554, "protocol": "tcp"}]) + assert result == "camera" diff --git a/frontend/src/components/canvas/nodes/index.tsx b/frontend/src/components/canvas/nodes/index.tsx index 4582581..1df32f7 100644 --- a/frontend/src/components/canvas/nodes/index.tsx +++ b/frontend/src/components/canvas/nodes/index.tsx @@ -1,7 +1,7 @@ import { type NodeProps, type Node } from '@xyflow/react' import { Globe, Router, Network, Server, Layers, Box, Container, - HardDrive, Cpu, Wifi, Circle, + HardDrive, Cpu, Wifi, Circle, Camera, } from 'lucide-react' import { BaseNode } from './BaseNode' import type { NodeData } from '@/types' @@ -18,4 +18,5 @@ export const LxcNode = (props: N) => export const IotNode = (props: N) => export const ApNode = (props: N) => +export const CameraNode = (props: N) => export const GenericNode = (props: N) => diff --git a/frontend/src/components/canvas/nodes/nodeTypes.ts b/frontend/src/components/canvas/nodes/nodeTypes.ts index 91ebfaa..1a670e7 100644 --- a/frontend/src/components/canvas/nodes/nodeTypes.ts +++ b/frontend/src/components/canvas/nodes/nodeTypes.ts @@ -1,4 +1,4 @@ -import { IspNode, RouterNode, SwitchNode, ServerNode, VmNode, LxcNode, NasNode, IotNode, ApNode, GenericNode } from './index' +import { IspNode, RouterNode, SwitchNode, ServerNode, VmNode, LxcNode, NasNode, IotNode, ApNode, CameraNode, GenericNode } from './index' import { ProxmoxGroupNode } from './ProxmoxGroupNode' export const nodeTypes = { @@ -12,5 +12,6 @@ export const nodeTypes = { nas: NasNode, iot: IotNode, ap: ApNode, + camera: CameraNode, generic: GenericNode, } diff --git a/frontend/src/types/__tests__/types.test.ts b/frontend/src/types/__tests__/types.test.ts index 094254d..8191c72 100644 --- a/frontend/src/types/__tests__/types.test.ts +++ b/frontend/src/types/__tests__/types.test.ts @@ -4,7 +4,7 @@ import type { CheckMethod } from '@/types' describe('NODE_TYPE_LABELS', () => { it('has an entry for every node type', () => { - const expectedTypes = ['isp', 'router', 'switch', 'server', 'proxmox', 'vm', 'lxc', 'nas', 'iot', 'ap', 'generic'] + const expectedTypes = ['isp', 'router', 'switch', 'server', 'proxmox', 'vm', 'lxc', 'nas', 'iot', 'ap', 'camera', 'generic'] expectedTypes.forEach((t) => { expect(NODE_TYPE_LABELS).toHaveProperty(t) expect(typeof NODE_TYPE_LABELS[t as keyof typeof NODE_TYPE_LABELS]).toBe('string') diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index f3146fd..4c2cc19 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -9,6 +9,7 @@ export type NodeType = | 'nas' | 'iot' | 'ap' + | 'camera' | 'generic' export type EdgeType = 'ethernet' | 'wifi' | 'iot' | 'vlan' | 'virtual' | 'cluster' @@ -67,6 +68,7 @@ export const NODE_TYPE_LABELS: Record = { nas: 'NAS', iot: 'IoT Device', ap: 'Access Point', + camera: 'Camera', generic: 'Generic Device', }