typescript-virtual-container 1.2.1 → 1.2.2
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/.github/workflows/publish.yml +25 -0
- package/README.md +25 -8
- package/package.json +6 -3
- package/dist/VirtualFileSystem/archive.d.ts +0 -5
- package/dist/VirtualFileSystem/archive.d.ts.map +0 -1
- package/dist/VirtualFileSystem/archive.js +0 -56
- package/dist/VirtualFileSystem/snapshot.d.ts +0 -5
- package/dist/VirtualFileSystem/snapshot.d.ts.map +0 -1
- package/dist/VirtualFileSystem/snapshot.js +0 -59
- package/dist/VirtualFileSystem/tree.d.ts +0 -3
- package/dist/VirtualFileSystem/tree.d.ts.map +0 -1
- package/dist/VirtualFileSystem/tree.js +0 -19
- package/dist/honeypot.d.ts +0 -132
- package/dist/honeypot.d.ts.map +0 -1
- package/dist/honeypot.js +0 -289
- package/src/VirtualFileSystem/archive.ts +0 -74
- package/src/VirtualFileSystem/snapshot.ts +0 -84
- package/src/VirtualFileSystem/tree.ts +0 -34
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Publish Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
id-token: write # Required for OIDC
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-node@v6
|
|
20
|
+
with:
|
|
21
|
+
node-version: '24'
|
|
22
|
+
registry-url: 'https://registry.npmjs.org'
|
|
23
|
+
- run: npm install
|
|
24
|
+
- run: npm run build --if-present
|
|
25
|
+
- run: npm publish
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `typescript-virtual-container`
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Scalable SSH/SFTP server with a virtual filesystem and typed programmatic API for testing, automation, 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)
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
`typescript-virtual-container` is a lightweight, fully-typed SSH/SFTP runtime written in TypeScript that provides:
|
|
38
38
|
|
|
39
39
|
- **SSH + SFTP Protocol Support**: Serve SSH shell/exec sessions and SFTP file operations on configurable ports.
|
|
40
|
-
- **Virtual Filesystem**:
|
|
40
|
+
- **Virtual Filesystem**: Fast developer workflow backed by a mirror directory under `.vfs/mirror`, with optional gzip compression and programmatic access.
|
|
41
41
|
- **User Management**: Create, authenticate, and manage virtual users with strict password hashing (scrypt) and sudo-like privilege elevation.
|
|
42
42
|
- **Programmatic Shell API**: Execute shell commands and query filesystem state directly from TypeScript without SSH overhead.
|
|
43
43
|
- **Event-Driven Architecture**: All core classes extend `EventEmitter` for lifecycle and operation tracking. Listen to auth events, filesystem operations, session lifecycle, and command execution for auditing and integration.
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
### What This Is
|
|
51
51
|
|
|
52
52
|
- A virtual shell runtime written in TypeScript.
|
|
53
|
-
-
|
|
53
|
+
- A virtual environment with its own virtual filesystem, user management, and command runtime.
|
|
54
54
|
- A practical tool for deterministic testing, automation pipelines, and SSH-like workflows without running real containers.
|
|
55
55
|
|
|
56
56
|
### What This Is Not
|
|
@@ -1684,17 +1684,34 @@ const ssh = new VirtualSshServer({
|
|
|
1684
1684
|
|
|
1685
1685
|
## Performance & Scalability
|
|
1686
1686
|
|
|
1687
|
-
###
|
|
1687
|
+
### Benchmarking
|
|
1688
1688
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1689
|
+
Use the built-in benchmark script to measure initialization and command throughput under concurrent shell loads:
|
|
1690
|
+
|
|
1691
|
+
```bash
|
|
1692
|
+
bun ./benchmark-virtualshell.ts
|
|
1693
|
+
```
|
|
1694
|
+
|
|
1695
|
+
The benchmark reports:
|
|
1696
|
+
|
|
1697
|
+
- shell initialization time by concurrency level
|
|
1698
|
+
- command execution time across all active shells
|
|
1699
|
+
- RSS memory growth during the run
|
|
1700
|
+
|
|
1701
|
+
Recent baseline runs show strong startup behavior up to 100 concurrent shells, and the runtime is designed to scale up to **1000 environments very easily** for testing and automation workloads.
|
|
1692
1702
|
|
|
1693
1703
|
### Concurrency
|
|
1694
1704
|
|
|
1695
1705
|
- SSH server handles multiple concurrent connections (event-driven)
|
|
1696
1706
|
- Programmatic `SshClient` is synchronous (executes sequentially per instance)
|
|
1697
1707
|
- Create multiple client instances for parallel operations
|
|
1708
|
+
- Horizontal shell instantiation (`new VirtualShell(...)`) is intended for high-volume scenarios, including large test matrices and multi-tenant simulation batches
|
|
1709
|
+
|
|
1710
|
+
### Scalability Notes
|
|
1711
|
+
|
|
1712
|
+
- Use a dedicated `basePath` per isolated environment to parallelize safely
|
|
1713
|
+
- Reuse long-lived shell instances when you need low-latency command bursts
|
|
1714
|
+
- Keep performance logging enabled in development (`DEV_MODE=1` or `RENDER_PERF=1`) to locate hotspots quickly
|
|
1698
1715
|
|
|
1699
1716
|
**Example:**
|
|
1700
1717
|
|
|
@@ -1740,7 +1757,7 @@ async function processResult(r: CommandResult) {
|
|
|
1740
1757
|
|
|
1741
1758
|
### Is this a real container runtime?
|
|
1742
1759
|
|
|
1743
|
-
No. It emulates SSH sessions, users, and filesystem behavior in
|
|
1760
|
+
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.
|
|
1744
1761
|
|
|
1745
1762
|
### Can I use this in production?
|
|
1746
1763
|
|
package/package.json
CHANGED
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"version": "1.2.
|
|
7
|
+
"version": "1.2.2",
|
|
8
8
|
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/itsrealfortune/typescript-virtual-container"
|
|
12
|
+
},
|
|
9
13
|
"keywords": [
|
|
10
14
|
"ssh",
|
|
11
15
|
"virtual-filesystem",
|
|
@@ -34,7 +38,6 @@
|
|
|
34
38
|
"typescript": "^5"
|
|
35
39
|
},
|
|
36
40
|
"dependencies": {
|
|
37
|
-
"ssh2": "^1.17.0"
|
|
38
|
-
"tar-stream": "^3.1.8"
|
|
41
|
+
"ssh2": "^1.17.0"
|
|
39
42
|
}
|
|
40
43
|
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { VfsSnapshot } from "../types/vfs";
|
|
2
|
-
export declare function archiveExists(archivePath: string): Promise<boolean>;
|
|
3
|
-
export declare function createTarBuffer(snapshotJson: string): Promise<Buffer>;
|
|
4
|
-
export declare function readSnapshotFromTar(tarBuffer: Buffer): Promise<VfsSnapshot>;
|
|
5
|
-
//# sourceMappingURL=archive.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../src/VirtualFileSystem/archive.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOzE;AAED,wBAAsB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuB3E;AAED,wBAAsB,mBAAmB,CACxC,SAAS,EAAE,MAAM,GACf,OAAO,CAAC,WAAW,CAAC,CAiCtB"}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import * as tarStream from "tar-stream";
|
|
3
|
-
export async function archiveExists(archivePath) {
|
|
4
|
-
try {
|
|
5
|
-
await fs.access(archivePath);
|
|
6
|
-
return true;
|
|
7
|
-
}
|
|
8
|
-
catch {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
export async function createTarBuffer(snapshotJson) {
|
|
13
|
-
const pack = tarStream.pack();
|
|
14
|
-
const chunks = [];
|
|
15
|
-
const finished = new Promise((resolve, reject) => {
|
|
16
|
-
pack.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
17
|
-
pack.on("error", reject);
|
|
18
|
-
pack.on("end", () => resolve(Buffer.concat(chunks)));
|
|
19
|
-
});
|
|
20
|
-
pack.entry({ name: "snapshot.json", mode: 0o600 }, snapshotJson, (error) => {
|
|
21
|
-
if (error) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
pack.finalize();
|
|
25
|
-
});
|
|
26
|
-
return finished;
|
|
27
|
-
}
|
|
28
|
-
export async function readSnapshotFromTar(tarBuffer) {
|
|
29
|
-
return new Promise((resolve, reject) => {
|
|
30
|
-
const extract = tarStream.extract();
|
|
31
|
-
let snapshotText = "";
|
|
32
|
-
let found = false;
|
|
33
|
-
extract.on("entry", (header, stream, next) => {
|
|
34
|
-
if (header.name === "snapshot.json") {
|
|
35
|
-
found = true;
|
|
36
|
-
stream.on("data", (chunk) => {
|
|
37
|
-
snapshotText += chunk.toString("utf8");
|
|
38
|
-
});
|
|
39
|
-
stream.on("end", next);
|
|
40
|
-
stream.resume();
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
stream.resume();
|
|
44
|
-
stream.on("end", next);
|
|
45
|
-
});
|
|
46
|
-
extract.on("finish", () => {
|
|
47
|
-
if (!found) {
|
|
48
|
-
reject(new Error("snapshot.json missing from archive"));
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
resolve(JSON.parse(snapshotText));
|
|
52
|
-
});
|
|
53
|
-
extract.on("error", reject);
|
|
54
|
-
extract.end(tarBuffer);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { VfsSnapshot } from "../types/vfs";
|
|
2
|
-
import type { InternalDirectoryNode } from "./internalTypes";
|
|
3
|
-
export declare function createSnapshot(root: InternalDirectoryNode): VfsSnapshot;
|
|
4
|
-
export declare function applySnapshot(rootTarget: InternalDirectoryNode, snapshot: VfsSnapshot): void;
|
|
5
|
-
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/VirtualFileSystem/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,WAAW,EAGX,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,iBAAiB,CAAC;AAgE3E,wBAAgB,cAAc,CAAC,IAAI,EAAE,qBAAqB,GAAG,WAAW,CAEvE;AAED,wBAAgB,aAAa,CAC5B,UAAU,EAAE,qBAAqB,EACjC,QAAQ,EAAE,WAAW,GACnB,IAAI,CAON"}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
function serializeNode(node) {
|
|
2
|
-
if (node.type === "file") {
|
|
3
|
-
return {
|
|
4
|
-
type: "file",
|
|
5
|
-
name: node.name,
|
|
6
|
-
mode: node.mode,
|
|
7
|
-
createdAt: node.createdAt.toISOString(),
|
|
8
|
-
updatedAt: node.updatedAt.toISOString(),
|
|
9
|
-
compressed: node.compressed,
|
|
10
|
-
contentBase64: node.content.toString("base64"),
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
return serializeDirectory(node);
|
|
14
|
-
}
|
|
15
|
-
function serializeDirectory(node) {
|
|
16
|
-
return {
|
|
17
|
-
type: "directory",
|
|
18
|
-
name: node.name,
|
|
19
|
-
mode: node.mode,
|
|
20
|
-
createdAt: node.createdAt.toISOString(),
|
|
21
|
-
updatedAt: node.updatedAt.toISOString(),
|
|
22
|
-
children: Array.from(node.children.values()).map((child) => serializeNode(child)),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function deserializeNode(node) {
|
|
26
|
-
if (node.type === "file") {
|
|
27
|
-
return {
|
|
28
|
-
type: "file",
|
|
29
|
-
name: node.name,
|
|
30
|
-
mode: node.mode,
|
|
31
|
-
createdAt: new Date(node.createdAt),
|
|
32
|
-
updatedAt: new Date(node.updatedAt),
|
|
33
|
-
content: Buffer.from(node.contentBase64, "base64"),
|
|
34
|
-
compressed: node.compressed,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
return deserializeDirectory(node);
|
|
38
|
-
}
|
|
39
|
-
function deserializeDirectory(node) {
|
|
40
|
-
return {
|
|
41
|
-
type: "directory",
|
|
42
|
-
name: node.name,
|
|
43
|
-
mode: node.mode,
|
|
44
|
-
createdAt: new Date(node.createdAt),
|
|
45
|
-
updatedAt: new Date(node.updatedAt),
|
|
46
|
-
children: new Map(node.children.map((child) => [child.name, deserializeNode(child)])),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
export function createSnapshot(root) {
|
|
50
|
-
return { root: serializeDirectory(root) };
|
|
51
|
-
}
|
|
52
|
-
export function applySnapshot(rootTarget, snapshot) {
|
|
53
|
-
const root = deserializeDirectory(snapshot.root);
|
|
54
|
-
rootTarget.name = root.name;
|
|
55
|
-
rootTarget.mode = root.mode;
|
|
56
|
-
rootTarget.createdAt = root.createdAt;
|
|
57
|
-
rootTarget.updatedAt = root.updatedAt;
|
|
58
|
-
rootTarget.children = root.children;
|
|
59
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../src/VirtualFileSystem/tree.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AA0B7D,wBAAgB,UAAU,CACzB,IAAI,EAAE,qBAAqB,EAC3B,SAAS,EAAE,MAAM,GACf,MAAM,CAIR"}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
function walkTree(node, indent, lines) {
|
|
2
|
-
const entries = Array.from(node.children.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
3
|
-
entries.forEach(([name, child], index) => {
|
|
4
|
-
const isLast = index === entries.length - 1;
|
|
5
|
-
const branch = isLast ? "`-- " : "|-- ";
|
|
6
|
-
const nextIndent = indent + (isLast ? " " : "| ");
|
|
7
|
-
if (child.type === "file") {
|
|
8
|
-
lines.push(`${indent}${branch}${name}${child.compressed ? " [gz]" : ""}`);
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
lines.push(`${indent}${branch}${name}/`);
|
|
12
|
-
walkTree(child, nextIndent, lines);
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
export function renderTree(node, rootLabel) {
|
|
16
|
-
const lines = [rootLabel];
|
|
17
|
-
walkTree(node, "", lines);
|
|
18
|
-
return lines.join("\n");
|
|
19
|
-
}
|
package/dist/honeypot.d.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Honeypot tracking and auditing module for virtual shell events.
|
|
3
|
-
*
|
|
4
|
-
* Attaches listeners to VirtualShell, VirtualFileSystem, VirtualUserManager,
|
|
5
|
-
* SshMimic, and SftpMimic instances to log all activity for security auditing,
|
|
6
|
-
* anomaly detection, and forensic analysis.
|
|
7
|
-
*
|
|
8
|
-
* @module honeypot
|
|
9
|
-
*/
|
|
10
|
-
import type { VirtualShell } from "./VirtualShell";
|
|
11
|
-
import type VirtualFileSystem from "./VirtualFileSystem";
|
|
12
|
-
import type { VirtualUserManager } from "./VirtualUserManager";
|
|
13
|
-
import type { SshMimic } from "./SSHMimic";
|
|
14
|
-
import type { SftpMimic } from "./SSHMimic/sftp";
|
|
15
|
-
/**
|
|
16
|
-
* Audit log entry recorded for each event.
|
|
17
|
-
*/
|
|
18
|
-
export interface AuditLogEntry {
|
|
19
|
-
timestamp: string;
|
|
20
|
-
type: string;
|
|
21
|
-
source: string;
|
|
22
|
-
details: Record<string, unknown>;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Statistics tracker for honeypot activity.
|
|
26
|
-
*/
|
|
27
|
-
export interface HoneyPotStats {
|
|
28
|
-
authAttempts: number;
|
|
29
|
-
authSuccesses: number;
|
|
30
|
-
authFailures: number;
|
|
31
|
-
commands: number;
|
|
32
|
-
fileWrites: number;
|
|
33
|
-
fileReads: number;
|
|
34
|
-
sessionStarts: number;
|
|
35
|
-
sessionEnds: number;
|
|
36
|
-
userCreated: number;
|
|
37
|
-
userDeleted: number;
|
|
38
|
-
clientConnects: number;
|
|
39
|
-
clientDisconnects: number;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* HoneyPot audit and event tracking utility.
|
|
43
|
-
*
|
|
44
|
-
* Singleton-like helper that attaches listeners to virtual shell components
|
|
45
|
-
* and maintains an audit log of all activity.
|
|
46
|
-
*/
|
|
47
|
-
export declare class HoneyPot {
|
|
48
|
-
private auditLog;
|
|
49
|
-
private stats;
|
|
50
|
-
private maxLogSize;
|
|
51
|
-
/**
|
|
52
|
-
* Creates a new HoneyPot instance.
|
|
53
|
-
*
|
|
54
|
-
* @param maxLogSize Maximum audit log entries to retain (default: 10000).
|
|
55
|
-
*/
|
|
56
|
-
constructor(maxLogSize?: number);
|
|
57
|
-
/**
|
|
58
|
-
* Attaches honeypot listeners to all provided event emitters.
|
|
59
|
-
*
|
|
60
|
-
* @param shell VirtualShell instance.
|
|
61
|
-
* @param vfs VirtualFileSystem instance.
|
|
62
|
-
* @param users VirtualUserManager instance.
|
|
63
|
-
* @param ssh SshMimic instance (optional).
|
|
64
|
-
* @param sftp SftpMimic instance (optional).
|
|
65
|
-
*/
|
|
66
|
-
attach(shell: VirtualShell, vfs: VirtualFileSystem, users: VirtualUserManager, ssh?: SshMimic, sftp?: SftpMimic): void;
|
|
67
|
-
/**
|
|
68
|
-
* Attaches to VirtualShell events.
|
|
69
|
-
*/
|
|
70
|
-
private attachVirtualShell;
|
|
71
|
-
/**
|
|
72
|
-
* Attaches to VirtualFileSystem events.
|
|
73
|
-
*/
|
|
74
|
-
private attachVirtualFileSystem;
|
|
75
|
-
/**
|
|
76
|
-
* Attaches to VirtualUserManager events.
|
|
77
|
-
*/
|
|
78
|
-
private attachVirtualUserManager;
|
|
79
|
-
/**
|
|
80
|
-
* Attaches to SshMimic events.
|
|
81
|
-
*/
|
|
82
|
-
private attachSshMimic;
|
|
83
|
-
/**
|
|
84
|
-
* Attaches to SftpMimic events.
|
|
85
|
-
*/
|
|
86
|
-
private attachSftpMimic;
|
|
87
|
-
/**
|
|
88
|
-
* Records an audit log entry.
|
|
89
|
-
*
|
|
90
|
-
* @param source Event source (e.g., "SshMimic", "VirtualFileSystem").
|
|
91
|
-
* @param type Event type.
|
|
92
|
-
* @param details Event-specific data.
|
|
93
|
-
*/
|
|
94
|
-
private log;
|
|
95
|
-
/**
|
|
96
|
-
* Returns audit log entries matching optional filters.
|
|
97
|
-
*
|
|
98
|
-
* @param type Optional event type filter.
|
|
99
|
-
* @param source Optional source filter.
|
|
100
|
-
* @returns Filtered audit log entries.
|
|
101
|
-
*/
|
|
102
|
-
getAuditLog(type?: string, source?: string): AuditLogEntry[];
|
|
103
|
-
/**
|
|
104
|
-
* Returns current activity statistics.
|
|
105
|
-
*
|
|
106
|
-
* @returns Snapshot of honeypot stats.
|
|
107
|
-
*/
|
|
108
|
-
getStats(): Readonly<HoneyPotStats>;
|
|
109
|
-
/**
|
|
110
|
-
* Clears audit log and resets statistics.
|
|
111
|
-
*/
|
|
112
|
-
reset(): void;
|
|
113
|
-
/**
|
|
114
|
-
* Returns recent log entries in reverse chronological order.
|
|
115
|
-
*
|
|
116
|
-
* @param limit Number of recent entries to return (default: 100).
|
|
117
|
-
* @returns Recent audit log entries.
|
|
118
|
-
*/
|
|
119
|
-
getRecent(limit?: number): AuditLogEntry[];
|
|
120
|
-
/**
|
|
121
|
-
* Detects potential security issues based on activity patterns.
|
|
122
|
-
*
|
|
123
|
-
* @returns Array of anomalies detected.
|
|
124
|
-
*/
|
|
125
|
-
detectAnomalies(): Array<{
|
|
126
|
-
type: string;
|
|
127
|
-
severity: "low" | "medium" | "high";
|
|
128
|
-
message: string;
|
|
129
|
-
}>;
|
|
130
|
-
}
|
|
131
|
-
export default HoneyPot;
|
|
132
|
-
//# sourceMappingURL=honeypot.d.ts.map
|
package/dist/honeypot.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"honeypot.d.ts","sourceRoot":"","sources":["../src/honeypot.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,iBAAiB,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,qBAAa,QAAQ;IACpB,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,KAAK,CAaX;IAEF,OAAO,CAAC,UAAU,CAAS;IAE3B;;;;OAIG;gBACS,UAAU,GAAE,MAAc;IAItC;;;;;;;;OAQG;IACI,MAAM,CACZ,KAAK,EAAE,YAAY,EACnB,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,kBAAkB,EACzB,GAAG,CAAC,EAAE,QAAQ,EACd,IAAI,CAAC,EAAE,SAAS,GACd,IAAI;IAYP;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAmB1B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAoB/B;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAkChC;;OAEG;IACH,OAAO,CAAC,cAAc;IAyCtB;;OAEG;IACH,OAAO,CAAC,eAAe;IAyCvB;;;;;;OAMG;IACH,OAAO,CAAC,GAAG;IAuBX;;;;;;OAMG;IACI,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,EAAE;IAOnE;;;;OAIG;IACI,QAAQ,IAAI,QAAQ,CAAC,aAAa,CAAC;IAI1C;;OAEG;IACI,KAAK,IAAI,IAAI;IAkBpB;;;;;OAKG;IACI,SAAS,CAAC,KAAK,GAAE,MAAY,GAAG,aAAa,EAAE;IAItD;;;;OAIG;IACI,eAAe,IAAI,KAAK,CAAC;QAC/B,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;QACpC,OAAO,EAAE,MAAM,CAAC;KAChB,CAAC;CAkDF;AAED,eAAe,QAAQ,CAAC"}
|
package/dist/honeypot.js
DELETED
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Honeypot tracking and auditing module for virtual shell events.
|
|
3
|
-
*
|
|
4
|
-
* Attaches listeners to VirtualShell, VirtualFileSystem, VirtualUserManager,
|
|
5
|
-
* SshMimic, and SftpMimic instances to log all activity for security auditing,
|
|
6
|
-
* anomaly detection, and forensic analysis.
|
|
7
|
-
*
|
|
8
|
-
* @module honeypot
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* HoneyPot audit and event tracking utility.
|
|
12
|
-
*
|
|
13
|
-
* Singleton-like helper that attaches listeners to virtual shell components
|
|
14
|
-
* and maintains an audit log of all activity.
|
|
15
|
-
*/
|
|
16
|
-
export class HoneyPot {
|
|
17
|
-
auditLog = [];
|
|
18
|
-
stats = {
|
|
19
|
-
authAttempts: 0,
|
|
20
|
-
authSuccesses: 0,
|
|
21
|
-
authFailures: 0,
|
|
22
|
-
commands: 0,
|
|
23
|
-
fileWrites: 0,
|
|
24
|
-
fileReads: 0,
|
|
25
|
-
sessionStarts: 0,
|
|
26
|
-
sessionEnds: 0,
|
|
27
|
-
userCreated: 0,
|
|
28
|
-
userDeleted: 0,
|
|
29
|
-
clientConnects: 0,
|
|
30
|
-
clientDisconnects: 0,
|
|
31
|
-
};
|
|
32
|
-
maxLogSize;
|
|
33
|
-
/**
|
|
34
|
-
* Creates a new HoneyPot instance.
|
|
35
|
-
*
|
|
36
|
-
* @param maxLogSize Maximum audit log entries to retain (default: 10000).
|
|
37
|
-
*/
|
|
38
|
-
constructor(maxLogSize = 10000) {
|
|
39
|
-
this.maxLogSize = maxLogSize;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Attaches honeypot listeners to all provided event emitters.
|
|
43
|
-
*
|
|
44
|
-
* @param shell VirtualShell instance.
|
|
45
|
-
* @param vfs VirtualFileSystem instance.
|
|
46
|
-
* @param users VirtualUserManager instance.
|
|
47
|
-
* @param ssh SshMimic instance (optional).
|
|
48
|
-
* @param sftp SftpMimic instance (optional).
|
|
49
|
-
*/
|
|
50
|
-
attach(shell, vfs, users, ssh, sftp) {
|
|
51
|
-
this.attachVirtualShell(shell);
|
|
52
|
-
this.attachVirtualFileSystem(vfs);
|
|
53
|
-
this.attachVirtualUserManager(users);
|
|
54
|
-
if (ssh) {
|
|
55
|
-
this.attachSshMimic(ssh);
|
|
56
|
-
}
|
|
57
|
-
if (sftp) {
|
|
58
|
-
this.attachSftpMimic(sftp);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Attaches to VirtualShell events.
|
|
63
|
-
*/
|
|
64
|
-
attachVirtualShell(shell) {
|
|
65
|
-
shell.on("initialized", () => {
|
|
66
|
-
this.log("VirtualShell", "initialized", {});
|
|
67
|
-
});
|
|
68
|
-
shell.on("command", (data) => {
|
|
69
|
-
this.stats.commands++;
|
|
70
|
-
this.log("VirtualShell", "command", data);
|
|
71
|
-
});
|
|
72
|
-
shell.on("session:start", (data) => {
|
|
73
|
-
this.stats.sessionStarts++;
|
|
74
|
-
this.log("VirtualShell", "session:start", data);
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Attaches to VirtualFileSystem events.
|
|
79
|
-
*/
|
|
80
|
-
attachVirtualFileSystem(vfs) {
|
|
81
|
-
vfs.on("file:read", (data) => {
|
|
82
|
-
this.stats.fileReads++;
|
|
83
|
-
this.log("VirtualFileSystem", "file:read", data);
|
|
84
|
-
});
|
|
85
|
-
vfs.on("file:write", (data) => {
|
|
86
|
-
this.stats.fileWrites++;
|
|
87
|
-
this.log("VirtualFileSystem", "file:write", data);
|
|
88
|
-
});
|
|
89
|
-
vfs.on("dir:create", (data) => {
|
|
90
|
-
this.log("VirtualFileSystem", "dir:create", data);
|
|
91
|
-
});
|
|
92
|
-
vfs.on("mirror:flush", () => {
|
|
93
|
-
this.log("VirtualFileSystem", "mirror:flush", {});
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Attaches to VirtualUserManager events.
|
|
98
|
-
*/
|
|
99
|
-
attachVirtualUserManager(users) {
|
|
100
|
-
users.on("initialized", () => {
|
|
101
|
-
this.log("VirtualUserManager", "initialized", {});
|
|
102
|
-
});
|
|
103
|
-
users.on("user:add", (data) => {
|
|
104
|
-
this.stats.userCreated++;
|
|
105
|
-
this.log("VirtualUserManager", "user:add", data);
|
|
106
|
-
});
|
|
107
|
-
users.on("user:delete", (data) => {
|
|
108
|
-
this.stats.userDeleted++;
|
|
109
|
-
this.log("VirtualUserManager", "user:delete", data);
|
|
110
|
-
});
|
|
111
|
-
users.on("session:register", (data) => {
|
|
112
|
-
this.log("VirtualUserManager", "session:register", data);
|
|
113
|
-
});
|
|
114
|
-
users.on("session:unregister", (data) => {
|
|
115
|
-
this.stats.sessionEnds++;
|
|
116
|
-
this.log("VirtualUserManager", "session:unregister", data);
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Attaches to SshMimic events.
|
|
121
|
-
*/
|
|
122
|
-
attachSshMimic(ssh) {
|
|
123
|
-
ssh.on("start", (data) => {
|
|
124
|
-
this.log("SshMimic", "start", data);
|
|
125
|
-
});
|
|
126
|
-
ssh.on("stop", () => {
|
|
127
|
-
this.log("SshMimic", "stop", {});
|
|
128
|
-
});
|
|
129
|
-
ssh.on("auth:success", (data) => {
|
|
130
|
-
this.stats.authAttempts++;
|
|
131
|
-
this.stats.authSuccesses++;
|
|
132
|
-
this.log("SshMimic", "auth:success", data);
|
|
133
|
-
});
|
|
134
|
-
ssh.on("auth:failure", (data) => {
|
|
135
|
-
this.stats.authAttempts++;
|
|
136
|
-
this.stats.authFailures++;
|
|
137
|
-
this.log("SshMimic", "auth:failure", data);
|
|
138
|
-
});
|
|
139
|
-
ssh.on("client:connect", () => {
|
|
140
|
-
this.stats.clientConnects++;
|
|
141
|
-
this.log("SshMimic", "client:connect", {});
|
|
142
|
-
});
|
|
143
|
-
ssh.on("client:disconnect", (data) => {
|
|
144
|
-
this.stats.clientDisconnects++;
|
|
145
|
-
this.log("SshMimic", "client:disconnect", data);
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Attaches to SftpMimic events.
|
|
150
|
-
*/
|
|
151
|
-
attachSftpMimic(sftp) {
|
|
152
|
-
sftp.on("start", (data) => {
|
|
153
|
-
this.log("SftpMimic", "start", data);
|
|
154
|
-
});
|
|
155
|
-
sftp.on("stop", () => {
|
|
156
|
-
this.log("SftpMimic", "stop", {});
|
|
157
|
-
});
|
|
158
|
-
sftp.on("auth:success", (data) => {
|
|
159
|
-
this.stats.authAttempts++;
|
|
160
|
-
this.stats.authSuccesses++;
|
|
161
|
-
this.log("SftpMimic", "auth:success", data);
|
|
162
|
-
});
|
|
163
|
-
sftp.on("auth:failure", (data) => {
|
|
164
|
-
this.stats.authAttempts++;
|
|
165
|
-
this.stats.authFailures++;
|
|
166
|
-
this.log("SftpMimic", "auth:failure", data);
|
|
167
|
-
});
|
|
168
|
-
sftp.on("client:connect", () => {
|
|
169
|
-
this.stats.clientConnects++;
|
|
170
|
-
this.log("SftpMimic", "client:connect", {});
|
|
171
|
-
});
|
|
172
|
-
sftp.on("client:disconnect", (data) => {
|
|
173
|
-
this.stats.clientDisconnects++;
|
|
174
|
-
this.log("SftpMimic", "client:disconnect", data);
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Records an audit log entry.
|
|
179
|
-
*
|
|
180
|
-
* @param source Event source (e.g., "SshMimic", "VirtualFileSystem").
|
|
181
|
-
* @param type Event type.
|
|
182
|
-
* @param details Event-specific data.
|
|
183
|
-
*/
|
|
184
|
-
log(source, type, details) {
|
|
185
|
-
const entry = {
|
|
186
|
-
timestamp: new Date().toISOString(),
|
|
187
|
-
type,
|
|
188
|
-
source,
|
|
189
|
-
details,
|
|
190
|
-
};
|
|
191
|
-
this.auditLog.push(entry);
|
|
192
|
-
// Trim log if exceeds max size
|
|
193
|
-
if (this.auditLog.length > this.maxLogSize) {
|
|
194
|
-
this.auditLog = this.auditLog.slice(-this.maxLogSize);
|
|
195
|
-
}
|
|
196
|
-
// Console output for real-time monitoring
|
|
197
|
-
console.log(`[AUDIT] ${entry.timestamp} | ${source} | ${type}`, details);
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Returns audit log entries matching optional filters.
|
|
201
|
-
*
|
|
202
|
-
* @param type Optional event type filter.
|
|
203
|
-
* @param source Optional source filter.
|
|
204
|
-
* @returns Filtered audit log entries.
|
|
205
|
-
*/
|
|
206
|
-
getAuditLog(type, source) {
|
|
207
|
-
return this.auditLog.filter((entry) => (!type || entry.type === type) && (!source || entry.source === source));
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Returns current activity statistics.
|
|
211
|
-
*
|
|
212
|
-
* @returns Snapshot of honeypot stats.
|
|
213
|
-
*/
|
|
214
|
-
getStats() {
|
|
215
|
-
return Object.freeze({ ...this.stats });
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Clears audit log and resets statistics.
|
|
219
|
-
*/
|
|
220
|
-
reset() {
|
|
221
|
-
this.auditLog = [];
|
|
222
|
-
this.stats = {
|
|
223
|
-
authAttempts: 0,
|
|
224
|
-
authSuccesses: 0,
|
|
225
|
-
authFailures: 0,
|
|
226
|
-
commands: 0,
|
|
227
|
-
fileWrites: 0,
|
|
228
|
-
fileReads: 0,
|
|
229
|
-
sessionStarts: 0,
|
|
230
|
-
sessionEnds: 0,
|
|
231
|
-
userCreated: 0,
|
|
232
|
-
userDeleted: 0,
|
|
233
|
-
clientConnects: 0,
|
|
234
|
-
clientDisconnects: 0,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Returns recent log entries in reverse chronological order.
|
|
239
|
-
*
|
|
240
|
-
* @param limit Number of recent entries to return (default: 100).
|
|
241
|
-
* @returns Recent audit log entries.
|
|
242
|
-
*/
|
|
243
|
-
getRecent(limit = 100) {
|
|
244
|
-
return this.auditLog.slice(Math.max(0, this.auditLog.length - limit));
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Detects potential security issues based on activity patterns.
|
|
248
|
-
*
|
|
249
|
-
* @returns Array of anomalies detected.
|
|
250
|
-
*/
|
|
251
|
-
detectAnomalies() {
|
|
252
|
-
const anomalies = [];
|
|
253
|
-
// High auth failure rate
|
|
254
|
-
if (this.stats.authAttempts > 0 &&
|
|
255
|
-
this.stats.authFailures / this.stats.authAttempts > 0.5) {
|
|
256
|
-
anomalies.push({
|
|
257
|
-
type: "high_auth_failure_rate",
|
|
258
|
-
severity: "medium",
|
|
259
|
-
message: `Auth failure rate: ${((this.stats.authFailures / this.stats.authAttempts) * 100).toFixed(1)}%`,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
// Excessive auth failures in short time
|
|
263
|
-
if (this.stats.authFailures > 10) {
|
|
264
|
-
anomalies.push({
|
|
265
|
-
type: "excessive_auth_failures",
|
|
266
|
-
severity: "high",
|
|
267
|
-
message: `${this.stats.authFailures} authentication failures detected`,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
// Unusual command execution volume
|
|
271
|
-
if (this.stats.commands > 1000) {
|
|
272
|
-
anomalies.push({
|
|
273
|
-
type: "high_command_volume",
|
|
274
|
-
severity: "low",
|
|
275
|
-
message: `${this.stats.commands} commands executed`,
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
// Unusual file write volume
|
|
279
|
-
if (this.stats.fileWrites > 500) {
|
|
280
|
-
anomalies.push({
|
|
281
|
-
type: "high_write_volume",
|
|
282
|
-
severity: "medium",
|
|
283
|
-
message: `${this.stats.fileWrites} file write operations`,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
return anomalies;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
export default HoneyPot;
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import * as tarStream from "tar-stream";
|
|
3
|
-
import type { VfsSnapshot } from "../types/vfs";
|
|
4
|
-
|
|
5
|
-
export async function archiveExists(archivePath: string): Promise<boolean> {
|
|
6
|
-
try {
|
|
7
|
-
await fs.access(archivePath);
|
|
8
|
-
return true;
|
|
9
|
-
} catch {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function createTarBuffer(snapshotJson: string): Promise<Buffer> {
|
|
15
|
-
const pack = tarStream.pack();
|
|
16
|
-
const chunks: Buffer[] = [];
|
|
17
|
-
|
|
18
|
-
const finished = new Promise<Buffer>((resolve, reject) => {
|
|
19
|
-
pack.on("data", (chunk: Buffer) => chunks.push(Buffer.from(chunk)));
|
|
20
|
-
pack.on("error", reject);
|
|
21
|
-
pack.on("end", () => resolve(Buffer.concat(chunks)));
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
pack.entry(
|
|
25
|
-
{ name: "snapshot.json", mode: 0o600 },
|
|
26
|
-
snapshotJson,
|
|
27
|
-
(error?: Error | null) => {
|
|
28
|
-
if (error) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
pack.finalize();
|
|
33
|
-
},
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
return finished;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function readSnapshotFromTar(
|
|
40
|
-
tarBuffer: Buffer,
|
|
41
|
-
): Promise<VfsSnapshot> {
|
|
42
|
-
return new Promise<VfsSnapshot>((resolve, reject) => {
|
|
43
|
-
const extract = tarStream.extract();
|
|
44
|
-
let snapshotText = "";
|
|
45
|
-
let found = false;
|
|
46
|
-
|
|
47
|
-
extract.on("entry", (header, stream, next) => {
|
|
48
|
-
if (header.name === "snapshot.json") {
|
|
49
|
-
found = true;
|
|
50
|
-
stream.on("data", (chunk: Buffer) => {
|
|
51
|
-
snapshotText += chunk.toString("utf8");
|
|
52
|
-
});
|
|
53
|
-
stream.on("end", next);
|
|
54
|
-
stream.resume();
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
stream.resume();
|
|
59
|
-
stream.on("end", next);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
extract.on("finish", () => {
|
|
63
|
-
if (!found) {
|
|
64
|
-
reject(new Error("snapshot.json missing from archive"));
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
resolve(JSON.parse(snapshotText) as VfsSnapshot);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
extract.on("error", reject);
|
|
72
|
-
extract.end(tarBuffer);
|
|
73
|
-
});
|
|
74
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
VfsSnapshot,
|
|
3
|
-
VfsSnapshotDirectoryNode,
|
|
4
|
-
VfsSnapshotNode,
|
|
5
|
-
} from "../types/vfs";
|
|
6
|
-
import type { InternalDirectoryNode, InternalNode } from "./internalTypes";
|
|
7
|
-
|
|
8
|
-
function serializeNode(node: InternalNode): VfsSnapshotNode {
|
|
9
|
-
if (node.type === "file") {
|
|
10
|
-
return {
|
|
11
|
-
type: "file",
|
|
12
|
-
name: node.name,
|
|
13
|
-
mode: node.mode,
|
|
14
|
-
createdAt: node.createdAt.toISOString(),
|
|
15
|
-
updatedAt: node.updatedAt.toISOString(),
|
|
16
|
-
compressed: node.compressed,
|
|
17
|
-
contentBase64: node.content.toString("base64"),
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return serializeDirectory(node);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function serializeDirectory(
|
|
25
|
-
node: InternalDirectoryNode,
|
|
26
|
-
): VfsSnapshotDirectoryNode {
|
|
27
|
-
return {
|
|
28
|
-
type: "directory",
|
|
29
|
-
name: node.name,
|
|
30
|
-
mode: node.mode,
|
|
31
|
-
createdAt: node.createdAt.toISOString(),
|
|
32
|
-
updatedAt: node.updatedAt.toISOString(),
|
|
33
|
-
children: Array.from(node.children.values()).map((child) =>
|
|
34
|
-
serializeNode(child),
|
|
35
|
-
),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function deserializeNode(node: VfsSnapshotNode): InternalNode {
|
|
40
|
-
if (node.type === "file") {
|
|
41
|
-
return {
|
|
42
|
-
type: "file",
|
|
43
|
-
name: node.name,
|
|
44
|
-
mode: node.mode,
|
|
45
|
-
createdAt: new Date(node.createdAt),
|
|
46
|
-
updatedAt: new Date(node.updatedAt),
|
|
47
|
-
content: Buffer.from(node.contentBase64, "base64"),
|
|
48
|
-
compressed: node.compressed,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return deserializeDirectory(node);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function deserializeDirectory(
|
|
56
|
-
node: VfsSnapshotDirectoryNode,
|
|
57
|
-
): InternalDirectoryNode {
|
|
58
|
-
return {
|
|
59
|
-
type: "directory",
|
|
60
|
-
name: node.name,
|
|
61
|
-
mode: node.mode,
|
|
62
|
-
createdAt: new Date(node.createdAt),
|
|
63
|
-
updatedAt: new Date(node.updatedAt),
|
|
64
|
-
children: new Map<string, InternalNode>(
|
|
65
|
-
node.children.map((child) => [child.name, deserializeNode(child)]),
|
|
66
|
-
),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function createSnapshot(root: InternalDirectoryNode): VfsSnapshot {
|
|
71
|
-
return { root: serializeDirectory(root) };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function applySnapshot(
|
|
75
|
-
rootTarget: InternalDirectoryNode,
|
|
76
|
-
snapshot: VfsSnapshot,
|
|
77
|
-
): void {
|
|
78
|
-
const root = deserializeDirectory(snapshot.root);
|
|
79
|
-
rootTarget.name = root.name;
|
|
80
|
-
rootTarget.mode = root.mode;
|
|
81
|
-
rootTarget.createdAt = root.createdAt;
|
|
82
|
-
rootTarget.updatedAt = root.updatedAt;
|
|
83
|
-
rootTarget.children = root.children;
|
|
84
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { InternalDirectoryNode } from "./internalTypes";
|
|
2
|
-
|
|
3
|
-
function walkTree(
|
|
4
|
-
node: InternalDirectoryNode,
|
|
5
|
-
indent: string,
|
|
6
|
-
lines: string[],
|
|
7
|
-
): void {
|
|
8
|
-
const entries = Array.from(node.children.entries()).sort(([a], [b]) =>
|
|
9
|
-
a.localeCompare(b),
|
|
10
|
-
);
|
|
11
|
-
|
|
12
|
-
entries.forEach(([name, child], index) => {
|
|
13
|
-
const isLast = index === entries.length - 1;
|
|
14
|
-
const branch = isLast ? "`-- " : "|-- ";
|
|
15
|
-
const nextIndent = indent + (isLast ? " " : "| ");
|
|
16
|
-
|
|
17
|
-
if (child.type === "file") {
|
|
18
|
-
lines.push(`${indent}${branch}${name}${child.compressed ? " [gz]" : ""}`);
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
lines.push(`${indent}${branch}${name}/`);
|
|
23
|
-
walkTree(child, nextIndent, lines);
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function renderTree(
|
|
28
|
-
node: InternalDirectoryNode,
|
|
29
|
-
rootLabel: string,
|
|
30
|
-
): string {
|
|
31
|
-
const lines: string[] = [rootLabel];
|
|
32
|
-
walkTree(node, "", lines);
|
|
33
|
-
return lines.join("\n");
|
|
34
|
-
}
|