diff --git a/backend/app/data/oui_database.json b/backend/app/data/oui_database.json new file mode 100644 index 0000000..af99dd8 --- /dev/null +++ b/backend/app/data/oui_database.json @@ -0,0 +1,221 @@ +[ + { + "vendor": "Proxmox / QEMU / KVM", + "type": "vm", + "prefixes": ["52:54:00", "bc:24:11"] + }, + { + "vendor": "VMware", + "type": "vm", + "prefixes": ["00:50:56", "00:0c:29", "00:05:69", "00:1c:14"] + }, + { + "vendor": "VirtualBox", + "type": "vm", + "prefixes": ["08:00:27"] + }, + { + "vendor": "Microsoft Hyper-V", + "type": "vm", + "prefixes": ["00:15:5d"] + }, + { + "vendor": "Xen", + "type": "vm", + "prefixes": ["00:16:3e"] + }, + + { + "vendor": "MikroTik", + "type": "router", + "prefixes": [ + "00:0c:42", + "08:55:31", + "18:fd:74", + "2c:c8:1b", + "48:8f:5a", + "4c:5e:0c", + "64:d1:54", + "6c:3b:6b", + "74:4d:28", + "b8:69:f4", + "c4:ad:34", + "cc:2d:e0", + "d4:ca:6d", + "dc:2c:6e", + "e4:8d:8c" + ] + }, + { + "vendor": "Ubiquiti", + "type": "ap", + "prefixes": [ + "00:15:6d", + "00:27:22", + "04:18:d6", + "24:5a:4c", + "24:a4:3c", + "44:d9:e7", + "68:72:51", + "68:d7:9a", + "74:83:c2", + "78:8a:20", + "78:45:58", + "80:2a:a8", + "94:2a:6f", + "9c:05:d6", + "b4:fb:e4", + "dc:9f:db", + "e0:63:da", + "f0:9f:c2", + "fc:ec:da" + ] + }, + { + "vendor": "Ruckus Wireless", + "type": "ap", + "prefixes": ["00:13:92", "4c:b1:cd", "8c:7a:15", "f0:b0:52", "c0:8a:de"] + }, + { + "vendor": "Aruba Networks (HPE)", + "type": "ap", + "prefixes": ["00:0b:86", "6c:f3:7f", "94:b4:0f", "9c:1c:12", "ac:a3:1e"] + }, + + { + "vendor": "Cisco Systems", + "type": "switch", + "prefixes": [ + "00:00:0c", + "00:1b:0d", + "00:1c:f6", + "00:1e:13", + "00:23:04", + "00:24:13", + "00:25:45", + "00:50:0b", + "b0:00:b4", + "b8:38:61", + "f8:c0:01" + ] + }, + { + "vendor": "Juniper Networks", + "type": "switch", + "prefixes": ["00:14:f6", "2c:6b:f5", "b0:c6:9a", "f0:1c:2d"] + }, + { + "vendor": "Zyxel", + "type": "switch", + "prefixes": ["00:13:49", "60:31:97", "ec:43:f6"] + }, + + { + "vendor": "Netgear", + "type": "router", + "prefixes": ["00:09:5b", "28:c6:8e", "c0:ff:d4", "2c:30:33", "a0:40:a0"] + }, + { + "vendor": "TP-Link", + "type": "router", + "prefixes": ["14:eb:b6", "60:e3:27", "b0:4e:26", "c4:e9:0a", "ec:08:6b"] + }, + + { + "vendor": "Synology", + "type": "nas", + "prefixes": ["00:11:32", "00:f4:6f", "90:09:d0"] + }, + { + "vendor": "QNAP Systems", + "type": "nas", + "prefixes": ["00:08:9b", "00:0e:23", "00:13:42", "04:f0:21", "24:5e:be"] + }, + { + "vendor": "Asustor", + "type": "nas", + "prefixes": ["e8:9c:25"] + }, + + { + "vendor": "Hikvision", + "type": "camera", + "prefixes": ["28:57:be", "44:19:b6", "b4:a3:82", "bc:ad:28", "c0:51:7e", "c0:56:e3", "c4:2f:90"] + }, + { + "vendor": "Dahua / Amcrest", + "type": "camera", + "prefixes": ["3c:ef:8c", "4c:11:bf", "90:02:a9", "bc:32:5f", "e0:50:8b"] + }, + { + "vendor": "Reolink", + "type": "camera", + "prefixes": ["ec:71:db"] + }, + { + "vendor": "Axis Communications", + "type": "camera", + "prefixes": ["00:40:8c", "ac:cc:8e"] + }, + + { + "vendor": "Raspberry Pi Foundation", + "type": "server", + "prefixes": ["28:cd:c1", "2c:cf:67", "b8:27:eb", "d8:3a:dd", "dc:a6:32", "e4:5f:01"] + }, + { + "vendor": "Dell", + "type": "server", + "prefixes": ["00:14:22", "90:b1:1c", "b0:83:fe", "b8:ca:3a", "f8:b1:56"] + }, + { + "vendor": "Supermicro", + "type": "server", + "prefixes": ["00:25:90", "0c:c4:7a", "ac:1f:6b"] + }, + + { + "vendor": "Shelly", + "type": "iot", + "prefixes": ["30:c6:f7", "34:94:54", "84:f3:eb", "ec:fa:bc"] + }, + { + "vendor": "Espressif (ESP8266 / ESP32)", + "type": "iot", + "prefixes": [ + "24:62:ab", + "30:ae:a4", + "3c:71:bf", + "8c:aa:b5", + "a0:20:a6", + "ac:67:b2", + "b4:e6:2d", + "cc:50:e3" + ] + }, + { + "vendor": "Sonoff / ITEAD", + "type": "iot", + "prefixes": ["dc:4f:22", "e8:db:84"] + }, + { + "vendor": "TP-Link Tapo / Kasa", + "type": "iot", + "prefixes": ["10:27:f5", "1c:3b:f3", "50:c7:bf", "b0:a7:b9"] + }, + { + "vendor": "Philips Hue", + "type": "iot", + "prefixes": ["00:17:88", "ec:b5:fa"] + }, + { + "vendor": "IKEA Tradfri", + "type": "iot", + "prefixes": ["00:21:2e", "34:13:e8"] + }, + { + "vendor": "Tuya / Smart Life", + "type": "iot", + "prefixes": ["68:57:2d", "d8:f1:5b"] + } +] diff --git a/backend/app/services/fingerprint.py b/backend/app/services/fingerprint.py index 1a2b4a8..460bdf3 100644 --- a/backend/app/services/fingerprint.py +++ b/backend/app/services/fingerprint.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Any _SIGNATURES: list[dict[str, Any]] | None = None +_OUI_MAP: dict[str, str] | None = None _LOCK = threading.Lock() @@ -26,6 +27,29 @@ def _load() -> list[dict[str, Any]]: return _SIGNATURES +def _load_oui() -> dict[str, str]: + """Load OUI database and flatten to {prefix: node_type}.""" + global _OUI_MAP + if _OUI_MAP is None: + with _LOCK: + if _OUI_MAP is None: + path = Path(__file__).parent.parent / "data" / "oui_database.json" + try: + with open(path) as f: + entries = json.load(f) + except FileNotFoundError as err: + raise FileNotFoundError( + f"oui_database.json not found at {path}. " + "This file should be bundled with the application." + ) from err + _OUI_MAP = { + prefix.lower(): entry["type"] + for entry in entries + for prefix in entry["prefixes"] + } + return _OUI_MAP + + def match_port(port: int, protocol: str, banner: str | None = None) -> dict[str, Any] | None: """Return the first signature matching port+protocol, optionally banner.""" for sig in _load(): @@ -65,55 +89,12 @@ def fingerprint_ports(open_ports: list[dict[str, Any]]) -> list[dict[str, Any]]: return results -# Known OUI prefixes — lowercase, colon-separated, first 3 octets -_MAC_OUI_TYPES: dict[str, str] = { - # Hypervisors / VMs - "52:54:00": "vm", # QEMU/KVM (Proxmox VMs) - "bc:24:11": "vm", # Proxmox official OUI (VMs and LXC, 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 - # Shelly - "34:94:54": "iot", - "84:f3:eb": "iot", - "ec:fa:bc": "iot", - "30:c6:f7": "iot", - # Espressif (ESP8266 / ESP32 — used by Sonoff, many DIY IoT) - "a0:20:a6": "iot", - "24:62:ab": "iot", - "30:ae:a4": "iot", - "cc:50:e3": "iot", - "ac:67:b2": "iot", - "b4:e6:2d": "iot", - "3c:71:bf": "iot", - "8c:aa:b5": "iot", - # Sonoff / ITEAD - "dc:4f:22": "iot", - "e8:db:84": "iot", - # Tapo / TP-Link smart home - "b0:a7:b9": "iot", - "50:c7:bf": "iot", - "1c:3b:f3": "iot", - "10:27:f5": "iot", - # Philips Hue - "00:17:88": "iot", - "ec:b5:fa": "iot", - # IKEA Tradfri - "34:13:e8": "iot", - "00:21:2e": "iot", - # Tuya / Smart Life (widely used chip in many brands) - "d8:f1:5b": "iot", - "68:57:2d": "iot", -} - - 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) + return _load_oui().get(prefix) _PORT_TYPE_HINTS: dict[int, str] = { diff --git a/backend/requirements.txt b/backend/requirements.txt index 01720fc..0b02927 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -15,7 +15,7 @@ pyyaml==6.0.2 types-PyYAML==6.0.12.20240917 websockets==13.1 httpx==0.27.2 -zeroconf==0.149.12 +zeroconf==0.149.16 aiomqtt==2.3.0 # Dev diff --git a/backend/tests/test_fingerprint.py b/backend/tests/test_fingerprint.py index 713b9bf..168b437 100644 --- a/backend/tests/test_fingerprint.py +++ b/backend/tests/test_fingerprint.py @@ -2,7 +2,12 @@ from unittest.mock import patch import pytest -from app.services.fingerprint import fingerprint_ports, match_port, suggest_node_type +from app.services.fingerprint import ( + fingerprint_ports, + match_port, + suggest_node_type, + suggest_type_from_mac, +) MOCK_SIGNATURES = [ {"port": 80, "protocol": "tcp", "banner_regex": None, "service_name": "HTTP", "icon": "🌐", "category": "web", "suggested_node_type": "server"}, @@ -173,3 +178,74 @@ def test_suggest_node_type_iot_wins_over_server_when_mqtt_present(): {"port": 1883, "protocol": "tcp"}, ]) assert result == "iot" + + +# ── OUI vendor detection ────────────────────────────────────────────────────── + +def test_suggest_type_from_mac_mikrotik_returns_router(): + # The motivating case: MikroTik MAC should be recognized as a router + assert suggest_type_from_mac("4c:5e:0c:11:22:33") == "router" + assert suggest_type_from_mac("b8:69:f4:aa:bb:cc") == "router" + + +def test_suggest_type_from_mac_ubiquiti_returns_ap(): + # Ubiquiti makes routers, switches, APs, cameras — most homelab gear is UniFi APs, + # so OUI defaults to "ap". Port hints can still upgrade to "router" if BGP/VPN open. + assert suggest_type_from_mac("24:a4:3c:11:22:33") == "ap" + assert suggest_type_from_mac("fc:ec:da:aa:bb:cc") == "ap" + + +def test_suggest_type_from_mac_synology_returns_nas(): + assert suggest_type_from_mac("00:11:32:11:22:33") == "nas" + + +def test_suggest_type_from_mac_qnap_returns_nas(): + assert suggest_type_from_mac("24:5e:be:aa:bb:cc") == "nas" + + +def test_suggest_type_from_mac_hikvision_returns_camera(): + assert suggest_type_from_mac("28:57:be:11:22:33") == "camera" + + +def test_suggest_type_from_mac_dahua_returns_camera(): + assert suggest_type_from_mac("3c:ef:8c:aa:bb:cc") == "camera" + + +def test_suggest_type_from_mac_cisco_returns_switch(): + assert suggest_type_from_mac("b8:38:61:11:22:33") == "switch" + + +def test_suggest_type_from_mac_raspberry_pi_returns_server(): + assert suggest_type_from_mac("b8:27:eb:11:22:33") == "server" + + +def test_suggest_type_from_mac_handles_uppercase(): + # MACs may arrive in any case; lookup must be case-insensitive + assert suggest_type_from_mac("4C:5E:0C:11:22:33") == "router" + + +def test_suggest_type_from_mac_unknown_oui_returns_none(): + assert suggest_type_from_mac("00:00:01:11:22:33") is None + + +def test_suggest_node_type_mikrotik_mac_returns_router_no_ports(): + # MikroTik device with no scanned ports should still be classified as router via MAC + assert suggest_node_type([], mac="4c:5e:0c:11:22:33") == "router" + + +def test_suggest_node_type_synology_mac_with_http_returns_nas(): + # NAS priority beats server, so a Synology MAC + open HTTP → nas + result = suggest_node_type( + [{"port": 80, "protocol": "tcp"}], + mac="00:11:32:11:22:33", + ) + assert result == "nas" + + +def test_suggest_node_type_ubiquiti_mac_with_bgp_upgrades_to_router(): + # Ubiquiti OUI suggests "ap", but BGP port hint upgrades to "router" (higher priority) + result = suggest_node_type( + [{"port": 179, "protocol": "tcp"}], + mac="24:a4:3c:11:22:33", + ) + assert result == "router" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7a7b97d..20a51d2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -118,12 +118,12 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -132,29 +132,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -171,13 +171,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -199,13 +199,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -236,9 +236,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -258,27 +258,27 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -339,52 +339,52 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -519,31 +519,31 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -551,13 +551,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -6638,9 +6638,19 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9138,9 +9148,9 @@ } }, "node_modules/undici": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz", - "integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "dev": true, "license": "MIT", "engines": {