From 70474aba3e3dafa995f1d1f25ef396b29021e3a9 Mon Sep 17 00:00:00 2001 From: Matt W <436037+mlw@users.noreply.github.com> Date: Wed, 24 Jan 2024 09:26:20 -0500 Subject: [PATCH] Sync clean all (#1275) * WIP Clean syncs now leave non-transitive rules by default * WIP Get existing tests compiling and passing * Remove clean all sync server key. Basic tests. * Add SNTConfiguratorTest, test deprecated key migration * Revert changes to santactl status output * Add new preflight response sync type key, lots of tests * Rework configurator flow a bit so calls cannot be made out of order * Comment clean sync states. Test all permutations. * Update docs for new sync keys * Doc updates as requested in PR --- Fuzzing/santad/src/databaseRuleAddRules.mm | 3 +- Source/common/BUILD | 11 ++ Source/common/SNTCommonEnums.h | 12 ++ Source/common/SNTConfigurator.h | 4 +- Source/common/SNTConfigurator.m | 90 ++++++++-- Source/common/SNTConfiguratorTest.m | 102 +++++++++++ Source/common/SNTRuleTest.m | 3 +- Source/common/SNTSyncConstants.h | 3 +- Source/common/SNTSyncConstants.m | 3 +- Source/common/SNTXPCControlInterface.h | 4 +- Source/common/SNTXPCControlInterface.m | 2 +- Source/common/SNTXPCSyncServiceInterface.h | 2 +- .../SNTXPCUnprivilegedControlInterface.h | 2 +- Source/santactl/Commands/SNTCommandRule.m | 4 +- Source/santactl/Commands/SNTCommandStatus.m | 4 +- Source/santactl/Commands/SNTCommandSync.m | 17 +- Source/santad/DataLayer/SNTRuleTable.h | 4 +- Source/santad/DataLayer/SNTRuleTable.m | 8 +- Source/santad/DataLayer/SNTRuleTableTest.m | 66 ++++++-- .../SNTEndpointSecurityAuthorizerTest.mm | 2 +- .../SNTEndpointSecurityDeviceManagerTest.mm | 16 +- ...ndpointSecurityFileAccessAuthorizerTest.mm | 2 +- Source/santad/Logs/EndpointSecurity/Logger.mm | 2 +- .../Logs/EndpointSecurity/LoggerTest.mm | 2 +- .../Serializers/ProtobufTest.mm | 2 +- Source/santad/SNTCompilerController.mm | 2 +- Source/santad/SNTDaemonControlController.mm | 15 +- Source/santad/SNTExecutionControllerTest.mm | 1 - Source/santasyncservice/SNTSyncEventUpload.m | 4 +- Source/santasyncservice/SNTSyncManager.h | 3 +- Source/santasyncservice/SNTSyncManager.m | 14 +- Source/santasyncservice/SNTSyncPostflight.m | 10 +- Source/santasyncservice/SNTSyncPreflight.m | 90 +++++++++- Source/santasyncservice/SNTSyncRuleDownload.m | 22 ++- Source/santasyncservice/SNTSyncService.m | 22 +-- Source/santasyncservice/SNTSyncStage.m | 2 +- Source/santasyncservice/SNTSyncState.h | 2 +- Source/santasyncservice/SNTSyncTest.m | 159 ++++++++++++++++-- docs/development/sync-protocol.md | 68 +++++--- 39 files changed, 629 insertions(+), 155 deletions(-) create mode 100644 Source/common/SNTConfiguratorTest.m diff --git a/Fuzzing/santad/src/databaseRuleAddRules.mm b/Fuzzing/santad/src/databaseRuleAddRules.mm index a4eae9224..0b35d465d 100644 --- a/Fuzzing/santad/src/databaseRuleAddRules.mm +++ b/Fuzzing/santad/src/databaseRuleAddRules.mm @@ -18,6 +18,7 @@ #import #import "SNTCommandController.h" +#import "SNTCommonEnums.h" #import "SNTRule.h" #import "SNTXPCControlInterface.h" @@ -58,7 +59,7 @@ [daemonConn resume]; [[daemonConn remoteObjectProxy] databaseRuleAddRules:@[ newRule ] - cleanSlate:NO + ruleCleanup:SNTRuleCleanupNone reply:^(NSError *error) { if (!error) { if (newRule.state == SNTRuleStateRemove) { diff --git a/Source/common/BUILD b/Source/common/BUILD index 1d2dd42d9..4df6c79f0 100644 --- a/Source/common/BUILD +++ b/Source/common/BUILD @@ -477,12 +477,23 @@ santa_unit_test( ], ) +santa_unit_test( + name = "SNTConfiguratorTest", + srcs = ["SNTConfiguratorTest.m"], + deps = [ + ":SNTCommonEnums", + ":SNTConfigurator", + "@OCMock", + ], +) + test_suite( name = "unit_tests", tests = [ ":PrefixTreeTest", ":SNTBlockMessageTest", ":SNTCachedDecisionTest", + ":SNTConfiguratorTest", ":SNTFileInfoTest", ":SNTKVOManagerTest", ":SNTMetricSetTest", diff --git a/Source/common/SNTCommonEnums.h b/Source/common/SNTCommonEnums.h index 2ee829134..b50e80e55 100644 --- a/Source/common/SNTCommonEnums.h +++ b/Source/common/SNTCommonEnums.h @@ -166,6 +166,18 @@ typedef NS_ENUM(NSInteger, SNTDeviceManagerStartupPreferences) { SNTDeviceManagerStartupPreferencesForceRemount, }; +typedef NS_ENUM(NSInteger, SNTSyncType) { + SNTSyncTypeNormal, + SNTSyncTypeClean, + SNTSyncTypeCleanAll, +}; + +typedef NS_ENUM(NSInteger, SNTRuleCleanup) { + SNTRuleCleanupNone, + SNTRuleCleanupAll, + SNTRuleCleanupNonTransitive, +}; + #ifdef __cplusplus enum class FileAccessPolicyDecision { kNoPolicy, diff --git a/Source/common/SNTConfigurator.h b/Source/common/SNTConfigurator.h index 1070c9475..767fad0b0 100644 --- a/Source/common/SNTConfigurator.h +++ b/Source/common/SNTConfigurator.h @@ -437,9 +437,9 @@ @property(nonatomic) NSDate *ruleSyncLastSuccess; /// -/// If YES a clean sync is required. +/// Type of sync required (e.g. normal, clean, etc.). /// -@property(nonatomic) BOOL syncCleanRequired; +@property(nonatomic) SNTSyncType syncTypeRequired; #pragma mark - USB Settings diff --git a/Source/common/SNTConfigurator.m b/Source/common/SNTConfigurator.m index 9dcce082c..404a3f652 100644 --- a/Source/common/SNTConfigurator.m +++ b/Source/common/SNTConfigurator.m @@ -53,6 +53,9 @@ @interface SNTConfigurator () /// Holds the last processed hash of the static rules list. @property(atomic) NSDictionary *cachedStaticRules; +@property(readonly, nonatomic) NSString *syncStateFilePath; +@property(nonatomic, copy) BOOL (^syncStateAccessAuthorizerBlock)(); + @end @implementation SNTConfigurator @@ -160,9 +163,19 @@ @implementation SNTConfigurator // The keys managed by a sync server. static NSString *const kFullSyncLastSuccess = @"FullSyncLastSuccess"; static NSString *const kRuleSyncLastSuccess = @"RuleSyncLastSuccess"; -static NSString *const kSyncCleanRequired = @"SyncCleanRequired"; +static NSString *const kSyncCleanRequiredDeprecated = @"SyncCleanRequired"; +static NSString *const kSyncTypeRequired = @"SyncTypeRequired"; - (instancetype)init { + return [self initWithSyncStateFile:kSyncStateFilePath + syncStateAccessAuthorizer:^BOOL() { + // Only access the sync state if a sync server is configured and running as root + return self.syncBaseURL != nil && geteuid() == 0; + }]; +} + +- (instancetype)initWithSyncStateFile:(NSString *)syncStateFilePath + syncStateAccessAuthorizer:(BOOL (^)(void))syncStateAccessAuthorizer { self = [super init]; if (self) { Class number = [NSNumber class]; @@ -184,7 +197,8 @@ - (instancetype)init { kRemountUSBModeKey : array, kFullSyncLastSuccess : date, kRuleSyncLastSuccess : date, - kSyncCleanRequired : number, + kSyncCleanRequiredDeprecated : number, + kSyncTypeRequired : number, kEnableAllEventUploadKey : number, kOverrideFileAccessActionKey : string, }; @@ -262,11 +276,21 @@ - (instancetype)init { kEntitlementsPrefixFilterKey : array, kEntitlementsTeamIDFilterKey : array, }; + + _syncStateFilePath = syncStateFilePath; + _syncStateAccessAuthorizerBlock = syncStateAccessAuthorizer; + _defaults = [NSUserDefaults standardUserDefaults]; [_defaults addSuiteNamed:@"com.google.santa"]; _configState = [self readForcedConfig]; [self cacheStaticRules]; + _syncState = [self readSyncStateFromDisk] ?: [NSMutableDictionary dictionary]; + if ([self migrateDeprecatedSyncStateKeys]) { + // Save the updated sync state if any keys were migrated. + [self saveSyncStateToDisk]; + } + _debugFlag = [[NSProcessInfo processInfo].arguments containsObject:@"--debug"]; [self startWatchingDefaults]; } @@ -432,7 +456,7 @@ + (NSSet *)keyPathsForValuesAffectingRuleSyncLastSuccess { return [self syncStateSet]; } -+ (NSSet *)keyPathsForValuesAffectingSyncCleanRequired { ++ (NSSet *)keyPathsForValuesAffectingSyncTypeRequired { return [self syncStateSet]; } @@ -823,12 +847,12 @@ - (void)setRuleSyncLastSuccess:(NSDate *)ruleSyncLastSuccess { [self updateSyncStateForKey:kRuleSyncLastSuccess value:ruleSyncLastSuccess]; } -- (BOOL)syncCleanRequired { - return [self.syncState[kSyncCleanRequired] boolValue]; +- (SNTSyncType)syncTypeRequired { + return (SNTSyncType)[self.syncState[kSyncTypeRequired] integerValue]; } -- (void)setSyncCleanRequired:(BOOL)syncCleanRequired { - [self updateSyncStateForKey:kSyncCleanRequired value:@(syncCleanRequired)]; +- (void)setSyncTypeRequired:(SNTSyncType)syncTypeRequired { + [self updateSyncStateForKey:kSyncTypeRequired value:@(syncTypeRequired)]; } - (NSString *)machineOwner { @@ -1100,12 +1124,12 @@ - (void)updateSyncStateForKey:(NSString *)key value:(id)value { /// Read the saved syncState. /// - (NSMutableDictionary *)readSyncStateFromDisk { - // Only read the sync state if a sync server is configured. - if (!self.syncBaseURL) return nil; - // Only santad should read this file. - if (geteuid() != 0) return nil; + if (!self.syncStateAccessAuthorizerBlock()) { + return nil; + } + NSMutableDictionary *syncState = - [NSMutableDictionary dictionaryWithContentsOfFile:kSyncStateFilePath]; + [NSMutableDictionary dictionaryWithContentsOfFile:self.syncStateFilePath]; for (NSString *key in syncState.allKeys) { if (self.syncServerKeyTypes[key] == [NSRegularExpression class]) { NSString *pattern = [syncState[key] isKindOfClass:[NSString class]] ? syncState[key] : nil; @@ -1115,24 +1139,54 @@ - (NSMutableDictionary *)readSyncStateFromDisk { continue; } } + return syncState; } +/// +/// Migrate any deprecated sync state keys/values to alternative keys/values. +/// +/// Returns YES if any keys were migrated. Otherwise NO. +/// +- (BOOL)migrateDeprecatedSyncStateKeys { + // Currently only one key to migrate + if (!self.syncState[kSyncCleanRequiredDeprecated]) { + return NO; + } + + NSMutableDictionary *syncState = self.syncState.mutableCopy; + + // If the kSyncTypeRequired key exists, its current value will take precedence. + // Otherwise, migrate the old value to be compatible with the new logic. + if (!self.syncState[kSyncTypeRequired]) { + syncState[kSyncTypeRequired] = [self.syncState[kSyncCleanRequiredDeprecated] boolValue] + ? @(SNTSyncTypeClean) + : @(SNTSyncTypeNormal); + } + + // Delete the deprecated key + syncState[kSyncCleanRequiredDeprecated] = nil; + + self.syncState = syncState; + + return YES; +} + /// /// Saves the current effective syncState to disk. /// - (void)saveSyncStateToDisk { - // Only save the sync state if a sync server is configured. - if (!self.syncBaseURL) return; - // Only santad should write to this file. - if (geteuid() != 0) return; + if (!self.syncStateAccessAuthorizerBlock()) { + return; + } + // Either remove NSMutableDictionary *syncState = self.syncState.mutableCopy; syncState[kAllowedPathRegexKey] = [syncState[kAllowedPathRegexKey] pattern]; syncState[kBlockedPathRegexKey] = [syncState[kBlockedPathRegexKey] pattern]; - [syncState writeToFile:kSyncStateFilePath atomically:YES]; + [syncState writeToFile:self.syncStateFilePath atomically:YES]; [[NSFileManager defaultManager] setAttributes:@{NSFilePosixPermissions : @0600} - ofItemAtPath:kSyncStateFilePath + ofItemAtPath:self.syncStateFilePath error:NULL]; } diff --git a/Source/common/SNTConfiguratorTest.m b/Source/common/SNTConfiguratorTest.m new file mode 100644 index 000000000..4f4e79587 --- /dev/null +++ b/Source/common/SNTConfiguratorTest.m @@ -0,0 +1,102 @@ +/// Copyright 2024 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 + +#import "Source/common/SNTCommonEnums.h" +#import "Source/common/SNTConfigurator.h" + +@interface SNTConfigurator (Testing) +- (instancetype)initWithSyncStateFile:(NSString *)syncStateFilePath + syncStateAccessAuthorizer:(BOOL (^)(void))syncStateAccessAuthorizer; + +@property NSDictionary *syncState; +@end + +@interface SNTConfiguratorTest : XCTestCase +@property NSFileManager *fileMgr; +@property NSString *testDir; +@end + +@implementation SNTConfiguratorTest + +- (void)setUp { + self.fileMgr = [NSFileManager defaultManager]; + self.testDir = + [NSString stringWithFormat:@"%@santa-configurator-%d", NSTemporaryDirectory(), getpid()]; + + XCTAssertTrue([self.fileMgr createDirectoryAtPath:self.testDir + withIntermediateDirectories:YES + attributes:nil + error:nil]); +} + +- (void)tearDown { + XCTAssertTrue([self.fileMgr removeItemAtPath:self.testDir error:nil]); +} + +- (void)runMigrationTestsWithSyncState:(NSDictionary *)syncStatePlist + verifier:(void (^)(SNTConfigurator *))verifierBlock { + NSString *syncStatePlistPath = + [NSString stringWithFormat:@"%@/test-sync-state.plist", self.testDir]; + + XCTAssertTrue([syncStatePlist writeToFile:syncStatePlistPath atomically:YES]); + + SNTConfigurator *cfg = [[SNTConfigurator alloc] initWithSyncStateFile:syncStatePlistPath + syncStateAccessAuthorizer:^{ + // Allow all access to the test plist + return YES; + }]; + + NSLog(@"sync state: %@", cfg.syncState); + + verifierBlock(cfg); + + XCTAssertTrue([self.fileMgr removeItemAtPath:syncStatePlistPath error:nil]); +} + +- (void)testInitMigratesSyncStateKeys { + // SyncCleanRequired = YES + [self runMigrationTestsWithSyncState:@{@"SyncCleanRequired" : [NSNumber numberWithBool:YES]} + verifier:^(SNTConfigurator *cfg) { + XCTAssertEqual(cfg.syncState.count, 1); + XCTAssertNil(cfg.syncState[@"SyncCleanRequired"]); + XCTAssertNotNil(cfg.syncState[@"SyncTypeRequired"]); + XCTAssertEqual([cfg.syncState[@"SyncTypeRequired"] integerValue], + SNTSyncTypeClean); + XCTAssertEqual(cfg.syncState.count, 1); + }]; + + // SyncCleanRequired = NO + [self runMigrationTestsWithSyncState:@{@"SyncCleanRequired" : [NSNumber numberWithBool:NO]} + verifier:^(SNTConfigurator *cfg) { + XCTAssertEqual(cfg.syncState.count, 1); + XCTAssertNil(cfg.syncState[@"SyncCleanRequired"]); + XCTAssertNotNil(cfg.syncState[@"SyncTypeRequired"]); + XCTAssertEqual([cfg.syncState[@"SyncTypeRequired"] integerValue], + SNTSyncTypeNormal); + XCTAssertEqual(cfg.syncState.count, 1); + }]; + + // Empty state + [self runMigrationTestsWithSyncState:@{} + verifier:^(SNTConfigurator *cfg) { + XCTAssertEqual(cfg.syncState.count, 0); + XCTAssertNil(cfg.syncState[@"SyncCleanRequired"]); + XCTAssertNil(cfg.syncState[@"SyncTypeRequired"]); + }]; +} + +@end diff --git a/Source/common/SNTRuleTest.m b/Source/common/SNTRuleTest.m index 28742122d..f46a15192 100644 --- a/Source/common/SNTRuleTest.m +++ b/Source/common/SNTRuleTest.m @@ -13,7 +13,8 @@ /// limitations under the License. #import -#include "Source/common/SNTCommonEnums.h" + +#import "Source/common/SNTCommonEnums.h" #import "Source/common/SNTSyncConstants.h" #import "Source/common/SNTRule.h" diff --git a/Source/common/SNTSyncConstants.h b/Source/common/SNTSyncConstants.h index 4e69f7039..9c8e2a415 100644 --- a/Source/common/SNTSyncConstants.h +++ b/Source/common/SNTSyncConstants.h @@ -32,7 +32,8 @@ extern NSString *const kClientModeMonitor; extern NSString *const kClientModeLockdown; extern NSString *const kBlockUSBMount; extern NSString *const kRemountUSBMode; -extern NSString *const kCleanSync; +extern NSString *const kCleanSyncDeprecated; +extern NSString *const kSyncType; extern NSString *const kAllowedPathRegex; extern NSString *const kAllowedPathRegexDeprecated; extern NSString *const kBlockedPathRegex; diff --git a/Source/common/SNTSyncConstants.m b/Source/common/SNTSyncConstants.m index f22cb9ae7..6b2f3c06a 100644 --- a/Source/common/SNTSyncConstants.m +++ b/Source/common/SNTSyncConstants.m @@ -32,7 +32,8 @@ NSString *const kRemountUSBMode = @"remount_usb_mode"; NSString *const kClientModeMonitor = @"MONITOR"; NSString *const kClientModeLockdown = @"LOCKDOWN"; -NSString *const kCleanSync = @"clean_sync"; +NSString *const kCleanSyncDeprecated = @"clean_sync"; +NSString *const kSyncType = @"sync_type"; NSString *const kAllowedPathRegex = @"allowed_path_regex"; NSString *const kAllowedPathRegexDeprecated = @"whitelist_regex"; NSString *const kBlockedPathRegex = @"blocked_path_regex"; diff --git a/Source/common/SNTXPCControlInterface.h b/Source/common/SNTXPCControlInterface.h index d3a5cc03d..257928fa3 100644 --- a/Source/common/SNTXPCControlInterface.h +++ b/Source/common/SNTXPCControlInterface.h @@ -28,7 +28,7 @@ /// Database ops /// - (void)databaseRuleAddRules:(NSArray *)rules - cleanSlate:(BOOL)cleanSlate + ruleCleanup:(SNTRuleCleanup)cleanupType reply:(void (^)(NSError *error))reply; - (void)databaseEventsPending:(void (^)(NSArray *events))reply; - (void)databaseRemoveEventsWithIDs:(NSArray *)ids; @@ -45,7 +45,7 @@ - (void)setClientMode:(SNTClientMode)mode reply:(void (^)(void))reply; - (void)setFullSyncLastSuccess:(NSDate *)date reply:(void (^)(void))reply; - (void)setRuleSyncLastSuccess:(NSDate *)date reply:(void (^)(void))reply; -- (void)setSyncCleanRequired:(BOOL)cleanReqd reply:(void (^)(void))reply; +- (void)setSyncTypeRequired:(SNTSyncType)syncType reply:(void (^)(void))reply; - (void)setAllowedPathRegex:(NSString *)pattern reply:(void (^)(void))reply; - (void)setBlockedPathRegex:(NSString *)pattern reply:(void (^)(void))reply; - (void)setBlockUSBMount:(BOOL)enabled reply:(void (^)(void))reply; diff --git a/Source/common/SNTXPCControlInterface.m b/Source/common/SNTXPCControlInterface.m index eded7fc52..07f4c7a55 100644 --- a/Source/common/SNTXPCControlInterface.m +++ b/Source/common/SNTXPCControlInterface.m @@ -50,7 +50,7 @@ + (void)initializeControlInterface:(NSXPCInterface *)r { ofReply:YES]; [r setClasses:[NSSet setWithObjects:[NSArray class], [SNTRule class], nil] - forSelector:@selector(databaseRuleAddRules:cleanSlate:reply:) + forSelector:@selector(databaseRuleAddRules:ruleCleanup:reply:) argumentIndex:0 ofReply:NO]; diff --git a/Source/common/SNTXPCSyncServiceInterface.h b/Source/common/SNTXPCSyncServiceInterface.h index 568c5b3fb..2803ff9a3 100644 --- a/Source/common/SNTXPCSyncServiceInterface.h +++ b/Source/common/SNTXPCSyncServiceInterface.h @@ -44,7 +44,7 @@ // Pass true to isClean to perform a clean sync, defaults to false. // - (void)syncWithLogListener:(NSXPCListenerEndpoint *)logListener - isClean:(BOOL)cleanSync + syncType:(SNTSyncType)syncType reply:(void (^)(SNTSyncStatusType))reply; // Spindown the syncservice. The syncservice will not automatically start back up. diff --git a/Source/common/SNTXPCUnprivilegedControlInterface.h b/Source/common/SNTXPCUnprivilegedControlInterface.h index 0bd69b5cc..da8c8f593 100644 --- a/Source/common/SNTXPCUnprivilegedControlInterface.h +++ b/Source/common/SNTXPCUnprivilegedControlInterface.h @@ -68,7 +68,7 @@ - (void)clientMode:(void (^)(SNTClientMode))reply; - (void)fullSyncLastSuccess:(void (^)(NSDate *))reply; - (void)ruleSyncLastSuccess:(void (^)(NSDate *))reply; -- (void)syncCleanRequired:(void (^)(BOOL))reply; +- (void)syncTypeRequired:(void (^)(SNTSyncType))reply; - (void)enableBundles:(void (^)(BOOL))reply; - (void)enableTransitiveRules:(void (^)(BOOL))reply; - (void)blockUSBMount:(void (^)(BOOL))reply; diff --git a/Source/santactl/Commands/SNTCommandRule.m b/Source/santactl/Commands/SNTCommandRule.m index 16c3d79a3..32a2f8b4d 100644 --- a/Source/santactl/Commands/SNTCommandRule.m +++ b/Source/santactl/Commands/SNTCommandRule.m @@ -251,7 +251,7 @@ - (void)runWithArguments:(NSArray *)arguments { [[self.daemonConn remoteObjectProxy] databaseRuleAddRules:@[ newRule ] - cleanSlate:NO + ruleCleanup:SNTRuleCleanupNone reply:^(NSError *error) { if (error) { printf("Failed to modify rules: %s", @@ -421,7 +421,7 @@ - (void)importJSONFile:(NSString *)jsonFilePath { [[self.daemonConn remoteObjectProxy] databaseRuleAddRules:parsedRules - cleanSlate:NO + ruleCleanup:SNTRuleCleanupNone reply:^(NSError *error) { if (error) { printf("Failed to modify rules: %s", diff --git a/Source/santactl/Commands/SNTCommandStatus.m b/Source/santactl/Commands/SNTCommandStatus.m index 9044ba0d6..58754d1e4 100644 --- a/Source/santactl/Commands/SNTCommandStatus.m +++ b/Source/santactl/Commands/SNTCommandStatus.m @@ -129,8 +129,8 @@ - (void)runWithArguments:(NSArray *)arguments { }]; __block BOOL syncCleanReqd = NO; - [rop syncCleanRequired:^(BOOL clean) { - syncCleanReqd = clean; + [rop syncTypeRequired:^(SNTSyncType syncType) { + syncCleanReqd = (syncType == SNTSyncTypeClean || syncType == SNTSyncTypeCleanAll); }]; __block BOOL pushNotifications = NO; diff --git a/Source/santactl/Commands/SNTCommandSync.m b/Source/santactl/Commands/SNTCommandSync.m index 489a7812a..87491797b 100644 --- a/Source/santactl/Commands/SNTCommandSync.m +++ b/Source/santactl/Commands/SNTCommandSync.m @@ -47,8 +47,10 @@ + (NSString *)longHelpText { return (@"If Santa is configured to synchronize with a server, " @"this is the command used for syncing.\n\n" @"Options:\n" - @" --clean: Perform a clean sync, erasing all existing rules and requesting a\n" - @" clean sync from the server."); + @" --clean: Perform a clean sync, erasing all existing non-transitive rules and\n" + @" requesting a clean sync from the server.\n" + @" --clean-all: Perform a clean sync, erasing all existing rules and requesting a\n" + @" clean sync from the server."); } - (void)runWithArguments:(NSArray *)arguments { @@ -75,10 +77,17 @@ - (void)runWithArguments:(NSArray *)arguments { lr.unprivilegedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SNTSyncServiceLogReceiverXPC)]; [lr resume]; - BOOL isClean = [NSProcessInfo.processInfo.arguments containsObject:@"--clean"]; + + SNTSyncType syncType = SNTSyncTypeNormal; + if ([NSProcessInfo.processInfo.arguments containsObject:@"--clean-all"]) { + syncType = SNTSyncTypeCleanAll; + } else if ([NSProcessInfo.processInfo.arguments containsObject:@"--clean"]) { + syncType = SNTSyncTypeClean; + } + [[ss remoteObjectProxy] syncWithLogListener:logListener.endpoint - isClean:isClean + syncType:syncType reply:^(SNTSyncStatusType status) { if (status == SNTSyncStatusTypeTooManySyncsInProgress) { [self didReceiveLog:@"Too many syncs in progress, try again later."]; diff --git a/Source/santad/DataLayer/SNTRuleTable.h b/Source/santad/DataLayer/SNTRuleTable.h index 6d7cb5389..7ad9b0ee8 100644 --- a/Source/santad/DataLayer/SNTRuleTable.h +++ b/Source/santad/DataLayer/SNTRuleTable.h @@ -75,11 +75,11 @@ /// transaction will abort if any rule fails to add. /// /// @param rules Array of SNTRule's to add. -/// @param cleanSlate If true, remove all rules before adding the new rules. +/// @param ruleCleanup Rule cleanup type to perform (e.g. all, none, non-transitive). /// @param error When returning NO, will be filled with appropriate error. /// @return YES if adding all rules passed, NO if any were rejected. /// -- (BOOL)addRules:(NSArray *)rules cleanSlate:(BOOL)cleanSlate error:(NSError **)error; +- (BOOL)addRules:(NSArray *)rules ruleCleanup:(SNTRuleCleanup)cleanupType error:(NSError **)error; /// /// Checks the given array of rules to see if adding any of them to the rules database would diff --git a/Source/santad/DataLayer/SNTRuleTable.m b/Source/santad/DataLayer/SNTRuleTable.m index 5c3fe3670..0b9d9d207 100644 --- a/Source/santad/DataLayer/SNTRuleTable.m +++ b/Source/santad/DataLayer/SNTRuleTable.m @@ -194,7 +194,7 @@ - (uint32_t)initializeDatabase:(FMDatabase *)db fromVersion:(uint32_t)version { @")"]; [db executeUpdate:@"CREATE UNIQUE INDEX rulesunique ON rules (shasum, type)"]; - [[SNTConfigurator configurator] setSyncCleanRequired:YES]; + [[SNTConfigurator configurator] setSyncTypeRequired:SNTSyncTypeCleanAll]; newVersion = 1; } @@ -403,7 +403,7 @@ - (SNTRule *)ruleForBinarySHA256:(NSString *)binarySHA256 #pragma mark Adding - (BOOL)addRules:(NSArray *)rules - cleanSlate:(BOOL)cleanSlate + ruleCleanup:(SNTRuleCleanup)cleanupType error:(NSError *__autoreleasing *)error { if (!rules || rules.count < 1) { [self fillError:error code:SNTRuleTableErrorEmptyRuleArray message:nil]; @@ -413,8 +413,10 @@ - (BOOL)addRules:(NSArray *)rules __block BOOL failed = NO; [self inTransaction:^(FMDatabase *db, BOOL *rollback) { - if (cleanSlate) { + if (cleanupType == SNTRuleCleanupAll) { [db executeUpdate:@"DELETE FROM rules"]; + } else if (cleanupType == SNTRuleCleanupNonTransitive) { + [db executeUpdate:@"DELETE FROM rules WHERE state != ?", @(SNTRuleStateAllowTransitive)]; } for (SNTRule *rule in rules) { diff --git a/Source/santad/DataLayer/SNTRuleTableTest.m b/Source/santad/DataLayer/SNTRuleTableTest.m index 0249c7521..65c41454b 100644 --- a/Source/santad/DataLayer/SNTRuleTableTest.m +++ b/Source/santad/DataLayer/SNTRuleTableTest.m @@ -65,6 +65,15 @@ - (SNTRule *)_exampleBinaryRule { return r; } +- (SNTRule *)_exampleTransitiveRule { + SNTRule *r = [[SNTRule alloc] init]; + r.identifier = @"1111e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b111"; + r.state = SNTRuleStateAllowTransitive; + r.type = SNTRuleTypeBinary; + r.customMsg = @"Transitive rule"; + return r; +} + - (SNTRule *)_exampleCertRule { SNTRule *r = [[SNTRule alloc] init]; r.identifier = @"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"; @@ -78,7 +87,7 @@ - (void)testAddRulesNotClean { NSUInteger binaryRuleCount = self.sut.binaryRuleCount; NSError *error; - [self.sut addRules:@[ [self _exampleBinaryRule] ] cleanSlate:NO error:&error]; + [self.sut addRules:@[ [self _exampleBinaryRule] ] ruleCleanup:SNTRuleCleanupNone error:&error]; XCTAssertEqual(self.sut.ruleCount, ruleCount + 1); XCTAssertEqual(self.sut.binaryRuleCount, binaryRuleCount + 1); @@ -88,24 +97,49 @@ - (void)testAddRulesNotClean { - (void)testAddRulesClean { // Add a binary rule without clean slate NSError *error = nil; - XCTAssertTrue([self.sut addRules:@[ [self _exampleBinaryRule] ] cleanSlate:NO error:&error]); + XCTAssertTrue([self.sut addRules:@[ [self _exampleBinaryRule] ] + ruleCleanup:SNTRuleCleanupNone + error:&error]); XCTAssertNil(error); // Now add a cert rule with a clean slate, assert that the binary rule was removed error = nil; - XCTAssertTrue(([self.sut addRules:@[ [self _exampleCertRule] ] cleanSlate:YES error:&error])); + XCTAssertTrue(([self.sut addRules:@[ [self _exampleCertRule] ] + ruleCleanup:SNTRuleCleanupAll + error:&error])); XCTAssertEqual([self.sut binaryRuleCount], 0); XCTAssertNil(error); } +- (void)testAddRulesCleanNonTransitive { + // Add a multiple binary rules, including a transitive rule + NSError *error = nil; + XCTAssertTrue(([self.sut addRules:@[ + [self _exampleBinaryRule], [self _exampleCertRule], [self _exampleTransitiveRule] + ] + ruleCleanup:SNTRuleCleanupNone + error:&error])); + XCTAssertEqual([self.sut binaryRuleCount], 2); + XCTAssertNil(error); + + // Now add a cert rule while cleaning non-transitive rules. Ensure the transitive rule remains + error = nil; + XCTAssertTrue(([self.sut addRules:@[ [self _exampleCertRule] ] + ruleCleanup:SNTRuleCleanupNonTransitive + error:&error])); + XCTAssertEqual([self.sut binaryRuleCount], 1); + XCTAssertEqual([self.sut certificateRuleCount], 1); + XCTAssertNil(error); +} + - (void)testAddMultipleRules { NSUInteger ruleCount = self.sut.ruleCount; NSError *error; [self.sut - addRules:@[ [self _exampleBinaryRule], [self _exampleCertRule], [self _exampleBinaryRule] ] - cleanSlate:NO - error:&error]; + addRules:@[ [self _exampleBinaryRule], [self _exampleCertRule], [self _exampleBinaryRule] ] + ruleCleanup:SNTRuleCleanupNone + error:&error]; XCTAssertEqual(self.sut.ruleCount, ruleCount + 2); XCTAssertNil(error); @@ -113,13 +147,13 @@ - (void)testAddMultipleRules { - (void)testAddRulesEmptyArray { NSError *error; - XCTAssertFalse([self.sut addRules:@[] cleanSlate:YES error:&error]); + XCTAssertFalse([self.sut addRules:@[] ruleCleanup:SNTRuleCleanupAll error:&error]); XCTAssertEqual(error.code, SNTRuleTableErrorEmptyRuleArray); } - (void)testAddRulesNilArray { NSError *error; - XCTAssertFalse([self.sut addRules:nil cleanSlate:YES error:&error]); + XCTAssertFalse([self.sut addRules:nil ruleCleanup:SNTRuleCleanupAll error:&error]); XCTAssertEqual(error.code, SNTRuleTableErrorEmptyRuleArray); } @@ -129,13 +163,13 @@ - (void)testAddInvalidRule { r.type = SNTRuleTypeCertificate; NSError *error; - XCTAssertFalse([self.sut addRules:@[ r ] cleanSlate:NO error:&error]); + XCTAssertFalse([self.sut addRules:@[ r ] ruleCleanup:SNTRuleCleanupNone error:&error]); XCTAssertEqual(error.code, SNTRuleTableErrorInvalidRule); } - (void)testFetchBinaryRule { [self.sut addRules:@[ [self _exampleBinaryRule], [self _exampleCertRule] ] - cleanSlate:NO + ruleCleanup:SNTRuleCleanupNone error:nil]; SNTRule *r = [self.sut @@ -158,7 +192,7 @@ - (void)testFetchBinaryRule { - (void)testFetchCertificateRule { [self.sut addRules:@[ [self _exampleBinaryRule], [self _exampleCertRule] ] - cleanSlate:NO + ruleCleanup:SNTRuleCleanupNone error:nil]; SNTRule *r = [self.sut @@ -181,7 +215,7 @@ - (void)testFetchCertificateRule { - (void)testFetchTeamIDRule { [self.sut addRules:@[ [self _exampleBinaryRule], [self _exampleTeamIDRule] ] - cleanSlate:NO + ruleCleanup:SNTRuleCleanupNone error:nil]; SNTRule *r = [self.sut ruleForBinarySHA256:nil @@ -205,7 +239,7 @@ - (void)testFetchSigningIDRule { [self _exampleBinaryRule], [self _exampleSigningIDRuleIsPlatform:YES], [self _exampleSigningIDRuleIsPlatform:NO] ] - cleanSlate:NO + ruleCleanup:SNTRuleCleanupNone error:nil]; XCTAssertEqual([self.sut signingIDRuleCount], 2); @@ -236,7 +270,7 @@ - (void)testFetchRuleOrdering { [self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule], [self _exampleSigningIDRuleIsPlatform:NO] ] - cleanSlate:NO + ruleCleanup:SNTRuleCleanupNone error:nil]; // This test verifies that the implicit rule ordering we've been abusing is still working. @@ -295,7 +329,7 @@ - (void)testBadDatabase { FMDatabaseQueue *dbq = [[FMDatabaseQueue alloc] initWithPath:dbPath]; SNTRuleTable *sut = [[SNTRuleTable alloc] initWithDatabaseQueue:dbq]; - [sut addRules:@[ [self _exampleBinaryRule] ] cleanSlate:NO error:nil]; + [sut addRules:@[ [self _exampleBinaryRule] ] ruleCleanup:SNTRuleCleanupNone error:nil]; XCTAssertGreaterThan(sut.ruleCount, 0); [[NSFileManager defaultManager] removeItemAtPath:dbPath error:NULL]; @@ -311,7 +345,7 @@ - (void)testRetrieveAllRulesWithMultipleRules { [self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule], [self _exampleSigningIDRuleIsPlatform:NO] ] - cleanSlate:NO + ruleCleanup:SNTRuleCleanupNone error:nil]; NSArray *rules = [self.sut retrieveAllRules]; diff --git a/Source/santad/EventProviders/SNTEndpointSecurityAuthorizerTest.mm b/Source/santad/EventProviders/SNTEndpointSecurityAuthorizerTest.mm index e92c6985a..b5519c2cc 100644 --- a/Source/santad/EventProviders/SNTEndpointSecurityAuthorizerTest.mm +++ b/Source/santad/EventProviders/SNTEndpointSecurityAuthorizerTest.mm @@ -22,7 +22,7 @@ #include #include -#include "Source/common/SNTCommonEnums.h" +#import "Source/common/SNTCommonEnums.h" #include "Source/common/TestUtils.h" #include "Source/santad/EventProviders/AuthResultCache.h" #include "Source/santad/EventProviders/EndpointSecurity/Client.h" diff --git a/Source/santad/EventProviders/SNTEndpointSecurityDeviceManagerTest.mm b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManagerTest.mm index b2ec19c07..b612d6032 100644 --- a/Source/santad/EventProviders/SNTEndpointSecurityDeviceManagerTest.mm +++ b/Source/santad/EventProviders/SNTEndpointSecurityDeviceManagerTest.mm @@ -375,16 +375,16 @@ - (void)testPerformStartupTasks { // Create mock disks with desired args MockDADisk * (^CreateMockDisk)(NSString *, NSString *) = ^MockDADisk *(NSString *mountOn, NSString *mountFrom) { - MockDADisk *mockDisk = [[MockDADisk alloc] init]; - mockDisk.diskDescription = @{ - @"DAVolumePath" : mountOn, // f_mntonname, - @"DADevicePath" : mountOn, // f_mntonname, - @"DAMediaBSDName" : mountFrom, // f_mntfromname, + MockDADisk *mockDisk = [[MockDADisk alloc] init]; + mockDisk.diskDescription = @{ + @"DAVolumePath" : mountOn, // f_mntonname, + @"DADevicePath" : mountOn, // f_mntonname, + @"DAMediaBSDName" : mountFrom, // f_mntfromname, + }; + + return mockDisk; }; - return mockDisk; - }; - // Reset the Mock DA property, setup disks and remount args, then trigger the test void (^PerformStartupTest)(NSArray *, NSArray *, SNTDeviceManagerStartupPreferences) = diff --git a/Source/santad/EventProviders/SNTEndpointSecurityFileAccessAuthorizerTest.mm b/Source/santad/EventProviders/SNTEndpointSecurityFileAccessAuthorizerTest.mm index 9e6079d05..78f5c208f 100644 --- a/Source/santad/EventProviders/SNTEndpointSecurityFileAccessAuthorizerTest.mm +++ b/Source/santad/EventProviders/SNTEndpointSecurityFileAccessAuthorizerTest.mm @@ -33,7 +33,7 @@ #include "Source/common/Platform.h" #include "Source/common/SNTCachedDecision.h" -#include "Source/common/SNTCommonEnums.h" +#import "Source/common/SNTCommonEnums.h" #import "Source/common/SNTConfigurator.h" #include "Source/common/TestUtils.h" #include "Source/santad/DataLayer/WatchItemPolicy.h" diff --git a/Source/santad/Logs/EndpointSecurity/Logger.mm b/Source/santad/Logs/EndpointSecurity/Logger.mm index 89aae0c4d..5fc3002c1 100644 --- a/Source/santad/Logs/EndpointSecurity/Logger.mm +++ b/Source/santad/Logs/EndpointSecurity/Logger.mm @@ -14,7 +14,7 @@ #include "Source/santad/Logs/EndpointSecurity/Logger.h" -#include "Source/common/SNTCommonEnums.h" +#import "Source/common/SNTCommonEnums.h" #include "Source/common/SNTLogging.h" #include "Source/common/SNTStoredEvent.h" #include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h" diff --git a/Source/santad/Logs/EndpointSecurity/LoggerTest.mm b/Source/santad/Logs/EndpointSecurity/LoggerTest.mm index 9704ef7e7..1681a77c1 100644 --- a/Source/santad/Logs/EndpointSecurity/LoggerTest.mm +++ b/Source/santad/Logs/EndpointSecurity/LoggerTest.mm @@ -23,7 +23,7 @@ #include #include -#include "Source/common/SNTCommonEnums.h" +#import "Source/common/SNTCommonEnums.h" #include "Source/common/TestUtils.h" #include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h" #include "Source/santad/EventProviders/EndpointSecurity/Message.h" diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm b/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm index 050dc3be5..3ebd94036 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm @@ -28,7 +28,7 @@ #include #import "Source/common/SNTCachedDecision.h" -#include "Source/common/SNTCommonEnums.h" +#import "Source/common/SNTCommonEnums.h" #import "Source/common/SNTConfigurator.h" #import "Source/common/SNTStoredEvent.h" #include "Source/common/TestUtils.h" diff --git a/Source/santad/SNTCompilerController.mm b/Source/santad/SNTCompilerController.mm index e6ac90c6e..1b2d5a571 100644 --- a/Source/santad/SNTCompilerController.mm +++ b/Source/santad/SNTCompilerController.mm @@ -158,7 +158,7 @@ - (void)createTransitiveRule:(const Message &)esMsg // Add the new rule to the rules database. NSError *err; - if (![ruleTable addRules:@[ rule ] cleanSlate:NO error:&err]) { + if (![ruleTable addRules:@[ rule ] ruleCleanup:SNTRuleCleanupNone error:&err]) { LOGE(@"unable to add new transitive rule to database: %@", err.localizedDescription); } else { logger->LogAllowlist(esMsg, [fi.SHA256 UTF8String]); diff --git a/Source/santad/SNTDaemonControlController.mm b/Source/santad/SNTDaemonControlController.mm index 12390d5ae..1e368bd23 100644 --- a/Source/santad/SNTDaemonControlController.mm +++ b/Source/santad/SNTDaemonControlController.mm @@ -105,17 +105,18 @@ - (void)databaseRuleCounts:(void (^)(int64_t binary, int64_t certificate, int64_ } - (void)databaseRuleAddRules:(NSArray *)rules - cleanSlate:(BOOL)cleanSlate + ruleCleanup:(SNTRuleCleanup)cleanupType reply:(void (^)(NSError *error))reply { SNTRuleTable *ruleTable = [SNTDatabaseController ruleTable]; // If any rules are added that are not plain allowlist rules, then flush decision cache. // In particular, the addition of allowlist compiler rules should cause a cache flush. // We also flush cache if a allowlist compiler rule is replaced with a allowlist rule. - BOOL flushCache = (cleanSlate || [ruleTable addedRulesShouldFlushDecisionCache:rules]); + BOOL flushCache = + ((cleanupType != SNTRuleCleanupNone) || [ruleTable addedRulesShouldFlushDecisionCache:rules]); NSError *error; - [ruleTable addRules:rules cleanSlate:cleanSlate error:&error]; + [ruleTable addRules:rules ruleCleanup:cleanupType error:&error]; // Whenever we add rules, we can also check for and remove outdated transitive rules. [ruleTable removeOutdatedTransitiveRules]; @@ -233,12 +234,12 @@ - (void)setRuleSyncLastSuccess:(NSDate *)date reply:(void (^)(void))reply { reply(); } -- (void)syncCleanRequired:(void (^)(BOOL))reply { - reply([[SNTConfigurator configurator] syncCleanRequired]); +- (void)syncTypeRequired:(void (^)(SNTSyncType))reply { + reply([[SNTConfigurator configurator] syncTypeRequired]); } -- (void)setSyncCleanRequired:(BOOL)cleanReqd reply:(void (^)(void))reply { - [[SNTConfigurator configurator] setSyncCleanRequired:cleanReqd]; +- (void)setSyncTypeRequired:(SNTSyncType)syncType reply:(void (^)(void))reply { + [[SNTConfigurator configurator] setSyncTypeRequired:syncType]; reply(); } diff --git a/Source/santad/SNTExecutionControllerTest.mm b/Source/santad/SNTExecutionControllerTest.mm index 24d836d16..d3bce0c40 100644 --- a/Source/santad/SNTExecutionControllerTest.mm +++ b/Source/santad/SNTExecutionControllerTest.mm @@ -18,7 +18,6 @@ #import #import #include -#include "Source/common/SNTCommonEnums.h" #import "Source/common/SNTCachedDecision.h" #import "Source/common/SNTCommonEnums.h" diff --git a/Source/santasyncservice/SNTSyncEventUpload.m b/Source/santasyncservice/SNTSyncEventUpload.m index 4cce233e0..d13075cf9 100644 --- a/Source/santasyncservice/SNTSyncEventUpload.m +++ b/Source/santasyncservice/SNTSyncEventUpload.m @@ -17,6 +17,7 @@ #import #import +#import "Source/common/SNTCommonEnums.h" #import "Source/common/SNTConfigurator.h" #import "Source/common/SNTFileInfo.h" #import "Source/common/SNTLogging.h" @@ -55,7 +56,8 @@ - (BOOL)uploadEvents:(NSArray *)events { if (uploadEvents.count >= self.syncState.eventBatchSize) break; } - if (!self.syncState.cleanSync || [[SNTConfigurator configurator] enableCleanSyncEventUpload]) { + if (self.syncState.syncType == SNTSyncTypeNormal || + [[SNTConfigurator configurator] enableCleanSyncEventUpload]) { NSDictionary *r = [self performRequest:[self requestWithDictionary:@{kEvents : uploadEvents}]]; if (!r) return NO; diff --git a/Source/santasyncservice/SNTSyncManager.h b/Source/santasyncservice/SNTSyncManager.h index 76777a3e2..0dcf934b9 100644 --- a/Source/santasyncservice/SNTSyncManager.h +++ b/Source/santasyncservice/SNTSyncManager.h @@ -14,6 +14,7 @@ #import +#import "Source/common/SNTCommonEnums.h" #import "Source/common/SNTXPCSyncServiceInterface.h" @class MOLXPCConnection; @@ -60,7 +61,7 @@ /// /// Pass true to isClean to perform a clean sync, defaults to false. /// -- (void)syncAndMakeItClean:(BOOL)clean withReply:(void (^)(SNTSyncStatusType))reply; +- (void)syncType:(SNTSyncType)syncType withReply:(void (^)(SNTSyncStatusType))reply; /// /// Handle SNTSyncServiceXPC messages forwarded from SNTSyncService. diff --git a/Source/santasyncservice/SNTSyncManager.m b/Source/santasyncservice/SNTSyncManager.m index 56f8e0fa4..f7ff4241e 100644 --- a/Source/santasyncservice/SNTSyncManager.m +++ b/Source/santasyncservice/SNTSyncManager.m @@ -87,7 +87,7 @@ - (instancetype)initWithDaemonConnection:(MOLXPCConnection *)daemonConn { _fullSyncTimer = [self createSyncTimerWithBlock:^{ [self rescheduleTimerQueue:self.fullSyncTimer secondsFromNow:_pushNotifications.pushNotificationsFullSyncInterval]; - [self syncAndMakeItClean:NO withReply:NULL]; + [self syncType:SNTSyncTypeNormal withReply:NULL]; }]; _ruleSyncTimer = [self createSyncTimerWithBlock:^{ dispatch_source_set_timer(self.ruleSyncTimer, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, @@ -177,19 +177,19 @@ - (void)syncSecondsFromNow:(uint64_t)seconds { [self rescheduleTimerQueue:self.fullSyncTimer secondsFromNow:seconds]; } -- (void)syncAndMakeItClean:(BOOL)clean withReply:(void (^)(SNTSyncStatusType))reply { +- (void)syncType:(SNTSyncType)syncType withReply:(void (^)(SNTSyncStatusType))reply { if (dispatch_semaphore_wait(self.syncLimiter, DISPATCH_TIME_NOW)) { if (reply) reply(SNTSyncStatusTypeTooManySyncsInProgress); return; } dispatch_async(self.syncQueue, ^() { SLOGI(@"Starting sync..."); - if (clean) { + if (syncType != SNTSyncTypeNormal) { dispatch_semaphore_t sema = dispatch_semaphore_create(0); - [[self.daemonConn remoteObjectProxy] setSyncCleanRequired:YES - reply:^() { - dispatch_semaphore_signal(sema); - }]; + [[self.daemonConn remoteObjectProxy] setSyncTypeRequired:syncType + reply:^() { + dispatch_semaphore_signal(sema); + }]; if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC))) { SLOGE(@"Timeout waiting for daemon"); if (reply) reply(SNTSyncStatusTypeDaemonTimeout); diff --git a/Source/santasyncservice/SNTSyncPostflight.m b/Source/santasyncservice/SNTSyncPostflight.m index 4f115c1e4..f5c8278c9 100644 --- a/Source/santasyncservice/SNTSyncPostflight.m +++ b/Source/santasyncservice/SNTSyncPostflight.m @@ -42,11 +42,11 @@ - (BOOL)sync { }]; } - // Remove clean sync flag if we did a clean sync - if (self.syncState.cleanSync) { - [rop setSyncCleanRequired:NO - reply:^{ - }]; + // Remove clean sync flag if we did a clean or clean all sync + if (self.syncState.syncType != SNTSyncTypeNormal) { + [rop setSyncTypeRequired:SNTSyncTypeNormal + reply:^{ + }]; } // Update allowlist/blocklist regexes diff --git a/Source/santasyncservice/SNTSyncPreflight.m b/Source/santasyncservice/SNTSyncPreflight.m index 7a9f416e1..ab9ff61cb 100644 --- a/Source/santasyncservice/SNTSyncPreflight.m +++ b/Source/santasyncservice/SNTSyncPreflight.m @@ -33,6 +33,39 @@ static id EnsureType(id val, Class c) { } } +/* + +Clean Sync Implementation Notes + +The clean sync implementation seems a bit complex at first glance, but boils +down to the following rules: + +1. If the server says to do a "clean" sync, a "clean" sync is performed, unless the + client specified a "clean all" sync, in which case "clean all" is performed. +2. If the server responded that it is performing a "clean all" sync, a "clean all" is performed. +3. All other server responses result in a "normal" sync. + +The following table expands upon the above logic to list most of the permutations: + +| Client Sync State | Clean Sync Request? | Server Response | Sync Type Performed | +| ----------------- | ------------------- | ------------------ | ------------------- | +| normal | No | normal OR | normal | +| normal | No | clean | clean | +| normal | No | clean_all | clean_all | +| normal | No | clean_sync (dep) | clean | +| normal | Yes | New AND Dep Key | Dep key ignored | +| clean | Yes | normal OR | normal | +| clean | Yes | clean | clean | +| clean | Yes | clean_all | clean_all | +| clean | Yes | clean_sync (dep) | clean | +| clean | Yes | New AND Dep Key | Dep key ignored | +| clean_all | Yes | normal OR | normal | +| clean_all | Yes | clean | clean_all | +| clean_all | Yes | clean_all | clean_all | +| clean_all | Yes | clean_sync (dep) | clean_all | +| clean_all | Yes | New AND Dep Key | Dep key ignored | + +*/ @implementation SNTSyncPreflight - (NSURL *)stageURL { @@ -75,14 +108,15 @@ - (BOOL)sync { } }]; - __block BOOL syncClean = NO; - [rop syncCleanRequired:^(BOOL clean) { - syncClean = clean; + __block SNTSyncType requestSyncType = SNTSyncTypeNormal; + [rop syncTypeRequired:^(SNTSyncType syncTypeRequired) { + requestSyncType = syncTypeRequired; }]; // If user requested it or we've never had a successful sync, try from a clean slate. - if (syncClean) { - SLOGD(@"Clean sync requested by user"); + if (requestSyncType == SNTSyncTypeClean || requestSyncType == SNTSyncTypeCleanAll) { + SLOGD(@"%@ sync requested by user", + (requestSyncType == SNTSyncTypeCleanAll) ? @"Clean All" : @"Clean"); requestDict[kRequestCleanSync] = @YES; } @@ -137,9 +171,51 @@ - (BOOL)sync { self.syncState.overrideFileAccessAction = EnsureType(resp[kOverrideFileAccessAction], [NSString class]); - if ([EnsureType(resp[kCleanSync], [NSNumber class]) boolValue]) { + // Default sync type is SNTSyncTypeNormal + // + // Logic overview: + // The requested sync type (clean or normal) is merely informative. The server + // can choose to respond with a normal, clean or clean_all. + // + // If the server responds that it will perform a clean sync, santa will + // treat it as either a clean or clean_all depending on which was requested. + // + // The server can also "override" the requested clean operation. If a normal + // sync was requested, but the server responded that it was doing a clean or + // clean_all sync, that will take precedence. Similarly, if only a clean sync + // was requested, the server can force a "clean_all" operation to take place. + self.syncState.syncType = SNTSyncTypeNormal; + + // If kSyncType response key exists, it overrides the kCleanSyncDeprecated value + // First check if the kSyncType reponse key exists. If so, it takes precedence + // over the kCleanSyncDeprecated key. + NSString *responseSyncType = [EnsureType(resp[kSyncType], [NSString class]) lowercaseString]; + if (responseSyncType) { + if ([responseSyncType isEqualToString:@"clean"]) { + // If the client wants to Clean All, this takes precedence. The server + // cannot override the client wanting to remove all rules. + if (requestSyncType == SNTSyncTypeCleanAll) { + self.syncState.syncType = SNTSyncTypeCleanAll; + } else { + self.syncState.syncType = SNTSyncTypeClean; + } + } else if ([responseSyncType isEqualToString:@"clean_all"]) { + self.syncState.syncType = SNTSyncTypeCleanAll; + } + } else if ([EnsureType(resp[kCleanSyncDeprecated], [NSNumber class]) boolValue]) { + // If the deprecated key is set, the type of sync clean performed should be + // the type that was requested. This must be set appropriately so that it + // can be propagated during the Rule Download stage so SNTRuleTable knows + // which rules to delete. + if (requestSyncType == SNTSyncTypeCleanAll) { + self.syncState.syncType = SNTSyncTypeCleanAll; + } else { + self.syncState.syncType = SNTSyncTypeClean; + } + } + + if (self.syncState.syncType != SNTSyncTypeNormal) { SLOGD(@"Clean sync requested by server"); - self.syncState.cleanSync = YES; } return YES; diff --git a/Source/santasyncservice/SNTSyncRuleDownload.m b/Source/santasyncservice/SNTSyncRuleDownload.m index 8c9f5c8af..dfbe93ac9 100644 --- a/Source/santasyncservice/SNTSyncRuleDownload.m +++ b/Source/santasyncservice/SNTSyncRuleDownload.m @@ -24,6 +24,15 @@ #import "Source/santasyncservice/SNTSyncLogging.h" #import "Source/santasyncservice/SNTSyncState.h" +SNTRuleCleanup SyncTypeToRuleCleanup(SNTSyncType syncType) { + switch (syncType) { + case SNTSyncTypeNormal: return SNTRuleCleanupNone; + case SNTSyncTypeClean: return SNTRuleCleanupNonTransitive; + case SNTSyncTypeCleanAll: return SNTRuleCleanupAll; + default: return SNTRuleCleanupNone; + } +} + @implementation SNTSyncRuleDownload - (NSURL *)stageURL { @@ -41,12 +50,13 @@ - (BOOL)sync { // Wait until finished or until 5 minutes pass. dispatch_semaphore_t sema = dispatch_semaphore_create(0); __block NSError *error; - [[self.daemonConn remoteObjectProxy] databaseRuleAddRules:newRules - cleanSlate:self.syncState.cleanSync - reply:^(NSError *e) { - error = e; - dispatch_semaphore_signal(sema); - }]; + [[self.daemonConn remoteObjectProxy] + databaseRuleAddRules:newRules + ruleCleanup:SyncTypeToRuleCleanup(self.syncState.syncType) + reply:^(NSError *e) { + error = e; + dispatch_semaphore_signal(sema); + }]; if (dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 300 * NSEC_PER_SEC))) { SLOGE(@"Failed to add rule(s) to database: timeout sending rules to daemon"); return NO; diff --git a/Source/santasyncservice/SNTSyncService.m b/Source/santasyncservice/SNTSyncService.m index 3e0ea6e34..4f5f19733 100644 --- a/Source/santasyncservice/SNTSyncService.m +++ b/Source/santasyncservice/SNTSyncService.m @@ -81,22 +81,22 @@ - (void)isFCMListening:(void (^)(BOOL))reply { // TODO(bur): Add support for santactl sync --debug to enable debug logging for that sync. - (void)syncWithLogListener:(NSXPCListenerEndpoint *)logListener - isClean:(BOOL)cleanSync + syncType:(SNTSyncType)syncType reply:(void (^)(SNTSyncStatusType))reply { MOLXPCConnection *ll = [[MOLXPCConnection alloc] initClientWithListener:logListener]; ll.remoteInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SNTSyncServiceLogReceiverXPC)]; [ll resume]; - [self.syncManager syncAndMakeItClean:cleanSync - withReply:^(SNTSyncStatusType status) { - if (status == SNTSyncStatusTypeSyncStarted) { - [[SNTSyncBroadcaster broadcaster] addLogListener:ll]; - return; - } - [[SNTSyncBroadcaster broadcaster] barrier]; - [[SNTSyncBroadcaster broadcaster] removeLogListener:ll]; - reply(status); - }]; + [self.syncManager syncType:syncType + withReply:^(SNTSyncStatusType status) { + if (status == SNTSyncStatusTypeSyncStarted) { + [[SNTSyncBroadcaster broadcaster] addLogListener:ll]; + return; + } + [[SNTSyncBroadcaster broadcaster] barrier]; + [[SNTSyncBroadcaster broadcaster] removeLogListener:ll]; + reply(status); + }]; } - (void)spindown { diff --git a/Source/santasyncservice/SNTSyncStage.m b/Source/santasyncservice/SNTSyncStage.m index 4e2aedc38..721bf2b83 100644 --- a/Source/santasyncservice/SNTSyncStage.m +++ b/Source/santasyncservice/SNTSyncStage.m @@ -13,10 +13,10 @@ /// limitations under the License. #import "Source/santasyncservice/SNTSyncStage.h" -#include "Source/common/SNTCommonEnums.h" #import +#import "Source/common/SNTCommonEnums.h" #import "Source/common/SNTConfigurator.h" #import "Source/common/SNTLogging.h" #import "Source/common/SNTSyncConstants.h" diff --git a/Source/santasyncservice/SNTSyncState.h b/Source/santasyncservice/SNTSyncState.h index 9a1844680..3a9a789c5 100644 --- a/Source/santasyncservice/SNTSyncState.h +++ b/Source/santasyncservice/SNTSyncState.h @@ -71,7 +71,7 @@ @property NSString *overrideFileAccessAction; /// Clean sync flag, if True, all existing rules should be deleted before inserting any new rules. -@property BOOL cleanSync; +@property SNTSyncType syncType; /// Batch size for uploading events. @property NSUInteger eventBatchSize; diff --git a/Source/santasyncservice/SNTSyncTest.m b/Source/santasyncservice/SNTSyncTest.m index d8abd5b7b..efc52167f 100644 --- a/Source/santasyncservice/SNTSyncTest.m +++ b/Source/santasyncservice/SNTSyncTest.m @@ -155,7 +155,8 @@ - (void)setupDefaultDaemonConnResponses { OCMOCK_VALUE(0), // teamID OCMOCK_VALUE(0), // signingID nil])]); - OCMStub([self.daemonConnRop syncCleanRequired:([OCMArg invokeBlockWithArgs:@NO, nil])]); + OCMStub([self.daemonConnRop + syncTypeRequired:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(SNTSyncTypeNormal), nil])]); OCMStub([self.daemonConnRop clientMode:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(SNTClientModeMonitor), nil])]); } @@ -378,7 +379,12 @@ - (void)testPreflightDatabaseCounts { [sut sync]; } -- (void)testPreflightCleanSync { +// This method is designed to help facilitate easy testing of many different +// permutations of clean sync request / response values and how syncType gets set. +- (void)cleanSyncPreflightRequiredSyncType:(SNTSyncType)requestedSyncType + expectcleanSyncRequest:(BOOL)expectcleanSyncRequest + expectedSyncType:(SNTSyncType)expectedSyncType + response:(NSDictionary *)resp { SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState]; OCMStub([self.daemonConnRop @@ -391,21 +397,146 @@ - (void)testPreflightCleanSync { nil])]); OCMStub([self.daemonConnRop clientMode:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(SNTClientModeMonitor), nil])]); - OCMStub([self.daemonConnRop syncCleanRequired:([OCMArg invokeBlockWithArgs:@YES, nil])]); + OCMStub([self.daemonConnRop + syncTypeRequired:([OCMArg invokeBlockWithArgs:OCMOCK_VALUE(requestedSyncType), nil])]); - NSData *respData = [self dataFromDict:@{kCleanSync : @YES}]; + NSData *respData = [self dataFromDict:resp]; [self stubRequestBody:respData response:nil error:nil validateBlock:^BOOL(NSURLRequest *req) { NSDictionary *requestDict = [self dictFromRequest:req]; - XCTAssertEqualObjects(requestDict[kRequestCleanSync], @YES); + if (expectcleanSyncRequest) { + XCTAssertEqualObjects(requestDict[kRequestCleanSync], @YES); + } else { + XCTAssertNil(requestDict[kRequestCleanSync]); + } return YES; }]; [sut sync]; - XCTAssertEqual(self.syncState.cleanSync, YES); + XCTAssertEqual(self.syncState.syncType, expectedSyncType); +} + +- (void)testPreflightStateNormalRequestEmptyResponseEmpty { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeNormal + expectcleanSyncRequest:NO + expectedSyncType:SNTSyncTypeNormal + response:@{}]; +} + +- (void)testPreflightStateNormalRequestEmptyResponseNormal { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeNormal + expectcleanSyncRequest:NO + expectedSyncType:SNTSyncTypeNormal + response:@{kSyncType : @"normal"}]; +} + +- (void)testPreflightStateNormalRequestEmptyResponseClean { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeNormal + expectcleanSyncRequest:NO + expectedSyncType:SNTSyncTypeClean + response:@{kSyncType : @"clean"}]; +} + +- (void)testPreflightStateNormalRequestEmptyResponseCleanAll { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeNormal + expectcleanSyncRequest:NO + expectedSyncType:SNTSyncTypeCleanAll + response:@{kSyncType : @"clean_all"}]; +} + +- (void)testPreflightStateNormalRequestEmptyResponseCleanDep { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeNormal + expectcleanSyncRequest:NO + expectedSyncType:SNTSyncTypeClean + response:@{kCleanSyncDeprecated : @YES}]; +} + +- (void)testPreflightStateCleanRequestCleanResponseEmpty { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeClean + expectcleanSyncRequest:YES + expectedSyncType:SNTSyncTypeNormal + response:@{}]; +} + +- (void)testPreflightStateCleanRequestCleanResponseNormal { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeClean + expectcleanSyncRequest:YES + expectedSyncType:SNTSyncTypeNormal + response:@{kSyncType : @"normal"}]; +} + +- (void)testPreflightStateCleanRequestCleanResponseClean { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeClean + expectcleanSyncRequest:YES + expectedSyncType:SNTSyncTypeClean + response:@{kSyncType : @"clean"}]; +} + +- (void)testPreflightStateCleanRequestCleanResponseCleanAll { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeClean + expectcleanSyncRequest:YES + expectedSyncType:SNTSyncTypeCleanAll + response:@{kSyncType : @"clean_all"}]; +} + +- (void)testPreflightStateCleanRequestCleanResponseCleanDep { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeClean + expectcleanSyncRequest:YES + expectedSyncType:SNTSyncTypeClean + response:@{kCleanSyncDeprecated : @YES}]; +} + +- (void)testPreflightStateCleanAllRequestCleanResponseEmpty { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeCleanAll + expectcleanSyncRequest:YES + expectedSyncType:SNTSyncTypeNormal + response:@{}]; +} + +- (void)testPreflightStateCleanAllRequestCleanResponseNormal { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeCleanAll + expectcleanSyncRequest:YES + expectedSyncType:SNTSyncTypeNormal + response:@{kSyncType : @"normal"}]; +} + +- (void)testPreflightStateCleanAllRequestCleanResponseClean { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeCleanAll + expectcleanSyncRequest:YES + expectedSyncType:SNTSyncTypeCleanAll + response:@{kSyncType : @"clean"}]; +} + +- (void)testPreflightStateCleanAllRequestCleanResponseCleanAll { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeCleanAll + expectcleanSyncRequest:YES + expectedSyncType:SNTSyncTypeCleanAll + response:@{kSyncType : @"clean_all"}]; +} + +- (void)testPreflightStateCleanAllRequestCleanResponseCleanDep { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeCleanAll + expectcleanSyncRequest:YES + expectedSyncType:SNTSyncTypeCleanAll + response:@{kCleanSyncDeprecated : @YES}]; +} + +- (void)testPreflightStateCleanAllRequestCleanResponseUnknown { + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeCleanAll + expectcleanSyncRequest:YES + expectedSyncType:SNTSyncTypeNormal + response:@{kSyncType : @"foo"}]; +} + +- (void)testPreflightStateCleanAllRequestCleanResponseTypeAndDepMismatch { + // Note: The kSyncType key takes precedence over kCleanSyncDeprecated if both are set + [self cleanSyncPreflightRequiredSyncType:SNTSyncTypeCleanAll + expectcleanSyncRequest:YES + expectedSyncType:SNTSyncTypeNormal + response:@{kSyncType : @"normal", kCleanSyncDeprecated : @YES}]; } - (void)testPreflightLockdown { @@ -568,7 +699,7 @@ - (void)testRuleDownload { // Stub out the call to invoke the block, verification of the input is later OCMStub([self.daemonConnRop databaseRuleAddRules:OCMOCK_ANY - cleanSlate:NO + ruleCleanup:SNTRuleCleanupNone reply:([OCMArg invokeBlockWithArgs:[NSNull null], nil])]); [sut sync]; @@ -594,7 +725,9 @@ - (void)testRuleDownload { customMsg:@"Banned team ID"], ]; - OCMVerify([self.daemonConnRop databaseRuleAddRules:rules cleanSlate:NO reply:OCMOCK_ANY]); + OCMVerify([self.daemonConnRop databaseRuleAddRules:rules + ruleCleanup:SNTRuleCleanupNone + reply:OCMOCK_ANY]); } #pragma mark - SNTSyncPostflight Tests @@ -612,9 +745,15 @@ - (void)testPostflightBasicResponse { XCTAssertTrue([sut sync]); OCMVerify([self.daemonConnRop setClientMode:SNTClientModeMonitor reply:OCMOCK_ANY]); - self.syncState.cleanSync = YES; + // For Clean syncs, the sync type required should be reset to normal + self.syncState.syncType = SNTSyncTypeClean; + XCTAssertTrue([sut sync]); + OCMVerify([self.daemonConnRop setSyncTypeRequired:SNTSyncTypeNormal reply:OCMOCK_ANY]); + + // For Clean All syncs, the sync type required should be reset to normal + self.syncState.syncType = SNTSyncTypeCleanAll; XCTAssertTrue([sut sync]); - OCMVerify([self.daemonConnRop setSyncCleanRequired:NO reply:OCMOCK_ANY]); + OCMVerify([self.daemonConnRop setSyncTypeRequired:SNTSyncTypeNormal reply:OCMOCK_ANY]); self.syncState.allowlistRegex = @"^horse$"; self.syncState.blocklistRegex = @"^donkey$"; diff --git a/docs/development/sync-protocol.md b/docs/development/sync-protocol.md index c0ecdaee2..8b7c7e2b7 100644 --- a/docs/development/sync-protocol.md +++ b/docs/development/sync-protocol.md @@ -8,7 +8,7 @@ parent: Development This document describes the protocol between Santa and the sync server, also known as the sync protocol. Implementors should be able to use this to create their own sync servers. -## Background +# Background Santa can be run and configured with a sync server. This allows an admin to easily configure and sync rules across a fleet of macOS systems. In addition to @@ -57,15 +57,15 @@ Where `` is a unique string identifier for the client. By default Santa uses the hardware UUID. It may also be set using the [MachineID, MachineIDPlist, and MachineIDKey options](../deployment/configuration.md) in the configuration. -## Authentication +# Authentication The protocol expects the client to authenticate the server via SSL/TLS. Additionally, a sync server may support client certificates and use mutual TLS. -## Stages +# Stages All URLs are of the form `//`, e.g. the preflight URL is `/preflight/`. -### Preflight +## Preflight The preflight stage is used by the client to report host information to the sync server and to retrieve a limited set of configuration settings from the server. @@ -80,7 +80,7 @@ sequenceDiagram server -->> client: preflight response ``` -#### `preflight` Request +### `preflight` Request The request consists of the following JSON keys: | Key | Required | Type | Meaning | Example Value | @@ -101,7 +101,7 @@ The request consists of the following JSON keys: | request_clean_sync | NO | bool | The client has requested a clean sync of its rules from the server | true | -### Example preflight request JSON Payload: +#### Example preflight request JSON Payload: ```json { @@ -122,7 +122,7 @@ The request consists of the following JSON keys: } ``` -#### `preflight` Response +### `preflight` Response If all of the data is well formed, the server responds with an HTTP status code of 200 and provides a JSON response. @@ -139,9 +139,10 @@ The JSON object has the following keys: | blocked_path_regex | NO | string | Regular expression to block a binary from executing by path | "/tmp/" | | block_usb_mount | NO | boolean | Block USB mass storage devices | true | | remount_usb_mode | NO | string | Force USB mass storage devices to be remounted with the following permissions (see [configuration](../deployment/configuration.md)) | | -| clean_sync | YES | boolean | Whether or not the rules should be dropped and synced entirely from the server | true | +| sync_type | NO | string | If set, the type of sync that the client should perform. Must be one of:
1.) `normal` (or not set) The server intends only to send new rules. The client will not drop any existing rules.
2.) `clean` Instructs the client to drop all non-transitive rules. The server intends to entirely sync all rules.
3.) `clean_all` Instructs the client to drop all rules. The server intends to entirely sync all rules.
See [Clean Syncs](#clean-syncs) for more info. | `normal`, `clean` or `clean_all` | | override_file_access_action | NO | string | Override file access config policy action. Must be:
1.) "Disable" to not log or block any rule violations.
2.) "AuditOnly" to only log violations, not block anything.
3.) "" (empty string) or "None" to not override the config | "Disable", or "AuditOnly", or "" (empty string) | + #### Example Preflight Response Payload ```json @@ -156,7 +157,24 @@ The JSON object has the following keys: } ``` -### EventUpload +### Clean Syncs + +Clean syncs will result in rules being deleted from the host before applying the newly synced rule set from the server. When the server indicates it is performing a clean sync, it means it intends to sync all current rules to the client. + +The client maintains a "sync type state" that controls the type of sync it wants to perform (i.e. `normal`, `clean` or `clean_all`). This is typically set by using `santactl sync`, `santactl sync --clean`, or `santactl sync --clean-all` respectively. Either clean sync type state being set will result in the `request_clean_sync` key being set to true in the [Preflight Request](#preflight-request). + +There are three types of syncs the server can set: `normal`, `clean`, and `clean_all`. The server indicates the type of sync it wants to perform by setting the `sync_type` key in the [Preflight Response](#preflight-response). When a sever performs a normal sync, it only intends to send new rules to the client. When a server performs either a `clean` or `clean_all` sync, it intends to send all rules and the client should delete appropriate rules (non-transitive, or all). The server should try to honor the `request_clean_sync` key if set to true in the [Preflight Request](#preflight-request) by setting the `sync_type` to `clean` (or possibly `clean_all` if desired). + +The rules for resolving the type of sync that will be performed are as follows: +1. If the server responds with a `sync_type` of `clean`, a clean sync is performed (regardless of whether or not it was requested by the client), unless the client sync type state was `clean_all`, in which case a `clean_all` sync type is performed. +2. If the server responded that it is performing a `clean_all` sync, a `clean all` is performed (regardless of whether or not it was requested by the client) +3. Otherwise, a normal sync is performed + +A client that has a `clean` or `clean_all` sync type state set will continue to request a clean sync until it is satisfied by the server. If a client has requested a clean sync, but the server has not responded that it will perform a clean sync, then the client will not delete any rules before applying the new rules received from the server. + +If the deprecated [Preflight Response](#preflight-response) key `clean_sync` is set, it is treated as if the `sync_type` key were set to `clean`. This is a change in behavior to what was previously performed in that not all rules are dropped anymore, only non-transitive rules. Servers should stop using the `clean_sync` key and migrate to using the `sync_type` key. + +## EventUpload After the `preflight` stage has completed the client then initiates the `eventupload` stage if it has any events to upload. If there aren't any events @@ -170,14 +188,14 @@ sequenceDiagram server -->> client: eventupload response ``` -#### `eventupload` Request +### `eventupload` Request | Key | Required | Type | Meaning | Example Value | |---|---|---|---|---| | events | YES | list of event objects | list of events to upload | see example payload | -##### Event Objects +#### Event Objects :information_source: Events are explained in more depth in the [Events page](../concepts/events.md). @@ -211,7 +229,7 @@ sequenceDiagram | signing_id | NO | string | Signing ID of the binary that was executed | "EQHXZ8M8AV:com.google.Chrome" | | team_id | NO | string | Team ID of the binary that was executed | "EQHXZ8M8AV" | -##### Signing Chain Objects +#### Signing Chain Objects | Key | Required | Type | Meaning | Example Value | |---|---|---|---|---| @@ -223,7 +241,7 @@ sequenceDiagram | valid_until | YES | int | Unix timestamp of when the cert expires | 1678983513 | -##### `eventupload` Request Example Payload +#### `eventupload` Request Example Payload ```json { @@ -283,7 +301,7 @@ sequenceDiagram } ``` -#### `eventupload` Response +### `eventupload` Response The server should reply with an HTTP 200 if the request was successfully received and processed. @@ -292,7 +310,7 @@ The server should reply with an HTTP 200 if the request was successfully receive |---|---|---|---|---| | event_upload_bundle_binaries | NO | list of strings | An array of bundle hashes that the sync server needs to be uploaded | ["8621d92262aef379d3cfe9e099f287be5b996a281995b5cc64932f7d62f3dc85"] | -##### `eventupload` Response Example Payload +#### `eventupload` Response Example Payload ```json @@ -301,7 +319,7 @@ The server should reply with an HTTP 200 if the request was successfully receive } ``` -### Rule Download +## Rule Download After events have been uploaded to the sync server, the `ruledownload` stage begins in a full sync. @@ -321,7 +339,7 @@ Santa applies rules idempotently and is designed to receive rules multiple times One caveat to be aware of is that when a clean sync is requested in the `preflight` stage, the client expects that at least one rule will be sent by the sync service in the `ruledownload` stage. If no rules are sent then the client is expected to keep its old set of rules prior to the client or server requesting a clean sync and the client will continue to request a clean sync on all subsequent syncs until a successful sync completes that includes at least one rule. -#### `ruledownload` Request +### `ruledownload` Request This stage is initiated via an HTTP POST request to the URL `/ruledownload/` @@ -330,7 +348,7 @@ One caveat to be aware of is that when a clean sync is requested in the `preflig | cursor | NO | string | a field used by the sync server to indicate where the next batch of rules should start | -##### `ruledownload` Request Example Payload +#### `ruledownload` Request Example Payload On the first request the payload is an empty dictionary @@ -346,7 +364,7 @@ On subsequent requests to the server the `cursor` field is sent with the value f {"cursor":"CpgBChcKCnVwZGF0ZWRfZHQSCQjh94a58uLlAhJ5ahVzfmdvb2dsZS5jb206YXBwbm90aHJyYAsSCUJsb2NrYWJsZSJAMTczOThkYWQzZDAxZGRmYzllMmEwYjBiMWQxYzQyMjY1OWM2ZjA3YmU1MmY3ZjQ1OTVmNDNlZjRhZWI5MGI4YQwLEgRSdWxlGICA8MvA0tIJDBgAIAA="} ``` -#### `ruledownload` Response +### `ruledownload` Response When a `ruledownload` request is received, the sync server responds with a JSON object containing a list of rule objects and a cursor so the client can resume @@ -357,7 +375,7 @@ downloading if the rules need to be downloaded in multiple batches. | cursor | NO | string | Used to continue a rule download in a future request | | rules | YES | list of Rule objects | List of rule objects (see next section). | -##### Rules Objects +#### Rules Objects | Key | Required | Type | Meaning | Example Value | @@ -372,7 +390,7 @@ downloading if the rules need to be downloaded in multiple batches. | file\_bundle\_hash | NO | string | The SHA256 of all binaries in a bundle | "7466e3687f540bcb7792c6d14d5a186667dbe18a85021857b42effe9f0370805" | -##### Example `ruledownload` Response Payload +#### Example `ruledownload` Response Payload ```json { @@ -400,7 +418,7 @@ downloading if the rules need to be downloaded in multiple batches. } ``` -### Postflight +## Postflight The postflight stage is used for the client to inform the sync server that it has successfully finished syncing. After sending the request, the client is expected to update its internal state applying any configuration changes sent by the sync server during the preflight step. @@ -412,7 +430,7 @@ sequenceDiagram server -->> client: postflight response ``` -#### `postflight` Request +### `postflight` Request The request consists of the following JSON keys: @@ -421,7 +439,7 @@ The request consists of the following JSON keys: | rules_received | YES | int | The number of rules the client received from all ruledownlaod requests. | 211 | | rules_processed | YES | int | The number of rules that were processed from all ruledownload requests. | 212 | -### Example postflight request JSON Payload: +#### Example postflight request JSON Payload: ```json { @@ -431,7 +449,7 @@ The request consists of the following JSON keys: ``` -#### `postflight` Response +### `postflight` Response The server should reply with an HTTP 200 if the request was successfully received and processed. Any message body is ignored by the client.