Compare commits
52 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 | ||
|
|
fd23a5c3b7 | ||
|
|
ec203e8796 | ||
|
|
57ff69208d | ||
|
|
f00b7d2ded | ||
|
|
9791fdd53c | ||
|
|
26e2203f1e | ||
|
|
4a47195d12 | ||
|
|
4436e221df | ||
|
|
deccc8a148 | ||
|
|
06da796a4d | ||
|
|
7b99a76d0d | ||
|
|
c2d3e99446 | ||
|
|
6db7fea8ae | ||
|
|
6fcb4cfe63 | ||
|
|
8b55ee4da5 |
18
.allstar/binary_artifacts.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
# Ignore reason: These crafted binaries are used in tests
|
||||
ignorePaths:
|
||||
- Source/common/testdata/bad_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
- Source/common/testdata/missing_pagezero
|
||||
- Source/common/testdata/32bitplist
|
||||
- Source/common/testdata/BundleExample.app/Contents/MacOS/BundleExample
|
||||
- Source/common/testdata/DirectoryBundle/Contents/MacOS/DirectoryBundle
|
||||
- Source/common/testdata/DirectoryBundle/Contents/Resources/BundleExample.app/Contents/MacOS/BundleExample
|
||||
- Source/santad/testdata/binaryrules/badbinary
|
||||
- Source/santad/testdata/binaryrules/goodbinary
|
||||
- Source/santad/testdata/binaryrules/badcert
|
||||
- Source/santad/testdata/binaryrules/banned_teamid_allowed_binary
|
||||
- Source/santad/testdata/binaryrules/banned_teamid
|
||||
- Source/santad/testdata/binaryrules/goodcert
|
||||
- Source/santad/testdata/binaryrules/noop
|
||||
- Source/santad/testdata/binaryrules/rules.db
|
||||
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
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
name: Check Markdown links
|
||||
name: Check Markdown
|
||||
|
||||
on:
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.md"
|
||||
|
||||
jobs:
|
||||
markdown-link-check:
|
||||
markdown-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
- run: "! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'"
|
||||
7
.github/workflows/ci.yml
vendored
@@ -54,10 +54,3 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
path-to-lcov: ./bazel-out/_coverage/_coverage_report.dat
|
||||
flag-name: Unit
|
||||
|
||||
benchmark:
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run All Tests
|
||||
run: ./Testing/benchmark.sh
|
||||
|
||||
14
BUILD
@@ -74,14 +74,14 @@ launchctl load /Library/LaunchAgents/com.google.santa.plist
|
||||
run_command(
|
||||
name = "reload",
|
||||
srcs = [
|
||||
"//Source/santa:Santa",
|
||||
"//Source/gui:Santa",
|
||||
],
|
||||
cmd = """
|
||||
set -e
|
||||
|
||||
rm -rf /tmp/bazel_santa_reload
|
||||
unzip -d /tmp/bazel_santa_reload \
|
||||
$${BUILD_WORKSPACE_DIRECTORY}/bazel-out/*$(COMPILATION_MODE)*/bin/Source/santa/Santa.zip >/dev/null
|
||||
$${BUILD_WORKSPACE_DIRECTORY}/bazel-out/*$(COMPILATION_MODE)*/bin/Source/gui/Santa.zip >/dev/null
|
||||
echo "You may be asked for your password for sudo"
|
||||
sudo BINARIES=/tmp/bazel_santa_reload CONF=$${BUILD_WORKSPACE_DIRECTORY}/Conf \
|
||||
$${BUILD_WORKSPACE_DIRECTORY}/Conf/install.sh
|
||||
@@ -96,7 +96,7 @@ echo "Time to stop being naughty"
|
||||
genrule(
|
||||
name = "release",
|
||||
srcs = [
|
||||
"//Source/santa:Santa",
|
||||
"//Source/gui:Santa",
|
||||
"Conf/install.sh",
|
||||
"Conf/uninstall.sh",
|
||||
"Conf/com.google.santa.bundleservice.plist",
|
||||
@@ -191,16 +191,10 @@ test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
"//Source/common:unit_tests",
|
||||
"//Source/gui:unit_tests",
|
||||
"//Source/santactl:unit_tests",
|
||||
"//Source/santad:unit_tests",
|
||||
"//Source/santametricservice:unit_tests",
|
||||
"//Source/santasyncservice:unit_tests",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "benchmarks",
|
||||
tests = [
|
||||
"//Source/santad:SNTApplicationBenchmark",
|
||||
],
|
||||
)
|
||||
|
||||
10
README.md
@@ -1,13 +1,13 @@
|
||||
# Santa [](https://github.com/google/santa/actions/workflows/ci.yml) [](https://coveralls.io/github/google/santa?branch=main)
|
||||
# Santa [](https://github.com/google/santa/actions/workflows/ci.yml)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/santa/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
<img src="https://raw.githubusercontent.com/google/santa/main/Source/gui/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
|
||||
</p>
|
||||
|
||||
Santa is a binary authorization system for macOS. It consists of a system
|
||||
extension that monitors for executions, a daemon that makes execution decisions
|
||||
Santa is a binary authorization system for macOS. It consists of a system
|
||||
extension that monitors for executions, a daemon that makes execution decisions
|
||||
based on the contents of a local database, a GUI agent that notifies the user in
|
||||
case of a block decision and a command-line utility for managing the system and
|
||||
case of a block decision and a command-line utility for managing the system and
|
||||
synchronizing the database with a server.
|
||||
|
||||
It is named Santa because it keeps track of binaries that are naughty or nice.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Reporting a Vulnerability
|
||||
|
||||
If you believe you have found a security vulnerability, we would appreciate private disclosure
|
||||
so that we can work on a fix before disclosure. Any vulnerabilities reported to us will be
|
||||
so that we can work on a fix before disclosure. Any vulnerabilities reported to us will be
|
||||
disclosed publicly either when a new version with fixes is released or 90 days has passed,
|
||||
whichever comes first.
|
||||
|
||||
|
||||
@@ -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
@@ -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,17 +245,18 @@
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableMachineIDDecoration;
|
||||
|
||||
///
|
||||
/// Use an internal cache for decisions instead of relying on the caching
|
||||
/// mechanism built-in to the EndpointSecurity framework. This may increase
|
||||
/// performance, particularly when Santa is run alongside other system
|
||||
/// extensions.
|
||||
/// Has no effect if the system extension is not being used. Defaults to NO.
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableSysxCache;
|
||||
|
||||
#pragma mark - GUI Settings
|
||||
|
||||
///
|
||||
/// When silent mode is enabled, Santa will never show notifications for
|
||||
/// blocked processes.
|
||||
///
|
||||
/// This can be a very confusing experience for users, use with caution.
|
||||
///
|
||||
/// Defaults to NO.
|
||||
///
|
||||
@property(readonly, nonatomic) BOOL enableSilentMode;
|
||||
|
||||
///
|
||||
/// The text to display when opening Santa.app.
|
||||
/// If unset, the default text will be displayed.
|
||||
@@ -377,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
|
||||
@@ -66,7 +67,8 @@ static NSString *const kMachineOwnerPlistKeyKey = @"MachineOwnerKey";
|
||||
static NSString *const kMachineIDPlistFileKey = @"MachineIDPlist";
|
||||
static NSString *const kMachineIDPlistKeyKey = @"MachineIDKey";
|
||||
|
||||
static NSString *const kAboutText = @"AboutText";
|
||||
static NSString *const kEnableSilentModeKey = @"EnableSilentMode";
|
||||
static NSString *const kAboutTextKey = @"AboutText";
|
||||
static NSString *const kMoreInfoURLKey = @"MoreInfoURL";
|
||||
static NSString *const kEventDetailURLKey = @"EventDetailURL";
|
||||
static NSString *const kEventDetailTextKey = @"EventDetailText";
|
||||
@@ -86,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";
|
||||
@@ -172,7 +172,8 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
kRemountUSBModeKey : array,
|
||||
kEnablePageZeroProtectionKey : number,
|
||||
kEnableBadSignatureProtectionKey : number,
|
||||
kAboutText : string,
|
||||
kEnableSilentModeKey : number,
|
||||
kAboutTextKey : string,
|
||||
kMoreInfoURLKey : string,
|
||||
kEventDetailURLKey : string,
|
||||
kEventDetailTextKey : string,
|
||||
@@ -199,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,
|
||||
@@ -303,6 +303,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableSilentMode {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingAboutText {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -387,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];
|
||||
}
|
||||
|
||||
@@ -419,10 +423,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return [self syncAndConfigStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableSysxCache {
|
||||
return [self configStateSet];
|
||||
}
|
||||
|
||||
+ (NSSet *)keyPathsForValuesAffectingEnableForkAndExitLogging {
|
||||
return [self configStateSet];
|
||||
}
|
||||
@@ -468,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
|
||||
@@ -577,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;
|
||||
@@ -611,8 +614,13 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (BOOL)enableSilentMode {
|
||||
NSNumber *number = self.configState[kEnableSilentModeKey];
|
||||
return number ? [number boolValue] : NO;
|
||||
}
|
||||
|
||||
- (NSString *)aboutText {
|
||||
return self.configState[kAboutText];
|
||||
return self.configState[kAboutTextKey];
|
||||
}
|
||||
|
||||
- (NSURL *)moreInfoURL {
|
||||
@@ -755,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 {
|
||||
@@ -782,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;
|
||||
@@ -855,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];
|
||||
}
|
||||
|
||||
///
|
||||
@@ -959,7 +964,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
|
||||
syncState[kAllowedPathRegexKey] = [syncState[kAllowedPathRegexKey] pattern];
|
||||
syncState[kBlockedPathRegexKey] = [syncState[kBlockedPathRegexKey] pattern];
|
||||
[syncState writeToFile:kSyncStateFilePath atomically:YES];
|
||||
[[NSFileManager defaultManager] setAttributes:@{NSFilePosixPermissions : @0644}
|
||||
[[NSFileManager defaultManager] setAttributes:@{NSFilePosixPermissions : @0600}
|
||||
ofItemAtPath:kSyncStateFilePath
|
||||
error:NULL];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -12,6 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class MOLCodesignChecker;
|
||||
@@ -32,6 +33,14 @@
|
||||
///
|
||||
- (instancetype)initWithPath:(NSString *)path error:(NSError **)error;
|
||||
|
||||
///
|
||||
/// Convenience initializer.
|
||||
///
|
||||
/// @param esFile Pointer to an es_file_t provided by the EndpointSecurity framework.
|
||||
/// Assumes that the path is a resolved path.
|
||||
///
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile error:(NSError **)error;
|
||||
|
||||
///
|
||||
/// Convenience initializer.
|
||||
///
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2015 Google Inc. All rights reserved.
|
||||
/// Copyright 2015-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -25,6 +25,8 @@
|
||||
#include <sys/stat.h>
|
||||
#include <sys/xattr.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
|
||||
// Simple class to hold the data of a mach_header and the offset within the file
|
||||
// in which that header was found.
|
||||
@interface MachHeaderWithOffset : NSObject
|
||||
@@ -48,6 +50,7 @@
|
||||
@property NSFileHandle *fileHandle;
|
||||
@property NSUInteger fileSize;
|
||||
@property NSString *fileOwnerHomeDir;
|
||||
@property NSString *sha256Storage;
|
||||
|
||||
// Cached properties
|
||||
@property NSBundle *bundleRef;
|
||||
@@ -63,6 +66,26 @@
|
||||
extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
|
||||
- (instancetype)initWithResolvedPath:(NSString *)path error:(NSError **)error {
|
||||
struct stat fileStat;
|
||||
if (path.length) {
|
||||
lstat(path.UTF8String, &fileStat);
|
||||
}
|
||||
return [self initWithResolvedPath:path stat:&fileStat error:error];
|
||||
}
|
||||
|
||||
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile error:(NSError **)error {
|
||||
return [self initWithResolvedPath:@(esFile->path.data) stat:&esFile->stat error:error];
|
||||
}
|
||||
|
||||
- (instancetype)initWithResolvedPath:(NSString *)path
|
||||
stat:(const struct stat *)fileStat
|
||||
error:(NSError **)error {
|
||||
if (!fileStat) {
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"NULL stat buffer unsupported");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_path = path;
|
||||
@@ -76,9 +99,7 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
return nil;
|
||||
}
|
||||
|
||||
struct stat fileStat;
|
||||
lstat(_path.UTF8String, &fileStat);
|
||||
if (!((S_IFMT & fileStat.st_mode) == S_IFREG)) {
|
||||
if (!((S_IFMT & fileStat->st_mode) == S_IFREG)) {
|
||||
if (error) {
|
||||
NSString *errStr = [NSString stringWithFormat:@"Non regular file: %s", strerror(errno)];
|
||||
*error = [NSError errorWithDomain:@"com.google.santa.fileinfo"
|
||||
@@ -88,12 +109,12 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
return nil;
|
||||
}
|
||||
|
||||
_fileSize = fileStat.st_size;
|
||||
_fileSize = fileStat->st_size;
|
||||
|
||||
if (_fileSize == 0) return nil;
|
||||
|
||||
if (fileStat.st_uid != 0) {
|
||||
struct passwd *pwd = getpwuid(fileStat.st_uid);
|
||||
if (fileStat->st_uid != 0) {
|
||||
struct passwd *pwd = getpwuid(fileStat->st_uid);
|
||||
if (pwd) {
|
||||
_fileOwnerHomeDir = @(pwd->pw_dir);
|
||||
}
|
||||
@@ -214,9 +235,13 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
|
||||
}
|
||||
|
||||
- (NSString *)SHA256 {
|
||||
NSString *sha256;
|
||||
[self hashSHA1:NULL SHA256:&sha256];
|
||||
return sha256;
|
||||
// Memoize the value
|
||||
if (!self.sha256Storage) {
|
||||
NSString *sha256;
|
||||
[self hashSHA1:NULL SHA256:&sha256];
|
||||
self.sha256Storage = sha256;
|
||||
}
|
||||
return self.sha256Storage;
|
||||
}
|
||||
|
||||
#pragma mark File Type Info
|
||||
|
||||
34
Source/common/SNTKVOManager.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// The callback type when KVO notifications are received for observed key paths.
|
||||
// The first parameter is the previous value, the second paramter is the new value.
|
||||
typedef void (^KVOCallback)(id oldValue, id newValue);
|
||||
|
||||
@interface SNTKVOManager : NSObject
|
||||
|
||||
// Add an observer for the selector on the given object. When a KVO notification
|
||||
// is received, the callback is called. If the notification contains objects that
|
||||
// are not of the expectedType, nil is passed as the argument to the callback.
|
||||
// The observer is removed when the returned instance is deallocated.
|
||||
- (instancetype)initWithObject:(id)object
|
||||
selector:(SEL)selector
|
||||
type:(Class)expectedType
|
||||
callback:(KVOCallback)callback;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
72
Source/common/SNTKVOManager.mm
Normal file
@@ -0,0 +1,72 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/common/SNTKVOManager.h"
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
|
||||
@interface SNTKVOManager ()
|
||||
@property KVOCallback callback;
|
||||
@property Class expectedType;
|
||||
@property NSString *keyPath;
|
||||
@property id object;
|
||||
@end
|
||||
|
||||
@implementation SNTKVOManager
|
||||
|
||||
- (instancetype)initWithObject:(id)object
|
||||
selector:(SEL)selector
|
||||
type:(Class)expectedType
|
||||
callback:(KVOCallback)callback {
|
||||
self = [super self];
|
||||
if (self) {
|
||||
NSString *selectorName = NSStringFromSelector(selector);
|
||||
if (![object respondsToSelector:selector]) {
|
||||
LOGE(@"Attempt to add observer for an unknown selector (%@) for object (%@)", selectorName,
|
||||
[object class]);
|
||||
return nil;
|
||||
}
|
||||
|
||||
_object = object;
|
||||
_keyPath = selectorName;
|
||||
_expectedType = expectedType;
|
||||
_callback = callback;
|
||||
|
||||
[object addObserver:self
|
||||
forKeyPath:selectorName
|
||||
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
|
||||
context:NULL];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self.object removeObserver:self forKeyPath:self.keyPath context:NULL];
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||
ofObject:(id)object
|
||||
change:(NSDictionary<NSString *, id> *)change
|
||||
context:(void *)context {
|
||||
id oldValue = [change[NSKeyValueChangeOldKey] isKindOfClass:self.expectedType]
|
||||
? change[NSKeyValueChangeOldKey]
|
||||
: nil;
|
||||
id newValue = [change[NSKeyValueChangeNewKey] isKindOfClass:self.expectedType]
|
||||
? change[NSKeyValueChangeNewKey]
|
||||
: nil;
|
||||
|
||||
self.callback(oldValue, newValue);
|
||||
}
|
||||
|
||||
@end
|
||||
129
Source/common/SNTKVOManagerTest.mm
Normal file
@@ -0,0 +1,129 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/common/SNTKVOManager.h"
|
||||
|
||||
@interface Foo : NSObject
|
||||
@property NSNumber *propNumber;
|
||||
@property NSArray *propArray;
|
||||
@property id propId;
|
||||
@end
|
||||
|
||||
@implementation Foo
|
||||
@end
|
||||
|
||||
@interface SNTKVOManagerTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTKVOManagerTest
|
||||
|
||||
- (void)testInvalidSelector {
|
||||
Foo *foo = [[Foo alloc] init];
|
||||
|
||||
SNTKVOManager *kvo = [[SNTKVOManager alloc] initWithObject:foo
|
||||
selector:NSSelectorFromString(@"doesNotExist")
|
||||
type:[NSNumber class]
|
||||
callback:^(id, id){
|
||||
}];
|
||||
|
||||
XCTAssertNil(kvo);
|
||||
}
|
||||
|
||||
- (void)testNormalOperation {
|
||||
Foo *foo = [[Foo alloc] init];
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
int origVal = 123;
|
||||
int update1 = 456;
|
||||
int update2 = 789;
|
||||
|
||||
foo.propNumber = @(origVal);
|
||||
|
||||
// Store the values from the callback to test against expected values
|
||||
__block int oldVal;
|
||||
__block int newVal;
|
||||
|
||||
SNTKVOManager *kvo =
|
||||
[[SNTKVOManager alloc] initWithObject:foo
|
||||
selector:@selector(propNumber)
|
||||
type:[NSNumber class]
|
||||
callback:^(NSNumber *oldValue, NSNumber *newValue) {
|
||||
oldVal = [oldValue intValue];
|
||||
newVal = [newValue intValue];
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
XCTAssertNotNil(kvo);
|
||||
|
||||
// Ensure an update to the observed property triggers the callback
|
||||
foo.propNumber = @(update1);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for first observable update");
|
||||
XCTAssertEqual(oldVal, origVal);
|
||||
XCTAssertEqual(newVal, update1);
|
||||
|
||||
// One more time why not
|
||||
foo.propNumber = @(update2);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for second observable update");
|
||||
XCTAssertEqual(oldVal, update1);
|
||||
XCTAssertEqual(newVal, update2);
|
||||
}
|
||||
|
||||
- (void)testUnexpectedTypes {
|
||||
Foo *foo = [[Foo alloc] init];
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
|
||||
NSString *origVal = @"any_val";
|
||||
NSString *update = @"new_val";
|
||||
foo.propId = origVal;
|
||||
|
||||
__block id oldVal;
|
||||
__block id newVal;
|
||||
|
||||
SNTKVOManager *kvo = [[SNTKVOManager alloc] initWithObject:foo
|
||||
selector:@selector(propId)
|
||||
type:[NSString class]
|
||||
callback:^(id oldValue, id newValue) {
|
||||
oldVal = oldValue;
|
||||
newVal = newValue;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
XCTAssertNotNil(kvo);
|
||||
|
||||
// Update to an unexpected type (here, NSNumber instead of NSString)
|
||||
foo.propId = @(123);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for first observable update");
|
||||
XCTAssertEqualObjects(oldVal, origVal);
|
||||
XCTAssertNil(newVal);
|
||||
|
||||
// Update again with an expected type, ensure oldVal is now nil
|
||||
foo.propId = update;
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
|
||||
"Failed waiting for first observable update");
|
||||
XCTAssertNil(oldVal);
|
||||
XCTAssertEqualObjects(newVal, update);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -1,4 +1,5 @@
|
||||
load("@build_bazel_rules_apple//apple:macos.bzl", "macos_application")
|
||||
load("//:helper.bzl", "santa_unit_test")
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
@@ -31,6 +32,9 @@ objc_library(
|
||||
"SNTNotificationManager.m",
|
||||
"main.m",
|
||||
],
|
||||
hdrs = [
|
||||
"SNTNotificationManager.h",
|
||||
],
|
||||
data = [
|
||||
"Resources/AboutWindow.xib",
|
||||
"Resources/DeviceMessageWindow.xib",
|
||||
@@ -49,6 +53,7 @@ objc_library(
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"//Source/common:SNTStrengthify",
|
||||
"//Source/common:SNTSyncConstants",
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"//Source/common:SNTXPCNotifierInterface",
|
||||
"@MOLCertificate",
|
||||
@@ -89,3 +94,26 @@ macos_application(
|
||||
visibility = ["//:santa_package_group"],
|
||||
deps = [":SantaGUI_lib"],
|
||||
)
|
||||
|
||||
santa_unit_test(
|
||||
name = "SNTNotificationManagerTest",
|
||||
srcs = [
|
||||
"SNTNotificationManagerTest.m",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"Cocoa",
|
||||
],
|
||||
deps = [
|
||||
":SantaGUI_lib",
|
||||
"//Source/common:SNTStoredEvent",
|
||||
"@OCMock",
|
||||
],
|
||||
)
|
||||
|
||||
test_suite(
|
||||
name = "unit_tests",
|
||||
tests = [
|
||||
":SNTNotificationManagerTest",
|
||||
],
|
||||
visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
@@ -12,7 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santa/SNTAboutWindowController.h"
|
||||
#import "Source/gui/SNTAboutWindowController.h"
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santa/SNTAccessibleTextField.h"
|
||||
#import "Source/gui/SNTAccessibleTextField.h"
|
||||
|
||||
@implementation SNTAccessibleTextField
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santa/SNTAppDelegate.h"
|
||||
#import "Source/gui/SNTAppDelegate.h"
|
||||
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTStrengthify.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santa/SNTAboutWindowController.h"
|
||||
#import "Source/santa/SNTNotificationManager.h"
|
||||
#import "Source/gui/SNTAboutWindowController.h"
|
||||
#import "Source/gui/SNTNotificationManager.h"
|
||||
|
||||
@interface SNTAppDelegate ()
|
||||
@property SNTAboutWindowController *aboutWindowController;
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#import "Source/santa/SNTMessageWindowController.h"
|
||||
#import "Source/gui/SNTMessageWindowController.h"
|
||||
|
||||
@class SNTStoredEvent;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santa/SNTBinaryMessageWindowController.h"
|
||||
#import "Source/gui/SNTBinaryMessageWindowController.h"
|
||||
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <SecurityInterface/SFCertificatePanel.h>
|
||||
@@ -20,7 +20,7 @@
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/santa/SNTMessageWindow.h"
|
||||
#import "Source/gui/SNTMessageWindow.h"
|
||||
|
||||
@interface SNTBinaryMessageWindowController ()
|
||||
/// The custom message to display for this event
|
||||
@@ -139,7 +139,9 @@
|
||||
- (NSString *)publisherInfo {
|
||||
MOLCertificate *leafCert = [self.event.signingChain firstObject];
|
||||
|
||||
if (leafCert.commonName && leafCert.orgName) {
|
||||
if ([leafCert.commonName isEqualToString:@"Apple Mac OS Application Signing"]) {
|
||||
return [NSString stringWithFormat:@"App Store (Team ID: %@)", self.event.teamID];
|
||||
} else if (leafCert.commonName && leafCert.orgName) {
|
||||
return [NSString stringWithFormat:@"%@ - %@", leafCert.orgName, leafCert.commonName];
|
||||
} else if (leafCert.commonName) {
|
||||
return leafCert.commonName;
|
||||
@@ -14,7 +14,7 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/santa/SNTMessageWindowController.h"
|
||||
#import "Source/gui/SNTMessageWindowController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santa/SNTDeviceMessageWindowController.h"
|
||||
#import "Source/gui/SNTDeviceMessageWindowController.h"
|
||||
|
||||
#import "Source/common/SNTBlockMessage.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTDeviceEvent.h"
|
||||
#import "Source/santa/SNTMessageWindow.h"
|
||||
#import "Source/gui/SNTMessageWindow.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santa/SNTMessageWindow.h"
|
||||
#import "Source/gui/SNTMessageWindow.h"
|
||||
|
||||
@implementation SNTMessageWindow
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#import "Source/santa/SNTMessageWindowController.h"
|
||||
#import "Source/gui/SNTMessageWindowController.h"
|
||||
|
||||
#import "Source/santa/SNTMessageWindow.h"
|
||||
#import "Source/gui/SNTMessageWindow.h"
|
||||
|
||||
@implementation SNTMessageWindowController
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#import "Source/common/SNTXPCNotifierInterface.h"
|
||||
#import "Source/santa/SNTBinaryMessageWindowController.h"
|
||||
#import "Source/santa/SNTDeviceMessageWindowController.h"
|
||||
#import "Source/santa/SNTMessageWindowController.h"
|
||||
#import "Source/gui/SNTBinaryMessageWindowController.h"
|
||||
#import "Source/gui/SNTDeviceMessageWindowController.h"
|
||||
#import "Source/gui/SNTMessageWindowController.h"
|
||||
|
||||
///
|
||||
/// Keeps track of pending notifications and ensures only one is presented to the user at a time.
|
||||
@@ -12,8 +12,9 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import "Source/santa/SNTNotificationManager.h"
|
||||
#import "Source/gui/SNTNotificationManager.h"
|
||||
|
||||
#import <MOLCertificate/MOLCertificate.h>
|
||||
#import <MOLXPCConnection/MOLXPCConnection.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
|
||||
@@ -23,8 +24,9 @@
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#import "Source/common/SNTStrengthify.h"
|
||||
#import "Source/common/SNTSyncConstants.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santa/SNTMessageWindowController.h"
|
||||
#import "Source/gui/SNTMessageWindowController.h"
|
||||
|
||||
@interface SNTNotificationManager ()
|
||||
|
||||
@@ -112,12 +114,59 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
|
||||
pendingMsg.delegate = self;
|
||||
[self.pendingNotifications addObject:pendingMsg];
|
||||
[self postDistributedNotification:pendingMsg];
|
||||
|
||||
if (!self.currentWindowController) {
|
||||
[self showQueuedWindow];
|
||||
}
|
||||
}
|
||||
|
||||
// For blocked execution notifications, post an NSDistributedNotificationCenter
|
||||
// notification with the important details from the stored event. Distributed
|
||||
// notifications are system-wide broadcasts that can be sent by apps and observed
|
||||
// from separate processes. This allows users of Santa to write tools that
|
||||
// perform actions when we block execution, such as trigger management tools or
|
||||
// display an enterprise-specific UI (which is particularly useful when combined
|
||||
// with the EnableSilentMode configuration option, to disable Santa's standard UI).
|
||||
- (void)postDistributedNotification:(SNTMessageWindowController *)pendingMsg {
|
||||
if (![pendingMsg isKindOfClass:[SNTBinaryMessageWindowController class]]) {
|
||||
return;
|
||||
}
|
||||
SNTBinaryMessageWindowController *wc = (SNTBinaryMessageWindowController *)pendingMsg;
|
||||
NSDistributedNotificationCenter *dc = [NSDistributedNotificationCenter defaultCenter];
|
||||
NSMutableArray<NSDictionary *> *signingChain =
|
||||
[NSMutableArray arrayWithCapacity:wc.event.signingChain.count];
|
||||
for (MOLCertificate *cert in wc.event.signingChain) {
|
||||
[signingChain addObject:@{
|
||||
kCertSHA256 : cert.SHA256 ?: @"",
|
||||
kCertCN : cert.commonName ?: @"",
|
||||
kCertOrg : cert.orgName ?: @"",
|
||||
kCertOU : cert.orgUnit ?: @"",
|
||||
kCertValidFrom : @([cert.validFrom timeIntervalSince1970]) ?: @0,
|
||||
kCertValidUntil : @([cert.validUntil timeIntervalSince1970]) ?: @0,
|
||||
}];
|
||||
}
|
||||
NSDictionary *userInfo = @{
|
||||
kFileSHA256 : wc.event.fileSHA256 ?: @"",
|
||||
kFilePath : wc.event.filePath ?: @"",
|
||||
kFileBundleName : wc.event.fileBundleName ?: @"",
|
||||
kFileBundleID : wc.event.fileBundleID ?: @"",
|
||||
kFileBundleVersion : wc.event.fileBundleVersion ?: @"",
|
||||
kFileBundleShortVersionString : wc.event.fileBundleVersionString ?: @"",
|
||||
kTeamID : wc.event.teamID ?: @"",
|
||||
kExecutingUser : wc.event.executingUser ?: @"",
|
||||
kExecutionTime : @([wc.event.occurrenceDate timeIntervalSince1970]) ?: @0,
|
||||
kPID : wc.event.pid ?: @0,
|
||||
kPPID : wc.event.ppid ?: @0,
|
||||
kParentName : wc.event.parentName ?: @"",
|
||||
kSigningChain : signingChain,
|
||||
};
|
||||
|
||||
[dc postNotificationName:@"com.google.santa.notification.blockedeexecution"
|
||||
object:@"com.google.santa"
|
||||
userInfo:userInfo];
|
||||
}
|
||||
|
||||
- (void)showQueuedWindow {
|
||||
// Notifications arrive on a background thread but UI updates must happen on the main thread.
|
||||
// This includes making windows.
|
||||
@@ -208,6 +257,8 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
#pragma mark SNTNotifierXPC protocol methods
|
||||
|
||||
- (void)postClientModeNotification:(SNTClientMode)clientmode {
|
||||
if ([SNTConfigurator configurator].enableSilentMode) return;
|
||||
|
||||
UNUserNotificationCenter *un = [UNUserNotificationCenter currentNotificationCenter];
|
||||
|
||||
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
|
||||
@@ -246,6 +297,8 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
}
|
||||
|
||||
- (void)postRuleSyncNotificationWithCustomMessage:(NSString *)message {
|
||||
if ([SNTConfigurator configurator].enableSilentMode) return;
|
||||
|
||||
UNUserNotificationCenter *un = [UNUserNotificationCenter currentNotificationCenter];
|
||||
|
||||
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
|
||||
@@ -262,6 +315,8 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
}
|
||||
|
||||
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message {
|
||||
if ([SNTConfigurator configurator].enableSilentMode) return;
|
||||
|
||||
if (!event) {
|
||||
LOGI(@"Error: Missing event object in message received from daemon!");
|
||||
return;
|
||||
@@ -274,6 +329,8 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
|
||||
}
|
||||
|
||||
- (void)postUSBBlockNotification:(SNTDeviceEvent *)event withCustomMessage:(NSString *)message {
|
||||
if ([SNTConfigurator configurator].enableSilentMode) return;
|
||||
|
||||
if (!event) {
|
||||
LOGI(@"Error: Missing event object in message received from daemon!");
|
||||
return;
|
||||
74
Source/gui/SNTNotificationManagerTest.m
Normal file
@@ -0,0 +1,74 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "Source/gui/SNTNotificationManager.h"
|
||||
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
|
||||
@class SNTBinaryMessageWindowController;
|
||||
|
||||
@interface SNTNotificationManager (Testing)
|
||||
- (void)hashBundleBinariesForEvent:(SNTStoredEvent *)event
|
||||
withController:(SNTBinaryMessageWindowController *)controller;
|
||||
@end
|
||||
|
||||
@interface SNTNotificationManagerTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SNTNotificationManagerTest
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
fclose(stdout);
|
||||
}
|
||||
|
||||
- (void)testPostBlockNotificationSendsDistributedNotification {
|
||||
SNTStoredEvent *ev = [[SNTStoredEvent alloc] init];
|
||||
ev.fileSHA256 = @"the-sha256";
|
||||
ev.filePath = @"/Applications/Safari.app/Contents/MacOS/Safari";
|
||||
ev.fileBundleName = @"Safari";
|
||||
ev.fileBundlePath = @"/Applications/Safari.app";
|
||||
ev.fileBundleID = @"com.apple.Safari";
|
||||
ev.fileBundleVersion = @"18614.1.14.1.15";
|
||||
ev.fileBundleVersionString = @"16.0";
|
||||
ev.executingUser = @"rah";
|
||||
ev.occurrenceDate = [NSDate dateWithTimeIntervalSince1970:1660221048];
|
||||
ev.decision = SNTEventStateBlockBinary;
|
||||
ev.pid = @84156;
|
||||
ev.ppid = @1;
|
||||
ev.parentName = @"launchd";
|
||||
|
||||
SNTNotificationManager *sut = OCMPartialMock([[SNTNotificationManager alloc] init]);
|
||||
OCMStub([sut hashBundleBinariesForEvent:OCMOCK_ANY withController:OCMOCK_ANY]).andDo(nil);
|
||||
|
||||
id dncMock = OCMClassMock([NSDistributedNotificationCenter class]);
|
||||
OCMStub([dncMock defaultCenter]).andReturn(dncMock);
|
||||
|
||||
[sut postBlockNotification:ev withCustomMessage:@""];
|
||||
|
||||
OCMVerify([dncMock postNotificationName:@"com.google.santa.notification.blockedeexecution"
|
||||
object:@"com.google.santa"
|
||||
userInfo:[OCMArg checkWithBlock:^BOOL(NSDictionary *userInfo) {
|
||||
XCTAssertEqualObjects(userInfo[@"file_sha256"], @"the-sha256");
|
||||
XCTAssertEqualObjects(userInfo[@"pid"], @84156);
|
||||
XCTAssertEqualObjects(userInfo[@"ppid"], @1);
|
||||
XCTAssertEqualObjects(userInfo[@"execution_time"], @1660221048);
|
||||
return YES;
|
||||
}]]);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTXPCControlInterface.h"
|
||||
#import "Source/santa/SNTAppDelegate.h"
|
||||
#import "Source/gui/SNTAppDelegate.h"
|
||||
|
||||
@interface SNTSystemExtensionDelegate : NSObject <OSSystemExtensionRequestDelegate>
|
||||
@end
|
||||
@@ -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);
|
||||
@@ -119,7 +139,7 @@ REGISTER_COMMAND_NAME(@"metrics")
|
||||
printf(">>> Root Labels\n");
|
||||
[self prettyPrintRootLabels:normalizedMetrics[@"root_labels"]];
|
||||
printf("\n");
|
||||
printf(">>> Metrics \n");
|
||||
printf(">>> Metrics\n");
|
||||
[self prettyPrintMetricValues:normalizedMetrics[@"metrics"]];
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
hostname | testHost
|
||||
username | testUser
|
||||
|
||||
>>> Metrics
|
||||
>>> Metrics
|
||||
Metric Name | /santa/rules
|
||||
Description | Number of rules
|
||||
Type | SNTMetricTypeGaugeInt64
|
||||
@@ -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
@@ -105,6 +105,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
|
||||
// This is a Santa-curated list of paths to check on startup. This list will be merged
|
||||
// with the set of default muted paths from ES.
|
||||
|
||||
NSSet *santaDefinedCriticalPaths = [NSSet setWithArray:@[
|
||||
@"/usr/libexec/trustd",
|
||||
@"/usr/lib/dyld",
|
||||
@@ -136,6 +137,12 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
NSMutableDictionary *bins = [NSMutableDictionary dictionary];
|
||||
for (NSString *path in [SNTRuleTable criticalSystemBinaryPaths]) {
|
||||
SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithPath:path];
|
||||
if (!binInfo.SHA256) {
|
||||
// If there isn't a hash, no need to compute the other info here.
|
||||
// Just continue on to the next binary.
|
||||
LOGW(@"Unable to compute hash for critical system binary %@.", path);
|
||||
continue;
|
||||
}
|
||||
MOLCodesignChecker *csInfo = [binInfo codesignCheckerWithError:NULL];
|
||||
|
||||
// Make sure the critical system binary is signed by the same chain as launchd/self
|
||||
@@ -143,9 +150,9 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
|
||||
if ([csInfo signingInformationMatches:self.launchdCSInfo]) {
|
||||
systemBin = YES;
|
||||
} else if (![csInfo signingInformationMatches:self.santadCSInfo]) {
|
||||
LOGE(@"Unable to validate critical system binary. "
|
||||
LOGW(@"Unable to validate critical system binary %@. "
|
||||
@"pid 1: %@, santad: %@ and %@: %@ do not match.",
|
||||
self.launchdCSInfo.leafCertificate, self.santadCSInfo.leafCertificate, path,
|
||||
path, self.launchdCSInfo.leafCertificate, self.santadCSInfo.leafCertificate, path,
|
||||
csInfo.leafCertificate);
|
||||
continue;
|
||||
}
|
||||
|
||||
75
Source/santad/EventProviders/AuthResultCache.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_AUTHRESULTCACHE_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_AUTHRESULTCACHE_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <sys/stat.h>
|
||||
#include <memory>
|
||||
|
||||
#import "Source/common/SNTCommon.h"
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
enum class FlushCacheMode {
|
||||
kNonRootOnly,
|
||||
kAllCaches,
|
||||
};
|
||||
|
||||
class AuthResultCache {
|
||||
public:
|
||||
// Santa currently only flushes caches when new DENY rules are added, not
|
||||
// ALLOW rules. This means this value should be low enough so that if a
|
||||
// previously denied binary is allowed, it can be re-executed by the user in a
|
||||
// timely manner. But the value should be high enough to allow the cache to be
|
||||
// effective in the event the binary is executed in rapid succession.
|
||||
AuthResultCache(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms = 1500);
|
||||
virtual ~AuthResultCache();
|
||||
|
||||
AuthResultCache(AuthResultCache &&other) = delete;
|
||||
AuthResultCache &operator=(AuthResultCache &&rhs) = delete;
|
||||
AuthResultCache(const AuthResultCache &other) = delete;
|
||||
AuthResultCache &operator=(const AuthResultCache &other) = delete;
|
||||
|
||||
virtual bool AddToCache(const es_file_t *es_file, santa_action_t decision);
|
||||
virtual void RemoveFromCache(const es_file_t *es_file);
|
||||
virtual santa_action_t CheckCache(const es_file_t *es_file);
|
||||
virtual santa_action_t CheckCache(santa_vnode_id_t vnode_id);
|
||||
|
||||
virtual void FlushCache(FlushCacheMode mode);
|
||||
|
||||
virtual NSArray<NSNumber *> *CacheCounts();
|
||||
|
||||
private:
|
||||
virtual SantaCache<santa_vnode_id_t, uint64_t> *CacheForVnodeID(santa_vnode_id_t vnode_id);
|
||||
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *root_cache_;
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *nonroot_cache_;
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
uint64_t root_devno_;
|
||||
uint64_t cache_deny_time_ns_;
|
||||
dispatch_queue_t q_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
|
||||
#endif
|
||||
156
Source/santad/EventProviders/AuthResultCache.mm
Normal file
@@ -0,0 +1,156 @@
|
||||
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
|
||||
#include <mach/clock_types.h>
|
||||
#include <time.h>
|
||||
|
||||
#import "Source/common/SNTLogging.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
|
||||
template <>
|
||||
uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const &t) {
|
||||
return (SantaCacheHasher<uint64_t>(t.fsid) << 1) ^ SantaCacheHasher<uint64_t>(t.fileid);
|
||||
}
|
||||
|
||||
namespace santa::santad::event_providers {
|
||||
|
||||
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
|
||||
return santa_vnode_id_t{
|
||||
.fsid = (uint64_t)es_file->stat.st_dev,
|
||||
.fileid = es_file->stat.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
static inline uint64_t GetCurrentUptime() {
|
||||
return clock_gettime_nsec_np(CLOCK_MONOTONIC);
|
||||
}
|
||||
|
||||
// Decision is stored in upper 8 bits, timestamp in remaining 56.
|
||||
static inline uint64_t CacheableAction(santa_action_t action,
|
||||
uint64_t timestamp = GetCurrentUptime()) {
|
||||
return ((uint64_t)action << 56) | (timestamp & 0xFFFFFFFFFFFFFF);
|
||||
}
|
||||
|
||||
static inline santa_action_t ActionFromCachedValue(uint64_t cachedValue) {
|
||||
return (santa_action_t)(cachedValue >> 56);
|
||||
}
|
||||
|
||||
static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
|
||||
return (cachedValue & ~(0xFF00000000000000));
|
||||
}
|
||||
|
||||
AuthResultCache::AuthResultCache(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
uint64_t cache_deny_time_ms)
|
||||
: esapi_(esapi), cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
|
||||
root_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
nonroot_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
|
||||
|
||||
struct stat sb;
|
||||
if (stat("/", &sb) == 0) {
|
||||
root_devno_ = sb.st_dev;
|
||||
}
|
||||
|
||||
q_ = dispatch_queue_create(
|
||||
"com.google.santa.daemon.auth_result_cache.q",
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
|
||||
QOS_CLASS_USER_INTERACTIVE, 0));
|
||||
}
|
||||
|
||||
AuthResultCache::~AuthResultCache() {
|
||||
delete root_cache_;
|
||||
delete nonroot_cache_;
|
||||
}
|
||||
|
||||
bool AuthResultCache::AddToCache(const es_file_t *es_file, santa_action_t decision) {
|
||||
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
|
||||
switch (decision) {
|
||||
case ACTION_REQUEST_BINARY:
|
||||
return cache->set(vnode_id, CacheableAction(ACTION_REQUEST_BINARY, 0), 0);
|
||||
case ACTION_RESPOND_ALLOW: OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_ALLOW_COMPILER: OS_FALLTHROUGH;
|
||||
case ACTION_RESPOND_DENY:
|
||||
return cache->set(vnode_id, CacheableAction(decision),
|
||||
CacheableAction(ACTION_REQUEST_BINARY, 0));
|
||||
default:
|
||||
// This is a programming error. Bail.
|
||||
LOGE(@"Invalid cache value, exiting.");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
void AuthResultCache::RemoveFromCache(const es_file_t *es_file) {
|
||||
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
|
||||
CacheForVnodeID(vnode_id)->remove(vnode_id);
|
||||
}
|
||||
|
||||
santa_action_t AuthResultCache::CheckCache(const es_file_t *es_file) {
|
||||
return CheckCache(VnodeForFile(es_file));
|
||||
}
|
||||
|
||||
santa_action_t AuthResultCache::CheckCache(santa_vnode_id_t vnode_id) {
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
|
||||
|
||||
uint64_t cached_val = cache->get(vnode_id);
|
||||
if (cached_val == 0) {
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
|
||||
santa_action_t result = ActionFromCachedValue(cached_val);
|
||||
|
||||
if (result == ACTION_RESPOND_DENY) {
|
||||
uint64_t expiry_time = TimestampFromCachedValue(cached_val) + cache_deny_time_ns_;
|
||||
if (expiry_time < GetCurrentUptime()) {
|
||||
cache->remove(vnode_id);
|
||||
return ACTION_UNSET;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
SantaCache<santa_vnode_id_t, uint64_t> *AuthResultCache::CacheForVnodeID(
|
||||
santa_vnode_id_t vnode_id) {
|
||||
return (vnode_id.fsid == root_devno_ || root_devno_ == 0) ? root_cache_ : nonroot_cache_;
|
||||
}
|
||||
|
||||
void AuthResultCache::FlushCache(FlushCacheMode mode) {
|
||||
nonroot_cache_->clear();
|
||||
if (mode == FlushCacheMode::kAllCaches) {
|
||||
root_cache_->clear();
|
||||
|
||||
// Clear the ES cache when all local caches are flushed. Assume the ES cache
|
||||
// doesn't need to be cleared when only flushing the non-root cache.
|
||||
//
|
||||
// Calling into ES should be done asynchronously since it could otherwise
|
||||
// potentially deadlock.
|
||||
auto shared_esapi = esapi_->shared_from_this();
|
||||
dispatch_async(q_, ^{
|
||||
// ES does not need a connected client to clear cache
|
||||
shared_esapi->ClearCache(Client());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
NSArray<NSNumber *> *AuthResultCache::CacheCounts() {
|
||||
return @[ @(root_cache_->count()), @(nonroot_cache_->count()) ];
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers
|
||||
225
Source/santad/EventProviders/AuthResultCacheTest.mm
Normal file
@@ -0,0 +1,225 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <Foundation/Foundation.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/SNTCommon.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/AuthResultCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
|
||||
using santa::santad::event_providers::AuthResultCache;
|
||||
using santa::santad::event_providers::FlushCacheMode;
|
||||
|
||||
// Grab the st_dev number of the root volume to match the root cache
|
||||
static uint64_t RootDevno() {
|
||||
static dispatch_once_t once_token;
|
||||
static uint64_t devno;
|
||||
dispatch_once(&once_token, ^{
|
||||
struct stat sb;
|
||||
stat("/", &sb);
|
||||
devno = sb.st_dev;
|
||||
});
|
||||
return devno;
|
||||
}
|
||||
|
||||
static inline es_file_t MakeCacheableFile(uint64_t devno, uint64_t ino) {
|
||||
return es_file_t{
|
||||
.path = {}, .path_truncated = false, .stat = {.st_dev = (dev_t)devno, .st_ino = ino}};
|
||||
}
|
||||
|
||||
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
|
||||
return santa_vnode_id_t{
|
||||
.fsid = (uint64_t)es_file->stat.st_dev,
|
||||
.fileid = es_file->stat.st_ino,
|
||||
};
|
||||
}
|
||||
|
||||
static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uint64_t root_count,
|
||||
uint64_t nonroot_count) {
|
||||
NSArray<NSNumber *> *counts = cache->CacheCounts();
|
||||
|
||||
XCTAssertNotNil(counts);
|
||||
XCTAssertEqual([counts count], 2);
|
||||
XCTAssertNotNil(counts[0]);
|
||||
XCTAssertNotNil(counts[1]);
|
||||
XCTAssertEqual([counts[0] unsignedLongLongValue], root_count);
|
||||
XCTAssertEqual([counts[1] unsignedLongLongValue], nonroot_count);
|
||||
}
|
||||
|
||||
@interface AuthResultCacheTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation AuthResultCacheTest
|
||||
|
||||
- (void)testEmptyCacheExpectedNumberOfCacheCounts {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testBasicOperation {
|
||||
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(esapi);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 222);
|
||||
|
||||
// Add the root file to the cache
|
||||
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_UNSET);
|
||||
|
||||
// Now add the non-root file
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
// Update the cached values
|
||||
cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW);
|
||||
cache->AddToCache(&nonrootFile, ACTION_RESPOND_DENY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
XCTAssertEqual(cache->CheckCache(VnodeForFile(&rootFile)), ACTION_RESPOND_ALLOW);
|
||||
XCTAssertEqual(cache->CheckCache(VnodeForFile(&nonrootFile)), ACTION_RESPOND_DENY);
|
||||
|
||||
// Remove the root file
|
||||
cache->RemoveFromCache(&rootFile);
|
||||
|
||||
AssertCacheCounts(cache, 0, 1);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_RESPOND_DENY);
|
||||
}
|
||||
|
||||
- (void)testFlushCache {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 111);
|
||||
|
||||
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
// Flush non-root only
|
||||
cache->FlushCache(FlushCacheMode::kNonRootOnly);
|
||||
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
// Add back the non-root file
|
||||
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
|
||||
|
||||
AssertCacheCounts(cache, 1, 1);
|
||||
|
||||
// Flush all caches
|
||||
// The call to ClearCache is asynchronous. Use a semaphore to
|
||||
// be notified when the mock is called.
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
EXPECT_CALL(*mockESApi, ClearCache).WillOnce(testing::InvokeWithoutArgs(^() {
|
||||
dispatch_semaphore_signal(sema);
|
||||
return true;
|
||||
}));
|
||||
cache->FlushCache(FlushCacheMode::kAllCaches);
|
||||
|
||||
XCTAssertEqual(0,
|
||||
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
|
||||
"ClearCache wasn't called within expected time window");
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
- (void)testCacheStateMachine {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
// Cached items must first be in the ACTION_REQUEST_BINARY state
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW));
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW_COMPILER));
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
// Items in the `ACTION_REQUEST_BINARY` state cannot reenter the same state
|
||||
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
santa_action_t allowed_transitions[] = {
|
||||
ACTION_RESPOND_ALLOW,
|
||||
ACTION_RESPOND_ALLOW_COMPILER,
|
||||
ACTION_RESPOND_DENY,
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(allowed_transitions) / sizeof(allowed_transitions[0]); i++) {
|
||||
// First make sure the item doesn't exist
|
||||
cache->RemoveFromCache(&rootFile);
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
|
||||
// Now add the item to be in the first allowed state
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
|
||||
|
||||
// Now assert the allowed transition
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, allowed_transitions[i]));
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), allowed_transitions[i]);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testCacheExpiry {
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
// Create a cache with a lowered cache expiry value
|
||||
uint64_t expiryMS = 250;
|
||||
auto cache = std::make_shared<AuthResultCache>(mockESApi, expiryMS);
|
||||
|
||||
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
|
||||
|
||||
// Add a file to the cache and put into the ACTION_RESPOND_DENY state
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
|
||||
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
|
||||
|
||||
// Ensure the file exists
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_RESPOND_DENY);
|
||||
|
||||
// Wait for the item to expire
|
||||
SleepMS(expiryMS);
|
||||
|
||||
// Check cache counts to make sure the item still exists
|
||||
AssertCacheCounts(cache, 1, 0);
|
||||
|
||||
// Now check the cache, which will remove the item
|
||||
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
|
||||
AssertCacheCounts(cache, 0, 0);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Copyright 2021 Google Inc. All rights reserved.
|
||||
/// Copyright 2021-2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
@@ -50,7 +50,8 @@ typedef void (^MockDADiskAppearedCallback)(DADiskRef ref);
|
||||
@end
|
||||
|
||||
//
|
||||
// All DiskArbitration functions used in SNTDeviceManager and shimmed out accordingly.
|
||||
// All DiskArbitration functions used in SNTEndpointSecurityDeviceManager
|
||||
// and shimmed out accordingly.
|
||||
//
|
||||
CF_EXTERN_C_BEGIN
|
||||
|
||||
|
||||
69
Source/santad/EventProviders/EndpointSecurity/Client.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_CLIENT_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_CLIENT_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class Client {
|
||||
public:
|
||||
explicit Client(es_client_t* client, es_new_client_result_t result)
|
||||
: client_(client), result_(result) {}
|
||||
|
||||
Client() : client_(nullptr), result_(ES_NEW_CLIENT_RESULT_ERR_INTERNAL) {}
|
||||
|
||||
virtual ~Client() {
|
||||
if (client_) {
|
||||
// Special case: Not using EndpointSecurityAPI here due to circular refs.
|
||||
es_delete_client(client_);
|
||||
}
|
||||
}
|
||||
|
||||
Client(Client&& other) {
|
||||
client_ = other.client_;
|
||||
result_ = other.result_;
|
||||
other.client_ = nullptr;
|
||||
other.result_ = ES_NEW_CLIENT_RESULT_ERR_INTERNAL;
|
||||
}
|
||||
|
||||
Client& operator=(Client&& rhs) {
|
||||
client_ = rhs.client_;
|
||||
result_ = rhs.result_;
|
||||
rhs.client_ = nullptr;
|
||||
rhs.result_ = ES_NEW_CLIENT_RESULT_ERR_INTERNAL;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Client(const Client& other) = delete;
|
||||
void operator=(const Client& rhs) = delete;
|
||||
|
||||
inline bool IsConnected() { return result_ == ES_NEW_CLIENT_RESULT_SUCCESS; }
|
||||
|
||||
inline es_new_client_result_t NewClientResult() { return result_; }
|
||||
|
||||
inline es_client_t* Get() const { return client_; }
|
||||
|
||||
private:
|
||||
es_client_t* client_;
|
||||
es_new_client_result_t result_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
118
Source/santad/EventProviders/EndpointSecurity/ClientTest.mm
Normal file
@@ -0,0 +1,118 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Client;
|
||||
|
||||
// Global semaphore used for custom `es_delete_client` function
|
||||
dispatch_semaphore_t gSema;
|
||||
|
||||
// Note: The Client class does not use the `EndpointSecurityAPI` wrappers due
|
||||
// to circular dependency issues. It is a special case that uses the underlying
|
||||
// ES API `es_delete_client` directly. This test override will signal the
|
||||
// `gSema` semaphore to indicate it has been called.
|
||||
es_return_t es_delete_client(es_client_t *_Nullable client) {
|
||||
dispatch_semaphore_signal(gSema);
|
||||
return ES_RETURN_SUCCESS;
|
||||
};
|
||||
|
||||
@interface ClientTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation ClientTest
|
||||
|
||||
- (void)setUp {
|
||||
gSema = dispatch_semaphore_create(0);
|
||||
}
|
||||
|
||||
- (void)testConstructorsAndDestructors {
|
||||
// Ensure constructors set internal state properly
|
||||
// Anonymous scopes used to ensure destructors called as expected
|
||||
|
||||
// Null `es_client_t*` *shouldn't* trigger `es_delete_client`
|
||||
{
|
||||
Client c;
|
||||
XCTAssertEqual(c.Get(), nullptr);
|
||||
XCTAssertEqual(c.NewClientResult(), ES_NEW_CLIENT_RESULT_ERR_INTERNAL);
|
||||
}
|
||||
|
||||
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client called unexpectedly");
|
||||
|
||||
// Nonnull `es_client_t*` *should* trigger `es_delete_client`
|
||||
{
|
||||
int fake;
|
||||
es_client_t *fakeClient = (es_client_t *)&fake;
|
||||
Client c(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
XCTAssertEqual(c.Get(), fakeClient);
|
||||
XCTAssertEqual(c.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client not called within expected time window");
|
||||
|
||||
// Test move constructor
|
||||
{
|
||||
int fake;
|
||||
es_client_t *fakeClient = (es_client_t *)&fake;
|
||||
Client c1(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
|
||||
Client c2(std::move(c1));
|
||||
|
||||
XCTAssertEqual(c1.Get(), nullptr);
|
||||
XCTAssertEqual(c2.Get(), fakeClient);
|
||||
XCTAssertEqual(c2.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
// Ensure `es_delete_client` was only called once when both `c1` and `c2`
|
||||
// are destructed.
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client not called within expected time window");
|
||||
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client called unexpectedly");
|
||||
|
||||
// Test move assignment
|
||||
{
|
||||
int fake;
|
||||
es_client_t *fakeClient = (es_client_t *)&fake;
|
||||
Client c1(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
Client c2;
|
||||
|
||||
c2 = std::move(c1);
|
||||
|
||||
XCTAssertEqual(c1.Get(), nullptr);
|
||||
XCTAssertEqual(c2.Get(), fakeClient);
|
||||
XCTAssertEqual(c2.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
// Ensure `es_delete_client` was only called once when both `c1` and `c2`
|
||||
// are destructed.
|
||||
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client not called within expected time window");
|
||||
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
|
||||
"es_delete_client called unexpectedly");
|
||||
}
|
||||
|
||||
- (void)testIsConnected {
|
||||
XCTAssertFalse(Client().IsConnected());
|
||||
XCTAssertFalse(Client(nullptr, ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED).IsConnected());
|
||||
XCTAssertTrue(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS).IsConnected());
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,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
@@ -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
@@ -0,0 +1,44 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHER_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHER_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Source/common/SantaCache.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class Enricher {
|
||||
public:
|
||||
Enricher();
|
||||
virtual ~Enricher() = default;
|
||||
virtual std::shared_ptr<EnrichedMessage> Enrich(Message &&msg);
|
||||
virtual EnrichedProcess Enrich(const es_process_t &es_proc);
|
||||
virtual EnrichedFile Enrich(const es_file_t &es_file);
|
||||
|
||||
virtual std::optional<std::shared_ptr<std::string>> UsernameForUID(uid_t uid);
|
||||
virtual std::optional<std::shared_ptr<std::string>> UsernameForGID(gid_t gid);
|
||||
|
||||
private:
|
||||
SantaCache<uid_t, std::optional<std::shared_ptr<std::string>>>
|
||||
username_cache_;
|
||||
SantaCache<gid_t, std::optional<std::shared_ptr<std::string>>>
|
||||
groupname_cache_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
137
Source/santad/EventProviders/EndpointSecurity/Enricher.mm
Normal file
@@ -0,0 +1,137 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
|
||||
#include <EndpointSecurity/ESTypes.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <grp.h>
|
||||
#include <pwd.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <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
@@ -0,0 +1,60 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MESSAGE_H
|
||||
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MESSAGE_H
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
class EndpointSecurityAPI;
|
||||
|
||||
class Message {
|
||||
public:
|
||||
Message(std::shared_ptr<EndpointSecurityAPI> esapi,
|
||||
const es_message_t* es_msg);
|
||||
~Message();
|
||||
|
||||
Message(Message&& other);
|
||||
// Note: Safe to implement this, just not currently needed so left deleted.
|
||||
Message& operator=(Message&& rhs) = delete;
|
||||
|
||||
// In macOS 10.15, es_retain_message/es_release_message were unsupported
|
||||
// and required a full copy, which impacts performance if done too much...
|
||||
Message(const Message& other);
|
||||
Message& operator=(const Message& other) = delete;
|
||||
|
||||
// Operators to access underlying es_message_t
|
||||
const es_message_t* operator->() const { return es_msg_; }
|
||||
const es_message_t& operator*() const { return *es_msg_; }
|
||||
|
||||
std::string ParentProcessName() const;
|
||||
|
||||
private:
|
||||
std::shared_ptr<EndpointSecurityAPI> esapi_;
|
||||
es_message_t* es_msg_;
|
||||
|
||||
mutable std::string pname_;
|
||||
mutable std::string parent_pname_;
|
||||
|
||||
std::string GetProcessName(pid_t pid) const;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
|
||||
#endif
|
||||
65
Source/santad/EventProviders/EndpointSecurity/Message.mm
Normal file
@@ -0,0 +1,65 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
|
||||
#include <bsm/libbsm.h>
|
||||
#include <libproc.h>
|
||||
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
|
||||
namespace santa::santad::event_providers::endpoint_security {
|
||||
|
||||
Message::Message(std::shared_ptr<EndpointSecurityAPI> esapi, const es_message_t *es_msg)
|
||||
: esapi_(esapi) {
|
||||
es_msg_ = esapi_->RetainMessage(es_msg);
|
||||
}
|
||||
|
||||
Message::~Message() {
|
||||
if (es_msg_) {
|
||||
esapi_->ReleaseMessage(es_msg_);
|
||||
}
|
||||
}
|
||||
|
||||
Message::Message(Message &&other) {
|
||||
esapi_ = std::move(other.esapi_);
|
||||
es_msg_ = other.es_msg_;
|
||||
other.es_msg_ = nullptr;
|
||||
}
|
||||
|
||||
Message::Message(const Message &other) {
|
||||
esapi_ = other.esapi_;
|
||||
es_msg_ = other.es_msg_;
|
||||
esapi_->RetainMessage(es_msg_);
|
||||
}
|
||||
|
||||
std::string Message::ParentProcessName() const {
|
||||
if (parent_pname_.length() == 0) {
|
||||
parent_pname_ = GetProcessName(es_msg_->process->ppid);
|
||||
}
|
||||
return parent_pname_;
|
||||
}
|
||||
|
||||
std::string Message::GetProcessName(pid_t pid) const {
|
||||
// Note: proc_name() accesses the `pbi_name` field of `struct proc_bsdinfo`. The size of `pname`
|
||||
// here is meant to match the size of `pbi_name`, and one extra byte ensure zero-terminated.
|
||||
char pname[MAXCOMLEN * 2 + 1] = {};
|
||||
if (proc_name(pid, pname, sizeof(pname)) > 0) {
|
||||
return std::string(pname);
|
||||
} else {
|
||||
return std::string("");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace santa::santad::event_providers::endpoint_security
|
||||
135
Source/santad/EventProviders/EndpointSecurity/MessageTest.mm
Normal file
@@ -0,0 +1,135 @@
|
||||
/// Copyright 2022 Google Inc. All rights reserved.
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <libproc.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
|
||||
bool IsPidInUse(pid_t pid) {
|
||||
char pname[MAXCOMLEN * 2 + 1] = {};
|
||||
errno = 0;
|
||||
if (proc_name(pid, pname, sizeof(pname)) <= 0 && errno == ESRCH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The PID may or may not actually be in use, but assume it is
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to find an unused PID by looking for libproc returning ESRCH errno.
|
||||
// Start searching backwards from PID_MAX to increase likelyhood that the
|
||||
// returned PID will still be unused by the time it's being used.
|
||||
// TODO(mlw): Alternatively, we could inject the `proc_name` function into
|
||||
// the `Message` object to remove the guesswork here.
|
||||
pid_t AttemptToFindUnusedPID() {
|
||||
for (pid_t pid = 99999 /* PID_MAX */; pid > 1; pid--) {
|
||||
if (!IsPidInUse(pid)) {
|
||||
return pid;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@interface MessageTest : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation MessageTest
|
||||
|
||||
- (void)setUp {
|
||||
}
|
||||
|
||||
- (void)testConstructorsAndDestructors {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
// Constructing a `Message` retains the underlying `es_message_t` and it is
|
||||
// released when the `Message` object is destructed.
|
||||
{ Message m(mockESApi, &esMsg); }
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testCopyConstructor {
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
EXPECT_CALL(*mockESApi, ReleaseMessage(testing::_))
|
||||
.Times(2)
|
||||
.After(EXPECT_CALL(*mockESApi, RetainMessage(testing::_))
|
||||
.Times(2)
|
||||
.WillRepeatedly(testing::Return(&esMsg)));
|
||||
|
||||
{
|
||||
Message msg1(mockESApi, &esMsg);
|
||||
Message msg2(msg1);
|
||||
|
||||
// Both messages should now point to the same `es_message_t`
|
||||
XCTAssertEqual(msg1.operator->(), &esMsg);
|
||||
XCTAssertEqual(msg2.operator->(), &esMsg);
|
||||
}
|
||||
|
||||
// Ensure the retain/release mocks were called the expected number of times
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testGetParentProcessName {
|
||||
// Construct a message where the parent pid is ourself
|
||||
es_file_t procFile = MakeESFile("foo");
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(getpid(), 0));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
|
||||
|
||||
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
// Search for an *existing* parent process.
|
||||
{
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
std::string got = msg.ParentProcessName();
|
||||
std::string want = getprogname();
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
|
||||
// Search for a *non-existent* parent process.
|
||||
{
|
||||
pid_t newPpid = AttemptToFindUnusedPID();
|
||||
proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(newPpid, 34));
|
||||
|
||||
Message msg(mockESApi, &esMsg);
|
||||
|
||||
std::string got = msg.ParentProcessName();
|
||||
std::string want = "";
|
||||
|
||||
XCTAssertCppStringEqual(got, want);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,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
@@ -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
@@ -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,9 +12,9 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include "Source/santad/EventProviders/SNTEndpointSecurityClientBase.h"
|
||||
|
||||
#include "Source/santad/EventProviders/SNTEndpointSecurityManager.h"
|
||||
|
||||
@interface SNTCachingEndpointSecurityManager : SNTEndpointSecurityManager
|
||||
/// This should be treated as an Abstract Base Class and not directly instantiated
|
||||
@interface SNTEndpointSecurityClient : NSObject <SNTEndpointSecurityClientBase>
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
@end
|
||||