winbearable 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.
@@ -0,0 +1,79 @@
1
+ """winbearable — Make Windows + WSL2 bearable and locked down in pure Python."""
2
+
3
+ from .backends import (
4
+ InMemoryRegistryBackend,
5
+ InMemorySystemBackend,
6
+ NativeSystemBackend,
7
+ RegistryBackend,
8
+ SystemBackend,
9
+ WinregBackend,
10
+ )
11
+ from .const import __version__
12
+ from .engine import (
13
+ ApplySummary,
14
+ HighRiskConfirmationError,
15
+ apply_profile,
16
+ calculate_hardening_score,
17
+ detect_tweak,
18
+ evaluate_profile,
19
+ rollback_runs,
20
+ )
21
+ from .models import (
22
+ EnvironmentInfo,
23
+ ProfileSpec,
24
+ RegistryChangeDetail,
25
+ RiskLevel,
26
+ RunRecord,
27
+ TweakCheckResult,
28
+ TweakDefinition,
29
+ TweakStatus,
30
+ WslConfigSpec,
31
+ )
32
+ from .profiles import ProfileValidationError, generate_init_profile_toml, load_profile
33
+ from .state import StateStore
34
+ from .tweaks import TWEAK_CATALOG, get_all_tweaks, get_tweak
35
+ from .wsl import (
36
+ audit_wsl,
37
+ calculate_wsl_memory_gib,
38
+ format_host_memory_gib,
39
+ merge_ini_preserving_comments,
40
+ plan_or_apply_wsl,
41
+ )
42
+
43
+ __all__ = [
44
+ "ApplySummary",
45
+ "EnvironmentInfo",
46
+ "HighRiskConfirmationError",
47
+ "InMemoryRegistryBackend",
48
+ "InMemorySystemBackend",
49
+ "NativeSystemBackend",
50
+ "ProfileSpec",
51
+ "ProfileValidationError",
52
+ "RegistryBackend",
53
+ "RegistryChangeDetail",
54
+ "RiskLevel",
55
+ "RunRecord",
56
+ "StateStore",
57
+ "SystemBackend",
58
+ "TWEAK_CATALOG",
59
+ "TweakCheckResult",
60
+ "TweakDefinition",
61
+ "TweakStatus",
62
+ "WinregBackend",
63
+ "WslConfigSpec",
64
+ "__version__",
65
+ "apply_profile",
66
+ "audit_wsl",
67
+ "calculate_hardening_score",
68
+ "calculate_wsl_memory_gib",
69
+ "detect_tweak",
70
+ "evaluate_profile",
71
+ "format_host_memory_gib",
72
+ "generate_init_profile_toml",
73
+ "get_all_tweaks",
74
+ "get_tweak",
75
+ "load_profile",
76
+ "merge_ini_preserving_comments",
77
+ "plan_or_apply_wsl",
78
+ "rollback_runs",
79
+ ]
@@ -0,0 +1,6 @@
1
+ """Allow running `python -m winbearable`."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,27 @@
1
+ """Native Python backends for Windows registry, services, processes, and WSL."""
2
+
3
+ from .registry import (
4
+ InMemoryRegistryBackend,
5
+ RegistryBackend,
6
+ WinregBackend,
7
+ create_default_registry_backend,
8
+ )
9
+ from .system import (
10
+ CommandResult,
11
+ InMemorySystemBackend,
12
+ NativeSystemBackend,
13
+ SystemBackend,
14
+ create_default_system_backend,
15
+ )
16
+
17
+ __all__ = [
18
+ "CommandResult",
19
+ "InMemoryRegistryBackend",
20
+ "InMemorySystemBackend",
21
+ "NativeSystemBackend",
22
+ "RegistryBackend",
23
+ "SystemBackend",
24
+ "WinregBackend",
25
+ "create_default_registry_backend",
26
+ "create_default_system_backend",
27
+ ]
@@ -0,0 +1,330 @@
1
+ """Native Windows registry backend using stdlib winreg and in-memory test fake."""
2
+
3
+ import importlib
4
+ import sys
5
+ from typing import Any, Protocol
6
+
7
+ from ..const import (
8
+ HIVE_HKCU,
9
+ HIVE_HKLM,
10
+ REG_BINARY,
11
+ REG_DWORD,
12
+ REG_EXPAND_SZ,
13
+ REG_QWORD,
14
+ REG_SZ,
15
+ )
16
+ from ..models import PolicyConflictPath, RegistryValueSnapshot
17
+
18
+
19
+ class RegistryBackend(Protocol):
20
+ """Protocol for reading, writing, and snapshotting Windows registry keys."""
21
+
22
+ def key_exists(self, hive: str, key_path: str) -> bool:
23
+ """Return True if the specified registry key exists."""
24
+
25
+ def list_subkeys(self, hive: str, key_path: str) -> list[str]:
26
+ """List immediate subkey names under the specified registry key."""
27
+
28
+ def get_value(self, hive: str, key_path: str, value_name: str) -> tuple[int | str | None, str] | None:
29
+ """Read (value_data, value_type) or return None if key/value does not exist."""
30
+
31
+ def set_value(
32
+ self,
33
+ hive: str,
34
+ key_path: str,
35
+ value_name: str,
36
+ value_data: int | str | None,
37
+ value_type: str = REG_DWORD,
38
+ ) -> None:
39
+ """Create key if needed and set the specified registry value."""
40
+
41
+ def delete_value(self, hive: str, key_path: str, value_name: str) -> bool:
42
+ """Delete a registry value if present. Return True if deleted."""
43
+
44
+ def snapshot_value(self, hive: str, key_path: str, value_name: str) -> RegistryValueSnapshot:
45
+ """Capture current value state prior to modification."""
46
+
47
+ def restore_snapshot(self, snapshot: RegistryValueSnapshot) -> None:
48
+ """Restore a registry value to its snapshotted state."""
49
+
50
+ def check_policy_conflict(self, policy_checks: tuple[PolicyConflictPath, ...]) -> str | None:
51
+ """Return a policy conflict description if GPO/MDM enforces a conflicting setting."""
52
+
53
+
54
+ def _normalize_key_path(key_path: str) -> str:
55
+ return key_path.strip("\\")
56
+
57
+
58
+ class InMemoryRegistryBackend:
59
+ """Deterministic in-memory registry backend for Linux CI, unit tests, and dry-runs."""
60
+
61
+ def __init__(
62
+ self,
63
+ initial_values: dict[tuple[str, str, str], tuple[int | str | None, str]] | None = None,
64
+ initial_keys: set[tuple[str, str]] | None = None,
65
+ ) -> None:
66
+ self._keys: set[tuple[str, str]] = {
67
+ (HIVE_HKLM, ""),
68
+ (HIVE_HKCU, ""),
69
+ }
70
+ if initial_keys:
71
+ for hive, raw_path in initial_keys:
72
+ self._ensure_key_hierarchy(hive, raw_path)
73
+ self._values: dict[tuple[str, str, str], tuple[int | str | None, str]] = {}
74
+ if initial_values:
75
+ for (hive, raw_path, val_name), val_tuple in initial_values.items():
76
+ norm_path = _normalize_key_path(raw_path)
77
+ self._ensure_key_hierarchy(hive, norm_path)
78
+ self._values[(hive, norm_path, val_name)] = val_tuple
79
+
80
+ def _ensure_key_hierarchy(self, hive: str, key_path: str) -> None:
81
+ norm = _normalize_key_path(key_path)
82
+ self._keys.add((hive, ""))
83
+ if not norm:
84
+ return
85
+ parts = norm.split("\\")
86
+ for idx in range(1, len(parts) + 1):
87
+ prefix = "\\".join(parts[:idx])
88
+ self._keys.add((hive, prefix))
89
+
90
+ def key_exists(self, hive: str, key_path: str) -> bool:
91
+ norm = _normalize_key_path(key_path)
92
+ return (hive, norm) in self._keys
93
+
94
+ def list_subkeys(self, hive: str, key_path: str) -> list[str]:
95
+ norm = _normalize_key_path(key_path)
96
+ if not self.key_exists(hive, norm):
97
+ return []
98
+ prefix = f"{norm}\\" if norm else ""
99
+ subkeys: set[str] = set()
100
+ for cand_hive, cand_path in self._keys:
101
+ if cand_hive != hive or not cand_path or cand_path == norm:
102
+ continue
103
+ if cand_path.startswith(prefix):
104
+ remainder = cand_path[len(prefix) :]
105
+ if remainder and "\\" not in remainder:
106
+ subkeys.add(remainder)
107
+ return sorted(subkeys)
108
+
109
+ def get_value(self, hive: str, key_path: str, value_name: str) -> tuple[int | str | None, str] | None:
110
+ norm = _normalize_key_path(key_path)
111
+ return self._values.get((hive, norm, value_name))
112
+
113
+ def set_value(
114
+ self,
115
+ hive: str,
116
+ key_path: str,
117
+ value_name: str,
118
+ value_data: int | str | None,
119
+ value_type: str = REG_DWORD,
120
+ ) -> None:
121
+ norm = _normalize_key_path(key_path)
122
+ self._ensure_key_hierarchy(hive, norm)
123
+ self._values[(hive, norm, value_name)] = (value_data, value_type)
124
+
125
+ def delete_value(self, hive: str, key_path: str, value_name: str) -> bool:
126
+ norm = _normalize_key_path(key_path)
127
+ lookup = (hive, norm, value_name)
128
+ if lookup in self._values:
129
+ del self._values[lookup]
130
+ return True
131
+ return False
132
+
133
+ def snapshot_value(self, hive: str, key_path: str, value_name: str) -> RegistryValueSnapshot:
134
+ norm = _normalize_key_path(key_path)
135
+ existed_key = self.key_exists(hive, norm)
136
+ current = self.get_value(hive, norm, value_name)
137
+ if current is None:
138
+ return RegistryValueSnapshot(
139
+ hive=hive,
140
+ key_path=norm,
141
+ value_name=value_name,
142
+ key_existed=existed_key,
143
+ value_existed=False,
144
+ previous_data=None,
145
+ previous_type=None,
146
+ )
147
+ return RegistryValueSnapshot(
148
+ hive=hive,
149
+ key_path=norm,
150
+ value_name=value_name,
151
+ key_existed=existed_key,
152
+ value_existed=True,
153
+ previous_data=current[0],
154
+ previous_type=current[1],
155
+ )
156
+
157
+ def restore_snapshot(self, snapshot: RegistryValueSnapshot) -> None:
158
+ if snapshot.value_existed and snapshot.previous_type is not None:
159
+ self.set_value(
160
+ hive=snapshot.hive,
161
+ key_path=snapshot.key_path,
162
+ value_name=snapshot.value_name,
163
+ value_data=snapshot.previous_data,
164
+ value_type=snapshot.previous_type,
165
+ )
166
+ else:
167
+ self.delete_value(
168
+ hive=snapshot.hive,
169
+ key_path=snapshot.key_path,
170
+ value_name=snapshot.value_name,
171
+ )
172
+
173
+ def check_policy_conflict(self, policy_checks: tuple[PolicyConflictPath, ...]) -> str | None:
174
+ for policy in policy_checks:
175
+ existing = self.get_value(policy.hive, policy.key_path, policy.value_name)
176
+ if existing is None:
177
+ continue
178
+ current_val, _ = existing
179
+ if policy.enforced_value is None or current_val == policy.enforced_value:
180
+ return f"Enforced by policy at {policy.hive}\\{policy.key_path}\\{policy.value_name}={current_val}"
181
+ return None
182
+
183
+
184
+ class WinregBackend:
185
+ """Native Windows registry backend powered by stdlib winreg (zero PowerShell)."""
186
+
187
+ def __init__(self, winreg_module: Any | None = None) -> None:
188
+ if winreg_module is None:
189
+ if sys.platform != "win32":
190
+ raise RuntimeError("WinregBackend is only available on Windows (win32).")
191
+ winreg_module = importlib.import_module("winreg") # pragma: no cover
192
+
193
+ self._winreg: Any = winreg_module
194
+ self._hive_map: dict[str, int] = {
195
+ HIVE_HKCU: int(self._winreg.HKEY_CURRENT_USER),
196
+ HIVE_HKLM: int(self._winreg.HKEY_LOCAL_MACHINE),
197
+ }
198
+ self._type_to_int: dict[str, int] = {
199
+ REG_SZ: int(self._winreg.REG_SZ),
200
+ REG_EXPAND_SZ: int(self._winreg.REG_EXPAND_SZ),
201
+ REG_BINARY: int(self._winreg.REG_BINARY),
202
+ REG_DWORD: int(self._winreg.REG_DWORD),
203
+ REG_QWORD: int(self._winreg.REG_QWORD),
204
+ }
205
+ self._int_to_type: dict[int, str] = {val: key for key, val in self._type_to_int.items()}
206
+
207
+ def _resolve_hive(self, hive: str) -> int:
208
+ if hive not in self._hive_map:
209
+ raise ValueError(f"Unsupported registry hive: {hive}")
210
+ return self._hive_map[hive]
211
+
212
+ def key_exists(self, hive: str, key_path: str) -> bool:
213
+ root = self._resolve_hive(hive)
214
+ norm = _normalize_key_path(key_path)
215
+ try:
216
+ with self._winreg.OpenKey(root, norm, 0, self._winreg.KEY_READ):
217
+ return True
218
+ except OSError:
219
+ return False
220
+
221
+ def list_subkeys(self, hive: str, key_path: str) -> list[str]:
222
+ root = self._resolve_hive(hive)
223
+ norm = _normalize_key_path(key_path)
224
+ subkeys: list[str] = []
225
+ try:
226
+ with self._winreg.OpenKey(root, norm, 0, self._winreg.KEY_READ) as handle:
227
+ idx = 0
228
+ while True:
229
+ try:
230
+ subkeys.append(str(self._winreg.EnumKey(handle, idx)))
231
+ idx += 1
232
+ except OSError:
233
+ break
234
+ except OSError:
235
+ return []
236
+ return sorted(subkeys)
237
+
238
+ def get_value(self, hive: str, key_path: str, value_name: str) -> tuple[int | str | None, str] | None:
239
+ root = self._resolve_hive(hive)
240
+ norm = _normalize_key_path(key_path)
241
+ try:
242
+ with self._winreg.OpenKey(root, norm, 0, self._winreg.KEY_READ) as handle:
243
+ raw_data, raw_type = self._winreg.QueryValueEx(handle, value_name)
244
+ reg_type = self._int_to_type.get(int(raw_type), REG_SZ)
245
+ typed_data: int | str | None = raw_data if isinstance(raw_data, (int, str)) else None
246
+ return typed_data, reg_type
247
+ except OSError:
248
+ return None
249
+
250
+ def set_value(
251
+ self,
252
+ hive: str,
253
+ key_path: str,
254
+ value_name: str,
255
+ value_data: int | str | None,
256
+ value_type: str = REG_DWORD,
257
+ ) -> None:
258
+ root = self._resolve_hive(hive)
259
+ norm = _normalize_key_path(key_path)
260
+ raw_type = self._type_to_int.get(value_type, int(self._winreg.REG_DWORD))
261
+ with self._winreg.CreateKeyEx(root, norm, 0, self._winreg.KEY_WRITE) as handle:
262
+ self._winreg.SetValueEx(handle, value_name, 0, raw_type, value_data)
263
+
264
+ def delete_value(self, hive: str, key_path: str, value_name: str) -> bool:
265
+ root = self._resolve_hive(hive)
266
+ norm = _normalize_key_path(key_path)
267
+ try:
268
+ with self._winreg.OpenKey(root, norm, 0, self._winreg.KEY_SET_VALUE) as handle:
269
+ self._winreg.DeleteValue(handle, value_name)
270
+ return True
271
+ except OSError:
272
+ return False
273
+
274
+ def snapshot_value(self, hive: str, key_path: str, value_name: str) -> RegistryValueSnapshot:
275
+ norm = _normalize_key_path(key_path)
276
+ existed_key = self.key_exists(hive, norm)
277
+ current = self.get_value(hive, norm, value_name)
278
+ if current is None:
279
+ return RegistryValueSnapshot(
280
+ hive=hive,
281
+ key_path=norm,
282
+ value_name=value_name,
283
+ key_existed=existed_key,
284
+ value_existed=False,
285
+ previous_data=None,
286
+ previous_type=None,
287
+ )
288
+ return RegistryValueSnapshot(
289
+ hive=hive,
290
+ key_path=norm,
291
+ value_name=value_name,
292
+ key_existed=existed_key,
293
+ value_existed=True,
294
+ previous_data=current[0],
295
+ previous_type=current[1],
296
+ )
297
+
298
+ def restore_snapshot(self, snapshot: RegistryValueSnapshot) -> None:
299
+ if snapshot.value_existed and snapshot.previous_type is not None:
300
+ self.set_value(
301
+ hive=snapshot.hive,
302
+ key_path=snapshot.key_path,
303
+ value_name=snapshot.value_name,
304
+ value_data=snapshot.previous_data,
305
+ value_type=snapshot.previous_type,
306
+ )
307
+ else:
308
+ self.delete_value(
309
+ hive=snapshot.hive,
310
+ key_path=snapshot.key_path,
311
+ value_name=snapshot.value_name,
312
+ )
313
+
314
+ def check_policy_conflict(self, policy_checks: tuple[PolicyConflictPath, ...]) -> str | None:
315
+ for policy in policy_checks:
316
+ existing = self.get_value(policy.hive, policy.key_path, policy.value_name)
317
+ if existing is None:
318
+ continue
319
+ current_val, _ = existing
320
+ if policy.enforced_value is None or current_val == policy.enforced_value:
321
+ return f"Enforced by policy at {policy.hive}\\{policy.key_path}\\{policy.value_name}={current_val}"
322
+ return None
323
+
324
+
325
+ def create_default_registry_backend(platform_override: str | None = None) -> RegistryBackend:
326
+ """Create WinregBackend on Windows or InMemoryRegistryBackend on non-Windows hosts."""
327
+ target_platform = platform_override or sys.platform
328
+ if target_platform == "win32":
329
+ return WinregBackend() # pragma: no cover
330
+ return InMemoryRegistryBackend()