diff --git a/client/agent-settings.html b/client/agent-settings.html new file mode 100644 index 0000000..c3df2ba --- /dev/null +++ b/client/agent-settings.html @@ -0,0 +1,546 @@ + + + + + + + + + + +
+
+ + +
+ +
+ + +
16px
+
+ +
+ + +
+ +
+ +
+ +
+ No image selected + +
+
+
+ +
+ + +
100 lines from log file on startup
+
+ +
+ +
+ + 100% + +
+
Use Ctrl++ and Ctrl+- for quick zoom adjustment
+
+ +
+ + +
+
+ + + + \ No newline at end of file diff --git a/client/agent.html b/client/agent.html index 97da2a0..6a5cfaa 100644 --- a/client/agent.html +++ b/client/agent.html @@ -2,19 +2,25 @@ + + + + + Agent Terminal +
- - - - - - - - - - - - - - -
Agent IDHostnameUsernameProcessPIDIPArchPlatformCheckin
+ + + + + + + + + + + + + + +
Loading..--------
@@ -178,5 +207,143 @@ tr > td {
+ diff --git a/client/agent.js b/client/agent.js index f1b854c..190f195 100644 --- a/client/agent.js +++ b/client/agent.js @@ -31,7 +31,7 @@ const commands = [ 'mv', 'sleep', 'cp', 'load', 'upload', 'download', 'scexec', 'scan', 'exit', 'assembly', 'help', 'dns','cd', - 'bof' + 'bof', 'link', 'unlink' ]; global.commandHistory = []; @@ -86,10 +86,6 @@ const commandDetails = [ { name: "download", help: "Download a file from remote agent box to local operator box /\r\n\tdownload [remote_path]\r\n\tdownload /agent/src/file\r\n\t- Get from View > Downloads\r\n" }, { name: "link", help: "Peer-to-peer connect to a Loki TCP agent\r\n\tlink \r\n\tlink localhost 3000\r\n" }, { name: "unlink", help: "Disconnect from a Loki TCP agent\r\n\tunlink \r\n\tunlink localhost\r\n" }, - //{ name: "socks", help: "socks \r\n" + - // " - Connect to a SOCKS5 server.\r\n" + - // " Examples:\r\n" + - // " socks xyz.cloudfront.net:443/ws/agent/09f21e3b6e247b8a6a6ee2472f5\r\n" }, { name: "scan", help: "scan [-p] \r\n" + " - The target host or CIDR range to scan.\r\n" + " - Options:\r\n" + @@ -201,6 +197,7 @@ class Task { this.uploadChannel = 'u-' + Math.random().toString(36).substring(2, 14); this.command = command; this.taskid = Math.random().toString(36).substring(2, 14); + this.status = 'starting'; } } @@ -442,21 +439,26 @@ async function printToConsole(message, logToFileFlag = true) { } } -function loadPreviousLogs() { +function loadPreviousLogs(customHistoryLength = null) { try { let agent_log_name = `${pid_log}-${user_log}-${agentid_log}.log`; const logFile = path.join(directories.logDir, agent_log_name); log(`logfile : ${logFile}`); if (fs.existsSync(logFile)) { - log(`Loading previous logs from ${logFile}`); - const logs = fs.readFileSync(logFile, 'utf8'); - logs.split('\n').forEach(line => { - let cleanedLine = line.replace(/^\[.*?\]\s*\[.*?\]\s*/, '').trim(); - if (cleanedLine) { - printToConsole(cleanedLine, false); - } - }); + if (customHistoryLength !== null) { + // Use provided history length (for immediate reload) + loadLogsWithLength(logFile, customHistoryLength); + } else { + // Get history length setting (default to 100) + ipcRenderer.invoke('get-customize-settings').then(settings => { + const historyLength = settings?.historyLength || 100; + loadLogsWithLength(logFile, historyLength); + }).catch(error => { + log(`Error getting history length setting, using default: ${error}`); + loadLogsWithLength(logFile, 100); + }); + } } else { log(`No existing log file found for ${logFile}`); } @@ -465,6 +467,34 @@ function loadPreviousLogs() { } } +function loadLogsWithLength(logFile, historyLength) { + try { + const logs = fs.readFileSync(logFile, 'utf8'); + const allLines = logs.split('\n'); + const nonEmptyLines = allLines.filter(line => line.trim().length > 0); + + let linesToLoad; + if (historyLength === -1) { + // Load all lines (infinite) + linesToLoad = nonEmptyLines; + log(`Loading all ${nonEmptyLines.length} lines from ${logFile}`); + } else { + // Load last N lines + linesToLoad = nonEmptyLines.slice(-historyLength); + log(`Loading last ${linesToLoad.length} of ${nonEmptyLines.length} lines from ${logFile}`); + } + + linesToLoad.forEach(line => { + let cleanedLine = line.replace(/^\[.*?\]\s*\[.*?\]\s*/, '').trim(); + if (cleanedLine) { + printToConsole(cleanedLine, false); + } + }); + } catch (error) { + log(`Error in loadLogsWithLength: ${error.message}`); + } +} + function clearConsole() { const consoleOutput = document.getElementById('consoleOutput'); consoleOutput.innerHTML = ''; @@ -539,16 +569,37 @@ window.addEventListener('DOMContentLoaded', async () => { containerName = agent_object.container; containerKey = agent_object.aes; containerBlob = agent_object.blobs; - //agent = JSON.parse(agent_object); global.agent = agent_object; - document.title = `${agent_object.agentid.toUpperCase()} | ${agent_object.hostname.toUpperCase()} | ${agent_object.username.toUpperCase()} | ${agent_object.IP}`; + document.title = "Loki Agent"; + + // Immediately populate the agent table with the received data + populateAgentTable(agent_object); + + // Enable input immediately since we have agent data + global.inputload = true; + window_agentid = agent_object.agentid; + + // Set up logging variables + agentid_log = agent_object.agentid || ''; + user_log = agent_object.username || ''; + pid_log = agent_object.PID || ''; + + // Load command history + if (global.historyload === false && agent_object.hostname) { + log(`Loading previous command history for ${agent_object.hostname}`); + global.historyload = true; + loadCommandHistory(agent_object.hostname); + loadPreviousLogs(); + } const input = document.getElementById('consoleInput'); input.focus(); input.selectionStart = input.selectionEnd = input.value.length; + + log(`agent.js | container-data | Successfully initialized agent window for ${agent_object.agentid}`); } catch (error) { - log(error); + log(`container-data error: ${error} ${error.stack}`); } }); @@ -562,6 +613,7 @@ window.addEventListener('DOMContentLoaded', async () => { ipcRenderer.on('command-result', (event, result) => { log(`[AGENT][IPC] command-result : ${result}`); + log(`[AGENT][IPC] command-result : ${JSON.stringify(result)}`); printToConsole(result); }); @@ -611,6 +663,16 @@ window.addEventListener('DOMContentLoaded', async () => { ipcRenderer.send('cut'); event.preventDefault(); } + // Ctrl++ for zoom in + if (event.ctrlKey && (event.key === '+' || event.key === '=')) { + event.preventDefault(); + adjustZoom(0.1); + } + // Ctrl+- for zoom out + if (event.ctrlKey && event.key === '-') { + event.preventDefault(); + adjustZoom(-0.1); + } }); // Send message to main process to add test command menu item @@ -620,50 +682,116 @@ window.addEventListener('DOMContentLoaded', async () => { ipcRenderer.on('execute-test-command', () => { executeCommandTests(); }); + + // Listen for history reload from settings window + ipcRenderer.on('reload-history', (event, historyLength) => { + log(`=== RECEIVED RELOAD-HISTORY IPC ===`); + log(`New history length: ${historyLength}`); + + // Clear the console and reload with new history length + clearConsole(); + loadPreviousLogs(historyLength); + }); }); +function populateAgentTable(agentData) { + try { + const agentDataRow = document.getElementById('agentDataRow'); + if (!agentDataRow) { + log('populateAgentTable: agentDataRow element not found'); + return; + } + + if (!agentData) { + log('populateAgentTable: No agent data provided'); + return; + } + + log(`populateAgentTable: Populating table with agent data:`, { + agentid: agentData.agentid, + hostname: agentData.hostname, + username: agentData.username, + platform: agentData.platform + }); + + // Determine platform name + let platformName = agentData.platform || 'Unknown'; + if (agentData.platform === "darwin") { + platformName = "macOS"; + } else if (agentData.platform === "win32") { + platformName = "Windows"; + } else if (agentData.platform === "linux") { + platformName = "Linux"; + } + + // Process filename from full path + let fileName = agentData.Process || ''; + if (fileName) { + fileName = fileName.trim(); + if (fileName.includes("\\") && !fileName.includes("/")) { + fileName = path.win32.basename(fileName); + } else if (fileName.includes("/") && !fileName.includes("\\")) { + fileName = path.posix.basename(fileName); + } else { + fileName = fileName.split(/[/\\]/).pop(); + } + } + + // Update table cells with defensive checks + if (agentDataRow.cells.length >= 9) { + agentDataRow.cells[0].textContent = agentData.agentid || '-'; + agentDataRow.cells[1].textContent = agentData.hostname || '-'; + agentDataRow.cells[2].textContent = agentData.username || '-'; + agentDataRow.cells[3].textContent = fileName || '-'; + agentDataRow.cells[4].textContent = agentData.PID || '-'; + agentDataRow.cells[5].textContent = agentData.IP || '-'; + agentDataRow.cells[6].textContent = agentData.arch || '-'; + agentDataRow.cells[7].textContent = platformName || '-'; + agentDataRow.cells[8].textContent = agentData.checkin ? timeDifference(agentData.checkin) : 'Just connected'; + } else { + log(`populateAgentTable: Table row doesn't have enough cells: ${agentDataRow.cells.length}`); + } + + // Set logging variables + agentid_log = agentData.agentid || ''; + user_log = agentData.username || ''; + pid_log = agentData.PID || ''; + + log(`populateAgentTable: Successfully populated table for agent ${agentData.agentid}`); + } catch (error) { + log(`populateAgentTable error: ${error} ${error.stack}`); + } +} + async function updateTable() { try { - const agentTable = document.getElementById('agentTable').getElementsByTagName('tbody')[0]; + if (!global.agent || !global.agent.agentid) { + log('updateTable: No agent data available yet'); + return; + } + let agentid = global.agent.agentid; window_agentid = agentid; - let agentcheckin = await ipcRenderer.invoke('get-agent-checkin', agentid); - - if (agentcheckin) { - //log(`agentcheckin : ${agentcheckin}`); - agentcheckin = JSON.parse(agentcheckin); - - const filePath = agentcheckin.Process.trim(); - let fileName; - if (filePath.includes("\\") && !filePath.includes("/")) { - fileName = path.win32.basename(filePath); - } else if (filePath.includes("/") && !filePath.includes("\\")) { - fileName = path.posix.basename(filePath); - } else { - fileName = filePath.split(/[/\\]/).pop(); - } - for (let row of agentTable.rows) { - let platformName = agentcheckin.platform; - if (global.agent.platform === "darwin") { - platformName = "macOS"; - } else if (global.agent.platform === "win32") { - platformName = "Windows"; - } else if (global.agent.platform === "linux") { - platformName = "Linux"; + + // Try to get fresh checkin data, but don't block if it fails + try { + let agentcheckin = await ipcRenderer.invoke('get-agent-checkin', agentid); + if (agentcheckin) { + agentcheckin = JSON.parse(agentcheckin); + populateAgentTable(agentcheckin); + + if (global.historyload === false) { + log(`Loading previous command history`); + global.historyload = true; + loadCommandHistory(global.agent.hostname); + loadPreviousLogs(); } - row.cells[0].textContent = agentcheckin.agentid; - row.cells[1].textContent = agentcheckin.hostname; - row.cells[2].textContent = agentcheckin.username; - row.cells[3].textContent = fileName; - row.cells[4].textContent = agentcheckin.PID; - row.cells[5].textContent = agentcheckin.IP; - row.cells[6].textContent = agentcheckin.arch; - row.cells[7].textContent = platformName; - row.cells[8].textContent = timeDifference(agentcheckin.checkin); - agentid_log = agentcheckin.agentid; - user_log = agentcheckin.username; - pid_log = agentcheckin.PID; + global.inputload = true; } + } catch (checkinError) { + log(`updateTable: Failed to get checkin data, using existing agent data: ${checkinError}`); + // Fallback to using the original agent data + populateAgentTable(global.agent); if (global.historyload === false) { log(`Loading previous command history`); global.historyload = true; @@ -673,7 +801,7 @@ async function updateTable() { global.inputload = true; } } catch (error) { - log(`${error} ${error.stack}`); + log(`updateTable error: ${error} ${error.stack}`); } } setInterval(updateTable, 1000); @@ -711,10 +839,26 @@ async function format_ls_output(filesAndFolders) { } ipcRenderer.on('command-output', async (event, command_response) => { + log(`[AGENT][IPC] command-output : ${command_response.output}`); + log(`[AGENT][IPC] command : ${command_response.command}`); + log(`[AGENT][IPC] taskid : ${command_response.taskid}`); + log(`[AGENT][IPC] status : ${command_response.status}`); try { if (command_response.command.startsWith('ls')) { - let resultBuffer = await format_ls_output(command_response.output); - printToConsole(resultBuffer); + // Check if output is JSON before parsing + let isJson = false; + try { + JSON.parse(command_response.output); + isJson = true; + } catch (e) { + isJson = false; + } + if (isJson) { + let resultBuffer = await format_ls_output(command_response.output); + printToConsole(resultBuffer); + } else { + printToConsole(command_response.output); + } } else if (command_response.command.startsWith('bof ')) { if (command_response.command.startsWith('bof ')) { printToConsole(command_response.output.replace(//g, '>')); @@ -725,7 +869,7 @@ ipcRenderer.on('command-output', async (event, command_response) => { printToConsole(command_response.output); } } catch (error) { - log(`Error in ipcRender(delete-old-container): ${error.message}\r\n${error.stack}`); + log(`[!][AGENT][IPC][command-output] Error: ${error.message}\r\n${error.stack}`); } }); @@ -735,7 +879,7 @@ ipcRenderer.on('window-closing', async () => { log(`agentid: ${window_agentid}`); ipcRenderer.send('force-close', window_agentid); } catch (error) { - log(`Error in ipcRender(delete-old-container): ${error.message}\r\n${error.stack}`); + log(`[!][AGENT][IPC][window-closing] Error: ${error.message}\r\n${error.stack}`); } }); @@ -779,20 +923,18 @@ function moveCursorByWord(input, direction) { } const testCommands = [ - { command: "sleep 1 0"}, { command: "sleep 0 0"}, - { command: "cd C:\\TEMP"}, { command: "pwd"}, + { command: "cd C:\\TEMP"}, { command: "ls \"C:\\Program Files (x86)\\Microsoft\""}, { command: "ls C:\\\\Program\\ Files\\ (x86)\\\\Microsoft"}, { command: "ls 'C:\\Program Files (x86)\\Microsoft'"}, { command: "ls 'C:/Program Files (x86)/Microsoft'"}, { command: "dns reverse 1.1.1.1"}, { command: "dns lookup google.com"}, - { command: "bof /Users/bobby/CS/SA/dir/dir.x64.o go z \"C:\\Users\\user\\Desktop\""}, + { command: "bof /dev/dir.x64.o go z \"C:\\TEMP\\"}, { command: "spawn powershell.exe -c \"echo testing > C:\\TEMP\\testing.txt\""}, { command: "ls"}, - { command: "pwd"}, { command: "cat testing.txt"}, { command: "mv testing.txt testing2.txt"}, { command: "ls .\\"}, @@ -800,10 +942,10 @@ const testCommands = [ { command: "ls ./"}, { command: "download testing3.txt"}, { command: "cd"}, - { command: "upload /Users/bobby/Loki/downloads/testing3.txt ./"}, + { command: "upload /TEMP/testing3.txt ./"}, { command: "scan 127.0.0.1 -p22,80,445,8080"}, - { command: "assembly /Users/bobby/Rubeus_v4.0_InspirationOpinion.exe klist"}, - { command: "scexec /Users/bobby/popcalc.bin"} + { command: "assembly /dev/Rubeus_v4.0_InspirationOpinion.exe klist"}, + { command: "scexec /dev/popcalc.bin"} ]; async function executeCommandTests() { diff --git a/client/assets/images/agent.png b/client/assets/images/agent.png old mode 100644 new mode 100755 index 9554705..b183e25 Binary files a/client/assets/images/agent.png and b/client/assets/images/agent.png differ diff --git a/client/assets/images/apple.png b/client/assets/images/apple.png old mode 100644 new mode 100755 diff --git a/client/assets/images/azure.png b/client/assets/images/azure.png old mode 100644 new mode 100755 diff --git a/client/assets/images/background.png b/client/assets/images/background.png index eee21f9..f099728 100644 Binary files a/client/assets/images/background.png and b/client/assets/images/background.png differ diff --git a/client/assets/images/logo1.png b/client/assets/images/logo1.png new file mode 100644 index 0000000..bca34e9 Binary files /dev/null and b/client/assets/images/logo1.png differ diff --git a/client/assets/images/repo.png b/client/assets/images/repo.png new file mode 100644 index 0000000..584d1c3 Binary files /dev/null and b/client/assets/images/repo.png differ diff --git a/client/assets/linux/icon.png b/client/assets/linux/icon.png old mode 100644 new mode 100755 diff --git a/client/assets/mac/icon.icns b/client/assets/mac/icon.icns old mode 100644 new mode 100755 diff --git a/client/assets/win/icon.ico b/client/assets/win/icon.ico old mode 100644 new mode 100755 diff --git a/client/dashboard-settings.html b/client/dashboard-settings.html new file mode 100644 index 0000000..2ca7cec --- /dev/null +++ b/client/dashboard-settings.html @@ -0,0 +1,313 @@ + + + + + + + Dashboard Settings + + + + +
+ +
+ +
+ +
+ No image selected + +
+
+
+ +
+ + +
+
+ + + + \ No newline at end of file diff --git a/client/dashboard.html b/client/dashboard.html index eef9d9d..84fd2ca 100644 --- a/client/dashboard.html +++ b/client/dashboard.html @@ -3,7 +3,7 @@ - ]+++[========> Loki C2 Dashboard <========]+++[ + Loki Dashboard @@ -110,6 +264,161 @@ tr > td { + + + Loki Logo + + + Repo Logo + + diff --git a/client/dashboard.js b/client/dashboard.js index 8ebeb29..0a4e00b 100644 --- a/client/dashboard.js +++ b/client/dashboard.js @@ -37,6 +37,30 @@ function timeDifference(oldTimestamp) { return result.trim().replace(/,\s*$/, ''); // Remove trailing comma and space } +// Function to parse time strings back to milliseconds for sorting +function parseTimeStringToMilliseconds(timeString) { + if (!timeString || timeString.trim() === '') return 0; + + let totalMs = 0; + const msInSecond = 1000; + const msInMinute = msInSecond * 60; + const msInHour = msInMinute * 60; + const msInDay = msInHour * 24; + + // Parse patterns like "1d, 3h, 5m, 2s" or just "11s" + const dayMatch = timeString.match(/(\d+)d/); + const hourMatch = timeString.match(/(\d+)h/); + const minuteMatch = timeString.match(/(\d+)m/); + const secondMatch = timeString.match(/(\d+)s/); + + if (dayMatch) totalMs += parseInt(dayMatch[1]) * msInDay; + if (hourMatch) totalMs += parseInt(hourMatch[1]) * msInHour; + if (minuteMatch) totalMs += parseInt(minuteMatch[1]) * msInMinute; + if (secondMatch) totalMs += parseInt(secondMatch[1]) * msInSecond; + + return totalMs; +} + window.addEventListener('DOMContentLoaded', () => { let sortState = { column: null, order: 'none' }; @@ -173,6 +197,7 @@ window.addEventListener('DOMContentLoaded', async () => { try { let agentcheckins; agentcheckins = await ipcRenderer.invoke('get-containers'); + // log(`[DASHBOARD] agentcheckins : ${agentcheckins}`); if (agentcheckins != 0) { const agents = JSON.parse(agentcheckins); let agent_index = 0; @@ -204,29 +229,27 @@ window.addEventListener('DOMContentLoaded', async () => { } else if (agent.platform === "linux") { platformName = "Linux"; } - thisrow.cells[0].textContent = agent.agentid; - thisrow.cells[1].textContent = agent.container; - thisrow.cells[2].textContent = agent.hostname; - thisrow.cells[3].textContent = agent.username; - thisrow.cells[4].textContent = fileName; - thisrow.cells[5].textContent = agent.PID; - thisrow.cells[6].textContent = agent.IP; - thisrow.cells[7].textContent = agent.arch; - thisrow.cells[8].textContent = platformName; // Set formatted platform name - thisrow.cells[9].textContent = agent.mode; - thisrow.cells[10].textContent = timeDifference(agent.checkin); - thisrow.replaceWith(thisrow.cloneNode(true)); // Remove previous listeners - let table = document.getElementById('containerTable').getElementsByTagName('tbody')[0]; - for (let row of table.rows) { - if (row.cells[0].textContent == agent.agentid) { - thisrow = row; - break; - } + // Update cell content only if it has changed + if (thisrow.cells[0].textContent !== agent.agentid) thisrow.cells[0].textContent = agent.agentid; + if (thisrow.cells[1].textContent !== agent.container) thisrow.cells[1].textContent = agent.container; + if (thisrow.cells[2].textContent !== agent.hostname) thisrow.cells[2].textContent = agent.hostname; + if (thisrow.cells[3].textContent !== agent.username) thisrow.cells[3].textContent = agent.username; + if (thisrow.cells[4].textContent !== fileName) thisrow.cells[4].textContent = fileName; + if (thisrow.cells[5].textContent !== agent.PID) thisrow.cells[5].textContent = agent.PID; + if (thisrow.cells[6].textContent !== agent.IP) thisrow.cells[6].textContent = agent.IP; + if (thisrow.cells[7].textContent !== agent.arch) thisrow.cells[7].textContent = agent.arch; + if (thisrow.cells[8].textContent !== platformName) thisrow.cells[8].textContent = platformName; + if (thisrow.cells[9].textContent !== agent.mode) thisrow.cells[9].textContent = agent.mode; + if (thisrow.cells[10].textContent !== timeDifference(agent.checkin)) thisrow.cells[10].textContent = timeDifference(agent.checkin); + + // Only add click listener if this is a new row or if listener was removed + if (isnewrow || !thisrow.hasAttribute('data-click-listener')) { + thisrow.addEventListener('click', () => { + console.log("Row clicked!"); + ipcRenderer.send('open-container-window', agent.agentid); + }); + thisrow.setAttribute('data-click-listener', 'true'); } - thisrow.addEventListener('click', () => { - console.log("Row clicked!"); // Debugging: Check if it logs multiple times - ipcRenderer.send('open-container-window', agent.agentid); - }, { once: true }); // Ensures it only triggers once per element } }); updateTableSort(); @@ -287,7 +310,15 @@ window.addEventListener('DOMContentLoaded', async () => { rows.sort((a, b) => { const aText = a.querySelector(`td:nth-child(${getColumnIndex(sortState.column)})`).textContent.trim(); const bText = b.querySelector(`td:nth-child(${getColumnIndex(sortState.column)})`).textContent.trim(); - return sortState.order === 'asc' ? aText.localeCompare(bText) : bText.localeCompare(aText); + + // Special handling for checkin column (time sorting) + if (sortState.column === 'checkin') { + const aTime = parseTimeStringToMilliseconds(aText); + const bTime = parseTimeStringToMilliseconds(bText); + return sortState.order === 'asc' ? aTime - bTime : bTime - aTime; + } else { + return sortState.order === 'asc' ? aText.localeCompare(bText) : bText.localeCompare(aText); + } }); } diff --git a/client/kernel.js b/client/kernel.js index 5aa64a2..53c6847 100644 --- a/client/kernel.js +++ b/client/kernel.js @@ -1,10 +1,12 @@ -const { app, BrowserWindow, ipcMain, Menu, clipboard, shell, screen, dialog, MenuItem } = require('electron'); +const { app, BrowserWindow, ipcMain, Menu, clipboard, shell, screen, dialog, MenuItem, nativeTheme } = require('electron'); const fs = require('fs'); +const path = require('path'); const az = require('./azure'); const { getAppDataDir } = require('./common'); const directories = getAppDataDir(); global.config = require(directories.configFilePath); -const agents = []; +const agents = []; +global.removedAgents = []; let win; global.agentids = []; global.agentwindows = 0; @@ -13,6 +15,10 @@ global.dashboardWindow = null; global.agents = []; global.haltUpdate = false; +// Task queue system +global.taskQueue = []; +global.isProcessingTask = false; + class Container { constructor(name, key = {}, blobs = {}) @@ -47,10 +53,14 @@ class Task { this.outputChannel = 'o-' + Math.random().toString(36).substring(2, 14); this.command = command; this.taskid = Math.random().toString(36).substring(2, 14); + this.status = 'starting'; } } function createDashboardWindow() { + // Force dark theme + nativeTheme.themeSource = 'dark'; + const primaryDisplay = screen.getPrimaryDisplay(); const { width, height } = primaryDisplay.workAreaSize; @@ -58,14 +68,25 @@ function createDashboardWindow() { width: Math.floor(width * 0.76), height: Math.floor(height * 0.8), center: true, + darkTheme: true, webPreferences: { contextIsolation: false, enableRemoteModule: true, nodeIntegration: true }, }); + + // Set dashboard-specific menu when window is focused + global.dashboardWindow.on('focus', () => { + setDashboardMenu(); + }); + global.dashboardWindow.focus(); global.dashboardWindow.loadFile('dashboard.html'); + + // Set dashboard menu immediately + setDashboardMenu(); + console.log('Main window created'); } @@ -86,6 +107,7 @@ async function createContainerWindow(thisagentid) { width: Math.floor(width * 0.6), height: Math.floor(height * 0.7), center: true, + darkTheme: true, webPreferences: { contextIsolation: false, enableRemoteModule: true, @@ -93,14 +115,44 @@ async function createContainerWindow(thisagentid) { }, }); let agent_object = global.agents.find(agent => agent.agentid === thisagentid); + + if (!agent_object) { + console.error(`createContainerWindow: No agent found with ID ${thisagentid}`); + console.log('Available agents:', global.agents.map(a => a.agentid)); + return; + } + + console.log(`createContainerWindow: Found agent:`, agent_object.agentid, agent_object.hostname); global.agentWindowHandles[agent_object.agentid] = containerWin; global.agentwindows++; containerWin.loadFile('agent.html').then(() => { + console.log(`createContainerWindow: Sending container-data for agent ${agent_object.agentid}`); containerWin.webContents.send('container-data', agent_object); + + // Load and apply saved settings to new agent window + const customizePath = path.join(directories.configDir, 'customize.js'); + try { + if (fs.existsSync(customizePath)) { + delete require.cache[require.resolve(customizePath)]; + const savedSettings = require(customizePath); + console.log('Applying saved settings to new agent window:', savedSettings); + containerWin.webContents.send('apply-font-settings', savedSettings); + } + } catch (error) { + console.error('Error loading settings for new agent window:', error); + } }); console.log(`Container window created for container: ${agent_object.container}`); console.log(`Number of agent windows : ${global.agentwindows}`); + // Store the agent ID in the window object for menu handling + containerWin.agentId = thisagentid; + + // Set agent-specific menu when window is focused + containerWin.on('focus', () => { + setAgentMenu(); + }); + containerWin.on('close', async (event) => { event.preventDefault(); await containerWin.webContents.send('window-closing'); @@ -143,6 +195,7 @@ async function createExplorerWindow(thisagent) { width: Math.floor(width * 0.6), height: Math.floor(height * 0.7), center: true, + darkTheme: true, webPreferences: { contextIsolation: false, enableRemoteModule: true, @@ -169,10 +222,11 @@ function openAgentLogsExplorer() { function openConfigWindow() { const configWindow = new BrowserWindow({ width: 500, - height: 400, + height: 500, title: "Configuration", parent: win, modal: true, + darkTheme: true, webPreferences: { contextIsolation: false, enableRemoteModule: true, @@ -182,6 +236,619 @@ function openConfigWindow() { configWindow.loadFile('settings.html'); } +let agentSettingsWindows = new Map(); // Track settings windows per agent +let taskQueueWindows = new Map(); // Track task queue windows per agent +let dashboardSettingsWindow = null; // Track dashboard settings window + +function setDashboardMenu() { + const dashboardMenu = Menu.buildFromTemplate([ + { + label: 'View', + submenu: [ + { + label: 'Configuration', + click: () => { + openConfigWindow(); + } + }, + { + label: 'Downloads', + click: () => { + openDownloadsExplorer(); + } + }, + { + label: 'Agent Logs', + click: () => { + openAgentLogsExplorer(); + } + }, + { + type: 'separator' + }, + { + label: 'Dashboard Settings', + click: () => { + openDashboardSettingsWindow(); + } + } + ] + }, + { + label: 'Developer', + submenu: [ + { + label: 'Toggle Developer Tools', + accelerator: 'CmdOrCtrl+Shift+I', + click: () => { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + focusedWindow.webContents.toggleDevTools(); + } + } + }, + { + label: 'Perform Command Test', + click: () => { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + focusedWindow.webContents.send('execute-test-command'); + } + } + }, + { + role: 'reload' + } + ] + } + ]); + Menu.setApplicationMenu(dashboardMenu); +} + +function setAgentMenu() { + const agentMenu = Menu.buildFromTemplate([ + { + label: 'View', + submenu: [ + { + label: 'Configuration', + click: () => { + openConfigWindow(); + } + }, + { + label: 'Downloads', + click: () => { + openDownloadsExplorer(); + } + }, + { + label: 'Agent Logs', + click: () => { + openAgentLogsExplorer(); + } + }, + { + type: 'separator' + }, + { + label: 'Task Queue', + click: () => { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow && focusedWindow.agentId) { + console.log(`[TASK QUEUE] Menu item clicked for agent: ${focusedWindow.agentId}`); + openTaskQueueWindow(focusedWindow.agentId); + } + } + }, + { + type: 'separator' + }, + { + label: 'Agent Settings', + click: () => { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + console.log('Agent Settings clicked for agent window'); + openAgentSettingsWindow(focusedWindow); + } + } + } + ] + }, + { + label: 'Developer', + submenu: [ + { + label: 'Toggle Developer Tools', + accelerator: 'CmdOrCtrl+Shift+I', + click: () => { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + focusedWindow.webContents.toggleDevTools(); + } + } + }, + { + label: 'Perform Command Test', + click: () => { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + focusedWindow.webContents.send('execute-test-command'); + } + } + }, + { + role: 'reload' + } + ] + } + ]); + Menu.setApplicationMenu(agentMenu); +} + +function openTaskQueueWindow(agentId) { + // Force dark mode for this window + nativeTheme.themeSource = 'dark'; + console.log(`[TASK QUEUE] Opening task queue window for agent: ${agentId}`); + + // Check if task queue window already exists for this agent + if (taskQueueWindows.has(agentId)) { + const existingWindow = taskQueueWindows.get(agentId); + if (existingWindow && !existingWindow.isDestroyed()) { + console.log(`[TASK QUEUE] Focusing existing window for agent: ${agentId}`); + existingWindow.focus(); + return; + } + } + + try { + const taskQueueWindow = new BrowserWindow({ + width: 800, + height: 600, + title: `Task Queue - Agent ${agentId}`, + center: true, + resizable: true, + darkTheme: true, + webPreferences: { + contextIsolation: false, + enableRemoteModule: true, + nodeIntegration: true + }, + }); + + console.log(`[TASK QUEUE] Created window for agent: ${agentId}`); + + taskQueueWindow.loadFile('task-queue.html').then(() => { + console.log(`[TASK QUEUE] Loaded task-queue.html for agent: ${agentId}`); + }).catch((error) => { + console.error(`[TASK QUEUE] Error loading task-queue.html: ${error}`); + }); + + // Store reference + taskQueueWindows.set(agentId, taskQueueWindow); + + // Clean up reference when window is closed + taskQueueWindow.on('closed', () => { + console.log(`[TASK QUEUE] Window closed for agent: ${agentId}`); + taskQueueWindows.delete(agentId); + }); + + // Store agent ID for IPC communication + taskQueueWindow.agentId = agentId; + + // Send agent data when window loads + taskQueueWindow.webContents.on('did-finish-load', () => { + console.log(`[TASK QUEUE] Window finished loading for agent: ${agentId}`); + const agent = global.agents.find(agent => agent.agentid === agentId); + if (agent) { + console.log(`[TASK QUEUE] Sending agent data for: ${agentId}`); + taskQueueWindow.webContents.send('agent-data', agent); + } else { + console.log(`[TASK QUEUE] No agent found with ID: ${agentId}`); + } + }); + + } catch (error) { + console.error(`[TASK QUEUE] Error creating window for agent ${agentId}: ${error}`); + } +} + +function openAgentSettingsWindow(parentWindow) { + // Check if settings window already exists for this parent + if (agentSettingsWindows.has(parentWindow)) { + const existingWindow = agentSettingsWindows.get(parentWindow); + if (existingWindow && !existingWindow.isDestroyed()) { + existingWindow.focus(); + return; + } + } + + const settingsWindow = new BrowserWindow({ + width: 550, + height: 800, + title: "Agent Settings", + parent: parentWindow, + modal: false, + center: true, + resizable: true, + darkTheme: true, + webPreferences: { + contextIsolation: false, + enableRemoteModule: true, + nodeIntegration: true + }, + }); + + settingsWindow.loadFile('agent-settings.html'); + + // Store reference + agentSettingsWindows.set(parentWindow, settingsWindow); + + // Clean up reference when window is closed + settingsWindow.on('closed', () => { + agentSettingsWindows.delete(parentWindow); + }); + + // Store parent reference for IPC communication + settingsWindow.parentAgentWindow = parentWindow; +} + +function openDashboardSettingsWindow() { + // Check if settings window already exists + if (dashboardSettingsWindow && !dashboardSettingsWindow.isDestroyed()) { + dashboardSettingsWindow.focus(); + return; + } + + dashboardSettingsWindow = new BrowserWindow({ + width: 550, + height: 360, + title: "Dashboard Settings", + parent: global.dashboardWindow, + modal: false, + center: true, + resizable: true, + darkTheme: true, + webPreferences: { + contextIsolation: false, + enableRemoteModule: true, + nodeIntegration: true + }, + }); + + dashboardSettingsWindow.loadFile('dashboard-settings.html'); + + // Clean up reference when window is closed + dashboardSettingsWindow.on('closed', () => { + dashboardSettingsWindow = null; + }); +} + +// IPC handler for selecting background image +ipcMain.handle('select-background-image', async () => { + const result = await dialog.showOpenDialog({ + title: 'Select Background Image', + filters: [ + { name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'] }, + { name: 'All Files', extensions: ['*'] } + ], + properties: ['openFile'] + }); + + if (!result.canceled && result.filePaths.length > 0) { + return result.filePaths[0]; + } + + return null; +}); + +// IPC handler for applying agent settings +ipcMain.on('apply-agent-settings', (event, settings) => { + console.log('=== RECEIVED APPLY-AGENT-SETTINGS IPC ==='); + console.log('Settings received:', settings); + + // Save settings to customize.js file + const customizePath = path.join(directories.configDir, 'customize.js'); + console.log('Saving to customize path:', customizePath); + + // Load existing settings to preserve backgroundImage + let existingSettings = {}; + try { + if (fs.existsSync(customizePath)) { + delete require.cache[require.resolve(customizePath)]; + existingSettings = require(customizePath); + } + } catch (error) { + console.error('Error loading existing settings for merge:', error); + } + + const customizeConfig = { + fontFamily: settings.fontFamily, + fontSize: settings.fontSize, + fontColor: settings.fontColor, + zoom: settings.zoom, + backgroundImage: existingSettings.backgroundImage || null, // Preserve existing dashboard background image + agentBackgroundImage: settings.agentBackgroundImage, + historyLength: settings.historyLength !== undefined ? settings.historyLength : 100, + lastUpdated: new Date().toISOString(), + version: "1.0.0" + }; + + try { + fs.writeFileSync(customizePath, `module.exports = ${JSON.stringify(customizeConfig, null, 4)};`); + console.log('Customize settings saved successfully'); + } catch (error) { + console.error('Error saving customize settings:', error); + } + + // Find the settings window that sent this message + const settingsWindow = BrowserWindow.fromWebContents(event.sender); + console.log('Settings window found:', !!settingsWindow); + + const parentWindow = settingsWindow ? settingsWindow.parentAgentWindow : null; + console.log('Parent window found:', !!parentWindow); + console.log('Parent window destroyed?', parentWindow ? parentWindow.isDestroyed() : 'N/A'); + + if (parentWindow && !parentWindow.isDestroyed()) { + console.log('Sending apply-font-settings to parent window...'); + // Send settings to the parent agent window + parentWindow.webContents.send('apply-font-settings', settings); + console.log('apply-font-settings IPC sent to parent'); + } else { + console.error('Parent window not available or destroyed'); + + // Fallback: try to apply to all agent windows + console.log('Fallback: applying to all agent windows'); + Object.values(global.agentWindowHandles || {}).forEach((window, index) => { + if (window && !window.isDestroyed()) { + console.log(`Sending to agent window ${index}`); + window.webContents.send('apply-font-settings', settings); + } + }); + } + + // Also send settings to dashboard window for background image + if (global.dashboardWindow && !global.dashboardWindow.isDestroyed()) { + console.log('Sending apply-font-settings to dashboard window...'); + global.dashboardWindow.webContents.send('apply-font-settings', settings); + console.log('apply-font-settings IPC sent to dashboard'); + } +}); + +// IPC handler for reading customize settings +ipcMain.handle('get-customize-settings', () => { + const customizePath = path.join(directories.configDir, 'customize.js'); + console.log('Looking for customize.js at:', customizePath); + + try { + // Check if customize.js exists, if not create it with defaults + if (!fs.existsSync(customizePath)) { + console.log('Creating new customize.js file...'); + const defaultConfig = { + fontFamily: "'Fira Code', monospace", + fontSize: 16, + fontColor: "#c5c5c5", + zoom: 1.0, + backgroundImage: null, + agentBackgroundImage: null, + historyLength: 100, + lastUpdated: new Date().toISOString(), + version: "1.0.0" + }; + fs.writeFileSync(customizePath, `module.exports = ${JSON.stringify(defaultConfig, null, 4)};`); + console.log('Created default customize.js file at:', customizePath); + } + + // Clear require cache to get fresh data + delete require.cache[require.resolve(customizePath)]; + const customizeConfig = require(customizePath); + console.log('Loaded customize settings:', customizeConfig); + return customizeConfig; + } catch (error) { + console.error('Error loading customize settings:', error); + // Return defaults if file can't be read + return { + fontFamily: "'Fira Code', monospace", + fontSize: 16, + fontColor: "#c5c5c5", + zoom: 1.0, + backgroundImage: null, + agentBackgroundImage: null, + historyLength: 100 + }; + } +}); + +// IPC handler for closing agent settings window +ipcMain.on('close-agent-settings-window', (event) => { + const settingsWindow = BrowserWindow.fromWebContents(event.sender); + if (settingsWindow && !settingsWindow.isDestroyed()) { + settingsWindow.close(); + } +}); + +// IPC handler for reloading agent history with new length setting +ipcMain.on('reload-agent-history', (event, historyLength) => { + console.log('=== RECEIVED RELOAD-AGENT-HISTORY IPC ==='); + console.log('History length:', historyLength); + + // Find the settings window that sent this message + const settingsWindow = BrowserWindow.fromWebContents(event.sender); + const parentWindow = settingsWindow ? settingsWindow.parentAgentWindow : null; + + if (parentWindow && !parentWindow.isDestroyed()) { + console.log('Sending reload-history to parent agent window...'); + parentWindow.webContents.send('reload-history', historyLength); + console.log('reload-history IPC sent to parent'); + } else { + console.error('Parent window not available for history reload'); + } +}); + +// IPC handler for applying dashboard settings +ipcMain.on('apply-dashboard-settings', (event, settings) => { + console.log('=== RECEIVED APPLY-DASHBOARD-SETTINGS IPC ==='); + console.log('Dashboard settings received:', settings); + + // Load existing settings and merge with dashboard-specific settings + const customizePath = path.join(directories.configDir, 'customize.js'); + let existingSettings = {}; + + try { + if (fs.existsSync(customizePath)) { + delete require.cache[require.resolve(customizePath)]; + existingSettings = require(customizePath); + } + } catch (error) { + console.error('Error loading existing settings:', error); + } + + // Merge dashboard settings with existing settings + const customizeConfig = { + ...existingSettings, + backgroundImage: settings.backgroundImage, + lastUpdated: new Date().toISOString(), + version: "1.0.0" + }; + + try { + fs.writeFileSync(customizePath, `module.exports = ${JSON.stringify(customizeConfig, null, 4)};`); + console.log('Dashboard settings saved successfully'); + } catch (error) { + console.error('Error saving dashboard settings:', error); + } + + // Apply background image to dashboard window immediately + if (global.dashboardWindow && !global.dashboardWindow.isDestroyed()) { + console.log('Sending apply-font-settings to dashboard window...'); + global.dashboardWindow.webContents.send('apply-font-settings', customizeConfig); + console.log('apply-font-settings IPC sent to dashboard'); + } +}); + +// IPC handler for closing dashboard settings window +ipcMain.on('close-dashboard-settings-window', (event) => { + const settingsWindow = BrowserWindow.fromWebContents(event.sender); + if (settingsWindow && !settingsWindow.isDestroyed()) { + settingsWindow.close(); + } +}); + +// IPC handlers for task queue operations +ipcMain.handle('get-agent-tasks', (event, agentId) => { + const agent = global.agents.find(agent => agent.agentid === agentId); + return agent ? agent.tasks || [] : []; +}); + +ipcMain.handle('remove-task', (event, agentId, taskId) => { + try { + const agent = global.agents.find(agent => agent.agentid === agentId); + if (agent && agent.tasks) { + const taskIndex = agent.tasks.findIndex(task => task.taskid === taskId); + if (taskIndex !== -1) { + // Don't remove tasks that are currently processing + if (agent.tasks[taskIndex].status === 'processing') { + return { success: false, message: 'Cannot remove task that is currently processing' }; + } + agent.tasks.splice(taskIndex, 1); + console.log(`[TASK QUEUE] Removed task ${taskId} from agent ${agentId}`); + + // Notify all task queue windows for this agent + if (taskQueueWindows.has(agentId)) { + const window = taskQueueWindows.get(agentId); + if (window && !window.isDestroyed()) { + window.webContents.send('task-removed', taskId); + } + } + + return { success: true }; + } + } + return { success: false, message: 'Task not found' }; + } catch (error) { + console.error('Error removing task:', error); + return { success: false, message: error.message }; + } +}); + +ipcMain.handle('modify-task', (event, agentId, taskId, newCommand) => { + try { + const agent = global.agents.find(agent => agent.agentid === agentId); + if (agent && agent.tasks) { + const task = agent.tasks.find(task => task.taskid === taskId); + if (task) { + // Don't modify tasks that are currently processing + if (task.status === 'processing') { + return { success: false, message: 'Cannot modify task that is currently processing' }; + } + task.command = newCommand; + console.log(`[TASK QUEUE] Modified task ${taskId} for agent ${agentId}: ${newCommand}`); + + // Notify all task queue windows for this agent + if (taskQueueWindows.has(agentId)) { + const window = taskQueueWindows.get(agentId); + if (window && !window.isDestroyed()) { + window.webContents.send('task-modified', task); + } + } + + return { success: true }; + } + } + return { success: false, message: 'Task not found' }; + } catch (error) { + console.error('Error modifying task:', error); + return { success: false, message: error.message }; + } +}); + +ipcMain.handle('reorder-task', (event, agentId, taskId, direction) => { + try { + const agent = global.agents.find(agent => agent.agentid === agentId); + if (agent && agent.tasks) { + const taskIndex = agent.tasks.findIndex(task => task.taskid === taskId); + if (taskIndex !== -1) { + const task = agent.tasks[taskIndex]; + + // Don't move tasks that are processing + if (task.status === 'processing') { + return { success: false, message: 'Cannot reorder task that is currently processing' }; + } + + let newIndex = taskIndex; + if (direction === 'up' && taskIndex > 0) { + newIndex = taskIndex - 1; + } else if (direction === 'down' && taskIndex < agent.tasks.length - 1) { + newIndex = taskIndex + 1; + } else { + return { success: false, message: 'Cannot move task in that direction' }; + } + + // Remove task from current position and insert at new position + agent.tasks.splice(taskIndex, 1); + agent.tasks.splice(newIndex, 0, task); + + console.log(`[TASK QUEUE] Moved task ${taskId} ${direction} for agent ${agentId}`); + + // Notify all task queue windows for this agent + if (taskQueueWindows.has(agentId)) { + const window = taskQueueWindows.get(agentId); + if (window && !window.isDestroyed()) { + window.webContents.send('tasks-reordered', agent.tasks); + } + } + + return { success: true }; + } + } + return { success: false, message: 'Task not found' }; + } catch (error) { + console.error('Error reordering task:', error); + return { success: false, message: error.message }; + } +}); + ipcMain.handle('updateagent', async (event, agentid,newcontainerid) => { try { @@ -215,25 +882,24 @@ ipcMain.on('upload-client-command-to-input-channel', async (event, agent_object) console.log(`kernel.js : IPC "upload-client-command-to-input-channel`); let global_agent_object = global.agents.find(agent => agent.agentid === agent_object.agentid); let agent_task = agent_object.tasks[agent_object.tasks.length - 1]; + + // Set task status to queued and add to the global agent's task list + agent_task.status = 'queued'; + + // Initialize tasks array if it doesn't exist + if (!global_agent_object.tasks) { + global_agent_object.tasks = []; + } + global_agent_object.tasks.push(agent_task); - - let commandOutput = false; - while (commandOutput === false) { - commandOutput = await az.uploadCommand(agent_object); - if (commandOutput === false) { - await new Promise(resolve => setTimeout(resolve, 3000)); - } - } - console.log(`[KERNEL][IPC] command-output : ${commandOutput}`); - let command_response = { - 'command' : agent_task.command, - 'taskid' : agent_task.taskid, - 'output' : commandOutput - } - event.reply('command-output', command_response); + + console.log(`[TASK QUEUE] Task ${agent_task.taskid} added to queue for agent ${agent_object.agentid}`); + + // The task will be processed by the task queue processor + // No immediate response is sent - the queue processor will send the response when complete } catch (error) { - console.error('Error uploading command to Azure Blob Storage:', error); - event.reply('command-output', `Error: ${error.message}`); // Send error response + console.error('Error adding task to queue:', error); + event.reply('command-output', `Error: ${error.message}`); } }); @@ -293,9 +959,19 @@ ipcMain.handle('preload-agents', async () => { }); ipcMain.handle('get-containers', async () => { + // console.log(`[KENEL][IPC] get-containers : ${global.removedAgents}`); + // console.log(`[KENEL][IPC] global.haltUpdate : ${global.haltUpdate}`); if (global.haltUpdate == false) { let blobs = await az.updateDashboardTable(); + if (global.removedAgents.length > 0) + { + for (let i = 0; i < global.removedAgents.length; i++) + { + blobs.removedAgents = global.removedAgents[i]; + console.log(`[KENEL][IPC] global.removedAgents[${i}] : ${global.removedAgents[i]}`); + } + } return blobs; } else @@ -352,59 +1028,12 @@ ipcMain.on('execute-command', (event, command) => { app.whenReady().then(() => { console.log('App is ready'); createDashboardWindow(); - const menu = Menu.buildFromTemplate([ - { - label: 'View', - submenu: [ - { - label: 'Configuration', - click: () => { - openConfigWindow(); - } - }, - { - label: 'Downloads', - click: () => { - openDownloadsExplorer(); - } - }, - { - label: 'Agent Logs', - click: () => { - openAgentLogsExplorer(); - } - } - ] - }, - { - label: 'Developer', - submenu: [ - { - label: 'Toggle Developer Tools', - accelerator: 'CmdOrCtrl+Shift+I', - click: () => { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) { - focusedWindow.webContents.toggleDevTools(); - } - } - }, - { - label: 'Perform Command Test', - click: () => { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) { - focusedWindow.webContents.send('execute-test-command'); - } - } - }, - { - role: 'reload' - } - ] - } - ]); -Menu.setApplicationMenu(menu); + + // Start the task queue processor + startTaskQueue(); + + // Set initial dashboard menu + setDashboardMenu(); // Handle right-click context menu for table rows ipcMain.on('show-row-context-menu', (event, agentsDataJSON) => { let agentsData = JSON.parse(agentsDataJSON); @@ -416,7 +1045,7 @@ ipcMain.on('show-row-context-menu', (event, agentsDataJSON) => { click: async () => { try { global.haltUpdate = true; - + global.removedAgents.push(agentid); // Handle agent window cleanup if (global.agentWindowHandles[agentid]) { try { @@ -595,4 +1224,128 @@ app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createDashboardWindow(); } -}); \ No newline at end of file +}); + +// Task Queue Processor - runs every 0.5 seconds +function processTaskQueue() { + // Skip if already processing a task + if (global.isProcessingTask) { + return; + } + + // Find the next queued task across all agents + let nextTask = null; + let taskAgent = null; + + for (let agent of global.agents) { + if (agent.tasks && agent.tasks.length > 0) { + const queuedTask = agent.tasks.find(task => task.status === 'queued'); + if (queuedTask) { + nextTask = queuedTask; + taskAgent = agent; + break; // Process tasks in agent order + } + } + } + + if (!nextTask || !taskAgent) { + return; // No queued tasks found + } + + // Mark as processing + global.isProcessingTask = true; + nextTask.status = 'processing'; + + console.log(`[TASK QUEUE] Processing task ${nextTask.taskid} for agent ${taskAgent.agentid}`); + + // Process the task + processTask(taskAgent, nextTask) + .then((commandOutput) => { + console.log(`[TASK QUEUE] Task ${nextTask.taskid} completed with output:`, commandOutput); + // Detect error in output + if (typeof commandOutput === 'string' && commandOutput.trim().toLowerCase().startsWith('error')) { + nextTask.status = 'error'; + } else { + nextTask.status = 'completed'; + } + nextTask.output = commandOutput; // Save output to the task object + // Send result back to the agent window + const command_response = { + 'command': nextTask.command, + 'taskid': nextTask.taskid, + 'output': commandOutput, + 'status': nextTask.status + }; + const agentWindow = global.agentWindowHandles[taskAgent.agentid]; + if (agentWindow && !agentWindow.isDestroyed()) { + agentWindow.webContents.send('command-output', command_response); + } + }) + .catch((error) => { + console.error(`[TASK QUEUE] Error processing task ${nextTask.taskid}:`, error); + nextTask.status = 'error'; + nextTask.output = `Error: ${error.message}`; // Save error to the task object + const command_response = { + 'command': nextTask.command, + 'taskid': nextTask.taskid, + 'output': `Error: ${error.message}`, + 'status': nextTask.status + }; + + const agentWindow = global.agentWindowHandles[taskAgent.agentid]; + if (agentWindow && !agentWindow.isDestroyed()) { + agentWindow.webContents.send('command-output', command_response); + } + }) + .finally(() => { + // Mark as no longer processing + global.isProcessingTask = false; + console.log(`[TASK QUEUE] Finished processing task ${nextTask.taskid}`); + }); +} + +// Process individual task +async function processTask(agent, task) { + try { + // Ensure the command is included in the agent_object.tasks array as a string (legacy) or as an object with a command property + const agent_object = { + agentid: agent.agentid, + tasks: [typeof task === 'string' ? { command: task } : task], // Always pass as object with command property + // Include other necessary agent properties + container: agent.container, + key: agent.aes ? agent.aes.key : agent.key, + iv: agent.aes ? agent.aes.iv : agent.iv, + aes: agent.aes, + blobs: agent.blobs, + arch: agent.arch, + hostname: agent.hostname, + IP: agent.IP, + osRelease: agent.osRelease, + osType: agent.osType, + PID: agent.PID, + platform: agent.platform, + Process: agent.Process, + username: agent.username, + checkin: agent.checkin, + links: agent.links, + mode: agent.mode + }; + + let commandOutput = false; + while (commandOutput === false) { + commandOutput = await az.uploadCommand(agent_object); + if (commandOutput === false) { + await new Promise(resolve => setTimeout(resolve, 3000)); + } + } + return commandOutput; + } catch (error) { + throw error; + } +} + +// Start task queue processor +function startTaskQueue() { + console.log('[TASK QUEUE] Starting task queue processor...'); + setInterval(processTaskQueue, 500); // Run every 0.5 seconds +} \ No newline at end of file diff --git a/client/package.json b/client/package.json index cb3bd88..8e9dc22 100644 --- a/client/package.json +++ b/client/package.json @@ -1,14 +1,13 @@ { "name": "loki", - "version": "2.0.0", + "version": "2.4.0", "main": "./kernel.js", "scripts": { "start": "electron .", - "test": "echo \"Error: no test specified\" && exit 1", "pack": "electron-builder --dir", "dist": "electron-builder" }, - "keywords": ["C2","Electron","Red Team","adversary simulation","https://x.com/0xBoku","https://www.linkedin.com/in/bobby-cooke/"], + "keywords": ["C2","Electron","Red Team","Adversary Simulation","https://x.com/0xBoku","https://www.linkedin.com/in/bobby-cooke/"], "author": "Bobby Cooke", "homepage": "https://0xBoku.com", "license": "BSL", @@ -23,12 +22,11 @@ "copyright": "© 2025 Bobby Cooke", "asar": true, "files": [ - "kernel.js", "*.html", "*.js", "*.css", "assets/**", - "!node_modules/electron-builder/**" + "!node_modules/**" ], "mac": { "icon": "assets/mac/icon.icns", diff --git a/client/task-queue.html b/client/task-queue.html new file mode 100644 index 0000000..e52e811 --- /dev/null +++ b/client/task-queue.html @@ -0,0 +1,284 @@ + + + + + + Task Queue + + + +
+
+ Loading... + +
+
+
+ 0 + Queued +
+
+ 0 + Processing +
+
+ 0 + Completed +
+
+ 0 + Errors +
+
+
+ +
+ + + +
+ +
+
No tasks found
+
+ + + + \ No newline at end of file diff --git a/client/task-queue.js b/client/task-queue.js new file mode 100644 index 0000000..b289839 --- /dev/null +++ b/client/task-queue.js @@ -0,0 +1,357 @@ +const { ipcRenderer } = require('electron'); + +let currentAgent = null; +let allTasks = []; +let filteredTasks = []; +let currentFilter = 'all'; +let isEditing = false; +let expandedTaskId = null; // Track which task is expanded + +// Initialize when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + setupEventListeners(); + + // Auto-refresh every 2 seconds, but only if not editing + setInterval(() => { + if (!isEditing) { + refreshTasks(); + } + }, 2000); +}); + +// Listen for agent data from main process +ipcRenderer.on('agent-data', (event, agent) => { + currentAgent = agent; + updateAgentInfo(); + refreshTasks(); +}); + +// Listen for real-time task updates +ipcRenderer.on('task-removed', (event, taskId) => { + removeTaskFromList(taskId); + refreshDisplay(); +}); + +ipcRenderer.on('task-modified', (event, task) => { + updateTaskInList(task); + refreshDisplay(); +}); + +ipcRenderer.on('tasks-reordered', (event, tasks) => { + allTasks = tasks || []; + refreshDisplay(); +}); + +function setupEventListeners() { + // Filter change + document.getElementById('statusFilter').addEventListener('change', filterTasks); + + // Keyboard shortcuts + document.addEventListener('keydown', (event) => { + if (event.key === 'F5' || (event.ctrlKey && event.key === 'r')) { + event.preventDefault(); + refreshTasks(); + } + if (event.key === 'Escape') { + // Cancel any active editing + cancelAllEditing(); + } + }); +} + +function updateAgentInfo() { + if (!currentAgent) return; + + document.getElementById('agentId').textContent = currentAgent.agentid || 'Unknown'; + + const details = [ + currentAgent.hostname || 'Unknown Host', + currentAgent.username || 'Unknown User', + Array.isArray(currentAgent.IP) ? currentAgent.IP.join(', ') : (currentAgent.IP || 'Unknown IP') + ].join(' • '); + + document.getElementById('agentDetails').textContent = details; +} + +async function refreshTasks() { + if (!currentAgent) return; + + try { + const tasks = await ipcRenderer.invoke('get-agent-tasks', currentAgent.agentid); + allTasks = tasks || []; + refreshDisplay(); + } catch (error) { + console.error('Error fetching tasks:', error); + } +} + +function refreshDisplay() { + updateStats(); + filterTasks(); +} + +function updateStats() { + const stats = { + starting: 0, + queued: 0, + processing: 0, + completed: 0, + error: 0 + }; + + allTasks.forEach(task => { + if (stats.hasOwnProperty(task.status)) { + stats[task.status]++; + } + }); + + document.getElementById('queuedCount').textContent = stats.queued; + document.getElementById('processingCount').textContent = stats.processing; + document.getElementById('completedCount').textContent = stats.completed; + document.getElementById('errorCount').textContent = stats.error; +} + +function filterTasks() { + const filter = document.getElementById('statusFilter').value; + currentFilter = filter; + + if (filter === 'all') { + filteredTasks = [...allTasks]; + } else { + filteredTasks = allTasks.filter(task => task.status === filter); + } + + renderTasks(); +} + +function renderTasks() { + const taskList = document.getElementById('taskList'); + + if (filteredTasks.length === 0) { + taskList.innerHTML = '
No tasks found
'; + return; + } + + taskList.innerHTML = ''; + + filteredTasks.forEach((task, index) => { + const taskElement = createTaskElement(task, index); + taskList.appendChild(taskElement); + // Add click handler for completed or error tasks to expand/collapse + if (task.status === 'completed' || task.status === 'error') { + taskElement.addEventListener('click', (e) => { + // Only expand/collapse if not clicking a button + if (e.target.classList.contains('action-btn')) return; + if (expandedTaskId === task.taskid) { + expandedTaskId = null; + } else { + expandedTaskId = task.taskid; + } + renderTasks(); + }); + } + }); +} + +function createTaskElement(task, index) { + const taskItem = document.createElement('div'); + taskItem.className = `task-item ${task.status === 'processing' ? 'processing' : ''}`; + taskItem.dataset.taskId = task.taskid; + + const queuePosition = currentFilter === 'queued' ? index + 1 : null; + + let outputSection = ''; + if ((task.status === 'completed' || task.status === 'error') && expandedTaskId === task.taskid) { + outputSection = `
${escapeHtml(task.output || (task.status === 'error' ? 'No error output' : 'No output'))}
`; + } + + taskItem.innerHTML = ` + ${queuePosition ? `
${queuePosition}
` : ''} +
+
${escapeHtml(task.command)}
+
+ ${task.status} + ID: ${task.taskid} + Output: ${task.outputChannel || 'N/A'} + Upload: ${task.uploadChannel || 'N/A'} +
+ ${outputSection} +
+
+ ${createActionButtons(task)} +
+ `; + + return taskItem; +} + +function createActionButtons(task) { + const isProcessing = task.status === 'processing'; + const isCompleted = task.status === 'completed'; + const isError = task.status === 'error'; + const isQueued = task.status === 'queued'; + const canModify = !isProcessing && !isCompleted && !isError; + let buttons = ''; + + // No edit or remove for error tasks + if (isError) { + return buttons; + } + + if (canModify) { + buttons += ``; + } + + // For queued tasks, show a red X for removal (no prompt) + if (isQueued) { + buttons += ``; + } else if (canModify) { + // For other modifiable tasks (shouldn't happen, but fallback) + buttons += ``; + } + + if (isProcessing) { + buttons += ``; + } + + return buttons; +} + +async function editTask(taskId) { + const task = allTasks.find(t => t.taskid === taskId); + if (!task) return; + + isEditing = true; + + const commandElement = document.getElementById(`command-${taskId}`); + const originalCommand = task.command; + + // Create input field + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'edit-input'; + input.value = originalCommand; + + // Replace command text with input + commandElement.innerHTML = ''; + commandElement.appendChild(input); + input.focus(); + input.select(); + + // Handle save/cancel + const saveEdit = async () => { + const newCommand = input.value.trim(); + if (newCommand && newCommand !== originalCommand) { + try { + const result = await ipcRenderer.invoke('modify-task', currentAgent.agentid, taskId, newCommand); + if (result.success) { + task.command = newCommand; + commandElement.textContent = newCommand; + } else { + alert(`Error: ${result.message}`); + commandElement.textContent = originalCommand; + } + } catch (error) { + console.error('Error modifying task:', error); + alert('Error modifying task'); + commandElement.textContent = originalCommand; + } + } else { + commandElement.textContent = originalCommand; + } + isEditing = false; + }; + + const cancelEdit = () => { + commandElement.textContent = originalCommand; + isEditing = false; + }; + + input.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + saveEdit(); + } else if (event.key === 'Escape') { + event.preventDefault(); + cancelEdit(); + } + }); + + input.addEventListener('blur', saveEdit); +} + +// Remove task immediately, no prompt +async function removeTaskNoPrompt(taskId) { + try { + const result = await ipcRenderer.invoke('remove-task', currentAgent.agentid, taskId); + if (result.success) { + removeTaskFromList(taskId); + refreshDisplay(); + } else { + alert(`Error: ${result.message}`); + } + } catch (error) { + console.error('Error removing task:', error); + alert('Error removing task'); + } +} + +async function moveTask(taskId, direction) { + try { + const result = await ipcRenderer.invoke('reorder-task', currentAgent.agentid, taskId, direction); + if (!result.success) { + alert(`Error: ${result.message}`); + } + // The tasks-reordered event will handle the UI update + } catch (error) { + console.error('Error moving task:', error); + alert('Error moving task'); + } +} + +// Remove completed and error tasks +async function clearCompleted() { + const completedOrErrorTasks = allTasks.filter(task => task.status === 'completed' || task.status === 'error'); + if (completedOrErrorTasks.length === 0) { + alert('No completed or error tasks to clear'); + return; + } + if (!confirm(`Are you sure you want to remove ${completedOrErrorTasks.length} completed/error task(s)?`)) { + return; + } + try { + for (const task of completedOrErrorTasks) { + await ipcRenderer.invoke('remove-task', currentAgent.agentid, task.taskid); + } + refreshTasks(); + } catch (error) { + console.error('Error clearing completed/error tasks:', error); + alert('Error clearing completed/error tasks'); + } +} + +function removeTaskFromList(taskId) { + allTasks = allTasks.filter(task => task.taskid !== taskId); +} + +function updateTaskInList(updatedTask) { + const index = allTasks.findIndex(task => task.taskid === updatedTask.taskid); + if (index !== -1) { + allTasks[index] = updatedTask; + } +} + +function cancelAllEditing() { + if (isEditing) { + const activeInput = document.querySelector('.edit-input'); + if (activeInput) { + activeInput.dispatchEvent(new Event('blur')); + } + } +} + +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} \ No newline at end of file