`homeassistant.util.diagnostic` does not exist — verified against a real install: `import homeassistant.util.diagnostic` raises ModuleNotFoundError, and the helper has always lived in `homeassistant.helpers.redact`. The import sat inside a `try/except ImportError` that set the helper to None, so the documented G14 pass — the one carrying `_TO_REDACT`, hosts included — silently did nothing; only the marker scan (pairing-/key-shaped names) masked anything, which is why the existing test's pairing-code assertion stayed green. The helper is now imported at MODULE scope from homeassistant.helpers.redact and applied unconditionally after the marker-name pass. A missing module is a loud import error at integration load instead of a silent downgrade. * the diagnostics test now asserts `adb_host` and `protocol_host` really are masked in the emitted payload (options and a nested data value); * TestFaithfulHaStubs asserts the module USES the helper from that path (`nsh.diagnostics.async_redact_data is homeassistant.helpers.redact.async_redact_data`), that no production module imports the non-existent module (AST), and that the try/except ImportError fallback is gone; * `entity_stublib` gains a faithful `homeassistant.helpers.redact` stub (same masking, same nested mapping/list recursion, same None/empty-string skip) so the harness meets the import the way a real install does. Verified: 287 passed, 2 skipped (D2 stage); ruff clean. Full gate re-run on the completed branch: 289 passed / 2 skipped and 290 passed / 1 skipped.
101 lines
3.6 KiB
Python
101 lines
3.6 KiB
Python
"""Entry diagnostics for Nvidia Shield (P4-2b, t21 F1 platform-module move).
|
|
|
|
HA discovers ``async_get_config_entry_diagnostics`` through the integration's
|
|
``diagnostics`` platform module — a placement inside media_player.py never
|
|
serves in a real install (t20 finding F1).
|
|
|
|
The payload: redacted entry config (data + options), the manager's StateStore
|
|
snapshot via the public ``store_snapshot`` accessor, and both transports'
|
|
health. Cert paths are kept readable; key material / pairing secrets never
|
|
exist in stored state, and any key carrying a secret-shaped name is redacted
|
|
defensively.
|
|
"""
|
|
|
|
import time
|
|
from dataclasses import asdict, is_dataclass
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.redact import async_redact_data
|
|
|
|
from .connection import ShieldConnectionManager
|
|
|
|
_REDACT_KEY_MARKERS = ("pairing", "pin", "secret", "password", "token", "key_material")
|
|
_REDACTED = "**REDACTED**"
|
|
|
|
# G14: the documented explicit redaction field list — the audit calls out
|
|
# config fields like the hosts; pairing/key-shaped fields are masked by
|
|
# async_redact_data and the marker scan adds a second layer.
|
|
_TO_REDACT = {
|
|
"pairing_code",
|
|
"adb_pairing_code",
|
|
"protocol_pairing_code",
|
|
"serial_number",
|
|
"uuid",
|
|
"adb_host",
|
|
"protocol_host",
|
|
"host",
|
|
}
|
|
|
|
|
|
def _redact(obj):
|
|
"""Marker-based defensive pass (kept behind the async_redact_data helper)."""
|
|
"""Redact sensitive keys from a JSON-ish diagnostics payload."""
|
|
if isinstance(obj, dict):
|
|
return {
|
|
key: (
|
|
_REDACTED
|
|
if any(m in str(key).lower() for m in _REDACT_KEY_MARKERS)
|
|
else _redact(value)
|
|
)
|
|
for key, value in obj.items()
|
|
}
|
|
if isinstance(obj, list):
|
|
return [_redact(item) for item in obj]
|
|
return obj
|
|
|
|
|
|
def _store_snapshot(manager) -> dict[str, dict]:
|
|
"""TTL-stamped store snapshot; dataclass values expand to dicts."""
|
|
snapshot = {}
|
|
for name, (stamp, value) in manager.store.values_stamped().items():
|
|
if is_dataclass(value) and not isinstance(value, type):
|
|
value = asdict(value)
|
|
snapshot[name] = {"age": round(time.monotonic() - stamp, 3), "value": value}
|
|
return snapshot
|
|
|
|
|
|
async def async_get_config_entry_diagnostics(
|
|
hass: HomeAssistant, entry: ConfigEntry
|
|
) -> dict:
|
|
"""Entry diagnostics: redacted config + store snapshot + transport health."""
|
|
manager = getattr(entry, "runtime_data", None)
|
|
payload: dict = {
|
|
"entry": {
|
|
"data": dict(getattr(entry, "data", {}) or {}),
|
|
"options": dict(getattr(entry, "options", {}) or {}),
|
|
},
|
|
"store": {},
|
|
"transport": {},
|
|
}
|
|
if isinstance(manager, ShieldConnectionManager):
|
|
payload["store"] = manager.store_snapshot()
|
|
payload["transport"] = {
|
|
"adb": {"connected": manager.connected, "awake": manager.awake},
|
|
"protocol": {
|
|
"active": manager.protocol_active,
|
|
"available": manager.is_available,
|
|
"is_on": manager.is_on,
|
|
},
|
|
}
|
|
# G14 (t30) + t37 D2: the documented helper is the primary redaction path
|
|
# with an explicit field list (the hosts included), and the marker scan
|
|
# above runs defensively after it. It is imported at MODULE scope from
|
|
# homeassistant.helpers.redact — the real module; the old
|
|
# `homeassistant.util.diagnostic` never existed, and the swallowed
|
|
# ImportError turned the whole pass into a silent no-op.
|
|
return _redact(async_redact_data(payload, _TO_REDACT))
|
|
|
|
|
|
__all__ = ["async_get_config_entry_diagnostics"]
|