Continued Work on Remote Script Execution

This commit is contained in:
2025-09-04 04:52:01 -06:00
parent c6047c41d9
commit 772a9c31d0
8 changed files with 485 additions and 1196 deletions

View File

@@ -1,7 +1,6 @@
import os
import sys
import subprocess
import shlex
import ctypes
from datetime import datetime
@@ -11,12 +10,6 @@ def _now():
def project_paths():
r"""Derive important paths relative to the venv python (sys.executable).
Layout assumed at runtime:
<ProjectRoot>/Agent/Scripts/python.exe == sys.executable
<ProjectRoot>/Agent/Borealis/*.py == deployed agent files
"""
venv_scripts = os.path.dirname(os.path.abspath(sys.executable))
venv_root = os.path.abspath(os.path.join(venv_scripts, os.pardir))
project_root = os.path.abspath(os.path.join(venv_root, os.pardir))
@@ -31,8 +24,6 @@ def project_paths():
"borealis_dir": borealis_dir,
"logs_dir": logs_dir,
"temp_dir": temp_dir,
"service_script": os.path.join(borealis_dir, "windows_script_service.py"),
# Use tray launcher for the scheduled task
"agent_script": os.path.join(borealis_dir, "tray_launcher.py"),
}
@@ -53,43 +44,21 @@ def log_write(paths, name, text):
def is_admin():
try:
return ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore[attr-defined]
return ctypes.windll.shell32.IsUserAnAdmin() != 0
except Exception:
return False
def run(cmd, cwd=None, capture=False):
if isinstance(cmd, str):
shell = True
args = cmd
else:
shell = False
args = cmd
return subprocess.run(
args,
cwd=cwd,
shell=shell,
text=True,
capture_output=capture,
check=False,
)
def run(cmd, capture=False):
return subprocess.run(cmd, text=True, capture_output=capture, check=False)
def run_elevated_powershell(paths, ps_content, log_name):
"""Run a short PowerShell script elevated and wait for completion.
Writes combined output to Logs/log_name.
"""
ensure_dirs(paths)
log_path = os.path.join(paths["logs_dir"], log_name)
stub_path = os.path.join(paths["temp_dir"], f"elevate_{os.getpid()}_{log_name.replace('.', '_')}.ps1")
with open(stub_path, "w", encoding="utf-8") as f:
f.write(ps_content)
# Build powershell command
ps = "powershell.exe"
args = f"-NoProfile -ExecutionPolicy Bypass -File \"{stub_path}\""
# ShellExecute to run as admin
SEE_MASK_NOCLOSEPROCESS = 0x00000040
class SHELLEXECUTEINFO(ctypes.Structure):
_fields_ = [
@@ -109,31 +78,21 @@ def run_elevated_powershell(paths, ps_content, log_name):
("hIcon", ctypes.c_void_p),
("hProcess", ctypes.c_void_p),
]
sei = SHELLEXECUTEINFO()
sei.cbSize = ctypes.sizeof(SHELLEXECUTEINFO)
sei.fMask = SEE_MASK_NOCLOSEPROCESS
sei.hwnd = None
sei.lpVerb = "runas"
sei.lpFile = ps
sei.lpParameters = args
sei.lpFile = "powershell.exe"
sei.lpParameters = f"-NoProfile -ExecutionPolicy Bypass -File \"{stub_path}\""
sei.lpDirectory = paths["project_root"]
sei.nShow = 1
if not ctypes.windll.shell32.ShellExecuteExW(ctypes.byref(sei)):
log_write(paths, log_name, "[ERROR] UAC elevation failed (ShellExecuteExW)")
try:
os.remove(stub_path)
except Exception:
pass
return 1
# Wait for elevated process
ctypes.windll.kernel32.WaitForSingleObject(sei.hProcess, 0xFFFFFFFF)
# Capture output from stub if it appended
try:
with open(log_path, "a", encoding="utf-8") as f:
f.write("")
except Exception:
pass
hproc = sei.hProcess
if hproc:
ctypes.windll.kernel32.WaitForSingleObject(hproc, 0xFFFFFFFF)
try:
os.remove(stub_path)
except Exception:
@@ -141,262 +100,64 @@ def run_elevated_powershell(paths, ps_content, log_name):
return 0
def current_service_path(service_name):
"""Return the configured BinaryPathName for a service (or None)."""
try:
r = run(["sc.exe", "qc", service_name], capture=True)
if r.returncode != 0:
return None
for line in (r.stdout or "").splitlines():
if "BINARY_PATH_NAME" in line:
# Example: "BINARY_PATH_NAME : C:\\...\\python.exe C:\\...\\windows_script_service.py"
parts = line.split(":", 1)
if len(parts) == 2:
return parts[1].strip()
except Exception:
pass
return None
def ensure_script_service(paths):
service_name = "BorealisAgent"
log_name = "Borealis_ScriptService_Install.log"
ensure_dirs(paths)
log_write(paths, log_name, "[INFO] Ensuring script execution service...")
# Decide if install/update needed
need_install = False
bin_path = current_service_path(service_name)
expected_root = paths["venv_root"].lower()
if not bin_path:
need_install = True
else:
if expected_root not in bin_path.lower():
need_install = True
if not is_admin():
# Relaunch elevated to perform service installation/update
venv = paths["venv_python"].replace("\"", "\"\"")
srv = paths["service_script"].replace("\"", "\"\"")
log = os.path.join(paths["logs_dir"], log_name).replace("\"", "\"\"")
venv_dir = os.path.dirname(paths["venv_python"]).replace("\"", "\"\"")
postinstall = os.path.join(venv_dir, "pywin32_postinstall.py").replace("\"", "\"\"")
py_home = paths["venv_root"].replace("\"", "\"\"")
content = f"""
$ErrorActionPreference = 'Continue'
$venv = "{venv}"
$srv = "{srv}"
$log = "{log}"
$post = "{postinstall}"
$pyhome = "{py_home}"
try {{
try {{ New-Item -ItemType Directory -Force -Path (Split-Path $log -Parent) | Out-Null }} catch {{}}
# Remove legacy service names if present
try {{ sc.exe stop BorealisScriptAgent 2>$null | Out-Null }} catch {{}}
try {{ sc.exe delete BorealisScriptAgent 2>$null | Out-Null }} catch {{}}
try {{ sc.exe stop BorealisScriptService 2>$null | Out-Null }} catch {{}}
try {{ sc.exe delete BorealisScriptService 2>$null | Out-Null }} catch {{}}
if (Test-Path $post) {{ & $venv $post -install *>> "$log" }} else {{ & $venv -m pywin32_postinstall -install *>> "$log" }}
try {{ & $venv $srv remove *>> "$log" }} catch {{}}
& $venv $srv --startup auto install *>> "$log"
# Ensure registry points to correct module and PY path
reg add "HKLM\SYSTEM\CurrentControlSet\Services\{service_name}\PythonClass" /ve /t REG_SZ /d "windows_script_service.BorealisAgentService" /f | Out-Null
reg add "HKLM\SYSTEM\CurrentControlSet\Services\{service_name}\PythonPath" /ve /t REG_SZ /d "{paths['borealis_dir']}" /f | Out-Null
reg add "HKLM\SYSTEM\CurrentControlSet\Services\{service_name}\PythonHome" /ve /t REG_SZ /d "$pyhome" /f | Out-Null
sc.exe config {service_name} obj= LocalSystem start= auto | Out-File -FilePath "$log" -Append -Encoding UTF8
sc.exe start {service_name} | Out-File -FilePath "$log" -Append -Encoding UTF8
"[INFO] Completed service ensure." | Out-File -FilePath "$log" -Append -Encoding UTF8
}} catch {{
"[ERROR] $_" | Out-File -FilePath "$log" -Append -Encoding UTF8
exit 1
}}
"""
rc = run_elevated_powershell(paths, content, log_name)
return rc == 0
# Admin path: perform directly
try:
# Ensure pywin32 service hooks present in this venv
post_py = os.path.join(os.path.dirname(paths["venv_python"]), "pywin32_postinstall.py")
if os.path.isfile(post_py):
run([paths["venv_python"], post_py, "-install"]) # ignore rc
else:
run([paths["venv_python"], "-m", "pywin32_postinstall", "-install"]) # ignore rc
except Exception:
pass
try:
# Remove legacy service if it exists
run(["sc.exe", "stop", "BorealisScriptAgent"]) # ignore rc
run(["sc.exe", "delete", "BorealisScriptAgent"]) # ignore rc
run(["sc.exe", "stop", "BorealisScriptService"]) # ignore rc
run(["sc.exe", "delete", "BorealisScriptService"]) # ignore rc
if need_install:
run([paths["venv_python"], paths["service_script"], "remove"]) # ignore rc
r1 = run([paths["venv_python"], paths["service_script"], "--startup", "auto", "install"], capture=True)
log_write(paths, log_name, f"[INFO] install rc={r1.returncode} out={r1.stdout}\nerr={r1.stderr}")
# fix registry for module import and runtime resolution
# PythonHome: base interpreter home (from pyvenv.cfg 'home') so pythonservice can load pythonXY.dll
# PythonPath: add Borealis dir and venv site-packages including pywin32 dirs
try:
cfg = os.path.join(paths["venv_root"], "pyvenv.cfg")
base_home = None
if os.path.isfile(cfg):
with open(cfg, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
if line.strip().lower().startswith("home ="):
base_home = line.split("=",1)[1].strip()
break
if not base_home:
# fallback to parent of venv Scripts
base_home = os.path.dirname(os.path.dirname(paths["venv_python"]))
except Exception:
base_home = os.path.dirname(os.path.dirname(paths["venv_python"]))
site = os.path.join(paths["venv_root"], "Lib", "site-packages")
pypath = ";".join([
paths["borealis_dir"],
site,
os.path.join(site, "win32"),
os.path.join(site, "win32", "lib"),
os.path.join(site, "pywin32_system32"),
])
run(["reg", "add", fr"HKLM\\SYSTEM\\CurrentControlSet\\Services\\{service_name}\\PythonClass", "/ve", "/t", "REG_SZ", "/d", "windows_script_service.BorealisAgentService", "/f"]) # noqa
run(["reg", "add", fr"HKLM\\SYSTEM\\CurrentControlSet\\Services\\{service_name}\\PythonPath", "/ve", "/t", "REG_SZ", "/d", pypath, "/f"]) # noqa
run(["reg", "add", fr"HKLM\\SYSTEM\\CurrentControlSet\\Services\\{service_name}\\PythonHome", "/ve", "/t", "REG_SZ", "/d", base_home, "/f"]) # noqa
run(["sc.exe", "config", service_name, "obj=", "LocalSystem"]) # ensure LocalSystem
run(["sc.exe", "start", service_name])
# quick validate
qc = run(["sc.exe", "query", service_name], capture=True)
ok = (qc.returncode == 0)
log_write(paths, log_name, f"[INFO] ensure complete (ok={ok})")
return ok
except Exception as e:
log_write(paths, log_name, f"[ERROR] ensure (admin) failed: {e}")
return False
def ensure_user_logon_task(paths):
"""Ensure the per-user scheduled task that launches the agent in the user's session.
Name: "Borealis Agent"
Trigger: On logon (current user)
Action: <venv_python> -W ignore::SyntaxWarning <agent_script>
"""Ensure per-user scheduled task that launches the helper at logon.
Task name: "Borealis Agent"
"""
ensure_dirs(paths)
log_name = "Borealis_CollectorTask_Install.log"
task_name = "Borealis Agent"
# Use pythonw.exe to avoid opening a console window in the user's session
pyw = paths.get("venv_pythonw") or paths["venv_python"]
cmd = f"\"{pyw}\" -W ignore::SyntaxWarning \"{paths['agent_script']}\""
# If task exists, try remove (non-elevated first)
cmd = f'"{pyw}" -W ignore::SyntaxWarning "{paths["agent_script"]}"'
# Try create non-elevated
q = run(["schtasks.exe", "/Query", "/TN", task_name])
if q.returncode == 0:
d = run(["schtasks.exe", "/Delete", "/TN", task_name, "/F"], capture=True)
d = run(["schtasks.exe", "/Delete", "/TN", task_name, "/F"])
if d.returncode != 0:
# Attempt elevated deletion (task might have been created under admin previously)
ps = f"""
$ErrorActionPreference = 'SilentlyContinue'
try {{ schtasks.exe /Delete /TN "{task_name}" /F | Out-Null }} catch {{}}
"""
run_elevated_powershell(paths, ps, log_name)
c = run(["schtasks.exe", "/Create", "/SC", "ONLOGON", "/TN", task_name, "/TR", cmd, "/F", "/RL", "LIMITED"], capture=True)
log_write(paths, log_name, f"[INFO] create rc={c.returncode} out={c.stdout} err={c.stderr}")
if c.returncode != 0:
# Fallback: elevate and register task for the current user SID
user = os.environ.get("USERNAME", "")
domain = os.environ.get("USERDOMAIN", os.environ.get("COMPUTERNAME", ""))
py = (paths.get("venv_pythonw") or paths["venv_python"]).replace("\"", "\"\"")
ag = paths["agent_script"].replace("\"", "\"\"")
content = f"""
$ErrorActionPreference = 'Continue'
pass
c = run(["schtasks.exe", "/Create", "/SC", "ONLOGON", "/TN", task_name, "/TR", cmd, "/F", "/RL", "LIMITED"])
if c.returncode == 0:
run(["schtasks.exe", "/Run", "/TN", task_name])
return True
# Elevated fallback using ScheduledTasks cmdlets for better reliability
ps = f"""
$ErrorActionPreference='Continue'
$task = "{task_name}"
$py = "{py}"
$arg = "-W ignore::SyntaxWarning {ag}"
$cmd = '"' + $py + '" ' + $arg
$user = "{domain}\{user}"
try {{
# Resolve user SID
$sid = (New-Object System.Security.Principal.NTAccount($user)).Translate([System.Security.Principal.SecurityIdentifier]).Value
}} catch {{ $sid = $null }}
try {{
# Delete any existing task (any scope)
try {{ schtasks.exe /Delete /TN $task /F | Out-Null }} catch {{}}
$action = New-ScheduledTaskAction -Execute $py -Argument $arg
$trigger = New-ScheduledTaskTrigger -AtLogOn
$settings = New-ScheduledTaskSettingsSet -Hidden
if ($sid) {{
$principal = New-ScheduledTaskPrincipal -UserId $sid -LogonType Interactive -RunLevel Limited
Register-ScheduledTask -TaskName $task -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
}} else {{
# Fallback: bind by username (use Interactive to avoid password)
$principal = New-ScheduledTaskPrincipal -UserId "{domain}\{user}" -LogonType Interactive -RunLevel Limited
Register-ScheduledTask -TaskName $task -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
}}
}} catch {{
"[ERROR] Task register failed: $_" | Out-File -FilePath "{os.path.join(paths['logs_dir'], log_name)}" -Append -Encoding UTF8
exit 1
}}
$py = "{pyw}"
$arg = "-W ignore::SyntaxWarning {paths['agent_script']}"
try {{ Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue }} catch {{}}
$action = New-ScheduledTaskAction -Execute $py -Argument $arg
$trigger= New-ScheduledTaskTrigger -AtLogOn
$settings = New-ScheduledTaskSettingsSet -Hidden
Register-ScheduledTask -TaskName $task -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null
Start-ScheduledTask -TaskName $task | Out-Null
"""
rc = run_elevated_powershell(paths, content, log_name)
if rc != 0:
return False
else:
# Created via schtasks; set Hidden=true via elevated PowerShell (schtasks lacks a /HIDDEN switch)
ps_hide = f"""
$ErrorActionPreference = 'SilentlyContinue'
try {{
$settings = New-ScheduledTaskSettingsSet -Hidden
Set-ScheduledTask -TaskName "{task_name}" -Settings $settings | Out-Null
}} catch {{}}
"""
run_elevated_powershell(paths, ps_hide, log_name)
# Start immediately (if a session is active)
run(["schtasks.exe", "/Run", "/TN", task_name])
return True
rc = run_elevated_powershell(paths, ps, "Borealis_CollectorTask_Install.log")
return rc == 0
def ensure_all():
paths = project_paths()
ensure_dirs(paths)
ok_svc = ensure_script_service(paths)
# Service now launches per-session helper; scheduled task is not required.
return 0 if ok_svc else 1
ok = ensure_user_logon_task(paths)
return 0 if ok else 1
def main(argv):
# Simple CLI
if len(argv) <= 1:
print("Usage: agent_deployment.py [ensure-all|service-install|service-remove|task-ensure|task-remove]")
print("Usage: agent_deployment.py [ensure-all|task-ensure|task-remove]")
return 2
cmd = argv[1].lower()
paths = project_paths()
ensure_dirs(paths)
if cmd == "ensure-all":
return ensure_all()
if cmd == "service-install":
return 0 if ensure_script_service(paths) else 1
if cmd == "service-remove":
name = "BorealisAgent"
if not is_admin():
ps = f"try {{ sc.exe stop {name} }} catch {{}}; try {{ sc.exe delete {name} }} catch {{}}"
return run_elevated_powershell(paths, ps, "Borealis_ScriptService_Remove.log")
run(["sc.exe", "stop", name])
r = run(["sc.exe", "delete", name])
return r.returncode
if cmd == "task-ensure":
return 0 if ensure_user_logon_task(paths) else 1
if cmd == "task-remove":
tn = "Borealis Agent"
r = run(["schtasks.exe", "/Delete", "/TN", tn, "/F"])
return r.returncode
return run(["schtasks.exe", "/Delete", "/TN", "Borealis Agent", "/F"]).returncode
print(f"Unknown command: {cmd}")
return 2
if __name__ == "__main__":
sys.exit(main(sys.argv))