Skip to content

Commit

Permalink
Sync clean all (#1275)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mlw authored Jan 24, 2024
1 parent f4ad76b commit 70474ab
Show file tree
Hide file tree
Showing 39 changed files with 629 additions and 155 deletions.
3 changes: 2 additions & 1 deletion Fuzzing/santad/src/databaseRuleAddRules.mm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#import <MOLXPCConnection/MOLXPCConnection.h>

#import "SNTCommandController.h"
#import "SNTCommonEnums.h"
#import "SNTRule.h"
#import "SNTXPCControlInterface.h"

Expand Down Expand Up @@ -58,7 +59,7 @@
[daemonConn resume];
[[daemonConn remoteObjectProxy]
databaseRuleAddRules:@[ newRule ]
cleanSlate:NO
ruleCleanup:SNTRuleCleanupNone
reply:^(NSError *error) {
if (!error) {
if (newRule.state == SNTRuleStateRemove) {
Expand Down
11 changes: 11 additions & 0 deletions Source/common/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions Source/common/SNTCommonEnums.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions Source/common/SNTConfigurator.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
90 changes: 72 additions & 18 deletions Source/common/SNTConfigurator.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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];
Expand All @@ -184,7 +197,8 @@ - (instancetype)init {
kRemountUSBModeKey : array,
kFullSyncLastSuccess : date,
kRuleSyncLastSuccess : date,
kSyncCleanRequired : number,
kSyncCleanRequiredDeprecated : number,
kSyncTypeRequired : number,
kEnableAllEventUploadKey : number,
kOverrideFileAccessActionKey : string,
};
Expand Down Expand Up @@ -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];
}
Expand Down Expand Up @@ -432,7 +456,7 @@ + (NSSet *)keyPathsForValuesAffectingRuleSyncLastSuccess {
return [self syncStateSet];
}

+ (NSSet *)keyPathsForValuesAffectingSyncCleanRequired {
+ (NSSet *)keyPathsForValuesAffectingSyncTypeRequired {
return [self syncStateSet];
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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];
}

Expand Down
102 changes: 102 additions & 0 deletions Source/common/SNTConfiguratorTest.m
Original file line number Diff line number Diff line change
@@ -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 <Foundation/Foundation.h>
#import <XCTest/XCTest.h>

#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
3 changes: 2 additions & 1 deletion Source/common/SNTRuleTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
/// limitations under the License.

#import <XCTest/XCTest.h>
#include "Source/common/SNTCommonEnums.h"

#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTSyncConstants.h"

#import "Source/common/SNTRule.h"
Expand Down
3 changes: 2 additions & 1 deletion Source/common/SNTSyncConstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion Source/common/SNTSyncConstants.m
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
4 changes: 2 additions & 2 deletions Source/common/SNTXPCControlInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion Source/common/SNTXPCControlInterface.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down
Loading

0 comments on commit 70474ab

Please sign in to comment.