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.
- package/README.md +175 -201
- package/bin/50c.js +2079 -2284
- package/lib/pre-publish.js +812 -1361
- package/lib/subagent.js +7 -13
- package/lib/team.js +691 -714
- package/lib/tools-registry.js +184 -226
- package/package.json +39 -44
- package/lib/backdoor-checker.js +0 -1497
- package/lib/ip-utils.js +0 -47
package/lib/backdoor-checker.js
DELETED
|
@@ -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 };
|