mirror of
https://github.com/google/santa.git
synced 2026-01-18 02:37:53 -05:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72969a3c92 | ||
|
|
d2dbed78dd | ||
|
|
8fa91e4ff0 | ||
|
|
551763146d | ||
|
|
7a7f0cd5a8 | ||
|
|
fcb49701b3 | ||
|
|
c9ef723fc5 | ||
|
|
dc6732ef04 | ||
|
|
a48900a4ae | ||
|
|
bb49118d94 | ||
|
|
456333d6d2 |
18
.allstar/binary_artifacts.yaml
Normal file
18
.allstar/binary_artifacts.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
# Ignore reason: These crafted binaries are used in tests
|
||||
ignorePaths:
|
||||
- Source/common/testdata/bad_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
- Source/common/testdata/32bitplist
|
||||
- Source/common/testdata/BundleExample.app/Contents/MacOS/BundleExample
|
||||
- Source/common/testdata/DirectoryBundle/Contents/MacOS/DirectoryBundle
|
||||
- Source/common/testdata/DirectoryBundle/Contents/Resources/BundleExample.app/Contents/MacOS/BundleExample
|
||||
- Source/santad/testdata/binaryrules/badbinary
|
||||
- Source/santad/testdata/binaryrules/goodbinary
|
||||
- Source/santad/testdata/binaryrules/badcert
|
||||
- Source/santad/testdata/binaryrules/banned_teamid_allowed_binary
|
||||
- Source/santad/testdata/binaryrules/banned_teamid
|
||||
- Source/santad/testdata/binaryrules/goodcert
|
||||
- Source/santad/testdata/binaryrules/noop
|
||||
- Source/santad/testdata/binaryrules/rules.db
|
||||
11
.bazelrc
11
.bazelrc
@@ -3,3 +3,14 @@ build --apple_generate_dsym --define=apple.propagate_embedded_extra_outputs=yes
|
||||
build --copt=-Werror
|
||||
build --copt=-Wall
|
||||
build --copt=-Wno-error=deprecated-declarations
|
||||
build --per_file_copt=.*\.mm\$@-std=c++17
|
||||
build --cxxopt=-std=c++17
|
||||
|
||||
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"
|
||||
build:asan --copt="-fsanitize=address"
|
||||
build:asan --copt="-DADDRESS_SANITIZER"
|
||||
build:asan --linkopt="-fsanitize=address"
|
||||
|
||||
@@ -1 +1 @@
|
||||
5.0.0
|
||||
5.3.0
|
||||
|
||||
2
.github/workflows/check-markdown.yml
vendored
2
.github/workflows/check-markdown.yml
vendored
@@ -11,4 +11,4 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
- run: "! git grep -EIn $'[ \t]+$'"
|
||||
- run: "! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'"
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -20,7 +20,6 @@ jobs:
|
||||
- name: Run linters
|
||||
run: ./Testing/lint.sh
|
||||
|
||||
|
||||
build_userspace:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -55,10 +54,3 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path-to-lcov: ./bazel-out/_coverage/_coverage_report.dat
|
||||
flag-name: Unit
|
||||
|
||||
benchmark:
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run All Tests
|
||||
run: ./Testing/benchmark.sh
|
||||
|
||||
7
BUILD
7
BUILD
@@ -198,10 +198,3 @@ test_suite(
|
||||
"//Source/santasyncservice:unit_tests",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "benchmarks",
|
||||
tests = [
|
||||
"//Source/santad:SNTApplicationBenchmark",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Santa [](https://github.com/google/santa/actions/workflows/ci.yml) [](https://coveralls.io/github/google/santa?branch=main)
|
||||
# Santa [](https://github.com/google/santa/actions/workflows/ci.yml)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/santa/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/gui/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
</p>
|
||||
|
||||
Santa is a binary authorization system for macOS. It consists of a system
|
||||
|
||||
@@ -83,12 +83,6 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTAllowlistInfo",
|
||||
srcs = ["SNTAllowlistInfo.m"],
|
||||
hdrs = ["SNTAllowlistInfo.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTCommonEnums",
|
||||
hdrs = ["SNTCommonEnums.h"],
|
||||
@@ -106,6 +100,23 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTKVOManager",
|
||||
srcs = ["SNTKVOManager.mm"],
|
||||
hdrs = ["SNTKVOManager.h"],
|
||||
deps = [
|
||||
":SNTLogging",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTKVOManagerTest",
|
||||
srcs = ["SNTKVOManagerTest.mm"],
|
||||
deps = [
|
||||
":SNTKVOManager",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTDropRootPrivs",
|
||||
srcs = ["SNTDropRootPrivs.m"],
|
||||
@@ -117,6 +128,7 @@ objc_library(
|
||||
srcs = ["SNTFileInfo.m"],
|
||||
hdrs = ["SNTFileInfo.h"],
|
||||
deps = [
|
||||
":SNTLogging",
|
||||
"@FMDB",
|
||||
"@MOLCodesignChecker",
|
||||
],
|
||||
@@ -298,13 +310,40 @@ santa_unit_test(
|
||||
deps = [":SNTMetricSet"],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTCachedDecisionTest",
|
||||
srcs = ["SNTCachedDecisionTest.mm"],
|
||||
deps = [
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":SNTCachedDecisionTest",
|
||||
":SNTFileInfoTest",
|
||||
":SNTKVOManagerTest",
|
||||
":SNTMetricSetTest",
|
||||
":SNTPrefixTreeTest",
|
||||
":SNTRuleTest",
|
||||
":SantaCacheTest",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "TestUtils",
|
||||
testonly = 1,
|
||||
srcs = ["TestUtils.mm"],
|
||||
hdrs = ["TestUtils.h"],
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTAllowlistInfo.h"
|
||||
|
||||
@implementation SNTAllowlistInfo
|
||||
|
||||
- (instancetype)initWithPid:(pid_t)pid
|
||||
pidversion:(int)pidver
|
||||
targetPath:(NSString *)targetPath
|
||||
sha256:(NSString *)hash {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_pid = pid;
|
||||
_pidversion = pidver;
|
||||
_targetPath = targetPath;
|
||||
_sha256 = hash;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,10 +12,11 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
|
||||
@class MOLCertificate;
|
||||
|
||||
@@ -24,6 +25,8 @@
|
||||
///
|
||||
@interface SNTCachedDecision : NSObject
|
||||
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile;
|
||||
|
||||
@property santa_vnode_id_t vnodeId;
|
||||
@property SNTEventState decision;
|
||||
@property NSString *decisionExtra;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -15,4 +15,14 @@
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
|
||||
@implementation SNTCachedDecision
|
||||
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_vnodeId.fsid = (uint64_t)esFile->stat.st_dev;
|
||||
_vnodeId.fileid = esFile->stat.st_ino;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
36
Source/common/SNTCachedDecisionTest.mm
Normal file
36
Source/common/SNTCachedDecisionTest.mm
Normal file
@@ -0,0 +1,36 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
|
||||
@interface SNTCachedDecisionTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTCachedDecisionTest
|
||||
|
||||
- (void)testSNTCachedDecisionInit {
|
||||
// Ensure the vnodeId field is properly set from the es_file_t
|
||||
struct stat sb = MakeStat(1234, 5678);
|
||||
es_file_t file = MakeESFile("foo", sb);
|
||||
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] initWithEndpointSecurityFile:&file];
|
||||
|
||||
XCTAssertEqual(sb.st_ino, cd.vnodeId.fileid);
|
||||
XCTAssertEqual(sb.st_dev, cd.vnodeId.fsid);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -27,41 +27,22 @@
|
||||
#define unlikely(x) __builtin_expect(!!(x), 0)
|
||||
|
||||
typedef enum {
|
||||
ACTION_UNSET = 0,
|
||||
ACTION_UNSET,
|
||||
|
||||
// REQUESTS
|
||||
ACTION_REQUEST_SHUTDOWN = 10,
|
||||
ACTION_REQUEST_BINARY = 11,
|
||||
// If an operation is awaiting a cache decision from a similar operation
|
||||
// currently being processed, it will poll about every 5 ms for an answer.
|
||||
ACTION_REQUEST_BINARY,
|
||||
|
||||
// RESPONSES
|
||||
ACTION_RESPOND_ALLOW = 20,
|
||||
ACTION_RESPOND_DENY = 21,
|
||||
ACTION_RESPOND_TOOLONG = 22,
|
||||
ACTION_RESPOND_ACK = 23,
|
||||
ACTION_RESPOND_ALLOW_COMPILER = 24,
|
||||
// The following response is stored only in the kernel decision cache.
|
||||
// It is removed by SNTCompilerController
|
||||
ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE = 25,
|
||||
|
||||
// NOTIFY
|
||||
ACTION_NOTIFY_EXEC = 30,
|
||||
ACTION_NOTIFY_WRITE = 31,
|
||||
ACTION_NOTIFY_RENAME = 32,
|
||||
ACTION_NOTIFY_LINK = 33,
|
||||
ACTION_NOTIFY_EXCHANGE = 34,
|
||||
ACTION_NOTIFY_DELETE = 35,
|
||||
ACTION_NOTIFY_WHITELIST = 36,
|
||||
ACTION_NOTIFY_FORK = 37,
|
||||
ACTION_NOTIFY_EXIT = 38,
|
||||
|
||||
// ERROR
|
||||
ACTION_ERROR = 99,
|
||||
ACTION_RESPOND_ALLOW,
|
||||
ACTION_RESPOND_DENY,
|
||||
ACTION_RESPOND_ALLOW_COMPILER,
|
||||
} santa_action_t;
|
||||
|
||||
#define RESPONSE_VALID(x) \
|
||||
(x == ACTION_RESPOND_ALLOW || x == ACTION_RESPOND_DENY || \
|
||||
x == ACTION_RESPOND_ALLOW_COMPILER || \
|
||||
x == ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE)
|
||||
x == ACTION_RESPOND_ALLOW_COMPILER)
|
||||
|
||||
// Struct to manage vnode IDs
|
||||
typedef struct santa_vnode_id_t {
|
||||
@@ -75,28 +56,4 @@ typedef struct santa_vnode_id_t {
|
||||
#endif
|
||||
} santa_vnode_id_t;
|
||||
|
||||
typedef struct {
|
||||
santa_action_t action;
|
||||
santa_vnode_id_t vnode_id;
|
||||
uid_t uid;
|
||||
gid_t gid;
|
||||
pid_t pid;
|
||||
int pidversion;
|
||||
pid_t ppid;
|
||||
char path[MAXPATHLEN];
|
||||
char newpath[MAXPATHLEN];
|
||||
char ttypath[MAXPATHLEN];
|
||||
// For file events, this is the process name.
|
||||
// For exec requests, this is the parent process name.
|
||||
// While process names can technically be 4*MAXPATHLEN, that never
|
||||
// actually happens, so only take MAXPATHLEN and throw away any excess.
|
||||
char pname[MAXPATHLEN];
|
||||
|
||||
// This points to a copy of the original ES message.
|
||||
void *es_message;
|
||||
|
||||
// This points to an NSArray of the process arguments.
|
||||
void *args_array;
|
||||
} santa_message_t;
|
||||
|
||||
#endif // SANTA__COMMON__COMMON_H
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -57,6 +57,7 @@ typedef NS_ENUM(NSInteger, SNTEventState) {
|
||||
SNTEventStateBlockCertificate = 1 << 18,
|
||||
SNTEventStateBlockScope = 1 << 19,
|
||||
SNTEventStateBlockTeamID = 1 << 20,
|
||||
SNTEventStateBlockLongPath = 1 << 21,
|
||||
|
||||
// Bits 24-31 store allow decision types
|
||||
SNTEventStateAllowUnknown = 1 << 24,
|
||||
@@ -120,5 +121,4 @@ typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
|
||||
static const char *kSantaDPath =
|
||||
"/Applications/Santa.app/Contents/Library/SystemExtensions/"
|
||||
"com.google.santa.daemon.systemextension/Contents/MacOS/com.google.santa.daemon";
|
||||
static const char *kSantaCtlPath = "/Applications/Santa.app/Contents/MacOS/santactl";
|
||||
static const char *kSantaAppPath = "/Applications/Santa.app";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -65,7 +65,8 @@
|
||||
/// <key>rule_type</key>
|
||||
/// <string>BINARY</string> (one of BINARY, CERTIFICATE or TEAMID)
|
||||
/// <key>policy</key>
|
||||
/// <string>BLOCKLIST</string> (one of ALLOWLIST, ALLOWLIST_COMPILER, BLOCKLIST, SILENT_BLOCKLIST)
|
||||
/// <string>BLOCKLIST</string> (one of ALLOWLIST, ALLOWLIST_COMPILER, BLOCKLIST,
|
||||
/// SILENT_BLOCKLIST)
|
||||
/// </dict>
|
||||
/// </array>
|
||||
///
|
||||
@@ -244,15 +245,6 @@
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableMachineIDDecoration;
|
||||
|
||||
///
|
||||
/// Use an internal cache for decisions instead of relying on the caching
|
||||
/// mechanism built-in to the EndpointSecurity framework. This may increase
|
||||
/// performance, particularly when Santa is run alongside other system
|
||||
/// extensions.
|
||||
/// Has no effect if the system extension is not being used. Defaults to NO.
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableSysxCache;
|
||||
|
||||
#pragma mark - GUI Settings
|
||||
|
||||
///
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2014-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
/// Holds the last processed hash of the static rules list.
|
||||
@property(atomic) NSDictionary *cachedStaticRules;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SNTConfigurator
|
||||
@@ -94,8 +95,6 @@ static NSString *const kMailDirectoryEventMaxFlushTimeSec = @"MailDirectoryEvent
|
||||
|
||||
static NSString *const kEnableMachineIDDecoration = @"EnableMachineIDDecoration";
|
||||
|
||||
static NSString *const kEnableSysxCache = @"EnableSysxCache";
|
||||
|
||||
static NSString *const kEnableForkAndExitLogging = @"EnableForkAndExitLogging";
|
||||
static NSString *const kIgnoreOtherEndpointSecurityClients = @"IgnoreOtherEndpointSecurityClients";
|
||||
static NSString *const kEnableDebugLogging = @"EnableDebugLogging";
|
||||
@@ -206,7 +205,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kMailDirectorySizeThresholdMB : number,
|
||||
kMailDirectoryEventMaxFlushTimeSec : number,
|
||||
kEnableMachineIDDecoration : number,
|
||||
kEnableSysxCache : number,
|
||||
kEnableForkAndExitLogging : number,
|
||||
kIgnoreOtherEndpointSecurityClients : number,
|
||||
kEnableDebugLogging : number,
|
||||
@@ -425,10 +423,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self syncAndConfigStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableSysxCache {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableForkAndExitLogging {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -793,11 +787,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (BOOL)enableSysxCache {
|
||||
NSNumber *number = self.configState[kEnableSysxCache];
|
||||
return number ? [number boolValue] : YES;
|
||||
}
|
||||
|
||||
- (BOOL)enableCleanSyncEventUpload {
|
||||
NSNumber *number = self.configState[kSyncEnableCleanSyncEventUpload];
|
||||
return number ? [number boolValue] : NO;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,6 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class MOLCodesignChecker;
|
||||
@@ -32,6 +33,14 @@
|
||||
///
|
||||
- (instancetype)initWithPath:(NSString *)path error:(NSError **)error;
|
||||
|
||||
///
|
||||
/// Convenience initializer.
|
||||
///
|
||||
/// @param esFile Pointer to an es_file_t provided by the EndpointSecurity framework.
|
||||
/// Assumes that the path is a resolved path.
|
||||
///
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile error:(NSError **)error;
|
||||
|
||||
///
|
||||
/// Convenience initializer.
|
||||
///
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -25,6 +25,8 @@
|
||||
#include <sys/stat.h>
|
||||
#include <sys/xattr.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
|
||||
// Simple class to hold the data of a mach_header and the offset within the file
|
||||
// in which that header was found.
|
||||
@interface MachHeaderWithOffset : NSObject
|
||||
@@ -48,6 +50,7 @@
|
||||
@property NSFileHandle *fileHandle;
|
||||
@property NSUInteger fileSize;
|
||||
@property NSString *fileOwnerHomeDir;
|
||||
@property NSString *sha256Storage;
|
||||
|
||||
// Cached properties
|
||||
@property NSBundle *bundleRef;
|
||||
@@ -63,6 +66,26 @@
|
||||
extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
|
||||
- (instancetype)initWithResolvedPath:(NSString *)path error:(NSError **)error {
|
||||
struct stat fileStat;
|
||||
if (path.length) {
|
||||
lstat(path.UTF8String, &fileStat);
|
||||
}
|
||||
return [self initWithResolvedPath:path stat:&fileStat error:error];
|
||||
}
|
||||
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile error:(NSError **)error {
|
||||
return [self initWithResolvedPath:@(esFile->path.data) stat:&esFile->stat error:error];
|
||||
}
|
||||
|
||||
- (instancetype)initWithResolvedPath:(NSString *)path
|
||||
stat:(const struct stat *)fileStat
|
||||
error:(NSError **)error {
|
||||
if (!fileStat) {
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"NULL stat buffer unsupported");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_path = path;
|
||||
@@ -76,9 +99,7 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
return nil;
|
||||
}
|
||||
|
||||
struct stat fileStat;
|
||||
lstat(_path.UTF8String, &fileStat);
|
||||
if (!((S_IFMT & fileStat.st_mode) == S_IFREG)) {
|
||||
if (!((S_IFMT & fileStat->st_mode) == S_IFREG)) {
|
||||
if (error) {
|
||||
NSString *errStr = [NSString stringWithFormat:@"Non regular file: %s", strerror(errno)];
|
||||
*error = [NSError errorWithDomain:@"com.google.santa.fileinfo"
|
||||
@@ -88,12 +109,12 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
return nil;
|
||||
}
|
||||
|
||||
_fileSize = fileStat.st_size;
|
||||
_fileSize = fileStat->st_size;
|
||||
|
||||
if (_fileSize == 0) return nil;
|
||||
|
||||
if (fileStat.st_uid != 0) {
|
||||
struct passwd *pwd = getpwuid(fileStat.st_uid);
|
||||
if (fileStat->st_uid != 0) {
|
||||
struct passwd *pwd = getpwuid(fileStat->st_uid);
|
||||
if (pwd) {
|
||||
_fileOwnerHomeDir = @(pwd->pw_dir);
|
||||
}
|
||||
@@ -214,9 +235,13 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
}
|
||||
|
||||
- (NSString *)SHA256 {
|
||||
NSString *sha256;
|
||||
[self hashSHA1:NULL SHA256:&sha256];
|
||||
return sha256;
|
||||
// Memoize the value
|
||||
if (!self.sha256Storage) {
|
||||
NSString *sha256;
|
||||
[self hashSHA1:NULL SHA256:&sha256];
|
||||
self.sha256Storage = sha256;
|
||||
}
|
||||
return self.sha256Storage;
|
||||
}
|
||||
|
||||
#pragma mark File Type Info
|
||||
|
||||
34
Source/common/SNTKVOManager.h
Normal file
34
Source/common/SNTKVOManager.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/// 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.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// The callback type when KVO notifications are received for observed key paths.
|
||||
// The first parameter is the previous value, the second paramter is the new value.
|
||||
typedef void (^KVOCallback)(id oldValue, id newValue);
|
||||
|
||||
@interface SNTKVOManager : NSObject
|
||||
|
||||
// Add an observer for the selector on the given object. When a KVO notification
|
||||
// is received, the callback is called. If the notification contains objects that
|
||||
// are not of the expectedType, nil is passed as the argument to the callback.
|
||||
// The observer is removed when the returned instance is deallocated.
|
||||
- (instancetype)initWithObject:(id)object
|
||||
selector:(SEL)selector
|
||||
type:(Class)expectedType
|
||||
callback:(KVOCallback)callback;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
72
Source/common/SNTKVOManager.mm
Normal file
72
Source/common/SNTKVOManager.mm
Normal file
@@ -0,0 +1,72 @@
|
||||
/// 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.
|
||||
|
||||
#import "Source/common/SNTKVOManager.h"
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
|
||||
@interface SNTKVOManager ()
|
||||
@property KVOCallback callback;
|
||||
@property Class expectedType;
|
||||
@property NSString *keyPath;
|
||||
@property id object;
|
||||
@end
|
||||
|
||||
@implementation SNTKVOManager
|
||||
|
||||
- (instancetype)initWithObject:(id)object
|
||||
selector:(SEL)selector
|
||||
type:(Class)expectedType
|
||||
callback:(KVOCallback)callback {
|
||||
self = [super self];
|
||||
if (self) {
|
||||
NSString *selectorName = NSStringFromSelector(selector);
|
||||
if (![object respondsToSelector:selector]) {
|
||||
LOGE(@"Attempt to add observer for an unknown selector (%@) for object (%@)", selectorName,
|
||||
[object class]);
|
||||
return nil;
|
||||
}
|
||||
|
||||
_object = object;
|
||||
_keyPath = selectorName;
|
||||
_expectedType = expectedType;
|
||||
_callback = callback;
|
||||
|
||||
[object addObserver:self
|
||||
forKeyPath:selectorName
|
||||
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
|
||||
context:NULL];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self.object removeObserver:self forKeyPath:self.keyPath context:NULL];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||
ofObject:(id)object
|
||||
change:(NSDictionary<NSString *, id> *)change
|
||||
context:(void *)context {
|
||||
id oldValue = [change[NSKeyValueChangeOldKey] isKindOfClass:self.expectedType]
|
||||
? change[NSKeyValueChangeOldKey]
|
||||
: nil;
|
||||
id newValue = [change[NSKeyValueChangeNewKey] isKindOfClass:self.expectedType]
|
||||
? change[NSKeyValueChangeNewKey]
|
||||
: nil;
|
||||
|
||||
self.callback(oldValue, newValue);
|
||||
}
|
||||
|
||||
@end
|
||||
129
Source/common/SNTKVOManagerTest.mm
Normal file
129
Source/common/SNTKVOManagerTest.mm
Normal file
@@ -0,0 +1,129 @@
|
||||
/// 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.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTKVOManager.h"
|
||||
|
||||
@interface Foo : NSObject
|
||||
@property NSNumber *propNumber;
|
||||
@property NSArray *propArray;
|
||||
@property id propId;
|
||||
@end
|
||||
|
||||
@implementation Foo
|
||||
@end
|
||||
|
||||
@interface SNTKVOManagerTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTKVOManagerTest
|
||||
|
||||
- (void)testInvalidSelector {
|
||||
Foo *foo = [[Foo alloc] init];
|
||||
|
||||
SNTKVOManager *kvo = [[SNTKVOManager alloc] initWithObject:foo
|
||||
selector:NSSelectorFromString(@"doesNotExist")
|
||||
type:[NSNumber class]
|
||||
callback:^(id, id){
|
||||
}];
|
||||
|
||||
XCTAssertNil(kvo);
|
||||
}
|
||||
|
||||
- (void)testNormalOperation {
|
||||
Foo *foo = [[Foo alloc] init];
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
int origVal = 123;
|
||||
int update1 = 456;
|
||||
int update2 = 789;
|
||||
|
||||
foo.propNumber = @(origVal);
|
||||
|
||||
// Store the values from the callback to test against expected values
|
||||
__block int oldVal;
|
||||
__block int newVal;
|
||||
|
||||
SNTKVOManager *kvo =
|
||||
[[SNTKVOManager alloc] initWithObject:foo
|
||||
selector:@selector(propNumber)
|
||||
type:[NSNumber class]
|
||||
callback:^(NSNumber *oldValue, NSNumber *newValue) {
|
||||
oldVal = [oldValue intValue];
|
||||
newVal = [newValue intValue];
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
XCTAssertNotNil(kvo);
|
||||
|
||||
// Ensure an update to the observed property triggers the callback
|
||||
foo.propNumber = @(update1);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for first observable update");
|
||||
XCTAssertEqual(oldVal, origVal);
|
||||
XCTAssertEqual(newVal, update1);
|
||||
|
||||
// One more time why not
|
||||
foo.propNumber = @(update2);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for second observable update");
|
||||
XCTAssertEqual(oldVal, update1);
|
||||
XCTAssertEqual(newVal, update2);
|
||||
}
|
||||
|
||||
- (void)testUnexpectedTypes {
|
||||
Foo *foo = [[Foo alloc] init];
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
NSString *origVal = @"any_val";
|
||||
NSString *update = @"new_val";
|
||||
foo.propId = origVal;
|
||||
|
||||
__block id oldVal;
|
||||
__block id newVal;
|
||||
|
||||
SNTKVOManager *kvo = [[SNTKVOManager alloc] initWithObject:foo
|
||||
selector:@selector(propId)
|
||||
type:[NSString class]
|
||||
callback:^(id oldValue, id newValue) {
|
||||
oldVal = oldValue;
|
||||
newVal = newValue;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
XCTAssertNotNil(kvo);
|
||||
|
||||
// Update to an unexpected type (here, NSNumber instead of NSString)
|
||||
foo.propId = @(123);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for first observable update");
|
||||
XCTAssertEqualObjects(oldVal, origVal);
|
||||
XCTAssertNil(newVal);
|
||||
|
||||
// Update again with an expected type, ensure oldVal is now nil
|
||||
foo.propId = update;
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for first observable update");
|
||||
XCTAssertNil(oldVal);
|
||||
XCTAssertEqualObjects(newVal, update);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -51,18 +51,18 @@
|
||||
/// Designated initializer.
|
||||
///
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg
|
||||
timestamp:(NSUInteger)timestamp;
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg
|
||||
timestamp:(NSUInteger)timestamp;
|
||||
|
||||
///
|
||||
/// Initialize with a default timestamp: current time if rule state is transitive, 0 otherwise.
|
||||
///
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg;
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg;
|
||||
|
||||
///
|
||||
/// Initialize with a dictionary received from a sync server.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -95,7 +95,6 @@
|
||||
///
|
||||
@property NSArray *signingChain;
|
||||
|
||||
|
||||
///
|
||||
/// If the executed file was signed, this is the Team ID if present in the signature information.
|
||||
///
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
/// Copyright 2016-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,10 +12,14 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#define STRONGIFY(var) \
|
||||
_Pragma("clang diagnostic push") \
|
||||
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
|
||||
__strong __typeof(var) var = (Weak_##var); \
|
||||
// clang-format off
|
||||
|
||||
#define STRONGIFY(var) \
|
||||
_Pragma("clang diagnostic push") \
|
||||
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
|
||||
__strong __typeof(var) var = (Weak_##var); \
|
||||
_Pragma("clang diagnostic pop")
|
||||
|
||||
#define WEAKIFY(var) __weak __typeof(var) Weak_##var = (var);
|
||||
|
||||
// clang-format on
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -18,8 +18,11 @@
|
||||
@implementation SNTSystemInfo
|
||||
|
||||
+ (NSString *)serialNumber {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
io_service_t platformExpert =
|
||||
IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"));
|
||||
#pragma clang diagnostic pop
|
||||
if (!platformExpert) return nil;
|
||||
|
||||
NSString *serial = CFBridgingRelease(IORegistryEntryCreateCFProperty(
|
||||
@@ -31,8 +34,11 @@
|
||||
}
|
||||
|
||||
+ (NSString *)hardwareUUID {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
io_service_t platformExpert =
|
||||
IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"));
|
||||
#pragma clang diagnostic pop
|
||||
if (!platformExpert) return nil;
|
||||
|
||||
NSString *uuid = CFBridgingRelease(IORegistryEntryCreateCFProperty(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -15,8 +15,8 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
|
||||
@class SNTRule;
|
||||
@class SNTStoredEvent;
|
||||
@@ -31,7 +31,6 @@
|
||||
/// Cache Ops
|
||||
///
|
||||
- (void)cacheCounts:(void (^)(uint64_t rootCache, uint64_t nonRootCache))reply;
|
||||
- (void)cacheBucketCount:(void (^)(NSArray *))reply;
|
||||
- (void)checkCacheForVnodeID:(santa_vnode_id_t)vnodeID withReply:(void (^)(santa_action_t))reply;
|
||||
|
||||
///
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
/// Copyright 2016-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <libkern/OSAtomic.h>
|
||||
#include <libkern/OSTypes.h>
|
||||
#include <os/log.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/cdefs.h>
|
||||
|
||||
@@ -26,11 +27,6 @@
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
|
||||
#define panic(args...) \
|
||||
printf(args); \
|
||||
printf("\n"); \
|
||||
abort()
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
@@ -334,7 +330,9 @@ class SantaCache {
|
||||
inline void unlock(struct bucket *bucket) const {
|
||||
if (unlikely(OSAtomicTestAndClear(7, (volatile uint8_t *)&bucket->head) ==
|
||||
0)) {
|
||||
panic("SantaCache::unlock(): Tried to unlock an unlocked lock");
|
||||
os_log_error(OS_LOG_DEFAULT,
|
||||
"SantaCache::unlock(): Tried to unlock an unlocked lock");
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
60
Source/common/TestUtils.h
Normal file
60
Source/common/TestUtils.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__COMMON__TESTUTILS_H
|
||||
#define SANTA__COMMON__TESTUTILS_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define NOBODY_UID ((unsigned int)-2)
|
||||
#define NOBODY_GID ((unsigned int)-2)
|
||||
|
||||
// Bubble up googletest expectation failures to XCTest failures
|
||||
#define XCTBubbleMockVerifyAndClearExpectations(mock) \
|
||||
XCTAssertTrue(::testing::Mock::VerifyAndClearExpectations(mock), \
|
||||
"Expected calls were not properly mocked")
|
||||
|
||||
// Pretty print C string match errors
|
||||
#define XCTAssertCStringEqual(got, want) \
|
||||
XCTAssertTrue(strcmp((got), (want)) == 0, @"\nMismatched strings.\n\t got: %s\n\twant: %s", \
|
||||
(got), (want))
|
||||
|
||||
// Pretty print C++ string match errors
|
||||
#define XCTAssertCppStringEqual(got, want) XCTAssertCStringEqual((got).c_str(), (want).c_str())
|
||||
|
||||
// Helper to ensure at least `ms` milliseconds are slept, even if the sleep
|
||||
// function returns early due to interrupts.
|
||||
void SleepMS(long ms);
|
||||
|
||||
enum class ActionType {
|
||||
Auth,
|
||||
Notify,
|
||||
};
|
||||
|
||||
// Helpers to construct various ES structs
|
||||
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver);
|
||||
struct stat MakeStat(ino_t ino, dev_t devno = 0);
|
||||
es_string_token_t MakeESStringToken(const char *s);
|
||||
es_file_t MakeESFile(const char *path, struct stat sb = {});
|
||||
es_process_t MakeESProcess(es_file_t *file, audit_token_t tok = {}, audit_token_t parent_tok = {});
|
||||
es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc,
|
||||
ActionType action_type = ActionType::Notify,
|
||||
uint64_t future_deadline_ms = 100000);
|
||||
|
||||
#endif
|
||||
107
Source/common/TestUtils.mm
Normal file
107
Source/common/TestUtils.mm
Normal file
@@ -0,0 +1,107 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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/common/TestUtils.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <mach/mach_time.h>
|
||||
#include <time.h>
|
||||
|
||||
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver) {
|
||||
return audit_token_t{
|
||||
.val =
|
||||
{
|
||||
0,
|
||||
NOBODY_UID,
|
||||
NOBODY_GID,
|
||||
NOBODY_UID,
|
||||
NOBODY_GID,
|
||||
(unsigned int)pid,
|
||||
0,
|
||||
(unsigned int)pidver,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
struct stat MakeStat(ino_t ino, dev_t devno) {
|
||||
return (struct stat){
|
||||
.st_dev = devno,
|
||||
.st_ino = ino,
|
||||
};
|
||||
}
|
||||
|
||||
es_string_token_t MakeESStringToken(const char *s) {
|
||||
return es_string_token_t{
|
||||
.length = strlen(s),
|
||||
.data = s,
|
||||
};
|
||||
}
|
||||
|
||||
es_file_t MakeESFile(const char *path, struct stat sb) {
|
||||
return es_file_t{
|
||||
.path = MakeESStringToken(path),
|
||||
.path_truncated = false,
|
||||
.stat = sb,
|
||||
};
|
||||
}
|
||||
|
||||
es_process_t MakeESProcess(es_file_t *file, audit_token_t tok, audit_token_t parent_tok) {
|
||||
return es_process_t{
|
||||
.audit_token = tok,
|
||||
.ppid = audit_token_to_pid(parent_tok),
|
||||
.original_ppid = audit_token_to_pid(parent_tok),
|
||||
.executable = file,
|
||||
.parent_audit_token = parent_tok,
|
||||
};
|
||||
}
|
||||
|
||||
static uint64_t AddMillisToMachTime(uint64_t ms, uint64_t machTime) {
|
||||
static dispatch_once_t onceToken;
|
||||
static mach_timebase_info_data_t timebase;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
mach_timebase_info(&timebase);
|
||||
});
|
||||
|
||||
// Convert given machTime to nanoseconds
|
||||
uint64_t nanoTime = machTime * timebase.numer / timebase.denom;
|
||||
|
||||
// Add the ms offset
|
||||
nanoTime += (ms * NSEC_PER_MSEC);
|
||||
|
||||
// Convert back to machTime
|
||||
return nanoTime * timebase.denom / timebase.numer;
|
||||
}
|
||||
|
||||
es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc, ActionType action_type,
|
||||
uint64_t future_deadline_ms) {
|
||||
return es_message_t{
|
||||
.deadline = AddMillisToMachTime(future_deadline_ms, mach_absolute_time()),
|
||||
.process = proc,
|
||||
.action_type =
|
||||
(action_type == ActionType::Notify) ? ES_ACTION_TYPE_NOTIFY : ES_ACTION_TYPE_AUTH,
|
||||
.event_type = et,
|
||||
};
|
||||
}
|
||||
|
||||
void SleepMS(long ms) {
|
||||
struct timespec ts {
|
||||
.tv_sec = ms / 1000, .tv_nsec = (long)((ms % 1000) * NSEC_PER_MSEC),
|
||||
};
|
||||
|
||||
while (nanosleep(&ts, &ts) != 0) {
|
||||
XCTAssertEqual(errno, EINTR);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//
|
||||
// !!! WARNING !!!
|
||||
// This proto is in beta format and subject to change.
|
||||
// This proto is for demonstration purposes only and will be changing.
|
||||
// Do not rely on this format.
|
||||
//
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
@@ -26,7 +26,6 @@ objc_library(
|
||||
"//:opt_build": [],
|
||||
"//conditions:default": [
|
||||
"Commands/SNTCommandBundleInfo.m",
|
||||
"Commands/SNTCommandCacheHistogram.m",
|
||||
"Commands/SNTCommandCheckCache.m",
|
||||
"Commands/SNTCommandFlushCache.m",
|
||||
],
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/// Copyright 2018 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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.
|
||||
|
||||
#ifdef DEBUG
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
|
||||
@interface SNTCommandCacheHistogram : SNTCommand <SNTCommandProtocol>
|
||||
@end
|
||||
|
||||
@implementation SNTCommandCacheHistogram
|
||||
|
||||
REGISTER_COMMAND_NAME(@"cachehistogram")
|
||||
|
||||
+ (BOOL)requiresRoot {
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)requiresDaemonConn {
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (NSString *)shortHelpText {
|
||||
return @"Print a cache distribution histogram.";
|
||||
}
|
||||
|
||||
+ (NSString *)longHelpText {
|
||||
return (@"Prints a histogram of each bucket of the in-kernel cache\n"
|
||||
@" Use -g to get 'graphical' output\n"
|
||||
@"Only available in DEBUG builds.");
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
[[self.daemonConn remoteObjectProxy] cacheBucketCount:^(NSArray *counts) {
|
||||
NSMutableDictionary<NSNumber *, NSNumber *> *d = [NSMutableDictionary dictionary];
|
||||
[counts enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
|
||||
d[obj] = @([d[obj] intValue] + 1);
|
||||
}];
|
||||
printf("There are %llu empty buckets\n", [d[@0] unsignedLongLongValue]);
|
||||
|
||||
for (NSNumber *key in [d.allKeys sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
if ([key isEqual:@0]) continue;
|
||||
uint64_t k = [key unsignedLongLongValue];
|
||||
uint64_t v = [d[key] unsignedLongLongValue];
|
||||
|
||||
if ([[[NSProcessInfo processInfo] arguments] containsObject:@"-g"]) {
|
||||
printf("%4llu: ", k);
|
||||
for (uint64_t y = 0; y < v; ++y) {
|
||||
printf("#");
|
||||
}
|
||||
printf("\n");
|
||||
} else {
|
||||
printf("%4llu bucket[s] have %llu %s\n", v, k, k > 1 ? "entries" : "entry");
|
||||
}
|
||||
}
|
||||
exit(0);
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
/// Copyright 2016-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -62,9 +62,6 @@ REGISTER_COMMAND_NAME(@"checkcache")
|
||||
} else if (action == ACTION_RESPOND_ALLOW_COMPILER) {
|
||||
LOGI(@"File exists in [allowlist compiler] kernel cache");
|
||||
exit(0);
|
||||
} else if (action == ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE) {
|
||||
LOGI(@"File exists in [allowlist pending_transitive] kernel cache");
|
||||
exit(0);
|
||||
} else if (action == ACTION_UNSET) {
|
||||
LOGE(@"File does not exist in cache");
|
||||
exit(1);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -385,6 +385,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
|
||||
case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break;
|
||||
case SNTEventStateAllowTransitive: [output appendString:@" (Transitive)"]; break;
|
||||
case SNTEventStateBlockLongPath: [output appendString:@" (Long Path)"]; break;
|
||||
|
||||
default: output = @"None".mutableCopy; break;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -74,18 +74,14 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
|
||||
SNTConfigurator *configurator = [SNTConfigurator configurator];
|
||||
|
||||
BOOL cachingEnabled = [configurator enableSysxCache];
|
||||
|
||||
// Cache status
|
||||
__block uint64_t rootCacheCount = -1, nonRootCacheCount = -1;
|
||||
if (cachingEnabled) {
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] cacheCounts:^(uint64_t rootCache, uint64_t nonRootCache) {
|
||||
rootCacheCount = rootCache;
|
||||
nonRootCacheCount = nonRootCache;
|
||||
dispatch_group_leave(group);
|
||||
}];
|
||||
}
|
||||
dispatch_group_enter(group);
|
||||
[[self.daemonConn remoteObjectProxy] 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;
|
||||
@@ -215,12 +211,12 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
@"transitive_rules" : @(enableTransitiveRules),
|
||||
},
|
||||
} mutableCopy];
|
||||
if (cachingEnabled) {
|
||||
stats[@"cache"] = @{
|
||||
@"root_cache_count" : @(rootCacheCount),
|
||||
@"non_root_cache_count" : @(nonRootCacheCount),
|
||||
};
|
||||
}
|
||||
|
||||
stats[@"cache"] = @{
|
||||
@"root_cache_count" : @(rootCacheCount),
|
||||
@"non_root_cache_count" : @(nonRootCacheCount),
|
||||
};
|
||||
|
||||
NSData *statsData = [NSJSONSerialization dataWithJSONObject:stats
|
||||
options:NSJSONWritingPrettyPrinted
|
||||
error:nil];
|
||||
@@ -238,11 +234,9 @@ REGISTER_COMMAND_NAME(@"status")
|
||||
printf(" %-25s | %lld (Peak: %.2f%%)\n", "Watchdog CPU Events", cpuEvents, cpuPeak);
|
||||
printf(" %-25s | %lld (Peak: %.2fMB)\n", "Watchdog RAM Events", ramEvents, ramPeak);
|
||||
|
||||
if (cachingEnabled) {
|
||||
printf(">>> Cache Info\n");
|
||||
printf(" %-25s | %lld\n", "Root cache count", rootCacheCount);
|
||||
printf(" %-25s | %lld\n", "Non-root cache count", nonRootCacheCount);
|
||||
}
|
||||
printf(">>> Cache Info\n");
|
||||
printf(" %-25s | %lld\n", "Root cache count", rootCacheCount);
|
||||
printf(" %-25s | %lld\n", "Non-root cache count", nonRootCacheCount);
|
||||
|
||||
printf(">>> Database Info\n");
|
||||
printf(" %-25s | %lld\n", "Binary Rules", binaryRuleCount);
|
||||
|
||||
1214
Source/santad/BUILD
1214
Source/santad/BUILD
File diff suppressed because it is too large
Load Diff
@@ -105,6 +105,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
|
||||
// This is a Santa-curated list of paths to check on startup. This list will be merged
|
||||
// with the set of default muted paths from ES.
|
||||
|
||||
NSSet *santaDefinedCriticalPaths = [NSSet setWithArray:@[
|
||||
@"/usr/libexec/trustd",
|
||||
@"/usr/lib/dyld",
|
||||
@@ -136,6 +137,12 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
NSMutableDictionary *bins = [NSMutableDictionary dictionary];
|
||||
for (NSString *path in [SNTRuleTable criticalSystemBinaryPaths]) {
|
||||
SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithPath:path];
|
||||
if (!binInfo.SHA256) {
|
||||
// If there isn't a hash, no need to compute the other info here.
|
||||
// Just continue on to the next binary.
|
||||
LOGW(@"Unable to compute hash for critical system binary %@.", path);
|
||||
continue;
|
||||
}
|
||||
MOLCodesignChecker *csInfo = [binInfo codesignCheckerWithError:NULL];
|
||||
|
||||
// Make sure the critical system binary is signed by the same chain as launchd/self
|
||||
@@ -143,9 +150,9 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
if ([csInfo signingInformationMatches:self.launchdCSInfo]) {
|
||||
systemBin = YES;
|
||||
} else if (![csInfo signingInformationMatches:self.santadCSInfo]) {
|
||||
LOGE(@"Unable to validate critical system binary. "
|
||||
LOGW(@"Unable to validate critical system binary %@. "
|
||||
@"pid 1: %@, santad: %@ and %@: %@ do not match.",
|
||||
self.launchdCSInfo.leafCertificate, self.santadCSInfo.leafCertificate, path,
|
||||
path, self.launchdCSInfo.leafCertificate, self.santadCSInfo.leafCertificate, path,
|
||||
csInfo.leafCertificate);
|
||||
continue;
|
||||
}
|
||||
|
||||
75
Source/santad/EventProviders/AuthResultCache.h
Normal file
75
Source/santad/EventProviders/AuthResultCache.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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_AUTHRESULTCACHE_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_AUTHRESULTCACHE_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <sys/stat.h>
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
enum class FlushCacheMode {
|
||||
kNonRootOnly,
|
||||
kAllCaches,
|
||||
};
|
||||
|
||||
class AuthResultCache {
|
||||
public:
|
||||
// Santa currently only flushes caches when new DENY rules are added, not
|
||||
// ALLOW rules. This means this value should be low enough so that if a
|
||||
// previously denied binary is allowed, it can be re-executed by the user in a
|
||||
// timely manner. But the value should be high enough to allow the cache to be
|
||||
// effective in the event the binary is executed in rapid succession.
|
||||
AuthResultCache(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms = 1500);
|
||||
virtual ~AuthResultCache();
|
||||
|
||||
AuthResultCache(AuthResultCache &&other) = delete;
|
||||
AuthResultCache &operator=(AuthResultCache &&rhs) = delete;
|
||||
AuthResultCache(const AuthResultCache &other) = delete;
|
||||
AuthResultCache &operator=(const AuthResultCache &other) = delete;
|
||||
|
||||
virtual bool AddToCache(const es_file_t *es_file, santa_action_t decision);
|
||||
virtual void RemoveFromCache(const es_file_t *es_file);
|
||||
virtual santa_action_t CheckCache(const es_file_t *es_file);
|
||||
virtual santa_action_t CheckCache(santa_vnode_id_t vnode_id);
|
||||
|
||||
virtual void FlushCache(FlushCacheMode mode);
|
||||
|
||||
virtual NSArray<NSNumber *> *CacheCounts();
|
||||
|
||||
private:
|
||||
virtual SantaCache<santa_vnode_id_t, uint64_t> *CacheForVnodeID(santa_vnode_id_t vnode_id);
|
||||
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *root_cache_;
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *nonroot_cache_;
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
uint64_t root_devno_;
|
||||
uint64_t cache_deny_time_ns_;
|
||||
dispatch_queue_t q_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
|
||||
#endif
|
||||
156
Source/santad/EventProviders/AuthResultCache.mm
Normal file
156
Source/santad/EventProviders/AuthResultCache.mm
Normal file
@@ -0,0 +1,156 @@
|
||||
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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/AuthResultCache.h"
|
||||
|
||||
#include <mach/clock_types.h>
|
||||
#include <time.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
|
||||
template <>
|
||||
uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const &t) {
|
||||
return (SantaCacheHasher<uint64_t>(t.fsid) << 1) ^ SantaCacheHasher<uint64_t>(t.fileid);
|
||||
}
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
|
||||
return santa_vnode_id_t{
|
||||
.fsid = (uint64_t)es_file->stat.st_dev,
|
||||
.fileid = es_file->stat.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
static inline uint64_t GetCurrentUptime() {
|
||||
return clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
}
|
||||
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
static inline uint64_t CacheableAction(santa_action_t action,
|
||||
uint64_t timestamp = GetCurrentUptime()) {
|
||||
return ((uint64_t)action << 56) | (timestamp & 0xFFFFFFFFFFFFFF);
|
||||
}
|
||||
|
||||
static inline santa_action_t ActionFromCachedValue(uint64_t cachedValue) {
|
||||
return (santa_action_t)(cachedValue >> 56);
|
||||
}
|
||||
|
||||
static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
|
||||
return (cachedValue & ~(0xFF00000000000000));
|
||||
}
|
||||
|
||||
AuthResultCache::AuthResultCache(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms)
|
||||
: esapi_(esapi), cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
|
||||
root_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
nonroot_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
|
||||
struct stat sb;
|
||||
if (stat("/", &sb) == 0) {
|
||||
root_devno_ = sb.st_dev;
|
||||
}
|
||||
|
||||
q_ = dispatch_queue_create(
|
||||
"com.google.santa.daemon.auth_result_cache.q",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
}
|
||||
|
||||
AuthResultCache::~AuthResultCache() {
|
||||
delete root_cache_;
|
||||
delete nonroot_cache_;
|
||||
}
|
||||
|
||||
bool AuthResultCache::AddToCache(const es_file_t *es_file, santa_action_t decision) {
|
||||
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
|
||||
switch (decision) {
|
||||
case ACTION_REQUEST_BINARY:
|
||||
return cache->set(vnode_id, CacheableAction(ACTION_REQUEST_BINARY, 0), 0);
|
||||
case ACTION_RESPOND_ALLOW: OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_ALLOW_COMPILER: OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_DENY:
|
||||
return cache->set(vnode_id, CacheableAction(decision),
|
||||
CacheableAction(ACTION_REQUEST_BINARY, 0));
|
||||
default:
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"Invalid cache value, exiting.");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
void AuthResultCache::RemoveFromCache(const es_file_t *es_file) {
|
||||
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
|
||||
CacheForVnodeID(vnode_id)->remove(vnode_id);
|
||||
}
|
||||
|
||||
santa_action_t AuthResultCache::CheckCache(const es_file_t *es_file) {
|
||||
return CheckCache(VnodeForFile(es_file));
|
||||
}
|
||||
|
||||
santa_action_t AuthResultCache::CheckCache(santa_vnode_id_t vnode_id) {
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
|
||||
|
||||
uint64_t cached_val = cache->get(vnode_id);
|
||||
if (cached_val == 0) {
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
|
||||
santa_action_t result = ActionFromCachedValue(cached_val);
|
||||
|
||||
if (result == ACTION_RESPOND_DENY) {
|
||||
uint64_t expiry_time = TimestampFromCachedValue(cached_val) + cache_deny_time_ns_;
|
||||
if (expiry_time < GetCurrentUptime()) {
|
||||
cache->remove(vnode_id);
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *AuthResultCache::CacheForVnodeID(
|
||||
santa_vnode_id_t vnode_id) {
|
||||
return (vnode_id.fsid == root_devno_ || root_devno_ == 0) ? root_cache_ : nonroot_cache_;
|
||||
}
|
||||
|
||||
void AuthResultCache::FlushCache(FlushCacheMode mode) {
|
||||
nonroot_cache_->clear();
|
||||
if (mode == FlushCacheMode::kAllCaches) {
|
||||
root_cache_->clear();
|
||||
|
||||
// Clear the ES cache when all local caches are flushed. Assume the ES cache
|
||||
// doesn't need to be cleared when only flushing the non-root cache.
|
||||
//
|
||||
// Calling into ES should be done asynchronously since it could otherwise
|
||||
// potentially deadlock.
|
||||
auto shared_esapi = esapi_->shared_from_this();
|
||||
dispatch_async(q_, ^{
|
||||
// ES does not need a connected client to clear cache
|
||||
shared_esapi->ClearCache(Client());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
NSArray<NSNumber *> *AuthResultCache::CacheCounts() {
|
||||
return @[ @(root_cache_->count()), @(nonroot_cache_->count()) ];
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
225
Source/santad/EventProviders/AuthResultCacheTest.mm
Normal file
225
Source/santad/EventProviders/AuthResultCacheTest.mm
Normal file
@@ -0,0 +1,225 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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 <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <Foundation/Foundation.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
|
||||
// Grab the st_dev number of the root volume to match the root cache
|
||||
static uint64_t RootDevno() {
|
||||
static dispatch_once_t once_token;
|
||||
static uint64_t devno;
|
||||
dispatch_once(&once_token, ^{
|
||||
struct stat sb;
|
||||
stat("/", &sb);
|
||||
devno = sb.st_dev;
|
||||
});
|
||||
return devno;
|
||||
}
|
||||
|
||||
static inline es_file_t MakeCacheableFile(uint64_t devno, uint64_t ino) {
|
||||
return es_file_t{
|
||||
.path = {}, .path_truncated = false, .stat = {.st_dev = (dev_t)devno, .st_ino = ino}};
|
||||
}
|
||||
|
||||
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
|
||||
return santa_vnode_id_t{
|
||||
.fsid = (uint64_t)es_file->stat.st_dev,
|
||||
.fileid = es_file->stat.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uint64_t root_count,
|
||||
uint64_t nonroot_count) {
|
||||
NSArray<NSNumber *> *counts = cache->CacheCounts();
|
||||
|
||||
XCTAssertNotNil(counts);
|
||||
XCTAssertEqual([counts count], 2);
|
||||
XCTAssertNotNil(counts[0]);
|
||||
XCTAssertNotNil(counts[1]);
|
||||
XCTAssertEqual([counts[0] unsignedLongLongValue], root_count);
|
||||
XCTAssertEqual([counts[1] unsignedLongLongValue], nonroot_count);
|
||||
}
|
||||
|
||||
@interface AuthResultCacheTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation AuthResultCacheTest
|
||||
|
||||
- (void)testEmptyCacheExpectedNumberOfCacheCounts {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testBasicOperation {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 222);
|
||||
|
||||
// Add the root file to the cache
|
||||
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_UNSET);
|
||||
|
||||
// Now add the non-root file
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
// Update the cached values
|
||||
cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW);
|
||||
cache->AddToCache(&nonrootFile, ACTION_RESPOND_DENY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
XCTAssertEqual(cache->CheckCache(VnodeForFile(&rootFile)), ACTION_RESPOND_ALLOW);
|
||||
XCTAssertEqual(cache->CheckCache(VnodeForFile(&nonrootFile)), ACTION_RESPOND_DENY);
|
||||
|
||||
// Remove the root file
|
||||
cache->RemoveFromCache(&rootFile);
|
||||
|
||||
AssertCacheCounts(cache, 0, 1);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_RESPOND_DENY);
|
||||
}
|
||||
|
||||
- (void)testFlushCache {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 111);
|
||||
|
||||
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
// Flush non-root only
|
||||
cache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
// Add back the non-root file
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
// Flush all caches
|
||||
// The call to ClearCache is asynchronous. Use a semaphore to
|
||||
// be notified when the mock is called.
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
EXPECT_CALL(*mockESApi, ClearCache).WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
dispatch_semaphore_signal(sema);
|
||||
return true;
|
||||
}));
|
||||
cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
"ClearCache wasn't called within expected time window");
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testCacheStateMachine {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
// Cached items must first be in the ACTION_REQUEST_BINARY state
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW));
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW_COMPILER));
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
// Items in the `ACTION_REQUEST_BINARY` state cannot reenter the same state
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
santa_action_t allowed_transitions[] = {
|
||||
ACTION_RESPOND_ALLOW,
|
||||
ACTION_RESPOND_ALLOW_COMPILER,
|
||||
ACTION_RESPOND_DENY,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(allowed_transitions) / sizeof(allowed_transitions[0]); i++) {
|
||||
// First make sure the item doesn't exist
|
||||
cache->RemoveFromCache(&rootFile);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
|
||||
// Now add the item to be in the first allowed state
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
// Now assert the allowed transition
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, allowed_transitions[i]));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), allowed_transitions[i]);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testCacheExpiry {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
// Create a cache with a lowered cache expiry value
|
||||
uint64_t expiryMS = 250;
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi, expiryMS);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
// Add a file to the cache and put into the ACTION_RESPOND_DENY state
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
|
||||
|
||||
// Ensure the file exists
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_RESPOND_DENY);
|
||||
|
||||
// Wait for the item to expire
|
||||
SleepMS(expiryMS);
|
||||
|
||||
// Check cache counts to make sure the item still exists
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
// Now check the cache, which will remove the item
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2021-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -50,7 +50,8 @@ typedef void (^MockDADiskAppearedCallback)(DADiskRef ref);
|
||||
@end
|
||||
|
||||
//
|
||||
// All DiskArbitration functions used in SNTDeviceManager and shimmed out accordingly.
|
||||
// All DiskArbitration functions used in SNTEndpointSecurityDeviceManager
|
||||
// and shimmed out accordingly.
|
||||
//
|
||||
CF_EXTERN_C_BEGIN
|
||||
|
||||
|
||||
69
Source/santad/EventProviders/EndpointSecurity/Client.h
Normal file
69
Source/santad/EventProviders/EndpointSecurity/Client.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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_ENDPOINTSECURITY_CLIENT_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_CLIENT_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class Client {
|
||||
public:
|
||||
explicit Client(es_client_t* client, es_new_client_result_t result)
|
||||
: client_(client), result_(result) {}
|
||||
|
||||
Client() : client_(nullptr), result_(ES_NEW_CLIENT_RESULT_ERR_INTERNAL) {}
|
||||
|
||||
virtual ~Client() {
|
||||
if (client_) {
|
||||
// Special case: Not using EndpointSecurityAPI here due to circular refs.
|
||||
es_delete_client(client_);
|
||||
}
|
||||
}
|
||||
|
||||
Client(Client&& other) {
|
||||
client_ = other.client_;
|
||||
result_ = other.result_;
|
||||
other.client_ = nullptr;
|
||||
other.result_ = ES_NEW_CLIENT_RESULT_ERR_INTERNAL;
|
||||
}
|
||||
|
||||
Client& operator=(Client&& rhs) {
|
||||
client_ = rhs.client_;
|
||||
result_ = rhs.result_;
|
||||
rhs.client_ = nullptr;
|
||||
rhs.result_ = ES_NEW_CLIENT_RESULT_ERR_INTERNAL;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Client(const Client& other) = delete;
|
||||
void operator=(const Client& rhs) = delete;
|
||||
|
||||
inline bool IsConnected() { return result_ == ES_NEW_CLIENT_RESULT_SUCCESS; }
|
||||
|
||||
inline es_new_client_result_t NewClientResult() { return result_; }
|
||||
|
||||
inline es_client_t* Get() const { return client_; }
|
||||
|
||||
private:
|
||||
es_client_t* client_;
|
||||
es_new_client_result_t result_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
118
Source/santad/EventProviders/EndpointSecurity/ClientTest.mm
Normal file
118
Source/santad/EventProviders/EndpointSecurity/ClientTest.mm
Normal file
@@ -0,0 +1,118 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
|
||||
// Global semaphore used for custom `es_delete_client` function
|
||||
dispatch_semaphore_t gSema;
|
||||
|
||||
// Note: The Client class does not use the `EndpointSecurityAPI` wrappers due
|
||||
// to circular dependency issues. It is a special case that uses the underlying
|
||||
// ES API `es_delete_client` directly. This test override will signal the
|
||||
// `gSema` semaphore to indicate it has been called.
|
||||
es_return_t es_delete_client(es_client_t *_Nullable client) {
|
||||
dispatch_semaphore_signal(gSema);
|
||||
return ES_RETURN_SUCCESS;
|
||||
};
|
||||
|
||||
@interface ClientTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation ClientTest
|
||||
|
||||
- (void)setUp {
|
||||
gSema = dispatch_semaphore_create(0);
|
||||
}
|
||||
|
||||
- (void)testConstructorsAndDestructors {
|
||||
// Ensure constructors set internal state properly
|
||||
// Anonymous scopes used to ensure destructors called as expected
|
||||
|
||||
// Null `es_client_t*` *shouldn't* trigger `es_delete_client`
|
||||
{
|
||||
Client c;
|
||||
XCTAssertEqual(c.Get(), nullptr);
|
||||
XCTAssertEqual(c.NewClientResult(), ES_NEW_CLIENT_RESULT_ERR_INTERNAL);
|
||||
}
|
||||
|
||||
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client called unexpectedly");
|
||||
|
||||
// Nonnull `es_client_t*` *should* trigger `es_delete_client`
|
||||
{
|
||||
int fake;
|
||||
es_client_t *fakeClient = (es_client_t *)&fake;
|
||||
Client c(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
XCTAssertEqual(c.Get(), fakeClient);
|
||||
XCTAssertEqual(c.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client not called within expected time window");
|
||||
|
||||
// Test move constructor
|
||||
{
|
||||
int fake;
|
||||
es_client_t *fakeClient = (es_client_t *)&fake;
|
||||
Client c1(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
|
||||
Client c2(std::move(c1));
|
||||
|
||||
XCTAssertEqual(c1.Get(), nullptr);
|
||||
XCTAssertEqual(c2.Get(), fakeClient);
|
||||
XCTAssertEqual(c2.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
// Ensure `es_delete_client` was only called once when both `c1` and `c2`
|
||||
// are destructed.
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client not called within expected time window");
|
||||
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client called unexpectedly");
|
||||
|
||||
// Test move assignment
|
||||
{
|
||||
int fake;
|
||||
es_client_t *fakeClient = (es_client_t *)&fake;
|
||||
Client c1(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
Client c2;
|
||||
|
||||
c2 = std::move(c1);
|
||||
|
||||
XCTAssertEqual(c1.Get(), nullptr);
|
||||
XCTAssertEqual(c2.Get(), fakeClient);
|
||||
XCTAssertEqual(c2.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
// Ensure `es_delete_client` was only called once when both `c1` and `c2`
|
||||
// are destructed.
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client not called within expected time window");
|
||||
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client called unexpectedly");
|
||||
}
|
||||
|
||||
- (void)testIsConnected {
|
||||
XCTAssertFalse(Client().IsConnected());
|
||||
XCTAssertFalse(Client(nullptr, ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED).IsConnected());
|
||||
XCTAssertTrue(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS).IsConnected());
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,52 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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_ENDPOINTSECURITY_ENDPOINTSECURITYAPI_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENDPOINTSECURITYAPI_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class EndpointSecurityAPI : public std::enable_shared_from_this<EndpointSecurityAPI> {
|
||||
public:
|
||||
virtual ~EndpointSecurityAPI() = default;
|
||||
|
||||
virtual Client NewClient(void (^message_handler)(es_client_t *, Message));
|
||||
|
||||
virtual bool Subscribe(const Client &client, const std::set<es_event_type_t> &);
|
||||
|
||||
virtual es_message_t *RetainMessage(const es_message_t *msg);
|
||||
virtual void ReleaseMessage(es_message_t *msg);
|
||||
|
||||
virtual bool RespondAuthResult(const Client &client, const Message &msg, es_auth_result_t result,
|
||||
bool cache);
|
||||
|
||||
virtual bool MuteProcess(const Client &client, const audit_token_t *tok);
|
||||
|
||||
virtual bool ClearCache(const Client &client);
|
||||
|
||||
virtual uint32_t ExecArgCount(const es_event_exec_t *event);
|
||||
virtual es_string_token_t ExecArg(const es_event_exec_t *event, uint32_t index);
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,87 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Client EndpointSecurityAPI::NewClient(void (^message_handler)(es_client_t *, Message)) {
|
||||
es_client_t *client = NULL;
|
||||
|
||||
auto shared_esapi = shared_from_this();
|
||||
es_new_client_result_t res = es_new_client(&client, ^(es_client_t *c, const es_message_t *msg) {
|
||||
@autoreleasepool {
|
||||
message_handler(c, Message(shared_esapi, msg));
|
||||
}
|
||||
});
|
||||
|
||||
return Client(client, res);
|
||||
}
|
||||
|
||||
es_message_t *EndpointSecurityAPI::RetainMessage(const es_message_t *msg) {
|
||||
if (@available(macOS 11.0, *)) {
|
||||
es_retain_message(msg);
|
||||
es_message_t *nonconst = const_cast<es_message_t *>(msg);
|
||||
return nonconst;
|
||||
} else {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return es_copy_message(msg);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
void EndpointSecurityAPI::ReleaseMessage(es_message_t *msg) {
|
||||
if (@available(macOS 11.0, *)) {
|
||||
es_release_message(msg);
|
||||
} else {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return es_free_message(msg);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::Subscribe(const Client &client,
|
||||
const std::set<es_event_type_t> &event_types) {
|
||||
std::vector<es_event_type_t> subs(event_types.begin(), event_types.end());
|
||||
return es_subscribe(client.Get(), subs.data(), (uint32_t)subs.size()) == ES_RETURN_SUCCESS;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::RespondAuthResult(const Client &client, const Message &msg,
|
||||
es_auth_result_t result, bool cache) {
|
||||
return es_respond_auth_result(client.Get(), &(*msg), result, cache) == ES_RESPOND_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::MuteProcess(const Client &client, const audit_token_t *tok) {
|
||||
return es_mute_process(client.Get(), tok) == ES_RETURN_SUCCESS;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::ClearCache(const Client &client) {
|
||||
return es_clear_cache(client.Get()) == ES_CLEAR_CACHE_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
uint32_t EndpointSecurityAPI::ExecArgCount(const es_event_exec_t *event) {
|
||||
return es_exec_arg_count(event);
|
||||
}
|
||||
|
||||
es_string_token_t EndpointSecurityAPI::ExecArg(const es_event_exec_t *event, uint32_t index) {
|
||||
return es_exec_arg(event, index);
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
214
Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h
Normal file
214
Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h
Normal file
@@ -0,0 +1,214 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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.
|
||||
|
||||
/// This file groups all of the enriched message types - that is the
|
||||
/// objects that are constructed to hold all enriched event data prior
|
||||
/// to being logged.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHEDTYPES_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHEDTYPES_H
|
||||
|
||||
#include <time.h>
|
||||
#include <uuid/uuid.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class EnrichedFile {
|
||||
public:
|
||||
EnrichedFile(std::optional<std::shared_ptr<std::string>> &&user,
|
||||
std::optional<std::shared_ptr<std::string>> &&group,
|
||||
std::optional<std::shared_ptr<std::string>> &&hash)
|
||||
: user_(std::move(user)),
|
||||
group_(std::move(group)),
|
||||
hash_(std::move(hash)) {}
|
||||
|
||||
private:
|
||||
std::optional<std::shared_ptr<std::string>> user_;
|
||||
std::optional<std::shared_ptr<std::string>> group_;
|
||||
std::optional<std::shared_ptr<std::string>> hash_;
|
||||
};
|
||||
|
||||
class EnrichedProcess {
|
||||
public:
|
||||
EnrichedProcess(std::optional<std::shared_ptr<std::string>> &&effective_user,
|
||||
std::optional<std::shared_ptr<std::string>> &&effective_group,
|
||||
std::optional<std::shared_ptr<std::string>> &&real_user,
|
||||
std::optional<std::shared_ptr<std::string>> &&real_group,
|
||||
EnrichedFile &&executable)
|
||||
: effective_user_(std::move(effective_user)),
|
||||
effective_group_(std::move(effective_group)),
|
||||
real_user_(std::move(real_user)),
|
||||
real_group_(std::move(real_group)),
|
||||
executable_(std::move(executable)) {}
|
||||
|
||||
const std::optional<std::shared_ptr<std::string>> &real_user() const {
|
||||
return real_user_;
|
||||
}
|
||||
const std::optional<std::shared_ptr<std::string>> &real_group() const {
|
||||
return real_group_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<std::shared_ptr<std::string>> effective_user_;
|
||||
std::optional<std::shared_ptr<std::string>> effective_group_;
|
||||
std::optional<std::shared_ptr<std::string>> real_user_;
|
||||
std::optional<std::shared_ptr<std::string>> real_group_;
|
||||
EnrichedFile executable_;
|
||||
};
|
||||
|
||||
class EnrichedEventType {
|
||||
public:
|
||||
EnrichedEventType(Message &&es_msg, EnrichedProcess &&instigator)
|
||||
: es_msg_(std::move(es_msg)), instigator_(std::move(instigator)) {}
|
||||
|
||||
EnrichedEventType(EnrichedEventType &&other)
|
||||
: es_msg_(std::move(other.es_msg_)),
|
||||
instigator_(std::move(other.instigator_)) {}
|
||||
|
||||
virtual ~EnrichedEventType() = default;
|
||||
|
||||
const es_message_t &es_msg() const { return *es_msg_; }
|
||||
|
||||
const EnrichedProcess &instigator() const { return instigator_; }
|
||||
|
||||
private:
|
||||
Message es_msg_;
|
||||
EnrichedProcess instigator_;
|
||||
};
|
||||
|
||||
class EnrichedClose : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedClose(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedFile &&target)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
target_(std::move(target)) {}
|
||||
|
||||
private:
|
||||
EnrichedFile target_;
|
||||
};
|
||||
|
||||
class EnrichedExchange : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedExchange(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedFile &&file1, EnrichedFile &&file2)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
file1_(std::move(file1)),
|
||||
file2_(std::move(file2)) {}
|
||||
|
||||
private:
|
||||
EnrichedFile file1_;
|
||||
EnrichedFile file2_;
|
||||
};
|
||||
|
||||
class EnrichedExec : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedExec(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedProcess &&target, std::optional<EnrichedFile> &&script,
|
||||
std::optional<EnrichedFile> working_dir)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
target_(std::move(target)),
|
||||
script_(std::move(script)),
|
||||
working_dir_(std::move(working_dir)) {}
|
||||
|
||||
private:
|
||||
EnrichedProcess target_;
|
||||
std::optional<EnrichedFile> script_;
|
||||
std::optional<EnrichedFile> working_dir_;
|
||||
};
|
||||
|
||||
class EnrichedExit : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedExit(Message &&es_msg, EnrichedProcess &&instigator)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)) {}
|
||||
};
|
||||
|
||||
class EnrichedFork : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedFork(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedProcess &&target)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
target_(std::move(target)) {}
|
||||
|
||||
private:
|
||||
EnrichedProcess target_;
|
||||
};
|
||||
|
||||
class EnrichedLink : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedLink(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedFile &&source, EnrichedFile &&target_dir)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
source_(std::move(source)),
|
||||
target_dir_(std::move(target_dir)) {}
|
||||
|
||||
private:
|
||||
EnrichedFile source_;
|
||||
EnrichedFile target_dir_;
|
||||
};
|
||||
|
||||
class EnrichedRename : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedRename(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedFile &&source, std::optional<EnrichedFile> &&target,
|
||||
std::optional<EnrichedFile> &&target_dir)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
source_(std::move(source)),
|
||||
target_(std::move(target)),
|
||||
target_dir_(std::move(target_dir)) {}
|
||||
|
||||
private:
|
||||
EnrichedFile source_;
|
||||
std::optional<EnrichedFile> target_;
|
||||
std::optional<EnrichedFile> target_dir_;
|
||||
};
|
||||
|
||||
class EnrichedUnlink : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedUnlink(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedFile &&target)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
target_(std::move(target)) {}
|
||||
|
||||
private:
|
||||
EnrichedFile target_;
|
||||
};
|
||||
|
||||
using EnrichedType =
|
||||
std::variant<EnrichedClose, EnrichedExchange, EnrichedExec, EnrichedExit,
|
||||
EnrichedFork, EnrichedLink, EnrichedRename, EnrichedUnlink>;
|
||||
|
||||
class EnrichedMessage {
|
||||
public:
|
||||
EnrichedMessage(EnrichedType &&msg) : msg_(std::move(msg)) {
|
||||
uuid_generate(uuid_);
|
||||
clock_gettime(CLOCK_REALTIME, &enrichment_time_);
|
||||
}
|
||||
|
||||
const EnrichedType &GetEnrichedMessage() { return msg_; }
|
||||
|
||||
private:
|
||||
uuid_t uuid_;
|
||||
struct timespec enrichment_time_;
|
||||
EnrichedType msg_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
44
Source/santad/EventProviders/EndpointSecurity/Enricher.h
Normal file
44
Source/santad/EventProviders/EndpointSecurity/Enricher.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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_ENDPOINTSECURITY_ENRICHER_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHER_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class Enricher {
|
||||
public:
|
||||
Enricher();
|
||||
virtual ~Enricher() = default;
|
||||
virtual std::shared_ptr<EnrichedMessage> Enrich(Message &&msg);
|
||||
virtual EnrichedProcess Enrich(const es_process_t &es_proc);
|
||||
virtual EnrichedFile Enrich(const es_file_t &es_file);
|
||||
|
||||
virtual std::optional<std::shared_ptr<std::string>> UsernameForUID(uid_t uid);
|
||||
virtual std::optional<std::shared_ptr<std::string>> UsernameForGID(gid_t gid);
|
||||
|
||||
private:
|
||||
SantaCache<uid_t, std::optional<std::shared_ptr<std::string>>>
|
||||
username_cache_;
|
||||
SantaCache<gid_t, std::optional<std::shared_ptr<std::string>>>
|
||||
groupname_cache_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
137
Source/santad/EventProviders/EndpointSecurity/Enricher.mm
Normal file
137
Source/santad/EventProviders/EndpointSecurity/Enricher.mm
Normal file
@@ -0,0 +1,137 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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/EndpointSecurity/Enricher.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <grp.h>
|
||||
#include <pwd.h>
|
||||
#include <sys/types.h>
|
||||
#include <uuid/uuid.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Enricher::Enricher() : username_cache_(256), groupname_cache_(256) {}
|
||||
|
||||
std::shared_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
// TODO(mlw): Consider potential design patterns that could help reduce memory usage under load
|
||||
// (such as maybe the flyweight pattern)
|
||||
switch (es_msg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedClose(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.close.target)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedExchange(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exchangedata.file1),
|
||||
Enrich(*es_msg->event.exchangedata.file2)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedExec(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exec.target),
|
||||
(es_msg->version >= 2 && es_msg->event.exec.script)
|
||||
? std::make_optional(Enrich(*es_msg->event.exec.script))
|
||||
: std::nullopt,
|
||||
(es_msg->version >= 3) ? std::make_optional(Enrich(*es_msg->event.exec.cwd))
|
||||
: std::nullopt));
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedFork(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.fork.child)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT:
|
||||
return std::make_shared<EnrichedMessage>(
|
||||
EnrichedExit(std::move(es_msg), Enrich(*es_msg->process)));
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK:
|
||||
return std::make_shared<EnrichedMessage>(
|
||||
EnrichedLink(std::move(es_msg), Enrich(*es_msg->process),
|
||||
Enrich(*es_msg->event.link.source), Enrich(*es_msg->event.link.target_dir)));
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: {
|
||||
if (es_msg->event.rename.destination_type == ES_DESTINATION_TYPE_NEW_PATH) {
|
||||
return std::make_shared<EnrichedMessage>(EnrichedRename(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source),
|
||||
std::nullopt, Enrich(*es_msg->event.rename.destination.new_path.dir)));
|
||||
} else {
|
||||
return std::make_shared<EnrichedMessage>(EnrichedRename(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source),
|
||||
Enrich(*es_msg->event.rename.destination.existing_file), std::nullopt));
|
||||
}
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedUnlink(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.unlink.target)));
|
||||
default:
|
||||
// This is a programming error
|
||||
LOGE(@"Attempting to enrich an unhandled event type: %d", es_msg->event_type);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
EnrichedProcess Enricher::Enrich(const es_process_t &es_proc) {
|
||||
return EnrichedProcess(UsernameForUID(audit_token_to_euid(es_proc.audit_token)),
|
||||
UsernameForGID(audit_token_to_egid(es_proc.audit_token)),
|
||||
UsernameForUID(audit_token_to_ruid(es_proc.audit_token)),
|
||||
UsernameForGID(audit_token_to_rgid(es_proc.audit_token)),
|
||||
Enrich(*es_proc.executable));
|
||||
}
|
||||
|
||||
EnrichedFile Enricher::Enrich(const es_file_t &es_file) {
|
||||
// TODO(mlw): Consider having the enricher perform file hashing. This will
|
||||
// make more sense if we start including hashes in more event types.
|
||||
return EnrichedFile(UsernameForUID(es_file.stat.st_uid), UsernameForGID(es_file.stat.st_gid),
|
||||
std::nullopt);
|
||||
}
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> Enricher::UsernameForUID(uid_t uid) {
|
||||
std::optional<std::shared_ptr<std::string>> username = username_cache_.get(uid);
|
||||
|
||||
if (username.has_value()) {
|
||||
return username;
|
||||
} else {
|
||||
struct passwd *pw = getpwuid(uid);
|
||||
if (pw) {
|
||||
username = std::make_shared<std::string>(pw->pw_name);
|
||||
} else {
|
||||
username = std::nullopt;
|
||||
}
|
||||
|
||||
username_cache_.set(uid, username);
|
||||
|
||||
return username;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> Enricher::UsernameForGID(gid_t gid) {
|
||||
std::optional<std::shared_ptr<std::string>> groupname = groupname_cache_.get(gid);
|
||||
|
||||
if (groupname.has_value()) {
|
||||
return groupname;
|
||||
} else {
|
||||
struct group *gr = getgrgid(gid);
|
||||
if (gr) {
|
||||
groupname = std::make_shared<std::string>(gr->gr_name);
|
||||
} else {
|
||||
groupname = std::nullopt;
|
||||
}
|
||||
|
||||
groupname_cache_.set(gid, groupname);
|
||||
|
||||
return groupname;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
@@ -0,0 +1,49 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
|
||||
@interface EnricherTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation EnricherTest
|
||||
|
||||
- (void)testUidGid {
|
||||
Enricher enricher;
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> user = enricher.UsernameForUID(NOBODY_UID);
|
||||
XCTAssertTrue(user.has_value());
|
||||
XCTAssertEqual(strcmp(user->get()->c_str(), "nobody"), 0);
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> group = enricher.UsernameForGID(NOBODY_GID);
|
||||
XCTAssertTrue(group.has_value());
|
||||
XCTAssertEqual(strcmp(group->get()->c_str(), "nobody"), 0);
|
||||
|
||||
uid_t invalidUID = (uid_t)-123;
|
||||
gid_t invalidGID = (gid_t)-123;
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> invalidUser = enricher.UsernameForUID(invalidUID);
|
||||
XCTAssertFalse(invalidUser.has_value());
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> invalidGroup = enricher.UsernameForGID(invalidGID);
|
||||
XCTAssertFalse(invalidGroup.has_value());
|
||||
}
|
||||
|
||||
@end
|
||||
60
Source/santad/EventProviders/EndpointSecurity/Message.h
Normal file
60
Source/santad/EventProviders/EndpointSecurity/Message.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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_ENDPOINTSECURITY_MESSAGE_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MESSAGE_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class EndpointSecurityAPI;
|
||||
|
||||
class Message {
|
||||
public:
|
||||
Message(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
const es_message_t* es_msg);
|
||||
~Message();
|
||||
|
||||
Message(Message&& other);
|
||||
// Note: Safe to implement this, just not currently needed so left deleted.
|
||||
Message& operator=(Message&& rhs) = delete;
|
||||
|
||||
// In macOS 10.15, es_retain_message/es_release_message were unsupported
|
||||
// and required a full copy, which impacts performance if done too much...
|
||||
Message(const Message& other);
|
||||
Message& operator=(const Message& other) = delete;
|
||||
|
||||
// Operators to access underlying es_message_t
|
||||
const es_message_t* operator->() const { return es_msg_; }
|
||||
const es_message_t& operator*() const { return *es_msg_; }
|
||||
|
||||
std::string ParentProcessName() const;
|
||||
|
||||
private:
|
||||
std::shared_ptr<EndpointSecurityAPI> esapi_;
|
||||
es_message_t* es_msg_;
|
||||
|
||||
mutable std::string pname_;
|
||||
mutable std::string parent_pname_;
|
||||
|
||||
std::string GetProcessName(pid_t pid) const;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
65
Source/santad/EventProviders/EndpointSecurity/Message.mm
Normal file
65
Source/santad/EventProviders/EndpointSecurity/Message.mm
Normal file
@@ -0,0 +1,65 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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/EndpointSecurity/Message.h"
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
#include <libproc.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Message::Message(std::shared_ptr<EndpointSecurityAPI> esapi, const es_message_t *es_msg)
|
||||
: esapi_(esapi) {
|
||||
es_msg_ = esapi_->RetainMessage(es_msg);
|
||||
}
|
||||
|
||||
Message::~Message() {
|
||||
if (es_msg_) {
|
||||
esapi_->ReleaseMessage(es_msg_);
|
||||
}
|
||||
}
|
||||
|
||||
Message::Message(Message &&other) {
|
||||
esapi_ = std::move(other.esapi_);
|
||||
es_msg_ = other.es_msg_;
|
||||
other.es_msg_ = nullptr;
|
||||
}
|
||||
|
||||
Message::Message(const Message &other) {
|
||||
esapi_ = other.esapi_;
|
||||
es_msg_ = other.es_msg_;
|
||||
esapi_->RetainMessage(es_msg_);
|
||||
}
|
||||
|
||||
std::string Message::ParentProcessName() const {
|
||||
if (parent_pname_.length() == 0) {
|
||||
parent_pname_ = GetProcessName(es_msg_->process->ppid);
|
||||
}
|
||||
return parent_pname_;
|
||||
}
|
||||
|
||||
std::string Message::GetProcessName(pid_t pid) const {
|
||||
// Note: proc_name() accesses the `pbi_name` field of `struct proc_bsdinfo`. The size of `pname`
|
||||
// here is meant to match the size of `pbi_name`, and one extra byte ensure zero-terminated.
|
||||
char pname[MAXCOMLEN * 2 + 1] = {};
|
||||
if (proc_name(pid, pname, sizeof(pname)) > 0) {
|
||||
return std::string(pname);
|
||||
} else {
|
||||
return std::string("");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
135
Source/santad/EventProviders/EndpointSecurity/MessageTest.mm
Normal file
135
Source/santad/EventProviders/EndpointSecurity/MessageTest.mm
Normal file
@@ -0,0 +1,135 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libproc.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
bool IsPidInUse(pid_t pid) {
|
||||
char pname[MAXCOMLEN * 2 + 1] = {};
|
||||
errno = 0;
|
||||
if (proc_name(pid, pname, sizeof(pname)) <= 0 && errno == ESRCH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The PID may or may not actually be in use, but assume it is
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to find an unused PID by looking for libproc returning ESRCH errno.
|
||||
// Start searching backwards from PID_MAX to increase likelyhood that the
|
||||
// returned PID will still be unused by the time it's being used.
|
||||
// TODO(mlw): Alternatively, we could inject the `proc_name` function into
|
||||
// the `Message` object to remove the guesswork here.
|
||||
pid_t AttemptToFindUnusedPID() {
|
||||
for (pid_t pid = 99999 /* PID_MAX */; pid > 1; pid--) {
|
||||
if (!IsPidInUse(pid)) {
|
||||
return pid;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@interface MessageTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation MessageTest
|
||||
|
||||
- (void)setUp {
|
||||
}
|
||||
|
||||
- (void)testConstructorsAndDestructors {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
// Constructing a `Message` retains the underlying `es_message_t` and it is
|
||||
// released when the `Message` object is destructed.
|
||||
{ Message m(mockESApi, &esMsg); }
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testCopyConstructor {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
EXPECT_CALL(*mockESApi, ReleaseMessage(testing::_))
|
||||
.Times(2)
|
||||
.After(EXPECT_CALL(*mockESApi, RetainMessage(testing::_))
|
||||
.Times(2)
|
||||
.WillRepeatedly(testing::Return(&esMsg)));
|
||||
|
||||
{
|
||||
Message msg1(mockESApi, &esMsg);
|
||||
Message msg2(msg1);
|
||||
|
||||
// Both messages should now point to the same `es_message_t`
|
||||
XCTAssertEqual(msg1.operator->(), &esMsg);
|
||||
XCTAssertEqual(msg2.operator->(), &esMsg);
|
||||
}
|
||||
|
||||
// Ensure the retain/release mocks were called the expected number of times
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testGetParentProcessName {
|
||||
// Construct a message where the parent pid is ourself
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(getpid(), 0));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
// Search for an *existing* parent process.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
std::string got = msg.ParentProcessName();
|
||||
std::string want = getprogname();
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
// Search for a *non-existent* parent process.
|
||||
{
|
||||
pid_t newPpid = AttemptToFindUnusedPID();
|
||||
proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(newPpid, 34));
|
||||
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
std::string got = msg.ParentProcessName();
|
||||
std::string want = "";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,75 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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_ENDPOINTSECURITY_MOCKENDPOINTSECURITYAPI_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MOCKENDPOINTSECURITYAPI_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
|
||||
class MockEndpointSecurityAPI
|
||||
: public santa::santad::event_providers::endpoint_security::EndpointSecurityAPI {
|
||||
public:
|
||||
MOCK_METHOD(santa::santad::event_providers::endpoint_security::Client, NewClient,
|
||||
(void (^message_handler)(
|
||||
es_client_t *, santa::santad::event_providers::endpoint_security::Message)));
|
||||
|
||||
MOCK_METHOD(bool, Subscribe,
|
||||
(const santa::santad::event_providers::endpoint_security::Client &,
|
||||
const std::set<es_event_type_t> &));
|
||||
|
||||
MOCK_METHOD(es_message_t *, RetainMessage, (const es_message_t *msg));
|
||||
MOCK_METHOD(void, ReleaseMessage, (es_message_t * msg));
|
||||
|
||||
MOCK_METHOD(bool, RespondAuthResult,
|
||||
(const santa::santad::event_providers::endpoint_security::Client &,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
es_auth_result_t result, bool cache));
|
||||
|
||||
MOCK_METHOD(bool, MuteProcess,
|
||||
(const santa::santad::event_providers::endpoint_security::Client &,
|
||||
const audit_token_t *tok));
|
||||
|
||||
MOCK_METHOD(bool, ClearCache,
|
||||
(const santa::santad::event_providers::endpoint_security::Client &));
|
||||
|
||||
MOCK_METHOD(uint32_t, ExecArgCount, (const es_event_exec_t *event));
|
||||
MOCK_METHOD(es_string_token_t, ExecArg, (const es_event_exec_t *event, uint32_t index));
|
||||
|
||||
void SetExpectationsESNewClient() {
|
||||
EXPECT_CALL(*this, NewClient)
|
||||
.WillOnce(testing::Return(santa::santad::event_providers::endpoint_security::Client(
|
||||
nullptr, ES_NEW_CLIENT_RESULT_SUCCESS)));
|
||||
EXPECT_CALL(*this, MuteProcess).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*this, ClearCache).WillRepeatedly(testing::Return(true));
|
||||
EXPECT_CALL(*this, Subscribe).WillRepeatedly(testing::Return(true));
|
||||
}
|
||||
|
||||
void SetExpectationsRetainReleaseMessage(es_message_t *msg) {
|
||||
EXPECT_CALL(*this, ReleaseMessage).Times(testing::AnyNumber());
|
||||
EXPECT_CALL(*this, RetainMessage).WillRepeatedly(testing::Return(msg));
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,104 +0,0 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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 <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <bsm/libbsm.h>
|
||||
|
||||
CF_EXTERN_C_BEGIN
|
||||
es_string_token_t MakeStringToken(const NSString *_Nonnull s);
|
||||
|
||||
es_file_t MakeESFile(const char *_Nonnull path);
|
||||
es_process_t MakeESProcess(es_file_t *_Nonnull esFile);
|
||||
es_message_t MakeESMessage(es_event_type_t eventType, es_process_t *_Nonnull instigator,
|
||||
struct timespec ts);
|
||||
CF_EXTERN_C_END
|
||||
|
||||
@class ESMessage;
|
||||
typedef void (^ESMessageBuilderBlock)(ESMessage *_Nonnull builder);
|
||||
|
||||
// An ObjC builder wrapper around es_message_t
|
||||
@interface ESMessage : NSObject
|
||||
@property(nonatomic, readwrite, strong) NSString *_Nullable binaryPath;
|
||||
@property(nonatomic, readwrite) es_file_t *_Nonnull executable;
|
||||
@property(nonatomic, readwrite) es_process_t *_Nonnull process;
|
||||
@property(nonatomic, readwrite) es_message_t *_Nonnull message;
|
||||
@property(nonatomic, readonly) pid_t pid;
|
||||
|
||||
- (instancetype _Nonnull)initWithBlock:(ESMessageBuilderBlock _Nullable)block
|
||||
NS_DESIGNATED_INITIALIZER;
|
||||
@end
|
||||
|
||||
@interface ESResponse : NSObject
|
||||
@property(nonatomic) es_auth_result_t result;
|
||||
@property(nonatomic) bool shouldCache;
|
||||
@end
|
||||
|
||||
typedef void (^ESCallback)(ESResponse *_Nonnull);
|
||||
|
||||
// Singleton wrapper around all of the kernel-level EndpointSecurity framework functions.
|
||||
@interface MockEndpointSecurity : NSObject
|
||||
@property NSMutableArray *_Nonnull subscriptions;
|
||||
- (void)reset;
|
||||
- (void)registerResponseCallback:(es_event_type_t)t withCallback:(ESCallback _Nonnull)callback;
|
||||
- (void)triggerHandler:(es_message_t *_Nonnull)msg;
|
||||
|
||||
/// Retrieve an initialized singleton MockEndpointSecurity object
|
||||
+ (instancetype _Nonnull)mockEndpointSecurity;
|
||||
@end
|
||||
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_message_t *_Nullable es_copy_message(const es_message_t *_Nonnull msg);
|
||||
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
void es_free_message(es_message_t *_Nonnull msg);
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_new_client_result_t es_new_client(es_client_t *_Nullable *_Nonnull client,
|
||||
es_handler_block_t _Nonnull handler);
|
||||
|
||||
API_AVAILABLE(macos(10.15)) API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_mute_process(es_client_t * _Nonnull client,
|
||||
const audit_token_t * _Nonnull audit_token);
|
||||
|
||||
#if defined(MAC_OS_VERSION_12_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0
|
||||
API_AVAILABLE(macos(12.0))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_muted_paths_events(es_client_t *_Nonnull client,
|
||||
es_muted_paths_t *_Nonnull *_Nullable muted_paths);
|
||||
|
||||
API_AVAILABLE(macos(12.0))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
void es_release_muted_paths(es_muted_paths_t *_Nonnull muted_paths);
|
||||
#endif
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_respond_result_t es_respond_auth_result(es_client_t *_Nonnull client,
|
||||
const es_message_t *_Nonnull message,
|
||||
es_auth_result_t result, bool cache);
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_subscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
|
||||
uint32_t event_count);
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos) es_return_t es_delete_client(es_client_t *_Nullable client);
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_unsubscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
|
||||
uint32_t event_count);
|
||||
@@ -1,367 +0,0 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h"
|
||||
|
||||
CF_EXTERN_C_BEGIN
|
||||
es_string_token_t MakeStringToken(const NSString *_Nonnull s) {
|
||||
return (es_string_token_t){
|
||||
.length = [s length],
|
||||
.data = [s UTF8String],
|
||||
};
|
||||
}
|
||||
|
||||
es_file_t MakeESFile(const char *path) {
|
||||
es_file_t esFile = {};
|
||||
|
||||
esFile.path.data = path;
|
||||
esFile.path.length = strlen(path);
|
||||
esFile.path_truncated = false;
|
||||
|
||||
// Note: stat info is currently unused / not populated
|
||||
|
||||
return esFile;
|
||||
}
|
||||
|
||||
es_process_t MakeESProcess(es_file_t *esFile) {
|
||||
es_process_t esProc = {};
|
||||
esProc.executable = esFile;
|
||||
return esProc;
|
||||
}
|
||||
|
||||
es_message_t MakeESMessage(es_event_type_t eventType, es_process_t *instigator,
|
||||
struct timespec ts) {
|
||||
es_message_t esMsg = {};
|
||||
|
||||
esMsg.time = ts;
|
||||
esMsg.event_type = eventType;
|
||||
esMsg.process = instigator;
|
||||
|
||||
return esMsg;
|
||||
}
|
||||
|
||||
CF_EXTERN_C_END
|
||||
|
||||
@implementation ESMessage
|
||||
- (instancetype)init {
|
||||
return [self initWithBlock:nil];
|
||||
}
|
||||
|
||||
- (instancetype)initWithBlock:(ESMessageBuilderBlock)block {
|
||||
NSParameterAssert(block);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_pid = arc4random();
|
||||
[self initBaseObjects];
|
||||
block(self);
|
||||
[self fillLinks];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initBaseObjects {
|
||||
self.executable = static_cast<es_file_t *>(calloc(1, sizeof(es_file_t)));
|
||||
self.process = static_cast<es_process_t *>(calloc(1, sizeof(es_process_t)));
|
||||
|
||||
self.process->ppid = self.pid;
|
||||
self.process->original_ppid = self.pid;
|
||||
self.process->group_id = static_cast<pid_t>(arc4random());
|
||||
self.process->session_id = static_cast<pid_t>(arc4random());
|
||||
self.process->codesigning_flags =
|
||||
0x1 | 0x20000000; // CS_VALID | CS_SIGNED -> See kern/cs_blobs.h
|
||||
self.process->is_platform_binary = false;
|
||||
self.process->is_es_client = false;
|
||||
|
||||
self.message = static_cast<es_message_t *>(calloc(1, sizeof(es_message_t)));
|
||||
self.message->version = 4;
|
||||
self.message->mach_time = DISPATCH_TIME_NOW;
|
||||
self.message->deadline = DISPATCH_TIME_FOREVER;
|
||||
self.message->seq_num = 1;
|
||||
}
|
||||
|
||||
- (void)fillLinks {
|
||||
if (self.binaryPath != nil) {
|
||||
self.executable->path = MakeStringToken(self.binaryPath);
|
||||
}
|
||||
|
||||
if (self.process->executable == NULL) {
|
||||
self.process->executable = self.executable;
|
||||
}
|
||||
if (self.message->process == NULL) {
|
||||
self.message->process = self.process;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
free(self.process);
|
||||
free(self.executable);
|
||||
free(self.message);
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation ESResponse
|
||||
@end
|
||||
|
||||
@interface MockESClient : NSObject
|
||||
@property NSMutableArray *_Nonnull subscriptions;
|
||||
@property es_handler_block_t handler;
|
||||
@end
|
||||
|
||||
@implementation MockESClient
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
@synchronized(self) {
|
||||
_subscriptions = [NSMutableArray arrayWithCapacity:ES_EVENT_TYPE_LAST];
|
||||
for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) {
|
||||
[self.subscriptions addObject:@NO];
|
||||
}
|
||||
}
|
||||
}
|
||||
return self;
|
||||
};
|
||||
|
||||
- (void)resetSubscriptions {
|
||||
for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) {
|
||||
_subscriptions[i] = @NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)triggerHandler:(es_message_t *_Nonnull)msg {
|
||||
self.handler((__bridge es_client_t *_Nullable)self, msg);
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
@synchronized(self) {
|
||||
[self.subscriptions removeAllObjects];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface MockEndpointSecurity ()
|
||||
@property NSMutableArray<MockESClient *> *clients;
|
||||
|
||||
// Array of collections of ESCallback blocks
|
||||
// This should be of size ES_EVENT_TYPE_LAST, allowing for indexing by ES_EVENT_TYPE_xxx members.
|
||||
@property NSMutableArray<NSMutableArray<ESCallback> *> *responseCallbacks;
|
||||
@end
|
||||
|
||||
@implementation MockEndpointSecurity
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
@synchronized(self) {
|
||||
_clients = [NSMutableArray array];
|
||||
_responseCallbacks = [NSMutableArray arrayWithCapacity:ES_EVENT_TYPE_LAST];
|
||||
for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) {
|
||||
[self.responseCallbacks addObject:[NSMutableArray array]];
|
||||
}
|
||||
[self reset];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
};
|
||||
|
||||
- (void)resetResponseCallbacks {
|
||||
for (NSMutableArray *callback in self.responseCallbacks) {
|
||||
if (callback != nil) {
|
||||
[callback removeAllObjects];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reset {
|
||||
@synchronized(self) {
|
||||
[self.clients removeAllObjects];
|
||||
[self resetResponseCallbacks];
|
||||
}
|
||||
};
|
||||
|
||||
- (void)newClient:(es_client_t *_Nullable *_Nonnull)client
|
||||
handler:(es_handler_block_t __strong)handler {
|
||||
// es_client_t is generally used as a pointer to an opaque struct (secretly a mach port).
|
||||
// There is also a few nonnull initialization checks on it.
|
||||
MockESClient *mockClient = [[MockESClient alloc] init];
|
||||
*client = (__bridge es_client_t *)mockClient;
|
||||
mockClient.handler = handler;
|
||||
[self.clients addObject:mockClient];
|
||||
}
|
||||
|
||||
- (BOOL)removeClient:(es_client_t *_Nonnull)client {
|
||||
MockESClient *clientToRemove = [self findClient:client];
|
||||
|
||||
if (!clientToRemove) {
|
||||
NSLog(@"Attempted to remove unknown mock es client.");
|
||||
return NO;
|
||||
}
|
||||
|
||||
[self.clients removeObject:clientToRemove];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)triggerHandler:(es_message_t *_Nonnull)msg {
|
||||
for (MockESClient *client in self.clients) {
|
||||
if (client.subscriptions[msg->event_type]) {
|
||||
[client triggerHandler:msg];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)registerResponseCallback:(es_event_type_t)t withCallback:(ESCallback _Nonnull)callback {
|
||||
@synchronized(self) {
|
||||
[self.responseCallbacks[t] addObject:callback];
|
||||
}
|
||||
}
|
||||
|
||||
- (es_respond_result_t)respond_auth_result:(const es_message_t *_Nonnull)msg
|
||||
result:(es_auth_result_t)result
|
||||
cache:(bool)cache {
|
||||
@synchronized(self) {
|
||||
ESResponse *response = [[ESResponse alloc] init];
|
||||
response.result = result;
|
||||
response.shouldCache = cache;
|
||||
for (void (^callback)(ESResponse *) in self.responseCallbacks[msg->event_type]) {
|
||||
callback(response);
|
||||
}
|
||||
}
|
||||
return ES_RESPOND_RESULT_SUCCESS;
|
||||
};
|
||||
|
||||
- (MockESClient *)findClient:(es_client_t *)client {
|
||||
for (MockESClient *c in self.clients) {
|
||||
// Since we're mocking out a C interface and using this exact pointer as our
|
||||
// client identifier, only check for pointer equality.
|
||||
if (client == (__bridge es_client_t *)c) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)setSubscriptions:(const es_event_type_t *_Nonnull)events
|
||||
event_count:(uint32_t)event_count
|
||||
value:(NSNumber *)value
|
||||
client:(es_client_t *)client {
|
||||
@synchronized(self) {
|
||||
MockESClient *toUpdate = [self findClient:client];
|
||||
|
||||
if (toUpdate == nil) {
|
||||
NSLog(@"setting subscription for unknown client");
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < event_count; i++) {
|
||||
toUpdate.subscriptions[events[i]] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ (instancetype _Nonnull)mockEndpointSecurity {
|
||||
static MockEndpointSecurity *sharedES;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedES = [[MockEndpointSecurity alloc] init];
|
||||
});
|
||||
return sharedES;
|
||||
};
|
||||
@end
|
||||
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_message_t *_Nullable es_copy_message(const es_message_t *_Nonnull msg) {
|
||||
return (es_message_t *)msg;
|
||||
};
|
||||
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
void es_free_message(es_message_t *_Nonnull msg){};
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_new_client_result_t es_new_client(es_client_t *_Nullable *_Nonnull client,
|
||||
es_handler_block_t _Nonnull handler) {
|
||||
[[MockEndpointSecurity mockEndpointSecurity] newClient:client handler:handler];
|
||||
return ES_NEW_CLIENT_RESULT_SUCCESS;
|
||||
};
|
||||
|
||||
es_return_t es_mute_process(es_client_t * _Nonnull client,
|
||||
const audit_token_t * _Nonnull audit_token) {
|
||||
return ES_RETURN_SUCCESS;
|
||||
}
|
||||
|
||||
#if defined(MAC_OS_VERSION_12_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0
|
||||
API_AVAILABLE(macos(12.0))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_muted_paths_events(es_client_t *_Nonnull client,
|
||||
es_muted_paths_t *_Nonnull *_Nullable muted_paths) {
|
||||
es_muted_paths_t *tmp = (es_muted_paths_t *)malloc(sizeof(es_muted_paths_t));
|
||||
|
||||
tmp->count = 0;
|
||||
*muted_paths = (es_muted_paths_t *_Nullable)tmp;
|
||||
|
||||
return ES_RETURN_SUCCESS;
|
||||
};
|
||||
|
||||
API_AVAILABLE(macos(12.0))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
void es_release_muted_paths(es_muted_paths_t *_Nonnull muted_paths) {
|
||||
free(muted_paths);
|
||||
}
|
||||
#endif
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos) es_return_t es_delete_client(es_client_t *_Nullable client) {
|
||||
if (![[MockEndpointSecurity mockEndpointSecurity] removeClient:client]) {
|
||||
return ES_RETURN_ERROR;
|
||||
}
|
||||
return ES_RETURN_SUCCESS;
|
||||
};
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_respond_result_t es_respond_auth_result(es_client_t *_Nonnull client,
|
||||
const es_message_t *_Nonnull message,
|
||||
es_auth_result_t result, bool cache) {
|
||||
return [[MockEndpointSecurity mockEndpointSecurity] respond_auth_result:message
|
||||
result:result
|
||||
cache:cache];
|
||||
};
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_subscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
|
||||
uint32_t event_count) {
|
||||
[[MockEndpointSecurity mockEndpointSecurity] setSubscriptions:events
|
||||
event_count:event_count
|
||||
value:@YES
|
||||
client:client];
|
||||
return ES_RETURN_SUCCESS;
|
||||
}
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_unsubscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
|
||||
uint32_t event_count) {
|
||||
[[MockEndpointSecurity mockEndpointSecurity] setSubscriptions:events
|
||||
event_count:event_count
|
||||
value:@NO
|
||||
client:client];
|
||||
|
||||
return ES_RETURN_SUCCESS;
|
||||
};
|
||||
@@ -1,210 +0,0 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTCachingEndpointSecurityManager.h"
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SantaCache.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <bsm/libbsm.h>
|
||||
|
||||
uint64_t GetCurrentUptime() {
|
||||
return clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
}
|
||||
template <>
|
||||
uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const &t) {
|
||||
return (SantaCacheHasher<uint64_t>(t.fsid) << 1) ^ SantaCacheHasher<uint64_t>(t.fileid);
|
||||
}
|
||||
|
||||
@implementation SNTCachingEndpointSecurityManager {
|
||||
// Create 2 separate caches, mapping from the (filesysem + vnode ID) to a decision with a timestamp.
|
||||
// The root cache is for decisions on the root volume, which can never be unmounted and the other
|
||||
// is for executions from all other volumes. This cache will be emptied if any volume is unmounted.
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *_rootDecisionCache;
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *_nonRootDecisionCache;
|
||||
uint64_t _rootVnodeID;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_rootDecisionCache = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
_nonRootDecisionCache = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
|
||||
// Store the filesystem ID of the root vnode for split-cache usage.
|
||||
// If the stat fails for any reason _rootVnodeID will be 0 and all decisions will be in a single cache.
|
||||
struct stat rootStat;
|
||||
if (stat("/", &rootStat) == 0) {
|
||||
_rootVnodeID = (uint64_t)rootStat.st_dev;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (_rootDecisionCache) delete _rootDecisionCache;
|
||||
if (_nonRootDecisionCache) delete _nonRootDecisionCache;
|
||||
}
|
||||
|
||||
- (BOOL)respondFromCache:(es_message_t *)m API_AVAILABLE(macos(10.15)) {
|
||||
auto vnode_id = [self vnodeIDForFile:m->event.exec.target->executable];
|
||||
while (true) {
|
||||
// Check to see if item is in cache
|
||||
auto return_action = [self checkCache:vnode_id];
|
||||
|
||||
// If item was in cache with a valid response, return it.
|
||||
// If item is in cache but hasn't received a response yet, sleep for a bit.
|
||||
// If item is not in cache, break out of loop and forward request to callback.
|
||||
if (RESPONSE_VALID(return_action)) {
|
||||
switch (return_action) {
|
||||
case ACTION_RESPOND_ALLOW:
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
|
||||
break;
|
||||
case ACTION_RESPOND_ALLOW_COMPILER: {
|
||||
pid_t pid = audit_token_to_pid(m->process->audit_token);
|
||||
[self setIsCompilerPID:pid];
|
||||
// Don't let ES cache compilers
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
break;
|
||||
}
|
||||
default: es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false); break;
|
||||
}
|
||||
return YES;
|
||||
} else if (return_action == ACTION_REQUEST_BINARY || return_action == ACTION_RESPOND_ACK) {
|
||||
// TODO(rah): Look at a replacement for msleep(), maybe NSCondition
|
||||
usleep(5000);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[self addToCache:vnode_id decision:ACTION_REQUEST_BINARY currentTicks:0];
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (int)postAction:(santa_action_t)action
|
||||
forMessage:(santa_message_t)sm API_AVAILABLE(macos(10.15)) {
|
||||
es_respond_result_t ret;
|
||||
switch (action) {
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
[self setIsCompilerPID:sm.pid];
|
||||
|
||||
// Allow the exec and cache in our internal cache but don't let ES cache, because then
|
||||
// we won't see future execs of the compiler in order to record the PID.
|
||||
[self addToCache:sm.vnode_id
|
||||
decision:ACTION_RESPOND_ALLOW_COMPILER
|
||||
currentTicks:GetCurrentUptime()];
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW,
|
||||
false);
|
||||
break;
|
||||
case ACTION_RESPOND_ALLOW:
|
||||
case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE:
|
||||
[self addToCache:sm.vnode_id decision:ACTION_RESPOND_ALLOW currentTicks:GetCurrentUptime()];
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW,
|
||||
true);
|
||||
break;
|
||||
case ACTION_RESPOND_DENY:
|
||||
[self addToCache:sm.vnode_id decision:ACTION_RESPOND_DENY currentTicks:GetCurrentUptime()];
|
||||
OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_TOOLONG:
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_DENY,
|
||||
false);
|
||||
break;
|
||||
case ACTION_RESPOND_ACK: return ES_RESPOND_RESULT_SUCCESS;
|
||||
default: ret = ES_RESPOND_RESULT_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
- (void)addToCache:(santa_vnode_id_t)identifier
|
||||
decision:(santa_action_t)decision
|
||||
currentTicks:(uint64_t)microsecs {
|
||||
auto _decisionCache = [self cacheForVnodeID:identifier];
|
||||
switch (decision) {
|
||||
case ACTION_REQUEST_BINARY:
|
||||
_decisionCache->set(identifier, (uint64_t)ACTION_REQUEST_BINARY << 56, 0);
|
||||
break;
|
||||
case ACTION_RESPOND_ACK:
|
||||
_decisionCache->set(identifier, (uint64_t)ACTION_RESPOND_ACK << 56,
|
||||
((uint64_t)ACTION_REQUEST_BINARY << 56));
|
||||
break;
|
||||
case ACTION_RESPOND_ALLOW:
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
case ACTION_RESPOND_DENY: {
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
uint64_t val = ((uint64_t)decision << 56) | (microsecs & 0xFFFFFFFFFFFFFF);
|
||||
if (!_decisionCache->set(identifier, val, ((uint64_t)ACTION_REQUEST_BINARY << 56))) {
|
||||
_decisionCache->set(identifier, val, ((uint64_t)ACTION_RESPOND_ACK << 56));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE: {
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
uint64_t val = ((uint64_t)decision << 56) | (microsecs & 0xFFFFFFFFFFFFFF);
|
||||
_decisionCache->set(identifier, val, 0);
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
// TODO(rah): Look at a replacement for wakeup(), maybe NSCondition
|
||||
}
|
||||
|
||||
- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly API_AVAILABLE(macos(10.15)) {
|
||||
_nonRootDecisionCache->clear();
|
||||
if (!nonRootOnly) _rootDecisionCache->clear();
|
||||
if (!self.connectionEstablished) return YES; // if not connected, there's nothing to flush.
|
||||
return es_clear_cache(self.client) == ES_CLEAR_CACHE_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
- (NSArray<NSNumber *> *)cacheCounts {
|
||||
return @[ @(_rootDecisionCache->count()), @(_nonRootDecisionCache->count()) ];
|
||||
}
|
||||
|
||||
- (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID {
|
||||
auto result = ACTION_UNSET;
|
||||
uint64_t decision_time = 0;
|
||||
|
||||
uint64_t cache_val = [self cacheForVnodeID:vnodeID]->get(vnodeID);
|
||||
if (cache_val == 0) return result;
|
||||
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
result = (santa_action_t)(cache_val >> 56);
|
||||
decision_time = (cache_val & ~(0xFF00000000000000));
|
||||
|
||||
if (RESPONSE_VALID(result)) {
|
||||
if (result == ACTION_RESPOND_DENY) {
|
||||
auto expiry_time = decision_time + (500 * 100000); // kMaxCacheDenyTimeMilliseconds
|
||||
if (expiry_time < GetCurrentUptime()) {
|
||||
[self cacheForVnodeID:vnodeID]->remove(vnodeID);
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeID {
|
||||
[self cacheForVnodeID:vnodeID]->remove(vnodeID);
|
||||
// TODO(rah): Look at a replacement for wakeup(), maybe NSCondition
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (SantaCache<santa_vnode_id_t, uint64_t> *)cacheForVnodeID:(santa_vnode_id_t)vnodeID {
|
||||
return (vnodeID.fsid == _rootVnodeID || _rootVnodeID == 0) ? _rootDecisionCache : _nonRootDecisionCache;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,291 +0,0 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <DiskArbitration/DiskArbitration.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#import <bsm/libbsm.h>
|
||||
|
||||
#include <sys/mount.h>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/santad/EventProviders/SNTDeviceManager.h"
|
||||
|
||||
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
|
||||
#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h"
|
||||
|
||||
@interface SNTDeviceManagerTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@end
|
||||
|
||||
@implementation SNTDeviceManagerTest
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator eventLogType]).andReturn(-1);
|
||||
|
||||
fclose(stdout);
|
||||
}
|
||||
|
||||
- (ESResponse *)triggerTestMountEvent:(SNTDeviceManager *)deviceManager
|
||||
mockES:(MockEndpointSecurity *)mockES
|
||||
mockDA:(MockDiskArbitration *)mockDA
|
||||
eventType:(es_event_type_t)eventType
|
||||
diskInfoOverrides:(NSDictionary *)diskInfo {
|
||||
if (!deviceManager.subscribed) {
|
||||
// [deviceManager listen] is synchronous, but we want to asynchronously dispatch it
|
||||
// with an enforced timeout to ensure that we never run into issues where the client
|
||||
// never instantiates.
|
||||
XCTestExpectation *initExpectation =
|
||||
[self expectationWithDescription:@"Wait for SNTDeviceManager to instantiate"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
[deviceManager listen];
|
||||
});
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
while (!deviceManager.subscribed)
|
||||
;
|
||||
[initExpectation fulfill];
|
||||
});
|
||||
[self waitForExpectations:@[ initExpectation ] timeout:60.0];
|
||||
}
|
||||
|
||||
struct statfs *fs = static_cast<struct statfs *>(calloc(1, sizeof(struct statfs)));
|
||||
NSString *test_mntfromname = @"/dev/disk2s1";
|
||||
NSString *test_mntonname = @"/Volumes/KATE'S 4G";
|
||||
const char *c_mntfromname = [test_mntfromname UTF8String];
|
||||
const char *c_mntonname = [test_mntonname UTF8String];
|
||||
|
||||
strncpy(fs->f_mntfromname, c_mntfromname, MAXPATHLEN);
|
||||
strncpy(fs->f_mntonname, c_mntonname, MAXPATHLEN);
|
||||
|
||||
MockDADisk *disk = [[MockDADisk alloc] init];
|
||||
disk.diskDescription = @{
|
||||
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey : @"USB",
|
||||
(__bridge NSString *)kDADiskDescriptionMediaRemovableKey : @YES,
|
||||
@"DAVolumeMountable" : @YES,
|
||||
@"DAVolumePath" : test_mntonname,
|
||||
@"DADeviceModel" : @"Some device model",
|
||||
@"DADevicePath" : test_mntonname,
|
||||
@"DADeviceVendor" : @"Some vendor",
|
||||
@"DAAppearanceTime" : @0,
|
||||
@"DAMediaBSDName" : test_mntfromname,
|
||||
};
|
||||
|
||||
if (diskInfo != nil) {
|
||||
NSMutableDictionary *mergedDiskDescription = [disk.diskDescription mutableCopy];
|
||||
for (NSString *key in diskInfo) {
|
||||
mergedDiskDescription[key] = diskInfo[key];
|
||||
}
|
||||
disk.diskDescription = (NSDictionary *)mergedDiskDescription;
|
||||
}
|
||||
|
||||
[mockDA insert:disk bsdName:test_mntfromname];
|
||||
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.binaryPath = @"/System/Library/Filesystems/msdos.fs/Contents/Resources/mount_msdos";
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = eventType;
|
||||
if (eventType == ES_EVENT_TYPE_AUTH_MOUNT) {
|
||||
m.message->event = (es_events_t){.mount = {.statfs = fs}};
|
||||
} else {
|
||||
m.message->event = (es_events_t){.remount = {.statfs = fs}};
|
||||
}
|
||||
}];
|
||||
|
||||
XCTestExpectation *mountExpectation =
|
||||
[self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:eventType
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[mountExpectation fulfill];
|
||||
}];
|
||||
|
||||
[mockES triggerHandler:m.message];
|
||||
|
||||
[self waitForExpectations:@[ mountExpectation ] timeout:60.0];
|
||||
free(fs);
|
||||
|
||||
return got;
|
||||
}
|
||||
|
||||
- (void)testUSBBlockDisabled {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA reset];
|
||||
|
||||
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
|
||||
deviceManager.blockUSBMount = NO;
|
||||
ESResponse *got = [self triggerTestMountEvent:deviceManager
|
||||
mockES:mockES
|
||||
mockDA:mockDA
|
||||
eventType:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
|
||||
- (void)testRemount {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA reset];
|
||||
|
||||
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
|
||||
deviceManager.blockUSBMount = YES;
|
||||
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"Wait for SNTDeviceManager's blockCallback to trigger"];
|
||||
|
||||
__block NSString *gotmntonname, *gotmntfromname;
|
||||
__block NSArray<NSString *> *gotRemountedArgs;
|
||||
deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
gotRemountedArgs = event.remountArgs;
|
||||
gotmntonname = event.mntonname;
|
||||
gotmntfromname = event.mntfromname;
|
||||
[expectation fulfill];
|
||||
};
|
||||
|
||||
ESResponse *got = [self triggerTestMountEvent:deviceManager
|
||||
mockES:mockES
|
||||
mockDA:mockDA
|
||||
eventType:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);
|
||||
XCTAssertEqual(mockDA.wasRemounted, YES);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertEqualObjects(gotRemountedArgs, deviceManager.remountArgs);
|
||||
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
|
||||
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
|
||||
}
|
||||
|
||||
- (void)testBlockNoRemount {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA reset];
|
||||
|
||||
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
|
||||
deviceManager.blockUSBMount = YES;
|
||||
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"Wait for SNTDeviceManager's blockCallback to trigger"];
|
||||
|
||||
__block NSString *gotmntonname, *gotmntfromname;
|
||||
__block NSArray<NSString *> *gotRemountedArgs;
|
||||
deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
gotRemountedArgs = event.remountArgs;
|
||||
gotmntonname = event.mntonname;
|
||||
gotmntfromname = event.mntfromname;
|
||||
[expectation fulfill];
|
||||
};
|
||||
|
||||
ESResponse *got = [self triggerTestMountEvent:deviceManager
|
||||
mockES:mockES
|
||||
mockDA:mockDA
|
||||
eventType:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertNil(gotRemountedArgs);
|
||||
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
|
||||
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
|
||||
}
|
||||
|
||||
- (void)testEnsureRemountsCannotChangePerms {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA reset];
|
||||
|
||||
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
|
||||
deviceManager.blockUSBMount = YES;
|
||||
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"Wait for SNTDeviceManager's blockCallback to trigger"];
|
||||
|
||||
__block NSString *gotmntonname, *gotmntfromname;
|
||||
__block NSArray<NSString *> *gotRemountedArgs;
|
||||
deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
gotRemountedArgs = event.remountArgs;
|
||||
gotmntonname = event.mntonname;
|
||||
gotmntfromname = event.mntfromname;
|
||||
[expectation fulfill];
|
||||
};
|
||||
|
||||
ESResponse *got = [self triggerTestMountEvent:deviceManager
|
||||
mockES:mockES
|
||||
mockDA:mockDA
|
||||
eventType:ES_EVENT_TYPE_AUTH_REMOUNT
|
||||
diskInfoOverrides:nil];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);
|
||||
XCTAssertEqual(mockDA.wasRemounted, YES);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:10.0];
|
||||
|
||||
XCTAssertEqualObjects(gotRemountedArgs, deviceManager.remountArgs);
|
||||
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
|
||||
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
|
||||
}
|
||||
|
||||
- (void)testEnsureDMGsDoNotPrompt {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA reset];
|
||||
|
||||
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
|
||||
deviceManager.blockUSBMount = YES;
|
||||
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
XCTFail(@"Should not be called");
|
||||
};
|
||||
|
||||
NSDictionary *diskInfo = @{
|
||||
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey: @"Virtual Interface",
|
||||
(__bridge NSString *)kDADiskDescriptionDeviceModelKey: @"Disk Image",
|
||||
(__bridge NSString *)kDADiskDescriptionMediaNameKey: @"disk image",
|
||||
};
|
||||
|
||||
|
||||
ESResponse *got = [self triggerTestMountEvent:deviceManager
|
||||
mockES:mockES
|
||||
mockDA:mockDA
|
||||
eventType:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:diskInfo];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW);
|
||||
XCTAssertEqual(mockDA.wasRemounted, NO);
|
||||
}
|
||||
@end
|
||||
38
Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h
Normal file
38
Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
#import "Source/santad/SNTExecutionController.h"
|
||||
|
||||
/// ES Client focused on subscribing to AUTH variants and authorizing the events
|
||||
/// based on configured policy.
|
||||
@interface SNTEndpointSecurityAuthorizer
|
||||
: SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)
|
||||
esApi
|
||||
execController:(SNTExecutionController *)execController
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:
|
||||
(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache;
|
||||
|
||||
@end
|
||||
145
Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.mm
Normal file
145
Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.mm
Normal file
@@ -0,0 +1,145 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#include <os/base.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
@interface SNTEndpointSecurityAuthorizer ()
|
||||
@property SNTCompilerController *compilerController;
|
||||
@property SNTExecutionController *execController;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityAuthorizer {
|
||||
std::shared_ptr<AuthResultCache> _authResultCache;
|
||||
}
|
||||
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
execController:(SNTExecutionController *)execController
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache {
|
||||
self = [super initWithESAPI:std::move(esApi)];
|
||||
if (self) {
|
||||
_execController = execController;
|
||||
_compilerController = compilerController;
|
||||
_authResultCache = authResultCache;
|
||||
|
||||
[self establishClientOrDie];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)processMessage:(const Message &)msg {
|
||||
const es_file_t *targetFile = msg->event.exec.target->executable;
|
||||
|
||||
while (true) {
|
||||
santa_action_t returnAction = self->_authResultCache->CheckCache(targetFile);
|
||||
if (RESPONSE_VALID(returnAction)) {
|
||||
es_auth_result_t authResult = ES_AUTH_RESULT_DENY;
|
||||
|
||||
switch (returnAction) {
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
[self.compilerController setProcess:msg->event.exec.target->audit_token isCompiler:true];
|
||||
OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_ALLOW: authResult = ES_AUTH_RESULT_ALLOW; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
[self respondToMessage:msg
|
||||
withAuthResult:authResult
|
||||
cacheable:(authResult == ES_AUTH_RESULT_ALLOW)];
|
||||
return;
|
||||
} else if (returnAction == ACTION_REQUEST_BINARY) {
|
||||
// TODO(mlw): Add a metric here to observe how ofthen this happens in practice.
|
||||
// TODO(mlw): Look into caching a `Deferred<value>` to better prevent
|
||||
// raciness of multiple threads checking the cache simultaneously.
|
||||
// Also mitigates need to poll.
|
||||
usleep(5000);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self->_authResultCache->AddToCache(targetFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
[self.execController validateExecEvent:msg
|
||||
postAction:^bool(santa_action_t action) {
|
||||
return [self postAction:action forMessage:msg];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg {
|
||||
if (unlikely(esMsg->event_type != ES_EVENT_TYPE_AUTH_EXEC)) {
|
||||
// This is a programming error
|
||||
LOGE(@"Atteempting to authorize a non-exec event");
|
||||
[NSException raise:@"Invalid event type"
|
||||
format:@"Authorizing unexpected event type: %d", esMsg->event_type];
|
||||
}
|
||||
|
||||
if (![self.execController synchronousShouldProcessExecEvent:esMsg]) {
|
||||
[self postAction:ACTION_RESPOND_DENY forMessage:esMsg];
|
||||
return;
|
||||
}
|
||||
|
||||
[self processMessage:std::move(esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
[self processMessage:msg];
|
||||
}];
|
||||
}
|
||||
|
||||
- (bool)postAction:(santa_action_t)action forMessage:(const Message &)esMsg {
|
||||
es_auth_result_t authResult;
|
||||
|
||||
switch (action) {
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
[self.compilerController setProcess:esMsg->event.exec.target->audit_token isCompiler:true];
|
||||
OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_ALLOW: authResult = ES_AUTH_RESULT_ALLOW; break;
|
||||
case ACTION_RESPOND_DENY: authResult = ES_AUTH_RESULT_DENY; break;
|
||||
default:
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"Invalid action for postAction, exiting.");
|
||||
[NSException raise:@"Invalid post action" format:@"Invalid post action: %d", action];
|
||||
}
|
||||
|
||||
self->_authResultCache->AddToCache(esMsg->event.exec.target->executable, action);
|
||||
|
||||
// Don't let the ES framework cache DENY results. Santa only flushes ES cache
|
||||
// when a new DENY rule is received. If DENY results were cached and a rule
|
||||
// update made the executable allowable, ES would continue to apply the DENY
|
||||
// cached result. Note however that the local AuthResultCache will cache
|
||||
// DENY results.
|
||||
return [self respondToMessage:esMsg
|
||||
withAuthResult:authResult
|
||||
cacheable:(authResult == ES_AUTH_RESULT_ALLOW)];
|
||||
}
|
||||
|
||||
- (void)enable {
|
||||
[super subscribeAndClearCache:{
|
||||
ES_EVENT_TYPE_AUTH_EXEC,
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,273 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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 <EndpointSecurity/ESTypes.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
#import "Source/santad/SNTExecutionController.h"
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
class MockAuthResultCache : public AuthResultCache {
|
||||
public:
|
||||
using AuthResultCache::AuthResultCache;
|
||||
|
||||
MOCK_METHOD(bool, AddToCache, (const es_file_t *es_file, santa_action_t decision));
|
||||
MOCK_METHOD(santa_action_t, CheckCache, (const es_file_t *es_file));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityAuthorizer (Testing)
|
||||
- (void)processMessage:(const Message &)msg;
|
||||
- (bool)postAction:(santa_action_t)action forMessage:(const Message &)esMsg;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityAuthorizerTest : XCTestCase
|
||||
@property id mockExecController;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityAuthorizerTest
|
||||
|
||||
- (void)setUp {
|
||||
self.mockExecController = OCMStrictClassMock([SNTExecutionController class]);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.mockExecController stopMocking];
|
||||
}
|
||||
|
||||
- (void)testEnable {
|
||||
// Ensure the client subscribes to expected event types
|
||||
std::set<es_event_type_t> expectedEventSubs{ES_EVENT_TYPE_AUTH_EXEC};
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
id authClient = [[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi];
|
||||
|
||||
EXPECT_CALL(*mockESApi, ClearCache)
|
||||
.After(EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs))
|
||||
.WillOnce(testing::Return(true)))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
[authClient enable];
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
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(&esMsg);
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
execController:self.mockExecController
|
||||
compilerController:nil
|
||||
authResultCache:nullptr];
|
||||
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
// Test unhandled event type
|
||||
{
|
||||
// Temporarily change the event type
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
|
||||
XCTAssertThrows([authClient handleMessage:Message(mockESApi, &esMsg)]);
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_EXEC;
|
||||
}
|
||||
|
||||
// Test SNTExecutionController determines the event shouldn't be processed
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(NO);
|
||||
|
||||
OCMExpect([mockAuthClient postAction:ACTION_RESPOND_DENY forMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient postAction:ACTION_RESPOND_DENY forMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
|
||||
[mockAuthClient handleMessage:std::move(msg)];
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
}
|
||||
|
||||
// Test SNTExecutionController determines the event should be processed and
|
||||
// processMessage:handler: is called.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(YES);
|
||||
|
||||
OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg) handler:[OCMArg any]])
|
||||
.ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg) handler:[OCMArg any]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
|
||||
[mockAuthClient handleMessage:std::move(msg)];
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
[mockAuthClient stopMocking];
|
||||
}
|
||||
|
||||
- (void)testProcessMessageWaitThenAllow {
|
||||
// This test ensures that if there is an outstanding action for
|
||||
// an item, it will check the cache again until a result exists.
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_file_t execFile = MakeESFile("bar");
|
||||
es_process_t execProc = MakeESProcess(&execFile, MakeAuditToken(12, 23), MakeAuditToken(34, 45));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth);
|
||||
esMsg.event.exec.target = &execProc;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
EXPECT_CALL(*mockAuthCache, CheckCache)
|
||||
.WillOnce(testing::Return(ACTION_REQUEST_BINARY))
|
||||
.WillOnce(testing::Return(ACTION_REQUEST_BINARY))
|
||||
.WillOnce(testing::Return(ACTION_RESPOND_ALLOW_COMPILER))
|
||||
.WillOnce(testing::Return(ACTION_UNSET));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(testing::_, ACTION_REQUEST_BINARY))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
id mockCompilerController = OCMStrictClassMock([SNTCompilerController class]);
|
||||
OCMExpect([mockCompilerController setProcess:execProc.audit_token isCompiler:true]);
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
execController:self.mockExecController
|
||||
compilerController:mockCompilerController
|
||||
authResultCache:mockAuthCache];
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
// This block tests that processing is held up until an outstanding thread
|
||||
// processing another event completes and returns a result. This test
|
||||
// specifically will check the `ACTION_RESPOND_ALLOW_COMPILER` flow.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
OCMExpect([mockAuthClient respondToMessage:msg
|
||||
withAuthResult:ES_AUTH_RESULT_ALLOW
|
||||
cacheable:true]);
|
||||
|
||||
[mockAuthClient processMessage:msg];
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
XCTAssertTrue(OCMVerifyAll(mockCompilerController));
|
||||
}
|
||||
|
||||
// This block tests uncached events storing appropriate cache marker and then
|
||||
// running the exec controller to validate the exec event.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
OCMExpect([self.mockExecController validateExecEvent:msg postAction:OCMOCK_ANY])
|
||||
.ignoringNonObjectArgs();
|
||||
|
||||
[mockAuthClient processMessage:msg];
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
XCTAssertTrue(OCMVerifyAll(mockCompilerController));
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
|
||||
|
||||
[mockCompilerController stopMocking];
|
||||
[mockAuthClient stopMocking];
|
||||
}
|
||||
|
||||
- (void)testPostAction {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_file_t execFile = MakeESFile("bar");
|
||||
es_process_t execProc = MakeESProcess(&execFile, MakeAuditToken(12, 23), MakeAuditToken(34, 45));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth);
|
||||
esMsg.event.exec.target = &execProc;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_ALLOW_COMPILER))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_ALLOW))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_DENY))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
id mockCompilerController = OCMStrictClassMock([SNTCompilerController class]);
|
||||
OCMExpect([mockCompilerController setProcess:execProc.audit_token isCompiler:true]);
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
execController:self.mockExecController
|
||||
compilerController:mockCompilerController
|
||||
authResultCache:mockAuthCache];
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
XCTAssertThrows([mockAuthClient postAction:(santa_action_t)123 forMessage:msg]);
|
||||
|
||||
std::map<santa_action_t, es_auth_result_t> actions = {
|
||||
{ACTION_RESPOND_ALLOW_COMPILER, ES_AUTH_RESULT_ALLOW},
|
||||
{ACTION_RESPOND_ALLOW, ES_AUTH_RESULT_ALLOW},
|
||||
{ACTION_RESPOND_DENY, ES_AUTH_RESULT_DENY},
|
||||
};
|
||||
|
||||
for (const auto &kv : actions) {
|
||||
OCMExpect([mockAuthClient respondToMessage:msg
|
||||
withAuthResult:kv.second
|
||||
cacheable:kv.second == ES_AUTH_RESULT_ALLOW]);
|
||||
|
||||
[mockAuthClient postAction:kv.first forMessage:msg];
|
||||
}
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
|
||||
|
||||
[mockCompilerController stopMocking];
|
||||
[mockAuthClient stopMocking];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,17 +12,9 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/Santa.pbobjc.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol SNTLogOutput<NSObject>
|
||||
|
||||
- (void)logEvent:(SNTPBSantaMessage *)event;
|
||||
- (void)flush;
|
||||
#include "Source/santad/EventProviders/SNTEndpointSecurityClientBase.h"
|
||||
|
||||
/// This should be treated as an Abstract Base Class and not directly instantiated
|
||||
@interface SNTEndpointSecurityClient : NSObject <SNTEndpointSecurityClientBase>
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
243
Source/santad/EventProviders/SNTEndpointSecurityClient.mm
Normal file
243
Source/santad/EventProviders/SNTEndpointSecurityClient.mm
Normal file
@@ -0,0 +1,243 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <mach/mach_time.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/qos.h>
|
||||
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
@interface SNTEndpointSecurityClient ()
|
||||
@property int64_t deadlineMarginMS;
|
||||
@end
|
||||
;
|
||||
|
||||
@implementation SNTEndpointSecurityClient {
|
||||
std::shared_ptr<EndpointSecurityAPI> _esApi;
|
||||
Client _esClient;
|
||||
mach_timebase_info_data_t _timebase;
|
||||
dispatch_queue_t _authQueue;
|
||||
dispatch_queue_t _notifyQueue;
|
||||
}
|
||||
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_esApi = std::move(esApi);
|
||||
_deadlineMarginMS = 5000;
|
||||
|
||||
if (mach_timebase_info(&_timebase) != KERN_SUCCESS) {
|
||||
LOGE(@"Failed to get mach timebase info");
|
||||
// Assumed to be transitory failure. Let the daemon restart.
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
_authQueue = dispatch_queue_create(
|
||||
"com.google.santa.daemon.auth_queue",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
|
||||
_notifyQueue = dispatch_queue_create(
|
||||
"com.google.santa.daemon.notify_queue",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_BACKGROUND, 0));
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)errorMessageForNewClientResult:(es_new_client_result_t)result {
|
||||
switch (result) {
|
||||
case ES_NEW_CLIENT_RESULT_SUCCESS: return nil;
|
||||
case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED: return @"Full-disk access not granted";
|
||||
case ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED: return @"Not entitled";
|
||||
case ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED: return @"Not running as root";
|
||||
case ES_NEW_CLIENT_RESULT_ERR_INVALID_ARGUMENT: return @"Invalid argument";
|
||||
case ES_NEW_CLIENT_RESULT_ERR_INTERNAL: return @"Internal error";
|
||||
case ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS: return @"Too many simultaneous clients";
|
||||
default: return @"Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg {
|
||||
// This method should only be used by classes derived
|
||||
// from SNTEndpointSecurityClient.
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
}
|
||||
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg
|
||||
ignoringOtherESClients:(BOOL)ignoringOtherESClients {
|
||||
if (esMsg->process->is_es_client && ignoringOtherESClients) {
|
||||
if (esMsg->action_type == ES_ACTION_TYPE_AUTH) {
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:true];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)establishClientOrDie {
|
||||
if (self->_esClient.IsConnected()) {
|
||||
// This is a programming error
|
||||
LOGE(@"Client already established. Aborting.");
|
||||
[NSException raise:@"Client already established" format:@"IsConnected already true"];
|
||||
}
|
||||
|
||||
self->_esClient = self->_esApi->NewClient(^(es_client_t *c, Message esMsg) {
|
||||
if ([self shouldHandleMessage:esMsg
|
||||
ignoringOtherESClients:[[SNTConfigurator configurator]
|
||||
ignoreOtherEndpointSecurityClients]]) {
|
||||
[self handleMessage:std::move(esMsg)];
|
||||
}
|
||||
});
|
||||
|
||||
if (!self->_esClient.IsConnected()) {
|
||||
NSString *errMsg = [self errorMessageForNewClientResult:_esClient.NewClientResult()];
|
||||
LOGE(@"Unable to create EndpointSecurity client: %@", errMsg);
|
||||
[NSException raise:@"Failed to create ES client" format:@"%@", errMsg];
|
||||
} else {
|
||||
LOGI(@"Connected to EndpointSecurity");
|
||||
}
|
||||
|
||||
if (![self muteSelf]) {
|
||||
[NSException raise:@"ES Mute Failure" format:@"Failed to mute self"];
|
||||
}
|
||||
}
|
||||
|
||||
+ (bool)populateAuditTokenSelf:(audit_token_t *)tok {
|
||||
mach_msg_type_number_t count = TASK_AUDIT_TOKEN_COUNT;
|
||||
if (task_info(mach_task_self(), TASK_AUDIT_TOKEN, (task_info_t)tok, &count) != KERN_SUCCESS) {
|
||||
LOGE(@"Failed to fetch this client's audit token.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
- (bool)muteSelf {
|
||||
audit_token_t myAuditToken;
|
||||
if (![SNTEndpointSecurityClient populateAuditTokenSelf:&myAuditToken]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!self->_esApi->MuteProcess(self->_esClient, &myAuditToken)) {
|
||||
LOGE(@"Failed to mute this client's process.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
- (bool)clearCache {
|
||||
return _esApi->ClearCache(self->_esClient);
|
||||
}
|
||||
|
||||
- (bool)subscribe:(const std::set<es_event_type_t> &)events {
|
||||
return _esApi->Subscribe(_esClient, events);
|
||||
}
|
||||
|
||||
- (bool)subscribeAndClearCache:(const std::set<es_event_type_t> &)events {
|
||||
return [self subscribe:events] && [self clearCache];
|
||||
}
|
||||
|
||||
- (bool)respondToMessage:(const Message &)msg
|
||||
withAuthResult:(es_auth_result_t)result
|
||||
cacheable:(bool)cacheable {
|
||||
return _esApi->RespondAuthResult(_esClient, msg, result, cacheable);
|
||||
}
|
||||
|
||||
- (void)processEnrichedMessage:(std::shared_ptr<EnrichedMessage>)msg
|
||||
handler:(void (^)(std::shared_ptr<EnrichedMessage>))messageHandler {
|
||||
dispatch_async(_notifyQueue, ^{
|
||||
messageHandler(std::move(msg));
|
||||
});
|
||||
}
|
||||
|
||||
- (void)processMessage:(Message &&)msg handler:(void (^)(const Message &))messageHandler {
|
||||
if (unlikely(msg->action_type != ES_ACTION_TYPE_AUTH)) {
|
||||
// This is a programming error
|
||||
LOGE(@"Attempting to process non-AUTH message");
|
||||
[NSException raise:@"Attempt to process non-auth message"
|
||||
format:@"Unexpected event type received: %d", msg->event_type];
|
||||
}
|
||||
|
||||
dispatch_semaphore_t processingSema = dispatch_semaphore_create(0);
|
||||
// Add 1 to the processing semaphore. We're not creating it with a starting
|
||||
// value of 1 because that requires that the semaphore is not deallocated
|
||||
// until its value matches the starting value, which we don't need.
|
||||
dispatch_semaphore_signal(processingSema);
|
||||
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
|
||||
|
||||
const uint64_t timeout = NSEC_PER_MSEC * (self.deadlineMarginMS);
|
||||
uint64_t deadlineMachTime = msg->deadline - mach_absolute_time();
|
||||
uint64_t deadlineNano = deadlineMachTime * _timebase.numer / _timebase.denom;
|
||||
|
||||
// TODO(mlw): How should we handle `deadlineNano <= timeout`. Will currently
|
||||
// result in the deadline block being dispatched immediately (and therefore
|
||||
// the event will be denied).
|
||||
|
||||
// Workaround for compiler bug that doesn't properly close over variables
|
||||
// Note: On macOS 10.15 this will cause extra message copies.
|
||||
__block Message processMsg = msg;
|
||||
__block Message deadlineMsg = msg;
|
||||
|
||||
dispatch_after(
|
||||
dispatch_time(DISPATCH_TIME_NOW, deadlineNano - timeout), self->_authQueue, ^(void) {
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Handler has already responded, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
bool res = [self respondToMessage:deadlineMsg
|
||||
withAuthResult:ES_AUTH_RESULT_DENY
|
||||
cacheable:false];
|
||||
|
||||
LOGE(@"SNTEndpointSecurityClient: deadline reached: deny pid=%d, event type: %d ret=%d",
|
||||
audit_token_to_pid(deadlineMsg->process->audit_token), deadlineMsg->event_type, res);
|
||||
dispatch_semaphore_signal(deadlineExpiredSema);
|
||||
});
|
||||
|
||||
dispatch_async(self->_authQueue, ^{
|
||||
messageHandler(deadlineMsg);
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Deadline expired, wait for deadline block to finish.
|
||||
dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+ (bool)isDatabasePath:(const std::string_view)path {
|
||||
// TODO(mlw): These values should come from `SNTDatabaseController`. But right
|
||||
// now they live as NSStrings. We should make them `std::string_view` types
|
||||
// in order to use them here efficiently, but will need to make the
|
||||
// `SNTDatabaseController` an ObjC++ file.
|
||||
return (path == "/private/var/db/santa/rules.db" || path == "/private/var/db/santa/events.db");
|
||||
}
|
||||
|
||||
@end
|
||||
73
Source/santad/EventProviders/SNTEndpointSecurityClientBase.h
Normal file
73
Source/santad/EventProviders/SNTEndpointSecurityClientBase.h
Normal file
@@ -0,0 +1,73 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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 <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <bsm/libbsm.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
@protocol SNTEndpointSecurityClientBase
|
||||
|
||||
- (instancetype)initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi;
|
||||
|
||||
/// @note If this fails to establish a new ES client via `es_new_client`, an exception is raised
|
||||
/// that should terminate the program.
|
||||
- (void)establishClientOrDie;
|
||||
|
||||
- (bool)subscribe:(const std::set<es_event_type_t> &)events;
|
||||
|
||||
/// Clears the ES cache after setting subscriptions.
|
||||
/// There's a gap between creating a client and subscribing to events. Creating
|
||||
/// the client triggers a cache flush automatically but any events that happen
|
||||
/// prior to subscribing could've been cached by another client. Clearing after
|
||||
/// subscribing mitigates this posibility.
|
||||
- (bool)subscribeAndClearCache:(const std::set<es_event_type_t> &)events;
|
||||
|
||||
/// Responds to the Message with the given auth result
|
||||
///
|
||||
/// @param Message The wrapped es_message_t being responded to
|
||||
/// @param result Either ES_AUTH_RESULT_ALLOW or ES_AUTH_RESULT_DENY
|
||||
/// @param cacheable true if ES should attempt to cache the result, otherwise false
|
||||
/// @return true if the response was successful, otherwise false
|
||||
- (bool)respondToMessage:(const santa::santad::event_providers::endpoint_security::Message &)msg
|
||||
withAuthResult:(es_auth_result_t)result
|
||||
cacheable:(bool)cacheable;
|
||||
|
||||
- (void)
|
||||
processEnrichedMessage:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage>)msg
|
||||
handler:
|
||||
(void (^)(std::shared_ptr<
|
||||
santa::santad::event_providers::endpoint_security::EnrichedMessage>))
|
||||
messageHandler;
|
||||
|
||||
- (void)processMessage:(santa::santad::event_providers::endpoint_security::Message &&)msg
|
||||
handler:
|
||||
(void (^)(const santa::santad::event_providers::endpoint_security::Message &))
|
||||
messageHandler;
|
||||
|
||||
- (bool)clearCache;
|
||||
|
||||
+ (bool)isDatabasePath:(const std::string_view)path;
|
||||
+ (bool)populateAuditTokenSelf:(audit_token_t *)tok;
|
||||
|
||||
@end
|
||||
395
Source/santad/EventProviders/SNTEndpointSecurityClientTest.mm
Normal file
395
Source/santad/EventProviders/SNTEndpointSecurityClientTest.mm
Normal file
@@ -0,0 +1,395 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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 <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFile;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedProcess;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
@interface SNTEndpointSecurityClient (Testing)
|
||||
- (void)establishClientOrDie;
|
||||
- (bool)muteSelf;
|
||||
- (NSString *)errorMessageForNewClientResult:(es_new_client_result_t)result;
|
||||
- (void)handleMessage:(Message &&)esMsg;
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg
|
||||
ignoringOtherESClients:(BOOL)ignoringOtherESClients;
|
||||
|
||||
@property int64_t deadlineMarginMS;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityClientTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityClientTest
|
||||
|
||||
- (void)testEstablishClientOrDie {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
EXPECT_CALL(*mockESApi, MuteProcess).WillOnce(testing::Return(true));
|
||||
|
||||
EXPECT_CALL(*mockESApi, NewClient)
|
||||
.WillOnce(testing::Return(Client()))
|
||||
.WillOnce(testing::Return(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS)));
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
// First time throws because mock triggers failed connection
|
||||
// Second time succeeds
|
||||
XCTAssertThrows([client establishClientOrDie]);
|
||||
XCTAssertNoThrow([client establishClientOrDie]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testErrorMessageForNewClientResult {
|
||||
std::map<es_new_client_result_t, std::string> resultMessagePairs{
|
||||
{ES_NEW_CLIENT_RESULT_SUCCESS, ""},
|
||||
{ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED, "Full-disk access not granted"},
|
||||
{ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED, "Not entitled"},
|
||||
{ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED, "Not running as root"},
|
||||
{ES_NEW_CLIENT_RESULT_ERR_INVALID_ARGUMENT, "Invalid argument"},
|
||||
{ES_NEW_CLIENT_RESULT_ERR_INTERNAL, "Internal error"},
|
||||
{ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS, "Too many simultaneous clients"},
|
||||
{(es_new_client_result_t)123, "Unknown error"},
|
||||
};
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:nullptr];
|
||||
|
||||
for (const auto &kv : resultMessagePairs) {
|
||||
NSString *message = [client errorMessageForNewClientResult:kv.first];
|
||||
XCTAssertEqual(0, strcmp([(message ?: @"") UTF8String], kv.second.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
es_message_t esMsg;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
{ XCTAssertThrows([client handleMessage:Message(mockESApi, &esMsg)]); }
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testHandleMessageWithClient {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_FORK, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
// Have subscribe fail the first time, meaning clear cache only called once.
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_ALLOW, true))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
// Is ES client, but don't ignore others == Should Handle
|
||||
esMsg.process->is_es_client = true;
|
||||
XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:NO]);
|
||||
|
||||
// Not ES client, but ignore others == Should Handle
|
||||
esMsg.process->is_es_client = false;
|
||||
XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
|
||||
|
||||
// Is ES client, don't ignore others, and non-AUTH == Don't Handle
|
||||
esMsg.process->is_es_client = true;
|
||||
XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
|
||||
|
||||
// Is ES client, don't ignore others, and AUTH == Respond and Don't Handle
|
||||
esMsg.process->is_es_client = true;
|
||||
esMsg.action_type = ES_ACTION_TYPE_AUTH;
|
||||
XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testPopulateAuditTokenSelf {
|
||||
audit_token_t myAuditToken;
|
||||
|
||||
[SNTEndpointSecurityClient populateAuditTokenSelf:&myAuditToken];
|
||||
|
||||
XCTAssertEqual(audit_token_to_pid(myAuditToken), getpid());
|
||||
XCTAssertNotEqual(audit_token_to_pidversion(myAuditToken), 0);
|
||||
}
|
||||
|
||||
- (void)testMuteSelf {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
EXPECT_CALL(*mockESApi, MuteProcess)
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(false));
|
||||
|
||||
XCTAssertTrue([client muteSelf]);
|
||||
XCTAssertFalse([client muteSelf]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testClearCache {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
// Test the underlying clear cache impl returning both true and false
|
||||
EXPECT_CALL(*mockESApi, ClearCache)
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(false));
|
||||
|
||||
XCTAssertTrue([client clearCache]);
|
||||
XCTAssertFalse([client clearCache]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testSubscribe {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
std::set<es_event_type_t> events = {
|
||||
ES_EVENT_TYPE_NOTIFY_CLOSE,
|
||||
ES_EVENT_TYPE_NOTIFY_EXIT,
|
||||
};
|
||||
|
||||
// Test the underlying subscribe impl returning both true and false
|
||||
EXPECT_CALL(*mockESApi, Subscribe(testing::_, events))
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(false));
|
||||
|
||||
XCTAssertTrue([client subscribe:events]);
|
||||
XCTAssertFalse([client subscribe:events]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testSubscribeAndClearCache {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
// Have subscribe fail the first time, meaning clear cache only called once.
|
||||
EXPECT_CALL(*mockESApi, ClearCache)
|
||||
.After(EXPECT_CALL(*mockESApi, Subscribe)
|
||||
.WillOnce(testing::Return(false))
|
||||
.WillOnce(testing::Return(true)))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
XCTAssertFalse([client subscribeAndClearCache:{}]);
|
||||
XCTAssertTrue([client subscribeAndClearCache:{}]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testRespondToMessageWithAuthResultCacheable {
|
||||
es_message_t esMsg;
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
es_auth_result_t result = ES_AUTH_RESULT_DENY;
|
||||
bool cacheable = true;
|
||||
|
||||
// Have subscribe fail the first time, meaning clear cache only called once.
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, result, cacheable))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertTrue([client respondToMessage:msg withAuthResult:result cacheable:cacheable]);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testProcessEnrichedMessageHandler {
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
// Note: In this test, `RetainMessage` isn't setup to return anything. This
|
||||
// means that the underlying `es_msg_` in the `Message` object is NULL, and
|
||||
// therefore no call to `ReleaseMessage` is ever made (hence no expectations).
|
||||
// Because we don't need to operate on the es_msg_, this simplifies the test.
|
||||
EXPECT_CALL(*mockESApi, RetainMessage);
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
es_message_t esMsg;
|
||||
auto enrichedMsg = std::make_shared<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &esMsg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
[client processEnrichedMessage:enrichedMsg
|
||||
handler:^(std::shared_ptr<EnrichedMessage> msg) {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Handler block not called within expected time window");
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testIsDatabasePath {
|
||||
XCTAssertTrue([SNTEndpointSecurityClient isDatabasePath:"/private/var/db/santa/rules.db"]);
|
||||
XCTAssertTrue([SNTEndpointSecurityClient isDatabasePath:"/private/var/db/santa/events.db"]);
|
||||
|
||||
XCTAssertFalse([SNTEndpointSecurityClient isDatabasePath:"/not/a/db/path"]);
|
||||
}
|
||||
|
||||
- (void)testProcessMessageHandlerBadEventType {
|
||||
es_file_t proc_file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&proc_file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
{
|
||||
XCTAssertThrows([client processMessage:Message(mockESApi, &esMsg)
|
||||
handler:^(const Message &msg){
|
||||
}]);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
// Note: This test triggers a leak warning on the mock object, however it is
|
||||
// benign. The dispatch block to handle deadline expiration in
|
||||
// `processMessage:handler:` will retain the mock object an extra time.
|
||||
// But since this test sets a long deadline in order to ensure the handler block
|
||||
// runs first, the deadline handler block will not have finished executing by
|
||||
// the time the test exits, making GMock think the object was leaked.
|
||||
- (void)testProcessMessageHandler {
|
||||
es_file_t proc_file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&proc_file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &proc, ActionType::Auth,
|
||||
45 * 1000); // Long deadline to not hit
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
{
|
||||
XCTAssertNoThrow([client processMessage:Message(mockESApi, &esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}]);
|
||||
}
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
"Handler block not called within expected time window");
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testProcessMessageHandlerWithDeadlineTimeout {
|
||||
// Set a es_message_t deadline of 750ms
|
||||
// Set a deadline leeway in the `SNTEndpointSecurityClient` of 500ms
|
||||
// Mock `RespondAuthResult` which is called from the deadline handler
|
||||
// Signal the semaphore from the mock
|
||||
// Wait a few seconds for the semaphore (should take ~250ms)
|
||||
//
|
||||
// Two semaphotes are used:
|
||||
// 1. deadlineSema - used to wait in the handler block until the deadline
|
||||
// block has a chance to execute
|
||||
// 2. controlSema - used to block control flow in the test until the
|
||||
// deadlineSema is signaled (or a timeout waiting on deadlineSema)
|
||||
es_file_t proc_file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&proc_file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &proc, ActionType::Auth,
|
||||
750); // 750ms timeout
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
dispatch_semaphore_t deadlineSema = dispatch_semaphore_create(0);
|
||||
dispatch_semaphore_t controlSema = dispatch_semaphore_create(0);
|
||||
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_DENY, false))
|
||||
.WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
// Signal deadlineSema to let the handler block continue execution
|
||||
dispatch_semaphore_signal(deadlineSema);
|
||||
return true;
|
||||
}));
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
client.deadlineMarginMS = 500;
|
||||
|
||||
{
|
||||
__block long result;
|
||||
XCTAssertNoThrow([client processMessage:Message(mockESApi, &esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
result = dispatch_semaphore_wait(
|
||||
deadlineSema,
|
||||
dispatch_time(DISPATCH_TIME_NOW, 4 * NSEC_PER_SEC));
|
||||
|
||||
// Once done waiting on deadlineSema, trigger controlSema to
|
||||
// continue test
|
||||
dispatch_semaphore_signal(controlSema);
|
||||
}]);
|
||||
|
||||
XCTAssertEqual(
|
||||
0, dispatch_semaphore_wait(controlSema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Control sema not signaled within expected time window");
|
||||
|
||||
XCTAssertEqual(result, 0);
|
||||
}
|
||||
|
||||
// Allow some time for the threads in `processMessage:handler:` to finish.
|
||||
// It isn't critical that they do, but if the dispatch blocks don't complete
|
||||
// we may get warnings from GMock about calls to ReleaseMessage after
|
||||
// verifying and clearing. Sleep a little bit here to reduce chances of
|
||||
// seeing the warning (but still possible)
|
||||
SleepMS(100);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2021-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -11,12 +11,16 @@
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <DiskArbitration/DiskArbitration.h>
|
||||
|
||||
#include <DiskArbitration/DiskArbitration.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -26,16 +30,18 @@ typedef void (^SNTDeviceBlockCallback)(SNTDeviceEvent *event);
|
||||
* Manages DiskArbitration and EndpointSecurity to monitor/block/remount USB
|
||||
* storage devices.
|
||||
*/
|
||||
@interface SNTDeviceManager : NSObject
|
||||
@interface SNTEndpointSecurityDeviceManager
|
||||
: SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
|
||||
|
||||
@property(nonatomic, readwrite) BOOL subscribed;
|
||||
@property(nonatomic, readwrite) BOOL blockUSBMount;
|
||||
@property(nonatomic, readwrite, nullable) NSArray<NSString *> *remountArgs;
|
||||
@property(nonatomic, nullable) SNTDeviceBlockCallback deviceBlockCallback;
|
||||
|
||||
- (instancetype)init;
|
||||
- (void)listen;
|
||||
- (BOOL)subscribed;
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
|
||||
authResultCache:(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache;
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2021-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -11,21 +11,39 @@
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import "Source/santad/EventProviders/SNTDeviceManager.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h"
|
||||
|
||||
#import <DiskArbitration/DiskArbitration.h>
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
#include <errno.h>
|
||||
#include <libproc.h>
|
||||
#include <sys/mount.h>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/santad/Logs/SNTEventLog.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager ()
|
||||
|
||||
- (void)logDiskAppeared:(NSDictionary *)props;
|
||||
- (void)logDiskDisappeared:(NSDictionary *)props;
|
||||
|
||||
@property DASessionRef diskArbSession;
|
||||
@property(nonatomic, readonly) dispatch_queue_t diskQueue;
|
||||
|
||||
@end
|
||||
|
||||
void diskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
|
||||
if (dissenter) {
|
||||
@@ -36,17 +54,18 @@ void diskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context
|
||||
IOReturn subSystemCode = err_get_sub(status);
|
||||
IOReturn errorCode = err_get_code(status);
|
||||
|
||||
LOGE(
|
||||
@"SNTDeviceManager: dissenter status codes: system: %d, subsystem: %d, err: %d; status: %s",
|
||||
systemCode, subSystemCode, errorCode, [statusString UTF8String]);
|
||||
LOGE(@"SNTEndpointSecurityDeviceManager: dissenter status codes: system: %d, subsystem: %d, "
|
||||
@"err: %d; status: %s",
|
||||
systemCode, subSystemCode, errorCode, [statusString UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
void diskAppearedCallback(DADiskRef disk, void *context) {
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
SNTEventLog *logger = [SNTEventLog logger];
|
||||
if (logger) [logger logDiskAppeared:props];
|
||||
SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context;
|
||||
|
||||
[dm logDiskAppeared:props];
|
||||
}
|
||||
|
||||
void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *context) {
|
||||
@@ -54,8 +73,9 @@ void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *conte
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
|
||||
if (props[@"DAVolumePath"]) {
|
||||
SNTEventLog *logger = [SNTEventLog logger];
|
||||
if (logger) [logger logDiskAppeared:props];
|
||||
SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context;
|
||||
|
||||
[dm logDiskAppeared:props];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +83,9 @@ void diskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
|
||||
SNTEventLog *logger = [SNTEventLog logger];
|
||||
if (logger) [logger logDiskDisappeared:props];
|
||||
SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context;
|
||||
|
||||
[dm logDiskDisappeared:props];
|
||||
}
|
||||
|
||||
NSArray<NSString *> *maskToMountArgs(long remountOpts) {
|
||||
@@ -101,126 +122,97 @@ long mountArgsToMask(NSArray<NSString *> *args) {
|
||||
else if ([arg isEqualToString:@"async"])
|
||||
flags |= MNT_ASYNC;
|
||||
else
|
||||
LOGE(@"SNTDeviceManager: unexpected mount arg: %@", arg);
|
||||
LOGE(@"SNTEndpointSecurityDeviceManager: unexpected mount arg: %@", arg);
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SNTDeviceManager ()
|
||||
@implementation SNTEndpointSecurityDeviceManager {
|
||||
std::shared_ptr<AuthResultCache> _authResultCache;
|
||||
std::shared_ptr<Logger> _logger;
|
||||
}
|
||||
|
||||
@property DASessionRef diskArbSession;
|
||||
@property(nonatomic, readonly) es_client_t *client;
|
||||
@property(nonatomic, readonly) dispatch_queue_t esAuthQueue;
|
||||
@property(nonatomic, readonly) dispatch_queue_t diskQueue;
|
||||
@end
|
||||
|
||||
@implementation SNTDeviceManager
|
||||
|
||||
- (instancetype)init API_AVAILABLE(macos(10.15)) {
|
||||
self = [super init];
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
logger:(std::shared_ptr<Logger>)logger
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache {
|
||||
self = [super initWithESAPI:std::move(esApi)];
|
||||
if (self) {
|
||||
_logger = logger;
|
||||
_authResultCache = authResultCache;
|
||||
_blockUSBMount = false;
|
||||
|
||||
_diskQueue = dispatch_queue_create("com.google.santad.disk_queue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_esAuthQueue =
|
||||
dispatch_queue_create("com.google.santa.daemon.es_device_auth", DISPATCH_QUEUE_CONCURRENT);
|
||||
_diskQueue = dispatch_queue_create("com.google.santa.daemon.disk_queue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_diskArbSession = DASessionCreate(NULL);
|
||||
DASessionSetDispatchQueue(_diskArbSession, _diskQueue);
|
||||
|
||||
if (@available(macos 10.15, *)) [self initES];
|
||||
[self establishClientOrDie];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initES API_AVAILABLE(macos(10.15)) {
|
||||
while (!self.client) {
|
||||
es_client_t *client = NULL;
|
||||
es_new_client_result_t ret = es_new_client(&client, ^(es_client_t *c, const es_message_t *m) {
|
||||
// Set timeout to 5 seconds before the ES deadline.
|
||||
[self handleESMessageWithTimeout:m
|
||||
withClient:c
|
||||
timeout:dispatch_time(m->deadline, NSEC_PER_SEC * -5)];
|
||||
});
|
||||
- (void)logDiskAppeared:(NSDictionary *)props {
|
||||
self->_logger->LogDiskAppeared(props);
|
||||
}
|
||||
|
||||
switch (ret) {
|
||||
case ES_NEW_CLIENT_RESULT_SUCCESS:
|
||||
LOGI(@"Connected to EndpointSecurity");
|
||||
_client = client;
|
||||
return;
|
||||
case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED:
|
||||
LOGE(@"Unable to create EndpointSecurity client, not full-disk access permitted");
|
||||
LOGE(@"Sleeping for 30s before restarting.");
|
||||
sleep(30);
|
||||
exit(ret);
|
||||
default:
|
||||
LOGE(@"Unable to create es client: %d. Sleeping for a minute.", ret);
|
||||
sleep(60);
|
||||
continue;
|
||||
}
|
||||
- (void)logDiskDisappeared:(NSDictionary *)props {
|
||||
self->_logger->LogDiskDisappeared(props);
|
||||
}
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg {
|
||||
if (!self.blockUSBMount) {
|
||||
// TODO: We should also unsubscribe from events when this isn't set, but
|
||||
// this is generally a low-volume event type.
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:false];
|
||||
return;
|
||||
}
|
||||
|
||||
if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_UNMOUNT) {
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
[self processMessage:std::move(esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
es_auth_result_t result = [self handleAuthMount:msg];
|
||||
[self respondToMessage:msg withAuthResult:result cacheable:false];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)listenES API_AVAILABLE(macos(10.15)) {
|
||||
while (!self.client)
|
||||
usleep(100000); // 100ms
|
||||
|
||||
es_event_type_t events[] = {
|
||||
ES_EVENT_TYPE_AUTH_MOUNT,
|
||||
ES_EVENT_TYPE_AUTH_REMOUNT,
|
||||
};
|
||||
|
||||
es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t));
|
||||
if (sret != ES_RETURN_SUCCESS)
|
||||
LOGE(@"SNTDeviceManager: unable to subscribe to auth mount events: %d", sret);
|
||||
}
|
||||
|
||||
- (void)listenDA {
|
||||
- (void)enable {
|
||||
DARegisterDiskAppearedCallback(_diskArbSession, NULL, diskAppearedCallback,
|
||||
(__bridge void *)self);
|
||||
DARegisterDiskDescriptionChangedCallback(_diskArbSession, NULL, NULL,
|
||||
diskDescriptionChangedCallback, (__bridge void *)self);
|
||||
DARegisterDiskDisappearedCallback(_diskArbSession, NULL, diskDisappearedCallback,
|
||||
(__bridge void *)self);
|
||||
|
||||
[super subscribeAndClearCache:{
|
||||
ES_EVENT_TYPE_AUTH_MOUNT,
|
||||
ES_EVENT_TYPE_AUTH_REMOUNT,
|
||||
ES_EVENT_TYPE_NOTIFY_UNMOUNT,
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)listen {
|
||||
[self listenDA];
|
||||
if (@available(macos 10.15, *)) [self listenES];
|
||||
self.subscribed = YES;
|
||||
}
|
||||
|
||||
- (void)handleAuthMount:(const es_message_t *)m
|
||||
withClient:(es_client_t *)c API_AVAILABLE(macos(10.15)) {
|
||||
if (!self.blockUSBMount) {
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
return;
|
||||
}
|
||||
|
||||
- (es_auth_result_t)handleAuthMount:(const Message &)m {
|
||||
struct statfs *eventStatFS;
|
||||
BOOL isRemount = NO;
|
||||
|
||||
switch (m->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_MOUNT: eventStatFS = m->event.mount.statfs; break;
|
||||
case ES_EVENT_TYPE_AUTH_REMOUNT:
|
||||
eventStatFS = m->event.remount.statfs;
|
||||
isRemount = YES;
|
||||
break;
|
||||
case ES_EVENT_TYPE_AUTH_REMOUNT: eventStatFS = m->event.remount.statfs; break;
|
||||
default:
|
||||
// This is a programming error
|
||||
LOGE(@"Unexpected Event Type passed to DeviceManager handleAuthMount: %d", m->event_type);
|
||||
// Fail closed.
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false);
|
||||
assert(0 && "SNTDeviceManager: unexpected event type");
|
||||
return;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
long mountMode = eventStatFS->f_flags;
|
||||
pid_t pid = audit_token_to_pid(m->process->audit_token);
|
||||
LOGD(@"SNTDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu",
|
||||
m->process->executable->path.data, pid, mountMode);
|
||||
LOGD(
|
||||
@"SNTEndpointSecurityDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu",
|
||||
m->process->executable->path.data, pid, mountMode);
|
||||
|
||||
DADiskRef disk = DADiskCreateFromBSDName(NULL, self.diskArbSession, eventStatFS->f_mntfromname);
|
||||
CFAutorelease(disk);
|
||||
@@ -232,12 +224,13 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue];
|
||||
NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey];
|
||||
BOOL isUSB = [protocol isEqualToString:@"USB"];
|
||||
BOOL isVirtual = [protocol isEqualToString: @"Virtual Interface"];
|
||||
BOOL isVirtual = [protocol isEqualToString:@"Virtual Interface"];
|
||||
|
||||
NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey];
|
||||
|
||||
// TODO: check kind and protocol for banned things (e.g. MTP).
|
||||
LOGD(@"SNTDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d isRemovable: %d "
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d "
|
||||
@"isRemovable: %d "
|
||||
@"isEjectable: %d",
|
||||
protocol, kind, isInternal, isRemovable, isEjectable);
|
||||
|
||||
@@ -245,8 +238,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
// also are okay with operations for devices that are non-removal as long as
|
||||
// they are NOT a USB device.
|
||||
if (isInternal || isVirtual || (!isRemovable && !isEjectable && !isUSB)) {
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
return;
|
||||
return ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
SNTDeviceEvent *event = [[SNTDeviceEvent alloc]
|
||||
@@ -259,17 +251,16 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
event.remountArgs = self.remountArgs;
|
||||
long remountOpts = mountArgsToMask(self.remountArgs);
|
||||
|
||||
LOGD(@"SNTDeviceManager: mountMode: %@", maskToMountArgs(mountMode));
|
||||
LOGD(@"SNTDeviceManager: remountOpts: %@", maskToMountArgs(remountOpts));
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: mountMode: %@", maskToMountArgs(mountMode));
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: remountOpts: %@", maskToMountArgs(remountOpts));
|
||||
|
||||
if ((mountMode & remountOpts) == remountOpts && !isRemount) {
|
||||
LOGD(@"SNTDeviceManager: Allowing as mount as flags match remountOpts");
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
return;
|
||||
if ((mountMode & remountOpts) == remountOpts && m->event_type != ES_EVENT_TYPE_AUTH_REMOUNT) {
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: Allowing as mount as flags match remountOpts");
|
||||
return ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
long newMode = mountMode | remountOpts;
|
||||
LOGI(@"SNTDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)",
|
||||
LOGI(@"SNTEndpointSecurityDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)",
|
||||
eventStatFS->f_mntfromname, eventStatFS->f_mntonname, mountMode, newMode);
|
||||
[self remount:disk mountMode:newMode];
|
||||
}
|
||||
@@ -278,7 +269,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
self.deviceBlockCallback(event);
|
||||
}
|
||||
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false);
|
||||
return ES_AUTH_RESULT_DENY;
|
||||
}
|
||||
|
||||
- (void)remount:(DADiskRef)disk mountMode:(long)remountMask {
|
||||
@@ -293,66 +284,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
free(argv);
|
||||
}
|
||||
|
||||
// handleESMessage handles an ES message synchronously. This will block all incoming ES events
|
||||
// until either we serve a response or we hit the auth deadline. Prefer [SNTDeviceManager
|
||||
// handleESMessageWithTimeout]
|
||||
// TODO(tnek): generalize this timeout handling logic so that EndpointSecurityManager can use it
|
||||
// too.
|
||||
- (void)handleESMessageWithTimeout:(const es_message_t *)m
|
||||
withClient:(es_client_t *)c
|
||||
timeout:(dispatch_time_t)timeout API_AVAILABLE(macos(10.15)) {
|
||||
// ES will kill our whole client if we don't meet the es_message auth deadline, so we try to
|
||||
// gracefully handle it with a deny-by-default in the worst-case before it can do that.
|
||||
// This isn't an issue for notify events, so we're in no rush for those.
|
||||
es_message_t *mc = es_copy_message(m);
|
||||
|
||||
dispatch_semaphore_t processingSema = dispatch_semaphore_create(0);
|
||||
// Add 1 to the processing semaphore. We're not creating it with a starting
|
||||
// value of 1 because that requires that the semaphore is not deallocated
|
||||
// until its value matches the starting value, which we don't need.
|
||||
dispatch_semaphore_signal(processingSema);
|
||||
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
|
||||
|
||||
if (mc->action_type == ES_ACTION_TYPE_AUTH) {
|
||||
dispatch_after(timeout, self.esAuthQueue, ^(void) {
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Handler already responded, nothing to do.
|
||||
return;
|
||||
}
|
||||
LOGE(@"SNTDeviceManager: deadline reached: deny pid=%d ret=%d",
|
||||
audit_token_to_pid(mc->process->audit_token),
|
||||
es_respond_auth_result(c, mc, ES_AUTH_RESULT_DENY, false));
|
||||
dispatch_semaphore_signal(deadlineExpiredSema);
|
||||
});
|
||||
}
|
||||
|
||||
dispatch_async(self.esAuthQueue, ^{
|
||||
[self handleESMessage:mc withClient:c];
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Deadline expired, wait for deadline block to finish.
|
||||
dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER);
|
||||
}
|
||||
es_free_message(mc);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)handleESMessage:(const es_message_t *)m
|
||||
withClient:(es_client_t *)c API_AVAILABLE(macos(10.15)) {
|
||||
switch (m->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_REMOUNT: {
|
||||
[[fallthrough]];
|
||||
}
|
||||
case ES_EVENT_TYPE_AUTH_MOUNT: {
|
||||
[self handleAuthMount:m withClient:c];
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
LOGE(@"SNTDeviceManager: unexpected event type: %d", m->event_type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,347 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <DiskArbitration/DiskArbitration.h>
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#import <bsm/libbsm.h>
|
||||
#import <dispatch/dispatch.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/mount.h>
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h"
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
class MockAuthResultCache : public AuthResultCache {
|
||||
public:
|
||||
using AuthResultCache::AuthResultCache;
|
||||
|
||||
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager (Testing)
|
||||
- (void)logDiskAppeared:(NSDictionary *)props;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManagerTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property MockDiskArbitration *mockDA;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityDeviceManagerTest
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator eventLogType]).andReturn(-1);
|
||||
|
||||
self.mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[self.mockDA reset];
|
||||
|
||||
fclose(stdout);
|
||||
}
|
||||
|
||||
- (void)triggerTestMountEvent:(es_event_type_t)eventType
|
||||
diskInfoOverrides:(NSDictionary *)diskInfo
|
||||
expectedAuthResult:(es_auth_result_t)expectedAuthResult
|
||||
deviceManagerSetup:(void (^)(SNTEndpointSecurityDeviceManager *))setupDMCallback {
|
||||
struct statfs fs = {0};
|
||||
NSString *test_mntfromname = @"/dev/disk2s1";
|
||||
NSString *test_mntonname = @"/Volumes/KATE'S 4G";
|
||||
|
||||
strncpy(fs.f_mntfromname, [test_mntfromname UTF8String], sizeof(fs.f_mntfromname));
|
||||
strncpy(fs.f_mntonname, [test_mntonname UTF8String], sizeof(fs.f_mntonname));
|
||||
|
||||
MockDADisk *disk = [[MockDADisk alloc] init];
|
||||
disk.diskDescription = @{
|
||||
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey : @"USB",
|
||||
(__bridge NSString *)kDADiskDescriptionMediaRemovableKey : @YES,
|
||||
@"DAVolumeMountable" : @YES,
|
||||
@"DAVolumePath" : test_mntonname,
|
||||
@"DADeviceModel" : @"Some device model",
|
||||
@"DADevicePath" : test_mntonname,
|
||||
@"DADeviceVendor" : @"Some vendor",
|
||||
@"DAAppearanceTime" : @0,
|
||||
@"DAMediaBSDName" : test_mntfromname,
|
||||
};
|
||||
|
||||
if (diskInfo != nil) {
|
||||
NSMutableDictionary *mergedDiskDescription = [disk.diskDescription mutableCopy];
|
||||
for (NSString *key in diskInfo) {
|
||||
mergedDiskDescription[key] = diskInfo[key];
|
||||
}
|
||||
disk.diskDescription = (NSDictionary *)mergedDiskDescription;
|
||||
}
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
|
||||
SNTEndpointSecurityDeviceManager *deviceManager =
|
||||
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
|
||||
logger:nullptr
|
||||
authResultCache:nullptr];
|
||||
|
||||
setupDMCallback(deviceManager);
|
||||
|
||||
// Stub the log method since a mock `Logger` object isn't used.
|
||||
id partialDeviceManager = OCMPartialMock(deviceManager);
|
||||
OCMStub([partialDeviceManager logDiskAppeared:OCMOCK_ANY]);
|
||||
|
||||
[self.mockDA insert:disk bsdName:test_mntfromname];
|
||||
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(eventType, &proc, ActionType::Auth, 6000);
|
||||
// Need a pointer to esMsg to capture in blocks below.
|
||||
es_message_t *heapESMsg = &esMsg;
|
||||
|
||||
__block int retainCount = 0;
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
EXPECT_CALL(*mockESApi, ReleaseMessage).WillRepeatedly(^{
|
||||
if (retainCount == 0) {
|
||||
XCTFail(@"Under retain!");
|
||||
}
|
||||
retainCount--;
|
||||
if (retainCount == 0) {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}
|
||||
});
|
||||
EXPECT_CALL(*mockESApi, RetainMessage).WillRepeatedly(^{
|
||||
retainCount++;
|
||||
return heapESMsg;
|
||||
});
|
||||
|
||||
if (eventType == ES_EVENT_TYPE_AUTH_MOUNT) {
|
||||
esMsg.event.mount.statfs = &fs;
|
||||
} else if (eventType == ES_EVENT_TYPE_AUTH_REMOUNT) {
|
||||
esMsg.event.remount.statfs = &fs;
|
||||
} else {
|
||||
// Programming error. Fail the test.
|
||||
XCTFail(@"Unhandled event type in test: %d", eventType);
|
||||
}
|
||||
|
||||
XCTestExpectation *mountExpectation =
|
||||
[self expectationWithDescription:@"Wait for response from ES"];
|
||||
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, expectedAuthResult, false))
|
||||
.WillOnce(testing::InvokeWithoutArgs(^bool {
|
||||
[mountExpectation fulfill];
|
||||
return true;
|
||||
}));
|
||||
|
||||
[deviceManager handleMessage:Message(mockESApi, &esMsg)];
|
||||
|
||||
[self waitForExpectations:@[ mountExpectation ] timeout:60.0];
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
"Failed waiting for message to be processed...");
|
||||
|
||||
[partialDeviceManager stopMocking];
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testUSBBlockDisabled {
|
||||
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil
|
||||
expectedAuthResult:ES_AUTH_RESULT_ALLOW
|
||||
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
|
||||
dm.blockUSBMount = NO;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testRemount {
|
||||
NSArray *wantRemountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:
|
||||
@"Wait for SNTEndpointSecurityDeviceManager's blockCallback to trigger"];
|
||||
|
||||
__block NSString *gotmntonname, *gotmntfromname;
|
||||
__block NSArray<NSString *> *gotRemountedArgs;
|
||||
|
||||
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil
|
||||
expectedAuthResult:ES_AUTH_RESULT_DENY
|
||||
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
|
||||
dm.blockUSBMount = YES;
|
||||
dm.remountArgs = wantRemountArgs;
|
||||
|
||||
dm.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
gotRemountedArgs = event.remountArgs;
|
||||
gotmntonname = event.mntonname;
|
||||
gotmntfromname = event.mntfromname;
|
||||
[expectation fulfill];
|
||||
};
|
||||
}];
|
||||
|
||||
XCTAssertEqual(self.mockDA.wasRemounted, YES);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertEqualObjects(gotRemountedArgs, wantRemountArgs);
|
||||
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
|
||||
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
|
||||
}
|
||||
|
||||
- (void)testBlockNoRemount {
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:
|
||||
@"Wait for SNTEndpointSecurityDeviceManager's blockCallback to trigger"];
|
||||
|
||||
__block NSString *gotmntonname, *gotmntfromname;
|
||||
__block NSArray<NSString *> *gotRemountedArgs;
|
||||
|
||||
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil
|
||||
expectedAuthResult:ES_AUTH_RESULT_DENY
|
||||
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
|
||||
dm.blockUSBMount = YES;
|
||||
|
||||
dm.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
gotRemountedArgs = event.remountArgs;
|
||||
gotmntonname = event.mntonname;
|
||||
gotmntfromname = event.mntfromname;
|
||||
[expectation fulfill];
|
||||
};
|
||||
}];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertNil(gotRemountedArgs);
|
||||
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
|
||||
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
|
||||
}
|
||||
|
||||
- (void)testEnsureRemountsCannotChangePerms {
|
||||
NSArray *wantRemountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:
|
||||
@"Wait for SNTEndpointSecurityDeviceManager's blockCallback to trigger"];
|
||||
|
||||
__block NSString *gotmntonname, *gotmntfromname;
|
||||
__block NSArray<NSString *> *gotRemountedArgs;
|
||||
|
||||
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil
|
||||
expectedAuthResult:ES_AUTH_RESULT_DENY
|
||||
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
|
||||
dm.blockUSBMount = YES;
|
||||
dm.remountArgs = wantRemountArgs;
|
||||
|
||||
dm.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
gotRemountedArgs = event.remountArgs;
|
||||
gotmntonname = event.mntonname;
|
||||
gotmntfromname = event.mntfromname;
|
||||
[expectation fulfill];
|
||||
};
|
||||
}];
|
||||
|
||||
XCTAssertEqual(self.mockDA.wasRemounted, YES);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:10.0];
|
||||
|
||||
XCTAssertEqualObjects(gotRemountedArgs, wantRemountArgs);
|
||||
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
|
||||
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
|
||||
}
|
||||
|
||||
- (void)testEnsureDMGsDoNotPrompt {
|
||||
NSArray *wantRemountArgs = @[ @"noexec", @"rdonly" ];
|
||||
NSDictionary *diskInfo = @{
|
||||
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey : @"Virtual Interface",
|
||||
(__bridge NSString *)kDADiskDescriptionDeviceModelKey : @"Disk Image",
|
||||
(__bridge NSString *)kDADiskDescriptionMediaNameKey : @"disk image",
|
||||
};
|
||||
|
||||
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:diskInfo
|
||||
expectedAuthResult:ES_AUTH_RESULT_ALLOW
|
||||
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
|
||||
dm.blockUSBMount = YES;
|
||||
dm.remountArgs = wantRemountArgs;
|
||||
|
||||
dm.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
XCTFail(@"Should not be called");
|
||||
};
|
||||
}];
|
||||
|
||||
XCTAssertEqual(self.mockDA.wasRemounted, NO);
|
||||
}
|
||||
|
||||
- (void)testNotifyUnmountFlushesCache {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_UNMOUNT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
EXPECT_CALL(*mockAuthCache, FlushCache);
|
||||
|
||||
SNTEndpointSecurityDeviceManager *deviceManager =
|
||||
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
|
||||
logger:nullptr
|
||||
authResultCache:mockAuthCache];
|
||||
|
||||
deviceManager.blockUSBMount = YES;
|
||||
|
||||
[deviceManager handleMessage:Message(mockESApi, &esMsg)];
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
|
||||
}
|
||||
|
||||
- (void)testEnable {
|
||||
// Ensure the client subscribes to expected event types
|
||||
std::set<es_event_type_t> expectedEventSubs{
|
||||
ES_EVENT_TYPE_AUTH_MOUNT,
|
||||
ES_EVENT_TYPE_AUTH_REMOUNT,
|
||||
ES_EVENT_TYPE_NOTIFY_UNMOUNT,
|
||||
};
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
id deviceClient = [[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi];
|
||||
|
||||
EXPECT_CALL(*mockESApi, ClearCache(testing::_))
|
||||
.After(EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs))
|
||||
.WillOnce(testing::Return(true)))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
[deviceClient enable];
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,32 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
// Protocol that all subclasses of `SNTEndpointSecurityClient` should adhere to.
|
||||
@protocol SNTEndpointSecurityEventHandler <NSObject>
|
||||
|
||||
// Called Synchronously and serially for each message provided by the
|
||||
// EndpointSecurity framework.
|
||||
- (void)handleMessage:(santa::santad::event_providers::endpoint_security::Message &&)esMsg;
|
||||
|
||||
// Called after Santa has finished initializing itself.
|
||||
// This is an optimal place to subscribe to ES events
|
||||
- (void)enable;
|
||||
|
||||
@end
|
||||
@@ -1,37 +0,0 @@
|
||||
/// Copyright 2019 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
#include "Source/santad/EventProviders/SNTEventProvider.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
@interface SNTEndpointSecurityManager : NSObject <SNTEventProvider>
|
||||
- (santa_vnode_id_t)vnodeIDForFile:(es_file_t *)file;
|
||||
|
||||
- (BOOL)isCompilerPID:(pid_t)pid;
|
||||
- (void)setIsCompilerPID:(pid_t)pid;
|
||||
- (void)setNotCompilerPID:(pid_t)pid;
|
||||
|
||||
// Returns YES if the path was truncated.
|
||||
// The populated buffer will be NUL terminated.
|
||||
+ (BOOL)populateBufferFromESFile:(es_file_t *)file buffer:(char *)buffer size:(size_t)size;
|
||||
|
||||
@property(nonatomic, copy) void (^decisionCallback)(santa_message_t);
|
||||
@property(nonatomic, copy) void (^logCallback)(santa_message_t);
|
||||
@property(readonly, nonatomic) es_client_t *client;
|
||||
|
||||
@end
|
||||
@@ -1,645 +0,0 @@
|
||||
/// Copyright 2019 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityManager.h"
|
||||
|
||||
#include "Source/common/SNTPrefixTree.h"
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SantaCache.h"
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
#include <libproc.h>
|
||||
#include <atomic>
|
||||
|
||||
// Gleaned from https://opensource.apple.com/source/xnu/xnu-4903.241.1/bsd/sys/proc_internal.h
|
||||
static const pid_t PID_MAX = 99999;
|
||||
|
||||
@interface SNTEndpointSecurityManager () {
|
||||
std::atomic<bool> _compilerPIDs[PID_MAX];
|
||||
}
|
||||
|
||||
@property(nonatomic) SNTPrefixTree *prefixTree;
|
||||
@property(nonatomic, readonly) dispatch_queue_t esAuthQueue;
|
||||
@property(nonatomic, readonly) dispatch_queue_t esNotifyQueue;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityManager
|
||||
|
||||
- (instancetype)init API_AVAILABLE(macos(10.15)) {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
// To avoid nil deref from es_events arriving before listenForDecisionRequests or
|
||||
// listenForLogRequests in the MockEndpointSecurity testing util.
|
||||
_decisionCallback = ^(santa_message_t) {};
|
||||
_logCallback = ^(santa_message_t) {};
|
||||
[self establishClient];
|
||||
[self muteSelf];
|
||||
_prefixTree = new SNTPrefixTree();
|
||||
_esAuthQueue =
|
||||
dispatch_queue_create("com.google.santa.daemon.es_auth", DISPATCH_QUEUE_CONCURRENT);
|
||||
dispatch_set_target_queue(_esAuthQueue,
|
||||
dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
_esNotifyQueue =
|
||||
dispatch_queue_create("com.google.santa.daemon.es_notify", DISPATCH_QUEUE_CONCURRENT);
|
||||
dispatch_set_target_queue(_esNotifyQueue, dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0));
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc API_AVAILABLE(macos(10.15)) {
|
||||
if (_client) {
|
||||
es_unsubscribe_all(_client);
|
||||
es_delete_client(_client);
|
||||
}
|
||||
if (_prefixTree) delete _prefixTree;
|
||||
}
|
||||
|
||||
- (void)muteSelf {
|
||||
audit_token_t myAuditToken;
|
||||
mach_msg_type_number_t count = TASK_AUDIT_TOKEN_COUNT;
|
||||
if (task_info(mach_task_self(), TASK_AUDIT_TOKEN, (task_info_t)&myAuditToken, &count) ==
|
||||
KERN_SUCCESS) {
|
||||
if (es_mute_process(self.client, &myAuditToken) == ES_RETURN_SUCCESS) {
|
||||
return;
|
||||
} else {
|
||||
LOGE(@"Failed to mute this client's process, its events will not be muted.");
|
||||
}
|
||||
} else {
|
||||
LOGE(@"Failed to fetch this client's audit token. Its events will not be muted.");
|
||||
}
|
||||
|
||||
// If we get here, Santa was unable to mute itself. Assume transitory and bail.
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
- (void)establishClient API_AVAILABLE(macos(10.15)) {
|
||||
while (!self.client) {
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
es_client_t *client = NULL;
|
||||
es_new_client_result_t ret = es_new_client(&client, ^(es_client_t *c, const es_message_t *m) {
|
||||
pid_t pid = audit_token_to_pid(m->process->audit_token);
|
||||
int pidversion = audit_token_to_pidversion(m->process->audit_token);
|
||||
|
||||
// If enabled, skip any action generated from another endpoint security client.
|
||||
if (m->process->is_es_client && config.ignoreOtherEndpointSecurityClients) {
|
||||
if (m->action_type == ES_ACTION_TYPE_AUTH) {
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform the following checks on this serial queue.
|
||||
// Some checks are simple filters that avoid copying m.
|
||||
// However, the bulk of the work done here is to support transitive whitelisting.
|
||||
switch (m->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC: {
|
||||
// Deny results are currently logged when ES_EVENT_TYPE_AUTH_EXEC posts a deny.
|
||||
// TODO(bur/rah): For ES log denies from NOTIFY messages instead of AUTH.
|
||||
if (m->action.notify.result.auth == ES_AUTH_RESULT_DENY) return;
|
||||
|
||||
// Continue log this event
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: {
|
||||
// Ignore unmodified files
|
||||
if (!m->event.close.modified) return;
|
||||
|
||||
// Remove from decision cache in case this is invalidating a cached binary.
|
||||
[self removeCacheEntryForVnodeID:[self vnodeIDForFile:m->event.close.target]];
|
||||
|
||||
// Create a transitive rule if the file was modified by a running compiler
|
||||
if ([self isCompilerPID:pid]) {
|
||||
santa_message_t sm = {};
|
||||
BOOL truncated =
|
||||
[SNTEndpointSecurityManager populateBufferFromESFile:m->event.close.target
|
||||
buffer:sm.path
|
||||
size:sizeof(sm.path)];
|
||||
if (truncated) {
|
||||
LOGE(@"CLOSE: error creating transitive rule, the path is truncated: path=%s pid=%d",
|
||||
sm.path, pid);
|
||||
break;
|
||||
}
|
||||
if ([@(sm.path) hasPrefix:@"/dev/"]) {
|
||||
break;
|
||||
}
|
||||
sm.action = ACTION_NOTIFY_WHITELIST;
|
||||
sm.pid = pid;
|
||||
sm.pidversion = pidversion;
|
||||
LOGI(@"CLOSE: creating a transitive rule: path=%s pid=%d", sm.path, sm.pid);
|
||||
self.decisionCallback(sm);
|
||||
}
|
||||
|
||||
// Continue log this event
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: {
|
||||
// Create a transitive rule if the file was renamed by a running compiler
|
||||
if ([self isCompilerPID:pid]) {
|
||||
santa_message_t sm = {};
|
||||
BOOL truncated = [self populateRenamedNewPathFromESMessage:m->event.rename
|
||||
buffer:sm.path
|
||||
size:sizeof(sm.path)];
|
||||
if (truncated) {
|
||||
LOGE(@"RENAME: error creating transitive rule, the path is truncated: path=%s pid=%d",
|
||||
sm.path, pid);
|
||||
break;
|
||||
}
|
||||
if ([@(sm.path) hasPrefix:@"/dev/"]) {
|
||||
break;
|
||||
}
|
||||
sm.action = ACTION_NOTIFY_WHITELIST;
|
||||
sm.pid = pid;
|
||||
sm.pidversion = pidversion;
|
||||
LOGI(@"RENAME: creating a transitive rule: path=%s pid=%d", sm.path, sm.pid);
|
||||
self.decisionCallback(sm);
|
||||
}
|
||||
|
||||
// Continue log this event
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT: {
|
||||
// Update the set of running compiler PIDs
|
||||
[self setNotCompilerPID:pid];
|
||||
|
||||
// Skip the standard pipeline and just log.
|
||||
if (![config enableForkAndExitLogging]) return;
|
||||
santa_message_t sm = {};
|
||||
sm.action = ACTION_NOTIFY_EXIT;
|
||||
sm.pid = pid;
|
||||
sm.pidversion = pidversion;
|
||||
sm.ppid = m->process->original_ppid;
|
||||
audit_token_t at = m->process->audit_token;
|
||||
sm.uid = audit_token_to_ruid(at);
|
||||
sm.gid = audit_token_to_rgid(at);
|
||||
dispatch_async(self.esNotifyQueue, ^{
|
||||
self.logCallback(sm);
|
||||
});
|
||||
return;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_UNMOUNT: {
|
||||
// Flush the non-root cache - the root disk cannot be unmounted
|
||||
// so it isn't necessary to flush its cache.
|
||||
//
|
||||
// Flushing the cache calls back into ES. We need to perform this off the handler thread
|
||||
// otherwise we could potentially deadlock.
|
||||
dispatch_async(self.esAuthQueue, ^() {
|
||||
[self flushCacheNonRootOnly:YES];
|
||||
});
|
||||
|
||||
// Skip all other processing
|
||||
return;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK: {
|
||||
// Skip the standard pipeline and just log.
|
||||
if (![config enableForkAndExitLogging]) return;
|
||||
santa_message_t sm = {};
|
||||
sm.action = ACTION_NOTIFY_FORK;
|
||||
sm.ppid = m->event.fork.child->original_ppid;
|
||||
audit_token_t at = m->event.fork.child->audit_token;
|
||||
sm.pid = audit_token_to_pid(at);
|
||||
sm.pidversion = audit_token_to_pidversion(at);
|
||||
sm.uid = audit_token_to_ruid(at);
|
||||
sm.gid = audit_token_to_rgid(at);
|
||||
dispatch_async(self.esNotifyQueue, ^{
|
||||
self.logCallback(sm);
|
||||
});
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (m->action_type) {
|
||||
case ES_ACTION_TYPE_AUTH: {
|
||||
// Copy the message
|
||||
es_message_t *mc = es_copy_message(m);
|
||||
|
||||
dispatch_semaphore_t processingSema = dispatch_semaphore_create(0);
|
||||
// Add 1 to the processing semaphore. We're not creating it with a starting
|
||||
// value of 1 because that requires that the semaphore is not deallocated
|
||||
// until its value matches the starting value, which we don't need.
|
||||
dispatch_semaphore_signal(processingSema);
|
||||
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
|
||||
|
||||
// Create a timer to deny the execution 5 seconds before the deadline,
|
||||
// if a response hasn't already been sent. This block will still be enqueued if
|
||||
// the the deadline - 5 secs is < DISPATCH_TIME_NOW.
|
||||
// As of 10.15.5, a typical deadline is 60 seconds.
|
||||
dispatch_after(dispatch_time(m->deadline, NSEC_PER_SEC * -5), self.esAuthQueue, ^(void) {
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Handler has already responded, nothing to do.
|
||||
return;
|
||||
}
|
||||
LOGE(@"SNTEndpointSecurityManager: deadline reached: deny pid=%d ret=%d", pid,
|
||||
es_respond_auth_result(self.client, mc, ES_AUTH_RESULT_DENY, false));
|
||||
dispatch_semaphore_signal(deadlineExpiredSema);
|
||||
});
|
||||
|
||||
// Dispatch off to the handler and return control to ES.
|
||||
dispatch_async(self.esAuthQueue, ^{
|
||||
[self messageHandler:mc];
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Deadline expired, wait for deadline block to finish.
|
||||
dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER);
|
||||
}
|
||||
es_free_message(mc);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ES_ACTION_TYPE_NOTIFY: {
|
||||
// Copy the message and return control back to ES
|
||||
es_message_t *mc = es_copy_message(m);
|
||||
dispatch_async(self.esNotifyQueue, ^{
|
||||
[self messageHandler:mc];
|
||||
es_free_message(mc);
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
switch (ret) {
|
||||
case ES_NEW_CLIENT_RESULT_SUCCESS:
|
||||
LOGI(@"Connected to EndpointSecurity");
|
||||
_client = client;
|
||||
return;
|
||||
case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED:
|
||||
LOGE(@"Unable to create EndpointSecurity client, not full-disk access permitted");
|
||||
LOGE(@"Sleeping for 30s before restarting.");
|
||||
sleep(30);
|
||||
exit(ret);
|
||||
default:
|
||||
LOGE(@"Unable to create es client: %d. Sleeping for a minute.", ret);
|
||||
sleep(60);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)respondFromCache:(es_message_t *)m API_AVAILABLE(macos(10.15)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)messageHandler:(es_message_t *)m API_AVAILABLE(macos(10.15)) {
|
||||
santa_message_t sm = {};
|
||||
sm.es_message = (void *)m;
|
||||
|
||||
es_process_t *targetProcess = NULL;
|
||||
es_file_t *targetFile = NULL;
|
||||
void (^callback)(santa_message_t);
|
||||
|
||||
switch (m->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_EXEC: {
|
||||
if ([self respondFromCache:m]) {
|
||||
return;
|
||||
}
|
||||
|
||||
sm.action = ACTION_REQUEST_BINARY;
|
||||
targetFile = m->event.exec.target->executable;
|
||||
targetProcess = m->event.exec.target;
|
||||
callback = self.decisionCallback;
|
||||
|
||||
[SNTEndpointSecurityManager populateBufferFromESFile:m->process->tty
|
||||
buffer:sm.ttypath
|
||||
size:sizeof(sm.ttypath)];
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC: {
|
||||
sm.action = ACTION_NOTIFY_EXEC;
|
||||
targetFile = m->event.exec.target->executable;
|
||||
targetProcess = m->event.exec.target;
|
||||
|
||||
// TODO(rah): Profile this, it might need to be improved.
|
||||
uint32_t argCount = es_exec_arg_count(&(m->event.exec));
|
||||
NSMutableArray *args = [NSMutableArray arrayWithCapacity:argCount];
|
||||
for (int i = 0; i < argCount; ++i) {
|
||||
es_string_token_t arg = es_exec_arg(&(m->event.exec), i);
|
||||
NSString *argStr = [[NSString alloc] initWithBytes:arg.data
|
||||
length:arg.length
|
||||
encoding:NSUTF8StringEncoding];
|
||||
if (argStr.length) [args addObject:argStr];
|
||||
}
|
||||
sm.args_array = (void *)CFBridgingRetain(args);
|
||||
callback = self.logCallback;
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_AUTH_UNLINK: {
|
||||
es_string_token_t pathToken = m->event.unlink.target->path;
|
||||
NSString *path = [[NSString alloc] initWithBytes:pathToken.data
|
||||
length:pathToken.length
|
||||
encoding:NSUTF8StringEncoding];
|
||||
if ([self isDatabasePath:path]) {
|
||||
LOGW(@"Preventing attempt to delete Santa databases!");
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true);
|
||||
return;
|
||||
}
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
|
||||
return;
|
||||
}
|
||||
case ES_EVENT_TYPE_AUTH_RENAME: {
|
||||
es_string_token_t pathToken = m->event.rename.source->path;
|
||||
NSString *path = [[NSString alloc] initWithBytes:pathToken.data
|
||||
length:pathToken.length
|
||||
encoding:NSUTF8StringEncoding];
|
||||
|
||||
if ([self isDatabasePath:path]) {
|
||||
LOGW(@"Preventing attempt to rename Santa databases!");
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true);
|
||||
return;
|
||||
}
|
||||
if (m->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
|
||||
es_string_token_t destToken = m->event.rename.destination.existing_file->path;
|
||||
NSString *destPath = [[NSString alloc] initWithBytes:destToken.data
|
||||
length:destToken.length
|
||||
encoding:NSUTF8StringEncoding];
|
||||
if ([self isDatabasePath:destPath]) {
|
||||
LOGW(@"Preventing attempt to overwrite Santa databases!");
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
|
||||
return;
|
||||
}
|
||||
case ES_EVENT_TYPE_AUTH_KEXTLOAD: {
|
||||
es_string_token_t identifier = m->event.kextload.identifier;
|
||||
NSString *ident = [[NSString alloc] initWithBytes:identifier.data
|
||||
length:identifier.length
|
||||
encoding:NSUTF8StringEncoding];
|
||||
if ([ident isEqualToString:@"com.google.santa-driver"]) {
|
||||
LOGW(@"Preventing attempt to load Santa kext!");
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true);
|
||||
return;
|
||||
}
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
|
||||
return;
|
||||
}
|
||||
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: {
|
||||
sm.action = ACTION_NOTIFY_WRITE;
|
||||
targetFile = m->event.close.target;
|
||||
targetProcess = m->process;
|
||||
callback = self.logCallback;
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK: {
|
||||
sm.action = ACTION_NOTIFY_DELETE;
|
||||
targetFile = m->event.unlink.target;
|
||||
targetProcess = m->process;
|
||||
callback = self.logCallback;
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK: {
|
||||
sm.action = ACTION_NOTIFY_LINK;
|
||||
targetFile = m->event.link.source;
|
||||
targetProcess = m->process;
|
||||
NSString *p = @(m->event.link.target_dir->path.data);
|
||||
p = [p stringByAppendingPathComponent:@(m->event.link.target_filename.data)];
|
||||
[SNTEndpointSecurityManager populateBufferFromString:p.UTF8String
|
||||
buffer:sm.newpath
|
||||
size:sizeof(sm.newpath)];
|
||||
callback = self.logCallback;
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: {
|
||||
sm.action = ACTION_NOTIFY_RENAME;
|
||||
targetFile = m->event.rename.source;
|
||||
targetProcess = m->process;
|
||||
[self populateRenamedNewPathFromESMessage:m->event.rename
|
||||
buffer:sm.newpath
|
||||
size:sizeof(sm.newpath)];
|
||||
callback = self.logCallback;
|
||||
break;
|
||||
}
|
||||
default: LOGE(@"Unknown es message: %d", m->event_type); return;
|
||||
}
|
||||
|
||||
// Deny auth exec events if the path doesn't fit in the santa message.
|
||||
// TODO(bur/rah): Add support for larger paths.
|
||||
if ([SNTEndpointSecurityManager populateBufferFromESFile:targetFile
|
||||
buffer:sm.path
|
||||
size:sizeof(sm.path)] &&
|
||||
m->event_type == ES_EVENT_TYPE_AUTH_EXEC) {
|
||||
LOGE(@"path is truncated, deny: %s", sm.path);
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter file op events matching the prefix tree.
|
||||
if (!(m->event_type == ES_EVENT_TYPE_AUTH_EXEC || m->event_type == ES_EVENT_TYPE_NOTIFY_EXEC) &&
|
||||
self.prefixTree->HasPrefix(sm.path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sm.vnode_id.fsid = targetFile->stat.st_dev;
|
||||
sm.vnode_id.fileid = targetFile->stat.st_ino;
|
||||
sm.uid = audit_token_to_ruid(targetProcess->audit_token);
|
||||
sm.gid = audit_token_to_rgid(targetProcess->audit_token);
|
||||
sm.pid = audit_token_to_pid(targetProcess->audit_token);
|
||||
sm.pidversion = audit_token_to_pidversion(targetProcess->audit_token);
|
||||
sm.ppid = targetProcess->original_ppid;
|
||||
proc_name((m->event_type == ES_EVENT_TYPE_AUTH_EXEC) ? sm.ppid : sm.pid, sm.pname, 1024);
|
||||
callback(sm);
|
||||
if (sm.args_array) {
|
||||
CFBridgingRelease(sm.args_array);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)listenForDecisionRequests:(void (^)(santa_message_t))callback API_AVAILABLE(macos(10.15)) {
|
||||
while (!self.connectionEstablished)
|
||||
usleep(100000); // 100ms
|
||||
|
||||
self.decisionCallback = callback;
|
||||
es_event_type_t events[] = {
|
||||
ES_EVENT_TYPE_AUTH_EXEC,
|
||||
ES_EVENT_TYPE_AUTH_UNLINK,
|
||||
ES_EVENT_TYPE_AUTH_RENAME,
|
||||
ES_EVENT_TYPE_AUTH_KEXTLOAD,
|
||||
|
||||
// This is in the decision callback because it's used for detecting
|
||||
// the exit of a 'compiler' used by transitive whitelisting.
|
||||
ES_EVENT_TYPE_NOTIFY_EXIT,
|
||||
|
||||
// This is in the decision callback because it's used for clearing the
|
||||
// caches when a disk is unmounted.
|
||||
ES_EVENT_TYPE_NOTIFY_UNMOUNT,
|
||||
};
|
||||
es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t));
|
||||
if (sret != ES_RETURN_SUCCESS) LOGE(@"Unable to subscribe to auth events: %d", sret);
|
||||
|
||||
// There's a gap between creating a client and subscribing to events. Creating the client
|
||||
// triggers a cache flush automatically but any events that happen in this gap could be allowed
|
||||
// and cached, so we force the cache to flush again.
|
||||
[self flushCacheNonRootOnly:NO];
|
||||
}
|
||||
|
||||
- (void)listenForLogRequests:(void (^)(santa_message_t))callback API_AVAILABLE(macos(10.15)) {
|
||||
while (!self.connectionEstablished)
|
||||
usleep(100000); // 100ms
|
||||
|
||||
self.logCallback = callback;
|
||||
es_event_type_t events[] = {
|
||||
ES_EVENT_TYPE_NOTIFY_EXEC, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_LINK,
|
||||
ES_EVENT_TYPE_NOTIFY_RENAME, ES_EVENT_TYPE_NOTIFY_UNLINK, ES_EVENT_TYPE_NOTIFY_FORK,
|
||||
};
|
||||
es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t));
|
||||
if (sret != ES_RETURN_SUCCESS) LOGE(@"Unable to subscribe to notify events: %d", sret);
|
||||
}
|
||||
|
||||
- (int)postAction:(santa_action_t)action
|
||||
forMessage:(santa_message_t)sm API_AVAILABLE(macos(10.15)) {
|
||||
es_respond_result_t ret;
|
||||
switch (action) {
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
[self setIsCompilerPID:sm.pid];
|
||||
|
||||
// Allow the exec, but don't cache the decision so subsequent execs of the compiler get
|
||||
// marked appropriately.
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW,
|
||||
false);
|
||||
break;
|
||||
case ACTION_RESPOND_ALLOW:
|
||||
case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE:
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW,
|
||||
true);
|
||||
break;
|
||||
case ACTION_RESPOND_DENY:
|
||||
case ACTION_RESPOND_TOOLONG:
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_DENY,
|
||||
false);
|
||||
break;
|
||||
case ACTION_RESPOND_ACK: return ES_RESPOND_RESULT_SUCCESS;
|
||||
default: ret = ES_RESPOND_RESULT_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly API_AVAILABLE(macos(10.15)) {
|
||||
if (!self.connectionEstablished) return YES; // if not connected, there's nothing to flush.
|
||||
return es_clear_cache(self.client) == ES_CLEAR_CACHE_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
- (void)fileModificationPrefixFilterAdd:(NSArray *)filters {
|
||||
for (NSString *filter in filters) {
|
||||
self.prefixTree->AddPrefix(filter.fileSystemRepresentation);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fileModificationPrefixFilterReset {
|
||||
self.prefixTree->Reset();
|
||||
}
|
||||
|
||||
- (NSArray<NSNumber *> *)cacheCounts {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSArray<NSNumber *> *)cacheBucketCount {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID {
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
|
||||
- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeId {
|
||||
return KERN_FAILURE;
|
||||
}
|
||||
|
||||
- (BOOL)connectionEstablished {
|
||||
return self.client != nil;
|
||||
}
|
||||
|
||||
#pragma mark helpers
|
||||
|
||||
// Returns YES if the path was truncated.
|
||||
// The populated buffer will be NUL terminated.
|
||||
+ (BOOL)populateBufferFromESFile:(es_file_t *)file buffer:(char *)buffer size:(size_t)size {
|
||||
if (file == NULL) return NO;
|
||||
return [SNTEndpointSecurityManager populateBufferFromString:file->path.data
|
||||
buffer:buffer
|
||||
size:size];
|
||||
}
|
||||
|
||||
// Returns YES if the path was truncated.
|
||||
// The populated buffer will be NUL terminated.
|
||||
+ (BOOL)populateBufferFromString:(const char *)string buffer:(char *)buffer size:(size_t)size {
|
||||
return strlcpy(buffer, string, size) >= size;
|
||||
}
|
||||
|
||||
- (BOOL)populateRenamedNewPathFromESMessage:(es_event_rename_t)mv
|
||||
buffer:(char *)buffer
|
||||
size:(size_t)size {
|
||||
BOOL truncated = NO;
|
||||
switch (mv.destination_type) {
|
||||
case ES_DESTINATION_TYPE_NEW_PATH: {
|
||||
NSString *p = @(mv.destination.new_path.dir->path.data);
|
||||
p = [p stringByAppendingPathComponent:@(mv.destination.new_path.filename.data)];
|
||||
truncated = [SNTEndpointSecurityManager populateBufferFromString:p.UTF8String
|
||||
buffer:buffer
|
||||
size:size];
|
||||
break;
|
||||
}
|
||||
case ES_DESTINATION_TYPE_EXISTING_FILE: {
|
||||
truncated = [SNTEndpointSecurityManager populateBufferFromESFile:mv.destination.existing_file
|
||||
buffer:buffer
|
||||
size:size];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return truncated;
|
||||
}
|
||||
|
||||
- (santa_vnode_id_t)vnodeIDForFile:(es_file_t *)file {
|
||||
return {
|
||||
.fsid = (uint64_t)file->stat.st_dev,
|
||||
.fileid = file->stat.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
- (BOOL)isDatabasePath:(NSString *)path {
|
||||
return [path isEqualToString:@"/private/var/db/santa/rules.db"] ||
|
||||
[path isEqualToString:@"/private/var/db/santa/events.db"];
|
||||
}
|
||||
|
||||
- (BOOL)isCompilerPID:(pid_t)pid {
|
||||
return (pid && pid < PID_MAX && self->_compilerPIDs[pid].load());
|
||||
}
|
||||
|
||||
- (void)setIsCompilerPID:(pid_t)pid {
|
||||
if (pid < 1) {
|
||||
LOGE(@"Unable to watch compiler pid=%d", pid);
|
||||
} else if (pid >= PID_MAX) {
|
||||
LOGE(@"Unable to watch compiler pid=%d >= PID_MAX(%d)", pid, PID_MAX);
|
||||
} else {
|
||||
self->_compilerPIDs[pid].store(true);
|
||||
LOGD(@"Watching compiler pid=%d", pid);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setNotCompilerPID:(pid_t)pid {
|
||||
if (pid && pid < PID_MAX) self->_compilerPIDs[pid].store(false);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,250 +0,0 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <XCTest/XCTest.h>
|
||||
#import <bsm/libbsm.h>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityManager.h"
|
||||
|
||||
// Must be imported last to overload libEndpointSecurity functions.
|
||||
#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h"
|
||||
|
||||
const NSString *const kEventsDBPath = @"/private/var/db/santa/events.db";
|
||||
const NSString *const kRulesDBPath = @"/private/var/db/santa/rules.db";
|
||||
const NSString *const kBenignPath = @"/some/other/path";
|
||||
|
||||
@interface SNTEndpointSecurityManagerTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityManagerTest
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
fclose(stdout);
|
||||
}
|
||||
|
||||
- (void)testDenyOnTimeout {
|
||||
// There should be two events: an early uncached DENY as the consequence for not
|
||||
// meeting the decision deadline and an actual cached decision from our message
|
||||
// handler.
|
||||
__block int wantNumResp = 2;
|
||||
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
|
||||
(void)snt; // Make it appear used for the sake of -Wunused-variable
|
||||
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"Wait for santa's Auth dispatch queue"];
|
||||
|
||||
__block NSMutableArray<ESResponse *> *events = [NSMutableArray array];
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK
|
||||
withCallback:^(ESResponse *r) {
|
||||
@synchronized(self) {
|
||||
[events addObject:r];
|
||||
}
|
||||
|
||||
if (events.count >= wantNumResp) {
|
||||
[expectation fulfill];
|
||||
}
|
||||
}];
|
||||
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(kEventsDBPath)};
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.binaryPath = @"somebinary";
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = ES_EVENT_TYPE_AUTH_UNLINK;
|
||||
m.message->event = (es_events_t){.unlink = {.target = &dbFile}};
|
||||
m.message->mach_time = 1234;
|
||||
m.message->deadline = 1234;
|
||||
}];
|
||||
|
||||
[mockES triggerHandler:m.message];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
for (ESResponse *resp in events) {
|
||||
XCTAssertEqual(
|
||||
resp.result, ES_AUTH_RESULT_DENY,
|
||||
@"Failed to automatically deny on timeout and also the malicious event afterwards");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testDeleteRulesDB {
|
||||
NSDictionary<const NSString *, NSNumber *> *testCases = @{
|
||||
kEventsDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
kRulesDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
kBenignPath : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
|
||||
};
|
||||
for (const NSString *testPath in testCases) {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
|
||||
(void)snt; // Make it appear used for the sake of -Wunused-variable
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(testPath)};
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.binaryPath = @"somebinary";
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = ES_EVENT_TYPE_AUTH_UNLINK;
|
||||
m.message->event = (es_events_t){.unlink = {.target = &dbFile}};
|
||||
}];
|
||||
|
||||
[mockES triggerHandler:m.message];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertEqual(got.result, [testCases objectForKey:testPath].intValue,
|
||||
@"Incorrect handling of delete of %@", testPath);
|
||||
XCTAssertTrue(got.shouldCache, @"Failed to cache deletion decision of %@", testPath);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testSkipOtherESEvents {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
|
||||
(void)snt; // Make it appear used for the sake of -Wunused-variable
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(@"/some/other/path")};
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.process->is_es_client = true;
|
||||
m.binaryPath = @"somebinary";
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = ES_EVENT_TYPE_AUTH_UNLINK;
|
||||
m.message->event = (es_events_t){.unlink = {.target = &dbFile}};
|
||||
}];
|
||||
|
||||
[mockES triggerHandler:m.message];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
|
||||
- (void)testRenameOverwriteRulesDB {
|
||||
NSDictionary<const NSString *, NSNumber *> *testCases = @{
|
||||
kEventsDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
kRulesDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
kBenignPath : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
|
||||
};
|
||||
for (const NSString *testPath in testCases) {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
|
||||
(void)snt; // Make it appear used for the sake of -Wunused-variable
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_RENAME
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
__block es_file_t otherFile = {.path = MakeStringToken(@"/some/other/path")};
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(testPath)};
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.binaryPath = @"somebinary";
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = ES_EVENT_TYPE_AUTH_RENAME;
|
||||
m.message->event = (es_events_t){
|
||||
.rename =
|
||||
{
|
||||
.source = &otherFile,
|
||||
.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE,
|
||||
.destination = {.existing_file = &dbFile},
|
||||
},
|
||||
};
|
||||
}];
|
||||
|
||||
[mockES triggerHandler:m.message];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertEqual(got.result, [testCases objectForKey:testPath].intValue,
|
||||
@"Incorrect handling of rename of %@", testPath);
|
||||
XCTAssertTrue(got.shouldCache, @"Failed to cache rename auth decision of %@", testPath);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testRenameRulesDB {
|
||||
NSDictionary<const NSString *, NSNumber *> *testCases = @{
|
||||
kEventsDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
kRulesDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
kBenignPath : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
|
||||
};
|
||||
|
||||
for (const NSString *testPath in testCases) {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
|
||||
(void)snt; // Make it appear used for the sake of -Wunused-variable
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_RENAME
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
__block es_file_t otherFile = {.path = MakeStringToken(@"/some/other/path")};
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(testPath)};
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.binaryPath = @"somebinary";
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = ES_EVENT_TYPE_AUTH_RENAME;
|
||||
m.message->event = (es_events_t){
|
||||
.rename =
|
||||
{
|
||||
.source = &dbFile,
|
||||
.destination_type = ES_DESTINATION_TYPE_NEW_PATH,
|
||||
.destination = {.new_path =
|
||||
{
|
||||
.dir = &otherFile,
|
||||
.filename = MakeStringToken(@"someotherfilename"),
|
||||
}},
|
||||
},
|
||||
};
|
||||
}];
|
||||
|
||||
[mockES triggerHandler:m.message];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertEqual(got.result, [testCases objectForKey:testPath].intValue,
|
||||
@"Incorrect handling of rename of %@", testPath);
|
||||
|
||||
XCTAssertTrue(got.shouldCache, @"Failed to cache rename auth decision of %@", testPath);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
40
Source/santad/EventProviders/SNTEndpointSecurityRecorder.h
Normal file
40
Source/santad/EventProviders/SNTEndpointSecurityRecorder.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTPrefixTree.h"
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
|
||||
/// ES Client focused on subscribing to NOTIFY event variants with the intention of enriching
|
||||
/// received messages and logging the information.
|
||||
@interface SNTEndpointSecurityRecorder : SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)
|
||||
esApi
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
|
||||
enricher:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:
|
||||
(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache
|
||||
prefixTree:(std::shared_ptr<SNTPrefixTree>)prefixTree;
|
||||
|
||||
@end
|
||||
119
Source/santad/EventProviders/SNTEndpointSecurityRecorder.mm
Normal file
119
Source/santad/EventProviders/SNTEndpointSecurityRecorder.mm
Normal file
@@ -0,0 +1,119 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
switch (msg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: return msg->event.close.target;
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK: return msg->event.link.source;
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: return msg->event.rename.source;
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK: return msg->event.unlink.target;
|
||||
default: return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
@interface SNTEndpointSecurityRecorder ()
|
||||
@property SNTCompilerController *compilerController;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityRecorder {
|
||||
std::shared_ptr<AuthResultCache> _authResultCache;
|
||||
std::shared_ptr<Enricher> _enricher;
|
||||
std::shared_ptr<Logger> _logger;
|
||||
std::shared_ptr<SNTPrefixTree> _prefixTree;
|
||||
}
|
||||
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
logger:(std::shared_ptr<Logger>)logger
|
||||
enricher:(std::shared_ptr<Enricher>)enricher
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache
|
||||
prefixTree:(std::shared_ptr<SNTPrefixTree>)prefixTree {
|
||||
self = [super initWithESAPI:std::move(esApi)];
|
||||
if (self) {
|
||||
_enricher = enricher;
|
||||
_logger = logger;
|
||||
_compilerController = compilerController;
|
||||
_authResultCache = authResultCache;
|
||||
_prefixTree = prefixTree;
|
||||
|
||||
[self establishClientOrDie];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg {
|
||||
// Pre-enrichment processing
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE:
|
||||
// TODO(mlw): Once we move to building with the macOS 13 SDK, we should also check
|
||||
// the `was_mapped_writable` field
|
||||
if (esMsg->event.close.modified == false) {
|
||||
// Ignore unmodified files
|
||||
return;
|
||||
}
|
||||
|
||||
self->_authResultCache->RemoveFromCache(esMsg->event.close.target);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
[self.compilerController handleEvent:esMsg withLogger:self->_logger];
|
||||
|
||||
// Filter file op events matching the prefix tree.
|
||||
es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg));
|
||||
if (targetFile != NULL && self->_prefixTree->HasPrefix(targetFile->path.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enrich the message inline with the ES handler block to capture enrichment
|
||||
// data as close to the source event as possible.
|
||||
std::shared_ptr<EnrichedMessage> sharedEnrichedMessage = _enricher->Enrich(std::move(esMsg));
|
||||
|
||||
// Asynchronously log the message
|
||||
[self processEnrichedMessage:std::move(sharedEnrichedMessage)
|
||||
handler:^(std::shared_ptr<EnrichedMessage> msg) {
|
||||
self->_logger->Log(std::move(msg));
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)enable {
|
||||
[super subscribe:{
|
||||
ES_EVENT_TYPE_NOTIFY_CLOSE,
|
||||
ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA,
|
||||
ES_EVENT_TYPE_NOTIFY_EXEC,
|
||||
ES_EVENT_TYPE_NOTIFY_FORK,
|
||||
ES_EVENT_TYPE_NOTIFY_EXIT,
|
||||
ES_EVENT_TYPE_NOTIFY_LINK,
|
||||
ES_EVENT_TYPE_NOTIFY_RENAME,
|
||||
ES_EVENT_TYPE_NOTIFY_UNLINK,
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
211
Source/santad/EventProviders/SNTEndpointSecurityRecorderTest.mm
Normal file
211
Source/santad/EventProviders/SNTEndpointSecurityRecorderTest.mm
Normal file
@@ -0,0 +1,211 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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 <EndpointSecurity/ESTypes.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstddef>
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
class MockEnricher : public Enricher {
|
||||
public:
|
||||
MOCK_METHOD(std::shared_ptr<EnrichedMessage>, Enrich, (Message &&));
|
||||
};
|
||||
|
||||
class MockAuthResultCache : public AuthResultCache {
|
||||
public:
|
||||
using AuthResultCache::AuthResultCache;
|
||||
|
||||
MOCK_METHOD(void, RemoveFromCache, (const es_file_t *));
|
||||
};
|
||||
|
||||
class MockLogger : public Logger {
|
||||
public:
|
||||
using Logger::Logger;
|
||||
|
||||
MOCK_METHOD(void, Log, (std::shared_ptr<EnrichedMessage>));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityRecorderTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityRecorderTest
|
||||
|
||||
- (void)testEnable {
|
||||
// Ensure the client subscribes to expected event types
|
||||
std::set<es_event_type_t> expectedEventSubs{
|
||||
ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA, ES_EVENT_TYPE_NOTIFY_EXEC,
|
||||
ES_EVENT_TYPE_NOTIFY_FORK, ES_EVENT_TYPE_NOTIFY_EXIT, ES_EVENT_TYPE_NOTIFY_LINK,
|
||||
ES_EVENT_TYPE_NOTIFY_RENAME, ES_EVENT_TYPE_NOTIFY_UNLINK,
|
||||
};
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
id recorderClient = [[SNTEndpointSecurityRecorder alloc] initWithESAPI:mockESApi];
|
||||
|
||||
EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs)).WillOnce(testing::Return(true));
|
||||
|
||||
[recorderClient enable];
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc, ActionType::Auth);
|
||||
es_file_t targetFile = MakeESFile("bar");
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
std::shared_ptr<EnrichedMessage> enrichedMsg = std::shared_ptr<EnrichedMessage>(nullptr);
|
||||
|
||||
auto mockEnricher = std::make_shared<MockEnricher>();
|
||||
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(enrichedMsg));
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFile)).Times(1);
|
||||
|
||||
// NOTE: Currently unable to create a partial mock of the
|
||||
// `SNTEndpointSecurityRecorder` object. There is a bug in OCMock that doesn't
|
||||
// properly handle the `processEnrichedMessage:handler:` block. Instead this
|
||||
// test will mock the `Log` method that is called in the handler block.
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
auto mockLogger = std::make_shared<MockLogger>(nullptr, nullptr);
|
||||
EXPECT_CALL(*mockLogger, Log).WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}));
|
||||
|
||||
auto prefixTree = std::make_shared<SNTPrefixTree>();
|
||||
|
||||
id mockCC = OCMStrictClassMock([SNTCompilerController class]);
|
||||
|
||||
SNTEndpointSecurityRecorder *recorderClient =
|
||||
[[SNTEndpointSecurityRecorder alloc] initWithESAPI:mockESApi
|
||||
logger:mockLogger
|
||||
enricher:mockEnricher
|
||||
compilerController:mockCC
|
||||
authResultCache:mockAuthCache
|
||||
prefixTree:prefixTree];
|
||||
|
||||
// CLOSE not modified, bail early
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg.event.close.modified = false;
|
||||
esMsg.event.close.target = NULL;
|
||||
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, &esMsg)]);
|
||||
}
|
||||
|
||||
// CLOSE modified, remove from cache
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg.event.close.modified = true;
|
||||
esMsg.event.close.target = &targetFile;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)];
|
||||
|
||||
XCTAssertEqual(
|
||||
0, dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Log wasn't called within expected time window");
|
||||
}
|
||||
|
||||
// LINK, Prefix match, bail early
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_LINK;
|
||||
esMsg.event.link.source = &targetFile;
|
||||
prefixTree->AddPrefix(esMsg.event.link.source->path.data);
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)];
|
||||
}
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockCC));
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockEnricher.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockLogger.get());
|
||||
|
||||
[mockCC stopMocking];
|
||||
}
|
||||
|
||||
- (void)testGetTargetFileForPrefixTree {
|
||||
// Ensure `GetTargetFileForPrefixTree` returns expected field for each
|
||||
// subscribed event type in the `SNTEndpointSecurityRecorder`.
|
||||
extern es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg);
|
||||
|
||||
es_file_t closeFile = MakeESFile("close");
|
||||
es_file_t linkFile = MakeESFile("link");
|
||||
es_file_t renameFile = MakeESFile("rename");
|
||||
es_file_t unlinkFile = MakeESFile("unlink");
|
||||
es_message_t esMsg;
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg.event.close.target = &closeFile;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &closeFile);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_LINK;
|
||||
esMsg.event.link.source = &linkFile;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &linkFile);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_RENAME;
|
||||
esMsg.event.rename.source = &renameFile;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &renameFile);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_UNLINK;
|
||||
esMsg.event.unlink.target = &unlinkFile;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &unlinkFile);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_FORK;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXIT;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,33 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
|
||||
/// ES Client focused on mitigating accidental or malicious tampering of Santa and its components.
|
||||
@interface SNTEndpointSecurityTamperResistance
|
||||
: SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,113 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#include <string.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-driver";
|
||||
|
||||
@implementation SNTEndpointSecurityTamperResistance {
|
||||
std::shared_ptr<Logger> _logger;
|
||||
}
|
||||
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
logger:(std::shared_ptr<Logger>)logger {
|
||||
self = [super initWithESAPI:std::move(esApi)];
|
||||
if (self) {
|
||||
_logger = logger;
|
||||
|
||||
[self establishClientOrDie];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg {
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_UNLINK: {
|
||||
if ([SNTEndpointSecurityTamperResistance
|
||||
isDatabasePath:esMsg->event.unlink.target->path.data]) {
|
||||
// Do not cache so that each attempt to remove santa is logged
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_DENY cacheable:false];
|
||||
LOGW(@"Preventing attempt to delete Santa databases!");
|
||||
} else {
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:true];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_RENAME: {
|
||||
if ([SNTEndpointSecurityTamperResistance
|
||||
isDatabasePath:esMsg->event.rename.source->path.data]) {
|
||||
// Do not cache so that each attempt to remove santa is logged
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_DENY cacheable:false];
|
||||
LOGW(@"Preventing attempt to rename Santa databases!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (esMsg->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
|
||||
if ([SNTEndpointSecurityTamperResistance
|
||||
isDatabasePath:esMsg->event.rename.destination.existing_file->path.data]) {
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_DENY cacheable:false];
|
||||
LOGW(@"Preventing attempt to overwrite Santa databases!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get to here, no more reasons to deny the event, so allow it
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:true];
|
||||
return;
|
||||
}
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_KEXTLOAD: {
|
||||
// TODO(mlw): Since we don't package the kext anymore, we should consider removing this.
|
||||
// TODO(mlw): Consider logging when kext loads are attempted.
|
||||
es_auth_result_t res = ES_AUTH_RESULT_ALLOW;
|
||||
if (strcmp(esMsg->event.kextload.identifier.data, kSantaKextIdentifier.data()) == 0) {
|
||||
LOGW(@"Preventing attempt to load Santa kext!");
|
||||
res = ES_AUTH_RESULT_DENY;
|
||||
}
|
||||
[self respondToMessage:esMsg withAuthResult:res cacheable:true];
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
// Unexpected event type, this is a programming error
|
||||
[NSException raise:@"Invalid event type"
|
||||
format:@"Invalid tamper resistance event type: %d", esMsg->event_type];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)enable {
|
||||
// TODO(mlw): For macOS 13, use new mute and invert APIs to limit the
|
||||
// messages sent for these events to the Santa-specific directories
|
||||
// checked in the `handleMessage:` method.
|
||||
|
||||
[super subscribeAndClearCache:{
|
||||
ES_EVENT_TYPE_AUTH_KEXTLOAD,
|
||||
ES_EVENT_TYPE_AUTH_UNLINK,
|
||||
ES_EVENT_TYPE_AUTH_RENAME,
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,190 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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 <EndpointSecurity/ESTypes.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
static constexpr std::string_view kEventsDBPath = "/private/var/db/santa/events.db";
|
||||
static constexpr std::string_view kRulesDBPath = "/private/var/db/santa/rules.db";
|
||||
static constexpr std::string_view kBenignPath = "/some/other/path";
|
||||
static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-driver";
|
||||
|
||||
@interface SNTEndpointSecurityTamperResistanceTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityTamperResistanceTest
|
||||
|
||||
- (void)testEnable {
|
||||
// Ensure the client subscribes to expected event types
|
||||
std::set<es_event_type_t> expectedEventSubs{
|
||||
ES_EVENT_TYPE_AUTH_KEXTLOAD,
|
||||
ES_EVENT_TYPE_AUTH_UNLINK,
|
||||
ES_EVENT_TYPE_AUTH_RENAME,
|
||||
};
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
EXPECT_CALL(*mockESApi, NewClient(testing::_))
|
||||
.WillOnce(testing::Return(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS)));
|
||||
EXPECT_CALL(*mockESApi, MuteProcess(testing::_, testing::_)).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, ClearCache(testing::_))
|
||||
.After(EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs))
|
||||
.WillOnce(testing::Return(true)))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
SNTEndpointSecurityTamperResistance *tamperClient =
|
||||
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi logger:nullptr];
|
||||
id mockTamperClient = OCMPartialMock(tamperClient);
|
||||
|
||||
[mockTamperClient enable];
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
[mockTamperClient stopMocking];
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
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);
|
||||
|
||||
es_file_t fileEventsDB = MakeESFile(kEventsDBPath.data());
|
||||
es_file_t fileRulesDB = MakeESFile(kRulesDBPath.data());
|
||||
es_file_t fileBenign = MakeESFile(kBenignPath.data());
|
||||
|
||||
es_string_token_t santaTok = MakeESStringToken(kSantaKextIdentifier.data());
|
||||
es_string_token_t benignTok = MakeESStringToken(kBenignPath.data());
|
||||
|
||||
std::map<es_file_t *, es_auth_result_t> pathToResult{
|
||||
{&fileEventsDB, ES_AUTH_RESULT_DENY},
|
||||
{&fileRulesDB, ES_AUTH_RESULT_DENY},
|
||||
{&fileBenign, ES_AUTH_RESULT_ALLOW},
|
||||
};
|
||||
|
||||
std::map<es_string_token_t *, es_auth_result_t> kextIdToResult{
|
||||
{&santaTok, ES_AUTH_RESULT_DENY},
|
||||
{&benignTok, ES_AUTH_RESULT_ALLOW},
|
||||
};
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
SNTEndpointSecurityTamperResistance *tamperClient =
|
||||
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi logger:nullptr];
|
||||
|
||||
id mockTamperClient = OCMPartialMock(tamperClient);
|
||||
|
||||
// Unable to use `OCMExpect` here because we cannot match on the `Message`
|
||||
// parameter. In order to verify the `AuthResult` and `Cacheable` parameters,
|
||||
// instead use `OCMStub` and extract the arguments in order to assert their
|
||||
// expected values.
|
||||
__block es_auth_result_t gotAuthResult;
|
||||
__block bool gotCachable;
|
||||
OCMStub([mockTamperClient respondToMessage:Message(mockESApi, &esMsg)
|
||||
withAuthResult:(es_auth_result_t)0
|
||||
cacheable:false])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(^(NSInvocation *inv) {
|
||||
[inv getArgument:&gotAuthResult atIndex:3];
|
||||
[inv getArgument:&gotCachable atIndex:4];
|
||||
});
|
||||
|
||||
// First check unhandled event types will crash
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertThrows([tamperClient handleMessage:Message(mockESApi, &esMsg)]);
|
||||
}
|
||||
|
||||
// Check UNLINK tamper events
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_UNLINK;
|
||||
for (const auto &kv : pathToResult) {
|
||||
Message msg(mockESApi, &esMsg);
|
||||
esMsg.event.unlink.target = kv.first;
|
||||
|
||||
[mockTamperClient handleMessage:std::move(msg)];
|
||||
|
||||
XCTAssertEqual(gotAuthResult, kv.second);
|
||||
XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
}
|
||||
|
||||
// Check RENAME `source` tamper events
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_RENAME;
|
||||
for (const auto &kv : pathToResult) {
|
||||
Message msg(mockESApi, &esMsg);
|
||||
esMsg.event.rename.source = kv.first;
|
||||
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
|
||||
|
||||
[mockTamperClient handleMessage:std::move(msg)];
|
||||
|
||||
XCTAssertEqual(gotAuthResult, kv.second);
|
||||
XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
}
|
||||
|
||||
// Check RENAME `dest` tamper events
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_RENAME;
|
||||
esMsg.event.rename.source = &fileBenign;
|
||||
for (const auto &kv : pathToResult) {
|
||||
Message msg(mockESApi, &esMsg);
|
||||
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
|
||||
esMsg.event.rename.destination.existing_file = kv.first;
|
||||
|
||||
[mockTamperClient handleMessage:std::move(msg)];
|
||||
|
||||
XCTAssertEqual(gotAuthResult, kv.second);
|
||||
XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
}
|
||||
|
||||
// Check KEXTLOAD tamper events
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_KEXTLOAD;
|
||||
|
||||
for (const auto &kv : kextIdToResult) {
|
||||
Message msg(mockESApi, &esMsg);
|
||||
esMsg.event.kextload.identifier = *kv.first;
|
||||
|
||||
[mockTamperClient handleMessage:std::move(msg)];
|
||||
|
||||
XCTAssertEqual(gotAuthResult, kv.second);
|
||||
XCTAssertEqual(gotCachable, true); // Note: Kext responses always cached
|
||||
}
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTAssertTrue(OCMVerifyAll(mockTamperClient));
|
||||
|
||||
[mockTamperClient stopMocking];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,33 +0,0 @@
|
||||
/// Copyright 2019 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
|
||||
@protocol SNTEventProvider <NSObject>
|
||||
|
||||
- (void)listenForDecisionRequests:(void (^)(santa_message_t message))callback;
|
||||
- (void)listenForLogRequests:(void (^)(santa_message_t message))callback;
|
||||
- (int)postAction:(santa_action_t)action forMessage:(santa_message_t)sm;
|
||||
- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly;
|
||||
- (void)fileModificationPrefixFilterAdd:(NSArray *)filters;
|
||||
- (void)fileModificationPrefixFilterReset;
|
||||
- (NSArray<NSNumber *> *)cacheCounts;
|
||||
- (NSArray<NSNumber *> *)cacheBucketCount;
|
||||
- (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID;
|
||||
- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeId;
|
||||
@property(readonly) BOOL connectionEstablished;
|
||||
|
||||
@end
|
||||
69
Source/santad/Logs/EndpointSecurity/Logger.h
Normal file
69
Source/santad/Logs/EndpointSecurity/Logger.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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__LOGS_ENDPOINTSECURITY_LOGGER_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_LOGGER_H
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h"
|
||||
|
||||
// Forward declarations
|
||||
@class SNTStoredEvent;
|
||||
namespace santa::santad::logs::endpoint_security {
|
||||
class LoggerPeer;
|
||||
}
|
||||
|
||||
namespace santa::santad::logs::endpoint_security {
|
||||
|
||||
class Logger {
|
||||
public:
|
||||
static std::unique_ptr<Logger> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTEventLogType log_type, NSString *event_log_path);
|
||||
|
||||
Logger(std::shared_ptr<serializers::Serializer> serializer,
|
||||
std::shared_ptr<writers::Writer> writer);
|
||||
|
||||
virtual ~Logger() = default;
|
||||
|
||||
virtual void Log(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg);
|
||||
|
||||
void LogAllowlist(const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
const std::string_view hash);
|
||||
|
||||
void LogBundleHashingEvents(NSArray<SNTStoredEvent *> *events);
|
||||
|
||||
void LogDiskAppeared(NSDictionary *props);
|
||||
void LogDiskDisappeared(NSDictionary *props);
|
||||
|
||||
friend class santa::santad::logs::endpoint_security::LoggerPeer;
|
||||
|
||||
private:
|
||||
std::shared_ptr<serializers::Serializer> serializer_;
|
||||
std::shared_ptr<writers::Writer> writer_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security
|
||||
|
||||
#endif
|
||||
89
Source/santad/Logs/EndpointSecurity/Logger.mm
Normal file
89
Source/santad/Logs/EndpointSecurity/Logger.mm
Normal file
@@ -0,0 +1,89 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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/Logs/EndpointSecurity/Logger.h"
|
||||
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#include "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/File.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Null.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Syslog.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::BasicString;
|
||||
using santa::santad::logs::endpoint_security::serializers::Empty;
|
||||
using santa::santad::logs::endpoint_security::writers::File;
|
||||
using santa::santad::logs::endpoint_security::writers::Null;
|
||||
using santa::santad::logs::endpoint_security::writers::Syslog;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security {
|
||||
|
||||
// Flush the write buffer every 5 seconds
|
||||
static const uint64_t kFlushBufferTimeoutMS = 10000;
|
||||
// Batch writes up to 128kb
|
||||
static const size_t kBufferBatchSizeBytes = (1024 * 128);
|
||||
// Reserve an extra 4kb of buffer space to account for event overflow
|
||||
static const size_t kMaxExpectedWriteSizeBytes = 4096;
|
||||
|
||||
// Translate configured log type to appropriate Serializer/Writer pairs
|
||||
std::unique_ptr<Logger> Logger::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTEventLogType log_type, NSString *event_log_path) {
|
||||
switch (log_type) {
|
||||
case SNTEventLogTypeFilelog:
|
||||
return std::make_unique<Logger>(
|
||||
BasicString::Create(esapi),
|
||||
File::Create(event_log_path, kFlushBufferTimeoutMS, kBufferBatchSizeBytes,
|
||||
kMaxExpectedWriteSizeBytes));
|
||||
case SNTEventLogTypeSyslog:
|
||||
return std::make_unique<Logger>(BasicString::Create(esapi, false), Syslog::Create());
|
||||
case SNTEventLogTypeNull: return std::make_unique<Logger>(Empty::Create(), Null::Create());
|
||||
case SNTEventLogTypeProtobuf:
|
||||
LOGE(@"The EventLogType value protobuf is not supported in this release");
|
||||
return nullptr;
|
||||
default: LOGE(@"Invalid log type: %ld", log_type); return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Logger::Logger(std::shared_ptr<serializers::Serializer> serializer,
|
||||
std::shared_ptr<writers::Writer> writer)
|
||||
: serializer_(std::move(serializer)), writer_(std::move(writer)) {}
|
||||
|
||||
void Logger::Log(std::shared_ptr<EnrichedMessage> msg) {
|
||||
writer_->Write(serializer_->SerializeMessage(std::move(msg)));
|
||||
}
|
||||
|
||||
void Logger::LogAllowlist(const Message &msg, const std::string_view hash) {
|
||||
writer_->Write(serializer_->SerializeAllowlist(msg, hash));
|
||||
}
|
||||
|
||||
void Logger::LogBundleHashingEvents(NSArray<SNTStoredEvent *> *events) {
|
||||
for (SNTStoredEvent *se in events) {
|
||||
writer_->Write(serializer_->SerializeBundleHashingEvent(se));
|
||||
}
|
||||
}
|
||||
|
||||
void Logger::LogDiskAppeared(NSDictionary *props) {
|
||||
writer_->Write(serializer_->SerializeDiskAppeared(props));
|
||||
}
|
||||
|
||||
void Logger::LogDiskDisappeared(NSDictionary *props) {
|
||||
writer_->Write(serializer_->SerializeDiskDisappeared(props));
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security
|
||||
198
Source/santad/Logs/EndpointSecurity/LoggerTest.mm
Normal file
198
Source/santad/Logs/EndpointSecurity/LoggerTest.mm
Normal file
@@ -0,0 +1,198 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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 <Foundation/Foundation.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/File.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Null.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Syslog.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFile;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedProcess;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
using santa::santad::logs::endpoint_security::serializers::BasicString;
|
||||
using santa::santad::logs::endpoint_security::serializers::Empty;
|
||||
using santa::santad::logs::endpoint_security::writers::File;
|
||||
using santa::santad::logs::endpoint_security::writers::Null;
|
||||
using santa::santad::logs::endpoint_security::writers::Syslog;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security {
|
||||
|
||||
class LoggerPeer : public Logger {
|
||||
public:
|
||||
// Make base class constructors visible
|
||||
using Logger::Logger;
|
||||
|
||||
LoggerPeer(std::unique_ptr<Logger> l) : Logger(l->serializer_, l->writer_) {}
|
||||
|
||||
std::shared_ptr<serializers::Serializer> Serializer() { return serializer_; }
|
||||
|
||||
std::shared_ptr<writers::Writer> Writer() { return writer_; }
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security
|
||||
|
||||
using santa::santad::logs::endpoint_security::LoggerPeer;
|
||||
|
||||
class MockSerializer : public Empty {
|
||||
public:
|
||||
MOCK_METHOD(std::vector<uint8_t>, SerializeMessage, (const EnrichedClose &msg));
|
||||
|
||||
MOCK_METHOD(std::vector<uint8_t>, SerializeAllowlist, (const Message &, const std::string_view));
|
||||
|
||||
MOCK_METHOD(std::vector<uint8_t>, SerializeBundleHashingEvent, (SNTStoredEvent *));
|
||||
MOCK_METHOD(std::vector<uint8_t>, SerializeDiskAppeared, (NSDictionary *));
|
||||
MOCK_METHOD(std::vector<uint8_t>, SerializeDiskDisappeared, (NSDictionary *));
|
||||
};
|
||||
|
||||
class MockWriter : public Null {
|
||||
public:
|
||||
MOCK_METHOD(void, Write, (std::vector<uint8_t> && bytes));
|
||||
};
|
||||
|
||||
@interface LoggerTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation LoggerTest
|
||||
|
||||
- (void)testCreate {
|
||||
// Ensure that the factory method creates expected serializers/writers pairs
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
XCTAssertEqual(nullptr, Logger::Create(mockESApi, (SNTEventLogType)123, @"/tmp"));
|
||||
XCTAssertEqual(nullptr, Logger::Create(mockESApi, SNTEventLogTypeProtobuf, @"/tmp"));
|
||||
|
||||
LoggerPeer logger(Logger::Create(mockESApi, SNTEventLogTypeFilelog, @"/tmp/temppy"));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<BasicString>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<File>(logger.Writer()));
|
||||
|
||||
logger = LoggerPeer(Logger::Create(mockESApi, SNTEventLogTypeSyslog, @"/tmp/temppy"));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<BasicString>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Syslog>(logger.Writer()));
|
||||
|
||||
logger = LoggerPeer(Logger::Create(mockESApi, SNTEventLogTypeNull, @"/tmp/temppy"));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Empty>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Null>(logger.Writer()));
|
||||
}
|
||||
|
||||
- (void)testLog {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto mockSerializer = std::make_shared<MockSerializer>();
|
||||
auto mockWriter = std::make_shared<MockWriter>();
|
||||
|
||||
// Ensure all Logger::Log* methods call the serializer followed by the writer
|
||||
es_message_t msg;
|
||||
|
||||
// Note: In this test, `RetainMessage` isn't setup to return anything. This
|
||||
// means that the underlying `es_msg_` in the `Message` object is NULL, and
|
||||
// therefore no call to `ReleaseMessage` is ever made (hence no expectations).
|
||||
// Because we don't need to operate on the es_msg_, this simplifies the test.
|
||||
EXPECT_CALL(*mockESApi, RetainMessage);
|
||||
auto enrichedMsg = std::make_shared<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &msg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
EXPECT_CALL(*mockSerializer, SerializeMessage(testing::A<const EnrichedClose &>())).Times(1);
|
||||
EXPECT_CALL(*mockWriter, Write).Times(1);
|
||||
|
||||
Logger(mockSerializer, mockWriter).Log(enrichedMsg);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
}
|
||||
|
||||
- (void)testLogAllowList {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto mockSerializer = std::make_shared<MockSerializer>();
|
||||
auto mockWriter = std::make_shared<MockWriter>();
|
||||
es_message_t msg;
|
||||
std::string_view hash = "this_is_my_test_hash";
|
||||
|
||||
EXPECT_CALL(*mockESApi, RetainMessage);
|
||||
EXPECT_CALL(*mockSerializer, SerializeAllowlist(testing::_, hash));
|
||||
EXPECT_CALL(*mockWriter, Write);
|
||||
|
||||
Logger(mockSerializer, mockWriter).LogAllowlist(Message(mockESApi, &msg), hash);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
}
|
||||
|
||||
- (void)testLogBundleHashingEvents {
|
||||
auto mockSerializer = std::make_shared<MockSerializer>();
|
||||
auto mockWriter = std::make_shared<MockWriter>();
|
||||
NSArray<id> *events = @[ @"event1", @"event2", @"event3" ];
|
||||
|
||||
EXPECT_CALL(*mockSerializer, SerializeBundleHashingEvent).Times((int)[events count]);
|
||||
EXPECT_CALL(*mockWriter, Write).Times((int)[events count]);
|
||||
|
||||
Logger(mockSerializer, mockWriter).LogBundleHashingEvents(events);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
}
|
||||
|
||||
- (void)testLogDiskAppeared {
|
||||
auto mockSerializer = std::make_shared<MockSerializer>();
|
||||
auto mockWriter = std::make_shared<MockWriter>();
|
||||
|
||||
EXPECT_CALL(*mockSerializer, SerializeDiskAppeared);
|
||||
EXPECT_CALL(*mockWriter, Write);
|
||||
|
||||
Logger(mockSerializer, mockWriter).LogDiskAppeared(@{@"key" : @"value"});
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
}
|
||||
|
||||
- (void)testLogDiskDisappeared {
|
||||
auto mockSerializer = std::make_shared<MockSerializer>();
|
||||
auto mockWriter = std::make_shared<MockWriter>();
|
||||
|
||||
EXPECT_CALL(*mockSerializer, SerializeDiskDisappeared);
|
||||
EXPECT_CALL(*mockWriter, Write);
|
||||
|
||||
Logger(mockSerializer, mockWriter).LogDiskDisappeared(@{@"key" : @"value"});
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,74 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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__LOGS_ENDPOINTSECURITY_SERIALIZERS_BASICSTRING_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_BASICSTRING_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
class BasicString : public Serializer {
|
||||
public:
|
||||
static std::shared_ptr<BasicString> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
bool prefix_time_name = true);
|
||||
|
||||
BasicString(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
bool prefix_time_name);
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
|
||||
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;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedFork &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedLink &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeAllowlist(
|
||||
const santa::santad::event_providers::endpoint_security::Message &,
|
||||
const std::string_view) override;
|
||||
|
||||
std::vector<uint8_t> SerializeBundleHashingEvent(SNTStoredEvent *) override;
|
||||
|
||||
std::vector<uint8_t> SerializeDiskAppeared(NSDictionary *) override;
|
||||
std::vector<uint8_t> SerializeDiskDisappeared(NSDictionary *) override;
|
||||
|
||||
private:
|
||||
std::string CreateDefaultString(size_t reserved_size = 512);
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
bool prefix_time_name_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
#endif
|
||||
608
Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm
Normal file
608
Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm
Normal file
@@ -0,0 +1,608 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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/Logs/EndpointSecurity/Serializers/BasicString.h"
|
||||
|
||||
#import <Security/Security.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <libgen.h>
|
||||
#include <mach/message.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/kauth.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExit;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFork;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedLink;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedRename;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
// 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);
|
||||
extern "C" CFURLRef __nullable SecTranslocateCreateOriginalPathForURL(CFURLRef translocatedPath,
|
||||
CFErrorRef *__nullable error);
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
static inline SanitizableString FilePath(const es_file_t *file) {
|
||||
return SanitizableString(file);
|
||||
}
|
||||
|
||||
static inline pid_t Pid(const audit_token_t &tok) {
|
||||
return audit_token_to_pid(tok);
|
||||
}
|
||||
|
||||
static inline pid_t Pidversion(const audit_token_t &tok) {
|
||||
return audit_token_to_pidversion(tok);
|
||||
}
|
||||
|
||||
static inline pid_t RealUser(const audit_token_t &tok) {
|
||||
return audit_token_to_ruid(tok);
|
||||
}
|
||||
|
||||
static inline pid_t RealGroup(const audit_token_t &tok) {
|
||||
return audit_token_to_rgid(tok);
|
||||
}
|
||||
|
||||
static inline void SetThreadIDs(uid_t uid, gid_t gid) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated"
|
||||
pthread_setugid_np(uid, gid);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
static inline const mach_port_t GetDefaultIOKitCommsPort() {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return kIOMasterPortDefault;
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
static NSString *SerialForDevice(NSString *devPath) {
|
||||
if (!devPath.length) {
|
||||
return nil;
|
||||
}
|
||||
NSString *serial;
|
||||
io_registry_entry_t device =
|
||||
IORegistryEntryFromPath(GetDefaultIOKitCommsPort(), devPath.UTF8String);
|
||||
while (!serial && device) {
|
||||
CFMutableDictionaryRef device_properties = NULL;
|
||||
IORegistryEntryCreateCFProperties(device, &device_properties, kCFAllocatorDefault, kNilOptions);
|
||||
NSDictionary *properties = CFBridgingRelease(device_properties);
|
||||
if (properties[@"Serial Number"]) {
|
||||
serial = properties[@"Serial Number"];
|
||||
} else if (properties[@"kUSBSerialNumberString"]) {
|
||||
serial = properties[@"kUSBSerialNumberString"];
|
||||
}
|
||||
|
||||
if (serial) {
|
||||
IOObjectRelease(device);
|
||||
break;
|
||||
}
|
||||
|
||||
io_registry_entry_t parent;
|
||||
IORegistryEntryGetParentEntry(device, kIOServicePlane, &parent);
|
||||
IOObjectRelease(device);
|
||||
device = parent;
|
||||
}
|
||||
|
||||
return [serial stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
}
|
||||
|
||||
static NSString *DiskImageForDevice(NSString *devPath) {
|
||||
devPath = [devPath stringByDeletingLastPathComponent];
|
||||
if (!devPath.length) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
io_registry_entry_t device =
|
||||
IORegistryEntryFromPath(GetDefaultIOKitCommsPort(), devPath.UTF8String);
|
||||
CFMutableDictionaryRef device_properties = NULL;
|
||||
IORegistryEntryCreateCFProperties(device, &device_properties, kCFAllocatorDefault, kNilOptions);
|
||||
NSDictionary *properties = CFBridgingRelease(device_properties);
|
||||
IOObjectRelease(device);
|
||||
|
||||
if (properties[@"image-path"]) {
|
||||
NSString *result = [[NSString alloc] initWithData:properties[@"image-path"]
|
||||
encoding:NSUTF8StringEncoding];
|
||||
return [result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
static NSString *OriginalPathForTranslocation(const es_process_t *esProc) {
|
||||
if (!esProc) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Note: Benchmarks showed better performance using `URLWithString` with a `file://` prefix
|
||||
// compared to using `fileURLWithPath`.
|
||||
CFURLRef cfExecURL = (__bridge CFURLRef)
|
||||
[NSURL URLWithString:[NSString stringWithFormat:@"file://%s", esProc->executable->path.data]];
|
||||
NSURL *origURL = nil;
|
||||
bool isTranslocated = false;
|
||||
|
||||
if (SecTranslocateIsTranslocatedURL(cfExecURL, &isTranslocated, NULL) && isTranslocated) {
|
||||
bool dropPrivs = true;
|
||||
if (@available(macOS 12.0, *)) {
|
||||
dropPrivs = false;
|
||||
}
|
||||
|
||||
if (dropPrivs) {
|
||||
SetThreadIDs(RealUser(esProc->audit_token), RealGroup(esProc->audit_token));
|
||||
}
|
||||
|
||||
origURL = CFBridgingRelease(SecTranslocateCreateOriginalPathForURL(cfExecURL, NULL));
|
||||
|
||||
if (dropPrivs) {
|
||||
SetThreadIDs(KAUTH_UID_NONE, KAUTH_GID_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
return [origURL path];
|
||||
}
|
||||
|
||||
es_file_t *GetAllowListTargetFile(const Message &msg) {
|
||||
switch (msg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: return msg->event.close.target;
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: return msg->event.rename.source;
|
||||
default:
|
||||
// This is a programming error
|
||||
LOGE(@"Unexpected event type for AllowList");
|
||||
[NSException raise:@"Unexpected type"
|
||||
format:@"Unexpected event type for AllowList: %d", msg->event_type];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
static NSDateFormatter *GetDateFormatter() {
|
||||
static dispatch_once_t onceToken;
|
||||
static NSDateFormatter *dateFormatter;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
dateFormatter = [[NSDateFormatter alloc] init];
|
||||
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
|
||||
dateFormatter.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601];
|
||||
dateFormatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
|
||||
});
|
||||
|
||||
return dateFormatter;
|
||||
}
|
||||
|
||||
std::string GetDecisionString(SNTEventState event_state) {
|
||||
if (event_state & SNTEventStateAllow) {
|
||||
return "ALLOW";
|
||||
} else if (event_state & SNTEventStateBlock) {
|
||||
return "DENY";
|
||||
} else {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
std::string GetReasonString(SNTEventState event_state) {
|
||||
switch (event_state) {
|
||||
case SNTEventStateAllowBinary: return "BINARY";
|
||||
case SNTEventStateAllowCompiler: return "COMPILER";
|
||||
case SNTEventStateAllowTransitive: return "TRANSITIVE";
|
||||
case SNTEventStateAllowPendingTransitive: return "PENDING_TRANSITIVE";
|
||||
case SNTEventStateAllowCertificate: return "CERT";
|
||||
case SNTEventStateAllowScope: return "SCOPE";
|
||||
case SNTEventStateAllowTeamID: return "TEAMID";
|
||||
case SNTEventStateAllowUnknown: return "UNKNOWN";
|
||||
case SNTEventStateBlockBinary: return "BINARY";
|
||||
case SNTEventStateBlockCertificate: return "CERT";
|
||||
case SNTEventStateBlockScope: return "SCOPE";
|
||||
case SNTEventStateBlockTeamID: return "TEAMID";
|
||||
case SNTEventStateBlockLongPath: return "LONG_PATH";
|
||||
case SNTEventStateBlockUnknown: return "UNKNOWN";
|
||||
default: return "NOTRUNNING";
|
||||
}
|
||||
}
|
||||
|
||||
std::string GetModeString(SNTClientMode mode) {
|
||||
switch (mode) {
|
||||
case SNTClientModeMonitor: return "M";
|
||||
case SNTClientModeLockdown: return "L";
|
||||
default: return "U";
|
||||
}
|
||||
}
|
||||
|
||||
static inline void AppendProcess(std::string &str, const es_process_t *es_proc) {
|
||||
char bname[MAXPATHLEN];
|
||||
str.append("|pid=");
|
||||
str.append(std::to_string(Pid(es_proc->audit_token)));
|
||||
str.append("|ppid=");
|
||||
str.append(std::to_string(es_proc->original_ppid));
|
||||
str.append("|process=");
|
||||
str.append(basename_r(FilePath(es_proc->executable).Sanitized().data(), bname) ?: "");
|
||||
str.append("|processpath=");
|
||||
str.append(FilePath(es_proc->executable).Sanitized());
|
||||
}
|
||||
|
||||
static inline void AppendUserGroup(std::string &str, const audit_token_t &tok,
|
||||
const std::optional<std::shared_ptr<std::string>> &user,
|
||||
const std::optional<std::shared_ptr<std::string>> &group) {
|
||||
str.append("|uid=");
|
||||
str.append(std::to_string(RealUser(tok)));
|
||||
str.append("|user=");
|
||||
str.append(user.has_value() ? user->get()->c_str() : "(null)");
|
||||
str.append("|gid=");
|
||||
str.append(std::to_string(RealGroup(tok)));
|
||||
str.append("|group=");
|
||||
str.append(group.has_value() ? group->get()->c_str() : "(null)");
|
||||
}
|
||||
|
||||
static char *FormattedDateString(char *buf, size_t len) {
|
||||
struct timeval tv;
|
||||
struct tm tm;
|
||||
|
||||
gettimeofday(&tv, NULL);
|
||||
gmtime_r(&tv.tv_sec, &tm);
|
||||
|
||||
strftime(buf, len, "%Y-%m-%dT%H:%M:%S", &tm);
|
||||
snprintf(buf, len, "%s.%03dZ", buf, tv.tv_usec / 1000);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
static inline NSString *NonNull(NSString *str) {
|
||||
return str ?: @"";
|
||||
}
|
||||
|
||||
std::shared_ptr<BasicString> BasicString::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
bool prefix_time_name) {
|
||||
return std::make_shared<BasicString>(esapi, prefix_time_name);
|
||||
}
|
||||
|
||||
BasicString::BasicString(std::shared_ptr<EndpointSecurityAPI> esapi, bool prefix_time_name)
|
||||
: esapi_(esapi), prefix_time_name_(prefix_time_name) {}
|
||||
|
||||
std::string BasicString::CreateDefaultString(size_t reserved_size) {
|
||||
std::string str;
|
||||
str.reserve(1024);
|
||||
|
||||
if (prefix_time_name_) {
|
||||
char buf[32];
|
||||
|
||||
str.append("[");
|
||||
str.append(FormattedDateString(buf, sizeof(buf)));
|
||||
str.append("] I santad: ");
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
inline std::vector<uint8_t> FinalizeString(std::string &str) {
|
||||
str.append("\n");
|
||||
std::vector<uint8_t> vec(str.length());
|
||||
std::copy(str.begin(), str.end(), vec.begin());
|
||||
return vec;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedClose &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=WRITE|path=");
|
||||
str.append(FilePath(esm.event.close.target).Sanitized());
|
||||
|
||||
AppendProcess(str, esm.process);
|
||||
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExchange &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=EXCHANGE|path=");
|
||||
str.append(FilePath(esm.event.exchangedata.file1).Sanitized());
|
||||
str.append("|newpath=");
|
||||
str.append(FilePath(esm.event.exchangedata.file2).Sanitized());
|
||||
|
||||
AppendProcess(str, esm.process);
|
||||
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg) {
|
||||
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];
|
||||
|
||||
str.append("action=EXEC|decision=");
|
||||
str.append(GetDecisionString(cd.decision));
|
||||
str.append("|reason=");
|
||||
str.append(GetReasonString(cd.decision));
|
||||
|
||||
if (cd.decisionExtra) {
|
||||
str.append("|explain=");
|
||||
str.append([cd.decisionExtra UTF8String]);
|
||||
}
|
||||
|
||||
if (cd.sha256) {
|
||||
str.append("|sha256=");
|
||||
str.append([cd.sha256 UTF8String]);
|
||||
}
|
||||
|
||||
if (cd.certSHA256) {
|
||||
str.append("|cert_sha256=");
|
||||
str.append([cd.certSHA256 UTF8String]);
|
||||
str.append("|cert_cn=");
|
||||
str.append(SanitizableString(cd.certCommonName).Sanitized());
|
||||
}
|
||||
|
||||
if (cd.teamID.length) {
|
||||
str.append("|teamid=");
|
||||
str.append([NonNull(cd.teamID) UTF8String]);
|
||||
}
|
||||
|
||||
if (cd.quarantineURL) {
|
||||
str.append("|quarantine_url=");
|
||||
str.append(SanitizableString(cd.quarantineURL).Sanitized());
|
||||
}
|
||||
|
||||
str.append("|pid=");
|
||||
str.append(std::to_string(Pid(esm.event.exec.target->audit_token)));
|
||||
str.append("|pidversion=");
|
||||
str.append(std::to_string(Pidversion(esm.event.exec.target->audit_token)));
|
||||
str.append("|ppid=");
|
||||
str.append(std::to_string(esm.event.exec.target->original_ppid));
|
||||
|
||||
AppendUserGroup(str, esm.event.exec.target->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
|
||||
str.append("|mode=");
|
||||
str.append(GetModeString([[SNTConfigurator configurator] clientMode]));
|
||||
str.append("|path=");
|
||||
str.append(FilePath(esm.event.exec.target->executable).Sanitized());
|
||||
|
||||
NSString *origPath = OriginalPathForTranslocation(esm.event.exec.target);
|
||||
if (origPath) {
|
||||
str.append("|origpath=");
|
||||
str.append(SanitizableString(origPath).Sanitized());
|
||||
}
|
||||
|
||||
uint32_t argCount = esapi_->ExecArgCount(&esm.event.exec);
|
||||
if (argCount > 0) {
|
||||
str.append("|args=");
|
||||
for (uint32_t i = 0; i < argCount; i++) {
|
||||
if (i != 0) {
|
||||
str.append(" ");
|
||||
}
|
||||
|
||||
str.append(SanitizableString(esapi_->ExecArg(&esm.event.exec, i)).Sanitized());
|
||||
}
|
||||
}
|
||||
|
||||
if ([[SNTConfigurator configurator] enableMachineIDDecoration]) {
|
||||
str.append("|machineid=");
|
||||
str.append([NonNull([[SNTConfigurator configurator] machineID]) UTF8String]);
|
||||
}
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExit &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=EXIT|pid=");
|
||||
str.append(std::to_string(Pid(esm.process->audit_token)));
|
||||
str.append("|pidversion=");
|
||||
str.append(std::to_string(Pidversion(esm.process->audit_token)));
|
||||
str.append("|ppid=");
|
||||
str.append(std::to_string(esm.process->original_ppid));
|
||||
str.append("|uid=");
|
||||
str.append(std::to_string(RealUser(esm.process->audit_token)));
|
||||
str.append("|gid=");
|
||||
str.append(std::to_string(RealGroup(esm.process->audit_token)));
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedFork &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=FORK|pid=");
|
||||
str.append(std::to_string(Pid(esm.event.fork.child->audit_token)));
|
||||
str.append("|pidversion=");
|
||||
str.append(std::to_string(Pidversion(esm.event.fork.child->audit_token)));
|
||||
str.append("|ppid=");
|
||||
str.append(std::to_string(esm.event.fork.child->original_ppid));
|
||||
str.append("|uid=");
|
||||
str.append(std::to_string(RealUser(esm.event.fork.child->audit_token)));
|
||||
str.append("|gid=");
|
||||
str.append(std::to_string(RealGroup(esm.event.fork.child->audit_token)));
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedLink &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=LINK|path=");
|
||||
str.append(FilePath(esm.event.link.source).Sanitized());
|
||||
str.append("|newpath=");
|
||||
str.append(FilePath(esm.event.link.target_dir).Sanitized());
|
||||
str.append("/");
|
||||
str.append(SanitizableString(esm.event.link.target_filename).Sanitized());
|
||||
|
||||
AppendProcess(str, esm.process);
|
||||
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedRename &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=RENAME|path=");
|
||||
str.append(FilePath(esm.event.rename.source).Sanitized());
|
||||
str.append("|newpath=");
|
||||
|
||||
switch (esm.event.rename.destination_type) {
|
||||
case ES_DESTINATION_TYPE_EXISTING_FILE:
|
||||
str.append(FilePath(esm.event.rename.destination.existing_file).Sanitized());
|
||||
break;
|
||||
case ES_DESTINATION_TYPE_NEW_PATH:
|
||||
str.append(FilePath(esm.event.rename.destination.new_path.dir).Sanitized());
|
||||
str.append("/");
|
||||
str.append(SanitizableString(esm.event.rename.destination.new_path.filename).Sanitized());
|
||||
break;
|
||||
default: str.append("(null)"); break;
|
||||
}
|
||||
|
||||
AppendProcess(str, esm.process);
|
||||
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=DELETE|path=");
|
||||
str.append(FilePath(esm.event.unlink.target).Sanitized());
|
||||
|
||||
AppendProcess(str, esm.process);
|
||||
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeAllowlist(const Message &msg,
|
||||
const std::string_view hash) {
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=ALLOWLIST|pid=");
|
||||
str.append(std::to_string(Pid(msg->process->audit_token)));
|
||||
str.append("|pidversion=");
|
||||
str.append(std::to_string(Pidversion(msg->process->audit_token)));
|
||||
str.append("|path=");
|
||||
str.append(FilePath(GetAllowListTargetFile(msg)).Sanitized());
|
||||
str.append("|sha256=");
|
||||
str.append(hash);
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeBundleHashingEvent(SNTStoredEvent *event) {
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=BUNDLE|sha256=");
|
||||
str.append([NonNull(event.fileSHA256) UTF8String]);
|
||||
str.append("|bundlehash=");
|
||||
str.append([NonNull(event.fileBundleHash) UTF8String]);
|
||||
str.append("|bundlename=");
|
||||
str.append([NonNull(event.fileBundleName) UTF8String]);
|
||||
str.append("|bundleid=");
|
||||
str.append([NonNull(event.fileBundleID) UTF8String]);
|
||||
str.append("|bundlepath=");
|
||||
str.append([NonNull(event.fileBundlePath) UTF8String]);
|
||||
str.append("|path=");
|
||||
str.append([NonNull(event.filePath) UTF8String]);
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeDiskAppeared(NSDictionary *props) {
|
||||
NSString *dmgPath = nil;
|
||||
NSString *serial = nil;
|
||||
if ([props[@"DADeviceModel"] isEqual:@"Disk Image"]) {
|
||||
dmgPath = DiskImageForDevice(props[@"DADevicePath"]);
|
||||
} else {
|
||||
serial = SerialForDevice(props[@"DADevicePath"]);
|
||||
}
|
||||
|
||||
NSString *model = [NSString
|
||||
stringWithFormat:@"%@ %@", NonNull(props[@"DADeviceVendor"]), NonNull(props[@"DADeviceModel"])];
|
||||
model = [model stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
|
||||
NSString *appearanceDateString = [GetDateFormatter()
|
||||
stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:[props[@"DAAppearanceTime"]
|
||||
doubleValue]]];
|
||||
|
||||
std::string str = CreateDefaultString();
|
||||
str.append("action=DISKAPPEAR");
|
||||
str.append("|mount=");
|
||||
str.append([NonNull([props[@"DAVolumePath"] path]) UTF8String]);
|
||||
str.append("|volume=");
|
||||
str.append([NonNull(props[@"DAVolumeName"]) UTF8String]);
|
||||
str.append("|bsdname=");
|
||||
str.append([NonNull(props[@"DAMediaBSDName"]) UTF8String]);
|
||||
str.append("|fs=");
|
||||
str.append([NonNull(props[@"DAVolumeKind"]) UTF8String]);
|
||||
str.append("|model=");
|
||||
str.append([NonNull(model) UTF8String]);
|
||||
str.append("|serial=");
|
||||
str.append([NonNull(serial) UTF8String]);
|
||||
str.append("|bus=");
|
||||
str.append([NonNull(props[@"DADeviceProtocol"]) UTF8String]);
|
||||
str.append("|dmgpath=");
|
||||
str.append([NonNull(dmgPath) UTF8String]);
|
||||
str.append("|appearance=");
|
||||
str.append([NonNull(appearanceDateString) UTF8String]);
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeDiskDisappeared(NSDictionary *props) {
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=DISKDISAPPEAR");
|
||||
str.append("|mount=");
|
||||
str.append([NonNull([props[@"DAVolumePath"] path]) UTF8String]);
|
||||
str.append("|volume=");
|
||||
str.append([NonNull(props[@"DAVolumeName"]) UTF8String]);
|
||||
str.append("|bsdname=");
|
||||
str.append([NonNull(props[@"DAMediaBSDName"]) UTF8String]);
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
@@ -0,0 +1,418 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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 <EndpointSecurity/ESTypes.h>
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::BasicString;
|
||||
using santa::santad::logs::endpoint_security::serializers::Serializer;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
extern es_file_t *GetAllowListTargetFile(const Message &msg);
|
||||
extern std::string GetDecisionString(SNTEventState event_state);
|
||||
extern std::string GetReasonString(SNTEventState event_state);
|
||||
extern std::string GetModeString(SNTClientMode mode);
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
using santa::santad::logs::endpoint_security::serializers::GetAllowListTargetFile;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetDecisionString;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetModeString;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetReasonString;
|
||||
|
||||
std::string BasicStringSerializeMessage(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(esMsg);
|
||||
|
||||
std::shared_ptr<Serializer> bs = BasicString::Create(mockESApi, false);
|
||||
std::vector<uint8_t> ret = bs->SerializeMessage(Enricher().Enrich(Message(mockESApi, esMsg)));
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
return std::string(ret.begin(), ret.end());
|
||||
}
|
||||
|
||||
std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
return BasicStringSerializeMessage(mockESApi, esMsg);
|
||||
}
|
||||
|
||||
@interface BasicStringTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property id mockDecisionCache;
|
||||
|
||||
@property SNTCachedDecision *testCachedDecision;
|
||||
@end
|
||||
|
||||
@implementation BasicStringTest
|
||||
|
||||
- (void)setUp {
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(YES);
|
||||
OCMStub([self.mockConfigurator machineID]).andReturn(@"my_id");
|
||||
|
||||
self.testCachedDecision = [[SNTCachedDecision alloc] init];
|
||||
self.testCachedDecision.decision = SNTEventStateAllowBinary;
|
||||
self.testCachedDecision.decisionExtra = @"extra!";
|
||||
self.testCachedDecision.sha256 = @"1234_hash";
|
||||
self.testCachedDecision.quarantineURL = @"google.com";
|
||||
self.testCachedDecision.certSHA256 = @"5678_hash";
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
OCMStub([self.mockDecisionCache cachedDecisionForFile:{}])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(self.testCachedDecision);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.mockConfigurator stopMocking];
|
||||
[self.mockDecisionCache stopMocking];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageClose {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_file_t file = MakeESFile("close_file");
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
|
||||
esMsg.event.close.modified = true;
|
||||
esMsg.event.close.target = &file;
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=WRITE|path=close_file"
|
||||
"|pid=12|ppid=56|process=foo|processpath=foo"
|
||||
"|uid=-2|user=nobody|gid=-2|group=nobody\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExchange {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_file_t file1 = MakeESFile("exchange_1");
|
||||
es_file_t file2 = MakeESFile("exchange_2");
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA, &proc);
|
||||
esMsg.event.exchangedata.file1 = &file1;
|
||||
esMsg.event.exchangedata.file2 = &file2;
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=EXCHANGE|path=exchange_1|newpath=exchange_2"
|
||||
"|pid=12|ppid=56|process=foo|processpath=foo"
|
||||
"|uid=-2|user=nobody|gid=-2|group=nobody\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExec {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
|
||||
es_file_t execFile = MakeESFile("execpath|");
|
||||
es_process_t procExec = MakeESProcess(&execFile, MakeAuditToken(12, 89), MakeAuditToken(56, 78));
|
||||
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXEC, &proc);
|
||||
esMsg.event.exec.target = &procExec;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
EXPECT_CALL(*mockESApi, ExecArgCount).WillOnce(testing::Return(3));
|
||||
|
||||
EXPECT_CALL(*mockESApi, ExecArg)
|
||||
.WillOnce(testing::Return(es_string_token_t{9, "exec|path"}))
|
||||
.WillOnce(testing::Return(es_string_token_t{5, "-l\n-t"}))
|
||||
.WillOnce(testing::Return(es_string_token_t{8, "-v\r--foo"}));
|
||||
|
||||
std::string got = BasicStringSerializeMessage(mockESApi, &esMsg);
|
||||
std::string want = "action=EXEC|decision=ALLOW|reason=BINARY|explain=extra!|sha256=1234_hash|"
|
||||
"cert_sha256=5678_hash|cert_cn=|quarantine_url=google.com|pid=12|pidversion="
|
||||
"89|ppid=56|uid=-2|user=nobody|gid=-2|group=nobody|mode=L|path=execpath<pipe>|"
|
||||
"args=exec<pipe>path -l\\n-t -v\\r--foo|machineid=my_id\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExit {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=EXIT|pid=12|pidversion=34|ppid=56|uid=-2|gid=-2\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageFork {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_file_t procChildFile = MakeESFile("foo_child");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_process_t procChild =
|
||||
MakeESProcess(&procChildFile, MakeAuditToken(67, 89), MakeAuditToken(12, 34));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_FORK, &proc);
|
||||
esMsg.event.fork.child = &procChild;
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=FORK|pid=67|pidversion=89|ppid=12|uid=-2|gid=-2\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageLink {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_file_t srcFile = MakeESFile("link_src");
|
||||
es_file_t dstDir = MakeESFile("link_dst");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_LINK, &proc);
|
||||
esMsg.event.link.source = &srcFile;
|
||||
esMsg.event.link.target_dir = &dstDir;
|
||||
esMsg.event.link.target_filename = MakeESStringToken("link_name");
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=LINK|path=link_src|newpath=link_dst/link_name"
|
||||
"|pid=12|ppid=56|process=foo|processpath=foo"
|
||||
"|uid=-2|user=nobody|gid=-2|group=nobody\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageRename {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_file_t srcFile = MakeESFile("rename_src");
|
||||
es_file_t dstFile = MakeESFile("rename_dst");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &proc);
|
||||
esMsg.event.rename.source = &srcFile;
|
||||
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
|
||||
esMsg.event.rename.destination.existing_file = &dstFile;
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=RENAME|path=rename_src|newpath=rename_dst"
|
||||
"|pid=12|ppid=56|process=foo|processpath=foo"
|
||||
"|uid=-2|user=nobody|gid=-2|group=nobody\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageUnlink {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_file_t targetFile = MakeESFile("deleted_file");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_UNLINK, &proc);
|
||||
esMsg.event.unlink.target = &targetFile;
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=DELETE|path=deleted_file"
|
||||
"|pid=12|ppid=56|process=foo|processpath=foo"
|
||||
"|uid=-2|user=nobody|gid=-2|group=nobody\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeAllowlist {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
|
||||
esMsg.event.close.target = &file;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(mockESApi, false)
|
||||
->SerializeAllowlist(Message(mockESApi, &esMsg), "test_hash");
|
||||
|
||||
XCTAssertTrue(testing::Mock::VerifyAndClearExpectations(mockESApi.get()),
|
||||
"Expected calls were not properly mocked");
|
||||
|
||||
std::string got(ret.begin(), ret.end());
|
||||
std::string want = "action=ALLOWLIST|pid=12|pidversion=34|path=foo"
|
||||
"|sha256=test_hash\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeBundleHashingEvent {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
|
||||
se.fileSHA256 = @"file_hash";
|
||||
se.fileBundleHash = @"file_bundle_hash";
|
||||
se.fileBundleName = @"file_bundle_Name";
|
||||
se.fileBundleID = nil;
|
||||
se.fileBundlePath = @"file_bundle_path";
|
||||
se.filePath = @"file_path";
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeBundleHashingEvent(se);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
|
||||
std::string want = "action=BUNDLE|sha256=file_hash"
|
||||
"|bundlehash=file_bundle_hash|bundlename=file_bundle_Name|bundleid="
|
||||
"|bundlepath=file_bundle_path|path=file_path\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeDiskAppeared {
|
||||
NSDictionary *props = @{
|
||||
@"DADevicePath" : @"",
|
||||
@"DADeviceVendor" : @"vendor",
|
||||
@"DADeviceModel" : @"model",
|
||||
@"DAAppearanceTime" : @(1252487349), // 2009-09-09 09:09:09
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"path"],
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
@"DAVolumeKind" : @"apfs",
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeDiskAppeared(props);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
|
||||
std::string want = "action=DISKAPPEAR|mount=path|volume=|bsdname=bsd|fs=apfs"
|
||||
"|model=vendor model|serial=|bus=usb|dmgpath="
|
||||
"|appearance=2040-09-09T09:09:09.000Z\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeDiskDisappeared {
|
||||
NSDictionary *props = @{
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"path"],
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeDiskDisappeared(props);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
|
||||
std::string want = "action=DISKDISAPPEAR|mount=path|volume=|bsdname=bsd\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testGetDecisionString {
|
||||
std::map<SNTEventState, std::string> stateToDecision = {
|
||||
{SNTEventStateUnknown, "UNKNOWN"},
|
||||
{SNTEventStateBundleBinary, "UNKNOWN"},
|
||||
{SNTEventStateBlockUnknown, "DENY"},
|
||||
{SNTEventStateBlockBinary, "DENY"},
|
||||
{SNTEventStateBlockCertificate, "DENY"},
|
||||
{SNTEventStateBlockScope, "DENY"},
|
||||
{SNTEventStateBlockTeamID, "DENY"},
|
||||
{SNTEventStateBlockLongPath, "DENY"},
|
||||
{SNTEventStateAllowUnknown, "ALLOW"},
|
||||
{SNTEventStateAllowBinary, "ALLOW"},
|
||||
{SNTEventStateAllowCertificate, "ALLOW"},
|
||||
{SNTEventStateAllowScope, "ALLOW"},
|
||||
{SNTEventStateAllowCompiler, "ALLOW"},
|
||||
{SNTEventStateAllowTransitive, "ALLOW"},
|
||||
{SNTEventStateAllowPendingTransitive, "ALLOW"},
|
||||
{SNTEventStateAllowTeamID, "ALLOW"},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToDecision) {
|
||||
XCTAssertCppStringEqual(GetDecisionString(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testGetReasonString {
|
||||
std::map<SNTEventState, std::string> stateToReason = {
|
||||
{SNTEventStateUnknown, "NOTRUNNING"},
|
||||
{SNTEventStateBundleBinary, "NOTRUNNING"},
|
||||
{SNTEventStateBlockUnknown, "UNKNOWN"},
|
||||
{SNTEventStateBlockBinary, "BINARY"},
|
||||
{SNTEventStateBlockCertificate, "CERT"},
|
||||
{SNTEventStateBlockScope, "SCOPE"},
|
||||
{SNTEventStateBlockTeamID, "TEAMID"},
|
||||
{SNTEventStateBlockLongPath, "LONG_PATH"},
|
||||
{SNTEventStateAllowUnknown, "UNKNOWN"},
|
||||
{SNTEventStateAllowBinary, "BINARY"},
|
||||
{SNTEventStateAllowCertificate, "CERT"},
|
||||
{SNTEventStateAllowScope, "SCOPE"},
|
||||
{SNTEventStateAllowCompiler, "COMPILER"},
|
||||
{SNTEventStateAllowTransitive, "TRANSITIVE"},
|
||||
{SNTEventStateAllowPendingTransitive, "PENDING_TRANSITIVE"},
|
||||
{SNTEventStateAllowTeamID, "TEAMID"},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToReason) {
|
||||
XCTAssertCppStringEqual(GetReasonString(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testGetModeString {
|
||||
std::map<SNTClientMode, std::string> modeToString = {
|
||||
{SNTClientModeMonitor, "M"},
|
||||
{SNTClientModeLockdown, "L"},
|
||||
{(SNTClientMode)123, "U"},
|
||||
};
|
||||
|
||||
for (const auto &kv : modeToString) {
|
||||
XCTAssertCppStringEqual(GetModeString(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testGetAllowListTargetFile {
|
||||
es_file_t closeTargetFile = MakeESFile("close_target");
|
||||
es_file_t renameSourceFile = MakeESFile("rename_source");
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
{
|
||||
esMsg.event.close.target = &closeTargetFile;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
es_file_t *target = GetAllowListTargetFile(msg);
|
||||
XCTAssertEqual(target, &closeTargetFile);
|
||||
}
|
||||
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_RENAME;
|
||||
esMsg.event.rename.source = &renameSourceFile;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
es_file_t *target = GetAllowListTargetFile(msg);
|
||||
XCTAssertEqual(target, &renameSourceFile);
|
||||
}
|
||||
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXIT;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertThrows(GetAllowListTargetFile(msg));
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
60
Source/santad/Logs/EndpointSecurity/Serializers/Empty.h
Normal file
60
Source/santad/Logs/EndpointSecurity/Serializers/Empty.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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__LOGS_ENDPOINTSECURITY_SERIALIZERS_EMPTY_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_EMPTY_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
class Empty : public Serializer {
|
||||
public:
|
||||
static std::shared_ptr<Empty> Create();
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
|
||||
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;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedFork &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedLink &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeAllowlist(
|
||||
const santa::santad::event_providers::endpoint_security::Message &,
|
||||
const std::string_view) override;
|
||||
|
||||
std::vector<uint8_t> SerializeBundleHashingEvent(SNTStoredEvent *) override;
|
||||
|
||||
std::vector<uint8_t> SerializeDiskAppeared(NSDictionary *) override;
|
||||
std::vector<uint8_t> SerializeDiskDisappeared(NSDictionary *) override;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
#endif
|
||||
81
Source/santad/Logs/EndpointSecurity/Serializers/Empty.mm
Normal file
81
Source/santad/Logs/EndpointSecurity/Serializers/Empty.mm
Normal file
@@ -0,0 +1,81 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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/Logs/EndpointSecurity/Serializers/Empty.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExit;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFork;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedLink;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedRename;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
std::shared_ptr<Empty> Empty::Create() {
|
||||
return std::make_shared<Empty>();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedClose &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExchange &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExec &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExit &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedFork &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedLink &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedRename &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeAllowlist(const Message &msg, const std::string_view hash) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeBundleHashingEvent(SNTStoredEvent *event) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeDiskAppeared(NSDictionary *props) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeDiskDisappeared(NSDictionary *props) {
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
53
Source/santad/Logs/EndpointSecurity/Serializers/EmptyTest.mm
Normal file
53
Source/santad/Logs/EndpointSecurity/Serializers/EmptyTest.mm
Normal file
@@ -0,0 +1,53 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
|
||||
|
||||
using santa::santad::logs::endpoint_security::serializers::Empty;
|
||||
|
||||
namespace es = santa::santad::event_providers::endpoint_security;
|
||||
|
||||
@interface EmptyTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation EmptyTest
|
||||
|
||||
- (void)testAllSerializersReturnEmptyVector {
|
||||
std::shared_ptr<Empty> e = Empty::Create();
|
||||
|
||||
// We can get away with passing a fake argument to the `Serialize*` methods
|
||||
// instead of constructing real ones since the Empty class never touches the
|
||||
// input parameter.
|
||||
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::EnrichedExit *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedFork *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedLink *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedRename *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedUnlink *)&fake).size(), 0);
|
||||
|
||||
XCTAssertEqual(e->SerializeAllowlist(*(es::Message *)&fake, "").size(), 0);
|
||||
XCTAssertEqual(e->SerializeBundleHashingEvent(nil).size(), 0);
|
||||
XCTAssertEqual(e->SerializeDiskAppeared(nil).size(), 0);
|
||||
XCTAssertEqual(e->SerializeDiskDisappeared(nil).size(), 0);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,62 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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__LOGS_ENDPOINTSECURITY_SERIALIZERS_SANITIZABLESTRING_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SANITIZABLESTRING_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
// Small helper class that will sanitize a given string, but will only use new
|
||||
// memory if the string required sanitization. If the string is already
|
||||
// sanitized, this class only uses the given buffers.
|
||||
class SanitizableString {
|
||||
public:
|
||||
SanitizableString(const es_file_t *file);
|
||||
SanitizableString(const es_string_token_t &tok);
|
||||
SanitizableString(const char *str, size_t len);
|
||||
SanitizableString(NSString *str);
|
||||
|
||||
SanitizableString(SanitizableString &&other) = delete;
|
||||
SanitizableString(const SanitizableString &other) = delete;
|
||||
SanitizableString &operator=(const SanitizableString &rhs) = delete;
|
||||
SanitizableString &operator=(SanitizableString &&rhs) = delete;
|
||||
|
||||
// Return the original, unsanitized string
|
||||
std::string_view String() const;
|
||||
|
||||
// Return the sanitized string
|
||||
std::string_view Sanitized() const;
|
||||
|
||||
static std::optional<std::string> SanitizeString(const char *str);
|
||||
static std::optional<std::string> SanitizeString(const char *str, size_t length);
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &ss, const SanitizableString &sani_string);
|
||||
|
||||
private:
|
||||
const char *data_;
|
||||
size_t length_;
|
||||
mutable bool sanitized_ = false;
|
||||
mutable std::optional<std::string> sanitized_string_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,113 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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/Logs/EndpointSecurity/Serializers/SanitizableString.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
SanitizableString::SanitizableString(const es_file_t *file)
|
||||
: data_(file->path.data), length_(file->path.length) {}
|
||||
|
||||
SanitizableString::SanitizableString(const es_string_token_t &tok)
|
||||
: data_(tok.data), length_(tok.length) {}
|
||||
|
||||
SanitizableString::SanitizableString(NSString *str)
|
||||
: data_([str UTF8String]), length_([str length]) {}
|
||||
|
||||
SanitizableString::SanitizableString(const char *str, size_t len) : data_(str), length_(len) {}
|
||||
|
||||
std::string_view SanitizableString::String() const {
|
||||
return std::string_view(data_, length_);
|
||||
}
|
||||
|
||||
std::string_view SanitizableString::Sanitized() const {
|
||||
if (!sanitized_) {
|
||||
sanitized_ = true;
|
||||
sanitized_string_ = SanitizeString(data_, length_);
|
||||
}
|
||||
|
||||
if (sanitized_string_.has_value()) {
|
||||
return sanitized_string_.value();
|
||||
} else {
|
||||
if (data_) {
|
||||
return std::string_view(data_, length_);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::ostream &operator<<(std::ostream &ss, const SanitizableString &sani_string) {
|
||||
ss << sani_string.Sanitized();
|
||||
return ss;
|
||||
}
|
||||
|
||||
std::optional<std::string> SanitizableString::SanitizeString(const char *str) {
|
||||
return SanitizeString(str, str ? strlen(str) : 0);
|
||||
}
|
||||
|
||||
std::optional<std::string> SanitizableString::SanitizeString(const char *str, size_t length) {
|
||||
size_t strOffset = 0;
|
||||
char c = 0;
|
||||
std::string buf;
|
||||
bool reservedStringSpace = false;
|
||||
|
||||
if (!str) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (length < 1) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Loop through the string one character at a time, looking for the characters
|
||||
// we want to remove.
|
||||
for (const char *p = str; (c = *p) != 0; ++p) {
|
||||
if (c == '|' || c == '\n' || c == '\r') {
|
||||
if (!reservedStringSpace) {
|
||||
// Assume the common case won't grow the string length by more than a
|
||||
// factor of 2. String will grow more if it needs to.
|
||||
buf.reserve(length * 2);
|
||||
reservedStringSpace = true;
|
||||
}
|
||||
|
||||
// Copy from the last offset up to the character we just found into the buffer
|
||||
ptrdiff_t diff = p - str;
|
||||
buf.append(str + strOffset, diff - strOffset);
|
||||
|
||||
// Update the buffer and string offsets
|
||||
strOffset = diff + 1;
|
||||
|
||||
// Replace the found character and advance the buffer offset
|
||||
switch (c) {
|
||||
case '|': buf.append("<pipe>"); break;
|
||||
case '\n': buf.append("\\n"); break;
|
||||
case '\r': buf.append("\\r"); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (strOffset > 0 && strOffset < length) {
|
||||
// Copy any characters from the last match to the end of the string into the buffer.
|
||||
buf.append(str + strOffset, length - strOffset);
|
||||
}
|
||||
|
||||
if (reservedStringSpace) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
@@ -0,0 +1,90 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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/Logs/EndpointSecurity/Serializers/SanitizableString.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
|
||||
using santa::santad::logs::endpoint_security::serializers::SanitizableString;
|
||||
|
||||
@interface SanitizableStringTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SanitizableStringTest
|
||||
|
||||
- (void)testSanitizeString {
|
||||
const char *empty = "";
|
||||
size_t emptyLen = strlen(empty);
|
||||
const char *noSanitize = "nothing_to_sanitize";
|
||||
size_t noSanitizeLen = strlen(noSanitize);
|
||||
const char *sanitizable = "sani|tizable";
|
||||
size_t sanitizableLen = strlen(sanitizable);
|
||||
|
||||
// NULL pointers are handled
|
||||
XCTAssertFalse(SanitizableString::SanitizeString(NULL).has_value());
|
||||
|
||||
// Non-sanitized strings return std::nullopt
|
||||
XCTAssertEqual(std::nullopt, SanitizableString::SanitizeString(empty));
|
||||
XCTAssertEqual(std::nullopt, SanitizableString::SanitizeString(noSanitize));
|
||||
|
||||
// Intentional pointer compare to ensure the data member of the returned
|
||||
// string_view matches the original buffer when not sanitized, and not equal
|
||||
// when the string needs sanitization
|
||||
XCTAssertEqual(empty, SanitizableString(empty, emptyLen).Sanitized().data());
|
||||
XCTAssertEqual(noSanitize, SanitizableString(noSanitize, noSanitizeLen).Sanitized().data());
|
||||
XCTAssertNotEqual(sanitizable, SanitizableString(sanitizable, sanitizableLen).Sanitized().data());
|
||||
|
||||
// Ensure the `String` method always returns the unsanitized buffer
|
||||
XCTAssertEqual(empty, SanitizableString(empty, emptyLen).String().data());
|
||||
XCTAssertEqual(noSanitize, SanitizableString(noSanitize, noSanitizeLen).String().data());
|
||||
XCTAssertEqual(sanitizable, SanitizableString(sanitizable, sanitizableLen).String().data());
|
||||
|
||||
XCTAssertCStringEqual(SanitizableString(@"|").Sanitized().data(), "<pipe>");
|
||||
XCTAssertCStringEqual(SanitizableString(@"\n").Sanitized().data(), "\\n");
|
||||
XCTAssertCStringEqual(SanitizableString(@"\r").Sanitized().data(), "\\r");
|
||||
|
||||
XCTAssertCStringEqual(SanitizableString(@"a\nb\rc|").Sanitized().data(), "a\\nb\\rc<pipe>");
|
||||
XCTAssertCStringEqual(SanitizableString(@"a|trail").Sanitized().data(), "a<pipe>trail");
|
||||
|
||||
// Handle some long strings
|
||||
NSString *base = [NSString stringWithFormat:@"%@|abc", [@"" stringByPaddingToLength:66 * 1024
|
||||
withString:@"A"
|
||||
startingAtIndex:0]];
|
||||
|
||||
NSString *want = [NSString stringWithFormat:@"%@<pipe>abc", [@"" stringByPaddingToLength:66 * 1024
|
||||
withString:@"A"
|
||||
startingAtIndex:0]];
|
||||
|
||||
XCTAssertCStringEqual(SanitizableString(base).Sanitized().data(), [want UTF8String]);
|
||||
}
|
||||
|
||||
- (void)testStream {
|
||||
// Test that using the `<<` operator will sanitize the string
|
||||
std::ostringstream ss;
|
||||
const char *sanitizable = "sani|tizable";
|
||||
const char *sanitized = "sani<pipe>tizable";
|
||||
es_string_token_t tok = {.length = strlen(sanitizable), .data = sanitizable};
|
||||
|
||||
ss << SanitizableString(tok);
|
||||
|
||||
XCTAssertCStringEqual(ss.str().c_str(), sanitized);
|
||||
}
|
||||
|
||||
@end
|
||||
87
Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h
Normal file
87
Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h
Normal file
@@ -0,0 +1,87 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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__LOGS_ENDPOINTSECURITY_SERIALIZERS_SERIALIZER_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SERIALIZER_H
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
|
||||
@class SNTStoredEvent;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
class Serializer {
|
||||
public:
|
||||
virtual ~Serializer() = default;
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg) {
|
||||
return std::visit([this](const auto &arg) { return this->SerializeMessageTemplate(arg); },
|
||||
msg->GetEnrichedMessage());
|
||||
}
|
||||
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) = 0;
|
||||
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;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedFork &) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedLink &) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) = 0;
|
||||
virtual std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) = 0;
|
||||
|
||||
virtual std::vector<uint8_t> SerializeAllowlist(
|
||||
const santa::santad::event_providers::endpoint_security::Message &, const std::string_view) = 0;
|
||||
|
||||
virtual std::vector<uint8_t> SerializeBundleHashingEvent(SNTStoredEvent *) = 0;
|
||||
|
||||
virtual std::vector<uint8_t> SerializeDiskAppeared(NSDictionary *) = 0;
|
||||
virtual std::vector<uint8_t> SerializeDiskDisappeared(NSDictionary *) = 0;
|
||||
|
||||
private:
|
||||
// Template methods used to ensure a place to implement any desired
|
||||
// functionality that shouldn't be overridden by derived classes.
|
||||
std::vector<uint8_t> SerializeMessageTemplate(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &);
|
||||
std::vector<uint8_t> SerializeMessageTemplate(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExchange &);
|
||||
std::vector<uint8_t> SerializeMessageTemplate(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &);
|
||||
std::vector<uint8_t> SerializeMessageTemplate(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &);
|
||||
std::vector<uint8_t> SerializeMessageTemplate(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedFork &);
|
||||
std::vector<uint8_t> SerializeMessageTemplate(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedLink &);
|
||||
std::vector<uint8_t> SerializeMessageTemplate(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &);
|
||||
std::vector<uint8_t> SerializeMessageTemplate(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &);
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,58 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
namespace es = santa::santad::event_providers::endpoint_security;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedClose &msg) {
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExchange &msg) {
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExec &msg) {
|
||||
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]
|
||||
resetTimestampForCachedDecision:msg.es_msg().event.exec.target->executable->stat];
|
||||
}
|
||||
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedExit &msg) {
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedFork &msg) {
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedLink &msg) {
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedRename &msg) {
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedUnlink &msg) {
|
||||
return SerializeMessage(msg);
|
||||
}
|
||||
|
||||
}; // namespace santa::santad::logs::endpoint_security::serializers
|
||||
64
Source/santad/Logs/EndpointSecurity/Writers/File.h
Normal file
64
Source/santad/Logs/EndpointSecurity/Writers/File.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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__LOGS_ENDPOINTSECURITY_WRITERS_FILE_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_FILE_H
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h"
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
// Forward declarations
|
||||
namespace santa::santad::logs::endpoint_security::writers {
|
||||
class FilePeer;
|
||||
}
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::writers {
|
||||
|
||||
class File : public Writer, public std::enable_shared_from_this<File> {
|
||||
public:
|
||||
// Factory
|
||||
static std::shared_ptr<File> Create(NSString *path, uint64_t flush_timeout_ms,
|
||||
size_t batch_size_bytes,
|
||||
size_t max_expected_write_size_bytes);
|
||||
|
||||
File(NSString *path, size_t batch_size_bytes, size_t max_expected_write_size_bytes,
|
||||
dispatch_queue_t q, dispatch_source_t timer_source);
|
||||
~File();
|
||||
|
||||
void Write(std::vector<uint8_t> &&bytes) override;
|
||||
|
||||
friend class santa::santad::logs::endpoint_security::writers::FilePeer;
|
||||
|
||||
private:
|
||||
void OpenFileHandle();
|
||||
void WatchLogFile();
|
||||
void FlushBuffer();
|
||||
|
||||
std::vector<uint8_t> buffer_;
|
||||
size_t batch_size_bytes_;
|
||||
dispatch_queue_t q_;
|
||||
dispatch_source_t timer_source_;
|
||||
dispatch_source_t watch_source_;
|
||||
NSString *path_;
|
||||
NSFileHandle *file_handle_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
#endif
|
||||
115
Source/santad/Logs/EndpointSecurity/Writers/File.mm
Normal file
115
Source/santad/Logs/EndpointSecurity/Writers/File.mm
Normal file
@@ -0,0 +1,115 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/Logs/EndpointSecurity/Writers/File.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::writers {
|
||||
|
||||
std::shared_ptr<File> File::Create(NSString *path, uint64_t flush_timeout_ms,
|
||||
size_t batch_size_bytes, size_t max_expected_write_size_bytes) {
|
||||
dispatch_queue_t q = dispatch_queue_create("com.google.santa.daemon.file_event_log",
|
||||
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_MSEC * flush_timeout_ms, 0);
|
||||
|
||||
auto ret_writer =
|
||||
std::make_shared<File>(path, batch_size_bytes, max_expected_write_size_bytes, q, timer_source);
|
||||
ret_writer->WatchLogFile();
|
||||
|
||||
std::weak_ptr<File> weak_writer(ret_writer);
|
||||
dispatch_source_set_event_handler(ret_writer->timer_source_, ^{
|
||||
std::shared_ptr<File> shared_writer = weak_writer.lock();
|
||||
if (!shared_writer) {
|
||||
return;
|
||||
}
|
||||
shared_writer->FlushBuffer();
|
||||
});
|
||||
|
||||
dispatch_resume(ret_writer->timer_source_);
|
||||
|
||||
return ret_writer;
|
||||
}
|
||||
|
||||
File::File(NSString *path, size_t batch_size_bytes, size_t max_expected_write_size_bytes,
|
||||
dispatch_queue_t q, dispatch_source_t timer_source)
|
||||
: batch_size_bytes_(batch_size_bytes),
|
||||
q_(q),
|
||||
timer_source_(timer_source),
|
||||
watch_source_(nullptr) {
|
||||
path_ = path;
|
||||
buffer_.reserve(batch_size_bytes + max_expected_write_size_bytes);
|
||||
OpenFileHandle();
|
||||
}
|
||||
|
||||
void File::WatchLogFile() {
|
||||
if (watch_source_) {
|
||||
dispatch_source_cancel(watch_source_);
|
||||
}
|
||||
|
||||
watch_source_ = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, file_handle_.fileDescriptor,
|
||||
DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME, q_);
|
||||
|
||||
auto shared_this = shared_from_this();
|
||||
dispatch_source_set_event_handler(watch_source_, ^{
|
||||
[shared_this->file_handle_ closeFile];
|
||||
shared_this->OpenFileHandle();
|
||||
shared_this->WatchLogFile();
|
||||
});
|
||||
|
||||
dispatch_resume(watch_source_);
|
||||
}
|
||||
|
||||
File::~File() {
|
||||
if (timer_source_) {
|
||||
dispatch_source_cancel(timer_source_);
|
||||
}
|
||||
}
|
||||
|
||||
// IMPORTANT: Not thread safe.
|
||||
void File::OpenFileHandle() {
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
if (![fm fileExistsAtPath:path_]) {
|
||||
[fm createFileAtPath:path_ contents:nil attributes:nil];
|
||||
}
|
||||
file_handle_ = [NSFileHandle fileHandleForWritingAtPath:path_];
|
||||
[file_handle_ seekToEndOfFile];
|
||||
}
|
||||
|
||||
void File::Write(std::vector<uint8_t> &&bytes) {
|
||||
auto shared_this = shared_from_this();
|
||||
|
||||
// Workaround to move `bytes` into the block without a copy
|
||||
__block std::vector<uint8_t> temp_bytes = std::move(bytes);
|
||||
|
||||
dispatch_async(q_, ^{
|
||||
std::vector<uint8_t> moved_bytes = std::move(temp_bytes);
|
||||
|
||||
shared_this->buffer_.insert(shared_this->buffer_.end(), moved_bytes.begin(), moved_bytes.end());
|
||||
if (shared_this->buffer_.size() >= batch_size_bytes_) {
|
||||
shared_this->FlushBuffer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// IMPORTANT: Not thread safe.
|
||||
void File::FlushBuffer() {
|
||||
write(file_handle_.fileDescriptor, buffer_.data(), buffer_.size());
|
||||
buffer_.clear();
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
179
Source/santad/Logs/EndpointSecurity/Writers/FileTest.mm
Normal file
179
Source/santad/Logs/EndpointSecurity/Writers/FileTest.mm
Normal file
@@ -0,0 +1,179 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#import "Source/santad/Logs/EndpointSecurity/Writers/File.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::writers {
|
||||
|
||||
class FilePeer : public File {
|
||||
public:
|
||||
// Make constructors visible
|
||||
using File::File;
|
||||
|
||||
NSFileHandle *FileHandle() { return file_handle_; }
|
||||
|
||||
void BeginWatchingLogFile() { WatchLogFile(); }
|
||||
|
||||
size_t InternalBufferSize() { return buffer_.size(); }
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
using santa::santad::logs::endpoint_security::writers::FilePeer;
|
||||
|
||||
bool WaitFor(bool (^condition)(void)) {
|
||||
int attempts = 0;
|
||||
long sleepPerAttemptMS = 10; // Wait 10ms between checks
|
||||
long maxSleep = 2000; // Wait up to 2 seconds for new log file to be created
|
||||
long maxAttempts = maxSleep / sleepPerAttemptMS;
|
||||
|
||||
do {
|
||||
SleepMS(sleepPerAttemptMS);
|
||||
|
||||
// Break out once the condition holds
|
||||
if (condition()) {
|
||||
break;
|
||||
}
|
||||
} while (++attempts < maxAttempts);
|
||||
|
||||
return attempts < maxAttempts;
|
||||
}
|
||||
|
||||
bool WaitForNewLogFile(NSFileManager *fileManager, NSString *path) {
|
||||
return WaitFor(^bool() {
|
||||
return [fileManager fileExistsAtPath:path];
|
||||
});
|
||||
}
|
||||
|
||||
bool WaitForBufferSize(std::shared_ptr<FilePeer> file, size_t expectedSize) {
|
||||
return WaitFor(^bool() {
|
||||
return file->InternalBufferSize() == expectedSize;
|
||||
});
|
||||
}
|
||||
|
||||
@interface FileTest : XCTestCase
|
||||
@property NSString *path;
|
||||
@property NSString *logPath;
|
||||
@property NSString *logRenamePath;
|
||||
@property dispatch_queue_t q;
|
||||
@property dispatch_source_t timer;
|
||||
@property NSFileManager *fileManager;
|
||||
@end
|
||||
|
||||
@implementation FileTest
|
||||
|
||||
- (void)setUp {
|
||||
self.path = [NSString stringWithFormat:@"%@santa-%d", NSTemporaryDirectory(), getpid()];
|
||||
|
||||
self.logPath = [NSString stringWithFormat:@"%@/log.out", self.path];
|
||||
self.logRenamePath = [NSString stringWithFormat:@"%@/log.rename.out", self.path];
|
||||
|
||||
self.fileManager = [NSFileManager defaultManager];
|
||||
|
||||
XCTAssertTrue([self.fileManager createDirectoryAtPath:self.path
|
||||
withIntermediateDirectories:YES
|
||||
attributes:nil
|
||||
error:nil]);
|
||||
|
||||
self.q = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
|
||||
XCTAssertNotNil(self.q);
|
||||
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
|
||||
XCTAssertNotNil(self.timer);
|
||||
|
||||
// Resume the timer to ensure its not inadvertently cancelled first
|
||||
dispatch_resume(self.timer);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.fileManager removeItemAtPath:self.path error:nil];
|
||||
}
|
||||
|
||||
- (void)testWatchLogFile {
|
||||
auto file = std::make_shared<FilePeer>(self.logPath, 100, 500, self.q, self.timer);
|
||||
file->BeginWatchingLogFile();
|
||||
|
||||
// Constructing a File object will open the file at the given path
|
||||
struct stat wantSBOrig;
|
||||
struct stat gotSBOrig;
|
||||
XCTAssertEqual(stat([self.logPath UTF8String], &wantSBOrig), 0);
|
||||
XCTAssertEqual(fstat(file->FileHandle().fileDescriptor, &gotSBOrig), 0);
|
||||
XCTAssertEqual(wantSBOrig.st_ino, gotSBOrig.st_ino);
|
||||
|
||||
// Deleting the current log file will cause a new file to be created
|
||||
XCTAssertTrue([self.fileManager removeItemAtPath:self.logPath error:nil]);
|
||||
|
||||
XCTAssertTrue(WaitForNewLogFile(self.fileManager, self.logPath),
|
||||
"New log file not created within expected time after deletion");
|
||||
|
||||
struct stat wantSBAfterDelete;
|
||||
struct stat gotSBAfterDelete;
|
||||
XCTAssertEqual(stat([self.logPath UTF8String], &wantSBAfterDelete), 0);
|
||||
XCTAssertEqual(fstat(file->FileHandle().fileDescriptor, &gotSBAfterDelete), 0);
|
||||
|
||||
XCTAssertEqual(wantSBAfterDelete.st_ino, gotSBAfterDelete.st_ino);
|
||||
XCTAssertNotEqual(wantSBOrig.st_ino, wantSBAfterDelete.st_ino);
|
||||
|
||||
// Renaming the current log file will cause a new file to be created
|
||||
XCTAssertTrue([self.fileManager moveItemAtPath:self.logPath toPath:self.logRenamePath error:nil]);
|
||||
|
||||
XCTAssertTrue(WaitForNewLogFile(self.fileManager, self.logPath),
|
||||
"New log file not created within expected time after rename");
|
||||
|
||||
struct stat wantSBAfterRename;
|
||||
struct stat gotSBAfterRename;
|
||||
XCTAssertEqual(stat([self.logPath UTF8String], &wantSBAfterRename), 0);
|
||||
XCTAssertEqual(fstat(file->FileHandle().fileDescriptor, &gotSBAfterRename), 0);
|
||||
|
||||
XCTAssertEqual(wantSBAfterRename.st_ino, gotSBAfterRename.st_ino);
|
||||
XCTAssertNotEqual(wantSBAfterDelete.st_ino, wantSBAfterRename.st_ino);
|
||||
}
|
||||
|
||||
- (void)testWrite {
|
||||
// Start with empty file. Perform two writes. The first will only go into the
|
||||
// internal buffer. The second will meet/exceed capacity and flush to disk
|
||||
size_t bufferSize = 100;
|
||||
size_t writeSize = 50;
|
||||
auto file =
|
||||
std::make_shared<FilePeer>(self.logPath, bufferSize, bufferSize * 2, self.q, self.timer);
|
||||
|
||||
// Starting out, file size and internal buffer are 0
|
||||
struct stat gotSB;
|
||||
XCTAssertEqual(fstat(file->FileHandle().fileDescriptor, &gotSB), 0);
|
||||
XCTAssertEqual(0, gotSB.st_size);
|
||||
XCTAssertEqual(0, file->InternalBufferSize());
|
||||
|
||||
// After the first write, the buffer is 50 bytes, but the file is still 0
|
||||
file->Write(std::vector<uint8_t>(writeSize, 'A'));
|
||||
WaitForBufferSize(file, 50);
|
||||
XCTAssertEqual(fstat(file->FileHandle().fileDescriptor, &gotSB), 0);
|
||||
XCTAssertEqual(0, gotSB.st_size);
|
||||
XCTAssertEqual(50, file->InternalBufferSize());
|
||||
|
||||
// After the second write, the buffer is flushed. File size 100, buffer is 0.
|
||||
file->Write(std::vector<uint8_t>(writeSize, 'B'));
|
||||
WaitForBufferSize(file, 0);
|
||||
XCTAssertEqual(fstat(file->FileHandle().fileDescriptor, &gotSB), 0);
|
||||
XCTAssertEqual(100, gotSB.st_size);
|
||||
XCTAssertEqual(0, file->InternalBufferSize());
|
||||
}
|
||||
|
||||
@end
|
||||
35
Source/santad/Logs/EndpointSecurity/Writers/Null.h
Normal file
35
Source/santad/Logs/EndpointSecurity/Writers/Null.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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__LOGS_ENDPOINTSECURITY_WRITERS_NULL_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_NULL_H
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::writers {
|
||||
|
||||
class Null : public Writer {
|
||||
public:
|
||||
// Factory
|
||||
static std::shared_ptr<Null> Create();
|
||||
|
||||
void Write(std::vector<uint8_t>&& bytes) override;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
#endif
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,10 +12,16 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/Logs/SNTEventLog.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Null.h"
|
||||
|
||||
@interface SNTProtobufEventLog : SNTEventLog
|
||||
namespace santa::santad::logs::endpoint_security::writers {
|
||||
|
||||
- (void)logFileModification:(santa_message_t)message;
|
||||
std::shared_ptr<Null> Null::Create() {
|
||||
return std::make_shared<Null>();
|
||||
}
|
||||
|
||||
@end
|
||||
void Null::Write(std::vector<uint8_t> &&bytes) {
|
||||
// Intentionally do nothing
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
33
Source/santad/Logs/EndpointSecurity/Writers/Syslog.h
Normal file
33
Source/santad/Logs/EndpointSecurity/Writers/Syslog.h
Normal file
@@ -0,0 +1,33 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// http://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__LOGS_ENDPOINTSECURITY_WRITERS_SYSLOG_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_SYSLOG_H
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::writers {
|
||||
|
||||
class Syslog : public Writer {
|
||||
public:
|
||||
static std::shared_ptr<Syslog> Create();
|
||||
|
||||
void Write(std::vector<uint8_t>&& bytes) override;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
#endif
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,9 +12,18 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Syslog.h"
|
||||
|
||||
#include "Source/santad/EventProviders/SNTEndpointSecurityManager.h"
|
||||
#include <os/log.h>
|
||||
|
||||
@interface SNTCachingEndpointSecurityManager : SNTEndpointSecurityManager
|
||||
@end
|
||||
namespace santa::santad::logs::endpoint_security::writers {
|
||||
|
||||
std::shared_ptr<Syslog> Syslog::Create() {
|
||||
return std::make_shared<Syslog>();
|
||||
}
|
||||
|
||||
void Syslog::Write(std::vector<uint8_t> &&bytes) {
|
||||
os_log(OS_LOG_DEFAULT, "%{public}s", bytes.data());
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,16 +12,20 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_WRITER_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_WRITER_H
|
||||
|
||||
///
|
||||
/// The main controller class for santad
|
||||
///
|
||||
@interface SNTApplication : NSObject
|
||||
#include <vector>
|
||||
|
||||
///
|
||||
/// Begins fielding requests from the driver
|
||||
///
|
||||
- (void)start;
|
||||
namespace santa::santad::logs::endpoint_security::writers {
|
||||
|
||||
@end
|
||||
class Writer {
|
||||
public:
|
||||
virtual ~Writer() = default;
|
||||
|
||||
virtual void Write(std::vector<uint8_t>&& bytes) = 0;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::writers
|
||||
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user