diff --git a/Source/santad/EventProviders/SNTEndpointSecurityRecorder.mm b/Source/santad/EventProviders/SNTEndpointSecurityRecorder.mm index 31a3a956b..344643a34 100644 --- a/Source/santad/EventProviders/SNTEndpointSecurityRecorder.mm +++ b/Source/santad/EventProviders/SNTEndpointSecurityRecorder.mm @@ -13,6 +13,7 @@ /// limitations under the License. #import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h" +#include #include @@ -37,6 +38,7 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) { switch (msg->event_type) { case ES_EVENT_TYPE_NOTIFY_CLOSE: return msg->event.close.target; + case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA: return msg->event.exchangedata.file1; case ES_EVENT_TYPE_NOTIFY_LINK: return msg->event.link.source; case ES_EVENT_TYPE_NOTIFY_RENAME: return msg->event.rename.source; case ES_EVENT_TYPE_NOTIFY_UNLINK: return msg->event.unlink.target; @@ -91,10 +93,10 @@ - (void)handleMessage:(Message &&)esMsg BOOL shouldLogClose = esMsg->event.close.modified; #if HAVE_MACOS_13 - if (@available(macOS 13.5, *)) { + if (esMsg->version >= 6) { // As of macSO 13.0 we have a new field for if a file was mmaped with - // write permissions on close events. However it did not work until - // 13.5. + // write permissions on close events. However due to a bug in ES, it + // only worked for certain conditions until macOS 13.5 (FB12094635). // // If something was mmaped writable it was probably written to. Often // developer tools do this to avoid lots of write syscalls, e.g. go's @@ -114,8 +116,28 @@ - (void)handleMessage:(Message &&)esMsg self->_authResultCache->RemoveFromCache(esMsg->event.close.target); + break; + } + + default: break; + } + + [self.compilerController handleEvent:esMsg withLogger:self->_logger]; + + switch (esMsg->event_type) { + case ES_EVENT_TYPE_NOTIFY_CLOSE: OS_FALLTHROUGH; + case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA: OS_FALLTHROUGH; + case ES_EVENT_TYPE_NOTIFY_LINK: OS_FALLTHROUGH; + case ES_EVENT_TYPE_NOTIFY_RENAME: OS_FALLTHROUGH; + case ES_EVENT_TYPE_NOTIFY_UNLINK: { + es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg)); + + if (!targetFile) { + break; + } + // Only log file changes that match the given regex - NSString *targetPath = santa::common::StringToNSString(esMsg->event.close.target->path.data); + NSString *targetPath = santa::common::StringToNSString(targetFile->path.data); if (![[self.configurator fileChangesRegex] numberOfMatchesInString:targetPath options:0 @@ -127,25 +149,25 @@ - (void)handleMessage:(Message &&)esMsg return; } + if (self->_prefixTree->HasPrefix(targetFile->path.data)) { + NSLog(@"doing drop from prefix tree..."); + recordEventMetrics(EventDisposition::kDropped); + return; + } + break; } - default: break; - } - - [self.compilerController handleEvent:esMsg withLogger:self->_logger]; - if ((esMsg->event_type == ES_EVENT_TYPE_NOTIFY_FORK || - esMsg->event_type == ES_EVENT_TYPE_NOTIFY_EXIT) && - self.configurator.enableForkAndExitLogging == NO) { - recordEventMetrics(EventDisposition::kDropped); - return; - } + case ES_EVENT_TYPE_NOTIFY_FORK: OS_FALLTHROUGH; + case ES_EVENT_TYPE_NOTIFY_EXIT: { + if (self.configurator.enableForkAndExitLogging == NO) { + recordEventMetrics(EventDisposition::kDropped); + return; + } + break; + } - // Filter file op events matching the prefix tree. - es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg)); - if (targetFile != NULL && self->_prefixTree->HasPrefix(targetFile->path.data)) { - recordEventMetrics(EventDisposition::kDropped); - return; + default: break; } // Enrich the message inline with the ES handler block to capture enrichment diff --git a/Source/santad/EventProviders/SNTEndpointSecurityRecorderTest.mm b/Source/santad/EventProviders/SNTEndpointSecurityRecorderTest.mm index 0e4854ac5..dc6d3f065 100644 --- a/Source/santad/EventProviders/SNTEndpointSecurityRecorderTest.mm +++ b/Source/santad/EventProviders/SNTEndpointSecurityRecorderTest.mm @@ -103,7 +103,7 @@ - (void)testEnable { XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); } -typedef void (^testHelperBlock)(es_message_t *message, +typedef void (^TestHelperBlock)(es_message_t *message, std::shared_ptr mockESApi, id mockCC, SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr> prefixTree, @@ -114,7 +114,7 @@ typedef void (^testHelperBlock)(es_message_t *message, - (void)handleMessageShouldLog:(BOOL)shouldLog shouldRemoveFromCache:(BOOL)shouldRemoveFromCache - withBlock:(testHelperBlock)testBlock { + withBlock:(TestHelperBlock)testBlock { es_file_t file = MakeESFile("foo"); es_process_t proc = MakeESProcess(&file); es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc, ActionType::Auth); @@ -176,7 +176,7 @@ - (void)testHandleMessageWithCloseMappedWriteable { if (@available(macOS 13.0, *)) { // CLOSE not modified, but was_mapped_writable, should remove from cache, // and matches fileChangesRegex - testHelperBlock testBlock = + TestHelperBlock testBlock = ^(es_message_t *esMsg, std::shared_ptr mockESApi, id mockCC, SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr> prefixTree, __autoreleasing dispatch_semaphore_t *sema, @@ -208,7 +208,7 @@ - (void)testHandleEventCloseNotModifiedWithWasMappedWritable { if (@available(macOS 13.0, *)) { // CLOSE not modified, but was_mapped_writable, remove from cache, and does not match // fileChangesRegex - testHelperBlock testBlock = + TestHelperBlock testBlock = ^(es_message_t *esMsg, std::shared_ptr mockESApi, id mockCC, SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr> prefixTree, __autoreleasing dispatch_semaphore_t *sema, @@ -232,7 +232,7 @@ - (void)testHandleEventCloseNotModifiedWithWasMappedWritable { - (void)testHandleMessage { // CLOSE not modified, bail early - testHelperBlock testBlock = ^( + TestHelperBlock testBlock = ^( es_message_t *esMsg, std::shared_ptr mockESApi, id mockCC, SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr> prefixTree, __autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) { @@ -281,6 +281,7 @@ - (void)testHandleMessage { esMsg->event.close.modified = true; esMsg->event.close.target = &targetFileMissesRegex; Message msg(mockESApi, esMsg); + OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs(); XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg) recordEventMetrics:^(EventDisposition d) { XCTFail("Metrics record callback should not be called here"); @@ -289,6 +290,44 @@ - (void)testHandleMessage { [self handleMessageShouldLog:NO shouldRemoveFromCache:YES withBlock:testBlock]; + // UNLINK, remove from cache, but doesn't match fileChangesRegex + testBlock = ^( + es_message_t *esMsg, std::shared_ptr mockESApi, id mockCC, + SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr> prefixTree, + __autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) { + esMsg->event_type = ES_EVENT_TYPE_NOTIFY_UNLINK; + esMsg->event.unlink.target = &targetFileMissesRegex; + Message msg(mockESApi, esMsg); + OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs(); + XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg) + recordEventMetrics:^(EventDisposition d) { + XCTFail("Metrics record callback should not be called here"); + }]); + }; + + [self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock]; + + // EXCHANGEDATA, Prefix match, bail early + testBlock = ^( + es_message_t *esMsg, std::shared_ptr mockESApi, id mockCC, + SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr> prefixTree, + __autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics) { + esMsg->event_type = ES_EVENT_TYPE_NOTIFY_UNLINK; + esMsg->event.exchangedata.file1 = &targetFileMatchesRegex; + prefixTree->InsertPrefix(esMsg->event.exchangedata.file1->path.data, Unit{}); + Message msg(mockESApi, esMsg); + OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs(); + XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, esMsg) + recordEventMetrics:^(EventDisposition d) { + XCTAssertEqual(d, EventDisposition::kDropped); + dispatch_semaphore_signal(*semaMetrics); + }]); + + XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window"); + }; + + [self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock]; + // LINK, Prefix match, bail early testBlock = ^(es_message_t *esMsg, std::shared_ptr mockESApi, id mockCC, @@ -371,6 +410,7 @@ - (void)testGetTargetFileForPrefixTree { extern es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg); es_file_t closeFile = MakeESFile("close"); + es_file_t exchangedataFile = MakeESFile("exchangedata"); es_file_t linkFile = MakeESFile("link"); es_file_t renameFile = MakeESFile("rename"); es_file_t unlinkFile = MakeESFile("unlink"); @@ -393,7 +433,8 @@ - (void)testGetTargetFileForPrefixTree { XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &unlinkFile); esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA; - XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr); + esMsg.event.exchangedata.file1 = &exchangedataFile; + XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &exchangedataFile); esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC; XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);