Compare commits

...

19 Commits

Author SHA1 Message Date
Matt W
8f5f8de245 Only remount on startup if remount args are set (#1222) 2023-11-06 09:10:34 -05:00
Matt W
7c58648c35 Bump hedron commit. Minor WORKSPACE fixups. (#1221) 2023-11-03 10:03:11 -04:00
Matt W
3f3751eb18 Fix remount issue for APFS formatted drives (#1220)
* Fix issue mounting APFS drives with MNT_JOURNALED

* typo
2023-11-02 22:20:35 -04:00
Matt W
7aa2d69ce6 Add OnStartUSBOptions to santactl status (#1219) 2023-11-02 20:30:05 -04:00
Matt W
f9a937a6e4 Record metrics for device manager startup operations (#1218)
* Record metrics for device manager startup operations

* Update help text

* Update help text
2023-11-02 20:27:57 -04:00
Matt W
d2cbddd3fb Support remounting devices at startup with correct flags (#1216)
* Support remounting devices at startup with correct flags

* Add missing force remount condition
2023-11-02 14:37:28 -04:00
Pete Markowsky
ea7e11fc22 Add Support for CS_INVALIDATED events (#1210)
Add support for logging when codesigning has become invalidated for a process.

This adds support to the Recorder to log when codesigning is invalidated as reported by the Endpoint Security Framework's
ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED event.
2023-11-02 10:04:18 -04:00
Nick Gregory
7530b8f5c1 Add E2E testing for usb (#1214)
* e2e test usb mounting

* no poweroff

* no start

* drive usb via sync server since its up

sudo santactl status

sudo?

* revert nostart/nopoweroff

* bump VMCLI minimum os version
2023-11-01 11:44:00 -04:00
Matt W
64bb34b2ca Additional build deps (#1215)
* Update build deps

* lint
2023-10-31 14:16:28 -04:00
Matt W
c5c6037085 Unmount USB on start (#1211)
* WIP Allow configuring Santa to unmount existing mass storage devices on startup

* WIP fixup existing tests

* Add unmount on startup tests
2023-10-31 13:34:10 -04:00
Matt W
275a8ed607 Support printing bundle info via santactl fileinfo command (#1213) 2023-10-31 13:19:00 -04:00
Nick Gregory
28dd6cbaed Enable e2e testing on macOS 14 (#1209)
* e2e for macos 14

* no shutdown

* gh path

* dismiss santa popup after bad binary

* sleep for ui

* re-enable start vm

* re-enable poweroff

* tabs

* ratchet checkout actions in e2e
2023-10-30 17:45:37 -04:00
Pete Markowsky
8c466b4408 Fix issue preventing rule import / export from working (#1199)
* Fix issue preventing rule import / export from working.

* Removed unused --json option from help string.

* Document that import and export as taking a path argument.
2023-10-25 16:47:14 -04:00
p-harrison
373c676306 Update syncing-overview.md (#1205)
Update the syncing-overview.md document to note that FCM based push notifications are not currently available outside the internal Google deployment of Santa.
2023-10-25 14:17:22 -04:00
p-harrison
d214d510e5 Update configuration.md to note that push notifications not widely available (#1204)
Update the configuration.md document to note that FCM based push notifications are not currently available outside the internal Google deployment of Santa
2023-10-25 14:11:15 -04:00
Pete Markowsky
6314fe04e3 Remove mention of KEXT from README.md (#1202)
* Remove mention of kext from README.md
2023-10-25 14:07:43 -04:00
p-harrison
11d9c29daa docs: Update configuration.md to explain EnableDebugLogging (#1203)
Update configuration.md with details of the EnableDebugLogging configuration key.  Also some minor formatting changes.
2023-10-16 10:29:45 -04:00
Matt W
60238f0ed2 Minor doc updates. Add missing FAA config options. (#1197)
* Minor doc updates. Add missing FAA config options.

* Fix typo. Add higher res icon.
2023-10-06 12:30:36 -04:00
Russell Hancox
7aa731a76f santactl/sync: Drop root requirement (#1196)
Previously the sync command required root in order to establish a connection to santad with enough privilege to use the XPC methods for adding rules. Now that santasyncservice exists this requirement is no longer necessary and there is no risk in allowing unprivileged users to initiate a sync.

We still ensure that privileges are dropped, just in case someone does execute as root.
2023-09-29 12:56:15 -04:00
58 changed files with 1197 additions and 192 deletions

View File

@@ -1,25 +1,28 @@
name: E2E
on: workflow_dispatch
on:
schedule:
- cron: '0 4 * * *' # Every day at 4:00 UTC (not to interfere with fuzzing)
workflow_dispatch:
jobs:
start_vm:
runs-on: e2e-host
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Start VM
run: python3 Testing/integration/actions/start_vm.py macOS_12.bundle.tar.gz
run: python3 Testing/integration/actions/start_vm.py macOS_14.bundle.tar.gz
integration:
runs-on: e2e-vm
env:
VM_PASSWORD: ${{ secrets.VM_PASSWORD }}
steps:
- uses: actions/checkout@v3
- name: Install configuration profile
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # ratchet:actions/checkout@v3
- name: Add homebrew to PATH
run: echo "/opt/homebrew/bin/" >> $GITHUB_PATH
- name: Install configuration profile
run: bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
- name: Build, install, and start moroz
run: |
bazel build @com_github_groob_moroz//cmd/moroz:moroz
@@ -36,6 +39,8 @@ jobs:
run: ./Testing/integration/test_config_changes.sh
- name: Test sync server changes
run: ./Testing/integration/test_sync_changes.sh
- name: Test USB blocking
run: ./Testing/integration/test_usb.sh
- name: Poweroff
if: ${{ always() }}
run: sudo shutdown -h +1

View File

@@ -7,10 +7,10 @@
[![downloads](https://img.shields.io/github/downloads/google/santa/latest/total)](https://github.com/google/santa/releases/latest)
<p align="center">
<img src="https://raw.githubusercontent.com/google/santa/main/Source/gui/Resources/Images.xcassets/AppIcon.appiconset/santa-hat-icon-128.png" alt="Santa Icon" />
<img src="./docs/images/santa-sleigh-256.png" height="128" alt="Santa Icon" />
</p>
Santa is a binary authorization system for macOS. It consists of a system
Santa is a binary and file access authorization system for macOS. It consists of a system
extension that monitors for executions, a daemon that makes execution decisions
based on the contents of a local database, a GUI agent that notifies the user in
case of a block decision and a command-line utility for managing the system and
@@ -48,9 +48,7 @@ disclosure reporting.
the events database. In LOCKDOWN mode, only listed binaries are allowed to
run.
* Event logging: When the kext is loaded, all binary launches are logged. When
in either mode, all unknown or denied binaries are stored in the database to
enable later aggregation.
* Event logging: When the system extension is loaded, all binary launches are logged. When in either mode, all unknown or denied binaries are stored in the database to enable later aggregation.
* Certificate-based rules, with override levels: Instead of relying on a
binary's hash (or 'fingerprint'), executables can be allowed/blocked by their

View File

@@ -158,6 +158,14 @@ typedef NS_ENUM(NSInteger, SNTOverrideFileAccessAction) {
SNTOverrideFileAccessActionDiable,
};
typedef NS_ENUM(NSInteger, SNTDeviceManagerStartupPreferences) {
SNTDeviceManagerStartupPreferencesNone,
SNTDeviceManagerStartupPreferencesUnmount,
SNTDeviceManagerStartupPreferencesForceUnmount,
SNTDeviceManagerStartupPreferencesRemount,
SNTDeviceManagerStartupPreferencesForceRemount,
};
#ifdef __cplusplus
enum class FileAccessPolicyDecision {
kNoPolicy,

View File

@@ -454,6 +454,20 @@
///
@property(nonatomic) NSArray<NSString *> *remountUSBMode;
///
/// If set, defines the action that should be taken on existing USB mounts when
/// Santa starts up.
///
/// Supported values are:
/// * "Unmount": Unmount mass storage devices
/// * "ForceUnmount": Force unmount mass storage devices
///
///
/// Note: Existing mounts with mount flags that are a superset of RemountUSBMode
/// are unaffected and left mounted.
///
@property(readonly, nonatomic) SNTDeviceManagerStartupPreferences onStartUSBOptions;
///
/// If set, will override the action taken when a file access rule violation
/// occurs. This setting will apply across all rules in the file access policy.

View File

@@ -121,6 +121,7 @@ static NSString *const kClientModeKey = @"ClientMode";
static NSString *const kFailClosedKey = @"FailClosed";
static NSString *const kBlockUSBMountKey = @"BlockUSBMount";
static NSString *const kRemountUSBModeKey = @"RemountUSBMode";
static NSString *const kOnStartUSBOptions = @"OnStartUSBOptions";
static NSString *const kEnableTransitiveRulesKey = @"EnableTransitiveRules";
static NSString *const kEnableTransitiveRulesKeyDeprecated = @"EnableTransitiveWhitelisting";
static NSString *const kAllowedPathRegexKey = @"AllowedPathRegex";
@@ -181,6 +182,7 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
kBlockedPathRegexKeyDeprecated : re,
kBlockUSBMountKey : number,
kRemountUSBModeKey : array,
kOnStartUSBOptions : string,
kEnablePageZeroProtectionKey : number,
kEnableBadSignatureProtectionKey : number,
kEnableSilentModeKey : number,
@@ -635,6 +637,22 @@ static NSString *const kSyncCleanRequired = @"SyncCleanRequired";
return args;
}
- (SNTDeviceManagerStartupPreferences)onStartUSBOptions {
NSString *action = [self.configState[kOnStartUSBOptions] lowercaseString];
if ([action isEqualToString:@"unmount"]) {
return SNTDeviceManagerStartupPreferencesUnmount;
} else if ([action isEqualToString:@"forceunmount"]) {
return SNTDeviceManagerStartupPreferencesForceUnmount;
} else if ([action isEqualToString:@"remount"]) {
return SNTDeviceManagerStartupPreferencesRemount;
} else if ([action isEqualToString:@"forceremount"]) {
return SNTDeviceManagerStartupPreferencesForceRemount;
} else {
return SNTDeviceManagerStartupPreferencesNone;
}
}
- (NSDictionary<NSString *, SNTRule *> *)staticRules {
return self.cachedStaticRules;
}

View File

@@ -381,6 +381,11 @@ message Unlink {
optional FileInfo target = 2;
}
// Information about a processes codesigning invalidation event
message CodesigningInvalidated {
optional ProcessInfoLight instigator = 1;
}
// Information about a link event
message Link {
// The process performing the link
@@ -529,6 +534,7 @@ message SantaMessage {
Bundle bundle = 19;
Allowlist allowlist = 20;
FileAccess file_access = 21;
CodesigningInvalidated codesigning_invalidated = 22;
};
}

View File

@@ -115,6 +115,8 @@ santa_unit_test(
"//Source/common:SNTFileInfo",
"//Source/common:SNTLogging",
"//Source/common:SNTRule",
"//Source/common:SNTStoredEvent",
"//Source/common:SNTXPCBundleServiceInterface",
"//Source/common:SNTXPCControlInterface",
"@MOLCertificate",
"@MOLCodesignChecker",

View File

@@ -22,6 +22,8 @@
#import "Source/common/SNTFileInfo.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTRule.h"
#import "Source/common/SNTStoredEvent.h"
#import "Source/common/SNTXPCBundleServiceInterface.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santactl/SNTCommand.h"
#import "Source/santactl/SNTCommandController.h"
@@ -55,6 +57,13 @@ static NSString *const kValidUntil = @"Valid Until";
static NSString *const kSHA256 = @"SHA-256";
static NSString *const kSHA1 = @"SHA-1";
// bundle info keys
static NSString *const kBundleInfo = @"Bundle Info";
static NSString *const kBundlePath = @"Main Bundle Path";
static NSString *const kBundleID = @"Main Bundle ID";
static NSString *const kBundleHash = @"Bundle Hash";
static NSString *const kBundleHashes = @"Bundle Hashes";
// Message displayed when daemon communication fails
static NSString *const kCommunicationErrorMsg = @"Could not communicate with daemon";
@@ -72,6 +81,7 @@ NSString *formattedStringForKeyArray(NSArray<NSString *> *array) {
// Properties set from commandline flags
@property(nonatomic) BOOL recursive;
@property(nonatomic) BOOL jsonOutput;
@property(nonatomic) BOOL bundleInfo;
@property(nonatomic) NSNumber *certIndex;
@property(nonatomic, copy) NSArray<NSString *> *outputKeyList;
@property(nonatomic, copy) NSDictionary<NSString *, NSRegularExpression *> *outputFilters;
@@ -156,6 +166,7 @@ REGISTER_COMMAND_NAME(@"fileinfo")
@"\n"
@"Usage: santactl fileinfo [options] [file-paths]\n"
@" --recursive (-r): Search directories recursively.\n"
@" Incompatible with --bundleinfo.\n"
@" --json: Output in JSON format.\n"
@" --key: Search and return this one piece of information.\n"
@" You may specify multiple keys by repeating this flag.\n"
@@ -167,12 +178,16 @@ REGISTER_COMMAND_NAME(@"fileinfo")
@" signing chain to show info only for that certificate.\n"
@" 0 up to n for the leaf certificate up to the root\n"
@" -1 down to -n-1 for the root certificate down to the leaf\n"
@" Incompatible with --bundleinfo."
@"\n"
@" --filter: Use predicates of the form 'key=regex' to filter out which files\n"
@" are displayed. Valid keys are the same as for --key. Value is a\n"
@" case-insensitive regular expression which must match anywhere in\n"
@" the keyed property value for the file's info to be displayed.\n"
@" You may specify multiple filters by repeating this flag.\n"
@" --bundleinfo: If the file is part of a bundle, will also display bundle\n"
@" hash information and hashes of all bundle executables.\n"
@" Incompatible with --recursive and --cert-index.\n"
@"\n"
@"Examples: santactl fileinfo --cert-index 1 --key SHA-256 --json /usr/bin/yes\n"
@" santactl fileinfo --key SHA-256 --json /usr/bin/yes\n"
@@ -682,6 +697,48 @@ REGISTER_COMMAND_NAME(@"fileinfo")
if (outputDict[key]) continue; // ignore keys that we've already set due to a filter
outputDict[key] = self.propertyMap[key](self, fileInfo);
}
if (self.bundleInfo) {
SNTStoredEvent *se = [[SNTStoredEvent alloc] init];
se.fileBundlePath = fileInfo.bundlePath;
MOLXPCConnection *bc = [SNTXPCBundleServiceInterface configuredConnection];
[bc resume];
__block NSMutableDictionary *bundleInfo = [[NSMutableDictionary alloc] init];
bundleInfo[kBundlePath] = fileInfo.bundle.bundlePath;
bundleInfo[kBundleID] = fileInfo.bundle.bundleIdentifier;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[[bc remoteObjectProxy]
hashBundleBinariesForEvent:se
reply:^(NSString *hash, NSArray<SNTStoredEvent *> *events,
NSNumber *time) {
bundleInfo[kBundleHash] = hash;
NSMutableArray *bundleHashes = [[NSMutableArray alloc] init];
for (SNTStoredEvent *event in events) {
[bundleHashes
addObject:@{kSHA256 : event.fileSHA256, kPath : event.filePath}];
}
bundleInfo[kBundleHashes] = bundleHashes;
[[bc remoteObjectProxy] spindown];
dispatch_semaphore_signal(sema);
}];
int secondsToWait = 30;
if (dispatch_semaphore_wait(sema,
dispatch_time(DISPATCH_TIME_NOW, secondsToWait * NSEC_PER_SEC))) {
fprintf(stderr, "The bundle service did not finish collecting hashes within %d seconds\n",
secondsToWait);
}
outputDict[kBundleInfo] = bundleInfo;
}
}
// If there's nothing in the outputDict, then don't need to print anything.
@@ -710,6 +767,11 @@ REGISTER_COMMAND_NAME(@"fileinfo")
}
}
}
if (self.bundleInfo) {
[output appendString:[self stringForBundleInfo:outputDict[kBundleInfo] key:kBundleInfo]];
}
if (!singleKey) [output appendString:@"\n"];
}
@@ -739,6 +801,9 @@ REGISTER_COMMAND_NAME(@"fileinfo")
if ([arg caseInsensitiveCompare:@"--json"] == NSOrderedSame) {
self.jsonOutput = YES;
} else if ([arg caseInsensitiveCompare:@"--cert-index"] == NSOrderedSame) {
if (self.bundleInfo) {
[self printErrorUsageAndExit:@"\n--cert-index is incompatible with --bundleinfo"];
}
i += 1; // advance to next argument and grab index
if (i >= nargs || [arguments[i] hasPrefix:@"--"]) {
[self printErrorUsageAndExit:@"\n--cert-index requires an argument"];
@@ -788,7 +853,17 @@ REGISTER_COMMAND_NAME(@"fileinfo")
filters[key] = regex;
} else if ([arg caseInsensitiveCompare:@"--recursive"] == NSOrderedSame ||
[arg caseInsensitiveCompare:@"-r"] == NSOrderedSame) {
if (self.bundleInfo) {
[self printErrorUsageAndExit:@"\n--recursive is incompatible with --bundleinfo"];
}
self.recursive = YES;
} else if ([arg caseInsensitiveCompare:@"--bundleinfo"] == NSOrderedSame ||
[arg caseInsensitiveCompare:@"-b"] == NSOrderedSame) {
if (self.recursive || self.certIndex) {
[self printErrorUsageAndExit:
@"\n--bundleinfo is incompatible with --recursive and --cert-index"];
}
self.bundleInfo = YES;
} else {
[paths addObject:arg];
}
@@ -868,6 +943,22 @@ REGISTER_COMMAND_NAME(@"fileinfo")
return result.copy;
}
- (NSString *)stringForBundleInfo:(NSDictionary *)bundleInfo key:(NSString *)key {
NSMutableString *result = [NSMutableString string];
[result appendFormat:@"%@:\n", key];
[result appendFormat:@" %-20s: %@\n", kBundlePath.UTF8String, bundleInfo[kBundlePath]];
[result appendFormat:@" %-20s: %@\n", kBundleID.UTF8String, bundleInfo[kBundleID]];
[result appendFormat:@" %-20s: %@\n", kBundleHash.UTF8String, bundleInfo[kBundleHash]];
for (NSDictionary *hashPath in bundleInfo[kBundleHashes]) {
[result appendFormat:@" %@ %@\n", hashPath[kSHA256], hashPath[kPath]];
}
return [result copy];
}
- (NSString *)stringForCertificate:(NSDictionary *)cert withKeys:(NSArray *)keys index:(int)index {
if (!cert) return @"";
NSMutableString *result = [NSMutableString string];

View File

@@ -54,8 +54,8 @@ REGISTER_COMMAND_NAME(@"rule")
@" --compiler: allow and mark as a compiler\n"
@" --remove: remove existing rule\n"
@" --check: check for an existing rule\n"
@" --import: import rules from a JSON file\n"
@" --export: export rules to a JSON file\n"
@" --import {path}: import rules from a JSON file\n"
@" --export {path}: export rules to a JSON file\n"
@"\n"
@" One of:\n"
@" --path {path}: path of binary/bundle to add/remove.\n"
@@ -64,7 +64,6 @@ REGISTER_COMMAND_NAME(@"rule")
@" the rule state of a file.\n"
@" --identifier {sha256|teamID|signingID}: identifier to add/remove/check\n"
@" --sha256 {sha256}: hash to add/remove/check [deprecated]\n"
@" --json {path}: path to a JSON file containing a list of rules to add/remove\n"
@"\n"
@" Optionally:\n"
@" --teamid: add or check a team ID rule instead of binary\n"
@@ -174,11 +173,6 @@ REGISTER_COMMAND_NAME(@"rule")
} else if ([arg caseInsensitiveCompare:@"--force"] == NSOrderedSame) {
// Don't do anything special.
#endif
} else if ([arg caseInsensitiveCompare:@"--json"] == NSOrderedSame) {
if (++i > arguments.count - 1) {
[self printErrorUsageAndExit:@"--json requires an argument"];
}
jsonFilePath = arguments[i];
} else if ([arg caseInsensitiveCompare:@"--import"] == NSOrderedSame) {
if (exportRules) {
[self printErrorUsageAndExit:@"--import and --export are mutually exclusive"];
@@ -206,6 +200,21 @@ REGISTER_COMMAND_NAME(@"rule")
}
}
if (jsonFilePath.length > 0) {
if (importRules) {
if (newRule.identifier != nil || path != nil || check) {
[self printErrorUsageAndExit:@"--import can only be used by itself"];
}
[self importJSONFile:jsonFilePath];
} else if (exportRules) {
if (newRule.identifier != nil || path != nil || check) {
[self printErrorUsageAndExit:@"--export can only be used by itself"];
}
[self exportJSONFile:jsonFilePath];
}
return;
}
if (path) {
SNTFileInfo *fi = [[SNTFileInfo alloc] initWithPath:path];
if (!fi.path) {
@@ -236,16 +245,6 @@ REGISTER_COMMAND_NAME(@"rule")
return [self printStateOfRule:newRule daemonConnection:self.daemonConn];
}
// Note this block needs to come after the check block above.
if (jsonFilePath.length > 0) {
if (importRules) {
[self importJSONFile:jsonFilePath];
} else if (exportRules) {
[self exportJSONFile:jsonFilePath];
}
return;
}
if (newRule.state == SNTRuleStateUnknown) {
[self printErrorUsageAndExit:@"No state specified"];
} else if (!newRule.identifier) {

View File

@@ -15,11 +15,22 @@
#import <Foundation/Foundation.h>
#import <MOLXPCConnection/MOLXPCConnection.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTXPCControlInterface.h"
#import "Source/santactl/SNTCommand.h"
#import "Source/santactl/SNTCommandController.h"
NSString *StartupOptionToString(SNTDeviceManagerStartupPreferences pref) {
switch (pref) {
case SNTDeviceManagerStartupPreferencesUnmount: return @"Unmount";
case SNTDeviceManagerStartupPreferencesForceUnmount: return @"ForceUnmount";
case SNTDeviceManagerStartupPreferencesRemount: return @"Remount";
case SNTDeviceManagerStartupPreferencesForceRemount: return @"ForceRemount";
default: return @"None";
}
}
@interface SNTCommandStatus : SNTCommand <SNTCommandProtocol>
@end
@@ -195,6 +206,7 @@ REGISTER_COMMAND_NAME(@"status")
@"remount_usb_mode" : (configurator.blockUSBMount && configurator.remountUSBMode.count
? configurator.remountUSBMode
: @""),
@"on_start_usb_options" : StartupOptionToString(configurator.onStartUSBOptions),
},
@"database" : @{
@"binary_rules" : @(binaryRuleCount),
@@ -252,9 +264,11 @@ REGISTER_COMMAND_NAME(@"status")
printf(" %-25s | %s\n", "File Logging", (fileLogging ? "Yes" : "No"));
printf(" %-25s | %s\n", "USB Blocking", (configurator.blockUSBMount ? "Yes" : "No"));
if (configurator.blockUSBMount && configurator.remountUSBMode.count > 0) {
printf(" %-25s | %s\n", "USB Remounting Mode:",
printf(" %-25s | %s\n", "USB Remounting Mode",
[[configurator.remountUSBMode componentsJoinedByString:@", "] UTF8String]);
}
printf(" %-25s | %s\n", "On Start USB Options",
StartupOptionToString(configurator.onStartUSBOptions).UTF8String);
printf(" %-25s | %lld (Peak: %.2f%%)\n", "Watchdog CPU Events", cpuEvents, cpuPeak);
printf(" %-25s | %lld (Peak: %.2fMB)\n", "Watchdog RAM Events", ramEvents, ramPeak);

View File

@@ -32,7 +32,7 @@ REGISTER_COMMAND_NAME(@"sync")
#pragma mark SNTCommand protocol methods
+ (BOOL)requiresRoot {
return YES;
return NO;
}
+ (BOOL)requiresDaemonConn {

View File

@@ -395,8 +395,10 @@ objc_library(
":Metrics",
":SNTEndpointSecurityClient",
":SNTEndpointSecurityEventHandler",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTDeviceEvent",
"//Source/common:SNTLogging",
"//Source/common:SNTMetricSet",
],
)
@@ -1317,6 +1319,7 @@ santa_unit_test(
":Metrics",
":MockEndpointSecurityAPI",
":SNTEndpointSecurityDeviceManager",
"//Source/common:SNTCommonEnums",
"//Source/common:SNTConfigurator",
"//Source/common:SNTDeviceEvent",
"//Source/common:TestUtils",

View File

@@ -16,6 +16,9 @@
#include <CoreFoundation/CoreFoundation.h>
#include <DiskArbitration/DiskArbitration.h>
#include <Foundation/Foundation.h>
#include <sys/mount.h>
#include <sys/param.h>
#include <sys/ucred.h>
NS_ASSUME_NONNULL_BEGIN
@@ -27,6 +30,8 @@ NS_ASSUME_NONNULL_BEGIN
@interface MockDADisk : NSObject
@property(nonatomic) NSDictionary *diskDescription;
@property(nonatomic, readwrite) NSString *name;
@property(nonatomic) BOOL wasMounted;
@property(nonatomic) BOOL wasUnmounted;
@end
typedef void (^MockDADiskAppearedCallback)(DADiskRef ref);
@@ -36,19 +41,35 @@ typedef void (^MockDADiskAppearedCallback)(DADiskRef ref);
NSMutableDictionary<NSString *, MockDADisk *> *insertedDevices;
@property(nonatomic, readwrite, nonnull)
NSMutableArray<MockDADiskAppearedCallback> *diskAppearedCallbacks;
@property(nonatomic) BOOL wasRemounted;
@property(nonatomic, nullable) dispatch_queue_t sessionQueue;
- (instancetype _Nonnull)init;
- (void)reset;
// Also triggers DADiskRegisterDiskAppearedCallback
- (void)insert:(MockDADisk *)ref bsdName:(NSString *)bsdName;
- (void)insert:(MockDADisk *)ref;
// Retrieve an initialized singleton MockDiskArbitration object
+ (instancetype _Nonnull)mockDiskArbitration;
@end
@interface MockStatfs : NSObject
@property NSString *fromName;
@property NSString *onName;
@property NSNumber *flags;
- (instancetype _Nonnull)initFrom:(NSString *)from on:(NSString *)on flags:(NSNumber *)flags;
@end
@interface MockMounts : NSObject
@property(nonatomic) NSMutableDictionary<NSString *, MockStatfs *> *mounts;
- (instancetype _Nonnull)init;
- (void)reset;
- (void)insert:(MockStatfs *)sfs;
+ (instancetype _Nonnull)mockMounts;
@end
//
// All DiskArbitration functions used in SNTEndpointSecurityDeviceManager
// and shimmed out accordingly.
@@ -81,5 +102,9 @@ void DARegisterDiskDescriptionChangedCallback(DASessionRef session,
void DASessionSetDispatchQueue(DASessionRef session, dispatch_queue_t __nullable queue);
DASessionRef __nullable DASessionCreate(CFAllocatorRef __nullable allocator);
void DADiskUnmount(DADiskRef disk, DADiskUnmountOptions options,
DADiskUnmountCallback __nullable callback, void *__nullable context);
int getmntinfo_r_np(struct statfs *__nullable *__nullable mntbufp, int flags);
CF_EXTERN_C_END
NS_ASSUME_NONNULL_END

View File

@@ -14,6 +14,9 @@
#import <Foundation/Foundation.h>
#include <stdlib.h>
#include <sys/mount.h>
#include <sys/param.h>
#include <sys/ucred.h>
#import "Source/santad/EventProviders/DiskArbitrationTestUtil.h"
@@ -37,11 +40,14 @@ NS_ASSUME_NONNULL_BEGIN
[self.insertedDevices removeAllObjects];
[self.diskAppearedCallbacks removeAllObjects];
self.sessionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
self.wasRemounted = NO;
}
- (void)insert:(MockDADisk *)ref bsdName:(NSString *)bsdName {
self.insertedDevices[bsdName] = ref;
- (void)insert:(MockDADisk *)ref {
if (!ref.diskDescription[@"DAMediaBSDName"]) {
[NSException raise:@"Missing DAMediaBSDName"
format:@"The MockDADisk is missing the DAMediaBSDName diskDescription key."];
}
self.insertedDevices[ref.diskDescription[@"DAMediaBSDName"]] = ref;
for (MockDADiskAppearedCallback callback in self.diskAppearedCallbacks) {
dispatch_sync(self.sessionQueue, ^{
@@ -62,12 +68,58 @@ NS_ASSUME_NONNULL_BEGIN
@end
@implementation MockStatfs
- (instancetype _Nonnull)initFrom:(NSString *)from on:(NSString *)on flags:(NSNumber *)flags {
self = [super init];
if (self) {
_fromName = from;
_onName = on;
_flags = flags;
}
return self;
}
@end
@implementation MockMounts
- (instancetype _Nonnull)init {
self = [super init];
if (self) {
_mounts = [NSMutableDictionary dictionary];
}
return self;
}
- (void)reset {
[self.mounts removeAllObjects];
}
- (void)insert:(MockStatfs *)sfs {
self.mounts[sfs.fromName] = sfs;
}
+ (instancetype _Nonnull)mockMounts {
static MockMounts *sharedMounts;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMounts = [[MockMounts alloc] init];
});
return sharedMounts;
}
@end
void DADiskMountWithArguments(DADiskRef _Nonnull disk, CFURLRef __nullable path,
DADiskMountOptions options, DADiskMountCallback __nullable callback,
void *__nullable context,
CFStringRef __nullable arguments[_Nullable]) {
MockDiskArbitration *mockDA = [MockDiskArbitration mockDiskArbitration];
mockDA.wasRemounted = YES;
MockDADisk *mockDisk = (__bridge MockDADisk *)disk;
mockDisk.wasMounted = YES;
if (context) {
dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context;
dispatch_semaphore_signal(sema);
}
}
DADiskRef __nullable DADiskCreateFromBSDName(CFAllocatorRef __nullable allocator,
@@ -117,4 +169,32 @@ DASessionRef __nullable DASessionCreate(CFAllocatorRef __nullable allocator) {
return (__bridge DASessionRef)[MockDiskArbitration mockDiskArbitration];
};
void DADiskUnmount(DADiskRef disk, DADiskUnmountOptions options,
DADiskUnmountCallback __nullable callback, void *__nullable context) {
MockDADisk *mockDisk = (__bridge MockDADisk *)disk;
mockDisk.wasUnmounted = YES;
dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context;
dispatch_semaphore_signal(sema);
}
int getmntinfo_r_np(struct statfs *__nullable *__nullable mntbufp, int flags) {
MockMounts *mockMounts = [MockMounts mockMounts];
struct statfs *sfs = (struct statfs *)calloc(mockMounts.mounts.count, sizeof(struct statfs));
__block NSUInteger i = 0;
[mockMounts.mounts
enumerateKeysAndObjectsUsingBlock:^(NSString *key, MockStatfs *mockSfs, BOOL *stop) {
strlcpy(sfs[i].f_mntfromname, mockSfs.fromName.UTF8String, sizeof(sfs[i].f_mntfromname));
strlcpy(sfs[i].f_mntonname, mockSfs.onName.UTF8String, sizeof(sfs[i].f_mntonname));
sfs[i].f_flags = [mockSfs.flags unsignedIntValue];
i++;
}];
*mntbufp = sfs;
return (int)mockMounts.mounts.count;
}
NS_ASSUME_NONNULL_END

View File

@@ -320,9 +320,19 @@ class EnrichedUnlink : public EnrichedEventType {
EnrichedFile target_;
};
class EnrichedCSInvalidated : public EnrichedEventType {
public:
EnrichedCSInvalidated(Message &&es_msg, EnrichedProcess &&instigator)
: EnrichedEventType(std::move(es_msg), std::move(instigator)) {}
EnrichedCSInvalidated(EnrichedCSInvalidated &&other)
: EnrichedEventType(std::move(other)) {}
EnrichedCSInvalidated(const EnrichedCSInvalidated &other) = delete;
};
using EnrichedType =
std::variant<EnrichedClose, EnrichedExchange, EnrichedExec, EnrichedExit,
EnrichedFork, EnrichedLink, EnrichedRename, EnrichedUnlink>;
EnrichedFork, EnrichedLink, EnrichedRename, EnrichedUnlink,
EnrichedCSInvalidated>;
class EnrichedMessage {
public:

View File

@@ -74,6 +74,9 @@ std::unique_ptr<EnrichedMessage> Enricher::Enrich(Message &&es_msg) {
case ES_EVENT_TYPE_NOTIFY_UNLINK:
return std::make_unique<EnrichedMessage>(EnrichedUnlink(
std::move(es_msg), Enrich(*es_msg->process), Enrich(*es_msg->event.unlink.target)));
case ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED:
return std::make_unique<EnrichedMessage>(
EnrichedCSInvalidated(std::move(es_msg), Enrich(*es_msg->process)));
default:
// This is a programming error
LOGE(@"Attempting to enrich an unhandled event type: %d", es_msg->event_type);

View File

@@ -15,6 +15,7 @@
#include <DiskArbitration/DiskArbitration.h>
#import <Foundation/Foundation.h>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTDeviceEvent.h"
#import "Source/santad/EventProviders/AuthResultCache.h"
#include "Source/santad/EventProviders/EndpointSecurity/EndpointSecurityAPI.h"
@@ -39,11 +40,16 @@ typedef void (^SNTDeviceBlockCallback)(SNTDeviceEvent *event);
@property(nonatomic, nullable) SNTDeviceBlockCallback deviceBlockCallback;
- (instancetype)
initWithESAPI:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
authResultCache:(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache;
initWithESAPI:
(std::shared_ptr<santa::santad::event_providers::endpoint_security::EndpointSecurityAPI>)
esApi
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
logger:(std::shared_ptr<santa::santad::logs::endpoint_security::Logger>)logger
authResultCache:
(std::shared_ptr<santa::santad::event_providers::AuthResultCache>)authResultCache
blockUSBMount:(BOOL)blockUSBMount
remountUSBMode:(nullable NSArray<NSString *> *)remountUSBMode
startupPreferences:(SNTDeviceManagerStartupPreferences)startupPrefs;
@end

View File

@@ -24,9 +24,12 @@
#include <errno.h>
#include <libproc.h>
#include <sys/mount.h>
#include <sys/param.h>
#include <sys/ucred.h>
#import "Source/common/SNTDeviceEvent.h"
#import "Source/common/SNTLogging.h"
#import "Source/common/SNTMetricSet.h"
#include "Source/santad/EventProviders/EndpointSecurity/Message.h"
#include "Source/santad/Metrics.h"
@@ -38,32 +41,53 @@ using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::Message;
using santa::santad::logs::endpoint_security::Logger;
// Defined operations for startup metrics:
// Device shouldn't be operated on (e.g. not a mass storage device)
static NSString *const kMetricStartupDiskOperationSkip = @"Skipped";
// Device already had appropriate flags set
static NSString *const kMetricStartupDiskOperationAllowed = @"Allowed";
// Device failed to be unmounted
static NSString *const kMetricStartupDiskOperationUnmountFailed = @"UnmountFailed";
// Device failed to be remounted
static NSString *const kMetricStartupDiskOperationRemountFailed = @"RemountFailed";
// Remounts were requested, but remount args weren't set
static NSString *const kMetricStartupDiskOperationRemountSkipped = @"RemountSkipped";
// Operations on device matching the configured startup pref wwere successful
static NSString *const kMetricStartupDiskOperationSuccess = @"Success";
@interface SNTEndpointSecurityDeviceManager ()
- (void)logDiskAppeared:(NSDictionary *)props;
- (void)logDiskDisappeared:(NSDictionary *)props;
@property SNTMetricCounter *startupDiskMetrics;
@property DASessionRef diskArbSession;
@property(nonatomic, readonly) dispatch_queue_t diskQueue;
@property dispatch_semaphore_t diskSema;
@end
void diskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
void DiskMountedCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
if (dissenter) {
DAReturn status = DADissenterGetStatus(dissenter);
NSString *statusString = (NSString *)DADissenterGetStatusString(dissenter);
IOReturn systemCode = err_get_system(status);
IOReturn subSystemCode = err_get_sub(status);
IOReturn errorCode = err_get_code(status);
LOGE(@"SNTEndpointSecurityDeviceManager: dissenter status codes: system: %d, subsystem: %d, "
@"err: %d; status: %s",
systemCode, subSystemCode, errorCode, [statusString UTF8String]);
@"err: %d; status: %@",
systemCode, subSystemCode, errorCode,
CFBridgingRelease(DADissenterGetStatusString(dissenter)));
}
if (context) {
dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context;
dispatch_semaphore_signal(sema);
}
}
void diskAppearedCallback(DADiskRef disk, void *context) {
void DiskAppearedCallback(DADiskRef disk, void *context) {
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
if (![props[@"DAVolumeMountable"] boolValue]) return;
SNTEndpointSecurityDeviceManager *dm = (__bridge SNTEndpointSecurityDeviceManager *)context;
@@ -71,7 +95,7 @@ void diskAppearedCallback(DADiskRef disk, void *context) {
[dm logDiskAppeared:props];
}
void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *context) {
void DiskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *context) {
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
if (![props[@"DAVolumeMountable"] boolValue]) return;
@@ -82,7 +106,7 @@ void diskDescriptionChangedCallback(DADiskRef disk, CFArrayRef keys, void *conte
}
}
void diskDisappearedCallback(DADiskRef disk, void *context) {
void DiskDisappearedCallback(DADiskRef disk, void *context) {
NSDictionary *props = CFBridgingRelease(DADiskCopyDescription(disk));
if (![props[@"DAVolumeMountable"] boolValue]) return;
@@ -91,7 +115,22 @@ void diskDisappearedCallback(DADiskRef disk, void *context) {
[dm logDiskDisappeared:props];
}
NSArray<NSString *> *maskToMountArgs(long remountOpts) {
void DiskUnmountCallback(DADiskRef disk, DADissenterRef dissenter, void *context) {
if (dissenter) {
LOGW(@"Unable to unmount device: %@", CFBridgingRelease(DADissenterGetStatusString(dissenter)));
} else if (disk) {
NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk));
LOGI(@"Unmounted device: Model: %@, Vendor: %@, Path: %@",
diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceModelKey],
diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceVendorKey],
diskInfo[(__bridge NSString *)kDADiskDescriptionVolumePathKey]);
}
dispatch_semaphore_t sema = (__bridge dispatch_semaphore_t)context;
dispatch_semaphore_signal(sema);
}
NSArray<NSString *> *maskToMountArgs(uint32_t remountOpts) {
NSMutableArray<NSString *> *args = [NSMutableArray array];
if (remountOpts & MNT_RDONLY) [args addObject:@"rdonly"];
if (remountOpts & MNT_NOEXEC) [args addObject:@"noexec"];
@@ -104,28 +143,29 @@ NSArray<NSString *> *maskToMountArgs(long remountOpts) {
return args;
}
long mountArgsToMask(NSArray<NSString *> *args) {
long flags = 0;
uint32_t mountArgsToMask(NSArray<NSString *> *args) {
uint32_t flags = 0;
for (NSString *i in args) {
NSString *arg = [i lowercaseString];
if ([arg isEqualToString:@"rdonly"])
if ([arg isEqualToString:@"rdonly"]) {
flags |= MNT_RDONLY;
else if ([arg isEqualToString:@"noexec"])
} else if ([arg isEqualToString:@"noexec"]) {
flags |= MNT_NOEXEC;
else if ([arg isEqualToString:@"nosuid"])
} else if ([arg isEqualToString:@"nosuid"]) {
flags |= MNT_NOSUID;
else if ([arg isEqualToString:@"nobrowse"])
} else if ([arg isEqualToString:@"nobrowse"]) {
flags |= MNT_DONTBROWSE;
else if ([arg isEqualToString:@"noowners"])
} else if ([arg isEqualToString:@"noowners"]) {
flags |= MNT_UNKNOWNPERMISSIONS;
else if ([arg isEqualToString:@"nodev"])
} else if ([arg isEqualToString:@"nodev"]) {
flags |= MNT_NODEV;
else if ([arg isEqualToString:@"-j"])
} else if ([arg isEqualToString:@"-j"]) {
flags |= MNT_JOURNALED;
else if ([arg isEqualToString:@"async"])
} else if ([arg isEqualToString:@"async"]) {
flags |= MNT_ASYNC;
else
} else {
LOGE(@"SNTEndpointSecurityDeviceManager: unexpected mount arg: %@", arg);
}
}
return flags;
}
@@ -140,25 +180,205 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithESAPI:(std::shared_ptr<EndpointSecurityAPI>)esApi
metrics:(std::shared_ptr<santa::santad::Metrics>)metrics
logger:(std::shared_ptr<Logger>)logger
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache {
authResultCache:(std::shared_ptr<AuthResultCache>)authResultCache
blockUSBMount:(BOOL)blockUSBMount
remountUSBMode:(nullable NSArray<NSString *> *)remountUSBMode
startupPreferences:(SNTDeviceManagerStartupPreferences)startupPrefs {
self = [super initWithESAPI:std::move(esApi)
metrics:std::move(metrics)
processor:santa::santad::Processor::kDeviceManager];
if (self) {
_logger = logger;
_authResultCache = authResultCache;
_blockUSBMount = false;
_blockUSBMount = blockUSBMount;
_remountArgs = remountUSBMode;
_diskQueue = dispatch_queue_create("com.google.santa.daemon.disk_queue", DISPATCH_QUEUE_SERIAL);
_diskArbSession = DASessionCreate(NULL);
DASessionSetDispatchQueue(_diskArbSession, _diskQueue);
SNTMetricInt64Gauge *startupPrefsMetric =
[[SNTMetricSet sharedInstance] int64GaugeWithName:@"/santa/device_manager/startup_preference"
fieldNames:@[]
helpText:@"The current startup preference value"];
[[SNTMetricSet sharedInstance] registerCallback:^{
[startupPrefsMetric set:startupPrefs forFieldValues:@[]];
}];
_startupDiskMetrics = [[SNTMetricSet sharedInstance]
counterWithName:@"/santa/device_manager/startup_disk_operation"
fieldNames:@[ @"operation" ]
helpText:@"Count of the number of USB devices encountered per operation"];
[self performStartupTasks:startupPrefs];
[self establishClientOrDie];
}
return self;
}
- (uint32_t)updatedMountFlags:(struct statfs *)sfs {
uint32_t mask = sfs->f_flags | mountArgsToMask(self.remountArgs);
// NB: APFS mounts get MNT_JOURNALED implicitly set. However, mount_apfs
// does not support the `-j` option so this flag needs to be cleared.
if (strncmp(sfs->f_fstypename, "apfs", sizeof(sfs->f_fstypename)) == 0) {
mask &= ~MNT_JOURNALED;
}
return mask;
}
- (BOOL)shouldOperateOnDisk:(DADiskRef)disk {
NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk));
BOOL isInternal = [diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceInternalKey] boolValue];
BOOL isRemovable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaRemovableKey] boolValue];
BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue];
NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey];
BOOL isUSB = [protocol isEqualToString:@"USB"];
BOOL isSecureDigital = [protocol isEqualToString:@"Secure Digital"];
BOOL isVirtual = [protocol isEqualToString:@"Virtual Interface"];
NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey];
// TODO: check kind and protocol for banned things (e.g. MTP).
LOGD(@"SNTEndpointSecurityDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d "
@"isRemovable: %d isEjectable: %d",
protocol, kind, isInternal, isRemovable, isEjectable);
// if the device is internal, or virtual *AND* is not an SD Card,
// then allow the mount. This is to ensure we block SD cards inserted into
// the internal reader of some Macs, whilst also ensuring we don't block
// the internal storage device.
if ((isInternal || isVirtual) && !isSecureDigital) {
return false;
}
// We are okay with operations for devices that are non-removable as long as
// they are NOT a USB device, or an SD Card.
if (!isRemovable && !isEjectable && !isUSB && !isSecureDigital) {
return false;
}
return true;
}
- (BOOL)haveRemountArgs {
return [self.remountArgs count] > 0;
}
- (BOOL)remountUSBModeContainsFlags:(uint32_t)flags {
if (![self haveRemountArgs]) {
return false;
}
uint32_t requiredFlags = mountArgsToMask(self.remountArgs);
LOGD(@" Got mount flags: 0x%08x | %@", flags, maskToMountArgs(flags));
LOGD(@"Want mount flags: 0x%08x | %@", mountArgsToMask(self.remountArgs), self.remountArgs);
return (flags & requiredFlags) == requiredFlags;
}
- (void)incrementStartupMetricsOperation:(NSString *)op {
[self.startupDiskMetrics incrementForFieldValues:@[ op ]];
}
// NB: Remount options are implemented as separate "unmount" and "mount"
// operations instead of using the "update"/MNT_UPDATE flag. This is because
// filesystems often don't support many transitions (e.g. RW to RO). Performing
// the two step process has a higher chance of succeeding.
- (void)performStartupTasks:(SNTDeviceManagerStartupPreferences)startupPrefs {
if (!self.blockUSBMount || (startupPrefs != SNTDeviceManagerStartupPreferencesUnmount &&
startupPrefs != SNTDeviceManagerStartupPreferencesForceUnmount &&
startupPrefs != SNTDeviceManagerStartupPreferencesRemount &&
startupPrefs != SNTDeviceManagerStartupPreferencesForceRemount)) {
return;
}
struct statfs *mnts;
int numMounts = getmntinfo_r_np(&mnts, MNT_WAIT);
if (numMounts == 0) {
LOGE(@"Failed to get mount info: %d: %s", errno, strerror(errno));
return;
}
self.diskSema = dispatch_semaphore_create(0);
for (int i = 0; i < numMounts; i++) {
struct statfs *sfs = &mnts[i];
DADiskRef disk = DADiskCreateFromBSDName(NULL, self.diskArbSession, sfs->f_mntfromname);
if (!disk) {
LOGW(@"Unable to create disk reference for device: '%s' -> '%s'", sfs->f_mntfromname,
sfs->f_mntonname);
continue;
}
CFAutorelease(disk);
if (![self shouldOperateOnDisk:disk]) {
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationSkip];
continue;
}
if ([self remountUSBModeContainsFlags:sfs->f_flags]) {
LOGI(@"Allowing existing mount as flags contain RemountUSBMode. '%s' -> '%s'",
sfs->f_mntfromname, sfs->f_mntonname);
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationAllowed];
continue;
}
DADiskUnmountOptions unmountOptions = kDADiskUnmountOptionDefault;
if (startupPrefs == SNTDeviceManagerStartupPreferencesForceUnmount ||
startupPrefs == SNTDeviceManagerStartupPreferencesForceRemount) {
unmountOptions = kDADiskUnmountOptionForce;
}
LOGI(@"Attempting to unmount device: '%s' mounted on '%s'", sfs->f_mntfromname,
sfs->f_mntonname);
DADiskUnmount(disk, unmountOptions, DiskUnmountCallback, (__bridge void *)self.diskSema);
if (dispatch_semaphore_wait(self.diskSema,
dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
LOGW(
@"Unmounting '%s' mounted on '%s' took longer than expected. Device may still be mounted.",
sfs->f_mntfromname, sfs->f_mntonname);
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationUnmountFailed];
continue;
}
if (startupPrefs == SNTDeviceManagerStartupPreferencesRemount ||
startupPrefs == SNTDeviceManagerStartupPreferencesForceRemount) {
if (![self haveRemountArgs]) {
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationRemountSkipped];
LOGW(@"Remount requested during startup, but no remount args set. Leaving unmounted.");
continue;
}
uint32_t newMode = [self updatedMountFlags:sfs];
LOGI(@"Attempting to mount device again changing flags: 0x%08x --> 0x%08x", sfs->f_flags,
newMode);
[self remount:disk mountMode:newMode semaphore:self.diskSema];
if (dispatch_semaphore_wait(self.diskSema,
dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC))) {
LOGW(@"Failed to remount device after unmounting: %s", sfs->f_mntfromname);
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationRemountFailed];
continue;
}
}
[self incrementStartupMetricsOperation:kMetricStartupDiskOperationSuccess];
}
}
- (void)logDiskAppeared:(NSDictionary *)props {
self->_logger->LogDiskAppeared(props);
}
@@ -199,11 +419,11 @@ NS_ASSUME_NONNULL_BEGIN
}
- (void)enable {
DARegisterDiskAppearedCallback(_diskArbSession, NULL, diskAppearedCallback,
DARegisterDiskAppearedCallback(_diskArbSession, NULL, DiskAppearedCallback,
(__bridge void *)self);
DARegisterDiskDescriptionChangedCallback(_diskArbSession, NULL, NULL,
diskDescriptionChangedCallback, (__bridge void *)self);
DARegisterDiskDisappearedCallback(_diskArbSession, NULL, diskDisappearedCallback,
DiskDescriptionChangedCallback, (__bridge void *)self);
DARegisterDiskDisappearedCallback(_diskArbSession, NULL, DiskDisappearedCallback,
(__bridge void *)self);
[super subscribeAndClearCache:{
@@ -225,44 +445,15 @@ NS_ASSUME_NONNULL_BEGIN
exit(EXIT_FAILURE);
}
long mountMode = eventStatFS->f_flags;
pid_t pid = audit_token_to_pid(m->process->audit_token);
LOGD(
@"SNTEndpointSecurityDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %lu",
m->process->executable->path.data, pid, mountMode);
@"SNTEndpointSecurityDeviceManager: mount syscall arriving from path: %s, pid: %d, fflags: %u",
m->process->executable->path.data, pid, eventStatFS->f_flags);
DADiskRef disk = DADiskCreateFromBSDName(NULL, self.diskArbSession, eventStatFS->f_mntfromname);
CFAutorelease(disk);
// TODO(tnek): Log all of the other attributes available in diskInfo into a structured log format.
NSDictionary *diskInfo = CFBridgingRelease(DADiskCopyDescription(disk));
BOOL isInternal = [diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceInternalKey] boolValue];
BOOL isRemovable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaRemovableKey] boolValue];
BOOL isEjectable = [diskInfo[(__bridge NSString *)kDADiskDescriptionMediaEjectableKey] boolValue];
NSString *protocol = diskInfo[(__bridge NSString *)kDADiskDescriptionDeviceProtocolKey];
BOOL isUSB = [protocol isEqualToString:@"USB"];
BOOL isSecureDigital = [protocol isEqualToString:@"Secure Digital"];
BOOL isVirtual = [protocol isEqualToString:@"Virtual Interface"];
NSString *kind = diskInfo[(__bridge NSString *)kDADiskDescriptionMediaKindKey];
// TODO: check kind and protocol for banned things (e.g. MTP).
LOGD(@"SNTEndpointSecurityDeviceManager: DiskInfo Protocol: %@ Kind: %@ isInternal: %d "
@"isRemovable: %d "
@"isEjectable: %d",
protocol, kind, isInternal, isRemovable, isEjectable);
// if the device is internal, or virtual *AND* is not an SD Card,
// then allow the mount. This is to ensure we block SD cards inserted into
// the internal reader of some Macs, whilst also ensuring we don't block
// the internal storage device.
if ((isInternal || isVirtual) && !isSecureDigital) {
return ES_AUTH_RESULT_ALLOW;
}
// We are okay with operations for devices that are non-removable as long as
// they are NOT a USB device, or an SD Card.
if (!isRemovable && !isEjectable && !isUSB && !isSecureDigital) {
if (![self shouldOperateOnDisk:disk]) {
return ES_AUTH_RESULT_ALLOW;
}
@@ -270,24 +461,20 @@ NS_ASSUME_NONNULL_BEGIN
initWithOnName:[NSString stringWithUTF8String:eventStatFS->f_mntonname]
fromName:[NSString stringWithUTF8String:eventStatFS->f_mntfromname]];
BOOL shouldRemount = self.remountArgs != nil && [self.remountArgs count] > 0;
if (shouldRemount) {
if ([self haveRemountArgs]) {
event.remountArgs = self.remountArgs;
long remountOpts = mountArgsToMask(self.remountArgs);
LOGD(@"SNTEndpointSecurityDeviceManager: mountMode: %@", maskToMountArgs(mountMode));
LOGD(@"SNTEndpointSecurityDeviceManager: remountOpts: %@", maskToMountArgs(remountOpts));
if ((mountMode & remountOpts) == remountOpts && m->event_type != ES_EVENT_TYPE_AUTH_REMOUNT) {
LOGD(@"SNTEndpointSecurityDeviceManager: Allowing as mount as flags match remountOpts");
if ([self remountUSBModeContainsFlags:eventStatFS->f_flags] &&
m->event_type != ES_EVENT_TYPE_AUTH_REMOUNT) {
LOGD(@"Allowing mount as flags contain RemountUSBMode. '%s' -> '%s'",
eventStatFS->f_mntfromname, eventStatFS->f_mntonname);
return ES_AUTH_RESULT_ALLOW;
}
long newMode = mountMode | remountOpts;
LOGI(@"SNTEndpointSecurityDeviceManager: remounting device '%s'->'%s', flags (%lu) -> (%lu)",
eventStatFS->f_mntfromname, eventStatFS->f_mntonname, mountMode, newMode);
[self remount:disk mountMode:newMode];
uint32_t newMode = [self updatedMountFlags:eventStatFS];
LOGI(@"SNTEndpointSecurityDeviceManager: remounting device '%s'->'%s', flags (%u) -> (%u)",
eventStatFS->f_mntfromname, eventStatFS->f_mntonname, eventStatFS->f_flags, newMode);
[self remount:disk mountMode:newMode semaphore:nil];
}
if (self.deviceBlockCallback) {
@@ -297,14 +484,16 @@ NS_ASSUME_NONNULL_BEGIN
return ES_AUTH_RESULT_DENY;
}
- (void)remount:(DADiskRef)disk mountMode:(long)remountMask {
- (void)remount:(DADiskRef)disk
mountMode:(uint32_t)remountMask
semaphore:(nullable dispatch_semaphore_t)sema {
NSArray<NSString *> *args = maskToMountArgs(remountMask);
CFStringRef *argv = (CFStringRef *)calloc(args.count + 1, sizeof(CFStringRef));
CFArrayGetValues((__bridge CFArrayRef)args, CFRangeMake(0, (CFIndex)args.count),
(const void **)argv);
DADiskMountWithArguments(disk, NULL, kDADiskMountOptionDefault, diskMountedCallback,
(__bridge void *)self, (CFStringRef *)argv);
DADiskMountWithArguments(disk, NULL, kDADiskMountOptionDefault, DiskMountedCallback,
(__bridge void *)sema, (CFStringRef *)argv);
free(argv);
}

View File

@@ -26,6 +26,7 @@
#include <memory>
#include <set>
#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigurator.h"
#import "Source/common/SNTDeviceEvent.h"
#include "Source/common/TestUtils.h"
@@ -50,12 +51,17 @@ class MockAuthResultCache : public AuthResultCache {
};
@interface SNTEndpointSecurityDeviceManager (Testing)
- (instancetype)init;
- (void)logDiskAppeared:(NSDictionary *)props;
- (BOOL)shouldOperateOnDisk:(DADiskRef)disk;
- (void)performStartupTasks:(SNTDeviceManagerStartupPreferences)startupPrefs;
- (uint32_t)updatedMountFlags:(struct statfs *)sfs;
@end
@interface SNTEndpointSecurityDeviceManagerTest : XCTestCase
@property id mockConfigurator;
@property MockDiskArbitration *mockDA;
@property MockMounts *mockMounts;
@end
@implementation SNTEndpointSecurityDeviceManagerTest
@@ -70,6 +76,9 @@ class MockAuthResultCache : public AuthResultCache {
self.mockDA = [MockDiskArbitration mockDiskArbitration];
[self.mockDA reset];
self.mockMounts = [MockMounts mockMounts];
[self.mockMounts reset];
fclose(stdout);
}
@@ -112,7 +121,10 @@ class MockAuthResultCache : public AuthResultCache {
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
metrics:nullptr
logger:nullptr
authResultCache:nullptr];
authResultCache:nullptr
blockUSBMount:false
remountUSBMode:nil
startupPreferences:SNTDeviceManagerStartupPreferencesNone];
setupDMCallback(deviceManager);
@@ -120,7 +132,7 @@ class MockAuthResultCache : public AuthResultCache {
id partialDeviceManager = OCMPartialMock(deviceManager);
OCMStub([partialDeviceManager logDiskAppeared:OCMOCK_ANY]);
[self.mockDA insert:disk bsdName:test_mntfromname];
[self.mockDA insert:disk];
es_file_t file = MakeESFile("foo");
es_process_t proc = MakeESProcess(&file);
@@ -211,7 +223,8 @@ class MockAuthResultCache : public AuthResultCache {
};
}];
XCTAssertEqual(self.mockDA.wasRemounted, YES);
XCTAssertEqual(self.mockDA.insertedDevices.count, 1);
XCTAssertTrue([self.mockDA.insertedDevices allValues][0].wasMounted);
[self waitForExpectations:@[ expectation ] timeout:60.0];
@@ -274,7 +287,8 @@ class MockAuthResultCache : public AuthResultCache {
};
}];
XCTAssertEqual(self.mockDA.wasRemounted, YES);
XCTAssertEqual(self.mockDA.insertedDevices.count, 1);
XCTAssertTrue([self.mockDA.insertedDevices allValues][0].wasMounted);
[self waitForExpectations:@[ expectation ] timeout:10.0];
@@ -303,7 +317,8 @@ class MockAuthResultCache : public AuthResultCache {
};
}];
XCTAssertEqual(self.mockDA.wasRemounted, NO);
XCTAssertEqual(self.mockDA.insertedDevices.count, 1);
XCTAssertFalse([self.mockDA.insertedDevices allValues][0].wasMounted);
}
- (void)testNotifyUnmountFlushesCache {
@@ -324,7 +339,10 @@ class MockAuthResultCache : public AuthResultCache {
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:mockESApi
metrics:nullptr
logger:nullptr
authResultCache:mockAuthCache];
authResultCache:mockAuthCache
blockUSBMount:YES
remountUSBMode:nil
startupPreferences:SNTDeviceManagerStartupPreferencesNone];
deviceManager.blockUSBMount = YES;
@@ -340,6 +358,122 @@ class MockAuthResultCache : public AuthResultCache {
XCTBubbleMockVerifyAndClearExpectations(mockAuthCache.get());
}
- (void)testPerformStartupTasks {
SNTEndpointSecurityDeviceManager *deviceManager = [[SNTEndpointSecurityDeviceManager alloc] init];
id partialDeviceManager = OCMPartialMock(deviceManager);
OCMStub([partialDeviceManager shouldOperateOnDisk:nil]).ignoringNonObjectArgs().andReturn(YES);
deviceManager.blockUSBMount = YES;
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
[self.mockMounts insert:[[MockStatfs alloc] initFrom:@"d1" on:@"v1" flags:@(0x0)]];
[self.mockMounts insert:[[MockStatfs alloc] initFrom:@"d2"
on:@"v2"
flags:@(MNT_RDONLY | MNT_NOEXEC | MNT_JOURNALED)]];
// Create mock disks with desired args
MockDADisk * (^CreateMockDisk)(NSString *, NSString *) =
^MockDADisk *(NSString *mountOn, NSString *mountFrom) {
MockDADisk *mockDisk = [[MockDADisk alloc] init];
mockDisk.diskDescription = @{
@"DAVolumePath" : mountOn, // f_mntonname,
@"DADevicePath" : mountOn, // f_mntonname,
@"DAMediaBSDName" : mountFrom, // f_mntfromname,
};
return mockDisk;
};
// Reset the Mock DA property, setup disks and remount args, then trigger the test
void (^PerformStartupTest)(NSArray<MockDADisk *> *, NSArray<NSString *> *,
SNTDeviceManagerStartupPreferences) =
^void(NSArray<MockDADisk *> *disks, NSArray<NSString *> *remountArgs,
SNTDeviceManagerStartupPreferences startupPref) {
[self.mockDA reset];
for (MockDADisk *d in disks) {
[self.mockDA insert:d];
}
deviceManager.remountArgs = remountArgs;
[deviceManager performStartupTasks:startupPref];
};
// Unmount with RemountUSBMode set
{
MockDADisk *disk1 = CreateMockDisk(@"v1", @"d1");
MockDADisk *disk2 = CreateMockDisk(@"v2", @"d2");
PerformStartupTest(@[ disk1, disk2 ], @[ @"noexec", @"rdonly" ],
SNTDeviceManagerStartupPreferencesUnmount);
XCTAssertTrue(disk1.wasUnmounted);
XCTAssertFalse(disk1.wasMounted);
XCTAssertFalse(disk2.wasUnmounted);
XCTAssertFalse(disk2.wasMounted);
}
// Unmount with RemountUSBMode nil
{
MockDADisk *disk1 = CreateMockDisk(@"v1", @"d1");
MockDADisk *disk2 = CreateMockDisk(@"v2", @"d2");
PerformStartupTest(@[ disk1, disk2 ], nil, SNTDeviceManagerStartupPreferencesUnmount);
XCTAssertTrue(disk1.wasUnmounted);
XCTAssertFalse(disk1.wasMounted);
XCTAssertTrue(disk2.wasUnmounted);
XCTAssertFalse(disk2.wasMounted);
}
// Remount with RemountUSBMode set
{
MockDADisk *disk1 = CreateMockDisk(@"v1", @"d1");
MockDADisk *disk2 = CreateMockDisk(@"v2", @"d2");
PerformStartupTest(@[ disk1, disk2 ], @[ @"noexec", @"rdonly" ],
SNTDeviceManagerStartupPreferencesRemount);
XCTAssertTrue(disk1.wasUnmounted);
XCTAssertTrue(disk1.wasMounted);
XCTAssertFalse(disk2.wasUnmounted);
XCTAssertFalse(disk2.wasMounted);
}
// Unmount with RemountUSBMode nil
{
MockDADisk *disk1 = CreateMockDisk(@"v1", @"d1");
MockDADisk *disk2 = CreateMockDisk(@"v2", @"d2");
PerformStartupTest(@[ disk1, disk2 ], nil, SNTDeviceManagerStartupPreferencesRemount);
XCTAssertTrue(disk1.wasUnmounted);
XCTAssertFalse(disk1.wasMounted);
XCTAssertTrue(disk2.wasUnmounted);
XCTAssertFalse(disk2.wasMounted);
}
}
- (void)testUpdatedMountFlags {
struct statfs sfs;
strlcpy(sfs.f_fstypename, "foo", sizeof(sfs.f_fstypename));
sfs.f_flags = MNT_JOURNALED | MNT_NOSUID | MNT_NODEV;
SNTEndpointSecurityDeviceManager *deviceManager = [[SNTEndpointSecurityDeviceManager alloc] init];
deviceManager.remountArgs = @[ @"noexec", @"rdonly" ];
// For most filesystems, the flags are the union of what is in statfs and the remount args
XCTAssertEqual([deviceManager updatedMountFlags:&sfs], sfs.f_flags | MNT_RDONLY | MNT_NOEXEC);
// For APFS, flags are still unioned, but MNT_JOUNRNALED is cleared
strlcpy(sfs.f_fstypename, "apfs", sizeof(sfs.f_fstypename));
XCTAssertEqual([deviceManager updatedMountFlags:&sfs],
(sfs.f_flags | MNT_RDONLY | MNT_NOEXEC) & ~MNT_JOURNALED);
}
- (void)testEnable {
// Ensure the client subscribes to expected event types
std::set<es_event_type_t> expectedEventSubs{

View File

@@ -55,6 +55,8 @@ class BasicString : public Serializer {
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &) override;
std::vector<uint8_t> SerializeFileAccess(
const std::string &policy_version, const std::string &policy_name,

View File

@@ -37,6 +37,7 @@
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::EnrichedClose;
using santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated;
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
using santa::santad::event_providers::endpoint_security::EnrichedExec;
using santa::santad::event_providers::endpoint_security::EnrichedExit;
@@ -412,6 +413,19 @@ std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedUnlink &msg) {
return FinalizeString(str);
}
std::vector<uint8_t> BasicString::SerializeMessage(const EnrichedCSInvalidated &msg) {
const es_message_t &esm = msg.es_msg();
std::string str = CreateDefaultString();
str.append("action=CODESIGNING_INVALIDATED");
AppendProcess(str, esm.process);
AppendUserGroup(str, esm.process->audit_token, msg.instigator().real_user(),
msg.instigator().real_group());
str.append("|codesigning_flags=");
str.append([NSString stringWithFormat:@"0x%08x", esm.process->codesigning_flags].UTF8String);
return FinalizeString(str);
}
std::vector<uint8_t> BasicString::SerializeFileAccess(const std::string &policy_version,
const std::string &policy_name,
const Message &msg,

View File

@@ -251,6 +251,20 @@ std::string BasicStringSerializeMessage(es_message_t *esMsg) {
XCTAssertCppStringEqual(got, want);
}
- (void)testSerializeMessageCSInvalidated {
es_file_t procFile = MakeESFile("foo");
es_process_t proc = MakeESProcess(&procFile, MakeAuditToken(12, 34), MakeAuditToken(56, 78));
es_message_t esMsg = MakeESMessage(ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED, &proc);
std::string got = BasicStringSerializeMessage(&esMsg);
std::string want =
"action=CODESIGNING_INVALIDATED"
"|pid=12|ppid=56|process=foo|processpath=foo"
"|uid=-2|user=nobody|gid=-1|group=nogroup|codesigning_flags=0x00000000|machineid=my_id\n";
XCTAssertCppStringEqual(got, want);
}
- (void)testGetAccessTypeString {
std::map<es_event_type_t, std::string> accessTypeToString = {
{ES_EVENT_TYPE_AUTH_OPEN, "OPEN"}, {ES_EVENT_TYPE_AUTH_LINK, "LINK"},

View File

@@ -47,6 +47,8 @@ class Empty : public Serializer {
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &) override;
std::vector<uint8_t> SerializeFileAccess(
const std::string &policy_version, const std::string &policy_name,

View File

@@ -15,6 +15,7 @@
#include "Source/santad/Logs/EndpointSecurity/Serializers/Empty.h"
using santa::santad::event_providers::endpoint_security::EnrichedClose;
using santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated;
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
using santa::santad::event_providers::endpoint_security::EnrichedExec;
using santa::santad::event_providers::endpoint_security::EnrichedExit;
@@ -65,6 +66,10 @@ std::vector<uint8_t> Empty::SerializeMessage(const EnrichedUnlink &msg) {
return {};
}
std::vector<uint8_t> Empty::SerializeMessage(const EnrichedCSInvalidated &msg) {
return {};
}
std::vector<uint8_t> Empty::SerializeFileAccess(const std::string &policy_version,
const std::string &policy_name, const Message &msg,
const EnrichedProcess &enriched_process,

View File

@@ -43,6 +43,7 @@ namespace es = santa::santad::event_providers::endpoint_security;
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedLink *)&fake).size(), 0);
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedRename *)&fake).size(), 0);
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedUnlink *)&fake).size(), 0);
XCTAssertEqual(e->SerializeMessage(*(es::EnrichedCSInvalidated *)&fake).size(), 0);
XCTAssertEqual(e->SerializeAllowlist(*(es::Message *)&fake, "").size(), 0);
XCTAssertEqual(e->SerializeBundleHashingEvent(nil).size(), 0);

View File

@@ -56,6 +56,8 @@ class Protobuf : public Serializer {
const santa::santad::event_providers::endpoint_security::EnrichedRename &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) override;
std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &) override;
std::vector<uint8_t> SerializeFileAccess(
const std::string &policy_version, const std::string &policy_name,

View File

@@ -46,6 +46,7 @@ using google::protobuf::json::MessageToJsonString;
using santa::common::NSStringToUTF8StringView;
using santa::santad::event_providers::endpoint_security::EndpointSecurityAPI;
using santa::santad::event_providers::endpoint_security::EnrichedClose;
using santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated;
using santa::santad::event_providers::endpoint_security::EnrichedEventType;
using santa::santad::event_providers::endpoint_security::EnrichedExchange;
using santa::santad::event_providers::endpoint_security::EnrichedExec;
@@ -600,6 +601,17 @@ std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedUnlink &msg) {
return FinalizeProto(santa_msg);
}
std::vector<uint8_t> Protobuf::SerializeMessage(const EnrichedCSInvalidated &msg) {
Arena arena;
::pbv1::SantaMessage *santa_msg = CreateDefaultProto(&arena, msg);
::pbv1::CodesigningInvalidated *pb_cs_invalidated = santa_msg->mutable_codesigning_invalidated();
EncodeProcessInfoLight(pb_cs_invalidated->mutable_instigator(), msg.es_msg().version,
msg.es_msg().process, msg.instigator());
return FinalizeProto(santa_msg);
}
std::vector<uint8_t> Protobuf::SerializeFileAccess(const std::string &policy_version,
const std::string &policy_name,
const Message &msg,

View File

@@ -110,6 +110,7 @@ NSString *EventTypeToFilename(es_event_type_t eventType) {
case ES_EVENT_TYPE_NOTIFY_LINK: return @"link.json";
case ES_EVENT_TYPE_NOTIFY_RENAME: return @"rename.json";
case ES_EVENT_TYPE_NOTIFY_UNLINK: return @"unlink.json";
case ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED: return @"cs_invalidated.json";
default: XCTFail(@"Unhandled event type: %d", eventType); return nil;
}
}
@@ -145,6 +146,7 @@ const google::protobuf::Message &SantaMessageEvent(const ::pbv1::SantaMessage &s
case ::pbv1::SantaMessage::kBundle: return santaMsg.bundle();
case ::pbv1::SantaMessage::kAllowlist: return santaMsg.allowlist();
case ::pbv1::SantaMessage::kFileAccess: return santaMsg.file_access();
case ::pbv1::SantaMessage::kCodesigningInvalidated: return santaMsg.codesigning_invalidated();
case ::pbv1::SantaMessage::EVENT_NOT_SET:
XCTFail(@"Protobuf message SantaMessage did not set an 'event' field");
OS_FALLTHROUGH;
@@ -668,6 +670,14 @@ void SerializeAndCheckNonESEvents(
json:NO];
}
- (void)testSerializeMessageCodesigningInvalidated {
[self serializeAndCheckEvent:ES_EVENT_TYPE_NOTIFY_CS_INVALIDATED
messageSetup:^(std::shared_ptr<MockEndpointSecurityAPI> mockESApi,
es_message_t *esMsg) {
}
json:NO];
}
- (void)testGetAccessType {
std::map<es_event_type_t, ::pbv1::FileAccess::AccessType> eventTypeToAccessType = {
{ES_EVENT_TYPE_AUTH_CLONE, ::pbv1::FileAccess::ACCESS_TYPE_CLONE},

View File

@@ -61,6 +61,8 @@ class Serializer {
const santa::santad::event_providers::endpoint_security::EnrichedRename &) = 0;
virtual std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &) = 0;
virtual std::vector<uint8_t> SerializeMessage(
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &) = 0;
virtual std::vector<uint8_t> SerializeFileAccess(
const std::string &policy_version, const std::string &policy_name,
@@ -95,6 +97,8 @@ class Serializer {
const santa::santad::event_providers::endpoint_security::EnrichedRename &);
std::vector<uint8_t> SerializeMessageTemplate(
const santa::santad::event_providers::endpoint_security::EnrichedUnlink &);
std::vector<uint8_t> SerializeMessageTemplate(
const santa::santad::event_providers::endpoint_security::EnrichedCSInvalidated &);
bool enabled_machine_id_ = false;
std::string machine_id_;

View File

@@ -74,5 +74,8 @@ std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedRena
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedUnlink &msg) {
return SerializeMessage(msg);
}
std::vector<uint8_t> Serializer::SerializeMessageTemplate(const es::EnrichedCSInvalidated &msg) {
return SerializeMessage(msg);
}
}; // namespace santa::santad::logs::endpoint_security::serializers

View File

@@ -167,7 +167,7 @@ std::shared_ptr<Metrics> Metrics::Create(SNTMetricSet *metric_set, uint64_t inte
fieldNames:@[ @"Processor" ]
helpText:@"Events rate limited by each processor"];
SNTMetricCounter *faa_event_counts = [[SNTMetricSet sharedInstance]
SNTMetricCounter *faa_event_counts = [metric_set
counterWithName:@"/santa/file_access_authorizer/log/count"
fieldNames:@[
@"config_version", @"access_type", @"rule_id", @"status", @"operation", @"decision"

View File

@@ -103,10 +103,11 @@ void SantadMain(std::shared_ptr<EndpointSecurityAPI> esapi, std::shared_ptr<Logg
[[SNTEndpointSecurityDeviceManager alloc] initWithESAPI:esapi
metrics:metrics
logger:logger
authResultCache:auth_result_cache];
authResultCache:auth_result_cache
blockUSBMount:[configurator blockUSBMount]
remountUSBMode:[configurator remountUSBMode]
startupPreferences:[configurator onStartUSBOptions]];
device_client.blockUSBMount = [configurator blockUSBMount];
device_client.remountArgs = [configurator remountUSBMode];
device_client.deviceBlockCallback = ^(SNTDeviceEvent *event) {
[[notifier_queue.notifierConnection remoteObjectProxy]
postUSBBlockNotification:event

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,3 +32,13 @@ run_command(
name = "allow_sysex",
cmd = "osascript $${BUILD_WORKSPACE_DIRECTORY}/Testing/integration/allow_sysex.scpt",
)
run_command(
name = "dismiss_santa_popup",
cmd = "osascript $${BUILD_WORKSPACE_DIRECTORY}/Testing/integration/dismiss_santa_popup.scpt",
)
run_command(
name = "dismiss_usb_popup",
cmd = "osascript $${BUILD_WORKSPACE_DIRECTORY}/Testing/integration/dismiss_usb_popup.scpt",
)

View File

@@ -44,7 +44,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
(NSString *)bundleDir;
+ (VZVirtualMachine *)createVirtualMachineWithBundleDir:(NSString *)bundleDir;
+ (VZVirtualMachine *)createVirtualMachineWithBundleDir:(NSString *)bundleDir
roDisk:(NSString *)roDisk;
roDisk:(NSString *)roDisk
usbDisk:(NSString *)usbDisk;
@end

View File

@@ -75,6 +75,21 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
return disk;
}
+ (VZUSBMassStorageDeviceConfiguration *)createUSBDeviceConfigurationForDisk:(NSURL *)diskURL
readOnly:(BOOL)ro {
NSError *error;
VZDiskImageStorageDeviceAttachment *diskAttachment =
[[VZDiskImageStorageDeviceAttachment alloc] initWithURL:diskURL readOnly:ro error:&error];
if (!diskAttachment) {
NSLog(@"Failed to create VZDiskImageStorageDeviceAttachment: %@", error.localizedDescription);
exit(-1);
}
VZUSBMassStorageDeviceConfiguration *disk =
[[VZUSBMassStorageDeviceConfiguration alloc] initWithAttachment:diskAttachment];
return disk;
}
+ (VZVirtioNetworkDeviceConfiguration *)createNetworkDeviceConfiguration {
VZNATNetworkDeviceAttachment *natAttachment = [[VZNATNetworkDeviceAttachment alloc] init];
VZVirtioNetworkDeviceConfiguration *networkConfiguration =
@@ -189,15 +204,22 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
}
+ (VZVirtualMachine *)createVirtualMachineWithBundleDir:(NSString *)bundleDir
roDisk:(NSString *)roDisk {
roDisk:(NSString *)roDisk
usbDisk:(NSString *)usbDisk {
VZVirtualMachineConfiguration *configuration =
[self createBaseVirtualMachineConfigurationWithBundleDir:bundleDir];
if (roDisk) {
if (roDisk && ![roDisk isEqualToString:@""]) {
configuration.storageDevices = [configuration.storageDevices
arrayByAddingObject:[self createBlockDeviceConfigurationForDisk:[[NSURL alloc]
initFileURLWithPath:roDisk]
readOnly:YES]];
}
if (usbDisk && ![usbDisk isEqualToString:@""]) {
configuration.storageDevices = [configuration.storageDevices
arrayByAddingObject:[self createUSBDeviceConfigurationForDisk:[[NSURL alloc]
initFileURLWithPath:usbDisk]
readOnly:NO]];
}
NSError *error;
if (![configuration validateWithError:&error]) {
NSLog(@"Failed to validate configuration: %@", error.localizedDescription);
@@ -208,7 +230,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
}
+ (VZVirtualMachine *)createVirtualMachineWithBundleDir:(NSString *)bundleDir {
return [self createVirtualMachineWithBundleDir:bundleDir roDisk:nil];
return [self createVirtualMachineWithBundleDir:bundleDir roDisk:nil usbDisk:nil];
}
@end

View File

@@ -22,7 +22,7 @@ macos_application(
bundle_id = "com.google.santa.e2e.vmcli",
entitlements = "//Testing/integration/VM/Common:entitlements",
infoplists = ["//Testing/integration/VM/Common:plist"],
minimum_os_version = "12.0",
minimum_os_version = "13.0",
deps = [
":vmcli_lib",
],

View File

@@ -40,8 +40,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@end
int main(int argc, const char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s bundle_path", argv[0]);
if (argc < 2) {
fprintf(stderr, "Usage: %s bundle_path [usb_disk]", argv[0]);
exit(-1);
}
@@ -50,8 +50,15 @@ int main(int argc, const char *argv[]) {
bundleDir = [bundleDir stringByAppendingString:@"/"];
}
NSString *usbDisk;
if (argc > 2) {
usbDisk = @(argv[2]);
}
VZVirtualMachine *vm =
[MacOSVirtualMachineConfigurationHelper createVirtualMachineWithBundleDir:bundleDir];
[MacOSVirtualMachineConfigurationHelper createVirtualMachineWithBundleDir:bundleDir
roDisk:nil
usbDisk:usbDisk];
MacOSVirtualMachineDelegate *delegate = [MacOSVirtualMachineDelegate new];
vm.delegate = delegate;

View File

@@ -47,28 +47,37 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
#ifdef __arm64__
dispatch_async(dispatch_get_main_queue(), ^{
NSArray *args = [[NSProcessInfo processInfo] arguments];
NSMutableArray *args = [NSMutableArray arrayWithArray:[[NSProcessInfo processInfo] arguments]];
VZMacOSVirtualMachineStartOptions *options = [VZMacOSVirtualMachineStartOptions new];
NSString *bundleDir;
NSString *roDisk;
NSString *usbDisk;
if (args.count < 2) {
abortWithErrorMessage(@"Usage: VMGUI [-recovery] bundle_path [ro_disk]");
[args removeObjectAtIndex:0];
if (args.count == 0) {
abortWithErrorMessage(@"Usage: VMGUI [-recovery] bundle_path [ro_disk] [usb_disk]");
}
int bundleArg = 1;
if ([args[1] isEqualToString:@"-recovery"]) {
if ([args[0] isEqualToString:@"-recovery"]) {
options.startUpFromMacOSRecovery = YES;
if (args.count < 3) {
abortWithErrorMessage(@"Usage: VMGUI [-recovery] bundle_path [ro_disk]");
[args removeObjectAtIndex:0];
if (args.count == 0) {
abortWithErrorMessage(@"Usage: VMGUI [-recovery] bundle_path [ro_disk] [usb_disk]");
}
bundleArg = 2;
}
bundleDir = args[bundleArg];
if (args.count > bundleArg + 1) {
roDisk = args[bundleArg + 1];
bundleDir = args[0];
[args removeObjectAtIndex:0];
if (args.count) {
roDisk = args[0];
[args removeObjectAtIndex:0];
}
if (args.count) {
usbDisk = args[0];
[args removeObjectAtIndex:0];
}
if (![bundleDir hasSuffix:@"/"]) {
@@ -77,7 +86,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
VZVirtualMachine *vm =
[MacOSVirtualMachineConfigurationHelper createVirtualMachineWithBundleDir:bundleDir
roDisk:roDisk];
roDisk:roDisk
usbDisk:usbDisk];
self->_virtualMachine = vm;
self->_delegate = [MacOSVirtualMachineDelegate new];

View File

@@ -3,5 +3,5 @@
# through applescript.
# It's run as part of the template VM creation process.
osascript -e 'tell application "System Preferences" to activate'
osascript -e 'tell application "System Events" to tell process "System Preferences" to click menu item "Profiles" of menu 1 of menu bar item "View" of menu bar 1'
osascript -e 'tell application "System Settings" to activate'
osascript -e 'tell application "System Events" to tell process "System Settings" to click menu item "Profiles" of menu 1 of menu bar item "View" of menu bar 1'

View File

@@ -105,9 +105,13 @@ if __name__ == "__main__":
print(f"Snapshot: {snapshot_dir}")
# COW copy the image to this tempdir
subprocess.check_output(["cp", "-rc", extracted_path, snapshot_dir])
# Create a disk image for USB testing
usb_dmg = pathlib.Path(snapshot_dir) / "usb.dmg"
subprocess.check_output(["hdiutil", "create", "-size", "100M",
"-fs", "ExFAT", "-volname", "USB", usb_dmg])
try:
subprocess.check_output(
[VMCLI, pathlib.Path(snapshot_dir) / extracted_path.name],
[VMCLI, pathlib.Path(snapshot_dir) / extracted_path.name, usb_dmg],
timeout=TIMEOUT,
)
except subprocess.TimeoutExpired:

View File

@@ -1,33 +1,32 @@
-- Allows the Santa system extension in System Preferences.
-- Allows the Santa system extension in System Settings.
-- This is run inside test VMs.
on run argv
if application "System Preferences" is running then
tell application "System Preferences" to quit
if application "System Settings" is running then
tell application "System Settings" to quit
end if
delay 2
tell application "System Events"
tell process "UserNotificationCenter"
click button "Open Security Preferences" of window 1
click button "Open System Settings" of window 1
end tell
delay 3
tell process "System Preferences"
click button "Click the lock to make changes." of window "Security & Privacy"
delay 1
set value of text field "Password" of sheet 1 of window "Security & Privacy" to system attribute "VM_PASSWORD"
click button "Unlock" of sheet 1 of window "Security & Privacy"
tell process "System Settings"
-- Click the "Allow" under "system software ... was blocked from loading"
click button 1 of group 5 of scroll area 1 of group 1 of group 2 of splitter group 1 of group 1 of window 1
delay 2
click button "Allow" of tab group 1 of window "Security & Privacy"
set value of text field 2 of sheet 1 of window 1 to system attribute "VM_PASSWORD"
click button 1 of sheet 1 of window 1
end tell
end tell
delay 2
tell application "System Preferences" to quit
tell application "System Settings" to quit
delay 2
end run

View File

@@ -0,0 +1,10 @@
-- Dismiss the "blocked execution" popup from Santa.
-- This is run inside test VMs.
on run argv
tell application "System Events"
tell process "Santa"
click button "Ignore" of window 1
end tell
end tell
end run

View File

@@ -0,0 +1,10 @@
-- Dismiss the "disk remounted" popup from Santa.
-- This is run inside test VMs.
on run argv
tell application "System Events"
tell process "Santa"
click button 1 of group 1 of window 1
end tell
end tell
end run

View File

@@ -1,28 +1,37 @@
-- Installs the passed profile (.mobileconfig).
-- This is run inside test VMs, primarily to configure Santa.
-- macOS 13+ only due to changes in system settings/preferences scripting.
on run argv
do shell script "open " & item 1 of argv
if application "System Preferences" is running then
tell application "System Preferences" to quit
end if
delay 2
tell application "System Preferences" to activate
tell application "System Settings" to activate
delay 2
tell application "System Events"
tell process "System Preferences"
tell process "System Settings"
click menu item "Profiles" of menu 1 of menu bar item "View" of menu bar 1
delay 3
click button "Install…" of scroll area 1 of window "Profiles"
-- Thanks SwiftUI.
-- Press the +
click button 1 of group 2 of scroll area 1 of group 1 of group 2 of splitter group 1 of group 1 of window 1
delay 2
click button "Install" of sheet 1 of window "Profiles"
-- Cmd+Shift+G to select file
keystroke "G" using {command down, shift down}
delay 2
-- Type in the profile we want, and return to exit the "go to" sheet
keystroke item 1 of argv
keystroke return
delay 2
-- Return to choose the file
keystroke return
delay 2
-- Are you sure? Press continue
click button 2 of group 1 of sheet 1 of window 1
delay 2
-- Press install
click button "Install" of sheet 1 of window 1
end tell
delay 2
delay 5
tell process "SecurityAgent"
set value of text field 2 of window 1 to system attribute "VM_PASSWORD"
click button 2 of window 1
@@ -31,7 +40,7 @@ on run argv
delay 5
tell application "System Preferences" to quit
tell application "System Settings" to quit
delay 2
end run

View File

@@ -25,6 +25,11 @@ if [[ "$(sudo santactl status --json | jq .daemon.block_usb)" != "false" ]]; the
exit 1
fi
# Wait for the UI to have come up
sleep 5
bazel run //Testing/integration:dismiss_santa_popup
# Now change moroz to use the changed config, enabling USB blocking and removing the badbinary block rule
killall moroz
/tmp/moroz -configs="$GITHUB_WORKSPACE/Testing/integration/configs/moroz_changed/global.toml" -use-tls=false &

55
Testing/integration/test_usb.sh Executable file
View File

@@ -0,0 +1,55 @@
#!/bin/bash
set -xe
bazel run //Testing/integration:install_profile -- Testing/integration/configs/default.mobileconfig
sudo diskutil unmount force USB || true
killall moroz
/tmp/moroz -configs="$GITHUB_WORKSPACE/Testing/integration/configs/moroz_default/global.toml" -use-tls=false &
sudo santactl sync --debug
if [[ "$(sudo santactl status --json | jq .daemon.block_usb)" != "false" ]]; then
echo "USB blocking enabled with minimal config" >&2
exit 1
fi
sudo diskutil mount USB
echo test > /Volumes/USB/test
sync
sudo diskutil unmount force USB
killall moroz
/tmp/moroz -configs="$GITHUB_WORKSPACE/Testing/integration/configs/moroz_changed/global.toml" -use-tls=false &
sudo santactl sync --debug
if [[ "$(sudo santactl status --json | jq .daemon.block_usb)" != "true" ]]; then
echo "USB blocking config change didnt take effect" >&2
exit 1
fi
set +e
sudo diskutil mount USB
blocked=$?
set -e
if [[ $blocked == 0 ]]; then
echo "R/W mount succeeded with USB blocking enabled" >&2
exit 1
fi
sleep 5
# Santa should have remounted the disk RO for us. Check that it did.
bazel run //Testing/integration:dismiss_usb_popup
cat /Volumes/USB/test
sudo diskutil unmount force USB
# Ensure things can still be normally mounted if mount flags match remount opts.
set +e
sudo diskutil mount -mountOptions ro,noexec USB
blocked=$?
set -e
if [[ $blocked != 0 ]]; then
echo "RO+noexec mount failed with USB blocking enabled" >&2
exit 1
fi

View File

@@ -10,6 +10,7 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
# Abseil LTS branch, Aug 2023
http_archive(
name = "com_google_absl",
sha256 = "59d2976af9d6ecf001a81a35749a6e551a335b949d34918cfade07737b9d93c5",
strip_prefix = "abseil-cpp-20230802.0",
urls = ["https://github.com/abseil/abseil-cpp/archive/refs/tags/20230802.0.tar.gz"],
)
@@ -62,9 +63,9 @@ apple_support_dependencies()
# https://github.com/hedronvision/bazel-compile-commands-extractor
git_repository(
name = "hedron_compile_commands",
commit = "92db741ee6dee0c4a83a5c58be7747df7b89ed10",
commit = "ac6411f8f347e5525038cb7858db4969db9e74f2",
remote = "https://github.com/hedronvision/bazel-compile-commands-extractor.git",
shallow_since = "1640416382 -0800",
shallow_since = "1696885905 +0000",
)
load("@hedron_compile_commands//:workspace_setup.bzl", "hedron_compile_commands_setup")
@@ -159,7 +160,7 @@ objc_library(
patch_args = ["-p1"],
patches = ["//external_patches/OCMock:503.patch"],
remote = "https://github.com/erikdoe/ocmock",
shallow_since = "1609349457 +0100",
shallow_since = "1635703064 +0100",
)
# Moroz (for testing)

View File

@@ -4,15 +4,16 @@ parent: Binaries
# santactl
This may be the most complex part of Santa. It does two types of work:
`santactl` is a command line utility for interacting with Santa. It provides the
following functionality:
1. It contains all of the code and functionality for syncing with a
sync-server.
2. It can be used to view the state and configuration of Santa as a whole. It
can also inspect individual files. When running without a sync server it
also a supported method of managing the rules database.
* Viewing Santa status and configuration
* Inspect individual files and see how Santa would apply policy
* Trigger an immediate sync operation
* View version information
* If a sync server isn't configured, can be used to manually manage rules
* Printing protobuf logs as JSON
The details of santactl's syncing functionality are covered in [Syncing Overview](../introduction/syncing-overview.md).
This document will cover the status work that santactl performs.
##### status

View File

@@ -69,13 +69,15 @@ also known as mobileconfig files, which are in an Apple-specific XML format.
| MetricExtraLabels | Dictionary | A map of key value pairs to add to all metric root labels. (e.g. a=b,c=d) defaults to @{}). If a previously set key (e.g. host_name is set to "" then the key is remove from the metric root labels. Alternatively if a value is set for an existing key then the new value will override the old. |
| EnableAllEventUpload | Bool | If YES, the client will upload all execution events to the sync server, including those that were explicitly allowed. |
| DisableUnknownEventUpload | Bool | If YES, the client will *not* upload events for executions of unknown binaries allowed in monitor mode |
| BlockUSBMount | Bool | If set to 'True' blocking USB Mass storage feature is enabled. Defaults to `False`. |
| RemountUSBMode | Array | Array of strings for arguments to pass to mount -o (any of "rdonly", "noexec", "nosuid", "nobrowse", "noowners", "nodev", "async", "-j"). when forcibly remounting devices. No default. |
| BlockUSBMount | Bool | If YES, blocking USB Mass storage feature is enabled. Defaults to NO. |
| RemountUSBMode | Array | Array of strings for arguments to pass to mount -o (any of "rdonly", "noexec", "nosuid", "nobrowse", "noowners", "nodev", "async", "-j") when forcibly remounting devices. No default. |
| OnStartUSBOptions | String | If set, defines the action that should be taken on existing USB mounts when Santa starts up. Supported values are "Unmount", "ForceUnmount", "Remount", and "ForceRemount" (note: "remounts" are implemented by first unmounting and then mounting the device again). Existing mounts with mount flags that are a superset of RemountUSBMode are unaffected and left mounted. |
| FileAccessPolicyPlist | String | Path to a file access configuration plist. This is ignored if `FileAccessPolicy` is also set. |
| FileAccessPolicy | Dictionary | A complete file access configuration policy embedded in the main Santa config. If set, `FileAccessPolicyPlist` will be ignored. |
| FileAccessPolicyUpdateIntervalSec | Integer | Number of seconds between re-reading the file access policy config and policies/monitored paths updated. |
| SyncClientContentEncoding | String | Sets the Content-Encoding header for requests sent to the sync service. Acceptable values are "deflate", "gzip", "none" (Defaults to deflate.) |
| SyncExtraHeaders | Dictionary | Dictionary of additional headers to include in all requests made to the sync server. System managed headers such as Content-Length, Host, WWW-Authenticate etc will be ignored. |
| EnableDebugLogging | Bool | If YES, the client will log additional debug messages to the Apple Unified Log. For example, transitive rule creation logs can be viewed with `log stream --predicate 'sender=="com.google.santa.daemon"'`. Defaults to NO. |
\*overridable by the sync server: run `santactl status` to check the current
@@ -223,9 +225,9 @@ ways to install configuration profiles:
| allowed\_path\_regex | String | Same as the "Local Configuration" AllowedPathRegex. No default. |
| blocked\_path\_regex | String | Same as the "Local Configuration" BlockedPathRegex. No default. |
| full\_sync\_interval\* | Integer | The max time to wait before performing a full sync with the server. Defaults to 600 secs (10 minutes) if not set. |
| fcm\_token\* | String | The FCM token used by Santa to listen for FCM messages. Unique for every machine. No default. |
| fcm\_full\_sync\_interval\* | Integer | The full sync interval if a fcm\_token is set. Defaults to 14400 secs (4 hours). |
| fcm\_global\_rule\_sync\_deadline\* | Integer | The max time to wait before performing a rule sync when a global rule sync FCM message is received. This allows syncing to be staggered for global events to avoid spikes in server load. Defaults to 600 secs (10 min). |
| fcm\_token\* | String | The FCM token used by Santa to listen for FCM messages. Unique for every machine. No default. |
| fcm\_full\_sync\_interval\* | Integer | The full sync interval if a fcm\_token is set. Defaults to 14400 secs (4 hours). |
| fcm\_global\_rule\_sync\_deadline\*| Integer | The max time to wait before performing a rule sync when a global rule sync FCM message is received. This allows syncing to be staggered for global events to avoid spikes in server load. Defaults to 600 secs (10 min). |
| enable\_bundles\* | Bool | If set to `True` the bundle scanning feature is enabled. Defaults to `False`. |
| enable\_transitive\_rules | Bool | If set to `True` the transitive rule feature is enabled. Defaults to `False`. |
| enable\_all\_event\_upload | Bool | If set to `True` the client will upload events for all executions, including those that are explicitly allowed. |
@@ -237,6 +239,7 @@ ways to install configuration profiles:
**Performed once per preflight run (if set).
†The Firebase Cloud Messaging (FCM) based Push Notification system is only available on the internal Google deployment of Santa at this time
## MDM-Specific Client Configuration

View File

@@ -30,6 +30,8 @@ To enable this feature, the `FileAccessPolicyPlist` key in the main [Santa confi
| `AllowReadAccess` | `Options` | Boolean | No | v2023.1+ | If true, indicates the rule will **not** be applied to actions that are read-only access (e.g., opening a watched path for reading, or cloning a watched path). If false, the rule will apply both to read-only access and access that could modify the watched path. (Default = `false`) |
| `AuditOnly` | `Options` | Boolean | No | v2023.1+ | If true, operations violating the rule will only be logged. If false, operations violating the rule will be denied and logged. (Default = `true`) |
| `InvertProcessExceptions` | `Options` | Boolean | No | v2023.5+ | If true, logic is inverted for the list of processes defined by the `Processes` key such that the list becomes the set of processes that will be denied or allowed but audited. (Default = `false`) |
| `EnableSilentMode` | `Options` | String | No | v2023.7+ | If true, Santa will not display a GUI dialog when this rule is violated. |
| `EnableSilentTTYMode` | `Options` | String | No | v2023.7+ | If true, Santa will not post a message to the controlling TTY when this rule is violated. |
| `EventDetailURL` | `Options` | String | No | v2023.8+ | Rule-specific URL that overrides the top-level `EventDetailURL`. |
| `EventDetailText` | `Options` | String | No | v2023.8+ | Rule-specific button text that overrides the top-level `EventDetailText`. |
| `Processes` | `<Name>` | Array | No | v2023.1+ | A list of dictionaries defining processes that are allowed to access paths matching the globs defined with the `Paths` key. For a process performing the operation to be considered a match, it must match all defined attributes of at least one entry in the list. |

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -5,7 +5,7 @@ nav_order: 1
# Welcome to the Santa documentation
Santa is a binary 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.
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.
It is named Santa because it keeps track of binaries that are naughty or nice.
@@ -15,7 +15,7 @@ The project and the latest release is available on [**GitHub**](https://github.c
* [**Multiple modes:**](concepts/mode.md) In the default `MONITOR` mode, all binaries except those marked as blocked will be allowed to run, whilst being logged and recorded in the events database. In `LOCKDOWN` mode, only listed binaries are allowed to run.
* [**Event logging:**](concepts/events.md) All binary launches are logged. When in either mode, all unknown or denied binaries are stored in the database to enable later aggregation.
* [**Certificate-based rules, with override levels:**](concepts/rules.md) Instead of relying on a binary's hash (or 'fingerprint'), executables can be allowed/blocked by their signing certificate. You can therefore allow/block all binaries by a given publisher that were signed with that cert across version updates. A binary can only be allowed by its certificate if its signature validates correctly but a rule for a binary's fingerprint will override a decision for a certificate; i.e. you can allowlist a certificate while blocking a binary signed with that certificate, or vice-versa.
* [**Several supported rule types:**](concepts/rules.md) Executions can be allowed or denied by specifying rules based on several attributes. The supported rule types, in order of highest to lowest precedence are: binary hash, Signing ID, certificate hash, or Team ID. Since multiple rules can apply to a given binary, Santa will apply the rule with the highest precedence (i.e. you could use a Team ID rule to allow all binaries from some organization, but also add a Signing ID rule to deny a specific binary). Rules based on code signature properties (Signing ID, certificate hash, and Team ID) only apply if a bianry's signature validates correctly.
* **Path-based rules (via NSRegularExpression/ICU):** Binaries can be allowed/blocked based on the path they are launched from by matching against a configurable regex.
* [**Failsafe cert rules:**](concepts/rules.md#built-in-rules) You cannot put in a deny rule that would block the certificate used to sign launchd, a.k.a. pid 1, and therefore all components used in macOS. The binaries in every OS update (and in some cases entire new versions) are therefore automatically allowed. This does not affect binaries from Apple's App Store, which use various certs that change regularly for common apps. Likewise, you cannot block Santa itself.
* [**Components validate each other:**](binaries/index.md) Each of the components (the daemons, the GUI agent, and the command-line utility) communicate with each other using XPC and check that their signing certificates are identical before any communication is accepted.

View File

@@ -16,6 +16,10 @@ macOS systems. This document explains the syncing process.
#### Flow of a full sync
NOTE: Synchronization is now performed by its own agent, the `santasyncservice`.
The phases of synchronization described below still apply, but references to how
the process starts is outdated. This will be updated soon.
This is a high level overview of the syncing process. For a more a more detailed
account of each part, see the respective documentation. The santaclt binary can
be run in one of two modes, daemon and non-daemon. The non-daemon mode does one
@@ -44,7 +48,7 @@ syncs, listen for push notifications and upload events.
this will be 10min. However there are a few ways to manipulate this:
1. The sync server can send down a configuration in the preflight to
override the 10min interval. It can be anything greater than 10min.
2. Firebase Cloud Messaging (FCM) can be used. The sync server can send
2. Firebase Cloud Messaging (FCM) can be used*. The sync server can send
down a configuration in the preflight to have the santactl daemon to
start listening for FCM messages. If a connection to FCM is made, the
full sync interval drops to a default of 4 hours. This can be further
@@ -54,6 +58,8 @@ syncs, listen for push notifications and upload events.
5. Full syncs will continue to take place at their configured interval. If
configured FCM messages will continue to be digested and acted upon.
*The Firebase Cloud Messaging (FCM) based Push Notification system is only available on the internal Google deployment of Santa at this time
#### santactl XPC interface
When running as a daemon, the santactl process makes available an XPC interface