fix: audit round 2 - DPAPI secrets, pinned hop probe, gost exe hash, admin guard, PID-scoped browser tracking, emergency disengage button, build sidecar
This commit is contained in:
@@ -135,6 +135,59 @@ def _firefox_is_running_on_system() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _has_descendants(root_pid: int) -> bool:
|
||||
"""True iff *root_pid* or any descendant process is currently alive.
|
||||
|
||||
Walks the process tree from root_pid using WMIC ProcessId/ParentProcessId
|
||||
(no admin required). Falls back to checking the root PID's existence via
|
||||
tasklist if WMIC is missing on the host.
|
||||
"""
|
||||
if not root_pid:
|
||||
return False
|
||||
# Fast path: is the root PID itself still alive?
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["tasklist", "/FI", f"PID eq {root_pid}", "/NH", "/FO", "CSV"],
|
||||
capture_output=True, text=True, timeout=6,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000),
|
||||
)
|
||||
if str(root_pid) in (r.stdout or ""):
|
||||
return True
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
# Walk descendants via WMIC. Builds {parent: [child, ...]} then BFS.
|
||||
try:
|
||||
w = subprocess.run(
|
||||
["wmic", "process", "get", "ProcessId,ParentProcessId", "/FORMAT:CSV"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000),
|
||||
)
|
||||
children: dict[int, list[int]] = {}
|
||||
for line in (w.stdout or "").splitlines():
|
||||
parts = [p.strip() for p in line.split(",")]
|
||||
# CSV header: Node,ParentProcessId,ProcessId
|
||||
if len(parts) < 3 or not parts[1].isdigit() or not parts[2].isdigit():
|
||||
continue
|
||||
ppid, pid = int(parts[1]), int(parts[2])
|
||||
children.setdefault(ppid, []).append(pid)
|
||||
stack = [root_pid]
|
||||
seen = {root_pid}
|
||||
while stack:
|
||||
cur = stack.pop()
|
||||
kids = children.get(cur, [])
|
||||
for k in kids:
|
||||
if k in seen:
|
||||
continue
|
||||
seen.add(k)
|
||||
# Any live descendant = our session is still alive.
|
||||
return True
|
||||
return False
|
||||
except Exception: # noqa: BLE001
|
||||
# WMIC missing (newer Windows) — be conservative and return False
|
||||
# rather than the old "any firefox.exe = mine" heuristic.
|
||||
return False
|
||||
|
||||
|
||||
class BrowserSession:
|
||||
def __init__(self) -> None:
|
||||
self._proc: subprocess.Popen[str] | None = None
|
||||
@@ -145,16 +198,15 @@ class BrowserSession:
|
||||
if self._proc is None:
|
||||
return False
|
||||
# Firefox's initial launcher process exits quickly (code 0) while
|
||||
# browser child processes carry on. Poll the parent process but also
|
||||
# fall back to checking the system process list so the GUI doesn't
|
||||
# falsely report "stopped".
|
||||
# browser child processes carry on. Poll the parent process first.
|
||||
if self._proc.poll() is None:
|
||||
return True
|
||||
# Parent has exited — check if any firefox.exe is still alive AND we
|
||||
# launched within the last 5 minutes (to avoid false positives from
|
||||
# unrelated browser sessions).
|
||||
age = time.monotonic() - self._launched_at
|
||||
return age < 300 and _firefox_is_running_on_system()
|
||||
# Parent has exited — check whether any process in our spawned tree
|
||||
# (the original PID's children) is still alive. Using a PID-scoped
|
||||
# WMIC query avoids the old failure mode of treating any unrelated
|
||||
# firefox.exe on the machine as ours.
|
||||
launched_pid = self._proc.pid
|
||||
return _has_descendants(launched_pid)
|
||||
|
||||
def pid(self) -> int | None:
|
||||
if self._proc is None:
|
||||
|
||||
Reference in New Issue
Block a user