50c 3.9.4 → 3.9.6
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/bin/50c.js +2284 -2226
- package/lib/backdoor-checker.js +991 -230
- package/lib/ip-utils.js +47 -0
- package/lib/pre-publish.js +1361 -812
- package/lib/team.js +714 -691
- package/lib/tools-registry.js +226 -223
- package/package.json +7 -2
package/lib/backdoor-checker.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 50c Backdoor Checker -
|
|
2
|
+
* 50c Backdoor Checker v2 - Aggressive local security audit tool
|
|
3
3
|
* Runs entirely on the user's machine. FREE.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
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
|
|
13
22
|
*
|
|
14
23
|
* Cross-platform: Windows (PowerShell), Linux (bash), macOS (bash)
|
|
15
24
|
*/
|
|
@@ -29,16 +38,53 @@ const SUSPICIOUS_GEOS = ['CN', 'RU', 'KP', 'IR'];
|
|
|
29
38
|
const SUSPICIOUS_IDE_DIRS = [
|
|
30
39
|
'.verdent', '.verdant', '.verd',
|
|
31
40
|
'.trae', // ByteDance IDE
|
|
41
|
+
'.deveco', // Huawei IDE
|
|
32
42
|
];
|
|
33
43
|
|
|
34
44
|
// Known suspicious process names
|
|
35
45
|
const SUSPICIOUS_PROCESSES = [
|
|
36
|
-
'cryptominer', 'xmrig', 'minerd', 'cgminer',
|
|
37
|
-
'nc.exe', 'ncat', 'netcat',
|
|
38
|
-
'mimikatz', 'lazagne', 'procdump',
|
|
46
|
+
'cryptominer', 'xmrig', 'minerd', 'cgminer', 'bfgminer',
|
|
47
|
+
'nc.exe', 'ncat', 'netcat', 'socat',
|
|
48
|
+
'mimikatz', 'lazagne', 'procdump', 'rubeus',
|
|
39
49
|
'psexec', 'wmic', // lateral movement
|
|
50
|
+
'cobaltstrike', 'beacon', 'meterpreter',
|
|
51
|
+
'chisel', 'plink', 'ngrok', // tunneling
|
|
40
52
|
];
|
|
41
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
|
+
|
|
42
88
|
/**
|
|
43
89
|
* Run a command and return stdout (or error message)
|
|
44
90
|
*/
|
|
@@ -57,21 +103,34 @@ function run(cmd, timeout = 30000) {
|
|
|
57
103
|
}
|
|
58
104
|
|
|
59
105
|
/**
|
|
60
|
-
* Run PowerShell command (Windows)
|
|
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
|
|
61
110
|
*/
|
|
62
111
|
function ps(script, timeout = 30000) {
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
}
|
|
65
125
|
}
|
|
66
126
|
|
|
67
127
|
/**
|
|
68
128
|
* Geo-IP lookup using free API (ip-api.com)
|
|
69
|
-
* Returns { country, countryCode, city, isp, org }
|
|
70
129
|
*/
|
|
71
130
|
async function geoIP(ip) {
|
|
72
131
|
try {
|
|
73
132
|
const http = require('http');
|
|
74
|
-
return new Promise((resolve
|
|
133
|
+
return new Promise((resolve) => {
|
|
75
134
|
const req = http.get(`http://ip-api.com/json/${ip}?fields=country,countryCode,city,isp,org`, { timeout: 5000 }, (res) => {
|
|
76
135
|
let data = '';
|
|
77
136
|
res.on('data', c => data += c);
|
|
@@ -128,6 +187,281 @@ async function batchGeoIP(ips) {
|
|
|
128
187
|
}
|
|
129
188
|
}
|
|
130
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
|
+
|
|
131
465
|
// ============================================
|
|
132
466
|
// WINDOWS CHECKS
|
|
133
467
|
// ============================================
|
|
@@ -135,117 +469,336 @@ async function batchGeoIP(ips) {
|
|
|
135
469
|
function windowsChecks() {
|
|
136
470
|
const findings = [];
|
|
137
471
|
|
|
138
|
-
//
|
|
472
|
+
// --- Original checks ---
|
|
139
473
|
findings.push({ check: 'RDP Login History', data: getRDPLogins() });
|
|
140
|
-
|
|
141
|
-
// 2. Failed RDP attempts (Event ID 4625)
|
|
142
474
|
findings.push({ check: 'Failed Login Attempts', data: getFailedLogins() });
|
|
143
|
-
|
|
144
|
-
// 3. Local user accounts
|
|
145
475
|
findings.push({ check: 'Local User Accounts', data: getLocalUsers() });
|
|
146
|
-
|
|
147
|
-
// 4. Scheduled tasks (persistence)
|
|
148
476
|
findings.push({ check: 'Scheduled Tasks', data: getScheduledTasks() });
|
|
149
|
-
|
|
150
|
-
// 5. Startup programs (registry Run keys)
|
|
151
477
|
findings.push({ check: 'Startup Registry Keys', data: getStartupKeys() });
|
|
152
|
-
|
|
153
|
-
// 6. Running services (unusual ones)
|
|
154
478
|
findings.push({ check: 'Services', data: getServices() });
|
|
155
|
-
|
|
156
|
-
// 7. Active network connections
|
|
157
479
|
findings.push({ check: 'Active Network Connections', data: getNetConnections() });
|
|
158
|
-
|
|
159
|
-
// 8. Firewall rules
|
|
160
480
|
findings.push({ check: 'Firewall Rules', data: getFirewallRules() });
|
|
161
|
-
|
|
162
|
-
// 9. Certificates
|
|
163
481
|
findings.push({ check: 'Root Certificates', data: getCertificates() });
|
|
164
|
-
|
|
165
|
-
// 10. Proxy settings
|
|
166
482
|
findings.push({ check: 'Proxy Settings', data: getProxySettings() });
|
|
167
|
-
|
|
168
|
-
// 11. WinRM / Remote Management
|
|
169
483
|
findings.push({ check: 'Remote Management (WinRM)', data: getWinRM() });
|
|
170
|
-
|
|
171
|
-
// 12. SSH authorized_keys
|
|
172
484
|
findings.push({ check: 'SSH Authorized Keys', data: getSSHKeys() });
|
|
173
|
-
|
|
174
|
-
// 13. Suspicious IDE artifacts
|
|
175
485
|
findings.push({ check: 'Suspicious IDE Artifacts', data: getIDEArtifacts() });
|
|
176
|
-
|
|
177
|
-
// 14. PowerShell history (may reveal attacker commands)
|
|
178
486
|
findings.push({ check: 'PowerShell History', data: getPSHistory() });
|
|
179
|
-
|
|
180
|
-
// 15. DNS cache (suspicious domains)
|
|
181
487
|
findings.push({ check: 'DNS Cache', data: getDNSCache() });
|
|
182
|
-
|
|
183
|
-
// 16. Recently installed programs
|
|
184
488
|
findings.push({ check: 'Recently Installed Programs', data: getRecentInstalls() });
|
|
185
|
-
|
|
186
|
-
// 17. Hidden/suspicious processes
|
|
187
489
|
findings.push({ check: 'Suspicious Processes', data: getSuspiciousProcesses() });
|
|
188
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
|
+
|
|
189
509
|
return findings;
|
|
190
510
|
}
|
|
191
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
|
+
|
|
192
747
|
function getRDPLogins() {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
748
|
+
return ps(`
|
|
749
|
+
try {
|
|
750
|
+
$events = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624} -MaxEvents 200 -ErrorAction Stop |
|
|
196
751
|
Where-Object { $_.Properties[8].Value -eq 10 } |
|
|
197
752
|
Select-Object -First 50 |
|
|
198
753
|
ForEach-Object {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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"
|
|
204
759
|
}
|
|
205
|
-
|
|
206
|
-
|
|
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' }
|
|
207
762
|
`, 60000);
|
|
208
|
-
return result;
|
|
209
763
|
}
|
|
210
764
|
|
|
211
765
|
function getFailedLogins() {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
766
|
+
return ps(`
|
|
767
|
+
try {
|
|
768
|
+
$events = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} -MaxEvents 100 -ErrorAction Stop |
|
|
215
769
|
Select-Object -First 30 |
|
|
216
770
|
ForEach-Object {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
771
|
+
$ip = $_.Properties[19].Value
|
|
772
|
+
$user = $_.Properties[5].Value
|
|
773
|
+
$time = $_.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
|
|
774
|
+
"$time | $user | $ip"
|
|
221
775
|
}
|
|
222
|
-
|
|
223
|
-
|
|
776
|
+
if ($events) { $events -join [char]10 } else { 'No failed login attempts found' }
|
|
777
|
+
} catch { 'Access denied or Security log unavailable - run as Administrator' }
|
|
224
778
|
`, 60000);
|
|
225
|
-
return result;
|
|
226
779
|
}
|
|
227
780
|
|
|
228
781
|
function getLocalUsers() {
|
|
229
782
|
return ps(`
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
237
790
|
`);
|
|
238
791
|
}
|
|
239
792
|
|
|
240
793
|
function getScheduledTasks() {
|
|
241
794
|
return ps(`
|
|
242
|
-
|
|
243
|
-
|
|
795
|
+
Get-ScheduledTask | Where-Object { $_.State -ne 'Disabled' -and $_.TaskPath -notlike '\\Microsoft\\*' } |
|
|
796
|
+
ForEach-Object {
|
|
244
797
|
$name = $_.TaskName
|
|
245
798
|
$path = $_.TaskPath
|
|
246
799
|
$actions = ($_.Actions | ForEach-Object { $_.Execute + ' ' + $_.Arguments }) -join '; '
|
|
247
800
|
"$path$name | $actions"
|
|
248
|
-
|
|
801
|
+
} | Out-String
|
|
249
802
|
`, 60000);
|
|
250
803
|
}
|
|
251
804
|
|
|
@@ -259,84 +812,84 @@ function getStartupKeys() {
|
|
|
259
812
|
];
|
|
260
813
|
|
|
261
814
|
return ps(`
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
815
|
+
$keys = @(${keys.map(k => `'${k}'`).join(',')})
|
|
816
|
+
foreach ($key in $keys) {
|
|
817
|
+
if (Test-Path $key) {
|
|
265
818
|
"=== $key ==="
|
|
266
819
|
Get-ItemProperty $key -ErrorAction SilentlyContinue |
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
820
|
+
ForEach-Object { $_.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' } |
|
|
821
|
+
ForEach-Object { " $($_.Name) = $($_.Value)" }
|
|
822
|
+
}
|
|
271
823
|
}
|
|
824
|
+
}
|
|
272
825
|
`);
|
|
273
826
|
}
|
|
274
827
|
|
|
275
828
|
function getServices() {
|
|
276
829
|
return ps(`
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
281
834
|
`);
|
|
282
835
|
}
|
|
283
836
|
|
|
284
837
|
function getNetConnections() {
|
|
285
838
|
return ps(`
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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,
|
|
289
842
|
@{N='Process';E={(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName}} |
|
|
290
|
-
|
|
291
|
-
|
|
843
|
+
Sort-Object RemoteAddress |
|
|
844
|
+
Format-Table -AutoSize | Out-String
|
|
292
845
|
`);
|
|
293
846
|
}
|
|
294
847
|
|
|
295
848
|
function getFirewallRules() {
|
|
296
849
|
return ps(`
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
301
854
|
`);
|
|
302
855
|
}
|
|
303
856
|
|
|
304
857
|
function getCertificates() {
|
|
305
858
|
return ps(`
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
|
310
863
|
`);
|
|
311
864
|
}
|
|
312
865
|
|
|
313
866
|
function getProxySettings() {
|
|
314
867
|
return ps(`
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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"
|
|
325
878
|
`);
|
|
326
879
|
}
|
|
327
880
|
|
|
328
881
|
function getWinRM() {
|
|
329
882
|
return ps(`
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
"WARNING: WinRM is running - remote management is enabled"
|
|
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"
|
|
335
888
|
winrm get winrm/config/client 2>$null
|
|
336
|
-
}
|
|
337
|
-
} catch {
|
|
338
|
-
"WinRM: Not found or access denied"
|
|
339
889
|
}
|
|
890
|
+
} catch {
|
|
891
|
+
"WinRM: Not found or access denied"
|
|
892
|
+
}
|
|
340
893
|
`);
|
|
341
894
|
}
|
|
342
895
|
|
|
@@ -360,7 +913,6 @@ function getSSHKeys() {
|
|
|
360
913
|
results.push('No authorized_keys file found');
|
|
361
914
|
}
|
|
362
915
|
|
|
363
|
-
// Check for unusual files in .ssh
|
|
364
916
|
const files = fs.readdirSync(sshDir);
|
|
365
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));
|
|
366
918
|
if (unusual.length > 0) {
|
|
@@ -384,7 +936,6 @@ function getIDEArtifacts() {
|
|
|
384
936
|
const files = fs.readdirSync(fullPath);
|
|
385
937
|
results.push(` Contents: ${files.slice(0, 20).join(', ')}`);
|
|
386
938
|
|
|
387
|
-
// Check for MCP config with embedded secrets
|
|
388
939
|
const mcpPath = path.join(fullPath, 'mcp.json');
|
|
389
940
|
if (fs.existsSync(mcpPath)) {
|
|
390
941
|
results.push(` [!!] MCP config found: ${mcpPath}`);
|
|
@@ -392,7 +943,6 @@ function getIDEArtifacts() {
|
|
|
392
943
|
const mcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
|
|
393
944
|
const servers = Object.keys(mcp.mcpServers || {});
|
|
394
945
|
results.push(` MCP servers configured: ${servers.join(', ')}`);
|
|
395
|
-
// Check for embedded API keys
|
|
396
946
|
for (const [name, srv] of Object.entries(mcp.mcpServers || {})) {
|
|
397
947
|
if (srv.env) {
|
|
398
948
|
const envKeys = Object.keys(srv.env);
|
|
@@ -405,13 +955,21 @@ function getIDEArtifacts() {
|
|
|
405
955
|
}
|
|
406
956
|
}
|
|
407
957
|
|
|
408
|
-
//
|
|
409
|
-
const appDataPaths = [
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
+
}
|
|
415
973
|
|
|
416
974
|
for (const p of appDataPaths) {
|
|
417
975
|
if (p && fs.existsSync(p)) {
|
|
@@ -435,20 +993,28 @@ function getPSHistory() {
|
|
|
435
993
|
if (fs.existsSync(histPath)) {
|
|
436
994
|
try {
|
|
437
995
|
const content = fs.readFileSync(histPath, 'utf8');
|
|
438
|
-
const lines = content.split('\n').slice(-
|
|
996
|
+
const lines = content.split('\n').slice(-200); // last 200 commands
|
|
439
997
|
const suspicious = lines.filter(l => {
|
|
440
998
|
const lower = l.toLowerCase();
|
|
441
|
-
|
|
999
|
+
const isSuspicious = lower.includes('downloadstring') ||
|
|
442
1000
|
lower.includes('downloadfile') || lower.includes('net.webclient') ||
|
|
443
1001
|
lower.includes('iex') || lower.includes('invoke-expression') ||
|
|
444
|
-
lower.includes('bypass') || lower.includes('hidden') ||
|
|
445
1002
|
lower.includes('-enc ') || lower.includes('encodedcommand') ||
|
|
446
|
-
lower.includes('certutil') || lower.includes('bitsadmin')
|
|
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;
|
|
447
1013
|
});
|
|
448
1014
|
if (suspicious.length > 0) {
|
|
449
1015
|
return `[!] Suspicious PowerShell commands found:\n${suspicious.join('\n')}`;
|
|
450
1016
|
}
|
|
451
|
-
return `Last
|
|
1017
|
+
return `Last 200 commands checked - no suspicious patterns found (${lines.length} lines in history)`;
|
|
452
1018
|
} catch {
|
|
453
1019
|
return 'Could not read PowerShell history';
|
|
454
1020
|
}
|
|
@@ -458,43 +1024,43 @@ function getPSHistory() {
|
|
|
458
1024
|
|
|
459
1025
|
function getDNSCache() {
|
|
460
1026
|
return ps(`
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
+
}
|
|
470
1036
|
`);
|
|
471
1037
|
}
|
|
472
1038
|
|
|
473
1039
|
function getRecentInstalls() {
|
|
474
1040
|
return ps(`
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
480
1046
|
`);
|
|
481
1047
|
}
|
|
482
1048
|
|
|
483
1049
|
function getSuspiciousProcesses() {
|
|
484
1050
|
return ps(`
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
1051
|
+
$suspicious = @(${SUSPICIOUS_PROCESSES.map(p => `'${p}'`).join(',')})
|
|
1052
|
+
$procs = Get-Process -ErrorAction SilentlyContinue |
|
|
1053
|
+
Select-Object Name, Id, Path, Company |
|
|
1054
|
+
Where-Object {
|
|
489
1055
|
$_.Name -in $suspicious -or
|
|
490
1056
|
($_.Path -and ($_.Path -match '\\\\Temp\\\\' -or $_.Path -match '\\\\tmp\\\\' -or $_.Path -match '\\\\Downloads\\\\'))
|
|
491
|
-
}
|
|
492
|
-
if ($procs) {
|
|
493
|
-
"[!] Suspicious processes found:"
|
|
494
|
-
$procs | Format-Table -AutoSize | Out-String
|
|
495
|
-
} else {
|
|
496
|
-
"No known suspicious processes found"
|
|
497
1057
|
}
|
|
1058
|
+
if ($procs) {
|
|
1059
|
+
"[!] Suspicious processes found:"
|
|
1060
|
+
$procs | Format-Table -AutoSize | Out-String
|
|
1061
|
+
} else {
|
|
1062
|
+
"No known suspicious processes found"
|
|
1063
|
+
}
|
|
498
1064
|
`);
|
|
499
1065
|
}
|
|
500
1066
|
|
|
@@ -505,6 +1071,7 @@ function getSuspiciousProcesses() {
|
|
|
505
1071
|
function linuxChecks() {
|
|
506
1072
|
const findings = [];
|
|
507
1073
|
|
|
1074
|
+
// --- Original checks ---
|
|
508
1075
|
findings.push({ check: 'SSH Login History', data: run('last -50 2>/dev/null || echo "last command not available"') });
|
|
509
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"') });
|
|
510
1077
|
findings.push({ check: 'User Accounts', data: run('cat /etc/passwd | grep -v nologin | grep -v /false') });
|
|
@@ -515,13 +1082,81 @@ function linuxChecks() {
|
|
|
515
1082
|
findings.push({ check: 'Listening Ports', data: run('ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null') });
|
|
516
1083
|
findings.push({ check: 'Running Services', data: run('systemctl list-units --type=service --state=running 2>/dev/null | head -40') });
|
|
517
1084
|
findings.push({ check: 'Suspicious IDE Artifacts', data: getIDEArtifacts() });
|
|
518
|
-
findings.push({ check: 'Unusual SUID Binaries', data: run('find / -perm -4000 -type f 2>/dev/null | grep -v -E "(sudo|passwd|ping|mount|su|chsh|chfn|newgrp|gpasswd|pkexec)" | head -20') });
|
|
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) });
|
|
519
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') });
|
|
520
|
-
findings.push({ check: 'Bash History (suspicious)', data: run('grep -
|
|
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() });
|
|
521
1103
|
|
|
522
1104
|
return findings;
|
|
523
1105
|
}
|
|
524
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
|
+
|
|
525
1160
|
// ============================================
|
|
526
1161
|
// macOS CHECKS
|
|
527
1162
|
// ============================================
|
|
@@ -529,75 +1164,150 @@ function linuxChecks() {
|
|
|
529
1164
|
function macChecks() {
|
|
530
1165
|
const findings = [];
|
|
531
1166
|
|
|
532
|
-
//
|
|
1167
|
+
// --- Original checks ---
|
|
533
1168
|
findings.push({ check: 'Login History', data: run('last -50 2>/dev/null || echo "last command not available"') });
|
|
534
|
-
|
|
535
|
-
// 2. Failed SSH/login attempts via unified log
|
|
536
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) });
|
|
537
|
-
|
|
538
|
-
// 3. Remote login (SSH) status
|
|
539
1170
|
findings.push({ check: 'Remote Login (SSH) Status', data: run('systemsetup -getremotelogin 2>/dev/null || echo "Cannot check remote login status"') });
|
|
540
|
-
|
|
541
|
-
// 4. Screen sharing / VNC / ARD
|
|
542
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"') });
|
|
543
|
-
|
|
544
|
-
// 5. User accounts
|
|
545
1172
|
findings.push({ check: 'User Accounts', data: run('dscl . list /Users | grep -v "^_" | grep -v daemon | grep -v nobody') });
|
|
546
|
-
|
|
547
|
-
// 6. Admin users
|
|
548
1173
|
findings.push({ check: 'Admin Users', data: run('dscl . -read /Groups/admin GroupMembership 2>/dev/null || echo "Cannot read admin group"') });
|
|
549
|
-
|
|
550
|
-
// 7. LaunchAgents (user-level persistence - PRIMARY backdoor vector on macOS)
|
|
551
1174
|
findings.push({ check: 'User LaunchAgents', data: getMacLaunchItems(path.join(HOME, 'Library', 'LaunchAgents')) });
|
|
552
|
-
|
|
553
|
-
// 8. System LaunchAgents
|
|
554
1175
|
findings.push({ check: 'System LaunchAgents', data: getMacLaunchItems('/Library/LaunchAgents') });
|
|
555
|
-
|
|
556
|
-
// 9. LaunchDaemons (root-level persistence)
|
|
557
1176
|
findings.push({ check: 'LaunchDaemons', data: getMacLaunchItems('/Library/LaunchDaemons') });
|
|
558
|
-
|
|
559
|
-
// 10. Login Items (GUI persistence)
|
|
560
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"') });
|
|
561
|
-
|
|
562
|
-
// 11. Crontabs
|
|
563
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"') });
|
|
564
|
-
|
|
565
|
-
// 12. SSH authorized_keys
|
|
566
1179
|
findings.push({ check: 'SSH Authorized Keys', data: getSSHKeys() });
|
|
567
|
-
|
|
568
|
-
// 13. Active connections
|
|
569
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') });
|
|
570
|
-
|
|
571
|
-
// 14. Listening ports
|
|
572
1181
|
findings.push({ check: 'Listening Ports', data: run('lsof -i -P -n 2>/dev/null | grep LISTEN | head -30') });
|
|
573
|
-
|
|
574
|
-
// 15. Profiles (MDM / configuration profiles - can enforce proxy, certs, etc.)
|
|
575
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"') });
|
|
576
|
-
|
|
577
|
-
// 16. Keychain - look for suspicious root certs
|
|
578
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"') });
|
|
579
|
-
|
|
580
|
-
// 17. Proxy settings
|
|
581
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"') });
|
|
582
|
-
|
|
583
|
-
// 18. Firewall status
|
|
584
1185
|
findings.push({ check: 'Firewall Status', data: run('/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null || echo "Cannot check firewall"') });
|
|
585
|
-
|
|
586
|
-
// 19. Suspicious IDE artifacts
|
|
587
1186
|
findings.push({ check: 'Suspicious IDE Artifacts', data: getIDEArtifacts() });
|
|
588
|
-
|
|
589
|
-
// 20. Shell history (suspicious patterns)
|
|
590
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"') });
|
|
591
|
-
|
|
592
|
-
// 21. Gatekeeper and SIP status
|
|
593
1188
|
findings.push({ check: 'Gatekeeper & SIP Status', data: run('spctl --status 2>/dev/null; csrutil status 2>/dev/null') });
|
|
594
|
-
|
|
595
|
-
// 22. TCC (privacy permissions) - what apps have accessibility/screen recording/etc.
|
|
596
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)"') });
|
|
597
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
|
+
|
|
598
1207
|
return findings;
|
|
599
1208
|
}
|
|
600
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
|
+
|
|
601
1311
|
/**
|
|
602
1312
|
* Read macOS LaunchAgent/LaunchDaemon plist files for suspicious entries
|
|
603
1313
|
*/
|
|
@@ -614,14 +1324,11 @@ function getMacLaunchItems(dirPath) {
|
|
|
614
1324
|
|
|
615
1325
|
for (const file of files) {
|
|
616
1326
|
const fullPath = path.join(dirPath, file);
|
|
617
|
-
// Use plutil to convert plist to readable format
|
|
618
1327
|
const content = run(`plutil -p "${fullPath}" 2>/dev/null`);
|
|
619
1328
|
|
|
620
|
-
// Flag non-Apple items
|
|
621
1329
|
const isApple = file.startsWith('com.apple.') || file.startsWith('com.openssh.');
|
|
622
1330
|
const label = isApple ? ' ' : '[!] ';
|
|
623
1331
|
|
|
624
|
-
// Extract program/command from plist output
|
|
625
1332
|
const programMatch = content.match(/"Program(?:Arguments)?" => (?:\[[\s\S]*?\]|"[^"]*")/);
|
|
626
1333
|
const program = programMatch ? programMatch[0].substring(0, 120) : '';
|
|
627
1334
|
|
|
@@ -642,6 +1349,7 @@ async function runAudit(options = {}) {
|
|
|
642
1349
|
const startTime = Date.now();
|
|
643
1350
|
const result = {
|
|
644
1351
|
ok: true,
|
|
1352
|
+
version: 2,
|
|
645
1353
|
platform: PLATFORM,
|
|
646
1354
|
hostname: os.hostname(),
|
|
647
1355
|
timestamp: new Date().toISOString(),
|
|
@@ -660,22 +1368,51 @@ async function runAudit(options = {}) {
|
|
|
660
1368
|
result.findings = linuxChecks();
|
|
661
1369
|
}
|
|
662
1370
|
|
|
663
|
-
// Extract unique IPs from findings for geo-lookup
|
|
664
|
-
const
|
|
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
|
+
|
|
665
1381
|
for (const f of result.findings) {
|
|
666
|
-
if (typeof f.data
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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);
|
|
673
1404
|
}
|
|
674
1405
|
}
|
|
675
1406
|
}
|
|
676
1407
|
}
|
|
677
1408
|
|
|
678
|
-
// Batch geo-IP lookup
|
|
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
|
+
|
|
679
1416
|
if (allIPs.size > 0 && !options.skipGeo) {
|
|
680
1417
|
const geoResults = await batchGeoIP([...allIPs]);
|
|
681
1418
|
result.geoIP = {};
|
|
@@ -689,7 +1426,6 @@ async function runAudit(options = {}) {
|
|
|
689
1426
|
org: geo.org
|
|
690
1427
|
};
|
|
691
1428
|
|
|
692
|
-
// Flag suspicious countries
|
|
693
1429
|
if (SUSPICIOUS_GEOS.includes(geo.countryCode)) {
|
|
694
1430
|
result.alerts.push({
|
|
695
1431
|
severity: 'HIGH',
|
|
@@ -701,34 +1437,59 @@ async function runAudit(options = {}) {
|
|
|
701
1437
|
}
|
|
702
1438
|
}
|
|
703
1439
|
|
|
704
|
-
|
|
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
|
|
705
1447
|
for (const f of result.findings) {
|
|
706
1448
|
if (typeof f.data === 'string') {
|
|
707
|
-
if (f.data.includes('[
|
|
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('[!]')) {
|
|
708
1456
|
result.alerts.push({
|
|
709
|
-
severity:
|
|
1457
|
+
severity: 'MEDIUM',
|
|
1458
|
+
check: f.check,
|
|
710
1459
|
message: `${f.check}: ${f.data.split('\n').find(l => l.includes('[!]')) || f.data.substring(0, 200)}`
|
|
711
1460
|
});
|
|
712
1461
|
}
|
|
713
1462
|
}
|
|
714
1463
|
}
|
|
715
1464
|
|
|
1465
|
+
// Severity score
|
|
1466
|
+
let severityScore = 0;
|
|
1467
|
+
for (const alert of result.alerts) {
|
|
1468
|
+
severityScore += SEVERITY_WEIGHTS[alert.severity] || 0;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
716
1471
|
// Summary
|
|
717
1472
|
result.summary = {
|
|
718
1473
|
totalChecks: result.findings.length,
|
|
719
1474
|
alerts: result.alerts.length,
|
|
720
1475
|
highSeverity: result.alerts.filter(a => a.severity === 'HIGH').length,
|
|
721
1476
|
mediumSeverity: result.alerts.filter(a => a.severity === 'MEDIUM').length,
|
|
722
|
-
|
|
1477
|
+
uniqueExternalIPv4: allIPv4.size,
|
|
1478
|
+
uniqueExternalIPv6: allIPv6.size,
|
|
723
1479
|
suspiciousGeoIPs: result.alerts.filter(a => a.ip).length,
|
|
1480
|
+
severityScore,
|
|
724
1481
|
duration: Date.now() - startTime
|
|
725
1482
|
};
|
|
726
1483
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
+
}
|
|
732
1493
|
|
|
733
1494
|
return result;
|
|
734
1495
|
}
|