typescript-virtual-container 1.2.4 → 1.2.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/README.md +868 -1245
- package/benchmark-results.txt +21 -21
- package/dist/SSHMimic/index.d.ts +19 -2
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +116 -20
- package/dist/VirtualFileSystem/index.d.ts +115 -88
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +406 -258
- package/dist/VirtualShell/index.d.ts +3 -4
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +4 -6
- package/dist/VirtualUserManager/index.d.ts +25 -0
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +33 -0
- package/dist/commands/chmod.d.ts +3 -0
- package/dist/commands/chmod.d.ts.map +1 -0
- package/dist/commands/chmod.js +31 -0
- package/dist/commands/cp.d.ts +3 -0
- package/dist/commands/cp.d.ts.map +1 -0
- package/dist/commands/cp.js +68 -0
- package/dist/commands/find.d.ts +3 -0
- package/dist/commands/find.d.ts.map +1 -0
- package/dist/commands/find.js +48 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +61 -35
- package/dist/commands/head.d.ts +3 -0
- package/dist/commands/head.d.ts.map +1 -0
- package/dist/commands/head.js +30 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +25 -35
- package/dist/commands/ln.d.ts +3 -0
- package/dist/commands/ln.d.ts.map +1 -0
- package/dist/commands/ln.js +42 -0
- package/dist/commands/mv.d.ts +3 -0
- package/dist/commands/mv.d.ts.map +1 -0
- package/dist/commands/mv.js +35 -0
- package/dist/commands/tail.d.ts +3 -0
- package/dist/commands/tail.d.ts.map +1 -0
- package/dist/commands/tail.js +33 -0
- package/dist/commands/wc.d.ts +3 -0
- package/dist/commands/wc.d.ts.map +1 -0
- package/dist/commands/wc.js +48 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/package.json +5 -2
- package/scripts/publish-package.sh +70 -0
- package/src/SSHMimic/index.ts +143 -28
- package/src/VirtualFileSystem/index.ts +500 -280
- package/src/VirtualShell/index.ts +4 -6
- package/src/VirtualUserManager/index.ts +41 -0
- package/src/commands/chmod.ts +33 -0
- package/src/commands/cp.ts +76 -0
- package/src/commands/find.ts +61 -0
- package/src/commands/grep.ts +54 -38
- package/src/commands/head.ts +35 -0
- package/src/commands/index.ts +25 -43
- package/src/commands/ln.ts +47 -0
- package/src/commands/mv.ts +43 -0
- package/src/commands/tail.ts +37 -0
- package/src/commands/wc.ts +48 -0
- package/src/index.ts +1 -0
- package/standalone.js +62 -52
- package/standalone.js.map +4 -4
- package/tests/bun-test-shim.ts +1 -0
- package/tests/sftp.test.ts +115 -191
- package/tests/users.test.ts +66 -83
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `typescript-virtual-container`
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Pure in-memory SSH/SFTP server with a virtual filesystem and typed programmatic API for testing, automation, honeypots, and interactive shell scripting in TypeScript/JavaScript.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/typescript-virtual-container)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
- [Quick Start](#quick-start)
|
|
18
18
|
- [Architecture Overview](#architecture-overview)
|
|
19
19
|
- [API Reference](#api-reference)
|
|
20
|
+
- [VirtualSshServer](#virtualsshserver)
|
|
21
|
+
- [VirtualSftpServer](#virtualsftpserver)
|
|
22
|
+
- [VirtualShell](#virtualshell)
|
|
23
|
+
- [VirtualFileSystem](#virtualfilesystem)
|
|
24
|
+
- [VirtualUserManager](#virtualusermanager)
|
|
25
|
+
- [HoneyPot](#honeypot)
|
|
26
|
+
- [SshClient](#sshclient-programmatic-api)
|
|
27
|
+
- [Key Types](#key-types)
|
|
20
28
|
- [Usage Examples](#usage-examples)
|
|
21
29
|
- [Built-in Commands](#built-in-commands)
|
|
22
30
|
- [Configuration](#configuration)
|
|
@@ -24,7 +32,6 @@
|
|
|
24
32
|
- [Types & TypeScript](#types--typescript)
|
|
25
33
|
- [FAQ](#faq)
|
|
26
34
|
- [Troubleshooting](#troubleshooting)
|
|
27
|
-
- [Migration Guide](#migration-guide)
|
|
28
35
|
- [Contributing](#contributing)
|
|
29
36
|
- [Security](#security)
|
|
30
37
|
- [Support](#support)
|
|
@@ -32,43 +39,55 @@
|
|
|
32
39
|
- [Roadmap](#roadmap)
|
|
33
40
|
- [Changelog](#changelog)
|
|
34
41
|
|
|
42
|
+
---
|
|
43
|
+
|
|
35
44
|
## Overview
|
|
36
45
|
|
|
37
46
|
`typescript-virtual-container` is a lightweight, fully-typed SSH/SFTP runtime written in TypeScript that provides:
|
|
38
47
|
|
|
48
|
+
- **Pure in-memory filesystem**: No disk I/O at runtime. All state lives in a fast recursive in-memory tree. Use JSON snapshots for optional persistence.
|
|
39
49
|
- **SSH + SFTP Protocol Support**: Serve SSH shell/exec sessions and SFTP file operations on configurable ports.
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
50
|
+
- **Password & public-key authentication**: Register SSH public keys per user alongside (or instead of) password auth.
|
|
51
|
+
- **Rate limiting / brute-force protection**: Configurable per-IP lockout after N failed auth attempts.
|
|
52
|
+
- **User Management**: Create, authenticate, and manage virtual users with scrypt password hashing, sudo-like privilege elevation, and optional per-user disk quotas.
|
|
42
53
|
- **Programmatic Shell API**: Execute shell commands and query filesystem state directly from TypeScript without SSH overhead.
|
|
43
54
|
- **Event-Driven Architecture**: All core classes extend `EventEmitter` for lifecycle and operation tracking. Listen to auth events, filesystem operations, session lifecycle, and command execution for auditing and integration.
|
|
44
55
|
- **Security Auditing**: Built-in `HoneyPot` utility for comprehensive activity logging, event tracking, statistics collection, and anomaly detection across all components.
|
|
45
|
-
- **Built-in Commands**: `ls`, `cd`, `
|
|
56
|
+
- **40+ Built-in Commands**: `ls`, `cd`, `cat`, `cp`, `mv`, `ln`, `find`, `grep`, `wc`, `head`, `tail`, `chmod`, `mkdir`, `touch`, `rm`, `tree`, `nano`, `curl`, `wget`, `sudo`, `su`, `adduser`, `deluser`, and more. Shell compatibility is still being expanded.
|
|
46
57
|
- **Full TypeScript Support**: Complete JSDoc coverage, exported types, and first-class async/await for all operations.
|
|
47
58
|
|
|
59
|
+
---
|
|
60
|
+
|
|
48
61
|
## What This Is / What This Is Not
|
|
49
62
|
|
|
50
63
|
### What This Is
|
|
51
64
|
|
|
52
|
-
- A virtual shell runtime written in TypeScript
|
|
53
|
-
- A virtual environment with its own
|
|
65
|
+
- A virtual shell runtime written in TypeScript with a **pure in-memory filesystem**.
|
|
66
|
+
- A virtual environment with its own filesystem, user management, and command runtime.
|
|
54
67
|
- A practical tool for deterministic testing, automation pipelines, and SSH-like workflows without running real containers.
|
|
68
|
+
- A honeypot framework for capturing and auditing attacker behavior.
|
|
55
69
|
|
|
56
70
|
### What This Is Not
|
|
57
71
|
|
|
58
72
|
- Not a fully isolated container runtime.
|
|
59
|
-
- Not a security sandbox.
|
|
73
|
+
- Not a security sandbox — the VFS does not sandbox host filesystem access by spawned child processes (e.g. `wget`, `curl` delegate to the host binary).
|
|
60
74
|
- Not a kernel-level isolation boundary (unlike Docker/VM-based isolation).
|
|
61
75
|
|
|
62
76
|
This project emulates shell behavior for developer workflows. It is designed for realism and productivity, not hard security isolation.
|
|
63
77
|
|
|
78
|
+
---
|
|
79
|
+
|
|
64
80
|
## Why This Package
|
|
65
81
|
|
|
66
82
|
This package is designed for teams that need a realistic SSH-like runtime without spinning up real containers or VMs.
|
|
67
83
|
|
|
68
|
-
- **
|
|
84
|
+
- **Zero disk footprint by default**: The VFS operates entirely in memory. Opt into JSON snapshot persistence when you need it.
|
|
85
|
+
- **Deterministic test environments**: Repeatable state for CI pipelines and integration tests. Build a fixture snapshot once, hydrate for each test.
|
|
69
86
|
- **Low operational overhead**: No Docker daemon, no kernel namespaces, no privileged setup.
|
|
70
87
|
- **Fast feedback loops**: Programmatic API for command execution and filesystem assertions.
|
|
71
|
-
- **Developer-friendly internals**: Typed APIs, clear boundaries,
|
|
88
|
+
- **Developer-friendly internals**: Typed APIs, clear boundaries, composable building blocks, and full JSDoc.
|
|
89
|
+
|
|
90
|
+
---
|
|
72
91
|
|
|
73
92
|
## Installation
|
|
74
93
|
|
|
@@ -86,12 +105,23 @@ bun add typescript-virtual-container
|
|
|
86
105
|
|
|
87
106
|
```bash
|
|
88
107
|
git clone https://github.com/itsrealfortune/typescript-virtual-container/
|
|
89
|
-
cd virtual-
|
|
108
|
+
cd typescript-virtual-container
|
|
90
109
|
bun install
|
|
91
|
-
bun format
|
|
92
|
-
bun check
|
|
110
|
+
bun format # Format code per Biome
|
|
111
|
+
bun check # Lint and typecheck
|
|
112
|
+
bun run build
|
|
93
113
|
```
|
|
94
114
|
|
|
115
|
+
### Standalone (zero install)
|
|
116
|
+
|
|
117
|
+
To quickly try a standalone demo:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
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
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
95
125
|
## Compatibility
|
|
96
126
|
|
|
97
127
|
- **Node.js**: Recommended `>=18`
|
|
@@ -101,6 +131,8 @@ bun check # Lint and typecheck
|
|
|
101
131
|
|
|
102
132
|
The virtual filesystem and shell behavior are intentionally portable and do not depend on host-specific POSIX syscalls.
|
|
103
133
|
|
|
134
|
+
---
|
|
135
|
+
|
|
104
136
|
## Quick Start
|
|
105
137
|
|
|
106
138
|
### Running an SSH Server
|
|
@@ -108,23 +140,21 @@ The virtual filesystem and shell behavior are intentionally portable and do not
|
|
|
108
140
|
```typescript
|
|
109
141
|
import { VirtualSshServer } from "typescript-virtual-container";
|
|
110
142
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
hostname: "my-container"
|
|
143
|
+
const ssh = new VirtualSshServer({
|
|
144
|
+
port: 2222,
|
|
145
|
+
hostname: "my-container",
|
|
115
146
|
});
|
|
116
147
|
|
|
117
|
-
// Start server
|
|
118
148
|
await ssh.start();
|
|
119
149
|
console.log("SSH server listening on :2222");
|
|
120
150
|
|
|
121
|
-
// Connect externally
|
|
122
|
-
// ssh root@localhost -p 2222
|
|
151
|
+
// Connect externally:
|
|
152
|
+
// ssh root@localhost -p 2222
|
|
153
|
+
// root has no password by default — login is allowed without verification.
|
|
123
154
|
|
|
124
|
-
// Graceful shutdown
|
|
125
155
|
process.on("SIGTERM", () => {
|
|
126
|
-
|
|
127
|
-
|
|
156
|
+
ssh.stop();
|
|
157
|
+
process.exit(0);
|
|
128
158
|
});
|
|
129
159
|
```
|
|
130
160
|
|
|
@@ -135,17 +165,8 @@ import { VirtualSftpServer, VirtualShell, VirtualSshServer } from "typescript-vi
|
|
|
135
165
|
|
|
136
166
|
const shell = new VirtualShell("my-container");
|
|
137
167
|
|
|
138
|
-
const ssh
|
|
139
|
-
|
|
140
|
-
hostname: "my-container",
|
|
141
|
-
shell,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const sftp = new VirtualSftpServer({
|
|
145
|
-
port: 2223,
|
|
146
|
-
hostname: "my-container",
|
|
147
|
-
shell,
|
|
148
|
-
});
|
|
168
|
+
const ssh = new VirtualSshServer({ port: 2222, hostname: "my-container", shell });
|
|
169
|
+
const sftp = new VirtualSftpServer({ port: 2223, hostname: "my-container", shell });
|
|
149
170
|
|
|
150
171
|
await ssh.start();
|
|
151
172
|
await sftp.start();
|
|
@@ -156,18 +177,16 @@ console.log("SSH on :2222, SFTP on :2223");
|
|
|
156
177
|
### Using the Programmatic Client API
|
|
157
178
|
|
|
158
179
|
```typescript
|
|
159
|
-
import {
|
|
180
|
+
import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
160
181
|
|
|
161
182
|
const shell = new VirtualShell("typescript-vm");
|
|
162
|
-
const ssh
|
|
183
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
163
184
|
await ssh.start();
|
|
164
185
|
|
|
165
|
-
// Create authenticated client for specific user
|
|
166
186
|
const client = new SshClient(shell, "root");
|
|
167
187
|
|
|
168
|
-
// Execute commands programmatically
|
|
169
188
|
const list = await client.ls("/home");
|
|
170
|
-
console.log("stdout:", list.stdout);
|
|
189
|
+
console.log("stdout:", list.stdout);
|
|
171
190
|
|
|
172
191
|
const result = await client.pwd();
|
|
173
192
|
console.log("Current dir:", result.stdout);
|
|
@@ -183,109 +202,100 @@ await client.writeFile("output.txt", "Hello, World!");
|
|
|
183
202
|
ssh.stop();
|
|
184
203
|
```
|
|
185
204
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
<!-- ### Core Components
|
|
205
|
+
---
|
|
189
206
|
|
|
190
|
-
|
|
191
|
-
┌─────────────────────────────────────────────┐
|
|
192
|
-
│ SSH Server (SshMimic) │
|
|
193
|
-
│ Listens on :port, handles auth & sessions │
|
|
194
|
-
└──────────────┬──────────────────────────────┘
|
|
195
|
-
│
|
|
196
|
-
┌───────┴────────┬──────────────┐
|
|
197
|
-
│ │ │
|
|
198
|
-
┌──────┴──────┐ ┌─────┴─────┐ ┌────┴─────┐
|
|
199
|
-
│ VirtualFileSystem │ VirtualUserManager │ Command Runtime
|
|
200
|
-
│ In-mem FS w/ persist │ Auth & Sudoers │ Shell/Exec Mode
|
|
201
|
-
└──────────────┘ └──────────┘ └──────────┘
|
|
202
|
-
▲
|
|
203
|
-
│ Backed by disk
|
|
204
|
-
│ .vfs/mirror
|
|
205
|
-
└──────────────────────────────────┘
|
|
206
|
-
``` -->
|
|
207
|
+
## Architecture Overview
|
|
207
208
|
|
|
208
209
|
### Execution Modes
|
|
209
210
|
|
|
210
|
-
1. **SSH Shell Mode**: Interactive terminal session over SSH with readline, prompt, TTY resizing.
|
|
211
|
-
2. **SSH Exec Mode**: Non-interactive command execution (e.g
|
|
211
|
+
1. **SSH Shell Mode**: Interactive terminal session over SSH with readline, prompt, history, TTY resizing.
|
|
212
|
+
2. **SSH Exec Mode**: Non-interactive command execution (e.g. `ssh user@host "ls -la"`).
|
|
212
213
|
3. **SFTP Mode**: Remote file operations (`readdir`, `stat`, `readFile`, `writeFile`, `mkdir`, `rename`, etc.) with home-directory confinement.
|
|
213
|
-
4. **Programmatic Mode**: Direct TypeScript API via `SshClient
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
-
|
|
214
|
+
4. **Programmatic Mode**: Direct TypeScript API via `SshClient` — no SSH protocol overhead.
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
┌──────────────────────────────────────────────────────────────────────────┐
|
|
218
|
+
│ SshMimic (VirtualSshServer) SftpMimic (VirtualSftpServer) │
|
|
219
|
+
│ password auth · publickey auth SFTP protocol handlers │
|
|
220
|
+
│ per-IP rate limiting / lockout home-dir confinement │
|
|
221
|
+
└─────────────────────────┬────────────────────────────────────────────────┘
|
|
222
|
+
│
|
|
223
|
+
┌──────────▼──────────┐
|
|
224
|
+
│ VirtualShell │
|
|
225
|
+
│ pipeline parser │
|
|
226
|
+
│ command executor │
|
|
227
|
+
│ session manager │
|
|
228
|
+
└──┬──────────────┬───┘
|
|
229
|
+
│ │
|
|
230
|
+
┌────────────▼───┐ ┌─────▼───────────────┐
|
|
231
|
+
│VirtualFileSystem│ │ VirtualUserManager │
|
|
232
|
+
│ in-memory tree │ │ scrypt · sudoers │
|
|
233
|
+
│ gzip · symlinks │ │ publickey auth │
|
|
234
|
+
│ snapshot I/O │ │ quotas · sessions │
|
|
235
|
+
│ mode:memory|fs │ └─────────────────────-┘
|
|
236
|
+
└─────────────────┘
|
|
237
|
+
│
|
|
238
|
+
┌────────────▼────────────┐
|
|
239
|
+
│ HoneyPot │
|
|
240
|
+
│ audit log · stats │
|
|
241
|
+
│ anomaly detection │
|
|
242
|
+
└─────────────────────────┘
|
|
243
|
+
```
|
|
220
244
|
|
|
221
245
|
---
|
|
222
246
|
|
|
223
247
|
## API Reference
|
|
224
248
|
|
|
225
|
-
###
|
|
249
|
+
### `VirtualSshServer`
|
|
226
250
|
|
|
227
|
-
Main SSH server class
|
|
251
|
+
Main SSH server class. Wires the virtual shell runtime into `ssh2` sessions and manages authentication and session handlers.
|
|
228
252
|
|
|
229
253
|
#### Constructor
|
|
230
254
|
|
|
231
255
|
```typescript
|
|
232
|
-
new
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
256
|
+
new VirtualSshServer({
|
|
257
|
+
port: number; // TCP port to bind
|
|
258
|
+
hostname?: string; // Virtual hostname (default: "typescript-vm")
|
|
259
|
+
shell?: VirtualShell; // Optional shared shell instance (share state with SFTP)
|
|
260
|
+
maxAuthAttempts?: number; // Max failed auth per IP before lockout (default: 5)
|
|
261
|
+
lockoutDurationMs?: number; // Lockout duration in ms (default: 60_000)
|
|
236
262
|
})
|
|
237
263
|
```
|
|
238
264
|
|
|
239
|
-
|
|
240
|
-
- If `shell` is omitted, the server creates `new VirtualShell(hostname)` for you.
|
|
265
|
+
If `shell` is omitted, the server creates `new VirtualShell(hostname)` internally.
|
|
241
266
|
|
|
242
267
|
**Example:**
|
|
243
268
|
|
|
244
269
|
```typescript
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}, "./data");
|
|
250
|
-
const ssh = new SshMimic({
|
|
251
|
-
port: 2222,
|
|
252
|
-
hostname: "my-lab",
|
|
253
|
-
shell: virtualShell
|
|
270
|
+
const shell = new VirtualShell("my-lab", {
|
|
271
|
+
kernel: "1.0.0+itsrealfortune+1-amd64",
|
|
272
|
+
os: "Fortune GNU/Linux x64",
|
|
273
|
+
arch: "x86_64",
|
|
254
274
|
});
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
#### Methods
|
|
258
|
-
|
|
259
|
-
##### `async start(): Promise<number>`
|
|
260
|
-
|
|
261
|
-
Initializes virtual filesystem, user manager, and starts listening for SSH connections.
|
|
262
275
|
|
|
263
|
-
|
|
264
|
-
- **Throws**: Error if port not available or initialization fails
|
|
265
|
-
|
|
266
|
-
```typescript
|
|
267
|
-
const port = await ssh.start();
|
|
268
|
-
console.log(`Listening on ${port}`);
|
|
276
|
+
const ssh = new VirtualSshServer({ port: 2222, hostname: "my-lab", shell });
|
|
269
277
|
```
|
|
270
278
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
Cleanly closes server and all active connections.
|
|
279
|
+
#### Methods
|
|
274
280
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
281
|
+
| Method | Description |
|
|
282
|
+
|--------|-------------|
|
|
283
|
+
| `start(): Promise<number>` | Initialize VFS, users, start listening. Returns bound port. |
|
|
284
|
+
| `stop(): void` | Gracefully close server and all active connections. |
|
|
285
|
+
| `clearLockout(ip: string): void` | Manually lift a rate-limit lockout for an IP. |
|
|
286
|
+
| `getVfs(): VirtualFileSystem \| null` | Access VFS instance (null before start). |
|
|
287
|
+
| `getUsers(): VirtualUserManager \| null` | Access user manager (null before start). |
|
|
288
|
+
| `getHostname(): string` | Returns configured hostname. |
|
|
278
289
|
|
|
279
290
|
#### Events
|
|
280
291
|
|
|
281
|
-
`SshMimic` extends `EventEmitter` and emits the following events:
|
|
282
|
-
|
|
283
292
|
| Event | Data | Description |
|
|
284
293
|
|-------|------|-------------|
|
|
285
294
|
| `start` | `{ port: number }` | Server started and listening |
|
|
286
295
|
| `stop` | — | Server stopped |
|
|
287
|
-
| `auth:success` | `{ username
|
|
288
|
-
| `auth:failure` | `{ username
|
|
296
|
+
| `auth:success` | `{ username, remoteAddress, method? }` | User authenticated |
|
|
297
|
+
| `auth:failure` | `{ username, remoteAddress, reason?, method? }` | Auth failed |
|
|
298
|
+
| `auth:lockout` | `{ ip, until: Date }` | IP locked out after too many failures |
|
|
289
299
|
| `client:connect` | — | New SSH client connected |
|
|
290
300
|
| `client:disconnect` | `{ user: string }` | SSH client disconnected |
|
|
291
301
|
|
|
@@ -293,942 +303,557 @@ ssh.stop();
|
|
|
293
303
|
|
|
294
304
|
```typescript
|
|
295
305
|
ssh.on("auth:success", ({ username, remoteAddress }) => {
|
|
296
|
-
|
|
306
|
+
console.log(`[SSH] ${username} authenticated from ${remoteAddress}`);
|
|
297
307
|
});
|
|
298
308
|
|
|
299
|
-
ssh.on("auth:
|
|
300
|
-
|
|
309
|
+
ssh.on("auth:lockout", ({ ip, until }) => {
|
|
310
|
+
console.warn(`[SSH] ${ip} locked until ${until}`);
|
|
301
311
|
});
|
|
302
312
|
```
|
|
303
313
|
|
|
304
|
-
##### `getVfs(): VirtualFileSystem | null`
|
|
305
|
-
|
|
306
|
-
Returns the virtual filesystem instance. Null if server not started.
|
|
307
|
-
|
|
308
|
-
```typescript
|
|
309
|
-
const vfs = ssh.getVfs();
|
|
310
|
-
if (vfs) {
|
|
311
|
-
const content = vfs.readFile("/etc/hosts");
|
|
312
|
-
}
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
##### `getUsers(): VirtualUserManager | null`
|
|
316
|
-
|
|
317
|
-
Returns the user manager instance. Null if server not started.
|
|
318
|
-
|
|
319
|
-
```typescript
|
|
320
|
-
const users = ssh.getUsers();
|
|
321
|
-
const sessions = users.listActiveSessions();
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
##### `getHostname(): string`
|
|
325
|
-
|
|
326
|
-
Returns configured server hostname.
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
console.log(`Server name: ${ssh.getHostname()}`);
|
|
330
|
-
```
|
|
331
|
-
|
|
332
314
|
---
|
|
333
315
|
|
|
334
|
-
###
|
|
316
|
+
### `VirtualSftpServer`
|
|
335
317
|
|
|
336
|
-
SFTP server class
|
|
318
|
+
SFTP server class. Can share a `VirtualShell` with `VirtualSshServer` (recommended) or accept explicit `vfs` + `users` dependencies.
|
|
337
319
|
|
|
338
320
|
#### Constructor
|
|
339
321
|
|
|
340
322
|
```typescript
|
|
341
|
-
new
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
323
|
+
new VirtualSftpServer({
|
|
324
|
+
port: number;
|
|
325
|
+
hostname?: string;
|
|
326
|
+
shell?: VirtualShell; // share state with SSH server
|
|
327
|
+
vfs?: VirtualFileSystem; // explicit if no shell
|
|
328
|
+
users?: VirtualUserManager; // explicit if no shell
|
|
347
329
|
})
|
|
348
330
|
```
|
|
349
331
|
|
|
350
|
-
- If `shell` is provided, SFTP reuses the same users/filesystem state as SSH.
|
|
351
|
-
- If `shell` is omitted, pass `vfs` and `users` explicitly.
|
|
352
|
-
|
|
353
332
|
#### Methods
|
|
354
333
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
```typescript
|
|
360
|
-
const sftp = new SftpMimic({ port: 0, shell });
|
|
361
|
-
const boundPort = await sftp.start();
|
|
362
|
-
console.log(`SFTP listening on ${boundPort}`);
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
##### `stop(): void`
|
|
366
|
-
|
|
367
|
-
Stops the SFTP server.
|
|
368
|
-
|
|
369
|
-
```typescript
|
|
370
|
-
sftp.stop();
|
|
371
|
-
```
|
|
334
|
+
| Method | Description |
|
|
335
|
+
|--------|-------------|
|
|
336
|
+
| `start(): Promise<number>` | Start SFTP server, returns bound port. |
|
|
337
|
+
| `stop(): void` | Stop SFTP server. |
|
|
372
338
|
|
|
373
339
|
#### Behavior Notes
|
|
374
340
|
|
|
375
341
|
- Supports `password` and `keyboard-interactive` authentication.
|
|
376
342
|
- Resolves relative SFTP paths from `/home/<user>`.
|
|
377
|
-
- Confines all SFTP operations to `/home/<user>`
|
|
343
|
+
- Confines all SFTP operations to `/home/<user>` — blocks traversal attempts.
|
|
378
344
|
- Unsupported operations (`READLINK`, `SYMLINK`) return `OP_UNSUPPORTED`.
|
|
379
345
|
|
|
380
|
-
#### Events
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
|
385
|
-
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `
|
|
390
|
-
| `client:connect` | — | New SFTP client connected |
|
|
391
|
-
| `client:disconnect` | `{ user: string }` | SFTP client disconnected |
|
|
392
|
-
|
|
393
|
-
**Example:**
|
|
394
|
-
|
|
395
|
-
```typescript
|
|
396
|
-
sftp.on("auth:success", ({ username }) => {
|
|
397
|
-
console.log(`[SFTP] User ${username} authenticated`);
|
|
398
|
-
});
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
---
|
|
402
|
-
|
|
403
|
-
### SshClient (Programmatic Shell API)
|
|
404
|
-
|
|
405
|
-
Execute shell commands against a `VirtualShell` instance without SSH overhead. Maintains connection state (current working directory) across calls.
|
|
406
|
-
|
|
407
|
-
#### Constructor
|
|
408
|
-
|
|
409
|
-
```typescript
|
|
410
|
-
new SshClient(shell: VirtualShell, username: string)
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
- **shell**: Parent virtual shell instance
|
|
414
|
-
- **username**: User to authenticate as (no password required)
|
|
415
|
-
|
|
416
|
-
**Example:**
|
|
417
|
-
|
|
418
|
-
```typescript
|
|
419
|
-
const shell = new VirtualShell("typescript-vm");
|
|
420
|
-
const client = new SshClient(shell, "alice");
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
#### Methods
|
|
424
|
-
|
|
425
|
-
##### `async exec(command: string): Promise<CommandResult>`
|
|
426
|
-
|
|
427
|
-
Raw command execution. Returns structured output.
|
|
428
|
-
|
|
429
|
-
```typescript
|
|
430
|
-
const result = await client.exec("echo hello && exit 42");
|
|
431
|
-
console.log(result.stdout); // "hello"
|
|
432
|
-
console.log(result.exitCode); // 42
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
##### `async ls(path?: string): Promise<CommandResult>`
|
|
436
|
-
|
|
437
|
-
Lists directory contents. Defaults to current directory.
|
|
438
|
-
|
|
439
|
-
```typescript
|
|
440
|
-
const result = await client.ls("/tmp");
|
|
441
|
-
// result.stdout contains formatted listing
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
##### `async pwd(): Promise<CommandResult>`
|
|
445
|
-
|
|
446
|
-
Prints current working directory.
|
|
447
|
-
|
|
448
|
-
```typescript
|
|
449
|
-
const result = await client.pwd();
|
|
450
|
-
console.log("cwd:", result.stdout); // "/home/alice"
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
##### `async cd(path: string): Promise<CommandResult>`
|
|
454
|
-
|
|
455
|
-
Changes working directory. Updates internal state on success.
|
|
456
|
-
|
|
457
|
-
```typescript
|
|
458
|
-
const result = await client.cd("/var/log");
|
|
459
|
-
// Internal cwd now "/var/log"
|
|
460
|
-
|
|
461
|
-
const result2 = await client.ls(); // Listed from /var/log
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
##### `async cat(path: string): Promise<CommandResult>`
|
|
465
|
-
|
|
466
|
-
Reads file content via command.
|
|
467
|
-
|
|
468
|
-
```typescript
|
|
469
|
-
const result = await client.cat("/etc/hostname");
|
|
470
|
-
console.log(result.stdout);
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
##### `async mkdir(path: string, recursive?: boolean): Promise<CommandResult>`
|
|
474
|
-
|
|
475
|
-
Creates directory. Set `recursive=true` for `-p` flag.
|
|
476
|
-
|
|
477
|
-
```typescript
|
|
478
|
-
await client.mkdir("/tmp/nested/dirs", true);
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
##### `async touch(path: string): Promise<CommandResult>`
|
|
482
|
-
|
|
483
|
-
Creates empty file.
|
|
484
|
-
|
|
485
|
-
```typescript
|
|
486
|
-
await client.touch("/tmp/marker.txt");
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
##### `async rm(path: string, recursive?: boolean): Promise<CommandResult>`
|
|
490
|
-
|
|
491
|
-
Removes file or directory. Set `recursive=true` for `-r` flag.
|
|
492
|
-
|
|
493
|
-
```typescript
|
|
494
|
-
await client.rm("/tmp/old", true); // rm -r /tmp/old
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
##### `async readFile(path: string): Promise<CommandResult>`
|
|
498
|
-
|
|
499
|
-
Reads file content directly from VFS (programmatic, no shell).
|
|
500
|
-
|
|
501
|
-
```typescript
|
|
502
|
-
const result = await client.readFile("/etc/hostname");
|
|
503
|
-
console.log(result.stdout); // File content
|
|
504
|
-
if (result.exitCode !== 0) console.error(result.stderr);
|
|
505
|
-
```
|
|
506
|
-
|
|
507
|
-
##### `async writeFile(path: string, content: string): Promise<CommandResult>`
|
|
508
|
-
|
|
509
|
-
Writes file content directly to VFS (programmatic, no shell).
|
|
510
|
-
|
|
511
|
-
```typescript
|
|
512
|
-
await client.writeFile("/tmp/config.txt", "port=8080\nhost=localhost");
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
##### `async tree(path?: string): Promise<CommandResult>`
|
|
516
|
-
|
|
517
|
-
Renders ASCII directory tree.
|
|
518
|
-
|
|
519
|
-
```typescript
|
|
520
|
-
const result = await client.tree("/home");
|
|
521
|
-
console.log(result.stdout);
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
##### `async whoami(): Promise<CommandResult>`
|
|
525
|
-
|
|
526
|
-
Shows authenticated user.
|
|
527
|
-
|
|
528
|
-
```typescript
|
|
529
|
-
const result = await client.whoami();
|
|
530
|
-
console.log(result.stdout); // "alice" (or user passed to constructor)
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
##### `async hostname(): Promise<CommandResult>`
|
|
534
|
-
|
|
535
|
-
Shows server hostname.
|
|
536
|
-
|
|
537
|
-
```typescript
|
|
538
|
-
const result = await client.hostname();
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
##### `async who(): Promise<CommandResult>`
|
|
542
|
-
|
|
543
|
-
Lists active user sessions.
|
|
544
|
-
|
|
545
|
-
```typescript
|
|
546
|
-
const result = await client.who();
|
|
547
|
-
console.log(result.stdout); // Active sessions
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
##### `getCwd(): string`
|
|
551
|
-
|
|
552
|
-
Returns current working directory (local state, no I/O).
|
|
553
|
-
|
|
554
|
-
```typescript
|
|
555
|
-
await client.cd("/tmp");
|
|
556
|
-
console.log(client.getCwd()); // "/tmp"
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
##### `getUsername(): string`
|
|
560
|
-
|
|
561
|
-
Returns authenticated username (local state, no I/O).
|
|
562
|
-
|
|
563
|
-
```typescript
|
|
564
|
-
console.log(client.getUsername()); // Username from constructor
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
---
|
|
568
|
-
|
|
569
|
-
### VirtualShell
|
|
570
|
-
|
|
571
|
-
Encapsulates shell execution primitives used by the SSH runtime for command dispatch, interactive sessions, and the programmatic client.
|
|
572
|
-
|
|
573
|
-
#### ShellProperties
|
|
574
|
-
|
|
575
|
-
```typescript
|
|
576
|
-
interface ShellProperties {
|
|
577
|
-
kernel: string;
|
|
578
|
-
os: "Fortune GNU/Linux x64";
|
|
579
|
-
arch: "x86_64";
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
const defaultShellProperties: ShellProperties;
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
- `kernel` is displayed in shell/system information output.
|
|
586
|
-
- `os` and `arch` are fixed labels used by the shell runtime.
|
|
587
|
-
|
|
588
|
-
#### Constructor
|
|
589
|
-
|
|
590
|
-
```typescript
|
|
591
|
-
new VirtualShell(
|
|
592
|
-
hostname: string,
|
|
593
|
-
properties?: ShellProperties,
|
|
594
|
-
basePath?: string,
|
|
595
|
-
)
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
- **hostname**: Hostname injected into command context and prompt behavior.
|
|
599
|
-
- **properties**: Optional shell metadata. Defaults to `defaultShellProperties`.
|
|
600
|
-
- **basePath**: Optional directory used to resolve `.vfs/mirror` and auth storage (defaults to `.`).
|
|
601
|
-
|
|
602
|
-
**Example:**
|
|
603
|
-
|
|
604
|
-
```typescript
|
|
605
|
-
const shell = new VirtualShell("typescript-vm", {
|
|
606
|
-
kernel: "1.0.0+itsrealfortune+1-amd64",
|
|
607
|
-
os: "Fortune GNU/Linux x64",
|
|
608
|
-
arch: "x86_64",
|
|
609
|
-
}, "./data");
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
#### Methods
|
|
613
|
-
|
|
614
|
-
##### `addCommand(name: string, params: string[], callback: (ctx: CommandContext) => CommandResult | Promise<CommandResult>): void`
|
|
615
|
-
|
|
616
|
-
Registers a custom command at runtime.
|
|
617
|
-
|
|
618
|
-
```typescript
|
|
619
|
-
shell.addCommand("hello", [], () => ({ stdout: "hello", exitCode: 0 }));
|
|
620
|
-
```
|
|
621
|
-
|
|
622
|
-
##### `executeCommand(rawInput: string, authUser: string, cwd: string): void`
|
|
623
|
-
|
|
624
|
-
Runs one command input in shell mode for a given user and working directory.
|
|
625
|
-
|
|
626
|
-
```typescript
|
|
627
|
-
shell.executeCommand("ls -la", "root", "/home/root");
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
##### `startInteractiveSession(stream: ShellStream, authUser: string, sessionId: string | null, remoteAddress: string, terminalSize: { cols: number; rows: number }): void`
|
|
631
|
-
|
|
632
|
-
Starts an interactive shell session over a shell stream.
|
|
633
|
-
|
|
634
|
-
```typescript
|
|
635
|
-
shell.startInteractiveSession(
|
|
636
|
-
stream,
|
|
637
|
-
"root",
|
|
638
|
-
sessionId,
|
|
639
|
-
"127.0.0.1",
|
|
640
|
-
{ cols: 120, rows: 30 },
|
|
641
|
-
);
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
#### Events
|
|
645
|
-
|
|
646
|
-
`VirtualShell` extends `EventEmitter` and emits the following events:
|
|
647
|
-
|
|
648
|
-
| Event | Data | Description |
|
|
649
|
-
|-------|------|-------------|
|
|
650
|
-
| `initialized` | — | Shell initialization complete |
|
|
651
|
-
| `command` | `{ command: string; user: string; cwd: string }` | Command executed |
|
|
652
|
-
| `session:start` | `{ user: string; sessionId: string \| null; remoteAddress: string }` | Interactive session started |
|
|
653
|
-
|
|
654
|
-
**Example:**
|
|
655
|
-
|
|
656
|
-
```typescript
|
|
657
|
-
shell.on("command", ({ command, user, cwd }) => {
|
|
658
|
-
console.log(`[SHELL] User ${user} executed: ${command} (cwd: ${cwd})`);
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
shell.on("session:start", ({ user, remoteAddress }) => {
|
|
662
|
-
console.log(`[SHELL] Session started for ${user} from ${remoteAddress}`);
|
|
663
|
-
});
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
---
|
|
667
|
-
|
|
668
|
-
### VirtualFileSystem
|
|
669
|
-
|
|
670
|
-
Virtual filesystem abstraction backed by a mirror directory on disk, with optional gzip compression per file.
|
|
671
|
-
|
|
672
|
-
#### Constructor
|
|
673
|
-
|
|
674
|
-
```typescript
|
|
675
|
-
new VirtualFileSystem(baseDir?: string)
|
|
676
|
-
```
|
|
677
|
-
|
|
678
|
-
- **baseDir**: Directory used for the `.vfs/mirror` root (default: current working directory)
|
|
679
|
-
|
|
680
|
-
```typescript
|
|
681
|
-
const vfs = new VirtualFileSystem("./container-data");
|
|
682
|
-
// Mirror root at ./container-data/.vfs/mirror
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
#### Methods
|
|
686
|
-
|
|
687
|
-
#### Events
|
|
688
|
-
|
|
689
|
-
`VirtualFileSystem` extends `EventEmitter` and emits the following events:
|
|
690
|
-
|
|
691
|
-
| Event | Data | Description |
|
|
692
|
-
|-------|------|-------------|
|
|
693
|
-
| `file:read` | `{ path: string; size: number }` | File read |
|
|
694
|
-
| `file:write` | `{ path: string; size: number }` | File written |
|
|
695
|
-
| `dir:create` | `{ path: string; mode: number }` | Directory created |
|
|
696
|
-
| `mirror:flush` | — | Mirror persisted to disk |
|
|
697
|
-
|
|
698
|
-
**Example:**
|
|
699
|
-
|
|
700
|
-
```typescript
|
|
701
|
-
vfs.on("file:write", ({ path, size }) => {
|
|
702
|
-
console.log(`[VFS] File written: ${path} (${size} bytes)`);
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
vfs.on("dir:create", ({ path, mode }) => {
|
|
706
|
-
console.log(`[VFS] Directory created: ${path} (mode: ${mode.toString(8)})`);
|
|
707
|
-
});
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
##### `async restoreMirror(): Promise<void>`
|
|
711
|
-
|
|
712
|
-
Ensures mirror directory structure exists and is ready for operations.
|
|
713
|
-
|
|
714
|
-
```typescript
|
|
715
|
-
await vfs.restoreMirror();
|
|
716
|
-
```
|
|
717
|
-
|
|
718
|
-
##### `async flushMirror(): Promise<void>`
|
|
719
|
-
|
|
720
|
-
Compatibility hook to finalize mirror boundary operations.
|
|
721
|
-
|
|
722
|
-
```typescript
|
|
723
|
-
// After file modifications...
|
|
724
|
-
await vfs.flushMirror();
|
|
725
|
-
```
|
|
726
|
-
|
|
727
|
-
##### `mkdir(path: string, mode?: number): void`
|
|
728
|
-
|
|
729
|
-
Creates directory and any missing parents. Throws if parent is a file.
|
|
730
|
-
|
|
731
|
-
```typescript
|
|
732
|
-
vfs.mkdir("/home/user/.ssh", 0o700);
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
##### `writeFile(path: string, content: string | Buffer, options?: WriteFileOptions): void`
|
|
736
|
-
|
|
737
|
-
Writes file content. Creates parent directories if missing.
|
|
738
|
-
|
|
739
|
-
- **options.mode**: POSIX file mode (default: 0o644)
|
|
740
|
-
- **options.compress**: Store as gzip (default: false)
|
|
741
|
-
|
|
742
|
-
```typescript
|
|
743
|
-
vfs.writeFile("/etc/app.conf", "debug=true\n", { compress: true });
|
|
744
|
-
```
|
|
745
|
-
|
|
746
|
-
##### `readFile(path: string): string`
|
|
747
|
-
|
|
748
|
-
Reads file as UTF-8 string. Transparently decompresses if needed.
|
|
749
|
-
|
|
750
|
-
```typescript
|
|
751
|
-
const content = vfs.readFile("/etc/app.conf");
|
|
752
|
-
```
|
|
753
|
-
|
|
754
|
-
##### `exists(path: string): boolean`
|
|
755
|
-
|
|
756
|
-
Checks node existence (file or directory).
|
|
757
|
-
|
|
758
|
-
```typescript
|
|
759
|
-
if (!vfs.exists("/var/log")) {
|
|
760
|
-
vfs.mkdir("/var/log");
|
|
761
|
-
}
|
|
762
|
-
```
|
|
763
|
-
|
|
764
|
-
##### `stat(path: string): VfsNodeStats`
|
|
765
|
-
|
|
766
|
-
Returns metadata (type, size, dates, mode, etc.).
|
|
767
|
-
|
|
768
|
-
```typescript
|
|
769
|
-
const stats = vfs.stat("/etc/hostname");
|
|
770
|
-
if (stats.type === "file") {
|
|
771
|
-
console.log(`File size: ${stats.size} bytes`);
|
|
772
|
-
}
|
|
773
|
-
```
|
|
774
|
-
|
|
775
|
-
##### `list(dirPath?: string): string[]`
|
|
776
|
-
|
|
777
|
-
Lists child names in directory (sorted). Throws if path not a directory.
|
|
778
|
-
|
|
779
|
-
```typescript
|
|
780
|
-
const files = vfs.list("/home");
|
|
781
|
-
// ["alice", "bob", "root"]
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
##### `tree(dirPath?: string): string`
|
|
785
|
-
|
|
786
|
-
Renders ASCII tree view of directory hierarchy.
|
|
787
|
-
|
|
788
|
-
```typescript
|
|
789
|
-
console.log(vfs.tree("/home"));
|
|
790
|
-
```
|
|
791
|
-
|
|
792
|
-
##### `chmod(path: string, mode: number): void`
|
|
793
|
-
|
|
794
|
-
Updates file/dir permissions.
|
|
795
|
-
|
|
796
|
-
```typescript
|
|
797
|
-
vfs.chmod("/tmp/script.sh", 0o755);
|
|
798
|
-
```
|
|
799
|
-
|
|
800
|
-
##### `remove(path: string, options?: RemoveOptions): void`
|
|
801
|
-
|
|
802
|
-
Removes file or directory. Throws if directory not empty unless `recursive: true`.
|
|
803
|
-
|
|
804
|
-
```typescript
|
|
805
|
-
vfs.remove("/tmp/old", { recursive: true });
|
|
806
|
-
```
|
|
807
|
-
|
|
808
|
-
##### `move(fromPath: string, toPath: string): void`
|
|
809
|
-
|
|
810
|
-
Renames or moves node. Throws if destination exists.
|
|
811
|
-
|
|
812
|
-
```typescript
|
|
813
|
-
vfs.move("/var/tmp", "/var/backup");
|
|
814
|
-
```
|
|
815
|
-
|
|
816
|
-
##### `compressFile(path: string): void`
|
|
817
|
-
|
|
818
|
-
Gzip-compresses file content and marks as compressed.
|
|
819
|
-
|
|
820
|
-
```typescript
|
|
821
|
-
vfs.compressFile("/var/log/app.log");
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
##### `decompressFile(path: string): void`
|
|
825
|
-
|
|
826
|
-
Decompresses file content (inverse of `compressFile`).
|
|
827
|
-
|
|
828
|
-
```typescript
|
|
829
|
-
vfs.decompressFile("/var/log/app.log");
|
|
830
|
-
```
|
|
831
|
-
|
|
832
|
-
**Example:**
|
|
833
|
-
|
|
834
|
-
```typescript
|
|
835
|
-
vfs.on("file:write", ({ path, size }) => {
|
|
836
|
-
console.log(`[VFS] File written: ${path} (${size} bytes)`);
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
vfs.on("dir:create", ({ path, mode }) => {
|
|
840
|
-
console.log(`[VFS] Directory created: ${path} (mode: ${mode.toString(8)})`);
|
|
841
|
-
});
|
|
842
|
-
```
|
|
346
|
+
#### Events
|
|
347
|
+
|
|
348
|
+
| Event | Data | Description |
|
|
349
|
+
|-------|------|-------------|
|
|
350
|
+
| `start` | `{ port: number }` | SFTP server started |
|
|
351
|
+
| `stop` | — | SFTP server stopped |
|
|
352
|
+
| `auth:success` | `{ username, remoteAddress }` | User authenticated |
|
|
353
|
+
| `auth:failure` | `{ username, remoteAddress }` | Auth failed |
|
|
354
|
+
| `client:connect` | — | New SFTP client connected |
|
|
355
|
+
| `client:disconnect` | `{ user: string }` | SFTP client disconnected |
|
|
843
356
|
|
|
844
357
|
---
|
|
845
358
|
|
|
846
|
-
###
|
|
359
|
+
### `VirtualShell`
|
|
847
360
|
|
|
848
|
-
|
|
361
|
+
Coordinates the virtual filesystem, user manager, and command runtime. Used by both SSH servers and the programmatic `SshClient`.
|
|
849
362
|
|
|
850
363
|
#### Constructor
|
|
851
364
|
|
|
852
365
|
```typescript
|
|
853
|
-
new
|
|
366
|
+
new VirtualShell(
|
|
367
|
+
hostname: string,
|
|
368
|
+
properties?: ShellProperties,
|
|
369
|
+
vfsOptions?: VfsOptions,
|
|
370
|
+
)
|
|
854
371
|
```
|
|
855
372
|
|
|
856
|
-
- **
|
|
857
|
-
- **
|
|
858
|
-
- **
|
|
373
|
+
- **hostname**: Injected into command context and prompt.
|
|
374
|
+
- **properties**: Optional shell metadata shown in `uname`-like output. Defaults to `defaultShellProperties`.
|
|
375
|
+
- **vfsOptions**: Optional VFS persistence options — see [VirtualFileSystem](#virtualfilesystem).
|
|
859
376
|
|
|
860
377
|
```typescript
|
|
861
|
-
|
|
378
|
+
interface ShellProperties {
|
|
379
|
+
kernel: string; // e.g. "1.0.0+itsrealfortune+1-amd64"
|
|
380
|
+
os: string; // e.g. "Fortune GNU/Linux x64"
|
|
381
|
+
arch: string; // e.g. "x86_64"
|
|
382
|
+
}
|
|
862
383
|
```
|
|
863
384
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
##### `async initialize(): Promise<void>`
|
|
867
|
-
|
|
868
|
-
Loads users/sudoers from disk, ensures root exists, and initializes sessions.
|
|
385
|
+
**Example:**
|
|
869
386
|
|
|
870
387
|
```typescript
|
|
871
|
-
|
|
388
|
+
const shell = new VirtualShell("typescript-vm", {
|
|
389
|
+
kernel: "1.0.0+itsrealfortune+1-amd64",
|
|
390
|
+
os: "Fortune GNU/Linux x64",
|
|
391
|
+
arch: "x86_64",
|
|
392
|
+
}, {
|
|
393
|
+
mode: "fs",
|
|
394
|
+
snapshotPath: "./data",
|
|
395
|
+
});
|
|
872
396
|
```
|
|
873
397
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
Checks plaintext password against hashed record.
|
|
877
|
-
|
|
878
|
-
```typescript
|
|
879
|
-
if (users.verifyPassword("alice", "password123")) {
|
|
880
|
-
console.log("Auth OK");
|
|
881
|
-
}
|
|
882
|
-
```
|
|
398
|
+
#### Methods
|
|
883
399
|
|
|
884
|
-
|
|
400
|
+
| Method | Description |
|
|
401
|
+
|--------|-------------|
|
|
402
|
+
| `ensureInitialized(): Promise<void>` | Await this before using the shell programmatically. |
|
|
403
|
+
| `addCommand(name, params, callback)` | Register a custom shell command. |
|
|
404
|
+
| `executeCommand(rawInput, authUser, cwd)` | Run a raw command string. |
|
|
405
|
+
| `startInteractiveSession(stream, authUser, sessionId, remoteAddress, terminalSize)` | Start an SSH interactive session. |
|
|
406
|
+
| `writeFileAsUser(authUser, path, content)` | Write a file with quota enforcement. |
|
|
407
|
+
| `getVfs(): VirtualFileSystem \| null` | Access the VFS instance. |
|
|
408
|
+
| `getUsers(): VirtualUserManager \| null` | Access the user manager. |
|
|
409
|
+
| `getHostname(): string` | Returns the configured hostname. |
|
|
885
410
|
|
|
886
|
-
|
|
411
|
+
**Custom command example:**
|
|
887
412
|
|
|
888
413
|
```typescript
|
|
889
|
-
|
|
890
|
-
|
|
414
|
+
shell.addCommand("greet", ["[name]"], ({ args, authUser }) => {
|
|
415
|
+
const name = args[0] ?? authUser;
|
|
416
|
+
return { stdout: `Hello, ${name}!`, exitCode: 0 };
|
|
417
|
+
});
|
|
418
|
+
// Inside the shell: greet world → Hello, world!
|
|
891
419
|
```
|
|
892
420
|
|
|
893
|
-
|
|
421
|
+
#### Events
|
|
894
422
|
|
|
895
|
-
|
|
423
|
+
| Event | Data | Description |
|
|
424
|
+
|-------|------|-------------|
|
|
425
|
+
| `initialized` | — | Shell initialization complete |
|
|
426
|
+
| `command` | `{ command, user, cwd }` | A command was executed |
|
|
427
|
+
| `session:start` | `{ user, sessionId, remoteAddress }` | Interactive session started |
|
|
896
428
|
|
|
897
|
-
|
|
898
|
-
await users.deleteUser("bob");
|
|
899
|
-
```
|
|
429
|
+
---
|
|
900
430
|
|
|
901
|
-
|
|
431
|
+
### `VirtualFileSystem`
|
|
902
432
|
|
|
903
|
-
|
|
433
|
+
Pure in-memory virtual filesystem. All state lives in a recursive `Map`-based tree — no host filesystem access at runtime.
|
|
904
434
|
|
|
435
|
+
Two persistence modes are available via the `VfsOptions` constructor argument:
|
|
905
436
|
|
|
906
437
|
```typescript
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
910
|
-
```
|
|
911
|
-
|
|
912
|
-
##### `async addSudoer(username: string): Promise<void>`
|
|
438
|
+
// Default — pure in-memory, zero disk I/O
|
|
439
|
+
const vfs = new VirtualFileSystem();
|
|
440
|
+
const vfs = new VirtualFileSystem({ mode: "memory" });
|
|
913
441
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
442
|
+
// FS mode — JSON snapshot auto-saved to disk on flushMirror()
|
|
443
|
+
const vfs = new VirtualFileSystem({
|
|
444
|
+
mode: "fs",
|
|
445
|
+
snapshotPath: "./data", // writes ./data/vfs-snapshot.json
|
|
446
|
+
});
|
|
447
|
+
await vfs.restoreMirror(); // load from disk (silent no-op if no file yet)
|
|
448
|
+
// ... use vfs ...
|
|
449
|
+
await vfs.flushMirror(); // persist to disk
|
|
918
450
|
```
|
|
919
451
|
|
|
920
|
-
|
|
452
|
+
Both modes expose exactly the same API. The tree always lives in memory; `"fs"` mode adds a JSON round-trip on `restoreMirror` / `flushMirror`.
|
|
921
453
|
|
|
922
|
-
|
|
454
|
+
#### Constructor
|
|
923
455
|
|
|
924
456
|
```typescript
|
|
925
|
-
|
|
457
|
+
interface VfsOptions {
|
|
458
|
+
mode?: "memory" | "fs"; // default: "memory"
|
|
459
|
+
snapshotPath?: string; // required when mode is "fs"
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
new VirtualFileSystem(options?: VfsOptions)
|
|
926
463
|
```
|
|
927
464
|
|
|
928
|
-
|
|
465
|
+
#### Methods
|
|
929
466
|
|
|
930
|
-
|
|
467
|
+
| Method | Description |
|
|
468
|
+
|--------|-------------|
|
|
469
|
+
| `mkdir(path, mode?)` | Create directory and any missing parents. |
|
|
470
|
+
| `writeFile(path, content, options?)` | Write file (creates parent dirs). `options.compress` stores as gzip; `options.mode` sets POSIX mode bits. |
|
|
471
|
+
| `readFile(path): string` | Read file as UTF-8. Transparently decompresses gzip files. |
|
|
472
|
+
| `readFileRaw(path): Buffer` | Read file as Buffer (decompresses if needed). |
|
|
473
|
+
| `exists(path): boolean` | Test whether a file or directory exists. |
|
|
474
|
+
| `stat(path): VfsNodeStats` | Returns file/directory metadata. |
|
|
475
|
+
| `list(path?): string[]` | List direct children of a directory (sorted). |
|
|
476
|
+
| `tree(path?): string` | Render ASCII directory tree. |
|
|
477
|
+
| `move(from, to)` | Move or rename a node. Throws if destination exists. |
|
|
478
|
+
| `remove(path, options?)` | Delete file or directory. `options.recursive` required for non-empty dirs. |
|
|
479
|
+
| `chmod(path, mode)` | Update POSIX mode bits. |
|
|
480
|
+
| `compressFile(path)` | Gzip-compress file content in place. |
|
|
481
|
+
| `decompressFile(path)` | Gunzip file content in place. |
|
|
482
|
+
| `symlink(target, linkPath)` | Create a symbolic link (mode `0o120777`). |
|
|
483
|
+
| `isSymlink(path): boolean` | Returns true if the path is a symlink node. |
|
|
484
|
+
| `resolveSymlink(path, maxDepth?): string` | Resolve symlink chain to real path (default max 8 hops). |
|
|
485
|
+
| `getUsageBytes(path?): number` | Total stored bytes under a path. |
|
|
486
|
+
| `getMode(): VfsPersistenceMode` | Returns `"memory"` or `"fs"`. |
|
|
487
|
+
| `getSnapshotPath(): string \| null` | Snapshot file path in `"fs"` mode, or null. |
|
|
488
|
+
| `toSnapshot(): VfsSnapshot` | Export the whole tree as a JSON-serialisable snapshot. |
|
|
489
|
+
| `importSnapshot(snapshot)` | Replace current state from a snapshot (preserves mode). |
|
|
490
|
+
| `restoreMirror(): Promise<void>` | Load from disk (`"fs"` mode) / no-op (`"memory"` mode). |
|
|
491
|
+
| `flushMirror(): Promise<void>` | Save to disk (`"fs"` mode) / emit `mirror:flush` (`"memory"` mode). |
|
|
492
|
+
| `VirtualFileSystem.fromSnapshot(snapshot)` | **Static.** Create a new memory-mode instance from a snapshot. |
|
|
931
493
|
|
|
932
|
-
|
|
933
|
-
await users.setQuotaBytes("alice", 5 * 1024 * 1024); // 5 MB
|
|
934
|
-
```
|
|
494
|
+
#### Events
|
|
935
495
|
|
|
936
|
-
|
|
496
|
+
| Event | Data | Description |
|
|
497
|
+
|-------|------|-------------|
|
|
498
|
+
| `file:write` | `{ path, size }` | File written |
|
|
499
|
+
| `file:read` | `{ path, size }` | File read |
|
|
500
|
+
| `dir:create` | `{ path, mode }` | Directory created |
|
|
501
|
+
| `node:remove` | `{ path }` | File or directory deleted |
|
|
502
|
+
| `symlink:create` | `{ link, target }` | Symlink created |
|
|
503
|
+
| `snapshot:import` | — | `importSnapshot()` called |
|
|
504
|
+
| `snapshot:restore` | `{ path }` | Restored from disk (fs mode) |
|
|
505
|
+
| `mirror:flush` | `{ path? }` | Flushed (path present in fs mode) |
|
|
937
506
|
|
|
938
|
-
|
|
507
|
+
**Example:**
|
|
939
508
|
|
|
940
509
|
```typescript
|
|
941
|
-
|
|
942
|
-
|
|
510
|
+
vfs.on("file:write", ({ path, size }) => {
|
|
511
|
+
console.log(`[VFS] Written: ${path} (${size} bytes)`);
|
|
512
|
+
});
|
|
943
513
|
|
|
944
|
-
|
|
514
|
+
vfs.on("dir:create", ({ path, mode }) => {
|
|
515
|
+
console.log(`[VFS] Dir created: ${path} (mode: ${mode.toString(8)})`);
|
|
516
|
+
});
|
|
517
|
+
```
|
|
945
518
|
|
|
946
|
-
|
|
519
|
+
#### Memory mode — manual snapshot persistence
|
|
947
520
|
|
|
948
521
|
```typescript
|
|
949
|
-
|
|
950
|
-
|
|
522
|
+
import { VirtualFileSystem } from "typescript-virtual-container";
|
|
523
|
+
import { writeFileSync, readFileSync } from "node:fs";
|
|
951
524
|
|
|
952
|
-
|
|
525
|
+
const vfs = new VirtualFileSystem(); // mode: "memory"
|
|
526
|
+
vfs.writeFile("/etc/config.json", JSON.stringify({ debug: true }));
|
|
953
527
|
|
|
954
|
-
|
|
528
|
+
// Export to disk manually
|
|
529
|
+
writeFileSync("vfs-snapshot.json", JSON.stringify(vfs.toSnapshot()));
|
|
955
530
|
|
|
956
|
-
|
|
957
|
-
|
|
531
|
+
// Restore into a new instance
|
|
532
|
+
const snapshot = JSON.parse(readFileSync("vfs-snapshot.json", "utf8"));
|
|
533
|
+
const restored = VirtualFileSystem.fromSnapshot(snapshot);
|
|
534
|
+
console.log(restored.readFile("/etc/config.json")); // {"debug":true}
|
|
958
535
|
```
|
|
959
536
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
Validates a write operation against quota rules; throws when projected usage exceeds quota.
|
|
537
|
+
#### FS mode — automatic persistence across restarts
|
|
963
538
|
|
|
964
539
|
```typescript
|
|
965
|
-
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
##### `registerSession(username: string, remoteAddress: string): VirtualActiveSession`
|
|
540
|
+
import { VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
969
541
|
|
|
970
|
-
|
|
542
|
+
const shell = new VirtualShell("my-vm", undefined, {
|
|
543
|
+
mode: "fs",
|
|
544
|
+
snapshotPath: "./vfs-data",
|
|
545
|
+
});
|
|
971
546
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
547
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
548
|
+
await ssh.start();
|
|
549
|
+
// VFS is restored from ./vfs-data/vfs-snapshot.json on start (if it exists).
|
|
550
|
+
// flushMirror() is called after each write, persisting state to disk automatically.
|
|
975
551
|
```
|
|
976
552
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
Closes session. Safe to call with null.
|
|
553
|
+
---
|
|
980
554
|
|
|
981
|
-
|
|
982
|
-
users.unregisterSession(sessionId);
|
|
983
|
-
```
|
|
555
|
+
### `VirtualUserManager`
|
|
984
556
|
|
|
985
|
-
|
|
557
|
+
Manages virtual users, password hashing (scrypt), sudo privileges, per-user storage quotas, SSH public keys, and active session tracking.
|
|
986
558
|
|
|
987
|
-
|
|
559
|
+
#### Constructor
|
|
988
560
|
|
|
989
561
|
```typescript
|
|
990
|
-
|
|
562
|
+
new VirtualUserManager(
|
|
563
|
+
vfs: VirtualFileSystem,
|
|
564
|
+
autoSudoForNewUsers?: boolean, // default: true
|
|
565
|
+
)
|
|
991
566
|
```
|
|
992
567
|
|
|
993
|
-
|
|
568
|
+
- Auth data is stored inside the VFS at protected paths under `/virtual-env-js/.auth/`.
|
|
569
|
+
- `autoSudoForNewUsers`: when true, newly created users are automatically added to sudoers.
|
|
994
570
|
|
|
995
|
-
|
|
571
|
+
#### Methods
|
|
996
572
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
573
|
+
| Method | Description |
|
|
574
|
+
|--------|-------------|
|
|
575
|
+
| `initialize(): Promise<void>` | Load users/sudoers from VFS, ensure root exists. Call once on startup. |
|
|
576
|
+
| `verifyPassword(username, password): boolean` | Check plaintext password against stored hash. |
|
|
577
|
+
| `hasPassword(username): boolean` | Returns true if a password is set for the user. |
|
|
578
|
+
| `hashPassword(password): string` | Hash a password using the configured algorithm. |
|
|
579
|
+
| `addUser(username, password): Promise<void>` | Create user with home directory. |
|
|
580
|
+
| `deleteUser(username): Promise<void>` | Delete user. Cannot delete root. |
|
|
581
|
+
| `isSudoer(username): boolean` | Check if user has sudo privileges. |
|
|
582
|
+
| `addSudoer(username): Promise<void>` | Grant sudo privileges. |
|
|
583
|
+
| `removeSudoer(username): Promise<void>` | Revoke sudo privileges. Cannot remove root. |
|
|
584
|
+
| `setQuotaBytes(username, maxBytes): Promise<void>` | Set per-user write quota (bytes under `/home/<user>`). |
|
|
585
|
+
| `clearQuota(username): Promise<void>` | Remove quota limit. |
|
|
586
|
+
| `getQuotaBytes(username): number \| null` | Returns quota in bytes, or null if unlimited. |
|
|
587
|
+
| `getUsageBytes(username): number` | Returns current usage in bytes under `/home/<user>`. |
|
|
588
|
+
| `assertWriteWithinQuota(username, path, content)` | Throws if the write would exceed the user's quota. |
|
|
589
|
+
| `addAuthorizedKey(username, algo, data)` | Register an SSH public key for the user. |
|
|
590
|
+
| `getAuthorizedKeys(username)` | Returns the list of authorized keys for a user. |
|
|
591
|
+
| `removeAuthorizedKeys(username)` | Revoke all authorized keys for a user. |
|
|
592
|
+
| `registerSession(username, remoteAddress): VirtualActiveSession` | Start session tracking, returns session descriptor. |
|
|
593
|
+
| `unregisterSession(sessionId): void` | End session. Safe to call with null. |
|
|
594
|
+
| `updateSession(sessionId, username, remoteAddress): void` | Update session metadata (used by `su`/`sudo`). |
|
|
595
|
+
| `listActiveSessions(): VirtualActiveSession[]` | Returns all active sessions sorted by start time. |
|
|
1003
596
|
|
|
1004
597
|
#### Events
|
|
1005
598
|
|
|
1006
|
-
`VirtualUserManager` extends `EventEmitter` and emits the following events:
|
|
1007
|
-
|
|
1008
599
|
| Event | Data | Description |
|
|
1009
600
|
|-------|------|-------------|
|
|
1010
|
-
| `initialized` | — | User manager
|
|
1011
|
-
| `user:add` | `{ username
|
|
1012
|
-
| `user:delete` | `{ username
|
|
1013
|
-
| `
|
|
1014
|
-
| `
|
|
601
|
+
| `initialized` | — | User manager ready, root account ensured |
|
|
602
|
+
| `user:add` | `{ username }` | New user created |
|
|
603
|
+
| `user:delete` | `{ username }` | User deleted |
|
|
604
|
+
| `key:add` | `{ username, algo }` | Public key added |
|
|
605
|
+
| `key:remove` | `{ username }` | Public keys removed |
|
|
606
|
+
| `session:register` | `{ sessionId, username, remoteAddress }` | Session started |
|
|
607
|
+
| `session:unregister` | `{ sessionId, username }` | Session ended |
|
|
1015
608
|
|
|
1016
609
|
**Example:**
|
|
1017
610
|
|
|
1018
611
|
```typescript
|
|
1019
612
|
users.on("user:add", ({ username }) => {
|
|
1020
|
-
|
|
613
|
+
console.log(`[USERS] Created: ${username}`);
|
|
1021
614
|
});
|
|
1022
615
|
|
|
1023
616
|
users.on("session:register", ({ sessionId, username, remoteAddress }) => {
|
|
1024
|
-
|
|
1025
|
-
});
|
|
1026
|
-
|
|
1027
|
-
users.on("session:unregister", ({ sessionId, username }) => {
|
|
1028
|
-
console.log(`[USERS] Session ${sessionId} (${username}) closed`);
|
|
617
|
+
console.log(`[USERS] Session ${sessionId}: ${username} from ${remoteAddress}`);
|
|
1029
618
|
});
|
|
1030
619
|
```
|
|
1031
620
|
|
|
1032
621
|
---
|
|
1033
622
|
|
|
1034
|
-
### HoneyPot
|
|
623
|
+
### `HoneyPot`
|
|
1035
624
|
|
|
1036
|
-
Comprehensive security auditing and event tracking utility. Attaches to all core components
|
|
625
|
+
Comprehensive security auditing and event tracking utility. Attaches listeners to all core components to log activity, track statistics, and detect anomalies.
|
|
1037
626
|
|
|
1038
627
|
#### Constructor
|
|
1039
628
|
|
|
1040
629
|
```typescript
|
|
1041
|
-
new HoneyPot(maxLogSize?: number)
|
|
1042
|
-
```
|
|
1043
|
-
|
|
1044
|
-
- **maxLogSize**: Maximum audit log entries to retain (default: 10000)
|
|
1045
|
-
|
|
1046
|
-
```typescript
|
|
1047
|
-
const honeypot = new HoneyPot(5000); // Keep last 5000 audit entries
|
|
630
|
+
new HoneyPot(maxLogSize?: number) // default: 10000
|
|
1048
631
|
```
|
|
1049
632
|
|
|
1050
633
|
#### Methods
|
|
1051
634
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
635
|
+
| Method | Description |
|
|
636
|
+
|--------|-------------|
|
|
637
|
+
| `attach(shell, vfs, users, ssh?, sftp?)` | Subscribe to all event sources. |
|
|
638
|
+
| `getAuditLog(type?, source?): AuditLogEntry[]` | Full log, optionally filtered by event type and/or source component. |
|
|
639
|
+
| `getStats(): Readonly<HoneyPotStats>` | Aggregated activity counters. |
|
|
640
|
+
| `getRecent(limit?): AuditLogEntry[]` | Most recent entries in reverse chronological order. |
|
|
641
|
+
| `detectAnomalies()` | Analyze patterns — returns `{ type, severity, message }[]`. |
|
|
642
|
+
| `reset()` | Clear audit log and reset all stat counters. |
|
|
643
|
+
| `exportJson(): string` | Serialise full log + stats to a JSON string. |
|
|
644
|
+
|
|
645
|
+
#### HoneyPotStats fields
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
interface HoneyPotStats {
|
|
649
|
+
authAttempts: number;
|
|
650
|
+
authSuccesses: number;
|
|
651
|
+
authFailures: number;
|
|
652
|
+
commands: number;
|
|
653
|
+
fileWrites: number;
|
|
654
|
+
fileReads: number;
|
|
655
|
+
sessionStarts: number;
|
|
656
|
+
sessionEnds: number;
|
|
657
|
+
userCreated: number;
|
|
658
|
+
userDeleted: number;
|
|
659
|
+
clientConnects: number;
|
|
660
|
+
clientDisconnects: number;
|
|
661
|
+
}
|
|
1059
662
|
```
|
|
1060
663
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
Returns audit log entries with optional filtering by event type or source component.
|
|
664
|
+
#### Audit Log Entry
|
|
1064
665
|
|
|
1065
666
|
```typescript
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
//
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
// Only SshMimic events
|
|
1073
|
-
const sshLogs = honeypot.getAuditLog(undefined, "SshMimic");
|
|
1074
|
-
|
|
1075
|
-
// Combine filters
|
|
1076
|
-
const sshAuthLogs = honeypot.getAuditLog("auth:success", "SshMimic");
|
|
667
|
+
interface AuditLogEntry {
|
|
668
|
+
timestamp: string; // ISO-8601
|
|
669
|
+
type: string; // e.g. "auth:failure", "file:write"
|
|
670
|
+
source: string; // e.g. "SshMimic", "VirtualFileSystem"
|
|
671
|
+
details: Record<string, unknown>; // event-specific payload
|
|
672
|
+
}
|
|
1077
673
|
```
|
|
1078
674
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
Returns current activity statistics snapshot.
|
|
675
|
+
#### Example
|
|
1082
676
|
|
|
1083
677
|
```typescript
|
|
1084
|
-
|
|
1085
|
-
console.log(`Auth attempts: ${stats.authAttempts}`);
|
|
1086
|
-
console.log(`Auth successes: ${stats.authSuccesses}`);
|
|
1087
|
-
console.log(`Auth failures: ${stats.authFailures}`);
|
|
1088
|
-
console.log(`Commands executed: ${stats.commands}`);
|
|
1089
|
-
console.log(`File writes: ${stats.fileWrites}`);
|
|
1090
|
-
console.log(`File reads: ${stats.fileReads}`);
|
|
1091
|
-
console.log(`Sessions started: ${stats.sessionStarts}`);
|
|
1092
|
-
console.log(`Sessions ended: ${stats.sessionEnds}`);
|
|
1093
|
-
console.log(`Users created: ${stats.userCreated}`);
|
|
1094
|
-
console.log(`Users deleted: ${stats.userDeleted}`);
|
|
1095
|
-
console.log(`Client connects: ${stats.clientConnects}`);
|
|
1096
|
-
console.log(`Client disconnects: ${stats.clientDisconnects}`);
|
|
1097
|
-
```
|
|
1098
|
-
|
|
1099
|
-
##### `getRecent(limit?: number): AuditLogEntry[]`
|
|
678
|
+
import { HoneyPot, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
1100
679
|
|
|
1101
|
-
|
|
680
|
+
const shell = new VirtualShell("honeypot");
|
|
681
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
682
|
+
const hp = new HoneyPot(50_000);
|
|
1102
683
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
last50.forEach(entry => {
|
|
1106
|
-
console.log(`${entry.timestamp} | ${entry.source} | ${entry.type}`);
|
|
1107
|
-
console.log(`Details:`, entry.details);
|
|
1108
|
-
});
|
|
1109
|
-
```
|
|
684
|
+
await ssh.start();
|
|
685
|
+
hp.attach(shell, shell.vfs, shell.users, ssh);
|
|
1110
686
|
|
|
1111
|
-
|
|
687
|
+
// Filter audit log
|
|
688
|
+
const failures = hp.getAuditLog("auth:failure");
|
|
689
|
+
failures.forEach(e => console.log(e.details.username, e.details.remoteAddress));
|
|
1112
690
|
|
|
1113
|
-
|
|
691
|
+
// Detect anomalies
|
|
692
|
+
hp.detectAnomalies().forEach(a =>
|
|
693
|
+
console.log(`[${a.severity.toUpperCase()}] ${a.type}: ${a.message}`)
|
|
694
|
+
);
|
|
1114
695
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
console.log(` ${anomaly.message}`);
|
|
696
|
+
// Export on shutdown
|
|
697
|
+
process.on("SIGINT", () => {
|
|
698
|
+
require("fs").writeFileSync("audit.json", hp.exportJson());
|
|
699
|
+
process.exit(0);
|
|
1120
700
|
});
|
|
1121
701
|
```
|
|
1122
702
|
|
|
1123
|
-
|
|
703
|
+
**`detectAnomalies` detects:**
|
|
1124
704
|
- High authentication failure rates
|
|
1125
705
|
- Excessive authentication failures
|
|
1126
706
|
- Unusual command execution volume
|
|
1127
707
|
- Unusual file write volume
|
|
1128
708
|
|
|
1129
|
-
|
|
709
|
+
---
|
|
1130
710
|
|
|
1131
|
-
|
|
711
|
+
### `SshClient` (Programmatic API)
|
|
1132
712
|
|
|
1133
|
-
|
|
1134
|
-
honeypot.reset(); // Fresh start
|
|
1135
|
-
```
|
|
713
|
+
Execute shell commands against a `VirtualShell` without SSH protocol overhead. Maintains working-directory state across calls.
|
|
1136
714
|
|
|
1137
|
-
####
|
|
715
|
+
#### Constructor
|
|
1138
716
|
|
|
1139
717
|
```typescript
|
|
1140
|
-
|
|
1141
|
-
timestamp: string; // ISO-8601 timestamp
|
|
1142
|
-
type: string; // Event type (e.g., "auth:success", "file:write")
|
|
1143
|
-
source: string; // Event source component
|
|
1144
|
-
details: Record<string, unknown>; // Event-specific data
|
|
1145
|
-
}
|
|
718
|
+
new SshClient(shell: VirtualShell, username: string)
|
|
1146
719
|
```
|
|
1147
720
|
|
|
1148
|
-
|
|
721
|
+
No password required — the client authenticates by username only.
|
|
722
|
+
|
|
723
|
+
#### Methods
|
|
1149
724
|
|
|
1150
|
-
|
|
725
|
+
| Method | Description |
|
|
726
|
+
|--------|-------------|
|
|
727
|
+
| `exec(command): Promise<CommandResult>` | Run arbitrary raw command string. |
|
|
728
|
+
| `ls(path?)` | List directory (default: cwd). |
|
|
729
|
+
| `pwd()` | Print current working directory. |
|
|
730
|
+
| `cd(path)` | Change directory. Updates internal cwd state on success. |
|
|
731
|
+
| `cat(path)` | Read file content via `cat` command. |
|
|
732
|
+
| `readFile(path)` | Read file directly from VFS (programmatic, no shell parse). |
|
|
733
|
+
| `writeFile(path, content)` | Write file directly to VFS (programmatic). |
|
|
734
|
+
| `mkdir(path, recursive?)` | Create directory. `recursive=true` adds `-p`. |
|
|
735
|
+
| `touch(path)` | Create empty file. |
|
|
736
|
+
| `rm(path, recursive?)` | Remove file or directory. `recursive=true` adds `-r`. |
|
|
737
|
+
| `tree(path?)` | Render ASCII directory tree. |
|
|
738
|
+
| `whoami()` | Print current user. |
|
|
739
|
+
| `hostname()` | Print server hostname. |
|
|
740
|
+
| `who()` | List active sessions. |
|
|
741
|
+
| `getCwd(): string` | Returns current working directory (local, no I/O). |
|
|
742
|
+
| `getUsername(): string` | Returns authenticated username. |
|
|
1151
743
|
|
|
1152
|
-
|
|
744
|
+
**Example:**
|
|
1153
745
|
|
|
1154
|
-
|
|
746
|
+
```typescript
|
|
747
|
+
const shell = new VirtualShell("typescript-vm");
|
|
748
|
+
const client = new SshClient(shell, "alice");
|
|
1155
749
|
|
|
1156
|
-
|
|
750
|
+
await client.mkdir("/home/alice/projects", true);
|
|
751
|
+
await client.cd("/home/alice/projects");
|
|
1157
752
|
|
|
1158
|
-
|
|
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
|
-
```
|
|
753
|
+
console.log(client.getCwd()); // /home/alice/projects
|
|
1161
754
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
3. Clean up by removing the script after execution.
|
|
755
|
+
await client.writeFile("notes.txt", "Work in progress");
|
|
756
|
+
const list = await client.ls();
|
|
757
|
+
console.log(list.stdout); // notes.txt
|
|
1166
758
|
|
|
1167
|
-
|
|
759
|
+
const read = await client.readFile("notes.txt");
|
|
760
|
+
console.log(read.stdout); // Work in progress
|
|
761
|
+
```
|
|
1168
762
|
|
|
1169
763
|
---
|
|
1170
764
|
|
|
1171
765
|
### Key Types
|
|
1172
766
|
|
|
1173
|
-
#### CommandResult
|
|
767
|
+
#### `CommandResult`
|
|
1174
768
|
|
|
1175
|
-
|
|
769
|
+
Returned by all command executions (shell or programmatic).
|
|
1176
770
|
|
|
1177
771
|
```typescript
|
|
1178
772
|
interface CommandResult {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
773
|
+
stdout?: string; // Standard output
|
|
774
|
+
stderr?: string; // Standard error
|
|
775
|
+
exitCode?: number; // Exit code (default: 0)
|
|
776
|
+
nextCwd?: string; // Updated cwd (set by cd command)
|
|
777
|
+
clearScreen?: boolean; // Request terminal clear
|
|
778
|
+
closeSession?: boolean; // Request session close
|
|
779
|
+
switchUser?: string; // User switch (su/sudo)
|
|
780
|
+
openEditor?: NanoEditorSession; // Nano editor launch
|
|
781
|
+
openHtop?: boolean; // htop launch
|
|
782
|
+
sudoChallenge?: SudoChallenge; // Sudo password challenge
|
|
1189
783
|
}
|
|
1190
784
|
```
|
|
1191
785
|
|
|
1192
|
-
#### VfsNodeStats
|
|
1193
|
-
|
|
1194
|
-
File/directory metadata.
|
|
786
|
+
#### `VfsNodeStats`
|
|
1195
787
|
|
|
1196
788
|
```typescript
|
|
1197
789
|
type VfsNodeStats = VfsFileNode | VfsDirectoryNode;
|
|
1198
790
|
|
|
1199
791
|
interface VfsFileNode {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
792
|
+
type: "file";
|
|
793
|
+
name: string;
|
|
794
|
+
path: string;
|
|
795
|
+
mode: number; // POSIX mode bits
|
|
796
|
+
size: number; // Byte length (compressed size when compressed=true)
|
|
797
|
+
compressed: boolean;
|
|
798
|
+
createdAt: Date;
|
|
799
|
+
updatedAt: Date;
|
|
1208
800
|
}
|
|
1209
801
|
|
|
1210
802
|
interface VfsDirectoryNode {
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
803
|
+
type: "directory";
|
|
804
|
+
name: string;
|
|
805
|
+
path: string;
|
|
806
|
+
mode: number;
|
|
807
|
+
childrenCount: number;
|
|
808
|
+
createdAt: Date;
|
|
809
|
+
updatedAt: Date;
|
|
1218
810
|
}
|
|
1219
811
|
```
|
|
1220
812
|
|
|
1221
|
-
#### VirtualActiveSession
|
|
1222
|
-
|
|
1223
|
-
Active SSH/programmatic session descriptor.
|
|
813
|
+
#### `VirtualActiveSession`
|
|
1224
814
|
|
|
1225
815
|
```typescript
|
|
1226
816
|
interface VirtualActiveSession {
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
817
|
+
id: string; // UUID
|
|
818
|
+
username: string;
|
|
819
|
+
tty: string; // e.g. "pts/0"
|
|
820
|
+
remoteAddress: string; // Client IP or label
|
|
821
|
+
startedAt: string; // ISO-8601
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
#### `VfsSnapshot`
|
|
826
|
+
|
|
827
|
+
```typescript
|
|
828
|
+
interface VfsSnapshot {
|
|
829
|
+
root: VfsSnapshotDirectoryNode;
|
|
830
|
+
}
|
|
831
|
+
// VfsSnapshotNode = VfsSnapshotFileNode | VfsSnapshotDirectoryNode
|
|
832
|
+
// File nodes store content as base64 in contentBase64.
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
#### `ShellModule`
|
|
836
|
+
|
|
837
|
+
Contract for custom command plugins:
|
|
838
|
+
|
|
839
|
+
```typescript
|
|
840
|
+
interface ShellModule {
|
|
841
|
+
name: string;
|
|
842
|
+
params: string[];
|
|
843
|
+
aliases?: string[];
|
|
844
|
+
run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
interface CommandContext {
|
|
848
|
+
authUser: string;
|
|
849
|
+
hostname: string;
|
|
850
|
+
activeSessions: VirtualActiveSession[];
|
|
851
|
+
rawInput: string;
|
|
852
|
+
mode: "shell" | "exec";
|
|
853
|
+
args: string[];
|
|
854
|
+
stdin?: string;
|
|
855
|
+
cwd: string;
|
|
856
|
+
shell: VirtualShell;
|
|
1232
857
|
}
|
|
1233
858
|
```
|
|
1234
859
|
|
|
@@ -1238,72 +863,52 @@ interface VirtualActiveSession {
|
|
|
1238
863
|
|
|
1239
864
|
### Example 1: Basic SSH Server
|
|
1240
865
|
|
|
1241
|
-
Minimal server startup that accepts SSH connections:
|
|
1242
|
-
|
|
1243
866
|
```typescript
|
|
1244
867
|
import { VirtualSshServer } from "typescript-virtual-container";
|
|
1245
868
|
|
|
1246
|
-
const ssh = new VirtualSshServer({
|
|
1247
|
-
port: 2222,
|
|
1248
|
-
hostname: "lab-environment"
|
|
1249
|
-
});
|
|
1250
|
-
|
|
869
|
+
const ssh = new VirtualSshServer({ port: 2222, hostname: "lab-environment" });
|
|
1251
870
|
await ssh.start();
|
|
1252
|
-
console.log("SSH server ready. Connect
|
|
871
|
+
console.log("SSH server ready. Connect: ssh root@localhost -p 2222");
|
|
1253
872
|
|
|
1254
|
-
|
|
1255
|
-
process.on("SIGINT", () => {
|
|
1256
|
-
ssh.stop();
|
|
1257
|
-
process.exit(0);
|
|
1258
|
-
});
|
|
873
|
+
process.on("SIGINT", () => { ssh.stop(); process.exit(0); });
|
|
1259
874
|
```
|
|
1260
875
|
|
|
1261
|
-
**External SSH connection:**
|
|
1262
|
-
|
|
1263
876
|
```bash
|
|
1264
877
|
ssh root@localhost -p 2222
|
|
1265
|
-
# Password: root
|
|
1266
878
|
# $ whoami
|
|
1267
879
|
# root
|
|
1268
|
-
# $
|
|
1269
880
|
```
|
|
1270
881
|
|
|
1271
882
|
---
|
|
1272
883
|
|
|
1273
884
|
### Example 2: Programmatic File Operations
|
|
1274
885
|
|
|
1275
|
-
Create, read, modify files without SSH:
|
|
1276
|
-
|
|
1277
886
|
```typescript
|
|
1278
|
-
import {
|
|
887
|
+
import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
1279
888
|
|
|
1280
|
-
const shell
|
|
1281
|
-
const ssh
|
|
889
|
+
const shell = new VirtualShell("typescript-vm");
|
|
890
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
1282
891
|
await ssh.start();
|
|
1283
892
|
|
|
1284
893
|
const client = new SshClient(shell, "root");
|
|
1285
894
|
|
|
1286
|
-
// Create structure
|
|
1287
895
|
await client.mkdir("/app/config", true);
|
|
1288
896
|
await client.mkdir("/app/logs", true);
|
|
1289
897
|
|
|
1290
|
-
// Write config
|
|
1291
898
|
await client.writeFile("/app/config/settings.json", JSON.stringify({
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
899
|
+
environment: "dev",
|
|
900
|
+
port: 8080,
|
|
901
|
+
debug: true,
|
|
1295
902
|
}, null, 2));
|
|
1296
903
|
|
|
1297
|
-
// Read it back
|
|
1298
904
|
const result = await client.readFile("/app/config/settings.json");
|
|
1299
905
|
console.log("Config:", result.stdout);
|
|
1300
906
|
|
|
1301
|
-
// List directory
|
|
1302
907
|
const list = await client.ls("/app");
|
|
1303
908
|
console.log(list.stdout);
|
|
1304
909
|
|
|
1305
|
-
|
|
1306
|
-
console.log(
|
|
910
|
+
const tree = await client.tree("/app");
|
|
911
|
+
console.log(tree.stdout);
|
|
1307
912
|
|
|
1308
913
|
ssh.stop();
|
|
1309
914
|
```
|
|
@@ -1312,360 +917,340 @@ ssh.stop();
|
|
|
1312
917
|
|
|
1313
918
|
### Example 3: Multi-User Environment
|
|
1314
919
|
|
|
1315
|
-
Create users, manage permissions, session tracking:
|
|
1316
|
-
|
|
1317
920
|
```typescript
|
|
1318
|
-
import {
|
|
921
|
+
import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
1319
922
|
|
|
1320
923
|
const shell = new VirtualShell("typescript-vm");
|
|
1321
|
-
const ssh
|
|
924
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
1322
925
|
await ssh.start();
|
|
1323
926
|
|
|
1324
927
|
const users = ssh.getUsers()!;
|
|
1325
928
|
|
|
1326
|
-
// Create users
|
|
1327
929
|
await users.addUser("alice", "alice123");
|
|
1328
930
|
await users.addUser("bob", "bob456");
|
|
1329
|
-
console.log("Created users: alice, bob");
|
|
1330
931
|
|
|
1331
|
-
//
|
|
932
|
+
// Alice has sudo; Bob does not
|
|
1332
933
|
await users.removeSudoer("bob");
|
|
1333
|
-
await users.addSudoer("alice");
|
|
1334
934
|
|
|
1335
|
-
//
|
|
935
|
+
// Set a 5 MB quota for bob
|
|
936
|
+
await users.setQuotaBytes("bob", 5 * 1024 * 1024);
|
|
937
|
+
|
|
1336
938
|
const alice = new SshClient(shell, "alice");
|
|
1337
939
|
await alice.writeFile("/etc/important.conf", "secret=yes");
|
|
1338
940
|
|
|
1339
|
-
// Bob: Regular user
|
|
1340
941
|
const bob = new SshClient(shell, "bob");
|
|
1341
942
|
const result = await bob.cat("/etc/important.conf");
|
|
1342
|
-
console.log("Bob read file:", result.stderr);
|
|
943
|
+
console.log("Bob read file:", result.stderr); // permission denied
|
|
1343
944
|
|
|
1344
945
|
ssh.stop();
|
|
1345
946
|
```
|
|
1346
947
|
|
|
1347
948
|
---
|
|
1348
949
|
|
|
1349
|
-
### Example 4: Persistent State
|
|
950
|
+
### Example 4: Persistent State across Restarts
|
|
1350
951
|
|
|
1351
|
-
|
|
952
|
+
#### Memory mode (manual)
|
|
1352
953
|
|
|
1353
954
|
```typescript
|
|
1354
|
-
import {
|
|
955
|
+
import { VirtualFileSystem } from "typescript-virtual-container";
|
|
956
|
+
import { writeFileSync, readFileSync } from "node:fs";
|
|
1355
957
|
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
const
|
|
958
|
+
const vfs = new VirtualFileSystem();
|
|
959
|
+
vfs.writeFile("/data/report.txt", "Baseline data");
|
|
960
|
+
|
|
961
|
+
// Persist
|
|
962
|
+
writeFileSync("snapshot.json", JSON.stringify(vfs.toSnapshot()));
|
|
963
|
+
|
|
964
|
+
// Later, in a new process
|
|
965
|
+
const snapshot = JSON.parse(readFileSync("snapshot.json", "utf8"));
|
|
966
|
+
const restored = VirtualFileSystem.fromSnapshot(snapshot);
|
|
967
|
+
console.log(restored.readFile("/data/report.txt")); // Baseline data
|
|
968
|
+
```
|
|
1364
969
|
|
|
1365
|
-
|
|
1366
|
-
vfs1.writeFile("/data/report.txt", "Baseline data");
|
|
1367
|
-
await vfs1.flushMirror();
|
|
1368
|
-
ssh1.stop();
|
|
970
|
+
#### FS mode (automatic)
|
|
1369
971
|
|
|
1370
|
-
|
|
972
|
+
```typescript
|
|
973
|
+
import { VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
1371
974
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
port: 2223,
|
|
1376
|
-
shell: shell2
|
|
975
|
+
const shell = new VirtualShell("my-vm", undefined, {
|
|
976
|
+
mode: "fs",
|
|
977
|
+
snapshotPath: "./container-data",
|
|
1377
978
|
});
|
|
1378
|
-
await ssh2.start();
|
|
1379
|
-
const vfs2 = ssh2.getVfs()!;
|
|
1380
|
-
await vfs2.restoreMirror();
|
|
1381
979
|
|
|
1382
|
-
const
|
|
1383
|
-
|
|
980
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
981
|
+
await ssh.start();
|
|
982
|
+
// Snapshot is auto-restored on start and auto-written on each flushMirror() call.
|
|
1384
983
|
|
|
1385
|
-
|
|
984
|
+
process.on("SIGTERM", () => { ssh.stop(); process.exit(0); });
|
|
1386
985
|
```
|
|
1387
986
|
|
|
1388
987
|
---
|
|
1389
988
|
|
|
1390
|
-
### Example 5:
|
|
1391
|
-
|
|
1392
|
-
Simulate filesystem changes and verify outcomes:
|
|
989
|
+
### Example 5: Public-key authentication
|
|
1393
990
|
|
|
1394
991
|
```typescript
|
|
1395
|
-
import {
|
|
1396
|
-
|
|
1397
|
-
async function testDeployment() {
|
|
1398
|
-
const shell = new VirtualShell("typescript-vm");
|
|
1399
|
-
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
1400
|
-
await ssh.start();
|
|
1401
|
-
|
|
1402
|
-
const client = new SshClient(shell, "root");
|
|
992
|
+
import { VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
993
|
+
import { readFileSync } from "node:fs";
|
|
1403
994
|
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
await client.writeFile("/srv/app/package.json", '{"name":"myapp"}');
|
|
995
|
+
const shell = new VirtualShell("secure-vm");
|
|
996
|
+
await shell.ensureInitialized();
|
|
1407
997
|
|
|
1408
|
-
|
|
1409
|
-
await client.writeFile("/srv/app/app.js", 'console.log("v2.0");');
|
|
998
|
+
await shell.users.addUser("alice", "fallback-password");
|
|
1410
999
|
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
} else {
|
|
1416
|
-
console.error("✗ Deployment failed");
|
|
1417
|
-
}
|
|
1000
|
+
// Parse from ~/.ssh/id_ed25519.pub
|
|
1001
|
+
const pubLine = readFileSync(`${process.env.HOME}/.ssh/id_ed25519.pub`, "utf8").trim();
|
|
1002
|
+
const [algo, b64] = pubLine.split(" ");
|
|
1003
|
+
shell.users.addAuthorizedKey("alice", algo, Buffer.from(b64, "base64"));
|
|
1418
1004
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
testDeployment().catch(console.error);
|
|
1005
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
1006
|
+
await ssh.start();
|
|
1007
|
+
// ssh -i ~/.ssh/id_ed25519 alice@localhost -p 2222
|
|
1423
1008
|
```
|
|
1424
1009
|
|
|
1425
1010
|
---
|
|
1426
1011
|
|
|
1427
|
-
### Example 6:
|
|
1428
|
-
|
|
1429
|
-
Simulate shell workflows:
|
|
1012
|
+
### Example 6: Rate Limiting
|
|
1430
1013
|
|
|
1431
1014
|
```typescript
|
|
1432
|
-
|
|
1015
|
+
const ssh = new VirtualSshServer({
|
|
1016
|
+
port: 2222,
|
|
1017
|
+
maxAuthAttempts: 3, // lock after 3 consecutive failures
|
|
1018
|
+
lockoutDurationMs: 300_000, // 5-minute lockout
|
|
1019
|
+
});
|
|
1433
1020
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1021
|
+
ssh.on("auth:lockout", ({ ip, until }) => {
|
|
1022
|
+
console.warn(`[SSH] ${ip} locked until ${until}`);
|
|
1023
|
+
});
|
|
1437
1024
|
|
|
1438
|
-
|
|
1025
|
+
ssh.on("auth:failure", ({ username, remoteAddress }) => {
|
|
1026
|
+
console.warn(`[SSH] Failed login: ${username} from ${remoteAddress}`);
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// Manually lift a lockout (e.g. in an admin endpoint)
|
|
1030
|
+
ssh.clearLockout("192.168.1.100");
|
|
1031
|
+
```
|
|
1439
1032
|
|
|
1440
|
-
|
|
1441
|
-
await client.mkdir("/home/user/projects/myapp/src", true);
|
|
1442
|
-
await client.cd("/home/user/projects");
|
|
1033
|
+
---
|
|
1443
1034
|
|
|
1444
|
-
|
|
1035
|
+
### Example 7: CI/CD Automation & Assertions
|
|
1445
1036
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
console.log(client.getCwd()); // "/home/user/projects/myapp/src"
|
|
1037
|
+
```typescript
|
|
1038
|
+
import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
1449
1039
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1040
|
+
async function testDeployment() {
|
|
1041
|
+
const shell = new VirtualShell("typescript-vm");
|
|
1042
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
1043
|
+
await ssh.start();
|
|
1453
1044
|
|
|
1454
|
-
|
|
1455
|
-
const srcFiles = await client.ls();
|
|
1456
|
-
console.log(srcFiles.stdout); // main.ts, utils.ts
|
|
1045
|
+
const client = new SshClient(shell, "root");
|
|
1457
1046
|
|
|
1458
|
-
|
|
1459
|
-
await client.
|
|
1460
|
-
console.log(client.getCwd()); // "/home/user/projects/myapp"
|
|
1047
|
+
await client.mkdir("/srv/app", true);
|
|
1048
|
+
await client.writeFile("/srv/app/package.json", '{"name":"myapp","version":"1.0.0"}');
|
|
1461
1049
|
|
|
1462
|
-
|
|
1463
|
-
console.log(
|
|
1050
|
+
// Simulate deployment
|
|
1051
|
+
await client.writeFile("/srv/app/app.js", 'console.log("v2.0");');
|
|
1464
1052
|
|
|
1465
|
-
|
|
1053
|
+
const appContent = await client.readFile("/srv/app/app.js");
|
|
1054
|
+
if (appContent.stdout.includes("v2.0")) {
|
|
1055
|
+
console.log("✓ Deployment verified");
|
|
1056
|
+
} else {
|
|
1057
|
+
throw new Error("✗ Deployment failed");
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
ssh.stop();
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
testDeployment().catch(console.error);
|
|
1466
1064
|
```
|
|
1467
1065
|
|
|
1468
1066
|
---
|
|
1469
1067
|
|
|
1470
|
-
### Example
|
|
1471
|
-
|
|
1472
|
-
Graceful error handling in programmatic workflows:
|
|
1068
|
+
### Example 8: Snapshot-Based Test Fixtures
|
|
1473
1069
|
|
|
1474
1070
|
```typescript
|
|
1475
|
-
import {
|
|
1071
|
+
import { VirtualFileSystem } from "typescript-virtual-container";
|
|
1072
|
+
import type { VfsSnapshot } from "typescript-virtual-container";
|
|
1476
1073
|
|
|
1477
|
-
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1074
|
+
function buildFixture(): VfsSnapshot {
|
|
1075
|
+
const vfs = new VirtualFileSystem();
|
|
1076
|
+
vfs.mkdir("/app/config");
|
|
1077
|
+
vfs.writeFile("/app/config/settings.json", JSON.stringify({ env: "test" }));
|
|
1078
|
+
vfs.writeFile("/app/README.md", "# My App");
|
|
1079
|
+
return vfs.toSnapshot();
|
|
1080
|
+
}
|
|
1480
1081
|
|
|
1481
|
-
const
|
|
1082
|
+
const FIXTURE = buildFixture();
|
|
1482
1083
|
|
|
1483
|
-
//
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1084
|
+
// Each test gets a clean, isolated VFS from the same fixture
|
|
1085
|
+
test("reads config file", () => {
|
|
1086
|
+
const vfs = VirtualFileSystem.fromSnapshot(FIXTURE);
|
|
1087
|
+
const content = JSON.parse(vfs.readFile("/app/config/settings.json"));
|
|
1088
|
+
expect(content.env).toBe("test");
|
|
1089
|
+
});
|
|
1090
|
+
```
|
|
1488
1091
|
|
|
1489
|
-
|
|
1490
|
-
const cdResult = await client.cd("/invalid/path");
|
|
1491
|
-
if (cdResult.exitCode !== 0) {
|
|
1492
|
-
console.error("Invalid path");
|
|
1493
|
-
}
|
|
1092
|
+
---
|
|
1494
1093
|
|
|
1495
|
-
|
|
1496
|
-
const rmResult = await client.rm("/", true);
|
|
1497
|
-
console.log("Remove root:", rmResult.stderr); // Error
|
|
1094
|
+
### Example 9: Symlinks
|
|
1498
1095
|
|
|
1499
|
-
|
|
1096
|
+
```typescript
|
|
1097
|
+
const vfs = new VirtualFileSystem();
|
|
1098
|
+
vfs.mkdir("/usr/local/bin");
|
|
1099
|
+
vfs.writeFile("/opt/myapp/bin/app", "#!/bin/sh\necho hello");
|
|
1100
|
+
vfs.symlink("/opt/myapp/bin/app", "/usr/local/bin/app");
|
|
1101
|
+
|
|
1102
|
+
console.log(vfs.isSymlink("/usr/local/bin/app")); // true
|
|
1103
|
+
console.log(vfs.resolveSymlink("/usr/local/bin/app")); // /opt/myapp/bin/app
|
|
1500
1104
|
```
|
|
1501
1105
|
|
|
1502
1106
|
---
|
|
1503
1107
|
|
|
1504
|
-
### Example
|
|
1505
|
-
|
|
1506
|
-
Track all system activity, detect anomalies, and maintain security audit logs:
|
|
1108
|
+
### Example 10: Security Auditing with HoneyPot
|
|
1507
1109
|
|
|
1508
1110
|
```typescript
|
|
1509
1111
|
import {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1112
|
+
HoneyPot,
|
|
1113
|
+
SshClient,
|
|
1114
|
+
VirtualShell,
|
|
1115
|
+
VirtualSshServer,
|
|
1514
1116
|
} from "typescript-virtual-container";
|
|
1515
1117
|
|
|
1516
1118
|
const shell = new VirtualShell("typescript-vm");
|
|
1517
|
-
const ssh
|
|
1119
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
1518
1120
|
await ssh.start();
|
|
1519
1121
|
|
|
1520
1122
|
const users = ssh.getUsers()!;
|
|
1521
|
-
const vfs
|
|
1123
|
+
const vfs = ssh.getVfs()!;
|
|
1522
1124
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
honeypot.attach(shell, vfs, users, ssh);
|
|
1125
|
+
const hp = new HoneyPot(5000);
|
|
1126
|
+
hp.attach(shell, vfs, users, ssh);
|
|
1526
1127
|
|
|
1527
|
-
// Create users
|
|
1528
1128
|
await users.addUser("alice", "alice123");
|
|
1529
1129
|
await users.addUser("bob", "bob456");
|
|
1530
1130
|
|
|
1531
|
-
// Simulate activity
|
|
1532
1131
|
const alice = new SshClient(shell, "alice");
|
|
1533
1132
|
await alice.mkdir("/home/alice/projects", true);
|
|
1534
1133
|
await alice.writeFile("/home/alice/projects/app.txt", "My application");
|
|
1535
1134
|
await alice.ls("/home/alice/projects");
|
|
1536
1135
|
|
|
1537
1136
|
const bob = new SshClient(shell, "bob");
|
|
1538
|
-
//
|
|
1539
|
-
await bob.
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
console.log(
|
|
1545
|
-
console.log(`
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
console.log(`
|
|
1550
|
-
|
|
1551
|
-
console.log(`Sessions active: ${stats.sessionStarts}`);
|
|
1552
|
-
console.log(`Users created: ${stats.userCreated}`);
|
|
1553
|
-
|
|
1554
|
-
// Get recent events
|
|
1555
|
-
console.log("\n=== Last 5 Events ===");
|
|
1556
|
-
honeypot.getRecent(5).forEach((entry) => {
|
|
1557
|
-
console.log(`[${entry.timestamp}] ${entry.source} -> ${entry.type}`);
|
|
1558
|
-
console.log(` Details: ${JSON.stringify(entry.details, null, 2)}`);
|
|
1559
|
-
});
|
|
1137
|
+
await bob.readFile("/etc/shadow"); // will fail
|
|
1138
|
+
await bob.writeFile("/etc/passwd", ""); // will fail
|
|
1139
|
+
|
|
1140
|
+
// Stats
|
|
1141
|
+
const stats = hp.getStats();
|
|
1142
|
+
console.log(`Auth attempts: ${stats.authAttempts}`);
|
|
1143
|
+
console.log(`Commands run: ${stats.commands}`);
|
|
1144
|
+
console.log(`File writes: ${stats.fileWrites}`);
|
|
1145
|
+
|
|
1146
|
+
// Recent events
|
|
1147
|
+
hp.getRecent(5).forEach(e =>
|
|
1148
|
+
console.log(`[${e.timestamp}] ${e.source} → ${e.type}`)
|
|
1149
|
+
);
|
|
1560
1150
|
|
|
1561
|
-
//
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
anomalies.forEach((anomaly) => {
|
|
1566
|
-
console.log(
|
|
1567
|
-
`[${anomaly.severity.toUpperCase()}] ${anomaly.type}: ${anomaly.message}`,
|
|
1568
|
-
);
|
|
1569
|
-
});
|
|
1570
|
-
} else {
|
|
1571
|
-
console.log("No anomalies detected");
|
|
1572
|
-
}
|
|
1151
|
+
// Anomaly detection
|
|
1152
|
+
hp.detectAnomalies().forEach(a =>
|
|
1153
|
+
console.log(`[${a.severity.toUpperCase()}] ${a.type}: ${a.message}`)
|
|
1154
|
+
);
|
|
1573
1155
|
|
|
1574
|
-
// Filter
|
|
1575
|
-
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1156
|
+
// Filter by type and source
|
|
1157
|
+
const authFailures = hp.getAuditLog("auth:failure");
|
|
1158
|
+
const sshEvents = hp.getAuditLog(undefined, "SshMimic");
|
|
1159
|
+
console.log(`Auth failures: ${authFailures.length}`);
|
|
1160
|
+
console.log(`SSH events: ${sshEvents.length}`);
|
|
1161
|
+
|
|
1162
|
+
ssh.stop();
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
---
|
|
1166
|
+
|
|
1167
|
+
### Example 11: Error Handling
|
|
1582
1168
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1169
|
+
```typescript
|
|
1170
|
+
import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
1171
|
+
|
|
1172
|
+
const shell = new VirtualShell("typescript-vm");
|
|
1173
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
1174
|
+
await ssh.start();
|
|
1175
|
+
|
|
1176
|
+
const client = new SshClient(shell, "root");
|
|
1177
|
+
|
|
1178
|
+
const r1 = await client.readFile("/etc/nonexistent.conf");
|
|
1179
|
+
if (r1.exitCode !== 0) console.error("Read error:", r1.stderr);
|
|
1587
1180
|
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
console.log(`\nTotal audit entries: ${fullAuditLog.length}`);
|
|
1181
|
+
const r2 = await client.cd("/invalid/path");
|
|
1182
|
+
if (r2.exitCode !== 0) console.error("cd failed");
|
|
1591
1183
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1184
|
+
const r3 = await client.rm("/", true);
|
|
1185
|
+
console.log("Remove root:", r3.stderr); // Cannot remove root directory.
|
|
1594
1186
|
|
|
1595
1187
|
ssh.stop();
|
|
1596
1188
|
```
|
|
1597
1189
|
|
|
1598
|
-
|
|
1190
|
+
---
|
|
1599
1191
|
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
Auth failures: 0
|
|
1612
|
-
Commands executed: 8
|
|
1613
|
-
File writes: 1
|
|
1614
|
-
File reads: 2
|
|
1615
|
-
Sessions active: 2
|
|
1616
|
-
Users created: 2
|
|
1617
|
-
|
|
1618
|
-
=== Last 5 Events ===
|
|
1619
|
-
[2026-04-16T10:30:50.678Z] VirtualShell -> command
|
|
1620
|
-
Details: { command: 'ls /home/alice/projects', user: 'alice', cwd: '/home/alice/projects' }
|
|
1621
|
-
|
|
1622
|
-
=== Security Analysis ===
|
|
1623
|
-
No anomalies detected
|
|
1624
|
-
|
|
1625
|
-
=== All SSH Events ===
|
|
1626
|
-
Total SSH events: 4
|
|
1192
|
+
### Example 12: Concurrent Clients
|
|
1193
|
+
|
|
1194
|
+
```typescript
|
|
1195
|
+
const shell = new VirtualShell("typescript-vm");
|
|
1196
|
+
const client1 = new SshClient(shell, "alice");
|
|
1197
|
+
const client2 = new SshClient(shell, "bob");
|
|
1198
|
+
|
|
1199
|
+
const [r1, r2] = await Promise.all([
|
|
1200
|
+
client1.writeFile("/tmp/alice.txt", "Alice's data"),
|
|
1201
|
+
client2.writeFile("/tmp/bob.txt", "Bob's data"),
|
|
1202
|
+
]);
|
|
1627
1203
|
```
|
|
1628
1204
|
|
|
1629
1205
|
---
|
|
1630
1206
|
|
|
1631
1207
|
## Built-in Commands
|
|
1632
1208
|
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
| Command |
|
|
1636
|
-
|
|
1637
|
-
| `adduser <name> <pass>` | Create user
|
|
1638
|
-
| `cat <path>` |
|
|
1639
|
-
| `cd <path>` | Change directory |
|
|
1640
|
-
| `
|
|
1641
|
-
| `
|
|
1642
|
-
| `
|
|
1643
|
-
| `
|
|
1644
|
-
| `
|
|
1645
|
-
| `
|
|
1646
|
-
| `
|
|
1647
|
-
| `
|
|
1648
|
-
| `
|
|
1649
|
-
| `
|
|
1650
|
-
| `
|
|
1651
|
-
| `
|
|
1652
|
-
| `
|
|
1653
|
-
| `
|
|
1654
|
-
| `
|
|
1655
|
-
| `
|
|
1656
|
-
| `
|
|
1657
|
-
| `
|
|
1658
|
-
| `
|
|
1659
|
-
| `
|
|
1660
|
-
| `
|
|
1661
|
-
| `
|
|
1662
|
-
| `
|
|
1663
|
-
| `
|
|
1664
|
-
| `
|
|
1665
|
-
| `
|
|
1666
|
-
| `
|
|
1667
|
-
|
|
1668
|
-
|
|
1209
|
+
All commands are available in SSH shell mode and via `SshClient.exec()`.
|
|
1210
|
+
|
|
1211
|
+
| Command | Flags | Description |
|
|
1212
|
+
|---------|-------|-------------|
|
|
1213
|
+
| `adduser <name> <pass>` | | Create user (root only) |
|
|
1214
|
+
| `cat <path>` | | Print file contents |
|
|
1215
|
+
| `cd <path>` | | Change directory |
|
|
1216
|
+
| `chmod <mode> <file>` | | Change file permissions (octal) |
|
|
1217
|
+
| `clear` | | Clear terminal screen |
|
|
1218
|
+
| `cp <src> <dest>` | `-r` | Copy file or directory |
|
|
1219
|
+
| `curl <url>` | | Fetch URL (delegates to host binary) |
|
|
1220
|
+
| `deluser <name>` | | Delete user (root only, not root) |
|
|
1221
|
+
| `echo <text...>` | | Print text |
|
|
1222
|
+
| `env` | | List environment variables |
|
|
1223
|
+
| `exit [code]` | | Close session |
|
|
1224
|
+
| `export NAME=VALUE` | | Set/export shell variable |
|
|
1225
|
+
| `find [path]` | `-name <pat>` `-type f\|d` | Search for files |
|
|
1226
|
+
| `grep <pattern> [files]` | `-i` `-v` `-n` `-r` | Search file content |
|
|
1227
|
+
| `head [files]` | `-n <N>` | First N lines (default 10) |
|
|
1228
|
+
| `help` | | List all commands |
|
|
1229
|
+
| `hostname` | | Print hostname |
|
|
1230
|
+
| `htop` | | System monitor (mock) |
|
|
1231
|
+
| `ln <target> <link>` | `-s` | Create hard or symbolic link |
|
|
1232
|
+
| `ls [path]` | `-l` | List directory |
|
|
1233
|
+
| `mkdir <path>` | `-p` | Create directory |
|
|
1234
|
+
| `mv <src> <dest>` | | Move or rename |
|
|
1235
|
+
| `nano <path>` | | Interactive text editor |
|
|
1236
|
+
| `neofetch` | | System summary (mock) |
|
|
1237
|
+
| `passwd [user]` | | Change password |
|
|
1238
|
+
| `pwd` | | Print working directory |
|
|
1239
|
+
| `rm <path>` | `-r` | Remove file or directory |
|
|
1240
|
+
| `set` | | Show shell variables |
|
|
1241
|
+
| `sh <script>` | | Run shell script |
|
|
1242
|
+
| `su [user]` | | Switch user |
|
|
1243
|
+
| `sudo <cmd>` | `-i` | Run as root |
|
|
1244
|
+
| `tail [files]` | `-n <N>` | Last N lines (default 10) |
|
|
1245
|
+
| `touch <path>` | | Create/update file |
|
|
1246
|
+
| `tree [path]` | | ASCII directory tree |
|
|
1247
|
+
| `unset <name>` | | Remove shell variable |
|
|
1248
|
+
| `wc [files]` | `-l` `-w` `-c` | Line/word/byte count |
|
|
1249
|
+
| `wget <url>` | | Download file (delegates to host binary) |
|
|
1250
|
+
| `who` | | List active sessions |
|
|
1251
|
+
| `whoami` | | Print current user |
|
|
1252
|
+
|
|
1253
|
+
Custom commands can be added via `shell.addCommand()`.
|
|
1669
1254
|
|
|
1670
1255
|
---
|
|
1671
1256
|
|
|
@@ -1673,28 +1258,46 @@ Commands can be added via the VirtualShell addCommand() method for custom behavi
|
|
|
1673
1258
|
|
|
1674
1259
|
### Environment Variables
|
|
1675
1260
|
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1261
|
+
| Variable | Default | Description |
|
|
1262
|
+
|----------|---------|-------------|
|
|
1263
|
+
| `SSH_MIMIC_FAST_PASSWORD_HASH` | `""` | Use SHA-256 instead of scrypt (faster, less secure — dev only). Set to `1` or `true`. |
|
|
1264
|
+
| `SSH_MIMIC_AUTO_SUDO_NEW_USERS` | `"true"` | Auto-grant sudo to new users. Set to `0`, `false`, `no`, or `off` to disable. |
|
|
1265
|
+
| `DEV_MODE` | `""` | Enable performance logging. |
|
|
1266
|
+
| `RENDER_PERF` | `""` | Enable render performance logging. |
|
|
1680
1267
|
|
|
1681
1268
|
**Example:**
|
|
1682
1269
|
|
|
1683
1270
|
```bash
|
|
1684
|
-
export
|
|
1271
|
+
export SSH_MIMIC_FAST_PASSWORD_HASH=1
|
|
1685
1272
|
export SSH_MIMIC_AUTO_SUDO_NEW_USERS=false
|
|
1686
|
-
|
|
1273
|
+
node server.js
|
|
1687
1274
|
```
|
|
1688
1275
|
|
|
1689
|
-
### Runtime Options
|
|
1276
|
+
### Runtime Options Summary
|
|
1690
1277
|
|
|
1691
1278
|
```typescript
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1279
|
+
// VirtualShell
|
|
1280
|
+
new VirtualShell(
|
|
1281
|
+
hostname,
|
|
1282
|
+
properties?, // kernel, os, arch strings
|
|
1283
|
+
vfsOptions?, // { mode: "memory"|"fs", snapshotPath?: string }
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
// VirtualSshServer
|
|
1287
|
+
new VirtualSshServer({
|
|
1288
|
+
port,
|
|
1289
|
+
hostname?,
|
|
1290
|
+
shell?,
|
|
1291
|
+
maxAuthAttempts?, // default: 5
|
|
1292
|
+
lockoutDurationMs?, // default: 60_000
|
|
1293
|
+
})
|
|
1294
|
+
|
|
1295
|
+
// VirtualSftpServer
|
|
1296
|
+
new VirtualSftpServer({
|
|
1297
|
+
port,
|
|
1298
|
+
hostname?,
|
|
1299
|
+
shell?, // or: vfs + users separately
|
|
1300
|
+
})
|
|
1698
1301
|
```
|
|
1699
1302
|
|
|
1700
1303
|
---
|
|
@@ -1703,43 +1306,41 @@ const ssh = new VirtualSshServer({
|
|
|
1703
1306
|
|
|
1704
1307
|
### Benchmarking
|
|
1705
1308
|
|
|
1706
|
-
Use the built-in benchmark script
|
|
1309
|
+
Use the built-in benchmark script:
|
|
1707
1310
|
|
|
1708
1311
|
```bash
|
|
1709
1312
|
bun ./benchmark-virtualshell.ts
|
|
1710
1313
|
```
|
|
1711
1314
|
|
|
1712
1315
|
The benchmark reports:
|
|
1713
|
-
|
|
1714
|
-
-
|
|
1715
|
-
- command execution time across all active shells
|
|
1316
|
+
- Shell initialization time by concurrency level
|
|
1317
|
+
- Command execution time across active shells
|
|
1716
1318
|
- RSS memory growth during the run
|
|
1717
1319
|
|
|
1718
|
-
Recent
|
|
1320
|
+
Recent baselines show strong startup behavior up to 100 concurrent shells. The runtime is designed to scale easily to **1000+ parallel environments** for testing and automation workloads.
|
|
1719
1321
|
|
|
1720
1322
|
### Concurrency
|
|
1721
1323
|
|
|
1722
|
-
- SSH server handles multiple concurrent connections
|
|
1723
|
-
-
|
|
1724
|
-
-
|
|
1725
|
-
- Horizontal shell instantiation (`new VirtualShell(...)`) is intended for high-volume scenarios, including large test matrices and multi-tenant simulation batches
|
|
1324
|
+
- SSH server is event-driven and handles multiple concurrent connections.
|
|
1325
|
+
- `SshClient` is sequential per instance — create multiple instances for parallel operations.
|
|
1326
|
+
- Each `VirtualShell` instance is fully independent (separate VFS, users, state).
|
|
1726
1327
|
|
|
1727
|
-
###
|
|
1328
|
+
### Performance Tips
|
|
1728
1329
|
|
|
1729
|
-
- Use
|
|
1730
|
-
- Reuse long-lived shell instances
|
|
1731
|
-
- Keep
|
|
1330
|
+
- Use `SSH_MIMIC_FAST_PASSWORD_HASH=1` in test environments to skip scrypt overhead.
|
|
1331
|
+
- Reuse long-lived shell instances for low-latency command bursts.
|
|
1332
|
+
- Keep `DEV_MODE=1` enabled only during development (adds logging overhead).
|
|
1732
1333
|
|
|
1733
|
-
**
|
|
1334
|
+
**Parallel clients example:**
|
|
1734
1335
|
|
|
1735
1336
|
```typescript
|
|
1736
|
-
const shell
|
|
1337
|
+
const shell = new VirtualShell("typescript-vm");
|
|
1737
1338
|
const client1 = new SshClient(shell, "alice");
|
|
1738
1339
|
const client2 = new SshClient(shell, "bob");
|
|
1739
1340
|
|
|
1740
|
-
const [
|
|
1741
|
-
|
|
1742
|
-
|
|
1341
|
+
const [r1, r2] = await Promise.all([
|
|
1342
|
+
client1.writeFile("/tmp/alice.txt", "..."),
|
|
1343
|
+
client2.writeFile("/tmp/bob.txt", "..."),
|
|
1743
1344
|
]);
|
|
1744
1345
|
```
|
|
1745
1346
|
|
|
@@ -1751,145 +1352,167 @@ Full TypeScript support with exported types:
|
|
|
1751
1352
|
|
|
1752
1353
|
```typescript
|
|
1753
1354
|
import type {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1355
|
+
// Persistence
|
|
1356
|
+
VfsOptions,
|
|
1357
|
+
VfsPersistenceMode,
|
|
1358
|
+
// Filesystem
|
|
1359
|
+
VfsSnapshot,
|
|
1360
|
+
VfsNodeStats,
|
|
1361
|
+
VfsFileNode,
|
|
1362
|
+
VfsDirectoryNode,
|
|
1363
|
+
WriteFileOptions,
|
|
1364
|
+
RemoveOptions,
|
|
1365
|
+
// Commands
|
|
1366
|
+
CommandContext,
|
|
1367
|
+
CommandResult,
|
|
1368
|
+
CommandMode,
|
|
1369
|
+
CommandOutcome,
|
|
1370
|
+
ShellModule,
|
|
1371
|
+
SudoChallenge,
|
|
1372
|
+
NanoEditorSession,
|
|
1373
|
+
// Audit
|
|
1374
|
+
AuditLogEntry,
|
|
1375
|
+
HoneyPotStats,
|
|
1376
|
+
// Streams
|
|
1377
|
+
ShellStream,
|
|
1378
|
+
ExecStream,
|
|
1760
1379
|
} from "typescript-virtual-container";
|
|
1761
|
-
|
|
1762
|
-
async function processResult(r: CommandResult) {
|
|
1763
|
-
if (r.exitCode === 0 && r.stdout) {
|
|
1764
|
-
console.log("Success:", r.stdout);
|
|
1765
|
-
} else if (r.stderr) {
|
|
1766
|
-
console.error("Error:", r.stderr);
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
1380
|
```
|
|
1770
1381
|
|
|
1771
1382
|
---
|
|
1772
1383
|
|
|
1773
1384
|
## FAQ
|
|
1774
1385
|
|
|
1775
|
-
|
|
1386
|
+
**Is this a real container runtime?**
|
|
1387
|
+
No. It emulates SSH sessions, users, and filesystem behavior in a virtual runtime. Ideal for testing, simulations, and automation where full OS isolation is not required.
|
|
1776
1388
|
|
|
1777
|
-
|
|
1389
|
+
**Can I use this in production?**
|
|
1390
|
+
You can use it in production-like automation contexts (sandboxed command runners, test harnesses, training environments, honeypots). It is not a security boundary like a real container/VM. Shell command fidelity is still being expanded.
|
|
1778
1391
|
|
|
1779
|
-
|
|
1392
|
+
**Does the VFS touch the host filesystem?**
|
|
1393
|
+
In the default `"memory"` mode: no, all data lives in memory. In `"fs"` mode, it reads/writes a single JSON file (`vfs-snapshot.json`) inside the configured `snapshotPath` directory. No other host paths are accessed.
|
|
1780
1394
|
|
|
1781
|
-
|
|
1395
|
+
**Does data persist between restarts?**
|
|
1396
|
+
Only if you explicitly use `"fs"` mode or call `toSnapshot()` / `fromSnapshot()` manually. Memory mode is ephemeral.
|
|
1782
1397
|
|
|
1783
|
-
|
|
1398
|
+
**Can I run multiple isolated shells?**
|
|
1399
|
+
Yes. Each `new VirtualShell(...)` creates a completely independent VFS and user manager.
|
|
1784
1400
|
|
|
1785
|
-
|
|
1401
|
+
**Are custom commands shared between shell instances?**
|
|
1402
|
+
No. Custom commands registered with `shell.addCommand()` are instance-local.
|
|
1786
1403
|
|
|
1787
|
-
|
|
1404
|
+
**Is networking fully implemented for curl/wget?**
|
|
1405
|
+
`curl` and `wget` delegate to the host binaries. They are intended for realistic workflows, not full GNU tooling parity.
|
|
1788
1406
|
|
|
1789
|
-
|
|
1407
|
+
**Can I create custom commands?**
|
|
1408
|
+
Yes — use `shell.addCommand()` or implement the `ShellModule` interface directly.
|
|
1790
1409
|
|
|
1791
|
-
|
|
1410
|
+
**Is SFTP fully supported?**
|
|
1411
|
+
Core SFTP operations (open, read, write, stat, mkdir, remove, rename) are implemented. Some optional operations (extended attributes, symlinks) return `OP_UNSUPPORTED`.
|
|
1792
1412
|
|
|
1793
|
-
|
|
1413
|
+
**Can I use this for honeypot deployments?**
|
|
1414
|
+
Yes — that is one of its primary use-cases. Use `HoneyPot.attach()` to capture all activity, configure `maxAuthAttempts` to throttle scanners, and export audit logs on shutdown.
|
|
1794
1415
|
|
|
1795
1416
|
---
|
|
1796
1417
|
|
|
1797
1418
|
## Troubleshooting
|
|
1798
1419
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
```
|
|
1802
|
-
Error: listen EADDRINUSE :::2222
|
|
1803
|
-
```
|
|
1804
|
-
|
|
1805
|
-
**Solution**: Use a different port
|
|
1806
|
-
|
|
1420
|
+
**`Error: listen EADDRINUSE :::2222`**
|
|
1421
|
+
The port is already in use. Use a different port or stop the existing process.
|
|
1807
1422
|
```typescript
|
|
1808
1423
|
const ssh = new VirtualSshServer({ port: 3333 });
|
|
1809
1424
|
```
|
|
1810
1425
|
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
**Solution**:
|
|
1816
|
-
|
|
1817
|
-
```typescript
|
|
1818
|
-
process.env.SSH_MIMIC_ROOT_PASSWORD = "your-password";
|
|
1819
|
-
await ssh.start();
|
|
1820
|
-
```
|
|
1426
|
+
**SSH authentication always fails**
|
|
1427
|
+
- Check the password (root has no password by default — any login is accepted).
|
|
1428
|
+
- If you set a password, verify it with `users.verifyPassword(username, password)`.
|
|
1429
|
+
- Check if the IP is rate-limited: call `ssh.clearLockout(ip)`.
|
|
1821
1430
|
|
|
1822
|
-
|
|
1431
|
+
**Auth always fails with "lockout"**
|
|
1432
|
+
Call `ssh.clearLockout(ip)` or increase `maxAuthAttempts`. In tests, use `maxAuthAttempts: Infinity`.
|
|
1823
1433
|
|
|
1824
|
-
|
|
1434
|
+
**`Error: Too many levels of symbolic links`**
|
|
1435
|
+
A symlink chain exceeds 8 hops. Check for circular links or pass a larger `maxDepth` to `resolveSymlink()`.
|
|
1825
1436
|
|
|
1826
|
-
|
|
1437
|
+
**`Command 'xyz' not found` (exit code 127)**
|
|
1438
|
+
The command is not registered. Register it with `shell.addCommand()` or use `SshClient.exec()` with a handler.
|
|
1827
1439
|
|
|
1440
|
+
**File not found errors**
|
|
1441
|
+
Create the parent directory first:
|
|
1828
1442
|
```typescript
|
|
1829
|
-
const vfs = ssh.getVfs();
|
|
1830
1443
|
vfs.mkdir("/home/alice", 0o755);
|
|
1444
|
+
vfs.writeFile("/home/alice/file.txt", "content");
|
|
1831
1445
|
```
|
|
1832
1446
|
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
**Cause**: `flushMirror()` not called
|
|
1836
|
-
|
|
1837
|
-
**Solution**:
|
|
1838
|
-
|
|
1447
|
+
**`snapshotPath` is required error**
|
|
1448
|
+
You set `mode: "fs"` without providing `snapshotPath`:
|
|
1839
1449
|
```typescript
|
|
1840
|
-
|
|
1450
|
+
new VirtualFileSystem({ mode: "fs", snapshotPath: "./data" });
|
|
1841
1451
|
```
|
|
1842
1452
|
|
|
1843
1453
|
---
|
|
1844
1454
|
|
|
1845
1455
|
## Contributing
|
|
1846
1456
|
|
|
1847
|
-
1. Fork repository
|
|
1848
|
-
2. Create feature branch: `git checkout -b feat/my-feature`
|
|
1849
|
-
3. Make changes and add tests
|
|
1850
|
-
4. Format
|
|
1851
|
-
5. Push and open PR
|
|
1457
|
+
1. Fork the repository.
|
|
1458
|
+
2. Create a feature branch: `git checkout -b feat/my-feature`
|
|
1459
|
+
3. Make changes and add tests.
|
|
1460
|
+
4. Format and lint: `bun format && bun check`
|
|
1461
|
+
5. Push and open a PR.
|
|
1852
1462
|
|
|
1853
|
-
**Code
|
|
1854
|
-
- Biome formatting (opinionated)
|
|
1855
|
-
- Full TypeScript
|
|
1856
|
-
- JSDoc comments on public API
|
|
1857
|
-
- Async/await
|
|
1463
|
+
**Code quality standards:**
|
|
1464
|
+
- Biome formatting (opinionated, enforced by CI)
|
|
1465
|
+
- Full TypeScript — no `any`
|
|
1466
|
+
- JSDoc comments on all public API surface
|
|
1467
|
+
- Async/await throughout — no callbacks
|
|
1468
|
+
- Tests for new commands and VFS behavior
|
|
1858
1469
|
|
|
1859
1470
|
---
|
|
1860
1471
|
|
|
1861
1472
|
## Security
|
|
1862
1473
|
|
|
1863
|
-
- Passwords are hashed with `scrypt`
|
|
1864
|
-
- Root account
|
|
1865
|
-
- Sudo privileges are explicit and
|
|
1866
|
-
-
|
|
1867
|
-
-
|
|
1868
|
-
-
|
|
1474
|
+
- Passwords are hashed with `scrypt` by default (N=32768, r=8, p=1), with a random per-user salt.
|
|
1475
|
+
- Root account always exists and cannot be deleted.
|
|
1476
|
+
- Sudo privileges are explicit and stored in the VFS under `/virtual-env-js/.auth/sudoers`.
|
|
1477
|
+
- Per-IP rate limiting prevents automated brute-force attacks on the SSH server.
|
|
1478
|
+
- This project does **not** provide kernel-level or process-level isolation.
|
|
1479
|
+
- Do **not** expose a running instance to the public internet without understanding the risks — the virtual shell allows arbitrary command execution within the virtual environment.
|
|
1869
1480
|
|
|
1870
|
-
If you discover a vulnerability, avoid public disclosure in
|
|
1481
|
+
If you discover a vulnerability, avoid public disclosure in GitHub Issues. Contact maintainers privately first — see `SECURITY.md`.
|
|
1871
1482
|
|
|
1872
1483
|
---
|
|
1873
1484
|
|
|
1874
1485
|
## Support
|
|
1875
1486
|
|
|
1876
1487
|
- Open an issue for bugs, regressions, or feature requests.
|
|
1877
|
-
- Include Node/Bun version, package version, and a minimal reproduction.
|
|
1878
|
-
- For API questions, include the exact
|
|
1488
|
+
- Include your Node.js/Bun version, package version, and a minimal reproduction.
|
|
1489
|
+
- For API questions, include the exact call sequence plus expected vs. actual behavior.
|
|
1879
1490
|
|
|
1880
1491
|
---
|
|
1881
1492
|
|
|
1882
1493
|
## License
|
|
1883
1494
|
|
|
1884
|
-
MIT
|
|
1495
|
+
MIT — see [LICENSE](./LICENSE).
|
|
1885
1496
|
|
|
1886
1497
|
---
|
|
1887
1498
|
|
|
1888
1499
|
## Roadmap
|
|
1889
1500
|
|
|
1890
1501
|
- [x] Custom command plugin API
|
|
1891
|
-
- [x] Optional per-user quotas
|
|
1892
|
-
- [x] Improved shell compatibility
|
|
1893
|
-
- [
|
|
1502
|
+
- [x] Optional per-user storage quotas
|
|
1503
|
+
- [x] Improved shell compatibility (pipelines, redirections)
|
|
1504
|
+
- [x] Pure in-memory VFS with snapshot import/export
|
|
1505
|
+
- [x] Symlinks (`ln -s`, `isSymlink`, `resolveSymlink`)
|
|
1506
|
+
- [x] SSH public-key authentication
|
|
1507
|
+
- [x] Per-IP rate limiting and lockout
|
|
1508
|
+
- [x] New commands: `cp`, `mv`, `ln`, `find`, `wc`, `head`, `tail`, `chmod`
|
|
1894
1509
|
- [x] Structured event hooks (session open/close, file write, sudo challenge)
|
|
1895
|
-
- [ ]
|
|
1510
|
+
- [ ] Snapshot diff tooling for test assertions
|
|
1511
|
+
- [ ] WebSocket-based remote shell client (experimental)
|
|
1512
|
+
- [ ] Shell scripting: `if`/`for`/`while` constructs
|
|
1513
|
+
|
|
1514
|
+
---
|
|
1515
|
+
|
|
1516
|
+
## Changelog
|
|
1517
|
+
|
|
1518
|
+
See [CHANGELOG.md](./CHANGELOG.md).
|