@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.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/config/default.json +79 -0
- package/package.json +60 -0
- package/src/Orchestrator.js +482 -0
- package/src/agents/BaseAgent.js +35 -0
- package/src/agents/BlockchainAgent.js +453 -0
- package/src/agents/DeFiSecurityAgent.js +257 -0
- package/src/agents/NetworkAgent.js +341 -0
- package/src/agents/PermissionAgent.js +192 -0
- package/src/agents/PersistenceAgent.js +361 -0
- package/src/agents/ProcessAgent.js +572 -0
- package/src/agents/ResourceAgent.js +217 -0
- package/src/agents/SystemAgent.js +173 -0
- package/src/config/ConfigManager.js +446 -0
- package/src/index.js +629 -0
- package/src/llm/LLMAnalyzer.js +705 -0
- package/src/logging/Logger.js +352 -0
- package/src/report/ReportManager.js +445 -0
- package/src/report/generators/MarkdownGenerator.js +173 -0
- package/src/report/generators/PDFGenerator.js +616 -0
- package/src/report/templates/report.hbs +465 -0
- package/src/report/utils/formatter.js +426 -0
- package/src/report/utils/sanitizer.js +275 -0
- package/src/utils/commander.js +42 -0
- package/src/utils/signature.js +121 -0
|
@@ -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
|
+
}
|