@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,361 @@
|
|
|
1
|
+
import { BaseAgent } from './BaseAgent.js';
|
|
2
|
+
import { executeShellCommand } from '../utils/commander.js';
|
|
3
|
+
import { readdirSync, readFileSync, existsSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { parse } from 'path';
|
|
6
|
+
|
|
7
|
+
import { getSignatureAssessment } from '../utils/signature.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* PersistenceAgent - Detects persistence mechanisms on macOS
|
|
11
|
+
* This is the core security agent
|
|
12
|
+
*/
|
|
13
|
+
export class PersistenceAgent extends BaseAgent {
|
|
14
|
+
constructor() {
|
|
15
|
+
super('PersistenceAgent');
|
|
16
|
+
this.trustedPaths = [
|
|
17
|
+
'/Applications',
|
|
18
|
+
'/System',
|
|
19
|
+
'/System/Applications',
|
|
20
|
+
'/System/Library',
|
|
21
|
+
'/System/Library/CoreServices',
|
|
22
|
+
'/System/Library/PrivateFrameworks',
|
|
23
|
+
'/System/Library/LaunchDaemons',
|
|
24
|
+
'/System/Library/LaunchAgents',
|
|
25
|
+
'/usr/bin',
|
|
26
|
+
'/usr/sbin',
|
|
27
|
+
'/usr/lib',
|
|
28
|
+
'/usr/libexec'
|
|
29
|
+
];
|
|
30
|
+
this.suspiciousCommands = ['bash', 'sh', 'curl', 'wget', 'python', 'python3', 'perl', 'ruby'];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async analyze() {
|
|
34
|
+
// Cache signature lookups across all persistence checks to avoid repeated shell calls.
|
|
35
|
+
this.signatureCache = new Map();
|
|
36
|
+
|
|
37
|
+
const launchAgents = await this.scanLaunchAgents();
|
|
38
|
+
const launchDaemons = await this.scanLaunchDaemons();
|
|
39
|
+
const loginItems = await this.scanLoginItems();
|
|
40
|
+
const crontab = await this.scanCrontab();
|
|
41
|
+
|
|
42
|
+
const allFindings = [
|
|
43
|
+
...launchAgents,
|
|
44
|
+
...launchDaemons,
|
|
45
|
+
...loginItems,
|
|
46
|
+
...crontab
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
this.results = {
|
|
50
|
+
agent: this.name,
|
|
51
|
+
timestamp: new Date().toISOString(),
|
|
52
|
+
launchAgents: launchAgents.length,
|
|
53
|
+
launchDaemons: launchDaemons.length,
|
|
54
|
+
loginItems: loginItems.length,
|
|
55
|
+
crontabEntries: crontab.length,
|
|
56
|
+
findings: allFindings,
|
|
57
|
+
overallRisk: this.calculateOverallRisk(allFindings)
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return this.results;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Scan user LaunchAgents
|
|
65
|
+
*/
|
|
66
|
+
async scanLaunchAgents() {
|
|
67
|
+
const userHome = process.env.HOME;
|
|
68
|
+
const paths = [
|
|
69
|
+
join(userHome, 'Library/LaunchAgents'),
|
|
70
|
+
'/Library/LaunchAgents'
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const findings = [];
|
|
74
|
+
|
|
75
|
+
for (const basePath of paths) {
|
|
76
|
+
if (!existsSync(basePath)) continue;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const files = readdirSync(basePath);
|
|
80
|
+
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
if (!file.endsWith('.plist')) continue;
|
|
83
|
+
|
|
84
|
+
const fullPath = join(basePath, file);
|
|
85
|
+
const finding = await this.analyzePlist(fullPath, 'LaunchAgent');
|
|
86
|
+
|
|
87
|
+
if (finding) {
|
|
88
|
+
findings.push(finding);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// Permission denied or directory doesn't exist
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return findings;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Scan LaunchDaemons (system-wide)
|
|
101
|
+
*/
|
|
102
|
+
async scanLaunchDaemons() {
|
|
103
|
+
const paths = [
|
|
104
|
+
'/Library/LaunchDaemons',
|
|
105
|
+
'/System/Library/LaunchDaemons' // Read-only verification
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const findings = [];
|
|
109
|
+
|
|
110
|
+
for (const basePath of paths) {
|
|
111
|
+
if (!existsSync(basePath)) continue;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const files = readdirSync(basePath);
|
|
115
|
+
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
if (!file.endsWith('.plist')) continue;
|
|
118
|
+
|
|
119
|
+
const fullPath = join(basePath, file);
|
|
120
|
+
const finding = await this.analyzePlist(fullPath, 'LaunchDaemon');
|
|
121
|
+
|
|
122
|
+
if (finding) {
|
|
123
|
+
findings.push(finding);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Permission denied
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return findings;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Analyze a plist file for suspicious content
|
|
136
|
+
*/
|
|
137
|
+
async analyzePlist(plistPath, type) {
|
|
138
|
+
try {
|
|
139
|
+
// Use plutil to convert plist to JSON
|
|
140
|
+
const jsonOutput = await executeShellCommand(`plutil -convert json -o - "${plistPath}"`);
|
|
141
|
+
|
|
142
|
+
if (!jsonOutput) return null;
|
|
143
|
+
|
|
144
|
+
const plist = JSON.parse(jsonOutput);
|
|
145
|
+
const risks = [];
|
|
146
|
+
let riskLevel = 'low';
|
|
147
|
+
|
|
148
|
+
// Extract program path
|
|
149
|
+
let programPath = plist.Program || (plist.ProgramArguments && plist.ProgramArguments[0]) || '';
|
|
150
|
+
|
|
151
|
+
if (!programPath) return null;
|
|
152
|
+
|
|
153
|
+
// Check 1: Program path not in trusted locations
|
|
154
|
+
const isTrustedPath = this.trustedPaths.some(trusted => programPath.startsWith(trusted));
|
|
155
|
+
|
|
156
|
+
if (!isTrustedPath && !programPath.startsWith('/Library/Application Support')) {
|
|
157
|
+
risks.push('Program path is not in trusted location');
|
|
158
|
+
riskLevel = 'medium';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check 1b: Code-signing/Gatekeeper trust check (reduces false positives)
|
|
162
|
+
const signature = await getSignatureAssessment(programPath, this.signatureCache);
|
|
163
|
+
const isGatekeeperAccepted = signature.spctlAccepted;
|
|
164
|
+
const isAppleSigned = signature.signedByApple;
|
|
165
|
+
|
|
166
|
+
// Check 2: Uses suspicious commands
|
|
167
|
+
const programName = parse(programPath).base.toLowerCase();
|
|
168
|
+
if (this.suspiciousCommands.some(cmd => programName.includes(cmd))) {
|
|
169
|
+
risks.push(`Uses potentially suspicious command: ${programName}`);
|
|
170
|
+
riskLevel = 'medium';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check 3: Impersonates Apple services
|
|
174
|
+
const fileName = parse(plistPath).name;
|
|
175
|
+
if (fileName.startsWith('com.apple.') && !plistPath.startsWith('/System')) {
|
|
176
|
+
risks.push('Potentially impersonates Apple service');
|
|
177
|
+
riskLevel = 'high';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check 4: Runs on load or keeps alive
|
|
181
|
+
if (plist.RunAtLoad || plist.KeepAlive) {
|
|
182
|
+
risks.push('Configured to run at load or stay alive');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check 5: Has network listen sockets
|
|
186
|
+
if (plist.Sockets) {
|
|
187
|
+
risks.push('Has network listen sockets configured');
|
|
188
|
+
if (!isTrustedPath) riskLevel = 'high';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check 6: Program in user home directory
|
|
192
|
+
// NOTE: Many legitimate apps/dev tools install user LaunchAgents that run from ~/Library.
|
|
193
|
+
// Treat this as a medium signal unless combined with additional indicators.
|
|
194
|
+
if (programPath.includes('/Users/')) {
|
|
195
|
+
risks.push('Program located in user home directory');
|
|
196
|
+
riskLevel = riskLevel === 'low' ? 'medium' : riskLevel;
|
|
197
|
+
|
|
198
|
+
// Elevate if running from a hidden directory (common malware tactic)
|
|
199
|
+
if (programPath.includes('/.')) {
|
|
200
|
+
risks.push('Program located in hidden directory');
|
|
201
|
+
riskLevel = 'high';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// If Gatekeeper accepts the target program and we only have weak heuristics,
|
|
206
|
+
// downgrade/remove the "non-trusted location" signal.
|
|
207
|
+
const hasStrongSignal = risks.some(r =>
|
|
208
|
+
r.includes('Potentially impersonates Apple service') ||
|
|
209
|
+
r.includes('Has network listen sockets configured') ||
|
|
210
|
+
r.includes('Program located in hidden directory')
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if ((isGatekeeperAccepted || isAppleSigned) && !hasStrongSignal) {
|
|
214
|
+
// Remove the most common noisy signal
|
|
215
|
+
const filteredRisks = risks.filter(r => r !== 'Program path is not in trusted location');
|
|
216
|
+
risks.length = 0;
|
|
217
|
+
risks.push(...filteredRisks);
|
|
218
|
+
|
|
219
|
+
if (riskLevel === 'medium') {
|
|
220
|
+
riskLevel = 'low';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// System/Apple-signed items in Apple directories are almost always benign; drop them early
|
|
225
|
+
const isSystemLocation =
|
|
226
|
+
plistPath.startsWith('/System/Library') ||
|
|
227
|
+
programPath.startsWith('/System/Library') ||
|
|
228
|
+
programPath.startsWith('/usr/libexec');
|
|
229
|
+
|
|
230
|
+
if (isAppleSigned && isSystemLocation && !hasStrongSignal) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Only report when we have strong enough signals to reduce false positives.
|
|
235
|
+
// (high risk) OR (multiple risk indicators)
|
|
236
|
+
if (risks.length > 1 || riskLevel === 'high') {
|
|
237
|
+
return {
|
|
238
|
+
type: type.toLowerCase(),
|
|
239
|
+
plist: fileName,
|
|
240
|
+
path: plistPath,
|
|
241
|
+
program: programPath,
|
|
242
|
+
label: plist.Label || fileName,
|
|
243
|
+
risks,
|
|
244
|
+
risk: riskLevel,
|
|
245
|
+
trust: {
|
|
246
|
+
spctlAccepted: signature.spctlAccepted,
|
|
247
|
+
teamIdentifier: signature.teamIdentifier,
|
|
248
|
+
signedByApple: signature.signedByApple,
|
|
249
|
+
signedByDeveloperId: signature.signedByDeveloperId
|
|
250
|
+
},
|
|
251
|
+
description: `${type}: ${risks.join(', ')}`
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return null;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Scan Login Items using AppleScript
|
|
263
|
+
*/
|
|
264
|
+
async scanLoginItems() {
|
|
265
|
+
const script = `
|
|
266
|
+
tell application "System Events"
|
|
267
|
+
get name of every login item
|
|
268
|
+
end tell
|
|
269
|
+
`;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const output = await executeShellCommand(`osascript -e '${script}'`, { quiet: true });
|
|
273
|
+
|
|
274
|
+
if (!output) return [];
|
|
275
|
+
|
|
276
|
+
const items = output.split(',').map(item => item.trim());
|
|
277
|
+
const findings = [];
|
|
278
|
+
|
|
279
|
+
for (const item of items) {
|
|
280
|
+
// Try to find suspicious patterns
|
|
281
|
+
const itemLower = item.toLowerCase();
|
|
282
|
+
|
|
283
|
+
let risk = 'low';
|
|
284
|
+
const risks = [];
|
|
285
|
+
|
|
286
|
+
if (itemLower.includes('hidden') || itemLower.includes('crypto') || itemLower.includes('miner')) {
|
|
287
|
+
risk = 'high';
|
|
288
|
+
risks.push('Login item has suspicious name');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (risk !== 'low' || risks.length > 0) {
|
|
292
|
+
findings.push({
|
|
293
|
+
type: 'login_item',
|
|
294
|
+
name: item,
|
|
295
|
+
risks,
|
|
296
|
+
risk,
|
|
297
|
+
description: `Login item: ${item}`
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return findings;
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Scan crontab entries
|
|
310
|
+
*/
|
|
311
|
+
async scanCrontab() {
|
|
312
|
+
try {
|
|
313
|
+
const output = await executeShellCommand(
|
|
314
|
+
'command -v crontab >/dev/null 2>&1 && crontab -l 2>/dev/null',
|
|
315
|
+
{ quiet: true }
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (!output || output.includes('no crontab')) return [];
|
|
319
|
+
|
|
320
|
+
const lines = output.split('\n').filter(line => line.trim() && !line.startsWith('#'));
|
|
321
|
+
const findings = [];
|
|
322
|
+
|
|
323
|
+
for (const line of lines) {
|
|
324
|
+
let risk = 'low';
|
|
325
|
+
const risks = [];
|
|
326
|
+
|
|
327
|
+
// Check for suspicious commands
|
|
328
|
+
if (this.suspiciousCommands.some(cmd => line.toLowerCase().includes(cmd))) {
|
|
329
|
+
risks.push('Uses shell command in crontab');
|
|
330
|
+
risk = 'medium';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check for network operations
|
|
334
|
+
if (line.includes('curl') || line.includes('wget')) {
|
|
335
|
+
risks.push('Performs network operations');
|
|
336
|
+
risk = 'high';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Check for execution from user directories
|
|
340
|
+
if (line.includes('/Users/') && !line.includes('/Applications')) {
|
|
341
|
+
risks.push('Executes from user directory');
|
|
342
|
+
risk = 'high';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (risks.length > 0) {
|
|
346
|
+
findings.push({
|
|
347
|
+
type: 'crontab',
|
|
348
|
+
entry: line,
|
|
349
|
+
risks,
|
|
350
|
+
risk,
|
|
351
|
+
description: `Crontab entry: ${risks.join(', ')}`
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return findings;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|