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 ( + + + + Software Name + Version + Action + + + + {rows.map((s, i) => ( + + {s.name} + {s.version} + + + ))} + +
+ ); + }; + + const renderMemory = () => { + const rows = details.memory || []; + if (!rows.length) return placeholderTable(["Slot", "Speed", "Serial Number", "Capacity"]); + return ( + + + + Slot + Speed + Serial Number + Capacity + + + + {rows.map((m, i) => ( + + {m.slot} + {m.speed} + {m.serial} + {formatBytes(m.capacity)} + + ))} + +
+ ); + }; + + const renderStorage = () => { + const rows = details.storage || []; + if (!rows.length) + return placeholderTable(["Drive Letter", "Disk Type", "Usage", "Total Size", "Free %"]); + return ( + + + + Drive Letter + Disk Type + Usage + Total Size + Free % + + + + {rows.map((d, i) => ( + + {d.drive} + {d.disk_type} + + {d.usage !== undefined && d.usage !== null && d.usage !== "unknown" + ? `${d.usage.toFixed ? d.usage.toFixed(1) : d.usage}%` + : "unknown"} + + {formatBytes(d.total)} + + {d.free !== undefined && d.free !== null && d.free !== "unknown" + ? `${d.free.toFixed ? d.free.toFixed(1) : d.free}%` + : "unknown"} + + + ))} + +
+ ); + }; + + const renderNetwork = () => { + const rows = details.network || []; + if (!rows.length) return placeholderTable(["Adapter", "IP Address", "MAC Address"]); + return ( + + + + Adapter + IP Address + MAC Address + + + + {rows.map((n, i) => ( + + {n.adapter} + {(n.ips || []).join(", ")} + {n.mac} + + ))} + +
+ ); + }; + const tabs = [ { label: "Summary", content: renderSummary() }, { @@ -91,28 +225,10 @@ export default function DeviceDetails({ device, onBack }) { "Enabled/Disabled Status" ]) }, - { - label: "Software", - content: placeholderTable(["Software Name", "Version", "Action"]) - }, - { - label: "Memory", - content: placeholderTable(["Slot", "Speed", "Serial Number", "Capacity"]) - }, - { - label: "Storage", - content: placeholderTable([ - "Drive Letter", - "Disk Type", - "Usage", - "Total Size", - "Free %" - ]) - }, - { - label: "Network", - content: placeholderTable(["Adapter", "IP Address", "MAC Address"]) - } + { label: "Software", content: renderSoftware() }, + { label: "Memory", content: renderMemory() }, + { label: "Storage", content: renderStorage() }, + { label: "Network", content: renderNetwork() } ]; return ( diff --git a/Data/Server/server.py b/Data/Server/server.py index b953f95..658d614 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -379,6 +379,7 @@ def rename_workflow(): registered_agents: Dict[str, Dict] = {} agent_configurations: Dict[str, Dict] = {} latest_images: Dict[str, Dict] = {} +DEVICES_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "Devices")) @app.route("/api/agents") def get_agents(): @@ -388,6 +389,35 @@ def get_agents(): return jsonify(registered_agents) +@app.route("/api/agent/details", methods=["POST"]) +def save_agent_details(): + data = request.get_json(silent=True) or {} + agent_id = data.get("agent_id") + details = data.get("details") + if not agent_id or not isinstance(details, dict): + return jsonify({"error": "invalid payload"}), 400 + os.makedirs(DEVICES_ROOT, exist_ok=True) + path = os.path.join(DEVICES_ROOT, f"{agent_id}.json") + try: + with open(path, "w", encoding="utf-8") as fh: + json.dump(details, fh, indent=2) + return jsonify({"status": "ok"}) + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/agent/details/", methods=["GET"]) +def get_agent_details(agent_id: str): + path = os.path.join(DEVICES_ROOT, f"{agent_id}.json") + if os.path.isfile(path): + try: + with open(path, "r", encoding="utf-8") as fh: + return jsonify(json.load(fh)) + except Exception: + pass + return jsonify({}) + + @app.route("/api/agent/", methods=["DELETE"]) def delete_agent(agent_id: str): """Remove an agent from the in-memory registry."""