diff --git a/.bazelrc b/.bazelrc index e0fa8c7f4..4e7d6983c 100644 --- a/.bazelrc +++ b/.bazelrc @@ -3,3 +3,14 @@ build --apple_generate_dsym --define=apple.propagate_embedded_extra_outputs=yes build --copt=-Werror build --copt=-Wall build --copt=-Wno-error=deprecated-declarations +build --per_file_copt=.*\.mm\$@-std=c++17 +build --cxxopt=-std=c++17 + +build:asan --strip=never +build:asan --copt="-Wno-macro-redefined" +build:asan --copt="-D_FORTIFY_SOURCE=0" +build:asan --copt="-O1" +build:asan --copt="-fno-omit-frame-pointer" +build:asan --copt="-fsanitize=address" +build:asan --copt="-DADDRESS_SANITIZER" +build:asan --linkopt="-fsanitize=address" diff --git a/.github/workflows/check-markdown.yml b/.github/workflows/check-markdown.yml index 833f36d44..a6c4dcc35 100644 --- a/.github/workflows/check-markdown.yml +++ b/.github/workflows/check-markdown.yml @@ -11,4 +11,4 @@ jobs: steps: - uses: actions/checkout@master - uses: gaurav-nelson/github-action-markdown-link-check@v1 - - run: "! git grep -EIn $'[ \t]+$'" + - run: "! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a97744df6..c64d530a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,6 @@ jobs: - name: Run linters run: ./Testing/lint.sh - build_userspace: strategy: fail-fast: false @@ -55,10 +54,3 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: ./bazel-out/_coverage/_coverage_report.dat flag-name: Unit - - benchmark: - runs-on: macos-11 - steps: - - uses: actions/checkout@v2 - - name: Run All Tests - run: ./Testing/benchmark.sh diff --git a/BUILD b/BUILD index 795dbb967..e4e9ea143 100644 --- a/BUILD +++ b/BUILD @@ -198,10 +198,3 @@ test_suite( "//Source/santasyncservice:unit_tests", ], ) - -test_suite( - name = "benchmarks", - tests = [ - "//Source/santad:SNTApplicationBenchmark", - ], -) diff --git a/Source/common/BUILD b/Source/common/BUILD index aaae5ffee..2861edf5a 100644 --- a/Source/common/BUILD +++ b/Source/common/BUILD @@ -83,12 +83,6 @@ objc_library( ], ) -objc_library( - name = "SNTAllowlistInfo", - srcs = ["SNTAllowlistInfo.m"], - hdrs = ["SNTAllowlistInfo.h"], -) - objc_library( name = "SNTCommonEnums", hdrs = ["SNTCommonEnums.h"], @@ -106,6 +100,23 @@ objc_library( ], ) +objc_library( + name = "SNTKVOManager", + srcs = ["SNTKVOManager.mm"], + hdrs = ["SNTKVOManager.h"], + deps = [ + ":SNTLogging", + ], +) + +santa_unit_test( + name = "SNTKVOManagerTest", + srcs = ["SNTKVOManagerTest.mm"], + deps = [ + ":SNTKVOManager", + ], +) + objc_library( name = "SNTDropRootPrivs", srcs = ["SNTDropRootPrivs.m"], @@ -117,6 +128,7 @@ objc_library( srcs = ["SNTFileInfo.m"], hdrs = ["SNTFileInfo.h"], deps = [ + ":SNTLogging", "@FMDB", "@MOLCodesignChecker", ], @@ -298,13 +310,40 @@ santa_unit_test( deps = [":SNTMetricSet"], ) +santa_unit_test( + name = "SNTCachedDecisionTest", + srcs = ["SNTCachedDecisionTest.mm"], + deps = [ + "//Source/common:SNTCachedDecision", + "//Source/common:TestUtils", + "@OCMock", + ], +) + test_suite( name = "unit_tests", tests = [ + ":SNTCachedDecisionTest", ":SNTFileInfoTest", + ":SNTKVOManagerTest", ":SNTMetricSetTest", ":SNTPrefixTreeTest", + ":SNTRuleTest", ":SantaCacheTest", ], visibility = ["//:santa_package_group"], ) + +objc_library( + name = "TestUtils", + testonly = 1, + srcs = ["TestUtils.mm"], + hdrs = ["TestUtils.h"], + sdk_dylibs = [ + "bsm", + ], + deps = [ + "@OCMock", + "@com_google_googletest//:gtest", + ], +) diff --git a/Source/common/SNTAllowlistInfo.m b/Source/common/SNTAllowlistInfo.m deleted file mode 100644 index f185e845f..000000000 --- a/Source/common/SNTAllowlistInfo.m +++ /dev/null @@ -1,32 +0,0 @@ -/// Copyright 2021 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import "Source/common/SNTAllowlistInfo.h" - -@implementation SNTAllowlistInfo - -- (instancetype)initWithPid:(pid_t)pid - pidversion:(int)pidver - targetPath:(NSString *)targetPath - sha256:(NSString *)hash { - self = [super init]; - if (self) { - _pid = pid; - _pidversion = pidver; - _targetPath = targetPath; - _sha256 = hash; - } - return self; -} -@end diff --git a/Source/common/SNTCachedDecision.h b/Source/common/SNTCachedDecision.h index 36d2ae8ad..7df569cca 100644 --- a/Source/common/SNTCachedDecision.h +++ b/Source/common/SNTCachedDecision.h @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -12,10 +12,11 @@ /// See the License for the specific language governing permissions and /// limitations under the License. +#import #import -#import "Source/common/SNTCommonEnums.h" #import "Source/common/SNTCommon.h" +#import "Source/common/SNTCommonEnums.h" @class MOLCertificate; @@ -24,6 +25,8 @@ /// @interface SNTCachedDecision : NSObject +- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile; + @property santa_vnode_id_t vnodeId; @property SNTEventState decision; @property NSString *decisionExtra; diff --git a/Source/common/SNTCachedDecision.m b/Source/common/SNTCachedDecision.m index 8f0a48cc2..132ffd18d 100644 --- a/Source/common/SNTCachedDecision.m +++ b/Source/common/SNTCachedDecision.m @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -15,4 +15,14 @@ #import "Source/common/SNTCachedDecision.h" @implementation SNTCachedDecision + +- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile { + self = [super init]; + if (self) { + _vnodeId.fsid = (uint64_t)esFile->stat.st_dev; + _vnodeId.fileid = esFile->stat.st_ino; + } + return self; +} + @end diff --git a/Source/common/SNTCachedDecisionTest.mm b/Source/common/SNTCachedDecisionTest.mm new file mode 100644 index 000000000..88fc8f100 --- /dev/null +++ b/Source/common/SNTCachedDecisionTest.mm @@ -0,0 +1,36 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import + +#import "Source/common/SNTCachedDecision.h" +#include "Source/common/TestUtils.h" + +@interface SNTCachedDecisionTest : XCTestCase +@end + +@implementation SNTCachedDecisionTest + +- (void)testSNTCachedDecisionInit { + // Ensure the vnodeId field is properly set from the es_file_t + struct stat sb = MakeStat(1234, 5678); + es_file_t file = MakeESFile("foo", sb); + + SNTCachedDecision *cd = [[SNTCachedDecision alloc] initWithEndpointSecurityFile:&file]; + + XCTAssertEqual(sb.st_ino, cd.vnodeId.fileid); + XCTAssertEqual(sb.st_dev, cd.vnodeId.fsid); +} + +@end diff --git a/Source/common/SNTCommon.h b/Source/common/SNTCommon.h index 02a649bf1..01e700a08 100644 --- a/Source/common/SNTCommon.h +++ b/Source/common/SNTCommon.h @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -27,41 +27,23 @@ #define unlikely(x) __builtin_expect(!!(x), 0) typedef enum { - ACTION_UNSET = 0, + ACTION_UNSET, // REQUESTS - ACTION_REQUEST_SHUTDOWN = 10, - ACTION_REQUEST_BINARY = 11, + // If an operation is awaiting a cache decision from a similar operation + // currently being processed, it will poll about every 5 ms for an answer. + ACTION_REQUEST_BINARY, // RESPONSES - ACTION_RESPOND_ALLOW = 20, - ACTION_RESPOND_DENY = 21, - ACTION_RESPOND_TOOLONG = 22, - ACTION_RESPOND_ACK = 23, - ACTION_RESPOND_ALLOW_COMPILER = 24, - // The following response is stored only in the kernel decision cache. - // It is removed by SNTCompilerController - ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE = 25, + ACTION_RESPOND_ALLOW, + ACTION_RESPOND_DENY, + ACTION_RESPOND_ALLOW_COMPILER, - // NOTIFY - ACTION_NOTIFY_EXEC = 30, - ACTION_NOTIFY_WRITE = 31, - ACTION_NOTIFY_RENAME = 32, - ACTION_NOTIFY_LINK = 33, - ACTION_NOTIFY_EXCHANGE = 34, - ACTION_NOTIFY_DELETE = 35, - ACTION_NOTIFY_WHITELIST = 36, - ACTION_NOTIFY_FORK = 37, - ACTION_NOTIFY_EXIT = 38, - - // ERROR - ACTION_ERROR = 99, } santa_action_t; #define RESPONSE_VALID(x) \ (x == ACTION_RESPOND_ALLOW || x == ACTION_RESPOND_DENY || \ - x == ACTION_RESPOND_ALLOW_COMPILER || \ - x == ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE) + x == ACTION_RESPOND_ALLOW_COMPILER) // Struct to manage vnode IDs typedef struct santa_vnode_id_t { @@ -75,28 +57,4 @@ typedef struct santa_vnode_id_t { #endif } santa_vnode_id_t; -typedef struct { - santa_action_t action; - santa_vnode_id_t vnode_id; - uid_t uid; - gid_t gid; - pid_t pid; - int pidversion; - pid_t ppid; - char path[MAXPATHLEN]; - char newpath[MAXPATHLEN]; - char ttypath[MAXPATHLEN]; - // For file events, this is the process name. - // For exec requests, this is the parent process name. - // While process names can technically be 4*MAXPATHLEN, that never - // actually happens, so only take MAXPATHLEN and throw away any excess. - char pname[MAXPATHLEN]; - - // This points to a copy of the original ES message. - void *es_message; - - // This points to an NSArray of the process arguments. - void *args_array; -} santa_message_t; - #endif // SANTA__COMMON__COMMON_H diff --git a/Source/common/SNTCommonEnums.h b/Source/common/SNTCommonEnums.h index 51e69bc74..f7677a702 100644 --- a/Source/common/SNTCommonEnums.h +++ b/Source/common/SNTCommonEnums.h @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -57,6 +57,7 @@ typedef NS_ENUM(NSInteger, SNTEventState) { SNTEventStateBlockCertificate = 1 << 18, SNTEventStateBlockScope = 1 << 19, SNTEventStateBlockTeamID = 1 << 20, + SNTEventStateBlockLongPath = 1 << 21, // Bits 24-31 store allow decision types SNTEventStateAllowUnknown = 1 << 24, @@ -120,5 +121,4 @@ typedef NS_ENUM(NSInteger, SNTMetricFormatType) { static const char *kSantaDPath = "/Applications/Santa.app/Contents/Library/SystemExtensions/" "com.google.santa.daemon.systemextension/Contents/MacOS/com.google.santa.daemon"; -static const char *kSantaCtlPath = "/Applications/Santa.app/Contents/MacOS/santactl"; static const char *kSantaAppPath = "/Applications/Santa.app"; diff --git a/Source/common/SNTConfigurator.h b/Source/common/SNTConfigurator.h index e67920d70..e3695fd86 100644 --- a/Source/common/SNTConfigurator.h +++ b/Source/common/SNTConfigurator.h @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -65,7 +65,8 @@ /// rule_type /// BINARY (one of BINARY, CERTIFICATE or TEAMID) /// policy -/// BLOCKLIST (one of ALLOWLIST, ALLOWLIST_COMPILER, BLOCKLIST, SILENT_BLOCKLIST) +/// BLOCKLIST (one of ALLOWLIST, ALLOWLIST_COMPILER, BLOCKLIST, +/// SILENT_BLOCKLIST) /// /// /// @@ -244,15 +245,6 @@ /// @property(readonly, nonatomic) BOOL enableMachineIDDecoration; -/// -/// Use an internal cache for decisions instead of relying on the caching -/// mechanism built-in to the EndpointSecurity framework. This may increase -/// performance, particularly when Santa is run alongside other system -/// extensions. -/// Has no effect if the system extension is not being used. Defaults to NO. -/// -@property(readonly, nonatomic) BOOL enableSysxCache; - #pragma mark - GUI Settings /// diff --git a/Source/common/SNTConfigurator.m b/Source/common/SNTConfigurator.m index d5e7dbbd9..5c2d2a0f6 100644 --- a/Source/common/SNTConfigurator.m +++ b/Source/common/SNTConfigurator.m @@ -1,4 +1,4 @@ -/// Copyright 2021 Google Inc. All rights reserved. +/// Copyright 2014-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ @interface SNTConfigurator () /// Holds the last processed hash of the static rules list. @property(atomic) NSDictionary *cachedStaticRules; + @end @implementation SNTConfigurator @@ -94,8 +95,6 @@ @implementation SNTConfigurator static NSString *const kEnableMachineIDDecoration = @"EnableMachineIDDecoration"; -static NSString *const kEnableSysxCache = @"EnableSysxCache"; - static NSString *const kEnableForkAndExitLogging = @"EnableForkAndExitLogging"; static NSString *const kIgnoreOtherEndpointSecurityClients = @"IgnoreOtherEndpointSecurityClients"; static NSString *const kEnableDebugLogging = @"EnableDebugLogging"; @@ -206,7 +205,6 @@ - (instancetype)init { kMailDirectorySizeThresholdMB : number, kMailDirectoryEventMaxFlushTimeSec : number, kEnableMachineIDDecoration : number, - kEnableSysxCache : number, kEnableForkAndExitLogging : number, kIgnoreOtherEndpointSecurityClients : number, kEnableDebugLogging : number, @@ -425,10 +423,6 @@ + (NSSet *)keyPathsForValuesAffectingDisableUnknownEventUpload { return [self syncAndConfigStateSet]; } -+ (NSSet *)keyPathsForValuesAffectingEnableSysxCache { - return [self configStateSet]; -} - + (NSSet *)keyPathsForValuesAffectingEnableForkAndExitLogging { return [self configStateSet]; } @@ -793,11 +787,6 @@ - (BOOL)enableMachineIDDecoration { return number ? [number boolValue] : NO; } -- (BOOL)enableSysxCache { - NSNumber *number = self.configState[kEnableSysxCache]; - return number ? [number boolValue] : YES; -} - - (BOOL)enableCleanSyncEventUpload { NSNumber *number = self.configState[kSyncEnableCleanSyncEventUpload]; return number ? [number boolValue] : NO; diff --git a/Source/common/SNTFileInfo.h b/Source/common/SNTFileInfo.h index 9498397b6..32ca2ab16 100644 --- a/Source/common/SNTFileInfo.h +++ b/Source/common/SNTFileInfo.h @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -12,6 +12,7 @@ /// See the License for the specific language governing permissions and /// limitations under the License. +#import #import @class MOLCodesignChecker; @@ -32,6 +33,14 @@ /// - (instancetype)initWithPath:(NSString *)path error:(NSError **)error; +/// +/// Convenience initializer. +/// +/// @param esFile Pointer to an es_file_t provided by the EndpointSecurity framework. +/// Assumes that the path is a resolved path. +/// +- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile error:(NSError **)error; + /// /// Convenience initializer. /// diff --git a/Source/common/SNTFileInfo.m b/Source/common/SNTFileInfo.m index 67d5e300d..3cdb7ac8c 100644 --- a/Source/common/SNTFileInfo.m +++ b/Source/common/SNTFileInfo.m @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -25,6 +25,8 @@ #include #include +#import "Source/common/SNTLogging.h" + // Simple class to hold the data of a mach_header and the offset within the file // in which that header was found. @interface MachHeaderWithOffset : NSObject @@ -48,6 +50,7 @@ @interface SNTFileInfo () @property NSFileHandle *fileHandle; @property NSUInteger fileSize; @property NSString *fileOwnerHomeDir; +@property NSString *sha256Storage; // Cached properties @property NSBundle *bundleRef; @@ -63,6 +66,26 @@ @implementation SNTFileInfo extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE; - (instancetype)initWithResolvedPath:(NSString *)path error:(NSError **)error { + struct stat fileStat; + if (path.length) { + lstat(path.UTF8String, &fileStat); + } + return [self initWithResolvedPath:path stat:&fileStat error:error]; +} + +- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile error:(NSError **)error { + return [self initWithResolvedPath:@(esFile->path.data) stat:&esFile->stat error:error]; +} + +- (instancetype)initWithResolvedPath:(NSString *)path + stat:(const struct stat *)fileStat + error:(NSError **)error { + if (!fileStat) { + // This is a programming error. Bail. + LOGE(@"NULL stat buffer unsupported"); + exit(EXIT_FAILURE); + } + self = [super init]; if (self) { _path = path; @@ -76,9 +99,7 @@ - (instancetype)initWithResolvedPath:(NSString *)path error:(NSError **)error { return nil; } - struct stat fileStat; - lstat(_path.UTF8String, &fileStat); - if (!((S_IFMT & fileStat.st_mode) == S_IFREG)) { + if (!((S_IFMT & fileStat->st_mode) == S_IFREG)) { if (error) { NSString *errStr = [NSString stringWithFormat:@"Non regular file: %s", strerror(errno)]; *error = [NSError errorWithDomain:@"com.google.santa.fileinfo" @@ -88,12 +109,12 @@ - (instancetype)initWithResolvedPath:(NSString *)path error:(NSError **)error { return nil; } - _fileSize = fileStat.st_size; + _fileSize = fileStat->st_size; if (_fileSize == 0) return nil; - if (fileStat.st_uid != 0) { - struct passwd *pwd = getpwuid(fileStat.st_uid); + if (fileStat->st_uid != 0) { + struct passwd *pwd = getpwuid(fileStat->st_uid); if (pwd) { _fileOwnerHomeDir = @(pwd->pw_dir); } @@ -214,9 +235,13 @@ - (NSString *)SHA1 { } - (NSString *)SHA256 { - NSString *sha256; - [self hashSHA1:NULL SHA256:&sha256]; - return sha256; + // Memoize the value + if (!self.sha256Storage) { + NSString *sha256; + [self hashSHA1:NULL SHA256:&sha256]; + self.sha256Storage = sha256; + } + return self.sha256Storage; } #pragma mark File Type Info diff --git a/Source/common/SNTKVOManager.h b/Source/common/SNTKVOManager.h new file mode 100644 index 000000000..3d4d2ebd6 --- /dev/null +++ b/Source/common/SNTKVOManager.h @@ -0,0 +1,34 @@ +/// Copyright 2022 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import + +// The callback type when KVO notifications are received for observed key paths. +// The first parameter is the previous value, the second paramter is the new value. +typedef void (^KVOCallback)(id oldValue, id newValue); + +@interface SNTKVOManager : NSObject + +// Add an observer for the selector on the given object. When a KVO notification +// is received, the callback is called. If the notification contains objects that +// are not of the expectedType, nil is passed as the argument to the callback. +// The observer is removed when the returned instance is deallocated. +- (instancetype)initWithObject:(id)object + selector:(SEL)selector + type:(Class)expectedType + callback:(KVOCallback)callback; + +- (instancetype)init NS_UNAVAILABLE; + +@end diff --git a/Source/common/SNTKVOManager.mm b/Source/common/SNTKVOManager.mm new file mode 100644 index 000000000..4a66c56f6 --- /dev/null +++ b/Source/common/SNTKVOManager.mm @@ -0,0 +1,72 @@ +/// Copyright 2022 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import "Source/common/SNTKVOManager.h" + +#import "Source/common/SNTLogging.h" + +@interface SNTKVOManager () +@property KVOCallback callback; +@property Class expectedType; +@property NSString *keyPath; +@property id object; +@end + +@implementation SNTKVOManager + +- (instancetype)initWithObject:(id)object + selector:(SEL)selector + type:(Class)expectedType + callback:(KVOCallback)callback { + self = [super self]; + if (self) { + NSString *selectorName = NSStringFromSelector(selector); + if (![object respondsToSelector:selector]) { + LOGE(@"Attempt to add observer for an unknown selector (%@) for object (%@)", selectorName, + [object class]); + return nil; + } + + _object = object; + _keyPath = selectorName; + _expectedType = expectedType; + _callback = callback; + + [object addObserver:self + forKeyPath:selectorName + options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) + context:NULL]; + } + return self; +} + +- (void)dealloc { + [self.object removeObserver:self forKeyPath:self.keyPath context:NULL]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + id oldValue = [change[NSKeyValueChangeOldKey] isKindOfClass:self.expectedType] + ? change[NSKeyValueChangeOldKey] + : nil; + id newValue = [change[NSKeyValueChangeNewKey] isKindOfClass:self.expectedType] + ? change[NSKeyValueChangeNewKey] + : nil; + + self.callback(oldValue, newValue); +} + +@end diff --git a/Source/common/SNTKVOManagerTest.mm b/Source/common/SNTKVOManagerTest.mm new file mode 100644 index 000000000..56ae25347 --- /dev/null +++ b/Source/common/SNTKVOManagerTest.mm @@ -0,0 +1,129 @@ +/// Copyright 2022 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import + +#import "Source/common/SNTKVOManager.h" + +@interface Foo : NSObject +@property NSNumber *propNumber; +@property NSArray *propArray; +@property id propId; +@end + +@implementation Foo +@end + +@interface SNTKVOManagerTest : XCTestCase +@end + +@implementation SNTKVOManagerTest + +- (void)testInvalidSelector { + Foo *foo = [[Foo alloc] init]; + + SNTKVOManager *kvo = [[SNTKVOManager alloc] initWithObject:foo + selector:NSSelectorFromString(@"doesNotExist") + type:[NSNumber class] + callback:^(id, id){ + }]; + + XCTAssertNil(kvo); +} + +- (void)testNormalOperation { + Foo *foo = [[Foo alloc] init]; + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + + int origVal = 123; + int update1 = 456; + int update2 = 789; + + foo.propNumber = @(origVal); + + // Store the values from the callback to test against expected values + __block int oldVal; + __block int newVal; + + SNTKVOManager *kvo = + [[SNTKVOManager alloc] initWithObject:foo + selector:@selector(propNumber) + type:[NSNumber class] + callback:^(NSNumber *oldValue, NSNumber *newValue) { + oldVal = [oldValue intValue]; + newVal = [newValue intValue]; + dispatch_semaphore_signal(sema); + }]; + XCTAssertNotNil(kvo); + + // Ensure an update to the observed property triggers the callback + foo.propNumber = @(update1); + + XCTAssertEqual(0, + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)), + "Failed waiting for first observable update"); + XCTAssertEqual(oldVal, origVal); + XCTAssertEqual(newVal, update1); + + // One more time why not + foo.propNumber = @(update2); + + XCTAssertEqual(0, + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)), + "Failed waiting for second observable update"); + XCTAssertEqual(oldVal, update1); + XCTAssertEqual(newVal, update2); +} + +- (void)testUnexpectedTypes { + Foo *foo = [[Foo alloc] init]; + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + + NSString *origVal = @"any_val"; + NSString *update = @"new_val"; + foo.propId = origVal; + + __block id oldVal; + __block id newVal; + + SNTKVOManager *kvo = [[SNTKVOManager alloc] initWithObject:foo + selector:@selector(propId) + type:[NSString class] + callback:^(id oldValue, id newValue) { + oldVal = oldValue; + newVal = newValue; + dispatch_semaphore_signal(sema); + }]; + XCTAssertNotNil(kvo); + + // Update to an unexpected type (here, NSNumber instead of NSString) + foo.propId = @(123); + + XCTAssertEqual(0, + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)), + "Failed waiting for first observable update"); + XCTAssertEqualObjects(oldVal, origVal); + XCTAssertNil(newVal); + + // Update again with an expected type, ensure oldVal is now nil + foo.propId = update; + + XCTAssertEqual(0, + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)), + "Failed waiting for first observable update"); + XCTAssertNil(oldVal); + XCTAssertEqualObjects(newVal, update); +} + +@end diff --git a/Source/common/SNTRule.h b/Source/common/SNTRule.h index 0407f2ce7..d078d3efb 100644 --- a/Source/common/SNTRule.h +++ b/Source/common/SNTRule.h @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -51,18 +51,18 @@ /// Designated initializer. /// - (instancetype)initWithIdentifier:(NSString *)identifier - state:(SNTRuleState)state - type:(SNTRuleType)type - customMsg:(NSString *)customMsg - timestamp:(NSUInteger)timestamp; + state:(SNTRuleState)state + type:(SNTRuleType)type + customMsg:(NSString *)customMsg + timestamp:(NSUInteger)timestamp; /// /// Initialize with a default timestamp: current time if rule state is transitive, 0 otherwise. /// - (instancetype)initWithIdentifier:(NSString *)identifier - state:(SNTRuleState)state - type:(SNTRuleType)type - customMsg:(NSString *)customMsg; + state:(SNTRuleState)state + type:(SNTRuleType)type + customMsg:(NSString *)customMsg; /// /// Initialize with a dictionary received from a sync server. diff --git a/Source/common/SNTStoredEvent.h b/Source/common/SNTStoredEvent.h index d0a7a4aa6..7e9afa353 100644 --- a/Source/common/SNTStoredEvent.h +++ b/Source/common/SNTStoredEvent.h @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -95,7 +95,6 @@ /// @property NSArray *signingChain; - /// /// If the executed file was signed, this is the Team ID if present in the signature information. /// diff --git a/Source/common/SNTStrengthify.h b/Source/common/SNTStrengthify.h index 7df7e079d..8c70b0ccf 100644 --- a/Source/common/SNTStrengthify.h +++ b/Source/common/SNTStrengthify.h @@ -1,4 +1,4 @@ -/// Copyright 2016 Google Inc. All rights reserved. +/// Copyright 2016-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -12,10 +12,14 @@ /// See the License for the specific language governing permissions and /// limitations under the License. -#define STRONGIFY(var) \ - _Pragma("clang diagnostic push") \ - _Pragma("clang diagnostic ignored \"-Wshadow\"") \ - __strong __typeof(var) var = (Weak_##var); \ +// clang-format off + +#define STRONGIFY(var) \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wshadow\"") \ + __strong __typeof(var) var = (Weak_##var); \ _Pragma("clang diagnostic pop") #define WEAKIFY(var) __weak __typeof(var) Weak_##var = (var); + +// clang-format on diff --git a/Source/common/SNTSystemInfo.m b/Source/common/SNTSystemInfo.m index d23ae14b2..73d401d4c 100644 --- a/Source/common/SNTSystemInfo.m +++ b/Source/common/SNTSystemInfo.m @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -18,8 +18,11 @@ @implementation SNTSystemInfo + (NSString *)serialNumber { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" io_service_t platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice")); +#pragma clang diagnostic pop if (!platformExpert) return nil; NSString *serial = CFBridgingRelease(IORegistryEntryCreateCFProperty( @@ -31,8 +34,11 @@ + (NSString *)serialNumber { } + (NSString *)hardwareUUID { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" io_service_t platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice")); +#pragma clang diagnostic pop if (!platformExpert) return nil; NSString *uuid = CFBridgingRelease(IORegistryEntryCreateCFProperty( diff --git a/Source/common/SNTXPCUnprivilegedControlInterface.h b/Source/common/SNTXPCUnprivilegedControlInterface.h index f515ed31b..db88365fe 100644 --- a/Source/common/SNTXPCUnprivilegedControlInterface.h +++ b/Source/common/SNTXPCUnprivilegedControlInterface.h @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -15,8 +15,8 @@ #import #import -#import "Source/common/SNTCommonEnums.h" #import "Source/common/SNTCommon.h" +#import "Source/common/SNTCommonEnums.h" @class SNTRule; @class SNTStoredEvent; @@ -31,7 +31,6 @@ /// Cache Ops /// - (void)cacheCounts:(void (^)(uint64_t rootCache, uint64_t nonRootCache))reply; -- (void)cacheBucketCount:(void (^)(NSArray *))reply; - (void)checkCacheForVnodeID:(santa_vnode_id_t)vnodeID withReply:(void (^)(santa_action_t))reply; /// diff --git a/Source/common/SantaCache.h b/Source/common/SantaCache.h index 404c09fff..7d3e8da6b 100644 --- a/Source/common/SantaCache.h +++ b/Source/common/SantaCache.h @@ -1,4 +1,4 @@ -/// Copyright 2016 Google Inc. All rights reserved. +/// Copyright 2016-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -26,11 +27,6 @@ #include "Source/common/SNTCommon.h" -#define panic(args...) \ - printf(args); \ - printf("\n"); \ - abort() - #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -334,7 +330,9 @@ class SantaCache { inline void unlock(struct bucket *bucket) const { if (unlikely(OSAtomicTestAndClear(7, (volatile uint8_t *)&bucket->head) == 0)) { - panic("SantaCache::unlock(): Tried to unlock an unlocked lock"); + os_log_error(OS_LOG_DEFAULT, + "SantaCache::unlock(): Tried to unlock an unlocked lock"); + abort(); } } diff --git a/Source/common/TestUtils.h b/Source/common/TestUtils.h new file mode 100644 index 000000000..04079a6bf --- /dev/null +++ b/Source/common/TestUtils.h @@ -0,0 +1,60 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__COMMON__TESTUTILS_H +#define SANTA__COMMON__TESTUTILS_H + +#include +#import +#include +#include +#include +#include + +#define NOBODY_UID ((unsigned int)-2) +#define NOBODY_GID ((unsigned int)-2) + +// Bubble up googletest expectation failures to XCTest failures +#define XCTBubbleMockVerifyAndClearExpectations(mock) \ + XCTAssertTrue(::testing::Mock::VerifyAndClearExpectations(mock), \ + "Expected calls were not properly mocked") + +// Pretty print C string match errors +#define XCTAssertCStringEqual(got, want) \ + XCTAssertTrue(strcmp((got), (want)) == 0, @"\nMismatched strings.\n\t got: %s\n\twant: %s", \ + (got), (want)) + +// Pretty print C++ string match errors +#define XCTAssertCppStringEqual(got, want) XCTAssertCStringEqual((got).c_str(), (want).c_str()) + +// Helper to ensure at least `ms` milliseconds are slept, even if the sleep +// function returns early due to interrupts. +void SleepMS(long ms); + +enum class ActionType { + Auth, + Notify, +}; + +// Helpers to construct various ES structs +audit_token_t MakeAuditToken(pid_t pid, pid_t pidver); +struct stat MakeStat(ino_t ino, dev_t devno = 0); +es_string_token_t MakeESStringToken(const char *s); +es_file_t MakeESFile(const char *path, struct stat sb = {}); +es_process_t MakeESProcess(es_file_t *file, audit_token_t tok = {}, audit_token_t parent_tok = {}); +es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc, + ActionType action_type = ActionType::Notify, + uint64_t future_deadline_ms = 100000); + +#endif diff --git a/Source/common/TestUtils.mm b/Source/common/TestUtils.mm new file mode 100644 index 000000000..4a155caac --- /dev/null +++ b/Source/common/TestUtils.mm @@ -0,0 +1,107 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/common/TestUtils.h" + +#include +#include +#include +#include + +audit_token_t MakeAuditToken(pid_t pid, pid_t pidver) { + return audit_token_t{ + .val = + { + 0, + NOBODY_UID, + NOBODY_GID, + NOBODY_UID, + NOBODY_GID, + (unsigned int)pid, + 0, + (unsigned int)pidver, + }, + }; +} + +struct stat MakeStat(ino_t ino, dev_t devno) { + return (struct stat){ + .st_dev = devno, + .st_ino = ino, + }; +} + +es_string_token_t MakeESStringToken(const char *s) { + return es_string_token_t{ + .length = strlen(s), + .data = s, + }; +} + +es_file_t MakeESFile(const char *path, struct stat sb) { + return es_file_t{ + .path = MakeESStringToken(path), + .path_truncated = false, + .stat = sb, + }; +} + +es_process_t MakeESProcess(es_file_t *file, audit_token_t tok, audit_token_t parent_tok) { + return es_process_t{ + .audit_token = tok, + .ppid = audit_token_to_pid(parent_tok), + .original_ppid = audit_token_to_pid(parent_tok), + .executable = file, + .parent_audit_token = parent_tok, + }; +} + +static uint64_t AddMillisToMachTime(uint64_t ms, uint64_t machTime) { + static dispatch_once_t onceToken; + static mach_timebase_info_data_t timebase; + + dispatch_once(&onceToken, ^{ + mach_timebase_info(&timebase); + }); + + // Convert given machTime to nanoseconds + uint64_t nanoTime = machTime * timebase.numer / timebase.denom; + + // Add the ms offset + nanoTime += (ms * NSEC_PER_MSEC); + + // Convert back to machTime + return nanoTime * timebase.denom / timebase.numer; +} + +es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc, ActionType action_type, + uint64_t future_deadline_ms) { + return es_message_t{ + .deadline = AddMillisToMachTime(future_deadline_ms, mach_absolute_time()), + .process = proc, + .action_type = + (action_type == ActionType::Notify) ? ES_ACTION_TYPE_NOTIFY : ES_ACTION_TYPE_AUTH, + .event_type = et, + }; +} + +void SleepMS(long ms) { + struct timespec ts { + .tv_sec = ms / 1000, .tv_nsec = (long)((ms % 1000) * NSEC_PER_MSEC), + }; + + while (nanosleep(&ts, &ts) != 0) { + XCTAssertEqual(errno, EINTR); + } +} diff --git a/Source/common/santa.proto b/Source/common/santa.proto index bd8b5eb84..cce76e2e7 100644 --- a/Source/common/santa.proto +++ b/Source/common/santa.proto @@ -1,6 +1,7 @@ // // !!! WARNING !!! -// This proto is in beta format and subject to change. +// This proto is for demonstration purposes only and will be changing. +// Do not rely on this format. // syntax = "proto3"; diff --git a/Source/santactl/BUILD b/Source/santactl/BUILD index 5aec32e7f..ab1b84221 100644 --- a/Source/santactl/BUILD +++ b/Source/santactl/BUILD @@ -26,7 +26,6 @@ objc_library( "//:opt_build": [], "//conditions:default": [ "Commands/SNTCommandBundleInfo.m", - "Commands/SNTCommandCacheHistogram.m", "Commands/SNTCommandCheckCache.m", "Commands/SNTCommandFlushCache.m", ], diff --git a/Source/santactl/Commands/SNTCommandCacheHistogram.m b/Source/santactl/Commands/SNTCommandCacheHistogram.m deleted file mode 100644 index b0e985b57..000000000 --- a/Source/santactl/Commands/SNTCommandCacheHistogram.m +++ /dev/null @@ -1,79 +0,0 @@ -/// Copyright 2018 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#ifdef DEBUG - -#import -#import - -#import "Source/common/SNTLogging.h" -#import "Source/common/SNTXPCControlInterface.h" -#import "Source/santactl/SNTCommand.h" -#import "Source/santactl/SNTCommandController.h" - -@interface SNTCommandCacheHistogram : SNTCommand -@end - -@implementation SNTCommandCacheHistogram - -REGISTER_COMMAND_NAME(@"cachehistogram") - -+ (BOOL)requiresRoot { - return YES; -} - -+ (BOOL)requiresDaemonConn { - return YES; -} - -+ (NSString *)shortHelpText { - return @"Print a cache distribution histogram."; -} - -+ (NSString *)longHelpText { - return (@"Prints a histogram of each bucket of the in-kernel cache\n" - @" Use -g to get 'graphical' output\n" - @"Only available in DEBUG builds."); -} - -- (void)runWithArguments:(NSArray *)arguments { - [[self.daemonConn remoteObjectProxy] cacheBucketCount:^(NSArray *counts) { - NSMutableDictionary *d = [NSMutableDictionary dictionary]; - [counts enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - d[obj] = @([d[obj] intValue] + 1); - }]; - printf("There are %llu empty buckets\n", [d[@0] unsignedLongLongValue]); - - for (NSNumber *key in [d.allKeys sortedArrayUsingSelector:@selector(compare:)]) { - if ([key isEqual:@0]) continue; - uint64_t k = [key unsignedLongLongValue]; - uint64_t v = [d[key] unsignedLongLongValue]; - - if ([[[NSProcessInfo processInfo] arguments] containsObject:@"-g"]) { - printf("%4llu: ", k); - for (uint64_t y = 0; y < v; ++y) { - printf("#"); - } - printf("\n"); - } else { - printf("%4llu bucket[s] have %llu %s\n", v, k, k > 1 ? "entries" : "entry"); - } - } - exit(0); - }]; -} - -@end - -#endif diff --git a/Source/santactl/Commands/SNTCommandCheckCache.m b/Source/santactl/Commands/SNTCommandCheckCache.m index 074c1d981..872cd1dd4 100644 --- a/Source/santactl/Commands/SNTCommandCheckCache.m +++ b/Source/santactl/Commands/SNTCommandCheckCache.m @@ -1,4 +1,4 @@ -/// Copyright 2016 Google Inc. All rights reserved. +/// Copyright 2016-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -62,9 +62,6 @@ - (void)runWithArguments:(NSArray *)arguments { } else if (action == ACTION_RESPOND_ALLOW_COMPILER) { LOGI(@"File exists in [allowlist compiler] kernel cache"); exit(0); - } else if (action == ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE) { - LOGI(@"File exists in [allowlist pending_transitive] kernel cache"); - exit(0); } else if (action == ACTION_UNSET) { LOGE(@"File does not exist in cache"); exit(1); diff --git a/Source/santactl/Commands/SNTCommandFileInfo.m b/Source/santactl/Commands/SNTCommandFileInfo.m index ea17d5eba..b880daf87 100644 --- a/Source/santactl/Commands/SNTCommandFileInfo.m +++ b/Source/santactl/Commands/SNTCommandFileInfo.m @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -385,6 +385,7 @@ - (SNTAttributeBlock)rule { case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break; case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break; case SNTEventStateAllowTransitive: [output appendString:@" (Transitive)"]; break; + case SNTEventStateBlockLongPath: [output appendString:@" (Long Path)"]; break; default: output = @"None".mutableCopy; break; } diff --git a/Source/santactl/Commands/SNTCommandStatus.m b/Source/santactl/Commands/SNTCommandStatus.m index e5f1a6e03..f792bb4bd 100644 --- a/Source/santactl/Commands/SNTCommandStatus.m +++ b/Source/santactl/Commands/SNTCommandStatus.m @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -74,18 +74,14 @@ - (void)runWithArguments:(NSArray *)arguments { SNTConfigurator *configurator = [SNTConfigurator configurator]; - BOOL cachingEnabled = [configurator enableSysxCache]; - // Cache status __block uint64_t rootCacheCount = -1, nonRootCacheCount = -1; - if (cachingEnabled) { - dispatch_group_enter(group); - [[self.daemonConn remoteObjectProxy] cacheCounts:^(uint64_t rootCache, uint64_t nonRootCache) { - rootCacheCount = rootCache; - nonRootCacheCount = nonRootCache; - dispatch_group_leave(group); - }]; - } + dispatch_group_enter(group); + [[self.daemonConn remoteObjectProxy] cacheCounts:^(uint64_t rootCache, uint64_t nonRootCache) { + rootCacheCount = rootCache; + nonRootCacheCount = nonRootCache; + dispatch_group_leave(group); + }]; // Database counts __block int64_t eventCount = -1, binaryRuleCount = -1, certRuleCount = -1, teamIDRuleCount = -1; @@ -215,12 +211,12 @@ - (void)runWithArguments:(NSArray *)arguments { @"transitive_rules" : @(enableTransitiveRules), }, } mutableCopy]; - if (cachingEnabled) { - stats[@"cache"] = @{ - @"root_cache_count" : @(rootCacheCount), - @"non_root_cache_count" : @(nonRootCacheCount), - }; - } + + stats[@"cache"] = @{ + @"root_cache_count" : @(rootCacheCount), + @"non_root_cache_count" : @(nonRootCacheCount), + }; + NSData *statsData = [NSJSONSerialization dataWithJSONObject:stats options:NSJSONWritingPrettyPrinted error:nil]; @@ -238,11 +234,9 @@ - (void)runWithArguments:(NSArray *)arguments { printf(" %-25s | %lld (Peak: %.2f%%)\n", "Watchdog CPU Events", cpuEvents, cpuPeak); printf(" %-25s | %lld (Peak: %.2fMB)\n", "Watchdog RAM Events", ramEvents, ramPeak); - if (cachingEnabled) { - printf(">>> Cache Info\n"); - printf(" %-25s | %lld\n", "Root cache count", rootCacheCount); - printf(" %-25s | %lld\n", "Non-root cache count", nonRootCacheCount); - } + printf(">>> Cache Info\n"); + printf(" %-25s | %lld\n", "Root cache count", rootCacheCount); + printf(" %-25s | %lld\n", "Non-root cache count", nonRootCacheCount); printf(">>> Database Info\n"); printf(" %-25s | %lld\n", "Binary Rules", binaryRuleCount); diff --git a/Source/santad/BUILD b/Source/santad/BUILD index 9a7c8bf57..4b1100650 100644 --- a/Source/santad/BUILD +++ b/Source/santad/BUILD @@ -8,273 +8,527 @@ package( licenses(["notice"]) objc_library( - name = "database_controller", - srcs = [ - "DataLayer/SNTDatabaseTable.h", - "DataLayer/SNTDatabaseTable.m", - "DataLayer/SNTEventTable.h", - "DataLayer/SNTEventTable.m", - "DataLayer/SNTRuleTable.h", - "DataLayer/SNTRuleTable.m", - "SNTDatabaseController.h", - "SNTDatabaseController.m", + name = "SNTDatabaseTable", + srcs = ["DataLayer/SNTDatabaseTable.m"], + hdrs = ["DataLayer/SNTDatabaseTable.h"], + deps = [ + "//Source/common:SNTLogging", + "@FMDB", ], +) + +objc_library( + name = "SNTRuleTable", + srcs = ["DataLayer/SNTRuleTable.m"], + hdrs = ["DataLayer/SNTRuleTable.h"], deps = [ + ":SNTDatabaseTable", "//Source/common:SNTCachedDecision", "//Source/common:SNTCommonEnums", - "//Source/common:SNTConfigurator", "//Source/common:SNTFileInfo", - "//Source/common:SNTLogging", "//Source/common:SNTRule", - "//Source/common:SNTStoredEvent", - "@FMDB", "@MOLCertificate", "@MOLCodesignChecker", ], ) objc_library( - name = "SNTEventProvider", - hdrs = ["EventProviders/SNTEventProvider.h"], + name = "SNTEventTable", + srcs = ["DataLayer/SNTEventTable.m"], + hdrs = ["DataLayer/SNTEventTable.h"], + deps = [ + ":SNTDatabaseTable", + "//Source/common:SNTStoredEvent", + "@MOLCertificate", + ], +) + +objc_library( + name = "SNTDatabaseController", + srcs = ["SNTDatabaseController.m"], + hdrs = ["SNTDatabaseController.h"], deps = [ + ":SNTEventTable", + ":SNTRuleTable", + "//Source/common:SNTLogging", + "@FMDB", + ], +) + +objc_library( + name = "SNTEndpointSecurityEventHandler", + hdrs = ["EventProviders/SNTEndpointSecurityEventHandler.h"], + deps = [ + ":EndpointSecurityMessage", "//Source/common:SNTCommon", ], ) objc_library( - name = "endpoint_security_manager", + name = "SNTApplicationCoreMetrics", + srcs = ["SNTApplicationCoreMetrics.m"], + hdrs = ["SNTApplicationCoreMetrics.h"], + deps = [ + "//Source/common:SNTCommonEnums", + "//Source/common:SNTConfigurator", + "//Source/common:SNTMetricSet", + "//Source/common:SNTSystemInfo", + ], +) + +objc_library( + name = "DiskArbitrationTestLib", + testonly = 1, srcs = [ - "EventProviders/SNTEndpointSecurityManager.h", - "EventProviders/SNTEndpointSecurityManager.mm", + "EventProviders/DiskArbitrationTestUtil.h", + "EventProviders/DiskArbitrationTestUtil.mm", ], sdk_dylibs = [ "EndpointSecurity", "bsm", ], + sdk_frameworks = [ + "DiskArbitration", + "IOKit", + ], +) + +objc_library( + name = "SNTDecisionCache", + srcs = ["SNTDecisionCache.mm"], + hdrs = ["SNTDecisionCache.h"], deps = [ - ":SNTEventProvider", + ":SNTDatabaseController", + "//Source/common:SNTCachedDecision", + "//Source/common:SNTCommonEnums", + "//Source/common:SNTRule", + ], +) + +objc_library( + name = "SNTCompilerController", + srcs = ["SNTCompilerController.mm"], + hdrs = ["SNTCompilerController.h"], + deps = [ + ":EndpointSecurityLogger", + ":EndpointSecurityMessage", + ":SNTDecisionCache", + ":SNTRuleTable", + "//Source/common:SNTCachedDecision", "//Source/common:SNTCommon", - "//Source/common:SNTConfigurator", + "//Source/common:SNTFileInfo", "//Source/common:SNTLogging", - "//Source/common:SNTPrefixTree", - "//Source/common:SantaCache", + "//Source/common:SNTRule", + ], +) + +objc_library( + name = "SNTNotificationQueue", + srcs = ["SNTNotificationQueue.m"], + hdrs = ["SNTNotificationQueue.h"], + deps = [ + "//Source/common:SNTLogging", + "//Source/common:SNTStoredEvent", + "//Source/common:SNTXPCNotifierInterface", + "@MOLXPCConnection", ], ) objc_library( - name = "event_logs_common", + name = "SNTSyncdQueue", + srcs = ["SNTSyncdQueue.m"], + hdrs = ["SNTSyncdQueue.h"], + deps = [ + "//Source/common:SNTCommonEnums", + "//Source/common:SNTLogging", + "//Source/common:SNTStoredEvent", + "//Source/common:SNTXPCSyncServiceInterface", + "@MOLXPCConnection", + ], +) + +objc_library( + name = "SNTPolicyProcessor", srcs = [ "DataLayer/SNTDatabaseTable.h", "DataLayer/SNTRuleTable.h", - "Logs/SNTEventLog.h", - "Logs/SNTEventLog.m", - "Logs/SNTFileEventLog.h", - "Logs/SNTProtobufEventLog.h", - "Logs/SNTSyslogEventLog.h", - "SNTDatabaseController.h", - ], - hdrs = [ - "Logs/SNTEventLog.h", + "SNTPolicyProcessor.h", + "SNTPolicyProcessor.m", ], deps = [ - ":database_controller", - "//Source/common:SNTAllowlistInfo", "//Source/common:SNTCachedDecision", "//Source/common:SNTCommon", "//Source/common:SNTCommonEnums", - "//Source/common:SNTConfigurator", + "//Source/common:SNTFileInfo", "//Source/common:SNTLogging", "//Source/common:SNTRule", - "//Source/common:SNTStoredEvent", - "@FMDB", + "@MOLXPCConnection", ], ) objc_library( - name = "protobuf_event_logs", - srcs = [ - "Logs/SNTLogOutput.h", - "Logs/SNTProtobufEventLog.h", - "Logs/SNTProtobufEventLog.m", - "Logs/SNTSimpleMaildir.h", - "Logs/SNTSimpleMaildir.m", - ], + name = "SNTExecutionController", + srcs = ["SNTExecutionController.mm"], + hdrs = ["SNTExecutionController.h"], deps = [ - ":event_logs_common", - "//Source/common:SNTAllowlistInfo", + ":EndpointSecurityMessage", + ":SNTDecisionCache", + ":SNTNotificationQueue", + ":SNTPolicyProcessor", + ":SNTSyncdQueue", + "//Source/common:SNTBlockMessage", "//Source/common:SNTCachedDecision", "//Source/common:SNTCommon", - "//Source/common:SNTConfigurator", + "//Source/common:SNTCommonEnums", + "//Source/common:SNTDropRootPrivs", + "//Source/common:SNTFileInfo", "//Source/common:SNTLogging", "//Source/common:SNTMetricSet", - "//Source/common:SNTStoredEvent", - "//Source/common:santa_objc_proto", + "//Source/common:SNTRule", + "@MOLCodesignChecker", ], ) objc_library( - name = "syslog_event_logs", - srcs = [ - "EventProviders/SNTEndpointSecurityManager.h", - "EventProviders/SNTEventProvider.h", - "Logs/SNTSyslogEventLog.h", - "Logs/SNTSyslogEventLog.m", + name = "SNTEndpointSecurityClientBase", + hdrs = ["EventProviders/SNTEndpointSecurityClientBase.h"], + deps = [ + ":EndpointSecurityAPI", + ":EndpointSecurityClient", + ":EndpointSecurityEnrichedTypes", + ":EndpointSecurityMessage", ], +) + +objc_library( + name = "SNTEndpointSecurityClient", + srcs = ["EventProviders/SNTEndpointSecurityClient.mm"], + hdrs = ["EventProviders/SNTEndpointSecurityClient.h"], deps = [ - ":endpoint_security_manager", - ":event_logs_common", - "//Source/common:SNTAllowlistInfo", - "//Source/common:SNTCachedDecision", + ":EndpointSecurityAPI", + ":EndpointSecurityEnrichedTypes", + ":SNTEndpointSecurityClientBase", "//Source/common:SNTCommon", "//Source/common:SNTConfigurator", "//Source/common:SNTLogging", - "//Source/common:SNTStoredEvent", ], ) objc_library( - name = "file_event_logs", - srcs = [ - "Logs/SNTFileEventLog.h", - "Logs/SNTFileEventLog.m", - "Logs/SNTSyslogEventLog.h", + name = "SNTEndpointSecurityRecorder", + srcs = ["EventProviders/SNTEndpointSecurityRecorder.mm"], + hdrs = ["EventProviders/SNTEndpointSecurityRecorder.h"], + deps = [ + ":AuthResultCache", + ":EndpointSecurityAPI", + ":EndpointSecurityEnricher", + ":EndpointSecurityLogger", + ":SNTCompilerController", + ":SNTEndpointSecurityClient", + ":SNTEndpointSecurityEventHandler", + "//Source/common:SNTLogging", + "//Source/common:SNTPrefixTree", ], +) + +objc_library( + name = "SNTEndpointSecurityTamperResistance", + srcs = ["EventProviders/SNTEndpointSecurityTamperResistance.mm"], + hdrs = ["EventProviders/SNTEndpointSecurityTamperResistance.h"], deps = [ - ":event_logs_common", - "//Source/common:SNTCommon", - "//Source/common:SNTConfigurator", + ":EndpointSecurityAPI", + ":EndpointSecurityLogger", + ":EndpointSecurityMessage", + ":SNTEndpointSecurityClient", + ":SNTEndpointSecurityEventHandler", + ], +) + +objc_library( + name = "SNTEndpointSecurityAuthorizer", + srcs = ["EventProviders/SNTEndpointSecurityAuthorizer.mm"], + hdrs = ["EventProviders/SNTEndpointSecurityAuthorizer.h"], + deps = [ + ":AuthResultCache", + ":EndpointSecurityAPI", + ":EndpointSecurityEnricher", + ":SNTCompilerController", + ":SNTEndpointSecurityClient", + ":SNTEndpointSecurityEventHandler", + ":SNTExecutionController", + ], +) + +objc_library( + name = "SNTEndpointSecurityDeviceManager", + srcs = ["EventProviders/SNTEndpointSecurityDeviceManager.mm"], + hdrs = ["EventProviders/SNTEndpointSecurityDeviceManager.h"], + deps = [ + ":AuthResultCache", + ":EndpointSecurityAPI", + ":EndpointSecurityLogger", + ":EndpointSecurityMessage", + ":SNTEndpointSecurityClient", + ":SNTEndpointSecurityEventHandler", + "//Source/common:SNTDeviceEvent", "//Source/common:SNTLogging", - "//Source/common:SNTStrengthify", ], ) objc_library( - name = "event_logs", - hdrs = ["Logs/SNTEventLog.h"], + name = "AuthResultCache", + srcs = ["EventProviders/AuthResultCache.mm"], + hdrs = ["EventProviders/AuthResultCache.h"], deps = [ - ":file_event_logs", - ":protobuf_event_logs", - ":syslog_event_logs", - "//Source/common:SNTCommon", + ":EndpointSecurityAPI", + ":EndpointSecurityClient", + "//Source/common:SNTLogging", + "//Source/common:SantaCache", ], ) objc_library( - name = "santad_lib", - srcs = [ - "DataLayer/SNTDatabaseTable.h", - "DataLayer/SNTEventTable.h", - "DataLayer/SNTRuleTable.h", - "EventProviders/SNTCachingEndpointSecurityManager.h", - "EventProviders/SNTCachingEndpointSecurityManager.mm", - "EventProviders/SNTDeviceManager.h", - "EventProviders/SNTDeviceManager.mm", - "EventProviders/SNTEndpointSecurityManager.h", - "EventProviders/SNTEventProvider.h", - "SNTApplication.h", - "SNTApplication.m", - "SNTCompilerController.h", - "SNTCompilerController.m", - "SNTDaemonControlController.h", - "SNTDaemonControlController.m", - "SNTDatabaseController.h", - "SNTExecutionController.h", - "SNTExecutionController.m", - "SNTNotificationQueue.h", - "SNTNotificationQueue.m", - "SNTPolicyProcessor.h", - "SNTPolicyProcessor.m", - "SNTSyncdQueue.h", - "SNTSyncdQueue.m", - "main.m", + name = "EndpointSecurityEnricher", + srcs = ["EventProviders/EndpointSecurity/Enricher.mm"], + hdrs = ["EventProviders/EndpointSecurity/Enricher.h"], + deps = [ + ":EndpointSecurityEnrichedTypes", + "//Source/common:SNTLogging", + "//Source/common:SantaCache", + ], +) + +objc_library( + name = "EndpointSecurityEnrichedTypes", + hdrs = ["EventProviders/EndpointSecurity/EnrichedTypes.h"], + deps = [ + ":EndpointSecurityMessage", ], +) + +objc_library( + name = "EndpointSecuritySerializer", + srcs = ["Logs/EndpointSecurity/Serializers/Serializer.mm"], + hdrs = ["Logs/EndpointSecurity/Serializers/Serializer.h"], + deps = [ + ":EndpointSecurityEnrichedTypes", + ":EndpointSecurityMessage", + ":SNTDecisionCache", + ], +) + +objc_library( + name = "EndpointSecuritySerializerEmpty", + srcs = ["Logs/EndpointSecurity/Serializers/Empty.mm"], + hdrs = ["Logs/EndpointSecurity/Serializers/Empty.h"], + deps = [ + ":EndpointSecuritySerializer", + ], +) + +objc_library( + name = "EndpointSecuritySanitizableString", + srcs = ["Logs/EndpointSecurity/Serializers/SanitizableString.mm"], + hdrs = ["Logs/EndpointSecurity/Serializers/SanitizableString.h"], sdk_dylibs = [ "EndpointSecurity", - "bsm", ], - sdk_frameworks = [ - "DiskArbitration", - "IOKit", + deps = [ + ":EndpointSecurityMessage", ], +) + +objc_library( + name = "EndpointSecuritySerializerBasicString", + srcs = ["Logs/EndpointSecurity/Serializers/BasicString.mm"], + hdrs = ["Logs/EndpointSecurity/Serializers/BasicString.h"], deps = [ - ":SNTApplicationCoreMetrics", - ":database_controller", - ":endpoint_security_manager", - ":event_logs", - "//Source/common:SNTAllowlistInfo", - "//Source/common:SNTBlockMessage", - "//Source/common:SNTCachedDecision", - "//Source/common:SNTCommon", + ":EndpointSecuritySanitizableString", + ":EndpointSecuritySerializer", + ":SNTDecisionCache", + "//Source/common:SNTLogging", + "//Source/common:SNTStoredEvent", + ], +) + +objc_library( + name = "EndpointSecurityWriter", + hdrs = ["Logs/EndpointSecurity/Writers/Writer.h"], +) + +objc_library( + name = "EndpointSecurityWriterSyslog", + srcs = ["Logs/EndpointSecurity/Writers/Syslog.mm"], + hdrs = ["Logs/EndpointSecurity/Writers/Syslog.h"], + deps = [ + ":EndpointSecurityWriter", + ], +) + +objc_library( + name = "EndpointSecurityWriterFile", + srcs = ["Logs/EndpointSecurity/Writers/File.mm"], + hdrs = ["Logs/EndpointSecurity/Writers/File.h"], + deps = [ + ":EndpointSecurityWriter", + ], +) + +objc_library( + name = "EndpointSecurityWriterNull", + srcs = ["Logs/EndpointSecurity/Writers/Null.mm"], + hdrs = ["Logs/EndpointSecurity/Writers/Null.h"], + deps = [ + ":EndpointSecurityWriter", + ], +) + +objc_library( + name = "EndpointSecurityLogger", + srcs = ["Logs/EndpointSecurity/Logger.mm"], + hdrs = ["Logs/EndpointSecurity/Logger.h"], + deps = [ + ":EndpointSecurityAPI", + ":EndpointSecuritySerializer", + ":EndpointSecuritySerializerBasicString", + ":EndpointSecuritySerializerEmpty", + ":EndpointSecurityWriter", + ":EndpointSecurityWriterFile", + ":EndpointSecurityWriterNull", + ":EndpointSecurityWriterSyslog", "//Source/common:SNTCommonEnums", - "//Source/common:SNTConfigurator", - "//Source/common:SNTDeviceEvent", - "//Source/common:SNTDropRootPrivs", - "//Source/common:SNTFileInfo", "//Source/common:SNTLogging", - "//Source/common:SNTMetricSet", - "//Source/common:SNTRule", "//Source/common:SNTStoredEvent", - "//Source/common:SNTStrengthify", - "//Source/common:SNTXPCBundleServiceInterface", + ], +) + +objc_library( + name = "EndpointSecurityMessage", + srcs = [ + "EventProviders/EndpointSecurity/EndpointSecurityAPI.h", + "EventProviders/EndpointSecurity/Message.mm", + ], + hdrs = ["EventProviders/EndpointSecurity/Message.h"], + deps = [ + ":EndpointSecurityClient", + ], +) + +objc_library( + name = "EndpointSecurityClient", + hdrs = ["EventProviders/EndpointSecurity/Client.h"], +) + +objc_library( + name = "EndpointSecurityAPI", + srcs = ["EventProviders/EndpointSecurity/EndpointSecurityAPI.mm"], + hdrs = ["EventProviders/EndpointSecurity/EndpointSecurityAPI.h"], + deps = [ + ":EndpointSecurityClient", + ":EndpointSecurityMessage", + ], +) + +objc_library( + name = "SNTDaemonControlController", + srcs = ["SNTDaemonControlController.mm"], + hdrs = ["SNTDaemonControlController.h"], + deps = [ + ":AuthResultCache", + ":EndpointSecurityLogger", + ":SNTDatabaseController", + ":SNTNotificationQueue", + ":SNTPolicyProcessor", + ":SNTSyncdQueue", + "//Source/common:SNTLogging", + "//Source/common:SNTMetricSet", "//Source/common:SNTXPCControlInterface", - "//Source/common:SNTXPCMetricServiceInterface", "//Source/common:SNTXPCNotifierInterface", "//Source/common:SNTXPCSyncServiceInterface", - "//Source/common:SNTXPCUnprivilegedControlInterface", - "//Source/common:SantaCache", "@FMDB", - "@MOLCertificate", - "@MOLCodesignChecker", + ], +) + +objc_library( + name = "Metrics", + srcs = ["Metrics.mm"], + hdrs = ["Metrics.h"], + deps = [ + ":SNTApplicationCoreMetrics", + "//Source/common:SNTLogging", + "//Source/common:SNTMetricSet", + "//Source/common:SNTXPCMetricServiceInterface", "@MOLXPCConnection", ], ) objc_library( - name = "SNTApplicationCoreMetrics", - srcs = ["SNTApplicationCoreMetrics.m"], - hdrs = ["SNTApplicationCoreMetrics.h"], + name = "Santad", + srcs = ["Santad.mm"], + hdrs = ["Santad.h"], deps = [ + ":AuthResultCache", + ":EndpointSecurityAPI", + ":EndpointSecurityEnricher", + ":EndpointSecurityLogger", + ":Metrics", + ":SNTCompilerController", + ":SNTDaemonControlController", + ":SNTEndpointSecurityAuthorizer", + ":SNTEndpointSecurityDeviceManager", + ":SNTEndpointSecurityRecorder", + ":SNTEndpointSecurityTamperResistance", + ":SNTExecutionController", + ":SNTNotificationQueue", + ":SNTSyncdQueue", "//Source/common:SNTCommonEnums", "//Source/common:SNTConfigurator", - "//Source/common:SNTMetricSet", - "//Source/common:SNTSystemInfo", + "//Source/common:SNTKVOManager", + "//Source/common:SNTLogging", + "//Source/common:SNTPrefixTree", + "//Source/common:SNTXPCNotifierInterface", ], ) objc_library( - name = "EndpointSecurityTestLib", - testonly = 1, - srcs = [ - "EventProviders/EndpointSecurityTestUtil.h", - "EventProviders/EndpointSecurityTestUtil.mm", - ], - hdrs = [ - "EventProviders/EndpointSecurityTestUtil.h", - ], - sdk_dylibs = [ - "EndpointSecurity", - "bsm", - ], - sdk_frameworks = [ - "DiskArbitration", - "IOKit", + name = "SantadDeps", + srcs = ["SantadDeps.mm"], + hdrs = ["SantadDeps.h"], + deps = [ + ":AuthResultCache", + ":EndpointSecurityAPI", + ":EndpointSecurityEnricher", + ":EndpointSecurityLogger", + ":Metrics", + ":SNTCompilerController", + ":SNTDatabaseController", + ":SNTExecutionController", + ":SNTNotificationQueue", + ":SNTSyncdQueue", + "//Source/common:SNTPrefixTree", + "//Source/common:SNTXPCControlInterface", + "//Source/common:SNTXPCUnprivilegedControlInterface", + "@MOLXPCConnection", ], ) objc_library( - name = "DiskArbitrationTestLib", - testonly = 1, - srcs = [ - "EventProviders/DiskArbitrationTestUtil.h", - "EventProviders/DiskArbitrationTestUtil.mm", - ], + name = "santad_main", + srcs = ["main.mm"], sdk_dylibs = [ - "EndpointSecurity", "bsm", + "EndpointSecurity", ], sdk_frameworks = [ "DiskArbitration", - "IOKit", + ], + deps = [ + ":SNTDaemonControlController", + ":Santad", + ":SantadDeps", + "//Source/common:SNTConfigurator", + "//Source/common:SNTLogging", + "//Source/common:SNTXPCControlInterface", ], ) @@ -301,43 +555,23 @@ macos_bundle( }), version = "//:version", visibility = ["//:santa_package_group"], - deps = [":santad_lib"], + deps = [":santad_main"], ) -santa_unit_test( - name = "SNTExecutionControllerTest", - srcs = [ - "DataLayer/SNTDatabaseTable.h", - "DataLayer/SNTEventTable.h", - "DataLayer/SNTRuleTable.h", - "EventProviders/SNTEventProvider.h", - "SNTExecutionController.h", - "SNTExecutionControllerTest.m", - ], +# +# Begin test targets +# + +objc_library( + name = "MockEndpointSecurityAPI", + testonly = 1, + hdrs = ["EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"], sdk_dylibs = [ "EndpointSecurity", - "bsm", ], deps = [ - ":santad_lib", - "//Source/common:SNTBlockMessage", - "//Source/common:SNTCachedDecision", - "//Source/common:SNTCommon", - "//Source/common:SNTCommonEnums", - "//Source/common:SNTConfigurator", - "//Source/common:SNTDropRootPrivs", - "//Source/common:SNTFileInfo", - "//Source/common:SNTLogging", - "//Source/common:SNTMetricSet", - "//Source/common:SNTPrefixTree", - "//Source/common:SNTRule", - "//Source/common:SNTXPCNotifierInterface", - "//Source/common:SantaCache", - "@FMDB", - "@MOLCertificate", - "@MOLCodesignChecker", - "@MOLXPCConnection", - "@OCMock", + ":EndpointSecurityAPI", + "@com_google_googletest//:gtest", ], ) @@ -386,154 +620,386 @@ santa_unit_test( ) santa_unit_test( - name = "SNTEndpointSecurityManagerTest", - srcs = [ - "EventProviders/SNTEndpointSecurityManager.h", - "EventProviders/SNTEndpointSecurityManager.mm", - "EventProviders/SNTEndpointSecurityManagerTest.mm", - "EventProviders/SNTEventProvider.h", + name = "SantadTest", + srcs = ["SantadTest.mm"], + data = [ + "//Source/santad/testdata:binaryrules_testdata", ], minimum_os_version = "10.15", sdk_dylibs = [ - "EndpointSecurity", "bsm", + "EndpointSecurity", ], + sdk_frameworks = [ + "DiskArbitration", + ], + tags = ["exclusive"], deps = [ - ":EndpointSecurityTestLib", - "//Source/common:SNTCommon", + ":EndpointSecurityMessage", + ":MockEndpointSecurityAPI", + ":SNTDatabaseController", + ":SNTEndpointSecurityAuthorizer", + ":SantadDeps", "//Source/common:SNTConfigurator", - "//Source/common:SNTLogging", - "//Source/common:SNTPrefixTree", - "//Source/common:SantaCache", + "//Source/common:TestUtils", + "@MOLCertificate", + "@MOLCodesignChecker", + "@OCMock", + "@com_google_googletest//:gtest", ], ) santa_unit_test( - name = "SNTDeviceManagerTest", + name = "SNTApplicationCoreMetricsTest", srcs = [ - "EventProviders/DiskArbitrationTestUtil.h", - "EventProviders/SNTDeviceManager.h", - "EventProviders/SNTDeviceManagerTest.mm", + "SNTApplicationCoreMetricsTest.m", ], minimum_os_version = "10.15", + deps = [ + ":SNTApplicationCoreMetrics", + "//Source/common:SNTCommonEnums", + "//Source/common:SNTConfigurator", + "//Source/common:SNTMetricSet", + "//Source/common:SNTSystemInfo", + "//Source/santametricservice/Formats:SNTMetricFormatTestHelper", + "@OCMock", + ], +) + +santa_unit_test( + name = "EndpointSecuritySanitizableStringTest", + srcs = ["Logs/EndpointSecurity/Serializers/SanitizableStringTest.mm"], sdk_dylibs = [ "EndpointSecurity", + ], + deps = [ + ":EndpointSecuritySanitizableString", + "//Source/common:TestUtils", + "@OCMock", + ], +) + +santa_unit_test( + name = "EndpointSecuritySerializerBasicStringTest", + srcs = ["Logs/EndpointSecurity/Serializers/BasicStringTest.mm"], + sdk_dylibs = [ "bsm", + "EndpointSecurity", ], deps = [ - ":DiskArbitrationTestLib", - ":EndpointSecurityTestLib", - ":santad_lib", - "//Source/common:SNTCommon", + ":EndpointSecurityEnrichedTypes", + ":EndpointSecurityEnricher", + ":EndpointSecurityMessage", + ":EndpointSecuritySerializer", + ":EndpointSecuritySerializerBasicString", + ":MockEndpointSecurityAPI", + ":SNTDecisionCache", + "//Source/common:SNTCachedDecision", + "//Source/common:SNTCommonEnums", "//Source/common:SNTConfigurator", - "//Source/common:SNTDeviceEvent", - "//Source/common:SNTPrefixTree", - "//Source/common:SantaCache", + "//Source/common:SNTStoredEvent", + "//Source/common:TestUtils", "@OCMock", + "@com_google_googletest//:gtest", ], ) santa_unit_test( - name = "SNTApplicationTest", - srcs = [ - "SNTApplication.h", - "SNTApplicationTest.m", - "SNTDatabaseController.h", + name = "AuthResultCacheTest", + srcs = ["EventProviders/AuthResultCacheTest.mm"], + sdk_dylibs = [ + "EndpointSecurity", ], - data = [ - "//Source/santad/testdata:binaryrules_testdata", + deps = [ + ":AuthResultCache", + ":MockEndpointSecurityAPI", + "//Source/common:TestUtils", + "@OCMock", + "@com_google_googletest//:gtest", ], - minimum_os_version = "10.15", +) + +santa_unit_test( + name = "EndpointSecuritySerializerEmptyTest", + srcs = ["Logs/EndpointSecurity/Serializers/EmptyTest.mm"], + sdk_dylibs = [ + "EndpointSecurity", + ], + deps = [ + ":EndpointSecurityEnrichedTypes", + ":EndpointSecuritySerializerEmpty", + "@OCMock", + ], +) + +santa_unit_test( + name = "EndpointSecurityWriterFileTest", + srcs = ["Logs/EndpointSecurity/Writers/FileTest.mm"], + deps = [ + ":EndpointSecurityWriterFile", + "//Source/common:TestUtils", + "@OCMock", + "@com_google_googletest//:gtest", + ], +) + +santa_unit_test( + name = "EndpointSecurityLoggerTest", + srcs = ["Logs/EndpointSecurity/LoggerTest.mm"], sdk_dylibs = [ + "bsm", "EndpointSecurity", + ], + deps = [ + ":EndpointSecurityEnrichedTypes", + ":EndpointSecurityLogger", + ":EndpointSecurityMessage", + ":EndpointSecuritySerializer", + ":EndpointSecuritySerializerBasicString", + ":EndpointSecuritySerializerEmpty", + ":EndpointSecurityWriter", + ":EndpointSecurityWriterFile", + ":EndpointSecurityWriterNull", + ":EndpointSecurityWriterSyslog", + ":MockEndpointSecurityAPI", + "//Source/common:SNTCommonEnums", + "//Source/common:TestUtils", + "@OCMock", + "@com_google_googletest//:gtest", + ], +) + +santa_unit_test( + name = "EndpointSecurityClientTest", + srcs = ["EventProviders/EndpointSecurity/ClientTest.mm"], + deps = [ + ":EndpointSecurityClient", + "@OCMock", + ], +) + +santa_unit_test( + name = "EndpointSecurityEnricherTest", + srcs = ["EventProviders/EndpointSecurity/EnricherTest.mm"], + sdk_dylibs = [ "bsm", ], - tags = ["exclusive"], deps = [ - ":EndpointSecurityTestLib", - ":santad_lib", - "//Source/common:SNTConfigurator", - "@FMDB", - "@MOLCertificate", - "@MOLCodesignChecker", - "@MOLXPCConnection", + ":EndpointSecurityEnricher", + "//Source/common:TestUtils", "@OCMock", ], ) santa_unit_test( - name = "SNTApplicationBenchmark", - srcs = [ - "SNTApplicationBenchmark.m", + name = "EndpointSecurityMessageTest", + srcs = ["EventProviders/EndpointSecurity/MessageTest.mm"], + sdk_dylibs = [ + "bsm", + "EndpointSecurity", ], - data = [ - "//Source/santad/testdata:binaryrules_testdata", + deps = [ + ":EndpointSecurityMessage", + ":MockEndpointSecurityAPI", + "//Source/common:TestUtils", + "@OCMock", + "@com_google_googletest//:gtest", ], - minimum_os_version = "10.15", +) + +santa_unit_test( + name = "MetricsTest", + srcs = ["MetricsTest.mm"], + deps = [ + ":Metrics", + "@OCMock", + ], +) + +santa_unit_test( + name = "SNTDecisionCacheTest", + srcs = ["SNTDecisionCacheTest.mm"], sdk_dylibs = [ "EndpointSecurity", + ], + deps = [ + ":SNTDatabaseController", + ":SNTDecisionCache", + "//Source/common:SNTCommonEnums", + "//Source/common:SNTRule", + "//Source/common:TestUtils", + "@OCMock", + ], +) + +santa_unit_test( + name = "SNTEndpointSecurityClientTest", + srcs = ["EventProviders/SNTEndpointSecurityClientTest.mm"], + sdk_dylibs = [ "bsm", + "EndpointSecurity", ], deps = [ - ":EndpointSecurityTestLib", - ":santad_lib", - "@MOLCodesignChecker", - "@MOLXPCConnection", + ":EndpointSecurityClient", + ":EndpointSecurityEnrichedTypes", + ":EndpointSecurityMessage", + ":MockEndpointSecurityAPI", + ":SNTEndpointSecurityClient", + "//Source/common:TestUtils", "@OCMock", + "@com_google_googletest//:gtest", ], ) santa_unit_test( - name = "SNTApplicationCoreMetricsTest", - srcs = [ - "SNTApplicationCoreMetricsTest.m", + name = "SNTExecutionControllerTest", + srcs = ["SNTExecutionControllerTest.mm"], + sdk_dylibs = [ + "EndpointSecurity", + "bsm", ], - minimum_os_version = "10.15", deps = [ - ":SNTApplicationCoreMetrics", + ":EndpointSecurityMessage", + ":MockEndpointSecurityAPI", + ":SNTDatabaseController", + ":SNTDecisionCache", + ":SNTExecutionController", + "//Source/common:SNTCachedDecision", "//Source/common:SNTCommonEnums", "//Source/common:SNTConfigurator", + "//Source/common:SNTFileInfo", "//Source/common:SNTMetricSet", - "//Source/common:SNTSystemInfo", - "//Source/santametricservice/Formats:SNTMetricFormatTestHelper", + "//Source/common:SNTRule", + "//Source/common:TestUtils", + "@MOLCertificate", + "@MOLCodesignChecker", "@OCMock", ], ) santa_unit_test( - name = "SNTProtobufEventLogTest", - srcs = [ - "Logs/SNTLogOutput.h", - "Logs/SNTProtobufEventLog.h", - "Logs/SNTProtobufEventLogTest.m", - "Logs/SNTSimpleMaildir.h", + name = "SNTEndpointSecurityAuthorizerTest", + srcs = ["EventProviders/SNTEndpointSecurityAuthorizerTest.mm"], + sdk_dylibs = [ + "EndpointSecurity", ], - minimum_os_version = "10.15", deps = [ - ":EndpointSecurityTestLib", - ":event_logs", - "//Source/common:SNTAllowlistInfo", - "//Source/common:SNTCachedDecision", + ":AuthResultCache", + ":EndpointSecurityClient", + ":EndpointSecurityMessage", + ":MockEndpointSecurityAPI", + ":SNTCompilerController", + ":SNTEndpointSecurityAuthorizer", + "//Source/common:TestUtils", + "@OCMock", + "@com_google_googletest//:gtest", + ], +) + +santa_unit_test( + name = "SNTEndpointSecurityTamperResistanceTest", + srcs = ["EventProviders/SNTEndpointSecurityTamperResistanceTest.mm"], + sdk_dylibs = [ + "EndpointSecurity", + ], + deps = [ + ":EndpointSecurityClient", + ":EndpointSecurityMessage", + ":MockEndpointSecurityAPI", + ":SNTEndpointSecurityTamperResistance", + "//Source/common:TestUtils", + "@OCMock", + "@com_google_googletest//:gtest", + ], +) + +santa_unit_test( + name = "SNTEndpointSecurityRecorderTest", + srcs = ["EventProviders/SNTEndpointSecurityRecorderTest.mm"], + sdk_dylibs = [ + "EndpointSecurity", + ], + deps = [ + ":AuthResultCache", + ":EndpointSecurityClient", + ":EndpointSecurityEnricher", + ":EndpointSecurityLogger", + ":EndpointSecurityMessage", + ":MockEndpointSecurityAPI", + ":SNTCompilerController", + ":SNTEndpointSecurityRecorder", + "//Source/common:TestUtils", + "@OCMock", + "@com_google_googletest//:gtest", + ], +) + +santa_unit_test( + name = "SNTEndpointSecurityDeviceManagerTest", + srcs = ["EventProviders/SNTEndpointSecurityDeviceManagerTest.mm"], + sdk_dylibs = [ + "bsm", + "EndpointSecurity", + ], + deps = [ + ":DiskArbitrationTestLib", + ":EndpointSecurityClient", + ":EndpointSecurityMessage", + ":MockEndpointSecurityAPI", + ":SNTEndpointSecurityDeviceManager", "//Source/common:SNTConfigurator", - "//Source/common:SNTLogging", - "//Source/common:SNTRule", - "//Source/common:SNTStoredEvent", - "//Source/common:santa_objc_proto", + "//Source/common:SNTDeviceEvent", + "//Source/common:TestUtils", + "@OCMock", + "@com_google_googletest//:gtest", + ], +) + +santa_unit_test( + name = "SNTCompilerControllerTest", + srcs = ["SNTCompilerControllerTest.mm"], + sdk_dylibs = [ + "EndpointSecurity", + ], + deps = [ + ":EndpointSecurityLogger", + ":EndpointSecurityMessage", + ":MockEndpointSecurityAPI", + ":SNTCompilerController", + ":SNTDecisionCache", + "//Source/common:SNTCachedDecision", + "//Source/common:SNTFileInfo", + "//Source/common:TestUtils", "@OCMock", + "@com_google_googletest//:gtest", ], ) test_suite( name = "unit_tests", tests = [ + ":AuthResultCacheTest", + ":EndpointSecurityClientTest", + ":EndpointSecurityEnricherTest", + ":EndpointSecurityLoggerTest", + ":EndpointSecurityMessageTest", + ":EndpointSecuritySanitizableStringTest", + ":EndpointSecuritySerializerBasicStringTest", + ":EndpointSecuritySerializerEmptyTest", + ":EndpointSecurityWriterFileTest", + ":MetricsTest", ":SNTApplicationCoreMetricsTest", - ":SNTApplicationTest", - ":SNTDeviceManagerTest", - ":SNTEndpointSecurityManagerTest", + ":SNTCompilerControllerTest", + ":SNTDecisionCacheTest", + ":SNTEndpointSecurityAuthorizerTest", + ":SNTEndpointSecurityClientTest", + ":SNTEndpointSecurityDeviceManagerTest", + ":SNTEndpointSecurityRecorderTest", + ":SNTEndpointSecurityTamperResistanceTest", ":SNTEventTableTest", ":SNTExecutionControllerTest", - ":SNTProtobufEventLogTest", ":SNTRuleTableTest", + ":SantadTest", ], visibility = ["//:santa_package_group"], ) diff --git a/Source/santad/DataLayer/SNTRuleTable.m b/Source/santad/DataLayer/SNTRuleTable.m index a56f0b24b..d2c3d9fc4 100644 --- a/Source/santad/DataLayer/SNTRuleTable.m +++ b/Source/santad/DataLayer/SNTRuleTable.m @@ -137,6 +137,12 @@ - (void)setupSystemCriticalBinaries { NSMutableDictionary *bins = [NSMutableDictionary dictionary]; for (NSString *path in [SNTRuleTable criticalSystemBinaryPaths]) { SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithPath:path]; + if (!binInfo.SHA256) { + // If there isn't a hash, no need to compute the other info here. + // Just continue on to the next binary. + LOGW(@"Unable to compute hash for critical system binary %@.", path); + continue; + } MOLCodesignChecker *csInfo = [binInfo codesignCheckerWithError:NULL]; // Make sure the critical system binary is signed by the same chain as launchd/self @@ -144,7 +150,7 @@ - (void)setupSystemCriticalBinaries { if ([csInfo signingInformationMatches:self.launchdCSInfo]) { systemBin = YES; } else if (![csInfo signingInformationMatches:self.santadCSInfo]) { - LOGE(@"Unable to validate critical system binary %@. " + LOGW(@"Unable to validate critical system binary %@. " @"pid 1: %@, santad: %@ and %@: %@ do not match.", path, self.launchdCSInfo.leafCertificate, self.santadCSInfo.leafCertificate, path, csInfo.leafCertificate); diff --git a/Source/santad/EventProviders/AuthResultCache.h b/Source/santad/EventProviders/AuthResultCache.h new file mode 100644 index 000000000..a80748f89 --- /dev/null +++ b/Source/santad/EventProviders/AuthResultCache.h @@ -0,0 +1,75 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__EVENTPROVIDERS_AUTHRESULTCACHE_H +#define SANTA__SANTAD__EVENTPROVIDERS_AUTHRESULTCACHE_H + +#include +#import +#include +#include +#include + +#import "Source/common/SNTCommon.h" +#include "Source/common/SantaCache.h" +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" + +namespace santa::santad::event_providers { + +enum class FlushCacheMode { + kNonRootOnly, + kAllCaches, +}; + +class AuthResultCache { + public: + // Santa currently only flushes caches when new DENY rules are added, not + // ALLOW rules. This means this value should be low enough so that if a + // previously denied binary is allowed, it can be re-executed by the user in a + // timely manner. But the value should be high enough to allow the cache to be + // effective in the event the binary is executed in rapid succession. + AuthResultCache( + std::shared_ptr esapi, + uint64_t cache_deny_time_ms = 1500); + virtual ~AuthResultCache(); + + AuthResultCache(AuthResultCache &&other) = delete; + AuthResultCache &operator=(AuthResultCache &&rhs) = delete; + AuthResultCache(const AuthResultCache &other) = delete; + AuthResultCache &operator=(const AuthResultCache &other) = delete; + + virtual bool AddToCache(const es_file_t *es_file, santa_action_t decision); + virtual void RemoveFromCache(const es_file_t *es_file); + virtual santa_action_t CheckCache(const es_file_t *es_file); + virtual santa_action_t CheckCache(santa_vnode_id_t vnode_id); + + virtual void FlushCache(FlushCacheMode mode); + + virtual NSArray *CacheCounts(); + + private: + virtual SantaCache *CacheForVnodeID(santa_vnode_id_t vnode_id); + + SantaCache *root_cache_; + SantaCache *nonroot_cache_; + + std::shared_ptr esapi_; + uint64_t root_devno_; + uint64_t cache_deny_time_ns_; + dispatch_queue_t q_; +}; + +} // namespace santa::santad::event_providers + +#endif diff --git a/Source/santad/EventProviders/AuthResultCache.mm b/Source/santad/EventProviders/AuthResultCache.mm new file mode 100644 index 000000000..c947cdcaa --- /dev/null +++ b/Source/santad/EventProviders/AuthResultCache.mm @@ -0,0 +1,156 @@ + +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/EventProviders/AuthResultCache.h" + +#include +#include + +#import "Source/common/SNTLogging.h" +#include "Source/santad/EventProviders/EndpointSecurity/Client.h" + +using santa::santad::event_providers::endpoint_security::Client; +using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI; + +template <> +uint64_t SantaCacheHasher(santa_vnode_id_t const &t) { + return (SantaCacheHasher(t.fsid) << 1) ^ SantaCacheHasher(t.fileid); +} + +namespace santa::santad::event_providers { + +static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) { + return santa_vnode_id_t{ + .fsid = (uint64_t)es_file->stat.st_dev, + .fileid = es_file->stat.st_ino, + }; +} + +static inline uint64_t GetCurrentUptime() { + return clock_gettime_nsec_np(CLOCK_MONOTONIC); +} + +// Decision is stored in upper 8 bits, timestamp in remaining 56. +static inline uint64_t CacheableAction(santa_action_t action, + uint64_t timestamp = GetCurrentUptime()) { + return ((uint64_t)action << 56) | (timestamp & 0xFFFFFFFFFFFFFF); +} + +static inline santa_action_t ActionFromCachedValue(uint64_t cachedValue) { + return (santa_action_t)(cachedValue >> 56); +} + +static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) { + return (cachedValue & ~(0xFF00000000000000)); +} + +AuthResultCache::AuthResultCache(std::shared_ptr esapi, + uint64_t cache_deny_time_ms) + : esapi_(esapi), cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) { + root_cache_ = new SantaCache(); + nonroot_cache_ = new SantaCache(); + + struct stat sb; + if (stat("/", &sb) == 0) { + root_devno_ = sb.st_dev; + } + + q_ = dispatch_queue_create( + "com.google.santa.daemon.auth_result_cache.q", + dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL, + QOS_CLASS_USER_INTERACTIVE, 0)); +} + +AuthResultCache::~AuthResultCache() { + delete root_cache_; + delete nonroot_cache_; +} + +bool AuthResultCache::AddToCache(const es_file_t *es_file, santa_action_t decision) { + santa_vnode_id_t vnode_id = VnodeForFile(es_file); + SantaCache *cache = CacheForVnodeID(vnode_id); + switch (decision) { + case ACTION_REQUEST_BINARY: + return cache->set(vnode_id, CacheableAction(ACTION_REQUEST_BINARY, 0), 0); + case ACTION_RESPOND_ALLOW: OS_FALLTHROUGH; + case ACTION_RESPOND_ALLOW_COMPILER: OS_FALLTHROUGH; + case ACTION_RESPOND_DENY: + return cache->set(vnode_id, CacheableAction(decision), + CacheableAction(ACTION_REQUEST_BINARY, 0)); + default: + // This is a programming error. Bail. + LOGE(@"Invalid cache value, exiting."); + exit(EXIT_FAILURE); + } +} + +void AuthResultCache::RemoveFromCache(const es_file_t *es_file) { + santa_vnode_id_t vnode_id = VnodeForFile(es_file); + CacheForVnodeID(vnode_id)->remove(vnode_id); +} + +santa_action_t AuthResultCache::CheckCache(const es_file_t *es_file) { + return CheckCache(VnodeForFile(es_file)); +} + +santa_action_t AuthResultCache::CheckCache(santa_vnode_id_t vnode_id) { + SantaCache *cache = CacheForVnodeID(vnode_id); + + uint64_t cached_val = cache->get(vnode_id); + if (cached_val == 0) { + return ACTION_UNSET; + } + + santa_action_t result = ActionFromCachedValue(cached_val); + + if (result == ACTION_RESPOND_DENY) { + uint64_t expiry_time = TimestampFromCachedValue(cached_val) + cache_deny_time_ns_; + if (expiry_time < GetCurrentUptime()) { + cache->remove(vnode_id); + return ACTION_UNSET; + } + } + + return result; +} + +SantaCache *AuthResultCache::CacheForVnodeID( + santa_vnode_id_t vnode_id) { + return (vnode_id.fsid == root_devno_ || root_devno_ == 0) ? root_cache_ : nonroot_cache_; +} + +void AuthResultCache::FlushCache(FlushCacheMode mode) { + nonroot_cache_->clear(); + if (mode == FlushCacheMode::kAllCaches) { + root_cache_->clear(); + + // Clear the ES cache when all local caches are flushed. Assume the ES cache + // doesn't need to be cleared when only flushing the non-root cache. + // + // Calling into ES should be done asynchronously since it could otherwise + // potentially deadlock. + auto shared_esapi = esapi_->shared_from_this(); + dispatch_async(q_, ^{ + // ES does not need a connected client to clear cache + shared_esapi->ClearCache(Client()); + }); + } +} + +NSArray *AuthResultCache::CacheCounts() { + return @[ @(root_cache_->count()), @(nonroot_cache_->count()) ]; +} + +} // namespace santa::santad::event_providers diff --git a/Source/santad/EventProviders/AuthResultCacheTest.mm b/Source/santad/EventProviders/AuthResultCacheTest.mm new file mode 100644 index 000000000..a15820682 --- /dev/null +++ b/Source/santad/EventProviders/AuthResultCacheTest.mm @@ -0,0 +1,225 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include +#include +#import +#import +#include +#include +#include + +#include + +#include "Source/common/SNTCommon.h" +#include "Source/common/TestUtils.h" +#include "Source/santad/EventProviders/AuthResultCache.h" +#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h" + +using santa::santad::event_providers::AuthResultCache; +using santa::santad::event_providers::FlushCacheMode; + +// Grab the st_dev number of the root volume to match the root cache +static uint64_t RootDevno() { + static dispatch_once_t once_token; + static uint64_t devno; + dispatch_once(&once_token, ^{ + struct stat sb; + stat("/", &sb); + devno = sb.st_dev; + }); + return devno; +} + +static inline es_file_t MakeCacheableFile(uint64_t devno, uint64_t ino) { + return es_file_t{ + .path = {}, .path_truncated = false, .stat = {.st_dev = (dev_t)devno, .st_ino = ino}}; +} + +static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) { + return santa_vnode_id_t{ + .fsid = (uint64_t)es_file->stat.st_dev, + .fileid = es_file->stat.st_ino, + }; +} + +static inline void AssertCacheCounts(std::shared_ptr cache, uint64_t root_count, + uint64_t nonroot_count) { + NSArray *counts = cache->CacheCounts(); + + XCTAssertNotNil(counts); + XCTAssertEqual([counts count], 2); + XCTAssertNotNil(counts[0]); + XCTAssertNotNil(counts[1]); + XCTAssertEqual([counts[0] unsignedLongLongValue], root_count); + XCTAssertEqual([counts[1] unsignedLongLongValue], nonroot_count); +} + +@interface AuthResultCacheTest : XCTestCase +@end + +@implementation AuthResultCacheTest + +- (void)testEmptyCacheExpectedNumberOfCacheCounts { + auto esapi = std::make_shared(); + auto cache = std::make_shared(esapi); + + AssertCacheCounts(cache, 0, 0); +} + +- (void)testBasicOperation { + auto esapi = std::make_shared(); + auto cache = std::make_shared(esapi); + + es_file_t rootFile = MakeCacheableFile(RootDevno(), 111); + es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 222); + + // Add the root file to the cache + cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY); + + AssertCacheCounts(cache, 1, 0); + XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY); + XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_UNSET); + + // Now add the non-root file + cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY); + + AssertCacheCounts(cache, 1, 1); + XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY); + XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_REQUEST_BINARY); + + // Update the cached values + cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW); + cache->AddToCache(&nonrootFile, ACTION_RESPOND_DENY); + + AssertCacheCounts(cache, 1, 1); + XCTAssertEqual(cache->CheckCache(VnodeForFile(&rootFile)), ACTION_RESPOND_ALLOW); + XCTAssertEqual(cache->CheckCache(VnodeForFile(&nonrootFile)), ACTION_RESPOND_DENY); + + // Remove the root file + cache->RemoveFromCache(&rootFile); + + AssertCacheCounts(cache, 0, 1); + XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET); + XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_RESPOND_DENY); +} + +- (void)testFlushCache { + auto mockESApi = std::make_shared(); + auto cache = std::make_shared(mockESApi); + + es_file_t rootFile = MakeCacheableFile(RootDevno(), 111); + es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 111); + + cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY); + cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY); + + AssertCacheCounts(cache, 1, 1); + + // Flush non-root only + cache->FlushCache(FlushCacheMode::kNonRootOnly); + + AssertCacheCounts(cache, 1, 0); + + // Add back the non-root file + cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY); + + AssertCacheCounts(cache, 1, 1); + + // Flush all caches + // The call to ClearCache is asynchronous. Use a semaphore to + // be notified when the mock is called. + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + EXPECT_CALL(*mockESApi, ClearCache).WillOnce(testing::InvokeWithoutArgs(^() { + dispatch_semaphore_signal(sema); + return true; + })); + cache->FlushCache(FlushCacheMode::kAllCaches); + + XCTAssertEqual(0, + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)), + "ClearCache wasn't called within expected time window"); + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); + + AssertCacheCounts(cache, 0, 0); +} + +- (void)testCacheStateMachine { + auto mockESApi = std::make_shared(); + auto cache = std::make_shared(mockESApi); + + es_file_t rootFile = MakeCacheableFile(RootDevno(), 111); + + // Cached items must first be in the ACTION_REQUEST_BINARY state + XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW)); + XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW_COMPILER)); + XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY)); + XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET); + + XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY)); + XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY); + + // Items in the `ACTION_REQUEST_BINARY` state cannot reenter the same state + XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY)); + XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY); + + santa_action_t allowed_transitions[] = { + ACTION_RESPOND_ALLOW, + ACTION_RESPOND_ALLOW_COMPILER, + ACTION_RESPOND_DENY, + }; + + for (size_t i = 0; i < sizeof(allowed_transitions) / sizeof(allowed_transitions[0]); i++) { + // First make sure the item doesn't exist + cache->RemoveFromCache(&rootFile); + XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET); + + // Now add the item to be in the first allowed state + XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY)); + XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY); + + // Now assert the allowed transition + XCTAssertTrue(cache->AddToCache(&rootFile, allowed_transitions[i])); + XCTAssertEqual(cache->CheckCache(&rootFile), allowed_transitions[i]); + } +} + +- (void)testCacheExpiry { + auto mockESApi = std::make_shared(); + // Create a cache with a lowered cache expiry value + uint64_t expiryMS = 250; + auto cache = std::make_shared(mockESApi, expiryMS); + + es_file_t rootFile = MakeCacheableFile(RootDevno(), 111); + + // Add a file to the cache and put into the ACTION_RESPOND_DENY state + XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY)); + XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY)); + + // Ensure the file exists + XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_RESPOND_DENY); + + // Wait for the item to expire + SleepMS(expiryMS); + + // Check cache counts to make sure the item still exists + AssertCacheCounts(cache, 1, 0); + + // Now check the cache, which will remove the item + XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET); + AssertCacheCounts(cache, 0, 0); +} + +@end diff --git a/Source/santad/EventProviders/DiskArbitrationTestUtil.h b/Source/santad/EventProviders/DiskArbitrationTestUtil.h index 2083925bd..a01430258 100644 --- a/Source/santad/EventProviders/DiskArbitrationTestUtil.h +++ b/Source/santad/EventProviders/DiskArbitrationTestUtil.h @@ -1,4 +1,4 @@ -/// Copyright 2021 Google Inc. All rights reserved. +/// Copyright 2021-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -50,7 +50,8 @@ typedef void (^MockDADiskAppearedCallback)(DADiskRef ref); @end // -// All DiskArbitration functions used in SNTDeviceManager and shimmed out accordingly. +// All DiskArbitration functions used in SNTEndpointSecurityDeviceManager +// and shimmed out accordingly. // CF_EXTERN_C_BEGIN diff --git a/Source/santad/EventProviders/EndpointSecurity/Client.h b/Source/santad/EventProviders/EndpointSecurity/Client.h new file mode 100644 index 000000000..798cab149 --- /dev/null +++ b/Source/santad/EventProviders/EndpointSecurity/Client.h @@ -0,0 +1,69 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_CLIENT_H +#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_CLIENT_H + +#import + +#include + +namespace santa::santad::event_providers::endpoint_security { + +class Client { + public: + explicit Client(es_client_t* client, es_new_client_result_t result) + : client_(client), result_(result) {} + + Client() : client_(nullptr), result_(ES_NEW_CLIENT_RESULT_ERR_INTERNAL) {} + + virtual ~Client() { + if (client_) { + // Special case: Not using EndpointSecurityAPI here due to circular refs. + es_delete_client(client_); + } + } + + Client(Client&& other) { + client_ = other.client_; + result_ = other.result_; + other.client_ = nullptr; + other.result_ = ES_NEW_CLIENT_RESULT_ERR_INTERNAL; + } + + Client& operator=(Client&& rhs) { + client_ = rhs.client_; + result_ = rhs.result_; + rhs.client_ = nullptr; + rhs.result_ = ES_NEW_CLIENT_RESULT_ERR_INTERNAL; + return *this; + } + + Client(const Client& other) = delete; + void operator=(const Client& rhs) = delete; + + inline bool IsConnected() { return result_ == ES_NEW_CLIENT_RESULT_SUCCESS; } + + inline es_new_client_result_t NewClientResult() { return result_; } + + inline es_client_t* Get() const { return client_; } + + private: + es_client_t* client_; + es_new_client_result_t result_; +}; + +} // namespace santa::santad::event_providers::endpoint_security + +#endif diff --git a/Source/santad/EventProviders/EndpointSecurity/ClientTest.mm b/Source/santad/EventProviders/EndpointSecurity/ClientTest.mm new file mode 100644 index 000000000..0d14ee305 --- /dev/null +++ b/Source/santad/EventProviders/EndpointSecurity/ClientTest.mm @@ -0,0 +1,118 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import +#import +#include + +#include "Source/santad/EventProviders/EndpointSecurity/Client.h" + +using santa::santad::event_providers::endpoint_security::Client; + +// Global semaphore used for custom `es_delete_client` function +dispatch_semaphore_t gSema; + +// Note: The Client class does not use the `EndpointSecurityAPI` wrappers due +// to circular dependency issues. It is a special case that uses the underlying +// ES API `es_delete_client` directly. This test override will signal the +// `gSema` semaphore to indicate it has been called. +es_return_t es_delete_client(es_client_t *_Nullable client) { + dispatch_semaphore_signal(gSema); + return ES_RETURN_SUCCESS; +}; + +@interface ClientTest : XCTestCase +@end + +@implementation ClientTest + +- (void)setUp { + gSema = dispatch_semaphore_create(0); +} + +- (void)testConstructorsAndDestructors { + // Ensure constructors set internal state properly + // Anonymous scopes used to ensure destructors called as expected + + // Null `es_client_t*` *shouldn't* trigger `es_delete_client` + { + Client c; + XCTAssertEqual(c.Get(), nullptr); + XCTAssertEqual(c.NewClientResult(), ES_NEW_CLIENT_RESULT_ERR_INTERNAL); + } + + XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW), + "es_delete_client called unexpectedly"); + + // Nonnull `es_client_t*` *should* trigger `es_delete_client` + { + int fake; + es_client_t *fakeClient = (es_client_t *)&fake; + Client c(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS); + XCTAssertEqual(c.Get(), fakeClient); + XCTAssertEqual(c.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS); + } + + XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW), + "es_delete_client not called within expected time window"); + + // Test move constructor + { + int fake; + es_client_t *fakeClient = (es_client_t *)&fake; + Client c1(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS); + + Client c2(std::move(c1)); + + XCTAssertEqual(c1.Get(), nullptr); + XCTAssertEqual(c2.Get(), fakeClient); + XCTAssertEqual(c2.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS); + } + + // Ensure `es_delete_client` was only called once when both `c1` and `c2` + // are destructed. + XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW), + "es_delete_client not called within expected time window"); + XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW), + "es_delete_client called unexpectedly"); + + // Test move assignment + { + int fake; + es_client_t *fakeClient = (es_client_t *)&fake; + Client c1(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS); + Client c2; + + c2 = std::move(c1); + + XCTAssertEqual(c1.Get(), nullptr); + XCTAssertEqual(c2.Get(), fakeClient); + XCTAssertEqual(c2.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS); + } + + // Ensure `es_delete_client` was only called once when both `c1` and `c2` + // are destructed. + XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW), + "es_delete_client not called within expected time window"); + XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW), + "es_delete_client called unexpectedly"); +} + +- (void)testIsConnected { + XCTAssertFalse(Client().IsConnected()); + XCTAssertFalse(Client(nullptr, ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED).IsConnected()); + XCTAssertTrue(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS).IsConnected()); +} + +@end diff --git a/Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h b/Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h new file mode 100644 index 000000000..60f58dd6c --- /dev/null +++ b/Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h @@ -0,0 +1,53 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENDPOINTSECURITYAPI_H +#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENDPOINTSECURITYAPI_H + +#include + +#include + +#include "Source/santad/EventProviders/EndpointSecurity/Client.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" + +namespace santa::santad::event_providers::endpoint_security { + +class EndpointSecurityAPI : public std::enable_shared_from_this { + public: + virtual ~EndpointSecurityAPI() = default; + + virtual Client NewClient(void (^message_handler)(es_client_t *, Message)); + + virtual bool Subscribe(const Client &client, const std::set &); + + virtual es_message_t *RetainMessage(const es_message_t *msg); + virtual void ReleaseMessage(es_message_t *msg); + + virtual bool RespondAuthResult(const Client &client, const Message &msg, es_auth_result_t result, + bool cache); + + virtual bool MuteProcess(const Client &client, const audit_token_t *tok); + + virtual bool ClearCache(const Client &client); + + virtual uint32_t ExecArgCount(const es_event_exec_t *event); + virtual es_string_token_t ExecArg(const es_event_exec_t *event, uint32_t index); + + private: +}; + +} // namespace santa::santad::event_providers::endpoint_security + +#endif diff --git a/Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.mm b/Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.mm new file mode 100644 index 000000000..05fcc1000 --- /dev/null +++ b/Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.mm @@ -0,0 +1,87 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" +#include + +#include +#include + +namespace santa::santad::event_providers::endpoint_security { + +Client EndpointSecurityAPI::NewClient(void (^message_handler)(es_client_t *, Message)) { + es_client_t *client = NULL; + + auto shared_esapi = shared_from_this(); + es_new_client_result_t res = es_new_client(&client, ^(es_client_t *c, const es_message_t *msg) { + @autoreleasepool { + message_handler(c, Message(shared_esapi, msg)); + } + }); + + return Client(client, res); +} + +es_message_t *EndpointSecurityAPI::RetainMessage(const es_message_t *msg) { + if (@available(macOS 11.0, *)) { + es_retain_message(msg); + es_message_t *nonconst = const_cast(msg); + return nonconst; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return es_copy_message(msg); +#pragma clang diagnostic pop + } +} + +void EndpointSecurityAPI::ReleaseMessage(es_message_t *msg) { + if (@available(macOS 11.0, *)) { + es_release_message(msg); + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return es_free_message(msg); +#pragma clang diagnostic pop + } +} + +bool EndpointSecurityAPI::Subscribe(const Client &client, + const std::set &event_types) { + std::vector subs(event_types.begin(), event_types.end()); + return es_subscribe(client.Get(), subs.data(), (uint32_t)subs.size()) == ES_RETURN_SUCCESS; +} + +bool EndpointSecurityAPI::RespondAuthResult(const Client &client, const Message &msg, + es_auth_result_t result, bool cache) { + return es_respond_auth_result(client.Get(), &(*msg), result, cache) == ES_RESPOND_RESULT_SUCCESS; +} + +bool EndpointSecurityAPI::MuteProcess(const Client &client, const audit_token_t *tok) { + return es_mute_process(client.Get(), tok) == ES_RETURN_SUCCESS; +} + +bool EndpointSecurityAPI::ClearCache(const Client &client) { + return es_clear_cache(client.Get()) == ES_CLEAR_CACHE_RESULT_SUCCESS; +} + +uint32_t EndpointSecurityAPI::ExecArgCount(const es_event_exec_t *event) { + return es_exec_arg_count(event); +} + +es_string_token_t EndpointSecurityAPI::ExecArg(const es_event_exec_t *event, uint32_t index) { + return es_exec_arg(event, index); +} + +} // namespace santa::santad::event_providers::endpoint_security diff --git a/Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h b/Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h new file mode 100644 index 000000000..c7838e542 --- /dev/null +++ b/Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h @@ -0,0 +1,214 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +/// This file groups all of the enriched message types - that is the +/// objects that are constructed to hold all enriched event data prior +/// to being logged. + +#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHEDTYPES_H +#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHEDTYPES_H + +#include +#include + +#include +#include +#include + +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" + +namespace santa::santad::event_providers::endpoint_security { + +class EnrichedFile { + public: + EnrichedFile(std::optional> &&user, + std::optional> &&group, + std::optional> &&hash) + : user_(std::move(user)), + group_(std::move(group)), + hash_(std::move(hash)) {} + + private: + std::optional> user_; + std::optional> group_; + std::optional> hash_; +}; + +class EnrichedProcess { + public: + EnrichedProcess(std::optional> &&effective_user, + std::optional> &&effective_group, + std::optional> &&real_user, + std::optional> &&real_group, + EnrichedFile &&executable) + : effective_user_(std::move(effective_user)), + effective_group_(std::move(effective_group)), + real_user_(std::move(real_user)), + real_group_(std::move(real_group)), + executable_(std::move(executable)) {} + + const std::optional> &real_user() const { + return real_user_; + } + const std::optional> &real_group() const { + return real_group_; + } + + private: + std::optional> effective_user_; + std::optional> effective_group_; + std::optional> real_user_; + std::optional> real_group_; + EnrichedFile executable_; +}; + +class EnrichedEventType { + public: + EnrichedEventType(Message &&es_msg, EnrichedProcess &&instigator) + : es_msg_(std::move(es_msg)), instigator_(std::move(instigator)) {} + + EnrichedEventType(EnrichedEventType &&other) + : es_msg_(std::move(other.es_msg_)), + instigator_(std::move(other.instigator_)) {} + + virtual ~EnrichedEventType() = default; + + const es_message_t &es_msg() const { return *es_msg_; } + + const EnrichedProcess &instigator() const { return instigator_; } + + private: + Message es_msg_; + EnrichedProcess instigator_; +}; + +class EnrichedClose : public EnrichedEventType { + public: + EnrichedClose(Message &&es_msg, EnrichedProcess &&instigator, + EnrichedFile &&target) + : EnrichedEventType(std::move(es_msg), std::move(instigator)), + target_(std::move(target)) {} + + private: + EnrichedFile target_; +}; + +class EnrichedExchange : public EnrichedEventType { + public: + EnrichedExchange(Message &&es_msg, EnrichedProcess &&instigator, + EnrichedFile &&file1, EnrichedFile &&file2) + : EnrichedEventType(std::move(es_msg), std::move(instigator)), + file1_(std::move(file1)), + file2_(std::move(file2)) {} + + private: + EnrichedFile file1_; + EnrichedFile file2_; +}; + +class EnrichedExec : public EnrichedEventType { + public: + EnrichedExec(Message &&es_msg, EnrichedProcess &&instigator, + EnrichedProcess &&target, std::optional &&script, + std::optional working_dir) + : EnrichedEventType(std::move(es_msg), std::move(instigator)), + target_(std::move(target)), + script_(std::move(script)), + working_dir_(std::move(working_dir)) {} + + private: + EnrichedProcess target_; + std::optional script_; + std::optional working_dir_; +}; + +class EnrichedExit : public EnrichedEventType { + public: + EnrichedExit(Message &&es_msg, EnrichedProcess &&instigator) + : EnrichedEventType(std::move(es_msg), std::move(instigator)) {} +}; + +class EnrichedFork : public EnrichedEventType { + public: + EnrichedFork(Message &&es_msg, EnrichedProcess &&instigator, + EnrichedProcess &&target) + : EnrichedEventType(std::move(es_msg), std::move(instigator)), + target_(std::move(target)) {} + + private: + EnrichedProcess target_; +}; + +class EnrichedLink : public EnrichedEventType { + public: + EnrichedLink(Message &&es_msg, EnrichedProcess &&instigator, + EnrichedFile &&source, EnrichedFile &&target_dir) + : EnrichedEventType(std::move(es_msg), std::move(instigator)), + source_(std::move(source)), + target_dir_(std::move(target_dir)) {} + + private: + EnrichedFile source_; + EnrichedFile target_dir_; +}; + +class EnrichedRename : public EnrichedEventType { + public: + EnrichedRename(Message &&es_msg, EnrichedProcess &&instigator, + EnrichedFile &&source, std::optional &&target, + std::optional &&target_dir) + : EnrichedEventType(std::move(es_msg), std::move(instigator)), + source_(std::move(source)), + target_(std::move(target)), + target_dir_(std::move(target_dir)) {} + + private: + EnrichedFile source_; + std::optional target_; + std::optional target_dir_; +}; + +class EnrichedUnlink : public EnrichedEventType { + public: + EnrichedUnlink(Message &&es_msg, EnrichedProcess &&instigator, + EnrichedFile &&target) + : EnrichedEventType(std::move(es_msg), std::move(instigator)), + target_(std::move(target)) {} + + private: + EnrichedFile target_; +}; + +using EnrichedType = + std::variant; + +class EnrichedMessage { + public: + EnrichedMessage(EnrichedType &&msg) : msg_(std::move(msg)) { + uuid_generate(uuid_); + clock_gettime(CLOCK_REALTIME, &enrichment_time_); + } + + const EnrichedType &GetEnrichedMessage() { return msg_; } + + private: + uuid_t uuid_; + struct timespec enrichment_time_; + EnrichedType msg_; +}; + +} // namespace santa::santad::event_providers::endpoint_security + +#endif diff --git a/Source/santad/EventProviders/EndpointSecurity/Enricher.h b/Source/santad/EventProviders/EndpointSecurity/Enricher.h new file mode 100644 index 000000000..082b9a18d --- /dev/null +++ b/Source/santad/EventProviders/EndpointSecurity/Enricher.h @@ -0,0 +1,44 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHER_H +#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHER_H + +#include + +#include "Source/common/SantaCache.h" +#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" + +namespace santa::santad::event_providers::endpoint_security { + +class Enricher { + public: + Enricher(); + virtual ~Enricher() = default; + virtual std::shared_ptr Enrich(Message &&msg); + virtual EnrichedProcess Enrich(const es_process_t &es_proc); + virtual EnrichedFile Enrich(const es_file_t &es_file); + + virtual std::optional> UsernameForUID(uid_t uid); + virtual std::optional> UsernameForGID(gid_t gid); + + private: + SantaCache>> + username_cache_; + SantaCache>> + groupname_cache_; +}; + +} // namespace santa::santad::event_providers::endpoint_security + +#endif diff --git a/Source/santad/EventProviders/EndpointSecurity/Enricher.mm b/Source/santad/EventProviders/EndpointSecurity/Enricher.mm new file mode 100644 index 000000000..c09a97afb --- /dev/null +++ b/Source/santad/EventProviders/EndpointSecurity/Enricher.mm @@ -0,0 +1,137 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "Source/common/SNTLogging.h" +#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" + +namespace santa::santad::event_providers::endpoint_security { + +Enricher::Enricher() : username_cache_(256), groupname_cache_(256) {} + +std::shared_ptr Enricher::Enrich(Message &&es_msg) { + // TODO(mlw): Consider potential design patterns that could help reduce memory usage under load + // (such as maybe the flyweight pattern) + switch (es_msg->event_type) { + case ES_EVENT_TYPE_NOTIFY_CLOSE: + return std::make_shared(EnrichedClose( + std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.close.target))); + case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA: + return std::make_shared(EnrichedExchange( + std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exchangedata.file1), + Enrich(*es_msg->event.exchangedata.file2))); + case ES_EVENT_TYPE_NOTIFY_EXEC: + return std::make_shared(EnrichedExec( + std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exec.target), + (es_msg->version >= 2 && es_msg->event.exec.script) + ? std::make_optional(Enrich(*es_msg->event.exec.script)) + : std::nullopt, + (es_msg->version >= 3) ? std::make_optional(Enrich(*es_msg->event.exec.cwd)) + : std::nullopt)); + case ES_EVENT_TYPE_NOTIFY_FORK: + return std::make_shared(EnrichedFork( + std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.fork.child))); + case ES_EVENT_TYPE_NOTIFY_EXIT: + return std::make_shared( + EnrichedExit(std::move(es_msg), Enrich(*es_msg->process))); + case ES_EVENT_TYPE_NOTIFY_LINK: + return std::make_shared( + EnrichedLink(std::move(es_msg), Enrich(*es_msg->process), + Enrich(*es_msg->event.link.source), Enrich(*es_msg->event.link.target_dir))); + case ES_EVENT_TYPE_NOTIFY_RENAME: { + if (es_msg->event.rename.destination_type == ES_DESTINATION_TYPE_NEW_PATH) { + return std::make_shared(EnrichedRename( + std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source), + std::nullopt, Enrich(*es_msg->event.rename.destination.new_path.dir))); + } else { + return std::make_shared(EnrichedRename( + std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source), + Enrich(*es_msg->event.rename.destination.existing_file), std::nullopt)); + } + } + case ES_EVENT_TYPE_NOTIFY_UNLINK: + return std::make_shared(EnrichedUnlink( + std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.unlink.target))); + default: + // This is a programming error + LOGE(@"Attempting to enrich an unhandled event type: %d", es_msg->event_type); + exit(EXIT_FAILURE); + } +} + +EnrichedProcess Enricher::Enrich(const es_process_t &es_proc) { + return EnrichedProcess(UsernameForUID(audit_token_to_euid(es_proc.audit_token)), + UsernameForGID(audit_token_to_egid(es_proc.audit_token)), + UsernameForUID(audit_token_to_ruid(es_proc.audit_token)), + UsernameForGID(audit_token_to_rgid(es_proc.audit_token)), + Enrich(*es_proc.executable)); +} + +EnrichedFile Enricher::Enrich(const es_file_t &es_file) { + // TODO(mlw): Consider having the enricher perform file hashing. This will + // make more sense if we start including hashes in more event types. + return EnrichedFile(UsernameForUID(es_file.stat.st_uid), UsernameForGID(es_file.stat.st_gid), + std::nullopt); +} + +std::optional> Enricher::UsernameForUID(uid_t uid) { + std::optional> username = username_cache_.get(uid); + + if (username.has_value()) { + return username; + } else { + struct passwd *pw = getpwuid(uid); + if (pw) { + username = std::make_shared(pw->pw_name); + } else { + username = std::nullopt; + } + + username_cache_.set(uid, username); + + return username; + } +} + +std::optional> Enricher::UsernameForGID(gid_t gid) { + std::optional> groupname = groupname_cache_.get(gid); + + if (groupname.has_value()) { + return groupname; + } else { + struct group *gr = getgrgid(gid); + if (gr) { + groupname = std::make_shared(gr->gr_name); + } else { + groupname = std::nullopt; + } + + groupname_cache_.set(gid, groupname); + + return groupname; + } +} + +} // namespace santa::santad::event_providers::endpoint_security diff --git a/Source/santad/EventProviders/EndpointSecurity/EnricherTest.mm b/Source/santad/EventProviders/EndpointSecurity/EnricherTest.mm new file mode 100644 index 000000000..97a2b8b23 --- /dev/null +++ b/Source/santad/EventProviders/EndpointSecurity/EnricherTest.mm @@ -0,0 +1,49 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import +#import + +#include "Source/common/TestUtils.h" +#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h" + +using santa::santad::event_providers::endpoint_security::Enricher; + +@interface EnricherTest : XCTestCase +@end + +@implementation EnricherTest + +- (void)testUidGid { + Enricher enricher; + + std::optional> user = enricher.UsernameForUID(NOBODY_UID); + XCTAssertTrue(user.has_value()); + XCTAssertEqual(strcmp(user->get()->c_str(), "nobody"), 0); + + std::optional> group = enricher.UsernameForGID(NOBODY_GID); + XCTAssertTrue(group.has_value()); + XCTAssertEqual(strcmp(group->get()->c_str(), "nobody"), 0); + + uid_t invalidUID = (uid_t)-123; + gid_t invalidGID = (gid_t)-123; + + std::optional> invalidUser = enricher.UsernameForUID(invalidUID); + XCTAssertFalse(invalidUser.has_value()); + + std::optional> invalidGroup = enricher.UsernameForGID(invalidGID); + XCTAssertFalse(invalidGroup.has_value()); +} + +@end diff --git a/Source/santad/EventProviders/EndpointSecurity/Message.h b/Source/santad/EventProviders/EndpointSecurity/Message.h new file mode 100644 index 000000000..c68c1f9ef --- /dev/null +++ b/Source/santad/EventProviders/EndpointSecurity/Message.h @@ -0,0 +1,60 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MESSAGE_H +#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MESSAGE_H + +#import + +#include +#include + +namespace santa::santad::event_providers::endpoint_security { + +class EndpointSecurityAPI; + +class Message { + public: + Message(std::shared_ptr esapi, + const es_message_t* es_msg); + ~Message(); + + Message(Message&& other); + // Note: Safe to implement this, just not currently needed so left deleted. + Message& operator=(Message&& rhs) = delete; + + // In macOS 10.15, es_retain_message/es_release_message were unsupported + // and required a full copy, which impacts performance if done too much... + Message(const Message& other); + Message& operator=(const Message& other) = delete; + + // Operators to access underlying es_message_t + const es_message_t* operator->() const { return es_msg_; } + const es_message_t& operator*() const { return *es_msg_; } + + std::string ParentProcessName() const; + + private: + std::shared_ptr esapi_; + es_message_t* es_msg_; + + mutable std::string pname_; + mutable std::string parent_pname_; + + std::string GetProcessName(pid_t pid) const; +}; + +} // namespace santa::santad::event_providers::endpoint_security + +#endif diff --git a/Source/santad/EventProviders/EndpointSecurity/Message.mm b/Source/santad/EventProviders/EndpointSecurity/Message.mm new file mode 100644 index 000000000..2610672d9 --- /dev/null +++ b/Source/santad/EventProviders/EndpointSecurity/Message.mm @@ -0,0 +1,65 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" + +#include +#include + +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" + +namespace santa::santad::event_providers::endpoint_security { + +Message::Message(std::shared_ptr esapi, const es_message_t *es_msg) + : esapi_(esapi) { + es_msg_ = esapi_->RetainMessage(es_msg); +} + +Message::~Message() { + if (es_msg_) { + esapi_->ReleaseMessage(es_msg_); + } +} + +Message::Message(Message &&other) { + esapi_ = std::move(other.esapi_); + es_msg_ = other.es_msg_; + other.es_msg_ = nullptr; +} + +Message::Message(const Message &other) { + esapi_ = other.esapi_; + es_msg_ = other.es_msg_; + esapi_->RetainMessage(es_msg_); +} + +std::string Message::ParentProcessName() const { + if (parent_pname_.length() == 0) { + parent_pname_ = GetProcessName(es_msg_->process->ppid); + } + return parent_pname_; +} + +std::string Message::GetProcessName(pid_t pid) const { + // Note: proc_name() accesses the `pbi_name` field of `struct proc_bsdinfo`. The size of `pname` + // here is meant to match the size of `pbi_name`, and one extra byte ensure zero-terminated. + char pname[MAXCOMLEN * 2 + 1] = {}; + if (proc_name(pid, pname, sizeof(pname)) > 0) { + return std::string(pname); + } else { + return std::string(""); + } +} + +} // namespace santa::santad::event_providers::endpoint_security diff --git a/Source/santad/EventProviders/EndpointSecurity/MessageTest.mm b/Source/santad/EventProviders/EndpointSecurity/MessageTest.mm new file mode 100644 index 000000000..46fbdc3c0 --- /dev/null +++ b/Source/santad/EventProviders/EndpointSecurity/MessageTest.mm @@ -0,0 +1,135 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import +#import +#include +#include +#include +#include + +#include "Source/common/TestUtils.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" +#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h" + +using santa::santad::event_providers::endpoint_security::Message; + +bool IsPidInUse(pid_t pid) { + char pname[MAXCOMLEN * 2 + 1] = {}; + errno = 0; + if (proc_name(pid, pname, sizeof(pname)) <= 0 && errno == ESRCH) { + return false; + } + + // The PID may or may not actually be in use, but assume it is + return true; +} + +// Try to find an unused PID by looking for libproc returning ESRCH errno. +// Start searching backwards from PID_MAX to increase likelyhood that the +// returned PID will still be unused by the time it's being used. +// TODO(mlw): Alternatively, we could inject the `proc_name` function into +// the `Message` object to remove the guesswork here. +pid_t AttemptToFindUnusedPID() { + for (pid_t pid = 99999 /* PID_MAX */; pid > 1; pid--) { + if (!IsPidInUse(pid)) { + return pid; + } + } + + return 0; +} + +@interface MessageTest : XCTestCase +@end + +@implementation MessageTest + +- (void)setUp { +} + +- (void)testConstructorsAndDestructors { + es_file_t procFile = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78)); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc); + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + // Constructing a `Message` retains the underlying `es_message_t` and it is + // released when the `Message` object is destructed. + { Message m(mockESApi, &esMsg); } + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testCopyConstructor { + es_file_t procFile = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78)); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc); + + auto mockESApi = std::make_shared(); + EXPECT_CALL(*mockESApi, ReleaseMessage(testing::_)) + .Times(2) + .After(EXPECT_CALL(*mockESApi, RetainMessage(testing::_)) + .Times(2) + .WillRepeatedly(testing::Return(&esMsg))); + + { + Message msg1(mockESApi, &esMsg); + Message msg2(msg1); + + // Both messages should now point to the same `es_message_t` + XCTAssertEqual(msg1.operator->(), &esMsg); + XCTAssertEqual(msg2.operator->(), &esMsg); + } + + // Ensure the retain/release mocks were called the expected number of times + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testGetParentProcessName { + // Construct a message where the parent pid is ourself + es_file_t procFile = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(getpid(), 0)); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc); + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + // Search for an *existing* parent process. + { + Message msg(mockESApi, &esMsg); + + std::string got = msg.ParentProcessName(); + std::string want = getprogname(); + + XCTAssertCppStringEqual(got, want); + } + + // Search for a *non-existent* parent process. + { + pid_t newPpid = AttemptToFindUnusedPID(); + proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(newPpid, 34)); + + Message msg(mockESApi, &esMsg); + + std::string got = msg.ParentProcessName(); + std::string want = ""; + + XCTAssertCppStringEqual(got, want); + } +} + +@end diff --git a/Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h b/Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h new file mode 100644 index 000000000..353d6cdc5 --- /dev/null +++ b/Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h @@ -0,0 +1,72 @@ +/// Copyright 2021 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MOCKENDPOINTSECURITYAPI_H +#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MOCKENDPOINTSECURITYAPI_H + +#include +#include +#include + +#include "Source/santad/EventProviders/EndpointSecurity/Client.h" +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" + +using santa::santad::event_providers::endpoint_security::Client; + +class MockEndpointSecurityAPI + : public santa::santad::event_providers::endpoint_security::EndpointSecurityAPI { + public: + MOCK_METHOD(santa::santad::event_providers::endpoint_security::Client, NewClient, + (void (^message_handler)( + es_client_t *, santa::santad::event_providers::endpoint_security::Message))); + + MOCK_METHOD(bool, Subscribe, + (const santa::santad::event_providers::endpoint_security::Client &, + const std::set &)); + + MOCK_METHOD(es_message_t *, RetainMessage, (const es_message_t *msg)); + MOCK_METHOD(void, ReleaseMessage, (es_message_t * msg)); + + MOCK_METHOD(bool, RespondAuthResult, + (const santa::santad::event_providers::endpoint_security::Client &, + const santa::santad::event_providers::endpoint_security::Message &msg, + es_auth_result_t result, bool cache)); + + MOCK_METHOD(bool, MuteProcess, + (const santa::santad::event_providers::endpoint_security::Client &, + const audit_token_t *tok)); + + MOCK_METHOD(bool, ClearCache, + (const santa::santad::event_providers::endpoint_security::Client &)); + + MOCK_METHOD(uint32_t, ExecArgCount, (const es_event_exec_t *event)); + MOCK_METHOD(es_string_token_t, ExecArg, (const es_event_exec_t *event, uint32_t index)); + + void SetExpectationsESNewClient() { + EXPECT_CALL(*this, NewClient) + .WillOnce(testing::Return(santa::santad::event_providers::endpoint_security::Client( + nullptr, ES_NEW_CLIENT_RESULT_SUCCESS))); + EXPECT_CALL(*this, MuteProcess).WillOnce(testing::Return(true)); + EXPECT_CALL(*this, ClearCache).WillRepeatedly(testing::Return(true)); + EXPECT_CALL(*this, Subscribe).WillRepeatedly(testing::Return(true)); + } + + void SetExpectationsRetainReleaseMessage(es_message_t *msg) { + EXPECT_CALL(*this, ReleaseMessage).Times(testing::AnyNumber()); + EXPECT_CALL(*this, RetainMessage).WillRepeatedly(testing::Return(msg)); + } +}; + +#endif diff --git a/Source/santad/EventProviders/EndpointSecurityTestUtil.h b/Source/santad/EventProviders/EndpointSecurityTestUtil.h deleted file mode 100644 index b7d5336c5..000000000 --- a/Source/santad/EventProviders/EndpointSecurityTestUtil.h +++ /dev/null @@ -1,104 +0,0 @@ -/// Copyright 2021 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#include -#include -#include - -CF_EXTERN_C_BEGIN -es_string_token_t MakeStringToken(const NSString *_Nonnull s); - -es_file_t MakeESFile(const char *_Nonnull path); -es_process_t MakeESProcess(es_file_t *_Nonnull esFile); -es_message_t MakeESMessage(es_event_type_t eventType, es_process_t *_Nonnull instigator, - struct timespec ts); -CF_EXTERN_C_END - -@class ESMessage; -typedef void (^ESMessageBuilderBlock)(ESMessage *_Nonnull builder); - -// An ObjC builder wrapper around es_message_t -@interface ESMessage : NSObject -@property(nonatomic, readwrite, strong) NSString *_Nullable binaryPath; -@property(nonatomic, readwrite) es_file_t *_Nonnull executable; -@property(nonatomic, readwrite) es_process_t *_Nonnull process; -@property(nonatomic, readwrite) es_message_t *_Nonnull message; -@property(nonatomic, readonly) pid_t pid; - -- (instancetype _Nonnull)initWithBlock:(ESMessageBuilderBlock _Nullable)block - NS_DESIGNATED_INITIALIZER; -@end - -@interface ESResponse : NSObject -@property(nonatomic) es_auth_result_t result; -@property(nonatomic) bool shouldCache; -@end - -typedef void (^ESCallback)(ESResponse *_Nonnull); - -// Singleton wrapper around all of the kernel-level EndpointSecurity framework functions. -@interface MockEndpointSecurity : NSObject -@property NSMutableArray *_Nonnull subscriptions; -- (void)reset; -- (void)registerResponseCallback:(es_event_type_t)t withCallback:(ESCallback _Nonnull)callback; -- (void)triggerHandler:(es_message_t *_Nonnull)msg; - -/// Retrieve an initialized singleton MockEndpointSecurity object -+ (instancetype _Nonnull)mockEndpointSecurity; -@end - -API_UNAVAILABLE(ios, tvos, watchos) -es_message_t *_Nullable es_copy_message(const es_message_t *_Nonnull msg); - -API_UNAVAILABLE(ios, tvos, watchos) -void es_free_message(es_message_t *_Nonnull msg); - -API_AVAILABLE(macos(10.15)) -API_UNAVAILABLE(ios, tvos, watchos) -es_new_client_result_t es_new_client(es_client_t *_Nullable *_Nonnull client, - es_handler_block_t _Nonnull handler); - -API_AVAILABLE(macos(10.15)) API_UNAVAILABLE(ios, tvos, watchos) -es_return_t es_mute_process(es_client_t * _Nonnull client, - const audit_token_t * _Nonnull audit_token); - -#if defined(MAC_OS_VERSION_12_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0 -API_AVAILABLE(macos(12.0)) -API_UNAVAILABLE(ios, tvos, watchos) -es_return_t es_muted_paths_events(es_client_t *_Nonnull client, - es_muted_paths_t *_Nonnull *_Nullable muted_paths); - -API_AVAILABLE(macos(12.0)) -API_UNAVAILABLE(ios, tvos, watchos) -void es_release_muted_paths(es_muted_paths_t *_Nonnull muted_paths); -#endif - -API_AVAILABLE(macos(10.15)) -API_UNAVAILABLE(ios, tvos, watchos) -es_respond_result_t es_respond_auth_result(es_client_t *_Nonnull client, - const es_message_t *_Nonnull message, - es_auth_result_t result, bool cache); - -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 deleted file mode 100644 index e7d804dac..000000000 --- a/Source/santad/EventProviders/EndpointSecurityTestUtil.mm +++ /dev/null @@ -1,367 +0,0 @@ -/// Copyright 2021 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -#import - -#include -#include -#include - -#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h" - -CF_EXTERN_C_BEGIN -es_string_token_t MakeStringToken(const NSString *_Nonnull s) { - return (es_string_token_t){ - .length = [s length], - .data = [s UTF8String], - }; -} - -es_file_t MakeESFile(const char *path) { - es_file_t esFile = {}; - - esFile.path.data = path; - esFile.path.length = strlen(path); - esFile.path_truncated = false; - - // Note: stat info is currently unused / not populated - - return esFile; -} - -es_process_t MakeESProcess(es_file_t *esFile) { - es_process_t esProc = {}; - esProc.executable = esFile; - return esProc; -} - -es_message_t MakeESMessage(es_event_type_t eventType, es_process_t *instigator, - struct timespec ts) { - es_message_t esMsg = {}; - - esMsg.time = ts; - esMsg.event_type = eventType; - esMsg.process = instigator; - - return esMsg; -} - -CF_EXTERN_C_END - -@implementation ESMessage -- (instancetype)init { - return [self initWithBlock:nil]; -} - -- (instancetype)initWithBlock:(ESMessageBuilderBlock)block { - NSParameterAssert(block); - - self = [super init]; - if (self) { - _pid = arc4random(); - [self initBaseObjects]; - block(self); - [self fillLinks]; - } - return self; -} - -- (void)initBaseObjects { - self.executable = static_cast(calloc(1, sizeof(es_file_t))); - self.process = static_cast(calloc(1, sizeof(es_process_t))); - - self.process->ppid = self.pid; - self.process->original_ppid = self.pid; - self.process->group_id = static_cast(arc4random()); - self.process->session_id = static_cast(arc4random()); - self.process->codesigning_flags = - 0x1 | 0x20000000; // CS_VALID | CS_SIGNED -> See kern/cs_blobs.h - self.process->is_platform_binary = false; - self.process->is_es_client = false; - - self.message = static_cast(calloc(1, sizeof(es_message_t))); - self.message->version = 4; - self.message->mach_time = DISPATCH_TIME_NOW; - self.message->deadline = DISPATCH_TIME_FOREVER; - self.message->seq_num = 1; -} - -- (void)fillLinks { - if (self.binaryPath != nil) { - self.executable->path = MakeStringToken(self.binaryPath); - } - - if (self.process->executable == NULL) { - self.process->executable = self.executable; - } - if (self.message->process == NULL) { - self.message->process = self.process; - } -} - -- (void)dealloc { - free(self.process); - free(self.executable); - free(self.message); -} -@end - -@implementation ESResponse -@end - -@interface MockESClient : NSObject -@property NSMutableArray *_Nonnull subscriptions; -@property es_handler_block_t handler; -@end - -@implementation MockESClient - -- (instancetype)init { - self = [super init]; - if (self) { - @synchronized(self) { - _subscriptions = [NSMutableArray arrayWithCapacity:ES_EVENT_TYPE_LAST]; - for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) { - [self.subscriptions addObject:@NO]; - } - } - } - return self; -}; - -- (void)resetSubscriptions { - for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) { - _subscriptions[i] = @NO; - } -} - -- (void)triggerHandler:(es_message_t *_Nonnull)msg { - self.handler((__bridge es_client_t *_Nullable)self, msg); -} - -- (void)dealloc { - @synchronized(self) { - [self.subscriptions removeAllObjects]; - } -} - -@end - -@interface MockEndpointSecurity () -@property NSMutableArray *clients; - -// Array of collections of ESCallback blocks -// This should be of size ES_EVENT_TYPE_LAST, allowing for indexing by ES_EVENT_TYPE_xxx members. -@property NSMutableArray *> *responseCallbacks; -@end - -@implementation MockEndpointSecurity -- (instancetype)init { - self = [super init]; - if (self) { - @synchronized(self) { - _clients = [NSMutableArray array]; - _responseCallbacks = [NSMutableArray arrayWithCapacity:ES_EVENT_TYPE_LAST]; - for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) { - [self.responseCallbacks addObject:[NSMutableArray array]]; - } - [self reset]; - } - } - return self; -}; - -- (void)resetResponseCallbacks { - for (NSMutableArray *callback in self.responseCallbacks) { - if (callback != nil) { - [callback removeAllObjects]; - } - } -} - -- (void)reset { - @synchronized(self) { - [self.clients removeAllObjects]; - [self resetResponseCallbacks]; - } -}; - -- (void)newClient:(es_client_t *_Nullable *_Nonnull)client - handler:(es_handler_block_t __strong)handler { - // es_client_t is generally used as a pointer to an opaque struct (secretly a mach port). - // There is also a few nonnull initialization checks on it. - MockESClient *mockClient = [[MockESClient alloc] init]; - *client = (__bridge es_client_t *)mockClient; - mockClient.handler = handler; - [self.clients addObject:mockClient]; -} - -- (BOOL)removeClient:(es_client_t *_Nonnull)client { - MockESClient *clientToRemove = [self findClient:client]; - - if (!clientToRemove) { - NSLog(@"Attempted to remove unknown mock es client."); - return NO; - } - - [self.clients removeObject:clientToRemove]; - return YES; -} - -- (void)triggerHandler:(es_message_t *_Nonnull)msg { - for (MockESClient *client in self.clients) { - if (client.subscriptions[msg->event_type]) { - [client triggerHandler:msg]; - } - } -} - -- (void)registerResponseCallback:(es_event_type_t)t withCallback:(ESCallback _Nonnull)callback { - @synchronized(self) { - [self.responseCallbacks[t] addObject:callback]; - } -} - -- (es_respond_result_t)respond_auth_result:(const es_message_t *_Nonnull)msg - result:(es_auth_result_t)result - cache:(bool)cache { - @synchronized(self) { - ESResponse *response = [[ESResponse alloc] init]; - response.result = result; - response.shouldCache = cache; - for (void (^callback)(ESResponse *) in self.responseCallbacks[msg->event_type]) { - callback(response); - } - } - return ES_RESPOND_RESULT_SUCCESS; -}; - -- (MockESClient *)findClient:(es_client_t *)client { - for (MockESClient *c in self.clients) { - // Since we're mocking out a C interface and using this exact pointer as our - // client identifier, only check for pointer equality. - if (client == (__bridge es_client_t *)c) { - return c; - } - } - return nil; -} - -- (void)setSubscriptions:(const es_event_type_t *_Nonnull)events - event_count:(uint32_t)event_count - value:(NSNumber *)value - client:(es_client_t *)client { - @synchronized(self) { - MockESClient *toUpdate = [self findClient:client]; - - if (toUpdate == nil) { - NSLog(@"setting subscription for unknown client"); - return; - } - - for (size_t i = 0; i < event_count; i++) { - toUpdate.subscriptions[events[i]] = value; - } - } -} - -+ (instancetype _Nonnull)mockEndpointSecurity { - static MockEndpointSecurity *sharedES; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedES = [[MockEndpointSecurity alloc] init]; - }); - return sharedES; -}; -@end - -API_UNAVAILABLE(ios, tvos, watchos) -es_message_t *_Nullable es_copy_message(const es_message_t *_Nonnull msg) { - return (es_message_t *)msg; -}; - -API_UNAVAILABLE(ios, tvos, watchos) -void es_free_message(es_message_t *_Nonnull msg){}; - -API_AVAILABLE(macos(10.15)) -API_UNAVAILABLE(ios, tvos, watchos) -es_new_client_result_t es_new_client(es_client_t *_Nullable *_Nonnull client, - es_handler_block_t _Nonnull handler) { - [[MockEndpointSecurity mockEndpointSecurity] newClient:client handler:handler]; - return ES_NEW_CLIENT_RESULT_SUCCESS; -}; - -es_return_t es_mute_process(es_client_t * _Nonnull client, - const audit_token_t * _Nonnull audit_token) { - return ES_RETURN_SUCCESS; -} - -#if defined(MAC_OS_VERSION_12_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0 -API_AVAILABLE(macos(12.0)) -API_UNAVAILABLE(ios, tvos, watchos) -es_return_t es_muted_paths_events(es_client_t *_Nonnull client, - es_muted_paths_t *_Nonnull *_Nullable muted_paths) { - es_muted_paths_t *tmp = (es_muted_paths_t *)malloc(sizeof(es_muted_paths_t)); - - tmp->count = 0; - *muted_paths = (es_muted_paths_t *_Nullable)tmp; - - return ES_RETURN_SUCCESS; -}; - -API_AVAILABLE(macos(12.0)) -API_UNAVAILABLE(ios, tvos, watchos) -void es_release_muted_paths(es_muted_paths_t *_Nonnull muted_paths) { - free(muted_paths); -} -#endif - -API_AVAILABLE(macos(10.15)) -API_UNAVAILABLE(ios, tvos, watchos) es_return_t es_delete_client(es_client_t *_Nullable client) { - if (![[MockEndpointSecurity mockEndpointSecurity] removeClient:client]) { - return ES_RETURN_ERROR; - } - 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, - const es_message_t *_Nonnull message, - es_auth_result_t result, bool cache) { - return [[MockEndpointSecurity mockEndpointSecurity] respond_auth_result:message - result:result - cache:cache]; -}; - -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) { - [[MockEndpointSecurity mockEndpointSecurity] setSubscriptions:events - event_count:event_count - value:@YES - client:client]; - 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] setSubscriptions:events - event_count:event_count - value:@NO - client:client]; - - return ES_RETURN_SUCCESS; -}; diff --git a/Source/santad/EventProviders/SNTCachingEndpointSecurityManager.mm b/Source/santad/EventProviders/SNTCachingEndpointSecurityManager.mm deleted file mode 100644 index b08874d10..000000000 --- a/Source/santad/EventProviders/SNTCachingEndpointSecurityManager.mm +++ /dev/null @@ -1,210 +0,0 @@ -/// Copyright 2021 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import "Source/santad/EventProviders/SNTCachingEndpointSecurityManager.h" - -#import "Source/common/SNTLogging.h" -#import "Source/common/SantaCache.h" - -#include -#include - -uint64_t GetCurrentUptime() { - return clock_gettime_nsec_np(CLOCK_MONOTONIC); -} -template <> -uint64_t SantaCacheHasher(santa_vnode_id_t const &t) { - return (SantaCacheHasher(t.fsid) << 1) ^ SantaCacheHasher(t.fileid); -} - -@implementation SNTCachingEndpointSecurityManager { - // Create 2 separate caches, mapping from the (filesysem + vnode ID) to a decision with a timestamp. - // The root cache is for decisions on the root volume, which can never be unmounted and the other - // is for executions from all other volumes. This cache will be emptied if any volume is unmounted. - SantaCache *_rootDecisionCache; - SantaCache *_nonRootDecisionCache; - uint64_t _rootVnodeID; -} - -- (instancetype)init { - self = [super init]; - if (self) { - _rootDecisionCache = new SantaCache(); - _nonRootDecisionCache = new SantaCache(); - - // Store the filesystem ID of the root vnode for split-cache usage. - // If the stat fails for any reason _rootVnodeID will be 0 and all decisions will be in a single cache. - struct stat rootStat; - if (stat("/", &rootStat) == 0) { - _rootVnodeID = (uint64_t)rootStat.st_dev; - } - } - - return self; -} - -- (void)dealloc { - if (_rootDecisionCache) delete _rootDecisionCache; - if (_nonRootDecisionCache) delete _nonRootDecisionCache; -} - -- (BOOL)respondFromCache:(es_message_t *)m API_AVAILABLE(macos(10.15)) { - auto vnode_id = [self vnodeIDForFile:m->event.exec.target->executable]; - while (true) { - // Check to see if item is in cache - auto return_action = [self checkCache:vnode_id]; - - // If item was in cache with a valid response, return it. - // If item is in cache but hasn't received a response yet, sleep for a bit. - // If item is not in cache, break out of loop and forward request to callback. - if (RESPONSE_VALID(return_action)) { - switch (return_action) { - case ACTION_RESPOND_ALLOW: - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true); - break; - case ACTION_RESPOND_ALLOW_COMPILER: { - pid_t pid = audit_token_to_pid(m->process->audit_token); - [self setIsCompilerPID:pid]; - // Don't let ES cache compilers - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false); - break; - } - default: es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false); break; - } - return YES; - } else if (return_action == ACTION_REQUEST_BINARY || return_action == ACTION_RESPOND_ACK) { - // TODO(rah): Look at a replacement for msleep(), maybe NSCondition - usleep(5000); - } else { - break; - } - } - - [self addToCache:vnode_id decision:ACTION_REQUEST_BINARY currentTicks:0]; - return NO; -} - -- (int)postAction:(santa_action_t)action - forMessage:(santa_message_t)sm API_AVAILABLE(macos(10.15)) { - es_respond_result_t ret; - switch (action) { - case ACTION_RESPOND_ALLOW_COMPILER: - [self setIsCompilerPID:sm.pid]; - - // Allow the exec and cache in our internal cache but don't let ES cache, because then - // we won't see future execs of the compiler in order to record the PID. - [self addToCache:sm.vnode_id - decision:ACTION_RESPOND_ALLOW_COMPILER - currentTicks:GetCurrentUptime()]; - ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW, - false); - break; - case ACTION_RESPOND_ALLOW: - case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE: - [self addToCache:sm.vnode_id decision:ACTION_RESPOND_ALLOW currentTicks:GetCurrentUptime()]; - ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW, - true); - break; - case ACTION_RESPOND_DENY: - [self addToCache:sm.vnode_id decision:ACTION_RESPOND_DENY currentTicks:GetCurrentUptime()]; - OS_FALLTHROUGH; - case ACTION_RESPOND_TOOLONG: - ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_DENY, - false); - break; - case ACTION_RESPOND_ACK: return ES_RESPOND_RESULT_SUCCESS; - default: ret = ES_RESPOND_RESULT_ERR_INVALID_ARGUMENT; - } - - return ret; -} - -- (void)addToCache:(santa_vnode_id_t)identifier - decision:(santa_action_t)decision - currentTicks:(uint64_t)microsecs { - auto _decisionCache = [self cacheForVnodeID:identifier]; - switch (decision) { - case ACTION_REQUEST_BINARY: - _decisionCache->set(identifier, (uint64_t)ACTION_REQUEST_BINARY << 56, 0); - break; - case ACTION_RESPOND_ACK: - _decisionCache->set(identifier, (uint64_t)ACTION_RESPOND_ACK << 56, - ((uint64_t)ACTION_REQUEST_BINARY << 56)); - break; - case ACTION_RESPOND_ALLOW: - case ACTION_RESPOND_ALLOW_COMPILER: - case ACTION_RESPOND_DENY: { - // Decision is stored in upper 8 bits, timestamp in remaining 56. - uint64_t val = ((uint64_t)decision << 56) | (microsecs & 0xFFFFFFFFFFFFFF); - if (!_decisionCache->set(identifier, val, ((uint64_t)ACTION_REQUEST_BINARY << 56))) { - _decisionCache->set(identifier, val, ((uint64_t)ACTION_RESPOND_ACK << 56)); - } - break; - } - case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE: { - // Decision is stored in upper 8 bits, timestamp in remaining 56. - uint64_t val = ((uint64_t)decision << 56) | (microsecs & 0xFFFFFFFFFFFFFF); - _decisionCache->set(identifier, val, 0); - break; - } - default: break; - } - // TODO(rah): Look at a replacement for wakeup(), maybe NSCondition -} - -- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly API_AVAILABLE(macos(10.15)) { - _nonRootDecisionCache->clear(); - if (!nonRootOnly) _rootDecisionCache->clear(); - if (!self.connectionEstablished) return YES; // if not connected, there's nothing to flush. - return es_clear_cache(self.client) == ES_CLEAR_CACHE_RESULT_SUCCESS; -} - -- (NSArray *)cacheCounts { - return @[ @(_rootDecisionCache->count()), @(_nonRootDecisionCache->count()) ]; -} - -- (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID { - auto result = ACTION_UNSET; - uint64_t decision_time = 0; - - uint64_t cache_val = [self cacheForVnodeID:vnodeID]->get(vnodeID); - if (cache_val == 0) return result; - - // Decision is stored in upper 8 bits, timestamp in remaining 56. - result = (santa_action_t)(cache_val >> 56); - decision_time = (cache_val & ~(0xFF00000000000000)); - - if (RESPONSE_VALID(result)) { - if (result == ACTION_RESPOND_DENY) { - auto expiry_time = decision_time + (500 * 100000); // kMaxCacheDenyTimeMilliseconds - if (expiry_time < GetCurrentUptime()) { - [self cacheForVnodeID:vnodeID]->remove(vnodeID); - return ACTION_UNSET; - } - } - } - return result; -} - -- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeID { - [self cacheForVnodeID:vnodeID]->remove(vnodeID); - // TODO(rah): Look at a replacement for wakeup(), maybe NSCondition - return 0; -} - -- (SantaCache *)cacheForVnodeID:(santa_vnode_id_t)vnodeID { - return (vnodeID.fsid == _rootVnodeID || _rootVnodeID == 0) ? _rootDecisionCache : _nonRootDecisionCache; -} - -@end diff --git a/Source/santad/EventProviders/SNTDeviceManagerTest.mm b/Source/santad/EventProviders/SNTDeviceManagerTest.mm deleted file mode 100644 index a0e50f029..000000000 --- a/Source/santad/EventProviders/SNTDeviceManagerTest.mm +++ /dev/null @@ -1,291 +0,0 @@ -/// Copyright 2021 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -#import -#import -#import -#import - -#include - -#import "Source/common/SNTConfigurator.h" -#import "Source/common/SNTDeviceEvent.h" -#import "Source/santad/EventProviders/SNTDeviceManager.h" - -#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h" -#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h" - -@interface SNTDeviceManagerTest : XCTestCase -@property id mockConfigurator; -@end - -@implementation SNTDeviceManagerTest - -- (void)setUp { - [super setUp]; - self.mockConfigurator = OCMClassMock([SNTConfigurator class]); - OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator); - OCMStub([self.mockConfigurator eventLogType]).andReturn(-1); - - fclose(stdout); -} - -- (ESResponse *)triggerTestMountEvent:(SNTDeviceManager *)deviceManager - mockES:(MockEndpointSecurity *)mockES - mockDA:(MockDiskArbitration *)mockDA - eventType:(es_event_type_t)eventType - diskInfoOverrides:(NSDictionary *)diskInfo { - if (!deviceManager.subscribed) { - // [deviceManager listen] is synchronous, but we want to asynchronously dispatch it - // with an enforced timeout to ensure that we never run into issues where the client - // never instantiates. - XCTestExpectation *initExpectation = - [self expectationWithDescription:@"Wait for SNTDeviceManager to instantiate"]; - - dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ - [deviceManager listen]; - }); - - dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ - while (!deviceManager.subscribed) - ; - [initExpectation fulfill]; - }); - [self waitForExpectations:@[ initExpectation ] timeout:60.0]; - } - - struct statfs *fs = static_cast(calloc(1, sizeof(struct statfs))); - NSString *test_mntfromname = @"/dev/disk2s1"; - NSString *test_mntonname = @"/Volumes/KATE'S 4G"; - const char *c_mntfromname = [test_mntfromname UTF8String]; - const char *c_mntonname = [test_mntonname UTF8String]; - - strncpy(fs->f_mntfromname, c_mntfromname, MAXPATHLEN); - strncpy(fs->f_mntonname, c_mntonname, MAXPATHLEN); - - MockDADisk *disk = [[MockDADisk alloc] init]; - disk.diskDescription = @{ - (__bridge NSString *)kDADiskDescriptionDeviceProtocolKey : @"USB", - (__bridge NSString *)kDADiskDescriptionMediaRemovableKey : @YES, - @"DAVolumeMountable" : @YES, - @"DAVolumePath" : test_mntonname, - @"DADeviceModel" : @"Some device model", - @"DADevicePath" : test_mntonname, - @"DADeviceVendor" : @"Some vendor", - @"DAAppearanceTime" : @0, - @"DAMediaBSDName" : test_mntfromname, - }; - - if (diskInfo != nil) { - NSMutableDictionary *mergedDiskDescription = [disk.diskDescription mutableCopy]; - for (NSString *key in diskInfo) { - mergedDiskDescription[key] = diskInfo[key]; - } - disk.diskDescription = (NSDictionary *)mergedDiskDescription; - } - - [mockDA insert:disk bsdName:test_mntfromname]; - - ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) { - m.binaryPath = @"/System/Library/Filesystems/msdos.fs/Contents/Resources/mount_msdos"; - m.message->action_type = ES_ACTION_TYPE_AUTH; - m.message->event_type = eventType; - if (eventType == ES_EVENT_TYPE_AUTH_MOUNT) { - m.message->event = (es_events_t){.mount = {.statfs = fs}}; - } else { - m.message->event = (es_events_t){.remount = {.statfs = fs}}; - } - }]; - - XCTestExpectation *mountExpectation = - [self expectationWithDescription:@"Wait for response from ES"]; - __block ESResponse *got; - [mockES registerResponseCallback:eventType - withCallback:^(ESResponse *r) { - got = r; - [mountExpectation fulfill]; - }]; - - [mockES triggerHandler:m.message]; - - [self waitForExpectations:@[ mountExpectation ] timeout:60.0]; - free(fs); - - return got; -} - -- (void)testUSBBlockDisabled { - MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity]; - [mockES reset]; - - MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration]; - [mockDA reset]; - - SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init]; - deviceManager.blockUSBMount = NO; - ESResponse *got = [self triggerTestMountEvent:deviceManager - mockES:mockES - mockDA:mockDA - eventType:ES_EVENT_TYPE_AUTH_MOUNT - diskInfoOverrides:nil]; - - XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW); -} - -- (void)testRemount { - MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity]; - [mockES reset]; - - MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration]; - [mockDA reset]; - - SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init]; - deviceManager.blockUSBMount = YES; - deviceManager.remountArgs = @[ @"noexec", @"rdonly" ]; - - XCTestExpectation *expectation = - [self expectationWithDescription:@"Wait for SNTDeviceManager's blockCallback to trigger"]; - - __block NSString *gotmntonname, *gotmntfromname; - __block NSArray *gotRemountedArgs; - deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) { - gotRemountedArgs = event.remountArgs; - gotmntonname = event.mntonname; - gotmntfromname = event.mntfromname; - [expectation fulfill]; - }; - - ESResponse *got = [self triggerTestMountEvent:deviceManager - mockES:mockES - mockDA:mockDA - eventType:ES_EVENT_TYPE_AUTH_MOUNT - diskInfoOverrides:nil]; - - XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY); - XCTAssertEqual(mockDA.wasRemounted, YES); - - [self waitForExpectations:@[ expectation ] timeout:60.0]; - - XCTAssertEqualObjects(gotRemountedArgs, deviceManager.remountArgs); - XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G"); - XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1"); -} - -- (void)testBlockNoRemount { - MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity]; - [mockES reset]; - - MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration]; - [mockDA reset]; - - SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init]; - deviceManager.blockUSBMount = YES; - - XCTestExpectation *expectation = - [self expectationWithDescription:@"Wait for SNTDeviceManager's blockCallback to trigger"]; - - __block NSString *gotmntonname, *gotmntfromname; - __block NSArray *gotRemountedArgs; - deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) { - gotRemountedArgs = event.remountArgs; - gotmntonname = event.mntonname; - gotmntfromname = event.mntfromname; - [expectation fulfill]; - }; - - ESResponse *got = [self triggerTestMountEvent:deviceManager - mockES:mockES - mockDA:mockDA - eventType:ES_EVENT_TYPE_AUTH_MOUNT - diskInfoOverrides:nil]; - - XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY); - - [self waitForExpectations:@[ expectation ] timeout:60.0]; - - XCTAssertNil(gotRemountedArgs); - XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G"); - XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1"); -} - -- (void)testEnsureRemountsCannotChangePerms { - MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity]; - [mockES reset]; - - MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration]; - [mockDA reset]; - - SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init]; - deviceManager.blockUSBMount = YES; - deviceManager.remountArgs = @[ @"noexec", @"rdonly" ]; - - XCTestExpectation *expectation = - [self expectationWithDescription:@"Wait for SNTDeviceManager's blockCallback to trigger"]; - - __block NSString *gotmntonname, *gotmntfromname; - __block NSArray *gotRemountedArgs; - deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) { - gotRemountedArgs = event.remountArgs; - gotmntonname = event.mntonname; - gotmntfromname = event.mntfromname; - [expectation fulfill]; - }; - - ESResponse *got = [self triggerTestMountEvent:deviceManager - mockES:mockES - mockDA:mockDA - eventType:ES_EVENT_TYPE_AUTH_REMOUNT - diskInfoOverrides:nil]; - - XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY); - XCTAssertEqual(mockDA.wasRemounted, YES); - - [self waitForExpectations:@[ expectation ] timeout:10.0]; - - XCTAssertEqualObjects(gotRemountedArgs, deviceManager.remountArgs); - XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G"); - XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1"); -} - -- (void)testEnsureDMGsDoNotPrompt { - MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity]; - [mockES reset]; - - MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration]; - [mockDA reset]; - - SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init]; - deviceManager.blockUSBMount = YES; - deviceManager.remountArgs = @[ @"noexec", @"rdonly" ]; - - deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) { - XCTFail(@"Should not be called"); - }; - - NSDictionary *diskInfo = @{ - (__bridge NSString *)kDADiskDescriptionDeviceProtocolKey: @"Virtual Interface", - (__bridge NSString *)kDADiskDescriptionDeviceModelKey: @"Disk Image", - (__bridge NSString *)kDADiskDescriptionMediaNameKey: @"disk image", - }; - - - ESResponse *got = [self triggerTestMountEvent:deviceManager - mockES:mockES - mockDA:mockDA - eventType:ES_EVENT_TYPE_AUTH_MOUNT - diskInfoOverrides:diskInfo]; - - XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW); - XCTAssertEqual(mockDA.wasRemounted, NO); -} -@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h b/Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h new file mode 100644 index 000000000..386307b4f --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h @@ -0,0 +1,38 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" +#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h" + +#import "Source/santad/EventProviders/AuthResultCache.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h" +#import "Source/santad/SNTCompilerController.h" +#import "Source/santad/SNTExecutionController.h" + +/// ES Client focused on subscribing to AUTH variants and authorizing the events +/// based on configured policy. +@interface SNTEndpointSecurityAuthorizer + : SNTEndpointSecurityClient + +- (instancetype) + initWithESAPI: + (std::shared_ptr) + esApi + execController:(SNTExecutionController *)execController + compilerController:(SNTCompilerController *)compilerController + authResultCache: + (std::shared_ptr)authResultCache; + +@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.mm b/Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.mm new file mode 100644 index 000000000..3a228522f --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.mm @@ -0,0 +1,145 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import "Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h" + +#include +#include +#include + +#import "Source/common/SNTLogging.h" +#include "Source/santad/EventProviders/AuthResultCache.h" +#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" + +using santa::santad::event_providers::AuthResultCache; +using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI; +using santa::santad::event_providers::endpoint_security::Message; + +@interface SNTEndpointSecurityAuthorizer () +@property SNTCompilerController *compilerController; +@property SNTExecutionController *execController; +@end + +@implementation SNTEndpointSecurityAuthorizer { + std::shared_ptr _authResultCache; +} + +- (instancetype)initWithESAPI:(std::shared_ptr)esApi + execController:(SNTExecutionController *)execController + compilerController:(SNTCompilerController *)compilerController + authResultCache:(std::shared_ptr)authResultCache { + self = [super initWithESAPI:std::move(esApi)]; + if (self) { + _execController = execController; + _compilerController = compilerController; + _authResultCache = authResultCache; + + [self establishClientOrDie]; + } + return self; +} + +- (void)processMessage:(const Message &)msg { + const es_file_t *targetFile = msg->event.exec.target->executable; + + while (true) { + santa_action_t returnAction = self->_authResultCache->CheckCache(targetFile); + if (RESPONSE_VALID(returnAction)) { + es_auth_result_t authResult = ES_AUTH_RESULT_DENY; + + switch (returnAction) { + case ACTION_RESPOND_ALLOW_COMPILER: + [self.compilerController setProcess:msg->event.exec.target->audit_token isCompiler:true]; + OS_FALLTHROUGH; + case ACTION_RESPOND_ALLOW: authResult = ES_AUTH_RESULT_ALLOW; break; + default: break; + } + + [self respondToMessage:msg + withAuthResult:authResult + cacheable:(authResult == ES_AUTH_RESULT_ALLOW)]; + return; + } else if (returnAction == ACTION_REQUEST_BINARY) { + // TODO(mlw): Add a metric here to observe how ofthen this happens in practice. + // TODO(mlw): Look into caching a `Deferred` to better prevent + // raciness of multiple threads checking the cache simultaneously. + // Also mitigates need to poll. + usleep(5000); + } else { + break; + } + } + + self->_authResultCache->AddToCache(targetFile, ACTION_REQUEST_BINARY); + + [self.execController validateExecEvent:msg + postAction:^bool(santa_action_t action) { + return [self postAction:action forMessage:msg]; + }]; +} + +- (void)handleMessage:(Message &&)esMsg { + if (unlikely(esMsg->event_type != ES_EVENT_TYPE_AUTH_EXEC)) { + // This is a programming error + LOGE(@"Atteempting to authorize a non-exec event"); + [NSException raise:@"Invalid event type" + format:@"Authorizing unexpected event type: %d", esMsg->event_type]; + } + + if (![self.execController synchronousShouldProcessExecEvent:esMsg]) { + [self postAction:ACTION_RESPOND_DENY forMessage:esMsg]; + return; + } + + [self processMessage:std::move(esMsg) + handler:^(const Message &msg) { + [self processMessage:msg]; + }]; +} + +- (bool)postAction:(santa_action_t)action forMessage:(const Message &)esMsg { + es_auth_result_t authResult; + + switch (action) { + case ACTION_RESPOND_ALLOW_COMPILER: + [self.compilerController setProcess:esMsg->event.exec.target->audit_token isCompiler:true]; + OS_FALLTHROUGH; + case ACTION_RESPOND_ALLOW: authResult = ES_AUTH_RESULT_ALLOW; break; + case ACTION_RESPOND_DENY: authResult = ES_AUTH_RESULT_DENY; break; + default: + // This is a programming error. Bail. + LOGE(@"Invalid action for postAction, exiting."); + [NSException raise:@"Invalid post action" format:@"Invalid post action: %d", action]; + } + + self->_authResultCache->AddToCache(esMsg->event.exec.target->executable, action); + + // Don't let the ES framework cache DENY results. Santa only flushes ES cache + // when a new DENY rule is received. If DENY results were cached and a rule + // update made the executable allowable, ES would continue to apply the DENY + // cached result. Note however that the local AuthResultCache will cache + // DENY results. + return [self respondToMessage:esMsg + withAuthResult:authResult + cacheable:(authResult == ES_AUTH_RESULT_ALLOW)]; +} + +- (void)enable { + [super subscribeAndClearCache:{ + ES_EVENT_TYPE_AUTH_EXEC, + }]; +} + +@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityAuthorizerTest.mm b/Source/santad/EventProviders/SNTEndpointSecurityAuthorizerTest.mm new file mode 100644 index 000000000..17a2c748f --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityAuthorizerTest.mm @@ -0,0 +1,273 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include +#import +#import +#include +#include + +#include +#include +#include + +#include "Source/common/TestUtils.h" +#include "Source/santad/EventProviders/AuthResultCache.h" +#include "Source/santad/EventProviders/EndpointSecurity/Client.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" +#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h" +#import "Source/santad/SNTCompilerController.h" +#import "Source/santad/SNTExecutionController.h" + +using santa::santad::event_providers::AuthResultCache; +using santa::santad::event_providers::endpoint_security::Message; + +class MockAuthResultCache : public AuthResultCache { + public: + using AuthResultCache::AuthResultCache; + + MOCK_METHOD(bool, AddToCache, (const es_file_t *es_file, santa_action_t decision)); + MOCK_METHOD(santa_action_t, CheckCache, (const es_file_t *es_file)); +}; + +@interface SNTEndpointSecurityAuthorizer (Testing) +- (void)processMessage:(const Message &)msg; +- (bool)postAction:(santa_action_t)action forMessage:(const Message &)esMsg; +@end + +@interface SNTEndpointSecurityAuthorizerTest : XCTestCase +@property id mockExecController; +@end + +@implementation SNTEndpointSecurityAuthorizerTest + +- (void)setUp { + self.mockExecController = OCMStrictClassMock([SNTExecutionController class]); +} + +- (void)tearDown { + [self.mockExecController stopMocking]; +} + +- (void)testEnable { + // Ensure the client subscribes to expected event types + std::set expectedEventSubs{ES_EVENT_TYPE_AUTH_EXEC}; + auto mockESApi = std::make_shared(); + + id authClient = [[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi]; + + EXPECT_CALL(*mockESApi, ClearCache) + .After(EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs)) + .WillOnce(testing::Return(true))) + .WillOnce(testing::Return(true)); + + [authClient enable]; + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testHandleMessage { + es_file_t file = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&file); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth); + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsESNewClient(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + SNTEndpointSecurityAuthorizer *authClient = + [[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi + execController:self.mockExecController + compilerController:nil + authResultCache:nullptr]; + + id mockAuthClient = OCMPartialMock(authClient); + + // Test unhandled event type + { + // Temporarily change the event type + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC; + XCTAssertThrows([authClient handleMessage:Message(mockESApi, &esMsg)]); + esMsg.event_type = ES_EVENT_TYPE_AUTH_EXEC; + } + + // Test SNTExecutionController determines the event shouldn't be processed + { + Message msg(mockESApi, &esMsg); + + OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg]) + .ignoringNonObjectArgs() + .andReturn(NO); + + OCMExpect([mockAuthClient postAction:ACTION_RESPOND_DENY forMessage:Message(mockESApi, &esMsg)]) + .ignoringNonObjectArgs(); + OCMStub([mockAuthClient postAction:ACTION_RESPOND_DENY forMessage:Message(mockESApi, &esMsg)]) + .ignoringNonObjectArgs() + .andDo(nil); + + [mockAuthClient handleMessage:std::move(msg)]; + XCTAssertTrue(OCMVerifyAll(mockAuthClient)); + } + + // Test SNTExecutionController determines the event should be processed and + // processMessage:handler: is called. + { + Message msg(mockESApi, &esMsg); + + OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg]) + .ignoringNonObjectArgs() + .andReturn(YES); + + OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg) handler:[OCMArg any]]) + .ignoringNonObjectArgs(); + OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg) handler:[OCMArg any]]) + .ignoringNonObjectArgs() + .andDo(nil); + + [mockAuthClient handleMessage:std::move(msg)]; + XCTAssertTrue(OCMVerifyAll(mockAuthClient)); + } + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); + + [mockAuthClient stopMocking]; +} + +- (void)testProcessMessageWaitThenAllow { + // This test ensures that if there is an outstanding action for + // an item, it will check the cache again until a result exists. + es_file_t file = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&file); + es_file_t execFile = MakeESFile("bar"); + es_process_t execProc = MakeESProcess(&execFile, MakeAuditToken(12, 23), MakeAuditToken(34, 45)); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth); + esMsg.event.exec.target = &execProc; + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsESNewClient(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + auto mockAuthCache = std::make_shared(nullptr); + EXPECT_CALL(*mockAuthCache, CheckCache) + .WillOnce(testing::Return(ACTION_REQUEST_BINARY)) + .WillOnce(testing::Return(ACTION_REQUEST_BINARY)) + .WillOnce(testing::Return(ACTION_RESPOND_ALLOW_COMPILER)) + .WillOnce(testing::Return(ACTION_UNSET)); + EXPECT_CALL(*mockAuthCache, AddToCache(testing::_, ACTION_REQUEST_BINARY)) + .WillOnce(testing::Return(true)); + + id mockCompilerController = OCMStrictClassMock([SNTCompilerController class]); + OCMExpect([mockCompilerController setProcess:execProc.audit_token isCompiler:true]); + + SNTEndpointSecurityAuthorizer *authClient = + [[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi + execController:self.mockExecController + compilerController:mockCompilerController + authResultCache:mockAuthCache]; + id mockAuthClient = OCMPartialMock(authClient); + + // This block tests that processing is held up until an outstanding thread + // processing another event completes and returns a result. This test + // specifically will check the `ACTION_RESPOND_ALLOW_COMPILER` flow. + { + Message msg(mockESApi, &esMsg); + OCMExpect([mockAuthClient respondToMessage:msg + withAuthResult:ES_AUTH_RESULT_ALLOW + cacheable:true]); + + [mockAuthClient processMessage:msg]; + + XCTAssertTrue(OCMVerifyAll(mockAuthClient)); + XCTAssertTrue(OCMVerifyAll(mockCompilerController)); + } + + // This block tests uncached events storing appropriate cache marker and then + // running the exec controller to validate the exec event. + { + Message msg(mockESApi, &esMsg); + OCMExpect([self.mockExecController validateExecEvent:msg postAction:OCMOCK_ANY]) + .ignoringNonObjectArgs(); + + [mockAuthClient processMessage:msg]; + + XCTAssertTrue(OCMVerifyAll(mockAuthClient)); + XCTAssertTrue(OCMVerifyAll(mockCompilerController)); + } + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); + XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get()); + + [mockCompilerController stopMocking]; + [mockAuthClient stopMocking]; +} + +- (void)testPostAction { + es_file_t file = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&file); + es_file_t execFile = MakeESFile("bar"); + es_process_t execProc = MakeESProcess(&execFile, MakeAuditToken(12, 23), MakeAuditToken(34, 45)); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth); + esMsg.event.exec.target = &execProc; + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsESNewClient(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + auto mockAuthCache = std::make_shared(nullptr); + EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_ALLOW_COMPILER)) + .WillOnce(testing::Return(true)); + EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_ALLOW)) + .WillOnce(testing::Return(true)); + EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_DENY)) + .WillOnce(testing::Return(true)); + + id mockCompilerController = OCMStrictClassMock([SNTCompilerController class]); + OCMExpect([mockCompilerController setProcess:execProc.audit_token isCompiler:true]); + + SNTEndpointSecurityAuthorizer *authClient = + [[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi + execController:self.mockExecController + compilerController:mockCompilerController + authResultCache:mockAuthCache]; + id mockAuthClient = OCMPartialMock(authClient); + + { + Message msg(mockESApi, &esMsg); + + XCTAssertThrows([mockAuthClient postAction:(santa_action_t)123 forMessage:msg]); + + std::map actions = { + {ACTION_RESPOND_ALLOW_COMPILER, ES_AUTH_RESULT_ALLOW}, + {ACTION_RESPOND_ALLOW, ES_AUTH_RESULT_ALLOW}, + {ACTION_RESPOND_DENY, ES_AUTH_RESULT_DENY}, + }; + + for (const auto &kv : actions) { + OCMExpect([mockAuthClient respondToMessage:msg + withAuthResult:kv.second + cacheable:kv.second == ES_AUTH_RESULT_ALLOW]); + + [mockAuthClient postAction:kv.first forMessage:msg]; + } + } + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); + XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get()); + + [mockCompilerController stopMocking]; + [mockAuthClient stopMocking]; +} + +@end diff --git a/Source/santad/Logs/SNTLogOutput.h b/Source/santad/EventProviders/SNTEndpointSecurityClient.h similarity index 64% rename from Source/santad/Logs/SNTLogOutput.h rename to Source/santad/EventProviders/SNTEndpointSecurityClient.h index ce267d26f..6d006439d 100644 --- a/Source/santad/Logs/SNTLogOutput.h +++ b/Source/santad/EventProviders/SNTEndpointSecurityClient.h @@ -1,4 +1,4 @@ -/// Copyright 2021 Google Inc. All rights reserved. +/// Copyright 2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -12,17 +12,9 @@ /// See the License for the specific language governing permissions and /// limitations under the License. -#import - -#import "Source/common/Santa.pbobjc.h" - -NS_ASSUME_NONNULL_BEGIN - -@protocol SNTLogOutput - -- (void)logEvent:(SNTPBSantaMessage *)event; -- (void)flush; +#include "Source/santad/EventProviders/SNTEndpointSecurityClientBase.h" +/// This should be treated as an Abstract Base Class and not directly instantiated +@interface SNTEndpointSecurityClient : NSObject +- (instancetype)init NS_UNAVAILABLE; @end - -NS_ASSUME_NONNULL_END diff --git a/Source/santad/EventProviders/SNTEndpointSecurityClient.mm b/Source/santad/EventProviders/SNTEndpointSecurityClient.mm new file mode 100644 index 000000000..b9acc1323 --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityClient.mm @@ -0,0 +1,243 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h" +#include + +#include +#include +#include +#include +#include + +#import "Source/common/SNTCommon.h" +#import "Source/common/SNTConfigurator.h" +#import "Source/common/SNTLogging.h" +#include "Source/santad/EventProviders/EndpointSecurity/Client.h" +#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" + +using santa::santad::event_providers::endpoint_security::Client; +using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI; +using santa::santad::event_providers::endpoint_security::EnrichedMessage; +using santa::santad::event_providers::endpoint_security::Message; + +@interface SNTEndpointSecurityClient () +@property int64_t deadlineMarginMS; +@end +; + +@implementation SNTEndpointSecurityClient { + std::shared_ptr _esApi; + Client _esClient; + mach_timebase_info_data_t _timebase; + dispatch_queue_t _authQueue; + dispatch_queue_t _notifyQueue; +} + +- (instancetype)initWithESAPI:(std::shared_ptr)esApi { + self = [super init]; + if (self) { + _esApi = std::move(esApi); + _deadlineMarginMS = 5000; + + if (mach_timebase_info(&_timebase) != KERN_SUCCESS) { + LOGE(@"Failed to get mach timebase info"); + // Assumed to be transitory failure. Let the daemon restart. + exit(EXIT_FAILURE); + } + + _authQueue = dispatch_queue_create( + "com.google.santa.daemon.auth_queue", + dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL, + QOS_CLASS_USER_INTERACTIVE, 0)); + + _notifyQueue = dispatch_queue_create( + "com.google.santa.daemon.notify_queue", + dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL, + QOS_CLASS_BACKGROUND, 0)); + } + return self; +} + +- (NSString *)errorMessageForNewClientResult:(es_new_client_result_t)result { + switch (result) { + case ES_NEW_CLIENT_RESULT_SUCCESS: return nil; + case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED: return @"Full-disk access not granted"; + case ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED: return @"Not entitled"; + case ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED: return @"Not running as root"; + case ES_NEW_CLIENT_RESULT_ERR_INVALID_ARGUMENT: return @"Invalid argument"; + case ES_NEW_CLIENT_RESULT_ERR_INTERNAL: return @"Internal error"; + case ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS: return @"Too many simultaneous clients"; + default: return @"Unknown error"; + } +} + +- (void)handleMessage:(Message &&)esMsg { + // This method should only be used by classes derived + // from SNTEndpointSecurityClient. + [self doesNotRecognizeSelector:_cmd]; +} + +- (BOOL)shouldHandleMessage:(const Message &)esMsg + ignoringOtherESClients:(BOOL)ignoringOtherESClients { + if (esMsg->process->is_es_client && ignoringOtherESClients) { + if (esMsg->action_type == ES_ACTION_TYPE_AUTH) { + [self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:true]; + } + return NO; + } + + return YES; +} + +- (void)establishClientOrDie { + if (self->_esClient.IsConnected()) { + // This is a programming error + LOGE(@"Client already established. Aborting."); + [NSException raise:@"Client already established" format:@"IsConnected already true"]; + } + + self->_esClient = self->_esApi->NewClient(^(es_client_t *c, Message esMsg) { + if ([self shouldHandleMessage:esMsg + ignoringOtherESClients:[[SNTConfigurator configurator] + ignoreOtherEndpointSecurityClients]]) { + [self handleMessage:std::move(esMsg)]; + } + }); + + if (!self->_esClient.IsConnected()) { + NSString *errMsg = [self errorMessageForNewClientResult:_esClient.NewClientResult()]; + LOGE(@"Unable to create EndpointSecurity client: %@", errMsg); + [NSException raise:@"Failed to create ES client" format:@"%@", errMsg]; + } else { + LOGI(@"Connected to EndpointSecurity"); + } + + if (![self muteSelf]) { + [NSException raise:@"ES Mute Failure" format:@"Failed to mute self"]; + } +} + ++ (bool)populateAuditTokenSelf:(audit_token_t *)tok { + mach_msg_type_number_t count = TASK_AUDIT_TOKEN_COUNT; + if (task_info(mach_task_self(), TASK_AUDIT_TOKEN, (task_info_t)tok, &count) != KERN_SUCCESS) { + LOGE(@"Failed to fetch this client's audit token."); + return false; + } + + return true; +} + +- (bool)muteSelf { + audit_token_t myAuditToken; + if (![SNTEndpointSecurityClient populateAuditTokenSelf:&myAuditToken]) { + return false; + } + + if (!self->_esApi->MuteProcess(self->_esClient, &myAuditToken)) { + LOGE(@"Failed to mute this client's process."); + return false; + } + + return true; +} + +- (bool)clearCache { + return _esApi->ClearCache(self->_esClient); +} + +- (bool)subscribe:(const std::set &)events { + return _esApi->Subscribe(_esClient, events); +} + +- (bool)subscribeAndClearCache:(const std::set &)events { + return [self subscribe:events] && [self clearCache]; +} + +- (bool)respondToMessage:(const Message &)msg + withAuthResult:(es_auth_result_t)result + cacheable:(bool)cacheable { + return _esApi->RespondAuthResult(_esClient, msg, result, cacheable); +} + +- (void)processEnrichedMessage:(std::shared_ptr)msg + handler:(void (^)(std::shared_ptr))messageHandler { + dispatch_async(_notifyQueue, ^{ + messageHandler(std::move(msg)); + }); +} + +- (void)processMessage:(Message &&)msg handler:(void (^)(const Message &))messageHandler { + if (unlikely(msg->action_type != ES_ACTION_TYPE_AUTH)) { + // This is a programming error + LOGE(@"Attempting to process non-AUTH message"); + [NSException raise:@"Attempt to process non-auth message" + format:@"Unexpected event type received: %d", msg->event_type]; + } + + dispatch_semaphore_t processingSema = dispatch_semaphore_create(0); + // Add 1 to the processing semaphore. We're not creating it with a starting + // value of 1 because that requires that the semaphore is not deallocated + // until its value matches the starting value, which we don't need. + dispatch_semaphore_signal(processingSema); + dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0); + + const uint64_t timeout = NSEC_PER_MSEC * (self.deadlineMarginMS); + uint64_t deadlineMachTime = msg->deadline - mach_absolute_time(); + uint64_t deadlineNano = deadlineMachTime * _timebase.numer / _timebase.denom; + + // TODO(mlw): How should we handle `deadlineNano <= timeout`. Will currently + // result in the deadline block being dispatched immediately (and therefore + // the event will be denied). + + // Workaround for compiler bug that doesn't properly close over variables + // Note: On macOS 10.15 this will cause extra message copies. + __block Message processMsg = msg; + __block Message deadlineMsg = msg; + + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, deadlineNano - timeout), self->_authQueue, ^(void) { + if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) { + // Handler has already responded, nothing to do. + return; + } + + bool res = [self respondToMessage:deadlineMsg + withAuthResult:ES_AUTH_RESULT_DENY + cacheable:false]; + + LOGE(@"SNTEndpointSecurityClient: deadline reached: deny pid=%d, event type: %d ret=%d", + audit_token_to_pid(deadlineMsg->process->audit_token), deadlineMsg->event_type, res); + dispatch_semaphore_signal(deadlineExpiredSema); + }); + + dispatch_async(self->_authQueue, ^{ + messageHandler(deadlineMsg); + if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) { + // Deadline expired, wait for deadline block to finish. + dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER); + } + }); +} + ++ (bool)isDatabasePath:(const std::string_view)path { + // TODO(mlw): These values should come from `SNTDatabaseController`. But right + // now they live as NSStrings. We should make them `std::string_view` types + // in order to use them here efficiently, but will need to make the + // `SNTDatabaseController` an ObjC++ file. + return (path == "/private/var/db/santa/rules.db" || path == "/private/var/db/santa/events.db"); +} + +@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityClientBase.h b/Source/santad/EventProviders/SNTEndpointSecurityClientBase.h new file mode 100644 index 000000000..34c18e614 --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityClientBase.h @@ -0,0 +1,73 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include +#include + +#include +#include + +#import + +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" +#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" + +@protocol SNTEndpointSecurityClientBase + +- (instancetype)initWithESAPI: + (std::shared_ptr)esApi; + +/// @note If this fails to establish a new ES client via `es_new_client`, an exception is raised +/// that should terminate the program. +- (void)establishClientOrDie; + +- (bool)subscribe:(const std::set &)events; + +/// Clears the ES cache after setting subscriptions. +/// There's a gap between creating a client and subscribing to events. Creating +/// the client triggers a cache flush automatically but any events that happen +/// prior to subscribing could've been cached by another client. Clearing after +/// subscribing mitigates this posibility. +- (bool)subscribeAndClearCache:(const std::set &)events; + +/// Responds to the Message with the given auth result +/// +/// @param Message The wrapped es_message_t being responded to +/// @param result Either ES_AUTH_RESULT_ALLOW or ES_AUTH_RESULT_DENY +/// @param cacheable true if ES should attempt to cache the result, otherwise false +/// @return true if the response was successful, otherwise false +- (bool)respondToMessage:(const santa::santad::event_providers::endpoint_security::Message &)msg + withAuthResult:(es_auth_result_t)result + cacheable:(bool)cacheable; + +- (void) + processEnrichedMessage: + (std::shared_ptr)msg + handler: + (void (^)(std::shared_ptr< + santa::santad::event_providers::endpoint_security::EnrichedMessage>)) + messageHandler; + +- (void)processMessage:(santa::santad::event_providers::endpoint_security::Message &&)msg + handler: + (void (^)(const santa::santad::event_providers::endpoint_security::Message &)) + messageHandler; + +- (bool)clearCache; + ++ (bool)isDatabasePath:(const std::string_view)path; ++ (bool)populateAuditTokenSelf:(audit_token_t *)tok; + +@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityClientTest.mm b/Source/santad/EventProviders/SNTEndpointSecurityClientTest.mm new file mode 100644 index 000000000..1fd6efbd3 --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityClientTest.mm @@ -0,0 +1,395 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include +#import +#import +#include +#include +#include +#include + +#include + +#include "Source/common/TestUtils.h" +#include "Source/santad/EventProviders/EndpointSecurity/Client.h" +#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" +#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h" + +using santa::santad::event_providers::endpoint_security::Client; +using santa::santad::event_providers::endpoint_security::EnrichedClose; +using santa::santad::event_providers::endpoint_security::EnrichedFile; +using santa::santad::event_providers::endpoint_security::EnrichedMessage; +using santa::santad::event_providers::endpoint_security::EnrichedProcess; +using santa::santad::event_providers::endpoint_security::Message; + +@interface SNTEndpointSecurityClient (Testing) +- (void)establishClientOrDie; +- (bool)muteSelf; +- (NSString *)errorMessageForNewClientResult:(es_new_client_result_t)result; +- (void)handleMessage:(Message &&)esMsg; +- (BOOL)shouldHandleMessage:(const Message &)esMsg + ignoringOtherESClients:(BOOL)ignoringOtherESClients; + +@property int64_t deadlineMarginMS; +@end + +@interface SNTEndpointSecurityClientTest : XCTestCase +@end + +@implementation SNTEndpointSecurityClientTest + +- (void)testEstablishClientOrDie { + auto mockESApi = std::make_shared(); + + EXPECT_CALL(*mockESApi, MuteProcess).WillOnce(testing::Return(true)); + + EXPECT_CALL(*mockESApi, NewClient) + .WillOnce(testing::Return(Client())) + .WillOnce(testing::Return(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS))); + + SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi]; + + // First time throws because mock triggers failed connection + // Second time succeeds + XCTAssertThrows([client establishClientOrDie]); + XCTAssertNoThrow([client establishClientOrDie]); + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testErrorMessageForNewClientResult { + std::map resultMessagePairs{ + {ES_NEW_CLIENT_RESULT_SUCCESS, ""}, + {ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED, "Full-disk access not granted"}, + {ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED, "Not entitled"}, + {ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED, "Not running as root"}, + {ES_NEW_CLIENT_RESULT_ERR_INVALID_ARGUMENT, "Invalid argument"}, + {ES_NEW_CLIENT_RESULT_ERR_INTERNAL, "Internal error"}, + {ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS, "Too many simultaneous clients"}, + {(es_new_client_result_t)123, "Unknown error"}, + }; + + SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:nullptr]; + + for (const auto &kv : resultMessagePairs) { + NSString *message = [client errorMessageForNewClientResult:kv.first]; + XCTAssertEqual(0, strcmp([(message ?: @"") UTF8String], kv.second.c_str())); + } +} + +- (void)testHandleMessage { + es_message_t esMsg; + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi]; + + { XCTAssertThrows([client handleMessage:Message(mockESApi, &esMsg)]); } + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testHandleMessageWithClient { + es_file_t file = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&file); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_FORK, &proc); + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + // Have subscribe fail the first time, meaning clear cache only called once. + EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_ALLOW, true)) + .WillOnce(testing::Return(true)); + + SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi]; + + { + Message msg(mockESApi, &esMsg); + + // Is ES client, but don't ignore others == Should Handle + esMsg.process->is_es_client = true; + XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:NO]); + + // Not ES client, but ignore others == Should Handle + esMsg.process->is_es_client = false; + XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:YES]); + + // Is ES client, don't ignore others, and non-AUTH == Don't Handle + esMsg.process->is_es_client = true; + XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]); + + // Is ES client, don't ignore others, and AUTH == Respond and Don't Handle + esMsg.process->is_es_client = true; + esMsg.action_type = ES_ACTION_TYPE_AUTH; + XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]); + } + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testPopulateAuditTokenSelf { + audit_token_t myAuditToken; + + [SNTEndpointSecurityClient populateAuditTokenSelf:&myAuditToken]; + + XCTAssertEqual(audit_token_to_pid(myAuditToken), getpid()); + XCTAssertNotEqual(audit_token_to_pidversion(myAuditToken), 0); +} + +- (void)testMuteSelf { + auto mockESApi = std::make_shared(); + SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi]; + + EXPECT_CALL(*mockESApi, MuteProcess) + .WillOnce(testing::Return(true)) + .WillOnce(testing::Return(false)); + + XCTAssertTrue([client muteSelf]); + XCTAssertFalse([client muteSelf]); + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testClearCache { + auto mockESApi = std::make_shared(); + SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi]; + + // Test the underlying clear cache impl returning both true and false + EXPECT_CALL(*mockESApi, ClearCache) + .WillOnce(testing::Return(true)) + .WillOnce(testing::Return(false)); + + XCTAssertTrue([client clearCache]); + XCTAssertFalse([client clearCache]); + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testSubscribe { + auto mockESApi = std::make_shared(); + SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi]; + + std::set events = { + ES_EVENT_TYPE_NOTIFY_CLOSE, + ES_EVENT_TYPE_NOTIFY_EXIT, + }; + + // Test the underlying subscribe impl returning both true and false + EXPECT_CALL(*mockESApi, Subscribe(testing::_, events)) + .WillOnce(testing::Return(true)) + .WillOnce(testing::Return(false)); + + XCTAssertTrue([client subscribe:events]); + XCTAssertFalse([client subscribe:events]); + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testSubscribeAndClearCache { + auto mockESApi = std::make_shared(); + SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi]; + + // Have subscribe fail the first time, meaning clear cache only called once. + EXPECT_CALL(*mockESApi, ClearCache) + .After(EXPECT_CALL(*mockESApi, Subscribe) + .WillOnce(testing::Return(false)) + .WillOnce(testing::Return(true))) + .WillOnce(testing::Return(true)); + + XCTAssertFalse([client subscribeAndClearCache:{}]); + XCTAssertTrue([client subscribeAndClearCache:{}]); + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testRespondToMessageWithAuthResultCacheable { + es_message_t esMsg; + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + es_auth_result_t result = ES_AUTH_RESULT_DENY; + bool cacheable = true; + + // Have subscribe fail the first time, meaning clear cache only called once. + EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, result, cacheable)) + .WillOnce(testing::Return(true)); + + SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi]; + + { + Message msg(mockESApi, &esMsg); + XCTAssertTrue([client respondToMessage:msg withAuthResult:result cacheable:cacheable]); + } + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testProcessEnrichedMessageHandler { + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + auto mockESApi = std::make_shared(); + + // Note: In this test, `RetainMessage` isn't setup to return anything. This + // means that the underlying `es_msg_` in the `Message` object is NULL, and + // therefore no call to `ReleaseMessage` is ever made (hence no expectations). + // Because we don't need to operate on the es_msg_, this simplifies the test. + EXPECT_CALL(*mockESApi, RetainMessage); + + SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi]; + + es_message_t esMsg; + auto enrichedMsg = std::make_shared( + EnrichedClose(Message(mockESApi, &esMsg), + EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt, + EnrichedFile(std::nullopt, std::nullopt, std::nullopt)), + EnrichedFile(std::nullopt, std::nullopt, std::nullopt))); + + [client processEnrichedMessage:enrichedMsg + handler:^(std::shared_ptr msg) { + dispatch_semaphore_signal(sema); + }]; + + XCTAssertEqual(0, + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)), + "Handler block not called within expected time window"); + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testIsDatabasePath { + XCTAssertTrue([SNTEndpointSecurityClient isDatabasePath:"/private/var/db/santa/rules.db"]); + XCTAssertTrue([SNTEndpointSecurityClient isDatabasePath:"/private/var/db/santa/events.db"]); + + XCTAssertFalse([SNTEndpointSecurityClient isDatabasePath:"/not/a/db/path"]); +} + +- (void)testProcessMessageHandlerBadEventType { + es_file_t proc_file = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&proc_file); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc); + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi]; + + { + XCTAssertThrows([client processMessage:Message(mockESApi, &esMsg) + handler:^(const Message &msg){ + }]); + } + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +// Note: This test triggers a leak warning on the mock object, however it is +// benign. The dispatch block to handle deadline expiration in +// `processMessage:handler:` will retain the mock object an extra time. +// But since this test sets a long deadline in order to ensure the handler block +// runs first, the deadline handler block will not have finished executing by +// the time the test exits, making GMock think the object was leaked. +- (void)testProcessMessageHandler { + es_file_t proc_file = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&proc_file); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &proc, ActionType::Auth, + 45 * 1000); // Long deadline to not hit + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + + SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi]; + + { + XCTAssertNoThrow([client processMessage:Message(mockESApi, &esMsg) + handler:^(const Message &msg) { + dispatch_semaphore_signal(sema); + }]); + } + + XCTAssertEqual(0, + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)), + "Handler block not called within expected time window"); + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testProcessMessageHandlerWithDeadlineTimeout { + // Set a es_message_t deadline of 750ms + // Set a deadline leeway in the `SNTEndpointSecurityClient` of 500ms + // Mock `RespondAuthResult` which is called from the deadline handler + // Signal the semaphore from the mock + // Wait a few seconds for the semaphore (should take ~250ms) + // + // Two semaphotes are used: + // 1. deadlineSema - used to wait in the handler block until the deadline + // block has a chance to execute + // 2. controlSema - used to block control flow in the test until the + // deadlineSema is signaled (or a timeout waiting on deadlineSema) + es_file_t proc_file = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&proc_file); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &proc, ActionType::Auth, + 750); // 750ms timeout + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + dispatch_semaphore_t deadlineSema = dispatch_semaphore_create(0); + dispatch_semaphore_t controlSema = dispatch_semaphore_create(0); + + EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_DENY, false)) + .WillOnce(testing::InvokeWithoutArgs(^() { + // Signal deadlineSema to let the handler block continue execution + dispatch_semaphore_signal(deadlineSema); + return true; + })); + + SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi]; + client.deadlineMarginMS = 500; + + { + __block long result; + XCTAssertNoThrow([client processMessage:Message(mockESApi, &esMsg) + handler:^(const Message &msg) { + result = dispatch_semaphore_wait( + deadlineSema, + dispatch_time(DISPATCH_TIME_NOW, 4 * NSEC_PER_SEC)); + + // Once done waiting on deadlineSema, trigger controlSema to + // continue test + dispatch_semaphore_signal(controlSema); + }]); + + XCTAssertEqual( + 0, dispatch_semaphore_wait(controlSema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)), + "Control sema not signaled within expected time window"); + + XCTAssertEqual(result, 0); + } + + // Allow some time for the threads in `processMessage:handler:` to finish. + // It isn't critical that they do, but if the dispatch blocks don't complete + // we may get warnings from GMock about calls to ReleaseMessage after + // verifying and clearing. Sleep a little bit here to reduce chances of + // seeing the warning (but still possible) + SleepMS(100); + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +@end diff --git a/Source/santad/EventProviders/SNTDeviceManager.h b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h similarity index 53% rename from Source/santad/EventProviders/SNTDeviceManager.h rename to Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h index 9a0202d86..2f4242096 100644 --- a/Source/santad/EventProviders/SNTDeviceManager.h +++ b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h @@ -1,4 +1,4 @@ -/// Copyright 2021 Google Inc. All rights reserved. +/// Copyright 2021-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -11,12 +11,16 @@ /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. -#import -#import -#include +#include +#import -#include "Source/common/SNTDeviceEvent.h" +#import "Source/common/SNTDeviceEvent.h" +#import "Source/santad/EventProviders/AuthResultCache.h" +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h" +#include "Source/santad/Logs/EndpointSecurity/Logger.h" NS_ASSUME_NONNULL_BEGIN @@ -26,16 +30,18 @@ typedef void (^SNTDeviceBlockCallback)(SNTDeviceEvent *event); * Manages DiskArbitration and EndpointSecurity to monitor/block/remount USB * storage devices. */ -@interface SNTDeviceManager : NSObject +@interface SNTEndpointSecurityDeviceManager + : SNTEndpointSecurityClient -@property(nonatomic, readwrite) BOOL subscribed; @property(nonatomic, readwrite) BOOL blockUSBMount; @property(nonatomic, readwrite, nullable) NSArray *remountArgs; @property(nonatomic, nullable) SNTDeviceBlockCallback deviceBlockCallback; -- (instancetype)init; -- (void)listen; -- (BOOL)subscribed; +- (instancetype) + initWithESAPI: + (std::shared_ptr)esApi + logger:(std::shared_ptr)logger + authResultCache:(std::shared_ptr)authResultCache; @end diff --git a/Source/santad/EventProviders/SNTDeviceManager.mm b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.mm similarity index 51% rename from Source/santad/EventProviders/SNTDeviceManager.mm rename to Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.mm index b4c0a078e..6130568c0 100644 --- a/Source/santad/EventProviders/SNTDeviceManager.mm +++ b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.mm @@ -1,4 +1,4 @@ -/// Copyright 2021 Google Inc. All rights reserved. +/// Copyright 2021-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -11,21 +11,39 @@ /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. -#import "Source/santad/EventProviders/SNTDeviceManager.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h" #import +#include #import +#include +#include + #include #include #include #include -#include -#include #import "Source/common/SNTDeviceEvent.h" #import "Source/common/SNTLogging.h" -#import "Source/santad/Logs/SNTEventLog.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" + +using santa::santad::event_providers::AuthResultCache; +using santa::santad::event_providers::FlushCacheMode; +using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI; +using santa::santad::event_providers::endpoint_security::Message; +using santa::santad::logs::endpoint_security::Logger; + +@interface SNTEndpointSecurityDeviceManager () + +- (void)logDiskAppeared:(NSDictionary *)props; +- (void)logDiskDisappeared:(NSDictionary *)props; + +@property DASessionRef diskArbSession; +@property(nonatomic, readonly) dispatch_queue_t diskQueue; + +@end void diskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context) { if (dissenter) { @@ -36,17 +54,18 @@ void diskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context IOReturn subSystemCode = err_get_sub(status); IOReturn errorCode = err_get_code(status); - LOGE( - @"SNTDeviceManager: dissenter status codes: system: %d, subsystem: %d, err: %d; status: %s", - systemCode, subSystemCode, errorCode, [statusString UTF8String]); + LOGE(@"SNTEndpointSecurityDeviceManager: dissenter status codes: system: %d, subsystem: %d, " + @"err: %d; status: %s", + systemCode, subSystemCode, errorCode, [statusString UTF8String]); } } void diskAppearedCallback(DADiskRef disk, void *context) { NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk)); if (![props[@"DAVolumeMountable"] boolValue]) return; - SNTEventLog *logger = [SNTEventLog logger]; - if (logger) [logger logDiskAppeared:props]; + SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context; + + [dm logDiskAppeared:props]; } void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *context) { @@ -54,8 +73,9 @@ void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *conte if (![props[@"DAVolumeMountable"] boolValue]) return; if (props[@"DAVolumePath"]) { - SNTEventLog *logger = [SNTEventLog logger]; - if (logger) [logger logDiskAppeared:props]; + SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context; + + [dm logDiskAppeared:props]; } } @@ -63,8 +83,9 @@ void diskDisappearedCallback(DADiskRef disk, void *context) { NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk)); if (![props[@"DAVolumeMountable"] boolValue]) return; - SNTEventLog *logger = [SNTEventLog logger]; - if (logger) [logger logDiskDisappeared:props]; + SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context; + + [dm logDiskDisappeared:props]; } NSArray *maskToMountArgs(long remountOpts) { @@ -101,126 +122,96 @@ long mountArgsToMask(NSArray *args) { else if ([arg isEqualToString:@"async"]) flags |= MNT_ASYNC; else - LOGE(@"SNTDeviceManager: unexpected mount arg: %@", arg); + LOGE(@"SNTEndpointSecurityDeviceManager: unexpected mount arg: %@", arg); } return flags; } NS_ASSUME_NONNULL_BEGIN -@interface SNTDeviceManager () - -@property DASessionRef diskArbSession; -@property(nonatomic, readonly) es_client_t *client; -@property(nonatomic, readonly) dispatch_queue_t esAuthQueue; -@property(nonatomic, readonly) dispatch_queue_t diskQueue; -@end - -@implementation SNTDeviceManager +@implementation SNTEndpointSecurityDeviceManager { + std::shared_ptr _authResultCache; + std::shared_ptr _logger; +} -- (instancetype)init API_AVAILABLE(macos(10.15)) { - self = [super init]; +- (instancetype)initWithESAPI:(std::shared_ptr)esApi + logger:(std::shared_ptr)logger + authResultCache:(std::shared_ptr)authResultCache { + self = [super initWithESAPI:std::move(esApi)]; if (self) { + _logger = logger; _blockUSBMount = false; - _diskQueue = dispatch_queue_create("com.google.santad.disk_queue", DISPATCH_QUEUE_SERIAL); - - _esAuthQueue = - dispatch_queue_create("com.google.santa.daemon.es_device_auth", DISPATCH_QUEUE_CONCURRENT); + _diskQueue = dispatch_queue_create("com.google.santa.daemon.disk_queue", DISPATCH_QUEUE_SERIAL); _diskArbSession = DASessionCreate(NULL); DASessionSetDispatchQueue(_diskArbSession, _diskQueue); - if (@available(macos 10.15, *)) [self initES]; + [self establishClientOrDie]; } return self; } -- (void)initES API_AVAILABLE(macos(10.15)) { - while (!self.client) { - es_client_t *client = NULL; - es_new_client_result_t ret = es_new_client(&client, ^(es_client_t *c, const es_message_t *m) { - // Set timeout to 5 seconds before the ES deadline. - [self handleESMessageWithTimeout:m - withClient:c - timeout:dispatch_time(m->deadline, NSEC_PER_SEC * -5)]; - }); - - switch (ret) { - case ES_NEW_CLIENT_RESULT_SUCCESS: - LOGI(@"Connected to EndpointSecurity"); - _client = client; - return; - case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED: - LOGE(@"Unable to create EndpointSecurity client, not full-disk access permitted"); - LOGE(@"Sleeping for 30s before restarting."); - sleep(30); - exit(ret); - default: - LOGE(@"Unable to create es client: %d. Sleeping for a minute.", ret); - sleep(60); - continue; - } - } +- (void)logDiskAppeared:(NSDictionary *)props { + self->_logger->LogDiskAppeared(props); } -- (void)listenES API_AVAILABLE(macos(10.15)) { - while (!self.client) - usleep(100000); // 100ms +- (void)logDiskDisappeared:(NSDictionary *)props { + self->_logger->LogDiskDisappeared(props); +} - es_event_type_t events[] = { - ES_EVENT_TYPE_AUTH_MOUNT, - ES_EVENT_TYPE_AUTH_REMOUNT, - }; +- (void)handleMessage:(Message &&)esMsg { + if (!self.blockUSBMount) { + // TODO: We should also unsubscribe from events when this isn't set, but + // this is generally a low-volume event type. + [self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:false]; + return; + } - es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t)); - if (sret != ES_RETURN_SUCCESS) - LOGE(@"SNTDeviceManager: unable to subscribe to auth mount events: %d", sret); + if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_UNMOUNT) { + self->_authResultCache->FlushCache(FlushCacheMode::kNonRootOnly); + return; + } + + [self processMessage:std::move(esMsg) + handler:^(const Message &msg) { + es_auth_result_t result = [self handleAuthMount:msg]; + [self respondToMessage:msg withAuthResult:result cacheable:false]; + }]; } -- (void)listenDA { +- (void)enable { DARegisterDiskAppearedCallback(_diskArbSession, NULL, diskAppearedCallback, (__bridge void *)self); DARegisterDiskDescriptionChangedCallback(_diskArbSession, NULL, NULL, diskDescriptionChangedCallback, (__bridge void *)self); DARegisterDiskDisappearedCallback(_diskArbSession, NULL, diskDisappearedCallback, (__bridge void *)self); -} -- (void)listen { - [self listenDA]; - if (@available(macos 10.15, *)) [self listenES]; - self.subscribed = YES; + [super subscribeAndClearCache:{ + ES_EVENT_TYPE_AUTH_MOUNT, + ES_EVENT_TYPE_AUTH_REMOUNT, + ES_EVENT_TYPE_NOTIFY_UNMOUNT, + }]; } -- (void)handleAuthMount:(const es_message_t *)m - withClient:(es_client_t *)c API_AVAILABLE(macos(10.15)) { - if (!self.blockUSBMount) { - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false); - return; - } - +- (es_auth_result_t)handleAuthMount:(const Message &)m { struct statfs *eventStatFS; - BOOL isRemount = NO; switch (m->event_type) { case ES_EVENT_TYPE_AUTH_MOUNT: eventStatFS = m->event.mount.statfs; break; - case ES_EVENT_TYPE_AUTH_REMOUNT: - eventStatFS = m->event.remount.statfs; - isRemount = YES; - break; + case ES_EVENT_TYPE_AUTH_REMOUNT: eventStatFS = m->event.remount.statfs; break; default: + // This is a programming error LOGE(@"Unexpected Event Type passed to DeviceManager handleAuthMount: %d", m->event_type); - // Fail closed. - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false); - assert(0 && "SNTDeviceManager: unexpected event type"); - return; + exit(EXIT_FAILURE); } long mountMode = eventStatFS->f_flags; pid_t pid = audit_token_to_pid(m->process->audit_token); - LOGD(@"SNTDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu", - m->process->executable->path.data, pid, mountMode); + LOGD( + @"SNTEndpointSecurityDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu", + m->process->executable->path.data, pid, mountMode); DADiskRef disk = DADiskCreateFromBSDName(NULL, self.diskArbSession, eventStatFS->f_mntfromname); CFAutorelease(disk); @@ -232,12 +223,13 @@ - (void)handleAuthMount:(const es_message_t *)m BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue]; NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey]; BOOL isUSB = [protocol isEqualToString:@"USB"]; - BOOL isVirtual = [protocol isEqualToString: @"Virtual Interface"]; + BOOL isVirtual = [protocol isEqualToString:@"Virtual Interface"]; NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey]; // TODO: check kind and protocol for banned things (e.g. MTP). - LOGD(@"SNTDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d isRemovable: %d " + LOGD(@"SNTEndpointSecurityDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d " + @"isRemovable: %d " @"isEjectable: %d", protocol, kind, isInternal, isRemovable, isEjectable); @@ -245,8 +237,7 @@ - (void)handleAuthMount:(const es_message_t *)m // also are okay with operations for devices that are non-removal as long as // they are NOT a USB device. if (isInternal || isVirtual || (!isRemovable && !isEjectable && !isUSB)) { - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false); - return; + return ES_AUTH_RESULT_ALLOW; } SNTDeviceEvent *event = [[SNTDeviceEvent alloc] @@ -259,17 +250,16 @@ - (void)handleAuthMount:(const es_message_t *)m event.remountArgs = self.remountArgs; long remountOpts = mountArgsToMask(self.remountArgs); - LOGD(@"SNTDeviceManager: mountMode: %@", maskToMountArgs(mountMode)); - LOGD(@"SNTDeviceManager: remountOpts: %@", maskToMountArgs(remountOpts)); + LOGD(@"SNTEndpointSecurityDeviceManager: mountMode: %@", maskToMountArgs(mountMode)); + LOGD(@"SNTEndpointSecurityDeviceManager: remountOpts: %@", maskToMountArgs(remountOpts)); - if ((mountMode & remountOpts) == remountOpts && !isRemount) { - LOGD(@"SNTDeviceManager: Allowing as mount as flags match remountOpts"); - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false); - return; + if ((mountMode & remountOpts) == remountOpts && m->event_type != ES_EVENT_TYPE_AUTH_REMOUNT) { + LOGD(@"SNTEndpointSecurityDeviceManager: Allowing as mount as flags match remountOpts"); + return ES_AUTH_RESULT_ALLOW; } long newMode = mountMode | remountOpts; - LOGI(@"SNTDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)", + LOGI(@"SNTEndpointSecurityDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)", eventStatFS->f_mntfromname, eventStatFS->f_mntonname, mountMode, newMode); [self remount:disk mountMode:newMode]; } @@ -278,7 +268,7 @@ - (void)handleAuthMount:(const es_message_t *)m self.deviceBlockCallback(event); } - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false); + return ES_AUTH_RESULT_DENY; } - (void)remount:(DADiskRef)disk mountMode:(long)remountMask { @@ -293,66 +283,6 @@ - (void)remount:(DADiskRef)disk mountMode:(long)remountMask { free(argv); } -// handleESMessage handles an ES message synchronously. This will block all incoming ES events -// until either we serve a response or we hit the auth deadline. Prefer [SNTDeviceManager -// handleESMessageWithTimeout] -// TODO(tnek): generalize this timeout handling logic so that EndpointSecurityManager can use it -// too. -- (void)handleESMessageWithTimeout:(const es_message_t *)m - withClient:(es_client_t *)c - timeout:(dispatch_time_t)timeout API_AVAILABLE(macos(10.15)) { - // ES will kill our whole client if we don't meet the es_message auth deadline, so we try to - // gracefully handle it with a deny-by-default in the worst-case before it can do that. - // This isn't an issue for notify events, so we're in no rush for those. - es_message_t *mc = es_copy_message(m); - - dispatch_semaphore_t processingSema = dispatch_semaphore_create(0); - // Add 1 to the processing semaphore. We're not creating it with a starting - // value of 1 because that requires that the semaphore is not deallocated - // until its value matches the starting value, which we don't need. - dispatch_semaphore_signal(processingSema); - dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0); - - if (mc->action_type == ES_ACTION_TYPE_AUTH) { - dispatch_after(timeout, self.esAuthQueue, ^(void) { - if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) { - // Handler already responded, nothing to do. - return; - } - LOGE(@"SNTDeviceManager: deadline reached: deny pid=%d ret=%d", - audit_token_to_pid(mc->process->audit_token), - es_respond_auth_result(c, mc, ES_AUTH_RESULT_DENY, false)); - dispatch_semaphore_signal(deadlineExpiredSema); - }); - } - - dispatch_async(self.esAuthQueue, ^{ - [self handleESMessage:mc withClient:c]; - if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) { - // Deadline expired, wait for deadline block to finish. - dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER); - } - es_free_message(mc); - }); -} - -- (void)handleESMessage:(const es_message_t *)m - withClient:(es_client_t *)c API_AVAILABLE(macos(10.15)) { - switch (m->event_type) { - case ES_EVENT_TYPE_AUTH_REMOUNT: { - [[fallthrough]]; - } - case ES_EVENT_TYPE_AUTH_MOUNT: { - [self handleAuthMount:m withClient:c]; - break; - } - - default: - LOGE(@"SNTDeviceManager: unexpected event type: %d", m->event_type); - break; - } -} - @end NS_ASSUME_NONNULL_END diff --git a/Source/santad/EventProviders/SNTEndpointSecurityDeviceManagerTest.mm b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManagerTest.mm new file mode 100644 index 000000000..21a1d6896 --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManagerTest.mm @@ -0,0 +1,313 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import +#include +#import +#import +#import +#import +#include +#include +#include +#include "gmock/gmock.h" + +#include +#include + +#import "Source/common/SNTConfigurator.h" +#import "Source/common/SNTDeviceEvent.h" +#include "Source/common/TestUtils.h" +#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" +#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h" + +using santa::santad::event_providers::endpoint_security::Message; + +@interface SNTEndpointSecurityDeviceManager (Testing) +- (void)logDiskAppeared:(NSDictionary *)props; +@end + +@interface SNTEndpointSecurityDeviceManagerTest : XCTestCase +@property id mockConfigurator; +@property MockDiskArbitration *mockDA; +@end + +@implementation SNTEndpointSecurityDeviceManagerTest + +- (void)setUp { + [super setUp]; + + self.mockConfigurator = OCMClassMock([SNTConfigurator class]); + OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator); + OCMStub([self.mockConfigurator eventLogType]).andReturn(-1); + + self.mockDA = [MockDiskArbitration mockDiskArbitration]; + [self.mockDA reset]; + + fclose(stdout); +} + +- (void)triggerTestMountEvent:(es_event_type_t)eventType + diskInfoOverrides:(NSDictionary *)diskInfo + expectedAuthResult:(es_auth_result_t)expectedAuthResult + deviceManagerSetup:(void (^)(SNTEndpointSecurityDeviceManager *))setupDMCallback { + struct statfs fs = {0}; + NSString *test_mntfromname = @"/dev/disk2s1"; + NSString *test_mntonname = @"/Volumes/KATE'S 4G"; + + strncpy(fs.f_mntfromname, [test_mntfromname UTF8String], sizeof(fs.f_mntfromname)); + strncpy(fs.f_mntonname, [test_mntonname UTF8String], sizeof(fs.f_mntonname)); + + MockDADisk *disk = [[MockDADisk alloc] init]; + disk.diskDescription = @{ + (__bridge NSString *)kDADiskDescriptionDeviceProtocolKey : @"USB", + (__bridge NSString *)kDADiskDescriptionMediaRemovableKey : @YES, + @"DAVolumeMountable" : @YES, + @"DAVolumePath" : test_mntonname, + @"DADeviceModel" : @"Some device model", + @"DADevicePath" : test_mntonname, + @"DADeviceVendor" : @"Some vendor", + @"DAAppearanceTime" : @0, + @"DAMediaBSDName" : test_mntfromname, + }; + + if (diskInfo != nil) { + NSMutableDictionary *mergedDiskDescription = [disk.diskDescription mutableCopy]; + for (NSString *key in diskInfo) { + mergedDiskDescription[key] = diskInfo[key]; + } + disk.diskDescription = (NSDictionary *)mergedDiskDescription; + } + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsESNewClient(); + + SNTEndpointSecurityDeviceManager *deviceManager = + [[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi + logger:nullptr + authResultCache:nullptr]; + + setupDMCallback(deviceManager); + + // Stub the log method since a mock `Logger` object isn't used. + id partialDeviceManager = OCMPartialMock(deviceManager); + OCMStub([partialDeviceManager logDiskAppeared:OCMOCK_ANY]); + + [self.mockDA insert:disk bsdName:test_mntfromname]; + + es_file_t file = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&file); + es_message_t esMsg = MakeESMessage(eventType, &proc, ActionType::Auth, 6000); + // Need a pointer to esMsg to capture in blocks below. + es_message_t *heapESMsg = &esMsg; + + __block int retainCount = 0; + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + EXPECT_CALL(*mockESApi, ReleaseMessage).WillRepeatedly(^{ + if (retainCount == 0) { + XCTFail(@"Under retain!"); + } + retainCount--; + if (retainCount == 0) { + dispatch_semaphore_signal(sema); + } + }); + EXPECT_CALL(*mockESApi, RetainMessage).WillRepeatedly(^{ + retainCount++; + return heapESMsg; + }); + + if (eventType == ES_EVENT_TYPE_AUTH_MOUNT) { + esMsg.event.mount.statfs = &fs; + } else if (eventType == ES_EVENT_TYPE_AUTH_REMOUNT) { + esMsg.event.remount.statfs = &fs; + } else { + // Programming error. Fail the test. + XCTFail(@"Unhandled event type in test: %d", eventType); + } + + XCTestExpectation *mountExpectation = + [self expectationWithDescription:@"Wait for response from ES"]; + + EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, expectedAuthResult, false)) + .WillOnce(testing::InvokeWithoutArgs(^bool { + [mountExpectation fulfill]; + return true; + })); + + [deviceManager handleMessage:Message(mockESApi, &esMsg)]; + + [self waitForExpectations:@[ mountExpectation ] timeout:60.0]; + + XCTAssertEqual(0, + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)), + "Failed waiting for message to be processed..."); + + [partialDeviceManager stopMocking]; + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testUSBBlockDisabled { + [self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT + diskInfoOverrides:nil + expectedAuthResult:ES_AUTH_RESULT_ALLOW + deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) { + dm.blockUSBMount = NO; + }]; +} + +- (void)testRemount { + NSArray *wantRemountArgs = @[ @"noexec", @"rdonly" ]; + + XCTestExpectation *expectation = + [self expectationWithDescription: + @"Wait for SNTEndpointSecurityDeviceManager's blockCallback to trigger"]; + + __block NSString *gotmntonname, *gotmntfromname; + __block NSArray *gotRemountedArgs; + + [self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT + diskInfoOverrides:nil + expectedAuthResult:ES_AUTH_RESULT_DENY + deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) { + dm.blockUSBMount = YES; + dm.remountArgs = wantRemountArgs; + + dm.deviceBlockCallback = ^(SNTDeviceEvent *event) { + gotRemountedArgs = event.remountArgs; + gotmntonname = event.mntonname; + gotmntfromname = event.mntfromname; + [expectation fulfill]; + }; + }]; + + XCTAssertEqual(self.mockDA.wasRemounted, YES); + + [self waitForExpectations:@[ expectation ] timeout:60.0]; + + XCTAssertEqualObjects(gotRemountedArgs, wantRemountArgs); + XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G"); + XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1"); +} + +- (void)testBlockNoRemount { + XCTestExpectation *expectation = + [self expectationWithDescription: + @"Wait for SNTEndpointSecurityDeviceManager's blockCallback to trigger"]; + + __block NSString *gotmntonname, *gotmntfromname; + __block NSArray *gotRemountedArgs; + + [self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT + diskInfoOverrides:nil + expectedAuthResult:ES_AUTH_RESULT_DENY + deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) { + dm.blockUSBMount = YES; + + dm.deviceBlockCallback = ^(SNTDeviceEvent *event) { + gotRemountedArgs = event.remountArgs; + gotmntonname = event.mntonname; + gotmntfromname = event.mntfromname; + [expectation fulfill]; + }; + }]; + + [self waitForExpectations:@[ expectation ] timeout:60.0]; + + XCTAssertNil(gotRemountedArgs); + XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G"); + XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1"); +} + +- (void)testEnsureRemountsCannotChangePerms { + NSArray *wantRemountArgs = @[ @"noexec", @"rdonly" ]; + + XCTestExpectation *expectation = + [self expectationWithDescription: + @"Wait for SNTEndpointSecurityDeviceManager's blockCallback to trigger"]; + + __block NSString *gotmntonname, *gotmntfromname; + __block NSArray *gotRemountedArgs; + + [self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT + diskInfoOverrides:nil + expectedAuthResult:ES_AUTH_RESULT_DENY + deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) { + dm.blockUSBMount = YES; + dm.remountArgs = wantRemountArgs; + + dm.deviceBlockCallback = ^(SNTDeviceEvent *event) { + gotRemountedArgs = event.remountArgs; + gotmntonname = event.mntonname; + gotmntfromname = event.mntfromname; + [expectation fulfill]; + }; + }]; + + XCTAssertEqual(self.mockDA.wasRemounted, YES); + + [self waitForExpectations:@[ expectation ] timeout:10.0]; + + XCTAssertEqualObjects(gotRemountedArgs, wantRemountArgs); + XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G"); + XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1"); +} + +- (void)testEnsureDMGsDoNotPrompt { + NSArray *wantRemountArgs = @[ @"noexec", @"rdonly" ]; + NSDictionary *diskInfo = @{ + (__bridge NSString *)kDADiskDescriptionDeviceProtocolKey : @"Virtual Interface", + (__bridge NSString *)kDADiskDescriptionDeviceModelKey : @"Disk Image", + (__bridge NSString *)kDADiskDescriptionMediaNameKey : @"disk image", + }; + + [self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT + diskInfoOverrides:diskInfo + expectedAuthResult:ES_AUTH_RESULT_ALLOW + deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) { + dm.blockUSBMount = YES; + dm.remountArgs = wantRemountArgs; + + dm.deviceBlockCallback = ^(SNTDeviceEvent *event) { + XCTFail(@"Should not be called"); + }; + }]; + + XCTAssertEqual(self.mockDA.wasRemounted, NO); +} + +- (void)testEnable { + // Ensure the client subscribes to expected event types + std::set expectedEventSubs{ + ES_EVENT_TYPE_AUTH_MOUNT, + ES_EVENT_TYPE_AUTH_REMOUNT, + ES_EVENT_TYPE_NOTIFY_UNMOUNT, + }; + auto mockESApi = std::make_shared(); + + id deviceClient = [[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi]; + + EXPECT_CALL(*mockESApi, ClearCache(testing::_)) + .After(EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs)) + .WillOnce(testing::Return(true))) + .WillOnce(testing::Return(true)); + + [deviceClient enable]; + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h b/Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h new file mode 100644 index 000000000..27b004122 --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h @@ -0,0 +1,32 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import + +#include "Source/common/SNTCommon.h" + +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" + +// Protocol that all subclasses of `SNTEndpointSecurityClient` should adhere to. +@protocol SNTEndpointSecurityEventHandler + +// Called Synchronously and serially for each message provided by the +// EndpointSecurity framework. +- (void)handleMessage:(santa::santad::event_providers::endpoint_security::Message &&)esMsg; + +// Called after Santa has finished initializing itself. +// This is an optimal place to subscribe to ES events +- (void)enable; + +@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityManager.h b/Source/santad/EventProviders/SNTEndpointSecurityManager.h deleted file mode 100644 index 047b095e7..000000000 --- a/Source/santad/EventProviders/SNTEndpointSecurityManager.h +++ /dev/null @@ -1,37 +0,0 @@ -/// Copyright 2019 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import - -#include "Source/common/SNTCommon.h" -#include "Source/santad/EventProviders/SNTEventProvider.h" - -#include - -@interface SNTEndpointSecurityManager : NSObject -- (santa_vnode_id_t)vnodeIDForFile:(es_file_t *)file; - -- (BOOL)isCompilerPID:(pid_t)pid; -- (void)setIsCompilerPID:(pid_t)pid; -- (void)setNotCompilerPID:(pid_t)pid; - -// Returns YES if the path was truncated. -// The populated buffer will be NUL terminated. -+ (BOOL)populateBufferFromESFile:(es_file_t *)file buffer:(char *)buffer size:(size_t)size; - -@property(nonatomic, copy) void (^decisionCallback)(santa_message_t); -@property(nonatomic, copy) void (^logCallback)(santa_message_t); -@property(readonly, nonatomic) es_client_t *client; - -@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityManager.mm b/Source/santad/EventProviders/SNTEndpointSecurityManager.mm deleted file mode 100644 index 42cc60aaf..000000000 --- a/Source/santad/EventProviders/SNTEndpointSecurityManager.mm +++ /dev/null @@ -1,645 +0,0 @@ -/// Copyright 2019 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import "Source/santad/EventProviders/SNTEndpointSecurityManager.h" - -#include "Source/common/SNTPrefixTree.h" - -#import "Source/common/SNTConfigurator.h" -#import "Source/common/SNTLogging.h" -#import "Source/common/SantaCache.h" - -#include -#include -#include - -// Gleaned from https://opensource.apple.com/source/xnu/xnu-4903.241.1/bsd/sys/proc_internal.h -static const pid_t PID_MAX = 99999; - -@interface SNTEndpointSecurityManager () { - std::atomic _compilerPIDs[PID_MAX]; -} - -@property(nonatomic) SNTPrefixTree *prefixTree; -@property(nonatomic, readonly) dispatch_queue_t esAuthQueue; -@property(nonatomic, readonly) dispatch_queue_t esNotifyQueue; - -@end - -@implementation SNTEndpointSecurityManager - -- (instancetype)init API_AVAILABLE(macos(10.15)) { - self = [super init]; - if (self) { - // To avoid nil deref from es_events arriving before listenForDecisionRequests or - // listenForLogRequests in the MockEndpointSecurity testing util. - _decisionCallback = ^(santa_message_t) {}; - _logCallback = ^(santa_message_t) {}; - [self establishClient]; - [self muteSelf]; - _prefixTree = new SNTPrefixTree(); - _esAuthQueue = - dispatch_queue_create("com.google.santa.daemon.es_auth", DISPATCH_QUEUE_CONCURRENT); - dispatch_set_target_queue(_esAuthQueue, - dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)); - _esNotifyQueue = - dispatch_queue_create("com.google.santa.daemon.es_notify", DISPATCH_QUEUE_CONCURRENT); - dispatch_set_target_queue(_esNotifyQueue, dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)); - } - - return self; -} - -- (void)dealloc API_AVAILABLE(macos(10.15)) { - if (_client) { - es_unsubscribe_all(_client); - es_delete_client(_client); - } - if (_prefixTree) delete _prefixTree; -} - -- (void)muteSelf { - audit_token_t myAuditToken; - mach_msg_type_number_t count = TASK_AUDIT_TOKEN_COUNT; - if (task_info(mach_task_self(), TASK_AUDIT_TOKEN, (task_info_t)&myAuditToken, &count) == - KERN_SUCCESS) { - if (es_mute_process(self.client, &myAuditToken) == ES_RETURN_SUCCESS) { - return; - } else { - LOGE(@"Failed to mute this client's process, its events will not be muted."); - } - } else { - LOGE(@"Failed to fetch this client's audit token. Its events will not be muted."); - } - - // If we get here, Santa was unable to mute itself. Assume transitory and bail. - exit(EXIT_FAILURE); -} - -- (void)establishClient API_AVAILABLE(macos(10.15)) { - while (!self.client) { - SNTConfigurator *config = [SNTConfigurator configurator]; - - es_client_t *client = NULL; - es_new_client_result_t ret = es_new_client(&client, ^(es_client_t *c, const es_message_t *m) { - pid_t pid = audit_token_to_pid(m->process->audit_token); - int pidversion = audit_token_to_pidversion(m->process->audit_token); - - // If enabled, skip any action generated from another endpoint security client. - if (m->process->is_es_client && config.ignoreOtherEndpointSecurityClients) { - if (m->action_type == ES_ACTION_TYPE_AUTH) { - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false); - } - - return; - } - - // Perform the following checks on this serial queue. - // Some checks are simple filters that avoid copying m. - // However, the bulk of the work done here is to support transitive whitelisting. - switch (m->event_type) { - case ES_EVENT_TYPE_NOTIFY_EXEC: { - // Deny results are currently logged when ES_EVENT_TYPE_AUTH_EXEC posts a deny. - // TODO(bur/rah): For ES log denies from NOTIFY messages instead of AUTH. - if (m->action.notify.result.auth == ES_AUTH_RESULT_DENY) return; - - // Continue log this event - break; - } - case ES_EVENT_TYPE_NOTIFY_CLOSE: { - // Ignore unmodified files - if (!m->event.close.modified) return; - - // Remove from decision cache in case this is invalidating a cached binary. - [self removeCacheEntryForVnodeID:[self vnodeIDForFile:m->event.close.target]]; - - // Create a transitive rule if the file was modified by a running compiler - if ([self isCompilerPID:pid]) { - santa_message_t sm = {}; - BOOL truncated = - [SNTEndpointSecurityManager populateBufferFromESFile:m->event.close.target - buffer:sm.path - size:sizeof(sm.path)]; - if (truncated) { - LOGE(@"CLOSE: error creating transitive rule, the path is truncated: path=%s pid=%d", - sm.path, pid); - break; - } - if ([@(sm.path) hasPrefix:@"/dev/"]) { - break; - } - sm.action = ACTION_NOTIFY_WHITELIST; - sm.pid = pid; - sm.pidversion = pidversion; - LOGI(@"CLOSE: creating a transitive rule: path=%s pid=%d", sm.path, sm.pid); - self.decisionCallback(sm); - } - - // Continue log this event - break; - } - case ES_EVENT_TYPE_NOTIFY_RENAME: { - // Create a transitive rule if the file was renamed by a running compiler - if ([self isCompilerPID:pid]) { - santa_message_t sm = {}; - BOOL truncated = [self populateRenamedNewPathFromESMessage:m->event.rename - buffer:sm.path - size:sizeof(sm.path)]; - if (truncated) { - LOGE(@"RENAME: error creating transitive rule, the path is truncated: path=%s pid=%d", - sm.path, pid); - break; - } - if ([@(sm.path) hasPrefix:@"/dev/"]) { - break; - } - sm.action = ACTION_NOTIFY_WHITELIST; - sm.pid = pid; - sm.pidversion = pidversion; - LOGI(@"RENAME: creating a transitive rule: path=%s pid=%d", sm.path, sm.pid); - self.decisionCallback(sm); - } - - // Continue log this event - break; - } - case ES_EVENT_TYPE_NOTIFY_EXIT: { - // Update the set of running compiler PIDs - [self setNotCompilerPID:pid]; - - // Skip the standard pipeline and just log. - if (![config enableForkAndExitLogging]) return; - santa_message_t sm = {}; - sm.action = ACTION_NOTIFY_EXIT; - sm.pid = pid; - sm.pidversion = pidversion; - sm.ppid = m->process->original_ppid; - audit_token_t at = m->process->audit_token; - sm.uid = audit_token_to_ruid(at); - sm.gid = audit_token_to_rgid(at); - dispatch_async(self.esNotifyQueue, ^{ - self.logCallback(sm); - }); - return; - } - case ES_EVENT_TYPE_NOTIFY_UNMOUNT: { - // Flush the non-root cache - the root disk cannot be unmounted - // so it isn't necessary to flush its cache. - // - // Flushing the cache calls back into ES. We need to perform this off the handler thread - // otherwise we could potentially deadlock. - dispatch_async(self.esAuthQueue, ^() { - [self flushCacheNonRootOnly:YES]; - }); - - // Skip all other processing - return; - } - case ES_EVENT_TYPE_NOTIFY_FORK: { - // Skip the standard pipeline and just log. - if (![config enableForkAndExitLogging]) return; - santa_message_t sm = {}; - sm.action = ACTION_NOTIFY_FORK; - sm.ppid = m->event.fork.child->original_ppid; - audit_token_t at = m->event.fork.child->audit_token; - sm.pid = audit_token_to_pid(at); - sm.pidversion = audit_token_to_pidversion(at); - sm.uid = audit_token_to_ruid(at); - sm.gid = audit_token_to_rgid(at); - dispatch_async(self.esNotifyQueue, ^{ - self.logCallback(sm); - }); - return; - } - default: { - break; - } - } - - switch (m->action_type) { - case ES_ACTION_TYPE_AUTH: { - // Copy the message - es_message_t *mc = es_copy_message(m); - - dispatch_semaphore_t processingSema = dispatch_semaphore_create(0); - // Add 1 to the processing semaphore. We're not creating it with a starting - // value of 1 because that requires that the semaphore is not deallocated - // until its value matches the starting value, which we don't need. - dispatch_semaphore_signal(processingSema); - dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0); - - // Create a timer to deny the execution 5 seconds before the deadline, - // if a response hasn't already been sent. This block will still be enqueued if - // the the deadline - 5 secs is < DISPATCH_TIME_NOW. - // As of 10.15.5, a typical deadline is 60 seconds. - dispatch_after(dispatch_time(m->deadline, NSEC_PER_SEC * -5), self.esAuthQueue, ^(void) { - if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) { - // Handler has already responded, nothing to do. - return; - } - LOGE(@"SNTEndpointSecurityManager: deadline reached: deny pid=%d ret=%d", pid, - es_respond_auth_result(self.client, mc, ES_AUTH_RESULT_DENY, false)); - dispatch_semaphore_signal(deadlineExpiredSema); - }); - - // Dispatch off to the handler and return control to ES. - dispatch_async(self.esAuthQueue, ^{ - [self messageHandler:mc]; - if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) { - // Deadline expired, wait for deadline block to finish. - dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER); - } - es_free_message(mc); - }); - break; - } - case ES_ACTION_TYPE_NOTIFY: { - // Copy the message and return control back to ES - es_message_t *mc = es_copy_message(m); - dispatch_async(self.esNotifyQueue, ^{ - [self messageHandler:mc]; - es_free_message(mc); - }); - break; - } - default: { - break; - } - } - }); - - switch (ret) { - case ES_NEW_CLIENT_RESULT_SUCCESS: - LOGI(@"Connected to EndpointSecurity"); - _client = client; - return; - case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED: - LOGE(@"Unable to create EndpointSecurity client, not full-disk access permitted"); - LOGE(@"Sleeping for 30s before restarting."); - sleep(30); - exit(ret); - default: - LOGE(@"Unable to create es client: %d. Sleeping for a minute.", ret); - sleep(60); - continue; - } - } -} - -- (BOOL)respondFromCache:(es_message_t *)m API_AVAILABLE(macos(10.15)) { - return NO; -} - -- (void)messageHandler:(es_message_t *)m API_AVAILABLE(macos(10.15)) { - santa_message_t sm = {}; - sm.es_message = (void *)m; - - es_process_t *targetProcess = NULL; - es_file_t *targetFile = NULL; - void (^callback)(santa_message_t); - - switch (m->event_type) { - case ES_EVENT_TYPE_AUTH_EXEC: { - if ([self respondFromCache:m]) { - return; - } - - sm.action = ACTION_REQUEST_BINARY; - targetFile = m->event.exec.target->executable; - targetProcess = m->event.exec.target; - callback = self.decisionCallback; - - [SNTEndpointSecurityManager populateBufferFromESFile:m->process->tty - buffer:sm.ttypath - size:sizeof(sm.ttypath)]; - break; - } - case ES_EVENT_TYPE_NOTIFY_EXEC: { - sm.action = ACTION_NOTIFY_EXEC; - targetFile = m->event.exec.target->executable; - targetProcess = m->event.exec.target; - - // TODO(rah): Profile this, it might need to be improved. - uint32_t argCount = es_exec_arg_count(&(m->event.exec)); - NSMutableArray *args = [NSMutableArray arrayWithCapacity:argCount]; - for (int i = 0; i < argCount; ++i) { - es_string_token_t arg = es_exec_arg(&(m->event.exec), i); - NSString *argStr = [[NSString alloc] initWithBytes:arg.data - length:arg.length - encoding:NSUTF8StringEncoding]; - if (argStr.length) [args addObject:argStr]; - } - sm.args_array = (void *)CFBridgingRetain(args); - callback = self.logCallback; - break; - } - case ES_EVENT_TYPE_AUTH_UNLINK: { - es_string_token_t pathToken = m->event.unlink.target->path; - NSString *path = [[NSString alloc] initWithBytes:pathToken.data - length:pathToken.length - encoding:NSUTF8StringEncoding]; - if ([self isDatabasePath:path]) { - LOGW(@"Preventing attempt to delete Santa databases!"); - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true); - return; - } - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true); - return; - } - case ES_EVENT_TYPE_AUTH_RENAME: { - es_string_token_t pathToken = m->event.rename.source->path; - NSString *path = [[NSString alloc] initWithBytes:pathToken.data - length:pathToken.length - encoding:NSUTF8StringEncoding]; - - if ([self isDatabasePath:path]) { - LOGW(@"Preventing attempt to rename Santa databases!"); - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true); - return; - } - if (m->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) { - es_string_token_t destToken = m->event.rename.destination.existing_file->path; - NSString *destPath = [[NSString alloc] initWithBytes:destToken.data - length:destToken.length - encoding:NSUTF8StringEncoding]; - if ([self isDatabasePath:destPath]) { - LOGW(@"Preventing attempt to overwrite Santa databases!"); - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true); - return; - } - } - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true); - return; - } - case ES_EVENT_TYPE_AUTH_KEXTLOAD: { - es_string_token_t identifier = m->event.kextload.identifier; - NSString *ident = [[NSString alloc] initWithBytes:identifier.data - length:identifier.length - encoding:NSUTF8StringEncoding]; - if ([ident isEqualToString:@"com.google.santa-driver"]) { - LOGW(@"Preventing attempt to load Santa kext!"); - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true); - return; - } - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true); - return; - } - - case ES_EVENT_TYPE_NOTIFY_CLOSE: { - sm.action = ACTION_NOTIFY_WRITE; - targetFile = m->event.close.target; - targetProcess = m->process; - callback = self.logCallback; - break; - } - case ES_EVENT_TYPE_NOTIFY_UNLINK: { - sm.action = ACTION_NOTIFY_DELETE; - targetFile = m->event.unlink.target; - targetProcess = m->process; - callback = self.logCallback; - break; - } - case ES_EVENT_TYPE_NOTIFY_LINK: { - sm.action = ACTION_NOTIFY_LINK; - targetFile = m->event.link.source; - targetProcess = m->process; - NSString *p = @(m->event.link.target_dir->path.data); - p = [p stringByAppendingPathComponent:@(m->event.link.target_filename.data)]; - [SNTEndpointSecurityManager populateBufferFromString:p.UTF8String - buffer:sm.newpath - size:sizeof(sm.newpath)]; - callback = self.logCallback; - break; - } - case ES_EVENT_TYPE_NOTIFY_RENAME: { - sm.action = ACTION_NOTIFY_RENAME; - targetFile = m->event.rename.source; - targetProcess = m->process; - [self populateRenamedNewPathFromESMessage:m->event.rename - buffer:sm.newpath - size:sizeof(sm.newpath)]; - callback = self.logCallback; - break; - } - default: LOGE(@"Unknown es message: %d", m->event_type); return; - } - - // Deny auth exec events if the path doesn't fit in the santa message. - // TODO(bur/rah): Add support for larger paths. - if ([SNTEndpointSecurityManager populateBufferFromESFile:targetFile - buffer:sm.path - size:sizeof(sm.path)] && - m->event_type == ES_EVENT_TYPE_AUTH_EXEC) { - LOGE(@"path is truncated, deny: %s", sm.path); - es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false); - return; - } - - // Filter file op events matching the prefix tree. - if (!(m->event_type == ES_EVENT_TYPE_AUTH_EXEC || m->event_type == ES_EVENT_TYPE_NOTIFY_EXEC) && - self.prefixTree->HasPrefix(sm.path)) { - return; - } - - sm.vnode_id.fsid = targetFile->stat.st_dev; - sm.vnode_id.fileid = targetFile->stat.st_ino; - sm.uid = audit_token_to_ruid(targetProcess->audit_token); - sm.gid = audit_token_to_rgid(targetProcess->audit_token); - sm.pid = audit_token_to_pid(targetProcess->audit_token); - sm.pidversion = audit_token_to_pidversion(targetProcess->audit_token); - sm.ppid = targetProcess->original_ppid; - proc_name((m->event_type == ES_EVENT_TYPE_AUTH_EXEC) ? sm.ppid : sm.pid, sm.pname, 1024); - callback(sm); - if (sm.args_array) { - CFBridgingRelease(sm.args_array); - } -} - -- (void)listenForDecisionRequests:(void (^)(santa_message_t))callback API_AVAILABLE(macos(10.15)) { - while (!self.connectionEstablished) - usleep(100000); // 100ms - - self.decisionCallback = callback; - es_event_type_t events[] = { - ES_EVENT_TYPE_AUTH_EXEC, - ES_EVENT_TYPE_AUTH_UNLINK, - ES_EVENT_TYPE_AUTH_RENAME, - ES_EVENT_TYPE_AUTH_KEXTLOAD, - - // This is in the decision callback because it's used for detecting - // the exit of a 'compiler' used by transitive whitelisting. - ES_EVENT_TYPE_NOTIFY_EXIT, - - // This is in the decision callback because it's used for clearing the - // caches when a disk is unmounted. - ES_EVENT_TYPE_NOTIFY_UNMOUNT, - }; - es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t)); - if (sret != ES_RETURN_SUCCESS) LOGE(@"Unable to subscribe to auth events: %d", sret); - - // There's a gap between creating a client and subscribing to events. Creating the client - // triggers a cache flush automatically but any events that happen in this gap could be allowed - // and cached, so we force the cache to flush again. - [self flushCacheNonRootOnly:NO]; -} - -- (void)listenForLogRequests:(void (^)(santa_message_t))callback API_AVAILABLE(macos(10.15)) { - while (!self.connectionEstablished) - usleep(100000); // 100ms - - self.logCallback = callback; - es_event_type_t events[] = { - ES_EVENT_TYPE_NOTIFY_EXEC, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_LINK, - ES_EVENT_TYPE_NOTIFY_RENAME, ES_EVENT_TYPE_NOTIFY_UNLINK, ES_EVENT_TYPE_NOTIFY_FORK, - }; - es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t)); - if (sret != ES_RETURN_SUCCESS) LOGE(@"Unable to subscribe to notify events: %d", sret); -} - -- (int)postAction:(santa_action_t)action - forMessage:(santa_message_t)sm API_AVAILABLE(macos(10.15)) { - es_respond_result_t ret; - switch (action) { - case ACTION_RESPOND_ALLOW_COMPILER: - [self setIsCompilerPID:sm.pid]; - - // Allow the exec, but don't cache the decision so subsequent execs of the compiler get - // marked appropriately. - ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW, - false); - break; - case ACTION_RESPOND_ALLOW: - case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE: - ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW, - true); - break; - case ACTION_RESPOND_DENY: - case ACTION_RESPOND_TOOLONG: - ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_DENY, - false); - break; - case ACTION_RESPOND_ACK: return ES_RESPOND_RESULT_SUCCESS; - default: ret = ES_RESPOND_RESULT_ERR_INVALID_ARGUMENT; - } - - return ret; -} - -- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly API_AVAILABLE(macos(10.15)) { - if (!self.connectionEstablished) return YES; // if not connected, there's nothing to flush. - return es_clear_cache(self.client) == ES_CLEAR_CACHE_RESULT_SUCCESS; -} - -- (void)fileModificationPrefixFilterAdd:(NSArray *)filters { - for (NSString *filter in filters) { - self.prefixTree->AddPrefix(filter.fileSystemRepresentation); - } -} - -- (void)fileModificationPrefixFilterReset { - self.prefixTree->Reset(); -} - -- (NSArray *)cacheCounts { - return nil; -} - -- (NSArray *)cacheBucketCount { - return nil; -} - -- (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID { - return ACTION_UNSET; -} - -- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeId { - return KERN_FAILURE; -} - -- (BOOL)connectionEstablished { - return self.client != nil; -} - -#pragma mark helpers - -// Returns YES if the path was truncated. -// The populated buffer will be NUL terminated. -+ (BOOL)populateBufferFromESFile:(es_file_t *)file buffer:(char *)buffer size:(size_t)size { - if (file == NULL) return NO; - return [SNTEndpointSecurityManager populateBufferFromString:file->path.data - buffer:buffer - size:size]; -} - -// Returns YES if the path was truncated. -// The populated buffer will be NUL terminated. -+ (BOOL)populateBufferFromString:(const char *)string buffer:(char *)buffer size:(size_t)size { - return strlcpy(buffer, string, size) >= size; -} - -- (BOOL)populateRenamedNewPathFromESMessage:(es_event_rename_t)mv - buffer:(char *)buffer - size:(size_t)size { - BOOL truncated = NO; - switch (mv.destination_type) { - case ES_DESTINATION_TYPE_NEW_PATH: { - NSString *p = @(mv.destination.new_path.dir->path.data); - p = [p stringByAppendingPathComponent:@(mv.destination.new_path.filename.data)]; - truncated = [SNTEndpointSecurityManager populateBufferFromString:p.UTF8String - buffer:buffer - size:size]; - break; - } - case ES_DESTINATION_TYPE_EXISTING_FILE: { - truncated = [SNTEndpointSecurityManager populateBufferFromESFile:mv.destination.existing_file - buffer:buffer - size:size]; - break; - } - } - return truncated; -} - -- (santa_vnode_id_t)vnodeIDForFile:(es_file_t *)file { - return { - .fsid = (uint64_t)file->stat.st_dev, - .fileid = file->stat.st_ino, - }; -} - -- (BOOL)isDatabasePath:(NSString *)path { - return [path isEqualToString:@"/private/var/db/santa/rules.db"] || - [path isEqualToString:@"/private/var/db/santa/events.db"]; -} - -- (BOOL)isCompilerPID:(pid_t)pid { - return (pid && pid < PID_MAX && self->_compilerPIDs[pid].load()); -} - -- (void)setIsCompilerPID:(pid_t)pid { - if (pid < 1) { - LOGE(@"Unable to watch compiler pid=%d", pid); - } else if (pid >= PID_MAX) { - LOGE(@"Unable to watch compiler pid=%d >= PID_MAX(%d)", pid, PID_MAX); - } else { - self->_compilerPIDs[pid].store(true); - LOGD(@"Watching compiler pid=%d", pid); - } -} - -- (void)setNotCompilerPID:(pid_t)pid { - if (pid && pid < PID_MAX) self->_compilerPIDs[pid].store(false); -} - -@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityManagerTest.mm b/Source/santad/EventProviders/SNTEndpointSecurityManagerTest.mm deleted file mode 100644 index a280af8d2..000000000 --- a/Source/santad/EventProviders/SNTEndpointSecurityManagerTest.mm +++ /dev/null @@ -1,250 +0,0 @@ -/// Copyright 2021 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -#import -#import - -#import "Source/common/SNTConfigurator.h" -#import "Source/santad/EventProviders/SNTEndpointSecurityManager.h" - -// Must be imported last to overload libEndpointSecurity functions. -#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h" - -const NSString *const kEventsDBPath = @"/private/var/db/santa/events.db"; -const NSString *const kRulesDBPath = @"/private/var/db/santa/rules.db"; -const NSString *const kBenignPath = @"/some/other/path"; - -@interface SNTEndpointSecurityManagerTest : XCTestCase -@end - -@implementation SNTEndpointSecurityManagerTest - -- (void)setUp { - [super 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]; - (void)snt; // Make it appear used for the sake of -Wunused-variable - - XCTestExpectation *expectation = - [self expectationWithDescription:@"Wait for santa's Auth dispatch queue"]; - - __block NSMutableArray *events = [NSMutableArray array]; - [mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK - withCallback:^(ESResponse *r) { - @synchronized(self) { - [events addObject:r]; - } - - if (events.count >= wantNumResp) { - [expectation fulfill]; - } - }]; - - __block es_file_t dbFile = {.path = MakeStringToken(kEventsDBPath)}; - ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) { - m.binaryPath = @"somebinary"; - m.message->action_type = ES_ACTION_TYPE_AUTH; - m.message->event_type = ES_EVENT_TYPE_AUTH_UNLINK; - m.message->event = (es_events_t){.unlink = {.target = &dbFile}}; - m.message->mach_time = 1234; - m.message->deadline = 1234; - }]; - - [mockES triggerHandler:m.message]; - - [self waitForExpectations:@[ expectation ] timeout:60.0]; - - 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 { - NSDictionary *testCases = @{ - kEventsDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY], - kRulesDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY], - kBenignPath : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW], - }; - for (const NSString *testPath in testCases) { - MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity]; - [mockES reset]; - SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init]; - (void)snt; // Make it appear used for the sake of -Wunused-variable - - XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"]; - __block ESResponse *got; - [mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK - withCallback:^(ESResponse *r) { - got = r; - [expectation fulfill]; - }]; - - __block es_file_t dbFile = {.path = MakeStringToken(testPath)}; - ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) { - m.binaryPath = @"somebinary"; - m.message->action_type = ES_ACTION_TYPE_AUTH; - m.message->event_type = ES_EVENT_TYPE_AUTH_UNLINK; - m.message->event = (es_events_t){.unlink = {.target = &dbFile}}; - }]; - - [mockES triggerHandler:m.message]; - - [self waitForExpectations:@[ expectation ] timeout:60.0]; - - XCTAssertEqual(got.result, [testCases objectForKey:testPath].intValue, - @"Incorrect handling of delete of %@", testPath); - XCTAssertTrue(got.shouldCache, @"Failed to cache deletion decision of %@", testPath); - } -} - -- (void)testSkipOtherESEvents { - MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity]; - [mockES reset]; - SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init]; - (void)snt; // Make it appear used for the sake of -Wunused-variable - - XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"]; - __block ESResponse *got; - [mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK - withCallback:^(ESResponse *r) { - got = r; - [expectation fulfill]; - }]; - - __block es_file_t dbFile = {.path = MakeStringToken(@"/some/other/path")}; - ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) { - m.process->is_es_client = true; - m.binaryPath = @"somebinary"; - m.message->action_type = ES_ACTION_TYPE_AUTH; - m.message->event_type = ES_EVENT_TYPE_AUTH_UNLINK; - m.message->event = (es_events_t){.unlink = {.target = &dbFile}}; - }]; - - [mockES triggerHandler:m.message]; - - [self waitForExpectations:@[ expectation ] timeout:60.0]; - - XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW); -} - -- (void)testRenameOverwriteRulesDB { - NSDictionary *testCases = @{ - kEventsDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY], - kRulesDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY], - kBenignPath : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW], - }; - for (const NSString *testPath in testCases) { - MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity]; - [mockES reset]; - SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init]; - (void)snt; // Make it appear used for the sake of -Wunused-variable - - XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"]; - __block ESResponse *got; - [mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_RENAME - withCallback:^(ESResponse *r) { - got = r; - [expectation fulfill]; - }]; - - __block es_file_t otherFile = {.path = MakeStringToken(@"/some/other/path")}; - __block es_file_t dbFile = {.path = MakeStringToken(testPath)}; - ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) { - m.binaryPath = @"somebinary"; - m.message->action_type = ES_ACTION_TYPE_AUTH; - m.message->event_type = ES_EVENT_TYPE_AUTH_RENAME; - m.message->event = (es_events_t){ - .rename = - { - .source = &otherFile, - .destination_type = ES_DESTINATION_TYPE_EXISTING_FILE, - .destination = {.existing_file = &dbFile}, - }, - }; - }]; - - [mockES triggerHandler:m.message]; - - [self waitForExpectations:@[ expectation ] timeout:60.0]; - - XCTAssertEqual(got.result, [testCases objectForKey:testPath].intValue, - @"Incorrect handling of rename of %@", testPath); - XCTAssertTrue(got.shouldCache, @"Failed to cache rename auth decision of %@", testPath); - } -} - -- (void)testRenameRulesDB { - NSDictionary *testCases = @{ - kEventsDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY], - kRulesDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY], - kBenignPath : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW], - }; - - for (const NSString *testPath in testCases) { - MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity]; - [mockES reset]; - SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init]; - (void)snt; // Make it appear used for the sake of -Wunused-variable - - XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"]; - __block ESResponse *got; - [mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_RENAME - withCallback:^(ESResponse *r) { - got = r; - [expectation fulfill]; - }]; - - __block es_file_t otherFile = {.path = MakeStringToken(@"/some/other/path")}; - __block es_file_t dbFile = {.path = MakeStringToken(testPath)}; - ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) { - m.binaryPath = @"somebinary"; - m.message->action_type = ES_ACTION_TYPE_AUTH; - m.message->event_type = ES_EVENT_TYPE_AUTH_RENAME; - m.message->event = (es_events_t){ - .rename = - { - .source = &dbFile, - .destination_type = ES_DESTINATION_TYPE_NEW_PATH, - .destination = {.new_path = - { - .dir = &otherFile, - .filename = MakeStringToken(@"someotherfilename"), - }}, - }, - }; - }]; - - [mockES triggerHandler:m.message]; - - [self waitForExpectations:@[ expectation ] timeout:60.0]; - - XCTAssertEqual(got.result, [testCases objectForKey:testPath].intValue, - @"Incorrect handling of rename of %@", testPath); - - XCTAssertTrue(got.shouldCache, @"Failed to cache rename auth decision of %@", testPath); - } -} - -@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityRecorder.h b/Source/santad/EventProviders/SNTEndpointSecurityRecorder.h new file mode 100644 index 000000000..9dfcf5d67 --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityRecorder.h @@ -0,0 +1,40 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import "Source/common/SNTPrefixTree.h" +#import "Source/santad/EventProviders/AuthResultCache.h" +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" +#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h" +#include "Source/santad/Logs/EndpointSecurity/Logger.h" +#import "Source/santad/SNTCompilerController.h" + +/// ES Client focused on subscribing to NOTIFY event variants with the intention of enriching +/// received messages and logging the information. +@interface SNTEndpointSecurityRecorder : SNTEndpointSecurityClient + +- (instancetype) + initWithESAPI: + (std::shared_ptr) + esApi + logger:(std::shared_ptr)logger + enricher: + (std::shared_ptr)enricher + compilerController:(SNTCompilerController *)compilerController + authResultCache: + (std::shared_ptr)authResultCache + prefixTree:(std::shared_ptr)prefixTree; + +@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityRecorder.mm b/Source/santad/EventProviders/SNTEndpointSecurityRecorder.mm new file mode 100644 index 000000000..8d578903e --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityRecorder.mm @@ -0,0 +1,119 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h" + +#include + +#import "Source/common/SNTLogging.h" +#include "Source/santad/EventProviders/AuthResultCache.h" +#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" + +using santa::santad::event_providers::AuthResultCache; +using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI; +using santa::santad::event_providers::endpoint_security::EnrichedMessage; +using santa::santad::event_providers::endpoint_security::Enricher; +using santa::santad::event_providers::endpoint_security::Message; +using santa::santad::logs::endpoint_security::Logger; + +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_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; + default: return NULL; + } +} + +@interface SNTEndpointSecurityRecorder () +@property SNTCompilerController *compilerController; +@end + +@implementation SNTEndpointSecurityRecorder { + std::shared_ptr _authResultCache; + std::shared_ptr _enricher; + std::shared_ptr _logger; + std::shared_ptr _prefixTree; +} + +- (instancetype)initWithESAPI:(std::shared_ptr)esApi + logger:(std::shared_ptr)logger + enricher:(std::shared_ptr)enricher + compilerController:(SNTCompilerController *)compilerController + authResultCache:(std::shared_ptr)authResultCache + prefixTree:(std::shared_ptr)prefixTree { + self = [super initWithESAPI:std::move(esApi)]; + if (self) { + _enricher = enricher; + _logger = logger; + _compilerController = compilerController; + _authResultCache = authResultCache; + _prefixTree = prefixTree; + + [self establishClientOrDie]; + } + return self; +} + +- (void)handleMessage:(Message &&)esMsg { + // Pre-enrichment processing + switch (esMsg->event_type) { + case ES_EVENT_TYPE_NOTIFY_CLOSE: + // TODO(mlw): Once we move to building with the macOS 13 SDK, we should also check + // the `was_mapped_writable` field + if (esMsg->event.close.modified == false) { + // Ignore unmodified files + return; + } + + self->_authResultCache->RemoveFromCache(esMsg->event.close.target); + break; + default: break; + } + + [self.compilerController handleEvent:esMsg withLogger:self->_logger]; + + // Filter file op events matching the prefix tree. + es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg)); + if (targetFile != NULL && self->_prefixTree->HasPrefix(targetFile->path.data)) { + return; + } + + // Enrich the message inline with the ES handler block to capture enrichment + // data as close to the source event as possible. + std::shared_ptr sharedEnrichedMessage = _enricher->Enrich(std::move(esMsg)); + + // Asynchronously log the message + [self processEnrichedMessage:std::move(sharedEnrichedMessage) + handler:^(std::shared_ptr msg) { + self->_logger->Log(std::move(msg)); + }]; +} + +- (void)enable { + [super subscribe:{ + ES_EVENT_TYPE_NOTIFY_CLOSE, + ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA, + ES_EVENT_TYPE_NOTIFY_EXEC, + ES_EVENT_TYPE_NOTIFY_FORK, + ES_EVENT_TYPE_NOTIFY_EXIT, + ES_EVENT_TYPE_NOTIFY_LINK, + ES_EVENT_TYPE_NOTIFY_RENAME, + ES_EVENT_TYPE_NOTIFY_UNLINK, + }]; +} + +@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityRecorderTest.mm b/Source/santad/EventProviders/SNTEndpointSecurityRecorderTest.mm new file mode 100644 index 000000000..9a1679b88 --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityRecorderTest.mm @@ -0,0 +1,212 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include +#import +#import +#include +#include +#include +#include "gmock/gmock.h" + +#include +#include + +#include "Source/common/TestUtils.h" +#import "Source/santad/EventProviders/AuthResultCache.h" +#include "Source/santad/EventProviders/EndpointSecurity/Client.h" +#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" +#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" +#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h" +#include "Source/santad/Logs/EndpointSecurity/Logger.h" +#import "Source/santad/SNTCompilerController.h" + +using santa::santad::event_providers::AuthResultCache; +using santa::santad::event_providers::endpoint_security::EnrichedMessage; +using santa::santad::event_providers::endpoint_security::Enricher; +using santa::santad::event_providers::endpoint_security::Message; +using santa::santad::logs::endpoint_security::Logger; + +class MockEnricher : public Enricher { + public: + MOCK_METHOD(std::shared_ptr, Enrich, (Message &&)); +}; + +class MockAuthResultCache : public AuthResultCache { + public: + using AuthResultCache::AuthResultCache; + + MOCK_METHOD(void, RemoveFromCache, (const es_file_t *)); +}; + +class MockLogger : public Logger { + public: + using Logger::Logger; + + MOCK_METHOD(void, Log, (std::shared_ptr)); +}; + +@interface SNTEndpointSecurityRecorderTest : XCTestCase +@end + +@implementation SNTEndpointSecurityRecorderTest + +- (void)testEnable { + // Ensure the client subscribes to expected event types + std::set expectedEventSubs{ + ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA, ES_EVENT_TYPE_NOTIFY_EXEC, + ES_EVENT_TYPE_NOTIFY_FORK, ES_EVENT_TYPE_NOTIFY_EXIT, ES_EVENT_TYPE_NOTIFY_LINK, + ES_EVENT_TYPE_NOTIFY_RENAME, ES_EVENT_TYPE_NOTIFY_UNLINK, + }; + auto mockESApi = std::make_shared(); + + id recorderClient = [[SNTEndpointSecurityRecorder alloc] initWithESAPI:mockESApi]; + + EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs)).WillOnce(testing::Return(true)); + + [recorderClient enable]; + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)testHandleMessage { + 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); + es_file_t targetFile = MakeESFile("bar"); + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsESNewClient(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + std::shared_ptr enrichedMsg = std::shared_ptr(nullptr); + + auto mockEnricher = std::make_shared(); + EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(enrichedMsg)); + + auto mockAuthCache = std::make_shared(nullptr); + EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFile)).Times(1); + + // NOTE: Currently unable to create a partial mock of the + // `SNTEndpointSecurityRecorder` object. There is a bug in OCMock that doesn't + // properly handle the `processEnrichedMessage:handler:` block. Instead this + // test will mock the `Log` method that is called in the handler block. + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + auto mockLogger = std::make_shared(nullptr, nullptr); + EXPECT_CALL(*mockLogger, Log).WillOnce(testing::InvokeWithoutArgs(^() { + dispatch_semaphore_signal(sema); + })); + + auto prefixTree = std::make_shared(); + + id mockCC = OCMStrictClassMock([SNTCompilerController class]); + + SNTEndpointSecurityRecorder *recorderClient = + [[SNTEndpointSecurityRecorder alloc] initWithESAPI:mockESApi + logger:mockLogger + enricher:mockEnricher + compilerController:mockCC + authResultCache:mockAuthCache + prefixTree:prefixTree]; + + // CLOSE not modified, bail early + { + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE; + esMsg.event.close.modified = false; + esMsg.event.close.target = NULL; + + XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, &esMsg)]); + } + + // CLOSE modified, remove from cache + { + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE; + esMsg.event.close.modified = true; + esMsg.event.close.target = &targetFile; + Message msg(mockESApi, &esMsg); + + OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs(); + + [recorderClient handleMessage:std::move(msg)]; + + XCTAssertEqual( + 0, dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)), + "Log wasn't called within expected time window"); + } + + // LINK, Prefix match, bail early + { + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_LINK; + esMsg.event.link.source = &targetFile; + prefixTree->AddPrefix(esMsg.event.link.source->path.data); + Message msg(mockESApi, &esMsg); + + OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs(); + + [recorderClient handleMessage:std::move(msg)]; + } + + XCTAssertTrue(OCMVerifyAll(mockCC)); + + XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get()); + XCTBubbleMockVerifyAndClearExpectations(mockEnricher.get()); + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); + XCTBubbleMockVerifyAndClearExpectations(mockLogger.get()); + + [mockCC stopMocking]; +} + +- (void)testGetTargetFileForPrefixTree { + // Ensure `GetTargetFileForPrefixTree` returns expected field for each + // subscribed event type in the `SNTEndpointSecurityRecorder`. + extern es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg); + + es_file_t closeFile = MakeESFile("close"); + es_file_t linkFile = MakeESFile("link"); + es_file_t renameFile = MakeESFile("rename"); + es_file_t unlinkFile = MakeESFile("unlink"); + es_message_t esMsg; + + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE; + esMsg.event.close.target = &closeFile; + XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &closeFile); + + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_LINK; + esMsg.event.link.source = &linkFile; + XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &linkFile); + + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_RENAME; + esMsg.event.rename.source = &renameFile; + XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &renameFile); + + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_UNLINK; + esMsg.event.unlink.target = &unlinkFile; + XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &unlinkFile); + + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA; + XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr); + + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC; + XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr); + + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_FORK; + XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr); + + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXIT; + XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr); +} + +@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h b/Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h new file mode 100644 index 000000000..d042e97ce --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h @@ -0,0 +1,33 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import + +#include + +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h" +#include "Source/santad/Logs/EndpointSecurity/Logger.h" + +/// ES Client focused on mitigating accidental or malicious tampering of Santa and its components. +@interface SNTEndpointSecurityTamperResistance + : SNTEndpointSecurityClient + +- (instancetype) + initWithESAPI: + (std::shared_ptr)esApi + logger:(std::shared_ptr)logger; + +@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.mm b/Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.mm new file mode 100644 index 000000000..fd5f93972 --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.mm @@ -0,0 +1,113 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import "Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h" + +#include +#include + +#import "Source/common/SNTLogging.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" + +using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI; +using santa::santad::event_providers::endpoint_security::Message; +using santa::santad::logs::endpoint_security::Logger; + +static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-driver"; + +@implementation SNTEndpointSecurityTamperResistance { + std::shared_ptr _logger; +} + +- (instancetype)initWithESAPI:(std::shared_ptr)esApi + logger:(std::shared_ptr)logger { + self = [super initWithESAPI:std::move(esApi)]; + if (self) { + _logger = logger; + + [self establishClientOrDie]; + } + return self; +} + +- (void)handleMessage:(Message &&)esMsg { + switch (esMsg->event_type) { + case ES_EVENT_TYPE_AUTH_UNLINK: { + if ([SNTEndpointSecurityTamperResistance + isDatabasePath:esMsg->event.unlink.target->path.data]) { + // Do not cache so that each attempt to remove santa is logged + [self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_DENY cacheable:false]; + LOGW(@"Preventing attempt to delete Santa databases!"); + } else { + [self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:true]; + } + + return; + } + + case ES_EVENT_TYPE_AUTH_RENAME: { + if ([SNTEndpointSecurityTamperResistance + isDatabasePath:esMsg->event.rename.source->path.data]) { + // Do not cache so that each attempt to remove santa is logged + [self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_DENY cacheable:false]; + LOGW(@"Preventing attempt to rename Santa databases!"); + return; + } + + if (esMsg->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) { + if ([SNTEndpointSecurityTamperResistance + isDatabasePath:esMsg->event.rename.destination.existing_file->path.data]) { + [self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_DENY cacheable:false]; + LOGW(@"Preventing attempt to overwrite Santa databases!"); + return; + } + } + + // If we get to here, no more reasons to deny the event, so allow it + [self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:true]; + return; + } + + case ES_EVENT_TYPE_AUTH_KEXTLOAD: { + // TODO(mlw): Since we don't package the kext anymore, we should consider removing this. + // TODO(mlw): Consider logging when kext loads are attempted. + es_auth_result_t res = ES_AUTH_RESULT_ALLOW; + if (strcmp(esMsg->event.kextload.identifier.data, kSantaKextIdentifier.data()) == 0) { + LOGW(@"Preventing attempt to load Santa kext!"); + res = ES_AUTH_RESULT_DENY; + } + [self respondToMessage:esMsg withAuthResult:res cacheable:true]; + return; + } + + default: + // Unexpected event type, this is a programming error + [NSException raise:@"Invalid event type" + format:@"Invalid tamper resistance event type: %d", esMsg->event_type]; + } +} + +- (void)enable { + // TODO(mlw): For macOS 13, use new mute and invert APIs to limit the + // messages sent for these events to the Santa-specific directories + // checked in the `handleMessage:` method. + + [super subscribeAndClearCache:{ + ES_EVENT_TYPE_AUTH_KEXTLOAD, + ES_EVENT_TYPE_AUTH_UNLINK, + ES_EVENT_TYPE_AUTH_RENAME, + }]; +} + +@end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityTamperResistanceTest.mm b/Source/santad/EventProviders/SNTEndpointSecurityTamperResistanceTest.mm new file mode 100644 index 000000000..5e0e6f68f --- /dev/null +++ b/Source/santad/EventProviders/SNTEndpointSecurityTamperResistanceTest.mm @@ -0,0 +1,190 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include +#import +#import +#include +#include + +#include +#include +#include + +#include "Source/common/TestUtils.h" +#include "Source/santad/EventProviders/EndpointSecurity/Client.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" +#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h" + +using santa::santad::event_providers::endpoint_security::Client; +using santa::santad::event_providers::endpoint_security::Message; + +static constexpr std::string_view kEventsDBPath = "/private/var/db/santa/events.db"; +static constexpr std::string_view kRulesDBPath = "/private/var/db/santa/rules.db"; +static constexpr std::string_view kBenignPath = "/some/other/path"; +static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-driver"; + +@interface SNTEndpointSecurityTamperResistanceTest : XCTestCase +@end + +@implementation SNTEndpointSecurityTamperResistanceTest + +- (void)testEnable { + // Ensure the client subscribes to expected event types + std::set expectedEventSubs{ + ES_EVENT_TYPE_AUTH_KEXTLOAD, + ES_EVENT_TYPE_AUTH_UNLINK, + ES_EVENT_TYPE_AUTH_RENAME, + }; + + auto mockESApi = std::make_shared(); + EXPECT_CALL(*mockESApi, NewClient(testing::_)) + .WillOnce(testing::Return(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS))); + EXPECT_CALL(*mockESApi, MuteProcess(testing::_, testing::_)).WillOnce(testing::Return(true)); + EXPECT_CALL(*mockESApi, ClearCache(testing::_)) + .After(EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs)) + .WillOnce(testing::Return(true))) + .WillOnce(testing::Return(true)); + + SNTEndpointSecurityTamperResistance *tamperClient = + [[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi logger:nullptr]; + id mockTamperClient = OCMPartialMock(tamperClient); + + [mockTamperClient enable]; + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); + [mockTamperClient stopMocking]; +} + +- (void)testHandleMessage { + es_file_t file = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&file); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth); + + es_file_t fileEventsDB = MakeESFile(kEventsDBPath.data()); + es_file_t fileRulesDB = MakeESFile(kRulesDBPath.data()); + es_file_t fileBenign = MakeESFile(kBenignPath.data()); + + es_string_token_t santaTok = MakeESStringToken(kSantaKextIdentifier.data()); + es_string_token_t benignTok = MakeESStringToken(kBenignPath.data()); + + std::map pathToResult{ + {&fileEventsDB, ES_AUTH_RESULT_DENY}, + {&fileRulesDB, ES_AUTH_RESULT_DENY}, + {&fileBenign, ES_AUTH_RESULT_ALLOW}, + }; + + std::map kextIdToResult{ + {&santaTok, ES_AUTH_RESULT_DENY}, + {&benignTok, ES_AUTH_RESULT_ALLOW}, + }; + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsESNewClient(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + SNTEndpointSecurityTamperResistance *tamperClient = + [[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi logger:nullptr]; + + id mockTamperClient = OCMPartialMock(tamperClient); + + // Unable to use `OCMExpect` here because we cannot match on the `Message` + // parameter. In order to verify the `AuthResult` and `Cacheable` parameters, + // instead use `OCMStub` and extract the arguments in order to assert their + // expected values. + __block es_auth_result_t gotAuthResult; + __block bool gotCachable; + OCMStub([mockTamperClient respondToMessage:Message(mockESApi, &esMsg) + withAuthResult:(es_auth_result_t)0 + cacheable:false]) + .ignoringNonObjectArgs() + .andDo(^(NSInvocation *inv) { + [inv getArgument:&gotAuthResult atIndex:3]; + [inv getArgument:&gotCachable atIndex:4]; + }); + + // First check unhandled event types will crash + { + Message msg(mockESApi, &esMsg); + XCTAssertThrows([tamperClient handleMessage:Message(mockESApi, &esMsg)]); + } + + // Check UNLINK tamper events + { + esMsg.event_type = ES_EVENT_TYPE_AUTH_UNLINK; + for (const auto &kv : pathToResult) { + Message msg(mockESApi, &esMsg); + esMsg.event.unlink.target = kv.first; + + [mockTamperClient handleMessage:std::move(msg)]; + + XCTAssertEqual(gotAuthResult, kv.second); + XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW); + } + } + + // Check RENAME `source` tamper events + { + esMsg.event_type = ES_EVENT_TYPE_AUTH_RENAME; + for (const auto &kv : pathToResult) { + Message msg(mockESApi, &esMsg); + esMsg.event.rename.source = kv.first; + esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH; + + [mockTamperClient handleMessage:std::move(msg)]; + + XCTAssertEqual(gotAuthResult, kv.second); + XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW); + } + } + + // Check RENAME `dest` tamper events + { + esMsg.event_type = ES_EVENT_TYPE_AUTH_RENAME; + esMsg.event.rename.source = &fileBenign; + for (const auto &kv : pathToResult) { + Message msg(mockESApi, &esMsg); + esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE; + esMsg.event.rename.destination.existing_file = kv.first; + + [mockTamperClient handleMessage:std::move(msg)]; + + XCTAssertEqual(gotAuthResult, kv.second); + XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW); + } + } + + // Check KEXTLOAD tamper events + { + esMsg.event_type = ES_EVENT_TYPE_AUTH_KEXTLOAD; + + for (const auto &kv : kextIdToResult) { + Message msg(mockESApi, &esMsg); + esMsg.event.kextload.identifier = *kv.first; + + [mockTamperClient handleMessage:std::move(msg)]; + + XCTAssertEqual(gotAuthResult, kv.second); + XCTAssertEqual(gotCachable, true); // Note: Kext responses always cached + } + } + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); + XCTAssertTrue(OCMVerifyAll(mockTamperClient)); + + [mockTamperClient stopMocking]; +} + +@end diff --git a/Source/santad/EventProviders/SNTEventProvider.h b/Source/santad/EventProviders/SNTEventProvider.h deleted file mode 100644 index 3c82e8757..000000000 --- a/Source/santad/EventProviders/SNTEventProvider.h +++ /dev/null @@ -1,33 +0,0 @@ -/// Copyright 2019 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import - -#include "Source/common/SNTCommon.h" - -@protocol SNTEventProvider - -- (void)listenForDecisionRequests:(void (^)(santa_message_t message))callback; -- (void)listenForLogRequests:(void (^)(santa_message_t message))callback; -- (int)postAction:(santa_action_t)action forMessage:(santa_message_t)sm; -- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly; -- (void)fileModificationPrefixFilterAdd:(NSArray *)filters; -- (void)fileModificationPrefixFilterReset; -- (NSArray *)cacheCounts; -- (NSArray *)cacheBucketCount; -- (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID; -- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeId; -@property(readonly) BOOL connectionEstablished; - -@end diff --git a/Source/santad/Logs/EndpointSecurity/Logger.h b/Source/santad/Logs/EndpointSecurity/Logger.h new file mode 100644 index 000000000..dc19a8617 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Logger.h @@ -0,0 +1,69 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_LOGGER_H +#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_LOGGER_H + +#include +#include + +#import + +#import "Source/common/SNTCommonEnums.h" +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" +#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" +#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h" +#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h" + +// Forward declarations +@class SNTStoredEvent; +namespace santa::santad::logs::endpoint_security { +class LoggerPeer; +} + +namespace santa::santad::logs::endpoint_security { + +class Logger { + public: + static std::unique_ptr Create( + std::shared_ptr esapi, + SNTEventLogType log_type, NSString *event_log_path); + + Logger(std::shared_ptr serializer, + std::shared_ptr writer); + + virtual ~Logger() = default; + + virtual void Log( + std::shared_ptr msg); + + void LogAllowlist(const santa::santad::event_providers::endpoint_security::Message &msg, + const std::string_view hash); + + void LogBundleHashingEvents(NSArray *events); + + void LogDiskAppeared(NSDictionary *props); + void LogDiskDisappeared(NSDictionary *props); + + friend class santa::santad::logs::endpoint_security::LoggerPeer; + + private: + std::shared_ptr serializer_; + std::shared_ptr writer_; +}; + +} // namespace santa::santad::logs::endpoint_security + +#endif diff --git a/Source/santad/Logs/EndpointSecurity/Logger.mm b/Source/santad/Logs/EndpointSecurity/Logger.mm new file mode 100644 index 000000000..c7c5cc145 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Logger.mm @@ -0,0 +1,89 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/Logs/EndpointSecurity/Logger.h" + +#include "Source/common/SNTCommonEnums.h" +#include "Source/common/SNTLogging.h" +#include "Source/common/SNTStoredEvent.h" +#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h" +#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h" +#include "Source/santad/Logs/EndpointSecurity/Writers/File.h" +#include "Source/santad/Logs/EndpointSecurity/Writers/Null.h" +#include "Source/santad/Logs/EndpointSecurity/Writers/Syslog.h" + +using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI; +using santa::santad::event_providers::endpoint_security::EnrichedMessage; +using santa::santad::event_providers::endpoint_security::Message; +using santa::santad::logs::endpoint_security::serializers::BasicString; +using santa::santad::logs::endpoint_security::serializers::Empty; +using santa::santad::logs::endpoint_security::writers::File; +using santa::santad::logs::endpoint_security::writers::Null; +using santa::santad::logs::endpoint_security::writers::Syslog; + +namespace santa::santad::logs::endpoint_security { + +// Flush the write buffer every 5 seconds +static const uint64_t kFlushBufferTimeoutMS = 10000; +// Batch writes up to 128kb +static const size_t kBufferBatchSizeBytes = (1024 * 128); +// Reserve an extra 4kb of buffer space to account for event overflow +static const size_t kMaxExpectedWriteSizeBytes = 4096; + +// Translate configured log type to appropriate Serializer/Writer pairs +std::unique_ptr Logger::Create(std::shared_ptr esapi, + SNTEventLogType log_type, NSString *event_log_path) { + switch (log_type) { + case SNTEventLogTypeFilelog: + return std::make_unique( + BasicString::Create(esapi), + File::Create(event_log_path, kFlushBufferTimeoutMS, kBufferBatchSizeBytes, + kMaxExpectedWriteSizeBytes)); + case SNTEventLogTypeSyslog: + return std::make_unique(BasicString::Create(esapi, false), Syslog::Create()); + case SNTEventLogTypeNull: return std::make_unique(Empty::Create(), Null::Create()); + case SNTEventLogTypeProtobuf: + LOGE(@"The EventLogType value protobuf is not supported in this release"); + return nullptr; + default: LOGE(@"Invalid log type: %ld", log_type); return nullptr; + } +} + +Logger::Logger(std::shared_ptr serializer, + std::shared_ptr writer) + : serializer_(std::move(serializer)), writer_(std::move(writer)) {} + +void Logger::Log(std::shared_ptr msg) { + writer_->Write(serializer_->SerializeMessage(std::move(msg))); +} + +void Logger::LogAllowlist(const Message &msg, const std::string_view hash) { + writer_->Write(serializer_->SerializeAllowlist(msg, hash)); +} + +void Logger::LogBundleHashingEvents(NSArray *events) { + for (SNTStoredEvent *se in events) { + writer_->Write(serializer_->SerializeBundleHashingEvent(se)); + } +} + +void Logger::LogDiskAppeared(NSDictionary *props) { + writer_->Write(serializer_->SerializeDiskAppeared(props)); +} + +void Logger::LogDiskDisappeared(NSDictionary *props) { + writer_->Write(serializer_->SerializeDiskDisappeared(props)); +} + +} // namespace santa::santad::logs::endpoint_security diff --git a/Source/santad/Logs/EndpointSecurity/LoggerTest.mm b/Source/santad/Logs/EndpointSecurity/LoggerTest.mm new file mode 100644 index 000000000..8547c0438 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/LoggerTest.mm @@ -0,0 +1,198 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include +#import +#import +#include +#include + +#include +#include +#include +#include + +#include "Source/common/SNTCommonEnums.h" +#include "Source/common/TestUtils.h" +#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" +#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h" +#include "Source/santad/Logs/EndpointSecurity/Logger.h" +#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h" +#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h" +#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h" +#include "Source/santad/Logs/EndpointSecurity/Writers/File.h" +#include "Source/santad/Logs/EndpointSecurity/Writers/Null.h" +#include "Source/santad/Logs/EndpointSecurity/Writers/Syslog.h" +#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h" + +using santa::santad::event_providers::endpoint_security::EnrichedClose; +using santa::santad::event_providers::endpoint_security::EnrichedFile; +using santa::santad::event_providers::endpoint_security::EnrichedMessage; +using santa::santad::event_providers::endpoint_security::EnrichedProcess; +using santa::santad::event_providers::endpoint_security::Message; +using santa::santad::logs::endpoint_security::Logger; +using santa::santad::logs::endpoint_security::serializers::BasicString; +using santa::santad::logs::endpoint_security::serializers::Empty; +using santa::santad::logs::endpoint_security::writers::File; +using santa::santad::logs::endpoint_security::writers::Null; +using santa::santad::logs::endpoint_security::writers::Syslog; + +namespace santa::santad::logs::endpoint_security { + +class LoggerPeer : public Logger { + public: + // Make base class constructors visible + using Logger::Logger; + + LoggerPeer(std::unique_ptr l) : Logger(l->serializer_, l->writer_) {} + + std::shared_ptr Serializer() { return serializer_; } + + std::shared_ptr Writer() { return writer_; } +}; + +} // namespace santa::santad::logs::endpoint_security + +using santa::santad::logs::endpoint_security::LoggerPeer; + +class MockSerializer : public Empty { + public: + MOCK_METHOD(std::vector, SerializeMessage, (const EnrichedClose &msg)); + + MOCK_METHOD(std::vector, SerializeAllowlist, (const Message &, const std::string_view)); + + MOCK_METHOD(std::vector, SerializeBundleHashingEvent, (SNTStoredEvent *)); + MOCK_METHOD(std::vector, SerializeDiskAppeared, (NSDictionary *)); + MOCK_METHOD(std::vector, SerializeDiskDisappeared, (NSDictionary *)); +}; + +class MockWriter : public Null { + public: + MOCK_METHOD(void, Write, (std::vector && bytes)); +}; + +@interface LoggerTest : XCTestCase +@end + +@implementation LoggerTest + +- (void)testCreate { + // Ensure that the factory method creates expected serializers/writers pairs + auto mockESApi = std::make_shared(); + + XCTAssertEqual(nullptr, Logger::Create(mockESApi, (SNTEventLogType)123, @"/tmp")); + XCTAssertEqual(nullptr, Logger::Create(mockESApi, SNTEventLogTypeProtobuf, @"/tmp")); + + LoggerPeer logger(Logger::Create(mockESApi, SNTEventLogTypeFilelog, @"/tmp/temppy")); + XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast(logger.Serializer())); + XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast(logger.Writer())); + + logger = LoggerPeer(Logger::Create(mockESApi, SNTEventLogTypeSyslog, @"/tmp/temppy")); + XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast(logger.Serializer())); + XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast(logger.Writer())); + + logger = LoggerPeer(Logger::Create(mockESApi, SNTEventLogTypeNull, @"/tmp/temppy")); + XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast(logger.Serializer())); + XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast(logger.Writer())); +} + +- (void)testLog { + auto mockESApi = std::make_shared(); + auto mockSerializer = std::make_shared(); + auto mockWriter = std::make_shared(); + + // Ensure all Logger::Log* methods call the serializer followed by the writer + es_message_t msg; + + // Note: In this test, `RetainMessage` isn't setup to return anything. This + // means that the underlying `es_msg_` in the `Message` object is NULL, and + // therefore no call to `ReleaseMessage` is ever made (hence no expectations). + // Because we don't need to operate on the es_msg_, this simplifies the test. + EXPECT_CALL(*mockESApi, RetainMessage); + auto enrichedMsg = std::make_shared( + EnrichedClose(Message(mockESApi, &msg), + EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt, + EnrichedFile(std::nullopt, std::nullopt, std::nullopt)), + EnrichedFile(std::nullopt, std::nullopt, std::nullopt))); + + EXPECT_CALL(*mockSerializer, SerializeMessage(testing::A())).Times(1); + EXPECT_CALL(*mockWriter, Write).Times(1); + + Logger(mockSerializer, mockWriter).Log(enrichedMsg); + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); + XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get()); + XCTBubbleMockVerifyAndClearExpectations(mockWriter.get()); +} + +- (void)testLogAllowList { + auto mockESApi = std::make_shared(); + auto mockSerializer = std::make_shared(); + auto mockWriter = std::make_shared(); + es_message_t msg; + std::string_view hash = "this_is_my_test_hash"; + + EXPECT_CALL(*mockESApi, RetainMessage); + EXPECT_CALL(*mockSerializer, SerializeAllowlist(testing::_, hash)); + EXPECT_CALL(*mockWriter, Write); + + Logger(mockSerializer, mockWriter).LogAllowlist(Message(mockESApi, &msg), hash); + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); + XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get()); + XCTBubbleMockVerifyAndClearExpectations(mockWriter.get()); +} + +- (void)testLogBundleHashingEvents { + auto mockSerializer = std::make_shared(); + auto mockWriter = std::make_shared(); + NSArray *events = @[ @"event1", @"event2", @"event3" ]; + + EXPECT_CALL(*mockSerializer, SerializeBundleHashingEvent).Times((int)[events count]); + EXPECT_CALL(*mockWriter, Write).Times((int)[events count]); + + Logger(mockSerializer, mockWriter).LogBundleHashingEvents(events); + + XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get()); + XCTBubbleMockVerifyAndClearExpectations(mockWriter.get()); +} + +- (void)testLogDiskAppeared { + auto mockSerializer = std::make_shared(); + auto mockWriter = std::make_shared(); + + EXPECT_CALL(*mockSerializer, SerializeDiskAppeared); + EXPECT_CALL(*mockWriter, Write); + + Logger(mockSerializer, mockWriter).LogDiskAppeared(@{@"key" : @"value"}); + + XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get()); + XCTBubbleMockVerifyAndClearExpectations(mockWriter.get()); +} + +- (void)testLogDiskDisappeared { + auto mockSerializer = std::make_shared(); + auto mockWriter = std::make_shared(); + + EXPECT_CALL(*mockSerializer, SerializeDiskDisappeared); + EXPECT_CALL(*mockWriter, Write); + + Logger(mockSerializer, mockWriter).LogDiskDisappeared(@{@"key" : @"value"}); + + XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get()); + XCTBubbleMockVerifyAndClearExpectations(mockWriter.get()); +} + +@end diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h b/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h new file mode 100644 index 000000000..623f8b370 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h @@ -0,0 +1,72 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_BASICSTRING_H +#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_BASICSTRING_H + +#include +#include +#include + +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" +#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h" + +namespace santa::santad::logs::endpoint_security::serializers { + +class BasicString : public Serializer { + public: + static std::shared_ptr Create( + std::shared_ptr esapi, + bool prefix_time_name = true); + + BasicString( + std::shared_ptr esapi, + bool prefix_time_name); + + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedClose &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedExec &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedExit &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedFork &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedLink &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedRename &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override; + + std::vector SerializeAllowlist( + const santa::santad::event_providers::endpoint_security::Message &, + const std::string_view) override; + + std::vector SerializeBundleHashingEvent(SNTStoredEvent *) override; + + std::vector SerializeDiskAppeared(NSDictionary *) override; + std::vector SerializeDiskDisappeared(NSDictionary *) override; + + private: + std::string CreateDefaultString(size_t reserved_size = 512); + + std::shared_ptr esapi_; + bool prefix_time_name_; +}; + +} // namespace santa::santad::logs::endpoint_security::serializers + +#endif diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm b/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm new file mode 100644 index 000000000..e95550bd9 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm @@ -0,0 +1,608 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h" + +#import +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#import "Source/common/SNTCachedDecision.h" +#import "Source/common/SNTConfigurator.h" +#import "Source/common/SNTLogging.h" +#import "Source/common/SNTStoredEvent.h" +#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h" +#import "Source/santad/SNTDecisionCache.h" + +using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI; +using santa::santad::event_providers::endpoint_security::EnrichedClose; +using santa::santad::event_providers::endpoint_security::EnrichedExchange; +using santa::santad::event_providers::endpoint_security::EnrichedExec; +using santa::santad::event_providers::endpoint_security::EnrichedExit; +using santa::santad::event_providers::endpoint_security::EnrichedFork; +using santa::santad::event_providers::endpoint_security::EnrichedLink; +using santa::santad::event_providers::endpoint_security::EnrichedRename; +using santa::santad::event_providers::endpoint_security::EnrichedUnlink; +using santa::santad::event_providers::endpoint_security::Message; + +// These functions are exported by the Security framework, but are not included in headers +extern "C" Boolean SecTranslocateIsTranslocatedURL(CFURLRef path, bool *isTranslocated, + CFErrorRef *__nullable error); +extern "C" CFURLRef __nullable SecTranslocateCreateOriginalPathForURL(CFURLRef translocatedPath, + CFErrorRef *__nullable error); + +namespace santa::santad::logs::endpoint_security::serializers { + +static inline SanitizableString FilePath(const es_file_t *file) { + return SanitizableString(file); +} + +static inline pid_t Pid(const audit_token_t &tok) { + return audit_token_to_pid(tok); +} + +static inline pid_t Pidversion(const audit_token_t &tok) { + return audit_token_to_pidversion(tok); +} + +static inline pid_t RealUser(const audit_token_t &tok) { + return audit_token_to_ruid(tok); +} + +static inline pid_t RealGroup(const audit_token_t &tok) { + return audit_token_to_rgid(tok); +} + +static inline void SetThreadIDs(uid_t uid, gid_t gid) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + pthread_setugid_np(uid, gid); +#pragma clang diagnostic pop +} + +static inline const mach_port_t GetDefaultIOKitCommsPort() { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return kIOMasterPortDefault; +#pragma clang diagnostic pop +} + +static NSString *SerialForDevice(NSString *devPath) { + if (!devPath.length) { + return nil; + } + NSString *serial; + io_registry_entry_t device = + IORegistryEntryFromPath(GetDefaultIOKitCommsPort(), devPath.UTF8String); + while (!serial && device) { + CFMutableDictionaryRef device_properties = NULL; + IORegistryEntryCreateCFProperties(device, &device_properties, kCFAllocatorDefault, kNilOptions); + NSDictionary *properties = CFBridgingRelease(device_properties); + if (properties[@"Serial Number"]) { + serial = properties[@"Serial Number"]; + } else if (properties[@"kUSBSerialNumberString"]) { + serial = properties[@"kUSBSerialNumberString"]; + } + + if (serial) { + IOObjectRelease(device); + break; + } + + io_registry_entry_t parent; + IORegistryEntryGetParentEntry(device, kIOServicePlane, &parent); + IOObjectRelease(device); + device = parent; + } + + return [serial stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; +} + +static NSString *DiskImageForDevice(NSString *devPath) { + devPath = [devPath stringByDeletingLastPathComponent]; + if (!devPath.length) { + return nil; + } + + io_registry_entry_t device = + IORegistryEntryFromPath(GetDefaultIOKitCommsPort(), devPath.UTF8String); + CFMutableDictionaryRef device_properties = NULL; + IORegistryEntryCreateCFProperties(device, &device_properties, kCFAllocatorDefault, kNilOptions); + NSDictionary *properties = CFBridgingRelease(device_properties); + IOObjectRelease(device); + + if (properties[@"image-path"]) { + NSString *result = [[NSString alloc] initWithData:properties[@"image-path"] + encoding:NSUTF8StringEncoding]; + return [result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + } else { + return nil; + } +} + +static NSString *OriginalPathForTranslocation(const es_process_t *esProc) { + if (!esProc) { + return nil; + } + + // Note: Benchmarks showed better performance using `URLWithString` with a `file://` prefix + // compared to using `fileURLWithPath`. + CFURLRef cfExecURL = (__bridge CFURLRef) + [NSURL URLWithString:[NSString stringWithFormat:@"file://%s", esProc->executable->path.data]]; + NSURL *origURL = nil; + bool isTranslocated = false; + + if (SecTranslocateIsTranslocatedURL(cfExecURL, &isTranslocated, NULL) && isTranslocated) { + bool dropPrivs = true; + if (@available(macOS 12.0, *)) { + dropPrivs = false; + } + + if (dropPrivs) { + SetThreadIDs(RealUser(esProc->audit_token), RealGroup(esProc->audit_token)); + } + + origURL = CFBridgingRelease(SecTranslocateCreateOriginalPathForURL(cfExecURL, NULL)); + + if (dropPrivs) { + SetThreadIDs(KAUTH_UID_NONE, KAUTH_GID_NONE); + } + } + + return [origURL path]; +} + +es_file_t *GetAllowListTargetFile(const Message &msg) { + switch (msg->event_type) { + case ES_EVENT_TYPE_NOTIFY_CLOSE: return msg->event.close.target; + case ES_EVENT_TYPE_NOTIFY_RENAME: return msg->event.rename.source; + default: + // This is a programming error + LOGE(@"Unexpected event type for AllowList"); + [NSException raise:@"Unexpected type" + format:@"Unexpected event type for AllowList: %d", msg->event_type]; + return nil; + } +} + +static NSDateFormatter *GetDateFormatter() { + static dispatch_once_t onceToken; + static NSDateFormatter *dateFormatter; + + dispatch_once(&onceToken, ^{ + dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + dateFormatter.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601]; + dateFormatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; + }); + + return dateFormatter; +} + +std::string GetDecisionString(SNTEventState event_state) { + if (event_state & SNTEventStateAllow) { + return "ALLOW"; + } else if (event_state & SNTEventStateBlock) { + return "DENY"; + } else { + return "UNKNOWN"; + } +} + +std::string GetReasonString(SNTEventState event_state) { + switch (event_state) { + case SNTEventStateAllowBinary: return "BINARY"; + case SNTEventStateAllowCompiler: return "COMPILER"; + case SNTEventStateAllowTransitive: return "TRANSITIVE"; + case SNTEventStateAllowPendingTransitive: return "PENDING_TRANSITIVE"; + case SNTEventStateAllowCertificate: return "CERT"; + case SNTEventStateAllowScope: return "SCOPE"; + case SNTEventStateAllowTeamID: return "TEAMID"; + case SNTEventStateAllowUnknown: return "UNKNOWN"; + case SNTEventStateBlockBinary: return "BINARY"; + case SNTEventStateBlockCertificate: return "CERT"; + case SNTEventStateBlockScope: return "SCOPE"; + case SNTEventStateBlockTeamID: return "TEAMID"; + case SNTEventStateBlockLongPath: return "LONG_PATH"; + case SNTEventStateBlockUnknown: return "UNKNOWN"; + default: return "NOTRUNNING"; + } +} + +std::string GetModeString(SNTClientMode mode) { + switch (mode) { + case SNTClientModeMonitor: return "M"; + case SNTClientModeLockdown: return "L"; + default: return "U"; + } +} + +static inline void AppendProcess(std::string &str, const es_process_t *es_proc) { + char bname[MAXPATHLEN]; + str.append("|pid="); + str.append(std::to_string(Pid(es_proc->audit_token))); + str.append("|ppid="); + str.append(std::to_string(es_proc->original_ppid)); + str.append("|process="); + str.append(basename_r(FilePath(es_proc->executable).Sanitized().data(), bname) ?: ""); + str.append("|processpath="); + str.append(FilePath(es_proc->executable).Sanitized()); +} + +static inline void AppendUserGroup(std::string &str, const audit_token_t &tok, + const std::optional> &user, + const std::optional> &group) { + str.append("|uid="); + str.append(std::to_string(RealUser(tok))); + str.append("|user="); + str.append(user.has_value() ? user->get()->c_str() : "(null)"); + str.append("|gid="); + str.append(std::to_string(RealGroup(tok))); + str.append("|group="); + str.append(group.has_value() ? group->get()->c_str() : "(null)"); +} + +static char *FormattedDateString(char *buf, size_t len) { + struct timeval tv; + struct tm tm; + + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &tm); + + strftime(buf, len, "%Y-%m-%dT%H:%M:%S", &tm); + snprintf(buf, len, "%s.%03dZ", buf, tv.tv_usec / 1000); + + return buf; +} + +static inline NSString *NonNull(NSString *str) { + return str ?: @""; +} + +std::shared_ptr BasicString::Create(std::shared_ptr esapi, + bool prefix_time_name) { + return std::make_shared(esapi, prefix_time_name); +} + +BasicString::BasicString(std::shared_ptr esapi, bool prefix_time_name) + : esapi_(esapi), prefix_time_name_(prefix_time_name) {} + +std::string BasicString::CreateDefaultString(size_t reserved_size) { + std::string str; + str.reserve(1024); + + if (prefix_time_name_) { + char buf[32]; + + str.append("["); + str.append(FormattedDateString(buf, sizeof(buf))); + str.append("] I santad: "); + } + + return str; +} + +inline std::vector FinalizeString(std::string &str) { + str.append("\n"); + std::vector vec(str.length()); + std::copy(str.begin(), str.end(), vec.begin()); + return vec; +} + +std::vector BasicString::SerializeMessage(const EnrichedClose &msg) { + const es_message_t &esm = msg.es_msg(); + std::string str = CreateDefaultString(); + + str.append("action=WRITE|path="); + str.append(FilePath(esm.event.close.target).Sanitized()); + + AppendProcess(str, esm.process); + AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(), + msg.instigator().real_group()); + + return FinalizeString(str); +} + +std::vector BasicString::SerializeMessage(const EnrichedExchange &msg) { + const es_message_t &esm = msg.es_msg(); + std::string str = CreateDefaultString(); + + str.append("action=EXCHANGE|path="); + str.append(FilePath(esm.event.exchangedata.file1).Sanitized()); + str.append("|newpath="); + str.append(FilePath(esm.event.exchangedata.file2).Sanitized()); + + AppendProcess(str, esm.process); + AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(), + msg.instigator().real_group()); + + return FinalizeString(str); +} + +std::vector BasicString::SerializeMessage(const EnrichedExec &msg) { + const es_message_t &esm = msg.es_msg(); + std::string str = CreateDefaultString(1024); // EXECs tend to be bigger, reserve more space. + + SNTCachedDecision *cd = + [[SNTDecisionCache sharedCache] cachedDecisionForFile:esm.event.exec.target->executable->stat]; + + str.append("action=EXEC|decision="); + str.append(GetDecisionString(cd.decision)); + str.append("|reason="); + str.append(GetReasonString(cd.decision)); + + if (cd.decisionExtra) { + str.append("|explain="); + str.append([cd.decisionExtra UTF8String]); + } + + if (cd.sha256) { + str.append("|sha256="); + str.append([cd.sha256 UTF8String]); + } + + if (cd.certSHA256) { + str.append("|cert_sha256="); + str.append([cd.certSHA256 UTF8String]); + str.append("|cert_cn="); + str.append(SanitizableString(cd.certCommonName).Sanitized()); + } + + if (cd.teamID.length) { + str.append("|teamid="); + str.append([NonNull(cd.teamID) UTF8String]); + } + + if (cd.quarantineURL) { + str.append("|quarantine_url="); + str.append(SanitizableString(cd.quarantineURL).Sanitized()); + } + + str.append("|pid="); + str.append(std::to_string(Pid(esm.event.exec.target->audit_token))); + str.append("|pidversion="); + str.append(std::to_string(Pidversion(esm.event.exec.target->audit_token))); + str.append("|ppid="); + str.append(std::to_string(esm.event.exec.target->original_ppid)); + + AppendUserGroup(str, esm.event.exec.target->audit_token, msg.instigator().real_user(), + msg.instigator().real_group()); + + str.append("|mode="); + str.append(GetModeString([[SNTConfigurator configurator] clientMode])); + str.append("|path="); + str.append(FilePath(esm.event.exec.target->executable).Sanitized()); + + NSString *origPath = OriginalPathForTranslocation(esm.event.exec.target); + if (origPath) { + str.append("|origpath="); + str.append(SanitizableString(origPath).Sanitized()); + } + + uint32_t argCount = esapi_->ExecArgCount(&esm.event.exec); + if (argCount > 0) { + str.append("|args="); + for (uint32_t i = 0; i < argCount; i++) { + if (i != 0) { + str.append(" "); + } + + str.append(SanitizableString(esapi_->ExecArg(&esm.event.exec, i)).Sanitized()); + } + } + + if ([[SNTConfigurator configurator] enableMachineIDDecoration]) { + str.append("|machineid="); + str.append([NonNull([[SNTConfigurator configurator] machineID]) UTF8String]); + } + + return FinalizeString(str); +} + +std::vector BasicString::SerializeMessage(const EnrichedExit &msg) { + const es_message_t &esm = msg.es_msg(); + std::string str = CreateDefaultString(); + + str.append("action=EXIT|pid="); + str.append(std::to_string(Pid(esm.process->audit_token))); + str.append("|pidversion="); + str.append(std::to_string(Pidversion(esm.process->audit_token))); + str.append("|ppid="); + str.append(std::to_string(esm.process->original_ppid)); + str.append("|uid="); + str.append(std::to_string(RealUser(esm.process->audit_token))); + str.append("|gid="); + str.append(std::to_string(RealGroup(esm.process->audit_token))); + + return FinalizeString(str); +} + +std::vector BasicString::SerializeMessage(const EnrichedFork &msg) { + const es_message_t &esm = msg.es_msg(); + std::string str = CreateDefaultString(); + + str.append("action=FORK|pid="); + str.append(std::to_string(Pid(esm.event.fork.child->audit_token))); + str.append("|pidversion="); + str.append(std::to_string(Pidversion(esm.event.fork.child->audit_token))); + str.append("|ppid="); + str.append(std::to_string(esm.event.fork.child->original_ppid)); + str.append("|uid="); + str.append(std::to_string(RealUser(esm.event.fork.child->audit_token))); + str.append("|gid="); + str.append(std::to_string(RealGroup(esm.event.fork.child->audit_token))); + + return FinalizeString(str); +} + +std::vector BasicString::SerializeMessage(const EnrichedLink &msg) { + const es_message_t &esm = msg.es_msg(); + std::string str = CreateDefaultString(); + + str.append("action=LINK|path="); + str.append(FilePath(esm.event.link.source).Sanitized()); + str.append("|newpath="); + str.append(FilePath(esm.event.link.target_dir).Sanitized()); + str.append("/"); + str.append(SanitizableString(esm.event.link.target_filename).Sanitized()); + + AppendProcess(str, esm.process); + AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(), + msg.instigator().real_group()); + + return FinalizeString(str); +} + +std::vector BasicString::SerializeMessage(const EnrichedRename &msg) { + const es_message_t &esm = msg.es_msg(); + std::string str = CreateDefaultString(); + + str.append("action=RENAME|path="); + str.append(FilePath(esm.event.rename.source).Sanitized()); + str.append("|newpath="); + + switch (esm.event.rename.destination_type) { + case ES_DESTINATION_TYPE_EXISTING_FILE: + str.append(FilePath(esm.event.rename.destination.existing_file).Sanitized()); + break; + case ES_DESTINATION_TYPE_NEW_PATH: + str.append(FilePath(esm.event.rename.destination.new_path.dir).Sanitized()); + str.append("/"); + str.append(SanitizableString(esm.event.rename.destination.new_path.filename).Sanitized()); + break; + default: str.append("(null)"); break; + } + + AppendProcess(str, esm.process); + AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(), + msg.instigator().real_group()); + + return FinalizeString(str); +} + +std::vector BasicString::SerializeMessage(const EnrichedUnlink &msg) { + const es_message_t &esm = msg.es_msg(); + std::string str = CreateDefaultString(); + + str.append("action=DELETE|path="); + str.append(FilePath(esm.event.unlink.target).Sanitized()); + + AppendProcess(str, esm.process); + AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(), + msg.instigator().real_group()); + + return FinalizeString(str); +} + +std::vector BasicString::SerializeAllowlist(const Message &msg, + const std::string_view hash) { + std::string str = CreateDefaultString(); + + str.append("action=ALLOWLIST|pid="); + str.append(std::to_string(Pid(msg->process->audit_token))); + str.append("|pidversion="); + str.append(std::to_string(Pidversion(msg->process->audit_token))); + str.append("|path="); + str.append(FilePath(GetAllowListTargetFile(msg)).Sanitized()); + str.append("|sha256="); + str.append(hash); + + return FinalizeString(str); +} + +std::vector BasicString::SerializeBundleHashingEvent(SNTStoredEvent *event) { + std::string str = CreateDefaultString(); + + str.append("action=BUNDLE|sha256="); + str.append([NonNull(event.fileSHA256) UTF8String]); + str.append("|bundlehash="); + str.append([NonNull(event.fileBundleHash) UTF8String]); + str.append("|bundlename="); + str.append([NonNull(event.fileBundleName) UTF8String]); + str.append("|bundleid="); + str.append([NonNull(event.fileBundleID) UTF8String]); + str.append("|bundlepath="); + str.append([NonNull(event.fileBundlePath) UTF8String]); + str.append("|path="); + str.append([NonNull(event.filePath) UTF8String]); + + return FinalizeString(str); +} + +std::vector BasicString::SerializeDiskAppeared(NSDictionary *props) { + NSString *dmgPath = nil; + NSString *serial = nil; + if ([props[@"DADeviceModel"] isEqual:@"Disk Image"]) { + dmgPath = DiskImageForDevice(props[@"DADevicePath"]); + } else { + serial = SerialForDevice(props[@"DADevicePath"]); + } + + NSString *model = [NSString + stringWithFormat:@"%@ %@", NonNull(props[@"DADeviceVendor"]), NonNull(props[@"DADeviceModel"])]; + model = [model stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + NSString *appearanceDateString = [GetDateFormatter() + stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:[props[@"DAAppearanceTime"] + doubleValue]]]; + + std::string str = CreateDefaultString(); + str.append("action=DISKAPPEAR"); + str.append("|mount="); + str.append([NonNull([props[@"DAVolumePath"] path]) UTF8String]); + str.append("|volume="); + str.append([NonNull(props[@"DAVolumeName"]) UTF8String]); + str.append("|bsdname="); + str.append([NonNull(props[@"DAMediaBSDName"]) UTF8String]); + str.append("|fs="); + str.append([NonNull(props[@"DAVolumeKind"]) UTF8String]); + str.append("|model="); + str.append([NonNull(model) UTF8String]); + str.append("|serial="); + str.append([NonNull(serial) UTF8String]); + str.append("|bus="); + str.append([NonNull(props[@"DADeviceProtocol"]) UTF8String]); + str.append("|dmgpath="); + str.append([NonNull(dmgPath) UTF8String]); + str.append("|appearance="); + str.append([NonNull(appearanceDateString) UTF8String]); + + return FinalizeString(str); +} + +std::vector BasicString::SerializeDiskDisappeared(NSDictionary *props) { + std::string str = CreateDefaultString(); + + str.append("action=DISKDISAPPEAR"); + str.append("|mount="); + str.append([NonNull([props[@"DAVolumePath"] path]) UTF8String]); + str.append("|volume="); + str.append([NonNull(props[@"DAVolumeName"]) UTF8String]); + str.append("|bsdname="); + str.append([NonNull(props[@"DAMediaBSDName"]) UTF8String]); + + return FinalizeString(str); +} + +} // namespace santa::santad::logs::endpoint_security::serializers diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm b/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm new file mode 100644 index 000000000..d14db8ce5 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Serializers/BasicStringTest.mm @@ -0,0 +1,418 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include +#include +#import +#import +#import +#include +#include +#include + +#include +#include + +#import "Source/common/SNTCachedDecision.h" +#import "Source/common/SNTCommonEnums.h" +#import "Source/common/SNTConfigurator.h" +#import "Source/common/SNTStoredEvent.h" +#include "Source/common/TestUtils.h" +#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" +#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" +#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h" +#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h" +#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h" +#import "Source/santad/SNTDecisionCache.h" + +using santa::santad::event_providers::endpoint_security::Enricher; +using santa::santad::event_providers::endpoint_security::Message; +using santa::santad::logs::endpoint_security::serializers::BasicString; +using santa::santad::logs::endpoint_security::serializers::Serializer; + +namespace santa::santad::logs::endpoint_security::serializers { +extern es_file_t *GetAllowListTargetFile(const Message &msg); +extern std::string GetDecisionString(SNTEventState event_state); +extern std::string GetReasonString(SNTEventState event_state); +extern std::string GetModeString(SNTClientMode mode); +} // namespace santa::santad::logs::endpoint_security::serializers + +using santa::santad::logs::endpoint_security::serializers::GetAllowListTargetFile; +using santa::santad::logs::endpoint_security::serializers::GetDecisionString; +using santa::santad::logs::endpoint_security::serializers::GetModeString; +using santa::santad::logs::endpoint_security::serializers::GetReasonString; + +std::string BasicStringSerializeMessage(std::shared_ptr mockESApi, + es_message_t *esMsg) { + mockESApi->SetExpectationsRetainReleaseMessage(esMsg); + + std::shared_ptr bs = BasicString::Create(mockESApi, false); + std::vector ret = bs->SerializeMessage(Enricher().Enrich(Message(mockESApi, esMsg))); + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); + + return std::string(ret.begin(), ret.end()); +} + +std::string BasicStringSerializeMessage(es_message_t *esMsg) { + auto mockESApi = std::make_shared(); + return BasicStringSerializeMessage(mockESApi, esMsg); +} + +@interface BasicStringTest : XCTestCase +@property id mockConfigurator; +@property id mockDecisionCache; + +@property SNTCachedDecision *testCachedDecision; +@end + +@implementation BasicStringTest + +- (void)setUp { + self.mockConfigurator = OCMClassMock([SNTConfigurator class]); + OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator); + OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown); + OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(YES); + OCMStub([self.mockConfigurator machineID]).andReturn(@"my_id"); + + self.testCachedDecision = [[SNTCachedDecision alloc] init]; + self.testCachedDecision.decision = SNTEventStateAllowBinary; + self.testCachedDecision.decisionExtra = @"extra!"; + self.testCachedDecision.sha256 = @"1234_hash"; + self.testCachedDecision.quarantineURL = @"google.com"; + self.testCachedDecision.certSHA256 = @"5678_hash"; + + self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]); + OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache); + OCMStub([self.mockDecisionCache cachedDecisionForFile:{}]) + .ignoringNonObjectArgs() + .andReturn(self.testCachedDecision); +} + +- (void)tearDown { + [self.mockConfigurator stopMocking]; + [self.mockDecisionCache stopMocking]; +} + +- (void)testSerializeMessageClose { + es_file_t procFile = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78)); + es_file_t file = MakeESFile("close_file"); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc); + esMsg.event.close.modified = true; + esMsg.event.close.target = &file; + + std::string got = BasicStringSerializeMessage(&esMsg); + std::string want = "action=WRITE|path=close_file" + "|pid=12|ppid=56|process=foo|processpath=foo" + "|uid=-2|user=nobody|gid=-2|group=nobody\n"; + + XCTAssertCppStringEqual(got, want); +} + +- (void)testSerializeMessageExchange { + es_file_t procFile = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78)); + es_file_t file1 = MakeESFile("exchange_1"); + es_file_t file2 = MakeESFile("exchange_2"); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA, &proc); + esMsg.event.exchangedata.file1 = &file1; + esMsg.event.exchangedata.file2 = &file2; + + std::string got = BasicStringSerializeMessage(&esMsg); + std::string want = "action=EXCHANGE|path=exchange_1|newpath=exchange_2" + "|pid=12|ppid=56|process=foo|processpath=foo" + "|uid=-2|user=nobody|gid=-2|group=nobody\n"; + + XCTAssertCppStringEqual(got, want); +} + +- (void)testSerializeMessageExec { + es_file_t procFile = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78)); + + es_file_t execFile = MakeESFile("execpath|"); + es_process_t procExec = MakeESProcess(&execFile, MakeAuditToken(12, 89), MakeAuditToken(56, 78)); + + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXEC, &proc); + esMsg.event.exec.target = &procExec; + + auto mockESApi = std::make_shared(); + EXPECT_CALL(*mockESApi, ExecArgCount).WillOnce(testing::Return(3)); + + EXPECT_CALL(*mockESApi, ExecArg) + .WillOnce(testing::Return(es_string_token_t{9, "exec|path"})) + .WillOnce(testing::Return(es_string_token_t{5, "-l\n-t"})) + .WillOnce(testing::Return(es_string_token_t{8, "-v\r--foo"})); + + std::string got = BasicStringSerializeMessage(mockESApi, &esMsg); + std::string want = "action=EXEC|decision=ALLOW|reason=BINARY|explain=extra!|sha256=1234_hash|" + "cert_sha256=5678_hash|cert_cn=|quarantine_url=google.com|pid=12|pidversion=" + "89|ppid=56|uid=-2|user=nobody|gid=-2|group=nobody|mode=L|path=execpath|" + "args=execpath -l\\n-t -v\\r--foo|machineid=my_id\n"; + + XCTAssertCppStringEqual(got, want); +} + +- (void)testSerializeMessageExit { + es_file_t procFile = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78)); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc); + + std::string got = BasicStringSerializeMessage(&esMsg); + std::string want = "action=EXIT|pid=12|pidversion=34|ppid=56|uid=-2|gid=-2\n"; + + XCTAssertCppStringEqual(got, want); +} + +- (void)testSerializeMessageFork { + es_file_t procFile = MakeESFile("foo"); + es_file_t procChildFile = MakeESFile("foo_child"); + es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78)); + es_process_t procChild = + MakeESProcess(&procChildFile, MakeAuditToken(67, 89), MakeAuditToken(12, 34)); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_FORK, &proc); + esMsg.event.fork.child = &procChild; + + std::string got = BasicStringSerializeMessage(&esMsg); + std::string want = "action=FORK|pid=67|pidversion=89|ppid=12|uid=-2|gid=-2\n"; + + XCTAssertCppStringEqual(got, want); +} + +- (void)testSerializeMessageLink { + es_file_t procFile = MakeESFile("foo"); + es_file_t srcFile = MakeESFile("link_src"); + es_file_t dstDir = MakeESFile("link_dst"); + es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78)); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_LINK, &proc); + esMsg.event.link.source = &srcFile; + esMsg.event.link.target_dir = &dstDir; + esMsg.event.link.target_filename = MakeESStringToken("link_name"); + + std::string got = BasicStringSerializeMessage(&esMsg); + std::string want = "action=LINK|path=link_src|newpath=link_dst/link_name" + "|pid=12|ppid=56|process=foo|processpath=foo" + "|uid=-2|user=nobody|gid=-2|group=nobody\n"; + + XCTAssertCppStringEqual(got, want); +} + +- (void)testSerializeMessageRename { + es_file_t procFile = MakeESFile("foo"); + es_file_t srcFile = MakeESFile("rename_src"); + es_file_t dstFile = MakeESFile("rename_dst"); + es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78)); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &proc); + esMsg.event.rename.source = &srcFile; + esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE; + esMsg.event.rename.destination.existing_file = &dstFile; + + std::string got = BasicStringSerializeMessage(&esMsg); + std::string want = "action=RENAME|path=rename_src|newpath=rename_dst" + "|pid=12|ppid=56|process=foo|processpath=foo" + "|uid=-2|user=nobody|gid=-2|group=nobody\n"; + + XCTAssertCppStringEqual(got, want); +} + +- (void)testSerializeMessageUnlink { + es_file_t procFile = MakeESFile("foo"); + es_file_t targetFile = MakeESFile("deleted_file"); + es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78)); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_UNLINK, &proc); + esMsg.event.unlink.target = &targetFile; + + std::string got = BasicStringSerializeMessage(&esMsg); + std::string want = "action=DELETE|path=deleted_file" + "|pid=12|ppid=56|process=foo|processpath=foo" + "|uid=-2|user=nobody|gid=-2|group=nobody\n"; + + XCTAssertCppStringEqual(got, want); +} + +- (void)testSerializeAllowlist { + es_file_t file = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&file, MakeAuditToken(12, 34), MakeAuditToken(56, 78)); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc); + esMsg.event.close.target = &file; + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + std::vector ret = BasicString::Create(mockESApi, false) + ->SerializeAllowlist(Message(mockESApi, &esMsg), "test_hash"); + + XCTAssertTrue(testing::Mock::VerifyAndClearExpectations(mockESApi.get()), + "Expected calls were not properly mocked"); + + std::string got(ret.begin(), ret.end()); + std::string want = "action=ALLOWLIST|pid=12|pidversion=34|path=foo" + "|sha256=test_hash\n"; + + XCTAssertCppStringEqual(got, want); +} + +- (void)testSerializeBundleHashingEvent { + SNTStoredEvent *se = [[SNTStoredEvent alloc] init]; + + se.fileSHA256 = @"file_hash"; + se.fileBundleHash = @"file_bundle_hash"; + se.fileBundleName = @"file_bundle_Name"; + se.fileBundleID = nil; + se.fileBundlePath = @"file_bundle_path"; + se.filePath = @"file_path"; + + std::vector ret = BasicString::Create(nullptr, false)->SerializeBundleHashingEvent(se); + std::string got(ret.begin(), ret.end()); + + std::string want = "action=BUNDLE|sha256=file_hash" + "|bundlehash=file_bundle_hash|bundlename=file_bundle_Name|bundleid=" + "|bundlepath=file_bundle_path|path=file_path\n"; + + XCTAssertCppStringEqual(got, want); +} + +- (void)testSerializeDiskAppeared { + NSDictionary *props = @{ + @"DADevicePath" : @"", + @"DADeviceVendor" : @"vendor", + @"DADeviceModel" : @"model", + @"DAAppearanceTime" : @(1252487349), // 2009-09-09 09:09:09 + @"DAVolumePath" : [NSURL URLWithString:@"path"], + @"DAMediaBSDName" : @"bsd", + @"DAVolumeKind" : @"apfs", + @"DADeviceProtocol" : @"usb", + }; + + std::vector ret = BasicString::Create(nullptr, false)->SerializeDiskAppeared(props); + std::string got(ret.begin(), ret.end()); + + std::string want = "action=DISKAPPEAR|mount=path|volume=|bsdname=bsd|fs=apfs" + "|model=vendor model|serial=|bus=usb|dmgpath=" + "|appearance=2040-09-09T09:09:09.000Z\n"; + + XCTAssertCppStringEqual(got, want); +} + +- (void)testSerializeDiskDisappeared { + NSDictionary *props = @{ + @"DAVolumePath" : [NSURL URLWithString:@"path"], + @"DAMediaBSDName" : @"bsd", + }; + + std::vector ret = BasicString::Create(nullptr, false)->SerializeDiskDisappeared(props); + std::string got(ret.begin(), ret.end()); + + std::string want = "action=DISKDISAPPEAR|mount=path|volume=|bsdname=bsd\n"; + + XCTAssertCppStringEqual(got, want); +} + +- (void)testGetDecisionString { + std::map stateToDecision = { + {SNTEventStateUnknown, "UNKNOWN"}, + {SNTEventStateBundleBinary, "UNKNOWN"}, + {SNTEventStateBlockUnknown, "DENY"}, + {SNTEventStateBlockBinary, "DENY"}, + {SNTEventStateBlockCertificate, "DENY"}, + {SNTEventStateBlockScope, "DENY"}, + {SNTEventStateBlockTeamID, "DENY"}, + {SNTEventStateBlockLongPath, "DENY"}, + {SNTEventStateAllowUnknown, "ALLOW"}, + {SNTEventStateAllowBinary, "ALLOW"}, + {SNTEventStateAllowCertificate, "ALLOW"}, + {SNTEventStateAllowScope, "ALLOW"}, + {SNTEventStateAllowCompiler, "ALLOW"}, + {SNTEventStateAllowTransitive, "ALLOW"}, + {SNTEventStateAllowPendingTransitive, "ALLOW"}, + {SNTEventStateAllowTeamID, "ALLOW"}, + }; + + for (const auto &kv : stateToDecision) { + XCTAssertCppStringEqual(GetDecisionString(kv.first), kv.second); + } +} + +- (void)testGetReasonString { + std::map stateToReason = { + {SNTEventStateUnknown, "NOTRUNNING"}, + {SNTEventStateBundleBinary, "NOTRUNNING"}, + {SNTEventStateBlockUnknown, "UNKNOWN"}, + {SNTEventStateBlockBinary, "BINARY"}, + {SNTEventStateBlockCertificate, "CERT"}, + {SNTEventStateBlockScope, "SCOPE"}, + {SNTEventStateBlockTeamID, "TEAMID"}, + {SNTEventStateBlockLongPath, "LONG_PATH"}, + {SNTEventStateAllowUnknown, "UNKNOWN"}, + {SNTEventStateAllowBinary, "BINARY"}, + {SNTEventStateAllowCertificate, "CERT"}, + {SNTEventStateAllowScope, "SCOPE"}, + {SNTEventStateAllowCompiler, "COMPILER"}, + {SNTEventStateAllowTransitive, "TRANSITIVE"}, + {SNTEventStateAllowPendingTransitive, "PENDING_TRANSITIVE"}, + {SNTEventStateAllowTeamID, "TEAMID"}, + }; + + for (const auto &kv : stateToReason) { + XCTAssertCppStringEqual(GetReasonString(kv.first), kv.second); + } +} + +- (void)testGetModeString { + std::map modeToString = { + {SNTClientModeMonitor, "M"}, + {SNTClientModeLockdown, "L"}, + {(SNTClientMode)123, "U"}, + }; + + for (const auto &kv : modeToString) { + XCTAssertCppStringEqual(GetModeString(kv.first), kv.second); + } +} + +- (void)testGetAllowListTargetFile { + es_file_t closeTargetFile = MakeESFile("close_target"); + es_file_t renameSourceFile = MakeESFile("rename_source"); + es_file_t procFile = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&procFile); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc); + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + { + esMsg.event.close.target = &closeTargetFile; + Message msg(mockESApi, &esMsg); + es_file_t *target = GetAllowListTargetFile(msg); + XCTAssertEqual(target, &closeTargetFile); + } + + { + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_RENAME; + esMsg.event.rename.source = &renameSourceFile; + Message msg(mockESApi, &esMsg); + es_file_t *target = GetAllowListTargetFile(msg); + XCTAssertEqual(target, &renameSourceFile); + } + + { + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXIT; + Message msg(mockESApi, &esMsg); + XCTAssertThrows(GetAllowListTargetFile(msg)); + } +} + +@end diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/Empty.h b/Source/santad/Logs/EndpointSecurity/Serializers/Empty.h new file mode 100644 index 000000000..cd9b3296b --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Serializers/Empty.h @@ -0,0 +1,58 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_EMPTY_H +#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_EMPTY_H + +#include +#include + +#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h" + +namespace santa::santad::logs::endpoint_security::serializers { + +class Empty : public Serializer { + public: + static std::shared_ptr Create(); + + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedClose &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedExec &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedExit &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedFork &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedLink &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedRename &) override; + std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override; + + std::vector SerializeAllowlist( + const santa::santad::event_providers::endpoint_security::Message &, + const std::string_view) override; + + std::vector SerializeBundleHashingEvent(SNTStoredEvent *) override; + + std::vector SerializeDiskAppeared(NSDictionary *) override; + std::vector SerializeDiskDisappeared(NSDictionary *) override; +}; + +} // namespace santa::santad::logs::endpoint_security::serializers + +#endif diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/Empty.mm b/Source/santad/Logs/EndpointSecurity/Serializers/Empty.mm new file mode 100644 index 000000000..1d4180b3b --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Serializers/Empty.mm @@ -0,0 +1,81 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h" + +using santa::santad::event_providers::endpoint_security::EnrichedClose; +using santa::santad::event_providers::endpoint_security::EnrichedExchange; +using santa::santad::event_providers::endpoint_security::EnrichedExec; +using santa::santad::event_providers::endpoint_security::EnrichedExit; +using santa::santad::event_providers::endpoint_security::EnrichedFork; +using santa::santad::event_providers::endpoint_security::EnrichedLink; +using santa::santad::event_providers::endpoint_security::EnrichedRename; +using santa::santad::event_providers::endpoint_security::EnrichedUnlink; +using santa::santad::event_providers::endpoint_security::Message; + +namespace santa::santad::logs::endpoint_security::serializers { + +std::shared_ptr Empty::Create() { + return std::make_shared(); +} + +std::vector Empty::SerializeMessage(const EnrichedClose &msg) { + return {}; +} + +std::vector Empty::SerializeMessage(const EnrichedExchange &msg) { + return {}; +} + +std::vector Empty::SerializeMessage(const EnrichedExec &msg) { + return {}; +} + +std::vector Empty::SerializeMessage(const EnrichedExit &msg) { + return {}; +} + +std::vector Empty::SerializeMessage(const EnrichedFork &msg) { + return {}; +} + +std::vector Empty::SerializeMessage(const EnrichedLink &msg) { + return {}; +} + +std::vector Empty::SerializeMessage(const EnrichedRename &msg) { + return {}; +} + +std::vector Empty::SerializeMessage(const EnrichedUnlink &msg) { + return {}; +} + +std::vector Empty::SerializeAllowlist(const Message &msg, const std::string_view hash) { + return {}; +} + +std::vector Empty::SerializeBundleHashingEvent(SNTStoredEvent *event) { + return {}; +} + +std::vector Empty::SerializeDiskAppeared(NSDictionary *props) { + return {}; +} + +std::vector Empty::SerializeDiskDisappeared(NSDictionary *props) { + return {}; +} + +} // namespace santa::santad::logs::endpoint_security::serializers diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/EmptyTest.mm b/Source/santad/Logs/EndpointSecurity/Serializers/EmptyTest.mm new file mode 100644 index 000000000..f0e6b09f3 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Serializers/EmptyTest.mm @@ -0,0 +1,53 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import +#import +#import + +#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" +#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h" + +using santa::santad::logs::endpoint_security::serializers::Empty; + +namespace es = santa::santad::event_providers::endpoint_security; + +@interface EmptyTest : XCTestCase +@end + +@implementation EmptyTest + +- (void)testAllSerializersReturnEmptyVector { + std::shared_ptr e = Empty::Create(); + + // We can get away with passing a fake argument to the `Serialize*` methods + // instead of constructing real ones since the Empty class never touches the + // input parameter. + int fake; + XCTAssertEqual(e->SerializeMessage(*(es::EnrichedClose *)&fake).size(), 0); + XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExchange *)&fake).size(), 0); + XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExec *)&fake).size(), 0); + XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExit *)&fake).size(), 0); + XCTAssertEqual(e->SerializeMessage(*(es::EnrichedFork *)&fake).size(), 0); + XCTAssertEqual(e->SerializeMessage(*(es::EnrichedLink *)&fake).size(), 0); + XCTAssertEqual(e->SerializeMessage(*(es::EnrichedRename *)&fake).size(), 0); + XCTAssertEqual(e->SerializeMessage(*(es::EnrichedUnlink *)&fake).size(), 0); + + XCTAssertEqual(e->SerializeAllowlist(*(es::Message *)&fake, "").size(), 0); + XCTAssertEqual(e->SerializeBundleHashingEvent(nil).size(), 0); + XCTAssertEqual(e->SerializeDiskAppeared(nil).size(), 0); + XCTAssertEqual(e->SerializeDiskDisappeared(nil).size(), 0); +} + +@end diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h b/Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h new file mode 100644 index 000000000..568b009d3 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h @@ -0,0 +1,62 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SANITIZABLESTRING_H +#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SANITIZABLESTRING_H + +#include +#import + +#include +#include +#include + +namespace santa::santad::logs::endpoint_security::serializers { + +// Small helper class that will sanitize a given string, but will only use new +// memory if the string required sanitization. If the string is already +// sanitized, this class only uses the given buffers. +class SanitizableString { + public: + SanitizableString(const es_file_t *file); + SanitizableString(const es_string_token_t &tok); + SanitizableString(const char *str, size_t len); + SanitizableString(NSString *str); + + SanitizableString(SanitizableString &&other) = delete; + SanitizableString(const SanitizableString &other) = delete; + SanitizableString &operator=(const SanitizableString &rhs) = delete; + SanitizableString &operator=(SanitizableString &&rhs) = delete; + + // Return the original, unsanitized string + std::string_view String() const; + + // Return the sanitized string + std::string_view Sanitized() const; + + static std::optional SanitizeString(const char *str); + static std::optional SanitizeString(const char *str, size_t length); + + friend std::ostream &operator<<(std::ostream &ss, const SanitizableString &sani_string); + + private: + const char *data_; + size_t length_; + mutable bool sanitized_ = false; + mutable std::optional sanitized_string_; +}; + +} // namespace santa::santad::logs::endpoint_security::serializers + +#endif diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.mm b/Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.mm new file mode 100644 index 000000000..9a6886527 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.mm @@ -0,0 +1,113 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h" + +namespace santa::santad::logs::endpoint_security::serializers { + +SanitizableString::SanitizableString(const es_file_t *file) + : data_(file->path.data), length_(file->path.length) {} + +SanitizableString::SanitizableString(const es_string_token_t &tok) + : data_(tok.data), length_(tok.length) {} + +SanitizableString::SanitizableString(NSString *str) + : data_([str UTF8String]), length_([str length]) {} + +SanitizableString::SanitizableString(const char *str, size_t len) : data_(str), length_(len) {} + +std::string_view SanitizableString::String() const { + return std::string_view(data_, length_); +} + +std::string_view SanitizableString::Sanitized() const { + if (!sanitized_) { + sanitized_ = true; + sanitized_string_ = SanitizeString(data_, length_); + } + + if (sanitized_string_.has_value()) { + return sanitized_string_.value(); + } else { + if (data_) { + return std::string_view(data_, length_); + } else { + return ""; + } + } +} + +std::ostream &operator<<(std::ostream &ss, const SanitizableString &sani_string) { + ss << sani_string.Sanitized(); + return ss; +} + +std::optional SanitizableString::SanitizeString(const char *str) { + return SanitizeString(str, str ? strlen(str) : 0); +} + +std::optional SanitizableString::SanitizeString(const char *str, size_t length) { + size_t strOffset = 0; + char c = 0; + std::string buf; + bool reservedStringSpace = false; + + if (!str) { + return std::nullopt; + } + + if (length < 1) { + return std::nullopt; + } + + // Loop through the string one character at a time, looking for the characters + // we want to remove. + for (const char *p = str; (c = *p) != 0; ++p) { + if (c == '|' || c == '\n' || c == '\r') { + if (!reservedStringSpace) { + // Assume the common case won't grow the string length by more than a + // factor of 2. String will grow more if it needs to. + buf.reserve(length * 2); + reservedStringSpace = true; + } + + // Copy from the last offset up to the character we just found into the buffer + ptrdiff_t diff = p - str; + buf.append(str + strOffset, diff - strOffset); + + // Update the buffer and string offsets + strOffset = diff + 1; + + // Replace the found character and advance the buffer offset + switch (c) { + case '|': buf.append(""); break; + case '\n': buf.append("\\n"); break; + case '\r': buf.append("\\r"); break; + } + } + } + + if (strOffset > 0 && strOffset < length) { + // Copy any characters from the last match to the end of the string into the buffer. + buf.append(str + strOffset, length - strOffset); + } + + if (reservedStringSpace) { + return buf; + } + + return std::nullopt; +} + +} // namespace santa::santad::logs::endpoint_security::serializers diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/SanitizableStringTest.mm b/Source/santad/Logs/EndpointSecurity/Serializers/SanitizableStringTest.mm new file mode 100644 index 000000000..e373bfcc4 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Serializers/SanitizableStringTest.mm @@ -0,0 +1,90 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h" + +#include +#import +#import +#include +#include + +#include "Source/common/TestUtils.h" + +using santa::santad::logs::endpoint_security::serializers::SanitizableString; + +@interface SanitizableStringTest : XCTestCase +@end + +@implementation SanitizableStringTest + +- (void)testSanitizeString { + const char *empty = ""; + size_t emptyLen = strlen(empty); + const char *noSanitize = "nothing_to_sanitize"; + size_t noSanitizeLen = strlen(noSanitize); + const char *sanitizable = "sani|tizable"; + size_t sanitizableLen = strlen(sanitizable); + + // NULL pointers are handled + XCTAssertFalse(SanitizableString::SanitizeString(NULL).has_value()); + + // Non-sanitized strings return std::nullopt + XCTAssertEqual(std::nullopt, SanitizableString::SanitizeString(empty)); + XCTAssertEqual(std::nullopt, SanitizableString::SanitizeString(noSanitize)); + + // Intentional pointer compare to ensure the data member of the returned + // string_view matches the original buffer when not sanitized, and not equal + // when the string needs sanitization + XCTAssertEqual(empty, SanitizableString(empty, emptyLen).Sanitized().data()); + XCTAssertEqual(noSanitize, SanitizableString(noSanitize, noSanitizeLen).Sanitized().data()); + XCTAssertNotEqual(sanitizable, SanitizableString(sanitizable, sanitizableLen).Sanitized().data()); + + // Ensure the `String` method always returns the unsanitized buffer + XCTAssertEqual(empty, SanitizableString(empty, emptyLen).String().data()); + XCTAssertEqual(noSanitize, SanitizableString(noSanitize, noSanitizeLen).String().data()); + XCTAssertEqual(sanitizable, SanitizableString(sanitizable, sanitizableLen).String().data()); + + XCTAssertCStringEqual(SanitizableString(@"|").Sanitized().data(), ""); + XCTAssertCStringEqual(SanitizableString(@"\n").Sanitized().data(), "\\n"); + XCTAssertCStringEqual(SanitizableString(@"\r").Sanitized().data(), "\\r"); + + XCTAssertCStringEqual(SanitizableString(@"a\nb\rc|").Sanitized().data(), "a\\nb\\rc"); + XCTAssertCStringEqual(SanitizableString(@"a|trail").Sanitized().data(), "atrail"); + + // Handle some long strings + NSString *base = [NSString stringWithFormat:@"%@|abc", [@"" stringByPaddingToLength:66 * 1024 + withString:@"A" + startingAtIndex:0]]; + + NSString *want = [NSString stringWithFormat:@"%@abc", [@"" stringByPaddingToLength:66 * 1024 + withString:@"A" + startingAtIndex:0]]; + + XCTAssertCStringEqual(SanitizableString(base).Sanitized().data(), [want UTF8String]); +} + +- (void)testStream { + // Test that using the `<<` operator will sanitize the string + std::ostringstream ss; + const char *sanitizable = "sani|tizable"; + const char *sanitized = "sanitizable"; + es_string_token_t tok = {.length = strlen(sanitizable), .data = sanitizable}; + + ss << SanitizableString(tok); + + XCTAssertCStringEqual(ss.str().c_str(), sanitized); +} + +@end diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h b/Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h new file mode 100644 index 000000000..8f8197f7f --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h @@ -0,0 +1,87 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SERIALIZER_H +#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SERIALIZER_H + +#include +#include + +#import + +#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" + +@class SNTStoredEvent; + +namespace santa::santad::logs::endpoint_security::serializers { + +class Serializer { + public: + virtual ~Serializer() = default; + + std::vector SerializeMessage( + std::shared_ptr msg) { + return std::visit([this](const auto &arg) { return this->SerializeMessageTemplate(arg); }, + msg->GetEnrichedMessage()); + } + + virtual std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedClose &) = 0; + virtual std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedExchange &) = 0; + virtual std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedExec &) = 0; + virtual std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedExit &) = 0; + virtual std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedFork &) = 0; + virtual std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedLink &) = 0; + virtual std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedRename &) = 0; + virtual std::vector SerializeMessage( + const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) = 0; + + virtual std::vector SerializeAllowlist( + const santa::santad::event_providers::endpoint_security::Message &, const std::string_view) = 0; + + virtual std::vector SerializeBundleHashingEvent(SNTStoredEvent *) = 0; + + virtual std::vector SerializeDiskAppeared(NSDictionary *) = 0; + virtual std::vector SerializeDiskDisappeared(NSDictionary *) = 0; + + private: + // Template methods used to ensure a place to implement any desired + // functionality that shouldn't be overridden by derived classes. + std::vector SerializeMessageTemplate( + const santa::santad::event_providers::endpoint_security::EnrichedClose &); + std::vector SerializeMessageTemplate( + const santa::santad::event_providers::endpoint_security::EnrichedExchange &); + std::vector SerializeMessageTemplate( + const santa::santad::event_providers::endpoint_security::EnrichedExec &); + std::vector SerializeMessageTemplate( + const santa::santad::event_providers::endpoint_security::EnrichedExit &); + std::vector SerializeMessageTemplate( + const santa::santad::event_providers::endpoint_security::EnrichedFork &); + std::vector SerializeMessageTemplate( + const santa::santad::event_providers::endpoint_security::EnrichedLink &); + std::vector SerializeMessageTemplate( + const santa::santad::event_providers::endpoint_security::EnrichedRename &); + std::vector SerializeMessageTemplate( + const santa::santad::event_providers::endpoint_security::EnrichedUnlink &); +}; + +} // namespace santa::santad::logs::endpoint_security::serializers + +#endif diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/Serializer.mm b/Source/santad/Logs/EndpointSecurity/Serializers/Serializer.mm new file mode 100644 index 000000000..a8aa6ba67 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Serializers/Serializer.mm @@ -0,0 +1,58 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h" + +#include + +#import "Source/santad/SNTDecisionCache.h" + +namespace es = santa::santad::event_providers::endpoint_security; + +namespace santa::santad::logs::endpoint_security::serializers { + +std::vector Serializer::SerializeMessageTemplate(const es::EnrichedClose &msg) { + return SerializeMessage(msg); +} +std::vector Serializer::SerializeMessageTemplate(const es::EnrichedExchange &msg) { + return SerializeMessage(msg); +} +std::vector Serializer::SerializeMessageTemplate(const es::EnrichedExec &msg) { + const es_message_t &es_msg = msg.es_msg(); + if (es_msg.action_type == ES_ACTION_TYPE_NOTIFY && + es_msg.action.notify.result.auth == ES_AUTH_RESULT_ALLOW) { + // For allowed execs, cached decision timestamps must be updated + [[SNTDecisionCache sharedCache] + resetTimestampForCachedDecision:msg.es_msg().event.exec.target->executable->stat]; + } + + return SerializeMessage(msg); +} +std::vector Serializer::SerializeMessageTemplate(const es::EnrichedExit &msg) { + return SerializeMessage(msg); +} +std::vector Serializer::SerializeMessageTemplate(const es::EnrichedFork &msg) { + return SerializeMessage(msg); +} +std::vector Serializer::SerializeMessageTemplate(const es::EnrichedLink &msg) { + return SerializeMessage(msg); +} +std::vector Serializer::SerializeMessageTemplate(const es::EnrichedRename &msg) { + return SerializeMessage(msg); +} +std::vector Serializer::SerializeMessageTemplate(const es::EnrichedUnlink &msg) { + return SerializeMessage(msg); +} + +}; // namespace santa::santad::logs::endpoint_security::serializers diff --git a/Source/santad/Logs/EndpointSecurity/Writers/File.h b/Source/santad/Logs/EndpointSecurity/Writers/File.h new file mode 100644 index 000000000..557c070ef --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Writers/File.h @@ -0,0 +1,64 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_FILE_H +#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_FILE_H + +#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h" + +#include +#include + +#include +#include + +// Forward declarations +namespace santa::santad::logs::endpoint_security::writers { +class FilePeer; +} + +namespace santa::santad::logs::endpoint_security::writers { + +class File : public Writer, public std::enable_shared_from_this { + public: + // Factory + static std::shared_ptr Create(NSString *path, uint64_t flush_timeout_ms, + size_t batch_size_bytes, + size_t max_expected_write_size_bytes); + + File(NSString *path, size_t batch_size_bytes, size_t max_expected_write_size_bytes, + dispatch_queue_t q, dispatch_source_t timer_source); + ~File(); + + void Write(std::vector &&bytes) override; + + friend class santa::santad::logs::endpoint_security::writers::FilePeer; + + private: + void OpenFileHandle(); + void WatchLogFile(); + void FlushBuffer(); + + std::vector buffer_; + size_t batch_size_bytes_; + dispatch_queue_t q_; + dispatch_source_t timer_source_; + dispatch_source_t watch_source_; + NSString *path_; + NSFileHandle *file_handle_; +}; + +} // namespace santa::santad::logs::endpoint_security::writers + +#endif diff --git a/Source/santad/Logs/EndpointSecurity/Writers/File.mm b/Source/santad/Logs/EndpointSecurity/Writers/File.mm new file mode 100644 index 000000000..3f200e55d --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Writers/File.mm @@ -0,0 +1,115 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import "Source/santad/Logs/EndpointSecurity/Writers/File.h" + +#include + +namespace santa::santad::logs::endpoint_security::writers { + +std::shared_ptr File::Create(NSString *path, uint64_t flush_timeout_ms, + size_t batch_size_bytes, size_t max_expected_write_size_bytes) { + dispatch_queue_t q = dispatch_queue_create("com.google.santa.daemon.file_event_log", + DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL); + dispatch_source_t timer_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q); + + dispatch_source_set_timer(timer_source, dispatch_time(DISPATCH_TIME_NOW, 0), + NSEC_PER_MSEC * flush_timeout_ms, 0); + + auto ret_writer = + std::make_shared(path, batch_size_bytes, max_expected_write_size_bytes, q, timer_source); + ret_writer->WatchLogFile(); + + std::weak_ptr weak_writer(ret_writer); + dispatch_source_set_event_handler(ret_writer->timer_source_, ^{ + std::shared_ptr shared_writer = weak_writer.lock(); + if (!shared_writer) { + return; + } + shared_writer->FlushBuffer(); + }); + + dispatch_resume(ret_writer->timer_source_); + + return ret_writer; +} + +File::File(NSString *path, size_t batch_size_bytes, size_t max_expected_write_size_bytes, + dispatch_queue_t q, dispatch_source_t timer_source) + : batch_size_bytes_(batch_size_bytes), + q_(q), + timer_source_(timer_source), + watch_source_(nullptr) { + path_ = path; + buffer_.reserve(batch_size_bytes + max_expected_write_size_bytes); + OpenFileHandle(); +} + +void File::WatchLogFile() { + if (watch_source_) { + dispatch_source_cancel(watch_source_); + } + + watch_source_ = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, file_handle_.fileDescriptor, + DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME, q_); + + auto shared_this = shared_from_this(); + dispatch_source_set_event_handler(watch_source_, ^{ + [shared_this->file_handle_ closeFile]; + shared_this->OpenFileHandle(); + shared_this->WatchLogFile(); + }); + + dispatch_resume(watch_source_); +} + +File::~File() { + if (timer_source_) { + dispatch_source_cancel(timer_source_); + } +} + +// IMPORTANT: Not thread safe. +void File::OpenFileHandle() { + NSFileManager *fm = [NSFileManager defaultManager]; + if (![fm fileExistsAtPath:path_]) { + [fm createFileAtPath:path_ contents:nil attributes:nil]; + } + file_handle_ = [NSFileHandle fileHandleForWritingAtPath:path_]; + [file_handle_ seekToEndOfFile]; +} + +void File::Write(std::vector &&bytes) { + auto shared_this = shared_from_this(); + + // Workaround to move `bytes` into the block without a copy + __block std::vector temp_bytes = std::move(bytes); + + dispatch_async(q_, ^{ + std::vector moved_bytes = std::move(temp_bytes); + + shared_this->buffer_.insert(shared_this->buffer_.end(), moved_bytes.begin(), moved_bytes.end()); + if (shared_this->buffer_.size() >= batch_size_bytes_) { + shared_this->FlushBuffer(); + } + }); +} + +// IMPORTANT: Not thread safe. +void File::FlushBuffer() { + write(file_handle_.fileDescriptor, buffer_.data(), buffer_.size()); + buffer_.clear(); +} + +} // namespace santa::santad::logs::endpoint_security::writers diff --git a/Source/santad/Logs/EndpointSecurity/Writers/FileTest.mm b/Source/santad/Logs/EndpointSecurity/Writers/FileTest.mm new file mode 100644 index 000000000..3ad2777b4 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Writers/FileTest.mm @@ -0,0 +1,179 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import +#include +#include +#include + +#include + +#include "Source/common/TestUtils.h" +#import "Source/santad/Logs/EndpointSecurity/Writers/File.h" + +namespace santa::santad::logs::endpoint_security::writers { + +class FilePeer : public File { + public: + // Make constructors visible + using File::File; + + NSFileHandle *FileHandle() { return file_handle_; } + + void BeginWatchingLogFile() { WatchLogFile(); } + + size_t InternalBufferSize() { return buffer_.size(); } +}; + +} // namespace santa::santad::logs::endpoint_security::writers + +using santa::santad::logs::endpoint_security::writers::FilePeer; + +bool WaitFor(bool (^condition)(void)) { + int attempts = 0; + long sleepPerAttemptMS = 10; // Wait 10ms between checks + long maxSleep = 2000; // Wait up to 2 seconds for new log file to be created + long maxAttempts = maxSleep / sleepPerAttemptMS; + + do { + SleepMS(sleepPerAttemptMS); + + // Break out once the condition holds + if (condition()) { + break; + } + } while (++attempts < maxAttempts); + + return attempts < maxAttempts; +} + +bool WaitForNewLogFile(NSFileManager *fileManager, NSString *path) { + return WaitFor(^bool() { + return [fileManager fileExistsAtPath:path]; + }); +} + +bool WaitForBufferSize(std::shared_ptr file, size_t expectedSize) { + return WaitFor(^bool() { + return file->InternalBufferSize() == expectedSize; + }); +} + +@interface FileTest : XCTestCase +@property NSString *path; +@property NSString *logPath; +@property NSString *logRenamePath; +@property dispatch_queue_t q; +@property dispatch_source_t timer; +@property NSFileManager *fileManager; +@end + +@implementation FileTest + +- (void)setUp { + self.path = [NSString stringWithFormat:@"%@santa-%d", NSTemporaryDirectory(), getpid()]; + + self.logPath = [NSString stringWithFormat:@"%@/log.out", self.path]; + self.logRenamePath = [NSString stringWithFormat:@"%@/log.rename.out", self.path]; + + self.fileManager = [NSFileManager defaultManager]; + + XCTAssertTrue([self.fileManager createDirectoryAtPath:self.path + withIntermediateDirectories:YES + attributes:nil + error:nil]); + + self.q = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); + XCTAssertNotNil(self.q); + self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q); + XCTAssertNotNil(self.timer); + + // Resume the timer to ensure its not inadvertently cancelled first + dispatch_resume(self.timer); +} + +- (void)tearDown { + [self.fileManager removeItemAtPath:self.path error:nil]; +} + +- (void)testWatchLogFile { + auto file = std::make_shared(self.logPath, 100, 500, self.q, self.timer); + file->BeginWatchingLogFile(); + + // Constructing a File object will open the file at the given path + struct stat wantSBOrig; + struct stat gotSBOrig; + XCTAssertEqual(stat([self.logPath UTF8String], &wantSBOrig), 0); + XCTAssertEqual(fstat(file->FileHandle().fileDescriptor, &gotSBOrig), 0); + XCTAssertEqual(wantSBOrig.st_ino, gotSBOrig.st_ino); + + // Deleting the current log file will cause a new file to be created + XCTAssertTrue([self.fileManager removeItemAtPath:self.logPath error:nil]); + + XCTAssertTrue(WaitForNewLogFile(self.fileManager, self.logPath), + "New log file not created within expected time after deletion"); + + struct stat wantSBAfterDelete; + struct stat gotSBAfterDelete; + XCTAssertEqual(stat([self.logPath UTF8String], &wantSBAfterDelete), 0); + XCTAssertEqual(fstat(file->FileHandle().fileDescriptor, &gotSBAfterDelete), 0); + + XCTAssertEqual(wantSBAfterDelete.st_ino, gotSBAfterDelete.st_ino); + XCTAssertNotEqual(wantSBOrig.st_ino, wantSBAfterDelete.st_ino); + + // Renaming the current log file will cause a new file to be created + XCTAssertTrue([self.fileManager moveItemAtPath:self.logPath toPath:self.logRenamePath error:nil]); + + XCTAssertTrue(WaitForNewLogFile(self.fileManager, self.logPath), + "New log file not created within expected time after rename"); + + struct stat wantSBAfterRename; + struct stat gotSBAfterRename; + XCTAssertEqual(stat([self.logPath UTF8String], &wantSBAfterRename), 0); + XCTAssertEqual(fstat(file->FileHandle().fileDescriptor, &gotSBAfterRename), 0); + + XCTAssertEqual(wantSBAfterRename.st_ino, gotSBAfterRename.st_ino); + XCTAssertNotEqual(wantSBAfterDelete.st_ino, wantSBAfterRename.st_ino); +} + +- (void)testWrite { + // Start with empty file. Perform two writes. The first will only go into the + // internal buffer. The second will meet/exceed capacity and flush to disk + size_t bufferSize = 100; + size_t writeSize = 50; + auto file = + std::make_shared(self.logPath, bufferSize, bufferSize * 2, self.q, self.timer); + + // Starting out, file size and internal buffer are 0 + struct stat gotSB; + XCTAssertEqual(fstat(file->FileHandle().fileDescriptor, &gotSB), 0); + XCTAssertEqual(0, gotSB.st_size); + XCTAssertEqual(0, file->InternalBufferSize()); + + // After the first write, the buffer is 50 bytes, but the file is still 0 + file->Write(std::vector(writeSize, 'A')); + WaitForBufferSize(file, 50); + XCTAssertEqual(fstat(file->FileHandle().fileDescriptor, &gotSB), 0); + XCTAssertEqual(0, gotSB.st_size); + XCTAssertEqual(50, file->InternalBufferSize()); + + // After the second write, the buffer is flushed. File size 100, buffer is 0. + file->Write(std::vector(writeSize, 'B')); + WaitForBufferSize(file, 0); + XCTAssertEqual(fstat(file->FileHandle().fileDescriptor, &gotSB), 0); + XCTAssertEqual(100, gotSB.st_size); + XCTAssertEqual(0, file->InternalBufferSize()); +} + +@end diff --git a/Source/santad/Logs/EndpointSecurity/Writers/Null.h b/Source/santad/Logs/EndpointSecurity/Writers/Null.h new file mode 100644 index 000000000..692d7b7a1 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Writers/Null.h @@ -0,0 +1,35 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_NULL_H +#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_NULL_H + +#include +#include + +#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h" + +namespace santa::santad::logs::endpoint_security::writers { + +class Null : public Writer { + public: + // Factory + static std::shared_ptr Create(); + + void Write(std::vector&& bytes) override; +}; + +} // namespace santa::santad::logs::endpoint_security::writers + +#endif diff --git a/Source/santad/Logs/SNTProtobufEventLog.h b/Source/santad/Logs/EndpointSecurity/Writers/Null.mm similarity index 59% rename from Source/santad/Logs/SNTProtobufEventLog.h rename to Source/santad/Logs/EndpointSecurity/Writers/Null.mm index 218403336..44281c12d 100644 --- a/Source/santad/Logs/SNTProtobufEventLog.h +++ b/Source/santad/Logs/EndpointSecurity/Writers/Null.mm @@ -1,4 +1,4 @@ -/// Copyright 2021 Google Inc. All rights reserved. +/// Copyright 2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -12,10 +12,16 @@ /// See the License for the specific language governing permissions and /// limitations under the License. -#import "Source/santad/Logs/SNTEventLog.h" +#include "Source/santad/Logs/EndpointSecurity/Writers/Null.h" -@interface SNTProtobufEventLog : SNTEventLog +namespace santa::santad::logs::endpoint_security::writers { -- (void)logFileModification:(santa_message_t)message; +std::shared_ptr Null::Create() { + return std::make_shared(); +} -@end +void Null::Write(std::vector &&bytes) { + // Intentionally do nothing +} + +} // namespace santa::santad::logs::endpoint_security::writers diff --git a/Source/santad/Logs/EndpointSecurity/Writers/Syslog.h b/Source/santad/Logs/EndpointSecurity/Writers/Syslog.h new file mode 100644 index 000000000..ba490ed14 --- /dev/null +++ b/Source/santad/Logs/EndpointSecurity/Writers/Syslog.h @@ -0,0 +1,33 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_SYSLOG_H +#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_SYSLOG_H + +#include + +#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h" + +namespace santa::santad::logs::endpoint_security::writers { + +class Syslog : public Writer { + public: + static std::shared_ptr Create(); + + void Write(std::vector&& bytes) override; +}; + +} // namespace santa::santad::logs::endpoint_security::writers + +#endif diff --git a/Source/santad/EventProviders/SNTCachingEndpointSecurityManager.h b/Source/santad/Logs/EndpointSecurity/Writers/Syslog.mm similarity index 56% rename from Source/santad/EventProviders/SNTCachingEndpointSecurityManager.h rename to Source/santad/Logs/EndpointSecurity/Writers/Syslog.mm index eea7355d4..08b565cf2 100644 --- a/Source/santad/EventProviders/SNTCachingEndpointSecurityManager.h +++ b/Source/santad/Logs/EndpointSecurity/Writers/Syslog.mm @@ -1,4 +1,4 @@ -/// Copyright 2021 Google Inc. All rights reserved. +/// Copyright 2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -12,9 +12,18 @@ /// See the License for the specific language governing permissions and /// limitations under the License. -#import +#include "Source/santad/Logs/EndpointSecurity/Writers/Syslog.h" -#include "Source/santad/EventProviders/SNTEndpointSecurityManager.h" +#include -@interface SNTCachingEndpointSecurityManager : SNTEndpointSecurityManager -@end +namespace santa::santad::logs::endpoint_security::writers { + +std::shared_ptr Syslog::Create() { + return std::make_shared(); +} + +void Syslog::Write(std::vector &&bytes) { + os_log(OS_LOG_DEFAULT, "%{public}s", bytes.data()); +} + +} // namespace santa::santad::logs::endpoint_security::writers diff --git a/Source/santad/SNTApplication.h b/Source/santad/Logs/EndpointSecurity/Writers/Writer.h similarity index 57% rename from Source/santad/SNTApplication.h rename to Source/santad/Logs/EndpointSecurity/Writers/Writer.h index b02bfdc14..604b605d8 100644 --- a/Source/santad/SNTApplication.h +++ b/Source/santad/Logs/EndpointSecurity/Writers/Writer.h @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -12,16 +12,20 @@ /// See the License for the specific language governing permissions and /// limitations under the License. -#import +#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_WRITER_H +#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_WRITER_H -/// -/// The main controller class for santad -/// -@interface SNTApplication : NSObject +#include -/// -/// Begins fielding requests from the driver -/// -- (void)start; +namespace santa::santad::logs::endpoint_security::writers { + +class Writer { + public: + virtual ~Writer() = default; + + virtual void Write(std::vector&& bytes) = 0; +}; + +} // namespace santa::santad::logs::endpoint_security::writers -@end +#endif diff --git a/Source/santad/Logs/SNTEventLog.h b/Source/santad/Logs/SNTEventLog.h deleted file mode 100644 index 6b68c79e8..000000000 --- a/Source/santad/Logs/SNTEventLog.h +++ /dev/null @@ -1,68 +0,0 @@ -/// Copyright 2018 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import - -#include "Source/common/SNTCommon.h" - -@class SNTCachedDecision; -@class SNTStoredEvent; -@class SNTAllowlistInfo; - -/// -/// Abstract interface for logging execution and file write events to syslog -/// -@interface SNTEventLog : NSObject - -// Getter for a singleton SNTEventLog object. -// Determines which type of SNTEventLog to use based on [SNTConfigurator eventLogType]. -+ (instancetype)logger; - -// Methods implemented by a concrete subclass. -- (void)logDiskAppeared:(NSDictionary *)diskProperties; -- (void)logDiskDisappeared:(NSDictionary *)diskProperties; -- (void)logFileModification:(santa_message_t)message; -- (void)logDeniedExecution:(SNTCachedDecision *)cd withMessage:(santa_message_t)message; -- (void)logAllowedExecution:(santa_message_t)message; -- (void)logBundleHashingEvents:(NSArray *)events; -- (void)logFork:(santa_message_t)message; -- (void)logExit:(santa_message_t)message; -- (void)logAllowlist:(SNTAllowlistInfo *)allowlistInfo; -- (void)forceFlush; - -// Methods for storing, retrieving, and removing cached decisions. -- (void)cacheDecision:(SNTCachedDecision *)cd; -- (SNTCachedDecision *)cachedDecisionForMessage:(santa_message_t)message; -- (void)forgetCachedDecisionForVnodeId:(santa_vnode_id_t)vnodeId; - -// Method used to record the freshness of transitive rules. -- (void)resetTimestampForCachedDecision:(SNTCachedDecision *)cd; - -// String formatter helpers. -- (void)addArgsForPid:(pid_t)pid toString:(NSMutableString *)str; -- (NSString *)diskImageForDevice:(NSString *)devPath; -- (NSString *)nameForUID:(uid_t)uid; -- (NSString *)nameForGID:(gid_t)gid; -- (NSString *)sanitizeString:(NSString *)inStr; -- (NSString *)serialForDevice:(NSString *)devPath; -- (NSString *)originalPathForTranslocation:(const santa_message_t *)message; - -// A cache for usernames and groups. -@property(readonly, nonatomic) NSCache *userNameMap; -@property(readonly, nonatomic) NSCache *groupNameMap; - -// A UTC Date formatter. -@property(readonly, nonatomic) NSDateFormatter *dateFormatter; -@property(readonly, nonatomic) NSString *machineID; -@end diff --git a/Source/santad/Logs/SNTEventLog.m b/Source/santad/Logs/SNTEventLog.m deleted file mode 100644 index 6e68508e8..000000000 --- a/Source/santad/Logs/SNTEventLog.m +++ /dev/null @@ -1,459 +0,0 @@ -/// Copyright 2018 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -#import "Source/santad/Logs/SNTEventLog.h" - -#include -#include -#include -#include - -#import "Source/common/SNTCachedDecision.h" -#import "Source/common/SNTConfigurator.h" -#import "Source/common/SNTRule.h" -#import "Source/santad/DataLayer/SNTRuleTable.h" -#import "Source/santad/SNTDatabaseController.h" - -#import "Source/santad/Logs/SNTFileEventLog.h" -#import "Source/santad/Logs/SNTProtobufEventLog.h" -#import "Source/santad/Logs/SNTSyslogEventLog.h" - -@interface SNTEventLog () -@property NSMutableDictionary *detailStore; -@property dispatch_queue_t detailStoreQueue; -// Cache for sha256 -> date of last timestamp reset. -@property NSCache *timestampResetMap; -@end - -@implementation SNTEventLog - -- (instancetype)init { - self = [super init]; - if (self) { - _detailStore = [NSMutableDictionary dictionaryWithCapacity:10000]; - _detailStoreQueue = - dispatch_queue_create("com.google.santad.detail_store", DISPATCH_QUEUE_SERIAL); - - _userNameMap = [[NSCache alloc] init]; - _userNameMap.countLimit = 100; - _groupNameMap = [[NSCache alloc] init]; - _groupNameMap.countLimit = 100; - _timestampResetMap = [[NSCache alloc] init]; - _timestampResetMap.countLimit = 100; - - _dateFormatter = [[NSDateFormatter alloc] init]; - _dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; - _dateFormatter.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601]; - _dateFormatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; - - // Grab the system UUID on init - _machineID = [[SNTConfigurator configurator] machineID]; - } - return self; -} - -- (void)logDiskAppeared:(NSDictionary *)diskProperties { - [self doesNotRecognizeSelector:_cmd]; -} - -- (void)logDiskDisappeared:(NSDictionary *)diskProperties { - [self doesNotRecognizeSelector:_cmd]; -} - -- (void)logFileModification:(santa_message_t)message { - [self doesNotRecognizeSelector:_cmd]; -} - -- (void)logDeniedExecution:(SNTCachedDecision *)cd withMessage:(santa_message_t)message { - [self doesNotRecognizeSelector:_cmd]; -} - -- (void)logAllowedExecution:(santa_message_t)message { - [self doesNotRecognizeSelector:_cmd]; -} - -- (void)logBundleHashingEvents:(NSArray *)events { - [self doesNotRecognizeSelector:_cmd]; -} - -- (void)logFork:(santa_message_t)message { - [self doesNotRecognizeSelector:_cmd]; -} -- (void)logExit:(santa_message_t)message { - [self doesNotRecognizeSelector:_cmd]; -} - -- (void)logAllowlist:(SNTAllowlistInfo *)allowlistInfo { - [self doesNotRecognizeSelector:_cmd]; -} - -- (void)forceFlush { - [self doesNotRecognizeSelector:_cmd]; -} - -- (void)cacheDecision:(SNTCachedDecision *)cd { - dispatch_sync(self.detailStoreQueue, ^{ - self.detailStore[@(cd.vnodeId.fileid)] = cd; - }); -} - -- (SNTCachedDecision *)cachedDecisionForMessage:(santa_message_t)message { - __block SNTCachedDecision *cd; - dispatch_sync(self.detailStoreQueue, ^{ - cd = self.detailStore[@(message.vnode_id.fileid)]; - }); - return cd; -} - -- (void)forgetCachedDecisionForVnodeId:(santa_vnode_id_t)vnodeId { - dispatch_sync(self.detailStoreQueue, ^{ - [self.detailStore removeObjectForKey:@(vnodeId.fileid)]; - }); -} - -// Whenever a cached decision resulting from a transitive allowlist rule is used to allow the -// execution of a binary, we update the timestamp on the transitive rule in the rules database. -// To prevent writing to the database too often, we space out consecutive writes by 3600 seconds. -- (void)resetTimestampForCachedDecision:(SNTCachedDecision *)cd { - if (cd.decision != SNTEventStateAllowTransitive) return; - NSDate *lastUpdate = [self.timestampResetMap objectForKey:cd.sha256]; - if (!lastUpdate || -[lastUpdate timeIntervalSinceNow] > 3600) { - SNTRule *rule = [[SNTRule alloc] initWithIdentifier:cd.sha256 - state:SNTRuleStateAllowTransitive - type:SNTRuleTypeBinary - customMsg:nil]; - [[SNTDatabaseController ruleTable] resetTimestampForRule:rule]; - [self.timestampResetMap setObject:[NSDate date] forKey:cd.sha256]; - } -} - -/** - Sanitizes a given string if necessary, otherwise returns the original. -*/ -- (NSString *)sanitizeString:(NSString *)inStr { - NSUInteger length = [inStr lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - if (length < 1) return inStr; - - NSString *ret = [self sanitizeCString:inStr.UTF8String ofLength:length]; - if (ret) { - return ret; - } - return inStr; -} - -/** - Sanitize the given C-string, replacing |, \n and \r characters. - - @return a new NSString with the replaced contents, if necessary, otherwise nil. -*/ -- (NSString *)sanitizeCString:(const char *)str ofLength:(NSUInteger)length { - NSUInteger bufOffset = 0, strOffset = 0; - char c = 0; - char *buf = NULL; - BOOL shouldFree = NO; - - if (length < 1) return @""; - - // Loop through the string one character at a time, looking for the characters - // we want to remove. - for (const char *p = str; (c = *p) != 0; ++p) { - if (c == '|' || c == '\n' || c == '\r') { - if (!buf) { - // If string size * 6 is more than 64KiB use malloc, otherwise use stack space. - if (length * 6 > 64 * 1024) { - buf = malloc(length * 6); - shouldFree = YES; - } else { - buf = alloca(length * 6); - } - } - - // Copy from the last offset up to the character we just found into the buffer - ptrdiff_t diff = p - str; - memcpy(buf + bufOffset, str + strOffset, diff - strOffset); - - // Update the buffer and string offsets - bufOffset += diff - strOffset; - strOffset = diff + 1; - - // Replace the found character and advance the buffer offset - switch (c) { - case '|': - memcpy(buf + bufOffset, "", 6); - bufOffset += 6; - break; - case '\n': - memcpy(buf + bufOffset, "\\n", 2); - bufOffset += 2; - break; - case '\r': - memcpy(buf + bufOffset, "\\r", 2); - bufOffset += 2; - break; - } - } - } - - if (strOffset > 0 && strOffset < length) { - // Copy any characters from the last match to the end of the string into the buffer. - memcpy(buf + bufOffset, str + strOffset, length - strOffset); - bufOffset += length - strOffset; - } - - if (buf) { - // Only return a new string if there were matches - NSString *ret = [[NSString alloc] initWithBytes:buf - length:bufOffset - encoding:NSUTF8StringEncoding]; - if (shouldFree) { - free(buf); - } - - return ret; - } - return nil; -} - -/** - Use sysctl to get the arguments for a PID, appended to the given string. -*/ -- (void)addArgsForPid:(pid_t)pid toString:(NSMutableString *)str { - size_t argsSizeEstimate = 0, argsSize = 0, index = 0; - - // Use stack space up to 128KiB. - const size_t MAX_STACK_ALLOC = 128 * 1024; - char *bytes = alloca(MAX_STACK_ALLOC); - BOOL shouldFree = NO; - - int mib[] = {CTL_KERN, KERN_PROCARGS2, pid}; - - // Get estimated length of arg array - if (sysctl(mib, 3, NULL, &argsSizeEstimate, NULL, 0) < 0) return; - argsSize = argsSizeEstimate + 512; - - // If this is larger than our allocated stack space, alloc from heap. - if (argsSize > MAX_STACK_ALLOC) { - bytes = malloc(argsSize); - shouldFree = YES; - } - - // Get the args. If this fails, free if necessary and return. - if (sysctl(mib, 3, bytes, &argsSize, NULL, 0) != 0 || argsSize >= argsSizeEstimate + 512) { - if (shouldFree) { - free(bytes); - } - return; - } - - // Get argc, set index to the end of argc - int argc = 0; - memcpy(&argc, &bytes[0], sizeof(argc)); - index = sizeof(argc); - - // Skip past end of executable path and trailing NULLs - for (; index < argsSize; ++index) { - if (bytes[index] == '\0') { - ++index; - break; - } - } - for (; index < argsSize; ++index) { - if (bytes[index] != '\0') break; - } - - // Save the beginning of the arguments - size_t stringStart = index; - - // Replace all NULLs with spaces up until the first environment variable - int replacedNulls = 0; - for (; index < argsSize; ++index) { - if (bytes[index] == '\0') { - ++replacedNulls; - if (replacedNulls == argc) break; - bytes[index] = ' '; - } - } - - // Potentially sanitize the args string. - NSString *sanitized = [self sanitizeCString:&bytes[stringStart] ofLength:index - stringStart]; - if (sanitized) { - [str appendFormat:@"|args=%@", sanitized]; - } else { - [str appendFormat:@"|args=%@", @(&bytes[stringStart])]; - } - - if (shouldFree) { - free(bytes); - } -} - -- (NSString *)nameForUID:(uid_t)uid { - NSNumber *uidNumber = @(uid); - - NSString *name = [self.userNameMap objectForKey:uidNumber]; - if (name) return name; - - struct passwd *pw = getpwuid(uid); - if (pw) { - name = @(pw->pw_name); - [self.userNameMap setObject:name forKey:uidNumber]; - } - return name; -} - -- (NSString *)nameForGID:(gid_t)gid { - NSNumber *gidNumber = @(gid); - - NSString *name = [self.groupNameMap objectForKey:gidNumber]; - if (name) return name; - - struct group *gr = getgrgid(gid); - if (gr) { - name = @(gr->gr_name); - [self.groupNameMap setObject:name forKey:gidNumber]; - } - return name; -} - -/** - Given an IOKit device path (like those provided by DiskArbitration), find the disk - image path by looking up the device in the IOKit registry and getting its properties. - - This is largely the same as the way hdiutil gathers info for the "info" command. -*/ -- (NSString *)diskImageForDevice:(NSString *)devPath { - devPath = [devPath stringByDeletingLastPathComponent]; - if (!devPath.length) return nil; - io_registry_entry_t device = IORegistryEntryFromPath(kIOMasterPortDefault, devPath.UTF8String); - CFMutableDictionaryRef deviceProperties = NULL; - IORegistryEntryCreateCFProperties(device, &deviceProperties, kCFAllocatorDefault, kNilOptions); - NSDictionary *properties = CFBridgingRelease(deviceProperties); - IOObjectRelease(device); - - NSData *pathData = properties[@"image-path"]; - NSString *result = [[NSString alloc] initWithData:pathData encoding:NSUTF8StringEncoding]; - - return result; -} - -/** - Given an IOKit device path (like those provided by DiskArbitration), find the device serial number, - if there is one. This has only really been tested with USB and internal devices. -*/ -- (NSString *)serialForDevice:(NSString *)devPath { - if (!devPath.length) return nil; - NSString *serial; - io_registry_entry_t device = IORegistryEntryFromPath(kIOMasterPortDefault, devPath.UTF8String); - while (!serial && device) { - CFMutableDictionaryRef deviceProperties = NULL; - IORegistryEntryCreateCFProperties(device, &deviceProperties, kCFAllocatorDefault, kNilOptions); - NSDictionary *properties = CFBridgingRelease(deviceProperties); - if (properties[@"Serial Number"]) { - serial = properties[@"Serial Number"]; - } else if (properties[@"kUSBSerialNumberString"]) { - serial = properties[@"kUSBSerialNumberString"]; - } - - if (serial) { - IOObjectRelease(device); - break; - } - - io_registry_entry_t parent; - IORegistryEntryGetParentEntry(device, kIOServicePlane, &parent); - IOObjectRelease(device); - device = parent; - } - - return serial; -} - -/** - Uses the executable path, uid, and gid from a given santa_message_t to determine if the path - has been translocated by GateKeeper and if so, returns the original path of the executable. This - requires macOS 10.12 or higher. We use dlopen to access the functions we need in - Security.framework so that we can still build against the 10.11 SDK. If the path has not been - translocated or if running on macOS prior to 10.12, this method returns nil. - */ -- (NSString *)originalPathForTranslocation:(const santa_message_t *)message { - if (!message) { - return nil; - } - - // The first time this function is called, we attempt to find the addresses of - // SecTranslocateIsTranslocatedURL and SecTranslocateCreateOriginalPathForURL inside of the - // Security.framework library. If we were successful, handle will be non-NULL and is never - // closed. - static Boolean (*IsTranslocatedURL)(CFURLRef, bool *, CFErrorRef *) = NULL; - static CFURLRef __nullable (*CreateOriginalPathForURL)(CFURLRef, CFErrorRef *) = NULL; - static dispatch_once_t token; - dispatch_once(&token, ^{ - void *handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY); - if (handle) { - IsTranslocatedURL = dlsym(handle, "SecTranslocateIsTranslocatedURL"); - CreateOriginalPathForURL = dlsym(handle, "SecTranslocateCreateOriginalPathForURL"); - if (!IsTranslocatedURL || !CreateOriginalPathForURL) { - IsTranslocatedURL = NULL; - CreateOriginalPathForURL = NULL; - dlclose(handle); - } - } - }); - - // If we couldn't open the library or find the functions we need, don't do anything. - if (!IsTranslocatedURL || !CreateOriginalPathForURL) return nil; - - // Determine if the executable URL has been translocated or not. - CFURLRef cfExecURL = (__bridge CFURLRef)[NSURL fileURLWithPath:@(message->path)]; - bool isTranslocated = false; - if (!IsTranslocatedURL(cfExecURL, &isTranslocated, NULL) || !isTranslocated) return nil; - - // SecTranslocateCreateOriginalPathForURL requires that our uid be the same as the user who - // launched the executable. So we temporarily drop from root down to this uid, then reset. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated" - pthread_setugid_np(message->uid, message->gid); - NSURL *origURL = CFBridgingRelease(CreateOriginalPathForURL(cfExecURL, NULL)); - pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE); -#pragma clang diagnostic pop - - return [origURL path]; // this will be nil if there was an error -} - -+ (instancetype)logger { - static SNTEventLog *logger = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - switch ([[SNTConfigurator configurator] eventLogType]) { - case SNTEventLogTypeSyslog: { - logger = [[SNTSyslogEventLog alloc] init]; - break; - } - case SNTEventLogTypeFilelog: { - logger = [[SNTFileEventLog alloc] init]; - break; - } - case SNTEventLogTypeProtobuf: { - logger = [[SNTProtobufEventLog alloc] init]; - break; - } - case SNTEventLogTypeNull: { - // Messages sent to nil objects do nothing, which is perfect for a null logger. - logger = nil; - break; - } - } - }); - return logger; -} -@end diff --git a/Source/santad/Logs/SNTFileEventLog.h b/Source/santad/Logs/SNTFileEventLog.h deleted file mode 100644 index d03ea6572..000000000 --- a/Source/santad/Logs/SNTFileEventLog.h +++ /dev/null @@ -1,18 +0,0 @@ -/// Copyright 2018 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import "Source/santad/Logs/SNTSyslogEventLog.h" - -@interface SNTFileEventLog : SNTSyslogEventLog -@end diff --git a/Source/santad/Logs/SNTFileEventLog.m b/Source/santad/Logs/SNTFileEventLog.m deleted file mode 100644 index 9851a6165..000000000 --- a/Source/santad/Logs/SNTFileEventLog.m +++ /dev/null @@ -1,105 +0,0 @@ -/// Copyright 2018 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import "Source/santad/Logs/SNTFileEventLog.h" - -#import "Source/common/SNTConfigurator.h" -#import "Source/common/SNTLogging.h" -#import "Source/common/SNTStrengthify.h" - -@interface SNTFileEventLog () -@property NSFileHandle *fh; -@property(readonly, nonatomic) dispatch_queue_t q; -@property dispatch_source_t source; -@property(readonly, nonatomic) dispatch_source_t timer; -@property(readonly, nonatomic) NSString *path; -@property(readonly, nonatomic) NSMutableData *buffer; -@end - -@implementation SNTFileEventLog - -- (instancetype)init { - self = [super init]; - if (self) { - _q = dispatch_queue_create("com.google.santa.file_event_log", DISPATCH_QUEUE_SERIAL); - _path = [[SNTConfigurator configurator] eventLogPath]; - _fh = [self fileHandleForPath:_path]; - [self watchLogFile]; - // 8k buffer to batch logs for writing. - _buffer = [NSMutableData dataWithCapacity:8192]; - // To avoid long lulls in the log being updated, flush the buffer every second. - _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _q); - dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), NSEC_PER_SEC * 1, 0); - WEAKIFY(self); - dispatch_source_set_event_handler(_timer, ^{ - STRONGIFY(self); - [self flushBuffer]; - }); - dispatch_resume(_timer); - } - return self; -} - -- (NSFileHandle *)fileHandleForPath:(NSString *)path { - NSFileManager *fm = [NSFileManager defaultManager]; - if (![fm fileExistsAtPath:path]) { - [fm createFileAtPath:path contents:nil attributes:nil]; - } - NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:path]; - [fh seekToEndOfFile]; - return fh; -} - -- (void)watchLogFile { - if (self.source) { - dispatch_source_set_event_handler_f(self.source, NULL); - dispatch_source_cancel(self.source); - } - self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, self.fh.fileDescriptor, - DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME, self.q); - WEAKIFY(self); - dispatch_source_set_event_handler(self.source, ^{ - STRONGIFY(self); - [self.fh closeFile]; - self.fh = [self fileHandleForPath:self.path]; - [self watchLogFile]; - }); - dispatch_resume(self.source); -} - -- (void)writeLog:(NSString *)log { - dispatch_async(self.q, ^{ - NSString *dateString = [self.dateFormatter stringFromDate:[NSDate date]]; - NSString *outLog = [NSString stringWithFormat:@"[%@] I santad: %@\n", dateString, log]; - [self.buffer appendBytes:outLog.UTF8String - length:[outLog lengthOfBytesUsingEncoding:NSUTF8StringEncoding]]; - // Avoid excessive calls to write() by batching logs. - if (self.buffer.length >= 4096) { - [self flushBuffer]; - } - }); -} - -- (void)flushBuffer { - write(self.fh.fileDescriptor, self.buffer.bytes, self.buffer.length); - [self.buffer setLength:0]; -} - -- (void)forceFlush { - dispatch_sync(self.q, ^{ - [self flushBuffer]; - }); -} - -@end diff --git a/Source/santad/Logs/SNTProtobufEventLog.m b/Source/santad/Logs/SNTProtobufEventLog.m deleted file mode 100644 index 27548a551..000000000 --- a/Source/santad/Logs/SNTProtobufEventLog.m +++ /dev/null @@ -1,349 +0,0 @@ -/// Copyright 2021 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import "Source/santad/Logs/SNTProtobufEventLog.h" - -#import -#import -#import -#import - -#import "Source/common/SNTAllowlistInfo.h" -#import "Source/common/SNTCachedDecision.h" -#import "Source/common/SNTConfigurator.h" -#import "Source/common/SNTStoredEvent.h" -#import "Source/common/Santa.pbobjc.h" -#import "Source/santad/Logs/SNTSimpleMaildir.h" - -@interface SNTProtobufEventLog () -@property(readonly) id logOutput; -@end - -@implementation SNTProtobufEventLog - -- (instancetype)init { - SNTSimpleMaildir *mailDir = [[SNTSimpleMaildir alloc] - initWithBaseDirectory:[[SNTConfigurator configurator] mailDirectory] - filenamePrefix:@"out.log" - fileSizeThreshold:[[SNTConfigurator configurator] mailDirectoryFileSizeThresholdKB] * 1024 - directorySizeThreshold:[[SNTConfigurator configurator] mailDirectorySizeThresholdMB] * 1024 * - 1024 - maxTimeBetweenFlushes:[[SNTConfigurator configurator] mailDirectoryEventMaxFlushTimeSec]]; - return [self initWithLog:mailDir]; -} - -- (instancetype)initWithLog:(id)log { - if (!log) { - return nil; - } - - self = [super init]; - if (self) { - _logOutput = log; - } - return self; -} - -- (void)forceFlush { - [self.logOutput flush]; -} - -- (void)logWithSantaMessage:(santa_message_t *)santaMsg - wrapper:(void (^)(SNTPBSantaMessage *))setMessage { - SNTPBSantaMessage *smpb = [[SNTPBSantaMessage alloc] init]; - if (santaMsg && santaMsg->es_message) { - struct timespec ts = ((es_message_t *)santaMsg->es_message)->time; - NSDate *esTime = - [NSDate dateWithTimeIntervalSince1970:(double)(ts.tv_sec) + (((double)ts.tv_nsec) / 1E9)]; - smpb.eventTime = [[GPBTimestamp alloc] initWithDate:esTime]; - } else { - smpb.eventTime = [[GPBTimestamp alloc] initWithDate:[NSDate date]]; - } - setMessage(smpb); - [self.logOutput logEvent:smpb]; -} - -- (void)logWithWrapper:(void (^)(SNTPBSantaMessage *))setMessage { - return [self logWithSantaMessage:nil wrapper:setMessage]; -} - -- (SNTPBFileModification_Action)protobufActionForSantaMessageAction:(santa_action_t)action { - switch (action) { - case ACTION_NOTIFY_DELETE: return SNTPBFileModification_Action_ActionDelete; - case ACTION_NOTIFY_EXCHANGE: return SNTPBFileModification_Action_ActionExchange; - case ACTION_NOTIFY_LINK: return SNTPBFileModification_Action_ActionLink; - case ACTION_NOTIFY_RENAME: return SNTPBFileModification_Action_ActionRename; - case ACTION_NOTIFY_WRITE: return SNTPBFileModification_Action_ActionWrite; - default: return SNTPBFileModification_Action_ActionUnknown; - } -} - -- (NSString *)newpathForSantaMessage:(santa_message_t *)message { - if (!message) { - return nil; - } - - switch (message->action) { - case ACTION_NOTIFY_EXCHANGE: OS_FALLTHROUGH; - case ACTION_NOTIFY_LINK: OS_FALLTHROUGH; - case ACTION_NOTIFY_RENAME: return @(message->newpath); - default: return nil; - } -} - -- (NSString *)processPathForSantaMessage:(santa_message_t *)message { - if (!message) { - return nil; - } - - // If we have an ES message, use the path provided by the ES framework. - // Otherwise, attempt to lookup the path. Note that this will fail if the - // process being queried has already exited. - if (message->es_message) { - switch (message->action) { - case ACTION_NOTIFY_DELETE: OS_FALLTHROUGH; - case ACTION_NOTIFY_EXCHANGE: OS_FALLTHROUGH; - case ACTION_NOTIFY_LINK: OS_FALLTHROUGH; - case ACTION_NOTIFY_RENAME: OS_FALLTHROUGH; - case ACTION_NOTIFY_WRITE: { - return @(((es_message_t *)message->es_message)->process->executable->path.data); - } - default: return nil; - } - } else { - char path[PATH_MAX]; - path[0] = '\0'; - proc_pidpath(message->pid, path, sizeof(path)); - return @(path); - } -} - -- (SNTPBProcessInfo *)protobufProcessInfoForSantaMessage:(santa_message_t *)message { - if (!message) { - return nil; - } - - SNTPBProcessInfo *procInfo = [[SNTPBProcessInfo alloc] init]; - - procInfo.pid = message->pid; - procInfo.pidversion = message->pidversion; - procInfo.ppid = message->ppid; - procInfo.uid = message->uid; - procInfo.gid = message->gid; - - procInfo.user = [self nameForUID:message->uid]; - procInfo.group = [self nameForGID:message->gid]; - - return procInfo; -} - -- (SNTPBExecution_Decision)protobufDecisionForCachedDecision:(SNTCachedDecision *)cd { - if (cd.decision & SNTEventStateBlock) { - return SNTPBExecution_Decision_DecisionDeny; - } else if (cd.decision & SNTEventStateAllow) { - return SNTPBExecution_Decision_DecisionAllow; - } else { - return SNTPBExecution_Decision_DecisionUnknown; - } -} - -- (SNTPBExecution_Reason)protobufReasonForCachedDecision:(SNTCachedDecision *)cd { - switch (cd.decision) { - case SNTEventStateAllowBinary: return SNTPBExecution_Reason_ReasonBinary; - case SNTEventStateAllowCompiler: return SNTPBExecution_Reason_ReasonCompiler; - case SNTEventStateAllowTransitive: return SNTPBExecution_Reason_ReasonTransitive; - case SNTEventStateAllowPendingTransitive: return SNTPBExecution_Reason_ReasonPendingTransitive; - case SNTEventStateAllowCertificate: return SNTPBExecution_Reason_ReasonCert; - case SNTEventStateAllowScope: return SNTPBExecution_Reason_ReasonScope; - case SNTEventStateAllowTeamID: return SNTPBExecution_Reason_ReasonTeamId; - case SNTEventStateAllowUnknown: return SNTPBExecution_Reason_ReasonUnknown; - case SNTEventStateBlockBinary: return SNTPBExecution_Reason_ReasonBinary; - case SNTEventStateBlockCertificate: return SNTPBExecution_Reason_ReasonCert; - case SNTEventStateBlockScope: return SNTPBExecution_Reason_ReasonScope; - case SNTEventStateBlockTeamID: return SNTPBExecution_Reason_ReasonTeamId; - case SNTEventStateBlockUnknown: return SNTPBExecution_Reason_ReasonUnknown; - - case SNTEventStateAllow: OS_FALLTHROUGH; - case SNTEventStateUnknown: OS_FALLTHROUGH; - case SNTEventStateBundleBinary: OS_FALLTHROUGH; - case SNTEventStateBlock: return SNTPBExecution_Reason_ReasonNotRunning; - } - - return SNTPBExecution_Reason_ReasonUnknown; -} - -- (SNTPBExecution_Mode)protobufModeForClientMode:(SNTClientMode)mode { - switch (mode) { - case SNTClientModeMonitor: return SNTPBExecution_Mode_ModeMonitor; - case SNTClientModeLockdown: return SNTPBExecution_Mode_ModeLockdown; - case SNTClientModeUnknown: return SNTPBExecution_Mode_ModeUnknown; - } - return SNTPBExecution_Mode_ModeUnknown; -} - -- (void)logFileModification:(santa_message_t)message { - SNTPBFileModification *fileMod = [[SNTPBFileModification alloc] init]; - - fileMod.action = [self protobufActionForSantaMessageAction:message.action]; - fileMod.path = @(message.path); - fileMod.newpath = [self newpathForSantaMessage:&message]; - fileMod.process = @(message.pname); - fileMod.processPath = [self processPathForSantaMessage:&message]; - fileMod.processInfo = [self protobufProcessInfoForSantaMessage:&message]; - fileMod.machineId = - [[SNTConfigurator configurator] enableMachineIDDecoration] ? self.machineID : nil; - - [self logWithSantaMessage:&message - wrapper:^(SNTPBSantaMessage *sm) { - sm.fileModification = fileMod; - }]; -} - -- (void)logExecution:(santa_message_t)message withDecision:(SNTCachedDecision *)cd { - SNTPBExecution *exec = [[SNTPBExecution alloc] init]; - exec.decision = [self protobufDecisionForCachedDecision:cd]; - exec.reason = [self protobufReasonForCachedDecision:cd]; - exec.explain = cd.decisionExtra; - exec.sha256 = cd.sha256; - exec.certSha256 = cd.certSHA256; - exec.certCn = cd.certCommonName; - exec.quarantineURL = cd.quarantineURL; - exec.processInfo = [self protobufProcessInfoForSantaMessage:&message]; - exec.mode = [self protobufModeForClientMode:[[SNTConfigurator configurator] clientMode]]; - exec.path = @(message.path); - exec.originalPath = [self originalPathForTranslocation:&message]; - exec.argsArray = [(__bridge NSArray *)message.args_array mutableCopy]; - exec.machineId = - [[SNTConfigurator configurator] enableMachineIDDecoration] ? self.machineID : nil; - exec.teamId = cd.teamID; - - [self logWithSantaMessage:&message - wrapper:^(SNTPBSantaMessage *sm) { - sm.execution = exec; - }]; -} - -- (void)logDeniedExecution:(SNTCachedDecision *)cd withMessage:(santa_message_t)message { - [self logExecution:message withDecision:cd]; -} - -- (void)logAllowedExecution:(santa_message_t)message { - SNTCachedDecision *cd = [self cachedDecisionForMessage:message]; - [self logExecution:message withDecision:cd]; - - // We also reset the timestamp for transitive rules here, because it happens to be where we - // have access to both the execution notification and the sha256 associated with rule. - [self resetTimestampForCachedDecision:cd]; -} - -- (void)logDiskAppeared:(NSDictionary *)diskProperties { - NSString *dmgPath = nil; - NSString *serial = nil; - if ([diskProperties[@"DADeviceModel"] isEqual:@"Disk Image"]) { - dmgPath = [self diskImageForDevice:diskProperties[@"DADevicePath"]]; - } else { - serial = [self serialForDevice:diskProperties[@"DADevicePath"]]; - serial = [serial stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - } - - NSString *model = [NSString stringWithFormat:@"%@ %@", diskProperties[@"DADeviceVendor"] ?: @"", - diskProperties[@"DADeviceModel"] ?: @""]; - model = [model stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - - NSString *appearanceDateString = [self.dateFormatter - stringFromDate:[NSDate - dateWithTimeIntervalSinceReferenceDate:[diskProperties[@"DAAppearanceTime"] - doubleValue]]]; - - SNTPBDiskAppeared *diskAppeared = [[SNTPBDiskAppeared alloc] init]; - diskAppeared.mount = [diskProperties[@"DAVolumePath"] path]; - diskAppeared.volume = diskProperties[@"DAVolumeName"]; - diskAppeared.bsdName = diskProperties[@"DAMediaBSDName"]; - diskAppeared.fs = diskProperties[@"DAVolumeKind"]; - diskAppeared.model = model; - diskAppeared.serial = serial; - diskAppeared.bus = diskProperties[@"DADeviceProtocol"]; - diskAppeared.dmgPath = dmgPath; - diskAppeared.appearance = appearanceDateString; - - [self logWithWrapper:^(SNTPBSantaMessage *sm) { - sm.diskAppeared = diskAppeared; - }]; -} - -- (void)logDiskDisappeared:(NSDictionary *)diskProperties { - SNTPBDiskDisappeared *diskDisappeared = [[SNTPBDiskDisappeared alloc] init]; - - diskDisappeared.mount = [diskProperties[@"DAVolumePath"] path]; - diskDisappeared.volume = diskProperties[@"DAVolumeName"]; - diskDisappeared.bsdName = diskProperties[@"DAMediaBSDName"]; - - [self logWithWrapper:^(SNTPBSantaMessage *sm) { - sm.diskDisappeared = diskDisappeared; - }]; -} - -- (void)logBundleHashingEvents:(NSArray *)events { - for (SNTStoredEvent *event in events) { - SNTPBBundle *bundle = [[SNTPBBundle alloc] init]; - - bundle.sha256 = event.fileSHA256; - bundle.bundleHash = event.fileBundleHash; - bundle.bundleName = event.fileBundleName; - bundle.bundleId = event.fileBundleID; - bundle.bundlePath = event.fileBundlePath; - bundle.path = event.filePath; - - [self logWithWrapper:^(SNTPBSantaMessage *sm) { - sm.bundle = bundle; - }]; - } -} - -- (void)logFork:(santa_message_t)message { - SNTPBFork *forkEvent = [[SNTPBFork alloc] init]; - - forkEvent.processInfo = [self protobufProcessInfoForSantaMessage:&message]; - - [self logWithSantaMessage:&message - wrapper:^(SNTPBSantaMessage *sm) { - sm.fork = forkEvent; - }]; -} - -- (void)logExit:(santa_message_t)message { - SNTPBExit *exitEvent = [[SNTPBExit alloc] init]; - - exitEvent.processInfo = [self protobufProcessInfoForSantaMessage:&message]; - - [self logWithSantaMessage:&message - wrapper:^(SNTPBSantaMessage *sm) { - sm.exit = exitEvent; - }]; -} - -- (void)logAllowlist:(SNTAllowlistInfo *)allowlistInfo { - SNTPBAllowlist *allowlistEvent = [[SNTPBAllowlist alloc] init]; - - allowlistEvent.pid = allowlistInfo.pid; - allowlistEvent.pidversion = allowlistInfo.pidversion; - allowlistEvent.path = allowlistInfo.targetPath; - allowlistEvent.sha256 = allowlistInfo.sha256; - - [self logWithWrapper:^(SNTPBSantaMessage *sm) { - sm.allowlist = allowlistEvent; - }]; -} - -@end diff --git a/Source/santad/Logs/SNTProtobufEventLogTest.m b/Source/santad/Logs/SNTProtobufEventLogTest.m deleted file mode 100644 index 07f56c555..000000000 --- a/Source/santad/Logs/SNTProtobufEventLogTest.m +++ /dev/null @@ -1,438 +0,0 @@ -/// Copyright 2021 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import - -#import -#import -#import -#import - -#import "Source/common/SNTAllowlistInfo.h" -#import "Source/common/SNTCachedDecision.h" -#import "Source/common/SNTConfigurator.h" -#import "Source/common/SNTLogging.h" -#import "Source/common/SNTStoredEvent.h" -#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h" -#import "Source/santad/Logs/SNTProtobufEventLog.h" -#import "Source/santad/Logs/SNTSimpleMaildir.h" - -@interface SNTProtobufEventLogTest : XCTestCase -@property id mockConfigurator; -@property id mockLogOutput; -@end - -@interface SNTProtobufEventLog (Testing) -- (instancetype)initWithLog:(id)log; -@end - -NSString *getBestBundleName(NSBundle *bundle) { - NSDictionary *infoDict = [bundle infoDictionary]; - return [[infoDict objectForKey:@"CFBundleDisplayName"] description] - ?: [[infoDict objectForKey:@"CFBundleName"] description]; -} - -SNTStoredEvent *createTestBundleStoredEvent(NSBundle *bundle, NSString *fakeBundleHash, - NSString *fakeFileHash) { - if (!bundle) { - return nil; - } - - SNTStoredEvent *event = [[SNTStoredEvent alloc] init]; - - event.idx = @(arc4random()); - event.fileSHA256 = fakeFileHash; - event.fileBundleHash = fakeBundleHash; - event.fileBundleName = getBestBundleName(bundle); - event.fileBundleID = bundle.bundleIdentifier; - event.filePath = bundle.executablePath; - event.fileBundlePath = bundle.bundlePath; - - return event; -} - -id getEventForMessage(SNTPBSantaMessage *santaMsg, SNTPBSantaMessage_Message_OneOfCase expectedCase, - NSString *propertyName, Class expectedClass) { - if (santaMsg.messageOneOfCase != expectedCase) { - LOGE(@"Unexpected message type. Had: %d, Expected: %d", santaMsg.messageOneOfCase, - expectedCase); - return nil; - } - - id event = [santaMsg valueForKey:propertyName]; - XCTAssertTrue([event isKindOfClass:expectedClass], "Extracted unexpected class"); - - return event; -} - -NSBundle *getBundleForSystemApplication(NSString *appName) { - if (@available(macOS 10.15, *)) { - return - [NSBundle bundleWithPath:[NSString stringWithFormat:@"/System/Applications/%@", appName]]; - } else { - return [NSBundle bundleWithPath:[NSString stringWithFormat:@"/Applications/%@", appName]]; - } -} - -void assertProcessInfoMatchesExpected(SNTPBProcessInfo *procInfo, const santa_message_t *expected) { - XCTAssertTrue(procInfo.pid == expected->pid); - XCTAssertTrue(procInfo.pidversion == expected->pidversion); - XCTAssertTrue(procInfo.ppid == expected->ppid); - XCTAssertTrue(procInfo.uid == expected->uid); - XCTAssertTrue(procInfo.gid == expected->gid); - XCTAssertTrue([procInfo.user isEqualToString:@(user_from_uid(expected->uid, 0))]); - XCTAssertTrue([procInfo.group isEqualToString:@(group_from_gid(expected->gid, 0))]); -} - -// Creates a basic santa message with only process-related info filled out. -// Adding path data is left as an exercise to the caller. -santa_message_t getBasicSantaMessage(santa_action_t action) { - santa_message_t santaMsg = {0}; - - santaMsg.action = action; - santaMsg.uid = 242; - santaMsg.gid = 20; - santaMsg.pid = arc4random() % 1000; - santaMsg.pidversion = arc4random() % 1000; - santaMsg.ppid = arc4random() % 1000; - - return santaMsg; -} - -@implementation SNTProtobufEventLogTest - -- (void)setUp { - self.mockConfigurator = OCMClassMock([SNTConfigurator class]); - OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator); - OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown); - OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(NO); - - self.mockLogOutput = OCMStrictProtocolMock(@protocol(SNTLogOutput)); -} - -- (void)tearDown { - [self.mockConfigurator stopMocking]; - [self.mockLogOutput stopMocking]; -} - -- (void)testLogFileModification { - NSString *processName = @"launchd"; - NSString *processPath = @"/sbin/launchd"; - NSString *sourcePath = @"/foo/bar.txt"; - NSString *targetPath = @"/bar/foo.txt"; - struct timespec ts = {123, 456}; - - // Create a test ES message with some important data set - es_file_t esFile = MakeESFile([processPath UTF8String]); - es_process_t esProc = MakeESProcess(&esFile); - es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &esProc, ts); - - santa_message_t santaMsg = getBasicSantaMessage(ACTION_NOTIFY_RENAME); - strlcpy(santaMsg.path, [sourcePath UTF8String], sizeof(santaMsg.path)); - strlcpy(santaMsg.newpath, [targetPath UTF8String], sizeof(santaMsg.newpath)); - strlcpy(santaMsg.pname, [processName UTF8String], sizeof(santaMsg.pname)); - santaMsg.args_array = nil; - santaMsg.es_message = &esMsg; - - OCMExpect([self.mockLogOutput logEvent:[OCMArg checkWithBlock:^BOOL(SNTPBSantaMessage *sm) { - // Note: Only checking seconds because nano conversion can drift - // slightly due to double precision. - if (sm.eventTime.seconds != ts.tv_sec) { - LOGE(@"Unexpected message event time"); - return NO; - } - - SNTPBFileModification *fileMod = getEventForMessage( - sm, SNTPBSantaMessage_Message_OneOfCase_FileModification, - @"fileModification", [SNTPBFileModification class]); - - if (fileMod.action != SNTPBFileModification_Action_ActionRename || - ![fileMod.path isEqualToString:sourcePath] || - ![fileMod.newpath isEqualToString:targetPath] || - ![fileMod.process isEqualToString:processName] || - ![fileMod.processPath isEqualToString:processPath] || - !fileMod.hasProcessInfo || fileMod.processInfo == nil || - [fileMod.machineId length] != 0) { - LOGE(@"Unexpected file modification data"); - return NO; - } - - assertProcessInfoMatchesExpected(fileMod.processInfo, &santaMsg); - - return YES; - }]]); - - SNTProtobufEventLog *eventLog = [[SNTProtobufEventLog alloc] initWithLog:self.mockLogOutput]; - [eventLog logFileModification:santaMsg]; - - XCTAssertTrue(OCMVerifyAll(self.mockLogOutput), "Unable to verify all expectations"); -} - -- (void)testLogExecutionDenied { - NSString *processName = @"launchd"; - NSString *processPath = @"/sbin/launchd"; - NSArray *execArgs = @[ @"/sbin/launchd", @"--init", @"--testing" ]; - NSString *explanation = @"explanation"; - NSString *sha256 = @"a4587ab1e705a3804fd23c387a7bc1b39505f699eca35f57687809f8a7031d0f"; - NSString *certSHA256 = @"293d4a40b539dfddf9e011fb0e37f19aa86c96aad2e4bf481aac9e50487f3868"; - NSString *commonName = @"my cert common name"; - NSString *quarantineURL = @"http://localhost/quarantine"; - - santa_message_t santaMsg = getBasicSantaMessage(ACTION_NOTIFY_EXEC); - strlcpy(santaMsg.path, [processPath UTF8String], sizeof(santaMsg.path)); - santaMsg.newpath[0] = '\0'; - strlcpy(santaMsg.pname, [processName UTF8String], sizeof(santaMsg.pname)); - santaMsg.args_array = (__bridge void *)execArgs; - santaMsg.es_message = nil; - - SNTCachedDecision *cachedDecision = [[SNTCachedDecision alloc] init]; - cachedDecision.decision = SNTEventStateBlockTeamID; - cachedDecision.decisionExtra = explanation; - cachedDecision.sha256 = sha256; - cachedDecision.certSHA256 = certSHA256; - cachedDecision.certCommonName = commonName; - cachedDecision.quarantineURL = quarantineURL; - - OCMExpect([self.mockLogOutput - logEvent:[OCMArg checkWithBlock:^BOOL(SNTPBSantaMessage *sm) { - SNTPBExecution *exec = getEventForMessage(sm, SNTPBSantaMessage_Message_OneOfCase_Execution, - @"execution", [SNTPBExecution class]); - - if (exec.decision != SNTPBExecution_Decision_DecisionDeny || - exec.reason != SNTPBExecution_Reason_ReasonTeamId || - ![exec.explain isEqualToString:explanation] || ![exec.sha256 isEqualToString:sha256] || - ![exec.certSha256 isEqualToString:certSHA256] || - ![exec.certCn isEqualToString:commonName] || - ![exec.quarantineURL isEqualToString:quarantineURL] || !exec.hasProcessInfo || - exec.processInfo == nil || exec.mode != SNTPBExecution_Mode_ModeLockdown || - ![exec.path isEqualToString:processPath] || [exec.originalPath length] != 0 || - ![exec.argsArray isEqualToArray:execArgs] || [exec.machineId length] != 0) { - LOGE(@"Unexpected execution data"); - return NO; - } - - assertProcessInfoMatchesExpected(exec.processInfo, &santaMsg); - - return YES; - }]]); - - SNTProtobufEventLog *eventLog = [[SNTProtobufEventLog alloc] initWithLog:self.mockLogOutput]; - [eventLog logDeniedExecution:cachedDecision withMessage:santaMsg]; - - XCTAssertTrue(OCMVerifyAll(self.mockLogOutput), "Unable to verify all expectations"); -} - -- (void)testLogDiskAppeared { - NSString *mount = @"/mnt/appear"; - NSString *volume = @"Macintosh HD"; - NSString *bsdName = @"disk0s1"; - NSString *kind = @"apfs"; - NSString *deviceVendor = @"gCorp"; - NSString *deviceModel = @"G1"; - NSString *serial = @"fake_serial"; - NSString *devicePath = @"IODeviceTree:/"; - NSString *deviceProto = @"USB"; - NSString *appeared = @"2001-01-01T00:00:00.000Z"; - - NSDictionary *diskProperties = @{ - @"DAVolumePath" : [NSURL URLWithString:mount], - @"DAVolumeName" : volume, - @"DAMediaBSDName" : bsdName, - @"DAVolumeKind" : kind, - @"DADeviceVendor" : deviceVendor, - @"DADeviceModel" : deviceModel, - @"DADevicePath" : devicePath, - @"DADeviceProtocol" : deviceProto, - }; - - OCMExpect([self.mockLogOutput - logEvent:[OCMArg checkWithBlock:^BOOL(SNTPBSantaMessage *sm) { - SNTPBDiskAppeared *diskAppeared = - getEventForMessage(sm, SNTPBSantaMessage_Message_OneOfCase_DiskAppeared, @"diskAppeared", - [SNTPBDiskAppeared class]); - - if (![diskAppeared.mount isEqualToString:mount] || - ![diskAppeared.volume isEqualToString:volume] || - ![diskAppeared.bsdName isEqualToString:bsdName] || - ![diskAppeared.fs isEqualToString:kind] || - ![diskAppeared.model - isEqualToString:[NSString stringWithFormat:@"%@ %@", deviceVendor, deviceModel]] || - ![diskAppeared.serial isEqualToString:serial] || - ![diskAppeared.bus isEqualToString:deviceProto] || [diskAppeared.dmgPath length] != 0 || - ![diskAppeared.appearance isEqualToString:appeared]) { - LOGE(@"Unexpected disk appeared data"); - return NO; - } - - return YES; - }]]); - - SNTProtobufEventLog *eventLog = [[SNTProtobufEventLog alloc] initWithLog:self.mockLogOutput]; - - id eventLogMock = OCMPartialMock(eventLog); - OCMExpect([eventLogMock serialForDevice:[OCMArg checkWithBlock:^BOOL(NSString *path) { - return [path isEqualToString:devicePath]; - }]]) - .andReturn(serial); - - [eventLog logDiskAppeared:diskProperties]; - - XCTAssertTrue(OCMVerifyAll(self.mockLogOutput) && OCMVerifyAll(eventLogMock), - "Unable to verify all expectations"); - [eventLogMock stopMocking]; -} - -- (void)testLogDiskDisappeared { - NSString *mount = @"/mnt/disappear"; - NSString *volume = @"Macintosh HD"; - NSString *bsdName = @"disk0s2"; - - NSDictionary *diskProperties = @{ - @"DAVolumePath" : [NSURL URLWithString:mount], - @"DAVolumeName" : volume, - @"DAMediaBSDName" : bsdName, - }; - - OCMExpect([self.mockLogOutput logEvent:[OCMArg checkWithBlock:^BOOL(SNTPBSantaMessage *sm) { - SNTPBDiskDisappeared *diskDisappeared = getEventForMessage( - sm, SNTPBSantaMessage_Message_OneOfCase_DiskDisappeared, - @"diskDisappeared", [SNTPBDiskDisappeared class]); - - if (![diskDisappeared.mount isEqualToString:mount] || - ![diskDisappeared.volume isEqualToString:volume] || - ![diskDisappeared.bsdName isEqualToString:bsdName]) { - LOGE(@"Unexpected disk disappeared data"); - return NO; - } - - return YES; - }]]); - - SNTProtobufEventLog *eventLog = [[SNTProtobufEventLog alloc] initWithLog:self.mockLogOutput]; - [eventLog logDiskDisappeared:diskProperties]; - - XCTAssertTrue(OCMVerifyAll(self.mockLogOutput), "Unable to verify all expectations"); -} - -- (void)testLogBundleHashingEvents { - NSArray *storedEvents = @[ - createTestBundleStoredEvent(getBundleForSystemApplication(@"Calculator.app"), @"abc123", - @"xyz456"), - createTestBundleStoredEvent(getBundleForSystemApplication(@"Calendar.app"), @"123abc", - @"456xyz"), - ]; - - for (SNTStoredEvent *storedEvent in storedEvents) { - OCMExpect([self.mockLogOutput - logEvent:[OCMArg checkWithBlock:^BOOL(SNTPBSantaMessage *sm) { - SNTPBBundle *bundleEvent = getEventForMessage( - sm, SNTPBSantaMessage_Message_OneOfCase_Bundle, @"bundle", [SNTPBBundle class]); - - if (![bundleEvent.sha256 isEqualToString:storedEvent.fileSHA256] || - ![bundleEvent.bundleHash isEqualToString:storedEvent.fileBundleHash] || - ![bundleEvent.bundleName isEqualToString:storedEvent.fileBundleName] || - ![bundleEvent.bundleId isEqualToString:storedEvent.fileBundleID] || - ![bundleEvent.bundlePath isEqualToString:storedEvent.fileBundlePath] || - ![bundleEvent.path isEqualToString:storedEvent.filePath]) { - LOGE(@"Unexpected bundle event data for: %@", storedEvent.filePath); - return NO; - } - - return YES; - }]]); - } - - SNTProtobufEventLog *eventLog = [[SNTProtobufEventLog alloc] initWithLog:self.mockLogOutput]; - [eventLog logBundleHashingEvents:storedEvents]; - - XCTAssertTrue(OCMVerifyAll(self.mockLogOutput), "Unable to verify all expectations"); -} - -- (void)testLogFork { - santa_message_t santaMsg = getBasicSantaMessage(ACTION_NOTIFY_FORK); - - OCMExpect([self.mockLogOutput - logEvent:[OCMArg checkWithBlock:^BOOL(SNTPBSantaMessage *sm) { - SNTPBFork *forkEvent = getEventForMessage(sm, SNTPBSantaMessage_Message_OneOfCase_Fork, - @"fork", [SNTPBFork class]); - - if (!forkEvent.hasProcessInfo || forkEvent.processInfo == nil) { - LOGE(@"Unexpected fork data"); - return NO; - } - - assertProcessInfoMatchesExpected(forkEvent.processInfo, &santaMsg); - - return YES; - }]]); - - SNTProtobufEventLog *eventLog = [[SNTProtobufEventLog alloc] initWithLog:self.mockLogOutput]; - [eventLog logFork:santaMsg]; - - XCTAssertTrue(OCMVerifyAll(self.mockLogOutput), "Unable to verify all expectations"); -} - -- (void)testLogExit { - santa_message_t santaMsg = getBasicSantaMessage(ACTION_NOTIFY_EXIT); - - OCMExpect([self.mockLogOutput - logEvent:[OCMArg checkWithBlock:^BOOL(SNTPBSantaMessage *sm) { - SNTPBExit *exitEvent = getEventForMessage(sm, SNTPBSantaMessage_Message_OneOfCase_Exit, - @"exit", [SNTPBExit class]); - - if (!exitEvent.hasProcessInfo || exitEvent.processInfo == nil) { - LOGE(@"Unexpected exit data"); - return NO; - } - - assertProcessInfoMatchesExpected(exitEvent.processInfo, &santaMsg); - - return YES; - }]]); - - SNTProtobufEventLog *eventLog = [[SNTProtobufEventLog alloc] initWithLog:self.mockLogOutput]; - [eventLog logExit:santaMsg]; - - XCTAssertTrue(OCMVerifyAll(self.mockLogOutput), "Unable to verify all expectations"); -} - -- (void)testLogAllowlist { - SNTAllowlistInfo *allowlistInfo = [[SNTAllowlistInfo alloc] initWithPid:123 - pidversion:456 - targetPath:@"/sbin/launchd" - sha256:@"abc123"]; - - OCMExpect([self.mockLogOutput - logEvent:[OCMArg checkWithBlock:^BOOL(SNTPBSantaMessage *sm) { - SNTPBAllowlist *allowlistEvent = getEventForMessage( - sm, SNTPBSantaMessage_Message_OneOfCase_Allowlist, @"allowlist", [SNTPBAllowlist class]); - - if (allowlistEvent.pid != allowlistInfo.pid || - allowlistEvent.pidversion != allowlistInfo.pidversion || - ![allowlistEvent.path isEqualToString:allowlistInfo.targetPath] || - ![allowlistEvent.sha256 isEqualToString:allowlistInfo.sha256]) { - LOGE(@"Unexpected allowlist data"); - return NO; - } - - return YES; - }]]); - - SNTProtobufEventLog *eventLog = [[SNTProtobufEventLog alloc] initWithLog:self.mockLogOutput]; - [eventLog logAllowlist:allowlistInfo]; - - XCTAssertTrue(OCMVerifyAll(self.mockLogOutput), "Unable to verify all expectations"); -} - -@end diff --git a/Source/santad/Logs/SNTSimpleMaildir.h b/Source/santad/Logs/SNTSimpleMaildir.h deleted file mode 100644 index 1613963ca..000000000 --- a/Source/santad/Logs/SNTSimpleMaildir.h +++ /dev/null @@ -1,39 +0,0 @@ -/// Copyright 2021 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import - -#import "Source/common/Santa.pbobjc.h" -#import "Source/santad/Logs/SNTLogOutput.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface SNTSimpleMaildir : NSObject - -- (instancetype)initWithBaseDirectory:(NSString *)baseDirectory - filenamePrefix:(NSString *)filenamePrefix - fileSizeThreshold:(size_t)fileSiszeThreshold - directorySizeThreshold:(size_t)directorySizeThreshold - maxTimeBetweenFlushes:(NSTimeInterval)maxTimeBetweenFlushes - NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -- (void)logEvent:(SNTPBSantaMessage *)message; -- (void)flush; - -@end - -NS_ASSUME_NONNULL_END - diff --git a/Source/santad/Logs/SNTSimpleMaildir.m b/Source/santad/Logs/SNTSimpleMaildir.m deleted file mode 100644 index 9c10b6e4e..000000000 --- a/Source/santad/Logs/SNTSimpleMaildir.m +++ /dev/null @@ -1,396 +0,0 @@ -/// Copyright 2021 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import "Source/santad/Logs/SNTSimpleMaildir.h" - -#include -#include -#include - -#import "Source/common/SNTLogging.h" -#import "Source/common/SNTMetricSet.h" - -static NSString *kDefaultMetricFieldName = @"result"; -static NSString *kErrorUserInfoKey = @"MetricsFieldName"; - -/** Helper for creating errors. */ -static NSError *MakeError(NSString *description) { - return [NSError errorWithDomain:@"com.google.santa" - code:1 - userInfo:@{kErrorUserInfoKey : description}]; -} - -static NSString *ErrorToMetricFieldName(NSError *error) { - if (!error) { - return kDefaultMetricFieldName; - } - - return error.userInfo[kErrorUserInfoKey] - ?: [NSString stringWithFormat:@"%@:%d", error.domain, (int)error.code]; -} - -static size_t SNTRoundUpToNextPage(size_t size) { - const size_t pageSize = 4096; - - if (size % pageSize == 0) { - return size; - } - return pageSize * ((size / pageSize) + 1); -} - -static uint64_t getUptimeSeconds() { - static dispatch_once_t onceToken; - static mach_timebase_info_data_t info; - - dispatch_once(&onceToken, ^{ - mach_timebase_info(&info); - }); - - uint64_t cur = mach_absolute_time(); - - // Convert from nanoseconds to seconds - return cur * info.numer / (1000 * 1000 * 1000 * info.denom); -} - -NS_ASSUME_NONNULL_BEGIN - -@implementation SNTSimpleMaildir { - /** The prefix to use for new log files. */ - NSString *_filenamePrefix; - - /** The base, tmp and new directory for spooling files. */ - NSString *_baseDirectory; - NSString *_tmpDirectory; - NSString *_newDirectory; - - /** - * Timer that flushes every `maxTimeBetweenFlushes` seconds. - * Used to avoid excessive latency exporting events. - */ - NSTimer *_flushTimer; - - /** The size threshold after which to start a new log file. */ - size_t _fileSizeThreshold; - - /** Threshold for the estimated spool size. */ - size_t _spoolSizeThreshold; - - /** Temporary storage for SNTPBSantaMessage in an SNTPBLogBatch. */ - SNTPBLogBatch *_outputProto; - - /** Current serialized size of all events in the _outputProto batch */ - size_t _outputProtoSerializedSize; - - /** Current size of the file system spooling directory. */ - size_t _estimatedSpoolSize; - - /** Counter for the files we've already opened. Used to generate file names. */ - int _createdFileCount; - - /** Dispatch queue to synchronize flush operations */ - dispatch_queue_t _flushQueue; - - /** Counter for successful and failed event flushing to disk. */ - SNTMetricCounter *_eventsFlushedCounter; - - /** Counter for successful and failed event queueing in memory. */ - SNTMetricCounter *_eventsQueuedCounter; - - /** Gauge for the overall spool size, calculated periodically. */ - SNTMetricInt64Gauge *_spoolSizeGauge; - - /** - * Mach absolute time in seconds of the last time the spool size - * was calculated. - */ - uint64_t _lastCalculatedSpoolSizeTime; -} - -- (instancetype)initWithBaseDirectory:(NSString *)baseDirectory - filenamePrefix:(NSString *)filenamePrefix - fileSizeThreshold:(size_t)fileSizeThreshold - directorySizeThreshold:(size_t)directorySizeThreshold - maxTimeBetweenFlushes:(NSTimeInterval)maxTimeBetweenFlushes { - self = [super init]; - if (self) { - _baseDirectory = baseDirectory; - _tmpDirectory = [baseDirectory stringByAppendingPathComponent:@"tmp"]; - _newDirectory = [baseDirectory stringByAppendingPathComponent:@"new"]; - _filenamePrefix = [filenamePrefix copy]; - _fileSizeThreshold = fileSizeThreshold; - _spoolSizeThreshold = directorySizeThreshold; - _estimatedSpoolSize = SIZE_T_MAX; // Force a recalculation of the spool directory size - _createdFileCount = 0; - _outputProto = [[SNTPBLogBatch alloc] init]; - _outputProtoSerializedSize = 0; - _lastCalculatedSpoolSizeTime = 0; - - _eventsFlushedCounter = [[SNTMetricSet sharedInstance] - counterWithName:@"/santa/events_flushed" - fieldNames:@[ kDefaultMetricFieldName ] - helpText:@"Number of events flushed, with the result of the flush operation"]; - _eventsQueuedCounter = [[SNTMetricSet sharedInstance] - counterWithName:@"/santa/events_queued" - fieldNames:@[ kDefaultMetricFieldName ] - helpText:@"Number of events queued in memory, with the result " - @"of their conversion to anyproto"]; - _spoolSizeGauge = - [[SNTMetricSet sharedInstance] int64GaugeWithName:@"/santa/spool_size" - fieldNames:@[ kDefaultMetricFieldName ] - helpText:@"Snapshot of the current pool size"]; - - [[SNTMetricSet sharedInstance] registerCallback:^(void) { - // Only calculate spool size for metrics every 5 minutes - static const int frequencySecs = 300; - - uint64_t curTime = getUptimeSeconds(); - - if (curTime - _lastCalculatedSpoolSizeTime >= frequencySecs) { - NSError *err = nil; - size_t curSize = [SNTSimpleMaildir spoolDirectorySize:_newDirectory withError:&err]; - - if (err) { - // Failed to calculate spool size. Try again next time... - return; - } - - _lastCalculatedSpoolSizeTime = curTime; - - [_spoolSizeGauge set:curSize forFieldValues:@[ kDefaultMetricFieldName ]]; - } - }]; - - _flushQueue = - dispatch_queue_create("com.google.santa.daemon.mail", - dispatch_queue_attr_make_with_qos_class( - DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL, QOS_CLASS_DEFAULT, 0)); - - typeof(self) __weak weakSelf = self; - _flushTimer = [NSTimer scheduledTimerWithTimeInterval:maxTimeBetweenFlushes - repeats:YES - block:^(NSTimer *_Nonnull timer) { - [weakSelf flush]; - }]; - } - return self; -} - -- (void)dealloc { - [_flushTimer invalidate]; - [self flush]; -} - -/** Fires the flush timer programmatically. Used for testing purposes. */ -- (void)fireFlushTimer { - [_flushTimer fire]; -} - -/** - * Flushes out buffered data. - * - * Returns the number of events that we attempted to flush, and populates the error if that flush - * failed. - */ -- (void)flushLockedWithError:(NSError **)error { - NSAssert([_outputProto.recordsArray count] < INT_MAX, @"Too many records"); - - if ([_outputProto.recordsArray count] == 0) { - return; - } - - if (![self createSpoolDirectoriesWithError:error]) { - return; - } - - if (_estimatedSpoolSize > _spoolSizeThreshold) { - _estimatedSpoolSize = [SNTSimpleMaildir spoolDirectorySize:_newDirectory withError:error]; - if (_estimatedSpoolSize > _spoolSizeThreshold) { - if (error) { - *error = MakeError(@"file_system_threshold_exceeded"); - } - return; - } - } - - NSString *filename = [NSString stringWithFormat:@"%@.%u", _filenamePrefix, _createdFileCount]; - NSString *exposedFilepath = [_newDirectory stringByAppendingPathComponent:filename]; - NSString *outputFilepath = [_tmpDirectory stringByAppendingPathComponent:filename]; - NSOutputStream *outputFile = [NSOutputStream outputStreamToFileAtPath:outputFilepath append:NO]; - [outputFile open]; - _createdFileCount++; - - if (!outputFile) { - if (error) { - *error = MakeError(@"data_loss_on_open"); - } - return; - } - - BOOL writeSuccess = NO; - @try { - [_outputProto writeToOutputStream:outputFile]; - writeSuccess = YES; - } @catch (NSException *exception) { - NSLog(@"Error while writing to %@: %@", outputFilepath, exception); - if (error) { - *error = MakeError(@"data_loss_on_write"); - } - } - - [outputFile close]; - - if (!writeSuccess) { - // Unable to successfully write all data. - [[NSFileManager defaultManager] removeItemAtPath:outputFilepath error:nil]; - return; - } - - if (![[NSFileManager defaultManager] moveItemAtPath:outputFilepath - toPath:exposedFilepath - error:nil]) { - // Delete the tmp file if unable to move - [[NSFileManager defaultManager] removeItemAtPath:outputFilepath error:nil]; - if (error) { - *error = MakeError(@"data_loss_on_move"); - } - } - - if (error && !*error) { - _estimatedSpoolSize += _outputProtoSerializedSize; - } - - return; -} - -- (void)flushAndUpdateCountersLocked { - NSError *error = nil; - [self flushLockedWithError:&error]; - [_eventsFlushedCounter incrementBy:[_outputProto.recordsArray count] - forFieldValues:@[ ErrorToMetricFieldName(error) ]]; - - // Clear output buffer. - _outputProto = [[SNTPBLogBatch alloc] init]; - _outputProtoSerializedSize = 0; -} - -- (void)flush { - dispatch_sync(_flushQueue, ^{ - [self flushAndUpdateCountersLocked]; - }); -} - -- (BOOL)createDirectory:(NSString *)dir withError:(NSError **)error { - BOOL isDir; - // Check if the path exists - if (![[NSFileManager defaultManager] fileExistsAtPath:dir isDirectory:&isDir]) { - // If path doesn't exist, attempt to create it - if (![[NSFileManager defaultManager] createDirectoryAtPath:dir - withIntermediateDirectories:YES - attributes:nil - error:nil]) { - if (error) { - *error = MakeError(@"failed_to_create_dir"); - } - return NO; - } - } else if (!isDir) { - // The path existed, but it wasn't a directory - if (error) { - *error = MakeError(@"path_exists_not_directory"); - } - return NO; - } - - // If we made it here, the directory was created or already existed - return YES; -} - -- (BOOL)createSpoolDirectoriesWithError:(NSError **)error { - return [self createDirectory:_baseDirectory withError:error] && - [self createDirectory:_tmpDirectory withError:error] && - [self createDirectory:_newDirectory withError:error]; -} - -+ (size_t)spoolDirectorySize:(NSString *)newDirectory withError:(NSError **)error { - size_t totalSize = 0; - NSFileManager *fm = [NSFileManager defaultManager]; - NSError *enumerationError = nil; - NSArray *filenames = [fm contentsOfDirectoryAtPath:newDirectory - error:&enumerationError]; - if (enumerationError) { - *error = MakeError(@"spool_dir_enumeration_error"); - return 0; - } - - for (NSString *filename in filenames) { - NSError *attributesError = nil; - NSDictionary *attributes = - [fm attributesOfItemAtPath:[newDirectory stringByAppendingPathComponent:filename] - error:&attributesError]; - if (attributesError) { - if (error) { - *error = MakeError(@"spool_dir_attribute_retrieval_error"); - } - continue; - } - - totalSize += SNTRoundUpToNextPage([attributes fileSize]); - } - return totalSize; -} - -- (void)logEvent:(SNTPBSantaMessage *)event { - dispatch_sync(_flushQueue, ^{ - // Note: The `serializedSize` method is costly. In order to calculate the - // size of the `_outputProto` accurately, we need to add both the serialized - // size of the new event plus additional overhead incurred from adding the - // new event to the current array of events. The `anyObjOverhead` is used - // to store the calculated overhead when the first event is logged and the - // overhead is then used when calculating `_outputProtoSerializedSize`. - static size_t anyObjOverhead = 0; - static dispatch_once_t onceToken; - - if (_outputProtoSerializedSize > _fileSizeThreshold) { - [self flushAndUpdateCountersLocked]; - } - - NSError *error = nil; - GPBAny *any = [GPBAny anyWithMessage:event error:nil]; - if (any) { - [_outputProto.recordsArray addObject:any]; - - dispatch_once(&onceToken, ^{ - size_t outputSerializedSize = [_outputProto serializedSize]; - size_t eventSerializedSize = [event serializedSize]; - if (outputSerializedSize > eventSerializedSize) { - anyObjOverhead = outputSerializedSize - eventSerializedSize; - } - }); - - _outputProtoSerializedSize += [event serializedSize] + anyObjOverhead; - } else { - error = MakeError(@"enqueue_error"); - } - - [_eventsQueuedCounter incrementForFieldValues:@[ ErrorToMetricFieldName(error) ]]; - }); -} - -/** Intentionally left no-op method for this class. */ -- (void)logString:(NSString *)logLine { -} - -@end - -NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/Source/santad/Logs/SNTSyslogEventLog.h b/Source/santad/Logs/SNTSyslogEventLog.h deleted file mode 100644 index 40d150e1a..000000000 --- a/Source/santad/Logs/SNTSyslogEventLog.h +++ /dev/null @@ -1,18 +0,0 @@ -/// Copyright 2018 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import "Source/santad/Logs/SNTEventLog.h" - -@interface SNTSyslogEventLog : SNTEventLog -@end diff --git a/Source/santad/Logs/SNTSyslogEventLog.m b/Source/santad/Logs/SNTSyslogEventLog.m deleted file mode 100644 index 5e5c0653a..000000000 --- a/Source/santad/Logs/SNTSyslogEventLog.m +++ /dev/null @@ -1,311 +0,0 @@ -/// Copyright 2018 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import "Source/santad/Logs/SNTSyslogEventLog.h" - -#include -#import - -#import "Source/common/SNTAllowlistInfo.h" -#import "Source/common/SNTCachedDecision.h" -#import "Source/common/SNTConfigurator.h" -#import "Source/common/SNTLogging.h" -#import "Source/common/SNTStoredEvent.h" -#import "Source/santad/EventProviders/SNTEndpointSecurityManager.h" - -@implementation SNTSyslogEventLog - -- (void)logFileModification:(santa_message_t)message { - NSString *action, *newpath; - - NSString *path = @(message.path); - - switch (message.action) { - case ACTION_NOTIFY_DELETE: { - action = @"DELETE"; - break; - } - case ACTION_NOTIFY_EXCHANGE: { - action = @"EXCHANGE"; - newpath = @(message.newpath); - break; - } - case ACTION_NOTIFY_LINK: { - action = @"LINK"; - newpath = @(message.newpath); - break; - } - case ACTION_NOTIFY_RENAME: { - action = @"RENAME"; - newpath = @(message.newpath); - break; - } - case ACTION_NOTIFY_WRITE: { - action = @"WRITE"; - break; - } - default: action = @"UNKNOWN"; break; - } - - // init the string with 2k capacity to avoid reallocs - NSMutableString *outStr = [NSMutableString stringWithCapacity:2048]; - [outStr appendFormat:@"action=%@|path=%@", action, [self sanitizeString:path]]; - if (newpath) { - [outStr appendFormat:@"|newpath=%@", [self sanitizeString:newpath]]; - } - - char ppath[PATH_MAX] = "(null)"; - if (message.es_message) { - es_message_t *m = message.es_message; - [SNTEndpointSecurityManager populateBufferFromESFile:m->process->executable - buffer:ppath - size:sizeof(ppath)]; - } else { - proc_pidpath(message.pid, ppath, sizeof(ppath)); - } - - [outStr - appendFormat: - @"|pid=%d|pidversion=%d|ppid=%d|process=%s|processpath=%s|uid=%d|user=%@|gid=%d|group=%@", - message.pid, message.pidversion, message.ppid, message.pname, ppath, message.uid, - [self nameForUID:message.uid], message.gid, [self nameForGID:message.gid]]; - - if ([[SNTConfigurator configurator] enableMachineIDDecoration]) { - [outStr appendFormat:@"|machineid=%@", self.machineID]; - } - - [self writeLog:outStr]; -} - -- (void)logExecution:(santa_message_t)message withDecision:(SNTCachedDecision *)cd { - NSString *d, *r; - BOOL logArgs = NO; - - switch (cd.decision) { - case SNTEventStateAllowBinary: - d = @"ALLOW"; - r = @"BINARY"; - logArgs = YES; - break; - case SNTEventStateAllowCompiler: - d = @"ALLOW"; - r = @"COMPILER"; - logArgs = YES; - break; - case SNTEventStateAllowTransitive: - d = @"ALLOW"; - r = @"TRANSITIVE"; - logArgs = YES; - break; - case SNTEventStateAllowPendingTransitive: - d = @"ALLOW"; - r = @"PENDING_TRANSITIVE"; - logArgs = YES; - break; - case SNTEventStateAllowCertificate: - d = @"ALLOW"; - r = @"CERT"; - logArgs = YES; - break; - case SNTEventStateAllowScope: - d = @"ALLOW"; - r = @"SCOPE"; - logArgs = YES; - break; - case SNTEventStateAllowTeamID: - d = @"ALLOW"; - r = @"TEAMID"; - logArgs = YES; - break; - case SNTEventStateAllowUnknown: - d = @"ALLOW"; - r = @"UNKNOWN"; - logArgs = YES; - break; - case SNTEventStateBlockBinary: - d = @"DENY"; - r = @"BINARY"; - break; - case SNTEventStateBlockCertificate: - d = @"DENY"; - r = @"CERT"; - break; - case SNTEventStateBlockScope: - d = @"DENY"; - r = @"SCOPE"; - break; - case SNTEventStateBlockTeamID: - d = @"DENY"; - r = @"TEAMID"; - break; - case SNTEventStateBlockUnknown: - d = @"DENY"; - r = @"UNKNOWN"; - break; - default: - d = @"ALLOW"; - r = @"NOTRUNNING"; - logArgs = YES; - break; - } - - // init the string with 4k capacity to avoid reallocs - NSMutableString *outLog = [[NSMutableString alloc] initWithCapacity:4096]; - [outLog appendFormat:@"action=EXEC|decision=%@|reason=%@", d, r]; - - if (cd.decisionExtra) { - [outLog appendFormat:@"|explain=%@", cd.decisionExtra]; - } - - [outLog appendFormat:@"|sha256=%@", cd.sha256]; - - if (cd.certSHA256.length) { - [outLog appendFormat:@"|cert_sha256=%@|cert_cn=%@", cd.certSHA256, - [self sanitizeString:cd.certCommonName]]; - } - - if (cd.teamID.length) { - [outLog appendFormat:@"|teamid=%@", cd.teamID]; - } - - if (cd.quarantineURL) { - [outLog appendFormat:@"|quarantine_url=%@", [self sanitizeString:cd.quarantineURL]]; - } - - NSString *mode; - switch ([[SNTConfigurator configurator] clientMode]) { - case SNTClientModeMonitor: mode = @"M"; break; - case SNTClientModeLockdown: mode = @"L"; break; - default: mode = @"U"; break; - } - - [outLog - appendFormat:@"|pid=%d|pidversion=%d|ppid=%d|uid=%d|user=%@|gid=%d|group=%@|mode=%@|path=%@", - message.pid, message.pidversion, message.ppid, message.uid, - [self nameForUID:message.uid], message.gid, [self nameForGID:message.gid], mode, - [self sanitizeString:@(message.path)]]; - - // Check for app translocation by GateKeeper, and log original path if the case. - NSString *originalPath = [self originalPathForTranslocation:&message]; - if (originalPath) { - [outLog appendFormat:@"|origpath=%@", [self sanitizeString:originalPath]]; - } - - if (logArgs) { - if (message.args_array) { - NSArray *args = (__bridge NSArray *)message.args_array; - [outLog appendFormat:@"|args=%@", [args componentsJoinedByString:@" "]]; - } else { - [self addArgsForPid:message.pid toString:outLog]; - } - } - - if ([[SNTConfigurator configurator] enableMachineIDDecoration]) { - [outLog appendFormat:@"|machineid=%@", self.machineID]; - } - - [self writeLog:outLog]; -} - -- (void)logDeniedExecution:(SNTCachedDecision *)cd withMessage:(santa_message_t)message { - [self logExecution:message withDecision:cd]; -} - -- (void)logAllowedExecution:(santa_message_t)message { - SNTCachedDecision *cd = [self cachedDecisionForMessage:message]; - [self logExecution:message withDecision:cd]; - - // We also reset the timestamp for transitive rules here, because it happens to be where we - // have access to both the execution notification and the sha256 associated with rule. - [self resetTimestampForCachedDecision:cd]; -} - -- (void)logDiskAppeared:(NSDictionary *)diskProperties { - NSString *dmgPath = @""; - NSString *serial = @""; - if ([diskProperties[@"DADeviceModel"] isEqual:@"Disk Image"]) { - dmgPath = [self diskImageForDevice:diskProperties[@"DADevicePath"]]; - } else { - serial = [self serialForDevice:diskProperties[@"DADevicePath"]]; - serial = [serial stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - } - - NSString *model = [NSString stringWithFormat:@"%@ %@", diskProperties[@"DADeviceVendor"] ?: @"", - diskProperties[@"DADeviceModel"] ?: @""]; - model = [model stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - - double a = [diskProperties[@"DAAppearanceTime"] doubleValue]; - NSString *appearanceDateString = - [self.dateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:a]]; - - NSString *format = @"action=DISKAPPEAR|mount=%@|volume=%@|bsdname=%@|fs=%@|" - @"model=%@|serial=%@|bus=%@|dmgpath=%@|appearance=%@"; - NSString *outLog = [NSMutableString - stringWithFormat:format, [diskProperties[@"DAVolumePath"] path] ?: @"", - diskProperties[@"DAVolumeName"] ?: @"", - diskProperties[@"DAMediaBSDName"] ?: @"", - diskProperties[@"DAVolumeKind"] ?: @"", model ?: @"", serial, - diskProperties[@"DADeviceProtocol"] ?: @"", dmgPath, appearanceDateString]; - [self writeLog:outLog]; -} - -- (void)logDiskDisappeared:(NSDictionary *)diskProperties { - NSString *format = @"action=DISKDISAPPEAR|mount=%@|volume=%@|bsdname=%@"; - NSString *outLog = [NSMutableString - stringWithFormat:format, [diskProperties[@"DAVolumePath"] path] ?: @"", - diskProperties[@"DAVolumeName"] ?: @"", diskProperties[@"DAMediaBSDName"]]; - [self writeLog:outLog]; -} - -- (void)logBundleHashingEvents:(NSArray *)events { - for (SNTStoredEvent *event in events) { - NSString *format = - @"action=BUNDLE|sha256=%@|bundlehash=%@|bundlename=%@|bundleid=%@|bundlepath=%@|path=%@"; - NSString *outLog = [NSMutableString - stringWithFormat:format, event.fileSHA256, event.fileBundleHash, event.fileBundleName, - event.fileBundleID, event.fileBundlePath, event.filePath]; - [self writeLog:outLog]; - } -} - -- (void)logFork:(santa_message_t)message { - NSString *s = [NSString - stringWithFormat:@"action=FORK|pid=%d|pidversion=%d|ppid=%d|uid=%d|gid=%d", message.pid, - message.pidversion, message.ppid, message.uid, message.gid]; - [self writeLog:s]; -} - -- (void)logExit:(santa_message_t)message { - NSString *s = [NSString - stringWithFormat:@"action=EXIT|pid=%d|pidversion=%d|ppid=%d|uid=%d|gid=%d", message.pid, - message.pidversion, message.ppid, message.uid, message.gid]; - [self writeLog:s]; -} - -- (void)logAllowList:(SNTAllowlistInfo *)allowlistInfo { - [self - writeLog:[NSString stringWithFormat:@"action=ALLOWLIST|pid=%d|pidversion=%d|path=%@|sha256=%@", - allowlistInfo.pid, allowlistInfo.pidversion, - allowlistInfo.targetPath, allowlistInfo.sha256]]; -} - -- (void)writeLog:(NSString *)log { - LOGI(@"%@", log); -} - -- (void)forceFlush { - // Nothing to do -} - -@end diff --git a/Source/santad/Metrics.h b/Source/santad/Metrics.h new file mode 100644 index 000000000..8387baa64 --- /dev/null +++ b/Source/santad/Metrics.h @@ -0,0 +1,57 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__METRICS_H +#define SANTA__SANTAD__METRICS_H + +#import +#include + +#include + +namespace santa::santad { + +// Test interface - forward declaration +class MetricsPeer; + +class Metrics : public std::enable_shared_from_this { + public: + static std::shared_ptr Create(uint64_t interval); + + Metrics(MOLXPCConnection *metrics_connection, dispatch_queue_t q, dispatch_source_t timer_source, + uint64_t interval, void (^run_on_first_start)(void)); + + ~Metrics(); + + void StartPoll(); + void StopPoll(); + void SetInterval(uint64_t interval); + + friend class santa::santad::MetricsPeer; + + private: + MOLXPCConnection *metrics_connection_; + dispatch_queue_t q_; + dispatch_source_t timer_source_; + uint64_t interval_; + // Tracks whether or not the timer_source should be running. + // This helps manage dispatch source state to ensure the source is not + // suspended, resumed, or cancelled while in an improper state. + bool running_; + void (^run_on_first_start_)(void); +}; + +} // namespace santa::santad + +#endif diff --git a/Source/santad/Metrics.mm b/Source/santad/Metrics.mm new file mode 100644 index 000000000..8678d1e60 --- /dev/null +++ b/Source/santad/Metrics.mm @@ -0,0 +1,117 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/Metrics.h" + +#include + +#import "Source/common/SNTLogging.h" +#import "Source/common/SNTMetricSet.h" +#import "Source/common/SNTXPCMetricServiceInterface.h" +#import "Source/santad/SNTApplicationCoreMetrics.h" + +namespace santa::santad { + +std::shared_ptr Metrics::Create(uint64_t interval) { + dispatch_queue_t q = dispatch_queue_create("com.google.santa.santametricsservice.q", + DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL); + + dispatch_source_t timer_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q); + + MOLXPCConnection *metrics_connection = [SNTXPCMetricServiceInterface configuredConnection]; + + std::shared_ptr metrics = + std::make_shared(metrics_connection, q, timer_source, interval, ^() { + SNTRegisterCoreMetrics(); + [metrics_connection resume]; + }); + + std::weak_ptr weak_metrics(metrics); + dispatch_source_set_event_handler(metrics->timer_source_, ^{ + std::shared_ptr shared_metrics = weak_metrics.lock(); + if (!shared_metrics) { + return; + } + + // Ensure we're marked as `running_`, otherwise bail + if (!shared_metrics->running_) { + return; + } + + [[shared_metrics->metrics_connection_ remoteObjectProxy] + exportForMonitoring:[[SNTMetricSet sharedInstance] export]]; + }); + + return metrics; +} + +Metrics::Metrics(MOLXPCConnection *metrics_connection, dispatch_queue_t q, + dispatch_source_t timer_source, uint64_t interval, + void (^run_on_first_start)(void)) + : q_(q), + timer_source_(timer_source), + interval_(interval), + running_(false), + run_on_first_start_(run_on_first_start) { + metrics_connection_ = metrics_connection; + SetInterval(interval_); +} + +Metrics::~Metrics() { + if (!running_) { + // The source must be resumed prior to being cancelled. However, do not + // set `running_` to true so that nothing will get exported. + dispatch_resume(timer_source_); + } +} + +void Metrics::SetInterval(uint64_t interval) { + dispatch_sync(q_, ^{ + LOGI(@"Setting metrics interval to %llu (exporting? %s)", interval, running_ ? "YES" : "NO"); + interval_ = interval; + dispatch_source_set_timer(timer_source_, dispatch_time(DISPATCH_TIME_NOW, 0), + interval_ * NSEC_PER_SEC, 250 * NSEC_PER_MSEC); + }); +} + +void Metrics::StartPoll() { + static dispatch_once_t once_token; + dispatch_once(&once_token, ^{ + run_on_first_start_(); + }); + + dispatch_sync(q_, ^{ + if (!running_) { + LOGI(@"Starting to export metrics every %llu seconds", interval_); + running_ = true; + dispatch_resume(timer_source_); + } else { + LOGW(@"Attempted to start metrics poll while already started"); + } + }); +} + +void Metrics::StopPoll() { + dispatch_sync(q_, ^{ + if (running_) { + LOGI(@"Stopping metrics export"); + dispatch_suspend(timer_source_); + running_ = false; + } else { + LOGW(@"Attempted to stop metrics poll while already stopped"); + } + }); +} + +} // namespace santa::santad diff --git a/Source/santad/MetricsTest.mm b/Source/santad/MetricsTest.mm new file mode 100644 index 000000000..b8177301a --- /dev/null +++ b/Source/santad/MetricsTest.mm @@ -0,0 +1,99 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import +#import +#import +#include + +#include "Source/santad/Metrics.h" + +namespace santa::santad { + +class MetricsPeer : public Metrics { + public: + // Make base class constructors visible + using Metrics::Metrics; + + bool IsRunning() { return running_; } + + uint64_t Interval() { return interval_; } +}; + +} // namespace santa::santad + +using santa::santad::MetricsPeer; + +@interface MetricsTest : XCTestCase +@property dispatch_queue_t q; +@property dispatch_semaphore_t sema; +@property dispatch_source_t timer; +@end + +@implementation MetricsTest + +- (void)setUp { + self.q = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); + XCTAssertNotNil(self.q); + self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q); + XCTAssertNotNil(self.timer); + self.sema = dispatch_semaphore_create(0); +} + +- (void)testStartStop { + auto metrics = std::make_shared(nil, self.q, self.timer, 100, ^{ + dispatch_semaphore_signal(self.sema); + }); + + XCTAssertFalse(metrics->IsRunning()); + + metrics->StartPoll(); + XCTAssertEqual(0, dispatch_semaphore_wait(self.sema, DISPATCH_TIME_NOW), + "Initialization block never called"); + + // Should be marked running after starting + XCTAssertTrue(metrics->IsRunning()); + + metrics->StartPoll(); + + // Ensure the initialization block isn't called a second time + XCTAssertNotEqual(0, dispatch_semaphore_wait(self.sema, DISPATCH_TIME_NOW), + "Initialization block called second time unexpectedly"); + + // Double-start doesn't change the running state + XCTAssertTrue(metrics->IsRunning()); + + metrics->StopPoll(); + + // After stopping, the internal state is no longer marked running + XCTAssertFalse(metrics->IsRunning()); + + metrics->StopPoll(); + + // Double-stop doesn't change the running state + XCTAssertFalse(metrics->IsRunning()); +} + +- (void)testSetInterval { + auto metrics = std::make_shared(nil, self.q, self.timer, 100, + ^{ + }); + + XCTAssertEqual(100, metrics->Interval()); + + metrics->SetInterval(200); + XCTAssertEqual(200, metrics->Interval()); +} + +@end diff --git a/Source/santad/SNTApplication.m b/Source/santad/SNTApplication.m deleted file mode 100644 index 3a4ac819c..000000000 --- a/Source/santad/SNTApplication.m +++ /dev/null @@ -1,432 +0,0 @@ -/// Copyright 2015 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import "Source/santad/SNTApplication.h" - -#import - -#import "Source/common/SNTCommonEnums.h" -#import "Source/common/SNTConfigurator.h" -#import "Source/common/SNTDeviceEvent.h" -#import "Source/common/SNTDropRootPrivs.h" -#import "Source/common/SNTLogging.h" -#import "Source/common/SNTMetricSet.h" -#import "Source/common/SNTXPCControlInterface.h" -#import "Source/common/SNTXPCMetricServiceInterface.h" -#import "Source/common/SNTXPCNotifierInterface.h" -#import "Source/common/SNTXPCSyncServiceInterface.h" -#import "Source/common/SNTXPCUnprivilegedControlInterface.h" -#import "Source/santad/DataLayer/SNTEventTable.h" -#import "Source/santad/DataLayer/SNTRuleTable.h" -#import "Source/santad/EventProviders/SNTCachingEndpointSecurityManager.h" -#import "Source/santad/EventProviders/SNTDeviceManager.h" -#import "Source/santad/EventProviders/SNTEndpointSecurityManager.h" -#import "Source/santad/EventProviders/SNTEventProvider.h" -#import "Source/santad/Logs/SNTEventLog.h" -#import "Source/santad/SNTApplicationCoreMetrics.h" -#import "Source/santad/SNTCompilerController.h" -#import "Source/santad/SNTDaemonControlController.h" -#import "Source/santad/SNTDatabaseController.h" -#import "Source/santad/SNTExecutionController.h" -#import "Source/santad/SNTNotificationQueue.h" -#import "Source/santad/SNTSyncdQueue.h" - -@interface SNTApplication () -@property id eventProvider; -@property SNTExecutionController *execController; -@property SNTCompilerController *compilerController; -@property SNTDeviceManager *deviceManager; -@property MOLXPCConnection *controlConnection; -@property SNTNotificationQueue *notQueue; -@property MOLXPCConnection *metricsConnection; -@property dispatch_source_t metricsTimer; -@property SNTSyncdQueue *syncdQueue; -@end - -@implementation SNTApplication - -- (instancetype)init { - self = [super init]; - if (self) { - SNTConfigurator *configurator = [SNTConfigurator configurator]; - - if ([configurator enableSysxCache]) { - LOGI(@"Using CachingEndpointSecurity as event provider."); - _eventProvider = [[SNTCachingEndpointSecurityManager alloc] init]; - } else { - LOGI(@"Using EndpointSecurity as event provider."); - _eventProvider = [[SNTEndpointSecurityManager alloc] init]; - } - - if (!_eventProvider) { - LOGE(@"Failed to connect to driver, exiting."); - return nil; - } - - // Initialize tables - SNTRuleTable *ruleTable = [SNTDatabaseController ruleTable]; - if (!ruleTable) { - LOGE(@"Failed to initialize rule table."); - return nil; - } - SNTEventTable *eventTable = [SNTDatabaseController eventTable]; - if (!eventTable) { - LOGE(@"Failed to initialize event table."); - return nil; - } - - self.notQueue = [[SNTNotificationQueue alloc] init]; - - SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init]; - deviceManager.blockUSBMount = [configurator blockUSBMount]; - if ([configurator remountUSBMode] != nil) { - deviceManager.remountArgs = [configurator remountUSBMode]; - } - - NSString *deviceBlockMsg = deviceManager.remountArgs != nil - ? [configurator remountUSBBlockMessage] - : [configurator bannedUSBBlockMessage]; - - deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) { - [[self.notQueue.notifierConnection remoteObjectProxy] - postUSBBlockNotification:event - withCustomMessage:deviceBlockMsg]; - }; - - _deviceManager = deviceManager; - - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - // The filter is reset when santad disconnects from the driver. - // Add the default filters. - [self.eventProvider fileModificationPrefixFilterAdd:@[ @"/.", @"/dev/" ]]; - - // TODO(bur): Add KVO handling for fileChangesPrefixFilters. - [self.eventProvider fileModificationPrefixFilterAdd:[configurator fileChangesPrefixFilters]]; - }); - - self.notQueue = [[SNTNotificationQueue alloc] init]; - self.syncdQueue = [[SNTSyncdQueue alloc] init]; - - // Listen for actionable config changes. - NSKeyValueObservingOptions bits = (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld); - [configurator addObserver:self - forKeyPath:NSStringFromSelector(@selector(clientMode)) - options:bits - context:NULL]; - [configurator addObserver:self - forKeyPath:NSStringFromSelector(@selector(syncBaseURL)) - options:bits - context:NULL]; - [configurator addObserver:self - forKeyPath:NSStringFromSelector(@selector(allowedPathRegex)) - options:bits - context:NULL]; - [configurator addObserver:self - forKeyPath:NSStringFromSelector(@selector(blockedPathRegex)) - options:bits - context:NULL]; - [configurator addObserver:self - forKeyPath:NSStringFromSelector(@selector(exportMetrics)) - options:bits - context:NULL]; - [configurator addObserver:self - forKeyPath:NSStringFromSelector(@selector(metricExportInterval)) - options:bits - context:NULL]; - [configurator addObserver:self - forKeyPath:NSStringFromSelector(@selector(blockUSBMount)) - options:bits - context:NULL]; - [configurator addObserver:self - forKeyPath:NSStringFromSelector(@selector(remountUSBMode)) - options:bits - context:NULL]; - - // Establish XPC listener for Santa and santactl connections - SNTDaemonControlController *dc = - [[SNTDaemonControlController alloc] initWithEventProvider:_eventProvider - notificationQueue:self.notQueue - syncdQueue:self.syncdQueue]; - - _controlConnection = - [[MOLXPCConnection alloc] initServerWithName:[SNTXPCControlInterface serviceID]]; - _controlConnection.privilegedInterface = [SNTXPCControlInterface controlInterface]; - _controlConnection.unprivilegedInterface = - [SNTXPCUnprivilegedControlInterface controlInterface]; - _controlConnection.exportedObject = dc; - [_controlConnection resume]; - - // Initialize the transitive whitelisting controller object. - _compilerController = [[SNTCompilerController alloc] initWithEventProvider:_eventProvider]; - - // Initialize the binary checker object - _execController = [[SNTExecutionController alloc] initWithEventProvider:_eventProvider - ruleTable:ruleTable - eventTable:eventTable - notifierQueue:self.notQueue - syncdQueue:self.syncdQueue]; - // Establish a connection with the sync service if a sync server exists. - [self establishSyncServiceConnection]; - - if (!_execController) return nil; - - if ([configurator exportMetrics]) { - [self startMetricsPoll]; - } - } - - return self; -} - -- (void)start { - LOGI(@"Connected to driver, activating."); - - [self performSelectorInBackground:@selector(beginListeningForDecisionRequests) withObject:nil]; - [self performSelectorInBackground:@selector(beginListeningForLogRequests) withObject:nil]; - [self performSelectorInBackground:@selector(beginListeningForMountRequests) withObject:nil]; -} - -- (void)beginListeningForDecisionRequests { - [self.eventProvider listenForDecisionRequests:^(santa_message_t message) { - switch (message.action) { - case ACTION_REQUEST_SHUTDOWN: { - LOGI(@"Driver requested a shutdown"); - exit(0); - } - case ACTION_REQUEST_BINARY: { - [self->_execController validateBinaryWithMessage:message]; - break; - } - case ACTION_NOTIFY_WHITELIST: { - // Determine if we should add a transitive whitelisting rule for this new file. - // Requires that writing process was a compiler and that new file is executable. - [self.compilerController createTransitiveRule:message]; - break; - } - default: { - LOGE(@"Received decision request without a valid action: %d", message.action); - exit(1); - } - } - }]; -} - -- (void)beginListeningForLogRequests { - [self.eventProvider listenForLogRequests:^(santa_message_t message) { - switch (message.action) { - case ACTION_NOTIFY_DELETE: - case ACTION_NOTIFY_EXCHANGE: - case ACTION_NOTIFY_LINK: - case ACTION_NOTIFY_RENAME: - case ACTION_NOTIFY_WRITE: { - NSRegularExpression *re = [[SNTConfigurator configurator] fileChangesRegex]; - NSString *path = @(message.path); - if (!path) break; - if ([re numberOfMatchesInString:path options:0 range:NSMakeRange(0, path.length)]) { - [[SNTEventLog logger] logFileModification:message]; - } - break; - } - case ACTION_NOTIFY_EXEC: { - [[SNTEventLog logger] logAllowedExecution:message]; - break; - } - case ACTION_NOTIFY_FORK: [[SNTEventLog logger] logFork:message]; break; - case ACTION_NOTIFY_EXIT: [[SNTEventLog logger] logExit:message]; break; - default: LOGE(@"Received log request without a valid action: %d", message.action); break; - } - }]; -} - -- (void)beginListeningForMountRequests { - [self.deviceManager listen]; -} - -// Taken from Apple's Concurrency Programming Guide. -dispatch_source_t createDispatchTimer(uint64_t interval, uint64_t leeway, dispatch_queue_t queue, - dispatch_block_t block) { - dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); - - if (timer) { - dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway); - dispatch_source_set_event_handler(timer, block); - dispatch_resume(timer); - } - - return timer; -} - -/* - * Create a SNTMetricSet instance and start reporting essential metrics immediately to the metric - * service. - */ -- (void)startMetricsPoll { - NSUInteger interval = [[SNTConfigurator configurator] metricExportInterval]; - - LOGI(@"starting to export metrics every %ld seconds", interval); - void (^exportMetricsBlock)(void) = ^{ - [[self.metricsConnection remoteObjectProxy] - exportForMonitoring:[[SNTMetricSet sharedInstance] export]]; - }; - - static dispatch_once_t registerMetrics; - - dispatch_once(®isterMetrics, ^{ - _metricsConnection = [SNTXPCMetricServiceInterface configuredConnection]; - [_metricsConnection resume]; - - LOGD(@"registering core metrics"); - SNTRegisterCoreMetrics(); - exportMetricsBlock(); - }); - - dispatch_source_t timer = createDispatchTimer(interval * NSEC_PER_SEC, 1ull * NSEC_PER_SEC, - dispatch_get_main_queue(), exportMetricsBlock); - if (!timer) { - LOGE(@"failed to created timer for exporting metrics"); - return; - } - - _metricsTimer = timer; -} - -- (void)stopMetricsPoll { - if (!_metricsTimer) { - LOGE(@"stopMetricsPoll called while _metricsTimer is nil"); - return; - } - - dispatch_source_cancel(_metricsTimer); -} - -- (void)establishSyncServiceConnection { - // The syncBaseURL check is here to stop retrying if the sync server is removed. - // See -[syncBaseURLDidChange:] for more info. - if (![[SNTConfigurator configurator] syncBaseURL]) return; - MOLXPCConnection *ss = [SNTXPCSyncServiceInterface configuredConnection]; - // This will handle retying connection establishment if there are issues with the service - // during initialization (missing binary, malformed plist, bad code signature, etc.). - // Once those issues are resolved the connection will establish. - // This will also handle re-establishment if the service crashes or is killed. - WEAKIFY(self); - ss.invalidationHandler = ^(void) { - STRONGIFY(self); - self.syncdQueue.syncConnection.invalidationHandler = nil; - [self performSelectorOnMainThread:@selector(establishSyncServiceConnection) - withObject:nil - waitUntilDone:YES]; - }; - [ss resume]; // If there are issues establishing the connection resume will block for 2 seconds. - self.syncdQueue.syncConnection = ss; -} - -- (void)observeValueForKeyPath:(NSString *)keyPath - ofObject:(id)object - change:(NSDictionary *)change - context:(void *)context { - NSString *newKey = NSKeyValueChangeNewKey; - NSString *oldKey = NSKeyValueChangeOldKey; - if ([keyPath isEqualToString:NSStringFromSelector(@selector(clientMode))]) { - SNTClientMode new = - [ change[newKey] isKindOfClass : [NSNumber class] ] ? [ change[newKey] longLongValue ] : 0; - SNTClientMode old = - [change[oldKey] isKindOfClass:[NSNumber class]] ? [change[oldKey] longLongValue] : 0; - if (new != old) [self clientModeDidChange:new]; - } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(syncBaseURL))]) { - NSURL *new = [ change[newKey] isKindOfClass : [NSURL class] ] ? change[newKey] : nil; - NSURL *old = [change[oldKey] isKindOfClass:[NSURL class]] ? change[oldKey] : nil; - if (!new && !old) return; - if (![new.absoluteString isEqualToString:old.absoluteString]) [self syncBaseURLDidChange:new]; - } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(allowedPathRegex))] || - [keyPath isEqualToString:NSStringFromSelector(@selector(blockedPathRegex))]) { - NSRegularExpression *new = - [ change[newKey] isKindOfClass : [NSRegularExpression class] ] ? change[newKey] : nil; - NSRegularExpression *old = - [change[oldKey] isKindOfClass:[NSRegularExpression class]] ? change[oldKey] : nil; - if (!new && !old) return; - if (![new.pattern isEqualToString:old.pattern]) { - LOGI(@"Changed [allow|deny]list regex, flushing cache"); - [self.eventProvider flushCacheNonRootOnly:NO]; - } - } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(exportMetrics))]) { - BOOL new = [ change[newKey] boolValue ]; - BOOL old = [change[oldKey] boolValue]; - - if (old == NO && new == YES) { - LOGI(@"metricsExport changed NO -> YES, starting to export metrics"); - [self startMetricsPoll]; - } else if (old == YES && new == NO) { - LOGI(@"metricsExport changed YES -> NO, stopping export of metrics"); - [self stopMetricsPoll]; - } - } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(metricExportInterval))]) { - // clang-format off - NSUInteger new = [ change[newKey] unsignedIntegerValue ]; - NSUInteger old = [ change[oldKey] unsignedIntegerValue ]; - // clang-format on - - LOGI(@"MetricExportInterval changed from %ld to %ld restarting export", old, new); - - [self stopMetricsPoll]; - [self startMetricsPoll]; - } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(blockUSBMount))]) { - BOOL new = [ change[newKey] boolValue ]; - BOOL old = [change[oldKey] boolValue]; - - if (new != old) { - LOGI(@"BlockUSBMount changed: %d -> %d", old, new); - self.deviceManager.blockUSBMount = new; - } - } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(remountUSBMode))]) { - NSArray *new = [ change[newKey] isKindOfClass : [NSArray class] ] - ? (NSArray *)change[newKey] - : nil; - NSArray *old = - [change[oldKey] isKindOfClass:[NSArray class]] ? (NSArray *)change[oldKey] : nil; - - if (![old isEqualToArray:new]) { - LOGI(@"RemountArgs changed: %s -> %s", [[old componentsJoinedByString:@","] UTF8String], - [[new componentsJoinedByString:@","] UTF8String]); - self.deviceManager.remountArgs = new; - } - } -} - -- (void)clientModeDidChange:(SNTClientMode)clientMode { - if (clientMode == SNTClientModeLockdown) { - LOGI(@"Changed client mode, flushing cache."); - [self.eventProvider flushCacheNonRootOnly:NO]; - } - [[self.notQueue.notifierConnection remoteObjectProxy] postClientModeNotification:clientMode]; -} - -- (void)syncBaseURLDidChange:(NSURL *)syncBaseURL { - if (syncBaseURL) { - LOGI(@"Establishing a new sync service connection with SyncBaseURL: %@", syncBaseURL); - [NSObject cancelPreviousPerformRequestsWithTarget:[SNTConfigurator configurator] - selector:@selector(clearSyncState) - object:nil]; - [[self.syncdQueue.syncConnection remoteObjectProxy] spindown]; - [self establishSyncServiceConnection]; - } else { - LOGI(@"SyncBaseURL removed, spinning down sync service"); - [[self.syncdQueue.syncConnection remoteObjectProxy] spindown]; - // Keep the syncState active for 10 min in case com.apple.ManagedClient is flapping. - [[SNTConfigurator configurator] performSelector:@selector(clearSyncState) - withObject:nil - afterDelay:600]; - } -} - -@end diff --git a/Source/santad/SNTApplicationBenchmark.m b/Source/santad/SNTApplicationBenchmark.m deleted file mode 100644 index 45853f2d6..000000000 --- a/Source/santad/SNTApplicationBenchmark.m +++ /dev/null @@ -1,152 +0,0 @@ -/// Copyright 2021 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -#import -#import -#import -#import - -#import "Source/common/SNTConfigurator.h" -#import "Source/santad/SNTApplication.h" -#import "Source/santad/SNTDatabaseController.h" - -#include "Source/santad/EventProviders/EndpointSecurityTestUtil.h" - -@interface SNTApplicationBenchmark : XCTestCase -@property id mockSNTDatabaseController; -@property id mockConfigurator; -@end - -@implementation SNTApplicationBenchmark : XCTestCase - -- (void)setUp { - [super setUp]; - fclose(stdout); - self.mockSNTDatabaseController = OCMClassMock([SNTDatabaseController class]); - self.mockConfigurator = OCMClassMock([SNTConfigurator class]); - OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator); - OCMStub([self.mockConfigurator enableSysxCache]).andReturn(false); -} - -+ (NSArray *)defaultPerformanceMetrics { - return @[ - XCTPerformanceMetric_WallClockTime, - - // Metrics visible and controllable from the XCode UI but without a symbol exposed for them: - @"com.apple.XCTPerformanceMetric_RunTime", - @"com.apple.XCTPerformanceMetric_UserTime", - @"com.apple.XCTPerformanceMetric_SystemTime", - @"com.apple.XCTPerformanceMetric_HighWaterMarkForHeapAllocations", - @"com.apple.XCTPerformanceMetric_PersistentHeapAllocations", - @"com.apple.XCTPerformanceMetric_PersistentHeapAllocationsNodes", - @"com.apple.XCTPerformanceMetric_PersistentVMAllocations", - @"com.apple.XCTPerformanceMetric_TotalHeapAllocationsKilobytes", - @"com.apple.XCTPerformanceMetric_TransientHeapAllocationsKilobytes", - @"com.apple.XCTPerformanceMetric_TransientHeapAllocationsNodes", - @"com.apple.XCTPerformanceMetric_TransientVMAllocationsKilobytes", - @"com.apple.XCTPerformanceMetric_HighWaterMarkForVMAllocations", - ]; -} - -- (void)tearDown { - [self.mockSNTDatabaseController stopMocking]; - [self.mockConfigurator stopMocking]; - [super tearDown]; -} - -- (void)executeAndMeasure:(NSString *)binaryName testPath:(NSString *)testPath { - MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity]; - [mockES reset]; - - OCMStub([self.mockSNTDatabaseController databasePath]).andReturn(testPath); - - SNTApplication *app = [[SNTApplication alloc] init]; - [app start]; - - // es events will start flowing in as soon as es_subscribe is called, regardless - // of whether we're ready or not for it. - XCTestExpectation *santaInit = - [self expectationWithDescription:@"Wait for Santa to subscribe to EndpointSecurity"]; - - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - while ([mockES.subscriptions[ES_EVENT_TYPE_AUTH_EXEC] isEqualTo:@NO]) - ; - - [santaInit fulfill]; - }); - - // Ugly hack to deflake the test and allow listenForDecisionRequests to install the correct - // decision callback. - [self waitForExpectations:@[ santaInit ] timeout:2.0]; - - // MeasureMetrics actually runs all of the individual events asynchronously at once. - dispatch_semaphore_t sem = dispatch_semaphore_create(0); - - void (^executeBinary)(void) = ^void(void) { - NSString *binaryPath = [NSString pathWithComponents:@[ testPath, binaryName ]]; - struct stat fileStat; - lstat(binaryPath.UTF8String, &fileStat); - - ESMessage *msg = [[ESMessage alloc] initWithBlock:^(ESMessage *m) { - m.binaryPath = binaryPath; - m.executable->stat = fileStat; - m.message->action_type = ES_ACTION_TYPE_AUTH; - m.message->event_type = ES_EVENT_TYPE_AUTH_EXEC; - m.message->event = (es_events_t){.exec = {.target = m.process}}; - }]; - - __block BOOL complete = NO; - [mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_EXEC - withCallback:^(ESResponse *r) { - complete = YES; - }]; - - [self startMeasuring]; - [mockES triggerHandler:msg.message]; - while (!complete) - ; - [self stopMeasuring]; - dispatch_semaphore_signal(sem); - }; - - [self measureMetrics:[SNTApplicationBenchmark defaultPerformanceMetrics] - automaticallyStartMeasuring:false - forBlock:executeBinary]; - - int sampleSize = 10; - - for (size_t i = 0; i < sampleSize; i++) { - dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_SEC)); - } -} - -// Microbenchmarking analysis of binary execution -- (void)testMeasureExecutionDeny { - NSString *testPath = @"santa/Source/santad/testdata/binaryrules"; - NSString *fullTestPath = [NSString pathWithComponents:@[ - [[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testPath - ]]; - - [self executeAndMeasure:@"badbinary" testPath:fullTestPath]; -} - -- (void)testMeasureExecutionAllow { - NSString *testPath = @"santa/Source/santad/testdata/binaryrules"; - NSString *fullTestPath = [NSString pathWithComponents:@[ - [[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testPath - ]]; - - [self executeAndMeasure:@"goodbinary" testPath:fullTestPath]; -} - -@end diff --git a/Source/santad/SNTApplicationCoreMetrics.h b/Source/santad/SNTApplicationCoreMetrics.h index 8ec07f469..644af1bab 100644 --- a/Source/santad/SNTApplicationCoreMetrics.h +++ b/Source/santad/SNTApplicationCoreMetrics.h @@ -1,4 +1,4 @@ -/// Copyright 2021 Google Inc. All rights reserved. +/// Copyright 2021-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -12,6 +12,10 @@ /// See the License for the specific language governing permissions and /// limitations under the License. -#import "Source/common/SNTMetricSet.h" +#include + +__BEGIN_DECLS void SNTRegisterCoreMetrics(); + +__END_DECLS diff --git a/Source/santad/SNTCompilerController.h b/Source/santad/SNTCompilerController.h index c021763f7..19f2eb768 100644 --- a/Source/santad/SNTCompilerController.h +++ b/Source/santad/SNTCompilerController.h @@ -1,4 +1,4 @@ -/// Copyright 2017 Google Inc. All rights reserved. +/// Copyright 2017-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -13,19 +13,21 @@ /// limitations under the License. #import +#include -#import "Source/common/SNTCommon.h" -#import "Source/santad/EventProviders/SNTEventProvider.h" +#include -@class SNTEventLog; +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" +#include "Source/santad/Logs/EndpointSecurity/Logger.h" @interface SNTCompilerController : NSObject -// Designated initializer takes a SNTEventLog instance so that we can -// call saveDecisionDetails: to create a fake cached decision for transitive -// rule creation requests that are still pending. -- (instancetype)initWithEventProvider:(id)eventProvider; -// Whenever an executable file is closed or renamed whitelist the resulting file. -// We assume that we have already determined that the writing process was a compiler. -- (void)createTransitiveRule:(santa_message_t)message; +// This function will determine if the instigating process was a compiler and, +// for appropriate events, will create appropriate transitive rules. +- (BOOL)handleEvent:(const santa::santad::event_providers::endpoint_security::Message &)msg + withLogger:(std::shared_ptr)logger; + +// Set whether or not the given audit token should be tracked as a compiler +- (void)setProcess:(const audit_token_t &)tok isCompiler:(bool)isCompiler; + @end diff --git a/Source/santad/SNTCompilerController.m b/Source/santad/SNTCompilerController.m deleted file mode 100644 index 4a7b1e5b2..000000000 --- a/Source/santad/SNTCompilerController.m +++ /dev/null @@ -1,99 +0,0 @@ -/// Copyright 2017 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import "Source/santad/SNTCompilerController.h" - -#import "Source/common/SNTAllowlistInfo.h" -#import "Source/common/SNTCachedDecision.h" -#import "Source/common/SNTCommon.h" -#import "Source/common/SNTCommonEnums.h" -#import "Source/common/SNTFileInfo.h" -#import "Source/common/SNTLogging.h" -#import "Source/common/SNTRule.h" -#import "Source/santad/DataLayer/SNTRuleTable.h" -#import "Source/santad/EventProviders/SNTEventProvider.h" -#import "Source/santad/Logs/SNTEventLog.h" -#import "Source/santad/SNTDatabaseController.h" - -@interface SNTCompilerController () -@property id eventProvider; -@end - -@implementation SNTCompilerController - -- (instancetype)initWithEventProvider:(id)eventProvider { - self = [super init]; - if (self) { - _eventProvider = eventProvider; - } - return self; -} - -// Adds a fake cached decision to SNTEventLog for pending files. If the file -// is executed before we can create a transitive rule for it, then we can at -// least log the pending decision info. -- (void)saveFakeDecision:(santa_message_t)message { - SNTCachedDecision *cd = [[SNTCachedDecision alloc] init]; - cd.decision = SNTEventStateAllowPendingTransitive; - cd.vnodeId = message.vnode_id; - cd.sha256 = @"pending"; - [[SNTEventLog logger] cacheDecision:cd]; -} - -- (void)removeFakeDecision:(santa_message_t)message { - [[SNTEventLog logger] forgetCachedDecisionForVnodeId:message.vnode_id]; -} - -// Assume that this method is called only when we already know that the writing process is a -// compiler. It checks if the closed file is executable, and if so, transitively allowlists it. -// The passed in message contains the pid of the writing process and path of closed file. -- (void)createTransitiveRule:(santa_message_t)message { - [self saveFakeDecision:message]; - - NSString *target = @(message.path); - - // Check if this file is an executable. - SNTFileInfo *fi = [[SNTFileInfo alloc] initWithPath:target]; - if (fi.isExecutable) { - // Check if there is an existing (non-transitive) rule for this file. We leave existing rules - // alone, so that a allowlist or blocklist rule can't be overwritten by a transitive one. - SNTRuleTable *ruleTable = [SNTDatabaseController ruleTable]; - SNTRule *prevRule = [ruleTable ruleForBinarySHA256:fi.SHA256 certificateSHA256:nil teamID:nil]; - if (!prevRule || prevRule.state == SNTRuleStateAllowTransitive) { - // Construct a new transitive allowlist rule for the executable. - SNTRule *rule = [[SNTRule alloc] initWithIdentifier:fi.SHA256 - state:SNTRuleStateAllowTransitive - type:SNTRuleTypeBinary - customMsg:@""]; - - // Add the new rule to the rules database. - NSError *err; - if (![ruleTable addRules:@[ rule ] cleanSlate:NO error:&err]) { - LOGE(@"unable to add new transitive rule to database: %@", err.localizedDescription); - } else { - [[SNTEventLog logger] logAllowlist:[[SNTAllowlistInfo alloc] initWithPid:message.pid - pidversion:message.pidversion - targetPath:target - sha256:fi.SHA256]]; - } - } - } - - // Remove the temporary allow rule in the kernel decision cache. - [self.eventProvider removeCacheEntryForVnodeID:message.vnode_id]; - // Remove the "pending" decision info from SNTEventLog. - [self removeFakeDecision:message]; -} - -@end diff --git a/Source/santad/SNTCompilerController.mm b/Source/santad/SNTCompilerController.mm new file mode 100644 index 000000000..3055d007f --- /dev/null +++ b/Source/santad/SNTCompilerController.mm @@ -0,0 +1,169 @@ +/// Copyright 2017-2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import "Source/santad/SNTCompilerController.h" + +#include +#include +#include +#include + +#include + +#import "Source/common/SNTCachedDecision.h" +#import "Source/common/SNTCommon.h" +#import "Source/common/SNTCommonEnums.h" +#import "Source/common/SNTFileInfo.h" +#import "Source/common/SNTLogging.h" +#import "Source/common/SNTRule.h" +#import "Source/santad/DataLayer/SNTRuleTable.h" +#import "Source/santad/SNTDatabaseController.h" +#import "Source/santad/SNTDecisionCache.h" + +using santa::santad::event_providers::endpoint_security::Message; +using santa::santad::logs::endpoint_security::Logger; + +static const pid_t PID_MAX = 99999; +static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/"; + +@interface SNTCompilerController () { + std::atomic _compilerPIDs[PID_MAX]; +} +@end + +@implementation SNTCompilerController + +- (BOOL)isCompiler:(const audit_token_t &)tok { + pid_t pid = audit_token_to_pid(tok); + return pid >= 0 && pid < PID_MAX && self->_compilerPIDs[pid].load(); +} + +- (void)setProcess:(const audit_token_t &)tok isCompiler:(bool)isCompiler { + pid_t pid = audit_token_to_pid(tok); + if (pid < 1) { + LOGE(@"Unable to watch compiler pid=%d", pid); + } else if (pid >= PID_MAX) { + LOGE(@"Unable to watch compiler pid=%d >= PID_MAX(%d)", pid, PID_MAX); + } else { + self->_compilerPIDs[pid].store(isCompiler); + if (isCompiler) { + LOGD(@"Watching compiler pid=%d", pid); + } + } +} + +// Adds a fake cached decision to SNTDecisionCache for pending files. If the file +// is executed before we can create a transitive rule for it, then we can at +// least log the pending decision info. +- (void)saveFakeDecision:(const es_file_t *)esFile { + SNTCachedDecision *cd = [[SNTCachedDecision alloc] initWithEndpointSecurityFile:esFile]; + cd.decision = SNTEventStateAllowPendingTransitive; + cd.sha256 = @"pending"; + [[SNTDecisionCache sharedCache] cacheDecision:cd]; +} + +- (void)removeFakeDecision:(const es_file_t *)esFile { + [[SNTDecisionCache sharedCache] forgetCachedDecisionForFile:esFile->stat]; +} + +- (BOOL)handleEvent:(const Message &)esMsg withLogger:(std::shared_ptr)logger { + const es_file_t *targetFile = NULL; + + switch (esMsg->event_type) { + case ES_EVENT_TYPE_NOTIFY_CLOSE: + if (![self isCompiler:esMsg->process->audit_token]) { + return NO; + } + + if (strncmp(kIgnoredCompilerProcessPathPrefix.data(), esMsg->event.close.target->path.data, + kIgnoredCompilerProcessPathPrefix.length()) == 0) { + return NO; + } + + targetFile = esMsg->event.close.target; + + break; + case ES_EVENT_TYPE_NOTIFY_RENAME: + if (![self isCompiler:esMsg->process->audit_token]) { + return NO; + } + + // Note: For RENAME events, we process the `source`. This is the one + // that we sould be creating transitive rules for, not the destination. + if (strncmp(kIgnoredCompilerProcessPathPrefix.data(), esMsg->event.rename.source->path.data, + kIgnoredCompilerProcessPathPrefix.length()) == 0) { + return NO; + } + + targetFile = esMsg->event.rename.source; + + break; + case ES_EVENT_TYPE_NOTIFY_EXIT: + [self setProcess:esMsg->process->audit_token isCompiler:false]; + return YES; + default: return NO; + } + + // If we get here, we need to update transitve rules + if (targetFile) { + [self createTransitiveRule:esMsg target:targetFile logger:logger]; + return YES; + } else { + return NO; + } +} + +// Assume that this method is called only when we already know that the writing process is a +// compiler. It checks if the closed file is executable, and if so, transitively allowlists it. +// The passed in message contains the pid of the writing process and path of closed file. +- (void)createTransitiveRule:(const Message &)esMsg + target:(const es_file_t *)targetFile + logger:(std::shared_ptr)logger { + NSError *error = nil; + SNTFileInfo *fi = [[SNTFileInfo alloc] initWithEndpointSecurityFile:targetFile error:&error]; + if (error) { + LOGD(@"Unable to create SNTFileInfo while attempting to create transitive rule. Path: %@", + @(targetFile->path.data)); + return; + } + + [self saveFakeDecision:targetFile]; + + // Check if this file is an executable. + if (fi.isExecutable) { + // Check if there is an existing (non-transitive) rule for this file. We leave existing rules + // alone, so that a allowlist or blocklist rule can't be overwritten by a transitive one. + SNTRuleTable *ruleTable = [SNTDatabaseController ruleTable]; + SNTRule *prevRule = [ruleTable ruleForBinarySHA256:fi.SHA256 certificateSHA256:nil teamID:nil]; + if (!prevRule || prevRule.state == SNTRuleStateAllowTransitive) { + // Construct a new transitive allowlist rule for the executable. + SNTRule *rule = [[SNTRule alloc] initWithIdentifier:fi.SHA256 + state:SNTRuleStateAllowTransitive + type:SNTRuleTypeBinary + customMsg:@""]; + + // Add the new rule to the rules database. + NSError *err; + if (![ruleTable addRules:@[ rule ] cleanSlate:NO error:&err]) { + LOGE(@"unable to add new transitive rule to database: %@", err.localizedDescription); + } else { + logger->LogAllowlist(esMsg, [fi.SHA256 UTF8String]); + } + } + } + + [self removeFakeDecision:targetFile]; +} + +@end diff --git a/Source/santad/SNTCompilerControllerTest.mm b/Source/santad/SNTCompilerControllerTest.mm new file mode 100644 index 000000000..215954b16 --- /dev/null +++ b/Source/santad/SNTCompilerControllerTest.mm @@ -0,0 +1,254 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include +#import +#import +#include +#include +#include +#include +#include "Source/santad/SNTCompilerController.h" + +#include + +#import "Source/common/SNTCachedDecision.h" +#import "Source/common/SNTFileInfo.h" +#include "Source/common/TestUtils.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" +#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h" +#include "Source/santad/Logs/EndpointSecurity/Logger.h" +#import "Source/santad/SNTCompilerController.h" +#import "Source/santad/SNTDecisionCache.h" + +using santa::santad::event_providers::endpoint_security::Message; +using santa::santad::logs::endpoint_security::Logger; + +static const pid_t PID_MAX = 99999; + +@interface SNTCompilerController (Testing) +- (BOOL)isCompiler:(const audit_token_t &)tok; +- (void)saveFakeDecision:(const es_file_t *)esFile; +- (void)removeFakeDecision:(const es_file_t *)esFile; +- (void)createTransitiveRule:(const Message &)esMsg + target:(const es_file_t *)targetFile + logger:(std::shared_ptr)logger; +@end + +@interface SNTCompilerControllerTest : XCTestCase +@property id mockDecisionCache; +@property audit_token_t tok1; +@property audit_token_t tok2; +@property audit_token_t tokNegativePid; +@property audit_token_t tokLargePid; +@end + +@implementation SNTCompilerControllerTest + +- (void)setUp { + self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]); + OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache); + + self.tok1 = MakeAuditToken(12, 11); + self.tok2 = MakeAuditToken(34, 22); + self.tokNegativePid = MakeAuditToken(-1, 33); + self.tokLargePid = MakeAuditToken(PID_MAX + 1, 44); +} + +- (void)tearDown { + [self.mockDecisionCache stopMocking]; +} + +- (void)testIsCompiler { + SNTCompilerController *cc = [[SNTCompilerController alloc] init]; + + // Ensure invalid PIDs are handled + XCTAssertFalse([cc isCompiler:self.tokNegativePid]); + XCTAssertFalse([cc isCompiler:self.tokLargePid]); + + // Items in the compiler control cache are initially false + XCTAssertFalse([cc isCompiler:self.tok1]); + + // Start tracking a process as a compiler + [cc setProcess:self.tok1 isCompiler:true]; + XCTAssertTrue([cc isCompiler:self.tok1]); + + // Stop tracking a process as a compiler + [cc setProcess:self.tok1 isCompiler:false]; + XCTAssertFalse([cc isCompiler:self.tok1]); +} + +- (void)testSetProcessIsCompiler { + SNTCompilerController *cc = [[SNTCompilerController alloc] init]; + + // Ensure invalid PIDs are handled + XCTAssertNoThrow([cc setProcess:self.tokNegativePid isCompiler:true]); + XCTAssertNoThrow([cc setProcess:self.tokLargePid isCompiler:true]); + + // Ensure test tokens are initially false + XCTAssertFalse([cc isCompiler:self.tok1]); + XCTAssertFalse([cc isCompiler:self.tok2]); + + // Start tracking one of the toks + [cc setProcess:self.tok1 isCompiler:true]; + XCTAssertTrue([cc isCompiler:self.tok1]); + XCTAssertFalse([cc isCompiler:self.tok2]); + + // Start tracking both toks + [cc setProcess:self.tok2 isCompiler:true]; + XCTAssertTrue([cc isCompiler:self.tok1]); + XCTAssertTrue([cc isCompiler:self.tok2]); + + // Stop tracking one of the toks + [cc setProcess:self.tok1 isCompiler:false]; + XCTAssertFalse([cc isCompiler:self.tok1]); + XCTAssertTrue([cc isCompiler:self.tok2]); +} + +- (void)testSaveFakeDecision { + es_file_t file = MakeESFile("foo", { + .st_dev = 12, + .st_ino = 34, + }); + + OCMExpect([self.mockDecisionCache + cacheDecision:[OCMArg checkWithBlock:^BOOL(SNTCachedDecision *cd) { + return cd.vnodeId.fsid == file.stat.st_dev && cd.vnodeId.fileid == file.stat.st_ino && + cd.decision == SNTEventStateAllowPendingTransitive && + [cd.sha256 isEqualToString:@"pending"]; + }]]); + + SNTCompilerController *cc = [[SNTCompilerController alloc] init]; + [cc saveFakeDecision:&file]; + + XCTAssertTrue(OCMVerifyAll(self.mockDecisionCache), "Unable to verify all expectations"); +} + +- (void)testRemoveFakeDecision { + es_file_t file = MakeESFile("foo", { + .st_dev = 12, + .st_ino = 34, + }); + + OCMExpect([self.mockDecisionCache forgetCachedDecisionForFile:file.stat]); + + SNTCompilerController *cc = [[SNTCompilerController alloc] init]; + [cc removeFakeDecision:&file]; + + XCTAssertTrue(OCMVerifyAll(self.mockDecisionCache), "Unable to verify all expectations"); +} + +- (void)testHandleEventWithLogger { + es_file_t file = MakeESFile("foo"); + es_file_t ignoredFile = MakeESFile("/dev/bar"); + es_file_t normalFile = MakeESFile("bar"); + audit_token_t compilerTok = MakeAuditToken(12, 34); + audit_token_t notCompilerTok = MakeAuditToken(56, 78); + es_process_t compilerProc = MakeESProcess(&file, compilerTok, {}); + es_process_t notCompilerProc = MakeESProcess(&file, notCompilerTok, {}); + es_message_t esMsg; + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + SNTCompilerController *cc = [[SNTCompilerController alloc] init]; + + // Mark a process as a compiler for use with these tests. + [cc setProcess:compilerTok isCompiler:true]; + + // Ensure unhandled event types return appropriately + { + esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_FORK, ¬CompilerProc); + Message msg(mockESApi, &esMsg); + XCTAssertFalse([cc handleEvent:msg withLogger:nullptr]); + } + + // Ensure non-compiler process events return false + { + esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, ¬CompilerProc); + Message msg(mockESApi, &esMsg); + XCTAssertFalse([cc handleEvent:msg withLogger:nullptr]); + } + { + esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, ¬CompilerProc); + Message msg(mockESApi, &esMsg); + XCTAssertFalse([cc handleEvent:msg withLogger:nullptr]); + } + + // Ensure compiler process events are only handled with non-ignored paths + { + esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &compilerProc); + esMsg.event.close.target = &ignoredFile; + Message msg(mockESApi, &esMsg); + XCTAssertFalse([cc handleEvent:msg withLogger:nullptr]); + } + { + esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &compilerProc); + esMsg.event.rename.source = &ignoredFile; + Message msg(mockESApi, &esMsg); + XCTAssertFalse([cc handleEvent:msg withLogger:nullptr]); + } + + // Ensure EXIT events stop tracking the process as a compiler + { + esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &compilerProc); + Message msg(mockESApi, &esMsg); + + id mockCompilerController = OCMPartialMock(cc); + OCMExpect([mockCompilerController setProcess:compilerProc.audit_token isCompiler:false]); + + XCTAssertTrue([cc handleEvent:msg withLogger:nullptr]); + + XCTAssertTrue(OCMVerifyAll(mockCompilerController), "Unable to verify all expectations"); + [mockCompilerController stopMocking]; + } + + // Ensure transitive rules are created when the given event is handled + { + esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &compilerProc); + esMsg.event.close.target = &normalFile; + Message msg(mockESApi, &esMsg); + + id mockCompilerController = OCMPartialMock(cc); + + OCMExpect([mockCompilerController createTransitiveRule:msg + target:esMsg.event.close.target + logger:nullptr]) + .ignoringNonObjectArgs(); + + XCTAssertTrue([cc handleEvent:msg withLogger:nullptr]); + + XCTAssertTrue(OCMVerifyAll(mockCompilerController), "Unable to verify all expectations"); + [mockCompilerController stopMocking]; + } + { + esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &compilerProc); + esMsg.event.rename.source = &normalFile; + Message msg(mockESApi, &esMsg); + + id mockCompilerController = OCMPartialMock(cc); + + OCMExpect([mockCompilerController createTransitiveRule:msg + target:esMsg.event.close.target + logger:nullptr]) + .ignoringNonObjectArgs(); + + XCTAssertTrue([cc handleEvent:msg withLogger:nullptr]); + + XCTAssertTrue(OCMVerifyAll(mockCompilerController), "Unable to verify all expectations"); + [mockCompilerController stopMocking]; + } +} + +@end diff --git a/Source/santad/SNTDaemonControlController.h b/Source/santad/SNTDaemonControlController.h index 97f4e9edf..3e05a67dc 100644 --- a/Source/santad/SNTDaemonControlController.h +++ b/Source/santad/SNTDaemonControlController.h @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,10 +14,12 @@ #import +#include + #import "Source/common/SNTXPCControlInterface.h" -#import "Source/santad/EventProviders/SNTEventProvider.h" +#include "Source/santad/EventProviders/AuthResultCache.h" +#import "Source/santad/Logs/EndpointSecurity/Logger.h" -@class SNTEventLog; @class SNTNotificationQueue; @class SNTSyncdQueue; @@ -26,7 +28,10 @@ /// @interface SNTDaemonControlController : NSObject -- (instancetype)initWithEventProvider:(id)driverManager - notificationQueue:(SNTNotificationQueue *)notQueue - syncdQueue:(SNTSyncdQueue *)syncdQueue; +- (instancetype) + initWithAuthResultCache: + (std::shared_ptr)authResultCache + notificationQueue:(SNTNotificationQueue *)notQueue + syncdQueue:(SNTSyncdQueue *)syncdQueue + logger:(std::shared_ptr)logger; @end diff --git a/Source/santad/SNTDaemonControlController.m b/Source/santad/SNTDaemonControlController.mm similarity index 91% rename from Source/santad/SNTDaemonControlController.m rename to Source/santad/SNTDaemonControlController.mm index ae64ecde4..9aeaf6e21 100644 --- a/Source/santad/SNTDaemonControlController.m +++ b/Source/santad/SNTDaemonControlController.mm @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ /// limitations under the License. #import "Source/santad/SNTDaemonControlController.h" +#include #import @@ -29,13 +30,15 @@ #import "Source/common/SNTXPCSyncServiceInterface.h" #import "Source/santad/DataLayer/SNTEventTable.h" #import "Source/santad/DataLayer/SNTRuleTable.h" -#import "Source/santad/EventProviders/SNTEventProvider.h" -#import "Source/santad/Logs/SNTEventLog.h" #import "Source/santad/SNTDatabaseController.h" #import "Source/santad/SNTNotificationQueue.h" #import "Source/santad/SNTPolicyProcessor.h" #import "Source/santad/SNTSyncdQueue.h" +using santa::santad::event_providers::AuthResultCache; +using santa::santad::event_providers::FlushCacheMode; +using santa::santad::logs::endpoint_security::Logger; + // Globals used by the santad watchdog thread uint64_t watchdogCPUEvents = 0; uint64_t watchdogRAMEvents = 0; @@ -44,21 +47,25 @@ @interface SNTDaemonControlController () @property SNTPolicyProcessor *policyProcessor; -@property id eventProvider; @property SNTNotificationQueue *notQueue; @property SNTSyncdQueue *syncdQueue; @end -@implementation SNTDaemonControlController +@implementation SNTDaemonControlController { + std::shared_ptr _authResultCache; + std::shared_ptr _logger; +} -- (instancetype)initWithEventProvider:(id)eventProvider - notificationQueue:(SNTNotificationQueue *)notQueue - syncdQueue:(SNTSyncdQueue *)syncdQueue { +- (instancetype)initWithAuthResultCache:(std::shared_ptr)authResultCache + notificationQueue:(SNTNotificationQueue *)notQueue + syncdQueue:(SNTSyncdQueue *)syncdQueue + logger:(std::shared_ptr)logger { self = [super init]; if (self) { + _logger = logger; _policyProcessor = [[SNTPolicyProcessor alloc] initWithRuleTable:[SNTDatabaseController ruleTable]]; - _eventProvider = eventProvider; + _authResultCache = authResultCache; _notQueue = notQueue; _syncdQueue = syncdQueue; } @@ -68,20 +75,21 @@ - (instancetype)initWithEventProvider:(id)eventProvider #pragma mark Kernel ops - (void)cacheCounts:(void (^)(uint64_t, uint64_t))reply { - NSArray *counts = [self.eventProvider cacheCounts]; + NSArray *counts = self->_authResultCache->CacheCounts(); reply([counts[0] unsignedLongLongValue], [counts[1] unsignedLongLongValue]); } -- (void)cacheBucketCount:(void (^)(NSArray *))reply { - reply([self.eventProvider cacheBucketCount]); +- (void)flushAllCaches { + self->_authResultCache->FlushCache(FlushCacheMode::kAllCaches); } - (void)flushCache:(void (^)(BOOL))reply { - reply([self.eventProvider flushCacheNonRootOnly:NO]); + [self flushAllCaches]; + reply(YES); } - (void)checkCacheForVnodeID:(santa_vnode_id_t)vnodeID withReply:(void (^)(santa_action_t))reply { - reply([self.eventProvider checkCache:vnodeID]); + reply(self->_authResultCache->CheckCache(vnodeID)); } #pragma mark Database ops @@ -111,8 +119,8 @@ - (void)databaseRuleAddRules:(NSArray *)rules // The actual cache flushing happens after the new rules have been added to the database. if (flushCache) { - LOGI(@"Flushing decision cache"); - [self.eventProvider flushCacheNonRootOnly:NO]; + LOGI(@"Flushing caches"); + [self flushAllCaches]; } reply(error); @@ -309,7 +317,7 @@ - (void)syncBundleEvent:(SNTStoredEvent *)event relatedEvents:(NSArray_logger->LogBundleHashingEvents(events); WEAKIFY(self); diff --git a/Source/common/SNTAllowlistInfo.h b/Source/santad/SNTDecisionCache.h similarity index 58% rename from Source/common/SNTAllowlistInfo.h rename to Source/santad/SNTDecisionCache.h index e72116655..6707204e5 100644 --- a/Source/common/SNTAllowlistInfo.h +++ b/Source/santad/SNTDecisionCache.h @@ -1,4 +1,4 @@ -/// Copyright 2021 Google Inc. All rights reserved. +/// Copyright 2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -12,21 +12,19 @@ /// See the License for the specific language governing permissions and /// limitations under the License. +#include + #import -/// -/// Store information about new allowlist rules for later logging. -/// -@interface SNTAllowlistInfo : NSObject +#import "Source/common/SNTCachedDecision.h" + +@interface SNTDecisionCache : NSObject -@property pid_t pid; -@property int pidversion; -@property NSString *targetPath; -@property NSString *sha256; ++ (instancetype)sharedCache; -- (instancetype)initWithPid:(pid_t)pid - pidversion:(int)pidver - targetPath:(NSString*)targetPath - sha256:(NSString*)hash; +- (void)cacheDecision:(SNTCachedDecision *)cd; +- (SNTCachedDecision *)cachedDecisionForFile:(const struct stat &)statInfo; +- (void)forgetCachedDecisionForFile:(const struct stat &)statInfo; +- (void)resetTimestampForCachedDecision:(const struct stat &)statInfo; @end diff --git a/Source/santad/SNTDecisionCache.mm b/Source/santad/SNTDecisionCache.mm new file mode 100644 index 000000000..5d2d22b75 --- /dev/null +++ b/Source/santad/SNTDecisionCache.mm @@ -0,0 +1,99 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import "Source/santad/SNTDecisionCache.h" + +#include + +#import "Source/common/SNTRule.h" +#import "Source/santad/DataLayer/SNTRuleTable.h" +#import "Source/santad/SNTDatabaseController.h" + +@interface SNTDecisionCache () +@property NSMutableDictionary *detailStore; +@property dispatch_queue_t detailStoreQueue; +// Cache for sha256 -> date of last timestamp reset. +@property NSCache *timestampResetMap; +@end + +@implementation SNTDecisionCache + ++ (instancetype)sharedCache { + static SNTDecisionCache *cache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cache = [[SNTDecisionCache alloc] init]; + }); + return cache; +} + +- (instancetype)init { + self = [super init]; + if (self) { + // TODO(mlw): We should protect this structure with a read/write lock + // instead of a serial dispatch queue since it's expected that most most + // accesses will be lookups, not caching new items. + _detailStore = [NSMutableDictionary dictionaryWithCapacity:1000]; + _detailStoreQueue = dispatch_queue_create( + "com.google.santa.daemon.detail_store", + dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL, + QOS_CLASS_USER_INTERACTIVE, 0)); + + _timestampResetMap = [[NSCache alloc] init]; + _timestampResetMap.countLimit = 100; + } + return self; +} + +- (void)cacheDecision:(SNTCachedDecision *)cd { + dispatch_sync(self.detailStoreQueue, ^{ + self.detailStore[@(cd.vnodeId.fileid)] = cd; + }); +} + +- (SNTCachedDecision *)cachedDecisionForFile:(const struct stat &)statInfo { + __block SNTCachedDecision *cd; + dispatch_sync(self.detailStoreQueue, ^{ + cd = self.detailStore[@(statInfo.st_ino)]; + }); + return cd; +} + +- (void)forgetCachedDecisionForFile:(const struct stat &)statInfo { + dispatch_sync(self.detailStoreQueue, ^{ + [self.detailStore removeObjectForKey:@(statInfo.st_ino)]; + }); +} + +// Whenever a cached decision resulting from a transitive allowlist rule is used to allow the +// execution of a binary, we update the timestamp on the transitive rule in the rules database. +// To prevent writing to the database too often, we space out consecutive writes by 3600 seconds. +- (void)resetTimestampForCachedDecision:(const struct stat &)statInfo { + SNTCachedDecision *cd = [self cachedDecisionForFile:statInfo]; + if (!cd || cd.decision != SNTEventStateAllowTransitive || !cd.sha256) { + return; + } + + NSDate *lastUpdate = [self.timestampResetMap objectForKey:cd.sha256]; + if (!lastUpdate || -[lastUpdate timeIntervalSinceNow] > 3600) { + SNTRule *rule = [[SNTRule alloc] initWithIdentifier:cd.sha256 + state:SNTRuleStateAllowTransitive + type:SNTRuleTypeBinary + customMsg:nil]; + [[SNTDatabaseController ruleTable] resetTimestampForRule:rule]; + [self.timestampResetMap setObject:[NSDate date] forKey:cd.sha256]; + } +} + +@end diff --git a/Source/santad/SNTDecisionCacheTest.mm b/Source/santad/SNTDecisionCacheTest.mm new file mode 100644 index 000000000..33b8efed6 --- /dev/null +++ b/Source/santad/SNTDecisionCacheTest.mm @@ -0,0 +1,103 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#import +#import +#import +#include +#include +#include "Source/common/SNTCachedDecision.h" + +#import "Source/common/SNTCommon.h" +#import "Source/common/SNTCommonEnums.h" +#import "Source/common/SNTRule.h" +#include "Source/common/TestUtils.h" +#import "Source/santad/DataLayer/SNTRuleTable.h" +#import "Source/santad/SNTDatabaseController.h" +#import "Source/santad/SNTDecisionCache.h" + +SNTCachedDecision *MakeCachedDecision(struct stat sb, SNTEventState decision) { + SNTCachedDecision *cd = [[SNTCachedDecision alloc] init]; + + cd.decision = decision; + cd.sha256 = @"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + cd.vnodeId = { + .fsid = 0, + .fileid = sb.st_ino, + }; + + return cd; +} + +@interface SNTDecisionCacheTest : XCTestCase +@property id mockDatabaseController; +@property id mockRuleDatabase; +@end + +@implementation SNTDecisionCacheTest + +- (void)setUp { + self.mockDatabaseController = OCMClassMock([SNTDatabaseController class]); + self.mockRuleDatabase = OCMStrictClassMock([SNTRuleTable class]); +} + +- (void)testBasicOperation { + SNTDecisionCache *dc = [SNTDecisionCache sharedCache]; + + struct stat sb = MakeStat(1234); + + // First make sure the item isn't in the cache + XCTAssertNil([dc cachedDecisionForFile:sb]); + + // Add the item to the cache + SNTCachedDecision *cd = MakeCachedDecision(sb, SNTEventStateAllowTeamID); + [dc cacheDecision:cd]; + + // Ensure the item exists in the cache + SNTCachedDecision *cachedCD = [dc cachedDecisionForFile:sb]; + XCTAssertNotNil(cachedCD); + XCTAssertEqual(cachedCD.decision, cd.decision); + XCTAssertEqual(cachedCD.vnodeId.fileid, cd.vnodeId.fileid); + + // Delete the item from the cache and ensure it no longer exists + [dc forgetCachedDecisionForFile:sb]; + XCTAssertNil([dc cachedDecisionForFile:sb]); +} + +- (void)testResetTimestampForCachedDecision { + SNTDecisionCache *dc = [SNTDecisionCache sharedCache]; + struct stat sb = MakeStat(1234); + SNTCachedDecision *cd = MakeCachedDecision(sb, SNTEventStateAllowTransitive); + + [dc cacheDecision:cd]; + + OCMStub([self.mockDatabaseController ruleTable]).andReturn(self.mockRuleDatabase); + + OCMExpect([self.mockRuleDatabase + resetTimestampForRule:[OCMArg checkWithBlock:^BOOL(SNTRule *rule) { + return rule.identifier == cd.sha256 && rule.state == SNTRuleStateAllowTransitive && + rule.type == SNTRuleTypeBinary; + }]]); + + [dc resetTimestampForCachedDecision:sb]; + + // Timestamps should not be reset so frequently. Call a second time quickly + // but do not register a second expectation so that the test will fail if + // timestamps are actually reset a second time. + [dc resetTimestampForCachedDecision:sb]; + + XCTAssertTrue(OCMVerifyAll(self.mockRuleDatabase)); +} + +@end diff --git a/Source/santad/SNTExecutionController.h b/Source/santad/SNTExecutionController.h index 68890cb10..1ba23d0d5 100644 --- a/Source/santad/SNTExecutionController.h +++ b/Source/santad/SNTExecutionController.h @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,9 +14,10 @@ #import +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" + +#import "Source/common/SNTCommon.h" #import "Source/common/SNTCommonEnums.h" -#include "Source/common/SNTCommon.h" -#include "Source/santad/EventProviders/SNTEventProvider.h" const static NSString *kBlockBinary = @"BlockBinary"; const static NSString *kAllowBinary = @"AllowBinary"; @@ -34,11 +35,8 @@ const static NSString *kUnknownEventState = @"Unknown"; const static NSString *kBlockPrinterWorkaround = @"BlockPrinterWorkaround"; const static NSString *kAllowNoFileInfo = @"AllowNoFileInfo"; const static NSString *kDenyNoFileInfo = @"DenyNoFileInfo"; -const static NSString *kAllowNullVNode = @"AllowNullVNode"; +const static NSString *kBlockLongPath = @"BlockLongPath"; -@class MOLCodesignChecker; -@class SNTDriverManager; -@class SNTEventLog; @class SNTEventTable; @class SNTNotificationQueue; @class SNTRuleTable; @@ -54,19 +52,32 @@ const static NSString *kAllowNullVNode = @"AllowNullVNode"; /// @interface SNTExecutionController : NSObject -- (instancetype)initWithEventProvider:(id)eventProvider - ruleTable:(SNTRuleTable *)ruleTable - eventTable:(SNTEventTable *)eventTable - notifierQueue:(SNTNotificationQueue *)notifierQueue - syncdQueue:(SNTSyncdQueue *)syncdQueue; +- (instancetype)initWithRuleTable:(SNTRuleTable *)ruleTable + eventTable:(SNTEventTable *)eventTable + notifierQueue:(SNTNotificationQueue *)notifierQueue + syncdQueue:(SNTSyncdQueue *)syncdQueue; /// /// Handles the logic of deciding whether to allow the binary to run or not, sends the response to -/// the kernel, logs the event to the log and if necessary stores the event in the database and -/// sends a notification to the GUI agent. +/// the given `postAction` block. Also logs the event to the log and if necessary stores the event +/// in the database and sends a notification to the GUI agent. +/// +/// @param message The message received from the EndpointSecurity event provider. +/// @param postAction The block invoked with the desired response result. +/// +- (void)validateExecEvent:(const santa::santad::event_providers::endpoint_security::Message &)esMsg + postAction:(bool (^)(santa_action_t))postAction; + +/// +/// Perform light, synchronous processing of the given event to decide whether or not the +/// event should undergo full processing. The checks done by this function MUST NOT block +/// the thread (e.g. perform no XPC) and should be fast and efficient so as to mitigate +/// potential buildup of event backlog. /// -/// @param message The message sent from the kernel. +/// @param message The message received from the EndpointSecurity event provider. +/// @return bool True if the event should be processed, otherwise false. /// -- (void)validateBinaryWithMessage:(santa_message_t)message; +- (bool)synchronousShouldProcessExecEvent: + (const santa::santad::event_providers::endpoint_security::Message &)esMsg; @end diff --git a/Source/santad/SNTExecutionController.m b/Source/santad/SNTExecutionController.mm similarity index 69% rename from Source/santad/SNTExecutionController.m rename to Source/santad/SNTExecutionController.mm index b4d63263e..9a92696f1 100644 --- a/Source/santad/SNTExecutionController.m +++ b/Source/santad/SNTExecutionController.mm @@ -1,4 +1,5 @@ -/// Copyright 2015 Google Inc. All rights reserved. + +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -14,14 +15,13 @@ #import "Source/santad/SNTExecutionController.h" +#include #include #include #include +#include #include -#include "Source/common/SNTLogging.h" -#include "Source/common/SNTMetricSet.h" - #import #import "Source/common/SNTBlockMessage.h" @@ -30,24 +30,22 @@ #import "Source/common/SNTConfigurator.h" #import "Source/common/SNTDropRootPrivs.h" #import "Source/common/SNTFileInfo.h" +#import "Source/common/SNTLogging.h" +#import "Source/common/SNTMetricSet.h" #import "Source/common/SNTRule.h" #import "Source/common/SNTStoredEvent.h" #import "Source/santad/DataLayer/SNTEventTable.h" #import "Source/santad/DataLayer/SNTRuleTable.h" -#import "Source/santad/EventProviders/SNTEventProvider.h" -#import "Source/santad/Logs/SNTEventLog.h" +#import "Source/santad/SNTDecisionCache.h" #import "Source/santad/SNTNotificationQueue.h" #import "Source/santad/SNTPolicyProcessor.h" #import "Source/santad/SNTSyncdQueue.h" -// A binary is considered large at ~30MB. Large binaries take longer to hash and consequently -// longer to post a decision back to santa-driver. When a binary is considered large santad will -// let santa-driver know it has received its request and is working on a decision. This allows -// santa-driver to relax; it does not have to worry about resending the request due to a timeout. -static size_t kLargeBinarySize = 30 * 1024 * 1024; +using santa::santad::event_providers::endpoint_security::Message; + +static const size_t kMaxAllowedPathLength = MAXPATHLEN - 1; // -1 to account for null terminator @interface SNTExecutionController () -@property id eventProvider; @property SNTEventTable *eventTable; @property SNTNotificationQueue *notifierQueue; @property SNTPolicyProcessor *policyProcessor; @@ -70,21 +68,20 @@ @implementation SNTExecutionController #pragma mark Initializers -- (instancetype)initWithEventProvider:(id)eventProvider - ruleTable:(SNTRuleTable *)ruleTable - eventTable:(SNTEventTable *)eventTable - notifierQueue:(SNTNotificationQueue *)notifierQueue - syncdQueue:(SNTSyncdQueue *)syncdQueue { +- (instancetype)initWithRuleTable:(SNTRuleTable *)ruleTable + eventTable:(SNTEventTable *)eventTable + notifierQueue:(SNTNotificationQueue *)notifierQueue + syncdQueue:(SNTSyncdQueue *)syncdQueue { self = [super init]; if (self) { - _eventProvider = eventProvider; _ruleTable = ruleTable; _eventTable = eventTable; _notifierQueue = notifierQueue; _syncdQueue = syncdQueue; _policyProcessor = [[SNTPolicyProcessor alloc] initWithRuleTable:_ruleTable]; - _eventQueue = dispatch_queue_create("com.google.santad.event_upload", DISPATCH_QUEUE_SERIAL); + _eventQueue = + dispatch_queue_create("com.google.santa.daemon.event_upload", DISPATCH_QUEUE_SERIAL); // This establishes the XPC connection between libsecurity and syspolicyd. // Not doing this causes a deadlock as establishing this link goes through xpcproxy. @@ -114,6 +111,7 @@ - (void)incrementEventCounters:(SNTEventState)eventType { case SNTEventStateAllowUnknown: eventTypeStr = kAllowUnknown; break; case SNTEventStateAllowCompiler: eventTypeStr = kAllowCompiler; break; case SNTEventStateAllowTransitive: eventTypeStr = kAllowTransitive; break; + case SNTEventStateBlockLongPath: eventTypeStr = kBlockLongPath; break; default: eventTypeStr = kUnknownEventState; break; } @@ -122,29 +120,68 @@ - (void)incrementEventCounters:(SNTEventState)eventType { #pragma mark Binary Validation -- (void)validateBinaryWithMessage:(santa_message_t)message { - // Get info about the file. If we can't get this info, allow execution and log an error. - if (unlikely(message.path == NULL)) { - LOGE(@"Path for vnode_id is NULL: %llu/%llu", message.vnode_id.fsid, message.vnode_id.fileid); - [self.eventProvider postAction:ACTION_RESPOND_ALLOW forMessage:message]; - [self.events incrementForFieldValues:@[ (NSString *)kAllowNullVNode ]]; - return; +- (bool)synchronousShouldProcessExecEvent:(const Message &)esMsg { + if (unlikely(esMsg->event_type != ES_EVENT_TYPE_AUTH_EXEC)) { + // Programming error. Bail. + LOGE(@"Attempt to validate non-EXEC event. Event type: %d", esMsg->event_type); + [NSException + raise:@"Invalid event type" + format:@"synchronousShouldProcessExecEvent: Unexpected event type: %d", esMsg->event_type]; } + const es_process_t *targetProc = esMsg->event.exec.target; + + if (targetProc->executable->path.length > kMaxAllowedPathLength || + targetProc->executable->path_truncated) { + // Store a SNTCachedDecision so that this event gets properly logged + SNTCachedDecision *cd = + [[SNTCachedDecision alloc] initWithEndpointSecurityFile:targetProc->executable]; + cd.decision = SNTEventStateBlockLongPath; + cd.customMsg = [NSString stringWithFormat:@"Path exceeded max length for processing (%zu)", + targetProc->executable->path.length]; + + if (targetProc->team_id.data) { + cd.teamID = [NSString stringWithUTF8String:targetProc->team_id.data]; + } + + // TODO(mlw): We should be able to grab signing info to have more-enriched log messages in the + // future. The code to do this should probably be abstracted from the SNTPolicyProcessor. + + [[SNTDecisionCache sharedCache] cacheDecision:cd]; + + return NO; + } + + // An SNTCachedDecision will be created later on during full processing + return YES; +} + +- (void)validateExecEvent:(const Message &)esMsg postAction:(bool (^)(santa_action_t))postAction { + if (unlikely(esMsg->event_type != ES_EVENT_TYPE_AUTH_EXEC)) { + // Programming error. Bail. + LOGE(@"Attempt to validate non-EXEC event. Event type: %d", esMsg->event_type); + [NSException + raise:@"Invalid event type" + format:@"validateExecEvent:postAction: Unexpected event type: %d", esMsg->event_type]; + } + + // Get info about the file. If we can't get this info, respond appropriately and log an error. SNTConfigurator *config = [SNTConfigurator configurator]; + const es_process_t *targetProc = esMsg->event.exec.target; NSError *fileInfoError; - SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithPath:@(message.path) error:&fileInfoError]; + SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithEndpointSecurityFile:targetProc->executable + error:&fileInfoError]; if (unlikely(!binInfo)) { if (config.failClosed && config.clientMode == SNTClientModeLockdown) { - LOGE(@"Failed to read file %@: %@ and denying action", @(message.path), + LOGE(@"Failed to read file %@: %@ and denying action", @(targetProc->executable->path.data), fileInfoError.localizedDescription); - [self.eventProvider postAction:ACTION_RESPOND_DENY forMessage:message]; + postAction(ACTION_RESPOND_DENY); [self.events incrementForFieldValues:@[ (NSString *)kDenyNoFileInfo ]]; } else { - LOGE(@"Failed to read file %@: %@ but allowing action", @(message.path), + LOGE(@"Failed to read file %@: %@ but allowing action", @(targetProc->executable->path.data), fileInfoError.localizedDescription); - [self.eventProvider postAction:ACTION_RESPOND_ALLOW forMessage:message]; + postAction(ACTION_RESPOND_ALLOW); [self.events incrementForFieldValues:@[ (NSString *)kAllowNoFileInfo ]]; } return; @@ -152,21 +189,18 @@ - (void)validateBinaryWithMessage:(santa_message_t)message { // PrinterProxy workaround, see description above the method for more details. if ([self printerProxyWorkaround:binInfo]) { - [self.eventProvider postAction:ACTION_RESPOND_DENY forMessage:message]; + postAction(ACTION_RESPOND_DENY); [self.events incrementForFieldValues:@[ (NSString *)kBlockPrinterWorkaround ]]; return; } - // If the binary is large let santa-driver know we received the request and we are working on it. - if (binInfo.fileSize > kLargeBinarySize) { - LOGD(@"%@ is larger than %zu. Letting santa-driver know we are working on it.", binInfo.path, - kLargeBinarySize); - [self.eventProvider postAction:ACTION_RESPOND_ACK forMessage:message]; - // TODO(markowsky): Maybe add a metric here for how many large executables we're seeing. - } + // TODO(markowsky): Maybe add a metric here for how many large executables we're seeing. + // if (binInfo.fileSize > SomeUpperLimit) ... SNTCachedDecision *cd = [self.policyProcessor decisionForFileInfo:binInfo]; - cd.vnodeId = message.vnode_id; + + cd.vnodeId = {.fsid = (uint64_t)targetProc->executable->stat.st_dev, + .fileid = targetProc->executable->stat.st_ino}; // Formulate an initial action from the decision. santa_action_t action = @@ -175,12 +209,7 @@ - (void)validateBinaryWithMessage:(santa_message_t)message { // Save decision details for logging the execution later. For transitive rules, we also use // the shasum stored in the decision details to update the rule's timestamp whenever an // ACTION_NOTIFY_EXEC message related to the transitive rule is received. - NSString *ttyPath; - if (action == ACTION_RESPOND_ALLOW) { - [[SNTEventLog logger] cacheDecision:cd]; - } else { - ttyPath = @(message.ttypath); - } + [[SNTDecisionCache sharedCache] cacheDecision:cd]; // Upgrade the action to ACTION_RESPOND_ALLOW_COMPILER when appropriate, because we want the // kernel to track this information in its decision cache. @@ -188,8 +217,8 @@ - (void)validateBinaryWithMessage:(santa_message_t)message { action = ACTION_RESPOND_ALLOW_COMPILER; } - // Send the decision to the kernel. - [self.eventProvider postAction:action forMessage:message]; + // Respond with the decision. + postAction(action); // Increment counters; [self incrementEventCounters:cd.decision]; @@ -206,9 +235,9 @@ - (void)validateBinaryWithMessage:(santa_message_t)message { se.signingChain = cd.certChain; se.teamID = cd.teamID; - se.pid = @(message.pid); - se.ppid = @(message.ppid); - se.parentName = @(message.pname); + se.pid = @(audit_token_to_pid(targetProc->audit_token)); + se.ppid = @(audit_token_to_pid(targetProc->parent_audit_token)); + se.parentName = @(esMsg.ParentProcessName().c_str()); // Bundle data se.fileBundleID = [binInfo bundleIdentifier]; @@ -222,7 +251,7 @@ - (void)validateBinaryWithMessage:(santa_message_t)message { } // User data - struct passwd *user = getpwuid(message.uid); + struct passwd *user = getpwuid(audit_token_to_ruid(targetProc->audit_token)); if (user) se.executingUser = @(user->pw_name); NSArray *loggedInUsers, *currentSessions; [self loggedInUsers:&loggedInUsers sessions:¤tSessions]; @@ -244,8 +273,6 @@ - (void)validateBinaryWithMessage:(santa_message_t)message { // If binary was blocked, do the needful if (action != ACTION_RESPOND_ALLOW && action != ACTION_RESPOND_ALLOW_COMPILER) { - [[SNTEventLog logger] logDeniedExecution:cd withMessage:message]; - if (config.enableBundles && binInfo.bundle) { // If the binary is part of a bundle, find and hash all the related binaries in the bundle. // Let the GUI know hashing is needed. Once the hashing is complete the GUI will send a @@ -264,17 +291,21 @@ - (void)validateBinaryWithMessage:(santa_message_t)message { // Let the user know what happened, both on the terminal and in the GUI. NSAttributedString *s = [SNTBlockMessage attributedBlockMessageForEvent:se customMessage:cd.customMsg]; - NSMutableString *msg = [NSMutableString stringWithCapacity:1024]; - [msg appendFormat:@"\n\033[1mSanta\033[0m\n\n%@\n\n", s.string]; - [msg appendFormat:@"\033[1mPath: \033[0m %@\n" - @"\033[1mIdentifier:\033[0m %@\n" - @"\033[1mParent: \033[0m %@ (%@)\n\n", - se.filePath, se.fileSHA256, se.parentName, se.ppid]; - NSURL *detailURL = [SNTBlockMessage eventDetailURLForEvent:se]; - if (detailURL) { - [msg appendFormat:@"More info:\n%@\n\n", detailURL.absoluteString]; + + if (targetProc->tty && targetProc->tty->path.length > 0) { + NSMutableString *msg = [NSMutableString stringWithCapacity:1024]; + [msg appendFormat:@"\n\033[1mSanta\033[0m\n\n%@\n\n", s.string]; + [msg appendFormat:@"\033[1mPath: \033[0m %@\n" + @"\033[1mIdentifier:\033[0m %@\n" + @"\033[1mParent: \033[0m %@ (%@)\n\n", + se.filePath, se.fileSHA256, se.parentName, se.ppid]; + NSURL *detailURL = [SNTBlockMessage eventDetailURLForEvent:se]; + if (detailURL) { + [msg appendFormat:@"More info:\n%@\n\n", detailURL.absoluteString]; + } + + [self printMessage:msg toTTY:targetProc->tty->path.data]; } - if (ttyPath) [self printMessage:msg toTTY:ttyPath]; [self.notifierQueue addEvent:se customMessage:cd.customMsg]; } @@ -326,8 +357,8 @@ - (SNTFileInfo *)printerProxyFileInfo { return proxyInfo; } -- (void)printMessage:(NSString *)msg toTTY:(NSString *)path { - int fd = open(path.UTF8String, O_WRONLY | O_NOCTTY); +- (void)printMessage:(NSString *)msg toTTY:(const char *)path { + int fd = open(path, O_WRONLY | O_NOCTTY); write(fd, msg.UTF8String, msg.length); close(fd); } diff --git a/Source/santad/SNTExecutionControllerTest.m b/Source/santad/SNTExecutionControllerTest.mm similarity index 64% rename from Source/santad/SNTExecutionControllerTest.m rename to Source/santad/SNTExecutionControllerTest.mm index 81832f5f1..05a20e8c3 100644 --- a/Source/santad/SNTExecutionControllerTest.m +++ b/Source/santad/SNTExecutionControllerTest.mm @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -12,25 +12,44 @@ /// See the License for the specific language governing permissions and /// limitations under the License. +#include #import #import #import #import +#include +#include "Source/common/SNTCommon.h" +#include "Source/common/SNTCommonEnums.h" -#import "Source/santad/SNTExecutionController.h" - +#import "Source/common/SNTCachedDecision.h" +#import "Source/common/SNTCommonEnums.h" #import "Source/common/SNTConfigurator.h" #import "Source/common/SNTFileInfo.h" #import "Source/common/SNTMetricSet.h" #import "Source/common/SNTRule.h" +#include "Source/common/TestUtils.h" #import "Source/santad/DataLayer/SNTEventTable.h" #import "Source/santad/DataLayer/SNTRuleTable.h" -#import "Source/santad/EventProviders/SNTEventProvider.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" +#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h" +#import "Source/santad/SNTDecisionCache.h" +#import "Source/santad/SNTExecutionController.h" + +using santa::santad::event_providers::endpoint_security::Message; + +using PostActionBlock = bool (^)(santa_action_t); +using VerifyPostActionBlock = PostActionBlock (^)(santa_action_t); + +VerifyPostActionBlock verifyPostAction = ^PostActionBlock(santa_action_t wantAction) { + return ^bool(santa_action_t gotAction) { + XCTAssertEqual(gotAction, wantAction); + }; +}; @interface SNTExecutionControllerTest : XCTestCase +@property id mockDecisionCache; @property id mockConfigurator; @property id mockCodesignChecker; -@property id mockEventProvider; @property id mockFileInfo; @property id mockRuleDatabase; @property id mockEventDatabase; @@ -38,27 +57,19 @@ @interface SNTExecutionControllerTest : XCTestCase @property SNTExecutionController *sut; @end -@interface SNTTestEventProvider : NSObject -- (int)postAction:(santa_action_t)action forMessage:(santa_message_t)sm; -@end -@implementation SNTTestEventProvider -- (int)postAction:(santa_action_t)action forMessage:(santa_message_t)sm { - return 0; -} -@end - @implementation SNTExecutionControllerTest - (void)setUp { [super setUp]; - fclose(stdout); + self.mockDecisionCache = OCMStrictClassMock([SNTDecisionCache class]); + OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache); + OCMStub([self.mockDecisionCache cacheDecision:OCMOCK_ANY]); [[SNTMetricSet sharedInstance] reset]; self.mockCodesignChecker = OCMClassMock([MOLCodesignChecker class]); OCMStub([self.mockCodesignChecker alloc]).andReturn(self.mockCodesignChecker); - OCMStub([self.mockCodesignChecker initWithBinaryPath:OCMOCK_ANY error:[OCMArg setTo:NULL]]) .andReturn(self.mockCodesignChecker); @@ -67,11 +78,10 @@ - (void)setUp { NSURL *url = [NSURL URLWithString:@"https://localhost/test"]; OCMStub([self.mockConfigurator syncBaseURL]).andReturn(url); - self.mockEventProvider = OCMClassMock([SNTTestEventProvider class]); - self.mockFileInfo = OCMClassMock([SNTFileInfo class]); OCMStub([self.mockFileInfo alloc]).andReturn(self.mockFileInfo); - OCMStub([self.mockFileInfo initWithPath:OCMOCK_ANY error:[OCMArg setTo:nil]]) + OCMStub([self.mockFileInfo initWithEndpointSecurityFile:NULL error:[OCMArg setTo:nil]]) + .ignoringNonObjectArgs() .andReturn(self.mockFileInfo); OCMStub([self.mockFileInfo codesignCheckerWithError:[OCMArg setTo:nil]]) .andReturn(self.mockCodesignChecker); @@ -79,25 +89,16 @@ - (void)setUp { self.mockRuleDatabase = OCMClassMock([SNTRuleTable class]); self.mockEventDatabase = OCMClassMock([SNTEventTable class]); - self.sut = [[SNTExecutionController alloc] initWithEventProvider:self.mockEventProvider - ruleTable:self.mockRuleDatabase - eventTable:self.mockEventDatabase - notifierQueue:nil - syncdQueue:nil]; -} - -/// Return a pre-configured santa_message_ t for testing with. -- (santa_message_t)getMessage { - santa_message_t message = {}; - message.pid = 12; - message.ppid = 1; - message.vnode_id = [self getVnodeId]; - strncpy(message.path, "/a/file", 7); - return message; + self.sut = [[SNTExecutionController alloc] initWithRuleTable:self.mockRuleDatabase + eventTable:self.mockEventDatabase + notifierQueue:nil + syncdQueue:nil]; } -- (santa_vnode_id_t)getVnodeId { - return (santa_vnode_id_t){.fsid = 1234, .fileid = 5678}; +- (void)tearDown { + // Make sure `self.sut` is deallocated before the mocks are deallocated and + // call into `stopMocking`. + self.sut = nil; } - (void)checkMetricCounters:(const NSString *)expectedFieldValueName @@ -118,6 +119,95 @@ - (void)checkMetricCounters:(const NSString *)expectedFieldValueName } } +- (void)testSynchronousShouldProcessExecEvent { + es_file_t file = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&file); + es_file_t fileExec = MakeESFile("bar", { + .st_dev = 12, + .st_ino = 34, + }); + es_process_t procExec = MakeESProcess(&fileExec); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc); + esMsg.event.exec.target = &procExec; + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + // Undo the default mocks + self.mockDecisionCache = OCMStrictClassMock([SNTDecisionCache class]); + OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache); + + // Throw on non-AUTH EXEC events + { + esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC; + Message msg(mockESApi, &esMsg); + XCTAssertThrows([self.sut synchronousShouldProcessExecEvent:msg]); + } + + // "Normal" events should be processed + { + esMsg.event_type = ES_EVENT_TYPE_AUTH_EXEC; + Message msg(mockESApi, &esMsg); + XCTAssertTrue([self.sut synchronousShouldProcessExecEvent:msg]); + } + + // Long or truncated paths are not handled + { + size_t oldLen = esMsg.event.exec.target->executable->path.length; + esMsg.event.exec.target->executable->path.length = 24000; + es_file_t *targetExecutable = esMsg.event.exec.target->executable; + + Message msg(mockESApi, &esMsg); + + OCMExpect( + [self.mockDecisionCache cacheDecision:[OCMArg checkWithBlock:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateBlockLongPath && + cd.vnodeId.fsid == targetExecutable->stat.st_dev && + cd.vnodeId.fileid == targetExecutable->stat.st_ino; + }]]); + + XCTAssertFalse([self.sut synchronousShouldProcessExecEvent:msg]); + + esMsg.event.exec.target->executable->path.length = oldLen; + esMsg.event.exec.target->executable->path_truncated = true; + + OCMExpect( + [self.mockDecisionCache cacheDecision:[OCMArg checkWithBlock:^BOOL(SNTCachedDecision *cd) { + return cd.decision == SNTEventStateBlockLongPath && + cd.vnodeId.fsid == targetExecutable->stat.st_dev && + cd.vnodeId.fileid == targetExecutable->stat.st_ino; + }]]); + + XCTAssertFalse([self.sut synchronousShouldProcessExecEvent:msg]); + + XCTAssertTrue(OCMVerifyAll(self.mockDecisionCache)); + } + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + +- (void)validateExecEvent:(santa_action_t)wantAction { + es_file_t file = MakeESFile("foo"); + es_process_t proc = MakeESProcess(&file); + es_file_t fileExec = MakeESFile("bar", { + .st_dev = 12, + .st_ino = 34, + }); + es_process_t procExec = MakeESProcess(&fileExec); + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc); + esMsg.event.exec.target = &procExec; + + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsRetainReleaseMessage(&esMsg); + + { + Message msg(mockESApi, &esMsg); + [self.sut validateExecEvent:msg postAction:verifyPostAction(wantAction)]; + } + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); +} + - (void)testBinaryAllowRule { OCMStub([self.mockFileInfo isMachO]).andReturn(YES); OCMStub([self.mockFileInfo SHA256]).andReturn(@"a"); @@ -128,10 +218,8 @@ - (void)testBinaryAllowRule { OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil]) .andReturn(rule); - [self.sut validateBinaryWithMessage:[self getMessage]]; - - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]); - [self checkMetricCounters:@"AllowBinary" expected:@1]; + [self validateExecEvent:ACTION_RESPOND_ALLOW]; + [self checkMetricCounters:kAllowBinary expected:@1]; } - (void)testBinaryBlockRule { @@ -144,12 +232,8 @@ - (void)testBinaryBlockRule { OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil]) .andReturn(rule); - [self.sut validateBinaryWithMessage:[self getMessage]]; - - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_DENY forMessage:[self getMessage]]); - - // verify that we're incrementing the binary block - [self checkMetricCounters:@"BlockBinary" expected:@1]; + [self validateExecEvent:ACTION_RESPOND_DENY]; + [self checkMetricCounters:kBlockBinary expected:@1]; } - (void)testCertificateAllowRule { @@ -165,9 +249,7 @@ - (void)testCertificateAllowRule { OCMStub([self.mockRuleDatabase ruleForBinarySHA256:nil certificateSHA256:@"a" teamID:nil]) .andReturn(rule); - [self.sut validateBinaryWithMessage:[self getMessage]]; - - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]); + [self validateExecEvent:ACTION_RESPOND_ALLOW]; [self checkMetricCounters:kAllowCertificate expected:@1]; } @@ -186,11 +268,10 @@ - (void)testCertificateBlockRule { OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]); - [self.sut validateBinaryWithMessage:[self getMessage]]; + [self validateExecEvent:ACTION_RESPOND_DENY]; - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_DENY forMessage:[self getMessage]]); OCMVerifyAllWithDelay(self.mockEventDatabase, 1); - [self checkMetricCounters:@"BlockCertificate" expected:@1]; + [self checkMetricCounters:kBlockCertificate expected:@1]; } - (void)testBinaryAllowCompilerRule { @@ -204,10 +285,7 @@ - (void)testBinaryAllowCompilerRule { OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil]) .andReturn(rule); - [self.sut validateBinaryWithMessage:[self getMessage]]; - - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_ALLOW_COMPILER - forMessage:[self getMessage]]); + [self validateExecEvent:ACTION_RESPOND_ALLOW_COMPILER]; [self checkMetricCounters:kAllowCompiler expected:@1]; } @@ -222,9 +300,7 @@ - (void)testBinaryAllowCompilerRuleDisabled { OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil]) .andReturn(rule); - [self.sut validateBinaryWithMessage:[self getMessage]]; - - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]); + [self validateExecEvent:ACTION_RESPOND_ALLOW]; [self checkMetricCounters:kAllowBinary expected:@1]; } @@ -239,10 +315,7 @@ - (void)testBinaryAllowTransitiveRule { OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil]) .andReturn(rule); - [self.sut validateBinaryWithMessage:[self getMessage]]; - - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]); - + [self validateExecEvent:ACTION_RESPOND_ALLOW]; [self checkMetricCounters:kAllowTransitive expected:@1]; } @@ -260,9 +333,8 @@ - (void)testBinaryAllowTransitiveRuleDisabled { OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]); - [self.sut validateBinaryWithMessage:[self getMessage]]; + [self validateExecEvent:ACTION_RESPOND_DENY]; - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_DENY forMessage:[self getMessage]]); OCMVerifyAllWithDelay(self.mockEventDatabase, 1); [self checkMetricCounters:kAllowBinary expected:@0]; [self checkMetricCounters:kAllowTransitive expected:@0]; @@ -275,14 +347,13 @@ - (void)testDefaultDecision { OCMExpect([self.mockConfigurator clientMode]).andReturn(SNTClientModeMonitor); OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]); - [self.sut validateBinaryWithMessage:[self getMessage]]; - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]); + [self validateExecEvent:ACTION_RESPOND_ALLOW]; OCMExpect([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown); - [self.sut validateBinaryWithMessage:[self getMessage]]; - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_DENY forMessage:[self getMessage]]); - OCMVerifyAllWithDelay(self.mockEventDatabase, 1); + [self validateExecEvent:ACTION_RESPOND_DENY]; + + OCMVerifyAllWithDelay(self.mockEventDatabase, 1); [self checkMetricCounters:kBlockUnknown expected:@1]; [self checkMetricCounters:kAllowUnknown expected:@1]; } @@ -298,8 +369,8 @@ - (void)testUnreadableFailOpenLockdown { // Lockdown mode, no fail-closed OCMStub([self.mockConfigurator failClosed]).andReturn(NO); OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown); - [self.sut validateBinaryWithMessage:[self getMessage]]; - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]); + + [self validateExecEvent:ACTION_RESPOND_ALLOW]; [self checkMetricCounters:kAllowNoFileInfo expected:@1]; } @@ -314,8 +385,8 @@ - (void)testUnreadableFailClosedLockdown { // Lockdown mode, fail-closed OCMStub([self.mockConfigurator failClosed]).andReturn(YES); OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown); - [self.sut validateBinaryWithMessage:[self getMessage]]; - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_DENY forMessage:[self getMessage]]); + + [self validateExecEvent:ACTION_RESPOND_DENY]; [self checkMetricCounters:kDenyNoFileInfo expected:@1]; } @@ -330,22 +401,21 @@ - (void)testUnreadableFailClosedMonitor { // Monitor mode, fail-closed OCMStub([self.mockConfigurator failClosed]).andReturn(YES); OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeMonitor); - [self.sut validateBinaryWithMessage:[self getMessage]]; - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]); + + [self validateExecEvent:ACTION_RESPOND_ALLOW]; [self checkMetricCounters:kAllowNoFileInfo expected:@1]; } - (void)testMissingShasum { - [self.sut validateBinaryWithMessage:[self getMessage]]; - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]); + [self validateExecEvent:ACTION_RESPOND_ALLOW]; [self checkMetricCounters:kAllowScope expected:@1]; } - (void)testOutOfScope { OCMStub([self.mockFileInfo isMachO]).andReturn(NO); OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown); - [self.sut validateBinaryWithMessage:[self getMessage]]; - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]); + + [self validateExecEvent:ACTION_RESPOND_ALLOW]; [self checkMetricCounters:kAllowScope expected:@1]; } @@ -353,8 +423,8 @@ - (void)testPageZero { OCMStub([self.mockFileInfo isMachO]).andReturn(YES); OCMStub([self.mockFileInfo isMissingPageZero]).andReturn(YES); OCMExpect([self.mockEventDatabase addStoredEvent:OCMOCK_ANY]); - [self.sut validateBinaryWithMessage:[self getMessage]]; - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_DENY forMessage:[self getMessage]]); + + [self validateExecEvent:ACTION_RESPOND_DENY]; OCMVerifyAllWithDelay(self.mockEventDatabase, 1); [self checkMetricCounters:kBlockUnknown expected:@1]; } @@ -372,8 +442,7 @@ - (void)testAllEventUpload { OCMStub([self.mockRuleDatabase ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil]) .andReturn(rule); - [self.sut validateBinaryWithMessage:[self getMessage]]; - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]); + [self validateExecEvent:ACTION_RESPOND_ALLOW]; OCMVerifyAllWithDelay(self.mockEventDatabase, 1); } @@ -385,8 +454,7 @@ - (void)testDisableUnknownEventUpload { OCMExpect([self.mockConfigurator enableAllEventUpload]).andReturn(NO); OCMExpect([self.mockConfigurator disableUnknownEventUpload]).andReturn(YES); - [self.sut validateBinaryWithMessage:[self getMessage]]; - OCMVerify([self.mockEventProvider postAction:ACTION_RESPOND_ALLOW forMessage:[self getMessage]]); + [self validateExecEvent:ACTION_RESPOND_ALLOW]; OCMVerify(never(), [self.mockEventDatabase addStoredEvent:OCMOCK_ANY]); [self checkMetricCounters:kAllowUnknown expected:@1]; } diff --git a/Source/santad/SNTPolicyProcessor.h b/Source/santad/SNTPolicyProcessor.h index c6f635105..250572299 100644 --- a/Source/santad/SNTPolicyProcessor.h +++ b/Source/santad/SNTPolicyProcessor.h @@ -1,4 +1,4 @@ -/// Copyright 2015 Google Inc. All rights reserved. +/// Copyright 2015-2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -15,8 +15,8 @@ #import #import -#import "Source/common/SNTCommonEnums.h" #import "Source/common/SNTCommon.h" +#import "Source/common/SNTCommonEnums.h" @class MOLCodesignChecker; @class SNTCachedDecision; diff --git a/Source/santad/Santad.h b/Source/santad/Santad.h new file mode 100644 index 000000000..8e83cf910 --- /dev/null +++ b/Source/santad/Santad.h @@ -0,0 +1,47 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD_SANTAD_H +#define SANTA__SANTAD_SANTAD_H + +#import + +#include "Source/common/SNTPrefixTree.h" +#include "Source/santad/EventProviders/AuthResultCache.h" +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" +#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h" +#include "Source/santad/Logs/EndpointSecurity/Logger.h" +#include "Source/santad/Metrics.h" +#import "Source/santad/SNTCompilerController.h" +#import "Source/santad/SNTExecutionController.h" +#import "Source/santad/SNTNotificationQueue.h" +#import "Source/santad/SNTSyncdQueue.h" + +void SantadMain( + std::shared_ptr< + santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> + esapi, + std::shared_ptr logger, + std::shared_ptr metrics, + std::shared_ptr + enricher, + std::shared_ptr + auth_result_cache, + MOLXPCConnection* control_connection, + SNTCompilerController* compiler_controller, + SNTNotificationQueue* notifier_queue, SNTSyncdQueue* syncd_queue, + SNTExecutionController* exec_controller, + std::shared_ptr prefix_tree); + +#endif diff --git a/Source/santad/Santad.mm b/Source/santad/Santad.mm new file mode 100644 index 000000000..4453d0929 --- /dev/null +++ b/Source/santad/Santad.mm @@ -0,0 +1,284 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/Santad.h" + +#include + +#import "Source/common/SNTCommonEnums.h" +#import "Source/common/SNTConfigurator.h" +#import "Source/common/SNTKVOManager.h" +#import "Source/common/SNTLogging.h" +#import "Source/common/SNTXPCNotifierInterface.h" +#import "Source/common/SNTXPCSyncServiceInterface.h" +#include "Source/santad/EventProviders/AuthResultCache.h" +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" +#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h" +#include "Source/santad/Logs/EndpointSecurity/Logger.h" +#include "Source/santad/SNTDaemonControlController.h" + +using santa::santad::Metrics; +using santa::santad::event_providers::AuthResultCache; +using santa::santad::event_providers::FlushCacheMode; +using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI; +using santa::santad::event_providers::endpoint_security::Enricher; +using santa::santad::logs::endpoint_security::Logger; + +static void EstablishSyncServiceConnection(SNTSyncdQueue *syncd_queue) { + // The syncBaseURL check is here to stop retrying if the sync server is removed. + if (![[SNTConfigurator configurator] syncBaseURL]) { + return; + } + + MOLXPCConnection *ss = [SNTXPCSyncServiceInterface configuredConnection]; + + // This will handle retying connection establishment if there are issues with the service + // during initialization (missing binary, malformed plist, bad code signature, etc.). + // Once those issues are resolved the connection will establish. + // This will also handle re-establishment if the service crashes or is killed. + ss.invalidationHandler = ^(void) { + syncd_queue.syncConnection.invalidationHandler = nil; + dispatch_sync(dispatch_get_main_queue(), ^{ + EstablishSyncServiceConnection(syncd_queue); + }); + }; + [ss resume]; + syncd_queue.syncConnection = ss; +} + +void SantadMain(std::shared_ptr esapi, std::shared_ptr logger, + std::shared_ptr metrics, std::shared_ptr enricher, + std::shared_ptr auth_result_cache, + MOLXPCConnection *control_connection, SNTCompilerController *compiler_controller, + SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue, + SNTExecutionController *exec_controller, + std::shared_ptr prefix_tree) { + SNTConfigurator *configurator = [SNTConfigurator configurator]; + + SNTDaemonControlController *dc = + [[SNTDaemonControlController alloc] initWithAuthResultCache:auth_result_cache + notificationQueue:notifier_queue + syncdQueue:syncd_queue + logger:logger]; + + control_connection.exportedObject = dc; + [control_connection resume]; + + if ([configurator exportMetrics]) { + metrics->StartPoll(); + } + + SNTEndpointSecurityDeviceManager *device_client = + [[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:esapi + logger:logger + authResultCache:auth_result_cache]; + + device_client.blockUSBMount = [configurator blockUSBMount]; + device_client.remountArgs = [configurator remountUSBMode]; + device_client.deviceBlockCallback = ^(SNTDeviceEvent *event) { + [[notifier_queue.notifierConnection remoteObjectProxy] + postUSBBlockNotification:event + withCustomMessage:([configurator remountUSBMode] + ? [configurator bannedUSBBlockMessage] + : [configurator remountUSBBlockMessage])]; + }; + + SNTEndpointSecurityRecorder *monitor_client = + [[SNTEndpointSecurityRecorder alloc] initWithESAPI:esapi + logger:logger + enricher:enricher + compilerController:compiler_controller + authResultCache:auth_result_cache + prefixTree:prefix_tree]; + + SNTEndpointSecurityAuthorizer *authorizer_client = + [[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:esapi + execController:exec_controller + compilerController:compiler_controller + authResultCache:auth_result_cache]; + + SNTEndpointSecurityTamperResistance *tamper_client = + [[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:esapi logger:logger]; + + EstablishSyncServiceConnection(syncd_queue); + + NSArray *kvoObservers = @[ + [[SNTKVOManager alloc] initWithObject:configurator + selector:@selector(clientMode) + type:[NSNumber class] + callback:^(NSNumber *oldValue, NSNumber *newValue) { + if ([oldValue longLongValue] == [newValue longLongValue]) { + // Note: This case apparently can happen and if not checked + // will result in excessive notification messages sent to the + // user when calling `postClientModeNotification` below + return; + } + + SNTClientMode clientMode = + (SNTClientMode)[newValue longLongValue]; + + switch (clientMode) { + case SNTClientModeLockdown: + LOGI(@"Changed client mode to Lockdown, flushing cache."); + auth_result_cache->FlushCache(FlushCacheMode::kAllCaches); + break; + case SNTClientModeMonitor: + LOGI(@"Changed client mode to Monitor."); + break; + default: LOGW(@"Changed client mode to unknown value."); break; + } + + [[notifier_queue.notifierConnection remoteObjectProxy] + postClientModeNotification:clientMode]; + }], + [[SNTKVOManager alloc] + initWithObject:configurator + selector:@selector(syncBaseURL) + type:[NSURL class] + callback:^(NSURL *oldValue, NSURL *newValue) { + if ((!newValue && !oldValue) || + ([newValue.absoluteString isEqualToString:oldValue.absoluteString])) { + return; + } + + if (newValue) { + LOGI(@"Establishing a new sync service connection with SyncBaseURL: %@", newValue); + [NSObject cancelPreviousPerformRequestsWithTarget:[SNTConfigurator configurator] + selector:@selector(clearSyncState) + object:nil]; + [[syncd_queue.syncConnection remoteObjectProxy] spindown]; + EstablishSyncServiceConnection(syncd_queue); + } else { + LOGI(@"SyncBaseURL removed, spinning down sync service"); + [[syncd_queue.syncConnection remoteObjectProxy] spindown]; + // Keep the syncState active for 10 min in case com.apple.ManagedClient is + // flapping. + [[SNTConfigurator configurator] performSelector:@selector(clearSyncState) + withObject:nil + afterDelay:600]; + } + }], + [[SNTKVOManager alloc] + initWithObject:configurator + selector:@selector(exportMetrics) + type:[NSNumber class] + callback:^(NSNumber *oldValue, NSNumber *newValue) { + BOOL oldBool = [oldValue boolValue]; + BOOL newBool = [newValue boolValue]; + if (oldBool == NO && newBool == YES) { + LOGI(@"metricsExport changed NO -> YES, starting to export metrics"); + metrics->StartPoll(); + } else if (oldBool == YES && newBool == NO) { + LOGI(@"metricsExport changed YES -> NO, stopping export of metrics"); + metrics->StopPoll(); + } + }], + [[SNTKVOManager alloc] + initWithObject:configurator + selector:@selector(metricExportInterval) + type:[NSNumber class] + callback:^(NSNumber *oldValue, NSNumber *newValue) { + uint64_t oldInterval = [oldValue unsignedIntValue]; + uint64_t newInterval = [newValue unsignedIntValue]; + LOGI(@"MetricExportInterval changed from %llu to %llu restarting export", oldInterval, + newInterval); + metrics->SetInterval(newInterval); + }], + [[SNTKVOManager alloc] + initWithObject:configurator + selector:@selector(allowedPathRegex) + type:[NSRegularExpression class] + callback:^(NSRegularExpression *oldValue, NSRegularExpression *newValue) { + if ((!newValue && !oldValue) || + ([newValue.pattern isEqualToString:oldValue.pattern])) { + return; + } + + LOGI(@"Changed allowlist regex, flushing cache"); + auth_result_cache->FlushCache(FlushCacheMode::kAllCaches); + }], + [[SNTKVOManager alloc] + initWithObject:configurator + selector:@selector(blockedPathRegex) + type:[NSRegularExpression class] + callback:^(NSRegularExpression *oldValue, NSRegularExpression *newValue) { + if ((!newValue && !oldValue) || + ([newValue.pattern isEqualToString:oldValue.pattern])) { + return; + } + + LOGI(@"Changed denylist regex, flushing cache"); + auth_result_cache->FlushCache(FlushCacheMode::kAllCaches); + }], + [[SNTKVOManager alloc] initWithObject:configurator + selector:@selector(blockUSBMount) + type:[NSNumber class] + callback:^(NSNumber *oldValue, NSNumber *newValue) { + BOOL oldBool = [oldValue boolValue]; + BOOL newBool = [newValue boolValue]; + + if (oldBool == newBool) { + return; + } + + LOGI(@"BlockUSBMount changed: %d -> %d", oldBool, newBool); + device_client.blockUSBMount = newBool; + }], + [[SNTKVOManager alloc] initWithObject:configurator + selector:@selector(remountUSBMode) + type:[NSArray class] + callback:^(NSArray *oldValue, NSArray *newValue) { + if (!oldValue && !newValue) { + return; + } + + // Ensure the arrays are composed of strings + for (id element in oldValue) { + if (![element isKindOfClass:[NSString class]]) { + return; + } + } + + for (id element in newValue) { + if (![element isKindOfClass:[NSString class]]) { + return; + } + } + + if ([oldValue isEqualToArray:newValue]) { + return; + } + + LOGI(@"RemountArgs changed: %@ -> %@", + [oldValue componentsJoinedByString:@","], + [newValue componentsJoinedByString:@","]); + device_client.remountArgs = newValue; + }], + ]; + + // Make the compiler happy. The variable is only used to ensure proper lifetime + // of the SNTKVOManager objects it contains. + (void)kvoObservers; + + [monitor_client enable]; + [authorizer_client enable]; + [device_client enable]; + [tamper_client enable]; + + [[NSRunLoop mainRunLoop] run]; +} diff --git a/Source/santad/SantadDeps.h b/Source/santad/SantadDeps.h new file mode 100644 index 000000000..ab011e220 --- /dev/null +++ b/Source/santad/SantadDeps.h @@ -0,0 +1,80 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__SANTAD__SANTAD_DEPS_H +#define SANTA__SANTAD__SANTAD_DEPS_H + +#include +#import + +#include + +#include "Source/common/SNTPrefixTree.h" +#include "Source/santad/EventProviders/AuthResultCache.h" +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" +#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h" +#include "Source/santad/Logs/EndpointSecurity/Logger.h" +#include "Source/santad/Metrics.h" +#import "Source/santad/SNTCompilerController.h" +#import "Source/santad/SNTExecutionController.h" +#import "Source/santad/SNTNotificationQueue.h" +#import "Source/santad/SNTSyncdQueue.h" + +namespace santa::santad { + +class SantadDeps { + public: + static std::unique_ptr Create(NSUInteger metric_export_interval, + SNTEventLogType event_log_type, + NSString *event_log_path, + NSArray *prefix_filters); + + SantadDeps( + NSUInteger metric_export_interval, + std::shared_ptr esapi, + std::unique_ptr logger, + MOLXPCConnection *control_connection, SNTCompilerController *compiler_controller, + SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue, + SNTExecutionController *exec_controller, std::shared_ptr prefix_tree); + + std::shared_ptr AuthResultCache(); + std::shared_ptr Enricher(); + std::shared_ptr ESAPI(); + std::shared_ptr Logger(); + std::shared_ptr Metrics(); + MOLXPCConnection *ControlConnection(); + SNTCompilerController *CompilerController(); + SNTNotificationQueue *NotifierQueue(); + SNTSyncdQueue *SyncdQueue(); + SNTExecutionController *ExecController(); + std::shared_ptr PrefixTree(); + + private: + std::shared_ptr esapi_; + std::shared_ptr logger_; + std::shared_ptr metrics_; + std::shared_ptr enricher_; + std::shared_ptr auth_result_cache_; + + MOLXPCConnection *control_connection_; + SNTCompilerController *compiler_controller_; + SNTNotificationQueue *notifier_queue_; + SNTSyncdQueue *syncd_queue_; + SNTExecutionController *exec_controller_; + std::shared_ptr prefix_tree_; +}; + +} // namespace santa::santad + +#endif diff --git a/Source/santad/SantadDeps.mm b/Source/santad/SantadDeps.mm new file mode 100644 index 000000000..ad7c58773 --- /dev/null +++ b/Source/santad/SantadDeps.mm @@ -0,0 +1,175 @@ +/// Copyright 2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include "Source/santad/SantadDeps.h" +#include + +#include + +#import "Source/common/SNTLogging.h" +#import "Source/common/SNTXPCControlInterface.h" +#import "Source/santad/DataLayer/SNTEventTable.h" +#import "Source/santad/DataLayer/SNTRuleTable.h" +#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" +#import "Source/santad/SNTDatabaseController.h" + +using santa::santad::Metrics; +using santa::santad::event_providers::AuthResultCache; +using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI; +using santa::santad::event_providers::endpoint_security::Enricher; +using santa::santad::logs::endpoint_security::Logger; + +namespace santa::santad { + +std::unique_ptr SantadDeps::Create(NSUInteger metric_export_interval, + SNTEventLogType event_log_type, + NSString *event_log_path, + NSArray *prefix_filters) { + // TODO(mlw): The XPC interfaces should be injectable. Could either make a new + // protocol defining appropriate methods or accept values as params. + MOLXPCConnection *control_connection = + [[MOLXPCConnection alloc] initServerWithName:[SNTXPCControlInterface serviceID]]; + if (!control_connection) { + LOGE(@"Failed to initialize control connection."); + exit(EXIT_FAILURE); + } + + control_connection.privilegedInterface = [SNTXPCControlInterface controlInterface]; + control_connection.unprivilegedInterface = [SNTXPCUnprivilegedControlInterface controlInterface]; + + SNTRuleTable *rule_table = [SNTDatabaseController ruleTable]; + if (!rule_table) { + LOGE(@"Failed to initialize rule table."); + exit(EXIT_FAILURE); + } + + SNTEventTable *event_table = [SNTDatabaseController eventTable]; + if (!event_table) { + LOGE(@"Failed to initialize event table."); + exit(EXIT_FAILURE); + } + + SNTCompilerController *compiler_controller = [[SNTCompilerController alloc] init]; + if (!compiler_controller) { + LOGE(@"Failed to initialize compiler controller."); + exit(EXIT_FAILURE); + } + + SNTNotificationQueue *notifier_queue = [[SNTNotificationQueue alloc] init]; + if (!notifier_queue) { + LOGE(@"Failed to initialize notification queue."); + exit(EXIT_FAILURE); + } + + SNTSyncdQueue *syncd_queue = [[SNTSyncdQueue alloc] init]; + if (!syncd_queue) { + LOGE(@"Failed to initialize syncd queue."); + exit(EXIT_FAILURE); + } + + SNTExecutionController *exec_controller = + [[SNTExecutionController alloc] initWithRuleTable:rule_table + eventTable:event_table + notifierQueue:notifier_queue + syncdQueue:syncd_queue]; + if (!exec_controller) { + LOGE(@"Failed to initialize exec controller."); + exit(EXIT_FAILURE); + } + + std::shared_ptr prefix_tree = std::make_shared(); + for (NSString *filter in prefix_filters) { + prefix_tree->AddPrefix([filter fileSystemRepresentation]); + } + + std::shared_ptr esapi = std::make_shared(); + if (!esapi) { + LOGE(@"Failed to create ES API wrapper."); + exit(EXIT_FAILURE); + } + + std::unique_ptr<::Logger> logger = Logger::Create(esapi, event_log_type, event_log_path); + if (!logger) { + LOGE(@"Failed to create logger."); + exit(EXIT_FAILURE); + } + + return std::make_unique(metric_export_interval, esapi, std::move(logger), + control_connection, compiler_controller, notifier_queue, + syncd_queue, exec_controller, prefix_tree); +} + +SantadDeps::SantadDeps(NSUInteger metric_export_interval, + std::shared_ptr esapi, std::unique_ptr<::Logger> logger, + MOLXPCConnection *control_connection, + SNTCompilerController *compiler_controller, + SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue, + SNTExecutionController *exec_controller, + std::shared_ptr prefix_tree) + : esapi_(std::move(esapi)), + logger_(std::move(logger)), + metrics_(Metrics::Create(metric_export_interval)), + enricher_(std::make_shared<::Enricher>()), + auth_result_cache_(std::make_shared<::AuthResultCache>(esapi_)), + control_connection_(control_connection), + compiler_controller_(compiler_controller), + notifier_queue_(notifier_queue), + syncd_queue_(syncd_queue), + exec_controller_(exec_controller), + prefix_tree_(prefix_tree) {} + +std::shared_ptr<::AuthResultCache> SantadDeps::AuthResultCache() { + return auth_result_cache_; +} + +std::shared_ptr SantadDeps::Enricher() { + return enricher_; +} +std::shared_ptr SantadDeps::ESAPI() { + return esapi_; +} + +std::shared_ptr SantadDeps::Logger() { + return logger_; +} + +std::shared_ptr SantadDeps::Metrics() { + return metrics_; +} + +MOLXPCConnection *SantadDeps::ControlConnection() { + return control_connection_; +} + +SNTCompilerController *SantadDeps::CompilerController() { + return compiler_controller_; +} + +SNTNotificationQueue *SantadDeps::NotifierQueue() { + return notifier_queue_; +} + +SNTSyncdQueue *SantadDeps::SyncdQueue() { + return syncd_queue_; +} + +SNTExecutionController *SantadDeps::ExecController() { + return exec_controller_; +} + +std::shared_ptr SantadDeps::PrefixTree() { + return prefix_tree_; +} + +} // namespace santa::santad diff --git a/Source/santad/SNTApplicationTest.m b/Source/santad/SantadTest.mm similarity index 60% rename from Source/santad/SNTApplicationTest.m rename to Source/santad/SantadTest.mm index 86e478921..d5aa808fd 100644 --- a/Source/santad/SNTApplicationTest.m +++ b/Source/santad/SantadTest.mm @@ -1,4 +1,4 @@ -/// Copyright 2021 Google Inc. All rights reserved. +/// Copyright 2022 Google Inc. All rights reserved. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. @@ -11,26 +11,37 @@ /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. + #import #import #import #import #import #import +#import +#include +#include + +#include #import "Source/common/SNTConfigurator.h" -#import "Source/santad/SNTApplication.h" +#include "Source/common/TestUtils.h" +#include "Source/santad/EventProviders/EndpointSecurity/Message.h" +#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h" +#import "Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h" #import "Source/santad/SNTDatabaseController.h" +#include "Source/santad/SantadDeps.h" -#include "Source/santad/EventProviders/EndpointSecurityTestUtil.h" +using santa::santad::SantadDeps; +using santa::santad::event_providers::endpoint_security::Message; NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules"; -@interface SNTApplicationTest : XCTestCase +@interface SantadTest : XCTestCase @property id mockSNTDatabaseController; @end -@implementation SNTApplicationTest +@implementation SantadTest - (void)setUp { [super setUp]; fclose(stdout); @@ -45,8 +56,8 @@ - (void)tearDown { - (BOOL)checkBinaryExecution:(NSString *)binaryName wantResult:(es_auth_result_t)wantResult clientMode:(NSInteger)clientMode { - MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity]; - [mockES reset]; + auto mockESApi = std::make_shared(); + mockESApi->SetExpectationsESNewClient(); id mockConfigurator = OCMClassMock([SNTConfigurator class]); @@ -64,51 +75,74 @@ - (BOOL)checkBinaryExecution:(NSString *)binaryName OCMStub([self.mockSNTDatabaseController databasePath]).andReturn(testPath); - SNTApplication *app = [[SNTApplication alloc] init]; - [app start]; - - XCTestExpectation *santaInit = - [self expectationWithDescription:@"Wait for Santa to subscribe to EndpointSecurity"]; - - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - while ([mockES.subscriptions[ES_EVENT_TYPE_AUTH_EXEC] isEqualTo:@NO]) - ; - [santaInit fulfill]; - }); + std::unique_ptr deps = + SantadDeps::Create([mockConfigurator metricExportInterval], [mockConfigurator eventLogType], + [mockConfigurator eventLogPath], @[ @"/.", @"/dev/" ]); - // Ugly hack to deflake the test and allow listenForDecisionRequests to install the correct - // decision callback. - sleep(1); - [self waitForExpectations:@[ santaInit ] timeout:10.0]; + SNTEndpointSecurityAuthorizer *authClient = + [[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi + execController:deps->ExecController() + compilerController:deps->CompilerController() + authResultCache:deps->AuthResultCache()]; XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for santa's Auth dispatch queue"]; - __block ESResponse *got = nil; - [mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_EXEC - withCallback:^(ESResponse *r) { - got = r; - [expectation fulfill]; - }]; - - NSString *binaryPath = [NSString pathWithComponents:@[ testPath, binaryName ]]; + + EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, wantResult, + wantResult == ES_AUTH_RESULT_ALLOW)) + .WillOnce(testing::InvokeWithoutArgs(^bool { + [expectation fulfill]; + return true; + })); + + NSString *binaryPath = + [[NSString pathWithComponents:@[ testPath, binaryName ]] stringByResolvingSymlinksInPath]; struct stat fileStat; lstat(binaryPath.UTF8String, &fileStat); - ESMessage *msg = [[ESMessage alloc] initWithBlock:^(ESMessage *m) { - m.binaryPath = binaryPath; - m.executable->stat = fileStat; - m.message->action_type = ES_ACTION_TYPE_AUTH; - m.message->event_type = ES_EVENT_TYPE_AUTH_EXEC; - m.message->event = (es_events_t){.exec = {.target = m.process}}; - }]; + es_file_t file = MakeESFile([binaryPath UTF8String], fileStat); + es_process_t proc = MakeESProcess(&file); + // Set a 6.5 second deadline for the message. The base SNTEndpointSecurityClient + // class leaves a 5 second buffer to auto-respond to messages. A 6 second + // deadline means there is a 1.5 second leeway given for the processing block + // to finish its tasks and release the `Message`. This will add about 1 second + // to the run time of each test case since each one must wait for the + // deadline block to run and release the message. + es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth, 6500); + esMsg.event.exec.target = &proc; + // Need a pointer to esMsg to capture in blocks below. + es_message_t *heapESMsg = &esMsg; + + // The test must wait for the ES client async message processing to complete. + // Otherwise, the `es_message_t` stack variable will go out of scope and will + // result in undefined behavior in the async dispatch queue block. + // To do this, track the `Message` retain counts, and only allow the test + // to continue once the retain count drops to 0 indicating the client is + // no longer using the message. + __block int retainCount = 0; + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + EXPECT_CALL(*mockESApi, ReleaseMessage).WillRepeatedly(^{ + if (retainCount == 0) { + XCTFail("Under retain!"); + } + retainCount--; + if (retainCount == 0) { + dispatch_semaphore_signal(sema); + } + }); + EXPECT_CALL(*mockESApi, RetainMessage).WillRepeatedly(^{ + retainCount++; + return heapESMsg; + }); - [mockES triggerHandler:msg.message]; + [authClient handleMessage:Message(mockESApi, &esMsg)]; [self waitForExpectations:@[ expectation ] timeout:10.0]; - NSString *clientModeStr = (clientMode == SNTClientModeLockdown) ? @"LOCKDOWN" : @"MONITOR"; - XCTAssertEqual(got.result, wantResult, - @"received unexpected ES response on executing \"%@/%@\" in clientMode %@", - testPath, binaryName, clientModeStr); + XCTAssertEqual(0, + dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)), + "Failed waiting for message to be processed..."); + + XCTBubbleMockVerifyAndClearExpectations(mockESApi.get()); } /** diff --git a/Source/santad/main.m b/Source/santad/main.m deleted file mode 100644 index e412a21ad..000000000 --- a/Source/santad/main.m +++ /dev/null @@ -1,145 +0,0 @@ -/// Copyright 2015 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -#import - -#import "Source/common/SNTCommonEnums.h" -#import "Source/common/SNTConfigurator.h" -#import "Source/common/SNTLogging.h" -#import "Source/santad/SNTApplication.h" - -#include -#include -#include - -extern uint64_t watchdogCPUEvents; -extern uint64_t watchdogRAMEvents; -extern double watchdogCPUPeak; -extern double watchdogRAMPeak; - -/// Converts a timeval struct to double, converting the microseconds value to seconds. -static inline double timeval_to_double(time_value_t tv) { - return (double)tv.seconds + (double)tv.microseconds / 1000000.0; -} - -/// The watchdog thread function, used to monitor santad CPU/RAM usage and print a warning -/// if it goes over certain thresholds. -void *watchdogThreadFunction(__unused void *idata) { - pthread_setname_np("com.google.santa.watchdog"); - - // Number of seconds to wait between checks. - const int timeInterval = 30; - - // Amount of CPU usage to trigger warning, as a percentage averaged over timeInterval - // santad's usual CPU usage is 0-3% but can occasionally spike if lots of processes start at once. - const int cpuWarnThreshold = 20.0; - - // Amount of RAM usage to trigger warning, in MB. - // santad's usual RAM usage is between 5-50MB but can spike if lots of processes start at once. - const int memWarnThreshold = 250; - - double prevTotalTime = 0.0; - double prevRamUseMB = 0.0; - struct mach_task_basic_info taskInfo; - mach_msg_type_number_t taskInfoCount = MACH_TASK_BASIC_INFO_COUNT; - - while (true) { - @autoreleasepool { - if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&taskInfo, - &taskInfoCount) == KERN_SUCCESS) { - // CPU - double totalTime = - (timeval_to_double(taskInfo.user_time) + timeval_to_double(taskInfo.system_time)); - double percentage = (((totalTime - prevTotalTime) / (double)timeInterval) * 100.0); - prevTotalTime = totalTime; - - if (percentage > cpuWarnThreshold) { - LOGW(@"Watchdog: potentially high CPU use, ~%.2f%% over last %d seconds.", percentage, - timeInterval); - watchdogCPUEvents++; - } - - if (percentage > watchdogCPUPeak) watchdogCPUPeak = percentage; - - // RAM - double ramUseMB = (double)taskInfo.resident_size / 1024 / 1024; - if (ramUseMB > memWarnThreshold && ramUseMB > prevRamUseMB) { - LOGW(@"Watchdog: potentially high RAM use, RSS is %.2fMB.", ramUseMB); - watchdogRAMEvents++; - } - prevRamUseMB = ramUseMB; - - if (ramUseMB > watchdogRAMPeak) watchdogRAMPeak = ramUseMB; - } - - sleep(timeInterval); - } - } - return NULL; -} - -void cleanup() { - LOGI(@"com.google.santa.daemon is running from an unexpected path: cleaning up"); - NSFileManager *fm = [NSFileManager defaultManager]; - [fm removeItemAtPath:@"/Library/LaunchDaemons/com.google.santad.plist" error:NULL]; - - LOGI(@"loading com.google.santa.daemon as a SystemExtension"); - NSTask *t = [[NSTask alloc] init]; - t.launchPath = [@(kSantaAppPath) stringByAppendingString:@"/Contents/MacOS/Santa"]; - t.arguments = @[ @"--load-system-extension" ]; - [t launch]; - [t waitUntilExit]; - - t = [[NSTask alloc] init]; - t.launchPath = @"/bin/launchctl"; - t.arguments = @[ @"remove", @"com.google.santad" ]; - [t launch]; - [t waitUntilExit]; - - // This exit will likely never be called because the above launchctl command kill us. - exit(0); -} - -int main(int argc, const char *argv[]) { - @autoreleasepool { - // Do not wait on child processes - signal(SIGCHLD, SIG_IGN); - - NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary]; - NSProcessInfo *pi = [NSProcessInfo processInfo]; - - NSString *productVersion = infoDict[@"CFBundleShortVersionString"]; - NSString *buildVersion = - [[infoDict[@"CFBundleVersion"] componentsSeparatedByString:@"."] lastObject]; - - if ([pi.arguments containsObject:@"-v"]) { - printf("%s (build %s)\n", [productVersion UTF8String], [buildVersion UTF8String]); - return 0; - } - - LOGI(@"Started, version %@ (build %@)", productVersion, buildVersion); - - // Handle the case of macOS < 10.15 updating to >= 10.15. - if ([pi.arguments.firstObject isEqualToString:@(kSantaDPath)]) cleanup(); - - SNTApplication *s = [[SNTApplication alloc] init]; - [s start]; - - // Create watchdog thread - pthread_t watchdogThread; - pthread_create(&watchdogThread, NULL, watchdogThreadFunction, NULL); - - [[NSRunLoop mainRunLoop] run]; - } -} diff --git a/Source/santad/main.mm b/Source/santad/main.mm new file mode 100644 index 000000000..d17482139 --- /dev/null +++ b/Source/santad/main.mm @@ -0,0 +1,171 @@ +/// Copyright 2015-2022 Google Inc. All rights reserved. +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include +#include +#include +#include + +#import "Source/common/SNTConfigurator.h" +#import "Source/common/SNTLogging.h" +#import "Source/santad/Santad.h" +#include "Source/santad/SantadDeps.h" + +using santa::santad::SantadDeps; + +// Number of seconds to wait between checks. +const int kWatchdogTimeInterval = 30; + +extern "C" uint64_t watchdogCPUEvents; +extern "C" uint64_t watchdogRAMEvents; +extern "C" double watchdogCPUPeak; +extern "C" double watchdogRAMPeak; + +struct WatchdogState { + double prev_total_time; + double prev_ram_use_mb; +}; + +/// Converts a timeval struct to double, converting the microseconds value to seconds. +static inline double timeval_to_double(time_value_t tv) { + return (double)tv.seconds + (double)tv.microseconds / 1000000.0; +} + +/// The watchdog thread function, used to monitor santad CPU/RAM usage and print a warning +/// if it goes over certain thresholds. +static void SantaWatchdog(void *context) { + WatchdogState *state = (WatchdogState *)context; + + // Amount of CPU usage to trigger warning, as a percentage averaged over kWatchdogTimeInterval + // santad's usual CPU usage is 0-3% but can occasionally spike if lots of processes start at once. + const int cpu_warn_threshold = 20.0; + + // Amount of RAM usage to trigger warning, in MB. + // santad's usual RAM usage is between 5-50MB but can spike if lots of processes start at once. + const int mem_warn_threshold = 250; + + struct mach_task_basic_info info; + mach_msg_type_number_t task_info_count = MACH_TASK_BASIC_INFO_COUNT; + + if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &task_info_count) == + KERN_SUCCESS) { + // CPU + double total_time = (timeval_to_double(info.user_time) + timeval_to_double(info.system_time)); + double percentage = + (((total_time - state->prev_total_time) / (double)kWatchdogTimeInterval) * 100.0); + state->prev_total_time = total_time; + + if (percentage > cpu_warn_threshold) { + LOGW(@"Watchdog: potentially high CPU use, ~%.2f%% over last %d seconds.", percentage, + kWatchdogTimeInterval); + watchdogCPUEvents++; + } + + if (percentage > watchdogCPUPeak) watchdogCPUPeak = percentage; + + // RAM + double ram_use_mb = (double)info.resident_size / 1024 / 1024; + if (ram_use_mb > mem_warn_threshold && ram_use_mb > state->prev_ram_use_mb) { + LOGW(@"Watchdog: potentially high RAM use, RSS is %.2fMB.", ram_use_mb); + watchdogRAMEvents++; + } + state->prev_ram_use_mb = ram_use_mb; + + if (ram_use_mb > watchdogRAMPeak) { + watchdogRAMPeak = ram_use_mb; + } + } +} + +void CleanupAndReExec() { + LOGI(@"com.google.santa.daemon is running from an unexpected path: cleaning up"); + NSFileManager *fm = [NSFileManager defaultManager]; + [fm removeItemAtPath:@"/Library/LaunchDaemons/com.google.santad.plist" error:NULL]; + + LOGI(@"loading com.google.santa.daemon as a SystemExtension"); + NSTask *t = [[NSTask alloc] init]; + t.launchPath = [@(kSantaAppPath) stringByAppendingString:@"/Contents/MacOS/Santa"]; + t.arguments = @[ @"--load-system-extension" ]; + [t launch]; + [t waitUntilExit]; + + t = [[NSTask alloc] init]; + t.launchPath = @"/bin/launchctl"; + t.arguments = @[ @"remove", @"com.google.santad" ]; + [t launch]; + [t waitUntilExit]; + + // This exit will likely never be called because the above launchctl command will kill us. + exit(0); +} + +int main(int argc, char *argv[]) { + @autoreleasepool { + // Do not wait on child processes + signal(SIGCHLD, SIG_IGN); + + NSDictionary *info_dict = [[NSBundle mainBundle] infoDictionary]; + NSProcessInfo *pi = [NSProcessInfo processInfo]; + + NSString *product_version = info_dict[@"CFBundleShortVersionString"]; + NSString *build_version = + [[info_dict[@"CFBundleVersion"] componentsSeparatedByString:@"."] lastObject]; + + if ([pi.arguments containsObject:@"-v"]) { + printf("%s (build %s)\n", [product_version UTF8String], [build_version UTF8String]); + return 0; + } + + // Ensure Santa daemon is started as a system extension + if ([pi.arguments.firstObject isEqualToString:@(kSantaDPath)]) { + // Does not return + CleanupAndReExec(); + } + + dispatch_queue_t watchdog_queue = dispatch_queue_create( + "com.google.santa.daemon.watchdog", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL); + dispatch_source_t watchdog_timer = + dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, watchdog_queue); + + WatchdogState state = {.prev_total_time = 0.0, .prev_ram_use_mb = 0.0}; + + if (watchdog_timer) { + dispatch_source_set_timer(watchdog_timer, DISPATCH_TIME_NOW, + kWatchdogTimeInterval * NSEC_PER_SEC, 0); + dispatch_source_set_event_handler_f(watchdog_timer, SantaWatchdog); + dispatch_set_context(watchdog_timer, &state); + dispatch_resume(watchdog_timer); + } else { + LOGE(@"Failed to start Santa watchdog"); + } + + SNTConfigurator *configurator = [SNTConfigurator configurator]; + + // TODO(bur): Add KVO handling for fileChangesPrefixFilters. + NSArray *prefix_filters = + [@[ @"/.", @"/dev/" ] arrayByAddingObjectsFromArray:[configurator fileChangesPrefixFilters]]; + + std::unique_ptr deps = + SantadDeps::Create([configurator metricExportInterval], [configurator eventLogType], + [configurator eventLogPath], prefix_filters); + + // This doesn't return + SantadMain(deps->ESAPI(), deps->Logger(), deps->Metrics(), deps->Enricher(), + deps->AuthResultCache(), deps->ControlConnection(), deps->CompilerController(), + deps->NotifierQueue(), deps->SyncdQueue(), deps->ExecController(), + deps->PrefixTree()); + } + + return 0; +} diff --git a/Testing/benchmark.sh b/Testing/benchmark.sh deleted file mode 100755 index 6d165abae..000000000 --- a/Testing/benchmark.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -# TODO: Pull benchmarks from previous commit to check for regression -bazel test //:benchmarks --define=SANTA_BUILD_TYPE=adhoc diff --git a/Testing/fix.sh b/Testing/fix.sh index 684a8b4de..adcb3585a 100755 --- a/Testing/fix.sh +++ b/Testing/fix.sh @@ -1,5 +1,5 @@ #!/bin/bash GIT_ROOT=$(git rev-parse --show-toplevel) -find $GIT_ROOT \( -name "*.m" -o -name "*.h" -name "*.mm" \) -exec xcrun clang-format -i {} \+ +find $GIT_ROOT \( -name "*.m" -o -name "*.h" -o -name "*.mm" \) -exec xcrun clang-format -i {} \+ buildifier --lint=fix -r $GIT_ROOT diff --git a/Testing/integration/BUILD b/Testing/integration/BUILD index cad0cf79c..133c2608e 100644 --- a/Testing/integration/BUILD +++ b/Testing/integration/BUILD @@ -1,8 +1,4 @@ load("//:helper.bzl", "santa_unit_test") -load( - "@build_bazel_rules_apple//apple:macos.bzl", - "macos_command_line_application", -) package(default_visibility = ["//:santa_package_group"]) @@ -26,23 +22,3 @@ test_suite( ":SNTExecTest", ], ) - -objc_library( - name = "logging_benchmarks_lib", - testonly = 1, - srcs = [ - "SNTLoggingBenchmarks.m", - ], - deps = [ - "//Source/santad:EndpointSecurityTestLib", - "//Source/santad:event_logs", - ], -) - -macos_command_line_application( - name = "logging_benchmarks_bin", - testonly = 1, - bundle_id = "com.goole.santa.benchmark.logging", - minimum_os_version = "10.15", - deps = [":logging_benchmarks_lib"], -) diff --git a/Testing/integration/SNTLoggingBenchmarks.m b/Testing/integration/SNTLoggingBenchmarks.m deleted file mode 100644 index 20e0c7315..000000000 --- a/Testing/integration/SNTLoggingBenchmarks.m +++ /dev/null @@ -1,159 +0,0 @@ -/// Copyright 2021 Google Inc. All rights reserved. -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. - -// clang-format off -/// This benchmarking program pairs well with the hyperfine benchmarking utility: -/// https://github.com/sharkdp/hyperfine -/// -/// Some example invocations: -/// - Compare performance of different in-memory buffer sizes by controlling file size threshold -/// hyperfine --warmup 3 -P fssize 50 500 -D 50 'santa_logging_benchmarks_bin -i 20000 -l protobuf -f {fssize} -s 500 -t 5' -/// -/// - Compare the file logger vs the protobuf logger -/// hyperfine --warmup 3 --parameter-list logger file,protobuf 'santa_logging_benchmarks_bin -i 100000 -l {logger} -f 100 -s 500 -t 5' -// clang-format on - -#import -#import -#import -#import - -#import "Source/common/SNTConfigurator.h" -#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h" -#import "Source/santad/Logs/SNTFileEventLog.h" -#import "Source/santad/Logs/SNTProtobufEventLog.h" -#import "Source/santad/Logs/SNTSyslogEventLog.h" - -@interface SNTConfigurator (Testing) -@property NSMutableDictionary *configState; -@end - -void usage(void) { - fprintf(stderr, "Usage: %s [-i ] [-l (file|syslog|protobuf)]\n", getprogname()); -} - -void runLogFileModification(santa_message_t *msg, SNTEventLog *eventLog) { - msg->action = ACTION_NOTIFY_RENAME; - [eventLog logFileModification:*msg]; -} - -BOOL createTestDir(NSURL *dir) { - return [[NSFileManager defaultManager] createDirectoryAtURL:dir - withIntermediateDirectories:YES - attributes:nil - error:nil]; -} - -void setup(int iterations, SNTEventLog *eventLog) { - // Create and populate necessary values and data structures in advance to - // minimze the effect on overall run time. - static const char *commonProcName = "launchd"; - static const char *commonPath = "/sbin/launchd"; - static const char *commonNewPath = "/foo/bar.txt"; - NSArray *execArgs = @[ @"/sbin/launchd", @"--init", @"--testing" ]; - struct timespec ts = {123, 456}; - - es_file_t esFile = MakeESFile(commonPath); - es_process_t esProc = MakeESProcess(&esFile); - es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &esProc, ts); - - santa_message_t santaMsg = {0}; - - santaMsg.uid = 242; - santaMsg.gid = 20; - santaMsg.pid = 1; - santaMsg.pidversion = 2; - santaMsg.ppid = 3; - - strlcpy(santaMsg.path, commonPath, sizeof(santaMsg.path)); - strlcpy(santaMsg.newpath, commonNewPath, sizeof(santaMsg.newpath)); - strlcpy(santaMsg.pname, commonProcName, sizeof(santaMsg.pname)); - - santaMsg.args_array = (__bridge void *)execArgs; - santaMsg.es_message = &esMsg; - - for (int i = 0; i < iterations; i++) { - [eventLog logFileModification:santaMsg]; - } -} - -int main(int argc, char *argv[]) { - @autoreleasepool { - static const struct option longopts[] = { - {"iter", required_argument, NULL, 'i'}, - {"logger", required_argument, NULL, 'l'}, - {NULL, 0, NULL, 0}, - }; - - int ch; - int iterations = 5000; - Class eventLogClass = [SNTProtobufEventLog class]; - SNTConfigurator *configurator = [SNTConfigurator configurator]; - unsigned int fileSize = 100; - unsigned int dirSize = 500; - float flushFrequency = 5.0; - - while ((ch = getopt_long(argc, argv, "f:i:l:s:t:", longopts, NULL)) != -1) { - switch (ch) { - case 'f': fileSize = atoi(optarg); break; - case 'i': iterations = atoi(optarg); break; - case 'l': - if (strcmp(optarg, "syslog") == 0) { - eventLogClass = [SNTSyslogEventLog class]; - } else if (strcmp(optarg, "file") == 0) { - eventLogClass = [SNTFileEventLog class]; - } else if (strcmp(optarg, "protobuf") == 0) { - eventLogClass = [SNTProtobufEventLog class]; - } else { - usage(); - exit(1); - } - break; - case 's': dirSize = atoi(optarg); break; - case 't': flushFrequency = (float)atoi(optarg); break; - default: usage(); exit(1); - } - } - - NSURL *santaTestDir = - [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"santa_test"]]; - NSURL *tempDir = [santaTestDir URLByAppendingPathComponent:[[NSUUID UUID] UUIDString]]; - NSURL *mailDir = [tempDir URLByAppendingPathComponent:@"pblogger"]; - NSURL *fileDir = [tempDir URLByAppendingPathComponent:@"filelogger"]; - NSString *eventLogPath = [[fileDir URLByAppendingPathComponent:@"santa.log"] path]; - - [[NSFileManager defaultManager] createDirectoryAtURL:mailDir - withIntermediateDirectories:YES - attributes:nil - error:nil]; - [[NSFileManager defaultManager] createDirectoryAtURL:fileDir - withIntermediateDirectories:YES - attributes:nil - error:nil]; - - configurator.configState[@"EventLogPath"] = eventLogPath; - configurator.configState[@"MailDirectory"] = mailDir.path; - configurator.configState[@"MailDirectoryFileSizeThresholdKB"] = @(fileSize); - configurator.configState[@"MailDirectorySizeThresholdMB"] = @(dirSize); - configurator.configState[@"MailDirectoryEventMaxFlushTimeSec"] = @(flushFrequency); - - NSLog(@"Using log path: %@", configurator.eventLogPath); - NSLog(@"Using mail dir: %@", configurator.mailDirectory); - NSLog(@"Using logger: %@", eventLogClass); - - SNTEventLog *eventLog = [[eventLogClass alloc] init]; - setup(iterations, eventLog); - [eventLog forceFlush]; - } -} diff --git a/Testing/lint.sh b/Testing/lint.sh index d356c6a22..b17379169 100755 --- a/Testing/lint.sh +++ b/Testing/lint.sh @@ -4,10 +4,10 @@ function main() { GIT_ROOT=$(git rev-parse --show-toplevel) err=0 - find $GIT_ROOT \( -name "*.m" -o -name "*.h" -name "*.mm" \) -exec clang-format --Werror --dry-run {} \+ + find $GIT_ROOT \( -name "*.m" -o -name "*.h" -o -name "*.mm" \) -exec clang-format --Werror --dry-run {} \+ err="$(( $err | $? ))" - ! git grep -EIn $'[ \t]+$' + ! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch' err="$(( $err | $? ))" go install github.com/bazelbuild/buildtools/buildifier@latest diff --git a/WORKSPACE b/WORKSPACE index 87a9ce721..db24e3c5b 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -51,13 +51,21 @@ git_repository( name = "hedron_compile_commands", commit = "92db741ee6dee0c4a83a5c58be7747df7b89ed10", remote = "https://github.com/hedronvision/bazel-compile-commands-extractor.git", - shallow_since = "1638167585 -0800", + shallow_since = "1640416382 -0800", ) load("@hedron_compile_commands//:workspace_setup.bzl", "hedron_compile_commands_setup") hedron_compile_commands_setup() +# Googletest - tag: release-1.12.1 +http_archive( + name = "com_google_googletest", + sha256 = "ab78fa3f912d44d38b785ec011a25f26512aaedc5291f51f3807c592b506d33a", + strip_prefix = "googletest-58d77fa8070e8cec2dc1ed015d66b454c8d78850", + urls = ["https://github.com/google/googletest/archive/58d77fa8070e8cec2dc1ed015d66b454c8d78850.zip"], +) + # Macops MOL* dependencies git_repository( @@ -69,14 +77,16 @@ git_repository( git_repository( name = "MOLCertificate", + commit = "288553b8ac75d7dd68159ef5b57652a506b8217c", # tag = "v2.1", remote = "https://github.com/google/macops-molcertificate.git", - tag = "v2.1", + shallow_since = "1561303966 -0400", ) git_repository( name = "MOLCodesignChecker", + commit = "7ef66f1df15997defd7651b0ea5d6d9ec65a5b4f", # tag = "v2.2", remote = "https://github.com/google/macops-molcodesignchecker.git", - tag = "v2.2", + shallow_since = "1561303990 -0400", ) git_repository( @@ -126,7 +136,9 @@ objc_library( visibility = ["//visibility:public"], ) """, - commit = "4a49ebb985bc16fae9489771aa35482ccbea14a3", # tag = v3.8.1 + commit = "afd2c6924e8a36cb872bc475248b978f743c6050", # tag = v3.9.1 + patch_args = ["-p1"], + patches = ["//external_patches/OCMock:503.patch"], remote = "https://github.com/erikdoe/ocmock", shallow_since = "1609349457 +0100", ) diff --git a/docs/deployment/configuration.md b/docs/deployment/configuration.md index db93a35fb..c76ab7c8c 100644 --- a/docs/deployment/configuration.md +++ b/docs/deployment/configuration.md @@ -29,7 +29,6 @@ also known as mobileconfig files, which are in an Apple-specific XML format. | BlockedPathRegex\* | String | A regex to block if the binary, certificate, or Team ID scopes did not allow/block an execution. Regexes are specified in ICU format. | | EnableBadSignatureProtection | Bool | Enable bad signature protection, defaults to NO. If this flag is set to YES, binaries with a bad signing chain will be blocked even in MONITOR mode, **unless** the binary is allowed by an explicit rule. | | EnablePageZeroProtection | Bool | Enable `__PAGEZERO` protection, defaults to YES. If this flag is set to YES, 32-bit binaries that are missing the `__PAGEZERO` segment will be blocked even in MONITOR mode, **unless** the binary is allowed by an explicit rule. | -| EnableSysxCache | Bool | Enables a secondary cache that ensures better performance when multiple EndpointSecurity system extensions are installed. Defaults to YES in 2021.8, defaults to NO in earlier versions. | | EnableSilentMode | Bool | If true, Santa will not post any GUI notifications. This can be a very confusing experience for users, use with caution. Defaults to NO. | | AboutText | String | The text to display when the user opens Santa.app. If unset, the default text will be displayed. | | MoreInfoURL | String | The URL to open when the user clicks "More Info..." when opening Santa.app. If unset, the button will not be displayed. | @@ -54,7 +53,7 @@ also known as mobileconfig files, which are in an Apple-specific XML format. | MachineOwnerKey | String | The key to use on MachineOwnerPlist. | | MachineIDPlist | String | The path to a plist that contains the MachineOwnerKey / value pair. | | MachineIDKey | String | The key to use on MachineIDPlist. | -| EventLogType | String | Defines how event logs are stored. Options are 1) syslog: Sent to ASL or ULS (if built with the 10.12 SDK or later). 2) filelog: Sent to a file on disk. Use EventLogPath to specify a path. 3) protobuf (BETA): Sent to file on disk using maildir format. 4) null: Don't output any event logs. Defaults to filelog. | +| EventLogType | String | Defines how event logs are stored. Options are 1) syslog: Sent to ASL or ULS (if built with the 10.12 SDK or later). 2) filelog: Sent to a file on disk. Use EventLogPath to specify a path. 3) protobuf (TEMPORARILY UNSUPPORTED): Sent to file on disk using maildir format. 4) null: Don't output any event logs. Defaults to filelog. | | EventLogPath | String | If EventLogType is set to filelog, EventLogPath will provide the path to save logs. Defaults to /var/db/santa/santa.log. If you change this value ensure you also update com.google.santa.newsyslog.conf with the new path. | | MailDirectory | String | If EventLogType is set to protobuf, MailDirectory will provide the the base directory used to save files according to the maildir format. Defaults to /var/db/santa/mail. | | MailDirectoryFileSizeThresholdKB | Integer | If EventLogType is set to protobuf, MailDirectoryFileSizeThresholdKB defines the per-file size limit for files stored in the mail directory. Events are buffered in memory until this threshold would be exceeded (or MailDirectoryEventMaxFlushTimeSec is exceeded). Defaults to 100. | diff --git a/docs/deployment/troubleshooting.md b/docs/deployment/troubleshooting.md index 1a2dcbd48..8ad042c44 100644 --- a/docs/deployment/troubleshooting.md +++ b/docs/deployment/troubleshooting.md @@ -35,14 +35,6 @@ expected when mass-deploying. - Additionally, confirm the system extension and TCC/PPPC profiles are present as mentioned under the ["MDM-Specific Client Configuration"](configuration.md) section of that page -- If there is no "Cache Info" section, the EnableSysxCache key may not -be present in the payload configuring Santa or the framework applying the key -locally may not have properly loaded it into the applicable domain. You can -confirm its presence or absence with the following command: - -```sh -sudo /usr/bin/profiles -L -o stdout-xml | grep -A1 EnableSysxCache -``` - The local preferences would dictate the sync server used as well, and the next sections help you confirm how many rules have in fact been recognized by diff --git a/external_patches/OCMock/503.patch b/external_patches/OCMock/503.patch new file mode 100644 index 000000000..b6a3d8f8e --- /dev/null +++ b/external_patches/OCMock/503.patch @@ -0,0 +1,115 @@ +From 84fcb4d370644332e12c2d3c4a38796d9c1e3fd3 Mon Sep 17 00:00:00 2001 +From: Dave MacLachlan +Date: Mon, 12 Jul 2021 17:00:45 -0700 +Subject: [PATCH] Fix up crash when calling object_getClass on non object. + +This fixes up a crash that was found in Chrome when running on an iOS 15 device. +It did not show up on the simulator. + +https://chromium-review.googlesource.com/c/chromium/src/+/3011651 + +Calling `object_getClass` on something that isn't an object is at best undefined +behavior. +--- + Source/OCMock/OCMArg.m | 6 +++--- + Source/OCMock/OCMPassByRefSetter.h | 3 +++ + Source/OCMock/OCMPassByRefSetter.m | 32 ++++++++++++++++++++++++++++++ + 3 files changed, 38 insertions(+), 3 deletions(-) + +diff --git a/Source/OCMock/OCMArg.m b/Source/OCMock/OCMArg.m +index 063181ac..9902d610 100644 +--- a/Source/OCMock/OCMArg.m ++++ b/Source/OCMock/OCMArg.m +@@ -35,7 +35,7 @@ + (void *)anyPointer + + + (id __autoreleasing *)anyObjectRef + { +- return (id *)0x01234567; ++ return (id *)[self anyPointer]; + } + + + (SEL)anySelector +@@ -127,9 +127,9 @@ + (id)resolveSpecialValues:(NSValue *)value + if(type[0] == '^') + { + void *pointer = [value pointerValue]; +- if(pointer == (void *)0x01234567) ++ if(pointer == [self anyPointer]) + return [OCMArg any]; +- if((pointer != NULL) && (object_getClass((id)pointer) == [OCMPassByRefSetter class])) ++ if((pointer != NULL) && [OCMPassByRefSetter ptrIsPassByRefSetter:pointer]) + return (id)pointer; + } + else if(type[0] == ':') +diff --git a/Source/OCMock/OCMPassByRefSetter.h b/Source/OCMock/OCMPassByRefSetter.h +index a02c67f5..f3d68ff4 100644 +--- a/Source/OCMock/OCMPassByRefSetter.h ++++ b/Source/OCMock/OCMPassByRefSetter.h +@@ -23,4 +23,7 @@ + + - (id)initWithValue:(id)value; + ++// Returns YES if ptr is actually a OCMPassByRefSetter +++ (BOOL)ptrIsPassByRefSetter:(void*)ptr; ++ + @end +diff --git a/Source/OCMock/OCMPassByRefSetter.m b/Source/OCMock/OCMPassByRefSetter.m +index b3e20755..8f30459b 100644 +--- a/Source/OCMock/OCMPassByRefSetter.m ++++ b/Source/OCMock/OCMPassByRefSetter.m +@@ -19,11 +19,30 @@ + + @implementation OCMPassByRefSetter + ++// Stores a reference to each of our OCMPassByRefSetters so that OCMArg can ++// check any given pointer to verify that it is an OCMPassByRefSetter. ++// The pointers are stored as naked pointers with no reference counts. ++// Note: all accesses protected by @synchronized(gPointerTable) ++static NSHashTable *gPointerTable = NULL; ++ +++ (void)initialize ++{ ++ if (self == [OCMPassByRefSetter class]) ++ { ++ gPointerTable = [[NSHashTable hashTableWithOptions:NSPointerFunctionsOpaqueMemory | NSPointerFunctionsOpaquePersonality] retain]; ++ } ++} ++ + - (id)initWithValue:(id)aValue + { + if((self = [super init])) + { + value = [aValue retain]; ++ @synchronized(gPointerTable) ++ { ++ // This will throw if somehow we manage to put two of the same pointer in the table. ++ NSHashInsertKnownAbsent(gPointerTable, self); ++ } + } + + return self; +@@ -32,6 +51,11 @@ - (id)initWithValue:(id)aValue + - (void)dealloc + { + [value release]; ++ @synchronized(gPointerTable) ++ { ++ NSAssert(NSHashGet(gPointerTable, self) != NULL, @"self should be in the hash table"); ++ NSHashRemove(gPointerTable, self); ++ } + [super dealloc]; + } + +@@ -47,4 +71,12 @@ - (void)handleArgument:(id)arg + } + } + +++ (BOOL)ptrIsPassByRefSetter:(void*)ptr ++{ ++ @synchronized(gPointerTable) ++ { ++ return NSHashGet(gPointerTable, ptr) != NULL; ++ } ++} ++ + @end diff --git a/external_patches/OCMock/BUILD b/external_patches/OCMock/BUILD new file mode 100644 index 000000000..d12c00360 --- /dev/null +++ b/external_patches/OCMock/BUILD @@ -0,0 +1,5 @@ +licenses(["notice"]) + +package( + default_visibility = ["//:santa_package_group"], +) diff --git a/external_patches/README.md b/external_patches/README.md new file mode 100644 index 000000000..1a31c8ad6 --- /dev/null +++ b/external_patches/README.md @@ -0,0 +1 @@ +This directory contains the patches that need to be applied to external dependencies brought in via the Bazel WORKSPACE file. diff --git a/generate_cov.sh b/generate_cov.sh index 26f713c33..eea29d0f8 100755 --- a/generate_cov.sh +++ b/generate_cov.sh @@ -11,6 +11,7 @@ function main() { --combined_report=lcov \ --spawn_strategy=standalone \ --test_env=LCOV_MERGER=/usr/bin/true \ + --test_output=all \ //:unit_tests # The generated file has most of the source files relative to bazel's @@ -23,5 +24,6 @@ function main() { sed -i '' '/SF:\/Applications.*/,/end_of_record/d' ${COV_FILE} sed -i '' '/SF:.*santa\/bazel-out.*/,/end_of_record/d' ${COV_FILE} + find bazel-out/ -name "*.dat" -type f | tar -czf "raw_coverages.tgz" -T - } main