mirror of
https://github.com/google/santa.git
synced 2026-01-16 09:48:01 -05:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4c0d56bb6 | ||
|
|
908b1bcabe | ||
|
|
64e81bedc6 | ||
|
|
5dfab22fa7 | ||
|
|
5248e2a7eb | ||
|
|
e8db89c57c | ||
|
|
70474aba3e | ||
|
|
f4ad76b974 | ||
|
|
3b7061ea62 | ||
|
|
280d93ee08 | ||
|
|
f73463117f | ||
|
|
f93e1a56a0 | ||
|
|
d5195b55d2 | ||
|
|
15e5874d43 | ||
|
|
5e6fa09f1c | ||
|
|
ce2777ae94 | ||
|
|
f8a20d35b4 | ||
|
|
2e69370524 | ||
|
|
f9b4e00e0c | ||
|
|
e2e83a099c | ||
|
|
2cbf15566a | ||
|
|
1596990c65 | ||
|
|
221664436f | ||
|
|
65c660298c | ||
|
|
2b5d55781c | ||
|
|
84e6d6ccff | ||
|
|
c16f90f5f9 | ||
|
|
d503eae4d9 | ||
|
|
818518bb38 | ||
|
|
f499654951 | ||
|
|
a5e8d77d06 | ||
|
|
edac42e8b8 | ||
|
|
ce5e3d0ee4 | ||
|
|
3e51ec6b8a | ||
|
|
ed227f43d4 | ||
|
|
056ed75bf1 |
9
.github/workflows/check-markdown.yml
vendored
9
.github/workflows/check-markdown.yml
vendored
@@ -9,6 +9,9 @@ jobs:
|
||||
markdown-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
- run: "! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'"
|
||||
- name: "Checkout Santa"
|
||||
uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # ratchet:actions/checkout@master
|
||||
- name: "Check for deadlinks"
|
||||
uses: gaurav-nelson/github-action-markdown-link-check@5c5dfc0ac2e225883c0e5f03a85311ec2830d368 # ratchet:gaurav-nelson/github-action-markdown-link-check@v1
|
||||
- name: "Check for trailing whitespace and newlines"
|
||||
run: "! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'"
|
||||
|
||||
17
.github/workflows/e2e.yml
vendored
17
.github/workflows/e2e.yml
vendored
@@ -23,20 +23,23 @@ jobs:
|
||||
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 sync santa
|
||||
run: |
|
||||
bazel run :reload --define=SANTA_BUILD_TYPE=adhoc
|
||||
bazel run //Testing/integration:allow_sysex
|
||||
- name: Test config changes
|
||||
run: ./Testing/integration/test_config_changes.sh
|
||||
- name: Build, install, and start moroz
|
||||
run: |
|
||||
bazel build @com_github_groob_moroz//cmd/moroz:moroz
|
||||
cp bazel-bin/external/com_github_groob_moroz/cmd/moroz/moroz_/moroz /tmp/moroz
|
||||
/tmp/moroz -configs="$GITHUB_WORKSPACE/Testing/integration/configs/moroz_default/global.toml" -use-tls=false &
|
||||
- name: Build, install, and sync santa
|
||||
run: |
|
||||
bazel run :reload --define=SANTA_BUILD_TYPE=adhoc
|
||||
bazel run //Testing/integration:allow_sysex
|
||||
sudo santactl sync --debug
|
||||
- name: Run integration test binaries
|
||||
run: bazel test //Testing/integration:integration_tests
|
||||
- name: Test config changes
|
||||
run: ./Testing/integration/test_config_changes.sh
|
||||
run: |
|
||||
bazel test //Testing/integration:integration_tests
|
||||
sleep 3
|
||||
bazel run //Testing/integration:dismiss_santa_popup || true
|
||||
- name: Test sync server changes
|
||||
run: ./Testing/integration/test_sync_changes.sh
|
||||
- name: Test USB blocking
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
|
||||
#import "SNTCommandController.h"
|
||||
#import "SNTCommonEnums.h"
|
||||
#import "SNTRule.h"
|
||||
#import "SNTXPCControlInterface.h"
|
||||
|
||||
@@ -58,7 +59,7 @@ extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size
|
||||
[daemonConn resume];
|
||||
[[daemonConn remoteObjectProxy]
|
||||
databaseRuleAddRules:@[ newRule ]
|
||||
cleanSlate:NO
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
reply:^(NSError *error) {
|
||||
if (!error) {
|
||||
if (newRule.state == SNTRuleStateRemove) {
|
||||
|
||||
@@ -40,6 +40,12 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTDeepCopy",
|
||||
srcs = ["SNTDeepCopy.m"],
|
||||
hdrs = ["SNTDeepCopy.h"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "SantaCache",
|
||||
hdrs = ["SantaCache.h"],
|
||||
@@ -54,6 +60,56 @@ santa_unit_test(
|
||||
],
|
||||
)
|
||||
|
||||
# This target shouldn't be used directly.
|
||||
# Use a more specific scoped type instead.
|
||||
objc_library(
|
||||
name = "ScopedTypeRef",
|
||||
hdrs = ["ScopedTypeRef.h"],
|
||||
visibility = ["//Source/common:__pkg__"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "ScopedCFTypeRef",
|
||||
hdrs = ["ScopedCFTypeRef.h"],
|
||||
deps = [
|
||||
":ScopedTypeRef",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "ScopedCFTypeRefTest",
|
||||
srcs = ["ScopedCFTypeRefTest.mm"],
|
||||
sdk_frameworks = [
|
||||
"Security",
|
||||
],
|
||||
deps = [
|
||||
":ScopedCFTypeRef",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "ScopedIOObjectRef",
|
||||
hdrs = ["ScopedIOObjectRef.h"],
|
||||
sdk_frameworks = [
|
||||
"IOKit",
|
||||
],
|
||||
deps = [
|
||||
":ScopedTypeRef",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "ScopedIOObjectRefTest",
|
||||
srcs = ["ScopedIOObjectRefTest.mm"],
|
||||
sdk_frameworks = [
|
||||
"IOKit",
|
||||
],
|
||||
deps = [
|
||||
":ScopedIOObjectRef",
|
||||
"//Source/santad:EndpointSecuritySerializerUtilities",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "BranchPrediction",
|
||||
hdrs = ["BranchPrediction.h"],
|
||||
@@ -421,17 +477,30 @@ 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",
|
||||
":SNTRuleTest",
|
||||
":SantaCacheTest",
|
||||
":ScopedCFTypeRefTest",
|
||||
":ScopedIOObjectRefTest",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
@@ -74,6 +74,11 @@ class PrefixTree {
|
||||
node_count_ = 0;
|
||||
}
|
||||
|
||||
uint32_t NodeCount() {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
return node_count_;
|
||||
}
|
||||
|
||||
#if SANTA_PREFIX_TREE_DEBUG
|
||||
void Print() {
|
||||
char buf[max_depth_ + 1];
|
||||
@@ -82,11 +87,6 @@ class PrefixTree {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
PrintLocked(root_, buf, 0);
|
||||
}
|
||||
|
||||
uint32_t NodeCount() {
|
||||
absl::ReaderMutexLock lock(&lock_);
|
||||
return node_count_;
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
@property NSArray<MOLCertificate *> *certChain;
|
||||
@property NSString *teamID;
|
||||
@property NSString *signingID;
|
||||
@property NSDictionary *entitlements;
|
||||
@property BOOL entitlementsFiltered;
|
||||
|
||||
@property NSString *quarantineURL;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
///
|
||||
/// Enable Fail Close mode. Defaults to NO.
|
||||
/// This controls Santa's behavior when a failure occurs, such as an
|
||||
/// inability to read a file. By default, to prevent bugs or misconfiguration
|
||||
/// inability to read a file and as a default response when deadlines
|
||||
/// are about to expire. By default, to prevent bugs or misconfiguration
|
||||
/// from rendering a machine inoperable Santa will fail open and allow
|
||||
/// execution. With this setting enabled, Santa will fail closed if the client
|
||||
/// is in LOCKDOWN mode, offering a higher level of security but with a higher
|
||||
@@ -284,7 +285,7 @@
|
||||
|
||||
///
|
||||
/// Enabling this appends the Santa machine ID to the end of each log line. If nothing
|
||||
/// has been overriden, this is the host's UUID.
|
||||
/// has been overridden, this is the host's UUID.
|
||||
/// Defaults to NO.
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableMachineIDDecoration;
|
||||
@@ -437,9 +438,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
|
||||
|
||||
@@ -449,7 +450,7 @@
|
||||
@property(nonatomic) BOOL blockUSBMount;
|
||||
|
||||
///
|
||||
/// Comma-seperated `$ mount -o` arguments used for forced remounting of USB devices. Default
|
||||
/// Comma-separated `$ mount -o` arguments used for forced remounting of USB devices. Default
|
||||
/// to fully allow/deny without remounting if unset.
|
||||
///
|
||||
@property(nonatomic) NSArray<NSString *> *remountUSBMode;
|
||||
@@ -642,6 +643,18 @@
|
||||
///
|
||||
@property(readonly, nonatomic) NSUInteger metricExportTimeout;
|
||||
|
||||
///
|
||||
/// List of prefix strings for which individual entitlement keys with a matching
|
||||
/// prefix should not be logged.
|
||||
///
|
||||
@property(readonly, nonatomic) NSArray<NSString *> *entitlementsPrefixFilter;
|
||||
|
||||
///
|
||||
/// List of TeamIDs for which entitlements should not be logged. Use the string
|
||||
/// "platform" to refer to platform binaries.
|
||||
///
|
||||
@property(readonly, nonatomic) NSArray<NSString *> *entitlementsTeamIDFilter;
|
||||
|
||||
///
|
||||
/// Retrieve an initialized singleton configurator object using the default file path.
|
||||
///
|
||||
|
||||
@@ -20,6 +20,21 @@
|
||||
#import "Source/common/SNTStrengthify.h"
|
||||
#import "Source/common/SNTSystemInfo.h"
|
||||
|
||||
// Ensures the given object is an NSArray and only contains NSString value types
|
||||
static NSArray<NSString *> *EnsureArrayOfStrings(id obj) {
|
||||
if (![obj isKindOfClass:[NSArray class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
for (id item in obj) {
|
||||
if (![item isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@interface SNTConfigurator ()
|
||||
/// A NSUserDefaults object set to use the com.google.santa suite.
|
||||
@property(readonly, nonatomic) NSUserDefaults *defaults;
|
||||
@@ -38,6 +53,9 @@
|
||||
/// 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
|
||||
@@ -88,6 +106,8 @@ static NSString *const kModeNotificationLockdown = @"ModeNotificationLockdown";
|
||||
|
||||
static NSString *const kEnablePageZeroProtectionKey = @"EnablePageZeroProtection";
|
||||
static NSString *const kEnableBadSignatureProtectionKey = @"EnableBadSignatureProtection";
|
||||
static NSString *const kFailClosedKey = @"FailClosed";
|
||||
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
|
||||
|
||||
static NSString *const kFileChangesRegexKey = @"FileChangesRegex";
|
||||
static NSString *const kFileChangesPrefixFiltersKey = @"FileChangesPrefixFilters";
|
||||
@@ -116,21 +136,10 @@ static NSString *const kFCMProject = @"FCMProject";
|
||||
static NSString *const kFCMEntity = @"FCMEntity";
|
||||
static NSString *const kFCMAPIKey = @"FCMAPIKey";
|
||||
|
||||
// The keys managed by a sync server or mobileconfig.
|
||||
static NSString *const kClientModeKey = @"ClientMode";
|
||||
static NSString *const kFailClosedKey = @"FailClosed";
|
||||
static NSString *const kBlockUSBMountKey = @"BlockUSBMount";
|
||||
static NSString *const kRemountUSBModeKey = @"RemountUSBMode";
|
||||
static NSString *const kEntitlementsPrefixFilterKey = @"EntitlementsPrefixFilter";
|
||||
static NSString *const kEntitlementsTeamIDFilterKey = @"EntitlementsTeamIDFilter";
|
||||
|
||||
static NSString *const kOnStartUSBOptions = @"OnStartUSBOptions";
|
||||
static NSString *const kEnableTransitiveRulesKey = @"EnableTransitiveRules";
|
||||
static NSString *const kEnableTransitiveRulesKeyDeprecated = @"EnableTransitiveWhitelisting";
|
||||
static NSString *const kAllowedPathRegexKey = @"AllowedPathRegex";
|
||||
static NSString *const kAllowedPathRegexKeyDeprecated = @"WhitelistRegex";
|
||||
static NSString *const kBlockedPathRegexKey = @"BlockedPathRegex";
|
||||
static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
|
||||
static NSString *const kEnableAllEventUploadKey = @"EnableAllEventUpload";
|
||||
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
|
||||
static NSString *const kOverrideFileAccessActionKey = @"OverrideFileAccessAction";
|
||||
|
||||
static NSString *const kMetricFormat = @"MetricFormat";
|
||||
static NSString *const kMetricURL = @"MetricURL";
|
||||
@@ -138,12 +147,35 @@ static NSString *const kMetricExportInterval = @"MetricExportInterval";
|
||||
static NSString *const kMetricExportTimeout = @"MetricExportTimeout";
|
||||
static NSString *const kMetricExtraLabels = @"MetricExtraLabels";
|
||||
|
||||
// The keys managed by a sync server or mobileconfig.
|
||||
static NSString *const kClientModeKey = @"ClientMode";
|
||||
static NSString *const kBlockUSBMountKey = @"BlockUSBMount";
|
||||
static NSString *const kRemountUSBModeKey = @"RemountUSBMode";
|
||||
static NSString *const kEnableTransitiveRulesKey = @"EnableTransitiveRules";
|
||||
static NSString *const kEnableTransitiveRulesKeyDeprecated = @"EnableTransitiveWhitelisting";
|
||||
static NSString *const kAllowedPathRegexKey = @"AllowedPathRegex";
|
||||
static NSString *const kAllowedPathRegexKeyDeprecated = @"WhitelistRegex";
|
||||
static NSString *const kBlockedPathRegexKey = @"BlockedPathRegex";
|
||||
static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
|
||||
static NSString *const kEnableAllEventUploadKey = @"EnableAllEventUpload";
|
||||
static NSString *const kOverrideFileAccessActionKey = @"OverrideFileAccessAction";
|
||||
|
||||
// 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];
|
||||
@@ -165,7 +197,8 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kRemountUSBModeKey : array,
|
||||
kFullSyncLastSuccess : date,
|
||||
kRuleSyncLastSuccess : date,
|
||||
kSyncCleanRequired : number,
|
||||
kSyncCleanRequiredDeprecated : number,
|
||||
kSyncTypeRequired : number,
|
||||
kEnableAllEventUploadKey : number,
|
||||
kOverrideFileAccessActionKey : string,
|
||||
};
|
||||
@@ -240,12 +273,24 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kEnableAllEventUploadKey : number,
|
||||
kDisableUnknownEventUploadKey : number,
|
||||
kOverrideFileAccessActionKey : string,
|
||||
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];
|
||||
}
|
||||
@@ -411,7 +456,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self syncStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingSyncCleanRequired {
|
||||
+ (NSSet *)keyPathsForValuesAffectingSyncTypeRequired {
|
||||
return [self syncStateSet];
|
||||
}
|
||||
|
||||
@@ -527,6 +572,14 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self syncAndConfigStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEntitlementsPrefixFilter {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEntitlementsTeamIDFilter {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
#pragma mark Public Interface
|
||||
|
||||
- (SNTClientMode)clientMode {
|
||||
@@ -551,8 +604,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
|
||||
- (BOOL)failClosed {
|
||||
NSNumber *n = self.configState[kFailClosedKey];
|
||||
if (n) return [n boolValue];
|
||||
return NO;
|
||||
return [n boolValue] && self.clientMode == SNTClientModeLockdown;
|
||||
}
|
||||
|
||||
- (BOOL)enableTransitiveRules {
|
||||
@@ -794,12 +846,12 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
[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 {
|
||||
@@ -1071,12 +1123,12 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
/// 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;
|
||||
@@ -1086,24 +1138,54 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -1111,6 +1193,14 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
self.syncState = [NSMutableDictionary dictionary];
|
||||
}
|
||||
|
||||
- (NSArray *)entitlementsPrefixFilter {
|
||||
return EnsureArrayOfStrings(self.configState[kEntitlementsPrefixFilterKey]);
|
||||
}
|
||||
|
||||
- (NSArray *)entitlementsTeamIDFilter {
|
||||
return EnsureArrayOfStrings(self.configState[kEntitlementsTeamIDFilterKey]);
|
||||
}
|
||||
|
||||
#pragma mark Private Defaults Methods
|
||||
|
||||
- (NSRegularExpression *)expressionForPattern:(NSString *)pattern {
|
||||
|
||||
102
Source/common/SNTConfiguratorTest.m
Normal file
102
Source/common/SNTConfiguratorTest.m
Normal file
@@ -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
|
||||
27
Source/common/SNTDeepCopy.h
Normal file
27
Source/common/SNTDeepCopy.h
Normal file
@@ -0,0 +1,27 @@
|
||||
/// Copyright 2023 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>
|
||||
|
||||
@interface NSArray (SNTDeepCopy)
|
||||
|
||||
- (instancetype)sntDeepCopy;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSDictionary (SNTDeepCopy)
|
||||
|
||||
- (instancetype)sntDeepCopy;
|
||||
|
||||
@end
|
||||
53
Source/common/SNTDeepCopy.m
Normal file
53
Source/common/SNTDeepCopy.m
Normal file
@@ -0,0 +1,53 @@
|
||||
/// Copyright 2023 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTDeepCopy.h"
|
||||
|
||||
@implementation NSArray (SNTDeepCopy)
|
||||
|
||||
- (instancetype)sntDeepCopy {
|
||||
NSMutableArray<__kindof NSObject *> *deepCopy = [NSMutableArray arrayWithCapacity:self.count];
|
||||
for (id object in self) {
|
||||
if ([object respondsToSelector:@selector(sntDeepCopy)]) {
|
||||
[deepCopy addObject:[object sntDeepCopy]];
|
||||
} else if ([object respondsToSelector:@selector(copyWithZone:)]) {
|
||||
[deepCopy addObject:[object copy]];
|
||||
} else {
|
||||
[deepCopy addObject:object];
|
||||
}
|
||||
}
|
||||
return deepCopy;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSDictionary (SNTDeepCopy)
|
||||
|
||||
- (instancetype)sntDeepCopy {
|
||||
NSMutableDictionary<__kindof NSObject *, __kindof NSObject *> *deepCopy =
|
||||
[NSMutableDictionary dictionary];
|
||||
for (id key in self) {
|
||||
id value = self[key];
|
||||
if ([value respondsToSelector:@selector(sntDeepCopy)]) {
|
||||
deepCopy[key] = [value sntDeepCopy];
|
||||
} else if ([value respondsToSelector:@selector(copyWithZone:)]) {
|
||||
deepCopy[key] = [value copy];
|
||||
} else {
|
||||
deepCopy[key] = value;
|
||||
}
|
||||
}
|
||||
return deepCopy;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -15,7 +15,7 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// The callback type when KVO notifications are received for observed key paths.
|
||||
// The first parameter is the previous value, the second paramter is the new value.
|
||||
// The first parameter is the previous value, the second parameter is the new value.
|
||||
typedef void (^KVOCallback)(id oldValue, id newValue);
|
||||
|
||||
@interface SNTKVOManager : NSObject
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -32,7 +32,8 @@ NSString *const kBlockUSBMount = @"block_usb_mount";
|
||||
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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -50,7 +50,7 @@ NSString *const kBundleID = @"com.google.santa.daemon";
|
||||
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];
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -68,9 +68,11 @@
|
||||
- (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;
|
||||
- (void)remountUSBMode:(void (^)(NSArray<NSString *> *))reply;
|
||||
|
||||
///
|
||||
/// Metrics ops
|
||||
|
||||
@@ -245,7 +245,7 @@ struct S {
|
||||
uint64_t first_val;
|
||||
uint64_t second_val;
|
||||
|
||||
bool operator==(const S &rhs) {
|
||||
bool operator==(const S &rhs) const {
|
||||
return first_val == rhs.first_val && second_val == rhs.second_val;
|
||||
}
|
||||
};
|
||||
|
||||
29
Source/common/ScopedCFTypeRef.h
Normal file
29
Source/common/ScopedCFTypeRef.h
Normal file
@@ -0,0 +1,29 @@
|
||||
/// Copyright 2023 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.
|
||||
|
||||
#ifndef SANTA__COMMON__SCOPEDCFTYPEREF_H
|
||||
#define SANTA__COMMON__SCOPEDCFTYPEREF_H
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
#include "Source/common/ScopedTypeRef.h"
|
||||
|
||||
namespace santa::common {
|
||||
|
||||
template <typename CFT>
|
||||
using ScopedCFTypeRef = ScopedTypeRef<CFT, (CFT)NULL, CFRetain, CFRelease>;
|
||||
|
||||
} // namespace santa::common
|
||||
|
||||
#endif
|
||||
141
Source/common/ScopedCFTypeRefTest.mm
Normal file
141
Source/common/ScopedCFTypeRefTest.mm
Normal file
@@ -0,0 +1,141 @@
|
||||
/// Copyright 2023 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.
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <Security/Security.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include "XCTest/XCTest.h"
|
||||
|
||||
#include "Source/common/ScopedCFTypeRef.h"
|
||||
|
||||
using santa::common::ScopedCFTypeRef;
|
||||
|
||||
@interface ScopedCFTypeRefTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation ScopedCFTypeRefTest
|
||||
|
||||
- (void)testDefaultConstruction {
|
||||
// Default construction creates wraps a NULL object
|
||||
ScopedCFTypeRef<CFNumberRef> scopedRef;
|
||||
XCTAssertFalse(scopedRef.Unsafe());
|
||||
}
|
||||
|
||||
- (void)testOperatorBool {
|
||||
// Operator bool is `false` when object is null
|
||||
{
|
||||
ScopedCFTypeRef<CFNumberRef> scopedNullRef;
|
||||
XCTAssertFalse(scopedNullRef.Unsafe());
|
||||
XCTAssertFalse(scopedNullRef);
|
||||
}
|
||||
|
||||
// Operator bool is `true` when object is NOT null
|
||||
{
|
||||
int x = 123;
|
||||
CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &x);
|
||||
|
||||
ScopedCFTypeRef<CFNumberRef> scopedNumRef = ScopedCFTypeRef<CFNumberRef>::Assume(numRef);
|
||||
XCTAssertTrue(scopedNumRef.Unsafe());
|
||||
XCTAssertTrue(scopedNumRef);
|
||||
}
|
||||
}
|
||||
|
||||
// Note that CFMutableArray is used for testing, even when subtypes aren't
|
||||
// needed, because it is never optimized into immortal constant values, unlike
|
||||
// other types.
|
||||
- (void)testAssume {
|
||||
int want = 123;
|
||||
int got = 0;
|
||||
CFMutableArrayRef array = CFArrayCreateMutable(nullptr, /*capacity=*/0, &kCFTypeArrayCallBacks);
|
||||
|
||||
// Baseline state, initial retain count is 1 after object creation
|
||||
XCTAssertEqual(1, CFGetRetainCount(array));
|
||||
|
||||
CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &want);
|
||||
CFArrayAppendValue(array, numRef);
|
||||
CFRelease(numRef);
|
||||
|
||||
XCTAssertEqual(1, CFArrayGetCount(array));
|
||||
|
||||
{
|
||||
ScopedCFTypeRef<CFMutableArrayRef> scopedArray =
|
||||
ScopedCFTypeRef<CFMutableArrayRef>::Assume(array);
|
||||
|
||||
// Ensure ownership was taken, and retain count remains unchanged
|
||||
XCTAssertTrue(scopedArray.Unsafe());
|
||||
XCTAssertEqual(1, CFGetRetainCount(scopedArray.Unsafe()));
|
||||
|
||||
// Make sure the object contains expected contents
|
||||
CFMutableArrayRef ref = scopedArray.Unsafe();
|
||||
XCTAssertEqual(1, CFArrayGetCount(ref));
|
||||
XCTAssertTrue(
|
||||
CFNumberGetValue((CFNumberRef)CFArrayGetValueAtIndex(ref, 0), kCFNumberIntType, &got));
|
||||
XCTAssertEqual(want, got);
|
||||
}
|
||||
}
|
||||
|
||||
// Note that CFMutableArray is used for testing, even when subtypes aren't
|
||||
// needed, because it is never optimized into immortal constant values, unlike
|
||||
// other types.
|
||||
- (void)testRetain {
|
||||
int want = 123;
|
||||
int got = 0;
|
||||
CFMutableArrayRef array = CFArrayCreateMutable(nullptr, /*capacity=*/0, &kCFTypeArrayCallBacks);
|
||||
|
||||
// Baseline state, initial retain count is 1 after object creation
|
||||
XCTAssertEqual(1, CFGetRetainCount(array));
|
||||
|
||||
CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &want);
|
||||
CFArrayAppendValue(array, numRef);
|
||||
CFRelease(numRef);
|
||||
|
||||
XCTAssertEqual(1, CFArrayGetCount(array));
|
||||
|
||||
{
|
||||
ScopedCFTypeRef<CFMutableArrayRef> scopedArray =
|
||||
ScopedCFTypeRef<CFMutableArrayRef>::Retain(array);
|
||||
|
||||
// Ensure ownership was taken, and retain count was incremented
|
||||
XCTAssertTrue(scopedArray.Unsafe());
|
||||
XCTAssertEqual(2, CFGetRetainCount(scopedArray.Unsafe()));
|
||||
|
||||
// Make sure the object contains expected contents
|
||||
CFMutableArrayRef ref = scopedArray.Unsafe();
|
||||
XCTAssertEqual(1, CFArrayGetCount(ref));
|
||||
XCTAssertTrue(
|
||||
CFNumberGetValue((CFNumberRef)CFArrayGetValueAtIndex(ref, 0), kCFNumberIntType, &got));
|
||||
XCTAssertEqual(want, got);
|
||||
}
|
||||
|
||||
// The original `array` object should still be valid due to the extra retain.
|
||||
// Ensure the retain count has decreased since `scopedArray` went out of scope
|
||||
XCTAssertEqual(1, CFArrayGetCount(array));
|
||||
}
|
||||
|
||||
- (void)testInto {
|
||||
ScopedCFTypeRef<CFURLRef> scopedURLRef =
|
||||
ScopedCFTypeRef<CFURLRef>::Assume(CFURLCreateWithFileSystemPath(
|
||||
kCFAllocatorDefault, CFSTR("/usr/bin/true"), kCFURLPOSIXPathStyle, YES));
|
||||
|
||||
ScopedCFTypeRef<SecStaticCodeRef> scopedCodeRef;
|
||||
XCTAssertFalse(scopedCodeRef);
|
||||
|
||||
SecStaticCodeCreateWithPath(scopedURLRef.Unsafe(), kSecCSDefaultFlags,
|
||||
scopedCodeRef.InitializeInto());
|
||||
|
||||
// Ensure the scoped object was initialized
|
||||
XCTAssertTrue(scopedCodeRef);
|
||||
}
|
||||
|
||||
@end
|
||||
30
Source/common/ScopedIOObjectRef.h
Normal file
30
Source/common/ScopedIOObjectRef.h
Normal file
@@ -0,0 +1,30 @@
|
||||
/// Copyright 2023 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.
|
||||
|
||||
#ifndef SANTA__COMMON__SCOPEDIOOBJECTREF_H
|
||||
#define SANTA__COMMON__SCOPEDIOOBJECTREF_H
|
||||
|
||||
#include <IOKit/IOKitLib.h>
|
||||
|
||||
#include "Source/common/ScopedTypeRef.h"
|
||||
|
||||
namespace santa::common {
|
||||
|
||||
template <typename IOT>
|
||||
using ScopedIOObjectRef =
|
||||
ScopedTypeRef<IOT, (IOT)IO_OBJECT_NULL, IOObjectRetain, IOObjectRelease>;
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
104
Source/common/ScopedIOObjectRefTest.mm
Normal file
104
Source/common/ScopedIOObjectRefTest.mm
Normal file
@@ -0,0 +1,104 @@
|
||||
/// Copyright 2023 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.
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <IOKit/IOKitLib.h>
|
||||
#include <IOKit/usb/IOUSBLib.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#include "Source/common/ScopedIOObjectRef.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
|
||||
|
||||
using santa::common::ScopedIOObjectRef;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::GetDefaultIOKitCommsPort;
|
||||
|
||||
@interface ScopedIOObjectRefTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation ScopedIOObjectRefTest
|
||||
|
||||
- (void)testDefaultConstruction {
|
||||
// Default construction creates wraps a NULL object
|
||||
ScopedIOObjectRef<io_object_t> scopedRef;
|
||||
XCTAssertFalse(scopedRef.Unsafe());
|
||||
}
|
||||
|
||||
- (void)testOperatorBool {
|
||||
// Operator bool is `false` when object is null
|
||||
{
|
||||
ScopedIOObjectRef<io_object_t> scopedNullRef;
|
||||
XCTAssertFalse(scopedNullRef.Unsafe());
|
||||
XCTAssertFalse(scopedNullRef);
|
||||
}
|
||||
|
||||
// Operator bool is `true` when object is NOT null
|
||||
{
|
||||
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
|
||||
XCTAssertNotEqual((CFMutableDictionaryRef)NULL, matchingDict);
|
||||
|
||||
io_service_t service = IOServiceGetMatchingService(GetDefaultIOKitCommsPort(), matchingDict);
|
||||
|
||||
ScopedIOObjectRef<io_service_t> scopedServiceRef =
|
||||
ScopedIOObjectRef<io_service_t>::Assume(service);
|
||||
|
||||
XCTAssertTrue(scopedServiceRef.Unsafe());
|
||||
XCTAssertTrue(scopedServiceRef);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testAssume {
|
||||
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
|
||||
XCTAssertNotEqual((CFMutableDictionaryRef)NULL, matchingDict);
|
||||
|
||||
io_service_t service = IOServiceGetMatchingService(GetDefaultIOKitCommsPort(), matchingDict);
|
||||
|
||||
// Baseline state, initial retain count is 1 after object creation
|
||||
XCTAssertEqual(1, IOObjectGetUserRetainCount(service));
|
||||
XCTAssertNotEqual(IO_OBJECT_NULL, service);
|
||||
|
||||
{
|
||||
ScopedIOObjectRef<io_service_t> scopedIORef = ScopedIOObjectRef<io_service_t>::Assume(service);
|
||||
|
||||
// Ensure ownership was taken, and retain count remains unchanged
|
||||
XCTAssertTrue(scopedIORef.Unsafe());
|
||||
XCTAssertEqual(1, IOObjectGetUserRetainCount(scopedIORef.Unsafe()));
|
||||
XCTAssertNotEqual(IO_OBJECT_NULL, scopedIORef.Unsafe());
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testRetain {
|
||||
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
|
||||
XCTAssertNotEqual((CFMutableDictionaryRef)NULL, matchingDict);
|
||||
|
||||
io_service_t service = IOServiceGetMatchingService(GetDefaultIOKitCommsPort(), matchingDict);
|
||||
|
||||
// Baseline state, initial retain count is 1 after object creation
|
||||
XCTAssertEqual(1, IOObjectGetUserRetainCount(service));
|
||||
XCTAssertNotEqual(IO_OBJECT_NULL, service);
|
||||
|
||||
{
|
||||
ScopedIOObjectRef<io_service_t> scopedIORef = ScopedIOObjectRef<io_service_t>::Retain(service);
|
||||
|
||||
// Ensure ownership was taken, and retain count was incremented
|
||||
XCTAssertTrue(scopedIORef.Unsafe());
|
||||
XCTAssertEqual(2, IOObjectGetUserRetainCount(scopedIORef.Unsafe()));
|
||||
XCTAssertNotEqual(IO_OBJECT_NULL, scopedIORef.Unsafe());
|
||||
}
|
||||
|
||||
// The original `service` object should still be valid due to the extra retain.
|
||||
// Ensure the retain count has decreased since `scopedIORef` went out of scope.
|
||||
XCTAssertEqual(1, IOObjectGetUserRetainCount(service));
|
||||
}
|
||||
|
||||
@end
|
||||
80
Source/common/ScopedTypeRef.h
Normal file
80
Source/common/ScopedTypeRef.h
Normal file
@@ -0,0 +1,80 @@
|
||||
/// Copyright 2023 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.
|
||||
|
||||
#ifndef SANTA__COMMON__SCOPEDTYPEREF_H
|
||||
#define SANTA__COMMON__SCOPEDTYPEREF_H
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <assert.h>
|
||||
|
||||
namespace santa::common {
|
||||
|
||||
template <typename ElementT, ElementT InvalidV, auto RetainFunc,
|
||||
auto ReleaseFunc>
|
||||
class ScopedTypeRef {
|
||||
public:
|
||||
ScopedTypeRef() : object_(InvalidV) {}
|
||||
|
||||
// Can be implemented safely, but not currently needed
|
||||
ScopedTypeRef(ScopedTypeRef&& other) = delete;
|
||||
ScopedTypeRef& operator=(ScopedTypeRef&& rhs) = delete;
|
||||
ScopedTypeRef(const ScopedTypeRef& other) = delete;
|
||||
ScopedTypeRef& operator=(const ScopedTypeRef& other) = delete;
|
||||
|
||||
// Take ownership of a given object
|
||||
static ScopedTypeRef<ElementT, InvalidV, RetainFunc, ReleaseFunc> Assume(
|
||||
ElementT object) {
|
||||
return ScopedTypeRef<ElementT, InvalidV, RetainFunc, ReleaseFunc>(object);
|
||||
}
|
||||
|
||||
// Retain and take ownership of a given object
|
||||
static ScopedTypeRef<ElementT, InvalidV, RetainFunc, ReleaseFunc> Retain(
|
||||
ElementT object) {
|
||||
if (object) {
|
||||
RetainFunc(object);
|
||||
}
|
||||
return ScopedTypeRef<ElementT, InvalidV, RetainFunc, ReleaseFunc>(object);
|
||||
}
|
||||
|
||||
~ScopedTypeRef() {
|
||||
if (object_) {
|
||||
ReleaseFunc(object_);
|
||||
object_ = InvalidV;
|
||||
}
|
||||
}
|
||||
|
||||
explicit operator bool() { return object_ != InvalidV; }
|
||||
|
||||
ElementT Unsafe() { return object_; }
|
||||
|
||||
// This is to be used only to take ownership of objects that are created by
|
||||
// pass-by-pointer create functions. The object must not already be valid.
|
||||
// In non-opt builds, this is enforced by an assert that will terminate the
|
||||
// process.
|
||||
ElementT* InitializeInto() {
|
||||
assert(object_ == InvalidV);
|
||||
return &object_;
|
||||
}
|
||||
|
||||
private:
|
||||
// Not API.
|
||||
// Use Assume or Retain static methods.
|
||||
ScopedTypeRef(ElementT object) : object_(object) {}
|
||||
|
||||
ElementT object_;
|
||||
};
|
||||
|
||||
} // namespace santa::common
|
||||
|
||||
#endif
|
||||
@@ -92,7 +92,11 @@ es_process_t MakeESProcess(es_file_t *file, audit_token_t tok, audit_token_t par
|
||||
}
|
||||
|
||||
uint32_t MaxSupportedESMessageVersionForCurrentOS() {
|
||||
// Note: ES message v3 was only in betas.
|
||||
// Notes:
|
||||
// 1. ES message v3 was only in betas.
|
||||
// 2. Message version 7 appeared in macOS 13.3, but features from that are
|
||||
// not currently used. Leaving off support here so as to not require
|
||||
// adding v7 test JSON files.
|
||||
if (@available(macOS 13.0, *)) {
|
||||
return 6;
|
||||
} else if (@available(macOS 12.3, *)) {
|
||||
|
||||
@@ -213,6 +213,27 @@ message CertificateInfo {
|
||||
optional string common_name = 2;
|
||||
}
|
||||
|
||||
// Information about a single entitlement key/value pair
|
||||
message Entitlement {
|
||||
// The name of an entitlement
|
||||
optional string key = 1;
|
||||
|
||||
// The value of an entitlement
|
||||
optional string value = 2;
|
||||
}
|
||||
|
||||
// Information about entitlements
|
||||
message EntitlementInfo {
|
||||
// Whether or not the set of reported entilements is complete or has been
|
||||
// filtered (e.g. by configuration or clipped because too many to log).
|
||||
optional bool entitlements_filtered = 1;
|
||||
|
||||
// The set of entitlements associated with the target executable
|
||||
// Only top level keys are represented
|
||||
// Values (including nested keys) are JSON serialized
|
||||
repeated Entitlement entitlements = 2;
|
||||
}
|
||||
|
||||
// Information about a process execution event
|
||||
message Execution {
|
||||
// The process that executed the new image (e.g. the process that called
|
||||
@@ -286,6 +307,9 @@ message Execution {
|
||||
// The original path on disk of the target executable
|
||||
// Applies when executables are translocated
|
||||
optional string original_path = 15;
|
||||
|
||||
// Entitlement information about the target executbale
|
||||
optional EntitlementInfo entitlement_info = 16;
|
||||
}
|
||||
|
||||
// Information about a fork event
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" priority="900" constant="112" id="Pec-Pa-4aZ"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="Open Event..." bezelStyle="rounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="X1b-TF-1TL">
|
||||
<buttonCell key="cell" type="push" title="Open..." bezelStyle="rounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="X1b-TF-1TL">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
|
||||
@@ -41,6 +41,7 @@ objc_library(
|
||||
"Commands/SNTCommandFileInfo.m",
|
||||
"Commands/SNTCommandMetrics.h",
|
||||
"Commands/SNTCommandMetrics.m",
|
||||
"Commands/SNTCommandRule.h",
|
||||
"Commands/SNTCommandRule.m",
|
||||
"Commands/SNTCommandStatus.m",
|
||||
"Commands/SNTCommandSync.m",
|
||||
@@ -147,11 +148,27 @@ santa_unit_test(
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTCommandRuleTest",
|
||||
srcs = [
|
||||
"Commands/SNTCommandRule.h",
|
||||
"Commands/SNTCommandRuleTest.mm",
|
||||
"SNTCommand.h",
|
||||
"SNTCommandController.h",
|
||||
],
|
||||
deps = [
|
||||
":santactl_lib",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTRule",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":SNTCommandFileInfoTest",
|
||||
":SNTCommandMetricsTest",
|
||||
":SNTCommandRuleTest",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
20
Source/santactl/Commands/SNTCommandRule.h
Normal file
20
Source/santactl/Commands/SNTCommandRule.h
Normal file
@@ -0,0 +1,20 @@
|
||||
/// 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 "Source/santactl/SNTCommand.h"
|
||||
|
||||
@interface SNTCommandRule : SNTCommand <SNTCommandProtocol>
|
||||
@end
|
||||
@@ -23,12 +23,10 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santactl/Commands/SNTCommandRule.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
|
||||
@interface SNTCommandRule : SNTCommand <SNTCommandProtocol>
|
||||
@end
|
||||
|
||||
@implementation SNTCommandRule
|
||||
|
||||
REGISTER_COMMAND_NAME(@"rule")
|
||||
@@ -253,7 +251,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
databaseRuleAddRules:@[ newRule ]
|
||||
cleanSlate:NO
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
reply:^(NSError *error) {
|
||||
if (error) {
|
||||
printf("Failed to modify rules: %s",
|
||||
@@ -286,72 +284,100 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}];
|
||||
}
|
||||
|
||||
// IMPORTANT: This method makes no attempt to validate whether or not the data
|
||||
// in a rule is valid. It merely constructs a string with the given data.
|
||||
// E.g., TeamID compiler rules are not currently supproted, but if a test rule
|
||||
// is provided with that state, an appropriate string will be returned.
|
||||
+ (NSString *)stringifyRule:(SNTRule *)rule withColor:(BOOL)colorize {
|
||||
NSMutableString *output;
|
||||
// Rule state is saved as eventState for output colorization down below
|
||||
SNTEventState eventState = SNTEventStateUnknown;
|
||||
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateUnknown:
|
||||
output = [@"No rule exists with the given parameters" mutableCopy];
|
||||
break;
|
||||
case SNTRuleStateAllow: OS_FALLTHROUGH;
|
||||
case SNTRuleStateAllowCompiler: OS_FALLTHROUGH;
|
||||
case SNTRuleStateAllowTransitive:
|
||||
output = [@"Allowed" mutableCopy];
|
||||
eventState = SNTEventStateAllow;
|
||||
break;
|
||||
case SNTRuleStateBlock: OS_FALLTHROUGH;
|
||||
case SNTRuleStateSilentBlock:
|
||||
output = [@"Blocked" mutableCopy];
|
||||
eventState = SNTEventStateBlock;
|
||||
break;
|
||||
case SNTRuleStateRemove: OS_FALLTHROUGH;
|
||||
default:
|
||||
output = [NSMutableString stringWithFormat:@"Unexpected rule state: %ld", rule.state];
|
||||
break;
|
||||
}
|
||||
|
||||
if (rule.state == SNTRuleStateUnknown) {
|
||||
// No more output to append
|
||||
return output;
|
||||
}
|
||||
|
||||
[output appendString:@" ("];
|
||||
|
||||
switch (rule.type) {
|
||||
case SNTRuleTypeUnknown: [output appendString:@"Unknown"]; break;
|
||||
case SNTRuleTypeBinary: [output appendString:@"Binary"]; break;
|
||||
case SNTRuleTypeSigningID: [output appendString:@"SigningID"]; break;
|
||||
case SNTRuleTypeCertificate: [output appendString:@"Certificate"]; break;
|
||||
case SNTRuleTypeTeamID: [output appendString:@"TeamID"]; break;
|
||||
default:
|
||||
output = [NSMutableString stringWithFormat:@"Unexpected rule type: %ld", rule.type];
|
||||
break;
|
||||
}
|
||||
|
||||
// Add additional attributes
|
||||
switch (rule.state) {
|
||||
case SNTRuleStateAllowCompiler: [output appendString:@", Compiler"]; break;
|
||||
case SNTRuleStateAllowTransitive: [output appendString:@", Transitive"]; break;
|
||||
case SNTRuleStateSilentBlock: [output appendString:@", Silent"]; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
[output appendString:@")"];
|
||||
|
||||
// Colorize
|
||||
if (colorize) {
|
||||
if ((SNTEventStateAllow & eventState)) {
|
||||
[output insertString:@"\033[32m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else if ((SNTEventStateBlock & eventState)) {
|
||||
[output insertString:@"\033[31m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else {
|
||||
[output insertString:@"\033[33m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
}
|
||||
}
|
||||
|
||||
if (rule.state == SNTRuleStateAllowTransitive) {
|
||||
NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:rule.timestamp];
|
||||
[output appendString:[NSString stringWithFormat:@"\nlast access date: %@", [date description]]];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
- (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)daemonConn {
|
||||
id<SNTDaemonControlXPC> rop = [daemonConn synchronousRemoteObjectProxy];
|
||||
NSString *fileSHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil;
|
||||
NSString *certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil;
|
||||
NSString *teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil;
|
||||
NSString *signingID = (rule.type == SNTRuleTypeSigningID) ? rule.identifier : nil;
|
||||
__block NSMutableString *output;
|
||||
[rop decisionForFilePath:nil
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
reply:^(SNTEventState s) {
|
||||
output =
|
||||
(SNTEventStateAllow & s) ? @"Allowed".mutableCopy : @"Blocked".mutableCopy;
|
||||
switch (s) {
|
||||
case SNTEventStateAllowUnknown:
|
||||
case SNTEventStateBlockUnknown: [output appendString:@" (Unknown)"]; break;
|
||||
case SNTEventStateAllowBinary:
|
||||
case SNTEventStateBlockBinary: [output appendString:@" (Binary)"]; break;
|
||||
case SNTEventStateAllowCertificate:
|
||||
case SNTEventStateBlockCertificate:
|
||||
[output appendString:@" (Certificate)"];
|
||||
break;
|
||||
case SNTEventStateAllowScope:
|
||||
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
|
||||
case SNTEventStateAllowCompiler:
|
||||
[output appendString:@" (Compiler)"];
|
||||
break;
|
||||
case SNTEventStateAllowTransitive:
|
||||
[output appendString:@" (Transitive)"];
|
||||
break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
|
||||
case SNTEventStateAllowSigningID:
|
||||
case SNTEventStateBlockSigningID:
|
||||
[output appendString:@" (SigningID)"];
|
||||
break;
|
||||
default: output = @"None".mutableCopy; break;
|
||||
}
|
||||
if (isatty(STDOUT_FILENO)) {
|
||||
if ((SNTEventStateAllow & s)) {
|
||||
[output insertString:@"\033[32m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else if ((SNTEventStateBlock & s)) {
|
||||
[output insertString:@"\033[31m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else {
|
||||
[output insertString:@"\033[33m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
}
|
||||
}
|
||||
}];
|
||||
__block NSString *output;
|
||||
|
||||
[rop databaseRuleForBinarySHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
reply:^(SNTRule *r) {
|
||||
if (r.state == SNTRuleStateAllowTransitive) {
|
||||
NSDate *date =
|
||||
[NSDate dateWithTimeIntervalSinceReferenceDate:r.timestamp];
|
||||
[output appendString:[NSString
|
||||
stringWithFormat:@"\nlast access date: %@",
|
||||
[date description]]];
|
||||
}
|
||||
output = [SNTCommandRule stringifyRule:r
|
||||
withColor:(isatty(STDOUT_FILENO) == 1)];
|
||||
}];
|
||||
|
||||
printf("%s\n", output.UTF8String);
|
||||
@@ -395,7 +421,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
databaseRuleAddRules:parsedRules
|
||||
cleanSlate:NO
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
reply:^(NSError *error) {
|
||||
if (error) {
|
||||
printf("Failed to modify rules: %s",
|
||||
@@ -425,7 +451,7 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
NSMutableArray *rulesAsDicts = [[NSMutableArray alloc] init];
|
||||
|
||||
for (SNTRule *rule in rules) {
|
||||
// Omit transitive and remove rules as they're not relevan.
|
||||
// Omit transitive and remove rules as they're not relevant.
|
||||
if (rule.state == SNTRuleStateAllowTransitive || rule.state == SNTRuleStateRemove) {
|
||||
continue;
|
||||
}
|
||||
|
||||
96
Source/santactl/Commands/SNTCommandRuleTest.mm
Normal file
96
Source/santactl/Commands/SNTCommandRuleTest.mm
Normal file
@@ -0,0 +1,96 @@
|
||||
/// 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>
|
||||
|
||||
#include <map>
|
||||
#include <utility>
|
||||
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/santactl/Commands/SNTCommandRule.h"
|
||||
|
||||
@interface SNTCommandRule (Testing)
|
||||
+ (NSString *)stringifyRule:(SNTRule *)rule withColor:(BOOL)colorize;
|
||||
@end
|
||||
|
||||
@interface SNTRule ()
|
||||
@property(readwrite) NSUInteger timestamp;
|
||||
@end
|
||||
|
||||
@interface SNTCommandRuleTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTCommandRuleTest
|
||||
|
||||
- (void)testStringifyRule {
|
||||
std::map<std::pair<SNTRuleType, SNTRuleState>, NSString *> ruleCheckToString = {
|
||||
{{SNTRuleTypeUnknown, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
|
||||
{{SNTRuleTypeUnknown, SNTRuleStateAllow}, @"Allowed (Unknown)"},
|
||||
{{SNTRuleTypeUnknown, SNTRuleStateBlock}, @"Blocked (Unknown)"},
|
||||
{{SNTRuleTypeUnknown, SNTRuleStateSilentBlock}, @"Blocked (Unknown, Silent)"},
|
||||
{{SNTRuleTypeUnknown, SNTRuleStateRemove}, @"Unexpected rule state: 4 (Unknown)"},
|
||||
{{SNTRuleTypeUnknown, SNTRuleStateAllowCompiler}, @"Allowed (Unknown, Compiler)"},
|
||||
{{SNTRuleTypeUnknown, SNTRuleStateAllowTransitive},
|
||||
@"Allowed (Unknown, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
|
||||
|
||||
{{SNTRuleTypeBinary, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateAllow}, @"Allowed (Binary)"},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateBlock}, @"Blocked (Binary)"},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateSilentBlock}, @"Blocked (Binary, Silent)"},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateRemove}, @"Unexpected rule state: 4 (Binary)"},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateAllowCompiler}, @"Allowed (Binary, Compiler)"},
|
||||
{{SNTRuleTypeBinary, SNTRuleStateAllowTransitive},
|
||||
@"Allowed (Binary, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
|
||||
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateAllow}, @"Allowed (SigningID)"},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateBlock}, @"Blocked (SigningID)"},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateSilentBlock}, @"Blocked (SigningID, Silent)"},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateRemove}, @"Unexpected rule state: 4 (SigningID)"},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateAllowCompiler}, @"Allowed (SigningID, Compiler)"},
|
||||
{{SNTRuleTypeSigningID, SNTRuleStateAllowTransitive},
|
||||
@"Allowed (SigningID, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
|
||||
|
||||
{{SNTRuleTypeCertificate, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
|
||||
{{SNTRuleTypeCertificate, SNTRuleStateAllow}, @"Allowed (Certificate)"},
|
||||
{{SNTRuleTypeCertificate, SNTRuleStateBlock}, @"Blocked (Certificate)"},
|
||||
{{SNTRuleTypeCertificate, SNTRuleStateSilentBlock}, @"Blocked (Certificate, Silent)"},
|
||||
{{SNTRuleTypeCertificate, SNTRuleStateRemove}, @"Unexpected rule state: 4 (Certificate)"},
|
||||
{{SNTRuleTypeCertificate, SNTRuleStateAllowCompiler}, @"Allowed (Certificate, Compiler)"},
|
||||
{{SNTRuleTypeCertificate, SNTRuleStateAllowTransitive},
|
||||
@"Allowed (Certificate, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
|
||||
|
||||
{{SNTRuleTypeTeamID, SNTRuleStateUnknown}, @"No rule exists with the given parameters"},
|
||||
{{SNTRuleTypeTeamID, SNTRuleStateAllow}, @"Allowed (TeamID)"},
|
||||
{{SNTRuleTypeTeamID, SNTRuleStateBlock}, @"Blocked (TeamID)"},
|
||||
{{SNTRuleTypeTeamID, SNTRuleStateSilentBlock}, @"Blocked (TeamID, Silent)"},
|
||||
{{SNTRuleTypeTeamID, SNTRuleStateRemove}, @"Unexpected rule state: 4 (TeamID)"},
|
||||
{{SNTRuleTypeTeamID, SNTRuleStateAllowCompiler}, @"Allowed (TeamID, Compiler)"},
|
||||
{{SNTRuleTypeTeamID, SNTRuleStateAllowTransitive},
|
||||
@"Allowed (TeamID, Transitive)\nlast access date: 2023-03-08 20:26:40 +0000"},
|
||||
};
|
||||
|
||||
SNTRule *rule = [[SNTRule alloc] init];
|
||||
rule.timestamp = 700000000; // time interval since reference date
|
||||
|
||||
for (const auto &[typeAndState, want] : ruleCheckToString) {
|
||||
rule.type = typeAndState.first;
|
||||
rule.state = typeAndState.second;
|
||||
|
||||
NSString *got = [SNTCommandRule stringifyRule:rule withColor:NO];
|
||||
XCTAssertEqualObjects(got, want);
|
||||
}
|
||||
}
|
||||
@end
|
||||
@@ -56,7 +56,6 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
|
||||
|
||||
// Daemon status
|
||||
@@ -130,8 +129,8 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}];
|
||||
|
||||
__block BOOL syncCleanReqd = NO;
|
||||
[rop syncCleanRequired:^(BOOL clean) {
|
||||
syncCleanReqd = clean;
|
||||
[rop syncTypeRequired:^(SNTSyncType syncType) {
|
||||
syncCleanReqd = (syncType == SNTSyncTypeClean || syncType == SNTSyncTypeCleanAll);
|
||||
}];
|
||||
|
||||
__block BOOL pushNotifications = NO;
|
||||
@@ -169,10 +168,15 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
}
|
||||
}];
|
||||
|
||||
// Wait a maximum of 5s for stats collected from daemon to arrive.
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5))) {
|
||||
fprintf(stderr, "Failed to retrieve some stats from daemon\n\n");
|
||||
}
|
||||
__block BOOL blockUSBMount = NO;
|
||||
[rop blockUSBMount:^(BOOL response) {
|
||||
blockUSBMount = response;
|
||||
}];
|
||||
|
||||
__block NSArray<NSString *> *remountUSBMode;
|
||||
[rop remountUSBMode:^(NSArray<NSString *> *response) {
|
||||
remountUSBMode = response;
|
||||
}];
|
||||
|
||||
// Format dates
|
||||
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||||
@@ -196,16 +200,15 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"daemon" : @{
|
||||
@"driver_connected" : @(YES),
|
||||
@"mode" : clientMode ?: @"null",
|
||||
@"transitive_rules" : @(enableTransitiveRules),
|
||||
@"log_type" : eventLogType,
|
||||
@"file_logging" : @(fileLogging),
|
||||
@"watchdog_cpu_events" : @(cpuEvents),
|
||||
@"watchdog_ram_events" : @(ramEvents),
|
||||
@"watchdog_cpu_peak" : @(cpuPeak),
|
||||
@"watchdog_ram_peak" : @(ramPeak),
|
||||
@"block_usb" : @(configurator.blockUSBMount),
|
||||
@"remount_usb_mode" : (configurator.blockUSBMount && configurator.remountUSBMode.count
|
||||
? configurator.remountUSBMode
|
||||
: @""),
|
||||
@"block_usb" : @(blockUSBMount),
|
||||
@"remount_usb_mode" : (blockUSBMount && remountUSBMode.count ? remountUSBMode : @""),
|
||||
@"on_start_usb_options" : StartupOptionToString(configurator.onStartUSBOptions),
|
||||
},
|
||||
@"database" : @{
|
||||
@@ -227,7 +230,6 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"last_successful_rule" : ruleSyncLastSuccessStr ?: @"null",
|
||||
@"push_notifications" : pushNotifications ? @"Connected" : @"Disconnected",
|
||||
@"bundle_scanning" : @(enableBundles),
|
||||
@"transitive_rules" : @(enableTransitiveRules),
|
||||
},
|
||||
} mutableCopy];
|
||||
|
||||
@@ -260,12 +262,17 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
} else {
|
||||
printf(">>> Daemon Info\n");
|
||||
printf(" %-25s | %s\n", "Mode", [clientMode UTF8String]);
|
||||
|
||||
if (enableTransitiveRules) {
|
||||
printf(" %-25s | %s\n", "Transitive Rules", (enableTransitiveRules ? "Yes" : "No"));
|
||||
}
|
||||
|
||||
printf(" %-25s | %s\n", "Log Type", [eventLogType UTF8String]);
|
||||
printf(" %-25s | %s\n", "File Logging", (fileLogging ? "Yes" : "No"));
|
||||
printf(" %-25s | %s\n", "USB Blocking", (configurator.blockUSBMount ? "Yes" : "No"));
|
||||
if (configurator.blockUSBMount && configurator.remountUSBMode.count > 0) {
|
||||
printf(" %-25s | %s\n", "USB Blocking", (blockUSBMount ? "Yes" : "No"));
|
||||
if (blockUSBMount && remountUSBMode.count > 0) {
|
||||
printf(" %-25s | %s\n", "USB Remounting Mode",
|
||||
[[configurator.remountUSBMode componentsJoinedByString:@", "] UTF8String]);
|
||||
[[remountUSBMode componentsJoinedByString:@", "] UTF8String]);
|
||||
}
|
||||
printf(" %-25s | %s\n", "On Start USB Options",
|
||||
StartupOptionToString(configurator.onStartUSBOptions).UTF8String);
|
||||
@@ -308,7 +315,6 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
printf(" %-25s | %s\n", "Push Notifications",
|
||||
(pushNotifications ? "Connected" : "Disconnected"));
|
||||
printf(" %-25s | %s\n", "Bundle Scanning", (enableBundles ? "Yes" : "No"));
|
||||
printf(" %-25s | %s\n", "Transitive Rules", (enableTransitiveRules ? "Yes" : "No"));
|
||||
}
|
||||
|
||||
if (exportMetrics) {
|
||||
|
||||
@@ -47,8 +47,10 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
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 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
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."];
|
||||
|
||||
@@ -21,6 +21,9 @@ objc_library(
|
||||
name = "SNTRuleTable",
|
||||
srcs = ["DataLayer/SNTRuleTable.m"],
|
||||
hdrs = ["DataLayer/SNTRuleTable.h"],
|
||||
sdk_dylibs = [
|
||||
"EndpointSecurity",
|
||||
],
|
||||
deps = [
|
||||
":SNTDatabaseTable",
|
||||
"//Source/common:Platform",
|
||||
@@ -199,6 +202,7 @@ objc_library(
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeepCopy",
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTRule",
|
||||
@@ -236,10 +240,12 @@ objc_library(
|
||||
":SNTSyncdQueue",
|
||||
":TTYWriter",
|
||||
"//Source/common:BranchPrediction",
|
||||
"//Source/common:PrefixTree",
|
||||
"//Source/common:SNTBlockMessage",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeepCopy",
|
||||
"//Source/common:SNTDropRootPrivs",
|
||||
"//Source/common:SNTFileInfo",
|
||||
"//Source/common:SNTLogging",
|
||||
@@ -248,7 +254,9 @@ objc_library(
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:String",
|
||||
"//Source/common:Unit",
|
||||
"@MOLCodesignChecker",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -278,6 +286,7 @@ objc_library(
|
||||
":SNTEndpointSecurityClientBase",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:BranchPrediction",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SystemResources",
|
||||
@@ -470,7 +479,6 @@ objc_library(
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
":EndpointSecurityMessage",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SantaCache",
|
||||
@@ -897,6 +905,7 @@ santa_unit_test(
|
||||
":SNTDatabaseController",
|
||||
":SNTDecisionCache",
|
||||
":SNTEndpointSecurityAuthorizer",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SantadDeps",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTConfigurator",
|
||||
@@ -1174,7 +1183,9 @@ santa_unit_test(
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityClient",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SystemResources",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
@@ -1318,6 +1329,7 @@ santa_unit_test(
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityDeviceManager",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -194,7 +194,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
@")"];
|
||||
[db executeUpdate:@"CREATE UNIQUE INDEX rulesunique ON rules (shasum, type)"];
|
||||
|
||||
[[SNTConfigurator configurator] setSyncCleanRequired:YES];
|
||||
[[SNTConfigurator configurator] setSyncTypeRequired:SNTSyncTypeCleanAll];
|
||||
|
||||
newVersion = 1;
|
||||
}
|
||||
@@ -403,7 +403,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
#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 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
__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) {
|
||||
|
||||
@@ -65,6 +65,15 @@
|
||||
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 @@
|
||||
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)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)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 @@
|
||||
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)testFetchCertificateRule {
|
||||
[self.sut addRules:@[ [self _exampleBinaryRule], [self _exampleCertRule] ]
|
||||
cleanSlate:NO
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
SNTRule *r = [self.sut
|
||||
@@ -181,7 +215,7 @@
|
||||
|
||||
- (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 @@
|
||||
[self _exampleBinaryRule], [self _exampleSigningIDRuleIsPlatform:YES],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
]
|
||||
cleanSlate:NO
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
XCTAssertEqual([self.sut signingIDRuleCount], 2);
|
||||
@@ -236,7 +270,7 @@
|
||||
[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 @@
|
||||
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 @@
|
||||
[self _exampleCertRule], [self _exampleBinaryRule], [self _exampleTeamIDRule],
|
||||
[self _exampleSigningIDRuleIsPlatform:NO]
|
||||
]
|
||||
cleanSlate:NO
|
||||
ruleCleanup:SNTRuleCleanupNone
|
||||
error:nil];
|
||||
|
||||
NSArray<SNTRule *> *rules = [self.sut retrieveAllRules];
|
||||
|
||||
@@ -41,6 +41,8 @@ enum class FlushCacheReason {
|
||||
kStaticRulesChanged,
|
||||
kExplicitCommand,
|
||||
kFilesystemUnmounted,
|
||||
kEntitlementsPrefixFilterChanged,
|
||||
kEntitlementsTeamIDFilterChanged,
|
||||
};
|
||||
|
||||
class AuthResultCache {
|
||||
|
||||
@@ -31,6 +31,10 @@ static NSString *const kFlushCacheReasonRulesChanged = @"RulesChanged";
|
||||
static NSString *const kFlushCacheReasonStaticRulesChanged = @"StaticRulesChanged";
|
||||
static NSString *const kFlushCacheReasonExplicitCommand = @"ExplicitCommand";
|
||||
static NSString *const kFlushCacheReasonFilesystemUnmounted = @"FilesystemUnmounted";
|
||||
static NSString *const kFlushCacheReasonEntitlementsPrefixFilterChanged =
|
||||
@"EntitlementsPrefixFilterChanged";
|
||||
static NSString *const kFlushCacheReasonEntitlementsTeamIDFilterChanged =
|
||||
@"EntitlementsTeamIDFilterChanged";
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
@@ -59,6 +63,10 @@ NSString *const FlushCacheReasonToString(FlushCacheReason reason) {
|
||||
case FlushCacheReason::kStaticRulesChanged: return kFlushCacheReasonStaticRulesChanged;
|
||||
case FlushCacheReason::kExplicitCommand: return kFlushCacheReasonExplicitCommand;
|
||||
case FlushCacheReason::kFilesystemUnmounted: return kFlushCacheReasonFilesystemUnmounted;
|
||||
case FlushCacheReason::kEntitlementsPrefixFilterChanged:
|
||||
return kFlushCacheReasonEntitlementsPrefixFilterChanged;
|
||||
case FlushCacheReason::kEntitlementsTeamIDFilterChanged:
|
||||
return kFlushCacheReasonEntitlementsTeamIDFilterChanged;
|
||||
default:
|
||||
[NSException raise:@"Invalid reason"
|
||||
format:@"Unknown reason value: %d", static_cast<int>(reason)];
|
||||
|
||||
@@ -230,13 +230,16 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
|
||||
{FlushCacheReason::kStaticRulesChanged, @"StaticRulesChanged"},
|
||||
{FlushCacheReason::kExplicitCommand, @"ExplicitCommand"},
|
||||
{FlushCacheReason::kFilesystemUnmounted, @"FilesystemUnmounted"},
|
||||
{FlushCacheReason::kEntitlementsPrefixFilterChanged, @"EntitlementsPrefixFilterChanged"},
|
||||
{FlushCacheReason::kEntitlementsTeamIDFilterChanged, @"EntitlementsTeamIDFilterChanged"},
|
||||
};
|
||||
|
||||
for (const auto &kv : reasonToString) {
|
||||
XCTAssertEqualObjects(FlushCacheReasonToString(kv.first), kv.second);
|
||||
}
|
||||
|
||||
XCTAssertThrows(FlushCacheReasonToString((FlushCacheReason)12345));
|
||||
XCTAssertThrows(FlushCacheReasonToString(
|
||||
(FlushCacheReason)(static_cast<int>(FlushCacheReason::kEntitlementsTeamIDFilterChanged) + 1)));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#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"
|
||||
|
||||
@@ -21,11 +21,13 @@
|
||||
#include <stdlib.h>
|
||||
#include <sys/qos.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "Source/common/BranchPrediction.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/common/SystemResources.h"
|
||||
@@ -48,7 +50,9 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
"/private/var/db/santa/events.db"};
|
||||
|
||||
@interface SNTEndpointSecurityClient ()
|
||||
@property int64_t deadlineMarginMS;
|
||||
@property(nonatomic) double defaultBudget;
|
||||
@property(nonatomic) int64_t minAllowedHeadroom;
|
||||
@property(nonatomic) int64_t maxAllowedHeadroom;
|
||||
@property SNTConfigurator *configurator;
|
||||
@end
|
||||
|
||||
@@ -68,10 +72,18 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
if (self) {
|
||||
_esApi = std::move(esApi);
|
||||
_metrics = std::move(metrics);
|
||||
_deadlineMarginMS = 5000;
|
||||
_configurator = [SNTConfigurator configurator];
|
||||
_processor = processor;
|
||||
|
||||
// Default event processing budget is 80% of the deadline time
|
||||
_defaultBudget = 0.8;
|
||||
|
||||
// For events with small deadlines, clamp processing budget to 1s headroom
|
||||
_minAllowedHeadroom = 1 * NSEC_PER_SEC;
|
||||
|
||||
// For events with large deadlines, clamp processing budget to 5s headroom
|
||||
_maxAllowedHeadroom = 5 * NSEC_PER_SEC;
|
||||
|
||||
_authQueue = dispatch_queue_create(
|
||||
"com.google.santa.daemon.auth_queue",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
|
||||
@@ -125,6 +137,11 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
|
||||
self->_esClient = self->_esApi->NewClient(^(es_client_t *c, Message esMsg) {
|
||||
int64_t processingStart = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
|
||||
// Update event stats BEFORE calling into the processor class to ensure
|
||||
// sequence numbers are processed in order.
|
||||
self->_metrics->UpdateEventStats(self->_processor, esMsg.operator->());
|
||||
|
||||
es_event_type_t eventType = esMsg->event_type;
|
||||
if ([self shouldHandleMessage:esMsg]) {
|
||||
[self handleMessage:std::move(esMsg)
|
||||
@@ -250,6 +267,24 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
});
|
||||
}
|
||||
|
||||
- (int64_t)computeBudgetForDeadline:(uint64_t)deadline currentTime:(uint64_t)currentTime {
|
||||
// First get how much time we have left
|
||||
int64_t nanosUntilDeadline = (int64_t)MachTimeToNanos(deadline - currentTime);
|
||||
|
||||
// Compute the desired budget
|
||||
int64_t budget = nanosUntilDeadline * self.defaultBudget;
|
||||
|
||||
// See how much headroom is left
|
||||
int64_t headroom = nanosUntilDeadline - budget;
|
||||
|
||||
// Clamp headroom to maximize budget but ensure it's not so large as to not leave
|
||||
// enough time to respond in an emergency.
|
||||
headroom = std::clamp(headroom, self.minAllowedHeadroom, self.maxAllowedHeadroom);
|
||||
|
||||
// Return the processing budget given the allotted headroom
|
||||
return nanosUntilDeadline - headroom;
|
||||
}
|
||||
|
||||
- (void)processMessage:(Message &&)msg handler:(void (^)(const Message &))messageHandler {
|
||||
if (unlikely(msg->action_type != ES_ACTION_TYPE_AUTH)) {
|
||||
// This is a programming error
|
||||
@@ -265,33 +300,33 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
dispatch_semaphore_signal(processingSema);
|
||||
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
|
||||
|
||||
const uint64_t timeout = NSEC_PER_MSEC * (self.deadlineMarginMS);
|
||||
|
||||
uint64_t deadlineNano = MachTimeToNanos(msg->deadline - mach_absolute_time());
|
||||
|
||||
// TODO(mlw): How should we handle `deadlineNano <= timeout`. Will currently
|
||||
// result in the deadline block being dispatched immediately (and therefore
|
||||
// the event will be denied).
|
||||
int64_t processingBudget = [self computeBudgetForDeadline:msg->deadline
|
||||
currentTime:mach_absolute_time()];
|
||||
|
||||
// Workaround for compiler bug that doesn't properly close over variables
|
||||
__block Message processMsg = msg;
|
||||
__block Message deadlineMsg = msg;
|
||||
|
||||
dispatch_after(
|
||||
dispatch_time(DISPATCH_TIME_NOW, deadlineNano - timeout), self->_authQueue, ^(void) {
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Handler has already responded, nothing to do.
|
||||
return;
|
||||
}
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, processingBudget), self->_authQueue, ^(void) {
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Handler has already responded, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
bool res = [self respondToMessage:deadlineMsg
|
||||
withAuthResult:ES_AUTH_RESULT_DENY
|
||||
cacheable:false];
|
||||
es_auth_result_t authResult;
|
||||
if (self.configurator.failClosed) {
|
||||
authResult = ES_AUTH_RESULT_DENY;
|
||||
} else {
|
||||
authResult = ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
LOGE(@"SNTEndpointSecurityClient: deadline reached: deny pid=%d, event type: %d ret=%d",
|
||||
audit_token_to_pid(deadlineMsg->process->audit_token), deadlineMsg->event_type, res);
|
||||
dispatch_semaphore_signal(deadlineExpiredSema);
|
||||
});
|
||||
bool res = [self respondToMessage:deadlineMsg withAuthResult:authResult cacheable:false];
|
||||
|
||||
LOGE(@"SNTEndpointSecurityClient: deadline reached: pid=%d, event type: %d, result: %@, ret=%d",
|
||||
audit_token_to_pid(deadlineMsg->process->audit_token), deadlineMsg->event_type,
|
||||
(authResult == ES_AUTH_RESULT_DENY ? @"denied" : @"allowed"), res);
|
||||
dispatch_semaphore_signal(deadlineExpiredSema);
|
||||
});
|
||||
|
||||
dispatch_async(self->_authQueue, ^{
|
||||
messageHandler(processMsg);
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SystemResources.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
@@ -48,8 +50,11 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
- (void)handleMessage:(Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(santa::santad::EventDisposition disposition))recordEventMetrics;
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg;
|
||||
- (int64_t)computeBudgetForDeadline:(uint64_t)deadline currentTime:(uint64_t)currentTime;
|
||||
|
||||
@property int64_t deadlineMarginMS;
|
||||
@property(nonatomic) double defaultBudget;
|
||||
@property(nonatomic) int64_t minAllowedHeadroom;
|
||||
@property(nonatomic) int64_t maxAllowedHeadroom;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityClientTest : XCTestCase
|
||||
@@ -322,11 +327,14 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
// Ensure all paths are attempted to be muted even if some fail.
|
||||
// Ensure if any paths fail the overall result is false.
|
||||
EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, "a", WatchItemPathType::kLiteral))
|
||||
EXPECT_CALL(*mockESApi,
|
||||
MuteTargetPath(testing::_, std::string_view("a"), WatchItemPathType::kLiteral))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, "b", WatchItemPathType::kLiteral))
|
||||
EXPECT_CALL(*mockESApi,
|
||||
MuteTargetPath(testing::_, std::string_view("b"), WatchItemPathType::kLiteral))
|
||||
.WillOnce(testing::Return(false));
|
||||
EXPECT_CALL(*mockESApi, MuteTargetPath(testing::_, "c", WatchItemPathType::kPrefix))
|
||||
EXPECT_CALL(*mockESApi,
|
||||
MuteTargetPath(testing::_, std::string_view("c"), WatchItemPathType::kPrefix))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
std::vector<std::pair<std::string, WatchItemPathType>> paths = {
|
||||
@@ -349,11 +357,14 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
// Ensure all paths are attempted to be unmuted even if some fail.
|
||||
// Ensure if any paths fail the overall result is false.
|
||||
EXPECT_CALL(*mockESApi, UnmuteTargetPath(testing::_, "a", WatchItemPathType::kLiteral))
|
||||
EXPECT_CALL(*mockESApi,
|
||||
UnmuteTargetPath(testing::_, std::string_view("a"), WatchItemPathType::kLiteral))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, UnmuteTargetPath(testing::_, "b", WatchItemPathType::kLiteral))
|
||||
EXPECT_CALL(*mockESApi,
|
||||
UnmuteTargetPath(testing::_, std::string_view("b"), WatchItemPathType::kLiteral))
|
||||
.WillOnce(testing::Return(false));
|
||||
EXPECT_CALL(*mockESApi, UnmuteTargetPath(testing::_, "c", WatchItemPathType::kPrefix))
|
||||
EXPECT_CALL(*mockESApi,
|
||||
UnmuteTargetPath(testing::_, std::string_view("c"), WatchItemPathType::kPrefix))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
std::vector<std::pair<std::string, WatchItemPathType>> paths = {
|
||||
@@ -497,7 +508,47 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testProcessMessageHandlerWithDeadlineTimeout {
|
||||
- (void)testComputeBudgetForDeadlineCurrentTime {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// The test uses crafted values to make even numbers. Ensure the client has
|
||||
// expected values for these properties so the test can fail early if not.
|
||||
XCTAssertEqual(client.defaultBudget, 0.8);
|
||||
XCTAssertEqual(client.minAllowedHeadroom, 1 * NSEC_PER_SEC);
|
||||
XCTAssertEqual(client.maxAllowedHeadroom, 5 * NSEC_PER_SEC);
|
||||
|
||||
std::map<uint64_t, int64_t> deadlineMillisToBudgetMillis{
|
||||
// Further out deadlines clamp processing budget to maxAllowedHeadroom
|
||||
{45000, 40000},
|
||||
|
||||
// Closer deadlines allow a set percentage processing budget
|
||||
{15000, 12000},
|
||||
|
||||
// Near deadlines clamp processing budget to minAllowedHeadroom
|
||||
{3500, 2500}};
|
||||
|
||||
uint64_t curTime = mach_absolute_time();
|
||||
|
||||
for (const auto [deadlineMS, budgetMS] : deadlineMillisToBudgetMillis) {
|
||||
int64_t got =
|
||||
[client computeBudgetForDeadline:AddNanosecondsToMachTime(deadlineMS * NSEC_PER_MSEC, curTime)
|
||||
currentTime:curTime];
|
||||
|
||||
// Add 100us, then clip to ms to account for non-exact values due to timebase division
|
||||
got = (int64_t)((double)(got + (100 * NSEC_PER_USEC)) / (double)NSEC_PER_MSEC);
|
||||
|
||||
XCTAssertEqual(got, budgetMS);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)checkDeadlineExpiredFailClosed:(BOOL)shouldFailClosed {
|
||||
// Set a es_message_t deadline of 750ms
|
||||
// Set a deadline leeway in the `SNTEndpointSecurityClient` of 500ms
|
||||
// Mock `RespondFlagsResult` which is called from the deadline handler
|
||||
@@ -511,7 +562,7 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
// deadlineSema is signaled (or a timeout waiting on deadlineSema)
|
||||
es_file_t proc_file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&proc_file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &proc, ActionType::Auth,
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth,
|
||||
750); // 750ms timeout
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
@@ -520,18 +571,27 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
dispatch_semaphore_t deadlineSema = dispatch_semaphore_create(0);
|
||||
dispatch_semaphore_t controlSema = dispatch_semaphore_create(0);
|
||||
|
||||
EXPECT_CALL(*mockESApi, RespondFlagsResult(testing::_, testing::_, 0x0, false))
|
||||
es_auth_result_t wantAuthResult = shouldFailClosed ? ES_AUTH_RESULT_DENY : ES_AUTH_RESULT_ALLOW;
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, wantAuthResult, false))
|
||||
.WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
// Signal deadlineSema to let the handler block continue execution
|
||||
dispatch_semaphore_signal(deadlineSema);
|
||||
return true;
|
||||
}));
|
||||
|
||||
id mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([mockConfigurator configurator]).andReturn(mockConfigurator);
|
||||
|
||||
OCMExpect([mockConfigurator failClosed]).andReturn(shouldFailClosed);
|
||||
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
client.deadlineMarginMS = 500;
|
||||
|
||||
// Set min/max headroom the same to clamp the value for this test
|
||||
client.minAllowedHeadroom = 500 * NSEC_PER_MSEC;
|
||||
client.maxAllowedHeadroom = 500 * NSEC_PER_MSEC;
|
||||
|
||||
{
|
||||
__block long result;
|
||||
@@ -560,7 +620,18 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
// seeing the warning (but still possible)
|
||||
SleepMS(100);
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockConfigurator));
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
[mockConfigurator stopMocking];
|
||||
}
|
||||
|
||||
- (void)testDeadlineExpiredFailClosed {
|
||||
[self checkDeadlineExpiredFailClosed:YES];
|
||||
}
|
||||
|
||||
- (void)testDeadlineExpiredFailOpen {
|
||||
[self checkDeadlineExpiredFailClosed:NO];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
@@ -50,6 +51,12 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode, FlushCacheReason reason));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityClient (Testing)
|
||||
@property(nonatomic) double defaultBudget;
|
||||
@property(nonatomic) int64_t minAllowedHeadroom;
|
||||
@property(nonatomic) int64_t maxAllowedHeadroom;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager (Testing)
|
||||
- (instancetype)init;
|
||||
- (void)logDiskAppeared:(NSDictionary *)props;
|
||||
@@ -136,6 +143,11 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
|
||||
// This test is sensitive to ~1s processing budget.
|
||||
// Set a 5s headroom and 6s deadline
|
||||
deviceManager.minAllowedHeadroom = 5 * NSEC_PER_SEC;
|
||||
deviceManager.maxAllowedHeadroom = 5 * NSEC_PER_SEC;
|
||||
es_message_t esMsg = MakeESMessage(eventType, &proc, ActionType::Auth, 6000);
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
@@ -375,15 +387,15 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
// 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<MockDADisk *> *, NSArray<NSString *> *,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -134,6 +134,13 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
|
||||
[self.compilerController handleEvent:esMsg withLogger:self->_logger];
|
||||
|
||||
if ((esMsg->event_type == ES_EVENT_TYPE_NOTIFY_FORK ||
|
||||
esMsg->event_type == ES_EVENT_TYPE_NOTIFY_EXIT) &&
|
||||
self.configurator.enableForkAndExitLogging == NO) {
|
||||
recordEventMetrics(EventDisposition::kDropped);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter file op events matching the prefix tree.
|
||||
es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg));
|
||||
if (targetFile != NULL && self->_prefixTree->HasPrefix(targetFile->path.data)) {
|
||||
|
||||
@@ -112,9 +112,9 @@ typedef void (^testHelperBlock)(es_message_t *message,
|
||||
es_file_t targetFileMatchesRegex = MakeESFile("/foo/matches");
|
||||
es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
|
||||
- (void)handleMessageWithMatchCalls:(BOOL)regexMatchCalls
|
||||
withMissCalls:(BOOL)regexFailsMatchCalls
|
||||
withBlock:(testHelperBlock)testBlock {
|
||||
- (void)handleMessageShouldLog:(BOOL)shouldLog
|
||||
shouldRemoveFromCache:(BOOL)shouldRemoveFromCache
|
||||
withBlock:(testHelperBlock)testBlock {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc, ActionType::Auth);
|
||||
@@ -127,15 +127,10 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
|
||||
auto mockEnricher = std::make_shared<MockEnricher>();
|
||||
|
||||
if (regexMatchCalls) {
|
||||
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
|
||||
}
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMatchesRegex)).Times((int)regexMatchCalls);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFileMissesRegex))
|
||||
.Times((int)regexFailsMatchCalls);
|
||||
|
||||
if (shouldRemoveFromCache) {
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache).Times(1);
|
||||
}
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
// NOTE: Currently unable to create a partial mock of the
|
||||
@@ -144,7 +139,8 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
// test will mock the `Log` method that is called in the handler block.
|
||||
__block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
auto mockLogger = std::make_shared<MockLogger>(nullptr, nullptr);
|
||||
if (regexMatchCalls) {
|
||||
if (shouldLog) {
|
||||
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(std::move(enrichedMsg)));
|
||||
EXPECT_CALL(*mockLogger, Log).WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}));
|
||||
@@ -250,7 +246,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
}]);
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:NO withMissCalls:NO withBlock:testBlock];
|
||||
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
|
||||
|
||||
// CLOSE modified, remove from cache, and matches fileChangesRegex
|
||||
testBlock = ^(
|
||||
@@ -274,7 +270,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
XCTAssertSemaTrue(*sema, 5, "Log wasn't called within expected time window");
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:YES withMissCalls:NO withBlock:testBlock];
|
||||
[self handleMessageShouldLog:YES shouldRemoveFromCache:YES withBlock:testBlock];
|
||||
|
||||
// CLOSE modified, remove from cache, but doesn't match fileChangesRegex
|
||||
testBlock = ^(
|
||||
@@ -291,7 +287,7 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
}]);
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:NO withMissCalls:YES withBlock:testBlock];
|
||||
[self handleMessageShouldLog:NO shouldRemoveFromCache:YES withBlock:testBlock];
|
||||
|
||||
// LINK, Prefix match, bail early
|
||||
testBlock =
|
||||
@@ -316,7 +312,57 @@ es_file_t targetFileMissesRegex = MakeESFile("/foo/misses");
|
||||
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
};
|
||||
|
||||
[self handleMessageWithMatchCalls:NO withMissCalls:NO withBlock:testBlock];
|
||||
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
|
||||
|
||||
// EXIT, EnableForkAndExitLogging is false
|
||||
testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics)
|
||||
|
||||
{
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_EXIT;
|
||||
Message msg(mockESApi, esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
OCMExpect([self.mockConfigurator enableForkAndExitLogging]).andReturn(NO);
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(*semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
};
|
||||
|
||||
[self handleMessageShouldLog:NO shouldRemoveFromCache:NO withBlock:testBlock];
|
||||
|
||||
// FORK, EnableForkAndExitLogging is true
|
||||
testBlock =
|
||||
^(es_message_t *esMsg, std::shared_ptr<MockEndpointSecurityAPI> mockESApi, id mockCC,
|
||||
SNTEndpointSecurityRecorder *recorderClient, std::shared_ptr<PrefixTree<Unit>> prefixTree,
|
||||
__autoreleasing dispatch_semaphore_t *sema, __autoreleasing dispatch_semaphore_t *semaMetrics)
|
||||
|
||||
{
|
||||
esMsg->event_type = ES_EVENT_TYPE_NOTIFY_FORK;
|
||||
Message msg(mockESApi, esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
OCMExpect([self.mockConfigurator enableForkAndExitLogging]).andReturn(YES);
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(*semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(*semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
};
|
||||
|
||||
[self handleMessageShouldLog:YES shouldRemoveFromCache:NO withBlock:testBlock];
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(self.mockConfigurator));
|
||||
}
|
||||
|
||||
- (void)testGetTargetFileForPrefixTree {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#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"
|
||||
|
||||
@@ -71,6 +71,9 @@ namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
static constexpr NSUInteger kMaxEncodeObjectEntries = 64;
|
||||
static constexpr NSUInteger kMaxEncodeObjectLevels = 5;
|
||||
|
||||
std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTDecisionCache *decision_cache, bool json) {
|
||||
return std::make_shared<Protobuf>(esapi, std::move(decision_cache), json);
|
||||
@@ -449,6 +452,124 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExchange &msg) {
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
id StandardizedNestedObjects(id obj, int level) {
|
||||
if (level-- == 0) {
|
||||
return [obj description];
|
||||
}
|
||||
|
||||
if ([obj isKindOfClass:[NSNumber class]] || [obj isKindOfClass:[NSString class]]) {
|
||||
return obj;
|
||||
} else if ([obj isKindOfClass:[NSArray class]]) {
|
||||
NSMutableArray *arr = [NSMutableArray array];
|
||||
for (id item in obj) {
|
||||
[arr addObject:StandardizedNestedObjects(item, level)];
|
||||
}
|
||||
return arr;
|
||||
} else if ([obj isKindOfClass:[NSDictionary class]]) {
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
|
||||
for (id key in obj) {
|
||||
[dict setObject:StandardizedNestedObjects(obj[key], level) forKey:key];
|
||||
}
|
||||
return dict;
|
||||
} else if ([obj isKindOfClass:[NSData class]]) {
|
||||
return [obj base64EncodedStringWithOptions:0];
|
||||
} else if ([obj isKindOfClass:[NSDate class]]) {
|
||||
return [NSISO8601DateFormatter stringFromDate:obj
|
||||
timeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]
|
||||
formatOptions:NSISO8601DateFormatWithFractionalSeconds |
|
||||
NSISO8601DateFormatWithInternetDateTime];
|
||||
|
||||
} else {
|
||||
LOGW(@"Unexpected object encountered: %@", obj);
|
||||
return [obj description];
|
||||
}
|
||||
}
|
||||
|
||||
void EncodeEntitlements(::pbv1::Execution *pb_exec, SNTCachedDecision *cd) {
|
||||
::pbv1::EntitlementInfo *pb_entitlement_info = pb_exec->mutable_entitlement_info();
|
||||
|
||||
pb_entitlement_info->set_entitlements_filtered(cd.entitlementsFiltered != NO);
|
||||
|
||||
if (!cd.entitlements) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since nested objects with varying types is hard for the API to serialize to
|
||||
// JSON, first go through and standardize types to ensure better serialization
|
||||
// as well as a consitent view of data.
|
||||
NSDictionary *entitlements = StandardizedNestedObjects(cd.entitlements, kMaxEncodeObjectLevels);
|
||||
|
||||
__block int numObjectsToEncode = (int)std::min(kMaxEncodeObjectEntries, entitlements.count);
|
||||
|
||||
pb_entitlement_info->mutable_entitlements()->Reserve(numObjectsToEncode);
|
||||
|
||||
[entitlements enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
|
||||
if (numObjectsToEncode-- == 0) {
|
||||
// Because entitlements are being clipped, ensure that we update that
|
||||
// the set of entitlements were filtered.
|
||||
pb_entitlement_info->set_entitlements_filtered(true);
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
if (![key isKindOfClass:[NSString class]]) {
|
||||
LOGW(@"Skipping entitlement key with unexpected key type: %@", key);
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *err;
|
||||
NSData *jsonData;
|
||||
@try {
|
||||
jsonData = [NSJSONSerialization dataWithJSONObject:obj
|
||||
options:NSJSONWritingFragmentsAllowed
|
||||
error:&err];
|
||||
} @catch (NSException *e) {
|
||||
LOGW(@"Encountered entitlement that cannot directly convert to JSON: %@: %@", key, obj);
|
||||
}
|
||||
|
||||
if (!jsonData) {
|
||||
// If the first attempt to serialize to JSON failed, get a string
|
||||
// representation of the object via the `description` method and attempt
|
||||
// to serialize that instead. Serialization can fail for a number of
|
||||
// reasons, such as arrays including invalid types.
|
||||
@try {
|
||||
jsonData = [NSJSONSerialization dataWithJSONObject:[obj description]
|
||||
options:NSJSONWritingFragmentsAllowed
|
||||
error:&err];
|
||||
} @catch (NSException *e) {
|
||||
LOGW(@"Unable to create fallback string: %@: %@", key, obj);
|
||||
}
|
||||
|
||||
if (!jsonData) {
|
||||
@try {
|
||||
// As a final fallback, simply serialize an error message so that the
|
||||
// entitlement key is still logged.
|
||||
jsonData = [NSJSONSerialization dataWithJSONObject:@"JSON Serialization Failed"
|
||||
options:NSJSONWritingFragmentsAllowed
|
||||
error:&err];
|
||||
} @catch (NSException *e) {
|
||||
// This shouldn't be able to happen...
|
||||
LOGW(@"Failed to serialize fallback error message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This shouldn't be possible given the fallback code above. But handle it
|
||||
// just in case to prevent a crash.
|
||||
if (!jsonData) {
|
||||
LOGW(@"Failed to create valid JSON for entitlement: %@", key);
|
||||
return;
|
||||
}
|
||||
|
||||
::pbv1::Entitlement *pb_entitlement = pb_entitlement_info->add_entitlements();
|
||||
EncodeString([pb_entitlement] { return pb_entitlement->mutable_key(); },
|
||||
NSStringToUTF8StringView(key));
|
||||
EncodeString([pb_entitlement] { return pb_entitlement->mutable_value(); },
|
||||
NSStringToUTF8StringView([[NSString alloc] initWithData:jsonData
|
||||
encoding:NSUTF8StringEncoding]));
|
||||
}];
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCachedDecision *cd) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
@@ -525,6 +646,8 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCach
|
||||
NSString *orig_path = Utilities::OriginalPathForTranslocation(msg.es_msg().event.exec.target);
|
||||
EncodeString([pb_exec] { return pb_exec->mutable_original_path(); }, orig_path);
|
||||
|
||||
EncodeEntitlements(pb_exec, cd);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
#include <cstring>
|
||||
|
||||
#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"
|
||||
@@ -60,6 +60,7 @@ namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
extern void EncodeExitStatus(::pbv1::Exit *pbExit, int exitStatus);
|
||||
extern void EncodeEntitlements(::pbv1::Execution *pb_exec, SNTCachedDecision *cd);
|
||||
extern ::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state);
|
||||
extern ::pbv1::Execution::Reason GetReasonEnum(SNTEventState event_state);
|
||||
extern ::pbv1::Execution::Mode GetModeEnum(SNTClientMode mode);
|
||||
@@ -68,6 +69,7 @@ extern ::pbv1::FileAccess::AccessType GetAccessType(es_event_type_t event_type);
|
||||
extern ::pbv1::FileAccess::PolicyDecision GetPolicyDecision(FileAccessPolicyDecision decision);
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
using santa::santad::logs::endpoint_security::serializers::EncodeEntitlements;
|
||||
using santa::santad::logs::endpoint_security::serializers::EncodeExitStatus;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetAccessType;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetDecisionEnum;
|
||||
@@ -166,28 +168,35 @@ std::string ConvertMessageToJsonString(const ::pbv1::SantaMessage &santaMsg) {
|
||||
return json;
|
||||
}
|
||||
|
||||
NSDictionary *findDelta(NSDictionary *a, NSDictionary *b) {
|
||||
NSMutableDictionary *delta = NSMutableDictionary.dictionary;
|
||||
NSDictionary *FindDelta(NSDictionary *want, NSDictionary *got) {
|
||||
NSMutableDictionary *delta = [NSMutableDictionary dictionary];
|
||||
delta[@"want"] = [NSMutableDictionary dictionary];
|
||||
delta[@"got"] = [NSMutableDictionary dictionary];
|
||||
|
||||
// Find objects in a that don't exist or are different in b.
|
||||
[a enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
|
||||
id otherObj = b[key];
|
||||
// Find objects in `want` that don't exist or are different in `got`.
|
||||
[want enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
|
||||
id otherObj = got[key];
|
||||
|
||||
if (![obj isEqual:otherObj]) {
|
||||
delta[key] = obj;
|
||||
if (!otherObj) {
|
||||
delta[@"want"][key] = obj;
|
||||
delta[@"got"][key] = @"Key missing";
|
||||
} else if (![obj isEqual:otherObj]) {
|
||||
delta[@"want"][key] = obj;
|
||||
delta[@"got"][key] = otherObj;
|
||||
}
|
||||
}];
|
||||
|
||||
// Find objects in the other dictionary that don't exist in self
|
||||
[b enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL *_Nonnull stop) {
|
||||
id aObj = a[key];
|
||||
// Find objects in `got` that don't exist in `want`
|
||||
[got enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
|
||||
id aObj = want[key];
|
||||
|
||||
if (!aObj) {
|
||||
delta[key] = obj;
|
||||
delta[@"want"][key] = @"Key missing";
|
||||
delta[@"got"][key] = obj;
|
||||
}
|
||||
}];
|
||||
|
||||
return delta;
|
||||
return [delta[@"want"] count] > 0 ? delta : nil;
|
||||
}
|
||||
|
||||
void SerializeAndCheck(es_event_type_t eventType,
|
||||
@@ -268,9 +277,9 @@ void SerializeAndCheck(es_event_type_t eventType,
|
||||
error:&jsonError];
|
||||
XCTAssertNil(jsonError, @"failed to parse got data as JSON");
|
||||
|
||||
XCTAssertNil(FindDelta(wantJSONDict, gotJSONDict));
|
||||
// Note: Uncomment this line to help create testfile JSON when the assert above fails
|
||||
// XCTAssertEqualObjects([NSString stringWithUTF8String:gotData.c_str()], wantData);
|
||||
NSDictionary *delta = findDelta(wantJSONDict, gotJSONDict);
|
||||
XCTAssertEqualObjects(@{}, delta);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
@@ -338,6 +347,22 @@ void SerializeAndCheckNonESEvents(
|
||||
self.testCachedDecision.quarantineURL = @"google.com";
|
||||
self.testCachedDecision.certSHA256 = @"5678_cert_hash";
|
||||
self.testCachedDecision.decisionClientMode = SNTClientModeLockdown;
|
||||
self.testCachedDecision.entitlements = @{
|
||||
@"key_with_str_val" : @"bar",
|
||||
@"key_with_num_val" : @(1234),
|
||||
@"key_with_date_val" : [NSDate dateWithTimeIntervalSince1970:1699376402],
|
||||
@"key_with_data_val" : [@"Hello World" dataUsingEncoding:NSUTF8StringEncoding],
|
||||
@"key_with_arr_val" : @[ @"v1", @"v2", @"v3" ],
|
||||
@"key_with_arr_val_nested" : @[ @"v1", @"v2", @"v3", @[ @"nv1", @"nv2" ] ],
|
||||
@"key_with_arr_val_multitype" :
|
||||
@[ @"v1", @"v2", @"v3", @(123), [NSDate dateWithTimeIntervalSince1970:1699376402] ],
|
||||
@"key_with_dict_val" : @{@"k1" : @"v1", @"k2" : @"v2"},
|
||||
@"key_with_dict_val_nested" : @{
|
||||
@"k1" : @"v1",
|
||||
@"k2" : @"v2",
|
||||
@"k3" : @{@"nk1" : @"nv1", @"nk2" : [NSDate dateWithTimeIntervalSince1970:1699376402]}
|
||||
},
|
||||
};
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
@@ -573,6 +598,70 @@ void SerializeAndCheckNonESEvents(
|
||||
json:YES];
|
||||
}
|
||||
|
||||
- (void)testEncodeEntitlements {
|
||||
int kMaxEncodeObjectEntries = 64; // From Protobuf.mm
|
||||
// Test basic encoding without filtered entitlements
|
||||
{
|
||||
::pbv1::Execution pbExec;
|
||||
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.entitlements = @{@"com.google.test" : @(YES)};
|
||||
|
||||
XCTAssertEqual(0, pbExec.entitlement_info().entitlements_size());
|
||||
XCTAssertFalse(cd.entitlementsFiltered);
|
||||
XCTAssertEqual(1, cd.entitlements.count);
|
||||
|
||||
EncodeEntitlements(&pbExec, cd);
|
||||
|
||||
XCTAssertEqual(1, pbExec.entitlement_info().entitlements_size());
|
||||
XCTAssertTrue(pbExec.entitlement_info().has_entitlements_filtered());
|
||||
XCTAssertFalse(pbExec.entitlement_info().entitlements_filtered());
|
||||
}
|
||||
|
||||
// Test basic encoding with filtered entitlements
|
||||
{
|
||||
::pbv1::Execution pbExec;
|
||||
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.entitlements = @{@"com.google.test" : @(YES), @"com.google.test2" : @(NO)};
|
||||
cd.entitlementsFiltered = YES;
|
||||
|
||||
XCTAssertEqual(0, pbExec.entitlement_info().entitlements_size());
|
||||
XCTAssertTrue(cd.entitlementsFiltered);
|
||||
XCTAssertEqual(2, cd.entitlements.count);
|
||||
|
||||
EncodeEntitlements(&pbExec, cd);
|
||||
|
||||
XCTAssertEqual(2, pbExec.entitlement_info().entitlements_size());
|
||||
XCTAssertTrue(pbExec.entitlement_info().has_entitlements_filtered());
|
||||
XCTAssertTrue(pbExec.entitlement_info().entitlements_filtered());
|
||||
}
|
||||
|
||||
// Test max number of entitlements logged
|
||||
// When entitlements are clipped, `entitlements_filtered` is set to true
|
||||
{
|
||||
::pbv1::Execution pbExec;
|
||||
NSMutableDictionary *ents = [NSMutableDictionary dictionary];
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ents[[NSString stringWithFormat:@"k%d", i]] = @(i);
|
||||
}
|
||||
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.entitlements = ents;
|
||||
|
||||
XCTAssertEqual(0, pbExec.entitlement_info().entitlements_size());
|
||||
XCTAssertFalse(cd.entitlementsFiltered);
|
||||
XCTAssertGreaterThan(cd.entitlements.count, kMaxEncodeObjectEntries);
|
||||
|
||||
EncodeEntitlements(&pbExec, cd);
|
||||
|
||||
XCTAssertEqual(kMaxEncodeObjectEntries, pbExec.entitlement_info().entitlements_size());
|
||||
XCTAssertTrue(pbExec.entitlement_info().has_entitlements_filtered());
|
||||
XCTAssertTrue(pbExec.entitlement_info().entitlements_filtered());
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExit {
|
||||
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_EXIT
|
||||
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
|
||||
@@ -59,6 +59,8 @@ NSString *MountFromName(NSString *path);
|
||||
es_file_t *GetAllowListTargetFile(
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg);
|
||||
|
||||
const mach_port_t GetDefaultIOKitCommsPort();
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers::Utilities
|
||||
|
||||
#endif
|
||||
|
||||
@@ -80,7 +80,7 @@ NSString *OriginalPathForTranslocation(const es_process_t *es_proc) {
|
||||
return [origURL path];
|
||||
}
|
||||
|
||||
static inline const mach_port_t GetDefaultIOKitCommsPort() {
|
||||
const mach_port_t GetDefaultIOKitCommsPort() {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return kIOMasterPortDefault;
|
||||
|
||||
@@ -53,6 +53,7 @@ enum class FileAccessMetricStatus {
|
||||
|
||||
using EventCountTuple = std::tuple<Processor, es_event_type_t, EventDisposition>;
|
||||
using EventTimesTuple = std::tuple<Processor, es_event_type_t>;
|
||||
using EventStatsTuple = std::tuple<Processor, es_event_type_t>;
|
||||
using FileAccessMetricsPolicyVersion = std::string;
|
||||
using FileAccessMetricsPolicyName = std::string;
|
||||
using FileAccessEventCountTuple =
|
||||
@@ -65,8 +66,9 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
|
||||
Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
|
||||
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *faa_event_counts,
|
||||
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *));
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *drop_counts,
|
||||
SNTMetricCounter *faa_event_counts, SNTMetricSet *metric_set,
|
||||
void (^run_on_first_start)(Metrics *));
|
||||
|
||||
~Metrics();
|
||||
|
||||
@@ -78,6 +80,9 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
// Force an immediate flush and export of metrics
|
||||
void Export();
|
||||
|
||||
// Used for tracking event sequence numbers to determine if drops occured
|
||||
void UpdateEventStats(Processor processor, const es_message_t *msg);
|
||||
|
||||
void SetEventMetrics(Processor processor, es_event_type_t event_type,
|
||||
EventDisposition disposition, int64_t nanos);
|
||||
|
||||
@@ -90,6 +95,11 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
friend class santa::santad::MetricsPeer;
|
||||
|
||||
private:
|
||||
struct SequenceStats {
|
||||
uint64_t next_seq_num = 0;
|
||||
int64_t drops = 0;
|
||||
};
|
||||
|
||||
void FlushMetrics();
|
||||
void ExportLocked(SNTMetricSet *metric_set);
|
||||
|
||||
@@ -101,6 +111,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
SNTMetricCounter *event_counts_;
|
||||
SNTMetricCounter *rate_limit_counts_;
|
||||
SNTMetricCounter *faa_event_counts_;
|
||||
SNTMetricCounter *drop_counts_;
|
||||
SNTMetricSet *metric_set_;
|
||||
// Tracks whether or not the timer_source should be running.
|
||||
// This helps manage dispatch source state to ensure the source is not
|
||||
@@ -117,6 +128,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
std::map<EventTimesTuple, int64_t> event_times_cache_;
|
||||
std::map<Processor, int64_t> rate_limit_counts_cache_;
|
||||
std::map<FileAccessEventCountTuple, int64_t> faa_event_counts_cache_;
|
||||
std::map<EventStatsTuple, SequenceStats> drop_cache_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad
|
||||
|
||||
@@ -49,6 +49,7 @@ static NSString *const kEventTypeNotifyLink = @"NotifyLink";
|
||||
static NSString *const kEventTypeNotifyRename = @"NotifyRename";
|
||||
static NSString *const kEventTypeNotifyUnlink = @"NotifyUnlink";
|
||||
static NSString *const kEventTypeNotifyUnmount = @"NotifyUnmount";
|
||||
static NSString *const kPseudoEventTypeGlobal = @"Global";
|
||||
|
||||
static NSString *const kEventDispositionDropped = @"Dropped";
|
||||
static NSString *const kEventDispositionProcessed = @"Processed";
|
||||
@@ -103,6 +104,7 @@ NSString *const EventTypeToString(es_event_type_t eventType) {
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: return kEventTypeNotifyRename;
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK: return kEventTypeNotifyUnlink;
|
||||
case ES_EVENT_TYPE_NOTIFY_UNMOUNT: return kEventTypeNotifyUnmount;
|
||||
case ES_EVENT_TYPE_LAST: return kPseudoEventTypeGlobal;
|
||||
default:
|
||||
[NSException raise:@"Invalid event type" format:@"Invalid event type: %d", eventType];
|
||||
return nil;
|
||||
@@ -174,12 +176,17 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
|
||||
]
|
||||
helpText:@"Count of times a log is emitted from the File Access Authorizer client"];
|
||||
|
||||
std::shared_ptr<Metrics> metrics =
|
||||
std::make_shared<Metrics>(q, timer_source, interval, event_processing_times, event_counts,
|
||||
rate_limit_counts, faa_event_counts, metric_set, ^(Metrics *metrics) {
|
||||
SNTRegisterCoreMetrics();
|
||||
metrics->EstablishConnection();
|
||||
});
|
||||
SNTMetricCounter *drop_counts =
|
||||
[metric_set counterWithName:@"/santa/event_drop_count"
|
||||
fieldNames:@[ @"Processor", @"Event" ]
|
||||
helpText:@"Count of the number of drops for each event"];
|
||||
|
||||
std::shared_ptr<Metrics> metrics = std::make_shared<Metrics>(
|
||||
q, timer_source, interval, event_processing_times, event_counts, rate_limit_counts,
|
||||
faa_event_counts, drop_counts, metric_set, ^(Metrics *metrics) {
|
||||
SNTRegisterCoreMetrics();
|
||||
metrics->EstablishConnection();
|
||||
});
|
||||
|
||||
std::weak_ptr<Metrics> weak_metrics(metrics);
|
||||
dispatch_source_set_event_handler(metrics->timer_source_, ^{
|
||||
@@ -197,7 +204,8 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
|
||||
Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
|
||||
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricCounter *faa_event_counts,
|
||||
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *))
|
||||
SNTMetricCounter *drop_counts, SNTMetricSet *metric_set,
|
||||
void (^run_on_first_start)(Metrics *))
|
||||
: q_(q),
|
||||
timer_source_(timer_source),
|
||||
interval_(interval),
|
||||
@@ -205,6 +213,7 @@ Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t in
|
||||
event_counts_(event_counts),
|
||||
rate_limit_counts_(rate_limit_counts),
|
||||
faa_event_counts_(faa_event_counts),
|
||||
drop_counts_(drop_counts),
|
||||
metric_set_(metric_set),
|
||||
run_on_first_start_(run_on_first_start) {
|
||||
SetInterval(interval_);
|
||||
@@ -285,7 +294,22 @@ void Metrics::FlushMetrics() {
|
||||
]];
|
||||
}
|
||||
|
||||
for (auto &[key, stats] : drop_cache_) {
|
||||
if (stats.drops > 0) {
|
||||
NSString *processorName = ProcessorToString(std::get<Processor>(key));
|
||||
NSString *eventName = EventTypeToString(std::get<es_event_type_t>(key));
|
||||
|
||||
[drop_counts_ incrementBy:stats.drops forFieldValues:@[ processorName, eventName ]];
|
||||
|
||||
// Reset drops to 0, but leave sequence number intact. Sequence numbers
|
||||
// must persist so that accurate drops can be counted.
|
||||
stats.drops = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the maps so the next cycle begins with a clean state
|
||||
// IMPORTANT: Do not reset drop_cache_, the sequence numbers must persist
|
||||
// for accurate accounting
|
||||
event_counts_cache_ = {};
|
||||
event_times_cache_ = {};
|
||||
rate_limit_counts_cache_ = {};
|
||||
@@ -339,6 +363,41 @@ void Metrics::SetEventMetrics(Processor processor, es_event_type_t event_type,
|
||||
});
|
||||
}
|
||||
|
||||
void Metrics::UpdateEventStats(Processor processor, const es_message_t *msg) {
|
||||
dispatch_sync(events_q_, ^{
|
||||
EventStatsTuple event_stats_key{processor, msg->event_type};
|
||||
// NB: Using the invalid event type ES_EVENT_TYPE_LAST to store drop counts
|
||||
// based on the global sequence number.
|
||||
EventStatsTuple global_stats_key{processor, ES_EVENT_TYPE_LAST};
|
||||
|
||||
SequenceStats old_event_stats = drop_cache_[event_stats_key];
|
||||
SequenceStats old_global_stats = drop_cache_[global_stats_key];
|
||||
|
||||
// Sequence number should always increment by 1.
|
||||
// Drops detected if there is a difference between the current sequence
|
||||
// number and the expected sequence number
|
||||
int64_t new_event_drops = msg->seq_num - old_event_stats.next_seq_num;
|
||||
int64_t new_global_drops = msg->global_seq_num - old_global_stats.next_seq_num;
|
||||
|
||||
// Only log one or the other, prefer the event specific log.
|
||||
// For higher volume event types, we'll normally see the event-specific log eventually
|
||||
// For lower volume event types, we may only see the global drop message
|
||||
if (new_event_drops > 0) {
|
||||
LOGD(@"Drops detected for client: %@, event: %@, drops: %llu", ProcessorToString(processor),
|
||||
EventTypeToString(msg->event_type), new_event_drops);
|
||||
} else if (new_global_drops > 0) {
|
||||
LOGD(@"Drops detected globally for client: %@, drops: %llu", ProcessorToString(processor),
|
||||
new_global_drops);
|
||||
}
|
||||
|
||||
drop_cache_[event_stats_key] = SequenceStats{.next_seq_num = msg->seq_num + 1,
|
||||
.drops = old_event_stats.drops + new_event_drops};
|
||||
|
||||
drop_cache_[global_stats_key] = SequenceStats{
|
||||
.next_seq_num = msg->global_seq_num + 1, .drops = old_global_stats.drops + new_global_drops};
|
||||
});
|
||||
}
|
||||
|
||||
void Metrics::SetRateLimitingMetrics(Processor processor, int64_t events_rate_limited_count) {
|
||||
dispatch_sync(events_q_, ^{
|
||||
rate_limit_counts_cache_[processor] += events_rate_limited_count;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
using santa::santad::EventCountTuple;
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::EventStatsTuple;
|
||||
using santa::santad::EventTimesTuple;
|
||||
using santa::santad::FileAccessEventCountTuple;
|
||||
using santa::santad::Processor;
|
||||
@@ -47,12 +48,15 @@ class MetricsPeer : public Metrics {
|
||||
using Metrics::FlushMetrics;
|
||||
|
||||
// Private member variables
|
||||
using Metrics::drop_cache_;
|
||||
using Metrics::event_counts_cache_;
|
||||
using Metrics::event_times_cache_;
|
||||
using Metrics::faa_event_counts_cache_;
|
||||
using Metrics::interval_;
|
||||
using Metrics::rate_limit_counts_cache_;
|
||||
using Metrics::running_;
|
||||
|
||||
using Metrics::SequenceStats;
|
||||
};
|
||||
|
||||
} // namespace santa::santad
|
||||
@@ -62,9 +66,15 @@ using santa::santad::EventTypeToString;
|
||||
using santa::santad::FileAccessMetricStatus;
|
||||
using santa::santad::FileAccessMetricStatusToString;
|
||||
using santa::santad::FileAccessPolicyDecisionToString;
|
||||
using santa::santad::Metrics;
|
||||
using santa::santad::MetricsPeer;
|
||||
using santa::santad::ProcessorToString;
|
||||
|
||||
std::shared_ptr<MetricsPeer> CreateBasicMetricsPeer(dispatch_queue_t q, void (^block)(Metrics *)) {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
|
||||
return std::make_shared<MetricsPeer>(q, timer, 100, nil, nil, nil, nil, nil, nil, block);
|
||||
}
|
||||
|
||||
@interface MetricsTest : XCTestCase
|
||||
@property dispatch_queue_t q;
|
||||
@property dispatch_semaphore_t sema;
|
||||
@@ -79,11 +89,9 @@ using santa::santad::ProcessorToString;
|
||||
}
|
||||
|
||||
- (void)testStartStop {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m) {
|
||||
dispatch_semaphore_signal(self.sema);
|
||||
});
|
||||
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *) {
|
||||
dispatch_semaphore_signal(self.sema);
|
||||
});
|
||||
|
||||
XCTAssertFalse(metrics->running_);
|
||||
|
||||
@@ -115,10 +123,8 @@ using santa::santad::ProcessorToString;
|
||||
}
|
||||
|
||||
- (void)testSetInterval {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
});
|
||||
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
|
||||
});
|
||||
|
||||
XCTAssertEqual(100, metrics->interval_);
|
||||
|
||||
@@ -164,6 +170,7 @@ using santa::santad::ProcessorToString;
|
||||
{ES_EVENT_TYPE_NOTIFY_RENAME, @"NotifyRename"},
|
||||
{ES_EVENT_TYPE_NOTIFY_UNLINK, @"NotifyUnlink"},
|
||||
{ES_EVENT_TYPE_NOTIFY_UNMOUNT, @"NotifyUnmount"},
|
||||
{ES_EVENT_TYPE_LAST, @"Global"},
|
||||
};
|
||||
|
||||
for (const auto &kv : eventTypeToString) {
|
||||
@@ -224,11 +231,8 @@ using santa::santad::ProcessorToString;
|
||||
- (void)testSetEventMetrics {
|
||||
int64_t nanos = 1234;
|
||||
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
|
||||
});
|
||||
|
||||
// Initial maps are empty
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
|
||||
@@ -265,11 +269,8 @@ using santa::santad::ProcessorToString;
|
||||
}
|
||||
|
||||
- (void)testSetRateLimitingMetrics {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
|
||||
});
|
||||
|
||||
// Initial map is empty
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
|
||||
@@ -291,11 +292,8 @@ using santa::santad::ProcessorToString;
|
||||
}
|
||||
|
||||
- (void)testSetFileAccessEventMetrics {
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
|
||||
});
|
||||
|
||||
// Initial map is empty
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
|
||||
@@ -326,11 +324,64 @@ using santa::santad::ProcessorToString;
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_[ruleXyz], 1);
|
||||
}
|
||||
|
||||
- (void)testUpdateEventStats {
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXEC, NULL);
|
||||
esMsg.seq_num = 0;
|
||||
esMsg.global_seq_num = 0;
|
||||
|
||||
std::shared_ptr<MetricsPeer> metrics = CreateBasicMetricsPeer(self.q, ^(Metrics *){
|
||||
});
|
||||
|
||||
EventStatsTuple eventStats{Processor::kRecorder, ES_EVENT_TYPE_NOTIFY_EXEC};
|
||||
EventStatsTuple globalStats{Processor::kRecorder, ES_EVENT_TYPE_LAST};
|
||||
|
||||
// Map does not initially contain entries
|
||||
XCTAssertEqual(0, metrics->drop_cache_.size());
|
||||
|
||||
metrics->UpdateEventStats(Processor::kRecorder, &esMsg);
|
||||
|
||||
// After the first update, 2 entries exist, one for the event, and one for global
|
||||
XCTAssertEqual(2, metrics->drop_cache_.size());
|
||||
XCTAssertEqual(1, metrics->drop_cache_[eventStats].next_seq_num);
|
||||
XCTAssertEqual(0, metrics->drop_cache_[eventStats].drops);
|
||||
XCTAssertEqual(1, metrics->drop_cache_[globalStats].next_seq_num);
|
||||
XCTAssertEqual(0, metrics->drop_cache_[globalStats].drops);
|
||||
|
||||
// Increment sequence numbers by 1 and check that no drop was detected
|
||||
esMsg.seq_num++;
|
||||
esMsg.global_seq_num++;
|
||||
|
||||
metrics->UpdateEventStats(Processor::kRecorder, &esMsg);
|
||||
|
||||
XCTAssertEqual(2, metrics->drop_cache_.size());
|
||||
XCTAssertEqual(2, metrics->drop_cache_[eventStats].next_seq_num);
|
||||
XCTAssertEqual(0, metrics->drop_cache_[eventStats].drops);
|
||||
XCTAssertEqual(2, metrics->drop_cache_[globalStats].next_seq_num);
|
||||
XCTAssertEqual(0, metrics->drop_cache_[globalStats].drops);
|
||||
|
||||
// Now incremenet sequence numbers by a large amount to trigger drop detection
|
||||
esMsg.seq_num += 10;
|
||||
esMsg.global_seq_num += 10;
|
||||
|
||||
metrics->UpdateEventStats(Processor::kRecorder, &esMsg);
|
||||
|
||||
XCTAssertEqual(2, metrics->drop_cache_.size());
|
||||
XCTAssertEqual(12, metrics->drop_cache_[eventStats].next_seq_num);
|
||||
XCTAssertEqual(9, metrics->drop_cache_[eventStats].drops);
|
||||
XCTAssertEqual(12, metrics->drop_cache_[globalStats].next_seq_num);
|
||||
XCTAssertEqual(9, metrics->drop_cache_[globalStats].drops);
|
||||
}
|
||||
|
||||
- (void)testFlushMetrics {
|
||||
id mockEventProcessingTimes = OCMClassMock([SNTMetricInt64Gauge class]);
|
||||
id mockEventCounts = OCMClassMock([SNTMetricCounter class]);
|
||||
int64_t nanos = 1234;
|
||||
|
||||
// Initial update will have non-zero sequence numbers, triggering drop detection
|
||||
es_message_t esMsgWithDrops = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXEC, NULL);
|
||||
esMsgWithDrops.seq_num = 123;
|
||||
esMsgWithDrops.global_seq_num = 123;
|
||||
|
||||
OCMStub([mockEventCounts incrementBy:0 forFieldValues:[OCMArg any]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(^(NSInvocation *inv) {
|
||||
@@ -346,7 +397,7 @@ using santa::santad::ProcessorToString;
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
auto metrics =
|
||||
std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes, mockEventCounts,
|
||||
mockEventCounts, mockEventCounts, nil,
|
||||
mockEventCounts, mockEventCounts, mockEventCounts, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
@@ -355,6 +406,7 @@ using santa::santad::ProcessorToString;
|
||||
EventDisposition::kProcessed, nanos);
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN,
|
||||
EventDisposition::kProcessed, nanos * 2);
|
||||
metrics->UpdateEventStats(Processor::kRecorder, &esMsgWithDrops);
|
||||
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
|
||||
metrics->SetFileAccessEventMetrics("v1.0", "rule_abc", FileAccessMetricStatus::kOK,
|
||||
ES_EVENT_TYPE_AUTH_OPEN, FileAccessPolicyDecision::kDenied);
|
||||
@@ -364,24 +416,40 @@ using santa::santad::ProcessorToString;
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 2);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 1);
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 1);
|
||||
XCTAssertEqual(metrics->drop_cache_.size(), 2);
|
||||
|
||||
EventStatsTuple eventStats{Processor::kRecorder, esMsgWithDrops.event_type};
|
||||
EventStatsTuple globalStats{Processor::kRecorder, ES_EVENT_TYPE_LAST};
|
||||
XCTAssertEqual(metrics->drop_cache_[eventStats].next_seq_num, 124);
|
||||
XCTAssertEqual(metrics->drop_cache_[eventStats].drops, 123);
|
||||
XCTAssertEqual(metrics->drop_cache_[globalStats].next_seq_num, 124);
|
||||
XCTAssertEqual(metrics->drop_cache_[globalStats].drops, 123);
|
||||
|
||||
metrics->FlushMetrics();
|
||||
|
||||
// After setting two different event metrics, we expect the sema to be hit
|
||||
// five times - twice each for the event counts and event times maps, and
|
||||
// once for the rate limit count map.
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (1)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (2)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (3)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (4)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (5)");
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush (6)");
|
||||
// Expected call count is 8:
|
||||
// 2: event counts
|
||||
// 2: event times
|
||||
// 1: rate limit
|
||||
// 1: FAA
|
||||
// 2: drops (1 event, 1 global)
|
||||
int expectedCalls = 8;
|
||||
for (int i = 0; i < expectedCalls; i++) {
|
||||
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to flush");
|
||||
}
|
||||
|
||||
// After a flush, map sizes should be reset to 0
|
||||
XCTAssertEqual(metrics->event_counts_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->event_times_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->rate_limit_counts_cache_.size(), 0);
|
||||
XCTAssertEqual(metrics->faa_event_counts_cache_.size(), 0);
|
||||
// Note: The drop_cache_ should not be reset back to size 0. Instead, each
|
||||
// entry has the sequence number left intact, but drop counts reset to 0.
|
||||
XCTAssertEqual(metrics->drop_cache_.size(), 2);
|
||||
XCTAssertEqual(metrics->drop_cache_[eventStats].next_seq_num, 124);
|
||||
XCTAssertEqual(metrics->drop_cache_[eventStats].drops, 0);
|
||||
XCTAssertEqual(metrics->drop_cache_[globalStats].next_seq_num, 124);
|
||||
XCTAssertEqual(metrics->drop_cache_[globalStats].drops, 0);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
69
Source/santad/ProcessTree/BUILD
Normal file
69
Source/santad/ProcessTree/BUILD
Normal file
@@ -0,0 +1,69 @@
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
|
||||
|
||||
cc_library(
|
||||
name = "process",
|
||||
hdrs = ["process.h"],
|
||||
deps = [
|
||||
"//Source/santad/ProcessTree/annotations:annotator",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "process_tree",
|
||||
srcs = [
|
||||
"process_tree.cc",
|
||||
"process_tree_macos.mm",
|
||||
],
|
||||
hdrs = ["process_tree.h"],
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
":process",
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
"//Source/santad/ProcessTree/annotations:annotator",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/container:flat_hash_set",
|
||||
"@com_google_absl//absl/status",
|
||||
"@com_google_absl//absl/status:statusor",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
proto_library(
|
||||
name = "process_tree_proto",
|
||||
srcs = ["process_tree.proto"],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
cc_proto_library(
|
||||
name = "process_tree_cc_proto",
|
||||
deps = [":process_tree_proto"],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "process_tree_test_helpers",
|
||||
srcs = ["process_tree_test_helpers.mm"],
|
||||
hdrs = ["process_tree_test_helpers.h"],
|
||||
deps = [
|
||||
":process_tree",
|
||||
"@com_google_absl//absl/synchronization",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "process_tree_test",
|
||||
srcs = ["process_tree_test.mm"],
|
||||
deps = [
|
||||
":process",
|
||||
":process_tree_test_helpers",
|
||||
"//Source/santad/ProcessTree/annotations:annotator",
|
||||
],
|
||||
)
|
||||
11
Source/santad/ProcessTree/annotations/BUILD
Normal file
11
Source/santad/ProcessTree/annotations/BUILD
Normal file
@@ -0,0 +1,11 @@
|
||||
package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "annotator",
|
||||
hdrs = ["annotator.h"],
|
||||
deps = [
|
||||
"//Source/santad/ProcessTree:process_tree_cc_proto",
|
||||
],
|
||||
)
|
||||
40
Source/santad/ProcessTree/annotations/annotator.h
Normal file
40
Source/santad/ProcessTree/annotations/annotator.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/// Copyright 2023 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.
|
||||
#ifndef SANTA__SANTAD_PROCESSTREE_ANNOTATIONS_BASE_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_ANNOTATIONS_BASE_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "Source/santad/ProcessTree/process_tree.pb.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
class ProcessTree;
|
||||
class Process;
|
||||
|
||||
class Annotator {
|
||||
public:
|
||||
virtual ~Annotator() = default;
|
||||
|
||||
virtual void AnnotateFork(ProcessTree &tree, const Process &parent,
|
||||
const Process &child) = 0;
|
||||
virtual void AnnotateExec(ProcessTree &tree, const Process &orig_process,
|
||||
const Process &new_process) = 0;
|
||||
virtual std::optional<::santa::pb::v1::process_tree::Annotations> Proto()
|
||||
const = 0;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
114
Source/santad/ProcessTree/process.h
Normal file
114
Source/santad/ProcessTree/process.h
Normal file
@@ -0,0 +1,114 @@
|
||||
/// Copyright 2023 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.
|
||||
#ifndef SANTA__SANTAD_PROCESSTREE_PROCESS_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_PROCESS_H
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <typeindex>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/ProcessTree/annotations/annotator.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
struct Pid {
|
||||
pid_t pid;
|
||||
uint64_t pidversion;
|
||||
|
||||
friend bool operator==(const struct Pid &lhs, const struct Pid &rhs) {
|
||||
return lhs.pid == rhs.pid && lhs.pidversion == rhs.pidversion;
|
||||
}
|
||||
friend bool operator!=(const struct Pid &lhs, const struct Pid &rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename H>
|
||||
H AbslHashValue(H h, const struct Pid &p) {
|
||||
return H::combine(std::move(h), p.pid, p.pidversion);
|
||||
}
|
||||
|
||||
struct Cred {
|
||||
uid_t uid;
|
||||
gid_t gid;
|
||||
|
||||
friend bool operator==(const struct Cred &lhs, const struct Cred &rhs) {
|
||||
return lhs.uid == rhs.uid && lhs.gid == rhs.gid;
|
||||
}
|
||||
friend bool operator!=(const struct Cred &lhs, const struct Cred &rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
};
|
||||
|
||||
struct Program {
|
||||
std::string executable;
|
||||
std::vector<std::string> arguments;
|
||||
|
||||
friend bool operator==(const struct Program &lhs, const struct Program &rhs) {
|
||||
return lhs.executable == rhs.executable && lhs.arguments == rhs.arguments;
|
||||
}
|
||||
friend bool operator!=(const struct Program &lhs, const struct Program &rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
};
|
||||
|
||||
// Fwd decls
|
||||
class ProcessTree;
|
||||
|
||||
class Process {
|
||||
public:
|
||||
explicit Process(const Pid pid, const Cred cred,
|
||||
std::shared_ptr<const Program> program,
|
||||
std::shared_ptr<const Process> parent)
|
||||
: pid_(pid),
|
||||
effective_cred_(cred),
|
||||
program_(program),
|
||||
annotations_(),
|
||||
parent_(parent),
|
||||
refcnt_(0),
|
||||
tombstoned_(false) {}
|
||||
Process(const Process &) = default;
|
||||
Process &operator=(const Process &) = delete;
|
||||
Process(Process &&) = default;
|
||||
Process &operator=(Process &&) = delete;
|
||||
|
||||
// Const "attributes" are public
|
||||
const struct Pid pid_;
|
||||
const struct Cred effective_cred_;
|
||||
const std::shared_ptr<const Program> program_;
|
||||
|
||||
private:
|
||||
// This is not API.
|
||||
// The tree helper methods are the API, and we just happen to implement
|
||||
// annotation storage and the parent relation in memory on the process right
|
||||
// now.
|
||||
friend class ProcessTree;
|
||||
absl::flat_hash_map<std::type_index, std::shared_ptr<const Annotator>>
|
||||
annotations_;
|
||||
std::shared_ptr<const Process> parent_;
|
||||
// TODO(nickmg): atomic here breaks the build.
|
||||
int refcnt_;
|
||||
// If the process is tombstoned, the event removing it from the tree has been
|
||||
// processed, but refcnt>0 keeps it alive.
|
||||
bool tombstoned_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
311
Source/santad/ProcessTree/process_tree.cc
Normal file
311
Source/santad/ProcessTree/process_tree.cc
Normal file
@@ -0,0 +1,311 @@
|
||||
/// Copyright 2023 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.
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <typeindex>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/ProcessTree/annotations/annotator.h"
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.pb.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
void ProcessTree::BackfillInsertChildren(
|
||||
absl::flat_hash_map<pid_t, std::vector<Process>> &parent_map,
|
||||
std::shared_ptr<Process> parent, const Process &unlinked_proc) {
|
||||
auto proc = std::make_shared<Process>(
|
||||
unlinked_proc.pid_, unlinked_proc.effective_cred_,
|
||||
// Re-use shared pointers from parent if value equivalent
|
||||
(parent && *(unlinked_proc.program_) == *(parent->program_))
|
||||
? parent->program_
|
||||
: unlinked_proc.program_,
|
||||
parent);
|
||||
{
|
||||
absl::MutexLock lock(&mtx_);
|
||||
map_.emplace(unlinked_proc.pid_, proc);
|
||||
}
|
||||
|
||||
// The only case where we should not have a parent is the root processes
|
||||
// (e.g. init, kthreadd).
|
||||
if (parent) {
|
||||
for (auto &annotator : annotators_) {
|
||||
annotator->AnnotateFork(*this, *(proc->parent_), *proc);
|
||||
if (proc->program_ != proc->parent_->program_) {
|
||||
annotator->AnnotateExec(*this, *(proc->parent_), *proc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const Process &child : parent_map[unlinked_proc.pid_.pid]) {
|
||||
BackfillInsertChildren(parent_map, proc, child);
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessTree::HandleFork(uint64_t timestamp, const Process &parent,
|
||||
const Pid new_pid) {
|
||||
if (Step(timestamp)) {
|
||||
std::shared_ptr<Process> child;
|
||||
{
|
||||
absl::MutexLock lock(&mtx_);
|
||||
child = std::make_shared<Process>(new_pid, parent.effective_cred_,
|
||||
parent.program_, map_[parent.pid_]);
|
||||
map_.emplace(new_pid, child);
|
||||
}
|
||||
for (const auto &annotator : annotators_) {
|
||||
annotator->AnnotateFork(*this, parent, *child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessTree::HandleExec(uint64_t timestamp, const Process &p,
|
||||
const Pid new_pid, const Program prog,
|
||||
const Cred c) {
|
||||
if (Step(timestamp)) {
|
||||
// TODO(nickmg): should struct pid be reworked and only pid_version be
|
||||
// passed?
|
||||
assert(new_pid.pid == p.pid_.pid);
|
||||
|
||||
auto new_proc = std::make_shared<Process>(
|
||||
new_pid, c, std::make_shared<const Program>(prog), p.parent_);
|
||||
{
|
||||
absl::MutexLock lock(&mtx_);
|
||||
remove_at_.push_back({timestamp, p.pid_});
|
||||
map_.emplace(new_proc->pid_, new_proc);
|
||||
}
|
||||
for (const auto &annotator : annotators_) {
|
||||
annotator->AnnotateExec(*this, p, *new_proc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessTree::HandleExit(uint64_t timestamp, const Process &p) {
|
||||
if (Step(timestamp)) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
remove_at_.push_back({timestamp, p.pid_});
|
||||
}
|
||||
}
|
||||
|
||||
bool ProcessTree::Step(uint64_t timestamp) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
uint64_t new_cutoff = seen_timestamps_.front();
|
||||
if (timestamp < new_cutoff) {
|
||||
// Event timestamp is before the rolling list of seen events.
|
||||
// This event may or may not have been processed, but be conservative and
|
||||
// do not reprocess.
|
||||
return false;
|
||||
}
|
||||
|
||||
// seen_timestamps_ is sorted, so only look for the value if it's possibly
|
||||
// within the array.
|
||||
if (timestamp < seen_timestamps_.back()) {
|
||||
// TODO(nickmg): If array is made bigger, replace with a binary search.
|
||||
for (const auto seen_ts : seen_timestamps_) {
|
||||
if (seen_ts == timestamp) {
|
||||
// Event seen, signal it should not be reprocessed.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto insert_point =
|
||||
std::find_if(seen_timestamps_.rbegin(), seen_timestamps_.rend(),
|
||||
[&](uint64_t x) { return x < timestamp; });
|
||||
std::move(seen_timestamps_.begin() + 1, insert_point.base(),
|
||||
seen_timestamps_.begin());
|
||||
*insert_point = timestamp;
|
||||
|
||||
for (auto it = remove_at_.begin(); it != remove_at_.end();) {
|
||||
if (it->first < new_cutoff) {
|
||||
if (auto target = GetLocked(it->second);
|
||||
target && (*target)->refcnt_ > 0) {
|
||||
(*target)->tombstoned_ = true;
|
||||
} else {
|
||||
map_.erase(it->second);
|
||||
}
|
||||
it = remove_at_.erase(it);
|
||||
} else {
|
||||
it++;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProcessTree::RetainProcess(std::vector<struct Pid> &pids) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
for (const struct Pid &p : pids) {
|
||||
auto proc = GetLocked(p);
|
||||
if (proc) {
|
||||
(*proc)->refcnt_++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessTree::ReleaseProcess(std::vector<struct Pid> &pids) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
for (const struct Pid &p : pids) {
|
||||
auto proc = GetLocked(p);
|
||||
if (proc) {
|
||||
if (--(*proc)->refcnt_ == 0 && (*proc)->tombstoned_) {
|
||||
map_.erase(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
---
|
||||
Annotation get/set
|
||||
---
|
||||
*/
|
||||
|
||||
void ProcessTree::AnnotateProcess(const Process &p,
|
||||
std::shared_ptr<const Annotator> a) {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
const Annotator &x = *a;
|
||||
map_[p.pid_]->annotations_.emplace(std::type_index(typeid(x)), std::move(a));
|
||||
}
|
||||
|
||||
std::optional<::santa::pb::v1::process_tree::Annotations>
|
||||
ProcessTree::ExportAnnotations(const Pid p) {
|
||||
auto proc = Get(p);
|
||||
if (!proc || (*proc)->annotations_.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
::santa::pb::v1::process_tree::Annotations a;
|
||||
for (const auto &[_, annotation] : (*proc)->annotations_) {
|
||||
if (auto x = annotation->Proto(); x) a.MergeFrom(*x);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
/*
|
||||
---
|
||||
Tree inspection methods
|
||||
---
|
||||
*/
|
||||
|
||||
std::vector<std::shared_ptr<const Process>> ProcessTree::RootSlice(
|
||||
std::shared_ptr<const Process> p) const {
|
||||
std::vector<std::shared_ptr<const Process>> slice;
|
||||
while (p) {
|
||||
slice.push_back(p);
|
||||
p = p->parent_;
|
||||
}
|
||||
return slice;
|
||||
}
|
||||
|
||||
void ProcessTree::Iterate(
|
||||
std::function<void(std::shared_ptr<const Process> p)> f) const {
|
||||
std::vector<std::shared_ptr<const Process>> procs;
|
||||
{
|
||||
absl::ReaderMutexLock lock(&mtx_);
|
||||
procs.reserve(map_.size());
|
||||
for (auto &[_, proc] : map_) {
|
||||
procs.push_back(proc);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &p : procs) {
|
||||
f(p);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::shared_ptr<const Process>> ProcessTree::Get(
|
||||
const Pid target) const {
|
||||
absl::ReaderMutexLock lock(&mtx_);
|
||||
return GetLocked(target);
|
||||
}
|
||||
|
||||
std::optional<std::shared_ptr<Process>> ProcessTree::GetLocked(
|
||||
const Pid target) const {
|
||||
auto it = map_.find(target);
|
||||
if (it == map_.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::shared_ptr<const Process> ProcessTree::GetParent(const Process &p) const {
|
||||
return p.parent_;
|
||||
}
|
||||
|
||||
#if SANTA_PROCESS_TREE_DEBUG
|
||||
void ProcessTree::DebugDump(std::ostream &stream) const {
|
||||
absl::ReaderMutexLock lock(&mtx_);
|
||||
stream << map_.size() << " processes" << std::endl;
|
||||
DebugDumpLocked(stream, 0, 0);
|
||||
}
|
||||
|
||||
void ProcessTree::DebugDumpLocked(std::ostream &stream, int depth,
|
||||
pid_t ppid) const
|
||||
ABSL_SHARED_LOCKS_REQUIRED(mtx_) {
|
||||
for (auto &[_, process] : map_) {
|
||||
if ((ppid == 0 && !process->parent_) ||
|
||||
(process->parent_ && process->parent_->pid_.pid == ppid)) {
|
||||
stream << std::string(2 * depth, ' ') << process->pid_.pid
|
||||
<< process->program_->executable << std::endl;
|
||||
DebugDumpLocked(stream, depth + 1, process->pid_.pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
absl::StatusOr<std::shared_ptr<ProcessTree>> CreateTree(
|
||||
std::vector<std::unique_ptr<Annotator>> &&annotations) {
|
||||
absl::flat_hash_set<std::type_index> seen;
|
||||
for (const auto &annotator : annotations) {
|
||||
if (seen.count(std::type_index(typeid(annotator)))) {
|
||||
return absl::InvalidArgumentError(
|
||||
"Multiple annotators of the same class");
|
||||
}
|
||||
seen.emplace(std::type_index(typeid(annotator)));
|
||||
}
|
||||
auto tree = std::make_shared<ProcessTree>(std::move(annotations));
|
||||
if (auto status = tree->Backfill(); !status.ok()) {
|
||||
return status;
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/*
|
||||
----
|
||||
Tokens
|
||||
----
|
||||
*/
|
||||
|
||||
ProcessToken::ProcessToken(std::shared_ptr<ProcessTree> tree,
|
||||
std::vector<struct Pid> pids)
|
||||
: tree_(std::move(tree)), pids_(std::move(pids)) {
|
||||
tree_->RetainProcess(pids);
|
||||
}
|
||||
|
||||
ProcessToken::~ProcessToken() { tree_->ReleaseProcess(pids_); }
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
191
Source/santad/ProcessTree/process_tree.h
Normal file
191
Source/santad/ProcessTree/process_tree.h
Normal file
@@ -0,0 +1,191 @@
|
||||
/// Copyright 2023 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.
|
||||
#ifndef SANTA__SANTAD_PROCESSTREE_TREE_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_TREE_H
|
||||
|
||||
#include <memory>
|
||||
#include <typeinfo>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
absl::StatusOr<Process> LoadPID(pid_t pid);
|
||||
|
||||
// Fwd decl for test peer.
|
||||
class ProcessTreeTestPeer;
|
||||
|
||||
class ProcessTree {
|
||||
public:
|
||||
explicit ProcessTree(std::vector<std::unique_ptr<Annotator>> &&annotators)
|
||||
: annotators_(std::move(annotators)), seen_timestamps_({}) {}
|
||||
ProcessTree(const ProcessTree &) = delete;
|
||||
ProcessTree &operator=(const ProcessTree &) = delete;
|
||||
ProcessTree(ProcessTree &&) = delete;
|
||||
ProcessTree &operator=(ProcessTree &&) = delete;
|
||||
|
||||
// Initialize the tree with the processes currently running on the system.
|
||||
absl::Status Backfill();
|
||||
|
||||
// Inform the tree of a fork event, in which the parent process spawns a child
|
||||
// with the only difference between the two being the pid.
|
||||
void HandleFork(uint64_t timestamp, const Process &parent,
|
||||
struct Pid new_pid);
|
||||
|
||||
// Inform the tree of an exec event, in which the program and potentially cred
|
||||
// of a Process change.
|
||||
// p is the process performing the exec (running the "old" program),
|
||||
// and new_pid, prog, and cred are the new pid, program, and credentials
|
||||
// after the exec.
|
||||
// N.B. new_pid is required as the "pid version" will have changed.
|
||||
// It is a programming error to pass a new_pid such that
|
||||
// p.pid_.pid != new_pid.pid.
|
||||
void HandleExec(uint64_t timestamp, const Process &p,
|
||||
struct Pid new_pid, struct Program prog,
|
||||
struct Cred c);
|
||||
|
||||
// Inform the tree of a process exit.
|
||||
void HandleExit(uint64_t timestamp, const Process &p);
|
||||
|
||||
// Mark the given pids as needing to be retained in the tree's map for future
|
||||
// access. Normally, Processes are removed once all clients process past the
|
||||
// event which would remove the Process (e.g. exit), however in cases where
|
||||
// async processing occurs, the Process may need to be accessed after the
|
||||
// exit.
|
||||
void RetainProcess(std::vector<struct Pid> &pids);
|
||||
|
||||
// Release previously retained processes, signaling that the client is done
|
||||
// processing the event that retained them.
|
||||
void ReleaseProcess(std::vector<struct Pid> &pids);
|
||||
|
||||
// Annotate the given process with an Annotator (state).
|
||||
void AnnotateProcess(const Process &p, std::shared_ptr<const Annotator> a);
|
||||
|
||||
// Get the given annotation on the given process if it exists, or nullopt if
|
||||
// the annotation is not set.
|
||||
template <typename T>
|
||||
std::optional<std::shared_ptr<const T>> GetAnnotation(const Process &p) const;
|
||||
|
||||
// Get the fully merged proto form of all annotations on the given process.
|
||||
std::optional<::santa::pb::v1::process_tree::Annotations> ExportAnnotations(
|
||||
struct Pid p);
|
||||
|
||||
// Atomically get the slice of Processes going from the given process "up"
|
||||
// to the root. The root process has no parent. N.B. There may be more than
|
||||
// one root process. E.g. on Linux, both init (PID 1) and kthread (PID 2)
|
||||
// are considered roots, as they are reported to have PPID=0.
|
||||
std::vector<std::shared_ptr<const Process>> RootSlice(
|
||||
std::shared_ptr<const Process> p) const;
|
||||
|
||||
// Call f for all processes in the tree. The list of processes is captured
|
||||
// before invoking f, so it is safe to mutate the tree in f.
|
||||
void Iterate(std::function<void(std::shared_ptr<const Process>)> f) const;
|
||||
|
||||
// Get the Process for the given pid in the tree if it exists.
|
||||
std::optional<std::shared_ptr<const Process>> Get(
|
||||
struct Pid target) const;
|
||||
|
||||
// Traverse the tree from the given Process to its parent.
|
||||
std::shared_ptr<const Process> GetParent(const Process &p) const;
|
||||
|
||||
#if SANTA_PROCESS_TREE_DEBUG
|
||||
// Dump the tree in a human readable form to the given ostream.
|
||||
void DebugDump(std::ostream &stream) const;
|
||||
#endif
|
||||
|
||||
private:
|
||||
friend class ProcessTreeTestPeer;
|
||||
void BackfillInsertChildren(
|
||||
absl::flat_hash_map<pid_t, std::vector<Process>> &parent_map,
|
||||
std::shared_ptr<Process> parent, const Process &unlinked_proc);
|
||||
|
||||
// Mark that an event with the given timestamp is being processed.
|
||||
// Returns whether the given timestamp is "novel", and the tree should be
|
||||
// updated with the results of the event.
|
||||
bool Step(uint64_t timestamp);
|
||||
|
||||
std::optional<std::shared_ptr<Process>> GetLocked(
|
||||
struct Pid target) const ABSL_SHARED_LOCKS_REQUIRED(mtx_);
|
||||
|
||||
void DebugDumpLocked(std::ostream &stream, int depth, pid_t ppid) const;
|
||||
|
||||
std::vector<std::unique_ptr<Annotator>> annotators_;
|
||||
|
||||
mutable absl::Mutex mtx_;
|
||||
absl::flat_hash_map<const struct Pid, std::shared_ptr<Process>> map_
|
||||
ABSL_GUARDED_BY(mtx_);
|
||||
// List of pids which should be removed from map_, and at the timestamp at
|
||||
// which they should be.
|
||||
// Elements are removed when the timestamp falls out of the seen_timestamps_
|
||||
// list below, signifying that all clients have synced past the timestamp.
|
||||
std::vector<std::pair<uint64_t, struct Pid>> remove_at_ ABSL_GUARDED_BY(mtx_);
|
||||
// Rolling list of event timestamps processed by the tree.
|
||||
// This is used to ensure an event only gets processed once, even if events
|
||||
// come out of order.
|
||||
std::array<uint64_t, 32> seen_timestamps_ ABSL_GUARDED_BY(mtx_);
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
std::optional<std::shared_ptr<const T>> ProcessTree::GetAnnotation(
|
||||
const Process &p) const {
|
||||
auto it = p.annotations_.find(std::type_index(typeid(T)));
|
||||
if (it == p.annotations_.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::dynamic_pointer_cast<const T>(it->second);
|
||||
}
|
||||
|
||||
// Create a new tree, ensuring the provided annotations are valid and that
|
||||
// backfill is successful.
|
||||
absl::StatusOr<std::shared_ptr<ProcessTree>> CreateTree(
|
||||
std::vector<std::unique_ptr<Annotator>> &&annotations);
|
||||
|
||||
// ProcessTokens provide a lifetime based approach to retaining processes
|
||||
// in a ProcessTree. When a token is created with a list of pids that may need
|
||||
// to be referenced during processing of a given event, the ProcessToken informs
|
||||
// the tree to retain those pids in its map so any call to ProcessTree::Get()
|
||||
// during event processing succeeds. When the token is destroyed, it signals the
|
||||
// tree to release the pids, which removes them from the tree if they would have
|
||||
// fallen out otherwise due to a destruction event (e.g. exit).
|
||||
class ProcessToken {
|
||||
public:
|
||||
explicit ProcessToken(std::shared_ptr<ProcessTree> tree,
|
||||
std::vector<struct Pid> pids);
|
||||
~ProcessToken();
|
||||
ProcessToken(const ProcessToken &other)
|
||||
: ProcessToken(other.tree_, other.pids_) {}
|
||||
ProcessToken(ProcessToken &&other) noexcept
|
||||
: tree_(std::move(other.tree_)), pids_(std::move(other.pids_)) {}
|
||||
ProcessToken &operator=(const ProcessToken &other) {
|
||||
return *this = ProcessToken(other.tree_, other.pids_);
|
||||
}
|
||||
ProcessToken &operator=(ProcessToken &&other) noexcept {
|
||||
tree_ = std::move(other.tree_);
|
||||
pids_ = std::move(other.pids_);
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<ProcessTree> tree_;
|
||||
std::vector<struct Pid> pids_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
6
Source/santad/ProcessTree/process_tree.proto
Normal file
6
Source/santad/ProcessTree/process_tree.proto
Normal file
@@ -0,0 +1,6 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package santa.pb.v1.process_tree;
|
||||
|
||||
message Annotations {
|
||||
}
|
||||
78
Source/santad/ProcessTree/process_tree_macos.mm
Normal file
78
Source/santad/ProcessTree/process_tree_macos.mm
Normal file
@@ -0,0 +1,78 @@
|
||||
/// Copyright 2023 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.
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
#include <libproc.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/status/status.h"
|
||||
#include "absl/status/statusor.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
absl::StatusOr<Process> LoadPID(pid_t pid) {
|
||||
// TODO
|
||||
return absl::UnimplementedError("LoadPID not implemented");
|
||||
}
|
||||
|
||||
absl::Status ProcessTree::Backfill() {
|
||||
int n_procs = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
|
||||
if (n_procs < 0) {
|
||||
return absl::InternalError("proc_listpids failed");
|
||||
}
|
||||
n_procs /= sizeof(pid_t);
|
||||
|
||||
std::vector<pid_t> pids;
|
||||
pids.resize(n_procs + 16); // add space for a few more processes
|
||||
// in case some spawn in-between.
|
||||
|
||||
n_procs = proc_listpids(PROC_ALL_PIDS, 0, pids.data(), (int)(pids.size() * sizeof(pid_t)));
|
||||
if (n_procs < 0) {
|
||||
return absl::InternalError("proc_listpids failed");
|
||||
}
|
||||
n_procs /= sizeof(pid_t);
|
||||
pids.resize(n_procs);
|
||||
|
||||
absl::flat_hash_map<pid_t, std::vector<Process>> parent_map;
|
||||
for (pid_t pid : pids) {
|
||||
auto proc_status = LoadPID(pid);
|
||||
if (proc_status.ok()) {
|
||||
auto unlinked_proc = proc_status.value();
|
||||
|
||||
// Determine ppid
|
||||
// Alternatively, there's a sysctl interface:
|
||||
// https://chromium.googlesource.com/chromium/chromium/+/master/base/process_util_openbsd.cc#32
|
||||
struct proc_bsdinfo bsdinfo;
|
||||
if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &bsdinfo, sizeof(bsdinfo)) !=
|
||||
PROC_PIDTBSDINFO_SIZE) {
|
||||
continue;
|
||||
};
|
||||
|
||||
parent_map[bsdinfo.pbi_ppid].push_back(unlinked_proc);
|
||||
}
|
||||
}
|
||||
|
||||
auto &roots = parent_map[0];
|
||||
for (const Process &p : roots) {
|
||||
BackfillInsertChildren(parent_map, std::shared_ptr<Process>(), p);
|
||||
}
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
220
Source/santad/ProcessTree/process_tree_test.mm
Normal file
220
Source/santad/ProcessTree/process_tree_test.mm
Normal file
@@ -0,0 +1,220 @@
|
||||
/// Copyright 2023 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>
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Source/santad/ProcessTree/annotations/annotator.h"
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "Source/santad/ProcessTree/process_tree_test_helpers.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
|
||||
namespace ptpb = ::santa::pb::v1::process_tree;
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
static constexpr std::string_view kAnnotatedExecutable = "/usr/bin/login";
|
||||
|
||||
class TestAnnotator : public Annotator {
|
||||
public:
|
||||
TestAnnotator() {}
|
||||
void AnnotateFork(ProcessTree &tree, const Process &parent, const Process &child) override;
|
||||
void AnnotateExec(ProcessTree &tree, const Process &orig_process,
|
||||
const Process &new_process) override;
|
||||
std::optional<::ptpb::Annotations> Proto() const override;
|
||||
};
|
||||
|
||||
void TestAnnotator::AnnotateFork(ProcessTree &tree, const Process &parent, const Process &child) {
|
||||
// "Base case". Propagate existing annotations down to descendants.
|
||||
if (auto annotation = tree.GetAnnotation<TestAnnotator>(parent)) {
|
||||
tree.AnnotateProcess(child, std::move(*annotation));
|
||||
}
|
||||
}
|
||||
|
||||
void TestAnnotator::AnnotateExec(ProcessTree &tree, const Process &orig_process,
|
||||
const Process &new_process) {
|
||||
if (auto annotation = tree.GetAnnotation<TestAnnotator>(orig_process)) {
|
||||
tree.AnnotateProcess(new_process, std::move(*annotation));
|
||||
return;
|
||||
}
|
||||
|
||||
if (new_process.program_->executable == kAnnotatedExecutable) {
|
||||
tree.AnnotateProcess(new_process, std::make_shared<TestAnnotator>());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<::ptpb::Annotations> TestAnnotator::Proto() const {
|
||||
return std::nullopt;
|
||||
}
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
using namespace santa::santad::process_tree;
|
||||
|
||||
@interface ProcessTreeTest : XCTestCase
|
||||
@property std::shared_ptr<ProcessTreeTestPeer> tree;
|
||||
@property std::shared_ptr<const Process> initProc;
|
||||
@end
|
||||
|
||||
@implementation ProcessTreeTest
|
||||
|
||||
- (void)setUp {
|
||||
std::vector<std::unique_ptr<Annotator>> annotators{};
|
||||
self.tree = std::make_shared<ProcessTreeTestPeer>(std::move(annotators));
|
||||
self.initProc = self.tree->InsertInit();
|
||||
}
|
||||
|
||||
- (void)testSimpleOps {
|
||||
uint64_t event_id = 1;
|
||||
// PID 1.1: fork() -> PID 2.2
|
||||
const struct Pid child_pid = {.pid = 2, .pidversion = 2};
|
||||
self.tree->HandleFork(event_id++, *self.initProc, child_pid);
|
||||
|
||||
auto child_opt = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child_opt.has_value());
|
||||
std::shared_ptr<const Process> child = *child_opt;
|
||||
XCTAssertEqual(child->pid_, child_pid);
|
||||
XCTAssertEqual(child->program_, self.initProc->program_);
|
||||
XCTAssertEqual(child->effective_cred_, self.initProc->effective_cred_);
|
||||
XCTAssertEqual(self.tree->GetParent(*child), self.initProc);
|
||||
|
||||
// PID 2.2: exec("/bin/bash") -> PID 2.3
|
||||
const struct Pid child_exec_pid = {.pid = 2, .pidversion = 3};
|
||||
const struct Program child_exec_prog = {.executable = "/bin/bash",
|
||||
.arguments = {"/bin/bash", "-i"}};
|
||||
self.tree->HandleExec(event_id++, *child, child_exec_pid, child_exec_prog,
|
||||
child->effective_cred_);
|
||||
|
||||
child_opt = self.tree->Get(child_exec_pid);
|
||||
XCTAssertTrue(child_opt.has_value());
|
||||
child = *child_opt;
|
||||
XCTAssertEqual(child->pid_, child_exec_pid);
|
||||
XCTAssertEqual(*child->program_, child_exec_prog);
|
||||
XCTAssertEqual(child->effective_cred_, self.initProc->effective_cred_);
|
||||
}
|
||||
|
||||
- (void)testAnnotation {
|
||||
std::vector<std::unique_ptr<Annotator>> annotators{};
|
||||
annotators.emplace_back(std::make_unique<TestAnnotator>());
|
||||
self.tree = std::make_shared<ProcessTreeTestPeer>(std::move(annotators));
|
||||
self.initProc = self.tree->InsertInit();
|
||||
|
||||
uint64_t event_id = 1;
|
||||
const struct Cred cred = {.uid = 0, .gid = 0};
|
||||
|
||||
// PID 1.1: fork() -> PID 2.2
|
||||
const struct Pid login_pid = {.pid = 2, .pidversion = 2};
|
||||
self.tree->HandleFork(event_id++, *self.initProc, login_pid);
|
||||
|
||||
// PID 2.2: exec("/usr/bin/login") -> PID 2.3
|
||||
const struct Pid login_exec_pid = {.pid = 2, .pidversion = 3};
|
||||
const struct Program login_prog = {.executable = std::string(kAnnotatedExecutable),
|
||||
.arguments = {}};
|
||||
auto login = *self.tree->Get(login_pid);
|
||||
self.tree->HandleExec(event_id++, *login, login_exec_pid, login_prog, cred);
|
||||
|
||||
// Ensure we have an annotation on login itself...
|
||||
login = *self.tree->Get(login_exec_pid);
|
||||
auto annotation = self.tree->GetAnnotation<TestAnnotator>(*login);
|
||||
XCTAssertTrue(annotation.has_value());
|
||||
|
||||
// PID 2.3: fork() -> PID 3.3
|
||||
const struct Pid shell_pid = {.pid = 3, .pidversion = 3};
|
||||
self.tree->HandleFork(event_id++, *login, shell_pid);
|
||||
// PID 3.3: exec("/bin/zsh") -> PID 3.4
|
||||
const struct Pid shell_exec_pid = {.pid = 3, .pidversion = 4};
|
||||
const struct Program shell_prog = {.executable = "/bin/zsh", .arguments = {}};
|
||||
auto shell = *self.tree->Get(shell_pid);
|
||||
self.tree->HandleExec(event_id++, *shell, shell_exec_pid, shell_prog, cred);
|
||||
|
||||
// ... and also ensure we have an annotation on the descendant zsh.
|
||||
shell = *self.tree->Get(shell_exec_pid);
|
||||
annotation = self.tree->GetAnnotation<TestAnnotator>(*shell);
|
||||
XCTAssertTrue(annotation.has_value());
|
||||
}
|
||||
|
||||
- (void)testCleanup {
|
||||
uint64_t event_id = 1;
|
||||
const struct Pid child_pid = {.pid = 2, .pidversion = 2};
|
||||
{
|
||||
self.tree->HandleFork(event_id++, *self.initProc, child_pid);
|
||||
auto child = *self.tree->Get(child_pid);
|
||||
self.tree->HandleExit(event_id++, *child);
|
||||
}
|
||||
|
||||
// We should still be able to get a handle to child...
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child.has_value());
|
||||
}
|
||||
|
||||
// ... until we step far enough into the future (32 events).
|
||||
struct Pid churn_pid = {.pid = 3, .pidversion = 3};
|
||||
for (int i = 0; i < 32; i++) {
|
||||
self.tree->HandleFork(event_id++, *self.initProc, churn_pid);
|
||||
churn_pid.pid++;
|
||||
}
|
||||
|
||||
// Now when we try processing the next event, it should have fallen out of the tree.
|
||||
self.tree->HandleFork(event_id++, *self.initProc, churn_pid);
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertFalse(child.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testRefcountCleanup {
|
||||
uint64_t event_id = 1;
|
||||
const struct Pid child_pid = {.pid = 2, .pidversion = 2};
|
||||
{
|
||||
self.tree->HandleFork(event_id++, *self.initProc, child_pid);
|
||||
auto child = *self.tree->Get(child_pid);
|
||||
self.tree->HandleExit(event_id++, *child);
|
||||
}
|
||||
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child.has_value());
|
||||
std::vector<struct Pid> pids = {(*child)->pid_};
|
||||
self.tree->RetainProcess(pids);
|
||||
}
|
||||
|
||||
// Even if we step far into the future, we should still be able to lookup
|
||||
// the child.
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
struct Pid churn_pid = {.pid = 100 + i, .pidversion = (uint64_t)(100 + i)};
|
||||
self.tree->HandleFork(event_id++, *self.initProc, churn_pid);
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child.has_value());
|
||||
}
|
||||
|
||||
// But when released...
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertTrue(child.has_value());
|
||||
std::vector<struct Pid> pids = {(*child)->pid_};
|
||||
self.tree->ReleaseProcess(pids);
|
||||
}
|
||||
|
||||
// ... it should immediately be removed.
|
||||
{
|
||||
auto child = self.tree->Get(child_pid);
|
||||
XCTAssertFalse(child.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
32
Source/santad/ProcessTree/process_tree_test_helpers.h
Normal file
32
Source/santad/ProcessTree/process_tree_test_helpers.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/// Copyright 2023 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.
|
||||
#ifndef SANTA__SANTAD_PROCESSTREE_TREE_TEST_HELPERS_H
|
||||
#define SANTA__SANTAD_PROCESSTREE_TREE_TEST_HELPERS_H
|
||||
#include <memory>
|
||||
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
class ProcessTreeTestPeer : public ProcessTree {
|
||||
public:
|
||||
explicit ProcessTreeTestPeer(
|
||||
std::vector<std::unique_ptr<Annotator>> &&annotators)
|
||||
: ProcessTree(std::move(annotators)) {}
|
||||
std::shared_ptr<const Process> InsertInit();
|
||||
};
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
|
||||
#endif
|
||||
42
Source/santad/ProcessTree/process_tree_test_helpers.mm
Normal file
42
Source/santad/ProcessTree/process_tree_test_helpers.mm
Normal file
@@ -0,0 +1,42 @@
|
||||
/// Copyright 2023 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>
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include "Source/santad/ProcessTree/process.h"
|
||||
#include "Source/santad/ProcessTree/process_tree.h"
|
||||
|
||||
namespace santa::santad::process_tree {
|
||||
|
||||
class ProcessTreeTestPeer : public ProcessTree {
|
||||
public:
|
||||
std::shared_ptr<const Process> InsertInit();
|
||||
};
|
||||
|
||||
std::shared_ptr<const Process> ProcessTreeTestPeer::InsertInit() {
|
||||
absl::MutexLock lock(&mtx_);
|
||||
struct Pid initpid = {
|
||||
.pid = 1,
|
||||
.pidversion = 1,
|
||||
};
|
||||
auto proc = std::make_shared<Process>(
|
||||
initpid, (Cred){.uid = 0, .gid = 0},
|
||||
std::make_shared<Program>((Program){.executable = "/init", .arguments = {"/init"}}), nullptr);
|
||||
map_.emplace(initpid, proc);
|
||||
return proc;
|
||||
}
|
||||
|
||||
} // namespace santa::santad::process_tree
|
||||
@@ -132,8 +132,9 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
NSError *error = nil;
|
||||
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithEndpointSecurityFile:targetFile error:&error];
|
||||
if (error) {
|
||||
LOGD(@"Unable to create SNTFileInfo while attempting to create transitive rule. Path: %@",
|
||||
@(targetFile->path.data));
|
||||
LOGD(@"Unable to create SNTFileInfo while attempting to create transitive rule. Event: %d | "
|
||||
@"Path: %@ | Error: %@",
|
||||
(int)esMsg->event_type, @(targetFile->path.data), error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -157,7 +158,7 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/";
|
||||
|
||||
// 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]);
|
||||
|
||||
@@ -105,17 +105,18 @@ double watchdogRAMPeak = 0;
|
||||
}
|
||||
|
||||
- (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 @@ double watchdogRAMPeak = 0;
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -258,10 +259,19 @@ double watchdogRAMPeak = 0;
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)blockUSBMount:(void (^)(BOOL))reply {
|
||||
reply([[SNTConfigurator configurator] blockUSBMount]);
|
||||
}
|
||||
|
||||
- (void)setBlockUSBMount:(BOOL)enabled reply:(void (^)(void))reply {
|
||||
[[SNTConfigurator configurator] setBlockUSBMount:enabled];
|
||||
reply();
|
||||
}
|
||||
|
||||
- (void)remountUSBMode:(void (^)(NSArray<NSString *> *))reply {
|
||||
reply([[SNTConfigurator configurator] remountUSBMode]);
|
||||
}
|
||||
|
||||
- (void)setRemountUSBMode:(NSArray *)remountUSBMode reply:(void (^)(void))reply {
|
||||
[[SNTConfigurator configurator] setRemountUSBMode:remountUSBMode];
|
||||
reply();
|
||||
|
||||
@@ -57,7 +57,9 @@ const static NSString *kBlockLongPath = @"BlockLongPath";
|
||||
eventTable:(SNTEventTable *)eventTable
|
||||
notifierQueue:(SNTNotificationQueue *)notifierQueue
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue
|
||||
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter;
|
||||
ttyWriter:(std::shared_ptr<santa::santad::TTYWriter>)ttyWriter
|
||||
entitlementsPrefixFilter:(NSArray<NSString *> *)prefixFilter
|
||||
entitlementsTeamIDFilter:(NSArray<NSString *> *)teamIDFilter;
|
||||
|
||||
///
|
||||
/// Handles the logic of deciding whether to allow the binary to run or not, sends the response to
|
||||
@@ -82,4 +84,6 @@ const static NSString *kBlockLongPath = @"BlockLongPath";
|
||||
- (bool)synchronousShouldProcessExecEvent:
|
||||
(const santa::santad::event_providers::endpoint_security::Message &)esMsg;
|
||||
|
||||
- (void)updateEntitlementsPrefixFilter:(NSArray<NSString *> *)filter;
|
||||
- (void)updateEntitlementsTeamIDFilter:(NSArray<NSString *> *)filter;
|
||||
@end
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/SNTExecutionController.h"
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#include <bsm/libbsm.h>
|
||||
@@ -24,12 +25,16 @@
|
||||
#include <utmpx.h>
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include "Source/common/BranchPrediction.h"
|
||||
#include "Source/common/PrefixTree.h"
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeepCopy.h"
|
||||
#import "Source/common/SNTDropRootPrivs.h"
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
@@ -38,18 +43,39 @@
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/common/SantaVnode.h"
|
||||
#include "Source/common/String.h"
|
||||
#include "Source/common/Unit.h"
|
||||
#import "Source/santad/DataLayer/SNTEventTable.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
#import "Source/santad/SNTNotificationQueue.h"
|
||||
#import "Source/santad/SNTPolicyProcessor.h"
|
||||
#import "Source/santad/SNTSyncdQueue.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
|
||||
using santa::common::PrefixTree;
|
||||
using santa::common::Unit;
|
||||
using santa::santad::TTYWriter;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
static const size_t kMaxAllowedPathLength = MAXPATHLEN - 1; // -1 to account for null terminator
|
||||
|
||||
void UpdateTeamIDFilterLocked(std::set<std::string> &filterSet, NSArray<NSString *> *filter) {
|
||||
filterSet.clear();
|
||||
|
||||
for (NSString *prefix in filter) {
|
||||
filterSet.insert(santa::common::NSStringToUTF8String(prefix));
|
||||
}
|
||||
}
|
||||
|
||||
void UpdatePrefixFilterLocked(std::unique_ptr<PrefixTree<Unit>> &tree,
|
||||
NSArray<NSString *> *filter) {
|
||||
tree->Reset();
|
||||
|
||||
for (NSString *item in filter) {
|
||||
tree->InsertPrefix(item.UTF8String, Unit{});
|
||||
}
|
||||
}
|
||||
|
||||
@interface SNTExecutionController ()
|
||||
@property SNTEventTable *eventTable;
|
||||
@property SNTNotificationQueue *notifierQueue;
|
||||
@@ -63,6 +89,9 @@ static const size_t kMaxAllowedPathLength = MAXPATHLEN - 1; // -1 to account fo
|
||||
|
||||
@implementation SNTExecutionController {
|
||||
std::shared_ptr<TTYWriter> _ttyWriter;
|
||||
absl::Mutex _entitlementFilterMutex;
|
||||
std::set<std::string> _entitlementsTeamIDFilter;
|
||||
std::unique_ptr<PrefixTree<Unit>> _entitlementsPrefixFilter;
|
||||
}
|
||||
|
||||
static NSString *const kPrinterProxyPreMonterey =
|
||||
@@ -79,7 +108,9 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
eventTable:(SNTEventTable *)eventTable
|
||||
notifierQueue:(SNTNotificationQueue *)notifierQueue
|
||||
syncdQueue:(SNTSyncdQueue *)syncdQueue
|
||||
ttyWriter:(std::shared_ptr<TTYWriter>)ttyWriter {
|
||||
ttyWriter:(std::shared_ptr<TTYWriter>)ttyWriter
|
||||
entitlementsPrefixFilter:(NSArray<NSString *> *)entitlementsPrefixFilter
|
||||
entitlementsTeamIDFilter:(NSArray<NSString *> *)entitlementsTeamIDFilter {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_ruleTable = ruleTable;
|
||||
@@ -100,10 +131,25 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
_events = [metricSet counterWithName:@"/santa/events"
|
||||
fieldNames:@[ @"action_response" ]
|
||||
helpText:@"Events processed by Santa per response"];
|
||||
|
||||
self->_entitlementsPrefixFilter = std::make_unique<PrefixTree<Unit>>();
|
||||
|
||||
UpdatePrefixFilterLocked(self->_entitlementsPrefixFilter, entitlementsPrefixFilter);
|
||||
UpdateTeamIDFilterLocked(self->_entitlementsTeamIDFilter, entitlementsTeamIDFilter);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)updateEntitlementsPrefixFilter:(NSArray<NSString *> *)filter {
|
||||
absl::MutexLock lock(&self->_entitlementFilterMutex);
|
||||
UpdatePrefixFilterLocked(self->_entitlementsPrefixFilter, filter);
|
||||
}
|
||||
|
||||
- (void)updateEntitlementsTeamIDFilter:(NSArray<NSString *> *)filter {
|
||||
absl::MutexLock lock(&self->_entitlementFilterMutex);
|
||||
UpdateTeamIDFilterLocked(self->_entitlementsTeamIDFilter, filter);
|
||||
}
|
||||
|
||||
- (void)incrementEventCounters:(SNTEventState)eventType {
|
||||
const NSString *eventTypeStr;
|
||||
|
||||
@@ -184,7 +230,7 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithEndpointSecurityFile:targetProc->executable
|
||||
error:&fileInfoError];
|
||||
if (unlikely(!binInfo)) {
|
||||
if (config.failClosed && config.clientMode == SNTClientModeLockdown) {
|
||||
if (config.failClosed) {
|
||||
LOGE(@"Failed to read file %@: %@ and denying action", @(targetProc->executable->path.data),
|
||||
fileInfoError.localizedDescription);
|
||||
postAction(SNTActionRespondDeny);
|
||||
@@ -208,8 +254,41 @@ static NSString *const kPrinterProxyPostMonterey =
|
||||
// TODO(markowsky): Maybe add a metric here for how many large executables we're seeing.
|
||||
// if (binInfo.fileSize > SomeUpperLimit) ...
|
||||
|
||||
SNTCachedDecision *cd = [self.policyProcessor decisionForFileInfo:binInfo
|
||||
targetProcess:targetProc];
|
||||
SNTCachedDecision *cd = [self.policyProcessor
|
||||
decisionForFileInfo:binInfo
|
||||
targetProcess:targetProc
|
||||
entitlementsFilterCallback:^NSDictionary *(const char *teamID, NSDictionary *entitlements) {
|
||||
if (!entitlements) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
absl::ReaderMutexLock lock(&self->_entitlementFilterMutex);
|
||||
|
||||
if (teamID && self->_entitlementsTeamIDFilter.count(std::string(teamID)) > 0) {
|
||||
// Dropping entitlement logging for configured TeamID
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (self->_entitlementsPrefixFilter->NodeCount() == 0) {
|
||||
// Copying full entitlements for TeamID
|
||||
return [entitlements sntDeepCopy];
|
||||
} else {
|
||||
// Filtering entitlements for TeamID
|
||||
NSMutableDictionary *filtered = [NSMutableDictionary dictionary];
|
||||
|
||||
[entitlements enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
|
||||
if (!self->_entitlementsPrefixFilter->HasPrefix(key.UTF8String)) {
|
||||
if ([obj isKindOfClass:[NSArray class]] || [obj isKindOfClass:[NSDictionary class]]) {
|
||||
[filtered setObject:[obj sntDeepCopy] forKey:key];
|
||||
} else {
|
||||
[filtered setObject:[obj copy] forKey:key];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
return filtered.count > 0 ? filtered : nil;
|
||||
}
|
||||
}];
|
||||
|
||||
cd.vnodeId = SantaVnode::VnodeForFile(targetProc->executable);
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
@@ -96,7 +95,9 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
eventTable:self.mockEventDatabase
|
||||
notifierQueue:nil
|
||||
syncdQueue:nil
|
||||
ttyWriter:nullptr];
|
||||
ttyWriter:nullptr
|
||||
entitlementsPrefixFilter:nil
|
||||
entitlementsTeamIDFilter:nil];
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
@@ -556,7 +557,7 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
[self checkMetricCounters:kAllowUnknown expected:@1];
|
||||
}
|
||||
|
||||
- (void)testUnreadableFailOpenLockdown {
|
||||
- (void)testUnreadableFailOpen {
|
||||
// Undo the default mocks
|
||||
[self.mockFileInfo stopMocking];
|
||||
self.mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
@@ -564,15 +565,13 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
OCMStub([self.mockFileInfo alloc]).andReturn(nil);
|
||||
OCMStub([self.mockFileInfo initWithPath:OCMOCK_ANY error:[OCMArg setTo:nil]]).andReturn(nil);
|
||||
|
||||
// Lockdown mode, no fail-closed
|
||||
OCMStub([self.mockConfigurator failClosed]).andReturn(NO);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowNoFileInfo expected:@1];
|
||||
}
|
||||
|
||||
- (void)testUnreadableFailClosedLockdown {
|
||||
- (void)testUnreadableFailClosed {
|
||||
// Undo the default mocks
|
||||
[self.mockFileInfo stopMocking];
|
||||
self.mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
@@ -580,30 +579,12 @@ VerifyPostActionBlock verifyPostAction = ^PostActionBlock(SNTAction wantAction)
|
||||
OCMStub([self.mockFileInfo alloc]).andReturn(nil);
|
||||
OCMStub([self.mockFileInfo initWithPath:OCMOCK_ANY error:[OCMArg setTo:nil]]).andReturn(nil);
|
||||
|
||||
// Lockdown mode, fail-closed
|
||||
OCMStub([self.mockConfigurator failClosed]).andReturn(YES);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondDeny];
|
||||
[self checkMetricCounters:kDenyNoFileInfo expected:@1];
|
||||
}
|
||||
|
||||
- (void)testUnreadableFailClosedMonitor {
|
||||
// Undo the default mocks
|
||||
[self.mockFileInfo stopMocking];
|
||||
self.mockFileInfo = OCMClassMock([SNTFileInfo class]);
|
||||
|
||||
OCMStub([self.mockFileInfo alloc]).andReturn(nil);
|
||||
OCMStub([self.mockFileInfo initWithPath:OCMOCK_ANY error:[OCMArg setTo:nil]]).andReturn(nil);
|
||||
|
||||
// Monitor mode, fail-closed
|
||||
OCMStub([self.mockConfigurator failClosed]).andReturn(YES);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeMonitor);
|
||||
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowNoFileInfo expected:@1];
|
||||
}
|
||||
|
||||
- (void)testMissingShasum {
|
||||
[self validateExecEvent:SNTActionRespondAllow];
|
||||
[self checkMetricCounters:kAllowScope expected:@1];
|
||||
|
||||
@@ -36,22 +36,19 @@
|
||||
- (nullable instancetype)initWithRuleTable:(nonnull SNTRuleTable *)ruleTable;
|
||||
|
||||
///
|
||||
/// @param fileInfo A SNTFileInfo object.
|
||||
/// @param fileSHA256 The pre-calculated SHA256 hash for the file, can be nil. If nil the hash will
|
||||
/// be calculated by this method from the filePath.
|
||||
/// @param certificateSHA256 The pre-calculated SHA256 hash of the leaf certificate. If nil, the
|
||||
/// signature will be validated on the binary represented by fileInfo.
|
||||
///
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID;
|
||||
|
||||
/// Convenience initializer. Will obtain the teamID and construct the signingID
|
||||
/// identifier if able.
|
||||
///
|
||||
/// IMPORTANT: The lifetimes of arguments to `entitlementsFilterCallback` are
|
||||
/// only guaranteed for the duration of the call to the block. Do not perform
|
||||
/// any async processing without extending their lifetimes.
|
||||
///
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
targetProcess:(nonnull const es_process_t *)targetProc;
|
||||
targetProcess:(nonnull const es_process_t *)targetProc
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nonnull)(
|
||||
const char *_Nullable teamID,
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback;
|
||||
|
||||
///
|
||||
/// A wrapper for decisionForFileInfo:fileSHA256:certificateSHA256:. This method is slower as it
|
||||
|
||||
@@ -13,15 +13,18 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/SNTPolicyProcessor.h"
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#import <Security/SecCode.h>
|
||||
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#import <Security/Security.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeepCopy.h"
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTRule.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
|
||||
@@ -45,7 +48,11 @@
|
||||
fileSHA256:(nullable NSString *)fileSHA256
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID {
|
||||
signingID:(nullable NSString *)signingID
|
||||
isProdSignedCallback:(BOOL (^_Nonnull)())isProdSignedCallback
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nullable)(
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] init];
|
||||
cd.sha256 = fileSHA256 ?: fileInfo.SHA256;
|
||||
cd.teamID = teamID;
|
||||
@@ -92,10 +99,34 @@
|
||||
cd.signingID = nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary *entitlements =
|
||||
csInfo.signingInformation[(__bridge NSString *)kSecCodeInfoEntitlementsDict];
|
||||
|
||||
if (entitlementsFilterCallback) {
|
||||
cd.entitlements = entitlementsFilterCallback(entitlements);
|
||||
cd.entitlementsFiltered = (cd.entitlements.count == entitlements.count);
|
||||
} else {
|
||||
cd.entitlements = [entitlements sntDeepCopy];
|
||||
cd.entitlementsFiltered = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
cd.quarantineURL = fileInfo.quarantineDataURL;
|
||||
|
||||
// Do not evaluate TeamID/SigningID rules for dev-signed code based on the
|
||||
// assumption that orgs are generally more relaxed about dev signed cert
|
||||
// protections and users can more easily produce dev-signed code that
|
||||
// would otherwise be inadvertently allowed.
|
||||
// Note: Only perform the check if the SigningID is still set, otherwise
|
||||
// it is unsigned or had issues above that already cleared the values.
|
||||
if (cd.signingID && !isProdSignedCallback()) {
|
||||
LOGD(@"Ignoring TeamID and SigningID rules for code not signed with production cert: %@",
|
||||
cd.signingID);
|
||||
cd.teamID = nil;
|
||||
cd.signingID = nil;
|
||||
}
|
||||
|
||||
SNTRule *rule = [self.ruleTable ruleForBinarySHA256:cd.sha256
|
||||
signingID:cd.signingID
|
||||
certificateSHA256:cd.certSHA256
|
||||
@@ -221,17 +252,25 @@
|
||||
}
|
||||
|
||||
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
|
||||
targetProcess:(nonnull const es_process_t *)targetProc {
|
||||
targetProcess:(nonnull const es_process_t *)targetProc
|
||||
entitlementsFilterCallback:
|
||||
(NSDictionary *_Nullable (^_Nonnull)(
|
||||
const char *_Nullable teamID,
|
||||
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
|
||||
NSString *signingID;
|
||||
NSString *teamID;
|
||||
|
||||
const char *entitlementsFilterTeamID = NULL;
|
||||
|
||||
if (targetProc->signing_id.length > 0) {
|
||||
if (targetProc->team_id.length > 0) {
|
||||
entitlementsFilterTeamID = targetProc->team_id.data;
|
||||
teamID = [NSString stringWithUTF8String:targetProc->team_id.data];
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"%@:%@", teamID,
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
} else if (targetProc->is_platform_binary) {
|
||||
entitlementsFilterTeamID = "platform";
|
||||
signingID =
|
||||
[NSString stringWithFormat:@"platform:%@",
|
||||
[NSString stringWithUTF8String:targetProc->signing_id.data]];
|
||||
@@ -239,10 +278,16 @@
|
||||
}
|
||||
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
fileSHA256:nil
|
||||
certificateSHA256:nil
|
||||
teamID:teamID
|
||||
signingID:signingID];
|
||||
fileSHA256:nil
|
||||
certificateSHA256:nil
|
||||
teamID:teamID
|
||||
signingID:signingID
|
||||
isProdSignedCallback:^BOOL {
|
||||
return ((targetProc->codesigning_flags & CS_DEV_CODE) == 0);
|
||||
}
|
||||
entitlementsFilterCallback:^NSDictionary *(NSDictionary *entitlements) {
|
||||
return entitlementsFilterCallback(entitlementsFilterTeamID, entitlements);
|
||||
}];
|
||||
}
|
||||
|
||||
// Used by `$ santactl fileinfo`.
|
||||
@@ -251,15 +296,37 @@
|
||||
certificateSHA256:(nullable NSString *)certificateSHA256
|
||||
teamID:(nullable NSString *)teamID
|
||||
signingID:(nullable NSString *)signingID {
|
||||
SNTFileInfo *fileInfo;
|
||||
MOLCodesignChecker *csInfo;
|
||||
NSError *error;
|
||||
fileInfo = [[SNTFileInfo alloc] initWithPath:filePath error:&error];
|
||||
if (!fileInfo) LOGW(@"Failed to read file %@: %@", filePath, error.localizedDescription);
|
||||
|
||||
SNTFileInfo *fileInfo = [[SNTFileInfo alloc] initWithPath:filePath error:&error];
|
||||
if (!fileInfo) {
|
||||
LOGW(@"Failed to read file %@: %@", filePath, error.localizedDescription);
|
||||
} else {
|
||||
csInfo = [fileInfo codesignCheckerWithError:&error];
|
||||
if (error) {
|
||||
LOGW(@"Failed to get codesign ingo for file %@: %@", filePath, error.localizedDescription);
|
||||
}
|
||||
}
|
||||
|
||||
return [self decisionForFileInfo:fileInfo
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
signingID:signingID];
|
||||
signingID:signingID
|
||||
isProdSignedCallback:^BOOL {
|
||||
if (csInfo) {
|
||||
// Development OID values defined by Apple and used by the Security Framework
|
||||
// https://images.apple.com/certificateauthority/pdf/Apple_WWDR_CPS_v1.31.pdf
|
||||
NSArray *keys = @[ @"1.2.840.113635.100.6.1.2", @"1.2.840.113635.100.6.1.12" ];
|
||||
NSDictionary *vals = CFBridgingRelease(SecCertificateCopyValues(
|
||||
csInfo.leafCertificate.certRef, (__bridge CFArrayRef)keys, NULL));
|
||||
return vals.count == 0;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
entitlementsFilterCallback:nil];
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
@@ -358,6 +358,52 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
// Forcefully exit. The daemon will be restarted immediately.
|
||||
exit(EXIT_SUCCESS);
|
||||
}],
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
selector:@selector(entitlementsTeamIDFilter)
|
||||
type:[NSArray class]
|
||||
callback:^(NSArray<NSString *> *oldValue, NSArray<NSString *> *newValue) {
|
||||
if ((!oldValue && !newValue) || [oldValue isEqualToArray:newValue]) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGI(@"EntitlementsTeamIDFilter changed. '%@' --> '%@'. Flushing caches.", oldValue,
|
||||
newValue);
|
||||
|
||||
// Get the value from the configurator since that method ensures proper structure
|
||||
[exec_controller
|
||||
updateEntitlementsTeamIDFilter:[configurator entitlementsTeamIDFilter]];
|
||||
|
||||
// Clear the AuthResultCache, then clear the ES cache to ensure
|
||||
// future execs get SNTCachedDecision entitlement values filtered
|
||||
// with the new settings.
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kEntitlementsTeamIDFilterChanged);
|
||||
[authorizer_client clearCache];
|
||||
}],
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
selector:@selector(entitlementsPrefixFilter)
|
||||
type:[NSArray class]
|
||||
callback:^(NSArray<NSString *> *oldValue, NSArray<NSString *> *newValue) {
|
||||
if ((!oldValue && !newValue) || [oldValue isEqualToArray:newValue]) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGI(@"EntitlementsPrefixFilter changed. '%@' --> '%@'. Flushing caches.", oldValue,
|
||||
newValue);
|
||||
|
||||
// Get the value from the configurator since that method ensures proper structure
|
||||
[exec_controller
|
||||
updateEntitlementsPrefixFilter:[configurator entitlementsPrefixFilter]];
|
||||
|
||||
// Clear the AuthResultCache, then clear the ES cache to ensure
|
||||
// future execs get SNTCachedDecision entitlement values filtered
|
||||
// with the new settings.
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches,
|
||||
FlushCacheReason::kEntitlementsPrefixFilterChanged);
|
||||
[authorizer_client clearCache];
|
||||
}],
|
||||
]];
|
||||
|
||||
if (@available(macOS 13.0, *)) {
|
||||
|
||||
@@ -94,7 +94,9 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
|
||||
eventTable:event_table
|
||||
notifierQueue:notifier_queue
|
||||
syncdQueue:syncd_queue
|
||||
ttyWriter:tty_writer];
|
||||
ttyWriter:tty_writer
|
||||
entitlementsPrefixFilter:[configurator entitlementsPrefixFilter]
|
||||
entitlementsTeamIDFilter:[configurator entitlementsTeamIDFilter]];
|
||||
if (!exec_controller) {
|
||||
LOGE(@"Failed to initialize exec controller.");
|
||||
exit(EXIT_FAILURE);
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/Metrics.h"
|
||||
#import "Source/santad/SNTDatabaseController.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
@@ -45,6 +46,12 @@ static const char *kNoRuleMatchSigningID = "com.google.no_rule_match_signing_id"
|
||||
static const char *kBlockedTeamID = "EQHXZ8M8AV";
|
||||
static const char *kAllowedTeamID = "TJNVEKW352";
|
||||
|
||||
@interface SNTEndpointSecurityClient (Testing)
|
||||
@property(nonatomic) double defaultBudget;
|
||||
@property(nonatomic) int64_t minAllowedHeadroom;
|
||||
@property(nonatomic) int64_t maxAllowedHeadroom;
|
||||
@end
|
||||
|
||||
@interface SantadTest : XCTestCase
|
||||
@property id mockSNTDatabaseController;
|
||||
@end
|
||||
@@ -118,12 +125,14 @@ static const char *kAllowedTeamID = "TJNVEKW352";
|
||||
es_file_t file = MakeESFile([binaryPath UTF8String], fileStat);
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
proc.is_platform_binary = false;
|
||||
// Set a 6.5 second deadline for the message. The base SNTEndpointSecurityClient
|
||||
// class leaves a 5 second buffer to auto-respond to messages. A 6 second
|
||||
// deadline means there is a 1.5 second leeway given for the processing block
|
||||
|
||||
// Set a 6.5 second deadline for the message and clamp deadline headroom to 5
|
||||
// seconds. This means there is a 1.5 second leeway given for the processing block
|
||||
// to finish its tasks and release the `Message`. This will add about 1 second
|
||||
// to the run time of each test case since each one must wait for the
|
||||
// deadline block to run and release the message.
|
||||
authClient.minAllowedHeadroom = 5 * NSEC_PER_SEC;
|
||||
authClient.maxAllowedHeadroom = 5 * NSEC_PER_SEC;
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth, 6500);
|
||||
esMsg.event.exec.target = &proc;
|
||||
|
||||
|
||||
43
Source/santad/testdata/protobuf/v1/exec.json
vendored
43
Source/santad/testdata/protobuf/v1/exec.json
vendored
@@ -120,5 +120,46 @@
|
||||
}
|
||||
},
|
||||
"explain": "extra!",
|
||||
"quarantine_url": "google.com"
|
||||
"quarantine_url": "google.com",
|
||||
"entitlement_info": {
|
||||
"entitlements_filtered": false,
|
||||
"entitlements": [
|
||||
{
|
||||
"key": "key_with_arr_val_multitype",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_data_val",
|
||||
"value": "\"SGVsbG8gV29ybGQ=\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_str_val",
|
||||
"value": "\"bar\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val_nested",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val",
|
||||
"value": "{\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_date_val",
|
||||
"value": "\"2023-11-07T17:00:02.000Z\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val_nested",
|
||||
"value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_num_val",
|
||||
"value": "1234"
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val",
|
||||
"value": "[\"v1\",\"v2\",\"v3\"]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
43
Source/santad/testdata/protobuf/v2/exec.json
vendored
43
Source/santad/testdata/protobuf/v2/exec.json
vendored
@@ -148,5 +148,46 @@
|
||||
}
|
||||
},
|
||||
"explain": "extra!",
|
||||
"quarantine_url": "google.com"
|
||||
"quarantine_url": "google.com",
|
||||
"entitlement_info": {
|
||||
"entitlements_filtered": false,
|
||||
"entitlements": [
|
||||
{
|
||||
"key": "key_with_arr_val_multitype",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_data_val",
|
||||
"value": "\"SGVsbG8gV29ybGQ=\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_str_val",
|
||||
"value": "\"bar\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val_nested",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val",
|
||||
"value": "{\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_date_val",
|
||||
"value": "\"2023-11-07T17:00:02.000Z\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val_nested",
|
||||
"value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_num_val",
|
||||
"value": "1234"
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val",
|
||||
"value": "[\"v1\",\"v2\",\"v3\"]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
43
Source/santad/testdata/protobuf/v4/exec.json
vendored
43
Source/santad/testdata/protobuf/v4/exec.json
vendored
@@ -197,5 +197,46 @@
|
||||
}
|
||||
},
|
||||
"explain": "extra!",
|
||||
"quarantine_url": "google.com"
|
||||
"quarantine_url": "google.com",
|
||||
"entitlement_info": {
|
||||
"entitlements_filtered": false,
|
||||
"entitlements": [
|
||||
{
|
||||
"key": "key_with_arr_val_multitype",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_data_val",
|
||||
"value": "\"SGVsbG8gV29ybGQ=\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_str_val",
|
||||
"value": "\"bar\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val_nested",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val",
|
||||
"value": "{\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_date_val",
|
||||
"value": "\"2023-11-07T17:00:02.000Z\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val_nested",
|
||||
"value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_num_val",
|
||||
"value": "1234"
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val",
|
||||
"value": "[\"v1\",\"v2\",\"v3\"]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
43
Source/santad/testdata/protobuf/v5/exec.json
vendored
43
Source/santad/testdata/protobuf/v5/exec.json
vendored
@@ -197,5 +197,46 @@
|
||||
}
|
||||
},
|
||||
"explain": "extra!",
|
||||
"quarantine_url": "google.com"
|
||||
"quarantine_url": "google.com",
|
||||
"entitlement_info": {
|
||||
"entitlements_filtered": false,
|
||||
"entitlements": [
|
||||
{
|
||||
"key": "key_with_arr_val_multitype",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_data_val",
|
||||
"value": "\"SGVsbG8gV29ybGQ=\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_str_val",
|
||||
"value": "\"bar\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val_nested",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val",
|
||||
"value": "{\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_date_val",
|
||||
"value": "\"2023-11-07T17:00:02.000Z\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val_nested",
|
||||
"value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_num_val",
|
||||
"value": "1234"
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val",
|
||||
"value": "[\"v1\",\"v2\",\"v3\"]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
43
Source/santad/testdata/protobuf/v6/exec.json
vendored
43
Source/santad/testdata/protobuf/v6/exec.json
vendored
@@ -197,5 +197,46 @@
|
||||
}
|
||||
},
|
||||
"explain": "extra!",
|
||||
"quarantine_url": "google.com"
|
||||
"quarantine_url": "google.com",
|
||||
"entitlement_info": {
|
||||
"entitlements_filtered": false,
|
||||
"entitlements": [
|
||||
{
|
||||
"key": "key_with_arr_val_multitype",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",123,\"2023-11-07T17:00:02.000Z\"]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_data_val",
|
||||
"value": "\"SGVsbG8gV29ybGQ=\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_str_val",
|
||||
"value": "\"bar\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val_nested",
|
||||
"value": "[\"v1\",\"v2\",\"v3\",[\"nv1\",\"nv2\"]]"
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val",
|
||||
"value": "{\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_date_val",
|
||||
"value": "\"2023-11-07T17:00:02.000Z\""
|
||||
},
|
||||
{
|
||||
"key": "key_with_dict_val_nested",
|
||||
"value": "{\"k3\":{\"nk1\":\"nv1\",\"nk2\":\"2023-11-07T17:00:02.000Z\"},\"k2\":\"v2\",\"k1\":\"v1\"}"
|
||||
},
|
||||
{
|
||||
"key": "key_with_num_val",
|
||||
"value": "1234"
|
||||
},
|
||||
{
|
||||
"key": "key_with_arr_val",
|
||||
"value": "[\"v1\",\"v2\",\"v3\"]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTFileInfo.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
@@ -55,7 +56,8 @@
|
||||
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;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#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.
|
||||
|
||||
@@ -87,7 +87,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
|
||||
_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 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
|
||||
[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);
|
||||
|
||||
@@ -42,11 +42,11 @@
|
||||
}];
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -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 <empty> | 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 <empty> | 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 <empty> | 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 @@ static id EnsureType(id val, Class c) {
|
||||
}
|
||||
}];
|
||||
|
||||
__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 @@ static id EnsureType(id val, Class c) {
|
||||
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;
|
||||
|
||||
@@ -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 @@
|
||||
// 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;
|
||||
|
||||
@@ -81,22 +81,22 @@
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santasyncservice/SNTSyncStage.h"
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTSyncConstants.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;
|
||||
|
||||
@@ -155,7 +155,8 @@
|
||||
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 @@
|
||||
[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 @@
|
||||
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 @@
|
||||
// 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 @@
|
||||
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 @@
|
||||
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 setSyncCleanRequired:NO reply:OCMOCK_ANY]);
|
||||
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 setSyncTypeRequired:SNTSyncTypeNormal reply:OCMOCK_ANY]);
|
||||
|
||||
self.syncState.allowlistRegex = @"^horse$";
|
||||
self.syncState.blocklistRegex = @"^donkey$";
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -x
|
||||
|
||||
# TODO(nickmg): These `santactl status`s should be run with sudo to mirror the others,
|
||||
# however currently (2022-10-27) non-root status is what correctly reads from provisioning profile configuration.
|
||||
|
||||
bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
|
||||
if [[ "$(santactl status --json | jq .daemon.block_usb)" != "false" ]]; then
|
||||
echo "USB blocking enabled with minimal config" >&2
|
||||
|
||||
@@ -20,70 +20,80 @@ also known as mobileconfig files, which are in an Apple-specific XML format.
|
||||
|
||||
## Local Configuration Profile
|
||||
|
||||
| Key | Value Type | Description |
|
||||
| ----------------------------- | ---------- | ---------------------------------------- |
|
||||
| ClientMode\* | Integer | 1 = MONITOR, 2 = LOCKDOWN, defaults to MONITOR |
|
||||
| FailClosed | Bool | If true and the ClientMode is LOCKDOWN: execution will be denied when there is an error reading or processing an executable file. |
|
||||
| FileChangesRegex\* | String | The regex of paths to log file changes. Regexes are specified in ICU format. |
|
||||
| AllowedPathRegex\* | String | A regex to allow if the binary, certificate, or Team ID scopes did not allow/block execution. Regexes are specified in ICU format. |
|
||||
| BlockedPathRegex\* | String | A regex to block if the binary, certificate, or Team ID scopes did not allow/block an execution. Regexes are specified in ICU format. |
|
||||
| EnableBadSignatureProtection | Bool | Enable bad signature protection, defaults to NO. If this flag is set to YES, binaries with a bad signing chain will be blocked even in MONITOR mode, **unless** the binary is allowed by an explicit rule. |
|
||||
| EnablePageZeroProtection | Bool | Enable `__PAGEZERO` protection, defaults to YES. If this flag is set to YES, 32-bit binaries that are missing the `__PAGEZERO` segment will be blocked even in MONITOR mode, **unless** the binary is allowed by an explicit rule. |
|
||||
| EnableSilentMode | Bool | If true, Santa will not post any GUI notifications. This can be a very confusing experience for users, use with caution. Defaults to NO. |
|
||||
| EnableSilentTTYMode | Bool | If true, Santa will not post any TTY notifications. This can be a very confusing experience for users, use with caution. Defaults to NO. |
|
||||
| AboutText | String | The text to display when the user opens Santa.app. If unset, the default text will be displayed. |
|
||||
| MoreInfoURL | String | The URL to open when the user clicks "More Info..." when opening Santa.app. If unset, the button will not be displayed. |
|
||||
| EventDetailURL | String | See the [EventDetailURL](#eventdetailurl) section below. |
|
||||
| EventDetailText | String | Related to the above property, this string represents the text to show on the button. |
|
||||
| UnknownBlockMessage | String | In Lockdown mode this is the message shown to the user when an unknown binary is blocked. If this message is not configured a reasonable default is provided. |
|
||||
| BannedBlockMessage | String | This is the message shown to the user when a binary is blocked because of a rule if that rule doesn't provide a custom message. If this is not configured a reasonable default is provided. |
|
||||
| ModeNotificationMonitor | String | The notification text to display when the client goes into Monitor mode. Defaults to "Switching into Monitor mode". |
|
||||
| ModeNotificationLockdown | String | The notification text to display when the client goes into Lockdown mode. Defaults to "Switching into Lockdown mode". |
|
||||
| <a name="sync-base-url"></a>SyncBaseURL | String | The base URL of the sync server. |
|
||||
| SyncProxyConfiguration | Dictionary | The proxy configuration to use when syncing. See the [Apple Documentation](https://developer.apple.com/documentation/cfnetwork/global_proxy_settings_constants) for details on the keys that can be used in this dictionary. |
|
||||
| SyncEnableCleanSyncEventUpload | Bool | If true, events will be uploaded to the sync server even if a clean sync is requested. Defaults to false. |
|
||||
| ClientAuthCertificateFile | String | If set, this contains the location of a PKCS#12 certificate to be used for sync authentication. |
|
||||
| ClientAuthCertificatePassword | String | Contains the password for the PKCS#12 certificate. |
|
||||
| ClientAuthCertificateCN | String | If set, this is the Common Name of a certificate in the System keychain to be used for sync authentication. The corresponding private key must also be in the keychain. |
|
||||
| ClientAuthCertificateIssuerCN | String | If set, this is the Issuer Name of a certificate in the System keychain to be used for sync authentication. The corresponding private key must also be in the keychain. |
|
||||
| ServerAuthRootsData | Data | If set, this is valid PEM containing one or more certificates to be used for certificate pinning. To comply with [ATS](https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW57) the certificate chain must also be trusted in the keychain. |
|
||||
| ServerAuthRootsFile | String | The same as the above but is a path to a file on disk containing the PEM data. |
|
||||
| MachineOwner | String | The machine owner. |
|
||||
| MachineID | String | The machine ID. |
|
||||
| MachineOwnerPlist | String | The path to a plist that contains the MachineOwnerKey / value pair. |
|
||||
| MachineOwnerKey | String | The key to use on MachineOwnerPlist. |
|
||||
| MachineIDPlist | String | The path to a plist that contains the MachineOwnerKey / value pair. |
|
||||
| MachineIDKey | String | The key to use on MachineIDPlist. |
|
||||
| EventLogType | String | Defines how event logs are stored. Options are 1) syslog: Sent to ULS. 2) filelog: Sent to a file on disk. Use EventLogPath to specify a path. 3) protobuf (BETA): Sent to file on disk using a maildir-like format. 4) json (BETA): Same as file but output is one JSON object per line 5) null: Don't output any event logs. Defaults to filelog. |
|
||||
| EventLogPath | String | If EventLogType is set to filelog or json, EventLogPath will provide the path to save logs. Defaults to /var/db/santa/santa.log. If you change this value ensure you also update com.google.santa.newsyslog.conf with the new path. |
|
||||
| EventLogPath | String | If EventLogType is set to filelog, EventLogPath will provide the path to save logs. Defaults to /var/db/santa/santa.log. If you change this value ensure you also update com.google.santa.newsyslog.conf with the new path. |
|
||||
| SpoolDirectory | String | If EventLogType is set to protobuf, SpoolDirectory will provide the the base directory used to save files according to a maildir-like format. Defaults to /var/db/santa/spool. |
|
||||
| Key | Value Type | Description |
|
||||
| ---------------------------------- | ---------- | ---------------------------------------- |
|
||||
| ClientMode\* | Integer | 1 = MONITOR, 2 = LOCKDOWN, defaults to MONITOR |
|
||||
| FailClosed | Bool | If true and the ClientMode is LOCKDOWN: execution will be denied when there is an error reading or processing an executable file and when Santa has to make a default response just prior to deadlines expiring. Defaults to false. |
|
||||
| FileChangesRegex\* | String | The regex of paths to log file changes. Regexes are specified in ICU format. |
|
||||
| AllowedPathRegex\* | String | A regex to allow if the binary, certificate, or Team ID scopes did not allow/block execution. Regexes are specified in ICU format. |
|
||||
| BlockedPathRegex\* | String | A regex to block if the binary, certificate, or Team ID scopes did not allow/block an execution. Regexes are specified in ICU format. |
|
||||
| FileChangesPrefixFilters | Array | Array of path prefix strings. When an event is logged, if the target path (e.g. the file being written/removed/etc ) matches a prefix it will not be loggged. |
|
||||
| EnableBadSignatureProtection | Bool | If true, binaries with a bad signing chain will be blocked even in MONITOR mode, **unless** the binary is allowed by an explicit rule. Defaults to false. |
|
||||
| EnablePageZeroProtection | Bool | If true, 32-bit binaries that are missing the `__PAGEZERO` segment will be blocked even in MONITOR mode, **unless** the binary is allowed by an explicit rule. Defaults to true. |
|
||||
| EnableSilentMode | Bool | If true, Santa will not post any GUI notifications. This can be a very confusing experience for users, use with caution. Defaults to false. |
|
||||
| EnableTransitiveRules | Bool | If true, Santa will respect compiler rule types and create allow rules for the executables they produce. Defaults to false. |
|
||||
| EnableSilentTTYMode | Bool | If true, Santa will not post any TTY notifications. This can be a very confusing experience for users, use with caution. Defaults to false. |
|
||||
| EnableForkAndExitLogging | Bool | If true, Santa will log FORK and EXIT event types. Defaults to false. |
|
||||
| IgnoreOtherEndpointSecurityClients | Bool | If true, Santa will not process events that are generated by other EndpointSecurity clients that may be installed on the system. Defaults to false. |
|
||||
| AboutText | String | The text to display when the user opens Santa.app. If unset, the default text will be displayed. |
|
||||
| MoreInfoURL | String | The URL to open when the user clicks "More Info..." when opening Santa.app. If unset, the button will not be displayed. |
|
||||
| EventDetailURL | String | See the [EventDetailURL](#eventdetailurl) section below. |
|
||||
| EventDetailText | String | Related to the above property, this string represents the text to show on the button. |
|
||||
| UnknownBlockMessage | String | In Lockdown mode this is the message shown to the user when an unknown binary is blocked. If this message is not configured a reasonable default is provided. |
|
||||
| BannedBlockMessage | String | This is the message shown to the user when a binary is blocked because of a rule if that rule doesn't provide a custom message. If this is not configured a reasonable default is provided. |
|
||||
| ModeNotificationMonitor | String | The notification text to display when the client goes into Monitor mode. Defaults to "Switching into Monitor mode". |
|
||||
| ModeNotificationLockdown | String | The notification text to display when the client goes into Lockdown mode. Defaults to "Switching into Lockdown mode". |
|
||||
| SyncBaseURL | String | The base URL of the sync server. |
|
||||
| SyncProxyConfiguration | Dictionary | The proxy configuration to use when syncing. See the [Apple Documentation](https://developer.apple.com/documentation/cfnetwork/global_proxy_settings_constants) for details on the keys that can be used in this dictionary. |
|
||||
| SyncEnableCleanSyncEventUpload | Bool | If true, events will be uploaded to the sync server even if a clean sync is requested. Defaults to false. |
|
||||
| ClientAuthCertificateFile | String | If set, this contains the location of a PKCS#12 certificate to be used for sync authentication. |
|
||||
| ClientAuthCertificatePassword | String | Contains the password for the PKCS#12 certificate. |
|
||||
| ClientAuthCertificateCN | String | If set, this is the Common Name of a certificate in the System keychain to be used for sync authentication. The corresponding private key must also be in the keychain. |
|
||||
| ClientAuthCertificateIssuerCN | String | If set, this is the Issuer Name of a certificate in the System keychain to be used for sync authentication. The corresponding private key must also be in the keychain. |
|
||||
| ServerAuthRootsData | Data | If set, this is valid PEM containing one or more certificates to be used for certificate pinning. To comply with [ATS](https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW57) the certificate chain must also be trusted in the keychain. |
|
||||
| ServerAuthRootsFile | String | The same as the above but is a path to a file on disk containing the PEM data. |
|
||||
| MachineOwner | String | The machine owner. |
|
||||
| MachineID | String | The machine ID. |
|
||||
| MachineOwnerPlist | String | The path to a plist that contains the MachineOwnerKey / value pair. |
|
||||
| MachineOwnerKey | String | The key to use on MachineOwnerPlist. |
|
||||
| MachineIDPlist | String | The path to a plist that contains the MachineOwnerKey / value pair. |
|
||||
| MachineIDKey | String | The key to use on MachineIDPlist. |
|
||||
| EventLogType | String | Defines how event logs are stored. Options are 1) syslog: Sent to ULS. 2) filelog: Sent to a file on disk. Use EventLogPath to specify a path. 3) protobuf (BETA): Sent to file on disk using a maildir-like format. 4) json (BETA): Same as file but output is one JSON object per line 5) null: Don't output any event logs. Defaults to filelog. |
|
||||
| EventLogPath | String | If EventLogType is set to filelog or json, EventLogPath will provide the path to save logs. Defaults to /var/db/santa/santa.log. If you change this value ensure you also update com.google.santa.newsyslog.conf with the new path. |
|
||||
| SpoolDirectory | String | If EventLogType is set to protobuf, SpoolDirectory will provide the base directory used to save files according to a maildir-like format. Defaults to /var/db/santa/spool. |
|
||||
| SpoolDirectoryFileSizeThresholdKB | Integer | If EventLogType is set to protobuf, SpoolDirectoryFileSizeThresholdKB defines the per-file size limit for files stored in the spool directory. Events are buffered in memory until this threshold would be exceeded (or SpoolDirectoryEventMaxFlushTimeSec is exceeded). Defaults to 100. |
|
||||
| SpoolDirectorySizeThresholdMB | Integer | If EventLogType is set to protobuf, SpoolDirectorySizeThresholdMB defines the total combined size limit of all files in the spool directory. Once the threshold is met, no more events will be saved. Defaults to 100. |
|
||||
| SpoolDirectoryEventMaxFlushTimeSec | Integer | If EventLogType is set to protobuf, SpoolDirectoryEventMaxFlushTimeSec defines the maximum amount of time events will stay buffered in memory before being flushed to disk, regardless of whether or not SpoolDirectoryFileSizeThresholdKB would be exceeded. Defaults to 10. |
|
||||
| EnableMachineIDDecoration | Bool | If YES, this appends the MachineID to the end of each log line. Defaults to NO. |
|
||||
| MetricFormat | String | Format to export metrics as, supported formats are "rawjson" for a single JSON blob and "monarchjson" for a format consumable by Google's Monarch tooling. Defaults to "". |
|
||||
| MetricURL | String | URL describing where monitoring metrics should be exported. |
|
||||
| MetricExportInterval | Integer | Number of seconds to wait between exporting metrics. Defaults to 30. |
|
||||
| MetricExportTimeout | Integer | Number of seconds to wait before a timeout occurs when exporting metrics. Defaults to 30. |
|
||||
| MetricExtraLabels | Dictionary | A map of key value pairs to add to all metric root labels. (e.g. a=b,c=d) defaults to @{}). If a previously set key (e.g. host_name is set to "" then the key is remove from the metric root labels. Alternatively if a value is set for an existing key then the new value will override the old. |
|
||||
| EnableAllEventUpload | Bool | If YES, the client will upload all execution events to the sync server, including those that were explicitly allowed. |
|
||||
| DisableUnknownEventUpload | Bool | If YES, the client will *not* upload events for executions of unknown binaries allowed in monitor mode |
|
||||
| BlockUSBMount | Bool | If YES, blocking USB Mass storage feature is enabled. Defaults to NO. |
|
||||
| RemountUSBMode | Array | Array of strings for arguments to pass to mount -o (any of "rdonly", "noexec", "nosuid", "nobrowse", "noowners", "nodev", "async", "-j") when forcibly remounting devices. No default. |
|
||||
| OnStartUSBOptions | String | If set, defines the action that should be taken on existing USB mounts when Santa starts up. Supported values are "Unmount", "ForceUnmount", "Remount", and "ForceRemount" (note: "remounts" are implemented by first unmounting and then mounting the device again). Existing mounts with mount flags that are a superset of RemountUSBMode are unaffected and left mounted. |
|
||||
| FileAccessPolicyPlist | String | Path to a file access configuration plist. This is ignored if `FileAccessPolicy` is also set. |
|
||||
| FileAccessPolicy | Dictionary | A complete file access configuration policy embedded in the main Santa config. If set, `FileAccessPolicyPlist` will be ignored. |
|
||||
| FileAccessPolicyUpdateIntervalSec | Integer | Number of seconds between re-reading the file access policy config and policies/monitored paths updated. |
|
||||
| SyncClientContentEncoding | String | Sets the Content-Encoding header for requests sent to the sync service. Acceptable values are "deflate", "gzip", "none" (Defaults to deflate.) |
|
||||
| SyncExtraHeaders | Dictionary | Dictionary of additional headers to include in all requests made to the sync server. System managed headers such as Content-Length, Host, WWW-Authenticate etc will be ignored. |
|
||||
| EnableDebugLogging | Bool | If YES, the client will log additional debug messages to the Apple Unified Log. For example, transitive rule creation logs can be viewed with `log stream --predicate 'sender=="com.google.santa.daemon"'`. Defaults to NO. |
|
||||
| EnableMachineIDDecoration | Bool | If true, this appends the MachineID to the end of each log line. Defaults to false. |
|
||||
| MetricFormat | String | Format to export metrics as, supported formats are "rawjson" for a single JSON blob and "monarchjson" for a format consumable by Google's Monarch tooling. Defaults to "". |
|
||||
| MetricURL | String | URL describing where monitoring metrics should be exported. |
|
||||
| MetricExportInterval | Integer | Number of seconds to wait between exporting metrics. Defaults to 30. |
|
||||
| MetricExportTimeout | Integer | Number of seconds to wait before a timeout occurs when exporting metrics. Defaults to 30. |
|
||||
| MetricExtraLabels | Dictionary | A map of key value pairs to add to all metric root labels. (e.g. a=b,c=d) defaults to @{}). If a previously set key (e.g. host_name is set to "" then the key is remove from the metric root labels. Alternatively if a value is set for an existing key then the new value will override the old. |
|
||||
| EnableAllEventUpload | Bool | If true, the client will upload all execution events to the sync server, including those that were explicitly allowed. Defaults to false. |
|
||||
| DisableUnknownEventUpload | Bool | If true, the client will *not* upload events for executions of unknown binaries allowed in monitor mode. Defaults to false. |
|
||||
| BlockUSBMount | Bool | If true, blocking USB Mass storage feature is enabled. Defaults to false. |
|
||||
| RemountUSBMode | Array | Array of strings for arguments to pass to mount -o (any of "rdonly", "noexec", "nosuid", "nobrowse", "noowners", "nodev", "async", "-j") when forcibly remounting devices. No default. |
|
||||
| OnStartUSBOptions | String | If set, defines the action that should be taken on existing USB mounts when Santa starts up. Supported values are "Unmount", "ForceUnmount", "Remount", and "ForceRemount" (note: "remounts" are implemented by first unmounting and then mounting the device again). Existing mounts with mount flags that are a superset of RemountUSBMode are unaffected and left mounted. |
|
||||
| BannedUSBBlockMessage | String | Message to display when a USB device is prevented from being mounted. |
|
||||
| RemountUSBBlockMessage | String | Message to display when a USB device is allowed to be mounted with a subset of the requested flags as defined by `RemountUSBMode`. |
|
||||
| FileAccessPolicyPlist | String | Path to a file access configuration plist. This is ignored if `FileAccessPolicy` is also set. |
|
||||
| FileAccessPolicy | Dictionary | A complete file access configuration policy embedded in the main Santa config. If set, `FileAccessPolicyPlist` will be ignored. |
|
||||
| FileAccessPolicyUpdateIntervalSec | Integer | Number of seconds between re-reading the file access policy config and policies/monitored paths updated. |
|
||||
| FileAccessBlockMessage | String | This is the message shown to the user when a access to a file is blocked because of a rule defined by `FileAccessPolicy` if that rule doesn't provide a custom message. If this is not configured a reasonable default is provided. |
|
||||
| OverrideFileAccessAction | String | Defines a global override policy that applies to the enforcement of all `FileAccessPolicy` rules. Allowed values are: `auditonly` (no access will be blocked, only logged), `disabled` (no access will be blocked or logged), `none` (enforce policy as defined in each rule). Defaults to `none`. |
|
||||
| SyncClientContentEncoding | String | Sets the Content-Encoding header for requests sent to the sync service. Acceptable values are "deflate", "gzip", "none". Defaults to deflate. |
|
||||
| SyncExtraHeaders | Dictionary | Dictionary of additional headers to include in all requests made to the sync server. System managed headers such as Content-Length, Host, WWW-Authenticate etc will be ignored. |
|
||||
| EnableDebugLogging | Bool | If true, the client will log additional debug messages to the Apple Unified Log. For example, transitive rule creation logs can be viewed with `log stream --predicate 'sender=="com.google.santa.daemon"'`. Defaults to false. |
|
||||
| EntitlementsPrefixFilter | Array | Array of strings of entitlement prefixes that should not be logged (for example: `com.apple.private`). No default. |
|
||||
| EntitlementsTeamIDFilter | Array | Array of TeamID strings. Entitlements from processes with a matching TeamID in the code signature will not be logged. Use the value `platform` to filter entitlements from platform binaries. No default. |
|
||||
| [StaticRules](#static-rules) | Array | Array of rule dictionaries. The rules defined in this key take precedence over any rules in the rules database. |
|
||||
|
||||
|
||||
\*overridable by the sync server: run `santactl status` to check the current
|
||||
running config
|
||||
|
||||
##### EventDetailURL
|
||||
### EventDetailURL
|
||||
|
||||
When the user gets a block notification, a button can be displayed which will
|
||||
take them to a web page with more information about that event.
|
||||
@@ -104,7 +114,23 @@ them to. The following sequences will be replaced in the final URL:
|
||||
|
||||
For example: `https://sync-server-hostname/%machine_id%/%file_sha%`
|
||||
|
||||
##### Example Configuration Profile
|
||||
### Static Rules
|
||||
|
||||
Static rules are rules that are defined inline in the Santa configuration. These
|
||||
rules take precedence over any rules delivered via a sync server or set via
|
||||
`santactl`. The relative rule order precedence is the same for the static rule
|
||||
types as they are for traditional rules.
|
||||
|
||||
The allowed keys/values for defining static rules are the same as for rules that
|
||||
are sent via the sync server. Details on this structure are defined in the
|
||||
[Sync Protocol](https://santa.dev/development/sync-protocol.html#rules-objects)
|
||||
documentation.
|
||||
|
||||
Additionally, the
|
||||
[example configuration](https://github.com/google/santa/blob/d5195b55d2784776fa078096f59137d22da55b06/docs/deployment/com.google.santa.example.mobileconfig#L45)
|
||||
has a demonstration on how to define static rules.
|
||||
|
||||
### Example Configuration Profile
|
||||
|
||||
Here is an example of a configuration profile that could be set. It was
|
||||
generated with Tim Sutton's great
|
||||
@@ -233,6 +259,7 @@ ways to install configuration profiles:
|
||||
| enable\_all\_event\_upload | Bool | If set to `True` the client will upload events for all executions, including those that are explicitly allowed. |
|
||||
| block\_usb\_mount | Bool | If set to 'True' blocking USB Mass storage feature is enabled. Defaults to `False`. |
|
||||
| remount\_usb\_mode | Array | Array of strings for arguments to pass to mount -o (any of "rdonly", "noexec", "nosuid", "nobrowse", "noowners", "nodev", "async", "-j"). when forcibly remounting devices. No default. |
|
||||
| override\_file\_access\_action | String | Defines a global override policy that applies to the enforcement of all `FileAccessPolicy` rules. Allowed values are: `auditonly` (no access will be blocked, only logged), `disabled` (no access will be blocked or logged), `none` (enforce policy as defined in each rule). Defaults to `none`. |
|
||||
|
||||
|
||||
*Held only in memory. Not persistent upon process restart.
|
||||
|
||||
@@ -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 `<machine_id>` 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 `/<stage_name>/<machine_id>`, e.g. the preflight URL is `/preflight/<machine_id>`.
|
||||
|
||||
### 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:<br />1.) `normal` (or not set) The server intends only to send new rules. The client will not drop any existing rules.<br />2.) `clean` Instructs the client to drop all non-transitive rules. The server intends to entirely sync all rules.<br />3.) `clean_all` Instructs the client to drop all rules. The server intends to entirely sync all rules.<br />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:<br />1.) "Disable" to not log or block any rule violations.<br />2.) "AuditOnly" to only log violations, not block anything.<br />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).
|
||||
|
||||
@@ -187,7 +205,7 @@ sequenceDiagram
|
||||
| file_path | YES | string | Absolute file path to the executable that was blocked | "/tmp/foo" |
|
||||
| file_name | YES | string | Command portion of the path of the blocked executable | "foo" |
|
||||
| executing_user | NO | string | Username that executed the binary | "markowsky" |
|
||||
| execution_time | NO | float64 | Unix timestamp of when the execution occured | 23344234232 |
|
||||
| execution_time | NO | float64 | Unix timestamp of when the execution occurred | 23344234232 |
|
||||
| loggedin_users | NO | list of strings | List of usernames logged in according to utmp | ["markowsky"] |
|
||||
| current_sessions | NO | list of strings | List of user sessions | ["markowsky@console", "markowsky@ttys000"] |
|
||||
| decision | YES | string | The decision Santa made for this binary, BUNDLE_BINARY is used to preemptively report binaries in a bundle. **Must be one of the examples** | "ALLOW_BINARY", "ALLOW_CERTIFICATE", "ALLOW_SCOPE", "ALLOW_TEAMID", "ALLOW_UNKNOWN", "BLOCK_BINARY", "BLOCK_CERTIFICATE", "BLOCK_SCOPE", "BLOCK_TEAMID", "BLOCK_UNKNOWN", "BUNDLE_BINARY" |
|
||||
@@ -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.
|
||||
|
||||
@@ -319,7 +337,9 @@ If a clean sync was not requested by either the client or the sync service, then
|
||||
|
||||
Santa applies rules idempotently and is designed to receive rules multiple times without issue.
|
||||
|
||||
#### `ruledownload` Request
|
||||
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
|
||||
|
||||
This stage is initiated via an HTTP POST request to the URL `/ruledownload/<machine_id>`
|
||||
|
||||
@@ -328,7 +348,7 @@ Santa applies rules idempotently and is designed to receive rules multiple times
|
||||
| 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
|
||||
|
||||
@@ -344,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
|
||||
@@ -355,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 |
|
||||
@@ -370,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
|
||||
{
|
||||
@@ -398,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.
|
||||
|
||||
@@ -410,7 +430,7 @@ sequenceDiagram
|
||||
server -->> client: postflight response
|
||||
```
|
||||
|
||||
#### `postflight` Request
|
||||
### `postflight` Request
|
||||
|
||||
The request consists of the following JSON keys:
|
||||
|
||||
@@ -419,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
|
||||
{
|
||||
@@ -429,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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user