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);