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