50c 3.9.5 → 3.9.7
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.
Potentially problematic release.
This version of 50c might be problematic. Click here for more details.
- package/README.md +175 -201
- package/bin/50c.js +2079 -2278
- package/lib/pre-publish.js +812 -1361
- package/lib/subagent.js +7 -13
- package/lib/team.js +691 -714
- package/lib/tools-registry.js +184 -226
- package/package.json +39 -39
- package/lib/backdoor-checker.js +0 -732
- package/lib/ip-utils.js +0 -47
package/lib/backdoor-checker.js
DELETED
|
@@ -1,732 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 50c Backdoor Checker - Local security audit tool
|
|
3
|
-
* Runs entirely on the user's machine. FREE.
|
|
4
|
-
*
|
|
5
|
-
* Detects:
|
|
6
|
-
* - Unauthorized RDP/SSH logins (with geo-IP)
|
|
7
|
-
* - Persistence mechanisms (scheduled tasks, startup, registry, services)
|
|
8
|
-
* - Suspicious network connections to foreign IPs
|
|
9
|
-
* - Rogue certificates and proxy settings
|
|
10
|
-
* - IDE artifacts (Verdant, etc.)
|
|
11
|
-
* - Unauthorized user accounts
|
|
12
|
-
* - SSH authorized_keys anomalies
|
|
13
|
-
*
|
|
14
|
-
* Cross-platform: Windows (PowerShell), Linux (bash), macOS (bash)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const { exec, execSync } = require('child_process');
|
|
18
|
-
const os = require('os');
|
|
19
|
-
const fs = require('fs');
|
|
20
|
-
const path = require('path');
|
|
21
|
-
const { isPrivateIP, isValidIPv4, extractPublicIPs } = require('./ip-utils');
|
|
22
|
-
|
|
23
|
-
const PLATFORM = os.platform(); // win32, linux, darwin
|
|
24
|
-
const HOME = os.homedir();
|
|
25
|
-
|
|
26
|
-
// Suspicious country indicators in IP geolocation
|
|
27
|
-
const SUSPICIOUS_GEOS = ['CN', 'RU', 'KP', 'IR'];
|
|
28
|
-
|
|
29
|
-
// Known suspicious IDE directories
|
|
30
|
-
const SUSPICIOUS_IDE_DIRS = [
|
|
31
|
-
'.verdent', '.verdant', '.verd',
|
|
32
|
-
'.trae', // ByteDance IDE
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
// Known suspicious process names
|
|
36
|
-
const SUSPICIOUS_PROCESSES = [
|
|
37
|
-
'cryptominer', 'xmrig', 'minerd', 'cgminer',
|
|
38
|
-
'nc.exe', 'ncat', 'netcat',
|
|
39
|
-
'mimikatz', 'lazagne', 'procdump',
|
|
40
|
-
'psexec', 'wmic', // lateral movement
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Run a command and return stdout (or error message)
|
|
45
|
-
*/
|
|
46
|
-
function run(cmd, timeout = 30000) {
|
|
47
|
-
try {
|
|
48
|
-
return execSync(cmd, {
|
|
49
|
-
timeout,
|
|
50
|
-
encoding: 'utf8',
|
|
51
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
52
|
-
windowsHide: true,
|
|
53
|
-
maxBuffer: 10 * 1024 * 1024
|
|
54
|
-
}).trim();
|
|
55
|
-
} catch (e) {
|
|
56
|
-
return `[ERROR] ${e.message}`;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Run PowerShell command (Windows)
|
|
62
|
-
*/
|
|
63
|
-
function ps(script, timeout = 30000) {
|
|
64
|
-
const escaped = script.replace(/"/g, '\\"');
|
|
65
|
-
return run(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${escaped}"`, timeout);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Geo-IP lookup using free API (ip-api.com)
|
|
70
|
-
* Returns { country, countryCode, city, isp, org }
|
|
71
|
-
*/
|
|
72
|
-
async function geoIP(ip) {
|
|
73
|
-
try {
|
|
74
|
-
const http = require('http');
|
|
75
|
-
return new Promise((resolve, reject) => {
|
|
76
|
-
const req = http.get(`http://ip-api.com/json/${ip}?fields=country,countryCode,city,isp,org`, { timeout: 5000 }, (res) => {
|
|
77
|
-
let data = '';
|
|
78
|
-
res.on('data', c => data += c);
|
|
79
|
-
res.on('end', () => {
|
|
80
|
-
try { resolve(JSON.parse(data)); }
|
|
81
|
-
catch { resolve({ country: 'unknown', countryCode: '??' }); }
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
req.on('error', () => resolve({ country: 'unknown', countryCode: '??' }));
|
|
85
|
-
req.on('timeout', () => { req.destroy(); resolve({ country: 'unknown', countryCode: '??' }); });
|
|
86
|
-
});
|
|
87
|
-
} catch {
|
|
88
|
-
return { country: 'unknown', countryCode: '??' };
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Batch geo-IP lookup (ip-api.com supports batch of up to 100)
|
|
94
|
-
*/
|
|
95
|
-
async function batchGeoIP(ips) {
|
|
96
|
-
if (ips.length === 0) return {};
|
|
97
|
-
const uniqueIPs = [...new Set(ips)].slice(0, 100);
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const http = require('http');
|
|
101
|
-
const body = JSON.stringify(uniqueIPs.map(ip => ({ query: ip, fields: 'query,country,countryCode,city,isp,org' })));
|
|
102
|
-
|
|
103
|
-
return new Promise((resolve) => {
|
|
104
|
-
const req = http.request({
|
|
105
|
-
hostname: 'ip-api.com',
|
|
106
|
-
path: '/batch',
|
|
107
|
-
method: 'POST',
|
|
108
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
109
|
-
timeout: 10000
|
|
110
|
-
}, (res) => {
|
|
111
|
-
let data = '';
|
|
112
|
-
res.on('data', c => data += c);
|
|
113
|
-
res.on('end', () => {
|
|
114
|
-
try {
|
|
115
|
-
const results = JSON.parse(data);
|
|
116
|
-
const map = {};
|
|
117
|
-
for (const r of results) { map[r.query] = r; }
|
|
118
|
-
resolve(map);
|
|
119
|
-
} catch { resolve({}); }
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
req.on('error', () => resolve({}));
|
|
123
|
-
req.on('timeout', () => { req.destroy(); resolve({}); });
|
|
124
|
-
req.write(body);
|
|
125
|
-
req.end();
|
|
126
|
-
});
|
|
127
|
-
} catch {
|
|
128
|
-
return {};
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ============================================
|
|
133
|
-
// WINDOWS CHECKS
|
|
134
|
-
// ============================================
|
|
135
|
-
|
|
136
|
-
function windowsChecks() {
|
|
137
|
-
const findings = [];
|
|
138
|
-
|
|
139
|
-
// 1. RDP Login History (Event ID 4624 Type 10 = RemoteInteractive)
|
|
140
|
-
findings.push({ check: 'RDP Login History', data: getRDPLogins() });
|
|
141
|
-
|
|
142
|
-
// 2. Failed RDP attempts (Event ID 4625)
|
|
143
|
-
findings.push({ check: 'Failed Login Attempts', data: getFailedLogins() });
|
|
144
|
-
|
|
145
|
-
// 3. Local user accounts
|
|
146
|
-
findings.push({ check: 'Local User Accounts', data: getLocalUsers() });
|
|
147
|
-
|
|
148
|
-
// 4. Scheduled tasks (persistence)
|
|
149
|
-
findings.push({ check: 'Scheduled Tasks', data: getScheduledTasks() });
|
|
150
|
-
|
|
151
|
-
// 5. Startup programs (registry Run keys)
|
|
152
|
-
findings.push({ check: 'Startup Registry Keys', data: getStartupKeys() });
|
|
153
|
-
|
|
154
|
-
// 6. Running services (unusual ones)
|
|
155
|
-
findings.push({ check: 'Services', data: getServices() });
|
|
156
|
-
|
|
157
|
-
// 7. Active network connections
|
|
158
|
-
findings.push({ check: 'Active Network Connections', data: getNetConnections() });
|
|
159
|
-
|
|
160
|
-
// 8. Firewall rules
|
|
161
|
-
findings.push({ check: 'Firewall Rules', data: getFirewallRules() });
|
|
162
|
-
|
|
163
|
-
// 9. Certificates
|
|
164
|
-
findings.push({ check: 'Root Certificates', data: getCertificates() });
|
|
165
|
-
|
|
166
|
-
// 10. Proxy settings
|
|
167
|
-
findings.push({ check: 'Proxy Settings', data: getProxySettings() });
|
|
168
|
-
|
|
169
|
-
// 11. WinRM / Remote Management
|
|
170
|
-
findings.push({ check: 'Remote Management (WinRM)', data: getWinRM() });
|
|
171
|
-
|
|
172
|
-
// 12. SSH authorized_keys
|
|
173
|
-
findings.push({ check: 'SSH Authorized Keys', data: getSSHKeys() });
|
|
174
|
-
|
|
175
|
-
// 13. Suspicious IDE artifacts
|
|
176
|
-
findings.push({ check: 'Suspicious IDE Artifacts', data: getIDEArtifacts() });
|
|
177
|
-
|
|
178
|
-
// 14. PowerShell history (may reveal attacker commands)
|
|
179
|
-
findings.push({ check: 'PowerShell History', data: getPSHistory() });
|
|
180
|
-
|
|
181
|
-
// 15. DNS cache (suspicious domains)
|
|
182
|
-
findings.push({ check: 'DNS Cache', data: getDNSCache() });
|
|
183
|
-
|
|
184
|
-
// 16. Recently installed programs
|
|
185
|
-
findings.push({ check: 'Recently Installed Programs', data: getRecentInstalls() });
|
|
186
|
-
|
|
187
|
-
// 17. Hidden/suspicious processes
|
|
188
|
-
findings.push({ check: 'Suspicious Processes', data: getSuspiciousProcesses() });
|
|
189
|
-
|
|
190
|
-
return findings;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function getRDPLogins() {
|
|
194
|
-
const result = ps(`
|
|
195
|
-
try {
|
|
196
|
-
$events = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624} -MaxEvents 200 -ErrorAction Stop |
|
|
197
|
-
Where-Object { $_.Properties[8].Value -eq 10 } |
|
|
198
|
-
Select-Object -First 50 |
|
|
199
|
-
ForEach-Object {
|
|
200
|
-
$ip = $_.Properties[18].Value
|
|
201
|
-
$user = $_.Properties[5].Value
|
|
202
|
-
$domain = $_.Properties[6].Value
|
|
203
|
-
$time = $_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
|
|
204
|
-
"$time | $domain\\\\$user | $ip"
|
|
205
|
-
}
|
|
206
|
-
if ($events) { $events -join '\\n' } else { 'No RDP logins found in Security log' }
|
|
207
|
-
} catch { 'Access denied or Security log unavailable - run as Administrator' }
|
|
208
|
-
`, 60000);
|
|
209
|
-
return result;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function getFailedLogins() {
|
|
213
|
-
const result = ps(`
|
|
214
|
-
try {
|
|
215
|
-
$events = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} -MaxEvents 100 -ErrorAction Stop |
|
|
216
|
-
Select-Object -First 30 |
|
|
217
|
-
ForEach-Object {
|
|
218
|
-
$ip = $_.Properties[19].Value
|
|
219
|
-
$user = $_.Properties[5].Value
|
|
220
|
-
$time = $_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
|
|
221
|
-
"$time | $user | $ip"
|
|
222
|
-
}
|
|
223
|
-
if ($events) { $events -join '\\n' } else { 'No failed login attempts found' }
|
|
224
|
-
} catch { 'Access denied or Security log unavailable - run as Administrator' }
|
|
225
|
-
`, 60000);
|
|
226
|
-
return result;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function getLocalUsers() {
|
|
230
|
-
return ps(`
|
|
231
|
-
Get-LocalUser | ForEach-Object {
|
|
232
|
-
$name = $_.Name
|
|
233
|
-
$enabled = $_.Enabled
|
|
234
|
-
$lastLogon = $_.LastLogon
|
|
235
|
-
$desc = $_.Description
|
|
236
|
-
"$name | Enabled=$enabled | LastLogon=$lastLogon | $desc"
|
|
237
|
-
} | Out-String
|
|
238
|
-
`);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function getScheduledTasks() {
|
|
242
|
-
return ps(`
|
|
243
|
-
Get-ScheduledTask | Where-Object { $_.State -ne 'Disabled' -and $_.TaskPath -notlike '\\\\Microsoft\\\\*' } |
|
|
244
|
-
ForEach-Object {
|
|
245
|
-
$name = $_.TaskName
|
|
246
|
-
$path = $_.TaskPath
|
|
247
|
-
$actions = ($_.Actions | ForEach-Object { $_.Execute + ' ' + $_.Arguments }) -join '; '
|
|
248
|
-
"$path$name | $actions"
|
|
249
|
-
} | Out-String
|
|
250
|
-
`, 60000);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function getStartupKeys() {
|
|
254
|
-
const keys = [
|
|
255
|
-
'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run',
|
|
256
|
-
'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce',
|
|
257
|
-
'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run',
|
|
258
|
-
'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce',
|
|
259
|
-
'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Run',
|
|
260
|
-
];
|
|
261
|
-
|
|
262
|
-
return ps(`
|
|
263
|
-
$keys = @(${keys.map(k => `'${k}'`).join(',')})
|
|
264
|
-
foreach ($key in $keys) {
|
|
265
|
-
if (Test-Path $key) {
|
|
266
|
-
"=== $key ==="
|
|
267
|
-
Get-ItemProperty $key -ErrorAction SilentlyContinue |
|
|
268
|
-
ForEach-Object { $_.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' } |
|
|
269
|
-
ForEach-Object { " $($_.Name) = $($_.Value)" }
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
`);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function getServices() {
|
|
277
|
-
return ps(`
|
|
278
|
-
Get-Service | Where-Object { $_.Status -eq 'Running' -and $_.StartType -eq 'Automatic' } |
|
|
279
|
-
Where-Object { $_.DisplayName -notlike 'Windows*' -and $_.DisplayName -notlike 'Microsoft*' -and $_.DisplayName -notlike 'DCOM*' -and $_.DisplayName -notlike 'Plug*' } |
|
|
280
|
-
Select-Object Name, DisplayName, StartType |
|
|
281
|
-
Format-Table -AutoSize | Out-String
|
|
282
|
-
`);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function getNetConnections() {
|
|
286
|
-
return ps(`
|
|
287
|
-
Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue |
|
|
288
|
-
Where-Object { $_.RemoteAddress -ne '127.0.0.1' -and $_.RemoteAddress -ne '::1' -and $_.RemoteAddress -ne '0.0.0.0' } |
|
|
289
|
-
Select-Object LocalPort, RemoteAddress, RemotePort, OwningProcess,
|
|
290
|
-
@{N='Process';E={(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName}} |
|
|
291
|
-
Sort-Object RemoteAddress |
|
|
292
|
-
Format-Table -AutoSize | Out-String
|
|
293
|
-
`);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function getFirewallRules() {
|
|
297
|
-
return ps(`
|
|
298
|
-
Get-NetFirewallRule -Enabled True -Direction Inbound -Action Allow -ErrorAction SilentlyContinue |
|
|
299
|
-
Where-Object { $_.DisplayName -notlike 'Core Networking*' -and $_.DisplayName -notlike 'Windows*' } |
|
|
300
|
-
Select-Object -First 30 DisplayName, Profile, Direction |
|
|
301
|
-
Format-Table -AutoSize | Out-String
|
|
302
|
-
`);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function getCertificates() {
|
|
306
|
-
return ps(`
|
|
307
|
-
Get-ChildItem Cert:\\LocalMachine\\Root |
|
|
308
|
-
Where-Object { $_.NotAfter -gt (Get-Date) } |
|
|
309
|
-
Select-Object Subject, Issuer, NotAfter, Thumbprint |
|
|
310
|
-
Format-Table -AutoSize -Wrap | Out-String
|
|
311
|
-
`);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function getProxySettings() {
|
|
315
|
-
return ps(`
|
|
316
|
-
$proxy = Get-ItemProperty 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' -ErrorAction SilentlyContinue
|
|
317
|
-
"ProxyEnable: $($proxy.ProxyEnable)"
|
|
318
|
-
"ProxyServer: $($proxy.ProxyServer)"
|
|
319
|
-
"ProxyOverride: $($proxy.ProxyOverride)"
|
|
320
|
-
"AutoConfigURL: $($proxy.AutoConfigURL)"
|
|
321
|
-
""
|
|
322
|
-
"Environment:"
|
|
323
|
-
"HTTP_PROXY: $env:HTTP_PROXY"
|
|
324
|
-
"HTTPS_PROXY: $env:HTTPS_PROXY"
|
|
325
|
-
"ALL_PROXY: $env:ALL_PROXY"
|
|
326
|
-
`);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function getWinRM() {
|
|
330
|
-
return ps(`
|
|
331
|
-
try {
|
|
332
|
-
$status = Get-Service WinRM -ErrorAction Stop
|
|
333
|
-
"WinRM Status: $($status.Status)"
|
|
334
|
-
if ($status.Status -eq 'Running') {
|
|
335
|
-
"WARNING: WinRM is running - remote management is enabled"
|
|
336
|
-
winrm get winrm/config/client 2>$null
|
|
337
|
-
}
|
|
338
|
-
} catch {
|
|
339
|
-
"WinRM: Not found or access denied"
|
|
340
|
-
}
|
|
341
|
-
`);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function getSSHKeys() {
|
|
345
|
-
const results = [];
|
|
346
|
-
const sshDir = path.join(HOME, '.ssh');
|
|
347
|
-
|
|
348
|
-
if (fs.existsSync(sshDir)) {
|
|
349
|
-
const authKeys = path.join(sshDir, 'authorized_keys');
|
|
350
|
-
if (fs.existsSync(authKeys)) {
|
|
351
|
-
results.push('=== authorized_keys ===');
|
|
352
|
-
const content = fs.readFileSync(authKeys, 'utf8');
|
|
353
|
-
const keys = content.split('\n').filter(l => l.trim() && !l.startsWith('#'));
|
|
354
|
-
for (const key of keys) {
|
|
355
|
-
const parts = key.trim().split(/\s+/);
|
|
356
|
-
const comment = parts.length >= 3 ? parts.slice(2).join(' ') : 'no-comment';
|
|
357
|
-
const type = parts[0];
|
|
358
|
-
results.push(` ${type} ... ${comment}`);
|
|
359
|
-
}
|
|
360
|
-
} else {
|
|
361
|
-
results.push('No authorized_keys file found');
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Check for unusual files in .ssh
|
|
365
|
-
const files = fs.readdirSync(sshDir);
|
|
366
|
-
const unusual = files.filter(f => !['authorized_keys', 'known_hosts', 'config', 'id_rsa', 'id_rsa.pub', 'id_ed25519', 'id_ed25519.pub', 'id_ecdsa', 'id_ecdsa.pub', 'id_ed25519_automation', 'id_ed25519_automation.pub'].includes(f));
|
|
367
|
-
if (unusual.length > 0) {
|
|
368
|
-
results.push(`\nUnusual files in .ssh/: ${unusual.join(', ')}`);
|
|
369
|
-
}
|
|
370
|
-
} else {
|
|
371
|
-
results.push('No .ssh directory found');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
return results.join('\n');
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function getIDEArtifacts() {
|
|
378
|
-
const results = [];
|
|
379
|
-
|
|
380
|
-
for (const dir of SUSPICIOUS_IDE_DIRS) {
|
|
381
|
-
const fullPath = path.join(HOME, dir);
|
|
382
|
-
if (fs.existsSync(fullPath)) {
|
|
383
|
-
results.push(`[!] FOUND: ${fullPath}`);
|
|
384
|
-
try {
|
|
385
|
-
const files = fs.readdirSync(fullPath);
|
|
386
|
-
results.push(` Contents: ${files.slice(0, 20).join(', ')}`);
|
|
387
|
-
|
|
388
|
-
// Check for MCP config with embedded secrets
|
|
389
|
-
const mcpPath = path.join(fullPath, 'mcp.json');
|
|
390
|
-
if (fs.existsSync(mcpPath)) {
|
|
391
|
-
results.push(` [!!] MCP config found: ${mcpPath}`);
|
|
392
|
-
try {
|
|
393
|
-
const mcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
|
|
394
|
-
const servers = Object.keys(mcp.mcpServers || {});
|
|
395
|
-
results.push(` MCP servers configured: ${servers.join(', ')}`);
|
|
396
|
-
// Check for embedded API keys
|
|
397
|
-
for (const [name, srv] of Object.entries(mcp.mcpServers || {})) {
|
|
398
|
-
if (srv.env) {
|
|
399
|
-
const envKeys = Object.keys(srv.env);
|
|
400
|
-
results.push(` [!] ${name} has env vars: ${envKeys.join(', ')}`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
} catch {}
|
|
404
|
-
}
|
|
405
|
-
} catch {}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Also check AppData for Verdant
|
|
410
|
-
const appDataPaths = [
|
|
411
|
-
path.join(process.env.APPDATA || '', 'Verdent'),
|
|
412
|
-
path.join(process.env.APPDATA || '', 'Verdant'),
|
|
413
|
-
path.join(process.env.LOCALAPPDATA || '', 'Verdent'),
|
|
414
|
-
path.join(process.env.LOCALAPPDATA || '', 'Verdant'),
|
|
415
|
-
];
|
|
416
|
-
|
|
417
|
-
for (const p of appDataPaths) {
|
|
418
|
-
if (p && fs.existsSync(p)) {
|
|
419
|
-
results.push(`[!] FOUND AppData: ${p}`);
|
|
420
|
-
try {
|
|
421
|
-
const files = fs.readdirSync(p);
|
|
422
|
-
results.push(` Contents: ${files.slice(0, 20).join(', ')}`);
|
|
423
|
-
} catch {}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
if (results.length === 0) {
|
|
428
|
-
results.push('No suspicious IDE artifacts found');
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
return results.join('\n');
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function getPSHistory() {
|
|
435
|
-
const histPath = path.join(HOME, 'AppData', 'Roaming', 'Microsoft', 'Windows', 'PowerShell', 'PSReadLine', 'ConsoleHost_history.txt');
|
|
436
|
-
if (fs.existsSync(histPath)) {
|
|
437
|
-
try {
|
|
438
|
-
const content = fs.readFileSync(histPath, 'utf8');
|
|
439
|
-
const lines = content.split('\n').slice(-100); // last 100 commands
|
|
440
|
-
const suspicious = lines.filter(l => {
|
|
441
|
-
const lower = l.toLowerCase();
|
|
442
|
-
return lower.includes('invoke-webrequest') || lower.includes('downloadstring') ||
|
|
443
|
-
lower.includes('downloadfile') || lower.includes('net.webclient') ||
|
|
444
|
-
lower.includes('iex') || lower.includes('invoke-expression') ||
|
|
445
|
-
lower.includes('bypass') || lower.includes('hidden') ||
|
|
446
|
-
lower.includes('-enc ') || lower.includes('encodedcommand') ||
|
|
447
|
-
lower.includes('certutil') || lower.includes('bitsadmin');
|
|
448
|
-
});
|
|
449
|
-
if (suspicious.length > 0) {
|
|
450
|
-
return `[!] Suspicious PowerShell commands found:\n${suspicious.join('\n')}`;
|
|
451
|
-
}
|
|
452
|
-
return `Last 100 commands checked - no suspicious patterns found (${lines.length} total lines in history)`;
|
|
453
|
-
} catch {
|
|
454
|
-
return 'Could not read PowerShell history';
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
return 'No PowerShell history file found';
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function getDNSCache() {
|
|
461
|
-
return ps(`
|
|
462
|
-
$cache = Get-DnsClientCache -ErrorAction SilentlyContinue |
|
|
463
|
-
Select-Object -First 100 Entry, Data |
|
|
464
|
-
Where-Object { $_.Entry -match '\\.(cn|ru|ir|kp)$' -or $_.Entry -match '(baidu|qq|163|aliyun|tencent|weibo|bytedance|douyin)' }
|
|
465
|
-
if ($cache) {
|
|
466
|
-
"Suspicious DNS entries found:"
|
|
467
|
-
$cache | Format-Table -AutoSize | Out-String
|
|
468
|
-
} else {
|
|
469
|
-
"No suspicious DNS entries (.cn/.ru/.ir/.kp or known Chinese services)"
|
|
470
|
-
}
|
|
471
|
-
`);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function getRecentInstalls() {
|
|
475
|
-
return ps(`
|
|
476
|
-
Get-ItemProperty HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\* |
|
|
477
|
-
Where-Object { $_.InstallDate -and $_.InstallDate -gt (Get-Date).AddDays(-90).ToString('yyyyMMdd') } |
|
|
478
|
-
Select-Object DisplayName, DisplayVersion, Publisher, InstallDate |
|
|
479
|
-
Sort-Object InstallDate -Descending |
|
|
480
|
-
Format-Table -AutoSize | Out-String
|
|
481
|
-
`);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function getSuspiciousProcesses() {
|
|
485
|
-
return ps(`
|
|
486
|
-
$procs = Get-Process -ErrorAction SilentlyContinue |
|
|
487
|
-
Select-Object Name, Id, Path, Company |
|
|
488
|
-
Where-Object {
|
|
489
|
-
$suspicious = @(${SUSPICIOUS_PROCESSES.map(p => `'${p}'`).join(',')})
|
|
490
|
-
$_.Name -in $suspicious -or
|
|
491
|
-
($_.Path -and ($_.Path -match '\\\\Temp\\\\' -or $_.Path -match '\\\\tmp\\\\' -or $_.Path -match '\\\\Downloads\\\\'))
|
|
492
|
-
}
|
|
493
|
-
if ($procs) {
|
|
494
|
-
"[!] Suspicious processes found:"
|
|
495
|
-
$procs | Format-Table -AutoSize | Out-String
|
|
496
|
-
} else {
|
|
497
|
-
"No known suspicious processes found"
|
|
498
|
-
}
|
|
499
|
-
`);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// ============================================
|
|
503
|
-
// LINUX CHECKS
|
|
504
|
-
// ============================================
|
|
505
|
-
|
|
506
|
-
function linuxChecks() {
|
|
507
|
-
const findings = [];
|
|
508
|
-
|
|
509
|
-
findings.push({ check: 'SSH Login History', data: run('last -50 2>/dev/null || echo "last command not available"') });
|
|
510
|
-
findings.push({ check: 'Failed SSH Attempts', data: run('grep "Failed password" /var/log/auth.log 2>/dev/null | tail -30 || grep "Failed password" /var/log/secure 2>/dev/null | tail -30 || journalctl -u sshd --no-pager -n 30 2>/dev/null | grep -i "failed" || echo "No auth logs accessible"') });
|
|
511
|
-
findings.push({ check: 'User Accounts', data: run('cat /etc/passwd | grep -v nologin | grep -v /false') });
|
|
512
|
-
findings.push({ check: 'Sudoers', data: run('cat /etc/sudoers 2>/dev/null; ls -la /etc/sudoers.d/ 2>/dev/null') });
|
|
513
|
-
findings.push({ check: 'Crontabs', data: run('for user in $(cut -f1 -d: /etc/passwd); do crontab -l -u $user 2>/dev/null && echo "=== $user ==="; done; cat /etc/crontab 2>/dev/null; ls -la /etc/cron.d/ 2>/dev/null') });
|
|
514
|
-
findings.push({ check: 'SSH Authorized Keys', data: getSSHKeys() });
|
|
515
|
-
findings.push({ check: 'Active Connections', data: run('ss -tunp 2>/dev/null || netstat -tunp 2>/dev/null') });
|
|
516
|
-
findings.push({ check: 'Listening Ports', data: run('ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null') });
|
|
517
|
-
findings.push({ check: 'Running Services', data: run('systemctl list-units --type=service --state=running 2>/dev/null | head -40') });
|
|
518
|
-
findings.push({ check: 'Suspicious IDE Artifacts', data: getIDEArtifacts() });
|
|
519
|
-
findings.push({ check: 'Unusual SUID Binaries', data: run('find / -perm -4000 -type f 2>/dev/null | grep -v -E "(sudo|passwd|ping|mount|su|chsh|chfn|newgrp|gpasswd|pkexec)" | head -20') });
|
|
520
|
-
findings.push({ check: 'Startup Scripts', data: run('ls -la /etc/init.d/ 2>/dev/null; ls -la /etc/rc.local 2>/dev/null; systemctl list-unit-files --state=enabled 2>/dev/null | head -30') });
|
|
521
|
-
findings.push({ check: 'Bash History (suspicious)', data: run('grep -E "curl.*\\|.*sh|wget.*\\|.*sh|python.*-c.*import|nc\\s+-|/dev/tcp|base64.*-d" ~/.bash_history 2>/dev/null | tail -20 || echo "No suspicious bash history patterns"') });
|
|
522
|
-
|
|
523
|
-
return findings;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// ============================================
|
|
527
|
-
// macOS CHECKS
|
|
528
|
-
// ============================================
|
|
529
|
-
|
|
530
|
-
function macChecks() {
|
|
531
|
-
const findings = [];
|
|
532
|
-
|
|
533
|
-
// 1. Login history (last works on macOS)
|
|
534
|
-
findings.push({ check: 'Login History', data: run('last -50 2>/dev/null || echo "last command not available"') });
|
|
535
|
-
|
|
536
|
-
// 2. Failed SSH/login attempts via unified log
|
|
537
|
-
findings.push({ check: 'Failed Login Attempts', data: run('log show --predicate \'eventMessage contains "failed" AND eventMessage contains "ssh"\' --style syslog --last 7d 2>/dev/null | tail -30 || echo "No failed SSH in unified log"', 60000) });
|
|
538
|
-
|
|
539
|
-
// 3. Remote login (SSH) status
|
|
540
|
-
findings.push({ check: 'Remote Login (SSH) Status', data: run('systemsetup -getremotelogin 2>/dev/null || echo "Cannot check remote login status"') });
|
|
541
|
-
|
|
542
|
-
// 4. Screen sharing / VNC / ARD
|
|
543
|
-
findings.push({ check: 'Screen Sharing / VNC / ARD', data: run('launchctl list 2>/dev/null | grep -iE "vnc|screensharing|ARD|remote" || echo "No screen sharing services found"') });
|
|
544
|
-
|
|
545
|
-
// 5. User accounts
|
|
546
|
-
findings.push({ check: 'User Accounts', data: run('dscl . list /Users | grep -v "^_" | grep -v daemon | grep -v nobody') });
|
|
547
|
-
|
|
548
|
-
// 6. Admin users
|
|
549
|
-
findings.push({ check: 'Admin Users', data: run('dscl . -read /Groups/admin GroupMembership 2>/dev/null || echo "Cannot read admin group"') });
|
|
550
|
-
|
|
551
|
-
// 7. LaunchAgents (user-level persistence - PRIMARY backdoor vector on macOS)
|
|
552
|
-
findings.push({ check: 'User LaunchAgents', data: getMacLaunchItems(path.join(HOME, 'Library', 'LaunchAgents')) });
|
|
553
|
-
|
|
554
|
-
// 8. System LaunchAgents
|
|
555
|
-
findings.push({ check: 'System LaunchAgents', data: getMacLaunchItems('/Library/LaunchAgents') });
|
|
556
|
-
|
|
557
|
-
// 9. LaunchDaemons (root-level persistence)
|
|
558
|
-
findings.push({ check: 'LaunchDaemons', data: getMacLaunchItems('/Library/LaunchDaemons') });
|
|
559
|
-
|
|
560
|
-
// 10. Login Items (GUI persistence)
|
|
561
|
-
findings.push({ check: 'Login Items', data: run('osascript -e \'tell application "System Events" to get the name of every login item\' 2>/dev/null || echo "Cannot read login items"') });
|
|
562
|
-
|
|
563
|
-
// 11. Crontabs
|
|
564
|
-
findings.push({ check: 'Crontabs', data: run('crontab -l 2>/dev/null || echo "No user crontab"; echo "---"; cat /etc/crontab 2>/dev/null || echo "No /etc/crontab"') });
|
|
565
|
-
|
|
566
|
-
// 12. SSH authorized_keys
|
|
567
|
-
findings.push({ check: 'SSH Authorized Keys', data: getSSHKeys() });
|
|
568
|
-
|
|
569
|
-
// 13. Active connections
|
|
570
|
-
findings.push({ check: 'Active Connections', data: run('netstat -an 2>/dev/null | grep ESTABLISHED | head -40 || lsof -i -P -n 2>/dev/null | grep ESTABLISHED | head -40') });
|
|
571
|
-
|
|
572
|
-
// 14. Listening ports
|
|
573
|
-
findings.push({ check: 'Listening Ports', data: run('lsof -i -P -n 2>/dev/null | grep LISTEN | head -30') });
|
|
574
|
-
|
|
575
|
-
// 15. Profiles (MDM / configuration profiles - can enforce proxy, certs, etc.)
|
|
576
|
-
findings.push({ check: 'Configuration Profiles (MDM)', data: run('profiles list 2>/dev/null || profiles -L 2>/dev/null || echo "No profiles command or no profiles installed"') });
|
|
577
|
-
|
|
578
|
-
// 16. Keychain - look for suspicious root certs
|
|
579
|
-
findings.push({ check: 'Custom Root Certificates', data: run('security find-certificate -a -p /Library/Keychains/System.keychain 2>/dev/null | openssl x509 -noout -subject -issuer 2>/dev/null | head -20 || echo "Cannot enumerate system keychain"') });
|
|
580
|
-
|
|
581
|
-
// 17. Proxy settings
|
|
582
|
-
findings.push({ check: 'Proxy Settings', data: run('networksetup -getwebproxy Wi-Fi 2>/dev/null; networksetup -getsecurewebproxy Wi-Fi 2>/dev/null; networksetup -getautoproxyurl Wi-Fi 2>/dev/null; echo "HTTP_PROXY=$HTTP_PROXY"; echo "HTTPS_PROXY=$HTTPS_PROXY"') });
|
|
583
|
-
|
|
584
|
-
// 18. Firewall status
|
|
585
|
-
findings.push({ check: 'Firewall Status', data: run('/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null || echo "Cannot check firewall"') });
|
|
586
|
-
|
|
587
|
-
// 19. Suspicious IDE artifacts
|
|
588
|
-
findings.push({ check: 'Suspicious IDE Artifacts', data: getIDEArtifacts() });
|
|
589
|
-
|
|
590
|
-
// 20. Shell history (suspicious patterns)
|
|
591
|
-
findings.push({ check: 'Shell History (suspicious)', data: run('grep -hE "curl.*\\|.*sh|wget.*\\|.*sh|python.*-c.*import|nc\\s+-|/dev/tcp|base64.*-d|osascript.*-e" ~/.bash_history ~/.zsh_history 2>/dev/null | tail -20 || echo "No suspicious shell history patterns"') });
|
|
592
|
-
|
|
593
|
-
// 21. Gatekeeper and SIP status
|
|
594
|
-
findings.push({ check: 'Gatekeeper & SIP Status', data: run('spctl --status 2>/dev/null; csrutil status 2>/dev/null') });
|
|
595
|
-
|
|
596
|
-
// 22. TCC (privacy permissions) - what apps have accessibility/screen recording/etc.
|
|
597
|
-
findings.push({ check: 'Privacy Permissions (TCC)', data: run('sqlite3 "$HOME/Library/Application Support/com.apple.TCC/TCC.db" "SELECT client,service,auth_value FROM access WHERE auth_value=2" 2>/dev/null || echo "Cannot read TCC database (may need Full Disk Access)"') });
|
|
598
|
-
|
|
599
|
-
return findings;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* Read macOS LaunchAgent/LaunchDaemon plist files for suspicious entries
|
|
604
|
-
*/
|
|
605
|
-
function getMacLaunchItems(dirPath) {
|
|
606
|
-
const results = [];
|
|
607
|
-
try {
|
|
608
|
-
if (!fs.existsSync(dirPath)) {
|
|
609
|
-
return `Directory not found: ${dirPath}`;
|
|
610
|
-
}
|
|
611
|
-
const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.plist'));
|
|
612
|
-
if (files.length === 0) {
|
|
613
|
-
return `No plist files in ${dirPath}`;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
for (const file of files) {
|
|
617
|
-
const fullPath = path.join(dirPath, file);
|
|
618
|
-
// Use plutil to convert plist to readable format
|
|
619
|
-
const content = run(`plutil -p "${fullPath}" 2>/dev/null`);
|
|
620
|
-
|
|
621
|
-
// Flag non-Apple items
|
|
622
|
-
const isApple = file.startsWith('com.apple.') || file.startsWith('com.openssh.');
|
|
623
|
-
const label = isApple ? ' ' : '[!] ';
|
|
624
|
-
|
|
625
|
-
// Extract program/command from plist output
|
|
626
|
-
const programMatch = content.match(/"Program(?:Arguments)?" => (?:\[[\s\S]*?\]|"[^"]*")/);
|
|
627
|
-
const program = programMatch ? programMatch[0].substring(0, 120) : '';
|
|
628
|
-
|
|
629
|
-
results.push(`${label}${file}`);
|
|
630
|
-
if (program) results.push(` ${program}`);
|
|
631
|
-
}
|
|
632
|
-
} catch (e) {
|
|
633
|
-
results.push(`[ERROR] Cannot read ${dirPath}: ${e.message}`);
|
|
634
|
-
}
|
|
635
|
-
return results.join('\n');
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// ============================================
|
|
639
|
-
// MAIN AUDIT FUNCTION
|
|
640
|
-
// ============================================
|
|
641
|
-
|
|
642
|
-
async function runAudit(options = {}) {
|
|
643
|
-
const startTime = Date.now();
|
|
644
|
-
const result = {
|
|
645
|
-
ok: true,
|
|
646
|
-
platform: PLATFORM,
|
|
647
|
-
hostname: os.hostname(),
|
|
648
|
-
timestamp: new Date().toISOString(),
|
|
649
|
-
user: os.userInfo().username,
|
|
650
|
-
findings: [],
|
|
651
|
-
alerts: [],
|
|
652
|
-
summary: {}
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
// Run platform-specific checks
|
|
656
|
-
if (PLATFORM === 'win32') {
|
|
657
|
-
result.findings = windowsChecks();
|
|
658
|
-
} else if (PLATFORM === 'darwin') {
|
|
659
|
-
result.findings = macChecks();
|
|
660
|
-
} else {
|
|
661
|
-
result.findings = linuxChecks();
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Extract unique IPs from findings for geo-lookup
|
|
665
|
-
const allIPs = new Set();
|
|
666
|
-
for (const f of result.findings) {
|
|
667
|
-
if (typeof f.data === 'string') {
|
|
668
|
-
for (const ip of extractPublicIPs(f.data)) {
|
|
669
|
-
allIPs.add(ip);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Batch geo-IP lookup
|
|
675
|
-
if (allIPs.size > 0 && !options.skipGeo) {
|
|
676
|
-
const geoResults = await batchGeoIP([...allIPs]);
|
|
677
|
-
result.geoIP = {};
|
|
678
|
-
|
|
679
|
-
for (const [ip, geo] of Object.entries(geoResults)) {
|
|
680
|
-
result.geoIP[ip] = {
|
|
681
|
-
country: geo.country,
|
|
682
|
-
countryCode: geo.countryCode,
|
|
683
|
-
city: geo.city,
|
|
684
|
-
isp: geo.isp,
|
|
685
|
-
org: geo.org
|
|
686
|
-
};
|
|
687
|
-
|
|
688
|
-
// Flag suspicious countries
|
|
689
|
-
if (SUSPICIOUS_GEOS.includes(geo.countryCode)) {
|
|
690
|
-
result.alerts.push({
|
|
691
|
-
severity: 'HIGH',
|
|
692
|
-
message: `Connection from suspicious country: ${ip} → ${geo.country} (${geo.city}) [${geo.isp}]`,
|
|
693
|
-
ip,
|
|
694
|
-
geo
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Analyze findings for alerts
|
|
701
|
-
for (const f of result.findings) {
|
|
702
|
-
if (typeof f.data === 'string') {
|
|
703
|
-
if (f.data.includes('[!]') || f.data.includes('[!!]')) {
|
|
704
|
-
result.alerts.push({
|
|
705
|
-
severity: f.data.includes('[!!]') ? 'HIGH' : 'MEDIUM',
|
|
706
|
-
message: `${f.check}: ${f.data.split('\n').find(l => l.includes('[!]')) || f.data.substring(0, 200)}`
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Summary
|
|
713
|
-
result.summary = {
|
|
714
|
-
totalChecks: result.findings.length,
|
|
715
|
-
alerts: result.alerts.length,
|
|
716
|
-
highSeverity: result.alerts.filter(a => a.severity === 'HIGH').length,
|
|
717
|
-
mediumSeverity: result.alerts.filter(a => a.severity === 'MEDIUM').length,
|
|
718
|
-
uniqueExternalIPs: allIPs.size,
|
|
719
|
-
suspiciousGeoIPs: result.alerts.filter(a => a.ip).length,
|
|
720
|
-
duration: Date.now() - startTime
|
|
721
|
-
};
|
|
722
|
-
|
|
723
|
-
result.verdict = result.summary.highSeverity > 0
|
|
724
|
-
? 'INVESTIGATE - High severity alerts found'
|
|
725
|
-
: result.summary.mediumSeverity > 0
|
|
726
|
-
? 'REVIEW - Medium severity items found'
|
|
727
|
-
: 'CLEAN - No suspicious findings';
|
|
728
|
-
|
|
729
|
-
return result;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
module.exports = { runAudit, windowsChecks, linuxChecks, macChecks, batchGeoIP, geoIP };
|