endpointsecurity_rs/
lib.rs

1//! endpointsecurity-rs
2//!
3//! This crate provides safe bindings to the the [OSX Endpoint Security API](https://developer.apple.com/documentation/endpointsecurity).
4
5//! This crates operators over crossbeam channels where you can subscribe to the events you're interested in.
6//!
7//! Not all events are supported. If you want a event to be added, open an issue on our [github](https://github.com/SubconsciousCompute/endpointsecurity-rs) repo.
8//!
9
10use std::ffi::CStr;
11
12use crossbeam::channel;
13
14use block::ConcreteBlock;
15
16mod sys {
17    #![allow(non_upper_case_globals)]
18    #![allow(non_camel_case_types)]
19    #![allow(non_snake_case)]
20    #![allow(unused)]
21    #![allow(clippy::all)]
22    include!("./sys.rs");
23}
24
25#[allow(unused)]
26mod bsm;
27
28macro_rules! es_string_to_string {
29    ($ex: expr) => {
30        CStr::from_ptr($ex).to_string_lossy().to_string()
31    };
32}
33
34macro_rules! es_string_to_opt_string {
35    ($ex: expr) => {
36        if $ex.is_null() {
37            None
38        } else {
39            Some(CStr::from_ptr($ex).to_string_lossy().to_string())
40        }
41    };
42}
43
44/// Possible errors returned if [EsClient::new()] fails
45#[derive(Debug)]
46pub enum EsClientCreateError {
47    /// Arguments to [EsClient] are invalid
48    InvalidArgument = 1,
49    /// Internal Endpoint Security error
50    Internal,
51    /// Executable isn't signed with required entitlements
52    NotEntitled,
53    /// Operation not permitted
54    NotPermited,
55    /// Executable didn't run as root
56    NotPrivileged,
57    /// Too many clients are connected to Endpoint Security
58    TooManyClients,
59}
60
61impl EsClientCreateError {
62    fn from_u32(code: u32) -> Option<EsClientCreateError> {
63        match code {
64            1 => Some(EsClientCreateError::InvalidArgument),
65            2 => Some(EsClientCreateError::Internal),
66            3 => Some(EsClientCreateError::NotEntitled),
67            4 => Some(EsClientCreateError::NotPermited),
68            5 => Some(EsClientCreateError::NotPrivileged),
69            6 => Some(EsClientCreateError::TooManyClients),
70            _ => None,
71        }
72    }
73}
74
75impl std::fmt::Display for EsClientCreateError {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        let err_str = match self {
78            EsClientCreateError::InvalidArgument => "Error: Invalid Arguments were provided.",
79            EsClientCreateError::Internal => "Error: Communication with ES subsystem failed.",
80            EsClientCreateError::NotEntitled => "Error: Caller is not properly entitled to connect.",
81            EsClientCreateError::NotPermited => "Error: Caller lacks Transparency, Consent, and Control (TCC) approval from the user.",
82            EsClientCreateError::NotPrivileged => "Error: Must be run as root",
83            EsClientCreateError::TooManyClients => "Error: Too many connected clients",
84        };
85
86        f.write_str(err_str)
87    }
88}
89
90impl std::error::Error for EsClientCreateError {}
91
92/// All the events supported by Endpoint Security, see [more](https://developer.apple.com/documentation/endpointsecurity/event_types)
93///
94/// *README*: While all events are supported by the crate, only few have [EsEventData] types.
95/// If one of the event your interested in is missing, please send us a PR or open an issue on github.
96#[derive(Copy, Clone, Debug, Eq, PartialEq)]
97#[repr(u32)]
98pub enum EsEventType {
99    AuthExec,
100    AuthOpen,
101    AuthKExtLoad,
102    AuthMMap,
103    AuthMProtect,
104    AuthMount,
105    AuthRename,
106    AuthSignal,
107    AuthUnlink,
108    NotifyExec,
109    NotifyOpen,
110    NotifyFork,
111    NotifyClose,
112    NotifyCreate,
113    NotifyExchangeData,
114    NotifyExit,
115    NotifyGetTask,
116    NotifyKExtLoad,
117    NotifyKExtUnload,
118    NotifyLink,
119    NotifyMMap,
120    NotifyMProtect,
121    NotifyMount,
122    NotifyUnmount,
123    NotifyIOKitOpen,
124    NotifyRename,
125    NOtifySetAttrList,
126    NotifySetExtAttr,
127    NotifySetFlags,
128    NotifySetMode,
129    NotifySetOwner,
130    NotifySignal,
131    NotifyUnlink,
132    NotifyWrite,
133    AuthFileProviderMaterialize,
134    NotifyFileProviderMaterialize,
135    AuthFileProviderUpdate,
136    NotifyFileProviderUpdate,
137    AuthReadLink,
138    NotifyReadLink,
139    AuthTruncate,
140    NotifyTruncate,
141    AuthLink,
142    NotifyLookup,
143    AuthCreate,
144    AuthSetAttrList,
145    AuthSetExtAttr,
146    AuthSetFlags,
147    AuthSetMode,
148    AuthSetOwner,
149    AuthChdir,
150    NotifyChdir,
151    AuthGetAttrList,
152    NotifyGetAttrList,
153    NotifyStat,
154    NotifyAccess,
155    AuthChroot,
156    NotifyChroot,
157    AuthUtimes,
158    NotifyUtimes,
159    AuthClone,
160    NotifyClone,
161    NotifyFcntl,
162    AuthGetExtAttr,
163    NotifyGetExtAttr,
164    AuthListenExtAttr,
165    NotifyListenExtAttr,
166    AuthReadDir,
167    NotifyReadDir,
168    AuthDeleteExtAttr,
169    NotifyDeleteExtAttr,
170    AuthFsGetPath,
171    NotifyFsGetPath,
172    NotifyDup,
173    AuthSetTime,
174    NotifySetTime,
175    NotifyUIPCBind,
176    AuthUIPCBind,
177    NotifyUIPCConnect,
178    AuthUIPCConnect,
179    AuthExchangeData,
180    AuthSetACL,
181    NotifySetACL,
182    NotifyPTYGrant,
183    NotifyPTYClose,
184    AuthProcCheck,
185    NotifyProcCheck,
186    AuthGetTask,
187    AuthSearchFs,
188    NotifySearchFs,
189    AuthFcntl,
190    AuthIOKitOpen,
191    AuthProcSuspendResume,
192    NotifyProcSuspendResume,
193    NotifyCsInvalidDate,
194    NotifyGetTaskName,
195    NotfiyTrace,
196    NotifyRemoteThreadCreate,
197    AuthRemount,
198    NotifyRemount,
199    AuthGetTaskRead,
200    NotifyGetTaskRead,
201    NotifyGetTaskInspect,
202    NotifySetUid,
203    NotifySetGid,
204    NotifySetEUid,
205    NotifySetEGuid,
206    NotifySetREUid,
207    NotifySetREGuid,
208    AuthCopyFile,
209    NotifyCopyFile,
210    NotifyAuthentication,
211    NotifyXPMalwareDetected,
212    NotifyXPMalwareRemediated,
213    NotifyLWSessionLogin,
214    NotifyLWSessionLogout,
215    NotifyLWSessionLock,
216    NotifyLWSessionUnlock,
217    NotifyScreenSharingAttach,
218    AuthScreenSharingAttach,
219    NotifyOpenSSHLogin,
220    NotifyOpenSSHLogout,
221    NotifyBTMLaunchItemAdd,
222    NotifyBTMLaunchItemRemove,
223    Last,
224}
225
226/// The event type
227#[derive(Debug, PartialEq)]
228pub enum EsActionType {
229    Auth,
230    Notify,
231}
232
233#[repr(u32)]
234pub enum EsMutePath {
235    Prefix,
236    Literal,
237}
238
239/// Info returned by Notify/Auth Rename events.
240#[derive(Debug)]
241pub struct EsRename {
242    /// source file to rename
243    pub source: EsFile,
244    /// destination of renamed file if filename with same name already exists
245    pub destination_existing: Option<EsFile>,
246    /// destination of renamed file if filename with same name doesn't exists
247    pub destintaion_newpath: Option<(EsFile, String)>,
248}
249
250/// Status returned by Es on NotifySSH events
251#[derive(Debug)]
252pub enum EsSSHLoginResult {
253    LoginExceedMaxTries,
254    LoginRootDenied,
255    AuthSuccess,
256    FailNone,
257    FailPasswd,
258    FailKBDInt,
259    FailPubKey,
260    FailHostBased,
261    FailGSSApi,
262    InvalidUser,
263}
264
265#[derive(Debug)]
266pub enum EsAddressType {
267    None,
268    Ipv4(std::net::Ipv4Addr),
269    Ipv6(std::net::Ipv6Addr),
270    NamedSocket(String),
271}
272
273impl EsAddressType {
274    fn parse(str: &sys::es_string_token_t, ty: u32) -> Self {
275        if ty == 0 {
276            EsAddressType::None
277        } else {
278            let addr_str = unsafe {
279                std::ffi::CStr::from_ptr(str.data)
280                    .to_string_lossy()
281                    .to_string()
282            };
283            match ty {
284                1 => EsAddressType::Ipv4(addr_str.parse().unwrap()),
285                2 => EsAddressType::Ipv6(addr_str.parse().unwrap()),
286                _ => panic!("Shouldn't reach here"),
287            }
288        }
289    }
290}
291
292#[derive(Debug)]
293pub struct EsSshLogin {
294    pub success: bool,
295    pub result: EsSSHLoginResult,
296    pub source_address: EsAddressType,
297    pub username: String,
298    pub uid: Option<u32>,
299}
300
301impl From<&sys::es_event_openssh_login_t> for EsSshLogin {
302    fn from(value: &sys::es_event_openssh_login_t) -> Self {
303        let username = unsafe {
304            std::ffi::CStr::from_ptr(value.username.data)
305                .to_string_lossy()
306                .to_string()
307        };
308
309        let result = match value.result_type {
310            0 => EsSSHLoginResult::LoginExceedMaxTries,
311            1 => EsSSHLoginResult::LoginRootDenied,
312            2 => EsSSHLoginResult::AuthSuccess,
313            3 => EsSSHLoginResult::FailNone,
314            4 => EsSSHLoginResult::FailPasswd,
315            5 => EsSSHLoginResult::FailKBDInt,
316            6 => EsSSHLoginResult::FailPubKey,
317            7 => EsSSHLoginResult::FailHostBased,
318            8 => EsSSHLoginResult::FailGSSApi,
319            9 => EsSSHLoginResult::InvalidUser,
320            _ => panic!("Should never reach this case"),
321        };
322
323        Self {
324            success: value.success,
325            username,
326            uid: (if value.has_uid {
327                unsafe { Some(value.uid.uid) }
328            } else {
329                None
330            }),
331            result,
332            source_address: EsAddressType::parse(&value.source_address, value.source_address_type),
333        }
334    }
335}
336
337#[derive(Debug)]
338pub struct EsSSHLogout {
339    pub source_address: EsAddressType,
340    pub username: String,
341    pub uid: u32,
342}
343
344impl From<&sys::es_event_openssh_logout_t> for EsSSHLogout {
345    fn from(value: &sys::es_event_openssh_logout_t) -> Self {
346        Self {
347            username: unsafe {
348                std::ffi::CStr::from_ptr(value.username.data)
349                    .to_string_lossy()
350                    .to_string()
351            },
352            uid: value.uid,
353            source_address: EsAddressType::parse(&value.source_address, value.source_address_type),
354        }
355    }
356}
357
358#[derive(Debug)]
359pub struct EsUnlinkFile {
360    pub parent_dir: EsFile,
361    pub target: EsFile,
362}
363
364impl From<sys::es_event_unlink_t> for EsUnlinkFile {
365    fn from(value: sys::es_event_unlink_t) -> Self {
366        unsafe {
367            Self {
368                parent_dir: value.parent_dir.as_ref().unwrap().into(),
369                target: value.target.as_ref().unwrap().into(),
370            }
371        }
372    }
373}
374
375#[derive(Debug)]
376pub struct EsCopyFile {
377    pub source: Option<EsFile>,
378    pub target_file: Option<EsFile>,
379    pub target_dir: Option<EsFile>,
380    pub target_name: String,
381    pub mode: u16,
382    pub flags: i32,
383}
384
385impl From<sys::es_event_copyfile_t> for EsCopyFile {
386    fn from(value: sys::es_event_copyfile_t) -> Self {
387        unsafe {
388            Self {
389                source: value.source.as_ref().map(|src| src.into()),
390                target_file: value.target_file.as_ref().map(|tar| tar.into()),
391                target_dir: value.target_dir.as_ref().map(|t_dir| t_dir.into()),
392                target_name: CStr::from_ptr(value.target_name.data)
393                    .to_string_lossy()
394                    .to_string(),
395                mode: value.mode,
396                flags: value.flags,
397            }
398        }
399    }
400}
401
402#[derive(Debug)]
403pub enum EsDestination {
404    ExistingFile(EsFile),
405    NewPath(EsCreateMetadata),
406}
407
408#[derive(Debug)]
409pub struct EsCreateMetadata {
410    pub dir: EsFile,
411    pub filename: String,
412    pub mode: u16,
413}
414
415#[derive(Debug)]
416pub struct EsCreate {
417    pub destination: EsDestination,
418}
419
420impl From<sys::es_event_create_t> for EsCreate {
421    fn from(value: sys::es_event_create_t) -> Self {
422        let destination = match value.destination_type {
423            0 => unsafe {
424                EsDestination::ExistingFile(
425                    value.destination.existing_file.as_ref().unwrap().into(),
426                )
427            },
428            1 => {
429                let new_path = unsafe { value.destination.new_path };
430                unsafe {
431                    EsDestination::NewPath(EsCreateMetadata {
432                        dir: new_path.dir.as_ref().unwrap().into(),
433                        filename: es_string_to_string!(new_path.filename.data),
434                        mode: new_path.mode,
435                    })
436                }
437            }
438            _ => {
439                panic!("EsCreate: destination type out of range")
440            }
441        };
442        Self { destination }
443    }
444}
445
446#[derive(Debug)]
447pub struct EsClone {
448    pub source: EsFile,
449    pub target_dir: EsFile,
450    pub name: String,
451}
452
453impl From<sys::es_event_clone_t> for EsClone {
454    fn from(value: sys::es_event_clone_t) -> Self {
455        Self {
456            source: unsafe { value.source.as_ref().unwrap().into() },
457            target_dir: unsafe { value.target_dir.as_ref().unwrap().into() },
458            name: unsafe { es_string_to_string!(value.target_name.data) },
459        }
460    }
461}
462
463#[derive(Debug)]
464pub struct EsMMap {
465    pub protection: i32,
466    pub max_protection: i32,
467    pub flags: i32,
468    pub file_pos: u64,
469    pub source: EsFile,
470}
471
472impl From<sys::es_event_mmap_t> for EsMMap {
473    fn from(value: sys::es_event_mmap_t) -> Self {
474        Self {
475            protection: value.protection,
476            max_protection: value.max_protection,
477            flags: value.flags,
478            file_pos: value.file_pos,
479            source: unsafe { value.source.as_ref() }.unwrap().into(),
480        }
481    }
482}
483
484#[derive(Debug)]
485pub struct EsScreenSharingAttach {
486    pub success: bool,
487    pub source_address: EsAddressType,
488    pub viewer_appleid: Option<String>,
489    pub authentication_type: Option<String>,
490    pub session_username: Option<String>,
491    pub existing_session: bool,
492    pub graphical_session_id: u32,
493}
494
495impl From<sys::es_event_screensharing_attach_t> for EsScreenSharingAttach {
496    fn from(value: sys::es_event_screensharing_attach_t) -> Self {
497        Self {
498            success: value.success,
499            source_address: EsAddressType::parse(&value.source_address, value.source_address_type),
500            viewer_appleid: unsafe { es_string_to_opt_string!(value.viewer_appleid.data) },
501            authentication_type: unsafe {
502                es_string_to_opt_string!(value.authentication_type.data)
503            },
504            existing_session: value.existing_session,
505            graphical_session_id: value.graphical_session_id,
506            session_username: unsafe { es_string_to_opt_string!(value.viewer_appleid.data) },
507        }
508    }
509}
510
511#[derive(Debug)]
512pub enum EsEventData {
513    AuthOpen(EsFile),
514    AuthRename(EsRename),
515    AuthUnlink(EsUnlinkFile),
516    AuthReadDir(EsFile),
517    AuthChroot(EsFile),
518    AuthCopyFile(EsCopyFile),
519    NotifyCopyFile(EsCopyFile),
520
521    NotifyClone(EsClone),
522    AuthClone(EsClone),
523
524    NotifyMMap(EsMMap),
525    AuthMMap(EsMMap),
526
527    NotifyExit(i32),
528
529    NotifyOpen(EsFile),
530    NotifyExec(EsProcess),
531    NotifyWrite(EsFile),
532    NotifyRename(EsRename),
533    NotifyReadDir(EsFile),
534    NotifyChroot(EsFile),
535    /// 2nd argument is true if the file was modified
536    NotifyClose((EsFile, bool)),
537    NotifyOpenSSHLogin(EsSshLogin),
538    NotifyOpenSSHLogout(EsSSHLogout),
539    NotifyLWSessionLock(EsLWSession),
540    NotifyLWSessionUnlock(EsLWSession),
541    NotifyLWSessionLogin(EsLWSession),
542    NotifyLWSessionLogout(EsLWSession),
543}
544
545impl From<sys::es_event_rename_t> for EsRename {
546    fn from(value: sys::es_event_rename_t) -> Self {
547        let source = unsafe { value.source.as_ref().unwrap().into() };
548
549        let mut rename_info = Self {
550            source,
551            destination_existing: None,
552            destintaion_newpath: None,
553        };
554
555        if value.destination_type == 0 {
556            rename_info.destination_existing = unsafe {
557                value
558                    .destination
559                    .existing_file
560                    .as_ref()
561                    .map(|file| file.into())
562            };
563        } else {
564            rename_info.destintaion_newpath = Some((
565                unsafe { value.destination.new_path.dir.as_ref().unwrap().into() },
566                unsafe {
567                    std::ffi::CStr::from_ptr(value.destination.new_path.filename.data)
568                        .to_string_lossy()
569                        .to_string()
570                },
571            ));
572        }
573
574        rename_info
575    }
576}
577
578#[derive(Debug)]
579pub struct EsFile {
580    pub path: String,
581    pub path_truncated: bool,
582}
583
584impl From<&sys::es_file_t> for EsFile {
585    fn from(file: &sys::es_file_t) -> Self {
586        let path = unsafe { std::ffi::CStr::from_ptr(file.path.data) }
587            .to_string_lossy()
588            .to_string();
589
590        Self {
591            path,
592            path_truncated: file.path_truncated,
593        }
594    }
595}
596
597#[derive(Debug)]
598pub struct EsProcess {
599    /// process pid
600    pub pid: i32,
601    /// Parent pid
602    pub ppid: i32,
603    /// groupd id
604    pub gid: i32,
605    pub exe: EsFile,
606    audit_token: sys::audit_token_t,
607}
608
609impl EsProcess {
610    pub fn audit_token(&self) -> sys::audit_token_t {
611        self.audit_token
612    }
613
614    pub fn mute(&self, client: &EsClient) {
615        unsafe { sys::es_mute_process(client.client as _, &self.audit_token as _) };
616    }
617}
618
619impl From<&sys::es_process_t> for EsProcess {
620    fn from(value: &sys::es_process_t) -> Self {
621        let pid = unsafe { bsm::audit_token_to_pid(value.audit_token) };
622
623        Self {
624            audit_token: value.audit_token,
625            ppid: value.ppid,
626            gid: value.group_id,
627            exe: unsafe { value.executable.as_ref().unwrap().into() },
628            pid,
629        }
630    }
631}
632
633#[derive(Debug)]
634pub struct EsLWSession {
635    pub graphical_session_id: u32,
636    pub username: String,
637}
638
639impl EsLWSession {
640    fn from_es_type(id: u32, str: *const i8) -> Self {
641        Self {
642            graphical_session_id: id,
643            username: unsafe { CStr::from_ptr(str).to_string_lossy().to_string() },
644        }
645    }
646}
647
648pub struct EsUser {
649    pub uid: u32,
650    pub username: String,
651}
652
653pub struct EsLogin {
654    pub success: bool,
655    pub err: Option<String>,
656    pub user: EsUser,
657}
658
659impl From<&sys::es_event_login_login_t> for EsLogin {
660    fn from(value: &sys::es_event_login_login_t) -> Self {
661        Self {
662            success: value.success,
663            err: if !value.success {
664                Some(unsafe { es_string_to_string!(value.failure_message.data) })
665            } else {
666                None
667            },
668            user: EsUser {
669                uid: unsafe { value.uid.uid },
670                username: unsafe { es_string_to_string!(value.username.data) },
671            },
672        }
673    }
674}
675
676#[derive(Debug)]
677pub struct EsMessage {
678    pub action: EsActionType,
679    pub event: EsEventType,
680    pub event_data: Option<EsEventData>,
681    pub version: u32,
682    pub seq_num: u64,
683    pub process: Option<EsProcess>,
684    pub thread_id: Option<u64>,
685    message_ptr: *const sys::es_message_t,
686}
687
688impl EsMessage {
689    pub fn allow(&self, client: &EsClient) {
690        if self.action == EsActionType::Auth {
691            assert!(
692                unsafe { sys::es_respond_auth_result(client.client, self.message_ptr, 0, true) }
693                    == 0
694            );
695        }
696    }
697
698    pub fn deny(&self, client: &EsClient) {
699        if self.action == EsActionType::Auth {
700            assert!(
701                unsafe { sys::es_respond_auth_result(client.client, self.message_ptr, 1, true) }
702                    == 0
703            );
704        }
705    }
706}
707
708impl Drop for EsMessage {
709    fn drop(&mut self) {
710        unsafe { sys::es_release_message(self.message_ptr) };
711    }
712}
713
714impl From<&sys::es_message_t> for EsMessage {
715    fn from(message: &sys::es_message_t) -> Self {
716        let action = match message.action_type {
717            0 => EsActionType::Auth,
718            1 => EsActionType::Notify,
719            _ => panic!("Useless"),
720        };
721
722        // SAFETY: message.event_type will (hopefully) never be out of range
723        let eve_type: EsEventType = unsafe { std::mem::transmute(message.event_type) };
724        let process = unsafe { message.process.as_ref().map(|process| process.into()) };
725        let thread_id = unsafe { message.thread.as_ref().map(|tid| tid.thread_id) };
726
727        // unsafe { message.event.screensharing_attach.as_ref().unwrap() }
728
729        let eve = match eve_type {
730            EsEventType::AuthOpen => unsafe {
731                message
732                    .event
733                    .open
734                    .file
735                    .as_ref()
736                    .map(|file| EsEventData::AuthOpen(file.into()))
737            },
738            EsEventType::AuthRename => Some(EsEventData::AuthRename(unsafe {
739                message.event.rename.into()
740            })),
741            EsEventType::AuthUnlink => Some(EsEventData::AuthUnlink(unsafe {
742                message.event.unlink.into()
743            })),
744            EsEventType::AuthReadDir => unsafe {
745                message
746                    .event
747                    .readdir
748                    .target
749                    .as_ref()
750                    .map(|readdir| EsEventData::AuthReadDir(readdir.into()))
751            },
752            EsEventType::AuthChroot => unsafe {
753                message
754                    .event
755                    .chroot
756                    .target
757                    .as_ref()
758                    .map(|chroot| EsEventData::AuthChroot(chroot.into()))
759            },
760            EsEventType::NotifyChroot => unsafe {
761                message
762                    .event
763                    .chroot
764                    .target
765                    .as_ref()
766                    .map(|chroot| EsEventData::NotifyChroot(chroot.into()))
767            },
768            EsEventType::NotifyExec => unsafe {
769                message
770                    .event
771                    .exec
772                    .target
773                    .as_ref()
774                    .map(|tar| EsEventData::NotifyExec(tar.into()))
775            },
776            EsEventType::NotifyOpen => unsafe {
777                message
778                    .event
779                    .open
780                    .file
781                    .as_ref()
782                    .map(|file| EsEventData::NotifyOpen(file.into()))
783            },
784            EsEventType::NotifyWrite => unsafe {
785                message
786                    .event
787                    .write
788                    .target
789                    .as_ref()
790                    .map(|target| EsEventData::NotifyWrite(target.into()))
791            },
792            EsEventType::NotifyRename => Some(EsEventData::NotifyRename(unsafe {
793                message.event.rename.into()
794            })),
795            EsEventType::NotifyClose => Some(EsEventData::NotifyClose((
796                unsafe { message.event.close.target.as_ref().unwrap().into() },
797                unsafe { message.event.close.modified },
798            ))),
799            EsEventType::NotifyReadDir => unsafe {
800                message
801                    .event
802                    .readdir
803                    .target
804                    .as_ref()
805                    .map(|readdir| EsEventData::NotifyReadDir(readdir.into()))
806            },
807            EsEventType::NotifyOpenSSHLogin => unsafe {
808                message
809                    .event
810                    .openssh_login
811                    .as_ref()
812                    .map(|login| EsEventData::NotifyOpenSSHLogin(login.into()))
813            },
814            EsEventType::NotifyOpenSSHLogout => unsafe {
815                message
816                    .event
817                    .openssh_logout
818                    .as_ref()
819                    .map(|logout| EsEventData::NotifyOpenSSHLogout(logout.into()))
820            },
821            EsEventType::NotifyLWSessionLock => unsafe {
822                message.event.lw_session_lock.as_ref().map(|session| {
823                    EsEventData::NotifyLWSessionLock(EsLWSession::from_es_type(
824                        session.graphical_session_id,
825                        session.username.data,
826                    ))
827                })
828            },
829            EsEventType::NotifyLWSessionLogin => unsafe {
830                message.event.lw_session_login.as_ref().map(|session| {
831                    EsEventData::NotifyLWSessionLogin(EsLWSession::from_es_type(
832                        session.graphical_session_id,
833                        session.username.data,
834                    ))
835                })
836            },
837            EsEventType::NotifyLWSessionLogout => unsafe {
838                message.event.lw_session_logout.as_ref().map(|session| {
839                    EsEventData::NotifyLWSessionLogout(EsLWSession::from_es_type(
840                        session.graphical_session_id,
841                        session.username.data,
842                    ))
843                })
844            },
845            EsEventType::NotifyLWSessionUnlock => unsafe {
846                message.event.lw_session_unlock.as_ref().map(|session| {
847                    EsEventData::NotifyLWSessionUnlock(EsLWSession::from_es_type(
848                        session.graphical_session_id,
849                        session.username.data,
850                    ))
851                })
852            },
853            EsEventType::AuthCopyFile => unsafe {
854                Some(EsEventData::AuthCopyFile(message.event.copyfile.into()))
855            },
856            EsEventType::NotifyCopyFile => unsafe {
857                Some(EsEventData::NotifyCopyFile(message.event.copyfile.into()))
858            },
859            EsEventType::NotifyClone => unsafe {
860                Some(EsEventData::AuthClone(message.event.clone.into()))
861            },
862            EsEventType::AuthClone => unsafe {
863                Some(EsEventData::AuthClone(message.event.clone.into()))
864            },
865            EsEventType::NotifyMMap => unsafe {
866                Some(EsEventData::NotifyMMap(message.event.mmap.into()))
867            },
868            EsEventType::AuthMMap => unsafe {
869                Some(EsEventData::AuthMMap(message.event.mmap.into()))
870            },
871            EsEventType::NotifyExit => unsafe {
872                Some(EsEventData::NotifyExit(message.event.exit.stat))
873            },
874            _ => None,
875        };
876
877        unsafe { sys::es_retain_message(message as _) }
878
879        Self {
880            event: eve_type,
881            event_data: eve,
882            version: message.version,
883            seq_num: message.seq_num,
884            message_ptr: message as _,
885            action,
886            process,
887            thread_id,
888        }
889    }
890}
891
892/// Create a new client to connect to Endpoint Security.
893pub struct EsClient {
894    client: *mut sys::es_client_t,
895    subscribed_events: Vec<EsEventType>,
896    pub rx: crossbeam::channel::Receiver<EsMessage>,
897}
898
899impl EsClient {
900    extern "C" fn handler(_c: *mut sys::es_client_t, _m: *const sys::es_message_t) {}
901
902    /// Create a new client that connects to the ES subsystem.
903    ///
904    /// # Example
905    /// ```
906    ///     let client = endpointsecurity_rs::EsClient::new();
907    ///     assert!(client.is_ok());
908    /// ```
909    pub fn new() -> anyhow::Result<EsClient> {
910        let mut client: *mut sys::es_client_t = std::ptr::null_mut();
911
912        let (tx, rx) = channel::unbounded();
913
914        let handler = ConcreteBlock::new(move |c, msg: *const sys::es_message_t| {
915            let message = unsafe { msg.as_ref() };
916            if message.is_none() {
917                println!("Failed to get message reference");
918                return;
919            }
920            let message = message.unwrap();
921            assert!(msg as usize == message as *const _ as usize);
922
923            _ = tx.send(message.into());
924
925            // this call is just to infer the types in the closure
926            Self::handler(c, msg);
927        })
928        .copy();
929
930        if let Some(err) = EsClientCreateError::from_u32(unsafe {
931            sys::es_new_client(
932                &mut client as _,
933                &*handler as *const block::Block<_, _> as *mut std::ffi::c_void,
934            )
935        }) {
936            anyhow::bail!(err)
937        }
938
939        Ok(EsClient {
940            client,
941            subscribed_events: vec![],
942            rx,
943        })
944    }
945
946    /// Add a new event to subscribe
947    pub fn add_event(&mut self, event: EsEventType) -> &mut Self {
948        if !self.subscribed_events.contains(&event) {
949            self.subscribed_events.push(event);
950        }
951        self
952    }
953
954    /// Subscribe to all the events added using [Self::add_event]
955    pub fn subscribe(&self) {
956        let mut event_ids = vec![];
957        for evt in &self.subscribed_events {
958            event_ids.push(*evt as sys::es_event_type_t)
959        }
960
961        if unsafe {
962            sys::es_subscribe(self.client as _, event_ids.as_ptr(), event_ids.len() as u32)
963        } != 0
964        {
965            panic!("Error: Failed to subscribe");
966        }
967    }
968
969    /// returns true if call to unsubscribe is successful, otherwise false
970    pub fn unsubscribe_all(&self) -> bool {
971        (unsafe { sys::es_unsubscribe_all(self.client) } == 0)
972    }
973
974    /// returns true if call to unsubscribe is successful, otherwise false/
975    pub fn unsubscribe(&mut self, event: EsEventType) -> bool {
976        if let Some(idx) = self.subscribed_events.iter().position(|eve| *eve == event) {
977            self.subscribed_events.swap_remove(idx);
978        } else {
979            return false;
980        }
981
982        let events = vec![event as u32];
983
984        (unsafe { sys::es_unsubscribe(self.client, events.as_ptr(), events.len() as u32) } == 0)
985    }
986
987    /// Get the events that the user subscribed to. Returns `None` on error
988    pub fn subscriptions(&self) -> Option<Vec<EsEventType>> {
989        let mut count = 0;
990        let mut eves: *mut EsEventType = core::ptr::null_mut();
991        if unsafe {
992            sys::es_subscriptions(
993                self.client,
994                &mut count,
995                &mut eves as *mut *mut _ as *mut *mut u32,
996            )
997        } != 0
998        {
999            None
1000        } else {
1001            let events = unsafe { std::slice::from_raw_parts(eves, count) }.to_vec();
1002
1003            // im not sure if this is the correct way to free
1004            extern "C" {
1005                fn free(ptr: *mut std::ffi::c_void);
1006            }
1007            unsafe { free(eves as _) };
1008
1009            Some(events)
1010        }
1011    }
1012
1013    /// This function blocks
1014    pub fn recv_msg(&self) -> Result<EsMessage, channel::RecvError> {
1015        self.rx.recv()
1016    }
1017
1018    /// This function doesn't block
1019    pub fn try_recv_msg(&self) -> Result<EsMessage, channel::TryRecvError> {
1020        self.rx.try_recv()
1021    }
1022
1023    /// Suppresses events from executables that match a given path.
1024    /// Returns `true` if muting was succesful.
1025    pub fn mute_path(&self, path: &std::path::Path, ty: EsMutePath) -> bool {
1026        (unsafe { sys::es_mute_path(self.client, path.to_string_lossy().as_ptr() as _, ty as u32) }
1027            == 0)
1028    }
1029
1030    /// Restores event delivery from a previously-muted path.
1031    /// Returns `true` if muting was succesful.
1032    pub fn unmute_path(&self, path: &std::path::Path, ty: EsMutePath) -> bool {
1033        (unsafe {
1034            sys::es_unmute_path(self.client, path.to_string_lossy().as_ptr() as _, ty as u32)
1035        } == 0)
1036    }
1037
1038    /// Restores event delivery of a subset of events from a previously-muted path.
1039    pub fn unmute_path_events(
1040        &self,
1041        path: &std::path::Path,
1042        ty: EsMutePath,
1043        events: &[EsEventType],
1044    ) -> bool {
1045        let events: Vec<_> = events.iter().map(|event| *event as u32).collect();
1046
1047        (unsafe {
1048            sys::es_unmute_path_events(
1049                self.client,
1050                path.to_string_lossy().as_ptr() as _,
1051                ty as u32,
1052                events.as_ptr(),
1053                events.len(),
1054            )
1055        } == 0)
1056    }
1057
1058    /// Restores event delivery from previously-muted paths.
1059    pub fn unmute_all_paths(&self) -> bool {
1060        (unsafe { sys::es_unmute_all_paths(self.client) } == 0)
1061    }
1062
1063    /// Deletes the client
1064    pub fn destroy_client(self) {}
1065}
1066
1067impl Drop for EsClient {
1068    fn drop(&mut self) {
1069        if unsafe { sys::es_delete_client(self.client) } != 0 {
1070            println!("Failed to delete client");
1071        }
1072    }
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077
1078    #[test]
1079    pub fn test_new_es_client() {
1080        let client = crate::EsClient::new();
1081        assert!(client.is_ok());
1082    }
1083}