@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,217 @@
|
|
|
1
|
+
import { BaseAgent } from './BaseAgent.js';
|
|
2
|
+
import { executeShellCommand } from '../utils/commander.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ResourceAgent - Analyzes CPU, Memory, and IO usage
|
|
6
|
+
*/
|
|
7
|
+
export class ResourceAgent extends BaseAgent {
|
|
8
|
+
constructor() {
|
|
9
|
+
super('ResourceAgent');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async analyze() {
|
|
13
|
+
const processes = await this.getProcessInfo();
|
|
14
|
+
const memory = await this.getMemoryInfo();
|
|
15
|
+
const findings = this.analyzeFindings(processes);
|
|
16
|
+
|
|
17
|
+
this.results = {
|
|
18
|
+
agent: this.name,
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
topCpuProcesses: processes.cpu.slice(0, 10),
|
|
21
|
+
topMemoryProcesses: processes.memory.slice(0, 10),
|
|
22
|
+
memoryStats: memory,
|
|
23
|
+
suspiciousProcesses: findings,
|
|
24
|
+
overallRisk: this.calculateOverallRisk(findings)
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return this.results;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get process information using ps and top
|
|
32
|
+
*/
|
|
33
|
+
async getProcessInfo() {
|
|
34
|
+
const psOutput = await executeShellCommand('ps -axo pid,ppid,comm,%cpu,rss,etime');
|
|
35
|
+
const lines = psOutput.split('\n').slice(1); // Skip header
|
|
36
|
+
|
|
37
|
+
const processes = [];
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
if (!line.trim()) continue;
|
|
40
|
+
|
|
41
|
+
const parts = line.trim().split(/\s+/);
|
|
42
|
+
if (parts.length >= 6) {
|
|
43
|
+
const pid = parseInt(parts[0]);
|
|
44
|
+
const ppid = parseInt(parts[1]);
|
|
45
|
+
const command = parts[2];
|
|
46
|
+
const cpu = parseFloat(parts[3]);
|
|
47
|
+
const rss = parseInt(parts[4]); // KB
|
|
48
|
+
const etime = parts[5];
|
|
49
|
+
const uptimeSeconds = this.parseElapsedTime(etime);
|
|
50
|
+
|
|
51
|
+
processes.push({
|
|
52
|
+
pid,
|
|
53
|
+
ppid,
|
|
54
|
+
command,
|
|
55
|
+
cpu,
|
|
56
|
+
memory: Math.round(rss / 1024), // Convert to MB
|
|
57
|
+
uptime: etime,
|
|
58
|
+
uptimeSeconds
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cpuSorted = [...processes].sort((a, b) => b.cpu - a.cpu);
|
|
64
|
+
const memorySorted = [...processes].sort((a, b) => b.memory - a.memory);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
cpu: cpuSorted,
|
|
68
|
+
memory: memorySorted,
|
|
69
|
+
all: processes
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get memory statistics using vm_stat
|
|
75
|
+
*/
|
|
76
|
+
async getMemoryInfo() {
|
|
77
|
+
const vmOutput = await executeShellCommand('vm_stat');
|
|
78
|
+
const lines = vmOutput.split('\n');
|
|
79
|
+
|
|
80
|
+
const stats = {};
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
if (line.includes(':')) {
|
|
83
|
+
const [key, value] = line.split(':');
|
|
84
|
+
const cleanKey = key.trim().replace(/[^a-zA-Z0-9]/g, '_');
|
|
85
|
+
const numValue = parseInt(value.trim().replace('.', ''));
|
|
86
|
+
if (!isNaN(numValue)) {
|
|
87
|
+
stats[cleanKey] = numValue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Convert pages to MB (page size is typically 4096 bytes)
|
|
93
|
+
const pageSize = 4096;
|
|
94
|
+
const toMB = (pages) => Math.round((pages * pageSize) / (1024 * 1024));
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
free: toMB(stats.Pages_free || 0),
|
|
98
|
+
active: toMB(stats.Pages_active || 0),
|
|
99
|
+
inactive: toMB(stats.Pages_inactive || 0),
|
|
100
|
+
wired: toMB(stats.Pages_wired_down || 0),
|
|
101
|
+
compressed: toMB(stats.Pages_occupied_by_compressor || 0)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Analyze processes for suspicious activity
|
|
107
|
+
*/
|
|
108
|
+
analyzeFindings(processes) {
|
|
109
|
+
const findings = [];
|
|
110
|
+
const systemPaths = ['/Applications', '/System', '/usr/bin', '/usr/sbin', '/bin', '/sbin'];
|
|
111
|
+
const longRunningReported = new Set();
|
|
112
|
+
|
|
113
|
+
// Check high CPU processes not in system paths
|
|
114
|
+
for (const proc of processes.cpu.slice(0, 20)) {
|
|
115
|
+
if (proc.cpu > 50) {
|
|
116
|
+
const isSystemPath = systemPaths.some(path => proc.command.startsWith(path));
|
|
117
|
+
|
|
118
|
+
if (!isSystemPath && !proc.command.startsWith('/Library')) {
|
|
119
|
+
findings.push({
|
|
120
|
+
type: 'high_cpu_unusual_path',
|
|
121
|
+
pid: proc.pid,
|
|
122
|
+
command: proc.command,
|
|
123
|
+
cpu: proc.cpu,
|
|
124
|
+
memory: proc.memory,
|
|
125
|
+
risk: proc.cpu > 80 ? 'high' : 'medium',
|
|
126
|
+
description: `High CPU usage (${proc.cpu}%) from non-system path`
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check high memory processes
|
|
133
|
+
for (const proc of processes.memory.slice(0, 20)) {
|
|
134
|
+
if (proc.memory > 1000) { // > 1GB
|
|
135
|
+
const isSystemPath = systemPaths.some(path => proc.command.startsWith(path));
|
|
136
|
+
|
|
137
|
+
if (!isSystemPath) {
|
|
138
|
+
findings.push({
|
|
139
|
+
type: 'high_memory_unusual_path',
|
|
140
|
+
pid: proc.pid,
|
|
141
|
+
command: proc.command,
|
|
142
|
+
cpu: proc.cpu,
|
|
143
|
+
memory: proc.memory,
|
|
144
|
+
risk: proc.memory > 2000 ? 'high' : 'medium',
|
|
145
|
+
description: `High memory usage (${proc.memory}MB) from non-system path`
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check for processes with suspicious names
|
|
152
|
+
const suspiciousNames = ['miner', 'crypto', 'xmrig', 'coinhive', 'malware', 'trojan'];
|
|
153
|
+
for (const proc of processes.all) {
|
|
154
|
+
const commandLower = proc.command.toLowerCase();
|
|
155
|
+
for (const suspName of suspiciousNames) {
|
|
156
|
+
if (commandLower.includes(suspName)) {
|
|
157
|
+
findings.push({
|
|
158
|
+
type: 'suspicious_name',
|
|
159
|
+
pid: proc.pid,
|
|
160
|
+
command: proc.command,
|
|
161
|
+
cpu: proc.cpu,
|
|
162
|
+
memory: proc.memory,
|
|
163
|
+
risk: 'high',
|
|
164
|
+
description: `Process name contains suspicious keyword: ${suspName}`
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check for long-running background processes from non-system paths
|
|
171
|
+
for (const proc of processes.all) {
|
|
172
|
+
if (!proc.uptimeSeconds || proc.uptimeSeconds < 86400) continue; // > 24h
|
|
173
|
+
|
|
174
|
+
const isSystemPath = systemPaths.some(path => proc.command.startsWith(path));
|
|
175
|
+
if (isSystemPath || proc.command.startsWith('/Library')) continue;
|
|
176
|
+
|
|
177
|
+
const key = `${proc.pid}-long`;
|
|
178
|
+
if (longRunningReported.has(key)) continue;
|
|
179
|
+
|
|
180
|
+
longRunningReported.add(key);
|
|
181
|
+
|
|
182
|
+
const hours = Math.round(proc.uptimeSeconds / 3600);
|
|
183
|
+
findings.push({
|
|
184
|
+
type: 'long_running_background',
|
|
185
|
+
pid: proc.pid,
|
|
186
|
+
command: proc.command,
|
|
187
|
+
cpu: proc.cpu,
|
|
188
|
+
memory: proc.memory,
|
|
189
|
+
uptime: proc.uptime,
|
|
190
|
+
risk: proc.uptimeSeconds > 259200 ? 'high' : 'medium', // >3d high
|
|
191
|
+
description: `Process running ${hours}h from non-system path`
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return findings;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Convert ps etime ([[dd-]hh:]mm:ss) to seconds
|
|
200
|
+
*/
|
|
201
|
+
parseElapsedTime(etime) {
|
|
202
|
+
if (!etime) return 0;
|
|
203
|
+
|
|
204
|
+
const [dayPart, timePartRaw] = etime.includes('-') ? etime.split('-') : [null, etime];
|
|
205
|
+
const timePart = timePartRaw || etime;
|
|
206
|
+
const segments = timePart.split(':').map(seg => parseInt(seg, 10) || 0);
|
|
207
|
+
|
|
208
|
+
while (segments.length < 3) {
|
|
209
|
+
segments.unshift(0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const [hours, minutes, seconds] = segments.slice(-3);
|
|
213
|
+
const days = dayPart ? parseInt(dayPart, 10) || 0 : 0;
|
|
214
|
+
|
|
215
|
+
return (days * 86400) + (hours * 3600) + (minutes * 60) + seconds;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { BaseAgent } from './BaseAgent.js';
|
|
2
|
+
import { executeShellCommand } from '../utils/commander.js';
|
|
3
|
+
import { existsSync, readFileSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SystemAgent - Checks system integrity and security settings
|
|
7
|
+
*/
|
|
8
|
+
export class SystemAgent extends BaseAgent {
|
|
9
|
+
constructor() {
|
|
10
|
+
super('SystemAgent');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async analyze() {
|
|
14
|
+
const sip = await this.checkSIP();
|
|
15
|
+
const gatekeeper = await this.checkGatekeeper();
|
|
16
|
+
const updates = await this.checkSystemUpdates();
|
|
17
|
+
const sudoers = await this.checkSudoers();
|
|
18
|
+
|
|
19
|
+
const findings = [];
|
|
20
|
+
|
|
21
|
+
// Evaluate SIP
|
|
22
|
+
if (!sip.enabled) {
|
|
23
|
+
findings.push({
|
|
24
|
+
type: 'sip_disabled',
|
|
25
|
+
risk: 'high',
|
|
26
|
+
description: 'System Integrity Protection (SIP) is disabled'
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Evaluate Gatekeeper
|
|
31
|
+
if (!gatekeeper.enabled) {
|
|
32
|
+
findings.push({
|
|
33
|
+
type: 'gatekeeper_disabled',
|
|
34
|
+
risk: 'medium',
|
|
35
|
+
description: 'Gatekeeper is disabled, allowing unsigned apps'
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Evaluate updates
|
|
40
|
+
if (updates.available > 0) {
|
|
41
|
+
findings.push({
|
|
42
|
+
type: 'updates_available',
|
|
43
|
+
risk: 'medium',
|
|
44
|
+
description: `${updates.available} system update(s) available`
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Evaluate sudoers
|
|
49
|
+
if (sudoers.risks.length > 0) {
|
|
50
|
+
findings.push(...sudoers.risks);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.results = {
|
|
54
|
+
agent: this.name,
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
sip,
|
|
57
|
+
gatekeeper,
|
|
58
|
+
updates,
|
|
59
|
+
sudoers: sudoers.rules,
|
|
60
|
+
findings,
|
|
61
|
+
overallRisk: this.calculateOverallRisk(findings)
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return this.results;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check System Integrity Protection status
|
|
69
|
+
*/
|
|
70
|
+
async checkSIP() {
|
|
71
|
+
const output = await executeShellCommand('csrutil status');
|
|
72
|
+
const enabled = output.toLowerCase().includes('enabled');
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
enabled,
|
|
76
|
+
status: output.trim()
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check Gatekeeper status
|
|
82
|
+
*/
|
|
83
|
+
async checkGatekeeper() {
|
|
84
|
+
const output = await executeShellCommand('spctl --status');
|
|
85
|
+
const enabled = output.toLowerCase().includes('assessments enabled');
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
enabled,
|
|
89
|
+
status: output.trim()
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check for available system updates
|
|
95
|
+
*/
|
|
96
|
+
async checkSystemUpdates() {
|
|
97
|
+
const output = await executeShellCommand('softwareupdate -l 2>&1');
|
|
98
|
+
|
|
99
|
+
// Parse update count
|
|
100
|
+
const lines = output.split('\n');
|
|
101
|
+
let updateCount = 0;
|
|
102
|
+
const updates = [];
|
|
103
|
+
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
if (line.includes('*') || line.includes('Label:')) {
|
|
106
|
+
updateCount++;
|
|
107
|
+
updates.push(line.trim());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If output says "No new software available"
|
|
112
|
+
if (output.toLowerCase().includes('no new software available')) {
|
|
113
|
+
updateCount = 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
available: updateCount,
|
|
118
|
+
updates: updates.slice(0, 5), // Limit to first 5
|
|
119
|
+
lastCheck: new Date().toISOString()
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check sudoers configuration for security issues
|
|
125
|
+
*/
|
|
126
|
+
async checkSudoers() {
|
|
127
|
+
const rules = [];
|
|
128
|
+
const risks = [];
|
|
129
|
+
|
|
130
|
+
// Check main sudoers file
|
|
131
|
+
const sudoersPath = '/etc/sudoers';
|
|
132
|
+
const sudoersDPath = '/etc/sudoers.d';
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// Read sudoers file (may require permissions)
|
|
136
|
+
if (existsSync(sudoersPath)) {
|
|
137
|
+
const content = readFileSync(sudoersPath, 'utf-8');
|
|
138
|
+
const lines = content.split('\n');
|
|
139
|
+
|
|
140
|
+
for (const line of lines) {
|
|
141
|
+
if (line.trim() && !line.startsWith('#')) {
|
|
142
|
+
rules.push(line.trim());
|
|
143
|
+
|
|
144
|
+
// Check for NOPASSWD rules
|
|
145
|
+
if (line.includes('NOPASSWD')) {
|
|
146
|
+
risks.push({
|
|
147
|
+
type: 'sudoers_nopasswd',
|
|
148
|
+
rule: line.trim(),
|
|
149
|
+
risk: 'medium',
|
|
150
|
+
description: 'Sudoers rule allows passwordless sudo'
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check for overly permissive ALL rules
|
|
155
|
+
if (line.includes('ALL=(ALL)') || line.includes('ALL = (ALL) ALL')) {
|
|
156
|
+
risks.push({
|
|
157
|
+
type: 'sudoers_all_permissions',
|
|
158
|
+
rule: line.trim(),
|
|
159
|
+
risk: 'high',
|
|
160
|
+
description: 'Sudoers rule grants full permissions'
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
// Permission denied or file doesn't exist
|
|
168
|
+
rules.push('Unable to read sudoers file (permission denied)');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { rules, risks };
|
|
172
|
+
}
|
|
173
|
+
}
|