typescript-virtual-container 1.1.4 → 1.1.5
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/CHANGELOG.md +42 -0
- package/HONEYPOT.md +358 -0
- package/README.md +471 -16
- package/dist/SSHMimic/index.d.ts +2 -1
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +12 -1
- package/dist/SSHMimic/sftp.d.ts +3 -1
- package/dist/SSHMimic/sftp.d.ts.map +1 -1
- package/dist/SSHMimic/sftp.js +20 -1
- package/dist/VirtualFileSystem/index.d.ts +2 -1
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +8 -1
- package/dist/VirtualShell/index.d.ts +2 -1
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +6 -1
- package/dist/VirtualUserManager/index.d.ts +2 -1
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +19 -1
- package/dist/honeypot.d.ts +132 -0
- package/dist/honeypot.d.ts.map +1 -0
- package/dist/honeypot.js +289 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/examples/README.md +210 -0
- package/examples/honeypot-audit.ts +180 -0
- package/examples/honeypot-export.ts +253 -0
- package/examples/honeypot-quickstart.ts +110 -0
- package/package.json +1 -1
- package/src/Honeypot/index.ts +422 -0
- package/src/SSHMimic/index.ts +13 -1
- package/src/SSHMimic/sftp.ts +21 -1
- package/src/VirtualFileSystem/index.ts +8 -1
- package/src/VirtualShell/index.ts +6 -1
- package/src/VirtualUserManager/index.ts +21 -3
- package/src/index.ts +6 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Honeypot tracking and auditing module for virtual shell events.
|
|
3
|
+
*
|
|
4
|
+
* Attaches listeners to VirtualShell, VirtualFileSystem, VirtualUserManager,
|
|
5
|
+
* SshMimic, and SftpMimic instances to log all activity for security auditing,
|
|
6
|
+
* anomaly detection, and forensic analysis.
|
|
7
|
+
*
|
|
8
|
+
* @module honeypot
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { EventEmitter } from "node:events";
|
|
12
|
+
import type { SshMimic } from "../SSHMimic";
|
|
13
|
+
import type { SftpMimic } from "../SSHMimic/sftp";
|
|
14
|
+
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
15
|
+
import type { VirtualShell } from "../VirtualShell";
|
|
16
|
+
import type { VirtualUserManager } from "../VirtualUserManager";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Audit log entry recorded for each event.
|
|
20
|
+
*/
|
|
21
|
+
export interface AuditLogEntry {
|
|
22
|
+
timestamp: string;
|
|
23
|
+
type: string;
|
|
24
|
+
source: string;
|
|
25
|
+
details: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Statistics tracker for honeypot activity.
|
|
30
|
+
*/
|
|
31
|
+
export interface HoneyPotStats {
|
|
32
|
+
authAttempts: number;
|
|
33
|
+
authSuccesses: number;
|
|
34
|
+
authFailures: number;
|
|
35
|
+
commands: number;
|
|
36
|
+
fileWrites: number;
|
|
37
|
+
fileReads: number;
|
|
38
|
+
sessionStarts: number;
|
|
39
|
+
sessionEnds: number;
|
|
40
|
+
userCreated: number;
|
|
41
|
+
userDeleted: number;
|
|
42
|
+
clientConnects: number;
|
|
43
|
+
clientDisconnects: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* HoneyPot audit and event tracking utility.
|
|
48
|
+
*
|
|
49
|
+
* Singleton-like helper that attaches listeners to virtual shell components
|
|
50
|
+
* and maintains an audit log of all activity.
|
|
51
|
+
*/
|
|
52
|
+
export class HoneyPot {
|
|
53
|
+
private auditLog: AuditLogEntry[] = [];
|
|
54
|
+
private stats: HoneyPotStats = {
|
|
55
|
+
authAttempts: 0,
|
|
56
|
+
authSuccesses: 0,
|
|
57
|
+
authFailures: 0,
|
|
58
|
+
commands: 0,
|
|
59
|
+
fileWrites: 0,
|
|
60
|
+
fileReads: 0,
|
|
61
|
+
sessionStarts: 0,
|
|
62
|
+
sessionEnds: 0,
|
|
63
|
+
userCreated: 0,
|
|
64
|
+
userDeleted: 0,
|
|
65
|
+
clientConnects: 0,
|
|
66
|
+
clientDisconnects: 0,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
private maxLogSize: number;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Creates a new HoneyPot instance.
|
|
73
|
+
*
|
|
74
|
+
* @param maxLogSize Maximum audit log entries to retain (default: 10000).
|
|
75
|
+
*/
|
|
76
|
+
constructor(maxLogSize: number = 10000) {
|
|
77
|
+
this.maxLogSize = maxLogSize;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Attaches honeypot listeners to all provided event emitters.
|
|
82
|
+
*
|
|
83
|
+
* @param shell VirtualShell instance.
|
|
84
|
+
* @param vfs VirtualFileSystem instance.
|
|
85
|
+
* @param users VirtualUserManager instance.
|
|
86
|
+
* @param ssh SshMimic instance (optional).
|
|
87
|
+
* @param sftp SftpMimic instance (optional).
|
|
88
|
+
*/
|
|
89
|
+
public attach(
|
|
90
|
+
shell: VirtualShell,
|
|
91
|
+
vfs: VirtualFileSystem,
|
|
92
|
+
users: VirtualUserManager,
|
|
93
|
+
ssh?: SshMimic,
|
|
94
|
+
sftp?: SftpMimic,
|
|
95
|
+
): void {
|
|
96
|
+
this.attachVirtualShell(shell);
|
|
97
|
+
this.attachVirtualFileSystem(vfs);
|
|
98
|
+
this.attachVirtualUserManager(users);
|
|
99
|
+
if (ssh) {
|
|
100
|
+
this.attachSshMimic(ssh);
|
|
101
|
+
}
|
|
102
|
+
if (sftp) {
|
|
103
|
+
this.attachSftpMimic(sftp);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Attaches to VirtualShell events.
|
|
109
|
+
*/
|
|
110
|
+
private attachVirtualShell(shell: VirtualShell): void {
|
|
111
|
+
(shell as EventEmitter).on("initialized", () => {
|
|
112
|
+
this.log("VirtualShell", "initialized", {});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
(shell as EventEmitter).on("command", (data: Record<string, unknown>) => {
|
|
116
|
+
this.stats.commands++;
|
|
117
|
+
this.log("VirtualShell", "command", data);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
(shell as EventEmitter).on(
|
|
121
|
+
"session:start",
|
|
122
|
+
(data: Record<string, unknown>) => {
|
|
123
|
+
this.stats.sessionStarts++;
|
|
124
|
+
this.log("VirtualShell", "session:start", data);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Attaches to VirtualFileSystem events.
|
|
131
|
+
*/
|
|
132
|
+
private attachVirtualFileSystem(vfs: VirtualFileSystem): void {
|
|
133
|
+
(vfs as EventEmitter).on("file:read", (data: Record<string, unknown>) => {
|
|
134
|
+
this.stats.fileReads++;
|
|
135
|
+
this.log("VirtualFileSystem", "file:read", data);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
(vfs as EventEmitter).on("file:write", (data: Record<string, unknown>) => {
|
|
139
|
+
this.stats.fileWrites++;
|
|
140
|
+
this.log("VirtualFileSystem", "file:write", data);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
(vfs as EventEmitter).on("dir:create", (data: Record<string, unknown>) => {
|
|
144
|
+
this.log("VirtualFileSystem", "dir:create", data);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
(vfs as EventEmitter).on("mirror:flush", () => {
|
|
148
|
+
this.log("VirtualFileSystem", "mirror:flush", {});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Attaches to VirtualUserManager events.
|
|
154
|
+
*/
|
|
155
|
+
private attachVirtualUserManager(users: VirtualUserManager): void {
|
|
156
|
+
(users as EventEmitter).on("initialized", () => {
|
|
157
|
+
this.log("VirtualUserManager", "initialized", {});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
(users as EventEmitter).on("user:add", (data: Record<string, unknown>) => {
|
|
161
|
+
this.stats.userCreated++;
|
|
162
|
+
this.log("VirtualUserManager", "user:add", data);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
(users as EventEmitter).on(
|
|
166
|
+
"user:delete",
|
|
167
|
+
(data: Record<string, unknown>) => {
|
|
168
|
+
this.stats.userDeleted++;
|
|
169
|
+
this.log("VirtualUserManager", "user:delete", data);
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
(users as EventEmitter).on(
|
|
174
|
+
"session:register",
|
|
175
|
+
(data: Record<string, unknown>) => {
|
|
176
|
+
this.log("VirtualUserManager", "session:register", data);
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
(users as EventEmitter).on(
|
|
181
|
+
"session:unregister",
|
|
182
|
+
(data: Record<string, unknown>) => {
|
|
183
|
+
this.stats.sessionEnds++;
|
|
184
|
+
this.log("VirtualUserManager", "session:unregister", data);
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Attaches to SshMimic events.
|
|
191
|
+
*/
|
|
192
|
+
private attachSshMimic(ssh: SshMimic): void {
|
|
193
|
+
(ssh as EventEmitter).on("start", (data: Record<string, unknown>) => {
|
|
194
|
+
this.log("SshMimic", "start", data);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
(ssh as EventEmitter).on("stop", () => {
|
|
198
|
+
this.log("SshMimic", "stop", {});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
(ssh as EventEmitter).on(
|
|
202
|
+
"auth:success",
|
|
203
|
+
(data: Record<string, unknown>) => {
|
|
204
|
+
this.stats.authAttempts++;
|
|
205
|
+
this.stats.authSuccesses++;
|
|
206
|
+
this.log("SshMimic", "auth:success", data);
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
(ssh as EventEmitter).on(
|
|
211
|
+
"auth:failure",
|
|
212
|
+
(data: Record<string, unknown>) => {
|
|
213
|
+
this.stats.authAttempts++;
|
|
214
|
+
this.stats.authFailures++;
|
|
215
|
+
this.log("SshMimic", "auth:failure", data);
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
(ssh as EventEmitter).on("client:connect", () => {
|
|
220
|
+
this.stats.clientConnects++;
|
|
221
|
+
this.log("SshMimic", "client:connect", {});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
(ssh as EventEmitter).on(
|
|
225
|
+
"client:disconnect",
|
|
226
|
+
(data: Record<string, unknown>) => {
|
|
227
|
+
this.stats.clientDisconnects++;
|
|
228
|
+
this.log("SshMimic", "client:disconnect", data);
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Attaches to SftpMimic events.
|
|
235
|
+
*/
|
|
236
|
+
private attachSftpMimic(sftp: SftpMimic): void {
|
|
237
|
+
(sftp as EventEmitter).on("start", (data: Record<string, unknown>) => {
|
|
238
|
+
this.log("SftpMimic", "start", data);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
(sftp as EventEmitter).on("stop", () => {
|
|
242
|
+
this.log("SftpMimic", "stop", {});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
(sftp as EventEmitter).on(
|
|
246
|
+
"auth:success",
|
|
247
|
+
(data: Record<string, unknown>) => {
|
|
248
|
+
this.stats.authAttempts++;
|
|
249
|
+
this.stats.authSuccesses++;
|
|
250
|
+
this.log("SftpMimic", "auth:success", data);
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
(sftp as EventEmitter).on(
|
|
255
|
+
"auth:failure",
|
|
256
|
+
(data: Record<string, unknown>) => {
|
|
257
|
+
this.stats.authAttempts++;
|
|
258
|
+
this.stats.authFailures++;
|
|
259
|
+
this.log("SftpMimic", "auth:failure", data);
|
|
260
|
+
},
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
(sftp as EventEmitter).on("client:connect", () => {
|
|
264
|
+
this.stats.clientConnects++;
|
|
265
|
+
this.log("SftpMimic", "client:connect", {});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
(sftp as EventEmitter).on(
|
|
269
|
+
"client:disconnect",
|
|
270
|
+
(data: Record<string, unknown>) => {
|
|
271
|
+
this.stats.clientDisconnects++;
|
|
272
|
+
this.log("SftpMimic", "client:disconnect", data);
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Records an audit log entry.
|
|
279
|
+
*
|
|
280
|
+
* @param source Event source (e.g., "SshMimic", "VirtualFileSystem").
|
|
281
|
+
* @param type Event type.
|
|
282
|
+
* @param details Event-specific data.
|
|
283
|
+
*/
|
|
284
|
+
private log(
|
|
285
|
+
source: string,
|
|
286
|
+
type: string,
|
|
287
|
+
details: Record<string, unknown>,
|
|
288
|
+
): void {
|
|
289
|
+
const entry: AuditLogEntry = {
|
|
290
|
+
timestamp: new Date().toISOString(),
|
|
291
|
+
type,
|
|
292
|
+
source,
|
|
293
|
+
details,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
this.auditLog.push(entry);
|
|
297
|
+
|
|
298
|
+
// Trim log if exceeds max size
|
|
299
|
+
if (this.auditLog.length > this.maxLogSize) {
|
|
300
|
+
this.auditLog = this.auditLog.slice(-this.maxLogSize);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Console output for real-time monitoring
|
|
304
|
+
console.log(`[AUDIT] ${entry.timestamp} | ${source} | ${type}`, details);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Returns audit log entries matching optional filters.
|
|
309
|
+
*
|
|
310
|
+
* @param type Optional event type filter.
|
|
311
|
+
* @param source Optional source filter.
|
|
312
|
+
* @returns Filtered audit log entries.
|
|
313
|
+
*/
|
|
314
|
+
public getAuditLog(type?: string, source?: string): AuditLogEntry[] {
|
|
315
|
+
return this.auditLog.filter(
|
|
316
|
+
(entry) =>
|
|
317
|
+
(!type || entry.type === type) && (!source || entry.source === source),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Returns current activity statistics.
|
|
323
|
+
*
|
|
324
|
+
* @returns Snapshot of honeypot stats.
|
|
325
|
+
*/
|
|
326
|
+
public getStats(): Readonly<HoneyPotStats> {
|
|
327
|
+
return Object.freeze({ ...this.stats });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Clears audit log and resets statistics.
|
|
332
|
+
*/
|
|
333
|
+
public reset(): void {
|
|
334
|
+
this.auditLog = [];
|
|
335
|
+
this.stats = {
|
|
336
|
+
authAttempts: 0,
|
|
337
|
+
authSuccesses: 0,
|
|
338
|
+
authFailures: 0,
|
|
339
|
+
commands: 0,
|
|
340
|
+
fileWrites: 0,
|
|
341
|
+
fileReads: 0,
|
|
342
|
+
sessionStarts: 0,
|
|
343
|
+
sessionEnds: 0,
|
|
344
|
+
userCreated: 0,
|
|
345
|
+
userDeleted: 0,
|
|
346
|
+
clientConnects: 0,
|
|
347
|
+
clientDisconnects: 0,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Returns recent log entries in reverse chronological order.
|
|
353
|
+
*
|
|
354
|
+
* @param limit Number of recent entries to return (default: 100).
|
|
355
|
+
* @returns Recent audit log entries.
|
|
356
|
+
*/
|
|
357
|
+
public getRecent(limit: number = 100): AuditLogEntry[] {
|
|
358
|
+
return this.auditLog.slice(Math.max(0, this.auditLog.length - limit));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Detects potential security issues based on activity patterns.
|
|
363
|
+
*
|
|
364
|
+
* @returns Array of anomalies detected.
|
|
365
|
+
*/
|
|
366
|
+
public detectAnomalies(): Array<{
|
|
367
|
+
type: string;
|
|
368
|
+
severity: "low" | "medium" | "high";
|
|
369
|
+
message: string;
|
|
370
|
+
}> {
|
|
371
|
+
const anomalies: Array<{
|
|
372
|
+
type: string;
|
|
373
|
+
severity: "low" | "medium" | "high";
|
|
374
|
+
message: string;
|
|
375
|
+
}> = [];
|
|
376
|
+
|
|
377
|
+
// High auth failure rate
|
|
378
|
+
if (
|
|
379
|
+
this.stats.authAttempts > 0 &&
|
|
380
|
+
this.stats.authFailures / this.stats.authAttempts > 0.5
|
|
381
|
+
) {
|
|
382
|
+
anomalies.push({
|
|
383
|
+
type: "high_auth_failure_rate",
|
|
384
|
+
severity: "medium",
|
|
385
|
+
message: `Auth failure rate: ${(
|
|
386
|
+
(this.stats.authFailures / this.stats.authAttempts) * 100
|
|
387
|
+
).toFixed(1)}%`,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Excessive auth failures in short time
|
|
392
|
+
if (this.stats.authFailures > 10) {
|
|
393
|
+
anomalies.push({
|
|
394
|
+
type: "excessive_auth_failures",
|
|
395
|
+
severity: "high",
|
|
396
|
+
message: `${this.stats.authFailures} authentication failures detected`,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Unusual command execution volume
|
|
401
|
+
if (this.stats.commands > 1000) {
|
|
402
|
+
anomalies.push({
|
|
403
|
+
type: "high_command_volume",
|
|
404
|
+
severity: "low",
|
|
405
|
+
message: `${this.stats.commands} commands executed`,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Unusual file write volume
|
|
410
|
+
if (this.stats.fileWrites > 500) {
|
|
411
|
+
anomalies.push({
|
|
412
|
+
type: "high_write_volume",
|
|
413
|
+
severity: "medium",
|
|
414
|
+
message: `${this.stats.fileWrites} file write operations`,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return anomalies;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export default HoneyPot;
|
package/src/SSHMimic/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
1
2
|
import { Server as SshServer } from "ssh2";
|
|
2
3
|
import { VirtualShell } from "../VirtualShell";
|
|
3
4
|
import { runExec } from "./exec";
|
|
@@ -10,7 +11,7 @@ import { loadOrCreateHostKey } from "./hostKey";
|
|
|
10
11
|
* Create an instance, call {@link SshMimic.start}, and stop it with
|
|
11
12
|
* {@link SshMimic.stop} when your process exits.
|
|
12
13
|
*/
|
|
13
|
-
class SshMimic {
|
|
14
|
+
class SshMimic extends EventEmitter {
|
|
14
15
|
port: number;
|
|
15
16
|
server: SshServer | null;
|
|
16
17
|
private shell: VirtualShell;
|
|
@@ -32,6 +33,7 @@ class SshMimic {
|
|
|
32
33
|
hostname?: string;
|
|
33
34
|
shell?: VirtualShell;
|
|
34
35
|
}) {
|
|
36
|
+
super();
|
|
35
37
|
this.port = port;
|
|
36
38
|
this.shellHostname = hostname;
|
|
37
39
|
this.server = null;
|
|
@@ -60,6 +62,8 @@ class SshMimic {
|
|
|
60
62
|
let remoteAddress = "unknown";
|
|
61
63
|
let sessionId: string | null = null;
|
|
62
64
|
|
|
65
|
+
this.emit("client:connect");
|
|
66
|
+
|
|
63
67
|
client.on("authentication", (ctx) => {
|
|
64
68
|
shell;
|
|
65
69
|
if (ctx.method === "password") {
|
|
@@ -69,12 +73,17 @@ class SshMimic {
|
|
|
69
73
|
if (
|
|
70
74
|
!shell.users.verifyPassword(candidateUser, ctx.password ?? "")
|
|
71
75
|
) {
|
|
76
|
+
this.emit("auth:failure", {
|
|
77
|
+
username: candidateUser,
|
|
78
|
+
remoteAddress,
|
|
79
|
+
});
|
|
72
80
|
ctx.reject();
|
|
73
81
|
return;
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
authUser = candidateUser;
|
|
77
85
|
sessionId = shell.users.registerSession(authUser, remoteAddress).id;
|
|
86
|
+
this.emit("auth:success", { username: authUser, remoteAddress });
|
|
78
87
|
|
|
79
88
|
const homePath = `/home/${authUser}`;
|
|
80
89
|
if (!shell.vfs.exists(homePath)) {
|
|
@@ -95,6 +104,7 @@ class SshMimic {
|
|
|
95
104
|
|
|
96
105
|
client.on("close", () => {
|
|
97
106
|
shell.users.unregisterSession(sessionId);
|
|
107
|
+
this.emit("client:disconnect", { user: authUser });
|
|
98
108
|
sessionId = null;
|
|
99
109
|
});
|
|
100
110
|
|
|
@@ -149,6 +159,7 @@ class SshMimic {
|
|
|
149
159
|
this.server?.once("error", (err: unknown) => reject(err));
|
|
150
160
|
this.server?.listen(this.port, "0.0.0.0", () => {
|
|
151
161
|
console.log(`SSH Mimic listening on port ${this.port}`);
|
|
162
|
+
this.emit("start", { port: this.port });
|
|
152
163
|
resolve(this.port);
|
|
153
164
|
});
|
|
154
165
|
});
|
|
@@ -161,6 +172,7 @@ class SshMimic {
|
|
|
161
172
|
if (this.server) {
|
|
162
173
|
this.server.close(() => {
|
|
163
174
|
console.log("SSH Mimic stopped");
|
|
175
|
+
this.emit("stop");
|
|
164
176
|
});
|
|
165
177
|
}
|
|
166
178
|
}
|
package/src/SSHMimic/sftp.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** biome-ignore-all lint/style/useNamingConvention: const as enum */
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { AuthenticationType, KeyboardAuthContext } from "ssh2";
|
|
4
5
|
import { Server as SshServer } from "ssh2";
|
|
@@ -135,7 +136,7 @@ export interface SftpMimicOptions {
|
|
|
135
136
|
users?: VirtualUserManager;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
export class SftpMimic {
|
|
139
|
+
export class SftpMimic extends EventEmitter {
|
|
139
140
|
port: number;
|
|
140
141
|
server: SshServer | null;
|
|
141
142
|
private readonly hostname: string;
|
|
@@ -152,6 +153,7 @@ export class SftpMimic {
|
|
|
152
153
|
vfs,
|
|
153
154
|
users,
|
|
154
155
|
}: SftpMimicOptions) {
|
|
156
|
+
super();
|
|
155
157
|
this.port = port;
|
|
156
158
|
this.server = null;
|
|
157
159
|
this.hostname = hostname;
|
|
@@ -206,6 +208,8 @@ export class SftpMimic {
|
|
|
206
208
|
let sessionId: string | null = null;
|
|
207
209
|
let remoteAddress = "unknown";
|
|
208
210
|
|
|
211
|
+
this.emit("client:connect");
|
|
212
|
+
|
|
209
213
|
// Add error handling for the client
|
|
210
214
|
client.on("error", (error: unknown) => {
|
|
211
215
|
console.error(`[SFTP] Client error:`, error);
|
|
@@ -246,11 +250,16 @@ export class SftpMimic {
|
|
|
246
250
|
if (
|
|
247
251
|
!this.getUsers().verifyPassword(candidateUser, ctx.password ?? "")
|
|
248
252
|
) {
|
|
253
|
+
this.emit("auth:failure", {
|
|
254
|
+
username: candidateUser,
|
|
255
|
+
remoteAddress,
|
|
256
|
+
});
|
|
249
257
|
ctx.reject(allowedAuthMethods);
|
|
250
258
|
return;
|
|
251
259
|
}
|
|
252
260
|
|
|
253
261
|
acceptSession(candidateUser);
|
|
262
|
+
this.emit("auth:success", { username: authUser, remoteAddress });
|
|
254
263
|
ctx.accept();
|
|
255
264
|
return;
|
|
256
265
|
}
|
|
@@ -262,11 +271,19 @@ export class SftpMimic {
|
|
|
262
271
|
(answers) => {
|
|
263
272
|
const password = answers[0] ?? "";
|
|
264
273
|
if (!this.getUsers().verifyPassword(candidateUser, password)) {
|
|
274
|
+
this.emit("auth:failure", {
|
|
275
|
+
username: candidateUser,
|
|
276
|
+
remoteAddress,
|
|
277
|
+
});
|
|
265
278
|
keyboardCtx.reject(allowedAuthMethods);
|
|
266
279
|
return;
|
|
267
280
|
}
|
|
268
281
|
|
|
269
282
|
acceptSession(candidateUser);
|
|
283
|
+
this.emit("auth:success", {
|
|
284
|
+
username: authUser,
|
|
285
|
+
remoteAddress,
|
|
286
|
+
});
|
|
270
287
|
keyboardCtx.accept();
|
|
271
288
|
},
|
|
272
289
|
);
|
|
@@ -278,6 +295,7 @@ export class SftpMimic {
|
|
|
278
295
|
|
|
279
296
|
client.on("close", () => {
|
|
280
297
|
this.getUsers().unregisterSession(sessionId);
|
|
298
|
+
this.emit("client:disconnect", { user: authUser });
|
|
281
299
|
sessionId = null;
|
|
282
300
|
});
|
|
283
301
|
|
|
@@ -311,6 +329,7 @@ export class SftpMimic {
|
|
|
311
329
|
? address.port
|
|
312
330
|
: this.port;
|
|
313
331
|
console.log(`SFTP Mimic listening on port ${actualPort}`);
|
|
332
|
+
this.emit("start", { port: actualPort });
|
|
314
333
|
resolve(actualPort as number);
|
|
315
334
|
});
|
|
316
335
|
});
|
|
@@ -320,6 +339,7 @@ export class SftpMimic {
|
|
|
320
339
|
if (this.server) {
|
|
321
340
|
this.server.close(() => {
|
|
322
341
|
console.log("SFTP Mimic stopped");
|
|
342
|
+
this.emit("stop");
|
|
323
343
|
});
|
|
324
344
|
}
|
|
325
345
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
@@ -15,7 +16,7 @@ import { normalizePath } from "./path";
|
|
|
15
16
|
* {@link VirtualFileSystem.restoreMirror} on startup and
|
|
16
17
|
* {@link VirtualFileSystem.flushMirror} to persist pending changes.
|
|
17
18
|
*/
|
|
18
|
-
class VirtualFileSystem {
|
|
19
|
+
class VirtualFileSystem extends EventEmitter {
|
|
19
20
|
private readonly mirrorRoot: string;
|
|
20
21
|
|
|
21
22
|
private ensureMirrorRoot(): void {
|
|
@@ -93,6 +94,7 @@ class VirtualFileSystem {
|
|
|
93
94
|
* @param baseDir Base directory used to resolve mirror archive location.
|
|
94
95
|
*/
|
|
95
96
|
constructor(baseDir: string = process.cwd()) {
|
|
97
|
+
super();
|
|
96
98
|
this.mirrorRoot = path.resolve(baseDir, ".vfs", "mirror");
|
|
97
99
|
}
|
|
98
100
|
|
|
@@ -112,6 +114,7 @@ class VirtualFileSystem {
|
|
|
112
114
|
*/
|
|
113
115
|
public async flushMirror(): Promise<void> {
|
|
114
116
|
this.ensureMirrorRoot();
|
|
117
|
+
this.emit("mirror:flush");
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/**
|
|
@@ -129,6 +132,7 @@ class VirtualFileSystem {
|
|
|
129
132
|
);
|
|
130
133
|
}
|
|
131
134
|
fs.mkdirSync(fsPath, { recursive: true, mode });
|
|
135
|
+
this.emit("dir:create", { path: normalizePath(targetPath), mode });
|
|
132
136
|
}
|
|
133
137
|
|
|
134
138
|
/**
|
|
@@ -165,6 +169,7 @@ class VirtualFileSystem {
|
|
|
165
169
|
|
|
166
170
|
fs.writeFileSync(fsPath, storedContent);
|
|
167
171
|
fs.chmodSync(fsPath, options.mode ?? 0o644);
|
|
172
|
+
this.emit("file:write", { path: normalized, size: storedContent.length });
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
/**
|
|
@@ -184,6 +189,8 @@ class VirtualFileSystem {
|
|
|
184
189
|
|
|
185
190
|
const stored = fs.readFileSync(fsPath);
|
|
186
191
|
const raw = this.detectGzipFile(fsPath) ? gunzipSync(stored) : stored;
|
|
192
|
+
const normalized = normalizePath(targetPath);
|
|
193
|
+
this.emit("file:read", { path: normalized, size: raw.length });
|
|
187
194
|
return raw.toString("utf8");
|
|
188
195
|
}
|
|
189
196
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
2
3
|
import { createCustomCommand, registerCommand, runCommand } from "../commands";
|
|
3
4
|
import type { CommandContext, CommandResult } from "../types/commands";
|
|
4
5
|
import type { ShellStream } from "../types/streams";
|
|
@@ -46,7 +47,7 @@ function resolveAutoSudoForNewUsers(): boolean {
|
|
|
46
47
|
* Instances are used both by the SSH server facade and by the programmatic
|
|
47
48
|
* client API.
|
|
48
49
|
*/
|
|
49
|
-
class VirtualShell {
|
|
50
|
+
class VirtualShell extends EventEmitter {
|
|
50
51
|
basePath: string = ".";
|
|
51
52
|
vfs: VirtualFileSystem;
|
|
52
53
|
users: VirtualUserManager;
|
|
@@ -66,6 +67,7 @@ class VirtualShell {
|
|
|
66
67
|
properties?: ShellProperties,
|
|
67
68
|
basePath?: string,
|
|
68
69
|
) {
|
|
70
|
+
super();
|
|
69
71
|
this.hostname = hostname;
|
|
70
72
|
this.properties = properties || defaultShellProperties;
|
|
71
73
|
this.basePath = basePath || ".";
|
|
@@ -84,6 +86,7 @@ class VirtualShell {
|
|
|
84
86
|
this.initialized = (async () => {
|
|
85
87
|
await vfs.restoreMirror();
|
|
86
88
|
await users.initialize();
|
|
89
|
+
this.emit("initialized");
|
|
87
90
|
})();
|
|
88
91
|
}
|
|
89
92
|
|
|
@@ -124,6 +127,7 @@ class VirtualShell {
|
|
|
124
127
|
*/
|
|
125
128
|
executeCommand(rawInput: string, authUser: string, cwd: string): void {
|
|
126
129
|
runCommand(rawInput, authUser, this.hostname, "shell", cwd, this);
|
|
130
|
+
this.emit("command", { command: rawInput, user: authUser, cwd });
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
/**
|
|
@@ -143,6 +147,7 @@ class VirtualShell {
|
|
|
143
147
|
terminalSize: { cols: number; rows: number },
|
|
144
148
|
): void {
|
|
145
149
|
// Interactive shell logic
|
|
150
|
+
this.emit("session:start", { user: authUser, sessionId, remoteAddress });
|
|
146
151
|
startShell(
|
|
147
152
|
this.properties,
|
|
148
153
|
stream,
|