typescript-virtual-container 1.1.7 → 1.2.1

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.
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { EventEmitter } from "node:events";
3
3
  import { createCustomCommand, registerCommand, runCommand } from "../commands";
4
+ import { createPerfLogger } from "../utils/perfLogger";
4
5
  import VirtualFileSystem from "../VirtualFileSystem";
5
6
  import { VirtualUserManager } from "../VirtualUserManager";
6
7
  import { startShell } from "./shell";
@@ -9,12 +10,19 @@ const defaultShellProperties = {
9
10
  os: "Fortune GNU/Linux x64",
10
11
  arch: "x86_64",
11
12
  };
13
+ const perf = createPerfLogger("VirtualShell");
14
+ let cachedRootPassword = null;
12
15
  function resolveRootPassword() {
16
+ if (cachedRootPassword) {
17
+ return cachedRootPassword;
18
+ }
13
19
  const configured = process.env.SSH_MIMIC_ROOT_PASSWORD;
14
20
  if (configured && configured.trim().length > 0) {
15
- return configured;
21
+ cachedRootPassword = configured.trim();
22
+ return cachedRootPassword;
16
23
  }
17
24
  const generated = randomBytes(18).toString("base64url");
25
+ cachedRootPassword = generated;
18
26
  console.warn(`[ssh-mimic] SSH_MIMIC_ROOT_PASSWORD missing; generated ephemeral root password: ${generated}`);
19
27
  return generated;
20
28
  }
@@ -47,6 +55,7 @@ class VirtualShell extends EventEmitter {
47
55
  */
48
56
  constructor(hostname, properties, basePath) {
49
57
  super();
58
+ perf.mark("constructor");
50
59
  this.hostname = hostname;
51
60
  this.properties = properties || defaultShellProperties;
52
61
  this.basePath = basePath || ".";
@@ -67,6 +76,7 @@ class VirtualShell extends EventEmitter {
67
76
  * Call this before any authentication or command execution.
68
77
  */
69
78
  async ensureInitialized() {
79
+ perf.mark("ensureInitialized");
70
80
  await this.initialized;
71
81
  }
72
82
  /**
@@ -91,6 +101,7 @@ class VirtualShell extends EventEmitter {
91
101
  * @param cwd
92
102
  */
93
103
  executeCommand(rawInput, authUser, cwd) {
104
+ perf.mark("executeCommand");
94
105
  runCommand(rawInput, authUser, this.hostname, "shell", cwd, this);
95
106
  this.emit("command", { command: rawInput, user: authUser, cwd });
96
107
  }
@@ -103,6 +114,7 @@ class VirtualShell extends EventEmitter {
103
114
  * @param remoteAddress The address of the remote client.
104
115
  */
105
116
  startInteractiveSession(stream, authUser, sessionId, remoteAddress, terminalSize) {
117
+ perf.mark("startInteractiveSession");
106
118
  // Interactive shell logic
107
119
  this.emit("session:start", { user: authUser, sessionId, remoteAddress });
108
120
  startShell(this.properties, stream, authUser, this.hostname, sessionId, remoteAddress, terminalSize, this);
@@ -139,6 +151,7 @@ class VirtualShell extends EventEmitter {
139
151
  * @param content File content.
140
152
  */
141
153
  writeFileAsUser(authUser, targetPath, content) {
154
+ perf.mark("writeFileAsUser");
142
155
  this.users.assertWriteWithinQuota(authUser, targetPath, content);
143
156
  this.vfs.writeFile(targetPath, content);
144
157
  }
@@ -25,12 +25,14 @@ export interface VirtualActiveSession {
25
25
  /**
26
26
  * Persistent user, sudoers, and active-session manager for the shell runtime.
27
27
  *
28
- * Passwords are hashed with scrypt and stored in the backing virtual filesystem.
28
+ * Passwords are hashed with scrypt by default and stored in the backing virtual filesystem.
29
29
  */
30
30
  export declare class VirtualUserManager extends EventEmitter {
31
31
  private readonly vfs;
32
32
  private readonly defaultRootPassword;
33
33
  private readonly autoSudoForNewUsers;
34
+ private static readonly recordCache;
35
+ private static readonly fastPasswordHash;
34
36
  private readonly usersPath;
35
37
  private readonly sudoersPath;
36
38
  private readonly quotasPath;
@@ -169,6 +171,7 @@ export declare class VirtualUserManager extends EventEmitter {
169
171
  private loadSudoersFromVfs;
170
172
  private loadQuotasFromVfs;
171
173
  private persist;
174
+ private writeIfChanged;
172
175
  private createRecord;
173
176
  private hashPassword;
174
177
  private validateUsername;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualUserManager/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualUserManager/index.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,24 @@
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 { createPerfLogger } from "../utils/perfLogger";
5
+ function resolveFastPasswordHash() {
6
+ const configured = process.env.SSH_MIMIC_FAST_PASSWORD_HASH;
7
+ return (!!configured &&
8
+ !["0", "false", "no", "off"].includes(configured.toLowerCase()));
9
+ }
10
+ const perf = createPerfLogger("VirtualUserManager");
4
11
  /**
5
12
  * Persistent user, sudoers, and active-session manager for the shell runtime.
6
13
  *
7
- * Passwords are hashed with scrypt and stored in the backing virtual filesystem.
14
+ * Passwords are hashed with scrypt by default and stored in the backing virtual filesystem.
8
15
  */
9
16
  export class VirtualUserManager extends EventEmitter {
10
17
  vfs;
11
18
  defaultRootPassword;
12
19
  autoSudoForNewUsers;
20
+ static recordCache = new Map();
21
+ static fastPasswordHash = resolveFastPasswordHash();
13
22
  usersPath = "/virtual-env-js/.auth/htpasswd";
14
23
  sudoersPath = "/virtual-env-js/.auth/sudoers";
15
24
  quotasPath = "/virtual-env-js/.auth/quotas";
@@ -31,32 +40,39 @@ export class VirtualUserManager extends EventEmitter {
31
40
  this.vfs = vfs;
32
41
  this.defaultRootPassword = defaultRootPassword;
33
42
  this.autoSudoForNewUsers = autoSudoForNewUsers;
43
+ perf.mark("constructor");
34
44
  }
35
45
  /**
36
46
  * Loads users/sudoers from disk and ensures root account exists.
37
47
  * Also creates the current system user if not already present.
38
48
  */
39
49
  async initialize() {
50
+ perf.mark("initialize");
40
51
  this.loadFromVfs();
41
52
  this.loadSudoersFromVfs();
42
53
  this.loadQuotasFromVfs();
43
- this.users.set("root", this.createRecord("root", this.defaultRootPassword));
54
+ let changed = false;
55
+ if (!this.users.has("root")) {
56
+ this.users.set("root", this.createRecord("root", this.defaultRootPassword));
57
+ changed = true;
58
+ }
44
59
  this.sudoers.add("root");
45
60
  // Auto-create current system user for easier authentication
46
61
  const currentUser = process.env.USER || process.env.USERNAME;
47
62
  if (currentUser && currentUser !== "root" && !this.users.has(currentUser)) {
48
- // Use same password as root for convenience, or a generic default
49
63
  const userPassword = this.defaultRootPassword;
50
64
  this.users.set(currentUser, this.createRecord(currentUser, userPassword));
51
65
  this.sudoers.add(currentUser);
52
- // Create home directory for the system user
66
+ changed = true;
53
67
  const homePath = `/home/${currentUser}`;
54
68
  if (!this.vfs.exists(homePath)) {
55
69
  this.vfs.mkdir(homePath, 0o755);
56
70
  this.vfs.writeFile(`${homePath}/README.txt`, `Welcome to the virtual environment, ${currentUser}`);
57
71
  }
58
72
  }
59
- await this.persist();
73
+ if (changed) {
74
+ await this.persist();
75
+ }
60
76
  this.emit("initialized");
61
77
  }
62
78
  /**
@@ -66,6 +82,7 @@ export class VirtualUserManager extends EventEmitter {
66
82
  * @param maxBytes Quota ceiling in bytes.
67
83
  */
68
84
  async setQuotaBytes(username, maxBytes) {
85
+ perf.mark("setQuotaBytes");
69
86
  this.validateUsername(username);
70
87
  if (!this.users.has(username)) {
71
88
  throw new Error(`quota: user '${username}' does not exist`);
@@ -82,6 +99,7 @@ export class VirtualUserManager extends EventEmitter {
82
99
  * @param username Target username.
83
100
  */
84
101
  async clearQuota(username) {
102
+ perf.mark("clearQuota");
85
103
  this.validateUsername(username);
86
104
  this.quotas.delete(username);
87
105
  await this.persist();
@@ -93,6 +111,7 @@ export class VirtualUserManager extends EventEmitter {
93
111
  * @returns Quota in bytes, or null when unlimited.
94
112
  */
95
113
  getQuotaBytes(username) {
114
+ perf.mark("getQuotaBytes");
96
115
  return this.quotas.get(username) ?? null;
97
116
  }
98
117
  /**
@@ -102,6 +121,7 @@ export class VirtualUserManager extends EventEmitter {
102
121
  * @returns Current usage in bytes.
103
122
  */
104
123
  getUsageBytes(username) {
124
+ perf.mark("getUsageBytes");
105
125
  const homePath = `/home/${username}`;
106
126
  if (!this.vfs.exists(homePath)) {
107
127
  return 0;
@@ -118,6 +138,7 @@ export class VirtualUserManager extends EventEmitter {
118
138
  * @param nextContent New file content.
119
139
  */
120
140
  assertWriteWithinQuota(username, targetPath, nextContent) {
141
+ perf.mark("assertWriteWithinQuota");
121
142
  const quota = this.quotas.get(username);
122
143
  if (quota === undefined) {
123
144
  return;
@@ -152,6 +173,7 @@ export class VirtualUserManager extends EventEmitter {
152
173
  * @returns True when credentials are valid.
153
174
  */
154
175
  verifyPassword(username, password) {
176
+ perf.mark("verifyPassword");
155
177
  const record = this.users.get(username);
156
178
  if (!record) {
157
179
  return false;
@@ -165,10 +187,12 @@ export class VirtualUserManager extends EventEmitter {
165
187
  * @param password Initial plaintext password.
166
188
  */
167
189
  async addUser(username, password) {
190
+ perf.mark("addUser");
168
191
  this.validateUsername(username);
169
192
  this.validatePassword(password);
170
193
  if (this.users.has(username)) {
171
- throw new Error(`adduser: user '${username}' already exists`);
194
+ return;
195
+ // throw new Error(`adduser: user '${username}' already exists`);
172
196
  }
173
197
  this.users.set(username, this.createRecord(username, password));
174
198
  if (this.autoSudoForNewUsers) {
@@ -189,6 +213,7 @@ export class VirtualUserManager extends EventEmitter {
189
213
  * @param password New plaintext password.
190
214
  */
191
215
  async setPassword(username, password) {
216
+ perf.mark("setPassword");
192
217
  this.validateUsername(username);
193
218
  this.validatePassword(password);
194
219
  if (!this.users.has(username)) {
@@ -203,6 +228,7 @@ export class VirtualUserManager extends EventEmitter {
203
228
  * @param username Username to remove.
204
229
  */
205
230
  async deleteUser(username) {
231
+ perf.mark("deleteUser");
206
232
  this.validateUsername(username);
207
233
  if (username === "root") {
208
234
  throw new Error("deluser: cannot delete root");
@@ -221,6 +247,7 @@ export class VirtualUserManager extends EventEmitter {
221
247
  * @returns True when user can run sudo.
222
248
  */
223
249
  isSudoer(username) {
250
+ perf.mark("isSudoer");
224
251
  return this.sudoers.has(username);
225
252
  }
226
253
  /**
@@ -229,6 +256,7 @@ export class VirtualUserManager extends EventEmitter {
229
256
  * @param username Username to promote.
230
257
  */
231
258
  async addSudoer(username) {
259
+ perf.mark("addSudoer");
232
260
  this.validateUsername(username);
233
261
  if (!this.users.has(username)) {
234
262
  throw new Error(`sudoers: user '${username}' does not exist`);
@@ -242,6 +270,7 @@ export class VirtualUserManager extends EventEmitter {
242
270
  * @param username Username to demote.
243
271
  */
244
272
  async removeSudoer(username) {
273
+ perf.mark("removeSudoer");
245
274
  this.validateUsername(username);
246
275
  if (username === "root") {
247
276
  throw new Error("sudoers: cannot remove root");
@@ -257,6 +286,7 @@ export class VirtualUserManager extends EventEmitter {
257
286
  * @returns Registered session descriptor.
258
287
  */
259
288
  registerSession(username, remoteAddress) {
289
+ perf.mark("registerSession");
260
290
  const session = {
261
291
  id: randomUUID(),
262
292
  username,
@@ -278,6 +308,7 @@ export class VirtualUserManager extends EventEmitter {
278
308
  * @param sessionId Session identifier; ignored when nullish.
279
309
  */
280
310
  unregisterSession(sessionId) {
311
+ perf.mark("unregisterSession");
281
312
  if (!sessionId) {
282
313
  return;
283
314
  }
@@ -299,6 +330,7 @@ export class VirtualUserManager extends EventEmitter {
299
330
  * @param remoteAddress New remote address value.
300
331
  */
301
332
  updateSession(sessionId, username, remoteAddress) {
333
+ perf.mark("updateSession");
302
334
  if (!sessionId) {
303
335
  return;
304
336
  }
@@ -318,6 +350,7 @@ export class VirtualUserManager extends EventEmitter {
318
350
  * @returns Snapshot of active session descriptors.
319
351
  */
320
352
  listActiveSessions() {
353
+ perf.mark("listActiveSessions");
321
354
  return Array.from(this.activeSessions.values()).sort((left, right) => left.startedAt.localeCompare(right.startedAt));
322
355
  }
323
356
  loadFromVfs() {
@@ -378,30 +411,57 @@ export class VirtualUserManager extends EventEmitter {
378
411
  if (!this.vfs.exists(this.authDirPath)) {
379
412
  this.vfs.mkdir(this.authDirPath, 0o700);
380
413
  }
381
- const content = Array.from(this.users.values())
414
+ const authContent = Array.from(this.users.values())
382
415
  .sort((left, right) => left.username.localeCompare(right.username))
383
416
  .map((record) => [record.username, record.salt, record.passwordHash].join(":"))
384
417
  .join("\n");
385
- this.vfs.writeFile(this.usersPath, content.length > 0 ? `${content}\n` : "", { mode: 0o600 });
386
418
  const sudoersContent = Array.from(this.sudoers.values()).sort().join("\n");
387
- this.vfs.writeFile(this.sudoersPath, sudoersContent.length > 0 ? `${sudoersContent}\n` : "", { mode: 0o600 });
388
419
  const quotasContent = Array.from(this.quotas.entries())
389
420
  .sort(([left], [right]) => left.localeCompare(right))
390
421
  .map(([username, maxBytes]) => `${username}:${maxBytes}`)
391
422
  .join("\n");
392
- this.vfs.writeFile(this.quotasPath, quotasContent.length > 0 ? `${quotasContent}\n` : "", { mode: 0o600 });
393
- await this.vfs.flushMirror();
423
+ let changed = false;
424
+ changed =
425
+ this.writeIfChanged(this.usersPath, authContent.length > 0 ? `${authContent}\n` : "", 0o600) || changed;
426
+ changed =
427
+ this.writeIfChanged(this.sudoersPath, sudoersContent.length > 0 ? `${sudoersContent}\n` : "", 0o600) || changed;
428
+ changed =
429
+ this.writeIfChanged(this.quotasPath, quotasContent.length > 0 ? `${quotasContent}\n` : "", 0o600) || changed;
430
+ if (changed) {
431
+ await this.vfs.flushMirror();
432
+ }
433
+ }
434
+ writeIfChanged(targetPath, content, mode) {
435
+ if (this.vfs.exists(targetPath)) {
436
+ const existing = this.vfs.readFile(targetPath);
437
+ if (existing === content) {
438
+ this.vfs.chmod(targetPath, mode);
439
+ return false;
440
+ }
441
+ }
442
+ this.vfs.writeFile(targetPath, content, { mode });
443
+ return true;
394
444
  }
395
445
  createRecord(username, password) {
446
+ const cacheKey = `${username}:${password}`;
447
+ const cached = VirtualUserManager.recordCache.get(cacheKey);
448
+ if (cached) {
449
+ return cached;
450
+ }
396
451
  const salt = randomBytes(16).toString("hex");
397
- return {
452
+ const record = {
398
453
  username,
399
454
  salt,
400
455
  passwordHash: this.hashPassword(password, salt),
401
456
  };
457
+ VirtualUserManager.recordCache.set(cacheKey, record);
458
+ return record;
402
459
  }
403
460
  hashPassword(password, salt) {
404
- return scryptSync(password, salt, 64).toString("hex");
461
+ if (VirtualUserManager.fastPasswordHash) {
462
+ return createHash("sha256").update(`${salt}:${password}`).digest("hex");
463
+ }
464
+ return scryptSync(password, salt, 32).toString("hex");
405
465
  }
406
466
  validateUsername(username) {
407
467
  if (!username || username.trim() === "") {
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { HoneyPot } from "./Honeypot";
2
- import { SshClient } from "./SSHClient";
1
+ import { HoneyPot } from "./Honeypot/index";
2
+ import { SshClient } from "./SSHClient/index";
3
3
  import { SftpMimic, SshMimic } from "./SSHMimic/index";
4
- import VirtualFileSystem from "./VirtualFileSystem";
5
- import { VirtualShell } from "./VirtualShell";
6
- import { VirtualUserManager } from "./VirtualUserManager";
7
- export type { AuditLogEntry, HoneyPotStats, } from "./Honeypot";
4
+ import VirtualFileSystem from "./VirtualFileSystem/index";
5
+ import { VirtualShell } from "./VirtualShell/index";
6
+ import { VirtualUserManager } from "./VirtualUserManager/index";
7
+ export type { AuditLogEntry, HoneyPotStats, } from "./Honeypot/index";
8
8
  export type { CommandContext, CommandMode, CommandOutcome, CommandResult, NanoEditorSession, ShellModule, SudoChallenge, } from "./types/commands";
9
9
  export type { ExecStream, ShellStream } from "./types/streams";
10
10
  export type { RemoveOptions, VfsBaseNode, VfsDirectoryNode, VfsFileNode, VfsNodeStats, VfsNodeType, VfsSnapshot, VfsSnapshotBaseNode, VfsSnapshotDirectoryNode, VfsSnapshotFileNode, VfsSnapshotNode, WriteFileOptions, } from "./types/vfs";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,YAAY,EACX,aAAa,EACb,aAAa,GACb,MAAM,YAAY,CAAC;AACpB,YAAY,EACX,cAAc,EACd,WAAW,EACX,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,aAAa,GACb,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC/D,YAAY,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,WAAW,EACX,WAAW,EACX,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,EACnB,eAAe,EACf,gBAAgB,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,SAAS,IAAI,iBAAiB,EAC9B,YAAY,EACZ,QAAQ,IAAI,gBAAgB,EAC5B,kBAAkB,GAClB,CAAC;AAEF,OAAO,EACN,MAAM,EACN,OAAO,EACP,MAAM,GACN,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,iBAAiB,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE,YAAY,EACX,aAAa,EACb,aAAa,GACb,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACX,cAAc,EACd,WAAW,EACX,cAAc,EACd,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,aAAa,GACb,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC/D,YAAY,EACX,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,WAAW,EACX,YAAY,EACZ,WAAW,EACX,WAAW,EACX,mBAAmB,EACnB,wBAAwB,EACxB,mBAAmB,EACnB,eAAe,EACf,gBAAgB,GAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EACN,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,SAAS,IAAI,iBAAiB,EAC9B,YAAY,EACZ,QAAQ,IAAI,gBAAgB,EAC5B,kBAAkB,GAClB,CAAC;AAEF,OAAO,EACN,MAAM,EACN,OAAO,EACP,MAAM,GACN,MAAM,4BAA4B,CAAC"}
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { HoneyPot } from "./Honeypot";
2
- import { SshClient } from "./SSHClient";
1
+ import { HoneyPot } from "./Honeypot/index";
2
+ import { SshClient } from "./SSHClient/index";
3
3
  import { SftpMimic, SshMimic } from "./SSHMimic/index";
4
- import VirtualFileSystem from "./VirtualFileSystem";
5
- import { VirtualShell } from "./VirtualShell";
6
- import { VirtualUserManager } from "./VirtualUserManager";
4
+ import VirtualFileSystem from "./VirtualFileSystem/index";
5
+ import { VirtualShell } from "./VirtualShell/index";
6
+ import { VirtualUserManager } from "./VirtualUserManager/index";
7
7
  export { HoneyPot, SshClient, VirtualFileSystem, SftpMimic as VirtualSftpServer, VirtualShell, SshMimic as VirtualSshServer, VirtualUserManager, };
8
8
  export { getArg, getFlag, ifFlag, } from "./commands/command-helpers";
@@ -0,0 +1,9 @@
1
+ export type PerfLogger = {
2
+ enabled: boolean;
3
+ mark: (label: string) => void;
4
+ done: (label?: string) => void;
5
+ };
6
+ export declare function isPerfLoggingEnabled(): boolean;
7
+ export declare function createPerfLogger(scope: string): PerfLogger;
8
+ export declare function withPerf<T>(scope: string, label: string, work: () => Promise<T>): Promise<T>;
9
+ //# sourceMappingURL=perfLogger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perfLogger.d.ts","sourceRoot":"","sources":["../../src/utils/perfLogger.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,IAAI,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B,CAAC;AAiBF,wBAAgB,oBAAoB,IAAI,OAAO,CAI9C;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CA0B1D;AAED,wBAAsB,QAAQ,CAAC,CAAC,EAC/B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,CAAC,CAAC,CAYZ"}
@@ -0,0 +1,49 @@
1
+ function isTruthyEnv(value) {
2
+ return value === "1" || value === "true";
3
+ }
4
+ function nowMs() {
5
+ if (typeof performance !== "undefined" &&
6
+ typeof performance.now === "function") {
7
+ return performance.now();
8
+ }
9
+ return Date.now();
10
+ }
11
+ export function isPerfLoggingEnabled() {
12
+ return (isTruthyEnv(process.env.DEV_MODE) || isTruthyEnv(process.env.RENDER_PERF));
13
+ }
14
+ export function createPerfLogger(scope) {
15
+ const enabled = isPerfLoggingEnabled();
16
+ if (!enabled) {
17
+ return {
18
+ enabled,
19
+ mark: () => undefined,
20
+ done: () => undefined,
21
+ };
22
+ }
23
+ const startedAt = nowMs();
24
+ const mark = (label) => {
25
+ const elapsedMs = nowMs() - startedAt;
26
+ console.log(`[perf][${scope}] ${label}: ${elapsedMs.toFixed(1)}ms`);
27
+ };
28
+ const done = (label = "done") => {
29
+ mark(label);
30
+ };
31
+ return {
32
+ enabled,
33
+ mark,
34
+ done,
35
+ };
36
+ }
37
+ export async function withPerf(scope, label, work) {
38
+ const perf = createPerfLogger(scope);
39
+ if (!perf.enabled) {
40
+ return work();
41
+ }
42
+ perf.mark(`${label}:start`);
43
+ try {
44
+ return await work();
45
+ }
46
+ finally {
47
+ perf.done(`${label}:done`);
48
+ }
49
+ }
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.7",
7
+ "version": "1.2.1",
8
8
  "license": "MIT",
9
9
  "keywords": [
10
10
  "ssh",
@@ -11,6 +11,8 @@
11
11
  import type { EventEmitter } from "node:events";
12
12
  import type { SshMimic } from "../SSHMimic";
13
13
  import type { SftpMimic } from "../SSHMimic/sftp";
14
+ import type { PerfLogger } from "../utils/perfLogger";
15
+ import { createPerfLogger } from "../utils/perfLogger";
14
16
  import type VirtualFileSystem from "../VirtualFileSystem";
15
17
  import type { VirtualShell } from "../VirtualShell";
16
18
  import type { VirtualUserManager } from "../VirtualUserManager";
@@ -43,6 +45,8 @@ export interface HoneyPotStats {
43
45
  clientDisconnects: number;
44
46
  }
45
47
 
48
+ const perf: PerfLogger = createPerfLogger("HoneyPot");
49
+
46
50
  /**
47
51
  * HoneyPot audit and event tracking utility.
48
52
  *
@@ -74,6 +78,7 @@ export class HoneyPot {
74
78
  * @param maxLogSize Maximum audit log entries to retain (default: 10000).
75
79
  */
76
80
  constructor(maxLogSize: number = 10000) {
81
+ perf.mark("constructor");
77
82
  this.maxLogSize = maxLogSize;
78
83
  }
79
84
 
@@ -93,6 +98,7 @@ export class HoneyPot {
93
98
  ssh?: SshMimic,
94
99
  sftp?: SftpMimic,
95
100
  ): void {
101
+ perf.mark("attach");
96
102
  this.attachVirtualShell(shell);
97
103
  this.attachVirtualFileSystem(vfs);
98
104
  this.attachVirtualUserManager(users);
@@ -312,6 +318,7 @@ export class HoneyPot {
312
318
  * @returns Filtered audit log entries.
313
319
  */
314
320
  public getAuditLog(type?: string, source?: string): AuditLogEntry[] {
321
+ perf.mark("getAuditLog");
315
322
  return this.auditLog.filter(
316
323
  (entry) =>
317
324
  (!type || entry.type === type) && (!source || entry.source === source),
@@ -324,6 +331,7 @@ export class HoneyPot {
324
331
  * @returns Snapshot of honeypot stats.
325
332
  */
326
333
  public getStats(): Readonly<HoneyPotStats> {
334
+ perf.mark("getStats");
327
335
  return Object.freeze({ ...this.stats });
328
336
  }
329
337
 
@@ -331,6 +339,7 @@ export class HoneyPot {
331
339
  * Clears audit log and resets statistics.
332
340
  */
333
341
  public reset(): void {
342
+ perf.mark("reset");
334
343
  this.auditLog = [];
335
344
  this.stats = {
336
345
  authAttempts: 0,
@@ -355,6 +364,7 @@ export class HoneyPot {
355
364
  * @returns Recent audit log entries.
356
365
  */
357
366
  public getRecent(limit: number = 100): AuditLogEntry[] {
367
+ perf.mark("getRecent");
358
368
  return this.auditLog.slice(Math.max(0, this.auditLog.length - limit));
359
369
  }
360
370
 
@@ -368,6 +378,7 @@ export class HoneyPot {
368
378
  severity: "low" | "medium" | "high";
369
379
  message: string;
370
380
  }> {
381
+ perf.mark("detectAnomalies");
371
382
  const anomalies: Array<{
372
383
  type: string;
373
384
  severity: "low" | "medium" | "high";