@jishankai/solid-cli 1.0.0

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.
@@ -0,0 +1,572 @@
1
+ import { BaseAgent } from './BaseAgent.js';
2
+ import { executeShellCommand } from '../utils/commander.js';
3
+ import { getSignatureAssessment } from '../utils/signature.js';
4
+
5
+ /**
6
+ * ProcessAgent - Analyzes running processes for anomalies
7
+ */
8
+ export class ProcessAgent extends BaseAgent {
9
+ constructor() {
10
+ super('ProcessAgent');
11
+ this.systemPaths = [
12
+ // System directories
13
+ '/Applications',
14
+ '/System',
15
+ '/System/Applications',
16
+ '/System/Library',
17
+ '/System/Library/CoreServices',
18
+ '/System/Library/Frameworks',
19
+ '/Library',
20
+ '/Library/Apple',
21
+ '/Library/Application Support',
22
+ '/Library/Frameworks',
23
+ '/Library/PreferencePanes',
24
+ '/Library/LaunchAgents',
25
+ '/Library/LaunchDaemons',
26
+
27
+ // Standard Unix paths
28
+ '/bin',
29
+ '/sbin',
30
+ '/usr/bin',
31
+ '/usr/sbin',
32
+ '/usr/lib',
33
+ '/usr/libexec',
34
+ '/usr/local/bin',
35
+ '/usr/local/sbin',
36
+ '/usr/local/lib',
37
+ '/usr/local/share',
38
+ '/opt/homebrew/bin',
39
+ '/opt/local/bin',
40
+
41
+ // Developer tools
42
+ '/usr/bin/code',
43
+ '/Applications/Visual Studio Code.app',
44
+ '/Applications/Xcode.app',
45
+ '/Applications/Atom.app',
46
+ '/Applications/Sublime Text.app',
47
+ '/Developer',
48
+ '/Xcode',
49
+
50
+ // Common application directories
51
+ '/Applications/Microsoft Office',
52
+ '/Applications/Adobe',
53
+ '/Applications/Google Chrome.app',
54
+ '/Applications/Google Chrome',
55
+ '/Applications/Safari.app',
56
+ '/Applications/Firefox.app',
57
+ '/Applications/Opera.app',
58
+ '/Applications/Slack.app',
59
+ '/Applications/Discord.app',
60
+ '/Applications/Zoom.app',
61
+ '/Applications/Teams.app',
62
+ '/Applications/Skype.app',
63
+ '/Applications/Dropbox.app',
64
+ '/Applications/Spotify.app',
65
+ '/Applications/VLC.app',
66
+ '/Applications/QuickTime Player.app',
67
+ '/Applications/iTunes.app',
68
+ '/Applications/Preview.app',
69
+ '/Applications/TextEdit.app',
70
+ '/Applications/Activity Monitor.app',
71
+ '/Applications/Console.app',
72
+ '/Applications/System Preferences.app',
73
+ '/Applications/System Information.app',
74
+ '/Applications/Utilities',
75
+ '/Applications/Terminal.app',
76
+ '/Applications/iTerm.app',
77
+
78
+ // Homebrew paths
79
+ '/opt/homebrew',
80
+ '/usr/local/Cellar',
81
+ '/usr/local/Caskroom',
82
+
83
+ // Node.js and npm
84
+ '/usr/local/bin/node',
85
+ '/usr/local/bin/npm',
86
+ '/opt/homebrew/bin/node',
87
+ '/opt/homebrew/bin/npm',
88
+
89
+ // Python paths
90
+ '/usr/bin/python',
91
+ '/usr/bin/python3',
92
+ '/usr/local/bin/python',
93
+ '/usr/local/bin/python3',
94
+ '/opt/homebrew/bin/python',
95
+ '/opt/homebrew/bin/python3',
96
+
97
+ // Git
98
+ '/usr/bin/git',
99
+ '/usr/local/bin/git',
100
+ '/opt/homebrew/bin/git',
101
+
102
+ // Shell paths
103
+ '/bin/bash',
104
+ '/bin/zsh',
105
+ '/bin/fish',
106
+ '/bin/tcsh',
107
+ '/usr/local/bin/bash',
108
+ '/usr/local/bin/zsh',
109
+ '/opt/homebrew/bin/bash',
110
+ '/opt/homebrew/bin/zsh',
111
+
112
+ // macOS specific
113
+ '/System/Library/PrivateFrameworks',
114
+ '/System/Library/CoreServices/Finder.app',
115
+ '/System/Library/CoreServices/Dock.app',
116
+ '/System/Library/CoreServices/Menu Extras',
117
+ '/System/Library/CoreServices/Spotlight.app'
118
+ ];
119
+ this.systemProcessNames = [
120
+ // Core system processes
121
+ 'kernel_task', 'launchd', 'loginwindow',
122
+
123
+ // UI and desktop
124
+ 'Finder', 'SystemUIServer', 'WindowServer', 'Dock',
125
+ 'UserNotificationCenter', 'NotificationCenter', 'Spotlight',
126
+
127
+ // System services
128
+ 'cfprefsd', 'mds', 'mdworker', 'mds_stores', 'distnoted',
129
+ 'notifyd', 'powerd', 'tccd', 'locationd', 'opendirectoryd',
130
+ 'syslogd', 'logd', 'mDNSResponder', 'configd', 'systemstats',
131
+ 'coreaudiod', 'bluetoothd', 'airportd', 'securityd', 'warmd',
132
+ 'hidd', 'fseventsd', 'pbs', 'pboard',
133
+
134
+ // Network services
135
+ 'networkd', 'wifid', 'socketfilterfw',
136
+
137
+ // Graphics and display
138
+ 'WindowManager', 'coreservicesd', 'SkyLight',
139
+
140
+ // File system
141
+ 'diskarbitrationd', 'filecoordinationd',
142
+
143
+ // Security
144
+ 'authd', 'trustd', 'amfid',
145
+
146
+ // Development tools
147
+ 'node', 'python', 'python3', 'git', 'ssh',
148
+ 'bash', 'zsh', 'fish', 'tcsh',
149
+
150
+ // Common applications
151
+ 'Chrome', 'Safari', 'firefox', 'VSCode', 'code',
152
+ 'iTerm', 'Terminal',
153
+
154
+ // System utilities
155
+ 'Activity Monitor', 'Preview', 'Console'
156
+ ];
157
+ this.trustedSystemCommands = new Set([
158
+ // Core system processes
159
+ 'launchd', 'kernel_task', 'kernelinit', 'kextd', 'kerneld',
160
+
161
+ // macOS system services
162
+ 'WindowServer', 'SystemUIServer', 'Dock', 'Finder', 'loginwindow',
163
+ 'UserNotificationCenter', 'Spotlight', 'NotificationCenter',
164
+
165
+ // Background services (daemons)
166
+ 'mds', 'mdworker', 'mds_stores', 'distnoted', 'notifyd', 'powerd',
167
+ 'cfprefsd', 'usernoted', 'tccd', 'locationd', 'opendirectoryd',
168
+ 'syslogd', 'logd', 'mDNSResponder', 'configd', 'systemstats',
169
+ 'coreaudiod', 'bluetoothd', 'airportd', 'securityd', 'warmd',
170
+ 'hidd', 'cmiodalassistants', 'launchservicesd', 'iconservicesagent',
171
+ 'lskdd', 'lsd', 'fseventsd', 'pbs', 'pboard', 'pasteboardd',
172
+
173
+ // Network and connectivity
174
+ 'networkd', 'wifid', 'socketfilterfw', 'natd', 'pppd',
175
+ 'racoon', 'racoonctl', 'vpnd', 'netbiosd',
176
+
177
+ // Graphics and display
178
+ 'WindowManager', 'coreservicesd', 'SkyLight', 'HIToolbox',
179
+ 'CGSession', 'ScreenSaverEngine', 'SystemPreferences',
180
+
181
+ // File system and storage
182
+ 'fsapfs', 'hfs_mount', 'autodiskmount', 'diskarbitrationd',
183
+ 'diskmanagementd', 'filecoordinationd', 'synthesisd',
184
+
185
+ // Security and authentication
186
+ 'authd', 'authorizationhost', 'ocspd', 'trustd', 'securityd',
187
+ 'codesign', 'taskgated', 'sandboxd', 'amfid',
188
+
189
+ // Development tools (commonly installed)
190
+ 'node', 'npm', 'npx', 'yarn', 'pnpm', 'pnpx',
191
+ 'pip', 'pip3', 'python', 'python3', 'pip3',
192
+ 'git', 'git-credential-manager', 'git-gui', 'gitk',
193
+ 'ssh', 'ssh-agent', 'ssh-add', 'scp', 'sftp', 'rsync',
194
+ 'curl', 'wget', 'http', 'https', 'ftp',
195
+ 'brew', 'ruby', 'perl', 'java', 'javac', 'gradle', 'maven',
196
+ 'docker', 'docker-compose', 'kubectl', 'helm', 'minikube',
197
+ 'make', 'cmake', 'gcc', 'clang', 'clang++',
198
+ 'go', 'gorun', 'gobuild', 'rust', 'rustc', 'cargo',
199
+ 'php', 'composer', 'laravel', 'symfony',
200
+ 'typescript', 'ts-node', 'tsc', 'tsx',
201
+ 'eslint', 'prettier', 'jest', 'mocha', 'chai',
202
+ 'webpack', 'vite', 'parcel', 'rollup',
203
+ 'nodemon', 'pm2', 'forever', 'supervisor',
204
+ 'redis-server', 'redis-cli', 'mongod', 'mongo', 'mysql',
205
+ 'psql', 'postgres', 'sqlite3', 'mongo-express',
206
+ 'nginx', 'apache2', 'httpd', 'caddy', 'traefik',
207
+
208
+ // Common applications
209
+ 'Chrome', 'chromium', 'Safari', 'firefox', 'Opera',
210
+ 'Slack', 'Discord', 'Zoom', 'Teams', 'Skype',
211
+ 'VSCode', 'code', 'Xcode', 'Atom', 'Sublime_Text',
212
+ 'iTerm', 'Terminal', 'bash', 'zsh', 'fish', 'tcsh',
213
+
214
+ // macOS utilities
215
+ 'Activity Monitor', 'Preview', 'TextEdit', 'QuickLook',
216
+ 'ArchiveUtility', 'DiskUtility', 'Keychain Access',
217
+ 'System Information', 'Console', 'Automator',
218
+
219
+ // Third-party common software
220
+ 'Dropbox', 'GoogleDrive', 'OneDrive', 'Box',
221
+ 'Spotify', 'VLC', 'QuickTimePlayer', 'iTunes',
222
+ 'Microsoft Word', 'Microsoft Excel', 'Microsoft PowerPoint',
223
+ 'Adobe Acrobat', 'Adobe Reader', 'Photoshop',
224
+
225
+ // System maintenance
226
+ 'periodic', 'daily', 'weekly', 'monthly', 'launchctl',
227
+ 'systemsetup', 'softwareupdate', 'pmset', 'caffeinate',
228
+
229
+ // Input and peripherals
230
+ 'IOHIDSystem', 'IOHIDEventDriver', 'USBAgent',
231
+ 'BluetoothUIServer', 'AudioComponentRegistrar',
232
+
233
+ // Time and sync
234
+ 'timed', 'clockd', 'ntpd', 'networktime',
235
+
236
+ // Print and scanning
237
+ 'cupsd', 'cups-browsed', 'hpmud', 'ImageCaptureExtension',
238
+
239
+ // Accessibility
240
+ 'VoiceOver', 'AXUIServer', 'accessibilityd',
241
+
242
+ // Backup and recovery
243
+ 'TimeMachine', 'backupd', 'tmutil', 'rsync',
244
+
245
+ // Additional macOS system daemons
246
+ 'mediaremoted', 'watchdogd', 'kernelmanagerd', 'thermalmonitord',
247
+ 'apsd', 'apsrelayd', 'applepushserviced', 'com.apple.CommCenter',
248
+ 'commcenter', 'commcenterd', 'mobileassetd', 'assetcache',
249
+ 'assetcachingd', 'cacheserverd', 'cached', 'cachecheck',
250
+ 'logind', 'logindisplay', 'loginwindow', 'screensharingd',
251
+ 'remoted', 'remotepairingd', 'remotepairingtool',
252
+ 'sharingd', 'screencaptured', 'screenshotd',
253
+ 'corebrightnessd', 'backlightd', 'brightnessd',
254
+ 'controlcenterd', 'controlcenter', 'spotlightd',
255
+ 'searchpartyd', 'searchindexer', 'mds_stores',
256
+ 'useractivityd', 'useractivityagent', 'useractivitymonitor',
257
+ 'timed', 'timed_sync', 'networkd', 'networkd_privileged',
258
+ 'wifid', 'wirelessprovisioningd', 'wirelessproxd',
259
+ 'bluetoothd', 'bluetoothaudiod', 'bluetoothUIServer',
260
+ 'audioaccessoryd', 'audioaccessoryd',
261
+ 'coreaudiod', 'coreaudiohelperd', 'coreaudioaopd',
262
+ 'hidd', 'hidd_helper', 'hidd_system',
263
+ 'universalaccessd', 'accessibilityd', 'AXUIServer',
264
+ 'voiceover', 'voiceoverd',
265
+ 'distnoted', 'distributednotificationcenter',
266
+ 'nsurlsessiond', 'nsurlstoraged', 'webkitnetworkprocess',
267
+ 'webkitwebcontentprocess', 'webkitpluginprocess',
268
+ 'plugind', 'pluginmanagerd',
269
+ 'launchservicesd', 'lsd', 'lskdd',
270
+ 'iconservicesagent', 'iconservicesd',
271
+ 'pasteboardd', 'pboard', 'pbs',
272
+ 'cfprefsd', 'preferencesd', 'systempreferencesd',
273
+ 'tccd', 'tccutil', 'privacyd',
274
+ 'locationd', 'locationservicesd', 'geod',
275
+ 'compassd', 'magnetometerd', 'accelerometerd',
276
+ 'gyroscoped', 'barometerd',
277
+ 'fseventsd', 'fsapfs', 'filecoordinationd',
278
+ 'synthesisd', 'syncdefaultsd', 'syncservicesd',
279
+ 'diskarbitrationd', 'diskmanagementd', 'diskimagesd',
280
+ 'hdiutil', 'hdid', 'hdihelperd',
281
+ 'authd', 'authorizationhost', 'authtrampoline',
282
+ 'securityd', 'securityagent', 'securityhelperd',
283
+ 'codesign', 'codesign_allocate', 'taskgated',
284
+ 'amfid', 'amfite', 'applemobilefileintegrity',
285
+ 'sandboxd', 'sandboxd_helper', 'seatbeltd',
286
+ 'trustd', 'trustevaluationagent',
287
+ 'ocspd', 'ocsp_helperd',
288
+ 'certificateauthorityd', 'certificated',
289
+ 'keychaind', 'keychainaccesshelperd',
290
+ 'smartcardservicesd', 'tokend',
291
+ 'biometrickitd', 'touchid', 'faced',
292
+ 'corecrypto', 'corecryptod',
293
+ 'kernelmanagerd', 'kextd', 'kextcache',
294
+ 'systemstats', 'systemstatsd',
295
+ 'powerd', 'powermanagementd', 'pmset',
296
+ 'thermalmonitord', 'thermalmonitord_helper',
297
+ 'warmd', 'warmd_helper',
298
+ 'configd', 'configd_helper',
299
+ 'networksetup', 'networksetup_helper',
300
+ 'scutil', 'scutil_helper',
301
+ 'ifconfig', 'ifconfig_helper',
302
+ 'netstat', 'netstat_helper',
303
+ 'ping', 'ping_helper',
304
+ 'traceroute', 'traceroute_helper',
305
+ 'nslookup', 'nslookup_helper',
306
+ 'dig', 'dig_helper',
307
+ 'host', 'host_helper'
308
+ ]);
309
+ }
310
+
311
+ async analyze() {
312
+ const processes = await this.getProcessDetails();
313
+ let findings = this.analyzeProcesses(processes);
314
+
315
+ // Reduce false positives: apply signature/Gatekeeper trust to downgrade or drop weak-signal findings.
316
+ findings = await this.applyTrustHeuristics(findings);
317
+
318
+ this.results = {
319
+ agent: this.name,
320
+ timestamp: new Date().toISOString(),
321
+ totalProcesses: processes.length,
322
+ findings,
323
+ overallRisk: this.calculateOverallRisk(findings)
324
+ };
325
+
326
+ return this.results;
327
+ }
328
+
329
+ /**
330
+ * Get detailed process information
331
+ */
332
+ async getProcessDetails() {
333
+ // Get process list with parent relationships
334
+ const psOutput = await executeShellCommand('ps -axo pid,ppid,user,comm');
335
+ const lines = psOutput.split('\n').slice(1); // Skip header
336
+
337
+ const processes = [];
338
+
339
+ for (const line of lines) {
340
+ if (!line.trim()) continue;
341
+
342
+ const parts = line.trim().split(/\s+/);
343
+ if (parts.length >= 4) {
344
+ const pid = parseInt(parts[0]);
345
+ const ppid = parseInt(parts[1]);
346
+ const user = parts[2];
347
+ const command = parts.slice(3).join(' ');
348
+
349
+ // Try to get executable path with multiple fallbacks
350
+ const fullPath = await this.getExecutablePath(pid, command);
351
+
352
+ processes.push({
353
+ pid,
354
+ ppid,
355
+ user,
356
+ name: command.split('/').pop().split(' ')[0],
357
+ command,
358
+ path: fullPath
359
+ });
360
+ }
361
+ }
362
+
363
+ return processes;
364
+ }
365
+
366
+ /**
367
+ * Resolve executable path with fallbacks to reduce false positives
368
+ */
369
+ async getExecutablePath(pid, commandFallback) {
370
+ // Use ps-based lookups only to avoid macOS privacy prompts from lsof
371
+ try {
372
+ const cmdOutput = await executeShellCommand(
373
+ `ps -p ${pid} -o command= 2>/dev/null`,
374
+ { quiet: true }
375
+ );
376
+ if (cmdOutput) {
377
+ const candidate = cmdOutput.trim().split(' ')[0];
378
+ if (candidate.startsWith('/')) return candidate;
379
+ }
380
+ } catch (error) {
381
+ // ignore
382
+ }
383
+
384
+ try {
385
+ const commOutput = await executeShellCommand(
386
+ `ps -p ${pid} -o comm= 2>/dev/null`,
387
+ { quiet: true }
388
+ );
389
+ if (commOutput) {
390
+ const candidate = commOutput.trim().split(' ')[0];
391
+ if (candidate.startsWith('/')) return candidate;
392
+ }
393
+ } catch (error) {
394
+ // ignore
395
+ }
396
+
397
+ // Fallback to the short command we already have
398
+ return commandFallback;
399
+ }
400
+
401
+ /**
402
+ * Analyze processes for suspicious patterns
403
+ */
404
+ analyzeProcesses(processes) {
405
+ const findings = [];
406
+
407
+ for (const proc of processes) {
408
+ const risks = [];
409
+ let riskLevel = 'low';
410
+ const hasAbsolutePath = proc.path && proc.path.startsWith('/');
411
+ const isTrustedPath = hasAbsolutePath && this.systemPaths.some(sysPath => proc.path.startsWith(sysPath));
412
+ const isTrustedCommand = this.trustedSystemCommands.has(proc.name);
413
+ const isUserPath = hasAbsolutePath && proc.path.includes('/Users/');
414
+
415
+ // Check 1: System process name but running from user directory
416
+ if (hasAbsolutePath && this.systemProcessNames.includes(proc.name)) {
417
+ if (isUserPath) {
418
+ risks.push('System process name running from user directory');
419
+ riskLevel = 'high';
420
+ }
421
+ }
422
+
423
+ // Check 2: Process name doesn't match path
424
+ const pathBasename = hasAbsolutePath ? proc.path.split('/').pop().split(' ')[0] : '';
425
+ if (hasAbsolutePath && pathBasename && proc.name !== pathBasename) {
426
+ risks.push(`Process name mismatch (name: ${proc.name}, path: ${pathBasename})`);
427
+ riskLevel = 'medium';
428
+ }
429
+
430
+ // Check 3: Non-system path execution
431
+ if (hasAbsolutePath && !isTrustedPath && proc.path !== proc.command) {
432
+ risks.push('Running from non-standard location');
433
+
434
+ // Elevate risk if in hidden directory
435
+ if (proc.path.includes('/.')) {
436
+ risks.push('Running from hidden directory');
437
+ riskLevel = 'high';
438
+ } else if (isUserPath) {
439
+ riskLevel = 'medium';
440
+ } else {
441
+ // keep as low unless combined with other risks
442
+ riskLevel = riskLevel === 'low' ? 'medium' : riskLevel;
443
+ }
444
+ }
445
+
446
+ // Check 4: Suspicious parent process
447
+ const parent = processes.find(p => p.pid === proc.ppid);
448
+ if (parent && hasAbsolutePath && !isTrustedPath) {
449
+ if (parent.name === 'bash' || parent.name === 'sh' || parent.name === 'python') {
450
+ risks.push(`Spawned by shell: ${parent.name}`);
451
+ riskLevel = 'medium';
452
+ }
453
+ }
454
+
455
+ // Check 5: Root/elevated processes from user paths
456
+ if (hasAbsolutePath && (proc.user === 'root' || proc.user === '_coreaudiod') && proc.path.includes('/Users/')) {
457
+ risks.push('Elevated privileges from user directory');
458
+ riskLevel = 'high';
459
+ }
460
+
461
+ // Check 6: Hidden or obfuscated names
462
+ if (proc.name.startsWith('.') && !isTrustedCommand) {
463
+ risks.push('Hidden process name (starts with dot)');
464
+ riskLevel = 'high';
465
+ } else if (proc.name.match(/^[a-z]{1,2}[0-9]{6,}$/) && !isTrustedCommand) {
466
+ // Pattern like "ab123456" - likely obfuscated
467
+ risks.push('Suspicious obfuscated process name pattern');
468
+ riskLevel = 'high';
469
+ } else if (proc.name.match(/^[0-9a-f]{16,}$/i) && !isTrustedCommand) {
470
+ // Hex-like pattern - likely obfuscated
471
+ risks.push('Suspicious hex-like process name pattern');
472
+ riskLevel = 'high';
473
+ }
474
+
475
+ // If we only have a trusted command name and no absolute path, avoid flagging
476
+ if (!hasAbsolutePath && isTrustedCommand && risks.length === 0) {
477
+ continue;
478
+ }
479
+
480
+ // Only report if there are meaningful risks (high risk or multiple signals)
481
+ const shouldReport = risks.length > 1 || riskLevel === 'high';
482
+ if (shouldReport) {
483
+ findings.push({
484
+ type: 'suspicious_process',
485
+ pid: proc.pid,
486
+ name: proc.name,
487
+ path: proc.path,
488
+ user: proc.user,
489
+ ppid: proc.ppid,
490
+ parentName: parent?.name || 'unknown',
491
+ risks,
492
+ risk: riskLevel,
493
+ description: `Process ${proc.name} (${proc.pid}): ${risks.join(', ')}`
494
+ });
495
+ }
496
+ }
497
+
498
+ return findings;
499
+ }
500
+
501
+ /**
502
+ * Apply code-signing/Gatekeeper trust to reduce false positives.
503
+ *
504
+ * Strategy:
505
+ * - Only evaluate findings already considered "report-worthy".
506
+ * - If an executable is Gatekeeper-accepted and not showing strong malware signals
507
+ * (hidden dir, impersonation, elevated-from-user-home, obfuscation), downgrade/drop.
508
+ *
509
+ * @param {Array} findings
510
+ * @returns {Promise<Array>}
511
+ */
512
+ async applyTrustHeuristics(findings) {
513
+ const signatureCache = new Map();
514
+
515
+ const enriched = await Promise.all(findings.map(async (finding) => {
516
+ const assessment = await getSignatureAssessment(finding.path, signatureCache);
517
+
518
+ // Attach trust metadata for transparency (useful in reports)
519
+ const trust = {
520
+ spctlAccepted: assessment.spctlAccepted,
521
+ teamIdentifier: assessment.teamIdentifier,
522
+ signedByApple: assessment.signedByApple,
523
+ signedByDeveloperId: assessment.signedByDeveloperId
524
+ };
525
+
526
+ const strongSignals = [
527
+ 'System process name running from user directory',
528
+ 'Running from hidden directory',
529
+ 'Elevated privileges from user directory',
530
+ 'Hidden process name (starts with dot)',
531
+ 'Suspicious obfuscated process name pattern',
532
+ 'Suspicious hex-like process name pattern'
533
+ ];
534
+
535
+ const hasStrongSignal = (finding.risks || []).some(r => strongSignals.some(s => r.includes(s)));
536
+
537
+ // Apple-signed binaries in system locations with only weak signals should be dropped outright
538
+ const isAppleSystem = assessment.signedByApple && finding.path && (
539
+ finding.path.startsWith('/System') || finding.path.startsWith('/usr/libexec') || finding.path.startsWith('/usr/sbin')
540
+ );
541
+ if (isAppleSystem && !hasStrongSignal) {
542
+ return null;
543
+ }
544
+
545
+ // If Gatekeeper accepts it and we only have weak heuristics, downgrade.
546
+ if (assessment.spctlAccepted && !hasStrongSignal) {
547
+ // Downgrade medium to low; keep high as-is.
548
+ if (finding.risk === 'medium') {
549
+ return {
550
+ ...finding,
551
+ risk: 'low',
552
+ trust,
553
+ securityNote: 'Gatekeeper accepted (spctl). Downgraded to reduce false positives.'
554
+ };
555
+ }
556
+
557
+ return {
558
+ ...finding,
559
+ trust
560
+ };
561
+ }
562
+
563
+ return {
564
+ ...finding,
565
+ trust
566
+ };
567
+ }));
568
+
569
+ // Drop low-risk findings entirely to reduce noise (they were only medium before trust evaluation).
570
+ return enriched.filter(f => f && f.risk !== 'low');
571
+ }
572
+ }