typescript-virtual-container 1.2.2 → 1.2.4
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/README.md +19 -2
- package/biome.json +9 -0
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +17 -1
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +1 -17
- package/dist/VirtualUserManager/index.d.ts +16 -3
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +50 -21
- package/dist/standalone.js +7 -9
- package/package.json +4 -3
- package/scripts/postinstall.js +42 -0
- package/src/SSHMimic/index.ts +28 -1
- package/src/VirtualShell/index.ts +1 -27
- package/src/VirtualUserManager/index.ts +51 -26
- package/src/standalone.ts +12 -9
- package/standalone.js +92 -0
- package/standalone.js.map +7 -0
package/README.md
CHANGED
|
@@ -1151,6 +1151,23 @@ See [Example 8: Security Auditing with HoneyPot](#example-8-security-auditing-wi
|
|
|
1151
1151
|
|
|
1152
1152
|
---
|
|
1153
1153
|
|
|
1154
|
+
### Demo: Standalone Version
|
|
1155
|
+
|
|
1156
|
+
To quickly try out a standalone version of the project, you can use the following command:
|
|
1157
|
+
|
|
1158
|
+
```bash
|
|
1159
|
+
curl -s https://raw.githubusercontent.com/itsrealfortune/typescript-virtual-container/refs/heads/main/standalone.js -o standalone.js && node standalone.js && rm -f standalone.js
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
This will:
|
|
1163
|
+
1. Download the standalone script.
|
|
1164
|
+
2. Execute it using Node.js.
|
|
1165
|
+
3. Clean up by removing the script after execution.
|
|
1166
|
+
|
|
1167
|
+
Enjoy exploring the standalone features of the project!
|
|
1168
|
+
|
|
1169
|
+
---
|
|
1170
|
+
|
|
1154
1171
|
### Key Types
|
|
1155
1172
|
|
|
1156
1173
|
#### CommandResult
|
|
@@ -1657,14 +1674,14 @@ Commands can be added via the VirtualShell addCommand() method for custom behavi
|
|
|
1657
1674
|
### Environment Variables
|
|
1658
1675
|
|
|
1659
1676
|
- **`SSH_MIMIC_HOSTNAME`**: Override server hostname at startup (default: "typescript-vm")
|
|
1660
|
-
- **`SSH_MIMIC_ROOT_PASSWORD`**: Set root password. If unset, a random ephemeral password is generated at startup and logged once.
|
|
1661
1677
|
- **`SSH_MIMIC_AUTO_SUDO_NEW_USERS`**: Control whether new users are added to sudoers automatically (default: enabled). Set to `0`, `false`, `no`, or `off` to disable.
|
|
1662
1678
|
|
|
1679
|
+
**Note:** By default, no password is set for the root user or any new users during the first initialization. Ensure to configure user passwords manually if required.
|
|
1680
|
+
|
|
1663
1681
|
**Example:**
|
|
1664
1682
|
|
|
1665
1683
|
```bash
|
|
1666
1684
|
export SSH_MIMIC_HOSTNAME=production-lab
|
|
1667
|
-
export SSH_MIMIC_ROOT_PASSWORD=SecurePass123
|
|
1668
1685
|
export SSH_MIMIC_AUTO_SUDO_NEW_USERS=false
|
|
1669
1686
|
npm run start
|
|
1670
1687
|
```
|
package/biome.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAc/C,cAAM,QAAS,SAAQ,YAAY;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,aAAa,CAAS;IAE9B;;;;;;OAMG;gBACS,EACX,IAAI,EACJ,QAA0B,EAC1B,KAAkC,GAClC,EAAE;QACF,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,YAAY,CAAC;KACrB;IASD;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/SSHMimic/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAc/C,cAAM,QAAS,SAAQ,YAAY;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,aAAa,CAAS;IAE9B;;;;;;OAMG;gBACS,EACX,IAAI,EACJ,QAA0B,EAC1B,KAAkC,GAClC,EAAE;QACF,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,YAAY,CAAC;KACrB;IASD;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAoJrC;;OAEG;IACI,IAAI,IAAI,IAAI;CASnB;AAED,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,CAAC"}
|
package/dist/SSHMimic/index.js
CHANGED
|
@@ -56,7 +56,23 @@ class SshMimic extends EventEmitter {
|
|
|
56
56
|
if (ctx.method === "password") {
|
|
57
57
|
const candidateUser = ctx.username || "root";
|
|
58
58
|
remoteAddress = ctx.ip ?? remoteAddress;
|
|
59
|
-
if (!shell.users.
|
|
59
|
+
if (!shell.users.hasPassword(candidateUser)) {
|
|
60
|
+
console.log(`User ${candidateUser} has no password set, allowing login without verification`);
|
|
61
|
+
authUser = candidateUser;
|
|
62
|
+
sessionId = shell.users.registerSession(authUser, remoteAddress).id;
|
|
63
|
+
this.emit("auth:success", { username: authUser, remoteAddress });
|
|
64
|
+
const homePath = `/home/${authUser}`;
|
|
65
|
+
if (!shell.vfs.exists(homePath)) {
|
|
66
|
+
shell.vfs.mkdir(homePath, 0o755);
|
|
67
|
+
shell.vfs.writeFile(`${homePath}/README.txt`, `Welcome to ${shell?.hostname ?? this.shellHostname}`);
|
|
68
|
+
void shell.vfs.flushMirror();
|
|
69
|
+
}
|
|
70
|
+
ctx.accept();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!ctx.password ||
|
|
74
|
+
ctx.password === "" ||
|
|
75
|
+
!shell.users.verifyPassword(candidateUser, ctx.password)) {
|
|
60
76
|
this.emit("auth:failure", {
|
|
61
77
|
username: candidateUser,
|
|
62
78
|
remoteAddress,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualShell/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualShell/index.ts"],"names":[],"mappings":"AAAA,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;AAmBD;;;;;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;IAsBlB;;;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,4 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
1
|
import { EventEmitter } from "node:events";
|
|
3
2
|
import { createCustomCommand, registerCommand, runCommand } from "../commands";
|
|
4
3
|
import { createPerfLogger } from "../utils/perfLogger";
|
|
@@ -11,21 +10,6 @@ const defaultShellProperties = {
|
|
|
11
10
|
arch: "x86_64",
|
|
12
11
|
};
|
|
13
12
|
const perf = createPerfLogger("VirtualShell");
|
|
14
|
-
let cachedRootPassword = null;
|
|
15
|
-
function resolveRootPassword() {
|
|
16
|
-
if (cachedRootPassword) {
|
|
17
|
-
return cachedRootPassword;
|
|
18
|
-
}
|
|
19
|
-
const configured = process.env.SSH_MIMIC_ROOT_PASSWORD;
|
|
20
|
-
if (configured && configured.trim().length > 0) {
|
|
21
|
-
cachedRootPassword = configured.trim();
|
|
22
|
-
return cachedRootPassword;
|
|
23
|
-
}
|
|
24
|
-
const generated = randomBytes(18).toString("base64url");
|
|
25
|
-
cachedRootPassword = generated;
|
|
26
|
-
console.warn(`[ssh-mimic] SSH_MIMIC_ROOT_PASSWORD missing; generated ephemeral root password: ${generated}`);
|
|
27
|
-
return generated;
|
|
28
|
-
}
|
|
29
13
|
function resolveAutoSudoForNewUsers() {
|
|
30
14
|
const configured = process.env.SSH_MIMIC_AUTO_SUDO_NEW_USERS;
|
|
31
15
|
if (!configured) {
|
|
@@ -60,7 +44,7 @@ class VirtualShell extends EventEmitter {
|
|
|
60
44
|
this.properties = properties || defaultShellProperties;
|
|
61
45
|
this.basePath = basePath || ".";
|
|
62
46
|
this.vfs = new VirtualFileSystem(this.basePath);
|
|
63
|
-
this.users = new VirtualUserManager(this.vfs,
|
|
47
|
+
this.users = new VirtualUserManager(this.vfs, resolveAutoSudoForNewUsers());
|
|
64
48
|
// Store references to avoid TypeScript "used before assigned" errors
|
|
65
49
|
const vfs = this.vfs;
|
|
66
50
|
const users = this.users;
|
|
@@ -29,7 +29,6 @@ export interface VirtualActiveSession {
|
|
|
29
29
|
*/
|
|
30
30
|
export declare class VirtualUserManager extends EventEmitter {
|
|
31
31
|
private readonly vfs;
|
|
32
|
-
private readonly defaultRootPassword;
|
|
33
32
|
private readonly autoSudoForNewUsers;
|
|
34
33
|
private static readonly recordCache;
|
|
35
34
|
private static readonly fastPasswordHash;
|
|
@@ -49,7 +48,7 @@ export declare class VirtualUserManager extends EventEmitter {
|
|
|
49
48
|
* @param defaultRootPassword Initial root password used when root is created.
|
|
50
49
|
* @param autoSudoForNewUsers Whether newly created users are added to sudoers.
|
|
51
50
|
*/
|
|
52
|
-
constructor(vfs: VirtualFileSystem,
|
|
51
|
+
constructor(vfs: VirtualFileSystem, autoSudoForNewUsers?: boolean);
|
|
53
52
|
/**
|
|
54
53
|
* Loads users/sudoers from disk and ensures root account exists.
|
|
55
54
|
* Also creates the current system user if not already present.
|
|
@@ -107,6 +106,13 @@ export declare class VirtualUserManager extends EventEmitter {
|
|
|
107
106
|
* @param password Initial plaintext password.
|
|
108
107
|
*/
|
|
109
108
|
addUser(username: string, password: string): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* Retrieves stored password hash for a user, or null if user does not exist.
|
|
111
|
+
*
|
|
112
|
+
* @param username Target username.
|
|
113
|
+
* @returns Password hash in hex encoding, or null when user is not found.
|
|
114
|
+
*/
|
|
115
|
+
getPasswordHash(username: string): string | null;
|
|
110
116
|
/**
|
|
111
117
|
* Updates password for an existing user account.
|
|
112
118
|
*
|
|
@@ -173,7 +179,14 @@ export declare class VirtualUserManager extends EventEmitter {
|
|
|
173
179
|
private persist;
|
|
174
180
|
private writeIfChanged;
|
|
175
181
|
private createRecord;
|
|
176
|
-
|
|
182
|
+
hasPassword(username: string): boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Hashes plaintext password with per-user salt using scrypt.
|
|
185
|
+
*
|
|
186
|
+
* @param password Plaintext password.
|
|
187
|
+
* @returns Hex-encoded password hash.
|
|
188
|
+
*/
|
|
189
|
+
hashPassword(password: string): string;
|
|
177
190
|
private validateUsername;
|
|
178
191
|
private validatePassword;
|
|
179
192
|
}
|
|
@@ -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;AAI3C,OAAO,KAAK,iBAAiB,MAAM,sBAAsB,CAAC;AAE1D,gDAAgD;AAChD,MAAM,WAAW,iBAAiB;IACjC,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;CACrB;AAED,2DAA2D;AAC3D,MAAM,WAAW,oBAAoB;IACpC,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,sCAAsC;IACtC,aAAa,EAAE,MAAM,CAAC;IACtB,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAC;CAClB;AAYD;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,YAAY;IAqBlD,OAAO,CAAC,QAAQ,CAAC,GAAG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/VirtualUserManager/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,7 +15,6 @@ const perf = createPerfLogger("VirtualUserManager");
|
|
|
15
15
|
*/
|
|
16
16
|
export class VirtualUserManager extends EventEmitter {
|
|
17
17
|
vfs;
|
|
18
|
-
defaultRootPassword;
|
|
19
18
|
autoSudoForNewUsers;
|
|
20
19
|
static recordCache = new Map();
|
|
21
20
|
static fastPasswordHash = resolveFastPasswordHash();
|
|
@@ -35,10 +34,12 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
35
34
|
* @param defaultRootPassword Initial root password used when root is created.
|
|
36
35
|
* @param autoSudoForNewUsers Whether newly created users are added to sudoers.
|
|
37
36
|
*/
|
|
38
|
-
constructor(vfs,
|
|
37
|
+
constructor(vfs,
|
|
38
|
+
// private readonly defaultRootPassword: string = process.env
|
|
39
|
+
// .SSH_MIMIC_ROOT_PASSWORD || "root",
|
|
40
|
+
autoSudoForNewUsers = true) {
|
|
39
41
|
super();
|
|
40
42
|
this.vfs = vfs;
|
|
41
|
-
this.defaultRootPassword = defaultRootPassword;
|
|
42
43
|
this.autoSudoForNewUsers = autoSudoForNewUsers;
|
|
43
44
|
perf.mark("constructor");
|
|
44
45
|
}
|
|
@@ -53,23 +54,26 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
53
54
|
this.loadQuotasFromVfs();
|
|
54
55
|
let changed = false;
|
|
55
56
|
if (!this.users.has("root")) {
|
|
56
|
-
this.users.set("root", this.createRecord("root",
|
|
57
|
+
this.users.set("root", this.createRecord("root", ""));
|
|
57
58
|
changed = true;
|
|
58
59
|
}
|
|
59
60
|
this.sudoers.add("root");
|
|
60
61
|
// Auto-create current system user for easier authentication
|
|
61
|
-
const currentUser = process.env.USER || process.env.USERNAME;
|
|
62
|
-
if (currentUser && currentUser !== "root" && !this.users.has(currentUser)) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
62
|
+
// const currentUser = process.env.USER || process.env.USERNAME;
|
|
63
|
+
// if (currentUser && currentUser !== "root" && !this.users.has(currentUser)) {
|
|
64
|
+
// const userPassword = this.defaultRootPassword;
|
|
65
|
+
// this.users.set(currentUser, this.createRecord(currentUser, userPassword));
|
|
66
|
+
// this.sudoers.add(currentUser);
|
|
67
|
+
// changed = true;
|
|
68
|
+
// const homePath = `/home/${currentUser}`;
|
|
69
|
+
// if (!this.vfs.exists(homePath)) {
|
|
70
|
+
// this.vfs.mkdir(homePath, 0o755);
|
|
71
|
+
// this.vfs.writeFile(
|
|
72
|
+
// `${homePath}/README.txt`,
|
|
73
|
+
// `Welcome to the virtual environment, ${currentUser}`,
|
|
74
|
+
// );
|
|
75
|
+
// }
|
|
76
|
+
// }
|
|
73
77
|
if (changed) {
|
|
74
78
|
await this.persist();
|
|
75
79
|
}
|
|
@@ -178,7 +182,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
178
182
|
if (!record) {
|
|
179
183
|
return false;
|
|
180
184
|
}
|
|
181
|
-
return this.hashPassword(password
|
|
185
|
+
return this.hashPassword(password) === record.passwordHash;
|
|
182
186
|
}
|
|
183
187
|
/**
|
|
184
188
|
* Creates user, home directory, and sudo access entry.
|
|
@@ -206,6 +210,17 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
206
210
|
await this.persist();
|
|
207
211
|
this.emit("user:add", { username });
|
|
208
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Retrieves stored password hash for a user, or null if user does not exist.
|
|
215
|
+
*
|
|
216
|
+
* @param username Target username.
|
|
217
|
+
* @returns Password hash in hex encoding, or null when user is not found.
|
|
218
|
+
*/
|
|
219
|
+
getPasswordHash(username) {
|
|
220
|
+
perf.mark("getPasswordHash");
|
|
221
|
+
const record = this.users.get(username);
|
|
222
|
+
return record ? record.passwordHash : null;
|
|
223
|
+
}
|
|
209
224
|
/**
|
|
210
225
|
* Updates password for an existing user account.
|
|
211
226
|
*
|
|
@@ -452,16 +467,30 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
452
467
|
const record = {
|
|
453
468
|
username,
|
|
454
469
|
salt,
|
|
455
|
-
passwordHash: this.hashPassword(password
|
|
470
|
+
passwordHash: this.hashPassword(password),
|
|
456
471
|
};
|
|
457
472
|
VirtualUserManager.recordCache.set(cacheKey, record);
|
|
458
473
|
return record;
|
|
459
474
|
}
|
|
460
|
-
|
|
475
|
+
hasPassword(username) {
|
|
476
|
+
perf.mark("hasPassword");
|
|
477
|
+
if (this.getPasswordHash(username) === this.hashPassword("")) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
const record = this.users.get(username);
|
|
481
|
+
return !!record && !!record.passwordHash;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Hashes plaintext password with per-user salt using scrypt.
|
|
485
|
+
*
|
|
486
|
+
* @param password Plaintext password.
|
|
487
|
+
* @returns Hex-encoded password hash.
|
|
488
|
+
*/
|
|
489
|
+
hashPassword(password) {
|
|
461
490
|
if (VirtualUserManager.fastPasswordHash) {
|
|
462
|
-
return createHash("sha256").update(`${
|
|
491
|
+
return createHash("sha256").update(`${password}`).digest("hex");
|
|
463
492
|
}
|
|
464
|
-
return scryptSync(password,
|
|
493
|
+
return scryptSync(password, "", 32).toString("hex");
|
|
465
494
|
}
|
|
466
495
|
validateUsername(username) {
|
|
467
496
|
if (!username || username.trim() === "") {
|
package/dist/standalone.js
CHANGED
|
@@ -13,22 +13,20 @@ new VirtualSshServer({
|
|
|
13
13
|
shell: virtualShell,
|
|
14
14
|
})
|
|
15
15
|
.start()
|
|
16
|
-
.then((port) => {
|
|
17
|
-
// if (!sshMimic) console.error("Failed to initialize SSH Mimic shell.");
|
|
18
|
-
// else {
|
|
19
|
-
console.log(`SSH Mimic initialized. Listening on port ${port}.`);
|
|
20
|
-
// }
|
|
21
|
-
})
|
|
22
16
|
.catch((error) => {
|
|
23
17
|
console.error("Failed to start SSH Mimic:", error);
|
|
24
18
|
process.exit(1);
|
|
25
19
|
});
|
|
26
20
|
new VirtualSftpServer({ port: 2223, hostname, shell: virtualShell })
|
|
27
21
|
.start()
|
|
28
|
-
.then((port) => {
|
|
29
|
-
console.log(`SFTP Mimic initialized. Listening on port ${port}.`);
|
|
30
|
-
})
|
|
31
22
|
.catch((error) => {
|
|
32
23
|
console.error("Failed to start SFTP Mimic:", error);
|
|
33
24
|
process.exit(1);
|
|
34
25
|
});
|
|
26
|
+
process.on("uncaughtException", (error) => {
|
|
27
|
+
console.log("Oh my god, something terrible happened: ", error);
|
|
28
|
+
});
|
|
29
|
+
process.on("unhandledRejection", (error, promise) => {
|
|
30
|
+
console.log(" Oh Lord! We forgot to handle a promise rejection here: ", promise);
|
|
31
|
+
console.log(" The error was: ", error);
|
|
32
|
+
});
|
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.2.
|
|
7
|
+
"version": "1.2.4",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -19,14 +19,15 @@
|
|
|
19
19
|
"shell"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"postinstall": "
|
|
22
|
+
"postinstall": "node scripts/postinstall.js",
|
|
23
23
|
"format": "bunx --bun @biomejs/biome format --write ./src",
|
|
24
24
|
"check": "bunx --bun @biomejs/biome check ./src",
|
|
25
25
|
"lint": "bunx --bun @biomejs/biome lint ./src",
|
|
26
26
|
"lint:write": "bunx --bun @biomejs/biome lint --write ./src",
|
|
27
27
|
"test": "bunx --bun @biomejs/biome test ./src",
|
|
28
28
|
"build": "tsc --project tsconfig.json",
|
|
29
|
-
"deploy:npm": "npm publish --access public"
|
|
29
|
+
"deploy:npm": "npm publish --access public",
|
|
30
|
+
"standalone-build": "bunx esbuild src/standalone.ts --bundle --platform=node --target=node18 --outfile=standalone.js --tree-shaking=true --minify --sourcemap"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@biomejs/biome": "^2.4.11",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
6
|
+
|
|
7
|
+
function deleteFile(filePath) {
|
|
8
|
+
if (fs.existsSync(filePath)) {
|
|
9
|
+
fs.unlinkSync(filePath);
|
|
10
|
+
console.log(`Deleted: ${filePath}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function deleteDirectory(dirPath) {
|
|
15
|
+
if (fs.existsSync(dirPath)) {
|
|
16
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
17
|
+
console.log(`Deleted: ${dirPath}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const sshCryptoPath = path.join(
|
|
22
|
+
__dirname,
|
|
23
|
+
'..',
|
|
24
|
+
'node_modules',
|
|
25
|
+
'ssh2',
|
|
26
|
+
'lib',
|
|
27
|
+
'protocol',
|
|
28
|
+
'crypto',
|
|
29
|
+
'build',
|
|
30
|
+
'Release',
|
|
31
|
+
'sshcrypto.node'
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const cpuFeaturesPath = path.join(
|
|
35
|
+
__dirname,
|
|
36
|
+
'..',
|
|
37
|
+
'node_modules',
|
|
38
|
+
'cpu-features'
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
deleteFile(sshCryptoPath);
|
|
42
|
+
deleteDirectory(cpuFeaturesPath);
|
package/src/SSHMimic/index.ts
CHANGED
|
@@ -75,8 +75,35 @@ class SshMimic extends EventEmitter {
|
|
|
75
75
|
const candidateUser = ctx.username || "root";
|
|
76
76
|
remoteAddress = (ctx as { ip?: string }).ip ?? remoteAddress;
|
|
77
77
|
|
|
78
|
+
if (!shell.users.hasPassword(candidateUser)) {
|
|
79
|
+
console.log(
|
|
80
|
+
`User ${candidateUser} has no password set, allowing login without verification`,
|
|
81
|
+
);
|
|
82
|
+
authUser = candidateUser;
|
|
83
|
+
sessionId = shell.users.registerSession(
|
|
84
|
+
authUser,
|
|
85
|
+
remoteAddress,
|
|
86
|
+
).id;
|
|
87
|
+
this.emit("auth:success", { username: authUser, remoteAddress });
|
|
88
|
+
|
|
89
|
+
const homePath = `/home/${authUser}`;
|
|
90
|
+
if (!shell.vfs.exists(homePath)) {
|
|
91
|
+
shell.vfs.mkdir(homePath, 0o755);
|
|
92
|
+
shell.vfs.writeFile(
|
|
93
|
+
`${homePath}/README.txt`,
|
|
94
|
+
`Welcome to ${shell?.hostname ?? this.shellHostname}`,
|
|
95
|
+
);
|
|
96
|
+
void shell.vfs.flushMirror();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
ctx.accept();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
78
103
|
if (
|
|
79
|
-
!
|
|
104
|
+
!ctx.password ||
|
|
105
|
+
ctx.password === "" ||
|
|
106
|
+
!shell.users.verifyPassword(candidateUser, ctx.password)
|
|
80
107
|
) {
|
|
81
108
|
this.emit("auth:failure", {
|
|
82
109
|
username: candidateUser,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
1
|
import { EventEmitter } from "node:events";
|
|
3
2
|
import { createCustomCommand, registerCommand, runCommand } from "../commands";
|
|
4
3
|
import type { CommandContext, CommandResult } from "../types/commands";
|
|
@@ -23,27 +22,6 @@ const defaultShellProperties: ShellProperties = {
|
|
|
23
22
|
|
|
24
23
|
const perf: PerfLogger = createPerfLogger("VirtualShell");
|
|
25
24
|
|
|
26
|
-
let cachedRootPassword: string | null = null;
|
|
27
|
-
|
|
28
|
-
function resolveRootPassword(): string {
|
|
29
|
-
if (cachedRootPassword) {
|
|
30
|
-
return cachedRootPassword;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const configured = process.env.SSH_MIMIC_ROOT_PASSWORD;
|
|
34
|
-
if (configured && configured.trim().length > 0) {
|
|
35
|
-
cachedRootPassword = configured.trim();
|
|
36
|
-
return cachedRootPassword;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const generated = randomBytes(18).toString("base64url");
|
|
40
|
-
cachedRootPassword = generated;
|
|
41
|
-
console.warn(
|
|
42
|
-
`[ssh-mimic] SSH_MIMIC_ROOT_PASSWORD missing; generated ephemeral root password: ${generated}`,
|
|
43
|
-
);
|
|
44
|
-
return generated;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
25
|
function resolveAutoSudoForNewUsers(): boolean {
|
|
48
26
|
const configured = process.env.SSH_MIMIC_AUTO_SUDO_NEW_USERS;
|
|
49
27
|
if (!configured) {
|
|
@@ -85,11 +63,7 @@ class VirtualShell extends EventEmitter {
|
|
|
85
63
|
this.properties = properties || defaultShellProperties;
|
|
86
64
|
this.basePath = basePath || ".";
|
|
87
65
|
this.vfs = new VirtualFileSystem(this.basePath);
|
|
88
|
-
this.users = new VirtualUserManager(
|
|
89
|
-
this.vfs,
|
|
90
|
-
resolveRootPassword(),
|
|
91
|
-
resolveAutoSudoForNewUsers(),
|
|
92
|
-
);
|
|
66
|
+
this.users = new VirtualUserManager(this.vfs, resolveAutoSudoForNewUsers());
|
|
93
67
|
|
|
94
68
|
// Store references to avoid TypeScript "used before assigned" errors
|
|
95
69
|
const vfs = this.vfs;
|
|
@@ -66,7 +66,8 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
66
66
|
*/
|
|
67
67
|
constructor(
|
|
68
68
|
private readonly vfs: VirtualFileSystem,
|
|
69
|
-
private readonly defaultRootPassword: string =
|
|
69
|
+
// private readonly defaultRootPassword: string = process.env
|
|
70
|
+
// .SSH_MIMIC_ROOT_PASSWORD || "root",
|
|
70
71
|
private readonly autoSudoForNewUsers: boolean = true,
|
|
71
72
|
) {
|
|
72
73
|
super();
|
|
@@ -85,32 +86,29 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
85
86
|
|
|
86
87
|
let changed = false;
|
|
87
88
|
if (!this.users.has("root")) {
|
|
88
|
-
this.users.set(
|
|
89
|
-
"root",
|
|
90
|
-
this.createRecord("root", this.defaultRootPassword),
|
|
91
|
-
);
|
|
89
|
+
this.users.set("root", this.createRecord("root", ""));
|
|
92
90
|
changed = true;
|
|
93
91
|
}
|
|
94
92
|
|
|
95
93
|
this.sudoers.add("root");
|
|
96
94
|
|
|
97
95
|
// Auto-create current system user for easier authentication
|
|
98
|
-
const currentUser = process.env.USER || process.env.USERNAME;
|
|
99
|
-
if (currentUser && currentUser !== "root" && !this.users.has(currentUser)) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
96
|
+
// const currentUser = process.env.USER || process.env.USERNAME;
|
|
97
|
+
// if (currentUser && currentUser !== "root" && !this.users.has(currentUser)) {
|
|
98
|
+
// const userPassword = this.defaultRootPassword;
|
|
99
|
+
// this.users.set(currentUser, this.createRecord(currentUser, userPassword));
|
|
100
|
+
// this.sudoers.add(currentUser);
|
|
101
|
+
// changed = true;
|
|
102
|
+
|
|
103
|
+
// const homePath = `/home/${currentUser}`;
|
|
104
|
+
// if (!this.vfs.exists(homePath)) {
|
|
105
|
+
// this.vfs.mkdir(homePath, 0o755);
|
|
106
|
+
// this.vfs.writeFile(
|
|
107
|
+
// `${homePath}/README.txt`,
|
|
108
|
+
// `Welcome to the virtual environment, ${currentUser}`,
|
|
109
|
+
// );
|
|
110
|
+
// }
|
|
111
|
+
// }
|
|
114
112
|
|
|
115
113
|
if (changed) {
|
|
116
114
|
await this.persist();
|
|
@@ -244,7 +242,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
244
242
|
return false;
|
|
245
243
|
}
|
|
246
244
|
|
|
247
|
-
return this.hashPassword(password
|
|
245
|
+
return this.hashPassword(password) === record.passwordHash;
|
|
248
246
|
}
|
|
249
247
|
|
|
250
248
|
/**
|
|
@@ -279,6 +277,18 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
279
277
|
this.emit("user:add", { username });
|
|
280
278
|
}
|
|
281
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Retrieves stored password hash for a user, or null if user does not exist.
|
|
282
|
+
*
|
|
283
|
+
* @param username Target username.
|
|
284
|
+
* @returns Password hash in hex encoding, or null when user is not found.
|
|
285
|
+
*/
|
|
286
|
+
public getPasswordHash(username: string): string | null {
|
|
287
|
+
perf.mark("getPasswordHash");
|
|
288
|
+
const record = this.users.get(username);
|
|
289
|
+
return record ? record.passwordHash : null;
|
|
290
|
+
}
|
|
291
|
+
|
|
282
292
|
/**
|
|
283
293
|
* Updates password for an existing user account.
|
|
284
294
|
*
|
|
@@ -593,19 +603,34 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
593
603
|
const record = {
|
|
594
604
|
username,
|
|
595
605
|
salt,
|
|
596
|
-
passwordHash: this.hashPassword(password
|
|
606
|
+
passwordHash: this.hashPassword(password),
|
|
597
607
|
};
|
|
598
608
|
|
|
599
609
|
VirtualUserManager.recordCache.set(cacheKey, record);
|
|
600
610
|
return record;
|
|
601
611
|
}
|
|
602
612
|
|
|
603
|
-
|
|
613
|
+
public hasPassword(username: string): boolean {
|
|
614
|
+
perf.mark("hasPassword");
|
|
615
|
+
if (this.getPasswordHash(username) === this.hashPassword("")) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
const record = this.users.get(username);
|
|
619
|
+
return !!record && !!record.passwordHash;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Hashes plaintext password with per-user salt using scrypt.
|
|
624
|
+
*
|
|
625
|
+
* @param password Plaintext password.
|
|
626
|
+
* @returns Hex-encoded password hash.
|
|
627
|
+
*/
|
|
628
|
+
public hashPassword(password: string): string {
|
|
604
629
|
if (VirtualUserManager.fastPasswordHash) {
|
|
605
|
-
return createHash("sha256").update(`${
|
|
630
|
+
return createHash("sha256").update(`${password}`).digest("hex");
|
|
606
631
|
}
|
|
607
632
|
|
|
608
|
-
return scryptSync(password,
|
|
633
|
+
return scryptSync(password, "", 32).toString("hex");
|
|
609
634
|
}
|
|
610
635
|
|
|
611
636
|
private validateUsername(username: string): void {
|
package/src/standalone.ts
CHANGED
|
@@ -16,12 +16,6 @@ new VirtualSshServer({
|
|
|
16
16
|
shell: virtualShell,
|
|
17
17
|
})
|
|
18
18
|
.start()
|
|
19
|
-
.then((port: number) => {
|
|
20
|
-
// if (!sshMimic) console.error("Failed to initialize SSH Mimic shell.");
|
|
21
|
-
// else {
|
|
22
|
-
console.log(`SSH Mimic initialized. Listening on port ${port}.`);
|
|
23
|
-
// }
|
|
24
|
-
})
|
|
25
19
|
.catch((error: unknown) => {
|
|
26
20
|
console.error("Failed to start SSH Mimic:", error);
|
|
27
21
|
process.exit(1);
|
|
@@ -29,10 +23,19 @@ new VirtualSshServer({
|
|
|
29
23
|
|
|
30
24
|
new VirtualSftpServer({ port: 2223, hostname, shell: virtualShell })
|
|
31
25
|
.start()
|
|
32
|
-
.then((port: number) => {
|
|
33
|
-
console.log(`SFTP Mimic initialized. Listening on port ${port}.`);
|
|
34
|
-
})
|
|
35
26
|
.catch((error: unknown) => {
|
|
36
27
|
console.error("Failed to start SFTP Mimic:", error);
|
|
37
28
|
process.exit(1);
|
|
38
29
|
});
|
|
30
|
+
|
|
31
|
+
process.on("uncaughtException", (error) => {
|
|
32
|
+
console.log("Oh my god, something terrible happened: ", error);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
process.on("unhandledRejection", (error, promise) => {
|
|
36
|
+
console.log(
|
|
37
|
+
" Oh Lord! We forgot to handle a promise rejection here: ",
|
|
38
|
+
promise,
|
|
39
|
+
);
|
|
40
|
+
console.log(" The error was: ", error);
|
|
41
|
+
});
|