diff --git a/Data/Agent/borealis-agent.py b/Data/Agent/borealis-agent.py index 00df6b1..6eff17a 100644 --- a/Data/Agent/borealis-agent.py +++ b/Data/Agent/borealis-agent.py @@ -17,6 +17,8 @@ import time # Heartbeat timestamps import subprocess import getpass import datetime +import shutil +import string import requests try: @@ -465,63 +467,83 @@ def collect_storage(): "disk_type": "Removable" if "removable" in part.opts.lower() else "Fixed Disk", "usage": usage.percent, "total": usage.total, - "free": 100 - usage.percent, + "free": usage.free, + "used": usage.used, }) elif plat == "windows": - try: - out = subprocess.run( - ["wmic", "logicaldisk", "get", "DeviceID,Size,FreeSpace"], - capture_output=True, - text=True, - timeout=60, - ) - lines = [l for l in out.stdout.splitlines() if l.strip()][1:] - for line in lines: - parts = line.split() - if len(parts) >= 3: - drive, free, size = parts[0], parts[1], parts[2] - try: - total = float(size) - free_bytes = float(free) - used = total - free_bytes - usage = (used / total * 100) if total else 0 - free_pct = 100 - usage - disks.append({ - "drive": drive, - "disk_type": "Fixed Disk", - "usage": usage, - "total": total, - "free": free_pct, - }) - except Exception: - pass - except FileNotFoundError: - ps_cmd = ( - "Get-PSDrive -PSProvider FileSystem | " - "Select-Object Name,Free,Used,Capacity,Root | 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 d in data: - total = d.get("Capacity") or 0 - used = d.get("Used") or 0 - usage = (used / total * 100) if total else 0 - free = 100 - usage - drive = d.get("Root") or f"{d.get('Name','')}:" + found = False + 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, - "total": total, - "free": free, + "usage": (usage.used / usage.total * 100) if usage.total else 0, + "total": usage.total, + "free": usage.free, + "used": usage.used, }) + found = True + if not found: + try: + out = subprocess.run( + ["wmic", "logicaldisk", "get", "DeviceID,Size,FreeSpace"], + capture_output=True, + text=True, + timeout=60, + ) + lines = [l for l in out.stdout.splitlines() if l.strip()][1:] + for line in lines: + parts = line.split() + if len(parts) >= 3: + drive, free, size = parts[0], parts[1], parts[2] + try: + total = float(size) + free_bytes = float(free) + used = total - free_bytes + usage = (used / total * 100) if total else 0 + disks.append({ + "drive": drive, + "disk_type": "Fixed Disk", + "usage": usage, + "total": total, + "free": free_bytes, + "used": used, + }) + except Exception: + pass + except FileNotFoundError: + ps_cmd = ( + "Get-PSDrive -PSProvider FileSystem | " + "Select-Object Name,Free,Used,Capacity,Root | 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 d in data: + total = d.get("Capacity") or 0 + used = d.get("Used") or 0 + free_bytes = d.get("Free") or max(total - used, 0) + usage = (used / total * 100) if total else 0 + drive = d.get("Root") or f"{d.get('Name','')}:" + disks.append({ + "drive": drive, + "disk_type": "Fixed Disk", + "usage": usage, + "total": total, + "free": free_bytes, + "used": used, + }) else: out = subprocess.run( ["df", "-kP"], capture_output=True, text=True, timeout=60 @@ -532,14 +554,15 @@ def collect_storage(): if len(parts) >= 6: total = int(parts[1]) * 1024 used = int(parts[2]) * 1024 + free_bytes = int(parts[3]) * 1024 usage = float(parts[4].rstrip("%")) - free = 100 - usage disks.append({ "drive": parts[5], "disk_type": "Fixed Disk", "usage": usage, "total": total, - "free": free, + "free": free_bytes, + "used": used, }) except Exception as e: print(f"[WARN] collect_storage failed: {e}") diff --git a/Data/Server/WebUI/src/Device_Details.jsx b/Data/Server/WebUI/src/Device_Details.jsx index 5309b8a..b6cfffa 100644 --- a/Data/Server/WebUI/src/Device_Details.jsx +++ b/Data/Server/WebUI/src/Device_Details.jsx @@ -283,15 +283,65 @@ export default function DeviceDetails({ device, onBack }) { }; const renderStorage = () => { - const rows = (details.storage || []).map((d) => ({ - drive: d.drive, - disk_type: d.disk_type, - usage: d.usage !== undefined ? Number(d.usage) : undefined, - total: d.total !== undefined ? Number(d.total) : undefined, - free: d.free !== undefined ? Number(d.free) : undefined, - })); + const toNum = (val) => { + if (val === undefined || val === null) return undefined; + if (typeof val === "number") { + return Number.isNaN(val) ? undefined : val; + } + const n = parseFloat(String(val).replace(/[^0-9.]+/g, "")); + return Number.isNaN(n) ? undefined : n; + }; + + const rows = (details.storage || []).map((d) => { + const total = toNum(d.total); + let usagePct = toNum(d.usage); + let usedBytes = toNum(d.used); + let freeBytes = toNum(d.free); + let freePct; + + if (usagePct !== undefined) { + if (usagePct <= 1) usagePct *= 100; + freePct = 100 - usagePct; + } + + if (usedBytes === undefined && total !== undefined && usagePct !== undefined) { + usedBytes = (usagePct / 100) * total; + } + + if (freeBytes === undefined && total !== undefined && usedBytes !== undefined) { + freeBytes = total - usedBytes; + } + + if (freePct === undefined && total !== undefined && freeBytes !== undefined) { + freePct = (freeBytes / total) * 100; + } + + if (usagePct === undefined && freePct !== undefined) { + usagePct = 100 - freePct; + } + + return { + drive: d.drive, + disk_type: d.disk_type, + used: usedBytes, + freePct, + freeBytes, + total, + usage: usagePct, + }; + }); + if (!rows.length) - return placeholderTable(["Drive Letter", "Disk Type", "Usage", "Total Size", "Free %"]); + return placeholderTable([ + "Drive Letter", + "Disk Type", + "Used", + "Free %", + "Free GB", + "Total Size", + "Usage", + ]); + return ( @@ -299,9 +349,11 @@ export default function DeviceDetails({ device, onBack }) { Drive Letter Disk Type - Usage - Total Size + Used Free % + Free GB + Total Size + Usage @@ -309,6 +361,26 @@ export default function DeviceDetails({ device, onBack }) { {d.drive} {d.disk_type} + + {d.used !== undefined && !Number.isNaN(d.used) + ? formatBytes(d.used) + : "unknown"} + + + {d.freePct !== undefined && !Number.isNaN(d.freePct) + ? `${d.freePct.toFixed(1)}%` + : "unknown"} + + + {d.freeBytes !== undefined && !Number.isNaN(d.freeBytes) + ? formatBytes(d.freeBytes) + : "unknown"} + + + {d.total !== undefined && !Number.isNaN(d.total) + ? formatBytes(d.total) + : "unknown"} + @@ -318,7 +390,7 @@ export default function DeviceDetails({ device, onBack }) { sx={{ height: 10, bgcolor: "#333", - "& .MuiLinearProgress-bar": { bgcolor: "#58a6ff" } + "& .MuiLinearProgress-bar": { bgcolor: "#00d18c" } }} /> @@ -329,16 +401,6 @@ export default function DeviceDetails({ device, onBack }) { - - {d.total !== undefined && !Number.isNaN(d.total) - ? formatBytes(d.total) - : "unknown"} - - - {d.free !== undefined && !Number.isNaN(d.free) - ? `${d.free.toFixed(1)}%` - : "unknown"} - ))} diff --git a/Data/Server/WebUI/src/Device_List.jsx b/Data/Server/WebUI/src/Device_List.jsx index 0799c4c..fe28a0a 100644 --- a/Data/Server/WebUI/src/Device_List.jsx +++ b/Data/Server/WebUI/src/Device_List.jsx @@ -165,33 +165,44 @@ export default function DeviceList({ onSelectDevice }) { {sorted.map((r, i) => ( - onSelectDevice && onSelectDevice(r)} - sx={{ cursor: onSelectDevice ? "pointer" : "default" }} - > + - - {r.status} + + + {r.status} + + + onSelectDevice && onSelectDevice(r)} + sx={{ + color: "#58a6ff", + "&:hover": { + cursor: onSelectDevice ? "pointer" : "default", + textDecoration: onSelectDevice ? "underline" : "none", + }, + }} + > + {r.hostname} - {r.hostname} {timeSince(r.lastSeen)} {r.os} openMenu(e, r)} + onClick={(e) => { + e.stopPropagation(); + openMenu(e, r); + }} sx={{ color: "#ccc" }} > diff --git a/Data/Server/server.py b/Data/Server/server.py index 9738f97..6ac27c1 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -398,6 +398,36 @@ def init_db(): init_db() + +def load_agents_from_db(): + """Populate registered_agents with any devices stored in the database.""" + try: + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + cur.execute("SELECT hostname, details FROM device_details") + for hostname, details_json in cur.fetchall(): + try: + details = json.loads(details_json or "{}") + except Exception: + details = {} + summary = details.get("summary", {}) + agent_id = summary.get("agent_id") or hostname + registered_agents[agent_id] = { + "agent_id": agent_id, + "hostname": summary.get("hostname") or hostname, + "agent_operating_system": summary.get("operating_system") + or summary.get("agent_operating_system") + or "-", + "last_seen": summary.get("last_seen") or 0, + "status": "Offline", + } + conn.close() + except Exception as e: + print(f"[WARN] Failed to load agents from DB: {e}") + + +load_agents_from_db() + @app.route("/api/agents") def get_agents(): """ @@ -481,10 +511,20 @@ def set_device_description(hostname: str): @app.route("/api/agent/", methods=["DELETE"]) def delete_agent(agent_id: str): - """Remove an agent from the in-memory registry.""" - if agent_id in registered_agents: - registered_agents.pop(agent_id, None) - agent_configurations.pop(agent_id, None) + """Remove an agent from the registry and database.""" + info = registered_agents.pop(agent_id, None) + agent_configurations.pop(agent_id, None) + hostname = info.get("hostname") if info else None + if hostname: + try: + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + cur.execute("DELETE FROM device_details WHERE hostname = ?", (hostname,)) + conn.commit() + conn.close() + except Exception as e: + return jsonify({"error": str(e)}), 500 + if info: return jsonify({"status": "removed"}) return jsonify({"error": "agent not found"}), 404