Apple’s Endpoint Security: The First Dedicated macOS Security API
Before Endpoint Security existed, writing a macOS process monitor required significant effort with no dedicated APIs. Apple released this C framework specifically for third-party security tools, and it changed everything. It ships with a built-in utility called ES Logger that lets you explore events directly from the terminal. Real-world tool developers will tell you to skip the developer documentation and go straight to the three header files: ESClient, ESMessage, and ESTypes. Those contain ASCII flowcharts and detailed comments that the website omits.
Notification vs Authorization: Two Ways Events Reach Your Client
Endpoint Security exposes over 150 subscribable events declared in ESTypes.h, covering process spawns, file I/O, USB activity, and more. They fall into two categories. Notification events arrive after an action completes. They are read-only but still useful for building passive monitors. Authorization events arrive before an action executes, and the OS holds the action until your client responds with allow or deny. Subscribe to the auth exec event and every new process launch waits for your decision. Deny it and the process never starts.
The Global Cache Trap
When an authorization event is approved, ES_respond_auth_result accepts a cache flag. Set it and the OS skips querying your client for subsequent executions of the same binary. Wardle’s tool BlockBlock skipped caching, which caused CPU spikes when users compiled large Objective-C projects: the compiler fires repeatedly and ES queried BlockBlock each time. The ES cache is global. One non-caching client resets the cache for every other client, so JProtect ran full YARA scans on every launch. Also: caching is per binary inode, not per arguments. Cache bash and malware using bash bypasses your check.
Script Interpreters and the Null Script Member
Many macOS malware families ship as standalone scripts: JokerSpy is a malicious Python script, Sigin is a malicious bash script. Endpoint Security 2.0 added a script member to exec structures so clients see the actual script, not just the interpreter. It works when the script runs directly. Pass the script as an argument to Python instead, and the script member is null. The header file states this field is only valid if a script was executed directly, not as an argument. Workaround: enumerate interpreter arguments and treat file-path arguments as scripts.
Authorization Deadlines: Respond or Get Killed
Auth events pause process execution until your client responds. Apple sets a deadline: 3-4 seconds for system processes, about 15 seconds for applications. Miss it and macOS terminates your client without restarting it, leaving the system unprotected. BlockBlock displays a user prompt for non-notarized processes, but if the user is away the deadline expires. The fix: a dispatch semaphore that fires slightly before the deadline and sends an automatic response. One more trap: converting the deadline’s mach absolute time to nanoseconds on Apple Silicon requires a system helper. Direct conversion produces wrong values.
Muting, New Events, and What ES Cannot See
A file monitor gets flooded immediately: legitimate system file I/O is constant, and writing output generates more file events. ES_mute_process silences a process by audit token. ES_mute_path silences events for a path or prefix, though ES does not resolve symlinks. Mute inversion flips the model: instead of muting some processes, you receive events for only one target. MacOS 15.4 added TCC modification events, letting tools detect when malware grants itself full disk access. MacOS 26 adds code-sign validation categories, so distinguishing App Store binaries from ad-hoc signed malware no longer requires custom code.
Notable Quotes
this might cause you some consternation. Patrick Wardle · ▶ 1:02
I should use cache. Not so fast. Patrick Wardle · ▶ 20:32
Nuances, nuances, nuances. Patrick Wardle · ▶ 20:35
begging for this for years. Patrick Wardle · ▶ 41:12
Key Takeaways
- Always cache authorization responses; the ES cache is global and one non-caching client breaks all others.
- When a script interpreter runs, the script member is null unless the script was executed directly.
- Missing an auth event deadline terminates your client permanently, leaving macOS unprotected until manual restart.
- MacOS 15.4 TCC events and macOS 26 code-sign categories reduce the custom detection code required.
About the Speaker
Patrick Wardle is the founder of the Objective-See Foundation, the CEO and co-founder of DoubleYou, and the author of “The Art of Mac Malware” book series. Having worked at NASA and the NSA, as well as presenting at countless security conferences, he is intimately familiar with aliens, spies, and talking nerdy. Passionate about macOS security, Patrick spends his days discovering Apple 0days, studying macOS malware, and releasing free open-source security tools to protect Mac users.