mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be87b3eaf2 | ||
|
|
0fe672817e | ||
|
|
c3b2fbf512 | ||
|
|
2984d98cb9 | ||
|
|
5295faef0e | ||
|
|
0209344f62 | ||
|
|
53ca5eb811 | ||
|
|
33c7aab9f1 | ||
|
|
f6d837ac31 | ||
|
|
5e0a383662 | ||
|
|
8055b451bb | ||
|
|
c5e7736eef | ||
|
|
61558048c0 | ||
|
|
cf0e3fd3db | ||
|
|
15519c6de8 | ||
|
|
a415679980 | ||
|
|
27ae60e265 | ||
|
|
29a50f072c | ||
|
|
a97e82e316 | ||
|
|
532120ac02 | ||
|
|
ec934854fc | ||
|
|
ad0e2abdac | ||
|
|
dc11ea6534 | ||
|
|
3acf3c1d00 | ||
|
|
41bc3d2542 | ||
|
|
45a5d4e800 | ||
|
|
82bd981f31 | ||
|
|
6480d9c99b | ||
|
|
7e963080b3 | ||
|
|
e58cd7d125 | ||
|
|
db597e413b | ||
|
|
78f46896d5 | ||
|
|
cc0742dbfb | ||
|
|
9c2f76af72 | ||
|
|
a3ed5ccb40 | ||
|
|
b4149816c7 | ||
|
|
2313d6338d | ||
|
|
414fbff721 | ||
|
|
5a2e42e9b4 | ||
|
|
f8d1b2e880 | ||
|
|
5f4d2a92fc | ||
|
|
4ccffdca01 | ||
|
|
e60bbe1b55 | ||
|
|
eee2149439 |
31
.bazelrc
31
.bazelrc
@@ -9,17 +9,34 @@ build --cxxopt=-std=c++17
|
||||
build --copt=-DSANTA_OPEN_SOURCE=1
|
||||
build --cxxopt=-DSANTA_OPEN_SOURCE=1
|
||||
|
||||
build:asan --strip=never
|
||||
build:asan --copt="-Wno-macro-redefined"
|
||||
build:asan --copt="-D_FORTIFY_SOURCE=0"
|
||||
build:asan --copt="-O1"
|
||||
build:asan --copt="-fno-omit-frame-pointer"
|
||||
# Many config options for sanitizers pulled from
|
||||
# https://github.com/protocolbuffers/protobuf/blob/main/.bazelrc
|
||||
build:san-common --strip=never
|
||||
build:san-common --copt="-Wno-macro-redefined"
|
||||
build:san-common --copt="-D_FORTIFY_SOURCE=0"
|
||||
build:san-common --copt="-O1"
|
||||
build:san-common --copt="-fno-omit-frame-pointer"
|
||||
|
||||
build:asan --config=san-common
|
||||
build:asan --copt="-fsanitize=address"
|
||||
build:asan --copt="-DADDRESS_SANITIZER"
|
||||
build:asan --linkopt="-fsanitize=address"
|
||||
build:asan --test_env="ASAN_OPTIONS=log_path=/tmp/san_out"
|
||||
|
||||
build:fuzz --copt="-Wno-macro-redefined"
|
||||
build:fuzz --copt="-D_FORTIFY_SOURCE=0"
|
||||
build:tsan --config=san-common
|
||||
build:tsan --copt="-fsanitize=thread"
|
||||
build:tsan --copt="-DTHREAD_SANITIZER=1"
|
||||
build:tsan --linkopt="-fsanitize=thread"
|
||||
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 --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
|
||||
build:fuzz --@rules_fuzzing//fuzzing:cc_engine_instrumentation=libfuzzer
|
||||
build:fuzz --@rules_fuzzing//fuzzing:cc_engine_sanitizer=asan
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/continuous.yml
vendored
2
.github/workflows/continuous.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/e2e.yml
vendored
4
.github/workflows/e2e.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/fuzz.yml
vendored
4
.github/workflows/fuzz.yml
vendored
@@ -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
|
||||
|
||||
30
.github/workflows/sanitizers.yml
vendored
Normal file
30
.github/workflows/sanitizers.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: sanitizers
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 16 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
sanitizer: [asan, tsan, ubsan]
|
||||
steps:
|
||||
- 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_env=DYLD_INSERT_LIBRARIES=${DYLIB_PATH} \
|
||||
--runs_per_test 5 -t- :unit_tests \
|
||||
--define=SANTA_BUILD_TYPE=adhoc
|
||||
- name: Upload logs
|
||||
uses: actions/upload-artifact@v1
|
||||
if: failure()
|
||||
with:
|
||||
name: logs
|
||||
path: /tmp/san_out*
|
||||
@@ -119,6 +119,10 @@ objc_library(
|
||||
name = "SNTDeviceEvent",
|
||||
srcs = ["SNTDeviceEvent.m"],
|
||||
hdrs = ["SNTDeviceEvent.h"],
|
||||
module_name = "santa_common_SNTDeviceEvent",
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
],
|
||||
deps = [
|
||||
":SNTCommonEnums",
|
||||
],
|
||||
@@ -133,6 +137,10 @@ objc_library(
|
||||
name = "SNTConfigurator",
|
||||
srcs = ["SNTConfigurator.m"],
|
||||
hdrs = ["SNTConfigurator.h"],
|
||||
module_name = "santa_common_SNTConfigurator",
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
],
|
||||
deps = [
|
||||
":SNTCommonEnums",
|
||||
":SNTRule",
|
||||
@@ -200,6 +208,9 @@ objc_library(
|
||||
name = "SNTRule",
|
||||
srcs = ["SNTRule.m"],
|
||||
hdrs = ["SNTRule.h"],
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
],
|
||||
deps = [
|
||||
":SNTCommonEnums",
|
||||
":SNTSyncConstants",
|
||||
@@ -231,13 +242,19 @@ objc_library(
|
||||
name = "SNTSyncConstants",
|
||||
srcs = ["SNTSyncConstants.m"],
|
||||
hdrs = ["SNTSyncConstants.h"],
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTSystemInfo",
|
||||
srcs = ["SNTSystemInfo.m"],
|
||||
hdrs = ["SNTSystemInfo.h"],
|
||||
sdk_frameworks = ["IOKit"],
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
"IOKit",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
|
||||
@@ -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), ^{
|
||||
|
||||
@@ -194,6 +194,13 @@
|
||||
///
|
||||
@property(readonly, nonatomic) SNTEventLogType eventLogType;
|
||||
|
||||
///
|
||||
/// Returns the raw value of the EventLogType configuration key instead of being
|
||||
/// converted to the SNTEventLogType enum. If the key is not set, the default log
|
||||
/// type is returned.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *eventLogTypeRaw;
|
||||
|
||||
///
|
||||
/// If eventLogType is set to Filelog, eventLogPath will provide the path to save logs.
|
||||
/// Defaults to /var/db/santa/santa.log.
|
||||
@@ -238,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;
|
||||
|
||||
|
||||
@@ -97,6 +97,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";
|
||||
|
||||
@@ -211,6 +212,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kSpoolDirectoryFileSizeThresholdKB : number,
|
||||
kSpoolDirectorySizeThresholdMB : number,
|
||||
kSpoolDirectoryEventMaxFlushTimeSec : number,
|
||||
kFileAccessPolicy : dictionary,
|
||||
kFileAccessPolicyPlist : string,
|
||||
kFileAccessPolicyUpdateIntervalSec : number,
|
||||
kEnableMachineIDDecoration : number,
|
||||
@@ -242,7 +244,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
|
||||
#pragma mark Singleton retriever
|
||||
|
||||
+ (instancetype)configurator {
|
||||
// The returned value is marked unsafe_unretained to avoid unnecessary retain/release handling.
|
||||
// The object returned is guaranteed to exist for the lifetime of the process so there's no need
|
||||
// to do this handling.
|
||||
+ (__unsafe_unretained instancetype)configurator {
|
||||
static SNTConfigurator *sharedConfigurator;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@@ -416,6 +421,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicy {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingFileAccessPolicyPlist {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -776,6 +785,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)eventLogTypeRaw {
|
||||
return self.configState[kEventLogType] ?: @"file";
|
||||
}
|
||||
|
||||
- (NSString *)eventLogPath {
|
||||
return self.configState[kEventLogPath] ?: @"/var/db/santa/santa.log";
|
||||
}
|
||||
@@ -802,8 +815,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 {
|
||||
@@ -1113,6 +1135,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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -28,12 +28,16 @@ typedef struct SantaVnode {
|
||||
return fsid == rhs.fsid && fileid == rhs.fileid;
|
||||
}
|
||||
|
||||
static inline SantaVnode VnodeForFile(const es_file_t *es_file) {
|
||||
static inline SantaVnode VnodeForFile(const struct stat &sb) {
|
||||
return SantaVnode{
|
||||
.fsid = es_file->stat.st_dev,
|
||||
.fileid = es_file->stat.st_ino,
|
||||
.fsid = sb.st_dev,
|
||||
.fileid = sb.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
static inline SantaVnode VnodeForFile(const es_file_t *es_file) {
|
||||
return VnodeForFile(es_file->stat);
|
||||
}
|
||||
#endif
|
||||
} SantaVnode;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
//
|
||||
// !!! WARNING !!!
|
||||
// This proto is for demonstration purposes only and will be changing.
|
||||
// Do not rely on this format.
|
||||
//
|
||||
// Important: This schema is currently in BETA
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
@@ -235,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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
load("@build_bazel_rules_apple//apple:macos.bzl", "macos_application")
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
|
||||
licenses(["notice"])
|
||||
@@ -11,6 +12,25 @@ exports_files([
|
||||
"Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-256.png",
|
||||
])
|
||||
|
||||
swift_library(
|
||||
name = "SNTAboutWindowView",
|
||||
srcs = ["SNTAboutWindowView.swift"],
|
||||
generates_header = 1,
|
||||
deps = ["//Source/common:SNTConfigurator"],
|
||||
)
|
||||
|
||||
swift_library(
|
||||
name = "SNTDeviceMessageWindowView",
|
||||
srcs = [
|
||||
"SNTDeviceMessageWindowView.swift",
|
||||
],
|
||||
generates_header = 1,
|
||||
deps = [
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SantaGUI_lib",
|
||||
srcs = [
|
||||
@@ -24,8 +44,6 @@ objc_library(
|
||||
"SNTBinaryMessageWindowController.m",
|
||||
"SNTDeviceMessageWindowController.h",
|
||||
"SNTDeviceMessageWindowController.m",
|
||||
"SNTMessageWindow.h",
|
||||
"SNTMessageWindow.m",
|
||||
"SNTMessageWindowController.h",
|
||||
"SNTMessageWindowController.m",
|
||||
"SNTNotificationManager.h",
|
||||
@@ -36,8 +54,6 @@ objc_library(
|
||||
"SNTNotificationManager.h",
|
||||
],
|
||||
data = [
|
||||
"Resources/AboutWindow.xib",
|
||||
"Resources/DeviceMessageWindow.xib",
|
||||
"Resources/MessageWindow.xib",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
@@ -47,6 +63,8 @@ objc_library(
|
||||
"UserNotifications",
|
||||
],
|
||||
deps = [
|
||||
":SNTAboutWindowView",
|
||||
":SNTDeviceMessageWindowView",
|
||||
"//Source/common:SNTBlockMessage_SantaGUI",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTDeviceEvent",
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="SNTAboutWindowController">
|
||||
<connections>
|
||||
<outlet property="aboutTextField" destination="uh6-q0-RzL" id="oGn-hV-wwc"/>
|
||||
<outlet property="moreInfoButton" destination="SRu-Kf-vu5" id="Vj2-9Q-05d"/>
|
||||
<outlet property="window" destination="F0z-JX-Cv5" id="gIp-Ho-8D9"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Santa" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="F0z-JX-Cv5">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="200"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
|
||||
<view key="contentView" id="se5-gp-TjO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="200"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="BnL-ZS-kXw">
|
||||
<rect key="frame" x="199" y="140" width="83" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="40" id="UK2-2L-lPx"/>
|
||||
<constraint firstAttribute="width" constant="79" id="lDf-D7-qlY"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Santa" id="VVj-gU-bzy">
|
||||
<font key="font" size="34" name="HelveticaNeue-UltraLight"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uh6-q0-RzL">
|
||||
<rect key="frame" x="18" y="65" width="444" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<textFieldCell key="cell" sendsActionOnEndEditing="YES" alignment="center" id="CcT-ul-1eA">
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="title">Santa is an application control system for macOS.
|
||||
|
||||
There are no user-configurable settings.</string>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SRu-Kf-vu5">
|
||||
<rect key="frame" x="129" y="21" width="113" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="99" id="JHv-2J-QSe"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="More Info..." bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6fe-ju-aET">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="openMoreInfoURL:" target="-2" id="dps-TN-rkS"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Udo-BY-n7e">
|
||||
<rect key="frame" x="239" y="21" width="113" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="99" id="2Xc-ax-2bV"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="Dismiss" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="uSw-o1-lWW">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
DQ
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="orderOut:" target="-1" id="6oW-zI-zn5"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Udo-BY-n7e" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" priority="900" constant="191" id="1T4-DB-Dz8"/>
|
||||
<constraint firstItem="SRu-Kf-vu5" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="136" id="Ake-nU-qhW"/>
|
||||
<constraint firstItem="BnL-ZS-kXw" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" constant="20" symbolic="YES" id="Fj1-SG-mzF"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Udo-BY-n7e" secondAttribute="bottom" constant="28" id="bpF-hC-haN"/>
|
||||
<constraint firstAttribute="bottom" secondItem="SRu-Kf-vu5" secondAttribute="bottom" constant="28" id="fCB-02-SEt"/>
|
||||
<constraint firstItem="BnL-ZS-kXw" firstAttribute="centerX" secondItem="se5-gp-TjO" secondAttribute="centerX" id="kez-S0-6Gg"/>
|
||||
<constraint firstItem="Udo-BY-n7e" firstAttribute="leading" secondItem="SRu-Kf-vu5" secondAttribute="trailing" constant="11" id="sYO-yY-w9w"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="-2" id="0bl-1N-AYu"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="323" y="317"/>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -1,193 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="SNTDeviceMessageWindowController">
|
||||
<connections>
|
||||
<outlet property="window" destination="9Bq-yh-54f" id="Uhs-WF-TV9"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Santa Blocked Execution" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" visibleAtLaunch="NO" animationBehavior="none" id="9Bq-yh-54f" customClass="SNTMessageWindow">
|
||||
<windowStyleMask key="styleMask" utility="YES"/>
|
||||
<rect key="contentRect" x="167" y="107" width="515" height="318"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
|
||||
<view key="contentView" id="Iwq-Lx-rLv">
|
||||
<rect key="frame" x="0.0" y="0.0" width="515" height="318"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<userGuides>
|
||||
<userLayoutGuide location="344" affinity="minX"/>
|
||||
</userGuides>
|
||||
<subviews>
|
||||
<button focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kiB-jZ-69S">
|
||||
<rect key="frame" x="16" y="290" width="37" height="32"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<buttonCell key="cell" type="push" title="Hidden Button" alternateTitle="This button exists so neither of the other two buttons is pre-selected when the dialog opens." bezelStyle="rounded" alignment="center" borderStyle="border" focusRingType="none" transparent="YES" imageScaling="proportionallyDown" inset="2" id="XGa-Sl-F4t">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</buttonCell>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="cD5-Su-lXR" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="31" y="210" width="454" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="450" id="XgJ-EV-tBa"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" selectable="YES" refusesFirstResponder="YES" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="center" title="A message to the user goes here..." allowsEditingTextAttributes="YES" id="5tH-bG-UJA">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.attributedCustomMessage" id="376-sj-4Q1"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="d9e-Wv-Y5H" userLabel="Label: Path">
|
||||
<rect key="frame" x="8" y="139" width="142" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="138" id="Kqd-nX-7df"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Device Name" id="KgY-X1-ESG">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YNz-ka-cBi" userLabel="Label: Path">
|
||||
<rect key="frame" x="8" y="115" width="142" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="138" id="3wU-P0-gAC"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Device BSD Path" id="adC-be-Beh">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField toolTip="Device Name" verticalHuggingPriority="900" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" ambiguous="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="pc8-G9-4pJ" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="187" y="139" width="315" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="311" id="xVR-j3-dLw"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" title="Device Name" id="E7T-9h-ofr">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.event.mntonname" id="bOu-gv-1Vh"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField toolTip="Device BSD Path" verticalHuggingPriority="900" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="1000" ambiguous="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="bDE-Tl-UHg" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="187" y="115" width="315" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="311" id="p1W-f9-KBX"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" title="Device BSD Path" id="H1b-Ui-CYo">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.event.mntfromname" id="Sry-KY-HDb"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="t8c-Fx-e5h">
|
||||
<rect key="frame" x="217" y="248" width="82" height="40"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" refusesFirstResponder="YES" sendsActionOnEndEditing="YES" title="Santa" id="7YA-iB-Zma">
|
||||
<font key="font" metaFont="systemUltraLight" size="34"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField verticalHuggingPriority="750" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="i8e-8n-tZS" userLabel="Label: Path">
|
||||
<rect key="frame" x="8" y="91" width="142" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="138" id="a3r-ng-zxJ"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Remount Mode" id="4D0-8L-ihK">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="boolean" keyPath="accessibilityElement" value="NO"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField toolTip="Device BSD Path" verticalHuggingPriority="750" ambiguous="YES" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kmz-be-oWf" customClass="SNTAccessibleTextField">
|
||||
<rect key="frame" x="187" y="91" width="315" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="311" id="cbM-i5-bJ9"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" selectable="YES" sendsActionOnEndEditing="YES" alignment="left" title="Remount Mode" id="qI0-Na-kxN">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
<connections>
|
||||
<binding destination="-2" name="value" keyPath="self.event.readableRemountArgs" id="bOu-gv-1Vl"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="BbV-3h-mmL" userLabel="Dismiss Button">
|
||||
<rect key="frame" x="202" y="18" width="112" height="25"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="110" id="6Uh-Bd-N64"/>
|
||||
<constraint firstAttribute="height" constant="22" id="GH6-nw-6rD"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="roundTextured" title="Ok" bezelStyle="texturedRounded" alignment="center" state="on" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="XR6-Xa-gP4">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
DQ
|
||||
</string>
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES"/>
|
||||
</buttonCell>
|
||||
<accessibility description="Dismiss Dialog"/>
|
||||
<connections>
|
||||
<action selector="closeWindow:" target="-2" id="qQq-gh-8lw"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="BbV-3h-mmL" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" priority="500" constant="193" id="2uo-Cm-Tfp"/>
|
||||
<constraint firstItem="kmz-be-oWf" firstAttribute="height" secondItem="i8e-8n-tZS" secondAttribute="height" id="6Ow-Sl-6Wc"/>
|
||||
<constraint firstItem="d9e-Wv-Y5H" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="FPe-Rd-G4n"/>
|
||||
<constraint firstItem="t8c-Fx-e5h" firstAttribute="top" secondItem="Iwq-Lx-rLv" secondAttribute="top" constant="30" id="FuB-GX-0jg"/>
|
||||
<constraint firstItem="YNz-ka-cBi" firstAttribute="leading" secondItem="Iwq-Lx-rLv" secondAttribute="leading" constant="10" id="KmX-kX-VCN"/>
|
||||
<constraint firstItem="pc8-G9-4pJ" firstAttribute="top" secondItem="cD5-Su-lXR" secondAttribute="bottom" priority="950" constant="55" id="Nsl-zf-poH"/>
|
||||
<constraint firstItem="YNz-ka-cBi" firstAttribute="centerY" secondItem="bDE-Tl-UHg" secondAttribute="centerY" id="ObQ-RA-S5V"/>
|
||||
<constraint firstItem="d9e-Wv-Y5H" firstAttribute="centerY" secondItem="pc8-G9-4pJ" secondAttribute="centerY" id="SLv-F7-w5k"/>
|
||||
<constraint firstAttribute="centerX" secondItem="cD5-Su-lXR" secondAttribute="centerX" id="V0a-Py-iEc"/>
|
||||
<constraint firstItem="cD5-Su-lXR" firstAttribute="top" secondItem="t8c-Fx-e5h" secondAttribute="bottom" constant="22" id="dYg-zP-wh2"/>
|
||||
<constraint firstAttribute="centerX" secondItem="t8c-Fx-e5h" secondAttribute="centerX" id="h3d-Kc-q88"/>
|
||||
<constraint firstItem="kmz-be-oWf" firstAttribute="centerY" secondItem="i8e-8n-tZS" secondAttribute="centerY" id="ilC-5x-LNe"/>
|
||||
<constraint firstItem="bDE-Tl-UHg" firstAttribute="top" secondItem="pc8-G9-4pJ" secondAttribute="bottom" constant="8" id="pis-of-f93"/>
|
||||
<constraint firstItem="i8e-8n-tZS" firstAttribute="centerY" secondItem="YNz-ka-cBi" secondAttribute="centerY" constant="24" id="uZs-XT-PuZ"/>
|
||||
<constraint firstAttribute="bottom" secondItem="BbV-3h-mmL" secondAttribute="bottom" constant="35" id="ukF-FH-DE8"/>
|
||||
<constraint firstItem="YNz-ka-cBi" firstAttribute="firstBaseline" secondItem="d9e-Wv-Y5H" secondAttribute="baseline" constant="24" id="xZu-AY-wX5"/>
|
||||
<constraint firstItem="i8e-8n-tZS" firstAttribute="top" secondItem="YNz-ka-cBi" secondAttribute="bottom" constant="8" symbolic="YES" id="ylw-3s-9JW"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="initialFirstResponder" destination="kiB-jZ-69S" id="I96-dS-lq5"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="261.5" y="246"/>
|
||||
</window>
|
||||
<userDefaultsController representsSharedInstance="YES" id="iXx-cu-WYe"/>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -14,11 +14,5 @@
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface SNTAboutWindowController : NSWindowController
|
||||
|
||||
@property IBOutlet NSTextField *aboutTextField;
|
||||
@property IBOutlet NSButton *moreInfoButton;
|
||||
|
||||
- (IBAction)openMoreInfoURL:(id)sender;
|
||||
|
||||
@interface SNTAboutWindowController : NSWindowController <NSWindowDelegate>
|
||||
@end
|
||||
|
||||
@@ -13,30 +13,35 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/gui/SNTAboutWindowController.h"
|
||||
#import "Source/gui/SNTAboutWindowView-Swift.h"
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
|
||||
@implementation SNTAboutWindowController
|
||||
|
||||
- (instancetype)init {
|
||||
return [super initWithWindowNibName:@"AboutWindow"];
|
||||
- (void)showWindow:(id)sender {
|
||||
[super showWindow:sender];
|
||||
|
||||
if (self.window) [self.window orderOut:sender];
|
||||
|
||||
self.window =
|
||||
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 0, 0)
|
||||
styleMask:NSWindowStyleMaskClosable | NSWindowStyleMaskTitled
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
self.window.contentViewController = [SNTAboutWindowViewFactory createWithWindow:self.window];
|
||||
self.window.title = @"Santa";
|
||||
self.window.delegate = self;
|
||||
[self.window makeKeyAndOrderFront:nil];
|
||||
[self.window center];
|
||||
|
||||
// Add app to Cmd+Tab and Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyRegular;
|
||||
}
|
||||
|
||||
- (void)loadWindow {
|
||||
[super loadWindow];
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
NSString *aboutText = [config aboutText];
|
||||
if (aboutText) {
|
||||
[self.aboutTextField setStringValue:aboutText];
|
||||
}
|
||||
if (![config moreInfoURL]) {
|
||||
[self.moreInfoButton removeFromSuperview];
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)openMoreInfoURL:(id)sender {
|
||||
[[NSWorkspace sharedWorkspace] openURL:[[SNTConfigurator configurator] moreInfoURL]];
|
||||
[self close];
|
||||
- (void)windowWillClose:(NSNotification *)notification {
|
||||
// Remove app from Cmd+Tab and Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyAccessory;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
63
Source/gui/SNTAboutWindowView.swift
Normal file
63
Source/gui/SNTAboutWindowView.swift
Normal file
@@ -0,0 +1,63 @@
|
||||
import SwiftUI
|
||||
|
||||
import santa_common_SNTConfigurator
|
||||
|
||||
@objc public class SNTAboutWindowViewFactory : NSObject {
|
||||
@objc public static func createWith(window: NSWindow) -> NSViewController {
|
||||
return NSHostingController(rootView:SNTAboutWindowView(w:window).frame(width:400, height:200))
|
||||
}
|
||||
}
|
||||
|
||||
struct SNTAboutWindowView: View {
|
||||
let w: NSWindow?
|
||||
let c = SNTConfigurator()
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing:20.0) {
|
||||
Text("Santa").font(Font.custom("HelveticaNeue-UltraLight", size: 34.0))
|
||||
|
||||
if let t = c.aboutText {
|
||||
Text(t).multilineTextAlignment(.center)
|
||||
} else {
|
||||
Text("""
|
||||
Santa is an application control system for macOS.
|
||||
|
||||
There are no user-configurable settings.
|
||||
""").multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
HStack {
|
||||
if c.moreInfoURL?.absoluteString.isEmpty == false {
|
||||
Button(action: moreInfoButton) {
|
||||
Text("More Info...").frame(width: 90.0)
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: dismissButton) {
|
||||
Text("Dismiss").frame(width: 90.0)
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
|
||||
}.padding(10.0)
|
||||
}
|
||||
}
|
||||
|
||||
func dismissButton() {
|
||||
w?.close()
|
||||
}
|
||||
|
||||
func moreInfoButton() {
|
||||
if let u = c.moreInfoURL {
|
||||
NSWorkspace.shared.open(u)
|
||||
}
|
||||
w?.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Enable previews in Xcode.
|
||||
struct SNTAboutWindow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SNTAboutWindowView(w: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@
|
||||
}
|
||||
|
||||
- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag {
|
||||
self.aboutWindowController = [[SNTAboutWindowController alloc] init];
|
||||
if (!self.aboutWindowController) {
|
||||
self.aboutWindowController = [[SNTAboutWindowController alloc] init];
|
||||
}
|
||||
[self.aboutWindowController showWindow:self];
|
||||
return NO;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/gui/SNTMessageWindow.h"
|
||||
|
||||
@interface SNTBinaryMessageWindowController ()
|
||||
/// The custom message to display for this event
|
||||
|
||||
@@ -23,10 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
///
|
||||
/// Controller for a single message window.
|
||||
///
|
||||
@interface SNTDeviceMessageWindowController : SNTMessageWindowController
|
||||
|
||||
@property(weak) IBOutlet NSTextField *remountArgsLabel;
|
||||
@property(weak) IBOutlet NSTextField *remountArgsTitle;
|
||||
@interface SNTDeviceMessageWindowController : SNTMessageWindowController <NSWindowDelegate>
|
||||
|
||||
// The device event this window is for.
|
||||
@property(readonly) SNTDeviceEvent *event;
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/gui/SNTDeviceMessageWindowController.h"
|
||||
#import "Source/gui/SNTDeviceMessageWindowView-Swift.h"
|
||||
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/gui/SNTMessageWindow.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@implementation SNTDeviceMessageWindowController
|
||||
|
||||
- (instancetype)initWithEvent:(SNTDeviceEvent *)event message:(nullable NSString *)message {
|
||||
self = [super initWithWindowNibName:@"DeviceMessageWindow"];
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_event = event;
|
||||
_customMessage = message;
|
||||
@@ -36,12 +36,30 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)loadWindow {
|
||||
[super loadWindow];
|
||||
if (!self.event.remountArgs || [self.event.remountArgs count] <= 0) {
|
||||
[self.remountArgsLabel removeFromSuperview];
|
||||
[self.remountArgsTitle removeFromSuperview];
|
||||
}
|
||||
- (void)showWindow:(id)sender {
|
||||
if (self.window) [self.window orderOut:sender];
|
||||
|
||||
self.window =
|
||||
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 0, 0)
|
||||
styleMask:NSWindowStyleMaskClosable | NSWindowStyleMaskTitled
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
self.window.contentViewController =
|
||||
[SNTDeviceMessageWindowViewFactory createWithWindow:self.window
|
||||
event:self.event
|
||||
customMsg:self.attributedCustomMessage];
|
||||
self.window.delegate = self;
|
||||
|
||||
// Add app to Cmd+Tab and Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyRegular;
|
||||
|
||||
[super showWindow:sender];
|
||||
}
|
||||
|
||||
- (void)windowWillClose:(NSNotification *)notification {
|
||||
// Remove app from Cmd+Tab and Dock.
|
||||
NSApp.activationPolicy = NSApplicationActivationPolicyAccessory;
|
||||
[super windowWillClose:notification];
|
||||
}
|
||||
|
||||
- (NSAttributedString *)attributedCustomMessage {
|
||||
|
||||
76
Source/gui/SNTDeviceMessageWindowView.swift
Normal file
76
Source/gui/SNTDeviceMessageWindowView.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
import SwiftUI
|
||||
|
||||
import santa_common_SNTConfigurator
|
||||
import santa_common_SNTDeviceEvent
|
||||
|
||||
@objc public class SNTDeviceMessageWindowViewFactory : NSObject {
|
||||
@objc public static func createWith(window: NSWindow, event: SNTDeviceEvent, customMsg: NSAttributedString?) -> NSViewController {
|
||||
return NSHostingController(rootView:SNTDeviceMessageWindowView(window:window, event:event, customMsg:customMsg).frame(width:450, height:300))
|
||||
}
|
||||
}
|
||||
|
||||
struct SNTDeviceMessageWindowView: View {
|
||||
let window: NSWindow?
|
||||
let event: SNTDeviceEvent?
|
||||
let customMsg: NSAttributedString?
|
||||
|
||||
let c = SNTConfigurator()
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing:20.0) {
|
||||
Text("Santa").font(Font.custom("HelveticaNeue-UltraLight", size: 34.0))
|
||||
|
||||
if let t = customMsg {
|
||||
if #available(macOS 12.0, *) {
|
||||
let a = AttributedString(t)
|
||||
Text(a).multilineTextAlignment(.center).padding(15.0)
|
||||
} else {
|
||||
Text(t.description).multilineTextAlignment(.center).padding(15.0)
|
||||
}
|
||||
} else {
|
||||
Text("Mounting devices is blocked")
|
||||
}
|
||||
|
||||
HStack(spacing:5.0) {
|
||||
VStack(alignment: .trailing, spacing: 8.0) {
|
||||
Text("Device Name").bold()
|
||||
Text("Device BSD Path").bold()
|
||||
|
||||
if event!.remountArgs.count > 0 {
|
||||
Text("Remount Mode").bold()
|
||||
}
|
||||
}
|
||||
Spacer().frame(width: 10.0)
|
||||
VStack(alignment: .leading, spacing: 8.0) {
|
||||
Text(event!.mntonname)
|
||||
Text(event!.mntfromname)
|
||||
|
||||
if event!.remountArgs.count > 0 {
|
||||
Text(event!.readableRemountArgs())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Button(action: dismissButton) {
|
||||
Text("OK").frame(width: 90.0)
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
|
||||
}.padding(10.0)
|
||||
}
|
||||
}
|
||||
|
||||
func dismissButton() {
|
||||
window?.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Enable previews in Xcode.
|
||||
struct SNTDeviceMessageWindowView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SNTDeviceMessageWindowView(window: nil, event: nil, customMsg: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- (void)windowDidCloseSilenceHash:(NSString *)hash;
|
||||
@end
|
||||
|
||||
@interface SNTMessageWindowController : NSWindowController
|
||||
@interface SNTMessageWindowController : NSWindowController <NSWindowDelegate>
|
||||
|
||||
- (IBAction)showWindow:(id)sender;
|
||||
- (IBAction)closeWindow:(id)sender;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
#import "Source/gui/SNTMessageWindowController.h"
|
||||
|
||||
#import "Source/gui/SNTMessageWindow.h"
|
||||
|
||||
@implementation SNTMessageWindowController
|
||||
|
||||
- (IBAction)showWindow:(id)sender {
|
||||
[(SNTMessageWindow *)self.window fadeIn:sender];
|
||||
[self.window setLevel:NSPopUpMenuWindowLevel];
|
||||
[self.window setMovableByWindowBackground:YES];
|
||||
[self.window makeKeyAndOrderFront:sender];
|
||||
[self.window center];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
}
|
||||
- (IBAction)closeWindow:(id)sender {
|
||||
[(SNTMessageWindow *)self.window fadeOut:sender];
|
||||
[self windowWillClose:sender];
|
||||
[self.window close];
|
||||
}
|
||||
|
||||
- (void)windowWillClose:(NSNotification *)notification {
|
||||
@@ -21,12 +24,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)loadWindow {
|
||||
[super loadWindow];
|
||||
[self.window setLevel:NSPopUpMenuWindowLevel];
|
||||
[self.window setMovableByWindowBackground:YES];
|
||||
}
|
||||
|
||||
- (NSString *)messageHash {
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
return nil;
|
||||
|
||||
@@ -40,27 +40,27 @@ REGISTER_COMMAND_NAME(@"checkcache")
|
||||
}
|
||||
|
||||
+ (NSString *)shortHelpText {
|
||||
return @"Prints the status of a file in the kernel cache.";
|
||||
return @"Prints the status of a file in the cache.";
|
||||
}
|
||||
|
||||
+ (NSString *)longHelpText {
|
||||
return (@"Checks the in-kernel cache for desired file.\n"
|
||||
return (@"Checks the cache for desired file.\n"
|
||||
@"Returns 0 if successful, 1 otherwise");
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
SantaVnode vnodeID = [self vnodeIDForFile:arguments.firstObject];
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
[[self.daemonConn synchronousRemoteObjectProxy]
|
||||
checkCacheForVnodeID:vnodeID
|
||||
withReply:^(SNTAction action) {
|
||||
if (action == SNTActionRespondAllow) {
|
||||
LOGI(@"File exists in [allowlist] kernel cache");
|
||||
LOGI(@"File exists in [allowlist] cache");
|
||||
exit(0);
|
||||
} else if (action == SNTActionRespondDeny) {
|
||||
LOGI(@"File exists in [blocklist] kernel cache");
|
||||
LOGI(@"File exists in [blocklist] cache");
|
||||
exit(0);
|
||||
} else if (action == SNTActionRespondAllowCompiler) {
|
||||
LOGI(@"File exists in [allowlist compiler] kernel cache");
|
||||
LOGI(@"File exists in [allowlist compiler] cache");
|
||||
exit(0);
|
||||
} else if (action == SNTActionUnset) {
|
||||
LOGE(@"File does not exist in cache");
|
||||
|
||||
@@ -166,19 +166,10 @@ REGISTER_COMMAND_NAME(@"metrics")
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
__block NSDictionary *metrics;
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_group_enter(group);
|
||||
|
||||
[[self.daemonConn remoteObjectProxy] metrics:^(NSDictionary *exportedMetrics) {
|
||||
[[self.daemonConn synchronousRemoteObjectProxy] metrics:^(NSDictionary *exportedMetrics) {
|
||||
metrics = exportedMetrics;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
// Wait a maximum of 5s for metrics collected from daemon to arrive.
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5))) {
|
||||
fprintf(stderr, "Failed to retrieve metrics from daemon\n\n");
|
||||
}
|
||||
|
||||
metrics = [self filterMetrics:metrics withArguments:arguments];
|
||||
|
||||
[self prettyPrintMetrics:metrics asJSON:[arguments containsObject:@"--json"]];
|
||||
|
||||
@@ -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];
|
||||
@@ -210,87 +216,65 @@ REGISTER_COMMAND_NAME(@"rule")
|
||||
}
|
||||
|
||||
- (void)printStateOfRule:(SNTRule *)rule daemonConnection:(MOLXPCConnection *)daemonConn {
|
||||
id<SNTDaemonControlXPC> rop = [daemonConn synchronousRemoteObjectProxy];
|
||||
NSString *fileSHA256 = (rule.type == SNTRuleTypeBinary) ? rule.identifier : nil;
|
||||
NSString *certificateSHA256 = (rule.type == SNTRuleTypeCertificate) ? rule.identifier : nil;
|
||||
NSString *teamID = (rule.type == SNTRuleTypeTeamID) ? rule.identifier : nil;
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_group_enter(group);
|
||||
__block NSMutableString *output;
|
||||
[[daemonConn remoteObjectProxy] decisionForFilePath:nil
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
reply:^(SNTEventState s) {
|
||||
output = (SNTEventStateAllow & s)
|
||||
? @"Allowed".mutableCopy
|
||||
: @"Blocked".mutableCopy;
|
||||
switch (s) {
|
||||
case SNTEventStateAllowUnknown:
|
||||
case SNTEventStateBlockUnknown:
|
||||
[output appendString:@" (Unknown)"];
|
||||
break;
|
||||
case SNTEventStateAllowBinary:
|
||||
case SNTEventStateBlockBinary:
|
||||
[output appendString:@" (Binary)"];
|
||||
break;
|
||||
case SNTEventStateAllowCertificate:
|
||||
case SNTEventStateBlockCertificate:
|
||||
[output appendString:@" (Certificate)"];
|
||||
break;
|
||||
case SNTEventStateAllowScope:
|
||||
case SNTEventStateBlockScope:
|
||||
[output appendString:@" (Scope)"];
|
||||
break;
|
||||
case SNTEventStateAllowCompiler:
|
||||
[output appendString:@" (Compiler)"];
|
||||
break;
|
||||
case SNTEventStateAllowTransitive:
|
||||
[output appendString:@" (Transitive)"];
|
||||
break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
case SNTEventStateBlockTeamID:
|
||||
[output appendString:@" (TeamID)"];
|
||||
break;
|
||||
default: output = @"None".mutableCopy; break;
|
||||
}
|
||||
if (isatty(STDOUT_FILENO)) {
|
||||
if ((SNTEventStateAllow & s)) {
|
||||
[output insertString:@"\033[32m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else if ((SNTEventStateBlock & s)) {
|
||||
[output insertString:@"\033[31m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else {
|
||||
[output insertString:@"\033[33m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
}
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
printf("Cannot communicate with daemon");
|
||||
exit(1);
|
||||
}
|
||||
[rop decisionForFilePath:nil
|
||||
fileSHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
reply:^(SNTEventState s) {
|
||||
output =
|
||||
(SNTEventStateAllow & s) ? @"Allowed".mutableCopy : @"Blocked".mutableCopy;
|
||||
switch (s) {
|
||||
case SNTEventStateAllowUnknown:
|
||||
case SNTEventStateBlockUnknown: [output appendString:@" (Unknown)"]; break;
|
||||
case SNTEventStateAllowBinary:
|
||||
case SNTEventStateBlockBinary: [output appendString:@" (Binary)"]; break;
|
||||
case SNTEventStateAllowCertificate:
|
||||
case SNTEventStateBlockCertificate:
|
||||
[output appendString:@" (Certificate)"];
|
||||
break;
|
||||
case SNTEventStateAllowScope:
|
||||
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
|
||||
case SNTEventStateAllowCompiler:
|
||||
[output appendString:@" (Compiler)"];
|
||||
break;
|
||||
case SNTEventStateAllowTransitive:
|
||||
[output appendString:@" (Transitive)"];
|
||||
break;
|
||||
case SNTEventStateAllowTeamID:
|
||||
case SNTEventStateBlockTeamID: [output appendString:@" (TeamID)"]; break;
|
||||
default: output = @"None".mutableCopy; break;
|
||||
}
|
||||
if (isatty(STDOUT_FILENO)) {
|
||||
if ((SNTEventStateAllow & s)) {
|
||||
[output insertString:@"\033[32m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else if ((SNTEventStateBlock & s)) {
|
||||
[output insertString:@"\033[31m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
} else {
|
||||
[output insertString:@"\033[33m" atIndex:0];
|
||||
[output appendString:@"\033[0m"];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
dispatch_group_enter(group);
|
||||
[[daemonConn remoteObjectProxy]
|
||||
databaseRuleForBinarySHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
reply:^(SNTRule *r) {
|
||||
if (r.state == SNTRuleStateAllowTransitive) {
|
||||
NSDate *date =
|
||||
[NSDate dateWithTimeIntervalSinceReferenceDate:r.timestamp];
|
||||
[output
|
||||
appendString:[NSString stringWithFormat:@"\nlast access date: %@",
|
||||
[date description]]];
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
|
||||
printf("Cannot communicate with daemon");
|
||||
exit(1);
|
||||
}
|
||||
[rop databaseRuleForBinarySHA256:fileSHA256
|
||||
certificateSHA256:certificateSHA256
|
||||
teamID:teamID
|
||||
reply:^(SNTRule *r) {
|
||||
if (r.state == SNTRuleStateAllowTransitive) {
|
||||
NSDate *date =
|
||||
[NSDate dateWithTimeIntervalSinceReferenceDate:r.timestamp];
|
||||
[output appendString:[NSString
|
||||
stringWithFormat:@"\nlast access date: %@",
|
||||
[date description]]];
|
||||
}
|
||||
}];
|
||||
|
||||
printf("%s\n", output.UTF8String);
|
||||
exit(0);
|
||||
|
||||
@@ -46,116 +46,94 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
|
||||
|
||||
// Daemon status
|
||||
__block NSString *clientMode;
|
||||
__block uint64_t cpuEvents, ramEvents;
|
||||
__block double cpuPeak, ramPeak;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] clientMode:^(SNTClientMode cm) {
|
||||
[rop clientMode:^(SNTClientMode cm) {
|
||||
switch (cm) {
|
||||
case SNTClientModeMonitor: clientMode = @"Monitor"; break;
|
||||
case SNTClientModeLockdown: clientMode = @"Lockdown"; break;
|
||||
default: clientMode = [NSString stringWithFormat:@"Unknown (%ld)", cm]; break;
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] watchdogInfo:^(uint64_t wd_cpuEvents, uint64_t wd_ramEvents,
|
||||
double wd_cpuPeak, double wd_ramPeak) {
|
||||
|
||||
[rop watchdogInfo:^(uint64_t wd_cpuEvents, uint64_t wd_ramEvents, double wd_cpuPeak,
|
||||
double wd_ramPeak) {
|
||||
cpuEvents = wd_cpuEvents;
|
||||
cpuPeak = wd_cpuPeak;
|
||||
ramEvents = wd_ramEvents;
|
||||
ramPeak = wd_ramPeak;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
BOOL fileLogging = ([[SNTConfigurator configurator] fileChangesRegex] != nil);
|
||||
NSString *eventLogType = [[[SNTConfigurator configurator] eventLogTypeRaw] lowercaseString];
|
||||
|
||||
SNTConfigurator *configurator = [SNTConfigurator configurator];
|
||||
|
||||
// Cache status
|
||||
__block uint64_t rootCacheCount = -1, nonRootCacheCount = -1;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] cacheCounts:^(uint64_t rootCache, uint64_t nonRootCache) {
|
||||
[rop cacheCounts:^(uint64_t rootCache, uint64_t nonRootCache) {
|
||||
rootCacheCount = rootCache;
|
||||
nonRootCacheCount = nonRootCache;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
// Database counts
|
||||
__block int64_t eventCount = -1, binaryRuleCount = -1, certRuleCount = -1, teamIDRuleCount = -1;
|
||||
__block int64_t compilerRuleCount = -1, transitiveRuleCount = -1;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler, int64_t transitive,
|
||||
int64_t teamID) {
|
||||
binaryRuleCount = binary;
|
||||
certRuleCount = certificate;
|
||||
teamIDRuleCount = teamID;
|
||||
compilerRuleCount = compiler;
|
||||
transitiveRuleCount = transitive;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] databaseEventCount:^(int64_t count) {
|
||||
[rop databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID) {
|
||||
binaryRuleCount = binary;
|
||||
certRuleCount = certificate;
|
||||
teamIDRuleCount = teamID;
|
||||
compilerRuleCount = compiler;
|
||||
transitiveRuleCount = transitive;
|
||||
}];
|
||||
[rop databaseEventCount:^(int64_t count) {
|
||||
eventCount = count;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
// Static rule count
|
||||
__block int64_t staticRuleCount = -1;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] staticRuleCount:^(int64_t count) {
|
||||
[rop staticRuleCount:^(int64_t count) {
|
||||
staticRuleCount = count;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
// Sync status
|
||||
__block NSDate *fullSyncLastSuccess;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] fullSyncLastSuccess:^(NSDate *date) {
|
||||
[rop fullSyncLastSuccess:^(NSDate *date) {
|
||||
fullSyncLastSuccess = date;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
__block NSDate *ruleSyncLastSuccess;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] ruleSyncLastSuccess:^(NSDate *date) {
|
||||
[rop ruleSyncLastSuccess:^(NSDate *date) {
|
||||
ruleSyncLastSuccess = date;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
__block BOOL syncCleanReqd = NO;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] syncCleanRequired:^(BOOL clean) {
|
||||
[rop syncCleanRequired:^(BOOL clean) {
|
||||
syncCleanReqd = clean;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
__block BOOL pushNotifications = NO;
|
||||
if ([[SNTConfigurator configurator] syncBaseURL]) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] pushNotifications:^(BOOL response) {
|
||||
[rop pushNotifications:^(BOOL response) {
|
||||
pushNotifications = response;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
}
|
||||
|
||||
__block BOOL enableBundles = NO;
|
||||
if ([[SNTConfigurator configurator] syncBaseURL]) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] enableBundles:^(BOOL response) {
|
||||
[rop enableBundles:^(BOOL response) {
|
||||
enableBundles = response;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
}
|
||||
|
||||
__block BOOL enableTransitiveRules = NO;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] enableTransitiveRules:^(BOOL response) {
|
||||
[rop enableTransitiveRules:^(BOOL response) {
|
||||
enableTransitiveRules = response;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
__block BOOL watchItemsEnabled = NO;
|
||||
@@ -163,20 +141,16 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
__block NSString *watchItemsPolicyVersion = nil;
|
||||
__block NSString *watchItemsConfigPath = nil;
|
||||
__block NSTimeInterval watchItemsLastUpdateEpoch = 0;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
watchItemsState:^(BOOL enabled, uint64_t ruleCount, NSString *policyVersion,
|
||||
NSString *configPath, NSTimeInterval lastUpdateEpoch) {
|
||||
watchItemsEnabled = enabled;
|
||||
if (enabled) {
|
||||
watchItemsRuleCount = ruleCount;
|
||||
watchItemsPolicyVersion = policyVersion;
|
||||
watchItemsConfigPath = configPath;
|
||||
watchItemsLastUpdateEpoch = lastUpdateEpoch;
|
||||
}
|
||||
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
[rop watchItemsState:^(BOOL enabled, uint64_t ruleCount, NSString *policyVersion,
|
||||
NSString *configPath, NSTimeInterval lastUpdateEpoch) {
|
||||
watchItemsEnabled = enabled;
|
||||
if (enabled) {
|
||||
watchItemsRuleCount = ruleCount;
|
||||
watchItemsPolicyVersion = policyVersion;
|
||||
watchItemsConfigPath = configPath;
|
||||
watchItemsLastUpdateEpoch = lastUpdateEpoch;
|
||||
}
|
||||
}];
|
||||
|
||||
// Wait a maximum of 5s for stats collected from daemon to arrive.
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5))) {
|
||||
@@ -205,6 +179,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"daemon" : @{
|
||||
@"driver_connected" : @(YES),
|
||||
@"mode" : clientMode ?: @"null",
|
||||
@"log_type" : eventLogType,
|
||||
@"file_logging" : @(fileLogging),
|
||||
@"watchdog_cpu_events" : @(cpuEvents),
|
||||
@"watchdog_ram_events" : @(ramEvents),
|
||||
@@ -242,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 {
|
||||
@@ -265,6 +240,7 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
} else {
|
||||
printf(">>> Daemon Info\n");
|
||||
printf(" %-25s | %s\n", "Mode", [clientMode UTF8String]);
|
||||
printf(" %-25s | %s\n", "Log Type", [eventLogType UTF8String]);
|
||||
printf(" %-25s | %s\n", "File Logging", (fileLogging ? "Yes" : "No"));
|
||||
printf(" %-25s | %s\n", "USB Blocking", (configurator.blockUSBMount ? "Yes" : "No"));
|
||||
if (configurator.blockUSBMount && configurator.remountUSBMode.count > 0) {
|
||||
@@ -296,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,9 @@ objc_library(
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTRule",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -334,6 +337,7 @@ objc_library(
|
||||
":EndpointSecurityLogger",
|
||||
":EndpointSecurityMessage",
|
||||
":Metrics",
|
||||
":RateLimiter",
|
||||
":SNTDecisionCache",
|
||||
":SNTEndpointSecurityClient",
|
||||
":SNTEndpointSecurityEventHandler",
|
||||
@@ -342,6 +346,8 @@ objc_library(
|
||||
"//Source/common:Platform",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:SNTMetricSet",
|
||||
"//Source/common:SNTStrengthify",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
@@ -382,6 +388,17 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
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"],
|
||||
@@ -409,6 +426,7 @@ objc_library(
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
":EndpointSecurityMessage",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
"//Source/common:SNTConfigurator",
|
||||
],
|
||||
@@ -425,6 +443,9 @@ objc_library(
|
||||
":EndpointSecurityEnrichedTypes",
|
||||
":EndpointSecurityMessage",
|
||||
":SNTDecisionCache",
|
||||
"//Source/common:SantaCache",
|
||||
"//Source/common:SantaVnode",
|
||||
"//Source/common:SantaVnodeHash",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -434,6 +455,7 @@ objc_library(
|
||||
hdrs = ["Logs/EndpointSecurity/Serializers/Empty.h"],
|
||||
deps = [
|
||||
":EndpointSecuritySerializer",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -953,6 +975,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"],
|
||||
@@ -1098,6 +1130,7 @@ santa_unit_test(
|
||||
":MockEndpointSecurityAPI",
|
||||
":SNTEndpointSecurityClient",
|
||||
":WatchItemPolicy",
|
||||
"//Source/common:SNTConfigurator",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
@@ -1284,6 +1317,7 @@ test_suite(
|
||||
":EndpointSecurityWriterFileTest",
|
||||
":EndpointSecurityWriterSpoolTest",
|
||||
":MetricsTest",
|
||||
":RateLimiterTest",
|
||||
":SNTApplicationCoreMetricsTest",
|
||||
":SNTCompilerControllerTest",
|
||||
":SNTDecisionCacheTest",
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -36,18 +37,22 @@ static constexpr bool kWatchItemPolicyDefaultAuditOnly = true;
|
||||
struct WatchItemPolicy {
|
||||
struct Process {
|
||||
Process(std::string bp, std::string sid, std::string ti,
|
||||
std::vector<uint8_t> cdh, std::string ch)
|
||||
std::vector<uint8_t> cdh, std::string ch, std::optional<bool> pb)
|
||||
: binary_path(bp),
|
||||
signing_id(sid),
|
||||
team_id(ti),
|
||||
cdhash(std::move(cdh)),
|
||||
certificate_sha256(ch) {}
|
||||
certificate_sha256(ch),
|
||||
platform_binary(pb) {}
|
||||
|
||||
bool operator==(const Process &other) const {
|
||||
return binary_path == other.binary_path &&
|
||||
signing_id == other.signing_id && team_id == other.team_id &&
|
||||
cdhash == other.cdhash &&
|
||||
certificate_sha256 == other.certificate_sha256;
|
||||
certificate_sha256 == other.certificate_sha256 &&
|
||||
platform_binary.has_value() == other.platform_binary.has_value() &&
|
||||
platform_binary.value_or(false) ==
|
||||
other.platform_binary.value_or(false);
|
||||
}
|
||||
|
||||
bool operator!=(const Process &other) const { return !(*this == other); }
|
||||
@@ -57,6 +62,7 @@ struct WatchItemPolicy {
|
||||
std::string team_id;
|
||||
std::vector<uint8_t> cdhash;
|
||||
std::string certificate_sha256;
|
||||
std::optional<bool> platform_binary;
|
||||
};
|
||||
|
||||
WatchItemPolicy(std::string_view n, std::string_view p,
|
||||
|
||||
@@ -45,6 +45,7 @@ extern NSString *const kWatchItemConfigKeyProcessesCertificateSha256;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesSigningID;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesTeamID;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesCDHash;
|
||||
extern NSString *const kWatchItemConfigKeyProcessesPlatformBinary;
|
||||
|
||||
// Forward declarations
|
||||
namespace santa::santad::data_layer {
|
||||
@@ -69,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();
|
||||
@@ -79,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();
|
||||
@@ -86,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);
|
||||
@@ -97,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);
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#include <ctype.h>
|
||||
#include <glob.h>
|
||||
#include <objc/NSObjCRuntime.h>
|
||||
#include <sys/syslimits.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -56,6 +55,7 @@ NSString *const kWatchItemConfigKeyProcessesCertificateSha256 = @"CertificateSha
|
||||
NSString *const kWatchItemConfigKeyProcessesSigningID = @"SigningID";
|
||||
NSString *const kWatchItemConfigKeyProcessesTeamID = @"TeamID";
|
||||
NSString *const kWatchItemConfigKeyProcessesCDHash = @"CDHash";
|
||||
NSString *const kWatchItemConfigKeyProcessesPlatformBinary = @"PlatformBinary";
|
||||
|
||||
// https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/
|
||||
static constexpr NSUInteger kMaxTeamIDLength = 10;
|
||||
@@ -292,6 +292,8 @@ std::variant<Unit, PathList> VerifyConfigWatchItemPaths(NSArray<id> *paths, NSEr
|
||||
/// <string>AAAA</string>
|
||||
/// <key>TeamID</key>
|
||||
/// <string>BBBB</string>
|
||||
/// <key>PlatformBinary</key>
|
||||
/// <true/>
|
||||
/// </dict>
|
||||
/// <dict>
|
||||
/// <key>CertificateSha256</key>
|
||||
@@ -319,7 +321,9 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
false, HexValidator(CS_CDHASH_LEN * 2)) ||
|
||||
!VerifyConfigKey(process, kWatchItemConfigKeyProcessesCertificateSha256,
|
||||
[NSString class], err, false,
|
||||
HexValidator(CC_SHA256_DIGEST_LENGTH * 2))) {
|
||||
HexValidator(CC_SHA256_DIGEST_LENGTH * 2)) ||
|
||||
!VerifyConfigKey(process, kWatchItemConfigKeyProcessesPlatformBinary,
|
||||
[NSNumber class], err, false, nil)) {
|
||||
PopulateError(err, @"Failed to verify key content");
|
||||
return false;
|
||||
}
|
||||
@@ -329,7 +333,8 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
!process[kWatchItemConfigKeyProcessesSigningID] &&
|
||||
!process[kWatchItemConfigKeyProcessesTeamID] &&
|
||||
!process[kWatchItemConfigKeyProcessesCDHash] &&
|
||||
!process[kWatchItemConfigKeyProcessesCertificateSha256]) {
|
||||
!process[kWatchItemConfigKeyProcessesCertificateSha256] &&
|
||||
!process[kWatchItemConfigKeyProcessesPlatformBinary]) {
|
||||
PopulateError(err, @"No valid attributes set in process dictionary");
|
||||
return false;
|
||||
}
|
||||
@@ -340,7 +345,11 @@ std::variant<Unit, ProcessList> VerifyConfigWatchItemProcesses(NSDictionary *wat
|
||||
std::string([(process[kWatchItemConfigKeyProcessesTeamID] ?: @"") UTF8String]),
|
||||
HexStringToBytes(process[kWatchItemConfigKeyProcessesCDHash]),
|
||||
std::string(
|
||||
[(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @"") UTF8String])));
|
||||
[(process[kWatchItemConfigKeyProcessesCertificateSha256] ?: @"") UTF8String]),
|
||||
process[kWatchItemConfigKeyProcessesPlatformBinary]
|
||||
? std::make_optional(
|
||||
(bool)[process[kWatchItemConfigKeyProcessesPlatformBinary] boolValue])
|
||||
: std::nullopt));
|
||||
|
||||
return true;
|
||||
})) {
|
||||
@@ -421,6 +430,34 @@ bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsWatchItemNameValid(NSString *watch_item_name, NSError **err) {
|
||||
if (!watch_item_name) {
|
||||
// This shouldn't be possible as written, but handle just in case
|
||||
PopulateError(err, [NSString stringWithFormat:@"nil watch item name"]);
|
||||
return false;
|
||||
}
|
||||
|
||||
static dispatch_once_t once_token;
|
||||
static NSRegularExpression *regex;
|
||||
|
||||
dispatch_once(&once_token, ^{
|
||||
// Should only match legal C identifiers
|
||||
regex = [NSRegularExpression regularExpressionWithPattern:@"^[A-Za-z_][A-Za-z0-9_]*$"
|
||||
options:0
|
||||
error:nil];
|
||||
});
|
||||
|
||||
if ([regex numberOfMatchesInString:watch_item_name
|
||||
options:0
|
||||
range:NSMakeRange(0, watch_item_name.length)] != 1) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Key name must match regular expression \"%@\"",
|
||||
regex.pattern]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseConfig(NSDictionary *config, std::vector<std::shared_ptr<WatchItemPolicy>> &policies,
|
||||
NSError **err) {
|
||||
if (![config[kWatchItemConfigKeyVersion] isKindOfClass:[NSString class]]) {
|
||||
@@ -454,9 +491,11 @@ bool ParseConfig(NSDictionary *config, std::vector<std::shared_ptr<WatchItemPoli
|
||||
return false;
|
||||
}
|
||||
|
||||
if ([(NSString *)key length] == 0) {
|
||||
PopulateError(err, [NSString stringWithFormat:@"Invalid %@ key with length zero",
|
||||
kWatchItemConfigKeyWatchItems]);
|
||||
if (!IsWatchItemNameValid((NSString *)key, err)) {
|
||||
PopulateError(
|
||||
err, [NSString
|
||||
stringWithFormat:@"Invalid %@ key '%@': %@", kWatchItemConfigKeyWatchItems, key,
|
||||
(err && *err) ? (*err).localizedDescription : @"Unknown failure"]);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -481,24 +520,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),
|
||||
@@ -584,6 +652,8 @@ void WatchItems::UpdateCurrentState(
|
||||
|
||||
last_update_time_ = [[NSDate date] timeIntervalSince1970];
|
||||
|
||||
LOGD(@"Changes to watch items detected, notifying registered clients.");
|
||||
|
||||
for (const id<SNTEndpointSecurityDynamicEventHandler> &client : registerd_clients_) {
|
||||
// Note: Enable clients on an async queue in case they perform any
|
||||
// synchronous work that could trigger ES events. Otherwise they might
|
||||
@@ -647,7 +717,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_();
|
||||
@@ -677,11 +747,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_);
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ namespace santa::santad::data_layer {
|
||||
|
||||
extern bool ParseConfig(NSDictionary *config,
|
||||
std::vector<std::shared_ptr<WatchItemPolicy>> &policies, NSError **err);
|
||||
extern bool IsWatchItemNameValid(NSString *watch_item_name, NSError **err);
|
||||
extern bool ParseConfigSingleWatchItem(NSString *name, NSDictionary *watch_item,
|
||||
std::vector<std::shared_ptr<WatchItemPolicy>> &policies,
|
||||
NSError **err);
|
||||
@@ -60,12 +61,19 @@ 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
|
||||
|
||||
using santa::santad::data_layer::IsWatchItemNameValid;
|
||||
using santa::santad::data_layer::ParseConfig;
|
||||
using santa::santad::data_layer::ParseConfigSingleWatchItem;
|
||||
using santa::santad::data_layer::VerifyConfigWatchItemPaths;
|
||||
@@ -189,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);
|
||||
|
||||
@@ -210,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];
|
||||
@@ -311,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
|
||||
@@ -514,7 +522,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("mypath", "", "", {}, ""));
|
||||
WatchItemPolicy::Process("mypath", "", "", {}, "", std::nullopt));
|
||||
|
||||
// Test SigningID length limits
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
@@ -533,7 +541,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "com.google.test", "", {}, ""));
|
||||
WatchItemPolicy::Process("", "com.google.test", "", {}, "", std::nullopt));
|
||||
|
||||
// Test TeamID length limits
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
@@ -550,7 +558,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "", "myvalidtid", {}, ""));
|
||||
WatchItemPolicy::Process("", "", "myvalidtid", {}, "", std::nullopt));
|
||||
|
||||
// Test CDHash length limits
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
@@ -577,7 +585,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "", "", cdhashBytes, ""));
|
||||
WatchItemPolicy::Process("", "", "", cdhashBytes, "", std::nullopt));
|
||||
|
||||
// Test Cert Hash length limits
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
@@ -608,7 +616,22 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "", "", {}, [certHash UTF8String]));
|
||||
WatchItemPolicy::Process("", "", "", {}, [certHash UTF8String], std::nullopt));
|
||||
|
||||
// Test valid invalid PlatformBinary type
|
||||
proc_list = VerifyConfigWatchItemProcesses(
|
||||
@{kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesPlatformBinary : @"YES"} ]},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<Unit>(proc_list));
|
||||
|
||||
// Test valid valid PlatformBinary
|
||||
proc_list = VerifyConfigWatchItemProcesses(
|
||||
@{kWatchItemConfigKeyProcesses : @[ @{kWatchItemConfigKeyProcessesPlatformBinary : @(YES)} ]},
|
||||
&err);
|
||||
XCTAssertTrue(std::holds_alternative<santatest::ProcessList>(proc_list));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 1);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("", "", "", {}, "", std::make_optional(true)));
|
||||
|
||||
// Test valid multiple attributes, multiple procs
|
||||
proc_list = VerifyConfigWatchItemProcesses(@{
|
||||
@@ -619,6 +642,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
kWatchItemConfigKeyProcessesTeamID : @"validtid_1",
|
||||
kWatchItemConfigKeyProcessesCDHash : cdhash,
|
||||
kWatchItemConfigKeyProcessesCertificateSha256 : certHash,
|
||||
kWatchItemConfigKeyProcessesPlatformBinary : @(YES),
|
||||
},
|
||||
@{
|
||||
kWatchItemConfigKeyProcessesBinaryPath : @"mypath2",
|
||||
@@ -626,6 +650,7 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
kWatchItemConfigKeyProcessesTeamID : @"validtid_2",
|
||||
kWatchItemConfigKeyProcessesCDHash : cdhash,
|
||||
kWatchItemConfigKeyProcessesCertificateSha256 : certHash,
|
||||
kWatchItemConfigKeyProcessesPlatformBinary : @(NO),
|
||||
},
|
||||
]
|
||||
},
|
||||
@@ -634,10 +659,29 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list).size(), 2);
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[0],
|
||||
WatchItemPolicy::Process("mypath1", "com.google.test1", "validtid_1", cdhashBytes,
|
||||
[certHash UTF8String]));
|
||||
[certHash UTF8String], std::make_optional(true)));
|
||||
XCTAssertEqual(std::get<santatest::ProcessList>(proc_list)[1],
|
||||
WatchItemPolicy::Process("mypath2", "com.google.test2", "validtid_2", cdhashBytes,
|
||||
[certHash UTF8String]));
|
||||
[certHash UTF8String], std::make_optional(false)));
|
||||
}
|
||||
|
||||
- (void)testIsWatchItemNameValid {
|
||||
// Only legal C identifiers should be accepted
|
||||
XCTAssertFalse(IsWatchItemNameValid(nil, nil));
|
||||
XCTAssertFalse(IsWatchItemNameValid(@"", nil));
|
||||
XCTAssertFalse(IsWatchItemNameValid(@"1abc", nil));
|
||||
XCTAssertFalse(IsWatchItemNameValid(@"abc-1234", nil));
|
||||
XCTAssertFalse(IsWatchItemNameValid(@"a=b", nil));
|
||||
XCTAssertFalse(IsWatchItemNameValid(@"a!b", nil));
|
||||
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"_", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"_1", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"_1_", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"abc", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"A", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"A_B", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"FooName", nil));
|
||||
XCTAssertTrue(IsWatchItemNameValid(@"bar_Name", nil));
|
||||
}
|
||||
|
||||
- (void)testParseConfig {
|
||||
@@ -670,16 +714,16 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"" : @{}}},
|
||||
policies, &err));
|
||||
XCTAssertFalse(
|
||||
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"1" : @[]}},
|
||||
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"a" : @[]}},
|
||||
policies, &err));
|
||||
XCTAssertFalse(
|
||||
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"1" : @{}}},
|
||||
ParseConfig(@{kWatchItemConfigKeyVersion : @"1", kWatchItemConfigKeyWatchItems : @{@"a" : @{}}},
|
||||
policies, &err));
|
||||
|
||||
// Minimally successful config with watch item
|
||||
XCTAssertTrue(ParseConfig(@{
|
||||
kWatchItemConfigKeyVersion : @"1",
|
||||
kWatchItemConfigKeyWatchItems : @{@"1" : @{kWatchItemConfigKeyPaths : @[ @"asdf" ]}}
|
||||
kWatchItemConfigKeyWatchItems : @{@"a" : @{kWatchItemConfigKeyPaths : @[ @"asdf" ]}}
|
||||
},
|
||||
policies, &err));
|
||||
}
|
||||
@@ -753,8 +797,8 @@ static NSString *RepeatedString(NSString *str, NSUInteger len) {
|
||||
// Test multiple paths, options, and processes
|
||||
policies.clear();
|
||||
std::vector<WatchItemPolicy::Process> procs = {
|
||||
WatchItemPolicy::Process("pa", "", "", {}, ""),
|
||||
WatchItemPolicy::Process("pb", "", "", {}, ""),
|
||||
WatchItemPolicy::Process("pa", "", "", {}, "", std::nullopt),
|
||||
WatchItemPolicy::Process("pb", "", "", {}, "", std::nullopt),
|
||||
};
|
||||
|
||||
XCTAssertTrue(ParseConfigSingleWatchItem(@"rule", @{
|
||||
@@ -804,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
|
||||
|
||||
77
Source/santad/EventProviders/RateLimiter.h
Normal file
77
Source/santad/EventProviders/RateLimiter.h
Normal 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
|
||||
85
Source/santad/EventProviders/RateLimiter.mm
Normal file
85
Source/santad/EventProviders/RateLimiter.mm
Normal 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
|
||||
149
Source/santad/EventProviders/RateLimiterTest.mm
Normal file
149
Source/santad/EventProviders/RateLimiterTest.mm
Normal 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
|
||||
@@ -85,34 +85,41 @@ 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);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
// There is a benign leak of the mock object in this test.
|
||||
// `handleMessage:recordEventMetrics:` will call `processMessage:handler:` in the parent
|
||||
// class. This will dispatch to two blocks and create message copies. The block that
|
||||
// handles `deadline` timeouts will not complete before the test finishes, and the
|
||||
// mock object will think that it has been leaked.
|
||||
::testing::Mock::AllowLeak(mockESApi.get());
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
execController:self.mockExecController
|
||||
compilerController:nil
|
||||
authResultCache:nullptr];
|
||||
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
// Test unhandled event type
|
||||
{
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
// There is a benign leak of the mock object in this test.
|
||||
// `handleMessage:recordEventMetrics:` will call `processMessage:handler:` in the parent
|
||||
// class. This will dispatch to two blocks and create message copies. The block that
|
||||
// handles `deadline` timeouts will not complete before the test finishes, and the
|
||||
// mock object will think that it has been leaked.
|
||||
::testing::Mock::AllowLeak(mockESApi.get());
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
execController:self.mockExecController
|
||||
compilerController:nil
|
||||
authResultCache:nullptr];
|
||||
|
||||
// Temporarily change the event type
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
|
||||
XCTAssertThrows([authClient handleMessage:Message(mockESApi, &esMsg)
|
||||
@@ -120,60 +127,97 @@ class MockAuthResultCache : public AuthResultCache {
|
||||
XCTFail("Unhandled event types shouldn't call metrics recorder");
|
||||
}]);
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_EXEC;
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
// Test SNTExecutionController determines the event shouldn't be processed
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
::testing::Mock::AllowLeak(mockESApi.get());
|
||||
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(NO);
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
execController:self.mockExecController
|
||||
compilerController:nil
|
||||
authResultCache:nullptr];
|
||||
|
||||
OCMExpect([mockAuthClient postAction:SNTActionRespondDeny
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
// 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([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());
|
||||
}
|
||||
|
||||
// Test SNTExecutionController determines the event should be processed and
|
||||
// processMessage:handler: is called.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
::testing::Mock::AllowLeak(mockESApi.get());
|
||||
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(YES);
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
execController:self.mockExecController
|
||||
compilerController:nil
|
||||
authResultCache:nullptr];
|
||||
|
||||
OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg)]).ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
[mockAuthClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(YES);
|
||||
|
||||
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);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
}
|
||||
|
||||
[mockAuthClient stopMocking];
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
[mockAuthClient stopMocking];
|
||||
}
|
||||
|
||||
- (void)testProcessMessageWaitThenAllow {
|
||||
|
||||
@@ -49,6 +49,7 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
|
||||
@interface SNTEndpointSecurityClient ()
|
||||
@property int64_t deadlineMarginMS;
|
||||
@property SNTConfigurator *configurator;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityClient {
|
||||
@@ -68,6 +69,7 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
_esApi = std::move(esApi);
|
||||
_metrics = std::move(metrics);
|
||||
_deadlineMarginMS = 5000;
|
||||
_configurator = [SNTConfigurator configurator];
|
||||
_processor = processor;
|
||||
|
||||
_authQueue = dispatch_queue_create(
|
||||
@@ -103,9 +105,8 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
}
|
||||
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg
|
||||
ignoringOtherESClients:(BOOL)ignoringOtherESClients {
|
||||
if (esMsg->process->is_es_client && ignoringOtherESClients) {
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg {
|
||||
if (esMsg->process->is_es_client && [self.configurator ignoreOtherEndpointSecurityClients]) {
|
||||
if (esMsg->action_type == ES_ACTION_TYPE_AUTH) {
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:true];
|
||||
}
|
||||
@@ -125,9 +126,7 @@ constexpr std::string_view kProtectedFiles[] = {"/private/var/db/santa/rules.db"
|
||||
self->_esClient = self->_esApi->NewClient(^(es_client_t *c, Message esMsg) {
|
||||
int64_t processingStart = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
es_event_type_t eventType = esMsg->event_type;
|
||||
if ([self shouldHandleMessage:esMsg
|
||||
ignoringOtherESClients:[[SNTConfigurator configurator]
|
||||
ignoreOtherEndpointSecurityClients]]) {
|
||||
if ([self shouldHandleMessage:esMsg]) {
|
||||
[self handleMessage:std::move(esMsg)
|
||||
recordEventMetrics:^(EventDisposition disposition) {
|
||||
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/DataLayer/WatchItemPolicy.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
@@ -46,8 +47,7 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
- (NSString *)errorMessageForNewClientResult:(es_new_client_result_t)result;
|
||||
- (void)handleMessage:(Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(santa::santad::EventDisposition disposition))recordEventMetrics;
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg
|
||||
ignoringOtherESClients:(BOOL)ignoringOtherESClients;
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg;
|
||||
|
||||
@property int64_t deadlineMarginMS;
|
||||
@end
|
||||
@@ -130,6 +130,9 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_ALLOW, true))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
id mockConfigurator = OCMStrictClassMock([SNTConfigurator class]);
|
||||
OCMStub([mockConfigurator configurator]).andReturn(mockConfigurator);
|
||||
|
||||
SNTEndpointSecurityClient *client =
|
||||
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
@@ -139,24 +142,31 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
// Is ES client, but don't ignore others == Should Handle
|
||||
OCMExpect([mockConfigurator ignoreOtherEndpointSecurityClients]).andReturn(NO);
|
||||
esMsg.process->is_es_client = true;
|
||||
XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:NO]);
|
||||
XCTAssertTrue([client shouldHandleMessage:msg]);
|
||||
|
||||
// Not ES client, but ignore others == Should Handle
|
||||
// Don't setup configurator mock since it won't be called when `is_es_client` is false
|
||||
esMsg.process->is_es_client = false;
|
||||
XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
|
||||
XCTAssertTrue([client shouldHandleMessage:msg]);
|
||||
|
||||
// Is ES client, don't ignore others, and non-AUTH == Don't Handle
|
||||
OCMExpect([mockConfigurator ignoreOtherEndpointSecurityClients]).andReturn(YES);
|
||||
esMsg.process->is_es_client = true;
|
||||
XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
|
||||
XCTAssertFalse([client shouldHandleMessage:msg]);
|
||||
|
||||
// Is ES client, don't ignore others, and AUTH == Respond and Don't Handle
|
||||
OCMExpect([mockConfigurator ignoreOtherEndpointSecurityClients]).andReturn(YES);
|
||||
esMsg.process->is_es_client = true;
|
||||
esMsg.action_type = ES_ACTION_TYPE_AUTH;
|
||||
XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
|
||||
XCTAssertFalse([client shouldHandleMessage:msg]);
|
||||
}
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockConfigurator));
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
[mockConfigurator stopMocking];
|
||||
}
|
||||
|
||||
- (void)testPopulateAuditTokenSelf {
|
||||
@@ -359,6 +369,8 @@ using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
- (void)testRespondToMessageWithAuthResultCacheable {
|
||||
es_message_t esMsg;
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_EXEC;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage();
|
||||
|
||||
|
||||
@@ -248,10 +248,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
#include "Source/common/Platform.h"
|
||||
#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"
|
||||
@@ -39,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;
|
||||
@@ -53,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
|
||||
@@ -196,6 +201,7 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
std::shared_ptr<Logger> _logger;
|
||||
std::shared_ptr<WatchItems> _watchItems;
|
||||
std::shared_ptr<Enricher> _enricher;
|
||||
std::shared_ptr<RateLimiter> _rateLimiter;
|
||||
SantaCache<SantaVnode, NSString *> _certHashCache;
|
||||
}
|
||||
|
||||
@@ -209,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);
|
||||
@@ -218,6 +224,20 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
|
||||
_decisionCache = decisionCache;
|
||||
|
||||
_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];
|
||||
@@ -319,14 +339,24 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
// configured process exception while the check for a valid code signature
|
||||
// is more broad and applies whether or not process exceptions exist.
|
||||
if (esProc->codesigning_flags & CS_SIGNED) {
|
||||
// Check if the instigating process has an allowed TeamID
|
||||
if (!policyProc.team_id.empty() && esProc->team_id.data &&
|
||||
policyProc.team_id != esProc->team_id.data) {
|
||||
// Check whether or not the process is a platform binary if specified by the policy.
|
||||
if (policyProc.platform_binary.has_value() &&
|
||||
policyProc.platform_binary.value() != esProc->is_platform_binary) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!policyProc.signing_id.empty() && esProc->signing_id.data &&
|
||||
policyProc.signing_id != esProc->signing_id.data) {
|
||||
// If the policy contains a team ID, check that the instigating process
|
||||
// also has a team ID and matches the policy.
|
||||
if (!policyProc.team_id.empty() &&
|
||||
(!esProc->team_id.data || (policyProc.team_id != esProc->team_id.data))) {
|
||||
// We expected a team ID to match against, but the process didn't have one.
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the policy contains a signing ID, check that the instigating process
|
||||
// also has a signing ID and matches the policy.
|
||||
if (!policyProc.signing_id.empty() &&
|
||||
(!esProc->signing_id.data || (policyProc.signing_id != esProc->signing_id.data))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -420,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;
|
||||
@@ -433,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,9 +540,13 @@ void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets) {
|
||||
#endif
|
||||
|
||||
if (!self.isSubscribed) {
|
||||
self.isSubscribed = [super subscribe:events];
|
||||
[super clearCache];
|
||||
if ([super subscribe:events]) {
|
||||
self.isSubscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Always clear cache to ensure operations that were previously allowed are re-evaluated.
|
||||
[super clearCache];
|
||||
}
|
||||
|
||||
- (void)disable {
|
||||
|
||||
@@ -384,6 +384,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
esProc.codesigning_flags = CS_SIGNED;
|
||||
esProc.team_id = MakeESStringToken(teamId);
|
||||
esProc.signing_id = MakeESStringToken(signingId);
|
||||
esProc.is_platform_binary = true;
|
||||
std::memcpy(esProc.cdhash, cdhashBytes.data(), sizeof(esProc.cdhash));
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
@@ -405,7 +406,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(@(instigatingCertHash));
|
||||
|
||||
WatchItemPolicy::Process policyProc("", "", "", {}, "");
|
||||
WatchItemPolicy::Process policyProc("", "", "", {}, "", std::nullopt);
|
||||
|
||||
{
|
||||
// Process policy matching single attribute - path
|
||||
@@ -423,6 +424,11 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertTrue([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
policyProc.signing_id = "badid";
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
es_process_t esProcEmptySigningID = MakeESProcess(&esFile);
|
||||
esProcEmptySigningID.codesigning_flags = CS_SIGNED;
|
||||
esProcEmptySigningID.team_id.data = NULL;
|
||||
esProcEmptySigningID.team_id.length = 0;
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProcEmptySigningID]);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -432,6 +438,11 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertTrue([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
policyProc.team_id = "badid";
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
es_process_t esProcEmptyTeamID = MakeESProcess(&esFile);
|
||||
esProcEmptyTeamID.codesigning_flags = CS_SIGNED;
|
||||
esProcEmptyTeamID.signing_id.data = NULL;
|
||||
esProcEmptyTeamID.signing_id.length = 0;
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProcEmptyTeamID]);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -452,6 +463,15 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
}
|
||||
|
||||
{
|
||||
// Process policy matching single attribute - platform binary
|
||||
ClearWatchItemPolicyProcess(policyProc);
|
||||
policyProc.platform_binary = std::make_optional(true);
|
||||
XCTAssertTrue([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
policyProc.platform_binary = std::make_optional(false);
|
||||
XCTAssertFalse([accessClient policyProcess:policyProc matchesESProcess:&esProc]);
|
||||
}
|
||||
|
||||
{
|
||||
// Process policy with only a subset of matching attributes
|
||||
ClearWatchItemPolicyProcess(policyProc);
|
||||
@@ -476,7 +496,7 @@ void ClearWatchItemPolicyProcess(WatchItemPolicy::Process &proc) {
|
||||
const char *instigatingPath = "/path/to/proc";
|
||||
const char *instigatingTeamID = "my_teamid";
|
||||
const char *instigatingCertHash = "abc123";
|
||||
WatchItemPolicy::Process policyProc(instigatingPath, "", "", {}, "");
|
||||
WatchItemPolicy::Process policyProc(instigatingPath, "", "", {}, "", std::nullopt);
|
||||
std::array<uint8_t, 20> instigatingCDHash;
|
||||
instigatingCDHash.fill(0x41);
|
||||
es_file_t esFile = MakeESFile(instigatingPath);
|
||||
|
||||
@@ -65,6 +65,8 @@ class Logger {
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedProcess &enriched_process,
|
||||
const std::string &target, FileAccessPolicyDecision decision);
|
||||
|
||||
void Flush();
|
||||
|
||||
friend class santa::santad::logs::endpoint_security::LoggerPeer;
|
||||
|
||||
private:
|
||||
|
||||
@@ -107,4 +107,8 @@ void Logger::LogFileAccess(
|
||||
enriched_process, target, decision));
|
||||
}
|
||||
|
||||
void Logger::Flush() {
|
||||
writer_->Flush();
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
|
||||
@@ -41,7 +42,8 @@ class BasicString : public Serializer {
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &) override;
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &,
|
||||
SNTCachedDecision *) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
|
||||
@@ -241,12 +241,12 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExchange &msg)
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg) {
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg, SNTCachedDecision *cd) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString(1024); // EXECs tend to be bigger, reserve more space.
|
||||
|
||||
SNTCachedDecision *cd =
|
||||
[[SNTDecisionCache sharedCache] cachedDecisionForFile:esm.event.exec.target->executable->stat];
|
||||
// Only need to grab the shared instance once
|
||||
static SNTConfigurator *configurator = [SNTConfigurator configurator];
|
||||
|
||||
str.append("action=EXEC|decision=");
|
||||
str.append(GetDecisionString(cd.decision));
|
||||
@@ -291,7 +291,7 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg) {
|
||||
msg.instigator().real_group());
|
||||
|
||||
str.append("|mode=");
|
||||
str.append(GetModeString([[SNTConfigurator configurator] clientMode]));
|
||||
str.append(GetModeString([configurator clientMode]));
|
||||
str.append("|path=");
|
||||
str.append(FilePath(esm.event.exec.target->executable).Sanitized());
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
OCMStub([self.mockDecisionCache cachedDecisionForFile:{}])
|
||||
OCMStub([self.mockDecisionCache resetTimestampForCachedDecision:{}])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(self.testCachedDecision);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
@@ -33,7 +34,8 @@ class Empty : public Serializer {
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &) override;
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &,
|
||||
SNTCachedDecision *) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
|
||||
@@ -39,7 +39,7 @@ std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExchange &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExec &msg) {
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExec &msg, SNTCachedDecision *cd) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace es = santa::santad::event_providers::endpoint_security;
|
||||
int fake;
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedClose *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExchange *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExec *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExec *)&fake, nil).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExit *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedFork *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedLink *)&fake).size(), 0);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/common/santa_proto_include_wrapper.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
@@ -40,7 +41,8 @@ class Protobuf : public Serializer {
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &) override;
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &,
|
||||
SNTCachedDecision *) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
|
||||
@@ -384,7 +384,7 @@ static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info,
|
||||
|
||||
std::vector<uint8_t> Protobuf::FinalizeProto(::pbv1::SantaMessage *santa_msg) {
|
||||
std::vector<uint8_t> vec(santa_msg->ByteSizeLong());
|
||||
santa_msg->SerializeToArray(vec.data(), (int)vec.capacity());
|
||||
santa_msg->SerializeWithCachedSizesToArray(vec.data());
|
||||
return vec;
|
||||
}
|
||||
|
||||
@@ -418,12 +418,12 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExchange &msg) {
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg) {
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg, SNTCachedDecision *cd) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
SNTCachedDecision *cd = [[SNTDecisionCache sharedCache]
|
||||
cachedDecisionForFile:msg.es_msg().event.exec.target->executable->stat];
|
||||
// Only need to grab the shared instance once
|
||||
static SNTConfigurator *configurator = [SNTConfigurator configurator];
|
||||
|
||||
GetDecisionEnum(cd.decision);
|
||||
|
||||
@@ -444,28 +444,37 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg) {
|
||||
}
|
||||
|
||||
uint32_t arg_count = esapi_->ExecArgCount(&msg.es_msg().event.exec);
|
||||
for (uint32_t i = 0; i < arg_count; i++) {
|
||||
es_string_token_t tok = esapi_->ExecArg(&msg.es_msg().event.exec, i);
|
||||
pb_exec->add_args(tok.data, tok.length);
|
||||
if (arg_count > 0) {
|
||||
pb_exec->mutable_args()->Reserve(arg_count);
|
||||
for (uint32_t i = 0; i < arg_count; i++) {
|
||||
es_string_token_t tok = esapi_->ExecArg(&msg.es_msg().event.exec, i);
|
||||
pb_exec->add_args(tok.data, tok.length);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t env_count = esapi_->ExecEnvCount(&msg.es_msg().event.exec);
|
||||
for (uint32_t i = 0; i < env_count; i++) {
|
||||
es_string_token_t tok = esapi_->ExecEnv(&msg.es_msg().event.exec, i);
|
||||
pb_exec->add_envs(tok.data, tok.length);
|
||||
if (env_count > 0) {
|
||||
pb_exec->mutable_envs()->Reserve(env_count);
|
||||
for (uint32_t i = 0; i < env_count; i++) {
|
||||
es_string_token_t tok = esapi_->ExecEnv(&msg.es_msg().event.exec, i);
|
||||
pb_exec->add_envs(tok.data, tok.length);
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.es_msg().version >= 4) {
|
||||
int32_t max_fd = -1;
|
||||
uint32_t fd_count = esapi_->ExecFDCount(&msg.es_msg().event.exec);
|
||||
for (uint32_t i = 0; i < fd_count; i++) {
|
||||
const es_fd_t *fd = esapi_->ExecFD(&msg.es_msg().event.exec, i);
|
||||
max_fd = std::max(max_fd, fd->fd);
|
||||
::pbv1::FileDescriptor *pb_fd = pb_exec->add_fds();
|
||||
pb_fd->set_fd(fd->fd);
|
||||
pb_fd->set_fd_type(GetFileDescriptorType(fd->fdtype));
|
||||
if (fd->fdtype == PROX_FDTYPE_PIPE) {
|
||||
pb_fd->set_pipe_id(fd->pipe.pipe_id);
|
||||
if (fd_count > 0) {
|
||||
pb_exec->mutable_fds()->Reserve(fd_count);
|
||||
for (uint32_t i = 0; i < fd_count; i++) {
|
||||
const es_fd_t *fd = esapi_->ExecFD(&msg.es_msg().event.exec, i);
|
||||
max_fd = std::max(max_fd, fd->fd);
|
||||
::pbv1::FileDescriptor *pb_fd = pb_exec->add_fds();
|
||||
pb_fd->set_fd(fd->fd);
|
||||
pb_fd->set_fd_type(GetFileDescriptorType(fd->fdtype));
|
||||
if (fd->fdtype == PROX_FDTYPE_PIPE) {
|
||||
pb_fd->set_pipe_id(fd->pipe.pipe_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,7 +485,7 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg) {
|
||||
|
||||
pb_exec->set_decision(GetDecisionEnum(cd.decision));
|
||||
pb_exec->set_reason(GetReasonEnum(cd.decision));
|
||||
pb_exec->set_mode(GetModeEnum([[SNTConfigurator configurator] clientMode]));
|
||||
pb_exec->set_mode(GetModeEnum([configurator clientMode]));
|
||||
|
||||
if (cd.certSHA256 || cd.certCommonName) {
|
||||
EncodeCertificateInfo(pb_exec->mutable_certificate_info(), cd.certSHA256, cd.certCommonName);
|
||||
|
||||
@@ -283,7 +283,7 @@ void SerializeAndCheckNonESEvents(
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
OCMStub([self.mockDecisionCache cachedDecisionForFile:{}])
|
||||
OCMStub([self.mockDecisionCache resetTimestampForCachedDecision:{}])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(self.testCachedDecision);
|
||||
}
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SERIALIZER_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SERIALIZER_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
|
||||
@@ -46,7 +47,8 @@ class Serializer {
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &) = 0;
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &,
|
||||
SNTCachedDecision *cd) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
|
||||
@@ -46,15 +46,20 @@ std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExch
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExec &msg) {
|
||||
static SNTDecisionCache *decision_cache = [SNTDecisionCache sharedCache];
|
||||
|
||||
SNTCachedDecision *cd;
|
||||
const es_message_t &es_msg = msg.es_msg();
|
||||
if (es_msg.action_type == ES_ACTION_TYPE_NOTIFY &&
|
||||
es_msg.action.notify.result.auth == ES_AUTH_RESULT_ALLOW) {
|
||||
// For allowed execs, cached decision timestamps must be updated
|
||||
[[SNTDecisionCache sharedCache]
|
||||
cd = [decision_cache
|
||||
resetTimestampForCachedDecision:msg.es_msg().event.exec.target->executable->stat];
|
||||
} else {
|
||||
cd = [decision_cache cachedDecisionForFile:msg.es_msg().event.exec.target->executable->stat];
|
||||
}
|
||||
|
||||
return SerializeMessage(msg);
|
||||
return SerializeMessage(msg, cd);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExit &msg) {
|
||||
return SerializeMessage(msg);
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
|
||||
|
||||
#include "Source/common/SantaCache.h"
|
||||
#import "Source/common/SantaVnode.h"
|
||||
#include "Source/common/SantaVnodeHash.h"
|
||||
|
||||
// These functions are exported by the Security framework, but are not included in headers
|
||||
extern "C" Boolean SecTranslocateIsTranslocatedURL(CFURLRef path, bool *isTranslocated,
|
||||
CFErrorRef *__nullable error);
|
||||
@@ -32,10 +36,17 @@ static inline void SetThreadIDs(uid_t uid, gid_t gid) {
|
||||
}
|
||||
|
||||
NSString *OriginalPathForTranslocation(const es_process_t *es_proc) {
|
||||
// Cache vnodes that have been determined to not be translocated
|
||||
static SantaCache<SantaVnode, bool> isNotTranslocatedCache(1024);
|
||||
|
||||
if (!es_proc) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (isNotTranslocatedCache.get(SantaVnode::VnodeForFile(es_proc->executable))) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Note: Benchmarks showed better performance using `URLWithString` with a `file://` prefix
|
||||
// compared to using `fileURLWithPath`.
|
||||
CFURLRef cfExecURL = (__bridge CFURLRef)
|
||||
@@ -58,6 +69,8 @@ NSString *OriginalPathForTranslocation(const es_process_t *es_proc) {
|
||||
if (dropPrivs) {
|
||||
SetThreadIDs(KAUTH_UID_NONE, KAUTH_GID_NONE);
|
||||
}
|
||||
} else {
|
||||
isNotTranslocatedCache.set(SantaVnode::VnodeForFile(es_proc->executable), true);
|
||||
}
|
||||
|
||||
return [origURL path];
|
||||
|
||||
@@ -42,17 +42,18 @@ class File : public Writer, public std::enable_shared_from_this<File> {
|
||||
~File();
|
||||
|
||||
void Write(std::vector<uint8_t> &&bytes) override;
|
||||
void Flush() override;
|
||||
|
||||
friend class santa::santad::logs::endpoint_security::writers::FilePeer;
|
||||
|
||||
private:
|
||||
void OpenFileHandle();
|
||||
void OpenFileHandleLocked();
|
||||
void WatchLogFile();
|
||||
void FlushBuffer();
|
||||
void FlushLocked();
|
||||
bool ShouldFlush();
|
||||
|
||||
void EnsureCapacity(size_t additional_bytes);
|
||||
void CopyData(const std::vector<uint8_t> &bytes);
|
||||
void EnsureCapacityLocked(size_t additional_bytes);
|
||||
void CopyDataLocked(const std::vector<uint8_t> &bytes);
|
||||
|
||||
std::vector<uint8_t> buffer_;
|
||||
size_t batch_size_bytes_;
|
||||
|
||||
@@ -39,7 +39,7 @@ std::shared_ptr<File> File::Create(NSString *path, uint64_t flush_timeout_ms,
|
||||
if (!shared_writer) {
|
||||
return;
|
||||
}
|
||||
shared_writer->FlushBuffer();
|
||||
shared_writer->FlushLocked();
|
||||
});
|
||||
|
||||
dispatch_resume(ret_writer->timer_source_);
|
||||
@@ -55,7 +55,7 @@ File::File(NSString *path, size_t batch_size_bytes, size_t max_expected_write_si
|
||||
timer_source_(timer_source),
|
||||
watch_source_(nullptr) {
|
||||
path_ = path;
|
||||
OpenFileHandle();
|
||||
OpenFileHandleLocked();
|
||||
}
|
||||
|
||||
void File::WatchLogFile() {
|
||||
@@ -69,7 +69,7 @@ void File::WatchLogFile() {
|
||||
auto shared_this = shared_from_this();
|
||||
dispatch_source_set_event_handler(watch_source_, ^{
|
||||
[shared_this->file_handle_ closeFile];
|
||||
shared_this->OpenFileHandle();
|
||||
shared_this->OpenFileHandleLocked();
|
||||
shared_this->WatchLogFile();
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ File::~File() {
|
||||
}
|
||||
|
||||
// IMPORTANT: Not thread safe.
|
||||
void File::OpenFileHandle() {
|
||||
void File::OpenFileHandleLocked() {
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
if (![fm fileExistsAtPath:path_]) {
|
||||
[fm createFileAtPath:path_ contents:nil attributes:nil];
|
||||
@@ -101,10 +101,10 @@ void File::Write(std::vector<uint8_t> &&bytes) {
|
||||
dispatch_async(q_, ^{
|
||||
std::vector<uint8_t> moved_bytes = std::move(temp_bytes);
|
||||
|
||||
shared_this->CopyData(moved_bytes);
|
||||
shared_this->CopyDataLocked(moved_bytes);
|
||||
|
||||
if (shared_this->ShouldFlush()) {
|
||||
shared_this->FlushBuffer();
|
||||
shared_this->FlushLocked();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -113,22 +113,28 @@ bool File::ShouldFlush() {
|
||||
return buffer_offset_ >= batch_size_bytes_;
|
||||
}
|
||||
|
||||
void File::Flush() {
|
||||
dispatch_sync(q_, ^{
|
||||
FlushLocked();
|
||||
});
|
||||
}
|
||||
|
||||
// IMPORTANT: Not thread safe.
|
||||
void File::EnsureCapacity(size_t additional_bytes) {
|
||||
void File::EnsureCapacityLocked(size_t additional_bytes) {
|
||||
if ((buffer_offset_ + additional_bytes) > buffer_.capacity()) {
|
||||
buffer_.resize(buffer_.capacity() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT: Not thread safe.
|
||||
void File::CopyData(const std::vector<uint8_t> &bytes) {
|
||||
EnsureCapacity(bytes.size());
|
||||
void File::CopyDataLocked(const std::vector<uint8_t> &bytes) {
|
||||
EnsureCapacityLocked(bytes.size());
|
||||
std::copy(bytes.begin(), bytes.end(), buffer_.begin() + buffer_offset_);
|
||||
buffer_offset_ += bytes.size();
|
||||
}
|
||||
|
||||
// IMPORTANT: Not thread safe.
|
||||
void File::FlushBuffer() {
|
||||
void File::FlushLocked() {
|
||||
if (likely(buffer_offset_ > 0)) {
|
||||
write(file_handle_.fileDescriptor, buffer_.data(), buffer_offset_);
|
||||
|
||||
|
||||
@@ -29,15 +29,34 @@ class FilePeer : public File {
|
||||
// Make constructors visible
|
||||
using File::File;
|
||||
|
||||
using File::CopyData;
|
||||
using File::EnsureCapacity;
|
||||
using File::CopyDataLocked;
|
||||
using File::EnsureCapacityLocked;
|
||||
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
|
||||
@@ -192,7 +211,7 @@ bool WaitForBufferSize(std::shared_ptr<FilePeer> file, size_t expectedSize) {
|
||||
XCTAssertEqual(file->InternalBufferSize(), 0);
|
||||
XCTAssertEqual(file->InternalBufferCapacity(), initialCapacity);
|
||||
|
||||
file->EnsureCapacity(batchSize);
|
||||
file->EnsureCapacityLocked(batchSize);
|
||||
|
||||
// No data was written, so size is still 0
|
||||
XCTAssertEqual(file->InternalBufferSize(), 0);
|
||||
@@ -201,7 +220,7 @@ bool WaitForBufferSize(std::shared_ptr<FilePeer> file, size_t expectedSize) {
|
||||
// the initial amount
|
||||
XCTAssertEqual(file->InternalBufferCapacity(), initialCapacity);
|
||||
|
||||
file->EnsureCapacity(initialCapacity + 100);
|
||||
file->EnsureCapacityLocked(initialCapacity + 100);
|
||||
|
||||
// No data was written, so size is still 0
|
||||
XCTAssertEqual(file->InternalBufferSize(), 0);
|
||||
@@ -225,15 +244,15 @@ bool WaitForBufferSize(std::shared_ptr<FilePeer> file, size_t expectedSize) {
|
||||
XCTAssertEqual(file->InternalBufferSize(), 0);
|
||||
XCTAssertEqual(file->InternalBufferCapacity(), initialCapacity);
|
||||
|
||||
file->CopyData(bytes);
|
||||
file->CopyDataLocked(bytes);
|
||||
|
||||
// After a copy, buffer size should match copied data size
|
||||
XCTAssertEqual(file->InternalBufferSize(), bytes.size());
|
||||
|
||||
// Do a couple more copies that should require the buffer to grow and then
|
||||
// confirm the size/capacity still matches expectations
|
||||
file->CopyData(bytes);
|
||||
file->CopyData(bytes);
|
||||
file->CopyDataLocked(bytes);
|
||||
file->CopyDataLocked(bytes);
|
||||
XCTAssertEqual(file->InternalBufferSize(), bytes.size() * 3);
|
||||
XCTAssertEqual(file->InternalBufferCapacity(), initialCapacity * 2);
|
||||
}
|
||||
@@ -249,7 +268,7 @@ bool WaitForBufferSize(std::shared_ptr<FilePeer> file, size_t expectedSize) {
|
||||
XCTAssertFalse(file->ShouldFlush());
|
||||
|
||||
// Copy some data into the buffer
|
||||
file->CopyData(bytes);
|
||||
file->CopyDataLocked(bytes);
|
||||
|
||||
// Buffer size should be updated
|
||||
XCTAssertEqual(file->InternalBufferSize(), bytes.size());
|
||||
@@ -258,8 +277,8 @@ bool WaitForBufferSize(std::shared_ptr<FilePeer> file, size_t expectedSize) {
|
||||
XCTAssertFalse(file->ShouldFlush());
|
||||
|
||||
// Exceed the batch size
|
||||
file->CopyData(bytes);
|
||||
file->CopyData(bytes);
|
||||
file->CopyDataLocked(bytes);
|
||||
file->CopyDataLocked(bytes);
|
||||
|
||||
// Should want to flush now that the batch size is exceeded
|
||||
XCTAssertTrue(file->ShouldFlush());
|
||||
|
||||
@@ -28,6 +28,7 @@ class Null : public Writer {
|
||||
static std::shared_ptr<Null> Create();
|
||||
|
||||
void Write(std::vector<uint8_t>&& bytes) override;
|
||||
void Flush() override;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
@@ -24,4 +24,8 @@ void Null::Write(std::vector<uint8_t> &&bytes) {
|
||||
// Intentionally do nothing
|
||||
}
|
||||
|
||||
void Null::Flush() {
|
||||
// Intentionally do nothing
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
@@ -46,7 +46,7 @@ class Spool : public Writer, public std::enable_shared_from_this<Spool> {
|
||||
~Spool();
|
||||
|
||||
void Write(std::vector<uint8_t> &&bytes) override;
|
||||
bool Flush();
|
||||
void Flush() override;
|
||||
|
||||
void BeginFlushTask();
|
||||
|
||||
@@ -54,6 +54,8 @@ class Spool : public Writer, public std::enable_shared_from_this<Spool> {
|
||||
friend class santa::santad::logs::endpoint_security::writers::SpoolPeer;
|
||||
|
||||
private:
|
||||
bool FlushLocked();
|
||||
|
||||
dispatch_queue_t q_ = NULL;
|
||||
dispatch_source_t timer_source_ = NULL;
|
||||
::fsspool::FsSpoolWriter spool_writer_;
|
||||
|
||||
@@ -81,7 +81,7 @@ void Spool::BeginFlushTask() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shared_writer->Flush()) {
|
||||
if (!shared_writer->FlushLocked()) {
|
||||
LOGE(@"Spool writer: periodic flush failed.");
|
||||
}
|
||||
|
||||
@@ -94,7 +94,13 @@ void Spool::BeginFlushTask() {
|
||||
flush_task_started_ = true;
|
||||
}
|
||||
|
||||
bool Spool::Flush() {
|
||||
void Spool::Flush() {
|
||||
dispatch_sync(q_, ^{
|
||||
FlushLocked();
|
||||
});
|
||||
}
|
||||
|
||||
bool Spool::FlushLocked() {
|
||||
if (log_batch_writer_.Flush().ok()) {
|
||||
accumulated_bytes_ = 0;
|
||||
return true;
|
||||
@@ -122,7 +128,7 @@ void Spool::Write(std::vector<uint8_t> &&bytes) {
|
||||
any.set_type_url(type_url_);
|
||||
|
||||
if (shared_this->accumulated_bytes_ >= shared_this->spool_file_size_threshold_) {
|
||||
shared_this->Flush();
|
||||
shared_this->FlushLocked();
|
||||
}
|
||||
|
||||
// Only write the new message if we have room left.
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace santa::santad::logs::endpoint_security::writers {
|
||||
class SpoolPeer : public Spool {
|
||||
public:
|
||||
// Make constructors visible
|
||||
using Spool::FlushLocked;
|
||||
using Spool::Spool;
|
||||
|
||||
std::string GetTypeUrl() { return type_url_; }
|
||||
@@ -123,7 +124,7 @@ using santa::santad::logs::endpoint_security::writers::SpoolPeer;
|
||||
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count], 0);
|
||||
|
||||
// Manual Flush
|
||||
XCTAssertTrue(spool->Flush());
|
||||
XCTAssertTrue(spool->FlushLocked());
|
||||
|
||||
// A new log entry should exist
|
||||
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count], 1);
|
||||
|
||||
@@ -26,6 +26,7 @@ class Syslog : public Writer {
|
||||
static std::shared_ptr<Syslog> Create();
|
||||
|
||||
void Write(std::vector<uint8_t>&& bytes) override;
|
||||
void Flush() override;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
@@ -26,4 +26,8 @@ void Syslog::Write(std::vector<uint8_t> &&bytes) {
|
||||
os_log(OS_LOG_DEFAULT, "%{public}s", bytes.data());
|
||||
}
|
||||
|
||||
void Syslog::Flush() {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
@@ -24,6 +24,7 @@ class Writer {
|
||||
virtual ~Writer() = default;
|
||||
|
||||
virtual void Write(std::vector<uint8_t>&& bytes) = 0;
|
||||
virtual void Flush() = 0;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
@@ -49,10 +49,11 @@ using EventTimesTuple = std::tuple<Processor, es_event_type_t>;
|
||||
|
||||
class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
public:
|
||||
static std::shared_ptr<Metrics> Create(SNTMetricSet *metricSet, uint64_t interval);
|
||||
static std::shared_ptr<Metrics> Create(SNTMetricSet *metric_set, uint64_t interval);
|
||||
|
||||
Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
|
||||
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
|
||||
SNTMetricCounter *rate_limit_counts, SNTMetricSet *metric_set,
|
||||
void (^run_on_first_start)(Metrics *));
|
||||
|
||||
~Metrics();
|
||||
@@ -62,24 +63,32 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
|
||||
void StopPoll();
|
||||
void SetInterval(uint64_t interval);
|
||||
|
||||
void FlushMetrics();
|
||||
// Force an immediate flush and export of metrics
|
||||
void Export();
|
||||
|
||||
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:
|
||||
void FlushMetrics();
|
||||
void ExportLocked(SNTMetricSet *metric_set);
|
||||
|
||||
MOLXPCConnection *metrics_connection_;
|
||||
dispatch_queue_t q_;
|
||||
dispatch_source_t timer_source_;
|
||||
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
|
||||
// suspended, resumed, or cancelled while in an improper state.
|
||||
bool running_;
|
||||
bool running_ = false;
|
||||
void (^run_on_first_start_)(Metrics *);
|
||||
|
||||
// Separate queue used for setting event metrics
|
||||
@@ -89,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
|
||||
|
||||
@@ -108,27 +108,33 @@ NSString *const EventDispositionToString(EventDisposition d) {
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metricSet, uint64_t interval) {
|
||||
std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t interval) {
|
||||
dispatch_queue_t q = dispatch_queue_create("com.google.santa.santametricsservice.q",
|
||||
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
|
||||
|
||||
dispatch_source_t timer_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
|
||||
|
||||
SNTMetricInt64Gauge *event_processing_times =
|
||||
[metricSet int64GaugeWithName:@"/santa/event_processing_time"
|
||||
fieldNames:@[ @"Processor", @"Event" ]
|
||||
helpText:@"Time to process various event types by each processor"];
|
||||
[metric_set int64GaugeWithName:@"/santa/event_processing_time"
|
||||
fieldNames:@[ @"Processor", @"Event" ]
|
||||
helpText:@"Time to process various event types by each processor"];
|
||||
|
||||
SNTMetricCounter *event_counts =
|
||||
[metricSet counterWithName:@"/santa/event_count"
|
||||
fieldNames:@[ @"Processor", @"Event", @"Disposition" ]
|
||||
helpText:@"Events received and processed by each processor"];
|
||||
[metric_set counterWithName:@"/santa/event_count"
|
||||
fieldNames:@[ @"Processor", @"Event", @"Disposition" ]
|
||||
helpText:@"Events received and processed by each processor"];
|
||||
|
||||
std::shared_ptr<Metrics> metrics = std::make_shared<Metrics>(
|
||||
q, timer_source, interval, event_processing_times, event_counts, ^(Metrics *metrics) {
|
||||
SNTRegisterCoreMetrics();
|
||||
metrics->EstablishConnection();
|
||||
});
|
||||
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,
|
||||
rate_limit_counts, metric_set, ^(Metrics *metrics) {
|
||||
SNTRegisterCoreMetrics();
|
||||
metrics->EstablishConnection();
|
||||
});
|
||||
|
||||
std::weak_ptr<Metrics> weak_metrics(metrics);
|
||||
dispatch_source_set_event_handler(metrics->timer_source_, ^{
|
||||
@@ -137,10 +143,7 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metricSet, uint64_t inter
|
||||
return;
|
||||
}
|
||||
|
||||
shared_metrics->FlushMetrics();
|
||||
|
||||
[[shared_metrics->metrics_connection_ remoteObjectProxy]
|
||||
exportForMonitoring:[metricSet export]];
|
||||
shared_metrics->ExportLocked(metric_set);
|
||||
});
|
||||
|
||||
return metrics;
|
||||
@@ -148,13 +151,15 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metricSet, uint64_t inter
|
||||
|
||||
Metrics::Metrics(dispatch_queue_t q, dispatch_source_t timer_source, uint64_t interval,
|
||||
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
|
||||
SNTMetricCounter *rate_limit_counts, 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),
|
||||
running_(false),
|
||||
rate_limit_counts_(rate_limit_counts),
|
||||
metric_set_(metric_set),
|
||||
run_on_first_start_(run_on_first_start) {
|
||||
SetInterval(interval_);
|
||||
|
||||
@@ -184,6 +189,17 @@ void Metrics::EstablishConnection() {
|
||||
metrics_connection_ = metrics_connection;
|
||||
}
|
||||
|
||||
void Metrics::Export() {
|
||||
dispatch_sync(q_, ^{
|
||||
ExportLocked(metric_set_);
|
||||
});
|
||||
}
|
||||
|
||||
void Metrics::ExportLocked(SNTMetricSet *metric_set) {
|
||||
FlushMetrics();
|
||||
[[metrics_connection_ remoteObjectProxy] exportForMonitoring:[metric_set export]];
|
||||
}
|
||||
|
||||
void Metrics::FlushMetrics() {
|
||||
dispatch_sync(events_q_, ^{
|
||||
for (const auto &kv : event_counts_cache_) {
|
||||
@@ -202,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_ = {};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -254,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
|
||||
|
||||
@@ -39,6 +39,7 @@ extern NSString *const EventDispositionToString(EventDisposition d);
|
||||
class MetricsPeer : public Metrics {
|
||||
public:
|
||||
// Make base class constructors visible
|
||||
using Metrics::FlushMetrics;
|
||||
using Metrics::Metrics;
|
||||
|
||||
bool IsRunning() { return running_; }
|
||||
@@ -47,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
|
||||
@@ -71,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, ^(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());
|
||||
|
||||
@@ -107,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,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
});
|
||||
|
||||
@@ -181,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,
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, nil, nil, nil, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
@@ -220,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]);
|
||||
@@ -238,33 +266,38 @@ 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,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
auto metrics = std::make_shared<MetricsPeer>(self.q, timer, 100, mockEventProcessingTimes,
|
||||
mockEventCounts, mockEventCounts, nil,
|
||||
^(santa::santad::Metrics *m){
|
||||
// This block intentionally left blank
|
||||
});
|
||||
|
||||
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
|
||||
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
|
||||
|
||||
@@ -47,6 +47,29 @@ static void RegisterModeMetric(SNTMetricSet *metricSet) {
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the event log type metric checking the config before reporting the status.
|
||||
*/
|
||||
static void RegisterEventLogType(SNTMetricSet *metricSet) {
|
||||
SNTMetricStringGauge *logType = [metricSet stringGaugeWithName:@"/santa/log_type"
|
||||
fieldNames:@[]
|
||||
helpText:@"Santa's log type"];
|
||||
|
||||
// create a callback that gets the current log type
|
||||
[metricSet registerCallback:^{
|
||||
switch ([[SNTConfigurator configurator] eventLogType]) {
|
||||
case SNTEventLogTypeProtobuf: [logType set:@"protobuf" forFieldValues:@[]]; break;
|
||||
case SNTEventLogTypeSyslog: [logType set:@"syslog" forFieldValues:@[]]; break;
|
||||
case SNTEventLogTypeNull: [logType set:@"null" forFieldValues:@[]]; break;
|
||||
case SNTEventLogTypeFilelog: [logType set:@"file" forFieldValues:@[]]; break;
|
||||
default:
|
||||
// Should never be reached.
|
||||
[logType set:@"unknown" forFieldValues:@[]];
|
||||
break;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register metrics for measuring memory usage.
|
||||
*/
|
||||
@@ -129,6 +152,7 @@ static void RegisterCommonSantaMetrics(SNTMetricSet *metricSet) {
|
||||
value:[SNTSystemInfo osVersion]];
|
||||
|
||||
RegisterModeMetric(metricSet);
|
||||
RegisterEventLogType(metricSet);
|
||||
// TODO(markowsky) Register CSR status
|
||||
// TODO(markowsky) Register system extension status
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
- (void)testRegisteringCoreMetrics {
|
||||
OCMStub([self.mockConfigurator extraMetricLabels]).andReturn(self.extraMetricLabels);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
OCMStub([self.mockConfigurator eventLogType]).andReturn(SNTEventLogTypeProtobuf);
|
||||
|
||||
SNTRegisterCoreMetrics();
|
||||
|
||||
@@ -191,6 +192,18 @@
|
||||
} ],
|
||||
},
|
||||
},
|
||||
@"/santa/log_type" : @{
|
||||
@"description" : @"Santa's log type",
|
||||
@"type" : @6,
|
||||
@"fields" : @{
|
||||
@"" : @[ @{
|
||||
@"created" : fixedDate,
|
||||
@"data" : @"protobuf",
|
||||
@"last_updated" : fixedDate,
|
||||
@"value" : @""
|
||||
} ],
|
||||
},
|
||||
},
|
||||
},
|
||||
@"root_labels" : @{
|
||||
@"host_name" : hostname,
|
||||
|
||||
@@ -77,7 +77,7 @@ double watchdogRAMPeak = 0;
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Kernel ops
|
||||
#pragma mark Cache ops
|
||||
|
||||
- (void)cacheCounts:(void (^)(uint64_t, uint64_t))reply {
|
||||
NSArray<NSNumber *> *counts = self->_authResultCache->CacheCounts();
|
||||
|
||||
@@ -25,6 +25,6 @@
|
||||
- (void)cacheDecision:(SNTCachedDecision *)cd;
|
||||
- (SNTCachedDecision *)cachedDecisionForFile:(const struct stat &)statInfo;
|
||||
- (void)forgetCachedDecisionForFile:(const struct stat &)statInfo;
|
||||
- (void)resetTimestampForCachedDecision:(const struct stat &)statInfo;
|
||||
- (SNTCachedDecision *)resetTimestampForCachedDecision:(const struct stat &)statInfo;
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,17 +17,20 @@
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#import "Source/common/SNTRule.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/common/SantaVnode.h"
|
||||
#include "Source/common/SantaVnodeHash.h"
|
||||
#import "Source/santad/DataLayer/SNTRuleTable.h"
|
||||
#import "Source/santad/SNTDatabaseController.h"
|
||||
|
||||
@interface SNTDecisionCache ()
|
||||
@property NSMutableDictionary<NSNumber *, SNTCachedDecision *> *detailStore;
|
||||
@property dispatch_queue_t detailStoreQueue;
|
||||
// Cache for sha256 -> date of last timestamp reset.
|
||||
@property NSCache<NSString *, NSDate *> *timestampResetMap;
|
||||
@end
|
||||
|
||||
@implementation SNTDecisionCache
|
||||
@implementation SNTDecisionCache {
|
||||
SantaCache<SantaVnode, SNTCachedDecision *> _decisionCache;
|
||||
}
|
||||
|
||||
+ (instancetype)sharedCache {
|
||||
static SNTDecisionCache *cache;
|
||||
@@ -41,15 +44,6 @@
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
// TODO(mlw): We should protect this structure with a read/write lock
|
||||
// instead of a serial dispatch queue since it's expected that most most
|
||||
// accesses will be lookups, not caching new items.
|
||||
_detailStore = [NSMutableDictionary dictionaryWithCapacity:1000];
|
||||
_detailStoreQueue = dispatch_queue_create(
|
||||
"com.google.santa.daemon.detail_store",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
|
||||
_timestampResetMap = [[NSCache alloc] init];
|
||||
_timestampResetMap.countLimit = 100;
|
||||
}
|
||||
@@ -57,32 +51,24 @@
|
||||
}
|
||||
|
||||
- (void)cacheDecision:(SNTCachedDecision *)cd {
|
||||
dispatch_sync(self.detailStoreQueue, ^{
|
||||
self.detailStore[@(cd.vnodeId.fileid)] = cd;
|
||||
});
|
||||
self->_decisionCache.set(cd.vnodeId, cd);
|
||||
}
|
||||
|
||||
- (SNTCachedDecision *)cachedDecisionForFile:(const struct stat &)statInfo {
|
||||
__block SNTCachedDecision *cd;
|
||||
dispatch_sync(self.detailStoreQueue, ^{
|
||||
cd = self.detailStore[@(statInfo.st_ino)];
|
||||
});
|
||||
return cd;
|
||||
return self->_decisionCache.get(SantaVnode::VnodeForFile(statInfo));
|
||||
}
|
||||
|
||||
- (void)forgetCachedDecisionForFile:(const struct stat &)statInfo {
|
||||
dispatch_sync(self.detailStoreQueue, ^{
|
||||
[self.detailStore removeObjectForKey:@(statInfo.st_ino)];
|
||||
});
|
||||
self->_decisionCache.remove(SantaVnode::VnodeForFile(statInfo));
|
||||
}
|
||||
|
||||
// Whenever a cached decision resulting from a transitive allowlist rule is used to allow the
|
||||
// execution of a binary, we update the timestamp on the transitive rule in the rules database.
|
||||
// To prevent writing to the database too often, we space out consecutive writes by 3600 seconds.
|
||||
- (void)resetTimestampForCachedDecision:(const struct stat &)statInfo {
|
||||
- (SNTCachedDecision *)resetTimestampForCachedDecision:(const struct stat &)statInfo {
|
||||
SNTCachedDecision *cd = [self cachedDecisionForFile:statInfo];
|
||||
if (!cd || cd.decision != SNTEventStateAllowTransitive || !cd.sha256) {
|
||||
return;
|
||||
return cd;
|
||||
}
|
||||
|
||||
NSDate *lastUpdate = [self.timestampResetMap objectForKey:cd.sha256];
|
||||
@@ -94,6 +80,8 @@
|
||||
[[SNTDatabaseController ruleTable] resetTimestampForRule:rule];
|
||||
[self.timestampResetMap setObject:[NSDate date] forKey:cd.sha256];
|
||||
}
|
||||
|
||||
return cd;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -32,7 +32,7 @@ SNTCachedDecision *MakeCachedDecision(struct stat sb, SNTEventState decision) {
|
||||
cd.decision = decision;
|
||||
cd.sha256 = @"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
||||
cd.vnodeId = {
|
||||
.fsid = 0,
|
||||
.fsid = sb.st_dev,
|
||||
.fileid = sb.st_ino,
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "Source/santad/Santad.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/PrefixTree.h"
|
||||
@@ -296,12 +297,27 @@ 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]
|
||||
@@ -310,6 +326,36 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
|
||||
LOGI(@"StaticRules set has changed, flushing cache.");
|
||||
auth_result_cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
}],
|
||||
[[SNTKVOManager alloc]
|
||||
initWithObject:configurator
|
||||
selector:@selector(eventLogType)
|
||||
type:[NSNumber class]
|
||||
callback:^(NSNumber *oldValue, NSNumber *newValue) {
|
||||
NSInteger oldLogType = [oldValue integerValue];
|
||||
NSInteger newLogType = [newValue integerValue];
|
||||
|
||||
if (oldLogType == newLogType) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGW(@"EventLogType config changed (%ld --> %ld). Restarting...", oldLogType,
|
||||
newLogType);
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
logger->Flush();
|
||||
metrics->Export();
|
||||
|
||||
dispatch_semaphore_signal(sema);
|
||||
});
|
||||
|
||||
// Wait for a short amount of time for outstanding data to flush
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
|
||||
|
||||
// Forcefully exit. The daemon will be restarted immediately.
|
||||
exit(EXIT_SUCCESS);
|
||||
}],
|
||||
];
|
||||
|
||||
// Make the compiler happy. The variable is only used to ensure proper lifetime
|
||||
|
||||
@@ -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);
|
||||
|
||||
10
Source/santad/testdata/protobuf/v1/exec.json
vendored
10
Source/santad/testdata/protobuf/v1/exec.json
vendored
@@ -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",
|
||||
|
||||
10
Source/santad/testdata/protobuf/v2/exec.json
vendored
10
Source/santad/testdata/protobuf/v2/exec.json
vendored
@@ -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",
|
||||
|
||||
10
Source/santad/testdata/protobuf/v4/exec.json
vendored
10
Source/santad/testdata/protobuf/v4/exec.json
vendored
@@ -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": [
|
||||
{
|
||||
|
||||
10
Source/santad/testdata/protobuf/v5/exec.json
vendored
10
Source/santad/testdata/protobuf/v5/exec.json
vendored
@@ -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": [
|
||||
{
|
||||
|
||||
10
Source/santad/testdata/protobuf/v6/exec.json
vendored
10
Source/santad/testdata/protobuf/v6/exec.json
vendored
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -129,6 +129,8 @@ NSDictionary *validMetricsDict = nil;
|
||||
|
||||
OCMStub([self.mockMOLAuthenticatingURLSession alloc])
|
||||
.andReturn(self.mockMOLAuthenticatingURLSession);
|
||||
OCMStub([self.mockMOLAuthenticatingURLSession initWithSessionConfiguration:[OCMArg any]])
|
||||
.andReturn(self.mockMOLAuthenticatingURLSession);
|
||||
OCMStub([self.mockMOLAuthenticatingURLSession session]).andReturn(self.mockSession);
|
||||
|
||||
NSHTTPURLResponse *response =
|
||||
|
||||
@@ -18,94 +18,99 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/santametricservice/Writers/SNTMetricHTTPWriter.h"
|
||||
|
||||
@implementation SNTMetricHTTPWriter {
|
||||
@private
|
||||
MOLAuthenticatingURLSession *_authSession;
|
||||
}
|
||||
@interface SNTMetricHTTPWriter ()
|
||||
@property SNTConfigurator *configurator;
|
||||
@end
|
||||
|
||||
@implementation SNTMetricHTTPWriter
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_authSession = [[MOLAuthenticatingURLSession alloc] init];
|
||||
_configurator = [SNTConfigurator configurator];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (MOLAuthenticatingURLSession *)createSessionWithHostname:(NSURL *)url
|
||||
Timeout:(NSTimeInterval)timeout {
|
||||
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||
config.TLSMinimumSupportedProtocolVersion = tls_protocol_version_TLSv12;
|
||||
config.HTTPShouldUsePipelining = YES;
|
||||
config.timeoutIntervalForResource = timeout;
|
||||
|
||||
MOLAuthenticatingURLSession *session =
|
||||
[[MOLAuthenticatingURLSession alloc] initWithSessionConfiguration:config];
|
||||
session.serverHostname = url.host;
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post serialzied metrics to the specified URL one object at a time.
|
||||
**/
|
||||
- (BOOL)write:(NSArray<NSData *> *)metrics toURL:(NSURL *)url error:(NSError **)error {
|
||||
__block NSError *_blockError = nil;
|
||||
NSError *localError;
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
MOLAuthenticatingURLSession *authSession =
|
||||
[self createSessionWithHostname:url Timeout:self.configurator.metricExportTimeout];
|
||||
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
||||
request.HTTPMethod = @"POST";
|
||||
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
||||
|
||||
_authSession.serverHostname = url.host;
|
||||
NSURLSession *_session = _authSession.session;
|
||||
for (NSData *metric in metrics) {
|
||||
NSError *taskError;
|
||||
NSURLResponse *taskResponse;
|
||||
request.HTTPBody = (NSData *)metric;
|
||||
|
||||
dispatch_group_t requests = dispatch_group_create();
|
||||
// Note: In order to help ease mock writing in tests, the `task` variable is
|
||||
// sequestered into this anonymous scope to help ensure future edits outside
|
||||
// of this scope don'taccess the `task` variable which could mess up how the
|
||||
// tests are written.
|
||||
{
|
||||
NSURLSessionDataTask *task = [authSession.session
|
||||
dataTaskWithRequest:request
|
||||
completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response,
|
||||
NSError *_Nullable err) {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
[metrics enumerateObjectsUsingBlock:^(id value, NSUInteger index, BOOL *stop) {
|
||||
dispatch_group_enter(requests);
|
||||
[task resume];
|
||||
|
||||
request.HTTPBody = (NSData *)value;
|
||||
NSURLSessionDataTask *task = [_session
|
||||
dataTaskWithRequest:request
|
||||
completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response,
|
||||
NSError *_Nullable err) {
|
||||
if (err != nil) {
|
||||
_blockError = err;
|
||||
*stop = YES;
|
||||
} else if (response == nil) {
|
||||
*stop = YES;
|
||||
} else if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
||||
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
||||
|
||||
// Check HTTP error codes and create errors for any non-200.
|
||||
if (httpResponse && httpResponse.statusCode != 200) {
|
||||
_blockError = [[NSError alloc]
|
||||
initWithDomain:@"com.google.santa.metricservice.writers.http"
|
||||
code:httpResponse.statusCode
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey :
|
||||
[NSString stringWithFormat:@"received http status code %ld from %@",
|
||||
httpResponse.statusCode, url]
|
||||
}];
|
||||
*stop = YES;
|
||||
}
|
||||
}
|
||||
dispatch_group_leave(requests);
|
||||
}];
|
||||
|
||||
[task resume];
|
||||
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
int64_t timeout = (int64_t)config.metricExportTimeout;
|
||||
|
||||
// Wait up to timeout seconds for the request to complete.
|
||||
if (dispatch_group_wait(requests, dispatch_time(DISPATCH_TIME_NOW, (timeout * NSEC_PER_SEC))) !=
|
||||
0) {
|
||||
[task cancel];
|
||||
NSString *errMsg =
|
||||
[NSString stringWithFormat:@"HTTP request to %@ timed out after %lu seconds", url,
|
||||
(unsigned long)timeout];
|
||||
|
||||
_blockError = [[NSError alloc] initWithDomain:@"com.google.santa.metricservice.writers.http"
|
||||
code:ETIMEDOUT
|
||||
userInfo:@{NSLocalizedDescriptionKey : errMsg}];
|
||||
}
|
||||
}];
|
||||
|
||||
if (_blockError != nil) {
|
||||
// If the caller hasn't passed us an error then we ignore it.
|
||||
if (error != nil) {
|
||||
*error = [_blockError copy];
|
||||
// Note: Extracting property values once here to make test mock writing simpler
|
||||
taskError = task.error;
|
||||
taskResponse = task.response;
|
||||
}
|
||||
|
||||
return NO;
|
||||
// Note: localError will only store the last error that occured while
|
||||
// sending items from the array of metrics.
|
||||
if (taskError) {
|
||||
localError = taskError;
|
||||
} else if ([taskResponse isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
NSInteger statusCode = ((NSHTTPURLResponse *)taskResponse).statusCode;
|
||||
if (statusCode != 200) {
|
||||
localError = [[NSError alloc]
|
||||
initWithDomain:@"com.google.santa.metricservice.writers.http"
|
||||
code:statusCode
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : [NSString
|
||||
stringWithFormat:@"received http status code %ld from %@", statusCode, url]
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
if (error != nil) {
|
||||
*error = localError;
|
||||
}
|
||||
|
||||
// Success is determined by whether or not any failures occured while sending
|
||||
// any of the metrics.
|
||||
return localError == nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
OCMStub([self.mockMOLAuthenticatingURLSession alloc])
|
||||
.andReturn(self.mockMOLAuthenticatingURLSession);
|
||||
OCMStub([self.mockMOLAuthenticatingURLSession initWithSessionConfiguration:[OCMArg any]])
|
||||
.andReturn(self.mockMOLAuthenticatingURLSession);
|
||||
OCMStub([self.mockMOLAuthenticatingURLSession session]).andReturn(self.mockSession);
|
||||
|
||||
self.httpWriter = [[SNTMetricHTTPWriter alloc] init];
|
||||
@@ -46,6 +48,15 @@
|
||||
|
||||
void (^callCompletionHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
|
||||
NSDictionary *responseValue = self.mockResponses[0];
|
||||
|
||||
if (responseValue[@"error"] != nil) {
|
||||
OCMExpect([(NSURLSessionDataTask *)self.mockSessionDataTask error])
|
||||
.andReturn(responseValue[@"error"]);
|
||||
} else if (((NSHTTPURLResponse *)responseValue[@"response"]).statusCode != 200) {
|
||||
OCMExpect([(NSURLSessionDataTask *)self.mockSessionDataTask response])
|
||||
.andReturn(responseValue[@"response"]);
|
||||
}
|
||||
|
||||
if (responseValue != nil && completionHandler != nil) {
|
||||
completionHandler(responseValue[@"data"], responseValue[@"response"],
|
||||
responseValue[@"error"]);
|
||||
|
||||
@@ -30,52 +30,49 @@
|
||||
- (BOOL)sync {
|
||||
[self performRequest:[self requestWithDictionary:nil]];
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
void (^replyBlock)(void) = ^{
|
||||
dispatch_group_leave(group);
|
||||
};
|
||||
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
|
||||
|
||||
// Set client mode if it changed
|
||||
if (self.syncState.clientMode) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setClientMode:self.syncState.clientMode reply:replyBlock];
|
||||
[rop setClientMode:self.syncState.clientMode
|
||||
reply:^{
|
||||
}];
|
||||
}
|
||||
|
||||
// Remove clean sync flag if we did a clean sync
|
||||
if (self.syncState.cleanSync) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setSyncCleanRequired:NO reply:replyBlock];
|
||||
[rop setSyncCleanRequired:NO
|
||||
reply:^{
|
||||
}];
|
||||
}
|
||||
|
||||
// Update allowlist/blocklist regexes
|
||||
if (self.syncState.allowlistRegex) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setAllowedPathRegex:self.syncState.allowlistRegex
|
||||
reply:replyBlock];
|
||||
[rop setAllowedPathRegex:self.syncState.allowlistRegex
|
||||
reply:^{
|
||||
}];
|
||||
}
|
||||
if (self.syncState.blocklistRegex) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setBlockedPathRegex:self.syncState.blocklistRegex
|
||||
reply:replyBlock];
|
||||
[rop setBlockedPathRegex:self.syncState.blocklistRegex
|
||||
reply:^{
|
||||
}];
|
||||
}
|
||||
|
||||
if (self.syncState.blockUSBMount != nil) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setBlockUSBMount:[self.syncState.blockUSBMount boolValue]
|
||||
reply:replyBlock];
|
||||
[rop setBlockUSBMount:[self.syncState.blockUSBMount boolValue]
|
||||
reply:^{
|
||||
}];
|
||||
}
|
||||
if (self.syncState.remountUSBMode) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setRemountUSBMode:self.syncState.remountUSBMode
|
||||
reply:replyBlock];
|
||||
[rop setRemountUSBMode:self.syncState.remountUSBMode
|
||||
reply:^{
|
||||
}];
|
||||
}
|
||||
|
||||
// Update last sync success
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] setFullSyncLastSuccess:[NSDate date] reply:replyBlock];
|
||||
|
||||
// Wait for dispatch group
|
||||
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
|
||||
[rop setFullSyncLastSuccess:[NSDate date]
|
||||
reply:^{
|
||||
}];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@@ -44,42 +44,32 @@
|
||||
requestDict[kFCMToken] = self.syncState.pushNotificationsToken;
|
||||
}
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler, int64_t transitive,
|
||||
int64_t teamID) {
|
||||
requestDict[kBinaryRuleCount] = @(binary);
|
||||
requestDict[kCertificateRuleCount] = @(certificate);
|
||||
requestDict[kCompilerRuleCount] = @(compiler);
|
||||
requestDict[kTransitiveRuleCount] = @(transitive);
|
||||
requestDict[kTeamIDRuleCount] = @(teamID);
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
|
||||
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] clientMode:^(SNTClientMode cm) {
|
||||
// dispatch_group_t group = dispatch_group_create();
|
||||
// dispatch_group_enter(group);
|
||||
[rop databaseRuleCounts:^(int64_t binary, int64_t certificate, int64_t compiler,
|
||||
int64_t transitive, int64_t teamID) {
|
||||
requestDict[kBinaryRuleCount] = @(binary);
|
||||
requestDict[kCertificateRuleCount] = @(certificate);
|
||||
requestDict[kCompilerRuleCount] = @(compiler);
|
||||
requestDict[kTransitiveRuleCount] = @(transitive);
|
||||
requestDict[kTeamIDRuleCount] = @(teamID);
|
||||
}];
|
||||
|
||||
[rop clientMode:^(SNTClientMode cm) {
|
||||
switch (cm) {
|
||||
case SNTClientModeMonitor: requestDict[kClientMode] = kClientModeMonitor; break;
|
||||
case SNTClientModeLockdown: requestDict[kClientMode] = kClientModeLockdown; break;
|
||||
default: break;
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
__block BOOL syncClean = NO;
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] syncCleanRequired:^(BOOL clean) {
|
||||
[rop syncCleanRequired:^(BOOL clean) {
|
||||
syncClean = clean;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
|
||||
// Stop the sync if we are unable to communicate with daemon.
|
||||
if (dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC))) {
|
||||
SLOGE(@"Unable to communicate with daemon.");
|
||||
return NO;
|
||||
}
|
||||
|
||||
// If user requested it or we've never had a successful sync, try from a clean slate.
|
||||
if (syncClean) {
|
||||
SLOGD(@"Clean sync requested by user");
|
||||
@@ -91,38 +81,29 @@
|
||||
|
||||
if (!resp) return NO;
|
||||
|
||||
dispatch_group_enter(group);
|
||||
NSNumber *enableBundles = resp[kEnableBundles];
|
||||
if (!enableBundles) enableBundles = resp[kEnableBundlesDeprecated];
|
||||
[[self.daemonConn remoteObjectProxy] setEnableBundles:[enableBundles boolValue]
|
||||
reply:^{
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
[rop setEnableBundles:[enableBundles boolValue]
|
||||
reply:^{
|
||||
}];
|
||||
|
||||
dispatch_group_enter(group);
|
||||
NSNumber *enableTransitiveRules = resp[kEnableTransitiveRules];
|
||||
if (!enableTransitiveRules) enableTransitiveRules = resp[kEnableTransitiveRulesDeprecated];
|
||||
if (!enableTransitiveRules) enableTransitiveRules = resp[kEnableTransitiveRulesSuperDeprecated];
|
||||
BOOL enabled = [enableTransitiveRules boolValue];
|
||||
[[self.daemonConn remoteObjectProxy] setEnableTransitiveRules:enabled
|
||||
reply:^{
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
[rop setEnableTransitiveRules:enabled
|
||||
reply:^{
|
||||
}];
|
||||
|
||||
dispatch_group_enter(group);
|
||||
NSNumber *enableAllEventUpload = resp[kEnableAllEventUpload];
|
||||
[[self.daemonConn remoteObjectProxy] setEnableAllEventUpload:[enableAllEventUpload boolValue]
|
||||
reply:^{
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
[rop setEnableAllEventUpload:[enableAllEventUpload boolValue]
|
||||
reply:^{
|
||||
}];
|
||||
|
||||
dispatch_group_enter(group);
|
||||
NSNumber *disableUnknownEventUpload = resp[kDisableUnknownEventUpload];
|
||||
[[self.daemonConn remoteObjectProxy]
|
||||
setDisableUnknownEventUpload:[disableUnknownEventUpload boolValue]
|
||||
reply:^{
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
[rop setDisableUnknownEventUpload:[disableUnknownEventUpload boolValue]
|
||||
reply:^{
|
||||
}];
|
||||
|
||||
self.syncState.eventBatchSize = [resp[kBatchSize] unsignedIntegerValue] ?: kDefaultEventBatchSize;
|
||||
|
||||
@@ -170,7 +151,6 @@
|
||||
self.syncState.cleanSync = YES;
|
||||
}
|
||||
|
||||
dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC));
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
@property(readwrite) NSURLSession *urlSession;
|
||||
@property(readwrite) SNTSyncState *syncState;
|
||||
@property(readwrite) MOLXPCConnection *daemonConn;
|
||||
@property BOOL xsrfFetched;
|
||||
|
||||
@end
|
||||
|
||||
@@ -68,7 +67,8 @@
|
||||
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 = [requestBody zlibCompressed];
|
||||
if (compressed) {
|
||||
@@ -105,7 +105,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;
|
||||
}
|
||||
@@ -189,8 +190,7 @@
|
||||
|
||||
- (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 +199,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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
self.syncState.daemonConn = OCMClassMock([MOLXPCConnection class]);
|
||||
self.daemonConnRop = OCMProtocolMock(@protocol(SNTDaemonControlXPC));
|
||||
OCMStub([self.syncState.daemonConn remoteObjectProxy]).andReturn(self.daemonConnRop);
|
||||
OCMStub([self.syncState.daemonConn synchronousRemoteObjectProxy]).andReturn(self.daemonConnRop);
|
||||
|
||||
self.syncState.session = OCMClassMock([NSURLSession class]);
|
||||
|
||||
@@ -197,6 +198,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 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user