macOS EndpointSecurity客户端指定路径监听问题排查与实现
macOS EndpointSecurity 客户端问题排查与优化
一、无法监听目标路径的核心问题
你的代码存在关键逻辑错误:误将触发事件的进程可执行文件路径,当作被操作的文件路径进行判断,导致永远匹配不到目标目录的事件。
问题代码分析
在handleEvent函数中,你获取的msg->process->executable->path.data是触发事件的进程路径(比如打开文件的TextEdit路径为/Applications/TextEdit.app/Contents/MacOS/TextEdit),而非被操作的文件路径。因此[filePathString hasPrefix:path]的判断永远不成立,自然不会输出目标路径的事件日志。
解决方法
针对不同事件类型,获取对应的目标文件路径,示例对应关系:
ES_EVENT_TYPE_NOTIFY_OPEN→msg->event.open.target->path.dataES_EVENT_TYPE_NOTIFY_CREATE→msg->event.create.target->path.dataES_EVENT_TYPE_NOTIFY_WRITE→msg->event.write.target->path.data- 其他事件类型需参照EndpointSecurity框架文档中
es_message_t的结构定义获取对应路径。
二、仅保留目标路径监听(避免事件队列溢出)
EndpointSecurity默认推送所有路径的事件,若要仅监听指定路径,正确策略是:
- 先静音根目录
/,屏蔽所有路径的事件 - 再解除目标路径的静音,仅保留该路径的事件推送
这种方式能大幅减少事件数量,从根源避免队列溢出导致的事件丢失。
三、完整修正后的代码
#include <EndpointSecurity/EndpointSecurity.h> #include <os/log.h> #include <dispatch/dispatch.h> #include <Foundation/Foundation.h> static void handleEvent(es_client_t *client, const es_message_t *msg) { const char *targetPath = NULL; // 根据事件类型获取对应的目标文件路径 switch (msg->event_type) { case ES_EVENT_TYPE_NOTIFY_CREATE: targetPath = msg->event.create.target->path.data; break; case ES_EVENT_TYPE_NOTIFY_OPEN: targetPath = msg->event.open.target->path.data; break; case ES_EVENT_TYPE_NOTIFY_RENAME: targetPath = msg->event.rename.target->path.data; break; case ES_EVENT_TYPE_NOTIFY_CLOSE: targetPath = msg->event.close.target->path.data; break; case ES_EVENT_TYPE_NOTIFY_WRITE: targetPath = msg->event.write.target->path.data; break; case ES_EVENT_TYPE_NOTIFY_UNLINK: targetPath = msg->event.unlink.target->path.data; break; case ES_EVENT_TYPE_NOTIFY_EXIT: // EXIT事件无目标文件路径,直接处理 break; default: os_log_error(OS_LOG_DEFAULT, "未知事件类型: %d", msg->event_type); return; } // 单独处理EXIT事件 if (msg->event_type == ES_EVENT_TYPE_NOTIFY_EXIT) { os_log(OS_LOG_DEFAULT, "%{public}s (进程ID: %d) | EXIT: 退出状态: %d", msg->process->executable->path.data, audit_token_to_pid(msg->process->audit_token), msg->event.exit.stat); return; } // 目标路径为空时直接返回 if (!targetPath) { return; } NSString *targetPathString = [NSString stringWithUTF8String:targetPath]; NSString *watchPath = @"/Users/anoopvaidya/Documents"; os_log(OS_LOG_DEFAULT, "目标路径: %@, 监听路径: %@", targetPathString, watchPath); // 判断是否为目标路径下的事件 if ([targetPathString hasPrefix:watchPath]) { os_log(OS_LOG_DEFAULT, "匹配到目标路径: %@", targetPathString); // 输出对应事件详情 switch (msg->event_type) { case ES_EVENT_TYPE_NOTIFY_CREATE: os_log(OS_LOG_DEFAULT, "CREATE: %{public}s", targetPath); break; case ES_EVENT_TYPE_NOTIFY_OPEN: os_log(OS_LOG_DEFAULT, "OPEN: %{public}s", targetPath); break; case ES_EVENT_TYPE_NOTIFY_RENAME: os_log(OS_LOG_DEFAULT, "RENAME: %{public}s -> %{public}s", targetPath, msg->event.rename.new_path->path.data); break; case ES_EVENT_TYPE_NOTIFY_CLOSE: os_log(OS_LOG_DEFAULT, "CLOSE: %{public}s", targetPath); break; case ES_EVENT_TYPE_NOTIFY_WRITE: os_log(OS_LOG_DEFAULT, "WRITE: %{public}s", targetPath); break; case ES_EVENT_TYPE_NOTIFY_UNLINK: os_log(OS_LOG_DEFAULT, "UNLINK: %{public}s", targetPath); break; default: break; } } } void setupPathFilter(es_client_t *client) { // 1. 先静音根目录,屏蔽所有路径 if (es_mute_path_prefix(client, "/") != ES_RETURN_SUCCESS) { os_log_error(OS_LOG_DEFAULT, "静音根目录失败"); } // 2. 解除目标路径的静音,仅监听该路径 NSString *watchPath = @"/Users/anoopvaidya/Documents"; if (es_unmute_path_prefix(client, [watchPath UTF8String]) != ES_RETURN_SUCCESS) { os_log_error(OS_LOG_DEFAULT, "解除目标路径静音失败"); } } int main(int argc, char *argv[]) { es_client_t *client = NULL; es_new_client_result_t newClientResult = es_new_client(&client, ^(es_client_t *c, const es_message_t *message) { handleEvent(client, message); }); if (newClientResult != ES_NEW_CLIENT_RESULT_SUCCESS) { os_log_error(OS_LOG_DEFAULT, "创建ES客户端失败: %d", newClientResult); return 1; } // 设置路径过滤规则 setupPathFilter(client); // 订阅需要的事件类型 es_event_type_t events[] = { ES_EVENT_TYPE_NOTIFY_CREATE, ES_EVENT_TYPE_NOTIFY_OPEN, ES_EVENT_TYPE_NOTIFY_RENAME, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_WRITE, ES_EVENT_TYPE_NOTIFY_UNLINK, ES_EVENT_TYPE_NOTIFY_EXIT }; if (es_subscribe(client, events, sizeof(events) / sizeof(events[0])) != ES_RETURN_SUCCESS) { os_log_error(OS_LOG_DEFAULT, "订阅事件失败"); es_delete_client(client); return 1; } dispatch_main(); }
额外注意事项
- 必须以root权限运行客户端,否则无法获取部分用户目录的事件
- 测试时可先简化逻辑,比如去掉路径过滤,确认能收到事件后再逐步添加规则
- EndpointSecurity对高频率事件可能进行合并或丢弃,精准的路径过滤是避免该问题的最优方案
内容的提问来源于stack exchange,提问作者Anoop Vaidya
相关产品推荐
相关产品推荐

