apple-tools-mcp 1.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.
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/contacts.js +433 -0
- package/index.js +1502 -0
- package/indexer.js +1849 -0
- package/lib/audit.js +1083 -0
- package/lib/shell.js +321 -0
- package/lib/validators.js +432 -0
- package/package.json +116 -0
- package/scripts/audit-index.js +94 -0
- package/search.js +1652 -0
package/lib/shell.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe shell execution utilities for apple-tools-mcp
|
|
3
|
+
*
|
|
4
|
+
* This module provides secure wrappers around child_process functions
|
|
5
|
+
* to prevent command injection vulnerabilities by:
|
|
6
|
+
* - Using spawnSync instead of execSync (avoids shell interpolation)
|
|
7
|
+
* - Passing arguments as arrays, not concatenated strings
|
|
8
|
+
* - Validating inputs before execution
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawnSync } from "child_process";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { escapeSQL } from "./validators.js";
|
|
14
|
+
|
|
15
|
+
// ============ SQLITE3 EXECUTION ============
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Safely execute a sqlite3 query using spawnSync
|
|
19
|
+
* Prevents command injection by passing args as array
|
|
20
|
+
*
|
|
21
|
+
* @param {string} dbPath - Path to the SQLite database
|
|
22
|
+
* @param {string} query - SQL query to execute
|
|
23
|
+
* @param {object} options - Additional options
|
|
24
|
+
* @param {boolean} options.json - Return JSON output (default: true)
|
|
25
|
+
* @param {number} options.timeout - Timeout in ms (default: 30000)
|
|
26
|
+
* @param {number} options.maxBuffer - Max buffer size (default: 50MB)
|
|
27
|
+
* @returns {string} Raw output from sqlite3
|
|
28
|
+
* @throws {Error} If execution fails
|
|
29
|
+
*/
|
|
30
|
+
export function safeSqlite3(dbPath, query, options = {}) {
|
|
31
|
+
const {
|
|
32
|
+
json = true,
|
|
33
|
+
timeout = 30000,
|
|
34
|
+
maxBuffer = 50 * 1024 * 1024
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
// Validate database path exists and is absolute
|
|
38
|
+
if (!dbPath || typeof dbPath !== 'string') {
|
|
39
|
+
throw new Error('Database path is required');
|
|
40
|
+
}
|
|
41
|
+
if (!path.isAbsolute(dbPath)) {
|
|
42
|
+
throw new Error('Database path must be absolute');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Validate query
|
|
46
|
+
if (!query || typeof query !== 'string') {
|
|
47
|
+
throw new Error('SQL query is required');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Build args array - no shell interpolation possible
|
|
51
|
+
const args = [];
|
|
52
|
+
if (json) {
|
|
53
|
+
args.push('-json');
|
|
54
|
+
}
|
|
55
|
+
args.push(dbPath);
|
|
56
|
+
args.push(query.replace(/\n/g, ' ')); // Normalize whitespace
|
|
57
|
+
|
|
58
|
+
const result = spawnSync('sqlite3', args, {
|
|
59
|
+
encoding: 'utf-8',
|
|
60
|
+
timeout,
|
|
61
|
+
maxBuffer,
|
|
62
|
+
// Don't use shell - prevents injection
|
|
63
|
+
shell: false
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (result.error) {
|
|
67
|
+
throw result.error;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (result.status !== 0) {
|
|
71
|
+
const errorMsg = result.stderr || `sqlite3 exited with code ${result.status}`;
|
|
72
|
+
throw new Error(errorMsg);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return result.stdout;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Execute sqlite3 and parse JSON result
|
|
80
|
+
*
|
|
81
|
+
* @param {string} dbPath - Path to the SQLite database
|
|
82
|
+
* @param {string} query - SQL query to execute
|
|
83
|
+
* @param {object} options - Additional options
|
|
84
|
+
* @returns {Array} Parsed JSON array of results
|
|
85
|
+
*/
|
|
86
|
+
export function safeSqlite3Json(dbPath, query, options = {}) {
|
|
87
|
+
const output = safeSqlite3(dbPath, query, { ...options, json: true });
|
|
88
|
+
|
|
89
|
+
if (!output || output.trim() === '') {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(output);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.error('Failed to parse sqlite3 JSON output:', e.message);
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============ OSASCRIPT EXECUTION ============
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Safely execute AppleScript using spawnSync
|
|
105
|
+
* Uses stdin to pass script content, preventing shell injection
|
|
106
|
+
*
|
|
107
|
+
* @param {string} script - AppleScript to execute
|
|
108
|
+
* @param {object} options - Additional options
|
|
109
|
+
* @param {number} options.timeout - Timeout in ms (default: 30000)
|
|
110
|
+
* @returns {string} Output from AppleScript
|
|
111
|
+
* @throws {Error} If execution fails
|
|
112
|
+
*/
|
|
113
|
+
export function safeOsascript(script, options = {}) {
|
|
114
|
+
const {
|
|
115
|
+
timeout = 30000
|
|
116
|
+
} = options;
|
|
117
|
+
|
|
118
|
+
if (!script || typeof script !== 'string') {
|
|
119
|
+
throw new Error('AppleScript is required');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Use -e flag with the script content passed as argument
|
|
123
|
+
// This is safer than heredoc shell syntax
|
|
124
|
+
const result = spawnSync('osascript', ['-e', script], {
|
|
125
|
+
encoding: 'utf-8',
|
|
126
|
+
timeout,
|
|
127
|
+
shell: false
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (result.error) {
|
|
131
|
+
throw result.error;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// osascript may return non-zero for certain operations
|
|
135
|
+
// Return stdout if we have it, otherwise throw
|
|
136
|
+
if (result.status !== 0 && !result.stdout) {
|
|
137
|
+
const errorMsg = result.stderr || `osascript exited with code ${result.status}`;
|
|
138
|
+
throw new Error(errorMsg);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result.stdout;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ============ MDFIND EXECUTION ============
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Safely execute mdfind (Spotlight search) using spawnSync
|
|
148
|
+
*
|
|
149
|
+
* @param {string} query - Spotlight query
|
|
150
|
+
* @param {object} options - Additional options
|
|
151
|
+
* @param {string} options.onlyin - Directory to search in
|
|
152
|
+
* @param {number} options.timeout - Timeout in ms (default: 60000)
|
|
153
|
+
* @returns {string[]} Array of file paths
|
|
154
|
+
*/
|
|
155
|
+
export function safeMdfind(query, options = {}) {
|
|
156
|
+
const {
|
|
157
|
+
onlyin,
|
|
158
|
+
timeout = 60000
|
|
159
|
+
} = options;
|
|
160
|
+
|
|
161
|
+
if (!query || typeof query !== 'string') {
|
|
162
|
+
throw new Error('Search query is required');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const args = [];
|
|
166
|
+
|
|
167
|
+
if (onlyin) {
|
|
168
|
+
if (!path.isAbsolute(onlyin)) {
|
|
169
|
+
throw new Error('onlyin path must be absolute');
|
|
170
|
+
}
|
|
171
|
+
args.push('-onlyin', onlyin);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
args.push(query);
|
|
175
|
+
|
|
176
|
+
const result = spawnSync('mdfind', args, {
|
|
177
|
+
encoding: 'utf-8',
|
|
178
|
+
timeout,
|
|
179
|
+
maxBuffer: 100 * 1024 * 1024, // 100MB for large result sets
|
|
180
|
+
shell: false
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (result.error) {
|
|
184
|
+
throw result.error;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (result.status !== 0) {
|
|
188
|
+
const errorMsg = result.stderr || `mdfind exited with code ${result.status}`;
|
|
189
|
+
throw new Error(errorMsg);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Split output into lines, filter empty
|
|
193
|
+
return result.stdout
|
|
194
|
+
.split('\n')
|
|
195
|
+
.map(line => line.trim())
|
|
196
|
+
.filter(line => line.length > 0);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============ FIND EXECUTION ============
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Safely execute find command using spawnSync
|
|
203
|
+
*
|
|
204
|
+
* @param {string} searchPath - Directory to search
|
|
205
|
+
* @param {object} options - Find options
|
|
206
|
+
* @param {string} options.name - Filename pattern (-name)
|
|
207
|
+
* @param {string} options.type - File type (-type f, d, etc.)
|
|
208
|
+
* @param {string} options.mtime - Modification time (-mtime)
|
|
209
|
+
* @param {number} options.maxdepth - Max directory depth
|
|
210
|
+
* @param {number} options.timeout - Timeout in ms (default: 120000)
|
|
211
|
+
* @returns {string[]} Array of file paths
|
|
212
|
+
*/
|
|
213
|
+
export function safeFind(searchPath, options = {}) {
|
|
214
|
+
const {
|
|
215
|
+
name,
|
|
216
|
+
type,
|
|
217
|
+
mtime,
|
|
218
|
+
maxdepth,
|
|
219
|
+
timeout = 120000
|
|
220
|
+
} = options;
|
|
221
|
+
|
|
222
|
+
if (!searchPath || typeof searchPath !== 'string') {
|
|
223
|
+
throw new Error('Search path is required');
|
|
224
|
+
}
|
|
225
|
+
if (!path.isAbsolute(searchPath)) {
|
|
226
|
+
throw new Error('Search path must be absolute');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const args = [searchPath];
|
|
230
|
+
|
|
231
|
+
if (maxdepth !== undefined) {
|
|
232
|
+
const depth = parseInt(maxdepth);
|
|
233
|
+
if (!Number.isInteger(depth) || depth < 0) {
|
|
234
|
+
throw new Error('maxdepth must be a non-negative integer');
|
|
235
|
+
}
|
|
236
|
+
args.push('-maxdepth', String(depth));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (type) {
|
|
240
|
+
// Validate type is a single character
|
|
241
|
+
if (!/^[fdlbcps]$/.test(type)) {
|
|
242
|
+
throw new Error('Invalid find type');
|
|
243
|
+
}
|
|
244
|
+
args.push('-type', type);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (name) {
|
|
248
|
+
args.push('-name', name);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (mtime) {
|
|
252
|
+
// Validate mtime format (e.g., -1, +7, 0)
|
|
253
|
+
if (!/^[+-]?\d+$/.test(mtime)) {
|
|
254
|
+
throw new Error('Invalid mtime format');
|
|
255
|
+
}
|
|
256
|
+
args.push('-mtime', mtime);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const result = spawnSync('find', args, {
|
|
260
|
+
encoding: 'utf-8',
|
|
261
|
+
timeout,
|
|
262
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
263
|
+
shell: false
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
if (result.error) {
|
|
267
|
+
throw result.error;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// find may return non-zero if some paths are inaccessible
|
|
271
|
+
// We still want to return whatever paths it found
|
|
272
|
+
return result.stdout
|
|
273
|
+
.split('\n')
|
|
274
|
+
.map(line => line.trim())
|
|
275
|
+
.filter(line => line.length > 0);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ============ GENERIC SAFE SPAWN ============
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Generic safe spawn wrapper for other commands
|
|
282
|
+
*
|
|
283
|
+
* @param {string} command - Command to execute
|
|
284
|
+
* @param {string[]} args - Arguments as array
|
|
285
|
+
* @param {object} options - spawnSync options
|
|
286
|
+
* @returns {object} { stdout, stderr, status }
|
|
287
|
+
*/
|
|
288
|
+
export function safeSpawn(command, args = [], options = {}) {
|
|
289
|
+
const {
|
|
290
|
+
timeout = 30000,
|
|
291
|
+
maxBuffer = 10 * 1024 * 1024,
|
|
292
|
+
encoding = 'utf-8',
|
|
293
|
+
...restOptions
|
|
294
|
+
} = options;
|
|
295
|
+
|
|
296
|
+
if (!command || typeof command !== 'string') {
|
|
297
|
+
throw new Error('Command is required');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!Array.isArray(args)) {
|
|
301
|
+
throw new Error('Args must be an array');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const result = spawnSync(command, args, {
|
|
305
|
+
encoding,
|
|
306
|
+
timeout,
|
|
307
|
+
maxBuffer,
|
|
308
|
+
shell: false,
|
|
309
|
+
...restOptions
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (result.error) {
|
|
313
|
+
throw result.error;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
stdout: result.stdout || '',
|
|
318
|
+
stderr: result.stderr || '',
|
|
319
|
+
status: result.status
|
|
320
|
+
};
|
|
321
|
+
}
|