typescript-virtual-container 1.1.3 → 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/exec.d.ts.map +1 -1
- package/dist/SSHMimic/exec.js +8 -2
- package/dist/SSHMimic/index.d.ts +3 -1
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +21 -4
- package/dist/SSHMimic/sftp.d.ts +48 -0
- package/dist/SSHMimic/sftp.d.ts.map +1 -0
- package/dist/SSHMimic/sftp.js +595 -0
- package/dist/VirtualFileSystem/index.d.ts +8 -5
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +152 -154
- package/dist/VirtualShell/index.d.ts +8 -1
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +22 -5
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +7 -0
- package/dist/VirtualUserManager/index.d.ts +3 -1
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +34 -1
- package/dist/commands/exit.d.ts.map +1 -1
- package/dist/commands/exit.js +1 -0
- 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 +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/standalone.js +10 -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/exec.ts +18 -12
- package/src/SSHMimic/index.ts +29 -8
- package/src/SSHMimic/sftp.ts +853 -0
- package/src/VirtualFileSystem/index.ts +167 -190
- package/src/VirtualShell/index.ts +25 -9
- package/src/VirtualShell/shell.ts +7 -0
- package/src/VirtualUserManager/index.ts +41 -3
- package/src/commands/exit.ts +1 -0
- package/src/index.ts +8 -1
- package/src/standalone.ts +11 -1
- package/tests/sftp.test.ts +319 -0
- package/tests/ssh-exec.test.ts +45 -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
|
@@ -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/exec.ts
CHANGED
|
@@ -18,18 +18,24 @@ export function runExec(
|
|
|
18
18
|
): void {
|
|
19
19
|
Promise.resolve(
|
|
20
20
|
runCommand(cmd, authUser, hostname, "exec", `/home/${authUser}`, shell),
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
)
|
|
22
|
+
.then((result) => {
|
|
23
|
+
if (result.stdout) {
|
|
24
|
+
stream.write(`${toTtyLines(result.stdout)}\r\n`);
|
|
25
|
+
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
if (result.stderr) {
|
|
28
|
+
stream.stderr.write(`${toTtyLines(result.stderr)}\r\n`);
|
|
29
|
+
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
stream.exit(result.exitCode ?? 0);
|
|
32
|
+
void shell.vfs.flushMirror();
|
|
33
|
+
stream.end();
|
|
34
|
+
})
|
|
35
|
+
.catch((error) => {
|
|
36
|
+
console.error("Exec error:", error);
|
|
37
|
+
stream.stderr.write(`Error: ${String(error)}\r\n`);
|
|
38
|
+
stream.exit(1);
|
|
39
|
+
stream.end();
|
|
40
|
+
});
|
|
35
41
|
}
|
package/src/SSHMimic/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
1
2
|
import { Server as SshServer } from "ssh2";
|
|
2
3
|
import { VirtualShell } from "../VirtualShell";
|
|
4
|
+
import { runExec } from "./exec";
|
|
3
5
|
import { loadOrCreateHostKey } from "./hostKey";
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -9,7 +11,7 @@ import { loadOrCreateHostKey } from "./hostKey";
|
|
|
9
11
|
* Create an instance, call {@link SshMimic.start}, and stop it with
|
|
10
12
|
* {@link SshMimic.stop} when your process exits.
|
|
11
13
|
*/
|
|
12
|
-
class SshMimic {
|
|
14
|
+
class SshMimic extends EventEmitter {
|
|
13
15
|
port: number;
|
|
14
16
|
server: SshServer | null;
|
|
15
17
|
private shell: VirtualShell;
|
|
@@ -31,6 +33,7 @@ class SshMimic {
|
|
|
31
33
|
hostname?: string;
|
|
32
34
|
shell?: VirtualShell;
|
|
33
35
|
}) {
|
|
36
|
+
super();
|
|
34
37
|
this.port = port;
|
|
35
38
|
this.shellHostname = hostname;
|
|
36
39
|
this.server = null;
|
|
@@ -46,6 +49,9 @@ class SshMimic {
|
|
|
46
49
|
const shell = this.shell;
|
|
47
50
|
const privateKey = loadOrCreateHostKey();
|
|
48
51
|
|
|
52
|
+
// Ensure VirtualShell is fully initialized before accepting connections
|
|
53
|
+
await shell.ensureInitialized();
|
|
54
|
+
|
|
49
55
|
this.server = new SshServer(
|
|
50
56
|
{
|
|
51
57
|
hostKeys: [privateKey],
|
|
@@ -56,6 +62,8 @@ class SshMimic {
|
|
|
56
62
|
let remoteAddress = "unknown";
|
|
57
63
|
let sessionId: string | null = null;
|
|
58
64
|
|
|
65
|
+
this.emit("client:connect");
|
|
66
|
+
|
|
59
67
|
client.on("authentication", (ctx) => {
|
|
60
68
|
shell;
|
|
61
69
|
if (ctx.method === "password") {
|
|
@@ -65,12 +73,17 @@ class SshMimic {
|
|
|
65
73
|
if (
|
|
66
74
|
!shell.users.verifyPassword(candidateUser, ctx.password ?? "")
|
|
67
75
|
) {
|
|
76
|
+
this.emit("auth:failure", {
|
|
77
|
+
username: candidateUser,
|
|
78
|
+
remoteAddress,
|
|
79
|
+
});
|
|
68
80
|
ctx.reject();
|
|
69
81
|
return;
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
authUser = candidateUser;
|
|
73
85
|
sessionId = shell.users.registerSession(authUser, remoteAddress).id;
|
|
86
|
+
this.emit("auth:success", { username: authUser, remoteAddress });
|
|
74
87
|
|
|
75
88
|
const homePath = `/home/${authUser}`;
|
|
76
89
|
if (!shell.vfs.exists(homePath)) {
|
|
@@ -91,6 +104,7 @@ class SshMimic {
|
|
|
91
104
|
|
|
92
105
|
client.on("close", () => {
|
|
93
106
|
shell.users.unregisterSession(sessionId);
|
|
107
|
+
this.emit("client:disconnect", { user: authUser });
|
|
94
108
|
sessionId = null;
|
|
95
109
|
});
|
|
96
110
|
|
|
@@ -125,12 +139,16 @@ class SshMimic {
|
|
|
125
139
|
});
|
|
126
140
|
|
|
127
141
|
session.on("exec", (acceptExec, _rejectExec, info) => {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
const stream = acceptExec();
|
|
143
|
+
if (stream) {
|
|
144
|
+
runExec(
|
|
145
|
+
stream,
|
|
146
|
+
info.command.trim(),
|
|
147
|
+
authUser,
|
|
148
|
+
shell.hostname,
|
|
149
|
+
shell,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
134
152
|
});
|
|
135
153
|
});
|
|
136
154
|
});
|
|
@@ -139,8 +157,9 @@ class SshMimic {
|
|
|
139
157
|
|
|
140
158
|
return new Promise<number>((resolve, reject) => {
|
|
141
159
|
this.server?.once("error", (err: unknown) => reject(err));
|
|
142
|
-
this.server?.listen(this.port, "
|
|
160
|
+
this.server?.listen(this.port, "0.0.0.0", () => {
|
|
143
161
|
console.log(`SSH Mimic listening on port ${this.port}`);
|
|
162
|
+
this.emit("start", { port: this.port });
|
|
144
163
|
resolve(this.port);
|
|
145
164
|
});
|
|
146
165
|
});
|
|
@@ -153,9 +172,11 @@ class SshMimic {
|
|
|
153
172
|
if (this.server) {
|
|
154
173
|
this.server.close(() => {
|
|
155
174
|
console.log("SSH Mimic stopped");
|
|
175
|
+
this.emit("stop");
|
|
156
176
|
});
|
|
157
177
|
}
|
|
158
178
|
}
|
|
159
179
|
}
|
|
160
180
|
|
|
181
|
+
export { SftpMimic } from "./sftp";
|
|
161
182
|
export { SshMimic };
|