Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72969a3c92 | ||
|
|
d2dbed78dd | ||
|
|
8fa91e4ff0 | ||
|
|
551763146d | ||
|
|
7a7f0cd5a8 | ||
|
|
fcb49701b3 | ||
|
|
c9ef723fc5 | ||
|
|
dc6732ef04 | ||
|
|
a48900a4ae | ||
|
|
bb49118d94 | ||
|
|
456333d6d2 | ||
|
|
fd23a5c3b7 | ||
|
|
ec203e8796 | ||
|
|
57ff69208d | ||
|
|
f00b7d2ded | ||
|
|
9791fdd53c | ||
|
|
26e2203f1e | ||
|
|
4a47195d12 | ||
|
|
4436e221df | ||
|
|
deccc8a148 | ||
|
|
06da796a4d | ||
|
|
7b99a76d0d | ||
|
|
c2d3e99446 | ||
|
|
6db7fea8ae | ||
|
|
6fcb4cfe63 | ||
|
|
8b55ee4da5 |
18
.allstar/binary_artifacts.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
# Ignore reason: These crafted binaries are used in tests
|
||||
ignorePaths:
|
||||
- Source/common/testdata/bad_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
- Source/common/testdata/32bitplist
|
||||
- Source/common/testdata/BundleExample.app/Contents/MacOS/BundleExample
|
||||
- Source/common/testdata/DirectoryBundle/Contents/MacOS/DirectoryBundle
|
||||
- Source/common/testdata/DirectoryBundle/Contents/Resources/BundleExample.app/Contents/MacOS/BundleExample
|
||||
- Source/santad/testdata/binaryrules/badbinary
|
||||
- Source/santad/testdata/binaryrules/goodbinary
|
||||
- Source/santad/testdata/binaryrules/badcert
|
||||
- Source/santad/testdata/binaryrules/banned_teamid_allowed_binary
|
||||
- Source/santad/testdata/binaryrules/banned_teamid
|
||||
- Source/santad/testdata/binaryrules/goodcert
|
||||
- Source/santad/testdata/binaryrules/noop
|
||||
- Source/santad/testdata/binaryrules/rules.db
|
||||
11
.bazelrc
@@ -3,3 +3,14 @@ build --apple_generate_dsym --define=apple.propagate_embedded_extra_outputs=yes
|
||||
build --copt=-Werror
|
||||
build --copt=-Wall
|
||||
build --copt=-Wno-error=deprecated-declarations
|
||||
build --per_file_copt=.*\.mm\$@-std=c++17
|
||||
build --cxxopt=-std=c++17
|
||||
|
||||
build:asan --strip=never
|
||||
build:asan --copt="-Wno-macro-redefined"
|
||||
build:asan --copt="-D_FORTIFY_SOURCE=0"
|
||||
build:asan --copt="-O1"
|
||||
build:asan --copt="-fno-omit-frame-pointer"
|
||||
build:asan --copt="-fsanitize=address"
|
||||
build:asan --copt="-DADDRESS_SANITIZER"
|
||||
build:asan --linkopt="-fsanitize=address"
|
||||
|
||||
@@ -1 +1 @@
|
||||
5.0.0
|
||||
5.3.0
|
||||
|
||||
@@ -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'"
|
||||
7
.github/workflows/ci.yml
vendored
@@ -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
@@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
10
README.md
@@ -1,13 +1,13 @@
|
||||
# Santa [](https://github.com/google/santa/actions/workflows/ci.yml) [](https://coveralls.io/github/google/santa?branch=main)
|
||||
# Santa [](https://github.com/google/santa/actions/workflows/ci.yml)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/santa/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/gui/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
</p>
|
||||
|
||||
Santa is a binary authorization system for macOS. It consists of a system
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -83,12 +83,6 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTAllowlistInfo",
|
||||
srcs = ["SNTAllowlistInfo.m"],
|
||||
hdrs = ["SNTAllowlistInfo.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTCommonEnums",
|
||||
hdrs = ["SNTCommonEnums.h"],
|
||||
@@ -106,6 +100,23 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTKVOManager",
|
||||
srcs = ["SNTKVOManager.mm"],
|
||||
hdrs = ["SNTKVOManager.h"],
|
||||
deps = [
|
||||
":SNTLogging",
|
||||
],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTKVOManagerTest",
|
||||
srcs = ["SNTKVOManagerTest.mm"],
|
||||
deps = [
|
||||
":SNTKVOManager",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTDropRootPrivs",
|
||||
srcs = ["SNTDropRootPrivs.m"],
|
||||
@@ -117,6 +128,7 @@ objc_library(
|
||||
srcs = ["SNTFileInfo.m"],
|
||||
hdrs = ["SNTFileInfo.h"],
|
||||
deps = [
|
||||
":SNTLogging",
|
||||
"@FMDB",
|
||||
"@MOLCodesignChecker",
|
||||
],
|
||||
@@ -298,13 +310,40 @@ santa_unit_test(
|
||||
deps = [":SNTMetricSet"],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTCachedDecisionTest",
|
||||
srcs = ["SNTCachedDecisionTest.mm"],
|
||||
deps = [
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":SNTCachedDecisionTest",
|
||||
":SNTFileInfoTest",
|
||||
":SNTKVOManagerTest",
|
||||
":SNTMetricSetTest",
|
||||
":SNTPrefixTreeTest",
|
||||
":SNTRuleTest",
|
||||
":SantaCacheTest",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "TestUtils",
|
||||
testonly = 1,
|
||||
srcs = ["TestUtils.mm"],
|
||||
hdrs = ["TestUtils.h"],
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTAllowlistInfo.h"
|
||||
|
||||
@implementation SNTAllowlistInfo
|
||||
|
||||
- (instancetype)initWithPid:(pid_t)pid
|
||||
pidversion:(int)pidver
|
||||
targetPath:(NSString *)targetPath
|
||||
sha256:(NSString *)hash {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_pid = pid;
|
||||
_pidversion = pidver;
|
||||
_targetPath = targetPath;
|
||||
_sha256 = hash;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,10 +12,11 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
|
||||
@class MOLCertificate;
|
||||
|
||||
@@ -24,6 +25,8 @@
|
||||
///
|
||||
@interface SNTCachedDecision : NSObject
|
||||
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile;
|
||||
|
||||
@property santa_vnode_id_t vnodeId;
|
||||
@property SNTEventState decision;
|
||||
@property NSString *decisionExtra;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -15,4 +15,14 @@
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
|
||||
@implementation SNTCachedDecision
|
||||
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_vnodeId.fsid = (uint64_t)esFile->stat.st_dev;
|
||||
_vnodeId.fileid = esFile->stat.st_ino;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
36
Source/common/SNTCachedDecisionTest.mm
Normal file
@@ -0,0 +1,36 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
|
||||
@interface SNTCachedDecisionTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTCachedDecisionTest
|
||||
|
||||
- (void)testSNTCachedDecisionInit {
|
||||
// Ensure the vnodeId field is properly set from the es_file_t
|
||||
struct stat sb = MakeStat(1234, 5678);
|
||||
es_file_t file = MakeESFile("foo", sb);
|
||||
|
||||
SNTCachedDecision *cd = [[SNTCachedDecision alloc] initWithEndpointSecurityFile:&file];
|
||||
|
||||
XCTAssertEqual(sb.st_ino, cd.vnodeId.fileid);
|
||||
XCTAssertEqual(sb.st_dev, cd.vnodeId.fsid);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -27,41 +27,22 @@
|
||||
#define unlikely(x) __builtin_expect(!!(x), 0)
|
||||
|
||||
typedef enum {
|
||||
ACTION_UNSET = 0,
|
||||
ACTION_UNSET,
|
||||
|
||||
// REQUESTS
|
||||
ACTION_REQUEST_SHUTDOWN = 10,
|
||||
ACTION_REQUEST_BINARY = 11,
|
||||
// If an operation is awaiting a cache decision from a similar operation
|
||||
// currently being processed, it will poll about every 5 ms for an answer.
|
||||
ACTION_REQUEST_BINARY,
|
||||
|
||||
// RESPONSES
|
||||
ACTION_RESPOND_ALLOW = 20,
|
||||
ACTION_RESPOND_DENY = 21,
|
||||
ACTION_RESPOND_TOOLONG = 22,
|
||||
ACTION_RESPOND_ACK = 23,
|
||||
ACTION_RESPOND_ALLOW_COMPILER = 24,
|
||||
// The following response is stored only in the kernel decision cache.
|
||||
// It is removed by SNTCompilerController
|
||||
ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE = 25,
|
||||
|
||||
// NOTIFY
|
||||
ACTION_NOTIFY_EXEC = 30,
|
||||
ACTION_NOTIFY_WRITE = 31,
|
||||
ACTION_NOTIFY_RENAME = 32,
|
||||
ACTION_NOTIFY_LINK = 33,
|
||||
ACTION_NOTIFY_EXCHANGE = 34,
|
||||
ACTION_NOTIFY_DELETE = 35,
|
||||
ACTION_NOTIFY_WHITELIST = 36,
|
||||
ACTION_NOTIFY_FORK = 37,
|
||||
ACTION_NOTIFY_EXIT = 38,
|
||||
|
||||
// ERROR
|
||||
ACTION_ERROR = 99,
|
||||
ACTION_RESPOND_ALLOW,
|
||||
ACTION_RESPOND_DENY,
|
||||
ACTION_RESPOND_ALLOW_COMPILER,
|
||||
} santa_action_t;
|
||||
|
||||
#define RESPONSE_VALID(x) \
|
||||
(x == ACTION_RESPOND_ALLOW || x == ACTION_RESPOND_DENY || \
|
||||
x == ACTION_RESPOND_ALLOW_COMPILER || \
|
||||
x == ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE)
|
||||
x == ACTION_RESPOND_ALLOW_COMPILER)
|
||||
|
||||
// Struct to manage vnode IDs
|
||||
typedef struct santa_vnode_id_t {
|
||||
@@ -75,28 +56,4 @@ typedef struct santa_vnode_id_t {
|
||||
#endif
|
||||
} santa_vnode_id_t;
|
||||
|
||||
typedef struct {
|
||||
santa_action_t action;
|
||||
santa_vnode_id_t vnode_id;
|
||||
uid_t uid;
|
||||
gid_t gid;
|
||||
pid_t pid;
|
||||
int pidversion;
|
||||
pid_t ppid;
|
||||
char path[MAXPATHLEN];
|
||||
char newpath[MAXPATHLEN];
|
||||
char ttypath[MAXPATHLEN];
|
||||
// For file events, this is the process name.
|
||||
// For exec requests, this is the parent process name.
|
||||
// While process names can technically be 4*MAXPATHLEN, that never
|
||||
// actually happens, so only take MAXPATHLEN and throw away any excess.
|
||||
char pname[MAXPATHLEN];
|
||||
|
||||
// This points to a copy of the original ES message.
|
||||
void *es_message;
|
||||
|
||||
// This points to an NSArray of the process arguments.
|
||||
void *args_array;
|
||||
} santa_message_t;
|
||||
|
||||
#endif // SANTA__COMMON__COMMON_H
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -57,6 +57,7 @@ typedef NS_ENUM(NSInteger, SNTEventState) {
|
||||
SNTEventStateBlockCertificate = 1 << 18,
|
||||
SNTEventStateBlockScope = 1 << 19,
|
||||
SNTEventStateBlockTeamID = 1 << 20,
|
||||
SNTEventStateBlockLongPath = 1 << 21,
|
||||
|
||||
// Bits 24-31 store allow decision types
|
||||
SNTEventStateAllowUnknown = 1 << 24,
|
||||
@@ -120,5 +121,4 @@ typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
|
||||
static const char *kSantaDPath =
|
||||
"/Applications/Santa.app/Contents/Library/SystemExtensions/"
|
||||
"com.google.santa.daemon.systemextension/Contents/MacOS/com.google.santa.daemon";
|
||||
static const char *kSantaCtlPath = "/Applications/Santa.app/Contents/MacOS/santactl";
|
||||
static const char *kSantaAppPath = "/Applications/Santa.app";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -65,7 +65,8 @@
|
||||
/// <key>rule_type</key>
|
||||
/// <string>BINARY</string> (one of BINARY, CERTIFICATE or TEAMID)
|
||||
/// <key>policy</key>
|
||||
/// <string>BLOCKLIST</string> (one of ALLOWLIST, ALLOWLIST_COMPILER, BLOCKLIST, SILENT_BLOCKLIST)
|
||||
/// <string>BLOCKLIST</string> (one of ALLOWLIST, ALLOWLIST_COMPILER, BLOCKLIST,
|
||||
/// SILENT_BLOCKLIST)
|
||||
/// </dict>
|
||||
/// </array>
|
||||
///
|
||||
@@ -244,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.
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,6 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class MOLCodesignChecker;
|
||||
@@ -32,6 +33,14 @@
|
||||
///
|
||||
- (instancetype)initWithPath:(NSString *)path error:(NSError **)error;
|
||||
|
||||
///
|
||||
/// Convenience initializer.
|
||||
///
|
||||
/// @param esFile Pointer to an es_file_t provided by the EndpointSecurity framework.
|
||||
/// Assumes that the path is a resolved path.
|
||||
///
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile error:(NSError **)error;
|
||||
|
||||
///
|
||||
/// Convenience initializer.
|
||||
///
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -25,6 +25,8 @@
|
||||
#include <sys/stat.h>
|
||||
#include <sys/xattr.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
|
||||
// Simple class to hold the data of a mach_header and the offset within the file
|
||||
// in which that header was found.
|
||||
@interface MachHeaderWithOffset : NSObject
|
||||
@@ -48,6 +50,7 @@
|
||||
@property NSFileHandle *fileHandle;
|
||||
@property NSUInteger fileSize;
|
||||
@property NSString *fileOwnerHomeDir;
|
||||
@property NSString *sha256Storage;
|
||||
|
||||
// Cached properties
|
||||
@property NSBundle *bundleRef;
|
||||
@@ -63,6 +66,26 @@
|
||||
extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
|
||||
- (instancetype)initWithResolvedPath:(NSString *)path error:(NSError **)error {
|
||||
struct stat fileStat;
|
||||
if (path.length) {
|
||||
lstat(path.UTF8String, &fileStat);
|
||||
}
|
||||
return [self initWithResolvedPath:path stat:&fileStat error:error];
|
||||
}
|
||||
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile error:(NSError **)error {
|
||||
return [self initWithResolvedPath:@(esFile->path.data) stat:&esFile->stat error:error];
|
||||
}
|
||||
|
||||
- (instancetype)initWithResolvedPath:(NSString *)path
|
||||
stat:(const struct stat *)fileStat
|
||||
error:(NSError **)error {
|
||||
if (!fileStat) {
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"NULL stat buffer unsupported");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_path = path;
|
||||
@@ -76,9 +99,7 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
return nil;
|
||||
}
|
||||
|
||||
struct stat fileStat;
|
||||
lstat(_path.UTF8String, &fileStat);
|
||||
if (!((S_IFMT & fileStat.st_mode) == S_IFREG)) {
|
||||
if (!((S_IFMT & fileStat->st_mode) == S_IFREG)) {
|
||||
if (error) {
|
||||
NSString *errStr = [NSString stringWithFormat:@"Non regular file: %s", strerror(errno)];
|
||||
*error = [NSError errorWithDomain:@"com.google.santa.fileinfo"
|
||||
@@ -88,12 +109,12 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
return nil;
|
||||
}
|
||||
|
||||
_fileSize = fileStat.st_size;
|
||||
_fileSize = fileStat->st_size;
|
||||
|
||||
if (_fileSize == 0) return nil;
|
||||
|
||||
if (fileStat.st_uid != 0) {
|
||||
struct passwd *pwd = getpwuid(fileStat.st_uid);
|
||||
if (fileStat->st_uid != 0) {
|
||||
struct passwd *pwd = getpwuid(fileStat->st_uid);
|
||||
if (pwd) {
|
||||
_fileOwnerHomeDir = @(pwd->pw_dir);
|
||||
}
|
||||
@@ -214,9 +235,13 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
}
|
||||
|
||||
- (NSString *)SHA256 {
|
||||
NSString *sha256;
|
||||
[self hashSHA1:NULL SHA256:&sha256];
|
||||
return sha256;
|
||||
// Memoize the value
|
||||
if (!self.sha256Storage) {
|
||||
NSString *sha256;
|
||||
[self hashSHA1:NULL SHA256:&sha256];
|
||||
self.sha256Storage = sha256;
|
||||
}
|
||||
return self.sha256Storage;
|
||||
}
|
||||
|
||||
#pragma mark File Type Info
|
||||
|
||||
34
Source/common/SNTKVOManager.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// The callback type when KVO notifications are received for observed key paths.
|
||||
// The first parameter is the previous value, the second paramter is the new value.
|
||||
typedef void (^KVOCallback)(id oldValue, id newValue);
|
||||
|
||||
@interface SNTKVOManager : NSObject
|
||||
|
||||
// Add an observer for the selector on the given object. When a KVO notification
|
||||
// is received, the callback is called. If the notification contains objects that
|
||||
// are not of the expectedType, nil is passed as the argument to the callback.
|
||||
// The observer is removed when the returned instance is deallocated.
|
||||
- (instancetype)initWithObject:(id)object
|
||||
selector:(SEL)selector
|
||||
type:(Class)expectedType
|
||||
callback:(KVOCallback)callback;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
72
Source/common/SNTKVOManager.mm
Normal file
@@ -0,0 +1,72 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTKVOManager.h"
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
|
||||
@interface SNTKVOManager ()
|
||||
@property KVOCallback callback;
|
||||
@property Class expectedType;
|
||||
@property NSString *keyPath;
|
||||
@property id object;
|
||||
@end
|
||||
|
||||
@implementation SNTKVOManager
|
||||
|
||||
- (instancetype)initWithObject:(id)object
|
||||
selector:(SEL)selector
|
||||
type:(Class)expectedType
|
||||
callback:(KVOCallback)callback {
|
||||
self = [super self];
|
||||
if (self) {
|
||||
NSString *selectorName = NSStringFromSelector(selector);
|
||||
if (![object respondsToSelector:selector]) {
|
||||
LOGE(@"Attempt to add observer for an unknown selector (%@) for object (%@)", selectorName,
|
||||
[object class]);
|
||||
return nil;
|
||||
}
|
||||
|
||||
_object = object;
|
||||
_keyPath = selectorName;
|
||||
_expectedType = expectedType;
|
||||
_callback = callback;
|
||||
|
||||
[object addObserver:self
|
||||
forKeyPath:selectorName
|
||||
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
|
||||
context:NULL];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self.object removeObserver:self forKeyPath:self.keyPath context:NULL];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||
ofObject:(id)object
|
||||
change:(NSDictionary<NSString *, id> *)change
|
||||
context:(void *)context {
|
||||
id oldValue = [change[NSKeyValueChangeOldKey] isKindOfClass:self.expectedType]
|
||||
? change[NSKeyValueChangeOldKey]
|
||||
: nil;
|
||||
id newValue = [change[NSKeyValueChangeNewKey] isKindOfClass:self.expectedType]
|
||||
? change[NSKeyValueChangeNewKey]
|
||||
: nil;
|
||||
|
||||
self.callback(oldValue, newValue);
|
||||
}
|
||||
|
||||
@end
|
||||
129
Source/common/SNTKVOManagerTest.mm
Normal file
@@ -0,0 +1,129 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTKVOManager.h"
|
||||
|
||||
@interface Foo : NSObject
|
||||
@property NSNumber *propNumber;
|
||||
@property NSArray *propArray;
|
||||
@property id propId;
|
||||
@end
|
||||
|
||||
@implementation Foo
|
||||
@end
|
||||
|
||||
@interface SNTKVOManagerTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTKVOManagerTest
|
||||
|
||||
- (void)testInvalidSelector {
|
||||
Foo *foo = [[Foo alloc] init];
|
||||
|
||||
SNTKVOManager *kvo = [[SNTKVOManager alloc] initWithObject:foo
|
||||
selector:NSSelectorFromString(@"doesNotExist")
|
||||
type:[NSNumber class]
|
||||
callback:^(id, id){
|
||||
}];
|
||||
|
||||
XCTAssertNil(kvo);
|
||||
}
|
||||
|
||||
- (void)testNormalOperation {
|
||||
Foo *foo = [[Foo alloc] init];
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
int origVal = 123;
|
||||
int update1 = 456;
|
||||
int update2 = 789;
|
||||
|
||||
foo.propNumber = @(origVal);
|
||||
|
||||
// Store the values from the callback to test against expected values
|
||||
__block int oldVal;
|
||||
__block int newVal;
|
||||
|
||||
SNTKVOManager *kvo =
|
||||
[[SNTKVOManager alloc] initWithObject:foo
|
||||
selector:@selector(propNumber)
|
||||
type:[NSNumber class]
|
||||
callback:^(NSNumber *oldValue, NSNumber *newValue) {
|
||||
oldVal = [oldValue intValue];
|
||||
newVal = [newValue intValue];
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
XCTAssertNotNil(kvo);
|
||||
|
||||
// Ensure an update to the observed property triggers the callback
|
||||
foo.propNumber = @(update1);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for first observable update");
|
||||
XCTAssertEqual(oldVal, origVal);
|
||||
XCTAssertEqual(newVal, update1);
|
||||
|
||||
// One more time why not
|
||||
foo.propNumber = @(update2);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for second observable update");
|
||||
XCTAssertEqual(oldVal, update1);
|
||||
XCTAssertEqual(newVal, update2);
|
||||
}
|
||||
|
||||
- (void)testUnexpectedTypes {
|
||||
Foo *foo = [[Foo alloc] init];
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
NSString *origVal = @"any_val";
|
||||
NSString *update = @"new_val";
|
||||
foo.propId = origVal;
|
||||
|
||||
__block id oldVal;
|
||||
__block id newVal;
|
||||
|
||||
SNTKVOManager *kvo = [[SNTKVOManager alloc] initWithObject:foo
|
||||
selector:@selector(propId)
|
||||
type:[NSString class]
|
||||
callback:^(id oldValue, id newValue) {
|
||||
oldVal = oldValue;
|
||||
newVal = newValue;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
XCTAssertNotNil(kvo);
|
||||
|
||||
// Update to an unexpected type (here, NSNumber instead of NSString)
|
||||
foo.propId = @(123);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for first observable update");
|
||||
XCTAssertEqualObjects(oldVal, origVal);
|
||||
XCTAssertNil(newVal);
|
||||
|
||||
// Update again with an expected type, ensure oldVal is now nil
|
||||
foo.propId = update;
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for first observable update");
|
||||
XCTAssertNil(oldVal);
|
||||
XCTAssertEqualObjects(newVal, update);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -51,18 +51,18 @@
|
||||
/// Designated initializer.
|
||||
///
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg
|
||||
timestamp:(NSUInteger)timestamp;
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg
|
||||
timestamp:(NSUInteger)timestamp;
|
||||
|
||||
///
|
||||
/// Initialize with a default timestamp: current time if rule state is transitive, 0 otherwise.
|
||||
///
|
||||
- (instancetype)initWithIdentifier:(NSString *)identifier
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg;
|
||||
state:(SNTRuleState)state
|
||||
type:(SNTRuleType)type
|
||||
customMsg:(NSString *)customMsg;
|
||||
|
||||
///
|
||||
/// Initialize with a dictionary received from a sync server.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -95,7 +95,6 @@
|
||||
///
|
||||
@property NSArray *signingChain;
|
||||
|
||||
|
||||
///
|
||||
/// If the executed file was signed, this is the Team ID if present in the signature information.
|
||||
///
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
/// Copyright 2016-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,10 +12,14 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#define STRONGIFY(var) \
|
||||
_Pragma("clang diagnostic push") \
|
||||
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
|
||||
__strong __typeof(var) var = (Weak_##var); \
|
||||
// clang-format off
|
||||
|
||||
#define STRONGIFY(var) \
|
||||
_Pragma("clang diagnostic push") \
|
||||
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
|
||||
__strong __typeof(var) var = (Weak_##var); \
|
||||
_Pragma("clang diagnostic pop")
|
||||
|
||||
#define WEAKIFY(var) __weak __typeof(var) Weak_##var = (var);
|
||||
|
||||
// clang-format on
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -18,8 +18,11 @@
|
||||
@implementation SNTSystemInfo
|
||||
|
||||
+ (NSString *)serialNumber {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
io_service_t platformExpert =
|
||||
IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"));
|
||||
#pragma clang diagnostic pop
|
||||
if (!platformExpert) return nil;
|
||||
|
||||
NSString *serial = CFBridgingRelease(IORegistryEntryCreateCFProperty(
|
||||
@@ -31,8 +34,11 @@
|
||||
}
|
||||
|
||||
+ (NSString *)hardwareUUID {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
io_service_t platformExpert =
|
||||
IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"));
|
||||
#pragma clang diagnostic pop
|
||||
if (!platformExpert) return nil;
|
||||
|
||||
NSString *uuid = CFBridgingRelease(IORegistryEntryCreateCFProperty(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -15,8 +15,8 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
|
||||
@class SNTRule;
|
||||
@class SNTStoredEvent;
|
||||
@@ -31,7 +31,6 @@
|
||||
/// Cache Ops
|
||||
///
|
||||
- (void)cacheCounts:(void (^)(uint64_t rootCache, uint64_t nonRootCache))reply;
|
||||
- (void)cacheBucketCount:(void (^)(NSArray *))reply;
|
||||
- (void)checkCacheForVnodeID:(santa_vnode_id_t)vnodeID withReply:(void (^)(santa_action_t))reply;
|
||||
|
||||
///
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
/// Copyright 2016-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
#include <libkern/OSAtomic.h>
|
||||
#include <libkern/OSTypes.h>
|
||||
#include <os/log.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/cdefs.h>
|
||||
|
||||
@@ -26,11 +27,6 @@
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
|
||||
#define panic(args...) \
|
||||
printf(args); \
|
||||
printf("\n"); \
|
||||
abort()
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
@@ -334,7 +330,9 @@ class SantaCache {
|
||||
inline void unlock(struct bucket *bucket) const {
|
||||
if (unlikely(OSAtomicTestAndClear(7, (volatile uint8_t *)&bucket->head) ==
|
||||
0)) {
|
||||
panic("SantaCache::unlock(): Tried to unlock an unlocked lock");
|
||||
os_log_error(OS_LOG_DEFAULT,
|
||||
"SantaCache::unlock(): Tried to unlock an unlocked lock");
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
60
Source/common/TestUtils.h
Normal file
@@ -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
@@ -0,0 +1,107 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <mach/mach_time.h>
|
||||
#include <time.h>
|
||||
|
||||
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver) {
|
||||
return audit_token_t{
|
||||
.val =
|
||||
{
|
||||
0,
|
||||
NOBODY_UID,
|
||||
NOBODY_GID,
|
||||
NOBODY_UID,
|
||||
NOBODY_GID,
|
||||
(unsigned int)pid,
|
||||
0,
|
||||
(unsigned int)pidver,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
struct stat MakeStat(ino_t ino, dev_t devno) {
|
||||
return (struct stat){
|
||||
.st_dev = devno,
|
||||
.st_ino = ino,
|
||||
};
|
||||
}
|
||||
|
||||
es_string_token_t MakeESStringToken(const char *s) {
|
||||
return es_string_token_t{
|
||||
.length = strlen(s),
|
||||
.data = s,
|
||||
};
|
||||
}
|
||||
|
||||
es_file_t MakeESFile(const char *path, struct stat sb) {
|
||||
return es_file_t{
|
||||
.path = MakeESStringToken(path),
|
||||
.path_truncated = false,
|
||||
.stat = sb,
|
||||
};
|
||||
}
|
||||
|
||||
es_process_t MakeESProcess(es_file_t *file, audit_token_t tok, audit_token_t parent_tok) {
|
||||
return es_process_t{
|
||||
.audit_token = tok,
|
||||
.ppid = audit_token_to_pid(parent_tok),
|
||||
.original_ppid = audit_token_to_pid(parent_tok),
|
||||
.executable = file,
|
||||
.parent_audit_token = parent_tok,
|
||||
};
|
||||
}
|
||||
|
||||
static uint64_t AddMillisToMachTime(uint64_t ms, uint64_t machTime) {
|
||||
static dispatch_once_t onceToken;
|
||||
static mach_timebase_info_data_t timebase;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
mach_timebase_info(&timebase);
|
||||
});
|
||||
|
||||
// Convert given machTime to nanoseconds
|
||||
uint64_t nanoTime = machTime * timebase.numer / timebase.denom;
|
||||
|
||||
// Add the ms offset
|
||||
nanoTime += (ms * NSEC_PER_MSEC);
|
||||
|
||||
// Convert back to machTime
|
||||
return nanoTime * timebase.denom / timebase.numer;
|
||||
}
|
||||
|
||||
es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc, ActionType action_type,
|
||||
uint64_t future_deadline_ms) {
|
||||
return es_message_t{
|
||||
.deadline = AddMillisToMachTime(future_deadline_ms, mach_absolute_time()),
|
||||
.process = proc,
|
||||
.action_type =
|
||||
(action_type == ActionType::Notify) ? ES_ACTION_TYPE_NOTIFY : ES_ACTION_TYPE_AUTH,
|
||||
.event_type = et,
|
||||
};
|
||||
}
|
||||
|
||||
void SleepMS(long ms) {
|
||||
struct timespec ts {
|
||||
.tv_sec = ms / 1000, .tv_nsec = (long)((ms % 1000) * NSEC_PER_MSEC),
|
||||
};
|
||||
|
||||
while (nanosleep(&ts, &ts) != 0) {
|
||||
XCTAssertEqual(errno, EINTR);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//
|
||||
// !!! WARNING !!!
|
||||
// This proto is in beta format and subject to change.
|
||||
// This proto is for demonstration purposes only and will be changing.
|
||||
// Do not rely on this format.
|
||||
//
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#import "Source/santa/SNTMessageWindowController.h"
|
||||
#import "Source/gui/SNTMessageWindowController.h"
|
||||
|
||||
@class SNTStoredEvent;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
76
Source/gui/SNTNotificationManagerTest.m
Normal 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
|
||||
@@ -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
|
||||
@@ -26,7 +26,6 @@ objc_library(
|
||||
"//:opt_build": [],
|
||||
"//conditions:default": [
|
||||
"Commands/SNTCommandBundleInfo.m",
|
||||
"Commands/SNTCommandCacheHistogram.m",
|
||||
"Commands/SNTCommandCheckCache.m",
|
||||
"Commands/SNTCommandFlushCache.m",
|
||||
],
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/// Copyright 2018 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifdef DEBUG
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
|
||||
@interface SNTCommandCacheHistogram : SNTCommand <SNTCommandProtocol>
|
||||
@end
|
||||
|
||||
@implementation SNTCommandCacheHistogram
|
||||
|
||||
REGISTER_COMMAND_NAME(@"cachehistogram")
|
||||
|
||||
+ (BOOL)requiresRoot {
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)requiresDaemonConn {
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (NSString *)shortHelpText {
|
||||
return @"Print a cache distribution histogram.";
|
||||
}
|
||||
|
||||
+ (NSString *)longHelpText {
|
||||
return (@"Prints a histogram of each bucket of the in-kernel cache\n"
|
||||
@" Use -g to get 'graphical' output\n"
|
||||
@"Only available in DEBUG builds.");
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
[[self.daemonConn remoteObjectProxy] cacheBucketCount:^(NSArray *counts) {
|
||||
NSMutableDictionary<NSNumber *, NSNumber *> *d = [NSMutableDictionary dictionary];
|
||||
[counts enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
|
||||
d[obj] = @([d[obj] intValue] + 1);
|
||||
}];
|
||||
printf("There are %llu empty buckets\n", [d[@0] unsignedLongLongValue]);
|
||||
|
||||
for (NSNumber *key in [d.allKeys sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
if ([key isEqual:@0]) continue;
|
||||
uint64_t k = [key unsignedLongLongValue];
|
||||
uint64_t v = [d[key] unsignedLongLongValue];
|
||||
|
||||
if ([[[NSProcessInfo processInfo] arguments] containsObject:@"-g"]) {
|
||||
printf("%4llu: ", k);
|
||||
for (uint64_t y = 0; y < v; ++y) {
|
||||
printf("#");
|
||||
}
|
||||
printf("\n");
|
||||
} else {
|
||||
printf("%4llu bucket[s] have %llu %s\n", v, k, k > 1 ? "entries" : "entry");
|
||||
}
|
||||
}
|
||||
exit(0);
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2016 Google Inc. All rights reserved.
|
||||
/// Copyright 2016-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -62,9 +62,6 @@ REGISTER_COMMAND_NAME(@"checkcache")
|
||||
} else if (action == ACTION_RESPOND_ALLOW_COMPILER) {
|
||||
LOGI(@"File exists in [allowlist compiler] kernel cache");
|
||||
exit(0);
|
||||
} else if (action == ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE) {
|
||||
LOGI(@"File exists in [allowlist pending_transitive] kernel cache");
|
||||
exit(0);
|
||||
} else if (action == ACTION_UNSET) {
|
||||
LOGE(@"File does not exist in cache");
|
||||
exit(1);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -385,6 +385,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
|
||||
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
|
||||
case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break;
|
||||
case SNTEventStateAllowTransitive: [output appendString:@" (Transitive)"]; break;
|
||||
case SNTEventStateBlockLongPath: [output appendString:@" (Long Path)"]; break;
|
||||
|
||||
default: output = @"None".mutableCopy; break;
|
||||
}
|
||||
|
||||
@@ -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"]];
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
hostname | testHost
|
||||
username | testUser
|
||||
|
||||
>>> Metrics
|
||||
>>> Metrics
|
||||
Metric Name | /santa/rules
|
||||
Description | Number of rules
|
||||
Type | SNTMetricTypeGaugeInt64
|
||||
|
||||
1214
Source/santad/BUILD
@@ -105,6 +105,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
|
||||
// This is a Santa-curated list of paths to check on startup. This list will be merged
|
||||
// with the set of default muted paths from ES.
|
||||
|
||||
NSSet *santaDefinedCriticalPaths = [NSSet setWithArray:@[
|
||||
@"/usr/libexec/trustd",
|
||||
@"/usr/lib/dyld",
|
||||
@@ -136,6 +137,12 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
NSMutableDictionary *bins = [NSMutableDictionary dictionary];
|
||||
for (NSString *path in [SNTRuleTable criticalSystemBinaryPaths]) {
|
||||
SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithPath:path];
|
||||
if (!binInfo.SHA256) {
|
||||
// If there isn't a hash, no need to compute the other info here.
|
||||
// Just continue on to the next binary.
|
||||
LOGW(@"Unable to compute hash for critical system binary %@.", path);
|
||||
continue;
|
||||
}
|
||||
MOLCodesignChecker *csInfo = [binInfo codesignCheckerWithError:NULL];
|
||||
|
||||
// Make sure the critical system binary is signed by the same chain as launchd/self
|
||||
@@ -143,9 +150,9 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
if ([csInfo signingInformationMatches:self.launchdCSInfo]) {
|
||||
systemBin = YES;
|
||||
} else if (![csInfo signingInformationMatches:self.santadCSInfo]) {
|
||||
LOGE(@"Unable to validate critical system binary. "
|
||||
LOGW(@"Unable to validate critical system binary %@. "
|
||||
@"pid 1: %@, santad: %@ and %@: %@ do not match.",
|
||||
self.launchdCSInfo.leafCertificate, self.santadCSInfo.leafCertificate, path,
|
||||
path, self.launchdCSInfo.leafCertificate, self.santadCSInfo.leafCertificate, path,
|
||||
csInfo.leafCertificate);
|
||||
continue;
|
||||
}
|
||||
|
||||
75
Source/santad/EventProviders/AuthResultCache.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_AUTHRESULTCACHE_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_AUTHRESULTCACHE_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <sys/stat.h>
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
enum class FlushCacheMode {
|
||||
kNonRootOnly,
|
||||
kAllCaches,
|
||||
};
|
||||
|
||||
class AuthResultCache {
|
||||
public:
|
||||
// Santa currently only flushes caches when new DENY rules are added, not
|
||||
// ALLOW rules. This means this value should be low enough so that if a
|
||||
// previously denied binary is allowed, it can be re-executed by the user in a
|
||||
// timely manner. But the value should be high enough to allow the cache to be
|
||||
// effective in the event the binary is executed in rapid succession.
|
||||
AuthResultCache(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms = 1500);
|
||||
virtual ~AuthResultCache();
|
||||
|
||||
AuthResultCache(AuthResultCache &&other) = delete;
|
||||
AuthResultCache &operator=(AuthResultCache &&rhs) = delete;
|
||||
AuthResultCache(const AuthResultCache &other) = delete;
|
||||
AuthResultCache &operator=(const AuthResultCache &other) = delete;
|
||||
|
||||
virtual bool AddToCache(const es_file_t *es_file, santa_action_t decision);
|
||||
virtual void RemoveFromCache(const es_file_t *es_file);
|
||||
virtual santa_action_t CheckCache(const es_file_t *es_file);
|
||||
virtual santa_action_t CheckCache(santa_vnode_id_t vnode_id);
|
||||
|
||||
virtual void FlushCache(FlushCacheMode mode);
|
||||
|
||||
virtual NSArray<NSNumber *> *CacheCounts();
|
||||
|
||||
private:
|
||||
virtual SantaCache<santa_vnode_id_t, uint64_t> *CacheForVnodeID(santa_vnode_id_t vnode_id);
|
||||
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *root_cache_;
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *nonroot_cache_;
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
uint64_t root_devno_;
|
||||
uint64_t cache_deny_time_ns_;
|
||||
dispatch_queue_t q_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
|
||||
#endif
|
||||
156
Source/santad/EventProviders/AuthResultCache.mm
Normal file
@@ -0,0 +1,156 @@
|
||||
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
|
||||
#include <mach/clock_types.h>
|
||||
#include <time.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
|
||||
template <>
|
||||
uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const &t) {
|
||||
return (SantaCacheHasher<uint64_t>(t.fsid) << 1) ^ SantaCacheHasher<uint64_t>(t.fileid);
|
||||
}
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
|
||||
return santa_vnode_id_t{
|
||||
.fsid = (uint64_t)es_file->stat.st_dev,
|
||||
.fileid = es_file->stat.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
static inline uint64_t GetCurrentUptime() {
|
||||
return clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
}
|
||||
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
static inline uint64_t CacheableAction(santa_action_t action,
|
||||
uint64_t timestamp = GetCurrentUptime()) {
|
||||
return ((uint64_t)action << 56) | (timestamp & 0xFFFFFFFFFFFFFF);
|
||||
}
|
||||
|
||||
static inline santa_action_t ActionFromCachedValue(uint64_t cachedValue) {
|
||||
return (santa_action_t)(cachedValue >> 56);
|
||||
}
|
||||
|
||||
static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
|
||||
return (cachedValue & ~(0xFF00000000000000));
|
||||
}
|
||||
|
||||
AuthResultCache::AuthResultCache(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms)
|
||||
: esapi_(esapi), cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
|
||||
root_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
nonroot_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
|
||||
struct stat sb;
|
||||
if (stat("/", &sb) == 0) {
|
||||
root_devno_ = sb.st_dev;
|
||||
}
|
||||
|
||||
q_ = dispatch_queue_create(
|
||||
"com.google.santa.daemon.auth_result_cache.q",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
}
|
||||
|
||||
AuthResultCache::~AuthResultCache() {
|
||||
delete root_cache_;
|
||||
delete nonroot_cache_;
|
||||
}
|
||||
|
||||
bool AuthResultCache::AddToCache(const es_file_t *es_file, santa_action_t decision) {
|
||||
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
|
||||
switch (decision) {
|
||||
case ACTION_REQUEST_BINARY:
|
||||
return cache->set(vnode_id, CacheableAction(ACTION_REQUEST_BINARY, 0), 0);
|
||||
case ACTION_RESPOND_ALLOW: OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_ALLOW_COMPILER: OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_DENY:
|
||||
return cache->set(vnode_id, CacheableAction(decision),
|
||||
CacheableAction(ACTION_REQUEST_BINARY, 0));
|
||||
default:
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"Invalid cache value, exiting.");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
void AuthResultCache::RemoveFromCache(const es_file_t *es_file) {
|
||||
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
|
||||
CacheForVnodeID(vnode_id)->remove(vnode_id);
|
||||
}
|
||||
|
||||
santa_action_t AuthResultCache::CheckCache(const es_file_t *es_file) {
|
||||
return CheckCache(VnodeForFile(es_file));
|
||||
}
|
||||
|
||||
santa_action_t AuthResultCache::CheckCache(santa_vnode_id_t vnode_id) {
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
|
||||
|
||||
uint64_t cached_val = cache->get(vnode_id);
|
||||
if (cached_val == 0) {
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
|
||||
santa_action_t result = ActionFromCachedValue(cached_val);
|
||||
|
||||
if (result == ACTION_RESPOND_DENY) {
|
||||
uint64_t expiry_time = TimestampFromCachedValue(cached_val) + cache_deny_time_ns_;
|
||||
if (expiry_time < GetCurrentUptime()) {
|
||||
cache->remove(vnode_id);
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *AuthResultCache::CacheForVnodeID(
|
||||
santa_vnode_id_t vnode_id) {
|
||||
return (vnode_id.fsid == root_devno_ || root_devno_ == 0) ? root_cache_ : nonroot_cache_;
|
||||
}
|
||||
|
||||
void AuthResultCache::FlushCache(FlushCacheMode mode) {
|
||||
nonroot_cache_->clear();
|
||||
if (mode == FlushCacheMode::kAllCaches) {
|
||||
root_cache_->clear();
|
||||
|
||||
// Clear the ES cache when all local caches are flushed. Assume the ES cache
|
||||
// doesn't need to be cleared when only flushing the non-root cache.
|
||||
//
|
||||
// Calling into ES should be done asynchronously since it could otherwise
|
||||
// potentially deadlock.
|
||||
auto shared_esapi = esapi_->shared_from_this();
|
||||
dispatch_async(q_, ^{
|
||||
// ES does not need a connected client to clear cache
|
||||
shared_esapi->ClearCache(Client());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
NSArray<NSNumber *> *AuthResultCache::CacheCounts() {
|
||||
return @[ @(root_cache_->count()), @(nonroot_cache_->count()) ];
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
225
Source/santad/EventProviders/AuthResultCacheTest.mm
Normal file
@@ -0,0 +1,225 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <Foundation/Foundation.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
|
||||
// Grab the st_dev number of the root volume to match the root cache
|
||||
static uint64_t RootDevno() {
|
||||
static dispatch_once_t once_token;
|
||||
static uint64_t devno;
|
||||
dispatch_once(&once_token, ^{
|
||||
struct stat sb;
|
||||
stat("/", &sb);
|
||||
devno = sb.st_dev;
|
||||
});
|
||||
return devno;
|
||||
}
|
||||
|
||||
static inline es_file_t MakeCacheableFile(uint64_t devno, uint64_t ino) {
|
||||
return es_file_t{
|
||||
.path = {}, .path_truncated = false, .stat = {.st_dev = (dev_t)devno, .st_ino = ino}};
|
||||
}
|
||||
|
||||
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
|
||||
return santa_vnode_id_t{
|
||||
.fsid = (uint64_t)es_file->stat.st_dev,
|
||||
.fileid = es_file->stat.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uint64_t root_count,
|
||||
uint64_t nonroot_count) {
|
||||
NSArray<NSNumber *> *counts = cache->CacheCounts();
|
||||
|
||||
XCTAssertNotNil(counts);
|
||||
XCTAssertEqual([counts count], 2);
|
||||
XCTAssertNotNil(counts[0]);
|
||||
XCTAssertNotNil(counts[1]);
|
||||
XCTAssertEqual([counts[0] unsignedLongLongValue], root_count);
|
||||
XCTAssertEqual([counts[1] unsignedLongLongValue], nonroot_count);
|
||||
}
|
||||
|
||||
@interface AuthResultCacheTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation AuthResultCacheTest
|
||||
|
||||
- (void)testEmptyCacheExpectedNumberOfCacheCounts {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testBasicOperation {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 222);
|
||||
|
||||
// Add the root file to the cache
|
||||
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_UNSET);
|
||||
|
||||
// Now add the non-root file
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
// Update the cached values
|
||||
cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW);
|
||||
cache->AddToCache(&nonrootFile, ACTION_RESPOND_DENY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
XCTAssertEqual(cache->CheckCache(VnodeForFile(&rootFile)), ACTION_RESPOND_ALLOW);
|
||||
XCTAssertEqual(cache->CheckCache(VnodeForFile(&nonrootFile)), ACTION_RESPOND_DENY);
|
||||
|
||||
// Remove the root file
|
||||
cache->RemoveFromCache(&rootFile);
|
||||
|
||||
AssertCacheCounts(cache, 0, 1);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_RESPOND_DENY);
|
||||
}
|
||||
|
||||
- (void)testFlushCache {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 111);
|
||||
|
||||
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
// Flush non-root only
|
||||
cache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
// Add back the non-root file
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
// Flush all caches
|
||||
// The call to ClearCache is asynchronous. Use a semaphore to
|
||||
// be notified when the mock is called.
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
EXPECT_CALL(*mockESApi, ClearCache).WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
dispatch_semaphore_signal(sema);
|
||||
return true;
|
||||
}));
|
||||
cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
"ClearCache wasn't called within expected time window");
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testCacheStateMachine {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
// Cached items must first be in the ACTION_REQUEST_BINARY state
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW));
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW_COMPILER));
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
// Items in the `ACTION_REQUEST_BINARY` state cannot reenter the same state
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
santa_action_t allowed_transitions[] = {
|
||||
ACTION_RESPOND_ALLOW,
|
||||
ACTION_RESPOND_ALLOW_COMPILER,
|
||||
ACTION_RESPOND_DENY,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(allowed_transitions) / sizeof(allowed_transitions[0]); i++) {
|
||||
// First make sure the item doesn't exist
|
||||
cache->RemoveFromCache(&rootFile);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
|
||||
// Now add the item to be in the first allowed state
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
// Now assert the allowed transition
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, allowed_transitions[i]));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), allowed_transitions[i]);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testCacheExpiry {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
// Create a cache with a lowered cache expiry value
|
||||
uint64_t expiryMS = 250;
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi, expiryMS);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
// Add a file to the cache and put into the ACTION_RESPOND_DENY state
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
|
||||
|
||||
// Ensure the file exists
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_RESPOND_DENY);
|
||||
|
||||
// Wait for the item to expire
|
||||
SleepMS(expiryMS);
|
||||
|
||||
// Check cache counts to make sure the item still exists
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
// Now check the cache, which will remove the item
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2021-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -50,7 +50,8 @@ typedef void (^MockDADiskAppearedCallback)(DADiskRef ref);
|
||||
@end
|
||||
|
||||
//
|
||||
// All DiskArbitration functions used in SNTDeviceManager and shimmed out accordingly.
|
||||
// All DiskArbitration functions used in SNTEndpointSecurityDeviceManager
|
||||
// and shimmed out accordingly.
|
||||
//
|
||||
CF_EXTERN_C_BEGIN
|
||||
|
||||
|
||||
69
Source/santad/EventProviders/EndpointSecurity/Client.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_CLIENT_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_CLIENT_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class Client {
|
||||
public:
|
||||
explicit Client(es_client_t* client, es_new_client_result_t result)
|
||||
: client_(client), result_(result) {}
|
||||
|
||||
Client() : client_(nullptr), result_(ES_NEW_CLIENT_RESULT_ERR_INTERNAL) {}
|
||||
|
||||
virtual ~Client() {
|
||||
if (client_) {
|
||||
// Special case: Not using EndpointSecurityAPI here due to circular refs.
|
||||
es_delete_client(client_);
|
||||
}
|
||||
}
|
||||
|
||||
Client(Client&& other) {
|
||||
client_ = other.client_;
|
||||
result_ = other.result_;
|
||||
other.client_ = nullptr;
|
||||
other.result_ = ES_NEW_CLIENT_RESULT_ERR_INTERNAL;
|
||||
}
|
||||
|
||||
Client& operator=(Client&& rhs) {
|
||||
client_ = rhs.client_;
|
||||
result_ = rhs.result_;
|
||||
rhs.client_ = nullptr;
|
||||
rhs.result_ = ES_NEW_CLIENT_RESULT_ERR_INTERNAL;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Client(const Client& other) = delete;
|
||||
void operator=(const Client& rhs) = delete;
|
||||
|
||||
inline bool IsConnected() { return result_ == ES_NEW_CLIENT_RESULT_SUCCESS; }
|
||||
|
||||
inline es_new_client_result_t NewClientResult() { return result_; }
|
||||
|
||||
inline es_client_t* Get() const { return client_; }
|
||||
|
||||
private:
|
||||
es_client_t* client_;
|
||||
es_new_client_result_t result_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
118
Source/santad/EventProviders/EndpointSecurity/ClientTest.mm
Normal file
@@ -0,0 +1,118 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
|
||||
// Global semaphore used for custom `es_delete_client` function
|
||||
dispatch_semaphore_t gSema;
|
||||
|
||||
// Note: The Client class does not use the `EndpointSecurityAPI` wrappers due
|
||||
// to circular dependency issues. It is a special case that uses the underlying
|
||||
// ES API `es_delete_client` directly. This test override will signal the
|
||||
// `gSema` semaphore to indicate it has been called.
|
||||
es_return_t es_delete_client(es_client_t *_Nullable client) {
|
||||
dispatch_semaphore_signal(gSema);
|
||||
return ES_RETURN_SUCCESS;
|
||||
};
|
||||
|
||||
@interface ClientTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation ClientTest
|
||||
|
||||
- (void)setUp {
|
||||
gSema = dispatch_semaphore_create(0);
|
||||
}
|
||||
|
||||
- (void)testConstructorsAndDestructors {
|
||||
// Ensure constructors set internal state properly
|
||||
// Anonymous scopes used to ensure destructors called as expected
|
||||
|
||||
// Null `es_client_t*` *shouldn't* trigger `es_delete_client`
|
||||
{
|
||||
Client c;
|
||||
XCTAssertEqual(c.Get(), nullptr);
|
||||
XCTAssertEqual(c.NewClientResult(), ES_NEW_CLIENT_RESULT_ERR_INTERNAL);
|
||||
}
|
||||
|
||||
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client called unexpectedly");
|
||||
|
||||
// Nonnull `es_client_t*` *should* trigger `es_delete_client`
|
||||
{
|
||||
int fake;
|
||||
es_client_t *fakeClient = (es_client_t *)&fake;
|
||||
Client c(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
XCTAssertEqual(c.Get(), fakeClient);
|
||||
XCTAssertEqual(c.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client not called within expected time window");
|
||||
|
||||
// Test move constructor
|
||||
{
|
||||
int fake;
|
||||
es_client_t *fakeClient = (es_client_t *)&fake;
|
||||
Client c1(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
|
||||
Client c2(std::move(c1));
|
||||
|
||||
XCTAssertEqual(c1.Get(), nullptr);
|
||||
XCTAssertEqual(c2.Get(), fakeClient);
|
||||
XCTAssertEqual(c2.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
// Ensure `es_delete_client` was only called once when both `c1` and `c2`
|
||||
// are destructed.
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client not called within expected time window");
|
||||
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client called unexpectedly");
|
||||
|
||||
// Test move assignment
|
||||
{
|
||||
int fake;
|
||||
es_client_t *fakeClient = (es_client_t *)&fake;
|
||||
Client c1(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
Client c2;
|
||||
|
||||
c2 = std::move(c1);
|
||||
|
||||
XCTAssertEqual(c1.Get(), nullptr);
|
||||
XCTAssertEqual(c2.Get(), fakeClient);
|
||||
XCTAssertEqual(c2.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
// Ensure `es_delete_client` was only called once when both `c1` and `c2`
|
||||
// are destructed.
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client not called within expected time window");
|
||||
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client called unexpectedly");
|
||||
}
|
||||
|
||||
- (void)testIsConnected {
|
||||
XCTAssertFalse(Client().IsConnected());
|
||||
XCTAssertFalse(Client(nullptr, ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED).IsConnected());
|
||||
XCTAssertTrue(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS).IsConnected());
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,52 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENDPOINTSECURITYAPI_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENDPOINTSECURITYAPI_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class EndpointSecurityAPI : public std::enable_shared_from_this<EndpointSecurityAPI> {
|
||||
public:
|
||||
virtual ~EndpointSecurityAPI() = default;
|
||||
|
||||
virtual Client NewClient(void (^message_handler)(es_client_t *, Message));
|
||||
|
||||
virtual bool Subscribe(const Client &client, const std::set<es_event_type_t> &);
|
||||
|
||||
virtual es_message_t *RetainMessage(const es_message_t *msg);
|
||||
virtual void ReleaseMessage(es_message_t *msg);
|
||||
|
||||
virtual bool RespondAuthResult(const Client &client, const Message &msg, es_auth_result_t result,
|
||||
bool cache);
|
||||
|
||||
virtual bool MuteProcess(const Client &client, const audit_token_t *tok);
|
||||
|
||||
virtual bool ClearCache(const Client &client);
|
||||
|
||||
virtual uint32_t ExecArgCount(const es_event_exec_t *event);
|
||||
virtual es_string_token_t ExecArg(const es_event_exec_t *event, uint32_t index);
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,87 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Client EndpointSecurityAPI::NewClient(void (^message_handler)(es_client_t *, Message)) {
|
||||
es_client_t *client = NULL;
|
||||
|
||||
auto shared_esapi = shared_from_this();
|
||||
es_new_client_result_t res = es_new_client(&client, ^(es_client_t *c, const es_message_t *msg) {
|
||||
@autoreleasepool {
|
||||
message_handler(c, Message(shared_esapi, msg));
|
||||
}
|
||||
});
|
||||
|
||||
return Client(client, res);
|
||||
}
|
||||
|
||||
es_message_t *EndpointSecurityAPI::RetainMessage(const es_message_t *msg) {
|
||||
if (@available(macOS 11.0, *)) {
|
||||
es_retain_message(msg);
|
||||
es_message_t *nonconst = const_cast<es_message_t *>(msg);
|
||||
return nonconst;
|
||||
} else {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return es_copy_message(msg);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
void EndpointSecurityAPI::ReleaseMessage(es_message_t *msg) {
|
||||
if (@available(macOS 11.0, *)) {
|
||||
es_release_message(msg);
|
||||
} else {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
return es_free_message(msg);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::Subscribe(const Client &client,
|
||||
const std::set<es_event_type_t> &event_types) {
|
||||
std::vector<es_event_type_t> subs(event_types.begin(), event_types.end());
|
||||
return es_subscribe(client.Get(), subs.data(), (uint32_t)subs.size()) == ES_RETURN_SUCCESS;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::RespondAuthResult(const Client &client, const Message &msg,
|
||||
es_auth_result_t result, bool cache) {
|
||||
return es_respond_auth_result(client.Get(), &(*msg), result, cache) == ES_RESPOND_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::MuteProcess(const Client &client, const audit_token_t *tok) {
|
||||
return es_mute_process(client.Get(), tok) == ES_RETURN_SUCCESS;
|
||||
}
|
||||
|
||||
bool EndpointSecurityAPI::ClearCache(const Client &client) {
|
||||
return es_clear_cache(client.Get()) == ES_CLEAR_CACHE_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
uint32_t EndpointSecurityAPI::ExecArgCount(const es_event_exec_t *event) {
|
||||
return es_exec_arg_count(event);
|
||||
}
|
||||
|
||||
es_string_token_t EndpointSecurityAPI::ExecArg(const es_event_exec_t *event, uint32_t index) {
|
||||
return es_exec_arg(event, index);
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
214
Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h
Normal file
@@ -0,0 +1,214 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
/// This file groups all of the enriched message types - that is the
|
||||
/// objects that are constructed to hold all enriched event data prior
|
||||
/// to being logged.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHEDTYPES_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHEDTYPES_H
|
||||
|
||||
#include <time.h>
|
||||
#include <uuid/uuid.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class EnrichedFile {
|
||||
public:
|
||||
EnrichedFile(std::optional<std::shared_ptr<std::string>> &&user,
|
||||
std::optional<std::shared_ptr<std::string>> &&group,
|
||||
std::optional<std::shared_ptr<std::string>> &&hash)
|
||||
: user_(std::move(user)),
|
||||
group_(std::move(group)),
|
||||
hash_(std::move(hash)) {}
|
||||
|
||||
private:
|
||||
std::optional<std::shared_ptr<std::string>> user_;
|
||||
std::optional<std::shared_ptr<std::string>> group_;
|
||||
std::optional<std::shared_ptr<std::string>> hash_;
|
||||
};
|
||||
|
||||
class EnrichedProcess {
|
||||
public:
|
||||
EnrichedProcess(std::optional<std::shared_ptr<std::string>> &&effective_user,
|
||||
std::optional<std::shared_ptr<std::string>> &&effective_group,
|
||||
std::optional<std::shared_ptr<std::string>> &&real_user,
|
||||
std::optional<std::shared_ptr<std::string>> &&real_group,
|
||||
EnrichedFile &&executable)
|
||||
: effective_user_(std::move(effective_user)),
|
||||
effective_group_(std::move(effective_group)),
|
||||
real_user_(std::move(real_user)),
|
||||
real_group_(std::move(real_group)),
|
||||
executable_(std::move(executable)) {}
|
||||
|
||||
const std::optional<std::shared_ptr<std::string>> &real_user() const {
|
||||
return real_user_;
|
||||
}
|
||||
const std::optional<std::shared_ptr<std::string>> &real_group() const {
|
||||
return real_group_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<std::shared_ptr<std::string>> effective_user_;
|
||||
std::optional<std::shared_ptr<std::string>> effective_group_;
|
||||
std::optional<std::shared_ptr<std::string>> real_user_;
|
||||
std::optional<std::shared_ptr<std::string>> real_group_;
|
||||
EnrichedFile executable_;
|
||||
};
|
||||
|
||||
class EnrichedEventType {
|
||||
public:
|
||||
EnrichedEventType(Message &&es_msg, EnrichedProcess &&instigator)
|
||||
: es_msg_(std::move(es_msg)), instigator_(std::move(instigator)) {}
|
||||
|
||||
EnrichedEventType(EnrichedEventType &&other)
|
||||
: es_msg_(std::move(other.es_msg_)),
|
||||
instigator_(std::move(other.instigator_)) {}
|
||||
|
||||
virtual ~EnrichedEventType() = default;
|
||||
|
||||
const es_message_t &es_msg() const { return *es_msg_; }
|
||||
|
||||
const EnrichedProcess &instigator() const { return instigator_; }
|
||||
|
||||
private:
|
||||
Message es_msg_;
|
||||
EnrichedProcess instigator_;
|
||||
};
|
||||
|
||||
class EnrichedClose : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedClose(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedFile &&target)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
target_(std::move(target)) {}
|
||||
|
||||
private:
|
||||
EnrichedFile target_;
|
||||
};
|
||||
|
||||
class EnrichedExchange : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedExchange(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedFile &&file1, EnrichedFile &&file2)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
file1_(std::move(file1)),
|
||||
file2_(std::move(file2)) {}
|
||||
|
||||
private:
|
||||
EnrichedFile file1_;
|
||||
EnrichedFile file2_;
|
||||
};
|
||||
|
||||
class EnrichedExec : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedExec(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedProcess &&target, std::optional<EnrichedFile> &&script,
|
||||
std::optional<EnrichedFile> working_dir)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
target_(std::move(target)),
|
||||
script_(std::move(script)),
|
||||
working_dir_(std::move(working_dir)) {}
|
||||
|
||||
private:
|
||||
EnrichedProcess target_;
|
||||
std::optional<EnrichedFile> script_;
|
||||
std::optional<EnrichedFile> working_dir_;
|
||||
};
|
||||
|
||||
class EnrichedExit : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedExit(Message &&es_msg, EnrichedProcess &&instigator)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)) {}
|
||||
};
|
||||
|
||||
class EnrichedFork : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedFork(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedProcess &&target)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
target_(std::move(target)) {}
|
||||
|
||||
private:
|
||||
EnrichedProcess target_;
|
||||
};
|
||||
|
||||
class EnrichedLink : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedLink(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedFile &&source, EnrichedFile &&target_dir)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
source_(std::move(source)),
|
||||
target_dir_(std::move(target_dir)) {}
|
||||
|
||||
private:
|
||||
EnrichedFile source_;
|
||||
EnrichedFile target_dir_;
|
||||
};
|
||||
|
||||
class EnrichedRename : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedRename(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedFile &&source, std::optional<EnrichedFile> &&target,
|
||||
std::optional<EnrichedFile> &&target_dir)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
source_(std::move(source)),
|
||||
target_(std::move(target)),
|
||||
target_dir_(std::move(target_dir)) {}
|
||||
|
||||
private:
|
||||
EnrichedFile source_;
|
||||
std::optional<EnrichedFile> target_;
|
||||
std::optional<EnrichedFile> target_dir_;
|
||||
};
|
||||
|
||||
class EnrichedUnlink : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedUnlink(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedFile &&target)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
target_(std::move(target)) {}
|
||||
|
||||
private:
|
||||
EnrichedFile target_;
|
||||
};
|
||||
|
||||
using EnrichedType =
|
||||
std::variant<EnrichedClose, EnrichedExchange, EnrichedExec, EnrichedExit,
|
||||
EnrichedFork, EnrichedLink, EnrichedRename, EnrichedUnlink>;
|
||||
|
||||
class EnrichedMessage {
|
||||
public:
|
||||
EnrichedMessage(EnrichedType &&msg) : msg_(std::move(msg)) {
|
||||
uuid_generate(uuid_);
|
||||
clock_gettime(CLOCK_REALTIME, &enrichment_time_);
|
||||
}
|
||||
|
||||
const EnrichedType &GetEnrichedMessage() { return msg_; }
|
||||
|
||||
private:
|
||||
uuid_t uuid_;
|
||||
struct timespec enrichment_time_;
|
||||
EnrichedType msg_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
44
Source/santad/EventProviders/EndpointSecurity/Enricher.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHER_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHER_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class Enricher {
|
||||
public:
|
||||
Enricher();
|
||||
virtual ~Enricher() = default;
|
||||
virtual std::shared_ptr<EnrichedMessage> Enrich(Message &&msg);
|
||||
virtual EnrichedProcess Enrich(const es_process_t &es_proc);
|
||||
virtual EnrichedFile Enrich(const es_file_t &es_file);
|
||||
|
||||
virtual std::optional<std::shared_ptr<std::string>> UsernameForUID(uid_t uid);
|
||||
virtual std::optional<std::shared_ptr<std::string>> UsernameForGID(gid_t gid);
|
||||
|
||||
private:
|
||||
SantaCache<uid_t, std::optional<std::shared_ptr<std::string>>>
|
||||
username_cache_;
|
||||
SantaCache<gid_t, std::optional<std::shared_ptr<std::string>>>
|
||||
groupname_cache_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
137
Source/santad/EventProviders/EndpointSecurity/Enricher.mm
Normal file
@@ -0,0 +1,137 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <grp.h>
|
||||
#include <pwd.h>
|
||||
#include <sys/types.h>
|
||||
#include <uuid/uuid.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Enricher::Enricher() : username_cache_(256), groupname_cache_(256) {}
|
||||
|
||||
std::shared_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
|
||||
// TODO(mlw): Consider potential design patterns that could help reduce memory usage under load
|
||||
// (such as maybe the flyweight pattern)
|
||||
switch (es_msg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedClose(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.close.target)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedExchange(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exchangedata.file1),
|
||||
Enrich(*es_msg->event.exchangedata.file2)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedExec(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.exec.target),
|
||||
(es_msg->version >= 2 && es_msg->event.exec.script)
|
||||
? std::make_optional(Enrich(*es_msg->event.exec.script))
|
||||
: std::nullopt,
|
||||
(es_msg->version >= 3) ? std::make_optional(Enrich(*es_msg->event.exec.cwd))
|
||||
: std::nullopt));
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedFork(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.fork.child)));
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT:
|
||||
return std::make_shared<EnrichedMessage>(
|
||||
EnrichedExit(std::move(es_msg), Enrich(*es_msg->process)));
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK:
|
||||
return std::make_shared<EnrichedMessage>(
|
||||
EnrichedLink(std::move(es_msg), Enrich(*es_msg->process),
|
||||
Enrich(*es_msg->event.link.source), Enrich(*es_msg->event.link.target_dir)));
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: {
|
||||
if (es_msg->event.rename.destination_type == ES_DESTINATION_TYPE_NEW_PATH) {
|
||||
return std::make_shared<EnrichedMessage>(EnrichedRename(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source),
|
||||
std::nullopt, Enrich(*es_msg->event.rename.destination.new_path.dir)));
|
||||
} else {
|
||||
return std::make_shared<EnrichedMessage>(EnrichedRename(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.rename.source),
|
||||
Enrich(*es_msg->event.rename.destination.existing_file), std::nullopt));
|
||||
}
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK:
|
||||
return std::make_shared<EnrichedMessage>(EnrichedUnlink(
|
||||
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.unlink.target)));
|
||||
default:
|
||||
// This is a programming error
|
||||
LOGE(@"Attempting to enrich an unhandled event type: %d", es_msg->event_type);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
EnrichedProcess Enricher::Enrich(const es_process_t &es_proc) {
|
||||
return EnrichedProcess(UsernameForUID(audit_token_to_euid(es_proc.audit_token)),
|
||||
UsernameForGID(audit_token_to_egid(es_proc.audit_token)),
|
||||
UsernameForUID(audit_token_to_ruid(es_proc.audit_token)),
|
||||
UsernameForGID(audit_token_to_rgid(es_proc.audit_token)),
|
||||
Enrich(*es_proc.executable));
|
||||
}
|
||||
|
||||
EnrichedFile Enricher::Enrich(const es_file_t &es_file) {
|
||||
// TODO(mlw): Consider having the enricher perform file hashing. This will
|
||||
// make more sense if we start including hashes in more event types.
|
||||
return EnrichedFile(UsernameForUID(es_file.stat.st_uid), UsernameForGID(es_file.stat.st_gid),
|
||||
std::nullopt);
|
||||
}
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> Enricher::UsernameForUID(uid_t uid) {
|
||||
std::optional<std::shared_ptr<std::string>> username = username_cache_.get(uid);
|
||||
|
||||
if (username.has_value()) {
|
||||
return username;
|
||||
} else {
|
||||
struct passwd *pw = getpwuid(uid);
|
||||
if (pw) {
|
||||
username = std::make_shared<std::string>(pw->pw_name);
|
||||
} else {
|
||||
username = std::nullopt;
|
||||
}
|
||||
|
||||
username_cache_.set(uid, username);
|
||||
|
||||
return username;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> Enricher::UsernameForGID(gid_t gid) {
|
||||
std::optional<std::shared_ptr<std::string>> groupname = groupname_cache_.get(gid);
|
||||
|
||||
if (groupname.has_value()) {
|
||||
return groupname;
|
||||
} else {
|
||||
struct group *gr = getgrgid(gid);
|
||||
if (gr) {
|
||||
groupname = std::make_shared<std::string>(gr->gr_name);
|
||||
} else {
|
||||
groupname = std::nullopt;
|
||||
}
|
||||
|
||||
groupname_cache_.set(gid, groupname);
|
||||
|
||||
return groupname;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
@@ -0,0 +1,49 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
|
||||
@interface EnricherTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation EnricherTest
|
||||
|
||||
- (void)testUidGid {
|
||||
Enricher enricher;
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> user = enricher.UsernameForUID(NOBODY_UID);
|
||||
XCTAssertTrue(user.has_value());
|
||||
XCTAssertEqual(strcmp(user->get()->c_str(), "nobody"), 0);
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> group = enricher.UsernameForGID(NOBODY_GID);
|
||||
XCTAssertTrue(group.has_value());
|
||||
XCTAssertEqual(strcmp(group->get()->c_str(), "nobody"), 0);
|
||||
|
||||
uid_t invalidUID = (uid_t)-123;
|
||||
gid_t invalidGID = (gid_t)-123;
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> invalidUser = enricher.UsernameForUID(invalidUID);
|
||||
XCTAssertFalse(invalidUser.has_value());
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> invalidGroup = enricher.UsernameForGID(invalidGID);
|
||||
XCTAssertFalse(invalidGroup.has_value());
|
||||
}
|
||||
|
||||
@end
|
||||
60
Source/santad/EventProviders/EndpointSecurity/Message.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MESSAGE_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MESSAGE_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class EndpointSecurityAPI;
|
||||
|
||||
class Message {
|
||||
public:
|
||||
Message(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
const es_message_t* es_msg);
|
||||
~Message();
|
||||
|
||||
Message(Message&& other);
|
||||
// Note: Safe to implement this, just not currently needed so left deleted.
|
||||
Message& operator=(Message&& rhs) = delete;
|
||||
|
||||
// In macOS 10.15, es_retain_message/es_release_message were unsupported
|
||||
// and required a full copy, which impacts performance if done too much...
|
||||
Message(const Message& other);
|
||||
Message& operator=(const Message& other) = delete;
|
||||
|
||||
// Operators to access underlying es_message_t
|
||||
const es_message_t* operator->() const { return es_msg_; }
|
||||
const es_message_t& operator*() const { return *es_msg_; }
|
||||
|
||||
std::string ParentProcessName() const;
|
||||
|
||||
private:
|
||||
std::shared_ptr<EndpointSecurityAPI> esapi_;
|
||||
es_message_t* es_msg_;
|
||||
|
||||
mutable std::string pname_;
|
||||
mutable std::string parent_pname_;
|
||||
|
||||
std::string GetProcessName(pid_t pid) const;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
65
Source/santad/EventProviders/EndpointSecurity/Message.mm
Normal file
@@ -0,0 +1,65 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
#include <libproc.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Message::Message(std::shared_ptr<EndpointSecurityAPI> esapi, const es_message_t *es_msg)
|
||||
: esapi_(esapi) {
|
||||
es_msg_ = esapi_->RetainMessage(es_msg);
|
||||
}
|
||||
|
||||
Message::~Message() {
|
||||
if (es_msg_) {
|
||||
esapi_->ReleaseMessage(es_msg_);
|
||||
}
|
||||
}
|
||||
|
||||
Message::Message(Message &&other) {
|
||||
esapi_ = std::move(other.esapi_);
|
||||
es_msg_ = other.es_msg_;
|
||||
other.es_msg_ = nullptr;
|
||||
}
|
||||
|
||||
Message::Message(const Message &other) {
|
||||
esapi_ = other.esapi_;
|
||||
es_msg_ = other.es_msg_;
|
||||
esapi_->RetainMessage(es_msg_);
|
||||
}
|
||||
|
||||
std::string Message::ParentProcessName() const {
|
||||
if (parent_pname_.length() == 0) {
|
||||
parent_pname_ = GetProcessName(es_msg_->process->ppid);
|
||||
}
|
||||
return parent_pname_;
|
||||
}
|
||||
|
||||
std::string Message::GetProcessName(pid_t pid) const {
|
||||
// Note: proc_name() accesses the `pbi_name` field of `struct proc_bsdinfo`. The size of `pname`
|
||||
// here is meant to match the size of `pbi_name`, and one extra byte ensure zero-terminated.
|
||||
char pname[MAXCOMLEN * 2 + 1] = {};
|
||||
if (proc_name(pid, pname, sizeof(pname)) > 0) {
|
||||
return std::string(pname);
|
||||
} else {
|
||||
return std::string("");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
135
Source/santad/EventProviders/EndpointSecurity/MessageTest.mm
Normal file
@@ -0,0 +1,135 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libproc.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
bool IsPidInUse(pid_t pid) {
|
||||
char pname[MAXCOMLEN * 2 + 1] = {};
|
||||
errno = 0;
|
||||
if (proc_name(pid, pname, sizeof(pname)) <= 0 && errno == ESRCH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The PID may or may not actually be in use, but assume it is
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to find an unused PID by looking for libproc returning ESRCH errno.
|
||||
// Start searching backwards from PID_MAX to increase likelyhood that the
|
||||
// returned PID will still be unused by the time it's being used.
|
||||
// TODO(mlw): Alternatively, we could inject the `proc_name` function into
|
||||
// the `Message` object to remove the guesswork here.
|
||||
pid_t AttemptToFindUnusedPID() {
|
||||
for (pid_t pid = 99999 /* PID_MAX */; pid > 1; pid--) {
|
||||
if (!IsPidInUse(pid)) {
|
||||
return pid;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@interface MessageTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation MessageTest
|
||||
|
||||
- (void)setUp {
|
||||
}
|
||||
|
||||
- (void)testConstructorsAndDestructors {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
// Constructing a `Message` retains the underlying `es_message_t` and it is
|
||||
// released when the `Message` object is destructed.
|
||||
{ Message m(mockESApi, &esMsg); }
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testCopyConstructor {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
EXPECT_CALL(*mockESApi, ReleaseMessage(testing::_))
|
||||
.Times(2)
|
||||
.After(EXPECT_CALL(*mockESApi, RetainMessage(testing::_))
|
||||
.Times(2)
|
||||
.WillRepeatedly(testing::Return(&esMsg)));
|
||||
|
||||
{
|
||||
Message msg1(mockESApi, &esMsg);
|
||||
Message msg2(msg1);
|
||||
|
||||
// Both messages should now point to the same `es_message_t`
|
||||
XCTAssertEqual(msg1.operator->(), &esMsg);
|
||||
XCTAssertEqual(msg2.operator->(), &esMsg);
|
||||
}
|
||||
|
||||
// Ensure the retain/release mocks were called the expected number of times
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testGetParentProcessName {
|
||||
// Construct a message where the parent pid is ourself
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(getpid(), 0));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
// Search for an *existing* parent process.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
std::string got = msg.ParentProcessName();
|
||||
std::string want = getprogname();
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
// Search for a *non-existent* parent process.
|
||||
{
|
||||
pid_t newPpid = AttemptToFindUnusedPID();
|
||||
proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(newPpid, 34));
|
||||
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
std::string got = msg.ParentProcessName();
|
||||
std::string want = "";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,75 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MOCKENDPOINTSECURITYAPI_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MOCKENDPOINTSECURITYAPI_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
|
||||
class MockEndpointSecurityAPI
|
||||
: public santa::santad::event_providers::endpoint_security::EndpointSecurityAPI {
|
||||
public:
|
||||
MOCK_METHOD(santa::santad::event_providers::endpoint_security::Client, NewClient,
|
||||
(void (^message_handler)(
|
||||
es_client_t *, santa::santad::event_providers::endpoint_security::Message)));
|
||||
|
||||
MOCK_METHOD(bool, Subscribe,
|
||||
(const santa::santad::event_providers::endpoint_security::Client &,
|
||||
const std::set<es_event_type_t> &));
|
||||
|
||||
MOCK_METHOD(es_message_t *, RetainMessage, (const es_message_t *msg));
|
||||
MOCK_METHOD(void, ReleaseMessage, (es_message_t * msg));
|
||||
|
||||
MOCK_METHOD(bool, RespondAuthResult,
|
||||
(const santa::santad::event_providers::endpoint_security::Client &,
|
||||
const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
es_auth_result_t result, bool cache));
|
||||
|
||||
MOCK_METHOD(bool, MuteProcess,
|
||||
(const santa::santad::event_providers::endpoint_security::Client &,
|
||||
const audit_token_t *tok));
|
||||
|
||||
MOCK_METHOD(bool, ClearCache,
|
||||
(const santa::santad::event_providers::endpoint_security::Client &));
|
||||
|
||||
MOCK_METHOD(uint32_t, ExecArgCount, (const es_event_exec_t *event));
|
||||
MOCK_METHOD(es_string_token_t, ExecArg, (const es_event_exec_t *event, uint32_t index));
|
||||
|
||||
void SetExpectationsESNewClient() {
|
||||
EXPECT_CALL(*this, NewClient)
|
||||
.WillOnce(testing::Return(santa::santad::event_providers::endpoint_security::Client(
|
||||
nullptr, ES_NEW_CLIENT_RESULT_SUCCESS)));
|
||||
EXPECT_CALL(*this, MuteProcess).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*this, ClearCache).WillRepeatedly(testing::Return(true));
|
||||
EXPECT_CALL(*this, Subscribe).WillRepeatedly(testing::Return(true));
|
||||
}
|
||||
|
||||
void SetExpectationsRetainReleaseMessage(es_message_t *msg) {
|
||||
EXPECT_CALL(*this, ReleaseMessage).Times(testing::AnyNumber());
|
||||
EXPECT_CALL(*this, RetainMessage).WillRepeatedly(testing::Return(msg));
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,104 +0,0 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <bsm/libbsm.h>
|
||||
|
||||
CF_EXTERN_C_BEGIN
|
||||
es_string_token_t MakeStringToken(const NSString *_Nonnull s);
|
||||
|
||||
es_file_t MakeESFile(const char *_Nonnull path);
|
||||
es_process_t MakeESProcess(es_file_t *_Nonnull esFile);
|
||||
es_message_t MakeESMessage(es_event_type_t eventType, es_process_t *_Nonnull instigator,
|
||||
struct timespec ts);
|
||||
CF_EXTERN_C_END
|
||||
|
||||
@class ESMessage;
|
||||
typedef void (^ESMessageBuilderBlock)(ESMessage *_Nonnull builder);
|
||||
|
||||
// An ObjC builder wrapper around es_message_t
|
||||
@interface ESMessage : NSObject
|
||||
@property(nonatomic, readwrite, strong) NSString *_Nullable binaryPath;
|
||||
@property(nonatomic, readwrite) es_file_t *_Nonnull executable;
|
||||
@property(nonatomic, readwrite) es_process_t *_Nonnull process;
|
||||
@property(nonatomic, readwrite) es_message_t *_Nonnull message;
|
||||
@property(nonatomic, readonly) pid_t pid;
|
||||
|
||||
- (instancetype _Nonnull)initWithBlock:(ESMessageBuilderBlock _Nullable)block
|
||||
NS_DESIGNATED_INITIALIZER;
|
||||
@end
|
||||
|
||||
@interface ESResponse : NSObject
|
||||
@property(nonatomic) es_auth_result_t result;
|
||||
@property(nonatomic) bool shouldCache;
|
||||
@end
|
||||
|
||||
typedef void (^ESCallback)(ESResponse *_Nonnull);
|
||||
|
||||
// Singleton wrapper around all of the kernel-level EndpointSecurity framework functions.
|
||||
@interface MockEndpointSecurity : NSObject
|
||||
@property NSMutableArray *_Nonnull subscriptions;
|
||||
- (void)reset;
|
||||
- (void)registerResponseCallback:(es_event_type_t)t withCallback:(ESCallback _Nonnull)callback;
|
||||
- (void)triggerHandler:(es_message_t *_Nonnull)msg;
|
||||
|
||||
/// Retrieve an initialized singleton MockEndpointSecurity object
|
||||
+ (instancetype _Nonnull)mockEndpointSecurity;
|
||||
@end
|
||||
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_message_t *_Nullable es_copy_message(const es_message_t *_Nonnull msg);
|
||||
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
void es_free_message(es_message_t *_Nonnull msg);
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_new_client_result_t es_new_client(es_client_t *_Nullable *_Nonnull client,
|
||||
es_handler_block_t _Nonnull handler);
|
||||
|
||||
API_AVAILABLE(macos(10.15)) API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_mute_process(es_client_t * _Nonnull client,
|
||||
const audit_token_t * _Nonnull audit_token);
|
||||
|
||||
#if defined(MAC_OS_VERSION_12_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0
|
||||
API_AVAILABLE(macos(12.0))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_muted_paths_events(es_client_t *_Nonnull client,
|
||||
es_muted_paths_t *_Nonnull *_Nullable muted_paths);
|
||||
|
||||
API_AVAILABLE(macos(12.0))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
void es_release_muted_paths(es_muted_paths_t *_Nonnull muted_paths);
|
||||
#endif
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_respond_result_t es_respond_auth_result(es_client_t *_Nonnull client,
|
||||
const es_message_t *_Nonnull message,
|
||||
es_auth_result_t result, bool cache);
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_subscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
|
||||
uint32_t event_count);
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos) es_return_t es_delete_client(es_client_t *_Nullable client);
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_unsubscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
|
||||
uint32_t event_count);
|
||||
@@ -1,367 +0,0 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h"
|
||||
|
||||
CF_EXTERN_C_BEGIN
|
||||
es_string_token_t MakeStringToken(const NSString *_Nonnull s) {
|
||||
return (es_string_token_t){
|
||||
.length = [s length],
|
||||
.data = [s UTF8String],
|
||||
};
|
||||
}
|
||||
|
||||
es_file_t MakeESFile(const char *path) {
|
||||
es_file_t esFile = {};
|
||||
|
||||
esFile.path.data = path;
|
||||
esFile.path.length = strlen(path);
|
||||
esFile.path_truncated = false;
|
||||
|
||||
// Note: stat info is currently unused / not populated
|
||||
|
||||
return esFile;
|
||||
}
|
||||
|
||||
es_process_t MakeESProcess(es_file_t *esFile) {
|
||||
es_process_t esProc = {};
|
||||
esProc.executable = esFile;
|
||||
return esProc;
|
||||
}
|
||||
|
||||
es_message_t MakeESMessage(es_event_type_t eventType, es_process_t *instigator,
|
||||
struct timespec ts) {
|
||||
es_message_t esMsg = {};
|
||||
|
||||
esMsg.time = ts;
|
||||
esMsg.event_type = eventType;
|
||||
esMsg.process = instigator;
|
||||
|
||||
return esMsg;
|
||||
}
|
||||
|
||||
CF_EXTERN_C_END
|
||||
|
||||
@implementation ESMessage
|
||||
- (instancetype)init {
|
||||
return [self initWithBlock:nil];
|
||||
}
|
||||
|
||||
- (instancetype)initWithBlock:(ESMessageBuilderBlock)block {
|
||||
NSParameterAssert(block);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_pid = arc4random();
|
||||
[self initBaseObjects];
|
||||
block(self);
|
||||
[self fillLinks];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initBaseObjects {
|
||||
self.executable = static_cast<es_file_t *>(calloc(1, sizeof(es_file_t)));
|
||||
self.process = static_cast<es_process_t *>(calloc(1, sizeof(es_process_t)));
|
||||
|
||||
self.process->ppid = self.pid;
|
||||
self.process->original_ppid = self.pid;
|
||||
self.process->group_id = static_cast<pid_t>(arc4random());
|
||||
self.process->session_id = static_cast<pid_t>(arc4random());
|
||||
self.process->codesigning_flags =
|
||||
0x1 | 0x20000000; // CS_VALID | CS_SIGNED -> See kern/cs_blobs.h
|
||||
self.process->is_platform_binary = false;
|
||||
self.process->is_es_client = false;
|
||||
|
||||
self.message = static_cast<es_message_t *>(calloc(1, sizeof(es_message_t)));
|
||||
self.message->version = 4;
|
||||
self.message->mach_time = DISPATCH_TIME_NOW;
|
||||
self.message->deadline = DISPATCH_TIME_FOREVER;
|
||||
self.message->seq_num = 1;
|
||||
}
|
||||
|
||||
- (void)fillLinks {
|
||||
if (self.binaryPath != nil) {
|
||||
self.executable->path = MakeStringToken(self.binaryPath);
|
||||
}
|
||||
|
||||
if (self.process->executable == NULL) {
|
||||
self.process->executable = self.executable;
|
||||
}
|
||||
if (self.message->process == NULL) {
|
||||
self.message->process = self.process;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
free(self.process);
|
||||
free(self.executable);
|
||||
free(self.message);
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation ESResponse
|
||||
@end
|
||||
|
||||
@interface MockESClient : NSObject
|
||||
@property NSMutableArray *_Nonnull subscriptions;
|
||||
@property es_handler_block_t handler;
|
||||
@end
|
||||
|
||||
@implementation MockESClient
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
@synchronized(self) {
|
||||
_subscriptions = [NSMutableArray arrayWithCapacity:ES_EVENT_TYPE_LAST];
|
||||
for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) {
|
||||
[self.subscriptions addObject:@NO];
|
||||
}
|
||||
}
|
||||
}
|
||||
return self;
|
||||
};
|
||||
|
||||
- (void)resetSubscriptions {
|
||||
for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) {
|
||||
_subscriptions[i] = @NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)triggerHandler:(es_message_t *_Nonnull)msg {
|
||||
self.handler((__bridge es_client_t *_Nullable)self, msg);
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
@synchronized(self) {
|
||||
[self.subscriptions removeAllObjects];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface MockEndpointSecurity ()
|
||||
@property NSMutableArray<MockESClient *> *clients;
|
||||
|
||||
// Array of collections of ESCallback blocks
|
||||
// This should be of size ES_EVENT_TYPE_LAST, allowing for indexing by ES_EVENT_TYPE_xxx members.
|
||||
@property NSMutableArray<NSMutableArray<ESCallback> *> *responseCallbacks;
|
||||
@end
|
||||
|
||||
@implementation MockEndpointSecurity
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
@synchronized(self) {
|
||||
_clients = [NSMutableArray array];
|
||||
_responseCallbacks = [NSMutableArray arrayWithCapacity:ES_EVENT_TYPE_LAST];
|
||||
for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) {
|
||||
[self.responseCallbacks addObject:[NSMutableArray array]];
|
||||
}
|
||||
[self reset];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
};
|
||||
|
||||
- (void)resetResponseCallbacks {
|
||||
for (NSMutableArray *callback in self.responseCallbacks) {
|
||||
if (callback != nil) {
|
||||
[callback removeAllObjects];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reset {
|
||||
@synchronized(self) {
|
||||
[self.clients removeAllObjects];
|
||||
[self resetResponseCallbacks];
|
||||
}
|
||||
};
|
||||
|
||||
- (void)newClient:(es_client_t *_Nullable *_Nonnull)client
|
||||
handler:(es_handler_block_t __strong)handler {
|
||||
// es_client_t is generally used as a pointer to an opaque struct (secretly a mach port).
|
||||
// There is also a few nonnull initialization checks on it.
|
||||
MockESClient *mockClient = [[MockESClient alloc] init];
|
||||
*client = (__bridge es_client_t *)mockClient;
|
||||
mockClient.handler = handler;
|
||||
[self.clients addObject:mockClient];
|
||||
}
|
||||
|
||||
- (BOOL)removeClient:(es_client_t *_Nonnull)client {
|
||||
MockESClient *clientToRemove = [self findClient:client];
|
||||
|
||||
if (!clientToRemove) {
|
||||
NSLog(@"Attempted to remove unknown mock es client.");
|
||||
return NO;
|
||||
}
|
||||
|
||||
[self.clients removeObject:clientToRemove];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)triggerHandler:(es_message_t *_Nonnull)msg {
|
||||
for (MockESClient *client in self.clients) {
|
||||
if (client.subscriptions[msg->event_type]) {
|
||||
[client triggerHandler:msg];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)registerResponseCallback:(es_event_type_t)t withCallback:(ESCallback _Nonnull)callback {
|
||||
@synchronized(self) {
|
||||
[self.responseCallbacks[t] addObject:callback];
|
||||
}
|
||||
}
|
||||
|
||||
- (es_respond_result_t)respond_auth_result:(const es_message_t *_Nonnull)msg
|
||||
result:(es_auth_result_t)result
|
||||
cache:(bool)cache {
|
||||
@synchronized(self) {
|
||||
ESResponse *response = [[ESResponse alloc] init];
|
||||
response.result = result;
|
||||
response.shouldCache = cache;
|
||||
for (void (^callback)(ESResponse *) in self.responseCallbacks[msg->event_type]) {
|
||||
callback(response);
|
||||
}
|
||||
}
|
||||
return ES_RESPOND_RESULT_SUCCESS;
|
||||
};
|
||||
|
||||
- (MockESClient *)findClient:(es_client_t *)client {
|
||||
for (MockESClient *c in self.clients) {
|
||||
// Since we're mocking out a C interface and using this exact pointer as our
|
||||
// client identifier, only check for pointer equality.
|
||||
if (client == (__bridge es_client_t *)c) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)setSubscriptions:(const es_event_type_t *_Nonnull)events
|
||||
event_count:(uint32_t)event_count
|
||||
value:(NSNumber *)value
|
||||
client:(es_client_t *)client {
|
||||
@synchronized(self) {
|
||||
MockESClient *toUpdate = [self findClient:client];
|
||||
|
||||
if (toUpdate == nil) {
|
||||
NSLog(@"setting subscription for unknown client");
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < event_count; i++) {
|
||||
toUpdate.subscriptions[events[i]] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ (instancetype _Nonnull)mockEndpointSecurity {
|
||||
static MockEndpointSecurity *sharedES;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedES = [[MockEndpointSecurity alloc] init];
|
||||
});
|
||||
return sharedES;
|
||||
};
|
||||
@end
|
||||
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_message_t *_Nullable es_copy_message(const es_message_t *_Nonnull msg) {
|
||||
return (es_message_t *)msg;
|
||||
};
|
||||
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
void es_free_message(es_message_t *_Nonnull msg){};
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_new_client_result_t es_new_client(es_client_t *_Nullable *_Nonnull client,
|
||||
es_handler_block_t _Nonnull handler) {
|
||||
[[MockEndpointSecurity mockEndpointSecurity] newClient:client handler:handler];
|
||||
return ES_NEW_CLIENT_RESULT_SUCCESS;
|
||||
};
|
||||
|
||||
es_return_t es_mute_process(es_client_t * _Nonnull client,
|
||||
const audit_token_t * _Nonnull audit_token) {
|
||||
return ES_RETURN_SUCCESS;
|
||||
}
|
||||
|
||||
#if defined(MAC_OS_VERSION_12_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0
|
||||
API_AVAILABLE(macos(12.0))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_muted_paths_events(es_client_t *_Nonnull client,
|
||||
es_muted_paths_t *_Nonnull *_Nullable muted_paths) {
|
||||
es_muted_paths_t *tmp = (es_muted_paths_t *)malloc(sizeof(es_muted_paths_t));
|
||||
|
||||
tmp->count = 0;
|
||||
*muted_paths = (es_muted_paths_t *_Nullable)tmp;
|
||||
|
||||
return ES_RETURN_SUCCESS;
|
||||
};
|
||||
|
||||
API_AVAILABLE(macos(12.0))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
void es_release_muted_paths(es_muted_paths_t *_Nonnull muted_paths) {
|
||||
free(muted_paths);
|
||||
}
|
||||
#endif
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos) es_return_t es_delete_client(es_client_t *_Nullable client) {
|
||||
if (![[MockEndpointSecurity mockEndpointSecurity] removeClient:client]) {
|
||||
return ES_RETURN_ERROR;
|
||||
}
|
||||
return ES_RETURN_SUCCESS;
|
||||
};
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_respond_result_t es_respond_auth_result(es_client_t *_Nonnull client,
|
||||
const es_message_t *_Nonnull message,
|
||||
es_auth_result_t result, bool cache) {
|
||||
return [[MockEndpointSecurity mockEndpointSecurity] respond_auth_result:message
|
||||
result:result
|
||||
cache:cache];
|
||||
};
|
||||
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_subscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
|
||||
uint32_t event_count) {
|
||||
[[MockEndpointSecurity mockEndpointSecurity] setSubscriptions:events
|
||||
event_count:event_count
|
||||
value:@YES
|
||||
client:client];
|
||||
return ES_RETURN_SUCCESS;
|
||||
}
|
||||
API_AVAILABLE(macos(10.15))
|
||||
API_UNAVAILABLE(ios, tvos, watchos)
|
||||
es_return_t es_unsubscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
|
||||
uint32_t event_count) {
|
||||
[[MockEndpointSecurity mockEndpointSecurity] setSubscriptions:events
|
||||
event_count:event_count
|
||||
value:@NO
|
||||
client:client];
|
||||
|
||||
return ES_RETURN_SUCCESS;
|
||||
};
|
||||
@@ -1,210 +0,0 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTCachingEndpointSecurityManager.h"
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SantaCache.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <bsm/libbsm.h>
|
||||
|
||||
uint64_t GetCurrentUptime() {
|
||||
return clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
}
|
||||
template <>
|
||||
uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const &t) {
|
||||
return (SantaCacheHasher<uint64_t>(t.fsid) << 1) ^ SantaCacheHasher<uint64_t>(t.fileid);
|
||||
}
|
||||
|
||||
@implementation SNTCachingEndpointSecurityManager {
|
||||
// Create 2 separate caches, mapping from the (filesysem + vnode ID) to a decision with a timestamp.
|
||||
// The root cache is for decisions on the root volume, which can never be unmounted and the other
|
||||
// is for executions from all other volumes. This cache will be emptied if any volume is unmounted.
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *_rootDecisionCache;
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *_nonRootDecisionCache;
|
||||
uint64_t _rootVnodeID;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_rootDecisionCache = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
_nonRootDecisionCache = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
|
||||
// Store the filesystem ID of the root vnode for split-cache usage.
|
||||
// If the stat fails for any reason _rootVnodeID will be 0 and all decisions will be in a single cache.
|
||||
struct stat rootStat;
|
||||
if (stat("/", &rootStat) == 0) {
|
||||
_rootVnodeID = (uint64_t)rootStat.st_dev;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (_rootDecisionCache) delete _rootDecisionCache;
|
||||
if (_nonRootDecisionCache) delete _nonRootDecisionCache;
|
||||
}
|
||||
|
||||
- (BOOL)respondFromCache:(es_message_t *)m API_AVAILABLE(macos(10.15)) {
|
||||
auto vnode_id = [self vnodeIDForFile:m->event.exec.target->executable];
|
||||
while (true) {
|
||||
// Check to see if item is in cache
|
||||
auto return_action = [self checkCache:vnode_id];
|
||||
|
||||
// If item was in cache with a valid response, return it.
|
||||
// If item is in cache but hasn't received a response yet, sleep for a bit.
|
||||
// If item is not in cache, break out of loop and forward request to callback.
|
||||
if (RESPONSE_VALID(return_action)) {
|
||||
switch (return_action) {
|
||||
case ACTION_RESPOND_ALLOW:
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
|
||||
break;
|
||||
case ACTION_RESPOND_ALLOW_COMPILER: {
|
||||
pid_t pid = audit_token_to_pid(m->process->audit_token);
|
||||
[self setIsCompilerPID:pid];
|
||||
// Don't let ES cache compilers
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
break;
|
||||
}
|
||||
default: es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false); break;
|
||||
}
|
||||
return YES;
|
||||
} else if (return_action == ACTION_REQUEST_BINARY || return_action == ACTION_RESPOND_ACK) {
|
||||
// TODO(rah): Look at a replacement for msleep(), maybe NSCondition
|
||||
usleep(5000);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[self addToCache:vnode_id decision:ACTION_REQUEST_BINARY currentTicks:0];
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (int)postAction:(santa_action_t)action
|
||||
forMessage:(santa_message_t)sm API_AVAILABLE(macos(10.15)) {
|
||||
es_respond_result_t ret;
|
||||
switch (action) {
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
[self setIsCompilerPID:sm.pid];
|
||||
|
||||
// Allow the exec and cache in our internal cache but don't let ES cache, because then
|
||||
// we won't see future execs of the compiler in order to record the PID.
|
||||
[self addToCache:sm.vnode_id
|
||||
decision:ACTION_RESPOND_ALLOW_COMPILER
|
||||
currentTicks:GetCurrentUptime()];
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW,
|
||||
false);
|
||||
break;
|
||||
case ACTION_RESPOND_ALLOW:
|
||||
case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE:
|
||||
[self addToCache:sm.vnode_id decision:ACTION_RESPOND_ALLOW currentTicks:GetCurrentUptime()];
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW,
|
||||
true);
|
||||
break;
|
||||
case ACTION_RESPOND_DENY:
|
||||
[self addToCache:sm.vnode_id decision:ACTION_RESPOND_DENY currentTicks:GetCurrentUptime()];
|
||||
OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_TOOLONG:
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_DENY,
|
||||
false);
|
||||
break;
|
||||
case ACTION_RESPOND_ACK: return ES_RESPOND_RESULT_SUCCESS;
|
||||
default: ret = ES_RESPOND_RESULT_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
- (void)addToCache:(santa_vnode_id_t)identifier
|
||||
decision:(santa_action_t)decision
|
||||
currentTicks:(uint64_t)microsecs {
|
||||
auto _decisionCache = [self cacheForVnodeID:identifier];
|
||||
switch (decision) {
|
||||
case ACTION_REQUEST_BINARY:
|
||||
_decisionCache->set(identifier, (uint64_t)ACTION_REQUEST_BINARY << 56, 0);
|
||||
break;
|
||||
case ACTION_RESPOND_ACK:
|
||||
_decisionCache->set(identifier, (uint64_t)ACTION_RESPOND_ACK << 56,
|
||||
((uint64_t)ACTION_REQUEST_BINARY << 56));
|
||||
break;
|
||||
case ACTION_RESPOND_ALLOW:
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
case ACTION_RESPOND_DENY: {
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
uint64_t val = ((uint64_t)decision << 56) | (microsecs & 0xFFFFFFFFFFFFFF);
|
||||
if (!_decisionCache->set(identifier, val, ((uint64_t)ACTION_REQUEST_BINARY << 56))) {
|
||||
_decisionCache->set(identifier, val, ((uint64_t)ACTION_RESPOND_ACK << 56));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE: {
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
uint64_t val = ((uint64_t)decision << 56) | (microsecs & 0xFFFFFFFFFFFFFF);
|
||||
_decisionCache->set(identifier, val, 0);
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
// TODO(rah): Look at a replacement for wakeup(), maybe NSCondition
|
||||
}
|
||||
|
||||
- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly API_AVAILABLE(macos(10.15)) {
|
||||
_nonRootDecisionCache->clear();
|
||||
if (!nonRootOnly) _rootDecisionCache->clear();
|
||||
if (!self.connectionEstablished) return YES; // if not connected, there's nothing to flush.
|
||||
return es_clear_cache(self.client) == ES_CLEAR_CACHE_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
- (NSArray<NSNumber *> *)cacheCounts {
|
||||
return @[ @(_rootDecisionCache->count()), @(_nonRootDecisionCache->count()) ];
|
||||
}
|
||||
|
||||
- (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID {
|
||||
auto result = ACTION_UNSET;
|
||||
uint64_t decision_time = 0;
|
||||
|
||||
uint64_t cache_val = [self cacheForVnodeID:vnodeID]->get(vnodeID);
|
||||
if (cache_val == 0) return result;
|
||||
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
result = (santa_action_t)(cache_val >> 56);
|
||||
decision_time = (cache_val & ~(0xFF00000000000000));
|
||||
|
||||
if (RESPONSE_VALID(result)) {
|
||||
if (result == ACTION_RESPOND_DENY) {
|
||||
auto expiry_time = decision_time + (500 * 100000); // kMaxCacheDenyTimeMilliseconds
|
||||
if (expiry_time < GetCurrentUptime()) {
|
||||
[self cacheForVnodeID:vnodeID]->remove(vnodeID);
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeID {
|
||||
[self cacheForVnodeID:vnodeID]->remove(vnodeID);
|
||||
// TODO(rah): Look at a replacement for wakeup(), maybe NSCondition
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (SantaCache<santa_vnode_id_t, uint64_t> *)cacheForVnodeID:(santa_vnode_id_t)vnodeID {
|
||||
return (vnodeID.fsid == _rootVnodeID || _rootVnodeID == 0) ? _rootDecisionCache : _nonRootDecisionCache;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,291 +0,0 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <DiskArbitration/DiskArbitration.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#import <bsm/libbsm.h>
|
||||
|
||||
#include <sys/mount.h>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/santad/EventProviders/SNTDeviceManager.h"
|
||||
|
||||
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
|
||||
#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h"
|
||||
|
||||
@interface SNTDeviceManagerTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@end
|
||||
|
||||
@implementation SNTDeviceManagerTest
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator eventLogType]).andReturn(-1);
|
||||
|
||||
fclose(stdout);
|
||||
}
|
||||
|
||||
- (ESResponse *)triggerTestMountEvent:(SNTDeviceManager *)deviceManager
|
||||
mockES:(MockEndpointSecurity *)mockES
|
||||
mockDA:(MockDiskArbitration *)mockDA
|
||||
eventType:(es_event_type_t)eventType
|
||||
diskInfoOverrides:(NSDictionary *)diskInfo {
|
||||
if (!deviceManager.subscribed) {
|
||||
// [deviceManager listen] is synchronous, but we want to asynchronously dispatch it
|
||||
// with an enforced timeout to ensure that we never run into issues where the client
|
||||
// never instantiates.
|
||||
XCTestExpectation *initExpectation =
|
||||
[self expectationWithDescription:@"Wait for SNTDeviceManager to instantiate"];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
[deviceManager listen];
|
||||
});
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
while (!deviceManager.subscribed)
|
||||
;
|
||||
[initExpectation fulfill];
|
||||
});
|
||||
[self waitForExpectations:@[ initExpectation ] timeout:60.0];
|
||||
}
|
||||
|
||||
struct statfs *fs = static_cast<struct statfs *>(calloc(1, sizeof(struct statfs)));
|
||||
NSString *test_mntfromname = @"/dev/disk2s1";
|
||||
NSString *test_mntonname = @"/Volumes/KATE'S 4G";
|
||||
const char *c_mntfromname = [test_mntfromname UTF8String];
|
||||
const char *c_mntonname = [test_mntonname UTF8String];
|
||||
|
||||
strncpy(fs->f_mntfromname, c_mntfromname, MAXPATHLEN);
|
||||
strncpy(fs->f_mntonname, c_mntonname, MAXPATHLEN);
|
||||
|
||||
MockDADisk *disk = [[MockDADisk alloc] init];
|
||||
disk.diskDescription = @{
|
||||
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey : @"USB",
|
||||
(__bridge NSString *)kDADiskDescriptionMediaRemovableKey : @YES,
|
||||
@"DAVolumeMountable" : @YES,
|
||||
@"DAVolumePath" : test_mntonname,
|
||||
@"DADeviceModel" : @"Some device model",
|
||||
@"DADevicePath" : test_mntonname,
|
||||
@"DADeviceVendor" : @"Some vendor",
|
||||
@"DAAppearanceTime" : @0,
|
||||
@"DAMediaBSDName" : test_mntfromname,
|
||||
};
|
||||
|
||||
if (diskInfo != nil) {
|
||||
NSMutableDictionary *mergedDiskDescription = [disk.diskDescription mutableCopy];
|
||||
for (NSString *key in diskInfo) {
|
||||
mergedDiskDescription[key] = diskInfo[key];
|
||||
}
|
||||
disk.diskDescription = (NSDictionary *)mergedDiskDescription;
|
||||
}
|
||||
|
||||
[mockDA insert:disk bsdName:test_mntfromname];
|
||||
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.binaryPath = @"/System/Library/Filesystems/msdos.fs/Contents/Resources/mount_msdos";
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = eventType;
|
||||
if (eventType == ES_EVENT_TYPE_AUTH_MOUNT) {
|
||||
m.message->event = (es_events_t){.mount = {.statfs = fs}};
|
||||
} else {
|
||||
m.message->event = (es_events_t){.remount = {.statfs = fs}};
|
||||
}
|
||||
}];
|
||||
|
||||
XCTestExpectation *mountExpectation =
|
||||
[self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:eventType
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[mountExpectation fulfill];
|
||||
}];
|
||||
|
||||
[mockES triggerHandler:m.message];
|
||||
|
||||
[self waitForExpectations:@[ mountExpectation ] timeout:60.0];
|
||||
free(fs);
|
||||
|
||||
return got;
|
||||
}
|
||||
|
||||
- (void)testUSBBlockDisabled {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA reset];
|
||||
|
||||
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
|
||||
deviceManager.blockUSBMount = NO;
|
||||
ESResponse *got = [self triggerTestMountEvent:deviceManager
|
||||
mockES:mockES
|
||||
mockDA:mockDA
|
||||
eventType:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
|
||||
- (void)testRemount {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA reset];
|
||||
|
||||
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
|
||||
deviceManager.blockUSBMount = YES;
|
||||
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"Wait for SNTDeviceManager's blockCallback to trigger"];
|
||||
|
||||
__block NSString *gotmntonname, *gotmntfromname;
|
||||
__block NSArray<NSString *> *gotRemountedArgs;
|
||||
deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
gotRemountedArgs = event.remountArgs;
|
||||
gotmntonname = event.mntonname;
|
||||
gotmntfromname = event.mntfromname;
|
||||
[expectation fulfill];
|
||||
};
|
||||
|
||||
ESResponse *got = [self triggerTestMountEvent:deviceManager
|
||||
mockES:mockES
|
||||
mockDA:mockDA
|
||||
eventType:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);
|
||||
XCTAssertEqual(mockDA.wasRemounted, YES);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertEqualObjects(gotRemountedArgs, deviceManager.remountArgs);
|
||||
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
|
||||
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
|
||||
}
|
||||
|
||||
- (void)testBlockNoRemount {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA reset];
|
||||
|
||||
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
|
||||
deviceManager.blockUSBMount = YES;
|
||||
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"Wait for SNTDeviceManager's blockCallback to trigger"];
|
||||
|
||||
__block NSString *gotmntonname, *gotmntfromname;
|
||||
__block NSArray<NSString *> *gotRemountedArgs;
|
||||
deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
gotRemountedArgs = event.remountArgs;
|
||||
gotmntonname = event.mntonname;
|
||||
gotmntfromname = event.mntfromname;
|
||||
[expectation fulfill];
|
||||
};
|
||||
|
||||
ESResponse *got = [self triggerTestMountEvent:deviceManager
|
||||
mockES:mockES
|
||||
mockDA:mockDA
|
||||
eventType:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertNil(gotRemountedArgs);
|
||||
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
|
||||
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
|
||||
}
|
||||
|
||||
- (void)testEnsureRemountsCannotChangePerms {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA reset];
|
||||
|
||||
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
|
||||
deviceManager.blockUSBMount = YES;
|
||||
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"Wait for SNTDeviceManager's blockCallback to trigger"];
|
||||
|
||||
__block NSString *gotmntonname, *gotmntfromname;
|
||||
__block NSArray<NSString *> *gotRemountedArgs;
|
||||
deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
gotRemountedArgs = event.remountArgs;
|
||||
gotmntonname = event.mntonname;
|
||||
gotmntfromname = event.mntfromname;
|
||||
[expectation fulfill];
|
||||
};
|
||||
|
||||
ESResponse *got = [self triggerTestMountEvent:deviceManager
|
||||
mockES:mockES
|
||||
mockDA:mockDA
|
||||
eventType:ES_EVENT_TYPE_AUTH_REMOUNT
|
||||
diskInfoOverrides:nil];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);
|
||||
XCTAssertEqual(mockDA.wasRemounted, YES);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:10.0];
|
||||
|
||||
XCTAssertEqualObjects(gotRemountedArgs, deviceManager.remountArgs);
|
||||
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
|
||||
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
|
||||
}
|
||||
|
||||
- (void)testEnsureDMGsDoNotPrompt {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
|
||||
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[mockDA reset];
|
||||
|
||||
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
|
||||
deviceManager.blockUSBMount = YES;
|
||||
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
XCTFail(@"Should not be called");
|
||||
};
|
||||
|
||||
NSDictionary *diskInfo = @{
|
||||
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey: @"Virtual Interface",
|
||||
(__bridge NSString *)kDADiskDescriptionDeviceModelKey: @"Disk Image",
|
||||
(__bridge NSString *)kDADiskDescriptionMediaNameKey: @"disk image",
|
||||
};
|
||||
|
||||
|
||||
ESResponse *got = [self triggerTestMountEvent:deviceManager
|
||||
mockES:mockES
|
||||
mockDA:mockDA
|
||||
eventType:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:diskInfo];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW);
|
||||
XCTAssertEqual(mockDA.wasRemounted, NO);
|
||||
}
|
||||
@end
|
||||
38
Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
#import "Source/santad/SNTExecutionController.h"
|
||||
|
||||
/// ES Client focused on subscribing to AUTH variants and authorizing the events
|
||||
/// based on configured policy.
|
||||
@interface SNTEndpointSecurityAuthorizer
|
||||
: SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)
|
||||
esApi
|
||||
execController:(SNTExecutionController *)execController
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:
|
||||
(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache;
|
||||
|
||||
@end
|
||||
145
Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.mm
Normal file
@@ -0,0 +1,145 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#include <os/base.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
@interface SNTEndpointSecurityAuthorizer ()
|
||||
@property SNTCompilerController *compilerController;
|
||||
@property SNTExecutionController *execController;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityAuthorizer {
|
||||
std::shared_ptr<AuthResultCache> _authResultCache;
|
||||
}
|
||||
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
execController:(SNTExecutionController *)execController
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache {
|
||||
self = [super initWithESAPI:std::move(esApi)];
|
||||
if (self) {
|
||||
_execController = execController;
|
||||
_compilerController = compilerController;
|
||||
_authResultCache = authResultCache;
|
||||
|
||||
[self establishClientOrDie];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)processMessage:(const Message &)msg {
|
||||
const es_file_t *targetFile = msg->event.exec.target->executable;
|
||||
|
||||
while (true) {
|
||||
santa_action_t returnAction = self->_authResultCache->CheckCache(targetFile);
|
||||
if (RESPONSE_VALID(returnAction)) {
|
||||
es_auth_result_t authResult = ES_AUTH_RESULT_DENY;
|
||||
|
||||
switch (returnAction) {
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
[self.compilerController setProcess:msg->event.exec.target->audit_token isCompiler:true];
|
||||
OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_ALLOW: authResult = ES_AUTH_RESULT_ALLOW; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
[self respondToMessage:msg
|
||||
withAuthResult:authResult
|
||||
cacheable:(authResult == ES_AUTH_RESULT_ALLOW)];
|
||||
return;
|
||||
} else if (returnAction == ACTION_REQUEST_BINARY) {
|
||||
// TODO(mlw): Add a metric here to observe how ofthen this happens in practice.
|
||||
// TODO(mlw): Look into caching a `Deferred<value>` to better prevent
|
||||
// raciness of multiple threads checking the cache simultaneously.
|
||||
// Also mitigates need to poll.
|
||||
usleep(5000);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self->_authResultCache->AddToCache(targetFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
[self.execController validateExecEvent:msg
|
||||
postAction:^bool(santa_action_t action) {
|
||||
return [self postAction:action forMessage:msg];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg {
|
||||
if (unlikely(esMsg->event_type != ES_EVENT_TYPE_AUTH_EXEC)) {
|
||||
// This is a programming error
|
||||
LOGE(@"Atteempting to authorize a non-exec event");
|
||||
[NSException raise:@"Invalid event type"
|
||||
format:@"Authorizing unexpected event type: %d", esMsg->event_type];
|
||||
}
|
||||
|
||||
if (![self.execController synchronousShouldProcessExecEvent:esMsg]) {
|
||||
[self postAction:ACTION_RESPOND_DENY forMessage:esMsg];
|
||||
return;
|
||||
}
|
||||
|
||||
[self processMessage:std::move(esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
[self processMessage:msg];
|
||||
}];
|
||||
}
|
||||
|
||||
- (bool)postAction:(santa_action_t)action forMessage:(const Message &)esMsg {
|
||||
es_auth_result_t authResult;
|
||||
|
||||
switch (action) {
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
[self.compilerController setProcess:esMsg->event.exec.target->audit_token isCompiler:true];
|
||||
OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_ALLOW: authResult = ES_AUTH_RESULT_ALLOW; break;
|
||||
case ACTION_RESPOND_DENY: authResult = ES_AUTH_RESULT_DENY; break;
|
||||
default:
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"Invalid action for postAction, exiting.");
|
||||
[NSException raise:@"Invalid post action" format:@"Invalid post action: %d", action];
|
||||
}
|
||||
|
||||
self->_authResultCache->AddToCache(esMsg->event.exec.target->executable, action);
|
||||
|
||||
// Don't let the ES framework cache DENY results. Santa only flushes ES cache
|
||||
// when a new DENY rule is received. If DENY results were cached and a rule
|
||||
// update made the executable allowable, ES would continue to apply the DENY
|
||||
// cached result. Note however that the local AuthResultCache will cache
|
||||
// DENY results.
|
||||
return [self respondToMessage:esMsg
|
||||
withAuthResult:authResult
|
||||
cacheable:(authResult == ES_AUTH_RESULT_ALLOW)];
|
||||
}
|
||||
|
||||
- (void)enable {
|
||||
[super subscribeAndClearCache:{
|
||||
ES_EVENT_TYPE_AUTH_EXEC,
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,273 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
#import "Source/santad/SNTExecutionController.h"
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
class MockAuthResultCache : public AuthResultCache {
|
||||
public:
|
||||
using AuthResultCache::AuthResultCache;
|
||||
|
||||
MOCK_METHOD(bool, AddToCache, (const es_file_t *es_file, santa_action_t decision));
|
||||
MOCK_METHOD(santa_action_t, CheckCache, (const es_file_t *es_file));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityAuthorizer (Testing)
|
||||
- (void)processMessage:(const Message &)msg;
|
||||
- (bool)postAction:(santa_action_t)action forMessage:(const Message &)esMsg;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityAuthorizerTest : XCTestCase
|
||||
@property id mockExecController;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityAuthorizerTest
|
||||
|
||||
- (void)setUp {
|
||||
self.mockExecController = OCMStrictClassMock([SNTExecutionController class]);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.mockExecController stopMocking];
|
||||
}
|
||||
|
||||
- (void)testEnable {
|
||||
// Ensure the client subscribes to expected event types
|
||||
std::set<es_event_type_t> expectedEventSubs{ES_EVENT_TYPE_AUTH_EXEC};
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
id authClient = [[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi];
|
||||
|
||||
EXPECT_CALL(*mockESApi, ClearCache)
|
||||
.After(EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs))
|
||||
.WillOnce(testing::Return(true)))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
[authClient enable];
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
execController:self.mockExecController
|
||||
compilerController:nil
|
||||
authResultCache:nullptr];
|
||||
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
// Test unhandled event type
|
||||
{
|
||||
// Temporarily change the event type
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
|
||||
XCTAssertThrows([authClient handleMessage:Message(mockESApi, &esMsg)]);
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_EXEC;
|
||||
}
|
||||
|
||||
// Test SNTExecutionController determines the event shouldn't be processed
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(NO);
|
||||
|
||||
OCMExpect([mockAuthClient postAction:ACTION_RESPOND_DENY forMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient postAction:ACTION_RESPOND_DENY forMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
|
||||
[mockAuthClient handleMessage:std::move(msg)];
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
}
|
||||
|
||||
// Test SNTExecutionController determines the event should be processed and
|
||||
// processMessage:handler: is called.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([self.mockExecController synchronousShouldProcessExecEvent:msg])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(YES);
|
||||
|
||||
OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg) handler:[OCMArg any]])
|
||||
.ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg) handler:[OCMArg any]])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
|
||||
[mockAuthClient handleMessage:std::move(msg)];
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
[mockAuthClient stopMocking];
|
||||
}
|
||||
|
||||
- (void)testProcessMessageWaitThenAllow {
|
||||
// This test ensures that if there is an outstanding action for
|
||||
// an item, it will check the cache again until a result exists.
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_file_t execFile = MakeESFile("bar");
|
||||
es_process_t execProc = MakeESProcess(&execFile, MakeAuditToken(12, 23), MakeAuditToken(34, 45));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth);
|
||||
esMsg.event.exec.target = &execProc;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
EXPECT_CALL(*mockAuthCache, CheckCache)
|
||||
.WillOnce(testing::Return(ACTION_REQUEST_BINARY))
|
||||
.WillOnce(testing::Return(ACTION_REQUEST_BINARY))
|
||||
.WillOnce(testing::Return(ACTION_RESPOND_ALLOW_COMPILER))
|
||||
.WillOnce(testing::Return(ACTION_UNSET));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(testing::_, ACTION_REQUEST_BINARY))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
id mockCompilerController = OCMStrictClassMock([SNTCompilerController class]);
|
||||
OCMExpect([mockCompilerController setProcess:execProc.audit_token isCompiler:true]);
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
execController:self.mockExecController
|
||||
compilerController:mockCompilerController
|
||||
authResultCache:mockAuthCache];
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
// This block tests that processing is held up until an outstanding thread
|
||||
// processing another event completes and returns a result. This test
|
||||
// specifically will check the `ACTION_RESPOND_ALLOW_COMPILER` flow.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
OCMExpect([mockAuthClient respondToMessage:msg
|
||||
withAuthResult:ES_AUTH_RESULT_ALLOW
|
||||
cacheable:true]);
|
||||
|
||||
[mockAuthClient processMessage:msg];
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
XCTAssertTrue(OCMVerifyAll(mockCompilerController));
|
||||
}
|
||||
|
||||
// This block tests uncached events storing appropriate cache marker and then
|
||||
// running the exec controller to validate the exec event.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
OCMExpect([self.mockExecController validateExecEvent:msg postAction:OCMOCK_ANY])
|
||||
.ignoringNonObjectArgs();
|
||||
|
||||
[mockAuthClient processMessage:msg];
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
XCTAssertTrue(OCMVerifyAll(mockCompilerController));
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
|
||||
|
||||
[mockCompilerController stopMocking];
|
||||
[mockAuthClient stopMocking];
|
||||
}
|
||||
|
||||
- (void)testPostAction {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_file_t execFile = MakeESFile("bar");
|
||||
es_process_t execProc = MakeESProcess(&execFile, MakeAuditToken(12, 23), MakeAuditToken(34, 45));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth);
|
||||
esMsg.event.exec.target = &execProc;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_ALLOW_COMPILER))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_ALLOW))
|
||||
.WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockAuthCache, AddToCache(&execFile, ACTION_RESPOND_DENY))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
id mockCompilerController = OCMStrictClassMock([SNTCompilerController class]);
|
||||
OCMExpect([mockCompilerController setProcess:execProc.audit_token isCompiler:true]);
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
execController:self.mockExecController
|
||||
compilerController:mockCompilerController
|
||||
authResultCache:mockAuthCache];
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
XCTAssertThrows([mockAuthClient postAction:(santa_action_t)123 forMessage:msg]);
|
||||
|
||||
std::map<santa_action_t, es_auth_result_t> actions = {
|
||||
{ACTION_RESPOND_ALLOW_COMPILER, ES_AUTH_RESULT_ALLOW},
|
||||
{ACTION_RESPOND_ALLOW, ES_AUTH_RESULT_ALLOW},
|
||||
{ACTION_RESPOND_DENY, ES_AUTH_RESULT_DENY},
|
||||
};
|
||||
|
||||
for (const auto &kv : actions) {
|
||||
OCMExpect([mockAuthClient respondToMessage:msg
|
||||
withAuthResult:kv.second
|
||||
cacheable:kv.second == ES_AUTH_RESULT_ALLOW]);
|
||||
|
||||
[mockAuthClient postAction:kv.first forMessage:msg];
|
||||
}
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
|
||||
|
||||
[mockCompilerController stopMocking];
|
||||
[mockAuthClient stopMocking];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,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
|
||||
243
Source/santad/EventProviders/SNTEndpointSecurityClient.mm
Normal file
@@ -0,0 +1,243 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <mach/mach_time.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/qos.h>
|
||||
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
@interface SNTEndpointSecurityClient ()
|
||||
@property int64_t deadlineMarginMS;
|
||||
@end
|
||||
;
|
||||
|
||||
@implementation SNTEndpointSecurityClient {
|
||||
std::shared_ptr<EndpointSecurityAPI> _esApi;
|
||||
Client _esClient;
|
||||
mach_timebase_info_data_t _timebase;
|
||||
dispatch_queue_t _authQueue;
|
||||
dispatch_queue_t _notifyQueue;
|
||||
}
|
||||
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_esApi = std::move(esApi);
|
||||
_deadlineMarginMS = 5000;
|
||||
|
||||
if (mach_timebase_info(&_timebase) != KERN_SUCCESS) {
|
||||
LOGE(@"Failed to get mach timebase info");
|
||||
// Assumed to be transitory failure. Let the daemon restart.
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
_authQueue = dispatch_queue_create(
|
||||
"com.google.santa.daemon.auth_queue",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
|
||||
_notifyQueue = dispatch_queue_create(
|
||||
"com.google.santa.daemon.notify_queue",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_BACKGROUND, 0));
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)errorMessageForNewClientResult:(es_new_client_result_t)result {
|
||||
switch (result) {
|
||||
case ES_NEW_CLIENT_RESULT_SUCCESS: return nil;
|
||||
case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED: return @"Full-disk access not granted";
|
||||
case ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED: return @"Not entitled";
|
||||
case ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED: return @"Not running as root";
|
||||
case ES_NEW_CLIENT_RESULT_ERR_INVALID_ARGUMENT: return @"Invalid argument";
|
||||
case ES_NEW_CLIENT_RESULT_ERR_INTERNAL: return @"Internal error";
|
||||
case ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS: return @"Too many simultaneous clients";
|
||||
default: return @"Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg {
|
||||
// This method should only be used by classes derived
|
||||
// from SNTEndpointSecurityClient.
|
||||
[self doesNotRecognizeSelector:_cmd];
|
||||
}
|
||||
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg
|
||||
ignoringOtherESClients:(BOOL)ignoringOtherESClients {
|
||||
if (esMsg->process->is_es_client && ignoringOtherESClients) {
|
||||
if (esMsg->action_type == ES_ACTION_TYPE_AUTH) {
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:true];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)establishClientOrDie {
|
||||
if (self->_esClient.IsConnected()) {
|
||||
// This is a programming error
|
||||
LOGE(@"Client already established. Aborting.");
|
||||
[NSException raise:@"Client already established" format:@"IsConnected already true"];
|
||||
}
|
||||
|
||||
self->_esClient = self->_esApi->NewClient(^(es_client_t *c, Message esMsg) {
|
||||
if ([self shouldHandleMessage:esMsg
|
||||
ignoringOtherESClients:[[SNTConfigurator configurator]
|
||||
ignoreOtherEndpointSecurityClients]]) {
|
||||
[self handleMessage:std::move(esMsg)];
|
||||
}
|
||||
});
|
||||
|
||||
if (!self->_esClient.IsConnected()) {
|
||||
NSString *errMsg = [self errorMessageForNewClientResult:_esClient.NewClientResult()];
|
||||
LOGE(@"Unable to create EndpointSecurity client: %@", errMsg);
|
||||
[NSException raise:@"Failed to create ES client" format:@"%@", errMsg];
|
||||
} else {
|
||||
LOGI(@"Connected to EndpointSecurity");
|
||||
}
|
||||
|
||||
if (![self muteSelf]) {
|
||||
[NSException raise:@"ES Mute Failure" format:@"Failed to mute self"];
|
||||
}
|
||||
}
|
||||
|
||||
+ (bool)populateAuditTokenSelf:(audit_token_t *)tok {
|
||||
mach_msg_type_number_t count = TASK_AUDIT_TOKEN_COUNT;
|
||||
if (task_info(mach_task_self(), TASK_AUDIT_TOKEN, (task_info_t)tok, &count) != KERN_SUCCESS) {
|
||||
LOGE(@"Failed to fetch this client's audit token.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
- (bool)muteSelf {
|
||||
audit_token_t myAuditToken;
|
||||
if (![SNTEndpointSecurityClient populateAuditTokenSelf:&myAuditToken]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!self->_esApi->MuteProcess(self->_esClient, &myAuditToken)) {
|
||||
LOGE(@"Failed to mute this client's process.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
- (bool)clearCache {
|
||||
return _esApi->ClearCache(self->_esClient);
|
||||
}
|
||||
|
||||
- (bool)subscribe:(const std::set<es_event_type_t> &)events {
|
||||
return _esApi->Subscribe(_esClient, events);
|
||||
}
|
||||
|
||||
- (bool)subscribeAndClearCache:(const std::set<es_event_type_t> &)events {
|
||||
return [self subscribe:events] && [self clearCache];
|
||||
}
|
||||
|
||||
- (bool)respondToMessage:(const Message &)msg
|
||||
withAuthResult:(es_auth_result_t)result
|
||||
cacheable:(bool)cacheable {
|
||||
return _esApi->RespondAuthResult(_esClient, msg, result, cacheable);
|
||||
}
|
||||
|
||||
- (void)processEnrichedMessage:(std::shared_ptr<EnrichedMessage>)msg
|
||||
handler:(void (^)(std::shared_ptr<EnrichedMessage>))messageHandler {
|
||||
dispatch_async(_notifyQueue, ^{
|
||||
messageHandler(std::move(msg));
|
||||
});
|
||||
}
|
||||
|
||||
- (void)processMessage:(Message &&)msg handler:(void (^)(const Message &))messageHandler {
|
||||
if (unlikely(msg->action_type != ES_ACTION_TYPE_AUTH)) {
|
||||
// This is a programming error
|
||||
LOGE(@"Attempting to process non-AUTH message");
|
||||
[NSException raise:@"Attempt to process non-auth message"
|
||||
format:@"Unexpected event type received: %d", msg->event_type];
|
||||
}
|
||||
|
||||
dispatch_semaphore_t processingSema = dispatch_semaphore_create(0);
|
||||
// Add 1 to the processing semaphore. We're not creating it with a starting
|
||||
// value of 1 because that requires that the semaphore is not deallocated
|
||||
// until its value matches the starting value, which we don't need.
|
||||
dispatch_semaphore_signal(processingSema);
|
||||
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
|
||||
|
||||
const uint64_t timeout = NSEC_PER_MSEC * (self.deadlineMarginMS);
|
||||
uint64_t deadlineMachTime = msg->deadline - mach_absolute_time();
|
||||
uint64_t deadlineNano = deadlineMachTime * _timebase.numer / _timebase.denom;
|
||||
|
||||
// TODO(mlw): How should we handle `deadlineNano <= timeout`. Will currently
|
||||
// result in the deadline block being dispatched immediately (and therefore
|
||||
// the event will be denied).
|
||||
|
||||
// Workaround for compiler bug that doesn't properly close over variables
|
||||
// Note: On macOS 10.15 this will cause extra message copies.
|
||||
__block Message processMsg = msg;
|
||||
__block Message deadlineMsg = msg;
|
||||
|
||||
dispatch_after(
|
||||
dispatch_time(DISPATCH_TIME_NOW, deadlineNano - timeout), self->_authQueue, ^(void) {
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Handler has already responded, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
bool res = [self respondToMessage:deadlineMsg
|
||||
withAuthResult:ES_AUTH_RESULT_DENY
|
||||
cacheable:false];
|
||||
|
||||
LOGE(@"SNTEndpointSecurityClient: deadline reached: deny pid=%d, event type: %d ret=%d",
|
||||
audit_token_to_pid(deadlineMsg->process->audit_token), deadlineMsg->event_type, res);
|
||||
dispatch_semaphore_signal(deadlineExpiredSema);
|
||||
});
|
||||
|
||||
dispatch_async(self->_authQueue, ^{
|
||||
messageHandler(deadlineMsg);
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Deadline expired, wait for deadline block to finish.
|
||||
dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+ (bool)isDatabasePath:(const std::string_view)path {
|
||||
// TODO(mlw): These values should come from `SNTDatabaseController`. But right
|
||||
// now they live as NSStrings. We should make them `std::string_view` types
|
||||
// in order to use them here efficiently, but will need to make the
|
||||
// `SNTDatabaseController` an ObjC++ file.
|
||||
return (path == "/private/var/db/santa/rules.db" || path == "/private/var/db/santa/events.db");
|
||||
}
|
||||
|
||||
@end
|
||||
73
Source/santad/EventProviders/SNTEndpointSecurityClientBase.h
Normal file
@@ -0,0 +1,73 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <bsm/libbsm.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
@protocol SNTEndpointSecurityClientBase
|
||||
|
||||
- (instancetype)initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi;
|
||||
|
||||
/// @note If this fails to establish a new ES client via `es_new_client`, an exception is raised
|
||||
/// that should terminate the program.
|
||||
- (void)establishClientOrDie;
|
||||
|
||||
- (bool)subscribe:(const std::set<es_event_type_t> &)events;
|
||||
|
||||
/// Clears the ES cache after setting subscriptions.
|
||||
/// There's a gap between creating a client and subscribing to events. Creating
|
||||
/// the client triggers a cache flush automatically but any events that happen
|
||||
/// prior to subscribing could've been cached by another client. Clearing after
|
||||
/// subscribing mitigates this posibility.
|
||||
- (bool)subscribeAndClearCache:(const std::set<es_event_type_t> &)events;
|
||||
|
||||
/// Responds to the Message with the given auth result
|
||||
///
|
||||
/// @param Message The wrapped es_message_t being responded to
|
||||
/// @param result Either ES_AUTH_RESULT_ALLOW or ES_AUTH_RESULT_DENY
|
||||
/// @param cacheable true if ES should attempt to cache the result, otherwise false
|
||||
/// @return true if the response was successful, otherwise false
|
||||
- (bool)respondToMessage:(const santa::santad::event_providers::endpoint_security::Message &)msg
|
||||
withAuthResult:(es_auth_result_t)result
|
||||
cacheable:(bool)cacheable;
|
||||
|
||||
- (void)
|
||||
processEnrichedMessage:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage>)msg
|
||||
handler:
|
||||
(void (^)(std::shared_ptr<
|
||||
santa::santad::event_providers::endpoint_security::EnrichedMessage>))
|
||||
messageHandler;
|
||||
|
||||
- (void)processMessage:(santa::santad::event_providers::endpoint_security::Message &&)msg
|
||||
handler:
|
||||
(void (^)(const santa::santad::event_providers::endpoint_security::Message &))
|
||||
messageHandler;
|
||||
|
||||
- (bool)clearCache;
|
||||
|
||||
+ (bool)isDatabasePath:(const std::string_view)path;
|
||||
+ (bool)populateAuditTokenSelf:(audit_token_t *)tok;
|
||||
|
||||
@end
|
||||
395
Source/santad/EventProviders/SNTEndpointSecurityClientTest.mm
Normal file
@@ -0,0 +1,395 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFile;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedProcess;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
@interface SNTEndpointSecurityClient (Testing)
|
||||
- (void)establishClientOrDie;
|
||||
- (bool)muteSelf;
|
||||
- (NSString *)errorMessageForNewClientResult:(es_new_client_result_t)result;
|
||||
- (void)handleMessage:(Message &&)esMsg;
|
||||
- (BOOL)shouldHandleMessage:(const Message &)esMsg
|
||||
ignoringOtherESClients:(BOOL)ignoringOtherESClients;
|
||||
|
||||
@property int64_t deadlineMarginMS;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityClientTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityClientTest
|
||||
|
||||
- (void)testEstablishClientOrDie {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
EXPECT_CALL(*mockESApi, MuteProcess).WillOnce(testing::Return(true));
|
||||
|
||||
EXPECT_CALL(*mockESApi, NewClient)
|
||||
.WillOnce(testing::Return(Client()))
|
||||
.WillOnce(testing::Return(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS)));
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
// First time throws because mock triggers failed connection
|
||||
// Second time succeeds
|
||||
XCTAssertThrows([client establishClientOrDie]);
|
||||
XCTAssertNoThrow([client establishClientOrDie]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testErrorMessageForNewClientResult {
|
||||
std::map<es_new_client_result_t, std::string> resultMessagePairs{
|
||||
{ES_NEW_CLIENT_RESULT_SUCCESS, ""},
|
||||
{ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED, "Full-disk access not granted"},
|
||||
{ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED, "Not entitled"},
|
||||
{ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED, "Not running as root"},
|
||||
{ES_NEW_CLIENT_RESULT_ERR_INVALID_ARGUMENT, "Invalid argument"},
|
||||
{ES_NEW_CLIENT_RESULT_ERR_INTERNAL, "Internal error"},
|
||||
{ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS, "Too many simultaneous clients"},
|
||||
{(es_new_client_result_t)123, "Unknown error"},
|
||||
};
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:nullptr];
|
||||
|
||||
for (const auto &kv : resultMessagePairs) {
|
||||
NSString *message = [client errorMessageForNewClientResult:kv.first];
|
||||
XCTAssertEqual(0, strcmp([(message ?: @"") UTF8String], kv.second.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
es_message_t esMsg;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
{ XCTAssertThrows([client handleMessage:Message(mockESApi, &esMsg)]); }
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testHandleMessageWithClient {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_FORK, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
// Have subscribe fail the first time, meaning clear cache only called once.
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_ALLOW, true))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
// Is ES client, but don't ignore others == Should Handle
|
||||
esMsg.process->is_es_client = true;
|
||||
XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:NO]);
|
||||
|
||||
// Not ES client, but ignore others == Should Handle
|
||||
esMsg.process->is_es_client = false;
|
||||
XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
|
||||
|
||||
// Is ES client, don't ignore others, and non-AUTH == Don't Handle
|
||||
esMsg.process->is_es_client = true;
|
||||
XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
|
||||
|
||||
// Is ES client, don't ignore others, and AUTH == Respond and Don't Handle
|
||||
esMsg.process->is_es_client = true;
|
||||
esMsg.action_type = ES_ACTION_TYPE_AUTH;
|
||||
XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testPopulateAuditTokenSelf {
|
||||
audit_token_t myAuditToken;
|
||||
|
||||
[SNTEndpointSecurityClient populateAuditTokenSelf:&myAuditToken];
|
||||
|
||||
XCTAssertEqual(audit_token_to_pid(myAuditToken), getpid());
|
||||
XCTAssertNotEqual(audit_token_to_pidversion(myAuditToken), 0);
|
||||
}
|
||||
|
||||
- (void)testMuteSelf {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
EXPECT_CALL(*mockESApi, MuteProcess)
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(false));
|
||||
|
||||
XCTAssertTrue([client muteSelf]);
|
||||
XCTAssertFalse([client muteSelf]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testClearCache {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
// Test the underlying clear cache impl returning both true and false
|
||||
EXPECT_CALL(*mockESApi, ClearCache)
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(false));
|
||||
|
||||
XCTAssertTrue([client clearCache]);
|
||||
XCTAssertFalse([client clearCache]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testSubscribe {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
std::set<es_event_type_t> events = {
|
||||
ES_EVENT_TYPE_NOTIFY_CLOSE,
|
||||
ES_EVENT_TYPE_NOTIFY_EXIT,
|
||||
};
|
||||
|
||||
// Test the underlying subscribe impl returning both true and false
|
||||
EXPECT_CALL(*mockESApi, Subscribe(testing::_, events))
|
||||
.WillOnce(testing::Return(true))
|
||||
.WillOnce(testing::Return(false));
|
||||
|
||||
XCTAssertTrue([client subscribe:events]);
|
||||
XCTAssertFalse([client subscribe:events]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testSubscribeAndClearCache {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
// Have subscribe fail the first time, meaning clear cache only called once.
|
||||
EXPECT_CALL(*mockESApi, ClearCache)
|
||||
.After(EXPECT_CALL(*mockESApi, Subscribe)
|
||||
.WillOnce(testing::Return(false))
|
||||
.WillOnce(testing::Return(true)))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
XCTAssertFalse([client subscribeAndClearCache:{}]);
|
||||
XCTAssertTrue([client subscribeAndClearCache:{}]);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testRespondToMessageWithAuthResultCacheable {
|
||||
es_message_t esMsg;
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
es_auth_result_t result = ES_AUTH_RESULT_DENY;
|
||||
bool cacheable = true;
|
||||
|
||||
// Have subscribe fail the first time, meaning clear cache only called once.
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, result, cacheable))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertTrue([client respondToMessage:msg withAuthResult:result cacheable:cacheable]);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testProcessEnrichedMessageHandler {
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
// Note: In this test, `RetainMessage` isn't setup to return anything. This
|
||||
// means that the underlying `es_msg_` in the `Message` object is NULL, and
|
||||
// therefore no call to `ReleaseMessage` is ever made (hence no expectations).
|
||||
// Because we don't need to operate on the es_msg_, this simplifies the test.
|
||||
EXPECT_CALL(*mockESApi, RetainMessage);
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
es_message_t esMsg;
|
||||
auto enrichedMsg = std::make_shared<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &esMsg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
[client processEnrichedMessage:enrichedMsg
|
||||
handler:^(std::shared_ptr<EnrichedMessage> msg) {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Handler block not called within expected time window");
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testIsDatabasePath {
|
||||
XCTAssertTrue([SNTEndpointSecurityClient isDatabasePath:"/private/var/db/santa/rules.db"]);
|
||||
XCTAssertTrue([SNTEndpointSecurityClient isDatabasePath:"/private/var/db/santa/events.db"]);
|
||||
|
||||
XCTAssertFalse([SNTEndpointSecurityClient isDatabasePath:"/not/a/db/path"]);
|
||||
}
|
||||
|
||||
- (void)testProcessMessageHandlerBadEventType {
|
||||
es_file_t proc_file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&proc_file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
{
|
||||
XCTAssertThrows([client processMessage:Message(mockESApi, &esMsg)
|
||||
handler:^(const Message &msg){
|
||||
}]);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
// Note: This test triggers a leak warning on the mock object, however it is
|
||||
// benign. The dispatch block to handle deadline expiration in
|
||||
// `processMessage:handler:` will retain the mock object an extra time.
|
||||
// But since this test sets a long deadline in order to ensure the handler block
|
||||
// runs first, the deadline handler block will not have finished executing by
|
||||
// the time the test exits, making GMock think the object was leaked.
|
||||
- (void)testProcessMessageHandler {
|
||||
es_file_t proc_file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&proc_file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &proc, ActionType::Auth,
|
||||
45 * 1000); // Long deadline to not hit
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
|
||||
{
|
||||
XCTAssertNoThrow([client processMessage:Message(mockESApi, &esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}]);
|
||||
}
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
"Handler block not called within expected time window");
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testProcessMessageHandlerWithDeadlineTimeout {
|
||||
// Set a es_message_t deadline of 750ms
|
||||
// Set a deadline leeway in the `SNTEndpointSecurityClient` of 500ms
|
||||
// Mock `RespondAuthResult` which is called from the deadline handler
|
||||
// Signal the semaphore from the mock
|
||||
// Wait a few seconds for the semaphore (should take ~250ms)
|
||||
//
|
||||
// Two semaphotes are used:
|
||||
// 1. deadlineSema - used to wait in the handler block until the deadline
|
||||
// block has a chance to execute
|
||||
// 2. controlSema - used to block control flow in the test until the
|
||||
// deadlineSema is signaled (or a timeout waiting on deadlineSema)
|
||||
es_file_t proc_file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&proc_file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &proc, ActionType::Auth,
|
||||
750); // 750ms timeout
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
dispatch_semaphore_t deadlineSema = dispatch_semaphore_create(0);
|
||||
dispatch_semaphore_t controlSema = dispatch_semaphore_create(0);
|
||||
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_DENY, false))
|
||||
.WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
// Signal deadlineSema to let the handler block continue execution
|
||||
dispatch_semaphore_signal(deadlineSema);
|
||||
return true;
|
||||
}));
|
||||
|
||||
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
|
||||
client.deadlineMarginMS = 500;
|
||||
|
||||
{
|
||||
__block long result;
|
||||
XCTAssertNoThrow([client processMessage:Message(mockESApi, &esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
result = dispatch_semaphore_wait(
|
||||
deadlineSema,
|
||||
dispatch_time(DISPATCH_TIME_NOW, 4 * NSEC_PER_SEC));
|
||||
|
||||
// Once done waiting on deadlineSema, trigger controlSema to
|
||||
// continue test
|
||||
dispatch_semaphore_signal(controlSema);
|
||||
}]);
|
||||
|
||||
XCTAssertEqual(
|
||||
0, dispatch_semaphore_wait(controlSema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Control sema not signaled within expected time window");
|
||||
|
||||
XCTAssertEqual(result, 0);
|
||||
}
|
||||
|
||||
// Allow some time for the threads in `processMessage:handler:` to finish.
|
||||
// It isn't critical that they do, but if the dispatch blocks don't complete
|
||||
// we may get warnings from GMock about calls to ReleaseMessage after
|
||||
// verifying and clearing. Sleep a little bit here to reduce chances of
|
||||
// seeing the warning (but still possible)
|
||||
SleepMS(100);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2021-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -11,12 +11,16 @@
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <DiskArbitration/DiskArbitration.h>
|
||||
|
||||
#include <DiskArbitration/DiskArbitration.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -26,16 +30,18 @@ typedef void (^SNTDeviceBlockCallback)(SNTDeviceEvent *event);
|
||||
* Manages DiskArbitration and EndpointSecurity to monitor/block/remount USB
|
||||
* storage devices.
|
||||
*/
|
||||
@interface SNTDeviceManager : NSObject
|
||||
@interface SNTEndpointSecurityDeviceManager
|
||||
: SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
|
||||
|
||||
@property(nonatomic, readwrite) BOOL subscribed;
|
||||
@property(nonatomic, readwrite) BOOL blockUSBMount;
|
||||
@property(nonatomic, readwrite, nullable) NSArray<NSString *> *remountArgs;
|
||||
@property(nonatomic, nullable) SNTDeviceBlockCallback deviceBlockCallback;
|
||||
|
||||
- (instancetype)init;
|
||||
- (void)listen;
|
||||
- (BOOL)subscribed;
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
|
||||
authResultCache:(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache;
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2021-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -11,21 +11,39 @@
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import "Source/santad/EventProviders/SNTDeviceManager.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h"
|
||||
|
||||
#import <DiskArbitration/DiskArbitration.h>
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
#include <errno.h>
|
||||
#include <libproc.h>
|
||||
#include <sys/mount.h>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/santad/Logs/SNTEventLog.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager ()
|
||||
|
||||
- (void)logDiskAppeared:(NSDictionary *)props;
|
||||
- (void)logDiskDisappeared:(NSDictionary *)props;
|
||||
|
||||
@property DASessionRef diskArbSession;
|
||||
@property(nonatomic, readonly) dispatch_queue_t diskQueue;
|
||||
|
||||
@end
|
||||
|
||||
void diskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
|
||||
if (dissenter) {
|
||||
@@ -36,17 +54,18 @@ void diskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context
|
||||
IOReturn subSystemCode = err_get_sub(status);
|
||||
IOReturn errorCode = err_get_code(status);
|
||||
|
||||
LOGE(
|
||||
@"SNTDeviceManager: dissenter status codes: system: %d, subsystem: %d, err: %d; status: %s",
|
||||
systemCode, subSystemCode, errorCode, [statusString UTF8String]);
|
||||
LOGE(@"SNTEndpointSecurityDeviceManager: dissenter status codes: system: %d, subsystem: %d, "
|
||||
@"err: %d; status: %s",
|
||||
systemCode, subSystemCode, errorCode, [statusString UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
void diskAppearedCallback(DADiskRef disk, void *context) {
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
SNTEventLog *logger = [SNTEventLog logger];
|
||||
if (logger) [logger logDiskAppeared:props];
|
||||
SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context;
|
||||
|
||||
[dm logDiskAppeared:props];
|
||||
}
|
||||
|
||||
void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *context) {
|
||||
@@ -54,8 +73,9 @@ void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *conte
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
|
||||
if (props[@"DAVolumePath"]) {
|
||||
SNTEventLog *logger = [SNTEventLog logger];
|
||||
if (logger) [logger logDiskAppeared:props];
|
||||
SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context;
|
||||
|
||||
[dm logDiskAppeared:props];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +83,9 @@ void diskDisappearedCallback(DADiskRef disk, void *context) {
|
||||
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
|
||||
if (![props[@"DAVolumeMountable"] boolValue]) return;
|
||||
|
||||
SNTEventLog *logger = [SNTEventLog logger];
|
||||
if (logger) [logger logDiskDisappeared:props];
|
||||
SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context;
|
||||
|
||||
[dm logDiskDisappeared:props];
|
||||
}
|
||||
|
||||
NSArray<NSString *> *maskToMountArgs(long remountOpts) {
|
||||
@@ -101,126 +122,97 @@ long mountArgsToMask(NSArray<NSString *> *args) {
|
||||
else if ([arg isEqualToString:@"async"])
|
||||
flags |= MNT_ASYNC;
|
||||
else
|
||||
LOGE(@"SNTDeviceManager: unexpected mount arg: %@", arg);
|
||||
LOGE(@"SNTEndpointSecurityDeviceManager: unexpected mount arg: %@", arg);
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SNTDeviceManager ()
|
||||
@implementation SNTEndpointSecurityDeviceManager {
|
||||
std::shared_ptr<AuthResultCache> _authResultCache;
|
||||
std::shared_ptr<Logger> _logger;
|
||||
}
|
||||
|
||||
@property DASessionRef diskArbSession;
|
||||
@property(nonatomic, readonly) es_client_t *client;
|
||||
@property(nonatomic, readonly) dispatch_queue_t esAuthQueue;
|
||||
@property(nonatomic, readonly) dispatch_queue_t diskQueue;
|
||||
@end
|
||||
|
||||
@implementation SNTDeviceManager
|
||||
|
||||
- (instancetype)init API_AVAILABLE(macos(10.15)) {
|
||||
self = [super init];
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
logger:(std::shared_ptr<Logger>)logger
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache {
|
||||
self = [super initWithESAPI:std::move(esApi)];
|
||||
if (self) {
|
||||
_logger = logger;
|
||||
_authResultCache = authResultCache;
|
||||
_blockUSBMount = false;
|
||||
|
||||
_diskQueue = dispatch_queue_create("com.google.santad.disk_queue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_esAuthQueue =
|
||||
dispatch_queue_create("com.google.santa.daemon.es_device_auth", DISPATCH_QUEUE_CONCURRENT);
|
||||
_diskQueue = dispatch_queue_create("com.google.santa.daemon.disk_queue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_diskArbSession = DASessionCreate(NULL);
|
||||
DASessionSetDispatchQueue(_diskArbSession, _diskQueue);
|
||||
|
||||
if (@available(macos 10.15, *)) [self initES];
|
||||
[self establishClientOrDie];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)initES API_AVAILABLE(macos(10.15)) {
|
||||
while (!self.client) {
|
||||
es_client_t *client = NULL;
|
||||
es_new_client_result_t ret = es_new_client(&client, ^(es_client_t *c, const es_message_t *m) {
|
||||
// Set timeout to 5 seconds before the ES deadline.
|
||||
[self handleESMessageWithTimeout:m
|
||||
withClient:c
|
||||
timeout:dispatch_time(m->deadline, NSEC_PER_SEC * -5)];
|
||||
});
|
||||
- (void)logDiskAppeared:(NSDictionary *)props {
|
||||
self->_logger->LogDiskAppeared(props);
|
||||
}
|
||||
|
||||
switch (ret) {
|
||||
case ES_NEW_CLIENT_RESULT_SUCCESS:
|
||||
LOGI(@"Connected to EndpointSecurity");
|
||||
_client = client;
|
||||
return;
|
||||
case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED:
|
||||
LOGE(@"Unable to create EndpointSecurity client, not full-disk access permitted");
|
||||
LOGE(@"Sleeping for 30s before restarting.");
|
||||
sleep(30);
|
||||
exit(ret);
|
||||
default:
|
||||
LOGE(@"Unable to create es client: %d. Sleeping for a minute.", ret);
|
||||
sleep(60);
|
||||
continue;
|
||||
}
|
||||
- (void)logDiskDisappeared:(NSDictionary *)props {
|
||||
self->_logger->LogDiskDisappeared(props);
|
||||
}
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg {
|
||||
if (!self.blockUSBMount) {
|
||||
// TODO: We should also unsubscribe from events when this isn't set, but
|
||||
// this is generally a low-volume event type.
|
||||
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:false];
|
||||
return;
|
||||
}
|
||||
|
||||
if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_UNMOUNT) {
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
[self processMessage:std::move(esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
es_auth_result_t result = [self handleAuthMount:msg];
|
||||
[self respondToMessage:msg withAuthResult:result cacheable:false];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)listenES API_AVAILABLE(macos(10.15)) {
|
||||
while (!self.client)
|
||||
usleep(100000); // 100ms
|
||||
|
||||
es_event_type_t events[] = {
|
||||
ES_EVENT_TYPE_AUTH_MOUNT,
|
||||
ES_EVENT_TYPE_AUTH_REMOUNT,
|
||||
};
|
||||
|
||||
es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t));
|
||||
if (sret != ES_RETURN_SUCCESS)
|
||||
LOGE(@"SNTDeviceManager: unable to subscribe to auth mount events: %d", sret);
|
||||
}
|
||||
|
||||
- (void)listenDA {
|
||||
- (void)enable {
|
||||
DARegisterDiskAppearedCallback(_diskArbSession, NULL, diskAppearedCallback,
|
||||
(__bridge void *)self);
|
||||
DARegisterDiskDescriptionChangedCallback(_diskArbSession, NULL, NULL,
|
||||
diskDescriptionChangedCallback, (__bridge void *)self);
|
||||
DARegisterDiskDisappearedCallback(_diskArbSession, NULL, diskDisappearedCallback,
|
||||
(__bridge void *)self);
|
||||
|
||||
[super subscribeAndClearCache:{
|
||||
ES_EVENT_TYPE_AUTH_MOUNT,
|
||||
ES_EVENT_TYPE_AUTH_REMOUNT,
|
||||
ES_EVENT_TYPE_NOTIFY_UNMOUNT,
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)listen {
|
||||
[self listenDA];
|
||||
if (@available(macos 10.15, *)) [self listenES];
|
||||
self.subscribed = YES;
|
||||
}
|
||||
|
||||
- (void)handleAuthMount:(const es_message_t *)m
|
||||
withClient:(es_client_t *)c API_AVAILABLE(macos(10.15)) {
|
||||
if (!self.blockUSBMount) {
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
return;
|
||||
}
|
||||
|
||||
- (es_auth_result_t)handleAuthMount:(const Message &)m {
|
||||
struct statfs *eventStatFS;
|
||||
BOOL isRemount = NO;
|
||||
|
||||
switch (m->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_MOUNT: eventStatFS = m->event.mount.statfs; break;
|
||||
case ES_EVENT_TYPE_AUTH_REMOUNT:
|
||||
eventStatFS = m->event.remount.statfs;
|
||||
isRemount = YES;
|
||||
break;
|
||||
case ES_EVENT_TYPE_AUTH_REMOUNT: eventStatFS = m->event.remount.statfs; break;
|
||||
default:
|
||||
// This is a programming error
|
||||
LOGE(@"Unexpected Event Type passed to DeviceManager handleAuthMount: %d", m->event_type);
|
||||
// Fail closed.
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false);
|
||||
assert(0 && "SNTDeviceManager: unexpected event type");
|
||||
return;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
long mountMode = eventStatFS->f_flags;
|
||||
pid_t pid = audit_token_to_pid(m->process->audit_token);
|
||||
LOGD(@"SNTDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu",
|
||||
m->process->executable->path.data, pid, mountMode);
|
||||
LOGD(
|
||||
@"SNTEndpointSecurityDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu",
|
||||
m->process->executable->path.data, pid, mountMode);
|
||||
|
||||
DADiskRef disk = DADiskCreateFromBSDName(NULL, self.diskArbSession, eventStatFS->f_mntfromname);
|
||||
CFAutorelease(disk);
|
||||
@@ -232,12 +224,13 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue];
|
||||
NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey];
|
||||
BOOL isUSB = [protocol isEqualToString:@"USB"];
|
||||
BOOL isVirtual = [protocol isEqualToString: @"Virtual Interface"];
|
||||
BOOL isVirtual = [protocol isEqualToString:@"Virtual Interface"];
|
||||
|
||||
NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey];
|
||||
|
||||
// TODO: check kind and protocol for banned things (e.g. MTP).
|
||||
LOGD(@"SNTDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d isRemovable: %d "
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d "
|
||||
@"isRemovable: %d "
|
||||
@"isEjectable: %d",
|
||||
protocol, kind, isInternal, isRemovable, isEjectable);
|
||||
|
||||
@@ -245,8 +238,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
// also are okay with operations for devices that are non-removal as long as
|
||||
// they are NOT a USB device.
|
||||
if (isInternal || isVirtual || (!isRemovable && !isEjectable && !isUSB)) {
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
return;
|
||||
return ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
SNTDeviceEvent *event = [[SNTDeviceEvent alloc]
|
||||
@@ -259,17 +251,16 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
event.remountArgs = self.remountArgs;
|
||||
long remountOpts = mountArgsToMask(self.remountArgs);
|
||||
|
||||
LOGD(@"SNTDeviceManager: mountMode: %@", maskToMountArgs(mountMode));
|
||||
LOGD(@"SNTDeviceManager: remountOpts: %@", maskToMountArgs(remountOpts));
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: mountMode: %@", maskToMountArgs(mountMode));
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: remountOpts: %@", maskToMountArgs(remountOpts));
|
||||
|
||||
if ((mountMode & remountOpts) == remountOpts && !isRemount) {
|
||||
LOGD(@"SNTDeviceManager: Allowing as mount as flags match remountOpts");
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
return;
|
||||
if ((mountMode & remountOpts) == remountOpts && m->event_type != ES_EVENT_TYPE_AUTH_REMOUNT) {
|
||||
LOGD(@"SNTEndpointSecurityDeviceManager: Allowing as mount as flags match remountOpts");
|
||||
return ES_AUTH_RESULT_ALLOW;
|
||||
}
|
||||
|
||||
long newMode = mountMode | remountOpts;
|
||||
LOGI(@"SNTDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)",
|
||||
LOGI(@"SNTEndpointSecurityDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)",
|
||||
eventStatFS->f_mntfromname, eventStatFS->f_mntonname, mountMode, newMode);
|
||||
[self remount:disk mountMode:newMode];
|
||||
}
|
||||
@@ -278,7 +269,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
self.deviceBlockCallback(event);
|
||||
}
|
||||
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false);
|
||||
return ES_AUTH_RESULT_DENY;
|
||||
}
|
||||
|
||||
- (void)remount:(DADiskRef)disk mountMode:(long)remountMask {
|
||||
@@ -293,66 +284,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
free(argv);
|
||||
}
|
||||
|
||||
// handleESMessage handles an ES message synchronously. This will block all incoming ES events
|
||||
// until either we serve a response or we hit the auth deadline. Prefer [SNTDeviceManager
|
||||
// handleESMessageWithTimeout]
|
||||
// TODO(tnek): generalize this timeout handling logic so that EndpointSecurityManager can use it
|
||||
// too.
|
||||
- (void)handleESMessageWithTimeout:(const es_message_t *)m
|
||||
withClient:(es_client_t *)c
|
||||
timeout:(dispatch_time_t)timeout API_AVAILABLE(macos(10.15)) {
|
||||
// ES will kill our whole client if we don't meet the es_message auth deadline, so we try to
|
||||
// gracefully handle it with a deny-by-default in the worst-case before it can do that.
|
||||
// This isn't an issue for notify events, so we're in no rush for those.
|
||||
es_message_t *mc = es_copy_message(m);
|
||||
|
||||
dispatch_semaphore_t processingSema = dispatch_semaphore_create(0);
|
||||
// Add 1 to the processing semaphore. We're not creating it with a starting
|
||||
// value of 1 because that requires that the semaphore is not deallocated
|
||||
// until its value matches the starting value, which we don't need.
|
||||
dispatch_semaphore_signal(processingSema);
|
||||
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
|
||||
|
||||
if (mc->action_type == ES_ACTION_TYPE_AUTH) {
|
||||
dispatch_after(timeout, self.esAuthQueue, ^(void) {
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Handler already responded, nothing to do.
|
||||
return;
|
||||
}
|
||||
LOGE(@"SNTDeviceManager: deadline reached: deny pid=%d ret=%d",
|
||||
audit_token_to_pid(mc->process->audit_token),
|
||||
es_respond_auth_result(c, mc, ES_AUTH_RESULT_DENY, false));
|
||||
dispatch_semaphore_signal(deadlineExpiredSema);
|
||||
});
|
||||
}
|
||||
|
||||
dispatch_async(self.esAuthQueue, ^{
|
||||
[self handleESMessage:mc withClient:c];
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Deadline expired, wait for deadline block to finish.
|
||||
dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER);
|
||||
}
|
||||
es_free_message(mc);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)handleESMessage:(const es_message_t *)m
|
||||
withClient:(es_client_t *)c API_AVAILABLE(macos(10.15)) {
|
||||
switch (m->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_REMOUNT: {
|
||||
[[fallthrough]];
|
||||
}
|
||||
case ES_EVENT_TYPE_AUTH_MOUNT: {
|
||||
[self handleAuthMount:m withClient:c];
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
LOGE(@"SNTDeviceManager: unexpected event type: %d", m->event_type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||