pal-explorer-cli 0.4.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.
- package/LICENSE.md +18 -0
- package/README.md +314 -0
- package/bin/pal.js +230 -0
- package/extensions/@palexplorer/analytics/README.md +45 -0
- package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
- package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
- package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
- package/extensions/@palexplorer/analytics/extension.json +27 -0
- package/extensions/@palexplorer/analytics/index.js +186 -0
- package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
- package/extensions/@palexplorer/audit/extension.json +17 -0
- package/extensions/@palexplorer/audit/index.js +2 -0
- package/extensions/@palexplorer/auth-email/extension.json +17 -0
- package/extensions/@palexplorer/auth-email/index.js +102 -0
- package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
- package/extensions/@palexplorer/auth-oauth/index.js +199 -0
- package/extensions/@palexplorer/chat/extension.json +17 -0
- package/extensions/@palexplorer/chat/index.js +2 -0
- package/extensions/@palexplorer/discovery/extension.json +16 -0
- package/extensions/@palexplorer/discovery/index.js +111 -0
- package/extensions/@palexplorer/email-notifications/extension.json +23 -0
- package/extensions/@palexplorer/email-notifications/index.js +242 -0
- package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
- package/extensions/@palexplorer/explorer-integration/index.js +122 -0
- package/extensions/@palexplorer/groups/extension.json +17 -0
- package/extensions/@palexplorer/groups/index.js +2 -0
- package/extensions/@palexplorer/networks/extension.json +17 -0
- package/extensions/@palexplorer/networks/index.js +2 -0
- package/extensions/@palexplorer/share-links/extension.json +17 -0
- package/extensions/@palexplorer/share-links/index.js +2 -0
- package/extensions/@palexplorer/sync/extension.json +17 -0
- package/extensions/@palexplorer/sync/index.js +2 -0
- package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
- package/extensions/@palexplorer/user-mgmt/index.js +2 -0
- package/extensions/@palexplorer/vfs/extension.json +17 -0
- package/extensions/@palexplorer/vfs/index.js +167 -0
- package/lib/capabilities.js +263 -0
- package/lib/commands/analytics.js +175 -0
- package/lib/commands/api-keys.js +131 -0
- package/lib/commands/audit.js +235 -0
- package/lib/commands/auth.js +137 -0
- package/lib/commands/backup.js +76 -0
- package/lib/commands/billing.js +148 -0
- package/lib/commands/chat.js +217 -0
- package/lib/commands/cloud-backup.js +231 -0
- package/lib/commands/comment.js +99 -0
- package/lib/commands/completion.js +203 -0
- package/lib/commands/compliance.js +218 -0
- package/lib/commands/config.js +136 -0
- package/lib/commands/connect.js +44 -0
- package/lib/commands/dept.js +294 -0
- package/lib/commands/device.js +146 -0
- package/lib/commands/download.js +226 -0
- package/lib/commands/explorer.js +178 -0
- package/lib/commands/extension.js +970 -0
- package/lib/commands/favorite.js +90 -0
- package/lib/commands/federation.js +270 -0
- package/lib/commands/file.js +533 -0
- package/lib/commands/group.js +271 -0
- package/lib/commands/gui-share.js +29 -0
- package/lib/commands/init.js +61 -0
- package/lib/commands/invite.js +59 -0
- package/lib/commands/list.js +59 -0
- package/lib/commands/log.js +116 -0
- package/lib/commands/nearby.js +108 -0
- package/lib/commands/network.js +251 -0
- package/lib/commands/notify.js +198 -0
- package/lib/commands/org.js +273 -0
- package/lib/commands/pal.js +180 -0
- package/lib/commands/permissions.js +216 -0
- package/lib/commands/pin.js +97 -0
- package/lib/commands/protocol.js +357 -0
- package/lib/commands/rbac.js +147 -0
- package/lib/commands/recover.js +36 -0
- package/lib/commands/register.js +171 -0
- package/lib/commands/relay.js +131 -0
- package/lib/commands/remote.js +368 -0
- package/lib/commands/revoke.js +50 -0
- package/lib/commands/scanner.js +280 -0
- package/lib/commands/schedule.js +344 -0
- package/lib/commands/scim.js +203 -0
- package/lib/commands/search.js +181 -0
- package/lib/commands/serve.js +438 -0
- package/lib/commands/server.js +350 -0
- package/lib/commands/share-link.js +199 -0
- package/lib/commands/share.js +323 -0
- package/lib/commands/sso.js +200 -0
- package/lib/commands/status.js +136 -0
- package/lib/commands/stream.js +562 -0
- package/lib/commands/su.js +187 -0
- package/lib/commands/sync.js +827 -0
- package/lib/commands/transfers.js +152 -0
- package/lib/commands/uninstall.js +188 -0
- package/lib/commands/update.js +204 -0
- package/lib/commands/user.js +276 -0
- package/lib/commands/vfs.js +84 -0
- package/lib/commands/web.js +52 -0
- package/lib/commands/webhook.js +180 -0
- package/lib/commands/whoami.js +59 -0
- package/lib/commands/workspace.js +121 -0
- package/lib/core/accessLog.js +54 -0
- package/lib/core/analytics.js +99 -0
- package/lib/core/backup.js +84 -0
- package/lib/core/billing.js +336 -0
- package/lib/core/bitfieldStore.js +53 -0
- package/lib/core/connectionManager.js +182 -0
- package/lib/core/dhtDiscovery.js +148 -0
- package/lib/core/discoveryClient.js +408 -0
- package/lib/core/extensionAnalyzer.js +357 -0
- package/lib/core/extensionSandbox.js +250 -0
- package/lib/core/extensionWorkerHost.js +166 -0
- package/lib/core/extensions.js +1082 -0
- package/lib/core/fileDiff.js +69 -0
- package/lib/core/groups.js +119 -0
- package/lib/core/identity.js +340 -0
- package/lib/core/mdnsService.js +126 -0
- package/lib/core/networks.js +81 -0
- package/lib/core/permissions.js +109 -0
- package/lib/core/pro.js +27 -0
- package/lib/core/resolver.js +74 -0
- package/lib/core/serverList.js +224 -0
- package/lib/core/sharePolicy.js +69 -0
- package/lib/core/shares.js +325 -0
- package/lib/core/signalingServer.js +441 -0
- package/lib/core/streamTransport.js +106 -0
- package/lib/core/su.js +55 -0
- package/lib/core/syncEngine.js +264 -0
- package/lib/core/syncState.js +159 -0
- package/lib/core/transfers.js +259 -0
- package/lib/core/users.js +225 -0
- package/lib/core/vfs.js +216 -0
- package/lib/core/webServer.js +702 -0
- package/lib/core/webrtcStream.js +396 -0
- package/lib/crypto/chatEncryption.js +57 -0
- package/lib/crypto/shareEncryption.js +195 -0
- package/lib/crypto/sharePassword.js +35 -0
- package/lib/crypto/streamEncryption.js +189 -0
- package/lib/package.json +1 -0
- package/lib/protocol/envelope.js +271 -0
- package/lib/protocol/handler.js +191 -0
- package/lib/protocol/index.js +27 -0
- package/lib/protocol/messages.js +247 -0
- package/lib/protocol/negotiation.js +127 -0
- package/lib/protocol/policy.js +142 -0
- package/lib/protocol/router.js +86 -0
- package/lib/protocol/sync.js +122 -0
- package/lib/utils/cli.js +15 -0
- package/lib/utils/config.js +123 -0
- package/lib/utils/configIntegrity.js +87 -0
- package/lib/utils/downloadDir.js +9 -0
- package/lib/utils/explorer.js +83 -0
- package/lib/utils/format.js +12 -0
- package/lib/utils/help.js +357 -0
- package/lib/utils/logger.js +103 -0
- package/lib/utils/torrent.js +203 -0
- package/package.json +71 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import * as acorn from 'acorn';
|
|
2
|
+
import * as walk from 'acorn-walk';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const BLOCKED_MODULES = new Set([
|
|
7
|
+
'child_process', 'cluster', 'dgram', 'dns', 'http2',
|
|
8
|
+
'inspector', 'net', 'repl', 'tls', 'vm', 'worker_threads',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const DANGEROUS_GLOBALS = new Set([
|
|
12
|
+
'eval', 'Function',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const DANGEROUS_PROCESS_PROPS = new Set([
|
|
16
|
+
'exit', 'kill', 'abort', 'env', 'dlopen', 'binding', '_linkedBinding',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const DANGEROUS_PROPERTY_ACCESS = new Set([
|
|
20
|
+
'constructor', '__proto__', 'prototype',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function analyzeCode(code, filePath = '<unknown>') {
|
|
24
|
+
const findings = [];
|
|
25
|
+
let ast;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
ast = acorn.parse(code, {
|
|
29
|
+
ecmaVersion: 'latest',
|
|
30
|
+
sourceType: 'module',
|
|
31
|
+
allowImportExportEverywhere: true,
|
|
32
|
+
locations: true,
|
|
33
|
+
});
|
|
34
|
+
} catch (err) {
|
|
35
|
+
findings.push({
|
|
36
|
+
type: 'parse-error',
|
|
37
|
+
severity: 'high',
|
|
38
|
+
message: `Failed to parse: ${err.message}`,
|
|
39
|
+
line: err.loc?.line,
|
|
40
|
+
});
|
|
41
|
+
return { findings, blocked: [], dangerous: findings };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const blocked = [];
|
|
45
|
+
const dangerous = [];
|
|
46
|
+
|
|
47
|
+
// ── Static imports: import ... from 'module' ──
|
|
48
|
+
walk.simple(ast, {
|
|
49
|
+
ImportDeclaration(node) {
|
|
50
|
+
const mod = node.source.value;
|
|
51
|
+
if (BLOCKED_MODULES.has(mod)) {
|
|
52
|
+
const f = {
|
|
53
|
+
type: 'blocked-import',
|
|
54
|
+
severity: 'critical',
|
|
55
|
+
message: `Static import of blocked module '${mod}'`,
|
|
56
|
+
module: mod,
|
|
57
|
+
line: node.loc?.start.line,
|
|
58
|
+
};
|
|
59
|
+
findings.push(f);
|
|
60
|
+
blocked.push(f);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ── Dynamic imports: import('module') and await import('module') ──
|
|
66
|
+
walk.simple(ast, {
|
|
67
|
+
ImportExpression(node) {
|
|
68
|
+
if (node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
69
|
+
const mod = node.source.value;
|
|
70
|
+
if (BLOCKED_MODULES.has(mod)) {
|
|
71
|
+
const f = {
|
|
72
|
+
type: 'blocked-import',
|
|
73
|
+
severity: 'critical',
|
|
74
|
+
message: `Dynamic import of blocked module '${mod}'`,
|
|
75
|
+
module: mod,
|
|
76
|
+
line: node.loc?.start.line,
|
|
77
|
+
};
|
|
78
|
+
findings.push(f);
|
|
79
|
+
blocked.push(f);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
// Non-literal dynamic import — always dangerous (can import anything at runtime)
|
|
83
|
+
const f = {
|
|
84
|
+
type: 'dynamic-import',
|
|
85
|
+
severity: 'high',
|
|
86
|
+
message: 'Non-literal dynamic import() — can bypass module restrictions at runtime',
|
|
87
|
+
line: node.loc?.start.line,
|
|
88
|
+
};
|
|
89
|
+
findings.push(f);
|
|
90
|
+
dangerous.push(f);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ── require() calls ──
|
|
96
|
+
walk.simple(ast, {
|
|
97
|
+
CallExpression(node) {
|
|
98
|
+
// require('module')
|
|
99
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'require') {
|
|
100
|
+
if (node.arguments[0]?.type === 'Literal' && typeof node.arguments[0].value === 'string') {
|
|
101
|
+
const mod = node.arguments[0].value;
|
|
102
|
+
if (BLOCKED_MODULES.has(mod)) {
|
|
103
|
+
const f = {
|
|
104
|
+
type: 'blocked-import',
|
|
105
|
+
severity: 'critical',
|
|
106
|
+
message: `require() of blocked module '${mod}'`,
|
|
107
|
+
module: mod,
|
|
108
|
+
line: node.loc?.start.line,
|
|
109
|
+
};
|
|
110
|
+
findings.push(f);
|
|
111
|
+
blocked.push(f);
|
|
112
|
+
}
|
|
113
|
+
} else if (node.arguments[0]?.type !== 'Literal') {
|
|
114
|
+
const f = {
|
|
115
|
+
type: 'dynamic-require',
|
|
116
|
+
severity: 'high',
|
|
117
|
+
message: 'Non-literal require() — can bypass module restrictions at runtime',
|
|
118
|
+
line: node.loc?.start.line,
|
|
119
|
+
};
|
|
120
|
+
findings.push(f);
|
|
121
|
+
dangerous.push(f);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// eval(...)
|
|
126
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'eval') {
|
|
127
|
+
const f = {
|
|
128
|
+
type: 'eval',
|
|
129
|
+
severity: 'critical',
|
|
130
|
+
message: 'eval() — arbitrary code execution',
|
|
131
|
+
line: node.loc?.start.line,
|
|
132
|
+
};
|
|
133
|
+
findings.push(f);
|
|
134
|
+
dangerous.push(f);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// new Function(...)
|
|
138
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
139
|
+
node.callee.object?.name === 'Function' &&
|
|
140
|
+
node.callee.property?.name === 'constructor') {
|
|
141
|
+
const f = {
|
|
142
|
+
type: 'function-constructor',
|
|
143
|
+
severity: 'critical',
|
|
144
|
+
message: 'Function.constructor() — arbitrary code execution',
|
|
145
|
+
line: node.loc?.start.line,
|
|
146
|
+
};
|
|
147
|
+
findings.push(f);
|
|
148
|
+
dangerous.push(f);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
NewExpression(node) {
|
|
153
|
+
// new Function(...)
|
|
154
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
|
|
155
|
+
const f = {
|
|
156
|
+
type: 'function-constructor',
|
|
157
|
+
severity: 'critical',
|
|
158
|
+
message: 'new Function() — arbitrary code execution',
|
|
159
|
+
line: node.loc?.start.line,
|
|
160
|
+
};
|
|
161
|
+
findings.push(f);
|
|
162
|
+
dangerous.push(f);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ── process.exit/kill/env/abort/binding ──
|
|
168
|
+
walk.simple(ast, {
|
|
169
|
+
MemberExpression(node) {
|
|
170
|
+
if (node.object.type === 'Identifier' && node.object.name === 'process' &&
|
|
171
|
+
node.property.type === 'Identifier' && DANGEROUS_PROCESS_PROPS.has(node.property.name)) {
|
|
172
|
+
const f = {
|
|
173
|
+
type: 'process-access',
|
|
174
|
+
severity: node.property.name === 'env' ? 'high' : 'critical',
|
|
175
|
+
message: `process.${node.property.name} — ${getProcessMessage(node.property.name)}`,
|
|
176
|
+
line: node.loc?.start.line,
|
|
177
|
+
};
|
|
178
|
+
findings.push(f);
|
|
179
|
+
dangerous.push(f);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// globalThis[...] / global[...] — computed property access on globals
|
|
183
|
+
if (node.computed && node.object.type === 'Identifier' &&
|
|
184
|
+
(node.object.name === 'globalThis' || node.object.name === 'global')) {
|
|
185
|
+
const f = {
|
|
186
|
+
type: 'global-computed-access',
|
|
187
|
+
severity: 'high',
|
|
188
|
+
message: `Computed property access on ${node.object.name} — can access any global`,
|
|
189
|
+
line: node.loc?.start.line,
|
|
190
|
+
};
|
|
191
|
+
findings.push(f);
|
|
192
|
+
dangerous.push(f);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Reflect.* usage
|
|
196
|
+
if (node.object.type === 'Identifier' && node.object.name === 'Reflect') {
|
|
197
|
+
const f = {
|
|
198
|
+
type: 'reflect',
|
|
199
|
+
severity: 'medium',
|
|
200
|
+
message: `Reflect.${node.property?.name || '?'} — can modify object behavior`,
|
|
201
|
+
line: node.loc?.start.line,
|
|
202
|
+
};
|
|
203
|
+
findings.push(f);
|
|
204
|
+
dangerous.push(f);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// __proto__ access
|
|
208
|
+
if (node.property.type === 'Identifier' && node.property.name === '__proto__') {
|
|
209
|
+
const f = {
|
|
210
|
+
type: 'proto-access',
|
|
211
|
+
severity: 'high',
|
|
212
|
+
message: '__proto__ access — prototype pollution risk',
|
|
213
|
+
line: node.loc?.start.line,
|
|
214
|
+
};
|
|
215
|
+
findings.push(f);
|
|
216
|
+
dangerous.push(f);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ── Proxy usage ──
|
|
222
|
+
walk.simple(ast, {
|
|
223
|
+
NewExpression(node) {
|
|
224
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'Proxy') {
|
|
225
|
+
const f = {
|
|
226
|
+
type: 'proxy',
|
|
227
|
+
severity: 'medium',
|
|
228
|
+
message: 'new Proxy() — can intercept all object operations',
|
|
229
|
+
line: node.loc?.start.line,
|
|
230
|
+
};
|
|
231
|
+
findings.push(f);
|
|
232
|
+
dangerous.push(f);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ── Object.defineProperty on globals ──
|
|
238
|
+
walk.simple(ast, {
|
|
239
|
+
CallExpression(node) {
|
|
240
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
241
|
+
node.callee.object?.name === 'Object' &&
|
|
242
|
+
(node.callee.property?.name === 'defineProperty' || node.callee.property?.name === 'defineProperties')) {
|
|
243
|
+
const target = node.arguments?.[0];
|
|
244
|
+
if (target?.type === 'Identifier' &&
|
|
245
|
+
(target.name === 'globalThis' || target.name === 'global' || target.name === 'process' || target.name === 'window')) {
|
|
246
|
+
const f = {
|
|
247
|
+
type: 'global-modification',
|
|
248
|
+
severity: 'critical',
|
|
249
|
+
message: `Object.${node.callee.property.name}(${target.name}) — modifying global objects`,
|
|
250
|
+
line: node.loc?.start.line,
|
|
251
|
+
};
|
|
252
|
+
findings.push(f);
|
|
253
|
+
dangerous.push(f);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ── String concatenation tricks to build module names ──
|
|
260
|
+
// Check for patterns like: const m = 'child' + '_process'; import(m)
|
|
261
|
+
// This is caught by the non-literal dynamic import check above
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
findings,
|
|
265
|
+
blocked,
|
|
266
|
+
dangerous,
|
|
267
|
+
hasCritical: findings.some(f => f.severity === 'critical'),
|
|
268
|
+
hasHigh: findings.some(f => f.severity === 'high'),
|
|
269
|
+
summary: {
|
|
270
|
+
total: findings.length,
|
|
271
|
+
critical: findings.filter(f => f.severity === 'critical').length,
|
|
272
|
+
high: findings.filter(f => f.severity === 'high').length,
|
|
273
|
+
medium: findings.filter(f => f.severity === 'medium').length,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function analyzeFile(filePath) {
|
|
279
|
+
try {
|
|
280
|
+
const code = fs.readFileSync(filePath, 'utf8');
|
|
281
|
+
return analyzeCode(code, filePath);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
return {
|
|
284
|
+
findings: [{ type: 'read-error', severity: 'high', message: `Cannot read file: ${err.message}` }],
|
|
285
|
+
blocked: [],
|
|
286
|
+
dangerous: [],
|
|
287
|
+
hasCritical: false,
|
|
288
|
+
hasHigh: true,
|
|
289
|
+
summary: { total: 1, critical: 0, high: 1, medium: 0 },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function walkJsFiles(dir, baseName, results = []) {
|
|
295
|
+
try {
|
|
296
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
297
|
+
for (const entry of entries) {
|
|
298
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
299
|
+
const full = path.join(dir, entry.name);
|
|
300
|
+
if (entry.isDirectory()) {
|
|
301
|
+
walkJsFiles(full, baseName, results);
|
|
302
|
+
} else if (entry.isFile() && entry.name.endsWith('.js') && entry.name !== baseName) {
|
|
303
|
+
results.push(full);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch {}
|
|
307
|
+
return results;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function analyzeExtension(extPath, manifest) {
|
|
311
|
+
const mainFile = path.join(extPath, manifest.main || 'index.js');
|
|
312
|
+
const results = [analyzeFile(mainFile)];
|
|
313
|
+
|
|
314
|
+
// Recursively scan all .js files in the extension directory (not node_modules)
|
|
315
|
+
const jsFiles = walkJsFiles(extPath, manifest.main || 'index.js');
|
|
316
|
+
for (const filePath of jsFiles) {
|
|
317
|
+
results.push(analyzeFile(filePath));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const merged = {
|
|
321
|
+
findings: results.flatMap(r => r.findings),
|
|
322
|
+
blocked: results.flatMap(r => r.blocked),
|
|
323
|
+
dangerous: results.flatMap(r => r.dangerous),
|
|
324
|
+
};
|
|
325
|
+
merged.hasCritical = merged.findings.some(f => f.severity === 'critical');
|
|
326
|
+
merged.hasHigh = merged.findings.some(f => f.severity === 'high');
|
|
327
|
+
merged.summary = {
|
|
328
|
+
total: merged.findings.length,
|
|
329
|
+
critical: merged.findings.filter(f => f.severity === 'critical').length,
|
|
330
|
+
high: merged.findings.filter(f => f.severity === 'high').length,
|
|
331
|
+
medium: merged.findings.filter(f => f.severity === 'medium').length,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
return merged;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getProcessMessage(prop) {
|
|
338
|
+
const msgs = {
|
|
339
|
+
exit: 'can crash the host application',
|
|
340
|
+
kill: 'can kill processes',
|
|
341
|
+
abort: 'can crash the process immediately',
|
|
342
|
+
env: 'can access environment secrets',
|
|
343
|
+
dlopen: 'can load native modules',
|
|
344
|
+
binding: 'can access internal Node.js bindings',
|
|
345
|
+
_linkedBinding: 'can access internal Node.js bindings',
|
|
346
|
+
};
|
|
347
|
+
return msgs[prop] || 'dangerous process access';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export {
|
|
351
|
+
analyzeCode,
|
|
352
|
+
analyzeFile,
|
|
353
|
+
analyzeExtension,
|
|
354
|
+
BLOCKED_MODULES,
|
|
355
|
+
DANGEROUS_GLOBALS,
|
|
356
|
+
DANGEROUS_PROCESS_PROPS,
|
|
357
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { Worker, MessageChannel } from 'worker_threads';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const WORKER_PATH = path.join(__dirname, 'extensionWorkerHost.js');
|
|
9
|
+
|
|
10
|
+
const WORKER_MEMORY_LIMIT_MB = 64;
|
|
11
|
+
const HOOK_TIMEOUT_MS = 5000;
|
|
12
|
+
const ACTIVATE_TIMEOUT_MS = 10000;
|
|
13
|
+
|
|
14
|
+
class SandboxedExtension {
|
|
15
|
+
constructor(extPath, manifest, context) {
|
|
16
|
+
this.extPath = extPath;
|
|
17
|
+
this.manifest = manifest;
|
|
18
|
+
this.context = context;
|
|
19
|
+
this.worker = null;
|
|
20
|
+
this.pendingCalls = new Map();
|
|
21
|
+
this.callId = 0;
|
|
22
|
+
this.alive = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async start() {
|
|
26
|
+
const perms = this.manifest.permissions || [];
|
|
27
|
+
const allowedPaths = this._resolveAllowedPaths();
|
|
28
|
+
|
|
29
|
+
this.worker = new Worker(WORKER_PATH, {
|
|
30
|
+
workerData: {
|
|
31
|
+
extPath: this.extPath,
|
|
32
|
+
main: this.manifest.main || 'index.js',
|
|
33
|
+
permissions: perms,
|
|
34
|
+
allowedPaths,
|
|
35
|
+
config: this.context.configSnapshot || {},
|
|
36
|
+
appInfo: this.context.appInfo || {},
|
|
37
|
+
extName: this.manifest.name,
|
|
38
|
+
},
|
|
39
|
+
resourceLimits: {
|
|
40
|
+
maxOldGenerationSizeMb: WORKER_MEMORY_LIMIT_MB,
|
|
41
|
+
maxYoungGenerationSizeMb: WORKER_MEMORY_LIMIT_MB / 4,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
this.alive = true;
|
|
46
|
+
|
|
47
|
+
this.worker.on('message', (msg) => this._handleMessage(msg));
|
|
48
|
+
this.worker.on('error', (err) => {
|
|
49
|
+
console.error(`[sandbox:${this.manifest.name}] Worker error:`, err.message);
|
|
50
|
+
this.alive = false;
|
|
51
|
+
this._rejectAll(err);
|
|
52
|
+
});
|
|
53
|
+
this.worker.on('exit', (code) => {
|
|
54
|
+
this.alive = false;
|
|
55
|
+
if (code !== 0) {
|
|
56
|
+
console.error(`[sandbox:${this.manifest.name}] Worker exited with code ${code}`);
|
|
57
|
+
}
|
|
58
|
+
this._rejectAll(new Error(`Worker exited with code ${code}`));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await this._call('activate', {}, ACTIVATE_TIMEOUT_MS);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async callHook(event, payload) {
|
|
65
|
+
if (!this.alive) return { blocked: false };
|
|
66
|
+
try {
|
|
67
|
+
return await this._call('hook', { event, payload }, HOOK_TIMEOUT_MS);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(`[sandbox:${this.manifest.name}] Hook ${event} error:`, err.message);
|
|
70
|
+
return { blocked: false };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async stop() {
|
|
75
|
+
if (!this.alive) return;
|
|
76
|
+
try {
|
|
77
|
+
await this._call('deactivate', {}, 5000);
|
|
78
|
+
} catch {}
|
|
79
|
+
this.worker.terminate();
|
|
80
|
+
this.alive = false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_call(method, args, timeout) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const id = ++this.callId;
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
this.pendingCalls.delete(id);
|
|
88
|
+
reject(new Error(`Timeout calling ${method} after ${timeout}ms`));
|
|
89
|
+
}, timeout);
|
|
90
|
+
|
|
91
|
+
this.pendingCalls.set(id, { resolve, reject, timer });
|
|
92
|
+
this.worker.postMessage({ type: 'call', id, method, args });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_handleMessage(msg) {
|
|
97
|
+
if (msg.type === 'response') {
|
|
98
|
+
const pending = this.pendingCalls.get(msg.id);
|
|
99
|
+
if (pending) {
|
|
100
|
+
clearTimeout(pending.timer);
|
|
101
|
+
this.pendingCalls.delete(msg.id);
|
|
102
|
+
if (msg.error) pending.reject(new Error(msg.error));
|
|
103
|
+
else pending.resolve(msg.result);
|
|
104
|
+
}
|
|
105
|
+
} else if (msg.type === 'api-call') {
|
|
106
|
+
this._handleApiCall(msg);
|
|
107
|
+
} else if (msg.type === 'log') {
|
|
108
|
+
const level = msg.level || 'info';
|
|
109
|
+
console[level](`[ext:${this.manifest.name}]`, ...msg.args);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async _handleApiCall(msg) {
|
|
114
|
+
const perms = new Set(this.manifest.permissions || []);
|
|
115
|
+
let result, error;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
switch (msg.method) {
|
|
119
|
+
case 'config.get': {
|
|
120
|
+
if (!perms.has('config:read')) throw new Error('Permission denied: config:read');
|
|
121
|
+
result = this.context.configGet(msg.args.key);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case 'config.set': {
|
|
125
|
+
if (!perms.has('config:write')) throw new Error('Permission denied: config:write');
|
|
126
|
+
this.context.configSet(msg.args.key, msg.args.value);
|
|
127
|
+
result = true;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
case 'store.get': {
|
|
131
|
+
if (!perms.has('store:read')) throw new Error('Permission denied: store:read');
|
|
132
|
+
result = this.context.storeGet(msg.args.key);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case 'store.set': {
|
|
136
|
+
if (!perms.has('store:write')) throw new Error('Permission denied: store:write');
|
|
137
|
+
const valStr = JSON.stringify(msg.args.value);
|
|
138
|
+
if (valStr && valStr.length > 1048576) {
|
|
139
|
+
throw new Error('Store value exceeds 1MB limit');
|
|
140
|
+
}
|
|
141
|
+
const storeKeys = this.context.storeKeys?.() || [];
|
|
142
|
+
if (storeKeys.length >= 1000 && !storeKeys.includes(msg.args.key)) {
|
|
143
|
+
throw new Error('Store key limit exceeded (max 1000)');
|
|
144
|
+
}
|
|
145
|
+
this.context.storeSet(msg.args.key, msg.args.value);
|
|
146
|
+
result = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 'store.delete': {
|
|
150
|
+
if (!perms.has('store:write')) throw new Error('Permission denied: store:write');
|
|
151
|
+
this.context.storeDelete(msg.args.key);
|
|
152
|
+
result = true;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'identity.get': {
|
|
156
|
+
if (!perms.has('identity:read')) throw new Error('Permission denied: identity:read');
|
|
157
|
+
result = this.context.getIdentity();
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 'shares.list': {
|
|
161
|
+
if (!perms.has('shares:read')) throw new Error('Permission denied: shares:read');
|
|
162
|
+
result = this.context.listShares();
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case 'peers.list': {
|
|
166
|
+
if (!perms.has('peers:read')) throw new Error('Permission denied: peers:read');
|
|
167
|
+
result = this.context.listPeers();
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case 'http.fetch': {
|
|
171
|
+
if (!perms.has('net:http')) throw new Error('Permission denied: net:http');
|
|
172
|
+
this.context.logHttpCall(msg.args.url, msg.args.opts?.method || 'GET');
|
|
173
|
+
const resp = await fetch(msg.args.url, {
|
|
174
|
+
...msg.args.opts,
|
|
175
|
+
signal: AbortSignal.timeout(30000),
|
|
176
|
+
});
|
|
177
|
+
result = {
|
|
178
|
+
status: resp.status,
|
|
179
|
+
headers: Object.fromEntries(resp.headers),
|
|
180
|
+
body: await resp.text(),
|
|
181
|
+
};
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case 'fs.read': {
|
|
185
|
+
if (!perms.has('fs:read')) throw new Error('Permission denied: fs:read');
|
|
186
|
+
const resolved = path.resolve(msg.args.filePath);
|
|
187
|
+
this._enforcePathScope(resolved);
|
|
188
|
+
result = fs.readFileSync(resolved, msg.args.encoding || 'utf8');
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'fs.write': {
|
|
192
|
+
if (!perms.has('fs:write')) throw new Error('Permission denied: fs:write');
|
|
193
|
+
const resolvedW = path.resolve(msg.args.filePath);
|
|
194
|
+
this._enforcePathScope(resolvedW);
|
|
195
|
+
fs.writeFileSync(resolvedW, msg.args.data);
|
|
196
|
+
result = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case 'fs.delete': {
|
|
200
|
+
if (!perms.has('fs:delete')) throw new Error('Permission denied: fs:delete');
|
|
201
|
+
const resolvedD = path.resolve(msg.args.filePath);
|
|
202
|
+
this._enforcePathScope(resolvedD);
|
|
203
|
+
fs.unlinkSync(resolvedD);
|
|
204
|
+
result = true;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case 'notify.show': {
|
|
208
|
+
if (!perms.has('notifications')) throw new Error('Permission denied: notifications');
|
|
209
|
+
this.context.notify(msg.args.title, msg.args.body);
|
|
210
|
+
result = true;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
default:
|
|
214
|
+
throw new Error(`Unknown API method: ${msg.method}`);
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
error = err.message;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.worker.postMessage({ type: 'api-response', id: msg.id, result, error });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_enforcePathScope(filePath) {
|
|
224
|
+
let resolved;
|
|
225
|
+
try { resolved = fs.realpathSync(filePath); } catch { resolved = path.resolve(filePath); }
|
|
226
|
+
const allowed = this._resolveAllowedPaths();
|
|
227
|
+
const inScope = allowed.some(p => resolved.startsWith(p + path.sep) || resolved === p);
|
|
228
|
+
if (!inScope) {
|
|
229
|
+
throw new Error(`Path access denied: ${filePath} is outside allowed paths`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
_resolveAllowedPaths() {
|
|
234
|
+
if (this._cachedPaths) return this._cachedPaths;
|
|
235
|
+
const shares = this.context.listShares ? this.context.listShares() : [];
|
|
236
|
+
const sharePaths = shares.map(s => path.resolve(s.path)).filter(Boolean);
|
|
237
|
+
this._cachedPaths = sharePaths.length > 0 ? sharePaths : [];
|
|
238
|
+
return this._cachedPaths;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_rejectAll(err) {
|
|
242
|
+
for (const [id, pending] of this.pendingCalls) {
|
|
243
|
+
clearTimeout(pending.timer);
|
|
244
|
+
pending.reject(err);
|
|
245
|
+
}
|
|
246
|
+
this.pendingCalls.clear();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export { SandboxedExtension, WORKER_MEMORY_LIMIT_MB, HOOK_TIMEOUT_MS, ACTIVATE_TIMEOUT_MS };
|