@jishankai/solid-cli 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.
@@ -0,0 +1,705 @@
1
+ import OpenAI from 'openai';
2
+ import Anthropic from '@anthropic-ai/sdk';
3
+ import { writeFileSync, mkdirSync, existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { ReportSanitizer } from '../report/utils/sanitizer.js';
6
+
7
+ /**
8
+ * LLM Analyzer - Uses AI to analyze security findings and provide recommendations
9
+ */
10
+ export class LLMAnalyzer {
11
+ constructor(provider = 'openai', apiKey = null, options = {}) {
12
+ this.provider = provider.toLowerCase();
13
+ this.apiKey = apiKey || this.getApiKeyFromEnv();
14
+ this.enableLogging = options.enableLogging !== false; // Default: true
15
+ this.enabled = options.enabled !== false; // Default: true
16
+ this.logDir = options.logDir || './logs/llm-requests';
17
+ this.mode = options.mode || 'summary'; // 'summary' | 'full'
18
+ this.maxTokens = options.maxTokens || 4000;
19
+ this.minFindingsToAnalyze = options.minFindingsToAnalyze || 1;
20
+ this.reportSanitizer = new ReportSanitizer({
21
+ redactUserPaths: true,
22
+ redactIPs: true,
23
+ redactUsernames: true,
24
+ preserveDomains: false
25
+ });
26
+ this.findingDelimiter = '::';
27
+
28
+ if (this.provider === 'openai') {
29
+ this.client = new OpenAI({ apiKey: this.apiKey });
30
+ this.model = 'gpt-4.1';
31
+ } else if (this.provider === 'claude') {
32
+ this.client = new Anthropic({ apiKey: this.apiKey });
33
+ this.model = 'claude-3-5-sonnet-20241022';
34
+ }
35
+
36
+ // Ensure log directory exists
37
+ if (this.enableLogging) {
38
+ this.ensureLogDirectory();
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Ensure log directory exists
44
+ */
45
+ ensureLogDirectory() {
46
+ if (!existsSync(this.logDir)) {
47
+ mkdirSync(this.logDir, { recursive: true });
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Get API key from environment variables
53
+ */
54
+ getApiKeyFromEnv() {
55
+ if (this.provider === 'openai') {
56
+ return process.env.OPENAI_API_KEY;
57
+ } else if (this.provider === 'claude') {
58
+ return process.env.ANTHROPIC_API_KEY;
59
+ }
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Analyze results using LLM (PRIVACY PROTECTED)
65
+ */
66
+ async analyze(results, options = {}) {
67
+ if (!this.enabled) {
68
+ return { provider: this.provider, model: this.model, analysis: 'LLM analysis disabled', usage: {} };
69
+ }
70
+
71
+ const totalFindings = results?.summary?.totalFindings || 0;
72
+ if (totalFindings < this.minFindingsToAnalyze) {
73
+ return { provider: this.provider, model: this.model, analysis: 'LLM skipped (no significant findings)', usage: {} };
74
+ }
75
+
76
+ if (!this.apiKey) {
77
+ throw new Error(`API key not found for ${this.provider}. Set ${this.provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY'} environment variable.`);
78
+ }
79
+
80
+ const prompt = options.promptOverride || this.buildPrompt(results, options);
81
+
82
+ // CRITICAL SECURITY CHECK: Scan for sensitive data before sending to LLM
83
+ const securityCheck = this.performSecurityCheck(prompt);
84
+ if (securityCheck.hasSensitiveData) {
85
+ console.error('\n🚨 SECURITY ALERT: Sensitive data detected in LLM prompt!');
86
+ console.error('🚨 Analysis aborted to prevent private key leakage.');
87
+ console.error('🚨 Sensitive patterns found:', securityCheck.sensitivePatterns);
88
+
89
+ throw new Error('SECURITY: Sensitive data detected - LLM analysis aborted to prevent privacy leakage');
90
+ }
91
+
92
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
93
+
94
+ // Log the request BEFORE sending to LLM (with security note)
95
+ if (this.enableLogging) {
96
+ this.logRequest(prompt, timestamp, results, securityCheck);
97
+ }
98
+
99
+ try {
100
+ let response;
101
+
102
+ if (this.provider === 'openai') {
103
+ response = await this.analyzeWithOpenAI(prompt);
104
+ } else if (this.provider === 'claude') {
105
+ response = await this.analyzeWithClaude(prompt);
106
+ }
107
+
108
+ // Log the response
109
+ if (this.enableLogging) {
110
+ this.logResponse(response, timestamp);
111
+ }
112
+
113
+ return response;
114
+ } catch (error) {
115
+ // Log the error
116
+ if (this.enableLogging) {
117
+ this.logError(error, timestamp);
118
+ }
119
+
120
+ throw new Error(`LLM analysis failed: ${error.message}`);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Perform security check on prompt to detect sensitive data
126
+ */
127
+ performSecurityCheck(prompt) {
128
+ const sensitivePatterns = [
129
+ // Ethereum addresses (0x + 40 hex chars)
130
+ {
131
+ pattern: /0x[a-fA-F0-9]{40}/g,
132
+ name: 'Ethereum address',
133
+ test: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb8'
134
+ },
135
+ // Private keys (64+ hex chars)
136
+ {
137
+ pattern: /[a-fA-F0-9]{64,}/g,
138
+ name: 'Potential private key',
139
+ test: 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'
140
+ },
141
+ // Private key phrases
142
+ {
143
+ pattern: /(private|mnemonic|seed).*?[=:][a-zA-Z0-9+/]{8,}/gi,
144
+ name: 'Private key phrase',
145
+ test: 'private=0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb8'
146
+ },
147
+ // API keys/tokens (32+ chars)
148
+ {
149
+ pattern: /[a-zA-Z0-9+/]{32,}={0,2}/g,
150
+ name: 'API key or token',
151
+ test: 'sk-1234567890abcdef1234567890abcdef12345678'
152
+ },
153
+ // Wallet import formats (50+ chars)
154
+ {
155
+ pattern: /[6][a-km-zA-HJ-NP-Z1-9]{50,}/g,
156
+ name: 'Wallet import format',
157
+ test: '6PRApM1x8E1p2p5rTPeBqnm9ewwGcM5DHN'
158
+ },
159
+ // Seed phrases (12+ words with spaces)
160
+ {
161
+ pattern: /\b([a-z]+(\s+[a-z]+){11,})\b/gi,
162
+ name: 'Potential seed phrase',
163
+ test: 'word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12'
164
+ },
165
+ // Bitcoin addresses (starts with 1 or 3)
166
+ {
167
+ pattern: /\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b/g,
168
+ name: 'Bitcoin address',
169
+ test: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'
170
+ },
171
+ // Long hex sequences
172
+ {
173
+ pattern: /\b[a-fA-F0-9]{16,}\b/g,
174
+ name: 'Long hex string',
175
+ test: 'abcdef1234567890abcdef'
176
+ }
177
+ ];
178
+
179
+ const detectedPatterns = [];
180
+ let hasSensitiveData = false;
181
+
182
+ for (const { pattern, name, test } of sensitivePatterns) {
183
+ // Test pattern with known examples first
184
+ const testMatches = test.match(pattern);
185
+ const matchIterator = prompt.matchAll(pattern);
186
+ const filteredMatches = [];
187
+
188
+ for (const match of matchIterator) {
189
+ const value = match[0];
190
+ if (name === 'Potential seed phrase' && !this.isLikelySeedPhrase(value)) {
191
+ continue;
192
+ }
193
+ if (name === 'API key or token' && !this.isLikelyApiKey(value)) {
194
+ continue;
195
+ }
196
+ filteredMatches.push(value);
197
+ }
198
+
199
+ if (filteredMatches.length > 0) {
200
+ hasSensitiveData = true;
201
+ detectedPatterns.push({
202
+ name,
203
+ count: filteredMatches.length,
204
+ samples: filteredMatches.slice(0, 2).map(m => {
205
+ // Truncate sensitive data for logging
206
+ const truncated = m.length > 20 ? m.substring(0, 20) + '***' : m;
207
+ return truncated.replace(/[a-fA-F0-9]/g, '*'); // Additional masking
208
+ })
209
+ });
210
+ }
211
+ }
212
+
213
+ return {
214
+ hasSensitiveData,
215
+ sensitivePatterns: detectedPatterns,
216
+ promptLength: prompt.length
217
+ };
218
+ }
219
+
220
+ isLikelySeedPhrase(candidate) {
221
+ if (!candidate) return false;
222
+ const words = candidate.trim().toLowerCase().split(/\s+/);
223
+ if (words.length < 12 || words.length > 24) return false;
224
+ if (words.some(word => !/^[a-z]+$/.test(word))) return false;
225
+
226
+ const stopwords = new Set([
227
+ 'the', 'and', 'that', 'with', 'from', 'this', 'have', 'will', 'your',
228
+ 'macos', 'analysis', 'system', 'security', 'process', 'service', 'launch',
229
+ 'agent', 'apple', 'icloud', 'profile'
230
+ ]);
231
+ const stopwordHits = words.filter(word => stopwords.has(word)).length;
232
+ const uniqueWords = new Set(words).size;
233
+
234
+ return stopwordHits <= 2 && uniqueWords >= words.length - 2;
235
+ }
236
+
237
+ isLikelyApiKey(candidate) {
238
+ if (!candidate) return false;
239
+ if (candidate.length < 24 || candidate.length > 120) return false;
240
+ if (candidate.includes('<key>') || candidate.includes('</key>')) return false;
241
+
242
+ const hasUpper = /[A-Z]/.test(candidate);
243
+ const hasLower = /[a-z]/.test(candidate);
244
+ const hasDigit = /\d/.test(candidate);
245
+ const hasSymbol = /[+/=_-]/.test(candidate);
246
+ const uniqueRatio = new Set(candidate).size / candidate.length;
247
+
248
+ // Require mixed character classes and avoid low-entropy strings
249
+ if (!(hasDigit && (hasUpper || hasLower))) return false;
250
+ if (uniqueRatio < 0.2) return false;
251
+
252
+ // Ignore obvious plist or XML-like tokens
253
+ if (/^string$/i.test(candidate) || /^data$/i.test(candidate)) return false;
254
+
255
+ // Skip long runs of a single character
256
+ if (/^(.)\1{10,}$/.test(candidate)) return false;
257
+
258
+ return true;
259
+ }
260
+
261
+ extractPerFinding(text) {
262
+ if (!text) return {};
263
+
264
+ // Try fenced JSON block first
265
+ const fenceMatch = text.match(/```json\s*([\s\S]*?)```/);
266
+ const candidate = fenceMatch ? fenceMatch[1] : text;
267
+
268
+ try {
269
+ const parsed = JSON.parse(candidate);
270
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
271
+ return parsed;
272
+ }
273
+ } catch (error) {
274
+ // Ignore parse errors
275
+ }
276
+
277
+ return {};
278
+ }
279
+
280
+ /**
281
+ * Log the request to LLM provider (PRIVACY PROTECTED)
282
+ */
283
+ logRequest(prompt, timestamp, results, securityCheck = null) {
284
+ const logData = {
285
+ timestamp: new Date().toISOString(),
286
+ provider: this.provider,
287
+ model: this.model,
288
+ securityStatus: securityCheck ? {
289
+ hasSensitiveData: securityCheck.hasSensitiveData,
290
+ sensitivePatterns: securityCheck.sensitivePatterns,
291
+ promptLength: securityCheck.promptLength
292
+ } : null,
293
+ privacyNotice: '🔒 PRIVACY PROTECTED: Sensitive data automatically redacted',
294
+ systemInfo: {
295
+ hostname: results.hostname,
296
+ osVersion: results.osVersion,
297
+ mode: results.mode,
298
+ totalFindings: results.summary.totalFindings,
299
+ highRisk: results.summary.highRiskFindings,
300
+ mediumRisk: results.summary.mediumRiskFindings,
301
+ lowRisk: results.summary.lowRiskFindings
302
+ },
303
+ prompt: {
304
+ length: prompt.length,
305
+ preview: prompt.substring(0, 500) + '...',
306
+ fullContent: prompt,
307
+ sanitizationApplied: true
308
+ }
309
+ };
310
+
311
+ const filename = `request-${timestamp}.json`;
312
+ const filepath = join(this.logDir, filename);
313
+
314
+ try {
315
+ writeFileSync(filepath, JSON.stringify(logData, null, 2), 'utf-8');
316
+ console.log(`\n📝 LLM request logged to: ${filepath}`);
317
+ console.log(`🔒 Privacy protection applied - sensitive data redacted\n`);
318
+ } catch (error) {
319
+ console.warn(`Warning: Failed to write request log: ${error.message}`);
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Log the response from LLM provider
325
+ */
326
+ logResponse(response, timestamp) {
327
+ const logData = {
328
+ timestamp: new Date().toISOString(),
329
+ provider: response.provider,
330
+ model: response.model,
331
+ analysis: response.analysis,
332
+ usage: response.usage
333
+ };
334
+
335
+ const filename = `response-${timestamp}.json`;
336
+ const filepath = join(this.logDir, filename);
337
+
338
+ try {
339
+ writeFileSync(filepath, JSON.stringify(logData, null, 2), 'utf-8');
340
+ console.log(`📝 LLM response logged to: ${filepath}\n`);
341
+ } catch (error) {
342
+ console.warn(`Warning: Failed to write response log: ${error.message}`);
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Log errors
348
+ */
349
+ logError(error, timestamp) {
350
+ const logData = {
351
+ timestamp: new Date().toISOString(),
352
+ provider: this.provider,
353
+ error: error.message,
354
+ stack: error.stack
355
+ };
356
+
357
+ const filename = `error-${timestamp}.json`;
358
+ const filepath = join(this.logDir, filename);
359
+
360
+ try {
361
+ writeFileSync(filepath, JSON.stringify(logData, null, 2), 'utf-8');
362
+ console.log(`📝 Error logged to: ${filepath}\n`);
363
+ } catch (writeError) {
364
+ console.warn(`Warning: Failed to write error log: ${writeError.message}`);
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Build the analysis prompt
370
+ */
371
+ buildPrompt(results, options) {
372
+ const systemInfo = `
373
+ System Information:
374
+ - Hostname: ${results.hostname}
375
+ - macOS Version: ${results.osVersion}
376
+ - Analysis Mode: ${results.mode}
377
+ - Analysis Time: ${results.timestamp}
378
+ - Overall Risk Level: ${results.overallRisk}
379
+
380
+ Summary:
381
+ - Total Findings: ${results.summary.totalFindings}
382
+ - High Risk: ${results.summary.highRiskFindings}
383
+ - Medium Risk: ${results.summary.mediumRiskFindings}
384
+ - Low Risk: ${results.summary.lowRiskFindings}
385
+ `;
386
+
387
+ const mode = options.mode || this.mode;
388
+ const findingLimit = mode === 'summary' ? 10 : 20;
389
+
390
+ const resourceSnapshot = this.formatResourceSnapshot(results.agents?.resource);
391
+ const findingsData = this.formatFindings(results.agents, findingLimit);
392
+
393
+ const objective = options.objective || 'integrated';
394
+ let taskDescription = '';
395
+
396
+ if (objective === 'security') {
397
+ taskDescription = `Act as a macOS security responder. From the findings:
398
+ 1) Identify likely malware/persistence/backdoor chains and any privilege escalation or data exfiltration paths.
399
+ 2) Prioritize Critical/High issues with rationale and map to specific artifacts (plist, PID, path, socket).
400
+ 3) Provide a validation playbook: exact commands to confirm or triage each issue (ps/lsof/launchctl/spctl/codesign/kill/quarantine).
401
+ 4) Give containment + remediation steps per Critical/High: what to disable/kill/remove/quarantine, config/profile/tcc changes if needed, vendor update/uninstall, and a rollback note.
402
+ 5) Flag legitimate-but-sensitive tools to avoid false positives.
403
+ 6) For EACH finding (including medium/low) using the provided FindingID, produce purpose + risk + action in JSON for later attachment to that finding.`;
404
+ } else if (objective === 'performance') {
405
+ taskDescription = `Act as a macOS performance specialist. From the findings:
406
+ 1) Identify processes consuming excessive resources and whether usage is justified.
407
+ 2) Recommend actions to reduce load (disable services, limit background tasks, clean temp caches).
408
+ 3) Provide commands or steps to validate improvements.`;
409
+ } else {
410
+ taskDescription = `Act as a macOS security responder with performance awareness. From the findings:
411
+ 1) Identify likely malware/persistence/backdoor chains, privilege escalation, and data exfil paths.
412
+ 2) Prioritize Critical/High issues with rationale mapped to artifacts (plist, PID, path, socket) and highlight any resource-heavy offenders.
413
+ 3) Provide a validation playbook: commands to confirm/triage (ps/lsof/launchctl/spctl/codesign/kill/quarantine/systemextensionsctl/tccutil).
414
+ 4) Give containment/remediation steps per Critical/High: what to disable/kill/remove/quarantine, config/profile/tcc changes if needed, vendor update/uninstall, and a rollback note.
415
+ 5) Flag legitimate-but-sensitive tools to reduce false positives.
416
+ 6) Offer performance optimizations (CPU/memory) and commands to verify improvements.`;
417
+ }
418
+
419
+ return `${taskDescription}
420
+
421
+ ${systemInfo}
422
+
423
+ ${resourceSnapshot ? `Resource Snapshot:\n${resourceSnapshot}\n` : ''}
424
+
425
+ Detailed Findings:
426
+ ${findingsData}
427
+
428
+ Please provide structured markdown with sections:
429
+ 1) Executive Summary (2-3 sentences)
430
+ 2) Critical Issues (prioritized, map to PIDs/paths/plists/sockets)
431
+ 3) Issue-wise Remediation Plan (Critical/High only): action, exact command(s) or steps, expected outcome, rollback note
432
+ 4) Validation Playbook (evidence to collect before remediation, commands to confirm)
433
+ 5) Containment & Clean-up (kill/disable/remove/quarantine; note required privileges)
434
+ 6) Performance notes (if resource-heavy items exist)
435
+ 7) Triage Checklist (concise, bullet list)
436
+
437
+ Then append a JSON block fenced with \`\`\`json named PER_FINDING mapping each FindingID to {"purpose": "what the program/config is typically used for (vendor/legit/malicious)", "risk": "concise risk assessment + why (persistence/listen socket/signing/behavior)", "action": "clear decision and commands (keep/monitor/disable/remove) with rollback note"}.
438
+ Example:
439
+ \`\`\`json
440
+ {
441
+ "ResourceAgent#0": {"purpose": "menu bar monitor from vendor X", "risk": "low – signed, no persistence", "action": "keep; monitor CPU with 'top -o cpu'"},
442
+ "PersistenceAgent#2": {"purpose": "auto-launch helper", "risk": "high – unsigned launchdaemon listening on 0.0.0.0:1234", "action": "disable: sudo launchctl bootout system /Library/LaunchDaemons/com.foo.plist; remove plist; rollback: launchctl bootstrap"}
443
+ }
444
+ \`\`\`
445
+ If you cannot map a finding, omit it. No extra prose after the JSON block.
446
+
447
+ Keep output concise; avoid repeating redacted data.
448
+
449
+ IMPORTANT: Include all risks (high/medium/low) in PER_FINDING.`;
450
+ }
451
+
452
+ /**
453
+ * Format findings for the prompt (PRIVACY PROTECTED)
454
+ */
455
+ formatFindings(agents, limit = 20) {
456
+ let output = '';
457
+
458
+ for (const [agentKey, agentResult] of Object.entries(agents)) {
459
+ if (agentResult.error) {
460
+ output += `\n## ${agentKey} Agent: ERROR\n${agentResult.error}\n`;
461
+ continue;
462
+ }
463
+
464
+ output += `\n## ${agentResult.agent}\n`;
465
+ output += `Risk Level: ${agentResult.overallRisk}\n`;
466
+
467
+ if (agentResult.findings && agentResult.findings.length > 0) {
468
+ output += `Findings: ${agentResult.findings.length}\n\n`;
469
+
470
+ agentResult.findings.slice(0, limit).forEach((finding, index) => {
471
+ const findingId = `${agentKey}#${index}`;
472
+ output += `### ${finding.type} [${finding.risk}]\n`;
473
+ output += `- FindingID: ${findingId}\n`;
474
+
475
+ // Deep sanitize using report sanitizer first, then apply LLM-specific redactions
476
+ const safeDescription = this.reportSanitizer.sanitizeText(finding.description);
477
+ const sanitizedDescription = this.sanitizeForLLM(safeDescription);
478
+ output += `${sanitizedDescription}\n`;
479
+
480
+ // Add relevant details with sanitization
481
+ if (finding.pid) output += `- PID: ${finding.pid}\n`;
482
+ if (finding.command) {
483
+ const safeCommand = this.reportSanitizer.sanitizeText(finding.command);
484
+ const sanitizedCommand = this.sanitizeForLLM(safeCommand);
485
+ output += `- Command: ${sanitizedCommand}\n`;
486
+ }
487
+ if (finding.path) {
488
+ const safePath = this.reportSanitizer.sanitizePath(finding.path);
489
+ const sanitizedPath = this.sanitizePathForLLM(safePath);
490
+ output += `- Path: ${sanitizedPath}\n`;
491
+ }
492
+ if (finding.program) {
493
+ const safeProgram = this.reportSanitizer.sanitizeText(finding.program);
494
+ const sanitizedProgram = this.sanitizeForLLM(safeProgram);
495
+ output += `- Program: ${sanitizedProgram}\n`;
496
+ }
497
+ if (finding.plist) {
498
+ const safePlist = this.reportSanitizer.sanitizePath(finding.plist);
499
+ const sanitizedPlist = this.sanitizePathForLLM(safePlist);
500
+ output += `- Plist: ${sanitizedPlist}\n`;
501
+ }
502
+
503
+ if (finding.risks && finding.risks.length > 0) {
504
+ output += `- Risks: ${finding.risks.join(', ')}\n`;
505
+ }
506
+
507
+ // Add privacy protection note for sensitive findings
508
+ if (this.isSensitiveFinding(finding)) {
509
+ output += `- Privacy Note: Sensitive data redacted for security\n`;
510
+ }
511
+
512
+ output += '\n';
513
+ });
514
+
515
+ if (agentResult.findings.length > limit) {
516
+ output += `...truncated ${agentResult.findings.length - limit} additional findings for brevity...\n\n`;
517
+ }
518
+ } else {
519
+ output += 'No significant findings.\n\n';
520
+ }
521
+ }
522
+
523
+ return output;
524
+ }
525
+
526
+ /**
527
+ * Sanitize text for LLM to prevent private key exposure
528
+ */
529
+ sanitizeForLLM(text) {
530
+ if (!text) return text;
531
+
532
+ return text
533
+ // Redact Ethereum addresses
534
+ .replace(/0x[a-fA-F0-9]{40}/g, '0x***REDACTED***')
535
+ // Redact potential private keys (64 hex chars)
536
+ .replace(/[a-fA-F0-9]{64}/g, '***REDACTED***')
537
+ // Redact private key phrases and values
538
+ .replace(/(private|mnemonic|seed).*?[=:][a-zA-Z0-9+/]{8,}/gi, '$1***REDACTED***')
539
+ // Redact API keys and tokens
540
+ .replace(/[a-zA-Z0-9]{32,}={0,2}/g, '***REDACTED***')
541
+ // Redact long hex strings
542
+ .replace(/[a-fA-F0-9]{16,}/g, '***REDACTED***')
543
+ // Redact potential wallet import formats
544
+ .replace(/(6|5)[a-km-zA-HJ-NP-Z1-9]{50,}/g, '***WALLET-REDACTED***')
545
+ // Redact potential seed phrases (12+ words)
546
+ .replace(/([a-z]+\s){11,}[a-z]+/gi, '***SEED-PHRASE-REDACTED***')
547
+ // Redact IPv4 addresses
548
+ .replace(/\b(\d{1,3}\.){3}\d{1,3}\b/g, '***IP-REDACTED***')
549
+ // Redact common domains/URLs
550
+ .replace(/https?:\/\/[^\s]+/gi, '***URL-REDACTED***');
551
+ }
552
+
553
+ /**
554
+ * Sanitize file paths for LLM
555
+ */
556
+ sanitizePathForLLM(path) {
557
+ if (!path) return path;
558
+
559
+ return path
560
+ // Redact user directory
561
+ .replace(/\/Users\/[^\/]+/g, '/Users/***REDACTED***')
562
+ // Redact username from paths
563
+ .replace(/\/home\/[^\/]+/g, '/home/***REDACTED***')
564
+ // Redact potential wallet file paths
565
+ .replace(/(wallet|keystore|private|seed|mnemonic)[^\/]*\/[^\/]*/gi, '***WALLET-PATH-REDACTED***')
566
+ // Redact hostname fragments
567
+ .replace(/\b([a-zA-Z0-9_-]+)\.local\b/gi, '***HOST-REDACTED***');
568
+ }
569
+
570
+ /**
571
+ * Check if finding contains sensitive information
572
+ */
573
+ isSensitiveFinding(finding) {
574
+ const sensitiveTypes = [
575
+ 'wallet_file',
576
+ 'wallet_key_environment',
577
+ 'sensitive_clipboard_content',
578
+ 'private_key_detected',
579
+ 'seed_phrase_detected',
580
+ 'mnemonic_detected'
581
+ ];
582
+
583
+ const sensitiveKeywords = [
584
+ 'private key', 'seed phrase', 'mnemonic', 'wallet key',
585
+ 'ethereum address', 'bitcoin address', 'crypto key'
586
+ ];
587
+
588
+ // Check type
589
+ if (sensitiveTypes.includes(finding.type)) {
590
+ return true;
591
+ }
592
+
593
+ // Check description
594
+ if (finding.description) {
595
+ const lowerDesc = finding.description.toLowerCase();
596
+ for (const keyword of sensitiveKeywords) {
597
+ if (lowerDesc.includes(keyword)) {
598
+ return true;
599
+ }
600
+ }
601
+ }
602
+
603
+ return false;
604
+ }
605
+
606
+ /**
607
+ * Summarize resource usage for the prompt
608
+ */
609
+ formatResourceSnapshot(resourceResult) {
610
+ if (!resourceResult) return '';
611
+
612
+ const { topCpuProcesses = [], topMemoryProcesses = [], memoryStats = {} } = resourceResult;
613
+
614
+ const topCpu = topCpuProcesses.slice(0, 5).map(p => {
615
+ const sanitizedCommand = this.sanitizePathForLLM(this.sanitizeForLLM(p.command));
616
+ return `- ${sanitizedCommand} (PID ${p.pid}) CPU: ${p.cpu}% MEM: ${p.memory}MB Uptime: ${p.uptime}`;
617
+ }).join('\n');
618
+
619
+ const topMem = topMemoryProcesses.slice(0, 5).map(p => {
620
+ const sanitizedCommand = this.sanitizePathForLLM(this.sanitizeForLLM(p.command));
621
+ return `- ${sanitizedCommand} (PID ${p.pid}) MEM: ${p.memory}MB CPU: ${p.cpu}% Uptime: ${p.uptime}`;
622
+ }).join('\n');
623
+
624
+ return `Memory Stats (MB):
625
+ - Free: ${memoryStats.free ?? 'N/A'}
626
+ - Active: ${memoryStats.active ?? 'N/A'}
627
+ - Inactive: ${memoryStats.inactive ?? 'N/A'}
628
+ - Wired: ${memoryStats.wired ?? 'N/A'}
629
+ - Compressed: ${memoryStats.compressed ?? 'N/A'}
630
+
631
+ Top CPU Processes:
632
+ ${topCpu || '- None captured'}
633
+
634
+ Top Memory Processes:
635
+ ${topMem || '- None captured'}`;
636
+ }
637
+
638
+ /**
639
+ * Analyze with OpenAI
640
+ */
641
+ async analyzeWithOpenAI(prompt) {
642
+ const response = await this.client.chat.completions.create({
643
+ model: this.model,
644
+ messages: [
645
+ {
646
+ role: 'system',
647
+ content: 'You are a macOS security expert specializing in system analysis, malware detection, and security auditing. Provide clear, actionable recommendations with specific file paths and commands.'
648
+ },
649
+ {
650
+ role: 'user',
651
+ content: prompt
652
+ }
653
+ ],
654
+ temperature: 0.3,
655
+ max_tokens: this.maxTokens
656
+ });
657
+
658
+ const rawText = response.choices[0].message.content;
659
+ const perFinding = this.extractPerFinding(rawText);
660
+
661
+ return {
662
+ provider: 'openai',
663
+ model: this.model,
664
+ analysis: rawText,
665
+ perFinding,
666
+ usage: {
667
+ promptTokens: response.usage.prompt_tokens,
668
+ completionTokens: response.usage.completion_tokens,
669
+ totalTokens: response.usage.total_tokens
670
+ }
671
+ };
672
+ }
673
+
674
+ /**
675
+ * Analyze with Claude
676
+ */
677
+ async analyzeWithClaude(prompt) {
678
+ const response = await this.client.messages.create({
679
+ model: this.model,
680
+ max_tokens: this.maxTokens,
681
+ temperature: 0.3,
682
+ system: 'You are a macOS security expert specializing in system analysis, malware detection, and security auditing. Provide clear, actionable recommendations with specific file paths and commands.',
683
+ messages: [
684
+ {
685
+ role: 'user',
686
+ content: prompt
687
+ }
688
+ ]
689
+ });
690
+
691
+ const rawText = response.content[0].text;
692
+ const perFinding = this.extractPerFinding(rawText);
693
+
694
+ return {
695
+ provider: 'claude',
696
+ model: this.model,
697
+ analysis: rawText,
698
+ perFinding,
699
+ usage: {
700
+ inputTokens: response.usage.input_tokens,
701
+ outputTokens: response.usage.output_tokens
702
+ }
703
+ };
704
+ }
705
+ }