-
-
Save Omar-Ikram/8e6721d8e83a3da69b31d4c2612a68ba to your computer and use it in GitHub Desktop.
// | |
// main.m | |
// EndpointSecurityDemo | |
// | |
// Created by Omar Ikram on 17/06/2019 - macOS Catalina 10.15 Beta 1 (19A471t) | |
// Updated by Omar Ikram on 15/08/2019 - macOS Catalina 10.15 Beta 5 (19A526h) | |
// Updated by Omar Ikram on 01/12/2019 - macOS Catalina 10.15 (19A583) | |
// Updated by Omar Ikram on 31/01/2021 - macOS Big Sur 11.1 (20C69) | |
// Updated by Omar Ikram on 07/05/2021 - macOS Big Sur 11.3.1 (20E241) | |
// Updated by Omar Ikram on 04/07/2021 - macOS Monterey 12 Beta 2 (21A5268h) | |
// Updated by Omar Ikram on 08/01/2022 - macOS Monterey 12.1 (21C52) | |
// Updated by Omar Ikram on 15/02/2022 - macOS Monterey 12.2.1 (21D62) | |
// Updated by Omar Ikram on 04/01/2025 - macOS Sequoia 15.2 (24C101) | |
// | |
/* | |
A demo of using Apple's EndpointSecurity framework - tested on macOS Sequoia 15.2 (24C101). | |
Minimum supported version: macOS Catalina 10.15 | |
This demo is an update of previous demos, which has been updated to support the latest API changes | |
Apple has made for macOS Sequoia 15. | |
The demo has also been expanded significantly to include more detail and cover more of the API. | |
The code, hopefully, should be self explanatory. Important details are marked by a comment | |
starting with "Note:". | |
Disclaimer: | |
This code is provided as is and is only intended to be used for illustration purposes. This code is | |
not production-ready and is not meant to be used in a production environment. Use it at your own risk! | |
Setup: | |
1. Build with Xcode 16 (tested with Version 16.2 (16C5032a)), having the macOS deployment target set | |
to 10.15 (or later) and the Hardened Runtime capability enabled. | |
2. Link with libraries: | |
- libEndpointSecurity.tbd (Endpoint Security functions) | |
- libbsm.tbd (Audit Token functions) | |
- UniformTypeIdentifiers.framework (UTI functions, which is not available on macOS Catalina 10.15 | |
, so it needs to be optinally linked - e.g. with the '-weak_framework' linker option) | |
3. Codesign with entitlement 'com.apple.developer.endpoint-security.client'. | |
If your Apple Developer account has been granted the entitlement from Apple, then the program needs | |
to be compiled as an App (i.e. Application Bundle). This will allow you to assign a Provisioning | |
Profile to the program, which you need to have associated the entitlement to it. | |
If you have not been granted the entitlement. You can still build the program (as an App or Command | |
Line Tool), but it will only be able to run on a machine which has SIP disabled (best to use a VM). | |
Runtime: | |
1. Test environment should be a macOS 10.15+ machine. | |
2. Run the demo binary in a terminal as root (e.g. with sudo). | |
i) Running with no arguments will display a simple usage message. | |
ii) Running with the 'serial' argument will run the demo using | |
the example serial event message handler. | |
iii) Running with the 'asynchronous' argument will run the demo using | |
the example asynchronous event message handler. | |
iv) Adding the 'verbose' argument at the end will turn on verbose logging. | |
3. Terminal will display messages related to subscribed events. | |
4. The demo will demonstrate processing Endpoint Security event messages | |
serially or asynchronously (depending on the selected command line argument given). | |
The demo will also demonstrate using Endpoint Security Auth events to make the | |
following Auth based decisions: | |
i) Block the 'top' binary and 'Calculator' app bundle from running. | |
ii) Block 'vim' binary from reading plain text files. | |
5. CTL-C to exit. | |
*/ | |
#import <Foundation/Foundation.h> | |
#import <EndpointSecurity/EndpointSecurity.h> | |
#import <bsm/libbsm.h> | |
#import <signal.h> | |
#import <mach/mach_time.h> | |
#import <Kernel/kern/cs_blobs.h> | |
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h> | |
#import <Appkit/AppKit.h> | |
#import <libproc.h> | |
#pragma mark Globals | |
es_client_t *g_client = nil; | |
NSSet *g_blocked_paths = nil; | |
NSDateFormatter *g_date_formater = nil; | |
// Endpoint Security event handler selected at startup from the command line | |
es_handler_block_t g_handler = nil; | |
// Used to detect if any events have been dropped by the kernel | |
uint64_t g_global_seq_num = 0; | |
NSMutableDictionary *g_seq_nums = nil; | |
// Set to true if want to cache the results of an auth event response | |
bool g_cache_auth_results = false; | |
// Logs can become quite busy, especially when subscribing to ES_EVENT_TYPE_AUTH_OPEN events. | |
// Only log all event messages when the flag is enabled; | |
// otherwise only denied Auth event messages will be logged. | |
bool g_verbose_logging = false; | |
#pragma mark Helpers - Mach Absolute Time | |
// This could be running on either Apple Silicon or Intel based CPUs. | |
// We will need to apply timebase information when converting Mach absolute time to nanoseconds: | |
// https://developer.apple.com/documentation/apple_silicon/addressing_architectural_differences_in_your_macos_code#3616875 | |
// | |
// Note: Running x86_64 code running under Rosetta 2 will have timebase information for Intel CPUs. | |
// This will cause discrepancies when converting Mach absolute time values from Endpoint Security Messages. | |
// The best option would be to compile your client as a universal binary: | |
// https://developer.apple.com/documentation/xcode/building_a_universal_macos_binary | |
uint64_t MachTimeToNanoseconds(uint64_t machTime) { | |
uint64_t nanoseconds = 0; | |
static mach_timebase_info_data_t sTimebase; | |
if(sTimebase.denom == 0) | |
(void)mach_timebase_info(&sTimebase); | |
nanoseconds = ((machTime * sTimebase.numer) / sTimebase.denom); | |
return nanoseconds; | |
} | |
uint64_t MachTimeToSeconds(uint64_t machTime) { | |
return MachTimeToNanoseconds(machTime) / NSEC_PER_SEC; | |
} | |
#pragma mark Helpers - Code Signing | |
typedef struct { | |
const NSString* name; | |
int value; | |
} CSFlag; | |
#define CSFLAG(flag) {@#flag, flag} | |
// Code signing flags defined in cs_blobs.h | |
const CSFlag g_csFlags[] = { | |
CSFLAG(CS_VALID), CSFLAG(CS_ADHOC), CSFLAG(CS_GET_TASK_ALLOW), | |
CSFLAG(CS_INSTALLER), CSFLAG(CS_FORCED_LV), CSFLAG(CS_INVALID_ALLOWED), | |
CSFLAG(CS_HARD), CSFLAG(CS_KILL), CSFLAG(CS_CHECK_EXPIRATION), | |
CSFLAG(CS_RESTRICT), CSFLAG(CS_ENFORCEMENT), CSFLAG(CS_REQUIRE_LV), | |
CSFLAG(CS_ENTITLEMENTS_VALIDATED), CSFLAG(CS_NVRAM_UNRESTRICTED), | |
CSFLAG(CS_RUNTIME), CSFLAG(CS_LINKER_SIGNED), CSFLAG(CS_ALLOWED_MACHO), | |
CSFLAG(CS_EXEC_SET_HARD), CSFLAG(CS_EXEC_SET_KILL), CSFLAG(CS_EXEC_SET_ENFORCEMENT), | |
CSFLAG(CS_EXEC_INHERIT_SIP), CSFLAG(CS_KILLED), CSFLAG(CS_DYLD_PLATFORM), | |
CSFLAG(CS_PLATFORM_BINARY), CSFLAG(CS_PLATFORM_PATH), CSFLAG(CS_DEBUGGED), | |
CSFLAG(CS_SIGNED), CSFLAG(CS_DEV_CODE) | |
}; | |
NSString* codesigning_flags_str(const uint32_t codesigning_flags) { | |
NSMutableArray *match_flags = [NSMutableArray new]; | |
// Test which code signing flags have been set and add the matched ones to an array | |
for(uint32_t i = 0; i < (sizeof g_csFlags / sizeof *g_csFlags); i++) { | |
if((codesigning_flags & g_csFlags[i].value) == g_csFlags[i].value) { | |
[match_flags addObject:g_csFlags[i].name]; | |
} | |
} | |
return [match_flags componentsJoinedByString:@","]; | |
} | |
#pragma mark Helpers - Endpoint Security | |
NSString* esstring_to_nsstring(const es_string_token_t es_string_token) { | |
if(es_string_token.data && es_string_token.length > 0) { | |
// es_string_token.data is a pointer to a null-terminated string | |
return [NSString stringWithUTF8String:es_string_token.data]; | |
} else { | |
return @""; | |
} | |
} | |
const NSString* event_type_str(const es_event_type_t event_type) { | |
static const NSString *names[] = { | |
// The following events are available beginning in macOS 10.15 | |
@"ES_EVENT_TYPE_AUTH_EXEC", @"ES_EVENT_TYPE_AUTH_OPEN", @"ES_EVENT_TYPE_AUTH_KEXTLOAD", | |
@"ES_EVENT_TYPE_AUTH_MMAP", @"ES_EVENT_TYPE_AUTH_MPROTECT", @"ES_EVENT_TYPE_AUTH_MOUNT", | |
@"ES_EVENT_TYPE_AUTH_RENAME", @"ES_EVENT_TYPE_AUTH_SIGNAL", @"ES_EVENT_TYPE_AUTH_UNLINK", | |
@"ES_EVENT_TYPE_NOTIFY_EXEC", @"ES_EVENT_TYPE_NOTIFY_OPEN", @"ES_EVENT_TYPE_NOTIFY_FORK", | |
@"ES_EVENT_TYPE_NOTIFY_CLOSE", @"ES_EVENT_TYPE_NOTIFY_CREATE", @"ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA", | |
@"ES_EVENT_TYPE_NOTIFY_EXIT", @"ES_EVENT_TYPE_NOTIFY_GET_TASK", @"ES_EVENT_TYPE_NOTIFY_KEXTLOAD", | |
@"ES_EVENT_TYPE_NOTIFY_KEXTUNLOAD", @"ES_EVENT_TYPE_NOTIFY_LINK", @"ES_EVENT_TYPE_NOTIFY_MMAP", | |
@"ES_EVENT_TYPE_NOTIFY_MPROTECT", @"ES_EVENT_TYPE_NOTIFY_MOUNT", @"ES_EVENT_TYPE_NOTIFY_UNMOUNT", | |
@"ES_EVENT_TYPE_NOTIFY_IOKIT_OPEN", @"ES_EVENT_TYPE_NOTIFY_RENAME", @"ES_EVENT_TYPE_NOTIFY_SETATTRLIST", | |
@"ES_EVENT_TYPE_NOTIFY_SETEXTATTR", @"ES_EVENT_TYPE_NOTIFY_SETFLAGS", @"ES_EVENT_TYPE_NOTIFY_SETMODE", | |
@"ES_EVENT_TYPE_NOTIFY_SETOWNER", @"ES_EVENT_TYPE_NOTIFY_SIGNAL", @"ES_EVENT_TYPE_NOTIFY_UNLINK", | |
@"ES_EVENT_TYPE_NOTIFY_WRITE", @"ES_EVENT_TYPE_AUTH_FILE_PROVIDER_MATERIALIZE", | |
@"ES_EVENT_TYPE_NOTIFY_FILE_PROVIDER_MATERIALIZE", @"ES_EVENT_TYPE_AUTH_FILE_PROVIDER_UPDATE", | |
@"ES_EVENT_TYPE_NOTIFY_FILE_PROVIDER_UPDATE", @"ES_EVENT_TYPE_AUTH_READLINK", @"ES_EVENT_TYPE_NOTIFY_READLINK", | |
@"ES_EVENT_TYPE_AUTH_TRUNCATE", @"ES_EVENT_TYPE_NOTIFY_TRUNCATE", @"ES_EVENT_TYPE_AUTH_LINK", | |
@"ES_EVENT_TYPE_NOTIFY_LOOKUP", @"ES_EVENT_TYPE_AUTH_CREATE", @"ES_EVENT_TYPE_AUTH_SETATTRLIST", | |
@"ES_EVENT_TYPE_AUTH_SETEXTATTR", @"ES_EVENT_TYPE_AUTH_SETFLAGS", @"ES_EVENT_TYPE_AUTH_SETMODE", | |
@"ES_EVENT_TYPE_AUTH_SETOWNER", | |
// The following events are available beginning in macOS 10.15.1 | |
@"ES_EVENT_TYPE_AUTH_CHDIR", @"ES_EVENT_TYPE_NOTIFY_CHDIR", @"ES_EVENT_TYPE_AUTH_GETATTRLIST", | |
@"ES_EVENT_TYPE_NOTIFY_GETATTRLIST", @"ES_EVENT_TYPE_NOTIFY_STAT", @"ES_EVENT_TYPE_NOTIFY_ACCESS", | |
@"ES_EVENT_TYPE_AUTH_CHROOT", @"ES_EVENT_TYPE_NOTIFY_CHROOT", @"ES_EVENT_TYPE_AUTH_UTIMES", | |
@"ES_EVENT_TYPE_NOTIFY_UTIMES", @"ES_EVENT_TYPE_AUTH_CLONE", @"ES_EVENT_TYPE_NOTIFY_CLONE", | |
@"ES_EVENT_TYPE_NOTIFY_FCNTL", @"ES_EVENT_TYPE_AUTH_GETEXTATTR", @"ES_EVENT_TYPE_NOTIFY_GETEXTATTR", | |
@"ES_EVENT_TYPE_AUTH_LISTEXTATTR", @"ES_EVENT_TYPE_NOTIFY_LISTEXTATTR", @"ES_EVENT_TYPE_AUTH_READDIR", | |
@"ES_EVENT_TYPE_NOTIFY_READDIR", @"ES_EVENT_TYPE_AUTH_DELETEEXTATTR", @"ES_EVENT_TYPE_NOTIFY_DELETEEXTATTR", | |
@"ES_EVENT_TYPE_AUTH_FSGETPATH", @"ES_EVENT_TYPE_NOTIFY_FSGETPATH", @"ES_EVENT_TYPE_NOTIFY_DUP", | |
@"ES_EVENT_TYPE_AUTH_SETTIME", @"ES_EVENT_TYPE_NOTIFY_SETTIME", @"ES_EVENT_TYPE_NOTIFY_UIPC_BIND", | |
@"ES_EVENT_TYPE_AUTH_UIPC_BIND", @"ES_EVENT_TYPE_NOTIFY_UIPC_CONNECT", @"ES_EVENT_TYPE_AUTH_UIPC_CONNECT", | |
@"ES_EVENT_TYPE_AUTH_EXCHANGEDATA", @"ES_EVENT_TYPE_AUTH_SETACL", @"ES_EVENT_TYPE_NOTIFY_SETACL", | |
// The following events are available beginning in macOS 10.15.4 | |
@"ES_EVENT_TYPE_NOTIFY_PTY_GRANT", @"ES_EVENT_TYPE_NOTIFY_PTY_CLOSE", @"ES_EVENT_TYPE_AUTH_PROC_CHECK", | |
@"ES_EVENT_TYPE_NOTIFY_PROC_CHECK", @"ES_EVENT_TYPE_AUTH_GET_TASK", | |
// The following events are available beginning in macOS 11.0 | |
@"ES_EVENT_TYPE_AUTH_SEARCHFS", @"ES_EVENT_TYPE_NOTIFY_SEARCHFS", @"ES_EVENT_TYPE_AUTH_FCNTL", | |
@"ES_EVENT_TYPE_AUTH_IOKIT_OPEN", @"ES_EVENT_TYPE_AUTH_PROC_SUSPEND_RESUME", | |
@"ES_EVENT_TYPE_NOTIFY_PROC_SUSPEND_RESUME", @"ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED", | |
@"ES_EVENT_TYPE_NOTIFY_GET_TASK_NAME", @"ES_EVENT_TYPE_NOTIFY_TRACE", | |
@"ES_EVENT_TYPE_NOTIFY_REMOTE_THREAD_CREATE", @"ES_EVENT_TYPE_AUTH_REMOUNT", @"ES_EVENT_TYPE_NOTIFY_REMOUNT", | |
// The following events are available beginning in macOS 11.3 | |
@"ES_EVENT_TYPE_AUTH_GET_TASK_READ", @"ES_EVENT_TYPE_NOTIFY_GET_TASK_READ", | |
@"ES_EVENT_TYPE_NOTIFY_GET_TASK_INSPECT", | |
// The following events are available beginning in macOS 12.0 | |
@"ES_EVENT_TYPE_NOTIFY_SETUID", @"ES_EVENT_TYPE_NOTIFY_SETGID", @"ES_EVENT_TYPE_NOTIFY_SETEUID", | |
@"ES_EVENT_TYPE_NOTIFY_SETEGID", @"ES_EVENT_TYPE_NOTIFY_SETREUID", | |
@"ES_EVENT_TYPE_NOTIFY_SETREGID", @"ES_EVENT_TYPE_AUTH_COPYFILE", @"ES_EVENT_TYPE_NOTIFY_COPYFILE", | |
// The following events are available beginning in macOS 13.0 | |
@"ES_EVENT_TYPE_NOTIFY_AUTHENTICATION", @"ES_EVENT_TYPE_NOTIFY_XP_MALWARE_DETECTED", | |
@"ES_EVENT_TYPE_NOTIFY_XP_MALWARE_REMEDIATED", @"ES_EVENT_TYPE_NOTIFY_LW_SESSION_LOGIN", | |
@"ES_EVENT_TYPE_NOTIFY_LW_SESSION_LOGOUT", @"ES_EVENT_TYPE_NOTIFY_LW_SESSION_LOCK", | |
@"ES_EVENT_TYPE_NOTIFY_LW_SESSION_UNLOCK", @"ES_EVENT_TYPE_NOTIFY_SCREENSHARING_ATTACH", | |
@"ES_EVENT_TYPE_NOTIFY_SCREENSHARING_DETACH", @"ES_EVENT_TYPE_NOTIFY_OPENSSH_LOGIN", | |
@"ES_EVENT_TYPE_NOTIFY_OPENSSH_LOGOUT", @"ES_EVENT_TYPE_NOTIFY_LOGIN_LOGIN", | |
@"ES_EVENT_TYPE_NOTIFY_LOGIN_LOGOUT", @"ES_EVENT_TYPE_NOTIFY_BTM_LAUNCH_ITEM_ADD", | |
@"ES_EVENT_TYPE_NOTIFY_BTM_LAUNCH_ITEM_REMOVE", | |
// The following events are available beginning in macOS 14.0 | |
@"ES_EVENT_TYPE_NOTIFY_PROFILE_ADD", @"ES_EVENT_TYPE_NOTIFY_PROFILE_REMOVE", @"ES_EVENT_TYPE_NOTIFY_SU", | |
@"ES_EVENT_TYPE_NOTIFY_AUTHORIZATION_PETITION", @"ES_EVENT_TYPE_NOTIFY_AUTHORIZATION_JUDGEMENT", | |
@"ES_EVENT_TYPE_NOTIFY_SUDO", @"ES_EVENT_TYPE_NOTIFY_OD_GROUP_ADD", @"ES_EVENT_TYPE_NOTIFY_OD_GROUP_REMOVE", | |
@"ES_EVENT_TYPE_NOTIFY_OD_GROUP_SET", @"ES_EVENT_TYPE_NOTIFY_OD_MODIFY_PASSWORD", | |
@"ES_EVENT_TYPE_NOTIFY_OD_DISABLE_USER", @"ES_EVENT_TYPE_NOTIFY_OD_ENABLE_USER", | |
@"ES_EVENT_TYPE_NOTIFY_OD_ATTRIBUTE_VALUE_ADD", @"ES_EVENT_TYPE_NOTIFY_OD_ATTRIBUTE_VALUE_REMOVE", | |
@"ES_EVENT_TYPE_NOTIFY_OD_ATTRIBUTE_SET", @"ES_EVENT_TYPE_NOTIFY_OD_CREATE_USER", | |
@"ES_EVENT_TYPE_NOTIFY_OD_CREATE_GROUP", @"ES_EVENT_TYPE_NOTIFY_OD_DELETE_USER", | |
@"ES_EVENT_TYPE_NOTIFY_OD_DELETE_GROUP", @"ES_EVENT_TYPE_NOTIFY_XPC_CONNECT", | |
// The following events are available beginning in macOS 15.0 | |
@"ES_EVENT_TYPE_NOTIFY_GATEKEEPER_USER_OVERRIDE" | |
}; | |
if(event_type >= ES_EVENT_TYPE_LAST) { | |
return [NSString stringWithFormat:@"Unknown/Unsupported event type: %d", event_type]; | |
} | |
return names[event_type]; | |
} | |
NSString* events_str(size_t count, const es_event_type_t* events) { | |
NSMutableArray *arr = [NSMutableArray new]; | |
for(size_t i = 0; i < count; i++) { | |
[arr addObject:event_type_str(events[i])]; | |
} | |
return [arr componentsJoinedByString:@", "]; | |
} | |
// On macOS Big Sur 11, Apple have deprecated es_copy_message in favour of es_retain_message | |
es_message_t * copy_message(const es_message_t * msg) { | |
if(@available(macOS 11.0, *)) { | |
es_retain_message(msg); | |
// simulate a copy | |
return (es_message_t*) msg; | |
} else { | |
return es_copy_message(msg); | |
} | |
} | |
// On macOS Big Sur 11, Apple have deprecated es_free_message in favour of es_release_message | |
void free_message(es_message_t * _Nonnull msg) { | |
if(@available(macOS 11.0, *)) { | |
es_release_message(msg); | |
} else { | |
es_free_message(msg); | |
} | |
} | |
#pragma mark Helpers - Misc | |
NSString* fdtype_str(const uint32_t fdtype) { | |
switch(fdtype) { | |
case PROX_FDTYPE_ATALK: return @"ATALK"; | |
case PROX_FDTYPE_VNODE: return @"VNODE"; | |
case PROX_FDTYPE_SOCKET: return @"SOCKET"; | |
case PROX_FDTYPE_PSHM: return @"PSHM"; | |
case PROX_FDTYPE_PSEM: return @"PSEM"; | |
case PROX_FDTYPE_KQUEUE: return @"KQUEUE"; | |
case PROX_FDTYPE_PIPE: return @"PIPE"; | |
case PROX_FDTYPE_FSEVENTS: return @"FSEVENTS"; | |
case PROX_FDTYPE_NETPOLICY: return @"NETPOLICY"; | |
default: return [NSString stringWithFormat:@"Unknown/Unsupported fdtype: %d", | |
fdtype]; | |
} | |
} | |
void init_date_formater(void) { | |
// Display dates in RFC 3339 date and time format: https://www.ietf.org/rfc/rfc3339.txt | |
g_date_formater = [NSDateFormatter new]; | |
g_date_formater.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; | |
g_date_formater.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZZZZZ"; | |
g_date_formater.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; | |
} | |
NSString* formatted_date_str(__darwin_time_t secs_since_1970) { | |
NSDate *date = [NSDate dateWithTimeIntervalSince1970:secs_since_1970]; | |
return [g_date_formater stringFromDate:date]; | |
} | |
bool is_system_file(const NSString* path) { | |
// For the purpose of this demo. A system file is a file that is under these directories: | |
for(NSString* prefix in @[@"/System/", @"/usr/share/"]) { | |
if([path hasPrefix:prefix]) { | |
return true; | |
} | |
} | |
return false; | |
} | |
bool is_plain_text_file(const NSString* path) { | |
if(@available(macOS 11.0, *)) { | |
UTType* utt = [UTType typeWithFilenameExtension:[path pathExtension]]; | |
return [utt conformsToType:UTTypePlainText]; | |
} else { | |
return [[NSWorkspace sharedWorkspace] | |
filenameExtension:[path pathExtension] | |
isValidForType:@"public.plain-text"]; | |
} | |
} | |
char* filetype_str(const mode_t st_mode) { | |
switch(((st_mode) & S_IFMT)) { | |
case S_IFBLK: return "BLK"; | |
case S_IFCHR: return "CHR"; | |
case S_IFDIR: return "DIR"; | |
case S_IFIFO: return "FIFO"; | |
case S_IFREG: return "REG"; | |
case S_IFLNK: return "LINK"; | |
case S_IFSOCK: return "SOCK"; | |
default: return ""; | |
} | |
} | |
#pragma mark - Logging | |
#define BOOL_VALUE(x) x ? "Yes" : "No" | |
int g_log_indent = 0; | |
#define LOG_INDENT_INC() {g_log_indent += 2;} | |
#define LOG_INDENT_DEC() {g_log_indent -= 2;} | |
#define LOG_IMPORTANT_INFO(fmt, ...) NSLog(@"*** " @#fmt @" ***", ##__VA_ARGS__) | |
#define LOG_INFO(fmt, ...) NSLog(@"%*s" @#fmt, g_log_indent, "", ##__VA_ARGS__) | |
#define LOG_ERROR(fmt, ...) NSLog(@"ERROR: " @#fmt, ##__VA_ARGS__) | |
#define LOG_VERBOSE_EVENT_MESSAGE(msg) { \ | |
if(g_verbose_logging) { \ | |
log_event_message(msg); \ | |
} \ | |
} | |
#define LOG_NON_VERBOSE_EVENT_MESSAGE(msg) { \ | |
if(!g_verbose_logging) { \ | |
log_event_message(msg); \ | |
} \ | |
} | |
void log_audit_token(const NSString* header, const audit_token_t audit_token) { | |
LOG_INFO("%@:", header); | |
LOG_INDENT_INC(); | |
LOG_INFO("pid: %d", audit_token_to_pid(audit_token)); | |
LOG_INFO("ruid: %d", audit_token_to_ruid(audit_token)); | |
LOG_INFO("euid: %d", audit_token_to_euid(audit_token)); | |
LOG_INFO("rgid: %d", audit_token_to_rgid(audit_token)); | |
LOG_INFO("egid: %d", audit_token_to_egid(audit_token)); | |
LOG_INDENT_DEC(); | |
} | |
API_AVAILABLE(macos(12.0)) | |
bool log_muted_paths_events(void) { | |
es_muted_paths_t *muted_paths = NULL; | |
es_return_t result = es_muted_paths_events(g_client, &muted_paths); | |
if(ES_RETURN_SUCCESS != result) { | |
LOG_ERROR("es_muted_paths_events: ES_RETURN_ERROR"); | |
return false; | |
} | |
if(NULL == muted_paths) { | |
// There are no muted paths | |
return true; | |
} | |
LOG_IMPORTANT_INFO("Muted Paths"); | |
for(size_t i = 0; i < muted_paths->count; i++) { | |
es_muted_path_t muted_path = muted_paths->paths[i]; | |
LOG_INFO("muted_path[%ld]: %@", i, esstring_to_nsstring(muted_path.path)); | |
if(g_verbose_logging) { | |
LOG_INDENT_INC(); | |
LOG_INFO("type: %s", (muted_path.type == ES_MUTE_PATH_TYPE_PREFIX) ? "Prefix" : "Literal"); | |
LOG_INFO("event_count: %ld", muted_path.event_count); | |
LOG_INFO("events: %@", events_str(muted_path.event_count, muted_path.events)); | |
LOG_INDENT_DEC(); | |
} | |
} | |
es_release_muted_paths(muted_paths); | |
return true; | |
} | |
bool log_subscribed_events(void) { | |
// Log the subscribed events | |
size_t count = 0; | |
es_event_type_t *events = NULL; | |
es_return_t result = es_subscriptions(g_client, &count, &events); | |
if(ES_RETURN_SUCCESS != result) { | |
LOG_ERROR("es_subscriptions: ES_RETURN_ERROR"); | |
return false; | |
} | |
LOG_IMPORTANT_INFO("Subscribed Events: %@", events_str(count, events)); | |
free(events); | |
return true; | |
} | |
void log_file(const NSString* header, const es_file_t* file) { | |
if(!file) { | |
LOG_INFO("%@: (null)", header); | |
return; | |
} | |
LOG_INFO("%@:", header); | |
LOG_INDENT_INC(); | |
LOG_INFO("path: %@", esstring_to_nsstring(file->path)); | |
LOG_INFO("path_truncated: %s", BOOL_VALUE(file->path_truncated)); | |
LOG_INFO("stat.st_dev: %d", file->stat.st_dev); | |
LOG_INFO("stat.st_ino: %llu", file->stat.st_ino); | |
LOG_INFO("stat.st_mode: %u (%s)", file->stat.st_mode, filetype_str(file->stat.st_mode)); | |
LOG_INFO("stat.st_nlink: %u", file->stat.st_nlink); | |
LOG_INFO("stat.st_uid: %u", file->stat.st_uid); | |
LOG_INFO("stat.st_gid: %u", file->stat.st_gid); | |
LOG_INFO("stat.st_atime: %@", formatted_date_str(file->stat.st_atime)); | |
LOG_INFO("stat.st_mtime: %@", formatted_date_str(file->stat.st_mtime)); | |
LOG_INFO("stat.st_ctime: %@", formatted_date_str(file->stat.st_ctime)); | |
LOG_INFO("stat.st_birthtime: %@", formatted_date_str(file->stat.st_birthtime)); | |
LOG_INFO("stat.st_size: %lld", file->stat.st_size); | |
LOG_INFO("stat.st_blocks: %lld", file->stat.st_blocks); | |
LOG_INFO("stat.st_blksize: %d", file->stat.st_blksize); | |
LOG_INFO("stat.st_flags: %u", file->stat.st_flags); | |
LOG_INFO("stat.st_gen: %u", file->stat.st_gen); | |
LOG_INDENT_DEC(); | |
} | |
void log_proc(uint32_t msg_version, const NSString* header, const es_process_t* proc) { | |
if(!proc) { | |
LOG_INFO("%@: (null)", header); | |
return; | |
} | |
LOG_INFO("%@:", header); | |
LOG_INDENT_INC(); | |
log_audit_token(@"proc.audit_token", proc->audit_token); | |
LOG_INFO("proc.ppid: %d", proc->ppid); | |
LOG_INFO("proc.original_ppid: %d", proc->original_ppid); | |
if(msg_version >= 4) { | |
log_audit_token(@"proc.responsible_audit_token", proc->responsible_audit_token); | |
log_audit_token(@"proc.parent_audit_token", proc->parent_audit_token); | |
} | |
LOG_INFO("proc.group_id: %d", proc->group_id); | |
LOG_INFO("proc.session_id: %d", proc->session_id); | |
LOG_INFO("proc.is_platform_binary: %s", BOOL_VALUE(proc->is_platform_binary)); | |
LOG_INFO("proc.is_es_client: %s", BOOL_VALUE(proc->is_es_client)); | |
LOG_INFO("proc.signing_id: %@", esstring_to_nsstring(proc->signing_id)); | |
LOG_INFO("proc.team_id: %@", esstring_to_nsstring(proc->team_id)); | |
if(msg_version >= 3) { | |
LOG_INFO("proc.start_time: %@", formatted_date_str(proc->start_time.tv_sec)); | |
} | |
LOG_INFO("proc.codesigning_flags: %x (%@)", | |
proc->codesigning_flags, codesigning_flags_str(proc->codesigning_flags)); | |
// proc.cdhash | |
NSMutableString *hash = [NSMutableString string]; | |
for(uint32_t i = 0; i < CS_CDHASH_LEN; i++) { | |
[hash appendFormat:@"%02x", proc->cdhash[i]]; | |
} | |
LOG_INFO("proc.cdhash: %@", hash); | |
log_file(@"proc.executable", proc->executable); | |
if(msg_version >= 2 && proc->tty) { | |
log_file(@"proc.tty", proc->tty); | |
} | |
LOG_INDENT_DEC(); | |
} | |
void log_command_line_arguments(const es_event_exec_t* exec) { | |
uint32_t arg_count = es_exec_arg_count(exec); | |
LOG_INFO("event.exec.arg_count: %u", arg_count); | |
LOG_INDENT_INC(); | |
// Extract each argument and log it out | |
for(uint32_t i = 0; i < arg_count; i++) { | |
es_string_token_t arg = es_exec_arg(exec, i); | |
LOG_INFO("arg[%d]: %@", i, esstring_to_nsstring(arg)); | |
} | |
LOG_INDENT_DEC(); | |
} | |
void log_environment_variable(const es_event_exec_t* exec) { | |
uint32_t env_count = es_exec_env_count(exec); | |
LOG_INFO("event.exec.env_count: %u", env_count); | |
LOG_INDENT_INC(); | |
// Extract each env and log it out | |
for(uint32_t i = 0; i < env_count; i++) { | |
es_string_token_t arg = es_exec_env(exec, i); | |
LOG_INFO("env[%d]: %@", i, esstring_to_nsstring(arg)); | |
} | |
LOG_INDENT_DEC(); | |
} | |
void log_file_descriptors(const es_event_exec_t* exec) { | |
if(@available(macOS 11.0, *)) { | |
uint32_t fd_count = es_exec_fd_count(exec); | |
LOG_INFO("event.exec.fd_count: %u", fd_count); | |
LOG_INDENT_INC(); | |
// Extract each fd and log it out | |
for(uint32_t i = 0; i < fd_count; i++) { | |
// Pointer must not outlive event | |
const es_fd_t *arg = es_exec_fd(exec, i); | |
LOG_INFO("fd[%d].fd: %d", i, arg->fd); | |
LOG_INFO("fd[%d].fdtype: %@", i, fdtype_str(arg->fdtype)); | |
if(PROX_FDTYPE_PIPE == arg->fdtype) { | |
LOG_INFO("fd[%d].fd: %llu", i, arg->pipe.pipe_id); | |
} | |
} | |
LOG_INDENT_DEC(); | |
} | |
} | |
void log_event_exec(uint32_t msg_version, const es_event_exec_t* exec) { | |
log_proc(msg_version, @"event.exec.target", exec->target); | |
log_command_line_arguments(exec); | |
log_environment_variable(exec); | |
log_file_descriptors(exec); | |
if(msg_version >= 2 && exec->script) { | |
log_file(@"event.exec.script", exec->script); | |
} | |
if(msg_version >= 3) { | |
log_file(@"event.exec.cwd", exec->cwd); | |
} | |
if(msg_version >= 4) { | |
LOG_INFO("event.exec.last_fd: %d", exec->last_fd); | |
} | |
} | |
void log_event_open(const es_event_open_t* open) { | |
NSMutableArray *match_flags = [NSMutableArray new]; | |
if((open->fflag & FREAD) == FREAD) { | |
[match_flags addObject:@"FREAD"]; | |
} | |
if((open->fflag & FWRITE) == FWRITE) { | |
[match_flags addObject:@"FWRITE"]; | |
} | |
LOG_INFO("event.open.fflag: %d (%@)", | |
open->fflag, [match_flags componentsJoinedByString:@", "]); | |
log_file(@"event.open.file", open->file); | |
} | |
// Logs the top level datatype sent by Endpoint Security subsystem to its clients | |
void log_event_message(const es_message_t *msg) { | |
LOG_INFO("--- EVENT MESSAGE ----"); | |
LOG_INFO("event_type: %@ (%d)", event_type_str(msg->event_type), msg->event_type); | |
// Note: Apple have designed the Endpoint Security structures to support additional fields | |
// in the future. Always check the version of the message before using a field, in the message | |
// or sub-structure, which has been added to a later version of Endpoint Security. | |
// Only new fields are added. Existing fields should be available in future revisions. | |
uint32_t version = msg->version; | |
LOG_INFO("version: %u", version); | |
LOG_INFO("time: %@", formatted_date_str(msg->time.tv_sec)); | |
LOG_INFO("mach_time: %lld", msg->mach_time); | |
// Note: It's very important that an auth event is processed within the deadline: | |
// https://developer.apple.com/documentation/endpointsecurity/es_message_t/3334985-deadline | |
// From an Apple Security Engineer: | |
// "You must respond by the deadline. | |
// It is not configurable. | |
// It won't get longer, but it will get shorter." | |
// https://developer.apple.com/forums/thread/649552?answerId=615802022#615802022 | |
LOG_INFO("deadline: %llu", msg->deadline); | |
uint64_t deadlineInterval = msg->deadline; | |
if(deadlineInterval > 0) { | |
deadlineInterval -= msg->mach_time; | |
} | |
LOG_INFO("deadline interval: %llu (%llu seconds)", | |
deadlineInterval, MachTimeToSeconds(deadlineInterval)); | |
// Note: You can use the seq_num field to detect if the kernel had to drop any event messages, | |
// for an event type, to the client. | |
if(version >= 2) { | |
LOG_INFO("seq_num: %lld", msg->seq_num); | |
} | |
// Note: You can use the global_seq_num field to detect if the kernel had to drop any event | |
// messages to the client. | |
if(version >= 4) { | |
LOG_INFO("global_seq_num: %lld", msg->global_seq_num); | |
} | |
if(version >= 4 && msg->thread) { | |
LOG_INFO("thread_id: %lld", msg->thread->thread_id); | |
} | |
LOG_INFO("action_type: %s", (msg->action_type == ES_ACTION_TYPE_AUTH) ? "Auth" : "Notify"); | |
log_proc(version, @"process", msg->process); | |
// Event specific logging | |
switch(msg->event_type) { | |
case ES_EVENT_TYPE_AUTH_EXEC: { | |
log_event_exec(version, &msg->event.exec); | |
} | |
break; | |
case ES_EVENT_TYPE_AUTH_OPEN: { | |
log_event_open(&msg->event.open); | |
} | |
break; | |
case ES_EVENT_TYPE_NOTIFY_FORK: { | |
log_proc(version, @"event.fork.child", msg->event.fork.child); | |
} | |
break; | |
case ES_EVENT_TYPE_LAST: | |
default: { | |
// Not interested | |
} | |
} | |
LOG_INFO(""); | |
} | |
// Demonstrates detecting dropped event messages from the kernel, by either | |
// using the using the seq_num or global_seq_num fields in an event message | |
void detect_and_log_dropped_events(const es_message_t *msg) { | |
uint32_t version = msg->version; | |
// Note: You can use the seq_num field to detect if the kernel had to | |
// drop any event messages, for an event type, to the client. | |
if(version >= 2) { | |
uint64_t seq_num = msg->seq_num; | |
const NSString *type = event_type_str(msg->event_type); | |
NSNumber *last_seq_num = [g_seq_nums objectForKey:type]; | |
if(last_seq_num != nil) { | |
uint64_t expected_seq_num = [last_seq_num unsignedLongLongValue] + 1; | |
if(seq_num > expected_seq_num) { | |
LOG_ERROR("EVENTS DROPPED! seq_num is ahead by: %llu", | |
(seq_num - expected_seq_num)); | |
} | |
} | |
[g_seq_nums setObject:[NSNumber numberWithUnsignedLong:seq_num] forKey:type]; | |
} | |
// Note: You can use the global_seq_num field to detect if the kernel had to | |
// drop any event messages to the client. | |
if(version >= 4) { | |
uint64_t global_seq_num = msg->global_seq_num; | |
if(global_seq_num > ++g_global_seq_num) { | |
LOG_ERROR("EVENTS DROPPED! global_seq_num is ahead by: %llu", | |
(global_seq_num - g_global_seq_num)); | |
g_global_seq_num = global_seq_num; | |
} | |
} | |
} | |
#pragma mark - Endpoint Secuirty Demo | |
// Clean-up before exiting | |
void sig_handler(int sig) { | |
LOG_IMPORTANT_INFO("Tidying Up"); | |
if(g_client) { | |
es_unsubscribe_all(g_client); | |
es_delete_client(g_client); | |
} | |
LOG_IMPORTANT_INFO("Exiting"); | |
exit(EXIT_SUCCESS); | |
} | |
void print_usage(const char *name) { | |
printf("Usage: %s (serial | asynchronous) (verbose)\n", name); | |
printf("Arguments:\n"); | |
printf("\tserial\t\tUse serial message handler\n"); | |
printf("\tasynchronous\tUse asynchronous message handler\n"); | |
printf("\tverbose\t\tTurns on verbose logging\n"); | |
} | |
// An example handler to make auth (allow or block) decisions. | |
// Returns either an ES_AUTH_RESULT_ALLOW or ES_AUTH_RESULT_DENY. | |
es_auth_result_t auth_event_handler(const es_message_t *msg) { | |
// NOTE: You should ignore events from other ES Clients; | |
// otherwise you may trigger more events causing a potentially infinite cycle. | |
if(msg->process->is_es_client) { | |
return ES_AUTH_RESULT_ALLOW; | |
} | |
// Ignore events from root processes | |
if(0 == audit_token_to_ruid(msg->process->audit_token)) { | |
return ES_AUTH_RESULT_ALLOW; | |
} | |
// Block exec if path of process is in our blocked paths list | |
if(ES_EVENT_TYPE_AUTH_EXEC == msg->event_type) { | |
NSString *path = esstring_to_nsstring(msg->event.exec.target->executable->path); | |
if(![g_blocked_paths containsObject:path]) { | |
return ES_AUTH_RESULT_ALLOW; | |
} | |
// Process is in our blocked list | |
LOG_IMPORTANT_INFO("BLOCKING EXEC: %@", path); | |
return ES_AUTH_RESULT_DENY; | |
} | |
// Block vim from accessing plain text files | |
if(ES_EVENT_TYPE_AUTH_OPEN == msg->event_type) { | |
NSString *processPath = esstring_to_nsstring(msg->process->executable->path); | |
if(![processPath isEqualToString:@"/usr/bin/vim"]) { | |
// Not vim | |
return ES_AUTH_RESULT_ALLOW; | |
} | |
NSString *filePath = esstring_to_nsstring(msg->event.open.file->path); | |
if(is_system_file(filePath)) { | |
// Ignore System files | |
return ES_AUTH_RESULT_ALLOW; | |
} | |
if(!is_plain_text_file(filePath)) { | |
// Not a text file | |
return ES_AUTH_RESULT_ALLOW; | |
} | |
// Process is vim trying to access a text file | |
LOG_IMPORTANT_INFO("BLOCKING OPEN: %@", filePath); | |
return ES_AUTH_RESULT_DENY; | |
} | |
// All good | |
return ES_AUTH_RESULT_ALLOW; | |
} | |
// Sends a response back to Endpoint Security for an auth event | |
// Note: You must always send a response back before the deadline expires. | |
void respond_to_auth_event(es_client_t *clt, const es_message_t *msg, es_auth_result_t result) { | |
// Only log ES_AUTH_RESULT_DENY results when verbose logging is disabled | |
if(ES_AUTH_RESULT_DENY == result) { | |
LOG_NON_VERBOSE_EVENT_MESSAGE(msg); | |
} | |
// Note: You use es_respond_auth_result() to respond to auth events, | |
// except for ES_EVENT_TYPE_AUTH_OPEN events, which require a response | |
// using es_respond_flags_result() instead. | |
if(ES_EVENT_TYPE_AUTH_OPEN == msg->event_type) { | |
uint32_t authorized_flags = 0; | |
if(ES_AUTH_RESULT_ALLOW == result) { | |
authorized_flags = msg->event.open.fflag; | |
} | |
es_respond_result_t res = | |
es_respond_flags_result(clt, msg, authorized_flags, g_cache_auth_results); | |
if(ES_RESPOND_RESULT_SUCCESS != res) { | |
LOG_ERROR("es_respond_flags_result: %d", res); | |
} | |
} else { | |
es_respond_result_t res = | |
es_respond_auth_result(clt, msg, result, g_cache_auth_results); | |
if(ES_RESPOND_RESULT_SUCCESS != res) { | |
LOG_ERROR("es_respond_auth_result: %d", res); | |
} | |
} | |
} | |
// Example of an event message handler to process event messages serially from Endpoint Security. | |
es_handler_block_t serial_message_handler = ^(es_client_t *clt, const es_message_t *msg) { | |
// Endpoint Security, by default, calls a event message handler serially for each message. | |
LOG_VERBOSE_EVENT_MESSAGE(msg); | |
// NOTE: It is important to process events in a timely manner. | |
// The kernel will start to drop events for the client if they are not responded to in time. | |
detect_and_log_dropped_events(msg); | |
// Auth events require a response sent back before the deadline expires | |
if(ES_ACTION_TYPE_AUTH == msg->action_type) { | |
respond_to_auth_event(clt, msg, auth_event_handler(msg)); | |
} | |
}; | |
// Example of an event message handler to process event messages asynchronously from Endpoint Security | |
es_handler_block_t asynchronous_message_handler = ^(es_client_t *clt, const es_message_t *msg) { | |
// Endpoint Security, by default, calls a event message handler serially for each message. | |
// We copy/retain the message so that we can process and respond to auth events asynchronously. | |
LOG_VERBOSE_EVENT_MESSAGE(msg); | |
// NOTE: It is important to process events in a timely manner. | |
// The kernel will start to drop events for the client if they are not responded to in time. | |
detect_and_log_dropped_events(msg); | |
// Copy/Retain the event message so that we process the event asynchronously | |
es_message_t *copied_msg = copy_message(msg); | |
if(!copied_msg) { | |
LOG_ERROR("Failed to copy message"); | |
return; | |
} | |
// Demonstrates handling events out of order, by processing 'ES_ACTION_TYPE_AUTH' events on | |
// a separate thread. Sleep for 20s for 'ES_EVENT_TYPE_AUTH_EXEC' events if the result | |
// is an ES_AUTH_RESULT_DENY. | |
if(ES_ACTION_TYPE_AUTH == copied_msg->action_type) { | |
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(void){ | |
es_auth_result_t result = auth_event_handler(copied_msg); | |
if(ES_AUTH_RESULT_DENY == result && | |
ES_EVENT_TYPE_AUTH_EXEC == copied_msg->event_type) { | |
[NSThread sleepForTimeInterval:20.0]; | |
} | |
// Auth events require a response sent back before the deadline expires | |
respond_to_auth_event(clt, copied_msg, result); | |
free_message(copied_msg); | |
}); | |
return; | |
} | |
// Free/release the message | |
free_message(copied_msg); | |
}; | |
es_handler_block_t get_message_handler_from_commandline_args(int argc, const char * argv[]) { | |
if(argc < 2) { | |
// No command line argument was given | |
return nil; | |
} | |
// check if verbose logging argument was given | |
if(argc > 2) { | |
NSString *verbose = [[NSString stringWithUTF8String:argv[2]] lowercaseString]; | |
g_verbose_logging = [verbose isEqualToString:@"verbose"]; | |
} | |
// Try and find an event message handler that matches the first command line argument | |
NSString *arg = [[NSString stringWithUTF8String:argv[1]] lowercaseString]; | |
NSDictionary *handlers = @{ | |
@"serial" : serial_message_handler, | |
@"asynchronous" : asynchronous_message_handler | |
}; | |
return [handlers objectForKey:arg]; | |
} | |
// On macOS Monterey 12, Apple have deprecated es_mute_path_literal in favour of es_mute_path | |
bool mute_path(const char* path) | |
{ | |
es_return_t result = ES_RETURN_ERROR; | |
if(@available(macOS 12.0, *)) { | |
result = es_mute_path(g_client, path, ES_MUTE_PATH_TYPE_LITERAL); | |
} else { | |
result = es_mute_path_literal(g_client, path); | |
} | |
if(ES_RETURN_SUCCESS != result) { | |
LOG_ERROR("mute_path: ES_RETURN_ERROR"); | |
return false; | |
} | |
return true; | |
} | |
// Note: This function shows the boilerplate code required to setup a connection to Endpoint Security | |
// and subscribe to events. | |
bool setup_endpoint_security(void) { | |
// Create a new client with an associated event message handler. | |
// Requires 'com.apple.developer.endpoint-security.client' entitlement. | |
es_new_client_result_t res = es_new_client(&g_client, g_handler); | |
if(ES_NEW_CLIENT_RESULT_SUCCESS != res) { | |
switch(res) { | |
case ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED: | |
LOG_ERROR("Application requires 'com.apple.developer.endpoint-security.client' entitlement"); | |
break; | |
case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED: | |
LOG_ERROR("Application lacks Transparency, Consent, and Control (TCC) approval " | |
"from the user. This can be resolved by granting 'Full Disk Access' from " | |
"the 'Security & Privacy' tab of System Preferences."); | |
break; | |
case ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED: | |
LOG_ERROR("Application needs to be run as root"); | |
break; | |
default: | |
LOG_ERROR("es_new_client: %d", res); | |
} | |
return false; | |
} | |
// Explicitly clear the cache of previous cached results from this demo or other ES Clients | |
es_clear_cache_result_t resCache = es_clear_cache(g_client); | |
if(ES_CLEAR_CACHE_RESULT_SUCCESS != resCache) { | |
LOG_ERROR("es_clear_cache: %d", resCache); | |
return false; | |
} | |
// Subscribe to the events we're interested in | |
es_event_type_t events[] = { | |
ES_EVENT_TYPE_AUTH_EXEC | |
, ES_EVENT_TYPE_AUTH_OPEN | |
, ES_EVENT_TYPE_NOTIFY_FORK | |
}; | |
es_return_t subscribed = es_subscribe(g_client, events, sizeof events / sizeof *events); | |
if(ES_RETURN_ERROR == subscribed) { | |
LOG_ERROR("es_subscribe: ES_RETURN_ERROR"); | |
return false; | |
} | |
// All good | |
return log_subscribed_events(); | |
} | |
int main(int argc, const char * argv[]) { | |
signal(SIGINT, &sig_handler); | |
@autoreleasepool { | |
// Init global vars | |
g_handler = get_message_handler_from_commandline_args(argc, argv); | |
if(!g_handler) { | |
print_usage(argv[0]); | |
return 1; | |
} | |
init_date_formater(); | |
g_seq_nums = [NSMutableDictionary new]; | |
// List of paths to be blocked. | |
// For this demo we will block the top binary and Calculator app bundle. | |
g_blocked_paths = [NSSet setWithObjects: | |
@"/usr/bin/top", | |
@"/System/Applications/Calculator.app/Contents/MacOS/Calculator", | |
nil]; | |
if(!setup_endpoint_security()) { | |
return 1; | |
} | |
// Note: Endpoint Security have a set of es_mute* functions to suppress events for a process. | |
// Uncomment the 'mute_path' line below to stop receiving events from the 'vim' binary. | |
// This program will then stop receiving 'ES_EVENT_TYPE_AUTH_OPEN' events for vim and will no | |
// longer be able to block vim from opening plain text files. | |
// mute_path("/usr/bin/vim"); | |
if(@available(macOS 12.0, *)) { | |
// Note: Endpoint Security for performance reasons will automatically mute a set of paths | |
// on creation of new clients ('es_new_client'). | |
// macOS Monterey 12 now has the 'es_muted_paths_events' function, which can be used to | |
// inspect the muted paths. It is possible to unmute these paths (e.g. by using | |
// 'es_release_muted_paths'), but Apple advises against this. | |
log_muted_paths_events(); | |
} else { | |
// ES on macOS Monterey 12 implicitly mutes events from cfprefsd. We need to explicitly do | |
// this on older versions of macOS to prevent deadlocks in this program. This is because | |
// UTType and NSDate objects, used in parts of this program, may implicitly | |
// make NSUserDefaults calls which will generate ES events for cfprefsd. | |
mute_path("/usr/sbin/cfprefsd"); | |
} | |
// Start handling events from Endpoint Security | |
dispatch_main(); | |
} | |
return 0; | |
} |
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>com.apple.developer.endpoint-security.client</key> | |
<true/> | |
</dict> | |
</plist> |
@Omar-Ikram : I have implemented ES client, it tracks everything from "/" directory. How can I make it observe only one particular user directory?
@anoopvaidya Apple have introduced a new feature with Endpoint Security in macOS Ventura called Mute Inversion, which should do what you have asked for.
Apple explain Mute Inversion in their WWDC2022 video:
https://developer.apple.com/videos/play/wwdc2022/110345/
Also the Endpoint Security API header files have a detailed description about the feature for the es_invert_muting
function.
* @brief Invert the mute state of a given mute dimension
*
* @param client The es_client_t for which muting will be inverted
* @param mute_type The type of muting to invert (process, path, or target path).
*
* @return es_return_t A value indicating whether or not muting was inverted
*
* @discussion Inverting muting can be used to create a client that monitors a specific process(es) or set of directories
* When muting is inverted it still combines with other types of muting using OR, and inversion happens first.
* Consider a series of inputs for a system where pid 12 is muted, process muting is inverted, and /bin/bash is also path muted:
* (12, /bin/foo) MATCHING (true, false) INVERSION (false, false) || false → event is not suppressed
* (13, /bin/foo) MATCHING (false, false) INVERSION (true, false) || true → event is suppressed
* (12, /bin/bash) MATCHING (true, true) INVERSION (false, true) || true → event is suppressed
*
* Note that because muting is combined using OR even when pid 12 is being selected using inverted process muting,
* (12, /bin/bash) is still suppressed because the path is muted
*
* The relationship between all three types of muting (proc,path,target-path) and how each can be inverted is complex.
* The below flow chart explains in detail exactly how muting is applied in the kernel:
*
* ┌──────────────────┐
* │ Event │
* └──────────────────┘
* │
* ▼
* ┌──────────────────┐ ┌──────────────────┐
* │ Is Subscribed? │────No────────────────────────────────────▶│ Suppress Event │
* └──────────────────┘ └──────────────────┘
* │ ▲
* Yes│ │
* ▼ ┌────────────────┐ │
* ┌──────────────────┐ │ Is Proc Muting │ │
* │ Is Proc Muted? ├─Yes──▶│ Inverted? ├──No───────────────────────▶│
* └─────────┬────────┘ └────────────────┘ │
* │ │ │
* No│ Yes │
* ▼ │ │
* ┌──────────────────┐ │ │
* │ Is Proc Muting │ │ │
* │ Inverted? │──Yes───────────)───────────────────────────────────▶│
* └─────────┬────────┘ │ │
* │ │ │
* No│◀────────────────────────┘ │
* ▼ ┌───────────────┐ │
* ┌──────────────────┐ │Is Path Muting │ │
* │ Is Path Muted? │──Yes──▶│ Inverted? ├──No───────────────────────▶│
* └─────────┬────────┘ └───────┬───────┘ │
* │ │ │
* No│ Yes │
* ▼ │ │
* ┌──────────────────┐ │ │
* │ Is Path Muting │ │ │
* │ Inverted? │──Yes───────────)───────────────────────────────────▶│
* └─────────┬────────┘ │ │
* │ │ │
* No│◀────────────────────────┘ │
* ▼ │
* ┌──────────────────┐ │
* │ Event Supports │ ┌───────────────┐ ┌─────────────────┐ │
* │ Target Path │─Yes─▶│Is Target Path ├─Yes─▶│ Are ANY target ├─No─▶│
* │ Muting? │ │Muting Inverted│ │ paths muted? │ │
* └──────────────────┘ └──────┬────────┘ └───────┬─────────┘ │
* │ │ │ │
* No│ No│ Yes │
* │ ▼ │ │
* │ ┌────────────────┐ │ │
* │ │ Are ALL target │ │ │
* │ │ paths muted? ├─Yes──────────)───────────────┘
* │ └───────┬────────┘ │
* │ │ │
* │ No│ │
* │◀─────────────────────┘ │
* │ │
* │◀─────────────────────────────────────────────┘
* │
* ▼
* ┌──────────────────┐
* │ Deliver Event │
* └──────────────────┘
*
* @note Mute inversion does NOT clear the default mute set.
* When a new `es_client_t` is created certain paths are muted by default.
* This is known as "the default mute set".
* The default mute set exists to protect ES clients from deadlocks, and to prevent watchdog timeout panics.
* Creating a new client and calling `es_invert_muting(c, ES_MUTE_INVERSION_TYPE_PATH)` will result in the default mute set being selected rather than muted.
* In most cases this is unintended.
* Consider calling `es_unmute_all_paths` before inverting process path muting.
* Consider calling `es_unmute_all_target_paths` before inverting target path muting.
* Make sure the client has no auth subscriptions before doing so.
* If desired the default mute set can be saved using `es_muted_paths_events` and then restored after inverting again.
*
*/
@Omar-Ikram Yes, I have already implemented it with muting https://github.com/anoopvaidya/FileAuditSystem/commits/main
Later I realized that only Events from any executable(.app or similar) was being notified. So, my initial query itself is wrong.
What I need is to observe a folder for any CRUD operations and act upon that. Is this possible using SEXT or I need to use FSEvents?
@Ajit72 I've uploaded a complete Xcode project for this gist. If you're still having trouble getting the gist to work, I would suggest that you checkout the project: https://github.com/Omar-Ikram/EndpointSecurityDemo.