From 28dd6cbaed75c5f656f8a975e81e9fd1462c367b Mon Sep 17 00:00:00 2001 From: Nick Gregory Date: Mon, 30 Oct 2023 17:45:37 -0400 Subject: [PATCH 1/4] Enable e2e testing on macOS 14 (#1209) * e2e for macos 14 * no shutdown * gh path * dismiss santa popup after bad binary * sleep for ui * re-enable start vm * re-enable poweroff * tabs * ratchet checkout actions in e2e --- .github/workflows/e2e.yml | 15 ++++---- Testing/integration/BUILD | 5 +++ Testing/integration/VM/bash_control.sh | 4 +-- Testing/integration/allow_sysex.scpt | 21 ++++++----- Testing/integration/dismiss_santa_popup.scpt | 10 ++++++ Testing/integration/install_profile.scpt | 37 ++++++++++++-------- Testing/integration/test_sync_changes.sh | 5 +++ 7 files changed, 64 insertions(+), 33 deletions(-) create mode 100644 Testing/integration/dismiss_santa_popup.scpt diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f8b7b1421..1e6968ac7 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,25 +1,28 @@ name: E2E -on: workflow_dispatch +on: + schedule: + - cron: '0 4 * * *' # Every day at 4:00 UTC (not to interfere with fuzzing) + workflow_dispatch: jobs: start_vm: runs-on: e2e-host steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3 - name: Start VM - run: python3 Testing/integration/actions/start_vm.py macOS_12.bundle.tar.gz + run: python3 Testing/integration/actions/start_vm.py macOS_14.bundle.tar.gz integration: runs-on: e2e-vm env: VM_PASSWORD: ${{ secrets.VM_PASSWORD }} steps: - - uses: actions/checkout@v3 - - name: Install configuration profile - run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3 - name: Add homebrew to PATH run: echo "/opt/homebrew/bin/" >> $GITHUB_PATH + - name: Install configuration profile + run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig - name: Build, install, and start moroz run: | bazel build @com_github_groob_moroz//cmd/moroz:moroz diff --git a/Testing/integration/BUILD b/Testing/integration/BUILD index 63a9ca765..e50b36a98 100644 --- a/Testing/integration/BUILD +++ b/Testing/integration/BUILD @@ -32,3 +32,8 @@ run_command( name = "allow_sysex", cmd = "osascript $${BUILD_WORKSPACE_DIRECTORY}/Testing/integration/allow_sysex.scpt", ) + +run_command( + name = "dismiss_santa_popup", + cmd = "osascript $${BUILD_WORKSPACE_DIRECTORY}/Testing/integration/dismiss_santa_popup.scpt", +) diff --git a/Testing/integration/VM/bash_control.sh b/Testing/integration/VM/bash_control.sh index c3acc670e..3d22042e4 100755 --- a/Testing/integration/VM/bash_control.sh +++ b/Testing/integration/VM/bash_control.sh @@ -3,5 +3,5 @@ # through applescript. # It's run as part of the template VM creation process. -osascript -e 'tell application "System Preferences" to activate' -osascript -e 'tell application "System Events" to tell process "System Preferences" to click menu item "Profiles" of menu 1 of menu bar item "View" of menu bar 1' +osascript -e 'tell application "System Settings" to activate' +osascript -e 'tell application "System Events" to tell process "System Settings" to click menu item "Profiles" of menu 1 of menu bar item "View" of menu bar 1' diff --git a/Testing/integration/allow_sysex.scpt b/Testing/integration/allow_sysex.scpt index 7b3c2fba9..15426297a 100644 --- a/Testing/integration/allow_sysex.scpt +++ b/Testing/integration/allow_sysex.scpt @@ -1,33 +1,32 @@ --- Allows the Santa system extension in System Preferences. +-- Allows the Santa system extension in System Settings. -- This is run inside test VMs. on run argv - if application "System Preferences" is running then - tell application "System Preferences" to quit + if application "System Settings" is running then + tell application "System Settings" to quit end if delay 2 tell application "System Events" tell process "UserNotificationCenter" - click button "Open Security Preferences" of window 1 + click button "Open System Settings" of window 1 end tell delay 3 - tell process "System Preferences" - click button "Click the lock to make changes." of window "Security & Privacy" - delay 1 - set value of text field "Password" of sheet 1 of window "Security & Privacy" to system attribute "VM_PASSWORD" - click button "Unlock" of sheet 1 of window "Security & Privacy" + tell process "System Settings" + -- Click the "Allow" under "system software ... was blocked from loading" + click button 1 of group 5 of scroll area 1 of group 1 of group 2 of splitter group 1 of group 1 of window 1 delay 2 - click button "Allow" of tab group 1 of window "Security & Privacy" + set value of text field 2 of sheet 1 of window 1 to system attribute "VM_PASSWORD" + click button 1 of sheet 1 of window 1 end tell end tell delay 2 - tell application "System Preferences" to quit + tell application "System Settings" to quit delay 2 end run diff --git a/Testing/integration/dismiss_santa_popup.scpt b/Testing/integration/dismiss_santa_popup.scpt new file mode 100644 index 000000000..5f0a7e9cc --- /dev/null +++ b/Testing/integration/dismiss_santa_popup.scpt @@ -0,0 +1,10 @@ +-- Dismiss the "blocked execution" popup from Santa. +-- This is run inside test VMs. + +on run argv + tell application "System Events" + tell process "Santa" + click button "Ignore" of window 1 + end tell + end tell +end run diff --git a/Testing/integration/install_profile.scpt b/Testing/integration/install_profile.scpt index 832b386e7..ed0d7867a 100644 --- a/Testing/integration/install_profile.scpt +++ b/Testing/integration/install_profile.scpt @@ -1,28 +1,37 @@ -- Installs the passed profile (.mobileconfig). -- This is run inside test VMs, primarily to configure Santa. +-- macOS 13+ only due to changes in system settings/preferences scripting. on run argv - do shell script "open " & item 1 of argv - - if application "System Preferences" is running then - tell application "System Preferences" to quit - end if - - delay 2 - - tell application "System Preferences" to activate + tell application "System Settings" to activate delay 2 tell application "System Events" - tell process "System Preferences" + tell process "System Settings" click menu item "Profiles" of menu 1 of menu bar item "View" of menu bar 1 delay 3 - click button "Install…" of scroll area 1 of window "Profiles" + -- Thanks SwiftUI. + -- Press the + + click button 1 of group 2 of scroll area 1 of group 1 of group 2 of splitter group 1 of group 1 of window 1 + delay 2 + -- Cmd+Shift+G to select file + keystroke "G" using {command down, shift down} + delay 2 + -- Type in the profile we want, and return to exit the "go to" sheet + keystroke item 1 of argv + keystroke return + delay 2 + -- Return to choose the file + keystroke return + delay 2 + -- Are you sure? Press continue + click button 2 of group 1 of sheet 1 of window 1 delay 2 - click button "Install" of sheet 1 of window "Profiles" + -- Press install + click button "Install" of sheet 1 of window 1 end tell - delay 2 + delay 5 tell process "SecurityAgent" set value of text field 2 of window 1 to system attribute "VM_PASSWORD" click button 2 of window 1 @@ -31,7 +40,7 @@ on run argv delay 5 - tell application "System Preferences" to quit + tell application "System Settings" to quit delay 2 end run diff --git a/Testing/integration/test_sync_changes.sh b/Testing/integration/test_sync_changes.sh index 179c1f088..2292a7b2e 100755 --- a/Testing/integration/test_sync_changes.sh +++ b/Testing/integration/test_sync_changes.sh @@ -25,6 +25,11 @@ if [[ "$(sudo santactl status --json | jq .daemon.block_usb)" != "false" ]]; the exit 1 fi +# Wait for the UI to have come up +sleep 5 + +bazel run //Testing/integration:dismiss_santa_popup + # Now change moroz to use the changed config, enabling USB blocking and removing the badbinary block rule killall moroz /tmp/moroz -configs="$GITHUB_WORKSPACE/Testing/integration/configs/moroz_changed/global.toml" -use-tls=false & From 275a8ed6071a0dfe9e47a07b8e073463105be817 Mon Sep 17 00:00:00 2001 From: Matt W <436037+mlw@users.noreply.github.com> Date: Tue, 31 Oct 2023 13:19:00 -0400 Subject: [PATCH 2/4] Support printing bundle info via santactl fileinfo command (#1213) --- Source/santactl/Commands/SNTCommandFileInfo.m | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/Source/santactl/Commands/SNTCommandFileInfo.m b/Source/santactl/Commands/SNTCommandFileInfo.m index f2940050d..d9cdf1f9d 100644 --- a/Source/santactl/Commands/SNTCommandFileInfo.m +++ b/Source/santactl/Commands/SNTCommandFileInfo.m @@ -22,6 +22,8 @@ #import "Source/common/SNTFileInfo.h" #import "Source/common/SNTLogging.h" #import "Source/common/SNTRule.h" +#import "Source/common/SNTStoredEvent.h" +#import "Source/common/SNTXPCBundleServiceInterface.h" #import "Source/common/SNTXPCControlInterface.h" #import "Source/santactl/SNTCommand.h" #import "Source/santactl/SNTCommandController.h" @@ -55,6 +57,13 @@ static NSString *const kSHA256 = @"SHA-256"; static NSString *const kSHA1 = @"SHA-1"; +// bundle info keys +static NSString *const kBundleInfo = @"Bundle Info"; +static NSString *const kBundlePath = @"Main Bundle Path"; +static NSString *const kBundleID = @"Main Bundle ID"; +static NSString *const kBundleHash = @"Bundle Hash"; +static NSString *const kBundleHashes = @"Bundle Hashes"; + // Message displayed when daemon communication fails static NSString *const kCommunicationErrorMsg = @"Could not communicate with daemon"; @@ -72,6 +81,7 @@ @interface SNTCommandFileInfo : SNTCommand // Properties set from commandline flags @property(nonatomic) BOOL recursive; @property(nonatomic) BOOL jsonOutput; +@property(nonatomic) BOOL bundleInfo; @property(nonatomic) NSNumber *certIndex; @property(nonatomic, copy) NSArray *outputKeyList; @property(nonatomic, copy) NSDictionary *outputFilters; @@ -156,6 +166,7 @@ + (NSString *)longHelpText { @"\n" @"Usage: santactl fileinfo [options] [file-paths]\n" @" --recursive (-r): Search directories recursively.\n" + @" Incompatible with --bundleinfo.\n" @" --json: Output in JSON format.\n" @" --key: Search and return this one piece of information.\n" @" You may specify multiple keys by repeating this flag.\n" @@ -167,12 +178,16 @@ + (NSString *)longHelpText { @" signing chain to show info only for that certificate.\n" @" 0 up to n for the leaf certificate up to the root\n" @" -1 down to -n-1 for the root certificate down to the leaf\n" + @" Incompatible with --bundleinfo." @"\n" @" --filter: Use predicates of the form 'key=regex' to filter out which files\n" @" are displayed. Valid keys are the same as for --key. Value is a\n" @" case-insensitive regular expression which must match anywhere in\n" @" the keyed property value for the file's info to be displayed.\n" @" You may specify multiple filters by repeating this flag.\n" + @" --bundleinfo: If the file is part of a bundle, will also display bundle\n" + @" hash information and hashes of all bundle executables.\n" + @" Incompatible with --recursive and --cert-index.\n" @"\n" @"Examples: santactl fileinfo --cert-index 1 --key SHA-256 --json /usr/bin/yes\n" @" santactl fileinfo --key SHA-256 --json /usr/bin/yes\n" @@ -682,6 +697,46 @@ - (void)printInfoForFile:(NSString *)path { if (outputDict[key]) continue; // ignore keys that we've already set due to a filter outputDict[key] = self.propertyMap[key](self, fileInfo); } + + if (self.bundleInfo) { + SNTStoredEvent *se = [[SNTStoredEvent alloc] init]; + se.fileBundlePath = fileInfo.bundlePath; + + MOLXPCConnection *bc = [SNTXPCBundleServiceInterface configuredConnection]; + [bc resume]; + + __block NSMutableDictionary *bundleInfo = [[NSMutableDictionary alloc] init]; + + bundleInfo[kBundlePath] = fileInfo.bundle.bundlePath; + bundleInfo[kBundleID] = fileInfo.bundle.bundleIdentifier; + + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + + [[bc remoteObjectProxy] + hashBundleBinariesForEvent:se + reply:^(NSString *hash, NSArray *events, + NSNumber *time) { + bundleInfo[kBundleHash] = hash; + + NSMutableArray *bundleHashes = [[NSMutableArray alloc] init]; + + for (SNTStoredEvent *event in events) { + [bundleHashes + addObject:@{kSHA256 : event.fileSHA256, kPath : event.filePath}]; + } + + bundleInfo[kBundleHashes] = bundleHashes; + [[bc remoteObjectProxy] spindown]; + dispatch_semaphore_signal(sema); + }]; + + int secondsToWait = 30; + if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, secondsToWait * NSEC_PER_SEC))) { + fprintf(stderr, "The bundle service did not finish collecting hashes within %d seconds\n", secondsToWait); + } + + outputDict[kBundleInfo] = bundleInfo; + } } // If there's nothing in the outputDict, then don't need to print anything. @@ -710,6 +765,11 @@ - (void)printInfoForFile:(NSString *)path { } } } + + if (self.bundleInfo) { + [output appendString:[self stringForBundleInfo:outputDict[kBundleInfo] key:kBundleInfo]]; + } + if (!singleKey) [output appendString:@"\n"]; } @@ -739,6 +799,9 @@ - (NSArray *)parseArguments:(NSArray *)arguments { if ([arg caseInsensitiveCompare:@"--json"] == NSOrderedSame) { self.jsonOutput = YES; } else if ([arg caseInsensitiveCompare:@"--cert-index"] == NSOrderedSame) { + if (self.bundleInfo) { + [self printErrorUsageAndExit:@"\n--cert-index is incompatible with --bundleinfo"]; + } i += 1; // advance to next argument and grab index if (i >= nargs || [arguments[i] hasPrefix:@"--"]) { [self printErrorUsageAndExit:@"\n--cert-index requires an argument"]; @@ -788,7 +851,17 @@ - (NSArray *)parseArguments:(NSArray *)arguments { filters[key] = regex; } else if ([arg caseInsensitiveCompare:@"--recursive"] == NSOrderedSame || [arg caseInsensitiveCompare:@"-r"] == NSOrderedSame) { + if (self.bundleInfo) { + [self printErrorUsageAndExit:@"\n--recursive is incompatible with --bundleinfo"]; + } self.recursive = YES; + } else if ([arg caseInsensitiveCompare:@"--bundleinfo"] == NSOrderedSame || + [arg caseInsensitiveCompare:@"-b"] == NSOrderedSame) { + if (self.recursive || self.certIndex) { + [self printErrorUsageAndExit: + @"\n--bundleinfo is incompatible with --recursive and --cert-index"]; + } + self.bundleInfo = YES; } else { [paths addObject:arg]; } @@ -868,6 +941,22 @@ - (NSString *)stringForSigningChain:(NSArray *)signingChain key:(NSString *)key return result.copy; } +- (NSString *)stringForBundleInfo:(NSDictionary *)bundleInfo key:(NSString *)key { + NSMutableString *result = [NSMutableString string]; + + [result appendFormat:@"%@:\n", key]; + + [result appendFormat:@" %-20s: %@\n", kBundlePath.UTF8String, bundleInfo[kBundlePath]]; + [result appendFormat:@" %-20s: %@\n", kBundleID.UTF8String, bundleInfo[kBundleID]]; + [result appendFormat:@" %-20s: %@\n", kBundleHash.UTF8String, bundleInfo[kBundleHash]]; + + for (NSDictionary *hashPath in bundleInfo[kBundleHashes]) { + [result appendFormat:@" %@ %@\n", hashPath[kSHA256], hashPath[kPath]]; + } + + return [result copy]; +} + - (NSString *)stringForCertificate:(NSDictionary *)cert withKeys:(NSArray *)keys index:(int)index { if (!cert) return @""; NSMutableString *result = [NSMutableString string]; From c5c6037085dd0d3cf4907461708d523b544467e9 Mon Sep 17 00:00:00 2001 From: Matt W <436037+mlw@users.noreply.github.com> Date: Tue, 31 Oct 2023 13:34:10 -0400 Subject: [PATCH 3/4] Unmount USB on start (#1211) * WIP Allow configuring Santa to unmount existing mass storage devices on startup * WIP fixup existing tests * Add unmount on startup tests --- Source/common/SNTCommonEnums.h | 6 + Source/common/SNTConfigurator.h | 14 ++ Source/common/SNTConfigurator.m | 14 ++ Source/santad/BUILD | 2 + .../EventProviders/DiskArbitrationTestUtil.h | 25 +++ .../EventProviders/DiskArbitrationTestUtil.mm | 72 +++++++ .../SNTEndpointSecurityDeviceManager.h | 16 +- .../SNTEndpointSecurityDeviceManager.mm | 188 +++++++++++++----- .../SNTEndpointSecurityDeviceManagerTest.mm | 66 +++++- Source/santad/Santad.mm | 7 +- docs/deployment/configuration.md | 1 + 11 files changed, 356 insertions(+), 55 deletions(-) diff --git a/Source/common/SNTCommonEnums.h b/Source/common/SNTCommonEnums.h index f571ed880..08da253ad 100644 --- a/Source/common/SNTCommonEnums.h +++ b/Source/common/SNTCommonEnums.h @@ -158,6 +158,12 @@ typedef NS_ENUM(NSInteger, SNTOverrideFileAccessAction) { SNTOverrideFileAccessActionDiable, }; +typedef NS_ENUM(NSInteger, SNTDeviceManagerStartupPreferences) { + SNTDeviceManagerStartupPreferencesNone, + SNTDeviceManagerStartupPreferencesUnmount, + SNTDeviceManagerStartupPreferencesForceUnmount, +}; + #ifdef __cplusplus enum class FileAccessPolicyDecision { kNoPolicy, diff --git a/Source/common/SNTConfigurator.h b/Source/common/SNTConfigurator.h index 2b838860f..30a5c43b7 100644 --- a/Source/common/SNTConfigurator.h +++ b/Source/common/SNTConfigurator.h @@ -454,6 +454,20 @@ /// @property(nonatomic) NSArray *remountUSBMode; +/// +/// If set, defines the action that should be taken on existing USB mounts when +/// Santa starts up. +/// +/// Supported values are: +/// * "Unmount": Unmount mass storage devices +/// * "ForceUnmount": Force unmount mass storage devices +/// +/// +/// Note: Existing mounts with mount flags that are a superset of RemountUSBMode +/// are unaffected and left mounted. +/// +@property(readonly, nonatomic) SNTDeviceManagerStartupPreferences onStartUSBOptions; + /// /// If set, will override the action taken when a file access rule violation /// occurs. This setting will apply across all rules in the file access policy. diff --git a/Source/common/SNTConfigurator.m b/Source/common/SNTConfigurator.m index 2a3e0753f..d7226ee30 100644 --- a/Source/common/SNTConfigurator.m +++ b/Source/common/SNTConfigurator.m @@ -121,6 +121,7 @@ @implementation SNTConfigurator static NSString *const kFailClosedKey = @"FailClosed"; static NSString *const kBlockUSBMountKey = @"BlockUSBMount"; static NSString *const kRemountUSBModeKey = @"RemountUSBMode"; +static NSString *const kOnStartUSBOptions = @"OnStartUSBOptions"; static NSString *const kEnableTransitiveRulesKey = @"EnableTransitiveRules"; static NSString *const kEnableTransitiveRulesKeyDeprecated = @"EnableTransitiveWhitelisting"; static NSString *const kAllowedPathRegexKey = @"AllowedPathRegex"; @@ -181,6 +182,7 @@ - (instancetype)init { kBlockedPathRegexKeyDeprecated : re, kBlockUSBMountKey : number, kRemountUSBModeKey : array, + kOnStartUSBOptions : string, kEnablePageZeroProtectionKey : number, kEnableBadSignatureProtectionKey : number, kEnableSilentModeKey : number, @@ -635,6 +637,18 @@ - (void)setRemountUSBMode:(NSArray *)args { return args; } +- (SNTDeviceManagerStartupPreferences)onStartUSBOptions { + NSString *action = [self.configState[kOnStartUSBOptions] lowercaseString]; + + if ([action isEqualToString:@"unmount"]) { + return SNTDeviceManagerStartupPreferencesUnmount; + } else if ([action isEqualToString:@"forceunmount"]) { + return SNTDeviceManagerStartupPreferencesForceUnmount; + } else { + return SNTDeviceManagerStartupPreferencesNone; + } +} + - (NSDictionary *)staticRules { return self.cachedStaticRules; } diff --git a/Source/santad/BUILD b/Source/santad/BUILD index 2ac33d62b..6ed04fcfd 100644 --- a/Source/santad/BUILD +++ b/Source/santad/BUILD @@ -395,6 +395,7 @@ objc_library( ":Metrics", ":SNTEndpointSecurityClient", ":SNTEndpointSecurityEventHandler", + "//Source/common:SNTCommonEnums", "//Source/common:SNTDeviceEvent", "//Source/common:SNTLogging", ], @@ -1317,6 +1318,7 @@ santa_unit_test( ":Metrics", ":MockEndpointSecurityAPI", ":SNTEndpointSecurityDeviceManager", + "//Source/common:SNTCommonEnums", "//Source/common:SNTConfigurator", "//Source/common:SNTDeviceEvent", "//Source/common:TestUtils", diff --git a/Source/santad/EventProviders/DiskArbitrationTestUtil.h b/Source/santad/EventProviders/DiskArbitrationTestUtil.h index a01430258..f7f960960 100644 --- a/Source/santad/EventProviders/DiskArbitrationTestUtil.h +++ b/Source/santad/EventProviders/DiskArbitrationTestUtil.h @@ -16,6 +16,9 @@ #include #include #include +#include +#include +#include NS_ASSUME_NONNULL_BEGIN @@ -27,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN @interface MockDADisk : NSObject @property(nonatomic) NSDictionary *diskDescription; @property(nonatomic, readwrite) NSString *name; +@property(nonatomic) BOOL wasUnmounted; @end typedef void (^MockDADiskAppearedCallback)(DADiskRef ref); @@ -49,6 +53,23 @@ typedef void (^MockDADiskAppearedCallback)(DADiskRef ref); + (instancetype _Nonnull)mockDiskArbitration; @end +@interface MockStatfs : NSObject +@property NSString *fromName; +@property NSString *onName; +@property NSNumber *flags; + +- (instancetype _Nonnull)initFrom:(NSString *)from on:(NSString *)on flags:(NSNumber *)flags; +@end + +@interface MockMounts : NSObject +@property(nonatomic) NSMutableDictionary *mounts; + +- (instancetype _Nonnull)init; +- (void)reset; +- (void)insert:(MockStatfs *)sfs; ++ (instancetype _Nonnull)mockMounts; +@end + // // All DiskArbitration functions used in SNTEndpointSecurityDeviceManager // and shimmed out accordingly. @@ -81,5 +102,9 @@ void DARegisterDiskDescriptionChangedCallback(DASessionRef session, void DASessionSetDispatchQueue(DASessionRef session, dispatch_queue_t __nullable queue); DASessionRef __nullable DASessionCreate(CFAllocatorRef __nullable allocator); +void DADiskUnmount(DADiskRef disk, DADiskUnmountOptions options, + DADiskUnmountCallback __nullable callback, void *__nullable context); +int getmntinfo_r_np(struct statfs *__nullable *__nullable mntbufp, int flags); + CF_EXTERN_C_END NS_ASSUME_NONNULL_END diff --git a/Source/santad/EventProviders/DiskArbitrationTestUtil.mm b/Source/santad/EventProviders/DiskArbitrationTestUtil.mm index 69af68f70..9387b047a 100644 --- a/Source/santad/EventProviders/DiskArbitrationTestUtil.mm +++ b/Source/santad/EventProviders/DiskArbitrationTestUtil.mm @@ -14,6 +14,9 @@ #import #include +#include +#include +#include #import "Source/santad/EventProviders/DiskArbitrationTestUtil.h" @@ -62,6 +65,47 @@ + (instancetype _Nonnull)mockDiskArbitration { @end +@implementation MockStatfs +- (instancetype _Nonnull)initFrom:(NSString *)from on:(NSString *)on flags:(NSNumber *)flags { + self = [super init]; + if (self) { + _fromName = from; + _onName = on; + _flags = flags; + } + return self; +} +@end + +@implementation MockMounts + +- (instancetype _Nonnull)init { + self = [super init]; + if (self) { + _mounts = [NSMutableDictionary dictionary]; + } + return self; +} + +- (void)reset { + [self.mounts removeAllObjects]; +} + +- (void)insert:(MockStatfs *)sfs { + self.mounts[sfs.fromName] = sfs; +} + ++ (instancetype _Nonnull)mockMounts { + static MockMounts *sharedMounts; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedMounts = [[MockMounts alloc] init]; + }); + return sharedMounts; +} + +@end + void DADiskMountWithArguments(DADiskRef _Nonnull disk, CFURLRef __nullable path, DADiskMountOptions options, DADiskMountCallback __nullable callback, void *__nullable context, @@ -117,4 +161,32 @@ DASessionRef __nullable DASessionCreate(CFAllocatorRef __nullable allocator) { return (__bridge DASessionRef)[MockDiskArbitration mockDiskArbitration]; }; +void DADiskUnmount(DADiskRef disk, DADiskUnmountOptions options, + DADiskUnmountCallback __nullable callback, void *__nullable context) { + MockDADisk *mockDisk = (__bridge MockDADisk *)disk; + mockDisk.wasUnmounted = YES; + + dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context; + dispatch_semaphore_signal(sema); +} + +int getmntinfo_r_np(struct statfs *__nullable *__nullable mntbufp, int flags) { + MockMounts *mockMounts = [MockMounts mockMounts]; + + struct statfs *sfs = (struct statfs *)calloc(mockMounts.mounts.count, sizeof(struct statfs)); + + __block NSUInteger i = 0; + [mockMounts.mounts + enumerateKeysAndObjectsUsingBlock:^(NSString *key, MockStatfs *mockSfs, BOOL *stop) { + strlcpy(sfs[i].f_mntfromname, mockSfs.fromName.UTF8String, sizeof(sfs[i].f_mntfromname)); + strlcpy(sfs[i].f_mntonname, mockSfs.onName.UTF8String, sizeof(sfs[i].f_mntonname)); + sfs[i].f_flags = [mockSfs.flags unsignedIntValue]; + i++; + }]; + + *mntbufp = sfs; + + return (int)mockMounts.mounts.count; +} + NS_ASSUME_NONNULL_END diff --git a/Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h index dee7d0ec4..41eab8196 100644 --- a/Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h +++ b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h @@ -15,6 +15,7 @@ #include #import +#import "Source/common/SNTCommonEnums.h" #import "Source/common/SNTDeviceEvent.h" #import "Source/santad/EventProviders/AuthResultCache.h" #include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h" @@ -39,11 +40,16 @@ typedef void (^SNTDeviceBlockCallback)(SNTDeviceEvent *event); @property(nonatomic, nullable) SNTDeviceBlockCallback deviceBlockCallback; - (instancetype) - initWithESAPI: - (std::shared_ptr)esApi - metrics:(std::shared_ptr)metrics - logger:(std::shared_ptr)logger - authResultCache:(std::shared_ptr)authResultCache; + initWithESAPI: + (std::shared_ptr) + esApi + metrics:(std::shared_ptr)metrics + logger:(std::shared_ptr)logger + authResultCache: + (std::shared_ptr)authResultCache + blockUSBMount:(BOOL)blockUSBMount + remountUSBMode:(nullable NSArray *)remountUSBMode + startupPreferences:(SNTDeviceManagerStartupPreferences)startupPrefs; @end diff --git a/Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.mm b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.mm index 5588b0177..1015ceb4c 100644 --- a/Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.mm +++ b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.mm @@ -24,6 +24,8 @@ #include #include #include +#include +#include #import "Source/common/SNTDeviceEvent.h" #import "Source/common/SNTLogging.h" @@ -45,6 +47,7 @@ - (void)logDiskDisappeared:(NSDictionary *)props; @property DASessionRef diskArbSession; @property(nonatomic, readonly) dispatch_queue_t diskQueue; +@property dispatch_semaphore_t startupUnmountSema; @end @@ -91,7 +94,7 @@ void diskDisappearedCallback(DADiskRef disk, void *context) { [dm logDiskDisappeared:props]; } -NSArray *maskToMountArgs(long remountOpts) { +NSArray *maskToMountArgs(uint32_t remountOpts) { NSMutableArray *args = [NSMutableArray array]; if (remountOpts & MNT_RDONLY) [args addObject:@"rdonly"]; if (remountOpts & MNT_NOEXEC) [args addObject:@"noexec"]; @@ -104,8 +107,8 @@ void diskDisappearedCallback(DADiskRef disk, void *context) { return args; } -long mountArgsToMask(NSArray *args) { - long flags = 0; +uint32_t mountArgsToMask(NSArray *args) { + uint32_t flags = 0; for (NSString *i in args) { NSString *arg = [i lowercaseString]; if ([arg isEqualToString:@"rdonly"]) @@ -130,6 +133,21 @@ long mountArgsToMask(NSArray *args) { return flags; } +void UnmountCallback(DADiskRef disk, DADissenterRef dissenter, void *context) { + if (dissenter) { + LOGW(@"Unable to unmount device: %@", CFBridgingRelease(DADissenterGetStatusString(dissenter))); + } else if (disk) { + NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk)); + LOGI(@"Unmounted device: Model: %@, Vendor: %@, Path: %@", + diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceModelKey], + diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceVendorKey], + diskInfo[(__bridge NSString *)kDADiskDescriptionVolumePathKey]); + } + + dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context; + dispatch_semaphore_signal(sema); +} + NS_ASSUME_NONNULL_BEGIN @implementation SNTEndpointSecurityDeviceManager { @@ -140,25 +158,134 @@ @implementation SNTEndpointSecurityDeviceManager { - (instancetype)initWithESAPI:(std::shared_ptr)esApi metrics:(std::shared_ptr)metrics logger:(std::shared_ptr)logger - authResultCache:(std::shared_ptr)authResultCache { + authResultCache:(std::shared_ptr)authResultCache + blockUSBMount:(BOOL)blockUSBMount + remountUSBMode:(nullable NSArray *)remountUSBMode + startupPreferences:(SNTDeviceManagerStartupPreferences)startupPrefs { self = [super initWithESAPI:std::move(esApi) metrics:std::move(metrics) processor:santa::santad::Processor::kDeviceManager]; if (self) { _logger = logger; _authResultCache = authResultCache; - _blockUSBMount = false; + _blockUSBMount = blockUSBMount; + _remountArgs = remountUSBMode; _diskQueue = dispatch_queue_create("com.google.santa.daemon.disk_queue", DISPATCH_QUEUE_SERIAL); _diskArbSession = DASessionCreate(NULL); DASessionSetDispatchQueue(_diskArbSession, _diskQueue); + [self performStartupTasks:startupPrefs]; + [self establishClientOrDie]; } return self; } +- (BOOL)shouldOperateOnDisk:(DADiskRef)disk { + NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk)); + + BOOL isInternal = [diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceInternalKey] boolValue]; + BOOL isRemovable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaRemovableKey] boolValue]; + BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue]; + NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey]; + BOOL isUSB = [protocol isEqualToString:@"USB"]; + BOOL isSecureDigital = [protocol isEqualToString:@"Secure Digital"]; + BOOL isVirtual = [protocol isEqualToString:@"Virtual Interface"]; + + NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey]; + + // TODO: check kind and protocol for banned things (e.g. MTP). + LOGD(@"SNTEndpointSecurityDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d " + @"isRemovable: %d isEjectable: %d", + protocol, kind, isInternal, isRemovable, isEjectable); + + // if the device is internal, or virtual *AND* is not an SD Card, + // then allow the mount. This is to ensure we block SD cards inserted into + // the internal reader of some Macs, whilst also ensuring we don't block + // the internal storage device. + if ((isInternal || isVirtual) && !isSecureDigital) { + return false; + } + + // We are okay with operations for devices that are non-removable as long as + // they are NOT a USB device, or an SD Card. + if (!isRemovable && !isEjectable && !isUSB && !isSecureDigital) { + return false; + } + + return true; +} + +- (BOOL)remountUSBModeContainsFlags:(uint32_t)flags { + uint32_t requiredFlags = mountArgsToMask(self.remountArgs); + + LOGD(@" Got mount flags: 0x%08x | %@", flags, maskToMountArgs(flags)); + LOGD(@"Want mount flags: 0x%08x | %@", mountArgsToMask(self.remountArgs), self.remountArgs); + + return (flags & requiredFlags) == requiredFlags; +} + +- (void)performStartupTasks:(SNTDeviceManagerStartupPreferences)startupPrefs { + if (!self.blockUSBMount || (startupPrefs != SNTDeviceManagerStartupPreferencesUnmount && + startupPrefs != SNTDeviceManagerStartupPreferencesForceUnmount)) { + return; + } + + struct statfs *mnts; + int numMounts = getmntinfo_r_np(&mnts, MNT_WAIT); + + if (numMounts == 0) { + LOGE(@"Failed to get mount info: %d: %s", errno, strerror(errno)); + return; + } + + self.startupUnmountSema = dispatch_semaphore_create(0); + int numUnmountAttempts = 0; + + for (int i = 0; i < numMounts; i++) { + struct statfs *sfs = &mnts[i]; + + DADiskRef disk = DADiskCreateFromBSDName(NULL, self.diskArbSession, sfs->f_mntfromname); + if (!disk) { + LOGW(@"Unable to create disk reference for device: '%s' -> '%s'", sfs->f_mntfromname, + sfs->f_mntonname); + continue; + } + + CFAutorelease(disk); + + if (![self shouldOperateOnDisk:disk]) { + continue; + } + + if (self.remountArgs != nil && [self remountUSBModeContainsFlags:sfs->f_flags]) { + LOGI(@"Allowing existing mount as flags contain RemountUSBMode. '%s' -> '%s'", + sfs->f_mntfromname, sfs->f_mntonname); + continue; + } + + DADiskUnmountOptions unmountOptions = kDADiskUnmountOptionDefault; + if (startupPrefs == SNTDeviceManagerStartupPreferencesForceUnmount) { + unmountOptions = kDADiskUnmountOptionForce; + } + + LOGI(@"Attempting to unmount device: '%s' mounted on '%s'", sfs->f_mntfromname, + sfs->f_mntonname); + + DADiskUnmount(disk, unmountOptions, UnmountCallback, (__bridge void *)self.startupUnmountSema); + numUnmountAttempts++; + } + + while (numUnmountAttempts-- > 0) { + if (dispatch_semaphore_wait(self.startupUnmountSema, + dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) { + LOGW(@"An unmount attempt took longer than expected. Device may still be mounted."); + } + } +} + - (void)logDiskAppeared:(NSDictionary *)props { self->_logger->LogDiskAppeared(props); } @@ -225,44 +352,16 @@ - (es_auth_result_t)handleAuthMount:(const Message &)m { exit(EXIT_FAILURE); } - long mountMode = eventStatFS->f_flags; + uint32_t mountMode = eventStatFS->f_flags; pid_t pid = audit_token_to_pid(m->process->audit_token); LOGD( - @"SNTEndpointSecurityDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu", + @"SNTEndpointSecurityDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %u", m->process->executable->path.data, pid, mountMode); DADiskRef disk = DADiskCreateFromBSDName(NULL, self.diskArbSession, eventStatFS->f_mntfromname); CFAutorelease(disk); - // TODO(tnek): Log all of the other attributes available in diskInfo into a structured log format. - NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk)); - BOOL isInternal = [diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceInternalKey] boolValue]; - BOOL isRemovable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaRemovableKey] boolValue]; - BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue]; - NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey]; - BOOL isUSB = [protocol isEqualToString:@"USB"]; - BOOL isSecureDigital = [protocol isEqualToString:@"Secure Digital"]; - BOOL isVirtual = [protocol isEqualToString:@"Virtual Interface"]; - - NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey]; - - // TODO: check kind and protocol for banned things (e.g. MTP). - LOGD(@"SNTEndpointSecurityDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d " - @"isRemovable: %d " - @"isEjectable: %d", - protocol, kind, isInternal, isRemovable, isEjectable); - - // if the device is internal, or virtual *AND* is not an SD Card, - // then allow the mount. This is to ensure we block SD cards inserted into - // the internal reader of some Macs, whilst also ensuring we don't block - // the internal storage device. - if ((isInternal || isVirtual) && !isSecureDigital) { - return ES_AUTH_RESULT_ALLOW; - } - - // We are okay with operations for devices that are non-removable as long as - // they are NOT a USB device, or an SD Card. - if (!isRemovable && !isEjectable && !isUSB && !isSecureDigital) { + if (![self shouldOperateOnDisk:disk]) { return ES_AUTH_RESULT_ALLOW; } @@ -274,18 +373,17 @@ - (es_auth_result_t)handleAuthMount:(const Message &)m { if (shouldRemount) { event.remountArgs = self.remountArgs; - long remountOpts = mountArgsToMask(self.remountArgs); - - LOGD(@"SNTEndpointSecurityDeviceManager: mountMode: %@", maskToMountArgs(mountMode)); - LOGD(@"SNTEndpointSecurityDeviceManager: remountOpts: %@", maskToMountArgs(remountOpts)); + uint32_t remountOpts = mountArgsToMask(self.remountArgs); - if ((mountMode & remountOpts) == remountOpts && m->event_type != ES_EVENT_TYPE_AUTH_REMOUNT) { - LOGD(@"SNTEndpointSecurityDeviceManager: Allowing as mount as flags match remountOpts"); + if ([self remountUSBModeContainsFlags:mountMode] && + m->event_type != ES_EVENT_TYPE_AUTH_REMOUNT) { + LOGD(@"Allowing mount as flags contain RemountUSBMode. '%s' -> '%s'", + eventStatFS->f_mntfromname, eventStatFS->f_mntonname); return ES_AUTH_RESULT_ALLOW; } - long newMode = mountMode | remountOpts; - LOGI(@"SNTEndpointSecurityDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)", + uint32_t newMode = mountMode | remountOpts; + LOGI(@"SNTEndpointSecurityDeviceManager: remounting device '%s'->'%s', flags (%u) -> (%u)", eventStatFS->f_mntfromname, eventStatFS->f_mntonname, mountMode, newMode); [self remount:disk mountMode:newMode]; } @@ -297,7 +395,7 @@ - (es_auth_result_t)handleAuthMount:(const Message &)m { return ES_AUTH_RESULT_DENY; } -- (void)remount:(DADiskRef)disk mountMode:(long)remountMask { +- (void)remount:(DADiskRef)disk mountMode:(uint32_t)remountMask { NSArray *args = maskToMountArgs(remountMask); CFStringRef *argv = (CFStringRef *)calloc(args.count + 1, sizeof(CFStringRef)); CFArrayGetValues((__bridge CFArrayRef)args, CFRangeMake(0, (CFIndex)args.count), diff --git a/Source/santad/EventProviders/SNTEndpointSecurityDeviceManagerTest.mm b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManagerTest.mm index 2c980e593..ea5e1400a 100644 --- a/Source/santad/EventProviders/SNTEndpointSecurityDeviceManagerTest.mm +++ b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManagerTest.mm @@ -26,6 +26,7 @@ #include #include +#import "Source/common/SNTCommonEnums.h" #import "Source/common/SNTConfigurator.h" #import "Source/common/SNTDeviceEvent.h" #include "Source/common/TestUtils.h" @@ -50,12 +51,16 @@ }; @interface SNTEndpointSecurityDeviceManager (Testing) +- (instancetype)init; - (void)logDiskAppeared:(NSDictionary *)props; +- (BOOL)shouldOperateOnDisk:(DADiskRef)disk; +- (void)performStartupTasks:(SNTDeviceManagerStartupPreferences)startupPrefs; @end @interface SNTEndpointSecurityDeviceManagerTest : XCTestCase @property id mockConfigurator; @property MockDiskArbitration *mockDA; +@property MockMounts *mockMounts; @end @implementation SNTEndpointSecurityDeviceManagerTest @@ -70,6 +75,9 @@ - (void)setUp { self.mockDA = [MockDiskArbitration mockDiskArbitration]; [self.mockDA reset]; + self.mockMounts = [MockMounts mockMounts]; + [self.mockMounts reset]; + fclose(stdout); } @@ -112,7 +120,10 @@ - (void)triggerTestMountEvent:(es_event_type_t)eventType [[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi metrics:nullptr logger:nullptr - authResultCache:nullptr]; + authResultCache:nullptr + blockUSBMount:false + remountUSBMode:nil + startupPreferences:SNTDeviceManagerStartupPreferencesNone]; setupDMCallback(deviceManager); @@ -324,7 +335,10 @@ - (void)testNotifyUnmountFlushesCache { [[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi metrics:nullptr logger:nullptr - authResultCache:mockAuthCache]; + authResultCache:mockAuthCache + blockUSBMount:YES + remountUSBMode:nil + startupPreferences:SNTDeviceManagerStartupPreferencesNone]; deviceManager.blockUSBMount = YES; @@ -340,6 +354,54 @@ - (void)testNotifyUnmountFlushesCache { XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get()); } +- (void)testPerformStartupTasks { + SNTEndpointSecurityDeviceManager *deviceManager = [[SNTEndpointSecurityDeviceManager alloc] init]; + + id partialDeviceManager = OCMPartialMock(deviceManager); + OCMStub([partialDeviceManager shouldOperateOnDisk:nil]).ignoringNonObjectArgs().andReturn(YES); + + deviceManager.blockUSBMount = YES; + deviceManager.remountArgs = @[ @"noexec", @"rdonly" ]; + + [self.mockMounts insert:[[MockStatfs alloc] initFrom:@"d1" on:@"v1" flags:@(0x0)]]; + [self.mockMounts insert:[[MockStatfs alloc] initFrom:@"d2" + on:@"v2" + flags:@(MNT_RDONLY | MNT_NOEXEC | MNT_JOURNALED)]]; + + MockDADisk *disk1 = [[MockDADisk alloc] init]; + MockDADisk *disk2 = [[MockDADisk alloc] init]; + + disk1.diskDescription = @{ + @"DAVolumePath" : @"v1", // f_mntonname, + @"DADevicePath" : @"v1", // f_mntonname, + @"DAMediaBSDName" : @"d1", // f_mntfromname, + }; + + disk2.diskDescription = @{ + @"DAVolumePath" : @"v2", // f_mntonname, + @"DADevicePath" : @"v2", // f_mntonname, + @"DAMediaBSDName" : @"d2", // f_mntfromname, + }; + + [self.mockDA insert:disk1 bsdName:disk1.diskDescription[@"DAMediaBSDName"]]; + [self.mockDA insert:disk2 bsdName:disk2.diskDescription[@"DAMediaBSDName"]]; + + [deviceManager performStartupTasks:SNTDeviceManagerStartupPreferencesUnmount]; + + XCTAssertTrue(disk1.wasUnmounted); + XCTAssertFalse(disk2.wasUnmounted); + + // Re-run tests with no remount args set, everything should be unmounted + disk1.wasUnmounted = NO; + disk2.wasUnmounted = NO; + deviceManager.remountArgs = nil; + + [deviceManager performStartupTasks:SNTDeviceManagerStartupPreferencesUnmount]; + + XCTAssertTrue(disk1.wasUnmounted); + XCTAssertTrue(disk2.wasUnmounted); +} + - (void)testEnable { // Ensure the client subscribes to expected event types std::set expectedEventSubs{ diff --git a/Source/santad/Santad.mm b/Source/santad/Santad.mm index 54508fd2d..fa24170db 100644 --- a/Source/santad/Santad.mm +++ b/Source/santad/Santad.mm @@ -103,10 +103,11 @@ void SantadMain(std::shared_ptr esapi, std::shared_ptr Date: Tue, 31 Oct 2023 14:16:28 -0400 Subject: [PATCH 4/4] Additional build deps (#1215) * Update build deps * lint --- Source/santactl/BUILD | 2 ++ Source/santactl/Commands/SNTCommandFileInfo.m | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/santactl/BUILD b/Source/santactl/BUILD index 8d1467e33..e41ccc68a 100644 --- a/Source/santactl/BUILD +++ b/Source/santactl/BUILD @@ -115,6 +115,8 @@ santa_unit_test( "//Source/common:SNTFileInfo", "//Source/common:SNTLogging", "//Source/common:SNTRule", + "//Source/common:SNTStoredEvent", + "//Source/common:SNTXPCBundleServiceInterface", "//Source/common:SNTXPCControlInterface", "@MOLCertificate", "@MOLCodesignChecker", diff --git a/Source/santactl/Commands/SNTCommandFileInfo.m b/Source/santactl/Commands/SNTCommandFileInfo.m index d9cdf1f9d..9888752b7 100644 --- a/Source/santactl/Commands/SNTCommandFileInfo.m +++ b/Source/santactl/Commands/SNTCommandFileInfo.m @@ -731,8 +731,10 @@ - (void)printInfoForFile:(NSString *)path { }]; int secondsToWait = 30; - if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, secondsToWait * NSEC_PER_SEC))) { - fprintf(stderr, "The bundle service did not finish collecting hashes within %d seconds\n", secondsToWait); + if (dispatch_semaphore_wait(sema, + dispatch_time(DISPATCH_TIME_NOW, secondsToWait * NSEC_PER_SEC))) { + fprintf(stderr, "The bundle service did not finish collecting hashes within %d seconds\n", + secondsToWait); } outputDict[kBundleInfo] = bundleInfo;