mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41c918ee87 | ||
|
|
1adb6d2726 | ||
|
|
8c531a256b | ||
|
|
5829363733 | ||
|
|
379f283c62 | ||
|
|
2082345c02 | ||
|
|
dd8f81a60e | ||
|
|
8ccb0813f1 | ||
|
|
b24e7e42bf | ||
|
|
4821ebebd5 | ||
|
|
efeaa82618 | ||
|
|
3f3de02644 | ||
|
|
f6c9456ea7 | ||
|
|
2aaff051c8 | ||
|
|
2df7e91c87 | ||
|
|
37644acd01 | ||
|
|
899ca89e23 | ||
|
|
e7281f1c55 | ||
|
|
bf0ca24ae7 | ||
|
|
4fe8b7908f | ||
|
|
a8dd332402 | ||
|
|
6631b0a8e3 | ||
|
|
07e09db608 | ||
|
|
d041a48c97 | ||
|
|
1683e09cc8 | ||
|
|
d6c73e0c6c | ||
|
|
72969a3c92 | ||
|
|
d2dbed78dd | ||
|
|
8fa91e4ff0 | ||
|
|
551763146d | ||
|
|
7a7f0cd5a8 | ||
|
|
fcb49701b3 | ||
|
|
c9ef723fc5 | ||
|
|
dc6732ef04 | ||
|
|
a48900a4ae | ||
|
|
bb49118d94 | ||
|
|
456333d6d2 |
18
.allstar/binary_artifacts.yaml
Normal file
18
.allstar/binary_artifacts.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
# Ignore reason: These crafted binaries are used in tests
|
||||
ignorePaths:
|
||||
- Source/common/testdata/bad_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
- Source/common/testdata/32bitplist
|
||||
- Source/common/testdata/BundleExample.app/Contents/MacOS/BundleExample
|
||||
- Source/common/testdata/DirectoryBundle/Contents/MacOS/DirectoryBundle
|
||||
- Source/common/testdata/DirectoryBundle/Contents/Resources/BundleExample.app/Contents/MacOS/BundleExample
|
||||
- Source/santad/testdata/binaryrules/badbinary
|
||||
- Source/santad/testdata/binaryrules/goodbinary
|
||||
- Source/santad/testdata/binaryrules/badcert
|
||||
- Source/santad/testdata/binaryrules/banned_teamid_allowed_binary
|
||||
- Source/santad/testdata/binaryrules/banned_teamid
|
||||
- Source/santad/testdata/binaryrules/goodcert
|
||||
- Source/santad/testdata/binaryrules/noop
|
||||
- Source/santad/testdata/binaryrules/rules.db
|
||||
14
.bazelrc
14
.bazelrc
@@ -3,3 +3,17 @@ 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 --copt=-DSANTA_OPEN_SOURCE=1
|
||||
build --cxxopt=-DSANTA_OPEN_SOURCE=1
|
||||
|
||||
build:asan --strip=never
|
||||
build:asan --copt="-Wno-macro-redefined"
|
||||
build:asan --copt="-D_FORTIFY_SOURCE=0"
|
||||
build:asan --copt="-O1"
|
||||
build:asan --copt="-fno-omit-frame-pointer"
|
||||
build:asan --copt="-fsanitize=address"
|
||||
build:asan --copt="-DADDRESS_SANITIZER"
|
||||
build:asan --linkopt="-fsanitize=address"
|
||||
|
||||
@@ -1 +1 @@
|
||||
5.0.0
|
||||
5.3.0
|
||||
|
||||
2
.github/workflows/check-markdown.yml
vendored
2
.github/workflows/check-markdown.yml
vendored
@@ -11,4 +11,4 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
- run: "! git grep -EIn $'[ \t]+$'"
|
||||
- run: "! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'"
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -20,7 +20,6 @@ jobs:
|
||||
- name: Run linters
|
||||
run: ./Testing/lint.sh
|
||||
|
||||
|
||||
build_userspace:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -55,10 +54,3 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path-to-lcov: ./bazel-out/_coverage/_coverage_report.dat
|
||||
flag-name: Unit
|
||||
|
||||
benchmark:
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run All Tests
|
||||
run: ./Testing/benchmark.sh
|
||||
|
||||
7
BUILD
7
BUILD
@@ -198,10 +198,3 @@ test_suite(
|
||||
"//Source/santasyncservice:unit_tests",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "benchmarks",
|
||||
tests = [
|
||||
"//Source/santad:SNTApplicationBenchmark",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Santa [](https://github.com/google/santa/actions/workflows/ci.yml) [](https://coveralls.io/github/google/santa?branch=main)
|
||||
# Santa [](https://github.com/google/santa/actions/workflows/ci.yml)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/santa/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/gui/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
</p>
|
||||
|
||||
Santa is a binary authorization system for macOS. It consists of a system
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
load("@rules_proto_grpc//objc:defs.bzl", "objc_proto_library")
|
||||
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
|
||||
|
||||
package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
@@ -16,11 +16,19 @@ proto_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_proto_library(
|
||||
name = "santa_objc_proto",
|
||||
copts = ["-fno-objc-arc"],
|
||||
non_arc_srcs = ["Santa.pbobjc.m"],
|
||||
protos = [":santa_proto"],
|
||||
cc_proto_library(
|
||||
name = "santa_cc_proto",
|
||||
deps = [":santa_proto"],
|
||||
)
|
||||
|
||||
# Note: Simple wrapper for a `cc_proto_library` target which cannot be directly
|
||||
# depended upon by an `objc_library` target.
|
||||
cc_library(
|
||||
name = "santa_cc_proto_library_wrapper",
|
||||
hdrs = ["santa_proto_include_wrapper.h"],
|
||||
deps = [
|
||||
":santa_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
@@ -83,15 +91,9 @@ objc_library(
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTAllowlistInfo",
|
||||
srcs = ["SNTAllowlistInfo.m"],
|
||||
hdrs = ["SNTAllowlistInfo.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTCommonEnums",
|
||||
hdrs = ["SNTCommonEnums.h"],
|
||||
textual_hdrs = ["SNTCommonEnums.h"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
@@ -106,6 +108,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 +136,7 @@ objc_library(
|
||||
srcs = ["SNTFileInfo.m"],
|
||||
hdrs = ["SNTFileInfo.h"],
|
||||
deps = [
|
||||
":SNTLogging",
|
||||
"@FMDB",
|
||||
"@MOLCodesignChecker",
|
||||
],
|
||||
@@ -298,13 +318,40 @@ santa_unit_test(
|
||||
deps = [":SNTMetricSet"],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTCachedDecisionTest",
|
||||
srcs = ["SNTCachedDecisionTest.mm"],
|
||||
deps = [
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:TestUtils",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":SNTCachedDecisionTest",
|
||||
":SNTFileInfoTest",
|
||||
":SNTKVOManagerTest",
|
||||
":SNTMetricSetTest",
|
||||
":SNTPrefixTreeTest",
|
||||
":SNTRuleTest",
|
||||
":SantaCacheTest",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "TestUtils",
|
||||
testonly = 1,
|
||||
srcs = ["TestUtils.mm"],
|
||||
hdrs = ["TestUtils.h"],
|
||||
sdk_dylibs = [
|
||||
"bsm",
|
||||
],
|
||||
deps = [
|
||||
"@OCMock",
|
||||
"@com_google_googletest//:gtest",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTAllowlistInfo.h"
|
||||
|
||||
@implementation SNTAllowlistInfo
|
||||
|
||||
- (instancetype)initWithPid:(pid_t)pid
|
||||
pidversion:(int)pidver
|
||||
targetPath:(NSString *)targetPath
|
||||
sha256:(NSString *)hash {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_pid = pid;
|
||||
_pidversion = pidver;
|
||||
_targetPath = targetPath;
|
||||
_sha256 = hash;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,10 +12,11 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
|
||||
@class MOLCertificate;
|
||||
|
||||
@@ -24,6 +25,8 @@
|
||||
///
|
||||
@interface SNTCachedDecision : NSObject
|
||||
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile;
|
||||
|
||||
@property santa_vnode_id_t vnodeId;
|
||||
@property SNTEventState decision;
|
||||
@property NSString *decisionExtra;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -15,4 +15,14 @@
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
|
||||
@implementation SNTCachedDecision
|
||||
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_vnodeId.fsid = (uint64_t)esFile->stat.st_dev;
|
||||
_vnodeId.fileid = esFile->stat.st_ino;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
36
Source/common/SNTCachedDecisionTest.mm
Normal file
36
Source/common/SNTCachedDecisionTest.mm
Normal file
@@ -0,0 +1,36 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
|
||||
@interface SNTCachedDecisionTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTCachedDecisionTest
|
||||
|
||||
- (void)testSNTCachedDecisionInit {
|
||||
// Ensure the vnodeId field is properly set from the es_file_t
|
||||
struct stat sb = MakeStat();
|
||||
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>
|
||||
///
|
||||
@@ -182,10 +183,10 @@
|
||||
/// SNTEventLogTypeSyslog "syslog": Sent to ASL or ULS (if built with the 10.12 SDK or later).
|
||||
/// SNTEventLogTypeFilelog "file": Sent to a file on disk. Use eventLogPath to specify a path.
|
||||
/// SNTEventLogTypeNull "null": Logs nothing
|
||||
/// SNTEventLogTypeProtobuf "protobuf": (BETA) Sent to a file on disk, using maildir format. Use
|
||||
/// mailDirectory to specify a path. Use mailDirectoryFileSizeThresholdKB,
|
||||
/// mailDirectorySizeThresholdMB and mailDirectoryEventMaxFlushTimeSec to configure
|
||||
/// additional maildir format settings.
|
||||
/// SNTEventLogTypeProtobuf "protobuf": (BETA) Sent to a file on disk, using a maildir-like
|
||||
/// format. Use spoolDirectory to specify a path. Use spoolDirectoryFileSizeThresholdKB,
|
||||
/// spoolDirectorySizeThresholdMB and spoolDirectoryEventMaxFlushTimeSec to configure
|
||||
/// additional settings.
|
||||
/// Defaults to SNTEventLogTypeFilelog.
|
||||
/// For mobileconfigs use EventLogType as the key and syslog or filelog strings as the value.
|
||||
///
|
||||
@@ -202,40 +203,40 @@
|
||||
@property(readonly, nonatomic) NSString *eventLogPath;
|
||||
|
||||
///
|
||||
/// If eventLogType is set to protobuf, mailDirectory will provide the base path used for
|
||||
/// saving logs using the maildir format.
|
||||
/// Defaults to /var/db/santa/mail.
|
||||
/// If eventLogType is set to protobuf, spoolDirectory will provide the base path used for
|
||||
/// saving logs using a maildir-like format.
|
||||
/// Defaults to /var/db/santa/spool.
|
||||
///
|
||||
/// @note: This property is KVO compliant, but should only be read once at santad startup.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *mailDirectory;
|
||||
@property(readonly, nonatomic) NSString *spoolDirectory;
|
||||
|
||||
///
|
||||
/// If eventLogType is set to protobuf, mailDirectoryFileSizeThresholdKB sets the per-file size
|
||||
/// limit for files saved in the mailDirectory.
|
||||
/// If eventLogType is set to protobuf, spoolDirectoryFileSizeThresholdKB sets the per-file size
|
||||
/// limit for files saved in the spoolDirectory.
|
||||
/// Defaults to 250.
|
||||
///
|
||||
/// @note: This property is KVO compliant, but should only be read once at santad startup.
|
||||
///
|
||||
@property(readonly, nonatomic) NSUInteger spoolDirectoryFileSizeThresholdKB;
|
||||
|
||||
///
|
||||
/// If eventLogType is set to protobuf, spoolDirectorySizeThresholdMB sets the total size
|
||||
/// limit for all files saved in the spoolDirectory.
|
||||
/// Defaults to 100.
|
||||
///
|
||||
/// @note: This property is KVO compliant, but should only be read once at santad startup.
|
||||
///
|
||||
@property(readonly, nonatomic) NSUInteger mailDirectoryFileSizeThresholdKB;
|
||||
@property(readonly, nonatomic) NSUInteger spoolDirectorySizeThresholdMB;
|
||||
|
||||
///
|
||||
/// If eventLogType is set to protobuf, mailDirectorySizeThresholdMB sets the total size
|
||||
/// limit for all files saved in the mailDirectory.
|
||||
/// Defaults to 500.
|
||||
///
|
||||
/// @note: This property is KVO compliant, but should only be read once at santad startup.
|
||||
///
|
||||
@property(readonly, nonatomic) NSUInteger mailDirectorySizeThresholdMB;
|
||||
|
||||
///
|
||||
/// If eventLogType is set to protobuf, mailDirectoryEventMaxFlushTimeSec sets the maximum amount
|
||||
/// If eventLogType is set to protobuf, spoolDirectoryEventMaxFlushTimeSec sets the maximum amount
|
||||
/// of time an event will be stored in memory before being written to disk.
|
||||
/// Defaults to 5.0.
|
||||
/// Defaults to 15.0.
|
||||
///
|
||||
/// @note: This property is KVO compliant, but should only be read once at santad startup.
|
||||
///
|
||||
@property(readonly, nonatomic) float mailDirectoryEventMaxFlushTimeSec;
|
||||
@property(readonly, nonatomic) float spoolDirectoryEventMaxFlushTimeSec;
|
||||
|
||||
///
|
||||
/// Enabling this appends the Santa machine ID to the end of each log line. If nothing
|
||||
@@ -244,15 +245,6 @@
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableMachineIDDecoration;
|
||||
|
||||
///
|
||||
/// Use an internal cache for decisions instead of relying on the caching
|
||||
/// mechanism built-in to the EndpointSecurity framework. This may increase
|
||||
/// performance, particularly when Santa is run alongside other system
|
||||
/// extensions.
|
||||
/// Has no effect if the system extension is not being used. Defaults to NO.
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableSysxCache;
|
||||
|
||||
#pragma mark - GUI Settings
|
||||
|
||||
///
|
||||
@@ -387,12 +379,6 @@
|
||||
///
|
||||
@property(nonatomic) NSArray<NSString *> *remountUSBMode;
|
||||
|
||||
///
|
||||
/// When `blockUSBMount` is set, this is the message shown to the user when a device is blocked
|
||||
/// If this message is not configured, a reasonable default is provided.
|
||||
///
|
||||
@property(readonly, nonatomic) NSString *usbBlockMessage;
|
||||
|
||||
///
|
||||
/// If set, this over-rides the default machine ID used for syncing.
|
||||
///
|
||||
|
||||
@@ -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
|
||||
@@ -87,15 +88,13 @@ static NSString *const kFileChangesPrefixFiltersKey = @"FileChangesPrefixFilters
|
||||
|
||||
static NSString *const kEventLogType = @"EventLogType";
|
||||
static NSString *const kEventLogPath = @"EventLogPath";
|
||||
static NSString *const kMailDirectory = @"MailDirectory";
|
||||
static NSString *const kMailDirectoryFileSizeThresholdKB = @"MailDirectoryFileSizeThresholdKB";
|
||||
static NSString *const kMailDirectorySizeThresholdMB = @"MailDirectorySizeThresholdMB";
|
||||
static NSString *const kMailDirectoryEventMaxFlushTimeSec = @"MailDirectoryEventMaxFlushTimeSec";
|
||||
static NSString *const kSpoolDirectory = @"SpoolDirectory";
|
||||
static NSString *const kSpoolDirectoryFileSizeThresholdKB = @"SpoolDirectoryFileSizeThresholdKB";
|
||||
static NSString *const kSpoolDirectorySizeThresholdMB = @"SpoolDirectorySizeThresholdMB";
|
||||
static NSString *const kSpoolDirectoryEventMaxFlushTimeSec = @"SpoolDirectoryEventMaxFlushTimeSec";
|
||||
|
||||
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";
|
||||
@@ -173,7 +172,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kRemountUSBModeKey : array,
|
||||
kEnablePageZeroProtectionKey : number,
|
||||
kEnableBadSignatureProtectionKey : number,
|
||||
kEnableSilentModeKey : string,
|
||||
kEnableSilentModeKey : number,
|
||||
kAboutTextKey : string,
|
||||
kMoreInfoURLKey : string,
|
||||
kEventDetailURLKey : string,
|
||||
@@ -201,12 +200,11 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kMachineIDPlistKeyKey : string,
|
||||
kEventLogType : string,
|
||||
kEventLogPath : string,
|
||||
kMailDirectory : string,
|
||||
kMailDirectoryFileSizeThresholdKB : number,
|
||||
kMailDirectorySizeThresholdMB : number,
|
||||
kMailDirectoryEventMaxFlushTimeSec : number,
|
||||
kSpoolDirectory : string,
|
||||
kSpoolDirectoryFileSizeThresholdKB : number,
|
||||
kSpoolDirectorySizeThresholdMB : number,
|
||||
kSpoolDirectoryEventMaxFlushTimeSec : number,
|
||||
kEnableMachineIDDecoration : number,
|
||||
kEnableSysxCache : number,
|
||||
kEnableForkAndExitLogging : number,
|
||||
kIgnoreOtherEndpointSecurityClients : number,
|
||||
kEnableDebugLogging : number,
|
||||
@@ -393,19 +391,19 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingMailDirectory {
|
||||
+ (NSSet *)keyPathsForValuesAffectingSpoolDirectory {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingMailDirectoryFileSizeThresholdKB {
|
||||
+ (NSSet *)keyPathsForValuesAffectingSpoolDirectoryFileSizeThresholdKB {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingMailDirectorySizeThresholdMB {
|
||||
+ (NSSet *)keyPathsForValuesAffectingSpoolDirectorySizeThresholdMB {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingMailDirectoryEventMaxFlushTimeSec {
|
||||
+ (NSSet *)keyPathsForValuesAffectingSpoolDirectoryEventMaxFlushTimeSec {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
@@ -425,10 +423,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self syncAndConfigStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableSysxCache {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableForkAndExitLogging {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -474,15 +468,15 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingRemountUSBMode {
|
||||
return [self configStateSet];
|
||||
return [self syncAndConfigStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingRemountUSBBlockMessage {
|
||||
return [self syncAndConfigStateSet];
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingUsbBlockMessage {
|
||||
return [self syncAndConfigStateSet];
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
#pragma mark Public Interface
|
||||
@@ -583,7 +577,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)remountUSBMode {
|
||||
NSArray<NSString *> *args = self.configState[kRemountUSBModeKey];
|
||||
NSArray<NSString *> *args = self.syncState[kRemountUSBModeKey];
|
||||
if (!args) {
|
||||
args = (NSArray<NSString *> *)self.configState[kRemountUSBModeKey];
|
||||
}
|
||||
for (id arg in args) {
|
||||
if (![arg isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
@@ -766,26 +763,26 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return self.configState[kEventLogPath] ?: @"/var/db/santa/santa.log";
|
||||
}
|
||||
|
||||
- (NSString *)mailDirectory {
|
||||
return self.configState[kMailDirectory] ?: @"/var/db/santa/mail";
|
||||
- (NSString *)spoolDirectory {
|
||||
return self.configState[kSpoolDirectory] ?: @"/var/db/santa/spool";
|
||||
}
|
||||
|
||||
- (NSUInteger)mailDirectoryFileSizeThresholdKB {
|
||||
return self.configState[kMailDirectoryFileSizeThresholdKB]
|
||||
? [self.configState[kMailDirectoryFileSizeThresholdKB] unsignedIntegerValue]
|
||||
- (NSUInteger)spoolDirectoryFileSizeThresholdKB {
|
||||
return self.configState[kSpoolDirectoryFileSizeThresholdKB]
|
||||
? [self.configState[kSpoolDirectoryFileSizeThresholdKB] unsignedIntegerValue]
|
||||
: 250;
|
||||
}
|
||||
|
||||
- (NSUInteger)spoolDirectorySizeThresholdMB {
|
||||
return self.configState[kSpoolDirectorySizeThresholdMB]
|
||||
? [self.configState[kSpoolDirectorySizeThresholdMB] unsignedIntegerValue]
|
||||
: 100;
|
||||
}
|
||||
|
||||
- (NSUInteger)mailDirectorySizeThresholdMB {
|
||||
return self.configState[kMailDirectorySizeThresholdMB]
|
||||
? [self.configState[kMailDirectorySizeThresholdMB] unsignedIntegerValue]
|
||||
: 500;
|
||||
}
|
||||
|
||||
- (float)mailDirMaxFlushTime {
|
||||
return self.configState[kMailDirectoryEventMaxFlushTimeSec]
|
||||
? [self.configState[kMailDirectoryEventMaxFlushTimeSec] floatValue]
|
||||
: 5.0;
|
||||
- (float)spoolDirectoryEventMaxFlushTimeSec {
|
||||
return self.configState[kSpoolDirectoryEventMaxFlushTimeSec]
|
||||
? [self.configState[kSpoolDirectoryEventMaxFlushTimeSec] floatValue]
|
||||
: 15.0;
|
||||
}
|
||||
|
||||
- (BOOL)enableMachineIDDecoration {
|
||||
@@ -793,11 +790,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;
|
||||
@@ -866,8 +858,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
}
|
||||
|
||||
- (BOOL)blockUSBMount {
|
||||
NSNumber *number = self.configState[kBlockUSBMountKey];
|
||||
return number ? [number boolValue] : NO;
|
||||
NSNumber *n = self.syncState[kBlockUSBMountKey];
|
||||
if (n) return [n boolValue];
|
||||
|
||||
return [self.configState[kBlockUSBMountKey] boolValue];
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,6 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class MOLCodesignChecker;
|
||||
@@ -32,6 +33,14 @@
|
||||
///
|
||||
- (instancetype)initWithPath:(NSString *)path error:(NSError **)error;
|
||||
|
||||
///
|
||||
/// Convenience initializer.
|
||||
///
|
||||
/// @param esFile Pointer to an es_file_t provided by the EndpointSecurity framework.
|
||||
/// Assumes that the path is a resolved path.
|
||||
///
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile error:(NSError **)error;
|
||||
|
||||
///
|
||||
/// Convenience initializer.
|
||||
///
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -25,6 +25,8 @@
|
||||
#include <sys/stat.h>
|
||||
#include <sys/xattr.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
|
||||
// Simple class to hold the data of a mach_header and the offset within the file
|
||||
// in which that header was found.
|
||||
@interface MachHeaderWithOffset : NSObject
|
||||
@@ -48,6 +50,7 @@
|
||||
@property NSFileHandle *fileHandle;
|
||||
@property NSUInteger fileSize;
|
||||
@property NSString *fileOwnerHomeDir;
|
||||
@property NSString *sha256Storage;
|
||||
|
||||
// Cached properties
|
||||
@property NSBundle *bundleRef;
|
||||
@@ -63,6 +66,26 @@
|
||||
extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
|
||||
- (instancetype)initWithResolvedPath:(NSString *)path error:(NSError **)error {
|
||||
struct stat fileStat;
|
||||
if (path.length) {
|
||||
lstat(path.UTF8String, &fileStat);
|
||||
}
|
||||
return [self initWithResolvedPath:path stat:&fileStat error:error];
|
||||
}
|
||||
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile error:(NSError **)error {
|
||||
return [self initWithResolvedPath:@(esFile->path.data) stat:&esFile->stat error:error];
|
||||
}
|
||||
|
||||
- (instancetype)initWithResolvedPath:(NSString *)path
|
||||
stat:(const struct stat *)fileStat
|
||||
error:(NSError **)error {
|
||||
if (!fileStat) {
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"NULL stat buffer unsupported");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_path = path;
|
||||
@@ -76,9 +99,7 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
return nil;
|
||||
}
|
||||
|
||||
struct stat fileStat;
|
||||
lstat(_path.UTF8String, &fileStat);
|
||||
if (!((S_IFMT & fileStat.st_mode) == S_IFREG)) {
|
||||
if (!((S_IFMT & fileStat->st_mode) == S_IFREG)) {
|
||||
if (error) {
|
||||
NSString *errStr = [NSString stringWithFormat:@"Non regular file: %s", strerror(errno)];
|
||||
*error = [NSError errorWithDomain:@"com.google.santa.fileinfo"
|
||||
@@ -88,12 +109,12 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
return nil;
|
||||
}
|
||||
|
||||
_fileSize = fileStat.st_size;
|
||||
_fileSize = fileStat->st_size;
|
||||
|
||||
if (_fileSize == 0) return nil;
|
||||
|
||||
if (fileStat.st_uid != 0) {
|
||||
struct passwd *pwd = getpwuid(fileStat.st_uid);
|
||||
if (fileStat->st_uid != 0) {
|
||||
struct passwd *pwd = getpwuid(fileStat->st_uid);
|
||||
if (pwd) {
|
||||
_fileOwnerHomeDir = @(pwd->pw_dir);
|
||||
}
|
||||
@@ -214,9 +235,13 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
}
|
||||
|
||||
- (NSString *)SHA256 {
|
||||
NSString *sha256;
|
||||
[self hashSHA1:NULL SHA256:&sha256];
|
||||
return sha256;
|
||||
// Memoize the value
|
||||
if (!self.sha256Storage) {
|
||||
NSString *sha256;
|
||||
[self hashSHA1:NULL SHA256:&sha256];
|
||||
self.sha256Storage = sha256;
|
||||
}
|
||||
return self.sha256Storage;
|
||||
}
|
||||
|
||||
#pragma mark File Type Info
|
||||
|
||||
34
Source/common/SNTKVOManager.h
Normal file
34
Source/common/SNTKVOManager.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// The callback type when KVO notifications are received for observed key paths.
|
||||
// The first parameter is the previous value, the second paramter is the new value.
|
||||
typedef void (^KVOCallback)(id oldValue, id newValue);
|
||||
|
||||
@interface SNTKVOManager : NSObject
|
||||
|
||||
// Add an observer for the selector on the given object. When a KVO notification
|
||||
// is received, the callback is called. If the notification contains objects that
|
||||
// are not of the expectedType, nil is passed as the argument to the callback.
|
||||
// The observer is removed when the returned instance is deallocated.
|
||||
- (instancetype)initWithObject:(id)object
|
||||
selector:(SEL)selector
|
||||
type:(Class)expectedType
|
||||
callback:(KVOCallback)callback;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
72
Source/common/SNTKVOManager.mm
Normal file
72
Source/common/SNTKVOManager.mm
Normal file
@@ -0,0 +1,72 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTKVOManager.h"
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
|
||||
@interface SNTKVOManager ()
|
||||
@property KVOCallback callback;
|
||||
@property Class expectedType;
|
||||
@property NSString *keyPath;
|
||||
@property id object;
|
||||
@end
|
||||
|
||||
@implementation SNTKVOManager
|
||||
|
||||
- (instancetype)initWithObject:(id)object
|
||||
selector:(SEL)selector
|
||||
type:(Class)expectedType
|
||||
callback:(KVOCallback)callback {
|
||||
self = [super self];
|
||||
if (self) {
|
||||
NSString *selectorName = NSStringFromSelector(selector);
|
||||
if (![object respondsToSelector:selector]) {
|
||||
LOGE(@"Attempt to add observer for an unknown selector (%@) for object (%@)", selectorName,
|
||||
[object class]);
|
||||
return nil;
|
||||
}
|
||||
|
||||
_object = object;
|
||||
_keyPath = selectorName;
|
||||
_expectedType = expectedType;
|
||||
_callback = callback;
|
||||
|
||||
[object addObserver:self
|
||||
forKeyPath:selectorName
|
||||
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
|
||||
context:NULL];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self.object removeObserver:self forKeyPath:self.keyPath context:NULL];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||
ofObject:(id)object
|
||||
change:(NSDictionary<NSString *, id> *)change
|
||||
context:(void *)context {
|
||||
id oldValue = [change[NSKeyValueChangeOldKey] isKindOfClass:self.expectedType]
|
||||
? change[NSKeyValueChangeOldKey]
|
||||
: nil;
|
||||
id newValue = [change[NSKeyValueChangeNewKey] isKindOfClass:self.expectedType]
|
||||
? change[NSKeyValueChangeNewKey]
|
||||
: nil;
|
||||
|
||||
self.callback(oldValue, newValue);
|
||||
}
|
||||
|
||||
@end
|
||||
129
Source/common/SNTKVOManagerTest.mm
Normal file
129
Source/common/SNTKVOManagerTest.mm
Normal file
@@ -0,0 +1,129 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTKVOManager.h"
|
||||
|
||||
@interface Foo : NSObject
|
||||
@property NSNumber *propNumber;
|
||||
@property NSArray *propArray;
|
||||
@property id propId;
|
||||
@end
|
||||
|
||||
@implementation Foo
|
||||
@end
|
||||
|
||||
@interface SNTKVOManagerTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTKVOManagerTest
|
||||
|
||||
- (void)testInvalidSelector {
|
||||
Foo *foo = [[Foo alloc] init];
|
||||
|
||||
SNTKVOManager *kvo = [[SNTKVOManager alloc] initWithObject:foo
|
||||
selector:NSSelectorFromString(@"doesNotExist")
|
||||
type:[NSNumber class]
|
||||
callback:^(id, id){
|
||||
}];
|
||||
|
||||
XCTAssertNil(kvo);
|
||||
}
|
||||
|
||||
- (void)testNormalOperation {
|
||||
Foo *foo = [[Foo alloc] init];
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
int origVal = 123;
|
||||
int update1 = 456;
|
||||
int update2 = 789;
|
||||
|
||||
foo.propNumber = @(origVal);
|
||||
|
||||
// Store the values from the callback to test against expected values
|
||||
__block int oldVal;
|
||||
__block int newVal;
|
||||
|
||||
SNTKVOManager *kvo =
|
||||
[[SNTKVOManager alloc] initWithObject:foo
|
||||
selector:@selector(propNumber)
|
||||
type:[NSNumber class]
|
||||
callback:^(NSNumber *oldValue, NSNumber *newValue) {
|
||||
oldVal = [oldValue intValue];
|
||||
newVal = [newValue intValue];
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
XCTAssertNotNil(kvo);
|
||||
|
||||
// Ensure an update to the observed property triggers the callback
|
||||
foo.propNumber = @(update1);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for first observable update");
|
||||
XCTAssertEqual(oldVal, origVal);
|
||||
XCTAssertEqual(newVal, update1);
|
||||
|
||||
// One more time why not
|
||||
foo.propNumber = @(update2);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for second observable update");
|
||||
XCTAssertEqual(oldVal, update1);
|
||||
XCTAssertEqual(newVal, update2);
|
||||
}
|
||||
|
||||
- (void)testUnexpectedTypes {
|
||||
Foo *foo = [[Foo alloc] init];
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
NSString *origVal = @"any_val";
|
||||
NSString *update = @"new_val";
|
||||
foo.propId = origVal;
|
||||
|
||||
__block id oldVal;
|
||||
__block id newVal;
|
||||
|
||||
SNTKVOManager *kvo = [[SNTKVOManager alloc] initWithObject:foo
|
||||
selector:@selector(propId)
|
||||
type:[NSString class]
|
||||
callback:^(id oldValue, id newValue) {
|
||||
oldVal = oldValue;
|
||||
newVal = newValue;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
XCTAssertNotNil(kvo);
|
||||
|
||||
// Update to an unexpected type (here, NSNumber instead of NSString)
|
||||
foo.propId = @(123);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for first observable update");
|
||||
XCTAssertEqualObjects(oldVal, origVal);
|
||||
XCTAssertNil(newVal);
|
||||
|
||||
// Update again with an expected type, ensure oldVal is now nil
|
||||
foo.propId = update;
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for first observable update");
|
||||
XCTAssertNil(oldVal);
|
||||
XCTAssertEqualObjects(newVal, update);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -280,15 +280,12 @@ NSString *SNTMetricMakeStringFromMetricType(SNTMetricType metricType) {
|
||||
if (_fieldNames.count == 0) {
|
||||
metricDict[@"fields"][@""] = @[ [self encodeMetricValueForFieldValues:@[]] ];
|
||||
} else {
|
||||
for (NSString *fieldName in _fieldNames) {
|
||||
NSMutableArray *fieldVals = [[NSMutableArray alloc] init];
|
||||
NSMutableArray *fieldVals = [[NSMutableArray alloc] init];
|
||||
|
||||
for (NSArray<NSString *> *fieldValues in _metricsForFieldValues) {
|
||||
[fieldVals addObject:[self encodeMetricValueForFieldValues:fieldValues]];
|
||||
}
|
||||
|
||||
metricDict[@"fields"][fieldName] = fieldVals;
|
||||
for (NSArray<NSString *> *fieldValues in _metricsForFieldValues) {
|
||||
[fieldVals addObject:[self encodeMetricValueForFieldValues:fieldValues]];
|
||||
}
|
||||
metricDict[@"fields"][[_fieldNames componentsJoinedByString:@","]] = fieldVals;
|
||||
}
|
||||
return metricDict;
|
||||
}
|
||||
|
||||
@@ -672,4 +672,35 @@
|
||||
output);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testEnsureMetricsWithMultipleFieldNamesSerializeOnce {
|
||||
SNTMetricSet *metricSet = [[SNTMetricSet alloc] initWithHostname:@"testHost"
|
||||
username:@"testUser"];
|
||||
|
||||
SNTMetricCounter *c =
|
||||
[metricSet counterWithName:@"/santa/events"
|
||||
fieldNames:@[ @"client", @"event_type" ]
|
||||
helpText:@"Count of events on the host for a given ES client"];
|
||||
[c incrementBy:1 forFieldValues:@[ @"device_manager", @"auth_mount" ]];
|
||||
|
||||
NSDictionary *expected = @{
|
||||
@"/santa/events" : @{
|
||||
@"description" : @"Count of events on the host for a given ES client",
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeCounter],
|
||||
@"fields" : @{
|
||||
@"client,event_type" : @[
|
||||
@{
|
||||
@"value" : @"device_manager,auth_mount",
|
||||
@"created" : [NSDate date],
|
||||
@"last_updated" : [NSDate date],
|
||||
@"data" : [NSNumber numberWithInt:1],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
NSDictionary *got = [metricSet export][@"metrics"];
|
||||
XCTAssertEqualObjects(expected, got, @"metrics do not match expected");
|
||||
}
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
75
Source/common/TestUtils.h
Normal file
75
Source/common/TestUtils.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__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 NOGROUP_GID ((unsigned int)-1)
|
||||
|
||||
// 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())
|
||||
|
||||
#define XCTAssertSemaTrue(s, sec, m) \
|
||||
XCTAssertEqual( \
|
||||
0, dispatch_semaphore_wait((s), dispatch_time(DISPATCH_TIME_NOW, (sec)*NSEC_PER_SEC)), m)
|
||||
|
||||
// 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);
|
||||
|
||||
/// Construct a `struct stat` buffer with each member having a unique value.
|
||||
/// @param offset An optional offset to be added to each member. useful when
|
||||
/// a test has multiple stats and you'd like for them each to have different
|
||||
/// values across the members.
|
||||
struct stat MakeStat(int offset = 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);
|
||||
|
||||
uint32_t MaxSupportedESMessageVersionForCurrentOS();
|
||||
|
||||
#endif
|
||||
145
Source/common/TestUtils.mm
Normal file
145
Source/common/TestUtils.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.
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <mach/mach_time.h>
|
||||
#include <time.h>
|
||||
#include <uuid/uuid.h>
|
||||
|
||||
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver) {
|
||||
return audit_token_t{
|
||||
.val =
|
||||
{
|
||||
0,
|
||||
NOBODY_UID,
|
||||
NOGROUP_GID,
|
||||
NOBODY_UID,
|
||||
NOGROUP_GID,
|
||||
(unsigned int)pid,
|
||||
0,
|
||||
(unsigned int)pidver,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
struct stat MakeStat(int offset) {
|
||||
return (struct stat){
|
||||
.st_dev = 1 + offset,
|
||||
.st_mode = (mode_t)(2 + offset),
|
||||
.st_nlink = (nlink_t)(3 + offset),
|
||||
.st_ino = (uint64_t)(4 + offset),
|
||||
.st_uid = NOBODY_UID,
|
||||
.st_gid = NOGROUP_GID,
|
||||
.st_rdev = 5 + offset,
|
||||
.st_atimespec = {.tv_sec = 100 + offset, .tv_nsec = 200 + offset},
|
||||
.st_mtimespec = {.tv_sec = 101 + offset, .tv_nsec = 21 + offset},
|
||||
.st_ctimespec = {.tv_sec = 102 + offset, .tv_nsec = 202 + offset},
|
||||
.st_birthtimespec = {.tv_sec = 103 + offset, .tv_nsec = 203 + offset},
|
||||
.st_size = 6 + offset,
|
||||
.st_blocks = 7 + offset,
|
||||
.st_blksize = 8 + offset,
|
||||
.st_flags = (uint32_t)(9 + offset),
|
||||
.st_gen = (uint32_t)(10 + offset),
|
||||
};
|
||||
}
|
||||
|
||||
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),
|
||||
.group_id = 111,
|
||||
.session_id = 222,
|
||||
.is_platform_binary = true,
|
||||
.is_es_client = true,
|
||||
.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;
|
||||
}
|
||||
|
||||
uint32_t MaxSupportedESMessageVersionForCurrentOS() {
|
||||
// Note: ES message v3 was only in betas.
|
||||
if (@available(macOS 13.0, *)) {
|
||||
return 6;
|
||||
} else if (@available(macOS 12.3, *)) {
|
||||
return 5;
|
||||
} else if (@available(macOS 11.0, *)) {
|
||||
return 4;
|
||||
} else if (@available(macOS 10.15.4, *)) {
|
||||
return 2;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
es_message_t MakeESMessage(es_event_type_t et, es_process_t *proc, ActionType action_type,
|
||||
uint64_t future_deadline_ms) {
|
||||
es_message_t es_msg = {
|
||||
.deadline = AddMillisToMachTime(future_deadline_ms, mach_absolute_time()),
|
||||
.process = proc,
|
||||
.action_type =
|
||||
(action_type == ActionType::Notify) ? ES_ACTION_TYPE_NOTIFY : ES_ACTION_TYPE_AUTH,
|
||||
.event_type = et,
|
||||
};
|
||||
|
||||
es_msg.version = MaxSupportedESMessageVersionForCurrentOS();
|
||||
|
||||
return es_msg;
|
||||
}
|
||||
|
||||
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";
|
||||
@@ -10,135 +11,489 @@ import "google/protobuf/timestamp.proto";
|
||||
|
||||
option objc_class_prefix = "SNTPB";
|
||||
|
||||
package santa;
|
||||
package santa.pb.v1;
|
||||
|
||||
message ProcessInfo {
|
||||
// User ID and associated username
|
||||
message UserInfo {
|
||||
optional int32 uid = 1;
|
||||
optional string name = 2;
|
||||
}
|
||||
|
||||
// Group ID and associated group name
|
||||
message GroupInfo {
|
||||
optional int32 gid = 1;
|
||||
optional string name = 2;
|
||||
}
|
||||
|
||||
// A process is uniquely identified on macOS by its pid and pidversion
|
||||
message ProcessID {
|
||||
optional int32 pid = 1;
|
||||
optional int32 pidversion = 2;
|
||||
optional int32 ppid = 3;
|
||||
optional int32 uid = 4;
|
||||
optional string user = 5;
|
||||
optional int32 gid = 6;
|
||||
optional string group = 7;
|
||||
}
|
||||
|
||||
message FileModification {
|
||||
enum Action {
|
||||
ACTION_UNKNOWN = 0;
|
||||
ACTION_DELETE = 1;
|
||||
ACTION_EXCHANGE = 2;
|
||||
ACTION_LINK = 3;
|
||||
ACTION_RENAME = 4;
|
||||
ACTION_WRITE = 5;
|
||||
// Code signature information
|
||||
message CodeSignature {
|
||||
// The code directory hash identifies a specific version of a program
|
||||
optional bytes cdhash = 1;
|
||||
|
||||
// The signing id of the code signature
|
||||
optional string signing_id = 2;
|
||||
|
||||
// The team id of the code signature
|
||||
optional string team_id = 3;
|
||||
}
|
||||
|
||||
// Stat information for a file
|
||||
// Mimics data from `stat(2)`
|
||||
message Stat {
|
||||
optional int32 dev = 1;
|
||||
optional uint32 mode = 2;
|
||||
optional uint32 nlink = 3;
|
||||
optional uint64 ino = 4;
|
||||
optional UserInfo user = 5;
|
||||
optional GroupInfo group = 6;
|
||||
optional int32 rdev = 7;
|
||||
optional google.protobuf.Timestamp access_time = 8;
|
||||
optional google.protobuf.Timestamp modification_time = 9;
|
||||
optional google.protobuf.Timestamp change_time = 10;
|
||||
optional google.protobuf.Timestamp birth_time = 11;
|
||||
optional int64 size = 12;
|
||||
optional int64 blocks = 13;
|
||||
optional int32 blksize = 14;
|
||||
optional uint32 flags = 15;
|
||||
optional int32 gen = 16;
|
||||
}
|
||||
|
||||
// Hash value and metadata describing hash algorithm used
|
||||
message Hash {
|
||||
enum HashAlgo {
|
||||
HASH_ALGO_UNKNOWN = 0;
|
||||
HASH_ALGO_SHA256 = 1;
|
||||
}
|
||||
|
||||
optional Action action = 1;
|
||||
optional string path = 2;
|
||||
optional string newpath = 3;
|
||||
optional string process = 4;
|
||||
optional string process_path = 5;
|
||||
optional ProcessInfo process_info = 6;
|
||||
optional string machine_id = 7;
|
||||
optional HashAlgo type = 1;
|
||||
optional string hash = 2;
|
||||
}
|
||||
|
||||
// File information
|
||||
message FileInfo {
|
||||
// File path
|
||||
optional string path = 1;
|
||||
|
||||
// Whether or not the path is truncated
|
||||
optional bool truncated = 2;
|
||||
|
||||
// Stat information
|
||||
optional Stat stat = 3;
|
||||
|
||||
// Hash of file contents
|
||||
optional Hash hash = 4;
|
||||
}
|
||||
|
||||
// Light variant of `FileInfo` message to help minimize on-disk/on-wire sizes
|
||||
message FileInfoLight {
|
||||
// File path
|
||||
optional string path = 1;
|
||||
|
||||
// Whether or not the path is truncated
|
||||
optional bool truncated = 2;
|
||||
}
|
||||
|
||||
// File descriptor information
|
||||
message FileDescriptor {
|
||||
// Enum types gathered from `<sys/proc_info.h>`
|
||||
enum FDType {
|
||||
FD_TYPE_UNKNOWN = 0;
|
||||
FD_TYPE_ATALK = 1;
|
||||
FD_TYPE_VNODE = 2;
|
||||
FD_TYPE_SOCKET = 3;
|
||||
FD_TYPE_PSHM = 4;
|
||||
FD_TYPE_PSEM = 5;
|
||||
FD_TYPE_KQUEUE = 6;
|
||||
FD_TYPE_PIPE = 7;
|
||||
FD_TYPE_FSEVENTS = 8;
|
||||
FD_TYPE_NETPOLICY = 9;
|
||||
FD_TYPE_CHANNEL = 10;
|
||||
FD_TYPE_NEXUS = 11;
|
||||
}
|
||||
|
||||
// File descriptor value
|
||||
optional int32 fd = 1;
|
||||
|
||||
// Type of file object
|
||||
optional FDType fd_type = 2;
|
||||
|
||||
// Unique id of the pipe for correlation with other file descriptors
|
||||
// pointing to the same or other end of the same pipe
|
||||
// Note: Only valid when `fd_type` is `FD_TYPE_PIPE`
|
||||
optional uint64 pipe_id = 3;
|
||||
}
|
||||
|
||||
// Process information
|
||||
message ProcessInfo {
|
||||
// Process ID of the process
|
||||
optional ProcessID id = 1;
|
||||
|
||||
// Process ID of the parent process
|
||||
optional ProcessID parent_id = 2;
|
||||
|
||||
// Process ID of the process responsible for this one
|
||||
optional ProcessID responsible_id = 3;
|
||||
|
||||
// Original parent ID, remains stable in the event a process is reparented
|
||||
optional int32 original_parent_pid = 4;
|
||||
|
||||
// Process group id the process belongs to
|
||||
optional int32 group_id = 5;
|
||||
|
||||
// Session id the process belongs to
|
||||
optional int32 session_id = 6;
|
||||
|
||||
// Effective user/group info
|
||||
optional UserInfo effective_user = 7;
|
||||
optional GroupInfo effective_group = 8;
|
||||
|
||||
// Real user/group info
|
||||
optional UserInfo real_user = 9;
|
||||
optional GroupInfo real_group = 10;
|
||||
|
||||
// Whether or not the process was signed with Apple certificates
|
||||
optional bool is_platform_binary = 11;
|
||||
|
||||
// Whether or not the process is an ES client
|
||||
optional bool is_es_client = 12;
|
||||
|
||||
// Code signature information for the process
|
||||
optional CodeSignature code_signature = 13;
|
||||
|
||||
// Codesigning flags for the process (from `<Kernel/kern/cs_blobs.h>`)
|
||||
optional uint32 cs_flags = 14;
|
||||
|
||||
// File information for the executable backing this process
|
||||
optional FileInfo executable = 15;
|
||||
|
||||
// File information for the associated TTY
|
||||
optional FileInfoLight tty = 16;
|
||||
|
||||
// Time the process was started
|
||||
optional google.protobuf.Timestamp start_time = 17;
|
||||
}
|
||||
|
||||
// Light variant of ProcessInfo message to help minimize on-disk/on-wire sizes
|
||||
message ProcessInfoLight {
|
||||
// Process ID of the process
|
||||
optional ProcessID id = 1;
|
||||
|
||||
// Process ID of the parent process
|
||||
optional ProcessID parent_id = 2;
|
||||
|
||||
// Original parent ID, remains stable in the event a process is reparented
|
||||
optional int32 original_parent_pid = 3;
|
||||
|
||||
// Process group id the process belongs to
|
||||
optional int32 group_id = 4;
|
||||
|
||||
// Session id the process belongs to
|
||||
optional int32 session_id = 5;
|
||||
|
||||
// Effective user/group info
|
||||
optional UserInfo effective_user = 6;
|
||||
optional GroupInfo effective_group = 7;
|
||||
|
||||
// Real user/group info
|
||||
optional UserInfo real_user = 8;
|
||||
optional GroupInfo real_group = 9;
|
||||
|
||||
// File information for the executable backing this process
|
||||
optional FileInfoLight executable = 10;
|
||||
}
|
||||
|
||||
// Certificate information
|
||||
message CertificateInfo {
|
||||
// Hash of the certificate data
|
||||
optional Hash hash = 1;
|
||||
|
||||
// Common name used in the certificate
|
||||
optional string common_name = 2;
|
||||
}
|
||||
|
||||
// Information about a process execution event
|
||||
message Execution {
|
||||
// The process that executed the new image (e.g. the process that called
|
||||
// `execve(2)` or `posix_spawn(2)``)
|
||||
optional ProcessInfoLight instigator = 1;
|
||||
|
||||
// Process info for the newly formed execution
|
||||
optional ProcessInfo target = 2;
|
||||
|
||||
// Script file information
|
||||
// Only valid when a script was executed directly and not as an argument to
|
||||
// an interpreter (e.g. `./foo.sh`, not `/bin/sh ./foo.sh`)
|
||||
optional FileInfo script = 3;
|
||||
|
||||
// The current working directory of the `target` at exec time
|
||||
optional FileInfo working_directory = 4;
|
||||
|
||||
// List of process arguments
|
||||
repeated string args = 5;
|
||||
|
||||
// List of environment variables
|
||||
repeated string envs = 6;
|
||||
|
||||
// List of file descriptors
|
||||
repeated FileDescriptor fds = 7;
|
||||
|
||||
// Whether or not the list of `fds` is complete or contains partial info
|
||||
optional bool fd_list_truncated = 8;
|
||||
|
||||
// Whether or not the target execution was allowed
|
||||
enum Decision {
|
||||
DECISION_UNKNOWN = 0;
|
||||
DECISION_ALLOW = 1;
|
||||
DECISION_DENY = 2;
|
||||
}
|
||||
optional Decision decision = 9;
|
||||
|
||||
// The policy applied when determining the decision
|
||||
enum Reason {
|
||||
REASON_UNKNOWN = 0;
|
||||
REASON_BINARY = 1;
|
||||
REASON_CERT = 2;
|
||||
REASON_COMPILER = 3;
|
||||
REASON_NOT_RUNNING = 4;
|
||||
REASON_PENDING_TRANSITIVE = 5;
|
||||
REASON_SCOPE = 6;
|
||||
REASON_TEAM_ID = 7;
|
||||
REASON_TRANSITIVE = 8;
|
||||
REASON_LONG_PATH = 9;
|
||||
REASON_NOT_RUNNING = 10;
|
||||
}
|
||||
optional Reason reason = 10;
|
||||
|
||||
// The mode Santa was in when the decision was applied
|
||||
enum Mode {
|
||||
MODE_UNKNOWN = 0;
|
||||
MODE_LOCKDOWN = 1;
|
||||
MODE_MONITOR = 2;
|
||||
}
|
||||
optional Mode mode = 11;
|
||||
|
||||
optional Decision decision = 1;
|
||||
optional Reason reason = 2;
|
||||
optional string explain = 3;
|
||||
optional string sha256 = 4;
|
||||
optional string cert_sha256 = 5;
|
||||
optional string cert_cn = 6;
|
||||
optional string quarantine_url = 7;
|
||||
optional ProcessInfo process_info = 8;
|
||||
optional Mode mode = 9;
|
||||
optional string path = 10;
|
||||
optional string original_path = 11;
|
||||
repeated string args = 12;
|
||||
optional string machine_id = 13;
|
||||
optional string team_id = 14;
|
||||
// Certificate information for the target executable
|
||||
optional CertificateInfo certificate_info = 12;
|
||||
|
||||
// Additional Santa metadata
|
||||
optional string explain = 13;
|
||||
|
||||
// Information known to LaunchServices about the target executable file
|
||||
optional string quarantine_url = 14;
|
||||
|
||||
// The original path on disk of the target executable
|
||||
// Applies when executables are translocated
|
||||
optional string original_path = 15;
|
||||
}
|
||||
|
||||
message DiskAppeared {
|
||||
optional string mount = 1;
|
||||
optional string volume = 2;
|
||||
optional string bsd_name = 3;
|
||||
optional string fs = 4;
|
||||
optional string model = 5;
|
||||
optional string serial = 6;
|
||||
optional string bus = 7;
|
||||
optional string dmg_path = 8;
|
||||
optional string appearance = 9;
|
||||
// Information about a fork event
|
||||
message Fork {
|
||||
// The forking process
|
||||
optional ProcessInfoLight instigator = 1;
|
||||
|
||||
// The newly formed child process
|
||||
optional ProcessInfoLight child = 2;
|
||||
}
|
||||
|
||||
message DiskDisappeared {
|
||||
optional string mount = 1;
|
||||
optional string volume = 2;
|
||||
optional string bsd_name = 3;
|
||||
// Information about an exit event
|
||||
message Exit {
|
||||
// The process that is exiting
|
||||
optional ProcessInfoLight instigator = 1;
|
||||
|
||||
// Exit status code information
|
||||
message Exited {
|
||||
optional int32 exit_status = 1;
|
||||
}
|
||||
|
||||
// Signal code
|
||||
message Signaled {
|
||||
optional int32 signal = 1;
|
||||
}
|
||||
|
||||
// Information on how/why the process exited
|
||||
oneof ExitType {
|
||||
Exited exited = 2;
|
||||
Signaled signaled = 3;
|
||||
Signaled stopped = 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Information about an open event
|
||||
message Open {
|
||||
// The process that is opening the file
|
||||
optional ProcessInfoLight instigator = 1;
|
||||
|
||||
// The file being opened
|
||||
optional FileInfo target = 2;
|
||||
|
||||
// Bitmask of flags used to open the file
|
||||
// Note: Represents the mask applied by the kernel, not the typical `open(2)`
|
||||
// flags (e.g. FREAD, FWRITE instead of O_RDONLY, O_RDWR, etc...)
|
||||
optional int32 flags = 3;
|
||||
}
|
||||
|
||||
// Information about a close event
|
||||
message Close {
|
||||
// The process closing the file
|
||||
optional ProcessInfoLight instigator = 1;
|
||||
|
||||
// The file being closed
|
||||
optional FileInfo target = 2;
|
||||
|
||||
// Whether or not the file was written to
|
||||
optional bool modified = 3;
|
||||
}
|
||||
|
||||
// Information about an exchagedata event
|
||||
// This event is not applicable to all filesystems (notably APFS)
|
||||
message Exchangedata {
|
||||
// The process that is exchanging the data
|
||||
optional ProcessInfoLight instigator = 1;
|
||||
|
||||
// File information for the two files in the exchangedata operation
|
||||
optional FileInfo file1 = 2;
|
||||
optional FileInfo file2 = 3;
|
||||
}
|
||||
|
||||
// Information about a rename event
|
||||
message Rename {
|
||||
// The process renaming the file
|
||||
optional ProcessInfoLight instigator = 1;
|
||||
|
||||
// The source file being renamed
|
||||
optional FileInfo source = 2;
|
||||
|
||||
// The target path when the rename is complete
|
||||
optional string target = 3;
|
||||
|
||||
// Whether or not the target path previously existed
|
||||
optional bool target_existed = 4;
|
||||
}
|
||||
|
||||
// Information about an unlink event
|
||||
message Unlink {
|
||||
// The process deleting the file
|
||||
optional ProcessInfoLight instigator = 1;
|
||||
|
||||
// The file being deleted
|
||||
optional FileInfo target = 2;
|
||||
}
|
||||
|
||||
// Information about a link event
|
||||
message Link {
|
||||
// The process performing the link
|
||||
optional ProcessInfoLight instigator = 1;
|
||||
|
||||
// The source file being linked
|
||||
optional FileInfo source = 2;
|
||||
|
||||
// The path of the new link
|
||||
optional string target = 3;
|
||||
}
|
||||
|
||||
// Information about when disks are added or removed
|
||||
message Disk {
|
||||
// Whether the disk just appeared or disappeared from the system
|
||||
enum Action {
|
||||
ACTION_UNKNOWN = 0;
|
||||
ACTION_APPEARED = 1;
|
||||
ACTION_DISAPPEARED = 2;
|
||||
}
|
||||
optional Action action = 1;
|
||||
|
||||
// Volume path
|
||||
optional string mount = 2;
|
||||
|
||||
// Volume name
|
||||
optional string volume = 3;
|
||||
|
||||
// Media BSD name
|
||||
optional string bsd_name = 4;
|
||||
|
||||
// Kind of volume
|
||||
optional string fs = 5;
|
||||
|
||||
// Device vendor and model information
|
||||
optional string model = 6;
|
||||
|
||||
// Serial number of the device
|
||||
optional string serial = 7;
|
||||
|
||||
// Device protocol
|
||||
optional string bus = 8;
|
||||
|
||||
// Path of the DMG
|
||||
optional string dmg_path = 9;
|
||||
|
||||
// Time device appeared/disappeared
|
||||
optional google.protobuf.Timestamp appearance = 10;
|
||||
}
|
||||
|
||||
// Information emitted when Santa captures bundle information
|
||||
message Bundle {
|
||||
// This is the hash of the file within the bundle that triggered the event
|
||||
optional string sha256 = 1;
|
||||
optional Hash file_hash = 1;
|
||||
|
||||
// This is the hash of the hashes of all executables in the bundle
|
||||
optional string bundle_hash = 2;
|
||||
optional Hash bundle_hash = 2;
|
||||
|
||||
// Name of the bundle
|
||||
optional string bundle_name = 3;
|
||||
|
||||
// Bundle identifier
|
||||
optional string bundle_id = 4;
|
||||
|
||||
// Bundle path
|
||||
optional string bundle_path = 5;
|
||||
|
||||
// Path of the file within the bundle that triggered the event
|
||||
optional string path = 6;
|
||||
}
|
||||
|
||||
message Fork {
|
||||
optional ProcessInfo process_info = 1;
|
||||
}
|
||||
|
||||
message Exit {
|
||||
optional ProcessInfo process_info = 1;
|
||||
}
|
||||
|
||||
// Information for a transitive allowlist rule
|
||||
message Allowlist {
|
||||
optional int32 pid = 1;
|
||||
optional int32 pidversion = 2;
|
||||
optional string path = 3;
|
||||
optional string sha256 = 4;
|
||||
// The process that caused the allowlist rule to be generated
|
||||
optional ProcessInfoLight instigator = 1;
|
||||
|
||||
// The file the new allowlist rule applies to
|
||||
optional FileInfo target = 2;
|
||||
}
|
||||
|
||||
// A message encapsulating a single event
|
||||
message SantaMessage {
|
||||
google.protobuf.Timestamp event_time = 1;
|
||||
// Machine ID of the host emitting this log
|
||||
// Only valid when EnableMachineIDDecoration configuration option is set
|
||||
optional string machine_id = 1;
|
||||
|
||||
oneof message {
|
||||
FileModification file_modification = 2;
|
||||
Execution execution = 3;
|
||||
DiskAppeared disk_appeared = 4;
|
||||
DiskDisappeared disk_disappeared = 5;
|
||||
Bundle bundle = 6;
|
||||
Fork fork = 7;
|
||||
Exit exit = 8;
|
||||
Allowlist allowlist = 9;
|
||||
}
|
||||
// Timestamp when the event occurred
|
||||
optional google.protobuf.Timestamp event_time = 2;
|
||||
|
||||
// Timestamp when Santa finished processing the event
|
||||
optional google.protobuf.Timestamp processed_time = 3;
|
||||
|
||||
// Event type being described by this message
|
||||
oneof event {
|
||||
Execution execution = 10;
|
||||
Fork fork = 11;
|
||||
Exit exit = 12;
|
||||
Close close = 13;
|
||||
Rename rename = 14;
|
||||
Unlink unlink = 15;
|
||||
Link link = 16;
|
||||
Exchangedata exchangedata = 17;
|
||||
Disk disk = 18;
|
||||
Bundle bundle = 19;
|
||||
Allowlist allowlist = 20;
|
||||
};
|
||||
}
|
||||
|
||||
message SantaMessageBatch {
|
||||
repeated SantaMessage messages = 1;
|
||||
}
|
||||
|
||||
message LogBatch {
|
||||
|
||||
20
Source/common/santa_proto_include_wrapper.h
Normal file
20
Source/common/santa_proto_include_wrapper.h
Normal file
@@ -0,0 +1,20 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__COMMON_SANTA_PROTO_INCLUDE_WRAPPER_H
|
||||
#define SANTA__COMMON_SANTA_PROTO_INCLUDE_WRAPPER_H
|
||||
|
||||
#include "Source/common/santa.pb.h"
|
||||
|
||||
#endif
|
||||
@@ -12,8 +12,6 @@
|
||||
/// 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>
|
||||
|
||||
|
||||
@@ -7,13 +7,36 @@ package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "santactl_cmd",
|
||||
srcs = [
|
||||
"SNTCommand.m",
|
||||
"SNTCommandController.m",
|
||||
],
|
||||
hdrs = [
|
||||
"SNTCommand.h",
|
||||
"SNTCommandController.h",
|
||||
],
|
||||
deps = [
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTCommandPrintLog",
|
||||
srcs = ["Commands/SNTCommandPrintLog.mm"],
|
||||
deps = [
|
||||
":santactl_cmd",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:santa_cc_proto_library_wrapper",
|
||||
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:binaryproto_cc_proto_library_wrapper",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "santactl_lib",
|
||||
srcs = [
|
||||
"SNTCommand.h",
|
||||
"SNTCommand.m",
|
||||
"SNTCommandController.h",
|
||||
"SNTCommandController.m",
|
||||
"main.m",
|
||||
"Commands/SNTCommandFileInfo.m",
|
||||
"Commands/SNTCommandRule.m",
|
||||
@@ -26,7 +49,6 @@ objc_library(
|
||||
"//:opt_build": [],
|
||||
"//conditions:default": [
|
||||
"Commands/SNTCommandBundleInfo.m",
|
||||
"Commands/SNTCommandCacheHistogram.m",
|
||||
"Commands/SNTCommandCheckCache.m",
|
||||
"Commands/SNTCommandFlushCache.m",
|
||||
],
|
||||
@@ -34,6 +56,8 @@ objc_library(
|
||||
sdk_dylibs = ["libz"],
|
||||
sdk_frameworks = ["IOKit"],
|
||||
deps = [
|
||||
":SNTCommandPrintLog",
|
||||
":santactl_cmd",
|
||||
"//Source/common:SNTCachedDecision",
|
||||
"//Source/common:SNTCommon",
|
||||
"//Source/common:SNTCommonEnums",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -68,14 +68,34 @@ REGISTER_COMMAND_NAME(@"metrics")
|
||||
|
||||
for (NSString *fieldName in metric[@"fields"]) {
|
||||
for (NSDictionary *field in metric[@"fields"][fieldName]) {
|
||||
const char *fieldNameStr = [fieldName cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
const char *fieldValueStr = [field[@"value"] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
const char *createdStr = [field[@"created"] UTF8String];
|
||||
const char *lastUpdatedStr = [field[@"last_updated"] UTF8String];
|
||||
const char *data = [[NSString stringWithFormat:@"%@", field[@"data"]] UTF8String];
|
||||
|
||||
if (strlen(fieldNameStr) > 0) {
|
||||
printf(" %-25s | %s=%s\n", "Field", fieldNameStr, fieldValueStr);
|
||||
NSArray<NSString *> *fields = [fieldName componentsSeparatedByString:@","];
|
||||
NSArray<NSString *> *fieldValues = [field[@"value"] componentsSeparatedByString:@","];
|
||||
|
||||
if (fields.count != fieldValues.count) {
|
||||
fprintf(stderr, "metric %s has a different number of field names and field values",
|
||||
[fieldName UTF8String]);
|
||||
continue;
|
||||
}
|
||||
|
||||
NSString *fieldDisplayString = @"";
|
||||
|
||||
if (fields.count >= 1 && fields[0].length) {
|
||||
for (int i = 0; i < fields.count; i++) {
|
||||
fieldDisplayString = [fieldDisplayString
|
||||
stringByAppendingString:[NSString
|
||||
stringWithFormat:@"%@=%@", fields[i], fieldValues[i]]];
|
||||
if (i < fields.count - 1) {
|
||||
fieldDisplayString = [fieldDisplayString stringByAppendingString:@","];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (![fieldDisplayString isEqualToString:@""]) {
|
||||
printf(" %-25s | %s\n", "Field", [fieldDisplayString UTF8String]);
|
||||
}
|
||||
|
||||
printf(" %-25s | %s\n", "Created", createdStr);
|
||||
|
||||
131
Source/santactl/Commands/SNTCommandPrintLog.mm
Normal file
131
Source/santactl/Commands/SNTCommandPrintLog.mm
Normal file
@@ -0,0 +1,131 @@
|
||||
/// 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>
|
||||
#include <google/protobuf/util/json_util.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#include "Source/common/santa_proto_include_wrapper.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/binaryproto_proto_include_wrapper.h"
|
||||
#include "google/protobuf/any.pb.h"
|
||||
|
||||
using google::protobuf::util::JsonPrintOptions;
|
||||
using google::protobuf::util::MessageToJsonString;
|
||||
using santa::fsspool::binaryproto::LogBatch;
|
||||
namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
@interface SNTCommandPrintLog : SNTCommand <SNTCommandProtocol>
|
||||
@end
|
||||
|
||||
@implementation SNTCommandPrintLog
|
||||
|
||||
REGISTER_COMMAND_NAME(@"printlog")
|
||||
|
||||
+ (BOOL)requiresRoot {
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)requiresDaemonConn {
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (NSString *)shortHelpText {
|
||||
return @"Prints the contents of Santa protobuf log files as JSON.";
|
||||
}
|
||||
|
||||
+ (NSString *)longHelpText {
|
||||
return @"Prints the contents of serialized Santa protobuf logs as JSON.\n"
|
||||
@"Multiple paths can be provided. The output is a list of all the \n"
|
||||
@"SantaMessage entries per-file. E.g.: \n"
|
||||
@" [\n"
|
||||
@" [\n"
|
||||
@" ... file 1 contents ...\n"
|
||||
@" ],\n"
|
||||
@" [\n"
|
||||
@" ... file N contents ...\n"
|
||||
@" ]\n"
|
||||
@" ]";
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
JsonPrintOptions options;
|
||||
options.always_print_enums_as_ints = false;
|
||||
options.always_print_primitive_fields = true;
|
||||
options.preserve_proto_field_names = true;
|
||||
options.add_whitespace = true;
|
||||
|
||||
for (int argIdx = 0; argIdx < [arguments count]; argIdx++) {
|
||||
NSString *path = arguments[argIdx];
|
||||
int fd = open([path UTF8String], O_RDONLY);
|
||||
if (fd == -1) {
|
||||
LOGE(@"Failed to open '%@': errno: %d: %s", path, errno, strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
LogBatch logBatch;
|
||||
bool ret = logBatch.ParseFromFileDescriptor(fd);
|
||||
close(fd);
|
||||
|
||||
if (!ret) {
|
||||
LOGE(@"Failed to parse '%@'", path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argIdx != 0) {
|
||||
std::cout << ",";
|
||||
} else {
|
||||
// Print the opening outer JSON array
|
||||
std::cout << "[";
|
||||
}
|
||||
std::cout << "\n[\n";
|
||||
|
||||
int numRecords = logBatch.records_size();
|
||||
|
||||
for (int i = 0; i < numRecords; i++) {
|
||||
const google::protobuf::Any &any = logBatch.records(i);
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
if (!any.UnpackTo(&santaMsg)) {
|
||||
LOGE(@"Failed to unpack Any proto to SantaMessage in file '%@'", path);
|
||||
break;
|
||||
}
|
||||
|
||||
if (i != 0) {
|
||||
std::cout << ",\n";
|
||||
}
|
||||
|
||||
std::string json;
|
||||
if (!MessageToJsonString(santaMsg, &json, options).ok()) {
|
||||
LOGE(@"Unable to convert message to JSON in file: '%@'", path);
|
||||
}
|
||||
std::cout << json;
|
||||
}
|
||||
|
||||
std::cout << "]" << std::flush;
|
||||
|
||||
if (argIdx == ([arguments count] - 1)) {
|
||||
// Print the closing outer JSON array
|
||||
std::cout << "]\n";
|
||||
}
|
||||
}
|
||||
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
@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.
|
||||
@@ -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);
|
||||
|
||||
@@ -38,18 +38,18 @@
|
||||
"type" : 9,
|
||||
"description" : "Count of process exec events on the host",
|
||||
"fields" : {
|
||||
"rule_type" : [
|
||||
"rule_type,client" : [
|
||||
{
|
||||
"created" : "2021-09-16T21:07:34.826Z",
|
||||
"last_updated" : "2021-09-16T21:07:34.826Z",
|
||||
"value" : "binary",
|
||||
"data" : 1
|
||||
"value" : "certificate,authorizer",
|
||||
"data" : 2
|
||||
},
|
||||
{
|
||||
"created" : "2021-09-16T21:07:34.826Z",
|
||||
"last_updated" : "2021-09-16T21:07:34.826Z",
|
||||
"value" : "certificate",
|
||||
"data" : 2
|
||||
"value" : "binary,authorizer",
|
||||
"data" : 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -30,14 +30,14 @@
|
||||
Metric Name | /santa/events
|
||||
Description | Count of process exec events on the host
|
||||
Type | SNTMetricTypeCounter
|
||||
Field | rule_type=binary
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 1
|
||||
Field | rule_type=certificate
|
||||
Field | rule_type=certificate,client=authorizer
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 2
|
||||
Field | rule_type=binary,client=authorizer
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 1
|
||||
|
||||
Metric Name | /santa/using_endpoint_security_framework
|
||||
Description | Is santad using the endpoint security framework
|
||||
|
||||
1336
Source/santad/BUILD
1336
Source/santad/BUILD
File diff suppressed because it is too large
Load Diff
@@ -105,6 +105,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
|
||||
// This is a Santa-curated list of paths to check on startup. This list will be merged
|
||||
// with the set of default muted paths from ES.
|
||||
|
||||
NSSet *santaDefinedCriticalPaths = [NSSet setWithArray:@[
|
||||
@"/usr/libexec/trustd",
|
||||
@"/usr/lib/dyld",
|
||||
@@ -136,6 +137,12 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
NSMutableDictionary *bins = [NSMutableDictionary dictionary];
|
||||
for (NSString *path in [SNTRuleTable criticalSystemBinaryPaths]) {
|
||||
SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithPath:path];
|
||||
if (!binInfo.SHA256) {
|
||||
// If there isn't a hash, no need to compute the other info here.
|
||||
// Just continue on to the next binary.
|
||||
LOGW(@"Unable to compute hash for critical system binary %@.", path);
|
||||
continue;
|
||||
}
|
||||
MOLCodesignChecker *csInfo = [binInfo codesignCheckerWithError:NULL];
|
||||
|
||||
// Make sure the critical system binary is signed by the same chain as launchd/self
|
||||
@@ -143,9 +150,9 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
if ([csInfo signingInformationMatches:self.launchdCSInfo]) {
|
||||
systemBin = YES;
|
||||
} else if (![csInfo signingInformationMatches:self.santadCSInfo]) {
|
||||
LOGE(@"Unable to validate critical system binary. "
|
||||
LOGW(@"Unable to validate critical system binary %@. "
|
||||
@"pid 1: %@, santad: %@ and %@: %@ do not match.",
|
||||
self.launchdCSInfo.leafCertificate, self.santadCSInfo.leafCertificate, path,
|
||||
path, self.launchdCSInfo.leafCertificate, self.santadCSInfo.leafCertificate, path,
|
||||
csInfo.leafCertificate);
|
||||
continue;
|
||||
}
|
||||
|
||||
75
Source/santad/EventProviders/AuthResultCache.h
Normal file
75
Source/santad/EventProviders/AuthResultCache.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_AUTHRESULTCACHE_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_AUTHRESULTCACHE_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <sys/stat.h>
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
enum class FlushCacheMode {
|
||||
kNonRootOnly,
|
||||
kAllCaches,
|
||||
};
|
||||
|
||||
class AuthResultCache {
|
||||
public:
|
||||
// Santa currently only flushes caches when new DENY rules are added, not
|
||||
// ALLOW rules. This means this value should be low enough so that if a
|
||||
// previously denied binary is allowed, it can be re-executed by the user in a
|
||||
// timely manner. But the value should be high enough to allow the cache to be
|
||||
// effective in the event the binary is executed in rapid succession.
|
||||
AuthResultCache(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms = 1500);
|
||||
virtual ~AuthResultCache();
|
||||
|
||||
AuthResultCache(AuthResultCache &&other) = delete;
|
||||
AuthResultCache &operator=(AuthResultCache &&rhs) = delete;
|
||||
AuthResultCache(const AuthResultCache &other) = delete;
|
||||
AuthResultCache &operator=(const AuthResultCache &other) = delete;
|
||||
|
||||
virtual bool AddToCache(const es_file_t *es_file, santa_action_t decision);
|
||||
virtual void RemoveFromCache(const es_file_t *es_file);
|
||||
virtual santa_action_t CheckCache(const es_file_t *es_file);
|
||||
virtual santa_action_t CheckCache(santa_vnode_id_t vnode_id);
|
||||
|
||||
virtual void FlushCache(FlushCacheMode mode);
|
||||
|
||||
virtual NSArray<NSNumber *> *CacheCounts();
|
||||
|
||||
private:
|
||||
virtual SantaCache<santa_vnode_id_t, uint64_t> *CacheForVnodeID(santa_vnode_id_t vnode_id);
|
||||
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *root_cache_;
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *nonroot_cache_;
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
uint64_t root_devno_;
|
||||
uint64_t cache_deny_time_ns_;
|
||||
dispatch_queue_t q_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
|
||||
#endif
|
||||
156
Source/santad/EventProviders/AuthResultCache.mm
Normal file
156
Source/santad/EventProviders/AuthResultCache.mm
Normal file
@@ -0,0 +1,156 @@
|
||||
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
|
||||
#include <mach/clock_types.h>
|
||||
#include <time.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
|
||||
template <>
|
||||
uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const &t) {
|
||||
return (SantaCacheHasher<uint64_t>(t.fsid) << 1) ^ SantaCacheHasher<uint64_t>(t.fileid);
|
||||
}
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
|
||||
return santa_vnode_id_t{
|
||||
.fsid = (uint64_t)es_file->stat.st_dev,
|
||||
.fileid = es_file->stat.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
static inline uint64_t GetCurrentUptime() {
|
||||
return clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
}
|
||||
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
static inline uint64_t CacheableAction(santa_action_t action,
|
||||
uint64_t timestamp = GetCurrentUptime()) {
|
||||
return ((uint64_t)action << 56) | (timestamp & 0xFFFFFFFFFFFFFF);
|
||||
}
|
||||
|
||||
static inline santa_action_t ActionFromCachedValue(uint64_t cachedValue) {
|
||||
return (santa_action_t)(cachedValue >> 56);
|
||||
}
|
||||
|
||||
static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
|
||||
return (cachedValue & ~(0xFF00000000000000));
|
||||
}
|
||||
|
||||
AuthResultCache::AuthResultCache(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms)
|
||||
: esapi_(esapi), cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
|
||||
root_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
nonroot_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
|
||||
struct stat sb;
|
||||
if (stat("/", &sb) == 0) {
|
||||
root_devno_ = sb.st_dev;
|
||||
}
|
||||
|
||||
q_ = dispatch_queue_create(
|
||||
"com.google.santa.daemon.auth_result_cache.q",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
}
|
||||
|
||||
AuthResultCache::~AuthResultCache() {
|
||||
delete root_cache_;
|
||||
delete nonroot_cache_;
|
||||
}
|
||||
|
||||
bool AuthResultCache::AddToCache(const es_file_t *es_file, santa_action_t decision) {
|
||||
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
|
||||
switch (decision) {
|
||||
case ACTION_REQUEST_BINARY:
|
||||
return cache->set(vnode_id, CacheableAction(ACTION_REQUEST_BINARY, 0), 0);
|
||||
case ACTION_RESPOND_ALLOW: OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_ALLOW_COMPILER: OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_DENY:
|
||||
return cache->set(vnode_id, CacheableAction(decision),
|
||||
CacheableAction(ACTION_REQUEST_BINARY, 0));
|
||||
default:
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"Invalid cache value, exiting.");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
void AuthResultCache::RemoveFromCache(const es_file_t *es_file) {
|
||||
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
|
||||
CacheForVnodeID(vnode_id)->remove(vnode_id);
|
||||
}
|
||||
|
||||
santa_action_t AuthResultCache::CheckCache(const es_file_t *es_file) {
|
||||
return CheckCache(VnodeForFile(es_file));
|
||||
}
|
||||
|
||||
santa_action_t AuthResultCache::CheckCache(santa_vnode_id_t vnode_id) {
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
|
||||
|
||||
uint64_t cached_val = cache->get(vnode_id);
|
||||
if (cached_val == 0) {
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
|
||||
santa_action_t result = ActionFromCachedValue(cached_val);
|
||||
|
||||
if (result == ACTION_RESPOND_DENY) {
|
||||
uint64_t expiry_time = TimestampFromCachedValue(cached_val) + cache_deny_time_ns_;
|
||||
if (expiry_time < GetCurrentUptime()) {
|
||||
cache->remove(vnode_id);
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *AuthResultCache::CacheForVnodeID(
|
||||
santa_vnode_id_t vnode_id) {
|
||||
return (vnode_id.fsid == root_devno_ || root_devno_ == 0) ? root_cache_ : nonroot_cache_;
|
||||
}
|
||||
|
||||
void AuthResultCache::FlushCache(FlushCacheMode mode) {
|
||||
nonroot_cache_->clear();
|
||||
if (mode == FlushCacheMode::kAllCaches) {
|
||||
root_cache_->clear();
|
||||
|
||||
// Clear the ES cache when all local caches are flushed. Assume the ES cache
|
||||
// doesn't need to be cleared when only flushing the non-root cache.
|
||||
//
|
||||
// Calling into ES should be done asynchronously since it could otherwise
|
||||
// potentially deadlock.
|
||||
auto shared_esapi = esapi_->shared_from_this();
|
||||
dispatch_async(q_, ^{
|
||||
// ES does not need a connected client to clear cache
|
||||
shared_esapi->ClearCache(Client());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
NSArray<NSNumber *> *AuthResultCache::CacheCounts() {
|
||||
return @[ @(root_cache_->count()), @(nonroot_cache_->count()) ];
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
225
Source/santad/EventProviders/AuthResultCacheTest.mm
Normal file
225
Source/santad/EventProviders/AuthResultCacheTest.mm
Normal file
@@ -0,0 +1,225 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <Foundation/Foundation.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
|
||||
// Grab the st_dev number of the root volume to match the root cache
|
||||
static uint64_t RootDevno() {
|
||||
static dispatch_once_t once_token;
|
||||
static uint64_t devno;
|
||||
dispatch_once(&once_token, ^{
|
||||
struct stat sb;
|
||||
stat("/", &sb);
|
||||
devno = sb.st_dev;
|
||||
});
|
||||
return devno;
|
||||
}
|
||||
|
||||
static inline es_file_t MakeCacheableFile(uint64_t devno, uint64_t ino) {
|
||||
return es_file_t{
|
||||
.path = {}, .path_truncated = false, .stat = {.st_dev = (dev_t)devno, .st_ino = ino}};
|
||||
}
|
||||
|
||||
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
|
||||
return santa_vnode_id_t{
|
||||
.fsid = (uint64_t)es_file->stat.st_dev,
|
||||
.fileid = es_file->stat.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uint64_t root_count,
|
||||
uint64_t nonroot_count) {
|
||||
NSArray<NSNumber *> *counts = cache->CacheCounts();
|
||||
|
||||
XCTAssertNotNil(counts);
|
||||
XCTAssertEqual([counts count], 2);
|
||||
XCTAssertNotNil(counts[0]);
|
||||
XCTAssertNotNil(counts[1]);
|
||||
XCTAssertEqual([counts[0] unsignedLongLongValue], root_count);
|
||||
XCTAssertEqual([counts[1] unsignedLongLongValue], nonroot_count);
|
||||
}
|
||||
|
||||
@interface AuthResultCacheTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation AuthResultCacheTest
|
||||
|
||||
- (void)testEmptyCacheExpectedNumberOfCacheCounts {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testBasicOperation {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 222);
|
||||
|
||||
// Add the root file to the cache
|
||||
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_UNSET);
|
||||
|
||||
// Now add the non-root file
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
// Update the cached values
|
||||
cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW);
|
||||
cache->AddToCache(&nonrootFile, ACTION_RESPOND_DENY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
XCTAssertEqual(cache->CheckCache(VnodeForFile(&rootFile)), ACTION_RESPOND_ALLOW);
|
||||
XCTAssertEqual(cache->CheckCache(VnodeForFile(&nonrootFile)), ACTION_RESPOND_DENY);
|
||||
|
||||
// Remove the root file
|
||||
cache->RemoveFromCache(&rootFile);
|
||||
|
||||
AssertCacheCounts(cache, 0, 1);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_RESPOND_DENY);
|
||||
}
|
||||
|
||||
- (void)testFlushCache {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 111);
|
||||
|
||||
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
// Flush non-root only
|
||||
cache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
// Add back the non-root file
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
// Flush all caches
|
||||
// The call to ClearCache is asynchronous. Use a semaphore to
|
||||
// be notified when the mock is called.
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
EXPECT_CALL(*mockESApi, ClearCache).WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
dispatch_semaphore_signal(sema);
|
||||
return true;
|
||||
}));
|
||||
cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
"ClearCache wasn't called within expected time window");
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testCacheStateMachine {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
// Cached items must first be in the ACTION_REQUEST_BINARY state
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW));
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW_COMPILER));
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
// Items in the `ACTION_REQUEST_BINARY` state cannot reenter the same state
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
santa_action_t allowed_transitions[] = {
|
||||
ACTION_RESPOND_ALLOW,
|
||||
ACTION_RESPOND_ALLOW_COMPILER,
|
||||
ACTION_RESPOND_DENY,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(allowed_transitions) / sizeof(allowed_transitions[0]); i++) {
|
||||
// First make sure the item doesn't exist
|
||||
cache->RemoveFromCache(&rootFile);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
|
||||
// Now add the item to be in the first allowed state
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
// Now assert the allowed transition
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, allowed_transitions[i]));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), allowed_transitions[i]);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testCacheExpiry {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
// Create a cache with a lowered cache expiry value
|
||||
uint64_t expiryMS = 250;
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi, expiryMS);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
// Add a file to the cache and put into the ACTION_RESPOND_DENY state
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
|
||||
|
||||
// Ensure the file exists
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_RESPOND_DENY);
|
||||
|
||||
// Wait for the item to expire
|
||||
SleepMS(expiryMS);
|
||||
|
||||
// Check cache counts to make sure the item still exists
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
// Now check the cache, which will remove the item
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2021-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -50,7 +50,8 @@ typedef void (^MockDADiskAppearedCallback)(DADiskRef ref);
|
||||
@end
|
||||
|
||||
//
|
||||
// All DiskArbitration functions used in SNTDeviceManager and shimmed out accordingly.
|
||||
// All DiskArbitration functions used in SNTEndpointSecurityDeviceManager
|
||||
// and shimmed out accordingly.
|
||||
//
|
||||
CF_EXTERN_C_BEGIN
|
||||
|
||||
|
||||
69
Source/santad/EventProviders/EndpointSecurity/Client.h
Normal file
69
Source/santad/EventProviders/EndpointSecurity/Client.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_CLIENT_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_CLIENT_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class Client {
|
||||
public:
|
||||
explicit Client(es_client_t* client, es_new_client_result_t result)
|
||||
: client_(client), result_(result) {}
|
||||
|
||||
Client() : client_(nullptr), result_(ES_NEW_CLIENT_RESULT_ERR_INTERNAL) {}
|
||||
|
||||
virtual ~Client() {
|
||||
if (client_) {
|
||||
// Special case: Not using EndpointSecurityAPI here due to circular refs.
|
||||
es_delete_client(client_);
|
||||
}
|
||||
}
|
||||
|
||||
Client(Client&& other) {
|
||||
client_ = other.client_;
|
||||
result_ = other.result_;
|
||||
other.client_ = nullptr;
|
||||
other.result_ = ES_NEW_CLIENT_RESULT_ERR_INTERNAL;
|
||||
}
|
||||
|
||||
Client& operator=(Client&& rhs) {
|
||||
client_ = rhs.client_;
|
||||
result_ = rhs.result_;
|
||||
rhs.client_ = nullptr;
|
||||
rhs.result_ = ES_NEW_CLIENT_RESULT_ERR_INTERNAL;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Client(const Client& other) = delete;
|
||||
void operator=(const Client& rhs) = delete;
|
||||
|
||||
inline bool IsConnected() { return result_ == ES_NEW_CLIENT_RESULT_SUCCESS; }
|
||||
|
||||
inline es_new_client_result_t NewClientResult() { return result_; }
|
||||
|
||||
inline es_client_t* Get() const { return client_; }
|
||||
|
||||
private:
|
||||
es_client_t* client_;
|
||||
es_new_client_result_t result_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
118
Source/santad/EventProviders/EndpointSecurity/ClientTest.mm
Normal file
118
Source/santad/EventProviders/EndpointSecurity/ClientTest.mm
Normal file
@@ -0,0 +1,118 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
|
||||
// Global semaphore used for custom `es_delete_client` function
|
||||
dispatch_semaphore_t gSema;
|
||||
|
||||
// Note: The Client class does not use the `EndpointSecurityAPI` wrappers due
|
||||
// to circular dependency issues. It is a special case that uses the underlying
|
||||
// ES API `es_delete_client` directly. This test override will signal the
|
||||
// `gSema` semaphore to indicate it has been called.
|
||||
es_return_t es_delete_client(es_client_t *_Nullable client) {
|
||||
dispatch_semaphore_signal(gSema);
|
||||
return ES_RETURN_SUCCESS;
|
||||
};
|
||||
|
||||
@interface ClientTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation ClientTest
|
||||
|
||||
- (void)setUp {
|
||||
gSema = dispatch_semaphore_create(0);
|
||||
}
|
||||
|
||||
- (void)testConstructorsAndDestructors {
|
||||
// Ensure constructors set internal state properly
|
||||
// Anonymous scopes used to ensure destructors called as expected
|
||||
|
||||
// Null `es_client_t*` *shouldn't* trigger `es_delete_client`
|
||||
{
|
||||
Client c;
|
||||
XCTAssertEqual(c.Get(), nullptr);
|
||||
XCTAssertEqual(c.NewClientResult(), ES_NEW_CLIENT_RESULT_ERR_INTERNAL);
|
||||
}
|
||||
|
||||
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client called unexpectedly");
|
||||
|
||||
// Nonnull `es_client_t*` *should* trigger `es_delete_client`
|
||||
{
|
||||
int fake;
|
||||
es_client_t *fakeClient = (es_client_t *)&fake;
|
||||
Client c(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
XCTAssertEqual(c.Get(), fakeClient);
|
||||
XCTAssertEqual(c.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client not called within expected time window");
|
||||
|
||||
// Test move constructor
|
||||
{
|
||||
int fake;
|
||||
es_client_t *fakeClient = (es_client_t *)&fake;
|
||||
Client c1(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
|
||||
Client c2(std::move(c1));
|
||||
|
||||
XCTAssertEqual(c1.Get(), nullptr);
|
||||
XCTAssertEqual(c2.Get(), fakeClient);
|
||||
XCTAssertEqual(c2.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
// Ensure `es_delete_client` was only called once when both `c1` and `c2`
|
||||
// are destructed.
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client not called within expected time window");
|
||||
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client called unexpectedly");
|
||||
|
||||
// Test move assignment
|
||||
{
|
||||
int fake;
|
||||
es_client_t *fakeClient = (es_client_t *)&fake;
|
||||
Client c1(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
Client c2;
|
||||
|
||||
c2 = std::move(c1);
|
||||
|
||||
XCTAssertEqual(c1.Get(), nullptr);
|
||||
XCTAssertEqual(c2.Get(), fakeClient);
|
||||
XCTAssertEqual(c2.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
// Ensure `es_delete_client` was only called once when both `c1` and `c2`
|
||||
// are destructed.
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client not called within expected time window");
|
||||
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client called unexpectedly");
|
||||
}
|
||||
|
||||
- (void)testIsConnected {
|
||||
XCTAssertFalse(Client().IsConnected());
|
||||
XCTAssertFalse(Client(nullptr, ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED).IsConnected());
|
||||
XCTAssertTrue(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS).IsConnected());
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,58 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#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);
|
||||
|
||||
virtual uint32_t ExecEnvCount(const es_event_exec_t *event);
|
||||
virtual es_string_token_t ExecEnv(const es_event_exec_t *event, uint32_t index);
|
||||
|
||||
virtual uint32_t ExecFDCount(const es_event_exec_t *event);
|
||||
virtual const es_fd_t *ExecFD(const es_event_exec_t *event, uint32_t index);
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,111 @@
|
||||
/// 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);
|
||||
}
|
||||
|
||||
uint32_t EndpointSecurityAPI::ExecEnvCount(const es_event_exec_t *event) {
|
||||
return es_exec_env_count(event);
|
||||
}
|
||||
|
||||
es_string_token_t EndpointSecurityAPI::ExecEnv(const es_event_exec_t *event, uint32_t index) {
|
||||
return es_exec_env(event, index);
|
||||
}
|
||||
|
||||
uint32_t EndpointSecurityAPI::ExecFDCount(const es_event_exec_t *event) {
|
||||
if (@available(macOS 11.0, *)) {
|
||||
return es_exec_fd_count(event);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const es_fd_t *EndpointSecurityAPI::ExecFD(const es_event_exec_t *event, uint32_t index) {
|
||||
if (@available(macOS 11.0, *)) {
|
||||
return es_exec_fd(event, index);
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
326
Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h
Normal file
326
Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h
Normal file
@@ -0,0 +1,326 @@
|
||||
/// 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 <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class EnrichedFile {
|
||||
public:
|
||||
EnrichedFile()
|
||||
: user_(std::nullopt), group_(std::nullopt), hash_(std::nullopt) {}
|
||||
|
||||
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)) {}
|
||||
|
||||
EnrichedFile(EnrichedFile &&other)
|
||||
: user_(std::move(other.user_)),
|
||||
group_(std::move(other.group_)),
|
||||
hash_(std::move(other.hash_)) {}
|
||||
|
||||
EnrichedFile(const EnrichedFile &other) = delete;
|
||||
|
||||
const std::optional<std::shared_ptr<std::string>> &user() const {
|
||||
return user_;
|
||||
}
|
||||
const std::optional<std::shared_ptr<std::string>> &group() const {
|
||||
return group_;
|
||||
}
|
||||
|
||||
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()
|
||||
: effective_user_(std::nullopt),
|
||||
effective_group_(std::nullopt),
|
||||
real_user_(std::nullopt),
|
||||
real_group_(std::nullopt) {}
|
||||
|
||||
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)) {}
|
||||
|
||||
EnrichedProcess(EnrichedProcess &&other)
|
||||
: effective_user_(std::move(other.effective_user_)),
|
||||
effective_group_(std::move(other.effective_group_)),
|
||||
real_user_(std::move(other.real_user_)),
|
||||
real_group_(std::move(other.real_group_)),
|
||||
executable_(std::move(other.executable_)) {}
|
||||
|
||||
EnrichedProcess(const EnrichedProcess &other) = delete;
|
||||
|
||||
const std::optional<std::shared_ptr<std::string>> &effective_user() const {
|
||||
return effective_user_;
|
||||
}
|
||||
const std::optional<std::shared_ptr<std::string>> &effective_group() const {
|
||||
return effective_group_;
|
||||
}
|
||||
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_;
|
||||
}
|
||||
const EnrichedFile &executable() const { return executable_; }
|
||||
|
||||
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)) {
|
||||
clock_gettime(CLOCK_REALTIME, &enrichment_time_);
|
||||
}
|
||||
|
||||
EnrichedEventType(EnrichedEventType &&other)
|
||||
: es_msg_(std::move(other.es_msg_)),
|
||||
instigator_(std::move(other.instigator_)),
|
||||
enrichment_time_(std::move(other.enrichment_time_)) {}
|
||||
|
||||
EnrichedEventType(const EnrichedEventType &other) = delete;
|
||||
|
||||
virtual ~EnrichedEventType() = default;
|
||||
|
||||
const es_message_t &es_msg() const { return *es_msg_; }
|
||||
const EnrichedProcess &instigator() const { return instigator_; }
|
||||
struct timespec enrichment_time() const {
|
||||
// No reason to return a reference
|
||||
return enrichment_time_;
|
||||
}
|
||||
|
||||
private:
|
||||
Message es_msg_;
|
||||
EnrichedProcess instigator_;
|
||||
struct timespec enrichment_time_;
|
||||
};
|
||||
|
||||
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)) {}
|
||||
|
||||
EnrichedClose(EnrichedClose &&other)
|
||||
: EnrichedEventType(std::move(other)),
|
||||
target_(std::move(other.target_)) {}
|
||||
|
||||
EnrichedClose(const EnrichedClose &other) = delete;
|
||||
|
||||
const EnrichedFile &target() const { return 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)) {}
|
||||
|
||||
EnrichedExchange(EnrichedExchange &&other)
|
||||
: EnrichedEventType(std::move(other)),
|
||||
file1_(std::move(other.file1_)),
|
||||
file2_(std::move(other.file2_)) {}
|
||||
|
||||
EnrichedExchange(const EnrichedExchange &other) = delete;
|
||||
|
||||
const EnrichedFile &file1() const { return file1_; }
|
||||
const EnrichedFile &file2() const { return 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)) {}
|
||||
|
||||
EnrichedExec(EnrichedExec &&other)
|
||||
: EnrichedEventType(std::move(other)),
|
||||
target_(std::move(other.target_)),
|
||||
script_(std::move(other.script_)),
|
||||
working_dir_(std::move(other.working_dir_)) {}
|
||||
|
||||
EnrichedExec(const EnrichedExec &other) = delete;
|
||||
|
||||
const EnrichedProcess &target() const { return target_; }
|
||||
const std::optional<EnrichedFile> &script() const { return script_; }
|
||||
const std::optional<EnrichedFile> &working_dir() const {
|
||||
return 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)) {}
|
||||
|
||||
EnrichedExit(EnrichedExit &&other) : EnrichedEventType(std::move(other)) {}
|
||||
|
||||
EnrichedExit(const EnrichedExit &other) = delete;
|
||||
};
|
||||
|
||||
class EnrichedFork : public EnrichedEventType {
|
||||
public:
|
||||
EnrichedFork(Message &&es_msg, EnrichedProcess &&instigator,
|
||||
EnrichedProcess &&child)
|
||||
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
|
||||
child_(std::move(child)) {}
|
||||
|
||||
EnrichedFork(EnrichedFork &&other)
|
||||
: EnrichedEventType(std::move(other)), child_(std::move(other.child_)) {}
|
||||
|
||||
EnrichedFork(const EnrichedFork &other) = delete;
|
||||
|
||||
const EnrichedProcess &child() const { return child_; }
|
||||
|
||||
private:
|
||||
EnrichedProcess child_;
|
||||
};
|
||||
|
||||
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)) {}
|
||||
|
||||
EnrichedLink(EnrichedLink &&other)
|
||||
: EnrichedEventType(std::move(other)),
|
||||
source_(std::move(other.source_)),
|
||||
target_dir_(std::move(other.target_dir_)) {}
|
||||
|
||||
EnrichedLink(const EnrichedLink &other) = delete;
|
||||
|
||||
const EnrichedFile &source() const { return source_; }
|
||||
|
||||
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)) {}
|
||||
|
||||
EnrichedRename(EnrichedRename &&other)
|
||||
: EnrichedEventType(std::move(other)),
|
||||
source_(std::move(other.source_)),
|
||||
target_(std::move(other.target_)),
|
||||
target_dir_(std::move(other.target_dir_)) {}
|
||||
|
||||
EnrichedRename(const EnrichedRename &other) = delete;
|
||||
|
||||
const EnrichedFile &source() const { return source_; }
|
||||
|
||||
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)) {}
|
||||
|
||||
EnrichedUnlink(EnrichedUnlink &&other)
|
||||
: EnrichedEventType(std::move(other)),
|
||||
target_(std::move(other.target_)) {}
|
||||
|
||||
EnrichedUnlink(const EnrichedUnlink &other) = delete;
|
||||
|
||||
const EnrichedFile &target() const { return 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)) {}
|
||||
|
||||
const EnrichedType &GetEnrichedMessage() { return msg_; }
|
||||
|
||||
private:
|
||||
EnrichedType msg_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
44
Source/santad/EventProviders/EndpointSecurity/Enricher.h
Normal file
44
Source/santad/EventProviders/EndpointSecurity/Enricher.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHER_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHER_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class Enricher {
|
||||
public:
|
||||
Enricher();
|
||||
virtual ~Enricher() = default;
|
||||
virtual std::shared_ptr<EnrichedMessage> Enrich(Message &&msg);
|
||||
virtual EnrichedProcess Enrich(const es_process_t &es_proc);
|
||||
virtual EnrichedFile Enrich(const es_file_t &es_file);
|
||||
|
||||
virtual std::optional<std::shared_ptr<std::string>> UsernameForUID(uid_t uid);
|
||||
virtual std::optional<std::shared_ptr<std::string>> UsernameForGID(gid_t gid);
|
||||
|
||||
private:
|
||||
SantaCache<uid_t, std::optional<std::shared_ptr<std::string>>>
|
||||
username_cache_;
|
||||
SantaCache<gid_t, std::optional<std::shared_ptr<std::string>>>
|
||||
groupname_cache_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
137
Source/santad/EventProviders/EndpointSecurity/Enricher.mm
Normal file
137
Source/santad/EventProviders/EndpointSecurity/Enricher.mm
Normal file
@@ -0,0 +1,137 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <grp.h>
|
||||
#include <pwd.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <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 && es_msg->event.exec.cwd)
|
||||
? 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(NOGROUP_GID);
|
||||
XCTAssertTrue(group.has_value());
|
||||
XCTAssertEqual(strcmp(group->get()->c_str(), "nogroup"), 0);
|
||||
|
||||
uid_t invalidUID = (uid_t)-123;
|
||||
gid_t invalidGID = (gid_t)-123;
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> invalidUser = enricher.UsernameForUID(invalidUID);
|
||||
XCTAssertFalse(invalidUser.has_value());
|
||||
|
||||
std::optional<std::shared_ptr<std::string>> invalidGroup = enricher.UsernameForGID(invalidGID);
|
||||
XCTAssertFalse(invalidGroup.has_value());
|
||||
}
|
||||
|
||||
@end
|
||||
60
Source/santad/EventProviders/EndpointSecurity/Message.h
Normal file
60
Source/santad/EventProviders/EndpointSecurity/Message.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MESSAGE_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MESSAGE_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class EndpointSecurityAPI;
|
||||
|
||||
class Message {
|
||||
public:
|
||||
Message(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
const es_message_t* es_msg);
|
||||
~Message();
|
||||
|
||||
Message(Message&& other);
|
||||
// Note: Safe to implement this, just not currently needed so left deleted.
|
||||
Message& operator=(Message&& rhs) = delete;
|
||||
|
||||
// In macOS 10.15, es_retain_message/es_release_message were unsupported
|
||||
// and required a full copy, which impacts performance if done too much...
|
||||
Message(const Message& other);
|
||||
Message& operator=(const Message& other) = delete;
|
||||
|
||||
// Operators to access underlying es_message_t
|
||||
const es_message_t* operator->() const { return es_msg_; }
|
||||
const es_message_t& operator*() const { return *es_msg_; }
|
||||
|
||||
std::string ParentProcessName() const;
|
||||
|
||||
private:
|
||||
std::shared_ptr<EndpointSecurityAPI> esapi_;
|
||||
es_message_t* es_msg_;
|
||||
|
||||
mutable std::string pname_;
|
||||
mutable std::string parent_pname_;
|
||||
|
||||
std::string GetProcessName(pid_t pid) const;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
65
Source/santad/EventProviders/EndpointSecurity/Message.mm
Normal file
65
Source/santad/EventProviders/EndpointSecurity/Message.mm
Normal file
@@ -0,0 +1,65 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
#include <libproc.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Message::Message(std::shared_ptr<EndpointSecurityAPI> esapi, const es_message_t *es_msg)
|
||||
: esapi_(esapi) {
|
||||
es_msg_ = esapi_->RetainMessage(es_msg);
|
||||
}
|
||||
|
||||
Message::~Message() {
|
||||
if (es_msg_) {
|
||||
esapi_->ReleaseMessage(es_msg_);
|
||||
}
|
||||
}
|
||||
|
||||
Message::Message(Message &&other) {
|
||||
esapi_ = std::move(other.esapi_);
|
||||
es_msg_ = other.es_msg_;
|
||||
other.es_msg_ = nullptr;
|
||||
}
|
||||
|
||||
Message::Message(const Message &other) {
|
||||
esapi_ = other.esapi_;
|
||||
es_msg_ = other.es_msg_;
|
||||
esapi_->RetainMessage(es_msg_);
|
||||
}
|
||||
|
||||
std::string Message::ParentProcessName() const {
|
||||
if (parent_pname_.length() == 0) {
|
||||
parent_pname_ = GetProcessName(es_msg_->process->ppid);
|
||||
}
|
||||
return parent_pname_;
|
||||
}
|
||||
|
||||
std::string Message::GetProcessName(pid_t pid) const {
|
||||
// Note: proc_name() accesses the `pbi_name` field of `struct proc_bsdinfo`. The size of `pname`
|
||||
// here is meant to match the size of `pbi_name`, and one extra byte ensure zero-terminated.
|
||||
char pname[MAXCOMLEN * 2 + 1] = {};
|
||||
if (proc_name(pid, pname, sizeof(pname)) > 0) {
|
||||
return std::string(pname);
|
||||
} else {
|
||||
return std::string("");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
135
Source/santad/EventProviders/EndpointSecurity/MessageTest.mm
Normal file
135
Source/santad/EventProviders/EndpointSecurity/MessageTest.mm
Normal file
@@ -0,0 +1,135 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libproc.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
bool IsPidInUse(pid_t pid) {
|
||||
char pname[MAXCOMLEN * 2 + 1] = {};
|
||||
errno = 0;
|
||||
if (proc_name(pid, pname, sizeof(pname)) <= 0 && errno == ESRCH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The PID may or may not actually be in use, but assume it is
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to find an unused PID by looking for libproc returning ESRCH errno.
|
||||
// Start searching backwards from PID_MAX to increase likelyhood that the
|
||||
// returned PID will still be unused by the time it's being used.
|
||||
// TODO(mlw): Alternatively, we could inject the `proc_name` function into
|
||||
// the `Message` object to remove the guesswork here.
|
||||
pid_t AttemptToFindUnusedPID() {
|
||||
for (pid_t pid = 99999 /* PID_MAX */; pid > 1; pid--) {
|
||||
if (!IsPidInUse(pid)) {
|
||||
return pid;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@interface MessageTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation MessageTest
|
||||
|
||||
- (void)setUp {
|
||||
}
|
||||
|
||||
- (void)testConstructorsAndDestructors {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
// Constructing a `Message` retains the underlying `es_message_t` and it is
|
||||
// released when the `Message` object is destructed.
|
||||
{ Message m(mockESApi, &esMsg); }
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testCopyConstructor {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
EXPECT_CALL(*mockESApi, ReleaseMessage(testing::_))
|
||||
.Times(2)
|
||||
.After(EXPECT_CALL(*mockESApi, RetainMessage(testing::_))
|
||||
.Times(2)
|
||||
.WillRepeatedly(testing::Return(&esMsg)));
|
||||
|
||||
{
|
||||
Message msg1(mockESApi, &esMsg);
|
||||
Message msg2(msg1);
|
||||
|
||||
// Both messages should now point to the same `es_message_t`
|
||||
XCTAssertEqual(msg1.operator->(), &esMsg);
|
||||
XCTAssertEqual(msg2.operator->(), &esMsg);
|
||||
}
|
||||
|
||||
// Ensure the retain/release mocks were called the expected number of times
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testGetParentProcessName {
|
||||
// Construct a message where the parent pid is ourself
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(getpid(), 0));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
// Search for an *existing* parent process.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
std::string got = msg.ParentProcessName();
|
||||
std::string want = getprogname();
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
// Search for a *non-existent* parent process.
|
||||
{
|
||||
pid_t newPpid = AttemptToFindUnusedPID();
|
||||
proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(newPpid, 34));
|
||||
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
std::string got = msg.ParentProcessName();
|
||||
std::string want = "";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,81 @@
|
||||
/// 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));
|
||||
|
||||
MOCK_METHOD(uint32_t, ExecEnvCount, (const es_event_exec_t *event));
|
||||
MOCK_METHOD(es_string_token_t, ExecEnv, (const es_event_exec_t *event, uint32_t index));
|
||||
|
||||
MOCK_METHOD(uint32_t, ExecFDCount, (const es_event_exec_t *event));
|
||||
MOCK_METHOD(const es_fd_t *, ExecFD, (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
|
||||
40
Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h
Normal file
40
Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h
Normal file
@@ -0,0 +1,40 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#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"
|
||||
#include "Source/santad/Metrics.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
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
execController:(SNTExecutionController *)execController
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:
|
||||
(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache;
|
||||
|
||||
@end
|
||||
157
Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.mm
Normal file
157
Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.mm
Normal file
@@ -0,0 +1,157 @@
|
||||
/// 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"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
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
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
execController:(SNTExecutionController *)execController
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache {
|
||||
self = [super initWithESAPI:std::move(esApi)
|
||||
metrics:std::move(metrics)
|
||||
processor:santa::santad::Processor::kAuthorizer];
|
||||
if (self) {
|
||||
_execController = execController;
|
||||
_compilerController = compilerController;
|
||||
_authResultCache = authResultCache;
|
||||
|
||||
[self establishClientOrDie];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return @"Authorizer";
|
||||
}
|
||||
|
||||
- (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
|
||||
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
|
||||
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];
|
||||
recordEventMetrics(EventDisposition::kDropped);
|
||||
return;
|
||||
}
|
||||
|
||||
[self processMessage:std::move(esMsg)
|
||||
handler:^(const Message &msg) {
|
||||
[self processMessage:msg];
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
}];
|
||||
}
|
||||
|
||||
- (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,304 @@
|
||||
/// 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"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
#import "Source/santad/SNTExecutionController.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
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
|
||||
metrics:nullptr
|
||||
processor:santa::santad::Processor::kAuthorizer];
|
||||
|
||||
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);
|
||||
|
||||
// There is a benign leak of the mock object in this test.
|
||||
// `handleMessage:recordEventMetrics:` will call `processMessage:handler:` in the parent
|
||||
// class. This will dispatch to two blocks and create message copies. The block that
|
||||
// handles `deadline` timeouts will not complete before the test finishes, and the
|
||||
// mock object will think that it has been leaked.
|
||||
::testing::Mock::AllowLeak(mockESApi.get());
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
SNTEndpointSecurityAuthorizer *authClient =
|
||||
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
execController:self.mockExecController
|
||||
compilerController:nil
|
||||
authResultCache:nullptr];
|
||||
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
// Test unhandled event type
|
||||
{
|
||||
// Temporarily change the event type
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
|
||||
XCTAssertThrows([authClient handleMessage:Message(mockESApi, &esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Unhandled event types shouldn't call metrics recorder");
|
||||
}]);
|
||||
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)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
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)]).ignoringNonObjectArgs();
|
||||
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg)])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(nil);
|
||||
|
||||
[mockAuthClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertTrue(OCMVerifyAll(mockAuthClient));
|
||||
}
|
||||
|
||||
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
|
||||
metrics:nullptr
|
||||
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
|
||||
metrics:nullptr
|
||||
execController:self.mockExecController
|
||||
compilerController:mockCompilerController
|
||||
authResultCache:mockAuthCache];
|
||||
id mockAuthClient = OCMPartialMock(authClient);
|
||||
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
XCTAssertThrows([mockAuthClient postAction:(santa_action_t)123 forMessage:msg]);
|
||||
|
||||
std::map<santa_action_t, es_auth_result_t> actions = {
|
||||
{ACTION_RESPOND_ALLOW_COMPILER, ES_AUTH_RESULT_ALLOW},
|
||||
{ACTION_RESPOND_ALLOW, ES_AUTH_RESULT_ALLOW},
|
||||
{ACTION_RESPOND_DENY, ES_AUTH_RESULT_DENY},
|
||||
};
|
||||
|
||||
for (const auto &kv : actions) {
|
||||
OCMExpect([mockAuthClient respondToMessage:msg
|
||||
withAuthResult:kv.second
|
||||
cacheable:kv.second == ES_AUTH_RESULT_ALLOW]);
|
||||
|
||||
[mockAuthClient postAction:kv.first forMessage:msg];
|
||||
}
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
|
||||
|
||||
[mockCompilerController stopMocking];
|
||||
[mockAuthClient stopMocking];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,17 +12,9 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/Santa.pbobjc.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol SNTLogOutput<NSObject>
|
||||
|
||||
- (void)logEvent:(SNTPBSantaMessage *)event;
|
||||
- (void)flush;
|
||||
#include "Source/santad/EventProviders/SNTEndpointSecurityClientBase.h"
|
||||
|
||||
/// This should be treated as an Abstract Base Class and not directly instantiated
|
||||
@interface SNTEndpointSecurityClient : NSObject <SNTEndpointSecurityClientBase>
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
264
Source/santad/EventProviders/SNTEndpointSecurityClient.mm
Normal file
264
Source/santad/EventProviders/SNTEndpointSecurityClient.mm
Normal file
@@ -0,0 +1,264 @@
|
||||
/// 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/EndpointSecurity.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"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::Metrics;
|
||||
using santa::santad::Processor;
|
||||
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;
|
||||
std::shared_ptr<Metrics> _metrics;
|
||||
Client _esClient;
|
||||
mach_timebase_info_data_t _timebase;
|
||||
dispatch_queue_t _authQueue;
|
||||
dispatch_queue_t _notifyQueue;
|
||||
Processor _processor;
|
||||
}
|
||||
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<Metrics>)metrics
|
||||
processor:(Processor)processor {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_esApi = std::move(esApi);
|
||||
_metrics = std::move(metrics);
|
||||
_deadlineMarginMS = 5000;
|
||||
_processor = processor;
|
||||
|
||||
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
|
||||
recordEventMetrics:(void (^)(EventDisposition disposition))recordEventMetrics {
|
||||
// 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) {
|
||||
int64_t processingStart = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
es_event_type_t eventType = esMsg->event_type;
|
||||
if ([self shouldHandleMessage:esMsg
|
||||
ignoringOtherESClients:[[SNTConfigurator configurator]
|
||||
ignoreOtherEndpointSecurityClients]]) {
|
||||
[self handleMessage:std::move(esMsg)
|
||||
recordEventMetrics:^(EventDisposition disposition) {
|
||||
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
self->_metrics->SetEventMetrics(self->_processor, eventType, disposition,
|
||||
processingEnd - processingStart);
|
||||
}];
|
||||
} else {
|
||||
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
self->_metrics->SetEventMetrics(self->_processor, eventType, EventDisposition::kDropped,
|
||||
processingEnd - processingStart);
|
||||
}
|
||||
});
|
||||
|
||||
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 (%@)", self);
|
||||
}
|
||||
|
||||
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
|
||||
77
Source/santad/EventProviders/SNTEndpointSecurityClientBase.h
Normal file
77
Source/santad/EventProviders/SNTEndpointSecurityClientBase.h
Normal file
@@ -0,0 +1,77 @@
|
||||
/// 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"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
@protocol SNTEndpointSecurityClientBase
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
processor:(santa::santad::Processor)processor;
|
||||
|
||||
/// @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
|
||||
437
Source/santad/EventProviders/SNTEndpointSecurityClientTest.mm
Normal file
437
Source/santad/EventProviders/SNTEndpointSecurityClientTest.mm
Normal file
@@ -0,0 +1,437 @@
|
||||
/// 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"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::Processor;
|
||||
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
|
||||
recordEventMetrics:(void (^)(santa::santad::EventDisposition disposition))recordEventMetrics;
|
||||
- (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
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// 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
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
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
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
{ XCTAssertThrows([client handleMessage:Message(mockESApi, &esMsg) recordEventMetrics:nil]); }
|
||||
|
||||
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
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
{
|
||||
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
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
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
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// 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
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
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
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
// 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
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
{
|
||||
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
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
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
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
{
|
||||
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
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
|
||||
{
|
||||
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
|
||||
metrics:nullptr
|
||||
processor:Processor::kUnknown];
|
||||
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,17 @@
|
||||
/// 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"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -26,16 +31,19 @@ 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
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
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,41 @@
|
||||
/// 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"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
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 +56,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 +75,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 +85,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 +124,108 @@ 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
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
logger:(std::shared_ptr<Logger>)logger
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache {
|
||||
self = [super initWithESAPI:std::move(esApi)
|
||||
metrics:std::move(metrics)
|
||||
processor:santa::santad::Processor::kDeviceManager];
|
||||
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);
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return @"Device Manager";
|
||||
}
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
|
||||
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];
|
||||
recordEventMetrics(EventDisposition::kDropped);
|
||||
return;
|
||||
}
|
||||
|
||||
if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_UNMOUNT) {
|
||||
self->_authResultCache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
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];
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
}];
|
||||
}
|
||||
|
||||
- (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 +237,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 +251,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 +264,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 +282,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 +297,6 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
free(argv);
|
||||
}
|
||||
|
||||
// handleESMessage handles an ES message synchronously. This will block all incoming ES events
|
||||
// until either we serve a response or we hit the auth deadline. Prefer [SNTDeviceManager
|
||||
// handleESMessageWithTimeout]
|
||||
// TODO(tnek): generalize this timeout handling logic so that EndpointSecurityManager can use it
|
||||
// too.
|
||||
- (void)handleESMessageWithTimeout:(const es_message_t *)m
|
||||
withClient:(es_client_t *)c
|
||||
timeout:(dispatch_time_t)timeout API_AVAILABLE(macos(10.15)) {
|
||||
// ES will kill our whole client if we don't meet the es_message auth deadline, so we try to
|
||||
// gracefully handle it with a deny-by-default in the worst-case before it can do that.
|
||||
// This isn't an issue for notify events, so we're in no rush for those.
|
||||
es_message_t *mc = es_copy_message(m);
|
||||
|
||||
dispatch_semaphore_t processingSema = dispatch_semaphore_create(0);
|
||||
// Add 1 to the processing semaphore. We're not creating it with a starting
|
||||
// value of 1 because that requires that the semaphore is not deallocated
|
||||
// until its value matches the starting value, which we don't need.
|
||||
dispatch_semaphore_signal(processingSema);
|
||||
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
|
||||
|
||||
if (mc->action_type == ES_ACTION_TYPE_AUTH) {
|
||||
dispatch_after(timeout, self.esAuthQueue, ^(void) {
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Handler already responded, nothing to do.
|
||||
return;
|
||||
}
|
||||
LOGE(@"SNTDeviceManager: deadline reached: deny pid=%d ret=%d",
|
||||
audit_token_to_pid(mc->process->audit_token),
|
||||
es_respond_auth_result(c, mc, ES_AUTH_RESULT_DENY, false));
|
||||
dispatch_semaphore_signal(deadlineExpiredSema);
|
||||
});
|
||||
}
|
||||
|
||||
dispatch_async(self.esAuthQueue, ^{
|
||||
[self handleESMessage:mc withClient:c];
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Deadline expired, wait for deadline block to finish.
|
||||
dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER);
|
||||
}
|
||||
es_free_message(mc);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)handleESMessage:(const es_message_t *)m
|
||||
withClient:(es_client_t *)c API_AVAILABLE(macos(10.15)) {
|
||||
switch (m->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_REMOUNT: {
|
||||
[[fallthrough]];
|
||||
}
|
||||
case ES_EVENT_TYPE_AUTH_MOUNT: {
|
||||
[self handleAuthMount:m withClient:c];
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
LOGE(@"SNTDeviceManager: unexpected event type: %d", m->event_type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,369 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <DiskArbitration/DiskArbitration.h>
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#import <bsm/libbsm.h>
|
||||
#import <dispatch/dispatch.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/mount.h>
|
||||
#include <cstddef>
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
class MockAuthResultCache : public AuthResultCache {
|
||||
public:
|
||||
using AuthResultCache::AuthResultCache;
|
||||
|
||||
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManager (Testing)
|
||||
- (void)logDiskAppeared:(NSDictionary *)props;
|
||||
@end
|
||||
|
||||
@interface SNTEndpointSecurityDeviceManagerTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property MockDiskArbitration *mockDA;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityDeviceManagerTest
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator eventLogType]).andReturn(-1);
|
||||
|
||||
self.mockDA = [MockDiskArbitration mockDiskArbitration];
|
||||
[self.mockDA reset];
|
||||
|
||||
fclose(stdout);
|
||||
}
|
||||
|
||||
- (void)triggerTestMountEvent:(es_event_type_t)eventType
|
||||
diskInfoOverrides:(NSDictionary *)diskInfo
|
||||
expectedAuthResult:(es_auth_result_t)expectedAuthResult
|
||||
deviceManagerSetup:(void (^)(SNTEndpointSecurityDeviceManager *))setupDMCallback {
|
||||
struct statfs fs = {0};
|
||||
NSString *test_mntfromname = @"/dev/disk2s1";
|
||||
NSString *test_mntonname = @"/Volumes/KATE'S 4G";
|
||||
|
||||
strncpy(fs.f_mntfromname, [test_mntfromname UTF8String], sizeof(fs.f_mntfromname));
|
||||
strncpy(fs.f_mntonname, [test_mntonname UTF8String], sizeof(fs.f_mntonname));
|
||||
|
||||
MockDADisk *disk = [[MockDADisk alloc] init];
|
||||
disk.diskDescription = @{
|
||||
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey : @"USB",
|
||||
(__bridge NSString *)kDADiskDescriptionMediaRemovableKey : @YES,
|
||||
@"DAVolumeMountable" : @YES,
|
||||
@"DAVolumePath" : test_mntonname,
|
||||
@"DADeviceModel" : @"Some device model",
|
||||
@"DADevicePath" : test_mntonname,
|
||||
@"DADeviceVendor" : @"Some vendor",
|
||||
@"DAAppearanceTime" : @0,
|
||||
@"DAMediaBSDName" : test_mntfromname,
|
||||
};
|
||||
|
||||
if (diskInfo != nil) {
|
||||
NSMutableDictionary *mergedDiskDescription = [disk.diskDescription mutableCopy];
|
||||
for (NSString *key in diskInfo) {
|
||||
mergedDiskDescription[key] = diskInfo[key];
|
||||
}
|
||||
disk.diskDescription = (NSDictionary *)mergedDiskDescription;
|
||||
}
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
|
||||
SNTEndpointSecurityDeviceManager *deviceManager =
|
||||
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:nullptr
|
||||
authResultCache:nullptr];
|
||||
|
||||
setupDMCallback(deviceManager);
|
||||
|
||||
// Stub the log method since a mock `Logger` object isn't used.
|
||||
id partialDeviceManager = OCMPartialMock(deviceManager);
|
||||
OCMStub([partialDeviceManager logDiskAppeared:OCMOCK_ANY]);
|
||||
|
||||
[self.mockDA insert:disk bsdName:test_mntfromname];
|
||||
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(eventType, &proc, ActionType::Auth, 6000);
|
||||
// Need a pointer to esMsg to capture in blocks below.
|
||||
es_message_t *heapESMsg = &esMsg;
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
__block int retainCount = 0;
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
EXPECT_CALL(*mockESApi, ReleaseMessage).WillRepeatedly(^{
|
||||
if (retainCount == 0) {
|
||||
XCTFail(@"Under retain!");
|
||||
}
|
||||
retainCount--;
|
||||
if (retainCount == 0) {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}
|
||||
});
|
||||
EXPECT_CALL(*mockESApi, RetainMessage).WillRepeatedly(^{
|
||||
retainCount++;
|
||||
return heapESMsg;
|
||||
});
|
||||
|
||||
if (eventType == ES_EVENT_TYPE_AUTH_MOUNT) {
|
||||
esMsg.event.mount.statfs = &fs;
|
||||
} else if (eventType == ES_EVENT_TYPE_AUTH_REMOUNT) {
|
||||
esMsg.event.remount.statfs = &fs;
|
||||
} else {
|
||||
// Programming error. Fail the test.
|
||||
XCTFail(@"Unhandled event type in test: %d", eventType);
|
||||
}
|
||||
|
||||
XCTestExpectation *mountExpectation =
|
||||
[self expectationWithDescription:@"Wait for response from ES"];
|
||||
|
||||
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, expectedAuthResult, false))
|
||||
.WillOnce(testing::InvokeWithoutArgs(^bool {
|
||||
[mountExpectation fulfill];
|
||||
return true;
|
||||
}));
|
||||
|
||||
[deviceManager handleMessage:Message(mockESApi, &esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, deviceManager.blockUSBMount ? EventDisposition::kProcessed
|
||||
: EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
[self waitForExpectations:@[ mountExpectation ] timeout:60.0];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertSemaTrue(sema, 5, "Failed waiting for message to be processed...");
|
||||
|
||||
[partialDeviceManager stopMocking];
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testUSBBlockDisabled {
|
||||
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil
|
||||
expectedAuthResult:ES_AUTH_RESULT_ALLOW
|
||||
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
|
||||
dm.blockUSBMount = NO;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)testRemount {
|
||||
NSArray *wantRemountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:
|
||||
@"Wait for SNTEndpointSecurityDeviceManager's blockCallback to trigger"];
|
||||
|
||||
__block NSString *gotmntonname, *gotmntfromname;
|
||||
__block NSArray<NSString *> *gotRemountedArgs;
|
||||
|
||||
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil
|
||||
expectedAuthResult:ES_AUTH_RESULT_DENY
|
||||
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
|
||||
dm.blockUSBMount = YES;
|
||||
dm.remountArgs = wantRemountArgs;
|
||||
|
||||
dm.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
gotRemountedArgs = event.remountArgs;
|
||||
gotmntonname = event.mntonname;
|
||||
gotmntfromname = event.mntfromname;
|
||||
[expectation fulfill];
|
||||
};
|
||||
}];
|
||||
|
||||
XCTAssertEqual(self.mockDA.wasRemounted, YES);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertEqualObjects(gotRemountedArgs, wantRemountArgs);
|
||||
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
|
||||
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
|
||||
}
|
||||
|
||||
- (void)testBlockNoRemount {
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:
|
||||
@"Wait for SNTEndpointSecurityDeviceManager's blockCallback to trigger"];
|
||||
|
||||
__block NSString *gotmntonname, *gotmntfromname;
|
||||
__block NSArray<NSString *> *gotRemountedArgs;
|
||||
|
||||
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil
|
||||
expectedAuthResult:ES_AUTH_RESULT_DENY
|
||||
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
|
||||
dm.blockUSBMount = YES;
|
||||
|
||||
dm.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
gotRemountedArgs = event.remountArgs;
|
||||
gotmntonname = event.mntonname;
|
||||
gotmntfromname = event.mntfromname;
|
||||
[expectation fulfill];
|
||||
};
|
||||
}];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertNil(gotRemountedArgs);
|
||||
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
|
||||
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
|
||||
}
|
||||
|
||||
- (void)testEnsureRemountsCannotChangePerms {
|
||||
NSArray *wantRemountArgs = @[ @"noexec", @"rdonly" ];
|
||||
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:
|
||||
@"Wait for SNTEndpointSecurityDeviceManager's blockCallback to trigger"];
|
||||
|
||||
__block NSString *gotmntonname, *gotmntfromname;
|
||||
__block NSArray<NSString *> *gotRemountedArgs;
|
||||
|
||||
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:nil
|
||||
expectedAuthResult:ES_AUTH_RESULT_DENY
|
||||
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
|
||||
dm.blockUSBMount = YES;
|
||||
dm.remountArgs = wantRemountArgs;
|
||||
|
||||
dm.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
gotRemountedArgs = event.remountArgs;
|
||||
gotmntonname = event.mntonname;
|
||||
gotmntfromname = event.mntfromname;
|
||||
[expectation fulfill];
|
||||
};
|
||||
}];
|
||||
|
||||
XCTAssertEqual(self.mockDA.wasRemounted, YES);
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:10.0];
|
||||
|
||||
XCTAssertEqualObjects(gotRemountedArgs, wantRemountArgs);
|
||||
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
|
||||
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
|
||||
}
|
||||
|
||||
- (void)testEnsureDMGsDoNotPrompt {
|
||||
NSArray *wantRemountArgs = @[ @"noexec", @"rdonly" ];
|
||||
NSDictionary *diskInfo = @{
|
||||
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey : @"Virtual Interface",
|
||||
(__bridge NSString *)kDADiskDescriptionDeviceModelKey : @"Disk Image",
|
||||
(__bridge NSString *)kDADiskDescriptionMediaNameKey : @"disk image",
|
||||
};
|
||||
|
||||
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
|
||||
diskInfoOverrides:diskInfo
|
||||
expectedAuthResult:ES_AUTH_RESULT_ALLOW
|
||||
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
|
||||
dm.blockUSBMount = YES;
|
||||
dm.remountArgs = wantRemountArgs;
|
||||
|
||||
dm.deviceBlockCallback = ^(SNTDeviceEvent *event) {
|
||||
XCTFail(@"Should not be called");
|
||||
};
|
||||
}];
|
||||
|
||||
XCTAssertEqual(self.mockDA.wasRemounted, NO);
|
||||
}
|
||||
|
||||
- (void)testNotifyUnmountFlushesCache {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_UNMOUNT, &proc);
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
EXPECT_CALL(*mockAuthCache, FlushCache);
|
||||
|
||||
SNTEndpointSecurityDeviceManager *deviceManager =
|
||||
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:nullptr
|
||||
authResultCache:mockAuthCache];
|
||||
|
||||
deviceManager.blockUSBMount = YES;
|
||||
|
||||
[deviceManager handleMessage:Message(mockESApi, &esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
|
||||
}
|
||||
|
||||
- (void)testEnable {
|
||||
// Ensure the client subscribes to expected event types
|
||||
std::set<es_event_type_t> expectedEventSubs{
|
||||
ES_EVENT_TYPE_AUTH_MOUNT,
|
||||
ES_EVENT_TYPE_AUTH_REMOUNT,
|
||||
ES_EVENT_TYPE_NOTIFY_UNMOUNT,
|
||||
};
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
id deviceClient = [[SNTEndpointSecurityDeviceManager alloc]
|
||||
initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:santa::santad::Processor::kDeviceManager];
|
||||
|
||||
EXPECT_CALL(*mockESApi, ClearCache(testing::_))
|
||||
.After(EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs))
|
||||
.WillOnce(testing::Return(true)))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
[deviceClient enable];
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,34 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
// Protocol that all subclasses of `SNTEndpointSecurityClient` should adhere to.
|
||||
@protocol SNTEndpointSecurityEventHandler <NSObject>
|
||||
|
||||
// Called Synchronously and serially for each message provided by the
|
||||
// EndpointSecurity framework.
|
||||
- (void)handleMessage:(santa::santad::event_providers::endpoint_security::Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(santa::santad::EventDisposition))recordEventMetrics;
|
||||
|
||||
// Called after Santa has finished initializing itself.
|
||||
// This is an optimal place to subscribe to ES events
|
||||
- (void)enable;
|
||||
|
||||
@end
|
||||
@@ -1,37 +0,0 @@
|
||||
/// Copyright 2019 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
#include "Source/santad/EventProviders/SNTEventProvider.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
@interface SNTEndpointSecurityManager : NSObject <SNTEventProvider>
|
||||
- (santa_vnode_id_t)vnodeIDForFile:(es_file_t *)file;
|
||||
|
||||
- (BOOL)isCompilerPID:(pid_t)pid;
|
||||
- (void)setIsCompilerPID:(pid_t)pid;
|
||||
- (void)setNotCompilerPID:(pid_t)pid;
|
||||
|
||||
// Returns YES if the path was truncated.
|
||||
// The populated buffer will be NUL terminated.
|
||||
+ (BOOL)populateBufferFromESFile:(es_file_t *)file buffer:(char *)buffer size:(size_t)size;
|
||||
|
||||
@property(nonatomic, copy) void (^decisionCallback)(santa_message_t);
|
||||
@property(nonatomic, copy) void (^logCallback)(santa_message_t);
|
||||
@property(readonly, nonatomic) es_client_t *client;
|
||||
|
||||
@end
|
||||
@@ -1,645 +0,0 @@
|
||||
/// Copyright 2019 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityManager.h"
|
||||
|
||||
#include "Source/common/SNTPrefixTree.h"
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SantaCache.h"
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
#include <libproc.h>
|
||||
#include <atomic>
|
||||
|
||||
// Gleaned from https://opensource.apple.com/source/xnu/xnu-4903.241.1/bsd/sys/proc_internal.h
|
||||
static const pid_t PID_MAX = 99999;
|
||||
|
||||
@interface SNTEndpointSecurityManager () {
|
||||
std::atomic<bool> _compilerPIDs[PID_MAX];
|
||||
}
|
||||
|
||||
@property(nonatomic) SNTPrefixTree *prefixTree;
|
||||
@property(nonatomic, readonly) dispatch_queue_t esAuthQueue;
|
||||
@property(nonatomic, readonly) dispatch_queue_t esNotifyQueue;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityManager
|
||||
|
||||
- (instancetype)init API_AVAILABLE(macos(10.15)) {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
// To avoid nil deref from es_events arriving before listenForDecisionRequests or
|
||||
// listenForLogRequests in the MockEndpointSecurity testing util.
|
||||
_decisionCallback = ^(santa_message_t) {};
|
||||
_logCallback = ^(santa_message_t) {};
|
||||
[self establishClient];
|
||||
[self muteSelf];
|
||||
_prefixTree = new SNTPrefixTree();
|
||||
_esAuthQueue =
|
||||
dispatch_queue_create("com.google.santa.daemon.es_auth", DISPATCH_QUEUE_CONCURRENT);
|
||||
dispatch_set_target_queue(_esAuthQueue,
|
||||
dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
_esNotifyQueue =
|
||||
dispatch_queue_create("com.google.santa.daemon.es_notify", DISPATCH_QUEUE_CONCURRENT);
|
||||
dispatch_set_target_queue(_esNotifyQueue, dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0));
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc API_AVAILABLE(macos(10.15)) {
|
||||
if (_client) {
|
||||
es_unsubscribe_all(_client);
|
||||
es_delete_client(_client);
|
||||
}
|
||||
if (_prefixTree) delete _prefixTree;
|
||||
}
|
||||
|
||||
- (void)muteSelf {
|
||||
audit_token_t myAuditToken;
|
||||
mach_msg_type_number_t count = TASK_AUDIT_TOKEN_COUNT;
|
||||
if (task_info(mach_task_self(), TASK_AUDIT_TOKEN, (task_info_t)&myAuditToken, &count) ==
|
||||
KERN_SUCCESS) {
|
||||
if (es_mute_process(self.client, &myAuditToken) == ES_RETURN_SUCCESS) {
|
||||
return;
|
||||
} else {
|
||||
LOGE(@"Failed to mute this client's process, its events will not be muted.");
|
||||
}
|
||||
} else {
|
||||
LOGE(@"Failed to fetch this client's audit token. Its events will not be muted.");
|
||||
}
|
||||
|
||||
// If we get here, Santa was unable to mute itself. Assume transitory and bail.
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
- (void)establishClient API_AVAILABLE(macos(10.15)) {
|
||||
while (!self.client) {
|
||||
SNTConfigurator *config = [SNTConfigurator configurator];
|
||||
|
||||
es_client_t *client = NULL;
|
||||
es_new_client_result_t ret = es_new_client(&client, ^(es_client_t *c, const es_message_t *m) {
|
||||
pid_t pid = audit_token_to_pid(m->process->audit_token);
|
||||
int pidversion = audit_token_to_pidversion(m->process->audit_token);
|
||||
|
||||
// If enabled, skip any action generated from another endpoint security client.
|
||||
if (m->process->is_es_client && config.ignoreOtherEndpointSecurityClients) {
|
||||
if (m->action_type == ES_ACTION_TYPE_AUTH) {
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform the following checks on this serial queue.
|
||||
// Some checks are simple filters that avoid copying m.
|
||||
// However, the bulk of the work done here is to support transitive whitelisting.
|
||||
switch (m->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC: {
|
||||
// Deny results are currently logged when ES_EVENT_TYPE_AUTH_EXEC posts a deny.
|
||||
// TODO(bur/rah): For ES log denies from NOTIFY messages instead of AUTH.
|
||||
if (m->action.notify.result.auth == ES_AUTH_RESULT_DENY) return;
|
||||
|
||||
// Continue log this event
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: {
|
||||
// Ignore unmodified files
|
||||
if (!m->event.close.modified) return;
|
||||
|
||||
// Remove from decision cache in case this is invalidating a cached binary.
|
||||
[self removeCacheEntryForVnodeID:[self vnodeIDForFile:m->event.close.target]];
|
||||
|
||||
// Create a transitive rule if the file was modified by a running compiler
|
||||
if ([self isCompilerPID:pid]) {
|
||||
santa_message_t sm = {};
|
||||
BOOL truncated =
|
||||
[SNTEndpointSecurityManager populateBufferFromESFile:m->event.close.target
|
||||
buffer:sm.path
|
||||
size:sizeof(sm.path)];
|
||||
if (truncated) {
|
||||
LOGE(@"CLOSE: error creating transitive rule, the path is truncated: path=%s pid=%d",
|
||||
sm.path, pid);
|
||||
break;
|
||||
}
|
||||
if ([@(sm.path) hasPrefix:@"/dev/"]) {
|
||||
break;
|
||||
}
|
||||
sm.action = ACTION_NOTIFY_WHITELIST;
|
||||
sm.pid = pid;
|
||||
sm.pidversion = pidversion;
|
||||
LOGI(@"CLOSE: creating a transitive rule: path=%s pid=%d", sm.path, sm.pid);
|
||||
self.decisionCallback(sm);
|
||||
}
|
||||
|
||||
// Continue log this event
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: {
|
||||
// Create a transitive rule if the file was renamed by a running compiler
|
||||
if ([self isCompilerPID:pid]) {
|
||||
santa_message_t sm = {};
|
||||
BOOL truncated = [self populateRenamedNewPathFromESMessage:m->event.rename
|
||||
buffer:sm.path
|
||||
size:sizeof(sm.path)];
|
||||
if (truncated) {
|
||||
LOGE(@"RENAME: error creating transitive rule, the path is truncated: path=%s pid=%d",
|
||||
sm.path, pid);
|
||||
break;
|
||||
}
|
||||
if ([@(sm.path) hasPrefix:@"/dev/"]) {
|
||||
break;
|
||||
}
|
||||
sm.action = ACTION_NOTIFY_WHITELIST;
|
||||
sm.pid = pid;
|
||||
sm.pidversion = pidversion;
|
||||
LOGI(@"RENAME: creating a transitive rule: path=%s pid=%d", sm.path, sm.pid);
|
||||
self.decisionCallback(sm);
|
||||
}
|
||||
|
||||
// Continue log this event
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT: {
|
||||
// Update the set of running compiler PIDs
|
||||
[self setNotCompilerPID:pid];
|
||||
|
||||
// Skip the standard pipeline and just log.
|
||||
if (![config enableForkAndExitLogging]) return;
|
||||
santa_message_t sm = {};
|
||||
sm.action = ACTION_NOTIFY_EXIT;
|
||||
sm.pid = pid;
|
||||
sm.pidversion = pidversion;
|
||||
sm.ppid = m->process->original_ppid;
|
||||
audit_token_t at = m->process->audit_token;
|
||||
sm.uid = audit_token_to_ruid(at);
|
||||
sm.gid = audit_token_to_rgid(at);
|
||||
dispatch_async(self.esNotifyQueue, ^{
|
||||
self.logCallback(sm);
|
||||
});
|
||||
return;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_UNMOUNT: {
|
||||
// Flush the non-root cache - the root disk cannot be unmounted
|
||||
// so it isn't necessary to flush its cache.
|
||||
//
|
||||
// Flushing the cache calls back into ES. We need to perform this off the handler thread
|
||||
// otherwise we could potentially deadlock.
|
||||
dispatch_async(self.esAuthQueue, ^() {
|
||||
[self flushCacheNonRootOnly:YES];
|
||||
});
|
||||
|
||||
// Skip all other processing
|
||||
return;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK: {
|
||||
// Skip the standard pipeline and just log.
|
||||
if (![config enableForkAndExitLogging]) return;
|
||||
santa_message_t sm = {};
|
||||
sm.action = ACTION_NOTIFY_FORK;
|
||||
sm.ppid = m->event.fork.child->original_ppid;
|
||||
audit_token_t at = m->event.fork.child->audit_token;
|
||||
sm.pid = audit_token_to_pid(at);
|
||||
sm.pidversion = audit_token_to_pidversion(at);
|
||||
sm.uid = audit_token_to_ruid(at);
|
||||
sm.gid = audit_token_to_rgid(at);
|
||||
dispatch_async(self.esNotifyQueue, ^{
|
||||
self.logCallback(sm);
|
||||
});
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (m->action_type) {
|
||||
case ES_ACTION_TYPE_AUTH: {
|
||||
// Copy the message
|
||||
es_message_t *mc = es_copy_message(m);
|
||||
|
||||
dispatch_semaphore_t processingSema = dispatch_semaphore_create(0);
|
||||
// Add 1 to the processing semaphore. We're not creating it with a starting
|
||||
// value of 1 because that requires that the semaphore is not deallocated
|
||||
// until its value matches the starting value, which we don't need.
|
||||
dispatch_semaphore_signal(processingSema);
|
||||
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
|
||||
|
||||
// Create a timer to deny the execution 5 seconds before the deadline,
|
||||
// if a response hasn't already been sent. This block will still be enqueued if
|
||||
// the the deadline - 5 secs is < DISPATCH_TIME_NOW.
|
||||
// As of 10.15.5, a typical deadline is 60 seconds.
|
||||
dispatch_after(dispatch_time(m->deadline, NSEC_PER_SEC * -5), self.esAuthQueue, ^(void) {
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Handler has already responded, nothing to do.
|
||||
return;
|
||||
}
|
||||
LOGE(@"SNTEndpointSecurityManager: deadline reached: deny pid=%d ret=%d", pid,
|
||||
es_respond_auth_result(self.client, mc, ES_AUTH_RESULT_DENY, false));
|
||||
dispatch_semaphore_signal(deadlineExpiredSema);
|
||||
});
|
||||
|
||||
// Dispatch off to the handler and return control to ES.
|
||||
dispatch_async(self.esAuthQueue, ^{
|
||||
[self messageHandler:mc];
|
||||
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
|
||||
// Deadline expired, wait for deadline block to finish.
|
||||
dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER);
|
||||
}
|
||||
es_free_message(mc);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ES_ACTION_TYPE_NOTIFY: {
|
||||
// Copy the message and return control back to ES
|
||||
es_message_t *mc = es_copy_message(m);
|
||||
dispatch_async(self.esNotifyQueue, ^{
|
||||
[self messageHandler:mc];
|
||||
es_free_message(mc);
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
switch (ret) {
|
||||
case ES_NEW_CLIENT_RESULT_SUCCESS:
|
||||
LOGI(@"Connected to EndpointSecurity");
|
||||
_client = client;
|
||||
return;
|
||||
case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED:
|
||||
LOGE(@"Unable to create EndpointSecurity client, not full-disk access permitted");
|
||||
LOGE(@"Sleeping for 30s before restarting.");
|
||||
sleep(30);
|
||||
exit(ret);
|
||||
default:
|
||||
LOGE(@"Unable to create es client: %d. Sleeping for a minute.", ret);
|
||||
sleep(60);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)respondFromCache:(es_message_t *)m API_AVAILABLE(macos(10.15)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)messageHandler:(es_message_t *)m API_AVAILABLE(macos(10.15)) {
|
||||
santa_message_t sm = {};
|
||||
sm.es_message = (void *)m;
|
||||
|
||||
es_process_t *targetProcess = NULL;
|
||||
es_file_t *targetFile = NULL;
|
||||
void (^callback)(santa_message_t);
|
||||
|
||||
switch (m->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_EXEC: {
|
||||
if ([self respondFromCache:m]) {
|
||||
return;
|
||||
}
|
||||
|
||||
sm.action = ACTION_REQUEST_BINARY;
|
||||
targetFile = m->event.exec.target->executable;
|
||||
targetProcess = m->event.exec.target;
|
||||
callback = self.decisionCallback;
|
||||
|
||||
[SNTEndpointSecurityManager populateBufferFromESFile:m->process->tty
|
||||
buffer:sm.ttypath
|
||||
size:sizeof(sm.ttypath)];
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC: {
|
||||
sm.action = ACTION_NOTIFY_EXEC;
|
||||
targetFile = m->event.exec.target->executable;
|
||||
targetProcess = m->event.exec.target;
|
||||
|
||||
// TODO(rah): Profile this, it might need to be improved.
|
||||
uint32_t argCount = es_exec_arg_count(&(m->event.exec));
|
||||
NSMutableArray *args = [NSMutableArray arrayWithCapacity:argCount];
|
||||
for (int i = 0; i < argCount; ++i) {
|
||||
es_string_token_t arg = es_exec_arg(&(m->event.exec), i);
|
||||
NSString *argStr = [[NSString alloc] initWithBytes:arg.data
|
||||
length:arg.length
|
||||
encoding:NSUTF8StringEncoding];
|
||||
if (argStr.length) [args addObject:argStr];
|
||||
}
|
||||
sm.args_array = (void *)CFBridgingRetain(args);
|
||||
callback = self.logCallback;
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_AUTH_UNLINK: {
|
||||
es_string_token_t pathToken = m->event.unlink.target->path;
|
||||
NSString *path = [[NSString alloc] initWithBytes:pathToken.data
|
||||
length:pathToken.length
|
||||
encoding:NSUTF8StringEncoding];
|
||||
if ([self isDatabasePath:path]) {
|
||||
LOGW(@"Preventing attempt to delete Santa databases!");
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true);
|
||||
return;
|
||||
}
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
|
||||
return;
|
||||
}
|
||||
case ES_EVENT_TYPE_AUTH_RENAME: {
|
||||
es_string_token_t pathToken = m->event.rename.source->path;
|
||||
NSString *path = [[NSString alloc] initWithBytes:pathToken.data
|
||||
length:pathToken.length
|
||||
encoding:NSUTF8StringEncoding];
|
||||
|
||||
if ([self isDatabasePath:path]) {
|
||||
LOGW(@"Preventing attempt to rename Santa databases!");
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true);
|
||||
return;
|
||||
}
|
||||
if (m->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
|
||||
es_string_token_t destToken = m->event.rename.destination.existing_file->path;
|
||||
NSString *destPath = [[NSString alloc] initWithBytes:destToken.data
|
||||
length:destToken.length
|
||||
encoding:NSUTF8StringEncoding];
|
||||
if ([self isDatabasePath:destPath]) {
|
||||
LOGW(@"Preventing attempt to overwrite Santa databases!");
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
|
||||
return;
|
||||
}
|
||||
case ES_EVENT_TYPE_AUTH_KEXTLOAD: {
|
||||
es_string_token_t identifier = m->event.kextload.identifier;
|
||||
NSString *ident = [[NSString alloc] initWithBytes:identifier.data
|
||||
length:identifier.length
|
||||
encoding:NSUTF8StringEncoding];
|
||||
if ([ident isEqualToString:@"com.google.santa-driver"]) {
|
||||
LOGW(@"Preventing attempt to load Santa kext!");
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true);
|
||||
return;
|
||||
}
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
|
||||
return;
|
||||
}
|
||||
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: {
|
||||
sm.action = ACTION_NOTIFY_WRITE;
|
||||
targetFile = m->event.close.target;
|
||||
targetProcess = m->process;
|
||||
callback = self.logCallback;
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK: {
|
||||
sm.action = ACTION_NOTIFY_DELETE;
|
||||
targetFile = m->event.unlink.target;
|
||||
targetProcess = m->process;
|
||||
callback = self.logCallback;
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK: {
|
||||
sm.action = ACTION_NOTIFY_LINK;
|
||||
targetFile = m->event.link.source;
|
||||
targetProcess = m->process;
|
||||
NSString *p = @(m->event.link.target_dir->path.data);
|
||||
p = [p stringByAppendingPathComponent:@(m->event.link.target_filename.data)];
|
||||
[SNTEndpointSecurityManager populateBufferFromString:p.UTF8String
|
||||
buffer:sm.newpath
|
||||
size:sizeof(sm.newpath)];
|
||||
callback = self.logCallback;
|
||||
break;
|
||||
}
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: {
|
||||
sm.action = ACTION_NOTIFY_RENAME;
|
||||
targetFile = m->event.rename.source;
|
||||
targetProcess = m->process;
|
||||
[self populateRenamedNewPathFromESMessage:m->event.rename
|
||||
buffer:sm.newpath
|
||||
size:sizeof(sm.newpath)];
|
||||
callback = self.logCallback;
|
||||
break;
|
||||
}
|
||||
default: LOGE(@"Unknown es message: %d", m->event_type); return;
|
||||
}
|
||||
|
||||
// Deny auth exec events if the path doesn't fit in the santa message.
|
||||
// TODO(bur/rah): Add support for larger paths.
|
||||
if ([SNTEndpointSecurityManager populateBufferFromESFile:targetFile
|
||||
buffer:sm.path
|
||||
size:sizeof(sm.path)] &&
|
||||
m->event_type == ES_EVENT_TYPE_AUTH_EXEC) {
|
||||
LOGE(@"path is truncated, deny: %s", sm.path);
|
||||
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter file op events matching the prefix tree.
|
||||
if (!(m->event_type == ES_EVENT_TYPE_AUTH_EXEC || m->event_type == ES_EVENT_TYPE_NOTIFY_EXEC) &&
|
||||
self.prefixTree->HasPrefix(sm.path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sm.vnode_id.fsid = targetFile->stat.st_dev;
|
||||
sm.vnode_id.fileid = targetFile->stat.st_ino;
|
||||
sm.uid = audit_token_to_ruid(targetProcess->audit_token);
|
||||
sm.gid = audit_token_to_rgid(targetProcess->audit_token);
|
||||
sm.pid = audit_token_to_pid(targetProcess->audit_token);
|
||||
sm.pidversion = audit_token_to_pidversion(targetProcess->audit_token);
|
||||
sm.ppid = targetProcess->original_ppid;
|
||||
proc_name((m->event_type == ES_EVENT_TYPE_AUTH_EXEC) ? sm.ppid : sm.pid, sm.pname, 1024);
|
||||
callback(sm);
|
||||
if (sm.args_array) {
|
||||
CFBridgingRelease(sm.args_array);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)listenForDecisionRequests:(void (^)(santa_message_t))callback API_AVAILABLE(macos(10.15)) {
|
||||
while (!self.connectionEstablished)
|
||||
usleep(100000); // 100ms
|
||||
|
||||
self.decisionCallback = callback;
|
||||
es_event_type_t events[] = {
|
||||
ES_EVENT_TYPE_AUTH_EXEC,
|
||||
ES_EVENT_TYPE_AUTH_UNLINK,
|
||||
ES_EVENT_TYPE_AUTH_RENAME,
|
||||
ES_EVENT_TYPE_AUTH_KEXTLOAD,
|
||||
|
||||
// This is in the decision callback because it's used for detecting
|
||||
// the exit of a 'compiler' used by transitive whitelisting.
|
||||
ES_EVENT_TYPE_NOTIFY_EXIT,
|
||||
|
||||
// This is in the decision callback because it's used for clearing the
|
||||
// caches when a disk is unmounted.
|
||||
ES_EVENT_TYPE_NOTIFY_UNMOUNT,
|
||||
};
|
||||
es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t));
|
||||
if (sret != ES_RETURN_SUCCESS) LOGE(@"Unable to subscribe to auth events: %d", sret);
|
||||
|
||||
// There's a gap between creating a client and subscribing to events. Creating the client
|
||||
// triggers a cache flush automatically but any events that happen in this gap could be allowed
|
||||
// and cached, so we force the cache to flush again.
|
||||
[self flushCacheNonRootOnly:NO];
|
||||
}
|
||||
|
||||
- (void)listenForLogRequests:(void (^)(santa_message_t))callback API_AVAILABLE(macos(10.15)) {
|
||||
while (!self.connectionEstablished)
|
||||
usleep(100000); // 100ms
|
||||
|
||||
self.logCallback = callback;
|
||||
es_event_type_t events[] = {
|
||||
ES_EVENT_TYPE_NOTIFY_EXEC, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_LINK,
|
||||
ES_EVENT_TYPE_NOTIFY_RENAME, ES_EVENT_TYPE_NOTIFY_UNLINK, ES_EVENT_TYPE_NOTIFY_FORK,
|
||||
};
|
||||
es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t));
|
||||
if (sret != ES_RETURN_SUCCESS) LOGE(@"Unable to subscribe to notify events: %d", sret);
|
||||
}
|
||||
|
||||
- (int)postAction:(santa_action_t)action
|
||||
forMessage:(santa_message_t)sm API_AVAILABLE(macos(10.15)) {
|
||||
es_respond_result_t ret;
|
||||
switch (action) {
|
||||
case ACTION_RESPOND_ALLOW_COMPILER:
|
||||
[self setIsCompilerPID:sm.pid];
|
||||
|
||||
// Allow the exec, but don't cache the decision so subsequent execs of the compiler get
|
||||
// marked appropriately.
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW,
|
||||
false);
|
||||
break;
|
||||
case ACTION_RESPOND_ALLOW:
|
||||
case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE:
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW,
|
||||
true);
|
||||
break;
|
||||
case ACTION_RESPOND_DENY:
|
||||
case ACTION_RESPOND_TOOLONG:
|
||||
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_DENY,
|
||||
false);
|
||||
break;
|
||||
case ACTION_RESPOND_ACK: return ES_RESPOND_RESULT_SUCCESS;
|
||||
default: ret = ES_RESPOND_RESULT_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly API_AVAILABLE(macos(10.15)) {
|
||||
if (!self.connectionEstablished) return YES; // if not connected, there's nothing to flush.
|
||||
return es_clear_cache(self.client) == ES_CLEAR_CACHE_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
- (void)fileModificationPrefixFilterAdd:(NSArray *)filters {
|
||||
for (NSString *filter in filters) {
|
||||
self.prefixTree->AddPrefix(filter.fileSystemRepresentation);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fileModificationPrefixFilterReset {
|
||||
self.prefixTree->Reset();
|
||||
}
|
||||
|
||||
- (NSArray<NSNumber *> *)cacheCounts {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSArray<NSNumber *> *)cacheBucketCount {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID {
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
|
||||
- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeId {
|
||||
return KERN_FAILURE;
|
||||
}
|
||||
|
||||
- (BOOL)connectionEstablished {
|
||||
return self.client != nil;
|
||||
}
|
||||
|
||||
#pragma mark helpers
|
||||
|
||||
// Returns YES if the path was truncated.
|
||||
// The populated buffer will be NUL terminated.
|
||||
+ (BOOL)populateBufferFromESFile:(es_file_t *)file buffer:(char *)buffer size:(size_t)size {
|
||||
if (file == NULL) return NO;
|
||||
return [SNTEndpointSecurityManager populateBufferFromString:file->path.data
|
||||
buffer:buffer
|
||||
size:size];
|
||||
}
|
||||
|
||||
// Returns YES if the path was truncated.
|
||||
// The populated buffer will be NUL terminated.
|
||||
+ (BOOL)populateBufferFromString:(const char *)string buffer:(char *)buffer size:(size_t)size {
|
||||
return strlcpy(buffer, string, size) >= size;
|
||||
}
|
||||
|
||||
- (BOOL)populateRenamedNewPathFromESMessage:(es_event_rename_t)mv
|
||||
buffer:(char *)buffer
|
||||
size:(size_t)size {
|
||||
BOOL truncated = NO;
|
||||
switch (mv.destination_type) {
|
||||
case ES_DESTINATION_TYPE_NEW_PATH: {
|
||||
NSString *p = @(mv.destination.new_path.dir->path.data);
|
||||
p = [p stringByAppendingPathComponent:@(mv.destination.new_path.filename.data)];
|
||||
truncated = [SNTEndpointSecurityManager populateBufferFromString:p.UTF8String
|
||||
buffer:buffer
|
||||
size:size];
|
||||
break;
|
||||
}
|
||||
case ES_DESTINATION_TYPE_EXISTING_FILE: {
|
||||
truncated = [SNTEndpointSecurityManager populateBufferFromESFile:mv.destination.existing_file
|
||||
buffer:buffer
|
||||
size:size];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return truncated;
|
||||
}
|
||||
|
||||
- (santa_vnode_id_t)vnodeIDForFile:(es_file_t *)file {
|
||||
return {
|
||||
.fsid = (uint64_t)file->stat.st_dev,
|
||||
.fileid = file->stat.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
- (BOOL)isDatabasePath:(NSString *)path {
|
||||
return [path isEqualToString:@"/private/var/db/santa/rules.db"] ||
|
||||
[path isEqualToString:@"/private/var/db/santa/events.db"];
|
||||
}
|
||||
|
||||
- (BOOL)isCompilerPID:(pid_t)pid {
|
||||
return (pid && pid < PID_MAX && self->_compilerPIDs[pid].load());
|
||||
}
|
||||
|
||||
- (void)setIsCompilerPID:(pid_t)pid {
|
||||
if (pid < 1) {
|
||||
LOGE(@"Unable to watch compiler pid=%d", pid);
|
||||
} else if (pid >= PID_MAX) {
|
||||
LOGE(@"Unable to watch compiler pid=%d >= PID_MAX(%d)", pid, PID_MAX);
|
||||
} else {
|
||||
self->_compilerPIDs[pid].store(true);
|
||||
LOGD(@"Watching compiler pid=%d", pid);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setNotCompilerPID:(pid_t)pid {
|
||||
if (pid && pid < PID_MAX) self->_compilerPIDs[pid].store(false);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,250 +0,0 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#import <XCTest/XCTest.h>
|
||||
#import <bsm/libbsm.h>
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityManager.h"
|
||||
|
||||
// Must be imported last to overload libEndpointSecurity functions.
|
||||
#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h"
|
||||
|
||||
const NSString *const kEventsDBPath = @"/private/var/db/santa/events.db";
|
||||
const NSString *const kRulesDBPath = @"/private/var/db/santa/rules.db";
|
||||
const NSString *const kBenignPath = @"/some/other/path";
|
||||
|
||||
@interface SNTEndpointSecurityManagerTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityManagerTest
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
fclose(stdout);
|
||||
}
|
||||
|
||||
- (void)testDenyOnTimeout {
|
||||
// There should be two events: an early uncached DENY as the consequence for not
|
||||
// meeting the decision deadline and an actual cached decision from our message
|
||||
// handler.
|
||||
__block int wantNumResp = 2;
|
||||
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
|
||||
(void)snt; // Make it appear used for the sake of -Wunused-variable
|
||||
|
||||
XCTestExpectation *expectation =
|
||||
[self expectationWithDescription:@"Wait for santa's Auth dispatch queue"];
|
||||
|
||||
__block NSMutableArray<ESResponse *> *events = [NSMutableArray array];
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK
|
||||
withCallback:^(ESResponse *r) {
|
||||
@synchronized(self) {
|
||||
[events addObject:r];
|
||||
}
|
||||
|
||||
if (events.count >= wantNumResp) {
|
||||
[expectation fulfill];
|
||||
}
|
||||
}];
|
||||
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(kEventsDBPath)};
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.binaryPath = @"somebinary";
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = ES_EVENT_TYPE_AUTH_UNLINK;
|
||||
m.message->event = (es_events_t){.unlink = {.target = &dbFile}};
|
||||
m.message->mach_time = 1234;
|
||||
m.message->deadline = 1234;
|
||||
}];
|
||||
|
||||
[mockES triggerHandler:m.message];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
for (ESResponse *resp in events) {
|
||||
XCTAssertEqual(
|
||||
resp.result, ES_AUTH_RESULT_DENY,
|
||||
@"Failed to automatically deny on timeout and also the malicious event afterwards");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testDeleteRulesDB {
|
||||
NSDictionary<const NSString *, NSNumber *> *testCases = @{
|
||||
kEventsDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
kRulesDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
kBenignPath : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
|
||||
};
|
||||
for (const NSString *testPath in testCases) {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
|
||||
(void)snt; // Make it appear used for the sake of -Wunused-variable
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(testPath)};
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.binaryPath = @"somebinary";
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = ES_EVENT_TYPE_AUTH_UNLINK;
|
||||
m.message->event = (es_events_t){.unlink = {.target = &dbFile}};
|
||||
}];
|
||||
|
||||
[mockES triggerHandler:m.message];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertEqual(got.result, [testCases objectForKey:testPath].intValue,
|
||||
@"Incorrect handling of delete of %@", testPath);
|
||||
XCTAssertTrue(got.shouldCache, @"Failed to cache deletion decision of %@", testPath);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testSkipOtherESEvents {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
|
||||
(void)snt; // Make it appear used for the sake of -Wunused-variable
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(@"/some/other/path")};
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.process->is_es_client = true;
|
||||
m.binaryPath = @"somebinary";
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = ES_EVENT_TYPE_AUTH_UNLINK;
|
||||
m.message->event = (es_events_t){.unlink = {.target = &dbFile}};
|
||||
}];
|
||||
|
||||
[mockES triggerHandler:m.message];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
|
||||
- (void)testRenameOverwriteRulesDB {
|
||||
NSDictionary<const NSString *, NSNumber *> *testCases = @{
|
||||
kEventsDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
kRulesDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
kBenignPath : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
|
||||
};
|
||||
for (const NSString *testPath in testCases) {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
|
||||
(void)snt; // Make it appear used for the sake of -Wunused-variable
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_RENAME
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
__block es_file_t otherFile = {.path = MakeStringToken(@"/some/other/path")};
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(testPath)};
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.binaryPath = @"somebinary";
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = ES_EVENT_TYPE_AUTH_RENAME;
|
||||
m.message->event = (es_events_t){
|
||||
.rename =
|
||||
{
|
||||
.source = &otherFile,
|
||||
.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE,
|
||||
.destination = {.existing_file = &dbFile},
|
||||
},
|
||||
};
|
||||
}];
|
||||
|
||||
[mockES triggerHandler:m.message];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertEqual(got.result, [testCases objectForKey:testPath].intValue,
|
||||
@"Incorrect handling of rename of %@", testPath);
|
||||
XCTAssertTrue(got.shouldCache, @"Failed to cache rename auth decision of %@", testPath);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testRenameRulesDB {
|
||||
NSDictionary<const NSString *, NSNumber *> *testCases = @{
|
||||
kEventsDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
kRulesDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
|
||||
kBenignPath : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
|
||||
};
|
||||
|
||||
for (const NSString *testPath in testCases) {
|
||||
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
|
||||
[mockES reset];
|
||||
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
|
||||
(void)snt; // Make it appear used for the sake of -Wunused-variable
|
||||
|
||||
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
|
||||
__block ESResponse *got;
|
||||
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_RENAME
|
||||
withCallback:^(ESResponse *r) {
|
||||
got = r;
|
||||
[expectation fulfill];
|
||||
}];
|
||||
|
||||
__block es_file_t otherFile = {.path = MakeStringToken(@"/some/other/path")};
|
||||
__block es_file_t dbFile = {.path = MakeStringToken(testPath)};
|
||||
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
|
||||
m.binaryPath = @"somebinary";
|
||||
m.message->action_type = ES_ACTION_TYPE_AUTH;
|
||||
m.message->event_type = ES_EVENT_TYPE_AUTH_RENAME;
|
||||
m.message->event = (es_events_t){
|
||||
.rename =
|
||||
{
|
||||
.source = &dbFile,
|
||||
.destination_type = ES_DESTINATION_TYPE_NEW_PATH,
|
||||
.destination = {.new_path =
|
||||
{
|
||||
.dir = &otherFile,
|
||||
.filename = MakeStringToken(@"someotherfilename"),
|
||||
}},
|
||||
},
|
||||
};
|
||||
}];
|
||||
|
||||
[mockES triggerHandler:m.message];
|
||||
|
||||
[self waitForExpectations:@[ expectation ] timeout:60.0];
|
||||
|
||||
XCTAssertEqual(got.result, [testCases objectForKey:testPath].intValue,
|
||||
@"Incorrect handling of rename of %@", testPath);
|
||||
|
||||
XCTAssertTrue(got.shouldCache, @"Failed to cache rename auth decision of %@", testPath);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
42
Source/santad/EventProviders/SNTEndpointSecurityRecorder.h
Normal file
42
Source/santad/EventProviders/SNTEndpointSecurityRecorder.h
Normal file
@@ -0,0 +1,42 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTPrefixTree.h"
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#import "Source/santad/Metrics.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
|
||||
/// ES Client focused on subscribing to NOTIFY event variants with the intention of enriching
|
||||
/// received messages and logging the information.
|
||||
@interface SNTEndpointSecurityRecorder : SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)
|
||||
esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
|
||||
enricher:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:
|
||||
(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache
|
||||
prefixTree:(std::shared_ptr<SNTPrefixTree>)prefixTree;
|
||||
|
||||
@end
|
||||
134
Source/santad/EventProviders/SNTEndpointSecurityRecorder.mm
Normal file
134
Source/santad/EventProviders/SNTEndpointSecurityRecorder.mm
Normal file
@@ -0,0 +1,134 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.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"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
|
||||
switch (msg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: return msg->event.close.target;
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK: return msg->event.link.source;
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: return msg->event.rename.source;
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK: return msg->event.unlink.target;
|
||||
default: return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
@interface SNTEndpointSecurityRecorder ()
|
||||
@property SNTCompilerController *compilerController;
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityRecorder {
|
||||
std::shared_ptr<AuthResultCache> _authResultCache;
|
||||
std::shared_ptr<Enricher> _enricher;
|
||||
std::shared_ptr<Logger> _logger;
|
||||
std::shared_ptr<SNTPrefixTree> _prefixTree;
|
||||
}
|
||||
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
logger:(std::shared_ptr<Logger>)logger
|
||||
enricher:(std::shared_ptr<Enricher>)enricher
|
||||
compilerController:(SNTCompilerController *)compilerController
|
||||
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache
|
||||
prefixTree:(std::shared_ptr<SNTPrefixTree>)prefixTree {
|
||||
self = [super initWithESAPI:std::move(esApi)
|
||||
metrics:std::move(metrics)
|
||||
processor:santa::santad::Processor::kRecorder];
|
||||
if (self) {
|
||||
_enricher = enricher;
|
||||
_logger = logger;
|
||||
_compilerController = compilerController;
|
||||
_authResultCache = authResultCache;
|
||||
_prefixTree = prefixTree;
|
||||
|
||||
[self establishClientOrDie];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return @"Recorder";
|
||||
}
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
|
||||
// Pre-enrichment processing
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE:
|
||||
// TODO(mlw): Once we move to building with the macOS 13 SDK, we should also check
|
||||
// the `was_mapped_writable` field
|
||||
if (esMsg->event.close.modified == false) {
|
||||
// Ignore unmodified files
|
||||
// Note: Do not record metrics in this case. These are not considered "drops"
|
||||
// because this is not a failure case. Ideally we would tell ES to not send
|
||||
// these events in the first place but no such mechanism currently exists.
|
||||
return;
|
||||
}
|
||||
|
||||
self->_authResultCache->RemoveFromCache(esMsg->event.close.target);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
[self.compilerController handleEvent:esMsg withLogger:self->_logger];
|
||||
|
||||
// Filter file op events matching the prefix tree.
|
||||
es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg));
|
||||
if (targetFile != NULL && self->_prefixTree->HasPrefix(targetFile->path.data)) {
|
||||
recordEventMetrics(EventDisposition::kDropped);
|
||||
return;
|
||||
}
|
||||
|
||||
// Enrich the message inline with the ES handler block to capture enrichment
|
||||
// data as close to the source event as possible.
|
||||
std::shared_ptr<EnrichedMessage> sharedEnrichedMessage = _enricher->Enrich(std::move(esMsg));
|
||||
|
||||
// Asynchronously log the message
|
||||
[self processEnrichedMessage:std::move(sharedEnrichedMessage)
|
||||
handler:^(std::shared_ptr<EnrichedMessage> msg) {
|
||||
self->_logger->Log(std::move(msg));
|
||||
recordEventMetrics(EventDisposition::kProcessed);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)enable {
|
||||
[super subscribe:{
|
||||
ES_EVENT_TYPE_NOTIFY_CLOSE,
|
||||
ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA,
|
||||
ES_EVENT_TYPE_NOTIFY_EXEC,
|
||||
ES_EVENT_TYPE_NOTIFY_EXIT,
|
||||
ES_EVENT_TYPE_NOTIFY_FORK,
|
||||
ES_EVENT_TYPE_NOTIFY_LINK,
|
||||
ES_EVENT_TYPE_NOTIFY_RENAME,
|
||||
ES_EVENT_TYPE_NOTIFY_UNLINK,
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
231
Source/santad/EventProviders/SNTEndpointSecurityRecorderTest.mm
Normal file
231
Source/santad/EventProviders/SNTEndpointSecurityRecorderTest.mm
Normal file
@@ -0,0 +1,231 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstddef>
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#import "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
#import "Source/santad/SNTCompilerController.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::Processor;
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
class MockEnricher : public Enricher {
|
||||
public:
|
||||
MOCK_METHOD(std::shared_ptr<EnrichedMessage>, Enrich, (Message &&));
|
||||
};
|
||||
|
||||
class MockAuthResultCache : public AuthResultCache {
|
||||
public:
|
||||
using AuthResultCache::AuthResultCache;
|
||||
|
||||
MOCK_METHOD(void, RemoveFromCache, (const es_file_t *));
|
||||
};
|
||||
|
||||
class MockLogger : public Logger {
|
||||
public:
|
||||
using Logger::Logger;
|
||||
|
||||
MOCK_METHOD(void, Log, (std::shared_ptr<EnrichedMessage>));
|
||||
};
|
||||
|
||||
@interface SNTEndpointSecurityRecorderTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityRecorderTest
|
||||
|
||||
- (void)testEnable {
|
||||
// Ensure the client subscribes to expected event types
|
||||
std::set<es_event_type_t> expectedEventSubs{
|
||||
ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA, ES_EVENT_TYPE_NOTIFY_EXEC,
|
||||
ES_EVENT_TYPE_NOTIFY_FORK, ES_EVENT_TYPE_NOTIFY_EXIT, ES_EVENT_TYPE_NOTIFY_LINK,
|
||||
ES_EVENT_TYPE_NOTIFY_RENAME, ES_EVENT_TYPE_NOTIFY_UNLINK,
|
||||
};
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
id recorderClient = [[SNTEndpointSecurityRecorder alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
processor:Processor::kRecorder];
|
||||
|
||||
EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs)).WillOnce(testing::Return(true));
|
||||
|
||||
[recorderClient enable];
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc, ActionType::Auth);
|
||||
es_file_t targetFile = MakeESFile("bar");
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
std::shared_ptr<EnrichedMessage> enrichedMsg = std::shared_ptr<EnrichedMessage>(nullptr);
|
||||
|
||||
auto mockEnricher = std::make_shared<MockEnricher>();
|
||||
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(enrichedMsg));
|
||||
|
||||
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
|
||||
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFile)).Times(1);
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
// NOTE: Currently unable to create a partial mock of the
|
||||
// `SNTEndpointSecurityRecorder` object. There is a bug in OCMock that doesn't
|
||||
// properly handle the `processEnrichedMessage:handler:` block. Instead this
|
||||
// test will mock the `Log` method that is called in the handler block.
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
auto mockLogger = std::make_shared<MockLogger>(nullptr, nullptr);
|
||||
EXPECT_CALL(*mockLogger, Log).WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
dispatch_semaphore_signal(sema);
|
||||
}));
|
||||
|
||||
auto prefixTree = std::make_shared<SNTPrefixTree>();
|
||||
|
||||
id mockCC = OCMStrictClassMock([SNTCompilerController class]);
|
||||
|
||||
SNTEndpointSecurityRecorder *recorderClient =
|
||||
[[SNTEndpointSecurityRecorder alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:mockLogger
|
||||
enricher:mockEnricher
|
||||
compilerController:mockCC
|
||||
authResultCache:mockAuthCache
|
||||
prefixTree:prefixTree];
|
||||
|
||||
// CLOSE not modified, bail early
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg.event.close.modified = false;
|
||||
esMsg.event.close.target = NULL;
|
||||
|
||||
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, &esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Metrics record callback should not be called here");
|
||||
}]);
|
||||
}
|
||||
|
||||
// CLOSE modified, remove from cache
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg.event.close.modified = true;
|
||||
esMsg.event.close.target = &targetFile;
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kProcessed);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertSemaTrue(sema, 5, "Log wasn't called within expected time window");
|
||||
}
|
||||
|
||||
// LINK, Prefix match, bail early
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_LINK;
|
||||
esMsg.event.link.source = &targetFile;
|
||||
prefixTree->AddPrefix(esMsg.event.link.source->path.data);
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
|
||||
|
||||
[recorderClient handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
}
|
||||
|
||||
XCTAssertTrue(OCMVerifyAll(mockCC));
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockEnricher.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockLogger.get());
|
||||
|
||||
[mockCC stopMocking];
|
||||
}
|
||||
|
||||
- (void)testGetTargetFileForPrefixTree {
|
||||
// Ensure `GetTargetFileForPrefixTree` returns expected field for each
|
||||
// subscribed event type in the `SNTEndpointSecurityRecorder`.
|
||||
extern es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg);
|
||||
|
||||
es_file_t closeFile = MakeESFile("close");
|
||||
es_file_t linkFile = MakeESFile("link");
|
||||
es_file_t renameFile = MakeESFile("rename");
|
||||
es_file_t unlinkFile = MakeESFile("unlink");
|
||||
es_message_t esMsg;
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
|
||||
esMsg.event.close.target = &closeFile;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &closeFile);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_LINK;
|
||||
esMsg.event.link.source = &linkFile;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &linkFile);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_RENAME;
|
||||
esMsg.event.rename.source = &renameFile;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &renameFile);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_UNLINK;
|
||||
esMsg.event.unlink.target = &unlinkFile;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &unlinkFile);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_FORK;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
|
||||
|
||||
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXIT;
|
||||
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,35 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
/// ES Client focused on mitigating accidental or malicious tampering of Santa and its components.
|
||||
@interface SNTEndpointSecurityTamperResistance
|
||||
: SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
|
||||
|
||||
- (instancetype)
|
||||
initWithESAPI:
|
||||
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,122 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#include <string.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
|
||||
static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-driver";
|
||||
|
||||
@implementation SNTEndpointSecurityTamperResistance {
|
||||
std::shared_ptr<Logger> _logger;
|
||||
}
|
||||
|
||||
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
|
||||
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
|
||||
logger:(std::shared_ptr<Logger>)logger {
|
||||
self = [super initWithESAPI:std::move(esApi)
|
||||
metrics:std::move(metrics)
|
||||
processor:santa::santad::Processor::kTamperResistance];
|
||||
if (self) {
|
||||
_logger = logger;
|
||||
|
||||
[self establishClientOrDie];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return @"Tamper Resistance";
|
||||
}
|
||||
|
||||
- (void)handleMessage:(Message &&)esMsg
|
||||
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
|
||||
es_auth_result_t result = ES_AUTH_RESULT_ALLOW;
|
||||
switch (esMsg->event_type) {
|
||||
case ES_EVENT_TYPE_AUTH_UNLINK: {
|
||||
if ([SNTEndpointSecurityTamperResistance
|
||||
isDatabasePath:esMsg->event.unlink.target->path.data]) {
|
||||
result = ES_AUTH_RESULT_DENY;
|
||||
LOGW(@"Preventing attempt to delete Santa databases!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_RENAME: {
|
||||
if ([SNTEndpointSecurityTamperResistance
|
||||
isDatabasePath:esMsg->event.rename.source->path.data]) {
|
||||
result = ES_AUTH_RESULT_DENY;
|
||||
LOGW(@"Preventing attempt to rename Santa databases!");
|
||||
break;
|
||||
}
|
||||
|
||||
if (esMsg->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
|
||||
if ([SNTEndpointSecurityTamperResistance
|
||||
isDatabasePath:esMsg->event.rename.destination.existing_file->path.data]) {
|
||||
result = ES_AUTH_RESULT_DENY;
|
||||
LOGW(@"Preventing attempt to overwrite Santa databases!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case ES_EVENT_TYPE_AUTH_KEXTLOAD: {
|
||||
// TODO(mlw): Since we don't package the kext anymore, we should consider removing this.
|
||||
// TODO(mlw): Consider logging when kext loads are attempted.
|
||||
if (strcmp(esMsg->event.kextload.identifier.data, kSantaKextIdentifier.data()) == 0) {
|
||||
result = ES_AUTH_RESULT_DENY;
|
||||
LOGW(@"Preventing attempt to load Santa kext!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// Unexpected event type, this is a programming error
|
||||
[NSException raise:@"Invalid event type"
|
||||
format:@"Invalid tamper resistance event type: %d", esMsg->event_type];
|
||||
}
|
||||
|
||||
// Do not cache denied operations so that each tamper attempt is logged
|
||||
[self respondToMessage:esMsg withAuthResult:result cacheable:result == ES_AUTH_RESULT_ALLOW];
|
||||
|
||||
// For this client, a processed event is one that was found to be violating anti-tamper policy
|
||||
recordEventMetrics(result == ES_AUTH_RESULT_DENY ? EventDisposition::kProcessed
|
||||
: EventDisposition::kDropped);
|
||||
}
|
||||
|
||||
- (void)enable {
|
||||
// TODO(mlw): For macOS 13, use new mute and invert APIs to limit the
|
||||
// messages sent for these events to the Santa-specific directories
|
||||
// checked in the `handleMessage:` method.
|
||||
|
||||
[super subscribeAndClearCache:{
|
||||
ES_EVENT_TYPE_AUTH_KEXTLOAD,
|
||||
ES_EVENT_TYPE_AUTH_UNLINK,
|
||||
ES_EVENT_TYPE_AUTH_RENAME,
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,231 @@
|
||||
/// 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 <stdlib.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#import "Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h"
|
||||
#import "Source/santad/Metrics.h"
|
||||
|
||||
using santa::santad::EventDisposition;
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
static constexpr std::string_view kEventsDBPath = "/private/var/db/santa/events.db";
|
||||
static constexpr std::string_view kRulesDBPath = "/private/var/db/santa/rules.db";
|
||||
static constexpr std::string_view kBenignPath = "/some/other/path";
|
||||
static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-driver";
|
||||
|
||||
@interface SNTEndpointSecurityTamperResistanceTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTEndpointSecurityTamperResistanceTest
|
||||
|
||||
- (void)testEnable {
|
||||
// Ensure the client subscribes to expected event types
|
||||
std::set<es_event_type_t> expectedEventSubs{
|
||||
ES_EVENT_TYPE_AUTH_KEXTLOAD,
|
||||
ES_EVENT_TYPE_AUTH_UNLINK,
|
||||
ES_EVENT_TYPE_AUTH_RENAME,
|
||||
};
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
EXPECT_CALL(*mockESApi, NewClient(testing::_))
|
||||
.WillOnce(testing::Return(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS)));
|
||||
EXPECT_CALL(*mockESApi, MuteProcess(testing::_, testing::_)).WillOnce(testing::Return(true));
|
||||
EXPECT_CALL(*mockESApi, ClearCache(testing::_))
|
||||
.After(EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs))
|
||||
.WillOnce(testing::Return(true)))
|
||||
.WillOnce(testing::Return(true));
|
||||
|
||||
SNTEndpointSecurityTamperResistance *tamperClient =
|
||||
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:nullptr];
|
||||
id mockTamperClient = OCMPartialMock(tamperClient);
|
||||
|
||||
[mockTamperClient enable];
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
[mockTamperClient stopMocking];
|
||||
}
|
||||
|
||||
- (void)testHandleMessage {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file);
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth);
|
||||
|
||||
es_file_t fileEventsDB = MakeESFile(kEventsDBPath.data());
|
||||
es_file_t fileRulesDB = MakeESFile(kRulesDBPath.data());
|
||||
es_file_t fileBenign = MakeESFile(kBenignPath.data());
|
||||
|
||||
es_string_token_t santaTok = MakeESStringToken(kSantaKextIdentifier.data());
|
||||
es_string_token_t benignTok = MakeESStringToken(kBenignPath.data());
|
||||
|
||||
std::map<es_file_t *, es_auth_result_t> pathToResult{
|
||||
{&fileEventsDB, ES_AUTH_RESULT_DENY},
|
||||
{&fileRulesDB, ES_AUTH_RESULT_DENY},
|
||||
{&fileBenign, ES_AUTH_RESULT_ALLOW},
|
||||
};
|
||||
|
||||
std::map<es_string_token_t *, es_auth_result_t> kextIdToResult{
|
||||
{&santaTok, ES_AUTH_RESULT_DENY},
|
||||
{&benignTok, ES_AUTH_RESULT_ALLOW},
|
||||
};
|
||||
|
||||
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsESNewClient();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
SNTEndpointSecurityTamperResistance *tamperClient =
|
||||
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi
|
||||
metrics:nullptr
|
||||
logger:nullptr];
|
||||
|
||||
id mockTamperClient = OCMPartialMock(tamperClient);
|
||||
|
||||
// Unable to use `OCMExpect` here because we cannot match on the `Message`
|
||||
// parameter. In order to verify the `AuthResult` and `Cacheable` parameters,
|
||||
// instead use `OCMStub` and extract the arguments in order to assert their
|
||||
// expected values.
|
||||
__block es_auth_result_t gotAuthResult;
|
||||
__block bool gotCachable;
|
||||
OCMStub([mockTamperClient respondToMessage:Message(mockESApi, &esMsg)
|
||||
withAuthResult:(es_auth_result_t)0
|
||||
cacheable:false])
|
||||
.ignoringNonObjectArgs()
|
||||
.andDo(^(NSInvocation *inv) {
|
||||
[inv getArgument:&gotAuthResult atIndex:3];
|
||||
[inv getArgument:&gotCachable atIndex:4];
|
||||
});
|
||||
|
||||
// First check unhandled event types will crash
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
XCTAssertThrows([tamperClient handleMessage:Message(mockESApi, &esMsg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTFail("Unhandled event types shouldn't call metrics recorder");
|
||||
}]);
|
||||
}
|
||||
|
||||
// Check UNLINK tamper events
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_UNLINK;
|
||||
for (const auto &kv : pathToResult) {
|
||||
Message msg(mockESApi, &esMsg);
|
||||
esMsg.event.unlink.target = kv.first;
|
||||
|
||||
[mockTamperClient
|
||||
handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, kv.second == ES_AUTH_RESULT_DENY ? EventDisposition::kProcessed
|
||||
: EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
|
||||
XCTAssertEqual(gotAuthResult, kv.second);
|
||||
XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
}
|
||||
|
||||
// Check RENAME `source` tamper events
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_RENAME;
|
||||
for (const auto &kv : pathToResult) {
|
||||
Message msg(mockESApi, &esMsg);
|
||||
esMsg.event.rename.source = kv.first;
|
||||
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
|
||||
|
||||
[mockTamperClient
|
||||
handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, kv.second == ES_AUTH_RESULT_DENY ? EventDisposition::kProcessed
|
||||
: EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertEqual(gotAuthResult, kv.second);
|
||||
XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
}
|
||||
|
||||
// Check RENAME `dest` tamper events
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_RENAME;
|
||||
esMsg.event.rename.source = &fileBenign;
|
||||
for (const auto &kv : pathToResult) {
|
||||
Message msg(mockESApi, &esMsg);
|
||||
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
|
||||
esMsg.event.rename.destination.existing_file = kv.first;
|
||||
|
||||
[mockTamperClient
|
||||
handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, kv.second == ES_AUTH_RESULT_DENY ? EventDisposition::kProcessed
|
||||
: EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertEqual(gotAuthResult, kv.second);
|
||||
XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
}
|
||||
|
||||
// Check KEXTLOAD tamper events
|
||||
{
|
||||
esMsg.event_type = ES_EVENT_TYPE_AUTH_KEXTLOAD;
|
||||
|
||||
for (const auto &kv : kextIdToResult) {
|
||||
Message msg(mockESApi, &esMsg);
|
||||
esMsg.event.kextload.identifier = *kv.first;
|
||||
|
||||
[mockTamperClient
|
||||
handleMessage:std::move(msg)
|
||||
recordEventMetrics:^(EventDisposition d) {
|
||||
XCTAssertEqual(d, kv.second == ES_AUTH_RESULT_DENY ? EventDisposition::kProcessed
|
||||
: EventDisposition::kDropped);
|
||||
dispatch_semaphore_signal(semaMetrics);
|
||||
}];
|
||||
|
||||
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
|
||||
XCTAssertEqual(gotAuthResult, kv.second);
|
||||
XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW);
|
||||
}
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTAssertTrue(OCMVerifyAll(mockTamperClient));
|
||||
|
||||
[mockTamperClient stopMocking];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,33 +0,0 @@
|
||||
/// Copyright 2019 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
|
||||
@protocol SNTEventProvider <NSObject>
|
||||
|
||||
- (void)listenForDecisionRequests:(void (^)(santa_message_t message))callback;
|
||||
- (void)listenForLogRequests:(void (^)(santa_message_t message))callback;
|
||||
- (int)postAction:(santa_action_t)action forMessage:(santa_message_t)sm;
|
||||
- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly;
|
||||
- (void)fileModificationPrefixFilterAdd:(NSArray *)filters;
|
||||
- (void)fileModificationPrefixFilterReset;
|
||||
- (NSArray<NSNumber *> *)cacheCounts;
|
||||
- (NSArray<NSNumber *> *)cacheBucketCount;
|
||||
- (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID;
|
||||
- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeId;
|
||||
@property(readonly) BOOL connectionEstablished;
|
||||
|
||||
@end
|
||||
71
Source/santad/Logs/EndpointSecurity/Logger.h
Normal file
71
Source/santad/Logs/EndpointSecurity/Logger.h
Normal file
@@ -0,0 +1,71 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_LOGGER_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_LOGGER_H
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h"
|
||||
|
||||
// Forward declarations
|
||||
@class SNTStoredEvent;
|
||||
namespace santa::santad::logs::endpoint_security {
|
||||
class LoggerPeer;
|
||||
}
|
||||
|
||||
namespace santa::santad::logs::endpoint_security {
|
||||
|
||||
class Logger {
|
||||
public:
|
||||
static std::unique_ptr<Logger> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
SNTEventLogType log_type, NSString *event_log_path, NSString *spool_log_path,
|
||||
size_t spool_dir_size_threshold, size_t spool_file_size_threshold,
|
||||
uint64_t spool_flush_timeout_ms);
|
||||
|
||||
Logger(std::shared_ptr<serializers::Serializer> serializer,
|
||||
std::shared_ptr<writers::Writer> writer);
|
||||
|
||||
virtual ~Logger() = default;
|
||||
|
||||
virtual void Log(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg);
|
||||
|
||||
void LogAllowlist(const santa::santad::event_providers::endpoint_security::Message &msg,
|
||||
const std::string_view hash);
|
||||
|
||||
void LogBundleHashingEvents(NSArray<SNTStoredEvent *> *events);
|
||||
|
||||
void LogDiskAppeared(NSDictionary *props);
|
||||
void LogDiskDisappeared(NSDictionary *props);
|
||||
|
||||
friend class santa::santad::logs::endpoint_security::LoggerPeer;
|
||||
|
||||
private:
|
||||
std::shared_ptr<serializers::Serializer> serializer_;
|
||||
std::shared_ptr<writers::Writer> writer_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security
|
||||
|
||||
#endif
|
||||
100
Source/santad/Logs/EndpointSecurity/Logger.mm
Normal file
100
Source/santad/Logs/EndpointSecurity/Logger.mm
Normal file
@@ -0,0 +1,100 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#include "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/File.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Null.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Spool.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Syslog.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::BasicString;
|
||||
using santa::santad::logs::endpoint_security::serializers::Empty;
|
||||
using santa::santad::logs::endpoint_security::serializers::Protobuf;
|
||||
using santa::santad::logs::endpoint_security::writers::File;
|
||||
using santa::santad::logs::endpoint_security::writers::Null;
|
||||
using santa::santad::logs::endpoint_security::writers::Spool;
|
||||
using santa::santad::logs::endpoint_security::writers::Syslog;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security {
|
||||
|
||||
// Flush the write buffer every 5 seconds
|
||||
static const uint64_t kFlushBufferTimeoutMS = 10000;
|
||||
// Batch writes up to 128kb
|
||||
static const size_t kBufferBatchSizeBytes = (1024 * 128);
|
||||
// Reserve an extra 4kb of buffer space to account for event overflow
|
||||
static const size_t kMaxExpectedWriteSizeBytes = 4096;
|
||||
|
||||
// Translate configured log type to appropriate Serializer/Writer pairs
|
||||
std::unique_ptr<Logger> Logger::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
SNTEventLogType log_type, NSString *event_log_path,
|
||||
NSString *spool_log_path, size_t spool_dir_size_threshold,
|
||||
size_t spool_file_size_threshold,
|
||||
uint64_t spool_flush_timeout_ms) {
|
||||
switch (log_type) {
|
||||
case SNTEventLogTypeFilelog:
|
||||
return std::make_unique<Logger>(
|
||||
BasicString::Create(esapi),
|
||||
File::Create(event_log_path, kFlushBufferTimeoutMS, kBufferBatchSizeBytes,
|
||||
kMaxExpectedWriteSizeBytes));
|
||||
case SNTEventLogTypeSyslog:
|
||||
return std::make_unique<Logger>(BasicString::Create(esapi, false), Syslog::Create());
|
||||
case SNTEventLogTypeNull: return std::make_unique<Logger>(Empty::Create(), Null::Create());
|
||||
case SNTEventLogTypeProtobuf:
|
||||
LOGW(@"The EventLogType value protobuf is currently in beta. The protobuf schema is subject "
|
||||
@"to change.");
|
||||
return std::make_unique<Logger>(
|
||||
Protobuf::Create(esapi),
|
||||
Spool::Create([spool_log_path UTF8String], spool_dir_size_threshold,
|
||||
spool_file_size_threshold, spool_flush_timeout_ms));
|
||||
default: LOGE(@"Invalid log type: %ld", log_type); return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Logger::Logger(std::shared_ptr<serializers::Serializer> serializer,
|
||||
std::shared_ptr<writers::Writer> writer)
|
||||
: serializer_(std::move(serializer)), writer_(std::move(writer)) {}
|
||||
|
||||
void Logger::Log(std::shared_ptr<EnrichedMessage> msg) {
|
||||
writer_->Write(serializer_->SerializeMessage(std::move(msg)));
|
||||
}
|
||||
|
||||
void Logger::LogAllowlist(const Message &msg, const std::string_view hash) {
|
||||
writer_->Write(serializer_->SerializeAllowlist(msg, hash));
|
||||
}
|
||||
|
||||
void Logger::LogBundleHashingEvents(NSArray<SNTStoredEvent *> *events) {
|
||||
for (SNTStoredEvent *se in events) {
|
||||
writer_->Write(serializer_->SerializeBundleHashingEvent(se));
|
||||
}
|
||||
}
|
||||
|
||||
void Logger::LogDiskAppeared(NSDictionary *props) {
|
||||
writer_->Write(serializer_->SerializeDiskAppeared(props));
|
||||
}
|
||||
|
||||
void Logger::LogDiskDisappeared(NSDictionary *props) {
|
||||
writer_->Write(serializer_->SerializeDiskDisappeared(props));
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security
|
||||
210
Source/santad/Logs/EndpointSecurity/LoggerTest.mm
Normal file
210
Source/santad/Logs/EndpointSecurity/LoggerTest.mm
Normal file
@@ -0,0 +1,210 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/File.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Null.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Spool.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Syslog.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFile;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedProcess;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::Logger;
|
||||
using santa::santad::logs::endpoint_security::serializers::BasicString;
|
||||
using santa::santad::logs::endpoint_security::serializers::Empty;
|
||||
using santa::santad::logs::endpoint_security::serializers::Protobuf;
|
||||
using santa::santad::logs::endpoint_security::writers::File;
|
||||
using santa::santad::logs::endpoint_security::writers::Null;
|
||||
using santa::santad::logs::endpoint_security::writers::Spool;
|
||||
using santa::santad::logs::endpoint_security::writers::Syslog;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security {
|
||||
|
||||
class LoggerPeer : public Logger {
|
||||
public:
|
||||
// Make base class constructors visible
|
||||
using Logger::Logger;
|
||||
|
||||
LoggerPeer(std::unique_ptr<Logger> l) : Logger(l->serializer_, l->writer_) {}
|
||||
|
||||
std::shared_ptr<serializers::Serializer> Serializer() { return serializer_; }
|
||||
|
||||
std::shared_ptr<writers::Writer> Writer() { return writer_; }
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security
|
||||
|
||||
using santa::santad::logs::endpoint_security::LoggerPeer;
|
||||
|
||||
class MockSerializer : public Empty {
|
||||
public:
|
||||
MOCK_METHOD(std::vector<uint8_t>, SerializeMessage, (const EnrichedClose &msg));
|
||||
|
||||
MOCK_METHOD(std::vector<uint8_t>, SerializeAllowlist, (const Message &, const std::string_view));
|
||||
|
||||
MOCK_METHOD(std::vector<uint8_t>, SerializeBundleHashingEvent, (SNTStoredEvent *));
|
||||
MOCK_METHOD(std::vector<uint8_t>, SerializeDiskAppeared, (NSDictionary *));
|
||||
MOCK_METHOD(std::vector<uint8_t>, SerializeDiskDisappeared, (NSDictionary *));
|
||||
};
|
||||
|
||||
class MockWriter : public Null {
|
||||
public:
|
||||
MOCK_METHOD(void, Write, (std::vector<uint8_t> && bytes));
|
||||
};
|
||||
|
||||
@interface LoggerTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation LoggerTest
|
||||
|
||||
- (void)testCreate {
|
||||
// Ensure that the factory method creates expected serializers/writers pairs
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
XCTAssertEqual(nullptr, Logger::Create(mockESApi, (SNTEventLogType)123, @"/tmp/temppy",
|
||||
@"/tmp/spool", 1, 1, 1));
|
||||
|
||||
LoggerPeer logger(
|
||||
Logger::Create(mockESApi, SNTEventLogTypeFilelog, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<BasicString>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<File>(logger.Writer()));
|
||||
|
||||
logger = LoggerPeer(
|
||||
Logger::Create(mockESApi, SNTEventLogTypeSyslog, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<BasicString>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Syslog>(logger.Writer()));
|
||||
|
||||
logger = LoggerPeer(
|
||||
Logger::Create(mockESApi, SNTEventLogTypeNull, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Empty>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Null>(logger.Writer()));
|
||||
|
||||
logger = LoggerPeer(
|
||||
Logger::Create(mockESApi, SNTEventLogTypeProtobuf, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Protobuf>(logger.Serializer()));
|
||||
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Spool>(logger.Writer()));
|
||||
}
|
||||
|
||||
- (void)testLog {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto mockSerializer = std::make_shared<MockSerializer>();
|
||||
auto mockWriter = std::make_shared<MockWriter>();
|
||||
|
||||
// Ensure all Logger::Log* methods call the serializer followed by the writer
|
||||
es_message_t msg;
|
||||
|
||||
// Note: In this test, `RetainMessage` isn't setup to return anything. This
|
||||
// means that the underlying `es_msg_` in the `Message` object is NULL, and
|
||||
// therefore no call to `ReleaseMessage` is ever made (hence no expectations).
|
||||
// Because we don't need to operate on the es_msg_, this simplifies the test.
|
||||
EXPECT_CALL(*mockESApi, RetainMessage);
|
||||
auto enrichedMsg = std::make_shared<EnrichedMessage>(
|
||||
EnrichedClose(Message(mockESApi, &msg),
|
||||
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
|
||||
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
|
||||
|
||||
EXPECT_CALL(*mockSerializer, SerializeMessage(testing::A<const EnrichedClose &>())).Times(1);
|
||||
EXPECT_CALL(*mockWriter, Write).Times(1);
|
||||
|
||||
Logger(mockSerializer, mockWriter).Log(enrichedMsg);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
}
|
||||
|
||||
- (void)testLogAllowList {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto mockSerializer = std::make_shared<MockSerializer>();
|
||||
auto mockWriter = std::make_shared<MockWriter>();
|
||||
es_message_t msg;
|
||||
std::string_view hash = "this_is_my_test_hash";
|
||||
|
||||
EXPECT_CALL(*mockESApi, RetainMessage);
|
||||
EXPECT_CALL(*mockSerializer, SerializeAllowlist(testing::_, hash));
|
||||
EXPECT_CALL(*mockWriter, Write);
|
||||
|
||||
Logger(mockSerializer, mockWriter).LogAllowlist(Message(mockESApi, &msg), hash);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
}
|
||||
|
||||
- (void)testLogBundleHashingEvents {
|
||||
auto mockSerializer = std::make_shared<MockSerializer>();
|
||||
auto mockWriter = std::make_shared<MockWriter>();
|
||||
NSArray<id> *events = @[ @"event1", @"event2", @"event3" ];
|
||||
|
||||
EXPECT_CALL(*mockSerializer, SerializeBundleHashingEvent).Times((int)[events count]);
|
||||
EXPECT_CALL(*mockWriter, Write).Times((int)[events count]);
|
||||
|
||||
Logger(mockSerializer, mockWriter).LogBundleHashingEvents(events);
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
}
|
||||
|
||||
- (void)testLogDiskAppeared {
|
||||
auto mockSerializer = std::make_shared<MockSerializer>();
|
||||
auto mockWriter = std::make_shared<MockWriter>();
|
||||
|
||||
EXPECT_CALL(*mockSerializer, SerializeDiskAppeared);
|
||||
EXPECT_CALL(*mockWriter, Write);
|
||||
|
||||
Logger(mockSerializer, mockWriter).LogDiskAppeared(@{@"key" : @"value"});
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
}
|
||||
|
||||
- (void)testLogDiskDisappeared {
|
||||
auto mockSerializer = std::make_shared<MockSerializer>();
|
||||
auto mockWriter = std::make_shared<MockWriter>();
|
||||
|
||||
EXPECT_CALL(*mockSerializer, SerializeDiskDisappeared);
|
||||
EXPECT_CALL(*mockWriter, Write);
|
||||
|
||||
Logger(mockSerializer, mockWriter).LogDiskDisappeared(@{@"key" : @"value"});
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,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__LOGS_ENDPOINTSECURITY_SERIALIZERS_BASICSTRING_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_BASICSTRING_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
class BasicString : public Serializer {
|
||||
public:
|
||||
static std::shared_ptr<BasicString> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
bool prefix_time_name = true);
|
||||
|
||||
BasicString(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
bool prefix_time_name);
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedFork &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedLink &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeAllowlist(
|
||||
const santa::santad::event_providers::endpoint_security::Message &,
|
||||
const std::string_view) override;
|
||||
|
||||
std::vector<uint8_t> SerializeBundleHashingEvent(SNTStoredEvent *) override;
|
||||
|
||||
std::vector<uint8_t> SerializeDiskAppeared(NSDictionary *) override;
|
||||
std::vector<uint8_t> SerializeDiskDisappeared(NSDictionary *) override;
|
||||
|
||||
private:
|
||||
std::string CreateDefaultString(size_t reserved_size = 512);
|
||||
std::vector<uint8_t> FinalizeString(std::string &str);
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
bool prefix_time_name_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
#endif
|
||||
476
Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm
Normal file
476
Source/santad/Logs/EndpointSecurity/Serializers/BasicString.mm
Normal file
@@ -0,0 +1,476 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
|
||||
|
||||
#import <Security/Security.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <libgen.h>
|
||||
#include <mach/message.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/kauth.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExit;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFork;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedLink;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedRename;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::NonNull;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pid;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pidversion;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::RealGroup;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::RealUser;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
static inline SanitizableString FilePath(const es_file_t *file) {
|
||||
return SanitizableString(file);
|
||||
}
|
||||
|
||||
static NSDateFormatter *GetDateFormatter() {
|
||||
static dispatch_once_t onceToken;
|
||||
static NSDateFormatter *dateFormatter;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
dateFormatter = [[NSDateFormatter alloc] init];
|
||||
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
|
||||
dateFormatter.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601];
|
||||
dateFormatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
|
||||
});
|
||||
|
||||
return dateFormatter;
|
||||
}
|
||||
|
||||
std::string GetDecisionString(SNTEventState event_state) {
|
||||
if (event_state & SNTEventStateAllow) {
|
||||
return "ALLOW";
|
||||
} else if (event_state & SNTEventStateBlock) {
|
||||
return "DENY";
|
||||
} else {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
std::string GetReasonString(SNTEventState event_state) {
|
||||
switch (event_state) {
|
||||
case SNTEventStateAllowBinary: return "BINARY";
|
||||
case SNTEventStateAllowCompiler: return "COMPILER";
|
||||
case SNTEventStateAllowTransitive: return "TRANSITIVE";
|
||||
case SNTEventStateAllowPendingTransitive: return "PENDING_TRANSITIVE";
|
||||
case SNTEventStateAllowCertificate: return "CERT";
|
||||
case SNTEventStateAllowScope: return "SCOPE";
|
||||
case SNTEventStateAllowTeamID: return "TEAMID";
|
||||
case SNTEventStateAllowUnknown: return "UNKNOWN";
|
||||
case SNTEventStateBlockBinary: return "BINARY";
|
||||
case SNTEventStateBlockCertificate: return "CERT";
|
||||
case SNTEventStateBlockScope: return "SCOPE";
|
||||
case SNTEventStateBlockTeamID: return "TEAMID";
|
||||
case SNTEventStateBlockLongPath: return "LONG_PATH";
|
||||
case SNTEventStateBlockUnknown: return "UNKNOWN";
|
||||
default: return "NOTRUNNING";
|
||||
}
|
||||
}
|
||||
|
||||
std::string GetModeString(SNTClientMode mode) {
|
||||
switch (mode) {
|
||||
case SNTClientModeMonitor: return "M";
|
||||
case SNTClientModeLockdown: return "L";
|
||||
default: return "U";
|
||||
}
|
||||
}
|
||||
|
||||
static inline void AppendProcess(std::string &str, const es_process_t *es_proc) {
|
||||
char bname[MAXPATHLEN];
|
||||
str.append("|pid=");
|
||||
str.append(std::to_string(Pid(es_proc->audit_token)));
|
||||
str.append("|ppid=");
|
||||
str.append(std::to_string(es_proc->original_ppid));
|
||||
str.append("|process=");
|
||||
str.append(basename_r(FilePath(es_proc->executable).Sanitized().data(), bname) ?: "");
|
||||
str.append("|processpath=");
|
||||
str.append(FilePath(es_proc->executable).Sanitized());
|
||||
}
|
||||
|
||||
static inline void AppendUserGroup(std::string &str, const audit_token_t &tok,
|
||||
const std::optional<std::shared_ptr<std::string>> &user,
|
||||
const std::optional<std::shared_ptr<std::string>> &group) {
|
||||
str.append("|uid=");
|
||||
str.append(std::to_string(RealUser(tok)));
|
||||
str.append("|user=");
|
||||
str.append(user.has_value() ? user->get()->c_str() : "(null)");
|
||||
str.append("|gid=");
|
||||
str.append(std::to_string(RealGroup(tok)));
|
||||
str.append("|group=");
|
||||
str.append(group.has_value() ? group->get()->c_str() : "(null)");
|
||||
}
|
||||
|
||||
static char *FormattedDateString(char *buf, size_t len) {
|
||||
struct timeval tv;
|
||||
struct tm tm;
|
||||
|
||||
gettimeofday(&tv, NULL);
|
||||
gmtime_r(&tv.tv_sec, &tm);
|
||||
|
||||
strftime(buf, len, "%Y-%m-%dT%H:%M:%S", &tm);
|
||||
snprintf(buf, len, "%s.%03dZ", buf, tv.tv_usec / 1000);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::shared_ptr<BasicString> BasicString::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
bool prefix_time_name) {
|
||||
return std::make_shared<BasicString>(esapi, prefix_time_name);
|
||||
}
|
||||
|
||||
BasicString::BasicString(std::shared_ptr<EndpointSecurityAPI> esapi, bool prefix_time_name)
|
||||
: esapi_(esapi), prefix_time_name_(prefix_time_name) {}
|
||||
|
||||
std::string BasicString::CreateDefaultString(size_t reserved_size) {
|
||||
std::string str;
|
||||
str.reserve(1024);
|
||||
|
||||
if (prefix_time_name_) {
|
||||
char buf[32];
|
||||
|
||||
str.append("[");
|
||||
str.append(FormattedDateString(buf, sizeof(buf)));
|
||||
str.append("] I santad: ");
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::FinalizeString(std::string &str) {
|
||||
if (EnabledMachineID()) {
|
||||
str.append("|machineid=");
|
||||
str.append(MachineID());
|
||||
}
|
||||
str.append("\n");
|
||||
|
||||
std::vector<uint8_t> vec(str.length());
|
||||
std::copy(str.begin(), str.end(), vec.begin());
|
||||
return vec;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedClose &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=WRITE|path=");
|
||||
str.append(FilePath(esm.event.close.target).Sanitized());
|
||||
|
||||
AppendProcess(str, esm.process);
|
||||
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExchange &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=EXCHANGE|path=");
|
||||
str.append(FilePath(esm.event.exchangedata.file1).Sanitized());
|
||||
str.append("|newpath=");
|
||||
str.append(FilePath(esm.event.exchangedata.file2).Sanitized());
|
||||
|
||||
AppendProcess(str, esm.process);
|
||||
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString(1024); // EXECs tend to be bigger, reserve more space.
|
||||
|
||||
SNTCachedDecision *cd =
|
||||
[[SNTDecisionCache sharedCache] cachedDecisionForFile:esm.event.exec.target->executable->stat];
|
||||
|
||||
str.append("action=EXEC|decision=");
|
||||
str.append(GetDecisionString(cd.decision));
|
||||
str.append("|reason=");
|
||||
str.append(GetReasonString(cd.decision));
|
||||
|
||||
if (cd.decisionExtra) {
|
||||
str.append("|explain=");
|
||||
str.append([cd.decisionExtra UTF8String]);
|
||||
}
|
||||
|
||||
if (cd.sha256) {
|
||||
str.append("|sha256=");
|
||||
str.append([cd.sha256 UTF8String]);
|
||||
}
|
||||
|
||||
if (cd.certSHA256) {
|
||||
str.append("|cert_sha256=");
|
||||
str.append([cd.certSHA256 UTF8String]);
|
||||
str.append("|cert_cn=");
|
||||
str.append(SanitizableString(cd.certCommonName).Sanitized());
|
||||
}
|
||||
|
||||
if (cd.teamID.length) {
|
||||
str.append("|teamid=");
|
||||
str.append([NonNull(cd.teamID) UTF8String]);
|
||||
}
|
||||
|
||||
if (cd.quarantineURL) {
|
||||
str.append("|quarantine_url=");
|
||||
str.append(SanitizableString(cd.quarantineURL).Sanitized());
|
||||
}
|
||||
|
||||
str.append("|pid=");
|
||||
str.append(std::to_string(Pid(esm.event.exec.target->audit_token)));
|
||||
str.append("|pidversion=");
|
||||
str.append(std::to_string(Pidversion(esm.event.exec.target->audit_token)));
|
||||
str.append("|ppid=");
|
||||
str.append(std::to_string(esm.event.exec.target->original_ppid));
|
||||
|
||||
AppendUserGroup(str, esm.event.exec.target->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
|
||||
str.append("|mode=");
|
||||
str.append(GetModeString([[SNTConfigurator configurator] clientMode]));
|
||||
str.append("|path=");
|
||||
str.append(FilePath(esm.event.exec.target->executable).Sanitized());
|
||||
|
||||
NSString *origPath = Utilities::OriginalPathForTranslocation(esm.event.exec.target);
|
||||
if (origPath) {
|
||||
str.append("|origpath=");
|
||||
str.append(SanitizableString(origPath).Sanitized());
|
||||
}
|
||||
|
||||
uint32_t argCount = esapi_->ExecArgCount(&esm.event.exec);
|
||||
if (argCount > 0) {
|
||||
str.append("|args=");
|
||||
for (uint32_t i = 0; i < argCount; i++) {
|
||||
if (i != 0) {
|
||||
str.append(" ");
|
||||
}
|
||||
|
||||
str.append(SanitizableString(esapi_->ExecArg(&esm.event.exec, i)).Sanitized());
|
||||
}
|
||||
}
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExit &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=EXIT|pid=");
|
||||
str.append(std::to_string(Pid(esm.process->audit_token)));
|
||||
str.append("|pidversion=");
|
||||
str.append(std::to_string(Pidversion(esm.process->audit_token)));
|
||||
str.append("|ppid=");
|
||||
str.append(std::to_string(esm.process->original_ppid));
|
||||
str.append("|uid=");
|
||||
str.append(std::to_string(RealUser(esm.process->audit_token)));
|
||||
str.append("|gid=");
|
||||
str.append(std::to_string(RealGroup(esm.process->audit_token)));
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedFork &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=FORK|pid=");
|
||||
str.append(std::to_string(Pid(esm.event.fork.child->audit_token)));
|
||||
str.append("|pidversion=");
|
||||
str.append(std::to_string(Pidversion(esm.event.fork.child->audit_token)));
|
||||
str.append("|ppid=");
|
||||
str.append(std::to_string(esm.event.fork.child->original_ppid));
|
||||
str.append("|uid=");
|
||||
str.append(std::to_string(RealUser(esm.event.fork.child->audit_token)));
|
||||
str.append("|gid=");
|
||||
str.append(std::to_string(RealGroup(esm.event.fork.child->audit_token)));
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedLink &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=LINK|path=");
|
||||
str.append(FilePath(esm.event.link.source).Sanitized());
|
||||
str.append("|newpath=");
|
||||
str.append(FilePath(esm.event.link.target_dir).Sanitized());
|
||||
str.append("/");
|
||||
str.append(SanitizableString(esm.event.link.target_filename).Sanitized());
|
||||
|
||||
AppendProcess(str, esm.process);
|
||||
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedRename &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=RENAME|path=");
|
||||
str.append(FilePath(esm.event.rename.source).Sanitized());
|
||||
str.append("|newpath=");
|
||||
|
||||
switch (esm.event.rename.destination_type) {
|
||||
case ES_DESTINATION_TYPE_EXISTING_FILE:
|
||||
str.append(FilePath(esm.event.rename.destination.existing_file).Sanitized());
|
||||
break;
|
||||
case ES_DESTINATION_TYPE_NEW_PATH:
|
||||
str.append(FilePath(esm.event.rename.destination.new_path.dir).Sanitized());
|
||||
str.append("/");
|
||||
str.append(SanitizableString(esm.event.rename.destination.new_path.filename).Sanitized());
|
||||
break;
|
||||
default: str.append("(null)"); break;
|
||||
}
|
||||
|
||||
AppendProcess(str, esm.process);
|
||||
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
const es_message_t &esm = msg.es_msg();
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=DELETE|path=");
|
||||
str.append(FilePath(esm.event.unlink.target).Sanitized());
|
||||
|
||||
AppendProcess(str, esm.process);
|
||||
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
|
||||
msg.instigator().real_group());
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeAllowlist(const Message &msg,
|
||||
const std::string_view hash) {
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=ALLOWLIST|pid=");
|
||||
str.append(std::to_string(Pid(msg->process->audit_token)));
|
||||
str.append("|pidversion=");
|
||||
str.append(std::to_string(Pidversion(msg->process->audit_token)));
|
||||
str.append("|path=");
|
||||
str.append(FilePath(Utilities::GetAllowListTargetFile(msg)).Sanitized());
|
||||
str.append("|sha256=");
|
||||
str.append(hash);
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeBundleHashingEvent(SNTStoredEvent *event) {
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=BUNDLE|sha256=");
|
||||
str.append([NonNull(event.fileSHA256) UTF8String]);
|
||||
str.append("|bundlehash=");
|
||||
str.append([NonNull(event.fileBundleHash) UTF8String]);
|
||||
str.append("|bundlename=");
|
||||
str.append([NonNull(event.fileBundleName) UTF8String]);
|
||||
str.append("|bundleid=");
|
||||
str.append([NonNull(event.fileBundleID) UTF8String]);
|
||||
str.append("|bundlepath=");
|
||||
str.append([NonNull(event.fileBundlePath) UTF8String]);
|
||||
str.append("|path=");
|
||||
str.append([NonNull(event.filePath) UTF8String]);
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeDiskAppeared(NSDictionary *props) {
|
||||
NSString *dmg_path = nil;
|
||||
NSString *serial = nil;
|
||||
if ([props[@"DADeviceModel"] isEqual:@"Disk Image"]) {
|
||||
dmg_path = Utilities::DiskImageForDevice(props[@"DADevicePath"]);
|
||||
} else {
|
||||
serial = Utilities::SerialForDevice(props[@"DADevicePath"]);
|
||||
}
|
||||
|
||||
NSString *model = [NSString
|
||||
stringWithFormat:@"%@ %@", NonNull(props[@"DADeviceVendor"]), NonNull(props[@"DADeviceModel"])];
|
||||
model = [model stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
|
||||
NSString *appearanceDateString = [GetDateFormatter()
|
||||
stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:[props[@"DAAppearanceTime"]
|
||||
doubleValue]]];
|
||||
|
||||
std::string str = CreateDefaultString();
|
||||
str.append("action=DISKAPPEAR");
|
||||
str.append("|mount=");
|
||||
str.append([NonNull([props[@"DAVolumePath"] path]) UTF8String]);
|
||||
str.append("|volume=");
|
||||
str.append([NonNull(props[@"DAVolumeName"]) UTF8String]);
|
||||
str.append("|bsdname=");
|
||||
str.append([NonNull(props[@"DAMediaBSDName"]) UTF8String]);
|
||||
str.append("|fs=");
|
||||
str.append([NonNull(props[@"DAVolumeKind"]) UTF8String]);
|
||||
str.append("|model=");
|
||||
str.append([NonNull(model) UTF8String]);
|
||||
str.append("|serial=");
|
||||
str.append([NonNull(serial) UTF8String]);
|
||||
str.append("|bus=");
|
||||
str.append([NonNull(props[@"DADeviceProtocol"]) UTF8String]);
|
||||
str.append("|dmgpath=");
|
||||
str.append([NonNull(dmg_path) UTF8String]);
|
||||
str.append("|appearance=");
|
||||
str.append([NonNull(appearanceDateString) UTF8String]);
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BasicString::SerializeDiskDisappeared(NSDictionary *props) {
|
||||
std::string str = CreateDefaultString();
|
||||
|
||||
str.append("action=DISKDISAPPEAR");
|
||||
str.append("|mount=");
|
||||
str.append([NonNull([props[@"DAVolumePath"] path]) UTF8String]);
|
||||
str.append("|volume=");
|
||||
str.append([NonNull(props[@"DAVolumeName"]) UTF8String]);
|
||||
str.append("|bsdname=");
|
||||
str.append([NonNull(props[@"DAMediaBSDName"]) UTF8String]);
|
||||
|
||||
return FinalizeString(str);
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
@@ -0,0 +1,394 @@
|
||||
/// 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 <Foundation/Foundation.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::BasicString;
|
||||
using santa::santad::logs::endpoint_security::serializers::Serializer;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
extern std::string GetDecisionString(SNTEventState event_state);
|
||||
extern std::string GetReasonString(SNTEventState event_state);
|
||||
extern std::string GetModeString(SNTClientMode mode);
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
using santa::santad::logs::endpoint_security::serializers::GetDecisionString;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetModeString;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetReasonString;
|
||||
|
||||
std::string BasicStringSerializeMessage(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(esMsg);
|
||||
|
||||
std::shared_ptr<Serializer> bs = BasicString::Create(mockESApi, false);
|
||||
std::vector<uint8_t> ret = bs->SerializeMessage(Enricher().Enrich(Message(mockESApi, esMsg)));
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
return std::string(ret.begin(), ret.end());
|
||||
}
|
||||
|
||||
std::string BasicStringSerializeMessage(es_message_t *esMsg) {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
return BasicStringSerializeMessage(mockESApi, esMsg);
|
||||
}
|
||||
|
||||
@interface BasicStringTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property id mockDecisionCache;
|
||||
|
||||
@property SNTCachedDecision *testCachedDecision;
|
||||
@end
|
||||
|
||||
@implementation BasicStringTest
|
||||
|
||||
- (void)setUp {
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(YES);
|
||||
OCMStub([self.mockConfigurator machineID]).andReturn(@"my_id");
|
||||
|
||||
self.testCachedDecision = [[SNTCachedDecision alloc] init];
|
||||
self.testCachedDecision.decision = SNTEventStateAllowBinary;
|
||||
self.testCachedDecision.decisionExtra = @"extra!";
|
||||
self.testCachedDecision.sha256 = @"1234_hash";
|
||||
self.testCachedDecision.quarantineURL = @"google.com";
|
||||
self.testCachedDecision.certSHA256 = @"5678_hash";
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
OCMStub([self.mockDecisionCache cachedDecisionForFile:{}])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(self.testCachedDecision);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.mockConfigurator stopMocking];
|
||||
[self.mockDecisionCache stopMocking];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageClose {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_file_t file = MakeESFile("close_file");
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
|
||||
esMsg.event.close.modified = true;
|
||||
esMsg.event.close.target = &file;
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=WRITE|path=close_file"
|
||||
"|pid=12|ppid=56|process=foo|processpath=foo"
|
||||
"|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExchange {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_file_t file1 = MakeESFile("exchange_1");
|
||||
es_file_t file2 = MakeESFile("exchange_2");
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA, &proc);
|
||||
esMsg.event.exchangedata.file1 = &file1;
|
||||
esMsg.event.exchangedata.file2 = &file2;
|
||||
|
||||
// Arbitrarily overwriting mock to test not adding machine id in this event
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(NO);
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=EXCHANGE|path=exchange_1|newpath=exchange_2"
|
||||
"|pid=12|ppid=56|process=foo|processpath=foo"
|
||||
"|uid=-2|user=nobody|gid=-1|group=nogroup\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExec {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
|
||||
es_file_t execFile = MakeESFile("execpath|");
|
||||
es_process_t procExec = MakeESProcess(&execFile, MakeAuditToken(12, 89), MakeAuditToken(56, 78));
|
||||
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXEC, &proc);
|
||||
esMsg.event.exec.target = &procExec;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
EXPECT_CALL(*mockESApi, ExecArgCount).WillOnce(testing::Return(3));
|
||||
|
||||
EXPECT_CALL(*mockESApi, ExecArg)
|
||||
.WillOnce(testing::Return(es_string_token_t{9, "exec|path"}))
|
||||
.WillOnce(testing::Return(es_string_token_t{5, "-l\n-t"}))
|
||||
.WillOnce(testing::Return(es_string_token_t{8, "-v\r--foo"}));
|
||||
|
||||
std::string got = BasicStringSerializeMessage(mockESApi, &esMsg);
|
||||
std::string want =
|
||||
"action=EXEC|decision=ALLOW|reason=BINARY|explain=extra!|sha256=1234_hash|"
|
||||
"cert_sha256=5678_hash|cert_cn=|quarantine_url=google.com|pid=12|pidversion="
|
||||
"89|ppid=56|uid=-2|user=nobody|gid=-1|group=nogroup|mode=L|path=execpath<pipe>|"
|
||||
"args=exec<pipe>path -l\\n-t -v\\r--foo|machineid=my_id\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExit {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=EXIT|pid=12|pidversion=34|ppid=56|uid=-2|gid=-1|machineid=my_id\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageFork {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_file_t procChildFile = MakeESFile("foo_child");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_process_t procChild =
|
||||
MakeESProcess(&procChildFile, MakeAuditToken(67, 89), MakeAuditToken(12, 34));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_FORK, &proc);
|
||||
esMsg.event.fork.child = &procChild;
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=FORK|pid=67|pidversion=89|ppid=12|uid=-2|gid=-1|machineid=my_id\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageLink {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_file_t srcFile = MakeESFile("link_src");
|
||||
es_file_t dstDir = MakeESFile("link_dst");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_LINK, &proc);
|
||||
esMsg.event.link.source = &srcFile;
|
||||
esMsg.event.link.target_dir = &dstDir;
|
||||
esMsg.event.link.target_filename = MakeESStringToken("link_name");
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=LINK|path=link_src|newpath=link_dst/link_name"
|
||||
"|pid=12|ppid=56|process=foo|processpath=foo"
|
||||
"|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageRename {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_file_t srcFile = MakeESFile("rename_src");
|
||||
es_file_t dstFile = MakeESFile("rename_dst");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &proc);
|
||||
esMsg.event.rename.source = &srcFile;
|
||||
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
|
||||
esMsg.event.rename.destination.existing_file = &dstFile;
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=RENAME|path=rename_src|newpath=rename_dst"
|
||||
"|pid=12|ppid=56|process=foo|processpath=foo"
|
||||
"|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageUnlink {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_file_t targetFile = MakeESFile("deleted_file");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_UNLINK, &proc);
|
||||
esMsg.event.unlink.target = &targetFile;
|
||||
|
||||
std::string got = BasicStringSerializeMessage(&esMsg);
|
||||
std::string want = "action=DELETE|path=deleted_file"
|
||||
"|pid=12|ppid=56|process=foo|processpath=foo"
|
||||
"|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeAllowlist {
|
||||
es_file_t file = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&file, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
|
||||
esMsg.event.close.target = &file;
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(mockESApi, false)
|
||||
->SerializeAllowlist(Message(mockESApi, &esMsg), "test_hash");
|
||||
|
||||
XCTAssertTrue(testing::Mock::VerifyAndClearExpectations(mockESApi.get()),
|
||||
"Expected calls were not properly mocked");
|
||||
|
||||
std::string got(ret.begin(), ret.end());
|
||||
std::string want = "action=ALLOWLIST|pid=12|pidversion=34|path=foo"
|
||||
"|sha256=test_hash|machineid=my_id\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeBundleHashingEvent {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
|
||||
se.fileSHA256 = @"file_hash";
|
||||
se.fileBundleHash = @"file_bundle_hash";
|
||||
se.fileBundleName = @"file_bundle_Name";
|
||||
se.fileBundleID = nil;
|
||||
se.fileBundlePath = @"file_bundle_path";
|
||||
se.filePath = @"file_path";
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeBundleHashingEvent(se);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
|
||||
std::string want = "action=BUNDLE|sha256=file_hash"
|
||||
"|bundlehash=file_bundle_hash|bundlename=file_bundle_Name|bundleid="
|
||||
"|bundlepath=file_bundle_path|path=file_path|machineid=my_id\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeDiskAppeared {
|
||||
NSDictionary *props = @{
|
||||
@"DADevicePath" : @"",
|
||||
@"DADeviceVendor" : @"vendor",
|
||||
@"DADeviceModel" : @"model",
|
||||
@"DAAppearanceTime" : @(1252487349), // 2009-09-09 09:09:09
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"path"],
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
@"DAVolumeKind" : @"apfs",
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
};
|
||||
|
||||
// Arbitrarily overwriting mock to test not adding machine id in this event
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(NO);
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeDiskAppeared(props);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
|
||||
std::string want = "action=DISKAPPEAR|mount=path|volume=|bsdname=bsd|fs=apfs"
|
||||
"|model=vendor model|serial=|bus=usb|dmgpath="
|
||||
"|appearance=2040-09-09T09:09:09.000Z\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testSerializeDiskDisappeared {
|
||||
NSDictionary *props = @{
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"path"],
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeDiskDisappeared(props);
|
||||
std::string got(ret.begin(), ret.end());
|
||||
|
||||
std::string want = "action=DISKDISAPPEAR|mount=path|volume=|bsdname=bsd|machineid=my_id\n";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
- (void)testGetDecisionString {
|
||||
std::map<SNTEventState, std::string> stateToDecision = {
|
||||
{SNTEventStateUnknown, "UNKNOWN"},
|
||||
{SNTEventStateBundleBinary, "UNKNOWN"},
|
||||
{SNTEventStateBlockUnknown, "DENY"},
|
||||
{SNTEventStateBlockBinary, "DENY"},
|
||||
{SNTEventStateBlockCertificate, "DENY"},
|
||||
{SNTEventStateBlockScope, "DENY"},
|
||||
{SNTEventStateBlockTeamID, "DENY"},
|
||||
{SNTEventStateBlockLongPath, "DENY"},
|
||||
{SNTEventStateAllowUnknown, "ALLOW"},
|
||||
{SNTEventStateAllowBinary, "ALLOW"},
|
||||
{SNTEventStateAllowCertificate, "ALLOW"},
|
||||
{SNTEventStateAllowScope, "ALLOW"},
|
||||
{SNTEventStateAllowCompiler, "ALLOW"},
|
||||
{SNTEventStateAllowTransitive, "ALLOW"},
|
||||
{SNTEventStateAllowPendingTransitive, "ALLOW"},
|
||||
{SNTEventStateAllowTeamID, "ALLOW"},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToDecision) {
|
||||
XCTAssertCppStringEqual(GetDecisionString(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testGetReasonString {
|
||||
std::map<SNTEventState, std::string> stateToReason = {
|
||||
{SNTEventStateUnknown, "NOTRUNNING"},
|
||||
{SNTEventStateBundleBinary, "NOTRUNNING"},
|
||||
{SNTEventStateBlockUnknown, "UNKNOWN"},
|
||||
{SNTEventStateBlockBinary, "BINARY"},
|
||||
{SNTEventStateBlockCertificate, "CERT"},
|
||||
{SNTEventStateBlockScope, "SCOPE"},
|
||||
{SNTEventStateBlockTeamID, "TEAMID"},
|
||||
{SNTEventStateBlockLongPath, "LONG_PATH"},
|
||||
{SNTEventStateAllowUnknown, "UNKNOWN"},
|
||||
{SNTEventStateAllowBinary, "BINARY"},
|
||||
{SNTEventStateAllowCertificate, "CERT"},
|
||||
{SNTEventStateAllowScope, "SCOPE"},
|
||||
{SNTEventStateAllowCompiler, "COMPILER"},
|
||||
{SNTEventStateAllowTransitive, "TRANSITIVE"},
|
||||
{SNTEventStateAllowPendingTransitive, "PENDING_TRANSITIVE"},
|
||||
{SNTEventStateAllowTeamID, "TEAMID"},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToReason) {
|
||||
XCTAssertCppStringEqual(GetReasonString(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testGetModeString {
|
||||
std::map<SNTClientMode, std::string> modeToString = {
|
||||
{SNTClientModeMonitor, "M"},
|
||||
{SNTClientModeLockdown, "L"},
|
||||
{(SNTClientMode)123, "U"},
|
||||
};
|
||||
|
||||
for (const auto &kv : modeToString) {
|
||||
XCTAssertCppStringEqual(GetModeString(kv.first), kv.second);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
60
Source/santad/Logs/EndpointSecurity/Serializers/Empty.h
Normal file
60
Source/santad/Logs/EndpointSecurity/Serializers/Empty.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_EMPTY_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_EMPTY_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
class Empty : public Serializer {
|
||||
public:
|
||||
static std::shared_ptr<Empty> Create();
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedFork &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedLink &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeAllowlist(
|
||||
const santa::santad::event_providers::endpoint_security::Message &,
|
||||
const std::string_view) override;
|
||||
|
||||
std::vector<uint8_t> SerializeBundleHashingEvent(SNTStoredEvent *) override;
|
||||
|
||||
std::vector<uint8_t> SerializeDiskAppeared(NSDictionary *) override;
|
||||
std::vector<uint8_t> SerializeDiskDisappeared(NSDictionary *) override;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
#endif
|
||||
81
Source/santad/Logs/EndpointSecurity/Serializers/Empty.mm
Normal file
81
Source/santad/Logs/EndpointSecurity/Serializers/Empty.mm
Normal file
@@ -0,0 +1,81 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExit;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFork;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedLink;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedRename;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
std::shared_ptr<Empty> Empty::Create() {
|
||||
return std::make_shared<Empty>();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedClose &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExchange &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExec &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExit &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedFork &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedLink &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedRename &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeAllowlist(const Message &msg, const std::string_view hash) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeBundleHashingEvent(SNTStoredEvent *event) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeDiskAppeared(NSDictionary *props) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Empty::SerializeDiskDisappeared(NSDictionary *props) {
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
53
Source/santad/Logs/EndpointSecurity/Serializers/EmptyTest.mm
Normal file
53
Source/santad/Logs/EndpointSecurity/Serializers/EmptyTest.mm
Normal file
@@ -0,0 +1,53 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
|
||||
|
||||
using santa::santad::logs::endpoint_security::serializers::Empty;
|
||||
|
||||
namespace es = santa::santad::event_providers::endpoint_security;
|
||||
|
||||
@interface EmptyTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation EmptyTest
|
||||
|
||||
- (void)testAllSerializersReturnEmptyVector {
|
||||
std::shared_ptr<Empty> e = Empty::Create();
|
||||
|
||||
// We can get away with passing a fake argument to the `Serialize*` methods
|
||||
// instead of constructing real ones since the Empty class never touches the
|
||||
// input parameter.
|
||||
int fake;
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedClose *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExchange *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExec *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExit *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedFork *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedLink *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedRename *)&fake).size(), 0);
|
||||
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedUnlink *)&fake).size(), 0);
|
||||
|
||||
XCTAssertEqual(e->SerializeAllowlist(*(es::Message *)&fake, "").size(), 0);
|
||||
XCTAssertEqual(e->SerializeBundleHashingEvent(nil).size(), 0);
|
||||
XCTAssertEqual(e->SerializeDiskAppeared(nil).size(), 0);
|
||||
XCTAssertEqual(e->SerializeDiskDisappeared(nil).size(), 0);
|
||||
}
|
||||
|
||||
@end
|
||||
80
Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h
Normal file
80
Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h
Normal file
@@ -0,0 +1,80 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_PROTOBUF_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_PROTOBUF_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <google/protobuf/arena.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/common/santa_proto_include_wrapper.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
class Protobuf : public Serializer {
|
||||
public:
|
||||
static std::shared_ptr<Protobuf> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi);
|
||||
|
||||
Protobuf(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi);
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedFork &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedLink &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeAllowlist(
|
||||
const santa::santad::event_providers::endpoint_security::Message &,
|
||||
const std::string_view) override;
|
||||
|
||||
std::vector<uint8_t> SerializeBundleHashingEvent(SNTStoredEvent *) override;
|
||||
|
||||
std::vector<uint8_t> SerializeDiskAppeared(NSDictionary *) override;
|
||||
std::vector<uint8_t> SerializeDiskDisappeared(NSDictionary *) override;
|
||||
|
||||
private:
|
||||
::santa::pb::v1::SantaMessage *CreateDefaultProto(google::protobuf::Arena *arena);
|
||||
::santa::pb::v1::SantaMessage *CreateDefaultProto(
|
||||
google::protobuf::Arena *arena,
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedEventType &msg);
|
||||
::santa::pb::v1::SantaMessage *CreateDefaultProto(google::protobuf::Arena *arena,
|
||||
struct timespec event_time,
|
||||
struct timespec processed_time);
|
||||
|
||||
std::vector<uint8_t> FinalizeProto(::santa::pb::v1::SantaMessage *santa_msg);
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
#endif
|
||||
630
Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm
Normal file
630
Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm
Normal file
@@ -0,0 +1,630 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <mach/message.h>
|
||||
#include <math.h>
|
||||
#include <sys/proc_info.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
#include "google/protobuf/timestamp.pb.h"
|
||||
|
||||
using google::protobuf::Arena;
|
||||
using google::protobuf::Timestamp;
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExit;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFile;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFork;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedLink;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedProcess;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedRename;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::EffectiveGroup;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::EffectiveUser;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::NonNull;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pid;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pidversion;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::RealGroup;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::RealUser;
|
||||
|
||||
namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi) {
|
||||
return std::make_shared<Protobuf>(esapi);
|
||||
}
|
||||
|
||||
Protobuf::Protobuf(std::shared_ptr<EndpointSecurityAPI> esapi) : esapi_(esapi) {}
|
||||
|
||||
static inline void EncodeTimestamp(Timestamp *timestamp, struct timespec ts) {
|
||||
timestamp->set_seconds(ts.tv_sec);
|
||||
timestamp->set_nanos((int32_t)ts.tv_nsec);
|
||||
}
|
||||
|
||||
static inline void EncodeTimestamp(Timestamp *timestamp, struct timeval tv) {
|
||||
EncodeTimestamp(timestamp, (struct timespec){tv.tv_sec, tv.tv_usec * 1000});
|
||||
}
|
||||
|
||||
static inline void EncodeProcessID(pbv1::ProcessID *proc_id, const audit_token_t &tok) {
|
||||
proc_id->set_pid(Pid(tok));
|
||||
proc_id->set_pidversion(Pidversion(tok));
|
||||
}
|
||||
|
||||
static inline void EncodePath(std::string *buf, const es_file_t *dir,
|
||||
const es_string_token_t file) {
|
||||
buf->append(std::string_view(dir->path.data, dir->path.length));
|
||||
buf->append("/");
|
||||
buf->append(std::string_view(file.data, file.length));
|
||||
}
|
||||
|
||||
static inline void EncodePath(std::string *buf, const es_file_t *es_file) {
|
||||
buf->append(std::string_view(es_file->path.data, es_file->path.length));
|
||||
}
|
||||
|
||||
static inline void EncodeString(std::string *buf, NSString *value) {
|
||||
if (value) {
|
||||
buf->append(std::string_view([value UTF8String], [value length]));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeString(std::string *buf, std::string_view value) {
|
||||
if (value.length() > 0) {
|
||||
buf->append(std::string_view(value.data(), value.length()));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeUserInfo(::pbv1::UserInfo *pb_user_info, uid_t uid,
|
||||
const std::optional<std::shared_ptr<std::string>> &name) {
|
||||
pb_user_info->set_uid(uid);
|
||||
if (name.has_value()) {
|
||||
pb_user_info->set_name(*name->get());
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeGroupInfo(::pbv1::GroupInfo *pb_group_info, gid_t gid,
|
||||
const std::optional<std::shared_ptr<std::string>> &name) {
|
||||
pb_group_info->set_gid(gid);
|
||||
if (name.has_value()) {
|
||||
pb_group_info->set_name(*name->get());
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeHash(::pbv1::Hash *pb_hash, NSString *sha256) {
|
||||
if (sha256) {
|
||||
pb_hash->set_type(::pbv1::Hash::HASH_ALGO_SHA256);
|
||||
pb_hash->set_hash([sha256 UTF8String], [sha256 length]);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeStat(::pbv1::Stat *pb_stat, const struct stat &sb,
|
||||
const std::optional<std::shared_ptr<std::string>> &username,
|
||||
const std::optional<std::shared_ptr<std::string>> &groupname) {
|
||||
pb_stat->set_dev(sb.st_dev);
|
||||
pb_stat->set_mode(sb.st_mode);
|
||||
pb_stat->set_nlink(sb.st_nlink);
|
||||
pb_stat->set_ino(sb.st_ino);
|
||||
EncodeUserInfo(pb_stat->mutable_user(), sb.st_uid, username);
|
||||
EncodeGroupInfo(pb_stat->mutable_group(), sb.st_gid, groupname);
|
||||
pb_stat->set_rdev(sb.st_rdev);
|
||||
EncodeTimestamp(pb_stat->mutable_access_time(), sb.st_atimespec);
|
||||
EncodeTimestamp(pb_stat->mutable_modification_time(), sb.st_mtimespec);
|
||||
EncodeTimestamp(pb_stat->mutable_change_time(), sb.st_ctimespec);
|
||||
EncodeTimestamp(pb_stat->mutable_birth_time(), sb.st_birthtimespec);
|
||||
pb_stat->set_size(sb.st_size);
|
||||
pb_stat->set_blocks(sb.st_blocks);
|
||||
pb_stat->set_blksize(sb.st_blksize);
|
||||
pb_stat->set_flags(sb.st_flags);
|
||||
pb_stat->set_gen(sb.st_gen);
|
||||
}
|
||||
|
||||
static inline void EncodeFileInfo(::pbv1::FileInfo *pb_file, const es_file_t *es_file,
|
||||
const EnrichedFile &enriched_file, NSString *sha256 = nil) {
|
||||
EncodePath(pb_file->mutable_path(), es_file);
|
||||
pb_file->set_truncated(es_file->path_truncated);
|
||||
EncodeStat(pb_file->mutable_stat(), es_file->stat, enriched_file.user(), enriched_file.group());
|
||||
if (sha256) {
|
||||
EncodeHash(pb_file->mutable_hash(), sha256);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeFileInfoLight(::pbv1::FileInfoLight *pb_file, const es_file_t *es_file) {
|
||||
EncodePath(pb_file->mutable_path(), es_file);
|
||||
pb_file->set_truncated(es_file->path_truncated);
|
||||
}
|
||||
|
||||
static inline void EncodeProcessInfoLight(::pbv1::ProcessInfoLight *pb_proc_info,
|
||||
uint32_t message_version, const es_process_t *es_proc,
|
||||
const EnrichedProcess &enriched_proc) {
|
||||
EncodeProcessID(pb_proc_info->mutable_id(), es_proc->audit_token);
|
||||
EncodeProcessID(pb_proc_info->mutable_parent_id(), es_proc->parent_audit_token);
|
||||
|
||||
pb_proc_info->set_original_parent_pid(es_proc->original_ppid);
|
||||
pb_proc_info->set_group_id(es_proc->group_id);
|
||||
pb_proc_info->set_session_id(es_proc->session_id);
|
||||
|
||||
EncodeUserInfo(pb_proc_info->mutable_effective_user(), EffectiveUser(es_proc->audit_token),
|
||||
enriched_proc.effective_user());
|
||||
EncodeUserInfo(pb_proc_info->mutable_real_user(), RealUser(es_proc->audit_token),
|
||||
enriched_proc.real_user());
|
||||
EncodeGroupInfo(pb_proc_info->mutable_effective_group(), EffectiveGroup(es_proc->audit_token),
|
||||
enriched_proc.effective_group());
|
||||
EncodeGroupInfo(pb_proc_info->mutable_real_group(), RealGroup(es_proc->audit_token),
|
||||
enriched_proc.real_group());
|
||||
|
||||
EncodeFileInfoLight(pb_proc_info->mutable_executable(), es_proc->executable);
|
||||
}
|
||||
|
||||
static inline void EncodeProcessInfo(::pbv1::ProcessInfo *pb_proc_info, uint32_t message_version,
|
||||
const es_process_t *es_proc,
|
||||
const EnrichedProcess &enriched_proc,
|
||||
SNTCachedDecision *cd = nil) {
|
||||
EncodeProcessID(pb_proc_info->mutable_id(), es_proc->audit_token);
|
||||
EncodeProcessID(pb_proc_info->mutable_parent_id(), es_proc->parent_audit_token);
|
||||
if (message_version >= 4) {
|
||||
EncodeProcessID(pb_proc_info->mutable_responsible_id(), es_proc->responsible_audit_token);
|
||||
}
|
||||
|
||||
pb_proc_info->set_original_parent_pid(es_proc->original_ppid);
|
||||
pb_proc_info->set_group_id(es_proc->group_id);
|
||||
pb_proc_info->set_session_id(es_proc->session_id);
|
||||
|
||||
EncodeUserInfo(pb_proc_info->mutable_effective_user(), EffectiveUser(es_proc->audit_token),
|
||||
enriched_proc.effective_user());
|
||||
EncodeUserInfo(pb_proc_info->mutable_real_user(), RealUser(es_proc->audit_token),
|
||||
enriched_proc.real_user());
|
||||
EncodeGroupInfo(pb_proc_info->mutable_effective_group(), EffectiveGroup(es_proc->audit_token),
|
||||
enriched_proc.effective_group());
|
||||
EncodeGroupInfo(pb_proc_info->mutable_real_group(), RealGroup(es_proc->audit_token),
|
||||
enriched_proc.real_group());
|
||||
|
||||
pb_proc_info->set_is_platform_binary(es_proc->is_platform_binary);
|
||||
pb_proc_info->set_is_es_client(es_proc->is_es_client);
|
||||
|
||||
if (es_proc->codesigning_flags & CS_SIGNED) {
|
||||
::pbv1::CodeSignature *pb_code_sig = pb_proc_info->mutable_code_signature();
|
||||
pb_code_sig->set_cdhash(es_proc->cdhash, sizeof(es_proc->cdhash));
|
||||
if (es_proc->signing_id.length > 0) {
|
||||
pb_code_sig->set_signing_id(es_proc->signing_id.data, es_proc->signing_id.length);
|
||||
}
|
||||
|
||||
if (es_proc->team_id.length > 0) {
|
||||
pb_code_sig->set_team_id(es_proc->team_id.data, es_proc->team_id.length);
|
||||
}
|
||||
}
|
||||
|
||||
pb_proc_info->set_cs_flags(es_proc->codesigning_flags);
|
||||
|
||||
EncodeFileInfo(pb_proc_info->mutable_executable(), es_proc->executable,
|
||||
enriched_proc.executable(), cd.sha256);
|
||||
if (message_version >= 2 && es_proc->tty) {
|
||||
EncodeFileInfoLight(pb_proc_info->mutable_tty(), es_proc->tty);
|
||||
}
|
||||
|
||||
if (message_version >= 3) {
|
||||
EncodeTimestamp(pb_proc_info->mutable_start_time(), es_proc->start_time);
|
||||
}
|
||||
}
|
||||
|
||||
void EncodeExitStatus(::pbv1::Exit *pb_exit, int exitStatus) {
|
||||
if (WIFEXITED(exitStatus)) {
|
||||
pb_exit->mutable_exited()->set_exit_status(WEXITSTATUS(exitStatus));
|
||||
} else if (WIFSIGNALED(exitStatus)) {
|
||||
pb_exit->mutable_signaled()->set_signal(WTERMSIG(exitStatus));
|
||||
} else if (WIFSTOPPED(exitStatus)) {
|
||||
pb_exit->mutable_stopped()->set_signal(WSTOPSIG(exitStatus));
|
||||
} else {
|
||||
LOGE(@"Unknown exit status encountered: %d", exitStatus);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info, NSString *cert_hash,
|
||||
NSString *common_name) {
|
||||
if (cert_hash) {
|
||||
EncodeHash(pb_cert_info->mutable_hash(), cert_hash);
|
||||
}
|
||||
|
||||
if (common_name) {
|
||||
pb_cert_info->set_common_name([common_name UTF8String], [common_name length]);
|
||||
}
|
||||
}
|
||||
|
||||
::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state) {
|
||||
if (event_state & SNTEventStateAllow) {
|
||||
return ::pbv1::Execution::DECISION_ALLOW;
|
||||
} else if (event_state & SNTEventStateBlock) {
|
||||
return ::pbv1::Execution::DECISION_DENY;
|
||||
} else {
|
||||
return ::pbv1::Execution::DECISION_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
::pbv1::Execution::Reason GetReasonEnum(SNTEventState event_state) {
|
||||
switch (event_state) {
|
||||
case SNTEventStateAllowBinary: return ::pbv1::Execution::REASON_BINARY;
|
||||
case SNTEventStateAllowCompiler: return ::pbv1::Execution::REASON_COMPILER;
|
||||
case SNTEventStateAllowTransitive: return ::pbv1::Execution::REASON_TRANSITIVE;
|
||||
case SNTEventStateAllowPendingTransitive: return ::pbv1::Execution::REASON_PENDING_TRANSITIVE;
|
||||
case SNTEventStateAllowCertificate: return ::pbv1::Execution::REASON_CERT;
|
||||
case SNTEventStateAllowScope: return ::pbv1::Execution::REASON_SCOPE;
|
||||
case SNTEventStateAllowTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
|
||||
case SNTEventStateAllowUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
|
||||
case SNTEventStateBlockBinary: return ::pbv1::Execution::REASON_BINARY;
|
||||
case SNTEventStateBlockCertificate: return ::pbv1::Execution::REASON_CERT;
|
||||
case SNTEventStateBlockScope: return ::pbv1::Execution::REASON_SCOPE;
|
||||
case SNTEventStateBlockTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
|
||||
case SNTEventStateBlockLongPath: return ::pbv1::Execution::REASON_LONG_PATH;
|
||||
case SNTEventStateBlockUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
|
||||
default: return ::pbv1::Execution::REASON_NOT_RUNNING;
|
||||
}
|
||||
}
|
||||
|
||||
::pbv1::Execution::Mode GetModeEnum(SNTClientMode mode) {
|
||||
switch (mode) {
|
||||
case SNTClientModeMonitor: return ::pbv1::Execution::MODE_MONITOR;
|
||||
case SNTClientModeLockdown: return ::pbv1::Execution::MODE_LOCKDOWN;
|
||||
case SNTClientModeUnknown: return ::pbv1::Execution::MODE_UNKNOWN;
|
||||
default: return ::pbv1::Execution::MODE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
::pbv1::FileDescriptor::FDType GetFileDescriptorType(uint32_t fdtype) {
|
||||
switch (fdtype) {
|
||||
case PROX_FDTYPE_ATALK: return ::pbv1::FileDescriptor::FD_TYPE_ATALK;
|
||||
case PROX_FDTYPE_VNODE: return ::pbv1::FileDescriptor::FD_TYPE_VNODE;
|
||||
case PROX_FDTYPE_SOCKET: return ::pbv1::FileDescriptor::FD_TYPE_SOCKET;
|
||||
case PROX_FDTYPE_PSHM: return ::pbv1::FileDescriptor::FD_TYPE_PSHM;
|
||||
case PROX_FDTYPE_PSEM: return ::pbv1::FileDescriptor::FD_TYPE_PSEM;
|
||||
case PROX_FDTYPE_KQUEUE: return ::pbv1::FileDescriptor::FD_TYPE_KQUEUE;
|
||||
case PROX_FDTYPE_PIPE: return ::pbv1::FileDescriptor::FD_TYPE_PIPE;
|
||||
case PROX_FDTYPE_FSEVENTS: return ::pbv1::FileDescriptor::FD_TYPE_FSEVENTS;
|
||||
case PROX_FDTYPE_NETPOLICY: return ::pbv1::FileDescriptor::FD_TYPE_NETPOLICY;
|
||||
// Note: CHANNEL and NEXUS types weren't exposed until Xcode v13 SDK.
|
||||
// Not using the macros to be able to build on older SDK versions.
|
||||
case 10 /* PROX_FDTYPE_CHANNEL */: return ::pbv1::FileDescriptor::FD_TYPE_CHANNEL;
|
||||
case 11 /* PROX_FDTYPE_NEXUS */: return ::pbv1::FileDescriptor::FD_TYPE_NEXUS;
|
||||
default: return ::pbv1::FileDescriptor::FD_TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena, struct timespec event_time,
|
||||
struct timespec processed_time) {
|
||||
::pbv1::SantaMessage *santa_msg = Arena::CreateMessage<::pbv1::SantaMessage>(arena);
|
||||
|
||||
if (EnabledMachineID()) {
|
||||
EncodeString(santa_msg->mutable_machine_id(), MachineID());
|
||||
}
|
||||
EncodeTimestamp(santa_msg->mutable_event_time(), event_time);
|
||||
EncodeTimestamp(santa_msg->mutable_processed_time(), processed_time);
|
||||
|
||||
return santa_msg;
|
||||
}
|
||||
|
||||
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena, const EnrichedEventType &msg) {
|
||||
return CreateDefaultProto(arena, msg.es_msg().time, msg.enrichment_time());
|
||||
}
|
||||
|
||||
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
|
||||
return CreateDefaultProto(arena, ts, ts);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::FinalizeProto(::pbv1::SantaMessage *santa_msg) {
|
||||
std::vector<uint8_t> vec(santa_msg->ByteSizeLong());
|
||||
santa_msg->SerializeToArray(vec.data(), (int)vec.capacity());
|
||||
return vec;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedClose &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Close *pb_close = santa_msg->mutable_close();
|
||||
|
||||
EncodeProcessInfoLight(pb_close->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
|
||||
msg.instigator());
|
||||
EncodeFileInfo(pb_close->mutable_target(), msg.es_msg().event.close.target, msg.target());
|
||||
pb_close->set_modified(msg.es_msg().event.close.modified);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExchange &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Exchangedata *pb_exchangedata = santa_msg->mutable_exchangedata();
|
||||
|
||||
EncodeProcessInfoLight(pb_exchangedata->mutable_instigator(), msg.es_msg().version,
|
||||
msg.es_msg().process, msg.instigator());
|
||||
EncodeFileInfo(pb_exchangedata->mutable_file1(), msg.es_msg().event.exchangedata.file1,
|
||||
msg.file1());
|
||||
EncodeFileInfo(pb_exchangedata->mutable_file2(), msg.es_msg().event.exchangedata.file2,
|
||||
msg.file2());
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
SNTCachedDecision *cd = [[SNTDecisionCache sharedCache]
|
||||
cachedDecisionForFile:msg.es_msg().event.exec.target->executable->stat];
|
||||
|
||||
GetDecisionEnum(cd.decision);
|
||||
|
||||
::pbv1::Execution *pb_exec = santa_msg->mutable_execution();
|
||||
|
||||
EncodeProcessInfoLight(pb_exec->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
|
||||
msg.instigator());
|
||||
EncodeProcessInfo(pb_exec->mutable_target(), msg.es_msg().version, msg.es_msg().event.exec.target,
|
||||
msg.target(), cd);
|
||||
|
||||
if (msg.es_msg().version >= 2 && msg.script().has_value()) {
|
||||
EncodeFileInfo(pb_exec->mutable_script(), msg.es_msg().event.exec.script, msg.script().value());
|
||||
}
|
||||
|
||||
if (msg.es_msg().version >= 3 && msg.working_dir().has_value()) {
|
||||
EncodeFileInfo(pb_exec->mutable_working_directory(), msg.es_msg().event.exec.cwd,
|
||||
msg.working_dir().value());
|
||||
}
|
||||
|
||||
uint32_t arg_count = esapi_->ExecArgCount(&msg.es_msg().event.exec);
|
||||
for (uint32_t i = 0; i < arg_count; i++) {
|
||||
es_string_token_t tok = esapi_->ExecArg(&msg.es_msg().event.exec, i);
|
||||
pb_exec->add_args(tok.data, tok.length);
|
||||
}
|
||||
|
||||
uint32_t env_count = esapi_->ExecEnvCount(&msg.es_msg().event.exec);
|
||||
for (uint32_t i = 0; i < env_count; i++) {
|
||||
es_string_token_t tok = esapi_->ExecEnv(&msg.es_msg().event.exec, i);
|
||||
pb_exec->add_envs(tok.data, tok.length);
|
||||
}
|
||||
|
||||
if (msg.es_msg().version >= 4) {
|
||||
int32_t max_fd = -1;
|
||||
uint32_t fd_count = esapi_->ExecFDCount(&msg.es_msg().event.exec);
|
||||
for (uint32_t i = 0; i < fd_count; i++) {
|
||||
const es_fd_t *fd = esapi_->ExecFD(&msg.es_msg().event.exec, i);
|
||||
max_fd = std::max(max_fd, fd->fd);
|
||||
::pbv1::FileDescriptor *pb_fd = pb_exec->add_fds();
|
||||
pb_fd->set_fd(fd->fd);
|
||||
pb_fd->set_fd_type(GetFileDescriptorType(fd->fdtype));
|
||||
if (fd->fdtype == PROX_FDTYPE_PIPE) {
|
||||
pb_fd->set_pipe_id(fd->pipe.pipe_id);
|
||||
}
|
||||
}
|
||||
|
||||
// If the `max_fd` seen is less than `last_fd`, we know that ES truncated
|
||||
// the set of returned file descriptors
|
||||
pb_exec->set_fd_list_truncated(max_fd < msg.es_msg().event.exec.last_fd);
|
||||
}
|
||||
|
||||
pb_exec->set_decision(GetDecisionEnum(cd.decision));
|
||||
pb_exec->set_reason(GetReasonEnum(cd.decision));
|
||||
pb_exec->set_mode(GetModeEnum([[SNTConfigurator configurator] clientMode]));
|
||||
|
||||
if (cd.certSHA256 || cd.certCommonName) {
|
||||
EncodeCertificateInfo(pb_exec->mutable_certificate_info(), cd.certSHA256, cd.certCommonName);
|
||||
}
|
||||
|
||||
if (cd.decisionExtra) {
|
||||
pb_exec->set_explain([cd.decisionExtra UTF8String], [cd.decisionExtra length]);
|
||||
}
|
||||
|
||||
if (cd.quarantineURL) {
|
||||
pb_exec->set_quarantine_url([cd.quarantineURL UTF8String], [cd.quarantineURL length]);
|
||||
}
|
||||
|
||||
NSString *orig_path = Utilities::OriginalPathForTranslocation(msg.es_msg().event.exec.target);
|
||||
if (orig_path) {
|
||||
pb_exec->set_original_path([orig_path UTF8String], [orig_path length]);
|
||||
}
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExit &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Exit *pb_exit = santa_msg->mutable_exit();
|
||||
|
||||
EncodeProcessInfoLight(pb_exit->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
|
||||
msg.instigator());
|
||||
EncodeExitStatus(pb_exit, msg.es_msg().event.exit.stat);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedFork &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Fork *pb_fork = santa_msg->mutable_fork();
|
||||
|
||||
EncodeProcessInfoLight(pb_fork->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
|
||||
msg.instigator());
|
||||
EncodeProcessInfoLight(pb_fork->mutable_child(), msg.es_msg().version,
|
||||
msg.es_msg().event.fork.child, msg.child());
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedLink &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Link *pb_link = santa_msg->mutable_link();
|
||||
EncodeProcessInfoLight(pb_link->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
|
||||
msg.instigator());
|
||||
EncodeFileInfo(pb_link->mutable_source(), msg.es_msg().event.link.source, msg.source());
|
||||
EncodePath(pb_link->mutable_target(), msg.es_msg().event.link.target_dir,
|
||||
msg.es_msg().event.link.target_filename);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedRename &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Rename *pb_rename = santa_msg->mutable_rename();
|
||||
EncodeProcessInfoLight(pb_rename->mutable_instigator(), msg.es_msg().version,
|
||||
msg.es_msg().process, msg.instigator());
|
||||
EncodeFileInfo(pb_rename->mutable_source(), msg.es_msg().event.rename.source, msg.source());
|
||||
if (msg.es_msg().event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
|
||||
EncodePath(pb_rename->mutable_target(), msg.es_msg().event.rename.destination.existing_file);
|
||||
pb_rename->set_target_existed(true);
|
||||
} else {
|
||||
EncodePath(pb_rename->mutable_target(), msg.es_msg().event.rename.destination.new_path.dir,
|
||||
msg.es_msg().event.rename.destination.new_path.filename);
|
||||
pb_rename->set_target_existed(false);
|
||||
}
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Unlink *pb_unlink = santa_msg->mutable_unlink();
|
||||
EncodeProcessInfoLight(pb_unlink->mutable_instigator(), msg.es_msg().version,
|
||||
msg.es_msg().process, msg.instigator());
|
||||
EncodeFileInfo(pb_unlink->mutable_target(), msg.es_msg().event.unlink.target, msg.target());
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeAllowlist(const Message &msg, const std::string_view hash) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena);
|
||||
|
||||
const es_file_t *es_file = Utilities::GetAllowListTargetFile(msg);
|
||||
|
||||
EnrichedFile enriched_file(std::nullopt, std::nullopt, std::nullopt);
|
||||
EnrichedProcess enriched_process;
|
||||
|
||||
::pbv1::Allowlist *pb_allowlist = santa_msg->mutable_allowlist();
|
||||
EncodeProcessInfoLight(pb_allowlist->mutable_instigator(), msg->version, msg->process,
|
||||
enriched_process);
|
||||
|
||||
EncodeFileInfo(pb_allowlist->mutable_target(), es_file, enriched_file,
|
||||
[NSString stringWithFormat:@"%s", hash.data()]);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeBundleHashingEvent(SNTStoredEvent *event) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena);
|
||||
|
||||
::pbv1::Bundle *pb_bundle = santa_msg->mutable_bundle();
|
||||
|
||||
EncodeHash(pb_bundle->mutable_file_hash(), event.fileSHA256);
|
||||
EncodeHash(pb_bundle->mutable_bundle_hash(), event.fileBundleHash);
|
||||
pb_bundle->set_bundle_name([NonNull(event.fileBundleName) UTF8String]);
|
||||
pb_bundle->set_bundle_id([NonNull(event.fileBundleID) UTF8String]);
|
||||
pb_bundle->set_bundle_path([NonNull(event.fileBundlePath) UTF8String]);
|
||||
pb_bundle->set_path([NonNull(event.filePath) UTF8String]);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
static void EncodeDisk(::pbv1::Disk *pb_disk, ::pbv1::Disk_Action action, NSDictionary *props) {
|
||||
pb_disk->set_action(action);
|
||||
|
||||
NSString *dmg_path = nil;
|
||||
NSString *serial = nil;
|
||||
if ([props[@"DADeviceModel"] isEqual:@"Disk Image"]) {
|
||||
dmg_path = Utilities::DiskImageForDevice(props[@"DADevicePath"]);
|
||||
} else {
|
||||
serial = Utilities::SerialForDevice(props[@"DADevicePath"]);
|
||||
}
|
||||
|
||||
NSString *model = [NSString
|
||||
stringWithFormat:@"%@ %@", NonNull(props[@"DADeviceVendor"]), NonNull(props[@"DADeviceModel"])];
|
||||
model = [model stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
|
||||
EncodeString(pb_disk->mutable_mount(), [props[@"DAVolumePath"] path]);
|
||||
EncodeString(pb_disk->mutable_volume(), props[@"DAVolumeName"]);
|
||||
EncodeString(pb_disk->mutable_bsd_name(), props[@"DAMediaBSDName"]);
|
||||
EncodeString(pb_disk->mutable_fs(), props[@"DAVolumeKind"]);
|
||||
EncodeString(pb_disk->mutable_model(), model);
|
||||
EncodeString(pb_disk->mutable_serial(), serial);
|
||||
EncodeString(pb_disk->mutable_bus(), props[@"DADeviceProtocol"]);
|
||||
EncodeString(pb_disk->mutable_dmg_path(), dmg_path);
|
||||
|
||||
if (props[@"DAAppearanceTime"]) {
|
||||
// Note: `DAAppearanceTime` is set via `CFAbsoluteTimeGetCurrent`, which uses the defined
|
||||
// reference date of `Jan 1 2001 00:00:00 GMT` (not the typical `00:00:00 UTC on 1 January
|
||||
// 1970`).
|
||||
NSDate *appearance =
|
||||
[NSDate dateWithTimeIntervalSinceReferenceDate:[props[@"DAAppearanceTime"] doubleValue]];
|
||||
NSTimeInterval interval = [appearance timeIntervalSince1970];
|
||||
double seconds;
|
||||
double fractional = modf(interval, &seconds);
|
||||
struct timespec ts = {
|
||||
.tv_sec = (long)seconds,
|
||||
.tv_nsec = (long)(fractional * NSEC_PER_SEC),
|
||||
};
|
||||
EncodeTimestamp(pb_disk->mutable_appearance(), ts);
|
||||
Timestamp timestamp = pb_disk->appearance();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeDiskAppeared(NSDictionary *props) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena);
|
||||
|
||||
EncodeDisk(santa_msg->mutable_disk(), ::pbv1::Disk::ACTION_APPEARED, props);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeDiskDisappeared(NSDictionary *props) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena);
|
||||
|
||||
EncodeDisk(santa_msg->mutable_disk(), ::pbv1::Disk::ACTION_DISAPPEARED, props);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
638
Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm
Normal file
638
Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm
Normal file
@@ -0,0 +1,638 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/proc_info.h>
|
||||
#include <sys/signal.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <uuid/uuid.h>
|
||||
#include <cstring>
|
||||
|
||||
#include <google/protobuf/util/json_util.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/common/santa_proto_include_wrapper.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
#include "google/protobuf/any.pb.h"
|
||||
#include "google/protobuf/timestamp.pb.h"
|
||||
|
||||
using google::protobuf::Timestamp;
|
||||
using google::protobuf::util::JsonPrintOptions;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::Protobuf;
|
||||
using santa::santad::logs::endpoint_security::serializers::Serializer;
|
||||
|
||||
namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
extern void EncodeExitStatus(::pbv1::Exit *pbExit, int exitStatus);
|
||||
extern ::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state);
|
||||
extern ::pbv1::Execution::Reason GetReasonEnum(SNTEventState event_state);
|
||||
extern ::pbv1::Execution::Mode GetModeEnum(SNTClientMode mode);
|
||||
extern ::pbv1::FileDescriptor::FDType GetFileDescriptorType(uint32_t fdtype);
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
using santa::santad::logs::endpoint_security::serializers::EncodeExitStatus;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetDecisionEnum;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetFileDescriptorType;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetModeEnum;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetReasonEnum;
|
||||
|
||||
JsonPrintOptions DefaultJsonPrintOptions() {
|
||||
JsonPrintOptions options;
|
||||
options.always_print_enums_as_ints = false;
|
||||
options.always_print_primitive_fields = false;
|
||||
options.preserve_proto_field_names = true;
|
||||
options.add_whitespace = true;
|
||||
return options;
|
||||
}
|
||||
|
||||
NSString *TestJsonPath(NSString *jsonFileName, uint32_t version) {
|
||||
static dispatch_once_t onceToken;
|
||||
static NSString *testPath;
|
||||
static NSString *testDataRepoPath = @"santa/Source/santad/testdata/protobuf";
|
||||
NSString *testDataRepoVersionPath = [NSString stringWithFormat:@"v%u", version];
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
testPath = [NSString pathWithComponents:@[
|
||||
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testDataRepoPath
|
||||
]];
|
||||
});
|
||||
|
||||
return [NSString pathWithComponents:@[ testPath, testDataRepoVersionPath, jsonFileName ]];
|
||||
}
|
||||
|
||||
NSString *EventTypeToFilename(es_event_type_t eventType) {
|
||||
switch (eventType) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: return @"close.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA: return @"exchangedata.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC: return @"exec.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT: return @"exit.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK: return @"fork.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK: return @"link.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: return @"rename.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK: return @"unlink.json";
|
||||
default: XCTFail(@"Unhandled event type: %d", eventType); return nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSString *LoadTestJson(NSString *jsonFileName, uint32_t version) {
|
||||
NSError *err = nil;
|
||||
NSString *jsonData = [NSString stringWithContentsOfFile:TestJsonPath(jsonFileName, version)
|
||||
encoding:NSUTF8StringEncoding
|
||||
error:&err];
|
||||
|
||||
if (err) {
|
||||
XCTFail(@"Failed to load test data \"%@\": %@", jsonFileName, err);
|
||||
}
|
||||
|
||||
return jsonData;
|
||||
}
|
||||
|
||||
bool CompareTime(const Timestamp ×tamp, struct timespec ts) {
|
||||
return timestamp.seconds() == ts.tv_sec && timestamp.nanos() == ts.tv_nsec;
|
||||
}
|
||||
|
||||
void CheckSantaMessage(const ::pbv1::SantaMessage &santaMsg, const es_message_t &esMsg,
|
||||
struct timespec enrichmentTime) {
|
||||
XCTAssertTrue(CompareTime(santaMsg.processed_time(), enrichmentTime));
|
||||
XCTAssertTrue(CompareTime(santaMsg.event_time(), esMsg.time));
|
||||
}
|
||||
|
||||
const google::protobuf::Message &SantaMessageEvent(const ::pbv1::SantaMessage &santaMsg) {
|
||||
switch (santaMsg.event_case()) {
|
||||
case ::pbv1::SantaMessage::kExecution: return santaMsg.execution();
|
||||
case ::pbv1::SantaMessage::kFork: return santaMsg.fork();
|
||||
case ::pbv1::SantaMessage::kExit: return santaMsg.exit();
|
||||
case ::pbv1::SantaMessage::kClose: return santaMsg.close();
|
||||
case ::pbv1::SantaMessage::kRename: return santaMsg.rename();
|
||||
case ::pbv1::SantaMessage::kUnlink: return santaMsg.unlink();
|
||||
case ::pbv1::SantaMessage::kLink: return santaMsg.link();
|
||||
case ::pbv1::SantaMessage::kExchangedata: return santaMsg.exchangedata();
|
||||
case ::pbv1::SantaMessage::kDisk: return santaMsg.disk();
|
||||
case ::pbv1::SantaMessage::kBundle: return santaMsg.bundle();
|
||||
case ::pbv1::SantaMessage::kAllowlist: return santaMsg.allowlist();
|
||||
case ::pbv1::SantaMessage::EVENT_NOT_SET:
|
||||
XCTFail(@"Protobuf message SantaMessage did not set an 'event' field");
|
||||
OS_FALLTHROUGH;
|
||||
default:
|
||||
[NSException raise:@"Required protobuf field not set"
|
||||
format:@"SantaMessage missing required field 'event'"];
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
std::string ConvertMessageToJsonString(const ::pbv1::SantaMessage &santaMsg) {
|
||||
JsonPrintOptions options = DefaultJsonPrintOptions();
|
||||
const google::protobuf::Message &message = SantaMessageEvent(santaMsg);
|
||||
|
||||
std::string json;
|
||||
XCTAssertTrue(google::protobuf::util::MessageToJsonString(message, &json, options).ok());
|
||||
return json;
|
||||
}
|
||||
|
||||
void CheckProto(const ::pbv1::SantaMessage &santaMsg,
|
||||
std::shared_ptr<EnrichedMessage> enrichedMsg) {
|
||||
return std::visit(
|
||||
[santaMsg](const EnrichedEventType &enrichedEvent) {
|
||||
CheckSantaMessage(santaMsg, enrichedEvent.es_msg(), enrichedEvent.enrichment_time());
|
||||
NSString *wantData = LoadTestJson(EventTypeToFilename(enrichedEvent.es_msg().event_type),
|
||||
enrichedEvent.es_msg().version);
|
||||
std::string got = ConvertMessageToJsonString(santaMsg);
|
||||
|
||||
XCTAssertEqualObjects([NSString stringWithUTF8String:got.c_str()], wantData);
|
||||
},
|
||||
enrichedMsg->GetEnrichedMessage());
|
||||
}
|
||||
|
||||
void SerializeAndCheck(es_event_type_t eventType,
|
||||
void (^messageSetup)(std::shared_ptr<MockEndpointSecurityAPI>,
|
||||
es_message_t *)) {
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
|
||||
cur_version++) {
|
||||
if (cur_version == 3) {
|
||||
// Note: Version 3 was only in a macOS beta.
|
||||
continue;
|
||||
}
|
||||
|
||||
es_file_t procFile = MakeESFile("foo", MakeStat(100));
|
||||
es_file_t ttyFile = MakeESFile("footty", MakeStat(200));
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(eventType, &proc);
|
||||
esMsg.process->tty = &ttyFile;
|
||||
esMsg.version = cur_version;
|
||||
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
messageSetup(mockESApi, &esMsg);
|
||||
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
|
||||
std::shared_ptr<EnrichedMessage> enrichedMsg = Enricher().Enrich(Message(mockESApi, &esMsg));
|
||||
|
||||
std::vector<uint8_t> vec = bs->SerializeMessage(enrichedMsg);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
|
||||
CheckProto(santaMsg, enrichedMsg);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
@interface ProtobufTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property id mockDecisionCache;
|
||||
@property SNTCachedDecision *testCachedDecision;
|
||||
@end
|
||||
|
||||
@implementation ProtobufTest
|
||||
|
||||
- (void)setUp {
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(YES);
|
||||
OCMStub([self.mockConfigurator machineID]).andReturn(@"my_machine_id");
|
||||
|
||||
self.testCachedDecision = [[SNTCachedDecision alloc] init];
|
||||
self.testCachedDecision.decision = SNTEventStateAllowBinary;
|
||||
self.testCachedDecision.decisionExtra = @"extra!";
|
||||
self.testCachedDecision.sha256 = @"1234_file_hash";
|
||||
self.testCachedDecision.quarantineURL = @"google.com";
|
||||
self.testCachedDecision.certSHA256 = @"5678_cert_hash";
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
OCMStub([self.mockDecisionCache cachedDecisionForFile:{}])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(self.testCachedDecision);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.mockConfigurator stopMocking];
|
||||
[self.mockDecisionCache stopMocking];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageClose {
|
||||
__block es_file_t file = MakeESFile("close_file", MakeStat(300));
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_CLOSE,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.close.modified = true;
|
||||
esMsg->event.close.target = &file;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExchange {
|
||||
__block es_file_t file1 = MakeESFile("exchange_file_1", MakeStat(300));
|
||||
__block es_file_t file2 = MakeESFile("exchange_file_1", MakeStat(400));
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.exchangedata.file1 = &file1;
|
||||
esMsg->event.exchangedata.file2 = &file2;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testGetDecisionEnum {
|
||||
std::map<SNTEventState, ::pbv1::Execution::Decision> stateToDecision = {
|
||||
{SNTEventStateUnknown, ::pbv1::Execution::DECISION_UNKNOWN},
|
||||
{SNTEventStateBundleBinary, ::pbv1::Execution::DECISION_UNKNOWN},
|
||||
{SNTEventStateBlockUnknown, ::pbv1::Execution::DECISION_DENY},
|
||||
{SNTEventStateBlockBinary, ::pbv1::Execution::DECISION_DENY},
|
||||
{SNTEventStateBlockCertificate, ::pbv1::Execution::DECISION_DENY},
|
||||
{SNTEventStateBlockScope, ::pbv1::Execution::DECISION_DENY},
|
||||
{SNTEventStateBlockTeamID, ::pbv1::Execution::DECISION_DENY},
|
||||
{SNTEventStateBlockLongPath, ::pbv1::Execution::DECISION_DENY},
|
||||
{SNTEventStateAllowUnknown, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowBinary, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowCertificate, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowScope, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowCompiler, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowTransitive, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowPendingTransitive, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowTeamID, ::pbv1::Execution::DECISION_ALLOW},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToDecision) {
|
||||
XCTAssertEqual(GetDecisionEnum(kv.first), kv.second, @"Bad decision for state: %ld", kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testGetReasonEnum {
|
||||
std::map<SNTEventState, ::pbv1::Execution::Reason> stateToReason = {
|
||||
{SNTEventStateUnknown, ::pbv1::Execution::REASON_NOT_RUNNING},
|
||||
{SNTEventStateBundleBinary, ::pbv1::Execution::REASON_NOT_RUNNING},
|
||||
{SNTEventStateBlockUnknown, ::pbv1::Execution::REASON_UNKNOWN},
|
||||
{SNTEventStateBlockBinary, ::pbv1::Execution::REASON_BINARY},
|
||||
{SNTEventStateBlockCertificate, ::pbv1::Execution::REASON_CERT},
|
||||
{SNTEventStateBlockScope, ::pbv1::Execution::REASON_SCOPE},
|
||||
{SNTEventStateBlockTeamID, ::pbv1::Execution::REASON_TEAM_ID},
|
||||
{SNTEventStateBlockLongPath, ::pbv1::Execution::REASON_LONG_PATH},
|
||||
{SNTEventStateAllowUnknown, ::pbv1::Execution::REASON_UNKNOWN},
|
||||
{SNTEventStateAllowBinary, ::pbv1::Execution::REASON_BINARY},
|
||||
{SNTEventStateAllowCertificate, ::pbv1::Execution::REASON_CERT},
|
||||
{SNTEventStateAllowScope, ::pbv1::Execution::REASON_SCOPE},
|
||||
{SNTEventStateAllowCompiler, ::pbv1::Execution::REASON_COMPILER},
|
||||
{SNTEventStateAllowTransitive, ::pbv1::Execution::REASON_TRANSITIVE},
|
||||
{SNTEventStateAllowPendingTransitive, ::pbv1::Execution::REASON_PENDING_TRANSITIVE},
|
||||
{SNTEventStateAllowTeamID, ::pbv1::Execution::REASON_TEAM_ID},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToReason) {
|
||||
XCTAssertEqual(GetReasonEnum(kv.first), kv.second, @"Bad reason for state: %ld", kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testGetModeEnum {
|
||||
std::map<SNTClientMode, ::pbv1::Execution::Mode> clientModeToExecMode = {
|
||||
{SNTClientModeUnknown, ::pbv1::Execution::MODE_UNKNOWN},
|
||||
{SNTClientModeMonitor, ::pbv1::Execution::MODE_MONITOR},
|
||||
{SNTClientModeLockdown, ::pbv1::Execution::MODE_LOCKDOWN},
|
||||
{(SNTClientMode)123, ::pbv1::Execution::MODE_UNKNOWN},
|
||||
};
|
||||
|
||||
for (const auto &kv : clientModeToExecMode) {
|
||||
XCTAssertEqual(GetModeEnum(kv.first), kv.second, @"Bad mode for state: %ld", kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testGetFileDescriptorType {
|
||||
std::map<uint32_t, ::pbv1::FileDescriptor::FDType> fdtypeToEnumType = {
|
||||
{PROX_FDTYPE_ATALK, ::pbv1::FileDescriptor::FD_TYPE_ATALK},
|
||||
{PROX_FDTYPE_VNODE, ::pbv1::FileDescriptor::FD_TYPE_VNODE},
|
||||
{PROX_FDTYPE_SOCKET, ::pbv1::FileDescriptor::FD_TYPE_SOCKET},
|
||||
{PROX_FDTYPE_PSHM, ::pbv1::FileDescriptor::FD_TYPE_PSHM},
|
||||
{PROX_FDTYPE_PSEM, ::pbv1::FileDescriptor::FD_TYPE_PSEM},
|
||||
{PROX_FDTYPE_KQUEUE, ::pbv1::FileDescriptor::FD_TYPE_KQUEUE},
|
||||
{PROX_FDTYPE_PIPE, ::pbv1::FileDescriptor::FD_TYPE_PIPE},
|
||||
{PROX_FDTYPE_FSEVENTS, ::pbv1::FileDescriptor::FD_TYPE_FSEVENTS},
|
||||
{PROX_FDTYPE_NETPOLICY, ::pbv1::FileDescriptor::FD_TYPE_NETPOLICY},
|
||||
{10 /* PROX_FDTYPE_CHANNEL */, ::pbv1::FileDescriptor::FD_TYPE_CHANNEL},
|
||||
{11 /* PROX_FDTYPE_NEXUS */, ::pbv1::FileDescriptor::FD_TYPE_NEXUS},
|
||||
};
|
||||
|
||||
for (const auto &kv : fdtypeToEnumType) {
|
||||
XCTAssertEqual(GetFileDescriptorType(kv.first), kv.second, @"Bad fd type name for fdtype: %u",
|
||||
kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExec {
|
||||
es_file_t procFileTarget = MakeESFile("fooexec", MakeStat(300));
|
||||
__block es_process_t procTarget =
|
||||
MakeESProcess(&procFileTarget, MakeAuditToken(23, 45), MakeAuditToken(67, 89));
|
||||
__block es_file_t fileCwd = MakeESFile("cwd", MakeStat(400));
|
||||
__block es_file_t fileScript = MakeESFile("script.sh", MakeStat(500));
|
||||
__block es_fd_t fd1 = {.fd = 1, .fdtype = PROX_FDTYPE_VNODE};
|
||||
__block es_fd_t fd2 = {.fd = 2, .fdtype = PROX_FDTYPE_SOCKET};
|
||||
__block es_fd_t fd3 = {.fd = 3, .fdtype = PROX_FDTYPE_PIPE, .pipe = {.pipe_id = 123}};
|
||||
|
||||
procTarget.codesigning_flags = CS_SIGNED | CS_HARD | CS_KILL;
|
||||
memset(procTarget.cdhash, 'A', sizeof(procTarget.cdhash));
|
||||
procTarget.signing_id = MakeESStringToken("my_signing_id");
|
||||
procTarget.team_id = MakeESStringToken("my_team_id");
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXEC, ^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.exec.target = &procTarget;
|
||||
esMsg->event.exec.cwd = &fileCwd;
|
||||
esMsg->event.exec.script = &fileScript;
|
||||
|
||||
// For version 5, simulate a "truncated" set of FDs
|
||||
if (esMsg->version == 5) {
|
||||
esMsg->event.exec.last_fd = 123;
|
||||
} else {
|
||||
esMsg->event.exec.last_fd = 3;
|
||||
}
|
||||
|
||||
EXPECT_CALL(*mockESApi, ExecArgCount).WillOnce(testing::Return(3));
|
||||
EXPECT_CALL(*mockESApi, ExecArg)
|
||||
.WillOnce(testing::Return(MakeESStringToken("exec_path")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("-l")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("--foo")));
|
||||
|
||||
EXPECT_CALL(*mockESApi, ExecEnvCount).WillOnce(testing::Return(2));
|
||||
EXPECT_CALL(*mockESApi, ExecEnv)
|
||||
.WillOnce(testing::Return(MakeESStringToken("ENV_PATH=/path/to/bin:/and/another")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("DEBUG=1")));
|
||||
|
||||
if (esMsg->version >= 4) {
|
||||
EXPECT_CALL(*mockESApi, ExecFDCount).WillOnce(testing::Return(3));
|
||||
EXPECT_CALL(*mockESApi, ExecFD)
|
||||
.WillOnce(testing::Return(&fd1))
|
||||
.WillOnce(testing::Return(&fd2))
|
||||
.WillOnce(testing::Return(&fd3));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExit {
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXIT,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.exit.stat = W_EXITCODE(1, 0);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testEncodeExitStatus {
|
||||
{
|
||||
::pbv1::Exit pbExit;
|
||||
EncodeExitStatus(&pbExit, W_EXITCODE(1, 0));
|
||||
XCTAssertTrue(pbExit.has_exited());
|
||||
XCTAssertEqual(1, pbExit.exited().exit_status());
|
||||
}
|
||||
|
||||
{
|
||||
::pbv1::Exit pbExit;
|
||||
EncodeExitStatus(&pbExit, W_EXITCODE(2, SIGUSR1));
|
||||
XCTAssertTrue(pbExit.has_signaled());
|
||||
XCTAssertEqual(SIGUSR1, pbExit.signaled().signal());
|
||||
}
|
||||
|
||||
{
|
||||
::pbv1::Exit pbExit;
|
||||
EncodeExitStatus(&pbExit, W_STOPCODE(SIGSTOP));
|
||||
XCTAssertTrue(pbExit.has_stopped());
|
||||
XCTAssertEqual(SIGSTOP, pbExit.stopped().signal());
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageFork {
|
||||
__block es_file_t procFileChild = MakeESFile("foo_child", MakeStat(300));
|
||||
__block es_file_t ttyFileChild = MakeESFile("footty", MakeStat(400));
|
||||
__block es_process_t procChild =
|
||||
MakeESProcess(&procFileChild, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
procChild.tty = &ttyFileChild;
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_FORK,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.fork.child = &procChild;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageLink {
|
||||
__block es_file_t fileSource = MakeESFile("source", MakeStat(300));
|
||||
__block es_file_t fileTargetDir = MakeESFile("target_dir");
|
||||
es_string_token_t targetTok = MakeESStringToken("target_file");
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_LINK,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.link.source = &fileSource;
|
||||
esMsg->event.link.target_dir = &fileTargetDir;
|
||||
esMsg->event.link.target_filename = targetTok;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageRename {
|
||||
__block es_file_t fileSource = MakeESFile("source", MakeStat(300));
|
||||
__block es_file_t fileTargetDir = MakeESFile("target_dir");
|
||||
es_string_token_t targetTok = MakeESStringToken("target_file");
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_RENAME,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.rename.source = &fileSource;
|
||||
// Test new and existing destination types
|
||||
if (esMsg->version == 4) {
|
||||
esMsg->event.rename.destination.existing_file = &fileTargetDir;
|
||||
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
|
||||
} else {
|
||||
esMsg->event.rename.destination.new_path.dir = &fileTargetDir;
|
||||
esMsg->event.rename.destination.new_path.filename = targetTok;
|
||||
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageUnlink {
|
||||
__block es_file_t fileTarget = MakeESFile("unlink_file", MakeStat(300));
|
||||
__block es_file_t fileTargetParent = MakeESFile("unlink_file_parent", MakeStat(400));
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_UNLINK,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.unlink.target = &fileTarget;
|
||||
esMsg->event.unlink.parent_dir = &fileTargetParent;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeAllowlist {
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
|
||||
cur_version++) {
|
||||
if (cur_version == 3) {
|
||||
// Note: Version 3 was only in a macOS beta.
|
||||
continue;
|
||||
}
|
||||
|
||||
es_file_t procFile = MakeESFile("foo", MakeStat(100));
|
||||
es_file_t ttyFile = MakeESFile("footty", MakeStat(200));
|
||||
es_file_t closeFile = MakeESFile("close_file", MakeStat(300));
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
|
||||
esMsg.process->tty = &ttyFile;
|
||||
esMsg.version = cur_version;
|
||||
esMsg.event.close.modified = true;
|
||||
esMsg.event.close.target = &closeFile;
|
||||
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
|
||||
|
||||
std::vector<uint8_t> vec = bs->SerializeAllowlist(Message(mockESApi, &esMsg), "hash_value");
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
|
||||
NSString *wantData = LoadTestJson(@"allowlist.json", esMsg.version);
|
||||
std::string got = ConvertMessageToJsonString(santaMsg);
|
||||
|
||||
XCTAssertEqualObjects([NSString stringWithUTF8String:got.c_str()], wantData);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testSerializeBundleHashingEvent {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
|
||||
se.fileSHA256 = @"file_hash";
|
||||
se.fileBundleHash = @"file_bundle_hash";
|
||||
se.fileBundleName = @"file_bundle_name";
|
||||
se.fileBundleID = nil;
|
||||
se.fileBundlePath = @"file_bundle_path";
|
||||
se.filePath = @"file_path";
|
||||
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeBundleHashingEvent(se);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
XCTAssertTrue(santaMsg.has_bundle());
|
||||
|
||||
const ::pbv1::Bundle &pbBundle = santaMsg.bundle();
|
||||
|
||||
::pbv1::Hash pbHash = pbBundle.file_hash();
|
||||
XCTAssertEqualObjects(@(pbHash.hash().c_str()), se.fileSHA256);
|
||||
XCTAssertEqual(pbHash.type(), ::pbv1::Hash::HASH_ALGO_SHA256);
|
||||
|
||||
pbHash = pbBundle.bundle_hash();
|
||||
XCTAssertEqualObjects(@(pbHash.hash().c_str()), se.fileBundleHash);
|
||||
XCTAssertEqual(pbHash.type(), ::pbv1::Hash::HASH_ALGO_SHA256);
|
||||
|
||||
XCTAssertEqualObjects(@(pbBundle.bundle_name().c_str()), se.fileBundleName);
|
||||
XCTAssertEqualObjects(@(pbBundle.bundle_id().c_str()), @"");
|
||||
XCTAssertEqualObjects(@(pbBundle.bundle_path().c_str()), se.fileBundlePath);
|
||||
XCTAssertEqualObjects(@(pbBundle.path().c_str()), se.filePath);
|
||||
}
|
||||
|
||||
- (void)testSerializeDiskAppeared {
|
||||
NSDictionary *props = @{
|
||||
@"DADevicePath" : @"",
|
||||
@"DADeviceVendor" : @"vendor",
|
||||
@"DADeviceModel" : @"model",
|
||||
@"DAAppearanceTime" : @(123456789),
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"path"],
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
@"DAVolumeKind" : @"apfs",
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeDiskAppeared(props);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
XCTAssertTrue(santaMsg.has_disk());
|
||||
|
||||
const ::pbv1::Disk &pbDisk = santaMsg.disk();
|
||||
|
||||
XCTAssertEqual(pbDisk.action(), ::pbv1::Disk::ACTION_APPEARED);
|
||||
|
||||
XCTAssertEqualObjects(@(pbDisk.mount().c_str()), [props[@"DAVolumePath"] path]);
|
||||
XCTAssertEqualObjects(@(pbDisk.volume().c_str()), @"");
|
||||
XCTAssertEqualObjects(@(pbDisk.bsd_name().c_str()), props[@"DAMediaBSDName"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.fs().c_str()), props[@"DAVolumeKind"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.model().c_str()), @"vendor model");
|
||||
XCTAssertEqualObjects(@(pbDisk.serial().c_str()), @"");
|
||||
XCTAssertEqualObjects(@(pbDisk.bus().c_str()), props[@"DADeviceProtocol"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.dmg_path().c_str()), @"");
|
||||
|
||||
// Note: `DAAppearanceTime` is treated as a reference time since 2001 and is converted to a
|
||||
// reference time of 1970. Skip the calculation in the test here, just ensure the value is set.
|
||||
XCTAssertGreaterThan(pbDisk.appearance().seconds(), 1);
|
||||
}
|
||||
|
||||
- (void)testSerializeDiskDisppeared {
|
||||
NSDictionary *props = @{
|
||||
@"DADevicePath" : @"",
|
||||
@"DADeviceVendor" : @"vendor",
|
||||
@"DADeviceModel" : @"model",
|
||||
@"DAAppearanceTime" : @(123456789),
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"path"],
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
@"DAVolumeKind" : @"apfs",
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeDiskDisappeared(props);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
XCTAssertTrue(santaMsg.has_disk());
|
||||
|
||||
const ::pbv1::Disk &pbDisk = santaMsg.disk();
|
||||
|
||||
XCTAssertEqual(pbDisk.action(), ::pbv1::Disk::ACTION_DISAPPEARED);
|
||||
|
||||
XCTAssertEqualObjects(@(pbDisk.mount().c_str()), [props[@"DAVolumePath"] path]);
|
||||
XCTAssertEqualObjects(@(pbDisk.volume().c_str()), @"");
|
||||
XCTAssertEqualObjects(@(pbDisk.bsd_name().c_str()), props[@"DAMediaBSDName"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.fs().c_str()), props[@"DAVolumeKind"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.model().c_str()), @"vendor model");
|
||||
XCTAssertEqualObjects(@(pbDisk.serial().c_str()), @"");
|
||||
XCTAssertEqualObjects(@(pbDisk.bus().c_str()), props[@"DADeviceProtocol"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.dmg_path().c_str()), @"");
|
||||
|
||||
// Note: `DAAppearanceTime` is treated as a reference time since 2001 and is converted to a
|
||||
// reference time of 1970. Skip the calculation in the test here, just ensure the value is set.
|
||||
XCTAssertGreaterThan(pbDisk.appearance().seconds(), 1);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,62 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SANITIZABLESTRING_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SANITIZABLESTRING_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
// Small helper class that will sanitize a given string, but will only use new
|
||||
// memory if the string required sanitization. If the string is already
|
||||
// sanitized, this class only uses the given buffers.
|
||||
class SanitizableString {
|
||||
public:
|
||||
SanitizableString(const es_file_t *file);
|
||||
SanitizableString(const es_string_token_t &tok);
|
||||
SanitizableString(const char *str, size_t len);
|
||||
SanitizableString(NSString *str);
|
||||
|
||||
SanitizableString(SanitizableString &&other) = delete;
|
||||
SanitizableString(const SanitizableString &other) = delete;
|
||||
SanitizableString &operator=(const SanitizableString &rhs) = delete;
|
||||
SanitizableString &operator=(SanitizableString &&rhs) = delete;
|
||||
|
||||
// Return the original, unsanitized string
|
||||
std::string_view String() const;
|
||||
|
||||
// Return the sanitized string
|
||||
std::string_view Sanitized() const;
|
||||
|
||||
static std::optional<std::string> SanitizeString(const char *str);
|
||||
static std::optional<std::string> SanitizeString(const char *str, size_t length);
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &ss, const SanitizableString &sani_string);
|
||||
|
||||
private:
|
||||
const char *data_;
|
||||
size_t length_;
|
||||
mutable bool sanitized_ = false;
|
||||
mutable std::optional<std::string> sanitized_string_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,113 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
SanitizableString::SanitizableString(const es_file_t *file)
|
||||
: data_(file->path.data), length_(file->path.length) {}
|
||||
|
||||
SanitizableString::SanitizableString(const es_string_token_t &tok)
|
||||
: data_(tok.data), length_(tok.length) {}
|
||||
|
||||
SanitizableString::SanitizableString(NSString *str)
|
||||
: data_([str UTF8String]), length_([str length]) {}
|
||||
|
||||
SanitizableString::SanitizableString(const char *str, size_t len) : data_(str), length_(len) {}
|
||||
|
||||
std::string_view SanitizableString::String() const {
|
||||
return std::string_view(data_, length_);
|
||||
}
|
||||
|
||||
std::string_view SanitizableString::Sanitized() const {
|
||||
if (!sanitized_) {
|
||||
sanitized_ = true;
|
||||
sanitized_string_ = SanitizeString(data_, length_);
|
||||
}
|
||||
|
||||
if (sanitized_string_.has_value()) {
|
||||
return sanitized_string_.value();
|
||||
} else {
|
||||
if (data_) {
|
||||
return std::string_view(data_, length_);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::ostream &operator<<(std::ostream &ss, const SanitizableString &sani_string) {
|
||||
ss << sani_string.Sanitized();
|
||||
return ss;
|
||||
}
|
||||
|
||||
std::optional<std::string> SanitizableString::SanitizeString(const char *str) {
|
||||
return SanitizeString(str, str ? strlen(str) : 0);
|
||||
}
|
||||
|
||||
std::optional<std::string> SanitizableString::SanitizeString(const char *str, size_t length) {
|
||||
size_t strOffset = 0;
|
||||
char c = 0;
|
||||
std::string buf;
|
||||
bool reservedStringSpace = false;
|
||||
|
||||
if (!str) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (length < 1) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Loop through the string one character at a time, looking for the characters
|
||||
// we want to remove.
|
||||
for (const char *p = str; (c = *p) != 0; ++p) {
|
||||
if (c == '|' || c == '\n' || c == '\r') {
|
||||
if (!reservedStringSpace) {
|
||||
// Assume the common case won't grow the string length by more than a
|
||||
// factor of 2. String will grow more if it needs to.
|
||||
buf.reserve(length * 2);
|
||||
reservedStringSpace = true;
|
||||
}
|
||||
|
||||
// Copy from the last offset up to the character we just found into the buffer
|
||||
ptrdiff_t diff = p - str;
|
||||
buf.append(str + strOffset, diff - strOffset);
|
||||
|
||||
// Update the buffer and string offsets
|
||||
strOffset = diff + 1;
|
||||
|
||||
// Replace the found character and advance the buffer offset
|
||||
switch (c) {
|
||||
case '|': buf.append("<pipe>"); break;
|
||||
case '\n': buf.append("\\n"); break;
|
||||
case '\r': buf.append("\\r"); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (strOffset > 0 && strOffset < length) {
|
||||
// Copy any characters from the last match to the end of the string into the buffer.
|
||||
buf.append(str + strOffset, length - strOffset);
|
||||
}
|
||||
|
||||
if (reservedStringSpace) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user