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
|
@@ -2,6 +2,7 @@ import { EventEmitter } from "node:events";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
5
|
+
import { createPerfLogger } from "../utils/perfLogger";
|
|
5
6
|
import { normalizePath } from "./path";
|
|
6
7
|
/**
|
|
7
8
|
* In-memory virtual filesystem with tar.gz mirror persistence.
|
|
@@ -10,6 +11,7 @@ import { normalizePath } from "./path";
|
|
|
10
11
|
* {@link VirtualFileSystem.restoreMirror} on startup and
|
|
11
12
|
* {@link VirtualFileSystem.flushMirror} to persist pending changes.
|
|
12
13
|
*/
|
|
14
|
+
const perf = createPerfLogger("VirtualFileSystem");
|
|
13
15
|
class VirtualFileSystem extends EventEmitter {
|
|
14
16
|
mirrorRoot;
|
|
15
17
|
ensureMirrorRoot() {
|
|
@@ -77,6 +79,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
77
79
|
*/
|
|
78
80
|
constructor(baseDir = process.cwd()) {
|
|
79
81
|
super();
|
|
82
|
+
perf.mark("constructor");
|
|
80
83
|
this.mirrorRoot = path.resolve(baseDir, ".vfs", "mirror");
|
|
81
84
|
}
|
|
82
85
|
/**
|
|
@@ -85,6 +88,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
85
88
|
* If archive does not exist or cannot be read, creates fresh mirror file.
|
|
86
89
|
*/
|
|
87
90
|
async restoreMirror() {
|
|
91
|
+
perf.mark("restoreMirror");
|
|
88
92
|
this.ensureMirrorRoot();
|
|
89
93
|
}
|
|
90
94
|
/**
|
|
@@ -93,6 +97,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
93
97
|
* No-op when nothing changed and archive already exists.
|
|
94
98
|
*/
|
|
95
99
|
async flushMirror() {
|
|
100
|
+
perf.mark("flushMirror");
|
|
96
101
|
this.ensureMirrorRoot();
|
|
97
102
|
this.emit("mirror:flush");
|
|
98
103
|
}
|
|
@@ -103,6 +108,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
103
108
|
* @param mode POSIX-like mode bits for new directories.
|
|
104
109
|
*/
|
|
105
110
|
mkdir(targetPath, mode = 0o755) {
|
|
111
|
+
perf.mark("mkdir");
|
|
106
112
|
this.ensureMirrorRoot();
|
|
107
113
|
const fsPath = this.resolveFsPath(targetPath);
|
|
108
114
|
if (fs.existsSync(fsPath) && !fs.statSync(fsPath).isDirectory()) {
|
|
@@ -121,6 +127,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
121
127
|
* @param options Optional write behavior (mode, compression).
|
|
122
128
|
*/
|
|
123
129
|
writeFile(targetPath, content, options = {}) {
|
|
130
|
+
perf.mark("writeFile");
|
|
124
131
|
this.ensureMirrorRoot();
|
|
125
132
|
const normalized = normalizePath(targetPath);
|
|
126
133
|
const fsPath = this.resolveFsPath(normalized);
|
|
@@ -147,6 +154,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
147
154
|
* @returns UTF-8 string content.
|
|
148
155
|
*/
|
|
149
156
|
readFile(targetPath) {
|
|
157
|
+
perf.mark("readFile");
|
|
150
158
|
this.ensureMirrorRoot();
|
|
151
159
|
const fsPath = this.resolveFsPath(targetPath);
|
|
152
160
|
if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isFile()) {
|
|
@@ -165,6 +173,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
165
173
|
* @returns True when file or directory exists.
|
|
166
174
|
*/
|
|
167
175
|
exists(targetPath) {
|
|
176
|
+
perf.mark("exists");
|
|
168
177
|
try {
|
|
169
178
|
const fsPath = this.resolveFsPath(targetPath);
|
|
170
179
|
return fs.existsSync(fsPath);
|
|
@@ -180,6 +189,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
180
189
|
* @param mode New POSIX-like mode.
|
|
181
190
|
*/
|
|
182
191
|
chmod(targetPath, mode) {
|
|
192
|
+
perf.mark("chmod");
|
|
183
193
|
const fsPath = this.resolveFsPath(targetPath);
|
|
184
194
|
if (!fs.existsSync(fsPath)) {
|
|
185
195
|
throw new Error(`Path '${normalizePath(targetPath)}' does not exist.`);
|
|
@@ -193,6 +203,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
193
203
|
* @returns Typed stat object based on node type.
|
|
194
204
|
*/
|
|
195
205
|
stat(targetPath) {
|
|
206
|
+
perf.mark("stat");
|
|
196
207
|
this.ensureMirrorRoot();
|
|
197
208
|
const normalized = normalizePath(targetPath);
|
|
198
209
|
const fsPath = this.resolveFsPath(normalized);
|
|
@@ -231,6 +242,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
231
242
|
* @returns Sorted child names.
|
|
232
243
|
*/
|
|
233
244
|
list(dirPath = "/") {
|
|
245
|
+
perf.mark("list");
|
|
234
246
|
const fsPath = this.resolveFsPath(dirPath);
|
|
235
247
|
if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isDirectory()) {
|
|
236
248
|
throw new Error(`Cannot list '${dirPath}': not a directory.`);
|
|
@@ -244,6 +256,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
244
256
|
* @returns Multi-line tree string.
|
|
245
257
|
*/
|
|
246
258
|
tree(dirPath = "/") {
|
|
259
|
+
perf.mark("tree");
|
|
247
260
|
const fsPath = this.resolveFsPath(dirPath);
|
|
248
261
|
if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isDirectory()) {
|
|
249
262
|
throw new Error(`Cannot render tree for '${dirPath}': not a directory.`);
|
|
@@ -261,6 +274,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
261
274
|
* @returns Total byte usage for file content under target path.
|
|
262
275
|
*/
|
|
263
276
|
getUsageBytes(targetPath = "/") {
|
|
277
|
+
perf.mark("getUsageBytes");
|
|
264
278
|
const fsPath = this.resolveFsPath(targetPath);
|
|
265
279
|
if (!fs.existsSync(fsPath)) {
|
|
266
280
|
throw new Error(`Path '${normalizePath(targetPath)}' does not exist.`);
|
|
@@ -273,6 +287,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
273
287
|
* @param targetPath Path to file.
|
|
274
288
|
*/
|
|
275
289
|
compressFile(targetPath) {
|
|
290
|
+
perf.mark("compressFile");
|
|
276
291
|
const fsPath = this.resolveFsPath(targetPath);
|
|
277
292
|
if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isFile()) {
|
|
278
293
|
throw new Error(`Cannot compress '${targetPath}': not a file.`);
|
|
@@ -288,6 +303,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
288
303
|
* @param targetPath Path to file.
|
|
289
304
|
*/
|
|
290
305
|
decompressFile(targetPath) {
|
|
306
|
+
perf.mark("decompressFile");
|
|
291
307
|
const fsPath = this.resolveFsPath(targetPath);
|
|
292
308
|
if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isFile()) {
|
|
293
309
|
throw new Error(`Cannot decompress '${targetPath}': not a file.`);
|
|
@@ -304,6 +320,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
304
320
|
* @param options Removal options, including recursive delete.
|
|
305
321
|
*/
|
|
306
322
|
remove(targetPath, options = {}) {
|
|
323
|
+
perf.mark("remove");
|
|
307
324
|
const normalized = normalizePath(targetPath);
|
|
308
325
|
if (normalized === "/") {
|
|
309
326
|
throw new Error("Cannot remove root directory.");
|
|
@@ -333,6 +350,7 @@ class VirtualFileSystem extends EventEmitter {
|
|
|
333
350
|
* @param toPath Destination path.
|
|
334
351
|
*/
|
|
335
352
|
move(fromPath, toPath) {
|
|
353
|
+
perf.mark("move");
|
|
336
354
|
const fromNormalized = normalizePath(fromPath);
|
|
337
355
|
const toNormalized = normalizePath(toPath);
|
|
338
356
|
if (fromNormalized === "/" || toNormalized === "/") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualShell/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualShell/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,OAAO,iBAAiB,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAG3D,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACb;AAwCD;;;;;GAKG;AACH,cAAM,YAAa,SAAQ,YAAY;IACtC,QAAQ,EAAE,MAAM,CAAO;IACvB,GAAG,EAAE,iBAAiB,CAAC;IACvB,KAAK,EAAE,kBAAkB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,eAAe,CAAC;IAC5B,OAAO,CAAC,WAAW,CAAgB;IAEnC;;;;;;OAMG;gBAEF,QAAQ,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,eAAe,EAC5B,QAAQ,CAAC,EAAE,MAAM;IA0BlB;;;OAGG;IACU,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAK/C;;;;;;OAMG;IACH,UAAU,CACT,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EAAE,EAChB,QAAQ,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,GACvE,IAAI;IASP;;;;;;OAMG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAMrE;;;;;;;OAOG;IAEH,uBAAuB,CACtB,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAC1C,IAAI;IAgBP;;;;OAIG;IACI,MAAM,IAAI,iBAAiB,GAAG,IAAI;IAIzC;;;;OAIG;IACI,QAAQ,IAAI,kBAAkB,GAAG,IAAI;IAI5C;;;;OAIG;IACI,WAAW,IAAI,MAAM;IAI5B;;;;;;OAMG;IACI,eAAe,CACrB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GAAG,MAAM,GACtB,IAAI;CAKP;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -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
|
-
|
|
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":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,6 +187,7 @@ 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)) {
|
|
@@ -190,6 +213,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
190
213
|
* @param password New plaintext password.
|
|
191
214
|
*/
|
|
192
215
|
async setPassword(username, password) {
|
|
216
|
+
perf.mark("setPassword");
|
|
193
217
|
this.validateUsername(username);
|
|
194
218
|
this.validatePassword(password);
|
|
195
219
|
if (!this.users.has(username)) {
|
|
@@ -204,6 +228,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
204
228
|
* @param username Username to remove.
|
|
205
229
|
*/
|
|
206
230
|
async deleteUser(username) {
|
|
231
|
+
perf.mark("deleteUser");
|
|
207
232
|
this.validateUsername(username);
|
|
208
233
|
if (username === "root") {
|
|
209
234
|
throw new Error("deluser: cannot delete root");
|
|
@@ -222,6 +247,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
222
247
|
* @returns True when user can run sudo.
|
|
223
248
|
*/
|
|
224
249
|
isSudoer(username) {
|
|
250
|
+
perf.mark("isSudoer");
|
|
225
251
|
return this.sudoers.has(username);
|
|
226
252
|
}
|
|
227
253
|
/**
|
|
@@ -230,6 +256,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
230
256
|
* @param username Username to promote.
|
|
231
257
|
*/
|
|
232
258
|
async addSudoer(username) {
|
|
259
|
+
perf.mark("addSudoer");
|
|
233
260
|
this.validateUsername(username);
|
|
234
261
|
if (!this.users.has(username)) {
|
|
235
262
|
throw new Error(`sudoers: user '${username}' does not exist`);
|
|
@@ -243,6 +270,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
243
270
|
* @param username Username to demote.
|
|
244
271
|
*/
|
|
245
272
|
async removeSudoer(username) {
|
|
273
|
+
perf.mark("removeSudoer");
|
|
246
274
|
this.validateUsername(username);
|
|
247
275
|
if (username === "root") {
|
|
248
276
|
throw new Error("sudoers: cannot remove root");
|
|
@@ -258,6 +286,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
258
286
|
* @returns Registered session descriptor.
|
|
259
287
|
*/
|
|
260
288
|
registerSession(username, remoteAddress) {
|
|
289
|
+
perf.mark("registerSession");
|
|
261
290
|
const session = {
|
|
262
291
|
id: randomUUID(),
|
|
263
292
|
username,
|
|
@@ -279,6 +308,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
279
308
|
* @param sessionId Session identifier; ignored when nullish.
|
|
280
309
|
*/
|
|
281
310
|
unregisterSession(sessionId) {
|
|
311
|
+
perf.mark("unregisterSession");
|
|
282
312
|
if (!sessionId) {
|
|
283
313
|
return;
|
|
284
314
|
}
|
|
@@ -300,6 +330,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
300
330
|
* @param remoteAddress New remote address value.
|
|
301
331
|
*/
|
|
302
332
|
updateSession(sessionId, username, remoteAddress) {
|
|
333
|
+
perf.mark("updateSession");
|
|
303
334
|
if (!sessionId) {
|
|
304
335
|
return;
|
|
305
336
|
}
|
|
@@ -319,6 +350,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
319
350
|
* @returns Snapshot of active session descriptors.
|
|
320
351
|
*/
|
|
321
352
|
listActiveSessions() {
|
|
353
|
+
perf.mark("listActiveSessions");
|
|
322
354
|
return Array.from(this.activeSessions.values()).sort((left, right) => left.startedAt.localeCompare(right.startedAt));
|
|
323
355
|
}
|
|
324
356
|
loadFromVfs() {
|
|
@@ -379,30 +411,57 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
379
411
|
if (!this.vfs.exists(this.authDirPath)) {
|
|
380
412
|
this.vfs.mkdir(this.authDirPath, 0o700);
|
|
381
413
|
}
|
|
382
|
-
const
|
|
414
|
+
const authContent = Array.from(this.users.values())
|
|
383
415
|
.sort((left, right) => left.username.localeCompare(right.username))
|
|
384
416
|
.map((record) => [record.username, record.salt, record.passwordHash].join(":"))
|
|
385
417
|
.join("\n");
|
|
386
|
-
this.vfs.writeFile(this.usersPath, content.length > 0 ? `${content}\n` : "", { mode: 0o600 });
|
|
387
418
|
const sudoersContent = Array.from(this.sudoers.values()).sort().join("\n");
|
|
388
|
-
this.vfs.writeFile(this.sudoersPath, sudoersContent.length > 0 ? `${sudoersContent}\n` : "", { mode: 0o600 });
|
|
389
419
|
const quotasContent = Array.from(this.quotas.entries())
|
|
390
420
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
391
421
|
.map(([username, maxBytes]) => `${username}:${maxBytes}`)
|
|
392
422
|
.join("\n");
|
|
393
|
-
|
|
394
|
-
|
|
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;
|
|
395
444
|
}
|
|
396
445
|
createRecord(username, password) {
|
|
446
|
+
const cacheKey = `${username}:${password}`;
|
|
447
|
+
const cached = VirtualUserManager.recordCache.get(cacheKey);
|
|
448
|
+
if (cached) {
|
|
449
|
+
return cached;
|
|
450
|
+
}
|
|
397
451
|
const salt = randomBytes(16).toString("hex");
|
|
398
|
-
|
|
452
|
+
const record = {
|
|
399
453
|
username,
|
|
400
454
|
salt,
|
|
401
455
|
passwordHash: this.hashPassword(password, salt),
|
|
402
456
|
};
|
|
457
|
+
VirtualUserManager.recordCache.set(cacheKey, record);
|
|
458
|
+
return record;
|
|
403
459
|
}
|
|
404
460
|
hashPassword(password, salt) {
|
|
405
|
-
|
|
461
|
+
if (VirtualUserManager.fastPasswordHash) {
|
|
462
|
+
return createHash("sha256").update(`${salt}:${password}`).digest("hex");
|
|
463
|
+
}
|
|
464
|
+
return scryptSync(password, salt, 32).toString("hex");
|
|
406
465
|
}
|
|
407
466
|
validateUsername(username) {
|
|
408
467
|
if (!username || username.trim() === "") {
|
|
@@ -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,8 +4,12 @@
|
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"version": "1.2.
|
|
7
|
+
"version": "1.2.2",
|
|
8
8
|
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/itsrealfortune/typescript-virtual-container"
|
|
12
|
+
},
|
|
9
13
|
"keywords": [
|
|
10
14
|
"ssh",
|
|
11
15
|
"virtual-filesystem",
|
|
@@ -34,7 +38,6 @@
|
|
|
34
38
|
"typescript": "^5"
|
|
35
39
|
},
|
|
36
40
|
"dependencies": {
|
|
37
|
-
"ssh2": "^1.17.0"
|
|
38
|
-
"tar-stream": "^3.1.8"
|
|
41
|
+
"ssh2": "^1.17.0"
|
|
39
42
|
}
|
|
40
43
|
}
|
package/src/Honeypot/index.ts
CHANGED
|
@@ -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";
|