From 95e87ad4c3e6318fa5f707e78b450d22372cd211 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Tue, 12 Aug 2025 21:59:27 -0600 Subject: [PATCH 1/4] Add device details component --- Data/Server/WebUI/src/App.jsx | 22 +++- Data/Server/WebUI/src/Device_Details.jsx | 143 +++++++++++++++++++++++ Data/Server/WebUI/src/Device_List.jsx | 9 +- 3 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 Data/Server/WebUI/src/Device_Details.jsx diff --git a/Data/Server/WebUI/src/App.jsx b/Data/Server/WebUI/src/App.jsx index 556e4f5..56307af 100644 --- a/Data/Server/WebUI/src/App.jsx +++ b/Data/Server/WebUI/src/App.jsx @@ -29,6 +29,7 @@ import DeviceList from "./Device_List"; import ScriptList from "./Script_List"; import ScheduledJobsList from "./Scheduled_Jobs_List"; import Login from "./Login.jsx"; +import DeviceDetails from "./Device_Details"; import { io } from "socket.io-client"; @@ -77,6 +78,7 @@ export default function App() { const [tabs, setTabs] = useState([{ id: "flow_1", tab_name: "Flow 1", nodes: [], edges: [] }]); const [activeTabId, setActiveTabId] = useState("flow_1"); const [currentPage, setCurrentPage] = useState("devices"); + const [selectedDevice, setSelectedDevice] = useState(null); const [aboutAnchorEl, setAboutAnchorEl] = useState(null); const [creditsDialogOpen, setCreditsDialogOpen] = useState(false); @@ -288,7 +290,25 @@ export default function App() { const renderMainContent = () => { switch (currentPage) { case "devices": - return ; + return ( + { + setSelectedDevice(d); + setCurrentPage("device_details"); + }} + /> + ); + + case "device_details": + return ( + { + setCurrentPage("devices"); + setSelectedDevice(null); + }} + /> + ); case "jobs": return ; diff --git a/Data/Server/WebUI/src/Device_Details.jsx b/Data/Server/WebUI/src/Device_Details.jsx new file mode 100644 index 0000000..75529c5 --- /dev/null +++ b/Data/Server/WebUI/src/Device_Details.jsx @@ -0,0 +1,143 @@ +////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/Device_Details.jsx + +import React, { useState, useEffect } from "react"; +import { + Paper, + Box, + Tabs, + Tab, + Typography, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + Button +} from "@mui/material"; + +export default function DeviceDetails({ device, onBack }) { + const [tab, setTab] = useState(0); + const [agent, setAgent] = useState(device || {}); + + useEffect(() => { + if (!device || !device.id) return; + const fetchAgent = 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] }); + } + } catch (e) { + console.warn("Failed to load agent", e); + } + }; + fetchAgent(); + }, [device]); + + 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" } + ]; + + const renderSummary = () => ( + + + {summaryItems.map((item) => ( + + {item.label} + {item.value} + + ))} + +
+ ); + + const placeholderTable = (headers) => ( + + + + {headers.map((h) => ( + {h} + ))} + + + + + + No data available. + + + +
+ ); + + const tabs = [ + { label: "Summary", content: renderSummary() }, + { + label: "Monitors", + content: placeholderTable([ + "Type", + "Description", + "Latest Value", + "Policy", + "Latest 10 Days of Alerts", + "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"]) + } + ]; + + return ( + + + {onBack && ( + + )} + + {agent.hostname || "Device Details"} + + + setTab(v)} + sx={{ borderBottom: 1, borderColor: "#333" }} + > + {tabs.map((t) => ( + + ))} + + {tabs[tab].content} + + ); +} + diff --git a/Data/Server/WebUI/src/Device_List.jsx b/Data/Server/WebUI/src/Device_List.jsx index 280e33b..0799c4c 100644 --- a/Data/Server/WebUI/src/Device_List.jsx +++ b/Data/Server/WebUI/src/Device_List.jsx @@ -35,7 +35,7 @@ function statusFromHeartbeat(tsSec, offlineAfter = 15) { return now - tsSec <= offlineAfter ? "Online" : "Offline"; } -export default function DeviceList() { +export default function DeviceList({ onSelectDevice }) { const [rows, setRows] = useState([]); const [orderBy, setOrderBy] = useState("status"); const [order, setOrder] = useState("desc"); @@ -165,7 +165,12 @@ export default function DeviceList() { {sorted.map((r, i) => ( - + onSelectDevice && onSelectDevice(r)} + sx={{ cursor: onSelectDevice ? "pointer" : "default" }} + > Date: Tue, 12 Aug 2025 22:14:07 -0600 Subject: [PATCH 2/4] Collect and expose agent device details --- Data/Agent/borealis-agent.py | 199 +++++++++++++++++++++++ Data/Server/WebUI/src/Device_Details.jsx | 190 +++++++++++++++++----- Data/Server/server.py | 30 ++++ 3 files changed, 382 insertions(+), 37 deletions(-) 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.""" From 4f3b2525987e97e5bd4f5447cfd306e13b8005db Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Tue, 12 Aug 2025 23:35:58 -0600 Subject: [PATCH 3/4] Hid Devices Folder from Git Repo --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 037425d..6da252f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,7 @@ Borealis-Server.exe /Dependencies/NodeJS/ /Dependencies/Python/ /Dependencies/AutoHotKey/ -/Data/Server/Python_API_Endpoints/Tesseract-OCR/ \ No newline at end of file +/Data/Server/Python_API_Endpoints/Tesseract-OCR/ + +# Deployed Environment Folders +/Devices/ \ No newline at end of file From 39990c121f320ed387714dd27d590924c8a38c9d Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Tue, 12 Aug 2025 23:37:11 -0600 Subject: [PATCH 4/4] Fixed Gitignore Section --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6da252f..b00fc68 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ Borealis-Server.exe /ElectronApp/ # Misc Files/Folders -.vs/s +.vs/ /Update_Staging/ /Macro_Testing/