From 05ef18ebba123192c82a6b49bc08c278d98f8a94 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Sat, 4 Oct 2025 15:27:53 -0600 Subject: [PATCH 1/2] Improve silent update targeting and logging --- Data/Agent/Roles/role_ScriptExec_SYSTEM.py | 68 +++++++++++++++++++++- Data/Server/server.py | 27 +++++++-- 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/Data/Agent/Roles/role_ScriptExec_SYSTEM.py b/Data/Agent/Roles/role_ScriptExec_SYSTEM.py index 39d0240..e211406 100644 --- a/Data/Agent/Roles/role_ScriptExec_SYSTEM.py +++ b/Data/Agent/Roles/role_ScriptExec_SYSTEM.py @@ -1,6 +1,7 @@ import os import re import asyncio +import datetime import tempfile import uuid import time @@ -39,6 +40,42 @@ def _find_borealis_root() -> Optional[str]: return None +def _agent_logs_root() -> str: + root = _find_borealis_root() or _project_root() + return os.path.abspath(os.path.join(root, 'Logs', 'Agent')) + + +def _rotate_daily(path: str): + try: + if os.path.isfile(path): + mtime = os.path.getmtime(path) + dt = datetime.datetime.fromtimestamp(mtime) + today = datetime.datetime.now().date() + if dt.date() != today: + base, ext = os.path.splitext(path) + suffix = dt.strftime('%Y-%m-%d') + newp = f"{base}.{suffix}{ext}" + try: + os.replace(path, newp) + except Exception: + pass + except Exception: + pass + + +def _write_updater_log(message: str): + try: + log_dir = _agent_logs_root() + os.makedirs(log_dir, exist_ok=True) + path = os.path.join(log_dir, 'updater.log') + _rotate_daily(path) + ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + with open(path, 'a', encoding='utf-8') as fh: + fh.write(f'[{ts}] {message}\n') + except Exception: + pass + + def _launch_silent_update_task(): if os.name != 'nt': raise RuntimeError('Silent update is supported on Windows hosts only.') @@ -86,6 +123,7 @@ def _launch_silent_update_task(): if proc.returncode != 0: stderr = proc.stderr or proc.stdout or 'scheduled task registration failed' raise RuntimeError(stderr.strip()) + return task_name def _canonical_env_key(name: str) -> str: cleaned = re.sub(r"[^A-Za-z0-9_]", "_", (name or "").strip()) @@ -341,7 +379,25 @@ class Role: return loop = asyncio.get_running_loop() - await loop.run_in_executor(None, _launch_silent_update_task) + request_id = (details.get('request_id') or '').strip() + req_disp = request_id or 'n/a' + host_disp = hostname or 'unknown' + _write_updater_log(f"Silent update request received for host '{host_disp}' (request_id={req_disp})") + try: + task_name = await loop.run_in_executor(None, _launch_silent_update_task) + except Exception as exc: + _write_updater_log( + f"Silent update launch failed for host '{host_disp}' (request_id={req_disp}): {exc}" + ) + try: + details['_silent_update_error_logged'] = True + except Exception: + pass + raise + + _write_updater_log( + f"Silent update scheduled via task '{task_name}' for host '{host_disp}' (request_id={req_disp})" + ) try: await sio.emit( @@ -355,6 +411,16 @@ class Role: except Exception: pass except Exception as e: + if not isinstance(details, dict) or not details.get('_silent_update_error_logged'): + try: + request_id = (details.get('request_id') or '').strip() + req_disp = request_id or 'n/a' + host_disp = hostname or 'unknown' + _write_updater_log( + f"Silent update encountered error on host '{host_disp}' (request_id={req_disp}): {e}" + ) + except Exception: + pass try: await sio.emit( 'agent_silent_update_status', diff --git a/Data/Server/server.py b/Data/Server/server.py index 12ad8a1..8a05597 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -3150,15 +3150,34 @@ def agent_silent_update(): return jsonify({"error": "No valid hostnames provided"}), 400 request_id = uuid.uuid4().hex - results: List[Dict[str, str]] = [] + now_ts = int(time.time()) + results: List[Dict[str, Any]] = [] + + # Map hostname -> connected agent_id(s) so we can target specific rooms. + host_to_agents: Dict[str, List[str]] = {} + for agent_id, info in (registered_agents or {}).items(): + try: + hostname = str(info.get("hostname") or "").strip().lower() + except Exception: + hostname = "" + if not hostname: + continue + host_to_agents.setdefault(hostname, []).append(agent_id) + for host in hostnames: payload = { "target_hostname": host, "request_id": request_id, - "requested_at": int(time.time()), + "requested_at": now_ts, } - socketio.emit("agent_silent_update", payload) - results.append({"hostname": host, "status": "queued"}) + target_agents = host_to_agents.get(host.strip().lower(), []) + if target_agents: + for agent_id in target_agents: + socketio.emit("agent_silent_update", payload, room=agent_id) + else: + # Fallback broadcast for legacy agents or if hostname lookup failed. + socketio.emit("agent_silent_update", payload) + results.append({"hostname": host, "status": "queued", "agent_ids": target_agents}) _write_service_log( "server", From d94ce9be3a3d787cff723fcec947aa8978e379a9 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Sat, 4 Oct 2025 20:36:19 -0600 Subject: [PATCH 2/2] Remove automatic agent update workflow --- Data/Agent/Roles/role_ScriptExec_SYSTEM.py | 154 ------------------ Data/Server/WebUI/src/Devices/Device_List.jsx | 82 +--------- Data/Server/server.py | 82 ---------- 3 files changed, 1 insertion(+), 317 deletions(-) diff --git a/Data/Agent/Roles/role_ScriptExec_SYSTEM.py b/Data/Agent/Roles/role_ScriptExec_SYSTEM.py index e211406..c0dbacb 100644 --- a/Data/Agent/Roles/role_ScriptExec_SYSTEM.py +++ b/Data/Agent/Roles/role_ScriptExec_SYSTEM.py @@ -1,7 +1,6 @@ import os import re import asyncio -import datetime import tempfile import uuid import time @@ -40,91 +39,6 @@ def _find_borealis_root() -> Optional[str]: return None -def _agent_logs_root() -> str: - root = _find_borealis_root() or _project_root() - return os.path.abspath(os.path.join(root, 'Logs', 'Agent')) - - -def _rotate_daily(path: str): - try: - if os.path.isfile(path): - mtime = os.path.getmtime(path) - dt = datetime.datetime.fromtimestamp(mtime) - today = datetime.datetime.now().date() - if dt.date() != today: - base, ext = os.path.splitext(path) - suffix = dt.strftime('%Y-%m-%d') - newp = f"{base}.{suffix}{ext}" - try: - os.replace(path, newp) - except Exception: - pass - except Exception: - pass - - -def _write_updater_log(message: str): - try: - log_dir = _agent_logs_root() - os.makedirs(log_dir, exist_ok=True) - path = os.path.join(log_dir, 'updater.log') - _rotate_daily(path) - ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - with open(path, 'a', encoding='utf-8') as fh: - fh.write(f'[{ts}] {message}\n') - except Exception: - pass - - -def _launch_silent_update_task(): - if os.name != 'nt': - raise RuntimeError('Silent update is supported on Windows hosts only.') - - root = _find_borealis_root() - if not root: - raise RuntimeError('Unable to locate Borealis.ps1 on this agent.') - - ps_exe = os.path.expandvars(r"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe") - if not os.path.isfile(ps_exe): - ps_exe = 'powershell.exe' - - task_name = f"Borealis Agent - SilentUpdate - {uuid.uuid4().hex}" - task_literal = _ps_literal(task_name) - ps_literal = _ps_literal(ps_exe) - root_literal = _ps_literal(root) - args_literal = _ps_literal('-NoProfile -ExecutionPolicy Bypass -File .\\Borealis.ps1 -SilentUpdate') - - cleanup_script = ( - "Start-Job -ScriptBlock { Start-Sleep -Seconds 600; " - f"try {{ Unregister-ScheduledTask -TaskName {task_literal} -Confirm:$false -ErrorAction SilentlyContinue }} catch {{}} }} | Out-Null" - ) - - ps_script = "\n".join( - [ - "$ErrorActionPreference='Stop'", - f"$task = {task_literal}", - "try { Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue } catch {}", - f"$action = New-ScheduledTaskAction -Execute {ps_literal} -Argument {args_literal} -WorkingDirectory {root_literal}", - "$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -DeleteExpiredTaskAfter (New-TimeSpan -Minutes 30)", - "$principal= New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest", - "Register-ScheduledTask -TaskName $task -Action $action -Settings $settings -Principal $principal -Force | Out-Null", - "Start-ScheduledTask -TaskName $task | Out-Null", - cleanup_script, - ] - ) - - flags = 0x08000000 if os.name == 'nt' else 0 - proc = subprocess.run( - [ps_exe, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', ps_script], - capture_output=True, - text=True, - creationflags=flags, - ) - if proc.returncode != 0: - stderr = proc.stderr or proc.stdout or 'scheduled task registration failed' - raise RuntimeError(stderr.strip()) - return task_name - def _canonical_env_key(name: str) -> str: cleaned = re.sub(r"[^A-Za-z0-9_]", "_", (name or "").strip()) return cleaned.upper() @@ -366,74 +280,6 @@ class Role: def register_events(self): sio = self.ctx.sio - @sio.on('agent_silent_update') - async def _on_agent_silent_update(payload): - hostname = None - details = payload if isinstance(payload, dict) else {} - try: - import socket - - hostname = socket.gethostname() - target = (details.get('target_hostname') or '').strip().lower() - if target and target != hostname.lower(): - return - - loop = asyncio.get_running_loop() - request_id = (details.get('request_id') or '').strip() - req_disp = request_id or 'n/a' - host_disp = hostname or 'unknown' - _write_updater_log(f"Silent update request received for host '{host_disp}' (request_id={req_disp})") - try: - task_name = await loop.run_in_executor(None, _launch_silent_update_task) - except Exception as exc: - _write_updater_log( - f"Silent update launch failed for host '{host_disp}' (request_id={req_disp}): {exc}" - ) - try: - details['_silent_update_error_logged'] = True - except Exception: - pass - raise - - _write_updater_log( - f"Silent update scheduled via task '{task_name}' for host '{host_disp}' (request_id={req_disp})" - ) - - try: - await sio.emit( - 'agent_silent_update_status', - { - 'request_id': details.get('request_id') or '', - 'hostname': hostname, - 'status': 'started', - }, - ) - except Exception: - pass - except Exception as e: - if not isinstance(details, dict) or not details.get('_silent_update_error_logged'): - try: - request_id = (details.get('request_id') or '').strip() - req_disp = request_id or 'n/a' - host_disp = hostname or 'unknown' - _write_updater_log( - f"Silent update encountered error on host '{host_disp}' (request_id={req_disp}): {e}" - ) - except Exception: - pass - try: - await sio.emit( - 'agent_silent_update_status', - { - 'request_id': details.get('request_id') or '', - 'hostname': hostname or '', - 'status': 'error', - 'error': str(e), - }, - ) - except Exception: - pass - @sio.on('quick_job_run') async def _on_quick_job_run(payload): try: diff --git a/Data/Server/WebUI/src/Devices/Device_List.jsx b/Data/Server/WebUI/src/Devices/Device_List.jsx index a52f612..ba9d4a7 100644 --- a/Data/Server/WebUI/src/Devices/Device_List.jsx +++ b/Data/Server/WebUI/src/Devices/Device_List.jsx @@ -18,9 +18,7 @@ import { MenuItem, Popover, TextField, - Tooltip, - Snackbar, - Alert + Tooltip } from "@mui/material"; import MoreVertIcon from "@mui/icons-material/MoreVert"; import FilterListIcon from "@mui/icons-material/FilterList"; @@ -63,8 +61,6 @@ export default function DeviceList({ onSelectDevice }) { // Track selection by agent id to avoid duplicate hostname collisions const [selectedIds, setSelectedIds] = useState(() => new Set()); const [quickJobOpen, setQuickJobOpen] = useState(false); - const [updateLoading, setUpdateLoading] = useState(false); - const [updateStatus, setUpdateStatus] = useState({ open: false, severity: "success", message: "" }); // Saved custom views (from server) const [views, setViews] = useState([]); // [{id, name, columns:[id], filters:{}}] @@ -426,49 +422,6 @@ export default function DeviceList({ onSelectDevice }) { }); }; - const triggerSilentUpdate = useCallback(async () => { - const hostnames = Array.from( - new Set( - rows - .filter((r) => selectedIds.has(r.id)) - .map((r) => r.hostname) - .filter(Boolean) - ) - ); - if (!hostnames.length) return; - - setUpdateLoading(true); - try { - const resp = await fetch('/api/agents/silent_update', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ hostnames }), - }); - let payload = null; - try { - payload = await resp.json(); - } catch (err) { - payload = null; - } - if (!resp.ok || (payload && payload.error)) { - const msg = payload && payload.error ? payload.error : `HTTP ${resp.status}`; - throw new Error(msg); - } - const count = Array.isArray(payload?.results) ? payload.results.length : hostnames.length; - const requestId = typeof payload?.request_id === 'string' && payload.request_id ? payload.request_id : ''; - setUpdateStatus({ - open: true, - severity: 'success', - message: `Silent update triggered for ${count} device${count === 1 ? '' : 's'}${requestId ? ` (request ${requestId})` : ''}.`, - }); - } catch (err) { - const message = err instanceof Error ? err.message : 'Unknown error'; - setUpdateStatus({ open: true, severity: 'error', message: `Failed to trigger silent update: ${message}` }); - } finally { - setUpdateLoading(false); - } - }, [rows, selectedIds]); - // Column drag handlers const onHeaderDragStart = (colId) => (e) => { dragColId.current = colId; @@ -620,22 +573,6 @@ export default function DeviceList({ onSelectDevice }) { {/* Second row: Quick Job button aligned under header title */} -