mcp-maestro-mobile-ai 1.3.1 → 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.
@@ -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
- // Use specified device, selected device, or let Maestro choose
54
- const targetDevice = deviceId || selectedDeviceId;
55
- const finalArgs = targetDevice ? ["--device", targetDevice, ...args] : args;
56
-
57
- const maestro = spawn("maestro", finalArgs, {
58
- shell: true,
59
- env: { ...process.env },
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
- let stdout = "";
63
- let stderr = "";
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
- maestro.stdout.on("data", (data) => {
66
- stdout += data.toString();
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
- maestro.stderr.on("data", (data) => {
70
- stderr += data.toString();
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
- maestro.on("close", (exitCode) => {
74
- resolve({ exitCode, stdout, stderr });
75
- });
138
+ const finalArgs = targetDevice
139
+ ? ["--device", targetDevice, ...args]
140
+ : args;
76
141
 
77
- maestro.on("error", (error) => {
78
- resolve({ exitCode: 1, stdout, stderr: error.message });
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
- selectedDeviceId = deviceId;
161
- logger.info(`Device selected: ${deviceId}`);
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: deviceId,
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, `${testName}-${Date.now()}.yaml`);
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