unctools 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unctools/__init__.py +100 -0
- unctools/converter.py +378 -0
- unctools/detector.py +531 -0
- unctools/operations.py +562 -0
- unctools/utils/__init__.py +40 -0
- unctools/utils/compat.py +331 -0
- unctools/utils/logger.py +228 -0
- unctools/utils/validation.py +321 -0
- unctools/windows/__init__.py +45 -0
- unctools/windows/network.py +490 -0
- unctools/windows/registry.py +410 -0
- unctools/windows/security.py +586 -0
- unctools-0.1.0.dist-info/METADATA +189 -0
- unctools-0.1.0.dist-info/RECORD +17 -0
- unctools-0.1.0.dist-info/WHEEL +5 -0
- unctools-0.1.0.dist-info/licenses/LICENSE +21 -0
- unctools-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Windows registry utilities for managing security zones and network settings.
|
|
3
|
+
|
|
4
|
+
This module provides functions for working with the Windows registry,
|
|
5
|
+
particularly for managing security zones for UNC paths, which can help
|
|
6
|
+
resolve access issues with network shares.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Optional, Dict, List, Tuple, Any, Union
|
|
13
|
+
|
|
14
|
+
# Set up module-level logger
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Check if we're running on Windows
|
|
18
|
+
IS_WINDOWS = os.name == 'nt'
|
|
19
|
+
|
|
20
|
+
# Try to import Windows-specific modules
|
|
21
|
+
if IS_WINDOWS:
|
|
22
|
+
try:
|
|
23
|
+
import winreg
|
|
24
|
+
HAVE_WINREG = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
HAVE_WINREG = False
|
|
27
|
+
logger.warning("winreg module not available. Registry operations will not work.")
|
|
28
|
+
else:
|
|
29
|
+
HAVE_WINREG = False
|
|
30
|
+
|
|
31
|
+
# Constants for Windows security zones
|
|
32
|
+
ZONE_LOCAL_INTRANET = 1
|
|
33
|
+
ZONE_TRUSTED_SITES = 2
|
|
34
|
+
ZONE_INTERNET = 3
|
|
35
|
+
ZONE_RESTRICTED_SITES = 4
|
|
36
|
+
|
|
37
|
+
# Registry paths
|
|
38
|
+
ZONEMAP_KEY_PATH = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap"
|
|
39
|
+
DOMAINS_KEY_PATH = ZONEMAP_KEY_PATH + r"\Domains"
|
|
40
|
+
RANGES_KEY_PATH = ZONEMAP_KEY_PATH + r"\Ranges"
|
|
41
|
+
|
|
42
|
+
def _ensure_admin_access() -> bool:
|
|
43
|
+
"""
|
|
44
|
+
Check if the script has administrative access to the Windows registry.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
True if the script has administrative access, False otherwise.
|
|
48
|
+
"""
|
|
49
|
+
if not IS_WINDOWS:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
import ctypes
|
|
54
|
+
return bool(ctypes.windll.shell32.IsUserAnAdmin())
|
|
55
|
+
except:
|
|
56
|
+
# If we can't check, assume we don't have admin access
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def add_to_intranet_zone(server_name: str, for_all_users: bool = False) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Add a server to the Local Intranet security zone.
|
|
62
|
+
|
|
63
|
+
This can help resolve access issues with UNC paths by lowering the security
|
|
64
|
+
restrictions for the specified server.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
server_name: The name of the server to add to the zone.
|
|
68
|
+
for_all_users: If True, attempt to add the server to the zone for all users.
|
|
69
|
+
This requires administrative privileges.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if the server was successfully added to the zone, False otherwise.
|
|
73
|
+
"""
|
|
74
|
+
if not IS_WINDOWS or not HAVE_WINREG:
|
|
75
|
+
logger.warning("Registry operations are only available on Windows.")
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
# Validate server name
|
|
79
|
+
if not re.match(r'^[a-zA-Z0-9][a-zA-Z0-9\-\.]*$', server_name):
|
|
80
|
+
logger.error(f"Invalid server name: {server_name}")
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
# Check if for_all_users requires admin access
|
|
84
|
+
if for_all_users and not _ensure_admin_access():
|
|
85
|
+
logger.warning("Administrative access required to add zone for all users.")
|
|
86
|
+
for_all_users = False
|
|
87
|
+
|
|
88
|
+
# Determine which registry hive to use
|
|
89
|
+
if for_all_users:
|
|
90
|
+
root_key = winreg.HKEY_LOCAL_MACHINE
|
|
91
|
+
logger.info(f"Adding {server_name} to Local Intranet zone for all users")
|
|
92
|
+
else:
|
|
93
|
+
root_key = winreg.HKEY_CURRENT_USER
|
|
94
|
+
logger.info(f"Adding {server_name} to Local Intranet zone for current user")
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Create or open the domain key
|
|
98
|
+
domain_path = DOMAINS_KEY_PATH + "\\" + server_name
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Try to create the key
|
|
102
|
+
domain_key = winreg.CreateKeyEx(root_key, domain_path, 0, winreg.KEY_WRITE)
|
|
103
|
+
except PermissionError:
|
|
104
|
+
logger.error(f"Permission denied accessing registry key: {domain_path}")
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
# Set the "*" value to Local Intranet zone (1)
|
|
109
|
+
winreg.SetValueEx(domain_key, "*", 0, winreg.REG_DWORD, ZONE_LOCAL_INTRANET)
|
|
110
|
+
logger.info(f"Added {server_name} to Local Intranet zone successfully.")
|
|
111
|
+
success = True
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(f"Failed to add {server_name} to zone: {e}")
|
|
114
|
+
success = False
|
|
115
|
+
|
|
116
|
+
# Close the key
|
|
117
|
+
winreg.CloseKey(domain_key)
|
|
118
|
+
|
|
119
|
+
return success
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Failed to add {server_name} to Local Intranet zone: {e}")
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def remove_from_zone(server_name: str, for_all_users: bool = False) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Remove a server from any security zone.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
server_name: The name of the server to remove from zones.
|
|
130
|
+
for_all_users: If True, attempt to remove the server from zones for all users.
|
|
131
|
+
This requires administrative privileges.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
True if the server was successfully removed, False otherwise.
|
|
135
|
+
"""
|
|
136
|
+
if not IS_WINDOWS or not HAVE_WINREG:
|
|
137
|
+
logger.warning("Registry operations are only available on Windows.")
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
# Check if for_all_users requires admin access
|
|
141
|
+
if for_all_users and not _ensure_admin_access():
|
|
142
|
+
logger.warning("Administrative access required to remove zone for all users.")
|
|
143
|
+
for_all_users = False
|
|
144
|
+
|
|
145
|
+
# Determine which registry hive to use
|
|
146
|
+
if for_all_users:
|
|
147
|
+
root_key = winreg.HKEY_LOCAL_MACHINE
|
|
148
|
+
logger.info(f"Removing {server_name} from security zones for all users")
|
|
149
|
+
else:
|
|
150
|
+
root_key = winreg.HKEY_CURRENT_USER
|
|
151
|
+
logger.info(f"Removing {server_name} from security zones for current user")
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
# Check if the key exists
|
|
155
|
+
domain_path = DOMAINS_KEY_PATH + "\\" + server_name
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
# Try to open the key
|
|
159
|
+
try:
|
|
160
|
+
domain_key = winreg.OpenKey(root_key, domain_path, 0, winreg.KEY_READ)
|
|
161
|
+
# Close right away, we just want to see if it exists
|
|
162
|
+
winreg.CloseKey(domain_key)
|
|
163
|
+
except FileNotFoundError:
|
|
164
|
+
logger.info(f"Server {server_name} not found in any zone.")
|
|
165
|
+
return True # Already not in a zone
|
|
166
|
+
|
|
167
|
+
# Delete the key
|
|
168
|
+
try:
|
|
169
|
+
winreg.DeleteKey(root_key, domain_path)
|
|
170
|
+
logger.info(f"Removed {server_name} from security zones successfully.")
|
|
171
|
+
return True
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(f"Failed to remove {server_name} from zones: {e}")
|
|
174
|
+
return False
|
|
175
|
+
except PermissionError:
|
|
176
|
+
logger.error(f"Permission denied accessing registry key: {domain_path}")
|
|
177
|
+
return False
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f"Failed to remove {server_name} from zones: {e}")
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
def check_zone(server_name: str) -> Optional[int]:
|
|
183
|
+
"""
|
|
184
|
+
Check which security zone a server is in.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
server_name: The name of the server to check.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
The zone number if found, or None if the server is not in any zone.
|
|
191
|
+
Zone numbers:
|
|
192
|
+
1: Local Intranet
|
|
193
|
+
2: Trusted Sites
|
|
194
|
+
3: Internet
|
|
195
|
+
4: Restricted Sites
|
|
196
|
+
"""
|
|
197
|
+
if not IS_WINDOWS or not HAVE_WINREG:
|
|
198
|
+
logger.warning("Registry operations are only available on Windows.")
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
# Check current user settings first
|
|
203
|
+
domain_path = DOMAINS_KEY_PATH + "\\" + server_name
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
# Try to open the key
|
|
207
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, domain_path, 0, winreg.KEY_READ) as domain_key:
|
|
208
|
+
try:
|
|
209
|
+
value, _ = winreg.QueryValueEx(domain_key, "*")
|
|
210
|
+
return value
|
|
211
|
+
except FileNotFoundError:
|
|
212
|
+
# Check if there are any other entries
|
|
213
|
+
i = 0
|
|
214
|
+
while True:
|
|
215
|
+
try:
|
|
216
|
+
name, value, _ = winreg.EnumValue(domain_key, i)
|
|
217
|
+
if name and value is not None:
|
|
218
|
+
return value
|
|
219
|
+
i += 1
|
|
220
|
+
except OSError:
|
|
221
|
+
break
|
|
222
|
+
except FileNotFoundError:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
# If not found in current user, check all users (HKLM)
|
|
226
|
+
try:
|
|
227
|
+
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, domain_path, 0, winreg.KEY_READ) as domain_key:
|
|
228
|
+
try:
|
|
229
|
+
value, _ = winreg.QueryValueEx(domain_key, "*")
|
|
230
|
+
return value
|
|
231
|
+
except FileNotFoundError:
|
|
232
|
+
# Check if there are any other entries
|
|
233
|
+
i = 0
|
|
234
|
+
while True:
|
|
235
|
+
try:
|
|
236
|
+
name, value, _ = winreg.EnumValue(domain_key, i)
|
|
237
|
+
if name and value is not None:
|
|
238
|
+
return value
|
|
239
|
+
i += 1
|
|
240
|
+
except OSError:
|
|
241
|
+
break
|
|
242
|
+
except (FileNotFoundError, PermissionError):
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
# Not found in any zone
|
|
246
|
+
return None
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.error(f"Failed to check zone for {server_name}: {e}")
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
def fix_security_zone(server_name: str) -> bool:
|
|
252
|
+
"""
|
|
253
|
+
Fix security zone issues for a server.
|
|
254
|
+
|
|
255
|
+
This function adds the server to the Local Intranet zone, which can help
|
|
256
|
+
resolve access issues with UNC paths.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
server_name: The name of the server to fix.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
True if the fix was applied successfully, False otherwise.
|
|
263
|
+
"""
|
|
264
|
+
# Check if the server is already in a zone
|
|
265
|
+
current_zone = check_zone(server_name)
|
|
266
|
+
|
|
267
|
+
if current_zone == ZONE_LOCAL_INTRANET:
|
|
268
|
+
logger.info(f"Server {server_name} is already in the Local Intranet zone.")
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
# Try to add to current user's zones first
|
|
272
|
+
if add_to_intranet_zone(server_name, for_all_users=False):
|
|
273
|
+
return True
|
|
274
|
+
|
|
275
|
+
# If that fails and we have admin access, try for all users
|
|
276
|
+
if _ensure_admin_access():
|
|
277
|
+
return add_to_intranet_zone(server_name, for_all_users=True)
|
|
278
|
+
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
def get_all_zone_servers() -> Dict[str, int]:
|
|
282
|
+
"""
|
|
283
|
+
Get all servers that are in security zones.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
A dictionary mapping server names to zone numbers.
|
|
287
|
+
"""
|
|
288
|
+
if not IS_WINDOWS or not HAVE_WINREG:
|
|
289
|
+
logger.warning("Registry operations are only available on Windows.")
|
|
290
|
+
return {}
|
|
291
|
+
|
|
292
|
+
servers = {}
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
# Check HKCU first
|
|
296
|
+
try:
|
|
297
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, DOMAINS_KEY_PATH, 0, winreg.KEY_READ) as domains_key:
|
|
298
|
+
i = 0
|
|
299
|
+
while True:
|
|
300
|
+
try:
|
|
301
|
+
server = winreg.EnumKey(domains_key, i)
|
|
302
|
+
servers[server] = check_zone(server)
|
|
303
|
+
i += 1
|
|
304
|
+
except OSError:
|
|
305
|
+
break
|
|
306
|
+
except (FileNotFoundError, PermissionError):
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
# Then check HKLM if we have access
|
|
310
|
+
try:
|
|
311
|
+
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, DOMAINS_KEY_PATH, 0, winreg.KEY_READ) as domains_key:
|
|
312
|
+
i = 0
|
|
313
|
+
while True:
|
|
314
|
+
try:
|
|
315
|
+
server = winreg.EnumKey(domains_key, i)
|
|
316
|
+
if server not in servers: # Only add if not already found in HKCU
|
|
317
|
+
servers[server] = check_zone(server)
|
|
318
|
+
i += 1
|
|
319
|
+
except OSError:
|
|
320
|
+
break
|
|
321
|
+
except (FileNotFoundError, PermissionError):
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
return servers
|
|
325
|
+
except Exception as e:
|
|
326
|
+
logger.error(f"Failed to get all zone servers: {e}")
|
|
327
|
+
return {}
|
|
328
|
+
|
|
329
|
+
def backup_zone_settings(backup_file: str) -> bool:
|
|
330
|
+
"""
|
|
331
|
+
Backup all security zone settings to a file.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
backup_file: The path to save the backup to.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
True if the backup was successful, False otherwise.
|
|
338
|
+
"""
|
|
339
|
+
import json
|
|
340
|
+
|
|
341
|
+
servers = get_all_zone_servers()
|
|
342
|
+
|
|
343
|
+
if not servers:
|
|
344
|
+
logger.warning("No zone settings found to backup.")
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
with open(backup_file, 'w') as f:
|
|
349
|
+
json.dump(servers, f, indent=2)
|
|
350
|
+
logger.info(f"Backed up zone settings for {len(servers)} servers to {backup_file}")
|
|
351
|
+
return True
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.error(f"Failed to backup zone settings: {e}")
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
def restore_zone_settings(backup_file: str, for_all_users: bool = False) -> Tuple[int, int]:
|
|
357
|
+
"""
|
|
358
|
+
Restore security zone settings from a backup file.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
backup_file: The path to the backup file.
|
|
362
|
+
for_all_users: If True, attempt to restore settings for all users.
|
|
363
|
+
This requires administrative privileges.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
A tuple of (success_count, failure_count) indicating how many servers
|
|
367
|
+
were successfully restored and how many failed.
|
|
368
|
+
"""
|
|
369
|
+
import json
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
with open(backup_file, 'r') as f:
|
|
373
|
+
servers = json.load(f)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
logger.error(f"Failed to read backup file {backup_file}: {e}")
|
|
376
|
+
return (0, 0)
|
|
377
|
+
|
|
378
|
+
success_count = 0
|
|
379
|
+
failure_count = 0
|
|
380
|
+
|
|
381
|
+
for server, zone in servers.items():
|
|
382
|
+
try:
|
|
383
|
+
# Remove from all zones first
|
|
384
|
+
remove_from_zone(server)
|
|
385
|
+
|
|
386
|
+
if zone == ZONE_LOCAL_INTRANET:
|
|
387
|
+
if add_to_intranet_zone(server, for_all_users=for_all_users):
|
|
388
|
+
success_count += 1
|
|
389
|
+
else:
|
|
390
|
+
failure_count += 1
|
|
391
|
+
elif zone == ZONE_TRUSTED_SITES:
|
|
392
|
+
# Add code to restore to trusted sites
|
|
393
|
+
# Currently not implemented
|
|
394
|
+
failure_count += 1
|
|
395
|
+
elif zone == ZONE_INTERNET:
|
|
396
|
+
# Add code to restore to internet zone
|
|
397
|
+
# Currently not implemented
|
|
398
|
+
failure_count += 1
|
|
399
|
+
elif zone == ZONE_RESTRICTED_SITES:
|
|
400
|
+
# Add code to restore to restricted sites
|
|
401
|
+
# Currently not implemented
|
|
402
|
+
failure_count += 1
|
|
403
|
+
else:
|
|
404
|
+
failure_count += 1
|
|
405
|
+
except Exception as e:
|
|
406
|
+
logger.error(f"Failed to restore zone for {server}: {e}")
|
|
407
|
+
failure_count += 1
|
|
408
|
+
|
|
409
|
+
logger.info(f"Restored zone settings: {success_count} succeeded, {failure_count} failed")
|
|
410
|
+
return (success_count, failure_count)
|