diff --git a/esphome/__main__.py b/esphome/__main__.py index 99370064ef..9bd19e517a 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1424,10 +1424,11 @@ def command_config(args: ArgsProtocol, config: ConfigType) -> int | None: # 2026.12.0 once canonical sensitive fields are tagged. The lookahead skips # values that already render themselves: ``\033[8m`` (SensitiveStr wrap), # ``!secret`` (preserves the user-friendly tag), ``!lambda`` (multi-line -# block; first line is structural). Un-anchored on purpose to match the -# prior regex; leading ``\w*`` captures the full field name for warnings. +# block; first line is structural). The fragment must either start the +# field name or follow ``_`` so the warning names a real field; this avoids +# false positives like ``monkey:`` matching the ``key`` fragment. _LEGACY_REDACTION_RE = re.compile( - r"(?P\w*(?:password|key|psk|ssid))\: " + r"(?P\b(?:\w+_)?(?:password|key|psk|ssid))\: " r"(?!\\033\[8m|!secret\b|!lambda\b)(?P.+)" ) _LEGACY_REDACTION_REMOVAL = "2026.12.0" diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 1c58e9b294..26b550669f 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -424,6 +424,18 @@ def test_redact_with_legacy_fallback__does_not_match_fragment_in_middle( assert not any("legacy substring" in rec.message for rec in caplog.records) +def test_redact_with_legacy_fallback__does_not_match_fragment_as_suffix( + caplog: pytest.LogCaptureFixture, +) -> None: + """Fragment must start the name or follow ``_``; ``monkey:`` shouldn't + fire a 'legacy heuristic' warning because there's no sensitive field + here — the user has nothing to migrate.""" + with caplog.at_level(logging.WARNING, logger="esphome.__main__"): + out = _redact_with_legacy_fallback("monkey: 1234\n") + assert "\\033[8m" not in out + assert not any("legacy substring" in rec.message for rec in caplog.records) + + def test_command_config__invokes_legacy_fallback_when_redacting( tmp_path: Path, capfd: CaptureFixture[str] ) -> None: