mcp-maestro-mobile-ai 1.3.0 → 1.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/CHANGELOG.md +244 -143
- package/ROADMAP.md +21 -8
- package/package.json +6 -3
- package/src/mcp-server/index.js +1059 -816
- package/src/mcp-server/schemas/toolSchemas.js +636 -0
- package/src/mcp-server/utils/maestro.js +265 -29
- package/src/mcp-server/utils/security.js +1200 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Maestro Utility Functions
|
|
3
3
|
* Execute Maestro CLI commands and handle results
|
|
4
|
+
*
|
|
5
|
+
* Security: All command executions are validated against security policies
|
|
6
|
+
* defined in security.js before execution.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import { spawn, execSync } from "child_process";
|
|
@@ -10,6 +13,20 @@ import os from "os";
|
|
|
10
13
|
import { fileURLToPath } from "url";
|
|
11
14
|
import { dirname, join } from "path";
|
|
12
15
|
import { logger } from "./logger.js";
|
|
16
|
+
import {
|
|
17
|
+
SecurityError,
|
|
18
|
+
isSafeModeEnabled,
|
|
19
|
+
getSecurityConfig,
|
|
20
|
+
validateDeviceId,
|
|
21
|
+
validateAppId,
|
|
22
|
+
isMaestroCommandAllowed,
|
|
23
|
+
isAdbCommandAllowed,
|
|
24
|
+
containsBlockedPattern,
|
|
25
|
+
assertNoBlockedPatterns,
|
|
26
|
+
checkYamlSecurity,
|
|
27
|
+
logSecurityEvent,
|
|
28
|
+
sanitizeInput,
|
|
29
|
+
} from "./security.js";
|
|
13
30
|
|
|
14
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
32
|
const __dirname = dirname(__filename);
|
|
@@ -47,45 +64,135 @@ function getAdbPath() {
|
|
|
47
64
|
|
|
48
65
|
/**
|
|
49
66
|
* Execute Maestro CLI command with optional device targeting
|
|
67
|
+
* Includes security validation before execution
|
|
68
|
+
*
|
|
69
|
+
* @param {Array<string>} args - Command arguments
|
|
70
|
+
* @param {string} deviceId - Optional device ID
|
|
71
|
+
* @returns {Promise<object>} Execution result
|
|
72
|
+
* @throws {SecurityError} If command is not allowed
|
|
50
73
|
*/
|
|
51
74
|
export function executeMaestro(args, deviceId = null) {
|
|
52
|
-
return new Promise((resolve) => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
try {
|
|
77
|
+
// Security: Validate the Maestro command
|
|
78
|
+
const baseCommand = args[0];
|
|
79
|
+
const commandCheck = isMaestroCommandAllowed(baseCommand);
|
|
80
|
+
|
|
81
|
+
if (!commandCheck.allowed) {
|
|
82
|
+
logSecurityEvent("MAESTRO_COMMAND_REJECTED", {
|
|
83
|
+
command: baseCommand,
|
|
84
|
+
args,
|
|
85
|
+
reason: commandCheck.reason,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
reject(
|
|
89
|
+
new SecurityError(commandCheck.reason, "BLOCKED_COMMAND", {
|
|
90
|
+
command: baseCommand,
|
|
91
|
+
args,
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
61
96
|
|
|
62
|
-
|
|
63
|
-
|
|
97
|
+
// Security: Validate device ID if provided
|
|
98
|
+
const targetDevice = deviceId || selectedDeviceId;
|
|
99
|
+
if (targetDevice) {
|
|
100
|
+
try {
|
|
101
|
+
validateDeviceId(targetDevice);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
reject(error);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
64
107
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
108
|
+
// Security: Check for blocked patterns in arguments
|
|
109
|
+
for (const arg of args) {
|
|
110
|
+
if (typeof arg === "string") {
|
|
111
|
+
const patternCheck = containsBlockedPattern(arg, "shellCommand");
|
|
112
|
+
if (patternCheck.detected) {
|
|
113
|
+
logSecurityEvent("MAESTRO_ARG_BLOCKED_PATTERN", {
|
|
114
|
+
arg,
|
|
115
|
+
patternType: patternCheck.type,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
reject(
|
|
119
|
+
new SecurityError(
|
|
120
|
+
`Blocked pattern detected in argument: ${patternCheck.description}`,
|
|
121
|
+
"BLOCKED_PATTERN",
|
|
122
|
+
{ arg, patternType: patternCheck.type }
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
68
129
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
130
|
+
// Log the execution for audit trail
|
|
131
|
+
logSecurityEvent("MAESTRO_COMMAND_EXECUTE", {
|
|
132
|
+
command: baseCommand,
|
|
133
|
+
argsCount: args.length,
|
|
134
|
+
device: targetDevice || "default",
|
|
135
|
+
safeMode: isSafeModeEnabled(),
|
|
136
|
+
});
|
|
72
137
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
138
|
+
const finalArgs = targetDevice
|
|
139
|
+
? ["--device", targetDevice, ...args]
|
|
140
|
+
: args;
|
|
76
141
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
142
|
+
const maestro = spawn("maestro", finalArgs, {
|
|
143
|
+
shell: true,
|
|
144
|
+
env: { ...process.env },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
let stdout = "";
|
|
148
|
+
let stderr = "";
|
|
149
|
+
|
|
150
|
+
maestro.stdout.on("data", (data) => {
|
|
151
|
+
stdout += data.toString();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
maestro.stderr.on("data", (data) => {
|
|
155
|
+
stderr += data.toString();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
maestro.on("close", (exitCode) => {
|
|
159
|
+
resolve({ exitCode, stdout, stderr });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
maestro.on("error", (error) => {
|
|
163
|
+
resolve({ exitCode: 1, stdout, stderr: error.message });
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
reject(error);
|
|
167
|
+
}
|
|
80
168
|
});
|
|
81
169
|
}
|
|
82
170
|
|
|
83
171
|
/**
|
|
84
172
|
* List all connected devices with detailed information
|
|
173
|
+
* Includes security validation for ADB command
|
|
85
174
|
*/
|
|
86
175
|
export async function listDevices() {
|
|
87
176
|
try {
|
|
88
177
|
const adbPath = getAdbPath();
|
|
178
|
+
|
|
179
|
+
// Security: Validate ADB command
|
|
180
|
+
const adbCommand = "devices -l";
|
|
181
|
+
const adbCheck = isAdbCommandAllowed(adbCommand);
|
|
182
|
+
if (!adbCheck.allowed) {
|
|
183
|
+
logger.error(`ADB command blocked: ${adbCommand}`);
|
|
184
|
+
return {
|
|
185
|
+
success: false,
|
|
186
|
+
error: "Security policy prevented device listing",
|
|
187
|
+
devices: [],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
logSecurityEvent("ADB_COMMAND_EXECUTE", {
|
|
192
|
+
command: adbCommand,
|
|
193
|
+
operation: "list_devices",
|
|
194
|
+
});
|
|
195
|
+
|
|
89
196
|
let adbOutput;
|
|
90
197
|
try {
|
|
91
198
|
adbOutput = execSync(`"${adbPath}" devices -l`, {
|
|
@@ -155,13 +262,34 @@ export async function listDevices() {
|
|
|
155
262
|
|
|
156
263
|
/**
|
|
157
264
|
* Select a device for running tests
|
|
265
|
+
* Includes security validation of device ID
|
|
266
|
+
*
|
|
267
|
+
* @param {string} deviceId - Device ID to select
|
|
268
|
+
* @returns {object} Result with selected device
|
|
269
|
+
* @throws {SecurityError} If device ID is invalid
|
|
158
270
|
*/
|
|
159
271
|
export function selectDevice(deviceId) {
|
|
160
|
-
|
|
161
|
-
|
|
272
|
+
// Security: Validate device ID format
|
|
273
|
+
try {
|
|
274
|
+
validateDeviceId(deviceId);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
logger.warn(`Invalid device ID rejected: ${deviceId}`);
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Sanitize the device ID
|
|
281
|
+
const sanitizedDeviceId = sanitizeInput(deviceId);
|
|
282
|
+
|
|
283
|
+
selectedDeviceId = sanitizedDeviceId;
|
|
284
|
+
logger.info(`Device selected: ${sanitizedDeviceId}`);
|
|
285
|
+
|
|
286
|
+
logSecurityEvent("DEVICE_SELECTED", {
|
|
287
|
+
deviceId: sanitizedDeviceId,
|
|
288
|
+
});
|
|
289
|
+
|
|
162
290
|
return {
|
|
163
291
|
success: true,
|
|
164
|
-
selectedDevice:
|
|
292
|
+
selectedDevice: sanitizedDeviceId,
|
|
165
293
|
};
|
|
166
294
|
}
|
|
167
295
|
|
|
@@ -253,6 +381,11 @@ export async function checkDeviceConnection() {
|
|
|
253
381
|
|
|
254
382
|
/**
|
|
255
383
|
* Check if app is installed on device (optionally on specific device)
|
|
384
|
+
* Includes security validation of app ID and device ID
|
|
385
|
+
*
|
|
386
|
+
* @param {string} appId - App package ID to check
|
|
387
|
+
* @param {string} deviceId - Optional device ID
|
|
388
|
+
* @returns {object} Installation status
|
|
256
389
|
*/
|
|
257
390
|
export async function checkAppInstalled(appId, deviceId = null) {
|
|
258
391
|
if (!appId) {
|
|
@@ -263,12 +396,57 @@ export async function checkAppInstalled(appId, deviceId = null) {
|
|
|
263
396
|
};
|
|
264
397
|
}
|
|
265
398
|
|
|
399
|
+
// Security: Validate app ID format
|
|
400
|
+
try {
|
|
401
|
+
validateAppId(appId);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
logger.warn(`Invalid app ID rejected: ${appId}`);
|
|
404
|
+
return {
|
|
405
|
+
installed: false,
|
|
406
|
+
error: error.message,
|
|
407
|
+
hint: "App ID must be in format: com.example.app",
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Security: Validate device ID if provided
|
|
266
412
|
const targetDevice = deviceId || selectedDeviceId;
|
|
413
|
+
if (targetDevice) {
|
|
414
|
+
try {
|
|
415
|
+
validateDeviceId(targetDevice);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
return {
|
|
418
|
+
installed: false,
|
|
419
|
+
error: error.message,
|
|
420
|
+
hint: "Invalid device ID format",
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
267
425
|
const adbPath = getAdbPath();
|
|
426
|
+
|
|
427
|
+
// Security: Build command safely without string interpolation vulnerabilities
|
|
428
|
+
const adbArgs = targetDevice ? ["-s", targetDevice] : [];
|
|
268
429
|
const adbPrefix = targetDevice
|
|
269
430
|
? `"${adbPath}" -s ${targetDevice}`
|
|
270
431
|
: `"${adbPath}"`;
|
|
271
432
|
|
|
433
|
+
// Security: Validate the ADB command we're about to execute
|
|
434
|
+
const adbCommand = "shell pm list packages";
|
|
435
|
+
const adbCheck = isAdbCommandAllowed(adbCommand);
|
|
436
|
+
if (!adbCheck.allowed) {
|
|
437
|
+
logger.error(`ADB command blocked: ${adbCommand}`);
|
|
438
|
+
return {
|
|
439
|
+
installed: false,
|
|
440
|
+
error: "Security policy prevented app check",
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
logSecurityEvent("ADB_COMMAND_EXECUTE", {
|
|
445
|
+
command: adbCommand,
|
|
446
|
+
appId,
|
|
447
|
+
device: targetDevice || "default",
|
|
448
|
+
});
|
|
449
|
+
|
|
272
450
|
try {
|
|
273
451
|
const result = execSync(`${adbPrefix} shell pm list packages ${appId}`, {
|
|
274
452
|
encoding: "utf8",
|
|
@@ -327,18 +505,73 @@ export async function checkAppInstalled(appId, deviceId = null) {
|
|
|
327
505
|
|
|
328
506
|
/**
|
|
329
507
|
* Run a Maestro flow from YAML content with retry support
|
|
508
|
+
* Includes security validation of YAML content
|
|
509
|
+
*
|
|
510
|
+
* @param {string} yamlContent - YAML flow content
|
|
511
|
+
* @param {string} testName - Name of the test
|
|
512
|
+
* @param {object} options - Execution options
|
|
513
|
+
* @returns {Promise<object>} Test result
|
|
330
514
|
*/
|
|
331
515
|
export async function runMaestroFlow(yamlContent, testName, options = {}) {
|
|
332
516
|
const maxRetries = options.retries ?? DEFAULT_RETRIES;
|
|
333
517
|
const deviceId = options.deviceId || selectedDeviceId;
|
|
334
518
|
const startTime = Date.now();
|
|
335
519
|
|
|
520
|
+
// Security: Validate YAML content for dangerous patterns
|
|
521
|
+
const yamlSecurityCheck = checkYamlSecurity(yamlContent);
|
|
522
|
+
if (!yamlSecurityCheck.safe) {
|
|
523
|
+
logger.error(`YAML security check failed for test: ${testName}`, {
|
|
524
|
+
issues: yamlSecurityCheck.issues,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
logSecurityEvent("YAML_SECURITY_REJECTED", {
|
|
528
|
+
testName,
|
|
529
|
+
issues: yamlSecurityCheck.issues,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
success: false,
|
|
534
|
+
name: testName,
|
|
535
|
+
error: `YAML security check failed: ${yamlSecurityCheck.issues.join(
|
|
536
|
+
", "
|
|
537
|
+
)}`,
|
|
538
|
+
securityIssues: yamlSecurityCheck.issues,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Log warnings if any
|
|
543
|
+
if (yamlSecurityCheck.warnings && yamlSecurityCheck.warnings.length > 0) {
|
|
544
|
+
logger.warn(`YAML security warnings for test: ${testName}`, {
|
|
545
|
+
warnings: yamlSecurityCheck.warnings,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Security: Validate test name (used in file path)
|
|
550
|
+
try {
|
|
551
|
+
assertNoBlockedPatterns(testName, "test name", "filePath");
|
|
552
|
+
} catch (error) {
|
|
553
|
+
return {
|
|
554
|
+
success: false,
|
|
555
|
+
name: testName,
|
|
556
|
+
error: `Invalid test name: ${error.message}`,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Sanitize test name for file system
|
|
561
|
+
const safeTestName = testName.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
562
|
+
|
|
563
|
+
logSecurityEvent("TEST_EXECUTION_START", {
|
|
564
|
+
testName: safeTestName,
|
|
565
|
+
device: deviceId || "default",
|
|
566
|
+
safeMode: isSafeModeEnabled(),
|
|
567
|
+
});
|
|
568
|
+
|
|
336
569
|
// Ensure directories exist
|
|
337
570
|
await fs.mkdir(TEMP_DIR, { recursive: true });
|
|
338
571
|
await fs.mkdir(SCREENSHOTS_DIR, { recursive: true });
|
|
339
572
|
|
|
340
|
-
// Write YAML to temp file
|
|
341
|
-
const tempFile = join(TEMP_DIR, `${
|
|
573
|
+
// Write YAML to temp file with sanitized name
|
|
574
|
+
const tempFile = join(TEMP_DIR, `${safeTestName}-${Date.now()}.yaml`);
|
|
342
575
|
await fs.writeFile(tempFile, yamlContent, "utf8");
|
|
343
576
|
|
|
344
577
|
let lastResult = null;
|
|
@@ -483,14 +716,17 @@ export async function captureScreenshot(name, deviceId = null) {
|
|
|
483
716
|
}
|
|
484
717
|
|
|
485
718
|
/**
|
|
486
|
-
* Get configuration values
|
|
719
|
+
* Get configuration values including security settings
|
|
487
720
|
*/
|
|
488
721
|
export function getConfig() {
|
|
722
|
+
const securityConfig = getSecurityConfig();
|
|
723
|
+
|
|
489
724
|
return {
|
|
490
725
|
defaultTimeout: DEFAULT_TIMEOUT,
|
|
491
726
|
defaultRetries: DEFAULT_RETRIES,
|
|
492
727
|
maxResults: MAX_RESULTS,
|
|
493
728
|
selectedDevice: selectedDeviceId,
|
|
729
|
+
security: securityConfig,
|
|
494
730
|
};
|
|
495
731
|
}
|
|
496
732
|
|