mirror of
https://github.com/google/santa.git
synced 2026-01-19 02:58:27 -05:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80b26955b4 | ||
|
|
6a84023548 | ||
|
|
e70acefb5c | ||
|
|
41c918ee87 | ||
|
|
1adb6d2726 | ||
|
|
8c531a256b | ||
|
|
5829363733 | ||
|
|
379f283c62 | ||
|
|
2082345c02 | ||
|
|
dd8f81a60e | ||
|
|
8ccb0813f1 | ||
|
|
b24e7e42bf | ||
|
|
4821ebebd5 | ||
|
|
efeaa82618 | ||
|
|
3f3de02644 | ||
|
|
f6c9456ea7 | ||
|
|
2aaff051c8 | ||
|
|
2df7e91c87 | ||
|
|
37644acd01 | ||
|
|
899ca89e23 | ||
|
|
e7281f1c55 | ||
|
|
bf0ca24ae7 | ||
|
|
4fe8b7908f | ||
|
|
a8dd332402 | ||
|
|
6631b0a8e3 | ||
|
|
07e09db608 | ||
|
|
d041a48c97 | ||
|
|
1683e09cc8 | ||
|
|
d6c73e0c6c |
3
.bazelrc
3
.bazelrc
@@ -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"
|
||||
|
||||
@@ -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
298
Source/common/PrefixTree.h
Normal 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
|
||||
224
Source/common/PrefixTreeTest.mm
Normal file
224
Source/common/PrefixTreeTest.mm
Normal 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
|
||||
@@ -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];
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
@@ -280,15 +280,12 @@ NSString *SNTMetricMakeStringFromMetricType(SNTMetricType metricType) {
|
||||
if (_fieldNames.count == 0) {
|
||||
metricDict[@"fields"][@""] = @[ [self encodeMetricValueForFieldValues:@[]] ];
|
||||
} else {
|
||||
for (NSString *fieldName in _fieldNames) {
|
||||
NSMutableArray *fieldVals = [[NSMutableArray alloc] init];
|
||||
NSMutableArray *fieldVals = [[NSMutableArray alloc] init];
|
||||
|
||||
for (NSArray<NSString *> *fieldValues in _metricsForFieldValues) {
|
||||
[fieldVals addObject:[self encodeMetricValueForFieldValues:fieldValues]];
|
||||
}
|
||||
|
||||
metricDict[@"fields"][fieldName] = fieldVals;
|
||||
for (NSArray<NSString *> *fieldValues in _metricsForFieldValues) {
|
||||
[fieldVals addObject:[self encodeMetricValueForFieldValues:fieldValues]];
|
||||
}
|
||||
metricDict[@"fields"][[_fieldNames componentsJoinedByString:@","]] = fieldVals;
|
||||
}
|
||||
return metricDict;
|
||||
}
|
||||
|
||||
@@ -672,4 +672,35 @@
|
||||
output);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testEnsureMetricsWithMultipleFieldNamesSerializeOnce {
|
||||
SNTMetricSet *metricSet = [[SNTMetricSet alloc] initWithHostname:@"testHost"
|
||||
username:@"testUser"];
|
||||
|
||||
SNTMetricCounter *c =
|
||||
[metricSet counterWithName:@"/santa/events"
|
||||
fieldNames:@[ @"client", @"event_type" ]
|
||||
helpText:@"Count of events on the host for a given ES client"];
|
||||
[c incrementBy:1 forFieldValues:@[ @"device_manager", @"auth_mount" ]];
|
||||
|
||||
NSDictionary *expected = @{
|
||||
@"/santa/events" : @{
|
||||
@"description" : @"Count of events on the host for a given ES client",
|
||||
@"type" : [NSNumber numberWithInt:(int)SNTMetricTypeCounter],
|
||||
@"fields" : @{
|
||||
@"client,event_type" : @[
|
||||
@{
|
||||
@"value" : @"device_manager,auth_mount",
|
||||
@"created" : [NSDate date],
|
||||
@"last_updated" : [NSDate date],
|
||||
@"data" : [NSNumber numberWithInt:1],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
NSDictionary *got = [metricSet export][@"metrics"];
|
||||
XCTAssertEqualObjects(expected, got, @"metrics do not match expected");
|
||||
}
|
||||
@end
|
||||
|
||||
@@ -1,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_);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
24
Source/common/Unit.h
Normal 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
|
||||
@@ -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 {
|
||||
|
||||
20
Source/common/santa_proto_include_wrapper.h
Normal file
20
Source/common/santa_proto_include_wrapper.h
Normal file
@@ -0,0 +1,20 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__COMMON_SANTA_PROTO_INCLUDE_WRAPPER_H
|
||||
#define SANTA__COMMON_SANTA_PROTO_INCLUDE_WRAPPER_H
|
||||
|
||||
#include "Source/common/santa.pb.h"
|
||||
|
||||
#endif
|
||||
@@ -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;
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
// #import <MOLCertificate/MOLCertificate.h>
|
||||
// #import <MOLCodesignChecker/MOLCodesignChecker.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
|
||||
@@ -7,13 +7,36 @@ package(
|
||||
default_visibility = ["//:santa_package_group"],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "santactl_cmd",
|
||||
srcs = [
|
||||
"SNTCommand.m",
|
||||
"SNTCommandController.m",
|
||||
],
|
||||
hdrs = [
|
||||
"SNTCommand.h",
|
||||
"SNTCommandController.h",
|
||||
],
|
||||
deps = [
|
||||
"//Source/common:SNTXPCControlInterface",
|
||||
"@MOLXPCConnection",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "SNTCommandPrintLog",
|
||||
srcs = ["Commands/SNTCommandPrintLog.mm"],
|
||||
deps = [
|
||||
":santactl_cmd",
|
||||
"//Source/common:SNTLogging",
|
||||
"//Source/common:santa_cc_proto_library_wrapper",
|
||||
"//Source/santad/Logs/EndpointSecurity/Writers/FSSpool:binaryproto_cc_proto_library_wrapper",
|
||||
],
|
||||
)
|
||||
|
||||
objc_library(
|
||||
name = "santactl_lib",
|
||||
srcs = [
|
||||
"SNTCommand.h",
|
||||
"SNTCommand.m",
|
||||
"SNTCommandController.h",
|
||||
"SNTCommandController.m",
|
||||
"main.m",
|
||||
"Commands/SNTCommandFileInfo.m",
|
||||
"Commands/SNTCommandRule.m",
|
||||
@@ -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",
|
||||
|
||||
@@ -68,14 +68,34 @@ REGISTER_COMMAND_NAME(@"metrics")
|
||||
|
||||
for (NSString *fieldName in metric[@"fields"]) {
|
||||
for (NSDictionary *field in metric[@"fields"][fieldName]) {
|
||||
const char *fieldNameStr = [fieldName cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
const char *fieldValueStr = [field[@"value"] cStringUsingEncoding:NSUTF8StringEncoding];
|
||||
const char *createdStr = [field[@"created"] UTF8String];
|
||||
const char *lastUpdatedStr = [field[@"last_updated"] UTF8String];
|
||||
const char *data = [[NSString stringWithFormat:@"%@", field[@"data"]] UTF8String];
|
||||
|
||||
if (strlen(fieldNameStr) > 0) {
|
||||
printf(" %-25s | %s=%s\n", "Field", fieldNameStr, fieldValueStr);
|
||||
NSArray<NSString *> *fields = [fieldName componentsSeparatedByString:@","];
|
||||
NSArray<NSString *> *fieldValues = [field[@"value"] componentsSeparatedByString:@","];
|
||||
|
||||
if (fields.count != fieldValues.count) {
|
||||
fprintf(stderr, "metric %s has a different number of field names and field values",
|
||||
[fieldName UTF8String]);
|
||||
continue;
|
||||
}
|
||||
|
||||
NSString *fieldDisplayString = @"";
|
||||
|
||||
if (fields.count >= 1 && fields[0].length) {
|
||||
for (int i = 0; i < fields.count; i++) {
|
||||
fieldDisplayString = [fieldDisplayString
|
||||
stringByAppendingString:[NSString
|
||||
stringWithFormat:@"%@=%@", fields[i], fieldValues[i]]];
|
||||
if (i < fields.count - 1) {
|
||||
fieldDisplayString = [fieldDisplayString stringByAppendingString:@","];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (![fieldDisplayString isEqualToString:@""]) {
|
||||
printf(" %-25s | %s\n", "Field", [fieldDisplayString UTF8String]);
|
||||
}
|
||||
|
||||
printf(" %-25s | %s\n", "Created", createdStr);
|
||||
|
||||
131
Source/santactl/Commands/SNTCommandPrintLog.mm
Normal file
131
Source/santactl/Commands/SNTCommandPrintLog.mm
Normal file
@@ -0,0 +1,131 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <google/protobuf/util/json_util.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#include "Source/common/santa_proto_include_wrapper.h"
|
||||
#import "Source/santactl/SNTCommand.h"
|
||||
#import "Source/santactl/SNTCommandController.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Writers/FSSpool/binaryproto_proto_include_wrapper.h"
|
||||
#include "google/protobuf/any.pb.h"
|
||||
|
||||
using google::protobuf::util::JsonPrintOptions;
|
||||
using google::protobuf::util::MessageToJsonString;
|
||||
using santa::fsspool::binaryproto::LogBatch;
|
||||
namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
@interface SNTCommandPrintLog : SNTCommand <SNTCommandProtocol>
|
||||
@end
|
||||
|
||||
@implementation SNTCommandPrintLog
|
||||
|
||||
REGISTER_COMMAND_NAME(@"printlog")
|
||||
|
||||
+ (BOOL)requiresRoot {
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)requiresDaemonConn {
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (NSString *)shortHelpText {
|
||||
return @"Prints the contents of Santa protobuf log files as JSON.";
|
||||
}
|
||||
|
||||
+ (NSString *)longHelpText {
|
||||
return @"Prints the contents of serialized Santa protobuf logs as JSON.\n"
|
||||
@"Multiple paths can be provided. The output is a list of all the \n"
|
||||
@"SantaMessage entries per-file. E.g.: \n"
|
||||
@" [\n"
|
||||
@" [\n"
|
||||
@" ... file 1 contents ...\n"
|
||||
@" ],\n"
|
||||
@" [\n"
|
||||
@" ... file N contents ...\n"
|
||||
@" ]\n"
|
||||
@" ]";
|
||||
}
|
||||
|
||||
- (void)runWithArguments:(NSArray *)arguments {
|
||||
JsonPrintOptions options;
|
||||
options.always_print_enums_as_ints = false;
|
||||
options.always_print_primitive_fields = true;
|
||||
options.preserve_proto_field_names = true;
|
||||
options.add_whitespace = true;
|
||||
|
||||
for (int argIdx = 0; argIdx < [arguments count]; argIdx++) {
|
||||
NSString *path = arguments[argIdx];
|
||||
int fd = open([path UTF8String], O_RDONLY);
|
||||
if (fd == -1) {
|
||||
LOGE(@"Failed to open '%@': errno: %d: %s", path, errno, strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
LogBatch logBatch;
|
||||
bool ret = logBatch.ParseFromFileDescriptor(fd);
|
||||
close(fd);
|
||||
|
||||
if (!ret) {
|
||||
LOGE(@"Failed to parse '%@'", path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (argIdx != 0) {
|
||||
std::cout << ",";
|
||||
} else {
|
||||
// Print the opening outer JSON array
|
||||
std::cout << "[";
|
||||
}
|
||||
std::cout << "\n[\n";
|
||||
|
||||
int numRecords = logBatch.records_size();
|
||||
|
||||
for (int i = 0; i < numRecords; i++) {
|
||||
const google::protobuf::Any &any = logBatch.records(i);
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
if (!any.UnpackTo(&santaMsg)) {
|
||||
LOGE(@"Failed to unpack Any proto to SantaMessage in file '%@'", path);
|
||||
break;
|
||||
}
|
||||
|
||||
if (i != 0) {
|
||||
std::cout << ",\n";
|
||||
}
|
||||
|
||||
std::string json;
|
||||
if (!MessageToJsonString(santaMsg, &json, options).ok()) {
|
||||
LOGE(@"Unable to convert message to JSON in file: '%@'", path);
|
||||
}
|
||||
std::cout << json;
|
||||
}
|
||||
|
||||
std::cout << "]" << std::flush;
|
||||
|
||||
if (argIdx == ([arguments count] - 1)) {
|
||||
// Print the closing outer JSON array
|
||||
std::cout << "]\n";
|
||||
}
|
||||
}
|
||||
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -38,18 +38,18 @@
|
||||
"type" : 9,
|
||||
"description" : "Count of process exec events on the host",
|
||||
"fields" : {
|
||||
"rule_type" : [
|
||||
"rule_type,client" : [
|
||||
{
|
||||
"created" : "2021-09-16T21:07:34.826Z",
|
||||
"last_updated" : "2021-09-16T21:07:34.826Z",
|
||||
"value" : "binary",
|
||||
"data" : 1
|
||||
"value" : "certificate,authorizer",
|
||||
"data" : 2
|
||||
},
|
||||
{
|
||||
"created" : "2021-09-16T21:07:34.826Z",
|
||||
"last_updated" : "2021-09-16T21:07:34.826Z",
|
||||
"value" : "certificate",
|
||||
"data" : 2
|
||||
"value" : "binary,authorizer",
|
||||
"data" : 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -30,14 +30,14 @@
|
||||
Metric Name | /santa/events
|
||||
Description | Count of process exec events on the host
|
||||
Type | SNTMetricTypeCounter
|
||||
Field | rule_type=binary
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 1
|
||||
Field | rule_type=certificate
|
||||
Field | rule_type=certificate,client=authorizer
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 2
|
||||
Field | rule_type=binary,client=authorizer
|
||||
Created | 2021-09-16T21:07:34.826Z
|
||||
Last Updated | 2021-09-16T21:07:34.826Z
|
||||
Data | 1
|
||||
|
||||
Metric Name | /santa/using_endpoint_security_framework
|
||||
Description | Is santad using the endpoint security framework
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
80
Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h
Normal file
80
Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h
Normal file
@@ -0,0 +1,80 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#ifndef SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_PROTOBUF_H
|
||||
#define SANTA__SANTAD__LOGS_ENDPOINTSECURITY_SERIALIZERS_PROTOBUF_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <google/protobuf/arena.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "Source/common/santa_proto_include_wrapper.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
class Protobuf : public Serializer {
|
||||
public:
|
||||
static std::shared_ptr<Protobuf> Create(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi);
|
||||
|
||||
Protobuf(
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi);
|
||||
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedClose &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExchange &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExec &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedExit &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedFork &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedLink &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
|
||||
std::vector<uint8_t> SerializeMessage(
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
|
||||
|
||||
std::vector<uint8_t> SerializeAllowlist(
|
||||
const santa::santad::event_providers::endpoint_security::Message &,
|
||||
const std::string_view) override;
|
||||
|
||||
std::vector<uint8_t> SerializeBundleHashingEvent(SNTStoredEvent *) override;
|
||||
|
||||
std::vector<uint8_t> SerializeDiskAppeared(NSDictionary *) override;
|
||||
std::vector<uint8_t> SerializeDiskDisappeared(NSDictionary *) override;
|
||||
|
||||
private:
|
||||
::santa::pb::v1::SantaMessage *CreateDefaultProto(google::protobuf::Arena *arena);
|
||||
::santa::pb::v1::SantaMessage *CreateDefaultProto(
|
||||
google::protobuf::Arena *arena,
|
||||
const santa::santad::event_providers::endpoint_security::EnrichedEventType &msg);
|
||||
::santa::pb::v1::SantaMessage *CreateDefaultProto(google::protobuf::Arena *arena,
|
||||
struct timespec event_time,
|
||||
struct timespec processed_time);
|
||||
|
||||
std::vector<uint8_t> FinalizeProto(::santa::pb::v1::SantaMessage *santa_msg);
|
||||
|
||||
std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI> esapi_;
|
||||
};
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
#endif
|
||||
630
Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm
Normal file
630
Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.mm
Normal file
@@ -0,0 +1,630 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#include <bsm/libbsm.h>
|
||||
#include <mach/message.h>
|
||||
#include <math.h>
|
||||
#include <sys/proc_info.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#include "Source/common/SNTLogging.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
#include "google/protobuf/timestamp.pb.h"
|
||||
|
||||
using google::protobuf::Arena;
|
||||
using google::protobuf::Timestamp;
|
||||
|
||||
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedClose;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExec;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedExit;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFile;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedFork;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedLink;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedProcess;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedRename;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedUnlink;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::EffectiveGroup;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::EffectiveUser;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::NonNull;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pid;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::Pidversion;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::RealGroup;
|
||||
using santa::santad::logs::endpoint_security::serializers::Utilities::RealUser;
|
||||
|
||||
namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
|
||||
std::shared_ptr<Protobuf> Protobuf::Create(std::shared_ptr<EndpointSecurityAPI> esapi) {
|
||||
return std::make_shared<Protobuf>(esapi);
|
||||
}
|
||||
|
||||
Protobuf::Protobuf(std::shared_ptr<EndpointSecurityAPI> esapi) : esapi_(esapi) {}
|
||||
|
||||
static inline void EncodeTimestamp(Timestamp *timestamp, struct timespec ts) {
|
||||
timestamp->set_seconds(ts.tv_sec);
|
||||
timestamp->set_nanos((int32_t)ts.tv_nsec);
|
||||
}
|
||||
|
||||
static inline void EncodeTimestamp(Timestamp *timestamp, struct timeval tv) {
|
||||
EncodeTimestamp(timestamp, (struct timespec){tv.tv_sec, tv.tv_usec * 1000});
|
||||
}
|
||||
|
||||
static inline void EncodeProcessID(pbv1::ProcessID *proc_id, const audit_token_t &tok) {
|
||||
proc_id->set_pid(Pid(tok));
|
||||
proc_id->set_pidversion(Pidversion(tok));
|
||||
}
|
||||
|
||||
static inline void EncodePath(std::string *buf, const es_file_t *dir,
|
||||
const es_string_token_t file) {
|
||||
buf->append(std::string_view(dir->path.data, dir->path.length));
|
||||
buf->append("/");
|
||||
buf->append(std::string_view(file.data, file.length));
|
||||
}
|
||||
|
||||
static inline void EncodePath(std::string *buf, const es_file_t *es_file) {
|
||||
buf->append(std::string_view(es_file->path.data, es_file->path.length));
|
||||
}
|
||||
|
||||
static inline void EncodeString(std::string *buf, NSString *value) {
|
||||
if (value) {
|
||||
buf->append(std::string_view([value UTF8String], [value length]));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeString(std::string *buf, std::string_view value) {
|
||||
if (value.length() > 0) {
|
||||
buf->append(std::string_view(value.data(), value.length()));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeUserInfo(::pbv1::UserInfo *pb_user_info, uid_t uid,
|
||||
const std::optional<std::shared_ptr<std::string>> &name) {
|
||||
pb_user_info->set_uid(uid);
|
||||
if (name.has_value()) {
|
||||
pb_user_info->set_name(*name->get());
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeGroupInfo(::pbv1::GroupInfo *pb_group_info, gid_t gid,
|
||||
const std::optional<std::shared_ptr<std::string>> &name) {
|
||||
pb_group_info->set_gid(gid);
|
||||
if (name.has_value()) {
|
||||
pb_group_info->set_name(*name->get());
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeHash(::pbv1::Hash *pb_hash, NSString *sha256) {
|
||||
if (sha256) {
|
||||
pb_hash->set_type(::pbv1::Hash::HASH_ALGO_SHA256);
|
||||
pb_hash->set_hash([sha256 UTF8String], [sha256 length]);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeStat(::pbv1::Stat *pb_stat, const struct stat &sb,
|
||||
const std::optional<std::shared_ptr<std::string>> &username,
|
||||
const std::optional<std::shared_ptr<std::string>> &groupname) {
|
||||
pb_stat->set_dev(sb.st_dev);
|
||||
pb_stat->set_mode(sb.st_mode);
|
||||
pb_stat->set_nlink(sb.st_nlink);
|
||||
pb_stat->set_ino(sb.st_ino);
|
||||
EncodeUserInfo(pb_stat->mutable_user(), sb.st_uid, username);
|
||||
EncodeGroupInfo(pb_stat->mutable_group(), sb.st_gid, groupname);
|
||||
pb_stat->set_rdev(sb.st_rdev);
|
||||
EncodeTimestamp(pb_stat->mutable_access_time(), sb.st_atimespec);
|
||||
EncodeTimestamp(pb_stat->mutable_modification_time(), sb.st_mtimespec);
|
||||
EncodeTimestamp(pb_stat->mutable_change_time(), sb.st_ctimespec);
|
||||
EncodeTimestamp(pb_stat->mutable_birth_time(), sb.st_birthtimespec);
|
||||
pb_stat->set_size(sb.st_size);
|
||||
pb_stat->set_blocks(sb.st_blocks);
|
||||
pb_stat->set_blksize(sb.st_blksize);
|
||||
pb_stat->set_flags(sb.st_flags);
|
||||
pb_stat->set_gen(sb.st_gen);
|
||||
}
|
||||
|
||||
static inline void EncodeFileInfo(::pbv1::FileInfo *pb_file, const es_file_t *es_file,
|
||||
const EnrichedFile &enriched_file, NSString *sha256 = nil) {
|
||||
EncodePath(pb_file->mutable_path(), es_file);
|
||||
pb_file->set_truncated(es_file->path_truncated);
|
||||
EncodeStat(pb_file->mutable_stat(), es_file->stat, enriched_file.user(), enriched_file.group());
|
||||
if (sha256) {
|
||||
EncodeHash(pb_file->mutable_hash(), sha256);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeFileInfoLight(::pbv1::FileInfoLight *pb_file, const es_file_t *es_file) {
|
||||
EncodePath(pb_file->mutable_path(), es_file);
|
||||
pb_file->set_truncated(es_file->path_truncated);
|
||||
}
|
||||
|
||||
static inline void EncodeProcessInfoLight(::pbv1::ProcessInfoLight *pb_proc_info,
|
||||
uint32_t message_version, const es_process_t *es_proc,
|
||||
const EnrichedProcess &enriched_proc) {
|
||||
EncodeProcessID(pb_proc_info->mutable_id(), es_proc->audit_token);
|
||||
EncodeProcessID(pb_proc_info->mutable_parent_id(), es_proc->parent_audit_token);
|
||||
|
||||
pb_proc_info->set_original_parent_pid(es_proc->original_ppid);
|
||||
pb_proc_info->set_group_id(es_proc->group_id);
|
||||
pb_proc_info->set_session_id(es_proc->session_id);
|
||||
|
||||
EncodeUserInfo(pb_proc_info->mutable_effective_user(), EffectiveUser(es_proc->audit_token),
|
||||
enriched_proc.effective_user());
|
||||
EncodeUserInfo(pb_proc_info->mutable_real_user(), RealUser(es_proc->audit_token),
|
||||
enriched_proc.real_user());
|
||||
EncodeGroupInfo(pb_proc_info->mutable_effective_group(), EffectiveGroup(es_proc->audit_token),
|
||||
enriched_proc.effective_group());
|
||||
EncodeGroupInfo(pb_proc_info->mutable_real_group(), RealGroup(es_proc->audit_token),
|
||||
enriched_proc.real_group());
|
||||
|
||||
EncodeFileInfoLight(pb_proc_info->mutable_executable(), es_proc->executable);
|
||||
}
|
||||
|
||||
static inline void EncodeProcessInfo(::pbv1::ProcessInfo *pb_proc_info, uint32_t message_version,
|
||||
const es_process_t *es_proc,
|
||||
const EnrichedProcess &enriched_proc,
|
||||
SNTCachedDecision *cd = nil) {
|
||||
EncodeProcessID(pb_proc_info->mutable_id(), es_proc->audit_token);
|
||||
EncodeProcessID(pb_proc_info->mutable_parent_id(), es_proc->parent_audit_token);
|
||||
if (message_version >= 4) {
|
||||
EncodeProcessID(pb_proc_info->mutable_responsible_id(), es_proc->responsible_audit_token);
|
||||
}
|
||||
|
||||
pb_proc_info->set_original_parent_pid(es_proc->original_ppid);
|
||||
pb_proc_info->set_group_id(es_proc->group_id);
|
||||
pb_proc_info->set_session_id(es_proc->session_id);
|
||||
|
||||
EncodeUserInfo(pb_proc_info->mutable_effective_user(), EffectiveUser(es_proc->audit_token),
|
||||
enriched_proc.effective_user());
|
||||
EncodeUserInfo(pb_proc_info->mutable_real_user(), RealUser(es_proc->audit_token),
|
||||
enriched_proc.real_user());
|
||||
EncodeGroupInfo(pb_proc_info->mutable_effective_group(), EffectiveGroup(es_proc->audit_token),
|
||||
enriched_proc.effective_group());
|
||||
EncodeGroupInfo(pb_proc_info->mutable_real_group(), RealGroup(es_proc->audit_token),
|
||||
enriched_proc.real_group());
|
||||
|
||||
pb_proc_info->set_is_platform_binary(es_proc->is_platform_binary);
|
||||
pb_proc_info->set_is_es_client(es_proc->is_es_client);
|
||||
|
||||
if (es_proc->codesigning_flags & CS_SIGNED) {
|
||||
::pbv1::CodeSignature *pb_code_sig = pb_proc_info->mutable_code_signature();
|
||||
pb_code_sig->set_cdhash(es_proc->cdhash, sizeof(es_proc->cdhash));
|
||||
if (es_proc->signing_id.length > 0) {
|
||||
pb_code_sig->set_signing_id(es_proc->signing_id.data, es_proc->signing_id.length);
|
||||
}
|
||||
|
||||
if (es_proc->team_id.length > 0) {
|
||||
pb_code_sig->set_team_id(es_proc->team_id.data, es_proc->team_id.length);
|
||||
}
|
||||
}
|
||||
|
||||
pb_proc_info->set_cs_flags(es_proc->codesigning_flags);
|
||||
|
||||
EncodeFileInfo(pb_proc_info->mutable_executable(), es_proc->executable,
|
||||
enriched_proc.executable(), cd.sha256);
|
||||
if (message_version >= 2 && es_proc->tty) {
|
||||
EncodeFileInfoLight(pb_proc_info->mutable_tty(), es_proc->tty);
|
||||
}
|
||||
|
||||
if (message_version >= 3) {
|
||||
EncodeTimestamp(pb_proc_info->mutable_start_time(), es_proc->start_time);
|
||||
}
|
||||
}
|
||||
|
||||
void EncodeExitStatus(::pbv1::Exit *pb_exit, int exitStatus) {
|
||||
if (WIFEXITED(exitStatus)) {
|
||||
pb_exit->mutable_exited()->set_exit_status(WEXITSTATUS(exitStatus));
|
||||
} else if (WIFSIGNALED(exitStatus)) {
|
||||
pb_exit->mutable_signaled()->set_signal(WTERMSIG(exitStatus));
|
||||
} else if (WIFSTOPPED(exitStatus)) {
|
||||
pb_exit->mutable_stopped()->set_signal(WSTOPSIG(exitStatus));
|
||||
} else {
|
||||
LOGE(@"Unknown exit status encountered: %d", exitStatus);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void EncodeCertificateInfo(::pbv1::CertificateInfo *pb_cert_info, NSString *cert_hash,
|
||||
NSString *common_name) {
|
||||
if (cert_hash) {
|
||||
EncodeHash(pb_cert_info->mutable_hash(), cert_hash);
|
||||
}
|
||||
|
||||
if (common_name) {
|
||||
pb_cert_info->set_common_name([common_name UTF8String], [common_name length]);
|
||||
}
|
||||
}
|
||||
|
||||
::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state) {
|
||||
if (event_state & SNTEventStateAllow) {
|
||||
return ::pbv1::Execution::DECISION_ALLOW;
|
||||
} else if (event_state & SNTEventStateBlock) {
|
||||
return ::pbv1::Execution::DECISION_DENY;
|
||||
} else {
|
||||
return ::pbv1::Execution::DECISION_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
::pbv1::Execution::Reason GetReasonEnum(SNTEventState event_state) {
|
||||
switch (event_state) {
|
||||
case SNTEventStateAllowBinary: return ::pbv1::Execution::REASON_BINARY;
|
||||
case SNTEventStateAllowCompiler: return ::pbv1::Execution::REASON_COMPILER;
|
||||
case SNTEventStateAllowTransitive: return ::pbv1::Execution::REASON_TRANSITIVE;
|
||||
case SNTEventStateAllowPendingTransitive: return ::pbv1::Execution::REASON_PENDING_TRANSITIVE;
|
||||
case SNTEventStateAllowCertificate: return ::pbv1::Execution::REASON_CERT;
|
||||
case SNTEventStateAllowScope: return ::pbv1::Execution::REASON_SCOPE;
|
||||
case SNTEventStateAllowTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
|
||||
case SNTEventStateAllowUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
|
||||
case SNTEventStateBlockBinary: return ::pbv1::Execution::REASON_BINARY;
|
||||
case SNTEventStateBlockCertificate: return ::pbv1::Execution::REASON_CERT;
|
||||
case SNTEventStateBlockScope: return ::pbv1::Execution::REASON_SCOPE;
|
||||
case SNTEventStateBlockTeamID: return ::pbv1::Execution::REASON_TEAM_ID;
|
||||
case SNTEventStateBlockLongPath: return ::pbv1::Execution::REASON_LONG_PATH;
|
||||
case SNTEventStateBlockUnknown: return ::pbv1::Execution::REASON_UNKNOWN;
|
||||
default: return ::pbv1::Execution::REASON_NOT_RUNNING;
|
||||
}
|
||||
}
|
||||
|
||||
::pbv1::Execution::Mode GetModeEnum(SNTClientMode mode) {
|
||||
switch (mode) {
|
||||
case SNTClientModeMonitor: return ::pbv1::Execution::MODE_MONITOR;
|
||||
case SNTClientModeLockdown: return ::pbv1::Execution::MODE_LOCKDOWN;
|
||||
case SNTClientModeUnknown: return ::pbv1::Execution::MODE_UNKNOWN;
|
||||
default: return ::pbv1::Execution::MODE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
::pbv1::FileDescriptor::FDType GetFileDescriptorType(uint32_t fdtype) {
|
||||
switch (fdtype) {
|
||||
case PROX_FDTYPE_ATALK: return ::pbv1::FileDescriptor::FD_TYPE_ATALK;
|
||||
case PROX_FDTYPE_VNODE: return ::pbv1::FileDescriptor::FD_TYPE_VNODE;
|
||||
case PROX_FDTYPE_SOCKET: return ::pbv1::FileDescriptor::FD_TYPE_SOCKET;
|
||||
case PROX_FDTYPE_PSHM: return ::pbv1::FileDescriptor::FD_TYPE_PSHM;
|
||||
case PROX_FDTYPE_PSEM: return ::pbv1::FileDescriptor::FD_TYPE_PSEM;
|
||||
case PROX_FDTYPE_KQUEUE: return ::pbv1::FileDescriptor::FD_TYPE_KQUEUE;
|
||||
case PROX_FDTYPE_PIPE: return ::pbv1::FileDescriptor::FD_TYPE_PIPE;
|
||||
case PROX_FDTYPE_FSEVENTS: return ::pbv1::FileDescriptor::FD_TYPE_FSEVENTS;
|
||||
case PROX_FDTYPE_NETPOLICY: return ::pbv1::FileDescriptor::FD_TYPE_NETPOLICY;
|
||||
// Note: CHANNEL and NEXUS types weren't exposed until Xcode v13 SDK.
|
||||
// Not using the macros to be able to build on older SDK versions.
|
||||
case 10 /* PROX_FDTYPE_CHANNEL */: return ::pbv1::FileDescriptor::FD_TYPE_CHANNEL;
|
||||
case 11 /* PROX_FDTYPE_NEXUS */: return ::pbv1::FileDescriptor::FD_TYPE_NEXUS;
|
||||
default: return ::pbv1::FileDescriptor::FD_TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena, struct timespec event_time,
|
||||
struct timespec processed_time) {
|
||||
::pbv1::SantaMessage *santa_msg = Arena::CreateMessage<::pbv1::SantaMessage>(arena);
|
||||
|
||||
if (EnabledMachineID()) {
|
||||
EncodeString(santa_msg->mutable_machine_id(), MachineID());
|
||||
}
|
||||
EncodeTimestamp(santa_msg->mutable_event_time(), event_time);
|
||||
EncodeTimestamp(santa_msg->mutable_processed_time(), processed_time);
|
||||
|
||||
return santa_msg;
|
||||
}
|
||||
|
||||
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena, const EnrichedEventType &msg) {
|
||||
return CreateDefaultProto(arena, msg.es_msg().time, msg.enrichment_time());
|
||||
}
|
||||
|
||||
::pbv1::SantaMessage *Protobuf::CreateDefaultProto(Arena *arena) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
|
||||
return CreateDefaultProto(arena, ts, ts);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::FinalizeProto(::pbv1::SantaMessage *santa_msg) {
|
||||
std::vector<uint8_t> vec(santa_msg->ByteSizeLong());
|
||||
santa_msg->SerializeToArray(vec.data(), (int)vec.capacity());
|
||||
return vec;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedClose &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Close *pb_close = santa_msg->mutable_close();
|
||||
|
||||
EncodeProcessInfoLight(pb_close->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
|
||||
msg.instigator());
|
||||
EncodeFileInfo(pb_close->mutable_target(), msg.es_msg().event.close.target, msg.target());
|
||||
pb_close->set_modified(msg.es_msg().event.close.modified);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExchange &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Exchangedata *pb_exchangedata = santa_msg->mutable_exchangedata();
|
||||
|
||||
EncodeProcessInfoLight(pb_exchangedata->mutable_instigator(), msg.es_msg().version,
|
||||
msg.es_msg().process, msg.instigator());
|
||||
EncodeFileInfo(pb_exchangedata->mutable_file1(), msg.es_msg().event.exchangedata.file1,
|
||||
msg.file1());
|
||||
EncodeFileInfo(pb_exchangedata->mutable_file2(), msg.es_msg().event.exchangedata.file2,
|
||||
msg.file2());
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExec &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
SNTCachedDecision *cd = [[SNTDecisionCache sharedCache]
|
||||
cachedDecisionForFile:msg.es_msg().event.exec.target->executable->stat];
|
||||
|
||||
GetDecisionEnum(cd.decision);
|
||||
|
||||
::pbv1::Execution *pb_exec = santa_msg->mutable_execution();
|
||||
|
||||
EncodeProcessInfoLight(pb_exec->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
|
||||
msg.instigator());
|
||||
EncodeProcessInfo(pb_exec->mutable_target(), msg.es_msg().version, msg.es_msg().event.exec.target,
|
||||
msg.target(), cd);
|
||||
|
||||
if (msg.es_msg().version >= 2 && msg.script().has_value()) {
|
||||
EncodeFileInfo(pb_exec->mutable_script(), msg.es_msg().event.exec.script, msg.script().value());
|
||||
}
|
||||
|
||||
if (msg.es_msg().version >= 3 && msg.working_dir().has_value()) {
|
||||
EncodeFileInfo(pb_exec->mutable_working_directory(), msg.es_msg().event.exec.cwd,
|
||||
msg.working_dir().value());
|
||||
}
|
||||
|
||||
uint32_t arg_count = esapi_->ExecArgCount(&msg.es_msg().event.exec);
|
||||
for (uint32_t i = 0; i < arg_count; i++) {
|
||||
es_string_token_t tok = esapi_->ExecArg(&msg.es_msg().event.exec, i);
|
||||
pb_exec->add_args(tok.data, tok.length);
|
||||
}
|
||||
|
||||
uint32_t env_count = esapi_->ExecEnvCount(&msg.es_msg().event.exec);
|
||||
for (uint32_t i = 0; i < env_count; i++) {
|
||||
es_string_token_t tok = esapi_->ExecEnv(&msg.es_msg().event.exec, i);
|
||||
pb_exec->add_envs(tok.data, tok.length);
|
||||
}
|
||||
|
||||
if (msg.es_msg().version >= 4) {
|
||||
int32_t max_fd = -1;
|
||||
uint32_t fd_count = esapi_->ExecFDCount(&msg.es_msg().event.exec);
|
||||
for (uint32_t i = 0; i < fd_count; i++) {
|
||||
const es_fd_t *fd = esapi_->ExecFD(&msg.es_msg().event.exec, i);
|
||||
max_fd = std::max(max_fd, fd->fd);
|
||||
::pbv1::FileDescriptor *pb_fd = pb_exec->add_fds();
|
||||
pb_fd->set_fd(fd->fd);
|
||||
pb_fd->set_fd_type(GetFileDescriptorType(fd->fdtype));
|
||||
if (fd->fdtype == PROX_FDTYPE_PIPE) {
|
||||
pb_fd->set_pipe_id(fd->pipe.pipe_id);
|
||||
}
|
||||
}
|
||||
|
||||
// If the `max_fd` seen is less than `last_fd`, we know that ES truncated
|
||||
// the set of returned file descriptors
|
||||
pb_exec->set_fd_list_truncated(max_fd < msg.es_msg().event.exec.last_fd);
|
||||
}
|
||||
|
||||
pb_exec->set_decision(GetDecisionEnum(cd.decision));
|
||||
pb_exec->set_reason(GetReasonEnum(cd.decision));
|
||||
pb_exec->set_mode(GetModeEnum([[SNTConfigurator configurator] clientMode]));
|
||||
|
||||
if (cd.certSHA256 || cd.certCommonName) {
|
||||
EncodeCertificateInfo(pb_exec->mutable_certificate_info(), cd.certSHA256, cd.certCommonName);
|
||||
}
|
||||
|
||||
if (cd.decisionExtra) {
|
||||
pb_exec->set_explain([cd.decisionExtra UTF8String], [cd.decisionExtra length]);
|
||||
}
|
||||
|
||||
if (cd.quarantineURL) {
|
||||
pb_exec->set_quarantine_url([cd.quarantineURL UTF8String], [cd.quarantineURL length]);
|
||||
}
|
||||
|
||||
NSString *orig_path = Utilities::OriginalPathForTranslocation(msg.es_msg().event.exec.target);
|
||||
if (orig_path) {
|
||||
pb_exec->set_original_path([orig_path UTF8String], [orig_path length]);
|
||||
}
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedExit &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Exit *pb_exit = santa_msg->mutable_exit();
|
||||
|
||||
EncodeProcessInfoLight(pb_exit->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
|
||||
msg.instigator());
|
||||
EncodeExitStatus(pb_exit, msg.es_msg().event.exit.stat);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedFork &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Fork *pb_fork = santa_msg->mutable_fork();
|
||||
|
||||
EncodeProcessInfoLight(pb_fork->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
|
||||
msg.instigator());
|
||||
EncodeProcessInfoLight(pb_fork->mutable_child(), msg.es_msg().version,
|
||||
msg.es_msg().event.fork.child, msg.child());
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedLink &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Link *pb_link = santa_msg->mutable_link();
|
||||
EncodeProcessInfoLight(pb_link->mutable_instigator(), msg.es_msg().version, msg.es_msg().process,
|
||||
msg.instigator());
|
||||
EncodeFileInfo(pb_link->mutable_source(), msg.es_msg().event.link.source, msg.source());
|
||||
EncodePath(pb_link->mutable_target(), msg.es_msg().event.link.target_dir,
|
||||
msg.es_msg().event.link.target_filename);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedRename &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Rename *pb_rename = santa_msg->mutable_rename();
|
||||
EncodeProcessInfoLight(pb_rename->mutable_instigator(), msg.es_msg().version,
|
||||
msg.es_msg().process, msg.instigator());
|
||||
EncodeFileInfo(pb_rename->mutable_source(), msg.es_msg().event.rename.source, msg.source());
|
||||
if (msg.es_msg().event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
|
||||
EncodePath(pb_rename->mutable_target(), msg.es_msg().event.rename.destination.existing_file);
|
||||
pb_rename->set_target_existed(true);
|
||||
} else {
|
||||
EncodePath(pb_rename->mutable_target(), msg.es_msg().event.rename.destination.new_path.dir,
|
||||
msg.es_msg().event.rename.destination.new_path.filename);
|
||||
pb_rename->set_target_existed(false);
|
||||
}
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedUnlink &msg) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
|
||||
|
||||
::pbv1::Unlink *pb_unlink = santa_msg->mutable_unlink();
|
||||
EncodeProcessInfoLight(pb_unlink->mutable_instigator(), msg.es_msg().version,
|
||||
msg.es_msg().process, msg.instigator());
|
||||
EncodeFileInfo(pb_unlink->mutable_target(), msg.es_msg().event.unlink.target, msg.target());
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeAllowlist(const Message &msg, const std::string_view hash) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena);
|
||||
|
||||
const es_file_t *es_file = Utilities::GetAllowListTargetFile(msg);
|
||||
|
||||
EnrichedFile enriched_file(std::nullopt, std::nullopt, std::nullopt);
|
||||
EnrichedProcess enriched_process;
|
||||
|
||||
::pbv1::Allowlist *pb_allowlist = santa_msg->mutable_allowlist();
|
||||
EncodeProcessInfoLight(pb_allowlist->mutable_instigator(), msg->version, msg->process,
|
||||
enriched_process);
|
||||
|
||||
EncodeFileInfo(pb_allowlist->mutable_target(), es_file, enriched_file,
|
||||
[NSString stringWithFormat:@"%s", hash.data()]);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeBundleHashingEvent(SNTStoredEvent *event) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena);
|
||||
|
||||
::pbv1::Bundle *pb_bundle = santa_msg->mutable_bundle();
|
||||
|
||||
EncodeHash(pb_bundle->mutable_file_hash(), event.fileSHA256);
|
||||
EncodeHash(pb_bundle->mutable_bundle_hash(), event.fileBundleHash);
|
||||
pb_bundle->set_bundle_name([NonNull(event.fileBundleName) UTF8String]);
|
||||
pb_bundle->set_bundle_id([NonNull(event.fileBundleID) UTF8String]);
|
||||
pb_bundle->set_bundle_path([NonNull(event.fileBundlePath) UTF8String]);
|
||||
pb_bundle->set_path([NonNull(event.filePath) UTF8String]);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
static void EncodeDisk(::pbv1::Disk *pb_disk, ::pbv1::Disk_Action action, NSDictionary *props) {
|
||||
pb_disk->set_action(action);
|
||||
|
||||
NSString *dmg_path = nil;
|
||||
NSString *serial = nil;
|
||||
if ([props[@"DADeviceModel"] isEqual:@"Disk Image"]) {
|
||||
dmg_path = Utilities::DiskImageForDevice(props[@"DADevicePath"]);
|
||||
} else {
|
||||
serial = Utilities::SerialForDevice(props[@"DADevicePath"]);
|
||||
}
|
||||
|
||||
NSString *model = [NSString
|
||||
stringWithFormat:@"%@ %@", NonNull(props[@"DADeviceVendor"]), NonNull(props[@"DADeviceModel"])];
|
||||
model = [model stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
||||
|
||||
EncodeString(pb_disk->mutable_mount(), [props[@"DAVolumePath"] path]);
|
||||
EncodeString(pb_disk->mutable_volume(), props[@"DAVolumeName"]);
|
||||
EncodeString(pb_disk->mutable_bsd_name(), props[@"DAMediaBSDName"]);
|
||||
EncodeString(pb_disk->mutable_fs(), props[@"DAVolumeKind"]);
|
||||
EncodeString(pb_disk->mutable_model(), model);
|
||||
EncodeString(pb_disk->mutable_serial(), serial);
|
||||
EncodeString(pb_disk->mutable_bus(), props[@"DADeviceProtocol"]);
|
||||
EncodeString(pb_disk->mutable_dmg_path(), dmg_path);
|
||||
|
||||
if (props[@"DAAppearanceTime"]) {
|
||||
// Note: `DAAppearanceTime` is set via `CFAbsoluteTimeGetCurrent`, which uses the defined
|
||||
// reference date of `Jan 1 2001 00:00:00 GMT` (not the typical `00:00:00 UTC on 1 January
|
||||
// 1970`).
|
||||
NSDate *appearance =
|
||||
[NSDate dateWithTimeIntervalSinceReferenceDate:[props[@"DAAppearanceTime"] doubleValue]];
|
||||
NSTimeInterval interval = [appearance timeIntervalSince1970];
|
||||
double seconds;
|
||||
double fractional = modf(interval, &seconds);
|
||||
struct timespec ts = {
|
||||
.tv_sec = (long)seconds,
|
||||
.tv_nsec = (long)(fractional * NSEC_PER_SEC),
|
||||
};
|
||||
EncodeTimestamp(pb_disk->mutable_appearance(), ts);
|
||||
Timestamp timestamp = pb_disk->appearance();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeDiskAppeared(NSDictionary *props) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena);
|
||||
|
||||
EncodeDisk(santa_msg->mutable_disk(), ::pbv1::Disk::ACTION_APPEARED, props);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Protobuf::SerializeDiskDisappeared(NSDictionary *props) {
|
||||
Arena arena;
|
||||
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena);
|
||||
|
||||
EncodeDisk(santa_msg->mutable_disk(), ::pbv1::Disk::ACTION_DISAPPEARED, props);
|
||||
|
||||
return FinalizeProto(santa_msg);
|
||||
}
|
||||
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
638
Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm
Normal file
638
Source/santad/Logs/EndpointSecurity/Serializers/ProtobufTest.mm
Normal file
@@ -0,0 +1,638 @@
|
||||
/// Copyright 2022 Google LLC
|
||||
///
|
||||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
/// you may not use this file except in compliance with the License.
|
||||
/// You may obtain a copy of the License at
|
||||
///
|
||||
/// https://www.apache.org/licenses/LICENSE-2.0
|
||||
///
|
||||
/// Unless required by applicable law or agreed to in writing, software
|
||||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
/// See the License for the specific language governing permissions and
|
||||
/// limitations under the License.
|
||||
|
||||
#include <EndpointSecurity/EndpointSecurity.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <Kernel/kern/cs_blobs.h>
|
||||
#import <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/proc_info.h>
|
||||
#include <sys/signal.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <uuid/uuid.h>
|
||||
#include <cstring>
|
||||
|
||||
#include <google/protobuf/util/json_util.h>
|
||||
|
||||
#import "Source/common/SNTCachedDecision.h"
|
||||
#include "Source/common/SNTCommonEnums.h"
|
||||
#import "Source/common/SNTConfigurator.h"
|
||||
#import "Source/common/SNTStoredEvent.h"
|
||||
#include "Source/common/TestUtils.h"
|
||||
#include "Source/common/santa_proto_include_wrapper.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/EnrichedTypes.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Enricher.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
|
||||
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Protobuf.h"
|
||||
#include "Source/santad/Logs/EndpointSecurity/Serializers/Serializer.h"
|
||||
#import "Source/santad/SNTDecisionCache.h"
|
||||
#include "google/protobuf/any.pb.h"
|
||||
#include "google/protobuf/timestamp.pb.h"
|
||||
|
||||
using google::protobuf::Timestamp;
|
||||
using google::protobuf::util::JsonPrintOptions;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
|
||||
using santa::santad::event_providers::endpoint_security::EnrichedMessage;
|
||||
using santa::santad::event_providers::endpoint_security::Enricher;
|
||||
using santa::santad::event_providers::endpoint_security::Message;
|
||||
using santa::santad::logs::endpoint_security::serializers::Protobuf;
|
||||
using santa::santad::logs::endpoint_security::serializers::Serializer;
|
||||
|
||||
namespace pbv1 = ::santa::pb::v1;
|
||||
|
||||
namespace santa::santad::logs::endpoint_security::serializers {
|
||||
extern void EncodeExitStatus(::pbv1::Exit *pbExit, int exitStatus);
|
||||
extern ::pbv1::Execution::Decision GetDecisionEnum(SNTEventState event_state);
|
||||
extern ::pbv1::Execution::Reason GetReasonEnum(SNTEventState event_state);
|
||||
extern ::pbv1::Execution::Mode GetModeEnum(SNTClientMode mode);
|
||||
extern ::pbv1::FileDescriptor::FDType GetFileDescriptorType(uint32_t fdtype);
|
||||
} // namespace santa::santad::logs::endpoint_security::serializers
|
||||
|
||||
using santa::santad::logs::endpoint_security::serializers::EncodeExitStatus;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetDecisionEnum;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetFileDescriptorType;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetModeEnum;
|
||||
using santa::santad::logs::endpoint_security::serializers::GetReasonEnum;
|
||||
|
||||
JsonPrintOptions DefaultJsonPrintOptions() {
|
||||
JsonPrintOptions options;
|
||||
options.always_print_enums_as_ints = false;
|
||||
options.always_print_primitive_fields = false;
|
||||
options.preserve_proto_field_names = true;
|
||||
options.add_whitespace = true;
|
||||
return options;
|
||||
}
|
||||
|
||||
NSString *TestJsonPath(NSString *jsonFileName, uint32_t version) {
|
||||
static dispatch_once_t onceToken;
|
||||
static NSString *testPath;
|
||||
static NSString *testDataRepoPath = @"santa/Source/santad/testdata/protobuf";
|
||||
NSString *testDataRepoVersionPath = [NSString stringWithFormat:@"v%u", version];
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
testPath = [NSString pathWithComponents:@[
|
||||
[[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_SRCDIR"], testDataRepoPath
|
||||
]];
|
||||
});
|
||||
|
||||
return [NSString pathWithComponents:@[ testPath, testDataRepoVersionPath, jsonFileName ]];
|
||||
}
|
||||
|
||||
NSString *EventTypeToFilename(es_event_type_t eventType) {
|
||||
switch (eventType) {
|
||||
case ES_EVENT_TYPE_NOTIFY_CLOSE: return @"close.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA: return @"exchangedata.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_EXEC: return @"exec.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_EXIT: return @"exit.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_FORK: return @"fork.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_LINK: return @"link.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_RENAME: return @"rename.json";
|
||||
case ES_EVENT_TYPE_NOTIFY_UNLINK: return @"unlink.json";
|
||||
default: XCTFail(@"Unhandled event type: %d", eventType); return nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSString *LoadTestJson(NSString *jsonFileName, uint32_t version) {
|
||||
NSError *err = nil;
|
||||
NSString *jsonData = [NSString stringWithContentsOfFile:TestJsonPath(jsonFileName, version)
|
||||
encoding:NSUTF8StringEncoding
|
||||
error:&err];
|
||||
|
||||
if (err) {
|
||||
XCTFail(@"Failed to load test data \"%@\": %@", jsonFileName, err);
|
||||
}
|
||||
|
||||
return jsonData;
|
||||
}
|
||||
|
||||
bool CompareTime(const Timestamp ×tamp, struct timespec ts) {
|
||||
return timestamp.seconds() == ts.tv_sec && timestamp.nanos() == ts.tv_nsec;
|
||||
}
|
||||
|
||||
void CheckSantaMessage(const ::pbv1::SantaMessage &santaMsg, const es_message_t &esMsg,
|
||||
struct timespec enrichmentTime) {
|
||||
XCTAssertTrue(CompareTime(santaMsg.processed_time(), enrichmentTime));
|
||||
XCTAssertTrue(CompareTime(santaMsg.event_time(), esMsg.time));
|
||||
}
|
||||
|
||||
const google::protobuf::Message &SantaMessageEvent(const ::pbv1::SantaMessage &santaMsg) {
|
||||
switch (santaMsg.event_case()) {
|
||||
case ::pbv1::SantaMessage::kExecution: return santaMsg.execution();
|
||||
case ::pbv1::SantaMessage::kFork: return santaMsg.fork();
|
||||
case ::pbv1::SantaMessage::kExit: return santaMsg.exit();
|
||||
case ::pbv1::SantaMessage::kClose: return santaMsg.close();
|
||||
case ::pbv1::SantaMessage::kRename: return santaMsg.rename();
|
||||
case ::pbv1::SantaMessage::kUnlink: return santaMsg.unlink();
|
||||
case ::pbv1::SantaMessage::kLink: return santaMsg.link();
|
||||
case ::pbv1::SantaMessage::kExchangedata: return santaMsg.exchangedata();
|
||||
case ::pbv1::SantaMessage::kDisk: return santaMsg.disk();
|
||||
case ::pbv1::SantaMessage::kBundle: return santaMsg.bundle();
|
||||
case ::pbv1::SantaMessage::kAllowlist: return santaMsg.allowlist();
|
||||
case ::pbv1::SantaMessage::EVENT_NOT_SET:
|
||||
XCTFail(@"Protobuf message SantaMessage did not set an 'event' field");
|
||||
OS_FALLTHROUGH;
|
||||
default:
|
||||
[NSException raise:@"Required protobuf field not set"
|
||||
format:@"SantaMessage missing required field 'event'"];
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
std::string ConvertMessageToJsonString(const ::pbv1::SantaMessage &santaMsg) {
|
||||
JsonPrintOptions options = DefaultJsonPrintOptions();
|
||||
const google::protobuf::Message &message = SantaMessageEvent(santaMsg);
|
||||
|
||||
std::string json;
|
||||
XCTAssertTrue(google::protobuf::util::MessageToJsonString(message, &json, options).ok());
|
||||
return json;
|
||||
}
|
||||
|
||||
void CheckProto(const ::pbv1::SantaMessage &santaMsg,
|
||||
std::shared_ptr<EnrichedMessage> enrichedMsg) {
|
||||
return std::visit(
|
||||
[santaMsg](const EnrichedEventType &enrichedEvent) {
|
||||
CheckSantaMessage(santaMsg, enrichedEvent.es_msg(), enrichedEvent.enrichment_time());
|
||||
NSString *wantData = LoadTestJson(EventTypeToFilename(enrichedEvent.es_msg().event_type),
|
||||
enrichedEvent.es_msg().version);
|
||||
std::string got = ConvertMessageToJsonString(santaMsg);
|
||||
|
||||
XCTAssertEqualObjects([NSString stringWithUTF8String:got.c_str()], wantData);
|
||||
},
|
||||
enrichedMsg->GetEnrichedMessage());
|
||||
}
|
||||
|
||||
void SerializeAndCheck(es_event_type_t eventType,
|
||||
void (^messageSetup)(std::shared_ptr<MockEndpointSecurityAPI>,
|
||||
es_message_t *)) {
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
|
||||
cur_version++) {
|
||||
if (cur_version == 3) {
|
||||
// Note: Version 3 was only in a macOS beta.
|
||||
continue;
|
||||
}
|
||||
|
||||
es_file_t procFile = MakeESFile("foo", MakeStat(100));
|
||||
es_file_t ttyFile = MakeESFile("footty", MakeStat(200));
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(eventType, &proc);
|
||||
esMsg.process->tty = &ttyFile;
|
||||
esMsg.version = cur_version;
|
||||
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
messageSetup(mockESApi, &esMsg);
|
||||
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
|
||||
std::shared_ptr<EnrichedMessage> enrichedMsg = Enricher().Enrich(Message(mockESApi, &esMsg));
|
||||
|
||||
std::vector<uint8_t> vec = bs->SerializeMessage(enrichedMsg);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
|
||||
CheckProto(santaMsg, enrichedMsg);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
@interface ProtobufTest : XCTestCase
|
||||
@property id mockConfigurator;
|
||||
@property id mockDecisionCache;
|
||||
@property SNTCachedDecision *testCachedDecision;
|
||||
@end
|
||||
|
||||
@implementation ProtobufTest
|
||||
|
||||
- (void)setUp {
|
||||
self.mockConfigurator = OCMClassMock([SNTConfigurator class]);
|
||||
OCMStub([self.mockConfigurator configurator]).andReturn(self.mockConfigurator);
|
||||
OCMStub([self.mockConfigurator clientMode]).andReturn(SNTClientModeLockdown);
|
||||
OCMStub([self.mockConfigurator enableMachineIDDecoration]).andReturn(YES);
|
||||
OCMStub([self.mockConfigurator machineID]).andReturn(@"my_machine_id");
|
||||
|
||||
self.testCachedDecision = [[SNTCachedDecision alloc] init];
|
||||
self.testCachedDecision.decision = SNTEventStateAllowBinary;
|
||||
self.testCachedDecision.decisionExtra = @"extra!";
|
||||
self.testCachedDecision.sha256 = @"1234_file_hash";
|
||||
self.testCachedDecision.quarantineURL = @"google.com";
|
||||
self.testCachedDecision.certSHA256 = @"5678_cert_hash";
|
||||
|
||||
self.mockDecisionCache = OCMClassMock([SNTDecisionCache class]);
|
||||
OCMStub([self.mockDecisionCache sharedCache]).andReturn(self.mockDecisionCache);
|
||||
OCMStub([self.mockDecisionCache cachedDecisionForFile:{}])
|
||||
.ignoringNonObjectArgs()
|
||||
.andReturn(self.testCachedDecision);
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[self.mockConfigurator stopMocking];
|
||||
[self.mockDecisionCache stopMocking];
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageClose {
|
||||
__block es_file_t file = MakeESFile("close_file", MakeStat(300));
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_CLOSE,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.close.modified = true;
|
||||
esMsg->event.close.target = &file;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExchange {
|
||||
__block es_file_t file1 = MakeESFile("exchange_file_1", MakeStat(300));
|
||||
__block es_file_t file2 = MakeESFile("exchange_file_1", MakeStat(400));
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXCHANGEDATA,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.exchangedata.file1 = &file1;
|
||||
esMsg->event.exchangedata.file2 = &file2;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testGetDecisionEnum {
|
||||
std::map<SNTEventState, ::pbv1::Execution::Decision> stateToDecision = {
|
||||
{SNTEventStateUnknown, ::pbv1::Execution::DECISION_UNKNOWN},
|
||||
{SNTEventStateBundleBinary, ::pbv1::Execution::DECISION_UNKNOWN},
|
||||
{SNTEventStateBlockUnknown, ::pbv1::Execution::DECISION_DENY},
|
||||
{SNTEventStateBlockBinary, ::pbv1::Execution::DECISION_DENY},
|
||||
{SNTEventStateBlockCertificate, ::pbv1::Execution::DECISION_DENY},
|
||||
{SNTEventStateBlockScope, ::pbv1::Execution::DECISION_DENY},
|
||||
{SNTEventStateBlockTeamID, ::pbv1::Execution::DECISION_DENY},
|
||||
{SNTEventStateBlockLongPath, ::pbv1::Execution::DECISION_DENY},
|
||||
{SNTEventStateAllowUnknown, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowBinary, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowCertificate, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowScope, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowCompiler, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowTransitive, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowPendingTransitive, ::pbv1::Execution::DECISION_ALLOW},
|
||||
{SNTEventStateAllowTeamID, ::pbv1::Execution::DECISION_ALLOW},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToDecision) {
|
||||
XCTAssertEqual(GetDecisionEnum(kv.first), kv.second, @"Bad decision for state: %ld", kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testGetReasonEnum {
|
||||
std::map<SNTEventState, ::pbv1::Execution::Reason> stateToReason = {
|
||||
{SNTEventStateUnknown, ::pbv1::Execution::REASON_NOT_RUNNING},
|
||||
{SNTEventStateBundleBinary, ::pbv1::Execution::REASON_NOT_RUNNING},
|
||||
{SNTEventStateBlockUnknown, ::pbv1::Execution::REASON_UNKNOWN},
|
||||
{SNTEventStateBlockBinary, ::pbv1::Execution::REASON_BINARY},
|
||||
{SNTEventStateBlockCertificate, ::pbv1::Execution::REASON_CERT},
|
||||
{SNTEventStateBlockScope, ::pbv1::Execution::REASON_SCOPE},
|
||||
{SNTEventStateBlockTeamID, ::pbv1::Execution::REASON_TEAM_ID},
|
||||
{SNTEventStateBlockLongPath, ::pbv1::Execution::REASON_LONG_PATH},
|
||||
{SNTEventStateAllowUnknown, ::pbv1::Execution::REASON_UNKNOWN},
|
||||
{SNTEventStateAllowBinary, ::pbv1::Execution::REASON_BINARY},
|
||||
{SNTEventStateAllowCertificate, ::pbv1::Execution::REASON_CERT},
|
||||
{SNTEventStateAllowScope, ::pbv1::Execution::REASON_SCOPE},
|
||||
{SNTEventStateAllowCompiler, ::pbv1::Execution::REASON_COMPILER},
|
||||
{SNTEventStateAllowTransitive, ::pbv1::Execution::REASON_TRANSITIVE},
|
||||
{SNTEventStateAllowPendingTransitive, ::pbv1::Execution::REASON_PENDING_TRANSITIVE},
|
||||
{SNTEventStateAllowTeamID, ::pbv1::Execution::REASON_TEAM_ID},
|
||||
};
|
||||
|
||||
for (const auto &kv : stateToReason) {
|
||||
XCTAssertEqual(GetReasonEnum(kv.first), kv.second, @"Bad reason for state: %ld", kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testGetModeEnum {
|
||||
std::map<SNTClientMode, ::pbv1::Execution::Mode> clientModeToExecMode = {
|
||||
{SNTClientModeUnknown, ::pbv1::Execution::MODE_UNKNOWN},
|
||||
{SNTClientModeMonitor, ::pbv1::Execution::MODE_MONITOR},
|
||||
{SNTClientModeLockdown, ::pbv1::Execution::MODE_LOCKDOWN},
|
||||
{(SNTClientMode)123, ::pbv1::Execution::MODE_UNKNOWN},
|
||||
};
|
||||
|
||||
for (const auto &kv : clientModeToExecMode) {
|
||||
XCTAssertEqual(GetModeEnum(kv.first), kv.second, @"Bad mode for state: %ld", kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testGetFileDescriptorType {
|
||||
std::map<uint32_t, ::pbv1::FileDescriptor::FDType> fdtypeToEnumType = {
|
||||
{PROX_FDTYPE_ATALK, ::pbv1::FileDescriptor::FD_TYPE_ATALK},
|
||||
{PROX_FDTYPE_VNODE, ::pbv1::FileDescriptor::FD_TYPE_VNODE},
|
||||
{PROX_FDTYPE_SOCKET, ::pbv1::FileDescriptor::FD_TYPE_SOCKET},
|
||||
{PROX_FDTYPE_PSHM, ::pbv1::FileDescriptor::FD_TYPE_PSHM},
|
||||
{PROX_FDTYPE_PSEM, ::pbv1::FileDescriptor::FD_TYPE_PSEM},
|
||||
{PROX_FDTYPE_KQUEUE, ::pbv1::FileDescriptor::FD_TYPE_KQUEUE},
|
||||
{PROX_FDTYPE_PIPE, ::pbv1::FileDescriptor::FD_TYPE_PIPE},
|
||||
{PROX_FDTYPE_FSEVENTS, ::pbv1::FileDescriptor::FD_TYPE_FSEVENTS},
|
||||
{PROX_FDTYPE_NETPOLICY, ::pbv1::FileDescriptor::FD_TYPE_NETPOLICY},
|
||||
{10 /* PROX_FDTYPE_CHANNEL */, ::pbv1::FileDescriptor::FD_TYPE_CHANNEL},
|
||||
{11 /* PROX_FDTYPE_NEXUS */, ::pbv1::FileDescriptor::FD_TYPE_NEXUS},
|
||||
};
|
||||
|
||||
for (const auto &kv : fdtypeToEnumType) {
|
||||
XCTAssertEqual(GetFileDescriptorType(kv.first), kv.second, @"Bad fd type name for fdtype: %u",
|
||||
kv.first);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExec {
|
||||
es_file_t procFileTarget = MakeESFile("fooexec", MakeStat(300));
|
||||
__block es_process_t procTarget =
|
||||
MakeESProcess(&procFileTarget, MakeAuditToken(23, 45), MakeAuditToken(67, 89));
|
||||
__block es_file_t fileCwd = MakeESFile("cwd", MakeStat(400));
|
||||
__block es_file_t fileScript = MakeESFile("script.sh", MakeStat(500));
|
||||
__block es_fd_t fd1 = {.fd = 1, .fdtype = PROX_FDTYPE_VNODE};
|
||||
__block es_fd_t fd2 = {.fd = 2, .fdtype = PROX_FDTYPE_SOCKET};
|
||||
__block es_fd_t fd3 = {.fd = 3, .fdtype = PROX_FDTYPE_PIPE, .pipe = {.pipe_id = 123}};
|
||||
|
||||
procTarget.codesigning_flags = CS_SIGNED | CS_HARD | CS_KILL;
|
||||
memset(procTarget.cdhash, 'A', sizeof(procTarget.cdhash));
|
||||
procTarget.signing_id = MakeESStringToken("my_signing_id");
|
||||
procTarget.team_id = MakeESStringToken("my_team_id");
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXEC, ^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
|
||||
es_message_t *esMsg) {
|
||||
esMsg->event.exec.target = &procTarget;
|
||||
esMsg->event.exec.cwd = &fileCwd;
|
||||
esMsg->event.exec.script = &fileScript;
|
||||
|
||||
// For version 5, simulate a "truncated" set of FDs
|
||||
if (esMsg->version == 5) {
|
||||
esMsg->event.exec.last_fd = 123;
|
||||
} else {
|
||||
esMsg->event.exec.last_fd = 3;
|
||||
}
|
||||
|
||||
EXPECT_CALL(*mockESApi, ExecArgCount).WillOnce(testing::Return(3));
|
||||
EXPECT_CALL(*mockESApi, ExecArg)
|
||||
.WillOnce(testing::Return(MakeESStringToken("exec_path")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("-l")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("--foo")));
|
||||
|
||||
EXPECT_CALL(*mockESApi, ExecEnvCount).WillOnce(testing::Return(2));
|
||||
EXPECT_CALL(*mockESApi, ExecEnv)
|
||||
.WillOnce(testing::Return(MakeESStringToken("ENV_PATH=/path/to/bin:/and/another")))
|
||||
.WillOnce(testing::Return(MakeESStringToken("DEBUG=1")));
|
||||
|
||||
if (esMsg->version >= 4) {
|
||||
EXPECT_CALL(*mockESApi, ExecFDCount).WillOnce(testing::Return(3));
|
||||
EXPECT_CALL(*mockESApi, ExecFD)
|
||||
.WillOnce(testing::Return(&fd1))
|
||||
.WillOnce(testing::Return(&fd2))
|
||||
.WillOnce(testing::Return(&fd3));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageExit {
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_EXIT,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.exit.stat = W_EXITCODE(1, 0);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testEncodeExitStatus {
|
||||
{
|
||||
::pbv1::Exit pbExit;
|
||||
EncodeExitStatus(&pbExit, W_EXITCODE(1, 0));
|
||||
XCTAssertTrue(pbExit.has_exited());
|
||||
XCTAssertEqual(1, pbExit.exited().exit_status());
|
||||
}
|
||||
|
||||
{
|
||||
::pbv1::Exit pbExit;
|
||||
EncodeExitStatus(&pbExit, W_EXITCODE(2, SIGUSR1));
|
||||
XCTAssertTrue(pbExit.has_signaled());
|
||||
XCTAssertEqual(SIGUSR1, pbExit.signaled().signal());
|
||||
}
|
||||
|
||||
{
|
||||
::pbv1::Exit pbExit;
|
||||
EncodeExitStatus(&pbExit, W_STOPCODE(SIGSTOP));
|
||||
XCTAssertTrue(pbExit.has_stopped());
|
||||
XCTAssertEqual(SIGSTOP, pbExit.stopped().signal());
|
||||
}
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageFork {
|
||||
__block es_file_t procFileChild = MakeESFile("foo_child", MakeStat(300));
|
||||
__block es_file_t ttyFileChild = MakeESFile("footty", MakeStat(400));
|
||||
__block es_process_t procChild =
|
||||
MakeESProcess(&procFileChild, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
procChild.tty = &ttyFileChild;
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_FORK,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.fork.child = &procChild;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageLink {
|
||||
__block es_file_t fileSource = MakeESFile("source", MakeStat(300));
|
||||
__block es_file_t fileTargetDir = MakeESFile("target_dir");
|
||||
es_string_token_t targetTok = MakeESStringToken("target_file");
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_LINK,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.link.source = &fileSource;
|
||||
esMsg->event.link.target_dir = &fileTargetDir;
|
||||
esMsg->event.link.target_filename = targetTok;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageRename {
|
||||
__block es_file_t fileSource = MakeESFile("source", MakeStat(300));
|
||||
__block es_file_t fileTargetDir = MakeESFile("target_dir");
|
||||
es_string_token_t targetTok = MakeESStringToken("target_file");
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_RENAME,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.rename.source = &fileSource;
|
||||
// Test new and existing destination types
|
||||
if (esMsg->version == 4) {
|
||||
esMsg->event.rename.destination.existing_file = &fileTargetDir;
|
||||
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
|
||||
} else {
|
||||
esMsg->event.rename.destination.new_path.dir = &fileTargetDir;
|
||||
esMsg->event.rename.destination.new_path.filename = targetTok;
|
||||
esMsg->event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeMessageUnlink {
|
||||
__block es_file_t fileTarget = MakeESFile("unlink_file", MakeStat(300));
|
||||
__block es_file_t fileTargetParent = MakeESFile("unlink_file_parent", MakeStat(400));
|
||||
|
||||
SerializeAndCheck(ES_EVENT_TYPE_NOTIFY_UNLINK,
|
||||
^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi, es_message_t *esMsg) {
|
||||
esMsg->event.unlink.target = &fileTarget;
|
||||
esMsg->event.unlink.parent_dir = &fileTargetParent;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)testSerializeAllowlist {
|
||||
std::shared_ptr<MockEndpointSecurityAPI> mockESApi = std::make_shared<MockEndpointSecurityAPI>();
|
||||
|
||||
for (uint32_t cur_version = 1; cur_version <= MaxSupportedESMessageVersionForCurrentOS();
|
||||
cur_version++) {
|
||||
if (cur_version == 3) {
|
||||
// Note: Version 3 was only in a macOS beta.
|
||||
continue;
|
||||
}
|
||||
|
||||
es_file_t procFile = MakeESFile("foo", MakeStat(100));
|
||||
es_file_t ttyFile = MakeESFile("footty", MakeStat(200));
|
||||
es_file_t closeFile = MakeESFile("close_file", MakeStat(300));
|
||||
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
|
||||
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CLOSE, &proc);
|
||||
esMsg.process->tty = &ttyFile;
|
||||
esMsg.version = cur_version;
|
||||
esMsg.event.close.modified = true;
|
||||
esMsg.event.close.target = &closeFile;
|
||||
|
||||
mockESApi->SetExpectationsRetainReleaseMessage(&esMsg);
|
||||
|
||||
std::shared_ptr<Serializer> bs = Protobuf::Create(mockESApi);
|
||||
|
||||
std::vector<uint8_t> vec = bs->SerializeAllowlist(Message(mockESApi, &esMsg), "hash_value");
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
|
||||
NSString *wantData = LoadTestJson(@"allowlist.json", esMsg.version);
|
||||
std::string got = ConvertMessageToJsonString(santaMsg);
|
||||
|
||||
XCTAssertEqualObjects([NSString stringWithUTF8String:got.c_str()], wantData);
|
||||
}
|
||||
|
||||
XCTBubbleMockVerifyAndClearExpectations(mockESApi.get());
|
||||
}
|
||||
|
||||
- (void)testSerializeBundleHashingEvent {
|
||||
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
|
||||
|
||||
se.fileSHA256 = @"file_hash";
|
||||
se.fileBundleHash = @"file_bundle_hash";
|
||||
se.fileBundleName = @"file_bundle_name";
|
||||
se.fileBundleID = nil;
|
||||
se.fileBundlePath = @"file_bundle_path";
|
||||
se.filePath = @"file_path";
|
||||
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeBundleHashingEvent(se);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
XCTAssertTrue(santaMsg.has_bundle());
|
||||
|
||||
const ::pbv1::Bundle &pbBundle = santaMsg.bundle();
|
||||
|
||||
::pbv1::Hash pbHash = pbBundle.file_hash();
|
||||
XCTAssertEqualObjects(@(pbHash.hash().c_str()), se.fileSHA256);
|
||||
XCTAssertEqual(pbHash.type(), ::pbv1::Hash::HASH_ALGO_SHA256);
|
||||
|
||||
pbHash = pbBundle.bundle_hash();
|
||||
XCTAssertEqualObjects(@(pbHash.hash().c_str()), se.fileBundleHash);
|
||||
XCTAssertEqual(pbHash.type(), ::pbv1::Hash::HASH_ALGO_SHA256);
|
||||
|
||||
XCTAssertEqualObjects(@(pbBundle.bundle_name().c_str()), se.fileBundleName);
|
||||
XCTAssertEqualObjects(@(pbBundle.bundle_id().c_str()), @"");
|
||||
XCTAssertEqualObjects(@(pbBundle.bundle_path().c_str()), se.fileBundlePath);
|
||||
XCTAssertEqualObjects(@(pbBundle.path().c_str()), se.filePath);
|
||||
}
|
||||
|
||||
- (void)testSerializeDiskAppeared {
|
||||
NSDictionary *props = @{
|
||||
@"DADevicePath" : @"",
|
||||
@"DADeviceVendor" : @"vendor",
|
||||
@"DADeviceModel" : @"model",
|
||||
@"DAAppearanceTime" : @(123456789),
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"path"],
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
@"DAVolumeKind" : @"apfs",
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeDiskAppeared(props);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
XCTAssertTrue(santaMsg.has_disk());
|
||||
|
||||
const ::pbv1::Disk &pbDisk = santaMsg.disk();
|
||||
|
||||
XCTAssertEqual(pbDisk.action(), ::pbv1::Disk::ACTION_APPEARED);
|
||||
|
||||
XCTAssertEqualObjects(@(pbDisk.mount().c_str()), [props[@"DAVolumePath"] path]);
|
||||
XCTAssertEqualObjects(@(pbDisk.volume().c_str()), @"");
|
||||
XCTAssertEqualObjects(@(pbDisk.bsd_name().c_str()), props[@"DAMediaBSDName"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.fs().c_str()), props[@"DAVolumeKind"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.model().c_str()), @"vendor model");
|
||||
XCTAssertEqualObjects(@(pbDisk.serial().c_str()), @"");
|
||||
XCTAssertEqualObjects(@(pbDisk.bus().c_str()), props[@"DADeviceProtocol"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.dmg_path().c_str()), @"");
|
||||
|
||||
// Note: `DAAppearanceTime` is treated as a reference time since 2001 and is converted to a
|
||||
// reference time of 1970. Skip the calculation in the test here, just ensure the value is set.
|
||||
XCTAssertGreaterThan(pbDisk.appearance().seconds(), 1);
|
||||
}
|
||||
|
||||
- (void)testSerializeDiskDisppeared {
|
||||
NSDictionary *props = @{
|
||||
@"DADevicePath" : @"",
|
||||
@"DADeviceVendor" : @"vendor",
|
||||
@"DADeviceModel" : @"model",
|
||||
@"DAAppearanceTime" : @(123456789),
|
||||
@"DAVolumePath" : [NSURL URLWithString:@"path"],
|
||||
@"DAMediaBSDName" : @"bsd",
|
||||
@"DAVolumeKind" : @"apfs",
|
||||
@"DADeviceProtocol" : @"usb",
|
||||
};
|
||||
|
||||
std::vector<uint8_t> vec = Protobuf::Create(nullptr)->SerializeDiskDisappeared(props);
|
||||
std::string protoStr(vec.begin(), vec.end());
|
||||
|
||||
::pbv1::SantaMessage santaMsg;
|
||||
XCTAssertTrue(santaMsg.ParseFromString(protoStr));
|
||||
XCTAssertTrue(santaMsg.has_disk());
|
||||
|
||||
const ::pbv1::Disk &pbDisk = santaMsg.disk();
|
||||
|
||||
XCTAssertEqual(pbDisk.action(), ::pbv1::Disk::ACTION_DISAPPEARED);
|
||||
|
||||
XCTAssertEqualObjects(@(pbDisk.mount().c_str()), [props[@"DAVolumePath"] path]);
|
||||
XCTAssertEqualObjects(@(pbDisk.volume().c_str()), @"");
|
||||
XCTAssertEqualObjects(@(pbDisk.bsd_name().c_str()), props[@"DAMediaBSDName"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.fs().c_str()), props[@"DAVolumeKind"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.model().c_str()), @"vendor model");
|
||||
XCTAssertEqualObjects(@(pbDisk.serial().c_str()), @"");
|
||||
XCTAssertEqualObjects(@(pbDisk.bus().c_str()), props[@"DADeviceProtocol"]);
|
||||
XCTAssertEqualObjects(@(pbDisk.dmg_path().c_str()), @"");
|
||||
|
||||
// Note: `DAAppearanceTime` is treated as a reference time since 2001 and is converted to a
|
||||
// reference time of 1970. Skip the calculation in the test here, just ensure the value is set.
|
||||
XCTAssertGreaterThan(pbDisk.appearance().seconds(), 1);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
62
Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h
Normal file
62
Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h
Normal 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
|
||||
138
Source/santad/Logs/EndpointSecurity/Serializers/Utilities.mm
Normal file
138
Source/santad/Logs/EndpointSecurity/Serializers/Utilities.mm
Normal 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
|
||||
@@ -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
|
||||
74
Source/santad/Logs/EndpointSecurity/Writers/FSSpool/BUILD
Normal file
74
Source/santad/Logs/EndpointSecurity/Writers/FSSpool/BUILD
Normal 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",
|
||||
],
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
302
Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.cc
Normal file
302
Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.cc
Normal 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
|
||||
105
Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.h
Normal file
105
Source/santad/Logs/EndpointSecurity/Writers/FSSpool/fsspool.h
Normal 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_
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
76
Source/santad/Logs/EndpointSecurity/Writers/Spool.h
Normal file
76
Source/santad/Logs/EndpointSecurity/Writers/Spool.h
Normal 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
|
||||
146
Source/santad/Logs/EndpointSecurity/Writers/Spool.mm
Normal file
146
Source/santad/Logs/EndpointSecurity/Writers/Spool.mm
Normal 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
|
||||
156
Source/santad/Logs/EndpointSecurity/Writers/SpoolTest.mm
Normal file
156
Source/santad/Logs/EndpointSecurity/Writers/SpoolTest.mm
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
6
Source/santad/testdata/BUILD
vendored
6
Source/santad/testdata/BUILD
vendored
@@ -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"],
|
||||
)
|
||||
|
||||
61
Source/santad/testdata/protobuf/v1/allowlist.json
vendored
Normal file
61
Source/santad/testdata/protobuf/v1/allowlist.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
64
Source/santad/testdata/protobuf/v1/close.json
vendored
Normal file
64
Source/santad/testdata/protobuf/v1/close.json
vendored
Normal 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
|
||||
}
|
||||
91
Source/santad/testdata/protobuf/v1/exchangedata.json
vendored
Normal file
91
Source/santad/testdata/protobuf/v1/exchangedata.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
124
Source/santad/testdata/protobuf/v1/exec.json
vendored
Normal file
124
Source/santad/testdata/protobuf/v1/exec.json
vendored
Normal 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"
|
||||
}
|
||||
38
Source/santad/testdata/protobuf/v1/exit.json
vendored
Normal file
38
Source/santad/testdata/protobuf/v1/exit.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
68
Source/santad/testdata/protobuf/v1/fork.json
vendored
Normal file
68
Source/santad/testdata/protobuf/v1/fork.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
64
Source/santad/testdata/protobuf/v1/link.json
vendored
Normal file
64
Source/santad/testdata/protobuf/v1/link.json
vendored
Normal 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"
|
||||
}
|
||||
65
Source/santad/testdata/protobuf/v1/rename.json
vendored
Normal file
65
Source/santad/testdata/protobuf/v1/rename.json
vendored
Normal 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
|
||||
}
|
||||
63
Source/santad/testdata/protobuf/v1/unlink.json
vendored
Normal file
63
Source/santad/testdata/protobuf/v1/unlink.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
61
Source/santad/testdata/protobuf/v2/allowlist.json
vendored
Normal file
61
Source/santad/testdata/protobuf/v2/allowlist.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
64
Source/santad/testdata/protobuf/v2/close.json
vendored
Normal file
64
Source/santad/testdata/protobuf/v2/close.json
vendored
Normal 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
|
||||
}
|
||||
91
Source/santad/testdata/protobuf/v2/exchangedata.json
vendored
Normal file
91
Source/santad/testdata/protobuf/v2/exchangedata.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
152
Source/santad/testdata/protobuf/v2/exec.json
vendored
Normal file
152
Source/santad/testdata/protobuf/v2/exec.json
vendored
Normal 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"
|
||||
}
|
||||
38
Source/santad/testdata/protobuf/v2/exit.json
vendored
Normal file
38
Source/santad/testdata/protobuf/v2/exit.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
68
Source/santad/testdata/protobuf/v2/fork.json
vendored
Normal file
68
Source/santad/testdata/protobuf/v2/fork.json
vendored
Normal 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
Reference in New Issue
Block a user