Redact Z-Wave add-on options sensitive error details (#167239)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Oluwatobi Mustapha
2026-04-08 21:17:26 +02:00
committed by GitHub
co-authored by Copilot
parent b0511519a1
commit 4c8a660b2d
4 changed files with 118 additions and 6 deletions
+55 -3
View File
@@ -2,17 +2,69 @@
from __future__ import annotations
from homeassistant.components.hassio import AddonManager
from typing import Any
from homeassistant.components.hassio import AddonError, AddonManager
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.redact import async_redact_data
from homeassistant.helpers.singleton import singleton
from .const import ADDON_SLUG, DOMAIN, LOGGER
from .const import (
ADDON_SLUG,
CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY,
CONF_ADDON_LR_S2_AUTHENTICATED_KEY,
CONF_ADDON_NETWORK_KEY,
CONF_ADDON_S0_LEGACY_KEY,
CONF_ADDON_S2_ACCESS_CONTROL_KEY,
CONF_ADDON_S2_AUTHENTICATED_KEY,
CONF_ADDON_S2_UNAUTHENTICATED_KEY,
DOMAIN,
LOGGER,
)
DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager"
REDACT_ADDON_OPTION_KEYS = {
CONF_ADDON_S0_LEGACY_KEY,
CONF_ADDON_S2_ACCESS_CONTROL_KEY,
CONF_ADDON_S2_AUTHENTICATED_KEY,
CONF_ADDON_S2_UNAUTHENTICATED_KEY,
CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY,
CONF_ADDON_LR_S2_AUTHENTICATED_KEY,
CONF_ADDON_NETWORK_KEY,
}
def _redact_sensitive_option_values(message: str, config: dict[str, Any]) -> str:
"""Redact sensitive add-on option values in an error string."""
redacted_config = async_redact_data(config, REDACT_ADDON_OPTION_KEYS)
for key in REDACT_ADDON_OPTION_KEYS:
option_value = config.get(key)
if not isinstance(option_value, str) or not option_value:
continue
redacted_value = redacted_config.get(key)
if not isinstance(redacted_value, str):
continue
message = message.replace(option_value, redacted_value)
return message
class ZwaveAddonManager(AddonManager):
"""Addon manager for Z-Wave JS with redacted option errors."""
async def async_set_addon_options(self, config: dict[str, Any]) -> None:
"""Set add-on options."""
try:
await super().async_set_addon_options(config)
except AddonError as err:
raise AddonError(
_redact_sensitive_option_values(str(err), config)
) from None
@singleton(DATA_ADDON_MANAGER)
@callback
def get_addon_manager(hass: HomeAssistant) -> AddonManager:
"""Get the add-on manager."""
return AddonManager(hass, LOGGER, "Z-Wave JS", ADDON_SLUG)
return ZwaveAddonManager(hass, LOGGER, "Z-Wave JS", ADDON_SLUG)
+1
View File
@@ -46,6 +46,7 @@ HUMIDIFIER_ADC_T3000_ENTITY = "humidifier.adc_t3000_humidifier"
DEHUMIDIFIER_ADC_T3000_ENTITY = "humidifier.adc_t3000_dehumidifier"
PROPERTY_ULTRAVIOLET = "Ultraviolet"
TEST_SENSITIVE_NETWORK_KEY = "00112233445566778899AABBCCDDEEFF"
def replace_value_of_zwave_value(
+20 -3
View File
@@ -38,11 +38,14 @@ from homeassistant.components.zwave_js.helpers import SERVER_VERSION_TIMEOUT
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.redact import REDACTED
from homeassistant.helpers.service_info.esphome import ESPHomeServiceInfo
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from homeassistant.helpers.service_info.usb import UsbServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .common import TEST_SENSITIVE_NETWORK_KEY
from tests.common import MockConfigEntry, async_capture_events
ADDON_DISCOVERY_INFO = {
@@ -2542,13 +2545,23 @@ async def test_addon_installed_failures(
@pytest.mark.usefixtures("supervisor", "addon_installed", "addon_info")
@pytest.mark.parametrize("set_addon_options_side_effect", [SupervisorError()])
@pytest.mark.parametrize(
"set_addon_options_side_effect",
[
SupervisorError(
"not a valid value for dictionary value @ data['options']. "
f"Got {{'s0_legacy_key': '{TEST_SENSITIVE_NETWORK_KEY}'}}"
)
],
)
async def test_addon_installed_set_options_failure(
hass: HomeAssistant,
set_addon_options: AsyncMock,
start_addon: AsyncMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test all failures when add-on is installed."""
secret = TEST_SENSITIVE_NETWORK_KEY
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@@ -2594,7 +2607,7 @@ async def test_addon_installed_set_options_failure(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"s0_legacy_key": "new123",
"s0_legacy_key": secret,
"s2_access_control_key": "new456",
"s2_authenticated_key": "new789",
"s2_unauthenticated_key": "new987",
@@ -2608,7 +2621,7 @@ async def test_addon_installed_set_options_failure(
AddonsOptions(
config={
"device": "/test",
"s0_legacy_key": "new123",
"s0_legacy_key": secret,
"s2_access_control_key": "new456",
"s2_authenticated_key": "new789",
"s2_unauthenticated_key": "new987",
@@ -2622,6 +2635,10 @@ async def test_addon_installed_set_options_failure(
assert result["reason"] == "addon_set_config_failed"
assert start_addon.call_count == 0
assert "Failed to set the Z-Wave JS app options" in caplog.text
assert "not a valid value for dictionary value" in caplog.text
assert REDACTED in caplog.text
assert secret not in caplog.text
@pytest.mark.usefixtures("supervisor", "addon_installed")
+42
View File
@@ -34,12 +34,14 @@ from homeassistant.helpers import (
entity_registry as er,
issue_registry as ir,
)
from homeassistant.helpers.redact import REDACTED
from homeassistant.setup import async_setup_component
from .common import (
AIR_TEMPERATURE_SENSOR,
BULB_6_MULTI_COLOR_LIGHT_ENTITY,
EATON_RF9640_ENTITY,
TEST_SENSITIVE_NETWORK_KEY,
)
from tests.common import (
@@ -933,6 +935,46 @@ async def test_start_addon(
assert start_addon.call_args == call("core_zwave_js")
@pytest.mark.usefixtures("addon_installed", "addon_info")
@pytest.mark.parametrize(
"set_addon_options_side_effect",
[
SupervisorError(
"not a valid value for dictionary value @ data['options']. "
f"Got {{'s0_legacy_key': '{TEST_SENSITIVE_NETWORK_KEY}'}}"
)
],
)
async def test_start_addon_redacts_set_options_error(
hass: HomeAssistant,
install_addon: AsyncMock,
set_addon_options: AsyncMock,
start_addon: AsyncMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test startup redacts add-on options backend error details."""
device = "/test"
secret = TEST_SENSITIVE_NETWORK_KEY
entry = MockConfigEntry(
domain=DOMAIN,
title="Z-Wave JS",
data={"use_addon": True, "usb_path": device, "network_key": secret},
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
assert install_addon.call_count == 0
assert set_addon_options.call_count == 1
assert start_addon.call_count == 0
assert "Failed to set the Z-Wave JS app options" in caplog.text
assert "not a valid value for dictionary value" in caplog.text
assert REDACTED in caplog.text
assert secret not in caplog.text
@pytest.mark.usefixtures("addon_info")
async def test_install_addon(
hass: HomeAssistant,