22
33from dataclasses import dataclass
44from enum import Enum
5+ import ipaddress
56import logging
7+ import re
68from typing import Any
79
810from mashumaro import DataClassDictMixin
911
1012logger = logging .getLogger (__name__ )
1113
14+ # Regex for a standard MAC address format (e.g., 01:23:45:67:89:AB)
15+ # This handles both colon and hyphen separators.
16+ MAC_ADDRESS_REGEX = re .compile (r"^([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})$" )
17+
18+ # Regex for a MAC address mask (e.g., the redacted format 00:00:00:00:89:AB)
19+ MAC_ADDRESS_MASK_REGEX = re .compile (r"^(00:){4}[0-9a-fA-F]{2}[:-][0-9a-fA-F]{2}$" )
20+
21+
22+ def is_mac_address (value : str ) -> bool :
23+ """Check if a string is a valid MAC address."""
24+ return bool (MAC_ADDRESS_REGEX .match (value ))
25+
26+
27+ def is_mac_address_mask (value : str ) -> bool :
28+ """Check if a string is a valid MAC address mask (e.g., the redacted format)."""
29+ return bool (MAC_ADDRESS_MASK_REGEX .match (value ))
30+
31+
32+ def is_ip_address (value : str ) -> bool :
33+ """Check if a string is a valid IPv4 or IPv6 address."""
34+ try :
35+ ipaddress .ip_address (value )
36+ return True
37+ except ValueError :
38+ return False
39+
1240
1341def _check_and_log_unknown_enum_value (
1442 data_dict : dict [str , Any ],
@@ -31,6 +59,98 @@ def _check_and_log_unknown_enum_value(
3159 del data_dict [key ]
3260
3361
62+ def redact_data_smart (data : dict ) -> dict :
63+ """Recursively redacts sensitive keys in a dictionary."""
64+ sensitive_keys = {
65+ "hostname" ,
66+ "essid" ,
67+ "mac" ,
68+ "apmac" ,
69+ "hwaddr" ,
70+ "lastip" ,
71+ "ipaddr" ,
72+ "ip6addr" ,
73+ "device_id" ,
74+ "sys_id" ,
75+ "station_id" ,
76+ "platform" ,
77+ }
78+
79+ def _redact (d : dict ):
80+ if not isinstance (d , dict ):
81+ return d
82+
83+ redacted_d = {}
84+ for k , v in d .items ():
85+ if k in sensitive_keys :
86+ if isinstance (v , str ) and (is_mac_address (v ) or is_mac_address_mask (v )):
87+ # Redact only the first 6 hex characters of a MAC address
88+ redacted_d [k ] = "00:00:00:00:" + v .replace ("-" , ":" ).upper ()[- 5 :]
89+ elif isinstance (v , str ) and is_ip_address (v ):
90+ # Redact to a dummy local IP address
91+ redacted_d [k ] = "127.0.0.3"
92+ elif isinstance (v , list ) and all (
93+ isinstance (i , str ) and is_ip_address (i ) for i in v
94+ ):
95+ # Redact list of IPs to a dummy list
96+ redacted_d [k ] = ["127.0.0.3" ]
97+ else :
98+ redacted_d [k ] = "REDACTED"
99+ elif isinstance (v , dict ):
100+ redacted_d [k ] = _redact (v )
101+ elif isinstance (v , list ):
102+ redacted_d [k ] = [
103+ _redact (item ) if isinstance (item , dict ) else item for item in v
104+ ]
105+ else :
106+ redacted_d [k ] = v
107+ return redacted_d
108+
109+ return _redact (data )
110+
111+
112+ def _redact_ip_addresses (addresses : str | list [str ]) -> str | list [str ]:
113+ """Redacts the first three octets of an IPv4 address."""
114+ if isinstance (addresses , str ):
115+ addresses = [addresses ]
116+
117+ redacted_list = []
118+ for ip in addresses :
119+ try :
120+ parts = ip .split ("." )
121+ if len (parts ) == 4 :
122+ # Keep the last octet, but replace the rest with a placeholder.
123+ redacted_list .append (f"127.0.0.{ parts [3 ]} " )
124+ else :
125+ # Handle non-standard IPs or IPv6 if it shows up here
126+ redacted_list .append ("REDACTED" )
127+ except (IndexError , ValueError ):
128+ # In case the IP string is malformed
129+ redacted_list .append ("REDACTED" )
130+
131+ return redacted_list if isinstance (addresses , list ) else redacted_list [0 ]
132+
133+
134+ def _redact_mac_addresses (macs : str | list [str ]) -> str | list [str ]:
135+ """Redacts the first four octets of a MAC address."""
136+ if isinstance (macs , str ):
137+ macs = [macs ]
138+
139+ redacted_list = []
140+ for mac in macs :
141+ try :
142+ parts = mac .split (":" )
143+ if len (parts ) == 6 :
144+ # Keep the last two octets, replace the rest with a placeholder
145+ redacted_list .append (f"00:11:22:33:{ parts [4 ]} :{ parts [5 ]} " )
146+ else :
147+ redacted_list .append ("REDACTED" )
148+ except (IndexError , ValueError ):
149+ redacted_list .append ("REDACTED" )
150+
151+ return redacted_list if isinstance (macs , list ) else redacted_list [0 ]
152+
153+
34154class IeeeMode (Enum ):
35155 """Enum definition."""
36156
@@ -259,16 +379,16 @@ class Remote:
259379 rx_bytes : int
260380 antenna_gain : int
261381 cable_loss : int
262- height : int
263382 ethlist : list [EthList ]
264383 ipaddr : list [str ]
265- ip6addr : list [str ]
266384 gps : GPSData
267385 oob : bool
268386 unms : UnmsStatus
269387 airview : int
270388 service : ServiceTime
271389 mode : WirelessMode | None = None # Investigate why remotes can have no mode set
390+ ip6addr : list [str ] | None = None # For v4 only devices
391+ height : int | None = None
272392
273393 @classmethod
274394 def __pre_deserialize__ (cls , d : dict [str , Any ]) -> dict [str , Any ]:
0 commit comments