Skip to content

Commit dda0a3c

Browse files
committed
fix: Improve debug logs redaction
Put all redaction logic in one place. Redact identifiers from topic names and duids.
1 parent a8f5d06 commit dda0a3c

File tree

11 files changed

+136
-64
lines changed

11 files changed

+136
-64
lines changed

roborock/devices/device.py

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
import datetime
99
import logging
1010
from abc import ABC
11-
from collections.abc import Callable, Mapping
12-
from typing import Any, TypeVar, cast
11+
from collections.abc import Callable
12+
from typing import Any
1313

1414
from roborock.callbacks import CallbackList
1515
from roborock.data import HomeDataDevice, HomeDataProduct
16+
from roborock.diagnostics import redact_device_data
1617
from roborock.exceptions import RoborockException
1718
from roborock.roborock_message import RoborockMessage
1819
from roborock.util import RoborockLoggerAdapter
@@ -65,7 +66,7 @@ def __init__(
6566
"""
6667
TraitsMixin.__init__(self, trait)
6768
self._duid = device_info.duid
68-
self._logger = RoborockLoggerAdapter(self._duid, _LOGGER)
69+
self._logger = RoborockLoggerAdapter(duid=self._duid, logger=_LOGGER)
6970
self._name = device_info.name
7071
self._device_info = device_info
7172
self._product = product
@@ -223,52 +224,9 @@ def diagnostic_data(self) -> dict[str, Any]:
223224
"""Return diagnostics information about the device."""
224225
extra: dict[str, Any] = {}
225226
if self.v1_properties:
226-
extra["traits"] = _redact_data(self.v1_properties.as_dict())
227+
extra["traits"] = redact_device_data(self.v1_properties.as_dict())
227228
return {
228-
"device": _redact_data(self.device_info.as_dict()),
229-
"product": _redact_data(self.product.as_dict()),
229+
"device": redact_device_data(self.device_info.as_dict()),
230+
"product": redact_device_data(self.product.as_dict()),
230231
**extra,
231232
}
232-
233-
234-
T = TypeVar("T")
235-
236-
REDACT_KEYS = {
237-
# Potential identifiers
238-
"duid",
239-
"localKey",
240-
"mac",
241-
"bssid",
242-
"sn",
243-
"ip",
244-
"u",
245-
"s",
246-
"h",
247-
"k",
248-
# Large binary blobs are entirely omitted
249-
"imageContent",
250-
"mapData",
251-
"rawApiResponse",
252-
}
253-
REDACTED = "**REDACTED**"
254-
255-
256-
def _redact_data(data: T) -> T | dict[str, Any]:
257-
"""Redact sensitive data in a dict."""
258-
if not isinstance(data, (Mapping, list)):
259-
return data
260-
261-
if isinstance(data, list):
262-
return cast(T, [_redact_data(item) for item in data])
263-
264-
redacted = {**data}
265-
266-
for key, value in redacted.items():
267-
if key in REDACT_KEYS:
268-
redacted[key] = REDACTED
269-
elif isinstance(value, dict):
270-
redacted[key] = _redact_data(value)
271-
elif isinstance(value, list):
272-
redacted[key] = [_redact_data(item) for item in value]
273-
274-
return redacted

roborock/devices/local_channel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class LocalChannel(Channel):
5454

5555
def __init__(self, host: str, local_key: str, device_uid: str) -> None:
5656
self._host = host
57-
self._logger = RoborockLoggerAdapter(device_uid, _LOGGER)
57+
self._logger = RoborockLoggerAdapter(duid=device_uid, logger=_LOGGER)
5858
self._transport: asyncio.Transport | None = None
5959
self._protocol: _LocalProtocol | None = None
6060
self._subscribers: CallbackList[RoborockMessage] = CallbackList(self._logger)

roborock/devices/mqtt_channel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class MqttChannel(Channel):
2727
def __init__(self, mqtt_session: MqttSession, duid: str, local_key: str, rriot: RRiot, mqtt_params: MqttParams):
2828
self._mqtt_session = mqtt_session
2929
self._duid = duid
30-
self._logger = RoborockLoggerAdapter(duid, _LOGGER)
30+
self._logger = RoborockLoggerAdapter(duid=duid, logger=_LOGGER)
3131
self._local_key = local_key
3232
self._rriot = rriot
3333
self._mqtt_params = mqtt_params

roborock/devices/v1_channel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ def __init__(
178178
) -> None:
179179
"""Initialize the V1Channel."""
180180
self._device_uid = device_uid
181-
self._logger = RoborockLoggerAdapter(device_uid, _LOGGER)
181+
self._logger = RoborockLoggerAdapter(duid=device_uid, logger=_LOGGER)
182182
self._security_data = security_data
183183
self._mqtt_channel = mqtt_channel
184184
self._local_session = local_session

roborock/diagnostics.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from collections import Counter
1616
from collections.abc import Generator, Mapping
1717
from contextlib import contextmanager
18-
from typing import Any
18+
from typing import Any, TypeVar, cast
1919

2020

2121
class Diagnostics:
@@ -82,3 +82,70 @@ def reset(self) -> None:
8282
self._counter = Counter()
8383
for d in self._subkeys.values():
8484
d.reset()
85+
86+
87+
T = TypeVar("T")
88+
89+
REDACT_KEYS = {
90+
# Potential identifiers
91+
"localKey",
92+
"mac",
93+
"bssid",
94+
"sn",
95+
"ip",
96+
"u",
97+
"s",
98+
"h",
99+
"k",
100+
# Large binary blobs are entirely omitted
101+
"imageContent",
102+
"mapData",
103+
"rawApiResponse",
104+
}
105+
DEVICE_UID = "duid"
106+
REDACTED = "**REDACTED**"
107+
108+
109+
def redact_device_data(data: T) -> T | dict[str, Any]:
110+
"""Redact sensitive data in a dict."""
111+
if not isinstance(data, (Mapping, list)):
112+
return data
113+
114+
if isinstance(data, list):
115+
return cast(T, [redact_device_data(item) for item in data])
116+
117+
redacted = {**data}
118+
119+
for key, value in redacted.items():
120+
if key in REDACT_KEYS:
121+
redacted[key] = REDACTED
122+
elif key == DEVICE_UID and isinstance(value, str):
123+
redacted[key] = redact_device_uid(value)
124+
elif isinstance(value, dict):
125+
redacted[key] = redact_device_data(value)
126+
elif isinstance(value, list):
127+
redacted[key] = [redact_device_data(item) for item in value]
128+
129+
return redacted
130+
131+
132+
def redact_topic_name(topic: str) -> str:
133+
"""Redact potentially identifying information from a topic name."""
134+
parts = topic.split("/")
135+
if len(parts) < 5:
136+
return topic
137+
138+
redacted_parts = parts[:4]
139+
for part in parts[4:]:
140+
if len(part) <= 5:
141+
redacted_parts.append("*****")
142+
else:
143+
redacted_parts.append("*****" + part[-5:])
144+
return "/".join(redacted_parts)
145+
146+
147+
def redact_device_uid(duid: str) -> str:
148+
"""Redact a device UID to hide identifying information."""
149+
if len(duid) <= 5:
150+
return "*****"
151+
return "******" + duid[-5:]

roborock/util.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import Any, TypeVar
1111

1212
from roborock import RoborockException
13+
from roborock.diagnostics import redact_device_uid
1314

1415
T = TypeVar("T")
1516
DEFAULT_TIME_ZONE: datetime.tzinfo | None = datetime.datetime.now().astimezone().tzinfo
@@ -81,9 +82,19 @@ async def reset(self):
8182

8283

8384
class RoborockLoggerAdapter(logging.LoggerAdapter):
84-
def __init__(self, prefix: str, logger: logging.Logger) -> None:
85-
super().__init__(logger, {})
86-
self.prefix = prefix
85+
def __init__(
86+
self,
87+
duid: str | None = None,
88+
name: str | None = None,
89+
logger: logging.Logger | None = None,
90+
) -> None:
91+
super().__init__(logger or logging.getLogger(__name__), {})
92+
if name is not None:
93+
self.prefix = name
94+
elif duid is not None:
95+
self.prefix = redact_device_uid(duid)
96+
else:
97+
raise ValueError("Either duid or name must be provided")
8798

8899
def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, MutableMapping[str, Any]]:
89100
return f"[{self.prefix}] {msg}", kwargs

roborock/version_1_apis/roborock_local_client_v1.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def __init__(
5858
self._ack_nonce: int | None = None
5959
self._set_encoder_decoder()
6060
self.queue_timeout = queue_timeout
61-
self._logger = RoborockLoggerAdapter(device_data.device.name, _LOGGER)
61+
self._logger = RoborockLoggerAdapter(name=device_data.device.name, logger=_LOGGER)
6262

6363
@property
6464
def local_protocol_version(self) -> LocalProtocolVersion:

roborock/version_1_apis/roborock_mqtt_client_v1.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __init__(self, user_data: UserData, device_info: DeviceData, queue_timeout:
3232
security_data = create_security_data(rriot)
3333
RoborockClientV1.__init__(self, device_info, security_data=security_data)
3434
self.queue_timeout = queue_timeout
35-
self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER)
35+
self._logger = RoborockLoggerAdapter(name=device_info.device.name, logger=_LOGGER)
3636
self._security_data = security_data
3737

3838
async def _send_command(

roborock/version_a01_apis/roborock_mqtt_client_a01.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def __init__(
3737
RoborockMqttClient.__init__(self, user_data, device_info)
3838
RoborockClientA01.__init__(self, device_info, category)
3939
self.queue_timeout = queue_timeout
40-
self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER)
40+
self._logger = RoborockLoggerAdapter(name=device_info.device.name, logger=_LOGGER)
4141

4242
async def _send_message(self, roborock_message: RoborockMessage):
4343
await self._validate_connection()

tests/devices/__snapshots__/test_v1_device.ambr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
'128': 0,
1919
'133': 1,
2020
}),
21-
'duid': '**REDACTED**',
21+
'duid': '******bc123',
2222
'extra': '{"RRPhotoPrivacyVersion": "1"}',
2323
'featureSet': '2234201184108543',
2424
'fv': '02.56.02',
@@ -444,7 +444,7 @@
444444
'128': 0,
445445
'133': 1,
446446
}),
447-
'duid': '**REDACTED**',
447+
'duid': '******bc123',
448448
'extra': '{"RRPhotoPrivacyVersion": "1"}',
449449
'featureSet': '2234201184108543',
450450
'fv': '02.56.02',
@@ -850,7 +850,7 @@
850850
'128': 0,
851851
'133': 1,
852852
}),
853-
'duid': '**REDACTED**',
853+
'duid': '******bc123',
854854
'extra': '{"RRPhotoPrivacyVersion": "1"}',
855855
'featureSet': '2234201184108543',
856856
'fv': '02.56.02',
@@ -1227,7 +1227,7 @@
12271227
'128': 0,
12281228
'133': 1,
12291229
}),
1230-
'duid': '**REDACTED**',
1230+
'duid': '******bc123',
12311231
'extra': '{"RRPhotoPrivacyVersion": "1"}',
12321232
'featureSet': '2234201184108543',
12331233
'fv': '02.56.02',

0 commit comments

Comments
 (0)