Internals of macOS Endpoint Security Products
Discussing the past and future of Endpoint Security applications in macOS
Cover Illustration by cloudnienty
This research was done using software obtained by myself individually, analyzed using hardware owned by myself individually. Some code is simplified and edited to provide clarity.
The article is not intended to harm any company’s product and is constructed for educational purposes only.
The stealing of Google's Proprietary TPU architecture by a Chinese-national has underscored the risk of insider threats and the importance of endpoint security systems like EDRs and DLPs in the prevention of cloud-based exfiltration methods.
According to the indictment by the DOJ, he exfiltrated the data by copying the contents of the document from the Google source files into the Apple Notes application on his Google work laptop, and then converting them from Apple Notes to PDFs to avoid detection by Google’s DLP systems. This is admittedly a clever, albeit stupid way, on circumventing endpoint security systems. So if Google, which probably spends millions a year on security software, can still get their proprietary information stolen using the Apple Notes app, how does your implementation fares?
While the process of dissecting endpoint security agents and drivers in Windows are well documented, the same can't be said for macOS-based security solutions. While the implementation of DLP/EDR systems will become easier with the advent of System Extensions and the Endpoint Security API, older implementations might look like a black box in comparison. So as a goodbye to kernel extensions, we're gonna take a quick dive into how security products use kernel extensions.
The Funny World of macOS Internals
macOS is based on the XNU kernel ("X is Not Unix"), which is a hybrid kernel that combines elements from both the Mach and BSD.
Mach is a UNIX-compatible microkernel which is designed to minimize the amount of code running in the kernel space and instead allow many typical kernel functions, such as file system, networking, and I/O, to run as user-level tasks. Mach is responsible for many low-level operations a kernel typically handles, such as processor scheduling, multitasking, and virtual memory management.
On the other hand, BSD contributes higher-level features, such as the POSIX API, file system management (through APFS), networking, and what will be the main focus of the article, the KAuth KPI. KAuth is the kernel programming interface (KPI) thats responsible for mediating actions that affect the system's security posture, such as file access, network operations, and process management. It operates by registering listeners for various authorization scopes, which then respond to authorization requests by allowing, denying, or deferring decisions based on the policy implemented by the listener.
XNU (Darwin) is a descendent of Rhapsody (OPENSTEP/NeXTSTEP), which also a heavily customized mix of components such as OSFMK (Mach), 4.4BSD and Yellow Box (which would eventually become Cocoa). The Mach microkernel at the time had better symmetric multiprocessing and memory protection capabilities, and by combining it with FreeBSD (which was derived from the Unix codebase) XNU could maintain compatibility with existing Unix programs and APIs used in many academic and professional settings at the time.
As BSD and Mach are built upon different conceptual frameworks which leads to some funny interactions between the two, such as :
In BSD, signals are delivered to processes. However, in Mach, signals are delivered to individual threads. XNU bridges this gap by delivering signals to the Mach thread that is associated with the BSD process that is intended to receive the signal.
When a new process is created via
fork()
in BSD, the child process inherits a copy of the parent's file descriptors. In Mach, tasks do not have a notion of file descriptors. XNU handles this by creating a new task for the child process and sharing the parent's file descriptor rights with the child task.BSD manages memory at the process level, while Mach manages memory at the task level. XNU maps the BSD process memory model to Mach's task-based memory management by creating a Mach virtual memory object for each BSD process.
BSD schedules processes, while Mach schedules threads. XNU's scheduler is primarily based on Mach's thread scheduling mechanisms, but it also takes BSD's process priorities into account when determining which threads to schedule.
Mach's security model is based on port rights, whereas BSD's security model operates based on process ownership. Disparities between these two models have occasionally resulted in local privilege-escalation vulnerabilities.
While Mach provides a clean mechanism for kernel extensions through tasks, BSD lacks a similar mechanism. XNU allows for kernel extensions by leveraging Mach's task infrastructure, enabling third-party code to run in the kernel space as user-level tasks.
The last part is what we are interested in. Kernel Extensions (kexts) on macOS are akin to drivers in Windows, extending the functionality of the macOS kernel. This is what usually endpoint security vendors rely on to hook the kernel for security events, specifically they use the Kernel Authorization KPI (KAuth KPI). The KAuth KPI provides a mechanism for kernel extensions to perform authorization checks and enforce security policies for various kernel operations. It allows kernel extensions to register callbacks for specific scopes and actions, and intervene in the authorization process.
Kernel Extensions and KAuth KPI
The KAuth KPI organizes operations into different scopes, each representing an area of interest for authorization. Some common scopes include:
KAUTH_SCOPE_VNODE
: Covers operations on file-like objects (vnodes) such as executing, reading, writing, or deleting files.KAUTH_SCOPE_FILEOP
: Provides advisory notifications for file system operations, useful for logging or cache invalidation.KAUTH_SCOPE_PROCESS
: Relates to process management operations like forking, executing, or tracing processes.
Within each scope, there are specific actions that your kernel extension can monitor and authorize. For example, within the KAUTH_SCOPE_VNODE
scope, the KAUTH_VNODE_EXECUTE
action represents the execution of a file.
To use the KAuth KPI, your kernel extension needs to register a listener callback for the desired scope and action.
kern_return_t RegisterListener() {
vnode_listener_ = kauth_listen_scope(
KAUTH_SCOPE_VNODE, vnode_scope_callback, reinterpret_cast<void *>(this));
if (!vnode_listener_) return kIOReturnInternalError;
return kIOReturnSuccess;
}
The kauth_listen_scope
function registers a callback function (vnode_scope_callback
in this example) for the specified scope (KAUTH_SCOPE_VNODE
). The third argument is a cookie that will be passed to the callback function, allowing you to associate it with your kernel extension's context.
The listener callback function is where you can inspect the operation being performed and make an authorization decision.
extern "C" int vnode_scope_callback(
kauth_cred_t credential, void *cookie, kauth_action_t action,
uintptr_t arg0, uintptr_t arg1, uintptr_t arg2, uintptr_t arg3) {
// check if the action is KAUTH_VNODE_EXECUTE
if ((action & KAUTH_VNODE_EXECUTE) && !(action & KAUTH_VNODE_ACCESS)) {
// retrieve the vnode and VFS context from the arguments
vnode_t vp = reinterpret_cast<vnode_t>(arg1);
vfs_context_t context = reinterpret_cast<vfs_context_t>(arg0);
// perform authorization check
int result = AuthorizeFileExecution(credential, context, vp);
return result;
}
// defer to other listeners for actions we don't handle
return KAUTH_RESULT_DEFER;
}
In this example, the callback function checks if the action is KAUTH_VNODE_EXECUTE
. If it is, it retrieves the vnode and VFS context from the arguments (arg1
and arg0
, respectively). It then calls a AuthorizeFileExecution
function to perform the authorization check based on the provided credentials, context, and vnode. The result of this authorization check is returned to the KAuth KPI.
If the action is not KAUTH_VNODE_EXECUTE
, the callback function returns KAUTH_RESULT_DEFER
, deferring the authorization decision to other registered listeners or the default BSD permission model. The listener callback function needs to return one of the following values to the KAuth KPI, indicating the authorization decision:
KAUTH_RESULT_ALLOW
: Allow the operation to proceed.KAUTH_RESULT_DENY
: Deny the operation and prevent it from happening.KAUTH_RESULT_DEFER
: Defer the decision to other registered listeners or the default BSD permission model.
The decision-making logic for authorizing an operation can be as simple or complex as needed, depending on your security requirements. It could involve checking file signatures, consulting a whitelist or blacklist, or performing more advanced policy evaluations.
We can pretty quickly gather what a kext is doing by inspecting its properties list file (plist).
// info.plist for one kext of a market-leading DLP provider
// some lines are edited for clarity
<key>OSBundleLibraries</key>
<dict>
<key>com.apple.kpi.bsd</key>
<string>10.0.0</string>
<key>com.apple.kpi.iokit</key>
<string>10.0.0</string>
<key>com.apple.kpi.libkern</key>
<string>10.0.0</string>
<key>com.apple.kpi.mach</key>
<string>10.0.0</string>
<key>com.product.endpoint.process</key>
<string>1.0.0</string>
</dict>
At a high level, main DLP component launches and connects to its kernel extension. Then the kernel extension uses a number of other kernel extensions to perform certain operations :
com.apple.kpi.libkern
is a foundational library commonly used in the development of kernel extensions, which offers a base for creating and manipulating kexts.com.apple.kpi.bsd
which is used to access the BSD subsystem inside XNU to monitor intercept file operations, network communications, or process activities to prevent unauthorized data exfiltrationcom.apple.kpi.mach
which is used to access the Mach subsystem inside XNU to monitor for in-memory execution and unauthorized threadscom.apple.kpi.iokit
which is used to monitor external device connections (e.g., USB drives) or network interfaces, enabling the extension to block or audit data transfers that could lead to data loss
Lets say a user invokes a command to copy a file, such as using cp
in zsh, the command triggers file-system operations that are handled by the Virtual File System (VFS). KAuth listeners that are registered for file operations (VNODE scope) will receive notifications of the read and write requests. The kauth_action_t
will correspond to the file operations such as KAUTH_VNODE_READ_DATA
for reading from the source file and KAUTH_VNODE_WRITE_DATA
for writing to the destination.
// NOTE : This code is heavily edited for readibility
// the callback function for vnode scope events
static int Listener(
kauth_cred_t credential,
void * idata,
kauth_action_t action,
uintptr_t arg0,
uintptr_t arg1,
uintptr_t arg2,
uintptr_t arg3) {
// create human-readable paths and actions
err = CreateVnodePath(vp, &vpPath);
err = CreateVnodePath((vnode_t)arg1, &dvpPath);
err = CreateVnodeActionString(action, vnode_isdir(vp), &actionStr, &actionStrBufSize);
// refer to DLP Policy
char *dlpPolicy = NULL;
if (getPolicy(&dlpPolicy) == 0 && dlpPolicy != NULL) {
if (strcmp(vpPath, dlpPolicy) == 0 && (action & KAUTH_VNODE_WRITE_DATA)) {
// deny operation
result = KAUTH_RESULT_DENY;
}
}
if (vnode_isdir(vp) && (action & KAUTH_VNODE_ADD_FILE)) {
// allow peration
result = KAUTH_RESULT_ALLOW;
}
return result;
}
The security product receives these events and then forwards it to the agent policy holder, which finally decides whether to allow KAUTH_RESULT_ALLOW
, deny KAUTH_RESULT_DENY
, or defer to another listener KAUTH_RESULT_DEFER
for a decision (this is used when there is another product tacked onto the DLP that has additional policies in addition to the main DLP agent policy.
This is common as many endpoint security products bundle additional features or alternative products under one agent, which usually boil down to additional rulesets. For example, an additional check might be done by calling an alternative kext
.
static int Listener(
kauth_cred_t credential,
void * idata,
kauth_action_t action,
uintptr_t arg0,
uintptr_t arg1,
uintptr_t arg2,
uintptr_t arg3) {
// ... (previous code) ...
if (result == KAUTH_RESULT_DEFER) {
// call alternative kext to handle additional policy checks
result = CheckPolicy(credential, idata, action, arg0, arg1, arg2, arg3);
}
return result;
}
Process Protections Through TrustedBSD
While uninstalling endpoint security agents like DLPs and EDRs usually require a key generated from the master console, a user with root access to the system can simply remove the kext files and completely uninstall the agent. While removing admin access from the device can prove useful, users can still reset the password for a locked admin account by using Recovery Mode in macOS by using the resetpassword
utility in the terminal provided.
This is where process protection kexts enters the picture. This starts with the mac_policy_register
function, which is part of the TrustedBSD Mandatory Access Control (MAC) framework. This works similarly to how SELinux locks down certain linux processes and files from tampering.
TrustedBSD itself was introduced in Mac OS X 10.5. and is used by Apple to isolate applications from interacting with user-controlled objects. While the implementation of the sandbox isn't designed to protect an application from user tampering, many security vendors use it to do just that.
// callback function to handle process signal events
static int mpo_proc_check_signal_callback(kauth_cred_t cred, struct proc *p, int signum) {
char procName[MAXCOMLEN + 1];
proc_selfname(procName, sizeof(procName));
// check if the process being signaled is your DLP application
if (strcmp(procName, "com.company.endpoint.dlp") == 0) {
// block the signal if it's a termination signal (e.g., SIGTERM, SIGKILL)
if (signum == SIGTERM || signum == SIGKILL) {
return EPERM; // deny the signal
}
}
return 0; // allow the signal
}
// struct to hold the callback function pointers
static struct mac_policy_ops mac_ops = {
.mpo_proc_check_signal = mpo_proc_check_signal_callback,
// add other callback functions as needed
};
// struct to configure the policy
static struct mac_policy_conf mac_policy_conf = {
.mpc_name = "com.company.endpoint.dlp",
.mpc_labelname_count = 0,
.mpc_ops = &mac_ops,
.mpc_loadtime_flags = 0, // make the policy non-unloadable
.mpc_field_off = NULL,
.mpc_runtime_flags = 0
};
// register the policy during kext initialization
kern_return_t DLPProtectKext_start(kmod_info_t *ki, void *d) {
mac_policy_handle_t handle;
int error = mac_policy_register(&mac_policy_conf, &handle, d);
if (error != 0) {
printf("Failed to register DLP protection policy\n");
return KERN_FAILURE;
}
return KERN_SUCCESS;
}
In this example, the mpo_proc_check_signal_callback
function checks if the process being signaled is your DLP application (com.mycompany.dlp
). If it is, and the signal is a termination signal (SIGTERM
or SIGKILL
), the function returns EPERM
to deny the signal. Otherwise, it allows the signal by returning 0
.
The mac_ops
struct holds the callback function pointer, and the mac_policy_conf
struct configures the policy with a name, description, and the mac_ops
struct.
During the kernel extension's initialization (DLPProtectKext_start
), the policy is registered with the TrustedBSD framework using mac_policy_register
. The mpc_loadtime_flags
field is set to 0
to make the policy non-unloadable.
You can also utilize kexts to monitor for the PIDs dynamically by performing a bitwise operation on the signal number (SIGTERM
/SIGKILL
) and checks against a mask. It then checks if the calling process or the target process is the PID belonging to the protected process.
// callback function to handle process signal events
static int mpo_proc_check_signal_callback(kauth_cred_t cred, struct proc *p, int signum) {
pid_t calling_pid = proc_selfpid();
pid_t target_pid = proc_pid(p);
// check if the calling process or the target process is a trusted PID
for (int i = 0; i < num_trusted_pids; i++) {
if (calling_pid == trusted_pids[i] || target_pid == trusted_pids[i]) {
// block the signal if it's a termination signal (e.g., SIGTERM, SIGKILL)
if (signum == SIGTERM || signum == SIGKILL) {
return EPERM; // deny the signal
}
}
}
return 0; // allow the signal
}
// struct to hold the callback function pointers
static struct mac_policy_ops mac_ops = {
.mpo_proc_check_signal = mpo_proc_check_signal_callback,
// add other callback functions as needed
};
// struct to configure the policy
static struct mac_policy_conf mac_policy_conf = {
.mpc_name = "com.company.endpoint.dlp",
.mpc_labelname_count = 0,
.mpc_ops = &mac_ops,
.mpc_loadtime_flags = 0, // make the policy non-unloadable
.mpc_field_off = NULL,
.mpc_runtime_flags = 0
};
To block the SIGTERM or SIGKILL signal from being delivered to your trusted process, you can modify the mpo_proc_check_signal_callback
function to return EPERM
when the signal is detected as a termination signal, and the target process is a trusted PID.
This is why sometimes if you try to terminate the process of a kext-based security product or try to copy the configuration files in the Library folder, you'll receive an error message despite even having root level privileges. This is probably because there is a secondary agent that monitors and protects the integrity of the process and the binaries related to it, and sends a KAUTH_RESULT_DENY
message.
Limitations of Kext-based Security Products
One of the key issues with kernel extensions is their ability to introduce stability and security problems. Since kexts operate within the kernel, they bypass the usual macOS security mechanisms such as Gatekeeper and System Integrity Protection (SIP). There has also been alot of documented cases of third-party kernel extensions being broken because of a system update or causing system instability, this is due to Apple making constant revisions to kernel interfaces which third-party devs may not have clear insight to. The security risk posed by having usermode agents interact with kernel components have also been documented in Windows.
This is where the previously mentioned Endpoint Security API takes the torch.
Implementation using Endpoint Security API
The new Apple Endpoint Security (ES) API has largely replaced the KAuth KPI, which now generates a warning at compile time and uses the message __kpi_deprecated("Use EndpointSecurity instead")
to warn developers of the impending transitition. Some security vendors for macOS have either already transititioned to the ES API, have built their products around usermode-based security protections since the beginning, or are still planning to move their legacy kext-based implementations to system extensions.
While in the article before, we know that the ES API can give security products a rich event streams to capture, log, and prevent certain operations through an apple-built kernel extension. But there are also other benefits to being an ES application :
The process becomes protected by System Integrity Protection (SIP) preventing tampering of the extension and related processes by the user or external threat actors, making third-party security products enjoy the same level of protection as SIP-protected Apple binaries
There is also a greater level of protection for the daemon similar to the protection given to Apple-made system daemons, which means even root users cannot unload your
launchd
job (similar to Process Protection Light (PPL) processes in Windows)Your system extension can also launch and setup an event stream before other applications are able to execute (similar to Early Launch Anti-Malware (ELAM) drivers in Windows)
Like the KAuth KPI, the Endpoint Security API allows system extensions to subscribe to specific event types and receive notifications or authorization requests for those events. The system extension can then make decisions to allow or deny the requested operations based on its security policies. Both APIs also provide a callback mechanism for system extensions to receive event notifications and make authorization decisions (but the Endpoint Security API uses a more streamlined callback approach, as we'll see shortly)
One of the main differences between the Endpoint Security API and KAuth KPI is the execution environment. While KAuth operates within the kernel, the Endpoint Security API runs entirely in user space, making it more suitable for modern system extensions that are moving away from the kernel.
Another key difference is the event granularity. The Endpoint Security API provides a more fine-grained set of event types compared to the broad scopes offered by KAuth. This allows for better control over what events a system extension can monitor and authorize.
There are two ways to connect to the ES API, firstly using a launch daemon to act as a regular system scope daemon that will require the process running as root and also through the building of a system extesion to act as user-space receiver kernel extension (via EndpointSecurity.kext
).
Building your product as a system scope daemon is ideal for analysis and research tools (similar to Sysmon on Windows) as users don't need to deal with the system extension installation process and can connect immediately to the event stream. Building your product as a system extension is ideal for endpoint security products, that might enjoy the protections given by SIP to defend from potential tampering and also allows the extension to setup an event stream before other applications are active.
While subscribing to ES events is not difficult programmatically, ES is considered a managed capability in macOS. This means that to start building an ES application, it requires getting the com.apple.developer.endpoint-security.client
entitlement, which requires an entitlement from Apple via the Apple Developer System Extensions Request Form.
Once you get the approval for the entitlement, you or your organization can create system extensions and ES-enabled userspace apps freely. However, if you would rather skip this long and expensive process you can also subscribe and run non-entitled ES API applications by disabling SIP.
In creating ES apps, its good that EndpointSecurity
is offered as a C API its able to be used in alot of memory safe languages such as C/C++, Swift, Objective-C, and Rust. The code below to me is so straightforward and readable compared to the hieroglyphic-like KPI code from above that its ridiculous. This also has the benefit of reducing the possibilities for a memory corruption exploit, which is a top source of exploit in KPI implementations.
To use the Endpoint Security API, a system extension must create an Endpoint Security client and register a callback function.
es_client_t *client = NULL;
es_new_client_result_t ret = es_new_client(&client,
^(es_client_t *c, const es_message_t *m) {
// callback method
...
}
);
The es_new_client
function creates a new Endpoint Security client and takes a block (essentially a lambda function in C) as an argument. This block will be called whenever an event occurs that the client has subscribed to. Once an Endpoint Security client is created, the system extension can subscribe to specific event types it wants to monitor:
es_event_type_t events[] = { ES_EVENT_TYPE_AUTH_EXEC, ES_EVENT_TYPE_NOTIFY_EXIT };
es_return_t sret = es_subscribe(self.client, events, 2);
In this example, the system extension subscribes to the ES_EVENT_TYPE_AUTH_EXEC
event type, which represents executable file execution, and ES_EVENT_TYPE_NOTIFY_EXIT
, which notifies when a process exits.
When an event occurs that the client has subscribed to, the callback function registered with es_new_client
is invoked. The callback function receives a pointer to the client and a pointer to the event message (es_message_t
).
(es_client_t *c, const es_message_t *m) {
// check the event type
switch (m->action_type) {
case ES_ACTION_TYPE_AUTH:
// handle authorization events
switch (m->event_type) {
case ES_EVENT_TYPE_AUTH_EXEC:
// handle executable file execution
...
// respond with the authorization decision
es_respond_auth_result(c, m, ES_AUTH_RESULT_ALLOW);
break;
...
}
break;
case ES_ACTION_TYPE_NOTIFY:
// handle notification events
switch (m->event_type) {
case ES_EVENT_TYPE_NOTIFY_EXIT:
// handle process exit notification
...
break;
...
}
break;
...
}
}
The callback function then checks the action_type
of the event. If it's an authorization event (ES_ACTION_TYPE_AUTH
), the function further inspects the event_type
to determine the specific event, such as ES_EVENT_TYPE_AUTH_EXEC
for executable file execution.
For authorization events, the system extension can make a decision to allow or deny the operation by calling es_respond_auth_result
with the appropriate ES_AUTH_RESULT_ALLOW
or ES_AUTH_RESULT_DENY
value. For notification events (ES_ACTION_TYPE_NOTIFY
), the system extension can perform any necessary actions, such as logging or cache invalidation, but cannot influence the event outcome.
// an example handler to make auth (allow or block) decisions.
// returns either ES_AUTH_RESULT_ALLOW or ES_AUTH_RESULT_DENY.
es_auth_result_t auth_event_handler(const es_message_t *msg) {
switch (msg->event_type) {
case ES_EVENT_TYPE_AUTH_OPEN:
// access the event-specific data from the message union
const es_event_auth_open_t *openEvent = &msg->event.auth.open;
// check if the process is an Endpoint Security client
if (openEvent->target->is_es_client) {
return ES_AUTH_RESULT_ALLOW;
}
// get the file path
char filePath[PATH_MAX];
strlcpy(filePath, openEvent->target->vnode.path, sizeof(filePath));
// check if the process is vim trying to access a text file
if (strstr(openEvent->target->proc.name, "vim") && strstr(filePath, ".txt")) {
LOG_IMPORTANT_INFO("BLOCKING OPEN: %s", filePath);
return ES_AUTH_RESULT_DENY;
}
// all good
return ES_AUTH_RESULT_ALLOW;
default:
return ES_AUTH_RESULT_ALLOW;
}
}
One important aspect of the Endpoint Security API is the requirement to respond to authorization events by a specified deadline. If a system extension fails to respond in time, Apple may terminate the extension. There are a few solutions in the while i've seen to overcome this.
You can set a timer to issue a "deny" response shortly before the deadline in case the main daemon fails to respond in time. The idea is to start a timer when you receive an authorization event. If the timer expires before your authorization logic completes, you automatically send a "deny" response to the Endpoint Security API. This ensures that you always respond before the deadline, preventing Apple from terminating your extension.
// Start a watchdog timer when receiving an auth event
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_source_set_timer(timer, dispatch_walltime(NULL, NSEC_PER_SEC * (deadline - 2)), DISPATCH_TIME_FOREVER, 0);
dispatch_source_set_event_handler(timer, ^{
// Time is up, deny the event
es_respond_auth_result(client, msg, ES_AUTH_RESULT_DENY);
dispatch_source_cancel(timer);
});
dispatch_resume(timer);
// Run your authorization logic
es_auth_result_t result = auth_event_handler(msg);
// If logic completes before the timer, cancel the timer
dispatch_source_cancel(timer);
es_respond_auth_result(client, msg, result);
Another approach is to perform your authorization logic asynchronously, preferably on a separate thread or queue. This way, your main thread can respond to the Endpoint Security API within the deadline, while the asynchronous task handles the actual authorization decision.
Copy codedispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
// Run your authorization logic on a separate queue
es_auth_result_t result = auth_event_handler(msg);
dispatch_async(dispatch_get_main_queue(), ^{
// Respond on the main queue
es_respond_auth_result(client, msg, result);
});
});
// Respond with a temporary "allow" decision to meet the deadline
es_respond_auth_result(client, msg, ES_AUTH_RESULT_ALLOW);
In this example, the authorization logic runs on a separate queue, while the main queue responds with a temporary "allow" decision to meet the deadline. Once the asynchronous task completes, it dispatches back to the main queue to send the actual authorization decision.
Comapred to KPIs, being an EndpointSecurity
app in macOS has plenty of benefits both from an agent protection/compatibility perspective and from a developer experience perspective. It's definitely the way that the industry in general is moving with major vendors like Crowdstrike, SentinelOne, and Elastic already supports the new implementation.
But in the course of my research and work, i found that there are still alot of endpoint security products not using the new System Extensions method. You can figure out which products are currently using System Extensions by using the systemextensionsctl list
command.
In macOS Ventura and above, if you still want to use security products with the deprecated kernel extension methods you can still do so by going into RecoveryOS and enable Reduced Security
mode. This will both allow EDR/DLP products and also MDM products with kernel extensions to remain running in your system.
But this is truly a stopgap solution, if you are in a position where you have to compromise security because your vendor (which was already given around a four year transition gap to adapt to the new standards) haven't migrated to System Extensions, you should contact your principal or account manager immediately to pressure them to migrate. But your milelage may (most likely will) vary.