Compare commits

...

18 Commits
2024.6 ... main

Author SHA1 Message Date
Günther Noack
261425aa64 docs: Colorize callout and make the link work (#1445) 2025-02-05 14:23:36 +01:00
Günther Noack
c17c890356 docs: Add deprecation note (#1444) 2025-02-05 14:08:38 +01:00
Günther Noack
e4e1704495 Add deprecation message (#1443) 2025-02-05 13:54:29 +01:00
Tom Burgin
737525b746 kvo static rules (#1425) 2024-09-12 19:23:30 -04:00
Matt W
8199348091 Use runtime platform binary check for exec evals (#1424)
* Use runtime platform binary check for exec evals

* PR Feedback

* Remove parens to mitigate insane clang-formatting
2024-09-10 09:07:50 -04:00
Pete Markowsky
9f41fbb124 Fix: Change uint64 fields in syncv1.proto to uint32 for backwards compatibility (#1422)
Change the uint64 fields in the syncv1.proto to uint32 to ensure backwards compatibility.

This also updates the SNTSyncEventUpload code to use the uint32 values and updates sync protocol docs.
2024-09-08 15:46:30 -04:00
Matt W
ff0efe952b Use proper CanWrite method to safeguard TTY struct access (#1420) 2024-08-21 16:29:33 -04:00
Tom Burgin
c711129ac9 s/NSDictionary/NSBundle/ (#1418) 2024-08-15 12:36:01 -04:00
Russell Hancox
a56f6c5447 Project: Update rules_apple to 3.8.0 (#1417) 2024-08-13 14:20:19 -04:00
Russell Hancox
fadc9b505b sync: Drop rules_* fields in postflight to uint32 (#1415)
* sync: Drop rules_* fields in postflight to uint32

This lets the protobuf json serializer to send the values as ints (like NSJSONSerialization did) instead of strings. This will cause problems if someone has 4B rules but that's probably a sign of bigger problems
2024-08-12 15:40:01 -04:00
Tom Burgin
c7766d5993 length and count check (#1413) 2024-08-09 17:17:13 -04:00
Russell Hancox
341abf044b sync: Fix Content-Type logic bug, add test (#1412) 2024-08-08 17:43:53 -04:00
Tom Burgin
b1cf83a7e3 switch to CFBundleCopyInfoDictionaryInDirectory (#1411) 2024-08-08 16:30:33 -04:00
Russell Hancox
013b0b40af sync: Remove debug logging of request JSON. (#1410)
This was helpful while switching to the protobuf lib but due to the way SLOGD works it isn't limited to just debug invocations and is quite noisy
2024-08-08 14:06:52 -04:00
Russell Hancox
6093118ba1 sync: Improve logging when connection restored, avoid retries. (#1408)
* sync: Improve logging when connection restored, avoid retries.

Don't retry requests if the error is that the machine is offline
2024-07-31 13:29:25 -04:00
Russell Hancox
6719d4c32a sync: Upgrade from SCNetworkReachability -> nw_path_monitor (#1406) 2024-07-31 10:53:16 -04:00
Russell Hancox
1ce4756771 santad: Synchronize access to metric callback array (#1405) 2024-07-29 12:09:03 -04:00
Russell Hancox
9a7dcefb92 sync: Fix serial_num field name (#1404)
Disable the preserve_proto_field_names option when marshalling JSON requests as this prevents the json_name attribute on fields from working properly. Add that attribute to all fields so that they marshal as expected. Stop setting the always_print_enums_as_ints field as the value we're setting to is the default anyway.

Also add a test that preflight request data looks as expected.
2024-07-29 12:08:21 -04:00
26 changed files with 242 additions and 205 deletions

View File

@@ -4,7 +4,7 @@ bazel_dep(name = "apple_support", version = "1.15.1", repo_name = "build_bazel_a
bazel_dep(name = "abseil-cpp", version = "20240116.2", repo_name = "com_google_absl")
bazel_dep(name = "rules_python", version = "0.33.2")
bazel_dep(name = "rules_cc", version = "0.0.9")
bazel_dep(name = "rules_apple", version = "3.6.0", repo_name = "build_bazel_rules_apple")
bazel_dep(name = "rules_apple", version = "3.8.0", repo_name = "build_bazel_rules_apple")
bazel_dep(name = "rules_swift", version = "2.0.0-rc1", repo_name = "build_bazel_rules_swift")
bazel_dep(name = "rules_fuzzing", version = "0.5.2")
bazel_dep(name = "protobuf", version = "27.2", repo_name = "com_google_protobuf")

View File

@@ -1,5 +1,12 @@
# Santa
> [!NOTE]
> **As of 2025, Santa is no longer maintained by Google.** We encourage
> existing users to migrate to an actively maintained fork of Santa, such as
> https://github.com/northpolesec/santa.
---
[![license](https://img.shields.io/github/license/google/santa)](https://github.com/google/santa/blob/main/LICENSE)
[![CI](https://github.com/google/santa/actions/workflows/ci.yml/badge.svg)](https://github.com/google/santa/actions/workflows/ci.yml)
[![latest release](https://img.shields.io/github/v/release/google/santa.svg)](https://github.com/google/santa/releases/latest)

View File

@@ -368,7 +368,12 @@ static NSString *const kSyncTypeRequired = @"SyncTypeRequired";
}
+ (NSSet *)keyPathsForValuesAffectingStaticRules {
return [self configStateSet];
static NSSet *set;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
set = [NSSet setWithObject:NSStringFromSelector(@selector(cachedStaticRules))];
});
return set;
}
+ (NSSet *)keyPathsForValuesAffectingSyncBaseURL {

View File

@@ -423,8 +423,14 @@ extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE;
return self.infoDict;
}
d = self.bundle.infoDictionary;
if (d) {
// `-[NSBundle infoDictionary]` is heavily cached, changes to the Info.plist are not realized.
// Use `CFBundleCopyInfoDictionaryInDirectory` instead, which does not appear to cache.
NSString *bundlePath = [self bundlePath];
if (bundlePath.length) {
d = CFBridgingRelease(CFBundleCopyInfoDictionaryInDirectory(
(__bridge CFURLRef)[NSURL fileURLWithPath:bundlePath]));
}
if (d.count) {
self.infoDict = d;
return self.infoDict;
}

View File

@@ -605,10 +605,15 @@ NSString *SNTMetricMakeStringFromMetricType(SNTMetricType metricType) {
/** Export current state of the SNTMetricSet as an NSDictionary. */
- (NSDictionary *)export {
NSDictionary *exported = nil;
NSDictionary *exported;
NSArray *callbacks;
@synchronized(self) {
callbacks = [_callbacks mutableCopy];
}
// Invoke callbacks to ensure metrics are up to date.
for (void (^cb)(void) in _callbacks) {
for (void (^cb)(void) in callbacks) {
cb();
}

View File

@@ -54,4 +54,19 @@
///
+ (NSString *)modelIdentifier;
///
/// @return The Santa product version, e.g. 2024.6
///
+ (NSString *)santaProductVersion;
///
/// @return The Santa build version, e.g. 655965194
///
+ (NSString *)santaBuildVersion;
///
/// @return The full Santa versoin, e.g. 2024.6.655965194
///
+ (NSString *)santaFullVersion;
@end

View File

@@ -74,6 +74,21 @@
return @(model);
}
+ (NSString *)santaProductVersion {
NSDictionary *info_dict = [[NSBundle mainBundle] infoDictionary];
return info_dict[@"CFBundleShortVersionString"];
}
+ (NSString *)santaBuildVersion {
NSDictionary *info_dict = [[NSBundle mainBundle] infoDictionary];
return [[info_dict[@"CFBundleVersion"] componentsSeparatedByString:@"."] lastObject];
}
+ (NSString *)santaFullVersion {
NSDictionary *info_dict = [[NSBundle mainBundle] infoDictionary];
return info_dict[@"CFBundleVersion"];
}
#pragma mark - Internal
+ (NSDictionary *)_systemVersionDictionary {

View File

@@ -823,6 +823,7 @@ objc_library(
"//Source/common:SNTConfigurator",
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
"//Source/common:SNTSystemInfo",
"//Source/common:SNTXPCControlInterface",
"//Source/common:SystemResources",
],

View File

@@ -688,7 +688,7 @@ bool ShouldMessageTTY(const std::shared_ptr<WatchItemPolicy> &policy, const Mess
// Notify users on block decisions
if (ShouldNotifyUserDecision(policyDecision) &&
(!policy->silent || (!policy->silent_tty && msg->process->tty->path.length > 0))) {
(!policy->silent || (!policy->silent_tty && TTYWriter::CanWrite(msg->process)))) {
SNTCachedDecision *cd =
[self.decisionCache cachedDecisionForFile:msg->process->executable->stat];

View File

@@ -45,12 +45,6 @@
/// only guaranteed for the duration of the call to the block. Do not perform
/// any async processing without extending their lifetimes.
///
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
targetProcess:(nonnull const es_process_t *)targetProc
entitlementsFilterCallback:
(NSDictionary *_Nullable (^_Nonnull)(
const char *_Nullable teamID,
NSDictionary *_Nullable entitlements))entitlementsFilterCallback;
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
targetProcess:(nonnull const es_process_t *)targetProc
preCodesignCheckCallback:(void (^_Nullable)(void))preCodesignCheckCallback

View File

@@ -30,6 +30,12 @@
#import "Source/santad/DataLayer/SNTRuleTable.h"
#include "absl/container/flat_hash_map.h"
enum class PlatformBinaryState {
kRuntimeTrue = 0,
kRuntimeFalse,
kStaticCheck,
};
@interface SNTPolicyProcessor ()
@property SNTRuleTable *ruleTable;
@property SNTConfigurator *configurator;
@@ -129,7 +135,7 @@
}
static void UpdateCachedDecisionSigningInfo(
SNTCachedDecision *cd, MOLCodesignChecker *csInfo,
SNTCachedDecision *cd, MOLCodesignChecker *csInfo, PlatformBinaryState platformBinaryState,
NSDictionary *_Nullable (^entitlementsFilterCallback)(NSDictionary *_Nullable entitlements)) {
cd.certSHA256 = csInfo.leafCertificate.SHA256;
cd.certCommonName = csInfo.leafCertificate.commonName;
@@ -144,11 +150,18 @@ static void UpdateCachedDecisionSigningInfo(
cd.signingID = FormatSigningID(csInfo);
}
// Ensure that if no teamID exists that the signing info confirms it is a
// platform binary. If not, remove the signingID.
// Ensure that if no teamID exists but a signingID does exist, that the binary
// is a platform binary. If not, remove the signingID.
if (!cd.teamID && cd.signingID) {
if (!csInfo.platformBinary) {
cd.signingID = nil;
switch (platformBinaryState) {
case PlatformBinaryState::kRuntimeTrue: break;
case PlatformBinaryState::kStaticCheck:
if (!csInfo.platformBinary) {
cd.signingID = nil;
}
break;
case PlatformBinaryState::kRuntimeFalse: OS_FALLTHROUGH;
default: cd.signingID = nil; break;
}
}
@@ -170,6 +183,7 @@ static void UpdateCachedDecisionSigningInfo(
certificateSHA256:(nullable NSString *)certificateSHA256
teamID:(nullable NSString *)teamID
signingID:(nullable NSString *)signingID
platformBinaryState:(PlatformBinaryState)platformBinaryState
isProdSignedCallback:(BOOL (^_Nonnull)())isProdSignedCallback
entitlementsFilterCallback:(NSDictionary *_Nullable (^_Nullable)(
NSDictionary *_Nullable entitlements))entitlementsFilterCallback
@@ -215,7 +229,7 @@ static void UpdateCachedDecisionSigningInfo(
cd.signingID = nil;
cd.cdhash = nil;
} else {
UpdateCachedDecisionSigningInfo(cd, csInfo, entitlementsFilterCallback);
UpdateCachedDecisionSigningInfo(cd, csInfo, platformBinaryState, entitlementsFilterCallback);
}
}
@@ -276,18 +290,6 @@ static void UpdateCachedDecisionSigningInfo(
}
}
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
targetProcess:(nonnull const es_process_t *)targetProc
entitlementsFilterCallback:
(NSDictionary *_Nullable (^_Nonnull)(
const char *_Nullable teamID,
NSDictionary *_Nullable entitlements))entitlementsFilterCallback {
return [self decisionForFileInfo:fileInfo
targetProcess:targetProc
preCodesignCheckCallback:nil
entitlementsFilterCallback:entitlementsFilterCallback];
}
- (nonnull SNTCachedDecision *)decisionForFileInfo:(nonnull SNTFileInfo *)fileInfo
targetProcess:(nonnull const es_process_t *)targetProc
preCodesignCheckCallback:(void (^_Nullable)(void))preCodesignCheckCallback
@@ -338,6 +340,8 @@ static void UpdateCachedDecisionSigningInfo(
certificateSHA256:nil
teamID:teamID
signingID:signingID
platformBinaryState:targetProc->is_platform_binary ? PlatformBinaryState::kRuntimeTrue
: PlatformBinaryState::kRuntimeFalse
isProdSignedCallback:^BOOL {
return ((targetProc->codesigning_flags & CS_DEV_CODE) == 0);
}
@@ -369,6 +373,7 @@ static void UpdateCachedDecisionSigningInfo(
certificateSHA256:identifiers.certificateSHA256
teamID:identifiers.teamID
signingID:identifiers.signingID
platformBinaryState:PlatformBinaryState::kStaticCheck
isProdSignedCallback:^BOOL {
if (csInfo) {
// Development OID values defined by Apple and used by the Security Framework

View File

@@ -315,12 +315,9 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
}],
[[SNTKVOManager alloc] initWithObject:configurator
selector:@selector(staticRules)
type:[NSArray class]
callback:^(NSArray *oldValue, NSArray *newValue) {
NSSet *oldValueSet = [NSSet setWithArray:oldValue ?: @[]];
NSSet *newValueSet = [NSSet setWithArray:newValue ?: @[]];
if ([oldValueSet isEqualToSet:newValueSet]) {
type:[NSDictionary class]
callback:^(NSDictionary *oldValue, NSDictionary *newValue) {
if ([oldValue isEqualToDictionary:newValue]) {
return;
}

View File

@@ -20,6 +20,7 @@
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTMetricSet.h"
#import "Source/common/SNTSystemInfo.h"
#import "Source/common/SystemResources.h"
#import "Source/santad/Santad.h"
#include "Source/santad/SantadDeps.h"
@@ -111,13 +112,10 @@ int main(int argc, char *argv[]) {
// Do not wait on child processes
signal(SIGCHLD, SIG_IGN);
NSDictionary *info_dict = [[NSBundle mainBundle] infoDictionary];
NSString *product_version = [SNTSystemInfo santaProductVersion];
NSString *build_version = [SNTSystemInfo santaBuildVersion];
NSProcessInfo *pi = [NSProcessInfo processInfo];
NSString *product_version = info_dict[@"CFBundleShortVersionString"];
NSString *build_version =
[[info_dict[@"CFBundleVersion"] componentsSeparatedByString:@"."] lastObject];
if ([pi.arguments containsObject:@"-v"]) {
printf("%s (build %s)\n", [product_version UTF8String], [build_version UTF8String]);
return 0;

View File

@@ -22,7 +22,7 @@ objc_library(
name = "FCM_lib",
srcs = ["SNTSyncFCM.m"],
hdrs = ["SNTSyncFCM.h"],
sdk_frameworks = ["SystemConfiguration"],
sdk_frameworks = ["Network"],
deps = [
"@MOLAuthenticatingURLSession",
],
@@ -55,6 +55,7 @@ objc_library(
],
hdrs = ["SNTSyncManager.h"],
sdk_dylibs = ["libz"],
sdk_frameworks = ["Network"],
deps = [
":FCM_lib",
":broadcaster_lib",

View File

@@ -150,8 +150,8 @@ using santa::NSStringToUTF8String;
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_file_bundle_hash_millis([event.fileBundleHashMilliseconds unsignedIntValue]);
e->set_file_bundle_binary_count([event.fileBundleBinaryCount unsignedIntValue]);
e->set_pid([event.pid unsignedIntValue]);
e->set_ppid([event.ppid unsignedIntValue]);

View File

@@ -14,7 +14,7 @@
#import "Source/santasyncservice/SNTSyncFCM.h"
#import <SystemConfiguration/SystemConfiguration.h>
#import <Network/Network.h>
#import <MOLAuthenticatingURLSession/MOLAuthenticatingURLSession.h>
@@ -80,7 +80,7 @@ static const uint32_t kDefaultConnectDelayMaxSeconds = 10;
@property(copy, nonatomic) SNTSyncFCMMessageHandler messageHandler;
/** Is used throughout the class to reconnect to FCM after a connection loss. */
@property SCNetworkReachabilityRef reachability;
@property nw_path_monitor_t pathMonitor;
/** FCM client identities. */
@property(nonatomic, readonly) NSString *project;
@@ -97,21 +97,6 @@ static const uint32_t kDefaultConnectDelayMaxSeconds = 10;
@end
#pragma mark SCNetworkReachabilityCallBack
/** Called when the network state changes. */
static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags,
void *info) {
dispatch_async(dispatch_get_main_queue(), ^{
if (flags & kSCNetworkReachabilityFlagsReachable) {
SNTSyncFCM *FCMClient = (__bridge SNTSyncFCM *)info;
SEL s = @selector(reachabilityRestored);
[NSObject cancelPreviousPerformRequestsWithTarget:FCMClient selector:s object:nil];
[FCMClient performSelector:s withObject:nil afterDelay:1];
}
});
}
@implementation SNTSyncFCM
#pragma mark init/dealloc methods
@@ -184,11 +169,6 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
messageHandler:messageHandler];
}
/** Before this object is released ensure reachability release. */
- (void)dealloc {
[self stopReachability];
}
#pragma mark property methods
- (BOOL)isConnected {
@@ -212,24 +192,27 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
/** Start listening for network state changes on a background thread. */
- (void)startReachability {
if (self.reachability) return;
if (self.pathMonitor) return;
self.pathMonitor = nw_path_monitor_create();
nw_path_monitor_set_queue(self.pathMonitor, dispatch_get_main_queue());
nw_path_monitor_set_update_handler(self.pathMonitor, ^(nw_path_t path) {
if (nw_path_get_status(path) == nw_path_status_satisfied) {
SEL s = @selector(reachabilityRestored);
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:s object:nil];
[self performSelector:s withObject:nil afterDelay:1];
}
});
nw_path_monitor_set_cancel_handler(self.pathMonitor, ^{
self.pathMonitor = nil;
});
nw_path_monitor_start(self.pathMonitor);
LOGD(@"Reachability started.");
self.reachability =
SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, _connectComponents.host.UTF8String);
SCNetworkReachabilityContext context = {.info = (__bridge void *)self};
if (SCNetworkReachabilitySetCallback(self.reachability, reachabilityHandler, &context)) {
SCNetworkReachabilitySetDispatchQueue(
self.reachability, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
}
}
/** Stop listening for network state changes. */
- (void)stopReachability {
if (self.reachability) {
SCNetworkReachabilitySetDispatchQueue(self.reachability, NULL);
if (self.reachability) CFRelease(self.reachability);
self.reachability = NULL;
}
if (!self.pathMonitor) return;
nw_path_monitor_cancel(self.pathMonitor);
}
#pragma mark message methods

View File

@@ -16,7 +16,7 @@
#import <MOLAuthenticatingURLSession/MOLAuthenticatingURLSession.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#import <SystemConfiguration/SystemConfiguration.h>
#import <Network/Network.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
@@ -35,9 +35,7 @@
static const uint8_t kMaxEnqueuedSyncs = 2;
@interface SNTSyncManager () <SNTPushNotificationsDelegate> {
SCNetworkReachabilityRef _reachability;
}
@interface SNTSyncManager () <SNTPushNotificationsDelegate>
@property(nonatomic) dispatch_source_t fullSyncTimer;
@property(nonatomic) dispatch_source_t ruleSyncTimer;
@@ -48,6 +46,7 @@ static const uint8_t kMaxEnqueuedSyncs = 2;
@property(nonatomic) MOLXPCConnection *daemonConn;
@property(nonatomic) BOOL reachable;
@property nw_path_monitor_t pathMonitor;
@property SNTPushNotifications *pushNotifications;
@@ -58,22 +57,6 @@ static const uint8_t kMaxEnqueuedSyncs = 2;
@end
// Called when the network state changes
static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags,
void *info) {
// Put this check and set on the main thread to ensure serial access.
dispatch_async(dispatch_get_main_queue(), ^{
SNTSyncManager *commandSyncManager = (__bridge SNTSyncManager *)info;
// Only call the setter when there is a change. This will filter out the redundant calls to this
// callback whenever the network interface states change.
int reachable =
(flags & kSCNetworkReachabilityFlagsReachable) == kSCNetworkReachabilityFlagsReachable;
if (commandSyncManager.reachable != reachable) {
commandSyncManager.reachable = reachable;
}
});
}
@implementation SNTSyncManager
#pragma mark init
@@ -102,11 +85,6 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
return self;
}
- (void)dealloc {
// Ensure reachability is always stopped
[self stopReachability];
}
#pragma mark SNTSyncServiceXPC methods
- (void)postEventsToSyncServer:(NSArray<SNTStoredEvent *> *)events fromBundle:(BOOL)isFromBundle {
@@ -279,8 +257,8 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
return [self eventUploadWithSyncState:syncState];
}
LOGE(@"Preflight failed, will try again once %@ is reachable",
[[SNTConfigurator configurator] syncBaseURL].absoluteString);
SLOGE(@"Preflight failed, will try again once %@ is reachable",
[[SNTConfigurator configurator] syncBaseURL].absoluteString);
[self startReachability];
return SNTSyncStatusTypePreflightFailed;
}
@@ -408,39 +386,36 @@ static void reachabilityHandler(SCNetworkReachabilityRef target, SCNetworkReacha
- (void)setReachable:(BOOL)reachable {
_reachable = reachable;
if (reachable) {
LOGD(@"Internet connection has been restored, triggering a new sync.");
[self stopReachability];
[self sync];
}
}
// Start listening for network state changes on a background thread
// Start listening for network state changes.
- (void)startReachability {
dispatch_async(dispatch_get_main_queue(), ^{
if (self->_reachability) return;
const char *nodename = [[SNTConfigurator configurator] syncBaseURL].host.UTF8String;
self->_reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, nodename);
SCNetworkReachabilityContext context = {
.info = (__bridge_retained void *)self,
.release = (void (*)(const void *))CFBridgingRelease,
};
if (SCNetworkReachabilitySetCallback(self->_reachability, reachabilityHandler, &context)) {
SCNetworkReachabilitySetDispatchQueue(
self->_reachability, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
} else {
[self stopReachability];
if (self.pathMonitor) return;
self.pathMonitor = nw_path_monitor_create();
// Put the callback on the main thread to ensure serial access.
nw_path_monitor_set_queue(self.pathMonitor, dispatch_get_main_queue());
nw_path_monitor_set_update_handler(self.pathMonitor, ^(nw_path_t path) {
// Only call the setter when there is a change. This will filter out the redundant calls to
// this callback whenever the network interface states change.
int reachable = nw_path_get_status(path) == nw_path_status_satisfied;
if (self.reachable != reachable) {
self.reachable = reachable;
}
});
nw_path_monitor_set_cancel_handler(self.pathMonitor, ^{
self.pathMonitor = nil;
});
nw_path_monitor_start(self.pathMonitor);
}
// Stop listening for network state changes
- (void)stopReachability {
dispatch_async(dispatch_get_main_queue(), ^{
if (self->_reachability) {
SCNetworkReachabilitySetDispatchQueue(self->_reachability, NULL);
if (self->_reachability) CFRelease(self->_reachability);
self->_reachability = NULL;
}
});
if (!self.pathMonitor) return;
nw_path_monitor_cancel(self.pathMonitor);
}
@end

View File

@@ -38,8 +38,8 @@ using santa::NSStringToUTF8String;
google::protobuf::Arena arena;
auto req = google::protobuf::Arena::Create<::pbv1::PostflightRequest>(&arena);
req->set_machine_id(NSStringToUTF8String(self.syncState.machineID));
req->set_rules_received(self.syncState.rulesReceived);
req->set_rules_processed(self.syncState.rulesProcessed);
req->set_rules_received(static_cast<uint32_t>(self.syncState.rulesReceived));
req->set_rules_processed(static_cast<uint32_t>(self.syncState.rulesProcessed));
::pbv1::PostflightResponse response;
[self performRequest:[self requestWithMessage:req] intoMessage:&response timeout:30];

View File

@@ -87,8 +87,7 @@ The following table expands upon the above logic to list most of the permutation
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_santa_version(NSStringToUTF8String([SNTSystemInfo santaFullVersion]));
req->set_primary_user(NSStringToUTF8String(self.syncState.machineOwner));
if (self.syncState.pushNotificationsToken) {

View File

@@ -14,6 +14,7 @@
#import "Source/santasyncservice/SNTSyncStage.h"
#include <Foundation/Foundation.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#import "Source/common/SNTCommonEnums.h"
@@ -74,10 +75,7 @@ using santa::NSStringToUTF8String;
contentType:@"application/x-protobuf"];
}
google::protobuf::json::PrintOptions options{
.always_print_enums_as_ints = false,
.preserve_proto_field_names = true,
};
google::protobuf::json::PrintOptions options{};
std::string json;
absl::Status status = google::protobuf::json::MessageToJsonString(*message, &json, options);
@@ -88,13 +86,12 @@ using santa::NSStringToUTF8String;
return nil;
}
SLOGD(@"Request JSON: %s", json.c_str());
return [self requestWithData:[NSData dataWithBytes:json.data() length:json.size()]
contentType:@"application/json"];
}
- (NSMutableURLRequest *)requestWithData:(NSData *)requestBody contentType:(NSString *)contentType {
if (contentType.length) contentType = @"application/octet-stream";
if (!contentType.length) contentType = @"application/octet-stream";
NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:[self stageURL]];
[req setHTTPMethod:@"POST"];
@@ -179,6 +176,11 @@ using santa::NSStringToUTF8String;
data = [self performRequest:request timeout:timeout response:&response error:&requestError];
if (response.statusCode == 200) break;
// If the original request failed because of a "No network" error, break out of the loop,
// subsequent retries are pointless and the entire sync will be retried once a connection
// is established.
if (requestError.code == NSURLErrorNotConnectedToInternet) break;
// If the original request failed because of an auth error, attempt to get a new XSRF token and
// try again. Unfortunately some servers cause NSURLSession to return 'client cert required' or
// 'could not parse response' when a 403 occurs and SSL cert auth is enabled.

View File

@@ -22,6 +22,7 @@
#import "Source/common/SNTRule.h"
#import "Source/common/SNTStoredEvent.h"
#import "Source/common/SNTSyncConstants.h"
#import "Source/common/SNTSystemInfo.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santasyncservice/SNTSyncEventUpload.h"
#import "Source/santasyncservice/SNTSyncPostflight.h"
@@ -60,11 +61,19 @@
OCMStub([self.syncState.daemonConn remoteObjectProxy]).andReturn(self.daemonConnRop);
OCMStub([self.syncState.daemonConn synchronousRemoteObjectProxy]).andReturn(self.daemonConnRop);
id siMock = OCMClassMock([SNTSystemInfo class]);
OCMStub([siMock serialNumber]).andReturn(@"QYGF4QM373");
OCMStub([siMock longHostname]).andReturn(@"full-hostname.example.com");
OCMStub([siMock osVersion]).andReturn(@"14.5");
OCMStub([siMock osBuild]).andReturn(@"23F79");
OCMStub([siMock modelIdentifier]).andReturn(@"MacBookPro18,3");
OCMStub([siMock santaFullVersion]).andReturn(@"2024.6.655965194");
self.syncState.session = OCMClassMock([NSURLSession class]);
self.syncState.syncBaseURL = [NSURL URLWithString:@"https://myserver.local/"];
self.syncState.machineID = [[NSUUID UUID] UUIDString];
self.syncState.machineOwner = NSUserName();
self.syncState.machineID = @"50C7E1EB-2EF5-42D4-A084-A7966FC45A95";
self.syncState.machineOwner = @"username1";
}
#pragma mark Test Helpers
@@ -271,7 +280,17 @@
SNTSyncPreflight *sut = [[SNTSyncPreflight alloc] initWithState:self.syncState];
NSData *respData = [self dataFromFixture:@"sync_preflight_basic.json"];
[self stubRequestBody:respData response:nil error:nil validateBlock:nil];
[self
stubRequestBody:respData
response:nil
error:nil
validateBlock:^BOOL(NSURLRequest *req) {
NSData *gotReqData = [req HTTPBody];
NSData *expectedReqData = [self dataFromFixture:@"sync_preflight_request.json"];
XCTAssertEqualObjects(gotReqData, expectedReqData);
XCTAssertEqualObjects([req valueForHTTPHeaderField:@"Content-Type"], @"application/json");
return YES;
}];
XCTAssertTrue([sut sync]);
XCTAssertEqual(self.syncState.clientMode, SNTClientModeMonitor);

View File

@@ -35,27 +35,27 @@ enum ClientMode {
}
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;
string serial_number = 1 [json_name="serial_num"];
string hostname = 2 [json_name="hostname"];
string os_version = 3 [json_name="os_version"];
string os_build = 4 [json_name="os_build"];
string model_identifier = 5 [json_name="model_identifier"];
string santa_version = 6 [json_name="santa_version"];
string primary_user = 7 [json_name="primary_user"];
string push_notification_token = 8 [json_name="push_notification_token"];
ClientMode client_mode = 9;
bool request_clean_sync = 10;
ClientMode client_mode = 9 [json_name="client_mode"];
bool request_clean_sync = 10 [json_name="request_clean_sync"];
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;
uint32 binary_rule_count = 11 [json_name="binary_rule_count"];
uint32 certificate_rule_count = 12 [json_name="certificate_rule_count"];
uint32 compiler_rule_count = 13 [json_name="compiler_rule_count"];
uint32 transitive_rule_count = 14 [json_name="transitive_rule_count"];
uint32 teamid_rule_count = 15 [json_name="teamid_rule_count"];
uint32 signingid_rule_count = 16 [json_name="signingid_rule_count"];
uint32 cdhash_rule_count = 17 [json_name="cdhash_rule_count"];
// The UUID of the machine that is sending this preflight.
string machine_id = 18;
string machine_id = 18 [json_name="machine_id"];
}
enum SyncType {
@@ -125,16 +125,16 @@ message PreflightResponse {
optional 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"];
uint32 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"];
uint32 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"];
uint32 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.
@@ -159,8 +159,8 @@ message PreflightResponse {
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 uint32 deprecated_fcm_full_sync_interval_seconds = 1003 [json_name="fcm_full_sync_interval", deprecated=true];
optional uint32 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];
@@ -193,53 +193,53 @@ message Certificate {
string cn = 2;
string org = 3;
string ou = 4;
uint32 valid_from = 5;
uint32 valid_until = 6;
uint32 valid_from = 5 [json_name="valid_from"];
uint32 valid_until = 6 [json_name="valid_until"];
}
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_sha256 = 1 [json_name="file_sha256"];
string file_path = 2 [json_name="file_path"];
string file_name = 3 [json_name="file_name"];
string executing_user = 4 [json_name="executing_user"];
double execution_time = 5 [json_name="execution_time"];
repeated string logged_in_users = 6 [json_name="logged_in_users"];
repeated string current_sessions = 7 [json_name="current_sessions"];
Decision decision = 8 [json_name="decision"];
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;
string file_bundle_id = 9 [json_name="file_bundle_id"];
string file_bundle_path = 10 [json_name="file_bundle_path"];
string file_bundle_executable_rel_path = 11 [json_name="file_bundle_executable_rel_path"];
string file_bundle_name = 12 [json_name="file_bundle_name"];
string file_bundle_version = 13 [json_name="file_bundle_version"];
string file_bundle_version_string = 14 [json_name="file_bundle_version_string"];
string file_bundle_hash = 15 [json_name="file_bundle_hash"];
uint32 file_bundle_hash_millis = 16 [json_name="file_bundle_hash_millis"];
uint32 file_bundle_binary_count = 17 [json_name="file_bundle_binary_count"];
// pid_t is an int32
int32 pid = 18;
int32 ppid = 19;
string parent_name = 20;
int32 pid = 18 [json_name="pid"];
int32 ppid = 19 [json_name="ppid"];
string parent_name = 20 [json_name="parent_name"];
string team_id = 21;
string signing_id = 22;
string cdhash = 23;
string team_id = 21 [json_name="team_id"];
string signing_id = 22 [json_name="signing_id"];
string cdhash = 23 [json_name="cdhash"];
string quarantine_data_url = 24;
string quarantine_referer_url = 25;
string quarantine_data_url = 24 [json_name="quarantine_data_url"];
string quarantine_referer_url = 25 [json_name="quarantine_referer_url"];
// 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;
uint32 quarantine_timestamp = 26 [json_name="quarantine_timestamp"];
string quarantine_agent_bundle_id = 27 [json_name="quarantine_agent_bundle_id"];
repeated Certificate signing_chain = 28;
repeated Certificate signing_chain = 28 [json_name="signing_chain"];
}
message EventUploadRequest {
repeated Event events = 1;
repeated Event events = 1 [json_name="events"];
// The UUID of the machine where the event(s) occurred
string machine_id = 2;
string machine_id = 2 [json_name="machine_id"];
}
message EventUploadResponse {
@@ -294,9 +294,9 @@ message Rule {
}
message RuleDownloadRequest {
string cursor = 1;
string cursor = 1 [json_name="cursor"];
// The UUID of the machine that is requesting the rules.
string machine_id = 2;
string machine_id = 2 [json_name="machine_id"];
}
message RuleDownloadResponse {
@@ -305,10 +305,10 @@ message RuleDownloadResponse {
}
message PostflightRequest {
uint64 rules_received = 1;
uint64 rules_processed = 2;
uint32 rules_received = 1 [json_name="rules_received"];
uint32 rules_processed = 2 [json_name="rules_processed"];
// The UUID of the machine that is sending this postflight.
string machine_id = 3;
string machine_id = 3 [json_name="machine_id"];
}
message PostflightResponse { }

View File

@@ -0,0 +1 @@
{"serial_num":"QYGF4QM373","hostname":"full-hostname.example.com","os_version":"14.5","os_build":"23F79","model_identifier":"MacBookPro18,3","santa_version":"2024.6.655965194","primary_user":"username1","client_mode":"MONITOR","machine_id":"50C7E1EB-2EF5-42D4-A084-A7966FC45A95"}

View File

@@ -14,3 +14,7 @@ gh_edit_branch: "main"
plugins:
- jekyll-redirect-from
callouts:
important:
title: Important
color: blue

View File

@@ -140,7 +140,7 @@ The JSON object has the following keys:
| enable_bundles | Use previous setting | boolean | Enable bundle scanning | true |
| enable_transitive_rules | Use previous setting | boolean | Whether or not to enable transitive allowlisting | true |
| batch_size | Use a Santa-defined default value | integer | Number of events to upload at a time | 128 |
| full_sync_interval | Defaults to 600 seconds | integer | Number of seconds between full syncs. Note: Santa enforces a minimum value of 60. The default value will be used if a smaller value is provided. | 600 |
| full_sync_interval | Defaults to 600 seconds | uint32 | Number of seconds between full syncs. Note: Santa enforces a minimum value of 60. The default value will be used if a smaller value is provided. | 600 |
| client_mode | Use previous setting | string | Operating mode to set for the client | either `MONITOR` or `LOCKDOWN` |
| allowed_path_regex | Use previous setting | string | Regular expression to allow a binary to execute from a path | "/Users/markowsk/foo/.\*" |
| blocked_path_regex | Use previous setting | string | Regular expression to block a binary from executing by path | "/tmp/" |
@@ -223,8 +223,8 @@ sequenceDiagram
| file_bundle_version | NO | string | The bundle version string | "9999.1.1" |
| file_bundle_version_string | NO | string | Bundle short version string | "2.3.4" |
| file_bundle_hash | NO | string | SHA256 hash of all executables in the bundle | "7466e3687f540bcb7792c6d14d5a186667dbe18a85021857b42effe9f0370805" |
| file_bundle_hash_millis | NO | float64 | The time in milliseconds it took to find all of the binaries, hash and produce the bundle_hash | 1234775 |
| file_bundle_binary_count | NO | integer | The number of binaries in a bundle | 13 |
| file_bundle_hash_millis | NO | uint32 | The time in milliseconds it took to find all of the binaries, hash and produce the bundle_hash | 1234775 |
| file_bundle_binary_count | NO | uint32 | The number of binaries in a bundle | 13 |
| pid | NO | int | Process id of the executable that was blocked | 1234 |
| ppid | NO | int | Parent process id of the executable that was blocked | 456 |
| parent_name | NO | Parent process short command name of the executable that was blocked | "bar" |

View File

@@ -3,6 +3,11 @@ title: Home
nav_order: 1
---
{: .important }
**As of 2025, Santa is no longer maintained by Google.** We encourage existing
users to migrate to an actively maintained fork of Santa, such as
[https://github.com/northpolesec/santa](https://github.com/northpolesec/santa).
# Welcome to the Santa documentation
Santa is a binary and file access authorization system for macOS. It consists of a system extension that allows or denies attempted executions using a set of rules stored in a local database, a GUI agent that notifies the user in case of a block decision, a sync daemon responsible for syncing the database, and a server, and a command-line utility for managing the system.