diff --git a/Data/Agent/borealis-agent.py b/Data/Agent/borealis-agent.py index 5939332..fef724c 100644 --- a/Data/Agent/borealis-agent.py +++ b/Data/Agent/borealis-agent.py @@ -14,6 +14,15 @@ import random # Macro Randomization import platform # OS Detection import importlib.util import time # Heartbeat timestamps +import subprocess +import getpass + +import requests +try: + import psutil +except Exception: + psutil = None +import aiohttp import socketio from qasync import QEventLoop @@ -205,6 +214,195 @@ async def send_heartbeat(): print(f"[WARN] heartbeat emit failed: {e}") await asyncio.sleep(5) +# ---------------- Detailed Agent Data ---------------- + +def _get_internal_ip(): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except Exception: + return "unknown" + +def collect_summary(): + try: + last_user = getpass.getuser() + except Exception: + last_user = "unknown" + try: + last_reboot = "unknown" + if psutil: + last_reboot = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(psutil.boot_time())) + except Exception: + last_reboot = "unknown" + + created = CONFIG.data.get("created") + if not created: + created = time.strftime("%Y-%m-%d %H:%M:%S") + CONFIG.data["created"] = created + CONFIG._write() + + try: + external_ip = requests.get("https://api.ipify.org", timeout=5).text.strip() + except Exception: + external_ip = "unknown" + + return { + "hostname": socket.gethostname(), + "operating_system": CONFIG.data.get("agent_operating_system", detect_agent_os()), + "last_user": last_user, + "internal_ip": _get_internal_ip(), + "external_ip": external_ip, + "last_reboot": last_reboot, + "created": created, + } + +def collect_software(): + items = [] + plat = platform.system().lower() + try: + if plat == "windows": + out = subprocess.run(["wmic", "product", "get", "name,version"], capture_output=True, text=True, timeout=60) + for line in out.stdout.splitlines(): + if line.strip() and not line.lower().startswith("name"): + parts = line.strip().split(" ") + name = parts[0].strip() + version = parts[-1].strip() if len(parts) > 1 else "" + if name: + items.append({"name": name, "version": version}) + elif plat == "linux": + out = subprocess.run(["dpkg-query", "-W", "-f=${Package}\t${Version}\n"], capture_output=True, text=True) + for line in out.stdout.splitlines(): + if "\t" in line: + name, version = line.split("\t", 1) + items.append({"name": name, "version": version}) + else: + out = subprocess.run([sys.executable, "-m", "pip", "list", "--format", "json"], capture_output=True, text=True) + data = json.loads(out.stdout or "[]") + for pkg in data: + items.append({"name": pkg.get("name"), "version": pkg.get("version")}) + except Exception as e: + print(f"[WARN] collect_software failed: {e}") + return items[:100] + +def collect_memory(): + entries = [] + plat = platform.system().lower() + try: + if plat == "windows": + out = subprocess.run([ + "wmic", + "memorychip", + "get", + "BankLabel,Speed,SerialNumber,Capacity" + ], capture_output=True, text=True) + lines = [l for l in out.stdout.splitlines() if l.strip() and "BankLabel" not in l] + for line in lines: + parts = [p for p in line.split() if p] + if len(parts) >= 4: + entries.append({ + "slot": parts[0], + "speed": parts[1], + "serial": parts[2], + "capacity": parts[3], + }) + elif plat == "linux": + out = subprocess.run(["dmidecode", "-t", "17"], capture_output=True, text=True) + slot = speed = serial = capacity = None + for line in out.stdout.splitlines(): + line = line.strip() + if line.startswith("Locator:"): + slot = line.split(":", 1)[1].strip() + elif line.startswith("Speed:"): + speed = line.split(":", 1)[1].strip() + elif line.startswith("Serial Number:"): + serial = line.split(":", 1)[1].strip() + elif line.startswith("Size:"): + capacity = line.split(":", 1)[1].strip() + elif not line and slot: + entries.append({ + "slot": slot, + "speed": speed or "unknown", + "serial": serial or "unknown", + "capacity": capacity or "unknown", + }) + slot = speed = serial = capacity = None + if slot: + entries.append({ + "slot": slot, + "speed": speed or "unknown", + "serial": serial or "unknown", + "capacity": capacity or "unknown", + }) + except Exception as e: + print(f"[WARN] collect_memory failed: {e}") + + if not entries: + try: + if psutil: + 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 "removable" in part.opts.lower() else "Fixed Disk", + "usage": usage.percent, + "total": usage.total, + "free": 100 - usage.percent, + }) + except Exception as e: + print(f"[WARN] collect_storage failed: {e}") + return disks + +def collect_network(): + adapters = [] + try: + if psutil: + for name, addrs in psutil.net_if_addrs().items(): + ips = [a.address for a in addrs if getattr(a, "family", None) == socket.AF_INET] + mac = next((a.address for a in addrs if getattr(a, "family", None) == getattr(psutil, "AF_LINK", object)), "unknown") + adapters.append({"adapter": name, "ips": ips, "mac": mac}) + except Exception as e: + print(f"[WARN] collect_network failed: {e}") + return adapters + +async def send_agent_details(): + """Collect detailed agent data and send to server periodically.""" + while True: + try: + details = { + "summary": collect_summary(), + "software": collect_software(), + "memory": collect_memory(), + "storage": collect_storage(), + "network": collect_network(), + } + url = CONFIG.data.get("borealis_server_url", "http://localhost:5000") + "/api/agent/details" + async with aiohttp.ClientSession() as session: + await session.post(url, json={"agent_id": AGENT_ID, "details": details}, timeout=10) + except Exception as e: + print(f"[WARN] Failed to send agent details: {e}") + await asyncio.sleep(300) + @sio.event async def connect(): print(f"[WebSocket] Connected to Borealis Server with Agent ID: {AGENT_ID}") @@ -593,6 +791,7 @@ if __name__=='__main__': background_tasks.append(loop.create_task(idle_task())) # Start periodic heartbeats background_tasks.append(loop.create_task(send_heartbeat())) + background_tasks.append(loop.create_task(send_agent_details())) loop.run_forever() except Exception as e: print(f"[FATAL] Event loop crashed: {e}") diff --git a/Data/Server/WebUI/src/Device_Details.jsx b/Data/Server/WebUI/src/Device_Details.jsx index 75529c5..ef48df9 100644 --- a/Data/Server/WebUI/src/Device_Details.jsx +++ b/Data/Server/WebUI/src/Device_Details.jsx @@ -18,32 +18,51 @@ import { export default function DeviceDetails({ device, onBack }) { const [tab, setTab] = useState(0); const [agent, setAgent] = useState(device || {}); + const [details, setDetails] = useState({}); useEffect(() => { if (!device || !device.id) return; - const fetchAgent = async () => { + const load = async () => { try { - const res = await fetch("/api/agents"); - const data = await res.json(); - if (data && data[device.id]) { - setAgent({ id: device.id, ...data[device.id] }); + const [agentsRes, detailsRes] = await Promise.all([ + fetch("/api/agents"), + fetch(`/api/agent/details/${device.id}`) + ]); + const agentsData = await agentsRes.json(); + if (agentsData && agentsData[device.id]) { + setAgent({ id: device.id, ...agentsData[device.id] }); } + const detailData = await detailsRes.json(); + setDetails(detailData || {}); } catch (e) { - console.warn("Failed to load agent", e); + console.warn("Failed to load device info", e); } }; - fetchAgent(); + load(); }, [device]); + const formatBytes = (val) => { + if (val === undefined || val === null || val === "unknown") return "unknown"; + let num = Number(val); + const units = ["B", "KB", "MB", "GB", "TB"]; + let i = 0; + while (num >= 1024 && i < units.length - 1) { + num /= 1024; + i++; + } + return `${num.toFixed(1)} ${units[i]}`; + }; + + const summary = details.summary || {}; const summaryItems = [ - { label: "Device Name", value: agent.hostname || device?.hostname || "unknown" }, - { label: "Description", value: "unknown" }, - { label: "Operating System", value: agent.agent_operating_system || "unknown" }, - { label: "Last User", value: "unknown" }, - { label: "Internal IP", value: agent.internal_ip || "unknown" }, - { label: "External IP", value: "unknown" }, - { label: "Last Reboot", value: agent.last_reboot || "unknown" }, - { label: "Created", value: agent.created || "unknown" } + { label: "Device Name", value: summary.hostname || agent.hostname || device?.hostname || "unknown" }, + { label: "Description", value: summary.description || "unknown" }, + { label: "Operating System", value: summary.operating_system || agent.agent_operating_system || "unknown" }, + { label: "Last User", value: summary.last_user || "unknown" }, + { label: "Internal IP", value: summary.internal_ip || "unknown" }, + { label: "External IP", value: summary.external_ip || "unknown" }, + { label: "Last Reboot", value: summary.last_reboot || "unknown" }, + { label: "Created", value: summary.created || "unknown" } ]; const renderSummary = () => ( @@ -78,6 +97,121 @@ export default function DeviceDetails({ device, onBack }) { ); + const renderSoftware = () => { + const rows = details.software || []; + if (!rows.length) return placeholderTable(["Software Name", "Version", "Action"]); + return ( +