typescript-virtual-container 1.1.4 → 1.1.6

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 (39) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/HONEYPOT.md +358 -0
  3. package/README.md +471 -16
  4. package/dist/Honeypot/index.d.ts +132 -0
  5. package/dist/Honeypot/index.d.ts.map +1 -0
  6. package/dist/Honeypot/index.js +289 -0
  7. package/dist/SSHMimic/index.d.ts +2 -1
  8. package/dist/SSHMimic/index.d.ts.map +1 -1
  9. package/dist/SSHMimic/index.js +12 -1
  10. package/dist/SSHMimic/sftp.d.ts +3 -1
  11. package/dist/SSHMimic/sftp.d.ts.map +1 -1
  12. package/dist/SSHMimic/sftp.js +20 -1
  13. package/dist/VirtualFileSystem/index.d.ts +2 -1
  14. package/dist/VirtualFileSystem/index.d.ts.map +1 -1
  15. package/dist/VirtualFileSystem/index.js +8 -1
  16. package/dist/VirtualShell/index.d.ts +2 -1
  17. package/dist/VirtualShell/index.d.ts.map +1 -1
  18. package/dist/VirtualShell/index.js +6 -1
  19. package/dist/VirtualUserManager/index.d.ts +2 -1
  20. package/dist/VirtualUserManager/index.d.ts.map +1 -1
  21. package/dist/VirtualUserManager/index.js +19 -1
  22. package/dist/honeypot.d.ts +132 -0
  23. package/dist/honeypot.d.ts.map +1 -0
  24. package/dist/honeypot.js +289 -0
  25. package/dist/index.d.ts +3 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -1
  28. package/examples/README.md +210 -0
  29. package/examples/honeypot-audit.ts +180 -0
  30. package/examples/honeypot-export.ts +253 -0
  31. package/examples/honeypot-quickstart.ts +110 -0
  32. package/package.json +1 -1
  33. package/src/Honeypot/index.ts +422 -0
  34. package/src/SSHMimic/index.ts +13 -1
  35. package/src/SSHMimic/sftp.ts +21 -1
  36. package/src/VirtualFileSystem/index.ts +8 -1
  37. package/src/VirtualShell/index.ts +6 -1
  38. package/src/VirtualUserManager/index.ts +21 -3
  39. package/src/index.ts +6 -0
@@ -0,0 +1,110 @@
1
+ /**
2
+ * HoneyPot Quick Start Example
3
+ *
4
+ * A minimal, step-by-step introduction to HoneyPot auditing.
5
+ * Perfect for beginners.
6
+ *
7
+ * Run with: bun run examples/honeypot-quickstart.ts
8
+ */
9
+
10
+ import {
11
+ HoneyPot,
12
+ SshClient,
13
+ VirtualShell,
14
+ VirtualSshServer,
15
+ } from "../src/index";
16
+
17
+ async function quickStart() {
18
+ console.log("🍯 HoneyPot Quick Start\n");
19
+
20
+ // Step 1: Create virtual environment
21
+ console.log("Step 1️⃣ Creating virtual environment...");
22
+ const shell = new VirtualShell("my-lab");
23
+ const ssh = new VirtualSshServer({ port: 2222, shell });
24
+ await ssh.start();
25
+
26
+ const users = shell.getUsers()!;
27
+ const vfs = shell.getVfs()!;
28
+
29
+ console.log("✅ Environment ready\n");
30
+
31
+ // Step 2: Create HoneyPot instance
32
+ console.log("Step 2️⃣ Initializing HoneyPot...");
33
+ const honeypot = new HoneyPot();
34
+
35
+ // Step 3: Attach HoneyPot to all components
36
+ console.log("Step 3️⃣ Attaching HoneyPot to components...");
37
+ honeypot.attach(shell, vfs, users, ssh);
38
+
39
+ console.log("✅ HoneyPot is now tracking all activity\n");
40
+
41
+ // Step 4: Do some work (which will be audited)
42
+ console.log("Step 4️⃣ Performing some operations...\n");
43
+
44
+ // Create a user
45
+ await users.addUser("dev_user", "secure_pass");
46
+ console.log(" ✓ Created user 'dev_user'");
47
+
48
+ // Create a client
49
+ const client = new SshClient(shell, "dev_user");
50
+
51
+ // Create files
52
+ await client.mkdir("/app", true);
53
+ await client.writeFile("/app/config.json", '{"debug":true}');
54
+ await client.readFile("/app/config.json");
55
+
56
+ console.log(" ✓ Created /app directory and config.json");
57
+ console.log(" ✓ Read config.json\n");
58
+
59
+ // Step 5: Get statistics
60
+ console.log("Step 5️⃣ Viewing activity statistics...\n");
61
+ const stats = honeypot.getStats();
62
+ console.log(` 📊 Commands: ${stats.commands}`);
63
+ console.log(` 📝 File writes: ${stats.fileWrites}`);
64
+ console.log(` 📖 File reads: ${stats.fileReads}`);
65
+ console.log(` 👤 Users created: ${stats.userCreated}\n`);
66
+
67
+ // Step 6: Get recent events
68
+ console.log("Step 6️⃣ Last 5 events:\n");
69
+ honeypot.getRecent(5).forEach((entry, idx) => {
70
+ console.log(` ${idx + 1}. [${entry.source}] ${entry.type}`);
71
+ if (Object.keys(entry.details).length > 0) {
72
+ console.log(` └─ ${JSON.stringify(entry.details)}`);
73
+ }
74
+ });
75
+ console.log();
76
+
77
+ // Step 7: Query filtered logs
78
+ console.log("Step 7️⃣ Querying specific event types...\n");
79
+
80
+ const userEvents = honeypot.getAuditLog("user:add");
81
+ console.log(` 👤 User creation events: ${userEvents.length}`);
82
+
83
+ const fileEvents = honeypot.getAuditLog(undefined, "VirtualFileSystem");
84
+ console.log(` 📁 VirtualFileSystem events: ${fileEvents.length}\n`);
85
+
86
+ // Step 8: Detect anomalies
87
+ console.log("Step 8️⃣ Checking for anomalies...\n");
88
+ const anomalies = honeypot.detectAnomalies();
89
+ if (anomalies.length === 0) {
90
+ console.log(" ✅ No anomalies detected\n");
91
+ } else {
92
+ console.log(" ⚠️ Anomalies found:");
93
+ anomalies.forEach((a) => {
94
+ console.log(` • ${a.message}`);
95
+ });
96
+ console.log();
97
+ }
98
+
99
+ // Step 9: Export audit data (for storage/analysis)
100
+ console.log("Step 9️⃣ Exporting audit log...\n");
101
+ const fullLog = honeypot.getAuditLog();
102
+ console.log(` ✓ Exported ${fullLog.length} audit entries`);
103
+ console.log(` ✓ Ready to store in database, file, or monitoring system\n`);
104
+
105
+ // Cleanup
106
+ ssh.stop();
107
+ console.log("✅ Example complete!");
108
+ }
109
+
110
+ quickStart().catch(console.error);
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
7
- "version": "1.1.4",
7
+ "version": "1.1.6",
8
8
  "license": "MIT",
9
9
  "keywords": [
10
10
  "ssh",
@@ -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;
@@ -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
  }
@@ -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
  }