mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-27 02:01:57 -06:00
Refactored & Modularized Agent Roles
This commit is contained in:
2
Data/Agent/Roles/__init__.py
Normal file
2
Data/Agent/Roles/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# Roles package for Borealis Agent
|
||||
|
||||
280
Data/Agent/Roles/role_DeviceInventory.py
Normal file
280
Data/Agent/Roles/role_DeviceInventory.py
Normal 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)
|
||||
|
||||
122
Data/Agent/Roles/role_Macro.py
Normal file
122
Data/Agent/Roles/role_Macro.py
Normal 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))
|
||||
|
||||
322
Data/Agent/Roles/role_Screenshot.py
Normal file
322
Data/Agent/Roles/role_Screenshot.py
Normal 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()
|
||||
|
||||
217
Data/Agent/Roles/role_ScriptExec_CURRENTUSER.py
Normal file
217
Data/Agent/Roles/role_ScriptExec_CURRENTUSER.py
Normal 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)
|
||||
153
Data/Agent/Roles/role_ScriptExec_SYSTEM.py
Normal file
153
Data/Agent/Roles/role_ScriptExec_SYSTEM.py
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user