1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
//! Safe bindings for the [Endpoint Security Framework][esf] for Apple targets (macOS).
//!
//! The [`sys`] module contains the raw bindings since several types are publicly exported from there.
//!
//! At runtime, users should call [`version::set_runtime_version()`] before anything else, to indicate
//! on which macOS version the app is running on.
//!
//! The entry point is the [`Client`] type, which is a wrapper around [`es_client_t`][sys::es_client_t],
//! with the [`Client::new()`] method.
//!
//! After a `Client` has been created, [events][sys::es_event_type_t] can be subscribed to
//! using [`Client::subscribe()`]. Each time Endpoint Security gets an event that is part of the
//! subscribptions for your client, it will call the handler that was given to `Client::new()` with
//! the [message][Message] associated to the event. Note that `AUTH` events have an associated
//! deadline before which your handler must give a response else your client may be killed by macOS
//! to avoid stalling for the user.
//!
//! [esf]: https://developer.apple.com/documentation/endpointsecurity
#![cfg(target_os = "macos")]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#![allow(clippy::bool_comparison)]
#![warn(
missing_docs,
unused_crate_dependencies,
clippy::missing_safety_doc,
unreachable_pub,
clippy::missing_docs_in_private_items
)]
// Reexports [`endpoint_sec_sys`]
pub use endpoint_sec_sys as sys;
#[cfg(all(test, not(feature = "audit_token_from_pid")))]
use sysinfo as _;
#[cfg(test)]
use trybuild as _;
/// Our wrappers around Endpoint Security events cannot easily implement [`Debug`]: they contain
/// a reference to the original value (and sometimes the version). Since the structs behind the
/// references can change shape based on macOS' versions, we cannot rely on the one compiled in
/// our Rust bindings. What's more, some fields are only available in some versions of Endpoint
/// Security, others are behind pointers and associated with another (eg, array + len). This macro
/// makes it easier to implement [`Debug`] by simply passing the type and the functions to use for
/// the `Debug` impl. See examples of usage in the modules below.
macro_rules! impl_debug_eq_hash_with_functions {
($ty:ident$(<$lt: lifetime>)? $(with $version:ident)?; $($(#[$fmeta: meta])? $fname:ident),* $(,)?) => {
impl $(<$lt>)? ::core::fmt::Debug for $ty $(<$lt>)? {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
let mut d = f.debug_struct(::core::stringify!($ty));
$( d.field("version", &self.$version); )?
$( $(#[$fmeta])? d.field(::core::stringify!($fname), &self.$fname()); )*
d.finish()
}
}
impl $(<$lt>)? ::core::cmp::PartialEq for $ty $(<$lt>)? {
#[allow(unused_variables)]
fn eq(&self, other: &Self) -> bool {
$( if ::core::cmp::PartialEq::ne(&self.$version, &other.$version) { return false } )?
$( $(#[$fmeta])? if ::core::cmp::PartialEq::ne(&self.$fname(), &other.$fname()) { return false; } )*
true
}
}
impl $(<$lt>)? ::core::cmp::Eq for $ty $(<$lt>)? {}
impl $(<$lt>)? ::core::hash::Hash for $ty $(<$lt>)? {
#[allow(unused_variables)]
fn hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
$( ::core::hash::Hash::hash(&self.$version, state); )?
$( $(#[$fmeta])? ::core::hash::Hash::hash(&self.$fname(), state); )*
}
}
};
}
/// Helper macro to generate all necessary version checks for a function call.
macro_rules! versioned_call {
// It's not possible to use `feature = ::std::concat(...)` so we need to pass both forms.
(if cfg!($cfg: meta) && version >= ($major:literal, $minor:literal, $patch:literal) { $($if_tt:tt)* } $(else { $($else_tt:tt)* })?) => {
if ::std::cfg!($cfg) && $crate::version::is_version_or_more($major, $minor, $patch) {
#[cfg($cfg)]
{ $($if_tt)* }
#[cfg(not($cfg))]
// Safety: the cfg was checked just above
unsafe { ::std::hint::unreachable_unchecked() }
} $( else {
$($else_tt)*
} )?
};
}
// Publicly reexported modules
#[cfg(feature = "macos_10_15_1")]
mod acl;
mod action;
mod audit;
mod client;
mod event;
mod message;
mod mute;
// Not public
mod utils;
#[cfg(feature = "macos_10_15_1")]
pub use acl::*;
pub use action::*;
pub use audit::*;
pub use client::*;
pub use event::*;
pub use message::*;
pub use mute::*;
/// Helper module to avoid implementing version detection in this crate and make testing easier
/// by telling the crate its on a lower version than the real one.
pub mod version {
use std::sync::atomic::{AtomicU64, Ordering};
/// macOS major version
static MAJOR: AtomicU64 = AtomicU64::new(10);
/// macOS minor version
static MINOR: AtomicU64 = AtomicU64::new(15);
/// macOS patch version
static PATCH: AtomicU64 = AtomicU64::new(0);
/// Setup the runtime version of macOS, detected outside of this library.
///
/// Conservatively, this library assumes the default is 10.15.0 and will refuse to use functions
/// that only became available in later version of macOS and Endpoint Security.
///
/// Methods on [`Client`][super::Client] will check this version when calling a function only
/// available in macOS 11+ for example.
///
/// # Panics
///
/// Will panic if attempting to set a version below 10.15.0.
pub fn set_runtime_version(major: u64, minor: u64, patch: u64) {
if major < 10 || (major == 10 && minor < 15) {
panic!("Endpoint Security cannot run on versions inferiors to 10.15.0");
}
MAJOR.store(major, Ordering::Release);
MINOR.store(minor, Ordering::Release);
PATCH.store(patch, Ordering::Release);
}
/// `true` if the version setup in [`set_runtime_version()`] is at least the given
/// `major.minor.patch` here.
pub fn is_version_or_more(major: u64, minor: u64, patch: u64) -> bool {
let current_major = MAJOR.load(Ordering::Acquire);
let current_minor = MINOR.load(Ordering::Acquire);
let current_patch = PATCH.load(Ordering::Acquire);
(current_major, current_minor, current_patch) >= (major, minor, patch)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Endpoint Security cannot run on versions inferiors to 10.15.0")]
fn test_cannot_set_version_major_under_10() {
set_runtime_version(9, 16, 2);
}
#[test]
#[should_panic(expected = "Endpoint Security cannot run on versions inferiors to 10.15.0")]
fn test_cannot_set_version_minor_under_10_15() {
set_runtime_version(10, 14, 0);
}
#[test]
fn test_is_version_or_more_with_set_runtime() {
set_runtime_version(10, 15, 0);
assert!(is_version_or_more(10, 14, 99));
assert!(is_version_or_more(9, 15, 0));
assert!(is_version_or_more(9, 14, 1));
assert!(is_version_or_more(10, 15, 0));
assert!(!is_version_or_more(12, 3, 1));
assert!(!is_version_or_more(13, 3, 2));
assert!(!is_version_or_more(14, 5, 4));
assert!(!is_version_or_more(15, 0, 0));
set_runtime_version(13, 3, 2);
assert!(is_version_or_more(12, 3, 1));
assert!(is_version_or_more(13, 3, 2));
assert!(!is_version_or_more(14, 5, 4));
assert!(!is_version_or_more(15, 0, 0));
}
}
}