mirror of
https://github.com/google/santa.git
synced 2026-01-15 01:08:12 -05:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f5f8de245 | ||
|
|
7c58648c35 | ||
|
|
3f3751eb18 | ||
|
|
7aa2d69ce6 | ||
|
|
f9a937a6e4 | ||
|
|
d2cbddd3fb | ||
|
|
ea7e11fc22 | ||
|
|
7530b8f5c1 | ||
|
|
64bb34b2ca | ||
|
|
c5c6037085 | ||
|
|
275a8ed607 | ||
|
|
28dd6cbaed | ||
|
|
8c466b4408 | ||
|
|
373c676306 | ||
|
|
d214d510e5 | ||
|
|
6314fe04e3 | ||
|
|
11d9c29daa | ||
|
|
60238f0ed2 | ||
|
|
7aa731a76f |
17
.github/workflows/e2e.yml
vendored
17
.github/workflows/e2e.yml
vendored
@@ -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
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
[](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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ REGISTER_COMMAND_NAME(@"sync")
|
||||
#pragma mark SNTCommand protocol methods
|
||||
|
||||
+ (BOOL)requiresRoot {
|
||||
return YES;
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)requiresDaemonConn {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
35
Source/santad/testdata/protobuf/v1/cs_invalidated.json
vendored
Normal file
35
Source/santad/testdata/protobuf/v1/cs_invalidated.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Source/santad/testdata/protobuf/v2/cs_invalidated.json
vendored
Normal file
35
Source/santad/testdata/protobuf/v2/cs_invalidated.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Source/santad/testdata/protobuf/v4/cs_invalidated.json
vendored
Normal file
35
Source/santad/testdata/protobuf/v4/cs_invalidated.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Source/santad/testdata/protobuf/v5/cs_invalidated.json
vendored
Normal file
35
Source/santad/testdata/protobuf/v5/cs_invalidated.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Source/santad/testdata/protobuf/v6/cs_invalidated.json
vendored
Normal file
35
Source/santad/testdata/protobuf/v6/cs_invalidated.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
10
Testing/integration/dismiss_santa_popup.scpt
Normal file
10
Testing/integration/dismiss_santa_popup.scpt
Normal 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
|
||||
10
Testing/integration/dismiss_usb_popup.scpt
Normal file
10
Testing/integration/dismiss_usb_popup.scpt
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
55
Testing/integration/test_usb.sh
Executable 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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. |
|
||||
|
||||
BIN
docs/images/santa-sleigh-256.png
Normal file
BIN
docs/images/santa-sleigh-256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user