Compare commits

..

29 Commits

Author SHA1 Message Date
Russell Hancox
80b26955b4 GUI: Fix distributed notifications in silent mode (#936) 2022-11-16 09:53:56 -05:00
Matt W
6a84023548 Prefix tree updates (#931)
* WIP Rename SNTPrefixTree to PrefixTree

* WIP Implement the new PrefixTree and tests

* Add Unit type. Fix build and tests.

* lint

* Make NodeCount accessor for tests

* Updated comments
2022-11-14 13:16:49 +00:00
Russell Hancox
e70acefb5c Docs: Fix type of {allowed,blocked}_path_regex keys in preflight (#934) 2022-11-07 15:36:10 -05:00
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
151 changed files with 9054 additions and 1061 deletions

View File

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

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(
@@ -85,7 +93,7 @@ objc_library(
objc_library(
name = "SNTCommonEnums",
hdrs = ["SNTCommonEnums.h"],
textual_hdrs = ["SNTCommonEnums.h"],
)
objc_library(
@@ -150,12 +158,18 @@ objc_library(
deps = [":SNTConfigurator"],
)
cc_library(
name = "SNTPrefixTree",
srcs = ["SNTPrefixTree.cc"],
hdrs = ["SNTPrefixTree.h"],
copts = ["-std=c++11"],
deps = [":SNTLogging"],
objc_library(
name = "PrefixTree",
hdrs = ["PrefixTree.h"],
deps = [
":SNTLogging",
"@com_google_absl//absl/synchronization",
],
)
objc_library(
name = "Unit",
hdrs = ["Unit.h"],
)
objc_library(
@@ -299,9 +313,9 @@ santa_unit_test(
)
santa_unit_test(
name = "SNTPrefixTreeTest",
srcs = ["SNTPrefixTreeTest.mm"],
deps = [":SNTPrefixTree"],
name = "PrefixTreeTest",
srcs = ["PrefixTreeTest.mm"],
deps = [":PrefixTree"],
)
santa_unit_test(
@@ -323,11 +337,11 @@ santa_unit_test(
test_suite(
name = "unit_tests",
tests = [
":PrefixTreeTest",
":SNTCachedDecisionTest",
":SNTFileInfoTest",
":SNTKVOManagerTest",
":SNTMetricSetTest",
":SNTPrefixTreeTest",
":SNTRuleTest",
":SantaCacheTest",
],

298
Source/common/PrefixTree.h Normal file
View File

@@ -0,0 +1,298 @@
/// 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__PREFIXTREE_H
#define SANTA__COMMON__PREFIXTREE_H
#include <sys/syslimits.h>
#include <optional>
#import "Source/common/SNTLogging.h"
#include "absl/synchronization/mutex.h"
#if SANTA_PREFIX_TREE_DEBUG
#define DEBUG_LOG LOGD
#else
#define DEBUG_LOG(format, ...) // NOP
#endif
namespace santa::common {
template <typename ValueT>
class PrefixTree {
private:
// Forward declaration
enum class NodeType;
class TreeNode;
public:
PrefixTree(uint32_t max_depth = PATH_MAX)
: root_(new TreeNode()), max_depth_(max_depth), node_count_(0) {}
~PrefixTree() { PruneLocked(root_); }
bool InsertPrefix(const char *s, ValueT value) {
absl::MutexLock lock(&lock_);
return InsertLocked(s, value, NodeType::kPrefix);
}
bool InsertLiteral(const char *s, ValueT value) {
absl::MutexLock lock(&lock_);
return InsertLocked(s, value, NodeType::kLiteral);
}
bool HasPrefix(const char *input) {
absl::ReaderMutexLock lock(&lock_);
return HasPrefixLocked(input);
}
std::optional<ValueT> LookupLongestMatchingPrefix(const char *input) {
absl::ReaderMutexLock lock(&lock_);
return LookupLongestMatchingPrefixLocked(input);
}
void Reset() {
absl::MutexLock lock(&lock_);
PruneLocked(root_);
root_ = new TreeNode();
node_count_ = 0;
}
#if SANTA_PREFIX_TREE_DEBUG
void Print() {
char buf[max_depth_ + 1];
memset(buf, 0, sizeof(buf));
absl::ReaderMutexLock lock(&lock_);
PrintLocked(root_, buf, 0);
}
uint32_t NodeCount() {
absl::ReaderMutexLock lock(&lock_);
return node_count_;
}
#endif
private:
ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_)
bool InsertLocked(const char *input, ValueT value, NodeType node_type) {
const char *p = input;
TreeNode *node = root_;
while (*p) {
uint8_t cur_byte = (uint8_t)*p;
TreeNode *child_node = node->children_[cur_byte];
if (!child_node) {
// Current node doesn't exist...
// Create the rest of the nodes in the tree for the given string
// Keep a pointer to where this new branch starts from. If the
// input length exceeds max_depth, the new branch will need to
// be pruned.
TreeNode *branch_start_node = node;
uint8_t branch_start_byte = (uint8_t)*p;
do {
TreeNode *new_node = new TreeNode();
node->children_[cur_byte] = new_node;
node = new_node;
node_count_++;
// Check current depth...
if (p - input >= max_depth_) {
// Attempted to add a string that exceeded max depth
// Prune tree from start of this new branch
PruneLocked(branch_start_node->children_[branch_start_byte]);
branch_start_node->children_[branch_start_byte] = nullptr;
return false;
}
cur_byte = (uint8_t) * ++p;
} while (*p);
node->node_type_ = node_type;
node->value_ = value;
return true;
} else if (*(p + 1) == '\0') {
// Current node exists and we're at the end of our input...
// Note: The current node's data will be overwritten
// Only increment node count if the previous node type wasn't already a
// prefix or literal type (in which case it was already counted)
if (child_node->node_type_ == NodeType::kInner) {
node_count_++;
}
child_node->node_type_ = node_type;
child_node->value_ = value;
return true;
}
node = child_node;
p++;
}
// Should only get here when input is an empty string
return false;
}
ABSL_SHARED_LOCKS_REQUIRED(lock_)
bool HasPrefixLocked(const char *input) {
TreeNode *node = root_;
const char *p = input;
while (*p) {
node = node->children_[(uint8_t)*p++];
if (!node) {
break;
}
if (node->node_type_ == NodeType::kPrefix ||
(*p == '\0' && node->node_type_ == NodeType::kLiteral)) {
return true;
}
}
return false;
}
ABSL_SHARED_LOCKS_REQUIRED(lock_)
std::optional<ValueT> LookupLongestMatchingPrefixLocked(const char *input) {
TreeNode *node = root_;
TreeNode *match = nullptr;
const char *p = input;
while (*p) {
node = node->children_[(uint8_t)*p++];
if (!node) {
break;
}
if (node->node_type_ == NodeType::kPrefix ||
(*p == '\0' && node->node_type_ == NodeType::kLiteral)) {
match = node;
}
}
return match ? std::make_optional<ValueT>(match->value_) : std::nullopt;
}
ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_)
void PruneLocked(TreeNode *target) {
if (!target) {
return;
}
// For deep trees, a recursive approach will generate too many stack frames.
// Since the depth of the tree is configurable, err on the side of caution
// and use a "stack" to walk the tree in a non-recursive manner.
TreeNode **stack = new TreeNode *[node_count_ + 1];
if (!stack) {
LOGE(@"Unable to prune tree!");
return;
}
uint32_t count = 0;
// Seed the "stack" with a starting node.
stack[count++] = target;
// Start at the target node and walk the tree to find and delete all the
// sub-nodes.
while (count) {
TreeNode *node = stack[--count];
for (int i = 0; i < 256; ++i) {
if (!node->children_[i]) {
continue;
}
stack[count++] = node->children_[i];
}
delete node;
--node_count_;
}
delete[] stack;
}
#if SANTA_PREFIX_TREE_DEBUG
ABSL_SHARED_LOCKS_REQUIRED(lock_)
void PrintLocked(TreeNode *node, char *buf, uint32_t depth) {
for (size_t i = 0; i < 256; i++) {
TreeNode *cur_node = node->children_[i];
if (cur_node) {
buf[depth] = i;
if (cur_node->node_type_ != NodeType::kInner) {
printf("\t%s (type: %s)\n", buf,
cur_node->node_type_ == NodeType::kPrefix ? "prefix" : "literal");
}
PrintLocked(cur_node, buf, depth + 1);
buf[depth] = '\0';
}
}
}
#endif
enum class NodeType {
kInner = 0,
kPrefix,
kLiteral,
};
///
/// TreeNode is a wrapper class that represents one byte.
/// 1 node can represent a whole ASCII character.
/// For example a pointer to the 'A' node will be stored at children[0x41].
/// It takes 1-4 nodes to represent a UTF-8 encoded Unicode character.
///
/// The path for "/🤘" would look like this:
/// children[0x2f] -> children[0xf0] -> children[0x9f] -> children[0xa4]
/// -> children[0x98]
///
/// The path for "/dev" is:
/// children[0x2f] -> children[0x64] -> children[0x65] -> children[0x76]
///
/// Lookups of children are O(1).
///
/// Having the nodes represented by a smaller width, such as a nibble (1/2
/// byte), would drastically decrease the memory footprint but would double
/// required dereferences.
///
/// TODO(bur): Potentially convert this into a full on radix tree.
///
class TreeNode {
public:
TreeNode() : children_(), node_type_(NodeType::kInner) {}
~TreeNode() = default;
TreeNode *children_[256];
PrefixTree::NodeType node_type_;
ValueT value_;
};
TreeNode *root_;
const uint32_t max_depth_;
uint32_t node_count_ ABSL_GUARDED_BY(lock_);
absl::Mutex lock_;
};
} // namespace santa::common
#endif

View File

@@ -0,0 +1,224 @@
/// 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>
#define SANTA_PREFIX_TREE_DEBUG 1
#include "Source/common/PrefixTree.h"
using santa::common::PrefixTree;
@interface PrefixTreeTest : XCTestCase
@end
@implementation PrefixTreeTest
- (void)testBasic {
PrefixTree<int> tree;
XCTAssertFalse(tree.HasPrefix("/foo/bar/baz"));
XCTAssertFalse(tree.HasPrefix("/foo/bar.txt"));
XCTAssertFalse(tree.HasPrefix("/baz"));
XCTAssertTrue(tree.InsertPrefix("/foo", 12));
XCTAssertTrue(tree.InsertPrefix("/bar", 34));
XCTAssertTrue(tree.InsertLiteral("/foo/bar", 56));
// Re-inserting something that exists is allowed
XCTAssertTrue(tree.InsertLiteral("/foo", 78));
XCTAssertTrue(tree.InsertPrefix("/foo", 56));
XCTAssertTrue(tree.HasPrefix("/foo/bar/baz"));
XCTAssertTrue(tree.HasPrefix("/foo/bar.txt"));
XCTAssertFalse(tree.HasPrefix("/baz"));
// Empty strings are not supported
XCTAssertFalse(tree.InsertLiteral("", 0));
XCTAssertFalse(tree.InsertPrefix("", 0));
}
- (void)testHasPrefix {
PrefixTree<int> tree;
XCTAssertTrue(tree.InsertPrefix("/foo", 0));
XCTAssertTrue(tree.InsertLiteral("/bar", 0));
XCTAssertTrue(tree.InsertLiteral("/baz", 0));
XCTAssertTrue(tree.InsertLiteral("/qaz", 0));
// Check that a tree with a matching prefix is successful
XCTAssertTrue(tree.HasPrefix("/foo.txt"));
// This shouldn't succeed because `/bar` `/baz` and `qaz` are literals
XCTAssertFalse(tree.HasPrefix("/bar.txt"));
XCTAssertFalse(tree.HasPrefix("/baz.txt"));
XCTAssertFalse(tree.HasPrefix("/qaz.txt"));
// Now change `/bar` to a prefix type and retest HasPrefix
// `/bar.txt` should now succeed, but `/baz.txt` should still not pass
XCTAssertTrue(tree.InsertPrefix("/bar", 0));
XCTAssertTrue(tree.HasPrefix("/bar.txt"));
XCTAssertFalse(tree.HasPrefix("/baz.txt"));
XCTAssertFalse(tree.HasPrefix("/qaz.txt"));
// Insert a new prefix string to allow `/baz.txt` to have a valid prefix
XCTAssertTrue(tree.InsertPrefix("/b", 0));
XCTAssertTrue(tree.HasPrefix("/baz.txt"));
XCTAssertFalse(tree.HasPrefix("/qaz.txt"));
// An exact match on a literal allows HasPrefix to succeed
XCTAssertTrue(tree.InsertLiteral("/qaz.txt", 0));
XCTAssertTrue(tree.HasPrefix("/qaz.txt"));
}
- (void)testLookupLongestMatchingPrefix {
PrefixTree<int> tree;
XCTAssertTrue(tree.InsertPrefix("/foo", 12));
XCTAssertTrue(tree.InsertPrefix("/bar", 34));
XCTAssertTrue(tree.InsertPrefix("/foo/bar.txt", 56));
std::optional<int> value;
// Matching exact prefix
value = tree.LookupLongestMatchingPrefix("/foo");
XCTAssertEqual(value.value_or(0), 12);
// Ensure changing node type works as expected
// Literals must match exactly.
value = tree.LookupLongestMatchingPrefix("/foo/bar.txt.tmp");
XCTAssertEqual(value.value_or(0), 56);
XCTAssertTrue(tree.InsertLiteral("/foo/bar.txt", 90));
value = tree.LookupLongestMatchingPrefix("/foo/bar.txt.tmp");
XCTAssertEqual(value.value_or(0), 12);
// Inserting over an exiting node returns the new value
XCTAssertTrue(tree.InsertPrefix("/foo", 78));
value = tree.LookupLongestMatchingPrefix("/foo");
XCTAssertEqual(value.value_or(0), 78);
// No matching prefix
value = tree.LookupLongestMatchingPrefix("/asdf");
XCTAssertEqual(value.value_or(0), 0);
}
- (void)testNodeCounts {
const uint32_t maxDepth = 100;
PrefixTree<int> tree(100);
XCTAssertEqual(tree.NodeCount(), 0);
// Start with a small string
XCTAssertTrue(tree.InsertPrefix("asdf", 0));
XCTAssertEqual(tree.NodeCount(), 4);
// Add a couple more characters to the existing string
XCTAssertTrue(tree.InsertPrefix("asdfgh", 0));
XCTAssertEqual(tree.NodeCount(), 6);
// Inserting a string that exceeds max depth doesn't increase node count
XCTAssertFalse(tree.InsertPrefix(std::string(maxDepth + 10, 'A').c_str(), 0));
XCTAssertEqual(tree.NodeCount(), 6);
// Add a new string that is a prefix of an existing string
// This should increment the count by one since a new terminal node exists
XCTAssertTrue(tree.InsertPrefix("as", 0));
XCTAssertEqual(tree.NodeCount(), 7);
// Re-inserting onto an existing node shouldn't modify the count
tree.InsertLiteral("as", 0);
tree.InsertPrefix("as", 0);
XCTAssertEqual(tree.NodeCount(), 7);
}
- (void)testReset {
// Ensure resetting a tree removes all content
PrefixTree<int> tree;
tree.Reset();
XCTAssertEqual(tree.NodeCount(), 0);
XCTAssertTrue(tree.InsertPrefix("asdf", 0));
XCTAssertTrue(tree.InsertPrefix("qwerty", 0));
XCTAssertTrue(tree.HasPrefix("asdf"));
XCTAssertTrue(tree.HasPrefix("qwerty"));
XCTAssertEqual(tree.NodeCount(), 10);
tree.Reset();
XCTAssertFalse(tree.HasPrefix("asdf"));
XCTAssertFalse(tree.HasPrefix("qwerty"));
XCTAssertEqual(tree.NodeCount(), 0);
}
- (void)testComplexValues {
class Foo {
public:
Foo(int x) : x_(x) {}
int X() { return x_; }
private:
int x_;
};
PrefixTree<std::shared_ptr<Foo>> tree;
XCTAssertTrue(tree.InsertPrefix("foo", std::make_shared<Foo>(123)));
XCTAssertTrue(tree.InsertPrefix("bar", std::make_shared<Foo>(456)));
std::optional<std::shared_ptr<Foo>> value;
value = tree.LookupLongestMatchingPrefix("foo");
XCTAssertTrue(value.has_value() && value->get()->X() == 123);
value = tree.LookupLongestMatchingPrefix("bar");
XCTAssertTrue(value.has_value() && value->get()->X() == 456);
value = tree.LookupLongestMatchingPrefix("asdf");
XCTAssertFalse(value.has_value());
}
- (void)testThreading {
uint32_t count = 4096;
auto t = new PrefixTree<int>(count * (uint32_t)[NSUUID UUID].UUIDString.length);
NSMutableArray *UUIDs = [NSMutableArray arrayWithCapacity:count];
for (int i = 0; i < count; ++i) {
[UUIDs addObject:[NSUUID UUID].UUIDString];
}
__block BOOL stop = NO;
// Create a bunch of background noise.
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (uint64_t i = 0; i < UINT64_MAX; ++i) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
t->HasPrefix([UUIDs[i % count] UTF8String]);
});
if (stop) return;
}
});
// Fill up the tree.
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
XCTAssertEqual(t->InsertPrefix([UUIDs[i] UTF8String], 0), true);
});
// Make sure every leaf byte is found.
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
XCTAssertTrue(t->HasPrefix([UUIDs[i] UTF8String]));
});
stop = YES;
}
@end

View File

@@ -24,7 +24,7 @@
- (void)testSNTCachedDecisionInit {
// Ensure the vnodeId field is properly set from the es_file_t
struct stat sb = MakeStat(1234, 5678);
struct stat sb = MakeStat();
es_file_t file = MakeESFile("foo", sb);
SNTCachedDecision *cd = [[SNTCachedDecision alloc] initWithEndpointSecurityFile:&file];

View File

@@ -183,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.
///
@@ -203,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
@@ -379,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

@@ -88,10 +88,10 @@ 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";
@@ -172,7 +172,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kRemountUSBModeKey : array,
kEnablePageZeroProtectionKey : number,
kEnableBadSignatureProtectionKey : number,
kEnableSilentModeKey : string,
kEnableSilentModeKey : number,
kAboutTextKey : string,
kMoreInfoURLKey : string,
kEventDetailURLKey : string,
@@ -200,10 +200,10 @@ 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,
kEnableForkAndExitLogging : number,
kIgnoreOtherEndpointSecurityClients : number,
@@ -391,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];
}
@@ -468,15 +468,15 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
}
+ (NSSet *)keyPathsForValuesAffectingRemountUSBMode {
return [self configStateSet];
return [self syncAndConfigStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingRemountUSBBlockMessage {
return [self syncAndConfigStateSet];
return [self configStateSet];
}
+ (NSSet *)keyPathsForValuesAffectingUsbBlockMessage {
return [self syncAndConfigStateSet];
return [self configStateSet];
}
#pragma mark Public Interface
@@ -577,7 +577,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
}
- (NSArray<NSString *> *)remountUSBMode {
NSArray<NSString *> *args = self.configState[kRemountUSBModeKey];
NSArray<NSString *> *args = self.syncState[kRemountUSBModeKey];
if (!args) {
args = (NSArray<NSString *> *)self.configState[kRemountUSBModeKey];
}
for (id arg in args) {
if (![arg isKindOfClass:[NSString class]]) {
return nil;
@@ -760,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 {
@@ -855,8 +858,10 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
}
- (BOOL)blockUSBMount {
NSNumber *number = self.configState[kBlockUSBMountKey];
return number ? [number boolValue] : NO;
NSNumber *n = self.syncState[kBlockUSBMountKey];
if (n) return [n boolValue];
return [self.configState[kBlockUSBMountKey] boolValue];
}
///

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,227 +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.
#include "Source/common/SNTPrefixTree.h"
#include <string.h>
#include <mutex>
#define LOGD(format, ...) // NOP
#define LOGE(format, ...) // NOP
#define lck_rw_lock_shared(l) pthread_rwlock_rdlock(&l)
#define lck_rw_unlock_shared(l) pthread_rwlock_unlock(&l)
#define lck_rw_lock_exclusive(l) pthread_rwlock_wrlock(&l)
#define lck_rw_unlock_exclusive(l) pthread_rwlock_unlock(&l)
#define lck_rw_lock_shared_to_exclusive(l) \
({ \
pthread_rwlock_unlock(&l); \
false; \
})
#define lck_rw_lock_exclusive_to_shared(l) \
({ \
pthread_rwlock_unlock(&l); \
pthread_rwlock_rdlock(&l); \
})
#define lck_mtx_lock(l) l->lock()
#define lck_mtx_unlock(l) l->unlock()
SNTPrefixTree::SNTPrefixTree(uint32_t max_nodes) {
root_ = new SantaPrefixNode();
node_count_ = 0;
max_nodes_ = max_nodes;
pthread_rwlock_init(&spt_lock_, nullptr);
spt_add_lock_ = new std::mutex;
}
IOReturn SNTPrefixTree::AddPrefix(const char *prefix, uint64_t *node_count) {
// Serialize requests to AddPrefix. Otherwise one AddPrefix thread could
// overwrite whole branches of another. HasPrefix is still free to read the
// tree, until AddPrefix needs to modify it.
lck_mtx_lock(spt_add_lock_);
// Don't allow an empty prefix.
if (prefix[0] == '\0') return kIOReturnBadArgument;
LOGD("Trying to add prefix: %s", prefix);
// Enforce max tree depth.
size_t len = strnlen(prefix, max_nodes_);
// Grab a shared lock until a new branch is required.
lck_rw_lock_shared(spt_lock_);
SantaPrefixNode *node = root_;
for (size_t i = 0; i < len; ++i) {
// If there is a node in the path that is considered a prefix, stop adding.
// For our purposes we only care about the shortest path that matches.
if (node->isPrefix) break;
// Only process a byte at a time.
uint8_t value = (uint8_t)prefix[i];
// Create the child if it does not exist.
if (!node->children[value]) {
// Upgrade the shared lock.
// If the upgrade fails, the shared lock is released.
if (!lck_rw_lock_shared_to_exclusive(spt_lock_)) {
// Grab a new exclusive lock.
lck_rw_lock_exclusive(spt_lock_);
}
// Is there enough room for the rest of the prefix?
if ((node_count_ + (len - i)) > max_nodes_) {
LOGE("Prefix tree is full, can not add: %s", prefix);
if (node_count) *node_count = node_count_;
lck_rw_unlock_exclusive(spt_lock_);
lck_mtx_unlock(spt_add_lock_);
return kIOReturnNoResources;
}
// Create the rest of the prefix.
while (i < len) {
value = (uint8_t)prefix[i++];
SantaPrefixNode *new_node = new SantaPrefixNode();
node->children[value] = new_node;
++node_count_;
node = new_node;
}
// This is the end, mark the node as a prefix.
LOGD("Added prefix: %s", prefix);
node->isPrefix = true;
// Downgrade the exclusive lock
lck_rw_lock_exclusive_to_shared(spt_lock_);
} else if (i + 1 == len) {
// If the child does exist and it is the end...
// Set the new, higher prefix and prune the now dead nodes.
if (!lck_rw_lock_shared_to_exclusive(spt_lock_)) {
lck_rw_lock_exclusive(spt_lock_);
}
PruneNode(node->children[value]);
SantaPrefixNode *new_node = new SantaPrefixNode();
new_node->isPrefix = true;
node->children[value] = new_node;
++node_count_;
LOGD("Added prefix: %s", prefix);
lck_rw_lock_exclusive_to_shared(spt_lock_);
}
// Get ready for the next iteration.
node = node->children[value];
}
if (node_count) *node_count = node_count_;
lck_rw_unlock_shared(spt_lock_);
lck_mtx_unlock(spt_add_lock_);
return kIOReturnSuccess;
}
bool SNTPrefixTree::HasPrefix(const char *string) {
lck_rw_lock_shared(spt_lock_);
auto found = false;
SantaPrefixNode *node = root_;
// A well formed tree will always break this loop. Even if string doesn't
// terminate.
const char *p = string;
while (*p) {
// Only process a byte at a time.
node = node->children[(uint8_t)*p++];
// If it doesn't exist in the tree, no match.
if (!node) break;
// If it does exist, is it a prefix?
if (node->isPrefix) {
found = true;
break;
}
}
lck_rw_unlock_shared(spt_lock_);
return found;
}
void SNTPrefixTree::Reset() {
lck_rw_lock_exclusive(spt_lock_);
PruneNode(root_);
root_ = new SantaPrefixNode();
node_count_ = 0;
lck_rw_unlock_exclusive(spt_lock_);
}
void SNTPrefixTree::PruneNode(SantaPrefixNode *target) {
if (!target) return;
// For deep trees, a recursive approach will generate too many stack frames.
// Make a "stack" and walk the tree.
auto stack = new SantaPrefixNode *[node_count_ + 1];
if (!stack) {
LOGE("Unable to prune tree!");
return;
}
auto count = 0;
// Seed the "stack" with a starting node.
stack[count++] = target;
// Start at the target node and walk the tree to find and delete all the
// sub-nodes.
while (count) {
auto node = stack[--count];
for (int i = 0; i < 256; ++i) {
if (!node->children[i]) continue;
stack[count++] = node->children[i];
}
delete node;
--node_count_;
}
delete[] stack;
}
SNTPrefixTree::~SNTPrefixTree() {
lck_rw_lock_exclusive(spt_lock_);
PruneNode(root_);
root_ = nullptr;
lck_rw_unlock_exclusive(spt_lock_);
pthread_rwlock_destroy(&spt_lock_);
}

View File

@@ -1,91 +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.
#ifndef SANTA__SANTA_DRIVER__SANTAPREFIXTREE_H
#define SANTA__SANTA_DRIVER__SANTAPREFIXTREE_H
#include <IOKit/IOReturn.h>
#include <sys/param.h>
// Support for unit testing.
#include <pthread.h>
#include <stdint.h>
#include <mutex>
///
/// SantaPrefixTree is a simple prefix tree implementation.
/// Operations are thread safe.
///
class SNTPrefixTree {
public:
// Add a prefix to the tree.
// Optionally pass node_count to get the number of nodes after the add.
IOReturn AddPrefix(const char *, uint64_t *node_count = nullptr);
// Check if the tree has a prefix for string.
bool HasPrefix(const char *string);
// Reset the tree.
void Reset();
SNTPrefixTree(uint32_t max_nodes = kDefaultMaxNodes);
~SNTPrefixTree();
private:
///
/// SantaPrefixNode is a wrapper class that represents one byte.
/// 1 node can represent a whole ASCII character.
/// For example a pointer to the 'A' node will be stored at children[0x41].
/// It takes 1-4 nodes to represent a UTF-8 encoded Unicode character.
///
/// The path for "/🤘" would look like this:
/// children[0x2f] -> children[0xf0] -> children[0x9f] -> children[0xa4]
/// -> children[0x98]
///
/// The path for "/dev" is:
/// children[0x2f] -> children[0x64] -> children[0x65] -> children[0x76]
///
/// Lookups of children are O(1).
///
/// Having the nodes represented by a smaller width, such as a nibble (1/2
/// byte), would drastically decrease the memory footprint but would double
/// required dereferences.
///
/// TODO(bur): Potentially convert this into a full on radix tree.
///
class SantaPrefixNode {
public:
bool isPrefix;
SantaPrefixNode *children[256];
};
// PruneNode will remove the passed in node from the tree.
// The passed in node and all subnodes will be deleted.
// It is the caller's responsibility to reset the pointer to this node (held
// by the parent). If the tree is in use grab the exclusive lock.
void PruneNode(SantaPrefixNode *);
SantaPrefixNode *root_;
// Each node takes up ~2k, assuming MAXPATHLEN is 1024 max out at ~2MB.
static const uint32_t kDefaultMaxNodes = MAXPATHLEN;
uint32_t max_nodes_;
uint32_t node_count_;
pthread_rwlock_t spt_lock_;
std::mutex *spt_add_lock_;
};
#endif /* SANTA__SANTA_DRIVER__SANTAPREFIXTREE_H */

View File

@@ -1,73 +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.
#import <XCTest/XCTest.h>
#include "Source/common/SNTPrefixTree.h"
@interface SNTPrefixTreeTest : XCTestCase
@end
@implementation SNTPrefixTreeTest
- (void)testAddAndHas {
auto t = SNTPrefixTree();
XCTAssertFalse(t.HasPrefix("/private/var/tmp/file1"));
t.AddPrefix("/private/var/tmp/");
XCTAssertTrue(t.HasPrefix("/private/var/tmp/file1"));
}
- (void)testReset {
auto t = SNTPrefixTree();
t.AddPrefix("/private/var/tmp/");
XCTAssertTrue(t.HasPrefix("/private/var/tmp/file1"));
t.Reset();
XCTAssertFalse(t.HasPrefix("/private/var/tmp/file1"));
}
- (void)testThreading {
uint32_t count = 4096;
auto t = new SNTPrefixTree(count * (uint32_t)[NSUUID UUID].UUIDString.length);
NSMutableArray *UUIDs = [NSMutableArray arrayWithCapacity:count];
for (int i = 0; i < count; ++i) {
[UUIDs addObject:[NSUUID UUID].UUIDString];
}
__block BOOL stop = NO;
// Create a bunch of background noise.
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (uint64_t i = 0; i < UINT64_MAX; ++i) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
t->HasPrefix([UUIDs[i % count] UTF8String]);
});
if (stop) return;
}
});
// Fill up the tree.
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
XCTAssertEqual(t->AddPrefix([UUIDs[i] UTF8String]), kIOReturnSuccess);
});
// Make sure every leaf byte is found.
dispatch_apply(count, dispatch_get_global_queue(0, 0), ^(size_t i) {
XCTAssertTrue(t->HasPrefix([UUIDs[i] UTF8String]));
});
stop = YES;
}
@end

View File

@@ -23,7 +23,7 @@
#include <sys/stat.h>
#define NOBODY_UID ((unsigned int)-2)
#define NOBODY_GID ((unsigned int)-2)
#define NOGROUP_GID ((unsigned int)-1)
// Bubble up googletest expectation failures to XCTest failures
#define XCTBubbleMockVerifyAndClearExpectations(mock) \
@@ -38,6 +38,10 @@
// 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);
@@ -47,9 +51,18 @@ enum class ActionType {
Notify,
};
//
// Helpers to construct various ES structs
//
audit_token_t MakeAuditToken(pid_t pid, pid_t pidver);
struct stat MakeStat(ino_t ino, dev_t devno = 0);
/// 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 = {});
@@ -57,4 +70,6 @@ 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

View File

@@ -18,6 +18,7 @@
#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{
@@ -25,9 +26,9 @@ audit_token_t MakeAuditToken(pid_t pid, pid_t pidver) {
{
0,
NOBODY_UID,
NOBODY_GID,
NOGROUP_GID,
NOBODY_UID,
NOBODY_GID,
NOGROUP_GID,
(unsigned int)pid,
0,
(unsigned int)pidver,
@@ -35,10 +36,24 @@ audit_token_t MakeAuditToken(pid_t pid, pid_t pidver) {
};
}
struct stat MakeStat(ino_t ino, dev_t devno) {
struct stat MakeStat(int offset) {
return (struct stat){
.st_dev = devno,
.st_ino = ino,
.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),
};
}
@@ -62,6 +77,10 @@ es_process_t MakeESProcess(es_file_t *file, audit_token_t tok, audit_token_t par
.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,
};
@@ -85,15 +104,34 @@ static uint64_t AddMillisToMachTime(uint64_t ms, uint64_t 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) {
return es_message_t{
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) {

24
Source/common/Unit.h Normal file
View File

@@ -0,0 +1,24 @@
/// 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__UNIT_H
#define SANTA__COMMON__UNIT_H
namespace santa::common {
struct Unit {};
} // namespace santa::common
#endif

View File

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

@@ -92,10 +92,16 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
}
- (void)queueMessage:(SNTMessageWindowController *)pendingMsg {
NSString *messageHash = [pendingMsg messageHash];
// Post a distributed notification, regardless of queue state.
[self postDistributedNotification:pendingMsg];
// If GUI is in silent mode or if there's already a notification queued for
// this message, don't do anything else.
if ([SNTConfigurator configurator].enableSilentMode) return;
if ([self notificationAlreadyQueued:pendingMsg]) return;
// See if this message is silenced.
// See if this message has been user-silenced.
NSString *messageHash = [pendingMsg messageHash];
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSDate *silenceDate = [ud objectForKey:silencedNotificationsKey][messageHash];
if ([silenceDate isKindOfClass:[NSDate class]]) {
@@ -114,7 +120,6 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
pendingMsg.delegate = self;
[self.pendingNotifications addObject:pendingMsg];
[self postDistributedNotification:pendingMsg];
if (!self.currentWindowController) {
[self showQueuedWindow];
@@ -315,8 +320,6 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
}
- (void)postBlockNotification:(SNTStoredEvent *)event withCustomMessage:(NSString *)message {
if ([SNTConfigurator configurator].enableSilentMode) return;
if (!event) {
LOGI(@"Error: Missing event object in message received from daemon!");
return;
@@ -329,8 +332,6 @@ static NSString *const silencedNotificationsKey = @"SilencedNotifications";
}
- (void)postUSBBlockNotification:(SNTDeviceEvent *)event withCustomMessage:(NSString *)message {
if ([SNTConfigurator configurator].enableSilentMode) return;
if (!event) {
LOGI(@"Error: Missing event object in message received from daemon!");
return;

View File

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

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

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

View File

@@ -62,6 +62,7 @@ objc_library(
hdrs = ["EventProviders/SNTEndpointSecurityEventHandler.h"],
deps = [
":EndpointSecurityMessage",
":Metrics",
"//Source/common:SNTCommon",
],
)
@@ -208,6 +209,7 @@ objc_library(
":EndpointSecurityClient",
":EndpointSecurityEnrichedTypes",
":EndpointSecurityMessage",
":Metrics",
],
)
@@ -220,6 +222,7 @@ objc_library(
":EndpointSecurityClient",
":EndpointSecurityEnrichedTypes",
":EndpointSecurityMessage",
":Metrics",
":SNTEndpointSecurityClientBase",
"//Source/common:SNTCommon",
"//Source/common:SNTConfigurator",
@@ -238,11 +241,13 @@ objc_library(
":EndpointSecurityEnricher",
":EndpointSecurityLogger",
":EndpointSecurityMessage",
":Metrics",
":SNTCompilerController",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
"//Source/common:PrefixTree",
"//Source/common:SNTLogging",
"//Source/common:SNTPrefixTree",
"//Source/common:Unit",
],
)
@@ -254,6 +259,7 @@ objc_library(
":EndpointSecurityAPI",
":EndpointSecurityLogger",
":EndpointSecurityMessage",
":Metrics",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
"//Source/common:SNTLogging",
@@ -270,6 +276,7 @@ objc_library(
":EndpointSecurityEnrichedTypes",
":EndpointSecurityEnricher",
":EndpointSecurityMessage",
":Metrics",
":SNTCompilerController",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
@@ -287,6 +294,7 @@ objc_library(
":EndpointSecurityAPI",
":EndpointSecurityLogger",
":EndpointSecurityMessage",
":Metrics",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
"//Source/common:SNTDeviceEvent",
@@ -330,6 +338,21 @@ objc_library(
name = "EndpointSecuritySerializer",
srcs = ["Logs/EndpointSecurity/Serializers/Serializer.mm"],
hdrs = ["Logs/EndpointSecurity/Serializers/Serializer.h"],
deps = [
":EndpointSecurityEnrichedTypes",
":EndpointSecurityMessage",
":SNTDecisionCache",
"//Source/common:SNTConfigurator",
],
)
objc_library(
name = "EndpointSecuritySerializerUtilities",
srcs = ["Logs/EndpointSecurity/Serializers/Utilities.mm"],
hdrs = ["Logs/EndpointSecurity/Serializers/Utilities.h"],
sdk_dylibs = [
"bsm",
],
deps = [
":EndpointSecurityEnrichedTypes",
":EndpointSecurityMessage",
@@ -366,6 +389,7 @@ objc_library(
":EndpointSecurityAPI",
":EndpointSecuritySanitizableString",
":EndpointSecuritySerializer",
":EndpointSecuritySerializerUtilities",
":SNTDecisionCache",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTConfigurator",
@@ -374,6 +398,23 @@ objc_library(
],
)
objc_library(
name = "EndpointSecuritySerializerProtobuf",
srcs = ["Logs/EndpointSecurity/Serializers/Protobuf.mm"],
hdrs = ["Logs/EndpointSecurity/Serializers/Protobuf.h"],
deps = [
":EndpointSecurityAPI",
":EndpointSecuritySerializer",
":EndpointSecuritySerializerUtilities",
":SNTDecisionCache",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:SNTStoredEvent",
"//Source/common:santa_cc_proto_library_wrapper",
],
)
objc_library(
name = "EndpointSecurityWriter",
hdrs = ["Logs/EndpointSecurity/Writers/Writer.h"],
@@ -397,6 +438,20 @@ objc_library(
],
)
objc_library(
name = "EndpointSecurityWriterSpool",
srcs = ["Logs/EndpointSecurity/Writers/Spool.mm"],
hdrs = ["Logs/EndpointSecurity/Writers/Spool.h"],
deps = [
":EndpointSecurityWriter",
"//Source/common:SNTLogging",
"//Source/common:santa_cc_proto_library_wrapper",
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:fsspool",
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:fsspool_log_batch_writer",
"@com_google_absl//absl/strings",
],
)
objc_library(
name = "EndpointSecurityWriterNull",
srcs = ["Logs/EndpointSecurity/Writers/Null.mm"],
@@ -417,9 +472,11 @@ objc_library(
":EndpointSecuritySerializer",
":EndpointSecuritySerializerBasicString",
":EndpointSecuritySerializerEmpty",
":EndpointSecuritySerializerProtobuf",
":EndpointSecurityWriter",
":EndpointSecurityWriterFile",
":EndpointSecurityWriterNull",
":EndpointSecurityWriterSpool",
":EndpointSecurityWriterSyslog",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTLogging",
@@ -515,13 +572,14 @@ objc_library(
":SNTExecutionController",
":SNTNotificationQueue",
":SNTSyncdQueue",
"//Source/common:PrefixTree",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTKVOManager",
"//Source/common:SNTLogging",
"//Source/common:SNTPrefixTree",
"//Source/common:SNTXPCNotifierInterface",
"//Source/common:SNTXPCSyncServiceInterface",
"//Source/common:Unit",
"@MOLXPCConnection",
],
)
@@ -543,10 +601,13 @@ objc_library(
":SNTNotificationQueue",
":SNTRuleTable",
":SNTSyncdQueue",
"//Source/common:PrefixTree",
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:SNTPrefixTree",
"//Source/common:SNTMetricSet",
"//Source/common:SNTXPCControlInterface",
"//Source/common:SNTXPCUnprivilegedControlInterface",
"//Source/common:Unit",
"@MOLXPCConnection",
],
)
@@ -567,6 +628,7 @@ objc_library(
":SantadDeps",
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
"//Source/common:SNTXPCControlInterface",
],
)
@@ -677,6 +739,7 @@ santa_unit_test(
tags = ["exclusive"],
deps = [
":EndpointSecurityMessage",
":Metrics",
":MockEndpointSecurityAPI",
":SNTDatabaseController",
":SNTEndpointSecurityAuthorizer",
@@ -720,6 +783,19 @@ santa_unit_test(
],
)
santa_unit_test(
name = "EndpointSecuritySerializerUtilitiesTest",
srcs = ["Logs/EndpointSecurity/Serializers/UtilitiesTest.mm"],
deps = [
":EndpointSecurityMessage",
":EndpointSecuritySerializerUtilities",
":MockEndpointSecurityAPI",
"//Source/common:TestUtils",
"@OCMock",
"@com_google_googletest//:gtest",
],
)
santa_unit_test(
name = "EndpointSecuritySerializerBasicStringTest",
srcs = ["Logs/EndpointSecurity/Serializers/BasicStringTest.mm"],
@@ -745,6 +821,31 @@ santa_unit_test(
],
)
santa_unit_test(
name = "EndpointSecuritySerializerProtobufTest",
srcs = ["Logs/EndpointSecurity/Serializers/ProtobufTest.mm"],
data = [
"//Source/santad/testdata:protobuf_json_testdata",
],
deps = [
":EndpointSecurityEnrichedTypes",
":EndpointSecurityEnricher",
":EndpointSecurityMessage",
":EndpointSecuritySerializer",
":EndpointSecuritySerializerProtobuf",
":MockEndpointSecurityAPI",
":SNTDecisionCache",
"//Source/common:SNTCachedDecision",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTStoredEvent",
"//Source/common:TestUtils",
"//Source/common:santa_cc_proto_library_wrapper",
"@OCMock",
"@com_google_googletest//:gtest",
],
)
santa_unit_test(
name = "AuthResultCacheTest",
srcs = ["EventProviders/AuthResultCacheTest.mm"],
@@ -785,6 +886,17 @@ santa_unit_test(
],
)
santa_unit_test(
name = "EndpointSecurityWriterSpoolTest",
srcs = ["Logs/EndpointSecurity/Writers/SpoolTest.mm"],
deps = [
":EndpointSecurityWriterSpool",
"//Source/common:TestUtils",
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:fsspool",
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:fsspool_log_batch_writer",
],
)
santa_unit_test(
name = "EndpointSecurityLoggerTest",
srcs = ["Logs/EndpointSecurity/LoggerTest.mm"],
@@ -799,9 +911,11 @@ santa_unit_test(
":EndpointSecuritySerializer",
":EndpointSecuritySerializerBasicString",
":EndpointSecuritySerializerEmpty",
":EndpointSecuritySerializerProtobuf",
":EndpointSecurityWriter",
":EndpointSecurityWriterFile",
":EndpointSecurityWriterNull",
":EndpointSecurityWriterSpool",
":EndpointSecurityWriterSyslog",
":MockEndpointSecurityAPI",
"//Source/common:SNTCommonEnums",
@@ -854,6 +968,8 @@ santa_unit_test(
srcs = ["MetricsTest.mm"],
deps = [
":Metrics",
"//Source/common:SNTMetricSet",
"//Source/common:TestUtils",
"@OCMock",
],
)
@@ -888,6 +1004,7 @@ santa_unit_test(
":EndpointSecurityClient",
":EndpointSecurityEnrichedTypes",
":EndpointSecurityMessage",
":Metrics",
":MockEndpointSecurityAPI",
":SNTEndpointSecurityClient",
"//Source/common:TestUtils",
@@ -935,6 +1052,7 @@ santa_unit_test(
":AuthResultCache",
":EndpointSecurityClient",
":EndpointSecurityMessage",
":Metrics",
":MockEndpointSecurityAPI",
":SNTCompilerController",
":SNTEndpointSecurityAuthorizer",
@@ -954,6 +1072,7 @@ santa_unit_test(
deps = [
":EndpointSecurityClient",
":EndpointSecurityMessage",
":Metrics",
":MockEndpointSecurityAPI",
":SNTEndpointSecurityTamperResistance",
"//Source/common:SNTLogging",
@@ -976,10 +1095,13 @@ santa_unit_test(
":EndpointSecurityEnricher",
":EndpointSecurityLogger",
":EndpointSecurityMessage",
":Metrics",
":MockEndpointSecurityAPI",
":SNTCompilerController",
":SNTEndpointSecurityRecorder",
"//Source/common:PrefixTree",
"//Source/common:TestUtils",
"//Source/common:Unit",
"@OCMock",
"@com_google_googletest//:gtest",
],
@@ -997,6 +1119,7 @@ santa_unit_test(
":DiskArbitrationTestLib",
":EndpointSecurityClient",
":EndpointSecurityMessage",
":Metrics",
":MockEndpointSecurityAPI",
":SNTEndpointSecurityDeviceManager",
"//Source/common:SNTConfigurator",
@@ -1038,7 +1161,10 @@ test_suite(
":EndpointSecuritySanitizableStringTest",
":EndpointSecuritySerializerBasicStringTest",
":EndpointSecuritySerializerEmptyTest",
":EndpointSecuritySerializerProtobufTest",
":EndpointSecuritySerializerUtilitiesTest",
":EndpointSecurityWriterFileTest",
":EndpointSecurityWriterSpoolTest",
":MetricsTest",
":SNTApplicationCoreMetricsTest",
":SNTCompilerControllerTest",
@@ -1052,6 +1178,7 @@ test_suite(
":SNTExecutionControllerTest",
":SNTRuleTableTest",
":SantadTest",
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:fsspool_test",
],
visibility = ["//:santa_package_group"],
)

View File

@@ -45,6 +45,12 @@ class EndpointSecurityAPI : public std::enable_shared_from_this<EndpointSecurity
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

View File

@@ -84,4 +84,28 @@ es_string_token_t EndpointSecurityAPI::ExecArg(const es_event_exec_t *event, uin
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

@@ -20,7 +20,6 @@
#define SANTA__SANTAD__EVENTPROVIDERS_ENDPOINTSECURITY_ENRICHEDTYPES_H
#include <time.h>
#include <uuid/uuid.h>
#include <optional>
#include <string>
@@ -32,6 +31,9 @@ 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)
@@ -39,6 +41,20 @@ class EnrichedFile {
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_;
@@ -47,6 +63,12 @@ class EnrichedFile {
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,
@@ -58,12 +80,28 @@ class EnrichedProcess {
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_;
@@ -76,21 +114,30 @@ class EnrichedProcess {
class EnrichedEventType {
public:
EnrichedEventType(Message &&es_msg, EnrichedProcess &&instigator)
: es_msg_(std::move(es_msg)), instigator_(std::move(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_)) {}
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 {
@@ -100,6 +147,14 @@ class EnrichedClose : public EnrichedEventType {
: 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_;
};
@@ -112,6 +167,16 @@ class EnrichedExchange : public EnrichedEventType {
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_;
@@ -127,6 +192,20 @@ class EnrichedExec : public EnrichedEventType {
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_;
@@ -137,17 +216,28 @@ 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 &&target)
EnrichedProcess &&child)
: EnrichedEventType(std::move(es_msg), std::move(instigator)),
target_(std::move(target)) {}
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 target_;
EnrichedProcess child_;
};
class EnrichedLink : public EnrichedEventType {
@@ -158,6 +248,15 @@ class EnrichedLink : public EnrichedEventType {
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_;
@@ -173,6 +272,16 @@ class EnrichedRename : public EnrichedEventType {
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_;
@@ -186,6 +295,14 @@ class EnrichedUnlink : public EnrichedEventType {
: 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_;
};
@@ -196,16 +313,11 @@ using EnrichedType =
class EnrichedMessage {
public:
EnrichedMessage(EnrichedType &&msg) : msg_(std::move(msg)) {
uuid_generate(uuid_);
clock_gettime(CLOCK_REALTIME, &enrichment_time_);
}
EnrichedMessage(EnrichedType &&msg) : msg_(std::move(msg)) {}
const EnrichedType &GetEnrichedMessage() { return msg_; }
private:
uuid_t uuid_;
struct timespec enrichment_time_;
EnrichedType msg_;
};

View File

@@ -19,7 +19,6 @@
#include <grp.h>
#include <pwd.h>
#include <sys/types.h>
#include <uuid/uuid.h>
#include <memory>
#include <optional>
@@ -48,8 +47,9 @@ std::shared_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
(es_msg->version >= 2 && es_msg->event.exec.script)
? std::make_optional(Enrich(*es_msg->event.exec.script))
: std::nullopt,
(es_msg->version >= 3) ? std::make_optional(Enrich(*es_msg->event.exec.cwd))
: std::nullopt));
(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)));

View File

@@ -32,9 +32,9 @@ using santa::santad::event_providers::endpoint_security::Enricher;
XCTAssertTrue(user.has_value());
XCTAssertEqual(strcmp(user->get()->c_str(), "nobody"), 0);
std::optional<std::shared_ptr<std::string>> group = enricher.UsernameForGID(NOBODY_GID);
std::optional<std::shared_ptr<std::string>> group = enricher.UsernameForGID(NOGROUP_GID);
XCTAssertTrue(group.has_value());
XCTAssertEqual(strcmp(group->get()->c_str(), "nobody"), 0);
XCTAssertEqual(strcmp(group->get()->c_str(), "nogroup"), 0);
uid_t invalidUID = (uid_t)-123;
gid_t invalidGID = (gid_t)-123;

View File

@@ -57,6 +57,12 @@ class MockEndpointSecurityAPI
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(

View File

@@ -18,6 +18,7 @@
#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"
@@ -30,6 +31,7 @@
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:

View File

@@ -22,7 +22,9 @@
#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;
@@ -37,10 +39,13 @@ using santa::santad::event_providers::endpoint_security::Message;
}
- (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)];
self = [super initWithESAPI:std::move(esApi)
metrics:std::move(metrics)
processor:santa::santad::Processor::kAuthorizer];
if (self) {
_execController = execController;
_compilerController = compilerController;
@@ -51,6 +56,10 @@ using santa::santad::event_providers::endpoint_security::Message;
return self;
}
- (NSString *)description {
return @"Authorizer";
}
- (void)processMessage:(const Message &)msg {
const es_file_t *targetFile = msg->event.exec.target->executable;
@@ -90,7 +99,8 @@ using santa::santad::event_providers::endpoint_security::Message;
}];
}
- (void)handleMessage:(Message &&)esMsg {
- (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");
@@ -100,12 +110,14 @@ using santa::santad::event_providers::endpoint_security::Message;
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);
}];
}

View File

@@ -28,9 +28,11 @@
#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;
@@ -66,7 +68,10 @@ class MockAuthResultCache : public AuthResultCache {
std::set<es_event_type_t> expectedEventSubs{ES_EVENT_TYPE_AUTH_EXEC};
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
id authClient = [[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi];
id authClient =
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:santa::santad::Processor::kAuthorizer];
EXPECT_CALL(*mockESApi, ClearCache)
.After(EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs))
@@ -87,8 +92,18 @@ class MockAuthResultCache : public AuthResultCache {
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];
@@ -99,7 +114,10 @@ class MockAuthResultCache : public AuthResultCache {
{
// Temporarily change the event type
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXEC;
XCTAssertThrows([authClient handleMessage:Message(mockESApi, &esMsg)]);
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;
}
@@ -117,7 +135,13 @@ class MockAuthResultCache : public AuthResultCache {
.ignoringNonObjectArgs()
.andDo(nil);
[mockAuthClient handleMessage:std::move(msg)];
[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));
}
@@ -130,13 +154,18 @@ class MockAuthResultCache : public AuthResultCache {
.ignoringNonObjectArgs()
.andReturn(YES);
OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg) handler:[OCMArg any]])
.ignoringNonObjectArgs();
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg) handler:[OCMArg any]])
OCMExpect([mockAuthClient processMessage:Message(mockESApi, &esMsg)]).ignoringNonObjectArgs();
OCMStub([mockAuthClient processMessage:Message(mockESApi, &esMsg)])
.ignoringNonObjectArgs()
.andDo(nil);
[mockAuthClient handleMessage:std::move(msg)];
[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));
}
@@ -173,6 +202,7 @@ class MockAuthResultCache : public AuthResultCache {
SNTEndpointSecurityAuthorizer *authClient =
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
metrics:nullptr
execController:self.mockExecController
compilerController:mockCompilerController
authResultCache:mockAuthCache];
@@ -238,6 +268,7 @@ class MockAuthResultCache : public AuthResultCache {
SNTEndpointSecurityAuthorizer *authClient =
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
metrics:nullptr
execController:self.mockExecController
compilerController:mockCompilerController
authResultCache:mockAuthCache];

View File

@@ -13,8 +13,8 @@
/// limitations under the License.
#import "Source/santad/EventProviders/SNTEndpointSecurityClient.h"
#include <EndpointSecurity/ESTypes.h>
#include <EndpointSecurity/EndpointSecurity.h>
#include <bsm/libbsm.h>
#include <dispatch/dispatch.h>
#include <mach/mach_time.h>
@@ -27,7 +27,11 @@
#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;
@@ -36,21 +40,26 @@ 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 {
- (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");
@@ -84,7 +93,8 @@ using santa::santad::event_providers::endpoint_security::Message;
}
}
- (void)handleMessage:(Message &&)esMsg {
- (void)handleMessage:(Message &&)esMsg
recordEventMetrics:(void (^)(EventDisposition disposition))recordEventMetrics {
// This method should only be used by classes derived
// from SNTEndpointSecurityClient.
[self doesNotRecognizeSelector:_cmd];
@@ -110,10 +120,21 @@ using santa::santad::event_providers::endpoint_security::Message;
}
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)];
[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);
}
});
@@ -122,7 +143,7 @@ using santa::santad::event_providers::endpoint_security::Message;
LOGE(@"Unable to create EndpointSecurity client: %@", errMsg);
[NSException raise:@"Failed to create ES client" format:@"%@", errMsg];
} else {
LOGI(@"Connected to EndpointSecurity");
LOGI(@"Connected to EndpointSecurity (%@)", self);
}
if (![self muteSelf]) {

View File

@@ -23,11 +23,15 @@
#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;
- (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.

View File

@@ -28,7 +28,9 @@
#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;
@@ -40,7 +42,8 @@ using santa::santad::event_providers::endpoint_security::Message;
- (void)establishClientOrDie;
- (bool)muteSelf;
- (NSString *)errorMessageForNewClientResult:(es_new_client_result_t)result;
- (void)handleMessage:(Message &&)esMsg;
- (void)handleMessage:(Message &&)esMsg
recordEventMetrics:(void (^)(santa::santad::EventDisposition disposition))recordEventMetrics;
- (BOOL)shouldHandleMessage:(const Message &)esMsg
ignoringOtherESClients:(BOOL)ignoringOtherESClients;
@@ -61,7 +64,10 @@ using santa::santad::event_providers::endpoint_security::Message;
.WillOnce(testing::Return(Client()))
.WillOnce(testing::Return(Client(nullptr, ES_NEW_CLIENT_RESULT_SUCCESS)));
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
// First time throws because mock triggers failed connection
// Second time succeeds
@@ -83,7 +89,10 @@ using santa::santad::event_providers::endpoint_security::Message;
{(es_new_client_result_t)123, "Unknown error"},
};
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:nullptr];
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:nullptr
metrics:nullptr
processor:Processor::kUnknown];
for (const auto &kv : resultMessagePairs) {
NSString *message = [client errorMessageForNewClientResult:kv.first];
@@ -97,9 +106,12 @@ using santa::santad::event_providers::endpoint_security::Message;
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
{ XCTAssertThrows([client handleMessage:Message(mockESApi, &esMsg)]); }
{ XCTAssertThrows([client handleMessage:Message(mockESApi, &esMsg) recordEventMetrics:nil]); }
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
}
@@ -116,7 +128,10 @@ using santa::santad::event_providers::endpoint_security::Message;
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, ES_AUTH_RESULT_ALLOW, true))
.WillOnce(testing::Return(true));
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
{
Message msg(mockESApi, &esMsg);
@@ -153,7 +168,10 @@ using santa::santad::event_providers::endpoint_security::Message;
- (void)testMuteSelf {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
EXPECT_CALL(*mockESApi, MuteProcess)
.WillOnce(testing::Return(true))
@@ -167,7 +185,10 @@ using santa::santad::event_providers::endpoint_security::Message;
- (void)testClearCache {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
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)
@@ -182,7 +203,10 @@ using santa::santad::event_providers::endpoint_security::Message;
- (void)testSubscribe {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
std::set<es_event_type_t> events = {
ES_EVENT_TYPE_NOTIFY_CLOSE,
@@ -202,7 +226,10 @@ using santa::santad::event_providers::endpoint_security::Message;
- (void)testSubscribeAndClearCache {
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
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)
@@ -229,7 +256,10 @@ using santa::santad::event_providers::endpoint_security::Message;
EXPECT_CALL(*mockESApi, RespondAuthResult(testing::_, testing::_, result, cacheable))
.WillOnce(testing::Return(true));
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
{
Message msg(mockESApi, &esMsg);
@@ -249,7 +279,10 @@ using santa::santad::event_providers::endpoint_security::Message;
// 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];
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
es_message_t esMsg;
auto enrichedMsg = std::make_shared<EnrichedMessage>(
@@ -285,7 +318,10 @@ using santa::santad::event_providers::endpoint_security::Message;
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
{
XCTAssertThrows([client processMessage:Message(mockESApi, &esMsg)
@@ -313,7 +349,10 @@ using santa::santad::event_providers::endpoint_security::Message;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
{
XCTAssertNoThrow([client processMessage:Message(mockESApi, &esMsg)
@@ -359,7 +398,10 @@ using santa::santad::event_providers::endpoint_security::Message;
return true;
}));
SNTEndpointSecurityClient *client = [[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi];
SNTEndpointSecurityClient *client =
[[SNTEndpointSecurityClient alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kUnknown];
client.deadlineMarginMS = 500;
{

View File

@@ -21,6 +21,7 @@
#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
@@ -40,6 +41,7 @@ typedef void (^SNTDeviceBlockCallback)(SNTDeviceEvent *event);
- (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;

View File

@@ -28,7 +28,9 @@
#import "Source/common/SNTDeviceEvent.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::AuthResultCache;
using santa::santad::event_providers::FlushCacheMode;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
@@ -135,9 +137,12 @@ NS_ASSUME_NONNULL_BEGIN
}
- (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)];
self = [super initWithESAPI:std::move(esApi)
metrics:std::move(metrics)
processor:santa::santad::Processor::kDeviceManager];
if (self) {
_logger = logger;
_authResultCache = authResultCache;
@@ -161,16 +166,23 @@ NS_ASSUME_NONNULL_BEGIN
self->_logger->LogDiskDisappeared(props);
}
- (void)handleMessage:(Message &&)esMsg {
- (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;
}
@@ -178,6 +190,7 @@ NS_ASSUME_NONNULL_BEGIN
handler:^(const Message &msg) {
es_auth_result_t result = [self handleAuthMount:msg];
[self respondToMessage:msg withAuthResult:result cacheable:false];
recordEventMetrics(EventDisposition::kProcessed);
}];
}

View File

@@ -21,6 +21,7 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <sys/mount.h>
#include <cstddef>
#include <memory>
#include <set>
@@ -33,7 +34,9 @@
#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;
@@ -106,6 +109,7 @@ class MockAuthResultCache : public AuthResultCache {
SNTEndpointSecurityDeviceManager *deviceManager =
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
metrics:nullptr
logger:nullptr
authResultCache:nullptr];
@@ -123,6 +127,8 @@ class MockAuthResultCache : public AuthResultCache {
// 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(^{
@@ -157,13 +163,17 @@ class MockAuthResultCache : public AuthResultCache {
return true;
}));
[deviceManager handleMessage:Message(mockESApi, &esMsg)];
[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];
XCTAssertEqual(0,
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
"Failed waiting for message to be processed...");
XCTAssertSemaTrue(semaMetrics, 5, "Metrics not recorded within expected window");
XCTAssertSemaTrue(sema, 5, "Failed waiting for message to be processed...");
[partialDeviceManager stopMocking];
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
@@ -303,6 +313,8 @@ class MockAuthResultCache : public AuthResultCache {
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);
@@ -312,12 +324,19 @@ class MockAuthResultCache : public AuthResultCache {
SNTEndpointSecurityDeviceManager *deviceManager =
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
metrics:nullptr
logger:nullptr
authResultCache:mockAuthCache];
deviceManager.blockUSBMount = YES;
[deviceManager handleMessage:Message(mockESApi, &esMsg)];
[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());
@@ -332,7 +351,10 @@ class MockAuthResultCache : public AuthResultCache {
};
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
id deviceClient = [[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi];
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))

View File

@@ -17,13 +17,15 @@
#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;
- (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

View File

@@ -12,13 +12,15 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/common/SNTPrefixTree.h"
#import "Source/common/PrefixTree.h"
#import "Source/common/Unit.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
@@ -29,12 +31,13 @@
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;
prefixTree:(std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>)prefixTree;
@end

View File

@@ -14,13 +14,17 @@
#import "Source/santad/EventProviders/SNTEndpointSecurityRecorder.h"
#include <EndpointSecurity/ESTypes.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::common::PrefixTree;
using santa::common::Unit;
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;
@@ -46,16 +50,19 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
std::shared_ptr<AuthResultCache> _authResultCache;
std::shared_ptr<Enricher> _enricher;
std::shared_ptr<Logger> _logger;
std::shared_ptr<SNTPrefixTree> _prefixTree;
std::shared_ptr<PrefixTree<Unit>> _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)];
prefixTree:(std::shared_ptr<PrefixTree<Unit>>)prefixTree {
self = [super initWithESAPI:std::move(esApi)
metrics:std::move(metrics)
processor:santa::santad::Processor::kRecorder];
if (self) {
_enricher = enricher;
_logger = logger;
@@ -68,7 +75,12 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
return self;
}
- (void)handleMessage:(Message &&)esMsg {
- (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:
@@ -76,6 +88,9 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
// 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;
}
@@ -89,6 +104,7 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
// 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;
}
@@ -100,6 +116,7 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
[self processEnrichedMessage:std::move(sharedEnrichedMessage)
handler:^(std::shared_ptr<EnrichedMessage> msg) {
self->_logger->Log(std::move(msg));
recordEventMetrics(EventDisposition::kProcessed);
}];
}
@@ -108,8 +125,8 @@ es_file_t *GetTargetFileForPrefixTree(const es_message_t *msg) {
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_FORK,
ES_EVENT_TYPE_NOTIFY_LINK,
ES_EVENT_TYPE_NOTIFY_RENAME,
ES_EVENT_TYPE_NOTIFY_UNLINK,

View File

@@ -22,7 +22,9 @@
#include <memory>
#include <set>
#include "Source/common/PrefixTree.h"
#include "Source/common/TestUtils.h"
#include "Source/common/Unit.h"
#import "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/Client.h"
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
@@ -31,8 +33,13 @@
#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::common::PrefixTree;
using santa::common::Unit;
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;
@@ -72,7 +79,9 @@ class MockLogger : public Logger {
};
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
id recorderClient = [[SNTEndpointSecurityRecorder alloc] initWithESAPI:mockESApi];
id recorderClient = [[SNTEndpointSecurityRecorder alloc] initWithESAPI:mockESApi
metrics:nullptr
processor:Processor::kRecorder];
EXPECT_CALL(*mockESApi, Subscribe(testing::_, expectedEventSubs)).WillOnce(testing::Return(true));
@@ -99,6 +108,8 @@ class MockLogger : public Logger {
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
@@ -109,12 +120,13 @@ class MockLogger : public Logger {
dispatch_semaphore_signal(sema);
}));
auto prefixTree = std::make_shared<SNTPrefixTree>();
auto prefixTree = std::make_shared<PrefixTree<Unit>>();
id mockCC = OCMStrictClassMock([SNTCompilerController class]);
SNTEndpointSecurityRecorder *recorderClient =
[[SNTEndpointSecurityRecorder alloc] initWithESAPI:mockESApi
metrics:nullptr
logger:mockLogger
enricher:mockEnricher
compilerController:mockCC
@@ -127,7 +139,10 @@ class MockLogger : public Logger {
esMsg.event.close.modified = false;
esMsg.event.close.target = NULL;
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, &esMsg)]);
XCTAssertNoThrow([recorderClient handleMessage:Message(mockESApi, &esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTFail("Metrics record callback should not be called here");
}]);
}
// CLOSE modified, remove from cache
@@ -139,23 +154,32 @@ class MockLogger : public Logger {
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
[recorderClient handleMessage:std::move(msg)];
[recorderClient handleMessage:std::move(msg)
recordEventMetrics:^(EventDisposition d) {
XCTAssertEqual(d, EventDisposition::kProcessed);
dispatch_semaphore_signal(semaMetrics);
}];
XCTAssertEqual(
0, dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)),
"Log wasn't called within expected time window");
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);
prefixTree->InsertPrefix(esMsg.event.link.source->path.data, Unit{});
Message msg(mockESApi, &esMsg);
OCMExpect([mockCC handleEvent:msg withLogger:nullptr]).ignoringNonObjectArgs();
[recorderClient handleMessage:std::move(msg)];
[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));

View File

@@ -20,6 +20,7 @@
#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
@@ -28,6 +29,7 @@
- (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

@@ -19,7 +19,9 @@
#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;
@@ -31,8 +33,11 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
}
- (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)];
self = [super initWithESAPI:std::move(esApi)
metrics:std::move(metrics)
processor:santa::santad::Processor::kTamperResistance];
if (self) {
_logger = logger;
@@ -41,54 +46,51 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
return self;
}
- (void)handleMessage:(Message &&)esMsg {
- (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]) {
// Do not cache so that each attempt to remove santa is logged
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_DENY cacheable:false];
result = ES_AUTH_RESULT_DENY;
LOGW(@"Preventing attempt to delete Santa databases!");
} else {
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:true];
}
return;
break;
}
case ES_EVENT_TYPE_AUTH_RENAME: {
if ([SNTEndpointSecurityTamperResistance
isDatabasePath:esMsg->event.rename.source->path.data]) {
// Do not cache so that each attempt to remove santa is logged
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_DENY cacheable:false];
result = ES_AUTH_RESULT_DENY;
LOGW(@"Preventing attempt to rename Santa databases!");
return;
break;
}
if (esMsg->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
if ([SNTEndpointSecurityTamperResistance
isDatabasePath:esMsg->event.rename.destination.existing_file->path.data]) {
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_DENY cacheable:false];
result = ES_AUTH_RESULT_DENY;
LOGW(@"Preventing attempt to overwrite Santa databases!");
return;
break;
}
}
// If we get to here, no more reasons to deny the event, so allow it
[self respondToMessage:esMsg withAuthResult:ES_AUTH_RESULT_ALLOW cacheable:true];
return;
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.
es_auth_result_t res = ES_AUTH_RESULT_ALLOW;
if (strcmp(esMsg->event.kextload.identifier.data, kSantaKextIdentifier.data()) == 0) {
result = ES_AUTH_RESULT_DENY;
LOGW(@"Preventing attempt to load Santa kext!");
res = ES_AUTH_RESULT_DENY;
}
[self respondToMessage:esMsg withAuthResult:res cacheable:true];
return;
break;
}
default:
@@ -96,6 +98,13 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
[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 {

View File

@@ -17,6 +17,7 @@
#import <XCTest/XCTest.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <stdlib.h>
#include <map>
#include <memory>
@@ -27,7 +28,9 @@
#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;
@@ -59,7 +62,9 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
.WillOnce(testing::Return(true));
SNTEndpointSecurityTamperResistance *tamperClient =
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi logger:nullptr];
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi
metrics:nullptr
logger:nullptr];
id mockTamperClient = OCMPartialMock(tamperClient);
[mockTamperClient enable];
@@ -91,12 +96,16 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
{&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 logger:nullptr];
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:mockESApi
metrics:nullptr
logger:nullptr];
id mockTamperClient = OCMPartialMock(tamperClient);
@@ -118,7 +127,10 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
// First check unhandled event types will crash
{
Message msg(mockESApi, &esMsg);
XCTAssertThrows([tamperClient handleMessage:Message(mockESApi, &esMsg)]);
XCTAssertThrows([tamperClient handleMessage:Message(mockESApi, &esMsg)
recordEventMetrics:^(EventDisposition d) {
XCTFail("Unhandled event types shouldn't call metrics recorder");
}]);
}
// Check UNLINK tamper events
@@ -128,7 +140,15 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
Message msg(mockESApi, &esMsg);
esMsg.event.unlink.target = kv.first;
[mockTamperClient handleMessage:std::move(msg)];
[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);
@@ -143,8 +163,15 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
esMsg.event.rename.source = kv.first;
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
[mockTamperClient handleMessage:std::move(msg)];
[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);
}
@@ -159,8 +186,15 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
esMsg.event.rename.destination.existing_file = kv.first;
[mockTamperClient handleMessage:std::move(msg)];
[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);
}
@@ -174,10 +208,17 @@ static constexpr std::string_view kSantaKextIdentifier = "com.google.santa-drive
Message msg(mockESApi, &esMsg);
esMsg.event.kextload.identifier = *kv.first;
[mockTamperClient handleMessage:std::move(msg)];
[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, true); // Note: Kext responses always cached
XCTAssertEqual(gotCachable, kv.second == ES_AUTH_RESULT_ALLOW);
}
}

View File

@@ -39,7 +39,9 @@ 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);
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);

View File

@@ -19,8 +19,10 @@
#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;
@@ -28,8 +30,10 @@ 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 {
@@ -43,7 +47,10 @@ 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) {
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>(
@@ -54,8 +61,12 @@ std::unique_ptr<Logger> Logger::Create(std::shared_ptr<EndpointSecurityAPI> esap
return std::make_unique<Logger>(BasicString::Create(esapi, false), Syslog::Create());
case SNTEventLogTypeNull: return std::make_unique<Logger>(Empty::Create(), Null::Create());
case SNTEventLogTypeProtobuf:
LOGE(@"The EventLogType value protobuf is not supported in this release");
return nullptr;
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;
}
}

View File

@@ -31,9 +31,11 @@
#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"
@@ -45,8 +47,10 @@ 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 {
@@ -92,20 +96,28 @@ class MockWriter : public Null {
// Ensure that the factory method creates expected serializers/writers pairs
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
XCTAssertEqual(nullptr, Logger::Create(mockESApi, (SNTEventLogType)123, @"/tmp"));
XCTAssertEqual(nullptr, Logger::Create(mockESApi, SNTEventLogTypeProtobuf, @"/tmp"));
XCTAssertEqual(nullptr, Logger::Create(mockESApi, (SNTEventLogType)123, @"/tmp/temppy",
@"/tmp/spool", 1, 1, 1));
LoggerPeer logger(Logger::Create(mockESApi, SNTEventLogTypeFilelog, @"/tmp/temppy"));
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"));
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"));
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 {

View File

@@ -64,6 +64,7 @@ class BasicString : public Serializer {
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_;

View File

@@ -33,6 +33,7 @@
#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;
@@ -45,12 +46,11 @@ 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;
// These functions are exported by the Security framework, but are not included in headers
extern "C" Boolean SecTranslocateIsTranslocatedURL(CFURLRef path, bool *isTranslocated,
CFErrorRef *__nullable error);
extern "C" CFURLRef __nullable SecTranslocateCreateOriginalPathForURL(CFURLRef translocatedPath,
CFErrorRef *__nullable error);
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 {
@@ -58,134 +58,6 @@ static inline SanitizableString FilePath(const es_file_t *file) {
return SanitizableString(file);
}
static inline pid_t Pid(const audit_token_t &tok) {
return audit_token_to_pid(tok);
}
static inline pid_t Pidversion(const audit_token_t &tok) {
return audit_token_to_pidversion(tok);
}
static inline pid_t RealUser(const audit_token_t &tok) {
return audit_token_to_ruid(tok);
}
static inline pid_t RealGroup(const audit_token_t &tok) {
return audit_token_to_rgid(tok);
}
static inline void SetThreadIDs(uid_t uid, gid_t gid) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
pthread_setugid_np(uid, gid);
#pragma clang diagnostic pop
}
static inline const mach_port_t GetDefaultIOKitCommsPort() {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return kIOMasterPortDefault;
#pragma clang diagnostic pop
}
static NSString *SerialForDevice(NSString *devPath) {
if (!devPath.length) {
return nil;
}
NSString *serial;
io_registry_entry_t device =
IORegistryEntryFromPath(GetDefaultIOKitCommsPort(), devPath.UTF8String);
while (!serial && device) {
CFMutableDictionaryRef device_properties = NULL;
IORegistryEntryCreateCFProperties(device, &device_properties, kCFAllocatorDefault, kNilOptions);
NSDictionary *properties = CFBridgingRelease(device_properties);
if (properties[@"Serial Number"]) {
serial = properties[@"Serial Number"];
} else if (properties[@"kUSBSerialNumberString"]) {
serial = properties[@"kUSBSerialNumberString"];
}
if (serial) {
IOObjectRelease(device);
break;
}
io_registry_entry_t parent;
IORegistryEntryGetParentEntry(device, kIOServicePlane, &parent);
IOObjectRelease(device);
device = parent;
}
return [serial stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
}
static NSString *DiskImageForDevice(NSString *devPath) {
devPath = [devPath stringByDeletingLastPathComponent];
if (!devPath.length) {
return nil;
}
io_registry_entry_t device =
IORegistryEntryFromPath(GetDefaultIOKitCommsPort(), devPath.UTF8String);
CFMutableDictionaryRef device_properties = NULL;
IORegistryEntryCreateCFProperties(device, &device_properties, kCFAllocatorDefault, kNilOptions);
NSDictionary *properties = CFBridgingRelease(device_properties);
IOObjectRelease(device);
if (properties[@"image-path"]) {
NSString *result = [[NSString alloc] initWithData:properties[@"image-path"]
encoding:NSUTF8StringEncoding];
return [result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
} else {
return nil;
}
}
static NSString *OriginalPathForTranslocation(const es_process_t *esProc) {
if (!esProc) {
return nil;
}
// Note: Benchmarks showed better performance using `URLWithString` with a `file://` prefix
// compared to using `fileURLWithPath`.
CFURLRef cfExecURL = (__bridge CFURLRef)
[NSURL URLWithString:[NSString stringWithFormat:@"file://%s", esProc->executable->path.data]];
NSURL *origURL = nil;
bool isTranslocated = false;
if (SecTranslocateIsTranslocatedURL(cfExecURL, &isTranslocated, NULL) && isTranslocated) {
bool dropPrivs = true;
if (@available(macOS 12.0, *)) {
dropPrivs = false;
}
if (dropPrivs) {
SetThreadIDs(RealUser(esProc->audit_token), RealGroup(esProc->audit_token));
}
origURL = CFBridgingRelease(SecTranslocateCreateOriginalPathForURL(cfExecURL, NULL));
if (dropPrivs) {
SetThreadIDs(KAUTH_UID_NONE, KAUTH_GID_NONE);
}
}
return [origURL path];
}
es_file_t *GetAllowListTargetFile(const Message &msg) {
switch (msg->event_type) {
case ES_EVENT_TYPE_NOTIFY_CLOSE: return msg->event.close.target;
case ES_EVENT_TYPE_NOTIFY_RENAME: return msg->event.rename.source;
default:
// This is a programming error
LOGE(@"Unexpected event type for AllowList");
[NSException raise:@"Unexpected type"
format:@"Unexpected event type for AllowList: %d", msg->event_type];
return nil;
}
}
static NSDateFormatter *GetDateFormatter() {
static dispatch_once_t onceToken;
static NSDateFormatter *dateFormatter;
@@ -276,10 +148,6 @@ static char *FormattedDateString(char *buf, size_t len) {
return buf;
}
static inline NSString *NonNull(NSString *str) {
return str ?: @"";
}
std::shared_ptr<BasicString> BasicString::Create(std::shared_ptr<EndpointSecurityAPI> esapi,
bool prefix_time_name) {
return std::make_shared<BasicString>(esapi, prefix_time_name);
@@ -303,8 +171,13 @@ std::string BasicString::CreateDefaultString(size_t reserved_size) {
return str;
}
inline std::vector<uint8_t> FinalizeString(std::string &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;
@@ -394,7 +267,7 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg) {
str.append("|path=");
str.append(FilePath(esm.event.exec.target->executable).Sanitized());
NSString *origPath = OriginalPathForTranslocation(esm.event.exec.target);
NSString *origPath = Utilities::OriginalPathForTranslocation(esm.event.exec.target);
if (origPath) {
str.append("|origpath=");
str.append(SanitizableString(origPath).Sanitized());
@@ -412,11 +285,6 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedExec &msg) {
}
}
if ([[SNTConfigurator configurator] enableMachineIDDecoration]) {
str.append("|machineid=");
str.append([NonNull([[SNTConfigurator configurator] machineID]) UTF8String]);
}
return FinalizeString(str);
}
@@ -524,7 +392,7 @@ std::vector<uint8_t> BasicString::SerializeAllowlist(const Message &msg,
str.append("|pidversion=");
str.append(std::to_string(Pidversion(msg->process->audit_token)));
str.append("|path=");
str.append(FilePath(GetAllowListTargetFile(msg)).Sanitized());
str.append(FilePath(Utilities::GetAllowListTargetFile(msg)).Sanitized());
str.append("|sha256=");
str.append(hash);
@@ -551,12 +419,12 @@ std::vector<uint8_t> BasicString::SerializeBundleHashingEvent(SNTStoredEvent *ev
}
std::vector<uint8_t> BasicString::SerializeDiskAppeared(NSDictionary *props) {
NSString *dmgPath = nil;
NSString *dmg_path = nil;
NSString *serial = nil;
if ([props[@"DADeviceModel"] isEqual:@"Disk Image"]) {
dmgPath = DiskImageForDevice(props[@"DADevicePath"]);
dmg_path = Utilities::DiskImageForDevice(props[@"DADevicePath"]);
} else {
serial = SerialForDevice(props[@"DADevicePath"]);
serial = Utilities::SerialForDevice(props[@"DADevicePath"]);
}
NSString *model = [NSString
@@ -584,7 +452,7 @@ std::vector<uint8_t> BasicString::SerializeDiskAppeared(NSDictionary *props) {
str.append("|bus=");
str.append([NonNull(props[@"DADeviceProtocol"]) UTF8String]);
str.append("|dmgpath=");
str.append([NonNull(dmgPath) UTF8String]);
str.append([NonNull(dmg_path) UTF8String]);
str.append("|appearance=");
str.append([NonNull(appearanceDateString) UTF8String]);

View File

@@ -12,7 +12,6 @@
/// See the License for the specific language governing permissions and
/// limitations under the License.
#include <EndpointSecurity/ESTypes.h>
#include <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
@@ -43,13 +42,11 @@ using santa::santad::logs::endpoint_security::serializers::BasicString;
using santa::santad::logs::endpoint_security::serializers::Serializer;
namespace santa::santad::logs::endpoint_security::serializers {
extern es_file_t *GetAllowListTargetFile(const Message &msg);
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::GetAllowListTargetFile;
using santa::santad::logs::endpoint_security::serializers::GetDecisionString;
using santa::santad::logs::endpoint_security::serializers::GetModeString;
using santa::santad::logs::endpoint_security::serializers::GetReasonString;
@@ -117,7 +114,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
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=-2|group=nobody\n";
"|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
@@ -131,10 +128,15 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
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=-2|group=nobody\n";
"|uid=-2|user=nobody|gid=-1|group=nogroup\n";
XCTAssertCppStringEqual(got, want);
}
@@ -158,10 +160,11 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
.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=-2|group=nobody|mode=L|path=execpath<pipe>|"
"args=exec<pipe>path -l\\n-t -v\\r--foo|machineid=my_id\n";
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);
}
@@ -172,7 +175,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
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=-2\n";
std::string want = "action=EXIT|pid=12|pidversion=34|ppid=56|uid=-2|gid=-1|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
@@ -187,7 +190,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
esMsg.event.fork.child = &procChild;
std::string got = BasicStringSerializeMessage(&esMsg);
std::string want = "action=FORK|pid=67|pidversion=89|ppid=12|uid=-2|gid=-2\n";
std::string want = "action=FORK|pid=67|pidversion=89|ppid=12|uid=-2|gid=-1|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
@@ -205,7 +208,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
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=-2|group=nobody\n";
"|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
@@ -223,7 +226,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
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=-2|group=nobody\n";
"|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
@@ -238,7 +241,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
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=-2|group=nobody\n";
"|uid=-2|user=nobody|gid=-1|group=nogroup|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
@@ -260,7 +263,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
std::string got(ret.begin(), ret.end());
std::string want = "action=ALLOWLIST|pid=12|pidversion=34|path=foo"
"|sha256=test_hash\n";
"|sha256=test_hash|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
@@ -280,7 +283,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
std::string want = "action=BUNDLE|sha256=file_hash"
"|bundlehash=file_bundle_hash|bundlename=file_bundle_Name|bundleid="
"|bundlepath=file_bundle_path|path=file_path\n";
"|bundlepath=file_bundle_path|path=file_path|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
@@ -297,6 +300,11 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
@"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());
@@ -316,7 +324,7 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
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\n";
std::string want = "action=DISKDISAPPEAR|mount=path|volume=|bsdname=bsd|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
@@ -383,36 +391,4 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
}
}
- (void)testGetAllowListTargetFile {
es_file_t closeTargetFile = MakeESFile("close_target");
es_file_t renameSourceFile = MakeESFile("rename_source");
es_file_t procFile = MakeESFile("foo");
es_process_t proc = MakeESProcess(&procFile);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
{
esMsg.event.close.target = &closeTargetFile;
Message msg(mockESApi, &esMsg);
es_file_t *target = GetAllowListTargetFile(msg);
XCTAssertEqual(target, &closeTargetFile);
}
{
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_RENAME;
esMsg.event.rename.source = &renameSourceFile;
Message msg(mockESApi, &esMsg);
es_file_t *target = GetAllowListTargetFile(msg);
XCTAssertEqual(target, &renameSourceFile);
}
{
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXIT;
Message msg(mockESApi, &esMsg);
XCTAssertThrows(GetAllowListTargetFile(msg));
}
}
@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

@@ -28,6 +28,7 @@ namespace santa::santad::logs::endpoint_security::serializers {
class Serializer {
public:
Serializer();
virtual ~Serializer() = default;
std::vector<uint8_t> SerializeMessage(
@@ -36,6 +37,9 @@ class Serializer {
msg->GetEnrichedMessage());
}
bool EnabledMachineID();
std::string_view MachineID();
virtual std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedClose &) = 0;
virtual std::vector<uint8_t> SerializeMessage(
@@ -80,6 +84,9 @@ class Serializer {
const santa::santad::event_providers::endpoint_security::EnrichedRename &);
std::vector<uint8_t> SerializeMessageTemplate(
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &);
bool enabled_machine_id_ = false;
std::string machine_id_;
};
} // namespace santa::santad::logs::endpoint_security::serializers

View File

@@ -15,13 +15,30 @@
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
#include <EndpointSecurity/EndpointSecurity.h>
#include <string_view>
#import "Source/common/SNTConfigurator.h"
#import "Source/santad/SNTDecisionCache.h"
namespace es = santa::santad::event_providers::endpoint_security;
namespace santa::santad::logs::endpoint_security::serializers {
Serializer::Serializer() {
if ([[SNTConfigurator configurator] enableMachineIDDecoration]) {
enabled_machine_id_ = true;
machine_id_ = [[[SNTConfigurator configurator] machineID] UTF8String] ?: "";
}
}
bool Serializer::EnabledMachineID() {
return enabled_machine_id_;
}
std::string_view Serializer::MachineID() {
return std::string_view(machine_id_);
};
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedClose &msg) {
return SerializeMessage(msg);
}

View File

@@ -0,0 +1,62 @@
/// 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_UTILITIES_H
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_UTILITIES_H
#include <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#include <bsm/libbsm.h>
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
namespace santa::santad::logs::endpoint_security::serializers::Utilities {
static inline pid_t Pid(const audit_token_t &tok) {
return audit_token_to_pid(tok);
}
static inline pid_t Pidversion(const audit_token_t &tok) {
return audit_token_to_pidversion(tok);
}
static inline pid_t RealUser(const audit_token_t &tok) {
return audit_token_to_ruid(tok);
}
static inline pid_t RealGroup(const audit_token_t &tok) {
return audit_token_to_rgid(tok);
}
static inline pid_t EffectiveUser(const audit_token_t &tok) {
return audit_token_to_euid(tok);
}
static inline pid_t EffectiveGroup(const audit_token_t &tok) {
return audit_token_to_egid(tok);
}
static inline NSString *NonNull(NSString *str) {
return str ?: @"";
}
NSString *OriginalPathForTranslocation(const es_process_t *es_proc);
NSString *SerialForDevice(NSString *devPath);
NSString *DiskImageForDevice(NSString *devPath);
es_file_t *GetAllowListTargetFile(
const santa::santad::event_providers::endpoint_security::Message &msg);
} // namespace santa::santad::logs::endpoint_security::serializers::Utilities
#endif

View File

@@ -0,0 +1,138 @@
/// 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/Utilities.h"
// These functions are exported by the Security framework, but are not included in headers
extern "C" Boolean SecTranslocateIsTranslocatedURL(CFURLRef path, bool *isTranslocated,
CFErrorRef *__nullable error);
extern "C" CFURLRef __nullable SecTranslocateCreateOriginalPathForURL(CFURLRef translocatedPath,
CFErrorRef *__nullable error);
using santa::santad::event_providers::endpoint_security::Message;
namespace santa::santad::logs::endpoint_security::serializers::Utilities {
static inline void SetThreadIDs(uid_t uid, gid_t gid) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
pthread_setugid_np(uid, gid);
#pragma clang diagnostic pop
}
NSString *OriginalPathForTranslocation(const es_process_t *es_proc) {
if (!es_proc) {
return nil;
}
// Note: Benchmarks showed better performance using `URLWithString` with a `file://` prefix
// compared to using `fileURLWithPath`.
CFURLRef cfExecURL = (__bridge CFURLRef)
[NSURL URLWithString:[NSString stringWithFormat:@"file://%s", es_proc->executable->path.data]];
NSURL *origURL = nil;
bool isTranslocated = false;
if (SecTranslocateIsTranslocatedURL(cfExecURL, &isTranslocated, NULL) && isTranslocated) {
bool dropPrivs = true;
if (@available(macOS 12.0, *)) {
dropPrivs = false;
}
if (dropPrivs) {
SetThreadIDs(RealUser(es_proc->audit_token), RealGroup(es_proc->audit_token));
}
origURL = CFBridgingRelease(SecTranslocateCreateOriginalPathForURL(cfExecURL, NULL));
if (dropPrivs) {
SetThreadIDs(KAUTH_UID_NONE, KAUTH_GID_NONE);
}
}
return [origURL path];
}
static inline const mach_port_t GetDefaultIOKitCommsPort() {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return kIOMasterPortDefault;
#pragma clang diagnostic pop
}
NSString *SerialForDevice(NSString *devPath) {
if (!devPath.length) {
return nil;
}
NSString *serial;
io_registry_entry_t device =
IORegistryEntryFromPath(GetDefaultIOKitCommsPort(), devPath.UTF8String);
while (!serial && device) {
CFMutableDictionaryRef device_properties = NULL;
IORegistryEntryCreateCFProperties(device, &device_properties, kCFAllocatorDefault, kNilOptions);
NSDictionary *properties = CFBridgingRelease(device_properties);
if (properties[@"Serial Number"]) {
serial = properties[@"Serial Number"];
} else if (properties[@"kUSBSerialNumberString"]) {
serial = properties[@"kUSBSerialNumberString"];
}
if (serial) {
IOObjectRelease(device);
break;
}
io_registry_entry_t parent;
IORegistryEntryGetParentEntry(device, kIOServicePlane, &parent);
IOObjectRelease(device);
device = parent;
}
return [serial stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
}
NSString *DiskImageForDevice(NSString *devPath) {
devPath = [devPath stringByDeletingLastPathComponent];
if (!devPath.length) {
return nil;
}
io_registry_entry_t device =
IORegistryEntryFromPath(GetDefaultIOKitCommsPort(), devPath.UTF8String);
CFMutableDictionaryRef device_properties = NULL;
IORegistryEntryCreateCFProperties(device, &device_properties, kCFAllocatorDefault, kNilOptions);
NSDictionary *properties = CFBridgingRelease(device_properties);
IOObjectRelease(device);
if (properties[@"image-path"]) {
NSString *result = [[NSString alloc] initWithData:properties[@"image-path"]
encoding:NSUTF8StringEncoding];
return [result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
} else {
return nil;
}
}
es_file_t *GetAllowListTargetFile(const Message &msg) {
switch (msg->event_type) {
case ES_EVENT_TYPE_NOTIFY_CLOSE: return msg->event.close.target;
case ES_EVENT_TYPE_NOTIFY_RENAME: return msg->event.rename.source;
default:
// This is a programming error
[NSException raise:@"Unexpected type"
format:@"Unexpected event type for AllowList: %d", msg->event_type];
return nil;
}
}
} // namespace santa::santad::logs::endpoint_security::serializers::Utilities

View File

@@ -0,0 +1,65 @@
/// 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 <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <memory>
#include "Source/common/TestUtils.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::serializers::Utilities::GetAllowListTargetFile;
@interface UtilitiesTest : XCTestCase
@end
@implementation UtilitiesTest
- (void)testGetAllowListTargetFile {
es_file_t closeTargetFile = MakeESFile("close_target");
es_file_t renameSourceFile = MakeESFile("rename_source");
es_file_t procFile = MakeESFile("foo");
es_process_t proc = MakeESProcess(&procFile);
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
{
esMsg.event.close.target = &closeTargetFile;
Message msg(mockESApi, &esMsg);
es_file_t *target = GetAllowListTargetFile(msg);
XCTAssertEqual(target, &closeTargetFile);
}
{
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_RENAME;
esMsg.event.rename.source = &renameSourceFile;
Message msg(mockESApi, &esMsg);
es_file_t *target = GetAllowListTargetFile(msg);
XCTAssertEqual(target, &renameSourceFile);
}
{
esMsg.event_type = ES_EVENT_TYPE_NOTIFY_EXIT;
Message msg(mockESApi, &esMsg);
XCTAssertThrows(GetAllowListTargetFile(msg));
}
}
@end

View File

@@ -0,0 +1,74 @@
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
load("//:helper.bzl", "santa_unit_test")
package(
default_visibility = ["//:santa_package_group"],
)
proto_library(
name = "binaryproto_proto",
srcs = ["binaryproto.proto"],
deps = [
"@com_google_protobuf//:any_proto",
],
)
cc_proto_library(
name = "binaryproto_cc_proto",
deps = [
":binaryproto_proto",
],
)
cc_library(
name = "binaryproto_cc_proto_library_wrapper",
hdrs = ["binaryproto_proto_include_wrapper.h"],
deps = [
":binaryproto_cc_proto",
],
)
cc_library(
name = "fsspool",
srcs = [
"fsspool.cc",
"fsspool_nowindows.cc",
],
hdrs = [
"fsspool.h",
"fsspool_platform_specific.h",
],
deps = [
"@com_google_absl//absl/cleanup",
"@com_google_absl//absl/container:flat_hash_set",
"@com_google_absl//absl/random",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/time",
],
)
cc_library(
name = "fsspool_log_batch_writer",
srcs = ["fsspool_log_batch_writer.cc"],
hdrs = ["fsspool_log_batch_writer.h"],
deps = [
":binaryproto_cc_proto",
":fsspool",
"@com_google_absl//absl/base:core_headers",
"@com_google_absl//absl/status",
"@com_google_absl//absl/synchronization",
],
)
santa_unit_test(
name = "fsspool_test",
srcs = ["fsspool_test.mm"],
deps = [
":fsspool",
":fsspool_log_batch_writer",
"@OCMock",
],
)

View File

@@ -0,0 +1,12 @@
syntax = "proto3";
package santa.fsspool.binaryproto;
import "google/protobuf/any.proto";
option objc_class_prefix = "FSS";
// A LogBatch is a simple array of protos.
message LogBatch {
repeated google.protobuf.Any records = 1;
}

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__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_FSSPOOL_BINARYPROTO_PROTO_INCLUDE_WRAPPER_H
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_FSSPOOL_BINARYPROTO_PROTO_INCLUDE_WRAPPER_H
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/binaryproto.pb.h"
#endif

View File

@@ -0,0 +1,302 @@
/// 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/Writers/FSSpool/fsspool.h"
#include <fcntl.h>
#include <sys/stat.h>
#include <functional>
#include <limits>
#include <string>
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool_platform_specific.h"
#include "absl/random/random.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "absl/strings/substitute.h"
#include "absl/time/time.h"
namespace fsspool {
// Returns whether the given path exists and is a directory.
bool IsDirectory(const std::string& d) {
struct stat stats;
if (stat(d.c_str(), &stats) < 0) {
return false;
}
return StatIsDir(stats.st_mode);
}
namespace {
constexpr absl::string_view kSpoolDirName = "new";
constexpr absl::string_view kTmpDirName = "tmp";
// Estimates the disk occupation of a file of the given size,
// with the following heuristic: A typical disk cluster is 4KiB; files
// usually get written to disk in multiples of this unit.
size_t EstimateDiskOccupation(size_t fileSize) {
// kDiskClusterSize defines the typical size of a disk cluster (4KiB).
static constexpr size_t kDiskClusterSize = 4096;
size_t n_clusters = (fileSize + kDiskClusterSize - 1) / kDiskClusterSize;
// Empty files still occupy some space.
if (n_clusters == 0) {
n_clusters = 1;
}
return n_clusters * kDiskClusterSize;
}
// Creates a directory if it doesn't exist.
// It only accepts absolute paths.
absl::Status MkDir(const std::string& path) {
if (!IsAbsolutePath(path)) {
return absl::InvalidArgumentError(
absl::StrCat(path, " is not an absolute path."));
}
if (fsspool::MkDir(path.c_str(), 0700) < 0) {
if (errno == EEXIST && IsDirectory(path)) {
return absl::OkStatus();
}
return absl::ErrnoToStatus(errno, absl::StrCat("failed to create ", path));
}
return absl::OkStatus();
}
// Writes a buffer to the given file descriptor.
// Calls to write can result in a partially written file. Very rare cases in
// which this could happen (since we're writing to a regular file) include
// if we receive a signal during write or if the disk is full.
// Retry writing until we've flushed everything, return an error if any write
// fails.
absl::Status WriteBuffer(int fd, absl::string_view msg) {
while (!msg.empty()) {
const int n_written = Write(fd, msg);
if (n_written < 0) {
return absl::ErrnoToStatus(errno, "write() failed");
}
msg.remove_prefix(n_written);
}
return absl::OkStatus();
}
// Writes the given data to the given file, with permissions set to 0400.
// Roughly equivalent to file::SetContents.
absl::Status WriteTmpFile(const std::string& path, absl::string_view msg) {
const int fd = Open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0400);
if (fd < 0) {
return absl::ErrnoToStatus(errno, "open() failed");
}
absl::Status write_status = WriteBuffer(fd, msg);
Close(fd);
if (!write_status.ok()) {
// Delete the file so we don't leave garbage behind us.
if (Unlink(path.c_str()) < 0) {
// This is very unlikely (e.g. somehow permissions for the file changed
// since creation?), still worth logging the error.
return absl::ErrnoToStatus(
errno, absl::StrCat("Writing to ", path,
" failed (and deleting failed too)"));
}
return write_status;
}
return absl::OkStatus();
}
// Renames src to dest. Equivalent to file::Rename.
absl::Status RenameFile(const std::string& src, const std::string& dst) {
if (rename(src.c_str(), dst.c_str()) < 0) {
return absl::ErrnoToStatus(
errno, absl::StrCat("failed to rename ", src, " to ", dst));
}
return absl::OkStatus();
}
absl::StatusOr<size_t> EstimateDirSize(const std::string& dir) {
size_t estimate = 0;
absl::Status status =
IterateDirectory(dir, [&dir, &estimate](const std::string& file_name) {
/// NOMUTANTS--We could skip this condition altogether, as S_ISREG on
/// the directory would be false anyway.
if (file_name == std::string(".") || file_name == std::string("..")) {
return;
}
std::string file_path = absl::StrCat(dir, PathSeparator(), file_name);
struct stat stats;
if (stat(file_path.c_str(), &stats) < 0) {
return;
}
if (!StatIsReg(stats.st_mode)) {
return;
}
// Use st_size, as st_blocks is not available on Windows.
estimate += EstimateDiskOccupation(stats.st_size);
});
if (status.ok()) {
return estimate;
}
return status;
}
std::string SpoolDirectory(absl::string_view base_dir) {
return absl::StrCat(base_dir, PathSeparator(), kSpoolDirName);
}
} // namespace
FsSpoolWriter::FsSpoolWriter(absl::string_view base_dir, size_t max_spool_size)
: base_dir_(base_dir),
spool_dir_(SpoolDirectory(base_dir)),
tmp_dir_(absl::StrCat(base_dir, PathSeparator(), kTmpDirName)),
max_spool_size_(max_spool_size),
id_(absl::StrFormat("%016x", absl::Uniform<uint64_t>(
absl::BitGen(), 0,
std::numeric_limits<uint64_t>::max()))),
// Guess that the spool is full during construction, so we will recompute
// the actual spool size on the first write.
spool_size_estimate_(max_spool_size + 1) {}
absl::Status FsSpoolWriter::BuildDirectoryStructureIfNeeded() {
if (!IsDirectory(spool_dir_)) {
if (!IsDirectory(base_dir_)) {
if (absl::Status status = MkDir(base_dir_); !status.ok()) {
return status; // failed to create base directory
}
}
if (absl::Status status = MkDir(spool_dir_); !status.ok()) {
return status; // failed to create spool directory;
}
}
if (!IsDirectory(tmp_dir_)) {
// No need to check the base directory too, since spool_dir_ exists.
if (absl::Status status = MkDir(tmp_dir_); !status.ok()) {
return status; // failed to create tmp directory
}
}
return absl::OkStatus();
}
std::string FsSpoolWriter::UniqueFilename() {
std::string result = absl::StrFormat("%s_%020d", id_, sequence_number_);
sequence_number_++;
return result;
}
absl::Status FsSpoolWriter::WriteMessage(absl::string_view msg) {
if (absl::Status status = BuildDirectoryStructureIfNeeded(); !status.ok()) {
return status; // << "can't create directory structure for writer";
}
// Flush messages to a file in the temporary directory.
const std::string fname = UniqueFilename();
const std::string tmp_file = absl::StrCat(tmp_dir_, PathSeparator(), fname);
const std::string spool_file =
absl::StrCat(spool_dir_, PathSeparator(), fname);
// Recompute the spool size if we think we are
// over the limit.
if (spool_size_estimate_ > max_spool_size_) {
absl::StatusOr<size_t> estimate = EstimateDirSize(spool_dir_);
if (!estimate.ok()) {
return estimate.status(); // failed to recompute spool size
}
spool_size_estimate_ = *estimate;
if (spool_size_estimate_ > max_spool_size_) {
// Still over the limit: avoid writing.
return absl::UnavailableError(
"Spool size estimate greater than max allowed");
}
}
spool_size_estimate_ += EstimateDiskOccupation(msg.size());
if (absl::Status status = WriteTmpFile(tmp_file, msg); !status.ok()) {
return status; // writing to temporary file
}
if (absl::Status status = RenameFile(tmp_file, spool_file); !status.ok()) {
return status; // "moving tmp_file to the spooling area
}
return absl::OkStatus();
}
FsSpoolReader::FsSpoolReader(absl::string_view base_directory)
: base_dir_(base_directory), spool_dir_(SpoolDirectory(base_directory)) {}
int FsSpoolReader::NumberOfUnackedMessages() const {
return unacked_messages_.size();
}
absl::Status FsSpoolReader::AckMessage(const std::string& message_path) {
int remove_status = remove(message_path.c_str());
if ((remove_status != 0) && (errno != ENOENT)) {
return absl::ErrnoToStatus(
errno,
absl::Substitute("Failed to remove $0: $1", message_path, errno));
}
unacked_messages_.erase(message_path);
return absl::OkStatus();
}
absl::StatusOr<std::string> FsSpoolReader::NextMessagePath() {
absl::StatusOr<std::string> file_path = OldestSpooledFile();
if (!file_path.ok()) {
return file_path;
}
unacked_messages_.insert(*file_path);
return file_path;
}
absl::StatusOr<std::string> FsSpoolReader::OldestSpooledFile() {
if (!IsDirectory(spool_dir_)) {
return absl::NotFoundError(
"Spool directory is not a directory or it doesn't exist.");
}
absl::Time oldest_file_mtime;
std::string oldest_file_path;
absl::Status status = IterateDirectory(
spool_dir_, [this, &oldest_file_path,
&oldest_file_mtime](const std::string& file_name) {
std::string file_path =
absl::StrCat(spool_dir_, PathSeparator(), file_name);
struct stat stats;
if (stat(file_path.c_str(), &stats) < 0) {
return;
}
if (!StatIsReg(stats.st_mode)) {
return;
}
if (unacked_messages_.contains(file_path)) {
return;
}
absl::Time file_mtime = absl::FromTimeT(stats.st_mtime);
if (!oldest_file_path.empty() && oldest_file_mtime < file_mtime) {
return;
}
oldest_file_path = file_path;
oldest_file_mtime = file_mtime;
});
if (!status.ok()) {
return status;
}
if (oldest_file_path.empty()) {
return absl::NotFoundError("Empty FsSpool directory.");
}
return oldest_file_path;
}
} // namespace fsspool

View File

@@ -0,0 +1,105 @@
/// 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_WRITERS_FSSPOOL_FSSPOOL_H_
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_FSSPOOL_FSSPOOL_H_
// Namespace ::fsspool::fsspool implements a filesystem-backed message spool, to
// use as a lock-free IPC mechanism.
#include <string>
#include "absl/container/flat_hash_set.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
namespace fsspool {
// Enqueues messages into the spool. Multiple concurrent writers can
// write to the same directory. (Note that this class is only thread-compatible
// and not thread-safe though!)
class FsSpoolWriter {
public:
// The base, spool, and temporary directory will be created as needed on the
// first call to Write() - however the base directory can be created into an
// existing path (i.e. this class will not do an `mkdir -p`).
FsSpoolWriter(absl::string_view base_dir, size_t max_spool_size);
// Pushes the given byte array to the spool. The given maximum
// spool size will be enforced. Returns an error code. If the spool gets full,
// returns the UNAVAILABLE canonical code (which is retryable).
absl::Status WriteMessage(absl::string_view msg);
private:
const std::string base_dir_;
const std::string spool_dir_;
const std::string tmp_dir_;
// Approximate maximum size of the spooling area, in bytes. If a message is
// being written to a spooling area which already contains more than
// maxSpoolSize bytes, the write will not be executed. This is an approximate
// estimate: no care is taken to make an exact estimate (for example, if a
// file gets deleted from the spool while the estimate is being computed, the
// final estimate is likely to still include the size of that file).
const size_t max_spool_size_;
// 64bit hex ID for this writer. Used in combination with the sequence
// number to generate unique names for files. This is generated through
// util::random::NewGlobalID(), hence has only 52 bits of randomness.
const std::string id_;
// Sequence number of the next message to be written. This
// counter will be incremented at every Write call, so that the produced
// spooled files have different names.
uint64_t sequence_number_ = 0;
// Last estimate for the spool size. The estimate will grow every time we
// write messages (basically, we compute it as if there was no reader
// consuming messages). It will get updated with the actual value whenever we
// think we've passed the size limit. The new estimate will be the sum of the
// approximate disk space occupied by each message written (in multiples of
// 4KiB, i.e. a typical disk cluster size).
size_t spool_size_estimate_;
// Makes sure that all the required
// directories needed for correct operation of this Writer are present in the
// filesystem.
absl::Status BuildDirectoryStructureIfNeeded();
// Generates a unique filename by combining the random ID of
// this writer with a sequence number.
std::string UniqueFilename();
};
// This class is thread-unsafe.
class FsSpoolReader {
public:
explicit FsSpoolReader(absl::string_view base_directory);
absl::Status AckMessage(const std::string& message_path);
// Returns absl::NotFoundError in case the FsSpool is empty.
absl::StatusOr<std::string> NextMessagePath();
int NumberOfUnackedMessages() const;
private:
const std::string base_dir_;
const std::string spool_dir_;
absl::flat_hash_set<std::string> unacked_messages_;
absl::StatusOr<std::string> OldestSpooledFile();
};
} // namespace fsspool
#endif // SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_FSSPOOL_FSSPOOL_H_

View File

@@ -0,0 +1,76 @@
/// 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/Writers/FSSpool/fsspool_log_batch_writer.h"
#include <os/log.h>
#include <string>
#include "absl/status/status.h"
namespace fsspool {
FsSpoolLogBatchWriter::FsSpoolLogBatchWriter(FsSpoolWriter* fs_spool_writer,
size_t max_batch_size)
: writer_(fs_spool_writer), max_batch_size_(max_batch_size) {
cache_.mutable_records()->Reserve(max_batch_size_);
}
FsSpoolLogBatchWriter::~FsSpoolLogBatchWriter() {
absl::Status s = FlushNoLock();
if (!s.ok()) {
os_log(OS_LOG_DEFAULT, "Flush() failed with %s",
s.ToString(absl::StatusToStringMode::kWithEverything).c_str());
// LOG(WARNING) << "Flush() failed with " << s;
}
}
absl::Status FsSpoolLogBatchWriter::Flush() {
absl::MutexLock lock(&cache_mutex_);
return FlushNoLock();
}
absl::Status FsSpoolLogBatchWriter::FlushNoLock() {
if (cache_.mutable_records()->empty()) {
return absl::OkStatus();
}
std::string msg;
if (!cache_.SerializeToString(&msg)) {
return absl::InternalError("Failed to serialize internal LogBatch cache.");
}
{
absl::MutexLock lock(&writer_mutex_);
if (absl::Status status = writer_->WriteMessage(msg); !status.ok()) {
return status;
}
}
cache_.mutable_records()->Clear();
cache_.mutable_records()->Reserve(max_batch_size_);
return absl::OkStatus();
}
absl::Status FsSpoolLogBatchWriter::WriteMessage(
const ::google::protobuf::Any& msg) {
absl::MutexLock lock(&cache_mutex_);
if (cache_.records_size() >= max_batch_size_) {
if (absl::Status status = FlushNoLock(); !status.ok()) {
return status;
}
}
*cache_.mutable_records()->Add() = msg;
return absl::OkStatus();
}
} // namespace fsspool

View File

@@ -0,0 +1,70 @@
/// 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_WRITERS_FSSPOOL_FSSPOOLLOGBATCHWRITER_H
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_FSSPOOL_FSSPOOLLOGBATCHWRITER_H
#include <string>
#include <vector>
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/binaryproto.pb.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.h"
#include "absl/base/thread_annotations.h"
#include "absl/synchronization/mutex.h"
#include "google/protobuf/any.pb.h"
namespace fsspool {
// Provides FsSpool batching mechanism in the form of LogBatch proto messages.
//
// Example:
// FsSpoolWriter fsspool_writer(...);
// FsSpoolLogBatchWriter batch_writer(&fsspool_writer, 10);
// ASSERT_OK(batch_writer.WriteMessage(any_proto);
//
// Automatic flush happens in the event of the object destruction.
//
// Flush() method is provided, so the users of this class can implement periodic
// flushes. It is not necessary to call Flush() manually otherwise.
//
// The class is thread-safe.
class FsSpoolLogBatchWriter {
public:
FsSpoolLogBatchWriter(FsSpoolWriter* fs_spool_writer, size_t max_batch_size);
~FsSpoolLogBatchWriter();
// Writes Any proto message to the FsSpool. The write is cached according to
// the object configuration.
//
// This may return an error if flushing is unsuccessful.
absl::Status WriteMessage(const ::google::protobuf::Any& msg);
// Flush internal FsSpoolLogBatchWriter cache to disk. Calling this method is
// not necessary as the cache is flushed after max_batch_size limit is reached
// or when the objects is destroyed.
absl::Status Flush();
private:
absl::Mutex writer_mutex_ ABSL_ACQUIRED_AFTER(cache_mutex_);
FsSpoolWriter* writer_ ABSL_PT_GUARDED_BY(writer_mutex_);
size_t max_batch_size_;
absl::Mutex cache_mutex_;
santa::fsspool::binaryproto::LogBatch cache_ ABSL_GUARDED_BY(cache_mutex_);
absl::Status FlushNoLock() ABSL_SHARED_LOCKS_REQUIRED(cache_mutex_);
};
} // namespace fsspool
#endif // SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_FSSPOOL_FSSPOOLLOGBATCHWRITER_H

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.
#include <dirent.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <functional>
#include <string>
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool_platform_specific.h"
#include "absl/strings/match.h"
#include "absl/strings/str_format.h"
namespace fsspool {
absl::string_view PathSeparator() { return "/"; }
bool IsAbsolutePath(absl::string_view path) {
return absl::StartsWith(path, "/");
}
int Write(int fd, absl::string_view buf) {
return ::write(fd, buf.data(), buf.size());
}
int Unlink(const char* pathname) { return unlink(pathname); }
int MkDir(const char* path, mode_t mode) { return mkdir(path, mode); }
bool StatIsDir(mode_t mode) { return S_ISDIR(mode); }
bool StatIsReg(mode_t mode) { return S_ISREG(mode); }
int Open(const char* filename, int flags, mode_t mode) {
return open(filename, flags, mode);
}
int Close(int fd) { return close(fd); }
absl::Status IterateDirectory(
const std::string& dir, std::function<void(const std::string&)> callback) {
if (!IsDirectory(dir)) {
return absl::InvalidArgumentError(
absl::StrFormat("%s is not a directory", dir));
}
DIR* dp = opendir(dir.c_str());
if (dp == nullptr) {
return absl::ErrnoToStatus(errno, absl::StrCat("failed to open ", dir));
}
struct dirent* ep;
while ((ep = readdir(dp)) != nullptr) {
callback(ep->d_name);
}
closedir(dp);
return absl::OkStatus();
}
} // namespace fsspool

View File

@@ -0,0 +1,42 @@
/// 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_WRITERS_FSSPOOL_FSSPOOLPLATFORMSPECIFIC_H
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_FSSPOOL_FSSPOOLPLATFORMSPECIFIC_H
#include <functional>
#include <string>
#include "absl/status/status.h"
#include "absl/strings/string_view.h"
namespace fsspool {
absl::string_view PathSeparator();
bool IsAbsolutePath(absl::string_view path);
bool IsDirectory(const std::string& d);
int Close(int fd);
int Open(const char* filename, int flags, mode_t mode);
int MkDir(const char* path, mode_t mode);
bool StatIsDir(mode_t mode);
bool StatIsReg(mode_t mode);
int Unlink(const char* pathname);
int Write(int fd, absl::string_view buf);
absl::Status IterateDirectory(const std::string& dir,
std::function<void(const std::string&)> callback);
} // namespace fsspool
#endif // SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_FSSPOOL_FSSPOOLPLATFORMSPECIFIC_H

View File

@@ -0,0 +1,199 @@
/// 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>
#import <OCMock/OCMock.h>
#import <XCTest/XCTest.h>
#include <memory>
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool_log_batch_writer.h"
#include "google/protobuf/any.pb.h"
#include "google/protobuf/timestamp.pb.h"
using fsspool::FsSpoolLogBatchWriter;
using fsspool::FsSpoolWriter;
static constexpr size_t kSpoolSize = 1048576;
#define XCTAssertStatusOk(s) XCTAssertTrue((s).ok())
#define XCTAssertStatusNotOk(s) XCTAssertFalse((s).ok())
google::protobuf::Any TestAnyTimestamp(int64_t s, int32_t n) {
google::protobuf::Timestamp v;
v.set_seconds(s);
v.set_nanos(n);
google::protobuf::Any any;
any.PackFrom(v);
return any;
}
@interface FSSpoolTest : XCTestCase
@property NSString *testDir;
@property NSString *baseDir;
@property NSString *spoolDir;
@property NSString *tmpDir;
@property NSFileManager *fileMgr;
@end
@implementation FSSpoolTest
- (void)setUp {
self.testDir = [NSString stringWithFormat:@"%@fsspool-%d", NSTemporaryDirectory(), getpid()];
self.baseDir = [NSString stringWithFormat:@"%@/base", self.testDir];
self.spoolDir = [NSString stringWithFormat:@"%@/new", self.baseDir];
self.tmpDir = [NSString stringWithFormat:@"%@/tmp", self.baseDir];
self.fileMgr = [NSFileManager defaultManager];
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.baseDir]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.spoolDir]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.tmpDir]);
XCTAssertTrue([self.fileMgr createDirectoryAtPath:self.testDir
withIntermediateDirectories:YES
attributes:nil
error:nil]);
}
- (void)tearDown {
XCTAssertTrue([self.fileMgr removeItemAtPath:self.testDir error:nil]);
}
- (void)testSimpleWrite {
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.baseDir]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.spoolDir]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.tmpDir]);
std::string testData = "Good morning. This is some nice test data.";
XCTAssertStatusOk(writer->WriteMessage(testData));
NSError *err = nil;
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.tmpDir error:&err] count], 0);
XCTAssertNil(err);
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count], 1);
XCTAssertNil(err);
}
- (void)testSpoolFull {
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
const std::string largeMessage(kSpoolSize + 1, '\x42');
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.baseDir]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.spoolDir]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.tmpDir]);
// Write the first message. This will make the spool directory larger than the max.
XCTAssertStatusOk(writer->WriteMessage(largeMessage));
// Ensure the files are created
XCTAssertTrue([self.fileMgr fileExistsAtPath:self.baseDir]);
XCTAssertTrue([self.fileMgr fileExistsAtPath:self.spoolDir]);
XCTAssertTrue([self.fileMgr fileExistsAtPath:self.tmpDir]);
NSError *err = nil;
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.tmpDir error:&err] count], 0);
XCTAssertNil(err);
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count], 1);
XCTAssertNil(err);
// Try to write again, but expect failure. File counts shouldn't change.
XCTAssertStatusNotOk(writer->WriteMessage(largeMessage));
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.tmpDir error:&err] count], 0);
XCTAssertNil(err);
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count], 1);
XCTAssertNil(err);
}
- (void)testWriteMessageNoFlush {
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
FsSpoolLogBatchWriter batch_writer(writer.get(), 10);
// Ensure that writing in batch mode doesn't flsuh on individual writes.
XCTAssertStatusOk(batch_writer.WriteMessage(TestAnyTimestamp(123, 456)));
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.baseDir]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.spoolDir]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.tmpDir]);
}
- (void)testWriteMessageFlushAtCapacity {
static const int kCapacity = 5;
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
FsSpoolLogBatchWriter batch_writer(writer.get(), kCapacity);
// Ensure batch flushed once capacity exceeded
for (int i = 0; i < kCapacity + 1; i++) {
XCTAssertStatusOk(batch_writer.WriteMessage(TestAnyTimestamp(123, 456)));
}
NSError *err = nil;
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.tmpDir error:&err] count], 0);
XCTAssertNil(err);
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count], 1);
XCTAssertNil(err);
}
- (void)testWriteMessageMultipleFlush {
static const int kCapacity = 5;
static const int kExpectedFlushes = 3;
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
FsSpoolLogBatchWriter batch_writer(writer.get(), kCapacity);
// Ensure batch flushed expected number of times
for (int i = 0; i < kExpectedFlushes * kCapacity + 1; i++) {
XCTAssertStatusOk(batch_writer.WriteMessage(TestAnyTimestamp(123, 456)));
}
NSError *err = nil;
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.tmpDir error:&err] count], 0);
XCTAssertNil(err);
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count],
kExpectedFlushes);
XCTAssertNil(err);
}
- (void)testWriteMessageFlushOnDestroy {
static const int kCapacity = 10;
static const int kNumberOfWrites = 7;
auto writer = std::make_unique<FsSpoolWriter>([self.baseDir UTF8String], kSpoolSize);
{
// Extra scope to enforce early destroy of batch_writer.
FsSpoolLogBatchWriter batch_writer(writer.get(), kCapacity);
for (int i = 0; i < kNumberOfWrites; i++) {
XCTAssertStatusOk(batch_writer.WriteMessage(TestAnyTimestamp(123, 456)));
}
// Ensure nothing was written yet
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.baseDir]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.spoolDir]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.tmpDir]);
}
// Ensure the write happens when FsSpoolLogBatchWriter destructed
NSError *err = nil;
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.tmpDir error:&err] count], 0);
XCTAssertNil(err);
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count], 1);
XCTAssertNil(err);
}
@end

View File

@@ -0,0 +1,76 @@
/// 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_WRITERS_SPOOL_H
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_WRITERS_SPOOL_H
#import <Foundation/Foundation.h>
#include <dispatch/dispatch.h>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool_log_batch_writer.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Writer.h"
// Forward declarations
namespace santa::santad::logs::endpoint_security::writers {
class SpoolPeer;
}
namespace santa::santad::logs::endpoint_security::writers {
class Spool : public Writer, public std::enable_shared_from_this<Spool> {
public:
// Factory
static std::shared_ptr<Spool> Create(std::string_view base_dir, size_t max_spool_disk_size,
size_t max_spool_file_size, uint64_t flush_timeout_ms);
Spool(dispatch_queue_t q, dispatch_source_t timer_source, std::string_view base_dir,
size_t max_spool_disk_size, size_t max_spool_file_size,
void (^write_complete_f)(void) = nullptr, void (^flush_task_complete_f)(void) = nullptr);
~Spool();
void Write(std::vector<uint8_t> &&bytes) override;
bool Flush();
void BeginFlushTask();
// Peer class for testing
friend class santa::santad::logs::endpoint_security::writers::SpoolPeer;
private:
dispatch_queue_t q_ = NULL;
dispatch_source_t timer_source_ = NULL;
::fsspool::FsSpoolWriter spool_writer_;
::fsspool::FsSpoolLogBatchWriter log_batch_writer_;
const size_t spool_file_size_threshold_;
// Make a "leniency factor" of 20%. This will be used to allow some more
// records to accumulate in the event flushing fails for some reason.
const double spool_file_size_threshold_leniency_factor_ = 1.2;
const size_t spool_file_size_threshold_leniency_;
std::string type_url_;
bool flush_task_started_ = false;
void (^write_complete_f_)(void);
void (^flush_task_complete_f_)(void);
size_t accumulated_bytes_ = 0;
};
} // namespace santa::santad::logs::endpoint_security::writers
#endif

View File

@@ -0,0 +1,146 @@
/// 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/santad/Logs/EndpointSecurity/Writers/Spool.h"
#import "Source/common/SNTLogging.h"
#include "Source/common/santa_proto_include_wrapper.h"
#include "absl/strings/string_view.h"
static const char *kTypeGoogleApisComPrefix = "type.googleapis.com/";
namespace santa::santad::logs::endpoint_security::writers {
std::shared_ptr<Spool> Spool::Create(std::string_view base_dir, size_t max_spool_disk_size,
size_t max_spool_batch_size, uint64_t flush_timeout_ms) {
dispatch_queue_t q = dispatch_queue_create("com.google.santa.daemon.file_base_q",
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
dispatch_source_t timer_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
dispatch_source_set_timer(timer_source, dispatch_time(DISPATCH_TIME_NOW, 0),
NSEC_PER_MSEC * flush_timeout_ms, 0);
auto spool_writer =
std::make_shared<Spool>(q, timer_source, base_dir, max_spool_disk_size, max_spool_batch_size);
spool_writer->BeginFlushTask();
return spool_writer;
}
// Note: The `log_batch_writer_` has the batch size set to SIZE_T_MAX. This is because
// the decision on whether or not to flush is controlled by the Spool class here based
// on a "size of bytes" threshold, not "count of records" threshold used by the
// FsSpoolLogBatchWriter. As such, calling `FsSpoolLogBatchWriter::WriteMessage`
// should never flush.
Spool::Spool(dispatch_queue_t q, dispatch_source_t timer_source, std::string_view base_dir,
size_t max_spool_disk_size, size_t max_spool_file_size, void (^write_complete_f)(void),
void (^flush_task_complete_f)(void))
: q_(q),
timer_source_(timer_source),
spool_writer_(absl::string_view(base_dir.data(), base_dir.length()), max_spool_disk_size),
log_batch_writer_(&spool_writer_, SIZE_T_MAX),
spool_file_size_threshold_(max_spool_file_size),
spool_file_size_threshold_leniency_(spool_file_size_threshold_ *
spool_file_size_threshold_leniency_factor_),
write_complete_f_(write_complete_f),
flush_task_complete_f_(flush_task_complete_f) {
type_url_ = kTypeGoogleApisComPrefix + ::santa::pb::v1::SantaMessage::descriptor()->full_name();
}
Spool::~Spool() {
// Note: `log_batch_writer_` is automatically flushed when destroyed
if (!flush_task_started_) {
// The timer_source_ must be resumed to ensure it has a proper retain count before being
// destroyed. Additionally, it should first be cancelled to ensure the timer isn't ever fired
// (see man page for `dispatch_source_cancel(3)`).
dispatch_source_cancel(timer_source_);
dispatch_resume(timer_source_);
}
}
void Spool::BeginFlushTask() {
if (flush_task_started_) {
return;
}
std::weak_ptr<Spool> weak_writer = weak_from_this();
dispatch_source_set_event_handler(timer_source_, ^{
std::shared_ptr<Spool> shared_writer = weak_writer.lock();
if (!shared_writer) {
return;
}
if (!shared_writer->Flush()) {
LOGE(@"Spool writer: periodic flush failed.");
}
if (shared_writer->flush_task_complete_f_) {
shared_writer->flush_task_complete_f_();
}
});
dispatch_resume(timer_source_);
flush_task_started_ = true;
}
bool Spool::Flush() {
if (log_batch_writer_.Flush().ok()) {
accumulated_bytes_ = 0;
return true;
} else {
return false;
}
}
void Spool::Write(std::vector<uint8_t> &&bytes) {
auto shared_this = shared_from_this();
// Workaround to move `bytes` into the block without a copy
__block std::vector<uint8_t> temp_bytes = std::move(bytes);
dispatch_async(q_, ^{
std::vector<uint8_t> moved_bytes = std::move(temp_bytes);
// Manually pack an `Any` with a pre-serialized SantaMessage
google::protobuf::Any any;
#if SANTA_OPEN_SOURCE
any.set_value(moved_bytes.data(), moved_bytes.size());
#else
any.set_value(absl::string_view((const char*)moved_bytes.data(), moved_bytes.size()));
#endif
any.set_type_url(type_url_);
if (shared_this->accumulated_bytes_ >= shared_this->spool_file_size_threshold_) {
shared_this->Flush();
}
// Only write the new message if we have room left.
// This will account for Flush failing above.
// Use the more lenient threshold here in case the Flush failures are transitory.
if (shared_this->accumulated_bytes_ < shared_this->spool_file_size_threshold_leniency_) {
auto status = shared_this->log_batch_writer_.WriteMessage(any);
if (!status.ok()) {
LOGE(@"ProtoEventLogger::LogProto failed with: %s", status.ToString().c_str());
}
shared_this->accumulated_bytes_ += moved_bytes.size();
}
if (shared_this->write_complete_f_) {
shared_this->write_complete_f_();
}
});
}
} // namespace santa::santad::logs::endpoint_security::writers

View File

@@ -0,0 +1,156 @@
/// 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>
#import <XCTest/XCTest.h>
#include <dispatch/dispatch.h>
#include <unistd.h>
#include <memory>
#include "Source/common/TestUtils.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool_log_batch_writer.h"
#include "Source/santad/Logs/EndpointSecurity/Writers/Spool.h"
namespace santa::santad::logs::endpoint_security::writers {
class SpoolPeer : public Spool {
public:
// Make constructors visible
using Spool::Spool;
std::string GetTypeUrl() { return type_url_; }
};
} // namespace santa::santad::logs::endpoint_security::writers
using santa::santad::logs::endpoint_security::writers::SpoolPeer;
@interface SpoolTest : XCTestCase
@property dispatch_queue_t q;
@property dispatch_source_t timer;
@property NSFileManager *fileMgr;
@property NSString *testDir;
@property NSString *baseDir;
@property NSString *spoolDir;
@end
@implementation SpoolTest
- (void)setUp {
self.fileMgr = [NSFileManager defaultManager];
self.testDir = [NSString stringWithFormat:@"%@santa-spool-%d", NSTemporaryDirectory(), getpid()];
self.testDir = [NSString stringWithFormat:@"%@fsspool-%d", NSTemporaryDirectory(), getpid()];
self.baseDir = [NSString stringWithFormat:@"%@/base", self.testDir];
self.spoolDir = [NSString stringWithFormat:@"%@/new", self.baseDir];
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.baseDir]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:self.spoolDir]);
XCTAssertTrue([self.fileMgr createDirectoryAtPath:self.testDir
withIntermediateDirectories:YES
attributes:nil
error:nil]);
self.q = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
XCTAssertNotNil(self.q);
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
XCTAssertNotNil(self.timer);
}
- (void)tearDown {
XCTAssertTrue([self.fileMgr removeItemAtPath:self.testDir error:nil]);
}
- (void)testTypeUrl {
// Ensure the manually created type url isn't modified
auto spool =
std::make_shared<SpoolPeer>(self.q, self.timer, [self.baseDir UTF8String], 10240, 1024);
std::string wantTypeUrl("type.googleapis.com/santa.pb.v1.SantaMessage");
XCTAssertCppStringEqual(spool->GetTypeUrl(), wantTypeUrl);
}
- (void)testWrite {
const size_t writeSize = 50;
const uint64 periodicFlushMS = 400;
NSError *err = nil;
dispatch_semaphore_t semaWrite = dispatch_semaphore_create(0);
dispatch_semaphore_t semaFlush = dispatch_semaphore_create(0);
__block int flushCount = 0;
auto spool = std::make_shared<SpoolPeer>(
self.q, self.timer, [self.baseDir UTF8String], 10240, 1024,
^{
dispatch_semaphore_signal(semaWrite);
},
^{
flushCount++;
if (flushCount <= 2) {
// The first flush is the initial fire.
// The second flush should flush the new contents to disk
// Afterwards, nothing else waits on the semaphore, so stop signaling
dispatch_semaphore_signal(semaFlush);
}
});
// Set a custom timer interval for this test
dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, 0),
NSEC_PER_MSEC * periodicFlushMS, 0);
spool->Write(std::vector<uint8_t>(writeSize, 'A'));
XCTAssertEqual(
0, dispatch_semaphore_wait(semaWrite, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
"Second write didn't compelte within expected window");
// Sleep for a short time. Nothing should happen, but want to help ensure that if somehow
// if somehow timers were active that would be caught and fail the test.
sleep(1);
// Ensure nothing exists yet because periodic flush been started
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count], 0);
// Manual Flush
XCTAssertTrue(spool->Flush());
// A new log entry should exist
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count], 1);
// Start the periodic flush task
spool->BeginFlushTask();
XCTAssertEqual(
0, dispatch_semaphore_wait(semaFlush, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
"Initial flush task firing didn't occur within expected window");
// Ensure no growth in the amount of data
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count], 1);
// Write a second log entry and begin the period
spool->Write(std::vector<uint8_t>(writeSize, 'B'));
XCTAssertEqual(
0, dispatch_semaphore_wait(semaWrite, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
"Second write didn't compelte within expected window");
XCTAssertEqual(
0, dispatch_semaphore_wait(semaFlush, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)),
"Initial flush task firing didn't occur within expected window");
// Ensure the new log entry appears
XCTAssertEqual([[self.fileMgr contentsOfDirectoryAtPath:self.spoolDir error:&err] count], 2);
}
@end

View File

@@ -15,22 +15,40 @@
#ifndef SANTA__SANTAD__METRICS_H
#define SANTA__SANTAD__METRICS_H
#include <EndpointSecurity/EndpointSecurity.h>
#import <Foundation/Foundation.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#include <dispatch/dispatch.h>
#include <memory>
#import "Source/common/SNTMetricSet.h"
namespace santa::santad {
// Test interface - forward declaration
class MetricsPeer;
enum class EventDisposition {
kProcessed = 0,
kDropped,
};
enum class Processor {
kUnknown = 0,
kAuthorizer,
kDeviceManager,
kRecorder,
kTamperResistance,
};
class Metrics : public std::enable_shared_from_this<Metrics> {
public:
static std::shared_ptr<Metrics> Create(uint64_t interval);
static std::shared_ptr<Metrics> Create(SNTMetricSet *metricSet, uint64_t interval);
Metrics(MOLXPCConnection *metrics_connection, dispatch_queue_t q, dispatch_source_t timer_source,
uint64_t interval, void (^run_on_first_start)(void));
uint64_t interval, SNTMetricInt64Gauge *event_processing_times,
SNTMetricCounter *event_counts, void (^run_on_first_start)(void));
~Metrics();
@@ -38,6 +56,9 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
void StopPoll();
void SetInterval(uint64_t interval);
void SetEventMetrics(Processor processor, es_event_type_t event_type,
EventDisposition disposition, int64_t nanos);
friend class santa::santad::MetricsPeer;
private:
@@ -45,11 +66,17 @@ class Metrics : public std::enable_shared_from_this<Metrics> {
dispatch_queue_t q_;
dispatch_source_t timer_source_;
uint64_t interval_;
SNTMetricInt64Gauge *event_processing_times_;
SNTMetricCounter *event_counts_;
// Tracks whether or not the timer_source should be running.
// This helps manage dispatch source state to ensure the source is not
// suspended, resumed, or cancelled while in an improper state.
bool running_;
void (^run_on_first_start_)(void);
// Separate queue used for setting event metrics
// Mitigate issues where capturing metrics could be blocked on exporting
dispatch_queue_t events_q_;
};
} // namespace santa::santad

View File

@@ -13,17 +13,86 @@
/// limitations under the License.
#include "Source/santad/Metrics.h"
#include <EndpointSecurity/ESTypes.h>
#include <memory>
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTMetricSet.h"
#import "Source/common/SNTXPCMetricServiceInterface.h"
#import "Source/santad/SNTApplicationCoreMetrics.h"
static const NSString *kProcessorAuthorizer = @"Authorizer";
static const NSString *kProcessorDeviceManager = @"DeviceManager";
static const NSString *kProcessorRecorder = @"Recorder";
static const NSString *kProcessorTamperResistance = @"TamperResistance";
static const NSString *kEventTypeAuthExec = @"AuthExec";
static const NSString *kEventTypeAuthKextload = @"AuthKextload";
static const NSString *kEventTypeAuthMount = @"AuthMount";
static const NSString *kEventTypeAuthRemount = @"AuthRemount";
static const NSString *kEventTypeAuthRename = @"AuthRename";
static const NSString *kEventTypeAuthUnlink = @"AuthUnlink";
static const NSString *kEventTypeNotifyClose = @"NotifyClose";
static const NSString *kEventTypeNotifyExchangedata = @"NotifyExchangedata";
static const NSString *kEventTypeNotifyExec = @"NotifyExec";
static const NSString *kEventTypeNotifyExit = @"NotifyExit";
static const NSString *kEventTypeNotifyFork = @"NotifyFork";
static const NSString *kEventTypeNotifyLink = @"NotifyLink";
static const NSString *kEventTypeNotifyRename = @"NotifyRename";
static const NSString *kEventTypeNotifyUnlink = @"NotifyUnlink";
static const NSString *kEventTypeNotifyUnmount = @"NotifyUnmount";
static const NSString *kEventDispositionDropped = @"Dropped";
static const NSString *kEventDispositionProcessed = @"Processed";
namespace santa::santad {
std::shared_ptr<Metrics> Metrics::Create(uint64_t interval) {
const NSString *ProcessorToString(Processor processor) {
switch (processor) {
case Processor::kAuthorizer: return kProcessorAuthorizer;
case Processor::kDeviceManager: return kProcessorDeviceManager;
case Processor::kRecorder: return kProcessorRecorder;
case Processor::kTamperResistance: return kProcessorTamperResistance;
default:
[NSException raise:@"Invalid processor" format:@"Unknown processor value: %d", processor];
return nil;
}
}
const NSString *EventTypeToString(es_event_type_t eventType) {
switch (eventType) {
case ES_EVENT_TYPE_AUTH_EXEC: return kEventTypeAuthExec;
case ES_EVENT_TYPE_AUTH_KEXTLOAD: return kEventTypeAuthKextload;
case ES_EVENT_TYPE_AUTH_MOUNT: return kEventTypeAuthMount;
case ES_EVENT_TYPE_AUTH_REMOUNT: return kEventTypeAuthRemount;
case ES_EVENT_TYPE_AUTH_RENAME: return kEventTypeAuthRename;
case ES_EVENT_TYPE_AUTH_UNLINK: return kEventTypeAuthUnlink;
case ES_EVENT_TYPE_NOTIFY_CLOSE: return kEventTypeNotifyClose;
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA: return kEventTypeNotifyExchangedata;
case ES_EVENT_TYPE_NOTIFY_EXEC: return kEventTypeNotifyExec;
case ES_EVENT_TYPE_NOTIFY_EXIT: return kEventTypeNotifyExit;
case ES_EVENT_TYPE_NOTIFY_FORK: return kEventTypeNotifyFork;
case ES_EVENT_TYPE_NOTIFY_LINK: return kEventTypeNotifyLink;
case ES_EVENT_TYPE_NOTIFY_RENAME: return kEventTypeNotifyRename;
case ES_EVENT_TYPE_NOTIFY_UNLINK: return kEventTypeNotifyUnlink;
case ES_EVENT_TYPE_NOTIFY_UNMOUNT: return kEventTypeNotifyUnmount;
default:
[NSException raise:@"Invalid event type" format:@"Invalid event type: %d", eventType];
return nil;
}
}
const NSString *EventDispositionToString(EventDisposition d) {
switch (d) {
case EventDisposition::kDropped: return kEventDispositionDropped;
case EventDisposition::kProcessed: return kEventDispositionProcessed;
default:
[NSException raise:@"Invalid disposition" format:@"Unknown disposition value: %d", d];
return nil;
}
}
std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metricSet, uint64_t interval) {
dispatch_queue_t q = dispatch_queue_create("com.google.santa.santametricsservice.q",
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
@@ -31,8 +100,18 @@ std::shared_ptr<Metrics> Metrics::Create(uint64_t interval) {
MOLXPCConnection *metrics_connection = [SNTXPCMetricServiceInterface configuredConnection];
std::shared_ptr<Metrics> metrics =
std::make_shared<Metrics>(metrics_connection, q, timer_source, interval, ^() {
SNTMetricInt64Gauge *event_processing_times =
[metricSet int64GaugeWithName:@"/santa/event_processing_time"
fieldNames:@[ @"Processor", @"Event" ]
helpText:@"Time to process various event types by each processor"];
SNTMetricCounter *event_counts =
[metricSet counterWithName:@"/santa/event_count"
fieldNames:@[ @"Processor", @"Event", @"Disposition" ]
helpText:@"Events received and processed by each processor"];
std::shared_ptr<Metrics> metrics = std::make_shared<Metrics>(
metrics_connection, q, timer_source, interval, event_processing_times, event_counts, ^() {
SNTRegisterCoreMetrics();
[metrics_connection resume];
});
@@ -44,13 +123,8 @@ std::shared_ptr<Metrics> Metrics::Create(uint64_t interval) {
return;
}
// Ensure we're marked as `running_`, otherwise bail
if (!shared_metrics->running_) {
return;
}
[[shared_metrics->metrics_connection_ remoteObjectProxy]
exportForMonitoring:[[SNTMetricSet sharedInstance] export]];
exportForMonitoring:[metricSet export]];
});
return metrics;
@@ -58,20 +132,28 @@ std::shared_ptr<Metrics> Metrics::Create(uint64_t interval) {
Metrics::Metrics(MOLXPCConnection *metrics_connection, dispatch_queue_t q,
dispatch_source_t timer_source, uint64_t interval,
SNTMetricInt64Gauge *event_processing_times, SNTMetricCounter *event_counts,
void (^run_on_first_start)(void))
: q_(q),
timer_source_(timer_source),
interval_(interval),
event_processing_times_(event_processing_times),
event_counts_(event_counts),
running_(false),
run_on_first_start_(run_on_first_start) {
metrics_connection_ = metrics_connection;
SetInterval(interval_);
events_q_ = dispatch_queue_create("com.google.santa.santametricsservice.events_q",
DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
}
Metrics::~Metrics() {
if (!running_) {
// The source must be resumed prior to being cancelled. However, do not
// set `running_` to true so that nothing will get exported.
// The timer_source_ must be resumed to ensure it has a proper retain count before being
// destroyed. Additionally, it should first be cancelled to ensure the timer isn't ever fired
// (see man page for `dispatch_source_cancel(3)`).
dispatch_source_cancel(timer_source_);
dispatch_resume(timer_source_);
}
}
@@ -114,4 +196,16 @@ void Metrics::StopPoll() {
});
}
void Metrics::SetEventMetrics(Processor processor, es_event_type_t event_type,
EventDisposition event_disposition, int64_t nanos) {
dispatch_async(events_q_, ^{
NSString *processorName = (NSString *)ProcessorToString(processor);
NSString *eventName = (NSString *)EventTypeToString(event_type);
NSString *disposition = (NSString *)EventDispositionToString(event_disposition);
[event_counts_ incrementForFieldValues:@[ processorName, eventName, disposition ]];
[event_processing_times_ set:nanos forFieldValues:@[ processorName, eventName ]];
});
}
} // namespace santa::santad

View File

@@ -12,15 +12,27 @@
/// 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 <dispatch/dispatch.h>
#include <map>
#include "Source/common/SNTMetricSet.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/Metrics.h"
using santa::santad::EventDisposition;
using santa::santad::Processor;
namespace santa::santad {
extern const NSString *ProcessorToString(Processor processor);
extern const NSString *EventTypeToString(es_event_type_t eventType);
extern const NSString *EventDispositionToString(EventDisposition d);
class MetricsPeer : public Metrics {
public:
// Make base class constructors visible
@@ -33,12 +45,14 @@ class MetricsPeer : public Metrics {
} // namespace santa::santad
using santa::santad::EventDispositionToString;
using santa::santad::EventTypeToString;
using santa::santad::MetricsPeer;
using santa::santad::ProcessorToString;
@interface MetricsTest : XCTestCase
@property dispatch_queue_t q;
@property dispatch_semaphore_t sema;
@property dispatch_source_t timer;
@end
@implementation MetricsTest
@@ -46,13 +60,12 @@ using santa::santad::MetricsPeer;
- (void)setUp {
self.q = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL);
XCTAssertNotNil(self.q);
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
XCTAssertNotNil(self.timer);
self.sema = dispatch_semaphore_create(0);
}
- (void)testStartStop {
auto metrics = std::make_shared<MetricsPeer>(nil, self.q, self.timer, 100, ^{
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(nil, self.q, timer, 100, nil, nil, ^{
dispatch_semaphore_signal(self.sema);
});
@@ -86,7 +99,8 @@ using santa::santad::MetricsPeer;
}
- (void)testSetInterval {
auto metrics = std::make_shared<MetricsPeer>(nil, self.q, self.timer, 100,
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(nil, self.q, timer, 100, nil, nil,
^{
});
@@ -96,4 +110,88 @@ using santa::santad::MetricsPeer;
XCTAssertEqual(200, metrics->Interval());
}
- (void)testProcessorToString {
std::map<Processor, NSString *> processorToString = {
{Processor::kAuthorizer, @"Authorizer"},
{Processor::kDeviceManager, @"DeviceManager"},
{Processor::kRecorder, @"Recorder"},
{Processor::kTamperResistance, @"TamperResistance"},
};
for (const auto &kv : processorToString) {
XCTAssertEqualObjects(ProcessorToString(kv.first), kv.second);
}
XCTAssertThrows(ProcessorToString((Processor)12345));
}
- (void)testEventTypeToString {
std::map<es_event_type_t, NSString *> eventTypeToString = {
{ES_EVENT_TYPE_AUTH_EXEC, @"AuthExec"},
{ES_EVENT_TYPE_AUTH_KEXTLOAD, @"AuthKextload"},
{ES_EVENT_TYPE_AUTH_MOUNT, @"AuthMount"},
{ES_EVENT_TYPE_AUTH_REMOUNT, @"AuthRemount"},
{ES_EVENT_TYPE_AUTH_RENAME, @"AuthRename"},
{ES_EVENT_TYPE_AUTH_UNLINK, @"AuthUnlink"},
{ES_EVENT_TYPE_NOTIFY_CLOSE, @"NotifyClose"},
{ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA, @"NotifyExchangedata"},
{ES_EVENT_TYPE_NOTIFY_EXEC, @"NotifyExec"},
{ES_EVENT_TYPE_NOTIFY_EXIT, @"NotifyExit"},
{ES_EVENT_TYPE_NOTIFY_FORK, @"NotifyFork"},
{ES_EVENT_TYPE_NOTIFY_LINK, @"NotifyLink"},
{ES_EVENT_TYPE_NOTIFY_RENAME, @"NotifyRename"},
{ES_EVENT_TYPE_NOTIFY_UNLINK, @"NotifyUnlink"},
{ES_EVENT_TYPE_NOTIFY_UNMOUNT, @"NotifyUnmount"},
};
for (const auto &kv : eventTypeToString) {
XCTAssertEqualObjects(EventTypeToString(kv.first), kv.second);
}
XCTAssertThrows(EventTypeToString((es_event_type_t)12345));
}
- (void)testEventDispositionToString {
std::map<EventDisposition, NSString *> dispositionToString = {
{EventDisposition::kDropped, @"Dropped"},
{EventDisposition::kProcessed, @"Processed"},
};
for (const auto &kv : dispositionToString) {
XCTAssertEqualObjects(EventDispositionToString(kv.first), kv.second);
}
XCTAssertThrows(EventDispositionToString((EventDisposition)12345));
}
- (void)testSetEventMetrics {
id mockEventProcessingTimes = OCMClassMock([SNTMetricInt64Gauge class]);
id mockEventCounts = OCMClassMock([SNTMetricCounter class]);
int64_t nanos = 1234;
OCMStub([mockEventCounts incrementForFieldValues:[OCMArg any]]).andDo(^(NSInvocation *inv) {
dispatch_semaphore_signal(self.sema);
});
OCMStub([(SNTMetricInt64Gauge *)mockEventProcessingTimes set:nanos forFieldValues:[OCMArg any]])
.ignoringNonObjectArgs()
.andDo(^(NSInvocation *inv) {
dispatch_semaphore_signal(self.sema);
});
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.q);
auto metrics = std::make_shared<MetricsPeer>(nil, self.q, timer, 100, mockEventProcessingTimes,
mockEventCounts,
^{
// This block intentionally left blank
});
metrics->SetEventMetrics(Processor::kAuthorizer, ES_EVENT_TYPE_AUTH_EXEC,
EventDisposition::kProcessed, nanos);
// Note: Wait on the semaphore twice, once for each metric
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to update");
XCTAssertSemaTrue(self.sema, 5, "Failed waiting for metrics to update");
}
@end

View File

@@ -55,7 +55,7 @@ SNTCachedDecision *MakeCachedDecision(struct stat sb, SNTEventState decision) {
- (void)testBasicOperation {
SNTDecisionCache *dc = [SNTDecisionCache sharedCache];
struct stat sb = MakeStat(1234);
struct stat sb = MakeStat();
// First make sure the item isn't in the cache
XCTAssertNil([dc cachedDecisionForFile:sb]);
@@ -77,7 +77,7 @@ SNTCachedDecision *MakeCachedDecision(struct stat sb, SNTEventState decision) {
- (void)testResetTimestampForCachedDecision {
SNTDecisionCache *dc = [SNTDecisionCache sharedCache];
struct stat sb = MakeStat(1234);
struct stat sb = MakeStat();
SNTCachedDecision *cd = MakeCachedDecision(sb, SNTEventStateAllowTransitive);
[dc cacheDecision:cd];

View File

@@ -17,7 +17,8 @@
#import <MOLXPCConnection/MOLXPCConnection.h>
#include "Source/common/SNTPrefixTree.h"
#include "Source/common/PrefixTree.h"
#include "Source/common/Unit.h"
#include "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
@@ -42,6 +43,7 @@ void SantadMain(
SNTCompilerController* compiler_controller,
SNTNotificationQueue* notifier_queue, SNTSyncdQueue* syncd_queue,
SNTExecutionController* exec_controller,
std::shared_ptr<SNTPrefixTree> prefix_tree);
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>
prefix_tree);
#endif

View File

@@ -16,6 +16,7 @@
#include <memory>
#include "Source/common/PrefixTree.h"
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTKVOManager.h"
@@ -32,6 +33,8 @@
#include "Source/santad/Logs/EndpointSecurity/Logger.h"
#include "Source/santad/SNTDaemonControlController.h"
using santa::common::PrefixTree;
using santa::common::Unit;
using santa::santad::Metrics;
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::FlushCacheMode;
@@ -67,7 +70,7 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
MOLXPCConnection *control_connection, SNTCompilerController *compiler_controller,
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
SNTExecutionController *exec_controller,
std::shared_ptr<SNTPrefixTree> prefix_tree) {
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree) {
SNTConfigurator *configurator = [SNTConfigurator configurator];
SNTDaemonControlController *dc =
@@ -85,6 +88,7 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
SNTEndpointSecurityDeviceManager *device_client =
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:esapi
metrics:metrics
logger:logger
authResultCache:auth_result_cache];
@@ -94,12 +98,13 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
[[notifier_queue.notifierConnection remoteObjectProxy]
postUSBBlockNotification:event
withCustomMessage:([configurator remountUSBMode]
? [configurator bannedUSBBlockMessage]
: [configurator remountUSBBlockMessage])];
? [configurator remountUSBBlockMessage]
: [configurator bannedUSBBlockMessage])];
};
SNTEndpointSecurityRecorder *monitor_client =
[[SNTEndpointSecurityRecorder alloc] initWithESAPI:esapi
metrics:metrics
logger:logger
enricher:enricher
compilerController:compiler_controller
@@ -108,12 +113,13 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
SNTEndpointSecurityAuthorizer *authorizer_client =
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:esapi
metrics:metrics
execController:exec_controller
compilerController:compiler_controller
authResultCache:auth_result_cache];
SNTEndpointSecurityTamperResistance *tamper_client =
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:esapi logger:logger];
[[SNTEndpointSecurityTamperResistance alloc] initWithESAPI:esapi metrics:metrics logger:logger];
EstablishSyncServiceConnection(syncd_queue);
@@ -275,10 +281,14 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
// of the SNTKVOManager objects it contains.
(void)kvoObservers;
[monitor_client enable];
// IMPORTANT: ES will hold up third party execs until early boot clients make
// their first subscription. Ensuring the `Authorizer` client is enabled first
// means that the AUTH EXEC event is subscribed first and Santa can apply
// execution policy appropriately.
[authorizer_client enable];
[device_client enable];
[tamper_client enable];
[monitor_client enable];
[device_client enable];
[[NSRunLoop mainRunLoop] run];
}

View File

@@ -15,12 +15,15 @@
#ifndef SANTA__SANTAD__SANTAD_DEPS_H
#define SANTA__SANTAD__SANTAD_DEPS_H
#include <Foundation/Foundation.h>
#import <Foundation/Foundation.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#include <memory>
#include "Source/common/SNTPrefixTree.h"
#include "Source/common/PrefixTree.h"
#include "Source/common/SNTConfigurator.h"
#import "Source/common/SNTMetricSet.h"
#include "Source/common/Unit.h"
#include "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
@@ -35,22 +38,29 @@ namespace santa::santad {
class SantadDeps {
public:
static std::unique_ptr<SantadDeps> Create(NSUInteger metric_export_interval,
SNTEventLogType event_log_type,
NSString *event_log_path,
NSArray<NSString *> *prefix_filters);
static std::unique_ptr<SantadDeps> Create(SNTConfigurator *configurator,
SNTMetricSet *metric_set);
SantadDeps(
NSUInteger metric_export_interval,
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi,
std::unique_ptr<santa::santad::logs::endpoint_security::Logger> logger,
MOLXPCConnection *control_connection, SNTCompilerController *compiler_controller,
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
SNTExecutionController *exec_controller, std::shared_ptr<SNTPrefixTree> prefix_tree);
std::shared_ptr<santa::santad::event_providers::endpoint_security::
EndpointSecurityAPI>
esapi,
std::shared_ptr<santa::santad::Metrics> metrics,
std::unique_ptr<santa::santad::logs::endpoint_security::Logger> logger,
MOLXPCConnection *control_connection,
SNTCompilerController *compiler_controller,
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
SNTExecutionController *exec_controller,
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>>
prefix_tree);
std::shared_ptr<santa::santad::event_providers::AuthResultCache> AuthResultCache();
std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher> Enricher();
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> ESAPI();
std::shared_ptr<santa::santad::event_providers::AuthResultCache>
AuthResultCache();
std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>
Enricher();
std::shared_ptr<
santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>
ESAPI();
std::shared_ptr<santa::santad::logs::endpoint_security::Logger> Logger();
std::shared_ptr<santa::santad::Metrics> Metrics();
MOLXPCConnection *ControlConnection();
@@ -58,21 +68,25 @@ class SantadDeps {
SNTNotificationQueue *NotifierQueue();
SNTSyncdQueue *SyncdQueue();
SNTExecutionController *ExecController();
std::shared_ptr<SNTPrefixTree> PrefixTree();
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> PrefixTree();
private:
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
std::shared_ptr<
santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>
esapi_;
std::shared_ptr<santa::santad::logs::endpoint_security::Logger> logger_;
std::shared_ptr<santa::santad::Metrics> metrics_;
std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher> enricher_;
std::shared_ptr<santa::santad::event_providers::AuthResultCache> auth_result_cache_;
std::shared_ptr<santa::santad::event_providers::endpoint_security::Enricher>
enricher_;
std::shared_ptr<santa::santad::event_providers::AuthResultCache>
auth_result_cache_;
MOLXPCConnection *control_connection_;
SNTCompilerController *compiler_controller_;
SNTNotificationQueue *notifier_queue_;
SNTSyncdQueue *syncd_queue_;
SNTExecutionController *exec_controller_;
std::shared_ptr<SNTPrefixTree> prefix_tree_;
std::shared_ptr<santa::common::PrefixTree<santa::common::Unit>> prefix_tree_;
};
} // namespace santa::santad

View File

@@ -13,16 +13,18 @@
/// limitations under the License.
#include "Source/santad/SantadDeps.h"
#include <memory>
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTMetricSet.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santad/DataLayer/SNTEventTable.h"
#import "Source/santad/DataLayer/SNTRuleTable.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
#import "Source/santad/SNTDatabaseController.h"
using santa::common::PrefixTree;
using santa::common::Unit;
using santa::santad::Metrics;
using santa::santad::event_providers::AuthResultCache;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
@@ -31,10 +33,8 @@ using santa::santad::logs::endpoint_security::Logger;
namespace santa::santad {
std::unique_ptr<SantadDeps> SantadDeps::Create(NSUInteger metric_export_interval,
SNTEventLogType event_log_type,
NSString *event_log_path,
NSArray<NSString *> *prefix_filters) {
std::unique_ptr<SantadDeps> SantadDeps::Create(SNTConfigurator *configurator,
SNTMetricSet *metric_set) {
// TODO(mlw): The XPC interfaces should be injectable. Could either make a new
// protocol defining appropriate methods or accept values as params.
MOLXPCConnection *control_connection =
@@ -87,9 +87,13 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(NSUInteger metric_export_interval
exit(EXIT_FAILURE);
}
std::shared_ptr<SNTPrefixTree> prefix_tree = std::make_shared<SNTPrefixTree>();
std::shared_ptr<::PrefixTree<Unit>> prefix_tree = std::make_shared<::PrefixTree<Unit>>();
// TODO(bur): Add KVO handling for fileChangesPrefixFilters.
NSArray<NSString *> *prefix_filters =
[@[ @"/.", @"/dev/" ] arrayByAddingObjectsFromArray:[configurator fileChangesPrefixFilters]];
for (NSString *filter in prefix_filters) {
prefix_tree->AddPrefix([filter fileSystemRepresentation]);
prefix_tree->InsertPrefix([filter fileSystemRepresentation], Unit {});
}
std::shared_ptr<EndpointSecurityAPI> esapi = std::make_shared<EndpointSecurityAPI>();
@@ -98,27 +102,40 @@ std::unique_ptr<SantadDeps> SantadDeps::Create(NSUInteger metric_export_interval
exit(EXIT_FAILURE);
}
std::unique_ptr<::Logger> logger = Logger::Create(esapi, event_log_type, event_log_path);
size_t spool_file_threshold_bytes = [configurator spoolDirectoryFileSizeThresholdKB] * 1024;
size_t spool_dir_threshold_bytes = [configurator spoolDirectorySizeThresholdMB] * 1024 * 1024;
uint64_t spool_flush_timeout_ms = [configurator spoolDirectoryEventMaxFlushTimeSec] * 1000;
std::unique_ptr<::Logger> logger = Logger::Create(
esapi, [configurator eventLogType], [configurator eventLogPath], [configurator spoolDirectory],
spool_dir_threshold_bytes, spool_file_threshold_bytes, spool_flush_timeout_ms);
if (!logger) {
LOGE(@"Failed to create logger.");
exit(EXIT_FAILURE);
}
return std::make_unique<SantadDeps>(metric_export_interval, esapi, std::move(logger),
control_connection, compiler_controller, notifier_queue,
syncd_queue, exec_controller, prefix_tree);
std::shared_ptr<::Metrics> metrics =
Metrics::Create(metric_set, [configurator metricExportInterval]);
if (!metrics) {
LOGE(@"Failed to create metrics");
exit(EXIT_FAILURE);
}
return std::make_unique<SantadDeps>(esapi, metrics, std::move(logger), control_connection,
compiler_controller, notifier_queue, syncd_queue,
exec_controller, prefix_tree);
}
SantadDeps::SantadDeps(NSUInteger metric_export_interval,
std::shared_ptr<EndpointSecurityAPI> esapi, std::unique_ptr<::Logger> logger,
SantadDeps::SantadDeps(std::shared_ptr<EndpointSecurityAPI> esapi,
std::shared_ptr<::Metrics> metrics, std::unique_ptr<::Logger> logger,
MOLXPCConnection *control_connection,
SNTCompilerController *compiler_controller,
SNTNotificationQueue *notifier_queue, SNTSyncdQueue *syncd_queue,
SNTExecutionController *exec_controller,
std::shared_ptr<SNTPrefixTree> prefix_tree)
std::shared_ptr<::PrefixTree<Unit>> prefix_tree)
: esapi_(std::move(esapi)),
logger_(std::move(logger)),
metrics_(Metrics::Create(metric_export_interval)),
metrics_(std::move(metrics)),
enricher_(std::make_shared<::Enricher>()),
auth_result_cache_(std::make_shared<::AuthResultCache>(esapi_)),
control_connection_(control_connection),
@@ -143,7 +160,7 @@ std::shared_ptr<Logger> SantadDeps::Logger() {
return logger_;
}
std::shared_ptr<santa::santad::Metrics> SantadDeps::Metrics() {
std::shared_ptr<::Metrics> SantadDeps::Metrics() {
return metrics_;
}
@@ -167,7 +184,7 @@ SNTExecutionController *SantadDeps::ExecController() {
return exec_controller_;
}
std::shared_ptr<SNTPrefixTree> SantadDeps::PrefixTree() {
std::shared_ptr<PrefixTree<Unit>> SantadDeps::PrefixTree() {
return prefix_tree_;
}

View File

@@ -29,6 +29,7 @@
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
#import "Source/santad/EventProviders/SNTEndpointSecurityAuthorizer.h"
#import "Source/santad/Metrics.h"
#import "Source/santad/SNTDatabaseController.h"
#include "Source/santad/SantadDeps.h"
@@ -75,12 +76,11 @@ NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
OCMStub([self.mockSNTDatabaseController databasePath]).andReturn(testPath);
std::unique_ptr<SantadDeps> deps =
SantadDeps::Create([mockConfigurator metricExportInterval], [mockConfigurator eventLogType],
[mockConfigurator eventLogPath], @[ @"/.", @"/dev/" ]);
std::unique_ptr<SantadDeps> deps = SantadDeps::Create(mockConfigurator, nil);
SNTEndpointSecurityAuthorizer *authClient =
[[SNTEndpointSecurityAuthorizer alloc] initWithESAPI:mockESApi
metrics:deps->Metrics()
execController:deps->ExecController()
compilerController:deps->CompilerController()
authResultCache:deps->AuthResultCache()];
@@ -134,7 +134,10 @@ NSString *testBinariesPath = @"santa/Source/santad/testdata/binaryrules";
return heapESMsg;
});
[authClient handleMessage:Message(mockESApi, &esMsg)];
[authClient handleMessage:Message(mockESApi, &esMsg)
recordEventMetrics:^(santa::santad::EventDisposition d){
// This block intentionally left blank
}];
[self waitForExpectations:@[ expectation ] timeout:10.0];

View File

@@ -19,6 +19,7 @@
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTMetricSet.h"
#import "Source/santad/Santad.h"
#include "Source/santad/SantadDeps.h"
@@ -150,15 +151,8 @@ int main(int argc, char *argv[]) {
LOGE(@"Failed to start Santa watchdog");
}
SNTConfigurator *configurator = [SNTConfigurator configurator];
// TODO(bur): Add KVO handling for fileChangesPrefixFilters.
NSArray<NSString *> *prefix_filters =
[@[ @"/.", @"/dev/" ] arrayByAddingObjectsFromArray:[configurator fileChangesPrefixFilters]];
std::unique_ptr<SantadDeps> deps =
SantadDeps::Create([configurator metricExportInterval], [configurator eventLogType],
[configurator eventLogPath], prefix_filters);
SantadDeps::Create([SNTConfigurator configurator], [SNTMetricSet sharedInstance]);
// This doesn't return
SantadMain(deps->ESAPI(), deps->Logger(), deps->Metrics(), deps->Enricher(),

View File

@@ -5,3 +5,9 @@ filegroup(
srcs = glob(["binaryrules/*"]),
visibility = ["//:santa_package_group"],
)
filegroup(
name = "protobuf_json_testdata",
srcs = glob(["protobuf/**/*.json"]),
visibility = ["//:santa_package_group"],
)

View File

@@ -0,0 +1,61 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2
},
"effective_group": {
"gid": -1
},
"real_user": {
"uid": -2
},
"real_group": {
"gid": -1
},
"executable": {
"path": "foo",
"truncated": false
}
},
"target": {
"path": "close_file",
"truncated": false,
"stat": {
"dev": 301,
"mode": 302,
"nlink": 303,
"ino": "304",
"user": {
"uid": -2
},
"group": {
"gid": -1
},
"rdev": 305,
"access_time": "1970-01-01T00:06:40.000000500Z",
"modification_time": "1970-01-01T00:06:41.000000321Z",
"change_time": "1970-01-01T00:06:42.000000502Z",
"birth_time": "1970-01-01T00:06:43.000000503Z",
"size": "306",
"blocks": "307",
"blksize": 308,
"flags": 309,
"gen": 310
},
"hash": {
"type": "HASH_ALGO_SHA256",
"hash": "hash_value"
}
}
}

View File

@@ -0,0 +1,64 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo",
"truncated": false
}
},
"target": {
"path": "close_file",
"truncated": false,
"stat": {
"dev": 301,
"mode": 302,
"nlink": 303,
"ino": "304",
"user": {
"uid": -2,
"name": "nobody"
},
"group": {
"gid": -1,
"name": "nogroup"
},
"rdev": 305,
"access_time": "1970-01-01T00:06:40.000000500Z",
"modification_time": "1970-01-01T00:06:41.000000321Z",
"change_time": "1970-01-01T00:06:42.000000502Z",
"birth_time": "1970-01-01T00:06:43.000000503Z",
"size": "306",
"blocks": "307",
"blksize": 308,
"flags": 309,
"gen": 310
}
},
"modified": true
}

View File

@@ -0,0 +1,91 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo",
"truncated": false
}
},
"file1": {
"path": "exchange_file_1",
"truncated": false,
"stat": {
"dev": 301,
"mode": 302,
"nlink": 303,
"ino": "304",
"user": {
"uid": -2,
"name": "nobody"
},
"group": {
"gid": -1,
"name": "nogroup"
},
"rdev": 305,
"access_time": "1970-01-01T00:06:40.000000500Z",
"modification_time": "1970-01-01T00:06:41.000000321Z",
"change_time": "1970-01-01T00:06:42.000000502Z",
"birth_time": "1970-01-01T00:06:43.000000503Z",
"size": "306",
"blocks": "307",
"blksize": 308,
"flags": 309,
"gen": 310
}
},
"file2": {
"path": "exchange_file_1",
"truncated": false,
"stat": {
"dev": 401,
"mode": 402,
"nlink": 403,
"ino": "404",
"user": {
"uid": -2,
"name": "nobody"
},
"group": {
"gid": -1,
"name": "nogroup"
},
"rdev": 405,
"access_time": "1970-01-01T00:08:20.000000600Z",
"modification_time": "1970-01-01T00:08:21.000000421Z",
"change_time": "1970-01-01T00:08:22.000000602Z",
"birth_time": "1970-01-01T00:08:23.000000603Z",
"size": "406",
"blocks": "407",
"blksize": 408,
"flags": 409,
"gen": 410
}
}
}

View File

@@ -0,0 +1,124 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo",
"truncated": false
}
},
"target": {
"id": {
"pid": 23,
"pidversion": 45
},
"parent_id": {
"pid": 67,
"pidversion": 89
},
"original_parent_pid": 67,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"is_platform_binary": true,
"is_es_client": true,
"code_signature": {
"cdhash": "QUFBQUFBQUFBQUFBQUFBQUFBQUE=",
"signing_id": "my_signing_id",
"team_id": "my_team_id"
},
"cs_flags": 536871680,
"executable": {
"path": "fooexec",
"truncated": false,
"stat": {
"dev": 301,
"mode": 302,
"nlink": 303,
"ino": "304",
"user": {
"uid": -2,
"name": "nobody"
},
"group": {
"gid": -1,
"name": "nogroup"
},
"rdev": 305,
"access_time": "1970-01-01T00:06:40.000000500Z",
"modification_time": "1970-01-01T00:06:41.000000321Z",
"change_time": "1970-01-01T00:06:42.000000502Z",
"birth_time": "1970-01-01T00:06:43.000000503Z",
"size": "306",
"blocks": "307",
"blksize": 308,
"flags": 309,
"gen": 310
},
"hash": {
"type": "HASH_ALGO_SHA256",
"hash": "1234_file_hash"
}
}
},
"args": [
"exec_path",
"-l",
"--foo"
],
"envs": [
"ENV_PATH=/path/to/bin:/and/another",
"DEBUG=1"
],
"decision": "DECISION_ALLOW",
"reason": "REASON_BINARY",
"mode": "MODE_LOCKDOWN",
"certificate_info": {
"hash": {
"type": "HASH_ALGO_SHA256",
"hash": "5678_cert_hash"
}
},
"explain": "extra!",
"quarantine_url": "google.com"
}

View File

@@ -0,0 +1,38 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo",
"truncated": false
}
},
"exited": {
"exit_status": 1
}
}

View File

@@ -0,0 +1,68 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo",
"truncated": false
}
},
"child": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo_child",
"truncated": false
}
}
}

View File

@@ -0,0 +1,64 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo",
"truncated": false
}
},
"source": {
"path": "source",
"truncated": false,
"stat": {
"dev": 301,
"mode": 302,
"nlink": 303,
"ino": "304",
"user": {
"uid": -2,
"name": "nobody"
},
"group": {
"gid": -1,
"name": "nogroup"
},
"rdev": 305,
"access_time": "1970-01-01T00:06:40.000000500Z",
"modification_time": "1970-01-01T00:06:41.000000321Z",
"change_time": "1970-01-01T00:06:42.000000502Z",
"birth_time": "1970-01-01T00:06:43.000000503Z",
"size": "306",
"blocks": "307",
"blksize": 308,
"flags": 309,
"gen": 310
}
},
"target": "target_dir/target_file"
}

View File

@@ -0,0 +1,65 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo",
"truncated": false
}
},
"source": {
"path": "source",
"truncated": false,
"stat": {
"dev": 301,
"mode": 302,
"nlink": 303,
"ino": "304",
"user": {
"uid": -2,
"name": "nobody"
},
"group": {
"gid": -1,
"name": "nogroup"
},
"rdev": 305,
"access_time": "1970-01-01T00:06:40.000000500Z",
"modification_time": "1970-01-01T00:06:41.000000321Z",
"change_time": "1970-01-01T00:06:42.000000502Z",
"birth_time": "1970-01-01T00:06:43.000000503Z",
"size": "306",
"blocks": "307",
"blksize": 308,
"flags": 309,
"gen": 310
}
},
"target": "target_dir/target_file",
"target_existed": false
}

View File

@@ -0,0 +1,63 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo",
"truncated": false
}
},
"target": {
"path": "unlink_file",
"truncated": false,
"stat": {
"dev": 301,
"mode": 302,
"nlink": 303,
"ino": "304",
"user": {
"uid": -2,
"name": "nobody"
},
"group": {
"gid": -1,
"name": "nogroup"
},
"rdev": 305,
"access_time": "1970-01-01T00:06:40.000000500Z",
"modification_time": "1970-01-01T00:06:41.000000321Z",
"change_time": "1970-01-01T00:06:42.000000502Z",
"birth_time": "1970-01-01T00:06:43.000000503Z",
"size": "306",
"blocks": "307",
"blksize": 308,
"flags": 309,
"gen": 310
}
}
}

View File

@@ -0,0 +1,61 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2
},
"effective_group": {
"gid": -1
},
"real_user": {
"uid": -2
},
"real_group": {
"gid": -1
},
"executable": {
"path": "foo",
"truncated": false
}
},
"target": {
"path": "close_file",
"truncated": false,
"stat": {
"dev": 301,
"mode": 302,
"nlink": 303,
"ino": "304",
"user": {
"uid": -2
},
"group": {
"gid": -1
},
"rdev": 305,
"access_time": "1970-01-01T00:06:40.000000500Z",
"modification_time": "1970-01-01T00:06:41.000000321Z",
"change_time": "1970-01-01T00:06:42.000000502Z",
"birth_time": "1970-01-01T00:06:43.000000503Z",
"size": "306",
"blocks": "307",
"blksize": 308,
"flags": 309,
"gen": 310
},
"hash": {
"type": "HASH_ALGO_SHA256",
"hash": "hash_value"
}
}
}

View File

@@ -0,0 +1,64 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo",
"truncated": false
}
},
"target": {
"path": "close_file",
"truncated": false,
"stat": {
"dev": 301,
"mode": 302,
"nlink": 303,
"ino": "304",
"user": {
"uid": -2,
"name": "nobody"
},
"group": {
"gid": -1,
"name": "nogroup"
},
"rdev": 305,
"access_time": "1970-01-01T00:06:40.000000500Z",
"modification_time": "1970-01-01T00:06:41.000000321Z",
"change_time": "1970-01-01T00:06:42.000000502Z",
"birth_time": "1970-01-01T00:06:43.000000503Z",
"size": "306",
"blocks": "307",
"blksize": 308,
"flags": 309,
"gen": 310
}
},
"modified": true
}

View File

@@ -0,0 +1,91 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo",
"truncated": false
}
},
"file1": {
"path": "exchange_file_1",
"truncated": false,
"stat": {
"dev": 301,
"mode": 302,
"nlink": 303,
"ino": "304",
"user": {
"uid": -2,
"name": "nobody"
},
"group": {
"gid": -1,
"name": "nogroup"
},
"rdev": 305,
"access_time": "1970-01-01T00:06:40.000000500Z",
"modification_time": "1970-01-01T00:06:41.000000321Z",
"change_time": "1970-01-01T00:06:42.000000502Z",
"birth_time": "1970-01-01T00:06:43.000000503Z",
"size": "306",
"blocks": "307",
"blksize": 308,
"flags": 309,
"gen": 310
}
},
"file2": {
"path": "exchange_file_1",
"truncated": false,
"stat": {
"dev": 401,
"mode": 402,
"nlink": 403,
"ino": "404",
"user": {
"uid": -2,
"name": "nobody"
},
"group": {
"gid": -1,
"name": "nogroup"
},
"rdev": 405,
"access_time": "1970-01-01T00:08:20.000000600Z",
"modification_time": "1970-01-01T00:08:21.000000421Z",
"change_time": "1970-01-01T00:08:22.000000602Z",
"birth_time": "1970-01-01T00:08:23.000000603Z",
"size": "406",
"blocks": "407",
"blksize": 408,
"flags": 409,
"gen": 410
}
}
}

View File

@@ -0,0 +1,152 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo",
"truncated": false
}
},
"target": {
"id": {
"pid": 23,
"pidversion": 45
},
"parent_id": {
"pid": 67,
"pidversion": 89
},
"original_parent_pid": 67,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"is_platform_binary": true,
"is_es_client": true,
"code_signature": {
"cdhash": "QUFBQUFBQUFBQUFBQUFBQUFBQUE=",
"signing_id": "my_signing_id",
"team_id": "my_team_id"
},
"cs_flags": 536871680,
"executable": {
"path": "fooexec",
"truncated": false,
"stat": {
"dev": 301,
"mode": 302,
"nlink": 303,
"ino": "304",
"user": {
"uid": -2,
"name": "nobody"
},
"group": {
"gid": -1,
"name": "nogroup"
},
"rdev": 305,
"access_time": "1970-01-01T00:06:40.000000500Z",
"modification_time": "1970-01-01T00:06:41.000000321Z",
"change_time": "1970-01-01T00:06:42.000000502Z",
"birth_time": "1970-01-01T00:06:43.000000503Z",
"size": "306",
"blocks": "307",
"blksize": 308,
"flags": 309,
"gen": 310
},
"hash": {
"type": "HASH_ALGO_SHA256",
"hash": "1234_file_hash"
}
}
},
"script": {
"path": "script.sh",
"truncated": false,
"stat": {
"dev": 501,
"mode": 502,
"nlink": 503,
"ino": "504",
"user": {
"uid": -2,
"name": "nobody"
},
"group": {
"gid": -1,
"name": "nogroup"
},
"rdev": 505,
"access_time": "1970-01-01T00:10:00.000000700Z",
"modification_time": "1970-01-01T00:10:01.000000521Z",
"change_time": "1970-01-01T00:10:02.000000702Z",
"birth_time": "1970-01-01T00:10:03.000000703Z",
"size": "506",
"blocks": "507",
"blksize": 508,
"flags": 509,
"gen": 510
}
},
"args": [
"exec_path",
"-l",
"--foo"
],
"envs": [
"ENV_PATH=/path/to/bin:/and/another",
"DEBUG=1"
],
"decision": "DECISION_ALLOW",
"reason": "REASON_BINARY",
"mode": "MODE_LOCKDOWN",
"certificate_info": {
"hash": {
"type": "HASH_ALGO_SHA256",
"hash": "5678_cert_hash"
}
},
"explain": "extra!",
"quarantine_url": "google.com"
}

View File

@@ -0,0 +1,38 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo",
"truncated": false
}
},
"exited": {
"exit_status": 1
}
}

View File

@@ -0,0 +1,68 @@
{
"instigator": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo",
"truncated": false
}
},
"child": {
"id": {
"pid": 12,
"pidversion": 34
},
"parent_id": {
"pid": 56,
"pidversion": 78
},
"original_parent_pid": 56,
"group_id": 111,
"session_id": 222,
"effective_user": {
"uid": -2,
"name": "nobody"
},
"effective_group": {
"gid": -1,
"name": "nogroup"
},
"real_user": {
"uid": -2,
"name": "nobody"
},
"real_group": {
"gid": -1,
"name": "nogroup"
},
"executable": {
"path": "foo_child",
"truncated": false
}
}
}

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