Compare commits

...

26 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
Pete Markowsky
fd23a5c3b7 Fix up endTimestamp to be Monarch compliant (#879)
Fix up endTimestamp field to be Monarch compliant.
2022-08-16 22:32:29 -04:00
Russell Hancox
ec203e8796 Project: Rename Source/santa -> Source/gui (#877) 2022-08-12 14:19:01 -04:00
Russell Hancox
57ff69208d GUI: Missed a required dependency (#876) 2022-08-12 14:02:22 -04:00
Russell Hancox
f00b7d2ded GUI: Expose SNTNotificationManager.h for the test. (#875) 2022-08-12 13:46:25 -04:00
Russell Hancox
9791fdd53c Project: Add a GH action to prevent trailing whitespace (#873) 2022-08-12 12:46:11 -04:00
Russell Hancox
26e2203f1e GUI: Improve signing chain key reporting in distributed notifications. (#874)
Also add a group for GUI unit_tests and include in the overall project tests group.
2022-08-12 11:03:21 -04:00
Russell Hancox
4a47195d12 Santa: Post distributed notification when showing block UI (#870)
Fixes #869
2022-08-11 12:34:35 -04:00
Russell Hancox
4436e221df GUI: Add silent mode configuration option. (#871)
When enabled, this option disables *all* GUI notifications from Santa. This is intended for kiosk-style machines where it is not expected for users to _ever_ execute unknown binaries.

Fixes #862
2022-08-11 09:17:07 -04:00
Russell Hancox
deccc8a148 GUI: For App Store published apps, include team ID. (#872)
With this change, the publisher field for an App Store published app will be  instead of

Fixes #758
2022-08-11 08:15:42 -04:00
Henry S
06da796a4d Docs: add link to GitHub (#868) 2022-08-08 16:38:34 -04:00
Russell Hancox
7b99a76d0d Docs: Add StaticRules to example mobileconfig (#866) 2022-08-03 10:59:18 -04:00
Pete Markowsky
c2d3e99446 Sync Protocol Docs (#860)
Initial commit of sync protocol docs.
2022-07-28 17:27:43 -04:00
Russell Hancox
6db7fea8ae syncservice: Add tests for NSData+Zlib and Postflight (#864) 2022-07-26 13:05:35 -04:00
Kathryn Hancox
6fcb4cfe63 Docs: Add recommended rollout doc (#861) 2022-07-22 13:50:25 -04:00
bfreezy
8b55ee4da5 santad: only allow root read+write permissions on sync-state.plist (#858) 2022-07-18 13:32:08 -04:00
203 changed files with 11235 additions and 6425 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

@@ -1,13 +1,14 @@
name: Check Markdown links
name: Check Markdown
on:
on:
pull_request:
paths:
- "**.md"
jobs:
markdown-link-check:
markdown-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: gaurav-nelson/github-action-markdown-link-check@v1
- run: "! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'"

View File

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

14
BUILD
View File

@@ -74,14 +74,14 @@ launchctl load /Library/LaunchAgents/com.google.santa.plist
run_command(
name = "reload",
srcs = [
"//Source/santa:Santa",
"//Source/gui:Santa",
],
cmd = """
set -e
rm -rf /tmp/bazel_santa_reload
unzip -d /tmp/bazel_santa_reload \
$${BUILD_WORKSPACE_DIRECTORY}/bazel-out/*$(COMPILATION_MODE)*/bin/Source/santa/Santa.zip >/dev/null
$${BUILD_WORKSPACE_DIRECTORY}/bazel-out/*$(COMPILATION_MODE)*/bin/Source/gui/Santa.zip >/dev/null
echo "You may be asked for your password for sudo"
sudo BINARIES=/tmp/bazel_santa_reload CONF=$${BUILD_WORKSPACE_DIRECTORY}/Conf \
$${BUILD_WORKSPACE_DIRECTORY}/Conf/install.sh
@@ -96,7 +96,7 @@ echo "Time to stop being naughty"
genrule(
name = "release",
srcs = [
"//Source/santa:Santa",
"//Source/gui:Santa",
"Conf/install.sh",
"Conf/uninstall.sh",
"Conf/com.google.santa.bundleservice.plist",
@@ -191,16 +191,10 @@ test_suite(
name = "unit_tests",
tests = [
"//Source/common:unit_tests",
"//Source/gui:unit_tests",
"//Source/santactl:unit_tests",
"//Source/santad:unit_tests",
"//Source/santametricservice:unit_tests",
"//Source/santasyncservice:unit_tests",
],
)
test_suite(
name = "benchmarks",
tests = [
"//Source/santad:SNTApplicationBenchmark",
],
)

View File

@@ -1,13 +1,13 @@
# 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
extension that monitors for executions, a daemon that makes execution decisions
Santa is a binary authorization system for macOS. It consists of a system
extension that monitors for executions, a daemon that makes execution decisions
based on the contents of a local database, a GUI agent that notifies the user in
case of a block decision and a command-line utility for managing the system and
case of a block decision and a command-line utility for managing the system and
synchronizing the database with a server.
It is named Santa because it keeps track of binaries that are naughty or nice.

View File

@@ -1,7 +1,7 @@
# Reporting a Vulnerability
If you believe you have found a security vulnerability, we would appreciate private disclosure
so that we can work on a fix before disclosure. Any vulnerabilities reported to us will be
so that we can work on a fix before disclosure. Any vulnerabilities reported to us will be
disclosed publicly either when a new version with fixes is released or 90 days has passed,
whichever comes first.

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,17 +245,18 @@
///
@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
///
/// When silent mode is enabled, Santa will never show notifications for
/// blocked processes.
///
/// This can be a very confusing experience for users, use with caution.
///
/// Defaults to NO.
///
@property(readonly, nonatomic) BOOL enableSilentMode;
///
/// The text to display when opening Santa.app.
/// If unset, the default text will be displayed.

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
@@ -66,7 +67,8 @@ static NSString *const kMachineOwnerPlistKeyKey = @"MachineOwnerKey";
static NSString *const kMachineIDPlistFileKey = @"MachineIDPlist";
static NSString *const kMachineIDPlistKeyKey = @"MachineIDKey";
static NSString *const kAboutText = @"AboutText";
static NSString *const kEnableSilentModeKey = @"EnableSilentMode";
static NSString *const kAboutTextKey = @"AboutText";
static NSString *const kMoreInfoURLKey = @"MoreInfoURL";
static NSString *const kEventDetailURLKey = @"EventDetailURL";
static NSString *const kEventDetailTextKey = @"EventDetailText";
@@ -93,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";
@@ -172,7 +172,8 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kRemountUSBModeKey : array,
kEnablePageZeroProtectionKey : number,
kEnableBadSignatureProtectionKey : number,
kAboutText : string,
kEnableSilentModeKey : string,
kAboutTextKey : string,
kMoreInfoURLKey : string,
kEventDetailURLKey : string,
kEventDetailTextKey : string,
@@ -204,7 +205,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kMailDirectorySizeThresholdMB : number,
kMailDirectoryEventMaxFlushTimeSec : number,
kEnableMachineIDDecoration : number,
kEnableSysxCache : number,
kEnableForkAndExitLogging : number,
kIgnoreOtherEndpointSecurityClients : number,
kEnableDebugLogging : number,
@@ -303,6 +303,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingEnableSilentMode {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingAboutText {
return [self configStateSet];
}
@@ -419,10 +423,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self syncAndConfigStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingEnableSysxCache {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingEnableForkAndExitLogging {
return [self configStateSet];
}
@@ -611,8 +611,13 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return number ? [number boolValue] : NO;
}
- (BOOL)enableSilentMode {
NSNumber *number = self.configState[kEnableSilentModeKey];
return number ? [number boolValue] : NO;
}
- (NSString *)aboutText {
return self.configState[kAboutText];
return self.configState[kAboutTextKey];
}
- (NSURL *)moreInfoURL {
@@ -782,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;
@@ -959,7 +959,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
syncState[kAllowedPathRegexKey] = [syncState[kAllowedPathRegexKey] pattern];
syncState[kBlockedPathRegexKey] = [syncState[kBlockedPathRegexKey] pattern];
[syncState writeToFile:kSyncStateFilePath atomically:YES];
[[NSFileManager defaultManager] setAttributes:@{NSFilePosixPermissions : @0644}
[[NSFileManager defaultManager] setAttributes:@{NSFilePosixPermissions : @0600}
ofItemAtPath:kSyncStateFilePath
error:NULL];
}

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

@@ -1,4 +1,5 @@
load("@build_bazel_rules_apple//apple:macos.bzl", "macos_application")
load("//:helper.bzl", "santa_unit_test")
licenses(["notice"])
@@ -31,6 +32,9 @@ objc_library(
"SNTNotificationManager.m",
"main.m",
],
hdrs = [
"SNTNotificationManager.h",
],
data = [
"Resources/AboutWindow.xib",
"Resources/DeviceMessageWindow.xib",
@@ -49,6 +53,7 @@ objc_library(
"//Source/common:SNTLogging",
"//Source/common:SNTStoredEvent",
"//Source/common:SNTStrengthify",
"//Source/common:SNTSyncConstants",
"//Source/common:SNTXPCControlInterface",
"//Source/common:SNTXPCNotifierInterface",
"@MOLCertificate",
@@ -89,3 +94,26 @@ macos_application(
visibility = ["//:santa_package_group"],
deps = [":SantaGUI_lib"],
)
santa_unit_test(
name = "SNTNotificationManagerTest",
srcs = [
"SNTNotificationManagerTest.m",
],
sdk_frameworks = [
"Cocoa",
],
deps = [
":SantaGUI_lib",
"//Source/common:SNTStoredEvent",
"@OCMock",
],
)
test_suite(
name = "unit_tests",
tests = [
":SNTNotificationManagerTest",
],
visibility = ["//:santa_package_group"],
)

View File

@@ -12,7 +12,7 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santa/SNTAboutWindowController.h"
#import "Source/gui/SNTAboutWindowController.h"
#import "Source/common/SNTConfigurator.h"

View File

@@ -12,7 +12,7 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santa/SNTAccessibleTextField.h"
#import "Source/gui/SNTAccessibleTextField.h"
@implementation SNTAccessibleTextField

View File

@@ -12,7 +12,7 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santa/SNTAppDelegate.h"
#import "Source/gui/SNTAppDelegate.h"
#import <MOLXPCConnection/MOLXPCConnection.h>
@@ -20,8 +20,8 @@
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTStrengthify.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santa/SNTAboutWindowController.h"
#import "Source/santa/SNTNotificationManager.h"
#import "Source/gui/SNTAboutWindowController.h"
#import "Source/gui/SNTNotificationManager.h"
@interface SNTAppDelegate ()
@property SNTAboutWindowController *aboutWindowController;

View File

@@ -14,7 +14,7 @@
#import <Cocoa/Cocoa.h>
#import "Source/santa/SNTMessageWindowController.h"
#import "Source/gui/SNTMessageWindowController.h"
@class SNTStoredEvent;

View File

@@ -12,7 +12,7 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santa/SNTBinaryMessageWindowController.h"
#import "Source/gui/SNTBinaryMessageWindowController.h"
#import <MOLCertificate/MOLCertificate.h>
#import <SecurityInterface/SFCertificatePanel.h>
@@ -20,7 +20,7 @@
#import "Source/common/SNTBlockMessage.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTStoredEvent.h"
#import "Source/santa/SNTMessageWindow.h"
#import "Source/gui/SNTMessageWindow.h"
@interface SNTBinaryMessageWindowController ()
/// The custom message to display for this event
@@ -139,7 +139,9 @@
- (NSString *)publisherInfo {
MOLCertificate *leafCert = [self.event.signingChain firstObject];
if (leafCert.commonName && leafCert.orgName) {
if ([leafCert.commonName isEqualToString:@"Apple Mac OS Application Signing"]) {
return [NSString stringWithFormat:@"App Store (Team ID: %@)", self.event.teamID];
} else if (leafCert.commonName && leafCert.orgName) {
return [NSString stringWithFormat:@"%@ - %@", leafCert.orgName, leafCert.commonName];
} else if (leafCert.commonName) {
return leafCert.commonName;

View File

@@ -14,7 +14,7 @@
#import <Cocoa/Cocoa.h>
#import "Source/common/SNTDeviceEvent.h"
#import "Source/santa/SNTMessageWindowController.h"
#import "Source/gui/SNTMessageWindowController.h"
NS_ASSUME_NONNULL_BEGIN

View File

@@ -12,12 +12,12 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santa/SNTDeviceMessageWindowController.h"
#import "Source/gui/SNTDeviceMessageWindowController.h"
#import "Source/common/SNTBlockMessage.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTDeviceEvent.h"
#import "Source/santa/SNTMessageWindow.h"
#import "Source/gui/SNTMessageWindow.h"
NS_ASSUME_NONNULL_BEGIN

View File

@@ -12,7 +12,7 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santa/SNTMessageWindow.h"
#import "Source/gui/SNTMessageWindow.h"
@implementation SNTMessageWindow

View File

@@ -1,6 +1,6 @@
#import "Source/santa/SNTMessageWindowController.h"
#import "Source/gui/SNTMessageWindowController.h"
#import "Source/santa/SNTMessageWindow.h"
#import "Source/gui/SNTMessageWindow.h"
@implementation SNTMessageWindowController

View File

@@ -15,9 +15,9 @@
#import <Cocoa/Cocoa.h>
#import "Source/common/SNTXPCNotifierInterface.h"
#import "Source/santa/SNTBinaryMessageWindowController.h"
#import "Source/santa/SNTDeviceMessageWindowController.h"
#import "Source/santa/SNTMessageWindowController.h"
#import "Source/gui/SNTBinaryMessageWindowController.h"
#import "Source/gui/SNTDeviceMessageWindowController.h"
#import "Source/gui/SNTMessageWindowController.h"
///
/// Keeps track of pending notifications and ensures only one is presented to the user at a time.

View File

@@ -12,8 +12,9 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santa/SNTNotificationManager.h"
#import "Source/gui/SNTNotificationManager.h"
#import <MOLCertificate/MOLCertificate.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#import <UserNotifications/UserNotifications.h>
@@ -23,8 +24,9 @@
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTStoredEvent.h"
#import "Source/common/SNTStrengthify.h"
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santa/SNTMessageWindowController.h"
#import "Source/gui/SNTMessageWindowController.h"
@interface SNTNotificationManager ()
@@ -112,12 +114,59 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
pendingMsg.delegate = self;
[self.pendingNotifications addObject:pendingMsg];
[self postDistributedNotification:pendingMsg];
if (!self.currentWindowController) {
[self showQueuedWindow];
}
}
// For blocked execution notifications, post an NSDistributedNotificationCenter
// notification with the important details from the stored event. Distributed
// notifications are system-wide broadcasts that can be sent by apps and observed
// from separate processes. This allows users of Santa to write tools that
// perform actions when we block execution, such as trigger management tools or
// display an enterprise-specific UI (which is particularly useful when combined
// with the EnableSilentMode configuration option, to disable Santa's standard UI).
- (void)postDistributedNotification:(SNTMessageWindowController *)pendingMsg {
if (![pendingMsg isKindOfClass:[SNTBinaryMessageWindowController class]]) {
return;
}
SNTBinaryMessageWindowController *wc = (SNTBinaryMessageWindowController *)pendingMsg;
NSDistributedNotificationCenter *dc = [NSDistributedNotificationCenter defaultCenter];
NSMutableArray<NSDictionary *> *signingChain =
[NSMutableArray arrayWithCapacity:wc.event.signingChain.count];
for (MOLCertificate *cert in wc.event.signingChain) {
[signingChain addObject:@{
kCertSHA256 : cert.SHA256 ?: @"",
kCertCN : cert.commonName ?: @"",
kCertOrg : cert.orgName ?: @"",
kCertOU : cert.orgUnit ?: @"",
kCertValidFrom : @([cert.validFrom timeIntervalSince1970]) ?: @0,
kCertValidUntil : @([cert.validUntil timeIntervalSince1970]) ?: @0,
}];
}
NSDictionary *userInfo = @{
kFileSHA256 : wc.event.fileSHA256 ?: @"",
kFilePath : wc.event.filePath ?: @"",
kFileBundleName : wc.event.fileBundleName ?: @"",
kFileBundleID : wc.event.fileBundleID ?: @"",
kFileBundleVersion : wc.event.fileBundleVersion ?: @"",
kFileBundleShortVersionString : wc.event.fileBundleVersionString ?: @"",
kTeamID : wc.event.teamID ?: @"",
kExecutingUser : wc.event.executingUser ?: @"",
kExecutionTime : @([wc.event.occurrenceDate timeIntervalSince1970]) ?: @0,
kPID : wc.event.pid ?: @0,
kPPID : wc.event.ppid ?: @0,
kParentName : wc.event.parentName ?: @"",
kSigningChain : signingChain,
};
[dc postNotificationName:@"com.google.santa.notification.blockedeexecution"
object:@"com.google.santa"
userInfo:userInfo];
}
- (void)showQueuedWindow {
// Notifications arrive on a background thread but UI updates must happen on the main thread.
// This includes making windows.
@@ -208,6 +257,8 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
#pragma mark SNTNotifierXPC protocol methods
- (void)postClientModeNotification:(SNTClientMode)clientmode {
if ([SNTConfigurator configurator].enableSilentMode) return;
UNUserNotificationCenter *un = [UNUserNotificationCenter currentNotificationCenter];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
@@ -246,6 +297,8 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
}
- (void)postRuleSyncNotificationWithCustomMessage:(NSString *)message {
if ([SNTConfigurator configurator].enableSilentMode) return;
UNUserNotificationCenter *un = [UNUserNotificationCenter currentNotificationCenter];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
@@ -262,6 +315,8 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
}
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message {
if ([SNTConfigurator configurator].enableSilentMode) return;
if (!event) {
LOGI(@"Error: Missing event object in message received from daemon!");
return;
@@ -274,6 +329,8 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
}
- (void)postUSBBlockNotification:(SNTDeviceEvent *)event withCustomMessage:(NSString *)message {
if ([SNTConfigurator configurator].enableSilentMode) return;
if (!event) {
LOGI(@"Error: Missing event object in message received from daemon!");
return;

View File

@@ -0,0 +1,76 @@
/// 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 <MOLCertificate/MOLCertificate.h>
// #import <MOLCodesignChecker/MOLCodesignChecker.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#import "Source/gui/SNTNotificationManager.h"
#import "Source/common/SNTStoredEvent.h"
@class SNTBinaryMessageWindowController;
@interface SNTNotificationManager (Testing)
- (void)hashBundleBinariesForEvent:(SNTStoredEvent *)event
withController:(SNTBinaryMessageWindowController *)controller;
@end
@interface SNTNotificationManagerTest : XCTestCase
@end
@implementation SNTNotificationManagerTest
- (void)setUp {
[super setUp];
fclose(stdout);
}
- (void)testPostBlockNotificationSendsDistributedNotification {
SNTStoredEvent *ev = [[SNTStoredEvent alloc] init];
ev.fileSHA256 = @"the-sha256";
ev.filePath = @"/Applications/Safari.app/Contents/MacOS/Safari";
ev.fileBundleName = @"Safari";
ev.fileBundlePath = @"/Applications/Safari.app";
ev.fileBundleID = @"com.apple.Safari";
ev.fileBundleVersion = @"18614.1.14.1.15";
ev.fileBundleVersionString = @"16.0";
ev.executingUser = @"rah";
ev.occurrenceDate = [NSDate dateWithTimeIntervalSince1970:1660221048];
ev.decision = SNTEventStateBlockBinary;
ev.pid = @84156;
ev.ppid = @1;
ev.parentName = @"launchd";
SNTNotificationManager *sut = OCMPartialMock([[SNTNotificationManager alloc] init]);
OCMStub([sut hashBundleBinariesForEvent:OCMOCK_ANY withController:OCMOCK_ANY]).andDo(nil);
id dncMock = OCMClassMock([NSDistributedNotificationCenter class]);
OCMStub([dncMock defaultCenter]).andReturn(dncMock);
[sut postBlockNotification:ev withCustomMessage:@""];
OCMVerify([dncMock postNotificationName:@"com.google.santa.notification.blockedeexecution"
object:@"com.google.santa"
userInfo:[OCMArg checkWithBlock:^BOOL(NSDictionary *userInfo) {
XCTAssertEqualObjects(userInfo[@"file_sha256"], @"the-sha256");
XCTAssertEqualObjects(userInfo[@"pid"], @84156);
XCTAssertEqualObjects(userInfo[@"ppid"], @1);
XCTAssertEqualObjects(userInfo[@"execution_time"], @1660221048);
return YES;
}]]);
}
@end

View File

@@ -17,7 +17,7 @@
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santa/SNTAppDelegate.h"
#import "Source/gui/SNTAppDelegate.h"
@interface SNTSystemExtensionDelegate : NSObject <OSSystemExtensionRequestDelegate>
@end

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

@@ -119,7 +119,7 @@ REGISTER_COMMAND_NAME(@"metrics")
printf(">>> Root Labels\n");
[self prettyPrintRootLabels:normalizedMetrics[@"root_labels"]];
printf("\n");
printf(">>> Metrics \n");
printf(">>> Metrics\n");
[self prettyPrintMetricValues:normalizedMetrics[@"metrics"]];
}

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

View File

@@ -7,7 +7,7 @@
hostname | testHost
username | testUser
>>> Metrics
>>> Metrics
Metric Name | /santa/rules
Description | Number of rules
Type | SNTMetricTypeGaugeInt64

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,9 +12,9 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <Foundation/Foundation.h>
#include "Source/santad/EventProviders/SNTEndpointSecurityClientBase.h"
#include "Source/santad/EventProviders/SNTEndpointSecurityManager.h"
@interface SNTCachingEndpointSecurityManager : SNTEndpointSecurityManager
/// This should be treated as an Abstract Base Class and not directly instantiated
@interface SNTEndpointSecurityClient : NSObject <SNTEndpointSecurityClientBase>
- (instancetype)init NS_UNAVAILABLE;
@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

Some files were not shown because too many files have changed in this diff Show More