Compare commits

...

37 Commits

Author SHA1 Message Date
Matt W
41c918ee87 Don't add messages when accumulated bytes exceeds threshold (#932)
* Don't add messages when accumulated bytes exceeds threshold

* Add a leniency factor

* lint
2022-11-07 12:24:49 -05:00
Matt W
1adb6d2726 Update spool to flush on size thresholds instead of batch counts (#930) 2022-11-03 14:55:51 -04:00
Matt W
8c531a256b metrics and logging cleanup (#928)
* Metrics and ambiguous log cleanup

* Fix test
2022-11-01 14:47:49 +00:00
Russell Hancox
5829363733 GUI: Fix EnableSilentMode key (#927) 2022-11-01 10:11:21 -04:00
Pete Markowsky
379f283c62 Update Known Limitations for USB Mass Storage Blocking (#924)
* Updated known limitations.
2022-10-28 20:21:38 -04:00
Matt W
2082345c02 Change order that ES clients are enabled (#923) 2022-10-29 00:15:26 +00:00
Matt W
dd8f81a60e Fix issue in test that would crash on some platforms (#922) 2022-10-28 20:14:53 -04:00
Matt W
8ccb0813f1 More import fixes (#921)
* More import fixes

* lint
2022-10-28 15:57:01 -04:00
Matt W
b24e7e42bf Event metrics (#918)
* WIP. Record event count and processing time metrics. Tests don't currently build.

* Updated tests

* Fix field names

* Remove unused target

* formatting

* Cleanup from PR comments
2022-10-28 14:25:07 -04:00
Pete Markowsky
4821ebebd5 Fix: duplicates bug in SNTMetricSet when using multiple fields (#920)
Fix duplicates bug in SNTMetricSet when using multiple fields names.

This also fixes the santactl metric command and golden files for tests.
2022-10-28 13:50:08 -04:00
Matt W
efeaa82618 Fix issue with transposed remount/banned block messages (#917) 2022-10-26 20:54:17 -04:00
videlanicolas
3f3de02644 USB: usbBlockMessage is not being used. (#915) 2022-10-26 17:42:49 -04:00
Matt W
f6c9456ea7 Fix some more includes (#914) 2022-10-25 16:52:19 -04:00
Matt W
2aaff051c8 Various changes to fix import (#913) 2022-10-25 16:16:44 -04:00
Matt W
2df7e91c87 Change include to import (#912) 2022-10-24 11:56:02 -04:00
Matt W
37644acd01 Update build docs. Fixes #910 (#911) 2022-10-24 09:55:37 -04:00
Matt W
899ca89e23 Proto minimization (#909)
* Create Light variants of File and ProcessInfo messages to reduce disk/wire byte counts

* Updated golden test data
2022-10-21 19:48:37 -04:00
Matt W
e7281f1c55 Spool writer (#908)
* Spool writer and santactl command to print proto file

* Make valid JSON for multiple paths. Can now create proto/spool logger. Updated logger tests.

* Make fsspool writer and fsspool log batch writer injectable

* Add spool writer tests

* Updated help text for santactl printlog

* Include file cleanup

* Fix dispatch source destruction

* Change config keys for the new Spool writer

* Spool settings now configurable

* Fix param order

* Remove some test sleeps related to control flow
2022-10-21 16:43:12 -04:00
Matt W
bf0ca24ae7 Machine id proto (#907)
* Add MachineID to all BasicString serialized log messages

* machine_id now a top level proto field

* Remove commented code
2022-10-19 10:51:38 -04:00
np5
4fe8b7908f sync: Fix USB blocking config sync (#890) 2022-10-18 10:01:20 -04:00
Matt W
a8dd332402 Update include paths and add include guard (#905) 2022-10-14 17:58:36 -04:00
Matt W
6631b0a8e3 More import fixes (#904)
* Layering check disable

* workaround for layering issue
2022-10-14 17:20:20 -04:00
Matt W
07e09db608 Import fixes (#902)
* Apply clang-format to cc files

* Modify binaryproto namespace

* Add more required includes

* Add proto includes

* Assert message parsing succeeds in test

* Add optional keyword to proto fields to track presence. TESTS BROKEN.

* Update golden test data
2022-10-14 15:51:53 -04:00
Matt W
d041a48c97 Fsspool adopt (#900)
* Added fsspool library, tests

* Cleanup

* Remove extra visibility from BUILD file

* Import foundation so the linter doesn't complain
2022-10-13 20:47:52 -04:00
Matt W
1683e09cc8 Proto serializer (#897)
* Initial proto serializer with close event

* Define move ctors for enriched types, delete copy ctors

* More event proto serialization. Commonized proto test code.

* Started work serializing exec event. Added serializer utilities.

* More progress serializing exec event

* Add mroe test data. Test restructure to permit fine grained mocking.

* Env/FD ES types now wrapped in EndpointSecurityAPI. Added calls to proto serializer.

* Add fd type names to proto

* Version compat. Script and Working Dir encoding.

* Add process start time

* Serialize Link event

* Add null check, mainly to fix tests

* Handle versioned expectations

* Each test now build msg in callbacks to set better expectations

* Serialize rename event and tests

* Serialize unlink event and tests

* Serialize allowlist and bundle events. Add utilities tests.

* Formatting

* Disk event proto serialization and tests

* Fix test only issues

* Rename santa_new.proto to santa.proto

* Change fd type int and string to an enum

* Proto namespace now versioned

* Added comments to proto schema

* Add proto support to indicate if fd list truncated
2022-10-13 13:52:41 -04:00
Ivan Tadeu Ferreira Antunes Filho
d6c73e0c6c common: Make SNTCommonEnums a textual header (#896)
This change fixes -wunused-variable warnings. The header is not valid by itself and should be declared as a textual header rather than as a header.
2022-10-03 13:15:33 -04:00
Matt W
72969a3c92 Fix crash flushing cache on unmount events (#895) 2022-09-27 21:54:35 -04:00
Matt W
d2dbed78dd Return a value from the test block (#894) 2022-09-27 15:07:20 -04:00
Matt W
8fa91e4ff0 Build deps (#893)
* Too bad we can't require explicit build deps...

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

* Add more BUILD deps

* Include cleanup

* Even more BUILD deps

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

* Basic working ES and logging functionality

* Add in oneTBB and thread-safe-lru deps

* Added a bunch of enriched types

* Auto-mute self when establishing ES client

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

* Update copyright header blobs, convert some tabs to spaces

* Auth result cache. Fix getting translocation path.

* Added remaining cache methods

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

* Hooked up SNTPrefixTree

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

* Block loading Santa kext

* Added device manager client

* Properly log DiskAppear events

* Fix build to adopt new adhoc build

* Handle clearing cache on UNMOUNT events

* Ignore other ES clients if configured

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

* Recorder now logs asynchronously. Enricher now returns shared_ptrs.

* Added File writer. Added timestamps to BasicStream serializer.

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

* Fix build issue

* Address draft PR feedback

* santactl integrated, XPC works, fix file writer bug

* Integrate syncservice. Start observing some config changes.

* Add metrics service wrapper

* Add metrics config observers and metrics interval reset.

* Start better dependency control. Add Null logger support.

* Added more deps

* Added more deps

* Fix issue where metric service wasn't starting

* Add missing variant include

* Fix missing parent proc name

* Added googletest and new unit test macro

* Started expanding AuthResultCacheTest

* Properly mock EndpointSecurityAPI

* Finished AuthResultCacheTest

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

* Add FileTest. Abstract some File constants to Logger.

* Added Empty serializer test

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

* Added Unlink BasicString serialization test

* Added some more tests. Commonized some test code

* Finished BasicStringTest. Converted to XCTest.

* Standardize esapi variable naming

* Bubble up gTest expect failures to XCTest failures

* AuthResultCacheTest now uses XCTest. Added common TestUtils.h

* EmptyTest now uses XCTest.

* FileTest now uses XCTest

* LoggerTest now uses XCTest. Removed santa_unit_gtest bazel macro.

* Added ClientTest

* Add basic Enricher tests

* Add MessageTest. Make more TestUtils.

* Rename metrics to Metrics

* Add MetricsTest.

* Apply template pattern to Serializer

* Add SNTDecisionCacheTest.

* Add SNTCachedDecisionTest.

* Testing with coveralls debug mode

* Allow manual CI runs

* Remove unused property

* Started work on SNTEndpointSecurityClientTest.

* WIP SNTEndpointSecurityClientTest, fix test run issue

* Added more base ES client tests

* Add more base ES client tests

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

* Add utils test to test suite

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

* Single thread bazel coverage

* Updaload coverage file

* Updaload coverage file

* Old gen cov test

* Restructure message handlers to enable better testability

* Added enable tests for all ES clients

* Made a single MockEndpointSecurityAPI class to share everywhere

* Added most of SNTCompilerControllerTest

* Cleanup SNTCompilerControllerTest

* Started expanding Auth client test

* Finished up the Authorizer tests

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

* WIP for tamper resistance test. ASAN issues.

* Add OCMock patch to fix test issue on ARM Macs

* Changed patches directory name to external_patches

* Update WORKSPACE path

* Finished up Tamper Resistance tests

* Finished up Recorder tests.

* Move SNTExecutionControllerTest to ObjC++

* Initial work to port SNTExecutionControllerTest

* Finished porting SNTExecutionControllerTest.

* Added SNTExecutionControllerTest to list of unit tests

* Ported SNTEndpointSecurityDeviceManager.

* Test cleanup, use MockESAPI expectation helpers

* Verify SNTEndpointSecurityDeviceManager expectations differently

* Test cleanup, omit gTest param list where unused

* Log message cleanup

* Rename SNTApplicationTest to santad_test.mm

* Finished porting santad_test, formerly SNTApplicationTest

* Fix SNTEndpointSecurityDeviceManager issues

* Pulled in missed fixes. Updated tests.

* Renamed lowercase filenames to match rest of codebase

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

* WIP Started process of removing components no longer used

* WIP Continued process of removing components no longer used

* BUILD file cleanup. Proto warning. Removed unused global

* Rename SNTEventProvider to SNTEndpointSecurityEventHandler

* Rename SNTEndpointSecurityEventHandler protocol

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

* Ran testing/fix.sh

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

* clang-format

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

* Use MakeESProcess default params in tests

* Move variables to camelCase in objc classes

* More case changes

* Sanitize strings

* Change dispatch queue priorities and standardize daemon queue naming

* Exclude patch files in markdown check

* Ensure string log messages end with newline

* Fix BasicStringTest

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

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

* Update Source/common/SNTConfigurator.h

Suggestion adding whitespace in comment to fix clang-format mangling

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

* Removed santa_panic macro used in one place

* Updated comment about ES cachability

* Pin oneTBB to specific commit

* Address outstanding WORKSPACE 'canonical reproducible form' messages

* Use string append instead of ostringstream due to benchmark results

* Remove use of freind classes in EnrichedTypes.h

* Added SNTKVOManager, removed observers from SNTConfigurator.

* Fixed SNTEndpointSecurityRecorderTest class name

* Reduce usage of the auto keyword

* Each SNTKVOManager instance now adds its own observer

* Replaced more auto keywords with real types.

* Remove leftover code coverage debugging from ci.yml

* Updated comment

* Memoize SNTFileInfo sha256. Reduce some cache sizes.

* Fix issue checking for translocated paths

* Use more performant NSURL creation method

* Fix lint issue

* Address PR feedback

* Use an array literal for kvo objects

* Fix some clang tidy and import issues

* Replace third party LRU cache with SantaCache for now

* Fix clang tidy issues

* Address PR feedback

* Fix comment typo

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

* Added todo for when we adopt macOS 13

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

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

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

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

View File

@@ -0,0 +1,18 @@
# Ignore reason: These crafted binaries are used in tests
ignorePaths:
- Source/common/testdata/bad_pagezero
- Source/common/testdata/missing_pagezero
- Source/common/testdata/missing_pagezero
- Source/common/testdata/missing_pagezero
- Source/common/testdata/32bitplist
- Source/common/testdata/BundleExample.app/Contents/MacOS/BundleExample
- Source/common/testdata/DirectoryBundle/Contents/MacOS/DirectoryBundle
- Source/common/testdata/DirectoryBundle/Contents/Resources/BundleExample.app/Contents/MacOS/BundleExample
- Source/santad/testdata/binaryrules/badbinary
- Source/santad/testdata/binaryrules/goodbinary
- Source/santad/testdata/binaryrules/badcert
- Source/santad/testdata/binaryrules/banned_teamid_allowed_binary
- Source/santad/testdata/binaryrules/banned_teamid
- Source/santad/testdata/binaryrules/goodcert
- Source/santad/testdata/binaryrules/noop
- Source/santad/testdata/binaryrules/rules.db

View File

@@ -3,3 +3,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"

View File

@@ -1 +1 @@
5.0.0
5.3.0

View File

@@ -11,4 +11,4 @@ jobs:
steps:
- uses: actions/checkout@master
- uses: gaurav-nelson/github-action-markdown-link-check@v1
- run: "! git grep -EIn $'[ \t]+$'"
- run: "! git grep -EIn $'[ \t]+$' -- ':(exclude)*.patch'"

View File

@@ -20,7 +20,6 @@ jobs:
- name: Run linters
run: ./Testing/lint.sh
build_userspace:
strategy:
fail-fast: false
@@ -55,10 +54,3 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ./bazel-out/_coverage/_coverage_report.dat
flag-name: Unit
benchmark:
runs-on: macos-11
steps:
- uses: actions/checkout@v2
- name: Run All Tests
run: ./Testing/benchmark.sh

7
BUILD
View File

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

View File

@@ -1,7 +1,7 @@
# Santa [![CI](https://github.com/google/santa/actions/workflows/ci.yml/badge.svg)](https://github.com/google/santa/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/google/santa/badge.svg?branch=main)](https://coveralls.io/github/google/santa?branch=main)
# Santa [![CI](https://github.com/google/santa/actions/workflows/ci.yml/badge.svg)](https://github.com/google/santa/actions/workflows/ci.yml)
<p align="center">
<img src="https://raw.githubusercontent.com/google/santa/main/Source/santa/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
<img src="https://raw.githubusercontent.com/google/santa/main/Source/gui/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
</p>
Santa is a binary authorization system for macOS. It consists of a system

View File

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

View File

@@ -1,32 +0,0 @@
/// Copyright 2021 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/common/SNTAllowlistInfo.h"
@implementation SNTAllowlistInfo
- (instancetype)initWithPid:(pid_t)pid
pidversion:(int)pidver
targetPath:(NSString *)targetPath
sha256:(NSString *)hash {
self = [super init];
if (self) {
_pid = pid;
_pidversion = pidver;
_targetPath = targetPath;
_sha256 = hash;
}
return self;
}
@end

View File

@@ -1,4 +1,4 @@
/// Copyright 2015 Google Inc. All rights reserved.
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -12,10 +12,11 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTCommon.h"
#import "Source/common/SNTCommonEnums.h"
@class MOLCertificate;
@@ -24,6 +25,8 @@
///
@interface SNTCachedDecision : NSObject
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile;
@property santa_vnode_id_t vnodeId;
@property SNTEventState decision;
@property NSString *decisionExtra;

View File

@@ -1,4 +1,4 @@
/// Copyright 2015 Google Inc. All rights reserved.
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -15,4 +15,14 @@
#import "Source/common/SNTCachedDecision.h"
@implementation SNTCachedDecision
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile {
self = [super init];
if (self) {
_vnodeId.fsid = (uint64_t)esFile->stat.st_dev;
_vnodeId.fileid = esFile->stat.st_ino;
}
return self;
}
@end

View File

@@ -0,0 +1,36 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <XCTest/XCTest.h>
#import "Source/common/SNTCachedDecision.h"
#include "Source/common/TestUtils.h"
@interface SNTCachedDecisionTest : XCTestCase
@end
@implementation SNTCachedDecisionTest
- (void)testSNTCachedDecisionInit {
// Ensure the vnodeId field is properly set from the es_file_t
struct stat sb = MakeStat();
es_file_t file = MakeESFile("foo", sb);
SNTCachedDecision *cd = [[SNTCachedDecision alloc] initWithEndpointSecurityFile:&file];
XCTAssertEqual(sb.st_ino, cd.vnodeId.fileid);
XCTAssertEqual(sb.st_dev, cd.vnodeId.fsid);
}
@end

View File

@@ -1,4 +1,4 @@
/// Copyright 2015 Google Inc. All rights reserved.
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -27,41 +27,22 @@
#define unlikely(x) __builtin_expect(!!(x), 0)
typedef enum {
ACTION_UNSET = 0,
ACTION_UNSET,
// REQUESTS
ACTION_REQUEST_SHUTDOWN = 10,
ACTION_REQUEST_BINARY = 11,
// If an operation is awaiting a cache decision from a similar operation
// currently being processed, it will poll about every 5 ms for an answer.
ACTION_REQUEST_BINARY,
// RESPONSES
ACTION_RESPOND_ALLOW = 20,
ACTION_RESPOND_DENY = 21,
ACTION_RESPOND_TOOLONG = 22,
ACTION_RESPOND_ACK = 23,
ACTION_RESPOND_ALLOW_COMPILER = 24,
// The following response is stored only in the kernel decision cache.
// It is removed by SNTCompilerController
ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE = 25,
// NOTIFY
ACTION_NOTIFY_EXEC = 30,
ACTION_NOTIFY_WRITE = 31,
ACTION_NOTIFY_RENAME = 32,
ACTION_NOTIFY_LINK = 33,
ACTION_NOTIFY_EXCHANGE = 34,
ACTION_NOTIFY_DELETE = 35,
ACTION_NOTIFY_WHITELIST = 36,
ACTION_NOTIFY_FORK = 37,
ACTION_NOTIFY_EXIT = 38,
// ERROR
ACTION_ERROR = 99,
ACTION_RESPOND_ALLOW,
ACTION_RESPOND_DENY,
ACTION_RESPOND_ALLOW_COMPILER,
} santa_action_t;
#define RESPONSE_VALID(x) \
(x == ACTION_RESPOND_ALLOW || x == ACTION_RESPOND_DENY || \
x == ACTION_RESPOND_ALLOW_COMPILER || \
x == ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE)
x == ACTION_RESPOND_ALLOW_COMPILER)
// Struct to manage vnode IDs
typedef struct santa_vnode_id_t {
@@ -75,28 +56,4 @@ typedef struct santa_vnode_id_t {
#endif
} santa_vnode_id_t;
typedef struct {
santa_action_t action;
santa_vnode_id_t vnode_id;
uid_t uid;
gid_t gid;
pid_t pid;
int pidversion;
pid_t ppid;
char path[MAXPATHLEN];
char newpath[MAXPATHLEN];
char ttypath[MAXPATHLEN];
// For file events, this is the process name.
// For exec requests, this is the parent process name.
// While process names can technically be 4*MAXPATHLEN, that never
// actually happens, so only take MAXPATHLEN and throw away any excess.
char pname[MAXPATHLEN];
// This points to a copy of the original ES message.
void *es_message;
// This points to an NSArray of the process arguments.
void *args_array;
} santa_message_t;
#endif // SANTA__COMMON__COMMON_H

View File

@@ -1,4 +1,4 @@
/// Copyright 2015 Google Inc. All rights reserved.
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -57,6 +57,7 @@ typedef NS_ENUM(NSInteger, SNTEventState) {
SNTEventStateBlockCertificate = 1 << 18,
SNTEventStateBlockScope = 1 << 19,
SNTEventStateBlockTeamID = 1 << 20,
SNTEventStateBlockLongPath = 1 << 21,
// Bits 24-31 store allow decision types
SNTEventStateAllowUnknown = 1 << 24,
@@ -120,5 +121,4 @@ typedef NS_ENUM(NSInteger, SNTMetricFormatType) {
static const char *kSantaDPath =
"/Applications/Santa.app/Contents/Library/SystemExtensions/"
"com.google.santa.daemon.systemextension/Contents/MacOS/com.google.santa.daemon";
static const char *kSantaCtlPath = "/Applications/Santa.app/Contents/MacOS/santactl";
static const char *kSantaAppPath = "/Applications/Santa.app";

View File

@@ -1,4 +1,4 @@
/// Copyright 2015 Google Inc. All rights reserved.
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -65,7 +65,8 @@
/// <key>rule_type</key>
/// <string>BINARY</string> (one of BINARY, CERTIFICATE or TEAMID)
/// <key>policy</key>
/// <string>BLOCKLIST</string> (one of ALLOWLIST, ALLOWLIST_COMPILER, BLOCKLIST, SILENT_BLOCKLIST)
/// <string>BLOCKLIST</string> (one of ALLOWLIST, ALLOWLIST_COMPILER, BLOCKLIST,
/// SILENT_BLOCKLIST)
/// </dict>
/// </array>
///
@@ -182,10 +183,10 @@
/// SNTEventLogTypeSyslog "syslog": Sent to ASL or ULS (if built with the 10.12 SDK or later).
/// SNTEventLogTypeFilelog "file": Sent to a file on disk. Use eventLogPath to specify a path.
/// SNTEventLogTypeNull "null": Logs nothing
/// SNTEventLogTypeProtobuf "protobuf": (BETA) Sent to a file on disk, using maildir format. Use
/// mailDirectory to specify a path. Use mailDirectoryFileSizeThresholdKB,
/// mailDirectorySizeThresholdMB and mailDirectoryEventMaxFlushTimeSec to configure
/// additional maildir format settings.
/// SNTEventLogTypeProtobuf "protobuf": (BETA) Sent to a file on disk, using a maildir-like
/// format. Use spoolDirectory to specify a path. Use spoolDirectoryFileSizeThresholdKB,
/// spoolDirectorySizeThresholdMB and spoolDirectoryEventMaxFlushTimeSec to configure
/// additional settings.
/// Defaults to SNTEventLogTypeFilelog.
/// For mobileconfigs use EventLogType as the key and syslog or filelog strings as the value.
///
@@ -202,40 +203,40 @@
@property(readonly, nonatomic) NSString *eventLogPath;
///
/// If eventLogType is set to protobuf, mailDirectory will provide the base path used for
/// saving logs using the maildir format.
/// Defaults to /var/db/santa/mail.
/// If eventLogType is set to protobuf, spoolDirectory will provide the base path used for
/// saving logs using a maildir-like format.
/// Defaults to /var/db/santa/spool.
///
/// @note: This property is KVO compliant, but should only be read once at santad startup.
///
@property(readonly, nonatomic) NSString *mailDirectory;
@property(readonly, nonatomic) NSString *spoolDirectory;
///
/// If eventLogType is set to protobuf, mailDirectoryFileSizeThresholdKB sets the per-file size
/// limit for files saved in the mailDirectory.
/// If eventLogType is set to protobuf, spoolDirectoryFileSizeThresholdKB sets the per-file size
/// limit for files saved in the spoolDirectory.
/// Defaults to 250.
///
/// @note: This property is KVO compliant, but should only be read once at santad startup.
///
@property(readonly, nonatomic) NSUInteger spoolDirectoryFileSizeThresholdKB;
///
/// If eventLogType is set to protobuf, spoolDirectorySizeThresholdMB sets the total size
/// limit for all files saved in the spoolDirectory.
/// Defaults to 100.
///
/// @note: This property is KVO compliant, but should only be read once at santad startup.
///
@property(readonly, nonatomic) NSUInteger mailDirectoryFileSizeThresholdKB;
@property(readonly, nonatomic) NSUInteger spoolDirectorySizeThresholdMB;
///
/// If eventLogType is set to protobuf, mailDirectorySizeThresholdMB sets the total size
/// limit for all files saved in the mailDirectory.
/// Defaults to 500.
///
/// @note: This property is KVO compliant, but should only be read once at santad startup.
///
@property(readonly, nonatomic) NSUInteger mailDirectorySizeThresholdMB;
///
/// If eventLogType is set to protobuf, mailDirectoryEventMaxFlushTimeSec sets the maximum amount
/// If eventLogType is set to protobuf, spoolDirectoryEventMaxFlushTimeSec sets the maximum amount
/// of time an event will be stored in memory before being written to disk.
/// Defaults to 5.0.
/// Defaults to 15.0.
///
/// @note: This property is KVO compliant, but should only be read once at santad startup.
///
@property(readonly, nonatomic) float mailDirectoryEventMaxFlushTimeSec;
@property(readonly, nonatomic) float spoolDirectoryEventMaxFlushTimeSec;
///
/// Enabling this appends the Santa machine ID to the end of each log line. If nothing
@@ -244,15 +245,6 @@
///
@property(readonly, nonatomic) BOOL enableMachineIDDecoration;
///
/// Use an internal cache for decisions instead of relying on the caching
/// mechanism built-in to the EndpointSecurity framework. This may increase
/// performance, particularly when Santa is run alongside other system
/// extensions.
/// Has no effect if the system extension is not being used. Defaults to NO.
///
@property(readonly, nonatomic) BOOL enableSysxCache;
#pragma mark - GUI Settings
///
@@ -387,12 +379,6 @@
///
@property(nonatomic) NSArray<NSString *> *remountUSBMode;
///
/// When `blockUSBMount` is set, this is the message shown to the user when a device is blocked
/// If this message is not configured, a reasonable default is provided.
///
@property(readonly, nonatomic) NSString *usbBlockMessage;
///
/// If set, this over-rides the default machine ID used for syncing.
///

View File

@@ -1,4 +1,4 @@
/// Copyright 2021 Google Inc. All rights reserved.
/// Copyright 2014-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@
/// Holds the last processed hash of the static rules list.
@property(atomic) NSDictionary *cachedStaticRules;
@end
@implementation SNTConfigurator
@@ -87,15 +88,13 @@ static NSString *const kFileChangesPrefixFiltersKey = @"FileChangesPrefixFilters
static NSString *const kEventLogType = @"EventLogType";
static NSString *const kEventLogPath = @"EventLogPath";
static NSString *const kMailDirectory = @"MailDirectory";
static NSString *const kMailDirectoryFileSizeThresholdKB = @"MailDirectoryFileSizeThresholdKB";
static NSString *const kMailDirectorySizeThresholdMB = @"MailDirectorySizeThresholdMB";
static NSString *const kMailDirectoryEventMaxFlushTimeSec = @"MailDirectoryEventMaxFlushTimeSec";
static NSString *const kSpoolDirectory = @"SpoolDirectory";
static NSString *const kSpoolDirectoryFileSizeThresholdKB = @"SpoolDirectoryFileSizeThresholdKB";
static NSString *const kSpoolDirectorySizeThresholdMB = @"SpoolDirectorySizeThresholdMB";
static NSString *const kSpoolDirectoryEventMaxFlushTimeSec = @"SpoolDirectoryEventMaxFlushTimeSec";
static NSString *const kEnableMachineIDDecoration = @"EnableMachineIDDecoration";
static NSString *const kEnableSysxCache = @"EnableSysxCache";
static NSString *const kEnableForkAndExitLogging = @"EnableForkAndExitLogging";
static NSString *const kIgnoreOtherEndpointSecurityClients = @"IgnoreOtherEndpointSecurityClients";
static NSString *const kEnableDebugLogging = @"EnableDebugLogging";
@@ -173,7 +172,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kRemountUSBModeKey : array,
kEnablePageZeroProtectionKey : number,
kEnableBadSignatureProtectionKey : number,
kEnableSilentModeKey : string,
kEnableSilentModeKey : number,
kAboutTextKey : string,
kMoreInfoURLKey : string,
kEventDetailURLKey : string,
@@ -201,12 +200,11 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kMachineIDPlistKeyKey : string,
kEventLogType : string,
kEventLogPath : string,
kMailDirectory : string,
kMailDirectoryFileSizeThresholdKB : number,
kMailDirectorySizeThresholdMB : number,
kMailDirectoryEventMaxFlushTimeSec : number,
kSpoolDirectory : string,
kSpoolDirectoryFileSizeThresholdKB : number,
kSpoolDirectorySizeThresholdMB : number,
kSpoolDirectoryEventMaxFlushTimeSec : number,
kEnableMachineIDDecoration : number,
kEnableSysxCache : number,
kEnableForkAndExitLogging : number,
kIgnoreOtherEndpointSecurityClients : number,
kEnableDebugLogging : number,
@@ -393,19 +391,19 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingMailDirectory {
+ (NSSet *)keyPathsForValuesAffectingSpoolDirectory {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingMailDirectoryFileSizeThresholdKB {
+ (NSSet *)keyPathsForValuesAffectingSpoolDirectoryFileSizeThresholdKB {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingMailDirectorySizeThresholdMB {
+ (NSSet *)keyPathsForValuesAffectingSpoolDirectorySizeThresholdMB {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingMailDirectoryEventMaxFlushTimeSec {
+ (NSSet *)keyPathsForValuesAffectingSpoolDirectoryEventMaxFlushTimeSec {
return [self configStateSet];
}
@@ -425,10 +423,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return [self syncAndConfigStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingEnableSysxCache {
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingEnableForkAndExitLogging {
return [self configStateSet];
}
@@ -474,15 +468,15 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
}
+ (NSSet *)keyPathsForValuesAffectingRemountUSBMode {
return [self configStateSet];
return [self syncAndConfigStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingRemountUSBBlockMessage {
return [self syncAndConfigStateSet];
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingUsbBlockMessage {
return [self syncAndConfigStateSet];
return [self configStateSet];
}
#pragma mark Public Interface
@@ -583,7 +577,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
}
- (NSArray<NSString *> *)remountUSBMode {
NSArray<NSString *> *args = self.configState[kRemountUSBModeKey];
NSArray<NSString *> *args = self.syncState[kRemountUSBModeKey];
if (!args) {
args = (NSArray<NSString *> *)self.configState[kRemountUSBModeKey];
}
for (id arg in args) {
if (![arg isKindOfClass:[NSString class]]) {
return nil;
@@ -766,26 +763,26 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return self.configState[kEventLogPath] ?: @"/var/db/santa/santa.log";
}
- (NSString *)mailDirectory {
return self.configState[kMailDirectory] ?: @"/var/db/santa/mail";
- (NSString *)spoolDirectory {
return self.configState[kSpoolDirectory] ?: @"/var/db/santa/spool";
}
- (NSUInteger)mailDirectoryFileSizeThresholdKB {
return self.configState[kMailDirectoryFileSizeThresholdKB]
? [self.configState[kMailDirectoryFileSizeThresholdKB] unsignedIntegerValue]
- (NSUInteger)spoolDirectoryFileSizeThresholdKB {
return self.configState[kSpoolDirectoryFileSizeThresholdKB]
? [self.configState[kSpoolDirectoryFileSizeThresholdKB] unsignedIntegerValue]
: 250;
}
- (NSUInteger)spoolDirectorySizeThresholdMB {
return self.configState[kSpoolDirectorySizeThresholdMB]
? [self.configState[kSpoolDirectorySizeThresholdMB] unsignedIntegerValue]
: 100;
}
- (NSUInteger)mailDirectorySizeThresholdMB {
return self.configState[kMailDirectorySizeThresholdMB]
? [self.configState[kMailDirectorySizeThresholdMB] unsignedIntegerValue]
: 500;
}
- (float)mailDirMaxFlushTime {
return self.configState[kMailDirectoryEventMaxFlushTimeSec]
? [self.configState[kMailDirectoryEventMaxFlushTimeSec] floatValue]
: 5.0;
- (float)spoolDirectoryEventMaxFlushTimeSec {
return self.configState[kSpoolDirectoryEventMaxFlushTimeSec]
? [self.configState[kSpoolDirectoryEventMaxFlushTimeSec] floatValue]
: 15.0;
}
- (BOOL)enableMachineIDDecoration {
@@ -793,11 +790,6 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return number ? [number boolValue] : NO;
}
- (BOOL)enableSysxCache {
NSNumber *number = self.configState[kEnableSysxCache];
return number ? [number boolValue] : YES;
}
- (BOOL)enableCleanSyncEventUpload {
NSNumber *number = self.configState[kSyncEnableCleanSyncEventUpload];
return number ? [number boolValue] : NO;
@@ -866,8 +858,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
}
- (BOOL)blockUSBMount {
NSNumber *number = self.configState[kBlockUSBMountKey];
return number ? [number boolValue] : NO;
NSNumber *n = self.syncState[kBlockUSBMountKey];
if (n) return [n boolValue];
return [self.configState[kBlockUSBMountKey] boolValue];
}
///

View File

@@ -1,4 +1,4 @@
/// Copyright 2015 Google Inc. All rights reserved.
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -12,6 +12,7 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
@class MOLCodesignChecker;
@@ -32,6 +33,14 @@
///
- (instancetype)initWithPath:(NSString *)path error:(NSError **)error;
///
/// Convenience initializer.
///
/// @param esFile Pointer to an es_file_t provided by the EndpointSecurity framework.
/// Assumes that the path is a resolved path.
///
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile error:(NSError **)error;
///
/// Convenience initializer.
///

View File

@@ -1,4 +1,4 @@
/// Copyright 2015 Google Inc. All rights reserved.
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -25,6 +25,8 @@
#include <sys/stat.h>
#include <sys/xattr.h>
#import "Source/common/SNTLogging.h"
// Simple class to hold the data of a mach_header and the offset within the file
// in which that header was found.
@interface MachHeaderWithOffset : NSObject
@@ -48,6 +50,7 @@
@property NSFileHandle *fileHandle;
@property NSUInteger fileSize;
@property NSString *fileOwnerHomeDir;
@property NSString *sha256Storage;
// Cached properties
@property NSBundle *bundleRef;
@@ -63,6 +66,26 @@
extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
- (instancetype)initWithResolvedPath:(NSString *)path error:(NSError **)error {
struct stat fileStat;
if (path.length) {
lstat(path.UTF8String, &fileStat);
}
return [self initWithResolvedPath:path stat:&fileStat error:error];
}
- (instancetype)initWithEndpointSecurityFile:(const es_file_t *)esFile error:(NSError **)error {
return [self initWithResolvedPath:@(esFile->path.data) stat:&esFile->stat error:error];
}
- (instancetype)initWithResolvedPath:(NSString *)path
stat:(const struct stat *)fileStat
error:(NSError **)error {
if (!fileStat) {
// This is a programming error. Bail.
LOGE(@"NULL stat buffer unsupported");
exit(EXIT_FAILURE);
}
self = [super init];
if (self) {
_path = path;
@@ -76,9 +99,7 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
return nil;
}
struct stat fileStat;
lstat(_path.UTF8String, &fileStat);
if (!((S_IFMT & fileStat.st_mode) == S_IFREG)) {
if (!((S_IFMT & fileStat->st_mode) == S_IFREG)) {
if (error) {
NSString *errStr = [NSString stringWithFormat:@"Non regular file: %s", strerror(errno)];
*error = [NSError errorWithDomain:@"com.google.santa.fileinfo"
@@ -88,12 +109,12 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
return nil;
}
_fileSize = fileStat.st_size;
_fileSize = fileStat->st_size;
if (_fileSize == 0) return nil;
if (fileStat.st_uid != 0) {
struct passwd *pwd = getpwuid(fileStat.st_uid);
if (fileStat->st_uid != 0) {
struct passwd *pwd = getpwuid(fileStat->st_uid);
if (pwd) {
_fileOwnerHomeDir = @(pwd->pw_dir);
}
@@ -214,9 +235,13 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
}
- (NSString *)SHA256 {
NSString *sha256;
[self hashSHA1:NULL SHA256:&sha256];
return sha256;
// Memoize the value
if (!self.sha256Storage) {
NSString *sha256;
[self hashSHA1:NULL SHA256:&sha256];
self.sha256Storage = sha256;
}
return self.sha256Storage;
}
#pragma mark File Type Info

View File

@@ -0,0 +1,34 @@
/// Copyright 2022 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <Foundation/Foundation.h>
// The callback type when KVO notifications are received for observed key paths.
// The first parameter is the previous value, the second paramter is the new value.
typedef void (^KVOCallback)(id oldValue, id newValue);
@interface SNTKVOManager : NSObject
// Add an observer for the selector on the given object. When a KVO notification
// is received, the callback is called. If the notification contains objects that
// are not of the expectedType, nil is passed as the argument to the callback.
// The observer is removed when the returned instance is deallocated.
- (instancetype)initWithObject:(id)object
selector:(SEL)selector
type:(Class)expectedType
callback:(KVOCallback)callback;
- (instancetype)init NS_UNAVAILABLE;
@end

View File

@@ -0,0 +1,72 @@
/// Copyright 2022 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/common/SNTKVOManager.h"
#import "Source/common/SNTLogging.h"
@interface SNTKVOManager ()
@property KVOCallback callback;
@property Class expectedType;
@property NSString *keyPath;
@property id object;
@end
@implementation SNTKVOManager
- (instancetype)initWithObject:(id)object
selector:(SEL)selector
type:(Class)expectedType
callback:(KVOCallback)callback {
self = [super self];
if (self) {
NSString *selectorName = NSStringFromSelector(selector);
if (![object respondsToSelector:selector]) {
LOGE(@"Attempt to add observer for an unknown selector (%@) for object (%@)", selectorName,
[object class]);
return nil;
}
_object = object;
_keyPath = selectorName;
_expectedType = expectedType;
_callback = callback;
[object addObserver:self
forKeyPath:selectorName
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:NULL];
}
return self;
}
- (void)dealloc {
[self.object removeObserver:self forKeyPath:self.keyPath context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *, id> *)change
context:(void *)context {
id oldValue = [change[NSKeyValueChangeOldKey] isKindOfClass:self.expectedType]
? change[NSKeyValueChangeOldKey]
: nil;
id newValue = [change[NSKeyValueChangeNewKey] isKindOfClass:self.expectedType]
? change[NSKeyValueChangeNewKey]
: nil;
self.callback(oldValue, newValue);
}
@end

View File

@@ -0,0 +1,129 @@
/// Copyright 2022 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <XCTest/XCTest.h>
#import "Source/common/SNTKVOManager.h"
@interface Foo : NSObject
@property NSNumber *propNumber;
@property NSArray *propArray;
@property id propId;
@end
@implementation Foo
@end
@interface SNTKVOManagerTest : XCTestCase
@end
@implementation SNTKVOManagerTest
- (void)testInvalidSelector {
Foo *foo = [[Foo alloc] init];
SNTKVOManager *kvo = [[SNTKVOManager alloc] initWithObject:foo
selector:NSSelectorFromString(@"doesNotExist")
type:[NSNumber class]
callback:^(id, id){
}];
XCTAssertNil(kvo);
}
- (void)testNormalOperation {
Foo *foo = [[Foo alloc] init];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
int origVal = 123;
int update1 = 456;
int update2 = 789;
foo.propNumber = @(origVal);
// Store the values from the callback to test against expected values
__block int oldVal;
__block int newVal;
SNTKVOManager *kvo =
[[SNTKVOManager alloc] initWithObject:foo
selector:@selector(propNumber)
type:[NSNumber class]
callback:^(NSNumber *oldValue, NSNumber *newValue) {
oldVal = [oldValue intValue];
newVal = [newValue intValue];
dispatch_semaphore_signal(sema);
}];
XCTAssertNotNil(kvo);
// Ensure an update to the observed property triggers the callback
foo.propNumber = @(update1);
XCTAssertEqual(0,
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
"Failed waiting for first observable update");
XCTAssertEqual(oldVal, origVal);
XCTAssertEqual(newVal, update1);
// One more time why not
foo.propNumber = @(update2);
XCTAssertEqual(0,
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
"Failed waiting for second observable update");
XCTAssertEqual(oldVal, update1);
XCTAssertEqual(newVal, update2);
}
- (void)testUnexpectedTypes {
Foo *foo = [[Foo alloc] init];
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSString *origVal = @"any_val";
NSString *update = @"new_val";
foo.propId = origVal;
__block id oldVal;
__block id newVal;
SNTKVOManager *kvo = [[SNTKVOManager alloc] initWithObject:foo
selector:@selector(propId)
type:[NSString class]
callback:^(id oldValue, id newValue) {
oldVal = oldValue;
newVal = newValue;
dispatch_semaphore_signal(sema);
}];
XCTAssertNotNil(kvo);
// Update to an unexpected type (here, NSNumber instead of NSString)
foo.propId = @(123);
XCTAssertEqual(0,
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
"Failed waiting for first observable update");
XCTAssertEqualObjects(oldVal, origVal);
XCTAssertNil(newVal);
// Update again with an expected type, ensure oldVal is now nil
foo.propId = update;
XCTAssertEqual(0,
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
"Failed waiting for first observable update");
XCTAssertNil(oldVal);
XCTAssertEqualObjects(newVal, update);
}
@end

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
/// Copyright 2015 Google Inc. All rights reserved.
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -51,18 +51,18 @@
/// Designated initializer.
///
- (instancetype)initWithIdentifier:(NSString *)identifier
state:(SNTRuleState)state
type:(SNTRuleType)type
customMsg:(NSString *)customMsg
timestamp:(NSUInteger)timestamp;
state:(SNTRuleState)state
type:(SNTRuleType)type
customMsg:(NSString *)customMsg
timestamp:(NSUInteger)timestamp;
///
/// Initialize with a default timestamp: current time if rule state is transitive, 0 otherwise.
///
- (instancetype)initWithIdentifier:(NSString *)identifier
state:(SNTRuleState)state
type:(SNTRuleType)type
customMsg:(NSString *)customMsg;
state:(SNTRuleState)state
type:(SNTRuleType)type
customMsg:(NSString *)customMsg;
///
/// Initialize with a dictionary received from a sync server.

View File

@@ -1,4 +1,4 @@
/// Copyright 2015 Google Inc. All rights reserved.
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -95,7 +95,6 @@
///
@property NSArray *signingChain;
///
/// If the executed file was signed, this is the Team ID if present in the signature information.
///

View File

@@ -1,4 +1,4 @@
/// Copyright 2016 Google Inc. All rights reserved.
/// Copyright 2016-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -12,10 +12,14 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#define STRONGIFY(var) \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
__strong __typeof(var) var = (Weak_##var); \
// clang-format off
#define STRONGIFY(var) \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
__strong __typeof(var) var = (Weak_##var); \
_Pragma("clang diagnostic pop")
#define WEAKIFY(var) __weak __typeof(var) Weak_##var = (var);
// clang-format on

View File

@@ -1,4 +1,4 @@
/// Copyright 2015 Google Inc. All rights reserved.
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -18,8 +18,11 @@
@implementation SNTSystemInfo
+ (NSString *)serialNumber {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
io_service_t platformExpert =
IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"));
#pragma clang diagnostic pop
if (!platformExpert) return nil;
NSString *serial = CFBridgingRelease(IORegistryEntryCreateCFProperty(
@@ -31,8 +34,11 @@
}
+ (NSString *)hardwareUUID {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
io_service_t platformExpert =
IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"));
#pragma clang diagnostic pop
if (!platformExpert) return nil;
NSString *uuid = CFBridgingRelease(IORegistryEntryCreateCFProperty(

View File

@@ -1,4 +1,4 @@
/// Copyright 2015 Google Inc. All rights reserved.
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -15,8 +15,8 @@
#import <Foundation/Foundation.h>
#import <MOLCertificate/MOLCertificate.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTCommon.h"
#import "Source/common/SNTCommonEnums.h"
@class SNTRule;
@class SNTStoredEvent;
@@ -31,7 +31,6 @@
/// Cache Ops
///
- (void)cacheCounts:(void (^)(uint64_t rootCache, uint64_t nonRootCache))reply;
- (void)cacheBucketCount:(void (^)(NSArray *))reply;
- (void)checkCacheForVnodeID:(santa_vnode_id_t)vnodeID withReply:(void (^)(santa_action_t))reply;
///

View File

@@ -1,4 +1,4 @@
/// Copyright 2016 Google Inc. All rights reserved.
/// Copyright 2016-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
#include <libkern/OSAtomic.h>
#include <libkern/OSTypes.h>
#include <os/log.h>
#include <stdint.h>
#include <sys/cdefs.h>
@@ -26,11 +27,6 @@
#include "Source/common/SNTCommon.h"
#define panic(args...) \
printf(args); \
printf("\n"); \
abort()
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
@@ -334,7 +330,9 @@ class SantaCache {
inline void unlock(struct bucket *bucket) const {
if (unlikely(OSAtomicTestAndClear(7, (volatile uint8_t *)&bucket->head) ==
0)) {
panic("SantaCache::unlock(): Tried to unlock an unlocked lock");
os_log_error(OS_LOG_DEFAULT,
"SantaCache::unlock(): Tried to unlock an unlocked lock");
abort();
}
}

75
Source/common/TestUtils.h Normal file
View File

@@ -0,0 +1,75 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__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
View File

@@ -0,0 +1,145 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#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);
}
}

View File

@@ -1,6 +1,7 @@
//
// !!! WARNING !!!
// This proto is in beta format and subject to change.
// This proto is for demonstration purposes only and will be changing.
// Do not rely on this format.
//
syntax = "proto3";
@@ -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 {

View 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

View File

@@ -12,8 +12,6 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
// #import <MOLCertificate/MOLCertificate.h>
// #import <MOLCodesignChecker/MOLCodesignChecker.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>

View File

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

View File

@@ -1,79 +0,0 @@
/// Copyright 2018 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifdef DEBUG
#import <Foundation/Foundation.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santactl/SNTCommand.h"
#import "Source/santactl/SNTCommandController.h"
@interface SNTCommandCacheHistogram : SNTCommand <SNTCommandProtocol>
@end
@implementation SNTCommandCacheHistogram
REGISTER_COMMAND_NAME(@"cachehistogram")
+ (BOOL)requiresRoot {
return YES;
}
+ (BOOL)requiresDaemonConn {
return YES;
}
+ (NSString *)shortHelpText {
return @"Print a cache distribution histogram.";
}
+ (NSString *)longHelpText {
return (@"Prints a histogram of each bucket of the in-kernel cache\n"
@" Use -g to get 'graphical' output\n"
@"Only available in DEBUG builds.");
}
- (void)runWithArguments:(NSArray *)arguments {
[[self.daemonConn remoteObjectProxy] cacheBucketCount:^(NSArray *counts) {
NSMutableDictionary<NSNumber *, NSNumber *> *d = [NSMutableDictionary dictionary];
[counts enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
d[obj] = @([d[obj] intValue] + 1);
}];
printf("There are %llu empty buckets\n", [d[@0] unsignedLongLongValue]);
for (NSNumber *key in [d.allKeys sortedArrayUsingSelector:@selector(compare:)]) {
if ([key isEqual:@0]) continue;
uint64_t k = [key unsignedLongLongValue];
uint64_t v = [d[key] unsignedLongLongValue];
if ([[[NSProcessInfo processInfo] arguments] containsObject:@"-g"]) {
printf("%4llu: ", k);
for (uint64_t y = 0; y < v; ++y) {
printf("#");
}
printf("\n");
} else {
printf("%4llu bucket[s] have %llu %s\n", v, k, k > 1 ? "entries" : "entry");
}
}
exit(0);
}];
}
@end
#endif

View File

@@ -1,4 +1,4 @@
/// Copyright 2016 Google Inc. All rights reserved.
/// Copyright 2016-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -62,9 +62,6 @@ REGISTER_COMMAND_NAME(@"checkcache")
} else if (action == ACTION_RESPOND_ALLOW_COMPILER) {
LOGI(@"File exists in [allowlist compiler] kernel cache");
exit(0);
} else if (action == ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE) {
LOGI(@"File exists in [allowlist pending_transitive] kernel cache");
exit(0);
} else if (action == ACTION_UNSET) {
LOGE(@"File does not exist in cache");
exit(1);

View File

@@ -1,4 +1,4 @@
/// Copyright 2015 Google Inc. All rights reserved.
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -385,6 +385,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
case SNTEventStateBlockScope: [output appendString:@" (Scope)"]; break;
case SNTEventStateAllowCompiler: [output appendString:@" (Compiler)"]; break;
case SNTEventStateAllowTransitive: [output appendString:@" (Transitive)"]; break;
case SNTEventStateBlockLongPath: [output appendString:@" (Long Path)"]; break;
default: output = @"None".mutableCopy; break;
}

View File

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

View 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

View File

@@ -1,4 +1,4 @@
/// Copyright 2015 Google Inc. All rights reserved.
/// Copyright 2015-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -74,18 +74,14 @@ REGISTER_COMMAND_NAME(@"status")
SNTConfigurator *configurator = [SNTConfigurator configurator];
BOOL cachingEnabled = [configurator enableSysxCache];
// Cache status
__block uint64_t rootCacheCount = -1, nonRootCacheCount = -1;
if (cachingEnabled) {
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] cacheCounts:^(uint64_t rootCache, uint64_t nonRootCache) {
rootCacheCount = rootCache;
nonRootCacheCount = nonRootCache;
dispatch_group_leave(group);
}];
}
dispatch_group_enter(group);
[[self.daemonConn remoteObjectProxy] cacheCounts:^(uint64_t rootCache, uint64_t nonRootCache) {
rootCacheCount = rootCache;
nonRootCacheCount = nonRootCache;
dispatch_group_leave(group);
}];
// Database counts
__block int64_t eventCount = -1, binaryRuleCount = -1, certRuleCount = -1, teamIDRuleCount = -1;
@@ -215,12 +211,12 @@ REGISTER_COMMAND_NAME(@"status")
@"transitive_rules" : @(enableTransitiveRules),
},
} mutableCopy];
if (cachingEnabled) {
stats[@"cache"] = @{
@"root_cache_count" : @(rootCacheCount),
@"non_root_cache_count" : @(nonRootCacheCount),
};
}
stats[@"cache"] = @{
@"root_cache_count" : @(rootCacheCount),
@"non_root_cache_count" : @(nonRootCacheCount),
};
NSData *statsData = [NSJSONSerialization dataWithJSONObject:stats
options:NSJSONWritingPrettyPrinted
error:nil];
@@ -238,11 +234,9 @@ REGISTER_COMMAND_NAME(@"status")
printf(" %-25s | %lld (Peak: %.2f%%)\n", "Watchdog CPU Events", cpuEvents, cpuPeak);
printf(" %-25s | %lld (Peak: %.2fMB)\n", "Watchdog RAM Events", ramEvents, ramPeak);
if (cachingEnabled) {
printf(">>> Cache Info\n");
printf(" %-25s | %lld\n", "Root cache count", rootCacheCount);
printf(" %-25s | %lld\n", "Non-root cache count", nonRootCacheCount);
}
printf(">>> Cache Info\n");
printf(" %-25s | %lld\n", "Root cache count", rootCacheCount);
printf(" %-25s | %lld\n", "Non-root cache count", nonRootCacheCount);
printf(">>> Database Info\n");
printf(" %-25s | %lld\n", "Binary Rules", binaryRuleCount);

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -105,6 +105,7 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
// This is a Santa-curated list of paths to check on startup. This list will be merged
// with the set of default muted paths from ES.
NSSet *santaDefinedCriticalPaths = [NSSet setWithArray:@[
@"/usr/libexec/trustd",
@"/usr/lib/dyld",
@@ -136,6 +137,12 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
NSMutableDictionary *bins = [NSMutableDictionary dictionary];
for (NSString *path in [SNTRuleTable criticalSystemBinaryPaths]) {
SNTFileInfo *binInfo = [[SNTFileInfo alloc] initWithPath:path];
if (!binInfo.SHA256) {
// If there isn't a hash, no need to compute the other info here.
// Just continue on to the next binary.
LOGW(@"Unable to compute hash for critical system binary %@.", path);
continue;
}
MOLCodesignChecker *csInfo = [binInfo codesignCheckerWithError:NULL];
// Make sure the critical system binary is signed by the same chain as launchd/self
@@ -143,9 +150,9 @@ static void addPathsFromDefaultMuteSet(NSMutableSet *criticalPaths) API_AVAILABL
if ([csInfo signingInformationMatches:self.launchdCSInfo]) {
systemBin = YES;
} else if (![csInfo signingInformationMatches:self.santadCSInfo]) {
LOGE(@"Unable to validate critical system binary. "
LOGW(@"Unable to validate critical system binary %@. "
@"pid 1: %@, santad: %@ and %@: %@ do not match.",
self.launchdCSInfo.leafCertificate, self.santadCSInfo.leafCertificate, path,
path, self.launchdCSInfo.leafCertificate, self.santadCSInfo.leafCertificate, path,
csInfo.leafCertificate);
continue;
}

View File

@@ -0,0 +1,75 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__SANTAD__EVENTPROVIDERS_AUTHRESULTCACHE_H
#define SANTA__SANTAD__EVENTPROVIDERS_AUTHRESULTCACHE_H
#include <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#include <dispatch/dispatch.h>
#include <sys/stat.h>
#include <memory>
#import "Source/common/SNTCommon.h"
#include "Source/common/SantaCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
namespace santa::santad::event_providers {
enum class FlushCacheMode {
kNonRootOnly,
kAllCaches,
};
class AuthResultCache {
public:
// Santa currently only flushes caches when new DENY rules are added, not
// ALLOW rules. This means this value should be low enough so that if a
// previously denied binary is allowed, it can be re-executed by the user in a
// timely manner. But the value should be high enough to allow the cache to be
// effective in the event the binary is executed in rapid succession.
AuthResultCache(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
uint64_t cache_deny_time_ms = 1500);
virtual ~AuthResultCache();
AuthResultCache(AuthResultCache &&other) = delete;
AuthResultCache &operator=(AuthResultCache &&rhs) = delete;
AuthResultCache(const AuthResultCache &other) = delete;
AuthResultCache &operator=(const AuthResultCache &other) = delete;
virtual bool AddToCache(const es_file_t *es_file, santa_action_t decision);
virtual void RemoveFromCache(const es_file_t *es_file);
virtual santa_action_t CheckCache(const es_file_t *es_file);
virtual santa_action_t CheckCache(santa_vnode_id_t vnode_id);
virtual void FlushCache(FlushCacheMode mode);
virtual NSArray<NSNumber *> *CacheCounts();
private:
virtual SantaCache<santa_vnode_id_t, uint64_t> *CacheForVnodeID(santa_vnode_id_t vnode_id);
SantaCache<santa_vnode_id_t, uint64_t> *root_cache_;
SantaCache<santa_vnode_id_t, uint64_t> *nonroot_cache_;
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
uint64_t root_devno_;
uint64_t cache_deny_time_ns_;
dispatch_queue_t q_;
};
} // namespace santa::santad::event_providers
#endif

View File

@@ -0,0 +1,156 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include "Source/santad/EventProviders/AuthResultCache.h"
#include <mach/clock_types.h>
#include <time.h>
#import "Source/common/SNTLogging.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
using santa::santad::event_providers::endpoint_security::Client;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
template <>
uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const &t) {
return (SantaCacheHasher<uint64_t>(t.fsid) << 1) ^ SantaCacheHasher<uint64_t>(t.fileid);
}
namespace santa::santad::event_providers {
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
return santa_vnode_id_t{
.fsid = (uint64_t)es_file->stat.st_dev,
.fileid = es_file->stat.st_ino,
};
}
static inline uint64_t GetCurrentUptime() {
return clock_gettime_nsec_np(CLOCK_MONOTONIC);
}
// Decision is stored in upper 8 bits, timestamp in remaining 56.
static inline uint64_t CacheableAction(santa_action_t action,
uint64_t timestamp = GetCurrentUptime()) {
return ((uint64_t)action << 56) | (timestamp & 0xFFFFFFFFFFFFFF);
}
static inline santa_action_t ActionFromCachedValue(uint64_t cachedValue) {
return (santa_action_t)(cachedValue >> 56);
}
static inline uint64_t TimestampFromCachedValue(uint64_t cachedValue) {
return (cachedValue & ~(0xFF00000000000000));
}
AuthResultCache::AuthResultCache(std::shared_ptr<EndpointSecurityAPI> esapi,
uint64_t cache_deny_time_ms)
: esapi_(esapi), cache_deny_time_ns_(cache_deny_time_ms * NSEC_PER_MSEC) {
root_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
nonroot_cache_ = new SantaCache<santa_vnode_id_t, uint64_t>();
struct stat sb;
if (stat("/", &sb) == 0) {
root_devno_ = sb.st_dev;
}
q_ = dispatch_queue_create(
"com.google.santa.daemon.auth_result_cache.q",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL,
QOS_CLASS_USER_INTERACTIVE, 0));
}
AuthResultCache::~AuthResultCache() {
delete root_cache_;
delete nonroot_cache_;
}
bool AuthResultCache::AddToCache(const es_file_t *es_file, santa_action_t decision) {
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
switch (decision) {
case ACTION_REQUEST_BINARY:
return cache->set(vnode_id, CacheableAction(ACTION_REQUEST_BINARY, 0), 0);
case ACTION_RESPOND_ALLOW: OS_FALLTHROUGH;
case ACTION_RESPOND_ALLOW_COMPILER: OS_FALLTHROUGH;
case ACTION_RESPOND_DENY:
return cache->set(vnode_id, CacheableAction(decision),
CacheableAction(ACTION_REQUEST_BINARY, 0));
default:
// This is a programming error. Bail.
LOGE(@"Invalid cache value, exiting.");
exit(EXIT_FAILURE);
}
}
void AuthResultCache::RemoveFromCache(const es_file_t *es_file) {
santa_vnode_id_t vnode_id = VnodeForFile(es_file);
CacheForVnodeID(vnode_id)->remove(vnode_id);
}
santa_action_t AuthResultCache::CheckCache(const es_file_t *es_file) {
return CheckCache(VnodeForFile(es_file));
}
santa_action_t AuthResultCache::CheckCache(santa_vnode_id_t vnode_id) {
SantaCache<santa_vnode_id_t, uint64_t> *cache = CacheForVnodeID(vnode_id);
uint64_t cached_val = cache->get(vnode_id);
if (cached_val == 0) {
return ACTION_UNSET;
}
santa_action_t result = ActionFromCachedValue(cached_val);
if (result == ACTION_RESPOND_DENY) {
uint64_t expiry_time = TimestampFromCachedValue(cached_val) + cache_deny_time_ns_;
if (expiry_time < GetCurrentUptime()) {
cache->remove(vnode_id);
return ACTION_UNSET;
}
}
return result;
}
SantaCache<santa_vnode_id_t, uint64_t> *AuthResultCache::CacheForVnodeID(
santa_vnode_id_t vnode_id) {
return (vnode_id.fsid == root_devno_ || root_devno_ == 0) ? root_cache_ : nonroot_cache_;
}
void AuthResultCache::FlushCache(FlushCacheMode mode) {
nonroot_cache_->clear();
if (mode == FlushCacheMode::kAllCaches) {
root_cache_->clear();
// Clear the ES cache when all local caches are flushed. Assume the ES cache
// doesn't need to be cleared when only flushing the non-root cache.
//
// Calling into ES should be done asynchronously since it could otherwise
// potentially deadlock.
auto shared_esapi = esapi_->shared_from_this();
dispatch_async(q_, ^{
// ES does not need a connected client to clear cache
shared_esapi->ClearCache(Client());
});
}
}
NSArray<NSNumber *> *AuthResultCache::CacheCounts() {
return @[ @(root_cache_->count()), @(nonroot_cache_->count()) ];
}
} // namespace santa::santad::event_providers

View File

@@ -0,0 +1,225 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <EndpointSecurity/EndpointSecurity.h>
#include <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <time.h>
#include <memory>
#include "Source/common/SNTCommon.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::FlushCacheMode;
// Grab the st_dev number of the root volume to match the root cache
static uint64_t RootDevno() {
static dispatch_once_t once_token;
static uint64_t devno;
dispatch_once(&once_token, ^{
struct stat sb;
stat("/", &sb);
devno = sb.st_dev;
});
return devno;
}
static inline es_file_t MakeCacheableFile(uint64_t devno, uint64_t ino) {
return es_file_t{
.path = {}, .path_truncated = false, .stat = {.st_dev = (dev_t)devno, .st_ino = ino}};
}
static inline santa_vnode_id_t VnodeForFile(const es_file_t *es_file) {
return santa_vnode_id_t{
.fsid = (uint64_t)es_file->stat.st_dev,
.fileid = es_file->stat.st_ino,
};
}
static inline void AssertCacheCounts(std::shared_ptr<AuthResultCache> cache, uint64_t root_count,
uint64_t nonroot_count) {
NSArray<NSNumber *> *counts = cache->CacheCounts();
XCTAssertNotNil(counts);
XCTAssertEqual([counts count], 2);
XCTAssertNotNil(counts[0]);
XCTAssertNotNil(counts[1]);
XCTAssertEqual([counts[0] unsignedLongLongValue], root_count);
XCTAssertEqual([counts[1] unsignedLongLongValue], nonroot_count);
}
@interface AuthResultCacheTest : XCTestCase
@end
@implementation AuthResultCacheTest
- (void)testEmptyCacheExpectedNumberOfCacheCounts {
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
auto cache = std::make_shared<AuthResultCache>(esapi);
AssertCacheCounts(cache, 0, 0);
}
- (void)testBasicOperation {
auto esapi = std::make_shared<MockEndpointSecurityAPI>();
auto cache = std::make_shared<AuthResultCache>(esapi);
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 222);
// Add the root file to the cache
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
AssertCacheCounts(cache, 1, 0);
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_UNSET);
// Now add the non-root file
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
AssertCacheCounts(cache, 1, 1);
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_REQUEST_BINARY);
// Update the cached values
cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW);
cache->AddToCache(&nonrootFile, ACTION_RESPOND_DENY);
AssertCacheCounts(cache, 1, 1);
XCTAssertEqual(cache->CheckCache(VnodeForFile(&rootFile)), ACTION_RESPOND_ALLOW);
XCTAssertEqual(cache->CheckCache(VnodeForFile(&nonrootFile)), ACTION_RESPOND_DENY);
// Remove the root file
cache->RemoveFromCache(&rootFile);
AssertCacheCounts(cache, 0, 1);
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
XCTAssertEqual(cache->CheckCache(&nonrootFile), ACTION_RESPOND_DENY);
}
- (void)testFlushCache {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
auto cache = std::make_shared<AuthResultCache>(mockESApi);
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
es_file_t nonrootFile = MakeCacheableFile(RootDevno() + 123, 111);
cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY);
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
AssertCacheCounts(cache, 1, 1);
// Flush non-root only
cache->FlushCache(FlushCacheMode::kNonRootOnly);
AssertCacheCounts(cache, 1, 0);
// Add back the non-root file
cache->AddToCache(&nonrootFile, ACTION_REQUEST_BINARY);
AssertCacheCounts(cache, 1, 1);
// Flush all caches
// The call to ClearCache is asynchronous. Use a semaphore to
// be notified when the mock is called.
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
EXPECT_CALL(*mockESApi, ClearCache).WillOnce(testing::InvokeWithoutArgs(^() {
dispatch_semaphore_signal(sema);
return true;
}));
cache->FlushCache(FlushCacheMode::kAllCaches);
XCTAssertEqual(0,
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
"ClearCache wasn't called within expected time window");
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
AssertCacheCounts(cache, 0, 0);
}
- (void)testCacheStateMachine {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
auto cache = std::make_shared<AuthResultCache>(mockESApi);
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
// Cached items must first be in the ACTION_REQUEST_BINARY state
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW));
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_ALLOW_COMPILER));
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
// Items in the `ACTION_REQUEST_BINARY` state cannot reenter the same state
XCTAssertFalse(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
santa_action_t allowed_transitions[] = {
ACTION_RESPOND_ALLOW,
ACTION_RESPOND_ALLOW_COMPILER,
ACTION_RESPOND_DENY,
};
for (size_t i = 0; i < sizeof(allowed_transitions) / sizeof(allowed_transitions[0]); i++) {
// First make sure the item doesn't exist
cache->RemoveFromCache(&rootFile);
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
// Now add the item to be in the first allowed state
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_REQUEST_BINARY);
// Now assert the allowed transition
XCTAssertTrue(cache->AddToCache(&rootFile, allowed_transitions[i]));
XCTAssertEqual(cache->CheckCache(&rootFile), allowed_transitions[i]);
}
}
- (void)testCacheExpiry {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
// Create a cache with a lowered cache expiry value
uint64_t expiryMS = 250;
auto cache = std::make_shared<AuthResultCache>(mockESApi, expiryMS);
es_file_t rootFile = MakeCacheableFile(RootDevno(), 111);
// Add a file to the cache and put into the ACTION_RESPOND_DENY state
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_REQUEST_BINARY));
XCTAssertTrue(cache->AddToCache(&rootFile, ACTION_RESPOND_DENY));
// Ensure the file exists
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_RESPOND_DENY);
// Wait for the item to expire
SleepMS(expiryMS);
// Check cache counts to make sure the item still exists
AssertCacheCounts(cache, 1, 0);
// Now check the cache, which will remove the item
XCTAssertEqual(cache->CheckCache(&rootFile), ACTION_UNSET);
AssertCacheCounts(cache, 0, 0);
}
@end

View File

@@ -1,4 +1,4 @@
/// Copyright 2021 Google Inc. All rights reserved.
/// Copyright 2021-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -50,7 +50,8 @@ typedef void (^MockDADiskAppearedCallback)(DADiskRef ref);
@end
//
// All DiskArbitration functions used in SNTDeviceManager and shimmed out accordingly.
// All DiskArbitration functions used in SNTEndpointSecurityDeviceManager
// and shimmed out accordingly.
//
CF_EXTERN_C_BEGIN

View File

@@ -0,0 +1,69 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_CLIENT_H
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_CLIENT_H
#include <EndpointSecurity/EndpointSecurity.h>
#include <cstddef>
namespace santa::santad::event_providers::endpoint_security {
class Client {
public:
explicit Client(es_client_t* client, es_new_client_result_t result)
: client_(client), result_(result) {}
Client() : client_(nullptr), result_(ES_NEW_CLIENT_RESULT_ERR_INTERNAL) {}
virtual ~Client() {
if (client_) {
// Special case: Not using EndpointSecurityAPI here due to circular refs.
es_delete_client(client_);
}
}
Client(Client&& other) {
client_ = other.client_;
result_ = other.result_;
other.client_ = nullptr;
other.result_ = ES_NEW_CLIENT_RESULT_ERR_INTERNAL;
}
Client& operator=(Client&& rhs) {
client_ = rhs.client_;
result_ = rhs.result_;
rhs.client_ = nullptr;
rhs.result_ = ES_NEW_CLIENT_RESULT_ERR_INTERNAL;
return *this;
}
Client(const Client& other) = delete;
void operator=(const Client& rhs) = delete;
inline bool IsConnected() { return result_ == ES_NEW_CLIENT_RESULT_SUCCESS; }
inline es_new_client_result_t NewClientResult() { return result_; }
inline es_client_t* Get() const { return client_; }
private:
es_client_t* client_;
es_new_client_result_t result_;
};
} // namespace santa::santad::event_providers::endpoint_security
#endif

View File

@@ -0,0 +1,118 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <dispatch/dispatch.h>
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
using santa::santad::event_providers::endpoint_security::Client;
// Global semaphore used for custom `es_delete_client` function
dispatch_semaphore_t gSema;
// Note: The Client class does not use the `EndpointSecurityAPI` wrappers due
// to circular dependency issues. It is a special case that uses the underlying
// ES API `es_delete_client` directly. This test override will signal the
// `gSema` semaphore to indicate it has been called.
es_return_t es_delete_client(es_client_t *_Nullable client) {
dispatch_semaphore_signal(gSema);
return ES_RETURN_SUCCESS;
};
@interface ClientTest : XCTestCase
@end
@implementation ClientTest
- (void)setUp {
gSema = dispatch_semaphore_create(0);
}
- (void)testConstructorsAndDestructors {
// Ensure constructors set internal state properly
// Anonymous scopes used to ensure destructors called as expected
// Null `es_client_t*` *shouldn't* trigger `es_delete_client`
{
Client c;
XCTAssertEqual(c.Get(), nullptr);
XCTAssertEqual(c.NewClientResult(), ES_NEW_CLIENT_RESULT_ERR_INTERNAL);
}
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
"es_delete_client called unexpectedly");
// Nonnull `es_client_t*` *should* trigger `es_delete_client`
{
int fake;
es_client_t *fakeClient = (es_client_t *)&fake;
Client c(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
XCTAssertEqual(c.Get(), fakeClient);
XCTAssertEqual(c.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
}
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
"es_delete_client not called within expected time window");
// Test move constructor
{
int fake;
es_client_t *fakeClient = (es_client_t *)&fake;
Client c1(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
Client c2(std::move(c1));
XCTAssertEqual(c1.Get(), nullptr);
XCTAssertEqual(c2.Get(), fakeClient);
XCTAssertEqual(c2.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
}
// Ensure `es_delete_client` was only called once when both `c1` and `c2`
// are destructed.
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
"es_delete_client not called within expected time window");
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
"es_delete_client called unexpectedly");
// Test move assignment
{
int fake;
es_client_t *fakeClient = (es_client_t *)&fake;
Client c1(fakeClient, ES_NEW_CLIENT_RESULT_SUCCESS);
Client c2;
c2 = std::move(c1);
XCTAssertEqual(c1.Get(), nullptr);
XCTAssertEqual(c2.Get(), fakeClient);
XCTAssertEqual(c2.NewClientResult(), ES_NEW_CLIENT_RESULT_SUCCESS);
}
// Ensure `es_delete_client` was only called once when both `c1` and `c2`
// are destructed.
XCTAssertEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
"es_delete_client not called within expected time window");
XCTAssertNotEqual(0, dispatch_semaphore_wait(gSema, DISPATCH_TIME_NOW),
"es_delete_client called unexpectedly");
}
- (void)testIsConnected {
XCTAssertFalse(Client().IsConnected());
XCTAssertFalse(Client(nullptr, ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED).IsConnected());
XCTAssertTrue(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS).IsConnected());
}
@end

View File

@@ -0,0 +1,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

View File

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

View 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

View File

@@ -0,0 +1,44 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHER_H
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHER_H
#include <memory>
#include "Source/common/SantaCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
namespace santa::santad::event_providers::endpoint_security {
class Enricher {
public:
Enricher();
virtual ~Enricher() = default;
virtual std::shared_ptr<EnrichedMessage> Enrich(Message &&msg);
virtual EnrichedProcess Enrich(const es_process_t &es_proc);
virtual EnrichedFile Enrich(const es_file_t &es_file);
virtual std::optional<std::shared_ptr<std::string>> UsernameForUID(uid_t uid);
virtual std::optional<std::shared_ptr<std::string>> UsernameForGID(gid_t gid);
private:
SantaCache<uid_t, std::optional<std::shared_ptr<std::string>>>
username_cache_;
SantaCache<gid_t, std::optional<std::shared_ptr<std::string>>>
groupname_cache_;
};
} // namespace santa::santad::event_providers::endpoint_security
#endif

View File

@@ -0,0 +1,137 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
#include <EndpointSecurity/ESTypes.h>
#include <bsm/libbsm.h>
#include <grp.h>
#include <pwd.h>
#include <sys/types.h>
#include <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

View File

@@ -0,0 +1,49 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
using santa::santad::event_providers::endpoint_security::Enricher;
@interface EnricherTest : XCTestCase
@end
@implementation EnricherTest
- (void)testUidGid {
Enricher enricher;
std::optional<std::shared_ptr<std::string>> user = enricher.UsernameForUID(NOBODY_UID);
XCTAssertTrue(user.has_value());
XCTAssertEqual(strcmp(user->get()->c_str(), "nobody"), 0);
std::optional<std::shared_ptr<std::string>> group = enricher.UsernameForGID(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

View File

@@ -0,0 +1,60 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MESSAGE_H
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_MESSAGE_H
#include <EndpointSecurity/EndpointSecurity.h>
#include <memory>
#include <string>
namespace santa::santad::event_providers::endpoint_security {
class EndpointSecurityAPI;
class Message {
public:
Message(std::shared_ptr<EndpointSecurityAPI> esapi,
const es_message_t* es_msg);
~Message();
Message(Message&& other);
// Note: Safe to implement this, just not currently needed so left deleted.
Message& operator=(Message&& rhs) = delete;
// In macOS 10.15, es_retain_message/es_release_message were unsupported
// and required a full copy, which impacts performance if done too much...
Message(const Message& other);
Message& operator=(const Message& other) = delete;
// Operators to access underlying es_message_t
const es_message_t* operator->() const { return es_msg_; }
const es_message_t& operator*() const { return *es_msg_; }
std::string ParentProcessName() const;
private:
std::shared_ptr<EndpointSecurityAPI> esapi_;
es_message_t* es_msg_;
mutable std::string pname_;
mutable std::string parent_pname_;
std::string GetProcessName(pid_t pid) const;
};
} // namespace santa::santad::event_providers::endpoint_security
#endif

View File

@@ -0,0 +1,65 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include <bsm/libbsm.h>
#include <libproc.h>
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
namespace santa::santad::event_providers::endpoint_security {
Message::Message(std::shared_ptr<EndpointSecurityAPI> esapi, const es_message_t *es_msg)
: esapi_(esapi) {
es_msg_ = esapi_->RetainMessage(es_msg);
}
Message::~Message() {
if (es_msg_) {
esapi_->ReleaseMessage(es_msg_);
}
}
Message::Message(Message &&other) {
esapi_ = std::move(other.esapi_);
es_msg_ = other.es_msg_;
other.es_msg_ = nullptr;
}
Message::Message(const Message &other) {
esapi_ = other.esapi_;
es_msg_ = other.es_msg_;
esapi_->RetainMessage(es_msg_);
}
std::string Message::ParentProcessName() const {
if (parent_pname_.length() == 0) {
parent_pname_ = GetProcessName(es_msg_->process->ppid);
}
return parent_pname_;
}
std::string Message::GetProcessName(pid_t pid) const {
// Note: proc_name() accesses the `pbi_name` field of `struct proc_bsdinfo`. The size of `pname`
// here is meant to match the size of `pbi_name`, and one extra byte ensure zero-terminated.
char pname[MAXCOMLEN * 2 + 1] = {};
if (proc_name(pid, pname, sizeof(pname)) > 0) {
return std::string(pname);
} else {
return std::string("");
}
}
} // namespace santa::santad::event_providers::endpoint_security

View File

@@ -0,0 +1,135 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <libproc.h>
#include <stdlib.h>
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
using santa::santad::event_providers::endpoint_security::Message;
bool IsPidInUse(pid_t pid) {
char pname[MAXCOMLEN * 2 + 1] = {};
errno = 0;
if (proc_name(pid, pname, sizeof(pname)) <= 0 && errno == ESRCH) {
return false;
}
// The PID may or may not actually be in use, but assume it is
return true;
}
// Try to find an unused PID by looking for libproc returning ESRCH errno.
// Start searching backwards from PID_MAX to increase likelyhood that the
// returned PID will still be unused by the time it's being used.
// TODO(mlw): Alternatively, we could inject the `proc_name` function into
// the `Message` object to remove the guesswork here.
pid_t AttemptToFindUnusedPID() {
for (pid_t pid = 99999 /* PID_MAX */; pid > 1; pid--) {
if (!IsPidInUse(pid)) {
return pid;
}
}
return 0;
}
@interface MessageTest : XCTestCase
@end
@implementation MessageTest
- (void)setUp {
}
- (void)testConstructorsAndDestructors {
es_file_t procFile = MakeESFile("foo");
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
// Constructing a `Message` retains the underlying `es_message_t` and it is
// released when the `Message` object is destructed.
{ Message m(mockESApi, &esMsg); }
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testCopyConstructor {
es_file_t procFile = MakeESFile("foo");
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
EXPECT_CALL(*mockESApi, ReleaseMessage(testing::_))
.Times(2)
.After(EXPECT_CALL(*mockESApi, RetainMessage(testing::_))
.Times(2)
.WillRepeatedly(testing::Return(&esMsg)));
{
Message msg1(mockESApi, &esMsg);
Message msg2(msg1);
// Both messages should now point to the same `es_message_t`
XCTAssertEqual(msg1.operator->(), &esMsg);
XCTAssertEqual(msg2.operator->(), &esMsg);
}
// Ensure the retain/release mocks were called the expected number of times
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testGetParentProcessName {
// Construct a message where the parent pid is ourself
es_file_t procFile = MakeESFile("foo");
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(getpid(), 0));
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
// Search for an *existing* parent process.
{
Message msg(mockESApi, &esMsg);
std::string got = msg.ParentProcessName();
std::string want = getprogname();
XCTAssertCppStringEqual(got, want);
}
// Search for a *non-existent* parent process.
{
pid_t newPpid = AttemptToFindUnusedPID();
proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(newPpid, 34));
Message msg(mockESApi, &esMsg);
std::string got = msg.ParentProcessName();
std::string want = "";
XCTAssertCppStringEqual(got, want);
}
}
@end

View File

@@ -0,0 +1,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

View File

@@ -1,104 +0,0 @@
/// Copyright 2021 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <EndpointSecurity/EndpointSecurity.h>
#include <Foundation/Foundation.h>
#include <bsm/libbsm.h>
CF_EXTERN_C_BEGIN
es_string_token_t MakeStringToken(const NSString *_Nonnull s);
es_file_t MakeESFile(const char *_Nonnull path);
es_process_t MakeESProcess(es_file_t *_Nonnull esFile);
es_message_t MakeESMessage(es_event_type_t eventType, es_process_t *_Nonnull instigator,
struct timespec ts);
CF_EXTERN_C_END
@class ESMessage;
typedef void (^ESMessageBuilderBlock)(ESMessage *_Nonnull builder);
// An ObjC builder wrapper around es_message_t
@interface ESMessage : NSObject
@property(nonatomic, readwrite, strong) NSString *_Nullable binaryPath;
@property(nonatomic, readwrite) es_file_t *_Nonnull executable;
@property(nonatomic, readwrite) es_process_t *_Nonnull process;
@property(nonatomic, readwrite) es_message_t *_Nonnull message;
@property(nonatomic, readonly) pid_t pid;
- (instancetype _Nonnull)initWithBlock:(ESMessageBuilderBlock _Nullable)block
NS_DESIGNATED_INITIALIZER;
@end
@interface ESResponse : NSObject
@property(nonatomic) es_auth_result_t result;
@property(nonatomic) bool shouldCache;
@end
typedef void (^ESCallback)(ESResponse *_Nonnull);
// Singleton wrapper around all of the kernel-level EndpointSecurity framework functions.
@interface MockEndpointSecurity : NSObject
@property NSMutableArray *_Nonnull subscriptions;
- (void)reset;
- (void)registerResponseCallback:(es_event_type_t)t withCallback:(ESCallback _Nonnull)callback;
- (void)triggerHandler:(es_message_t *_Nonnull)msg;
/// Retrieve an initialized singleton MockEndpointSecurity object
+ (instancetype _Nonnull)mockEndpointSecurity;
@end
API_UNAVAILABLE(ios, tvos, watchos)
es_message_t *_Nullable es_copy_message(const es_message_t *_Nonnull msg);
API_UNAVAILABLE(ios, tvos, watchos)
void es_free_message(es_message_t *_Nonnull msg);
API_AVAILABLE(macos(10.15))
API_UNAVAILABLE(ios, tvos, watchos)
es_new_client_result_t es_new_client(es_client_t *_Nullable *_Nonnull client,
es_handler_block_t _Nonnull handler);
API_AVAILABLE(macos(10.15)) API_UNAVAILABLE(ios, tvos, watchos)
es_return_t es_mute_process(es_client_t * _Nonnull client,
const audit_token_t * _Nonnull audit_token);
#if defined(MAC_OS_VERSION_12_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0
API_AVAILABLE(macos(12.0))
API_UNAVAILABLE(ios, tvos, watchos)
es_return_t es_muted_paths_events(es_client_t *_Nonnull client,
es_muted_paths_t *_Nonnull *_Nullable muted_paths);
API_AVAILABLE(macos(12.0))
API_UNAVAILABLE(ios, tvos, watchos)
void es_release_muted_paths(es_muted_paths_t *_Nonnull muted_paths);
#endif
API_AVAILABLE(macos(10.15))
API_UNAVAILABLE(ios, tvos, watchos)
es_respond_result_t es_respond_auth_result(es_client_t *_Nonnull client,
const es_message_t *_Nonnull message,
es_auth_result_t result, bool cache);
API_AVAILABLE(macos(10.15))
API_UNAVAILABLE(ios, tvos, watchos)
es_return_t es_subscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
uint32_t event_count);
API_AVAILABLE(macos(10.15))
API_UNAVAILABLE(ios, tvos, watchos) es_return_t es_delete_client(es_client_t *_Nullable client);
API_AVAILABLE(macos(10.15))
API_UNAVAILABLE(ios, tvos, watchos)
es_return_t es_unsubscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
uint32_t event_count);

View File

@@ -1,367 +0,0 @@
/// Copyright 2021 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <Foundation/Foundation.h>
#include <EndpointSecurity/EndpointSecurity.h>
#include <bsm/libbsm.h>
#include <stdlib.h>
#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h"
CF_EXTERN_C_BEGIN
es_string_token_t MakeStringToken(const NSString *_Nonnull s) {
return (es_string_token_t){
.length = [s length],
.data = [s UTF8String],
};
}
es_file_t MakeESFile(const char *path) {
es_file_t esFile = {};
esFile.path.data = path;
esFile.path.length = strlen(path);
esFile.path_truncated = false;
// Note: stat info is currently unused / not populated
return esFile;
}
es_process_t MakeESProcess(es_file_t *esFile) {
es_process_t esProc = {};
esProc.executable = esFile;
return esProc;
}
es_message_t MakeESMessage(es_event_type_t eventType, es_process_t *instigator,
struct timespec ts) {
es_message_t esMsg = {};
esMsg.time = ts;
esMsg.event_type = eventType;
esMsg.process = instigator;
return esMsg;
}
CF_EXTERN_C_END
@implementation ESMessage
- (instancetype)init {
return [self initWithBlock:nil];
}
- (instancetype)initWithBlock:(ESMessageBuilderBlock)block {
NSParameterAssert(block);
self = [super init];
if (self) {
_pid = arc4random();
[self initBaseObjects];
block(self);
[self fillLinks];
}
return self;
}
- (void)initBaseObjects {
self.executable = static_cast<es_file_t *>(calloc(1, sizeof(es_file_t)));
self.process = static_cast<es_process_t *>(calloc(1, sizeof(es_process_t)));
self.process->ppid = self.pid;
self.process->original_ppid = self.pid;
self.process->group_id = static_cast<pid_t>(arc4random());
self.process->session_id = static_cast<pid_t>(arc4random());
self.process->codesigning_flags =
0x1 | 0x20000000; // CS_VALID | CS_SIGNED -> See kern/cs_blobs.h
self.process->is_platform_binary = false;
self.process->is_es_client = false;
self.message = static_cast<es_message_t *>(calloc(1, sizeof(es_message_t)));
self.message->version = 4;
self.message->mach_time = DISPATCH_TIME_NOW;
self.message->deadline = DISPATCH_TIME_FOREVER;
self.message->seq_num = 1;
}
- (void)fillLinks {
if (self.binaryPath != nil) {
self.executable->path = MakeStringToken(self.binaryPath);
}
if (self.process->executable == NULL) {
self.process->executable = self.executable;
}
if (self.message->process == NULL) {
self.message->process = self.process;
}
}
- (void)dealloc {
free(self.process);
free(self.executable);
free(self.message);
}
@end
@implementation ESResponse
@end
@interface MockESClient : NSObject
@property NSMutableArray *_Nonnull subscriptions;
@property es_handler_block_t handler;
@end
@implementation MockESClient
- (instancetype)init {
self = [super init];
if (self) {
@synchronized(self) {
_subscriptions = [NSMutableArray arrayWithCapacity:ES_EVENT_TYPE_LAST];
for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) {
[self.subscriptions addObject:@NO];
}
}
}
return self;
};
- (void)resetSubscriptions {
for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) {
_subscriptions[i] = @NO;
}
}
- (void)triggerHandler:(es_message_t *_Nonnull)msg {
self.handler((__bridge es_client_t *_Nullable)self, msg);
}
- (void)dealloc {
@synchronized(self) {
[self.subscriptions removeAllObjects];
}
}
@end
@interface MockEndpointSecurity ()
@property NSMutableArray<MockESClient *> *clients;
// Array of collections of ESCallback blocks
// This should be of size ES_EVENT_TYPE_LAST, allowing for indexing by ES_EVENT_TYPE_xxx members.
@property NSMutableArray<NSMutableArray<ESCallback> *> *responseCallbacks;
@end
@implementation MockEndpointSecurity
- (instancetype)init {
self = [super init];
if (self) {
@synchronized(self) {
_clients = [NSMutableArray array];
_responseCallbacks = [NSMutableArray arrayWithCapacity:ES_EVENT_TYPE_LAST];
for (size_t i = 0; i < ES_EVENT_TYPE_LAST; i++) {
[self.responseCallbacks addObject:[NSMutableArray array]];
}
[self reset];
}
}
return self;
};
- (void)resetResponseCallbacks {
for (NSMutableArray *callback in self.responseCallbacks) {
if (callback != nil) {
[callback removeAllObjects];
}
}
}
- (void)reset {
@synchronized(self) {
[self.clients removeAllObjects];
[self resetResponseCallbacks];
}
};
- (void)newClient:(es_client_t *_Nullable *_Nonnull)client
handler:(es_handler_block_t __strong)handler {
// es_client_t is generally used as a pointer to an opaque struct (secretly a mach port).
// There is also a few nonnull initialization checks on it.
MockESClient *mockClient = [[MockESClient alloc] init];
*client = (__bridge es_client_t *)mockClient;
mockClient.handler = handler;
[self.clients addObject:mockClient];
}
- (BOOL)removeClient:(es_client_t *_Nonnull)client {
MockESClient *clientToRemove = [self findClient:client];
if (!clientToRemove) {
NSLog(@"Attempted to remove unknown mock es client.");
return NO;
}
[self.clients removeObject:clientToRemove];
return YES;
}
- (void)triggerHandler:(es_message_t *_Nonnull)msg {
for (MockESClient *client in self.clients) {
if (client.subscriptions[msg->event_type]) {
[client triggerHandler:msg];
}
}
}
- (void)registerResponseCallback:(es_event_type_t)t withCallback:(ESCallback _Nonnull)callback {
@synchronized(self) {
[self.responseCallbacks[t] addObject:callback];
}
}
- (es_respond_result_t)respond_auth_result:(const es_message_t *_Nonnull)msg
result:(es_auth_result_t)result
cache:(bool)cache {
@synchronized(self) {
ESResponse *response = [[ESResponse alloc] init];
response.result = result;
response.shouldCache = cache;
for (void (^callback)(ESResponse *) in self.responseCallbacks[msg->event_type]) {
callback(response);
}
}
return ES_RESPOND_RESULT_SUCCESS;
};
- (MockESClient *)findClient:(es_client_t *)client {
for (MockESClient *c in self.clients) {
// Since we're mocking out a C interface and using this exact pointer as our
// client identifier, only check for pointer equality.
if (client == (__bridge es_client_t *)c) {
return c;
}
}
return nil;
}
- (void)setSubscriptions:(const es_event_type_t *_Nonnull)events
event_count:(uint32_t)event_count
value:(NSNumber *)value
client:(es_client_t *)client {
@synchronized(self) {
MockESClient *toUpdate = [self findClient:client];
if (toUpdate == nil) {
NSLog(@"setting subscription for unknown client");
return;
}
for (size_t i = 0; i < event_count; i++) {
toUpdate.subscriptions[events[i]] = value;
}
}
}
+ (instancetype _Nonnull)mockEndpointSecurity {
static MockEndpointSecurity *sharedES;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedES = [[MockEndpointSecurity alloc] init];
});
return sharedES;
};
@end
API_UNAVAILABLE(ios, tvos, watchos)
es_message_t *_Nullable es_copy_message(const es_message_t *_Nonnull msg) {
return (es_message_t *)msg;
};
API_UNAVAILABLE(ios, tvos, watchos)
void es_free_message(es_message_t *_Nonnull msg){};
API_AVAILABLE(macos(10.15))
API_UNAVAILABLE(ios, tvos, watchos)
es_new_client_result_t es_new_client(es_client_t *_Nullable *_Nonnull client,
es_handler_block_t _Nonnull handler) {
[[MockEndpointSecurity mockEndpointSecurity] newClient:client handler:handler];
return ES_NEW_CLIENT_RESULT_SUCCESS;
};
es_return_t es_mute_process(es_client_t * _Nonnull client,
const audit_token_t * _Nonnull audit_token) {
return ES_RETURN_SUCCESS;
}
#if defined(MAC_OS_VERSION_12_0) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_12_0
API_AVAILABLE(macos(12.0))
API_UNAVAILABLE(ios, tvos, watchos)
es_return_t es_muted_paths_events(es_client_t *_Nonnull client,
es_muted_paths_t *_Nonnull *_Nullable muted_paths) {
es_muted_paths_t *tmp = (es_muted_paths_t *)malloc(sizeof(es_muted_paths_t));
tmp->count = 0;
*muted_paths = (es_muted_paths_t *_Nullable)tmp;
return ES_RETURN_SUCCESS;
};
API_AVAILABLE(macos(12.0))
API_UNAVAILABLE(ios, tvos, watchos)
void es_release_muted_paths(es_muted_paths_t *_Nonnull muted_paths) {
free(muted_paths);
}
#endif
API_AVAILABLE(macos(10.15))
API_UNAVAILABLE(ios, tvos, watchos) es_return_t es_delete_client(es_client_t *_Nullable client) {
if (![[MockEndpointSecurity mockEndpointSecurity] removeClient:client]) {
return ES_RETURN_ERROR;
}
return ES_RETURN_SUCCESS;
};
API_AVAILABLE(macos(10.15))
API_UNAVAILABLE(ios, tvos, watchos)
es_respond_result_t es_respond_auth_result(es_client_t *_Nonnull client,
const es_message_t *_Nonnull message,
es_auth_result_t result, bool cache) {
return [[MockEndpointSecurity mockEndpointSecurity] respond_auth_result:message
result:result
cache:cache];
};
API_AVAILABLE(macos(10.15))
API_UNAVAILABLE(ios, tvos, watchos)
es_return_t es_subscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
uint32_t event_count) {
[[MockEndpointSecurity mockEndpointSecurity] setSubscriptions:events
event_count:event_count
value:@YES
client:client];
return ES_RETURN_SUCCESS;
}
API_AVAILABLE(macos(10.15))
API_UNAVAILABLE(ios, tvos, watchos)
es_return_t es_unsubscribe(es_client_t *_Nonnull client, const es_event_type_t *_Nonnull events,
uint32_t event_count) {
[[MockEndpointSecurity mockEndpointSecurity] setSubscriptions:events
event_count:event_count
value:@NO
client:client];
return ES_RETURN_SUCCESS;
};

View File

@@ -1,210 +0,0 @@
/// Copyright 2021 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santad/EventProviders/SNTCachingEndpointSecurityManager.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SantaCache.h"
#include <EndpointSecurity/EndpointSecurity.h>
#include <bsm/libbsm.h>
uint64_t GetCurrentUptime() {
return clock_gettime_nsec_np(CLOCK_MONOTONIC);
}
template <>
uint64_t SantaCacheHasher<santa_vnode_id_t>(santa_vnode_id_t const &t) {
return (SantaCacheHasher<uint64_t>(t.fsid) << 1) ^ SantaCacheHasher<uint64_t>(t.fileid);
}
@implementation SNTCachingEndpointSecurityManager {
// Create 2 separate caches, mapping from the (filesysem + vnode ID) to a decision with a timestamp.
// The root cache is for decisions on the root volume, which can never be unmounted and the other
// is for executions from all other volumes. This cache will be emptied if any volume is unmounted.
SantaCache<santa_vnode_id_t, uint64_t> *_rootDecisionCache;
SantaCache<santa_vnode_id_t, uint64_t> *_nonRootDecisionCache;
uint64_t _rootVnodeID;
}
- (instancetype)init {
self = [super init];
if (self) {
_rootDecisionCache = new SantaCache<santa_vnode_id_t, uint64_t>();
_nonRootDecisionCache = new SantaCache<santa_vnode_id_t, uint64_t>();
// Store the filesystem ID of the root vnode for split-cache usage.
// If the stat fails for any reason _rootVnodeID will be 0 and all decisions will be in a single cache.
struct stat rootStat;
if (stat("/", &rootStat) == 0) {
_rootVnodeID = (uint64_t)rootStat.st_dev;
}
}
return self;
}
- (void)dealloc {
if (_rootDecisionCache) delete _rootDecisionCache;
if (_nonRootDecisionCache) delete _nonRootDecisionCache;
}
- (BOOL)respondFromCache:(es_message_t *)m API_AVAILABLE(macos(10.15)) {
auto vnode_id = [self vnodeIDForFile:m->event.exec.target->executable];
while (true) {
// Check to see if item is in cache
auto return_action = [self checkCache:vnode_id];
// If item was in cache with a valid response, return it.
// If item is in cache but hasn't received a response yet, sleep for a bit.
// If item is not in cache, break out of loop and forward request to callback.
if (RESPONSE_VALID(return_action)) {
switch (return_action) {
case ACTION_RESPOND_ALLOW:
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
break;
case ACTION_RESPOND_ALLOW_COMPILER: {
pid_t pid = audit_token_to_pid(m->process->audit_token);
[self setIsCompilerPID:pid];
// Don't let ES cache compilers
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
break;
}
default: es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false); break;
}
return YES;
} else if (return_action == ACTION_REQUEST_BINARY || return_action == ACTION_RESPOND_ACK) {
// TODO(rah): Look at a replacement for msleep(), maybe NSCondition
usleep(5000);
} else {
break;
}
}
[self addToCache:vnode_id decision:ACTION_REQUEST_BINARY currentTicks:0];
return NO;
}
- (int)postAction:(santa_action_t)action
forMessage:(santa_message_t)sm API_AVAILABLE(macos(10.15)) {
es_respond_result_t ret;
switch (action) {
case ACTION_RESPOND_ALLOW_COMPILER:
[self setIsCompilerPID:sm.pid];
// Allow the exec and cache in our internal cache but don't let ES cache, because then
// we won't see future execs of the compiler in order to record the PID.
[self addToCache:sm.vnode_id
decision:ACTION_RESPOND_ALLOW_COMPILER
currentTicks:GetCurrentUptime()];
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW,
false);
break;
case ACTION_RESPOND_ALLOW:
case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE:
[self addToCache:sm.vnode_id decision:ACTION_RESPOND_ALLOW currentTicks:GetCurrentUptime()];
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW,
true);
break;
case ACTION_RESPOND_DENY:
[self addToCache:sm.vnode_id decision:ACTION_RESPOND_DENY currentTicks:GetCurrentUptime()];
OS_FALLTHROUGH;
case ACTION_RESPOND_TOOLONG:
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_DENY,
false);
break;
case ACTION_RESPOND_ACK: return ES_RESPOND_RESULT_SUCCESS;
default: ret = ES_RESPOND_RESULT_ERR_INVALID_ARGUMENT;
}
return ret;
}
- (void)addToCache:(santa_vnode_id_t)identifier
decision:(santa_action_t)decision
currentTicks:(uint64_t)microsecs {
auto _decisionCache = [self cacheForVnodeID:identifier];
switch (decision) {
case ACTION_REQUEST_BINARY:
_decisionCache->set(identifier, (uint64_t)ACTION_REQUEST_BINARY << 56, 0);
break;
case ACTION_RESPOND_ACK:
_decisionCache->set(identifier, (uint64_t)ACTION_RESPOND_ACK << 56,
((uint64_t)ACTION_REQUEST_BINARY << 56));
break;
case ACTION_RESPOND_ALLOW:
case ACTION_RESPOND_ALLOW_COMPILER:
case ACTION_RESPOND_DENY: {
// Decision is stored in upper 8 bits, timestamp in remaining 56.
uint64_t val = ((uint64_t)decision << 56) | (microsecs & 0xFFFFFFFFFFFFFF);
if (!_decisionCache->set(identifier, val, ((uint64_t)ACTION_REQUEST_BINARY << 56))) {
_decisionCache->set(identifier, val, ((uint64_t)ACTION_RESPOND_ACK << 56));
}
break;
}
case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE: {
// Decision is stored in upper 8 bits, timestamp in remaining 56.
uint64_t val = ((uint64_t)decision << 56) | (microsecs & 0xFFFFFFFFFFFFFF);
_decisionCache->set(identifier, val, 0);
break;
}
default: break;
}
// TODO(rah): Look at a replacement for wakeup(), maybe NSCondition
}
- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly API_AVAILABLE(macos(10.15)) {
_nonRootDecisionCache->clear();
if (!nonRootOnly) _rootDecisionCache->clear();
if (!self.connectionEstablished) return YES; // if not connected, there's nothing to flush.
return es_clear_cache(self.client) == ES_CLEAR_CACHE_RESULT_SUCCESS;
}
- (NSArray<NSNumber *> *)cacheCounts {
return @[ @(_rootDecisionCache->count()), @(_nonRootDecisionCache->count()) ];
}
- (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID {
auto result = ACTION_UNSET;
uint64_t decision_time = 0;
uint64_t cache_val = [self cacheForVnodeID:vnodeID]->get(vnodeID);
if (cache_val == 0) return result;
// Decision is stored in upper 8 bits, timestamp in remaining 56.
result = (santa_action_t)(cache_val >> 56);
decision_time = (cache_val & ~(0xFF00000000000000));
if (RESPONSE_VALID(result)) {
if (result == ACTION_RESPOND_DENY) {
auto expiry_time = decision_time + (500 * 100000); // kMaxCacheDenyTimeMilliseconds
if (expiry_time < GetCurrentUptime()) {
[self cacheForVnodeID:vnodeID]->remove(vnodeID);
return ACTION_UNSET;
}
}
}
return result;
}
- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeID {
[self cacheForVnodeID:vnodeID]->remove(vnodeID);
// TODO(rah): Look at a replacement for wakeup(), maybe NSCondition
return 0;
}
- (SantaCache<santa_vnode_id_t, uint64_t> *)cacheForVnodeID:(santa_vnode_id_t)vnodeID {
return (vnodeID.fsid == _rootVnodeID || _rootVnodeID == 0) ? _rootDecisionCache : _nonRootDecisionCache;
}
@end

View File

@@ -1,291 +0,0 @@
/// Copyright 2021 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <DiskArbitration/DiskArbitration.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#import <bsm/libbsm.h>
#include <sys/mount.h>
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTDeviceEvent.h"
#import "Source/santad/EventProviders/SNTDeviceManager.h"
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h"
@interface SNTDeviceManagerTest : XCTestCase
@property id mockConfigurator;
@end
@implementation SNTDeviceManagerTest
- (void)setUp {
[super setUp];
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
OCMStub([self.mockConfigurator eventLogType]).andReturn(-1);
fclose(stdout);
}
- (ESResponse *)triggerTestMountEvent:(SNTDeviceManager *)deviceManager
mockES:(MockEndpointSecurity *)mockES
mockDA:(MockDiskArbitration *)mockDA
eventType:(es_event_type_t)eventType
diskInfoOverrides:(NSDictionary *)diskInfo {
if (!deviceManager.subscribed) {
// [deviceManager listen] is synchronous, but we want to asynchronously dispatch it
// with an enforced timeout to ensure that we never run into issues where the client
// never instantiates.
XCTestExpectation *initExpectation =
[self expectationWithDescription:@"Wait for SNTDeviceManager to instantiate"];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
[deviceManager listen];
});
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
while (!deviceManager.subscribed)
;
[initExpectation fulfill];
});
[self waitForExpectations:@[ initExpectation ] timeout:60.0];
}
struct statfs *fs = static_cast<struct statfs *>(calloc(1, sizeof(struct statfs)));
NSString *test_mntfromname = @"/dev/disk2s1";
NSString *test_mntonname = @"/Volumes/KATE'S 4G";
const char *c_mntfromname = [test_mntfromname UTF8String];
const char *c_mntonname = [test_mntonname UTF8String];
strncpy(fs->f_mntfromname, c_mntfromname, MAXPATHLEN);
strncpy(fs->f_mntonname, c_mntonname, MAXPATHLEN);
MockDADisk *disk = [[MockDADisk alloc] init];
disk.diskDescription = @{
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey : @"USB",
(__bridge NSString *)kDADiskDescriptionMediaRemovableKey : @YES,
@"DAVolumeMountable" : @YES,
@"DAVolumePath" : test_mntonname,
@"DADeviceModel" : @"Some device model",
@"DADevicePath" : test_mntonname,
@"DADeviceVendor" : @"Some vendor",
@"DAAppearanceTime" : @0,
@"DAMediaBSDName" : test_mntfromname,
};
if (diskInfo != nil) {
NSMutableDictionary *mergedDiskDescription = [disk.diskDescription mutableCopy];
for (NSString *key in diskInfo) {
mergedDiskDescription[key] = diskInfo[key];
}
disk.diskDescription = (NSDictionary *)mergedDiskDescription;
}
[mockDA insert:disk bsdName:test_mntfromname];
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
m.binaryPath = @"/System/Library/Filesystems/msdos.fs/Contents/Resources/mount_msdos";
m.message->action_type = ES_ACTION_TYPE_AUTH;
m.message->event_type = eventType;
if (eventType == ES_EVENT_TYPE_AUTH_MOUNT) {
m.message->event = (es_events_t){.mount = {.statfs = fs}};
} else {
m.message->event = (es_events_t){.remount = {.statfs = fs}};
}
}];
XCTestExpectation *mountExpectation =
[self expectationWithDescription:@"Wait for response from ES"];
__block ESResponse *got;
[mockES registerResponseCallback:eventType
withCallback:^(ESResponse *r) {
got = r;
[mountExpectation fulfill];
}];
[mockES triggerHandler:m.message];
[self waitForExpectations:@[ mountExpectation ] timeout:60.0];
free(fs);
return got;
}
- (void)testUSBBlockDisabled {
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
[mockES reset];
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
[mockDA reset];
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
deviceManager.blockUSBMount = NO;
ESResponse *got = [self triggerTestMountEvent:deviceManager
mockES:mockES
mockDA:mockDA
eventType:ES_EVENT_TYPE_AUTH_MOUNT
diskInfoOverrides:nil];
XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW);
}
- (void)testRemount {
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
[mockES reset];
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
[mockDA reset];
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
deviceManager.blockUSBMount = YES;
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
XCTestExpectation *expectation =
[self expectationWithDescription:@"Wait for SNTDeviceManager's blockCallback to trigger"];
__block NSString *gotmntonname, *gotmntfromname;
__block NSArray<NSString *> *gotRemountedArgs;
deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
gotRemountedArgs = event.remountArgs;
gotmntonname = event.mntonname;
gotmntfromname = event.mntfromname;
[expectation fulfill];
};
ESResponse *got = [self triggerTestMountEvent:deviceManager
mockES:mockES
mockDA:mockDA
eventType:ES_EVENT_TYPE_AUTH_MOUNT
diskInfoOverrides:nil];
XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);
XCTAssertEqual(mockDA.wasRemounted, YES);
[self waitForExpectations:@[ expectation ] timeout:60.0];
XCTAssertEqualObjects(gotRemountedArgs, deviceManager.remountArgs);
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
}
- (void)testBlockNoRemount {
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
[mockES reset];
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
[mockDA reset];
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
deviceManager.blockUSBMount = YES;
XCTestExpectation *expectation =
[self expectationWithDescription:@"Wait for SNTDeviceManager's blockCallback to trigger"];
__block NSString *gotmntonname, *gotmntfromname;
__block NSArray<NSString *> *gotRemountedArgs;
deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
gotRemountedArgs = event.remountArgs;
gotmntonname = event.mntonname;
gotmntfromname = event.mntfromname;
[expectation fulfill];
};
ESResponse *got = [self triggerTestMountEvent:deviceManager
mockES:mockES
mockDA:mockDA
eventType:ES_EVENT_TYPE_AUTH_MOUNT
diskInfoOverrides:nil];
XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);
[self waitForExpectations:@[ expectation ] timeout:60.0];
XCTAssertNil(gotRemountedArgs);
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
}
- (void)testEnsureRemountsCannotChangePerms {
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
[mockES reset];
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
[mockDA reset];
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
deviceManager.blockUSBMount = YES;
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
XCTestExpectation *expectation =
[self expectationWithDescription:@"Wait for SNTDeviceManager's blockCallback to trigger"];
__block NSString *gotmntonname, *gotmntfromname;
__block NSArray<NSString *> *gotRemountedArgs;
deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
gotRemountedArgs = event.remountArgs;
gotmntonname = event.mntonname;
gotmntfromname = event.mntfromname;
[expectation fulfill];
};
ESResponse *got = [self triggerTestMountEvent:deviceManager
mockES:mockES
mockDA:mockDA
eventType:ES_EVENT_TYPE_AUTH_REMOUNT
diskInfoOverrides:nil];
XCTAssertEqual(got.result, ES_AUTH_RESULT_DENY);
XCTAssertEqual(mockDA.wasRemounted, YES);
[self waitForExpectations:@[ expectation ] timeout:10.0];
XCTAssertEqualObjects(gotRemountedArgs, deviceManager.remountArgs);
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
}
- (void)testEnsureDMGsDoNotPrompt {
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
[mockES reset];
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
[mockDA reset];
SNTDeviceManager *deviceManager = [[SNTDeviceManager alloc] init];
deviceManager.blockUSBMount = YES;
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
deviceManager.deviceBlockCallback = ^(SNTDeviceEvent *event) {
XCTFail(@"Should not be called");
};
NSDictionary *diskInfo = @{
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey: @"Virtual Interface",
(__bridge NSString *)kDADiskDescriptionDeviceModelKey: @"Disk Image",
(__bridge NSString *)kDADiskDescriptionMediaNameKey: @"disk image",
};
ESResponse *got = [self triggerTestMountEvent:deviceManager
mockES:mockES
mockDA:mockDA
eventType:ES_EVENT_TYPE_AUTH_MOUNT
diskInfoOverrides:diskInfo];
XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW);
XCTAssertEqual(mockDA.wasRemounted, NO);
}
@end

View File

@@ -0,0 +1,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

View 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

View File

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

View File

@@ -1,4 +1,4 @@
/// Copyright 2021 Google Inc. All rights reserved.
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -12,17 +12,9 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <Foundation/Foundation.h>
#import "Source/common/Santa.pbobjc.h"
NS_ASSUME_NONNULL_BEGIN
@protocol SNTLogOutput<NSObject>
- (void)logEvent:(SNTPBSantaMessage *)event;
- (void)flush;
#include "Source/santad/EventProviders/SNTEndpointSecurityClientBase.h"
/// This should be treated as an Abstract Base Class and not directly instantiated
@interface SNTEndpointSecurityClient : NSObject <SNTEndpointSecurityClientBase>
- (instancetype)init NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,264 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
#include <EndpointSecurity/EndpointSecurity.h>
#include <bsm/libbsm.h>
#include <dispatch/dispatch.h>
#include <mach/mach_time.h>
#include <stdlib.h>
#include <sys/qos.h>
#import "Source/common/SNTCommon.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTLogging.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/Metrics.h"
using santa::santad::EventDisposition;
using santa::santad::Metrics;
using santa::santad::Processor;
using santa::santad::event_providers::endpoint_security::Client;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
using santa::santad::event_providers::endpoint_security::Message;
@interface SNTEndpointSecurityClient ()
@property int64_t deadlineMarginMS;
@end
@implementation SNTEndpointSecurityClient {
std::shared_ptr<EndpointSecurityAPI> _esApi;
std::shared_ptr<Metrics> _metrics;
Client _esClient;
mach_timebase_info_data_t _timebase;
dispatch_queue_t _authQueue;
dispatch_queue_t _notifyQueue;
Processor _processor;
}
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<Metrics>)metrics
processor:(Processor)processor {
self = [super init];
if (self) {
_esApi = std::move(esApi);
_metrics = std::move(metrics);
_deadlineMarginMS = 5000;
_processor = processor;
if (mach_timebase_info(&_timebase) != KERN_SUCCESS) {
LOGE(@"Failed to get mach timebase info");
// Assumed to be transitory failure. Let the daemon restart.
exit(EXIT_FAILURE);
}
_authQueue = dispatch_queue_create(
"com.google.santa.daemon.auth_queue",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
QOS_CLASS_USER_INTERACTIVE, 0));
_notifyQueue = dispatch_queue_create(
"com.google.santa.daemon.notify_queue",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_CONCURRENT_WITH_AUTORELEASE_POOL,
QOS_CLASS_BACKGROUND, 0));
}
return self;
}
- (NSString *)errorMessageForNewClientResult:(es_new_client_result_t)result {
switch (result) {
case ES_NEW_CLIENT_RESULT_SUCCESS: return nil;
case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED: return @"Full-disk access not granted";
case ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED: return @"Not entitled";
case ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED: return @"Not running as root";
case ES_NEW_CLIENT_RESULT_ERR_INVALID_ARGUMENT: return @"Invalid argument";
case ES_NEW_CLIENT_RESULT_ERR_INTERNAL: return @"Internal error";
case ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS: return @"Too many simultaneous clients";
default: return @"Unknown error";
}
}
- (void)handleMessage:(Message &&)esMsg
recordEventMetrics:(void (^)(EventDisposition disposition))recordEventMetrics {
// This method should only be used by classes derived
// from SNTEndpointSecurityClient.
[self doesNotRecognizeSelector:_cmd];
}
- (BOOL)shouldHandleMessage:(const Message &)esMsg
ignoringOtherESClients:(BOOL)ignoringOtherESClients {
if (esMsg->process->is_es_client && ignoringOtherESClients) {
if (esMsg->action_type == ES_ACTION_TYPE_AUTH) {
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:true];
}
return NO;
}
return YES;
}
- (void)establishClientOrDie {
if (self->_esClient.IsConnected()) {
// This is a programming error
LOGE(@"Client already established. Aborting.");
[NSException raise:@"Client already established" format:@"IsConnected already true"];
}
self->_esClient = self->_esApi->NewClient(^(es_client_t *c, Message esMsg) {
int64_t processingStart = clock_gettime_nsec_np(CLOCK_MONOTONIC);
es_event_type_t eventType = esMsg->event_type;
if ([self shouldHandleMessage:esMsg
ignoringOtherESClients:[[SNTConfigurator configurator]
ignoreOtherEndpointSecurityClients]]) {
[self handleMessage:std::move(esMsg)
recordEventMetrics:^(EventDisposition disposition) {
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
self->_metrics->SetEventMetrics(self->_processor, eventType, disposition,
processingEnd - processingStart);
}];
} else {
int64_t processingEnd = clock_gettime_nsec_np(CLOCK_MONOTONIC);
self->_metrics->SetEventMetrics(self->_processor, eventType, EventDisposition::kDropped,
processingEnd - processingStart);
}
});
if (!self->_esClient.IsConnected()) {
NSString *errMsg = [self errorMessageForNewClientResult:_esClient.NewClientResult()];
LOGE(@"Unable to create EndpointSecurity client: %@", errMsg);
[NSException raise:@"Failed to create ES client" format:@"%@", errMsg];
} else {
LOGI(@"Connected to EndpointSecurity (%@)", self);
}
if (![self muteSelf]) {
[NSException raise:@"ES Mute Failure" format:@"Failed to mute self"];
}
}
+ (bool)populateAuditTokenSelf:(audit_token_t *)tok {
mach_msg_type_number_t count = TASK_AUDIT_TOKEN_COUNT;
if (task_info(mach_task_self(), TASK_AUDIT_TOKEN, (task_info_t)tok, &count) != KERN_SUCCESS) {
LOGE(@"Failed to fetch this client's audit token.");
return false;
}
return true;
}
- (bool)muteSelf {
audit_token_t myAuditToken;
if (![SNTEndpointSecurityClient populateAuditTokenSelf:&myAuditToken]) {
return false;
}
if (!self->_esApi->MuteProcess(self->_esClient, &myAuditToken)) {
LOGE(@"Failed to mute this client's process.");
return false;
}
return true;
}
- (bool)clearCache {
return _esApi->ClearCache(self->_esClient);
}
- (bool)subscribe:(const std::set<es_event_type_t> &)events {
return _esApi->Subscribe(_esClient, events);
}
- (bool)subscribeAndClearCache:(const std::set<es_event_type_t> &)events {
return [self subscribe:events] && [self clearCache];
}
- (bool)respondToMessage:(const Message &)msg
withAuthResult:(es_auth_result_t)result
cacheable:(bool)cacheable {
return _esApi->RespondAuthResult(_esClient, msg, result, cacheable);
}
- (void)processEnrichedMessage:(std::shared_ptr<EnrichedMessage>)msg
handler:(void (^)(std::shared_ptr<EnrichedMessage>))messageHandler {
dispatch_async(_notifyQueue, ^{
messageHandler(std::move(msg));
});
}
- (void)processMessage:(Message &&)msg handler:(void (^)(const Message &))messageHandler {
if (unlikely(msg->action_type != ES_ACTION_TYPE_AUTH)) {
// This is a programming error
LOGE(@"Attempting to process non-AUTH message");
[NSException raise:@"Attempt to process non-auth message"
format:@"Unexpected event type received: %d", msg->event_type];
}
dispatch_semaphore_t processingSema = dispatch_semaphore_create(0);
// Add 1 to the processing semaphore. We're not creating it with a starting
// value of 1 because that requires that the semaphore is not deallocated
// until its value matches the starting value, which we don't need.
dispatch_semaphore_signal(processingSema);
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
const uint64_t timeout = NSEC_PER_MSEC * (self.deadlineMarginMS);
uint64_t deadlineMachTime = msg->deadline - mach_absolute_time();
uint64_t deadlineNano = deadlineMachTime * _timebase.numer / _timebase.denom;
// TODO(mlw): How should we handle `deadlineNano <= timeout`. Will currently
// result in the deadline block being dispatched immediately (and therefore
// the event will be denied).
// Workaround for compiler bug that doesn't properly close over variables
// Note: On macOS 10.15 this will cause extra message copies.
__block Message processMsg = msg;
__block Message deadlineMsg = msg;
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, deadlineNano - timeout), self->_authQueue, ^(void) {
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
// Handler has already responded, nothing to do.
return;
}
bool res = [self respondToMessage:deadlineMsg
withAuthResult:ES_AUTH_RESULT_DENY
cacheable:false];
LOGE(@"SNTEndpointSecurityClient: deadline reached: deny pid=%d, event type: %d ret=%d",
audit_token_to_pid(deadlineMsg->process->audit_token), deadlineMsg->event_type, res);
dispatch_semaphore_signal(deadlineExpiredSema);
});
dispatch_async(self->_authQueue, ^{
messageHandler(deadlineMsg);
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
// Deadline expired, wait for deadline block to finish.
dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER);
}
});
}
+ (bool)isDatabasePath:(const std::string_view)path {
// TODO(mlw): These values should come from `SNTDatabaseController`. But right
// now they live as NSStrings. We should make them `std::string_view` types
// in order to use them here efficiently, but will need to make the
// `SNTDatabaseController` an ObjC++ file.
return (path == "/private/var/db/santa/rules.db" || path == "/private/var/db/santa/events.db");
}
@end

View File

@@ -0,0 +1,77 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <EndpointSecurity/EndpointSecurity.h>
#include <bsm/libbsm.h>
#include <memory>
#include <string>
#import <Foundation/Foundation.h>
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/Metrics.h"
@protocol SNTEndpointSecurityClientBase
- (instancetype)
initWithESAPI:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
processor:(santa::santad::Processor)processor;
/// @note If this fails to establish a new ES client via `es_new_client`, an exception is raised
/// that should terminate the program.
- (void)establishClientOrDie;
- (bool)subscribe:(const std::set<es_event_type_t> &)events;
/// Clears the ES cache after setting subscriptions.
/// There's a gap between creating a client and subscribing to events. Creating
/// the client triggers a cache flush automatically but any events that happen
/// prior to subscribing could've been cached by another client. Clearing after
/// subscribing mitigates this posibility.
- (bool)subscribeAndClearCache:(const std::set<es_event_type_t> &)events;
/// Responds to the Message with the given auth result
///
/// @param Message The wrapped es_message_t being responded to
/// @param result Either ES_AUTH_RESULT_ALLOW or ES_AUTH_RESULT_DENY
/// @param cacheable true if ES should attempt to cache the result, otherwise false
/// @return true if the response was successful, otherwise false
- (bool)respondToMessage:(const santa::santad::event_providers::endpoint_security::Message &)msg
withAuthResult:(es_auth_result_t)result
cacheable:(bool)cacheable;
- (void)
processEnrichedMessage:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage>)msg
handler:
(void (^)(std::shared_ptr<
santa::santad::event_providers::endpoint_security::EnrichedMessage>))
messageHandler;
- (void)processMessage:(santa::santad::event_providers::endpoint_security::Message &&)msg
handler:
(void (^)(const santa::santad::event_providers::endpoint_security::Message &))
messageHandler;
- (bool)clearCache;
+ (bool)isDatabasePath:(const std::string_view)path;
+ (bool)populateAuditTokenSelf:(audit_token_t *)tok;
@end

View File

@@ -0,0 +1,437 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <EndpointSecurity/EndpointSecurity.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <bsm/libbsm.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <mach/mach_time.h>
#include <memory>
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
#include "Source/santad/Metrics.h"
using santa::santad::Processor;
using santa::santad::event_providers::endpoint_security::Client;
using santa::santad::event_providers::endpoint_security::EnrichedClose;
using santa::santad::event_providers::endpoint_security::EnrichedFile;
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
using santa::santad::event_providers::endpoint_security::EnrichedProcess;
using santa::santad::event_providers::endpoint_security::Message;
@interface SNTEndpointSecurityClient (Testing)
- (void)establishClientOrDie;
- (bool)muteSelf;
- (NSString *)errorMessageForNewClientResult:(es_new_client_result_t)result;
- (void)handleMessage:(Message &&)esMsg
recordEventMetrics:(void (^)(santa::santad::EventDisposition disposition))recordEventMetrics;
- (BOOL)shouldHandleMessage:(const Message &)esMsg
ignoringOtherESClients:(BOOL)ignoringOtherESClients;
@property int64_t deadlineMarginMS;
@end
@interface SNTEndpointSecurityClientTest : XCTestCase
@end
@implementation SNTEndpointSecurityClientTest
- (void)testEstablishClientOrDie {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
EXPECT_CALL(*mockESApi, MuteProcess).WillOnce(testing::Return(true));
EXPECT_CALL(*mockESApi, NewClient)
.WillOnce(testing::Return(Client()))
.WillOnce(testing::Return(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS)));
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
// First time throws because mock triggers failed connection
// Second time succeeds
XCTAssertThrows([client establishClientOrDie]);
XCTAssertNoThrow([client establishClientOrDie]);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testErrorMessageForNewClientResult {
std::map<es_new_client_result_t, std::string> resultMessagePairs{
{ES_NEW_CLIENT_RESULT_SUCCESS, ""},
{ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED, "Full-disk access not granted"},
{ES_NEW_CLIENT_RESULT_ERR_NOT_ENTITLED, "Not entitled"},
{ES_NEW_CLIENT_RESULT_ERR_NOT_PRIVILEGED, "Not running as root"},
{ES_NEW_CLIENT_RESULT_ERR_INVALID_ARGUMENT, "Invalid argument"},
{ES_NEW_CLIENT_RESULT_ERR_INTERNAL, "Internal error"},
{ES_NEW_CLIENT_RESULT_ERR_TOO_MANY_CLIENTS, "Too many simultaneous clients"},
{(es_new_client_result_t)123, "Unknown error"},
};
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:nullptr
metrics:nullptr
processor:Processor::kUnknown];
for (const auto &kv : resultMessagePairs) {
NSString *message = [client errorMessageForNewClientResult:kv.first];
XCTAssertEqual(0, strcmp([(message ?: @"") UTF8String], kv.second.c_str()));
}
}
- (void)testHandleMessage {
es_message_t esMsg;
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
{ XCTAssertThrows([client handleMessage:Message(mockESApi, &esMsg) recordEventMetrics:nil]); }
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testHandleMessageWithClient {
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_FORK, &proc);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
// Have subscribe fail the first time, meaning clear cache only called once.
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_ALLOW, true))
.WillOnce(testing::Return(true));
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
{
Message msg(mockESApi, &esMsg);
// Is ES client, but don't ignore others == Should Handle
esMsg.process->is_es_client = true;
XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:NO]);
// Not ES client, but ignore others == Should Handle
esMsg.process->is_es_client = false;
XCTAssertTrue([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
// Is ES client, don't ignore others, and non-AUTH == Don't Handle
esMsg.process->is_es_client = true;
XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
// Is ES client, don't ignore others, and AUTH == Respond and Don't Handle
esMsg.process->is_es_client = true;
esMsg.action_type = ES_ACTION_TYPE_AUTH;
XCTAssertFalse([client shouldHandleMessage:msg ignoringOtherESClients:YES]);
}
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testPopulateAuditTokenSelf {
audit_token_t myAuditToken;
[SNTEndpointSecurityClient populateAuditTokenSelf:&myAuditToken];
XCTAssertEqual(audit_token_to_pid(myAuditToken), getpid());
XCTAssertNotEqual(audit_token_to_pidversion(myAuditToken), 0);
}
- (void)testMuteSelf {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
EXPECT_CALL(*mockESApi, MuteProcess)
.WillOnce(testing::Return(true))
.WillOnce(testing::Return(false));
XCTAssertTrue([client muteSelf]);
XCTAssertFalse([client muteSelf]);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testClearCache {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
// Test the underlying clear cache impl returning both true and false
EXPECT_CALL(*mockESApi, ClearCache)
.WillOnce(testing::Return(true))
.WillOnce(testing::Return(false));
XCTAssertTrue([client clearCache]);
XCTAssertFalse([client clearCache]);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testSubscribe {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
std::set<es_event_type_t> events = {
ES_EVENT_TYPE_NOTIFY_CLOSE,
ES_EVENT_TYPE_NOTIFY_EXIT,
};
// Test the underlying subscribe impl returning both true and false
EXPECT_CALL(*mockESApi, Subscribe(testing::_, events))
.WillOnce(testing::Return(true))
.WillOnce(testing::Return(false));
XCTAssertTrue([client subscribe:events]);
XCTAssertFalse([client subscribe:events]);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testSubscribeAndClearCache {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
// Have subscribe fail the first time, meaning clear cache only called once.
EXPECT_CALL(*mockESApi, ClearCache)
.After(EXPECT_CALL(*mockESApi, Subscribe)
.WillOnce(testing::Return(false))
.WillOnce(testing::Return(true)))
.WillOnce(testing::Return(true));
XCTAssertFalse([client subscribeAndClearCache:{}]);
XCTAssertTrue([client subscribeAndClearCache:{}]);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testRespondToMessageWithAuthResultCacheable {
es_message_t esMsg;
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
es_auth_result_t result = ES_AUTH_RESULT_DENY;
bool cacheable = true;
// Have subscribe fail the first time, meaning clear cache only called once.
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, result, cacheable))
.WillOnce(testing::Return(true));
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
{
Message msg(mockESApi, &esMsg);
XCTAssertTrue([client respondToMessage:msg withAuthResult:result cacheable:cacheable]);
}
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testProcessEnrichedMessageHandler {
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
// Note: In this test, `RetainMessage` isn't setup to return anything. This
// means that the underlying `es_msg_` in the `Message` object is NULL, and
// therefore no call to `ReleaseMessage` is ever made (hence no expectations).
// Because we don't need to operate on the es_msg_, this simplifies the test.
EXPECT_CALL(*mockESApi, RetainMessage);
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
es_message_t esMsg;
auto enrichedMsg = std::make_shared<EnrichedMessage>(
EnrichedClose(Message(mockESApi, &esMsg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
[client processEnrichedMessage:enrichedMsg
handler:^(std::shared_ptr<EnrichedMessage> msg) {
dispatch_semaphore_signal(sema);
}];
XCTAssertEqual(0,
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
"Handler block not called within expected time window");
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testIsDatabasePath {
XCTAssertTrue([SNTEndpointSecurityClient isDatabasePath:"/private/var/db/santa/rules.db"]);
XCTAssertTrue([SNTEndpointSecurityClient isDatabasePath:"/private/var/db/santa/events.db"]);
XCTAssertFalse([SNTEndpointSecurityClient isDatabasePath:"/not/a/db/path"]);
}
- (void)testProcessMessageHandlerBadEventType {
es_file_t proc_file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&proc_file);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
{
XCTAssertThrows([client processMessage:Message(mockESApi, &esMsg)
handler:^(const Message &msg){
}]);
}
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
// Note: This test triggers a leak warning on the mock object, however it is
// benign. The dispatch block to handle deadline expiration in
// `processMessage:handler:` will retain the mock object an extra time.
// But since this test sets a long deadline in order to ensure the handler block
// runs first, the deadline handler block will not have finished executing by
// the time the test exits, making GMock think the object was leaked.
- (void)testProcessMessageHandler {
es_file_t proc_file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&proc_file);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &proc, ActionType::Auth,
45 * 1000); // Long deadline to not hit
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
{
XCTAssertNoThrow([client processMessage:Message(mockESApi, &esMsg)
handler:^(const Message &msg) {
dispatch_semaphore_signal(sema);
}]);
}
XCTAssertEqual(0,
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
"Handler block not called within expected time window");
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testProcessMessageHandlerWithDeadlineTimeout {
// Set a es_message_t deadline of 750ms
// Set a deadline leeway in the `SNTEndpointSecurityClient` of 500ms
// Mock `RespondAuthResult` which is called from the deadline handler
// Signal the semaphore from the mock
// Wait a few seconds for the semaphore (should take ~250ms)
//
// Two semaphotes are used:
// 1. deadlineSema - used to wait in the handler block until the deadline
// block has a chance to execute
// 2. controlSema - used to block control flow in the test until the
// deadlineSema is signaled (or a timeout waiting on deadlineSema)
es_file_t proc_file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&proc_file);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_OPEN, &proc, ActionType::Auth,
750); // 750ms timeout
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
dispatch_semaphore_t deadlineSema = dispatch_semaphore_create(0);
dispatch_semaphore_t controlSema = dispatch_semaphore_create(0);
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_DENY, false))
.WillOnce(testing::InvokeWithoutArgs(^() {
// Signal deadlineSema to let the handler block continue execution
dispatch_semaphore_signal(deadlineSema);
return true;
}));
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
client.deadlineMarginMS = 500;
{
__block long result;
XCTAssertNoThrow([client processMessage:Message(mockESApi, &esMsg)
handler:^(const Message &msg) {
result = dispatch_semaphore_wait(
deadlineSema,
dispatch_time(DISPATCH_TIME_NOW, 4 * NSEC_PER_SEC));
// Once done waiting on deadlineSema, trigger controlSema to
// continue test
dispatch_semaphore_signal(controlSema);
}]);
XCTAssertEqual(
0, dispatch_semaphore_wait(controlSema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
"Control sema not signaled within expected time window");
XCTAssertEqual(result, 0);
}
// Allow some time for the threads in `processMessage:handler:` to finish.
// It isn't critical that they do, but if the dispatch blocks don't complete
// we may get warnings from GMock about calls to ReleaseMessage after
// verifying and clearing. Sleep a little bit here to reduce chances of
// seeing the warning (but still possible)
SleepMS(100);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
@end

View File

@@ -1,4 +1,4 @@
/// Copyright 2021 Google Inc. All rights reserved.
/// Copyright 2021-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -11,12 +11,17 @@
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <DiskArbitration/DiskArbitration.h>
#include <DiskArbitration/DiskArbitration.h>
#import <Foundation/Foundation.h>
#include <EndpointSecurity/EndpointSecurity.h>
#include "Source/common/SNTDeviceEvent.h"
#import "Source/common/SNTDeviceEvent.h"
#import "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#include "Source/santad/Metrics.h"
NS_ASSUME_NONNULL_BEGIN
@@ -26,16 +31,19 @@ typedef void (^SNTDeviceBlockCallback)(SNTDeviceEvent *event);
* Manages DiskArbitration and EndpointSecurity to monitor/block/remount USB
* storage devices.
*/
@interface SNTDeviceManager : NSObject
@interface SNTEndpointSecurityDeviceManager
: SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
@property(nonatomic, readwrite) BOOL subscribed;
@property(nonatomic, readwrite) BOOL blockUSBMount;
@property(nonatomic, readwrite, nullable) NSArray<NSString *> *remountArgs;
@property(nonatomic, nullable) SNTDeviceBlockCallback deviceBlockCallback;
- (instancetype)init;
- (void)listen;
- (BOOL)subscribed;
- (instancetype)
initWithESAPI:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
authResultCache:(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache;
@end

View File

@@ -1,4 +1,4 @@
/// Copyright 2021 Google Inc. All rights reserved.
/// Copyright 2021-2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
@@ -11,21 +11,41 @@
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santad/EventProviders/SNTDeviceManager.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h"
#import <DiskArbitration/DiskArbitration.h>
#include <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#include <atomic>
#include <memory>
#include <bsm/libbsm.h>
#include <errno.h>
#include <libproc.h>
#include <sys/mount.h>
#include <atomic>
#include <memory>
#import "Source/common/SNTDeviceEvent.h"
#import "Source/common/SNTLogging.h"
#import "Source/santad/Logs/SNTEventLog.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/Metrics.h"
using santa::santad::EventDisposition;
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::FlushCacheMode;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::Logger;
@interface SNTEndpointSecurityDeviceManager ()
- (void)logDiskAppeared:(NSDictionary *)props;
- (void)logDiskDisappeared:(NSDictionary *)props;
@property DASessionRef diskArbSession;
@property(nonatomic, readonly) dispatch_queue_t diskQueue;
@end
void diskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
if (dissenter) {
@@ -36,17 +56,18 @@ void diskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context
IOReturn subSystemCode = err_get_sub(status);
IOReturn errorCode = err_get_code(status);
LOGE(
@"SNTDeviceManager: dissenter status codes: system: %d, subsystem: %d, err: %d; status: %s",
systemCode, subSystemCode, errorCode, [statusString UTF8String]);
LOGE(@"SNTEndpointSecurityDeviceManager: dissenter status codes: system: %d, subsystem: %d, "
@"err: %d; status: %s",
systemCode, subSystemCode, errorCode, [statusString UTF8String]);
}
}
void diskAppearedCallback(DADiskRef disk, void *context) {
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
if (![props[@"DAVolumeMountable"] boolValue]) return;
SNTEventLog *logger = [SNTEventLog logger];
if (logger) [logger logDiskAppeared:props];
SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context;
[dm logDiskAppeared:props];
}
void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *context) {
@@ -54,8 +75,9 @@ void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *conte
if (![props[@"DAVolumeMountable"] boolValue]) return;
if (props[@"DAVolumePath"]) {
SNTEventLog *logger = [SNTEventLog logger];
if (logger) [logger logDiskAppeared:props];
SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context;
[dm logDiskAppeared:props];
}
}
@@ -63,8 +85,9 @@ void diskDisappearedCallback(DADiskRef disk, void *context) {
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
if (![props[@"DAVolumeMountable"] boolValue]) return;
SNTEventLog *logger = [SNTEventLog logger];
if (logger) [logger logDiskDisappeared:props];
SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context;
[dm logDiskDisappeared:props];
}
NSArray<NSString *> *maskToMountArgs(long remountOpts) {
@@ -101,126 +124,108 @@ long mountArgsToMask(NSArray<NSString *> *args) {
else if ([arg isEqualToString:@"async"])
flags |= MNT_ASYNC;
else
LOGE(@"SNTDeviceManager: unexpected mount arg: %@", arg);
LOGE(@"SNTEndpointSecurityDeviceManager: unexpected mount arg: %@", arg);
}
return flags;
}
NS_ASSUME_NONNULL_BEGIN
@interface SNTDeviceManager ()
@implementation SNTEndpointSecurityDeviceManager {
std::shared_ptr<AuthResultCache> _authResultCache;
std::shared_ptr<Logger> _logger;
}
@property DASessionRef diskArbSession;
@property(nonatomic, readonly) es_client_t *client;
@property(nonatomic, readonly) dispatch_queue_t esAuthQueue;
@property(nonatomic, readonly) dispatch_queue_t diskQueue;
@end
@implementation SNTDeviceManager
- (instancetype)init API_AVAILABLE(macos(10.15)) {
self = [super init];
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
logger:(std::shared_ptr<Logger>)logger
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache {
self = [super initWithESAPI:std::move(esApi)
metrics:std::move(metrics)
processor:santa::santad::Processor::kDeviceManager];
if (self) {
_logger = logger;
_authResultCache = authResultCache;
_blockUSBMount = false;
_diskQueue = dispatch_queue_create("com.google.santad.disk_queue", DISPATCH_QUEUE_SERIAL);
_esAuthQueue =
dispatch_queue_create("com.google.santa.daemon.es_device_auth", DISPATCH_QUEUE_CONCURRENT);
_diskQueue = dispatch_queue_create("com.google.santa.daemon.disk_queue", DISPATCH_QUEUE_SERIAL);
_diskArbSession = DASessionCreate(NULL);
DASessionSetDispatchQueue(_diskArbSession, _diskQueue);
if (@available(macos 10.15, *)) [self initES];
[self establishClientOrDie];
}
return self;
}
- (void)initES API_AVAILABLE(macos(10.15)) {
while (!self.client) {
es_client_t *client = NULL;
es_new_client_result_t ret = es_new_client(&client, ^(es_client_t *c, const es_message_t *m) {
// Set timeout to 5 seconds before the ES deadline.
[self handleESMessageWithTimeout:m
withClient:c
timeout:dispatch_time(m->deadline, NSEC_PER_SEC * -5)];
});
- (void)logDiskAppeared:(NSDictionary *)props {
self->_logger->LogDiskAppeared(props);
}
switch (ret) {
case ES_NEW_CLIENT_RESULT_SUCCESS:
LOGI(@"Connected to EndpointSecurity");
_client = client;
return;
case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED:
LOGE(@"Unable to create EndpointSecurity client, not full-disk access permitted");
LOGE(@"Sleeping for 30s before restarting.");
sleep(30);
exit(ret);
default:
LOGE(@"Unable to create es client: %d. Sleeping for a minute.", ret);
sleep(60);
continue;
}
- (void)logDiskDisappeared:(NSDictionary *)props {
self->_logger->LogDiskDisappeared(props);
}
- (NSString *)description {
return @"Device Manager";
}
- (void)handleMessage:(Message &&)esMsg
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
if (!self.blockUSBMount) {
// TODO: We should also unsubscribe from events when this isn't set, but
// this is generally a low-volume event type.
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:false];
recordEventMetrics(EventDisposition::kDropped);
return;
}
if (esMsg->event_type == ES_EVENT_TYPE_NOTIFY_UNMOUNT) {
self->_authResultCache->FlushCache(FlushCacheMode::kNonRootOnly);
recordEventMetrics(EventDisposition::kProcessed);
return;
}
[self processMessage:std::move(esMsg)
handler:^(const Message &msg) {
es_auth_result_t result = [self handleAuthMount:msg];
[self respondToMessage:msg withAuthResult:result cacheable:false];
recordEventMetrics(EventDisposition::kProcessed);
}];
}
- (void)listenES API_AVAILABLE(macos(10.15)) {
while (!self.client)
usleep(100000); // 100ms
es_event_type_t events[] = {
ES_EVENT_TYPE_AUTH_MOUNT,
ES_EVENT_TYPE_AUTH_REMOUNT,
};
es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t));
if (sret != ES_RETURN_SUCCESS)
LOGE(@"SNTDeviceManager: unable to subscribe to auth mount events: %d", sret);
}
- (void)listenDA {
- (void)enable {
DARegisterDiskAppearedCallback(_diskArbSession, NULL, diskAppearedCallback,
(__bridge void *)self);
DARegisterDiskDescriptionChangedCallback(_diskArbSession, NULL, NULL,
diskDescriptionChangedCallback, (__bridge void *)self);
DARegisterDiskDisappearedCallback(_diskArbSession, NULL, diskDisappearedCallback,
(__bridge void *)self);
[super subscribeAndClearCache:{
ES_EVENT_TYPE_AUTH_MOUNT,
ES_EVENT_TYPE_AUTH_REMOUNT,
ES_EVENT_TYPE_NOTIFY_UNMOUNT,
}];
}
- (void)listen {
[self listenDA];
if (@available(macos 10.15, *)) [self listenES];
self.subscribed = YES;
}
- (void)handleAuthMount:(const es_message_t *)m
withClient:(es_client_t *)c API_AVAILABLE(macos(10.15)) {
if (!self.blockUSBMount) {
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
return;
}
- (es_auth_result_t)handleAuthMount:(const Message &)m {
struct statfs *eventStatFS;
BOOL isRemount = NO;
switch (m->event_type) {
case ES_EVENT_TYPE_AUTH_MOUNT: eventStatFS = m->event.mount.statfs; break;
case ES_EVENT_TYPE_AUTH_REMOUNT:
eventStatFS = m->event.remount.statfs;
isRemount = YES;
break;
case ES_EVENT_TYPE_AUTH_REMOUNT: eventStatFS = m->event.remount.statfs; break;
default:
// This is a programming error
LOGE(@"Unexpected Event Type passed to DeviceManager handleAuthMount: %d", m->event_type);
// Fail closed.
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false);
assert(0 && "SNTDeviceManager: unexpected event type");
return;
exit(EXIT_FAILURE);
}
long mountMode = eventStatFS->f_flags;
pid_t pid = audit_token_to_pid(m->process->audit_token);
LOGD(@"SNTDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu",
m->process->executable->path.data, pid, mountMode);
LOGD(
@"SNTEndpointSecurityDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu",
m->process->executable->path.data, pid, mountMode);
DADiskRef disk = DADiskCreateFromBSDName(NULL, self.diskArbSession, eventStatFS->f_mntfromname);
CFAutorelease(disk);
@@ -232,12 +237,13 @@ NS_ASSUME_NONNULL_BEGIN
BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue];
NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey];
BOOL isUSB = [protocol isEqualToString:@"USB"];
BOOL isVirtual = [protocol isEqualToString: @"Virtual Interface"];
BOOL isVirtual = [protocol isEqualToString:@"Virtual Interface"];
NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey];
// TODO: check kind and protocol for banned things (e.g. MTP).
LOGD(@"SNTDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d isRemovable: %d "
LOGD(@"SNTEndpointSecurityDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d "
@"isRemovable: %d "
@"isEjectable: %d",
protocol, kind, isInternal, isRemovable, isEjectable);
@@ -245,8 +251,7 @@ NS_ASSUME_NONNULL_BEGIN
// also are okay with operations for devices that are non-removal as long as
// they are NOT a USB device.
if (isInternal || isVirtual || (!isRemovable && !isEjectable && !isUSB)) {
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
return;
return ES_AUTH_RESULT_ALLOW;
}
SNTDeviceEvent *event = [[SNTDeviceEvent alloc]
@@ -259,17 +264,16 @@ NS_ASSUME_NONNULL_BEGIN
event.remountArgs = self.remountArgs;
long remountOpts = mountArgsToMask(self.remountArgs);
LOGD(@"SNTDeviceManager: mountMode: %@", maskToMountArgs(mountMode));
LOGD(@"SNTDeviceManager: remountOpts: %@", maskToMountArgs(remountOpts));
LOGD(@"SNTEndpointSecurityDeviceManager: mountMode: %@", maskToMountArgs(mountMode));
LOGD(@"SNTEndpointSecurityDeviceManager: remountOpts: %@", maskToMountArgs(remountOpts));
if ((mountMode & remountOpts) == remountOpts && !isRemount) {
LOGD(@"SNTDeviceManager: Allowing as mount as flags match remountOpts");
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
return;
if ((mountMode & remountOpts) == remountOpts && m->event_type != ES_EVENT_TYPE_AUTH_REMOUNT) {
LOGD(@"SNTEndpointSecurityDeviceManager: Allowing as mount as flags match remountOpts");
return ES_AUTH_RESULT_ALLOW;
}
long newMode = mountMode | remountOpts;
LOGI(@"SNTDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)",
LOGI(@"SNTEndpointSecurityDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)",
eventStatFS->f_mntfromname, eventStatFS->f_mntonname, mountMode, newMode);
[self remount:disk mountMode:newMode];
}
@@ -278,7 +282,7 @@ NS_ASSUME_NONNULL_BEGIN
self.deviceBlockCallback(event);
}
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false);
return ES_AUTH_RESULT_DENY;
}
- (void)remount:(DADiskRef)disk mountMode:(long)remountMask {
@@ -293,66 +297,6 @@ NS_ASSUME_NONNULL_BEGIN
free(argv);
}
// handleESMessage handles an ES message synchronously. This will block all incoming ES events
// until either we serve a response or we hit the auth deadline. Prefer [SNTDeviceManager
// handleESMessageWithTimeout]
// TODO(tnek): generalize this timeout handling logic so that EndpointSecurityManager can use it
// too.
- (void)handleESMessageWithTimeout:(const es_message_t *)m
withClient:(es_client_t *)c
timeout:(dispatch_time_t)timeout API_AVAILABLE(macos(10.15)) {
// ES will kill our whole client if we don't meet the es_message auth deadline, so we try to
// gracefully handle it with a deny-by-default in the worst-case before it can do that.
// This isn't an issue for notify events, so we're in no rush for those.
es_message_t *mc = es_copy_message(m);
dispatch_semaphore_t processingSema = dispatch_semaphore_create(0);
// Add 1 to the processing semaphore. We're not creating it with a starting
// value of 1 because that requires that the semaphore is not deallocated
// until its value matches the starting value, which we don't need.
dispatch_semaphore_signal(processingSema);
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
if (mc->action_type == ES_ACTION_TYPE_AUTH) {
dispatch_after(timeout, self.esAuthQueue, ^(void) {
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
// Handler already responded, nothing to do.
return;
}
LOGE(@"SNTDeviceManager: deadline reached: deny pid=%d ret=%d",
audit_token_to_pid(mc->process->audit_token),
es_respond_auth_result(c, mc, ES_AUTH_RESULT_DENY, false));
dispatch_semaphore_signal(deadlineExpiredSema);
});
}
dispatch_async(self.esAuthQueue, ^{
[self handleESMessage:mc withClient:c];
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
// Deadline expired, wait for deadline block to finish.
dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER);
}
es_free_message(mc);
});
}
- (void)handleESMessage:(const es_message_t *)m
withClient:(es_client_t *)c API_AVAILABLE(macos(10.15)) {
switch (m->event_type) {
case ES_EVENT_TYPE_AUTH_REMOUNT: {
[[fallthrough]];
}
case ES_EVENT_TYPE_AUTH_MOUNT: {
[self handleAuthMount:m withClient:c];
break;
}
default:
LOGE(@"SNTDeviceManager: unexpected event type: %d", m->event_type);
break;
}
}
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,369 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <DiskArbitration/DiskArbitration.h>
#include <EndpointSecurity/EndpointSecurity.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#import <bsm/libbsm.h>
#import <dispatch/dispatch.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <sys/mount.h>
#include <cstddef>
#include <memory>
#include <set>
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTDeviceEvent.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/AuthResultCache.h"
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityDeviceManager.h"
#include "Source/santad/Metrics.h"
using santa::santad::EventDisposition;
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::FlushCacheMode;
using santa::santad::event_providers::endpoint_security::Message;
class MockAuthResultCache : public AuthResultCache {
public:
using AuthResultCache::AuthResultCache;
MOCK_METHOD(void, FlushCache, (FlushCacheMode mode));
};
@interface SNTEndpointSecurityDeviceManager (Testing)
- (void)logDiskAppeared:(NSDictionary *)props;
@end
@interface SNTEndpointSecurityDeviceManagerTest : XCTestCase
@property id mockConfigurator;
@property MockDiskArbitration *mockDA;
@end
@implementation SNTEndpointSecurityDeviceManagerTest
- (void)setUp {
[super setUp];
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
OCMStub([self.mockConfigurator eventLogType]).andReturn(-1);
self.mockDA = [MockDiskArbitration mockDiskArbitration];
[self.mockDA reset];
fclose(stdout);
}
- (void)triggerTestMountEvent:(es_event_type_t)eventType
diskInfoOverrides:(NSDictionary *)diskInfo
expectedAuthResult:(es_auth_result_t)expectedAuthResult
deviceManagerSetup:(void (^)(SNTEndpointSecurityDeviceManager *))setupDMCallback {
struct statfs fs = {0};
NSString *test_mntfromname = @"/dev/disk2s1";
NSString *test_mntonname = @"/Volumes/KATE'S 4G";
strncpy(fs.f_mntfromname, [test_mntfromname UTF8String], sizeof(fs.f_mntfromname));
strncpy(fs.f_mntonname, [test_mntonname UTF8String], sizeof(fs.f_mntonname));
MockDADisk *disk = [[MockDADisk alloc] init];
disk.diskDescription = @{
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey : @"USB",
(__bridge NSString *)kDADiskDescriptionMediaRemovableKey : @YES,
@"DAVolumeMountable" : @YES,
@"DAVolumePath" : test_mntonname,
@"DADeviceModel" : @"Some device model",
@"DADevicePath" : test_mntonname,
@"DADeviceVendor" : @"Some vendor",
@"DAAppearanceTime" : @0,
@"DAMediaBSDName" : test_mntfromname,
};
if (diskInfo != nil) {
NSMutableDictionary *mergedDiskDescription = [disk.diskDescription mutableCopy];
for (NSString *key in diskInfo) {
mergedDiskDescription[key] = diskInfo[key];
}
disk.diskDescription = (NSDictionary *)mergedDiskDescription;
}
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
SNTEndpointSecurityDeviceManager *deviceManager =
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
metrics:nullptr
logger:nullptr
authResultCache:nullptr];
setupDMCallback(deviceManager);
// Stub the log method since a mock `Logger` object isn't used.
id partialDeviceManager = OCMPartialMock(deviceManager);
OCMStub([partialDeviceManager logDiskAppeared:OCMOCK_ANY]);
[self.mockDA insert:disk bsdName:test_mntfromname];
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
es_message_t esMsg = MakeESMessage(eventType, &proc, ActionType::Auth, 6000);
// Need a pointer to esMsg to capture in blocks below.
es_message_t *heapESMsg = &esMsg;
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
__block int retainCount = 0;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
EXPECT_CALL(*mockESApi, ReleaseMessage).WillRepeatedly(^{
if (retainCount == 0) {
XCTFail(@"Under retain!");
}
retainCount--;
if (retainCount == 0) {
dispatch_semaphore_signal(sema);
}
});
EXPECT_CALL(*mockESApi, RetainMessage).WillRepeatedly(^{
retainCount++;
return heapESMsg;
});
if (eventType == ES_EVENT_TYPE_AUTH_MOUNT) {
esMsg.event.mount.statfs = &fs;
} else if (eventType == ES_EVENT_TYPE_AUTH_REMOUNT) {
esMsg.event.remount.statfs = &fs;
} else {
// Programming error. Fail the test.
XCTFail(@"Unhandled event type in test: %d", eventType);
}
XCTestExpectation *mountExpectation =
[self expectationWithDescription:@"Wait for response from ES"];
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, expectedAuthResult, false))
.WillOnce(testing::InvokeWithoutArgs(^bool {
[mountExpectation fulfill];
return true;
}));
[deviceManager handleMessage:Message(mockESApi, &esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, deviceManager.blockUSBMount ? EventDisposition::kProcessed
: EventDisposition::kDropped);
dispatch_semaphore_signal(semaMetrics);
}];
[self waitForExpectations:@[ mountExpectation ] timeout:60.0];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertSemaTrue(sema, 5, "Failed waiting for message to be processed...");
[partialDeviceManager stopMocking];
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testUSBBlockDisabled {
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
diskInfoOverrides:nil
expectedAuthResult:ES_AUTH_RESULT_ALLOW
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
dm.blockUSBMount = NO;
}];
}
- (void)testRemount {
NSArray *wantRemountArgs = @[ @"noexec", @"rdonly" ];
XCTestExpectation *expectation =
[self expectationWithDescription:
@"Wait for SNTEndpointSecurityDeviceManager's blockCallback to trigger"];
__block NSString *gotmntonname, *gotmntfromname;
__block NSArray<NSString *> *gotRemountedArgs;
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
diskInfoOverrides:nil
expectedAuthResult:ES_AUTH_RESULT_DENY
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
dm.blockUSBMount = YES;
dm.remountArgs = wantRemountArgs;
dm.deviceBlockCallback = ^(SNTDeviceEvent *event) {
gotRemountedArgs = event.remountArgs;
gotmntonname = event.mntonname;
gotmntfromname = event.mntfromname;
[expectation fulfill];
};
}];
XCTAssertEqual(self.mockDA.wasRemounted, YES);
[self waitForExpectations:@[ expectation ] timeout:60.0];
XCTAssertEqualObjects(gotRemountedArgs, wantRemountArgs);
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
}
- (void)testBlockNoRemount {
XCTestExpectation *expectation =
[self expectationWithDescription:
@"Wait for SNTEndpointSecurityDeviceManager's blockCallback to trigger"];
__block NSString *gotmntonname, *gotmntfromname;
__block NSArray<NSString *> *gotRemountedArgs;
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
diskInfoOverrides:nil
expectedAuthResult:ES_AUTH_RESULT_DENY
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
dm.blockUSBMount = YES;
dm.deviceBlockCallback = ^(SNTDeviceEvent *event) {
gotRemountedArgs = event.remountArgs;
gotmntonname = event.mntonname;
gotmntfromname = event.mntfromname;
[expectation fulfill];
};
}];
[self waitForExpectations:@[ expectation ] timeout:60.0];
XCTAssertNil(gotRemountedArgs);
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
}
- (void)testEnsureRemountsCannotChangePerms {
NSArray *wantRemountArgs = @[ @"noexec", @"rdonly" ];
XCTestExpectation *expectation =
[self expectationWithDescription:
@"Wait for SNTEndpointSecurityDeviceManager's blockCallback to trigger"];
__block NSString *gotmntonname, *gotmntfromname;
__block NSArray<NSString *> *gotRemountedArgs;
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
diskInfoOverrides:nil
expectedAuthResult:ES_AUTH_RESULT_DENY
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
dm.blockUSBMount = YES;
dm.remountArgs = wantRemountArgs;
dm.deviceBlockCallback = ^(SNTDeviceEvent *event) {
gotRemountedArgs = event.remountArgs;
gotmntonname = event.mntonname;
gotmntfromname = event.mntfromname;
[expectation fulfill];
};
}];
XCTAssertEqual(self.mockDA.wasRemounted, YES);
[self waitForExpectations:@[ expectation ] timeout:10.0];
XCTAssertEqualObjects(gotRemountedArgs, wantRemountArgs);
XCTAssertEqualObjects(gotmntonname, @"/Volumes/KATE'S 4G");
XCTAssertEqualObjects(gotmntfromname, @"/dev/disk2s1");
}
- (void)testEnsureDMGsDoNotPrompt {
NSArray *wantRemountArgs = @[ @"noexec", @"rdonly" ];
NSDictionary *diskInfo = @{
(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey : @"Virtual Interface",
(__bridge NSString *)kDADiskDescriptionDeviceModelKey : @"Disk Image",
(__bridge NSString *)kDADiskDescriptionMediaNameKey : @"disk image",
};
[self triggerTestMountEvent:ES_EVENT_TYPE_AUTH_MOUNT
diskInfoOverrides:diskInfo
expectedAuthResult:ES_AUTH_RESULT_ALLOW
deviceManagerSetup:^(SNTEndpointSecurityDeviceManager *dm) {
dm.blockUSBMount = YES;
dm.remountArgs = wantRemountArgs;
dm.deviceBlockCallback = ^(SNTDeviceEvent *event) {
XCTFail(@"Should not be called");
};
}];
XCTAssertEqual(self.mockDA.wasRemounted, NO);
}
- (void)testNotifyUnmountFlushesCache {
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_UNMOUNT, &proc);
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
EXPECT_CALL(*mockAuthCache, FlushCache);
SNTEndpointSecurityDeviceManager *deviceManager =
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
metrics:nullptr
logger:nullptr
authResultCache:mockAuthCache];
deviceManager.blockUSBMount = YES;
[deviceManager handleMessage:Message(mockESApi, &esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kProcessed);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
}
- (void)testEnable {
// Ensure the client subscribes to expected event types
std::set<es_event_type_t> expectedEventSubs{
ES_EVENT_TYPE_AUTH_MOUNT,
ES_EVENT_TYPE_AUTH_REMOUNT,
ES_EVENT_TYPE_NOTIFY_UNMOUNT,
};
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
id deviceClient = [[SNTEndpointSecurityDeviceManager alloc]
initWithESAPI:mockESApi
metrics:nullptr
processor:santa::santad::Processor::kDeviceManager];
EXPECT_CALL(*mockESApi, ClearCache(testing::_))
.After(EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs))
.WillOnce(testing::Return(true)))
.WillOnce(testing::Return(true));
[deviceClient enable];
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
@end

View File

@@ -0,0 +1,34 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <Foundation/Foundation.h>
#include "Source/common/SNTCommon.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/Metrics.h"
// Protocol that all subclasses of `SNTEndpointSecurityClient` should adhere to.
@protocol SNTEndpointSecurityEventHandler <NSObject>
// Called Synchronously and serially for each message provided by the
// EndpointSecurity framework.
- (void)handleMessage:(santa::santad::event_providers::endpoint_security::Message &&)esMsg
recordEventMetrics:(void (^)(santa::santad::EventDisposition))recordEventMetrics;
// Called after Santa has finished initializing itself.
// This is an optimal place to subscribe to ES events
- (void)enable;
@end

View File

@@ -1,37 +0,0 @@
/// Copyright 2019 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <Foundation/Foundation.h>
#include "Source/common/SNTCommon.h"
#include "Source/santad/EventProviders/SNTEventProvider.h"
#include <EndpointSecurity/EndpointSecurity.h>
@interface SNTEndpointSecurityManager : NSObject <SNTEventProvider>
- (santa_vnode_id_t)vnodeIDForFile:(es_file_t *)file;
- (BOOL)isCompilerPID:(pid_t)pid;
- (void)setIsCompilerPID:(pid_t)pid;
- (void)setNotCompilerPID:(pid_t)pid;
// Returns YES if the path was truncated.
// The populated buffer will be NUL terminated.
+ (BOOL)populateBufferFromESFile:(es_file_t *)file buffer:(char *)buffer size:(size_t)size;
@property(nonatomic, copy) void (^decisionCallback)(santa_message_t);
@property(nonatomic, copy) void (^logCallback)(santa_message_t);
@property(readonly, nonatomic) es_client_t *client;
@end

View File

@@ -1,645 +0,0 @@
/// Copyright 2019 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santad/EventProviders/SNTEndpointSecurityManager.h"
#include "Source/common/SNTPrefixTree.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SantaCache.h"
#include <bsm/libbsm.h>
#include <libproc.h>
#include <atomic>
// Gleaned from https://opensource.apple.com/source/xnu/xnu-4903.241.1/bsd/sys/proc_internal.h
static const pid_t PID_MAX = 99999;
@interface SNTEndpointSecurityManager () {
std::atomic<bool> _compilerPIDs[PID_MAX];
}
@property(nonatomic) SNTPrefixTree *prefixTree;
@property(nonatomic, readonly) dispatch_queue_t esAuthQueue;
@property(nonatomic, readonly) dispatch_queue_t esNotifyQueue;
@end
@implementation SNTEndpointSecurityManager
- (instancetype)init API_AVAILABLE(macos(10.15)) {
self = [super init];
if (self) {
// To avoid nil deref from es_events arriving before listenForDecisionRequests or
// listenForLogRequests in the MockEndpointSecurity testing util.
_decisionCallback = ^(santa_message_t) {};
_logCallback = ^(santa_message_t) {};
[self establishClient];
[self muteSelf];
_prefixTree = new SNTPrefixTree();
_esAuthQueue =
dispatch_queue_create("com.google.santa.daemon.es_auth", DISPATCH_QUEUE_CONCURRENT);
dispatch_set_target_queue(_esAuthQueue,
dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0));
_esNotifyQueue =
dispatch_queue_create("com.google.santa.daemon.es_notify", DISPATCH_QUEUE_CONCURRENT);
dispatch_set_target_queue(_esNotifyQueue, dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0));
}
return self;
}
- (void)dealloc API_AVAILABLE(macos(10.15)) {
if (_client) {
es_unsubscribe_all(_client);
es_delete_client(_client);
}
if (_prefixTree) delete _prefixTree;
}
- (void)muteSelf {
audit_token_t myAuditToken;
mach_msg_type_number_t count = TASK_AUDIT_TOKEN_COUNT;
if (task_info(mach_task_self(), TASK_AUDIT_TOKEN, (task_info_t)&myAuditToken, &count) ==
KERN_SUCCESS) {
if (es_mute_process(self.client, &myAuditToken) == ES_RETURN_SUCCESS) {
return;
} else {
LOGE(@"Failed to mute this client's process, its events will not be muted.");
}
} else {
LOGE(@"Failed to fetch this client's audit token. Its events will not be muted.");
}
// If we get here, Santa was unable to mute itself. Assume transitory and bail.
exit(EXIT_FAILURE);
}
- (void)establishClient API_AVAILABLE(macos(10.15)) {
while (!self.client) {
SNTConfigurator *config = [SNTConfigurator configurator];
es_client_t *client = NULL;
es_new_client_result_t ret = es_new_client(&client, ^(es_client_t *c, const es_message_t *m) {
pid_t pid = audit_token_to_pid(m->process->audit_token);
int pidversion = audit_token_to_pidversion(m->process->audit_token);
// If enabled, skip any action generated from another endpoint security client.
if (m->process->is_es_client && config.ignoreOtherEndpointSecurityClients) {
if (m->action_type == ES_ACTION_TYPE_AUTH) {
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, false);
}
return;
}
// Perform the following checks on this serial queue.
// Some checks are simple filters that avoid copying m.
// However, the bulk of the work done here is to support transitive whitelisting.
switch (m->event_type) {
case ES_EVENT_TYPE_NOTIFY_EXEC: {
// Deny results are currently logged when ES_EVENT_TYPE_AUTH_EXEC posts a deny.
// TODO(bur/rah): For ES log denies from NOTIFY messages instead of AUTH.
if (m->action.notify.result.auth == ES_AUTH_RESULT_DENY) return;
// Continue log this event
break;
}
case ES_EVENT_TYPE_NOTIFY_CLOSE: {
// Ignore unmodified files
if (!m->event.close.modified) return;
// Remove from decision cache in case this is invalidating a cached binary.
[self removeCacheEntryForVnodeID:[self vnodeIDForFile:m->event.close.target]];
// Create a transitive rule if the file was modified by a running compiler
if ([self isCompilerPID:pid]) {
santa_message_t sm = {};
BOOL truncated =
[SNTEndpointSecurityManager populateBufferFromESFile:m->event.close.target
buffer:sm.path
size:sizeof(sm.path)];
if (truncated) {
LOGE(@"CLOSE: error creating transitive rule, the path is truncated: path=%s pid=%d",
sm.path, pid);
break;
}
if ([@(sm.path) hasPrefix:@"/dev/"]) {
break;
}
sm.action = ACTION_NOTIFY_WHITELIST;
sm.pid = pid;
sm.pidversion = pidversion;
LOGI(@"CLOSE: creating a transitive rule: path=%s pid=%d", sm.path, sm.pid);
self.decisionCallback(sm);
}
// Continue log this event
break;
}
case ES_EVENT_TYPE_NOTIFY_RENAME: {
// Create a transitive rule if the file was renamed by a running compiler
if ([self isCompilerPID:pid]) {
santa_message_t sm = {};
BOOL truncated = [self populateRenamedNewPathFromESMessage:m->event.rename
buffer:sm.path
size:sizeof(sm.path)];
if (truncated) {
LOGE(@"RENAME: error creating transitive rule, the path is truncated: path=%s pid=%d",
sm.path, pid);
break;
}
if ([@(sm.path) hasPrefix:@"/dev/"]) {
break;
}
sm.action = ACTION_NOTIFY_WHITELIST;
sm.pid = pid;
sm.pidversion = pidversion;
LOGI(@"RENAME: creating a transitive rule: path=%s pid=%d", sm.path, sm.pid);
self.decisionCallback(sm);
}
// Continue log this event
break;
}
case ES_EVENT_TYPE_NOTIFY_EXIT: {
// Update the set of running compiler PIDs
[self setNotCompilerPID:pid];
// Skip the standard pipeline and just log.
if (![config enableForkAndExitLogging]) return;
santa_message_t sm = {};
sm.action = ACTION_NOTIFY_EXIT;
sm.pid = pid;
sm.pidversion = pidversion;
sm.ppid = m->process->original_ppid;
audit_token_t at = m->process->audit_token;
sm.uid = audit_token_to_ruid(at);
sm.gid = audit_token_to_rgid(at);
dispatch_async(self.esNotifyQueue, ^{
self.logCallback(sm);
});
return;
}
case ES_EVENT_TYPE_NOTIFY_UNMOUNT: {
// Flush the non-root cache - the root disk cannot be unmounted
// so it isn't necessary to flush its cache.
//
// Flushing the cache calls back into ES. We need to perform this off the handler thread
// otherwise we could potentially deadlock.
dispatch_async(self.esAuthQueue, ^() {
[self flushCacheNonRootOnly:YES];
});
// Skip all other processing
return;
}
case ES_EVENT_TYPE_NOTIFY_FORK: {
// Skip the standard pipeline and just log.
if (![config enableForkAndExitLogging]) return;
santa_message_t sm = {};
sm.action = ACTION_NOTIFY_FORK;
sm.ppid = m->event.fork.child->original_ppid;
audit_token_t at = m->event.fork.child->audit_token;
sm.pid = audit_token_to_pid(at);
sm.pidversion = audit_token_to_pidversion(at);
sm.uid = audit_token_to_ruid(at);
sm.gid = audit_token_to_rgid(at);
dispatch_async(self.esNotifyQueue, ^{
self.logCallback(sm);
});
return;
}
default: {
break;
}
}
switch (m->action_type) {
case ES_ACTION_TYPE_AUTH: {
// Copy the message
es_message_t *mc = es_copy_message(m);
dispatch_semaphore_t processingSema = dispatch_semaphore_create(0);
// Add 1 to the processing semaphore. We're not creating it with a starting
// value of 1 because that requires that the semaphore is not deallocated
// until its value matches the starting value, which we don't need.
dispatch_semaphore_signal(processingSema);
dispatch_semaphore_t deadlineExpiredSema = dispatch_semaphore_create(0);
// Create a timer to deny the execution 5 seconds before the deadline,
// if a response hasn't already been sent. This block will still be enqueued if
// the the deadline - 5 secs is < DISPATCH_TIME_NOW.
// As of 10.15.5, a typical deadline is 60 seconds.
dispatch_after(dispatch_time(m->deadline, NSEC_PER_SEC * -5), self.esAuthQueue, ^(void) {
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
// Handler has already responded, nothing to do.
return;
}
LOGE(@"SNTEndpointSecurityManager: deadline reached: deny pid=%d ret=%d", pid,
es_respond_auth_result(self.client, mc, ES_AUTH_RESULT_DENY, false));
dispatch_semaphore_signal(deadlineExpiredSema);
});
// Dispatch off to the handler and return control to ES.
dispatch_async(self.esAuthQueue, ^{
[self messageHandler:mc];
if (dispatch_semaphore_wait(processingSema, DISPATCH_TIME_NOW) != 0) {
// Deadline expired, wait for deadline block to finish.
dispatch_semaphore_wait(deadlineExpiredSema, DISPATCH_TIME_FOREVER);
}
es_free_message(mc);
});
break;
}
case ES_ACTION_TYPE_NOTIFY: {
// Copy the message and return control back to ES
es_message_t *mc = es_copy_message(m);
dispatch_async(self.esNotifyQueue, ^{
[self messageHandler:mc];
es_free_message(mc);
});
break;
}
default: {
break;
}
}
});
switch (ret) {
case ES_NEW_CLIENT_RESULT_SUCCESS:
LOGI(@"Connected to EndpointSecurity");
_client = client;
return;
case ES_NEW_CLIENT_RESULT_ERR_NOT_PERMITTED:
LOGE(@"Unable to create EndpointSecurity client, not full-disk access permitted");
LOGE(@"Sleeping for 30s before restarting.");
sleep(30);
exit(ret);
default:
LOGE(@"Unable to create es client: %d. Sleeping for a minute.", ret);
sleep(60);
continue;
}
}
}
- (BOOL)respondFromCache:(es_message_t *)m API_AVAILABLE(macos(10.15)) {
return NO;
}
- (void)messageHandler:(es_message_t *)m API_AVAILABLE(macos(10.15)) {
santa_message_t sm = {};
sm.es_message = (void *)m;
es_process_t *targetProcess = NULL;
es_file_t *targetFile = NULL;
void (^callback)(santa_message_t);
switch (m->event_type) {
case ES_EVENT_TYPE_AUTH_EXEC: {
if ([self respondFromCache:m]) {
return;
}
sm.action = ACTION_REQUEST_BINARY;
targetFile = m->event.exec.target->executable;
targetProcess = m->event.exec.target;
callback = self.decisionCallback;
[SNTEndpointSecurityManager populateBufferFromESFile:m->process->tty
buffer:sm.ttypath
size:sizeof(sm.ttypath)];
break;
}
case ES_EVENT_TYPE_NOTIFY_EXEC: {
sm.action = ACTION_NOTIFY_EXEC;
targetFile = m->event.exec.target->executable;
targetProcess = m->event.exec.target;
// TODO(rah): Profile this, it might need to be improved.
uint32_t argCount = es_exec_arg_count(&(m->event.exec));
NSMutableArray *args = [NSMutableArray arrayWithCapacity:argCount];
for (int i = 0; i < argCount; ++i) {
es_string_token_t arg = es_exec_arg(&(m->event.exec), i);
NSString *argStr = [[NSString alloc] initWithBytes:arg.data
length:arg.length
encoding:NSUTF8StringEncoding];
if (argStr.length) [args addObject:argStr];
}
sm.args_array = (void *)CFBridgingRetain(args);
callback = self.logCallback;
break;
}
case ES_EVENT_TYPE_AUTH_UNLINK: {
es_string_token_t pathToken = m->event.unlink.target->path;
NSString *path = [[NSString alloc] initWithBytes:pathToken.data
length:pathToken.length
encoding:NSUTF8StringEncoding];
if ([self isDatabasePath:path]) {
LOGW(@"Preventing attempt to delete Santa databases!");
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true);
return;
}
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
return;
}
case ES_EVENT_TYPE_AUTH_RENAME: {
es_string_token_t pathToken = m->event.rename.source->path;
NSString *path = [[NSString alloc] initWithBytes:pathToken.data
length:pathToken.length
encoding:NSUTF8StringEncoding];
if ([self isDatabasePath:path]) {
LOGW(@"Preventing attempt to rename Santa databases!");
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true);
return;
}
if (m->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
es_string_token_t destToken = m->event.rename.destination.existing_file->path;
NSString *destPath = [[NSString alloc] initWithBytes:destToken.data
length:destToken.length
encoding:NSUTF8StringEncoding];
if ([self isDatabasePath:destPath]) {
LOGW(@"Preventing attempt to overwrite Santa databases!");
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true);
return;
}
}
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
return;
}
case ES_EVENT_TYPE_AUTH_KEXTLOAD: {
es_string_token_t identifier = m->event.kextload.identifier;
NSString *ident = [[NSString alloc] initWithBytes:identifier.data
length:identifier.length
encoding:NSUTF8StringEncoding];
if ([ident isEqualToString:@"com.google.santa-driver"]) {
LOGW(@"Preventing attempt to load Santa kext!");
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, true);
return;
}
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_ALLOW, true);
return;
}
case ES_EVENT_TYPE_NOTIFY_CLOSE: {
sm.action = ACTION_NOTIFY_WRITE;
targetFile = m->event.close.target;
targetProcess = m->process;
callback = self.logCallback;
break;
}
case ES_EVENT_TYPE_NOTIFY_UNLINK: {
sm.action = ACTION_NOTIFY_DELETE;
targetFile = m->event.unlink.target;
targetProcess = m->process;
callback = self.logCallback;
break;
}
case ES_EVENT_TYPE_NOTIFY_LINK: {
sm.action = ACTION_NOTIFY_LINK;
targetFile = m->event.link.source;
targetProcess = m->process;
NSString *p = @(m->event.link.target_dir->path.data);
p = [p stringByAppendingPathComponent:@(m->event.link.target_filename.data)];
[SNTEndpointSecurityManager populateBufferFromString:p.UTF8String
buffer:sm.newpath
size:sizeof(sm.newpath)];
callback = self.logCallback;
break;
}
case ES_EVENT_TYPE_NOTIFY_RENAME: {
sm.action = ACTION_NOTIFY_RENAME;
targetFile = m->event.rename.source;
targetProcess = m->process;
[self populateRenamedNewPathFromESMessage:m->event.rename
buffer:sm.newpath
size:sizeof(sm.newpath)];
callback = self.logCallback;
break;
}
default: LOGE(@"Unknown es message: %d", m->event_type); return;
}
// Deny auth exec events if the path doesn't fit in the santa message.
// TODO(bur/rah): Add support for larger paths.
if ([SNTEndpointSecurityManager populateBufferFromESFile:targetFile
buffer:sm.path
size:sizeof(sm.path)] &&
m->event_type == ES_EVENT_TYPE_AUTH_EXEC) {
LOGE(@"path is truncated, deny: %s", sm.path);
es_respond_auth_result(self.client, m, ES_AUTH_RESULT_DENY, false);
return;
}
// Filter file op events matching the prefix tree.
if (!(m->event_type == ES_EVENT_TYPE_AUTH_EXEC || m->event_type == ES_EVENT_TYPE_NOTIFY_EXEC) &&
self.prefixTree->HasPrefix(sm.path)) {
return;
}
sm.vnode_id.fsid = targetFile->stat.st_dev;
sm.vnode_id.fileid = targetFile->stat.st_ino;
sm.uid = audit_token_to_ruid(targetProcess->audit_token);
sm.gid = audit_token_to_rgid(targetProcess->audit_token);
sm.pid = audit_token_to_pid(targetProcess->audit_token);
sm.pidversion = audit_token_to_pidversion(targetProcess->audit_token);
sm.ppid = targetProcess->original_ppid;
proc_name((m->event_type == ES_EVENT_TYPE_AUTH_EXEC) ? sm.ppid : sm.pid, sm.pname, 1024);
callback(sm);
if (sm.args_array) {
CFBridgingRelease(sm.args_array);
}
}
- (void)listenForDecisionRequests:(void (^)(santa_message_t))callback API_AVAILABLE(macos(10.15)) {
while (!self.connectionEstablished)
usleep(100000); // 100ms
self.decisionCallback = callback;
es_event_type_t events[] = {
ES_EVENT_TYPE_AUTH_EXEC,
ES_EVENT_TYPE_AUTH_UNLINK,
ES_EVENT_TYPE_AUTH_RENAME,
ES_EVENT_TYPE_AUTH_KEXTLOAD,
// This is in the decision callback because it's used for detecting
// the exit of a 'compiler' used by transitive whitelisting.
ES_EVENT_TYPE_NOTIFY_EXIT,
// This is in the decision callback because it's used for clearing the
// caches when a disk is unmounted.
ES_EVENT_TYPE_NOTIFY_UNMOUNT,
};
es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t));
if (sret != ES_RETURN_SUCCESS) LOGE(@"Unable to subscribe to auth events: %d", sret);
// There's a gap between creating a client and subscribing to events. Creating the client
// triggers a cache flush automatically but any events that happen in this gap could be allowed
// and cached, so we force the cache to flush again.
[self flushCacheNonRootOnly:NO];
}
- (void)listenForLogRequests:(void (^)(santa_message_t))callback API_AVAILABLE(macos(10.15)) {
while (!self.connectionEstablished)
usleep(100000); // 100ms
self.logCallback = callback;
es_event_type_t events[] = {
ES_EVENT_TYPE_NOTIFY_EXEC, ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_LINK,
ES_EVENT_TYPE_NOTIFY_RENAME, ES_EVENT_TYPE_NOTIFY_UNLINK, ES_EVENT_TYPE_NOTIFY_FORK,
};
es_return_t sret = es_subscribe(self.client, events, sizeof(events) / sizeof(es_event_type_t));
if (sret != ES_RETURN_SUCCESS) LOGE(@"Unable to subscribe to notify events: %d", sret);
}
- (int)postAction:(santa_action_t)action
forMessage:(santa_message_t)sm API_AVAILABLE(macos(10.15)) {
es_respond_result_t ret;
switch (action) {
case ACTION_RESPOND_ALLOW_COMPILER:
[self setIsCompilerPID:sm.pid];
// Allow the exec, but don't cache the decision so subsequent execs of the compiler get
// marked appropriately.
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW,
false);
break;
case ACTION_RESPOND_ALLOW:
case ACTION_RESPOND_ALLOW_PENDING_TRANSITIVE:
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_ALLOW,
true);
break;
case ACTION_RESPOND_DENY:
case ACTION_RESPOND_TOOLONG:
ret = es_respond_auth_result(self.client, (es_message_t *)sm.es_message, ES_AUTH_RESULT_DENY,
false);
break;
case ACTION_RESPOND_ACK: return ES_RESPOND_RESULT_SUCCESS;
default: ret = ES_RESPOND_RESULT_ERR_INVALID_ARGUMENT;
}
return ret;
}
- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly API_AVAILABLE(macos(10.15)) {
if (!self.connectionEstablished) return YES; // if not connected, there's nothing to flush.
return es_clear_cache(self.client) == ES_CLEAR_CACHE_RESULT_SUCCESS;
}
- (void)fileModificationPrefixFilterAdd:(NSArray *)filters {
for (NSString *filter in filters) {
self.prefixTree->AddPrefix(filter.fileSystemRepresentation);
}
}
- (void)fileModificationPrefixFilterReset {
self.prefixTree->Reset();
}
- (NSArray<NSNumber *> *)cacheCounts {
return nil;
}
- (NSArray<NSNumber *> *)cacheBucketCount {
return nil;
}
- (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID {
return ACTION_UNSET;
}
- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeId {
return KERN_FAILURE;
}
- (BOOL)connectionEstablished {
return self.client != nil;
}
#pragma mark helpers
// Returns YES if the path was truncated.
// The populated buffer will be NUL terminated.
+ (BOOL)populateBufferFromESFile:(es_file_t *)file buffer:(char *)buffer size:(size_t)size {
if (file == NULL) return NO;
return [SNTEndpointSecurityManager populateBufferFromString:file->path.data
buffer:buffer
size:size];
}
// Returns YES if the path was truncated.
// The populated buffer will be NUL terminated.
+ (BOOL)populateBufferFromString:(const char *)string buffer:(char *)buffer size:(size_t)size {
return strlcpy(buffer, string, size) >= size;
}
- (BOOL)populateRenamedNewPathFromESMessage:(es_event_rename_t)mv
buffer:(char *)buffer
size:(size_t)size {
BOOL truncated = NO;
switch (mv.destination_type) {
case ES_DESTINATION_TYPE_NEW_PATH: {
NSString *p = @(mv.destination.new_path.dir->path.data);
p = [p stringByAppendingPathComponent:@(mv.destination.new_path.filename.data)];
truncated = [SNTEndpointSecurityManager populateBufferFromString:p.UTF8String
buffer:buffer
size:size];
break;
}
case ES_DESTINATION_TYPE_EXISTING_FILE: {
truncated = [SNTEndpointSecurityManager populateBufferFromESFile:mv.destination.existing_file
buffer:buffer
size:size];
break;
}
}
return truncated;
}
- (santa_vnode_id_t)vnodeIDForFile:(es_file_t *)file {
return {
.fsid = (uint64_t)file->stat.st_dev,
.fileid = file->stat.st_ino,
};
}
- (BOOL)isDatabasePath:(NSString *)path {
return [path isEqualToString:@"/private/var/db/santa/rules.db"] ||
[path isEqualToString:@"/private/var/db/santa/events.db"];
}
- (BOOL)isCompilerPID:(pid_t)pid {
return (pid && pid < PID_MAX && self->_compilerPIDs[pid].load());
}
- (void)setIsCompilerPID:(pid_t)pid {
if (pid < 1) {
LOGE(@"Unable to watch compiler pid=%d", pid);
} else if (pid >= PID_MAX) {
LOGE(@"Unable to watch compiler pid=%d >= PID_MAX(%d)", pid, PID_MAX);
} else {
self->_compilerPIDs[pid].store(true);
LOGD(@"Watching compiler pid=%d", pid);
}
}
- (void)setNotCompilerPID:(pid_t)pid {
if (pid && pid < PID_MAX) self->_compilerPIDs[pid].store(false);
}
@end

View File

@@ -1,250 +0,0 @@
/// Copyright 2021 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <XCTest/XCTest.h>
#import <bsm/libbsm.h>
#import "Source/common/SNTConfigurator.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityManager.h"
// Must be imported last to overload libEndpointSecurity functions.
#import "Source/santad/EventProviders/EndpointSecurityTestUtil.h"
const NSString *const kEventsDBPath = @"/private/var/db/santa/events.db";
const NSString *const kRulesDBPath = @"/private/var/db/santa/rules.db";
const NSString *const kBenignPath = @"/some/other/path";
@interface SNTEndpointSecurityManagerTest : XCTestCase
@end
@implementation SNTEndpointSecurityManagerTest
- (void)setUp {
[super setUp];
fclose(stdout);
}
- (void)testDenyOnTimeout {
// There should be two events: an early uncached DENY as the consequence for not
// meeting the decision deadline and an actual cached decision from our message
// handler.
__block int wantNumResp = 2;
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
[mockES reset];
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
(void)snt; // Make it appear used for the sake of -Wunused-variable
XCTestExpectation *expectation =
[self expectationWithDescription:@"Wait for santa's Auth dispatch queue"];
__block NSMutableArray<ESResponse *> *events = [NSMutableArray array];
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK
withCallback:^(ESResponse *r) {
@synchronized(self) {
[events addObject:r];
}
if (events.count >= wantNumResp) {
[expectation fulfill];
}
}];
__block es_file_t dbFile = {.path = MakeStringToken(kEventsDBPath)};
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
m.binaryPath = @"somebinary";
m.message->action_type = ES_ACTION_TYPE_AUTH;
m.message->event_type = ES_EVENT_TYPE_AUTH_UNLINK;
m.message->event = (es_events_t){.unlink = {.target = &dbFile}};
m.message->mach_time = 1234;
m.message->deadline = 1234;
}];
[mockES triggerHandler:m.message];
[self waitForExpectations:@[ expectation ] timeout:60.0];
for (ESResponse *resp in events) {
XCTAssertEqual(
resp.result, ES_AUTH_RESULT_DENY,
@"Failed to automatically deny on timeout and also the malicious event afterwards");
}
}
- (void)testDeleteRulesDB {
NSDictionary<const NSString *, NSNumber *> *testCases = @{
kEventsDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
kRulesDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
kBenignPath : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
};
for (const NSString *testPath in testCases) {
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
[mockES reset];
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
(void)snt; // Make it appear used for the sake of -Wunused-variable
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
__block ESResponse *got;
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK
withCallback:^(ESResponse *r) {
got = r;
[expectation fulfill];
}];
__block es_file_t dbFile = {.path = MakeStringToken(testPath)};
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
m.binaryPath = @"somebinary";
m.message->action_type = ES_ACTION_TYPE_AUTH;
m.message->event_type = ES_EVENT_TYPE_AUTH_UNLINK;
m.message->event = (es_events_t){.unlink = {.target = &dbFile}};
}];
[mockES triggerHandler:m.message];
[self waitForExpectations:@[ expectation ] timeout:60.0];
XCTAssertEqual(got.result, [testCases objectForKey:testPath].intValue,
@"Incorrect handling of delete of %@", testPath);
XCTAssertTrue(got.shouldCache, @"Failed to cache deletion decision of %@", testPath);
}
}
- (void)testSkipOtherESEvents {
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
[mockES reset];
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
(void)snt; // Make it appear used for the sake of -Wunused-variable
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
__block ESResponse *got;
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_UNLINK
withCallback:^(ESResponse *r) {
got = r;
[expectation fulfill];
}];
__block es_file_t dbFile = {.path = MakeStringToken(@"/some/other/path")};
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
m.process->is_es_client = true;
m.binaryPath = @"somebinary";
m.message->action_type = ES_ACTION_TYPE_AUTH;
m.message->event_type = ES_EVENT_TYPE_AUTH_UNLINK;
m.message->event = (es_events_t){.unlink = {.target = &dbFile}};
}];
[mockES triggerHandler:m.message];
[self waitForExpectations:@[ expectation ] timeout:60.0];
XCTAssertEqual(got.result, ES_AUTH_RESULT_ALLOW);
}
- (void)testRenameOverwriteRulesDB {
NSDictionary<const NSString *, NSNumber *> *testCases = @{
kEventsDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
kRulesDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
kBenignPath : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
};
for (const NSString *testPath in testCases) {
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
[mockES reset];
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
(void)snt; // Make it appear used for the sake of -Wunused-variable
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
__block ESResponse *got;
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_RENAME
withCallback:^(ESResponse *r) {
got = r;
[expectation fulfill];
}];
__block es_file_t otherFile = {.path = MakeStringToken(@"/some/other/path")};
__block es_file_t dbFile = {.path = MakeStringToken(testPath)};
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
m.binaryPath = @"somebinary";
m.message->action_type = ES_ACTION_TYPE_AUTH;
m.message->event_type = ES_EVENT_TYPE_AUTH_RENAME;
m.message->event = (es_events_t){
.rename =
{
.source = &otherFile,
.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE,
.destination = {.existing_file = &dbFile},
},
};
}];
[mockES triggerHandler:m.message];
[self waitForExpectations:@[ expectation ] timeout:60.0];
XCTAssertEqual(got.result, [testCases objectForKey:testPath].intValue,
@"Incorrect handling of rename of %@", testPath);
XCTAssertTrue(got.shouldCache, @"Failed to cache rename auth decision of %@", testPath);
}
}
- (void)testRenameRulesDB {
NSDictionary<const NSString *, NSNumber *> *testCases = @{
kEventsDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
kRulesDBPath : [NSNumber numberWithInt:ES_AUTH_RESULT_DENY],
kBenignPath : [NSNumber numberWithInt:ES_AUTH_RESULT_ALLOW],
};
for (const NSString *testPath in testCases) {
MockEndpointSecurity *mockES = [MockEndpointSecurity mockEndpointSecurity];
[mockES reset];
SNTEndpointSecurityManager *snt = [[SNTEndpointSecurityManager alloc] init];
(void)snt; // Make it appear used for the sake of -Wunused-variable
XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for response from ES"];
__block ESResponse *got;
[mockES registerResponseCallback:ES_EVENT_TYPE_AUTH_RENAME
withCallback:^(ESResponse *r) {
got = r;
[expectation fulfill];
}];
__block es_file_t otherFile = {.path = MakeStringToken(@"/some/other/path")};
__block es_file_t dbFile = {.path = MakeStringToken(testPath)};
ESMessage *m = [[ESMessage alloc] initWithBlock:^(ESMessage *m) {
m.binaryPath = @"somebinary";
m.message->action_type = ES_ACTION_TYPE_AUTH;
m.message->event_type = ES_EVENT_TYPE_AUTH_RENAME;
m.message->event = (es_events_t){
.rename =
{
.source = &dbFile,
.destination_type = ES_DESTINATION_TYPE_NEW_PATH,
.destination = {.new_path =
{
.dir = &otherFile,
.filename = MakeStringToken(@"someotherfilename"),
}},
},
};
}];
[mockES triggerHandler:m.message];
[self waitForExpectations:@[ expectation ] timeout:60.0];
XCTAssertEqual(got.result, [testCases objectForKey:testPath].intValue,
@"Incorrect handling of rename of %@", testPath);
XCTAssertTrue(got.shouldCache, @"Failed to cache rename auth decision of %@", testPath);
}
}
@end

View File

@@ -0,0 +1,42 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/common/SNTPrefixTree.h"
#import "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#import "Source/santad/Metrics.h"
#import "Source/santad/SNTCompilerController.h"
/// ES Client focused on subscribing to NOTIFY event variants with the intention of enriching
/// received messages and logging the information.
@interface SNTEndpointSecurityRecorder : SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
- (instancetype)
initWithESAPI:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)
esApi
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
enricher:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>)enricher
compilerController:(SNTCompilerController *)compilerController
authResultCache:
(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache
prefixTree:(std::shared_ptr<SNTPrefixTree>)prefixTree;
@end

View File

@@ -0,0 +1,134 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h"
#include <EndpointSecurity/EndpointSecurity.h>
#import "Source/common/SNTLogging.h"
#include "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/Metrics.h"
using santa::santad::EventDisposition;
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
using santa::santad::event_providers::endpoint_security::Enricher;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::Logger;
es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
switch (msg->event_type) {
case ES_EVENT_TYPE_NOTIFY_CLOSE: return msg->event.close.target;
case ES_EVENT_TYPE_NOTIFY_LINK: return msg->event.link.source;
case ES_EVENT_TYPE_NOTIFY_RENAME: return msg->event.rename.source;
case ES_EVENT_TYPE_NOTIFY_UNLINK: return msg->event.unlink.target;
default: return NULL;
}
}
@interface SNTEndpointSecurityRecorder ()
@property SNTCompilerController *compilerController;
@end
@implementation SNTEndpointSecurityRecorder {
std::shared_ptr<AuthResultCache> _authResultCache;
std::shared_ptr<Enricher> _enricher;
std::shared_ptr<Logger> _logger;
std::shared_ptr<SNTPrefixTree> _prefixTree;
}
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
logger:(std::shared_ptr<Logger>)logger
enricher:(std::shared_ptr<Enricher>)enricher
compilerController:(SNTCompilerController *)compilerController
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache
prefixTree:(std::shared_ptr<SNTPrefixTree>)prefixTree {
self = [super initWithESAPI:std::move(esApi)
metrics:std::move(metrics)
processor:santa::santad::Processor::kRecorder];
if (self) {
_enricher = enricher;
_logger = logger;
_compilerController = compilerController;
_authResultCache = authResultCache;
_prefixTree = prefixTree;
[self establishClientOrDie];
}
return self;
}
- (NSString *)description {
return @"Recorder";
}
- (void)handleMessage:(Message &&)esMsg
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
// Pre-enrichment processing
switch (esMsg->event_type) {
case ES_EVENT_TYPE_NOTIFY_CLOSE:
// TODO(mlw): Once we move to building with the macOS 13 SDK, we should also check
// the `was_mapped_writable` field
if (esMsg->event.close.modified == false) {
// Ignore unmodified files
// Note: Do not record metrics in this case. These are not considered "drops"
// because this is not a failure case. Ideally we would tell ES to not send
// these events in the first place but no such mechanism currently exists.
return;
}
self->_authResultCache->RemoveFromCache(esMsg->event.close.target);
break;
default: break;
}
[self.compilerController handleEvent:esMsg withLogger:self->_logger];
// Filter file op events matching the prefix tree.
es_file_t *targetFile = GetTargetFileForPrefixTree(&(*esMsg));
if (targetFile != NULL && self->_prefixTree->HasPrefix(targetFile->path.data)) {
recordEventMetrics(EventDisposition::kDropped);
return;
}
// Enrich the message inline with the ES handler block to capture enrichment
// data as close to the source event as possible.
std::shared_ptr<EnrichedMessage> sharedEnrichedMessage = _enricher->Enrich(std::move(esMsg));
// Asynchronously log the message
[self processEnrichedMessage:std::move(sharedEnrichedMessage)
handler:^(std::shared_ptr<EnrichedMessage> msg) {
self->_logger->Log(std::move(msg));
recordEventMetrics(EventDisposition::kProcessed);
}];
}
- (void)enable {
[super subscribe:{
ES_EVENT_TYPE_NOTIFY_CLOSE,
ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA,
ES_EVENT_TYPE_NOTIFY_EXEC,
ES_EVENT_TYPE_NOTIFY_EXIT,
ES_EVENT_TYPE_NOTIFY_FORK,
ES_EVENT_TYPE_NOTIFY_LINK,
ES_EVENT_TYPE_NOTIFY_RENAME,
ES_EVENT_TYPE_NOTIFY_UNLINK,
}];
}
@end

View File

@@ -0,0 +1,231 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <EndpointSecurity/ESTypes.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <cstddef>
#include <memory>
#include <set>
#include "Source/common/TestUtils.h"
#import "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h"
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#include "Source/santad/Metrics.h"
#import "Source/santad/SNTCompilerController.h"
using santa::santad::EventDisposition;
using santa::santad::Processor;
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
using santa::santad::event_providers::endpoint_security::Enricher;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::Logger;
class MockEnricher : public Enricher {
public:
MOCK_METHOD(std::shared_ptr<EnrichedMessage>, Enrich, (Message &&));
};
class MockAuthResultCache : public AuthResultCache {
public:
using AuthResultCache::AuthResultCache;
MOCK_METHOD(void, RemoveFromCache, (const es_file_t *));
};
class MockLogger : public Logger {
public:
using Logger::Logger;
MOCK_METHOD(void, Log, (std::shared_ptr<EnrichedMessage>));
};
@interface SNTEndpointSecurityRecorderTest : XCTestCase
@end
@implementation SNTEndpointSecurityRecorderTest
- (void)testEnable {
// Ensure the client subscribes to expected event types
std::set<es_event_type_t> expectedEventSubs{
ES_EVENT_TYPE_NOTIFY_CLOSE, ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA, ES_EVENT_TYPE_NOTIFY_EXEC,
ES_EVENT_TYPE_NOTIFY_FORK, ES_EVENT_TYPE_NOTIFY_EXIT, ES_EVENT_TYPE_NOTIFY_LINK,
ES_EVENT_TYPE_NOTIFY_RENAME, ES_EVENT_TYPE_NOTIFY_UNLINK,
};
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
id recorderClient = [[SNTEndpointSecurityRecorder alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kRecorder];
EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs)).WillOnce(testing::Return(true));
[recorderClient enable];
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testHandleMessage {
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc, ActionType::Auth);
es_file_t targetFile = MakeESFile("bar");
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
std::shared_ptr<EnrichedMessage> enrichedMsg = std::shared_ptr<EnrichedMessage>(nullptr);
auto mockEnricher = std::make_shared<MockEnricher>();
EXPECT_CALL(*mockEnricher, Enrich).WillOnce(testing::Return(enrichedMsg));
auto mockAuthCache = std::make_shared<MockAuthResultCache>(nullptr);
EXPECT_CALL(*mockAuthCache, RemoveFromCache(&targetFile)).Times(1);
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
// NOTE: Currently unable to create a partial mock of the
// `SNTEndpointSecurityRecorder` object. There is a bug in OCMock that doesn't
// properly handle the `processEnrichedMessage:handler:` block. Instead this
// test will mock the `Log` method that is called in the handler block.
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
auto mockLogger = std::make_shared<MockLogger>(nullptr, nullptr);
EXPECT_CALL(*mockLogger, Log).WillOnce(testing::InvokeWithoutArgs(^() {
dispatch_semaphore_signal(sema);
}));
auto prefixTree = std::make_shared<SNTPrefixTree>();
id mockCC = OCMStrictClassMock([SNTCompilerController class]);
SNTEndpointSecurityRecorder *recorderClient =
[[SNTEndpointSecurityRecorder alloc] initWithESAPI:mockESApi
metrics:nullptr
logger:mockLogger
enricher:mockEnricher
compilerController:mockCC
authResultCache:mockAuthCache
prefixTree:prefixTree];
// CLOSE not modified, bail early
{
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
esMsg.event.close.modified = false;
esMsg.event.close.target = NULL;
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, &esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTFail("Metrics record callback should not be called here");
}]);
}
// CLOSE modified, remove from cache
{
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
esMsg.event.close.modified = true;
esMsg.event.close.target = &targetFile;
Message msg(mockESApi, &esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
[recorderClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kProcessed);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertSemaTrue(sema, 5, "Log wasn't called within expected time window");
}
// LINK, Prefix match, bail early
{
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_LINK;
esMsg.event.link.source = &targetFile;
prefixTree->AddPrefix(esMsg.event.link.source->path.data);
Message msg(mockESApi, &esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
[recorderClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kDropped);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
}
XCTAssertTrue(OCMVerifyAll(mockCC));
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
XCTBubbleMockVerifyAndClearExpectations(mockEnricher.get());
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
XCTBubbleMockVerifyAndClearExpectations(mockLogger.get());
[mockCC stopMocking];
}
- (void)testGetTargetFileForPrefixTree {
// Ensure `GetTargetFileForPrefixTree` returns expected field for each
// subscribed event type in the `SNTEndpointSecurityRecorder`.
extern es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg);
es_file_t closeFile = MakeESFile("close");
es_file_t linkFile = MakeESFile("link");
es_file_t renameFile = MakeESFile("rename");
es_file_t unlinkFile = MakeESFile("unlink");
es_message_t esMsg;
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_CLOSE;
esMsg.event.close.target = &closeFile;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &closeFile);
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_LINK;
esMsg.event.link.source = &linkFile;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &linkFile);
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_RENAME;
esMsg.event.rename.source = &renameFile;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &renameFile);
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_UNLINK;
esMsg.event.unlink.target = &unlinkFile;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), &unlinkFile);
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_FORK;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXIT;
XCTAssertEqual(GetTargetFileForPrefixTree(&esMsg), nullptr);
}
@end

View File

@@ -0,0 +1,35 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <Foundation/Foundation.h>
#include <memory>
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityEventHandler.h"
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#include "Source/santad/Metrics.h"
/// ES Client focused on mitigating accidental or malicious tampering of Santa and its components.
@interface SNTEndpointSecurityTamperResistance
: SNTEndpointSecurityClient <SNTEndpointSecurityEventHandler>
- (instancetype)
initWithESAPI:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger;
@end

View File

@@ -0,0 +1,122 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h"
#include <EndpointSecurity/ESTypes.h>
#include <string.h>
#import "Source/common/SNTLogging.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/Metrics.h"
using santa::santad::EventDisposition;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::Logger;
static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-driver";
@implementation SNTEndpointSecurityTamperResistance {
std::shared_ptr<Logger> _logger;
}
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
logger:(std::shared_ptr<Logger>)logger {
self = [super initWithESAPI:std::move(esApi)
metrics:std::move(metrics)
processor:santa::santad::Processor::kTamperResistance];
if (self) {
_logger = logger;
[self establishClientOrDie];
}
return self;
}
- (NSString *)description {
return @"Tamper Resistance";
}
- (void)handleMessage:(Message &&)esMsg
recordEventMetrics:(void (^)(EventDisposition))recordEventMetrics {
es_auth_result_t result = ES_AUTH_RESULT_ALLOW;
switch (esMsg->event_type) {
case ES_EVENT_TYPE_AUTH_UNLINK: {
if ([SNTEndpointSecurityTamperResistance
isDatabasePath:esMsg->event.unlink.target->path.data]) {
result = ES_AUTH_RESULT_DENY;
LOGW(@"Preventing attempt to delete Santa databases!");
}
break;
}
case ES_EVENT_TYPE_AUTH_RENAME: {
if ([SNTEndpointSecurityTamperResistance
isDatabasePath:esMsg->event.rename.source->path.data]) {
result = ES_AUTH_RESULT_DENY;
LOGW(@"Preventing attempt to rename Santa databases!");
break;
}
if (esMsg->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
if ([SNTEndpointSecurityTamperResistance
isDatabasePath:esMsg->event.rename.destination.existing_file->path.data]) {
result = ES_AUTH_RESULT_DENY;
LOGW(@"Preventing attempt to overwrite Santa databases!");
break;
}
}
break;
}
case ES_EVENT_TYPE_AUTH_KEXTLOAD: {
// TODO(mlw): Since we don't package the kext anymore, we should consider removing this.
// TODO(mlw): Consider logging when kext loads are attempted.
if (strcmp(esMsg->event.kextload.identifier.data, kSantaKextIdentifier.data()) == 0) {
result = ES_AUTH_RESULT_DENY;
LOGW(@"Preventing attempt to load Santa kext!");
}
break;
}
default:
// Unexpected event type, this is a programming error
[NSException raise:@"Invalid event type"
format:@"Invalid tamper resistance event type: %d", esMsg->event_type];
}
// Do not cache denied operations so that each tamper attempt is logged
[self respondToMessage:esMsg withAuthResult:result cacheable:result == ES_AUTH_RESULT_ALLOW];
// For this client, a processed event is one that was found to be violating anti-tamper policy
recordEventMetrics(result == ES_AUTH_RESULT_DENY ? EventDisposition::kProcessed
: EventDisposition::kDropped);
}
- (void)enable {
// TODO(mlw): For macOS 13, use new mute and invert APIs to limit the
// messages sent for these events to the Santa-specific directories
// checked in the `handleMessage:` method.
[super subscribeAndClearCache:{
ES_EVENT_TYPE_AUTH_KEXTLOAD,
ES_EVENT_TYPE_AUTH_UNLINK,
ES_EVENT_TYPE_AUTH_RENAME,
}];
}
@end

View File

@@ -0,0 +1,231 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <EndpointSecurity/ESTypes.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <stdlib.h>
#include <map>
#include <memory>
#include <set>
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityTamperResistance.h"
#import "Source/santad/Metrics.h"
using santa::santad::EventDisposition;
using santa::santad::event_providers::endpoint_security::Client;
using santa::santad::event_providers::endpoint_security::Message;
static constexpr std::string_view kEventsDBPath = "/private/var/db/santa/events.db";
static constexpr std::string_view kRulesDBPath = "/private/var/db/santa/rules.db";
static constexpr std::string_view kBenignPath = "/some/other/path";
static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-driver";
@interface SNTEndpointSecurityTamperResistanceTest : XCTestCase
@end
@implementation SNTEndpointSecurityTamperResistanceTest
- (void)testEnable {
// Ensure the client subscribes to expected event types
std::set<es_event_type_t> expectedEventSubs{
ES_EVENT_TYPE_AUTH_KEXTLOAD,
ES_EVENT_TYPE_AUTH_UNLINK,
ES_EVENT_TYPE_AUTH_RENAME,
};
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
EXPECT_CALL(*mockESApi, NewClient(testing::_))
.WillOnce(testing::Return(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS)));
EXPECT_CALL(*mockESApi, MuteProcess(testing::_, testing::_)).WillOnce(testing::Return(true));
EXPECT_CALL(*mockESApi, ClearCache(testing::_))
.After(EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs))
.WillOnce(testing::Return(true)))
.WillOnce(testing::Return(true));
SNTEndpointSecurityTamperResistance *tamperClient =
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi
metrics:nullptr
logger:nullptr];
id mockTamperClient = OCMPartialMock(tamperClient);
[mockTamperClient enable];
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
[mockTamperClient stopMocking];
}
- (void)testHandleMessage {
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_AUTH_EXEC, &proc, ActionType::Auth);
es_file_t fileEventsDB = MakeESFile(kEventsDBPath.data());
es_file_t fileRulesDB = MakeESFile(kRulesDBPath.data());
es_file_t fileBenign = MakeESFile(kBenignPath.data());
es_string_token_t santaTok = MakeESStringToken(kSantaKextIdentifier.data());
es_string_token_t benignTok = MakeESStringToken(kBenignPath.data());
std::map<es_file_t *, es_auth_result_t> pathToResult{
{&fileEventsDB, ES_AUTH_RESULT_DENY},
{&fileRulesDB, ES_AUTH_RESULT_DENY},
{&fileBenign, ES_AUTH_RESULT_ALLOW},
};
std::map<es_string_token_t *, es_auth_result_t> kextIdToResult{
{&santaTok, ES_AUTH_RESULT_DENY},
{&benignTok, ES_AUTH_RESULT_ALLOW},
};
dispatch_semaphore_t semaMetrics = dispatch_semaphore_create(0);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsESNewClient();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
SNTEndpointSecurityTamperResistance *tamperClient =
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi
metrics:nullptr
logger:nullptr];
id mockTamperClient = OCMPartialMock(tamperClient);
// Unable to use `OCMExpect` here because we cannot match on the `Message`
// parameter. In order to verify the `AuthResult` and `Cacheable` parameters,
// instead use `OCMStub` and extract the arguments in order to assert their
// expected values.
__block es_auth_result_t gotAuthResult;
__block bool gotCachable;
OCMStub([mockTamperClient respondToMessage:Message(mockESApi, &esMsg)
withAuthResult:(es_auth_result_t)0
cacheable:false])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *inv) {
[inv getArgument:&gotAuthResult atIndex:3];
[inv getArgument:&gotCachable atIndex:4];
});
// First check unhandled event types will crash
{
Message msg(mockESApi, &esMsg);
XCTAssertThrows([tamperClient handleMessage:Message(mockESApi, &esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTFail("Unhandled event types shouldn't call metrics recorder");
}]);
}
// Check UNLINK tamper events
{
esMsg.event_type = ES_EVENT_TYPE_AUTH_UNLINK;
for (const auto &kv : pathToResult) {
Message msg(mockESApi, &esMsg);
esMsg.event.unlink.target = kv.first;
[mockTamperClient
handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, kv.second == ES_AUTH_RESULT_DENY ? EventDisposition::kProcessed
: EventDisposition::kDropped);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertEqual(gotAuthResult, kv.second);
XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW);
}
}
// Check RENAME `source` tamper events
{
esMsg.event_type = ES_EVENT_TYPE_AUTH_RENAME;
for (const auto &kv : pathToResult) {
Message msg(mockESApi, &esMsg);
esMsg.event.rename.source = kv.first;
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
[mockTamperClient
handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, kv.second == ES_AUTH_RESULT_DENY ? EventDisposition::kProcessed
: EventDisposition::kDropped);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertEqual(gotAuthResult, kv.second);
XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW);
}
}
// Check RENAME `dest` tamper events
{
esMsg.event_type = ES_EVENT_TYPE_AUTH_RENAME;
esMsg.event.rename.source = &fileBenign;
for (const auto &kv : pathToResult) {
Message msg(mockESApi, &esMsg);
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
esMsg.event.rename.destination.existing_file = kv.first;
[mockTamperClient
handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, kv.second == ES_AUTH_RESULT_DENY ? EventDisposition::kProcessed
: EventDisposition::kDropped);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertEqual(gotAuthResult, kv.second);
XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW);
}
}
// Check KEXTLOAD tamper events
{
esMsg.event_type = ES_EVENT_TYPE_AUTH_KEXTLOAD;
for (const auto &kv : kextIdToResult) {
Message msg(mockESApi, &esMsg);
esMsg.event.kextload.identifier = *kv.first;
[mockTamperClient
handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, kv.second == ES_AUTH_RESULT_DENY ? EventDisposition::kProcessed
: EventDisposition::kDropped);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertEqual(gotAuthResult, kv.second);
XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW);
}
}
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
XCTAssertTrue(OCMVerifyAll(mockTamperClient));
[mockTamperClient stopMocking];
}
@end

View File

@@ -1,33 +0,0 @@
/// Copyright 2019 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <Foundation/Foundation.h>
#include "Source/common/SNTCommon.h"
@protocol SNTEventProvider <NSObject>
- (void)listenForDecisionRequests:(void (^)(santa_message_t message))callback;
- (void)listenForLogRequests:(void (^)(santa_message_t message))callback;
- (int)postAction:(santa_action_t)action forMessage:(santa_message_t)sm;
- (BOOL)flushCacheNonRootOnly:(BOOL)nonRootOnly;
- (void)fileModificationPrefixFilterAdd:(NSArray *)filters;
- (void)fileModificationPrefixFilterReset;
- (NSArray<NSNumber *> *)cacheCounts;
- (NSArray<NSNumber *> *)cacheBucketCount;
- (santa_action_t)checkCache:(santa_vnode_id_t)vnodeID;
- (kern_return_t)removeCacheEntryForVnodeID:(santa_vnode_id_t)vnodeId;
@property(readonly) BOOL connectionEstablished;
@end

View File

@@ -0,0 +1,71 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_LOGGER_H
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_LOGGER_H
#include <memory>
#include <string_view>
#import <Foundation/Foundation.h>
#import "Source/common/SNTCommonEnums.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h"
// Forward declarations
@class SNTStoredEvent;
namespace santa::santad::logs::endpoint_security {
class LoggerPeer;
}
namespace santa::santad::logs::endpoint_security {
class Logger {
public:
static std::unique_ptr<Logger> Create(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
SNTEventLogType log_type, NSString *event_log_path, NSString *spool_log_path,
size_t spool_dir_size_threshold, size_t spool_file_size_threshold,
uint64_t spool_flush_timeout_ms);
Logger(std::shared_ptr<serializers::Serializer> serializer,
std::shared_ptr<writers::Writer> writer);
virtual ~Logger() = default;
virtual void Log(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EnrichedMessage> msg);
void LogAllowlist(const santa::santad::event_providers::endpoint_security::Message &msg,
const std::string_view hash);
void LogBundleHashingEvents(NSArray<SNTStoredEvent *> *events);
void LogDiskAppeared(NSDictionary *props);
void LogDiskDisappeared(NSDictionary *props);
friend class santa::santad::logs::endpoint_security::LoggerPeer;
private:
std::shared_ptr<serializers::Serializer> serializer_;
std::shared_ptr<writers::Writer> writer_;
};
} // namespace santa::santad::logs::endpoint_security
#endif

View File

@@ -0,0 +1,100 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#include "Source/common/SNTCommonEnums.h"
#include "Source/common/SNTLogging.h"
#include "Source/common/SNTStoredEvent.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/File.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Null.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Spool.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Syslog.h"
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::serializers::BasicString;
using santa::santad::logs::endpoint_security::serializers::Empty;
using santa::santad::logs::endpoint_security::serializers::Protobuf;
using santa::santad::logs::endpoint_security::writers::File;
using santa::santad::logs::endpoint_security::writers::Null;
using santa::santad::logs::endpoint_security::writers::Spool;
using santa::santad::logs::endpoint_security::writers::Syslog;
namespace santa::santad::logs::endpoint_security {
// Flush the write buffer every 5 seconds
static const uint64_t kFlushBufferTimeoutMS = 10000;
// Batch writes up to 128kb
static const size_t kBufferBatchSizeBytes = (1024 * 128);
// Reserve an extra 4kb of buffer space to account for event overflow
static const size_t kMaxExpectedWriteSizeBytes = 4096;
// Translate configured log type to appropriate Serializer/Writer pairs
std::unique_ptr<Logger> Logger::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
SNTEventLogType log_type, NSString *event_log_path,
NSString *spool_log_path, size_t spool_dir_size_threshold,
size_t spool_file_size_threshold,
uint64_t spool_flush_timeout_ms) {
switch (log_type) {
case SNTEventLogTypeFilelog:
return std::make_unique<Logger>(
BasicString::Create(esapi),
File::Create(event_log_path, kFlushBufferTimeoutMS, kBufferBatchSizeBytes,
kMaxExpectedWriteSizeBytes));
case SNTEventLogTypeSyslog:
return std::make_unique<Logger>(BasicString::Create(esapi, false), Syslog::Create());
case SNTEventLogTypeNull: return std::make_unique<Logger>(Empty::Create(), Null::Create());
case SNTEventLogTypeProtobuf:
LOGW(@"The EventLogType value protobuf is currently in beta. The protobuf schema is subject "
@"to change.");
return std::make_unique<Logger>(
Protobuf::Create(esapi),
Spool::Create([spool_log_path UTF8String], spool_dir_size_threshold,
spool_file_size_threshold, spool_flush_timeout_ms));
default: LOGE(@"Invalid log type: %ld", log_type); return nullptr;
}
}
Logger::Logger(std::shared_ptr<serializers::Serializer> serializer,
std::shared_ptr<writers::Writer> writer)
: serializer_(std::move(serializer)), writer_(std::move(writer)) {}
void Logger::Log(std::shared_ptr<EnrichedMessage> msg) {
writer_->Write(serializer_->SerializeMessage(std::move(msg)));
}
void Logger::LogAllowlist(const Message &msg, const std::string_view hash) {
writer_->Write(serializer_->SerializeAllowlist(msg, hash));
}
void Logger::LogBundleHashingEvents(NSArray<SNTStoredEvent *> *events) {
for (SNTStoredEvent *se in events) {
writer_->Write(serializer_->SerializeBundleHashingEvent(se));
}
}
void Logger::LogDiskAppeared(NSDictionary *props) {
writer_->Write(serializer_->SerializeDiskAppeared(props));
}
void Logger::LogDiskDisappeared(NSDictionary *props) {
writer_->Write(serializer_->SerializeDiskDisappeared(props));
}
} // namespace santa::santad::logs::endpoint_security

View File

@@ -0,0 +1,210 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
#include <optional>
#include <string_view>
#include <vector>
#include "Source/common/SNTCommonEnums.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/File.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Null.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Spool.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Syslog.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h"
using santa::santad::event_providers::endpoint_security::EnrichedClose;
using santa::santad::event_providers::endpoint_security::EnrichedFile;
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
using santa::santad::event_providers::endpoint_security::EnrichedProcess;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::Logger;
using santa::santad::logs::endpoint_security::serializers::BasicString;
using santa::santad::logs::endpoint_security::serializers::Empty;
using santa::santad::logs::endpoint_security::serializers::Protobuf;
using santa::santad::logs::endpoint_security::writers::File;
using santa::santad::logs::endpoint_security::writers::Null;
using santa::santad::logs::endpoint_security::writers::Spool;
using santa::santad::logs::endpoint_security::writers::Syslog;
namespace santa::santad::logs::endpoint_security {
class LoggerPeer : public Logger {
public:
// Make base class constructors visible
using Logger::Logger;
LoggerPeer(std::unique_ptr<Logger> l) : Logger(l->serializer_, l->writer_) {}
std::shared_ptr<serializers::Serializer> Serializer() { return serializer_; }
std::shared_ptr<writers::Writer> Writer() { return writer_; }
};
} // namespace santa::santad::logs::endpoint_security
using santa::santad::logs::endpoint_security::LoggerPeer;
class MockSerializer : public Empty {
public:
MOCK_METHOD(std::vector<uint8_t>, SerializeMessage, (const EnrichedClose &msg));
MOCK_METHOD(std::vector<uint8_t>, SerializeAllowlist, (const Message &, const std::string_view));
MOCK_METHOD(std::vector<uint8_t>, SerializeBundleHashingEvent, (SNTStoredEvent *));
MOCK_METHOD(std::vector<uint8_t>, SerializeDiskAppeared, (NSDictionary *));
MOCK_METHOD(std::vector<uint8_t>, SerializeDiskDisappeared, (NSDictionary *));
};
class MockWriter : public Null {
public:
MOCK_METHOD(void, Write, (std::vector<uint8_t> && bytes));
};
@interface LoggerTest : XCTestCase
@end
@implementation LoggerTest
- (void)testCreate {
// Ensure that the factory method creates expected serializers/writers pairs
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
XCTAssertEqual(nullptr, Logger::Create(mockESApi, (SNTEventLogType)123, @"/tmp/temppy",
@"/tmp/spool", 1, 1, 1));
LoggerPeer logger(
Logger::Create(mockESApi, SNTEventLogTypeFilelog, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<BasicString>(logger.Serializer()));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<File>(logger.Writer()));
logger = LoggerPeer(
Logger::Create(mockESApi, SNTEventLogTypeSyslog, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<BasicString>(logger.Serializer()));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Syslog>(logger.Writer()));
logger = LoggerPeer(
Logger::Create(mockESApi, SNTEventLogTypeNull, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Empty>(logger.Serializer()));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Null>(logger.Writer()));
logger = LoggerPeer(
Logger::Create(mockESApi, SNTEventLogTypeProtobuf, @"/tmp/temppy", @"/tmp/spool", 1, 1, 1));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Protobuf>(logger.Serializer()));
XCTAssertNotEqual(nullptr, std::dynamic_pointer_cast<Spool>(logger.Writer()));
}
- (void)testLog {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
auto mockSerializer = std::make_shared<MockSerializer>();
auto mockWriter = std::make_shared<MockWriter>();
// Ensure all Logger::Log* methods call the serializer followed by the writer
es_message_t msg;
// Note: In this test, `RetainMessage` isn't setup to return anything. This
// means that the underlying `es_msg_` in the `Message` object is NULL, and
// therefore no call to `ReleaseMessage` is ever made (hence no expectations).
// Because we don't need to operate on the es_msg_, this simplifies the test.
EXPECT_CALL(*mockESApi, RetainMessage);
auto enrichedMsg = std::make_shared<EnrichedMessage>(
EnrichedClose(Message(mockESApi, &msg),
EnrichedProcess(std::nullopt, std::nullopt, std::nullopt, std::nullopt,
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)),
EnrichedFile(std::nullopt, std::nullopt, std::nullopt)));
EXPECT_CALL(*mockSerializer, SerializeMessage(testing::A<const EnrichedClose &>())).Times(1);
EXPECT_CALL(*mockWriter, Write).Times(1);
Logger(mockSerializer, mockWriter).Log(enrichedMsg);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
}
- (void)testLogAllowList {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
auto mockSerializer = std::make_shared<MockSerializer>();
auto mockWriter = std::make_shared<MockWriter>();
es_message_t msg;
std::string_view hash = "this_is_my_test_hash";
EXPECT_CALL(*mockESApi, RetainMessage);
EXPECT_CALL(*mockSerializer, SerializeAllowlist(testing::_, hash));
EXPECT_CALL(*mockWriter, Write);
Logger(mockSerializer, mockWriter).LogAllowlist(Message(mockESApi, &msg), hash);
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
}
- (void)testLogBundleHashingEvents {
auto mockSerializer = std::make_shared<MockSerializer>();
auto mockWriter = std::make_shared<MockWriter>();
NSArray<id> *events = @[ @"event1", @"event2", @"event3" ];
EXPECT_CALL(*mockSerializer, SerializeBundleHashingEvent).Times((int)[events count]);
EXPECT_CALL(*mockWriter, Write).Times((int)[events count]);
Logger(mockSerializer, mockWriter).LogBundleHashingEvents(events);
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
}
- (void)testLogDiskAppeared {
auto mockSerializer = std::make_shared<MockSerializer>();
auto mockWriter = std::make_shared<MockWriter>();
EXPECT_CALL(*mockSerializer, SerializeDiskAppeared);
EXPECT_CALL(*mockWriter, Write);
Logger(mockSerializer, mockWriter).LogDiskAppeared(@{@"key" : @"value"});
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
}
- (void)testLogDiskDisappeared {
auto mockSerializer = std::make_shared<MockSerializer>();
auto mockWriter = std::make_shared<MockWriter>();
EXPECT_CALL(*mockSerializer, SerializeDiskDisappeared);
EXPECT_CALL(*mockWriter, Write);
Logger(mockSerializer, mockWriter).LogDiskDisappeared(@{@"key" : @"value"});
XCTBubbleMockVerifyAndClearExpectations(mockSerializer.get());
XCTBubbleMockVerifyAndClearExpectations(mockWriter.get());
}
@end

View File

@@ -0,0 +1,75 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_BASICSTRING_H
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_BASICSTRING_H
#import <Foundation/Foundation.h>
#include <memory>
#include <sstream>
#include <vector>
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
namespace santa::santad::logs::endpoint_security::serializers {
class BasicString : public Serializer {
public:
static std::shared_ptr<BasicString> Create(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
bool prefix_time_name = true);
BasicString(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
bool prefix_time_name);
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedExec &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedFork &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedLink &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
std::vector<uint8_t> SerializeAllowlist(
const santa::santad::event_providers::endpoint_security::Message &,
const std::string_view) override;
std::vector<uint8_t> SerializeBundleHashingEvent(SNTStoredEvent *) override;
std::vector<uint8_t> SerializeDiskAppeared(NSDictionary *) override;
std::vector<uint8_t> SerializeDiskDisappeared(NSDictionary *) override;
private:
std::string CreateDefaultString(size_t reserved_size = 512);
std::vector<uint8_t> FinalizeString(std::string &str);
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
bool prefix_time_name_;
};
} // namespace santa::santad::logs::endpoint_security::serializers
#endif

View File

@@ -0,0 +1,476 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
#import <Security/Security.h>
#include <bsm/libbsm.h>
#include <libgen.h>
#include <mach/message.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/kauth.h>
#include <sys/param.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include <string>
#import "Source/common/SNTCachedDecision.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTStoredEvent.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
#import "Source/santad/SNTDecisionCache.h"
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::EnrichedClose;
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
using santa::santad::event_providers::endpoint_security::EnrichedExec;
using santa::santad::event_providers::endpoint_security::EnrichedExit;
using santa::santad::event_providers::endpoint_security::EnrichedFork;
using santa::santad::event_providers::endpoint_security::EnrichedLink;
using santa::santad::event_providers::endpoint_security::EnrichedRename;
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::serializers::Utilities::NonNull;
using santa::santad::logs::endpoint_security::serializers::Utilities::Pid;
using santa::santad::logs::endpoint_security::serializers::Utilities::Pidversion;
using santa::santad::logs::endpoint_security::serializers::Utilities::RealGroup;
using santa::santad::logs::endpoint_security::serializers::Utilities::RealUser;
namespace santa::santad::logs::endpoint_security::serializers {
static inline SanitizableString FilePath(const es_file_t *file) {
return SanitizableString(file);
}
static NSDateFormatter *GetDateFormatter() {
static dispatch_once_t onceToken;
static NSDateFormatter *dateFormatter;
dispatch_once(&onceToken, ^{
dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
dateFormatter.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierISO8601];
dateFormatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
});
return dateFormatter;
}
std::string GetDecisionString(SNTEventState event_state) {
if (event_state & SNTEventStateAllow) {
return "ALLOW";
} else if (event_state & SNTEventStateBlock) {
return "DENY";
} else {
return "UNKNOWN";
}
}
std::string GetReasonString(SNTEventState event_state) {
switch (event_state) {
case SNTEventStateAllowBinary: return "BINARY";
case SNTEventStateAllowCompiler: return "COMPILER";
case SNTEventStateAllowTransitive: return "TRANSITIVE";
case SNTEventStateAllowPendingTransitive: return "PENDING_TRANSITIVE";
case SNTEventStateAllowCertificate: return "CERT";
case SNTEventStateAllowScope: return "SCOPE";
case SNTEventStateAllowTeamID: return "TEAMID";
case SNTEventStateAllowUnknown: return "UNKNOWN";
case SNTEventStateBlockBinary: return "BINARY";
case SNTEventStateBlockCertificate: return "CERT";
case SNTEventStateBlockScope: return "SCOPE";
case SNTEventStateBlockTeamID: return "TEAMID";
case SNTEventStateBlockLongPath: return "LONG_PATH";
case SNTEventStateBlockUnknown: return "UNKNOWN";
default: return "NOTRUNNING";
}
}
std::string GetModeString(SNTClientMode mode) {
switch (mode) {
case SNTClientModeMonitor: return "M";
case SNTClientModeLockdown: return "L";
default: return "U";
}
}
static inline void AppendProcess(std::string &str, const es_process_t *es_proc) {
char bname[MAXPATHLEN];
str.append("|pid=");
str.append(std::to_string(Pid(es_proc->audit_token)));
str.append("|ppid=");
str.append(std::to_string(es_proc->original_ppid));
str.append("|process=");
str.append(basename_r(FilePath(es_proc->executable).Sanitized().data(), bname) ?: "");
str.append("|processpath=");
str.append(FilePath(es_proc->executable).Sanitized());
}
static inline void AppendUserGroup(std::string &str, const audit_token_t &tok,
const std::optional<std::shared_ptr<std::string>> &user,
const std::optional<std::shared_ptr<std::string>> &group) {
str.append("|uid=");
str.append(std::to_string(RealUser(tok)));
str.append("|user=");
str.append(user.has_value() ? user->get()->c_str() : "(null)");
str.append("|gid=");
str.append(std::to_string(RealGroup(tok)));
str.append("|group=");
str.append(group.has_value() ? group->get()->c_str() : "(null)");
}
static char *FormattedDateString(char *buf, size_t len) {
struct timeval tv;
struct tm tm;
gettimeofday(&tv, NULL);
gmtime_r(&tv.tv_sec, &tm);
strftime(buf, len, "%Y-%m-%dT%H:%M:%S", &tm);
snprintf(buf, len, "%s.%03dZ", buf, tv.tv_usec / 1000);
return buf;
}
std::shared_ptr<BasicString> BasicString::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
bool prefix_time_name) {
return std::make_shared<BasicString>(esapi, prefix_time_name);
}
BasicString::BasicString(std::shared_ptr<EndpointSecurityAPI> esapi, bool prefix_time_name)
: esapi_(esapi), prefix_time_name_(prefix_time_name) {}
std::string BasicString::CreateDefaultString(size_t reserved_size) {
std::string str;
str.reserve(1024);
if (prefix_time_name_) {
char buf[32];
str.append("[");
str.append(FormattedDateString(buf, sizeof(buf)));
str.append("] I santad: ");
}
return str;
}
std::vector<uint8_t> BasicString::FinalizeString(std::string &str) {
if (EnabledMachineID()) {
str.append("|machineid=");
str.append(MachineID());
}
str.append("\n");
std::vector<uint8_t> vec(str.length());
std::copy(str.begin(), str.end(), vec.begin());
return vec;
}
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedClose &msg) {
const es_message_t &esm = msg.es_msg();
std::string str = CreateDefaultString();
str.append("action=WRITE|path=");
str.append(FilePath(esm.event.close.target).Sanitized());
AppendProcess(str, esm.process);
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
msg.instigator().real_group());
return FinalizeString(str);
}
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExchange &msg) {
const es_message_t &esm = msg.es_msg();
std::string str = CreateDefaultString();
str.append("action=EXCHANGE|path=");
str.append(FilePath(esm.event.exchangedata.file1).Sanitized());
str.append("|newpath=");
str.append(FilePath(esm.event.exchangedata.file2).Sanitized());
AppendProcess(str, esm.process);
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
msg.instigator().real_group());
return FinalizeString(str);
}
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg) {
const es_message_t &esm = msg.es_msg();
std::string str = CreateDefaultString(1024); // EXECs tend to be bigger, reserve more space.
SNTCachedDecision *cd =
[[SNTDecisionCache sharedCache] cachedDecisionForFile:esm.event.exec.target->executable->stat];
str.append("action=EXEC|decision=");
str.append(GetDecisionString(cd.decision));
str.append("|reason=");
str.append(GetReasonString(cd.decision));
if (cd.decisionExtra) {
str.append("|explain=");
str.append([cd.decisionExtra UTF8String]);
}
if (cd.sha256) {
str.append("|sha256=");
str.append([cd.sha256 UTF8String]);
}
if (cd.certSHA256) {
str.append("|cert_sha256=");
str.append([cd.certSHA256 UTF8String]);
str.append("|cert_cn=");
str.append(SanitizableString(cd.certCommonName).Sanitized());
}
if (cd.teamID.length) {
str.append("|teamid=");
str.append([NonNull(cd.teamID) UTF8String]);
}
if (cd.quarantineURL) {
str.append("|quarantine_url=");
str.append(SanitizableString(cd.quarantineURL).Sanitized());
}
str.append("|pid=");
str.append(std::to_string(Pid(esm.event.exec.target->audit_token)));
str.append("|pidversion=");
str.append(std::to_string(Pidversion(esm.event.exec.target->audit_token)));
str.append("|ppid=");
str.append(std::to_string(esm.event.exec.target->original_ppid));
AppendUserGroup(str, esm.event.exec.target->audit_token, msg.instigator().real_user(),
msg.instigator().real_group());
str.append("|mode=");
str.append(GetModeString([[SNTConfigurator configurator] clientMode]));
str.append("|path=");
str.append(FilePath(esm.event.exec.target->executable).Sanitized());
NSString *origPath = Utilities::OriginalPathForTranslocation(esm.event.exec.target);
if (origPath) {
str.append("|origpath=");
str.append(SanitizableString(origPath).Sanitized());
}
uint32_t argCount = esapi_->ExecArgCount(&esm.event.exec);
if (argCount > 0) {
str.append("|args=");
for (uint32_t i = 0; i < argCount; i++) {
if (i != 0) {
str.append(" ");
}
str.append(SanitizableString(esapi_->ExecArg(&esm.event.exec, i)).Sanitized());
}
}
return FinalizeString(str);
}
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExit &msg) {
const es_message_t &esm = msg.es_msg();
std::string str = CreateDefaultString();
str.append("action=EXIT|pid=");
str.append(std::to_string(Pid(esm.process->audit_token)));
str.append("|pidversion=");
str.append(std::to_string(Pidversion(esm.process->audit_token)));
str.append("|ppid=");
str.append(std::to_string(esm.process->original_ppid));
str.append("|uid=");
str.append(std::to_string(RealUser(esm.process->audit_token)));
str.append("|gid=");
str.append(std::to_string(RealGroup(esm.process->audit_token)));
return FinalizeString(str);
}
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedFork &msg) {
const es_message_t &esm = msg.es_msg();
std::string str = CreateDefaultString();
str.append("action=FORK|pid=");
str.append(std::to_string(Pid(esm.event.fork.child->audit_token)));
str.append("|pidversion=");
str.append(std::to_string(Pidversion(esm.event.fork.child->audit_token)));
str.append("|ppid=");
str.append(std::to_string(esm.event.fork.child->original_ppid));
str.append("|uid=");
str.append(std::to_string(RealUser(esm.event.fork.child->audit_token)));
str.append("|gid=");
str.append(std::to_string(RealGroup(esm.event.fork.child->audit_token)));
return FinalizeString(str);
}
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedLink &msg) {
const es_message_t &esm = msg.es_msg();
std::string str = CreateDefaultString();
str.append("action=LINK|path=");
str.append(FilePath(esm.event.link.source).Sanitized());
str.append("|newpath=");
str.append(FilePath(esm.event.link.target_dir).Sanitized());
str.append("/");
str.append(SanitizableString(esm.event.link.target_filename).Sanitized());
AppendProcess(str, esm.process);
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
msg.instigator().real_group());
return FinalizeString(str);
}
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedRename &msg) {
const es_message_t &esm = msg.es_msg();
std::string str = CreateDefaultString();
str.append("action=RENAME|path=");
str.append(FilePath(esm.event.rename.source).Sanitized());
str.append("|newpath=");
switch (esm.event.rename.destination_type) {
case ES_DESTINATION_TYPE_EXISTING_FILE:
str.append(FilePath(esm.event.rename.destination.existing_file).Sanitized());
break;
case ES_DESTINATION_TYPE_NEW_PATH:
str.append(FilePath(esm.event.rename.destination.new_path.dir).Sanitized());
str.append("/");
str.append(SanitizableString(esm.event.rename.destination.new_path.filename).Sanitized());
break;
default: str.append("(null)"); break;
}
AppendProcess(str, esm.process);
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
msg.instigator().real_group());
return FinalizeString(str);
}
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedUnlink &msg) {
const es_message_t &esm = msg.es_msg();
std::string str = CreateDefaultString();
str.append("action=DELETE|path=");
str.append(FilePath(esm.event.unlink.target).Sanitized());
AppendProcess(str, esm.process);
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
msg.instigator().real_group());
return FinalizeString(str);
}
std::vector<uint8_t> BasicString::SerializeAllowlist(const Message &msg,
const std::string_view hash) {
std::string str = CreateDefaultString();
str.append("action=ALLOWLIST|pid=");
str.append(std::to_string(Pid(msg->process->audit_token)));
str.append("|pidversion=");
str.append(std::to_string(Pidversion(msg->process->audit_token)));
str.append("|path=");
str.append(FilePath(Utilities::GetAllowListTargetFile(msg)).Sanitized());
str.append("|sha256=");
str.append(hash);
return FinalizeString(str);
}
std::vector<uint8_t> BasicString::SerializeBundleHashingEvent(SNTStoredEvent *event) {
std::string str = CreateDefaultString();
str.append("action=BUNDLE|sha256=");
str.append([NonNull(event.fileSHA256) UTF8String]);
str.append("|bundlehash=");
str.append([NonNull(event.fileBundleHash) UTF8String]);
str.append("|bundlename=");
str.append([NonNull(event.fileBundleName) UTF8String]);
str.append("|bundleid=");
str.append([NonNull(event.fileBundleID) UTF8String]);
str.append("|bundlepath=");
str.append([NonNull(event.fileBundlePath) UTF8String]);
str.append("|path=");
str.append([NonNull(event.filePath) UTF8String]);
return FinalizeString(str);
}
std::vector<uint8_t> BasicString::SerializeDiskAppeared(NSDictionary *props) {
NSString *dmg_path = nil;
NSString *serial = nil;
if ([props[@"DADeviceModel"] isEqual:@"Disk Image"]) {
dmg_path = Utilities::DiskImageForDevice(props[@"DADevicePath"]);
} else {
serial = Utilities::SerialForDevice(props[@"DADevicePath"]);
}
NSString *model = [NSString
stringWithFormat:@"%@ %@", NonNull(props[@"DADeviceVendor"]), NonNull(props[@"DADeviceModel"])];
model = [model stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSString *appearanceDateString = [GetDateFormatter()
stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:[props[@"DAAppearanceTime"]
doubleValue]]];
std::string str = CreateDefaultString();
str.append("action=DISKAPPEAR");
str.append("|mount=");
str.append([NonNull([props[@"DAVolumePath"] path]) UTF8String]);
str.append("|volume=");
str.append([NonNull(props[@"DAVolumeName"]) UTF8String]);
str.append("|bsdname=");
str.append([NonNull(props[@"DAMediaBSDName"]) UTF8String]);
str.append("|fs=");
str.append([NonNull(props[@"DAVolumeKind"]) UTF8String]);
str.append("|model=");
str.append([NonNull(model) UTF8String]);
str.append("|serial=");
str.append([NonNull(serial) UTF8String]);
str.append("|bus=");
str.append([NonNull(props[@"DADeviceProtocol"]) UTF8String]);
str.append("|dmgpath=");
str.append([NonNull(dmg_path) UTF8String]);
str.append("|appearance=");
str.append([NonNull(appearanceDateString) UTF8String]);
return FinalizeString(str);
}
std::vector<uint8_t> BasicString::SerializeDiskDisappeared(NSDictionary *props) {
std::string str = CreateDefaultString();
str.append("action=DISKDISAPPEAR");
str.append("|mount=");
str.append([NonNull([props[@"DAVolumePath"] path]) UTF8String]);
str.append("|volume=");
str.append([NonNull(props[@"DAVolumeName"]) UTF8String]);
str.append("|bsdname=");
str.append([NonNull(props[@"DAMediaBSDName"]) UTF8String]);
return FinalizeString(str);
}
} // namespace santa::santad::logs::endpoint_security::serializers

View File

@@ -0,0 +1,394 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <bsm/libbsm.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <map>
#include <string>
#import "Source/common/SNTCachedDecision.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTStoredEvent.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/BasicString.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
#import "Source/santad/SNTDecisionCache.h"
using santa::santad::event_providers::endpoint_security::Enricher;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::serializers::BasicString;
using santa::santad::logs::endpoint_security::serializers::Serializer;
namespace santa::santad::logs::endpoint_security::serializers {
extern std::string GetDecisionString(SNTEventState event_state);
extern std::string GetReasonString(SNTEventState event_state);
extern std::string GetModeString(SNTClientMode mode);
} // namespace santa::santad::logs::endpoint_security::serializers
using santa::santad::logs::endpoint_security::serializers::GetDecisionString;
using santa::santad::logs::endpoint_security::serializers::GetModeString;
using santa::santad::logs::endpoint_security::serializers::GetReasonString;
std::string BasicStringSerializeMessage(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
mockESApi->SetExpectationsRetainReleaseMessage(esMsg);
std::shared_ptr<Serializer> bs = BasicString::Create(mockESApi, false);
std::vector<uint8_t> ret = bs->SerializeMessage(Enricher().Enrich(Message(mockESApi, esMsg)));
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
return std::string(ret.begin(), ret.end());
}
std::string BasicStringSerializeMessage(es_message_t *esMsg) {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
return BasicStringSerializeMessage(mockESApi, esMsg);
}
@interface BasicStringTest : XCTestCase
@property id mockConfigurator;
@property id mockDecisionCache;
@property SNTCachedDecision *testCachedDecision;
@end
@implementation BasicStringTest
- (void)setUp {
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(YES);
OCMStub([self.mockConfigurator machineID]).andReturn(@"my_id");
self.testCachedDecision = [[SNTCachedDecision alloc] init];
self.testCachedDecision.decision = SNTEventStateAllowBinary;
self.testCachedDecision.decisionExtra = @"extra!";
self.testCachedDecision.sha256 = @"1234_hash";
self.testCachedDecision.quarantineURL = @"google.com";
self.testCachedDecision.certSHA256 = @"5678_hash";
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
OCMStub([self.mockDecisionCache cachedDecisionForFile:{}])
.ignoringNonObjectArgs()
.andReturn(self.testCachedDecision);
}
- (void)tearDown {
[self.mockConfigurator stopMocking];
[self.mockDecisionCache stopMocking];
}
- (void)testSerializeMessageClose {
es_file_t procFile = MakeESFile("foo");
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_file_t file = MakeESFile("close_file");
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
esMsg.event.close.modified = true;
esMsg.event.close.target = &file;
std::string got = BasicStringSerializeMessage(&esMsg);
std::string want = "action=WRITE|path=close_file"
"|pid=12|ppid=56|process=foo|processpath=foo"
"|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
- (void)testSerializeMessageExchange {
es_file_t procFile = MakeESFile("foo");
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_file_t file1 = MakeESFile("exchange_1");
es_file_t file2 = MakeESFile("exchange_2");
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA, &proc);
esMsg.event.exchangedata.file1 = &file1;
esMsg.event.exchangedata.file2 = &file2;
// Arbitrarily overwriting mock to test not adding machine id in this event
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(NO);
std::string got = BasicStringSerializeMessage(&esMsg);
std::string want = "action=EXCHANGE|path=exchange_1|newpath=exchange_2"
"|pid=12|ppid=56|process=foo|processpath=foo"
"|uid=-2|user=nobody|gid=-1|group=nogroup\n";
XCTAssertCppStringEqual(got, want);
}
- (void)testSerializeMessageExec {
es_file_t procFile = MakeESFile("foo");
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_file_t execFile = MakeESFile("execpath|");
es_process_t procExec = MakeESProcess(&execFile, MakeAuditToken(12, 89), MakeAuditToken(56, 78));
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXEC, &proc);
esMsg.event.exec.target = &procExec;
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
EXPECT_CALL(*mockESApi, ExecArgCount).WillOnce(testing::Return(3));
EXPECT_CALL(*mockESApi, ExecArg)
.WillOnce(testing::Return(es_string_token_t{9, "exec|path"}))
.WillOnce(testing::Return(es_string_token_t{5, "-l\n-t"}))
.WillOnce(testing::Return(es_string_token_t{8, "-v\r--foo"}));
std::string got = BasicStringSerializeMessage(mockESApi, &esMsg);
std::string want =
"action=EXEC|decision=ALLOW|reason=BINARY|explain=extra!|sha256=1234_hash|"
"cert_sha256=5678_hash|cert_cn=|quarantine_url=google.com|pid=12|pidversion="
"89|ppid=56|uid=-2|user=nobody|gid=-1|group=nogroup|mode=L|path=execpath<pipe>|"
"args=exec<pipe>path -l\\n-t -v\\r--foo|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
- (void)testSerializeMessageExit {
es_file_t procFile = MakeESFile("foo");
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_EXIT, &proc);
std::string got = BasicStringSerializeMessage(&esMsg);
std::string want = "action=EXIT|pid=12|pidversion=34|ppid=56|uid=-2|gid=-1|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
- (void)testSerializeMessageFork {
es_file_t procFile = MakeESFile("foo");
es_file_t procChildFile = MakeESFile("foo_child");
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_process_t procChild =
MakeESProcess(&procChildFile, MakeAuditToken(67, 89), MakeAuditToken(12, 34));
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_FORK, &proc);
esMsg.event.fork.child = &procChild;
std::string got = BasicStringSerializeMessage(&esMsg);
std::string want = "action=FORK|pid=67|pidversion=89|ppid=12|uid=-2|gid=-1|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
- (void)testSerializeMessageLink {
es_file_t procFile = MakeESFile("foo");
es_file_t srcFile = MakeESFile("link_src");
es_file_t dstDir = MakeESFile("link_dst");
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_LINK, &proc);
esMsg.event.link.source = &srcFile;
esMsg.event.link.target_dir = &dstDir;
esMsg.event.link.target_filename = MakeESStringToken("link_name");
std::string got = BasicStringSerializeMessage(&esMsg);
std::string want = "action=LINK|path=link_src|newpath=link_dst/link_name"
"|pid=12|ppid=56|process=foo|processpath=foo"
"|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
- (void)testSerializeMessageRename {
es_file_t procFile = MakeESFile("foo");
es_file_t srcFile = MakeESFile("rename_src");
es_file_t dstFile = MakeESFile("rename_dst");
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_RENAME, &proc);
esMsg.event.rename.source = &srcFile;
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
esMsg.event.rename.destination.existing_file = &dstFile;
std::string got = BasicStringSerializeMessage(&esMsg);
std::string want = "action=RENAME|path=rename_src|newpath=rename_dst"
"|pid=12|ppid=56|process=foo|processpath=foo"
"|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
- (void)testSerializeMessageUnlink {
es_file_t procFile = MakeESFile("foo");
es_file_t targetFile = MakeESFile("deleted_file");
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_UNLINK, &proc);
esMsg.event.unlink.target = &targetFile;
std::string got = BasicStringSerializeMessage(&esMsg);
std::string want = "action=DELETE|path=deleted_file"
"|pid=12|ppid=56|process=foo|processpath=foo"
"|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
- (void)testSerializeAllowlist {
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
esMsg.event.close.target = &file;
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
std::vector<uint8_t> ret = BasicString::Create(mockESApi, false)
->SerializeAllowlist(Message(mockESApi, &esMsg), "test_hash");
XCTAssertTrue(testing::Mock::VerifyAndClearExpectations(mockESApi.get()),
"Expected calls were not properly mocked");
std::string got(ret.begin(), ret.end());
std::string want = "action=ALLOWLIST|pid=12|pidversion=34|path=foo"
"|sha256=test_hash|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
- (void)testSerializeBundleHashingEvent {
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
se.fileSHA256 = @"file_hash";
se.fileBundleHash = @"file_bundle_hash";
se.fileBundleName = @"file_bundle_Name";
se.fileBundleID = nil;
se.fileBundlePath = @"file_bundle_path";
se.filePath = @"file_path";
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeBundleHashingEvent(se);
std::string got(ret.begin(), ret.end());
std::string want = "action=BUNDLE|sha256=file_hash"
"|bundlehash=file_bundle_hash|bundlename=file_bundle_Name|bundleid="
"|bundlepath=file_bundle_path|path=file_path|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
- (void)testSerializeDiskAppeared {
NSDictionary *props = @{
@"DADevicePath" : @"",
@"DADeviceVendor" : @"vendor",
@"DADeviceModel" : @"model",
@"DAAppearanceTime" : @(1252487349), // 2009-09-09 09:09:09
@"DAVolumePath" : [NSURL URLWithString:@"path"],
@"DAMediaBSDName" : @"bsd",
@"DAVolumeKind" : @"apfs",
@"DADeviceProtocol" : @"usb",
};
// Arbitrarily overwriting mock to test not adding machine id in this event
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(NO);
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeDiskAppeared(props);
std::string got(ret.begin(), ret.end());
std::string want = "action=DISKAPPEAR|mount=path|volume=|bsdname=bsd|fs=apfs"
"|model=vendor model|serial=|bus=usb|dmgpath="
"|appearance=2040-09-09T09:09:09.000Z\n";
XCTAssertCppStringEqual(got, want);
}
- (void)testSerializeDiskDisappeared {
NSDictionary *props = @{
@"DAVolumePath" : [NSURL URLWithString:@"path"],
@"DAMediaBSDName" : @"bsd",
};
std::vector<uint8_t> ret = BasicString::Create(nullptr, false)->SerializeDiskDisappeared(props);
std::string got(ret.begin(), ret.end());
std::string want = "action=DISKDISAPPEAR|mount=path|volume=|bsdname=bsd|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
- (void)testGetDecisionString {
std::map<SNTEventState, std::string> stateToDecision = {
{SNTEventStateUnknown, "UNKNOWN"},
{SNTEventStateBundleBinary, "UNKNOWN"},
{SNTEventStateBlockUnknown, "DENY"},
{SNTEventStateBlockBinary, "DENY"},
{SNTEventStateBlockCertificate, "DENY"},
{SNTEventStateBlockScope, "DENY"},
{SNTEventStateBlockTeamID, "DENY"},
{SNTEventStateBlockLongPath, "DENY"},
{SNTEventStateAllowUnknown, "ALLOW"},
{SNTEventStateAllowBinary, "ALLOW"},
{SNTEventStateAllowCertificate, "ALLOW"},
{SNTEventStateAllowScope, "ALLOW"},
{SNTEventStateAllowCompiler, "ALLOW"},
{SNTEventStateAllowTransitive, "ALLOW"},
{SNTEventStateAllowPendingTransitive, "ALLOW"},
{SNTEventStateAllowTeamID, "ALLOW"},
};
for (const auto &kv : stateToDecision) {
XCTAssertCppStringEqual(GetDecisionString(kv.first), kv.second);
}
}
- (void)testGetReasonString {
std::map<SNTEventState, std::string> stateToReason = {
{SNTEventStateUnknown, "NOTRUNNING"},
{SNTEventStateBundleBinary, "NOTRUNNING"},
{SNTEventStateBlockUnknown, "UNKNOWN"},
{SNTEventStateBlockBinary, "BINARY"},
{SNTEventStateBlockCertificate, "CERT"},
{SNTEventStateBlockScope, "SCOPE"},
{SNTEventStateBlockTeamID, "TEAMID"},
{SNTEventStateBlockLongPath, "LONG_PATH"},
{SNTEventStateAllowUnknown, "UNKNOWN"},
{SNTEventStateAllowBinary, "BINARY"},
{SNTEventStateAllowCertificate, "CERT"},
{SNTEventStateAllowScope, "SCOPE"},
{SNTEventStateAllowCompiler, "COMPILER"},
{SNTEventStateAllowTransitive, "TRANSITIVE"},
{SNTEventStateAllowPendingTransitive, "PENDING_TRANSITIVE"},
{SNTEventStateAllowTeamID, "TEAMID"},
};
for (const auto &kv : stateToReason) {
XCTAssertCppStringEqual(GetReasonString(kv.first), kv.second);
}
}
- (void)testGetModeString {
std::map<SNTClientMode, std::string> modeToString = {
{SNTClientModeMonitor, "M"},
{SNTClientModeLockdown, "L"},
{(SNTClientMode)123, "U"},
};
for (const auto &kv : modeToString) {
XCTAssertCppStringEqual(GetModeString(kv.first), kv.second);
}
}
@end

View File

@@ -0,0 +1,60 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_EMPTY_H
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_EMPTY_H
#import <Foundation/Foundation.h>
#include <memory>
#include <vector>
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
namespace santa::santad::logs::endpoint_security::serializers {
class Empty : public Serializer {
public:
static std::shared_ptr<Empty> Create();
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedExec &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedFork &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedLink &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
std::vector<uint8_t> SerializeAllowlist(
const santa::santad::event_providers::endpoint_security::Message &,
const std::string_view) override;
std::vector<uint8_t> SerializeBundleHashingEvent(SNTStoredEvent *) override;
std::vector<uint8_t> SerializeDiskAppeared(NSDictionary *) override;
std::vector<uint8_t> SerializeDiskDisappeared(NSDictionary *) override;
};
} // namespace santa::santad::logs::endpoint_security::serializers
#endif

View File

@@ -0,0 +1,81 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
using santa::santad::event_providers::endpoint_security::EnrichedClose;
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
using santa::santad::event_providers::endpoint_security::EnrichedExec;
using santa::santad::event_providers::endpoint_security::EnrichedExit;
using santa::santad::event_providers::endpoint_security::EnrichedFork;
using santa::santad::event_providers::endpoint_security::EnrichedLink;
using santa::santad::event_providers::endpoint_security::EnrichedRename;
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
using santa::santad::event_providers::endpoint_security::Message;
namespace santa::santad::logs::endpoint_security::serializers {
std::shared_ptr<Empty> Empty::Create() {
return std::make_shared<Empty>();
}
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedClose &msg) {
return {};
}
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExchange &msg) {
return {};
}
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExec &msg) {
return {};
}
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedExit &msg) {
return {};
}
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedFork &msg) {
return {};
}
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedLink &msg) {
return {};
}
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedRename &msg) {
return {};
}
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedUnlink &msg) {
return {};
}
std::vector<uint8_t> Empty::SerializeAllowlist(const Message &msg, const std::string_view hash) {
return {};
}
std::vector<uint8_t> Empty::SerializeBundleHashingEvent(SNTStoredEvent *event) {
return {};
}
std::vector<uint8_t> Empty::SerializeDiskAppeared(NSDictionary *props) {
return {};
}
std::vector<uint8_t> Empty::SerializeDiskDisappeared(NSDictionary *props) {
return {};
}
} // namespace santa::santad::logs::endpoint_security::serializers

View File

@@ -0,0 +1,53 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
using santa::santad::logs::endpoint_security::serializers::Empty;
namespace es = santa::santad::event_providers::endpoint_security;
@interface EmptyTest : XCTestCase
@end
@implementation EmptyTest
- (void)testAllSerializersReturnEmptyVector {
std::shared_ptr<Empty> e = Empty::Create();
// We can get away with passing a fake argument to the `Serialize*` methods
// instead of constructing real ones since the Empty class never touches the
// input parameter.
int fake;
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedClose *)&fake).size(), 0);
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExchange *)&fake).size(), 0);
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExec *)&fake).size(), 0);
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedExit *)&fake).size(), 0);
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedFork *)&fake).size(), 0);
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedLink *)&fake).size(), 0);
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedRename *)&fake).size(), 0);
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedUnlink *)&fake).size(), 0);
XCTAssertEqual(e->SerializeAllowlist(*(es::Message *)&fake, "").size(), 0);
XCTAssertEqual(e->SerializeBundleHashingEvent(nil).size(), 0);
XCTAssertEqual(e->SerializeDiskAppeared(nil).size(), 0);
XCTAssertEqual(e->SerializeDiskDisappeared(nil).size(), 0);
}
@end

View File

@@ -0,0 +1,80 @@
/// Copyright 2022 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_PROTOBUF_H
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_PROTOBUF_H
#import <Foundation/Foundation.h>
#include <google/protobuf/arena.h>
#include <memory>
#include <vector>
#include "Source/common/santa_proto_include_wrapper.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
namespace santa::santad::logs::endpoint_security::serializers {
class Protobuf : public Serializer {
public:
static std::shared_ptr<Protobuf> Create(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi);
Protobuf(
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi);
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedExec &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedFork &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedLink &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
std::vector<uint8_t> SerializeAllowlist(
const santa::santad::event_providers::endpoint_security::Message &,
const std::string_view) override;
std::vector<uint8_t> SerializeBundleHashingEvent(SNTStoredEvent *) override;
std::vector<uint8_t> SerializeDiskAppeared(NSDictionary *) override;
std::vector<uint8_t> SerializeDiskDisappeared(NSDictionary *) override;
private:
::santa::pb::v1::SantaMessage *CreateDefaultProto(google::protobuf::Arena *arena);
::santa::pb::v1::SantaMessage *CreateDefaultProto(
google::protobuf::Arena *arena,
const santa::santad::event_providers::endpoint_security::EnrichedEventType &msg);
::santa::pb::v1::SantaMessage *CreateDefaultProto(google::protobuf::Arena *arena,
struct timespec event_time,
struct timespec processed_time);
std::vector<uint8_t> FinalizeProto(::santa::pb::v1::SantaMessage *santa_msg);
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
};
} // namespace santa::santad::logs::endpoint_security::serializers
#endif

View File

@@ -0,0 +1,630 @@
/// Copyright 2022 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
#include <EndpointSecurity/EndpointSecurity.h>
#include <Kernel/kern/cs_blobs.h>
#include <bsm/libbsm.h>
#include <mach/message.h>
#include <math.h>
#include <sys/proc_info.h>
#include <sys/wait.h>
#include <time.h>
#include <optional>
#include <string_view>
#import "Source/common/SNTCachedDecision.h"
#import "Source/common/SNTConfigurator.h"
#include "Source/common/SNTLogging.h"
#import "Source/common/SNTStoredEvent.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
#import "Source/santad/SNTDecisionCache.h"
#include "google/protobuf/timestamp.pb.h"
using google::protobuf::Arena;
using google::protobuf::Timestamp;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::EnrichedClose;
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
using santa::santad::event_providers::endpoint_security::EnrichedExec;
using santa::santad::event_providers::endpoint_security::EnrichedExit;
using santa::santad::event_providers::endpoint_security::EnrichedFile;
using santa::santad::event_providers::endpoint_security::EnrichedFork;
using santa::santad::event_providers::endpoint_security::EnrichedLink;
using santa::santad::event_providers::endpoint_security::EnrichedProcess;
using santa::santad::event_providers::endpoint_security::EnrichedRename;
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::serializers::Utilities::EffectiveGroup;
using santa::santad::logs::endpoint_security::serializers::Utilities::EffectiveUser;
using santa::santad::logs::endpoint_security::serializers::Utilities::NonNull;
using santa::santad::logs::endpoint_security::serializers::Utilities::Pid;
using santa::santad::logs::endpoint_security::serializers::Utilities::Pidversion;
using santa::santad::logs::endpoint_security::serializers::Utilities::RealGroup;
using santa::santad::logs::endpoint_security::serializers::Utilities::RealUser;
namespace pbv1 = ::santa::pb::v1;
namespace santa::santad::logs::endpoint_security::serializers {
std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi) {
return std::make_shared<Protobuf>(esapi);
}
Protobuf::Protobuf(std::shared_ptr<EndpointSecurityAPI> esapi) : esapi_(esapi) {}
static inline void EncodeTimestamp(Timestamp *timestamp, struct timespec ts) {
timestamp->set_seconds(ts.tv_sec);
timestamp->set_nanos((int32_t)ts.tv_nsec);
}
static inline void EncodeTimestamp(Timestamp *timestamp, struct timeval tv) {
EncodeTimestamp(timestamp, (struct timespec){tv.tv_sec, tv.tv_usec * 1000});
}
static inline void EncodeProcessID(pbv1::ProcessID *proc_id, const audit_token_t &tok) {
proc_id->set_pid(Pid(tok));
proc_id->set_pidversion(Pidversion(tok));
}
static inline void EncodePath(std::string *buf, const es_file_t *dir,
const es_string_token_t file) {
buf->append(std::string_view(dir->path.data, dir->path.length));
buf->append("/");
buf->append(std::string_view(file.data, file.length));
}
static inline void EncodePath(std::string *buf, const es_file_t *es_file) {
buf->append(std::string_view(es_file->path.data, es_file->path.length));
}
static inline void EncodeString(std::string *buf, NSString *value) {
if (value) {
buf->append(std::string_view([value UTF8String], [value length]));
}
}
static inline void EncodeString(std::string *buf, std::string_view value) {
if (value.length() > 0) {
buf->append(std::string_view(value.data(), value.length()));
}
}
static inline void EncodeUserInfo(::pbv1::UserInfo *pb_user_info, uid_t uid,
const std::optional<std::shared_ptr<std::string>> &name) {
pb_user_info->set_uid(uid);
if (name.has_value()) {
pb_user_info->set_name(*name->get());
}
}
static inline void EncodeGroupInfo(::pbv1::GroupInfo *pb_group_info, gid_t gid,
const std::optional<std::shared_ptr<std::string>> &name) {
pb_group_info->set_gid(gid);
if (name.has_value()) {
pb_group_info->set_name(*name->get());
}
}
static inline void EncodeHash(::pbv1::Hash *pb_hash, NSString *sha256) {
if (sha256) {
pb_hash->set_type(::pbv1::Hash::HASH_ALGO_SHA256);
pb_hash->set_hash([sha256 UTF8String], [sha256 length]);
}
}
static inline void EncodeStat(::pbv1::Stat *pb_stat, const struct stat &sb,
const std::optional<std::shared_ptr<std::string>> &username,
const std::optional<std::shared_ptr<std::string>> &groupname) {
pb_stat->set_dev(sb.st_dev);
pb_stat->set_mode(sb.st_mode);
pb_stat->set_nlink(sb.st_nlink);
pb_stat->set_ino(sb.st_ino);
EncodeUserInfo(pb_stat->mutable_user(), sb.st_uid, username);
EncodeGroupInfo(pb_stat->mutable_group(), sb.st_gid, groupname);
pb_stat->set_rdev(sb.st_rdev);
EncodeTimestamp(pb_stat->mutable_access_time(), sb.st_atimespec);
EncodeTimestamp(pb_stat->mutable_modification_time(), sb.st_mtimespec);
EncodeTimestamp(pb_stat->mutable_change_time(), sb.st_ctimespec);
EncodeTimestamp(pb_stat->mutable_birth_time(), sb.st_birthtimespec);
pb_stat->set_size(sb.st_size);
pb_stat->set_blocks(sb.st_blocks);
pb_stat->set_blksize(sb.st_blksize);
pb_stat->set_flags(sb.st_flags);
pb_stat->set_gen(sb.st_gen);
}
static inline void EncodeFileInfo(::pbv1::FileInfo *pb_file, const es_file_t *es_file,
const EnrichedFile &enriched_file, NSString *sha256 = nil) {
EncodePath(pb_file->mutable_path(), es_file);
pb_file->set_truncated(es_file->path_truncated);
EncodeStat(pb_file->mutable_stat(), es_file->stat, enriched_file.user(), enriched_file.group());
if (sha256) {
EncodeHash(pb_file->mutable_hash(), sha256);
}
}
static inline void EncodeFileInfoLight(::pbv1::FileInfoLight *pb_file, const es_file_t *es_file) {
EncodePath(pb_file->mutable_path(), es_file);
pb_file->set_truncated(es_file->path_truncated);
}
static inline void EncodeProcessInfoLight(::pbv1::ProcessInfoLight *pb_proc_info,
uint32_t message_version, const es_process_t *es_proc,
const EnrichedProcess &enriched_proc) {
EncodeProcessID(pb_proc_info->mutable_id(), es_proc->audit_token);
EncodeProcessID(pb_proc_info->mutable_parent_id(), es_proc->parent_audit_token);
pb_proc_info->set_original_parent_pid(es_proc->original_ppid);
pb_proc_info->set_group_id(es_proc->group_id);
pb_proc_info->set_session_id(es_proc->session_id);
EncodeUserInfo(pb_proc_info->mutable_effective_user(), EffectiveUser(es_proc->audit_token),
enriched_proc.effective_user());
EncodeUserInfo(pb_proc_info->mutable_real_user(), RealUser(es_proc->audit_token),
enriched_proc.real_user());
EncodeGroupInfo(pb_proc_info->mutable_effective_group(), EffectiveGroup(es_proc->audit_token),
enriched_proc.effective_group());
EncodeGroupInfo(pb_proc_info->mutable_real_group(), RealGroup(es_proc->audit_token),
enriched_proc.real_group());
EncodeFileInfoLight(pb_proc_info->mutable_executable(), es_proc->executable);
}
static inline void EncodeProcessInfo(::pbv1::ProcessInfo *pb_proc_info, uint32_t message_version,
const es_process_t *es_proc,
const EnrichedProcess &enriched_proc,
SNTCachedDecision *cd = nil) {
EncodeProcessID(pb_proc_info->mutable_id(), es_proc->audit_token);
EncodeProcessID(pb_proc_info->mutable_parent_id(), es_proc->parent_audit_token);
if (message_version >= 4) {
EncodeProcessID(pb_proc_info->mutable_responsible_id(), es_proc->responsible_audit_token);
}
pb_proc_info->set_original_parent_pid(es_proc->original_ppid);
pb_proc_info->set_group_id(es_proc->group_id);
pb_proc_info->set_session_id(es_proc->session_id);
EncodeUserInfo(pb_proc_info->mutable_effective_user(), EffectiveUser(es_proc->audit_token),
enriched_proc.effective_user());
EncodeUserInfo(pb_proc_info->mutable_real_user(), RealUser(es_proc->audit_token),
enriched_proc.real_user());
EncodeGroupInfo(pb_proc_info->mutable_effective_group(), EffectiveGroup(es_proc->audit_token),
enriched_proc.effective_group());
EncodeGroupInfo(pb_proc_info->mutable_real_group(), RealGroup(es_proc->audit_token),
enriched_proc.real_group());
pb_proc_info->set_is_platform_binary(es_proc->is_platform_binary);
pb_proc_info->set_is_es_client(es_proc->is_es_client);
if (es_proc->codesigning_flags & CS_SIGNED) {
::pbv1::CodeSignature *pb_code_sig = pb_proc_info->mutable_code_signature();
pb_code_sig->set_cdhash(es_proc->cdhash, sizeof(es_proc->cdhash));
if (es_proc->signing_id.length > 0) {
pb_code_sig->set_signing_id(es_proc->signing_id.data, es_proc->signing_id.length);
}
if (es_proc->team_id.length > 0) {
pb_code_sig->set_team_id(es_proc->team_id.data, es_proc->team_id.length);
}
}
pb_proc_info->set_cs_flags(es_proc->codesigning_flags);
EncodeFileInfo(pb_proc_info->mutable_executable(), es_proc->executable,
enriched_proc.executable(), cd.sha256);
if (message_version >= 2 && es_proc->tty) {
EncodeFileInfoLight(pb_proc_info->mutable_tty(), es_proc->tty);
}
if (message_version >= 3) {
EncodeTimestamp(pb_proc_info->mutable_start_time(), es_proc->start_time);
}
}
void EncodeExitStatus(::pbv1::Exit *pb_exit, int exitStatus) {
if (WIFEXITED(exitStatus)) {
pb_exit->mutable_exited()->set_exit_status(WEXITSTATUS(exitStatus));
} else if (WIFSIGNALED(exitStatus)) {
pb_exit->mutable_signaled()->set_signal(WTERMSIG(exitStatus));
} else if (WIFSTOPPED(exitStatus)) {
pb_exit->mutable_stopped()->set_signal(WSTOPSIG(exitStatus));
} else {
LOGE(@"Unknown exit status encountered: %d", exitStatus);
}
}
static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info, NSString *cert_hash,
NSString *common_name) {
if (cert_hash) {
EncodeHash(pb_cert_info->mutable_hash(), cert_hash);
}
if (common_name) {
pb_cert_info->set_common_name([common_name UTF8String], [common_name length]);
}
}
::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state) {
if (event_state & SNTEventStateAllow) {
return ::pbv1::Execution::DECISION_ALLOW;
} else if (event_state & SNTEventStateBlock) {
return ::pbv1::Execution::DECISION_DENY;
} else {
return ::pbv1::Execution::DECISION_UNKNOWN;
}
}
::pbv1::Execution::Reason GetReasonEnum(SNTEventState event_state) {
switch (event_state) {
case SNTEventStateAllowBinary: return ::pbv1::Execution::REASON_BINARY;
case SNTEventStateAllowCompiler: return ::pbv1::Execution::REASON_COMPILER;
case SNTEventStateAllowTransitive: return ::pbv1::Execution::REASON_TRANSITIVE;
case SNTEventStateAllowPendingTransitive: return ::pbv1::Execution::REASON_PENDING_TRANSITIVE;
case SNTEventStateAllowCertificate: return ::pbv1::Execution::REASON_CERT;
case SNTEventStateAllowScope: return ::pbv1::Execution::REASON_SCOPE;
case SNTEventStateAllowTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
case SNTEventStateAllowUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
case SNTEventStateBlockBinary: return ::pbv1::Execution::REASON_BINARY;
case SNTEventStateBlockCertificate: return ::pbv1::Execution::REASON_CERT;
case SNTEventStateBlockScope: return ::pbv1::Execution::REASON_SCOPE;
case SNTEventStateBlockTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
case SNTEventStateBlockLongPath: return ::pbv1::Execution::REASON_LONG_PATH;
case SNTEventStateBlockUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
default: return ::pbv1::Execution::REASON_NOT_RUNNING;
}
}
::pbv1::Execution::Mode GetModeEnum(SNTClientMode mode) {
switch (mode) {
case SNTClientModeMonitor: return ::pbv1::Execution::MODE_MONITOR;
case SNTClientModeLockdown: return ::pbv1::Execution::MODE_LOCKDOWN;
case SNTClientModeUnknown: return ::pbv1::Execution::MODE_UNKNOWN;
default: return ::pbv1::Execution::MODE_UNKNOWN;
}
}
::pbv1::FileDescriptor::FDType GetFileDescriptorType(uint32_t fdtype) {
switch (fdtype) {
case PROX_FDTYPE_ATALK: return ::pbv1::FileDescriptor::FD_TYPE_ATALK;
case PROX_FDTYPE_VNODE: return ::pbv1::FileDescriptor::FD_TYPE_VNODE;
case PROX_FDTYPE_SOCKET: return ::pbv1::FileDescriptor::FD_TYPE_SOCKET;
case PROX_FDTYPE_PSHM: return ::pbv1::FileDescriptor::FD_TYPE_PSHM;
case PROX_FDTYPE_PSEM: return ::pbv1::FileDescriptor::FD_TYPE_PSEM;
case PROX_FDTYPE_KQUEUE: return ::pbv1::FileDescriptor::FD_TYPE_KQUEUE;
case PROX_FDTYPE_PIPE: return ::pbv1::FileDescriptor::FD_TYPE_PIPE;
case PROX_FDTYPE_FSEVENTS: return ::pbv1::FileDescriptor::FD_TYPE_FSEVENTS;
case PROX_FDTYPE_NETPOLICY: return ::pbv1::FileDescriptor::FD_TYPE_NETPOLICY;
// Note: CHANNEL and NEXUS types weren't exposed until Xcode v13 SDK.
// Not using the macros to be able to build on older SDK versions.
case 10 /* PROX_FDTYPE_CHANNEL */: return ::pbv1::FileDescriptor::FD_TYPE_CHANNEL;
case 11 /* PROX_FDTYPE_NEXUS */: return ::pbv1::FileDescriptor::FD_TYPE_NEXUS;
default: return ::pbv1::FileDescriptor::FD_TYPE_UNKNOWN;
}
}
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena, struct timespec event_time,
struct timespec processed_time) {
::pbv1::SantaMessage *santa_msg = Arena::CreateMessage<::pbv1::SantaMessage>(arena);
if (EnabledMachineID()) {
EncodeString(santa_msg->mutable_machine_id(), MachineID());
}
EncodeTimestamp(santa_msg->mutable_event_time(), event_time);
EncodeTimestamp(santa_msg->mutable_processed_time(), processed_time);
return santa_msg;
}
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena, const EnrichedEventType &msg) {
return CreateDefaultProto(arena, msg.es_msg().time, msg.enrichment_time());
}
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return CreateDefaultProto(arena, ts, ts);
}
std::vector<uint8_t> Protobuf::FinalizeProto(::pbv1::SantaMessage *santa_msg) {
std::vector<uint8_t> vec(santa_msg->ByteSizeLong());
santa_msg->SerializeToArray(vec.data(), (int)vec.capacity());
return vec;
}
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedClose &msg) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
::pbv1::Close *pb_close = santa_msg->mutable_close();
EncodeProcessInfoLight(pb_close->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
msg.instigator());
EncodeFileInfo(pb_close->mutable_target(), msg.es_msg().event.close.target, msg.target());
pb_close->set_modified(msg.es_msg().event.close.modified);
return FinalizeProto(santa_msg);
}
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExchange &msg) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
::pbv1::Exchangedata *pb_exchangedata = santa_msg->mutable_exchangedata();
EncodeProcessInfoLight(pb_exchangedata->mutable_instigator(), msg.es_msg().version,
msg.es_msg().process, msg.instigator());
EncodeFileInfo(pb_exchangedata->mutable_file1(), msg.es_msg().event.exchangedata.file1,
msg.file1());
EncodeFileInfo(pb_exchangedata->mutable_file2(), msg.es_msg().event.exchangedata.file2,
msg.file2());
return FinalizeProto(santa_msg);
}
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
SNTCachedDecision *cd = [[SNTDecisionCache sharedCache]
cachedDecisionForFile:msg.es_msg().event.exec.target->executable->stat];
GetDecisionEnum(cd.decision);
::pbv1::Execution *pb_exec = santa_msg->mutable_execution();
EncodeProcessInfoLight(pb_exec->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
msg.instigator());
EncodeProcessInfo(pb_exec->mutable_target(), msg.es_msg().version, msg.es_msg().event.exec.target,
msg.target(), cd);
if (msg.es_msg().version >= 2 && msg.script().has_value()) {
EncodeFileInfo(pb_exec->mutable_script(), msg.es_msg().event.exec.script, msg.script().value());
}
if (msg.es_msg().version >= 3 && msg.working_dir().has_value()) {
EncodeFileInfo(pb_exec->mutable_working_directory(), msg.es_msg().event.exec.cwd,
msg.working_dir().value());
}
uint32_t arg_count = esapi_->ExecArgCount(&msg.es_msg().event.exec);
for (uint32_t i = 0; i < arg_count; i++) {
es_string_token_t tok = esapi_->ExecArg(&msg.es_msg().event.exec, i);
pb_exec->add_args(tok.data, tok.length);
}
uint32_t env_count = esapi_->ExecEnvCount(&msg.es_msg().event.exec);
for (uint32_t i = 0; i < env_count; i++) {
es_string_token_t tok = esapi_->ExecEnv(&msg.es_msg().event.exec, i);
pb_exec->add_envs(tok.data, tok.length);
}
if (msg.es_msg().version >= 4) {
int32_t max_fd = -1;
uint32_t fd_count = esapi_->ExecFDCount(&msg.es_msg().event.exec);
for (uint32_t i = 0; i < fd_count; i++) {
const es_fd_t *fd = esapi_->ExecFD(&msg.es_msg().event.exec, i);
max_fd = std::max(max_fd, fd->fd);
::pbv1::FileDescriptor *pb_fd = pb_exec->add_fds();
pb_fd->set_fd(fd->fd);
pb_fd->set_fd_type(GetFileDescriptorType(fd->fdtype));
if (fd->fdtype == PROX_FDTYPE_PIPE) {
pb_fd->set_pipe_id(fd->pipe.pipe_id);
}
}
// If the `max_fd` seen is less than `last_fd`, we know that ES truncated
// the set of returned file descriptors
pb_exec->set_fd_list_truncated(max_fd < msg.es_msg().event.exec.last_fd);
}
pb_exec->set_decision(GetDecisionEnum(cd.decision));
pb_exec->set_reason(GetReasonEnum(cd.decision));
pb_exec->set_mode(GetModeEnum([[SNTConfigurator configurator] clientMode]));
if (cd.certSHA256 || cd.certCommonName) {
EncodeCertificateInfo(pb_exec->mutable_certificate_info(), cd.certSHA256, cd.certCommonName);
}
if (cd.decisionExtra) {
pb_exec->set_explain([cd.decisionExtra UTF8String], [cd.decisionExtra length]);
}
if (cd.quarantineURL) {
pb_exec->set_quarantine_url([cd.quarantineURL UTF8String], [cd.quarantineURL length]);
}
NSString *orig_path = Utilities::OriginalPathForTranslocation(msg.es_msg().event.exec.target);
if (orig_path) {
pb_exec->set_original_path([orig_path UTF8String], [orig_path length]);
}
return FinalizeProto(santa_msg);
}
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExit &msg) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
::pbv1::Exit *pb_exit = santa_msg->mutable_exit();
EncodeProcessInfoLight(pb_exit->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
msg.instigator());
EncodeExitStatus(pb_exit, msg.es_msg().event.exit.stat);
return FinalizeProto(santa_msg);
}
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedFork &msg) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
::pbv1::Fork *pb_fork = santa_msg->mutable_fork();
EncodeProcessInfoLight(pb_fork->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
msg.instigator());
EncodeProcessInfoLight(pb_fork->mutable_child(), msg.es_msg().version,
msg.es_msg().event.fork.child, msg.child());
return FinalizeProto(santa_msg);
}
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedLink &msg) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
::pbv1::Link *pb_link = santa_msg->mutable_link();
EncodeProcessInfoLight(pb_link->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
msg.instigator());
EncodeFileInfo(pb_link->mutable_source(), msg.es_msg().event.link.source, msg.source());
EncodePath(pb_link->mutable_target(), msg.es_msg().event.link.target_dir,
msg.es_msg().event.link.target_filename);
return FinalizeProto(santa_msg);
}
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedRename &msg) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
::pbv1::Rename *pb_rename = santa_msg->mutable_rename();
EncodeProcessInfoLight(pb_rename->mutable_instigator(), msg.es_msg().version,
msg.es_msg().process, msg.instigator());
EncodeFileInfo(pb_rename->mutable_source(), msg.es_msg().event.rename.source, msg.source());
if (msg.es_msg().event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
EncodePath(pb_rename->mutable_target(), msg.es_msg().event.rename.destination.existing_file);
pb_rename->set_target_existed(true);
} else {
EncodePath(pb_rename->mutable_target(), msg.es_msg().event.rename.destination.new_path.dir,
msg.es_msg().event.rename.destination.new_path.filename);
pb_rename->set_target_existed(false);
}
return FinalizeProto(santa_msg);
}
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedUnlink &msg) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
::pbv1::Unlink *pb_unlink = santa_msg->mutable_unlink();
EncodeProcessInfoLight(pb_unlink->mutable_instigator(), msg.es_msg().version,
msg.es_msg().process, msg.instigator());
EncodeFileInfo(pb_unlink->mutable_target(), msg.es_msg().event.unlink.target, msg.target());
return FinalizeProto(santa_msg);
}
std::vector<uint8_t> Protobuf::SerializeAllowlist(const Message &msg, const std::string_view hash) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena);
const es_file_t *es_file = Utilities::GetAllowListTargetFile(msg);
EnrichedFile enriched_file(std::nullopt, std::nullopt, std::nullopt);
EnrichedProcess enriched_process;
::pbv1::Allowlist *pb_allowlist = santa_msg->mutable_allowlist();
EncodeProcessInfoLight(pb_allowlist->mutable_instigator(), msg->version, msg->process,
enriched_process);
EncodeFileInfo(pb_allowlist->mutable_target(), es_file, enriched_file,
[NSString stringWithFormat:@"%s", hash.data()]);
return FinalizeProto(santa_msg);
}
std::vector<uint8_t> Protobuf::SerializeBundleHashingEvent(SNTStoredEvent *event) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena);
::pbv1::Bundle *pb_bundle = santa_msg->mutable_bundle();
EncodeHash(pb_bundle->mutable_file_hash(), event.fileSHA256);
EncodeHash(pb_bundle->mutable_bundle_hash(), event.fileBundleHash);
pb_bundle->set_bundle_name([NonNull(event.fileBundleName) UTF8String]);
pb_bundle->set_bundle_id([NonNull(event.fileBundleID) UTF8String]);
pb_bundle->set_bundle_path([NonNull(event.fileBundlePath) UTF8String]);
pb_bundle->set_path([NonNull(event.filePath) UTF8String]);
return FinalizeProto(santa_msg);
}
static void EncodeDisk(::pbv1::Disk *pb_disk, ::pbv1::Disk_Action action, NSDictionary *props) {
pb_disk->set_action(action);
NSString *dmg_path = nil;
NSString *serial = nil;
if ([props[@"DADeviceModel"] isEqual:@"Disk Image"]) {
dmg_path = Utilities::DiskImageForDevice(props[@"DADevicePath"]);
} else {
serial = Utilities::SerialForDevice(props[@"DADevicePath"]);
}
NSString *model = [NSString
stringWithFormat:@"%@ %@", NonNull(props[@"DADeviceVendor"]), NonNull(props[@"DADeviceModel"])];
model = [model stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
EncodeString(pb_disk->mutable_mount(), [props[@"DAVolumePath"] path]);
EncodeString(pb_disk->mutable_volume(), props[@"DAVolumeName"]);
EncodeString(pb_disk->mutable_bsd_name(), props[@"DAMediaBSDName"]);
EncodeString(pb_disk->mutable_fs(), props[@"DAVolumeKind"]);
EncodeString(pb_disk->mutable_model(), model);
EncodeString(pb_disk->mutable_serial(), serial);
EncodeString(pb_disk->mutable_bus(), props[@"DADeviceProtocol"]);
EncodeString(pb_disk->mutable_dmg_path(), dmg_path);
if (props[@"DAAppearanceTime"]) {
// Note: `DAAppearanceTime` is set via `CFAbsoluteTimeGetCurrent`, which uses the defined
// reference date of `Jan 1 2001 00:00:00 GMT` (not the typical `00:00:00 UTC on 1 January
// 1970`).
NSDate *appearance =
[NSDate dateWithTimeIntervalSinceReferenceDate:[props[@"DAAppearanceTime"] doubleValue]];
NSTimeInterval interval = [appearance timeIntervalSince1970];
double seconds;
double fractional = modf(interval, &seconds);
struct timespec ts = {
.tv_sec = (long)seconds,
.tv_nsec = (long)(fractional * NSEC_PER_SEC),
};
EncodeTimestamp(pb_disk->mutable_appearance(), ts);
Timestamp timestamp = pb_disk->appearance();
}
}
std::vector<uint8_t> Protobuf::SerializeDiskAppeared(NSDictionary *props) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena);
EncodeDisk(santa_msg->mutable_disk(), ::pbv1::Disk::ACTION_APPEARED, props);
return FinalizeProto(santa_msg);
}
std::vector<uint8_t> Protobuf::SerializeDiskDisappeared(NSDictionary *props) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena);
EncodeDisk(santa_msg->mutable_disk(), ::pbv1::Disk::ACTION_DISAPPEARED, props);
return FinalizeProto(santa_msg);
}
} // namespace santa::santad::logs::endpoint_security::serializers

View File

@@ -0,0 +1,638 @@
/// Copyright 2022 Google LLC
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// https://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#include <Kernel/kern/cs_blobs.h>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <sys/proc_info.h>
#include <sys/signal.h>
#include <sys/wait.h>
#include <time.h>
#include <uuid/uuid.h>
#include <cstring>
#include <google/protobuf/util/json_util.h>
#import "Source/common/SNTCachedDecision.h"
#include "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTStoredEvent.h"
#include "Source/common/TestUtils.h"
#include "Source/common/santa_proto_include_wrapper.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
#import "Source/santad/SNTDecisionCache.h"
#include "google/protobuf/any.pb.h"
#include "google/protobuf/timestamp.pb.h"
using google::protobuf::Timestamp;
using google::protobuf::util::JsonPrintOptions;
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
using santa::santad::event_providers::endpoint_security::Enricher;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::serializers::Protobuf;
using santa::santad::logs::endpoint_security::serializers::Serializer;
namespace pbv1 = ::santa::pb::v1;
namespace santa::santad::logs::endpoint_security::serializers {
extern void EncodeExitStatus(::pbv1::Exit *pbExit, int exitStatus);
extern ::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state);
extern ::pbv1::Execution::Reason GetReasonEnum(SNTEventState event_state);
extern ::pbv1::Execution::Mode GetModeEnum(SNTClientMode mode);
extern ::pbv1::FileDescriptor::FDType GetFileDescriptorType(uint32_t fdtype);
} // namespace santa::santad::logs::endpoint_security::serializers
using santa::santad::logs::endpoint_security::serializers::EncodeExitStatus;
using santa::santad::logs::endpoint_security::serializers::GetDecisionEnum;
using santa::santad::logs::endpoint_security::serializers::GetFileDescriptorType;
using santa::santad::logs::endpoint_security::serializers::GetModeEnum;
using santa::santad::logs::endpoint_security::serializers::GetReasonEnum;
JsonPrintOptions DefaultJsonPrintOptions() {
JsonPrintOptions options;
options.always_print_enums_as_ints = false;
options.always_print_primitive_fields = false;
options.preserve_proto_field_names = true;
options.add_whitespace = true;
return options;
}
NSString *TestJsonPath(NSString *jsonFileName, uint32_t version) {
static dispatch_once_t onceToken;
static NSString *testPath;
static NSString *testDataRepoPath = @"santa/Source/santad/testdata/protobuf";
NSString *testDataRepoVersionPath = [NSString stringWithFormat:@"v%u", version];
dispatch_once(&onceToken, ^{
testPath = [NSString pathWithComponents:@[
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testDataRepoPath
]];
});
return [NSString pathWithComponents:@[ testPath, testDataRepoVersionPath, jsonFileName ]];
}
NSString *EventTypeToFilename(es_event_type_t eventType) {
switch (eventType) {
case ES_EVENT_TYPE_NOTIFY_CLOSE: return @"close.json";
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA: return @"exchangedata.json";
case ES_EVENT_TYPE_NOTIFY_EXEC: return @"exec.json";
case ES_EVENT_TYPE_NOTIFY_EXIT: return @"exit.json";
case ES_EVENT_TYPE_NOTIFY_FORK: return @"fork.json";
case ES_EVENT_TYPE_NOTIFY_LINK: return @"link.json";
case ES_EVENT_TYPE_NOTIFY_RENAME: return @"rename.json";
case ES_EVENT_TYPE_NOTIFY_UNLINK: return @"unlink.json";
default: XCTFail(@"Unhandled event type: %d", eventType); return nil;
}
}
NSString *LoadTestJson(NSString *jsonFileName, uint32_t version) {
NSError *err = nil;
NSString *jsonData = [NSString stringWithContentsOfFile:TestJsonPath(jsonFileName, version)
encoding:NSUTF8StringEncoding
error:&err];
if (err) {
XCTFail(@"Failed to load test data \"%@\": %@", jsonFileName, err);
}
return jsonData;
}
bool CompareTime(const Timestamp &timestamp, struct timespec ts) {
return timestamp.seconds() == ts.tv_sec && timestamp.nanos() == ts.tv_nsec;
}
void CheckSantaMessage(const ::pbv1::SantaMessage &santaMsg, const es_message_t &esMsg,
struct timespec enrichmentTime) {
XCTAssertTrue(CompareTime(santaMsg.processed_time(), enrichmentTime));
XCTAssertTrue(CompareTime(santaMsg.event_time(), esMsg.time));
}
const google::protobuf::Message &SantaMessageEvent(const ::pbv1::SantaMessage &santaMsg) {
switch (santaMsg.event_case()) {
case ::pbv1::SantaMessage::kExecution: return santaMsg.execution();
case ::pbv1::SantaMessage::kFork: return santaMsg.fork();
case ::pbv1::SantaMessage::kExit: return santaMsg.exit();
case ::pbv1::SantaMessage::kClose: return santaMsg.close();
case ::pbv1::SantaMessage::kRename: return santaMsg.rename();
case ::pbv1::SantaMessage::kUnlink: return santaMsg.unlink();
case ::pbv1::SantaMessage::kLink: return santaMsg.link();
case ::pbv1::SantaMessage::kExchangedata: return santaMsg.exchangedata();
case ::pbv1::SantaMessage::kDisk: return santaMsg.disk();
case ::pbv1::SantaMessage::kBundle: return santaMsg.bundle();
case ::pbv1::SantaMessage::kAllowlist: return santaMsg.allowlist();
case ::pbv1::SantaMessage::EVENT_NOT_SET:
XCTFail(@"Protobuf message SantaMessage did not set an 'event' field");
OS_FALLTHROUGH;
default:
[NSException raise:@"Required protobuf field not set"
format:@"SantaMessage missing required field 'event'"];
abort();
}
}
std::string ConvertMessageToJsonString(const ::pbv1::SantaMessage &santaMsg) {
JsonPrintOptions options = DefaultJsonPrintOptions();
const google::protobuf::Message &message = SantaMessageEvent(santaMsg);
std::string json;
XCTAssertTrue(google::protobuf::util::MessageToJsonString(message, &json, options).ok());
return json;
}
void CheckProto(const ::pbv1::SantaMessage &santaMsg,
std::shared_ptr<EnrichedMessage> enrichedMsg) {
return std::visit(
[santaMsg](const EnrichedEventType &enrichedEvent) {
CheckSantaMessage(santaMsg, enrichedEvent.es_msg(), enrichedEvent.enrichment_time());
NSString *wantData = LoadTestJson(EventTypeToFilename(enrichedEvent.es_msg().event_type),
enrichedEvent.es_msg().version);
std::string got = ConvertMessageToJsonString(santaMsg);
XCTAssertEqualObjects([NSString stringWithUTF8String:got.c_str()], wantData);
},
enrichedMsg->GetEnrichedMessage());
}
void SerializeAndCheck(es_event_type_t eventType,
void (^messageSetup)(std::shared_ptr<MockEndpointSecurityAPI>,
es_message_t *)) {
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
cur_version++) {
if (cur_version == 3) {
// Note: Version 3 was only in a macOS beta.
continue;
}
es_file_t procFile = MakeESFile("foo", MakeStat(100));
es_file_t ttyFile = MakeESFile("footty", MakeStat(200));
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_message_t esMsg = MakeESMessage(eventType, &proc);
esMsg.process->tty = &ttyFile;
esMsg.version = cur_version;
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
messageSetup(mockESApi, &esMsg);
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
std::shared_ptr<EnrichedMessage> enrichedMsg = Enricher().Enrich(Message(mockESApi, &esMsg));
std::vector<uint8_t> vec = bs->SerializeMessage(enrichedMsg);
std::string protoStr(vec.begin(), vec.end());
::pbv1::SantaMessage santaMsg;
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
CheckProto(santaMsg, enrichedMsg);
}
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
@interface ProtobufTest : XCTestCase
@property id mockConfigurator;
@property id mockDecisionCache;
@property SNTCachedDecision *testCachedDecision;
@end
@implementation ProtobufTest
- (void)setUp {
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(YES);
OCMStub([self.mockConfigurator machineID]).andReturn(@"my_machine_id");
self.testCachedDecision = [[SNTCachedDecision alloc] init];
self.testCachedDecision.decision = SNTEventStateAllowBinary;
self.testCachedDecision.decisionExtra = @"extra!";
self.testCachedDecision.sha256 = @"1234_file_hash";
self.testCachedDecision.quarantineURL = @"google.com";
self.testCachedDecision.certSHA256 = @"5678_cert_hash";
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
OCMStub([self.mockDecisionCache cachedDecisionForFile:{}])
.ignoringNonObjectArgs()
.andReturn(self.testCachedDecision);
}
- (void)tearDown {
[self.mockConfigurator stopMocking];
[self.mockDecisionCache stopMocking];
}
- (void)testSerializeMessageClose {
__block es_file_t file = MakeESFile("close_file", MakeStat(300));
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_CLOSE,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.close.modified = true;
esMsg->event.close.target = &file;
});
}
- (void)testSerializeMessageExchange {
__block es_file_t file1 = MakeESFile("exchange_file_1", MakeStat(300));
__block es_file_t file2 = MakeESFile("exchange_file_1", MakeStat(400));
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.exchangedata.file1 = &file1;
esMsg->event.exchangedata.file2 = &file2;
});
}
- (void)testGetDecisionEnum {
std::map<SNTEventState, ::pbv1::Execution::Decision> stateToDecision = {
{SNTEventStateUnknown, ::pbv1::Execution::DECISION_UNKNOWN},
{SNTEventStateBundleBinary, ::pbv1::Execution::DECISION_UNKNOWN},
{SNTEventStateBlockUnknown, ::pbv1::Execution::DECISION_DENY},
{SNTEventStateBlockBinary, ::pbv1::Execution::DECISION_DENY},
{SNTEventStateBlockCertificate, ::pbv1::Execution::DECISION_DENY},
{SNTEventStateBlockScope, ::pbv1::Execution::DECISION_DENY},
{SNTEventStateBlockTeamID, ::pbv1::Execution::DECISION_DENY},
{SNTEventStateBlockLongPath, ::pbv1::Execution::DECISION_DENY},
{SNTEventStateAllowUnknown, ::pbv1::Execution::DECISION_ALLOW},
{SNTEventStateAllowBinary, ::pbv1::Execution::DECISION_ALLOW},
{SNTEventStateAllowCertificate, ::pbv1::Execution::DECISION_ALLOW},
{SNTEventStateAllowScope, ::pbv1::Execution::DECISION_ALLOW},
{SNTEventStateAllowCompiler, ::pbv1::Execution::DECISION_ALLOW},
{SNTEventStateAllowTransitive, ::pbv1::Execution::DECISION_ALLOW},
{SNTEventStateAllowPendingTransitive, ::pbv1::Execution::DECISION_ALLOW},
{SNTEventStateAllowTeamID, ::pbv1::Execution::DECISION_ALLOW},
};
for (const auto &kv : stateToDecision) {
XCTAssertEqual(GetDecisionEnum(kv.first), kv.second, @"Bad decision for state: %ld", kv.first);
}
}
- (void)testGetReasonEnum {
std::map<SNTEventState, ::pbv1::Execution::Reason> stateToReason = {
{SNTEventStateUnknown, ::pbv1::Execution::REASON_NOT_RUNNING},
{SNTEventStateBundleBinary, ::pbv1::Execution::REASON_NOT_RUNNING},
{SNTEventStateBlockUnknown, ::pbv1::Execution::REASON_UNKNOWN},
{SNTEventStateBlockBinary, ::pbv1::Execution::REASON_BINARY},
{SNTEventStateBlockCertificate, ::pbv1::Execution::REASON_CERT},
{SNTEventStateBlockScope, ::pbv1::Execution::REASON_SCOPE},
{SNTEventStateBlockTeamID, ::pbv1::Execution::REASON_TEAM_ID},
{SNTEventStateBlockLongPath, ::pbv1::Execution::REASON_LONG_PATH},
{SNTEventStateAllowUnknown, ::pbv1::Execution::REASON_UNKNOWN},
{SNTEventStateAllowBinary, ::pbv1::Execution::REASON_BINARY},
{SNTEventStateAllowCertificate, ::pbv1::Execution::REASON_CERT},
{SNTEventStateAllowScope, ::pbv1::Execution::REASON_SCOPE},
{SNTEventStateAllowCompiler, ::pbv1::Execution::REASON_COMPILER},
{SNTEventStateAllowTransitive, ::pbv1::Execution::REASON_TRANSITIVE},
{SNTEventStateAllowPendingTransitive, ::pbv1::Execution::REASON_PENDING_TRANSITIVE},
{SNTEventStateAllowTeamID, ::pbv1::Execution::REASON_TEAM_ID},
};
for (const auto &kv : stateToReason) {
XCTAssertEqual(GetReasonEnum(kv.first), kv.second, @"Bad reason for state: %ld", kv.first);
}
}
- (void)testGetModeEnum {
std::map<SNTClientMode, ::pbv1::Execution::Mode> clientModeToExecMode = {
{SNTClientModeUnknown, ::pbv1::Execution::MODE_UNKNOWN},
{SNTClientModeMonitor, ::pbv1::Execution::MODE_MONITOR},
{SNTClientModeLockdown, ::pbv1::Execution::MODE_LOCKDOWN},
{(SNTClientMode)123, ::pbv1::Execution::MODE_UNKNOWN},
};
for (const auto &kv : clientModeToExecMode) {
XCTAssertEqual(GetModeEnum(kv.first), kv.second, @"Bad mode for state: %ld", kv.first);
}
}
- (void)testGetFileDescriptorType {
std::map<uint32_t, ::pbv1::FileDescriptor::FDType> fdtypeToEnumType = {
{PROX_FDTYPE_ATALK, ::pbv1::FileDescriptor::FD_TYPE_ATALK},
{PROX_FDTYPE_VNODE, ::pbv1::FileDescriptor::FD_TYPE_VNODE},
{PROX_FDTYPE_SOCKET, ::pbv1::FileDescriptor::FD_TYPE_SOCKET},
{PROX_FDTYPE_PSHM, ::pbv1::FileDescriptor::FD_TYPE_PSHM},
{PROX_FDTYPE_PSEM, ::pbv1::FileDescriptor::FD_TYPE_PSEM},
{PROX_FDTYPE_KQUEUE, ::pbv1::FileDescriptor::FD_TYPE_KQUEUE},
{PROX_FDTYPE_PIPE, ::pbv1::FileDescriptor::FD_TYPE_PIPE},
{PROX_FDTYPE_FSEVENTS, ::pbv1::FileDescriptor::FD_TYPE_FSEVENTS},
{PROX_FDTYPE_NETPOLICY, ::pbv1::FileDescriptor::FD_TYPE_NETPOLICY},
{10 /* PROX_FDTYPE_CHANNEL */, ::pbv1::FileDescriptor::FD_TYPE_CHANNEL},
{11 /* PROX_FDTYPE_NEXUS */, ::pbv1::FileDescriptor::FD_TYPE_NEXUS},
};
for (const auto &kv : fdtypeToEnumType) {
XCTAssertEqual(GetFileDescriptorType(kv.first), kv.second, @"Bad fd type name for fdtype: %u",
kv.first);
}
}
- (void)testSerializeMessageExec {
es_file_t procFileTarget = MakeESFile("fooexec", MakeStat(300));
__block es_process_t procTarget =
MakeESProcess(&procFileTarget, MakeAuditToken(23, 45), MakeAuditToken(67, 89));
__block es_file_t fileCwd = MakeESFile("cwd", MakeStat(400));
__block es_file_t fileScript = MakeESFile("script.sh", MakeStat(500));
__block es_fd_t fd1 = {.fd = 1, .fdtype = PROX_FDTYPE_VNODE};
__block es_fd_t fd2 = {.fd = 2, .fdtype = PROX_FDTYPE_SOCKET};
__block es_fd_t fd3 = {.fd = 3, .fdtype = PROX_FDTYPE_PIPE, .pipe = {.pipe_id = 123}};
procTarget.codesigning_flags = CS_SIGNED | CS_HARD | CS_KILL;
memset(procTarget.cdhash, 'A', sizeof(procTarget.cdhash));
procTarget.signing_id = MakeESStringToken("my_signing_id");
procTarget.team_id = MakeESStringToken("my_team_id");
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXEC, ^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
esMsg->event.exec.target = &procTarget;
esMsg->event.exec.cwd = &fileCwd;
esMsg->event.exec.script = &fileScript;
// For version 5, simulate a "truncated" set of FDs
if (esMsg->version == 5) {
esMsg->event.exec.last_fd = 123;
} else {
esMsg->event.exec.last_fd = 3;
}
EXPECT_CALL(*mockESApi, ExecArgCount).WillOnce(testing::Return(3));
EXPECT_CALL(*mockESApi, ExecArg)
.WillOnce(testing::Return(MakeESStringToken("exec_path")))
.WillOnce(testing::Return(MakeESStringToken("-l")))
.WillOnce(testing::Return(MakeESStringToken("--foo")));
EXPECT_CALL(*mockESApi, ExecEnvCount).WillOnce(testing::Return(2));
EXPECT_CALL(*mockESApi, ExecEnv)
.WillOnce(testing::Return(MakeESStringToken("ENV_PATH=/path/to/bin:/and/another")))
.WillOnce(testing::Return(MakeESStringToken("DEBUG=1")));
if (esMsg->version >= 4) {
EXPECT_CALL(*mockESApi, ExecFDCount).WillOnce(testing::Return(3));
EXPECT_CALL(*mockESApi, ExecFD)
.WillOnce(testing::Return(&fd1))
.WillOnce(testing::Return(&fd2))
.WillOnce(testing::Return(&fd3));
}
});
}
- (void)testSerializeMessageExit {
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXIT,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.exit.stat = W_EXITCODE(1, 0);
});
}
- (void)testEncodeExitStatus {
{
::pbv1::Exit pbExit;
EncodeExitStatus(&pbExit, W_EXITCODE(1, 0));
XCTAssertTrue(pbExit.has_exited());
XCTAssertEqual(1, pbExit.exited().exit_status());
}
{
::pbv1::Exit pbExit;
EncodeExitStatus(&pbExit, W_EXITCODE(2, SIGUSR1));
XCTAssertTrue(pbExit.has_signaled());
XCTAssertEqual(SIGUSR1, pbExit.signaled().signal());
}
{
::pbv1::Exit pbExit;
EncodeExitStatus(&pbExit, W_STOPCODE(SIGSTOP));
XCTAssertTrue(pbExit.has_stopped());
XCTAssertEqual(SIGSTOP, pbExit.stopped().signal());
}
}
- (void)testSerializeMessageFork {
__block es_file_t procFileChild = MakeESFile("foo_child", MakeStat(300));
__block es_file_t ttyFileChild = MakeESFile("footty", MakeStat(400));
__block es_process_t procChild =
MakeESProcess(&procFileChild, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
procChild.tty = &ttyFileChild;
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_FORK,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.fork.child = &procChild;
});
}
- (void)testSerializeMessageLink {
__block es_file_t fileSource = MakeESFile("source", MakeStat(300));
__block es_file_t fileTargetDir = MakeESFile("target_dir");
es_string_token_t targetTok = MakeESStringToken("target_file");
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_LINK,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.link.source = &fileSource;
esMsg->event.link.target_dir = &fileTargetDir;
esMsg->event.link.target_filename = targetTok;
});
}
- (void)testSerializeMessageRename {
__block es_file_t fileSource = MakeESFile("source", MakeStat(300));
__block es_file_t fileTargetDir = MakeESFile("target_dir");
es_string_token_t targetTok = MakeESStringToken("target_file");
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_RENAME,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.rename.source = &fileSource;
// Test new and existing destination types
if (esMsg->version == 4) {
esMsg->event.rename.destination.existing_file = &fileTargetDir;
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
} else {
esMsg->event.rename.destination.new_path.dir = &fileTargetDir;
esMsg->event.rename.destination.new_path.filename = targetTok;
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
}
});
}
- (void)testSerializeMessageUnlink {
__block es_file_t fileTarget = MakeESFile("unlink_file", MakeStat(300));
__block es_file_t fileTargetParent = MakeESFile("unlink_file_parent", MakeStat(400));
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_UNLINK,
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
esMsg->event.unlink.target = &fileTarget;
esMsg->event.unlink.parent_dir = &fileTargetParent;
});
}
- (void)testSerializeAllowlist {
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
cur_version++) {
if (cur_version == 3) {
// Note: Version 3 was only in a macOS beta.
continue;
}
es_file_t procFile = MakeESFile("foo", MakeStat(100));
es_file_t ttyFile = MakeESFile("footty", MakeStat(200));
es_file_t closeFile = MakeESFile("close_file", MakeStat(300));
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
esMsg.process->tty = &ttyFile;
esMsg.version = cur_version;
esMsg.event.close.modified = true;
esMsg.event.close.target = &closeFile;
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
std::vector<uint8_t> vec = bs->SerializeAllowlist(Message(mockESApi, &esMsg), "hash_value");
std::string protoStr(vec.begin(), vec.end());
::pbv1::SantaMessage santaMsg;
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
NSString *wantData = LoadTestJson(@"allowlist.json", esMsg.version);
std::string got = ConvertMessageToJsonString(santaMsg);
XCTAssertEqualObjects([NSString stringWithUTF8String:got.c_str()], wantData);
}
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
- (void)testSerializeBundleHashingEvent {
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
se.fileSHA256 = @"file_hash";
se.fileBundleHash = @"file_bundle_hash";
se.fileBundleName = @"file_bundle_name";
se.fileBundleID = nil;
se.fileBundlePath = @"file_bundle_path";
se.filePath = @"file_path";
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeBundleHashingEvent(se);
std::string protoStr(vec.begin(), vec.end());
::pbv1::SantaMessage santaMsg;
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
XCTAssertTrue(santaMsg.has_bundle());
const ::pbv1::Bundle &pbBundle = santaMsg.bundle();
::pbv1::Hash pbHash = pbBundle.file_hash();
XCTAssertEqualObjects(@(pbHash.hash().c_str()), se.fileSHA256);
XCTAssertEqual(pbHash.type(), ::pbv1::Hash::HASH_ALGO_SHA256);
pbHash = pbBundle.bundle_hash();
XCTAssertEqualObjects(@(pbHash.hash().c_str()), se.fileBundleHash);
XCTAssertEqual(pbHash.type(), ::pbv1::Hash::HASH_ALGO_SHA256);
XCTAssertEqualObjects(@(pbBundle.bundle_name().c_str()), se.fileBundleName);
XCTAssertEqualObjects(@(pbBundle.bundle_id().c_str()), @"");
XCTAssertEqualObjects(@(pbBundle.bundle_path().c_str()), se.fileBundlePath);
XCTAssertEqualObjects(@(pbBundle.path().c_str()), se.filePath);
}
- (void)testSerializeDiskAppeared {
NSDictionary *props = @{
@"DADevicePath" : @"",
@"DADeviceVendor" : @"vendor",
@"DADeviceModel" : @"model",
@"DAAppearanceTime" : @(123456789),
@"DAVolumePath" : [NSURL URLWithString:@"path"],
@"DAMediaBSDName" : @"bsd",
@"DAVolumeKind" : @"apfs",
@"DADeviceProtocol" : @"usb",
};
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeDiskAppeared(props);
std::string protoStr(vec.begin(), vec.end());
::pbv1::SantaMessage santaMsg;
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
XCTAssertTrue(santaMsg.has_disk());
const ::pbv1::Disk &pbDisk = santaMsg.disk();
XCTAssertEqual(pbDisk.action(), ::pbv1::Disk::ACTION_APPEARED);
XCTAssertEqualObjects(@(pbDisk.mount().c_str()), [props[@"DAVolumePath"] path]);
XCTAssertEqualObjects(@(pbDisk.volume().c_str()), @"");
XCTAssertEqualObjects(@(pbDisk.bsd_name().c_str()), props[@"DAMediaBSDName"]);
XCTAssertEqualObjects(@(pbDisk.fs().c_str()), props[@"DAVolumeKind"]);
XCTAssertEqualObjects(@(pbDisk.model().c_str()), @"vendor model");
XCTAssertEqualObjects(@(pbDisk.serial().c_str()), @"");
XCTAssertEqualObjects(@(pbDisk.bus().c_str()), props[@"DADeviceProtocol"]);
XCTAssertEqualObjects(@(pbDisk.dmg_path().c_str()), @"");
// Note: `DAAppearanceTime` is treated as a reference time since 2001 and is converted to a
// reference time of 1970. Skip the calculation in the test here, just ensure the value is set.
XCTAssertGreaterThan(pbDisk.appearance().seconds(), 1);
}
- (void)testSerializeDiskDisppeared {
NSDictionary *props = @{
@"DADevicePath" : @"",
@"DADeviceVendor" : @"vendor",
@"DADeviceModel" : @"model",
@"DAAppearanceTime" : @(123456789),
@"DAVolumePath" : [NSURL URLWithString:@"path"],
@"DAMediaBSDName" : @"bsd",
@"DAVolumeKind" : @"apfs",
@"DADeviceProtocol" : @"usb",
};
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeDiskDisappeared(props);
std::string protoStr(vec.begin(), vec.end());
::pbv1::SantaMessage santaMsg;
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
XCTAssertTrue(santaMsg.has_disk());
const ::pbv1::Disk &pbDisk = santaMsg.disk();
XCTAssertEqual(pbDisk.action(), ::pbv1::Disk::ACTION_DISAPPEARED);
XCTAssertEqualObjects(@(pbDisk.mount().c_str()), [props[@"DAVolumePath"] path]);
XCTAssertEqualObjects(@(pbDisk.volume().c_str()), @"");
XCTAssertEqualObjects(@(pbDisk.bsd_name().c_str()), props[@"DAMediaBSDName"]);
XCTAssertEqualObjects(@(pbDisk.fs().c_str()), props[@"DAVolumeKind"]);
XCTAssertEqualObjects(@(pbDisk.model().c_str()), @"vendor model");
XCTAssertEqualObjects(@(pbDisk.serial().c_str()), @"");
XCTAssertEqualObjects(@(pbDisk.bus().c_str()), props[@"DADeviceProtocol"]);
XCTAssertEqualObjects(@(pbDisk.dmg_path().c_str()), @"");
// Note: `DAAppearanceTime` is treated as a reference time since 2001 and is converted to a
// reference time of 1970. Skip the calculation in the test here, just ensure the value is set.
XCTAssertGreaterThan(pbDisk.appearance().seconds(), 1);
}
@end

View File

@@ -0,0 +1,62 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SANITIZABLESTRING_H
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_SANITIZABLESTRING_H
#include <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#include <optional>
#include <sstream>
#include <string>
namespace santa::santad::logs::endpoint_security::serializers {
// Small helper class that will sanitize a given string, but will only use new
// memory if the string required sanitization. If the string is already
// sanitized, this class only uses the given buffers.
class SanitizableString {
public:
SanitizableString(const es_file_t *file);
SanitizableString(const es_string_token_t &tok);
SanitizableString(const char *str, size_t len);
SanitizableString(NSString *str);
SanitizableString(SanitizableString &&other) = delete;
SanitizableString(const SanitizableString &other) = delete;
SanitizableString &operator=(const SanitizableString &rhs) = delete;
SanitizableString &operator=(SanitizableString &&rhs) = delete;
// Return the original, unsanitized string
std::string_view String() const;
// Return the sanitized string
std::string_view Sanitized() const;
static std::optional<std::string> SanitizeString(const char *str);
static std::optional<std::string> SanitizeString(const char *str, size_t length);
friend std::ostream &operator<<(std::ostream &ss, const SanitizableString &sani_string);
private:
const char *data_;
size_t length_;
mutable bool sanitized_ = false;
mutable std::optional<std::string> sanitized_string_;
};
} // namespace santa::santad::logs::endpoint_security::serializers
#endif

View File

@@ -0,0 +1,113 @@
/// Copyright 2022 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include "Source/santad/Logs/EndpointSecurity/Serializers/SanitizableString.h"
namespace santa::santad::logs::endpoint_security::serializers {
SanitizableString::SanitizableString(const es_file_t *file)
: data_(file->path.data), length_(file->path.length) {}
SanitizableString::SanitizableString(const es_string_token_t &tok)
: data_(tok.data), length_(tok.length) {}
SanitizableString::SanitizableString(NSString *str)
: data_([str UTF8String]), length_([str length]) {}
SanitizableString::SanitizableString(const char *str, size_t len) : data_(str), length_(len) {}
std::string_view SanitizableString::String() const {
return std::string_view(data_, length_);
}
std::string_view SanitizableString::Sanitized() const {
if (!sanitized_) {
sanitized_ = true;
sanitized_string_ = SanitizeString(data_, length_);
}
if (sanitized_string_.has_value()) {
return sanitized_string_.value();
} else {
if (data_) {
return std::string_view(data_, length_);
} else {
return "";
}
}
}
std::ostream &operator<<(std::ostream &ss, const SanitizableString &sani_string) {
ss << sani_string.Sanitized();
return ss;
}
std::optional<std::string> SanitizableString::SanitizeString(const char *str) {
return SanitizeString(str, str ? strlen(str) : 0);
}
std::optional<std::string> SanitizableString::SanitizeString(const char *str, size_t length) {
size_t strOffset = 0;
char c = 0;
std::string buf;
bool reservedStringSpace = false;
if (!str) {
return std::nullopt;
}
if (length < 1) {
return std::nullopt;
}
// Loop through the string one character at a time, looking for the characters
// we want to remove.
for (const char *p = str; (c = *p) != 0; ++p) {
if (c == '|' || c == '\n' || c == '\r') {
if (!reservedStringSpace) {
// Assume the common case won't grow the string length by more than a
// factor of 2. String will grow more if it needs to.
buf.reserve(length * 2);
reservedStringSpace = true;
}
// Copy from the last offset up to the character we just found into the buffer
ptrdiff_t diff = p - str;
buf.append(str + strOffset, diff - strOffset);
// Update the buffer and string offsets
strOffset = diff + 1;
// Replace the found character and advance the buffer offset
switch (c) {
case '|': buf.append("<pipe>"); break;
case '\n': buf.append("\\n"); break;
case '\r': buf.append("\\r"); break;
}
}
}
if (strOffset > 0 && strOffset < length) {
// Copy any characters from the last match to the end of the string into the buffer.
buf.append(str + strOffset, length - strOffset);
}
if (reservedStringSpace) {
return buf;
}
return std::nullopt;
}
} // namespace santa::santad::logs::endpoint_security::serializers

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