sync: Add a protobuf for the existing sync protocol (#1359)

This PR is intended to have no impact on existing sync servers. The fields and enum values in the protobuf have been named such that their JSON equivalents match the existing constants we have in the codebase.

Adding this provides a few benefits:

1. The protobuf serves as canonical documentation of the protocol in a form that's much easier to read than the existing code.
2. Protobuf parsing of JSON is likely to be better than our hand-written version.
3. We can (in a later PR) add a configuration option to use binary encoding instead of JSON, saving network during syncs.
4. Servers written in other languages are easier to write and update as time goes on, especially as we extend the protocol.
This commit is contained in:
Russell Hancox
2024-05-29 14:22:49 -04:00
committed by GitHub
parent 7502bc247f
commit a23b67d5de
11 changed files with 721 additions and 312 deletions

View File

@@ -1,4 +1,5 @@
load("@build_bazel_rules_apple//apple:macos.bzl", "macos_command_line_application")
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
load("//:helper.bzl", "santa_unit_test")
licenses(["notice"])
@@ -7,6 +8,16 @@ package(
default_visibility = ["//:santa_package_group"],
)
proto_library(
name = "sync_v1_proto",
srcs = ["syncv1.proto"],
)
cc_proto_library(
name = "sync_v1_cc_proto",
deps = [":sync_v1_proto"],
)
objc_library(
name = "FCM_lib",
srcs = ["SNTSyncFCM.m"],
@@ -27,18 +38,18 @@ objc_library(
"SNTPushNotificationsTracker.h",
"SNTPushNotificationsTracker.m",
"SNTSyncEventUpload.h",
"SNTSyncEventUpload.m",
"SNTSyncEventUpload.mm",
"SNTSyncLogging.h",
"SNTSyncLogging.m",
"SNTSyncManager.m",
"SNTSyncPostflight.h",
"SNTSyncPostflight.m",
"SNTSyncPostflight.mm",
"SNTSyncPreflight.h",
"SNTSyncPreflight.m",
"SNTSyncPreflight.mm",
"SNTSyncRuleDownload.h",
"SNTSyncRuleDownload.m",
"SNTSyncRuleDownload.mm",
"SNTSyncStage.h",
"SNTSyncStage.m",
"SNTSyncStage.mm",
"SNTSyncState.h",
"SNTSyncState.m",
],
@@ -47,6 +58,7 @@ objc_library(
deps = [
":FCM_lib",
":broadcaster_lib",
":sync_v1_cc_proto",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTFileInfo",
@@ -58,9 +70,11 @@ objc_library(
"//Source/common:SNTSystemInfo",
"//Source/common:SNTXPCControlInterface",
"//Source/common:SNTXPCSyncServiceInterface",
"//Source/common:String",
"@MOLAuthenticatingURLSession",
"@MOLCertificate",
"@MOLXPCConnection",
"@com_google_protobuf//src/google/protobuf/json",
],
)
@@ -77,20 +91,20 @@ santa_unit_test(
"SNTPushNotificationsTracker.h",
"SNTPushNotificationsTracker.m",
"SNTSyncEventUpload.h",
"SNTSyncEventUpload.m",
"SNTSyncEventUpload.mm",
"SNTSyncLogging.h",
"SNTSyncLogging.m",
"SNTSyncPostflight.h",
"SNTSyncPostflight.m",
"SNTSyncPostflight.mm",
"SNTSyncPreflight.h",
"SNTSyncPreflight.m",
"SNTSyncPreflight.mm",
"SNTSyncRuleDownload.h",
"SNTSyncRuleDownload.m",
"SNTSyncRuleDownload.mm",
"SNTSyncStage.h",
"SNTSyncStage.m",
"SNTSyncStage.mm",
"SNTSyncState.h",
"SNTSyncState.m",
"SNTSyncTest.m",
"SNTSyncTest.mm",
],
resources = glob([
"testdata/*.json",
@@ -100,6 +114,7 @@ santa_unit_test(
deps = [
":FCM_lib",
":broadcaster_lib",
":sync_v1_cc_proto",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTDropRootPrivs",
@@ -111,10 +126,12 @@ santa_unit_test(
"//Source/common:SNTSyncConstants",
"//Source/common:SNTSystemInfo",
"//Source/common:SNTXPCControlInterface",
"//Source/common:String",
"@MOLAuthenticatingURLSession",
"@MOLCertificate",
"@MOLXPCConnection",
"@OCMock",
"@com_google_protobuf//src/google/protobuf/json",
],
)

View File

@@ -1,167 +0,0 @@
/// Copyright 2015 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santasyncservice/SNTSyncEventUpload.h"
#import <MOLCertificate/MOLCertificate.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTFileInfo.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTStoredEvent.h"
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santasyncservice/NSData+Zlib.h"
#import "Source/santasyncservice/SNTSyncLogging.h"
#import "Source/santasyncservice/SNTSyncState.h"
@implementation SNTSyncEventUpload
- (NSURL *)stageURL {
NSString *stageName = [@"eventupload" stringByAppendingFormat:@"/%@", self.syncState.machineID];
return [NSURL URLWithString:stageName relativeToURL:self.syncState.syncBaseURL];
}
- (BOOL)sync {
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[[self.daemonConn remoteObjectProxy] databaseEventsPending:^(NSArray *events) {
if (events.count) {
[self uploadEvents:events];
}
dispatch_semaphore_signal(sema);
}];
return (dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER) == 0);
}
- (BOOL)uploadEvents:(NSArray *)events {
NSMutableArray *uploadEvents = [[NSMutableArray alloc] init];
NSMutableSet *eventIds = [NSMutableSet setWithCapacity:events.count];
for (SNTStoredEvent *event in events) {
[uploadEvents addObject:[self dictionaryForEvent:event]];
if (event.idx) [eventIds addObject:event.idx];
if (uploadEvents.count >= self.syncState.eventBatchSize) break;
}
if (self.syncState.syncType == SNTSyncTypeNormal ||
[[SNTConfigurator configurator] enableCleanSyncEventUpload]) {
NSDictionary *r = [self performRequest:[self requestWithDictionary:@{kEvents : uploadEvents}]];
if (!r) return NO;
// A list of bundle hashes that require their related binary events to be uploaded.
self.syncState.bundleBinaryRequests = r[kEventUploadBundleBinaries];
SLOGI(@"Uploaded %lu events", uploadEvents.count);
}
// Remove event IDs. For Bundle Events the ID is 0 so nothing happens.
[[self.daemonConn remoteObjectProxy] databaseRemoveEventsWithIDs:[eventIds allObjects]];
// See if there are any events remaining to upload
if (uploadEvents.count < events.count) {
NSRange nextEventsRange = NSMakeRange(uploadEvents.count, events.count - uploadEvents.count);
NSArray *nextEvents = [events subarrayWithRange:nextEventsRange];
return [self uploadEvents:nextEvents];
}
return YES;
}
- (NSDictionary *)dictionaryForEvent:(SNTStoredEvent *)event {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-literal-conversion"
#define ADDKEY(dict, key, value) \
if (value) dict[key] = value
NSMutableDictionary *newEvent = [NSMutableDictionary dictionary];
ADDKEY(newEvent, kFileSHA256, event.fileSHA256);
ADDKEY(newEvent, kFilePath, [event.filePath stringByDeletingLastPathComponent]);
ADDKEY(newEvent, kFileName, [event.filePath lastPathComponent]);
ADDKEY(newEvent, kExecutingUser, event.executingUser);
ADDKEY(newEvent, kExecutionTime, @([event.occurrenceDate timeIntervalSince1970]));
ADDKEY(newEvent, kLoggedInUsers, event.loggedInUsers);
ADDKEY(newEvent, kCurrentSessions, event.currentSessions);
switch (event.decision) {
case SNTEventStateAllowUnknown: ADDKEY(newEvent, kDecision, kDecisionAllowUnknown); break;
case SNTEventStateAllowBinary: ADDKEY(newEvent, kDecision, kDecisionAllowBinary); break;
case SNTEventStateAllowCertificate:
ADDKEY(newEvent, kDecision, kDecisionAllowCertificate);
break;
case SNTEventStateAllowScope: ADDKEY(newEvent, kDecision, kDecisionAllowScope); break;
case SNTEventStateAllowTeamID: ADDKEY(newEvent, kDecision, kDecisionAllowTeamID); break;
case SNTEventStateAllowSigningID: ADDKEY(newEvent, kDecision, kDecisionAllowSigningID); break;
case SNTEventStateAllowCDHash: ADDKEY(newEvent, kDecision, kDecisionAllowCDHash); break;
case SNTEventStateBlockUnknown: ADDKEY(newEvent, kDecision, kDecisionBlockUnknown); break;
case SNTEventStateBlockBinary: ADDKEY(newEvent, kDecision, kDecisionBlockBinary); break;
case SNTEventStateBlockCertificate:
ADDKEY(newEvent, kDecision, kDecisionBlockCertificate);
break;
case SNTEventStateBlockScope: ADDKEY(newEvent, kDecision, kDecisionBlockScope); break;
case SNTEventStateBlockTeamID: ADDKEY(newEvent, kDecision, kDecisionBlockTeamID); break;
case SNTEventStateBlockSigningID: ADDKEY(newEvent, kDecision, kDecisionBlockSigningID); break;
case SNTEventStateBlockCDHash: ADDKEY(newEvent, kDecision, kDecisionBlockCDHash); break;
case SNTEventStateBundleBinary:
ADDKEY(newEvent, kDecision, kDecisionBundleBinary);
[newEvent removeObjectForKey:kExecutionTime];
break;
default: ADDKEY(newEvent, kDecision, kDecisionUnknown);
}
ADDKEY(newEvent, kFileBundleID, event.fileBundleID);
ADDKEY(newEvent, kFileBundlePath, event.fileBundlePath);
ADDKEY(newEvent, kFileBundleExecutableRelPath, event.fileBundleExecutableRelPath);
ADDKEY(newEvent, kFileBundleName, event.fileBundleName);
ADDKEY(newEvent, kFileBundleVersion, event.fileBundleVersion);
ADDKEY(newEvent, kFileBundleShortVersionString, event.fileBundleVersionString);
ADDKEY(newEvent, kFileBundleHash, event.fileBundleHash);
ADDKEY(newEvent, kFileBundleHashMilliseconds, event.fileBundleHashMilliseconds);
ADDKEY(newEvent, kFileBundleBinaryCount, event.fileBundleBinaryCount);
ADDKEY(newEvent, kPID, event.pid);
ADDKEY(newEvent, kPPID, event.ppid);
ADDKEY(newEvent, kParentName, event.parentName);
ADDKEY(newEvent, kQuarantineDataURL, event.quarantineDataURL);
ADDKEY(newEvent, kQuarantineRefererURL, event.quarantineRefererURL);
ADDKEY(newEvent, kQuarantineTimestamp, @([event.quarantineTimestamp timeIntervalSince1970]));
ADDKEY(newEvent, kQuarantineAgentBundleID, event.quarantineAgentBundleID);
NSMutableArray *signingChain = [NSMutableArray arrayWithCapacity:event.signingChain.count];
for (NSUInteger i = 0; i < event.signingChain.count; ++i) {
MOLCertificate *cert = [event.signingChain objectAtIndex:i];
NSMutableDictionary *certDict = [NSMutableDictionary dictionary];
ADDKEY(certDict, kCertSHA256, cert.SHA256);
ADDKEY(certDict, kCertCN, cert.commonName);
ADDKEY(certDict, kCertOrg, cert.orgName);
ADDKEY(certDict, kCertOU, cert.orgUnit);
ADDKEY(certDict, kCertValidFrom, @([cert.validFrom timeIntervalSince1970]));
ADDKEY(certDict, kCertValidUntil, @([cert.validUntil timeIntervalSince1970]));
[signingChain addObject:certDict];
}
newEvent[kSigningChain] = signingChain;
ADDKEY(newEvent, kTeamID, event.teamID);
ADDKEY(newEvent, kSigningID, event.signingID);
ADDKEY(newEvent, kCDHash, event.cdhash);
return newEvent;
#undef ADDKEY
#pragma clang diagnostic pop
}
@end

View File

@@ -0,0 +1,180 @@
/// Copyright 2015 Google Inc. All rights reserved.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
#import "Source/santasyncservice/SNTSyncEventUpload.h"
#import <MOLCertificate/MOLCertificate.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTFileInfo.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTStoredEvent.h"
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/common/String.h"
#import "Source/santasyncservice/NSData+Zlib.h"
#import "Source/santasyncservice/SNTSyncLogging.h"
#import "Source/santasyncservice/SNTSyncState.h"
#include <google/protobuf/arena.h>
#include "Source/santasyncservice/syncv1.pb.h"
namespace pbv1 = ::santa::sync::v1;
using santa::common::NSStringToUTF8String;
@implementation SNTSyncEventUpload
- (NSURL *)stageURL {
NSString *stageName = [@"eventupload" stringByAppendingFormat:@"/%@", self.syncState.machineID];
return [NSURL URLWithString:stageName relativeToURL:self.syncState.syncBaseURL];
}
- (BOOL)sync {
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[[self.daemonConn remoteObjectProxy] databaseEventsPending:^(NSArray *events) {
if (events.count) {
[self uploadEvents:events];
}
dispatch_semaphore_signal(sema);
}];
return (dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER) == 0);
}
- (BOOL)uploadEvents:(NSArray *)events {
google::protobuf::Arena arena;
auto req = google::protobuf::Arena::Create<::pbv1::EventUploadRequest>(&arena);
google::protobuf::RepeatedPtrField<::pbv1::Event> *uploadEvents = req->mutable_events();
NSMutableSet *eventIds = [NSMutableSet setWithCapacity:events.count];
for (SNTStoredEvent *event in events) {
uploadEvents->Add([self messageForEvent:event]);
if (event.idx) [eventIds addObject:event.idx];
if (uploadEvents->size() >= self.syncState.eventBatchSize) break;
}
if (self.syncState.syncType == SNTSyncTypeNormal ||
[[SNTConfigurator configurator] enableCleanSyncEventUpload]) {
::pbv1::EventUploadResponse response;
NSError *err = [self performRequest:[self requestWithMessage:req]
intoMessage:&response
timeout:30];
if (err) {
SLOGE(@"Failed to upload events: %@", err);
return NO;
}
// A list of bundle hashes that require their related binary events to be uploaded.
if (response.event_upload_bundle_binaries_size()) {
self.syncState.bundleBinaryRequests =
[NSMutableArray arrayWithCapacity:response.event_upload_bundle_binaries_size()];
for (const std::string &bundle_binary : response.event_upload_bundle_binaries()) {
[(NSMutableArray *)self.syncState.bundleBinaryRequests
addObject:santa::common::StringToNSString(bundle_binary)];
}
}
SLOGI(@"Uploaded %d events", uploadEvents->size());
}
// Remove event IDs. For Bundle Events the ID is 0 so nothing happens.
[[self.daemonConn remoteObjectProxy] databaseRemoveEventsWithIDs:[eventIds allObjects]];
// See if there are any events remaining to upload
if (uploadEvents->size() < events.count) {
NSRange nextEventsRange =
NSMakeRange(uploadEvents->size(), events.count - uploadEvents->size());
NSArray *nextEvents = [events subarrayWithRange:nextEventsRange];
return [self uploadEvents:nextEvents];
}
return YES;
}
- (::pbv1::Event)messageForEvent:(SNTStoredEvent *)event {
google::protobuf::Arena arena;
auto e = google::protobuf::Arena::Create<::pbv1::Event>(&arena);
e->set_file_sha256(NSStringToUTF8String(event.fileSHA256));
e->set_file_path(NSStringToUTF8String([event.filePath stringByDeletingLastPathComponent]));
e->set_file_name(NSStringToUTF8String([event.filePath lastPathComponent]));
e->set_executing_user(NSStringToUTF8String(event.executingUser));
e->set_execution_time([event.occurrenceDate timeIntervalSince1970]);
for (NSString *user in event.loggedInUsers) {
e->add_logged_in_users(NSStringToUTF8String(user));
}
for (NSString *session in event.currentSessions) {
e->add_current_sessions(NSStringToUTF8String(session));
}
switch (event.decision) {
case SNTEventStateAllowUnknown: e->set_decision(::pbv1::ALLOW_UNKNOWN); break;
case SNTEventStateAllowBinary: e->set_decision(::pbv1::ALLOW_BINARY); break;
case SNTEventStateAllowCertificate: e->set_decision(::pbv1::ALLOW_CERTIFICATE); break;
case SNTEventStateAllowScope: e->set_decision(::pbv1::ALLOW_SCOPE); break;
case SNTEventStateAllowTeamID: e->set_decision(::pbv1::ALLOW_TEAMID); break;
case SNTEventStateAllowSigningID: e->set_decision(::pbv1::ALLOW_SIGNINGID); break;
case SNTEventStateAllowCDHash: e->set_decision(::pbv1::ALLOW_CDHASH); break;
case SNTEventStateBlockUnknown: e->set_decision(::pbv1::BLOCK_UNKNOWN); break;
case SNTEventStateBlockBinary: e->set_decision(::pbv1::BLOCK_BINARY); break;
case SNTEventStateBlockCertificate: e->set_decision(::pbv1::BLOCK_CERTIFICATE); break;
case SNTEventStateBlockScope: e->set_decision(::pbv1::BLOCK_SCOPE); break;
case SNTEventStateBlockTeamID: e->set_decision(::pbv1::BLOCK_TEAMID); break;
case SNTEventStateBlockSigningID: e->set_decision(::pbv1::BLOCK_SIGNINGID); break;
case SNTEventStateBlockCDHash: e->set_decision(::pbv1::BLOCK_CDHASH); break;
case SNTEventStateBundleBinary:
e->set_decision(::pbv1::BUNDLE_BINARY);
e->clear_execution_time();
break;
default: e->set_decision(::pbv1::DECISION_UNKNOWN);
}
e->set_file_bundle_id(NSStringToUTF8String(event.fileBundleID));
e->set_file_bundle_path(NSStringToUTF8String(event.fileBundlePath));
e->set_file_bundle_executable_rel_path(NSStringToUTF8String(event.fileBundleExecutableRelPath));
e->set_file_bundle_name(NSStringToUTF8String(event.fileBundleName));
e->set_file_bundle_version(NSStringToUTF8String(event.fileBundleVersion));
e->set_file_bundle_version_string(NSStringToUTF8String(event.fileBundleVersionString));
e->set_file_bundle_hash(NSStringToUTF8String(event.fileBundleHash));
e->set_file_bundle_hash_millis([event.fileBundleHashMilliseconds longLongValue]);
e->set_file_bundle_binary_count([event.fileBundleBinaryCount longLongValue]);
e->set_pid([event.pid unsignedIntValue]);
e->set_ppid([event.ppid unsignedIntValue]);
e->set_parent_name(NSStringToUTF8String(event.parentName));
e->set_quarantine_data_url(NSStringToUTF8String(event.quarantineDataURL));
e->set_quarantine_referer_url(NSStringToUTF8String(event.quarantineRefererURL));
e->set_quarantine_timestamp([event.quarantineTimestamp timeIntervalSince1970]);
e->set_quarantine_agent_bundle_id(NSStringToUTF8String(event.quarantineAgentBundleID));
e->set_team_id(NSStringToUTF8String(event.teamID));
e->set_signing_id(NSStringToUTF8String(event.signingID));
e->set_cdhash(NSStringToUTF8String(event.cdhash));
for (MOLCertificate *cert in event.signingChain) {
::pbv1::Certificate *c = e->add_signing_chain();
c->set_sha256(NSStringToUTF8String(cert.SHA256));
c->set_cn(NSStringToUTF8String(cert.commonName));
c->set_org(NSStringToUTF8String(cert.orgName));
c->set_ou(NSStringToUTF8String(cert.orgUnit));
c->set_valid_from([cert.validFrom timeIntervalSince1970]);
c->set_valid_until([cert.validUntil timeIntervalSince1970]);
}
return *e;
}
@end

View File

@@ -14,9 +14,17 @@
#import "Source/common/SNTLogging.h"
#ifdef __cplusplus
extern "C" {
#endif
void logSyncMessage(LogLevel level, NSString *format, ...)
__attribute__((format(__NSString__, 2, 3)));
#ifdef __cplusplus
}
#endif
///
/// Send logs to the standard pipeline AND to any active sync listeners.
/// Intended for use by the syncservice to send logs back to santactl instances.

View File

@@ -20,6 +20,10 @@
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santasyncservice/SNTSyncState.h"
#include <google/protobuf/arena.h>
#include "Source/santasyncservice/syncv1.pb.h"
namespace pbv1 = ::santa::sync::v1;
@implementation SNTSyncPostflight
- (NSURL *)stageURL {
@@ -28,10 +32,13 @@
}
- (BOOL)sync {
[self performRequest:[self requestWithDictionary:@{
kPostflightRulesReceived : @(self.syncState.rulesReceived),
kPostflightRulesProcessed : @(self.syncState.rulesProcessed),
}]];
google::protobuf::Arena arena;
auto req = google::protobuf::Arena::Create<::pbv1::PostflightRequest>(&arena);
req->set_rules_received(self.syncState.rulesReceived);
req->set_rules_processed(self.syncState.rulesProcessed);
::pbv1::PostflightResponse response;
[self performRequest:[self requestWithMessage:req] intoMessage:&response timeout:30];
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];

View File

@@ -13,6 +13,7 @@
/// limitations under the License.
#import "Source/santasyncservice/SNTSyncPreflight.h"
#include "Source/common/SNTCommonEnums.h"
#import <MOLXPCConnection/MOLXPCConnection.h>
@@ -21,17 +22,16 @@
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTSystemInfo.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/common/String.h"
#import "Source/santasyncservice/SNTSyncLogging.h"
#import "Source/santasyncservice/SNTSyncState.h"
// Return the given value or nil if not of the expected given class
static id EnsureType(id val, Class c) {
if ([val isKindOfClass:c]) {
return val;
} else {
return nil;
}
}
#include <google/protobuf/arena.h>
#include "Source/santasyncservice/syncv1.pb.h"
namespace pbv1 = ::santa::sync::v1;
using santa::common::NSStringToUTF8String;
using santa::common::StringToNSString;
/*
@@ -74,34 +74,37 @@ The following table expands upon the above logic to list most of the permutation
}
- (BOOL)sync {
NSMutableDictionary *requestDict = [NSMutableDictionary dictionary];
requestDict[kSerialNumber] = [SNTSystemInfo serialNumber];
requestDict[kHostname] = [SNTSystemInfo longHostname];
requestDict[kOSVer] = [SNTSystemInfo osVersion];
requestDict[kOSBuild] = [SNTSystemInfo osBuild];
requestDict[kModelIdentifier] = [SNTSystemInfo modelIdentifier];
requestDict[kSantaVer] = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
requestDict[kPrimaryUser] = self.syncState.machineOwner;
google::protobuf::Arena arena;
auto req = google::protobuf::Arena::Create<::pbv1::PreflightRequest>(&arena);
req->set_serial_number(NSStringToUTF8String([SNTSystemInfo serialNumber]));
req->set_hostname(NSStringToUTF8String([SNTSystemInfo longHostname]));
req->set_os_version(NSStringToUTF8String([SNTSystemInfo osVersion]));
req->set_os_build(NSStringToUTF8String([SNTSystemInfo osBuild]));
req->set_model_identifier(NSStringToUTF8String([SNTSystemInfo modelIdentifier]));
req->set_santa_version(
NSStringToUTF8String([[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]));
req->set_primary_user(NSStringToUTF8String(self.syncState.machineOwner));
if (self.syncState.pushNotificationsToken) {
requestDict[kFCMToken] = self.syncState.pushNotificationsToken;
req->set_push_notification_token(NSStringToUTF8String(self.syncState.pushNotificationsToken));
}
id<SNTDaemonControlXPC> rop = [self.daemonConn synchronousRemoteObjectProxy];
[rop databaseRuleCounts:^(struct RuleCounts counts) {
requestDict[kBinaryRuleCount] = @(counts.binary);
requestDict[kCertificateRuleCount] = @(counts.certificate);
requestDict[kCompilerRuleCount] = @(counts.compiler);
requestDict[kTransitiveRuleCount] = @(counts.transitive);
requestDict[kTeamIDRuleCount] = @(counts.teamID);
requestDict[kSigningIDRuleCount] = @(counts.signingID);
requestDict[kCDHashRuleCount] = @(counts.cdhash);
req->set_binary_rule_count(uint32(counts.binary));
req->set_certificate_rule_count(uint32(counts.certificate));
req->set_compiler_rule_count(uint32(counts.compiler));
req->set_transitive_rule_count(uint32(counts.transitive));
req->set_teamid_rule_count(uint32(counts.teamID));
req->set_signingid_rule_count(uint32(counts.signingID));
req->set_cdhash_rule_count(uint32(counts.cdhash));
}];
[rop clientMode:^(SNTClientMode cm) {
switch (cm) {
case SNTClientModeMonitor: requestDict[kClientMode] = kClientModeMonitor; break;
case SNTClientModeLockdown: requestDict[kClientMode] = kClientModeLockdown; break;
case SNTClientModeMonitor: req->set_client_mode(::pbv1::MONITOR); break;
case SNTClientModeLockdown: req->set_client_mode(::pbv1::LOCKDOWN); break;
default: break;
}
}];
@@ -115,59 +118,72 @@ The following table expands upon the above logic to list most of the permutation
if (requestSyncType == SNTSyncTypeClean || requestSyncType == SNTSyncTypeCleanAll) {
SLOGD(@"%@ sync requested by user",
(requestSyncType == SNTSyncTypeCleanAll) ? @"Clean All" : @"Clean");
requestDict[kRequestCleanSync] = @YES;
req->set_request_clean_sync(true);
}
NSURLRequest *req = [self requestWithDictionary:requestDict];
NSDictionary *resp = [self performRequest:req];
::pbv1::PreflightResponse resp;
NSError *err = [self performRequest:[self requestWithMessage:req] intoMessage:&resp timeout:30];
if (!resp) return NO;
if (err) {
SLOGE(@"Failed preflight request: %@", err);
return NO;
}
self.syncState.enableBundles = EnsureType(resp[kEnableBundles], [NSNumber class])
?: EnsureType(resp[kEnableBundlesDeprecated], [NSNumber class]);
self.syncState.enableTransitiveRules = EnsureType(resp[kEnableTransitiveRules], [NSNumber class])
?: EnsureType(resp[kEnableTransitiveRulesDeprecated], [NSNumber class])
?: EnsureType(resp[kEnableTransitiveRulesSuperDeprecated], [NSNumber class]);
self.syncState.enableAllEventUpload = EnsureType(resp[kEnableAllEventUpload], [NSNumber class]);
self.syncState.disableUnknownEventUpload =
EnsureType(resp[kDisableUnknownEventUpload], [NSNumber class]);
self.syncState.eventBatchSize =
[EnsureType(resp[kBatchSize], [NSNumber class]) unsignedIntegerValue] ?: kDefaultEventBatchSize;
self.syncState.enableBundles = @(resp.enable_bundles() || resp.deprecated_bundles_enabled());
self.syncState.enableTransitiveRules =
@(resp.enable_transitive_rules() || resp.deprecated_enabled_transitive_whitelisting() ||
resp.deprecated_transitive_whitelisting_enabled());
self.syncState.enableAllEventUpload = @(resp.enable_all_event_upload());
self.syncState.disableUnknownEventUpload = @(resp.disable_unknown_event_upload());
self.syncState.eventBatchSize = resp.batch_size();
// Don't let these go too low
NSUInteger value =
[EnsureType(resp[kFCMFullSyncInterval], [NSNumber class]) unsignedIntegerValue];
uint64_t value = resp.push_notification_full_sync_interval_seconds()
?: resp.deprecated_fcm_full_sync_interval_seconds();
self.syncState.pushNotificationsFullSyncInterval =
(value < kDefaultFullSyncInterval) ? kDefaultPushNotificationsFullSyncInterval : value;
value = [EnsureType(resp[kFCMGlobalRuleSyncDeadline], [NSNumber class]) unsignedIntegerValue];
value = resp.push_notification_global_rule_sync_deadline_seconds()
?: resp.deprecated_fcm_global_rule_sync_deadline_seconds();
self.syncState.pushNotificationsGlobalRuleSyncDeadline =
(value < 60) ? kDefaultPushNotificationsGlobalRuleSyncDeadline : value;
(value < kDefaultPushNotificationsGlobalRuleSyncDeadline)
? kDefaultPushNotificationsGlobalRuleSyncDeadline
: value;
// Check if our sync interval has changed
value = [EnsureType(resp[kFullSyncInterval], [NSNumber class]) unsignedIntegerValue];
value = resp.full_sync_interval_seconds();
self.syncState.fullSyncInterval = (value < 60) ? kDefaultFullSyncInterval : value;
if ([resp[kClientMode] isEqual:kClientModeMonitor]) {
self.syncState.clientMode = SNTClientModeMonitor;
} else if ([resp[kClientMode] isEqual:kClientModeLockdown]) {
self.syncState.clientMode = SNTClientModeLockdown;
switch (resp.client_mode()) {
case ::pbv1::MONITOR: self.syncState.clientMode = SNTClientModeMonitor; break;
case ::pbv1::LOCKDOWN: self.syncState.clientMode = SNTClientModeLockdown; break;
default: break;
}
self.syncState.allowlistRegex =
EnsureType(resp[kAllowedPathRegex], [NSString class])
?: EnsureType(resp[kAllowedPathRegexDeprecated], [NSString class]);
if (resp.has_allowed_path_regex()) {
self.syncState.allowlistRegex = StringToNSString(resp.allowed_path_regex());
} else if (resp.has_deprecated_whitelist_regex()) {
self.syncState.allowlistRegex = StringToNSString(resp.deprecated_whitelist_regex());
}
self.syncState.blocklistRegex =
EnsureType(resp[kBlockedPathRegex], [NSString class])
?: EnsureType(resp[kBlockedPathRegexDeprecated], [NSString class]);
if (resp.has_blocked_path_regex()) {
self.syncState.blocklistRegex = StringToNSString(resp.blocked_path_regex());
} else if (resp.has_deprecated_blacklist_regex()) {
self.syncState.blocklistRegex = StringToNSString(resp.deprecated_blacklist_regex());
}
self.syncState.blockUSBMount = EnsureType(resp[kBlockUSBMount], [NSNumber class]);
self.syncState.remountUSBMode = EnsureType(resp[kRemountUSBMode], [NSArray class]);
if (resp.has_block_usb_mount()) {
self.syncState.blockUSBMount = @(resp.block_usb_mount());
}
self.syncState.overrideFileAccessAction =
EnsureType(resp[kOverrideFileAccessAction], [NSString class]);
self.syncState.remountUSBMode = [NSMutableArray array];
for (const std::string &mode : resp.remount_usb_mode()) {
[(NSMutableArray *)self.syncState.remountUSBMode addObject:StringToNSString(mode)];
}
if (resp.has_override_file_access_action()) {
self.syncState.overrideFileAccessAction = StringToNSString(resp.override_file_access_action());
}
// Default sync type is SNTSyncTypeNormal
//
@@ -187,24 +203,26 @@ The following table expands upon the above logic to list most of the permutation
// If kSyncType response key exists, it overrides the kCleanSyncDeprecated value
// First check if the kSyncType reponse key exists. If so, it takes precedence
// over the kCleanSyncDeprecated key.
NSString *responseSyncType = [EnsureType(resp[kSyncType], [NSString class]) lowercaseString];
if (responseSyncType) {
if ([responseSyncType isEqualToString:@"clean"]) {
// If the client wants to Clean All, this takes precedence. The server
// cannot override the client wanting to remove all rules.
std::string responseSyncType = resp.sync_type();
if (!responseSyncType.empty()) {
// If the client wants to Clean All, this takes precedence. The server
// cannot override the client wanting to remove all rules.
if (responseSyncType == "clean") {
SLOGD(@"Clean sync requested by server");
if (requestSyncType == SNTSyncTypeCleanAll) {
self.syncState.syncType = SNTSyncTypeCleanAll;
} else {
self.syncState.syncType = SNTSyncTypeClean;
}
} else if ([responseSyncType isEqualToString:@"clean_all"]) {
} else if (responseSyncType == "clean_all") {
self.syncState.syncType = SNTSyncTypeCleanAll;
}
} else if ([EnsureType(resp[kCleanSyncDeprecated], [NSNumber class]) boolValue]) {
} else if (resp.deprecated_clean_sync()) {
// If the deprecated key is set, the type of sync clean performed should be
// the type that was requested. This must be set appropriately so that it
// can be propagated during the Rule Download stage so SNTRuleTable knows
// which rules to delete.
SLOGD(@"Clean sync requested by server");
if (requestSyncType == SNTSyncTypeCleanAll) {
self.syncState.syncType = SNTSyncTypeCleanAll;
} else {
@@ -212,10 +230,6 @@ The following table expands upon the above logic to list most of the permutation
}
}
if (self.syncState.syncType != SNTSyncTypeNormal) {
SLOGD(@"Clean sync requested by server");
}
return YES;
}

View File

@@ -20,10 +20,17 @@
#import "Source/common/SNTRule.h"
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/common/String.h"
#import "Source/santasyncservice/SNTPushNotificationsTracker.h"
#import "Source/santasyncservice/SNTSyncLogging.h"
#import "Source/santasyncservice/SNTSyncState.h"
#include <google/protobuf/arena.h>
#include "Source/santasyncservice/syncv1.pb.h"
namespace pbv1 = ::santa::sync::v1;
using santa::common::StringToNSString;
SNTRuleCleanup SyncTypeToRuleCleanup(SNTSyncType syncType) {
switch (syncType) {
case SNTSyncTypeNormal: return SNTRuleCleanupNone;
@@ -89,39 +96,84 @@ SNTRuleCleanup SyncTypeToRuleCleanup(SNTSyncType syncType) {
// Note that rules from the server are filtered. We only keep those whose rule_type
// is either BINARY or CERTIFICATE. PACKAGE rules are dropped.
- (NSArray<SNTRule *> *)downloadNewRulesFromServer {
google::protobuf::Arena arena;
self.syncState.rulesReceived = 0;
NSMutableArray<SNTRule *> *newRules = [NSMutableArray array];
NSString *cursor = nil;
do {
NSDictionary *requestDict = cursor ? @{kCursor : cursor} : @{};
NSDictionary *response = [self performRequest:[self requestWithDictionary:requestDict]];
std::string cursor;
if (![response isKindOfClass:[NSDictionary class]] ||
![response[kRules] isKindOfClass:[NSArray class]]) {
do {
auto req = google::protobuf::Arena::Create<::pbv1::RuleDownloadRequest>(&arena);
if (!cursor.empty()) {
req->set_cursor(cursor);
}
::pbv1::RuleDownloadResponse response;
NSError *err = [self performRequest:[self requestWithMessage:req]
intoMessage:&response
timeout:30];
if (err) {
SLOGE(@"Error downloading rules: %@", err);
return nil;
}
NSArray<NSDictionary *> *rules = response[kRules];
for (NSDictionary *ruleDict in rules) {
SNTRule *rule = [[SNTRule alloc] initWithDictionary:ruleDict];
if (!rule) {
SLOGD(@"Ignoring bad rule: %@", ruleDict);
for (const ::pbv1::Rule &rule : response.rules()) {
SNTRule *r = [self ruleFromProtoRule:rule];
if (!r) {
SLOGD(@"Ignoring bad rule: %s", rule.Utf8DebugString().c_str());
continue;
}
[self processBundleNotificationsForRule:rule fromDictionary:ruleDict];
[newRules addObject:rule];
[self processBundleNotificationsForRule:r fromProtoRule:&rule];
[newRules addObject:r];
}
SLOGI(@"Received %lu rules", (unsigned long)rules.count);
cursor = response[kCursor];
self.syncState.rulesReceived += rules.count;
} while (cursor);
cursor = response.cursor();
SLOGI(@"Received %lu rules", (unsigned long)response.rules_size());
self.syncState.rulesReceived += response.rules_size();
} while (!cursor.empty());
self.syncState.rulesProcessed = newRules.count;
return newRules;
}
- (SNTRule *)ruleFromProtoRule:(::pbv1::Rule)rule {
SNTRule *r = [[SNTRule alloc] init];
r.identifier = StringToNSString(rule.identifier());
if (!r.identifier.length) r.identifier = StringToNSString(rule.deprecated_sha256());
SNTRuleState state;
switch (rule.policy()) {
case ::pbv1::ALLOWLIST: state = SNTRuleStateAllow; break;
case ::pbv1::ALLOWLIST_COMPILER: state = SNTRuleStateAllowCompiler; break;
case ::pbv1::BLOCKLIST: state = SNTRuleStateBlock; break;
case ::pbv1::SILENT_BLOCKLIST: state = SNTRuleStateSilentBlock; break;
case ::pbv1::REMOVE: state = SNTRuleStateRemove; break;
default: LOGE(@"Failed to process rule with unknown policy: %d", rule.policy()); return nil;
}
r.state = state;
SNTRuleType type;
switch (rule.rule_type()) {
case ::pbv1::BINARY: type = SNTRuleTypeBinary; break;
case ::pbv1::CERTIFICATE: type = SNTRuleTypeCertificate; break;
case ::pbv1::TEAMID: type = SNTRuleTypeTeamID; break;
case ::pbv1::SIGNINGID: type = SNTRuleTypeSigningID; break;
case ::pbv1::CDHASH: type = SNTRuleTypeCDHash; break;
default: LOGE(@"Failed to process rule with unknown type: %d", rule.rule_type()); return nil;
}
r.type = type;
const std::string &custom_msg = rule.custom_msg();
if (!custom_msg.empty()) r.customMsg = StringToNSString(custom_msg);
const std::string &custom_url = rule.custom_url();
if (!custom_url.empty()) r.customURL = StringToNSString(custom_url);
return r;
}
// Send out push notifications for allowed bundles/binaries whose rule download was preceded by
// an associated announcing FCM message.
- (void)announceUnblockingRules:(NSArray<SNTRule *> *)newRules {
@@ -146,12 +198,13 @@ SNTRuleCleanup SyncTypeToRuleCleanup(SNTSyncType syncType) {
[tracker removeNotificationsForHashes:processed];
}
- (void)processBundleNotificationsForRule:(SNTRule *)rule fromDictionary:(NSDictionary *)dict {
- (void)processBundleNotificationsForRule:(SNTRule *)rule
fromProtoRule:(const ::pbv1::Rule *)protoRule {
// Check rule for extra notification related info.
if (rule.state == SNTRuleStateAllow || rule.state == SNTRuleStateAllowCompiler) {
// primaryHash is the bundle hash if there was a bundle hash included in the rule, otherwise
// it is simply the binary hash.
NSString *primaryHash = dict[kFileBundleHash];
NSString *primaryHash = StringToNSString(protoRule->file_bundle_hash());
if (primaryHash.length != 64) {
primaryHash = rule.identifier;
}
@@ -160,7 +213,7 @@ SNTRuleCleanup SyncTypeToRuleCleanup(SNTSyncType syncType) {
// number of rules associated with the primary hash that still need to be downloaded and added.
[[SNTPushNotificationsTracker tracker]
decrementPendingRulesForHash:primaryHash
totalRuleCount:dict[kFileBundleBinaryCount]];
totalRuleCount:@(protoRule->file_bundle_binary_count())];
}
}

View File

@@ -14,6 +14,10 @@
#import <Foundation/Foundation.h>
#ifdef __cplusplus
#include <google/protobuf/message.h>
#endif
@class SNTSyncState;
@class MOLXPCConnection;
@@ -48,27 +52,28 @@
#pragma mark Internal Helpers
#ifdef __cplusplus
/**
Creates an NSMutableURLRequest pointing at the URL for this stage and containing the JSON-encoded
data passed in as a dictionary.
data passed in as a protocol buffer message.
@param dictionary The values to POST to the server.
*/
- (nullable NSMutableURLRequest *)requestWithDictionary:(nullable NSDictionary *)dictionary;
- (nullable NSMutableURLRequest *)requestWithMessage:(nullable google::protobuf::Message *)message;
/**
Perform the passed in request and attempt to parse the response as JSON into a dictionary.
Perform the passed in request and attempt to parse the response as JSON into the provided protobuf
Message.
@param request The request to perform
@param timeout The number of seconds to allow the request to run before timing out.
@param message The message to parse the response into
@Param timeout The number of seconds to allow the request to run before timing out.
@return A populated dictionary if the response data was JSON, an empty dictionary if not and nil
if the request failed for any reason.
@return An error if performing the request failed.
*/
- (nullable NSDictionary *)performRequest:(nonnull NSURLRequest *)request
timeout:(NSTimeInterval)timeout;
/** Convenience version of performRequest:timeout: using a 30s timeout. */
- (nullable NSDictionary *)performRequest:(nonnull NSURLRequest *)request;
- (nullable NSError *)performRequest:(nonnull NSURLRequest *)request
intoMessage:(nullable google::protobuf::Message *)message
timeout:(NSTimeInterval)timeout;
#endif
@end

View File

@@ -21,10 +21,15 @@
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/common/String.h"
#import "Source/santasyncservice/NSData+Zlib.h"
#import "Source/santasyncservice/SNTSyncLogging.h"
#import "Source/santasyncservice/SNTSyncState.h"
#include <google/protobuf/json/json.h>
using santa::common::NSStringToUTF8String;
@interface SNTSyncStage ()
@property(readwrite) NSURLSession *urlSession;
@@ -55,17 +60,27 @@
__builtin_unreachable();
}
- (NSMutableURLRequest *)requestWithDictionary:(NSDictionary *)dictionary {
- (NSMutableURLRequest *)requestWithMessage:(google::protobuf::Message *)message {
NSData *requestBody = [NSData data];
if (dictionary) {
NSError *error;
requestBody = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&error];
if (error) {
SLOGD(@"Failed to encode JSON request: %@", error);
if (message) {
google::protobuf::json::PrintOptions options{
.always_print_enums_as_ints = false,
.preserve_proto_field_names = true,
};
std::string json;
absl::Status status = google::protobuf::json::MessageToJsonString(*message, &json, options);
if (!status.ok()) {
SLOGE(@"Failed to convert protobuf to JSON: %s", status.ToString().c_str());
return nil;
}
}
requestBody = [NSData dataWithBytes:json.data() length:json.size()];
}
return [self requestWithData:requestBody];
}
- (NSMutableURLRequest *)requestWithData:(NSData *)requestBody {
NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:[self stageURL]];
[req setHTTPMethod:@"POST"];
[req setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
@@ -129,9 +144,7 @@
}];
}
// Returns nil when there is a server connection issue. For other errors, such as
// an empty response or an unparseable response, an empty dictionary is returned.
- (NSDictionary *)performRequest:(NSURLRequest *)request timeout:(NSTimeInterval)timeout {
- (NSData *)dataFromRequest:(NSURLRequest *)request timeout:(NSTimeInterval)timeout {
NSHTTPURLResponse *response;
NSError *error;
NSData *data;
@@ -141,7 +154,7 @@
if (attempt >= 2) {
// Exponentially back off with larger and larger delays.
int exponentialBackoffMultiplier = 2; // E.g. 2^2 = 4, 2^3 = 8, 2^4 = 16...
struct timespec ts = {.tv_sec = pow(exponentialBackoffMultiplier, attempt)};
struct timespec ts = {.tv_sec = __darwin_time_t(pow(exponentialBackoffMultiplier, attempt))};
nanosleep(&ts, NULL);
}
@@ -177,19 +190,28 @@
LOGE(@"HTTP Response: %ld %@", code, errStr);
return nil;
}
if (data.length == 0) return @{};
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:[self stripXssi:data]
options:0
error:&error];
if (error) SLOGD(@"Failed to decode JSON response: %@", error);
return dict ?: @{};
return data;
}
- (NSDictionary *)performRequest:(NSURLRequest *)request {
return [self performRequest:request timeout:30];
- (NSError *)performRequest:(NSURLRequest *)request
intoMessage:(google::protobuf::Message *)message
timeout:(NSTimeInterval)timeout {
NSData *data = [self dataFromRequest:request timeout:timeout];
if (data.length == 0) return nil;
google::protobuf::json::ParseOptions options{
.ignore_unknown_fields = true,
};
NSString *jsonData = [[NSString alloc] initWithData:[self stripXssi:data]
encoding:NSUTF8StringEncoding];
absl::Status status =
google::protobuf::json::JsonStringToMessage(NSStringToUTF8String(jsonData), message, options);
if (!status.ok()) {
SLOGE(@"Failed to parse response JSON into message: %s", status.ToString().c_str());
return nil;
}
return nil;
}
#pragma mark Internal Helpers
@@ -236,10 +258,12 @@
- (NSData *)stripXssi:(NSData *)data {
static const char xssiOne[5] = {')', ']', '}', '\'', '\n'};
static const char xssiTwo[3] = {']', ')', '}'};
if (data.length >= sizeof(xssiOne) && strncmp(data.bytes, xssiOne, sizeof(xssiOne)) == 0) {
if (data.length >= sizeof(xssiOne) &&
strncmp((const char *)data.bytes, xssiOne, sizeof(xssiOne)) == 0) {
return [data subdataWithRange:NSMakeRange(sizeof(xssiOne), data.length - sizeof(xssiOne))];
}
if (data.length >= sizeof(xssiTwo) && strncmp(data.bytes, xssiTwo, sizeof(xssiTwo)) == 0) {
if (data.length >= sizeof(xssiTwo) &&
strncmp((const char *)data.bytes, xssiTwo, sizeof(xssiTwo)) == 0) {
return [data subdataWithRange:NSMakeRange(sizeof(xssiTwo), data.length - sizeof(xssiTwo))];
}
return data;

View File

@@ -217,7 +217,7 @@
SNTSyncStage *sut = [[SNTSyncStage alloc] initWithState:self.syncState];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:u1];
XCTAssertTrue([sut performRequest:req]);
XCTAssertNil([sut performRequest:req intoMessage:NULL timeout:5]);
XCTAssertEqualObjects(self.syncState.xsrfToken, @"my-xsrf-token");
}
@@ -260,7 +260,7 @@
SNTSyncStage *sut = [[SNTSyncStage alloc] initWithState:self.syncState];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:u1];
XCTAssertTrue([sut performRequest:req]);
XCTAssertNil([sut performRequest:req intoMessage:NULL timeout:5]);
XCTAssertEqualObjects(self.syncState.xsrfToken, @"my-xsrf-token");
}
@@ -346,13 +346,13 @@
SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState];
struct RuleCounts ruleCounts = {
.cdhash = 11,
.binary = 5,
.certificate = 8,
.compiler = 2,
.transitive = 19,
.teamID = 3,
.signingID = 123,
.cdhash = 11,
};
OCMStub([self.daemonConnRop

View File

@@ -0,0 +1,268 @@
syntax = "proto3";
package santa.sync.v1;
// Note: Many of the enums in this proto file do not conform to protocol buffer
// best practices. The reason for this is that this proto was developed to be
// backward-compatible with an existing JSON API and unlike field names, enum
// values cannot have their JSON representation overridden.
// SantaSync is service used to distribute configuration settings and rules to Santa.
service SantaSync {
// Preflight is used to send current configuration data to the server and to
// retrieve new configuration values from the server.
rpc Preflight(PreflightRequest) returns (PreflightResponse) {}
// EventUpload sends details about blocked (or would-be-blocked) executions to
// the server.
rpc EventUpload(EventUploadRequest) returns (EventUploadResponse) {}
// RuleDownload retrieves new rules from the server.
rpc RuleDownload(RuleDownloadRequest) returns (RuleDownloadResponse) {}
// Postflight sends status data back to the server after applying any new
// configuration received in the previous stages.
rpc Postflight(PostflightRequest) returns (PostflightResponse) {}
}
// ClientMode represents the operating mode for an agent.
enum ClientMode {
MONITOR = 0;
LOCKDOWN = 1;
}
message PreflightRequest {
string serial_number = 1 [json_name="serial_num"];
string hostname = 2;
string os_version = 3;
string os_build = 4;
string model_identifier = 5;
string santa_version = 6;
string primary_user = 7;
string push_notification_token = 8;
ClientMode client_mode = 9;
bool request_clean_sync = 10;
uint32 binary_rule_count = 11;
uint32 certificate_rule_count = 12;
uint32 compiler_rule_count = 13;
uint32 transitive_rule_count = 14;
uint32 teamid_rule_count = 15;
uint32 signingid_rule_count = 16;
uint32 cdhash_rule_count = 17;
}
message PreflightResponse {
ClientMode client_mode = 1;
// Possible values are "normal" (default if unspecified), "clean", or "clean_all".
// "normal" is a standard progressive sync
// "clean" deletes all previously received rules before applying the newly received rules.
// "clean_all" deletes all rules, including transitive rules, before applying the newly received rules.
// This should be an enum but we won't match lowercase values if specified as an enum unless the
// enum values are also lowercase (which is odd), and this is the behavior from the pre-proto protocol.
string sync_type = 2;
// Controls how many events Santa should upload in a single EventUpload request.
// If the server doesn't specify, the default is 50.
uint32 batch_size = 3;
// Enable bundle hashing and bundle rules.
bool enable_bundles = 4;
// Enable transitive (ALLOWLIST_COMPILER) rules.
// Without this enabled, any received ALLOWLIST_COMPILER rules will be treated as ALLOWLIST.
bool enable_transitive_rules = 5;
// Ordinarily, Santa will only upload events about executions that are denied or would be denied if the machine
// were in LOCKDOWN mode. With this enabled, Santa will upload details about all events.
bool enable_all_event_upload = 6;
// Ordinarily, Santa will only upload events about executions that are denied or would be denied if the machine
// were in LOCKDOWN mode. With this enabled, Santa will NOT upload events for binaries that would have been blocked in LOCKDOWN.
bool disable_unknown_event_upload = 7;
// Specifies the time interval in seconds between full syncs. Defaults to 600 (10 minutes). Cannot be set lower than 60.
uint64 full_sync_interval_seconds = 8 [json_name="full_sync_interval"];
// When push notifications are enabled, this overrides the full_sync_interval above. It is expected that Santa will not
// need to perform a full sync as frequently when push notifications are working. Defaults to 14400 (6 hours).
uint64 push_notification_full_sync_interval_seconds = 9 [json_name="push_notification_full_sync_interval"];
// The maximum number of seconds Santa can wait before triggering a rule sync after receiving a "global rule sync" notification.
// As these notifications cause every Santa client to try and sync, we add a random delay to each client to try and spread the
// load out on the sync server. This defaults to 600 (10 minutes).
uint64 push_notification_global_rule_sync_deadline_seconds = 10 [json_name="push_notification_global_rule_sync_deadline"];
// These two regexes are used to allow/block executions whose path matches. The provided regex must conform to ICU format.
// While this feature can be useful, its use should be very carefully considered as it is much riskier than real rules.
optional string allowed_path_regex = 11;
optional string blocked_path_regex = 12;
// Enable USB mount blocking
optional bool block_usb_mount = 13;
// If set, if a mount of a USB device happens and the mount flags match, the mount will be allowed.
// If the flags do not match, Santa will deny the mount but then remount with the provided flags.
repeated string remount_usb_mode = 14;
// Overrides the File Access Authorization (FAA) policy to change the performed action.
// Allowed values:
// `disable`: No action will be taken
// `auditonly`: Actions that would be denied are logged but allowed
// `none`: The policy will be applied as written
optional string override_file_access_action = 15;
// These fields are deprecated forms of other fields and exist here solely for backwards compatibility
optional bool deprecated_enabled_transitive_whitelisting = 1000 [json_name="enabled_transitive_whitelisting", deprecated=true];
optional bool deprecated_transitive_whitelisting_enabled = 1001 [json_name="transitive_whitelisting_enabled", deprecated=true];
optional bool deprecated_bundles_enabled = 1002 [json_name="bundles_enabled", deprecated=true];
optional uint64 deprecated_fcm_full_sync_interval_seconds = 1003 [json_name="fcm_full_sync_interval", deprecated=true];
optional uint64 deprecated_fcm_global_rule_sync_deadline_seconds = 1004 [json_name="fcm_global_rule_sync_deadline", deprecated=true];
optional string deprecated_whitelist_regex = 1005 [json_name="whitelist_regex", deprecated=true];
optional string deprecated_blacklist_regex = 1006 [json_name="blacklist_regex", deprecated=true];
// Deprecated but still supported key that acts like sync_type was set to "clean" unless
// the client had requested a clean sync, in which case it acts like "clean_all"
optional bool deprecated_clean_sync = 1007 [json_name="clean_sync", deprecated=true];
}
enum Decision {
DECISION_UNKNOWN = 0;
ALLOW_UNKNOWN = 1;
ALLOW_BINARY = 2;
ALLOW_CERTIFICATE = 3;
ALLOW_SCOPE = 4;
ALLOW_TEAMID = 5;
ALLOW_SIGNINGID = 6;
ALLOW_CDHASH = 7;
BLOCK_UNKNOWN = 8;
BLOCK_BINARY = 9;
BLOCK_CERTIFICATE = 10;
BLOCK_SCOPE = 11;
BLOCK_TEAMID = 12;
BLOCK_SIGNINGID = 13;
BLOCK_CDHASH = 14;
BUNDLE_BINARY = 15;
}
message Certificate {
string sha256 = 1;
string cn = 2;
string org = 3;
string ou = 4;
uint32 valid_from = 5;
uint32 valid_until = 6;
}
message Event {
string file_sha256 = 1;
string file_path = 2;
string file_name = 3;
string executing_user = 4;
double execution_time = 5;
repeated string logged_in_users = 6;
repeated string current_sessions = 7;
Decision decision = 8;
string file_bundle_id = 9;
string file_bundle_path = 10;
string file_bundle_executable_rel_path = 11;
string file_bundle_name = 12;
string file_bundle_version = 13;
string file_bundle_version_string = 14;
string file_bundle_hash = 15;
uint64 file_bundle_hash_millis = 16;
uint64 file_bundle_binary_count = 17;
// pid_t is an int32
int32 pid = 18;
int32 ppid = 19;
string parent_name = 20;
string team_id = 21;
string signing_id = 22;
string cdhash = 23;
string quarantine_data_url = 24;
string quarantine_referer_url = 25;
// Seconds since UNIX epoch. This field would ideally be an int64 but the protobuf library
// encodes that as a string, unlike NSJSONSerialization
uint32 quarantine_timestamp = 26;
string quarantine_agent_bundle_id = 27;
repeated Certificate signing_chain = 28;
}
message EventUploadRequest {
repeated Event events = 1;
}
message EventUploadResponse {
// A list of SHA-256's of bundle binaries that need to be uploaded.
repeated string event_upload_bundle_binaries = 1;
}
enum Policy {
option allow_alias = true;
POLICY_UNKNOWN = 0;
ALLOWLIST = 1;
ALLOWLIST_COMPILER = 2;
BLOCKLIST = 3;
SILENT_BLOCKLIST = 4;
REMOVE = 5;
// These enum values are deprecated and remain here for backward compatibility.
WHITELIST = 1;
WHITELIST_COMPILER = 2;
BLACKLIST = 3;
SILENT_BLACKLIST = 4;
}
enum RuleType {
RULETYPE_UNKNOWN = 0;
BINARY = 1;
CERTIFICATE = 2;
TEAMID = 3;
SIGNINGID = 4;
CDHASH = 5;
}
message Rule {
string identifier = 1;
Policy policy = 2;
RuleType rule_type = 3;
// For BLOCK_* rules, this will override the default block message shown to users.
string custom_msg = 4;
// For BLOCK_* rules, this will override the URL used by the "Open" button in the UI.
// The same format values used by the EventDetailURL configuration value can be used here.
// See: https://santa.dev/deployment/configuration#eventdetailurl
string custom_url = 5;
// These two fields are used for bundle binaries.
string file_bundle_hash = 6;
uint32 file_bundle_binary_count = 7;
string deprecated_sha256 = 1000 [json_name="sha256", deprecated=true]; // Use identifier instead
}
message RuleDownloadRequest {
string cursor = 1;
}
message RuleDownloadResponse {
repeated Rule rules = 1;
string cursor = 2;
}
message PostflightRequest {
uint64 rules_received = 1;
uint64 rules_processed = 2;
}
message PostflightResponse { }