typescript-virtual-container 1.5.3 → 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/README.md +43 -23
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/SSHMimic/executor.js +23 -5
  4. package/dist/VirtualPackageManager/index.js +10 -0
  5. package/dist/commands/basename.d.ts +13 -0
  6. package/dist/commands/basename.js +45 -0
  7. package/dist/commands/file.d.ts +8 -0
  8. package/dist/commands/file.js +57 -0
  9. package/dist/commands/fun.d.ts +32 -0
  10. package/dist/commands/fun.js +172 -0
  11. package/dist/commands/ifconfig.d.ts +7 -0
  12. package/dist/commands/ifconfig.js +52 -0
  13. package/dist/commands/last.d.ts +13 -0
  14. package/dist/commands/last.js +68 -0
  15. package/dist/commands/manuals-bundle.js +598 -6
  16. package/dist/commands/registry.js +24 -2
  17. package/dist/commands/runtime.js +22 -2
  18. package/dist/commands/sh.js +5 -0
  19. package/dist/commands/tput.d.ts +13 -0
  20. package/dist/commands/tput.js +76 -0
  21. package/dist/commands/w.d.ts +7 -0
  22. package/dist/commands/w.js +38 -0
  23. package/dist/utils/expand.d.ts +12 -0
  24. package/dist/utils/expand.js +84 -0
  25. package/package.json +9 -3
  26. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -50
  27. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -31
  28. package/.github/dependabot.yml +0 -27
  29. package/.github/pull_request_template.md +0 -21
  30. package/.github/workflows/create-pull-request.yml +0 -85
  31. package/.github/workflows/publish.yml +0 -25
  32. package/.github/workflows/test-battery.yml +0 -102
  33. package/.vscode/settings.json +0 -20
  34. package/CODE_OF_CONDUCT.md +0 -39
  35. package/CONTRIBUTING.md +0 -59
  36. package/HONEYPOT.md +0 -358
  37. package/SECURITY.md +0 -33
  38. package/benchmark-results.txt +0 -40
  39. package/benchmark-virtualshell.ts +0 -88
  40. package/biome.json +0 -37
  41. package/build.js +0 -22
  42. package/builds/fortune-nyx-v1.5.3-directbash-k6.1.0.mjs +0 -1764
  43. package/builds/fortune-nyx-v1.5.3-ssh-nosftp.js +0 -1764
  44. package/builds/fortune-nyx-v1.5.3-ssh.cjs +0 -1765
  45. package/builds/fortune-nyx-v1.5.3-web.min.js +0 -17036
  46. package/bun.lock +0 -244
  47. package/docs/.nojekyll +0 -1
  48. package/docs/app.js +0 -1751
  49. package/docs/assets/hierarchy.js +0 -1
  50. package/docs/assets/highlight.css +0 -162
  51. package/docs/assets/icons.js +0 -18
  52. package/docs/assets/icons.svg +0 -1
  53. package/docs/assets/main.js +0 -60
  54. package/docs/assets/navigation.js +0 -1
  55. package/docs/assets/search.js +0 -1
  56. package/docs/assets/style.css +0 -1633
  57. package/docs/classes/HoneyPot.html +0 -31
  58. package/docs/classes/IdleManager.html +0 -162
  59. package/docs/classes/SshClient.html +0 -66
  60. package/docs/classes/VirtualFileSystem.html +0 -279
  61. package/docs/classes/VirtualPackageManager.html +0 -63
  62. package/docs/classes/VirtualSftpServer.html +0 -169
  63. package/docs/classes/VirtualShell.html +0 -285
  64. package/docs/classes/VirtualSshServer.html +0 -182
  65. package/docs/classes/VirtualUserManager.html +0 -276
  66. package/docs/demo.html +0 -82
  67. package/docs/functions/assertDiff.html +0 -6
  68. package/docs/functions/diffSnapshots.html +0 -7
  69. package/docs/functions/formatDiff.html +0 -6
  70. package/docs/functions/getArg.html +0 -13
  71. package/docs/functions/getFlag.html +0 -15
  72. package/docs/functions/ifFlag.html +0 -11
  73. package/docs/hierarchy.html +0 -1
  74. package/docs/index.html +0 -1869
  75. package/docs/interfaces/AuditLogEntry.html +0 -6
  76. package/docs/interfaces/CommandContext.html +0 -22
  77. package/docs/interfaces/CommandResult.html +0 -26
  78. package/docs/interfaces/ExecStream.html +0 -11
  79. package/docs/interfaces/HoneyPotStats.html +0 -16
  80. package/docs/interfaces/IdleManagerOptions.html +0 -7
  81. package/docs/interfaces/InstalledPackage.html +0 -20
  82. package/docs/interfaces/NanoEditorSession.html +0 -8
  83. package/docs/interfaces/PackageDefinition.html +0 -30
  84. package/docs/interfaces/PackageFile.html +0 -8
  85. package/docs/interfaces/PasswordChallenge.html +0 -16
  86. package/docs/interfaces/RemoveOptions.html +0 -4
  87. package/docs/interfaces/ShellEnv.html +0 -6
  88. package/docs/interfaces/ShellModule.html +0 -14
  89. package/docs/interfaces/ShellProperties.html +0 -14
  90. package/docs/interfaces/ShellStream.html +0 -11
  91. package/docs/interfaces/SudoChallenge.html +0 -24
  92. package/docs/interfaces/VfsBaseNode.html +0 -12
  93. package/docs/interfaces/VfsDiff.html +0 -10
  94. package/docs/interfaces/VfsDiffEntry.html +0 -6
  95. package/docs/interfaces/VfsDiffModified.html +0 -10
  96. package/docs/interfaces/VfsDirectoryNode.html +0 -15
  97. package/docs/interfaces/VfsFileNode.html +0 -17
  98. package/docs/interfaces/VfsOptions.html +0 -26
  99. package/docs/interfaces/VfsSnapshot.html +0 -3
  100. package/docs/interfaces/VfsSnapshotBaseNode.html +0 -8
  101. package/docs/interfaces/VfsSnapshotDirectoryNode.html +0 -10
  102. package/docs/interfaces/VfsSnapshotFileNode.html +0 -12
  103. package/docs/interfaces/VirtualActiveSession.html +0 -12
  104. package/docs/interfaces/VirtualSftpServerOptions.html +0 -7
  105. package/docs/interfaces/VirtualShellVfsLike.html +0 -15
  106. package/docs/interfaces/VirtualShellVfsOptions.html +0 -3
  107. package/docs/interfaces/WriteFileOptions.html +0 -6
  108. package/docs/media/LICENSE +0 -21
  109. package/docs/modules.html +0 -1
  110. package/docs/types/ArgParseOptions.html +0 -4
  111. package/docs/types/CommandMode.html +0 -2
  112. package/docs/types/CommandOutcome.html +0 -2
  113. package/docs/types/IdleState.html +0 -1
  114. package/docs/types/VfsNodeStats.html +0 -2
  115. package/docs/types/VfsNodeType.html +0 -2
  116. package/docs/types/VfsPersistenceMode.html +0 -5
  117. package/docs/types/VfsSnapshotNode.html +0 -2
  118. package/examples/README.md +0 -288
  119. package/examples/app.js +0 -1751
  120. package/examples/app.ts +0 -299
  121. package/examples/build.js +0 -27
  122. package/examples/demo.html +0 -33
  123. package/examples/honeypot-audit.ts +0 -180
  124. package/examples/honeypot-export.ts +0 -253
  125. package/examples/honeypot-quickstart.ts +0 -110
  126. package/examples/index.html +0 -82
  127. package/examples/server.js +0 -55
  128. package/polyfills/buffer.js +0 -117
  129. package/polyfills/node_child_process/index.js +0 -2
  130. package/polyfills/node_crypto/index.js +0 -167
  131. package/polyfills/node_events/index.js +0 -9
  132. package/polyfills/node_fs/index.js +0 -202
  133. package/polyfills/node_fs/promises.js +0 -4
  134. package/polyfills/node_os/index.js +0 -9
  135. package/polyfills/node_path/index.js +0 -28
  136. package/polyfills/node_vm/index.js +0 -7
  137. package/polyfills/node_zlib/index.js +0 -3
  138. package/polyfills/process.js +0 -14
  139. package/polyfills/ssh2/index.js +0 -75
  140. package/scripts/build-all.mjs +0 -226
  141. package/scripts/build-names.mjs +0 -43
  142. package/scripts/generate-manuals-bundle.mjs +0 -49
  143. package/scripts/postinstall.js +0 -42
  144. package/scripts/publish-package.sh +0 -70
  145. package/src/Honeypot/index.ts +0 -457
  146. package/src/SSHClient/index.ts +0 -270
  147. package/src/SSHMimic/exec.ts +0 -49
  148. package/src/SSHMimic/executor.ts +0 -251
  149. package/src/SSHMimic/hostKey.ts +0 -21
  150. package/src/SSHMimic/index.ts +0 -337
  151. package/src/SSHMimic/loginBanner.ts +0 -36
  152. package/src/SSHMimic/loginFormat.ts +0 -10
  153. package/src/SSHMimic/prompt.ts +0 -14
  154. package/src/SSHMimic/sftp.ts +0 -883
  155. package/src/VirtualFileSystem/binaryPack.ts +0 -258
  156. package/src/VirtualFileSystem/index.ts +0 -1193
  157. package/src/VirtualFileSystem/internalTypes.ts +0 -43
  158. package/src/VirtualFileSystem/journal.ts +0 -171
  159. package/src/VirtualFileSystem/path.ts +0 -74
  160. package/src/VirtualPackageManager/index.ts +0 -996
  161. package/src/VirtualShell/idleManager.ts +0 -137
  162. package/src/VirtualShell/index.ts +0 -475
  163. package/src/VirtualShell/shell.ts +0 -700
  164. package/src/VirtualShell/shellParser.ts +0 -285
  165. package/src/VirtualUserManager/index.ts +0 -758
  166. package/src/bun.d.ts +0 -1
  167. package/src/commands/adduser.ts +0 -103
  168. package/src/commands/alias.ts +0 -69
  169. package/src/commands/apt.ts +0 -233
  170. package/src/commands/awk.ts +0 -168
  171. package/src/commands/base64.ts +0 -29
  172. package/src/commands/cat.ts +0 -52
  173. package/src/commands/cd.ts +0 -25
  174. package/src/commands/chmod.ts +0 -85
  175. package/src/commands/clear.ts +0 -15
  176. package/src/commands/command-helpers.ts +0 -286
  177. package/src/commands/cp.ts +0 -83
  178. package/src/commands/curl.ts +0 -147
  179. package/src/commands/cut.ts +0 -36
  180. package/src/commands/date.ts +0 -30
  181. package/src/commands/declare.ts +0 -49
  182. package/src/commands/deluser.ts +0 -98
  183. package/src/commands/df.ts +0 -23
  184. package/src/commands/diff.ts +0 -43
  185. package/src/commands/dpkg.ts +0 -180
  186. package/src/commands/du.ts +0 -56
  187. package/src/commands/echo.ts +0 -58
  188. package/src/commands/env.ts +0 -23
  189. package/src/commands/exit.ts +0 -18
  190. package/src/commands/export.ts +0 -34
  191. package/src/commands/find.ts +0 -68
  192. package/src/commands/free.ts +0 -47
  193. package/src/commands/grep.ts +0 -116
  194. package/src/commands/groups.ts +0 -19
  195. package/src/commands/gzip.ts +0 -88
  196. package/src/commands/head.ts +0 -52
  197. package/src/commands/help.ts +0 -152
  198. package/src/commands/helpers.ts +0 -234
  199. package/src/commands/history.ts +0 -34
  200. package/src/commands/hostname.ts +0 -14
  201. package/src/commands/htop.ts +0 -20
  202. package/src/commands/id.ts +0 -19
  203. package/src/commands/index.ts +0 -9
  204. package/src/commands/kill.ts +0 -19
  205. package/src/commands/ln.ts +0 -71
  206. package/src/commands/ls.ts +0 -243
  207. package/src/commands/lsb-release.ts +0 -63
  208. package/src/commands/man.ts +0 -31
  209. package/src/commands/manuals/adduser.txt +0 -11
  210. package/src/commands/manuals/apt-cache.txt +0 -12
  211. package/src/commands/manuals/apt.txt +0 -20
  212. package/src/commands/manuals/awk.txt +0 -13
  213. package/src/commands/manuals/cat.txt +0 -14
  214. package/src/commands/manuals/cd.txt +0 -16
  215. package/src/commands/manuals/chmod.txt +0 -16
  216. package/src/commands/manuals/clear.txt +0 -10
  217. package/src/commands/manuals/cp.txt +0 -10
  218. package/src/commands/manuals/curl.txt +0 -20
  219. package/src/commands/manuals/date.txt +0 -14
  220. package/src/commands/manuals/declare.txt +0 -12
  221. package/src/commands/manuals/deluser.txt +0 -10
  222. package/src/commands/manuals/df.txt +0 -10
  223. package/src/commands/manuals/dpkg-query.txt +0 -11
  224. package/src/commands/manuals/dpkg.txt +0 -14
  225. package/src/commands/manuals/du.txt +0 -11
  226. package/src/commands/manuals/echo.txt +0 -11
  227. package/src/commands/manuals/false.txt +0 -10
  228. package/src/commands/manuals/find.txt +0 -11
  229. package/src/commands/manuals/free.txt +0 -12
  230. package/src/commands/manuals/grep.txt +0 -13
  231. package/src/commands/manuals/groups.txt +0 -10
  232. package/src/commands/manuals/gzip.txt +0 -11
  233. package/src/commands/manuals/head.txt +0 -10
  234. package/src/commands/manuals/help.txt +0 -11
  235. package/src/commands/manuals/history.txt +0 -11
  236. package/src/commands/manuals/hostname.txt +0 -10
  237. package/src/commands/manuals/id.txt +0 -10
  238. package/src/commands/manuals/kill.txt +0 -13
  239. package/src/commands/manuals/ls.txt +0 -20
  240. package/src/commands/manuals/lsb_release.txt +0 -14
  241. package/src/commands/manuals/mkdir.txt +0 -10
  242. package/src/commands/manuals/mv.txt +0 -10
  243. package/src/commands/manuals/nano.txt +0 -11
  244. package/src/commands/manuals/neofetch.txt +0 -10
  245. package/src/commands/manuals/node.txt +0 -13
  246. package/src/commands/manuals/npm.txt +0 -13
  247. package/src/commands/manuals/npx.txt +0 -13
  248. package/src/commands/manuals/passwd.txt +0 -11
  249. package/src/commands/manuals/ping.txt +0 -10
  250. package/src/commands/manuals/printf.txt +0 -11
  251. package/src/commands/manuals/ps.txt +0 -10
  252. package/src/commands/manuals/pwd.txt +0 -10
  253. package/src/commands/manuals/python3.txt +0 -13
  254. package/src/commands/manuals/readlink.txt +0 -10
  255. package/src/commands/manuals/return.txt +0 -10
  256. package/src/commands/manuals/rm.txt +0 -10
  257. package/src/commands/manuals/sed.txt +0 -11
  258. package/src/commands/manuals/set.txt +0 -11
  259. package/src/commands/manuals/shift.txt +0 -10
  260. package/src/commands/manuals/sleep.txt +0 -10
  261. package/src/commands/manuals/sort.txt +0 -12
  262. package/src/commands/manuals/source.txt +0 -11
  263. package/src/commands/manuals/ssh.txt +0 -11
  264. package/src/commands/manuals/stat.txt +0 -10
  265. package/src/commands/manuals/su.txt +0 -13
  266. package/src/commands/manuals/sudo.txt +0 -11
  267. package/src/commands/manuals/tail.txt +0 -10
  268. package/src/commands/manuals/tar.txt +0 -19
  269. package/src/commands/manuals/tee.txt +0 -10
  270. package/src/commands/manuals/test.txt +0 -11
  271. package/src/commands/manuals/touch.txt +0 -11
  272. package/src/commands/manuals/tr.txt +0 -10
  273. package/src/commands/manuals/trap.txt +0 -10
  274. package/src/commands/manuals/true.txt +0 -10
  275. package/src/commands/manuals/type.txt +0 -10
  276. package/src/commands/manuals/uname.txt +0 -12
  277. package/src/commands/manuals/uniq.txt +0 -12
  278. package/src/commands/manuals/unset.txt +0 -10
  279. package/src/commands/manuals/uptime.txt +0 -11
  280. package/src/commands/manuals/wc.txt +0 -12
  281. package/src/commands/manuals/wget.txt +0 -12
  282. package/src/commands/manuals/which.txt +0 -10
  283. package/src/commands/manuals/whoami.txt +0 -10
  284. package/src/commands/manuals/xargs.txt +0 -10
  285. package/src/commands/manuals-bundle.ts +0 -898
  286. package/src/commands/mkdir.ts +0 -31
  287. package/src/commands/mv.ts +0 -50
  288. package/src/commands/nano.ts +0 -38
  289. package/src/commands/neofetch.ts +0 -53
  290. package/src/commands/node.ts +0 -341
  291. package/src/commands/npm.ts +0 -132
  292. package/src/commands/passwd.ts +0 -50
  293. package/src/commands/ping.ts +0 -32
  294. package/src/commands/printf.ts +0 -129
  295. package/src/commands/ps.ts +0 -58
  296. package/src/commands/pwd.ts +0 -9
  297. package/src/commands/python.ts +0 -2229
  298. package/src/commands/read.ts +0 -46
  299. package/src/commands/registry.ts +0 -249
  300. package/src/commands/rm.ts +0 -42
  301. package/src/commands/runtime.ts +0 -421
  302. package/src/commands/sed.ts +0 -68
  303. package/src/commands/seq.ts +0 -43
  304. package/src/commands/set.ts +0 -29
  305. package/src/commands/sh.ts +0 -467
  306. package/src/commands/shift.ts +0 -63
  307. package/src/commands/sleep.ts +0 -20
  308. package/src/commands/sort.ts +0 -46
  309. package/src/commands/source.ts +0 -52
  310. package/src/commands/stat.ts +0 -61
  311. package/src/commands/su.ts +0 -72
  312. package/src/commands/sudo.ts +0 -76
  313. package/src/commands/tail.ts +0 -53
  314. package/src/commands/tar.ts +0 -102
  315. package/src/commands/tee.ts +0 -36
  316. package/src/commands/test.ts +0 -137
  317. package/src/commands/touch.ts +0 -28
  318. package/src/commands/tr.ts +0 -70
  319. package/src/commands/tree.ts +0 -20
  320. package/src/commands/true.ts +0 -27
  321. package/src/commands/type.ts +0 -48
  322. package/src/commands/uname.ts +0 -29
  323. package/src/commands/uniq.ts +0 -39
  324. package/src/commands/unset.ts +0 -17
  325. package/src/commands/uptime.ts +0 -54
  326. package/src/commands/wc.ts +0 -55
  327. package/src/commands/wget.ts +0 -148
  328. package/src/commands/which.ts +0 -37
  329. package/src/commands/who.ts +0 -25
  330. package/src/commands/whoami.ts +0 -14
  331. package/src/commands/xargs.ts +0 -31
  332. package/src/index.ts +0 -67
  333. package/src/modules/linuxRootfs.ts +0 -1961
  334. package/src/modules/neofetch.ts +0 -358
  335. package/src/modules/shellInteractive.ts +0 -57
  336. package/src/modules/shellRuntime.ts +0 -76
  337. package/src/self-standalone.ts +0 -542
  338. package/src/standalone-wo-sftp.ts +0 -38
  339. package/src/standalone.ts +0 -72
  340. package/src/types/commands.ts +0 -146
  341. package/src/types/pipeline.ts +0 -52
  342. package/src/types/streams.ts +0 -32
  343. package/src/types/tar-stream.d.ts +0 -38
  344. package/src/types/vfs.ts +0 -98
  345. package/src/utils/expand.ts +0 -491
  346. package/src/utils/perfLogger.ts +0 -72
  347. package/src/utils/tokenize.ts +0 -98
  348. package/src/utils/vfsDiff.ts +0 -275
  349. package/tests/command-helpers.test.ts +0 -116
  350. package/tests/commands-admin-net.test.ts +0 -441
  351. package/tests/commands-advanced.test.ts +0 -456
  352. package/tests/commands-core.test.ts +0 -562
  353. package/tests/commands-missing.test.ts +0 -570
  354. package/tests/commands-specific-units.test.ts +0 -327
  355. package/tests/commands-text-sys.test.ts +0 -445
  356. package/tests/expand.test.ts +0 -170
  357. package/tests/helpers.test.ts +0 -97
  358. package/tests/new-features.test.ts +0 -1036
  359. package/tests/parser-executor.test.ts +0 -37
  360. package/tests/sftp.test.ts +0 -323
  361. package/tests/ssh-exec.test.ts +0 -45
  362. package/tests/test-helper.ts +0 -79
  363. package/tests/users.test.ts +0 -86
  364. package/tsconfig.json +0 -49
  365. package/typedoc.json +0 -47
@@ -1,758 +0,0 @@
1
- import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
2
- import { EventEmitter } from "node:events";
3
- import * as path from "node:path";
4
- import type { PerfLogger } from "../utils/perfLogger";
5
- import { createPerfLogger } from "../utils/perfLogger";
6
- import type VirtualFileSystem from "../VirtualFileSystem";
7
-
8
- /**
9
- * Persisted virtual user credential record.
10
- * @internal
11
- */
12
- export interface VirtualUserRecord {
13
- /** Unique login name. */
14
- username: string;
15
- /** Per-user random salt used for password hashing. */
16
- salt: string;
17
- /** Scrypt-derived password hash in hex encoding. */
18
- passwordHash: string;
19
- }
20
-
21
- /** Runtime representation of authenticated SSH session. */
22
- export interface VirtualActiveSession {
23
- /** Stable session identifier (UUID). */
24
- id: string;
25
- /** Username bound to session. */
26
- username: string;
27
- /** Virtual terminal identifier (pts/*). */
28
- tty: string;
29
- /** Remote client IP or host label. */
30
- remoteAddress: string;
31
- /** ISO-8601 start timestamp. */
32
- startedAt: string;
33
- }
34
-
35
- function resolveFastPasswordHash(): boolean {
36
- const configured = process.env.SSH_MIMIC_FAST_PASSWORD_HASH;
37
- return (
38
- !!configured &&
39
- !["0", "false", "no", "off"].includes(configured.toLowerCase())
40
- );
41
- }
42
-
43
- const perf: PerfLogger = createPerfLogger("VirtualUserManager");
44
-
45
- /**
46
- * Persistent user, sudoers, and active-session manager for the shell runtime.
47
- *
48
- * Passwords are hashed with scrypt by default and stored in the backing virtual filesystem.
49
- */
50
- export class VirtualUserManager extends EventEmitter {
51
- private static readonly recordCache = new Map<string, VirtualUserRecord>();
52
- private static readonly fastPasswordHash = resolveFastPasswordHash();
53
- private readonly usersPath = "/etc/htpasswd";
54
- private readonly sudoersPath = "/etc/sudoers";
55
- private readonly quotasPath = "/etc/quotas";
56
- private readonly authDirPath = "/.virtual-env-js/.auth";
57
- private readonly users = new Map<string, VirtualUserRecord>();
58
- private readonly sudoers = new Set<string>();
59
- private readonly quotas = new Map<string, number>();
60
- private readonly activeSessions = new Map<string, VirtualActiveSession>();
61
- private nextTty = 0;
62
-
63
- /**
64
- * Creates a user manager instance backed by a virtual filesystem.
65
- *
66
- * @param vfs Backing virtual filesystem used for persistence.
67
- * @param autoSudoForNewUsers Whether newly created users are added to sudoers.
68
- */
69
- constructor(
70
- private readonly vfs: VirtualFileSystem,
71
- // private readonly defaultRootPassword: string = process.env
72
- // .SSH_MIMIC_ROOT_PASSWORD || "root",
73
- private readonly autoSudoForNewUsers: boolean = true,
74
- ) {
75
- super();
76
- perf.mark("constructor");
77
- }
78
-
79
- /**
80
- * Loads users/sudoers from disk and ensures root account exists.
81
- * Also creates the current system user if not already present.
82
- */
83
- public async initialize(): Promise<void> {
84
- perf.mark("initialize");
85
- this.loadFromVfs();
86
- this.loadSudoersFromVfs();
87
- this.loadQuotasFromVfs();
88
-
89
- let changed = false;
90
- if (!this.users.has("root")) {
91
- this.users.set("root", this.createRecord("root", ""));
92
- changed = true;
93
- }
94
-
95
- this.sudoers.add("root");
96
-
97
- // Auto-create current system user for easier authentication
98
- // const currentUser = process.env.USER || process.env.USERNAME;
99
- // if (currentUser && currentUser !== "root" && !this.users.has(currentUser)) {
100
- // const userPassword = this.defaultRootPassword;
101
- // this.users.set(currentUser, this.createRecord(currentUser, userPassword));
102
- // this.sudoers.add(currentUser);
103
- // changed = true;
104
- // }
105
-
106
- const homePath = "/root";
107
- if (!this.vfs.exists(homePath)) {
108
- this.vfs.mkdir(homePath, 0o755);
109
- this.vfs.writeFile(
110
- `${homePath}/README.txt`,
111
- `Welcome to the virtual environment, root`,
112
- );
113
- }
114
-
115
- if (changed) {
116
- await this.persist();
117
- }
118
- this.emit("initialized");
119
- }
120
-
121
- /**
122
- * Sets max allowed bytes under /home/<username>.
123
- *
124
- * @param username Target username.
125
- * @param maxBytes Quota ceiling in bytes.
126
- */
127
- public async setQuotaBytes(
128
- username: string,
129
- maxBytes: number,
130
- ): Promise<void> {
131
- perf.mark("setQuotaBytes");
132
- this.validateUsername(username);
133
- if (!this.users.has(username)) {
134
- throw new Error(`quota: user '${username}' does not exist`);
135
- }
136
-
137
- if (!Number.isFinite(maxBytes) || maxBytes < 0) {
138
- throw new Error("quota: maxBytes must be a non-negative number");
139
- }
140
-
141
- this.quotas.set(username, Math.floor(maxBytes));
142
- await this.persist();
143
- }
144
-
145
- /**
146
- * Removes quota for a user.
147
- *
148
- * @param username Target username.
149
- */
150
- public async clearQuota(username: string): Promise<void> {
151
- perf.mark("clearQuota");
152
- this.validateUsername(username);
153
- this.quotas.delete(username);
154
- await this.persist();
155
- }
156
-
157
- /**
158
- * Gets configured quota in bytes for a user.
159
- *
160
- * @param username Target username.
161
- * @returns Quota in bytes, or null when unlimited.
162
- */
163
- public getQuotaBytes(username: string): number | null {
164
- perf.mark("getQuotaBytes");
165
- return this.quotas.get(username) ?? null;
166
- }
167
-
168
- /**
169
- * Computes current usage under /home/<username>.
170
- *
171
- * @param username Target username.
172
- * @returns Current usage in bytes.
173
- */
174
- public getUsageBytes(username: string): number {
175
- perf.mark("getUsageBytes");
176
- const homePath = username === "root" ? "/root" : `/home/${username}`;
177
- if (!this.vfs.exists(homePath)) {
178
- return 0;
179
- }
180
-
181
- return this.vfs.getUsageBytes(homePath);
182
- }
183
-
184
- /**
185
- * Validates that writing file content would not exceed user quota.
186
- *
187
- * Quotas are enforced only for writes inside /home/<username>.
188
- *
189
- * @param username Authenticated user.
190
- * @param targetPath Target file path.
191
- * @param nextContent New file content.
192
- */
193
- public assertWriteWithinQuota(
194
- username: string,
195
- targetPath: string,
196
- nextContent: string | Buffer,
197
- ): void {
198
- perf.mark("assertWriteWithinQuota");
199
- const quota = this.quotas.get(username);
200
- if (quota === undefined) {
201
- return;
202
- }
203
-
204
- const normalizedPath = normalizeVfsPath(targetPath);
205
- const homePath = normalizeVfsPath(username === "root" ? "/root" : `/home/${username}`);
206
- const inUserHome =
207
- normalizedPath === homePath || normalizedPath.startsWith(`${homePath}/`);
208
- if (!inUserHome) {
209
- return;
210
- }
211
-
212
- const currentUsage = this.getUsageBytes(username);
213
- let existingSize = 0;
214
- if (this.vfs.exists(normalizedPath)) {
215
- const existing = this.vfs.stat(normalizedPath);
216
- if (existing.type === "file") {
217
- existingSize = existing.size;
218
- }
219
- }
220
-
221
- const incomingSize = Buffer.isBuffer(nextContent)
222
- ? nextContent.length
223
- : Buffer.byteLength(nextContent, "utf8");
224
- const projectedUsage = currentUsage - existingSize + incomingSize;
225
-
226
- if (projectedUsage > quota) {
227
- throw new Error(
228
- `quota exceeded for '${username}': ${projectedUsage}/${quota} bytes`,
229
- );
230
- }
231
- }
232
-
233
- /**
234
- * Verifies plaintext password against stored record.
235
- *
236
- * @param username User login name.
237
- * @param password Plaintext password candidate.
238
- * @returns True when credentials are valid.
239
- */
240
- public verifyPassword(username: string, password: string): boolean {
241
- perf.mark("verifyPassword");
242
- const record = this.users.get(username);
243
- if (!record) {
244
- // Perform a dummy hash to avoid timing leakage on unknown usernames
245
- this.hashPassword(password, "");
246
- return false;
247
- }
248
-
249
- const computed = this.hashPassword(password, record.salt);
250
- const expected = record.passwordHash;
251
- // timingSafeEqual prevents timing-based password oracle attacks
252
- try {
253
- const a = Buffer.from(computed, "hex");
254
- const b = Buffer.from(expected, "hex");
255
- if (a.length !== b.length) return false;
256
- return timingSafeEqual(a, b);
257
- } catch {
258
- return computed === expected;
259
- }
260
- }
261
-
262
- /**
263
- * Creates user, home directory, and sudo access entry.
264
- *
265
- * @param username New username.
266
- * @param password Initial plaintext password.
267
- */
268
- public async addUser(username: string, password: string): Promise<void> {
269
- perf.mark("addUser");
270
- this.validateUsername(username);
271
- this.validatePassword(password);
272
-
273
- if (this.users.has(username)) {
274
- return;
275
- // throw new Error(`adduser: user '${username}' already exists`);
276
- }
277
-
278
- this.users.set(username, this.createRecord(username, password));
279
- if (this.autoSudoForNewUsers) {
280
- this.sudoers.add(username);
281
- }
282
- const homePath = username === "root" ? "/root" : `/home/${username}`;
283
- if (!this.vfs.exists(homePath)) {
284
- this.vfs.mkdir(homePath, 0o755);
285
- this.vfs.writeFile(
286
- `${homePath}/README.txt`,
287
- `Welcome to the virtual environment, ${username}`,
288
- );
289
- }
290
- await this.persist();
291
- this.emit("user:add", { username });
292
- }
293
-
294
- /**
295
- * Retrieves stored password hash for a user, or null if user does not exist.
296
- *
297
- * @param username Target username.
298
- * @returns Password hash in hex encoding, or null when user is not found.
299
- */
300
- public getPasswordHash(username: string): string | null {
301
- perf.mark("getPasswordHash");
302
- const record = this.users.get(username);
303
- return record ? record.passwordHash : null;
304
- }
305
-
306
- /**
307
- * Updates the password for an existing user account.
308
- *
309
- * @param username Username to update.
310
- * @param password New plaintext password (must be non-empty).
311
- * @throws When the user does not exist or the password is empty.
312
- */
313
- public async setPassword(username: string, password: string): Promise<void> {
314
- perf.mark("setPassword");
315
- this.validateUsername(username);
316
- this.validatePassword(password);
317
-
318
- if (!this.users.has(username)) {
319
- throw new Error(`passwd: user '${username}' does not exist`);
320
- }
321
-
322
- this.users.set(username, this.createRecord(username, password));
323
- await this.persist();
324
- }
325
-
326
- /**
327
- * Deletes an existing non-root user account and revokes sudo access.
328
- *
329
- * @param username Username to remove.
330
- * @throws When `username` is `"root"` or the user does not exist.
331
- */
332
- public async deleteUser(username: string): Promise<void> {
333
- perf.mark("deleteUser");
334
- this.validateUsername(username);
335
-
336
- if (username === "root") {
337
- throw new Error("deluser: cannot delete root");
338
- }
339
-
340
- if (!this.users.delete(username)) {
341
- throw new Error(`deluser: user '${username}' does not exist`);
342
- }
343
-
344
- this.sudoers.delete(username);
345
-
346
- this.emit("user:delete", { username });
347
- await this.persist();
348
- }
349
-
350
- /**
351
- * Checks whether user is member of sudoers set.
352
- *
353
- * @param username Username to test.
354
- * @returns True when user can run sudo.
355
- */
356
- public isSudoer(username: string): boolean {
357
- perf.mark("isSudoer");
358
- return this.sudoers.has(username);
359
- }
360
-
361
- /**
362
- * Grants sudo privileges to an existing user.
363
- *
364
- * @param username Username to promote.
365
- * @throws When the user does not exist.
366
- */
367
- public async addSudoer(username: string): Promise<void> {
368
- perf.mark("addSudoer");
369
- this.validateUsername(username);
370
- if (!this.users.has(username)) {
371
- throw new Error(`sudoers: user '${username}' does not exist`);
372
- }
373
-
374
- this.sudoers.add(username);
375
- await this.persist();
376
- }
377
-
378
- /**
379
- * Revokes sudo privileges from a user. Root cannot be demoted.
380
- *
381
- * @param username Username to demote.
382
- * @throws When `username` is `"root"`.
383
- */
384
- public async removeSudoer(username: string): Promise<void> {
385
- perf.mark("removeSudoer");
386
- this.validateUsername(username);
387
- if (username === "root") {
388
- throw new Error("sudoers: cannot remove root");
389
- }
390
-
391
- this.sudoers.delete(username);
392
- await this.persist();
393
- }
394
-
395
- /**
396
- * Registers a new active session and allocates a virtual TTY identifier.
397
- *
398
- * Called by the SSH server when a client is authenticated. The returned
399
- * descriptor is visible in `who` output and `listActiveSessions()`.
400
- *
401
- * @param username Authenticated username bound to the session.
402
- * @param remoteAddress IP address or hostname of the connecting client.
403
- * @returns The newly created `VirtualActiveSession` descriptor.
404
- */
405
- public registerSession(
406
- username: string,
407
- remoteAddress: string,
408
- ): VirtualActiveSession {
409
- perf.mark("registerSession");
410
- const session: VirtualActiveSession = {
411
- id: randomUUID(),
412
- username,
413
- tty: `pts/${this.nextTty++}`,
414
- remoteAddress,
415
- startedAt: new Date().toISOString(),
416
- };
417
- this.activeSessions.set(session.id, session);
418
- this.emit("session:register", {
419
- sessionId: session.id,
420
- username,
421
- remoteAddress,
422
- });
423
- return session;
424
- }
425
-
426
- /**
427
- * Removes an active session record when the connection closes.
428
- *
429
- * Safe to call with a `null` or `undefined` session ID — it will be a no-op.
430
- *
431
- * @param sessionId Session UUID returned by `registerSession()`, or nullish.
432
- */
433
- public unregisterSession(sessionId: string | null | undefined): void {
434
- perf.mark("unregisterSession");
435
- if (!sessionId) {
436
- return;
437
- }
438
-
439
- const session = this.activeSessions.get(sessionId);
440
- this.activeSessions.delete(sessionId);
441
- if (session) {
442
- this.emit("session:unregister", {
443
- sessionId,
444
- username: session.username,
445
- });
446
- }
447
- this.activeSessions.delete(sessionId);
448
- }
449
-
450
- /**
451
- * Updates the username and remote address metadata for an active session.
452
- *
453
- * Called internally by `su` and `sudo` when the effective user changes
454
- * within a session. Silently ignored when the session ID is nullish or
455
- * unknown.
456
- *
457
- * @param sessionId Session UUID to update, or nullish for no-op.
458
- * @param username New effective username.
459
- * @param remoteAddress New remote address (usually unchanged).
460
- */
461
- public updateSession(
462
- sessionId: string | null | undefined,
463
- username: string,
464
- remoteAddress: string,
465
- ): void {
466
- perf.mark("updateSession");
467
- if (!sessionId) {
468
- return;
469
- }
470
-
471
- const session = this.activeSessions.get(sessionId);
472
- if (!session) {
473
- return;
474
- }
475
-
476
- this.activeSessions.set(sessionId, {
477
- ...session,
478
- username,
479
- remoteAddress,
480
- });
481
- }
482
-
483
- /**
484
- * Returns a snapshot of all currently active sessions, sorted by start time.
485
- *
486
- * Used by `who`, `ps`, `uptime`, and the `HoneyPot` auditor.
487
- *
488
- * @returns Array of `VirtualActiveSession` descriptors.
489
- */
490
- public listActiveSessions(): VirtualActiveSession[] {
491
- perf.mark("listActiveSessions");
492
- return Array.from(this.activeSessions.values()).sort((left, right) =>
493
- left.startedAt.localeCompare(right.startedAt),
494
- );
495
- }
496
-
497
- /**
498
- * Returns a sorted list of all registered usernames.
499
- *
500
- * @returns Array of username strings sorted alphabetically.
501
- */
502
- public listUsers(): string[] {
503
- return Array.from(this.users.keys()).sort();
504
- }
505
-
506
- private loadFromVfs(): void {
507
- this.users.clear();
508
-
509
- if (!this.vfs.exists(this.usersPath)) {
510
- return;
511
- }
512
-
513
- const raw = this.vfs.readFile(this.usersPath);
514
- for (const line of raw.split("\n")) {
515
- const trimmed = line.trim();
516
- if (trimmed.length === 0) {
517
- continue;
518
- }
519
-
520
- const parts = trimmed.split(":");
521
- if (parts.length < 3) {
522
- continue;
523
- }
524
-
525
- const [username, salt, passwordHash] = parts;
526
- if (!username || !salt || !passwordHash) {
527
- continue;
528
- }
529
-
530
- this.users.set(username, { username, salt, passwordHash });
531
- }
532
- }
533
-
534
- private loadSudoersFromVfs(): void {
535
- this.sudoers.clear();
536
-
537
- if (!this.vfs.exists(this.sudoersPath)) {
538
- return;
539
- }
540
-
541
- const raw = this.vfs.readFile(this.sudoersPath);
542
- for (const line of raw.split("\n")) {
543
- const username = line.trim();
544
- if (username.length > 0) {
545
- this.sudoers.add(username);
546
- }
547
- }
548
- }
549
-
550
- private loadQuotasFromVfs(): void {
551
- this.quotas.clear();
552
-
553
- if (!this.vfs.exists(this.quotasPath)) {
554
- return;
555
- }
556
-
557
- const raw = this.vfs.readFile(this.quotasPath);
558
- for (const line of raw.split("\n")) {
559
- const trimmed = line.trim();
560
- if (trimmed.length === 0) {
561
- continue;
562
- }
563
-
564
- const [username, value] = trimmed.split(":");
565
- const bytes = Number.parseInt(value ?? "", 10);
566
- if (!username || !Number.isFinite(bytes) || bytes < 0) {
567
- continue;
568
- }
569
-
570
- this.quotas.set(username, bytes);
571
- }
572
- }
573
-
574
- private async persist(): Promise<void> {
575
- if (!this.vfs.exists(this.authDirPath)) {
576
- this.vfs.mkdir(this.authDirPath, 0o700);
577
- }
578
-
579
- const authContent = Array.from(this.users.values())
580
- .sort((left, right) => left.username.localeCompare(right.username))
581
- .map((record) =>
582
- [record.username, record.salt, record.passwordHash].join(":"),
583
- )
584
- .join("\n");
585
- const sudoersContent = Array.from(this.sudoers.values()).sort().join("\n");
586
- const quotasContent = Array.from(this.quotas.entries())
587
- .sort(([left], [right]) => left.localeCompare(right))
588
- .map(([username, maxBytes]) => `${username}:${maxBytes}`)
589
- .join("\n");
590
-
591
- let changed = false;
592
- changed =
593
- this.writeIfChanged(
594
- this.usersPath,
595
- authContent.length > 0 ? `${authContent}\n` : "",
596
- 0o600,
597
- ) || changed;
598
- changed =
599
- this.writeIfChanged(
600
- this.sudoersPath,
601
- sudoersContent.length > 0 ? `${sudoersContent}\n` : "",
602
- 0o600,
603
- ) || changed;
604
- changed =
605
- this.writeIfChanged(
606
- this.quotasPath,
607
- quotasContent.length > 0 ? `${quotasContent}\n` : "",
608
- 0o600,
609
- ) || changed;
610
-
611
- if (changed) {
612
- await this.vfs.flushMirror();
613
- }
614
- }
615
-
616
- private writeIfChanged(
617
- targetPath: string,
618
- content: string,
619
- mode: number,
620
- ): boolean {
621
- if (this.vfs.exists(targetPath)) {
622
- const existing = this.vfs.readFile(targetPath);
623
- if (existing === content) {
624
- this.vfs.chmod(targetPath, mode);
625
- return false;
626
- }
627
- }
628
-
629
- this.vfs.writeFile(targetPath, content, { mode });
630
- return true;
631
- }
632
-
633
- private createRecord(username: string, password: string): VirtualUserRecord {
634
- // Cache key is a hash of the inputs — never store plaintext password in memory
635
- const cacheKey = createHash("sha256").update(username).update(":").update(password).digest("hex");
636
- const cached = VirtualUserManager.recordCache.get(cacheKey);
637
- if (cached) {
638
- return cached;
639
- }
640
-
641
- const salt = randomBytes(16).toString("hex");
642
- const record = {
643
- username,
644
- salt,
645
- // Hash uses the generated salt — verifyPassword must use record.salt
646
- passwordHash: this.hashPassword(password, salt),
647
- };
648
-
649
- VirtualUserManager.recordCache.set(cacheKey, record);
650
- return record;
651
- }
652
-
653
- /**
654
- * Returns `true` when the user has a non-empty password set.
655
- *
656
- * A user with no password (or whose password hash matches the empty-string
657
- * hash) is allowed to authenticate without a credential check.
658
- *
659
- * @param username Target username.
660
- */
661
- public hasPassword(username: string): boolean {
662
- perf.mark("hasPassword");
663
- const record = this.users.get(username);
664
- if (!record) return false;
665
- // Empty password hash computed with the record's own salt
666
- const emptyHash = this.hashPassword("", record.salt);
667
- if (record.passwordHash === emptyHash) return false;
668
- return !!record.passwordHash;
669
- }
670
-
671
- /**
672
- * Hashes a plaintext password using scrypt (or SHA-256 in fast-hash mode).
673
- *
674
- * Set `SSH_MIMIC_FAST_PASSWORD_HASH=1` to switch to SHA-256 for test
675
- * environments where scrypt latency is undesirable.
676
- *
677
- * @param password Plaintext password string.
678
- * @returns Hex-encoded hash string.
679
- */
680
- /**
681
- * Hash a password with an optional salt.
682
- * When salt is provided (verify path), the same salt is used for a
683
- * deterministic hash. When omitted (create path), an empty salt is used
684
- * for backward compat — callers should pass the stored salt on verify.
685
- */
686
- public hashPassword(password: string, salt = ""): string {
687
- if (VirtualUserManager.fastPasswordHash) {
688
- return createHash("sha256")
689
- .update(salt)
690
- .update(password)
691
- .digest("hex");
692
- }
693
-
694
- return scryptSync(password, salt || "", 32).toString("hex");
695
- }
696
-
697
- private validateUsername(username: string): void {
698
- if (!username || username.trim() === "") {
699
- throw new Error("invalid username");
700
- }
701
-
702
- if (!/^[a-z_][a-z0-9_-]{0,31}$/i.test(username)) {
703
- throw new Error("invalid username");
704
- }
705
- }
706
-
707
- private validatePassword(password: string): void {
708
- if (!password || password.trim() === "") {
709
- throw new Error("invalid password");
710
- }
711
- }
712
- private readonly authorizedKeys = new Map<
713
- string,
714
- Array<{ algo: string; data: Buffer }>
715
- >();
716
-
717
- /**
718
- * Adds an SSH public key for a user, enabling public-key authentication.
719
- *
720
- * @param username Target user.
721
- * @param algo Key algorithm (e.g. "ssh-rsa", "ssh-ed25519").
722
- * @param data Raw key data as a Buffer (the base64-decoded key bytes).
723
- */
724
- public addAuthorizedKey(username: string, algo: string, data: Buffer): void {
725
- perf.mark("addAuthorizedKey");
726
- const keys = this.authorizedKeys.get(username) ?? [];
727
- keys.push({ algo, data });
728
- this.authorizedKeys.set(username, keys);
729
- this.emit("key:add", { username, algo });
730
- }
731
-
732
- /**
733
- * Removes all authorized keys for a user.
734
- *
735
- * @param username Target user.
736
- */
737
- public removeAuthorizedKeys(username: string): void {
738
- this.authorizedKeys.delete(username);
739
- this.emit("key:remove", { username });
740
- }
741
-
742
- /**
743
- * Returns the list of authorized keys for a user.
744
- * Returns an empty array when no keys are registered.
745
- *
746
- * @param username Target user.
747
- */
748
- public getAuthorizedKeys(
749
- username: string,
750
- ): Array<{ algo: string; data: Buffer }> {
751
- return this.authorizedKeys.get(username) ?? [];
752
- }
753
- }
754
-
755
- function normalizeVfsPath(targetPath: string): string {
756
- const normalized = path.posix.normalize(targetPath);
757
- return normalized.startsWith("/") ? normalized : `/${normalized}`;
758
- }