typescript-virtual-container 1.1.3 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/HONEYPOT.md +358 -0
- package/README.md +471 -16
- package/dist/SSHMimic/exec.d.ts.map +1 -1
- package/dist/SSHMimic/exec.js +8 -2
- package/dist/SSHMimic/index.d.ts +3 -1
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +21 -4
- package/dist/SSHMimic/sftp.d.ts +48 -0
- package/dist/SSHMimic/sftp.d.ts.map +1 -0
- package/dist/SSHMimic/sftp.js +595 -0
- package/dist/VirtualFileSystem/index.d.ts +8 -5
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +152 -154
- package/dist/VirtualShell/index.d.ts +8 -1
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +22 -5
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +7 -0
- package/dist/VirtualUserManager/index.d.ts +3 -1
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +34 -1
- package/dist/commands/exit.d.ts.map +1 -1
- package/dist/commands/exit.js +1 -0
- package/dist/honeypot.d.ts +132 -0
- package/dist/honeypot.d.ts.map +1 -0
- package/dist/honeypot.js +289 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/standalone.js +10 -1
- package/examples/README.md +210 -0
- package/examples/honeypot-audit.ts +180 -0
- package/examples/honeypot-export.ts +253 -0
- package/examples/honeypot-quickstart.ts +110 -0
- package/package.json +1 -1
- package/src/Honeypot/index.ts +422 -0
- package/src/SSHMimic/exec.ts +18 -12
- package/src/SSHMimic/index.ts +29 -8
- package/src/SSHMimic/sftp.ts +853 -0
- package/src/VirtualFileSystem/index.ts +167 -190
- package/src/VirtualShell/index.ts +25 -9
- package/src/VirtualShell/shell.ts +7 -0
- package/src/VirtualUserManager/index.ts +41 -3
- package/src/commands/exit.ts +1 -0
- package/src/index.ts +8 -1
- package/src/standalone.ts +11 -1
- package/tests/sftp.test.ts +319 -0
- package/tests/ssh-exec.test.ts +45 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes, randomUUID, scryptSync } from "node:crypto";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
4
5
|
|
|
@@ -31,7 +32,7 @@ export interface VirtualActiveSession {
|
|
|
31
32
|
*
|
|
32
33
|
* Passwords are hashed with scrypt and stored in the backing virtual filesystem.
|
|
33
34
|
*/
|
|
34
|
-
export class VirtualUserManager {
|
|
35
|
+
export class VirtualUserManager extends EventEmitter {
|
|
35
36
|
private readonly usersPath = "/virtual-env-js/.auth/htpasswd";
|
|
36
37
|
private readonly sudoersPath = "/virtual-env-js/.auth/sudoers";
|
|
37
38
|
private readonly quotasPath = "/virtual-env-js/.auth/quotas";
|
|
@@ -53,10 +54,13 @@ export class VirtualUserManager {
|
|
|
53
54
|
private readonly vfs: VirtualFileSystem,
|
|
54
55
|
private readonly defaultRootPassword: string = "root",
|
|
55
56
|
private readonly autoSudoForNewUsers: boolean = true,
|
|
56
|
-
) {
|
|
57
|
+
) {
|
|
58
|
+
super();
|
|
59
|
+
}
|
|
57
60
|
|
|
58
61
|
/**
|
|
59
62
|
* Loads users/sudoers from disk and ensures root account exists.
|
|
63
|
+
* Also creates the current system user if not already present.
|
|
60
64
|
*/
|
|
61
65
|
public async initialize(): Promise<void> {
|
|
62
66
|
this.loadFromVfs();
|
|
@@ -67,7 +71,27 @@ export class VirtualUserManager {
|
|
|
67
71
|
|
|
68
72
|
this.sudoers.add("root");
|
|
69
73
|
|
|
74
|
+
// Auto-create current system user for easier authentication
|
|
75
|
+
const currentUser = process.env.USER || process.env.USERNAME;
|
|
76
|
+
if (currentUser && currentUser !== "root" && !this.users.has(currentUser)) {
|
|
77
|
+
// Use same password as root for convenience, or a generic default
|
|
78
|
+
const userPassword = this.defaultRootPassword;
|
|
79
|
+
this.users.set(currentUser, this.createRecord(currentUser, userPassword));
|
|
80
|
+
this.sudoers.add(currentUser);
|
|
81
|
+
|
|
82
|
+
// Create home directory for the system user
|
|
83
|
+
const homePath = `/home/${currentUser}`;
|
|
84
|
+
if (!this.vfs.exists(homePath)) {
|
|
85
|
+
this.vfs.mkdir(homePath, 0o755);
|
|
86
|
+
this.vfs.writeFile(
|
|
87
|
+
`${homePath}/README.txt`,
|
|
88
|
+
`Welcome to the virtual environment, ${currentUser}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
70
93
|
await this.persist();
|
|
94
|
+
this.emit("initialized");
|
|
71
95
|
}
|
|
72
96
|
|
|
73
97
|
/**
|
|
@@ -220,6 +244,7 @@ export class VirtualUserManager {
|
|
|
220
244
|
);
|
|
221
245
|
}
|
|
222
246
|
await this.persist();
|
|
247
|
+
this.emit("user:add", { username });
|
|
223
248
|
}
|
|
224
249
|
|
|
225
250
|
/**
|
|
@@ -258,6 +283,7 @@ export class VirtualUserManager {
|
|
|
258
283
|
|
|
259
284
|
this.sudoers.delete(username);
|
|
260
285
|
|
|
286
|
+
this.emit("user:delete", { username });
|
|
261
287
|
await this.persist();
|
|
262
288
|
}
|
|
263
289
|
|
|
@@ -319,8 +345,12 @@ export class VirtualUserManager {
|
|
|
319
345
|
remoteAddress,
|
|
320
346
|
startedAt: new Date().toISOString(),
|
|
321
347
|
};
|
|
322
|
-
|
|
323
348
|
this.activeSessions.set(session.id, session);
|
|
349
|
+
this.emit("session:register", {
|
|
350
|
+
sessionId: session.id,
|
|
351
|
+
username,
|
|
352
|
+
remoteAddress,
|
|
353
|
+
});
|
|
324
354
|
return session;
|
|
325
355
|
}
|
|
326
356
|
|
|
@@ -334,6 +364,14 @@ export class VirtualUserManager {
|
|
|
334
364
|
return;
|
|
335
365
|
}
|
|
336
366
|
|
|
367
|
+
const session = this.activeSessions.get(sessionId);
|
|
368
|
+
this.activeSessions.delete(sessionId);
|
|
369
|
+
if (session) {
|
|
370
|
+
this.emit("session:unregister", {
|
|
371
|
+
sessionId,
|
|
372
|
+
username: session.username,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
337
375
|
this.activeSessions.delete(sessionId);
|
|
338
376
|
}
|
|
339
377
|
|
package/src/commands/exit.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { HoneyPot } from "./Honeypot";
|
|
1
2
|
import { SshClient } from "./SSHClient";
|
|
2
|
-
import { SshMimic } from "./SSHMimic/index";
|
|
3
|
+
import { SftpMimic, SshMimic } from "./SSHMimic/index";
|
|
3
4
|
import VirtualFileSystem from "./VirtualFileSystem";
|
|
4
5
|
import { VirtualShell } from "./VirtualShell";
|
|
5
6
|
import { VirtualUserManager } from "./VirtualUserManager";
|
|
6
7
|
|
|
8
|
+
export type {
|
|
9
|
+
AuditLogEntry,
|
|
10
|
+
HoneyPotStats,
|
|
11
|
+
} from "./Honeypot";
|
|
7
12
|
export type {
|
|
8
13
|
CommandContext,
|
|
9
14
|
CommandMode,
|
|
@@ -30,8 +35,10 @@ export type {
|
|
|
30
35
|
} from "./types/vfs";
|
|
31
36
|
|
|
32
37
|
export {
|
|
38
|
+
HoneyPot,
|
|
33
39
|
SshClient,
|
|
34
40
|
VirtualFileSystem,
|
|
41
|
+
SftpMimic as VirtualSftpServer,
|
|
35
42
|
VirtualShell,
|
|
36
43
|
SshMimic as VirtualSshServer,
|
|
37
44
|
VirtualUserManager,
|
package/src/standalone.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { VirtualShell, VirtualSshServer } from ".";
|
|
1
|
+
import { VirtualSftpServer, VirtualShell, VirtualSshServer } from ".";
|
|
2
2
|
|
|
3
3
|
const hostname = process.env.SSH_MIMIC_HOSTNAME ?? "typescript-vm";
|
|
4
4
|
const virtualShell = new VirtualShell(hostname);
|
|
@@ -26,3 +26,13 @@ new VirtualSshServer({
|
|
|
26
26
|
console.error("Failed to start SSH Mimic:", error);
|
|
27
27
|
process.exit(1);
|
|
28
28
|
});
|
|
29
|
+
|
|
30
|
+
new VirtualSftpServer({ port: 2223, hostname, shell: virtualShell })
|
|
31
|
+
.start()
|
|
32
|
+
.then((port: number) => {
|
|
33
|
+
console.log(`SFTP Mimic initialized. Listening on port ${port}.`);
|
|
34
|
+
})
|
|
35
|
+
.catch((error: unknown) => {
|
|
36
|
+
console.error("Failed to start SFTP Mimic:", error);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/// <reference types="bun" />
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
import { rmSync } from "node:fs";
|
|
4
|
+
import type { FileEntryWithStats, SFTPWrapper } from "ssh2";
|
|
5
|
+
import { Client } from "ssh2";
|
|
6
|
+
import { SftpMimic } from "../src/SSHMimic/sftp";
|
|
7
|
+
import VirtualFileSystem from "../src/VirtualFileSystem";
|
|
8
|
+
import { VirtualUserManager } from "../src/VirtualUserManager";
|
|
9
|
+
|
|
10
|
+
function makeTempBasePath(): string {
|
|
11
|
+
return `./temp-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function connectSftp(port: number): Promise<{ client: Client; sftp: SFTPWrapper }> {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const client = new Client();
|
|
17
|
+
client.on("ready", () => {
|
|
18
|
+
client.sftp((err, sftp) => {
|
|
19
|
+
if (err) {
|
|
20
|
+
client.end();
|
|
21
|
+
reject(err);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
resolve({ client, sftp });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
client.on("error", reject);
|
|
29
|
+
client.connect({
|
|
30
|
+
host: "127.0.0.1",
|
|
31
|
+
port,
|
|
32
|
+
username: "root",
|
|
33
|
+
password: "root",
|
|
34
|
+
hostVerifier: () => true,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function connectSftpWithUser(
|
|
40
|
+
port: number,
|
|
41
|
+
username: string,
|
|
42
|
+
password: string,
|
|
43
|
+
): Promise<{ client: Client; sftp: SFTPWrapper }> {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const client = new Client();
|
|
46
|
+
client.on("ready", () => {
|
|
47
|
+
client.sftp((err, sftp) => {
|
|
48
|
+
if (err) {
|
|
49
|
+
client.end();
|
|
50
|
+
reject(err);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
resolve({ client, sftp });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
client.on("error", reject);
|
|
58
|
+
client.connect({
|
|
59
|
+
host: "127.0.0.1",
|
|
60
|
+
port,
|
|
61
|
+
username,
|
|
62
|
+
password,
|
|
63
|
+
hostVerifier: () => true,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("SftpMimic", () => {
|
|
69
|
+
test("authenticates with VirtualUserManager and serves files from the VirtualFileSystem", async () => {
|
|
70
|
+
const tempBasePath = makeTempBasePath();
|
|
71
|
+
const vfs = new VirtualFileSystem(tempBasePath);
|
|
72
|
+
const users = new VirtualUserManager(vfs, "root");
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await users.initialize();
|
|
76
|
+
|
|
77
|
+
const rootPath = "/home/root";
|
|
78
|
+
if (!vfs.exists(rootPath)) {
|
|
79
|
+
vfs.mkdir(rootPath, 0o755);
|
|
80
|
+
}
|
|
81
|
+
vfs.writeFile(`${rootPath}/TEST.txt`, "hello world");
|
|
82
|
+
|
|
83
|
+
const server = new SftpMimic({
|
|
84
|
+
port: 0,
|
|
85
|
+
hostname: "test-sftp",
|
|
86
|
+
vfs,
|
|
87
|
+
users,
|
|
88
|
+
});
|
|
89
|
+
const port = await server.start();
|
|
90
|
+
|
|
91
|
+
const { client, sftp } = await connectSftp(port);
|
|
92
|
+
const list = await new Promise<FileEntryWithStats[]>((resolve, reject) => {
|
|
93
|
+
sftp.readdir(
|
|
94
|
+
"/home/root",
|
|
95
|
+
(err?: Error | null, list?: FileEntryWithStats[]) => {
|
|
96
|
+
if (err) {
|
|
97
|
+
reject(err);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
resolve(list || []);
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(list.map((entry) => entry.filename)).toContain("TEST.txt");
|
|
106
|
+
|
|
107
|
+
const content = await new Promise<string>((resolve, reject) => {
|
|
108
|
+
sftp.readFile(
|
|
109
|
+
"/home/root/TEST.txt",
|
|
110
|
+
"utf8",
|
|
111
|
+
(err?: Error | null, data?: Buffer) => {
|
|
112
|
+
if (err) {
|
|
113
|
+
reject(err);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
resolve((data || Buffer.alloc(0)).toString("utf8"));
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(content).toBe("hello world");
|
|
122
|
+
client.end();
|
|
123
|
+
server.stop();
|
|
124
|
+
} finally {
|
|
125
|
+
rmSync(tempBasePath, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("blocks path traversal attempts outside home directory", async () => {
|
|
130
|
+
const tempBasePath = makeTempBasePath();
|
|
131
|
+
const vfs = new VirtualFileSystem(tempBasePath);
|
|
132
|
+
const users = new VirtualUserManager(vfs, "root");
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
await users.initialize();
|
|
136
|
+
|
|
137
|
+
const rootPath = "/home/root";
|
|
138
|
+
if (!vfs.exists(rootPath)) {
|
|
139
|
+
vfs.mkdir(rootPath, 0o755);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const server = new SftpMimic({
|
|
143
|
+
port: 0,
|
|
144
|
+
hostname: "test-sftp",
|
|
145
|
+
vfs,
|
|
146
|
+
users,
|
|
147
|
+
});
|
|
148
|
+
const port = await server.start();
|
|
149
|
+
|
|
150
|
+
const { client, sftp } = await connectSftp(port);
|
|
151
|
+
|
|
152
|
+
// Try to read /etc/passwd (outside home directory) - should fail with PERMISSION_DENIED
|
|
153
|
+
const traversalAttempt = await new Promise<Error | null>((resolve) => {
|
|
154
|
+
sftp.stat(
|
|
155
|
+
"/etc/passwd",
|
|
156
|
+
(err?: Error | null) => {
|
|
157
|
+
resolve(err ?? null);
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(traversalAttempt).not.toBeNull();
|
|
163
|
+
expect(traversalAttempt?.message).toContain("Permission denied");
|
|
164
|
+
|
|
165
|
+
// Try to access /home/root which should work
|
|
166
|
+
const homeAccess = await new Promise<FileEntryWithStats[]>((
|
|
167
|
+
resolve,
|
|
168
|
+
reject,
|
|
169
|
+
) => {
|
|
170
|
+
sftp.readdir(
|
|
171
|
+
"/home/root",
|
|
172
|
+
(err?: Error | null, list?: FileEntryWithStats[]) => {
|
|
173
|
+
if (err) {
|
|
174
|
+
reject(err);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
resolve(list || []);
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(homeAccess).toBeDefined();
|
|
183
|
+
|
|
184
|
+
// Try to go up with ../ - should fail
|
|
185
|
+
const upTraversalAttempt = await new Promise<Error | null>((resolve) => {
|
|
186
|
+
sftp.readdir(
|
|
187
|
+
"/home/root/../../etc",
|
|
188
|
+
(err?: Error | null) => {
|
|
189
|
+
resolve(err ?? null);
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(upTraversalAttempt).not.toBeNull();
|
|
195
|
+
|
|
196
|
+
client.end();
|
|
197
|
+
server.stop();
|
|
198
|
+
} finally {
|
|
199
|
+
rmSync(tempBasePath, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("auto-creates current system user on initialization", async () => {
|
|
204
|
+
// Use a unique temp directory for this test to avoid VFS sharing
|
|
205
|
+
const tempPath = `./temp-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
206
|
+
const vfs = new VirtualFileSystem(tempPath);
|
|
207
|
+
const users = new VirtualUserManager(vfs, "testpass");
|
|
208
|
+
await users.initialize();
|
|
209
|
+
|
|
210
|
+
// Verify that the current system user was created
|
|
211
|
+
const currentUser = process.env.USER || process.env.USERNAME;
|
|
212
|
+
if (currentUser && currentUser !== "root") {
|
|
213
|
+
// Should be able to verify password with the default password (testpass)
|
|
214
|
+
const passwordValid = users.verifyPassword(currentUser, "testpass");
|
|
215
|
+
expect(passwordValid).toBe(true);
|
|
216
|
+
|
|
217
|
+
// Home directory should exist
|
|
218
|
+
const homePath = `/home/${currentUser}`;
|
|
219
|
+
expect(vfs.exists(homePath)).toBe(true);
|
|
220
|
+
|
|
221
|
+
// README.txt should exist in home
|
|
222
|
+
const readmePath = `${homePath}/README.txt`;
|
|
223
|
+
expect(vfs.exists(readmePath)).toBe(true);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Cleanup
|
|
227
|
+
const fs = await import("node:fs");
|
|
228
|
+
fs.rmSync(tempPath, { recursive: true, force: true });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("allows system user to authenticate and access SFTP", async () => {
|
|
232
|
+
const tempBasePath = makeTempBasePath();
|
|
233
|
+
const vfs = new VirtualFileSystem(tempBasePath);
|
|
234
|
+
const users = new VirtualUserManager(vfs, "root");
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
await users.initialize();
|
|
238
|
+
|
|
239
|
+
// Verify system user was created with the default password
|
|
240
|
+
const currentUser = process.env.USER || process.env.USERNAME;
|
|
241
|
+
if (!currentUser || currentUser === "root") {
|
|
242
|
+
// Skip test if we can't determine current user
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Ensure a deterministic password for environments where user data may persist.
|
|
247
|
+
await users.setPassword(currentUser, "root");
|
|
248
|
+
|
|
249
|
+
const server = new SftpMimic({
|
|
250
|
+
port: 0,
|
|
251
|
+
hostname: "test-sftp",
|
|
252
|
+
vfs,
|
|
253
|
+
users,
|
|
254
|
+
});
|
|
255
|
+
const port = await server.start();
|
|
256
|
+
|
|
257
|
+
// Connect as the system user (which was auto-created during initialize)
|
|
258
|
+
const { client, sftp } = await connectSftpWithUser(
|
|
259
|
+
port,
|
|
260
|
+
currentUser,
|
|
261
|
+
"root",
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// User should be able to list their home directory
|
|
265
|
+
const list = await new Promise<FileEntryWithStats[]>((resolve, reject) => {
|
|
266
|
+
sftp.readdir(
|
|
267
|
+
`/home/${currentUser}`,
|
|
268
|
+
(err?: Error | null, list?: FileEntryWithStats[]) => {
|
|
269
|
+
if (err) {
|
|
270
|
+
reject(err);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
resolve(list || []);
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// README.txt should be in the home directory
|
|
279
|
+
expect(list.map((e) => e.filename)).toContain("README.txt");
|
|
280
|
+
|
|
281
|
+
// Create a file as the system user
|
|
282
|
+
await new Promise<void>((resolve, reject) => {
|
|
283
|
+
sftp.writeFile(
|
|
284
|
+
`/home/${currentUser}/test-file.txt`,
|
|
285
|
+
Buffer.from("WinSCP test"),
|
|
286
|
+
(err?: Error | null) => {
|
|
287
|
+
if (err) {
|
|
288
|
+
reject(err);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
resolve();
|
|
292
|
+
},
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Read the file back
|
|
297
|
+
const content = await new Promise<string>((resolve, reject) => {
|
|
298
|
+
sftp.readFile(
|
|
299
|
+
`/home/${currentUser}/test-file.txt`,
|
|
300
|
+
"utf8",
|
|
301
|
+
(err?: Error | null, data?: Buffer) => {
|
|
302
|
+
if (err) {
|
|
303
|
+
reject(err);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
resolve((data || Buffer.alloc(0)).toString("utf8"));
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(content).toBe("WinSCP test");
|
|
312
|
+
|
|
313
|
+
client.end();
|
|
314
|
+
server.stop();
|
|
315
|
+
} finally {
|
|
316
|
+
rmSync(tempBasePath, { recursive: true, force: true });
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { VirtualShell } from "../src";
|
|
3
|
+
import { runExec } from "../src/SSHMimic/exec";
|
|
4
|
+
|
|
5
|
+
describe("SSH exec inline commands", () => {
|
|
6
|
+
test("runExec sends stdout, stderr, and exit code for inline commands", async () => {
|
|
7
|
+
const shell = new VirtualShell("localhost");
|
|
8
|
+
const stdout: string[] = [];
|
|
9
|
+
const stderr: string[] = [];
|
|
10
|
+
let exitCode: number = -1;
|
|
11
|
+
|
|
12
|
+
const stream = {
|
|
13
|
+
write(data: string) {
|
|
14
|
+
stdout.push(data);
|
|
15
|
+
},
|
|
16
|
+
stderr: {
|
|
17
|
+
write(data: string) {
|
|
18
|
+
stderr.push(data);
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
exit(code: number) {
|
|
22
|
+
exitCode = code;
|
|
23
|
+
},
|
|
24
|
+
end() {
|
|
25
|
+
return undefined;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
await shell.ensureInitialized();
|
|
30
|
+
|
|
31
|
+
const endPromise = new Promise<void>((resolve) => {
|
|
32
|
+
stream.end = () => {
|
|
33
|
+
resolve();
|
|
34
|
+
return;
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
runExec(stream as never, "echo hello", "root", "localhost", shell);
|
|
39
|
+
await endPromise;
|
|
40
|
+
|
|
41
|
+
expect(stdout.join("")).toContain("hello");
|
|
42
|
+
expect(stderr.join("")).toBe("");
|
|
43
|
+
expect(exitCode).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
});
|