typescript-virtual-container 1.2.0 → 1.2.2
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/.github/workflows/publish.yml +25 -0
- package/README.md +25 -8
- package/benchmark-results.txt +40 -0
- package/benchmark-virtualshell.ts +96 -0
- package/dist/Honeypot/index.d.ts.map +1 -1
- package/dist/Honeypot/index.js +9 -0
- package/dist/SSHClient/index.d.ts +0 -14
- package/dist/SSHClient/index.d.ts.map +1 -1
- package/dist/SSHClient/index.js +19 -0
- package/dist/SSHMimic/index.d.ts +0 -7
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +5 -0
- package/dist/SSHMimic/sftp.d.ts.map +1 -1
- package/dist/SSHMimic/sftp.js +5 -0
- package/dist/VirtualFileSystem/index.d.ts +0 -7
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +18 -0
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +14 -1
- package/dist/VirtualUserManager/index.d.ts +4 -1
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +72 -13
- package/dist/utils/perfLogger.d.ts +9 -0
- package/dist/utils/perfLogger.d.ts.map +1 -0
- package/dist/utils/perfLogger.js +49 -0
- package/package.json +6 -3
- package/src/Honeypot/index.ts +11 -0
- package/src/SSHClient/index.ts +23 -1
- package/src/SSHMimic/index.ts +6 -0
- package/src/SSHMimic/sftp.ts +8 -1
- package/src/VirtualFileSystem/index.ts +20 -0
- package/src/VirtualShell/index.ts +18 -1
- package/src/VirtualUserManager/index.ts +103 -26
- package/src/utils/perfLogger.ts +72 -0
- package/dist/VirtualFileSystem/archive.d.ts +0 -5
- package/dist/VirtualFileSystem/archive.d.ts.map +0 -1
- package/dist/VirtualFileSystem/archive.js +0 -56
- package/dist/VirtualFileSystem/snapshot.d.ts +0 -5
- package/dist/VirtualFileSystem/snapshot.d.ts.map +0 -1
- package/dist/VirtualFileSystem/snapshot.js +0 -59
- package/dist/VirtualFileSystem/tree.d.ts +0 -3
- package/dist/VirtualFileSystem/tree.d.ts.map +0 -1
- package/dist/VirtualFileSystem/tree.js +0 -19
- package/dist/honeypot.d.ts +0 -132
- package/dist/honeypot.d.ts.map +0 -1
- package/dist/honeypot.js +0 -289
- package/src/VirtualFileSystem/archive.ts +0 -74
- package/src/VirtualFileSystem/snapshot.ts +0 -84
- package/src/VirtualFileSystem/tree.ts +0 -34
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { randomBytes, randomUUID, scryptSync } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes, randomUUID, scryptSync } from "node:crypto";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import type { PerfLogger } from "../utils/perfLogger";
|
|
5
|
+
import { createPerfLogger } from "../utils/perfLogger";
|
|
4
6
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
5
7
|
|
|
6
8
|
/** Persisted virtual user credential record. */
|
|
@@ -27,12 +29,24 @@ export interface VirtualActiveSession {
|
|
|
27
29
|
startedAt: string;
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
function resolveFastPasswordHash(): boolean {
|
|
33
|
+
const configured = process.env.SSH_MIMIC_FAST_PASSWORD_HASH;
|
|
34
|
+
return (
|
|
35
|
+
!!configured &&
|
|
36
|
+
!["0", "false", "no", "off"].includes(configured.toLowerCase())
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const perf: PerfLogger = createPerfLogger("VirtualUserManager");
|
|
41
|
+
|
|
30
42
|
/**
|
|
31
43
|
* Persistent user, sudoers, and active-session manager for the shell runtime.
|
|
32
44
|
*
|
|
33
|
-
* Passwords are hashed with scrypt and stored in the backing virtual filesystem.
|
|
45
|
+
* Passwords are hashed with scrypt by default and stored in the backing virtual filesystem.
|
|
34
46
|
*/
|
|
35
47
|
export class VirtualUserManager extends EventEmitter {
|
|
48
|
+
private static readonly recordCache = new Map<string, VirtualUserRecord>();
|
|
49
|
+
private static readonly fastPasswordHash = resolveFastPasswordHash();
|
|
36
50
|
private readonly usersPath = "/virtual-env-js/.auth/htpasswd";
|
|
37
51
|
private readonly sudoersPath = "/virtual-env-js/.auth/sudoers";
|
|
38
52
|
private readonly quotasPath = "/virtual-env-js/.auth/quotas";
|
|
@@ -56,6 +70,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
56
70
|
private readonly autoSudoForNewUsers: boolean = true,
|
|
57
71
|
) {
|
|
58
72
|
super();
|
|
73
|
+
perf.mark("constructor");
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
/**
|
|
@@ -63,23 +78,30 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
63
78
|
* Also creates the current system user if not already present.
|
|
64
79
|
*/
|
|
65
80
|
public async initialize(): Promise<void> {
|
|
81
|
+
perf.mark("initialize");
|
|
66
82
|
this.loadFromVfs();
|
|
67
83
|
this.loadSudoersFromVfs();
|
|
68
84
|
this.loadQuotasFromVfs();
|
|
69
85
|
|
|
70
|
-
|
|
86
|
+
let changed = false;
|
|
87
|
+
if (!this.users.has("root")) {
|
|
88
|
+
this.users.set(
|
|
89
|
+
"root",
|
|
90
|
+
this.createRecord("root", this.defaultRootPassword),
|
|
91
|
+
);
|
|
92
|
+
changed = true;
|
|
93
|
+
}
|
|
71
94
|
|
|
72
95
|
this.sudoers.add("root");
|
|
73
96
|
|
|
74
97
|
// Auto-create current system user for easier authentication
|
|
75
98
|
const currentUser = process.env.USER || process.env.USERNAME;
|
|
76
99
|
if (currentUser && currentUser !== "root" && !this.users.has(currentUser)) {
|
|
77
|
-
// Use same password as root for convenience, or a generic default
|
|
78
100
|
const userPassword = this.defaultRootPassword;
|
|
79
101
|
this.users.set(currentUser, this.createRecord(currentUser, userPassword));
|
|
80
102
|
this.sudoers.add(currentUser);
|
|
103
|
+
changed = true;
|
|
81
104
|
|
|
82
|
-
// Create home directory for the system user
|
|
83
105
|
const homePath = `/home/${currentUser}`;
|
|
84
106
|
if (!this.vfs.exists(homePath)) {
|
|
85
107
|
this.vfs.mkdir(homePath, 0o755);
|
|
@@ -90,7 +112,9 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
90
112
|
}
|
|
91
113
|
}
|
|
92
114
|
|
|
93
|
-
|
|
115
|
+
if (changed) {
|
|
116
|
+
await this.persist();
|
|
117
|
+
}
|
|
94
118
|
this.emit("initialized");
|
|
95
119
|
}
|
|
96
120
|
|
|
@@ -104,6 +128,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
104
128
|
username: string,
|
|
105
129
|
maxBytes: number,
|
|
106
130
|
): Promise<void> {
|
|
131
|
+
perf.mark("setQuotaBytes");
|
|
107
132
|
this.validateUsername(username);
|
|
108
133
|
if (!this.users.has(username)) {
|
|
109
134
|
throw new Error(`quota: user '${username}' does not exist`);
|
|
@@ -123,6 +148,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
123
148
|
* @param username Target username.
|
|
124
149
|
*/
|
|
125
150
|
public async clearQuota(username: string): Promise<void> {
|
|
151
|
+
perf.mark("clearQuota");
|
|
126
152
|
this.validateUsername(username);
|
|
127
153
|
this.quotas.delete(username);
|
|
128
154
|
await this.persist();
|
|
@@ -135,6 +161,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
135
161
|
* @returns Quota in bytes, or null when unlimited.
|
|
136
162
|
*/
|
|
137
163
|
public getQuotaBytes(username: string): number | null {
|
|
164
|
+
perf.mark("getQuotaBytes");
|
|
138
165
|
return this.quotas.get(username) ?? null;
|
|
139
166
|
}
|
|
140
167
|
|
|
@@ -145,6 +172,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
145
172
|
* @returns Current usage in bytes.
|
|
146
173
|
*/
|
|
147
174
|
public getUsageBytes(username: string): number {
|
|
175
|
+
perf.mark("getUsageBytes");
|
|
148
176
|
const homePath = `/home/${username}`;
|
|
149
177
|
if (!this.vfs.exists(homePath)) {
|
|
150
178
|
return 0;
|
|
@@ -167,6 +195,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
167
195
|
targetPath: string,
|
|
168
196
|
nextContent: string | Buffer,
|
|
169
197
|
): void {
|
|
198
|
+
perf.mark("assertWriteWithinQuota");
|
|
170
199
|
const quota = this.quotas.get(username);
|
|
171
200
|
if (quota === undefined) {
|
|
172
201
|
return;
|
|
@@ -209,6 +238,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
209
238
|
* @returns True when credentials are valid.
|
|
210
239
|
*/
|
|
211
240
|
public verifyPassword(username: string, password: string): boolean {
|
|
241
|
+
perf.mark("verifyPassword");
|
|
212
242
|
const record = this.users.get(username);
|
|
213
243
|
if (!record) {
|
|
214
244
|
return false;
|
|
@@ -224,6 +254,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
224
254
|
* @param password Initial plaintext password.
|
|
225
255
|
*/
|
|
226
256
|
public async addUser(username: string, password: string): Promise<void> {
|
|
257
|
+
perf.mark("addUser");
|
|
227
258
|
this.validateUsername(username);
|
|
228
259
|
this.validatePassword(password);
|
|
229
260
|
|
|
@@ -255,6 +286,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
255
286
|
* @param password New plaintext password.
|
|
256
287
|
*/
|
|
257
288
|
public async setPassword(username: string, password: string): Promise<void> {
|
|
289
|
+
perf.mark("setPassword");
|
|
258
290
|
this.validateUsername(username);
|
|
259
291
|
this.validatePassword(password);
|
|
260
292
|
|
|
@@ -272,6 +304,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
272
304
|
* @param username Username to remove.
|
|
273
305
|
*/
|
|
274
306
|
public async deleteUser(username: string): Promise<void> {
|
|
307
|
+
perf.mark("deleteUser");
|
|
275
308
|
this.validateUsername(username);
|
|
276
309
|
|
|
277
310
|
if (username === "root") {
|
|
@@ -295,6 +328,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
295
328
|
* @returns True when user can run sudo.
|
|
296
329
|
*/
|
|
297
330
|
public isSudoer(username: string): boolean {
|
|
331
|
+
perf.mark("isSudoer");
|
|
298
332
|
return this.sudoers.has(username);
|
|
299
333
|
}
|
|
300
334
|
|
|
@@ -304,6 +338,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
304
338
|
* @param username Username to promote.
|
|
305
339
|
*/
|
|
306
340
|
public async addSudoer(username: string): Promise<void> {
|
|
341
|
+
perf.mark("addSudoer");
|
|
307
342
|
this.validateUsername(username);
|
|
308
343
|
if (!this.users.has(username)) {
|
|
309
344
|
throw new Error(`sudoers: user '${username}' does not exist`);
|
|
@@ -319,6 +354,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
319
354
|
* @param username Username to demote.
|
|
320
355
|
*/
|
|
321
356
|
public async removeSudoer(username: string): Promise<void> {
|
|
357
|
+
perf.mark("removeSudoer");
|
|
322
358
|
this.validateUsername(username);
|
|
323
359
|
if (username === "root") {
|
|
324
360
|
throw new Error("sudoers: cannot remove root");
|
|
@@ -339,6 +375,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
339
375
|
username: string,
|
|
340
376
|
remoteAddress: string,
|
|
341
377
|
): VirtualActiveSession {
|
|
378
|
+
perf.mark("registerSession");
|
|
342
379
|
const session: VirtualActiveSession = {
|
|
343
380
|
id: randomUUID(),
|
|
344
381
|
username,
|
|
@@ -361,6 +398,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
361
398
|
* @param sessionId Session identifier; ignored when nullish.
|
|
362
399
|
*/
|
|
363
400
|
public unregisterSession(sessionId: string | null | undefined): void {
|
|
401
|
+
perf.mark("unregisterSession");
|
|
364
402
|
if (!sessionId) {
|
|
365
403
|
return;
|
|
366
404
|
}
|
|
@@ -388,6 +426,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
388
426
|
username: string,
|
|
389
427
|
remoteAddress: string,
|
|
390
428
|
): void {
|
|
429
|
+
perf.mark("updateSession");
|
|
391
430
|
if (!sessionId) {
|
|
392
431
|
return;
|
|
393
432
|
}
|
|
@@ -410,6 +449,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
410
449
|
* @returns Snapshot of active session descriptors.
|
|
411
450
|
*/
|
|
412
451
|
public listActiveSessions(): VirtualActiveSession[] {
|
|
452
|
+
perf.mark("listActiveSessions");
|
|
413
453
|
return Array.from(this.activeSessions.values()).sort((left, right) =>
|
|
414
454
|
left.startedAt.localeCompare(right.startedAt),
|
|
415
455
|
);
|
|
@@ -488,47 +528,84 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
488
528
|
this.vfs.mkdir(this.authDirPath, 0o700);
|
|
489
529
|
}
|
|
490
530
|
|
|
491
|
-
const
|
|
531
|
+
const authContent = Array.from(this.users.values())
|
|
492
532
|
.sort((left, right) => left.username.localeCompare(right.username))
|
|
493
533
|
.map((record) =>
|
|
494
534
|
[record.username, record.salt, record.passwordHash].join(":"),
|
|
495
535
|
)
|
|
496
536
|
.join("\n");
|
|
497
|
-
|
|
498
|
-
this.vfs.writeFile(
|
|
499
|
-
this.usersPath,
|
|
500
|
-
content.length > 0 ? `${content}\n` : "",
|
|
501
|
-
{ mode: 0o600 },
|
|
502
|
-
);
|
|
503
537
|
const sudoersContent = Array.from(this.sudoers.values()).sort().join("\n");
|
|
504
|
-
this.vfs.writeFile(
|
|
505
|
-
this.sudoersPath,
|
|
506
|
-
sudoersContent.length > 0 ? `${sudoersContent}\n` : "",
|
|
507
|
-
{ mode: 0o600 },
|
|
508
|
-
);
|
|
509
538
|
const quotasContent = Array.from(this.quotas.entries())
|
|
510
539
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
511
540
|
.map(([username, maxBytes]) => `${username}:${maxBytes}`)
|
|
512
541
|
.join("\n");
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
542
|
+
|
|
543
|
+
let changed = false;
|
|
544
|
+
changed =
|
|
545
|
+
this.writeIfChanged(
|
|
546
|
+
this.usersPath,
|
|
547
|
+
authContent.length > 0 ? `${authContent}\n` : "",
|
|
548
|
+
0o600,
|
|
549
|
+
) || changed;
|
|
550
|
+
changed =
|
|
551
|
+
this.writeIfChanged(
|
|
552
|
+
this.sudoersPath,
|
|
553
|
+
sudoersContent.length > 0 ? `${sudoersContent}\n` : "",
|
|
554
|
+
0o600,
|
|
555
|
+
) || changed;
|
|
556
|
+
changed =
|
|
557
|
+
this.writeIfChanged(
|
|
558
|
+
this.quotasPath,
|
|
559
|
+
quotasContent.length > 0 ? `${quotasContent}\n` : "",
|
|
560
|
+
0o600,
|
|
561
|
+
) || changed;
|
|
562
|
+
|
|
563
|
+
if (changed) {
|
|
564
|
+
await this.vfs.flushMirror();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private writeIfChanged(
|
|
569
|
+
targetPath: string,
|
|
570
|
+
content: string,
|
|
571
|
+
mode: number,
|
|
572
|
+
): boolean {
|
|
573
|
+
if (this.vfs.exists(targetPath)) {
|
|
574
|
+
const existing = this.vfs.readFile(targetPath);
|
|
575
|
+
if (existing === content) {
|
|
576
|
+
this.vfs.chmod(targetPath, mode);
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
this.vfs.writeFile(targetPath, content, { mode });
|
|
582
|
+
return true;
|
|
519
583
|
}
|
|
520
584
|
|
|
521
585
|
private createRecord(username: string, password: string): VirtualUserRecord {
|
|
586
|
+
const cacheKey = `${username}:${password}`;
|
|
587
|
+
const cached = VirtualUserManager.recordCache.get(cacheKey);
|
|
588
|
+
if (cached) {
|
|
589
|
+
return cached;
|
|
590
|
+
}
|
|
591
|
+
|
|
522
592
|
const salt = randomBytes(16).toString("hex");
|
|
523
|
-
|
|
593
|
+
const record = {
|
|
524
594
|
username,
|
|
525
595
|
salt,
|
|
526
596
|
passwordHash: this.hashPassword(password, salt),
|
|
527
597
|
};
|
|
598
|
+
|
|
599
|
+
VirtualUserManager.recordCache.set(cacheKey, record);
|
|
600
|
+
return record;
|
|
528
601
|
}
|
|
529
602
|
|
|
530
603
|
private hashPassword(password: string, salt: string): string {
|
|
531
|
-
|
|
604
|
+
if (VirtualUserManager.fastPasswordHash) {
|
|
605
|
+
return createHash("sha256").update(`${salt}:${password}`).digest("hex");
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return scryptSync(password, salt, 32).toString("hex");
|
|
532
609
|
}
|
|
533
610
|
|
|
534
611
|
private validateUsername(username: string): void {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type PerfLogger = {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
mark: (label: string) => void;
|
|
4
|
+
done: (label?: string) => void;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function isTruthyEnv(value: string | undefined): boolean {
|
|
8
|
+
return value === "1" || value === "true";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function nowMs(): number {
|
|
12
|
+
if (
|
|
13
|
+
typeof performance !== "undefined" &&
|
|
14
|
+
typeof performance.now === "function"
|
|
15
|
+
) {
|
|
16
|
+
return performance.now();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return Date.now();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isPerfLoggingEnabled(): boolean {
|
|
23
|
+
return (
|
|
24
|
+
isTruthyEnv(process.env.DEV_MODE) || isTruthyEnv(process.env.RENDER_PERF)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createPerfLogger(scope: string): PerfLogger {
|
|
29
|
+
const enabled = isPerfLoggingEnabled();
|
|
30
|
+
if (!enabled) {
|
|
31
|
+
return {
|
|
32
|
+
enabled,
|
|
33
|
+
mark: () => undefined,
|
|
34
|
+
done: () => undefined,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const startedAt = nowMs();
|
|
39
|
+
|
|
40
|
+
const mark = (label: string): void => {
|
|
41
|
+
const elapsedMs = nowMs() - startedAt;
|
|
42
|
+
console.log(`[perf][${scope}] ${label}: ${elapsedMs.toFixed(1)}ms`);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const done = (label = "done"): void => {
|
|
46
|
+
mark(label);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
enabled,
|
|
51
|
+
mark,
|
|
52
|
+
done,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function withPerf<T>(
|
|
57
|
+
scope: string,
|
|
58
|
+
label: string,
|
|
59
|
+
work: () => Promise<T>,
|
|
60
|
+
): Promise<T> {
|
|
61
|
+
const perf = createPerfLogger(scope);
|
|
62
|
+
if (!perf.enabled) {
|
|
63
|
+
return work();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
perf.mark(`${label}:start`);
|
|
67
|
+
try {
|
|
68
|
+
return await work();
|
|
69
|
+
} finally {
|
|
70
|
+
perf.done(`${label}:done`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { VfsSnapshot } from "../types/vfs";
|
|
2
|
-
export declare function archiveExists(archivePath: string): Promise<boolean>;
|
|
3
|
-
export declare function createTarBuffer(snapshotJson: string): Promise<Buffer>;
|
|
4
|
-
export declare function readSnapshotFromTar(tarBuffer: Buffer): Promise<VfsSnapshot>;
|
|
5
|
-
//# sourceMappingURL=archive.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../src/VirtualFileSystem/archive.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOzE;AAED,wBAAsB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuB3E;AAED,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,MAAM,GACf,OAAO,CAAC,WAAW,CAAC,CAiCtB"}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import * as tarStream from "tar-stream";
|
|
3
|
-
export async function archiveExists(archivePath) {
|
|
4
|
-
try {
|
|
5
|
-
await fs.access(archivePath);
|
|
6
|
-
return true;
|
|
7
|
-
}
|
|
8
|
-
catch {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
export async function createTarBuffer(snapshotJson) {
|
|
13
|
-
const pack = tarStream.pack();
|
|
14
|
-
const chunks = [];
|
|
15
|
-
const finished = new Promise((resolve, reject) => {
|
|
16
|
-
pack.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
17
|
-
pack.on("error", reject);
|
|
18
|
-
pack.on("end", () => resolve(Buffer.concat(chunks)));
|
|
19
|
-
});
|
|
20
|
-
pack.entry({ name: "snapshot.json", mode: 0o600 }, snapshotJson, (error) => {
|
|
21
|
-
if (error) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
pack.finalize();
|
|
25
|
-
});
|
|
26
|
-
return finished;
|
|
27
|
-
}
|
|
28
|
-
export async function readSnapshotFromTar(tarBuffer) {
|
|
29
|
-
return new Promise((resolve, reject) => {
|
|
30
|
-
const extract = tarStream.extract();
|
|
31
|
-
let snapshotText = "";
|
|
32
|
-
let found = false;
|
|
33
|
-
extract.on("entry", (header, stream, next) => {
|
|
34
|
-
if (header.name === "snapshot.json") {
|
|
35
|
-
found = true;
|
|
36
|
-
stream.on("data", (chunk) => {
|
|
37
|
-
snapshotText += chunk.toString("utf8");
|
|
38
|
-
});
|
|
39
|
-
stream.on("end", next);
|
|
40
|
-
stream.resume();
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
stream.resume();
|
|
44
|
-
stream.on("end", next);
|
|
45
|
-
});
|
|
46
|
-
extract.on("finish", () => {
|
|
47
|
-
if (!found) {
|
|
48
|
-
reject(new Error("snapshot.json missing from archive"));
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
resolve(JSON.parse(snapshotText));
|
|
52
|
-
});
|
|
53
|
-
extract.on("error", reject);
|
|
54
|
-
extract.end(tarBuffer);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { VfsSnapshot } from "../types/vfs";
|
|
2
|
-
import type { InternalDirectoryNode } from "./internalTypes";
|
|
3
|
-
export declare function createSnapshot(root: InternalDirectoryNode): VfsSnapshot;
|
|
4
|
-
export declare function applySnapshot(rootTarget: InternalDirectoryNode, snapshot: VfsSnapshot): void;
|
|
5
|
-
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/VirtualFileSystem/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,WAAW,EAGX,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,iBAAiB,CAAC;AAgE3E,wBAAgB,cAAc,CAAC,IAAI,EAAE,qBAAqB,GAAG,WAAW,CAEvE;AAED,wBAAgB,aAAa,CAC5B,UAAU,EAAE,qBAAqB,EACjC,QAAQ,EAAE,WAAW,GACnB,IAAI,CAON"}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
function serializeNode(node) {
|
|
2
|
-
if (node.type === "file") {
|
|
3
|
-
return {
|
|
4
|
-
type: "file",
|
|
5
|
-
name: node.name,
|
|
6
|
-
mode: node.mode,
|
|
7
|
-
createdAt: node.createdAt.toISOString(),
|
|
8
|
-
updatedAt: node.updatedAt.toISOString(),
|
|
9
|
-
compressed: node.compressed,
|
|
10
|
-
contentBase64: node.content.toString("base64"),
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
return serializeDirectory(node);
|
|
14
|
-
}
|
|
15
|
-
function serializeDirectory(node) {
|
|
16
|
-
return {
|
|
17
|
-
type: "directory",
|
|
18
|
-
name: node.name,
|
|
19
|
-
mode: node.mode,
|
|
20
|
-
createdAt: node.createdAt.toISOString(),
|
|
21
|
-
updatedAt: node.updatedAt.toISOString(),
|
|
22
|
-
children: Array.from(node.children.values()).map((child) => serializeNode(child)),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function deserializeNode(node) {
|
|
26
|
-
if (node.type === "file") {
|
|
27
|
-
return {
|
|
28
|
-
type: "file",
|
|
29
|
-
name: node.name,
|
|
30
|
-
mode: node.mode,
|
|
31
|
-
createdAt: new Date(node.createdAt),
|
|
32
|
-
updatedAt: new Date(node.updatedAt),
|
|
33
|
-
content: Buffer.from(node.contentBase64, "base64"),
|
|
34
|
-
compressed: node.compressed,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
return deserializeDirectory(node);
|
|
38
|
-
}
|
|
39
|
-
function deserializeDirectory(node) {
|
|
40
|
-
return {
|
|
41
|
-
type: "directory",
|
|
42
|
-
name: node.name,
|
|
43
|
-
mode: node.mode,
|
|
44
|
-
createdAt: new Date(node.createdAt),
|
|
45
|
-
updatedAt: new Date(node.updatedAt),
|
|
46
|
-
children: new Map(node.children.map((child) => [child.name, deserializeNode(child)])),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
export function createSnapshot(root) {
|
|
50
|
-
return { root: serializeDirectory(root) };
|
|
51
|
-
}
|
|
52
|
-
export function applySnapshot(rootTarget, snapshot) {
|
|
53
|
-
const root = deserializeDirectory(snapshot.root);
|
|
54
|
-
rootTarget.name = root.name;
|
|
55
|
-
rootTarget.mode = root.mode;
|
|
56
|
-
rootTarget.createdAt = root.createdAt;
|
|
57
|
-
rootTarget.updatedAt = root.updatedAt;
|
|
58
|
-
rootTarget.children = root.children;
|
|
59
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../src/VirtualFileSystem/tree.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AA0B7D,wBAAgB,UAAU,CACzB,IAAI,EAAE,qBAAqB,EAC3B,SAAS,EAAE,MAAM,GACf,MAAM,CAIR"}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
function walkTree(node, indent, lines) {
|
|
2
|
-
const entries = Array.from(node.children.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
3
|
-
entries.forEach(([name, child], index) => {
|
|
4
|
-
const isLast = index === entries.length - 1;
|
|
5
|
-
const branch = isLast ? "`-- " : "|-- ";
|
|
6
|
-
const nextIndent = indent + (isLast ? " " : "| ");
|
|
7
|
-
if (child.type === "file") {
|
|
8
|
-
lines.push(`${indent}${branch}${name}${child.compressed ? " [gz]" : ""}`);
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
lines.push(`${indent}${branch}${name}/`);
|
|
12
|
-
walkTree(child, nextIndent, lines);
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
export function renderTree(node, rootLabel) {
|
|
16
|
-
const lines = [rootLabel];
|
|
17
|
-
walkTree(node, "", lines);
|
|
18
|
-
return lines.join("\n");
|
|
19
|
-
}
|
package/dist/honeypot.d.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
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
|
-
import type { VirtualShell } from "./VirtualShell";
|
|
11
|
-
import type VirtualFileSystem from "./VirtualFileSystem";
|
|
12
|
-
import type { VirtualUserManager } from "./VirtualUserManager";
|
|
13
|
-
import type { SshMimic } from "./SSHMimic";
|
|
14
|
-
import type { SftpMimic } from "./SSHMimic/sftp";
|
|
15
|
-
/**
|
|
16
|
-
* Audit log entry recorded for each event.
|
|
17
|
-
*/
|
|
18
|
-
export interface AuditLogEntry {
|
|
19
|
-
timestamp: string;
|
|
20
|
-
type: string;
|
|
21
|
-
source: string;
|
|
22
|
-
details: Record<string, unknown>;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Statistics tracker for honeypot activity.
|
|
26
|
-
*/
|
|
27
|
-
export interface HoneyPotStats {
|
|
28
|
-
authAttempts: number;
|
|
29
|
-
authSuccesses: number;
|
|
30
|
-
authFailures: number;
|
|
31
|
-
commands: number;
|
|
32
|
-
fileWrites: number;
|
|
33
|
-
fileReads: number;
|
|
34
|
-
sessionStarts: number;
|
|
35
|
-
sessionEnds: number;
|
|
36
|
-
userCreated: number;
|
|
37
|
-
userDeleted: number;
|
|
38
|
-
clientConnects: number;
|
|
39
|
-
clientDisconnects: number;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* HoneyPot audit and event tracking utility.
|
|
43
|
-
*
|
|
44
|
-
* Singleton-like helper that attaches listeners to virtual shell components
|
|
45
|
-
* and maintains an audit log of all activity.
|
|
46
|
-
*/
|
|
47
|
-
export declare class HoneyPot {
|
|
48
|
-
private auditLog;
|
|
49
|
-
private stats;
|
|
50
|
-
private maxLogSize;
|
|
51
|
-
/**
|
|
52
|
-
* Creates a new HoneyPot instance.
|
|
53
|
-
*
|
|
54
|
-
* @param maxLogSize Maximum audit log entries to retain (default: 10000).
|
|
55
|
-
*/
|
|
56
|
-
constructor(maxLogSize?: number);
|
|
57
|
-
/**
|
|
58
|
-
* Attaches honeypot listeners to all provided event emitters.
|
|
59
|
-
*
|
|
60
|
-
* @param shell VirtualShell instance.
|
|
61
|
-
* @param vfs VirtualFileSystem instance.
|
|
62
|
-
* @param users VirtualUserManager instance.
|
|
63
|
-
* @param ssh SshMimic instance (optional).
|
|
64
|
-
* @param sftp SftpMimic instance (optional).
|
|
65
|
-
*/
|
|
66
|
-
attach(shell: VirtualShell, vfs: VirtualFileSystem, users: VirtualUserManager, ssh?: SshMimic, sftp?: SftpMimic): void;
|
|
67
|
-
/**
|
|
68
|
-
* Attaches to VirtualShell events.
|
|
69
|
-
*/
|
|
70
|
-
private attachVirtualShell;
|
|
71
|
-
/**
|
|
72
|
-
* Attaches to VirtualFileSystem events.
|
|
73
|
-
*/
|
|
74
|
-
private attachVirtualFileSystem;
|
|
75
|
-
/**
|
|
76
|
-
* Attaches to VirtualUserManager events.
|
|
77
|
-
*/
|
|
78
|
-
private attachVirtualUserManager;
|
|
79
|
-
/**
|
|
80
|
-
* Attaches to SshMimic events.
|
|
81
|
-
*/
|
|
82
|
-
private attachSshMimic;
|
|
83
|
-
/**
|
|
84
|
-
* Attaches to SftpMimic events.
|
|
85
|
-
*/
|
|
86
|
-
private attachSftpMimic;
|
|
87
|
-
/**
|
|
88
|
-
* Records an audit log entry.
|
|
89
|
-
*
|
|
90
|
-
* @param source Event source (e.g., "SshMimic", "VirtualFileSystem").
|
|
91
|
-
* @param type Event type.
|
|
92
|
-
* @param details Event-specific data.
|
|
93
|
-
*/
|
|
94
|
-
private log;
|
|
95
|
-
/**
|
|
96
|
-
* Returns audit log entries matching optional filters.
|
|
97
|
-
*
|
|
98
|
-
* @param type Optional event type filter.
|
|
99
|
-
* @param source Optional source filter.
|
|
100
|
-
* @returns Filtered audit log entries.
|
|
101
|
-
*/
|
|
102
|
-
getAuditLog(type?: string, source?: string): AuditLogEntry[];
|
|
103
|
-
/**
|
|
104
|
-
* Returns current activity statistics.
|
|
105
|
-
*
|
|
106
|
-
* @returns Snapshot of honeypot stats.
|
|
107
|
-
*/
|
|
108
|
-
getStats(): Readonly<HoneyPotStats>;
|
|
109
|
-
/**
|
|
110
|
-
* Clears audit log and resets statistics.
|
|
111
|
-
*/
|
|
112
|
-
reset(): void;
|
|
113
|
-
/**
|
|
114
|
-
* Returns recent log entries in reverse chronological order.
|
|
115
|
-
*
|
|
116
|
-
* @param limit Number of recent entries to return (default: 100).
|
|
117
|
-
* @returns Recent audit log entries.
|
|
118
|
-
*/
|
|
119
|
-
getRecent(limit?: number): AuditLogEntry[];
|
|
120
|
-
/**
|
|
121
|
-
* Detects potential security issues based on activity patterns.
|
|
122
|
-
*
|
|
123
|
-
* @returns Array of anomalies detected.
|
|
124
|
-
*/
|
|
125
|
-
detectAnomalies(): Array<{
|
|
126
|
-
type: string;
|
|
127
|
-
severity: "low" | "medium" | "high";
|
|
128
|
-
message: string;
|
|
129
|
-
}>;
|
|
130
|
-
}
|
|
131
|
-
export default HoneyPot;
|
|
132
|
-
//# sourceMappingURL=honeypot.d.ts.map
|
package/dist/honeypot.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"honeypot.d.ts","sourceRoot":"","sources":["../src/honeypot.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,iBAAiB,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,qBAAa,QAAQ;IACpB,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,KAAK,CAaX;IAEF,OAAO,CAAC,UAAU,CAAS;IAE3B;;;;OAIG;gBACS,UAAU,GAAE,MAAc;IAItC;;;;;;;;OAQG;IACI,MAAM,CACZ,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,kBAAkB,EACzB,GAAG,CAAC,EAAE,QAAQ,EACd,IAAI,CAAC,EAAE,SAAS,GACd,IAAI;IAYP;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAmB1B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAoB/B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAkChC;;OAEG;IACH,OAAO,CAAC,cAAc;IAyCtB;;OAEG;IACH,OAAO,CAAC,eAAe;IAyCvB;;;;;;OAMG;IACH,OAAO,CAAC,GAAG;IAuBX;;;;;;OAMG;IACI,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,EAAE;IAOnE;;;;OAIG;IACI,QAAQ,IAAI,QAAQ,CAAC,aAAa,CAAC;IAI1C;;OAEG;IACI,KAAK,IAAI,IAAI;IAkBpB;;;;;OAKG;IACI,SAAS,CAAC,KAAK,GAAE,MAAY,GAAG,aAAa,EAAE;IAItD;;;;OAIG;IACI,eAAe,IAAI,KAAK,CAAC;QAC/B,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;QACpC,OAAO,EAAE,MAAM,CAAC;KAChB,CAAC;CAkDF;AAED,eAAe,QAAQ,CAAC"}
|