@unrdf/hooks 5.0.1
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/LICENSE +21 -0
- package/README.md +86 -0
- package/package.json +70 -0
- package/src/hooks/builtin-hooks.mjs +296 -0
- package/src/hooks/condition-cache.mjs +109 -0
- package/src/hooks/condition-evaluator.mjs +722 -0
- package/src/hooks/define-hook.mjs +211 -0
- package/src/hooks/effect-sandbox-worker.mjs +170 -0
- package/src/hooks/effect-sandbox.mjs +517 -0
- package/src/hooks/file-resolver.mjs +387 -0
- package/src/hooks/hook-chain-compiler.mjs +236 -0
- package/src/hooks/hook-executor-batching.mjs +277 -0
- package/src/hooks/hook-executor.mjs +465 -0
- package/src/hooks/hook-management.mjs +202 -0
- package/src/hooks/hook-scheduler.mjs +413 -0
- package/src/hooks/knowledge-hook-engine.mjs +358 -0
- package/src/hooks/knowledge-hook-manager.mjs +269 -0
- package/src/hooks/observability.mjs +531 -0
- package/src/hooks/policy-pack.mjs +572 -0
- package/src/hooks/quad-pool.mjs +249 -0
- package/src/hooks/quality-metrics.mjs +544 -0
- package/src/hooks/security/error-sanitizer.mjs +257 -0
- package/src/hooks/security/path-validator.mjs +194 -0
- package/src/hooks/security/sandbox-restrictions.mjs +331 -0
- package/src/hooks/telemetry.mjs +167 -0
- package/src/index.mjs +101 -0
- package/src/security/sandbox/browser-executor.mjs +220 -0
- package/src/security/sandbox/detector.mjs +342 -0
- package/src/security/sandbox/isolated-vm-executor.mjs +373 -0
- package/src/security/sandbox/vm2-executor.mjs +217 -0
- package/src/security/sandbox/worker-executor-runtime.mjs +74 -0
- package/src/security/sandbox/worker-executor.mjs +212 -0
- package/src/security/sandbox-adapter.mjs +141 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Isolated VM Executor
|
|
3
|
+
* @module security/sandbox/isolated-vm-executor
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Primary sandbox executor using isolated-vm for maximum security.
|
|
7
|
+
* Provides V8 isolate-based execution with:
|
|
8
|
+
* - Full memory isolation
|
|
9
|
+
* - CPU timeout controls
|
|
10
|
+
* - WASM support
|
|
11
|
+
* - Async/await support
|
|
12
|
+
* - Merkle verification of code integrity
|
|
13
|
+
* - OpenTelemetry instrumentation
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import ivm from 'isolated-vm';
|
|
17
|
+
import { trace, _context } from '@opentelemetry/api';
|
|
18
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
19
|
+
import { bytesToHex } from '@noble/hashes/utils';
|
|
20
|
+
|
|
21
|
+
const tracer = trace.getTracer('isolated-vm-executor');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert bytes to hex string
|
|
25
|
+
* @param {Uint8Array} bytes
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function toHex(bytes) {
|
|
29
|
+
return bytesToHex(bytes);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compute SHA-256 hash of code
|
|
34
|
+
* @param {string} code
|
|
35
|
+
* @returns {string} Hex-encoded hash
|
|
36
|
+
*/
|
|
37
|
+
function hashCode(code) {
|
|
38
|
+
const bytes = new TextEncoder().encode(code);
|
|
39
|
+
return toHex(sha256(bytes));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Threat detection patterns
|
|
44
|
+
*/
|
|
45
|
+
const THREAT_PATTERNS = [
|
|
46
|
+
// VM escape attempts
|
|
47
|
+
/constructor\s*\.\s*constructor/i,
|
|
48
|
+
/Function\s*\(\s*['"`]/i,
|
|
49
|
+
/eval\s*\(/i,
|
|
50
|
+
/\[\s*['"`]constructor['"`]\s*\]/i,
|
|
51
|
+
|
|
52
|
+
// Process/require access
|
|
53
|
+
/process\s*\.\s*binding/i,
|
|
54
|
+
/require\s*\(/i,
|
|
55
|
+
/import\s*\(/i,
|
|
56
|
+
/module\s*\.\s*exports/i,
|
|
57
|
+
|
|
58
|
+
// Prototype pollution
|
|
59
|
+
/__proto__/i,
|
|
60
|
+
/prototype\s*\[\s*['"`]/i,
|
|
61
|
+
|
|
62
|
+
// File system access
|
|
63
|
+
/fs\s*\.\s*(read|write)/i,
|
|
64
|
+
/child_process/i,
|
|
65
|
+
|
|
66
|
+
// Network access
|
|
67
|
+
/fetch\s*\(/i,
|
|
68
|
+
/XMLHttpRequest/i,
|
|
69
|
+
/WebSocket/i,
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Detect potential threats in code
|
|
74
|
+
* @param {string} code
|
|
75
|
+
* @returns {Array<Object>} Detected threats
|
|
76
|
+
*/
|
|
77
|
+
function detectThreats(code) {
|
|
78
|
+
const threats = [];
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < THREAT_PATTERNS.length; i++) {
|
|
81
|
+
const pattern = THREAT_PATTERNS[i];
|
|
82
|
+
if (pattern.test(code)) {
|
|
83
|
+
threats.push({
|
|
84
|
+
patternIndex: i,
|
|
85
|
+
pattern: pattern.toString(),
|
|
86
|
+
severity: i < 4 ? 'critical' : i < 10 ? 'high' : 'medium',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return threats;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Isolated VM Executor
|
|
96
|
+
*/
|
|
97
|
+
export class IsolatedVmExecutor {
|
|
98
|
+
/**
|
|
99
|
+
* @param {Object} [config] - Executor configuration
|
|
100
|
+
* @param {number} [config.memoryLimit=128] - Memory limit in MB
|
|
101
|
+
* @param {number} [config.timeout=5000] - Execution timeout in ms
|
|
102
|
+
* @param {boolean} [config.enableWasm=true] - Enable WASM support
|
|
103
|
+
* @param {boolean} [config.enableAsync=true] - Enable async/await
|
|
104
|
+
* @param {boolean} [config.enableThreatDetection=true] - Enable threat detection
|
|
105
|
+
* @param {boolean} [config.strictMode=true] - Enable strict mode
|
|
106
|
+
*/
|
|
107
|
+
constructor(config = {}) {
|
|
108
|
+
this.config = {
|
|
109
|
+
memoryLimit: config.memoryLimit || 128,
|
|
110
|
+
timeout: config.timeout || 5000,
|
|
111
|
+
enableWasm: config.enableWasm !== false,
|
|
112
|
+
enableAsync: config.enableAsync !== false,
|
|
113
|
+
enableThreatDetection: config.enableThreatDetection !== false,
|
|
114
|
+
strictMode: config.strictMode !== false,
|
|
115
|
+
...config,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** @type {Map<string, ivm.Isolate>} */
|
|
119
|
+
this.isolates = new Map();
|
|
120
|
+
|
|
121
|
+
/** @type {Map<string, ivm.Context>} */
|
|
122
|
+
this.contexts = new Map();
|
|
123
|
+
|
|
124
|
+
/** @type {Map<string, string>} - Code hashes for Merkle verification */
|
|
125
|
+
this.codeHashes = new Map();
|
|
126
|
+
|
|
127
|
+
this.executionCount = 0;
|
|
128
|
+
this.totalDuration = 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Execute code in isolated VM
|
|
133
|
+
* @param {string|Function} code - Code to execute
|
|
134
|
+
* @param {Object} [context] - Execution context
|
|
135
|
+
* @param {Object} [options] - Execution options
|
|
136
|
+
* @returns {Promise<Object>} Execution result
|
|
137
|
+
*/
|
|
138
|
+
async run(code, context = {}, options = {}) {
|
|
139
|
+
return tracer.startActiveSpan('security.isolate.execute', async span => {
|
|
140
|
+
const startTime = Date.now();
|
|
141
|
+
const executionId = `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
span.setAttributes({
|
|
145
|
+
'security.executor.type': 'isolated-vm',
|
|
146
|
+
'security.execution.id': executionId,
|
|
147
|
+
'security.memoryLimit': this.config.memoryLimit,
|
|
148
|
+
'security.timeout': options.timeout || this.config.timeout,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Convert function to string if needed
|
|
152
|
+
const codeString = typeof code === 'function' ? code.toString() : code;
|
|
153
|
+
|
|
154
|
+
// Threat detection
|
|
155
|
+
if (this.config.enableThreatDetection) {
|
|
156
|
+
const threats = detectThreats(codeString);
|
|
157
|
+
if (threats.length > 0) {
|
|
158
|
+
const criticalThreats = threats.filter(t => t.severity === 'critical');
|
|
159
|
+
if (criticalThreats.length > 0) {
|
|
160
|
+
span.setAttribute('security.threats.detected', threats.length);
|
|
161
|
+
span.setAttribute('security.threats.critical', criticalThreats.length);
|
|
162
|
+
span.setStatus({
|
|
163
|
+
code: 2,
|
|
164
|
+
message: 'Critical security threat detected',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Security threat detected: ${criticalThreats[0].pattern}. ` + `Execution blocked.`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
span.setAttribute('security.threats.count', threats.length);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Compute code hash for Merkle verification
|
|
176
|
+
const codeHash = hashCode(codeString);
|
|
177
|
+
this.codeHashes.set(executionId, codeHash);
|
|
178
|
+
span.setAttribute('security.code.hash', codeHash);
|
|
179
|
+
|
|
180
|
+
// Create isolate
|
|
181
|
+
const isolate = new ivm.Isolate({
|
|
182
|
+
memoryLimit: this.config.memoryLimit,
|
|
183
|
+
inspector: false, // Disable debugging for security
|
|
184
|
+
onCatastrophicError: msg => {
|
|
185
|
+
span.recordException(new Error(`Catastrophic error: ${msg}`));
|
|
186
|
+
this.destroyIsolate(executionId);
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
this.isolates.set(executionId, isolate);
|
|
191
|
+
|
|
192
|
+
// Create context
|
|
193
|
+
const ivmContext = await isolate.createContext();
|
|
194
|
+
this.contexts.set(executionId, ivmContext);
|
|
195
|
+
|
|
196
|
+
// Setup safe global environment
|
|
197
|
+
const jail = ivmContext.global;
|
|
198
|
+
await jail.set('global', jail.derefInto());
|
|
199
|
+
|
|
200
|
+
// Add safe console
|
|
201
|
+
const safeConsole = new ivm.Reference({
|
|
202
|
+
log: (...args) => console.log('[Sandbox]', ...args),
|
|
203
|
+
error: (...args) => console.error('[Sandbox]', ...args),
|
|
204
|
+
warn: (...args) => console.warn('[Sandbox]', ...args),
|
|
205
|
+
info: (...args) => console.info('[Sandbox]', ...args),
|
|
206
|
+
});
|
|
207
|
+
await jail.set('console', safeConsole);
|
|
208
|
+
|
|
209
|
+
// Add JSON support
|
|
210
|
+
const jsonRef = new ivm.Reference({
|
|
211
|
+
parse: str => JSON.parse(str),
|
|
212
|
+
stringify: obj => JSON.stringify(obj),
|
|
213
|
+
});
|
|
214
|
+
await jail.set('JSON', jsonRef);
|
|
215
|
+
|
|
216
|
+
// Add Math support
|
|
217
|
+
const mathRef = new ivm.Reference(Math);
|
|
218
|
+
await jail.set('Math', mathRef);
|
|
219
|
+
|
|
220
|
+
// Add Date support (limited)
|
|
221
|
+
const dateRef = new ivm.Reference({
|
|
222
|
+
now: () => Date.now(),
|
|
223
|
+
});
|
|
224
|
+
await jail.set('Date', dateRef);
|
|
225
|
+
|
|
226
|
+
// Inject context data
|
|
227
|
+
if (context && Object.keys(context).length > 0) {
|
|
228
|
+
for (const [key, value] of Object.entries(context)) {
|
|
229
|
+
const valueRef = new ivm.Reference(value);
|
|
230
|
+
await jail.set(key, valueRef);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Wrap code in strict mode if enabled
|
|
235
|
+
const wrappedCode = this.config.strictMode ? `"use strict";\n${codeString}` : codeString;
|
|
236
|
+
|
|
237
|
+
// Compile and execute
|
|
238
|
+
const script = await isolate.compileScript(wrappedCode);
|
|
239
|
+
const timeout = options.timeout || this.config.timeout;
|
|
240
|
+
|
|
241
|
+
const result = await script.run(ivmContext, {
|
|
242
|
+
timeout,
|
|
243
|
+
reference: true,
|
|
244
|
+
promise: this.config.enableAsync,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Copy result back to main isolate
|
|
248
|
+
const output = result ? await result.copy() : undefined;
|
|
249
|
+
|
|
250
|
+
// Cleanup
|
|
251
|
+
await this.destroyIsolate(executionId);
|
|
252
|
+
|
|
253
|
+
const duration = Date.now() - startTime;
|
|
254
|
+
this.executionCount++;
|
|
255
|
+
this.totalDuration += duration;
|
|
256
|
+
|
|
257
|
+
span.setAttributes({
|
|
258
|
+
'security.execution.duration': duration,
|
|
259
|
+
'security.execution.success': true,
|
|
260
|
+
});
|
|
261
|
+
span.setStatus({ code: 1 }); // OK
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
success: true,
|
|
265
|
+
result: output,
|
|
266
|
+
duration,
|
|
267
|
+
executionId,
|
|
268
|
+
codeHash,
|
|
269
|
+
memoryUsed: await this.getMemoryUsage(executionId).catch(() => ({
|
|
270
|
+
used: 0,
|
|
271
|
+
})),
|
|
272
|
+
};
|
|
273
|
+
} catch (error) {
|
|
274
|
+
const duration = Date.now() - startTime;
|
|
275
|
+
|
|
276
|
+
span.recordException(error);
|
|
277
|
+
span.setAttributes({
|
|
278
|
+
'security.execution.duration': duration,
|
|
279
|
+
'security.execution.success': false,
|
|
280
|
+
'security.error.message': error.message,
|
|
281
|
+
});
|
|
282
|
+
span.setStatus({ code: 2, message: error.message });
|
|
283
|
+
|
|
284
|
+
// Cleanup on error
|
|
285
|
+
await this.destroyIsolate(executionId).catch(() => {});
|
|
286
|
+
|
|
287
|
+
// Categorize error
|
|
288
|
+
let errorType = 'unknown';
|
|
289
|
+
if (error.message?.includes('timed out')) {
|
|
290
|
+
errorType = 'timeout';
|
|
291
|
+
} else if (error.message?.includes('memory')) {
|
|
292
|
+
errorType = 'memory_limit';
|
|
293
|
+
} else if (error.message?.includes('Security threat')) {
|
|
294
|
+
errorType = 'security_threat';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
span.setAttribute('security.error.type', errorType);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
success: false,
|
|
301
|
+
error: error.message,
|
|
302
|
+
errorType,
|
|
303
|
+
duration,
|
|
304
|
+
executionId,
|
|
305
|
+
codeHash: this.codeHashes.get(executionId),
|
|
306
|
+
};
|
|
307
|
+
} finally {
|
|
308
|
+
span.end();
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get memory usage for execution
|
|
315
|
+
* @param {string} executionId
|
|
316
|
+
* @returns {Promise<Object>}
|
|
317
|
+
*/
|
|
318
|
+
async getMemoryUsage(executionId) {
|
|
319
|
+
const isolate = this.isolates.get(executionId);
|
|
320
|
+
if (!isolate) {
|
|
321
|
+
return { used: 0, total: 0, limit: 0, percentage: 0 };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const heapStats = await isolate.getHeapStatistics();
|
|
325
|
+
return {
|
|
326
|
+
used: heapStats.used_heap_size,
|
|
327
|
+
total: heapStats.total_heap_size,
|
|
328
|
+
limit: heapStats.heap_size_limit,
|
|
329
|
+
percentage: (heapStats.used_heap_size / heapStats.heap_size_limit) * 100,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Destroy isolate and free resources
|
|
335
|
+
* @param {string} executionId
|
|
336
|
+
*/
|
|
337
|
+
async destroyIsolate(executionId) {
|
|
338
|
+
const isolate = this.isolates.get(executionId);
|
|
339
|
+
if (isolate) {
|
|
340
|
+
try {
|
|
341
|
+
isolate.dispose();
|
|
342
|
+
} catch (err) {
|
|
343
|
+
// Ignore disposal errors
|
|
344
|
+
}
|
|
345
|
+
this.isolates.delete(executionId);
|
|
346
|
+
this.contexts.delete(executionId);
|
|
347
|
+
this.codeHashes.delete(executionId);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get executor statistics
|
|
353
|
+
* @returns {Object}
|
|
354
|
+
*/
|
|
355
|
+
getStats() {
|
|
356
|
+
return {
|
|
357
|
+
type: 'isolated-vm',
|
|
358
|
+
config: this.config,
|
|
359
|
+
executionCount: this.executionCount,
|
|
360
|
+
averageDuration: this.executionCount > 0 ? this.totalDuration / this.executionCount : 0,
|
|
361
|
+
activeIsolates: this.isolates.size,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Cleanup all isolates
|
|
367
|
+
*/
|
|
368
|
+
async cleanup() {
|
|
369
|
+
for (const executionId of this.isolates.keys()) {
|
|
370
|
+
await this.destroyIsolate(executionId);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file VM2 Executor (Legacy - Deprecated)
|
|
3
|
+
* @module security/sandbox/vm2-executor
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Legacy sandbox executor using vm2 (DEPRECATED).
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ SECURITY WARNING ⚠️
|
|
9
|
+
* This executor is DEPRECATED and has known security vulnerabilities.
|
|
10
|
+
* It should only be used for backward compatibility with existing code.
|
|
11
|
+
* Migrate to isolated-vm or worker executors as soon as possible.
|
|
12
|
+
*
|
|
13
|
+
* Known issues:
|
|
14
|
+
* - VM escape vulnerabilities
|
|
15
|
+
* - Prototype pollution risks
|
|
16
|
+
* - No memory isolation
|
|
17
|
+
* - No CPU isolation
|
|
18
|
+
*
|
|
19
|
+
* @deprecated Use IsolatedVmExecutor or WorkerExecutor instead
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { trace } from '@opentelemetry/api';
|
|
23
|
+
|
|
24
|
+
const tracer = trace.getTracer('vm2-executor');
|
|
25
|
+
|
|
26
|
+
// Show deprecation warning on first import
|
|
27
|
+
console.warn(
|
|
28
|
+
'\n' +
|
|
29
|
+
'⚠️ ========================================= ⚠️\n' +
|
|
30
|
+
'⚠️ VM2 EXECUTOR DEPRECATION WARNING ⚠️\n' +
|
|
31
|
+
'⚠️ ========================================= ⚠️\n' +
|
|
32
|
+
'\n' +
|
|
33
|
+
'The vm2 package is deprecated and contains critical security vulnerabilities.\n' +
|
|
34
|
+
'This executor should NOT be used in production environments.\n' +
|
|
35
|
+
'\n' +
|
|
36
|
+
'Migrate to:\n' +
|
|
37
|
+
' - IsolatedVmExecutor (recommended): Full V8 isolation\n' +
|
|
38
|
+
' - WorkerExecutor: Process-level isolation\n' +
|
|
39
|
+
'\n' +
|
|
40
|
+
'For more information:\n' +
|
|
41
|
+
' https://github.com/patriksimek/vm2/issues/533\n' +
|
|
42
|
+
'\n' +
|
|
43
|
+
'To suppress this warning, set UNRDF_SUPPRESS_VM2_WARNING=1\n' +
|
|
44
|
+
'⚠️ ========================================= ⚠️\n'
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* VM2 Executor (Deprecated)
|
|
49
|
+
* @deprecated
|
|
50
|
+
*/
|
|
51
|
+
export class Vm2Executor {
|
|
52
|
+
/**
|
|
53
|
+
* @param {Object} [config] - Executor configuration
|
|
54
|
+
* @param {number} [config.timeout=5000] - Execution timeout in ms
|
|
55
|
+
* @param {Array<string>} [config.allowedModules] - Allowed modules
|
|
56
|
+
* @param {boolean} [config.strictMode=true] - Enable strict mode
|
|
57
|
+
*/
|
|
58
|
+
constructor(config = {}) {
|
|
59
|
+
this.config = {
|
|
60
|
+
timeout: config.timeout || 5000,
|
|
61
|
+
allowedModules: config.allowedModules || [],
|
|
62
|
+
strictMode: config.strictMode !== false,
|
|
63
|
+
...config,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.executionCount = 0;
|
|
67
|
+
this.totalDuration = 0;
|
|
68
|
+
|
|
69
|
+
// Show warning on each instantiation unless suppressed
|
|
70
|
+
if (!process.env.UNRDF_SUPPRESS_VM2_WARNING) {
|
|
71
|
+
console.warn(
|
|
72
|
+
'[DEPRECATION] Creating vm2 executor instance - consider migrating to isolated-vm'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Execute code in vm2
|
|
79
|
+
* @param {string|Function} code - Code to execute
|
|
80
|
+
* @param {Object} [context] - Execution context
|
|
81
|
+
* @param {Object} [options] - Execution options
|
|
82
|
+
* @returns {Promise<Object>} Execution result
|
|
83
|
+
*/
|
|
84
|
+
async run(code, context = {}, options = {}) {
|
|
85
|
+
return tracer.startActiveSpan('security.vm2.execute', async span => {
|
|
86
|
+
const startTime = Date.now();
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
span.setAttributes({
|
|
90
|
+
'security.executor.type': 'vm2',
|
|
91
|
+
'security.executor.deprecated': true,
|
|
92
|
+
'security.executor.security_level': 'low',
|
|
93
|
+
'security.timeout': options.timeout || this.config.timeout,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Dynamic import of vm2
|
|
97
|
+
let VM;
|
|
98
|
+
try {
|
|
99
|
+
const vm2Module = await import('vm2');
|
|
100
|
+
VM = vm2Module.VM;
|
|
101
|
+
} catch (err) {
|
|
102
|
+
throw new Error('vm2 not available - install with: pnpm add vm2');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Convert function to string if needed
|
|
106
|
+
const codeString = typeof code === 'function' ? code.toString() : code;
|
|
107
|
+
|
|
108
|
+
// Create VM instance
|
|
109
|
+
const vm = new VM({
|
|
110
|
+
timeout: options.timeout || this.config.timeout,
|
|
111
|
+
sandbox: {
|
|
112
|
+
...this._createSandboxGlobals(context),
|
|
113
|
+
console: this._createSafeConsole(),
|
|
114
|
+
},
|
|
115
|
+
eval: false,
|
|
116
|
+
wasm: false,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Wrap code in strict mode if enabled
|
|
120
|
+
const wrappedCode = this.config.strictMode
|
|
121
|
+
? `"use strict";\n(function() { ${codeString} })()`
|
|
122
|
+
: `(function() { ${codeString} })()`;
|
|
123
|
+
|
|
124
|
+
// Execute
|
|
125
|
+
const result = vm.run(wrappedCode);
|
|
126
|
+
|
|
127
|
+
const duration = Date.now() - startTime;
|
|
128
|
+
this.executionCount++;
|
|
129
|
+
this.totalDuration += duration;
|
|
130
|
+
|
|
131
|
+
span.setAttributes({
|
|
132
|
+
'security.execution.duration': duration,
|
|
133
|
+
'security.execution.success': true,
|
|
134
|
+
});
|
|
135
|
+
span.setStatus({ code: 1 }); // OK
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
success: true,
|
|
139
|
+
result,
|
|
140
|
+
duration,
|
|
141
|
+
executorType: 'vm2',
|
|
142
|
+
deprecated: true,
|
|
143
|
+
};
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const duration = Date.now() - startTime;
|
|
146
|
+
|
|
147
|
+
span.recordException(error);
|
|
148
|
+
span.setAttributes({
|
|
149
|
+
'security.execution.duration': duration,
|
|
150
|
+
'security.execution.success': false,
|
|
151
|
+
'security.error.message': error.message,
|
|
152
|
+
});
|
|
153
|
+
span.setStatus({ code: 2, message: error.message });
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: error.message,
|
|
158
|
+
duration,
|
|
159
|
+
executorType: 'vm2',
|
|
160
|
+
deprecated: true,
|
|
161
|
+
};
|
|
162
|
+
} finally {
|
|
163
|
+
span.end();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create sandbox globals
|
|
170
|
+
* @param {Object} context
|
|
171
|
+
* @returns {Object}
|
|
172
|
+
* @private
|
|
173
|
+
*/
|
|
174
|
+
_createSandboxGlobals(context) {
|
|
175
|
+
return {
|
|
176
|
+
Date: { now: () => Date.now() },
|
|
177
|
+
Math,
|
|
178
|
+
JSON,
|
|
179
|
+
...context,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Create safe console
|
|
185
|
+
* @returns {Object}
|
|
186
|
+
* @private
|
|
187
|
+
*/
|
|
188
|
+
_createSafeConsole() {
|
|
189
|
+
return {
|
|
190
|
+
log: (...args) => console.log('[VM2]', ...args),
|
|
191
|
+
error: (...args) => console.error('[VM2]', ...args),
|
|
192
|
+
warn: (...args) => console.warn('[VM2]', ...args),
|
|
193
|
+
info: (...args) => console.info('[VM2]', ...args),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get executor statistics
|
|
199
|
+
* @returns {Object}
|
|
200
|
+
*/
|
|
201
|
+
getStats() {
|
|
202
|
+
return {
|
|
203
|
+
type: 'vm2',
|
|
204
|
+
deprecated: true,
|
|
205
|
+
config: this.config,
|
|
206
|
+
executionCount: this.executionCount,
|
|
207
|
+
averageDuration: this.executionCount > 0 ? this.totalDuration / this.executionCount : 0,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Cleanup (no-op for vm2)
|
|
213
|
+
*/
|
|
214
|
+
async cleanup() {
|
|
215
|
+
// vm2 doesn't require cleanup
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Worker Thread Runtime
|
|
3
|
+
* @description Runtime environment for worker thread execution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parentPort, workerData } from 'worker_threads';
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const { code, context, config } = workerData;
|
|
10
|
+
|
|
11
|
+
// Create safe global environment
|
|
12
|
+
const sandbox = {
|
|
13
|
+
// Allowed globals
|
|
14
|
+
console: {
|
|
15
|
+
log: (...args) => console.log('[Worker]', ...args),
|
|
16
|
+
error: (...args) => console.error('[Worker]', ...args),
|
|
17
|
+
warn: (...args) => console.warn('[Worker]', ...args),
|
|
18
|
+
info: (...args) => console.info('[Worker]', ...args),
|
|
19
|
+
},
|
|
20
|
+
Date: {
|
|
21
|
+
now: () => Date.now(),
|
|
22
|
+
},
|
|
23
|
+
Math,
|
|
24
|
+
JSON,
|
|
25
|
+
// Context data
|
|
26
|
+
...context,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Add allowed globals from config
|
|
30
|
+
if (config.allowedGlobals) {
|
|
31
|
+
for (const globalName of config.allowedGlobals) {
|
|
32
|
+
if (globalName in globalThis && !(globalName in sandbox)) {
|
|
33
|
+
sandbox[globalName] = globalThis[globalName];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Execute code
|
|
39
|
+
// If code doesn't contain return, wrap it in return for expression evaluation
|
|
40
|
+
const wrappedCode =
|
|
41
|
+
code.trim().includes('return') || code.trim().includes(';') || code.trim().includes('{')
|
|
42
|
+
? code
|
|
43
|
+
: `return (${code})`;
|
|
44
|
+
|
|
45
|
+
const func = new Function(
|
|
46
|
+
...Object.keys(sandbox),
|
|
47
|
+
`
|
|
48
|
+
${config.strictMode ? '"use strict";' : ''}
|
|
49
|
+
${wrappedCode}
|
|
50
|
+
`
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const result = func(...Object.values(sandbox));
|
|
54
|
+
|
|
55
|
+
// Get memory usage
|
|
56
|
+
const memoryUsage = process.memoryUsage();
|
|
57
|
+
|
|
58
|
+
// Send result back
|
|
59
|
+
parentPort.postMessage({
|
|
60
|
+
success: true,
|
|
61
|
+
result,
|
|
62
|
+
memoryUsed: {
|
|
63
|
+
used: memoryUsage.heapUsed,
|
|
64
|
+
total: memoryUsage.heapTotal,
|
|
65
|
+
rss: memoryUsage.rss,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
parentPort.postMessage({
|
|
70
|
+
success: false,
|
|
71
|
+
error: error.message,
|
|
72
|
+
stack: error.stack,
|
|
73
|
+
});
|
|
74
|
+
}
|