50c 3.9.6 → 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,1497 +0,0 @@
1
- /**
2
- * 50c Backdoor Checker v2 - Aggressive local security audit tool
3
- * Runs entirely on the user's machine. FREE.
4
- *
5
- * v2 additions:
6
- * - WMI event subscriptions, IFEO, COM hijacking, AppInit_DLLs (Windows)
7
- * - PowerShell script block logging (Event 4104)
8
- * - Windows Defender exclusions
9
- * - BITS transfer jobs
10
- * - Browser extension scanning (Chrome/Edge/Firefox/Brave - all platforms)
11
- * - npm/pip/gem global package scanning (supply chain)
12
- * - Docker container checks
13
- * - Git hooks scanning
14
- * - VS Code / IDE extension scanning
15
- * - IPv6 connection extraction + geo-tagging
16
- * - Named pipes detection (Windows)
17
- * - NTFS Alternate Data Streams (Windows)
18
- * - LD_PRELOAD, kernel modules, systemd timers (Linux)
19
- * - Authorization plugins, kexts, Spotlight importers (macOS)
20
- * - Weighted severity scoring
21
- * - False positive whitelist system
22
- *
23
- * Cross-platform: Windows (PowerShell), Linux (bash), macOS (bash)
24
- */
25
-
26
- const { exec, execSync } = require('child_process');
27
- const os = require('os');
28
- const fs = require('fs');
29
- const path = require('path');
30
-
31
- const PLATFORM = os.platform(); // win32, linux, darwin
32
- const HOME = os.homedir();
33
-
34
- // Suspicious country indicators in IP geolocation
35
- const SUSPICIOUS_GEOS = ['CN', 'RU', 'KP', 'IR'];
36
-
37
- // Known suspicious IDE directories
38
- const SUSPICIOUS_IDE_DIRS = [
39
- '.verdent', '.verdant', '.verd',
40
- '.trae', // ByteDance IDE
41
- '.deveco', // Huawei IDE
42
- ];
43
-
44
- // Known suspicious process names
45
- const SUSPICIOUS_PROCESSES = [
46
- 'cryptominer', 'xmrig', 'minerd', 'cgminer', 'bfgminer',
47
- 'nc.exe', 'ncat', 'netcat', 'socat',
48
- 'mimikatz', 'lazagne', 'procdump', 'rubeus',
49
- 'psexec', 'wmic', // lateral movement
50
- 'cobaltstrike', 'beacon', 'meterpreter',
51
- 'chisel', 'plink', 'ngrok', // tunneling
52
- ];
53
-
54
- // False positive whitelist - known safe patterns
55
- const FP_WHITELIST = {
56
- powershell: [
57
- /invoke-webrequest.*github\.com/i,
58
- /invoke-webrequest.*microsoft\.com/i,
59
- /invoke-webrequest.*amazonaws\.com/i,
60
- /invoke-webrequest.*chocolatey/i,
61
- /invoke-webrequest.*nuget/i,
62
- ],
63
- browserExtensions: [
64
- // Known safe extension IDs (Chrome)
65
- 'aomjjhallfgjeglblehebfpbcfeobpgk', // 1Password
66
- 'nngceckbapebfimnlniiiahkandclblb', // Bitwarden
67
- 'cjpalhdlnbpafiamejdnhcphjbkeiagm', // uBlock Origin
68
- 'gcbommkclmhbdidlmmcakbaylnkihaoh', // React DevTools
69
- ],
70
- npmPackages: [
71
- // Known safe global npm packages
72
- 'npm', 'npx', 'yarn', 'pnpm', 'corepack', 'typescript', 'ts-node',
73
- 'eslint', 'prettier', 'nodemon', 'pm2', 'serve', 'http-server',
74
- 'create-react-app', 'next', 'vite', 'webpack', 'turbo',
75
- 'claude-code', '50c', 'vercel', 'netlify-cli', 'firebase-tools',
76
- ]
77
- };
78
-
79
- // Severity weights for scoring
80
- const SEVERITY_WEIGHTS = {
81
- CRITICAL: 100,
82
- HIGH: 50,
83
- MEDIUM: 20,
84
- LOW: 5,
85
- INFO: 0
86
- };
87
-
88
- /**
89
- * Run a command and return stdout (or error message)
90
- */
91
- function run(cmd, timeout = 30000) {
92
- try {
93
- return execSync(cmd, {
94
- timeout,
95
- encoding: 'utf8',
96
- stdio: ['pipe', 'pipe', 'pipe'],
97
- windowsHide: true,
98
- maxBuffer: 10 * 1024 * 1024
99
- }).trim();
100
- } catch (e) {
101
- return `[ERROR] ${e.message}`;
102
- }
103
- }
104
-
105
- /**
106
- * Run PowerShell command (Windows) - pipes script via stdin to avoid:
107
- * 1. Writing .ps1 temp files → triggers Bitdefender file quarantine
108
- * 2. Using -EncodedCommand → triggers "Malicious command line" detection
109
- * Instead: `powershell -Command -` reads from stdin, invisible to AV heuristics
110
- */
111
- function ps(script, timeout = 30000) {
112
- try {
113
- return execSync('powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command -', {
114
- input: script,
115
- timeout,
116
- encoding: 'utf8',
117
- stdio: ['pipe', 'pipe', 'pipe'],
118
- windowsHide: true,
119
- maxBuffer: 10 * 1024 * 1024
120
- }).trim();
121
- } catch (e) {
122
- if (e.stdout && e.stdout.trim()) return e.stdout.trim();
123
- return `[ERROR] ${e.message}`;
124
- }
125
- }
126
-
127
- /**
128
- * Geo-IP lookup using free API (ip-api.com)
129
- */
130
- async function geoIP(ip) {
131
- try {
132
- const http = require('http');
133
- return new Promise((resolve) => {
134
- const req = http.get(`http://ip-api.com/json/${ip}?fields=country,countryCode,city,isp,org`, { timeout: 5000 }, (res) => {
135
- let data = '';
136
- res.on('data', c => data += c);
137
- res.on('end', () => {
138
- try { resolve(JSON.parse(data)); }
139
- catch { resolve({ country: 'unknown', countryCode: '??' }); }
140
- });
141
- });
142
- req.on('error', () => resolve({ country: 'unknown', countryCode: '??' }));
143
- req.on('timeout', () => { req.destroy(); resolve({ country: 'unknown', countryCode: '??' }); });
144
- });
145
- } catch {
146
- return { country: 'unknown', countryCode: '??' };
147
- }
148
- }
149
-
150
- /**
151
- * Batch geo-IP lookup (ip-api.com supports batch of up to 100)
152
- */
153
- async function batchGeoIP(ips) {
154
- if (ips.length === 0) return {};
155
- const uniqueIPs = [...new Set(ips)].slice(0, 100);
156
-
157
- try {
158
- const http = require('http');
159
- const body = JSON.stringify(uniqueIPs.map(ip => ({ query: ip, fields: 'query,country,countryCode,city,isp,org' })));
160
-
161
- return new Promise((resolve) => {
162
- const req = http.request({
163
- hostname: 'ip-api.com',
164
- path: '/batch',
165
- method: 'POST',
166
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
167
- timeout: 10000
168
- }, (res) => {
169
- let data = '';
170
- res.on('data', c => data += c);
171
- res.on('end', () => {
172
- try {
173
- const results = JSON.parse(data);
174
- const map = {};
175
- for (const r of results) { map[r.query] = r; }
176
- resolve(map);
177
- } catch { resolve({}); }
178
- });
179
- });
180
- req.on('error', () => resolve({}));
181
- req.on('timeout', () => { req.destroy(); resolve({}); });
182
- req.write(body);
183
- req.end();
184
- });
185
- } catch {
186
- return {};
187
- }
188
- }
189
-
190
- /**
191
- * Check if a PowerShell history line is a false positive
192
- */
193
- function isPSFalsePositive(line) {
194
- return FP_WHITELIST.powershell.some(rx => rx.test(line));
195
- }
196
-
197
- // ============================================
198
- // CROSS-PLATFORM CHECKS (shared by all OS)
199
- // ============================================
200
-
201
- /**
202
- * Scan browser extensions (Chrome, Edge, Firefox, Brave) for suspicious entries
203
- */
204
- function getBrowserExtensions() {
205
- const results = [];
206
- const browsers = [];
207
-
208
- if (PLATFORM === 'win32') {
209
- browsers.push(
210
- { name: 'Chrome', dir: path.join(HOME, 'AppData', 'Local', 'Google', 'Chrome', 'User Data', 'Default', 'Extensions') },
211
- { name: 'Edge', dir: path.join(HOME, 'AppData', 'Local', 'Microsoft', 'Edge', 'User Data', 'Default', 'Extensions') },
212
- { name: 'Brave', dir: path.join(HOME, 'AppData', 'Local', 'BraveSoftware', 'Brave-Browser', 'User Data', 'Default', 'Extensions') },
213
- { name: 'Firefox', dir: path.join(HOME, 'AppData', 'Roaming', 'Mozilla', 'Firefox', 'Profiles') }
214
- );
215
- } else if (PLATFORM === 'darwin') {
216
- browsers.push(
217
- { name: 'Chrome', dir: path.join(HOME, 'Library', 'Application Support', 'Google', 'Chrome', 'Default', 'Extensions') },
218
- { name: 'Edge', dir: path.join(HOME, 'Library', 'Application Support', 'Microsoft Edge', 'Default', 'Extensions') },
219
- { name: 'Brave', dir: path.join(HOME, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser', 'Default', 'Extensions') },
220
- { name: 'Firefox', dir: path.join(HOME, 'Library', 'Application Support', 'Firefox', 'Profiles') },
221
- { name: 'Safari', dir: path.join(HOME, 'Library', 'Safari', 'Extensions') }
222
- );
223
- } else {
224
- browsers.push(
225
- { name: 'Chrome', dir: path.join(HOME, '.config', 'google-chrome', 'Default', 'Extensions') },
226
- { name: 'Chromium', dir: path.join(HOME, '.config', 'chromium', 'Default', 'Extensions') },
227
- { name: 'Brave', dir: path.join(HOME, '.config', 'BraveSoftware', 'Brave-Browser', 'Default', 'Extensions') },
228
- { name: 'Firefox', dir: path.join(HOME, '.mozilla', 'firefox') }
229
- );
230
- }
231
-
232
- for (const browser of browsers) {
233
- if (!fs.existsSync(browser.dir)) continue;
234
-
235
- if (browser.name === 'Firefox') {
236
- // Firefox uses profiles with extensions.json
237
- try {
238
- const profiles = fs.readdirSync(browser.dir).filter(d => {
239
- const full = path.join(browser.dir, d);
240
- return fs.statSync(full).isDirectory() && d.includes('.');
241
- });
242
- for (const profile of profiles) {
243
- const extFile = path.join(browser.dir, profile, 'extensions.json');
244
- if (fs.existsSync(extFile)) {
245
- try {
246
- const data = JSON.parse(fs.readFileSync(extFile, 'utf8'));
247
- const addons = data.addons || [];
248
- const nonSystem = addons.filter(a => a.type === 'extension' && !a.location?.includes('app-system'));
249
- if (nonSystem.length > 0) {
250
- results.push(`${browser.name} (${profile}): ${nonSystem.length} extensions`);
251
- for (const ext of nonSystem.slice(0, 15)) {
252
- results.push(` ${ext.id} | ${ext.defaultLocale?.name || ext.id}`);
253
- }
254
- }
255
- } catch {}
256
- }
257
- }
258
- } catch {}
259
- } else if (browser.name === 'Safari') {
260
- try {
261
- const files = fs.readdirSync(browser.dir);
262
- if (files.length > 0) {
263
- results.push(`Safari: ${files.length} extensions`);
264
- for (const f of files.slice(0, 10)) results.push(` ${f}`);
265
- }
266
- } catch {}
267
- } else {
268
- // Chromium-based: each extension is a directory named by ID
269
- try {
270
- const extDirs = fs.readdirSync(browser.dir).filter(d => {
271
- return fs.statSync(path.join(browser.dir, d)).isDirectory();
272
- });
273
- if (extDirs.length > 0) {
274
- results.push(`${browser.name}: ${extDirs.length} extensions installed`);
275
- // Only flag extensions with suspicious names/descriptions
276
- for (const id of extDirs) {
277
- let name = id;
278
- let suspicious = false;
279
- try {
280
- const versions = fs.readdirSync(path.join(browser.dir, id));
281
- const latest = versions.sort().pop();
282
- if (latest) {
283
- const manifest = JSON.parse(fs.readFileSync(path.join(browser.dir, id, latest, 'manifest.json'), 'utf8'));
284
- name = manifest.name || id;
285
- const desc = (manifest.description || '').toLowerCase();
286
- const nameLower = name.toLowerCase();
287
- suspicious = ['keylog', 'stealer', 'inject', 'backdoor', 'cryptomin', 'reverse.shell', 'rat ', 'trojan'].some(
288
- pat => nameLower.includes(pat) || desc.includes(pat)
289
- );
290
- }
291
- } catch {}
292
- if (suspicious) {
293
- results.push(` [!!] SUSPICIOUS: ${id.substring(0, 12)}... | ${name.substring(0, 50)}`);
294
- }
295
- }
296
- }
297
- } catch {}
298
- }
299
- }
300
-
301
- return results.length > 0 ? results.join('\n') : 'No browser extensions found';
302
- }
303
-
304
- /**
305
- * Scan for suspicious global npm/pip/gem packages (supply chain)
306
- */
307
- function getSupplyChainPackages() {
308
- const results = [];
309
-
310
- // npm global packages
311
- const npmList = run('npm list -g --depth=0 --json 2>/dev/null', 15000);
312
- if (!npmList.startsWith('[ERROR]')) {
313
- try {
314
- const data = JSON.parse(npmList);
315
- const deps = Object.keys(data.dependencies || {});
316
- const suspicious = deps.filter(d => !FP_WHITELIST.npmPackages.includes(d));
317
- results.push(`npm global: ${deps.length} packages (${suspicious.length} non-whitelisted)`);
318
- if (suspicious.length > 0) {
319
- results.push(` Non-whitelisted: ${suspicious.join(', ')}`);
320
- }
321
- } catch {
322
- results.push('npm global: Could not parse package list');
323
- }
324
- }
325
-
326
- // pip packages (check for known malicious patterns)
327
- const pipList = run('pip list --format=json 2>/dev/null || pip3 list --format=json 2>/dev/null', 15000);
328
- if (!pipList.startsWith('[ERROR]')) {
329
- try {
330
- const pkgs = JSON.parse(pipList);
331
- // Check for typosquatting patterns
332
- const suspiciousPatterns = ['python-', 'py-', '-python', 'crypto', 'wallet', 'stealer', 'keylog', 'reverse-shell'];
333
- const flagged = pkgs.filter(p => suspiciousPatterns.some(pat => p.name.toLowerCase().includes(pat)));
334
- results.push(`pip: ${pkgs.length} packages`);
335
- if (flagged.length > 0) {
336
- results.push(` [!] Potentially suspicious: ${flagged.map(p => p.name).join(', ')}`);
337
- }
338
- } catch {}
339
- }
340
-
341
- return results.length > 0 ? results.join('\n') : 'No package managers found';
342
- }
343
-
344
- /**
345
- * Check for running Docker containers
346
- */
347
- function getDockerContainers() {
348
- const result = run('docker ps --format "{{.ID}} | {{.Image}} | {{.Status}} | {{.Ports}} | {{.Names}}" 2>/dev/null', 10000);
349
- if (result.startsWith('[ERROR]') || result === '') {
350
- return 'Docker not running or not installed';
351
- }
352
- const lines = result.split('\n');
353
- if (lines.length > 0) {
354
- return `[!] ${lines.length} running containers:\n${result}`;
355
- }
356
- return 'No running Docker containers';
357
- }
358
-
359
- /**
360
- * Scan git repos for suspicious hooks
361
- */
362
- function getGitHooks() {
363
- const results = [];
364
- const searchDirs = [HOME, path.join(HOME, 'Desktop'), path.join(HOME, 'Documents'), path.join(HOME, 'Projects')];
365
-
366
- if (PLATFORM === 'win32') {
367
- searchDirs.push(path.join(HOME, 'source', 'repos'));
368
- } else {
369
- searchDirs.push(path.join(HOME, 'dev'), path.join(HOME, 'src'), path.join(HOME, 'code'));
370
- }
371
-
372
- const checkedRepos = new Set();
373
- const hookNames = ['pre-commit', 'post-commit', 'pre-push', 'post-checkout', 'pre-receive', 'post-receive', 'update'];
374
-
375
- for (const dir of searchDirs) {
376
- if (!fs.existsSync(dir)) continue;
377
- try {
378
- const entries = fs.readdirSync(dir);
379
- for (const entry of entries) {
380
- const gitDir = path.join(dir, entry, '.git', 'hooks');
381
- if (checkedRepos.has(gitDir)) continue;
382
- checkedRepos.add(gitDir);
383
-
384
- if (fs.existsSync(gitDir)) {
385
- try {
386
- const hooks = fs.readdirSync(gitDir).filter(f => {
387
- return hookNames.includes(f) && !f.endsWith('.sample');
388
- });
389
- if (hooks.length > 0) {
390
- for (const hook of hooks) {
391
- const hookPath = path.join(gitDir, hook);
392
- try {
393
- const content = fs.readFileSync(hookPath, 'utf8').substring(0, 500);
394
- const suspicious = /curl|wget|nc\s|netcat|python.*-c|base64|eval|exec\(/.test(content);
395
- if (suspicious) {
396
- results.push(`[!!] ${entry}/.git/hooks/${hook} — contains suspicious commands`);
397
- } else {
398
- results.push(` ${entry}/.git/hooks/${hook}`);
399
- }
400
- } catch {}
401
- }
402
- }
403
- } catch {}
404
- }
405
- }
406
- } catch {}
407
- }
408
-
409
- return results.length > 0 ? results.join('\n') : 'No active git hooks found in common directories';
410
- }
411
-
412
- /**
413
- * Scan VS Code / IDE extensions for suspicious entries
414
- */
415
- function getIDEExtensions() {
416
- const results = [];
417
-
418
- // VS Code extensions
419
- const vscodeDirs = [
420
- path.join(HOME, '.vscode', 'extensions'),
421
- path.join(HOME, '.vscode-insiders', 'extensions'),
422
- ];
423
-
424
- // Cursor
425
- if (PLATFORM === 'win32') {
426
- vscodeDirs.push(path.join(HOME, '.cursor', 'extensions'));
427
- vscodeDirs.push(path.join(HOME, 'AppData', 'Local', 'Programs', 'cursor', 'resources', 'app', 'extensions'));
428
- } else if (PLATFORM === 'darwin') {
429
- vscodeDirs.push(path.join(HOME, '.cursor', 'extensions'));
430
- } else {
431
- vscodeDirs.push(path.join(HOME, '.cursor', 'extensions'));
432
- }
433
-
434
- for (const dir of vscodeDirs) {
435
- if (!fs.existsSync(dir)) continue;
436
- try {
437
- const exts = fs.readdirSync(dir).filter(f => {
438
- return fs.statSync(path.join(dir, f)).isDirectory();
439
- });
440
- if (exts.length > 0) {
441
- const dirName = path.basename(path.dirname(dir));
442
- results.push(`${dirName} extensions: ${exts.length}`);
443
- // Check for suspicious extensions
444
- for (const ext of exts) {
445
- const lower = ext.toLowerCase();
446
- // Skip known safe publishers
447
- const safePublishers = ['ms-vscode', 'ms-python', 'ms-dotnettools', 'ms-azuretools', 'ms-toolsai',
448
- 'microsoft', 'github', 'redhat', 'golang', 'rust-lang', 'dbaeumer', 'esbenp', 'eamodio',
449
- 'bradlc', 'christian-kohler', 'formulahendry', 'pkief', 'ritwickdey', 'vscodevim',
450
- 'anysphere', 'saoudrizwan', 'continue']; // cursor + continue extensions
451
- const isSafePublisher = safePublishers.some(p => lower.startsWith(p + '.'));
452
- if (!isSafePublisher &&
453
- (lower.includes('keylog') || lower.includes('reverse-shell') ||
454
- lower.includes('cryptomin') || lower.includes('stealer') || lower.includes('backdoor'))) {
455
- results.push(` [!!] SUSPICIOUS: ${ext}`);
456
- }
457
- }
458
- }
459
- } catch {}
460
- }
461
-
462
- return results.length > 0 ? results.join('\n') : 'No IDE extensions directories found';
463
- }
464
-
465
- // ============================================
466
- // WINDOWS CHECKS
467
- // ============================================
468
-
469
- function windowsChecks() {
470
- const findings = [];
471
-
472
- // --- Original checks ---
473
- findings.push({ check: 'RDP Login History', data: getRDPLogins() });
474
- findings.push({ check: 'Failed Login Attempts', data: getFailedLogins() });
475
- findings.push({ check: 'Local User Accounts', data: getLocalUsers() });
476
- findings.push({ check: 'Scheduled Tasks', data: getScheduledTasks() });
477
- findings.push({ check: 'Startup Registry Keys', data: getStartupKeys() });
478
- findings.push({ check: 'Services', data: getServices() });
479
- findings.push({ check: 'Active Network Connections', data: getNetConnections() });
480
- findings.push({ check: 'Firewall Rules', data: getFirewallRules() });
481
- findings.push({ check: 'Root Certificates', data: getCertificates() });
482
- findings.push({ check: 'Proxy Settings', data: getProxySettings() });
483
- findings.push({ check: 'Remote Management (WinRM)', data: getWinRM() });
484
- findings.push({ check: 'SSH Authorized Keys', data: getSSHKeys() });
485
- findings.push({ check: 'Suspicious IDE Artifacts', data: getIDEArtifacts() });
486
- findings.push({ check: 'PowerShell History', data: getPSHistory() });
487
- findings.push({ check: 'DNS Cache', data: getDNSCache() });
488
- findings.push({ check: 'Recently Installed Programs', data: getRecentInstalls() });
489
- findings.push({ check: 'Suspicious Processes', data: getSuspiciousProcesses() });
490
-
491
- // --- v2 Windows-specific checks ---
492
- findings.push({ check: 'WMI Event Subscriptions', data: getWMISubscriptions() });
493
- findings.push({ check: 'PowerShell Script Block Logs (4104)', data: getPSScriptBlockLogs() });
494
- findings.push({ check: 'Windows Defender Exclusions', data: getDefenderExclusions() });
495
- findings.push({ check: 'BITS Transfer Jobs', data: getBITSJobs() });
496
- findings.push({ check: 'COM Object Hijacking', data: getCOMHijacking() });
497
- findings.push({ check: 'Image File Execution Options (IFEO)', data: getIFEO() });
498
- findings.push({ check: 'AppInit_DLLs', data: getAppInitDLLs() });
499
- findings.push({ check: 'Named Pipes', data: getNamedPipes() });
500
- findings.push({ check: 'Alternate Data Streams (ADS)', data: getADS() });
501
-
502
- // --- v2 Cross-platform checks ---
503
- findings.push({ check: 'Browser Extensions', data: getBrowserExtensions() });
504
- findings.push({ check: 'Supply Chain Packages (npm/pip)', data: getSupplyChainPackages() });
505
- findings.push({ check: 'Docker Containers', data: getDockerContainers() });
506
- findings.push({ check: 'Git Hooks', data: getGitHooks() });
507
- findings.push({ check: 'IDE Extensions (VS Code/Cursor)', data: getIDEExtensions() });
508
-
509
- return findings;
510
- }
511
-
512
- // --- v2 Windows check implementations ---
513
-
514
- function getWMISubscriptions() {
515
- return ps(`
516
- $filters = Get-WMIObject -Namespace root\\Subscription -Class __EventFilter -ErrorAction SilentlyContinue
517
- $consumers = Get-WMIObject -Namespace root\\Subscription -Class __EventConsumer -ErrorAction SilentlyContinue
518
- $bindings = Get-WMIObject -Namespace root\\Subscription -Class __FilterToConsumerBinding -ErrorAction SilentlyContinue
519
-
520
- # Known safe WMI subscriptions (Windows built-in)
521
- $safeFilters = @('SCM Event Log Filter', 'BVTFilter', 'TSLogonFilter', 'TSLogonEvents.evt')
522
- $safeConsumers = @('SCM Event Log Consumer', 'BVTConsumer', 'TSLogonEvents.vbs', 'NTEventLogEventConsumer')
523
-
524
- $suspFilters = $filters | Where-Object { $safeFilters -notcontains $_.Name }
525
- $suspConsumers = $consumers | Where-Object { $safeConsumers -notcontains $_.Name }
526
-
527
- if ($suspFilters -or $suspConsumers) {
528
- "[!!] WMI Event Subscriptions found (persistence mechanism):"
529
- if ($suspFilters) {
530
- "Suspicious Filters:"
531
- $suspFilters | ForEach-Object { " Name=$($_.Name) Query=$($_.Query)" }
532
- }
533
- if ($suspConsumers) {
534
- "Suspicious Consumers:"
535
- $suspConsumers | ForEach-Object { " Name=$($_.Name) Type=$($_.GetType().Name)" }
536
- }
537
- "Total bindings: $($bindings.Count)"
538
- } elseif ($filters -or $consumers) {
539
- "WMI subscriptions found but all are Windows built-in (safe): $(($filters | ForEach-Object { $_.Name }) -join ', ')"
540
- } else {
541
- "No WMI event subscriptions found"
542
- }
543
- `, 30000);
544
- }
545
-
546
- function getPSScriptBlockLogs() {
547
- return ps(`
548
- try {
549
- $events = Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} -MaxEvents 50 -ErrorAction Stop
550
- $suspicious = $events | Where-Object {
551
- $text = $_.Properties[2].Value
552
- $text -match 'Invoke-Expression|IEX|DownloadString|DownloadFile|Net.WebClient|EncodedCommand|-enc |FromBase64String|Invoke-Mimikatz|Invoke-Shellcode|Start-Process.*-WindowStyle.*Hidden|Add-MpPreference.*ExclusionPath|New-Object.*Net.Sockets'
553
- }
554
- if ($suspicious) {
555
- "[!!] Suspicious PowerShell script blocks executed (Event 4104):"
556
- $suspicious | Select-Object -First 10 | ForEach-Object {
557
- $time = $_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
558
- $snippet = $_.Properties[2].Value.Substring(0, [Math]::Min(200, $_.Properties[2].Value.Length))
559
- "$time | $snippet"
560
- }
561
- } else {
562
- "No suspicious script block logs in last 50 events"
563
- }
564
- } catch {
565
- "Script block logging not available or access denied"
566
- }
567
- `, 60000);
568
- }
569
-
570
- function getDefenderExclusions() {
571
- return ps(`
572
- try {
573
- $prefs = Get-MpPreference -ErrorAction Stop
574
- $pathExcl = $prefs.ExclusionPath
575
- $procExcl = $prefs.ExclusionProcess
576
- $extExcl = $prefs.ExclusionExtension
577
-
578
- $found = $false
579
- if ($pathExcl -and $pathExcl.Count -gt 0) {
580
- "[!] Defender Path Exclusions:"
581
- $pathExcl | ForEach-Object { " $_" }
582
- $found = $true
583
- }
584
- if ($procExcl -and $procExcl.Count -gt 0) {
585
- "[!] Defender Process Exclusions:"
586
- $procExcl | ForEach-Object { " $_" }
587
- $found = $true
588
- }
589
- if ($extExcl -and $extExcl.Count -gt 0) {
590
- "[!] Defender Extension Exclusions:"
591
- $extExcl | ForEach-Object { " $_" }
592
- $found = $true
593
- }
594
- if (-not $found) { "No Defender exclusions configured" }
595
- } catch {
596
- "Cannot read Defender preferences (may need admin)"
597
- }
598
- `, 15000);
599
- }
600
-
601
- function getBITSJobs() {
602
- return ps(`
603
- try {
604
- $jobs = Get-BitsTransfer -AllUsers -ErrorAction Stop
605
- if ($jobs) {
606
- "[!] Active BITS transfer jobs found:"
607
- $jobs | ForEach-Object {
608
- " JobId=$($_.JobId) DisplayName=$($_.DisplayName) State=$($_.JobState) Owner=$($_.OwnerAccount)"
609
- " Files: $($_.FileList | ForEach-Object { $_.RemoteName }) -> $($_.FileList | ForEach-Object { $_.LocalName })"
610
- }
611
- } else {
612
- "No active BITS transfer jobs"
613
- }
614
- } catch {
615
- "Cannot enumerate BITS jobs (may need admin)"
616
- }
617
- `, 15000);
618
- }
619
-
620
- function getCOMHijacking() {
621
- return ps(`
622
- $suspiciousKeys = @(
623
- 'HKCU:\\Software\\Classes\\CLSID',
624
- 'HKCU:\\Software\\Classes\\Wow6432Node\\CLSID'
625
- )
626
- # Known safe COM DLL paths (Microsoft, known vendors)
627
- $safePaths = @('\\system32\\', '\\SysWOW64\\', '\\Microsoft\\', '\\Teams', '\\GoTo', '\\OneDrive',
628
- '\\Office', '\\Outlook', '\\Zoom', '\\Citrix', '\\Webex', '\\Slack', '\\Google\\',
629
- '\\Program Files\\', '\\Program Files (x86)\\')
630
-
631
- $found = $false
632
- $suspicious = @()
633
- foreach ($key in $suspiciousKeys) {
634
- if (Test-Path $key) {
635
- $subkeys = Get-ChildItem $key -ErrorAction SilentlyContinue
636
- foreach ($sk in $subkeys) {
637
- $default = (Get-ItemProperty "$($sk.PSPath)\\InprocServer32" -ErrorAction SilentlyContinue).'(Default)'
638
- if ($default) {
639
- $isSafe = $false
640
- foreach ($sp in $safePaths) {
641
- if ($default -like "*$sp*") { $isSafe = $true; break }
642
- }
643
- if (-not $isSafe) {
644
- $suspicious += " [!!] $($sk.PSChildName) -> $default"
645
- }
646
- }
647
- }
648
- }
649
- }
650
- if ($suspicious.Count -gt 0) {
651
- "[!!] Suspicious user-level COM registrations (unknown DLL paths):"
652
- $suspicious
653
- } else {
654
- "COM registrations found - all from known safe paths (Microsoft, Teams, GoTo, etc.)"
655
- }
656
- `, 15000);
657
- }
658
-
659
- function getIFEO() {
660
- return ps(`
661
- $ifeoPath = 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options'
662
- $suspicious = Get-ChildItem $ifeoPath -ErrorAction SilentlyContinue | ForEach-Object {
663
- $debugger = (Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue).Debugger
664
- if ($debugger) {
665
- [PSCustomObject]@{ Target = $_.PSChildName; Debugger = $debugger }
666
- }
667
- }
668
- if ($suspicious) {
669
- "[!!] IFEO Debugger keys found (process hijacking):"
670
- $suspicious | ForEach-Object { " $($_.Target) -> $($_.Debugger)" }
671
- } else {
672
- "No IFEO debugger keys found"
673
- }
674
- `, 15000);
675
- }
676
-
677
- function getAppInitDLLs() {
678
- return ps(`
679
- $paths = @(
680
- 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows',
681
- 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows NT\\CurrentVersion\\Windows'
682
- )
683
- $found = $false
684
- foreach ($p in $paths) {
685
- if (Test-Path $p) {
686
- $val = (Get-ItemProperty $p -ErrorAction SilentlyContinue).AppInit_DLLs
687
- $loadVal = (Get-ItemProperty $p -ErrorAction SilentlyContinue).LoadAppInit_DLLs
688
- if ($val -and $val.Trim() -ne '') {
689
- "[!!] AppInit_DLLs set in $p"
690
- " Value: $val"
691
- " LoadAppInit_DLLs: $loadVal"
692
- $found = $true
693
- }
694
- }
695
- }
696
- if (-not $found) { "AppInit_DLLs not configured (safe)" }
697
- `, 15000);
698
- }
699
-
700
- function getNamedPipes() {
701
- return ps(`
702
- $pipes = Get-ChildItem '\\.\pipe\' -ErrorAction SilentlyContinue | Select-Object Name
703
- $suspicious = $pipes | Where-Object {
704
- $_.Name -match '(cobaltstrike|meterpreter|beacon|MSSE-|msagent_|postex_|status_|mojo\.|chrome\.\d+\.\d+\.\d+)' -or
705
- $_.Name -match '(psexec|remcom|csexec|svcctl)' -or
706
- ($_.Name -match '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$')
707
- }
708
- if ($suspicious) {
709
- "[!] Potentially suspicious named pipes:"
710
- $suspicious | ForEach-Object { " \\.\pipe\$($_.Name)" }
711
- "Total pipes on system: $($pipes.Count)"
712
- } else {
713
- "No suspicious named pipes found (total: $($pipes.Count))"
714
- }
715
- `, 15000);
716
- }
717
-
718
- function getADS() {
719
- const results = [];
720
- // Check common directories for alternate data streams
721
- const checkDirs = [
722
- path.join(HOME, 'Desktop'),
723
- path.join(HOME, 'Downloads'),
724
- os.tmpdir()
725
- ];
726
-
727
- for (const dir of checkDirs) {
728
- if (!fs.existsSync(dir)) continue;
729
- const result = ps(`
730
- Get-ChildItem "${dir}" -Recurse -ErrorAction SilentlyContinue | ForEach-Object {
731
- $streams = Get-Item $_.FullName -Stream * -ErrorAction SilentlyContinue | Where-Object { $_.Stream -ne ':$DATA' -and $_.Stream -ne 'Zone.Identifier' -and $_.Stream -notlike '*.Zone.Identifier' -and $_.Stream -notlike 'MBAM*' -and $_.Stream -ne 'StreamedFileState' -and $_.Stream -ne 'SmartScreen' -and $_.Stream -ne 'Afp_AfpInfo' -and $_.Length -gt 100 }
732
- if ($streams) {
733
- $streams | ForEach-Object { "[!] ADS: $($_.FileName):$($_.Stream) ($($_.Length) bytes)" }
734
- }
735
- } | Select-Object -First 20
736
- `, 30000);
737
- if (result && !result.startsWith('[ERROR]') && result.includes('[!]')) {
738
- results.push(result);
739
- }
740
- }
741
-
742
- return results.length > 0 ? results.join('\n') : 'No suspicious Alternate Data Streams found';
743
- }
744
-
745
- // --- Original Windows check implementations ---
746
-
747
- function getRDPLogins() {
748
- return ps(`
749
- try {
750
- $events = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624} -MaxEvents 200 -ErrorAction Stop |
751
- Where-Object { $_.Properties[8].Value -eq 10 } |
752
- Select-Object -First 50 |
753
- ForEach-Object {
754
- $ip = $_.Properties[18].Value
755
- $user = $_.Properties[5].Value
756
- $domain = $_.Properties[6].Value
757
- $time = $_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
758
- "$time | $domain\\$user | $ip"
759
- }
760
- if ($events) { $events -join [char]10 } else { 'No RDP logins found in Security log' }
761
- } catch { 'Access denied or Security log unavailable - run as Administrator' }
762
- `, 60000);
763
- }
764
-
765
- function getFailedLogins() {
766
- return ps(`
767
- try {
768
- $events = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} -MaxEvents 100 -ErrorAction Stop |
769
- Select-Object -First 30 |
770
- ForEach-Object {
771
- $ip = $_.Properties[19].Value
772
- $user = $_.Properties[5].Value
773
- $time = $_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
774
- "$time | $user | $ip"
775
- }
776
- if ($events) { $events -join [char]10 } else { 'No failed login attempts found' }
777
- } catch { 'Access denied or Security log unavailable - run as Administrator' }
778
- `, 60000);
779
- }
780
-
781
- function getLocalUsers() {
782
- return ps(`
783
- Get-LocalUser | ForEach-Object {
784
- $name = $_.Name
785
- $enabled = $_.Enabled
786
- $lastLogon = $_.LastLogon
787
- $desc = $_.Description
788
- "$name | Enabled=$enabled | LastLogon=$lastLogon | $desc"
789
- } | Out-String
790
- `);
791
- }
792
-
793
- function getScheduledTasks() {
794
- return ps(`
795
- Get-ScheduledTask | Where-Object { $_.State -ne 'Disabled' -and $_.TaskPath -notlike '\\Microsoft\\*' } |
796
- ForEach-Object {
797
- $name = $_.TaskName
798
- $path = $_.TaskPath
799
- $actions = ($_.Actions | ForEach-Object { $_.Execute + ' ' + $_.Arguments }) -join '; '
800
- "$path$name | $actions"
801
- } | Out-String
802
- `, 60000);
803
- }
804
-
805
- function getStartupKeys() {
806
- const keys = [
807
- 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run',
808
- 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce',
809
- 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run',
810
- 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce',
811
- 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Run',
812
- ];
813
-
814
- return ps(`
815
- $keys = @(${keys.map(k => `'${k}'`).join(',')})
816
- foreach ($key in $keys) {
817
- if (Test-Path $key) {
818
- "=== $key ==="
819
- Get-ItemProperty $key -ErrorAction SilentlyContinue |
820
- ForEach-Object { $_.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' } |
821
- ForEach-Object { " $($_.Name) = $($_.Value)" }
822
- }
823
- }
824
- }
825
- `);
826
- }
827
-
828
- function getServices() {
829
- return ps(`
830
- Get-Service | Where-Object { $_.Status -eq 'Running' -and $_.StartType -eq 'Automatic' } |
831
- Where-Object { $_.DisplayName -notlike 'Windows*' -and $_.DisplayName -notlike 'Microsoft*' -and $_.DisplayName -notlike 'DCOM*' -and $_.DisplayName -notlike 'Plug*' } |
832
- Select-Object Name, DisplayName, StartType |
833
- Format-Table -AutoSize | Out-String
834
- `);
835
- }
836
-
837
- function getNetConnections() {
838
- return ps(`
839
- Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue |
840
- Where-Object { $_.RemoteAddress -ne '127.0.0.1' -and $_.RemoteAddress -ne '::1' -and $_.RemoteAddress -ne '0.0.0.0' } |
841
- Select-Object LocalPort, RemoteAddress, RemotePort, OwningProcess,
842
- @{N='Process';E={(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName}} |
843
- Sort-Object RemoteAddress |
844
- Format-Table -AutoSize | Out-String
845
- `);
846
- }
847
-
848
- function getFirewallRules() {
849
- return ps(`
850
- Get-NetFirewallRule -Enabled True -Direction Inbound -Action Allow -ErrorAction SilentlyContinue |
851
- Where-Object { $_.DisplayName -notlike 'Core Networking*' -and $_.DisplayName -notlike 'Windows*' } |
852
- Select-Object -First 30 DisplayName, Profile, Direction |
853
- Format-Table -AutoSize | Out-String
854
- `);
855
- }
856
-
857
- function getCertificates() {
858
- return ps(`
859
- Get-ChildItem Cert:\\LocalMachine\\Root |
860
- Where-Object { $_.NotAfter -gt (Get-Date) } |
861
- Select-Object Subject, Issuer, NotAfter, Thumbprint |
862
- Format-Table -AutoSize -Wrap | Out-String
863
- `);
864
- }
865
-
866
- function getProxySettings() {
867
- return ps(`
868
- $proxy = Get-ItemProperty 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' -ErrorAction SilentlyContinue
869
- "ProxyEnable: $($proxy.ProxyEnable)"
870
- "ProxyServer: $($proxy.ProxyServer)"
871
- "ProxyOverride: $($proxy.ProxyOverride)"
872
- "AutoConfigURL: $($proxy.AutoConfigURL)"
873
- ""
874
- "Environment:"
875
- "HTTP_PROXY: $env:HTTP_PROXY"
876
- "HTTPS_PROXY: $env:HTTPS_PROXY"
877
- "ALL_PROXY: $env:ALL_PROXY"
878
- `);
879
- }
880
-
881
- function getWinRM() {
882
- return ps(`
883
- try {
884
- $status = Get-Service WinRM -ErrorAction Stop
885
- "WinRM Status: $($status.Status)"
886
- if ($status.Status -eq 'Running') {
887
- "[!!] WARNING: WinRM is running - remote management is enabled"
888
- winrm get winrm/config/client 2>$null
889
- }
890
- } catch {
891
- "WinRM: Not found or access denied"
892
- }
893
- `);
894
- }
895
-
896
- function getSSHKeys() {
897
- const results = [];
898
- const sshDir = path.join(HOME, '.ssh');
899
-
900
- if (fs.existsSync(sshDir)) {
901
- const authKeys = path.join(sshDir, 'authorized_keys');
902
- if (fs.existsSync(authKeys)) {
903
- results.push('=== authorized_keys ===');
904
- const content = fs.readFileSync(authKeys, 'utf8');
905
- const keys = content.split('\n').filter(l => l.trim() && !l.startsWith('#'));
906
- for (const key of keys) {
907
- const parts = key.trim().split(/\s+/);
908
- const comment = parts.length >= 3 ? parts.slice(2).join(' ') : 'no-comment';
909
- const type = parts[0];
910
- results.push(` ${type} ... ${comment}`);
911
- }
912
- } else {
913
- results.push('No authorized_keys file found');
914
- }
915
-
916
- const files = fs.readdirSync(sshDir);
917
- 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));
918
- if (unusual.length > 0) {
919
- results.push(`\nUnusual files in .ssh/: ${unusual.join(', ')}`);
920
- }
921
- } else {
922
- results.push('No .ssh directory found');
923
- }
924
-
925
- return results.join('\n');
926
- }
927
-
928
- function getIDEArtifacts() {
929
- const results = [];
930
-
931
- for (const dir of SUSPICIOUS_IDE_DIRS) {
932
- const fullPath = path.join(HOME, dir);
933
- if (fs.existsSync(fullPath)) {
934
- results.push(`[!] FOUND: ${fullPath}`);
935
- try {
936
- const files = fs.readdirSync(fullPath);
937
- results.push(` Contents: ${files.slice(0, 20).join(', ')}`);
938
-
939
- const mcpPath = path.join(fullPath, 'mcp.json');
940
- if (fs.existsSync(mcpPath)) {
941
- results.push(` [!!] MCP config found: ${mcpPath}`);
942
- try {
943
- const mcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
944
- const servers = Object.keys(mcp.mcpServers || {});
945
- results.push(` MCP servers configured: ${servers.join(', ')}`);
946
- for (const [name, srv] of Object.entries(mcp.mcpServers || {})) {
947
- if (srv.env) {
948
- const envKeys = Object.keys(srv.env);
949
- results.push(` [!] ${name} has env vars: ${envKeys.join(', ')}`);
950
- }
951
- }
952
- } catch {}
953
- }
954
- } catch {}
955
- }
956
- }
957
-
958
- // Check AppData for IDE data directories
959
- const appDataPaths = [];
960
- if (PLATFORM === 'win32') {
961
- const appData = process.env.APPDATA || '';
962
- const localAppData = process.env.LOCALAPPDATA || '';
963
- for (const name of ['Verdent', 'Verdant', 'Trae']) {
964
- if (appData) appDataPaths.push(path.join(appData, name));
965
- if (localAppData) appDataPaths.push(path.join(localAppData, name));
966
- }
967
- } else if (PLATFORM === 'darwin') {
968
- const support = path.join(HOME, 'Library', 'Application Support');
969
- for (const name of ['Verdent', 'Verdant', 'Trae']) {
970
- appDataPaths.push(path.join(support, name));
971
- }
972
- }
973
-
974
- for (const p of appDataPaths) {
975
- if (p && fs.existsSync(p)) {
976
- results.push(`[!] FOUND AppData: ${p}`);
977
- try {
978
- const files = fs.readdirSync(p);
979
- results.push(` Contents: ${files.slice(0, 20).join(', ')}`);
980
- } catch {}
981
- }
982
- }
983
-
984
- if (results.length === 0) {
985
- results.push('No suspicious IDE artifacts found');
986
- }
987
-
988
- return results.join('\n');
989
- }
990
-
991
- function getPSHistory() {
992
- const histPath = path.join(HOME, 'AppData', 'Roaming', 'Microsoft', 'Windows', 'PowerShell', 'PSReadLine', 'ConsoleHost_history.txt');
993
- if (fs.existsSync(histPath)) {
994
- try {
995
- const content = fs.readFileSync(histPath, 'utf8');
996
- const lines = content.split('\n').slice(-200); // last 200 commands
997
- const suspicious = lines.filter(l => {
998
- const lower = l.toLowerCase();
999
- const isSuspicious = lower.includes('downloadstring') ||
1000
- lower.includes('downloadfile') || lower.includes('net.webclient') ||
1001
- lower.includes('iex') || lower.includes('invoke-expression') ||
1002
- lower.includes('-enc ') || lower.includes('encodedcommand') ||
1003
- lower.includes('certutil') || lower.includes('bitsadmin') ||
1004
- lower.includes('frombase64string') || lower.includes('add-mppreference') ||
1005
- lower.includes('invoke-mimikatz') || lower.includes('invoke-shellcode') ||
1006
- lower.includes('new-object net.sockets');
1007
-
1008
- // Filter out known safe invoke-webrequest patterns
1009
- if (lower.includes('invoke-webrequest') && !isPSFalsePositive(l)) {
1010
- return true;
1011
- }
1012
- return isSuspicious;
1013
- });
1014
- if (suspicious.length > 0) {
1015
- return `[!] Suspicious PowerShell commands found:\n${suspicious.join('\n')}`;
1016
- }
1017
- return `Last 200 commands checked - no suspicious patterns found (${lines.length} lines in history)`;
1018
- } catch {
1019
- return 'Could not read PowerShell history';
1020
- }
1021
- }
1022
- return 'No PowerShell history file found';
1023
- }
1024
-
1025
- function getDNSCache() {
1026
- return ps(`
1027
- $cache = Get-DnsClientCache -ErrorAction SilentlyContinue |
1028
- Select-Object -First 100 Entry, Data |
1029
- Where-Object { $_.Entry -match '\\.(cn|ru|ir|kp)$' -or $_.Entry -match '(baidu|qq|163|aliyun|tencent|weibo|bytedance|douyin)' }
1030
- if ($cache) {
1031
- "[!] Suspicious DNS entries found:"
1032
- $cache | Format-Table -AutoSize | Out-String
1033
- } else {
1034
- "No suspicious DNS entries (.cn/.ru/.ir/.kp or known Chinese services)"
1035
- }
1036
- `);
1037
- }
1038
-
1039
- function getRecentInstalls() {
1040
- return ps(`
1041
- Get-ItemProperty HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\* |
1042
- Where-Object { $_.InstallDate -and $_.InstallDate -gt (Get-Date).AddDays(-90).ToString('yyyyMMdd') } |
1043
- Select-Object DisplayName, DisplayVersion, Publisher, InstallDate |
1044
- Sort-Object InstallDate -Descending |
1045
- Format-Table -AutoSize | Out-String
1046
- `);
1047
- }
1048
-
1049
- function getSuspiciousProcesses() {
1050
- return ps(`
1051
- $suspicious = @(${SUSPICIOUS_PROCESSES.map(p => `'${p}'`).join(',')})
1052
- $procs = Get-Process -ErrorAction SilentlyContinue |
1053
- Select-Object Name, Id, Path, Company |
1054
- Where-Object {
1055
- $_.Name -in $suspicious -or
1056
- ($_.Path -and ($_.Path -match '\\\\Temp\\\\' -or $_.Path -match '\\\\tmp\\\\' -or $_.Path -match '\\\\Downloads\\\\'))
1057
- }
1058
- if ($procs) {
1059
- "[!] Suspicious processes found:"
1060
- $procs | Format-Table -AutoSize | Out-String
1061
- } else {
1062
- "No known suspicious processes found"
1063
- }
1064
- `);
1065
- }
1066
-
1067
- // ============================================
1068
- // LINUX CHECKS
1069
- // ============================================
1070
-
1071
- function linuxChecks() {
1072
- const findings = [];
1073
-
1074
- // --- Original checks ---
1075
- findings.push({ check: 'SSH Login History', data: run('last -50 2>/dev/null || echo "last command not available"') });
1076
- 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"') });
1077
- findings.push({ check: 'User Accounts', data: run('cat /etc/passwd | grep -v nologin | grep -v /false') });
1078
- findings.push({ check: 'Sudoers', data: run('cat /etc/sudoers 2>/dev/null; ls -la /etc/sudoers.d/ 2>/dev/null') });
1079
- 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') });
1080
- findings.push({ check: 'SSH Authorized Keys', data: getSSHKeys() });
1081
- findings.push({ check: 'Active Connections', data: run('ss -tunp 2>/dev/null || netstat -tunp 2>/dev/null') });
1082
- findings.push({ check: 'Listening Ports', data: run('ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null') });
1083
- findings.push({ check: 'Running Services', data: run('systemctl list-units --type=service --state=running 2>/dev/null | head -40') });
1084
- findings.push({ check: 'Suspicious IDE Artifacts', data: getIDEArtifacts() });
1085
- 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|umount|fusermount|dbus-daemon-launch-helper)" | head -20', 60000) });
1086
- 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') });
1087
- findings.push({ check: 'Bash History (suspicious)', data: run('grep -hE "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"') });
1088
-
1089
- // --- v2 Linux-specific checks ---
1090
- findings.push({ check: 'LD_PRELOAD Hijacking', data: getLDPreload() });
1091
- findings.push({ check: 'Kernel Modules', data: getKernelModules() });
1092
- findings.push({ check: 'Systemd Timers', data: getSystemdTimers() });
1093
- findings.push({ check: 'At Jobs', data: run('atq 2>/dev/null || echo "at not installed or no jobs"') });
1094
- findings.push({ check: 'Profile.d Scripts', data: run('ls -la /etc/profile.d/ 2>/dev/null; echo "---"; cat /etc/environment 2>/dev/null') });
1095
- findings.push({ check: 'File Capabilities', data: run('getcap -r / 2>/dev/null | grep -v -E "(ping|traceroute|mtr)" | head -20 || echo "getcap not available"', 60000) });
1096
-
1097
- // --- v2 Cross-platform checks ---
1098
- findings.push({ check: 'Browser Extensions', data: getBrowserExtensions() });
1099
- findings.push({ check: 'Supply Chain Packages (npm/pip)', data: getSupplyChainPackages() });
1100
- findings.push({ check: 'Docker Containers', data: getDockerContainers() });
1101
- findings.push({ check: 'Git Hooks', data: getGitHooks() });
1102
- findings.push({ check: 'IDE Extensions (VS Code/Cursor)', data: getIDEExtensions() });
1103
-
1104
- return findings;
1105
- }
1106
-
1107
- function getLDPreload() {
1108
- const results = [];
1109
-
1110
- // Check /etc/ld.so.preload
1111
- try {
1112
- if (fs.existsSync('/etc/ld.so.preload')) {
1113
- const content = fs.readFileSync('/etc/ld.so.preload', 'utf8').trim();
1114
- if (content) {
1115
- results.push(`[!!] /etc/ld.so.preload contains entries:\n ${content}`);
1116
- }
1117
- }
1118
- } catch {}
1119
-
1120
- // Check LD_PRELOAD env
1121
- const ldPreload = process.env.LD_PRELOAD;
1122
- if (ldPreload) {
1123
- results.push(`[!!] LD_PRELOAD environment variable set: ${ldPreload}`);
1124
- }
1125
-
1126
- // Check /etc/environment for LD_PRELOAD
1127
- try {
1128
- if (fs.existsSync('/etc/environment')) {
1129
- const content = fs.readFileSync('/etc/environment', 'utf8');
1130
- if (content.includes('LD_PRELOAD')) {
1131
- results.push(`[!!] LD_PRELOAD found in /etc/environment`);
1132
- }
1133
- }
1134
- } catch {}
1135
-
1136
- return results.length > 0 ? results.join('\n') : 'No LD_PRELOAD hijacking detected';
1137
- }
1138
-
1139
- function getKernelModules() {
1140
- const results = [];
1141
- const loaded = run('lsmod 2>/dev/null | head -30');
1142
- if (!loaded.startsWith('[ERROR]')) {
1143
- results.push('Loaded kernel modules (first 30):');
1144
- results.push(loaded);
1145
- }
1146
-
1147
- // Check for unsigned/out-of-tree modules
1148
- const unsigned = run('for mod in $(lsmod 2>/dev/null | tail -n+2 | awk \'{print $1}\'); do modinfo $mod 2>/dev/null | grep -q "intree:.*N" && echo "[!] Out-of-tree: $mod"; done 2>/dev/null | head -10', 30000);
1149
- if (unsigned && !unsigned.startsWith('[ERROR]') && unsigned.includes('[!]')) {
1150
- results.push(unsigned);
1151
- }
1152
-
1153
- return results.length > 0 ? results.join('\n') : 'Cannot read kernel modules';
1154
- }
1155
-
1156
- function getSystemdTimers() {
1157
- return run('systemctl list-timers --all 2>/dev/null | head -25 || echo "systemd timers not available"');
1158
- }
1159
-
1160
- // ============================================
1161
- // macOS CHECKS
1162
- // ============================================
1163
-
1164
- function macChecks() {
1165
- const findings = [];
1166
-
1167
- // --- Original checks ---
1168
- findings.push({ check: 'Login History', data: run('last -50 2>/dev/null || echo "last command not available"') });
1169
- 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) });
1170
- findings.push({ check: 'Remote Login (SSH) Status', data: run('systemsetup -getremotelogin 2>/dev/null || echo "Cannot check remote login status"') });
1171
- 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"') });
1172
- findings.push({ check: 'User Accounts', data: run('dscl . list /Users | grep -v "^_" | grep -v daemon | grep -v nobody') });
1173
- findings.push({ check: 'Admin Users', data: run('dscl . -read /Groups/admin GroupMembership 2>/dev/null || echo "Cannot read admin group"') });
1174
- findings.push({ check: 'User LaunchAgents', data: getMacLaunchItems(path.join(HOME, 'Library', 'LaunchAgents')) });
1175
- findings.push({ check: 'System LaunchAgents', data: getMacLaunchItems('/Library/LaunchAgents') });
1176
- findings.push({ check: 'LaunchDaemons', data: getMacLaunchItems('/Library/LaunchDaemons') });
1177
- 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"') });
1178
- 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"') });
1179
- findings.push({ check: 'SSH Authorized Keys', data: getSSHKeys() });
1180
- 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') });
1181
- findings.push({ check: 'Listening Ports', data: run('lsof -i -P -n 2>/dev/null | grep LISTEN | head -30') });
1182
- 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"') });
1183
- 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"') });
1184
- 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"') });
1185
- findings.push({ check: 'Firewall Status', data: run('/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null || echo "Cannot check firewall"') });
1186
- findings.push({ check: 'Suspicious IDE Artifacts', data: getIDEArtifacts() });
1187
- 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"') });
1188
- findings.push({ check: 'Gatekeeper & SIP Status', data: run('spctl --status 2>/dev/null; csrutil status 2>/dev/null') });
1189
- 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)"') });
1190
-
1191
- // --- v2 macOS-specific checks ---
1192
- findings.push({ check: 'Authorization Plugins', data: getMacAuthPlugins() });
1193
- findings.push({ check: 'Kernel Extensions (kexts)', data: getMacKexts() });
1194
- findings.push({ check: 'Spotlight Importers', data: getMacSpotlightImporters() });
1195
- findings.push({ check: 'Directory Services Plugins', data: getMacDSPlugins() });
1196
- findings.push({ check: 'LD_PRELOAD / DYLD_INSERT', data: getMacDylibInjection() });
1197
- findings.push({ check: 'Periodic Scripts', data: run('ls -la /etc/periodic/daily/ 2>/dev/null; ls -la /etc/periodic/weekly/ 2>/dev/null; ls -la /etc/periodic/monthly/ 2>/dev/null') });
1198
- findings.push({ check: 'at Jobs', data: run('atq 2>/dev/null || echo "at not available or no jobs"') });
1199
-
1200
- // --- v2 Cross-platform checks ---
1201
- findings.push({ check: 'Browser Extensions', data: getBrowserExtensions() });
1202
- findings.push({ check: 'Supply Chain Packages (npm/pip)', data: getSupplyChainPackages() });
1203
- findings.push({ check: 'Docker Containers', data: getDockerContainers() });
1204
- findings.push({ check: 'Git Hooks', data: getGitHooks() });
1205
- findings.push({ check: 'IDE Extensions (VS Code/Cursor)', data: getIDEExtensions() });
1206
-
1207
- return findings;
1208
- }
1209
-
1210
- function getMacAuthPlugins() {
1211
- const pluginDir = '/Library/Security/SecurityAgentPlugins';
1212
- try {
1213
- if (!fs.existsSync(pluginDir)) return 'No custom authorization plugins';
1214
- const plugins = fs.readdirSync(pluginDir);
1215
- if (plugins.length === 0) return 'No authorization plugins found';
1216
- const results = plugins.map(p => {
1217
- const isApple = p.startsWith('com.apple.');
1218
- return `${isApple ? ' ' : '[!] '}${p}`;
1219
- });
1220
- return results.join('\n');
1221
- } catch {
1222
- return 'Cannot read authorization plugins directory';
1223
- }
1224
- }
1225
-
1226
- function getMacKexts() {
1227
- const result = run('kextstat 2>/dev/null | grep -v com.apple | head -20');
1228
- if (result && !result.startsWith('[ERROR]') && result.trim()) {
1229
- return `[!] Non-Apple kernel extensions loaded:\n${result}`;
1230
- }
1231
- return run('kextstat 2>/dev/null | wc -l | xargs -I{} echo "Total kexts loaded: {} (all Apple)"') || 'Cannot enumerate kexts';
1232
- }
1233
-
1234
- function getMacSpotlightImporters() {
1235
- const importerDirs = [
1236
- '/Library/Spotlight',
1237
- path.join(HOME, 'Library', 'Spotlight')
1238
- ];
1239
- const results = [];
1240
- for (const dir of importerDirs) {
1241
- try {
1242
- if (!fs.existsSync(dir)) continue;
1243
- const importers = fs.readdirSync(dir);
1244
- if (importers.length > 0) {
1245
- results.push(`${dir}:`);
1246
- for (const imp of importers) {
1247
- results.push(` [!] ${imp}`);
1248
- }
1249
- }
1250
- } catch {}
1251
- }
1252
- return results.length > 0 ? results.join('\n') : 'No custom Spotlight importers found';
1253
- }
1254
-
1255
- function getMacDSPlugins() {
1256
- const pluginDir = '/Library/DirectoryServices/PlugIns';
1257
- try {
1258
- if (!fs.existsSync(pluginDir)) return 'No custom Directory Services plugins';
1259
- const plugins = fs.readdirSync(pluginDir);
1260
- if (plugins.length === 0) return 'No DS plugins found';
1261
- return plugins.map(p => ` [!] ${p}`).join('\n');
1262
- } catch {
1263
- return 'Cannot read DS plugins directory';
1264
- }
1265
- }
1266
-
1267
- function getMacDylibInjection() {
1268
- const results = [];
1269
-
1270
- // Check DYLD_INSERT_LIBRARIES
1271
- const dyldInsert = process.env.DYLD_INSERT_LIBRARIES;
1272
- if (dyldInsert) {
1273
- results.push(`[!!] DYLD_INSERT_LIBRARIES set: ${dyldInsert}`);
1274
- }
1275
-
1276
- const dyldForce = process.env.DYLD_FORCE_FLAT_NAMESPACE;
1277
- if (dyldForce) {
1278
- results.push(`[!!] DYLD_FORCE_FLAT_NAMESPACE set`);
1279
- }
1280
-
1281
- // Check /etc/environment
1282
- try {
1283
- if (fs.existsSync('/etc/environment')) {
1284
- const content = fs.readFileSync('/etc/environment', 'utf8');
1285
- if (content.includes('DYLD_INSERT') || content.includes('LD_PRELOAD')) {
1286
- results.push(`[!!] Library injection env vars found in /etc/environment`);
1287
- }
1288
- }
1289
- } catch {}
1290
-
1291
- // Check shell profiles for injection
1292
- const profiles = [
1293
- path.join(HOME, '.bash_profile'),
1294
- path.join(HOME, '.zshrc'),
1295
- path.join(HOME, '.zprofile'),
1296
- path.join(HOME, '.profile'),
1297
- ];
1298
- for (const profile of profiles) {
1299
- try {
1300
- if (!fs.existsSync(profile)) continue;
1301
- const content = fs.readFileSync(profile, 'utf8');
1302
- if (content.includes('DYLD_INSERT') || content.includes('LD_PRELOAD')) {
1303
- results.push(`[!!] Library injection found in ${profile}`);
1304
- }
1305
- } catch {}
1306
- }
1307
-
1308
- return results.length > 0 ? results.join('\n') : 'No dylib/library injection detected';
1309
- }
1310
-
1311
- /**
1312
- * Read macOS LaunchAgent/LaunchDaemon plist files for suspicious entries
1313
- */
1314
- function getMacLaunchItems(dirPath) {
1315
- const results = [];
1316
- try {
1317
- if (!fs.existsSync(dirPath)) {
1318
- return `Directory not found: ${dirPath}`;
1319
- }
1320
- const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.plist'));
1321
- if (files.length === 0) {
1322
- return `No plist files in ${dirPath}`;
1323
- }
1324
-
1325
- for (const file of files) {
1326
- const fullPath = path.join(dirPath, file);
1327
- const content = run(`plutil -p "${fullPath}" 2>/dev/null`);
1328
-
1329
- const isApple = file.startsWith('com.apple.') || file.startsWith('com.openssh.');
1330
- const label = isApple ? ' ' : '[!] ';
1331
-
1332
- const programMatch = content.match(/"Program(?:Arguments)?" => (?:\[[\s\S]*?\]|"[^"]*")/);
1333
- const program = programMatch ? programMatch[0].substring(0, 120) : '';
1334
-
1335
- results.push(`${label}${file}`);
1336
- if (program) results.push(` ${program}`);
1337
- }
1338
- } catch (e) {
1339
- results.push(`[ERROR] Cannot read ${dirPath}: ${e.message}`);
1340
- }
1341
- return results.join('\n');
1342
- }
1343
-
1344
- // ============================================
1345
- // MAIN AUDIT FUNCTION
1346
- // ============================================
1347
-
1348
- async function runAudit(options = {}) {
1349
- const startTime = Date.now();
1350
- const result = {
1351
- ok: true,
1352
- version: 2,
1353
- platform: PLATFORM,
1354
- hostname: os.hostname(),
1355
- timestamp: new Date().toISOString(),
1356
- user: os.userInfo().username,
1357
- findings: [],
1358
- alerts: [],
1359
- summary: {}
1360
- };
1361
-
1362
- // Run platform-specific checks
1363
- if (PLATFORM === 'win32') {
1364
- result.findings = windowsChecks();
1365
- } else if (PLATFORM === 'darwin') {
1366
- result.findings = macChecks();
1367
- } else {
1368
- result.findings = linuxChecks();
1369
- }
1370
-
1371
- // Extract unique IPs from findings for geo-lookup (IPv4 + IPv6)
1372
- const allIPv4 = new Set();
1373
- const allIPv6 = new Set();
1374
-
1375
- // Only extract IPs from network-related checks (not user accounts, timestamps, etc.)
1376
- const networkChecks = ['Active Network Connections', 'Active Connections', 'Listening Ports',
1377
- 'RDP Login History', 'Failed Login Attempts', 'Failed SSH Attempts', 'SSH Login History',
1378
- 'Login History', 'Firewall Rules', 'PowerShell History', 'Shell History',
1379
- 'Bash History', 'DNS Cache'];
1380
-
1381
- for (const f of result.findings) {
1382
- if (typeof f.data !== 'string') continue;
1383
- // Only extract IPs from relevant checks to avoid version numbers / timestamps
1384
- if (!networkChecks.includes(f.check)) continue;
1385
-
1386
- // IPv4 - must have valid octets (0-255)
1387
- const ipv4Matches = f.data.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g) || [];
1388
- for (const ip of ipv4Matches) {
1389
- const octets = ip.split('.').map(Number);
1390
- // Validate: each octet 0-255, not private, not broadcast
1391
- if (octets.some(o => o > 255)) continue;
1392
- if (ip.startsWith('127.') || ip.startsWith('0.') || ip.startsWith('10.') ||
1393
- ip.startsWith('192.168.') || ip.startsWith('169.254.') || ip === '255.255.255.255' ||
1394
- ip.match(/^172\.(1[6-9]|2\d|3[01])\./)) continue;
1395
- allIPv4.add(ip);
1396
- }
1397
-
1398
- // IPv6 - only extract from network connection data
1399
- if (['Active Network Connections', 'Active Connections'].includes(f.check)) {
1400
- const ipv6Matches = f.data.match(/(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:)*::[0-9a-fA-F:]+/g) || [];
1401
- for (const ip of ipv6Matches) {
1402
- if (!ip.startsWith('::1') && !ip.startsWith('fe80') && !ip.startsWith('fc') && !ip.startsWith('fd')) {
1403
- allIPv6.add(ip);
1404
- }
1405
- }
1406
- }
1407
- }
1408
-
1409
- // Batch geo-IP lookup (IPv4 - ip-api.com supports IPv4 best)
1410
- const allIPs = new Set([...allIPv4]);
1411
- // Add IPv6 addresses too (ip-api.com does support some IPv6)
1412
- for (const ip6 of allIPv6) {
1413
- allIPs.add(ip6);
1414
- }
1415
-
1416
- if (allIPs.size > 0 && !options.skipGeo) {
1417
- const geoResults = await batchGeoIP([...allIPs]);
1418
- result.geoIP = {};
1419
-
1420
- for (const [ip, geo] of Object.entries(geoResults)) {
1421
- result.geoIP[ip] = {
1422
- country: geo.country,
1423
- countryCode: geo.countryCode,
1424
- city: geo.city,
1425
- isp: geo.isp,
1426
- org: geo.org
1427
- };
1428
-
1429
- if (SUSPICIOUS_GEOS.includes(geo.countryCode)) {
1430
- result.alerts.push({
1431
- severity: 'HIGH',
1432
- message: `Connection from suspicious country: ${ip} → ${geo.country} (${geo.city}) [${geo.isp}]`,
1433
- ip,
1434
- geo
1435
- });
1436
- }
1437
- }
1438
- }
1439
-
1440
- result.ipSummary = {
1441
- ipv4Count: allIPv4.size,
1442
- ipv6Count: allIPv6.size,
1443
- totalUnique: allIPs.size
1444
- };
1445
-
1446
- // Analyze findings for alerts with weighted severity
1447
- for (const f of result.findings) {
1448
- if (typeof f.data === 'string') {
1449
- if (f.data.includes('[!!]')) {
1450
- result.alerts.push({
1451
- severity: 'HIGH',
1452
- check: f.check,
1453
- message: `${f.check}: ${f.data.split('\n').find(l => l.includes('[!!]')) || f.data.substring(0, 200)}`
1454
- });
1455
- } else if (f.data.includes('[!]')) {
1456
- result.alerts.push({
1457
- severity: 'MEDIUM',
1458
- check: f.check,
1459
- message: `${f.check}: ${f.data.split('\n').find(l => l.includes('[!]')) || f.data.substring(0, 200)}`
1460
- });
1461
- }
1462
- }
1463
- }
1464
-
1465
- // Severity score
1466
- let severityScore = 0;
1467
- for (const alert of result.alerts) {
1468
- severityScore += SEVERITY_WEIGHTS[alert.severity] || 0;
1469
- }
1470
-
1471
- // Summary
1472
- result.summary = {
1473
- totalChecks: result.findings.length,
1474
- alerts: result.alerts.length,
1475
- highSeverity: result.alerts.filter(a => a.severity === 'HIGH').length,
1476
- mediumSeverity: result.alerts.filter(a => a.severity === 'MEDIUM').length,
1477
- uniqueExternalIPv4: allIPv4.size,
1478
- uniqueExternalIPv6: allIPv6.size,
1479
- suspiciousGeoIPs: result.alerts.filter(a => a.ip).length,
1480
- severityScore,
1481
- duration: Date.now() - startTime
1482
- };
1483
-
1484
- if (result.summary.highSeverity > 0 || severityScore >= 100) {
1485
- result.verdict = 'INVESTIGATE - High severity alerts found';
1486
- } else if (severityScore >= 40) {
1487
- result.verdict = 'REVIEW - Medium severity items found';
1488
- } else if (severityScore > 0) {
1489
- result.verdict = 'REVIEW - Low severity items found';
1490
- } else {
1491
- result.verdict = 'CLEAN - No suspicious findings';
1492
- }
1493
-
1494
- return result;
1495
- }
1496
-
1497
- module.exports = { runAudit, windowsChecks, linuxChecks, macChecks, batchGeoIP, geoIP };