mcp-maestro-mobile-ai 1.3.1 → 1.6.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/CHANGELOG.md +344 -152
- package/ROADMAP.md +21 -8
- package/package.json +9 -3
- package/src/mcp-server/index.js +1394 -826
- package/src/mcp-server/schemas/toolSchemas.js +820 -0
- package/src/mcp-server/tools/contextTools.js +309 -2
- package/src/mcp-server/tools/runTools.js +409 -31
- package/src/mcp-server/utils/knownIssues.js +564 -0
- package/src/mcp-server/utils/maestro.js +265 -29
- package/src/mcp-server/utils/promptAnalyzer.js +701 -0
- package/src/mcp-server/utils/security.js +1200 -0
- package/src/mcp-server/utils/yamlCache.js +381 -0
- package/src/mcp-server/utils/yamlGenerator.js +426 -0
- package/src/mcp-server/utils/yamlTemplate.js +303 -0
|
@@ -0,0 +1,1200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Utility Module
|
|
3
|
+
*
|
|
4
|
+
* Implements security boundaries for the MCP Maestro Mobile AI server.
|
|
5
|
+
* Includes Safe Mode, command allowlists, blocked operations, and input validation.
|
|
6
|
+
*
|
|
7
|
+
* @module security
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { logger } from "./logger.js";
|
|
12
|
+
|
|
13
|
+
// ============================================
|
|
14
|
+
// CONSTANTS & CONFIGURATION
|
|
15
|
+
// ============================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Security modes available
|
|
19
|
+
*/
|
|
20
|
+
export const SecurityMode = {
|
|
21
|
+
SAFE: "safe",
|
|
22
|
+
FULL: "full",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Security error codes
|
|
27
|
+
*/
|
|
28
|
+
export const SecurityErrorCode = {
|
|
29
|
+
SAFE_MODE_VIOLATION: "SAFE_MODE_VIOLATION",
|
|
30
|
+
BLOCKED_COMMAND: "BLOCKED_COMMAND",
|
|
31
|
+
BLOCKED_PATTERN: "BLOCKED_PATTERN",
|
|
32
|
+
INVALID_INPUT: "INVALID_INPUT",
|
|
33
|
+
UNAUTHORIZED_OPERATION: "UNAUTHORIZED_OPERATION",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ============================================
|
|
37
|
+
// SECURITY ERROR CLASS
|
|
38
|
+
// ============================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Custom error class for security violations
|
|
42
|
+
*/
|
|
43
|
+
export class SecurityError extends Error {
|
|
44
|
+
/**
|
|
45
|
+
* Create a SecurityError
|
|
46
|
+
* @param {string} message - Error message
|
|
47
|
+
* @param {string} code - Error code from SecurityErrorCode
|
|
48
|
+
* @param {object} details - Additional details about the error
|
|
49
|
+
*/
|
|
50
|
+
constructor(
|
|
51
|
+
message,
|
|
52
|
+
code = SecurityErrorCode.UNAUTHORIZED_OPERATION,
|
|
53
|
+
details = {}
|
|
54
|
+
) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = "SecurityError";
|
|
57
|
+
this.code = code;
|
|
58
|
+
this.details = details;
|
|
59
|
+
this.timestamp = new Date().toISOString();
|
|
60
|
+
|
|
61
|
+
// Log security event
|
|
62
|
+
logger.warn(`Security violation: ${code}`, {
|
|
63
|
+
message,
|
|
64
|
+
code,
|
|
65
|
+
details,
|
|
66
|
+
timestamp: this.timestamp,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Convert error to JSON for API responses
|
|
72
|
+
*/
|
|
73
|
+
toJSON() {
|
|
74
|
+
return {
|
|
75
|
+
error: this.name,
|
|
76
|
+
message: this.message,
|
|
77
|
+
code: this.code,
|
|
78
|
+
details: this.details,
|
|
79
|
+
timestamp: this.timestamp,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================
|
|
85
|
+
// SAFE MODE FUNCTIONS
|
|
86
|
+
// ============================================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if Safe Mode is enabled
|
|
90
|
+
* Safe Mode is ON by default for security
|
|
91
|
+
*
|
|
92
|
+
* @returns {boolean} True if Safe Mode is enabled
|
|
93
|
+
*/
|
|
94
|
+
export function isSafeModeEnabled() {
|
|
95
|
+
const safeMode = process.env.SAFE_MODE;
|
|
96
|
+
|
|
97
|
+
// Safe Mode is ON by default (secure by default)
|
|
98
|
+
// Only disable if explicitly set to 'false'
|
|
99
|
+
if (safeMode === "false" || safeMode === "0") {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the current security mode
|
|
108
|
+
*
|
|
109
|
+
* @returns {string} Current security mode ('safe' or 'full')
|
|
110
|
+
*/
|
|
111
|
+
export function getSecurityMode() {
|
|
112
|
+
return isSafeModeEnabled() ? SecurityMode.SAFE : SecurityMode.FULL;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get security configuration summary
|
|
117
|
+
*
|
|
118
|
+
* @returns {object} Security configuration details
|
|
119
|
+
*/
|
|
120
|
+
export function getSecurityConfig() {
|
|
121
|
+
return {
|
|
122
|
+
safeMode: isSafeModeEnabled(),
|
|
123
|
+
mode: getSecurityMode(),
|
|
124
|
+
logSecurityEvents: process.env.LOG_SECURITY_EVENTS !== "false",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================
|
|
129
|
+
// INPUT SANITIZATION
|
|
130
|
+
// ============================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Sanitize a string input by removing dangerous characters
|
|
134
|
+
*
|
|
135
|
+
* @param {string} input - Input string to sanitize
|
|
136
|
+
* @returns {string} Sanitized string
|
|
137
|
+
*/
|
|
138
|
+
export function sanitizeInput(input) {
|
|
139
|
+
if (typeof input !== "string") {
|
|
140
|
+
return input;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Remove null bytes
|
|
144
|
+
let sanitized = input.replace(/\0/g, "");
|
|
145
|
+
|
|
146
|
+
// Remove control characters (except newlines and tabs)
|
|
147
|
+
// Using character code range to avoid linter issues with control chars
|
|
148
|
+
sanitized = sanitized
|
|
149
|
+
.split("")
|
|
150
|
+
.filter((char) => {
|
|
151
|
+
const code = char.charCodeAt(0);
|
|
152
|
+
// Allow printable ASCII, newlines (\n = 10), carriage return (\r = 13), and tabs (\t = 9)
|
|
153
|
+
return (
|
|
154
|
+
code === 9 ||
|
|
155
|
+
code === 10 ||
|
|
156
|
+
code === 13 ||
|
|
157
|
+
(code >= 32 && code <= 126) ||
|
|
158
|
+
code > 127
|
|
159
|
+
);
|
|
160
|
+
})
|
|
161
|
+
.join("");
|
|
162
|
+
|
|
163
|
+
return sanitized;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Sanitize a file path
|
|
168
|
+
*
|
|
169
|
+
* @param {string} filePath - File path to sanitize
|
|
170
|
+
* @returns {string} Sanitized file path
|
|
171
|
+
* @throws {SecurityError} If path contains dangerous patterns
|
|
172
|
+
*/
|
|
173
|
+
export function sanitizeFilePath(filePath) {
|
|
174
|
+
if (typeof filePath !== "string") {
|
|
175
|
+
throw new SecurityError(
|
|
176
|
+
"File path must be a string",
|
|
177
|
+
SecurityErrorCode.INVALID_INPUT,
|
|
178
|
+
{ received: typeof filePath }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Normalize path separators
|
|
183
|
+
let sanitized = filePath.replace(/\\/g, "/");
|
|
184
|
+
|
|
185
|
+
// Remove null bytes
|
|
186
|
+
sanitized = sanitized.replace(/\0/g, "");
|
|
187
|
+
|
|
188
|
+
// Check for path traversal attempts
|
|
189
|
+
if (sanitized.includes("..")) {
|
|
190
|
+
throw new SecurityError(
|
|
191
|
+
"Path traversal not allowed",
|
|
192
|
+
SecurityErrorCode.BLOCKED_PATTERN,
|
|
193
|
+
{ path: filePath, pattern: ".." }
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return sanitized;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Validate that an app ID is properly formatted
|
|
202
|
+
*
|
|
203
|
+
* @param {string} appId - App package ID to validate
|
|
204
|
+
* @returns {boolean} True if valid
|
|
205
|
+
* @throws {SecurityError} If app ID is invalid
|
|
206
|
+
*/
|
|
207
|
+
export function validateAppId(appId) {
|
|
208
|
+
if (typeof appId !== "string" || appId.length === 0) {
|
|
209
|
+
throw new SecurityError(
|
|
210
|
+
"App ID must be a non-empty string",
|
|
211
|
+
SecurityErrorCode.INVALID_INPUT,
|
|
212
|
+
{ received: appId }
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Valid app ID pattern: lowercase letters, numbers, dots, underscores
|
|
217
|
+
// Examples: com.google.android.youtube, com.example.app
|
|
218
|
+
const appIdPattern = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/;
|
|
219
|
+
|
|
220
|
+
if (!appIdPattern.test(appId)) {
|
|
221
|
+
throw new SecurityError(
|
|
222
|
+
"Invalid app ID format",
|
|
223
|
+
SecurityErrorCode.INVALID_INPUT,
|
|
224
|
+
{ appId, expectedPattern: "com.example.app" }
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Validate a device ID format
|
|
233
|
+
*
|
|
234
|
+
* @param {string} deviceId - Device ID to validate
|
|
235
|
+
* @returns {boolean} True if valid
|
|
236
|
+
* @throws {SecurityError} If device ID is invalid
|
|
237
|
+
*/
|
|
238
|
+
export function validateDeviceId(deviceId) {
|
|
239
|
+
if (typeof deviceId !== "string" || deviceId.length === 0) {
|
|
240
|
+
throw new SecurityError(
|
|
241
|
+
"Device ID must be a non-empty string",
|
|
242
|
+
SecurityErrorCode.INVALID_INPUT,
|
|
243
|
+
{ received: deviceId }
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Valid device ID patterns:
|
|
248
|
+
// - emulator-5554 (emulator)
|
|
249
|
+
// - RF8M12345XY (physical device)
|
|
250
|
+
// - 192.168.1.100:5555 (network device)
|
|
251
|
+
const deviceIdPattern = /^[a-zA-Z0-9][a-zA-Z0-9_\-.:]*$/;
|
|
252
|
+
|
|
253
|
+
if (!deviceIdPattern.test(deviceId)) {
|
|
254
|
+
throw new SecurityError(
|
|
255
|
+
"Invalid device ID format",
|
|
256
|
+
SecurityErrorCode.INVALID_INPUT,
|
|
257
|
+
{ deviceId }
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check for maximum length
|
|
262
|
+
if (deviceId.length > 100) {
|
|
263
|
+
throw new SecurityError(
|
|
264
|
+
"Device ID too long",
|
|
265
|
+
SecurityErrorCode.INVALID_INPUT,
|
|
266
|
+
{ deviceId, maxLength: 100 }
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ============================================
|
|
274
|
+
// OPERATION PERMISSION CHECKS
|
|
275
|
+
// ============================================
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Operation types for permission checking
|
|
279
|
+
*/
|
|
280
|
+
export const OperationType = {
|
|
281
|
+
// Read-only operations (always allowed)
|
|
282
|
+
READ: "read",
|
|
283
|
+
LIST: "list",
|
|
284
|
+
VALIDATE: "validate",
|
|
285
|
+
|
|
286
|
+
// Test operations (allowed in all modes)
|
|
287
|
+
RUN_TEST: "run_test",
|
|
288
|
+
SCREENSHOT: "screenshot",
|
|
289
|
+
|
|
290
|
+
// Potentially destructive operations (blocked in Safe Mode)
|
|
291
|
+
INSTALL_APP: "install_app",
|
|
292
|
+
UNINSTALL_APP: "uninstall_app",
|
|
293
|
+
CLEAR_APP_DATA: "clear_app_data",
|
|
294
|
+
|
|
295
|
+
// Always blocked operations
|
|
296
|
+
SYSTEM_SETTINGS: "system_settings",
|
|
297
|
+
FILE_SYSTEM: "file_system",
|
|
298
|
+
DEVICE_ADMIN: "device_admin",
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Operations allowed in Safe Mode
|
|
303
|
+
*/
|
|
304
|
+
const SAFE_MODE_ALLOWED_OPERATIONS = new Set([
|
|
305
|
+
OperationType.READ,
|
|
306
|
+
OperationType.LIST,
|
|
307
|
+
OperationType.VALIDATE,
|
|
308
|
+
OperationType.RUN_TEST,
|
|
309
|
+
OperationType.SCREENSHOT,
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Operations allowed in Full Mode (Safe Mode + these)
|
|
314
|
+
*/
|
|
315
|
+
const FULL_MODE_ADDITIONAL_OPERATIONS = new Set([
|
|
316
|
+
OperationType.INSTALL_APP,
|
|
317
|
+
OperationType.UNINSTALL_APP,
|
|
318
|
+
OperationType.CLEAR_APP_DATA,
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Operations that are always blocked regardless of mode
|
|
323
|
+
*/
|
|
324
|
+
const ALWAYS_BLOCKED_OPERATIONS = new Set([
|
|
325
|
+
OperationType.SYSTEM_SETTINGS,
|
|
326
|
+
OperationType.FILE_SYSTEM,
|
|
327
|
+
OperationType.DEVICE_ADMIN,
|
|
328
|
+
]);
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Check if an operation is allowed based on current security mode
|
|
332
|
+
*
|
|
333
|
+
* @param {string} operation - Operation type from OperationType
|
|
334
|
+
* @returns {boolean} True if operation is allowed
|
|
335
|
+
*/
|
|
336
|
+
export function isOperationAllowed(operation) {
|
|
337
|
+
// Always blocked operations
|
|
338
|
+
if (ALWAYS_BLOCKED_OPERATIONS.has(operation)) {
|
|
339
|
+
logger.warn(`Blocked operation attempted: ${operation}`);
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check Safe Mode operations
|
|
344
|
+
if (SAFE_MODE_ALLOWED_OPERATIONS.has(operation)) {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// If not in Safe Mode, check additional operations
|
|
349
|
+
if (!isSafeModeEnabled() && FULL_MODE_ADDITIONAL_OPERATIONS.has(operation)) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Unknown operation - deny by default
|
|
354
|
+
logger.warn(`Unknown operation denied: ${operation}`);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Assert that an operation is allowed, throw if not
|
|
360
|
+
*
|
|
361
|
+
* @param {string} operation - Operation type from OperationType
|
|
362
|
+
* @param {string} description - Description for error message
|
|
363
|
+
* @throws {SecurityError} If operation is not allowed
|
|
364
|
+
*/
|
|
365
|
+
export function assertOperationAllowed(operation, description = "") {
|
|
366
|
+
if (!isOperationAllowed(operation)) {
|
|
367
|
+
const mode = getSecurityMode();
|
|
368
|
+
const isBlocked = ALWAYS_BLOCKED_OPERATIONS.has(operation);
|
|
369
|
+
|
|
370
|
+
throw new SecurityError(
|
|
371
|
+
isBlocked
|
|
372
|
+
? `Operation "${operation}" is not allowed: ${description}`
|
|
373
|
+
: `Operation "${operation}" is not allowed in Safe Mode: ${description}`,
|
|
374
|
+
isBlocked
|
|
375
|
+
? SecurityErrorCode.BLOCKED_COMMAND
|
|
376
|
+
: SecurityErrorCode.SAFE_MODE_VIOLATION,
|
|
377
|
+
{ operation, mode, description }
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ============================================
|
|
383
|
+
// COMMAND ALLOWLISTS
|
|
384
|
+
// ============================================
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Allowed Maestro CLI commands
|
|
388
|
+
* Only these commands can be executed via the MCP server
|
|
389
|
+
*/
|
|
390
|
+
export const MAESTRO_ALLOWED_COMMANDS = new Set([
|
|
391
|
+
"test", // Run test flow
|
|
392
|
+
"validate", // Validate YAML
|
|
393
|
+
"screenshot", // Capture screen
|
|
394
|
+
"--version", // Version check
|
|
395
|
+
"--help", // Help
|
|
396
|
+
"hierarchy", // View hierarchy (useful for debugging)
|
|
397
|
+
]);
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Allowed Maestro command flags
|
|
401
|
+
*/
|
|
402
|
+
export const MAESTRO_ALLOWED_FLAGS = new Set([
|
|
403
|
+
"--device", // Device selection
|
|
404
|
+
"--format", // Output format
|
|
405
|
+
"--output", // Output path
|
|
406
|
+
"--no-ansi", // Disable ANSI colors
|
|
407
|
+
"--debug", // Debug mode
|
|
408
|
+
"-e", // Environment variable
|
|
409
|
+
"--env", // Environment variable
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* ADB commands allowed in Safe Mode
|
|
414
|
+
* These are read-only and non-destructive operations
|
|
415
|
+
*/
|
|
416
|
+
export const ADB_SAFE_MODE_COMMANDS = new Set([
|
|
417
|
+
"devices", // List devices
|
|
418
|
+
"devices -l", // List devices with details
|
|
419
|
+
"get-state", // Get device state
|
|
420
|
+
"get-serialno", // Get serial number
|
|
421
|
+
"shell getprop", // Get device properties
|
|
422
|
+
"shell pm list packages", // List installed packages
|
|
423
|
+
"shell pm path", // Get APK path
|
|
424
|
+
"shell dumpsys package", // Package info
|
|
425
|
+
"shell input tap", // Tap gesture
|
|
426
|
+
"shell input text", // Text input
|
|
427
|
+
"shell input swipe", // Swipe gesture
|
|
428
|
+
"shell input keyevent", // Key event
|
|
429
|
+
"shell screencap", // Screenshot
|
|
430
|
+
"pull", // Pull file from device
|
|
431
|
+
"version", // ADB version
|
|
432
|
+
]);
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Additional ADB commands allowed in Full Mode
|
|
436
|
+
* These are potentially destructive operations
|
|
437
|
+
*/
|
|
438
|
+
export const ADB_FULL_MODE_COMMANDS = new Set([
|
|
439
|
+
"install", // Install APK
|
|
440
|
+
"install-multiple", // Install split APKs
|
|
441
|
+
"uninstall", // Uninstall app
|
|
442
|
+
"shell pm clear", // Clear app data
|
|
443
|
+
"shell am force-stop", // Force stop app
|
|
444
|
+
"shell am start", // Start activity
|
|
445
|
+
"shell am broadcast", // Send broadcast
|
|
446
|
+
"push", // Push file to device
|
|
447
|
+
"logcat", // View logs
|
|
448
|
+
"bugreport", // Bug report
|
|
449
|
+
]);
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* ADB commands that are ALWAYS blocked regardless of mode
|
|
453
|
+
* These are dangerous system-level operations
|
|
454
|
+
*/
|
|
455
|
+
export const ADB_BLOCKED_COMMANDS = new Set([
|
|
456
|
+
"root", // Root access
|
|
457
|
+
"unroot", // Unroot
|
|
458
|
+
"reboot", // Reboot device
|
|
459
|
+
"reboot-bootloader", // Reboot to bootloader
|
|
460
|
+
"reboot recovery", // Reboot to recovery
|
|
461
|
+
"shell rm", // Delete files
|
|
462
|
+
"shell rm -rf", // Recursive delete
|
|
463
|
+
"shell rm -r", // Recursive delete
|
|
464
|
+
"shell rmdir", // Remove directory
|
|
465
|
+
"shell mv", // Move files
|
|
466
|
+
"shell cp", // Copy files (can overwrite)
|
|
467
|
+
"shell dd", // Direct disk access
|
|
468
|
+
"shell su", // Superuser
|
|
469
|
+
"shell settings put", // Change settings
|
|
470
|
+
"shell settings delete", // Delete settings
|
|
471
|
+
"shell pm disable", // Disable package
|
|
472
|
+
"shell pm enable", // Enable package (can be dangerous)
|
|
473
|
+
"shell pm grant", // Grant permissions
|
|
474
|
+
"shell pm revoke", // Revoke permissions
|
|
475
|
+
"shell pm set-installer", // Set installer
|
|
476
|
+
"shell pm hide", // Hide package
|
|
477
|
+
"shell pm unhide", // Unhide package
|
|
478
|
+
"shell wm", // Window manager (screen changes)
|
|
479
|
+
"shell svc", // Service control
|
|
480
|
+
"shell setprop", // Set system properties
|
|
481
|
+
"shell am kill", // Kill process
|
|
482
|
+
"shell am kill-all", // Kill all processes
|
|
483
|
+
"shell am crash", // Crash app
|
|
484
|
+
"remount", // Remount filesystem
|
|
485
|
+
"disable-verity", // Disable verification
|
|
486
|
+
"enable-verity", // Enable verification
|
|
487
|
+
"keygen", // Key generation
|
|
488
|
+
"restore", // Restore backup
|
|
489
|
+
"sideload", // Sideload OTA
|
|
490
|
+
"shell content delete", // Delete content
|
|
491
|
+
"shell content update", // Update content
|
|
492
|
+
"emu kill", // Kill emulator
|
|
493
|
+
"emu avd stop", // Stop AVD
|
|
494
|
+
]);
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Check if a Maestro command is allowed
|
|
498
|
+
*
|
|
499
|
+
* @param {string} command - The Maestro command to check
|
|
500
|
+
* @returns {object} Result with allowed status and reason
|
|
501
|
+
*/
|
|
502
|
+
export function isMaestroCommandAllowed(command) {
|
|
503
|
+
if (typeof command !== "string" || command.length === 0) {
|
|
504
|
+
return {
|
|
505
|
+
allowed: false,
|
|
506
|
+
reason: "Command must be a non-empty string",
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Extract the base command (first argument)
|
|
511
|
+
const parts = command.trim().split(/\s+/);
|
|
512
|
+
const baseCommand = parts[0];
|
|
513
|
+
|
|
514
|
+
// Check if base command is allowed
|
|
515
|
+
if (!MAESTRO_ALLOWED_COMMANDS.has(baseCommand)) {
|
|
516
|
+
logSecurityEvent("MAESTRO_COMMAND_BLOCKED", {
|
|
517
|
+
command,
|
|
518
|
+
baseCommand,
|
|
519
|
+
reason: "Command not in allowlist",
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
allowed: false,
|
|
524
|
+
reason: `Maestro command "${baseCommand}" is not allowed`,
|
|
525
|
+
allowedCommands: Array.from(MAESTRO_ALLOWED_COMMANDS),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Validate flags if present
|
|
530
|
+
for (let i = 1; i < parts.length; i++) {
|
|
531
|
+
const part = parts[i];
|
|
532
|
+
// Check if it's a flag (starts with -)
|
|
533
|
+
if (part.startsWith("-")) {
|
|
534
|
+
// Extract flag name (handle --flag=value format)
|
|
535
|
+
const flagName = part.split("=")[0];
|
|
536
|
+
|
|
537
|
+
if (!MAESTRO_ALLOWED_FLAGS.has(flagName) && flagName !== "--device") {
|
|
538
|
+
// Allow unknown flags but log them
|
|
539
|
+
logger.info(`Unknown Maestro flag used: ${flagName}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
allowed: true,
|
|
546
|
+
command: baseCommand,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Check if an ADB command is allowed based on current security mode
|
|
552
|
+
*
|
|
553
|
+
* @param {string} command - The ADB command to check
|
|
554
|
+
* @returns {object} Result with allowed status and reason
|
|
555
|
+
*/
|
|
556
|
+
export function isAdbCommandAllowed(command) {
|
|
557
|
+
if (typeof command !== "string" || command.length === 0) {
|
|
558
|
+
return {
|
|
559
|
+
allowed: false,
|
|
560
|
+
reason: "Command must be a non-empty string",
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const trimmedCommand = command.trim();
|
|
565
|
+
const safeMode = isSafeModeEnabled();
|
|
566
|
+
|
|
567
|
+
// Check against blocked commands first (always blocked)
|
|
568
|
+
for (const blocked of ADB_BLOCKED_COMMANDS) {
|
|
569
|
+
if (
|
|
570
|
+
trimmedCommand === blocked ||
|
|
571
|
+
trimmedCommand.startsWith(blocked + " ")
|
|
572
|
+
) {
|
|
573
|
+
logSecurityEvent("ADB_COMMAND_BLOCKED", {
|
|
574
|
+
command: trimmedCommand,
|
|
575
|
+
blockedPattern: blocked,
|
|
576
|
+
reason: "Command is in blocked list",
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
allowed: false,
|
|
581
|
+
reason: `ADB command "${blocked}" is blocked for security reasons`,
|
|
582
|
+
blocked: true,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Check against Safe Mode allowlist
|
|
588
|
+
for (const allowed of ADB_SAFE_MODE_COMMANDS) {
|
|
589
|
+
if (
|
|
590
|
+
trimmedCommand === allowed ||
|
|
591
|
+
trimmedCommand.startsWith(allowed + " ")
|
|
592
|
+
) {
|
|
593
|
+
return {
|
|
594
|
+
allowed: true,
|
|
595
|
+
mode: "safe",
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// If not in Safe Mode, check Full Mode commands
|
|
601
|
+
if (!safeMode) {
|
|
602
|
+
for (const allowed of ADB_FULL_MODE_COMMANDS) {
|
|
603
|
+
if (
|
|
604
|
+
trimmedCommand === allowed ||
|
|
605
|
+
trimmedCommand.startsWith(allowed + " ")
|
|
606
|
+
) {
|
|
607
|
+
return {
|
|
608
|
+
allowed: true,
|
|
609
|
+
mode: "full",
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Check if it's a Full Mode command that's blocked by Safe Mode
|
|
616
|
+
for (const fullModeCmd of ADB_FULL_MODE_COMMANDS) {
|
|
617
|
+
if (
|
|
618
|
+
trimmedCommand === fullModeCmd ||
|
|
619
|
+
trimmedCommand.startsWith(fullModeCmd + " ")
|
|
620
|
+
) {
|
|
621
|
+
logSecurityEvent("ADB_COMMAND_SAFE_MODE_BLOCKED", {
|
|
622
|
+
command: trimmedCommand,
|
|
623
|
+
reason: "Command requires Full Mode",
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
return {
|
|
627
|
+
allowed: false,
|
|
628
|
+
reason: `ADB command "${fullModeCmd}" is not allowed in Safe Mode. Set SAFE_MODE=false to enable.`,
|
|
629
|
+
requiresFullMode: true,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Unknown command - deny by default
|
|
635
|
+
logSecurityEvent("ADB_COMMAND_UNKNOWN", {
|
|
636
|
+
command: trimmedCommand,
|
|
637
|
+
reason: "Command not recognized",
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
allowed: false,
|
|
642
|
+
reason: `ADB command not recognized or not allowed: "${trimmedCommand}"`,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Validate a complete command before execution
|
|
648
|
+
*
|
|
649
|
+
* @param {string} command - The command to validate
|
|
650
|
+
* @param {string} type - Command type: 'maestro' or 'adb'
|
|
651
|
+
* @returns {object} Validation result
|
|
652
|
+
* @throws {SecurityError} If command is not allowed
|
|
653
|
+
*/
|
|
654
|
+
export function validateCommand(command, type) {
|
|
655
|
+
let result;
|
|
656
|
+
|
|
657
|
+
if (type === "maestro") {
|
|
658
|
+
result = isMaestroCommandAllowed(command);
|
|
659
|
+
} else if (type === "adb") {
|
|
660
|
+
result = isAdbCommandAllowed(command);
|
|
661
|
+
} else {
|
|
662
|
+
throw new SecurityError(
|
|
663
|
+
`Unknown command type: ${type}`,
|
|
664
|
+
SecurityErrorCode.INVALID_INPUT,
|
|
665
|
+
{ type, allowedTypes: ["maestro", "adb"] }
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (!result.allowed) {
|
|
670
|
+
throw new SecurityError(
|
|
671
|
+
result.reason,
|
|
672
|
+
result.blocked
|
|
673
|
+
? SecurityErrorCode.BLOCKED_COMMAND
|
|
674
|
+
: SecurityErrorCode.SAFE_MODE_VIOLATION,
|
|
675
|
+
{ command, type, ...result }
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return result;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Get all allowed commands for display/documentation
|
|
684
|
+
*
|
|
685
|
+
* @returns {object} Object containing all allowlists
|
|
686
|
+
*/
|
|
687
|
+
export function getAllowedCommands() {
|
|
688
|
+
return {
|
|
689
|
+
maestro: {
|
|
690
|
+
commands: Array.from(MAESTRO_ALLOWED_COMMANDS),
|
|
691
|
+
flags: Array.from(MAESTRO_ALLOWED_FLAGS),
|
|
692
|
+
},
|
|
693
|
+
adb: {
|
|
694
|
+
safeMode: Array.from(ADB_SAFE_MODE_COMMANDS),
|
|
695
|
+
fullMode: Array.from(ADB_FULL_MODE_COMMANDS),
|
|
696
|
+
blocked: Array.from(ADB_BLOCKED_COMMANDS),
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ============================================
|
|
702
|
+
// BLOCKED PATTERNS & DETECTION
|
|
703
|
+
// ============================================
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Pattern types for categorizing blocked patterns
|
|
707
|
+
*/
|
|
708
|
+
export const PatternType = {
|
|
709
|
+
SHELL_INJECTION: "shell_injection",
|
|
710
|
+
COMMAND_SUBSTITUTION: "command_substitution",
|
|
711
|
+
PATH_TRAVERSAL: "path_traversal",
|
|
712
|
+
ENVIRONMENT_EXPANSION: "environment_expansion",
|
|
713
|
+
NULL_BYTE: "null_byte",
|
|
714
|
+
DANGEROUS_CHARACTERS: "dangerous_characters",
|
|
715
|
+
SQL_INJECTION: "sql_injection",
|
|
716
|
+
SCRIPT_INJECTION: "script_injection",
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Blocked patterns with descriptions
|
|
721
|
+
* Each pattern has a regex, type, and description for better error messages
|
|
722
|
+
*/
|
|
723
|
+
export const BLOCKED_PATTERNS = [
|
|
724
|
+
// Shell injection patterns
|
|
725
|
+
{
|
|
726
|
+
pattern: /[;&|]/,
|
|
727
|
+
type: PatternType.SHELL_INJECTION,
|
|
728
|
+
description: "Shell command chaining characters (; & |)",
|
|
729
|
+
example: "command1; command2",
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
pattern: /\|\|/,
|
|
733
|
+
type: PatternType.SHELL_INJECTION,
|
|
734
|
+
description: "Shell OR operator (||)",
|
|
735
|
+
example: "cmd1 || cmd2",
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
pattern: /&&/,
|
|
739
|
+
type: PatternType.SHELL_INJECTION,
|
|
740
|
+
description: "Shell AND operator (&&)",
|
|
741
|
+
example: "cmd1 && cmd2",
|
|
742
|
+
},
|
|
743
|
+
|
|
744
|
+
// Command substitution patterns
|
|
745
|
+
{
|
|
746
|
+
pattern: /`[^`]*`/,
|
|
747
|
+
type: PatternType.COMMAND_SUBSTITUTION,
|
|
748
|
+
description: "Backtick command substitution",
|
|
749
|
+
example: "`whoami`",
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
pattern: /\$\([^)]*\)/,
|
|
753
|
+
type: PatternType.COMMAND_SUBSTITUTION,
|
|
754
|
+
description: "Dollar-paren command substitution",
|
|
755
|
+
example: "$(whoami)",
|
|
756
|
+
},
|
|
757
|
+
|
|
758
|
+
// Path traversal patterns
|
|
759
|
+
{
|
|
760
|
+
pattern: /\.\.[/\\]/,
|
|
761
|
+
type: PatternType.PATH_TRAVERSAL,
|
|
762
|
+
description: "Parent directory traversal (../)",
|
|
763
|
+
example: "../../../etc/passwd",
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
pattern: /[/\\]\.\./,
|
|
767
|
+
type: PatternType.PATH_TRAVERSAL,
|
|
768
|
+
description: "Parent directory traversal (/..)",
|
|
769
|
+
example: "/dir/../secret",
|
|
770
|
+
},
|
|
771
|
+
|
|
772
|
+
// Environment variable expansion
|
|
773
|
+
{
|
|
774
|
+
pattern: /\$\{[^}]+\}/,
|
|
775
|
+
type: PatternType.ENVIRONMENT_EXPANSION,
|
|
776
|
+
description: "Environment variable expansion (${VAR})",
|
|
777
|
+
example: "${HOME}",
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
pattern: /\$[A-Z_][A-Z0-9_]*/i,
|
|
781
|
+
type: PatternType.ENVIRONMENT_EXPANSION,
|
|
782
|
+
description: "Environment variable reference ($VAR)",
|
|
783
|
+
example: "$PATH",
|
|
784
|
+
},
|
|
785
|
+
|
|
786
|
+
// Null byte injection - using Unicode escape for linter compatibility
|
|
787
|
+
{
|
|
788
|
+
pattern: new RegExp(String.fromCharCode(0)),
|
|
789
|
+
type: PatternType.NULL_BYTE,
|
|
790
|
+
description: "Null byte injection",
|
|
791
|
+
example: "file.txt\\0.jpg",
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
pattern: /%00/,
|
|
795
|
+
type: PatternType.NULL_BYTE,
|
|
796
|
+
description: "URL-encoded null byte",
|
|
797
|
+
example: "file.txt%00.jpg",
|
|
798
|
+
},
|
|
799
|
+
|
|
800
|
+
// Dangerous shell characters
|
|
801
|
+
{
|
|
802
|
+
pattern: /[><]/,
|
|
803
|
+
type: PatternType.DANGEROUS_CHARACTERS,
|
|
804
|
+
description: "Shell redirection characters (< >)",
|
|
805
|
+
example: "cmd > /etc/passwd",
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
pattern: /\n|\r/,
|
|
809
|
+
type: PatternType.DANGEROUS_CHARACTERS,
|
|
810
|
+
description: "Newline characters (potential command injection)",
|
|
811
|
+
example: "cmd\\nmalicious",
|
|
812
|
+
},
|
|
813
|
+
|
|
814
|
+
// Script injection (for YAML context)
|
|
815
|
+
{
|
|
816
|
+
pattern: /<script[^>]*>/i,
|
|
817
|
+
type: PatternType.SCRIPT_INJECTION,
|
|
818
|
+
description: "Script tag injection",
|
|
819
|
+
example: "<script>alert(1)</script>",
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
pattern: /javascript:/i,
|
|
823
|
+
type: PatternType.SCRIPT_INJECTION,
|
|
824
|
+
description: "JavaScript protocol",
|
|
825
|
+
example: "javascript:alert(1)",
|
|
826
|
+
},
|
|
827
|
+
];
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Patterns that are only blocked in certain contexts
|
|
831
|
+
*/
|
|
832
|
+
export const CONTEXT_SENSITIVE_PATTERNS = {
|
|
833
|
+
// Blocked only in shell commands
|
|
834
|
+
shellCommand: [
|
|
835
|
+
{
|
|
836
|
+
pattern: /\$/,
|
|
837
|
+
type: PatternType.ENVIRONMENT_EXPANSION,
|
|
838
|
+
description: "Dollar sign in shell context",
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
pattern: /!/,
|
|
842
|
+
type: PatternType.SHELL_INJECTION,
|
|
843
|
+
description: "History expansion character",
|
|
844
|
+
},
|
|
845
|
+
],
|
|
846
|
+
|
|
847
|
+
// Blocked only in file paths
|
|
848
|
+
filePath: [
|
|
849
|
+
{
|
|
850
|
+
pattern: /:/,
|
|
851
|
+
type: PatternType.DANGEROUS_CHARACTERS,
|
|
852
|
+
description: "Colon in file path (Windows drive or ADS)",
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
pattern: /\*/,
|
|
856
|
+
type: PatternType.DANGEROUS_CHARACTERS,
|
|
857
|
+
description: "Wildcard in file path",
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
pattern: /\?/,
|
|
861
|
+
type: PatternType.DANGEROUS_CHARACTERS,
|
|
862
|
+
description: "Wildcard in file path",
|
|
863
|
+
},
|
|
864
|
+
],
|
|
865
|
+
|
|
866
|
+
// Blocked only in app IDs
|
|
867
|
+
appId: [
|
|
868
|
+
{
|
|
869
|
+
pattern: /[^a-zA-Z0-9_.]/,
|
|
870
|
+
type: PatternType.DANGEROUS_CHARACTERS,
|
|
871
|
+
description: "Invalid character in app ID",
|
|
872
|
+
},
|
|
873
|
+
],
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Check if input contains any blocked patterns
|
|
878
|
+
*
|
|
879
|
+
* @param {string} input - Input string to check
|
|
880
|
+
* @param {string} context - Optional context for context-sensitive checks
|
|
881
|
+
* @returns {object} Result with detected status and pattern info
|
|
882
|
+
*/
|
|
883
|
+
export function containsBlockedPattern(input, context = null) {
|
|
884
|
+
if (typeof input !== "string") {
|
|
885
|
+
return {
|
|
886
|
+
detected: false,
|
|
887
|
+
reason: "Input is not a string",
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Check global blocked patterns
|
|
892
|
+
for (const patternInfo of BLOCKED_PATTERNS) {
|
|
893
|
+
if (patternInfo.pattern.test(input)) {
|
|
894
|
+
logSecurityEvent("BLOCKED_PATTERN_DETECTED", {
|
|
895
|
+
input: input.substring(0, 100), // Truncate for logging
|
|
896
|
+
patternType: patternInfo.type,
|
|
897
|
+
description: patternInfo.description,
|
|
898
|
+
context,
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
return {
|
|
902
|
+
detected: true,
|
|
903
|
+
type: patternInfo.type,
|
|
904
|
+
description: patternInfo.description,
|
|
905
|
+
example: patternInfo.example,
|
|
906
|
+
pattern: patternInfo.pattern.toString(),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Check context-sensitive patterns if context is provided
|
|
912
|
+
if (context && CONTEXT_SENSITIVE_PATTERNS[context]) {
|
|
913
|
+
for (const patternInfo of CONTEXT_SENSITIVE_PATTERNS[context]) {
|
|
914
|
+
if (patternInfo.pattern.test(input)) {
|
|
915
|
+
logSecurityEvent("CONTEXT_PATTERN_DETECTED", {
|
|
916
|
+
input: input.substring(0, 100),
|
|
917
|
+
patternType: patternInfo.type,
|
|
918
|
+
description: patternInfo.description,
|
|
919
|
+
context,
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
detected: true,
|
|
924
|
+
type: patternInfo.type,
|
|
925
|
+
description: patternInfo.description,
|
|
926
|
+
context,
|
|
927
|
+
pattern: patternInfo.pattern.toString(),
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return {
|
|
934
|
+
detected: false,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Assert that input does not contain blocked patterns
|
|
940
|
+
*
|
|
941
|
+
* @param {string} input - Input string to check
|
|
942
|
+
* @param {string} inputName - Name of the input for error messages
|
|
943
|
+
* @param {string} context - Optional context for context-sensitive checks
|
|
944
|
+
* @throws {SecurityError} If blocked pattern is detected
|
|
945
|
+
*/
|
|
946
|
+
export function assertNoBlockedPatterns(
|
|
947
|
+
input,
|
|
948
|
+
inputName = "input",
|
|
949
|
+
context = null
|
|
950
|
+
) {
|
|
951
|
+
const result = containsBlockedPattern(input, context);
|
|
952
|
+
|
|
953
|
+
if (result.detected) {
|
|
954
|
+
throw new SecurityError(
|
|
955
|
+
`Blocked pattern detected in ${inputName}: ${result.description}`,
|
|
956
|
+
SecurityErrorCode.BLOCKED_PATTERN,
|
|
957
|
+
{
|
|
958
|
+
inputName,
|
|
959
|
+
patternType: result.type,
|
|
960
|
+
description: result.description,
|
|
961
|
+
context,
|
|
962
|
+
}
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Sanitize input by removing or escaping blocked patterns
|
|
969
|
+
* Use with caution - validation is preferred over sanitization
|
|
970
|
+
*
|
|
971
|
+
* @param {string} input - Input string to sanitize
|
|
972
|
+
* @param {object} options - Sanitization options
|
|
973
|
+
* @returns {string} Sanitized string
|
|
974
|
+
*/
|
|
975
|
+
export function sanitizeBlockedPatterns(input, options = {}) {
|
|
976
|
+
if (typeof input !== "string") {
|
|
977
|
+
return input;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
let sanitized = input;
|
|
981
|
+
|
|
982
|
+
// Remove null bytes - using character code for linter compatibility
|
|
983
|
+
const nullByteRegex = new RegExp(String.fromCharCode(0), "g");
|
|
984
|
+
sanitized = sanitized.replace(nullByteRegex, "");
|
|
985
|
+
sanitized = sanitized.replace(/%00/g, "");
|
|
986
|
+
|
|
987
|
+
// Remove shell injection characters
|
|
988
|
+
if (options.removeShellChars !== false) {
|
|
989
|
+
sanitized = sanitized.replace(/[;&|`]/g, "");
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Remove path traversal
|
|
993
|
+
if (options.removePathTraversal !== false) {
|
|
994
|
+
sanitized = sanitized.replace(/\.\./g, "");
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Remove environment expansion (but keep $ for normal text)
|
|
998
|
+
if (options.removeEnvExpansion) {
|
|
999
|
+
sanitized = sanitized.replace(/\$\{[^}]+\}/g, "");
|
|
1000
|
+
sanitized = sanitized.replace(/\$\([^)]*\)/g, "");
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Remove control characters
|
|
1004
|
+
if (options.removeControlChars !== false) {
|
|
1005
|
+
sanitized = sanitized.replace(/[\n\r]/g, " ");
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return sanitized;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Validate and sanitize a complete command before execution
|
|
1013
|
+
* This combines pattern detection with command allowlist checking
|
|
1014
|
+
*
|
|
1015
|
+
* @param {string} command - Command to validate
|
|
1016
|
+
* @param {string} type - Command type ('maestro' or 'adb')
|
|
1017
|
+
* @returns {object} Validation result with sanitized command
|
|
1018
|
+
* @throws {SecurityError} If validation fails
|
|
1019
|
+
*/
|
|
1020
|
+
export function validateAndSanitizeCommand(command, type) {
|
|
1021
|
+
// First check for blocked patterns
|
|
1022
|
+
assertNoBlockedPatterns(command, `${type} command`, "shellCommand");
|
|
1023
|
+
|
|
1024
|
+
// Then validate against allowlists
|
|
1025
|
+
const allowlistResult = validateCommand(command, type);
|
|
1026
|
+
|
|
1027
|
+
return {
|
|
1028
|
+
valid: true,
|
|
1029
|
+
command: sanitizeInput(command),
|
|
1030
|
+
type,
|
|
1031
|
+
...allowlistResult,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Check if YAML content contains potentially dangerous patterns
|
|
1037
|
+
*
|
|
1038
|
+
* @param {string} yamlContent - YAML content to check
|
|
1039
|
+
* @returns {object} Result with detected issues
|
|
1040
|
+
*/
|
|
1041
|
+
export function checkYamlSecurity(yamlContent) {
|
|
1042
|
+
if (typeof yamlContent !== "string") {
|
|
1043
|
+
return {
|
|
1044
|
+
safe: false,
|
|
1045
|
+
issues: ["YAML content must be a string"],
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const issues = [];
|
|
1050
|
+
const warnings = [];
|
|
1051
|
+
|
|
1052
|
+
// Check for blocked patterns (excluding some that are valid in YAML)
|
|
1053
|
+
const nullBytePattern = new RegExp(String.fromCharCode(0));
|
|
1054
|
+
const dangerousPatterns = [
|
|
1055
|
+
{ pattern: /\$\([^)]*\)/, message: "Command substitution $()" },
|
|
1056
|
+
{ pattern: /`[^`]*`/, message: "Backtick command substitution" },
|
|
1057
|
+
{ pattern: /<script/i, message: "Script tag" },
|
|
1058
|
+
{ pattern: /javascript:/i, message: "JavaScript protocol" },
|
|
1059
|
+
{ pattern: nullBytePattern, message: "Null byte" },
|
|
1060
|
+
];
|
|
1061
|
+
|
|
1062
|
+
for (const { pattern, message } of dangerousPatterns) {
|
|
1063
|
+
if (pattern.test(yamlContent)) {
|
|
1064
|
+
issues.push(`Dangerous pattern detected: ${message}`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Check for suspicious YAML constructs
|
|
1069
|
+
if (yamlContent.includes("!!python")) {
|
|
1070
|
+
issues.push("Python tag detected - potential code execution");
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (yamlContent.includes("!!ruby")) {
|
|
1074
|
+
issues.push("Ruby tag detected - potential code execution");
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Warnings (not blocking but suspicious)
|
|
1078
|
+
if (yamlContent.includes("shell:") || yamlContent.includes("exec:")) {
|
|
1079
|
+
warnings.push("Shell/exec command found - review carefully");
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (issues.length > 0) {
|
|
1083
|
+
logSecurityEvent("YAML_SECURITY_ISSUES", {
|
|
1084
|
+
issueCount: issues.length,
|
|
1085
|
+
issues,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return {
|
|
1090
|
+
safe: issues.length === 0,
|
|
1091
|
+
issues,
|
|
1092
|
+
warnings,
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Get all blocked patterns for documentation
|
|
1098
|
+
*
|
|
1099
|
+
* @returns {object} All blocked patterns organized by type
|
|
1100
|
+
*/
|
|
1101
|
+
export function getBlockedPatterns() {
|
|
1102
|
+
const organized = {};
|
|
1103
|
+
|
|
1104
|
+
for (const patternInfo of BLOCKED_PATTERNS) {
|
|
1105
|
+
if (!organized[patternInfo.type]) {
|
|
1106
|
+
organized[patternInfo.type] = [];
|
|
1107
|
+
}
|
|
1108
|
+
organized[patternInfo.type].push({
|
|
1109
|
+
description: patternInfo.description,
|
|
1110
|
+
example: patternInfo.example,
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return {
|
|
1115
|
+
patterns: organized,
|
|
1116
|
+
contextSensitive: Object.keys(CONTEXT_SENSITIVE_PATTERNS),
|
|
1117
|
+
patternTypes: Object.values(PatternType),
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// ============================================
|
|
1122
|
+
// SECURITY EVENT LOGGING
|
|
1123
|
+
// ============================================
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Log a security event
|
|
1127
|
+
*
|
|
1128
|
+
* @param {string} event - Event type
|
|
1129
|
+
* @param {object} details - Event details
|
|
1130
|
+
*/
|
|
1131
|
+
export function logSecurityEvent(event, details = {}) {
|
|
1132
|
+
if (process.env.LOG_SECURITY_EVENTS === "false") {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
logger.info(`[SECURITY] ${event}`, {
|
|
1137
|
+
event,
|
|
1138
|
+
...details,
|
|
1139
|
+
timestamp: new Date().toISOString(),
|
|
1140
|
+
safeMode: isSafeModeEnabled(),
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// ============================================
|
|
1145
|
+
// EXPORTS
|
|
1146
|
+
// ============================================
|
|
1147
|
+
|
|
1148
|
+
export default {
|
|
1149
|
+
// Constants
|
|
1150
|
+
SecurityMode,
|
|
1151
|
+
SecurityErrorCode,
|
|
1152
|
+
OperationType,
|
|
1153
|
+
PatternType,
|
|
1154
|
+
|
|
1155
|
+
// Command Allowlists
|
|
1156
|
+
MAESTRO_ALLOWED_COMMANDS,
|
|
1157
|
+
MAESTRO_ALLOWED_FLAGS,
|
|
1158
|
+
ADB_SAFE_MODE_COMMANDS,
|
|
1159
|
+
ADB_FULL_MODE_COMMANDS,
|
|
1160
|
+
ADB_BLOCKED_COMMANDS,
|
|
1161
|
+
|
|
1162
|
+
// Blocked Patterns
|
|
1163
|
+
BLOCKED_PATTERNS,
|
|
1164
|
+
CONTEXT_SENSITIVE_PATTERNS,
|
|
1165
|
+
|
|
1166
|
+
// Classes
|
|
1167
|
+
SecurityError,
|
|
1168
|
+
|
|
1169
|
+
// Safe Mode functions
|
|
1170
|
+
isSafeModeEnabled,
|
|
1171
|
+
getSecurityMode,
|
|
1172
|
+
getSecurityConfig,
|
|
1173
|
+
|
|
1174
|
+
// Input sanitization
|
|
1175
|
+
sanitizeInput,
|
|
1176
|
+
sanitizeFilePath,
|
|
1177
|
+
validateAppId,
|
|
1178
|
+
validateDeviceId,
|
|
1179
|
+
|
|
1180
|
+
// Operation permissions
|
|
1181
|
+
isOperationAllowed,
|
|
1182
|
+
assertOperationAllowed,
|
|
1183
|
+
|
|
1184
|
+
// Command validation
|
|
1185
|
+
isMaestroCommandAllowed,
|
|
1186
|
+
isAdbCommandAllowed,
|
|
1187
|
+
validateCommand,
|
|
1188
|
+
getAllowedCommands,
|
|
1189
|
+
|
|
1190
|
+
// Pattern detection
|
|
1191
|
+
containsBlockedPattern,
|
|
1192
|
+
assertNoBlockedPatterns,
|
|
1193
|
+
sanitizeBlockedPatterns,
|
|
1194
|
+
validateAndSanitizeCommand,
|
|
1195
|
+
checkYamlSecurity,
|
|
1196
|
+
getBlockedPatterns,
|
|
1197
|
+
|
|
1198
|
+
// Logging
|
|
1199
|
+
logSecurityEvent,
|
|
1200
|
+
};
|