300 lines
10 KiB
Python
300 lines
10 KiB
Python
"""Reversibly disable Windows telemetry / data collection.
|
|
|
|
Targets, in order of leak severity:
|
|
|
|
• DiagTrack service — primary telemetry pipeline ("Connected User
|
|
Experiences and Telemetry"). Phones home every
|
|
few minutes.
|
|
• dmwappushservice — WAP Push routing (DSMS), historically tied to
|
|
DiagTrack. Disabled when present.
|
|
• AllowTelemetry (HKLM) — DataCollection policy. 0 = lowest tier.
|
|
• Activity History — EnableActivityFeed / Upload / PublishUserActivities.
|
|
• Advertising ID — HKCU\\…\\AdvertisingInfo\\Enabled.
|
|
• Cortana Bing search — HKCU\\…\\Search\\BingSearchEnabled.
|
|
• Scheduled tasks — Compatibility Appraiser + CEIP Consolidator
|
|
+ UsbCeip.
|
|
|
|
Every change is snapshotted before mutation; ``restore_telemetry`` rolls back.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import subprocess
|
|
import sys
|
|
import winreg
|
|
from dataclasses import dataclass, field
|
|
|
|
from .firewall import is_admin
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_SERVICES = ("DiagTrack", "dmwappushservice")
|
|
|
|
_SCHED_TASKS = (
|
|
r"\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser",
|
|
r"\Microsoft\Windows\Customer Experience Improvement Program\Consolidator",
|
|
r"\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip",
|
|
)
|
|
|
|
# (hive, path, value_name, dword_when_killed)
|
|
_REG_KILLS = (
|
|
(winreg.HKEY_LOCAL_MACHINE,
|
|
r"SOFTWARE\Policies\Microsoft\Windows\DataCollection",
|
|
"AllowTelemetry", 0),
|
|
(winreg.HKEY_LOCAL_MACHINE,
|
|
r"SOFTWARE\Policies\Microsoft\Windows\System",
|
|
"EnableActivityFeed", 0),
|
|
(winreg.HKEY_LOCAL_MACHINE,
|
|
r"SOFTWARE\Policies\Microsoft\Windows\System",
|
|
"PublishUserActivities", 0),
|
|
(winreg.HKEY_LOCAL_MACHINE,
|
|
r"SOFTWARE\Policies\Microsoft\Windows\System",
|
|
"UploadUserActivities", 0),
|
|
(winreg.HKEY_LOCAL_MACHINE,
|
|
r"SOFTWARE\Policies\Microsoft\Windows\CloudContent",
|
|
"DisableWindowsConsumerFeatures", 1),
|
|
(winreg.HKEY_CURRENT_USER,
|
|
r"Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo",
|
|
"Enabled", 0),
|
|
(winreg.HKEY_CURRENT_USER,
|
|
r"Software\Microsoft\Windows\CurrentVersion\Search",
|
|
"BingSearchEnabled", 0),
|
|
(winreg.HKEY_CURRENT_USER,
|
|
r"Software\Microsoft\Windows\CurrentVersion\Search",
|
|
"CortanaConsent", 0),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class TelemetrySnapshot:
|
|
service_state: dict[str, str] = field(default_factory=dict) # name -> 'auto'|'manual'|'disabled'|'absent'
|
|
sched_tasks_state: dict[str, bool] = field(default_factory=dict) # task -> was_enabled
|
|
reg_values: list[dict] = field(default_factory=list)
|
|
|
|
|
|
def _run(args: list[str], timeout: float = 12.0) -> tuple[int, str, str]:
|
|
try:
|
|
r = subprocess.run(
|
|
args, capture_output=True, text=True, timeout=timeout,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
)
|
|
return r.returncode, r.stdout or "", r.stderr or ""
|
|
except Exception as e:
|
|
return 1, "", str(e)
|
|
|
|
|
|
# ── service helpers ──────────────────────────────────────────────────────────
|
|
|
|
def _service_start_type(name: str) -> str:
|
|
code, out, _ = _run(["sc", "qc", name])
|
|
if code != 0:
|
|
return "absent"
|
|
for ln in out.splitlines():
|
|
s = ln.strip().lower()
|
|
if "start_type" in s:
|
|
if "2" in s or "auto" in s:
|
|
return "auto"
|
|
if "3" in s or "demand" in s or "manual" in s:
|
|
return "manual"
|
|
if "4" in s or "disabled" in s:
|
|
return "disabled"
|
|
return "absent"
|
|
|
|
|
|
def _set_service_disabled(name: str) -> bool:
|
|
_run(["sc", "stop", name], timeout=15)
|
|
code, _, _ = _run(["sc", "config", name, "start=", "disabled"])
|
|
return code == 0
|
|
|
|
|
|
def _set_service_start(name: str, want: str) -> bool:
|
|
start_value = {"auto": "auto", "manual": "demand", "disabled": "disabled"}.get(want)
|
|
if not start_value:
|
|
return False
|
|
_run(["sc", "stop", name], timeout=10)
|
|
code, _, _ = _run(["sc", "config", name, "start=", start_value])
|
|
if want == "auto":
|
|
_run(["sc", "start", name], timeout=15)
|
|
return code == 0
|
|
|
|
|
|
# ── scheduled-task helpers ──────────────────────────────────────────────────
|
|
|
|
def _task_is_enabled(name: str) -> bool | None:
|
|
code, out, _ = _run(["schtasks", "/Query", "/TN", name])
|
|
if code != 0:
|
|
return None
|
|
for ln in out.splitlines():
|
|
s = ln.strip().lower()
|
|
if s.startswith("status:") or "ready" in s or "disabled" in s:
|
|
if "disabled" in s:
|
|
return False
|
|
if "ready" in s or "running" in s:
|
|
return True
|
|
return True # assume ready when query succeeded
|
|
|
|
|
|
def _set_task_state(name: str, enable: bool) -> bool:
|
|
code, _, _ = _run([
|
|
"schtasks", "/Change", "/TN", name,
|
|
"/Enable" if enable else "/Disable",
|
|
])
|
|
return code == 0
|
|
|
|
|
|
# ── registry helpers ────────────────────────────────────────────────────────
|
|
|
|
def _read_reg_dword(hive: int, path: str, name: str) -> tuple[bool, int | None]:
|
|
"""Returns (present, value)."""
|
|
try:
|
|
with winreg.OpenKey(hive, path, 0, winreg.KEY_QUERY_VALUE) as key:
|
|
v, _ = winreg.QueryValueEx(key, name)
|
|
return True, int(v)
|
|
except OSError:
|
|
return False, None
|
|
|
|
|
|
def _write_reg_dword(hive: int, path: str, name: str, value: int) -> bool:
|
|
try:
|
|
with winreg.CreateKeyEx(hive, path, 0, winreg.KEY_SET_VALUE) as key:
|
|
winreg.SetValueEx(key, name, 0, winreg.REG_DWORD, value)
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def _delete_reg_value(hive: int, path: str, name: str) -> None:
|
|
try:
|
|
with winreg.OpenKey(hive, path, 0, winreg.KEY_SET_VALUE) as key:
|
|
winreg.DeleteValue(key, name)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
# ── public API ──────────────────────────────────────────────────────────────
|
|
|
|
def telemetry_status() -> dict[str, str]:
|
|
"""Read-only snapshot for the audit panel."""
|
|
if sys.platform == "darwin":
|
|
return {"macOS": "not applicable"}
|
|
out: dict[str, str] = {}
|
|
for svc in _SERVICES:
|
|
out[f"svc.{svc}"] = _service_start_type(svc)
|
|
pres, val = _read_reg_dword(
|
|
winreg.HKEY_LOCAL_MACHINE,
|
|
r"SOFTWARE\Policies\Microsoft\Windows\DataCollection",
|
|
"AllowTelemetry",
|
|
)
|
|
out["AllowTelemetry"] = "0 (killed)" if pres and val == 0 else (
|
|
f"{val}" if pres else "unset"
|
|
)
|
|
pres, val = _read_reg_dword(
|
|
winreg.HKEY_CURRENT_USER,
|
|
r"Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo",
|
|
"Enabled",
|
|
)
|
|
out["AdvertisingID"] = "off" if pres and val == 0 else (
|
|
"on" if pres and val == 1 else "unset"
|
|
)
|
|
return out
|
|
|
|
|
|
def engage_telemetry_kill() -> tuple[TelemetrySnapshot | None, list[str]]:
|
|
logs: list[str] = []
|
|
if sys.platform == "darwin":
|
|
return None, ["Telemetry kill is Windows-only; no macOS telemetry mutation applied."]
|
|
if not is_admin():
|
|
return None, ["Telemetry kill skipped — needs Administrator."]
|
|
snap = TelemetrySnapshot()
|
|
|
|
for svc in _SERVICES:
|
|
st = _service_start_type(svc)
|
|
snap.service_state[svc] = st
|
|
if st == "absent":
|
|
continue
|
|
if _set_service_disabled(svc):
|
|
logs.append(f"Service '{svc}' stopped + disabled (was {st}).")
|
|
else:
|
|
logs.append(f"Service '{svc}' could not be disabled.")
|
|
|
|
for task in _SCHED_TASKS:
|
|
was = _task_is_enabled(task)
|
|
if was is None:
|
|
continue
|
|
snap.sched_tasks_state[task] = was
|
|
if was:
|
|
if _set_task_state(task, enable=False):
|
|
logs.append(f"Scheduled task disabled: {task}")
|
|
|
|
for hive, path, name, kill_val in _REG_KILLS:
|
|
present, prev = _read_reg_dword(hive, path, name)
|
|
snap.reg_values.append({
|
|
"hive": "HKLM" if hive == winreg.HKEY_LOCAL_MACHINE else "HKCU",
|
|
"path": path,
|
|
"name": name,
|
|
"present": present,
|
|
"prev": prev,
|
|
})
|
|
if _write_reg_dword(hive, path, name, kill_val):
|
|
logs.append(f"reg: {('HKLM' if hive == winreg.HKEY_LOCAL_MACHINE else 'HKCU')}\\{path}\\{name} → {kill_val}")
|
|
else:
|
|
logs.append(f"reg write failed: {path}\\{name}")
|
|
|
|
return snap, logs
|
|
|
|
|
|
def restore_telemetry(snap: TelemetrySnapshot | None) -> list[str]:
|
|
if sys.platform == "darwin":
|
|
return []
|
|
if snap is None or not is_admin():
|
|
return []
|
|
logs: list[str] = []
|
|
|
|
for svc, prev in snap.service_state.items():
|
|
if prev == "absent":
|
|
continue
|
|
if _set_service_start(svc, prev):
|
|
logs.append(f"Service '{svc}' restored → {prev}.")
|
|
|
|
for task, was in snap.sched_tasks_state.items():
|
|
if was:
|
|
_set_task_state(task, enable=True)
|
|
logs.append(f"Scheduled task re-enabled: {task}")
|
|
|
|
for entry in snap.reg_values:
|
|
hive = winreg.HKEY_LOCAL_MACHINE if entry["hive"] == "HKLM" else winreg.HKEY_CURRENT_USER
|
|
path = entry["path"]
|
|
name = entry["name"]
|
|
if entry["present"]:
|
|
_write_reg_dword(hive, path, name, int(entry["prev"]))
|
|
logs.append(f"reg restore: {entry['hive']}\\{path}\\{name} ← {entry['prev']}")
|
|
else:
|
|
_delete_reg_value(hive, path, name)
|
|
logs.append(f"reg removed (was absent): {entry['hive']}\\{path}\\{name}")
|
|
return logs
|
|
|
|
|
|
def snapshot_to_json(snap: TelemetrySnapshot | None) -> str:
|
|
if snap is None:
|
|
return ""
|
|
return json.dumps({
|
|
"services": snap.service_state,
|
|
"tasks": snap.sched_tasks_state,
|
|
"reg": snap.reg_values,
|
|
})
|
|
|
|
|
|
def snapshot_from_json(raw: str) -> TelemetrySnapshot | None:
|
|
if not raw:
|
|
return None
|
|
try:
|
|
d = json.loads(raw)
|
|
return TelemetrySnapshot(
|
|
service_state=dict(d.get("services") or {}),
|
|
sched_tasks_state={k: bool(v) for k, v in (d.get("tasks") or {}).items()},
|
|
reg_values=list(d.get("reg") or []),
|
|
)
|
|
except Exception:
|
|
return None
|