lsh-framework 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +51 -0
- package/README.md +399 -0
- package/dist/app.js +33 -0
- package/dist/cicd/analytics.js +261 -0
- package/dist/cicd/auth.js +269 -0
- package/dist/cicd/cache-manager.js +172 -0
- package/dist/cicd/data-retention.js +305 -0
- package/dist/cicd/performance-monitor.js +224 -0
- package/dist/cicd/webhook-receiver.js +634 -0
- package/dist/cli.js +500 -0
- package/dist/commands/api.js +343 -0
- package/dist/commands/self.js +318 -0
- package/dist/commands/theme.js +257 -0
- package/dist/commands/zsh-import.js +240 -0
- package/dist/components/App.js +1 -0
- package/dist/components/Divider.js +29 -0
- package/dist/components/REPL.js +43 -0
- package/dist/components/Terminal.js +232 -0
- package/dist/components/UserInput.js +30 -0
- package/dist/daemon/api-server.js +315 -0
- package/dist/daemon/job-registry.js +554 -0
- package/dist/daemon/lshd.js +822 -0
- package/dist/daemon/monitoring-api.js +220 -0
- package/dist/examples/supabase-integration.js +106 -0
- package/dist/lib/api-error-handler.js +183 -0
- package/dist/lib/associative-arrays.js +285 -0
- package/dist/lib/base-api-server.js +290 -0
- package/dist/lib/base-command-registrar.js +286 -0
- package/dist/lib/base-job-manager.js +293 -0
- package/dist/lib/brace-expansion.js +160 -0
- package/dist/lib/builtin-commands.js +439 -0
- package/dist/lib/cloud-config-manager.js +347 -0
- package/dist/lib/command-validator.js +190 -0
- package/dist/lib/completion-system.js +344 -0
- package/dist/lib/cron-job-manager.js +364 -0
- package/dist/lib/daemon-client-helper.js +141 -0
- package/dist/lib/daemon-client.js +501 -0
- package/dist/lib/database-persistence.js +638 -0
- package/dist/lib/database-schema.js +259 -0
- package/dist/lib/enhanced-history-system.js +246 -0
- package/dist/lib/env-validator.js +265 -0
- package/dist/lib/executors/builtin-executor.js +52 -0
- package/dist/lib/extended-globbing.js +411 -0
- package/dist/lib/extended-parameter-expansion.js +227 -0
- package/dist/lib/floating-point-arithmetic.js +256 -0
- package/dist/lib/history-system.js +245 -0
- package/dist/lib/interactive-shell.js +460 -0
- package/dist/lib/job-builtins.js +580 -0
- package/dist/lib/job-manager.js +386 -0
- package/dist/lib/job-storage-database.js +156 -0
- package/dist/lib/job-storage-memory.js +73 -0
- package/dist/lib/logger.js +274 -0
- package/dist/lib/lshrc-init.js +177 -0
- package/dist/lib/pathname-expansion.js +216 -0
- package/dist/lib/prompt-system.js +328 -0
- package/dist/lib/script-runner.js +226 -0
- package/dist/lib/secrets-manager.js +193 -0
- package/dist/lib/shell-executor.js +2504 -0
- package/dist/lib/shell-parser.js +958 -0
- package/dist/lib/shell-types.js +6 -0
- package/dist/lib/shell.lib.js +40 -0
- package/dist/lib/supabase-client.js +58 -0
- package/dist/lib/theme-manager.js +476 -0
- package/dist/lib/variable-expansion.js +385 -0
- package/dist/lib/zsh-compatibility.js +658 -0
- package/dist/lib/zsh-import-manager.js +699 -0
- package/dist/lib/zsh-options.js +328 -0
- package/dist/pipeline/job-tracker.js +491 -0
- package/dist/pipeline/mcli-bridge.js +302 -0
- package/dist/pipeline/pipeline-service.js +1116 -0
- package/dist/pipeline/workflow-engine.js +867 -0
- package/dist/services/api/api.js +58 -0
- package/dist/services/api/auth.js +35 -0
- package/dist/services/api/config.js +7 -0
- package/dist/services/api/file.js +22 -0
- package/dist/services/cron/cron-registrar.js +235 -0
- package/dist/services/cron/cron.js +9 -0
- package/dist/services/daemon/daemon-registrar.js +565 -0
- package/dist/services/daemon/daemon.js +9 -0
- package/dist/services/lib/lib.js +86 -0
- package/dist/services/log-file-extractor.js +170 -0
- package/dist/services/secrets/secrets.js +94 -0
- package/dist/services/shell/shell.js +28 -0
- package/dist/services/supabase/supabase-registrar.js +367 -0
- package/dist/services/supabase/supabase.js +9 -0
- package/dist/services/zapier.js +16 -0
- package/dist/simple-api-server.js +148 -0
- package/dist/store/store.js +31 -0
- package/dist/util/lib.util.js +11 -0
- package/package.json +144 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud Configuration Manager
|
|
3
|
+
* Manages shell configuration with Supabase persistence
|
|
4
|
+
*/
|
|
5
|
+
import DatabasePersistence from './database-persistence.js';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
export class CloudConfigManager {
|
|
10
|
+
databasePersistence;
|
|
11
|
+
options;
|
|
12
|
+
localConfig = new Map();
|
|
13
|
+
cloudConfig = new Map();
|
|
14
|
+
syncTimer;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.options = {
|
|
17
|
+
userId: undefined,
|
|
18
|
+
enableCloudSync: true,
|
|
19
|
+
localConfigPath: path.join(os.homedir(), '.lshrc'),
|
|
20
|
+
syncInterval: 60000, // 1 minute
|
|
21
|
+
...options,
|
|
22
|
+
};
|
|
23
|
+
this.databasePersistence = new DatabasePersistence(this.options.userId);
|
|
24
|
+
this.loadLocalConfig();
|
|
25
|
+
if (this.options.enableCloudSync) {
|
|
26
|
+
this.initializeCloudSync();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Load local configuration file
|
|
31
|
+
*/
|
|
32
|
+
loadLocalConfig() {
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(this.options.localConfigPath)) {
|
|
35
|
+
const content = fs.readFileSync(this.options.localConfigPath, 'utf8');
|
|
36
|
+
const config = JSON.parse(content);
|
|
37
|
+
Object.entries(config).forEach(([key, value]) => {
|
|
38
|
+
this.localConfig.set(key, {
|
|
39
|
+
key,
|
|
40
|
+
value,
|
|
41
|
+
type: this.getType(value),
|
|
42
|
+
isDefault: false,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.error('Failed to load local config:', error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Save local configuration file
|
|
53
|
+
*/
|
|
54
|
+
saveLocalConfig() {
|
|
55
|
+
try {
|
|
56
|
+
const config = {};
|
|
57
|
+
this.localConfig.forEach((configValue, key) => {
|
|
58
|
+
config[key] = configValue.value;
|
|
59
|
+
});
|
|
60
|
+
fs.writeFileSync(this.options.localConfigPath, JSON.stringify(config, null, 2));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error('Failed to save local config:', error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get type of a value
|
|
68
|
+
*/
|
|
69
|
+
getType(value) {
|
|
70
|
+
if (typeof value === 'string')
|
|
71
|
+
return 'string';
|
|
72
|
+
if (typeof value === 'number')
|
|
73
|
+
return 'number';
|
|
74
|
+
if (typeof value === 'boolean')
|
|
75
|
+
return 'boolean';
|
|
76
|
+
if (Array.isArray(value))
|
|
77
|
+
return 'array';
|
|
78
|
+
if (typeof value === 'object' && value !== null)
|
|
79
|
+
return 'object';
|
|
80
|
+
return 'string';
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Initialize cloud synchronization
|
|
84
|
+
*/
|
|
85
|
+
async initializeCloudSync() {
|
|
86
|
+
try {
|
|
87
|
+
const isConnected = await this.databasePersistence.testConnection();
|
|
88
|
+
if (isConnected) {
|
|
89
|
+
console.log('✅ Cloud config sync enabled');
|
|
90
|
+
await this.loadCloudConfig();
|
|
91
|
+
this.startSyncTimer();
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.log('⚠️ Cloud config sync disabled - database not available');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error('Failed to initialize cloud config sync:', error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Load configuration from cloud
|
|
103
|
+
*/
|
|
104
|
+
async loadCloudConfig() {
|
|
105
|
+
try {
|
|
106
|
+
const cloudConfigs = await this.databasePersistence.getConfiguration();
|
|
107
|
+
cloudConfigs.forEach(config => {
|
|
108
|
+
this.cloudConfig.set(config.config_key, {
|
|
109
|
+
key: config.config_key,
|
|
110
|
+
value: this.parseConfigValue(config.config_value, config.config_type),
|
|
111
|
+
type: config.config_type,
|
|
112
|
+
description: config.description,
|
|
113
|
+
isDefault: config.is_default,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
// Merge cloud config with local config
|
|
117
|
+
this.mergeConfigurations();
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.error('Failed to load cloud config:', error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Parse configuration value based on type
|
|
125
|
+
*/
|
|
126
|
+
parseConfigValue(value, type) {
|
|
127
|
+
try {
|
|
128
|
+
switch (type) {
|
|
129
|
+
case 'string':
|
|
130
|
+
return value;
|
|
131
|
+
case 'number':
|
|
132
|
+
return parseFloat(value);
|
|
133
|
+
case 'boolean':
|
|
134
|
+
return value === 'true';
|
|
135
|
+
case 'array':
|
|
136
|
+
case 'object':
|
|
137
|
+
return JSON.parse(value);
|
|
138
|
+
default:
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
console.error(`Failed to parse config value ${value} as ${type}:`, error);
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Serialize configuration value to string
|
|
149
|
+
*/
|
|
150
|
+
serializeConfigValue(value, type) {
|
|
151
|
+
switch (type) {
|
|
152
|
+
case 'string':
|
|
153
|
+
case 'number':
|
|
154
|
+
case 'boolean':
|
|
155
|
+
return String(value);
|
|
156
|
+
case 'array':
|
|
157
|
+
case 'object':
|
|
158
|
+
return JSON.stringify(value);
|
|
159
|
+
default:
|
|
160
|
+
return String(value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Merge cloud and local configurations
|
|
165
|
+
*/
|
|
166
|
+
mergeConfigurations() {
|
|
167
|
+
// Cloud config takes precedence for non-local overrides
|
|
168
|
+
this.cloudConfig.forEach((cloudValue, key) => {
|
|
169
|
+
if (!this.localConfig.has(key)) {
|
|
170
|
+
this.localConfig.set(key, cloudValue);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Start periodic synchronization timer
|
|
176
|
+
*/
|
|
177
|
+
startSyncTimer() {
|
|
178
|
+
this.syncTimer = setInterval(() => {
|
|
179
|
+
this.syncToCloud();
|
|
180
|
+
}, this.options.syncInterval);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Stop synchronization timer
|
|
184
|
+
*/
|
|
185
|
+
stopSyncTimer() {
|
|
186
|
+
if (this.syncTimer) {
|
|
187
|
+
clearInterval(this.syncTimer);
|
|
188
|
+
this.syncTimer = undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Synchronize local configuration to cloud
|
|
193
|
+
*/
|
|
194
|
+
async syncToCloud() {
|
|
195
|
+
if (!this.options.enableCloudSync) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
for (const [key, configValue] of this.localConfig) {
|
|
200
|
+
await this.databasePersistence.saveConfiguration({
|
|
201
|
+
config_key: key,
|
|
202
|
+
config_value: this.serializeConfigValue(configValue.value, configValue.type),
|
|
203
|
+
config_type: configValue.type,
|
|
204
|
+
description: configValue.description,
|
|
205
|
+
is_default: configValue.isDefault,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
console.error('Failed to sync config to cloud:', error);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get configuration value
|
|
215
|
+
*/
|
|
216
|
+
get(key, defaultValue) {
|
|
217
|
+
const configValue = this.localConfig.get(key);
|
|
218
|
+
return configValue ? configValue.value : defaultValue;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Set configuration value
|
|
222
|
+
*/
|
|
223
|
+
set(key, value, description) {
|
|
224
|
+
const configValue = {
|
|
225
|
+
key,
|
|
226
|
+
value,
|
|
227
|
+
type: this.getType(value),
|
|
228
|
+
description,
|
|
229
|
+
isDefault: false,
|
|
230
|
+
};
|
|
231
|
+
this.localConfig.set(key, configValue);
|
|
232
|
+
this.saveLocalConfig();
|
|
233
|
+
// Sync to cloud if enabled
|
|
234
|
+
if (this.options.enableCloudSync) {
|
|
235
|
+
this.syncToCloud().catch(error => {
|
|
236
|
+
console.error('Failed to sync config to cloud:', error);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get all configuration keys
|
|
242
|
+
*/
|
|
243
|
+
getKeys() {
|
|
244
|
+
return Array.from(this.localConfig.keys());
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Get all configuration entries
|
|
248
|
+
*/
|
|
249
|
+
getAll() {
|
|
250
|
+
return Array.from(this.localConfig.values());
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Check if configuration key exists
|
|
254
|
+
*/
|
|
255
|
+
has(key) {
|
|
256
|
+
return this.localConfig.has(key);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Delete configuration key
|
|
260
|
+
*/
|
|
261
|
+
delete(key) {
|
|
262
|
+
this.localConfig.delete(key);
|
|
263
|
+
this.saveLocalConfig();
|
|
264
|
+
// Sync to cloud if enabled
|
|
265
|
+
if (this.options.enableCloudSync) {
|
|
266
|
+
this.syncToCloud().catch(error => {
|
|
267
|
+
console.error('Failed to sync config deletion to cloud:', error);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Reset configuration to defaults
|
|
273
|
+
*/
|
|
274
|
+
reset() {
|
|
275
|
+
this.localConfig.clear();
|
|
276
|
+
this.saveLocalConfig();
|
|
277
|
+
if (this.options.enableCloudSync) {
|
|
278
|
+
this.syncToCloud().catch(error => {
|
|
279
|
+
console.error('Failed to sync config reset to cloud:', error);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Export configuration to JSON
|
|
285
|
+
*/
|
|
286
|
+
export() {
|
|
287
|
+
const config = {};
|
|
288
|
+
this.localConfig.forEach((configValue, key) => {
|
|
289
|
+
config[key] = configValue.value;
|
|
290
|
+
});
|
|
291
|
+
return JSON.stringify(config, null, 2);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Import configuration from JSON
|
|
295
|
+
*/
|
|
296
|
+
import(configJson) {
|
|
297
|
+
try {
|
|
298
|
+
const config = JSON.parse(configJson);
|
|
299
|
+
Object.entries(config).forEach(([key, value]) => {
|
|
300
|
+
this.set(key, value);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
console.error('Failed to import configuration:', error);
|
|
305
|
+
throw new Error('Invalid configuration JSON');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Get configuration statistics
|
|
310
|
+
*/
|
|
311
|
+
getStats() {
|
|
312
|
+
const types = {};
|
|
313
|
+
this.localConfig.forEach(configValue => {
|
|
314
|
+
types[configValue.type] = (types[configValue.type] || 0) + 1;
|
|
315
|
+
});
|
|
316
|
+
return {
|
|
317
|
+
totalKeys: this.localConfig.size,
|
|
318
|
+
localKeys: this.localConfig.size,
|
|
319
|
+
cloudKeys: this.cloudConfig.size,
|
|
320
|
+
types,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Enable or disable cloud sync
|
|
325
|
+
*/
|
|
326
|
+
setCloudSyncEnabled(enabled) {
|
|
327
|
+
this.options.enableCloudSync = enabled;
|
|
328
|
+
if (enabled) {
|
|
329
|
+
this.initializeCloudSync();
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
this.stopSyncTimer();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Cleanup resources
|
|
337
|
+
*/
|
|
338
|
+
destroy() {
|
|
339
|
+
this.stopSyncTimer();
|
|
340
|
+
if (this.options.enableCloudSync) {
|
|
341
|
+
this.syncToCloud().catch(error => {
|
|
342
|
+
console.error('Failed to final config sync on destroy:', error);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
export default CloudConfigManager;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Validation Utilities
|
|
3
|
+
* Provides security validation for shell commands
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Dangerous command patterns that should trigger warnings or blocks
|
|
7
|
+
*/
|
|
8
|
+
const DANGEROUS_PATTERNS = [
|
|
9
|
+
// System modification
|
|
10
|
+
{ pattern: /rm\s+-rf\s+\/(?!\w)/i, message: 'Attempting to delete root filesystem', level: 'critical' },
|
|
11
|
+
{ pattern: /mkfs/i, message: 'Filesystem formatting command detected', level: 'critical' },
|
|
12
|
+
{ pattern: /dd\s+.*of=/i, message: 'Direct disk write detected', level: 'critical' },
|
|
13
|
+
// Privilege escalation
|
|
14
|
+
{ pattern: /sudo\s+su/i, message: 'Privilege escalation attempt', level: 'high' },
|
|
15
|
+
{ pattern: /sudo\s+.*passwd/i, message: 'Password modification attempt', level: 'high' },
|
|
16
|
+
// Network exfiltration
|
|
17
|
+
{ pattern: /curl\s+.*\|\s*bash/i, message: 'Remote code execution via curl', level: 'critical' },
|
|
18
|
+
{ pattern: /wget\s+.*\|\s*sh/i, message: 'Remote code execution via wget', level: 'critical' },
|
|
19
|
+
{ pattern: /nc\s+.*-e/i, message: 'Reverse shell attempt with netcat', level: 'critical' },
|
|
20
|
+
// Data exfiltration
|
|
21
|
+
{ pattern: /cat\s+\/etc\/shadow/i, message: 'Attempting to read shadow password file', level: 'critical' },
|
|
22
|
+
{ pattern: /cat\s+\/etc\/passwd/i, message: 'Attempting to read user account file', level: 'high' },
|
|
23
|
+
{ pattern: /\.ssh\/id_rsa/i, message: 'Attempting to access SSH private keys', level: 'critical' },
|
|
24
|
+
// Process manipulation
|
|
25
|
+
{ pattern: /kill\s+-9\s+1\b/i, message: 'Attempting to kill init process', level: 'critical' },
|
|
26
|
+
{ pattern: /pkill\s+-9\s+.*sshd/i, message: 'Attempting to kill SSH daemon', level: 'high' },
|
|
27
|
+
// Obfuscation attempts
|
|
28
|
+
{ pattern: /\$\(.*base64.*\)/i, message: 'Base64 encoded command detected', level: 'high' },
|
|
29
|
+
{ pattern: /eval.*\$\(/i, message: 'Dynamic command evaluation detected', level: 'high' },
|
|
30
|
+
// eslint-disable-next-line no-control-regex
|
|
31
|
+
{ pattern: /\x00/i, message: 'Null byte injection detected', level: 'critical' },
|
|
32
|
+
];
|
|
33
|
+
/**
|
|
34
|
+
* Patterns that should trigger warnings but might be legitimate
|
|
35
|
+
*/
|
|
36
|
+
const WARNING_PATTERNS = [
|
|
37
|
+
{ pattern: /rm\s+-rf/i, message: 'Recursive deletion command' },
|
|
38
|
+
{ pattern: /sudo/i, message: 'Elevated privileges requested' },
|
|
39
|
+
{ pattern: /chmod\s+777/i, message: 'Overly permissive file permissions' },
|
|
40
|
+
{ pattern: />\s*\/dev\/sda/i, message: 'Writing to disk device' },
|
|
41
|
+
{ pattern: /curl\s+.*-k/i, message: 'Insecure SSL certificate validation disabled' },
|
|
42
|
+
{ pattern: /:\(\)\{.*:\|:.*\}/i, message: 'Fork bomb pattern detected' },
|
|
43
|
+
];
|
|
44
|
+
/**
|
|
45
|
+
* Validate a shell command for security issues
|
|
46
|
+
*/
|
|
47
|
+
export function validateCommand(command, options = {}) {
|
|
48
|
+
const { allowDangerousCommands = false, maxLength = 10000, requireWhitelist = false, whitelist = [] } = options;
|
|
49
|
+
const result = {
|
|
50
|
+
isValid: true,
|
|
51
|
+
warnings: [],
|
|
52
|
+
errors: [],
|
|
53
|
+
riskLevel: 'low'
|
|
54
|
+
};
|
|
55
|
+
// Basic validation
|
|
56
|
+
if (!command || typeof command !== 'string') {
|
|
57
|
+
result.isValid = false;
|
|
58
|
+
result.errors.push('Command must be a non-empty string');
|
|
59
|
+
result.riskLevel = 'high';
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
if (command.trim().length === 0) {
|
|
63
|
+
result.isValid = false;
|
|
64
|
+
result.errors.push('Command cannot be empty or whitespace only');
|
|
65
|
+
result.riskLevel = 'high';
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
if (command.length > maxLength) {
|
|
69
|
+
result.isValid = false;
|
|
70
|
+
result.errors.push(`Command exceeds maximum length of ${maxLength} characters`);
|
|
71
|
+
result.riskLevel = 'high';
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
// Whitelist validation
|
|
75
|
+
if (requireWhitelist) {
|
|
76
|
+
const commandName = command.trim().split(/\s+/)[0];
|
|
77
|
+
if (!whitelist.includes(commandName)) {
|
|
78
|
+
result.isValid = false;
|
|
79
|
+
result.errors.push(`Command '${commandName}' is not in whitelist`);
|
|
80
|
+
result.riskLevel = 'high';
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Check for dangerous patterns
|
|
85
|
+
for (const { pattern, message, level } of DANGEROUS_PATTERNS) {
|
|
86
|
+
if (pattern.test(command)) {
|
|
87
|
+
if (!allowDangerousCommands) {
|
|
88
|
+
result.isValid = false;
|
|
89
|
+
result.errors.push(`BLOCKED: ${message}`);
|
|
90
|
+
result.riskLevel = level;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
result.warnings.push(`${message} (allowed by configuration)`);
|
|
94
|
+
if (level === 'critical' || level === 'high') {
|
|
95
|
+
result.riskLevel = level;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Check for warning patterns
|
|
101
|
+
for (const { pattern, message } of WARNING_PATTERNS) {
|
|
102
|
+
if (pattern.test(command)) {
|
|
103
|
+
result.warnings.push(message);
|
|
104
|
+
if (result.riskLevel === 'low') {
|
|
105
|
+
result.riskLevel = 'medium';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Additional checks for suspicious patterns
|
|
110
|
+
const suspiciousChecks = [
|
|
111
|
+
{
|
|
112
|
+
test: () => (command.match(/;/g) || []).length > 5,
|
|
113
|
+
message: 'Excessive command chaining detected',
|
|
114
|
+
level: 'medium'
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
test: () => (command.match(/\|/g) || []).length > 3,
|
|
118
|
+
message: 'Excessive pipe usage detected',
|
|
119
|
+
level: 'medium'
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
test: () => /\$\([^)]*\$\(/.test(command),
|
|
123
|
+
message: 'Nested command substitution detected',
|
|
124
|
+
level: 'high'
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
// eslint-disable-next-line no-control-regex
|
|
128
|
+
test: () => /[\x00-\x08\x0B\x0C\x0E-\x1F]/.test(command),
|
|
129
|
+
message: 'Control characters detected in command',
|
|
130
|
+
level: 'high'
|
|
131
|
+
}
|
|
132
|
+
];
|
|
133
|
+
for (const check of suspiciousChecks) {
|
|
134
|
+
if (check.test()) {
|
|
135
|
+
result.warnings.push(check.message);
|
|
136
|
+
if (check.level === 'high' && result.riskLevel !== 'critical') {
|
|
137
|
+
result.riskLevel = check.level;
|
|
138
|
+
}
|
|
139
|
+
else if (check.level === 'medium' && result.riskLevel === 'low') {
|
|
140
|
+
result.riskLevel = check.level;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Sanitize a string for use as a shell argument
|
|
148
|
+
* Note: This should be used for individual arguments, not full commands
|
|
149
|
+
*/
|
|
150
|
+
export function sanitizeShellArgument(arg) {
|
|
151
|
+
// Escape dangerous shell characters
|
|
152
|
+
return arg.replace(/([;&|`$(){}[\]\\<>'"*?~])/g, '\\$1');
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Quote a string for safe use in shell commands
|
|
156
|
+
*/
|
|
157
|
+
export function quoteForShell(str) {
|
|
158
|
+
// Use single quotes to prevent expansion, escape any single quotes in the string
|
|
159
|
+
return `'${str.replace(/'/g, "'\\''")}'`;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Parse command and extract the base command name
|
|
163
|
+
*/
|
|
164
|
+
export function getCommandName(command) {
|
|
165
|
+
const trimmed = command.trim();
|
|
166
|
+
// Handle sudo and other prefixes
|
|
167
|
+
const parts = trimmed.split(/\s+/);
|
|
168
|
+
let cmdIndex = 0;
|
|
169
|
+
// Handle 'sudo' prefix
|
|
170
|
+
if (parts[0] === 'sudo') {
|
|
171
|
+
cmdIndex = 1;
|
|
172
|
+
}
|
|
173
|
+
// Handle 'env' prefix with environment variables
|
|
174
|
+
else if (parts[0] === 'env') {
|
|
175
|
+
// Skip environment variable assignments (VAR=value format)
|
|
176
|
+
cmdIndex = 1;
|
|
177
|
+
while (cmdIndex < parts.length && parts[cmdIndex].includes('=')) {
|
|
178
|
+
cmdIndex++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const cmdPart = parts[cmdIndex] || '';
|
|
182
|
+
// Remove path if present
|
|
183
|
+
return cmdPart.split('/').pop() || '';
|
|
184
|
+
}
|
|
185
|
+
export default {
|
|
186
|
+
validateCommand,
|
|
187
|
+
sanitizeShellArgument,
|
|
188
|
+
quoteForShell,
|
|
189
|
+
getCommandName
|
|
190
|
+
};
|