De-mystifying Endpoint Security Messages
Endpoint Security is Apple's C API for monitoring system events. Its primary use is to allow security vendors to monitor for potentially malicious activity, but it also provides a great source of system telemetry for security researchers.
ES Subsystem primer
Typically, you would write a client that subscribes to specific events provided by the Endpoint Security Subsystem. These clients need to be signed with the coveted com.apple.developer.endpoint-security.client entitlement, which Apple may grant upon request.
Two classes of events exist; NOTIFY and AUTH. Notify events will simply alert your client after an action occurs (such as a process execution, or file write), while Auth(orisation) events require your client to actively approve or deny an action for it to complete. The list of events grows inconsistently across releases, and at the time of this post there are 104 distinct NOTIFY events.
For the curious (but un-entitled), macOS comes with a built-in ES Client /usr/bin/eslogger, which can be used to register and receive ES Notify events.
The anatomy of an ES message
es_message_t is the top-level datatype that encodes information sent from the ES subsystem to registered clients. Each security event being processed by the ES subsystem will be encoded in an es_message_t.
typedef struct {
uint32_t version;
struct timespec time;
uint64_t mach_time;
uint64_t deadline;
es_process_t *_Nonnull process;
uint64_t seq_num;
es_action_type_t action_type;
union {
es_event_id_t auth;
es_result_t notify;
} action;
es_event_type_t event_type;
es_events_t event;
es_thread_t *_Nullable thread;
uint64_t global_seq_num;
uint64_t opaque[];
} es_message_t;
While many of these fields are somewhat self-explanatory in nature, like many things the devil is in the detail.
Every message contains an event monitored by ES. The event is union of types specific to that kind of event. These are documented to various degrees in the ES header files which form part of the macOS SDK included with Xcode.
For instance, the ES event for a file unlink (delete) contains both target and parent_dir of type es_file_t.
typedef struct {
es_file_t *_Nonnull target;
es_file_t *_Nonnull parent_dir;
uint8_t reserved[64];
} es_event_unlink_t;
es_file_t is also a structure defined in ES that represents a filesystem resource.
typedef struct {
es_string_token_t path;
bool path_truncated;
struct stat stat;
} es_file_t;
Both of these definitions can be found in usr/include/EndpointSecurity/ESMessage.h within the macOS SDK.
Almost as equally as significant is process, which defines the process that performed the action defined in a message. Once again, as defined in ESMessage.h we can analyse the components of this defined type as:
typedef struct {
audit_token_t audit_token;
pid_t ppid;
pid_t original_ppid;
pid_t group_id;
pid_t session_id;
uint32_t codesigning_flags;
bool is_platform_binary;
bool is_es_client;
es_cdhash_t cdhash;
es_string_token_t signing_id;
es_string_token_t team_id;
es_file_t *_Nonnull executable;
es_file_t *_Nullable tty;
struct timeval start_time;
audit_token_t responsible_audit_token;
audit_token_t parent_audit_token;
es_cs_validation_category_t cs_validation_category;
} es_process_t;
Effectively, what we have is a hierarchical message envelope with context layers. Below is an example of our ES_EVENT_TYPE_NOTIFY_UNLINK event (audit tokens and stat removed for brevity):
{
"version": 10,
"time": "2025-12-29T02:21:42.907139146Z",
"mach_time": 212493431893,
"process": {
"audit_token": {},
"is_es_client": false,
"group_id": 3412,
"responsible_audit_token": {},
"is_platform_binary": true,
"tty": {
"stat": {},
"path_truncated": false,
"path": "/dev/ttys003"
},
"cs_validation_category": 1,
"codesigning_flags": 637606673,
"signing_id": "com.apple.rm",
"team_id": null,
"cdhash": "FD4F433664E0BD2AC41A745C93F56A9A1A397C6B",
"ppid": 3392,
"original_ppid": 3392,
"parent_audit_token": {},
"start_time": "2025-12-29T02:21:42.897629Z",
"session_id": 3391,
"executable": {
"stat": {},
"path_truncated": false,
"path": "/bin/rm"
}
},
"seq_num": 389,
"action_type": 1,
"action": {
"result": {
"result": {
"auth": 0
},
"result_type": 0
}
},
"event_type": 32,
"event": {
"unlink": {
"target": {
"path": "/Users/dowdy/Desktop/blabla",
"path_truncated": false,
"stat": {}
},
"parent_dir": {
"path": "/Users/dowdy/Desktop",
"path_truncated": false,
"stat": {}
}
}
},
"thread": {
"thread_id": 152879
}
"global_seq_num": 389
}
Registering for ES events
There are two main requirements placed on clients that subscribe to ES. Clients are required to be executing under a privileged context (i.e., root) otherwise a ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED error will be returned. Similarly, TCC Full Disk Access authorisation is required, or ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED will be returned.
To experiment with ES yourself, you can run sudo eslogger EVENT [...] in your FDA-enabled Terminal.app. All available NOTIFY events for your OS version will be presented by running eslogger --list-events.
You may find it preferable to pipe this output to | jq '.' to beautify the resulting JSON output.
Give it a go!