endpoint_sec/
message.rs

1//! Definitions of Endpoint Security Message.
2
3use std::ffi::OsStr;
4use std::ptr::NonNull;
5#[cfg(feature = "macos_10_15_4")]
6use std::time::Duration;
7use std::time::{Instant, SystemTime};
8
9use endpoint_sec_sys::*;
10
11use crate::{utils, Action, ActionResult, AuditToken, Event};
12
13/// A message from Endpoint Security.
14///
15/// Be careful with `AUTH` messages, they must be responded to before their deadline (see
16/// [`Message::deadline()`]) else Endpoint Security may kill your client as it slows the OS too much.
17///
18/// ## Implementation details (macOS 11+)
19///
20/// Behind the scene, [`es_message_t`] is a reference-counted object, which means `Clone`-ing a
21/// `Message` won't actually create a new message, but merely increment its refcount and return a
22/// new handle to that object. This is very cheap and can be done without much performance overhead.
23///
24/// ## Implementation details (macOS 10.15.x)
25///
26/// Dropping a `Message` while inside a handler may cause your app to **crash**. We copy the message
27/// before handing it over for your usage but that may not be enough, so be thorough in testing.
28///
29/// See <https://developer.apple.com/documentation/endpointsecurity/3366178-es_free_message>.
30#[doc(alias = "es_message_t")]
31#[repr(transparent)]
32pub struct Message(NonNull<es_message_t>);
33
34impl Message {
35    /// Create a new [`Message`] from a raw pointer.
36    ///
37    /// # Safety
38    ///
39    /// `msg` must point to a valid live [`es_message_t`] object.
40    ///
41    /// # Details
42    ///
43    /// On macOS 11.0+, with the feature `"macos_11_0_0"` (or more) active, this uses
44    #[cfg_attr(
45        feature = "macos_11_0_0",
46        doc = "[`es_retain_message()`], which is basically an `Arc::clone()`."
47    )]
48    #[cfg_attr(
49        not(feature = "macos_11_0_0"),
50        doc = "`es_retain_message()`, which is basically an `Arc::clone()`."
51    )]
52    ///
53    /// On macOS 10.15.x, this calls [`es_copy_message()`].
54    #[inline(always)]
55    pub unsafe fn from_raw(msg: NonNull<es_message_t>) -> Self {
56        let msg = versioned_call!(if cfg!(feature = "macos_11_0_0") && version >= (11, 0, 0) {
57            // Safety: the caller must guarantee that `msg` is a valid live es_message_t object.
58            unsafe { es_retain_message(msg.as_ref()) };
59            msg
60        } else {
61            // Safety: the caller must guarantee that `msg` is a valid live es_message_t object.
62            let msg = unsafe { es_copy_message(msg.as_ref()) };
63            NonNull::new(msg).expect("es_copy_message returned NULL")
64        });
65        Self(msg)
66    }
67
68    /// Allow to grab a reference out of the stored pointer.
69    ///
70    /// This allows to reduce the boilerplate for all other methods.
71    #[inline(always)]
72    pub(crate) fn get_raw_ref(&self) -> &es_message_t {
73        // Safety: inner message is a valid live object by construction.
74        unsafe { self.0.as_ref() }
75    }
76
77    /// Version of the Endpoint Security message.
78    #[inline(always)]
79    pub fn version(&self) -> u32 {
80        self.get_raw_ref().version
81    }
82
83    /// Time at which the event was generated.
84    ///
85    /// See also [`Self::time()`].
86    ///
87    /// Ref: <https://developer.apple.com/documentation/kernel/timespec>
88    #[inline(always)]
89    pub fn raw_time(&self) -> endpoint_sec_sys::timespec {
90        self.get_raw_ref().time
91    }
92
93    /// Time at which the event was generated, as a [`SystemTime`].
94    ///
95    /// See also [`Self::raw_time()`].
96    #[inline(always)]
97    pub fn time(&self) -> SystemTime {
98        let dur = utils::convert_timespec_to_duration(self.raw_time());
99        SystemTime::UNIX_EPOCH + dur
100    }
101
102    /// Time at which the event was generated, as Mach absolute time.
103    ///
104    /// This is basically a duration since the machine booted up.
105    ///
106    /// See also [`Self::mach_time()`].
107    ///
108    /// Ref: <https://developer.apple.com/documentation/kernel/1462446-mach_absolute_time>
109    #[inline(always)]
110    pub fn raw_mach_time(&self) -> u64 {
111        self.get_raw_ref().mach_time
112    }
113
114    /// Time at which the event was generated, as an [`Instant`].
115    ///
116    /// This is basically a duration since the machine booted up.
117    ///
118    /// See also [`Self::raw_mach_time()`].
119    #[inline(always)]
120    pub fn mach_time(&self) -> Result<Instant, TimeError> {
121        utils::convert_mach_time_to_instant(self.raw_mach_time())
122    }
123
124    /// Time before which an AUTH event **must** be responded to, as Mach absolute time.
125    ///
126    /// **Warning**: The client needs to respond to auth events prior to the `deadline` otherwise
127    /// the application will be killed.
128    ///
129    /// See also [`Self::deadline()`].
130    ///
131    /// Ref: <https://developer.apple.com/documentation/kernel/1462446-mach_absolute_time>
132    #[inline(always)]
133    pub fn raw_deadline(&self) -> u64 {
134        self.get_raw_ref().deadline
135    }
136
137    /// Time before which an AUTH event **must** be responded to, as an [`Instant`].
138    ///
139    /// **Warning**: The client needs to respond to auth events prior to the `deadline` otherwise
140    /// the application will be killed.
141    ///
142    /// See also [`Self::raw_deadline()`].
143    #[inline(always)]
144    pub fn deadline(&self) -> Result<Instant, TimeError> {
145        utils::convert_mach_time_to_instant(self.raw_deadline())
146    }
147
148    /// Describes the process that took the action.
149    #[inline(always)]
150    pub fn process(&self) -> Process<'_> {
151        Process::new(
152            // Safety: 'a tied to self, object obtained through ES
153            unsafe { self.get_raw_ref().process() },
154            self.version(),
155        )
156    }
157
158    /// Per client event sequence number on version 2 and later, otherwise None.
159    #[cfg(feature = "macos_10_15_4")]
160    #[inline(always)]
161    pub fn seq_num(&self) -> Option<u64> {
162        if self.version() >= 2 {
163            Some(self.get_raw_ref().seq_num)
164        } else {
165            None
166        }
167    }
168
169    /// Indicates if the action field is an auth or notify action.
170    #[inline(always)]
171    pub fn action_type(&self) -> es_action_type_t {
172        self.get_raw_ref().action_type
173    }
174
175    /// For auth events, contains the opaque auth ID that must be supplied when responding to the
176    /// event. For notify events, describes the result of the action.
177    #[inline(always)]
178    pub fn action(&self) -> Option<Action> {
179        match self.action_type() {
180            // Safety: we just checked the `action_type` member
181            es_action_type_t::ES_ACTION_TYPE_AUTH => Some(Action::Auth(unsafe { self.get_raw_ref().action.auth })),
182            // Safety: we just checked the `action_type` member and the content of `notify` is
183            // checked in the `ActionResult::from_raw` call
184            es_action_type_t::ES_ACTION_TYPE_NOTIFY => Some(Action::Notify(ActionResult::from_raw(unsafe {
185                self.get_raw_ref().action.notify
186            })?)),
187            _ => None,
188        }
189    }
190
191    /// Indicates which event struct is defined in the event union.
192    #[inline(always)]
193    pub fn event_type(&self) -> es_event_type_t {
194        self.get_raw_ref().event_type
195    }
196
197    /// Event associated to this message.
198    #[inline(always)]
199    pub fn event(&self) -> Option<Event<'_>> {
200        // Safety: all arguments are from the current message instance.
201        unsafe { Event::from_raw_parts(self.event_type(), &self.get_raw_ref().event, self.version()) }
202    }
203
204    /// Thread associated to this message (if present) on version 4 and later, otherwise None.
205    #[cfg(feature = "macos_11_0_0")]
206    #[inline(always)]
207    pub fn thread(&self) -> Option<Thread<'_>> {
208        if self.version() >= 4 {
209            // Safety: Safe as Thread cannot outlive self and meet all requirements of a reference.
210            let thread_res = unsafe { self.get_raw_ref().thread.as_ref() };
211
212            thread_res.map(Thread::new)
213        } else {
214            None
215        }
216    }
217
218    /// Per client global sequence number on version 4 and later, otherwise None.
219    #[cfg(feature = "macos_11_0_0")]
220    #[inline(always)]
221    pub fn global_seq_num(&self) -> Option<u64> {
222        if self.version() >= 4 {
223            Some(self.get_raw_ref().global_seq_num)
224        } else {
225            None
226        }
227    }
228}
229
230impl Clone for Message {
231    #[inline(always)]
232    fn clone(&self) -> Self {
233        // Safety: we already have a valid Message, increasing the reference count is ok
234        // on macOS < 11.0, this will call `es_copy_message` instead, which is semantically the same.
235        unsafe { Self::from_raw(self.0) }
236    }
237}
238
239impl Drop for Message {
240    #[inline(always)]
241    fn drop(&mut self) {
242        versioned_call!(if cfg!(feature = "macos_11_0_0") && version >= (11, 0, 0) {
243            // Safety: we are dropping the Message so it's safe to release it.
244            unsafe { es_release_message(self.0.as_ref()) };
245        } else {
246            // Safety: we are dropping the Message so it's safe to free it.
247            unsafe { es_free_message(self.0.as_ref()) };
248        })
249    }
250}
251
252/// Safety: Message is safe to send across threads - it does not contain any interior mutability, nor depend on current thread state.
253unsafe impl Send for Message {}
254/// Safety: Message is safe to share between threads - it does not contain any interior mutability.
255unsafe impl Sync for Message {}
256
257impl_debug_eq_hash_with_functions!(
258    Message;
259    action_type,
260    action,
261    deadline,
262    event,
263    event_type,
264    #[cfg(feature = "macos_11_0_0")]
265    global_seq_num,
266    mach_time,
267    process,
268    #[cfg(feature = "macos_10_15_4")]
269    seq_num,
270    #[cfg(feature = "macos_11_0_0")]
271    thread,
272    time,
273    version,
274);
275
276/// Error produced when trying to access [`Message::deadline()`] or equivalent functions because
277/// computing the `[`Instant`] overflowed.
278#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
279pub enum TimeError {
280    /// Computing the deadline overflowed
281    Overflow,
282}
283
284impl std::error::Error for TimeError {}
285
286impl std::fmt::Display for TimeError {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        f.write_str(match self {
289            Self::Overflow => "Computing deadline overflowed",
290        })
291    }
292}
293
294/// Information related to a thread.
295#[cfg(feature = "macos_11_0_0")]
296pub struct Thread<'a>(&'a es_thread_t);
297
298#[cfg(feature = "macos_11_0_0")]
299impl<'a> Thread<'a> {
300    /// Create a new [`Thread`] instance.
301    #[inline(always)]
302    pub const fn new(raw: &'a es_thread_t) -> Self {
303        Thread(raw)
304    }
305
306    /// The unique thread ID of the thread.
307    #[inline(always)]
308    pub fn thread_id(&self) -> u64 {
309        self.0.thread_id
310    }
311}
312
313#[cfg(feature = "macos_11_0_0")]
314// Safety: safe to send across threads: does not contain any interior mutability nor depend on current thread state
315unsafe impl Send for Thread<'_> {}
316
317#[cfg(feature = "macos_11_0_0")]
318impl_debug_eq_hash_with_functions!(Thread<'a>; thread_id);
319
320/// Provides the stat information and path to a file that relates to a security event.
321pub struct File<'a>(&'a es_file_t);
322
323impl<'a> File<'a> {
324    /// Create a new [`File`] instance.
325    #[inline(always)]
326    pub const fn new(raw: &'a es_file_t) -> Self {
327        File(raw)
328    }
329
330    /// The path to the file.
331    #[inline(always)]
332    pub fn path(&self) -> &'a OsStr {
333        // Safety: 'a tied to self, object obtained through ES
334        unsafe { self.0.path.as_os_str() }
335    }
336
337    /// Returns true to indicate if the path was truncated.
338    #[inline(always)]
339    pub fn path_truncated(&self) -> bool {
340        self.0.path_truncated
341    }
342
343    /// The [`stat`][struct@stat] to the file.
344    #[inline(always)]
345    pub fn stat(&self) -> &'a stat {
346        &self.0.stat
347    }
348}
349
350// Safety: safe to send across threads: does not contain any interior mutability nor depend on current thread state
351unsafe impl Send for File<'_> {}
352
353impl_debug_eq_hash_with_functions!(File<'a>; path, path_truncated, stat);
354
355/// Information related to a process.
356pub struct Process<'a> {
357    /// The raw reference.
358    raw: &'a es_process_t,
359
360    /// The version of the message.
361    version: u32,
362}
363
364impl<'a> Process<'a> {
365    /// Create a new [`Process`] instance.
366    #[inline(always)]
367    pub const fn new(raw: &'a es_process_t, version: u32) -> Self {
368        Process { raw, version }
369    }
370
371    /// Audit token of the process.
372    #[inline(always)]
373    pub fn audit_token(&self) -> AuditToken {
374        AuditToken::new(self.raw.audit_token)
375    }
376
377    /// Parent pid of the process.
378    ///
379    #[cfg_attr(
380        feature = "macos_11_0_0",
381        doc = "**Warning**: It is recommended to instead use [`Self::parent_audit_token()`] when available."
382    )]
383    #[cfg_attr(
384        not(feature = "macos_11_0_0"),
385        doc = "**Warning**: It is recommended to instead use `Self::parent_audit_token()` when available."
386    )]
387    #[inline(always)]
388    pub fn ppid(&self) -> pid_t {
389        self.raw.ppid
390    }
391
392    /// Original ppid of the process.
393    #[inline(always)]
394    pub fn original_ppid(&self) -> pid_t {
395        self.raw.original_ppid
396    }
397
398    /// Process group id the process belongs to.
399    #[inline(always)]
400    pub fn group_id(&self) -> pid_t {
401        self.raw.group_id
402    }
403
404    /// Process session id the process belongs to.
405    #[inline(always)]
406    pub fn session_id(&self) -> pid_t {
407        self.raw.session_id
408    }
409
410    /// Code signing flags of the process.
411    #[inline(always)]
412    pub fn codesigning_flags(&self) -> u32 {
413        self.raw.codesigning_flags
414    }
415
416    /// Indicates whether the process is a platform binary.
417    ///
418    /// **Note**: A "platform binary" is a binary signed with Apple certificates.
419    ///
420    /// ## Usage of `is_platform_binary` with `Message`s and `EventExec`s
421    ///
422    /// If your application is looking to allow/deny [`AuthExec`][crate::Event::AuthExec]
423    /// events, be sure to check [`EventExec::target()`][crate::EventExec::target], **not**
424    /// [`Message::process()`], else you will get the wrong result, especially since pretty much
425    /// all processes are lauched through `xpcproxy`, a platform binary.
426    #[inline(always)]
427    pub fn is_platform_binary(&self) -> bool {
428        self.raw.is_platform_binary
429    }
430
431    /// Indicates this process has the Endpoint Security entitlement.
432    #[inline(always)]
433    pub fn is_es_client(&self) -> bool {
434        self.raw.is_es_client
435    }
436
437    /// Code directory hash of the code signature associated with this process.
438    #[inline(always)]
439    pub fn cdhash(&self) -> [u8; 20] {
440        self.raw.cdhash
441    }
442
443    /// Signing id of the code signature associated with this process.
444    #[inline(always)]
445    pub fn signing_id(&self) -> &'a OsStr {
446        // Safety: 'a tied to self, object obtained through ES
447        unsafe { self.raw.signing_id.as_os_str() }
448    }
449
450    /// Team id of the code signature associated with this process.
451    #[inline(always)]
452    pub fn team_id(&self) -> &'a OsStr {
453        // Safety: 'a tied to self, object obtained through ES
454        unsafe { self.raw.team_id.as_os_str() }
455    }
456
457    /// Executable file that is executing in this process.
458    #[inline(always)]
459    pub fn executable(&self) -> File<'a> {
460        // Safety: 'a tied to self, object obtained through ES
461        File::new(unsafe { self.raw.executable() })
462    }
463
464    /// TTY associated to this process (if present) on version 2 and later, otherwise None.
465    #[cfg(feature = "macos_10_15_1")]
466    #[inline(always)]
467    pub fn tty(&self) -> Option<File<'a>> {
468        if self.version >= 2 {
469            // Safety: Safe as File cannot outlive self and meet all requirements of a reference.
470            unsafe { self.raw.tty.as_ref() }.map(File::new)
471        } else {
472            None
473        }
474    }
475
476    /// Process start time on version 3 and later, otherwise None.
477    #[cfg(feature = "macos_10_15_4")]
478    #[inline(always)]
479    pub fn start_time(&self) -> Option<SystemTime> {
480        if self.version >= 3 {
481            // timeval is the elapsed time since unix epoch, as such it shouldn't be negative.
482            let timestamp = Duration::from_secs(self.raw.start_time.tv_sec as u64)
483                + Duration::from_micros(self.raw.start_time.tv_usec as u64);
484
485            if let Some(system_time) = SystemTime::UNIX_EPOCH.checked_add(timestamp) {
486                Some(system_time)
487            } else {
488                // In case of overflow, default to epoch.
489                Some(SystemTime::UNIX_EPOCH)
490            }
491        } else {
492            None
493        }
494    }
495
496    /// Audit token of the process responsible for this process on version 4 and later, if any.
497    ///
498    /// **Warning**: It may be the process itself in case there is no responsible process or the
499    /// responsible process has already exited.
500    #[cfg(feature = "macos_11_0_0")]
501    #[inline(always)]
502    pub fn responsible_audit_token(&self) -> Option<AuditToken> {
503        if self.version >= 4 {
504            Some(AuditToken::new(self.raw.responsible_audit_token))
505        } else {
506            None
507        }
508    }
509
510    /// Audit token of the parent process on version 4 and later, otherwise None.
511    #[cfg(feature = "macos_11_0_0")]
512    #[inline(always)]
513    pub fn parent_audit_token(&self) -> Option<AuditToken> {
514        if self.version >= 4 {
515            Some(AuditToken::new(self.raw.parent_audit_token))
516        } else {
517            None
518        }
519    }
520}
521
522// Safety: safe to send across threads: does not contain any interior mutability nor depend on current thread state
523unsafe impl Send for Process<'_> {}
524
525impl_debug_eq_hash_with_functions!(
526    Process<'a> with version;
527    audit_token,
528    cdhash,
529    codesigning_flags,
530    executable,
531    group_id,
532    is_es_client,
533    is_platform_binary,
534    original_ppid,
535    #[cfg(feature = "macos_11_0_0")]
536    parent_audit_token,
537    ppid,
538    #[cfg(feature = "macos_11_0_0")]
539    responsible_audit_token,
540    session_id,
541    signing_id,
542    #[cfg(feature = "macos_10_15_4")]
543    start_time,
544    team_id,
545    #[cfg(feature = "macos_10_15_1")]
546    tty,
547);
548
549/// Describes machine-specific thread state as used by `thread_create_running()` and other Mach API functions.
550#[cfg(feature = "macos_11_0_0")]
551pub struct ThreadState<'a>(&'a es_thread_state_t);
552
553#[cfg(feature = "macos_11_0_0")]
554impl<'a> ThreadState<'a> {
555    /// Create a new [`ThreadState`] instance.
556    #[inline(always)]
557    pub const fn new(raw: &'a es_thread_state_t) -> Self {
558        ThreadState(raw)
559    }
560
561    /// Indicates the representation of the machine-specific thread state.
562    #[inline(always)]
563    pub fn flavor(&self) -> i32 {
564        self.0.flavor
565    }
566
567    /// The machine-specific thread state, equivalent to `thread_state_t` in Mach APIs.
568    #[inline(always)]
569    pub fn state(&self) -> &'a [u8] {
570        // Safety: 'a tied to self, object obtained through ES
571        unsafe { self.0.state.as_slice() }
572    }
573}
574
575#[cfg(feature = "macos_11_0_0")]
576// Safety: safe to send across threads: does not contain any interior mutability nor depend on current thread state
577unsafe impl Send for ThreadState<'_> {}
578
579#[cfg(feature = "macos_11_0_0")]
580impl_debug_eq_hash_with_functions!(ThreadState<'a>; flavor, state);