Compare commits

...

36 Commits

Author SHA1 Message Date
Matt W
e4c0d56bb6 Remove proc tree tests for now as the code isn't yet included in santa builds (#1287) 2024-02-08 16:01:47 -05:00
Matt W
908b1bcabe Add build dep for internal process (#1286) 2024-02-08 15:43:01 -05:00
Matt W
64e81bedc6 Respect fail closed on deadlines (#1285)
* Responses to events about to exceed deadline should respect FailClosed

* Only respect FailClosed when in Lockdown mode. Update docs.

* FailClosed in Configurator now wraps checking client mode

* PR feedback

* Fix execution controller tests with new FailClosed logic
2024-02-08 15:12:05 -05:00
Matt W
5dfab22fa7 Fix automatically denied events with small deadlines (#1284)
* Fix automatically denied events with small deadlines

* Fix up additional tests that had defined deadline interactions
2024-02-08 10:25:06 -05:00
Nick Gregory
5248e2a7eb Fix import issues and lint (#1282)
* lint

* case insensitive filesystems ahhhh

* tidy

* one last header
2024-02-07 17:46:42 -05:00
Nick Gregory
e8db89c57c ProcessTree: add core process tree logic (1/4) (#1236)
* ProcessTree: add core process tree logic

* make Step implicitly called by Handle* methods

* lint

* naming convention

* widen pidversion to be generic

* move os specific backfill to os specific impl

* simplify ts checking

* retain/release a whole vec of pids

* document processtoken

* lint

* namespace

* add process tree to project-wide unit test target

* case change annotations

* case change annotations

* remove stray comment

* default initialize seen_timestamps

* fix missing initialization of refcnt and tombstoned

* reshuffle pb namespace

* pr review

* move annotation registration to tree construction

* use factory function for tree construction
2024-02-05 14:30:54 -05:00
Matt W
70474aba3e Sync clean all (#1275)
* WIP Clean syncs now leave non-transitive rules by default

* WIP Get existing tests compiling and passing

* Remove clean all sync server key. Basic tests.

* Add SNTConfiguratorTest, test deprecated key migration

* Revert changes to santactl status output

* Add new preflight response sync type key, lots of tests

* Rework configurator flow a bit so calls cannot be made out of order

* Comment clean sync states. Test all permutations.

* Update docs for new sync keys

* Doc updates as requested in PR
2024-01-24 09:26:20 -05:00
Pete Markowsky
f4ad76b974 Make santactl status always print out transitive rule status if set (#1277)
* Make santactl status always print out transitive rule status even when not using a sync service.

* Fix typo in SNTCommandRule.m.

* Updated JSON values to put transitive_rules in the daemon section.
2024-01-22 12:16:47 -05:00
hugo-syn
3b7061ea62 chore: Fix typo s/occured/occurred/ (#1274)
Signed-off-by: hugo-syn <hugo.vincent@synacktiv.com>
2024-01-18 10:50:01 -05:00
hugo-syn
280d93ee08 chore: Fix multiple typos (#1273)
Signed-off-by: hugo-syn <hugo.vincent@synacktiv.com>
2024-01-18 09:17:52 -05:00
Matt W
f73463117f Add back support for EnableForkAndExitLogging config key (#1271) 2024-01-14 13:42:06 -05:00
Matt W
f93e1a56a0 Docs add missing config keys (#1270)
* Add missing config keys

* Use more consistent wording

* More consistent whitespace

* Reorder constants to appropriate section groups

* Update docs/deployment/configuration.md

Co-authored-by: Pete Markowsky <pmarkowsky@users.noreply.github.com>

---------

Co-authored-by: Pete Markowsky <pmarkowsky@users.noreply.github.com>
2024-01-13 00:08:16 -05:00
Pete Markowsky
d5195b55d2 Added documentation to clarify clean sync with zero rule behavior (#1259)
* Added documentation to clarify clean sync with zero rule behaivor.

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>
2024-01-09 16:10:27 -05:00
Matt W
15e5874d43 Fix wrong srcs paths (#1265) 2024-01-03 10:49:08 -05:00
Matt W
5e6fa09f1c Change build target visibility (#1264)
* Change build target visibility

* Add dependent headers as srcs. Remove unnecessary visibility.
2024-01-03 10:21:33 -05:00
Matt W
ce2777ae94 Fix santactl rule --check (#1262)
* Fix santactl rule check to only strictly show rule info

* Reorganized to make more testable, added tests
2024-01-03 09:52:14 -05:00
Matt W
f8a20d35b4 Fix issue with drop count calculations (#1256) 2023-12-13 17:01:11 -05:00
Matt W
2e69370524 Event drop metrics (#1253)
* Add dropped event detection and metrics

* Update metrics test for drop counts

* Comment new interface
2023-12-07 15:23:51 -05:00
Russell Hancox
f9b4e00e0c GUI: Change default button text to "Open..." (#1254) 2023-12-06 14:19:27 -05:00
Matt W
e2e83a099c Initial support for some scoped types (#1250)
* Add some scoped types to handle automatic releasing

* style

* comment typo
2023-12-05 18:51:07 -05:00
Matt W
2cbf15566a Revert "Project: Remove provisioning_profiles attributes from command-line tool rules (#1247)" (#1251)
This reverts commit 65c660298c.
2023-12-05 15:48:36 -05:00
Nick Gregory
1596990c65 reorder e2e tests (#1249) 2023-12-04 13:01:30 -05:00
Matt W
221664436f Expand debug logging for transitive rule failure case (#1248) 2023-11-30 15:47:48 -05:00
Russell Hancox
65c660298c Project: Remove provisioning_profiles attributes from command-line tool rules (#1247) 2023-11-30 13:50:38 -05:00
Matt W
2b5d55781c Revert back to C++17 for now (#1246) 2023-11-29 21:39:48 -05:00
Matt W
84e6d6ccff Fix USB state issue in santactl status (#1244) 2023-11-29 17:56:35 -05:00
Matt W
c16f90f5f9 Fix test issue caused by move to C++20 (#1245)
* Fix test issue caused by move to C++20

* Use spaceship operator as is the style of the time

* lint

* Add include
2023-11-29 16:52:23 -05:00
Matt W
d503eae4d9 Bump to C++20 (#1243) 2023-11-29 09:57:45 -05:00
Matt W
818518bb38 Ignore TeamID and SigningID rules for dev signed code (#1241)
* Ignore TID/SID rules for dev signed code

* Handle code paths from santactl

* Don't bother evaluating isProdSignedCallback if not necessary

* PR feedback. Link to docs.
2023-11-27 11:21:17 -05:00
Matt W
f499654951 Experimental metrics (#1238)
* Experimental metrics

* Fix tests, old platform builds

* Use more recent availability checks

* Update macro name, undef after usage
2023-11-20 13:02:58 -05:00
Matt W
a5e8d77d06 Entitlements logging config options (#1233)
* WIP add config support to filter logged entitlements

* Add EntitlementInfo proto message to store if entitlements were filtered

* Log cleanup

* Address PR feedback

* Address PR feedback
2023-11-13 09:39:32 -05:00
Matt W
edac42e8b8 Fix internal build issues, minor cleanup. (#1231) 2023-11-09 17:26:31 -05:00
Matt W
ce5e3d0ee4 Add support for logging entitlements in EXEC events (#1225)
* Add support for logging entitlements in EXEC events

* Standardize entitlement dictionary formatting
2023-11-09 16:26:57 -05:00
Pete Markowsky
3e51ec6b8a Add name for white space check (#1223)
* Add a name to the whitespace check in the check-markdown workflow.

* Pin workflow steps.
2023-11-09 15:26:51 -05:00
Travis Lane
ed227f43d4 Explicitly cast strings to std::string_view (#1230)
GoogleTest when built with GTEST_HAS_ABSL fails to convert these strings
to a `std::string_view`. Lets instead explicitly convert them to a
`std::string_view`.
2023-11-08 17:05:08 -05:00
Nick Gregory
056ed75bf1 dismiss santa popup after integration tests (#1226) 2023-11-07 14:42:03 -05:00
97 changed files with 3837 additions and 499 deletions

View File

@@ -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'"

View File

@@ -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

View File

@@ -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) {

View File

@@ -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"],
)

View File

@@ -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:

View File

@@ -38,6 +38,8 @@
@property NSArray<MOLCertificate *> *certChain;
@property NSString *teamID;
@property NSString *signingID;
@property NSDictionary *entitlements;
@property BOOL entitlementsFiltered;
@property NSString *quarantineURL;

View File

@@ -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,

View File

@@ -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.
///

View File

@@ -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 {

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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"

View File

@@ -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;

View File

@@ -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";

View File

@@ -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;

View File

@@ -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];

View File

@@ -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.

View File

@@ -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

View File

@@ -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;
}
};

View 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

View 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

View 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

View 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

View 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

View File

@@ -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, *)) {

View File

@@ -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

View File

@@ -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">

View File

@@ -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"],
)

View 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

View File

@@ -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;
}

View 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

View File

@@ -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) {

View File

@@ -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."];

View File

@@ -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",

View File

@@ -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

View File

@@ -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) {

View File

@@ -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];

View File

@@ -41,6 +41,8 @@ enum class FlushCacheReason {
kStaticRulesChanged,
kExplicitCommand,
kFilesystemUnmounted,
kEntitlementsPrefixFilterChanged,
kEntitlementsTeamIDFilterChanged,
};
class AuthResultCache {

View File

@@ -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)];

View File

@@ -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

View File

@@ -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"

View File

@@ -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);

View File

@@ -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

View File

@@ -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 *> *,

View File

@@ -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"

View File

@@ -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)) {

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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"

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View 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",
],
)

View 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",
],
)

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,6 @@
syntax = "proto3";
package santa.pb.v1.process_tree;
message Annotations {
}

View 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

View 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

View 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

View 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

View File

@@ -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]);

View File

@@ -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();

View File

@@ -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

View File

@@ -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);

View File

@@ -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];

View File

@@ -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

View File

@@ -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];
}
///

View File

@@ -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, *)) {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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\"]"
}
]
}
}

View File

@@ -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\"]"
}
]
}
}

View File

@@ -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\"]"
}
]
}
}

View File

@@ -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\"]"
}
]
}
}

View File

@@ -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\"]"
}
]
}
}

View File

@@ -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;

View File

@@ -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.

View File

@@ -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);

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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;

View File

@@ -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$";

View File

@@ -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

View File

@@ -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.

View File

@@ -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.