diff --git a/homeassistant/components/unifi/diagnostics.py b/homeassistant/components/unifi/diagnostics.py new file mode 100644 index 00000000000..a26fdd447d9 --- /dev/null +++ b/homeassistant/components/unifi/diagnostics.py @@ -0,0 +1,99 @@ +"""Diagnostics support for UniFi Network.""" +from __future__ import annotations + +from collections.abc import Mapping +from itertools import chain +from typing import Any + +from homeassistant.components.diagnostics import REDACTED, async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import format_mac + +from .const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN + +TO_REDACT = {CONF_CONTROLLER, CONF_PASSWORD} +REDACT_CLIENTS = {"bssid", "essid"} +REDACT_DEVICES = { + "anon_id", + "gateway_mac", + "geo_info", + "serial", + "x_authkey", + "x_fingerprint", + "x_iapp_key", + "x_ssh_hostkey_fingerprint", + "x_vwirekey", +} +REDACT_WLANS = {"bc_filter_list", "x_passphrase"} + + +@callback +def async_replace_data(data: Mapping, to_replace: dict[str, str]) -> dict[str, Any]: + """Replace sensitive data in a dict.""" + if not isinstance(data, (Mapping, list, set, tuple)): + return to_replace.get(data, data) + + redacted = {**data} + + for key, value in redacted.items(): + if isinstance(value, dict): + redacted[key] = async_replace_data(value, to_replace) + elif isinstance(value, (list, set, tuple)): + redacted[key] = [async_replace_data(item, to_replace) for item in value] + elif isinstance(value, str): + if value in to_replace: + redacted[key] = to_replace[value] + elif value.count(":") == 5: + redacted[key] = REDACTED + + return redacted + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + diag: dict[str, Any] = {} + macs_to_redact: dict[str, str] = {} + + diag["config"] = async_redact_data(config_entry.data, TO_REDACT) + diag["site_role"] = controller.site_role + + counter = 0 + for mac in chain(controller.api.clients, controller.api.devices): + macs_to_redact[mac] = format_mac(str(counter).zfill(12)) + counter += 1 + + for device in controller.api.devices.values(): + for entry in device.raw.get("ethernet_table", []): + mac = entry.get("mac", "") + if mac not in macs_to_redact: + macs_to_redact[mac] = format_mac(str(counter).zfill(12)) + counter += 1 + + diag["options"] = async_replace_data(config_entry.options, macs_to_redact) + + diag["entities"] = async_replace_data(controller.entities, macs_to_redact) + diag["clients"] = { + macs_to_redact[k]: async_redact_data( + async_replace_data(v.raw, macs_to_redact), REDACT_CLIENTS + ) + for k, v in controller.api.clients.items() + } + diag["devices"] = { + macs_to_redact[k]: async_redact_data( + async_replace_data(v.raw, macs_to_redact), REDACT_DEVICES + ) + for k, v in controller.api.devices.items() + } + diag["dpi_apps"] = {k: v.raw for k, v in controller.api.dpi_apps.items()} + diag["dpi_groups"] = {k: v.raw for k, v in controller.api.dpi_groups.items()} + diag["wlans"] = { + k: async_redact_data(async_replace_data(v.raw, macs_to_redact), REDACT_WLANS) + for k, v in controller.api.wlans.items() + } + + return diag diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py new file mode 100644 index 00000000000..f09a5a6862e --- /dev/null +++ b/tests/components/unifi/test_diagnostics.py @@ -0,0 +1,235 @@ +"""Test UniFi Network diagnostics.""" + +from homeassistant.components.unifi.const import ( + CONF_ALLOW_BANDWIDTH_SENSORS, + CONF_ALLOW_UPTIME_SENSORS, + CONF_BLOCK_CLIENT, +) +from homeassistant.components.unifi.device_tracker import CLIENT_TRACKER, DEVICE_TRACKER +from homeassistant.components.unifi.sensor import RX_SENSOR, TX_SENSOR, UPTIME_SENSOR +from homeassistant.components.unifi.switch import BLOCK_SWITCH, DPI_SWITCH, POE_SWITCH +from homeassistant.const import Platform + +from .test_controller import setup_unifi_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_entry_diagnostics(hass, hass_client, aioclient_mock): + """Test config entry diagnostics.""" + client = { + "blocked": False, + "hostname": "client_1", + "ip": "10.0.0.1", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + "name": "POE Client 1", + "oui": "Producer", + "sw_mac": "00:00:00:00:01:01", + "sw_port": 1, + "wired-rx_bytes": 1234000000, + "wired-tx_bytes": 5678000000, + } + device = { + "ethernet_table": [ + { + "mac": "22:22:22:22:22:22", + "num_port": 2, + "name": "eth0", + } + ], + "device_id": "mock-id", + "ip": "10.0.1.1", + "mac": "00:00:00:00:01:01", + "last_seen": 1562600145, + "model": "US16P150", + "name": "mock-name", + "port_overrides": [], + "port_table": [ + { + "mac_table": [ + { + "age": 1, + "mac": "00:00:00:00:00:01", + "static": False, + "uptime": 3971792, + "vlan": 1, + }, + { + "age": 1, + "mac": "11:11:11:11:11:11", + "static": True, + "uptime": 0, + "vlan": 0, + }, + ], + "media": "GE", + "name": "Port 1", + "port_idx": 1, + "poe_class": "Class 4", + "poe_enable": True, + "poe_mode": "auto", + "poe_power": "2.56", + "poe_voltage": "53.40", + "portconf_id": "1a1", + "port_poe": True, + "up": True, + }, + ], + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + } + dpi_app = { + "_id": "5f976f62e3c58f018ec7e17d", + "apps": [], + "blocked": True, + "cats": ["4"], + "enabled": True, + "log": True, + "site_id": "name", + } + dpi_group = { + "_id": "5f976f4ae3c58f018ec7dff6", + "name": "Block Media Streaming", + "site_id": "name", + "dpiapp_ids": ["5f976f62e3c58f018ec7e17d"], + } + + options = { + CONF_ALLOW_BANDWIDTH_SENSORS: True, + CONF_ALLOW_UPTIME_SENSORS: True, + CONF_BLOCK_CLIENT: ["00:00:00:00:00:01"], + } + config_entry = await setup_unifi_integration( + hass, + aioclient_mock, + options=options, + clients_response=[client], + devices_response=[device], + dpiapp_response=[dpi_app], + dpigroup_response=[dpi_group], + ) + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "config": { + "controller": "**REDACTED**", + "host": "1.2.3.4", + "password": "**REDACTED**", + "port": 1234, + "site": "site_id", + "username": "username", + "verify_ssl": False, + }, + "options": { + "allow_bandwidth_sensors": True, + "allow_uptime_sensors": True, + "block_client": ["00:00:00:00:00:00"], + }, + "site_role": "admin", + "entities": { + str(Platform.DEVICE_TRACKER): { + CLIENT_TRACKER: ["00:00:00:00:00:00"], + DEVICE_TRACKER: ["00:00:00:00:00:01"], + }, + str(Platform.SENSOR): { + RX_SENSOR: ["00:00:00:00:00:00"], + TX_SENSOR: ["00:00:00:00:00:00"], + UPTIME_SENSOR: ["00:00:00:00:00:00"], + }, + str(Platform.SWITCH): { + BLOCK_SWITCH: ["00:00:00:00:00:00"], + DPI_SWITCH: ["5f976f4ae3c58f018ec7dff6"], + POE_SWITCH: ["00:00:00:00:00:00"], + }, + }, + "clients": { + "00:00:00:00:00:00": { + "blocked": False, + "hostname": "client_1", + "ip": "10.0.0.1", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:00", + "name": "POE Client 1", + "oui": "Producer", + "sw_mac": "00:00:00:00:00:01", + "sw_port": 1, + "wired-rx_bytes": 1234000000, + "wired-tx_bytes": 5678000000, + } + }, + "devices": { + "00:00:00:00:00:01": { + "ethernet_table": [ + { + "mac": "00:00:00:00:00:02", + "num_port": 2, + "name": "eth0", + } + ], + "device_id": "mock-id", + "ip": "10.0.1.1", + "mac": "00:00:00:00:00:01", + "last_seen": 1562600145, + "model": "US16P150", + "name": "mock-name", + "port_overrides": [], + "port_table": [ + { + "mac_table": [ + { + "age": 1, + "mac": "00:00:00:00:00:00", + "static": False, + "uptime": 3971792, + "vlan": 1, + }, + { + "age": 1, + "mac": "**REDACTED**", + "static": True, + "uptime": 0, + "vlan": 0, + }, + ], + "media": "GE", + "name": "Port 1", + "port_idx": 1, + "poe_class": "Class 4", + "poe_enable": True, + "poe_mode": "auto", + "poe_power": "2.56", + "poe_voltage": "53.40", + "portconf_id": "1a1", + "port_poe": True, + "up": True, + }, + ], + "state": 1, + "type": "usw", + "version": "4.0.42.10433", + } + }, + "dpi_apps": { + "5f976f62e3c58f018ec7e17d": { + "_id": "5f976f62e3c58f018ec7e17d", + "apps": [], + "blocked": True, + "cats": ["4"], + "enabled": True, + "log": True, + "site_id": "name", + } + }, + "dpi_groups": { + "5f976f4ae3c58f018ec7dff6": { + "_id": "5f976f4ae3c58f018ec7dff6", + "name": "Block Media Streaming", + "site_id": "name", + "dpiapp_ids": ["5f976f62e3c58f018ec7e17d"], + } + }, + "wlans": {}, + }