From 54d6653973b7b174098321fa774a6cf3e2d831c6 Mon Sep 17 00:00:00 2001 From: Kent Ma Date: Wed, 11 Aug 2021 11:42:55 -0400 Subject: [PATCH] Include SNTEndpointSecurityManagerTest in the main test_suite (#566) * Include SNTEndpointSecurityManagerTest in the main test_suite and clean it up. This commit (1) adds es_unsubscribe and es_delete_client to our ESF shim to fix the test segfaulting, and (2) cleans up the unit tests themselves by breaking out the timeout test from the regular unlink test --- BUILD | 1 + .../EventProviders/EndpointSecurityTestUtil.h | 8 + .../EndpointSecurityTestUtil.mm | 14 ++ .../SNTEndpointSecurityManagerTest.mm | 156 +++++++++++++----- 4 files changed, 138 insertions(+), 41 deletions(-) diff --git a/BUILD b/BUILD index cc12f1e8c..5c0b9e0fe 100644 --- a/BUILD +++ b/BUILD @@ -232,5 +232,6 @@ test_suite( "//Source/santad:SNTEventTableTest", "//Source/santad:SNTExecutionControllerTest", "//Source/santad:SNTRuleTableTest", + "//Source/santad:SNTEndpointSecurityManagerTest", ], ) diff --git a/Source/santad/EventProviders/EndpointSecurityTestUtil.h b/Source/santad/EventProviders/EndpointSecurityTestUtil.h index 9e44ff723..e176546c8 100644 --- a/Source/santad/EventProviders/EndpointSecurityTestUtil.h +++ b/Source/santad/EventProviders/EndpointSecurityTestUtil.h @@ -59,3 +59,11 @@ API_AVAILABLE(macos(10.15)) API_UNAVAILABLE(ios, tvos, watchos) es_return_t es_subscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events, uint32_t event_count); + +API_AVAILABLE(macos(10.15)) +API_UNAVAILABLE(ios, tvos, watchos) es_return_t es_delete_client(es_client_t *_Nullable client); + +API_AVAILABLE(macos(10.15)) +API_UNAVAILABLE(ios, tvos, watchos) es_return_t + es_unsubscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events, + uint32_t event_count); diff --git a/Source/santad/EventProviders/EndpointSecurityTestUtil.mm b/Source/santad/EventProviders/EndpointSecurityTestUtil.mm index 5be1f9f40..1c661cfaa 100644 --- a/Source/santad/EventProviders/EndpointSecurityTestUtil.mm +++ b/Source/santad/EventProviders/EndpointSecurityTestUtil.mm @@ -115,6 +115,12 @@ es_new_client_result_t es_new_client(es_client_t *_Nullable *_Nonnull client, return ES_NEW_CLIENT_RESULT_SUCCESS; }; +API_AVAILABLE(macos(10.15)) +API_UNAVAILABLE(ios, tvos, watchos) es_return_t es_delete_client(es_client_t *_Nullable client) { + [[MockEndpointSecurity mockEndpointSecurity] reset]; + return ES_RETURN_SUCCESS; +}; + API_AVAILABLE(macos(10.15)) API_UNAVAILABLE(ios, tvos, watchos) es_respond_result_t es_respond_auth_result(es_client_t *_Nonnull client, @@ -132,3 +138,11 @@ es_return_t es_subscribe(es_client_t *_Nonnull client, const es_event_type_t *_N [MockEndpointSecurity mockEndpointSecurity].subscribed = YES; return ES_RETURN_SUCCESS; } +API_AVAILABLE(macos(10.15)) +API_UNAVAILABLE(ios, tvos, watchos) +es_return_t es_unsubscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events, + uint32_t event_count) { + [MockEndpointSecurity mockEndpointSecurity].subscribed = NO; + + return ES_RETURN_SUCCESS; +}; diff --git a/Source/santad/EventProviders/SNTEndpointSecurityManagerTest.mm b/Source/santad/EventProviders/SNTEndpointSecurityManagerTest.mm index 409cc3143..d4027c3da 100644 --- a/Source/santad/EventProviders/SNTEndpointSecurityManagerTest.mm +++ b/Source/santad/EventProviders/SNTEndpointSecurityManagerTest.mm @@ -33,34 +33,84 @@ - (void)setUp { fclose(stdout); } +- (void)testDenyOnTimeout { + // There should be two events: an early uncached DENY as the consequence for not + // meeting the decision deadline and an actual cached decision from our message + // handler. + __block int wantNumResp = 2; + + MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity]; + [mockES reset]; + SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Wait for santa's Auth dispatch queue"]; + + __block NSMutableArray *events = [NSMutableArray array]; + [mockES registerResponseCallback:^(ESResponse *r) { + @synchronized(self) { + [events addObject:r]; + } + + if (events.count >= wantNumResp) { + [expectation fulfill]; + } + }]; + + es_file_t dbFile = {.path = MakeStringToken(kEventsDBPath)}; + es_file_t otherBinary = {.path = MakeStringToken(@"somebinary")}; + es_process_t proc = { + .executable = &otherBinary, + .is_es_client = false, + .codesigning_flags = 570509313, + .session_id = 12345, + .group_id = 12345, + .ppid = 12345, + .original_ppid = 12345, + .is_platform_binary = false, + }; + es_event_unlink_t unlink_event = {.target = &dbFile}; + es_events_t event = {.unlink = unlink_event}; + es_message_t m = { + .version = 4, + .event_type = ES_EVENT_TYPE_AUTH_UNLINK, + .event = event, + .mach_time = 1234, + .action_type = ES_ACTION_TYPE_AUTH, + .deadline = 1234, + .process = &proc, + .seq_num = 1337, + }; + + [mockES triggerHandler:&m]; + + [self waitForExpectationsWithTimeout:10.0 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Santa auth test timed out without receiving two " + @"events. Instead, had error: %@", + error); + } + }]; + + for (ESResponse *resp in events) { + XCTAssertEqual( + resp.result, ES_AUTH_RESULT_DENY, + @"Failed to automatically deny on timeout and also the malicious event afterwards"); + } +} + - (void)testDeleteRulesDB { for (const NSString *testPath in @[ kEventsDBPath, kRulesDBPath ]) { - // There should be two events: an early uncached DENY as the consequence for not - // meeting the decision deadline and an actual cached decision from our message - // handler. - __block int wantNumResp = 2; - MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity]; [mockES reset]; SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init]; - snt.logCallback = ^(santa_message_t m) { - }; - snt.decisionCallback = ^(santa_message_t m) { - }; - - XCTestExpectation *expectation = - [self expectationWithDescription:@"Wait for santa's Auth dispatch queue"]; - - __block NSMutableArray *events = [NSMutableArray array]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"]; + __block ESResponse *got = nil; [mockES registerResponseCallback:^(ESResponse *r) { - @synchronized(self) { - [events addObject:r]; - } - - if (events.count >= wantNumResp) { - [expectation fulfill]; - } + got = r; + [expectation fulfill]; }]; es_file_t dbFile = {.path = MakeStringToken(testPath)}; @@ -68,15 +118,24 @@ - (void)testDeleteRulesDB { es_process_t proc = { .executable = &otherBinary, .is_es_client = false, + .codesigning_flags = 570509313, + .session_id = 12345, + .group_id = 12345, + .ppid = 12345, + .original_ppid = 12345, + .is_platform_binary = false, }; es_event_unlink_t unlink_event = {.target = &dbFile}; es_events_t event = {.unlink = unlink_event}; es_message_t m = { + .version = 4, .event_type = ES_EVENT_TYPE_AUTH_UNLINK, .event = event, + .mach_time = DISPATCH_TIME_NOW, .action_type = ES_ACTION_TYPE_AUTH, - .deadline = DISPATCH_TIME_NOW + NSEC_PER_SEC * 60, + .deadline = DISPATCH_TIME_FOREVER, .process = &proc, + .seq_num = 1337, }; [mockES triggerHandler:&m]; @@ -87,12 +146,8 @@ - (void)testDeleteRulesDB { } }]; - bool wasCached = false; - for (ESResponse *resp in events) { - XCTAssertEqual(resp.result, ES_AUTH_RESULT_DENY, @"Failed to deny deletion of %@", testPath); - wasCached = wasCached | resp.shouldCache; - } - XCTAssertTrue(wasCached, @"Failed to cache deletion decision of %@", testPath); + XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY, @"Failed to deny deletion of %@", testPath); + XCTAssertTrue(got.shouldCache, @"Failed to cache deletion decision of %@", testPath); } } @@ -101,29 +156,48 @@ - (void)testSkipOtherESEvents { [mockES reset]; SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init]; - snt.logCallback = ^(santa_message_t m) { - }; - snt.decisionCallback = ^(santa_message_t m) { - }; + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"]; - __block NSMutableArray *events = [NSMutableArray array]; + __block ESResponse *got = nil; [mockES registerResponseCallback:^(ESResponse *r) { - @synchronized(self) { - [events addObject:r]; - } + got = r; + [expectation fulfill]; }]; + es_file_t dbFile = {.path = MakeStringToken(@"/some/other/path")}; es_file_t otherBinary = {.path = MakeStringToken(@"somebinary")}; - es_process_t proc = {.executable = &otherBinary, .is_es_client = true}; + es_process_t proc = { + .executable = &otherBinary, + .is_es_client = true, + .codesigning_flags = 570509313, + .session_id = 12345, + .group_id = 12345, + .ppid = 12345, + .original_ppid = 12345, + .is_platform_binary = false, + }; + es_event_unlink_t unlink_event = {.target = &dbFile}; + es_events_t event = {.unlink = unlink_event}; es_message_t m = { + .version = 4, + .event_type = ES_EVENT_TYPE_AUTH_UNLINK, + .event = event, + .mach_time = DISPATCH_TIME_NOW, .action_type = ES_ACTION_TYPE_AUTH, + .deadline = DISPATCH_TIME_FOREVER, .process = &proc, + .seq_num = 1337, }; - [mockES triggerHandler:&m]; - XCTAssertEqual(events.count, 1); - XCTAssertEqual(events[0].result, ES_AUTH_RESULT_ALLOW); - XCTAssertEqual(events[0].shouldCache, false); + [mockES triggerHandler:&m]; + [self waitForExpectationsWithTimeout:10.0 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Santa auth test timed out with error: %@", error); + } + }]; + + XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW); } @end