107 lines
3.5 KiB
Python
107 lines
3.5 KiB
Python
"""Windows version / PowerShell capability probe.
|
|
|
|
Used by privacy features to log a single clear reason when a function silently
|
|
no-ops on older Windows Server hosts (Server 2008 R2 ships PowerShell 2.0 which
|
|
does not have ``Get-NetAdapter`` / ``Set-NetAdapter`` / ``Disable-NetAdapterBinding``
|
|
/ ``Get-DnsClientServerAddress`` / ``Rename-Computer``).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import platform
|
|
import subprocess
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from functools import lru_cache
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WinCompat:
|
|
release: str # e.g. "10", "2012ServerR2"
|
|
build: int # major build number
|
|
powershell_major: int # 0 = PowerShell not detected
|
|
has_net_cmdlets: bool # Get-NetAdapter etc. (PS 3.0+ on Server 2012+)
|
|
has_defender: bool # Add-MpPreference cmdlet exists
|
|
|
|
def summary(self) -> str:
|
|
if sys.platform != "win32":
|
|
return (
|
|
f"{platform.system()} release={self.release} "
|
|
"Windows-only privacy controls disabled"
|
|
)
|
|
return (
|
|
f"Windows release={self.release} build={self.build} "
|
|
f"PowerShell={self.powershell_major}.x "
|
|
f"NetAdapter cmdlets={'yes' if self.has_net_cmdlets else 'no'} "
|
|
f"Defender={'yes' if self.has_defender else 'no'}"
|
|
)
|
|
|
|
|
|
def _powershell_major() -> int:
|
|
try:
|
|
r = subprocess.run(
|
|
["powershell", "-NoProfile", "-NonInteractive", "-Command",
|
|
"$PSVersionTable.PSVersion.Major"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=12,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
)
|
|
out = (r.stdout or "").strip().splitlines()[-1] if r.stdout else ""
|
|
return int(out) if out.isdigit() else 0
|
|
except Exception:
|
|
return 0
|
|
|
|
|
|
def _has_cmdlet(name: str) -> bool:
|
|
try:
|
|
r = subprocess.run(
|
|
["powershell", "-NoProfile", "-NonInteractive", "-Command",
|
|
f"if (Get-Command {name} -ErrorAction SilentlyContinue) "
|
|
f"{{ 'yes' }} else {{ 'no' }}"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
)
|
|
return "yes" in (r.stdout or "").lower()
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def probe() -> WinCompat:
|
|
"""Cached host probe. Cheap on subsequent calls."""
|
|
if sys.platform != "win32":
|
|
info = WinCompat(
|
|
release=platform.release(),
|
|
build=0,
|
|
powershell_major=0,
|
|
has_net_cmdlets=False,
|
|
has_defender=False,
|
|
)
|
|
log.info("Compat probe: %s", info.summary())
|
|
return info
|
|
release = platform.release()
|
|
try:
|
|
build = int(platform.version().split(".")[-1])
|
|
except Exception:
|
|
build = 0
|
|
ps = _powershell_major()
|
|
# PowerShell >= 3.0 is the gate for the modern Net* cmdlets that all the
|
|
# privacy features rely on. Anything older (Server 2008 R2 RTM) only has
|
|
# PS 2.0 and needs WMI / netsh / ipconfig fallbacks.
|
|
has_net = ps >= 3 and _has_cmdlet("Get-NetAdapter")
|
|
has_def = _has_cmdlet("Add-MpPreference")
|
|
info = WinCompat(
|
|
release=release,
|
|
build=build,
|
|
powershell_major=ps,
|
|
has_net_cmdlets=has_net,
|
|
has_defender=has_def,
|
|
)
|
|
log.info("WinCompat probe: %s", info.summary())
|
|
return info
|