Compare commits

..

11 Commits

Author SHA1 Message Date
Matt W
72969a3c92 Fix crash flushing cache on unmount events (#895) 2022-09-27 21:54:35 -04:00
Matt W
d2dbed78dd Return a value from the test block (#894) 2022-09-27 15:07:20 -04:00
Matt W
8fa91e4ff0 Build deps (#893)
* Too bad we can't require explicit build deps...

* More deps
2022-09-23 13:55:48 -04:00
Matt W
551763146d Linter and BUILD deps fixups (#892)
* Minor changes to address lint issues

* Add more BUILD deps

* Include cleanup

* Even more BUILD deps

* Still more BUILD deps
2022-09-23 11:18:58 -04:00
Matt W
7a7f0cd5a8 Ingestion fixups (#891) 2022-09-22 12:30:34 -04:00
Matt W
fcb49701b3 ES and Logging Interfaces Redesign (#888)
* Initial structure for ES wrappers, enriched types, logging

* Basic working ES and logging functionality

* Add in oneTBB and thread-safe-lru deps

* Added a bunch of enriched types

* Auto-mute self when establishing ES client

* Basic auth, tamper client. Syslog of all events. Basic compiler tracking.

* Update copyright header blobs, convert some tabs to spaces

* Auth result cache. Fix getting translocation path.

* Added remaining cache methods

* Add AuthResultCache to Recorder client. Cache now operates on es_file_t.

* Hooked up SNTPrefixTree

* Fix CompilerController for RENAME. Fix AllowList logging missing path.

* Block loading Santa kext

* Added device manager client

* Properly log DiskAppear events

* Fix build to adopt new adhoc build

* Handle clearing cache on UNMOUNT events

* Ignore other ES clients if configured

* Remove SNTAllowlistInfo. Rename AllowList to Allowlist. Minor cleanup.

* Recorder now logs asynchronously. Enricher now returns shared_ptrs.

* Added File writer. Added timestamps to BasicStream serializer.

* Skip calling stat in SNTFileInfo when path given by ES.

* Fix build issue

* Address draft PR feedback

* santactl integrated, XPC works, fix file writer bug

* Integrate syncservice. Start observing some config changes.

* Add metrics service wrapper

* Add metrics config observers and metrics interval reset.

* Start better dependency control. Add Null logger support.

* Added more deps

* Added more deps

* Fix issue where metric service wasn't starting

* Add missing variant include

* Fix missing parent proc name

* Added googletest and new unit test macro

* Started expanding AuthResultCacheTest

* Properly mock EndpointSecurityAPI

* Finished AuthResultCacheTest

* bazelrc now builds all C++ as C++17. Added LoggerTest.

* Add FileTest. Abstract some File constants to Logger.

* Added Empty serializer test

* Started work on BasicStringTest. Fixed some BasicString serialization bugs.

* Added Unlink BasicString serialization test

* Added some more tests. Commonized some test code

* Finished BasicStringTest. Converted to XCTest.

* Standardize esapi variable naming

* Bubble up gTest expect failures to XCTest failures

* AuthResultCacheTest now uses XCTest. Added common TestUtils.h

* EmptyTest now uses XCTest.

* FileTest now uses XCTest

* LoggerTest now uses XCTest. Removed santa_unit_gtest bazel macro.

* Added ClientTest

* Add basic Enricher tests

* Add MessageTest. Make more TestUtils.

* Rename metrics to Metrics

* Add MetricsTest.

* Apply template pattern to Serializer

* Add SNTDecisionCacheTest.

* Add SNTCachedDecisionTest.

* Testing with coveralls debug mode

* Allow manual CI runs

* Remove unused property

* Started work on SNTEndpointSecurityClientTest.

* WIP SNTEndpointSecurityClientTest, fix test run issue

* Added more base ES client tests

* Add more base ES client tests

* Base ES client tests done. Added serializer utils/tests. Expanded basic string tests.

* Add utils test to test suite

* Add copy ctor. Add test output to bazel coverage.

* Single thread bazel coverage

* Updaload coverage file

* Updaload coverage file

* Old gen cov test

* Restructure message handlers to enable better testability

* Added enable tests for all ES clients

* Made a single MockEndpointSecurityAPI class to share everywhere

* Added most of SNTCompilerControllerTest

* Cleanup SNTCompilerControllerTest

* Started expanding Auth client test

* Finished up the Authorizer tests

* Move to using enum class for notify/auth instead of bool

* WIP for tamper resistance test. ASAN issues.

* Add OCMock patch to fix test issue on ARM Macs

* Changed patches directory name to external_patches

* Update WORKSPACE path

* Finished up Tamper Resistance tests

* Finished up Recorder tests.

* Move SNTExecutionControllerTest to ObjC++

* Initial work to port SNTExecutionControllerTest

* Finished porting SNTExecutionControllerTest.

* Added SNTExecutionControllerTest to list of unit tests

* Ported SNTEndpointSecurityDeviceManager.

* Test cleanup, use MockESAPI expectation helpers

* Verify SNTEndpointSecurityDeviceManager expectations differently

* Test cleanup, omit gTest param list where unused

* Log message cleanup

* Rename SNTApplicationTest to santad_test.mm

* Finished porting santad_test, formerly SNTApplicationTest

* Fix SNTEndpointSecurityDeviceManager issues

* Pulled in missed fixes. Updated tests.

* Renamed lowercase filenames to match rest of codebase

* Fix non-static dispatch_once_t, and noisy watching compiler log message

* WIP Started process of removing components no longer used

* WIP Continued process of removing components no longer used

* BUILD file cleanup. Proto warning. Removed unused global

* Rename SNTEventProvider to SNTEndpointSecurityEventHandler

* Rename SNTEndpointSecurityEventHandler protocol

* Remove EnableSysxCache option. Remove --quick flag used during dev.

* Ran testing/fix.sh

* Addmissing param to fix.sh that was omitting .mm files.

* clang-format

* Fix linter: find cmd missing .mm ext, git grep exclude patch files.

* Use MakeESProcess default params in tests

* Move variables to camelCase in objc classes

* More case changes

* Sanitize strings

* Change dispatch queue priorities and standardize daemon queue naming

* Exclude patch files in markdown check

* Ensure string log messages end with newline

* Fix BasicStringTest

* Disable clang-format in code producing different results in local/remote versions

* Moved to using date ranges in copyright notices as per current guidelines

* Update Source/common/SNTConfigurator.h

Suggestion adding whitespace in comment to fix clang-format mangling

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>

* Removed santa_panic macro used in one place

* Updated comment about ES cachability

* Pin oneTBB to specific commit

* Address outstanding WORKSPACE 'canonical reproducible form' messages

* Use string append instead of ostringstream due to benchmark results

* Remove use of freind classes in EnrichedTypes.h

* Added SNTKVOManager, removed observers from SNTConfigurator.

* Fixed SNTEndpointSecurityRecorderTest class name

* Reduce usage of the auto keyword

* Each SNTKVOManager instance now adds its own observer

* Replaced more auto keywords with real types.

* Remove leftover code coverage debugging from ci.yml

* Updated comment

* Memoize SNTFileInfo sha256. Reduce some cache sizes.

* Fix issue checking for translocated paths

* Use more performant NSURL creation method

* Fix lint issue

* Address PR feedback

* Use an array literal for kvo objects

* Fix some clang tidy and import issues

* Replace third party LRU cache with SantaCache for now

* Fix clang tidy issues

* Address PR feedback

* Fix comment typo

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

* Added todo for when we adopt macOS 13

Co-authored-by: Russell Hancox <russellhancox@users.noreply.github.com>
Co-authored-by: Pete Markowsky <pmarkowsky@users.noreply.github.com>
2022-09-22 10:18:41 -04:00
Russell Hancox
c9ef723fc5 Project: Update bazel and apple-rules (#887) 2022-08-29 17:52:27 -04:00
Pete Markowsky
dc6732ef04 Refactor the SNTApplicationTest unit tests to function correctly (#885)
* Refactor the SNTApplicationTest unit tests to function correctly.

The tests were originally written in a table style and were impacted by the lack of mocking the configurator. This caused issues with static rules to impact the unit tests.

Additionally added improved logging messages for critical binaries and a todo for macOS 13 unit tests.

Added goodbinary and rules.db test files to allstar's ignored paths.
2022-08-29 13:18:04 -04:00
Russell Hancox
a48900a4ae Allstar: Pre-emptively check-in binary_artifacts.yaml to exclude test binaries (#884) 2022-08-25 09:32:43 -04:00
Russell Hancox
bb49118d94 README: Try again, this time replacing the correct bit (#883) 2022-08-24 16:26:30 -04:00
Russell Hancox
456333d6d2 README: Fix logo link, remove coverage badge (#882) 2022-08-24 16:22:37 -04:00
151 changed files with 10370 additions and 6356 deletions

View 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

View File

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

View File

@@ -1 +1 @@
5.0.0
5.3.0

View File

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

View File

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

@@ -198,10 +198,3 @@ test_suite(
"//Source/santasyncservice:unit_tests",
],
)
test_suite(
name = "benchmarks",
tests = [
"//Source/santad:SNTApplicationBenchmark",
],
)

View File

@@ -1,7 +1,7 @@
# Santa [![CI](https://github.com/google/santa/actions/workflows/ci.yml/badge.svg)](https://github.com/google/santa/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/google/santa/badge.svg?branch=main)](https://coveralls.io/github/google/santa?branch=main)
# Santa [![CI](https://github.com/google/santa/actions/workflows/ci.yml/badge.svg)](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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,6 @@ objc_library(
"//:opt_build": [],
"//conditions:default": [
"Commands/SNTCommandBundleInfo.m",
"Commands/SNTCommandCacheHistogram.m",
"Commands/SNTCommandCheckCache.m",
"Commands/SNTCommandFlushCache.m",
],

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View 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

View 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

View 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

View File

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

View 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

View 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

View File

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

View 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.
#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

View 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

View 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

View 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

View File

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

View 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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

View 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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View 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

View 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.
#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

View File

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

View File

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

View File

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

View 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

View 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

View 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

View File

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

View 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

View File

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

View 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

View 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

View 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

View File

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

View File

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

View File

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

View 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

View File

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

View 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

View 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

View 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

View 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

View File

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

View 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

View File

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

View File

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