windows-exe-decompiler-mcp-server 0.1.0

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 (190) hide show
  1. package/CODEX_INSTALLATION.md +69 -0
  2. package/COPILOT_INSTALLATION.md +77 -0
  3. package/LICENSE +21 -0
  4. package/README.md +314 -0
  5. package/bin/windows-exe-decompiler-mcp-server.js +3 -0
  6. package/dist/analysis-provenance.d.ts +184 -0
  7. package/dist/analysis-provenance.js +74 -0
  8. package/dist/analysis-task-runner.d.ts +31 -0
  9. package/dist/analysis-task-runner.js +160 -0
  10. package/dist/artifact-inventory.d.ts +23 -0
  11. package/dist/artifact-inventory.js +175 -0
  12. package/dist/cache-manager.d.ts +128 -0
  13. package/dist/cache-manager.js +454 -0
  14. package/dist/confidence-semantics.d.ts +66 -0
  15. package/dist/confidence-semantics.js +122 -0
  16. package/dist/config.d.ts +335 -0
  17. package/dist/config.js +193 -0
  18. package/dist/database.d.ts +227 -0
  19. package/dist/database.js +601 -0
  20. package/dist/decompiler-worker.d.ts +441 -0
  21. package/dist/decompiler-worker.js +1962 -0
  22. package/dist/dynamic-trace.d.ts +95 -0
  23. package/dist/dynamic-trace.js +629 -0
  24. package/dist/env-validator.d.ts +15 -0
  25. package/dist/env-validator.js +249 -0
  26. package/dist/error-handler.d.ts +28 -0
  27. package/dist/error-handler.example.d.ts +22 -0
  28. package/dist/error-handler.example.js +141 -0
  29. package/dist/error-handler.js +139 -0
  30. package/dist/ghidra-analysis-status.d.ts +49 -0
  31. package/dist/ghidra-analysis-status.js +178 -0
  32. package/dist/ghidra-config.d.ts +134 -0
  33. package/dist/ghidra-config.js +464 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/index.js +200 -0
  36. package/dist/job-queue.d.ts +169 -0
  37. package/dist/job-queue.js +407 -0
  38. package/dist/logger.d.ts +106 -0
  39. package/dist/logger.js +176 -0
  40. package/dist/policy-guard.d.ts +115 -0
  41. package/dist/policy-guard.js +243 -0
  42. package/dist/process-output.d.ts +15 -0
  43. package/dist/process-output.js +90 -0
  44. package/dist/prompts/function-explanation-review.d.ts +5 -0
  45. package/dist/prompts/function-explanation-review.js +64 -0
  46. package/dist/prompts/semantic-name-review.d.ts +5 -0
  47. package/dist/prompts/semantic-name-review.js +63 -0
  48. package/dist/runtime-correlation.d.ts +34 -0
  49. package/dist/runtime-correlation.js +279 -0
  50. package/dist/runtime-paths.d.ts +3 -0
  51. package/dist/runtime-paths.js +11 -0
  52. package/dist/selection-diff.d.ts +667 -0
  53. package/dist/selection-diff.js +53 -0
  54. package/dist/semantic-name-suggestion-artifacts.d.ts +116 -0
  55. package/dist/semantic-name-suggestion-artifacts.js +314 -0
  56. package/dist/server.d.ts +129 -0
  57. package/dist/server.js +578 -0
  58. package/dist/tools/artifact-read.d.ts +235 -0
  59. package/dist/tools/artifact-read.js +317 -0
  60. package/dist/tools/artifacts-diff.d.ts +728 -0
  61. package/dist/tools/artifacts-diff.js +304 -0
  62. package/dist/tools/artifacts-list.d.ts +515 -0
  63. package/dist/tools/artifacts-list.js +389 -0
  64. package/dist/tools/attack-map.d.ts +290 -0
  65. package/dist/tools/attack-map.js +519 -0
  66. package/dist/tools/cache-observability.d.ts +4 -0
  67. package/dist/tools/cache-observability.js +36 -0
  68. package/dist/tools/code-function-cfg.d.ts +50 -0
  69. package/dist/tools/code-function-cfg.js +102 -0
  70. package/dist/tools/code-function-decompile.d.ts +55 -0
  71. package/dist/tools/code-function-decompile.js +103 -0
  72. package/dist/tools/code-function-disassemble.d.ts +43 -0
  73. package/dist/tools/code-function-disassemble.js +185 -0
  74. package/dist/tools/code-function-explain-apply.d.ts +255 -0
  75. package/dist/tools/code-function-explain-apply.js +225 -0
  76. package/dist/tools/code-function-explain-prepare.d.ts +535 -0
  77. package/dist/tools/code-function-explain-prepare.js +276 -0
  78. package/dist/tools/code-function-explain-review.d.ts +397 -0
  79. package/dist/tools/code-function-explain-review.js +589 -0
  80. package/dist/tools/code-function-rename-apply.d.ts +248 -0
  81. package/dist/tools/code-function-rename-apply.js +220 -0
  82. package/dist/tools/code-function-rename-prepare.d.ts +506 -0
  83. package/dist/tools/code-function-rename-prepare.js +279 -0
  84. package/dist/tools/code-function-rename-review.d.ts +574 -0
  85. package/dist/tools/code-function-rename-review.js +761 -0
  86. package/dist/tools/code-functions-list.d.ts +37 -0
  87. package/dist/tools/code-functions-list.js +91 -0
  88. package/dist/tools/code-functions-rank.d.ts +34 -0
  89. package/dist/tools/code-functions-rank.js +90 -0
  90. package/dist/tools/code-functions-reconstruct.d.ts +2725 -0
  91. package/dist/tools/code-functions-reconstruct.js +2807 -0
  92. package/dist/tools/code-functions-search.d.ts +39 -0
  93. package/dist/tools/code-functions-search.js +90 -0
  94. package/dist/tools/code-reconstruct-export.d.ts +1212 -0
  95. package/dist/tools/code-reconstruct-export.js +4002 -0
  96. package/dist/tools/code-reconstruct-plan.d.ts +274 -0
  97. package/dist/tools/code-reconstruct-plan.js +342 -0
  98. package/dist/tools/dotnet-metadata-extract.d.ts +541 -0
  99. package/dist/tools/dotnet-metadata-extract.js +355 -0
  100. package/dist/tools/dotnet-reconstruct-export.d.ts +567 -0
  101. package/dist/tools/dotnet-reconstruct-export.js +1151 -0
  102. package/dist/tools/dotnet-types-list.d.ts +325 -0
  103. package/dist/tools/dotnet-types-list.js +201 -0
  104. package/dist/tools/dynamic-dependencies.d.ts +115 -0
  105. package/dist/tools/dynamic-dependencies.js +213 -0
  106. package/dist/tools/dynamic-memory-import.d.ts +10 -0
  107. package/dist/tools/dynamic-memory-import.js +567 -0
  108. package/dist/tools/dynamic-trace-import.d.ts +10 -0
  109. package/dist/tools/dynamic-trace-import.js +235 -0
  110. package/dist/tools/entrypoint-fallback-disasm.d.ts +30 -0
  111. package/dist/tools/entrypoint-fallback-disasm.js +89 -0
  112. package/dist/tools/ghidra-analyze.d.ts +88 -0
  113. package/dist/tools/ghidra-analyze.js +208 -0
  114. package/dist/tools/ghidra-health.d.ts +37 -0
  115. package/dist/tools/ghidra-health.js +212 -0
  116. package/dist/tools/ioc-export.d.ts +209 -0
  117. package/dist/tools/ioc-export.js +542 -0
  118. package/dist/tools/packer-detect.d.ts +165 -0
  119. package/dist/tools/packer-detect.js +284 -0
  120. package/dist/tools/pe-exports-extract.d.ts +175 -0
  121. package/dist/tools/pe-exports-extract.js +253 -0
  122. package/dist/tools/pe-fingerprint.d.ts +234 -0
  123. package/dist/tools/pe-fingerprint.js +269 -0
  124. package/dist/tools/pe-imports-extract.d.ts +105 -0
  125. package/dist/tools/pe-imports-extract.js +245 -0
  126. package/dist/tools/report-generate.d.ts +157 -0
  127. package/dist/tools/report-generate.js +457 -0
  128. package/dist/tools/report-summarize.d.ts +2131 -0
  129. package/dist/tools/report-summarize.js +596 -0
  130. package/dist/tools/runtime-detect.d.ts +135 -0
  131. package/dist/tools/runtime-detect.js +247 -0
  132. package/dist/tools/sample-ingest.d.ts +94 -0
  133. package/dist/tools/sample-ingest.js +327 -0
  134. package/dist/tools/sample-profile-get.d.ts +183 -0
  135. package/dist/tools/sample-profile-get.js +121 -0
  136. package/dist/tools/sandbox-execute.d.ts +441 -0
  137. package/dist/tools/sandbox-execute.js +392 -0
  138. package/dist/tools/strings-extract.d.ts +375 -0
  139. package/dist/tools/strings-extract.js +314 -0
  140. package/dist/tools/strings-floss-decode.d.ts +143 -0
  141. package/dist/tools/strings-floss-decode.js +259 -0
  142. package/dist/tools/system-health.d.ts +434 -0
  143. package/dist/tools/system-health.js +446 -0
  144. package/dist/tools/task-cancel.d.ts +21 -0
  145. package/dist/tools/task-cancel.js +70 -0
  146. package/dist/tools/task-status.d.ts +27 -0
  147. package/dist/tools/task-status.js +106 -0
  148. package/dist/tools/task-sweep.d.ts +22 -0
  149. package/dist/tools/task-sweep.js +77 -0
  150. package/dist/tools/tool-help.d.ts +340 -0
  151. package/dist/tools/tool-help.js +261 -0
  152. package/dist/tools/yara-scan.d.ts +554 -0
  153. package/dist/tools/yara-scan.js +313 -0
  154. package/dist/types.d.ts +266 -0
  155. package/dist/types.js +41 -0
  156. package/dist/worker-pool.d.ts +204 -0
  157. package/dist/worker-pool.js +650 -0
  158. package/dist/workflows/deep-static.d.ts +104 -0
  159. package/dist/workflows/deep-static.js +276 -0
  160. package/dist/workflows/function-explanation-review.d.ts +655 -0
  161. package/dist/workflows/function-explanation-review.js +440 -0
  162. package/dist/workflows/reconstruct.d.ts +2053 -0
  163. package/dist/workflows/reconstruct.js +666 -0
  164. package/dist/workflows/semantic-name-review.d.ts +2418 -0
  165. package/dist/workflows/semantic-name-review.js +521 -0
  166. package/dist/workflows/triage.d.ts +659 -0
  167. package/dist/workflows/triage.js +1374 -0
  168. package/dist/workspace-manager.d.ts +150 -0
  169. package/dist/workspace-manager.js +411 -0
  170. package/ghidra_scripts/DecompileFunction.java +487 -0
  171. package/ghidra_scripts/DecompileFunction.py +150 -0
  172. package/ghidra_scripts/ExtractCFG.java +256 -0
  173. package/ghidra_scripts/ExtractCFG.py +233 -0
  174. package/ghidra_scripts/ExtractFunctions.java +442 -0
  175. package/ghidra_scripts/ExtractFunctions.py +101 -0
  176. package/ghidra_scripts/README.md +125 -0
  177. package/ghidra_scripts/SearchFunctionReferences.java +380 -0
  178. package/helpers/DotNetMetadataProbe/DotNetMetadataProbe.csproj +9 -0
  179. package/helpers/DotNetMetadataProbe/Program.cs +566 -0
  180. package/install-to-codex.ps1 +178 -0
  181. package/install-to-copilot.ps1 +303 -0
  182. package/package.json +101 -0
  183. package/requirements.txt +9 -0
  184. package/workers/requirements-dynamic.txt +11 -0
  185. package/workers/requirements.txt +8 -0
  186. package/workers/speakeasy_compat.py +175 -0
  187. package/workers/static_worker.py +5183 -0
  188. package/workers/yara_rules/default.yar +33 -0
  189. package/workers/yara_rules/malware_families.yar +93 -0
  190. package/workers/yara_rules/packers.yar +80 -0
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Workspace Manager
3
+ * Manages sample storage with SHA256-based bucketed directories
4
+ * Implements path normalization and security boundary checks
5
+ * Optimized for file I/O performance
6
+ */
7
+ import type { WorkspacePath } from './types.js';
8
+ /**
9
+ * WorkspaceManager handles creation and management of sample workspaces
10
+ * Each workspace is organized in a bucketed directory structure based on SHA256
11
+ *
12
+ * Performance optimizations (Requirement 26.3):
13
+ * - Async file operations where possible
14
+ * - Cached workspace path lookups
15
+ * - Batch directory creation
16
+ */
17
+ export declare class WorkspaceManager {
18
+ private workspaceRoot;
19
+ private workspacePathCache;
20
+ private readonly CACHE_SIZE_LIMIT;
21
+ constructor(workspaceRoot: string);
22
+ /**
23
+ * Ensure workspace root directory exists
24
+ */
25
+ private ensureWorkspaceRoot;
26
+ /**
27
+ * Create workspace directory structure for a sample
28
+ * Uses SHA256-based bucketing: workspaces/ab/cd/<sha256>/
29
+ *
30
+ * Requirements: 19.1, 19.2, 26.3 (file I/O optimization)
31
+ *
32
+ * Optimizations:
33
+ * - Async directory creation
34
+ * - Cached workspace paths
35
+ * - Batch subdirectory creation
36
+ *
37
+ * @param sampleId - Sample ID in format "sha256:<hex>"
38
+ * @returns WorkspacePath with all subdirectory paths
39
+ */
40
+ createWorkspace(sampleId: string): Promise<WorkspacePath>;
41
+ /**
42
+ * Get workspace path for an existing sample
43
+ *
44
+ * Requirements: 26.3 (file I/O optimization - cached lookups)
45
+ *
46
+ * @param sampleId - Sample ID in format "sha256:<hex>"
47
+ * @returns WorkspacePath with all subdirectory paths
48
+ */
49
+ getWorkspace(sampleId: string): Promise<WorkspacePath>;
50
+ /**
51
+ * Cache workspace path for faster lookups
52
+ * Implements LRU eviction when cache size limit is reached
53
+ *
54
+ * Requirements: 26.3 (file I/O optimization)
55
+ *
56
+ * @param sha256 - SHA256 hash
57
+ * @param workspacePath - Workspace path to cache
58
+ */
59
+ private cacheWorkspacePath;
60
+ /**
61
+ * Clear workspace path cache
62
+ * Useful after cleanup operations
63
+ */
64
+ clearWorkspaceCache(): void;
65
+ /**
66
+ * Extract SHA256 hash from sample ID
67
+ *
68
+ * @param sampleId - Sample ID in format "sha256:<hex>"
69
+ * @returns SHA256 hash string
70
+ */
71
+ private extractSha256;
72
+ /**
73
+ * Generate bucketed workspace path from SHA256
74
+ * Structure: workspaces/ab/cd/<sha256>/
75
+ *
76
+ * @param sha256 - SHA256 hash string
77
+ * @returns WorkspacePath object with all subdirectory paths
78
+ */
79
+ private generateWorkspacePath;
80
+ /**
81
+ * Normalize and validate a path is within workspace boundaries
82
+ * Prevents path traversal attacks
83
+ *
84
+ * Requirement: 29.5
85
+ *
86
+ * @param workspacePath - Base workspace path
87
+ * @param relativePath - Relative path to validate
88
+ * @returns Normalized absolute path
89
+ * @throws Error if path is outside workspace boundaries
90
+ */
91
+ normalizePath(workspacePath: string, relativePath: string): string;
92
+ /**
93
+ * Check if a path is within workspace boundaries
94
+ *
95
+ * Requirement: 29.5
96
+ *
97
+ * @param workspacePath - Base workspace path
98
+ * @param targetPath - Path to check
99
+ * @returns true if path is within boundaries
100
+ */
101
+ isWithinBoundaries(workspacePath: string, targetPath: string): boolean;
102
+ /**
103
+ * Get workspace root directory
104
+ */
105
+ getWorkspaceRoot(): string;
106
+ /**
107
+ * Clean up workspace for a sample
108
+ * Deletes all subdirectories and files
109
+ *
110
+ * Requirements: 19.6, 26.3 (file I/O optimization)
111
+ *
112
+ * @param sampleId - Sample ID in format "sha256:<hex>"
113
+ */
114
+ cleanup(sampleId: string): Promise<void>;
115
+ /**
116
+ * Clean up old workspaces based on retention policy
117
+ * Deletes workspaces older than the specified number of days
118
+ *
119
+ * Requirements: 操作约束 6 (30-day retention policy), 26.3 (file I/O optimization)
120
+ *
121
+ * Optimizations:
122
+ * - Async file operations
123
+ * - Parallel directory scanning
124
+ * - Batch deletion
125
+ *
126
+ * @param retentionDays - Number of days to retain workspaces (default: 30)
127
+ * @returns Number of workspaces cleaned up
128
+ */
129
+ cleanupOldWorkspaces(retentionDays?: number): Promise<number>;
130
+ /**
131
+ * Get workspace statistics for monitoring
132
+ * Requirements: 26.3 (file I/O optimization)
133
+ *
134
+ * @returns Object with workspace statistics
135
+ */
136
+ getWorkspaceStats(): Promise<{
137
+ totalWorkspaces: number;
138
+ totalSizeBytes: number;
139
+ oldestWorkspaceAge: number;
140
+ }>;
141
+ /**
142
+ * Get approximate size of a directory
143
+ * Requirements: 26.3 (file I/O optimization)
144
+ *
145
+ * @param dirPath - Directory path
146
+ * @returns Size in bytes
147
+ */
148
+ private getDirectorySize;
149
+ }
150
+ //# sourceMappingURL=workspace-manager.d.ts.map
@@ -0,0 +1,411 @@
1
+ /**
2
+ * Workspace Manager
3
+ * Manages sample storage with SHA256-based bucketed directories
4
+ * Implements path normalization and security boundary checks
5
+ * Optimized for file I/O performance
6
+ */
7
+ import fs from 'fs';
8
+ import fsPromises from 'fs/promises';
9
+ import path from 'path';
10
+ /**
11
+ * WorkspaceManager handles creation and management of sample workspaces
12
+ * Each workspace is organized in a bucketed directory structure based on SHA256
13
+ *
14
+ * Performance optimizations (Requirement 26.3):
15
+ * - Async file operations where possible
16
+ * - Cached workspace path lookups
17
+ * - Batch directory creation
18
+ */
19
+ export class WorkspaceManager {
20
+ workspaceRoot;
21
+ workspacePathCache = new Map();
22
+ CACHE_SIZE_LIMIT = 1000;
23
+ constructor(workspaceRoot) {
24
+ this.workspaceRoot = path.resolve(workspaceRoot);
25
+ this.ensureWorkspaceRoot();
26
+ }
27
+ /**
28
+ * Ensure workspace root directory exists
29
+ */
30
+ ensureWorkspaceRoot() {
31
+ if (!fs.existsSync(this.workspaceRoot)) {
32
+ fs.mkdirSync(this.workspaceRoot, { recursive: true });
33
+ }
34
+ }
35
+ /**
36
+ * Create workspace directory structure for a sample
37
+ * Uses SHA256-based bucketing: workspaces/ab/cd/<sha256>/
38
+ *
39
+ * Requirements: 19.1, 19.2, 26.3 (file I/O optimization)
40
+ *
41
+ * Optimizations:
42
+ * - Async directory creation
43
+ * - Cached workspace paths
44
+ * - Batch subdirectory creation
45
+ *
46
+ * @param sampleId - Sample ID in format "sha256:<hex>"
47
+ * @returns WorkspacePath with all subdirectory paths
48
+ */
49
+ async createWorkspace(sampleId) {
50
+ const sha256 = this.extractSha256(sampleId);
51
+ // Check cache first (Requirement 26.3)
52
+ const cachedPath = this.workspacePathCache.get(sha256);
53
+ if (cachedPath && fs.existsSync(cachedPath.root)) {
54
+ return cachedPath;
55
+ }
56
+ const workspacePath = this.generateWorkspacePath(sha256);
57
+ // Create directory structure asynchronously
58
+ const subdirs = ['original', 'cache', 'ghidra', 'dotnet', 'reports'];
59
+ // Create root workspace directory
60
+ await fsPromises.mkdir(workspacePath.root, { recursive: true });
61
+ // Create all subdirectories in parallel (Requirement 26.3)
62
+ await Promise.all(subdirs.map(subdir => fsPromises.mkdir(path.join(workspacePath.root, subdir), { recursive: true })));
63
+ // Cache the workspace path
64
+ this.cacheWorkspacePath(sha256, workspacePath);
65
+ return workspacePath;
66
+ }
67
+ /**
68
+ * Get workspace path for an existing sample
69
+ *
70
+ * Requirements: 26.3 (file I/O optimization - cached lookups)
71
+ *
72
+ * @param sampleId - Sample ID in format "sha256:<hex>"
73
+ * @returns WorkspacePath with all subdirectory paths
74
+ */
75
+ async getWorkspace(sampleId) {
76
+ const sha256 = this.extractSha256(sampleId);
77
+ // Check cache first (Requirement 26.3)
78
+ const cachedPath = this.workspacePathCache.get(sha256);
79
+ if (cachedPath) {
80
+ return cachedPath;
81
+ }
82
+ const workspacePath = this.generateWorkspacePath(sha256);
83
+ // Verify workspace exists
84
+ if (!fs.existsSync(workspacePath.root)) {
85
+ throw new Error(`Workspace not found for sample: ${sampleId}`);
86
+ }
87
+ // Cache the workspace path
88
+ this.cacheWorkspacePath(sha256, workspacePath);
89
+ return workspacePath;
90
+ }
91
+ /**
92
+ * Cache workspace path for faster lookups
93
+ * Implements LRU eviction when cache size limit is reached
94
+ *
95
+ * Requirements: 26.3 (file I/O optimization)
96
+ *
97
+ * @param sha256 - SHA256 hash
98
+ * @param workspacePath - Workspace path to cache
99
+ */
100
+ cacheWorkspacePath(sha256, workspacePath) {
101
+ // Evict oldest entry if cache is full
102
+ if (this.workspacePathCache.size >= this.CACHE_SIZE_LIMIT) {
103
+ const firstKey = this.workspacePathCache.keys().next().value;
104
+ if (firstKey) {
105
+ this.workspacePathCache.delete(firstKey);
106
+ }
107
+ }
108
+ this.workspacePathCache.set(sha256, workspacePath);
109
+ }
110
+ /**
111
+ * Clear workspace path cache
112
+ * Useful after cleanup operations
113
+ */
114
+ clearWorkspaceCache() {
115
+ this.workspacePathCache.clear();
116
+ }
117
+ /**
118
+ * Extract SHA256 hash from sample ID
119
+ *
120
+ * @param sampleId - Sample ID in format "sha256:<hex>"
121
+ * @returns SHA256 hash string
122
+ */
123
+ extractSha256(sampleId) {
124
+ if (!sampleId.startsWith('sha256:')) {
125
+ throw new Error(`Invalid sample ID format: ${sampleId}`);
126
+ }
127
+ const sha256 = sampleId.substring(7);
128
+ // Validate SHA256 format (64 hex characters)
129
+ if (!/^[a-f0-9]{64}$/i.test(sha256)) {
130
+ throw new Error(`Invalid SHA256 hash: ${sha256}`);
131
+ }
132
+ return sha256.toLowerCase();
133
+ }
134
+ /**
135
+ * Generate bucketed workspace path from SHA256
136
+ * Structure: workspaces/ab/cd/<sha256>/
137
+ *
138
+ * @param sha256 - SHA256 hash string
139
+ * @returns WorkspacePath object with all subdirectory paths
140
+ */
141
+ generateWorkspacePath(sha256) {
142
+ // Use first 2 and next 2 characters for bucketing
143
+ const bucket1 = sha256.substring(0, 2);
144
+ const bucket2 = sha256.substring(2, 4);
145
+ const root = path.join(this.workspaceRoot, bucket1, bucket2, sha256);
146
+ return {
147
+ root,
148
+ original: path.join(root, 'original'),
149
+ cache: path.join(root, 'cache'),
150
+ ghidra: path.join(root, 'ghidra'),
151
+ reports: path.join(root, 'reports'),
152
+ };
153
+ }
154
+ /**
155
+ * Normalize and validate a path is within workspace boundaries
156
+ * Prevents path traversal attacks
157
+ *
158
+ * Requirement: 29.5
159
+ *
160
+ * @param workspacePath - Base workspace path
161
+ * @param relativePath - Relative path to validate
162
+ * @returns Normalized absolute path
163
+ * @throws Error if path is outside workspace boundaries
164
+ */
165
+ normalizePath(workspacePath, relativePath) {
166
+ // Resolve to absolute path
167
+ const absolutePath = path.resolve(workspacePath, relativePath);
168
+ const normalizedPath = path.normalize(absolutePath);
169
+ // Check if path is within workspace boundaries
170
+ const workspaceAbsolute = path.resolve(workspacePath);
171
+ if (!normalizedPath.startsWith(workspaceAbsolute + path.sep) &&
172
+ normalizedPath !== workspaceAbsolute) {
173
+ throw new Error(`Path traversal detected: ${relativePath} is outside workspace boundaries`);
174
+ }
175
+ return normalizedPath;
176
+ }
177
+ /**
178
+ * Check if a path is within workspace boundaries
179
+ *
180
+ * Requirement: 29.5
181
+ *
182
+ * @param workspacePath - Base workspace path
183
+ * @param targetPath - Path to check
184
+ * @returns true if path is within boundaries
185
+ */
186
+ isWithinBoundaries(workspacePath, targetPath) {
187
+ try {
188
+ this.normalizePath(workspacePath, targetPath);
189
+ return true;
190
+ }
191
+ catch {
192
+ return false;
193
+ }
194
+ }
195
+ /**
196
+ * Get workspace root directory
197
+ */
198
+ getWorkspaceRoot() {
199
+ return this.workspaceRoot;
200
+ }
201
+ /**
202
+ * Clean up workspace for a sample
203
+ * Deletes all subdirectories and files
204
+ *
205
+ * Requirements: 19.6, 26.3 (file I/O optimization)
206
+ *
207
+ * @param sampleId - Sample ID in format "sha256:<hex>"
208
+ */
209
+ async cleanup(sampleId) {
210
+ const sha256 = this.extractSha256(sampleId);
211
+ const workspacePath = this.generateWorkspacePath(sha256);
212
+ // Remove from cache
213
+ this.workspacePathCache.delete(sha256);
214
+ // Check if workspace exists
215
+ if (!fs.existsSync(workspacePath.root)) {
216
+ // Workspace doesn't exist, nothing to clean up
217
+ return;
218
+ }
219
+ // Delete the entire workspace directory recursively (async)
220
+ await fsPromises.rm(workspacePath.root, { recursive: true, force: true });
221
+ }
222
+ /**
223
+ * Clean up old workspaces based on retention policy
224
+ * Deletes workspaces older than the specified number of days
225
+ *
226
+ * Requirements: 操作约束 6 (30-day retention policy), 26.3 (file I/O optimization)
227
+ *
228
+ * Optimizations:
229
+ * - Async file operations
230
+ * - Parallel directory scanning
231
+ * - Batch deletion
232
+ *
233
+ * @param retentionDays - Number of days to retain workspaces (default: 30)
234
+ * @returns Number of workspaces cleaned up
235
+ */
236
+ async cleanupOldWorkspaces(retentionDays = 30) {
237
+ const cutoffTime = Date.now() - (retentionDays * 24 * 60 * 60 * 1000);
238
+ let cleanedCount = 0;
239
+ // Traverse the bucketed directory structure
240
+ if (!fs.existsSync(this.workspaceRoot)) {
241
+ return 0;
242
+ }
243
+ const bucket1Dirs = await fsPromises.readdir(this.workspaceRoot);
244
+ // Process bucket1 directories in parallel (Requirement 26.3)
245
+ const cleanupPromises = [];
246
+ for (const bucket1 of bucket1Dirs) {
247
+ const bucket1Path = path.join(this.workspaceRoot, bucket1);
248
+ cleanupPromises.push((async () => {
249
+ let localCleanedCount = 0;
250
+ try {
251
+ // Skip if not a directory
252
+ const bucket1Stats = await fsPromises.stat(bucket1Path);
253
+ if (!bucket1Stats.isDirectory()) {
254
+ return 0;
255
+ }
256
+ const bucket2Dirs = await fsPromises.readdir(bucket1Path);
257
+ for (const bucket2 of bucket2Dirs) {
258
+ const bucket2Path = path.join(bucket1Path, bucket2);
259
+ try {
260
+ // Skip if not a directory
261
+ const bucket2Stats = await fsPromises.stat(bucket2Path);
262
+ if (!bucket2Stats.isDirectory()) {
263
+ continue;
264
+ }
265
+ const workspaceDirs = await fsPromises.readdir(bucket2Path);
266
+ for (const workspaceDir of workspaceDirs) {
267
+ const workspacePath = path.join(bucket2Path, workspaceDir);
268
+ try {
269
+ // Skip if not a directory
270
+ const workspaceStats = await fsPromises.stat(workspacePath);
271
+ if (!workspaceStats.isDirectory()) {
272
+ continue;
273
+ }
274
+ // Check modification time
275
+ if (workspaceStats.mtimeMs < cutoffTime) {
276
+ // Delete old workspace
277
+ await fsPromises.rm(workspacePath, { recursive: true, force: true });
278
+ localCleanedCount++;
279
+ // Remove from cache if present
280
+ this.workspacePathCache.delete(workspaceDir);
281
+ }
282
+ }
283
+ catch {
284
+ // Skip workspace on error
285
+ continue;
286
+ }
287
+ }
288
+ // Clean up empty bucket2 directories
289
+ const remainingFiles = await fsPromises.readdir(bucket2Path);
290
+ if (remainingFiles.length === 0) {
291
+ await fsPromises.rmdir(bucket2Path);
292
+ }
293
+ }
294
+ catch {
295
+ // Skip bucket2 on error
296
+ continue;
297
+ }
298
+ }
299
+ // Clean up empty bucket1 directories
300
+ const remainingFiles = await fsPromises.readdir(bucket1Path);
301
+ if (remainingFiles.length === 0) {
302
+ await fsPromises.rmdir(bucket1Path);
303
+ }
304
+ }
305
+ catch {
306
+ // Skip bucket1 on error
307
+ }
308
+ return localCleanedCount;
309
+ })());
310
+ }
311
+ // Wait for all cleanup operations to complete
312
+ const results = await Promise.all(cleanupPromises);
313
+ cleanedCount = results.reduce((sum, count) => sum + count, 0);
314
+ return cleanedCount;
315
+ }
316
+ /**
317
+ * Get workspace statistics for monitoring
318
+ * Requirements: 26.3 (file I/O optimization)
319
+ *
320
+ * @returns Object with workspace statistics
321
+ */
322
+ async getWorkspaceStats() {
323
+ let totalWorkspaces = 0;
324
+ let totalSizeBytes = 0;
325
+ let oldestMtime = Date.now();
326
+ if (!fs.existsSync(this.workspaceRoot)) {
327
+ return { totalWorkspaces: 0, totalSizeBytes: 0, oldestWorkspaceAge: 0 };
328
+ }
329
+ const bucket1Dirs = await fsPromises.readdir(this.workspaceRoot);
330
+ for (const bucket1 of bucket1Dirs) {
331
+ const bucket1Path = path.join(this.workspaceRoot, bucket1);
332
+ try {
333
+ const bucket1Stats = await fsPromises.stat(bucket1Path);
334
+ if (!bucket1Stats.isDirectory()) {
335
+ continue;
336
+ }
337
+ const bucket2Dirs = await fsPromises.readdir(bucket1Path);
338
+ for (const bucket2 of bucket2Dirs) {
339
+ const bucket2Path = path.join(bucket1Path, bucket2);
340
+ try {
341
+ const bucket2Stats = await fsPromises.stat(bucket2Path);
342
+ if (!bucket2Stats.isDirectory()) {
343
+ continue;
344
+ }
345
+ const workspaceDirs = await fsPromises.readdir(bucket2Path);
346
+ for (const workspaceDir of workspaceDirs) {
347
+ const workspacePath = path.join(bucket2Path, workspaceDir);
348
+ try {
349
+ const workspaceStats = await fsPromises.stat(workspacePath);
350
+ if (!workspaceStats.isDirectory()) {
351
+ continue;
352
+ }
353
+ totalWorkspaces++;
354
+ // Get directory size (approximate)
355
+ const size = await this.getDirectorySize(workspacePath);
356
+ totalSizeBytes += size;
357
+ // Track oldest workspace
358
+ if (workspaceStats.mtimeMs < oldestMtime) {
359
+ oldestMtime = workspaceStats.mtimeMs;
360
+ }
361
+ }
362
+ catch {
363
+ continue;
364
+ }
365
+ }
366
+ }
367
+ catch {
368
+ continue;
369
+ }
370
+ }
371
+ }
372
+ catch {
373
+ continue;
374
+ }
375
+ }
376
+ const oldestWorkspaceAge = Math.floor((Date.now() - oldestMtime) / (24 * 60 * 60 * 1000));
377
+ return {
378
+ totalWorkspaces,
379
+ totalSizeBytes,
380
+ oldestWorkspaceAge
381
+ };
382
+ }
383
+ /**
384
+ * Get approximate size of a directory
385
+ * Requirements: 26.3 (file I/O optimization)
386
+ *
387
+ * @param dirPath - Directory path
388
+ * @returns Size in bytes
389
+ */
390
+ async getDirectorySize(dirPath) {
391
+ let totalSize = 0;
392
+ try {
393
+ const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
394
+ for (const entry of entries) {
395
+ const entryPath = path.join(dirPath, entry.name);
396
+ if (entry.isDirectory()) {
397
+ totalSize += await this.getDirectorySize(entryPath);
398
+ }
399
+ else if (entry.isFile()) {
400
+ const stats = await fsPromises.stat(entryPath);
401
+ totalSize += stats.size;
402
+ }
403
+ }
404
+ }
405
+ catch {
406
+ // Ignore errors
407
+ }
408
+ return totalSize;
409
+ }
410
+ }
411
+ //# sourceMappingURL=workspace-manager.js.map