typescript-virtual-container 1.5.2 → 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 (364) 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/commands/basename.d.ts +13 -0
  5. package/dist/commands/basename.js +45 -0
  6. package/dist/commands/file.d.ts +8 -0
  7. package/dist/commands/file.js +57 -0
  8. package/dist/commands/fun.d.ts +32 -0
  9. package/dist/commands/fun.js +172 -0
  10. package/dist/commands/ifconfig.d.ts +7 -0
  11. package/dist/commands/ifconfig.js +52 -0
  12. package/dist/commands/last.d.ts +13 -0
  13. package/dist/commands/last.js +68 -0
  14. package/dist/commands/manuals-bundle.js +598 -6
  15. package/dist/commands/registry.js +24 -2
  16. package/dist/commands/runtime.js +159 -106
  17. package/dist/commands/sh.js +5 -0
  18. package/dist/commands/tput.d.ts +13 -0
  19. package/dist/commands/tput.js +76 -0
  20. package/dist/commands/w.d.ts +7 -0
  21. package/dist/commands/w.js +38 -0
  22. package/dist/utils/expand.d.ts +12 -0
  23. package/dist/utils/expand.js +84 -0
  24. package/package.json +9 -3
  25. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -50
  26. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -31
  27. package/.github/dependabot.yml +0 -27
  28. package/.github/pull_request_template.md +0 -21
  29. package/.github/workflows/create-pull-request.yml +0 -85
  30. package/.github/workflows/publish.yml +0 -25
  31. package/.github/workflows/test-battery.yml +0 -102
  32. package/.vscode/settings.json +0 -20
  33. package/CODE_OF_CONDUCT.md +0 -39
  34. package/CONTRIBUTING.md +0 -59
  35. package/HONEYPOT.md +0 -358
  36. package/SECURITY.md +0 -33
  37. package/benchmark-results.txt +0 -40
  38. package/benchmark-virtualshell.ts +0 -88
  39. package/biome.json +0 -37
  40. package/build.js +0 -22
  41. package/builds/fortune-nyx-v1.5.1-directbash-k6.1.0.mjs +0 -1768
  42. package/builds/fortune-nyx-v1.5.1-ssh-nosftp.js +0 -1768
  43. package/builds/fortune-nyx-v1.5.1-ssh.cjs +0 -1769
  44. package/builds/fortune-nyx-v1.5.1-web.min.js +0 -17022
  45. package/bun.lock +0 -244
  46. package/docs/.nojekyll +0 -1
  47. package/docs/app.js +0 -1755
  48. package/docs/assets/hierarchy.js +0 -1
  49. package/docs/assets/highlight.css +0 -162
  50. package/docs/assets/icons.js +0 -18
  51. package/docs/assets/icons.svg +0 -1
  52. package/docs/assets/main.js +0 -60
  53. package/docs/assets/navigation.js +0 -1
  54. package/docs/assets/search.js +0 -1
  55. package/docs/assets/style.css +0 -1633
  56. package/docs/classes/HoneyPot.html +0 -31
  57. package/docs/classes/IdleManager.html +0 -162
  58. package/docs/classes/SshClient.html +0 -66
  59. package/docs/classes/VirtualFileSystem.html +0 -279
  60. package/docs/classes/VirtualPackageManager.html +0 -63
  61. package/docs/classes/VirtualSftpServer.html +0 -169
  62. package/docs/classes/VirtualShell.html +0 -285
  63. package/docs/classes/VirtualSshServer.html +0 -182
  64. package/docs/classes/VirtualUserManager.html +0 -276
  65. package/docs/demo.html +0 -82
  66. package/docs/functions/assertDiff.html +0 -6
  67. package/docs/functions/diffSnapshots.html +0 -7
  68. package/docs/functions/formatDiff.html +0 -6
  69. package/docs/functions/getArg.html +0 -13
  70. package/docs/functions/getFlag.html +0 -15
  71. package/docs/functions/ifFlag.html +0 -11
  72. package/docs/hierarchy.html +0 -1
  73. package/docs/index.html +0 -1869
  74. package/docs/interfaces/AuditLogEntry.html +0 -6
  75. package/docs/interfaces/CommandContext.html +0 -22
  76. package/docs/interfaces/CommandResult.html +0 -26
  77. package/docs/interfaces/ExecStream.html +0 -11
  78. package/docs/interfaces/HoneyPotStats.html +0 -16
  79. package/docs/interfaces/IdleManagerOptions.html +0 -7
  80. package/docs/interfaces/InstalledPackage.html +0 -20
  81. package/docs/interfaces/NanoEditorSession.html +0 -8
  82. package/docs/interfaces/PackageDefinition.html +0 -30
  83. package/docs/interfaces/PackageFile.html +0 -8
  84. package/docs/interfaces/PasswordChallenge.html +0 -16
  85. package/docs/interfaces/RemoveOptions.html +0 -4
  86. package/docs/interfaces/ShellEnv.html +0 -6
  87. package/docs/interfaces/ShellModule.html +0 -14
  88. package/docs/interfaces/ShellProperties.html +0 -14
  89. package/docs/interfaces/ShellStream.html +0 -11
  90. package/docs/interfaces/SudoChallenge.html +0 -24
  91. package/docs/interfaces/VfsBaseNode.html +0 -12
  92. package/docs/interfaces/VfsDiff.html +0 -10
  93. package/docs/interfaces/VfsDiffEntry.html +0 -6
  94. package/docs/interfaces/VfsDiffModified.html +0 -10
  95. package/docs/interfaces/VfsDirectoryNode.html +0 -15
  96. package/docs/interfaces/VfsFileNode.html +0 -17
  97. package/docs/interfaces/VfsOptions.html +0 -26
  98. package/docs/interfaces/VfsSnapshot.html +0 -3
  99. package/docs/interfaces/VfsSnapshotBaseNode.html +0 -8
  100. package/docs/interfaces/VfsSnapshotDirectoryNode.html +0 -10
  101. package/docs/interfaces/VfsSnapshotFileNode.html +0 -12
  102. package/docs/interfaces/VirtualActiveSession.html +0 -12
  103. package/docs/interfaces/VirtualSftpServerOptions.html +0 -7
  104. package/docs/interfaces/VirtualShellVfsLike.html +0 -15
  105. package/docs/interfaces/VirtualShellVfsOptions.html +0 -3
  106. package/docs/interfaces/WriteFileOptions.html +0 -6
  107. package/docs/media/LICENSE +0 -21
  108. package/docs/modules.html +0 -1
  109. package/docs/types/ArgParseOptions.html +0 -4
  110. package/docs/types/CommandMode.html +0 -2
  111. package/docs/types/CommandOutcome.html +0 -2
  112. package/docs/types/IdleState.html +0 -1
  113. package/docs/types/VfsNodeStats.html +0 -2
  114. package/docs/types/VfsNodeType.html +0 -2
  115. package/docs/types/VfsPersistenceMode.html +0 -5
  116. package/docs/types/VfsSnapshotNode.html +0 -2
  117. package/examples/README.md +0 -288
  118. package/examples/app.js +0 -1755
  119. package/examples/app.ts +0 -299
  120. package/examples/build.js +0 -27
  121. package/examples/demo.html +0 -33
  122. package/examples/honeypot-audit.ts +0 -180
  123. package/examples/honeypot-export.ts +0 -253
  124. package/examples/honeypot-quickstart.ts +0 -110
  125. package/examples/index.html +0 -82
  126. package/examples/server.js +0 -55
  127. package/polyfills/buffer.js +0 -117
  128. package/polyfills/node_child_process/index.js +0 -2
  129. package/polyfills/node_crypto/index.js +0 -167
  130. package/polyfills/node_events/index.js +0 -9
  131. package/polyfills/node_fs/index.js +0 -202
  132. package/polyfills/node_fs/promises.js +0 -4
  133. package/polyfills/node_os/index.js +0 -9
  134. package/polyfills/node_path/index.js +0 -28
  135. package/polyfills/node_vm/index.js +0 -7
  136. package/polyfills/node_zlib/index.js +0 -3
  137. package/polyfills/process.js +0 -14
  138. package/polyfills/ssh2/index.js +0 -75
  139. package/scripts/build-all.mjs +0 -226
  140. package/scripts/build-names.mjs +0 -43
  141. package/scripts/generate-manuals-bundle.mjs +0 -49
  142. package/scripts/postinstall.js +0 -42
  143. package/scripts/publish-package.sh +0 -70
  144. package/src/Honeypot/index.ts +0 -457
  145. package/src/SSHClient/index.ts +0 -270
  146. package/src/SSHMimic/exec.ts +0 -49
  147. package/src/SSHMimic/executor.ts +0 -251
  148. package/src/SSHMimic/hostKey.ts +0 -21
  149. package/src/SSHMimic/index.ts +0 -337
  150. package/src/SSHMimic/loginBanner.ts +0 -36
  151. package/src/SSHMimic/loginFormat.ts +0 -10
  152. package/src/SSHMimic/prompt.ts +0 -14
  153. package/src/SSHMimic/sftp.ts +0 -883
  154. package/src/VirtualFileSystem/binaryPack.ts +0 -258
  155. package/src/VirtualFileSystem/index.ts +0 -1193
  156. package/src/VirtualFileSystem/internalTypes.ts +0 -43
  157. package/src/VirtualFileSystem/journal.ts +0 -171
  158. package/src/VirtualFileSystem/path.ts +0 -74
  159. package/src/VirtualPackageManager/index.ts +0 -1006
  160. package/src/VirtualShell/idleManager.ts +0 -137
  161. package/src/VirtualShell/index.ts +0 -475
  162. package/src/VirtualShell/shell.ts +0 -700
  163. package/src/VirtualShell/shellParser.ts +0 -285
  164. package/src/VirtualUserManager/index.ts +0 -758
  165. package/src/bun.d.ts +0 -1
  166. package/src/commands/adduser.ts +0 -103
  167. package/src/commands/alias.ts +0 -69
  168. package/src/commands/apt.ts +0 -233
  169. package/src/commands/awk.ts +0 -168
  170. package/src/commands/base64.ts +0 -29
  171. package/src/commands/cat.ts +0 -52
  172. package/src/commands/cd.ts +0 -25
  173. package/src/commands/chmod.ts +0 -85
  174. package/src/commands/clear.ts +0 -15
  175. package/src/commands/command-helpers.ts +0 -286
  176. package/src/commands/cp.ts +0 -83
  177. package/src/commands/curl.ts +0 -147
  178. package/src/commands/cut.ts +0 -36
  179. package/src/commands/date.ts +0 -30
  180. package/src/commands/declare.ts +0 -49
  181. package/src/commands/deluser.ts +0 -98
  182. package/src/commands/df.ts +0 -23
  183. package/src/commands/diff.ts +0 -43
  184. package/src/commands/dpkg.ts +0 -180
  185. package/src/commands/du.ts +0 -56
  186. package/src/commands/echo.ts +0 -58
  187. package/src/commands/env.ts +0 -23
  188. package/src/commands/exit.ts +0 -18
  189. package/src/commands/export.ts +0 -34
  190. package/src/commands/find.ts +0 -68
  191. package/src/commands/free.ts +0 -47
  192. package/src/commands/grep.ts +0 -116
  193. package/src/commands/groups.ts +0 -19
  194. package/src/commands/gzip.ts +0 -88
  195. package/src/commands/head.ts +0 -52
  196. package/src/commands/help.ts +0 -152
  197. package/src/commands/helpers.ts +0 -234
  198. package/src/commands/history.ts +0 -34
  199. package/src/commands/hostname.ts +0 -14
  200. package/src/commands/htop.ts +0 -20
  201. package/src/commands/id.ts +0 -19
  202. package/src/commands/index.ts +0 -9
  203. package/src/commands/kill.ts +0 -19
  204. package/src/commands/ln.ts +0 -71
  205. package/src/commands/ls.ts +0 -243
  206. package/src/commands/lsb-release.ts +0 -63
  207. package/src/commands/man.ts +0 -31
  208. package/src/commands/manuals/adduser.txt +0 -11
  209. package/src/commands/manuals/apt-cache.txt +0 -12
  210. package/src/commands/manuals/apt.txt +0 -20
  211. package/src/commands/manuals/awk.txt +0 -13
  212. package/src/commands/manuals/cat.txt +0 -14
  213. package/src/commands/manuals/cd.txt +0 -16
  214. package/src/commands/manuals/chmod.txt +0 -16
  215. package/src/commands/manuals/clear.txt +0 -10
  216. package/src/commands/manuals/cp.txt +0 -10
  217. package/src/commands/manuals/curl.txt +0 -20
  218. package/src/commands/manuals/date.txt +0 -14
  219. package/src/commands/manuals/declare.txt +0 -12
  220. package/src/commands/manuals/deluser.txt +0 -10
  221. package/src/commands/manuals/df.txt +0 -10
  222. package/src/commands/manuals/dpkg-query.txt +0 -11
  223. package/src/commands/manuals/dpkg.txt +0 -14
  224. package/src/commands/manuals/du.txt +0 -11
  225. package/src/commands/manuals/echo.txt +0 -11
  226. package/src/commands/manuals/false.txt +0 -10
  227. package/src/commands/manuals/find.txt +0 -11
  228. package/src/commands/manuals/free.txt +0 -12
  229. package/src/commands/manuals/grep.txt +0 -13
  230. package/src/commands/manuals/groups.txt +0 -10
  231. package/src/commands/manuals/gzip.txt +0 -11
  232. package/src/commands/manuals/head.txt +0 -10
  233. package/src/commands/manuals/help.txt +0 -11
  234. package/src/commands/manuals/history.txt +0 -11
  235. package/src/commands/manuals/hostname.txt +0 -10
  236. package/src/commands/manuals/id.txt +0 -10
  237. package/src/commands/manuals/kill.txt +0 -13
  238. package/src/commands/manuals/ls.txt +0 -20
  239. package/src/commands/manuals/lsb_release.txt +0 -14
  240. package/src/commands/manuals/mkdir.txt +0 -10
  241. package/src/commands/manuals/mv.txt +0 -10
  242. package/src/commands/manuals/nano.txt +0 -11
  243. package/src/commands/manuals/neofetch.txt +0 -10
  244. package/src/commands/manuals/node.txt +0 -13
  245. package/src/commands/manuals/npm.txt +0 -13
  246. package/src/commands/manuals/npx.txt +0 -13
  247. package/src/commands/manuals/passwd.txt +0 -11
  248. package/src/commands/manuals/ping.txt +0 -10
  249. package/src/commands/manuals/printf.txt +0 -11
  250. package/src/commands/manuals/ps.txt +0 -10
  251. package/src/commands/manuals/pwd.txt +0 -10
  252. package/src/commands/manuals/python3.txt +0 -13
  253. package/src/commands/manuals/readlink.txt +0 -10
  254. package/src/commands/manuals/return.txt +0 -10
  255. package/src/commands/manuals/rm.txt +0 -10
  256. package/src/commands/manuals/sed.txt +0 -11
  257. package/src/commands/manuals/set.txt +0 -11
  258. package/src/commands/manuals/shift.txt +0 -10
  259. package/src/commands/manuals/sleep.txt +0 -10
  260. package/src/commands/manuals/sort.txt +0 -12
  261. package/src/commands/manuals/source.txt +0 -11
  262. package/src/commands/manuals/ssh.txt +0 -11
  263. package/src/commands/manuals/stat.txt +0 -10
  264. package/src/commands/manuals/su.txt +0 -13
  265. package/src/commands/manuals/sudo.txt +0 -11
  266. package/src/commands/manuals/tail.txt +0 -10
  267. package/src/commands/manuals/tar.txt +0 -19
  268. package/src/commands/manuals/tee.txt +0 -10
  269. package/src/commands/manuals/test.txt +0 -11
  270. package/src/commands/manuals/touch.txt +0 -11
  271. package/src/commands/manuals/tr.txt +0 -10
  272. package/src/commands/manuals/trap.txt +0 -10
  273. package/src/commands/manuals/true.txt +0 -10
  274. package/src/commands/manuals/type.txt +0 -10
  275. package/src/commands/manuals/uname.txt +0 -12
  276. package/src/commands/manuals/uniq.txt +0 -12
  277. package/src/commands/manuals/unset.txt +0 -10
  278. package/src/commands/manuals/uptime.txt +0 -11
  279. package/src/commands/manuals/wc.txt +0 -12
  280. package/src/commands/manuals/wget.txt +0 -12
  281. package/src/commands/manuals/which.txt +0 -10
  282. package/src/commands/manuals/whoami.txt +0 -10
  283. package/src/commands/manuals/xargs.txt +0 -10
  284. package/src/commands/manuals-bundle.ts +0 -898
  285. package/src/commands/mkdir.ts +0 -31
  286. package/src/commands/mv.ts +0 -50
  287. package/src/commands/nano.ts +0 -38
  288. package/src/commands/neofetch.ts +0 -53
  289. package/src/commands/node.ts +0 -341
  290. package/src/commands/npm.ts +0 -132
  291. package/src/commands/passwd.ts +0 -50
  292. package/src/commands/ping.ts +0 -32
  293. package/src/commands/printf.ts +0 -129
  294. package/src/commands/ps.ts +0 -58
  295. package/src/commands/pwd.ts +0 -9
  296. package/src/commands/python.ts +0 -2229
  297. package/src/commands/read.ts +0 -46
  298. package/src/commands/registry.ts +0 -249
  299. package/src/commands/rm.ts +0 -42
  300. package/src/commands/runtime.ts +0 -378
  301. package/src/commands/sed.ts +0 -68
  302. package/src/commands/seq.ts +0 -43
  303. package/src/commands/set.ts +0 -29
  304. package/src/commands/sh.ts +0 -467
  305. package/src/commands/shift.ts +0 -63
  306. package/src/commands/sleep.ts +0 -20
  307. package/src/commands/sort.ts +0 -46
  308. package/src/commands/source.ts +0 -52
  309. package/src/commands/stat.ts +0 -61
  310. package/src/commands/su.ts +0 -72
  311. package/src/commands/sudo.ts +0 -76
  312. package/src/commands/tail.ts +0 -53
  313. package/src/commands/tar.ts +0 -102
  314. package/src/commands/tee.ts +0 -36
  315. package/src/commands/test.ts +0 -137
  316. package/src/commands/touch.ts +0 -28
  317. package/src/commands/tr.ts +0 -70
  318. package/src/commands/tree.ts +0 -20
  319. package/src/commands/true.ts +0 -27
  320. package/src/commands/type.ts +0 -48
  321. package/src/commands/uname.ts +0 -29
  322. package/src/commands/uniq.ts +0 -39
  323. package/src/commands/unset.ts +0 -17
  324. package/src/commands/uptime.ts +0 -54
  325. package/src/commands/wc.ts +0 -55
  326. package/src/commands/wget.ts +0 -148
  327. package/src/commands/which.ts +0 -37
  328. package/src/commands/who.ts +0 -25
  329. package/src/commands/whoami.ts +0 -14
  330. package/src/commands/xargs.ts +0 -31
  331. package/src/index.ts +0 -67
  332. package/src/modules/linuxRootfs.ts +0 -1961
  333. package/src/modules/neofetch.ts +0 -358
  334. package/src/modules/shellInteractive.ts +0 -57
  335. package/src/modules/shellRuntime.ts +0 -76
  336. package/src/self-standalone.ts +0 -542
  337. package/src/standalone-wo-sftp.ts +0 -38
  338. package/src/standalone.ts +0 -72
  339. package/src/types/commands.ts +0 -146
  340. package/src/types/pipeline.ts +0 -52
  341. package/src/types/streams.ts +0 -32
  342. package/src/types/tar-stream.d.ts +0 -38
  343. package/src/types/vfs.ts +0 -98
  344. package/src/utils/expand.ts +0 -491
  345. package/src/utils/perfLogger.ts +0 -72
  346. package/src/utils/tokenize.ts +0 -98
  347. package/src/utils/vfsDiff.ts +0 -275
  348. package/tests/command-helpers.test.ts +0 -116
  349. package/tests/commands-admin-net.test.ts +0 -441
  350. package/tests/commands-advanced.test.ts +0 -456
  351. package/tests/commands-core.test.ts +0 -562
  352. package/tests/commands-missing.test.ts +0 -570
  353. package/tests/commands-specific-units.test.ts +0 -327
  354. package/tests/commands-text-sys.test.ts +0 -445
  355. package/tests/expand.test.ts +0 -170
  356. package/tests/helpers.test.ts +0 -97
  357. package/tests/new-features.test.ts +0 -1036
  358. package/tests/parser-executor.test.ts +0 -37
  359. package/tests/sftp.test.ts +0 -323
  360. package/tests/ssh-exec.test.ts +0 -45
  361. package/tests/test-helper.ts +0 -79
  362. package/tests/users.test.ts +0 -86
  363. package/tsconfig.json +0 -49
  364. 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
- }