Compare commits

...

22 Commits

Author SHA1 Message Date
Russell Hancox
feac080fa7 sync: Permit XSRF header between sync stages/sessions (#1081) 2023-04-27 10:52:35 -04:00
Nick Gregory
d0f2a0ac4d One more TSAN fix (#1079) 2023-04-26 17:30:06 +02:00
Pete Markowsky
7fc06ea9d8 Make the sync client content encoding a tunable (#1076)
Make the sync client content encoding a tunable.

This makes the sync client's content encoding a tunable so that it can be
compatible with more sync servers.

Removed the "backwards compatibility" config option.

---------

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>
2023-04-24 15:00:29 +02:00
Russell Hancox
1dfeeac936 README: Add more badges (#1075) 2023-04-21 09:54:33 -04:00
Matt W
ac9b5d9399 Cache flush metrics (#1074)
* Added a reason enum when flushing auth result cache

* Set metrics when auth result cache is flushed.
2023-04-20 16:47:06 -04:00
Matt W
7f3f1c5448 Process unmount events first (#1073) 2023-04-19 11:13:13 -04:00
Russell Hancox
46efd6893f config: Add EnableSilentTTYMode key to disable TTY notifications. (#1072)
Fixes #1067
2023-04-19 10:38:24 -04:00
Matt W
50232578d6 Fix string length issues (#1070) 2023-04-13 10:03:52 -04:00
Russell Hancox
d83be03a20 sync: Add more complete XSSI prefix to be stripped. (#1068)
Sync will try stripping both the new longer prefix and the existing short prefix if the response data begins with either. This should have no impact on existing sync servers but will allow sync servers in the future to use the longer prefix if they wish.
2023-04-07 15:27:41 -04:00
Russell Hancox
119b29b534 GUI: Device event window, handle empty remount args (#1066) 2023-04-05 16:34:05 -04:00
Matt W
be87b3eaf2 Change types of repeated args and envs fields (#1063)
* Change types of repeated args and envs fields

* Update args and env testdata strings to base64

* Remove whitespace
2023-03-31 13:18:09 -04:00
Russell Hancox
0fe672817e sync: Fix case of empty header name (#1062) 2023-03-28 11:50:11 -04:00
Russell Hancox
c3b2fbf512 sync: Allow server to override the header for transmitting XSRF tokens (#1060)
This change allows a sync server to change the header that Santa will use to send XSRF tokens on subsequent requests by putting the header name in the  header.
2023-03-27 18:11:11 -04:00
Matt W
2984d98cb9 Document SigningID and PlatformBinary exception keys (#1059)
* Document SigningID and PlatformBinary exception keys

* Minor spacing
2023-03-25 11:34:06 -04:00
Nick Gregory
5295faef0e Fix a couple last TSAN failures (#1056)
* Skip testHandleMessage when testing with tsan

* fix other 2 tsan failures

* change action_env->test_env in bazelrc for sanitizers

* revert Source/santactl/BUILD formatting
2023-03-23 11:11:29 -04:00
Liam Nicholson
0209344f62 santad: Fix SD Card Block not operating on Internal SD Card Readers (#1055) 2023-03-22 17:54:11 -04:00
Matt W
53ca5eb811 Support filesystem monitoring config embedded in main Santa config (#1054)
* Allow setting file access policy in main Santa config

* Add some tests
2023-03-20 16:47:34 -04:00
Matt W
33c7aab9f1 Basic rate limiting for File Access Authorizer (#1053)
* WIP basic rate limiting support

* WIP added basic metrics when rate limited

* Hookup new metrics

* Cleanup old TODO

* PR feedback, update comments
2023-03-20 15:58:49 -04:00
Pål-Magnus Slåtto
f6d837ac31 chore(ci): Upgrade workflows to non-deprecated runtimes (#1052) 2023-03-15 09:42:16 -04:00
Matt W
5e0a383662 Properly report "file access client enabled" metrics (#1051) 2023-03-14 15:01:03 -04:00
Russell Hancox
8055b451bb Config: Ignore static rules with an invalid identifier (#1049) 2023-03-07 10:33:13 -05:00
Russell Hancox
c5e7736eef santactl/rule: Validate identifier is a valid SHA-256 for binary/cert rules (#1045)
Previously validation only applied when using the --sha256 flag, now it applies to --identifier too unless adding a team ID rule. The validation is also a bit more robust.

Fixes #1044
2023-03-01 13:44:44 -05:00
65 changed files with 1289 additions and 386 deletions

View File

@@ -21,20 +21,20 @@ build:asan --config=san-common
build:asan --copt="-fsanitize=address"
build:asan --copt="-DADDRESS_SANITIZER"
build:asan --linkopt="-fsanitize=address"
build:asan --action_env="ASAN_OPTIONS=log_path=/tmp/san_out"
build:asan --test_env="ASAN_OPTIONS=log_path=/tmp/san_out"
build:tsan --config=san-common
build:tsan --copt="-fsanitize=thread"
build:tsan --copt="-DTHREAD_SANITIZER=1"
build:tsan --linkopt="-fsanitize=thread"
build:asan --action_env="TSAN_OPTIONS=log_path=/tmp/san_out"
build:tsan --test_env="TSAN_OPTIONS=log_path=/tmp/san_out:halt_on_error=true"
build:ubsan --config=san-common
build:ubsan --copt="-fsanitize=undefined"
build:ubsan --copt="-DUNDEFINED_SANITIZER=1"
build:ubsan --copt="-fno-sanitize=function" --copt="-fno-sanitize=vptr"
build:ubsan --linkopt="-fsanitize=undefined"
build:ubsan --action_env="UBSAN_OPTIONS=log_path=/tmp/san_out"
build:ubsan --test_env="UBSAN_OPTIONS=log_path=/tmp/san_out"
build:fuzz --config=san-common
build:fuzz --@rules_fuzzing//fuzzing:cc_engine=@rules_fuzzing//fuzzing/engines:libfuzzer

View File

@@ -16,7 +16,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Run linters
run: ./Testing/lint.sh
@@ -27,7 +27,7 @@ jobs:
os: [macos-11, macos-12]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Build Userspace
run: bazel build --apple_generate_dsym -c opt :release --define=SANTA_BUILD_TYPE=adhoc
@@ -38,14 +38,14 @@ jobs:
os: [macos-11, macos-12]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Run All Tests
run: bazel test :unit_tests --define=SANTA_BUILD_TYPE=adhoc --test_output=errors
test_coverage:
runs-on: macos-11
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Generate test coverage
run: sh ./generate_cov.sh
- name: Coveralls

View File

@@ -8,6 +8,6 @@ jobs:
preqs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Checks for flaky tests
run: bazel test --test_strategy=exclusive --test_output=errors --runs_per_test 50 -t- :unit_tests --define=SANTA_BUILD_TYPE=adhoc

View File

@@ -6,7 +6,7 @@ jobs:
start_vm:
runs-on: e2e-host
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Start VM
run: python3 Testing/integration/actions/start_vm.py macOS_12.bundle.tar.gz
@@ -15,7 +15,7 @@ jobs:
env:
VM_PASSWORD: ${{ secrets.VM_PASSWORD }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Install configuration profile
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
- name: Add homebrew to PATH

View File

@@ -9,14 +9,14 @@ jobs:
start_vm:
runs-on: e2e-host
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Start VM
run: python3 Testing/integration/actions/start_vm.py macOS_13.bundle.tar.gz
fuzz:
runs-on: e2e-vm
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Setup libfuzzer
run: Fuzzing/install_libclang_fuzzer.sh
- name: Fuzz

View File

@@ -11,14 +11,14 @@ jobs:
matrix:
sanitizer: [asan, tsan, ubsan]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: ${{ matrix.sanitizer }}
run: |
CLANG_VERSION=$(clang --version | head -n 1 | cut -d' ' -f 4)
DYLIB_PATH="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/${CLANG_VERSION}/lib/darwin/libclang_rt.${{ matrix.sanitizer }}_osx_dynamic.dylib"
bazel test --config=${{ matrix.sanitizer }} \
--test_strategy=exclusive --test_output=errors \
--test_strategy=exclusive --test_output=all \
--test_env=DYLD_INSERT_LIBRARIES=${DYLIB_PATH} \
--runs_per_test 5 -t- :unit_tests \
--define=SANTA_BUILD_TYPE=adhoc

View File

@@ -1,4 +1,10 @@
# Santa [![CI](https://github.com/google/santa/actions/workflows/ci.yml/badge.svg)](https://github.com/google/santa/actions/workflows/ci.yml)
# Santa
[![license](https://img.shields.io/github/license/google/santa)](https://github.com/google/santa/blob/main/LICENSE)
[![CI](https://github.com/google/santa/actions/workflows/ci.yml/badge.svg)](https://github.com/google/santa/actions/workflows/ci.yml)
[![latest release](https://img.shields.io/github/v/release/google/santa.svg)](https://github.com/google/santa/releases/latest)
[![latest release date](https://img.shields.io/github/release-date/google/santa.svg)](https://github.com/google/santa/releases/latest)
[![downloads](https://img.shields.io/github/downloads/google/santa/latest/total)](https://github.com/google/santa/releases/latest)
<p align="center">
<img src="https://raw.githubusercontent.com/google/santa/main/Source/gui/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />

View File

@@ -69,6 +69,11 @@ objc_library(
hdrs = ["Platform.h"],
)
objc_library(
name = "String",
hdrs = ["String.h"],
)
objc_library(
name = "SantaVnodeHash",
srcs = ["SantaVnodeHash.mm"],

View File

@@ -191,12 +191,12 @@ using santa::common::PrefixTree;
uint32_t count = 4096;
auto t = new PrefixTree<int>(count * (uint32_t)[NSUUID UUID].UUIDString.length);
NSMutableArray *UUIDs = [NSMutableArray arrayWithCapacity:count];
__block NSMutableArray *UUIDs = [NSMutableArray arrayWithCapacity:count];
for (int i = 0; i < count; ++i) {
[UUIDs addObject:[NSUUID UUID].UUIDString];
}
__block BOOL stop = NO;
__block _Atomic BOOL stop = NO;
// Create a bunch of background noise.
dispatch_async(dispatch_get_global_queue(0, 0), ^{

View File

@@ -129,6 +129,12 @@ typedef NS_ENUM(NSInteger, SNTSyncStatusType) {
SNTSyncStatusTypeUnknown,
};
typedef NS_ENUM(NSInteger, SNTSyncContentEncoding) {
SNTSyncContentEncodingNone,
SNTSyncContentEncodingDeflate,
SNTSyncContentEncodingGzip,
};
typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
SNTMetricFormatTypeUnknown,
SNTMetricFormatTypeRawJSON,

View File

@@ -245,10 +245,20 @@
///
@property(readonly, nonatomic) float spoolDirectoryEventMaxFlushTimeSec;
///
/// If set, contains the filesystem access policy configuration.
///
/// @note: The property fileAccessPolicyPlist will be ignored if
/// fileAccessPolicy is set.
/// @note: This property is KVO compliant.
///
@property(readonly, nonatomic) NSDictionary *fileAccessPolicy;
///
/// If set, contains the path to the filesystem access policy config plist.
///
/// @note: This property is KVO compliant, but is only read once at santad startup.
/// @note: This property will be ignored if fileAccessPolicy is set.
/// @note: This property is KVO compliant.
///
@property(readonly, nonatomic) NSString *fileAccessPolicyPlist;
@@ -281,6 +291,14 @@
///
@property(readonly, nonatomic) BOOL enableSilentMode;
///
/// When silent TTY mode is enabled, Santa will not emit TTY notifications for
/// blocked processes.
///
/// Defaults to NO.
///
@property(readonly, nonatomic) BOOL enableSilentTTYMode;
///
/// The text to display when opening Santa.app.
/// If unset, the default text will be displayed.
@@ -392,6 +410,8 @@
///
@property(nonatomic) BOOL syncCleanRequired;
#pragma mark - USB Settings
///
/// USB Mount Blocking. Defaults to false.
///
@@ -502,6 +522,12 @@
///
@property(readonly, nonatomic) BOOL enableBackwardsCompatibleContentEncoding;
///
/// If set, "santactl sync" will use the supplied "Content-Encoding", possible
/// settings include "gzip", "deflate", "none". If empty defaults to "deflate".
///
@property(readonly, nonatomic) SNTSyncContentEncoding syncClientContentEncoding;
///
/// Contains the FCM project name.
///

View File

@@ -13,6 +13,7 @@
/// limitations under the License.
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTCommonEnums.h"
#include <sys/stat.h>
@@ -72,6 +73,7 @@ static NSString *const kMachineIDPlistFileKey = @"MachineIDPlist";
static NSString *const kMachineIDPlistKeyKey = @"MachineIDKey";
static NSString *const kEnableSilentModeKey = @"EnableSilentMode";
static NSString *const kEnableSilentTTYModeKey = @"EnableSilentTTYMode";
static NSString *const kAboutTextKey = @"AboutText";
static NSString *const kMoreInfoURLKey = @"MoreInfoURL";
static NSString *const kEventDetailURLKey = @"EventDetailURL";
@@ -97,6 +99,7 @@ static NSString *const kSpoolDirectoryFileSizeThresholdKB = @"SpoolDirectoryFile
static NSString *const kSpoolDirectorySizeThresholdMB = @"SpoolDirectorySizeThresholdMB";
static NSString *const kSpoolDirectoryEventMaxFlushTimeSec = @"SpoolDirectoryEventMaxFlushTimeSec";
static NSString *const kFileAccessPolicy = @"FileAccessPolicy";
static NSString *const kFileAccessPolicyPlist = @"FileAccessPolicyPlist";
static NSString *const kFileAccessPolicyUpdateIntervalSec = @"FileAccessPolicyUpdateIntervalSec";
@@ -106,8 +109,7 @@ static NSString *const kEnableForkAndExitLogging = @"EnableForkAndExitLogging";
static NSString *const kIgnoreOtherEndpointSecurityClients = @"IgnoreOtherEndpointSecurityClients";
static NSString *const kEnableDebugLogging = @"EnableDebugLogging";
static NSString *const kEnableBackwardsCompatibleContentEncoding =
@"EnableBackwardsCompatibleContentEncoding";
static NSString *const kClientContentEncoding = @"SyncClientContentEncoding";
static NSString *const kFCMProject = @"FCMProject";
static NSString *const kFCMEntity = @"FCMEntity";
@@ -127,7 +129,6 @@ static NSString *const kBlockedPathRegexKeyDeprecated = @"BlacklistRegex";
static NSString *const kEnableAllEventUploadKey = @"EnableAllEventUpload";
static NSString *const kDisableUnknownEventUploadKey = @"DisableUnknownEventUpload";
// TODO(markowsky): move these to sync server only.
static NSString *const kMetricFormat = @"MetricFormat";
static NSString *const kMetricURL = @"MetricURL";
static NSString *const kMetricExportInterval = @"MetricExportInterval";
@@ -180,6 +181,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kEnablePageZeroProtectionKey : number,
kEnableBadSignatureProtectionKey : number,
kEnableSilentModeKey : number,
kEnableSilentTTYModeKey : number,
kAboutTextKey : string,
kMoreInfoURLKey : string,
kEventDetailURLKey : string,
@@ -197,6 +199,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kClientAuthCertificatePasswordKey : string,
kClientAuthCertificateCNKey : string,
kClientAuthCertificateIssuerKey : string,
kClientContentEncoding : string,
kServerAuthRootsDataKey : data,
kServerAuthRootsFileKey : string,
kMachineOwnerKey : string,
@@ -211,13 +214,13 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kSpoolDirectoryFileSizeThresholdKB : number,
kSpoolDirectorySizeThresholdMB : number,
kSpoolDirectoryEventMaxFlushTimeSec : number,
kFileAccessPolicy : dictionary,
kFileAccessPolicyPlist : string,
kFileAccessPolicyUpdateIntervalSec : number,
kEnableMachineIDDecoration : number,
kEnableForkAndExitLogging : number,
kIgnoreOtherEndpointSecurityClients : number,
kEnableDebugLogging : number,
kEnableBackwardsCompatibleContentEncoding : number,
kFCMProject : string,
kFCMEntity : string,
kFCMAPIKey : string,
@@ -419,6 +422,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicy {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicyPlist {
return [self configStateSet];
}
@@ -455,10 +462,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingEnableBackwardsCompatibleContentEncoding {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingFcmProject {
return [self configStateSet];
}
@@ -639,6 +642,11 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return number ? [number boolValue] : NO;
}
- (BOOL)enableSilentTTYMode {
NSNumber *number = self.configState[kEnableSilentTTYModeKey];
return number ? [number boolValue] : NO;
}
- (NSString *)aboutText {
return self.configState[kAboutTextKey];
}
@@ -702,6 +710,20 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return self.configState[kClientAuthCertificateIssuerKey];
}
- (SNTSyncContentEncoding)syncClientContentEncoding {
NSString *contentEncoding = [self.configState[kClientContentEncoding] lowercaseString];
if ([contentEncoding isEqualToString:@"deflate"]) {
return SNTSyncContentEncodingDeflate;
} else if ([contentEncoding isEqualToString:@"gzip"]) {
return SNTSyncContentEncodingGzip;
} else if ([contentEncoding isEqualToString:@"none"]) {
return SNTSyncContentEncodingNone;
} else {
// Ensure we have the same default zlib behavior Santa's always had otherwise.
return SNTSyncContentEncodingDeflate;
}
}
- (NSData *)syncServerAuthRootsData {
return self.configState[kServerAuthRootsDataKey];
}
@@ -809,8 +831,17 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
: 15.0;
}
- (NSDictionary *)fileAccessPolicy {
return self.configState[kFileAccessPolicy];
}
- (NSString *)fileAccessPolicyPlist {
return self.configState[kFileAccessPolicyPlist];
// This property is ignored when kFileAccessPolicy is set
if (self.configState[kFileAccessPolicy]) {
return nil;
} else {
return self.configState[kFileAccessPolicyPlist];
}
}
- (uint32_t)fileAccessPolicyUpdateIntervalSec {
@@ -866,11 +897,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [number boolValue] || self.debugFlag;
}
- (BOOL)enableBackwardsCompatibleContentEncoding {
NSNumber *number = self.configState[kEnableBackwardsCompatibleContentEncoding];
return number ? [number boolValue] : NO;
}
- (NSString *)fcmProject {
return self.configState[kFCMProject];
}
@@ -1120,6 +1146,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
for (id rule in staticRules) {
if (![rule isKindOfClass:[NSDictionary class]]) return;
SNTRule *r = [[SNTRule alloc] initWithDictionary:rule];
if (!r) continue;
rules[r.identifier] = r;
}
self.cachedStaticRules = [rules copy];

View File

@@ -33,6 +33,15 @@
_type = type;
_customMsg = customMsg;
_timestamp = timestamp;
if (_type == SNTRuleTypeBinary || _type == SNTRuleTypeCertificate) {
NSCharacterSet *nonHex =
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
if ([[_identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length != 64)
return nil;
} else if (_identifier.length == 0) {
return nil;
}
}
return self;
}
@@ -55,52 +64,52 @@
- (instancetype)initWithDictionary:(NSDictionary *)dict {
if (![dict isKindOfClass:[NSDictionary class]]) return nil;
self = [super init];
if (self) {
_identifier = dict[kRuleIdentifier];
if (![_identifier isKindOfClass:[NSString class]] || !_identifier.length) {
_identifier = dict[kRuleSHA256];
}
if (![_identifier isKindOfClass:[NSString class]] || !_identifier.length) return nil;
NSString *policyString = dict[kRulePolicy];
if (![policyString isKindOfClass:[NSString class]]) return nil;
if ([policyString isEqual:kRulePolicyAllowlist] ||
[policyString isEqual:kRulePolicyAllowlistDeprecated]) {
_state = SNTRuleStateAllow;
} else if ([policyString isEqual:kRulePolicyAllowlistCompiler] ||
[policyString isEqual:kRulePolicyAllowlistCompilerDeprecated]) {
_state = SNTRuleStateAllowCompiler;
} else if ([policyString isEqual:kRulePolicyBlocklist] ||
[policyString isEqual:kRulePolicyBlocklistDeprecated]) {
_state = SNTRuleStateBlock;
} else if ([policyString isEqual:kRulePolicySilentBlocklist] ||
[policyString isEqual:kRulePolicySilentBlocklistDeprecated]) {
_state = SNTRuleStateSilentBlock;
} else if ([policyString isEqual:kRulePolicyRemove]) {
_state = SNTRuleStateRemove;
} else {
return nil;
}
NSString *ruleTypeString = dict[kRuleType];
if (![ruleTypeString isKindOfClass:[NSString class]]) return nil;
if ([ruleTypeString isEqual:kRuleTypeBinary]) {
_type = SNTRuleTypeBinary;
} else if ([ruleTypeString isEqual:kRuleTypeCertificate]) {
_type = SNTRuleTypeCertificate;
} else if ([ruleTypeString isEqual:kRuleTypeTeamID]) {
_type = SNTRuleTypeTeamID;
} else {
return nil;
}
NSString *customMsg = dict[kRuleCustomMsg];
if ([customMsg isKindOfClass:[NSString class]] && customMsg.length) {
_customMsg = customMsg;
}
NSString *identifier = dict[kRuleIdentifier];
if (![identifier isKindOfClass:[NSString class]] || !identifier.length) {
identifier = dict[kRuleSHA256];
}
return self;
if (![identifier isKindOfClass:[NSString class]] || !identifier.length) return nil;
NSString *policyString = dict[kRulePolicy];
SNTRuleState state;
if (![policyString isKindOfClass:[NSString class]]) return nil;
if ([policyString isEqual:kRulePolicyAllowlist] ||
[policyString isEqual:kRulePolicyAllowlistDeprecated]) {
state = SNTRuleStateAllow;
} else if ([policyString isEqual:kRulePolicyAllowlistCompiler] ||
[policyString isEqual:kRulePolicyAllowlistCompilerDeprecated]) {
state = SNTRuleStateAllowCompiler;
} else if ([policyString isEqual:kRulePolicyBlocklist] ||
[policyString isEqual:kRulePolicyBlocklistDeprecated]) {
state = SNTRuleStateBlock;
} else if ([policyString isEqual:kRulePolicySilentBlocklist] ||
[policyString isEqual:kRulePolicySilentBlocklistDeprecated]) {
state = SNTRuleStateSilentBlock;
} else if ([policyString isEqual:kRulePolicyRemove]) {
state = SNTRuleStateRemove;
} else {
return nil;
}
NSString *ruleTypeString = dict[kRuleType];
SNTRuleType type;
if (![ruleTypeString isKindOfClass:[NSString class]]) return nil;
if ([ruleTypeString isEqual:kRuleTypeBinary]) {
type = SNTRuleTypeBinary;
} else if ([ruleTypeString isEqual:kRuleTypeCertificate]) {
type = SNTRuleTypeCertificate;
} else if ([ruleTypeString isEqual:kRuleTypeTeamID]) {
type = SNTRuleTypeTeamID;
} else {
return nil;
}
NSString *customMsg = dict[kRuleCustomMsg];
if (![customMsg isKindOfClass:[NSString class]] || customMsg.length == 0) {
customMsg = nil;
}
return [self initWithIdentifier:identifier state:state type:type customMsg:customMsg];
}
#pragma mark NSSecureCoding

View File

@@ -25,22 +25,24 @@
SNTRule *sut;
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"some-sort-of-identifier",
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"BINARY",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(sut.type, SNTRuleTypeBinary);
XCTAssertEqual(sut.state, SNTRuleStateAllow);
sut = [[SNTRule alloc] initWithDictionary:@{
@"sha256" : @"some-sort-of-identifier",
@"sha256" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
@"policy" : @"BLOCKLIST",
@"rule_type" : @"CERTIFICATE",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(sut.type, SNTRuleTypeCertificate);
XCTAssertEqual(sut.state, SNTRuleStateBlock);
@@ -55,12 +57,13 @@
XCTAssertEqual(sut.state, SNTRuleStateSilentBlock);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"some-sort-of-identifier",
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
@"policy" : @"ALLOWLIST_COMPILER",
@"rule_type" : @"BINARY",
}];
XCTAssertNotNil(sut);
XCTAssertEqualObjects(sut.identifier, @"some-sort-of-identifier");
XCTAssertEqualObjects(sut.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(sut.type, SNTRuleTypeBinary);
XCTAssertEqual(sut.state, SNTRuleStateAllowCompiler);
@@ -94,12 +97,19 @@
XCTAssertNil(sut);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"an-identifier",
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
}];
XCTAssertNil(sut);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"an-identifier",
@"policy" : @"ALLOWLIST",
@"rule_type" : @"BINARY",
}];
XCTAssertNil(sut);
sut = [[SNTRule alloc] initWithDictionary:@{
@"identifier" : @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670",
@"policy" : @"OTHERPOLICY",
@"rule_type" : @"BINARY",
}];

View File

@@ -14,7 +14,8 @@
#import <Foundation/Foundation.h>
extern NSString *const kXSRFToken;
extern NSString *const kDefaultXSRFTokenHeader;
extern NSString *const kXSRFTokenHeader;
extern NSString *const kSerialNumber;
extern NSString *const kHostname;

View File

@@ -14,7 +14,8 @@
#import "Source/common/SNTSyncConstants.h"
NSString *const kXSRFToken = @"X-XSRF-TOKEN";
NSString *const kDefaultXSRFTokenHeader = @"X-XSRF-TOKEN";
NSString *const kXSRFTokenHeader = @"X-XSRF-TOKEN-HEADER";
NSString *const kSerialNumber = @"serial_num";
NSString *const kHostname = @"hostname";

35
Source/common/String.h Normal file
View File

@@ -0,0 +1,35 @@
/// 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__STRING_H
#define SANTA__COMMON__STRING_H
#include <Foundation/Foundation.h>
#include <string>
#include <string_view>
namespace santa::common {
static inline std::string_view NSStringToUTF8StringView(NSString *str) {
return std::string_view(str.UTF8String, [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
}
static inline std::string NSStringToUTF8String(NSString *str) {
return std::string(str.UTF8String, [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
}
} // namespace santa::common
#endif

View File

@@ -35,6 +35,9 @@ uint64_t MachTimeToNanos(uint64_t mach_time);
// Convert nanoseconds to mach absolute time
uint64_t NanosToMachTime(uint64_t nanos);
// Add some number of nanoseconds to a given mach time and return the new result
uint64_t AddNanosecondsToMachTime(uint64_t ns, uint64_t machTime);
// Get the result of proc_pidinfo with the PROC_PIDTASKINFO flavor
std::optional<SantaTaskInfo> GetTaskInfo();

View File

@@ -39,17 +39,28 @@ static mach_timebase_info_data_t GetTimebase() {
}
uint64_t MachTimeToNanos(uint64_t mach_time) {
mach_timebase_info_data_t timebase = GetTimebase();
static mach_timebase_info_data_t timebase = GetTimebase();
return mach_time * timebase.numer / timebase.denom;
}
uint64_t NanosToMachTime(uint64_t nanos) {
mach_timebase_info_data_t timebase = GetTimebase();
static mach_timebase_info_data_t timebase = GetTimebase();
return nanos * timebase.denom / timebase.numer;
}
uint64_t AddNanosecondsToMachTime(uint64_t ns, uint64_t machTime) {
// Convert machtime to nanoseconds
uint64_t nanoTime = MachTimeToNanos(machTime);
// Add the nanosecond offset
nanoTime += ns;
// Convert back to machTime
return NanosToMachTime(nanoTime);
}
std::optional<SantaTaskInfo> GetTaskInfo() {
struct proc_taskinfo pti;

View File

@@ -87,16 +87,6 @@ es_process_t MakeESProcess(es_file_t *file, audit_token_t tok, audit_token_t par
};
}
static uint64_t AddMillisToMachTime(uint64_t ms, uint64_t machTime) {
uint64_t nanoTime = MachTimeToNanos(machTime);
// Add the ms offset
nanoTime += (ms * NSEC_PER_MSEC);
// Convert back to machTime
return NanosToMachTime(nanoTime);
}
uint32_t MaxSupportedESMessageVersionForCurrentOS() {
// Note: ES message v3 was only in betas.
if (@available(macOS 13.0, *)) {
@@ -115,7 +105,7 @@ uint32_t MaxSupportedESMessageVersionForCurrentOS() {
es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc, ActionType action_type,
uint64_t future_deadline_ms) {
es_message_t es_msg = {
.deadline = AddMillisToMachTime(future_deadline_ms, mach_absolute_time()),
.deadline = AddNanosecondsToMachTime(future_deadline_ms * NSEC_PER_MSEC, mach_absolute_time()),
.process = proc,
.action_type =
(action_type == ActionType::Notify) ? ES_ACTION_TYPE_NOTIFY : ES_ACTION_TYPE_AUTH,

View File

@@ -231,10 +231,10 @@ message Execution {
optional FileInfo working_directory = 4;
// List of process arguments
repeated string args = 5;
repeated bytes args = 5;
// List of environment variables
repeated string envs = 6;
repeated bytes envs = 6;
// List of file descriptors
repeated FileDescriptor fds = 7;

View File

@@ -37,7 +37,7 @@ struct SNTDeviceMessageWindowView: View {
Text("Device Name").bold()
Text("Device BSD Path").bold()
if event!.remountArgs.count > 0 {
if event!.remountArgs?.count ?? 0 > 0 {
Text("Remount Mode").bold()
}
}
@@ -46,7 +46,7 @@ struct SNTDeviceMessageWindowView: View {
Text(event!.mntonname)
Text(event!.mntfromname)
if event!.remountArgs.count > 0 {
if event!.remountArgs?.count ?? 0 > 0 {
Text(event!.readableRemountArgs())
}
}

View File

@@ -37,14 +37,14 @@ objc_library(
objc_library(
name = "santactl_lib",
srcs = [
"main.m",
"Commands/SNTCommandFileInfo.m",
"Commands/SNTCommandRule.m",
"Commands/SNTCommandStatus.m",
"Commands/SNTCommandVersion.m",
"Commands/SNTCommandMetrics.h",
"Commands/SNTCommandMetrics.m",
"Commands/SNTCommandRule.m",
"Commands/SNTCommandStatus.m",
"Commands/SNTCommandSync.m",
"Commands/SNTCommandVersion.m",
"main.m",
] + select({
"//:opt_build": [],
"//conditions:default": [

View File

@@ -131,9 +131,6 @@ REGISTER_COMMAND_NAME(@"rule")
[self printErrorUsageAndExit:@"--sha256 requires an argument"];
}
newRule.identifier = arguments[i];
if (newRule.identifier.length != 64) {
[self printErrorUsageAndExit:@"--sha256 requires a valid SHA-256 as the argument"];
}
} else if ([arg caseInsensitiveCompare:@"--message"] == NSOrderedSame) {
if (++i > arguments.count - 1) {
[self printErrorUsageAndExit:@"--message requires an argument"];
@@ -148,6 +145,15 @@ REGISTER_COMMAND_NAME(@"rule")
}
}
if (newRule.type == SNTRuleTypeBinary || newRule.type == SNTRuleTypeCertificate) {
NSCharacterSet *nonHex =
[[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet];
if ([[newRule.identifier uppercaseString] stringByTrimmingCharactersInSet:nonHex].length !=
64) {
[self printErrorUsageAndExit:@"BINARY or CERTIFICATE rules require a valid SHA-256"];
}
}
if (check) {
if (!newRule.identifier) return [self printErrorUsageAndExit:@"--check requires --identifier"];
return [self printStateOfRule:newRule daemonConnection:self.daemonConn];

View File

@@ -217,7 +217,7 @@ REGISTER_COMMAND_NAME(@"status")
@"enabled" : @(watchItemsEnabled),
@"rule_count" : @(watchItemsRuleCount),
@"policy_version" : watchItemsPolicyVersion,
@"config_path" : watchItemsConfigPath,
@"config_path" : watchItemsConfigPath ?: @"null",
@"last_policy_update" : watchItemsLastUpdateStr ?: @"null",
};
} else {
@@ -272,7 +272,7 @@ REGISTER_COMMAND_NAME(@"status")
if (watchItemsEnabled) {
printf(" %-25s | %s\n", "Policy Version", watchItemsPolicyVersion.UTF8String);
printf(" %-25s | %llu\n", "Rule Count", watchItemsRuleCount);
printf(" %-25s | %s\n", "Config Path", watchItemsConfigPath.UTF8String);
printf(" %-25s | %s\n", "Config Path", (watchItemsConfigPath ?: @"(embedded)").UTF8String);
printf(" %-25s | %s\n", "Last Policy Update", watchItemsLastUpdateStr.UTF8String);
}

View File

@@ -49,6 +49,7 @@ objc_library(
":WatchItemPolicy",
"//Source/common:PrefixTree",
"//Source/common:SNTLogging",
"//Source/common:String",
"//Source/common:Unit",
],
)
@@ -232,6 +233,7 @@ objc_library(
"//Source/common:SNTRule",
"//Source/common:SNTStoredEvent",
"//Source/common:SantaVnode",
"//Source/common:String",
"@MOLCodesignChecker",
],
)
@@ -337,6 +339,7 @@ objc_library(
":EndpointSecurityLogger",
":EndpointSecurityMessage",
":Metrics",
":RateLimiter",
":SNTDecisionCache",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
@@ -346,6 +349,7 @@ objc_library(
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTMetricSet",
"//Source/common:SNTStrengthify",
"//Source/common:SantaCache",
"//Source/common:SantaVnode",
"//Source/common:SantaVnodeHash",
@@ -380,12 +384,24 @@ objc_library(
":EndpointSecurityClient",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
"//Source/common:SantaCache",
"//Source/common:SantaVnode",
"//Source/common:SantaVnodeHash",
],
)
objc_library(
name = "RateLimiter",
srcs = ["EventProviders/RateLimiter.mm"],
hdrs = ["EventProviders/RateLimiter.h"],
deps = [
":Metrics",
"//Source/common:BranchPrediction",
"//Source/common:SystemResources",
],
)
objc_library(
name = "EndpointSecurityEnricher",
srcs = ["EventProviders/EndpointSecurity/Enricher.mm"],
@@ -455,6 +471,7 @@ objc_library(
],
deps = [
":EndpointSecurityMessage",
"//Source/common:String",
],
)
@@ -488,6 +505,7 @@ objc_library(
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:SNTStoredEvent",
"//Source/common:String",
"//Source/common:santa_cc_proto_library_wrapper",
],
)
@@ -962,6 +980,16 @@ santa_unit_test(
],
)
santa_unit_test(
name = "RateLimiterTest",
srcs = ["EventProviders/RateLimiterTest.mm"],
deps = [
":Metrics",
":RateLimiter",
"//Source/common:SystemResources",
],
)
santa_unit_test(
name = "EndpointSecuritySerializerEmptyTest",
srcs = ["Logs/EndpointSecurity/Serializers/EmptyTest.mm"],
@@ -1294,6 +1322,7 @@ test_suite(
":EndpointSecurityWriterFileTest",
":EndpointSecurityWriterSpoolTest",
":MetricsTest",
":RateLimiterTest",
":SNTApplicationCoreMetricsTest",
":SNTCompilerControllerTest",
":SNTDecisionCacheTest",

View File

@@ -45,7 +45,7 @@
- (SNTRule *)_exampleBinaryRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"a";
r.identifier = @"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670";
r.state = SNTRuleStateBlock;
r.type = SNTRuleTypeBinary;
r.customMsg = @"A rule";
@@ -54,7 +54,7 @@
- (SNTRule *)_exampleCertRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"b";
r.identifier = @"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258";
r.state = SNTRuleStateAllow;
r.type = SNTRuleTypeCertificate;
return r;
@@ -112,7 +112,7 @@
- (void)testAddInvalidRule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = @"a";
r.identifier = @"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258";
r.type = SNTRuleTypeCertificate;
NSError *error;
@@ -125,12 +125,19 @@
cleanSlate:NO
error:nil];
SNTRule *r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:nil teamID:nil];
SNTRule *r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
certificateSHA256:nil
teamID:nil];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"a");
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(r.type, SNTRuleTypeBinary);
r = [self.sut ruleForBinarySHA256:@"b" certificateSHA256:nil teamID:nil];
r = [self.sut
ruleForBinarySHA256:@"b6ee1c3c5a715c049d14a8457faa6b6701b8507efe908300e238e0768bd759c2"
certificateSHA256:nil
teamID:nil];
XCTAssertNil(r);
}
@@ -139,12 +146,19 @@
cleanSlate:NO
error:nil];
SNTRule *r = [self.sut ruleForBinarySHA256:nil certificateSHA256:@"b" teamID:nil];
SNTRule *r = [self.sut
ruleForBinarySHA256:nil
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:nil];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"b");
XCTAssertEqualObjects(r.identifier,
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
XCTAssertEqual(r.type, SNTRuleTypeCertificate);
r = [self.sut ruleForBinarySHA256:nil certificateSHA256:@"a" teamID:nil];
r = [self.sut
ruleForBinarySHA256:nil
certificateSHA256:@"5bdab1288fc16892fef50c658db54f1e2e19cf8f71cc55f77de2b95e051e2562"
teamID:nil];
XCTAssertNil(r);
}
@@ -171,19 +185,31 @@
// This test verifies that the implicit rule ordering we've been abusing is still working.
// See the comment in SNTRuleTable#ruleForBinarySHA256:certificateSHA256:teamID
SNTRule *r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:@"b" teamID:@"teamID"];
SNTRule *r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:@"teamID"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"a");
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
r = [self.sut ruleForBinarySHA256:@"a" certificateSHA256:@"unknowncert" teamID:@"teamID"];
r = [self.sut
ruleForBinarySHA256:@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670"
certificateSHA256:@"unknowncert"
teamID:@"teamID"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"a");
XCTAssertEqualObjects(r.identifier,
@"b7c1e3fd640c5f211c89b02c2c6122f78ce322aa5c56eb0bb54bc422a8f8b670");
XCTAssertEqual(r.type, SNTRuleTypeBinary, @"Implicit rule ordering failed");
r = [self.sut ruleForBinarySHA256:@"unknown" certificateSHA256:@"b" teamID:@"teamID"];
r = [self.sut
ruleForBinarySHA256:@"unknown"
certificateSHA256:@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258"
teamID:@"teamID"];
XCTAssertNotNil(r);
XCTAssertEqualObjects(r.identifier, @"b");
XCTAssertEqualObjects(r.identifier,
@"7ae80b9ab38af0c63a9a81765f434d9a7cd8f720eb6037ef303de39d779bc258");
XCTAssertEqual(r.type, SNTRuleTypeCertificate, @"Implicit rule ordering failed");
}

View File

@@ -70,9 +70,15 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
// Factory
static std::shared_ptr<WatchItems> Create(NSString *config_path,
uint64_t reapply_config_frequency_secs);
// Factory
static std::shared_ptr<WatchItems> Create(NSDictionary *config,
uint64_t reapply_config_frequency_secs);
WatchItems(NSString *config_path_, dispatch_queue_t q, dispatch_source_t timer_source,
WatchItems(NSString *config_path, dispatch_queue_t q, dispatch_source_t timer_source,
void (^periodic_task_complete_f)(void) = nullptr);
WatchItems(NSDictionary *config, dispatch_queue_t q, dispatch_source_t timer_source,
void (^periodic_task_complete_f)(void) = nullptr);
~WatchItems();
void BeginPeriodicTask();
@@ -80,6 +86,8 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
void RegisterClient(id<SNTEndpointSecurityDynamicEventHandler> client);
void SetConfigPath(NSString *config_path);
void SetConfig(NSDictionary *config);
VersionAndPolicies FindPolciesForPaths(const std::vector<std::string_view> &paths);
std::optional<WatchItemsState> State();
@@ -87,6 +95,9 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
friend class santa::santad::data_layer::WatchItemsPeer;
private:
static std::shared_ptr<WatchItems> CreateInternal(NSString *config_path, NSDictionary *config,
uint64_t reapply_config_frequency_secs);
NSDictionary *ReadConfig();
NSDictionary *ReadConfigLocked() ABSL_SHARED_LOCKS_REQUIRED(lock_);
void ReloadConfig(NSDictionary *new_config);
@@ -98,6 +109,7 @@ class WatchItems : public std::enable_shared_from_this<WatchItems> {
std::set<std::pair<std::string, WatchItemPathType>> &paths);
NSString *config_path_;
NSDictionary *embedded_config_;
dispatch_queue_t q_;
dispatch_source_t timer_source_;
void (^periodic_task_complete_f_)(void);

View File

@@ -34,8 +34,12 @@
#import "Source/common/PrefixTree.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/String.h"
#import "Source/common/Unit.h"
#include "Source/santad/DataLayer/WatchItemPolicy.h"
using santa::common::NSStringToUTF8String;
using santa::common::NSStringToUTF8StringView;
using santa::common::PrefixTree;
using santa::common::Unit;
using santa::santad::data_layer::WatchItemPathType;
@@ -255,7 +259,7 @@ std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSEr
return Unit{};
}
path_list.push_back({std::string(path_str.UTF8String, path_str.length), path_type});
path_list.push_back({NSStringToUTF8String(path_str), path_type});
} else if ([path isKindOfClass:[NSString class]]) {
if (!LenRangeValidator(1, PATH_MAX)(path, err)) {
PopulateError(err, [NSString stringWithFormat:@"Invalid path length: %@",
@@ -264,8 +268,8 @@ std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSEr
return Unit{};
}
path_list.push_back({std::string(((NSString *)path).UTF8String, ((NSString *)path).length),
kWatchItemPolicyDefaultPathType});
path_list.push_back(
{NSStringToUTF8String(((NSString *)path)), kWatchItemPolicyDefaultPathType});
} else {
PopulateError(
err, [NSString stringWithFormat:
@@ -340,12 +344,11 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
}
proc_list.push_back(WatchItemPolicy::Process(
std::string([(process[kWatchItemConfigKeyProcessesBinaryPath] ?: @"") UTF8String]),
std::string([(process[kWatchItemConfigKeyProcessesSigningID] ?: @"") UTF8String]),
std::string([(process[kWatchItemConfigKeyProcessesTeamID] ?: @"") UTF8String]),
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesBinaryPath] ?: @""),
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesSigningID] ?: @""),
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesTeamID] ?: @""),
HexStringToBytes(process[kWatchItemConfigKeyProcessesCDHash]),
std::string(
[(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @"") UTF8String]),
NSStringToUTF8String(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @""),
process[kWatchItemConfigKeyProcessesPlatformBinary]
? std::make_optional(
(bool)[process[kWatchItemConfigKeyProcessesPlatformBinary] boolValue])
@@ -423,8 +426,8 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
for (const PathAndTypePair &path_type_pair : std::get<PathList>(path_list)) {
policies.push_back(std::make_shared<WatchItemPolicy>(
[name UTF8String], path_type_pair.first, path_type_pair.second, allow_read_access, audit_only,
std::get<ProcessList>(proc_list)));
NSStringToUTF8StringView(name), path_type_pair.first, path_type_pair.second,
allow_read_access, audit_only, std::get<ProcessList>(proc_list)));
}
return true;
@@ -520,24 +523,53 @@ bool ParseConfig(NSDictionary *config, std::vector<std::shared_ptr<WatchItemPoli
std::shared_ptr<WatchItems> WatchItems::Create(NSString *config_path,
uint64_t reapply_config_frequency_secs) {
return CreateInternal(config_path, nil, reapply_config_frequency_secs);
}
std::shared_ptr<WatchItems> WatchItems::Create(NSDictionary *config,
uint64_t reapply_config_frequency_secs) {
return CreateInternal(nil, config, reapply_config_frequency_secs);
}
std::shared_ptr<WatchItems> WatchItems::CreateInternal(NSString *config_path, NSDictionary *config,
uint64_t reapply_config_frequency_secs) {
if (reapply_config_frequency_secs < kMinReapplyConfigFrequencySecs) {
LOGW(@"Invalid watch item update interval provided: %llu. Min allowed: %llu",
reapply_config_frequency_secs, kMinReapplyConfigFrequencySecs);
return nullptr;
}
if (config_path && config) {
LOGW(@"Invalid arguments creating WatchItems - both config and config_path cannot be set.");
return nullptr;
}
dispatch_queue_t q = dispatch_queue_create("com.google.santa.daemon.watch_items.q",
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
dispatch_source_t timer_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
dispatch_source_set_timer(timer_source, dispatch_time(DISPATCH_TIME_NOW, 0),
NSEC_PER_SEC * reapply_config_frequency_secs, 0);
return std::make_shared<WatchItems>(config_path, q, timer_source);
if (config_path) {
return std::make_shared<WatchItems>(config_path, q, timer_source);
} else {
return std::make_shared<WatchItems>(config, q, timer_source);
}
}
WatchItems::WatchItems(NSString *config_path, dispatch_queue_t q, dispatch_source_t timer_source,
void (^periodic_task_complete_f)(void))
: config_path_(config_path),
embedded_config_(nil),
q_(q),
timer_source_(timer_source),
periodic_task_complete_f_(periodic_task_complete_f),
watch_items_(std::make_unique<WatchItemsTree>()) {}
WatchItems::WatchItems(NSDictionary *config, dispatch_queue_t q, dispatch_source_t timer_source,
void (^periodic_task_complete_f)(void))
: config_path_(nil),
embedded_config_(config),
q_(q),
timer_source_(timer_source),
periodic_task_complete_f_(periodic_task_complete_f),
@@ -616,7 +648,7 @@ void WatchItems::UpdateCurrentState(
std::swap(currently_monitored_paths_, new_monitored_paths);
current_config_ = new_config;
if (new_config) {
policy_version_ = [new_config[kWatchItemConfigKeyVersion] UTF8String];
policy_version_ = NSStringToUTF8String(new_config[kWatchItemConfigKeyVersion]);
} else {
policy_version_ = "";
}
@@ -688,7 +720,7 @@ void WatchItems::BeginPeriodicTask() {
return;
}
shared_watcher->ReloadConfig(shared_watcher->ReadConfig());
shared_watcher->ReloadConfig(embedded_config_ ?: shared_watcher->ReadConfig());
if (shared_watcher->periodic_task_complete_f_) {
shared_watcher->periodic_task_complete_f_();
@@ -718,11 +750,21 @@ void WatchItems::SetConfigPath(NSString *config_path) {
{
absl::MutexLock lock(&lock_);
config_path_ = config_path;
embedded_config_ = nil;
config = ReadConfigLocked();
}
ReloadConfig(config);
}
void WatchItems::SetConfig(NSDictionary *config) {
{
absl::MutexLock lock(&lock_);
config_path_ = nil;
embedded_config_ = config;
}
ReloadConfig(embedded_config_);
}
std::optional<WatchItemsState> WatchItems::State() {
absl::ReaderMutexLock lock(&lock_);

View File

@@ -61,8 +61,14 @@ extern std::variant<Unit, santatest::ProcessList> VerifyConfigWatchItemProcesses
NSDictionary *watch_item, NSError **err);
class WatchItemsPeer : public WatchItems {
public:
using WatchItems::ReloadConfig;
using WatchItems::WatchItems;
using WatchItems::ReloadConfig;
using WatchItems::SetConfig;
using WatchItems::SetConfigPath;
using WatchItems::config_path_;
using WatchItems::embedded_config_;
};
} // namespace santa::santad::data_layer
@@ -191,7 +197,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
// Changes in config dictionary will update policy info even if the
// filesystem didn't change.
{
WatchItemsPeer watchItems(nil, NULL, NULL);
WatchItemsPeer watchItems((NSString *)nil, NULL, NULL);
[self pushd:@"a"];
watchItems.ReloadConfig(configAllFilesOriginal);
@@ -212,7 +218,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
// Changes to fileystem structure are reflected when a config is reloaded
{
WatchItemsPeer watchItems(nil, NULL, NULL);
WatchItemsPeer watchItems((NSString *)nil, NULL, NULL);
[self pushd:@"a"];
watchItems.ReloadConfig(configAllFilesOriginal);
[self popd];
@@ -313,7 +319,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
}
});
WatchItemsPeer watchItems(nil, NULL, NULL);
WatchItemsPeer watchItems((NSString *)nil, NULL, NULL);
WatchItems::VersionAndPolicies policies;
// Resultant vector is same size as input vector
@@ -842,4 +848,23 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
XCTAssertGreaterThanOrEqual(state.last_config_load_epoch, startTime);
}
- (void)testSetConfigAndSetConfigPath {
// Test internal state when switching back and forth between path-based and
// dictionary-based config options.
WatchItemsPeer watchItems(@{}, NULL, NULL);
XCTAssertNil(watchItems.config_path_);
XCTAssertNotNil(watchItems.embedded_config_);
watchItems.SetConfigPath(@"/path/to/a/nonexistent/file/so/nothing/is/opened");
XCTAssertNotNil(watchItems.config_path_);
XCTAssertNil(watchItems.embedded_config_);
watchItems.SetConfig(@{});
XCTAssertNil(watchItems.config_path_);
XCTAssertNotNil(watchItems.embedded_config_);
}
@end

View File

@@ -22,6 +22,7 @@
#include <memory>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTMetricSet.h"
#include "Source/common/SantaCache.h"
#import "Source/common/SantaVnode.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
@@ -33,16 +34,29 @@ enum class FlushCacheMode {
kAllCaches,
};
enum class FlushCacheReason {
kClientModeChanged,
kPathRegexChanged,
kRulesChanged,
kStaticRulesChanged,
kExplicitCommand,
kFilesystemUnmounted,
};
class AuthResultCache {
public:
// Santa currently only flushes caches when new DENY rules are added, not
// ALLOW rules. This means this value should be low enough so that if a
// ALLOW rules. This means cache_deny_time_ms should be low enough so that if a
// previously denied binary is allowed, it can be re-executed by the user in a
// timely manner. But the value should be high enough to allow the cache to be
// effective in the event the binary is executed in rapid succession.
static std::unique_ptr<AuthResultCache> Create(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
SNTMetricSet *metric_set, uint64_t cache_deny_time_ms = 1500);
AuthResultCache(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
uint64_t cache_deny_time_ms = 1500);
SNTMetricCounter *flush_count, uint64_t cache_deny_time_ms = 1500);
virtual ~AuthResultCache();
AuthResultCache(AuthResultCache &&other) = delete;
@@ -55,7 +69,7 @@ class AuthResultCache {
virtual SNTAction CheckCache(const es_file_t *es_file);
virtual SNTAction CheckCache(SantaVnode vnode_id);
virtual void FlushCache(FlushCacheMode mode);
virtual void FlushCache(FlushCacheMode mode, FlushCacheReason reason);
virtual NSArray<NSNumber *> *CacheCounts();
@@ -66,6 +80,7 @@ class AuthResultCache {
SantaCache<SantaVnode, uint64_t> *nonroot_cache_;
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
SNTMetricCounter *flush_count_;
uint64_t root_devno_;
uint64_t cache_deny_time_ns_;
dispatch_queue_t q_;

View File

@@ -25,6 +25,13 @@
using santa::santad::event_providers::endpoint_security::Client;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
static NSString *const kFlushCacheReasonClientModeChanged = @"ClientModeChanged";
static NSString *const kFlushCacheReasonPathRegexChanged = @"PathRegexChanged";
static NSString *const kFlushCacheReasonRulesChanged = @"RulesChanged";
static NSString *const kFlushCacheReasonStaticRulesChanged = @"StaticRulesChanged";
static NSString *const kFlushCacheReasonExplicitCommand = @"ExplicitCommand";
static NSString *const kFlushCacheReasonFilesystemUnmounted = @"FilesystemUnmounted";
namespace santa::santad::event_providers {
static inline uint64_t GetCurrentUptime() {
@@ -44,9 +51,36 @@ static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
return (cachedValue & ~(0xFF00000000000000));
}
NSString *const FlushCacheReasonToString(FlushCacheReason reason) {
switch (reason) {
case FlushCacheReason::kClientModeChanged: return kFlushCacheReasonClientModeChanged;
case FlushCacheReason::kPathRegexChanged: return kFlushCacheReasonPathRegexChanged;
case FlushCacheReason::kRulesChanged: return kFlushCacheReasonRulesChanged;
case FlushCacheReason::kStaticRulesChanged: return kFlushCacheReasonStaticRulesChanged;
case FlushCacheReason::kExplicitCommand: return kFlushCacheReasonExplicitCommand;
case FlushCacheReason::kFilesystemUnmounted: return kFlushCacheReasonFilesystemUnmounted;
default:
[NSException raise:@"Invalid reason" format:@"Unknown reason value: %d", reason];
return nil;
}
}
std::unique_ptr<AuthResultCache> AuthResultCache::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
SNTMetricSet *metric_set,
uint64_t cache_deny_time_ms) {
SNTMetricCounter *flush_count =
[metric_set counterWithName:@"/santa/flush_count"
fieldNames:@[ @"Reason" ]
helpText:@"Count of times the auth result cache is flushed by reason"];
return std::make_unique<AuthResultCache>(esapi, flush_count, cache_deny_time_ms);
}
AuthResultCache::AuthResultCache(std::shared_ptr<EndpointSecurityAPI> esapi,
uint64_t cache_deny_time_ms)
: esapi_(esapi), cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
SNTMetricCounter *flush_count, uint64_t cache_deny_time_ms)
: esapi_(esapi),
flush_count_(flush_count),
cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
root_cache_ = new SantaCache<SantaVnode, uint64_t>();
nonroot_cache_ = new SantaCache<SantaVnode, uint64_t>();
@@ -118,7 +152,7 @@ SantaCache<SantaVnode, uint64_t> *AuthResultCache::CacheForVnodeID(SantaVnode vn
return (vnode_id.fsid == root_devno_ || root_devno_ == 0) ? root_cache_ : nonroot_cache_;
}
void AuthResultCache::FlushCache(FlushCacheMode mode) {
void AuthResultCache::FlushCache(FlushCacheMode mode, FlushCacheReason reason) {
nonroot_cache_->clear();
if (mode == FlushCacheMode::kAllCaches) {
root_cache_->clear();
@@ -134,6 +168,8 @@ void AuthResultCache::FlushCache(FlushCacheMode mode) {
shared_esapi->ClearCache(Client());
});
}
[flush_count_ incrementForFieldValues:@[ FlushCacheReasonToString(reason) ]];
}
NSArray<NSNumber *> *AuthResultCache::CacheCounts() {

View File

@@ -29,6 +29,13 @@
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::FlushCacheMode;
using santa::santad::event_providers::FlushCacheReason;
namespace santa::santad::event_providers {
extern NSString *const FlushCacheReasonToString(FlushCacheReason reason);
}
using santa::santad::event_providers::FlushCacheReasonToString;
// Grab the st_dev number of the root volume to match the root cache
static uint64_t RootDevno() {
@@ -66,14 +73,14 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
- (void)testEmptyCacheExpectedNumberOfCacheCounts {
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
auto cache = std::make_shared<AuthResultCache>(esapi);
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(esapi, nil);
AssertCacheCounts(cache, 0, 0);
}
- (void)testBasicOperation {
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
auto cache = std::make_shared<AuthResultCache>(esapi);
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(esapi, nil);
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 222);
@@ -110,7 +117,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
- (void)testFlushCache {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
auto cache = std::make_shared<AuthResultCache>(mockESApi);
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil);
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 111);
@@ -121,7 +128,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
AssertCacheCounts(cache, 1, 1);
// Flush non-root only
cache->FlushCache(FlushCacheMode::kNonRootOnly);
cache->FlushCache(FlushCacheMode::kNonRootOnly, FlushCacheReason::kClientModeChanged);
AssertCacheCounts(cache, 1, 0);
@@ -138,7 +145,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
dispatch_semaphore_signal(sema);
return true;
}));
cache->FlushCache(FlushCacheMode::kAllCaches);
cache->FlushCache(FlushCacheMode::kAllCaches, FlushCacheReason::kClientModeChanged);
XCTAssertEqual(0,
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
@@ -151,7 +158,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
- (void)testCacheStateMachine {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
auto cache = std::make_shared<AuthResultCache>(mockESApi);
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil);
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
@@ -193,7 +200,7 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
// Create a cache with a lowered cache expiry value
uint64_t expiryMS = 250;
auto cache = std::make_shared<AuthResultCache>(mockESApi, expiryMS);
std::shared_ptr<AuthResultCache> cache = AuthResultCache::Create(mockESApi, nil, expiryMS);
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
@@ -215,4 +222,21 @@ static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uin
AssertCacheCounts(cache, 0, 0);
}
- (void)testFlushCacheReasonToString {
std::map<FlushCacheReason, NSString *> reasonToString = {
{FlushCacheReason::kClientModeChanged, @"ClientModeChanged"},
{FlushCacheReason::kPathRegexChanged, @"PathRegexChanged"},
{FlushCacheReason::kRulesChanged, @"RulesChanged"},
{FlushCacheReason::kStaticRulesChanged, @"StaticRulesChanged"},
{FlushCacheReason::kExplicitCommand, @"ExplicitCommand"},
{FlushCacheReason::kFilesystemUnmounted, @"FilesystemUnmounted"},
};
for (const auto &kv : reasonToString) {
XCTAssertEqualObjects(FlushCacheReasonToString(kv.first), kv.second);
}
XCTAssertThrows(FlushCacheReasonToString((FlushCacheReason)12345));
}
@end

View File

@@ -0,0 +1,77 @@
/// Copyright 2022 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__EVENTPROVIDERS_RATELIMITER_H
#define SANTA__SANTAD__EVENTPROVIDERS_RATELIMITER_H
#import <Foundation/Foundation.h>
#include <atomic>
#include <memory>
#include "Source/santad/Metrics.h"
// Forward declarations
namespace santa::santad::event_providers {
class RateLimiterPeer;
}
namespace santa::santad::event_providers {
// Very basic rate limiting infrastructure.
// Currently only handles X events per duration.
//
// TODO(mlw): Support changing QPS via config
// TODO(mlw): Support per-rule QPS
// TODO(mlw): Consider adding sliding window support
class RateLimiter {
public:
// Factory
static std::shared_ptr<RateLimiter> Create(
std::shared_ptr<santa::santad::Metrics> metrics,
santa::santad::Processor processor, uint16_t max_qps,
NSTimeInterval reset_duration = kDefaultResetDuration);
RateLimiter(std::shared_ptr<santa::santad::Metrics> metrics,
santa::santad::Processor processor, uint16_t max_qps,
NSTimeInterval reset_duration);
enum class Decision {
kRateLimited = 0,
kAllowed,
};
Decision Decide(uint64_t cur_mach_time);
friend class santa::santad::event_providers::RateLimiterPeer;
private:
bool ShouldRateLimitLocked();
size_t EventsRateLimitedLocked();
void TryResetLocked(uint64_t cur_mach_time);
static constexpr NSTimeInterval kDefaultResetDuration = 15.0;
std::shared_ptr<santa::santad::Metrics> metrics_;
santa::santad::Processor processor_;
size_t log_count_total_ = 0;
size_t max_log_count_total_;
uint64_t reset_mach_time_;
uint64_t reset_duration_ns_;
dispatch_queue_t q_;
};
} // namespace santa::santad::event_providers
#endif

View File

@@ -0,0 +1,85 @@
/// Copyright 2022 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/EventProviders/RateLimiter.h"
#include "Source/common/BranchPrediction.h"
#include "Source/common/SystemResources.h"
using santa::santad::Metrics;
using santa::santad::Processor;
namespace santa::santad::event_providers {
std::shared_ptr<RateLimiter> RateLimiter::Create(std::shared_ptr<Metrics> metrics,
Processor processor, uint16_t max_qps,
NSTimeInterval reset_duration) {
return std::make_shared<RateLimiter>(std::move(metrics), processor, max_qps, reset_duration);
}
RateLimiter::RateLimiter(std::shared_ptr<Metrics> metrics, Processor processor, uint16_t max_qps,
NSTimeInterval reset_duration)
: metrics_(std::move(metrics)),
processor_(processor),
max_log_count_total_(reset_duration * max_qps),
reset_mach_time_(0),
reset_duration_ns_(reset_duration * NSEC_PER_SEC) {
q_ = dispatch_queue_create(
"com.google.santa.daemon.rate_limiter",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
QOS_CLASS_USER_INTERACTIVE, 0));
}
bool RateLimiter::ShouldRateLimitLocked() {
return log_count_total_ > max_log_count_total_;
}
size_t RateLimiter::EventsRateLimitedLocked() {
if (unlikely(ShouldRateLimitLocked())) {
return log_count_total_ - max_log_count_total_;
} else {
return 0;
}
}
void RateLimiter::TryResetLocked(uint64_t cur_mach_time) {
if (cur_mach_time > reset_mach_time_) {
if (metrics_) {
metrics_->SetRateLimitingMetrics(processor_, EventsRateLimitedLocked());
}
log_count_total_ = 0;
reset_mach_time_ = AddNanosecondsToMachTime(reset_duration_ns_, cur_mach_time);
}
}
RateLimiter::Decision RateLimiter::Decide(uint64_t cur_mach_time) {
__block RateLimiter::Decision decision;
dispatch_sync(q_, ^{
TryResetLocked(cur_mach_time);
++log_count_total_;
if (unlikely(ShouldRateLimitLocked())) {
decision = Decision::kRateLimited;
} else {
decision = RateLimiter::Decision::kAllowed;
}
});
return decision;
}
} // namespace santa::santad::event_providers

View File

@@ -0,0 +1,149 @@
/// Copyright 2022 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/EventProviders/RateLimiter.h"
#include <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
#include "Source/common/SystemResources.h"
#include "Source/santad/Metrics.h"
using santa::santad::event_providers::RateLimiter;
static const santa::santad::Processor kDefaultProcessor =
santa::santad::Processor::kFileAccessAuthorizer;
namespace santa::santad::event_providers {
class RateLimiterPeer : public RateLimiter {
public:
using RateLimiter::RateLimiter;
using RateLimiter::EventsRateLimitedLocked;
using RateLimiter::ShouldRateLimitLocked;
using RateLimiter::TryResetLocked;
using RateLimiter::log_count_total_;
using RateLimiter::reset_mach_time_;
};
} // namespace santa::santad::event_providers
using santa::santad::event_providers::RateLimiterPeer;
@interface RateLimiterTest : XCTestCase
@end
@implementation RateLimiterTest
- (void)testTryResetLocked {
// Create an object supporting 1 QPS, and a reset duration of 2s
uint16_t maxQps = 1;
NSTimeInterval resetDuration = 2;
RateLimiterPeer rlp(nullptr, kDefaultProcessor, maxQps, resetDuration);
// Check the current reset_mach_time_ is 0 so that it gets
// set when the first decision is made
XCTAssertEqual(rlp.reset_mach_time_, 0);
// Define our current mach time and create the expected new reset duration floor
uint64_t curMachTime = 1;
uint64_t expectedMachTime = AddNanosecondsToMachTime(resetDuration, curMachTime);
// Set a higher log count to ensure it is reset
rlp.log_count_total_ = 123;
rlp.TryResetLocked(curMachTime);
// Ensure values are reset appropriately
XCTAssertEqual(rlp.log_count_total_, 0);
XCTAssertGreaterThanOrEqual(rlp.reset_mach_time_, expectedMachTime);
// Setup values so that calling TryResetLocked shouldn't reset anything
size_t expectedLogCount = 123;
expectedMachTime = 456;
rlp.log_count_total_ = expectedLogCount;
rlp.reset_mach_time_ = expectedMachTime;
curMachTime = rlp.reset_mach_time_;
rlp.TryResetLocked(curMachTime);
// Ensure the values were not changed
XCTAssertEqual(rlp.log_count_total_, expectedLogCount);
XCTAssertGreaterThanOrEqual(rlp.reset_mach_time_, expectedMachTime);
}
- (void)testDecide {
// Create an object supporting 2 QPS, and a reset duration of 4s
uint16_t maxQps = 2;
NSTimeInterval resetDuration = 4;
uint64_t allowedLogsPerDuration = maxQps * resetDuration;
RateLimiterPeer rlp(nullptr, kDefaultProcessor, maxQps, resetDuration);
// Check the current log count is initially 0
XCTAssertEqual(rlp.log_count_total_, 0);
// Make the first decision
RateLimiter::Decision gotDecision;
for (uint64_t i = 0; i < (allowedLogsPerDuration); i++) {
gotDecision = rlp.Decide(0);
XCTAssertEqual(gotDecision, RateLimiter::Decision::kAllowed);
}
// Ensure the log count is the expected amount
XCTAssertEqual(rlp.log_count_total_, allowedLogsPerDuration);
// Make another decision and ensure the log count still increases and
// the decision is rate limited
gotDecision = rlp.Decide(0);
XCTAssertEqual(gotDecision, RateLimiter::Decision::kRateLimited);
XCTAssertEqual(rlp.log_count_total_, allowedLogsPerDuration + 1);
// Make another decision, though now with the cur mach time greater than
// the reset mach time. Then ensure values were appropriately reset.
uint64_t oldResetMachTime = rlp.reset_mach_time_;
gotDecision = rlp.Decide(rlp.reset_mach_time_ + 1);
XCTAssertEqual(gotDecision, RateLimiter::Decision::kAllowed);
XCTAssertEqual(rlp.log_count_total_, 1);
XCTAssertGreaterThan(rlp.reset_mach_time_, oldResetMachTime);
}
- (void)testShouldRateLimitAndCounts {
// Create an object supporting 2 QPS, and a reset duration of 4s
uint16_t maxQps = 2;
NSTimeInterval resetDuration = 4;
uint64_t allowedLogsPerDuration = maxQps * resetDuration;
uint64_t logsOverQPS = 5;
RateLimiterPeer rlp(nullptr, kDefaultProcessor, maxQps, resetDuration);
// Initially no rate limiting should apply
XCTAssertFalse(rlp.ShouldRateLimitLocked());
XCTAssertEqual(rlp.EventsRateLimitedLocked(), 0);
// Simulate a smmaller volume of logs received than QPS
rlp.log_count_total_ = allowedLogsPerDuration - 1;
XCTAssertFalse(rlp.ShouldRateLimitLocked());
XCTAssertEqual(rlp.EventsRateLimitedLocked(), 0);
// Simulate a larger volume of logs received than QPS
rlp.log_count_total_ = allowedLogsPerDuration + logsOverQPS;
XCTAssertTrue(rlp.ShouldRateLimitLocked());
XCTAssertEqual(rlp.EventsRateLimitedLocked(), logsOverQPS);
}
@end

View File

@@ -85,6 +85,15 @@ class MockAuthResultCache : public AuthResultCache {
}
- (void)testHandleMessage {
#ifdef THREAD_SANITIZER
// TSAN and this test do not get along in multiple ways.
// We get data race false positives in OCMock, and timeouts
// waiting for messages processing (presumably due to tsan's scheduling).
// Just skip it.
XCTSkip(@"TSAN enabled");
return;
#endif
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth);
@@ -137,27 +146,31 @@ class MockAuthResultCache : public AuthResultCache {
id mockAuthClient = OCMPartialMock(authClient);
Message msg(mockESApi, &esMsg);
// Scope so msg is destructed (and calls ReleaseMessage) before stopMocking is called.
{
Message msg(mockESApi, &esMsg);
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
.ignoringNonObjectArgs()
.andReturn(NO);
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
.ignoringNonObjectArgs()
.andReturn(NO);
OCMExpect([mockAuthClient postAction:SNTActionRespondDeny
OCMExpect([mockAuthClient postAction:SNTActionRespondDeny
forMessage:Message(mockESApi, &esMsg)])
.ignoringNonObjectArgs();
OCMStub([mockAuthClient postAction:SNTActionRespondDeny
forMessage:Message(mockESApi, &esMsg)])
.ignoringNonObjectArgs();
OCMStub([mockAuthClient postAction:SNTActionRespondDeny forMessage:Message(mockESApi, &esMsg)])
.ignoringNonObjectArgs()
.andDo(nil);
.ignoringNonObjectArgs()
.andDo(nil);
[mockAuthClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kDropped);
dispatch_semaphore_signal(semaMetrics);
}];
[mockAuthClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kDropped);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
}
[mockAuthClient stopMocking];
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
@@ -180,25 +193,27 @@ class MockAuthResultCache : public AuthResultCache {
id mockAuthClient = OCMPartialMock(authClient);
Message msg(mockESApi, &esMsg);
{
Message msg(mockESApi, &esMsg);
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
.ignoringNonObjectArgs()
.andReturn(YES);
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
.ignoringNonObjectArgs()
.andReturn(YES);
OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg)]).ignoringNonObjectArgs();
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg)])
.ignoringNonObjectArgs()
.andDo(nil);
OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg)]).ignoringNonObjectArgs();
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg)])
.ignoringNonObjectArgs()
.andDo(nil);
[mockAuthClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kProcessed);
dispatch_semaphore_signal(semaMetrics);
}];
[mockAuthClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kProcessed);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
}
[mockAuthClient stopMocking];
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
@@ -219,7 +234,7 @@ class MockAuthResultCache : public AuthResultCache {
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage();
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
EXPECT_CALL(*mockAuthCache, CheckCache)
.WillOnce(testing::Return(SNTActionRequestBinary))
.WillOnce(testing::Return(SNTActionRequestBinary))
@@ -286,7 +301,7 @@ class MockAuthResultCache : public AuthResultCache {
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage();
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllowCompiler))
.WillOnce(testing::Return(true));
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, SNTActionRespondAllow))

View File

@@ -414,6 +414,13 @@ using santa::santad::event_providers::endpoint_security::Message;
[client processEnrichedMessage:enrichedMsg
handler:^(std::shared_ptr<EnrichedMessage> msg) {
// reset the shared_ptr to drop the held message.
// This is a workaround for a TSAN only false positive
// which happens if we switch back to the sem wait
// after signaling, but _before_ the implicit release
// of msg. In that case, the mock verify and the
// call of the mock's Release method can data race.
msg.reset();
dispatch_semaphore_signal(sema);
}];

View File

@@ -33,6 +33,7 @@
using santa::santad::EventDisposition;
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::FlushCacheMode;
using santa::santad::event_providers::FlushCacheReason;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::Logger;
@@ -172,6 +173,15 @@ NS_ASSUME_NONNULL_BEGIN
- (void)handleMessage:(Message &&)esMsg
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
// Process the unmount event first so that caches are flushed before any
// other potential early returns.
if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_UNMOUNT) {
self->_authResultCache->FlushCache(FlushCacheMode::kNonRootOnly,
FlushCacheReason::kFilesystemUnmounted);
recordEventMetrics(EventDisposition::kProcessed);
return;
}
if (!self.blockUSBMount) {
// TODO: We should also unsubscribe from events when this isn't set, but
// this is generally a low-volume event type.
@@ -180,12 +190,6 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_UNMOUNT) {
self->_authResultCache->FlushCache(FlushCacheMode::kNonRootOnly);
recordEventMetrics(EventDisposition::kProcessed);
return;
}
[self processMessage:std::move(esMsg)
handler:^(const Message &msg) {
es_auth_result_t result = [self handleAuthMount:msg];
@@ -248,10 +252,17 @@ NS_ASSUME_NONNULL_BEGIN
@"isEjectable: %d",
protocol, kind, isInternal, isRemovable, isEjectable);
// If the device is internal or virtual we are okay with the operation. We
// also are okay with operations for devices that are non-removal as long as
// if the device is internal, or virtual *AND* is not an SD Card,
// then allow the mount. This is to ensure we block SD cards inserted into
// the internal reader of some Macs, whilst also ensuring we don't block
// the internal storage device.
if ((isInternal || isVirtual) && !isSecureDigital) {
return ES_AUTH_RESULT_ALLOW;
}
// We are okay with operations for devices that are non-removable as long as
// they are NOT a USB device, or an SD Card.
if (isInternal || isVirtual || (!isRemovable && !isEjectable && !isUSB && !isSecureDigital)) {
if (!isRemovable && !isEjectable && !isUSB && !isSecureDigital) {
return ES_AUTH_RESULT_ALLOW;
}

View File

@@ -39,13 +39,14 @@
using santa::santad::EventDisposition;
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::FlushCacheMode;
using santa::santad::event_providers::FlushCacheReason;
using santa::santad::event_providers::endpoint_security::Message;
class MockAuthResultCache : public AuthResultCache {
public:
using AuthResultCache::AuthResultCache;
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode));
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode, FlushCacheReason reason));
};
@interface SNTEndpointSecurityDeviceManager (Testing)
@@ -316,7 +317,7 @@ class MockAuthResultCache : public AuthResultCache {
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage();
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
EXPECT_CALL(*mockAuthCache, FlushCache);
SNTEndpointSecurityDeviceManager *deviceManager =

View File

@@ -33,6 +33,7 @@
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTMetricSet.h"
#import "Source/common/SNTStrengthify.h"
#include "Source/common/SantaCache.h"
#include "Source/common/SantaVnode.h"
#include "Source/common/SantaVnodeHash.h"
@@ -40,11 +41,13 @@
#include "Source/santad/DataLayer/WatchItems.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/RateLimiter.h"
using santa::santad::EventDisposition;
using santa::santad::data_layer::WatchItemPathType;
using santa::santad::data_layer::WatchItemPolicy;
using santa::santad::data_layer::WatchItems;
using santa::santad::event_providers::RateLimiter;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::Enricher;
using santa::santad::event_providers::endpoint_security::EnrichOptions;
@@ -54,6 +57,7 @@ using santa::santad::logs::endpoint_security::Logger;
NSString *kBadCertHash = @"BAD_CERT_HASH";
static constexpr uint32_t kOpenFlagsIndicatingWrite = FWRITE | O_APPEND | O_TRUNC;
static constexpr uint16_t kDefaultRateLimitQPS = 50;
// Small structure to hold a complete event path target being operated upon and
// a bool indicating whether the path is a readable target (e.g. a file being
@@ -191,13 +195,13 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
@interface SNTEndpointSecurityFileAccessAuthorizer ()
@property SNTDecisionCache *decisionCache;
@property bool isSubscribed;
@property SNTMetricBooleanGauge *famEnabled;
@end
@implementation SNTEndpointSecurityFileAccessAuthorizer {
std::shared_ptr<Logger> _logger;
std::shared_ptr<WatchItems> _watchItems;
std::shared_ptr<Enricher> _enricher;
std::shared_ptr<RateLimiter> _rateLimiter;
SantaCache<SantaVnode, NSString *> _certHashCache;
}
@@ -211,7 +215,7 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
decisionCache:(SNTDecisionCache *)decisionCache {
self = [super initWithESAPI:std::move(esApi)
metrics:std::move(metrics)
metrics:metrics
processor:santa::santad::Processor::kFileAccessAuthorizer];
if (self) {
_watchItems = std::move(watchItems);
@@ -220,11 +224,20 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
_decisionCache = decisionCache;
_famEnabled = [[SNTMetricSet sharedInstance]
_rateLimiter = RateLimiter::Create(metrics, santa::santad::Processor::kFileAccessAuthorizer,
kDefaultRateLimitQPS);
SNTMetricBooleanGauge *famEnabled = [[SNTMetricSet sharedInstance]
booleanGaugeWithName:@"/santa/fam_enabled"
fieldNames:@[]
helpText:@"Whether or not the FAM client is enabled"];
WEAKIFY(self);
[[SNTMetricSet sharedInstance] registerCallback:^{
STRONGIFY(self);
[famEnabled set:self.isSubscribed forFieldValues:@[]];
}];
[self establishClientOrDie];
[super enableTargetPathWatching];
@@ -437,8 +450,10 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
forTarget:target
toMessage:msg];
if (ShouldLogDecision(policyDecision)) {
if (optionalPolicy.has_value()) {
// Note: If ShouldLogDecision, it shouldn't be possible for optionalPolicy
// to not have a value. Performing the check just in case to prevent a crash.
if (ShouldLogDecision(policyDecision) && optionalPolicy.has_value()) {
if (_rateLimiter->Decide(msg->mach_time) == RateLimiter::Decision::kAllowed) {
std::string policyNameCopy = optionalPolicy.value()->name;
std::string policyVersionCopy = policyVersion;
std::string targetPathCopy = target.path;
@@ -450,10 +465,6 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
self->_enricher->Enrich(*esMsg->process, EnrichOptions::kLocalOnly),
targetPathCopy, policyDecision);
}];
} else {
LOGE(@"Unexpectedly missing policy: Unable to log file access event: %s -> %s",
Path(msg->process->executable).data(), target.path.c_str());
}
}
@@ -531,7 +542,6 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
if (!self.isSubscribed) {
if ([super subscribe:events]) {
self.isSubscribed = true;
[self.famEnabled set:YES forFieldValues:@[]];
}
}
@@ -543,7 +553,6 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
if (self.isSubscribed) {
if ([super unsubscribeAll]) {
self.isSubscribed = false;
[self.famEnabled set:NO forFieldValues:@[]];
}
[super unmuteEverything];
}

View File

@@ -105,7 +105,7 @@ class MockLogger : public Logger {
auto mockEnricher = std::make_shared<MockEnricher>();
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(enrichedMsg));
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr, nil);
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFile)).Times(1);
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);

View File

@@ -23,6 +23,7 @@
#include <sys/wait.h>
#include <time.h>
#include <functional>
#include <optional>
#include <string_view>
@@ -30,6 +31,7 @@
#import "Source/common/SNTConfigurator.h"
#include "Source/common/SNTLogging.h"
#import "Source/common/SNTStoredEvent.h"
#import "Source/common/String.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
#import "Source/santad/SNTDecisionCache.h"
@@ -38,6 +40,7 @@
using google::protobuf::Arena;
using google::protobuf::Timestamp;
using santa::common::NSStringToUTF8StringView;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::EnrichedClose;
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
@@ -94,15 +97,15 @@ static inline void EncodePath(std::string *buf, const es_file_t *es_file) {
buf->append(std::string_view(es_file->path.data, es_file->path.length));
}
static inline void EncodeString(std::string *buf, NSString *value) {
static inline void EncodeString(std::function<std::string *()> lazy_f, NSString *value) {
if (value) {
buf->append(std::string_view([value UTF8String], [value length]));
lazy_f()->append(NSStringToUTF8StringView(value));
}
}
static inline void EncodeString(std::string *buf, std::string_view value) {
static inline void EncodeString(std::function<std::string *()> lazy_f, std::string_view value) {
if (value.length() > 0) {
buf->append(std::string_view(value.data(), value.length()));
lazy_f()->append(value);
}
}
@@ -125,7 +128,7 @@ static inline void EncodeGroupInfo(::pbv1::GroupInfo *pb_group_info, gid_t gid,
static inline void EncodeHash(::pbv1::Hash *pb_hash, NSString *sha256) {
if (sha256) {
pb_hash->set_type(::pbv1::Hash::HASH_ALGO_SHA256);
pb_hash->set_hash([sha256 UTF8String], [sha256 length]);
EncodeString([pb_hash] { return pb_hash->mutable_hash(); }, sha256);
}
}
@@ -162,7 +165,7 @@ static inline void EncodeFileInfo(::pbv1::FileInfo *pb_file, const es_file_t *es
static inline void EncodeFileInfoLight(::pbv1::FileInfoLight *pb_file, std::string_view path,
bool truncated) {
EncodeString(pb_file->mutable_path(), path);
EncodeString([pb_file] { return pb_file->mutable_path(); }, path);
pb_file->set_truncated(truncated);
}
@@ -262,9 +265,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
EncodeHash(pb_cert_info->mutable_hash(), cert_hash);
}
if (common_name) {
pb_cert_info->set_common_name([common_name UTF8String], [common_name length]);
}
EncodeString([pb_cert_info] { return pb_cert_info->mutable_common_name(); }, common_name);
}
::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state) {
@@ -356,7 +357,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
::pbv1::SantaMessage *santa_msg = Arena::CreateMessage<::pbv1::SantaMessage>(arena);
if (EnabledMachineID()) {
EncodeString(santa_msg->mutable_machine_id(), MachineID());
EncodeString([santa_msg] { return santa_msg->mutable_machine_id(); }, MachineID());
}
EncodeTimestamp(santa_msg->mutable_event_time(), event_time);
EncodeTimestamp(santa_msg->mutable_processed_time(), processed_time);
@@ -491,18 +492,11 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCach
EncodeCertificateInfo(pb_exec->mutable_certificate_info(), cd.certSHA256, cd.certCommonName);
}
if (cd.decisionExtra) {
pb_exec->set_explain([cd.decisionExtra UTF8String], [cd.decisionExtra length]);
}
if (cd.quarantineURL) {
pb_exec->set_quarantine_url([cd.quarantineURL UTF8String], [cd.quarantineURL length]);
}
EncodeString([pb_exec] { return pb_exec->mutable_explain(); }, cd.decisionExtra);
EncodeString([pb_exec] { return pb_exec->mutable_quarantine_url(); }, cd.quarantineURL);
NSString *orig_path = Utilities::OriginalPathForTranslocation(msg.es_msg().event.exec.target);
if (orig_path) {
pb_exec->set_original_path([orig_path UTF8String], [orig_path length]);
}
EncodeString([pb_exec] { return pb_exec->mutable_original_path(); }, orig_path);
return FinalizeProto(santa_msg);
}
@@ -594,8 +588,9 @@ std::vector<uint8_t> Protobuf::SerializeFileAccess(const std::string &policy_ver
EncodeProcessInfo(file_access->mutable_instigator(), msg->version, msg->process,
enriched_process);
EncodeFileInfoLight(file_access->mutable_target(), target, false);
EncodeString(file_access->mutable_policy_version(), policy_version);
EncodeString(file_access->mutable_policy_name(), policy_name);
EncodeString([file_access] { return file_access->mutable_policy_version(); }, policy_version);
EncodeString([file_access] { return file_access->mutable_policy_name(); }, policy_name);
file_access->set_access_type(GetAccessType(msg->event_type));
file_access->set_policy_decision(GetPolicyDecision(decision));
@@ -629,10 +624,12 @@ std::vector<uint8_t> Protobuf::SerializeBundleHashingEvent(SNTStoredEvent *event
EncodeHash(pb_bundle->mutable_file_hash(), event.fileSHA256);
EncodeHash(pb_bundle->mutable_bundle_hash(), event.fileBundleHash);
pb_bundle->set_bundle_name([NonNull(event.fileBundleName) UTF8String]);
pb_bundle->set_bundle_id([NonNull(event.fileBundleID) UTF8String]);
pb_bundle->set_bundle_path([NonNull(event.fileBundlePath) UTF8String]);
pb_bundle->set_path([NonNull(event.filePath) UTF8String]);
EncodeString([pb_bundle] { return pb_bundle->mutable_bundle_name(); },
NonNull(event.fileBundleName));
EncodeString([pb_bundle] { return pb_bundle->mutable_bundle_id(); }, NonNull(event.fileBundleID));
EncodeString([pb_bundle] { return pb_bundle->mutable_bundle_path(); },
NonNull(event.fileBundlePath));
EncodeString([pb_bundle] { return pb_bundle->mutable_path(); }, NonNull(event.filePath));
return FinalizeProto(santa_msg);
}
@@ -652,14 +649,14 @@ static void EncodeDisk(::pbv1::Disk *pb_disk, ::pbv1::Disk_Action action, NSDict
stringWithFormat:@"%@ %@", NonNull(props[@"DADeviceVendor"]), NonNull(props[@"DADeviceModel"])];
model = [model stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
EncodeString(pb_disk->mutable_mount(), [props[@"DAVolumePath"] path]);
EncodeString(pb_disk->mutable_volume(), props[@"DAVolumeName"]);
EncodeString(pb_disk->mutable_bsd_name(), props[@"DAMediaBSDName"]);
EncodeString(pb_disk->mutable_fs(), props[@"DAVolumeKind"]);
EncodeString(pb_disk->mutable_model(), model);
EncodeString(pb_disk->mutable_serial(), serial);
EncodeString(pb_disk->mutable_bus(), props[@"DADeviceProtocol"]);
EncodeString(pb_disk->mutable_dmg_path(), dmg_path);
EncodeString([pb_disk] { return pb_disk->mutable_mount(); }, [props[@"DAVolumePath"] path]);
EncodeString([pb_disk] { return pb_disk->mutable_volume(); }, props[@"DAVolumeName"]);
EncodeString([pb_disk] { return pb_disk->mutable_bsd_name(); }, props[@"DAMediaBSDName"]);
EncodeString([pb_disk] { return pb_disk->mutable_fs(); }, props[@"DAVolumeKind"]);
EncodeString([pb_disk] { return pb_disk->mutable_model(); }, model);
EncodeString([pb_disk] { return pb_disk->mutable_serial(); }, serial);
EncodeString([pb_disk] { return pb_disk->mutable_bus(); }, props[@"DADeviceProtocol"]);
EncodeString([pb_disk] { return pb_disk->mutable_dmg_path(); }, dmg_path);
if (props[@"DAAppearanceTime"]) {
// Note: `DAAppearanceTime` is set via `CFAbsoluteTimeGetCurrent`, which uses the defined

View File

@@ -51,8 +51,7 @@ class SanitizableString {
friend std::ostream &operator<<(std::ostream &ss, const SanitizableString &sani_string);
private:
const char *data_;
size_t length_;
std::string_view data_;
mutable bool sanitized_ = false;
mutable std::optional<std::string> sanitized_string_;
};

View File

@@ -14,37 +14,35 @@
#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h"
#include "Source/common/String.h"
using santa::common::NSStringToUTF8StringView;
namespace santa::santad::logs::endpoint_security::serializers {
SanitizableString::SanitizableString(const es_file_t *file)
: data_(file->path.data), length_(file->path.length) {}
: data_(file->path.data, file->path.length) {}
SanitizableString::SanitizableString(const es_string_token_t &tok)
: data_(tok.data), length_(tok.length) {}
SanitizableString::SanitizableString(const es_string_token_t &tok) : data_(tok.data, tok.length) {}
SanitizableString::SanitizableString(NSString *str)
: data_([str UTF8String]), length_([str length]) {}
SanitizableString::SanitizableString(NSString *str) : data_(NSStringToUTF8StringView(str)) {}
SanitizableString::SanitizableString(const char *str, size_t len) : data_(str), length_(len) {}
SanitizableString::SanitizableString(const char *str, size_t len) : data_(str, len) {}
std::string_view SanitizableString::String() const {
return std::string_view(data_, length_);
return data_;
}
std::string_view SanitizableString::Sanitized() const {
if (!sanitized_) {
sanitized_ = true;
sanitized_string_ = SanitizeString(data_, length_);
sanitized_string_ = SanitizeString(data_.data(), data_.length());
}
if (sanitized_string_.has_value()) {
return sanitized_string_.value();
} else {
if (data_) {
return std::string_view(data_, length_);
} else {
return "";
}
return data_;
}
}

View File

@@ -34,10 +34,29 @@ class FilePeer : public File {
using File::ShouldFlush;
using File::WatchLogFile;
NSFileHandle *FileHandle() { return file_handle_; }
// Member accesses wrapped in a dispatch_sync to satisfy tsan.
NSFileHandle *FileHandle() {
__block NSFileHandle *h;
dispatch_sync(q_, ^{
h = file_handle_;
});
return h;
}
size_t InternalBufferSize() { return buffer_offset_; }
size_t InternalBufferCapacity() { return buffer_.capacity(); }
size_t InternalBufferSize() {
__block size_t s = 0;
dispatch_sync(q_, ^{
s = buffer_offset_;
});
return s;
}
size_t InternalBufferCapacity() {
__block size_t s = 0;
dispatch_sync(q_, ^{
s = buffer_.capacity();
});
return s;
}
};
} // namespace santa::santad::logs::endpoint_security::writers

View File

@@ -53,7 +53,8 @@ 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,
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *));
SNTMetricCounter *rate_limit_counts, SNTMetricSet *metric_set,
void (^run_on_first_start)(Metrics *));
~Metrics();
@@ -68,6 +69,8 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
void SetEventMetrics(Processor processor, es_event_type_t event_type,
EventDisposition disposition, int64_t nanos);
void SetRateLimitingMetrics(Processor processor, int64_t events_rate_limited_count);
friend class santa::santad::MetricsPeer;
private:
@@ -80,6 +83,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
uint64_t interval_;
SNTMetricInt64Gauge *event_processing_times_;
SNTMetricCounter *event_counts_;
SNTMetricCounter *rate_limit_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
@@ -94,6 +98,7 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
// Small caches for storing event metrics between metrics export operations
std::map<EventCountTuple, int64_t> event_counts_cache_;
std::map<EventTimesTuple, int64_t> event_times_cache_;
std::map<Processor, int64_t> rate_limit_counts_cache_;
};
} // namespace santa::santad

View File

@@ -124,9 +124,14 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
fieldNames:@[ @"Processor", @"Event", @"Disposition" ]
helpText:@"Events received and processed by each processor"];
SNTMetricCounter *rate_limit_counts =
[metric_set counterWithName:@"/santa/rate_limit_count"
fieldNames:@[ @"Processor" ]
helpText:@"Events rate limited by each processor"];
std::shared_ptr<Metrics> metrics =
std::make_shared<Metrics>(q, timer_source, interval, event_processing_times, event_counts,
metric_set, ^(Metrics *metrics) {
rate_limit_counts, metric_set, ^(Metrics *metrics) {
SNTRegisterCoreMetrics();
metrics->EstablishConnection();
});
@@ -146,12 +151,14 @@ 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,
SNTMetricSet *metric_set, void (^run_on_first_start)(Metrics *))
SNTMetricCounter *rate_limit_counts, SNTMetricSet *metric_set,
void (^run_on_first_start)(Metrics *))
: q_(q),
timer_source_(timer_source),
interval_(interval),
event_processing_times_(event_processing_times),
event_counts_(event_counts),
rate_limit_counts_(rate_limit_counts),
metric_set_(metric_set),
run_on_first_start_(run_on_first_start) {
SetInterval(interval_);
@@ -211,9 +218,16 @@ void Metrics::FlushMetrics() {
[event_processing_times_ set:kv.second forFieldValues:@[ processorName, eventName ]];
}
for (const auto &kv : rate_limit_counts_cache_) {
NSString *processorName = ProcessorToString(kv.first);
[rate_limit_counts_ incrementBy:kv.second forFieldValues:@[ processorName ]];
}
// Reset the maps so the next cycle begins with a clean state
event_counts_cache_ = {};
event_times_cache_ = {};
rate_limit_counts_cache_ = {};
});
}
@@ -263,4 +277,10 @@ void Metrics::SetEventMetrics(Processor processor, es_event_type_t event_type,
});
}
void Metrics::SetRateLimitingMetrics(Processor processor, int64_t events_rate_limited_count) {
dispatch_sync(events_q_, ^{
rate_limit_counts_cache_[processor] += events_rate_limited_count;
});
}
} // namespace santa::santad

View File

@@ -48,6 +48,7 @@ class MetricsPeer : public Metrics {
std::map<EventCountTuple, int64_t> &EventCounts() { return event_counts_cache_; };
std::map<EventTimesTuple, int64_t> &EventTimes() { return event_times_cache_; };
std::map<Processor, int64_t> &RateLimitCounts() { return rate_limit_counts_cache_; };
};
} // namespace santa::santad
@@ -72,10 +73,10 @@ 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, ^(santa::santad::Metrics *m) {
dispatch_semaphore_signal(self.sema);
});
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
^(santa::santad::Metrics *m) {
dispatch_semaphore_signal(self.sema);
});
XCTAssertFalse(metrics->IsRunning());
@@ -108,7 +109,7 @@ 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,
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
});
@@ -182,7 +183,7 @@ using santa::santad::ProcessorToString;
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,
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
@@ -221,6 +222,32 @@ using santa::santad::ProcessorToString;
XCTAssertEqual(metrics->EventTimes()[etOpen], nanos * 2);
}
- (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,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
// Initial map is empty
XCTAssertEqual(metrics->RateLimitCounts().size(), 0);
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
// Check sizes after setting metrics once
XCTAssertEqual(metrics->RateLimitCounts().size(), 1);
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 456);
metrics->SetRateLimitingMetrics(Processor::kAuthorizer, 789);
// Re-check expected counts. One was an update, so should only be 2 items
XCTAssertEqual(metrics->RateLimitCounts().size(), 2);
// Check map values
XCTAssertEqual(metrics->RateLimitCounts()[Processor::kFileAccessAuthorizer], 123 + 456);
XCTAssertEqual(metrics->RateLimitCounts()[Processor::kAuthorizer], 789);
}
- (void)testFlushMetrics {
id mockEventProcessingTimes = OCMClassMock([SNTMetricInt64Gauge class]);
id mockEventCounts = OCMClassMock([SNTMetricCounter class]);
@@ -240,7 +267,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, nil,
mockEventCounts, mockEventCounts, nil,
^(santa::santad::Metrics *m){
// This block intentionally left blank
});
@@ -249,23 +276,28 @@ using santa::santad::ProcessorToString;
EventDisposition::kProcessed, nanos);
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_OPEN,
EventDisposition::kProcessed, nanos * 2);
metrics->SetRateLimitingMetrics(Processor::kFileAccessAuthorizer, 123);
// First ensure we have the expected map sizes
XCTAssertEqual(metrics->EventCounts().size(), 2);
XCTAssertEqual(metrics->EventTimes().size(), 2);
XCTAssertEqual(metrics->RateLimitCounts().size(), 1);
metrics->FlushMetrics();
// After setting two different event metrics, we expect the sema to be hit
// four times - twice each for the event counts and event times maps
// 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)");
// After a flush, map sizes should be reset to 0
XCTAssertEqual(metrics->EventCounts().size(), 0);
XCTAssertEqual(metrics->EventTimes().size(), 0);
XCTAssertEqual(metrics->RateLimitCounts().size(), 0);
}
@end

View File

@@ -39,6 +39,7 @@ using santa::santad::data_layer::WatchItems;
using santa::santad::data_layer::WatchItemsState;
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::FlushCacheMode;
using santa::santad::event_providers::FlushCacheReason;
using santa::santad::logs::endpoint_security::Logger;
// Globals used by the santad watchdog thread
@@ -84,12 +85,9 @@ double watchdogRAMPeak = 0;
reply([counts[0] unsignedLongLongValue], [counts[1] unsignedLongLongValue]);
}
- (void)flushAllCaches {
self->_authResultCache->FlushCache(FlushCacheMode::kAllCaches);
}
- (void)flushCache:(void (^)(BOOL))reply {
[self flushAllCaches];
self->_authResultCache->FlushCache(FlushCacheMode::kAllCaches,
FlushCacheReason::kExplicitCommand);
reply(YES);
}
@@ -125,7 +123,7 @@ double watchdogRAMPeak = 0;
// The actual cache flushing happens after the new rules have been added to the database.
if (flushCache) {
LOGI(@"Flushing caches");
[self flushAllCaches];
self->_authResultCache->FlushCache(FlushCacheMode::kAllCaches, FlushCacheReason::kRulesChanged);
}
reply(error);

View File

@@ -36,6 +36,7 @@
#import "Source/common/SNTRule.h"
#import "Source/common/SNTStoredEvent.h"
#include "Source/common/SantaVnode.h"
#include "Source/common/String.h"
#import "Source/santad/DataLayer/SNTEventTable.h"
#import "Source/santad/DataLayer/SNTRuleTable.h"
#import "Source/santad/SNTDecisionCache.h"
@@ -293,7 +294,7 @@ static NSString *const kPrinterProxyPostMonterey =
NSAttributedString *s = [SNTBlockMessage attributedBlockMessageForEvent:se
customMessage:cd.customMsg];
if (targetProc->tty && targetProc->tty->path.length > 0) {
if (targetProc->tty && targetProc->tty->path.length > 0 && !config.enableSilentTTYMode) {
NSMutableString *msg = [NSMutableString stringWithCapacity:1024];
[msg appendFormat:@"\n\033[1mSanta\033[0m\n\n%@\n\n", s.string];
[msg appendFormat:@"\033[1mPath: \033[0m %@\n"
@@ -360,7 +361,8 @@ static NSString *const kPrinterProxyPostMonterey =
- (void)printMessage:(NSString *)msg toTTY:(const char *)path {
int fd = open(path, O_WRONLY | O_NOCTTY);
write(fd, msg.UTF8String, msg.length);
std::string_view str = santa::common::NSStringToUTF8StringView(msg);
write(fd, str.data(), str.length());
close(fd);
}

View File

@@ -43,6 +43,7 @@ using santa::santad::Metrics;
using santa::santad::data_layer::WatchItems;
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::FlushCacheMode;
using santa::santad::event_providers::FlushCacheReason;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::Enricher;
using santa::santad::logs::endpoint_security::Logger;
@@ -141,34 +142,33 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
EstablishSyncServiceConnection(syncd_queue);
NSArray<SNTKVOManager *> *kvoObservers = @[
[[SNTKVOManager alloc] initWithObject:configurator
selector:@selector(clientMode)
type:[NSNumber class]
callback:^(NSNumber *oldValue, NSNumber *newValue) {
if ([oldValue longLongValue] == [newValue longLongValue]) {
// Note: This case apparently can happen and if not checked
// will result in excessive notification messages sent to the
// user when calling `postClientModeNotification` below
return;
}
[[SNTKVOManager alloc]
initWithObject:configurator
selector:@selector(clientMode)
type:[NSNumber class]
callback:^(NSNumber *oldValue, NSNumber *newValue) {
if ([oldValue longLongValue] == [newValue longLongValue]) {
// Note: This case apparently can happen and if not checked
// will result in excessive notification messages sent to the
// user when calling `postClientModeNotification` below
return;
}
SNTClientMode clientMode =
(SNTClientMode)[newValue longLongValue];
SNTClientMode clientMode = (SNTClientMode)[newValue longLongValue];
switch (clientMode) {
case SNTClientModeLockdown:
LOGI(@"Changed client mode to Lockdown, flushing cache.");
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches);
break;
case SNTClientModeMonitor:
LOGI(@"Changed client mode to Monitor.");
break;
default: LOGW(@"Changed client mode to unknown value."); break;
}
switch (clientMode) {
case SNTClientModeLockdown:
LOGI(@"Changed client mode to Lockdown, flushing cache.");
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches,
FlushCacheReason::kClientModeChanged);
break;
case SNTClientModeMonitor: LOGI(@"Changed client mode to Monitor."); break;
default: LOGW(@"Changed client mode to unknown value."); break;
}
[[notifier_queue.notifierConnection remoteObjectProxy]
postClientModeNotification:clientMode];
}],
[[notifier_queue.notifierConnection remoteObjectProxy]
postClientModeNotification:clientMode];
}],
[[SNTKVOManager alloc]
initWithObject:configurator
selector:@selector(syncBaseURL)
@@ -233,7 +233,8 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
}
LOGI(@"Changed allowlist regex, flushing cache");
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches);
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches,
FlushCacheReason::kPathRegexChanged);
}],
[[SNTKVOManager alloc]
initWithObject:configurator
@@ -246,7 +247,8 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
}
LOGI(@"Changed denylist regex, flushing cache");
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches);
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches,
FlushCacheReason::kPathRegexChanged);
}],
[[SNTKVOManager alloc] initWithObject:configurator
selector:@selector(blockUSBMount)
@@ -297,19 +299,36 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
selector:@selector(fileAccessPolicyPlist)
type:[NSString class]
callback:^(NSString *oldValue, NSString *newValue) {
if ([configurator fileAccessPolicy]) {
// Ignore any changes to this key if fileAccessPolicy is set
return;
}
if (oldValue != newValue || (newValue && ![oldValue isEqualToString:newValue])) {
LOGI(@"Filesystem monitoring policy config path changed: %@ -> %@", oldValue,
newValue);
watch_items->SetConfigPath(newValue);
}
}],
[[SNTKVOManager alloc] initWithObject:configurator
selector:@selector(fileAccessPolicy)
type:[NSDictionary class]
callback:^(NSDictionary *oldValue, NSDictionary *newValue) {
if (oldValue != newValue ||
(newValue && ![oldValue isEqualToDictionary:newValue])) {
LOGI(@"Filesystem monitoring policy embedded config changed");
watch_items->SetConfig(newValue);
}
}],
[[SNTKVOManager alloc] initWithObject:configurator
selector:@selector(staticRules)
type:[NSArray class]
callback:^(NSArray *oldValue, NSArray *newValue) {
if ([oldValue isEqual:newValue]) return;
LOGI(@"StaticRules set has changed, flushing cache.");
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches);
auth_result_cache->FlushCache(
FlushCacheMode::kAllCaches,
FlushCacheReason::kStaticRulesChanged);
}],
[[SNTKVOManager alloc]
initWithObject:configurator

View File

@@ -49,6 +49,8 @@ class SantadDeps {
std::unique_ptr<santa::santad::logs::endpoint_security::Logger> logger,
std::shared_ptr<santa::santad::Metrics> metrics,
std::shared_ptr<santa::santad::data_layer::WatchItems> watch_items,
std::shared_ptr<santa::santad::event_providers::AuthResultCache>
auth_result_cache,
MOLXPCConnection *control_connection,
SNTCompilerController *compiler_controller,
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,

View File

@@ -117,8 +117,12 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
exit(EXIT_FAILURE);
}
std::shared_ptr<::WatchItems> watch_items = WatchItems::Create(
[configurator fileAccessPolicyPlist], [configurator fileAccessPolicyUpdateIntervalSec]);
std::shared_ptr<::WatchItems> watch_items =
[configurator fileAccessPolicy]
? WatchItems::Create([configurator fileAccessPolicy],
[configurator fileAccessPolicyUpdateIntervalSec])
: WatchItems::Create([configurator fileAccessPolicyPlist],
[configurator fileAccessPolicyUpdateIntervalSec]);
if (!watch_items) {
LOGE(@"Failed to create watch items");
exit(EXIT_FAILURE);
@@ -131,25 +135,31 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
exit(EXIT_FAILURE);
}
return std::make_unique<SantadDeps>(
esapi, std::move(logger), std::move(metrics), std::move(watch_items), control_connection,
compiler_controller, notifier_queue, syncd_queue, exec_controller, prefix_tree);
std::shared_ptr<::AuthResultCache> auth_result_cache = AuthResultCache::Create(esapi, metric_set);
if (!auth_result_cache) {
LOGE(@"Failed to create auth result cache");
exit(EXIT_FAILURE);
}
return std::make_unique<SantadDeps>(esapi, std::move(logger), std::move(metrics),
std::move(watch_items), std::move(auth_result_cache),
control_connection, compiler_controller, notifier_queue,
syncd_queue, exec_controller, prefix_tree);
}
SantadDeps::SantadDeps(std::shared_ptr<EndpointSecurityAPI> esapi, std::unique_ptr<::Logger> logger,
std::shared_ptr<::Metrics> metrics,
std::shared_ptr<::WatchItems> watch_items,
MOLXPCConnection *control_connection,
SNTCompilerController *compiler_controller,
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
SNTExecutionController *exec_controller,
std::shared_ptr<::PrefixTree<Unit>> prefix_tree)
SantadDeps::SantadDeps(
std::shared_ptr<EndpointSecurityAPI> esapi, std::unique_ptr<::Logger> logger,
std::shared_ptr<::Metrics> metrics, std::shared_ptr<::WatchItems> watch_items,
std::shared_ptr<santa::santad::event_providers::AuthResultCache> auth_result_cache,
MOLXPCConnection *control_connection, SNTCompilerController *compiler_controller,
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
SNTExecutionController *exec_controller, std::shared_ptr<::PrefixTree<Unit>> prefix_tree)
: esapi_(std::move(esapi)),
logger_(std::move(logger)),
metrics_(std::move(metrics)),
watch_items_(std::move(watch_items)),
enricher_(std::make_shared<::Enricher>()),
auth_result_cache_(std::make_shared<::AuthResultCache>(esapi_)),
auth_result_cache_(std::move(auth_result_cache)),
control_connection_(control_connection),
compiler_controller_(compiler_controller),
notifier_queue_(notifier_queue),

View File

@@ -102,13 +102,13 @@
}
},
"args": [
"exec_path",
"-l",
"--foo"
"ZXhlY19wYXRo",
"LWw=",
"LS1mb28="
],
"envs": [
"ENV_PATH=/path/to/bin:/and/another",
"DEBUG=1"
"RU5WX1BBVEg9L3BhdGgvdG8vYmluOi9hbmQvYW5vdGhlcg==",
"REVCVUc9MQ=="
],
"decision": "DECISION_ALLOW",
"reason": "REASON_BINARY",

View File

@@ -130,13 +130,13 @@
}
},
"args": [
"exec_path",
"-l",
"--foo"
"ZXhlY19wYXRo",
"LWw=",
"LS1mb28="
],
"envs": [
"ENV_PATH=/path/to/bin:/and/another",
"DEBUG=1"
"RU5WX1BBVEg9L3BhdGgvdG8vYmluOi9hbmQvYW5vdGhlcg==",
"REVCVUc9MQ=="
],
"decision": "DECISION_ALLOW",
"reason": "REASON_BINARY",

View File

@@ -163,13 +163,13 @@
}
},
"args": [
"exec_path",
"-l",
"--foo"
"ZXhlY19wYXRo",
"LWw=",
"LS1mb28="
],
"envs": [
"ENV_PATH=/path/to/bin:/and/another",
"DEBUG=1"
"RU5WX1BBVEg9L3BhdGgvdG8vYmluOi9hbmQvYW5vdGhlcg==",
"REVCVUc9MQ=="
],
"fds": [
{

View File

@@ -163,13 +163,13 @@
}
},
"args": [
"exec_path",
"-l",
"--foo"
"ZXhlY19wYXRo",
"LWw=",
"LS1mb28="
],
"envs": [
"ENV_PATH=/path/to/bin:/and/another",
"DEBUG=1"
"RU5WX1BBVEg9L3BhdGgvdG8vYmluOi9hbmQvYW5vdGhlcg==",
"REVCVUc9MQ=="
],
"fds": [
{

View File

@@ -163,13 +163,13 @@
}
},
"args": [
"exec_path",
"-l",
"--foo"
"ZXhlY19wYXRo",
"LWw=",
"LS1mb28="
],
"envs": [
"ENV_PATH=/path/to/bin:/and/another",
"DEBUG=1"
"RU5WX1BBVEg9L3BhdGgvdG8vYmluOi9hbmQvYW5vdGhlcg==",
"REVCVUc9MQ=="
],
"fds": [
{

View File

@@ -54,6 +54,7 @@ static const uint8_t kMaxEnqueuedSyncs = 2;
@property NSUInteger eventBatchSize;
@property NSString *xsrfToken;
@property NSString *xsrfTokenHeader;
@end
@@ -125,6 +126,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
[self startReachability];
}
self.xsrfToken = syncState.xsrfToken;
self.xsrfTokenHeader = syncState.xsrfTokenHeader;
}
- (void)postBundleEventToSyncServer:(SNTStoredEvent *)event
@@ -158,6 +160,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
[self startReachability];
}
self.xsrfToken = syncState.xsrfToken;
self.xsrfTokenHeader = syncState.xsrfTokenHeader;
}
- (void)isFCMListening:(void (^)(BOOL))reply {
@@ -229,6 +232,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
BOOL ret = [p sync];
LOGD(@"Rule download %@", ret ? @"complete" : @"failed");
self.xsrfToken = syncState.xsrfToken;
self.xsrfTokenHeader = syncState.xsrfTokenHeader;
});
}
@@ -254,6 +258,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
if ([p sync]) {
SLOGD(@"Preflight complete");
self.xsrfToken = syncState.xsrfToken;
self.xsrfTokenHeader = syncState.xsrfTokenHeader;
// Clean up reachability if it was started for a non-network error
[self stopReachability];
@@ -355,6 +360,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
}
syncState.xsrfToken = self.xsrfToken;
syncState.xsrfTokenHeader = self.xsrfTokenHeader;
NSURLSessionConfiguration *sessConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessConfig.connectionProxyDictionary = [[SNTConfigurator configurator] syncProxyConfig];
@@ -391,10 +397,7 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
syncState.session = [authURLSession session];
syncState.daemonConn = self.daemonConn;
syncState.compressedContentEncoding =
config.enableBackwardsCompatibleContentEncoding ? @"zlib" : @"deflate";
syncState.contentEncoding = config.syncClientContentEncoding;
syncState.pushNotificationsToken = self.pushNotifications.token;
return syncState;

View File

@@ -13,6 +13,7 @@
/// limitations under the License.
#import "Source/santasyncservice/SNTSyncStage.h"
#include "Source/common/SNTCommonEnums.h"
#import <MOLXPCConnection/MOLXPCConnection.h>
@@ -28,7 +29,6 @@
@property(readwrite) NSURLSession *urlSession;
@property(readwrite) SNTSyncState *syncState;
@property(readwrite) MOLXPCConnection *daemonConn;
@property BOOL xsrfFetched;
@end
@@ -68,12 +68,30 @@
NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:[self stageURL]];
[req setHTTPMethod:@"POST"];
[req setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[req setValue:self.syncState.xsrfToken forHTTPHeaderField:kXSRFToken];
NSString *xsrfHeader = self.syncState.xsrfTokenHeader ?: kDefaultXSRFTokenHeader;
[req setValue:self.syncState.xsrfToken forHTTPHeaderField:xsrfHeader];
NSData *compressed;
NSString *contentEncodingHeader;
switch (self.syncState.contentEncoding) {
case SNTSyncContentEncodingNone: break;
case SNTSyncContentEncodingGzip:
compressed = [requestBody gzipCompressed];
contentEncodingHeader = @"gzip";
break;
case SNTSyncContentEncodingDeflate:
compressed = [requestBody zlibCompressed];
contentEncodingHeader = @"deflate";
break;
default:
// This would be a programming error.
LOGD(@"Unexpected value for content encoding %ld", self.syncState.contentEncoding);
}
NSData *compressed = [requestBody zlibCompressed];
if (compressed) {
requestBody = compressed;
[req setValue:self.syncState.compressedContentEncoding forHTTPHeaderField:@"Content-Encoding"];
[req setValue:contentEncodingHeader forHTTPHeaderField:@"Content-Encoding"];
}
[req setHTTPBody:requestBody];
@@ -105,7 +123,8 @@
error.code == NSURLErrorCannotParseResponse) &&
[self fetchXSRFToken]) {
NSMutableURLRequest *mutableRequest = [request mutableCopy];
[mutableRequest setValue:self.syncState.xsrfToken forHTTPHeaderField:kXSRFToken];
NSString *xsrfHeader = self.syncState.xsrfTokenHeader ?: kDefaultXSRFTokenHeader;
[mutableRequest setValue:self.syncState.xsrfToken forHTTPHeaderField:xsrfHeader];
request = mutableRequest;
continue;
}
@@ -182,15 +201,20 @@
}
- (NSData *)stripXssi:(NSData *)data {
static const char xssi[3] = {']', ')', '}'};
if (data.length < 3 || strncmp(data.bytes, xssi, 3)) return data;
return [data subdataWithRange:NSMakeRange(3, data.length - 3)];
static const char xssiOne[5] = {')', ']', '}', '\'', '\n'};
static const char xssiTwo[3] = {']', ')', '}'};
if (data.length >= sizeof(xssiOne) && strncmp(data.bytes, xssiOne, sizeof(xssiOne)) == 0) {
return [data subdataWithRange:NSMakeRange(sizeof(xssiOne), data.length - sizeof(xssiOne))];
}
if (data.length >= sizeof(xssiTwo) && strncmp(data.bytes, xssiTwo, sizeof(xssiTwo)) == 0) {
return [data subdataWithRange:NSMakeRange(sizeof(xssiTwo), data.length - sizeof(xssiTwo))];
}
return data;
}
- (BOOL)fetchXSRFToken {
BOOL success = NO;
if (!self.xsrfFetched) { // only fetch token once per session
self.xsrfFetched = YES;
if (!self.syncState.xsrfToken.length) { // only fetch token once per session
NSString *stageName = [@"xsrf" stringByAppendingFormat:@"/%@", self.syncState.machineID];
NSURL *u = [NSURL URLWithString:stageName relativeToURL:self.syncState.syncBaseURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:u];
@@ -199,7 +223,9 @@
[self performRequest:request timeout:10 response:&response error:NULL];
if (response.statusCode == 200) {
NSDictionary *headers = [response allHeaderFields];
self.syncState.xsrfToken = headers[kXSRFToken];
self.syncState.xsrfToken = headers[kDefaultXSRFTokenHeader];
NSString *xsrfTokenHeader = headers[kXSRFTokenHeader];
self.syncState.xsrfTokenHeader = xsrfTokenHeader.length ? xsrfTokenHeader : nil;
SLOGD(@"Retrieved new XSRF token");
success = YES;
} else {

View File

@@ -35,6 +35,9 @@
/// An XSRF token to send in the headers with each request.
@property NSString *xsrfToken;
/// The header name to use when sending the XSRF token back to the server.
@property NSString *xsrfTokenHeader;
/// Full sync interval in seconds, defaults to kDefaultFullSyncInterval. If push notifications are
/// being used this interval will be ignored in favor of pushNotificationsFullSyncInterval.
@property NSUInteger fullSyncInterval;
@@ -71,8 +74,7 @@
/// Array of bundle IDs to find binaries for.
@property NSArray *bundleBinaryRequests;
/// The header value for ContentEncoding when sending compressed content.
/// Either "deflate" (default) or "zlib".
@property(copy) NSString *compressedContentEncoding;
/// The content-encoding to use for the client uploads during the sync session.
@property SNTSyncContentEncoding contentEncoding;
@end

View File

@@ -39,6 +39,10 @@
}
@end
@interface SNTSyncStage (XSSI)
- (NSData *)stripXssi:(NSData *)data;
@end
@interface SNTSyncTest : XCTestCase
@property SNTSyncState *syncState;
@property id<SNTDaemonControlXPC> daemonConnRop;
@@ -156,6 +160,27 @@
#pragma mark - SNTSyncStage Tests
- (void)testStripXssi {
SNTSyncStage *sut = [[SNTSyncStage alloc] initWithState:self.syncState];
char wantChar[3] = {'"', 'a', '"'};
NSData *want = [NSData dataWithBytes:wantChar length:3];
char dOne[8] = {')', ']', '}', '\'', '\n', '"', 'a', '"'};
XCTAssertEqualObjects([sut stripXssi:[NSData dataWithBytes:dOne length:8]], want, @"");
char dTwo[6] = {']', ')', '}', '"', 'a', '"'};
XCTAssertEqualObjects([sut stripXssi:[NSData dataWithBytes:dTwo length:6]], want, @"");
char dThree[5] = {')', ']', '}', '\'', '\n'};
XCTAssertEqualObjects([sut stripXssi:[NSData dataWithBytes:dThree length:5]], [NSData data], @"");
char dFour[3] = {']', ')', '}'};
XCTAssertEqualObjects([sut stripXssi:[NSData dataWithBytes:dFour length:3]], [NSData data], @"");
XCTAssertEqualObjects([sut stripXssi:want], want, @"");
}
- (void)testBaseFetchXSRFTokenSuccess {
// NOTE: This test only works if the other tests don't return a 403 and run before this test.
// The XSRF fetching code is inside a dispatch_once.
@@ -198,6 +223,49 @@
XCTAssertEqualObjects(self.syncState.xsrfToken, @"my-xsrf-token");
}
- (void)testBaseFetchXSRFTokenHeaderRedirect {
// Stub initial failing request
NSURLResponse *resp = [self responseWithCode:403 headerDict:nil];
[self stubRequestBody:nil
response:resp
error:nil
validateBlock:^BOOL(NSURLRequest *req) {
return ([req.URL.absoluteString containsString:@"/a/"] &&
![req valueForHTTPHeaderField:@"X-Client-Xsrf-Token"]);
}];
// Stub XSRF token request
resp = [self responseWithCode:200
headerDict:@{
@"X-XSRF-TOKEN" : @"my-xsrf-token",
@"X-XSRF-TOKEN-HEADER" : @"X-Client-Xsrf-Token",
}];
[self stubRequestBody:nil
response:resp
error:nil
validateBlock:^BOOL(NSURLRequest *req) {
return [req.URL.absoluteString containsString:@"/xsrf/"];
}];
// Stub succeeding request
[self stubRequestBody:nil
response:nil
error:nil
validateBlock:^BOOL(NSURLRequest *req) {
return ([req.URL.absoluteString containsString:@"/a/"] &&
[[req valueForHTTPHeaderField:@"X-CLIENT-XSRF-TOKEN"]
isEqualToString:@"my-xsrf-token"]);
}];
NSString *stageName = [@"a" stringByAppendingFormat:@"/%@", self.syncState.machineID];
NSURL *u1 = [NSURL URLWithString:stageName relativeToURL:self.syncState.syncBaseURL];
SNTSyncStage *sut = [[SNTSyncStage alloc] initWithState:self.syncState];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:u1];
XCTAssertTrue([sut performRequest:req]);
XCTAssertEqualObjects(self.syncState.xsrfToken, @"my-xsrf-token");
}
#pragma mark - SNTSyncPreflight Tests
- (void)testPreflightBasicResponse {

View File

@@ -30,6 +30,7 @@ also known as mobileconfig files, which are in an Apple-specific XML 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. |
@@ -71,6 +72,7 @@ also known as mobileconfig files, which are in an Apple-specific XML format.
| 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. |
| FileAccessPolicyPlist | String | (BETA) Path to a file access configuration plist. |
| FileAccessPolicyUpdateIntervalSec | Integer | (BETA) 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.) |
\*overridable by the sync server: run `santactl status` to check the current

View File

@@ -18,22 +18,24 @@ To enable this feature, the `FileAccessPolicyPlist` key in the main [Santa confi
## Configuration
| Key | Parent | Type | Required | Description |
| ------------------- | ------------ | ---------- | -------- | ----------- |
| `Version` | `<Root>` | String | Yes | Version of the configuration. Will be reported in events. |
| `WatchItems` | `<Root>` | Dictionary | No | The set of configuration items that will be monitored by Santa. |
| `<Name>` | `WatchItems` | Dictionary | No | A unique name that identifies a single watch item rule. This value will be reported in events. The name must be a legal C identifier (i.e., must conform to the regex `[A-Za-z_][A-Za-z0-9_]*`). |
| `Paths` | `<Name>` | Array | Yes | A list of either String or Dictionary types that contain path globs to monitor. String type entires will have default values applied for the attributes that can be manually set with the Dictionary type. |
| `Path` | `Paths` | String | Yes | The path glob to monitor. |
| `IsPrefix` | `Paths` | Boolean | No | Whether or not the path glob represents a prefix path. (Default = `false`) |
| `Options` | `<Name>` | Dictionary | No | Customizes the actions for a given rule. |
| `AllowReadAccess` | `Options` | Boolean | No | If true, indicates the rule will **not** be applied to actions that are read-only access (e.g., opening a watched path for reading, or cloning a watched path). If false, the rule will apply both to read-only access and access that could modify the watched path. (Default = `false`) |
| `AuditOnly` | `Options` | Boolean | No | If true, operations violating the rule will only be logged. If false, operations violating the rule will be denied and logged. (Default = `true`) |
| `Processes` | `<Name>` | Array | No | A list of dictionaries defining processes that are allowed to access paths matching the globs defined with the `Paths` key. For a process performing the operation to be considered a match, it must match all defined attributes of at least one entry in the list. |
| `BinaryPath` | `Processes` | String | No | A path literal that an instigating process must be executed from. |
| `TeamID` | `Processes` | String | No | Team ID of the instigating process. |
| `CertificateSha256` | `Processes` | String | No | SHA256 of the leaf certificate of the instigating process. |
| `CDHash` | `Processes` | String | No | CDHash of the instigating process. |
| Key | Parent | Type | Required | Santa Version | Description |
| :------------------ | :----------- | :--------- | :------- | :------------ | :---------- |
| `Version` | `<Root>` | String | Yes | v2023.1+ | Version of the configuration. Will be reported in events. |
| `WatchItems` | `<Root>` | Dictionary | No | v2023.1+ | The set of configuration items that will be monitored by Santa. |
| `<Name>` | `WatchItems` | Dictionary | No | v2023.1+ | A unique name that identifies a single watch item rule. This value will be reported in events. The name must be a legal C identifier (i.e., must conform to the regex `[A-Za-z_][A-Za-z0-9_]*`). |
| `Paths` | `<Name>` | Array | Yes | v2023.1+ | A list of either String or Dictionary types that contain path globs to monitor. String type entires will have default values applied for the attributes that can be manually set with the Dictionary type. |
| `Path` | `Paths` | String | Yes | v2023.1+ | The path glob to monitor. |
| `IsPrefix` | `Paths` | Boolean | No | v2023.1+ | Whether or not the path glob represents a prefix path. (Default = `false`) |
| `Options` | `<Name>` | Dictionary | No | v2023.1+ | Customizes the actions for a given rule. |
| `AllowReadAccess` | `Options` | Boolean | No | v2023.1+ | If true, indicates the rule will **not** be applied to actions that are read-only access (e.g., opening a watched path for reading, or cloning a watched path). If false, the rule will apply both to read-only access and access that could modify the watched path. (Default = `false`) |
| `AuditOnly` | `Options` | Boolean | No | v2023.1+ | If true, operations violating the rule will only be logged. If false, operations violating the rule will be denied and logged. (Default = `true`) |
| `Processes` | `<Name>` | Array | No | v2023.1+ | A list of dictionaries defining processes that are allowed to access paths matching the globs defined with the `Paths` key. For a process performing the operation to be considered a match, it must match all defined attributes of at least one entry in the list. |
| `BinaryPath` | `Processes` | String | No | v2023.1+ | A path literal that an instigating process must be executed from. |
| `TeamID` | `Processes` | String | No | v2023.1+ | Team ID of the instigating process. |
| `CertificateSha256` | `Processes` | String | No | v2023.1+ | SHA256 of the leaf certificate of the instigating process. |
| `CDHash` | `Processes` | String | No | v2023.1+ | CDHash of the instigating process. |
| `SigningID` | `Processes` | String | No | v2023.1+ | Signing ID of the instigating process. |
| `PlatformBinary` | `Processes` | Boolean | No | v2023.2+ | Whether or not the instigating process is a platform binary. |
### Example Configuration