typescript-virtual-container 1.2.3 → 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 +871 -1231
- package/benchmark-results.txt +21 -21
- package/biome.json +9 -0
- package/dist/SSHMimic/index.d.ts +19 -2
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +127 -15
- 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 +5 -23
- package/dist/VirtualUserManager/index.d.ts +41 -3
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +83 -21
- 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/dist/standalone.js +7 -9
- package/package.json +7 -3
- package/scripts/publish-package.sh +70 -0
- package/src/SSHMimic/index.ts +159 -17
- package/src/VirtualFileSystem/index.ts +500 -280
- package/src/VirtualShell/index.ts +5 -33
- package/src/VirtualUserManager/index.ts +92 -26
- 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/src/standalone.ts +12 -9
- package/standalone.js +102 -0
- package/standalone.js.map +7 -0
- 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
275
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
Initializes virtual filesystem, user manager, and starts listening for SSH connections.
|
|
262
|
-
|
|
263
|
-
- **Returns**: Bound port number
|
|
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
|
-
|
|
279
|
+
#### Methods
|
|
272
280
|
|
|
273
|
-
|
|
274
|
-
|
|
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,925 +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
346
|
#### Events
|
|
381
347
|
|
|
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
|
-
```
|
|
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>`
|
|
913
|
-
|
|
914
|
-
Grants sudo privileges to user.
|
|
438
|
+
// Default — pure in-memory, zero disk I/O
|
|
439
|
+
const vfs = new VirtualFileSystem();
|
|
440
|
+
const vfs = new VirtualFileSystem({ mode: "memory" });
|
|
915
441
|
|
|
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
|
-
```
|
|
540
|
+
import { VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
967
541
|
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
```
|
|
678
|
+
import { HoneyPot, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
1098
679
|
|
|
1099
|
-
|
|
680
|
+
const shell = new VirtualShell("honeypot");
|
|
681
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
682
|
+
const hp = new HoneyPot(50_000);
|
|
1100
683
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
```typescript
|
|
1104
|
-
const last50 = honeypot.getRecent(50);
|
|
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
|
+
---
|
|
710
|
+
|
|
711
|
+
### `SshClient` (Programmatic API)
|
|
712
|
+
|
|
713
|
+
Execute shell commands against a `VirtualShell` without SSH protocol overhead. Maintains working-directory state across calls.
|
|
1130
714
|
|
|
1131
|
-
|
|
715
|
+
#### Constructor
|
|
1132
716
|
|
|
1133
717
|
```typescript
|
|
1134
|
-
|
|
718
|
+
new SshClient(shell: VirtualShell, username: string)
|
|
1135
719
|
```
|
|
1136
720
|
|
|
1137
|
-
|
|
721
|
+
No password required — the client authenticates by username only.
|
|
722
|
+
|
|
723
|
+
#### Methods
|
|
724
|
+
|
|
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. |
|
|
743
|
+
|
|
744
|
+
**Example:**
|
|
1138
745
|
|
|
1139
746
|
```typescript
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
}
|
|
1146
|
-
```
|
|
747
|
+
const shell = new VirtualShell("typescript-vm");
|
|
748
|
+
const client = new SshClient(shell, "alice");
|
|
749
|
+
|
|
750
|
+
await client.mkdir("/home/alice/projects", true);
|
|
751
|
+
await client.cd("/home/alice/projects");
|
|
1147
752
|
|
|
1148
|
-
|
|
753
|
+
console.log(client.getCwd()); // /home/alice/projects
|
|
1149
754
|
|
|
1150
|
-
|
|
755
|
+
await client.writeFile("notes.txt", "Work in progress");
|
|
756
|
+
const list = await client.ls();
|
|
757
|
+
console.log(list.stdout); // notes.txt
|
|
758
|
+
|
|
759
|
+
const read = await client.readFile("notes.txt");
|
|
760
|
+
console.log(read.stdout); // Work in progress
|
|
761
|
+
```
|
|
1151
762
|
|
|
1152
763
|
---
|
|
1153
764
|
|
|
1154
765
|
### Key Types
|
|
1155
766
|
|
|
1156
|
-
#### CommandResult
|
|
767
|
+
#### `CommandResult`
|
|
1157
768
|
|
|
1158
|
-
|
|
769
|
+
Returned by all command executions (shell or programmatic).
|
|
1159
770
|
|
|
1160
771
|
```typescript
|
|
1161
772
|
interface CommandResult {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
|
1172
783
|
}
|
|
1173
784
|
```
|
|
1174
785
|
|
|
1175
|
-
#### VfsNodeStats
|
|
1176
|
-
|
|
1177
|
-
File/directory metadata.
|
|
786
|
+
#### `VfsNodeStats`
|
|
1178
787
|
|
|
1179
788
|
```typescript
|
|
1180
789
|
type VfsNodeStats = VfsFileNode | VfsDirectoryNode;
|
|
1181
790
|
|
|
1182
791
|
interface VfsFileNode {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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;
|
|
1191
800
|
}
|
|
1192
801
|
|
|
1193
802
|
interface VfsDirectoryNode {
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
803
|
+
type: "directory";
|
|
804
|
+
name: string;
|
|
805
|
+
path: string;
|
|
806
|
+
mode: number;
|
|
807
|
+
childrenCount: number;
|
|
808
|
+
createdAt: Date;
|
|
809
|
+
updatedAt: Date;
|
|
1201
810
|
}
|
|
1202
811
|
```
|
|
1203
812
|
|
|
1204
|
-
#### VirtualActiveSession
|
|
1205
|
-
|
|
1206
|
-
Active SSH/programmatic session descriptor.
|
|
813
|
+
#### `VirtualActiveSession`
|
|
1207
814
|
|
|
1208
815
|
```typescript
|
|
1209
816
|
interface VirtualActiveSession {
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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;
|
|
1215
857
|
}
|
|
1216
858
|
```
|
|
1217
859
|
|
|
@@ -1221,72 +863,52 @@ interface VirtualActiveSession {
|
|
|
1221
863
|
|
|
1222
864
|
### Example 1: Basic SSH Server
|
|
1223
865
|
|
|
1224
|
-
Minimal server startup that accepts SSH connections:
|
|
1225
|
-
|
|
1226
866
|
```typescript
|
|
1227
867
|
import { VirtualSshServer } from "typescript-virtual-container";
|
|
1228
868
|
|
|
1229
|
-
const ssh = new VirtualSshServer({
|
|
1230
|
-
port: 2222,
|
|
1231
|
-
hostname: "lab-environment"
|
|
1232
|
-
});
|
|
1233
|
-
|
|
869
|
+
const ssh = new VirtualSshServer({ port: 2222, hostname: "lab-environment" });
|
|
1234
870
|
await ssh.start();
|
|
1235
|
-
console.log("SSH server ready. Connect
|
|
871
|
+
console.log("SSH server ready. Connect: ssh root@localhost -p 2222");
|
|
1236
872
|
|
|
1237
|
-
|
|
1238
|
-
process.on("SIGINT", () => {
|
|
1239
|
-
ssh.stop();
|
|
1240
|
-
process.exit(0);
|
|
1241
|
-
});
|
|
873
|
+
process.on("SIGINT", () => { ssh.stop(); process.exit(0); });
|
|
1242
874
|
```
|
|
1243
875
|
|
|
1244
|
-
**External SSH connection:**
|
|
1245
|
-
|
|
1246
876
|
```bash
|
|
1247
877
|
ssh root@localhost -p 2222
|
|
1248
|
-
# Password: root
|
|
1249
878
|
# $ whoami
|
|
1250
879
|
# root
|
|
1251
|
-
# $
|
|
1252
880
|
```
|
|
1253
881
|
|
|
1254
882
|
---
|
|
1255
883
|
|
|
1256
884
|
### Example 2: Programmatic File Operations
|
|
1257
885
|
|
|
1258
|
-
Create, read, modify files without SSH:
|
|
1259
|
-
|
|
1260
886
|
```typescript
|
|
1261
|
-
import {
|
|
887
|
+
import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
1262
888
|
|
|
1263
|
-
const shell
|
|
1264
|
-
const ssh
|
|
889
|
+
const shell = new VirtualShell("typescript-vm");
|
|
890
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
1265
891
|
await ssh.start();
|
|
1266
892
|
|
|
1267
893
|
const client = new SshClient(shell, "root");
|
|
1268
894
|
|
|
1269
|
-
// Create structure
|
|
1270
895
|
await client.mkdir("/app/config", true);
|
|
1271
896
|
await client.mkdir("/app/logs", true);
|
|
1272
897
|
|
|
1273
|
-
// Write config
|
|
1274
898
|
await client.writeFile("/app/config/settings.json", JSON.stringify({
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
899
|
+
environment: "dev",
|
|
900
|
+
port: 8080,
|
|
901
|
+
debug: true,
|
|
1278
902
|
}, null, 2));
|
|
1279
903
|
|
|
1280
|
-
// Read it back
|
|
1281
904
|
const result = await client.readFile("/app/config/settings.json");
|
|
1282
905
|
console.log("Config:", result.stdout);
|
|
1283
906
|
|
|
1284
|
-
// List directory
|
|
1285
907
|
const list = await client.ls("/app");
|
|
1286
908
|
console.log(list.stdout);
|
|
1287
909
|
|
|
1288
|
-
|
|
1289
|
-
console.log(
|
|
910
|
+
const tree = await client.tree("/app");
|
|
911
|
+
console.log(tree.stdout);
|
|
1290
912
|
|
|
1291
913
|
ssh.stop();
|
|
1292
914
|
```
|
|
@@ -1295,360 +917,340 @@ ssh.stop();
|
|
|
1295
917
|
|
|
1296
918
|
### Example 3: Multi-User Environment
|
|
1297
919
|
|
|
1298
|
-
Create users, manage permissions, session tracking:
|
|
1299
|
-
|
|
1300
920
|
```typescript
|
|
1301
|
-
import {
|
|
921
|
+
import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
1302
922
|
|
|
1303
923
|
const shell = new VirtualShell("typescript-vm");
|
|
1304
|
-
const ssh
|
|
924
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
1305
925
|
await ssh.start();
|
|
1306
926
|
|
|
1307
927
|
const users = ssh.getUsers()!;
|
|
1308
928
|
|
|
1309
|
-
// Create users
|
|
1310
929
|
await users.addUser("alice", "alice123");
|
|
1311
930
|
await users.addUser("bob", "bob456");
|
|
1312
|
-
console.log("Created users: alice, bob");
|
|
1313
931
|
|
|
1314
|
-
//
|
|
932
|
+
// Alice has sudo; Bob does not
|
|
1315
933
|
await users.removeSudoer("bob");
|
|
1316
|
-
await users.addSudoer("alice");
|
|
1317
934
|
|
|
1318
|
-
//
|
|
935
|
+
// Set a 5 MB quota for bob
|
|
936
|
+
await users.setQuotaBytes("bob", 5 * 1024 * 1024);
|
|
937
|
+
|
|
1319
938
|
const alice = new SshClient(shell, "alice");
|
|
1320
939
|
await alice.writeFile("/etc/important.conf", "secret=yes");
|
|
1321
940
|
|
|
1322
|
-
// Bob: Regular user
|
|
1323
941
|
const bob = new SshClient(shell, "bob");
|
|
1324
942
|
const result = await bob.cat("/etc/important.conf");
|
|
1325
|
-
console.log("Bob read file:", result.stderr);
|
|
943
|
+
console.log("Bob read file:", result.stderr); // permission denied
|
|
1326
944
|
|
|
1327
945
|
ssh.stop();
|
|
1328
946
|
```
|
|
1329
947
|
|
|
1330
948
|
---
|
|
1331
949
|
|
|
1332
|
-
### Example 4: Persistent State
|
|
950
|
+
### Example 4: Persistent State across Restarts
|
|
1333
951
|
|
|
1334
|
-
|
|
952
|
+
#### Memory mode (manual)
|
|
1335
953
|
|
|
1336
954
|
```typescript
|
|
1337
|
-
import {
|
|
955
|
+
import { VirtualFileSystem } from "typescript-virtual-container";
|
|
956
|
+
import { writeFileSync, readFileSync } from "node:fs";
|
|
1338
957
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
});
|
|
1345
|
-
await ssh1.start();
|
|
1346
|
-
const vfs1 = ssh1.getVfs()!;
|
|
958
|
+
const vfs = new VirtualFileSystem();
|
|
959
|
+
vfs.writeFile("/data/report.txt", "Baseline data");
|
|
960
|
+
|
|
961
|
+
// Persist
|
|
962
|
+
writeFileSync("snapshot.json", JSON.stringify(vfs.toSnapshot()));
|
|
1347
963
|
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
+
```
|
|
969
|
+
|
|
970
|
+
#### FS mode (automatic)
|
|
1352
971
|
|
|
1353
|
-
|
|
972
|
+
```typescript
|
|
973
|
+
import { VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
1354
974
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
port: 2223,
|
|
1359
|
-
shell: shell2
|
|
975
|
+
const shell = new VirtualShell("my-vm", undefined, {
|
|
976
|
+
mode: "fs",
|
|
977
|
+
snapshotPath: "./container-data",
|
|
1360
978
|
});
|
|
1361
|
-
await ssh2.start();
|
|
1362
|
-
const vfs2 = ssh2.getVfs()!;
|
|
1363
|
-
await vfs2.restoreMirror();
|
|
1364
979
|
|
|
1365
|
-
const
|
|
1366
|
-
|
|
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.
|
|
1367
983
|
|
|
1368
|
-
|
|
984
|
+
process.on("SIGTERM", () => { ssh.stop(); process.exit(0); });
|
|
1369
985
|
```
|
|
1370
986
|
|
|
1371
987
|
---
|
|
1372
988
|
|
|
1373
|
-
### Example 5:
|
|
1374
|
-
|
|
1375
|
-
Simulate filesystem changes and verify outcomes:
|
|
989
|
+
### Example 5: Public-key authentication
|
|
1376
990
|
|
|
1377
991
|
```typescript
|
|
1378
|
-
import {
|
|
1379
|
-
|
|
1380
|
-
async function testDeployment() {
|
|
1381
|
-
const shell = new VirtualShell("typescript-vm");
|
|
1382
|
-
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
1383
|
-
await ssh.start();
|
|
992
|
+
import { VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
993
|
+
import { readFileSync } from "node:fs";
|
|
1384
994
|
|
|
1385
|
-
|
|
995
|
+
const shell = new VirtualShell("secure-vm");
|
|
996
|
+
await shell.ensureInitialized();
|
|
1386
997
|
|
|
1387
|
-
|
|
1388
|
-
await client.mkdir("/srv/app", true);
|
|
1389
|
-
await client.writeFile("/srv/app/package.json", '{"name":"myapp"}');
|
|
998
|
+
await shell.users.addUser("alice", "fallback-password");
|
|
1390
999
|
|
|
1391
|
-
|
|
1392
|
-
|
|
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"));
|
|
1393
1004
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
console.log("✓ Deployment verified");
|
|
1398
|
-
} else {
|
|
1399
|
-
console.error("✗ Deployment failed");
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
ssh.stop();
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
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
|
|
1406
1008
|
```
|
|
1407
1009
|
|
|
1408
1010
|
---
|
|
1409
1011
|
|
|
1410
|
-
### Example 6:
|
|
1411
|
-
|
|
1412
|
-
Simulate shell workflows:
|
|
1012
|
+
### Example 6: Rate Limiting
|
|
1413
1013
|
|
|
1414
1014
|
```typescript
|
|
1415
|
-
|
|
1015
|
+
const ssh = new VirtualSshServer({
|
|
1016
|
+
port: 2222,
|
|
1017
|
+
maxAuthAttempts: 3, // lock after 3 consecutive failures
|
|
1018
|
+
lockoutDurationMs: 300_000, // 5-minute lockout
|
|
1019
|
+
});
|
|
1416
1020
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1021
|
+
ssh.on("auth:lockout", ({ ip, until }) => {
|
|
1022
|
+
console.warn(`[SSH] ${ip} locked until ${until}`);
|
|
1023
|
+
});
|
|
1420
1024
|
|
|
1421
|
-
|
|
1025
|
+
ssh.on("auth:failure", ({ username, remoteAddress }) => {
|
|
1026
|
+
console.warn(`[SSH] Failed login: ${username} from ${remoteAddress}`);
|
|
1027
|
+
});
|
|
1422
1028
|
|
|
1423
|
-
//
|
|
1424
|
-
|
|
1425
|
-
|
|
1029
|
+
// Manually lift a lockout (e.g. in an admin endpoint)
|
|
1030
|
+
ssh.clearLockout("192.168.1.100");
|
|
1031
|
+
```
|
|
1426
1032
|
|
|
1427
|
-
|
|
1033
|
+
---
|
|
1428
1034
|
|
|
1429
|
-
|
|
1430
|
-
await client.cd("myapp/src");
|
|
1431
|
-
console.log(client.getCwd()); // "/home/user/projects/myapp/src"
|
|
1035
|
+
### Example 7: CI/CD Automation & Assertions
|
|
1432
1036
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
await client.writeFile("utils.ts", "export function util() {}");
|
|
1037
|
+
```typescript
|
|
1038
|
+
import { SshClient, VirtualShell, VirtualSshServer } from "typescript-virtual-container";
|
|
1436
1039
|
|
|
1437
|
-
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1040
|
+
async function testDeployment() {
|
|
1041
|
+
const shell = new VirtualShell("typescript-vm");
|
|
1042
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
1043
|
+
await ssh.start();
|
|
1440
1044
|
|
|
1441
|
-
|
|
1442
|
-
await client.cd("..");
|
|
1443
|
-
console.log(client.getCwd()); // "/home/user/projects/myapp"
|
|
1045
|
+
const client = new SshClient(shell, "root");
|
|
1444
1046
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1047
|
+
await client.mkdir("/srv/app", true);
|
|
1048
|
+
await client.writeFile("/srv/app/package.json", '{"name":"myapp","version":"1.0.0"}');
|
|
1447
1049
|
|
|
1448
|
-
|
|
1050
|
+
// Simulate deployment
|
|
1051
|
+
await client.writeFile("/srv/app/app.js", 'console.log("v2.0");');
|
|
1052
|
+
|
|
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);
|
|
1449
1064
|
```
|
|
1450
1065
|
|
|
1451
1066
|
---
|
|
1452
1067
|
|
|
1453
|
-
### Example
|
|
1454
|
-
|
|
1455
|
-
Graceful error handling in programmatic workflows:
|
|
1068
|
+
### Example 8: Snapshot-Based Test Fixtures
|
|
1456
1069
|
|
|
1457
1070
|
```typescript
|
|
1458
|
-
import {
|
|
1071
|
+
import { VirtualFileSystem } from "typescript-virtual-container";
|
|
1072
|
+
import type { VfsSnapshot } from "typescript-virtual-container";
|
|
1459
1073
|
|
|
1460
|
-
|
|
1461
|
-
const
|
|
1462
|
-
|
|
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
|
+
}
|
|
1463
1081
|
|
|
1464
|
-
const
|
|
1082
|
+
const FIXTURE = buildFixture();
|
|
1465
1083
|
|
|
1466
|
-
//
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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
|
+
```
|
|
1471
1091
|
|
|
1472
|
-
|
|
1473
|
-
const cdResult = await client.cd("/invalid/path");
|
|
1474
|
-
if (cdResult.exitCode !== 0) {
|
|
1475
|
-
console.error("Invalid path");
|
|
1476
|
-
}
|
|
1092
|
+
---
|
|
1477
1093
|
|
|
1478
|
-
|
|
1479
|
-
const rmResult = await client.rm("/", true);
|
|
1480
|
-
console.log("Remove root:", rmResult.stderr); // Error
|
|
1094
|
+
### Example 9: Symlinks
|
|
1481
1095
|
|
|
1482
|
-
|
|
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
|
|
1483
1104
|
```
|
|
1484
1105
|
|
|
1485
1106
|
---
|
|
1486
1107
|
|
|
1487
|
-
### Example
|
|
1488
|
-
|
|
1489
|
-
Track all system activity, detect anomalies, and maintain security audit logs:
|
|
1108
|
+
### Example 10: Security Auditing with HoneyPot
|
|
1490
1109
|
|
|
1491
1110
|
```typescript
|
|
1492
1111
|
import {
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1112
|
+
HoneyPot,
|
|
1113
|
+
SshClient,
|
|
1114
|
+
VirtualShell,
|
|
1115
|
+
VirtualSshServer,
|
|
1497
1116
|
} from "typescript-virtual-container";
|
|
1498
1117
|
|
|
1499
1118
|
const shell = new VirtualShell("typescript-vm");
|
|
1500
|
-
const ssh
|
|
1119
|
+
const ssh = new VirtualSshServer({ port: 2222, shell });
|
|
1501
1120
|
await ssh.start();
|
|
1502
1121
|
|
|
1503
1122
|
const users = ssh.getUsers()!;
|
|
1504
|
-
const vfs
|
|
1123
|
+
const vfs = ssh.getVfs()!;
|
|
1505
1124
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
honeypot.attach(shell, vfs, users, ssh);
|
|
1125
|
+
const hp = new HoneyPot(5000);
|
|
1126
|
+
hp.attach(shell, vfs, users, ssh);
|
|
1509
1127
|
|
|
1510
|
-
// Create users
|
|
1511
1128
|
await users.addUser("alice", "alice123");
|
|
1512
1129
|
await users.addUser("bob", "bob456");
|
|
1513
1130
|
|
|
1514
|
-
// Simulate activity
|
|
1515
1131
|
const alice = new SshClient(shell, "alice");
|
|
1516
1132
|
await alice.mkdir("/home/alice/projects", true);
|
|
1517
1133
|
await alice.writeFile("/home/alice/projects/app.txt", "My application");
|
|
1518
1134
|
await alice.ls("/home/alice/projects");
|
|
1519
1135
|
|
|
1520
1136
|
const bob = new SshClient(shell, "bob");
|
|
1521
|
-
//
|
|
1522
|
-
await bob.
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
console.log(
|
|
1528
|
-
console.log(`
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
console.log(`
|
|
1533
|
-
|
|
1534
|
-
console.log(`Sessions active: ${stats.sessionStarts}`);
|
|
1535
|
-
console.log(`Users created: ${stats.userCreated}`);
|
|
1536
|
-
|
|
1537
|
-
// Get recent events
|
|
1538
|
-
console.log("\n=== Last 5 Events ===");
|
|
1539
|
-
honeypot.getRecent(5).forEach((entry) => {
|
|
1540
|
-
console.log(`[${entry.timestamp}] ${entry.source} -> ${entry.type}`);
|
|
1541
|
-
console.log(` Details: ${JSON.stringify(entry.details, null, 2)}`);
|
|
1542
|
-
});
|
|
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
|
+
);
|
|
1543
1150
|
|
|
1544
|
-
//
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
anomalies.forEach((anomaly) => {
|
|
1549
|
-
console.log(
|
|
1550
|
-
`[${anomaly.severity.toUpperCase()}] ${anomaly.type}: ${anomaly.message}`,
|
|
1551
|
-
);
|
|
1552
|
-
});
|
|
1553
|
-
} else {
|
|
1554
|
-
console.log("No anomalies detected");
|
|
1555
|
-
}
|
|
1151
|
+
// Anomaly detection
|
|
1152
|
+
hp.detectAnomalies().forEach(a =>
|
|
1153
|
+
console.log(`[${a.severity.toUpperCase()}] ${a.type}: ${a.message}`)
|
|
1154
|
+
);
|
|
1556
1155
|
|
|
1557
|
-
// Filter
|
|
1558
|
-
|
|
1559
|
-
const
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
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
|
+
---
|
|
1565
1166
|
|
|
1566
|
-
|
|
1567
|
-
console.log("\n=== All SSH Events ===");
|
|
1568
|
-
const sshEvents = honeypot.getAuditLog(undefined, "SshMimic");
|
|
1569
|
-
console.log(` Total SSH events: ${sshEvents.length}`);
|
|
1167
|
+
### Example 11: Error Handling
|
|
1570
1168
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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);
|
|
1574
1180
|
|
|
1575
|
-
|
|
1576
|
-
|
|
1181
|
+
const r2 = await client.cd("/invalid/path");
|
|
1182
|
+
if (r2.exitCode !== 0) console.error("cd failed");
|
|
1183
|
+
|
|
1184
|
+
const r3 = await client.rm("/", true);
|
|
1185
|
+
console.log("Remove root:", r3.stderr); // Cannot remove root directory.
|
|
1577
1186
|
|
|
1578
1187
|
ssh.stop();
|
|
1579
1188
|
```
|
|
1580
1189
|
|
|
1581
|
-
|
|
1190
|
+
---
|
|
1191
|
+
|
|
1192
|
+
### Example 12: Concurrent Clients
|
|
1582
1193
|
|
|
1583
|
-
```
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
[
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
Auth attempts: 2
|
|
1593
|
-
Auth successes: 2
|
|
1594
|
-
Auth failures: 0
|
|
1595
|
-
Commands executed: 8
|
|
1596
|
-
File writes: 1
|
|
1597
|
-
File reads: 2
|
|
1598
|
-
Sessions active: 2
|
|
1599
|
-
Users created: 2
|
|
1600
|
-
|
|
1601
|
-
=== Last 5 Events ===
|
|
1602
|
-
[2026-04-16T10:30:50.678Z] VirtualShell -> command
|
|
1603
|
-
Details: { command: 'ls /home/alice/projects', user: 'alice', cwd: '/home/alice/projects' }
|
|
1604
|
-
|
|
1605
|
-
=== Security Analysis ===
|
|
1606
|
-
No anomalies detected
|
|
1607
|
-
|
|
1608
|
-
=== All SSH Events ===
|
|
1609
|
-
Total SSH events: 4
|
|
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
|
+
]);
|
|
1610
1203
|
```
|
|
1611
1204
|
|
|
1612
1205
|
---
|
|
1613
1206
|
|
|
1614
1207
|
## Built-in Commands
|
|
1615
1208
|
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
| Command |
|
|
1619
|
-
|
|
1620
|
-
| `adduser <name> <pass>` | Create user
|
|
1621
|
-
| `cat <path>` |
|
|
1622
|
-
| `cd <path>` | Change directory |
|
|
1623
|
-
| `
|
|
1624
|
-
| `
|
|
1625
|
-
| `
|
|
1626
|
-
| `
|
|
1627
|
-
| `
|
|
1628
|
-
| `
|
|
1629
|
-
| `
|
|
1630
|
-
| `
|
|
1631
|
-
| `
|
|
1632
|
-
| `
|
|
1633
|
-
| `
|
|
1634
|
-
| `
|
|
1635
|
-
| `
|
|
1636
|
-
| `
|
|
1637
|
-
| `
|
|
1638
|
-
| `
|
|
1639
|
-
| `
|
|
1640
|
-
| `
|
|
1641
|
-
| `
|
|
1642
|
-
| `
|
|
1643
|
-
| `
|
|
1644
|
-
| `
|
|
1645
|
-
| `
|
|
1646
|
-
| `
|
|
1647
|
-
| `
|
|
1648
|
-
| `
|
|
1649
|
-
| `
|
|
1650
|
-
|
|
1651
|
-
|
|
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()`.
|
|
1652
1254
|
|
|
1653
1255
|
---
|
|
1654
1256
|
|
|
@@ -1656,28 +1258,46 @@ Commands can be added via the VirtualShell addCommand() method for custom behavi
|
|
|
1656
1258
|
|
|
1657
1259
|
### Environment Variables
|
|
1658
1260
|
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
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. |
|
|
1662
1267
|
|
|
1663
1268
|
**Example:**
|
|
1664
1269
|
|
|
1665
1270
|
```bash
|
|
1666
|
-
export
|
|
1667
|
-
export SSH_MIMIC_ROOT_PASSWORD=SecurePass123
|
|
1271
|
+
export SSH_MIMIC_FAST_PASSWORD_HASH=1
|
|
1668
1272
|
export SSH_MIMIC_AUTO_SUDO_NEW_USERS=false
|
|
1669
|
-
|
|
1273
|
+
node server.js
|
|
1670
1274
|
```
|
|
1671
1275
|
|
|
1672
|
-
### Runtime Options
|
|
1276
|
+
### Runtime Options Summary
|
|
1673
1277
|
|
|
1674
1278
|
```typescript
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
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
|
+
})
|
|
1681
1301
|
```
|
|
1682
1302
|
|
|
1683
1303
|
---
|
|
@@ -1686,43 +1306,41 @@ const ssh = new VirtualSshServer({
|
|
|
1686
1306
|
|
|
1687
1307
|
### Benchmarking
|
|
1688
1308
|
|
|
1689
|
-
Use the built-in benchmark script
|
|
1309
|
+
Use the built-in benchmark script:
|
|
1690
1310
|
|
|
1691
1311
|
```bash
|
|
1692
1312
|
bun ./benchmark-virtualshell.ts
|
|
1693
1313
|
```
|
|
1694
1314
|
|
|
1695
1315
|
The benchmark reports:
|
|
1696
|
-
|
|
1697
|
-
-
|
|
1698
|
-
- command execution time across all active shells
|
|
1316
|
+
- Shell initialization time by concurrency level
|
|
1317
|
+
- Command execution time across active shells
|
|
1699
1318
|
- RSS memory growth during the run
|
|
1700
1319
|
|
|
1701
|
-
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.
|
|
1702
1321
|
|
|
1703
1322
|
### Concurrency
|
|
1704
1323
|
|
|
1705
|
-
- SSH server handles multiple concurrent connections
|
|
1706
|
-
-
|
|
1707
|
-
-
|
|
1708
|
-
- 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).
|
|
1709
1327
|
|
|
1710
|
-
###
|
|
1328
|
+
### Performance Tips
|
|
1711
1329
|
|
|
1712
|
-
- Use
|
|
1713
|
-
- Reuse long-lived shell instances
|
|
1714
|
-
- 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).
|
|
1715
1333
|
|
|
1716
|
-
**
|
|
1334
|
+
**Parallel clients example:**
|
|
1717
1335
|
|
|
1718
1336
|
```typescript
|
|
1719
|
-
const shell
|
|
1337
|
+
const shell = new VirtualShell("typescript-vm");
|
|
1720
1338
|
const client1 = new SshClient(shell, "alice");
|
|
1721
1339
|
const client2 = new SshClient(shell, "bob");
|
|
1722
1340
|
|
|
1723
|
-
const [
|
|
1724
|
-
|
|
1725
|
-
|
|
1341
|
+
const [r1, r2] = await Promise.all([
|
|
1342
|
+
client1.writeFile("/tmp/alice.txt", "..."),
|
|
1343
|
+
client2.writeFile("/tmp/bob.txt", "..."),
|
|
1726
1344
|
]);
|
|
1727
1345
|
```
|
|
1728
1346
|
|
|
@@ -1734,145 +1352,167 @@ Full TypeScript support with exported types:
|
|
|
1734
1352
|
|
|
1735
1353
|
```typescript
|
|
1736
1354
|
import type {
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
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,
|
|
1743
1379
|
} from "typescript-virtual-container";
|
|
1744
|
-
|
|
1745
|
-
async function processResult(r: CommandResult) {
|
|
1746
|
-
if (r.exitCode === 0 && r.stdout) {
|
|
1747
|
-
console.log("Success:", r.stdout);
|
|
1748
|
-
} else if (r.stderr) {
|
|
1749
|
-
console.error("Error:", r.stderr);
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
1380
|
```
|
|
1753
1381
|
|
|
1754
1382
|
---
|
|
1755
1383
|
|
|
1756
1384
|
## FAQ
|
|
1757
1385
|
|
|
1758
|
-
|
|
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.
|
|
1759
1388
|
|
|
1760
|
-
|
|
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.
|
|
1761
1391
|
|
|
1762
|
-
|
|
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.
|
|
1763
1394
|
|
|
1764
|
-
|
|
1395
|
+
**Does data persist between restarts?**
|
|
1396
|
+
Only if you explicitly use `"fs"` mode or call `toSnapshot()` / `fromSnapshot()` manually. Memory mode is ephemeral.
|
|
1765
1397
|
|
|
1766
|
-
|
|
1398
|
+
**Can I run multiple isolated shells?**
|
|
1399
|
+
Yes. Each `new VirtualShell(...)` creates a completely independent VFS and user manager.
|
|
1767
1400
|
|
|
1768
|
-
|
|
1401
|
+
**Are custom commands shared between shell instances?**
|
|
1402
|
+
No. Custom commands registered with `shell.addCommand()` are instance-local.
|
|
1769
1403
|
|
|
1770
|
-
|
|
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.
|
|
1771
1406
|
|
|
1772
|
-
|
|
1407
|
+
**Can I create custom commands?**
|
|
1408
|
+
Yes — use `shell.addCommand()` or implement the `ShellModule` interface directly.
|
|
1773
1409
|
|
|
1774
|
-
|
|
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`.
|
|
1775
1412
|
|
|
1776
|
-
|
|
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.
|
|
1777
1415
|
|
|
1778
1416
|
---
|
|
1779
1417
|
|
|
1780
1418
|
## Troubleshooting
|
|
1781
1419
|
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
```
|
|
1785
|
-
Error: listen EADDRINUSE :::2222
|
|
1786
|
-
```
|
|
1787
|
-
|
|
1788
|
-
**Solution**: Use a different port
|
|
1789
|
-
|
|
1420
|
+
**`Error: listen EADDRINUSE :::2222`**
|
|
1421
|
+
The port is already in use. Use a different port or stop the existing process.
|
|
1790
1422
|
```typescript
|
|
1791
1423
|
const ssh = new VirtualSshServer({ port: 3333 });
|
|
1792
1424
|
```
|
|
1793
1425
|
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
**Solution**:
|
|
1799
|
-
|
|
1800
|
-
```typescript
|
|
1801
|
-
process.env.SSH_MIMIC_ROOT_PASSWORD = "your-password";
|
|
1802
|
-
await ssh.start();
|
|
1803
|
-
```
|
|
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)`.
|
|
1804
1430
|
|
|
1805
|
-
|
|
1431
|
+
**Auth always fails with "lockout"**
|
|
1432
|
+
Call `ssh.clearLockout(ip)` or increase `maxAuthAttempts`. In tests, use `maxAuthAttempts: Infinity`.
|
|
1806
1433
|
|
|
1807
|
-
|
|
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()`.
|
|
1808
1436
|
|
|
1809
|
-
|
|
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.
|
|
1810
1439
|
|
|
1440
|
+
**File not found errors**
|
|
1441
|
+
Create the parent directory first:
|
|
1811
1442
|
```typescript
|
|
1812
|
-
const vfs = ssh.getVfs();
|
|
1813
1443
|
vfs.mkdir("/home/alice", 0o755);
|
|
1444
|
+
vfs.writeFile("/home/alice/file.txt", "content");
|
|
1814
1445
|
```
|
|
1815
1446
|
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
**Cause**: `flushMirror()` not called
|
|
1819
|
-
|
|
1820
|
-
**Solution**:
|
|
1821
|
-
|
|
1447
|
+
**`snapshotPath` is required error**
|
|
1448
|
+
You set `mode: "fs"` without providing `snapshotPath`:
|
|
1822
1449
|
```typescript
|
|
1823
|
-
|
|
1450
|
+
new VirtualFileSystem({ mode: "fs", snapshotPath: "./data" });
|
|
1824
1451
|
```
|
|
1825
1452
|
|
|
1826
1453
|
---
|
|
1827
1454
|
|
|
1828
1455
|
## Contributing
|
|
1829
1456
|
|
|
1830
|
-
1. Fork repository
|
|
1831
|
-
2. Create feature branch: `git checkout -b feat/my-feature`
|
|
1832
|
-
3. Make changes and add tests
|
|
1833
|
-
4. Format
|
|
1834
|
-
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.
|
|
1835
1462
|
|
|
1836
|
-
**Code
|
|
1837
|
-
- Biome formatting (opinionated)
|
|
1838
|
-
- Full TypeScript
|
|
1839
|
-
- JSDoc comments on public API
|
|
1840
|
-
- 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
|
|
1841
1469
|
|
|
1842
1470
|
---
|
|
1843
1471
|
|
|
1844
1472
|
## Security
|
|
1845
1473
|
|
|
1846
|
-
- Passwords are hashed with `scrypt`
|
|
1847
|
-
- Root account
|
|
1848
|
-
- Sudo privileges are explicit and
|
|
1849
|
-
-
|
|
1850
|
-
-
|
|
1851
|
-
-
|
|
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.
|
|
1852
1480
|
|
|
1853
|
-
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`.
|
|
1854
1482
|
|
|
1855
1483
|
---
|
|
1856
1484
|
|
|
1857
1485
|
## Support
|
|
1858
1486
|
|
|
1859
1487
|
- Open an issue for bugs, regressions, or feature requests.
|
|
1860
|
-
- Include Node/Bun version, package version, and a minimal reproduction.
|
|
1861
|
-
- 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.
|
|
1862
1490
|
|
|
1863
1491
|
---
|
|
1864
1492
|
|
|
1865
1493
|
## License
|
|
1866
1494
|
|
|
1867
|
-
MIT
|
|
1495
|
+
MIT — see [LICENSE](./LICENSE).
|
|
1868
1496
|
|
|
1869
1497
|
---
|
|
1870
1498
|
|
|
1871
1499
|
## Roadmap
|
|
1872
1500
|
|
|
1873
1501
|
- [x] Custom command plugin API
|
|
1874
|
-
- [x] Optional per-user quotas
|
|
1875
|
-
- [x] Improved shell compatibility
|
|
1876
|
-
- [
|
|
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`
|
|
1877
1509
|
- [x] Structured event hooks (session open/close, file write, sudo challenge)
|
|
1878
|
-
- [ ]
|
|
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).
|