From eb75f1a49f07aa94e8e32a7258fb9cea1c4574fd Mon Sep 17 00:00:00 2001 From: KingKDot Date: Fri, 22 May 2026 10:12:40 -0500 Subject: [PATCH] large purgatory improvements --- Overlord-Server/public/assets/purgatory-ui.js | 171 ++++++++++++++++-- Overlord-Server/public/purgatory.html | 69 ++++++- Overlord-Server/src/config.ts | 5 + Overlord-Server/src/db.ts | 162 ++++++++++++++++- .../src/server/routes/enrollment-routes.ts | 23 ++- .../routes/websocket-lifecycle-routes.ts | 31 +++- Overlord-Server/src/types.ts | 2 + 7 files changed, 430 insertions(+), 33 deletions(-) diff --git a/Overlord-Server/public/assets/purgatory-ui.js b/Overlord-Server/public/assets/purgatory-ui.js index c872b3e..ee46e79 100644 --- a/Overlord-Server/public/assets/purgatory-ui.js +++ b/Overlord-Server/public/assets/purgatory-ui.js @@ -31,12 +31,32 @@ const autoAcceptModalConfirm = document.getElementById("auto-accept-modal-confir const autoAcceptModalCancel = document.getElementById("auto-accept-modal-cancel"); const autoAcceptModalBackdrop = document.getElementById("auto-accept-modal-backdrop"); const approveAllBtn = document.getElementById("approve-all-btn"); +const denyAllBtn = document.getElementById("deny-all-btn"); +const unlessSuspiciousToggle = document.getElementById("unless-suspicious-toggle"); +const unlessSuspiciousRow = document.getElementById("unless-suspicious-row"); +const statSuspicious = document.getElementById("stat-suspicious"); +const denyReasonModal = document.getElementById("deny-reason-modal"); +const denyReasonInput = document.getElementById("deny-reason-input"); +const denyReasonModalConfirm = document.getElementById("deny-reason-modal-confirm"); +const denyReasonModalCancel = document.getElementById("deny-reason-modal-cancel"); +const denyReasonModalBackdrop = document.getElementById("deny-reason-modal-backdrop"); let currentFilter = "pending"; let searchQuery = ""; let clients = []; const expandedCells = new Set(); // tracks "clientId:field" keys +const SUSPICIOUS_FLAG_LABELS = { + hwid_flood: "HWID Flood (40+ same hardware ID)", + hw_flood: "Hardware Flood (40+ identical specs)", + no_hostname: "No Hostname", + no_user: "No Username", + ip_flood: "IP Flood (40+ from same IP recently)", + vm_hardware: "VM Detected (CPU/GPU indicates virtual machine)", + vm_ram: "VM Likely (≤4 GB round RAM)", + no_monitors: "No Monitors (headless/VM)", +}; + // ── API helpers ──────────────────────────────────────────────────── async function api(path, opts = {}) { const res = await fetch(path, { credentials: "include", ...opts }); @@ -48,6 +68,8 @@ async function loadSettings() { try { const s = await api("/api/enrollment/settings"); autoAcceptToggle.checked = !s.requireApproval; + unlessSuspiciousToggle.checked = !!s.autoApproveUnlessSuspicious; + unlessSuspiciousRow.classList.toggle("hidden", !autoAcceptToggle.checked); } catch {} } @@ -82,8 +104,9 @@ async function loadClients() { emptyEl.classList.add("hidden"); body.querySelectorAll("tr:not(#enrollment-loading)").forEach((r) => r.remove()); + const fetchFilter = currentFilter === "suspicious" ? "all" : currentFilter; try { - const data = await api(`/api/clients?page=1&pageSize=500&enrollmentFilter=${currentFilter}`); + const data = await api(`/api/clients?page=1&pageSize=1000&enrollmentFilter=${fetchFilter}`); clients = data.items || []; } catch { clients = []; @@ -92,19 +115,31 @@ async function loadClients() { // Sort by newest first (highest lastSeen = most recent) clients.sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0)); + // Update suspicious stat from full loaded set + const suspiciousCount = clients.filter((c) => (c.suspiciousFlags || []).length > 0).length; + if (statSuspicious) statSuspicious.textContent = suspiciousCount; + + // Apply suspicious tab filter + let base = clients; + if (currentFilter === "suspicious") { + base = clients.filter((c) => (c.suspiciousFlags || []).length > 0); + } + // Apply search filter const q = searchQuery.toLowerCase().trim(); const filtered = q - ? clients.filter((c) => { + ? base.filter((c) => { const fields = [c.host, c.user, c.ip, c.id, c.os, c.country, c.keyFingerprint, c.cpu, c.gpu, c.ram]; return fields.some((f) => f && String(f).toLowerCase().includes(q)); }) - : clients; + : base; loadingRow.classList.add("hidden"); - const hasPending = currentFilter === "pending" && filtered.some((c) => (c.enrollmentStatus || "pending") === "pending"); - approveAllBtn.classList.toggle("hidden", !hasPending); + const showBulkActions = (currentFilter === "pending" || currentFilter === "suspicious") && + filtered.some((c) => (c.enrollmentStatus || "pending") === "pending"); + approveAllBtn.classList.toggle("hidden", !showBulkActions); + denyAllBtn.classList.toggle("hidden", !showBulkActions); if (filtered.length === 0) { emptyEl.classList.remove("hidden"); @@ -116,13 +151,13 @@ async function loadClients() { tr.className = "hover:bg-slate-800/40 transition-colors"; tr.dataset.id = c.id; - const statusPill = statusBadge(c.enrollmentStatus || "pending"); + const statusPill = statusBadgeWithReason(c.enrollmentStatus || "pending", c.denyReason); const fp = c.keyFingerprint ? c.keyFingerprint.substring(0, 16) + "..." : "-"; const lastSeen = c.lastSeen ? timeAgo(c.lastSeen) : "-"; tr.innerHTML = ` - ${esc(c.host || c.id)} + ${esc(c.host || c.id)}${suspiciousBadges(c.suspiciousFlags)} ${esc(c.user || "-")} ${esc(c.os || "-")} ${expandableCell(c.id, "cpu", c.cpu)} @@ -152,6 +187,24 @@ function statusBadge(status) { return map[status] || map.pending; } +function statusBadgeWithReason(status, denyReason) { + const pill = statusBadge(status); + if (status === "denied" && denyReason) { + return `
${pill}
${esc(denyReason)}
`; + } + return pill; +} + +function suspiciousBadges(flags) { + if (!flags || flags.length === 0) return ""; + return flags.map((f) => { + const label = SUSPICIOUS_FLAG_LABELS[f] || f; + const isFlood = f.endsWith("_flood"); + const color = isFlood ? "bg-red-500/20 text-red-300 border-red-500/40" : "bg-amber-500/20 text-amber-300 border-amber-500/40"; + return `${esc(label.split(" ")[0])}`; + }).join(" "); +} + function actionButtons(c) { const status = c.enrollmentStatus || "pending"; let html = ""; @@ -217,6 +270,20 @@ async function setStatus(clientId, action) { await Promise.all([loadClients(), loadStats()]); } +async function denyClient(clientId, reason) { + try { + await api(`/api/enrollment/${encodeURIComponent(clientId)}/deny`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ reason: reason || undefined }), + }); + if (window.showToast) window.showToast("Client denied", "success"); + } catch (e) { + if (window.showToast) window.showToast(`Failed: ${e.message}`, "error"); + } + await Promise.all([loadClients(), loadStats()]); +} + async function bulkAction(action) { const ids = getSelectedIds(); if (ids.length === 0) return; @@ -260,6 +327,7 @@ document.querySelectorAll(".enrollment-tab").forEach((tab) => { denied: "bg-red-500/20 text-red-300 border-red-500/40", approved: "bg-emerald-500/20 text-emerald-300 border-emerald-500/40", "banned-ips": "bg-rose-500/20 text-rose-300 border-rose-500/40", + suspicious: "bg-orange-500/20 text-orange-300 border-orange-500/40", }; tab.className = `enrollment-tab px-4 py-2 rounded-lg text-sm font-medium ${colorMap[currentFilter] || ""} border`; @@ -268,6 +336,7 @@ document.querySelectorAll(".enrollment-tab").forEach((tab) => { emptyEl.classList.add("hidden"); bannedIpsSection.classList.remove("hidden"); approveAllBtn.classList.add("hidden"); + denyAllBtn.classList.add("hidden"); loadBannedIps(); } else { bannedIpsSection.classList.add("hidden"); @@ -298,7 +367,7 @@ body.addEventListener("click", (e) => { const id = btn.dataset.id; if (!id) return; if (btn.classList.contains("act-approve")) setStatus(id, "approve"); - else if (btn.classList.contains("act-deny")) setStatus(id, "deny"); + else if (btn.classList.contains("act-deny")) showDenyReasonModal(id); else if (btn.classList.contains("act-reset")) setStatus(id, "reset"); else if (btn.classList.contains("act-ban-ip")) banClientIp(id); }); @@ -476,6 +545,7 @@ autoAcceptToggle.addEventListener("change", async () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ requireApproval: true }), }); + unlessSuspiciousRow.classList.add("hidden"); if (window.showToast) window.showToast("Approval required — purgatory is active", "success"); } catch (e) { autoAcceptToggle.checked = true; // revert on error @@ -500,31 +570,102 @@ autoAcceptModalConfirm.addEventListener("click", async () => { body: JSON.stringify({ requireApproval: false }), }); autoAcceptToggle.checked = true; + unlessSuspiciousRow.classList.remove("hidden"); if (window.showToast) window.showToast("Always Allow enabled — agents auto-approved on connect", "success"); } catch (e) { if (window.showToast) window.showToast(`Failed: ${e.message}`, "error"); } }); -// ── Approve All Pending ──────────────────────────────────────────── +// ── Unless Suspicious sub-toggle ────────────────────────────────── +unlessSuspiciousToggle.addEventListener("change", async () => { + try { + await api("/api/enrollment/settings", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ autoApproveUnlessSuspicious: unlessSuspiciousToggle.checked }), + }); + const msg = unlessSuspiciousToggle.checked + ? "Suspicious agents will be held for review" + : "All agents auto-approved regardless of flags"; + if (window.showToast) window.showToast(msg, "success"); + } catch (e) { + unlessSuspiciousToggle.checked = !unlessSuspiciousToggle.checked; // revert + if (window.showToast) window.showToast(`Failed: ${e.message}`, "error"); + } +}); + +// ── Approve All / Deny All ──────────────────────────────────────── +function getActionablePendingIds() { + const base = currentFilter === "suspicious" + ? clients.filter((c) => (c.suspiciousFlags || []).length > 0) + : clients; + return base.filter((c) => (c.enrollmentStatus || "pending") === "pending").map((c) => c.id); +} + approveAllBtn.addEventListener("click", async () => { - const pendingIds = clients - .filter((c) => (c.enrollmentStatus || "pending") === "pending") - .map((c) => c.id); - if (pendingIds.length === 0) return; + const ids = getActionablePendingIds(); + if (ids.length === 0) return; try { await api("/api/enrollment/bulk", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ ids: pendingIds, action: "approve" }), + body: JSON.stringify({ ids, action: "approve" }), }); - if (window.showToast) window.showToast(`Approved ${pendingIds.length} client${pendingIds.length !== 1 ? "s" : ""}`, "success"); + if (window.showToast) window.showToast(`Approved ${ids.length} client${ids.length !== 1 ? "s" : ""}`, "success"); } catch (e) { if (window.showToast) window.showToast(`Failed: ${e.message}`, "error"); } await Promise.all([loadClients(), loadStats()]); }); +denyAllBtn.addEventListener("click", async () => { + const ids = getActionablePendingIds(); + if (ids.length === 0) return; + try { + await api("/api/enrollment/bulk", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ids, action: "deny" }), + }); + if (window.showToast) window.showToast(`Denied ${ids.length} client${ids.length !== 1 ? "s" : ""}`, "success"); + } catch (e) { + if (window.showToast) window.showToast(`Failed: ${e.message}`, "error"); + } + await Promise.all([loadClients(), loadStats()]); +}); + +// ── Deny Reason Modal ────────────────────────────────────────────── +let _denyTargetId = null; + +function showDenyReasonModal(clientId) { + _denyTargetId = clientId; + denyReasonInput.value = ""; + denyReasonModal.classList.remove("hidden"); + setTimeout(() => denyReasonInput.focus(), 50); +} + +function closeDenyReasonModal() { + denyReasonModal.classList.add("hidden"); + _denyTargetId = null; +} + +denyReasonModalCancel.addEventListener("click", closeDenyReasonModal); +denyReasonModalBackdrop.addEventListener("click", closeDenyReasonModal); + +denyReasonInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") denyReasonModalConfirm.click(); + if (e.key === "Escape") closeDenyReasonModal(); +}); + +denyReasonModalConfirm.addEventListener("click", async () => { + const id = _denyTargetId; + const reason = denyReasonInput.value.trim() || undefined; + closeDenyReasonModal(); + if (!id) return; + await denyClient(id, reason); +}); + // ── Init ─────────────────────────────────────────────────────────── loadSettings(); loadStats(); diff --git a/Overlord-Server/public/purgatory.html b/Overlord-Server/public/purgatory.html index 0b2cfd1..eec4570 100644 --- a/Overlord-Server/public/purgatory.html +++ b/Overlord-Server/public/purgatory.html @@ -36,16 +36,28 @@ 0 pending 0 approved 0 denied + 0 suspicious 0 banned IPs -
- - Always Allow - +
+
+ + Always Allow + +
+
@@ -68,10 +80,18 @@ class="enrollment-tab px-4 py-2 rounded-lg text-sm font-medium bg-slate-800 text-slate-400 border border-slate-700 hover:bg-slate-700"> Banned IPs +
+
@@ -212,6 +232,37 @@
+ + +