Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move handling for path targets to the FAAPolicyProcessor #264

Merged
merged 1 commit into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Source/santad/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -1497,7 +1497,9 @@ santa_unit_test(
name = "FAAPolicyProcessorTest",
srcs = ["EventProviders/FAAPolicyProcessorTest.mm"],
deps = [
":EndpointSecurityMessage",
":FAAPolicyProcessor",
":MockEndpointSecurityAPI",
":MockFAAPolicyProcessor",
":SNTDecisionCache",
":WatchItemPolicy",
Expand Down
13 changes: 12 additions & 1 deletion Source/santad/EventProviders/FAAPolicyProcessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ namespace santa {

class FAAPolicyProcessor {
public:
// Small structure to hold a complete event path target being operated upon and
// a bool indicating whether the path is a readable target (e.g. a file being
// opened or cloned)
struct PathTarget {
std::string path;
bool is_readable;
std::optional<std::pair<dev_t, ino_t>> devno_ino;
};

friend class MockFAAPolicyProcessor;

FAAPolicyProcessor(SNTDecisionCache *decision_cache);

virtual ~FAAPolicyProcessor() = default;
Expand All @@ -49,7 +60,7 @@ class FAAPolicyProcessor {

virtual SNTCachedDecision *__strong GetCachedDecision(const struct stat &stat_buf);

friend class MockFAAPolicyProcessor;
static void PopulatePathTargets(const Message &msg, std::vector<PathTarget> &targets);

private:
SNTDecisionCache *decision_cache_;
Expand Down
98 changes: 98 additions & 0 deletions Source/santad/EventProviders/FAAPolicyProcessor.mm
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,102 @@
return [decision_cache_ cachedDecisionForFile:stat_buf];
}

static inline std::string Path(const es_file_t *esFile) {
return std::string(esFile->path.data, esFile->path.length);
}

static inline std::string Path(const es_string_token_t &tok) {
return std::string(tok.data, tok.length);
}

static inline void PushBackIfNotTruncated(std::vector<FAAPolicyProcessor::PathTarget> &vec,
const es_file_t *esFile, bool isReadable = false) {
if (!esFile->path_truncated) {
vec.push_back({Path(esFile), isReadable,
isReadable ? std::make_optional<std::pair<dev_t, ino_t>>(
{esFile->stat.st_dev, esFile->stat.st_ino})
: std::nullopt});
}
}

// Note: This variant of PushBackIfNotTruncated can never be marked "isReadable"
static inline void PushBackIfNotTruncated(std::vector<FAAPolicyProcessor::PathTarget> &vec,
const es_file_t *dir, const es_string_token_t &name) {
if (!dir->path_truncated) {
vec.push_back({Path(dir) + "/" + Path(name), false, std::nullopt});
}
}

void FAAPolicyProcessor::PopulatePathTargets(const Message &msg,
std::vector<FAAPolicyProcessor::PathTarget> &targets) {
switch (msg->event_type) {
case ES_EVENT_TYPE_AUTH_CLONE:
PushBackIfNotTruncated(targets, msg->event.clone.source, true);
PushBackIfNotTruncated(targets, msg->event.clone.target_dir, msg->event.clone.target_name);
break;

case ES_EVENT_TYPE_AUTH_CREATE:
// AUTH CREATE events should always be ES_DESTINATION_TYPE_NEW_PATH
if (msg->event.create.destination_type == ES_DESTINATION_TYPE_NEW_PATH) {
PushBackIfNotTruncated(targets, msg->event.create.destination.new_path.dir,
msg->event.create.destination.new_path.filename);
} else {
LOGW(@"Unexpected destination type for create event: %d. Ignoring target.",
msg->event.create.destination_type);
}
break;

case ES_EVENT_TYPE_AUTH_COPYFILE:
PushBackIfNotTruncated(targets, msg->event.copyfile.source, true);
if (msg->event.copyfile.target_file) {
PushBackIfNotTruncated(targets, msg->event.copyfile.target_file);
} else {
PushBackIfNotTruncated(targets, msg->event.copyfile.target_dir,
msg->event.copyfile.target_name);
}
break;

case ES_EVENT_TYPE_AUTH_EXCHANGEDATA:
PushBackIfNotTruncated(targets, msg->event.exchangedata.file1);
PushBackIfNotTruncated(targets, msg->event.exchangedata.file2);
break;

case ES_EVENT_TYPE_AUTH_LINK:
PushBackIfNotTruncated(targets, msg->event.link.source);
PushBackIfNotTruncated(targets, msg->event.link.target_dir, msg->event.link.target_filename);
break;

case ES_EVENT_TYPE_AUTH_OPEN:
PushBackIfNotTruncated(targets, msg->event.open.file, true);
break;

case ES_EVENT_TYPE_AUTH_RENAME:
PushBackIfNotTruncated(targets, msg->event.rename.source);
if (msg->event.rename.destination_type == ES_DESTINATION_TYPE_EXISTING_FILE) {
PushBackIfNotTruncated(targets, msg->event.rename.destination.existing_file);
} else if (msg->event.rename.destination_type == ES_DESTINATION_TYPE_NEW_PATH) {
PushBackIfNotTruncated(targets, msg->event.rename.destination.new_path.dir,
msg->event.rename.destination.new_path.filename);
} else {
LOGW(@"Unexpected destination type for rename event: %d. Ignoring destination.",
msg->event.rename.destination_type);
}
break;

case ES_EVENT_TYPE_AUTH_TRUNCATE:
PushBackIfNotTruncated(targets, msg->event.truncate.target);
break;

case ES_EVENT_TYPE_AUTH_UNLINK:
PushBackIfNotTruncated(targets, msg->event.unlink.target);
break;

default:
[NSException
raise:@"Unexpected event type"
format:@"File Access Authorizer client does not handle event: %d", msg->event_type];
exit(EXIT_FAILURE);
}
}

} // namespace santa
208 changes: 208 additions & 0 deletions Source/santad/EventProviders/FAAPolicyProcessorTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@
#import "Source/common/SNTCachedDecision.h"
#include "Source/common/TestUtils.h"
#include "Source/santad/DataLayer/WatchItemPolicy.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/EventProviders/EndpointSecurity/MockEndpointSecurityAPI.h"
#include "Source/santad/EventProviders/MockFAAPolicyProcessor.h"
#include "Source/santad/SNTDecisionCache.h"

using santa::FAAPolicyProcessor;
using santa::Message;
using santa::MockFAAPolicyProcessor;
using santa::WatchItemProcess;

Expand All @@ -42,6 +46,11 @@ static void ClearWatchItemPolicyProcess(WatchItemProcess &proc) {
proc.cdhash.clear();
}

// Helper to create a devno/ino pair from an es_file_t
static inline std::pair<dev_t, ino_t> FileID(const es_file_t &file) {
return std::make_pair(file.stat.st_dev, file.stat.st_ino);
}

@interface FAAPolicyProcessorTest : XCTestCase
@property id cscMock;
@property id dcMock;
Expand Down Expand Up @@ -257,4 +266,203 @@ - (void)testPolicyProcessMatchesESProcess {
}
}

- (void)testPopulatePathTargets {
// This test ensures that the `GetPathTargets` functions returns the
// expected combination of targets for each handled event variant
es_file_t testFile1 = MakeESFile("test_file_1", MakeStat(100));
es_file_t testFile2 = MakeESFile("test_file_2", MakeStat(200));
es_file_t testDir = MakeESFile("test_dir", MakeStat(300));
es_string_token_t testTok = MakeESStringToken("test_tok");
std::string dirTok = std::string(testDir.path.data) + "/" + std::string(testTok.data);

es_message_t esMsg;

auto mockESApi = std::make_shared<MockEndpointSecurityAPI>();
mockESApi->SetExpectationsRetainReleaseMessage();

Message msg(mockESApi, &esMsg);

{
esMsg.event_type = ES_EVENT_TYPE_AUTH_OPEN;
esMsg.event.open.file = &testFile1;

std::vector<FAAPolicyProcessor::PathTarget> targets;
FAAPolicyProcessor::PopulatePathTargets(msg, targets);

XCTAssertEqual(targets.size(), 1);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].is_readable);
XCTAssertEqual(targets[0].devno_ino.value(), FileID(testFile1));
}

{
esMsg.event_type = ES_EVENT_TYPE_AUTH_LINK;
esMsg.event.link.source = &testFile1;
esMsg.event.link.target_dir = &testDir;
esMsg.event.link.target_filename = testTok;

std::vector<FAAPolicyProcessor::PathTarget> targets;
FAAPolicyProcessor::PopulatePathTargets(msg, targets);

XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].is_readable);
XCTAssertFalse(targets[0].devno_ino.has_value());
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].is_readable);
XCTAssertFalse(targets[1].devno_ino.has_value());
}

{
esMsg.event_type = ES_EVENT_TYPE_AUTH_RENAME;
esMsg.event.rename.source = &testFile1;

{
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_EXISTING_FILE;
esMsg.event.rename.destination.existing_file = &testFile2;

std::vector<FAAPolicyProcessor::PathTarget> targets;
FAAPolicyProcessor::PopulatePathTargets(msg, targets);

XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].is_readable);
XCTAssertFalse(targets[0].devno_ino.has_value());
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
XCTAssertFalse(targets[1].is_readable);
XCTAssertFalse(targets[1].devno_ino.has_value());
}

{
esMsg.event.rename.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
esMsg.event.rename.destination.new_path.dir = &testDir;
esMsg.event.rename.destination.new_path.filename = testTok;

std::vector<FAAPolicyProcessor::PathTarget> targets;
FAAPolicyProcessor::PopulatePathTargets(msg, targets);

XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].is_readable);
XCTAssertFalse(targets[0].devno_ino.has_value());
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].is_readable);
XCTAssertFalse(targets[1].devno_ino.has_value());
}
}

{
esMsg.event_type = ES_EVENT_TYPE_AUTH_UNLINK;
esMsg.event.unlink.target = &testFile1;

std::vector<FAAPolicyProcessor::PathTarget> targets;
FAAPolicyProcessor::PopulatePathTargets(msg, targets);

XCTAssertEqual(targets.size(), 1);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].is_readable);
XCTAssertFalse(targets[0].devno_ino.has_value());
}

{
esMsg.event_type = ES_EVENT_TYPE_AUTH_CLONE;
esMsg.event.clone.source = &testFile1;
esMsg.event.clone.target_dir = &testDir;
esMsg.event.clone.target_name = testTok;

std::vector<FAAPolicyProcessor::PathTarget> targets;
FAAPolicyProcessor::PopulatePathTargets(msg, targets);

XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].is_readable);
XCTAssertEqual(targets[0].devno_ino.value(), FileID(testFile1));
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].is_readable);
XCTAssertFalse(targets[1].devno_ino.has_value());
}

{
esMsg.event_type = ES_EVENT_TYPE_AUTH_EXCHANGEDATA;
esMsg.event.exchangedata.file1 = &testFile1;
esMsg.event.exchangedata.file2 = &testFile2;

std::vector<FAAPolicyProcessor::PathTarget> targets;
FAAPolicyProcessor::PopulatePathTargets(msg, targets);

XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].is_readable);
XCTAssertFalse(targets[0].devno_ino.has_value());
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
XCTAssertFalse(targets[1].is_readable);
XCTAssertFalse(targets[1].devno_ino.has_value());
}

{
esMsg.event_type = ES_EVENT_TYPE_AUTH_CREATE;
esMsg.event.create.destination_type = ES_DESTINATION_TYPE_NEW_PATH;
esMsg.event.create.destination.new_path.dir = &testDir;
esMsg.event.create.destination.new_path.filename = testTok;

std::vector<FAAPolicyProcessor::PathTarget> targets;
FAAPolicyProcessor::PopulatePathTargets(msg, targets);

XCTAssertEqual(targets.size(), 1);
XCTAssertCppStringEqual(targets[0].path, dirTok);
XCTAssertFalse(targets[0].is_readable);
XCTAssertFalse(targets[0].devno_ino.has_value());
}

{
esMsg.event_type = ES_EVENT_TYPE_AUTH_TRUNCATE;
esMsg.event.truncate.target = &testFile1;

std::vector<FAAPolicyProcessor::PathTarget> targets;
FAAPolicyProcessor::PopulatePathTargets(msg, targets);

XCTAssertEqual(targets.size(), 1);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertFalse(targets[0].is_readable);
XCTAssertFalse(targets[0].devno_ino.has_value());
}

{
esMsg.event_type = ES_EVENT_TYPE_AUTH_COPYFILE;
esMsg.event.copyfile.source = &testFile1;
esMsg.event.copyfile.target_dir = &testDir;
esMsg.event.copyfile.target_name = testTok;

{
esMsg.event.copyfile.target_file = nullptr;

std::vector<FAAPolicyProcessor::PathTarget> targets;
FAAPolicyProcessor::PopulatePathTargets(msg, targets);

XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].is_readable);
XCTAssertEqual(targets[0].devno_ino.value(), FileID(testFile1));
XCTAssertCppStringEqual(targets[1].path, dirTok);
XCTAssertFalse(targets[1].is_readable);
XCTAssertFalse(targets[1].devno_ino.has_value());
}

{
esMsg.event.copyfile.target_file = &testFile2;

std::vector<FAAPolicyProcessor::PathTarget> targets;
FAAPolicyProcessor::PopulatePathTargets(msg, targets);

XCTAssertEqual(targets.size(), 2);
XCTAssertCStringEqual(targets[0].path.c_str(), testFile1.path.data);
XCTAssertTrue(targets[0].is_readable);
XCTAssertEqual(targets[0].devno_ino.value(), FileID(testFile1));
XCTAssertCStringEqual(targets[1].path.c_str(), testFile2.path.data);
XCTAssertFalse(targets[1].is_readable);
XCTAssertFalse(targets[1].devno_ino.has_value());
}
}
}

@end
Loading