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.

@@ -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 };