diff --git a/.gitignore b/.gitignore
index 037425d..b00fc68 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,7 +17,7 @@ Borealis-Server.exe
/ElectronApp/
# Misc Files/Folders
-.vs/s
+.vs/
/Update_Staging/
/Macro_Testing/
@@ -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
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/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..ef48df9
--- /dev/null
+++ b/Data/Server/WebUI/src/Device_Details.jsx
@@ -0,0 +1,259 @@
+////////// 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 || {});
+ const [details, setDetails] = useState({});
+
+ useEffect(() => {
+ if (!device || !device.id) return;
+ const load = async () => {
+ try {
+ 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 device info", e);
+ }
+ };
+ 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: 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 = () => (
+
+
+ {summaryItems.map((item) => (
+
+ {item.label}
+ {item.value}
+
+ ))}
+
+
+ );
+
+ const placeholderTable = (headers) => (
+
+
+
+ {headers.map((h) => (
+ {h}
+ ))}
+
+
+
+
+
+ No data available.
+
+
+
+
+ );
+
+ 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() },
+ {
+ label: "Monitors",
+ content: placeholderTable([
+ "Type",
+ "Description",
+ "Latest Value",
+ "Policy",
+ "Latest 10 Days of Alerts",
+ "Enabled/Disabled Status"
+ ])
+ },
+ { label: "Software", content: renderSoftware() },
+ { label: "Memory", content: renderMemory() },
+ { label: "Storage", content: renderStorage() },
+ { label: "Network", content: renderNetwork() }
+ ];
+
+ 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" }}
+ >
", 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."""