Refactored & Modularized Agent Roles

This commit is contained in:
2025-09-18 14:29:29 -06:00
parent 30875b780b
commit 8a3f2ecd77
22 changed files with 1347 additions and 2190 deletions

View File

@@ -0,0 +1,2 @@
# Roles package for Borealis Agent

View File

@@ -0,0 +1,280 @@
import os
import json
import time
import socket
import platform
import subprocess
import shutil
import string
import asyncio
try:
import psutil # type: ignore
except Exception:
psutil = None
try:
import aiohttp
except Exception:
aiohttp = None
ROLE_NAME = 'device_inventory'
ROLE_CONTEXTS = ['interactive']
IS_WINDOWS = os.name == 'nt'
def detect_agent_os():
try:
plat = platform.system().lower()
if plat.startswith('win'):
try:
import winreg # type: ignore
reg_path = r"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"
access = getattr(winreg, 'KEY_READ', 0x20019)
try:
access |= winreg.KEY_WOW64_64KEY
except Exception:
pass
try:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path, 0, access)
except OSError:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path, 0, winreg.KEY_READ)
def _get(name, default=None):
try:
return winreg.QueryValueEx(key, name)[0]
except Exception:
return default
product_name = _get("ProductName", "")
display_version = _get("DisplayVersion", "")
release_id = _get("ReleaseId", "")
build_number = _get("CurrentBuildNumber", "") or _get("CurrentBuild", "")
ubr = _get("UBR", None)
try:
build_int = int(str(build_number).split(".")[0]) if build_number else 0
except Exception:
build_int = 0
if build_int >= 22000:
major_label = "11"
elif build_int >= 10240:
major_label = "10"
else:
major_label = platform.release()
os_name = f"Windows {major_label}"
version_label = display_version or release_id or ""
if isinstance(ubr, int):
build_str = f"{build_number}.{ubr}" if build_number else str(ubr)
else:
try:
build_str = f"{build_number}.{int(ubr)}" if build_number and ubr else (build_number or "")
except Exception:
build_str = build_number or ""
parts = [os_name]
if product_name and product_name.lower().startswith('windows '):
try:
tail = product_name.split(' ', 2)[2]
if tail:
parts.append(tail)
except Exception:
pass
if version_label:
parts.append(version_label)
if build_str:
parts.append(f"Build {build_str}")
return " ".join([p for p in parts if p]).strip() or platform.platform()
except Exception:
return platform.platform()
elif plat == 'darwin':
try:
out = subprocess.run(["sw_vers", "-productVersion"], capture_output=True, text=True, timeout=3)
ver = (out.stdout or '').strip()
return f"macOS {ver}" if ver else "macOS"
except Exception:
return "macOS"
else:
try:
import distro # type: ignore
name = distro.name(pretty=True) or distro.id()
ver = distro.version()
return f"{name} {ver}".strip()
except Exception:
return platform.platform()
except Exception:
return "Unknown"
def collect_summary(CONFIG):
try:
hostname = socket.gethostname()
return {
'hostname': hostname,
'os': CONFIG.data.get('agent_operating_system', detect_agent_os()),
'username': os.environ.get('USERNAME') or os.environ.get('USER') or '',
'domain': os.environ.get('USERDOMAIN') or '',
'uptime_sec': int(time.time() - psutil.boot_time()) if psutil else None,
}
except Exception:
return {'hostname': socket.gethostname()}
def collect_software():
# Placeholder: fuller inventory can be added later
return []
def collect_memory():
entries = []
try:
plat = platform.system().lower()
if plat == 'windows':
try:
ps_cmd = (
"Get-CimInstance Win32_PhysicalMemory | "
"Select-Object BankLabel,Speed,SerialNumber,Capacity | ConvertTo-Json"
)
out = subprocess.run(["powershell", "-NoProfile", "-Command", ps_cmd], capture_output=True, text=True, timeout=60)
data = json.loads(out.stdout or "[]")
if isinstance(data, dict):
data = [data]
for stick in data:
entries.append({
'slot': stick.get('BankLabel', 'unknown'),
'speed': str(stick.get('Speed', 'unknown')),
'serial': stick.get('SerialNumber', 'unknown'),
'capacity': stick.get('Capacity', 'unknown'),
})
except Exception:
pass
except Exception:
pass
if not entries and psutil:
try:
vm = psutil.virtual_memory()
entries.append({'slot': 'physical', 'speed': 'unknown', 'serial': 'unknown', 'capacity': vm.total})
except Exception:
pass
return entries
def collect_storage():
disks = []
try:
if psutil:
for part in psutil.disk_partitions():
try:
usage = psutil.disk_usage(part.mountpoint)
except Exception:
continue
disks.append({
'drive': part.device,
'disk_type': 'Removable' if isinstance(part.opts, str) and 'removable' in part.opts.lower() else 'Fixed Disk',
'usage': usage.percent,
'total': usage.total,
'free': usage.free,
'used': usage.used,
})
else:
# Fallback basic detection on Windows via drive letters
if IS_WINDOWS:
for letter in string.ascii_uppercase:
drive = f"{letter}:\\"
if os.path.exists(drive):
try:
usage = shutil.disk_usage(drive)
except Exception:
continue
disks.append({
'drive': drive,
'disk_type': 'Fixed Disk',
'usage': (usage.used / usage.total * 100) if usage.total else 0,
'total': usage.total,
'free': usage.free,
'used': usage.used,
})
except Exception:
pass
return disks
def collect_network():
adapters = []
try:
if IS_WINDOWS:
try:
ps_cmd = (
"Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | "
"ForEach-Object { $_ | Select-Object -Property InterfaceAlias, MacAddress } | ConvertTo-Json"
)
out = subprocess.run(["powershell", "-NoProfile", "-Command", ps_cmd], capture_output=True, text=True, timeout=60)
data = json.loads(out.stdout or "[]")
if isinstance(data, dict):
data = [data]
for a in data:
adapters.append({'adapter': a.get('InterfaceAlias', 'unknown'), 'ips': [], 'mac': a.get('MacAddress', 'unknown')})
except Exception:
pass
else:
out = subprocess.run(["ip", "-o", "-4", "addr", "show"], capture_output=True, text=True, timeout=60)
for line in out.stdout.splitlines():
parts = line.split()
if len(parts) >= 4:
name = parts[1]
ip = parts[3].split("/")[0]
adapters.append({'adapter': name, 'ips': [ip], 'mac': 'unknown'})
except Exception:
pass
return adapters
class Role:
def __init__(self, ctx):
self.ctx = ctx
try:
# Set OS string once
self.ctx.config.data['agent_operating_system'] = detect_agent_os()
self.ctx.config._write()
except Exception:
pass
# Start periodic reporter
try:
self.task = self.ctx.loop.create_task(self._report_loop())
except Exception:
self.task = None
def stop_all(self):
try:
if self.task:
self.task.cancel()
except Exception:
pass
async def _report_loop(self):
while True:
try:
details = {
'summary': collect_summary(self.ctx.config),
'software': collect_software(),
'memory': collect_memory(),
'storage': collect_storage(),
'network': collect_network(),
}
url = (self.ctx.config.data.get('borealis_server_url', 'http://localhost:5000') or '').rstrip('/') + '/api/agent/details'
payload = {
'agent_id': self.ctx.agent_id,
'hostname': details.get('summary', {}).get('hostname', socket.gethostname()),
'details': details,
}
if aiohttp is not None:
async with aiohttp.ClientSession() as session:
await session.post(url, json=payload, timeout=10)
except Exception:
pass
await asyncio.sleep(300)

View File

@@ -0,0 +1,122 @@
import os
import asyncio
import importlib.util
ROLE_NAME = 'macro'
ROLE_CONTEXTS = ['interactive']
def _load_macro_engines():
try:
base = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
path = os.path.join(base, 'Python_API_Endpoints', 'macro_engines.py')
spec = importlib.util.spec_from_file_location('macro_engines', path)
mod = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(mod)
return mod
except Exception:
class _Dummy:
def list_windows(self):
return []
def send_keypress_to_window(self, handle, key):
return False, 'unavailable'
def type_text_to_window(self, handle, text):
return False, 'unavailable'
return _Dummy()
macro_engines = _load_macro_engines()
class Role:
def __init__(self, ctx):
self.ctx = ctx
self.tasks = {}
def stop_all(self):
for t in list(self.tasks.values()):
try:
t.cancel()
except Exception:
pass
self.tasks.clear()
def on_config(self, roles_cfg):
macro_roles = [r for r in roles_cfg if (r.get('role') == 'macro')]
new_ids = {r.get('node_id') for r in macro_roles if r.get('node_id')}
old_ids = set(self.tasks.keys())
removed = old_ids - new_ids
for rid in removed:
t = self.tasks.pop(rid, None)
if t:
try:
t.cancel()
except Exception:
pass
for rcfg in macro_roles:
nid = rcfg.get('node_id')
if nid and nid not in self.tasks:
self.tasks[nid] = asyncio.create_task(self._macro_task(rcfg))
async def _macro_task(self, cfg):
nid = cfg.get('node_id')
last_trigger_value = 0
has_run_once = False
while True:
window_handle = cfg.get('window_handle')
macro_type = cfg.get('macro_type', 'keypress')
operation_mode = cfg.get('operation_mode', 'Continuous')
key = cfg.get('key')
text = cfg.get('text')
interval_ms = int(cfg.get('interval_ms', 1000))
randomize = cfg.get('randomize_interval', False)
random_min = int(cfg.get('random_min', 750))
random_max = int(cfg.get('random_max', 950))
active = cfg.get('active', True)
trigger = int(cfg.get('trigger', 0))
async def emit_macro_status(success, message=""):
try:
await self.ctx.sio.emit('macro_status', {
'agent_id': self.ctx.agent_id,
'node_id': nid,
'success': success,
'message': message,
})
except Exception:
pass
if not (active is True or str(active).lower() == 'true'):
await asyncio.sleep(0.2)
continue
try:
send_macro = False
if operation_mode == 'Run Once':
if not has_run_once:
send_macro = True
has_run_once = True
elif operation_mode == 'Continuous':
send_macro = True
elif operation_mode == 'Trigger-Continuous':
send_macro = (trigger == 1)
elif operation_mode == 'Trigger-Once':
if trigger == 1 and last_trigger_value != 1:
send_macro = True
else:
send_macro = False
if send_macro:
ok = False
if macro_type == 'keypress' and key:
ok = bool(macro_engines.send_keypress_to_window(window_handle, key))
elif macro_type == 'text' and text:
ok = bool(macro_engines.type_text_to_window(window_handle, text))
await emit_macro_status(ok, 'sent' if ok else 'failed')
last_trigger_value = trigger
except Exception as e:
await emit_macro_status(False, str(e))
# interval wait
await asyncio.sleep(max(0.05, (interval_ms or 1000) / 1000.0))

View File

@@ -0,0 +1,322 @@
import os
import asyncio
import concurrent.futures
from functools import partial
from io import BytesIO
import base64
import traceback
from PyQt5 import QtCore, QtGui, QtWidgets
from PIL import ImageGrab
import importlib.util
ROLE_NAME = 'screenshot'
ROLE_CONTEXTS = ['interactive']
# Load macro engines from the local Python_API_Endpoints directory for window listings
def _load_macro_engines():
try:
base = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
path = os.path.join(base, 'Python_API_Endpoints', 'macro_engines.py')
spec = importlib.util.spec_from_file_location('macro_engines', path)
mod = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(mod)
return mod
except Exception:
class _Dummy:
def list_windows(self):
return []
return _Dummy()
macro_engines = _load_macro_engines()
overlay_green_thickness = 4
overlay_gray_thickness = 2
handle_size = overlay_green_thickness * 2
extra_top_padding = overlay_green_thickness * 2 + 4
overlay_widgets = {}
class ScreenshotRegion(QtWidgets.QWidget):
def __init__(self, ctx, node_id, x=100, y=100, w=300, h=200, alias=None):
super().__init__()
self.ctx = ctx
self.node_id = node_id
self.alias = alias
self.setGeometry(
x - handle_size,
y - handle_size - extra_top_padding,
w + handle_size * 2,
h + handle_size * 2 + extra_top_padding,
)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.resize_dir = None
self.drag_offset = None
self._start_geom = None
self._start_pos = None
self.setMouseTracking(True)
def paintEvent(self, event):
p = QtGui.QPainter(self)
p.setRenderHint(QtGui.QPainter.Antialiasing)
w = self.width()
h = self.height()
p.setPen(QtGui.QPen(QtGui.QColor(130, 130, 130), overlay_gray_thickness))
p.drawRect(handle_size, handle_size + extra_top_padding, w - handle_size * 2, h - handle_size * 2 - extra_top_padding)
p.setPen(QtCore.Qt.NoPen)
p.setBrush(QtGui.QBrush(QtGui.QColor(0, 191, 255)))
edge = overlay_green_thickness * 3
p.drawRect(0, extra_top_padding, edge, overlay_green_thickness)
p.drawRect(0, extra_top_padding, overlay_green_thickness, edge)
p.drawRect(w - edge, extra_top_padding, edge, overlay_green_thickness)
p.drawRect(w - overlay_green_thickness, extra_top_padding, overlay_green_thickness, edge)
p.drawRect(0, h - overlay_green_thickness, edge, overlay_green_thickness)
p.drawRect(0, h - edge, overlay_green_thickness, edge)
p.drawRect(w - edge, h - overlay_green_thickness, edge, overlay_green_thickness)
p.drawRect(w - overlay_green_thickness, h - edge, overlay_green_thickness, edge)
long = overlay_green_thickness * 6
p.drawRect((w - long) // 2, extra_top_padding, long, overlay_green_thickness)
p.drawRect((w - long) // 2, h - overlay_green_thickness, long, overlay_green_thickness)
p.drawRect(0, (h + extra_top_padding - long) // 2, overlay_green_thickness, long)
p.drawRect(w - overlay_green_thickness, (h + extra_top_padding - long) // 2, overlay_green_thickness, long)
bar_width = overlay_green_thickness * 6
bar_height = overlay_green_thickness
bar_x = (w - bar_width) // 2
bar_y = 6
p.setBrush(QtGui.QColor(0, 191, 255))
p.drawRect(bar_x, bar_y - bar_height - 10, bar_width, bar_height * 4)
def get_geometry(self):
g = self.geometry()
return (
g.x() + handle_size,
g.y() + handle_size + extra_top_padding,
g.width() - handle_size * 2,
g.height() - handle_size * 2 - extra_top_padding,
)
def mousePressEvent(self, e):
if e.button() == QtCore.Qt.LeftButton:
pos = e.pos()
bar_width = overlay_green_thickness * 6
bar_height = overlay_green_thickness
bar_x = (self.width() - bar_width) // 2
bar_y = 2
bar_rect = QtCore.QRect(bar_x, bar_y, bar_width, bar_height)
if bar_rect.contains(pos):
self.drag_offset = e.globalPos() - self.frameGeometry().topLeft()
return
m = handle_size
dirs = []
if pos.x() <= m:
dirs.append('left')
if pos.x() >= self.width() - m:
dirs.append('right')
if pos.y() <= m + extra_top_padding:
dirs.append('top')
if pos.y() >= self.height() - m:
dirs.append('bottom')
if dirs:
self.resize_dir = '_'.join(dirs)
self._start_geom = self.geometry()
self._start_pos = e.globalPos()
else:
self.drag_offset = e.globalPos() - self.frameGeometry().topLeft()
def mouseMoveEvent(self, e):
if self.resize_dir and self._start_geom and self._start_pos:
dx = e.globalX() - self._start_pos.x()
dy = e.globalY() - self._start_pos.y()
geom = QtCore.QRect(self._start_geom)
if 'left' in self.resize_dir:
new_x = geom.x() + dx
new_w = geom.width() - dx
geom.setX(new_x)
geom.setWidth(new_w)
if 'right' in self.resize_dir:
geom.setWidth(self._start_geom.width() + dx)
if 'top' in self.resize_dir:
new_y = geom.y() + dy
new_h = geom.height() - dy
geom.setY(new_y)
geom.setHeight(new_h)
if 'bottom' in self.resize_dir:
geom.setHeight(self._start_geom.height() + dy)
self.setGeometry(geom)
elif self.drag_offset and e.buttons() & QtCore.Qt.LeftButton:
self.move(e.globalPos() - self.drag_offset)
def mouseReleaseEvent(self, e):
self.drag_offset = None
self.resize_dir = None
self._start_geom = None
self._start_pos = None
x, y, w, h = self.get_geometry()
self.ctx.config.data['regions'][self.node_id] = {'x': x, 'y': y, 'w': w, 'h': h}
try:
self.ctx.config._write()
except Exception:
pass
asyncio.create_task(self.ctx.sio.emit('agent_screenshot_task', {
'agent_id': self.ctx.agent_id,
'node_id': self.node_id,
'image_base64': '',
'x': x, 'y': y, 'w': w, 'h': h
}))
class Role:
def __init__(self, ctx):
self.ctx = ctx
self.tasks = {}
def register_events(self):
sio = self.ctx.sio
@sio.on('list_agent_windows')
async def _handle_list_windows(payload):
try:
windows = macro_engines.list_windows()
except Exception:
windows = []
await sio.emit('agent_window_list', {
'agent_id': self.ctx.agent_id,
'windows': windows,
})
def _close_overlay(self, node_id: str):
w = overlay_widgets.pop(node_id, None)
if w:
try:
w.close()
except Exception:
pass
def stop_all(self):
for t in list(self.tasks.values()):
try:
t.cancel()
except Exception:
pass
self.tasks.clear()
# Close all widgets
for nid in list(overlay_widgets.keys()):
self._close_overlay(nid)
def on_config(self, roles_cfg):
# Filter only screenshot roles
screenshot_roles = [r for r in roles_cfg if (r.get('role') == 'screenshot')]
# Optional: forward interval to SYSTEM helper via hook
try:
if screenshot_roles and 'send_service_control' in self.ctx.hooks:
interval_ms = int(screenshot_roles[0].get('interval', 1000))
try:
self.ctx.hooks['send_service_control']({'type': 'screenshot_config', 'interval_ms': interval_ms})
except Exception:
pass
except Exception:
pass
# Cancel tasks that are no longer present
new_ids = {r.get('node_id') for r in screenshot_roles if r.get('node_id')}
old_ids = set(self.tasks.keys())
removed = old_ids - new_ids
for rid in removed:
t = self.tasks.pop(rid, None)
if t:
try:
t.cancel()
except Exception:
pass
# Remove stored region and overlay
self.ctx.config.data.get('regions', {}).pop(rid, None)
try:
self._close_overlay(rid)
except Exception:
pass
if removed:
try:
self.ctx.config._write()
except Exception:
pass
# Start tasks for all screenshot roles in config
for rcfg in screenshot_roles:
nid = rcfg.get('node_id')
if not nid:
continue
if nid in self.tasks:
continue
task = asyncio.create_task(self._screenshot_task(rcfg))
self.tasks[nid] = task
async def _screenshot_task(self, cfg):
nid = cfg.get('node_id')
alias = cfg.get('alias', '')
reg = self.ctx.config.data.setdefault('regions', {})
r = reg.get(nid)
if r:
region = (r['x'], r['y'], r['w'], r['h'])
else:
region = (
cfg.get('x', 100),
cfg.get('y', 100),
cfg.get('w', 300),
cfg.get('h', 200),
)
reg[nid] = {'x': region[0], 'y': region[1], 'w': region[2], 'h': region[3]}
try:
self.ctx.config._write()
except Exception:
pass
if nid not in overlay_widgets:
widget = ScreenshotRegion(self.ctx, nid, *region, alias=alias)
overlay_widgets[nid] = widget
widget.show()
await self.ctx.sio.emit('agent_screenshot_task', {
'agent_id': self.ctx.agent_id,
'node_id': nid,
'image_base64': '',
'x': region[0], 'y': region[1], 'w': region[2], 'h': region[3]
})
interval = cfg.get('interval', 1000) / 1000.0
loop = asyncio.get_event_loop()
executor = concurrent.futures.ThreadPoolExecutor(max_workers=self.ctx.config.data.get('max_task_workers', 8))
try:
while True:
x, y, w, h = overlay_widgets[nid].get_geometry()
grab = partial(ImageGrab.grab, bbox=(x, y, x + w, y + h))
img = await loop.run_in_executor(executor, grab)
buf = BytesIO(); img.save(buf, format='PNG')
encoded = base64.b64encode(buf.getvalue()).decode('utf-8')
await self.ctx.sio.emit('agent_screenshot_task', {
'agent_id': self.ctx.agent_id,
'node_id': nid,
'image_base64': encoded,
'x': x, 'y': y, 'w': w, 'h': h
})
await asyncio.sleep(interval)
except asyncio.CancelledError:
pass
except Exception:
traceback.print_exc()

View File

@@ -0,0 +1,217 @@
import os
import sys
import asyncio
import tempfile
import uuid
from PyQt5 import QtWidgets, QtGui
ROLE_NAME = 'script_exec_currentuser'
ROLE_CONTEXTS = ['interactive']
IS_WINDOWS = os.name == 'nt'
def _write_temp_script(content: str, suffix: str):
temp_dir = os.path.join(tempfile.gettempdir(), "Borealis", "quick_jobs")
os.makedirs(temp_dir, exist_ok=True)
fd, path = tempfile.mkstemp(prefix="bj_", suffix=suffix, dir=temp_dir, text=True)
with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as fh:
fh.write(content or "")
return path
async def _run_powershell_local(path: str):
if IS_WINDOWS:
ps = os.path.expandvars(r"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
if not os.path.isfile(ps):
ps = "powershell.exe"
else:
ps = "pwsh"
try:
proc = await asyncio.create_subprocess_exec(
ps,
"-ExecutionPolicy", "Bypass" if IS_WINDOWS else "Bypass",
"-NoProfile",
"-File", path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
creationflags=(0x08000000 if IS_WINDOWS else 0)
)
out_b, err_b = await proc.communicate()
return proc.returncode, (out_b or b"").decode(errors='replace'), (err_b or b"").decode(errors='replace')
except Exception as e:
return -1, "", str(e)
async def _run_powershell_via_user_task(content: str):
if not IS_WINDOWS:
return -999, '', 'Windows only'
ps = os.path.expandvars(r"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
if not os.path.isfile(ps):
ps = 'powershell.exe'
path = None
out_path = None
import tempfile as _tf
try:
temp_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'Temp'))
os.makedirs(temp_dir, exist_ok=True)
fd, path = _tf.mkstemp(prefix='usr_task_', suffix='.ps1', dir=temp_dir, text=True)
with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as f:
f.write(content or '')
out_path = os.path.join(temp_dir, f'out_{uuid.uuid4().hex}.txt')
name = f"Borealis Agent - Task - {uuid.uuid4().hex} @ CurrentUser"
task_ps = f"""
$ErrorActionPreference='Continue'
$task = "{name}"
$ps = "{ps}"
$scr = "{path}"
$out = "{out_path}"
try {{ Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue }} catch {{}}
$action = New-ScheduledTaskAction -Execute $ps -Argument ('-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $scr + '" *> "' + $out + '"')
$settings = New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter (New-TimeSpan -Minutes 5) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$principal= New-ScheduledTaskPrincipal -UserId ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) -LogonType Interactive -RunLevel Limited
Register-ScheduledTask -TaskName $task -Action $action -Settings $settings -Principal $principal -Force | Out-Null
Start-ScheduledTask -TaskName $task | Out-Null
Start-Sleep -Seconds 2
Get-ScheduledTask -TaskName $task | Out-Null
"""
proc = await asyncio.create_subprocess_exec(ps, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', task_ps,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
out_b, err_b = await proc.communicate()
if proc.returncode != 0:
return -999, '', (err_b or out_b or b'').decode(errors='replace')
# Wait a short time for output file; best-effort
import time as _t
deadline = _t.time() + 30
out_data = ''
while _t.time() < deadline:
try:
if os.path.isfile(out_path) and os.path.getsize(out_path) > 0:
with open(out_path, 'r', encoding='utf-8', errors='replace') as f:
out_data = f.read()
break
except Exception:
pass
await asyncio.sleep(1)
# Cleanup best-effort
try:
await asyncio.create_subprocess_exec('powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', f"try {{ Unregister-ScheduledTask -TaskName '{name}' -Confirm:$false }} catch {{}}")
except Exception:
pass
try:
if path and os.path.isfile(path):
os.remove(path)
except Exception:
pass
try:
if out_path and os.path.isfile(out_path):
os.remove(out_path)
except Exception:
pass
return 0, out_data or '', ''
except Exception as e:
return -999, '', str(e)
class Role:
def __init__(self, ctx):
self.ctx = ctx
# Setup tray icon in interactive session
try:
self._setup_tray()
except Exception:
pass
def register_events(self):
sio = self.ctx.sio
@sio.on('quick_job_run')
async def _on_quick_job_run(payload):
try:
import socket
hostname = socket.gethostname()
target = (payload.get('target_hostname') or '').strip().lower()
if not target or target != hostname.lower():
return
job_id = payload.get('job_id')
script_type = (payload.get('script_type') or '').lower()
run_mode = (payload.get('run_mode') or 'current_user').lower()
content = payload.get('script_content') or ''
if run_mode == 'system':
return
if script_type != 'powershell':
await sio.emit('quick_job_result', { 'job_id': job_id, 'status': 'Failed', 'stdout': '', 'stderr': f"Unsupported type: {script_type}" })
return
if run_mode == 'admin':
rc, out, err = -1, '', 'Admin credentialed runs are disabled; use SYSTEM or Current User.'
else:
rc, out, err = await _run_powershell_via_user_task(content)
if rc == -999:
path = _write_temp_script(content, '.ps1')
rc, out, err = await _run_powershell_local(path)
status = 'Success' if rc == 0 else 'Failed'
await sio.emit('quick_job_result', {
'job_id': job_id,
'status': status,
'stdout': out,
'stderr': err,
})
except Exception as e:
try:
await sio.emit('quick_job_result', {
'job_id': payload.get('job_id') if isinstance(payload, dict) else None,
'status': 'Failed',
'stdout': '',
'stderr': str(e),
})
except Exception:
pass
def _setup_tray(self):
app = QtWidgets.QApplication.instance()
if app is None:
return
icon = None
try:
icon_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'Borealis.ico'))
if os.path.isfile(icon_path):
icon = QtGui.QIcon(icon_path)
except Exception:
pass
if icon is None:
icon = app.style().standardIcon(QtWidgets.QStyle.SP_ComputerIcon)
self.tray = QtWidgets.QSystemTrayIcon(icon)
self.tray.setToolTip('Borealis Agent')
menu = QtWidgets.QMenu()
act_restart = menu.addAction('Restart Agent')
act_quit = menu.addAction('Quit Agent')
act_restart.triggered.connect(self._restart_agent)
act_quit.triggered.connect(self._quit_agent)
self.tray.setContextMenu(menu)
self.tray.show()
def _restart_agent(self):
try:
# __file__ => Agent/Borealis/Roles/...
borealis_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
venv_root = os.path.abspath(os.path.join(borealis_dir, os.pardir))
venv_scripts = os.path.join(venv_root, 'Scripts')
pyw = os.path.join(venv_scripts, 'pythonw.exe')
exe = pyw if os.path.isfile(pyw) else sys.executable
agent_script = os.path.join(borealis_dir, 'agent.py')
import subprocess
subprocess.Popen([exe, '-W', 'ignore::SyntaxWarning', agent_script], cwd=borealis_dir)
except Exception:
pass
try:
QtWidgets.QApplication.instance().quit()
except Exception:
os._exit(0)
def _quit_agent(self):
try:
QtWidgets.QApplication.instance().quit()
except Exception:
os._exit(0)

View File

@@ -0,0 +1,153 @@
import os
import asyncio
import tempfile
import uuid
import time
import subprocess
ROLE_NAME = 'script_exec_system'
ROLE_CONTEXTS = ['system']
def _project_root():
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
def _run_powershell_script_content(content: str):
temp_dir = os.path.join(_project_root(), "Temp")
os.makedirs(temp_dir, exist_ok=True)
fd, path = tempfile.mkstemp(prefix="sj_", suffix=".ps1", dir=temp_dir, text=True)
with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as fh:
fh.write(content or "")
ps = os.path.expandvars(r"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
if not os.path.isfile(ps):
ps = "powershell.exe"
try:
flags = 0x08000000 if os.name == 'nt' else 0
proc = subprocess.run(
[ps, "-ExecutionPolicy", "Bypass", "-NoProfile", "-File", path],
capture_output=True,
text=True,
timeout=60*60,
creationflags=flags,
)
return proc.returncode, proc.stdout or "", proc.stderr or ""
except Exception as e:
return -1, "", str(e)
finally:
try:
if os.path.isfile(path):
os.remove(path)
except Exception:
pass
def _run_powershell_via_system_task(content: str):
ps_exe = os.path.expandvars(r"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe")
if not os.path.isfile(ps_exe):
ps_exe = 'powershell.exe'
try:
os.makedirs(os.path.join(_project_root(), 'Temp'), exist_ok=True)
script_fd, script_path = tempfile.mkstemp(prefix='sys_task_', suffix='.ps1', dir=os.path.join(_project_root(), 'Temp'), text=True)
with os.fdopen(script_fd, 'w', encoding='utf-8', newline='\n') as f:
f.write(content or '')
out_path = os.path.join(_project_root(), 'Temp', f'out_{uuid.uuid4().hex}.txt')
task_name = f"Borealis Agent - Task - {uuid.uuid4().hex} @ SYSTEM"
task_ps = f"""
$ErrorActionPreference='Continue'
$task = "{task_name}"
$ps = "{ps_exe}"
$scr = "{script_path}"
$out = "{out_path}"
try {{ Unregister-ScheduledTask -TaskName $task -Confirm:$false -ErrorAction SilentlyContinue }} catch {{}}
$action = New-ScheduledTaskAction -Execute $ps -Argument ('-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $scr + '" *> "' + $out + '"')
$settings = New-ScheduledTaskSettingsSet -DeleteExpiredTaskAfter (New-TimeSpan -Minutes 5) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$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
Start-Sleep -Seconds 2
Get-ScheduledTask -TaskName $task | Out-Null
"""
proc = subprocess.run([ps_exe, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', task_ps], capture_output=True, text=True)
if proc.returncode != 0:
return -999, '', (proc.stderr or proc.stdout or 'scheduled task creation failed')
deadline = time.time() + 60
out_data = ''
while time.time() < deadline:
try:
if os.path.isfile(out_path) and os.path.getsize(out_path) > 0:
with open(out_path, 'r', encoding='utf-8', errors='replace') as f:
out_data = f.read()
break
except Exception:
pass
time.sleep(1)
cleanup_ps = f"try {{ Unregister-ScheduledTask -TaskName '{task_name}' -Confirm:$false }} catch {{}}"
subprocess.run([ps_exe, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', cleanup_ps], capture_output=True, text=True)
try:
if os.path.isfile(script_path):
os.remove(script_path)
except Exception:
pass
try:
if os.path.isfile(out_path):
os.remove(out_path)
except Exception:
pass
return 0, out_data or '', ''
except Exception as e:
return -999, '', str(e)
class Role:
def __init__(self, ctx):
self.ctx = ctx
def register_events(self):
sio = self.ctx.sio
@sio.on('quick_job_run')
async def _on_quick_job_run(payload):
try:
import socket
hostname = socket.gethostname()
target = (payload.get('target_hostname') or '').strip().lower()
if target and target != hostname.lower():
return
run_mode = (payload.get('run_mode') or 'current_user').lower()
if run_mode != 'system':
return
job_id = payload.get('job_id')
script_type = (payload.get('script_type') or '').lower()
content = payload.get('script_content') or ''
if script_type != 'powershell':
await sio.emit('quick_job_result', {
'job_id': job_id,
'status': 'Failed',
'stdout': '',
'stderr': f"Unsupported type: {script_type}"
})
return
rc, out, err = _run_powershell_via_system_task(content)
if rc == -999:
rc, out, err = _run_powershell_script_content(content)
status = 'Success' if rc == 0 else 'Failed'
await sio.emit('quick_job_result', {
'job_id': job_id,
'status': status,
'stdout': out,
'stderr': err,
})
except Exception as e:
try:
await sio.emit('quick_job_result', {
'job_id': payload.get('job_id') if isinstance(payload, dict) else None,
'status': 'Failed',
'stdout': '',
'stderr': str(e),
})
except Exception:
pass